Футбол: Почему победители побеждают, а неудачники проигрывают

Смотрим 5 лет европейского футбола

Вступление

В этой статье мы изучим современные показатели в футболе (xG, xGA и xPTS) и их влияние в спортивной аналитике.

  • Ожидаемые голы (xG) – измеряет качество удара на основе нескольких переменных, таких как тип ассиста, угол удара и расстояние до ворот, был ли это удар головой и определялся ли он как большой шанс.
  • Ожидаемые голевые передачи (xGA) – измеряет вероятность того, что данный пас станет голевым. Он учитывает несколько факторов, включая тип паса, конечную точку паса и его длину.
  • Ожидаемые очки (xPTS) – измеряет вероятность определенной игры принести очки команде.

Эти показатели позволяют нам углубиться в футбольную статистику и понять результаты работы игроков и команд в целом и осознать роль удачи и мастерства в ней. Спойлер: они оба важны.

Процесс сбора данных для этой статьи описан в этом Kaggle kernel: Web Scraping Football Statistics

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import collections
import warnings

from IPython.core.display import display, HTML

# import plotly 
import plotly
import plotly.figure_factory as ff
import plotly.graph_objs as go
import plotly.offline as py
from plotly.offline import iplot, init_notebook_mode
import plotly.tools as tls

# configure things
warnings.filterwarnings('ignore')

pd.options.display.float_format = '{:,.2f}'.format  
pd.options.display.max_columns = 999

py.init_notebook_mode(connected=True)

%load_ext autoreload
%autoreload 2

%matplotlib inline
sns.set()

# !pip install plotly --upgrade

Импорт данных и визуальный анализ

df = pd.read_csv('../input/understat.com.csv')
df = df.rename(index=int, columns={'Unnamed: 0': 'league', 'Unnamed: 1': 'year'}) 
df.head()

В следующей визуализации мы проверим, сколько команд из каждой лиги были в топ-4 за последние 5 лет. Это может дать нам некоторую информацию о стабильности лучших команд из разных стран.

f = plt.figure(figsize=(25,12))
ax = f.add_subplot(2,3,1)
plt.xticks(rotation=45)
sns.barplot(x='team', y='pts', hue='year', data=df[(df['league'] == 'Bundesliga') & (df['position'] <= 4)], ax=ax)
ax = f.add_subplot(2,3,2)
plt.xticks(rotation=45)
sns.barplot(x='team', y='pts', hue='year', data=df[(df['league'] == 'EPL') & (df['position'] <= 4)], ax=ax)
ax = f.add_subplot(2,3,3)
plt.xticks(rotation=45)
sns.barplot(x='team', y='pts', hue='year', data=df[(df['league'] == 'La_liga') & (df['position'] <= 4)], ax=ax)
ax = f.add_subplot(2,3,4)
plt.xticks(rotation=45)
sns.barplot(x='team', y='pts', hue='year', data=df[(df['league'] == 'Serie_A') & (df['position'] <= 4)], ax=ax)
ax = f.add_subplot(2,3,5)
plt.xticks(rotation=45)
sns.barplot(x='team', y='pts', hue='year', data=df[(df['league'] == 'Ligue_1') & (df['position'] <= 4)], ax=ax)
ax = f.add_subplot(2,3,6)
plt.xticks(rotation=45)
sns.barplot(x='team', y='pts', hue='year', data=df[(df['league'] == 'RFPL') & (df['position'] <= 4)], ax=ax)

Как мы видим из этих гистограмм, есть команды, которые за последние 5 лет были в топ-4 только один раз, это означает, что это не является нормой для них, это означает, что если мы копнем глубже, мы можем обнаружить, что есть фактор удачи, который возможно, сыграл в пользу этим командам. Это просто теория, поэтому давайте присмотримся ближе к этим аномалиям.

Команды, которые попали в топ-4 только один раз в течение последних 5 сезонов:

  • Вольфсбург (2014) и Шальке 04 (2017) с Бундеслиги
  • Лестер (2015) с Английской Премьер Лиги
  • Вильяреал (2015) и Севилья (2016) с Испанской Ла Лиги
  • Лацио (2014) и Фиорентина (2014) с Итальянской Серии А
  • Лиль (2018) и Сент-Этьен (2018) с Французской Лиги 1
  • ФК Ростов (2015) и Динамо Москва (2014) из Российской Премьер Лиги

Давайте сохраним эти команды.

# Removing unnecessary for our analysis columns 
df_xg = df[['league', 'year', 'position', 'team', 'scored', 'xG', 'xG_diff', 'missed', 'xGA', 'xGA_diff', 'pts', 'xpts', 'xpts_diff']]

outlier_teams = ['Wolfsburg', 'Schalke 04', 'Leicester', 'Villareal', 'Sevilla', 'Lazio', 'Fiorentina', 'Lille', 'Saint-Etienne', 'FC Rostov', 'Dinamo Moscow']
# Checking if getting the first place requires fenomenal execution
first_place = df_xg[df_xg['position'] == 1]

# Get list of leagues
leagues = df['league'].drop_duplicates()
leagues = leagues.tolist()

# Get list of years
years = df['year'].drop_duplicates()
years = years.tolist()

Понять как победители побеждают

В этом разделе мы попытаемся найти некоторые шаблоны, которые могут помочь нам понять, каковы ингредиенты супа победы :D. Начнем с Бундеслиги.

Бундеслига

Данные в таблицах и заголовки в чартах на английском, потому что первоисточник сделано на этом языке и менять все подряд заняло бы слишком много времени. Извиняюсь 🙁

first_place[first_place['league'] == 'Bundesliga']
pts = go.Bar(x = years, y = first_place['pts'][first_place['league'] == 'Bundesliga'], name = 'PTS')
xpts = go.Bar(x = years, y = first_place['xpts'][first_place['league'] == 'Bundesliga'], name = 'Expected PTS')

data = [pts, xpts]

layout = go.Layout(
    barmode='group',
    title="Comparing Actual and Expected Points for Winner Team in Bundesliga",
    xaxis={'title': 'Year'},
    yaxis={'title': "Points",
    }
)

fig = go.Figure(data=data, layout=layout)
py.iplot(fig)

Посмотрев на таблицу и диаграмму, мы видим, что Бавария с каждым годом получает больше очков, чем они должны были получить, они забили больше голов, чем ожидаемо, и пропустили меньше, чем ожидалось (за исключением 2018 года, который все равно не нарушил их плана выиграть сезон, но это дает некоторые подсказки, Бавария сыграла хуже в том году, хотя и конкуренты не воспользовались этим).

# and from this table we see that Bayern dominates here totally, even when they do not play well
df_xg[(df_xg['position'] <= 2) & (df_xg['league'] == 'Bundesliga')].sort_values(by=['year','xpts'], ascending=False)

Ла Лига

first_place[first_place['league'] == 'La_liga']
pts = go.Bar(x = years, y = first_place['pts'][first_place['league'] == 'La_liga'], name = 'PTS')
xpts = go.Bar(x = years, y = first_place['xpts'][first_place['league'] == 'La_liga'], name = 'Expected PTS')

data = [pts, xpts]

layout = go.Layout(
    barmode='group',
    title="Comparing Actual and Expected Points for Winner Team in La Liga",
    xaxis={'title': 'Year'},
    yaxis={'title': "Points",
    }
)

fig = go.Figure(data=data, layout=layout)
py.iplot(fig)

Как мы видим из диаграммы выше, что в 2014 и 2015 годах Барселона создавала достаточно моментов, чтобы выиграть титул и не полагаться на личные навыки или удачу, из этих цифр на самом деле можно сказать, что настоящая КОМАНДА играла в те сезоны.

В 2016 году между Мадридом и Барселоной было много конкуренции, и в конце концов Мадриду повезло больше или королевский клуб имел яйца побольше в одной конкретной игре (или Барселоне не повезло / не показали яйца), и это стало ценой титула. Я уверен, что если копнуть глубже этот сезон, мы сможем найти именно этот поединок.

В 2017 и 2018 годах успех "Барселоны" в основном заслуга действиям Лионеля Месси, который забивал или делал голевые пасы в ситуациях, когда обычные игроки этого бы не сделали. Что и привело к такому скачку в разнице xPTS. И это заставляет меня думать (имея в виду, что "Реал" в этом сезоне очень активен на трансферном рынке), что все может закончиться плохо. Просто субъективное мнение, основанное на числах и просмотре матчей Барселоны. Очень надеюсь, что я ошибаюсь.

# comparing with runner-up
df_xg[(df_xg['position'] <= 2) & (df_xg['league'] == 'La_liga')].sort_values(by=['year','xpts'], ascending=False)

АПЛ

first_place[first_place['league'] == 'EPL']
pts = go.Bar(x = years, y = first_place['pts'][first_place['league'] == 'EPL'], name = 'PTS')
xpts = go.Bar(x = years, y = first_place['xpts'][first_place['league'] == 'EPL'], name = 'Expected PTS')

data = [pts, xpts]

layout = go.Layout(
    barmode='group',
    title="Comparing Actual and Expected Points for Winner Team in EPL",
    xaxis={'title': 'Year'},
    yaxis={'title': "Points",
    }
)

fig = go.Figure(data=data, layout=layout)
py.iplot(fig)

В АПЛ мы видим четкую тенденцию, которая говорит вам: "Чтобы победить, ты должен быть лучшим, чем статистика". Интересным случаем является история Лестера при победе в 2015 году: они получили на 12 очков больше, чем должны были, и в то же время Арсенал недополучил 6 очков! Вот почему мы любим футбол, потому что бывают такие непонятные вещи. Я не говорю, что это полностью удача, но она сыграла здесь значительную роль.

Еще одна интересная вещь - Манчестер Сити 2018 - они супер стабильные! Они забили лишь на один гол больше, чем ожидалось, пропустили на 2 меньше и получили 7 дополнительных очков, в то время как Ливерпуль боролся очень хорошо, имел чуть больше удачи на своей стороне, но не смог победить, несмотря на 13 очков опережения по xPTS .

Пеп заканчивает строительство машины уничтожения. Man City создает и преобразует свои моменты на основе мастерства и не полагается на удачу - это делает их очень опасными в следующем сезоне.

# comparing with runner-ups
df_xg[(df_xg['position'] <= 2) & (df_xg['league'] == 'EPL')].sort_values(by=['year','xpts'], ascending=False)

Лига 1

first_place[first_place['league'] == 'Ligue_1']
pts = go.Bar(x = years, y = first_place['pts'][first_place['league'] == 'Ligue_1'], name = 'PTS')
xpts = go.Bar(x = years, y = first_place['xpts'][first_place['league'] == 'Ligue_1'], name = 'Expected PTS')

data = [pts, xpts]

layout = go.Layout(
    barmode='group',
    title="Comparing Actual and Expected Points for Winner Team in Ligue 1",
    xaxis={'title': 'Year'},
    yaxis={'title': "Points",
    }
)

fig = go.Figure(data=data, layout=layout)
py.iplot(fig)

Во французской Ligue 1 мы продолжаем наблюдать тенденцию: "Чтобы выиграть, ты должен выполнить 110%, так как 100% недостаточно". Здесь Пари Сен-Жермен полностью доминирует. Только в 2016 году мы получаем исключение в лице Монако, который забил на 30 голов больше, чем ожидалось!!! и получил почти на 17 баллов больше, чем ожидалось! Удача? Достаточно хороший кусок. ПСЖ был хорош в том году, но Монако был чрезвычайным. Опять же, мы не можем утверждать, что это чистая удача или чистое мастерство, а это идеальное сочетание обоих в нужном месте в нужное время.

# comparing with runner-ups
df_xg[(df_xg['position'] <= 2) & (df_xg['league'] == 'Ligue_1')].sort_values(by=['year','xpts'], ascending=False)

Серия А

first_place[first_place['league'] == 'Serie_A']
pts = go.Bar(x = years, y = first_place['pts'][first_place['league'] == 'Serie_A'], name = 'PTS')
xpts = go.Bar(x = years, y = first_place['xpts'][first_place['league'] == 'Serie_A'], name = 'Expecetd PTS')

data = [pts, xpts]

layout = go.Layout(
    barmode='group',
    title="Comparing Actual and Expected Points for Winner Team in Serie A",
    xaxis={'title': 'Year'},
    yaxis={'title': "Points",
    }
)

fig = go.Figure(data=data, layout=layout)
py.iplot(fig)

В итальянской Серии А "Ювентус" доминирует 8 лет подряд, хотя не может достичь больших успехов в Лиге чемпионов. Я думаю, взглянув на эту диаграмму и цифры, мы можем понять, что Юве не имеет достаточно сильной конкуренции внутри страны и получает много "халявных" очков, что опять же следует из многих факторов, и мы можем увидеть, что Наполи два раза опережал "Ювентус" по показателю xPTS, но это настоящая жизнь, и, например, 2017 года, Юве был сумасшедшим и забил дополнительные 26 голов (или создал голы из ниоткуда), а Наполи пропустил на 3 больше, чем ожидалось (из-за ошибки вратаря или, возможно, поражения какой-то команде в 1 или 2 конкретных спаррингах). Как и в ситуации в Ла Лиге, когда Реал Мадрид стал чемпионом, я уверен, что мы сможем найти 1 или 2 игры, которые были ключевыми в том году.

Детали имеют значение в футболе. Понимаете, одна ошибка здесь, одна штанга там, и вы потеряли звание чемпиона страны.

# comparing to runner-ups
df_xg[(df_xg['position'] <= 2) & (df_xg['league'] == 'Serie_A')].sort_values(by=['year','xpts'], ascending=False)

РФПЛ

first_place[first_place['league'] == 'RFPL']
pts = go.Bar(x = years, y = first_place['pts'][first_place['league'] == 'RFPL'], name = 'PTS')
xpts = go.Bar(x = years, y = first_place['xpts'][first_place['league'] == 'RFPL'], name = 'Expected PTS')

data = [pts, xpts]

layout = go.Layout(
    barmode='group',
    title="Comparing Actual and Expected Points for Winner Team in RFPL",
    xaxis={'title': 'Year'},
    yaxis={'title': "Points",
    }
)

fig = go.Figure(data=data, layout=layout)
py.iplot(fig)

Я не слежу за российской Премьер-лигой, поэтому просто холодно глядя на данные, мы видим ту же схему, - забивать больше, чем ты заслуживаешь, а также интересная ситуация с московским ЦСКА с 2015 по 2017 год. За эти годы эти ребята были хорошие, но конвертировали свои преимущества только один раз, остальные два - если вы не конвертируете, вас наказывают, или ваш главный конкурент просто забивает больше.

В футболе нет справедливости :D. Хотя, я считаю, что с VAR цифры станут стабильными в последующие сезоны. Потому что одной из причин этих дополнительных голов и очков есть ошибки арбитров.

# comparing to runner-ups
df_xg[(df_xg['position'] <= 2) & (df_xg['league'] == 'RFPL')].sort_values(by=['year','xpts'], ascending=False)

Статистический обзор

Поскольку есть 6 лиг с различными командами, я решил сосредоточиться на одной для начала, чтобы проверить различные подходы, а затем повторить окончательную модель анализа на другие 5. И, поскольку я смотрю преимущественно Ла Лигу, я начну с этого соревнования, так как я знаю больше о нем.

# Creating separate DataFrames per each league
laliga = df_xg[df_xg['league'] == 'La_liga']
laliga.reset_index(inplace=True)
epl = df_xg[df_xg['league'] == 'EPL']
epl.reset_index(inplace=True)
bundesliga = df_xg[df_xg['league'] == 'Bundesliga']
bundesliga.reset_index(inplace=True)
seriea = df_xg[df_xg['league'] == 'Serie_A']
seriea.reset_index(inplace=True)
ligue1 = df_xg[df_xg['league'] == 'Ligue_1']
ligue1.reset_index(inplace=True)
rfpl = df_xg[df_xg['league'] == 'RFPL']
rfpl.reset_index(inplace=True)
laliga.describe()
def print_records_antirecords(df):
  print('Presenting some records and antirecords: n')
  for col in df.describe().columns:
    if col not in ['index', 'year', 'position']:
      team_min = df['team'].loc[df[col] == df.describe().loc['min',col]].values[0]
      year_min = df['year'].loc[df[col] == df.describe().loc['min',col]].values[0]
      team_max = df['team'].loc[df[col] == df.describe().loc['max',col]].values[0]
      year_max = df['year'].loc[df[col] == df.describe().loc['max',col]].values[0]
      val_min = df.describe().loc['min',col]
      val_max = df.describe().loc['max',col]
      print('The lowest value of {0} had {1} in {2} and it is equal to {3:.2f}'.format(col.upper(), team_min, year_min, val_min))
      print('The highest value of {0} had {1} in {2} and it is equal to {3:.2f}'.format(col.upper(), team_max, year_max, val_max))
      print('='*100)
      
# replace laliga with any league you want
print_records_antirecords(laliga)
Представление некоторых рекордов и антирекордов:
  Наименьшее значение  SCORED в Кордобы в 2014 году было 22,00
  Наибольшее значение  SCORED имел Реал Мадрид в 2014 году и он равен 118,00
  ====================================================================
  Наименьшее значение XG было в Эйбар в 2014 году и оно равно 29,56
  Наибольшее значение XG имела Барселона в 2015 году и оно равно 113,60
  ====================================================================
  Барселона в 2016 году имела наименьшее значение XG_DIFF и оно равно -22,45
  Наибольшее значение XG_DIFF имел Лас-Пальмас в 2017 году и оно равно 13,88
  ====================================================================
  Самый низкий показатель MISSED имел Atletico Madrid в 2015 году и он равен 18.00
  Наибольшее значение MISSED имела Осасуна в 2016 году и оно равно 94,00
  ====================================================================
  Наименьшее значение XGA было в Atletico Madrid в 2015 году и оно равно 27,80
  Наибольшее значение XGA имел Levante в 2018 году и оно равно 78,86
  ====================================================================
  Низким значением XGA_DIFF была Осасуна в 2016 году и она равна -29,18
  Наибольшее значение XGA_DIFF имела Валенсия в 2015 году и оно равно 13,69
  ====================================================================
  Наименьшее значение PTS было в Кордобы в 2014 году и оно равно 20,00
  Наибольшее значение PTS в Барселоне было в 2014 году и оно равно 94,00
  ====================================================================
  Низкое значение XPTS было в Гранаде в 2016 году и оно равно 26,50
  Наибольшее значение XPTS имели Барселона в 2015 году и оно равно 94,38
  ====================================================================
  Самый низкий показатель XPTS_DIFF имел Atletico Madrid в 2017 году и он равен -17,40
  Наибольшее значение XPTS_DIFF имели Deportivo La Coruna в 2017 году и оно равно 20,16
trace0 = go.Scatter(
    x = laliga['position'][laliga['year'] == 2014], 
    y = laliga['xG_diff'][laliga['year'] == 2014],
    name = '2014',
    mode = 'lines+markers'
)

trace1 = go.Scatter(
    x = laliga['position'][laliga['year'] == 2015], 
    y = laliga['xG_diff'][laliga['year'] == 2015],
    name='2015',
    mode = 'lines+markers'
)

trace2 = go.Scatter(
    x = laliga['position'][laliga['year'] == 2016], 
    y = laliga['xG_diff'][laliga['year'] == 2016],
    name='2016',
    mode = 'lines+markers'
)

trace3 = go.Scatter(
    x = laliga['position'][laliga['year'] == 2017], 
    y = laliga['xG_diff'][laliga['year'] == 2017],
    name='2017',
    mode = 'lines+markers'
)

trace4 = go.Scatter(
    x = laliga['position'][laliga['year'] == 2018], 
    y = laliga['xG_diff'][laliga['year'] == 2018],
    name='2018',
    mode = 'lines+markers'
)

data = [trace0, trace1, trace2, trace3, trace4]

layout = go.Layout(
    title="Comparing xG gap between positions",
    xaxis={'title': 'Year'},
    yaxis={'title': "xG difference",
    }
)

fig = go.Figure(data=data, layout=layout)
py.iplot(fig)
trace0 = go.Scatter(
    x = laliga['position'][laliga['year'] == 2014], 
    y = laliga['xGA_diff'][laliga['year'] == 2014],
    name = '2014',
    mode = 'lines+markers'
)

trace1 = go.Scatter(
    x = laliga['position'][laliga['year'] == 2015], 
    y = laliga['xGA_diff'][laliga['year'] == 2015],
    name='2015',
    mode = 'lines+markers'
)

trace2 = go.Scatter(
    x = laliga['position'][laliga['year'] == 2016], 
    y = laliga['xGA_diff'][laliga['year'] == 2016],
    name='2016',
    mode = 'lines+markers'
)

trace3 = go.Scatter(
    x = laliga['position'][laliga['year'] == 2017], 
    y = laliga['xGA_diff'][laliga['year'] == 2017],
    name='2017',
    mode = 'lines+markers'
)

trace4 = go.Scatter(
    x = laliga['position'][laliga['year'] == 2018], 
    y = laliga['xGA_diff'][laliga['year'] == 2018],
    name='2018',
    mode = 'lines+markers'
)

data = [trace0, trace1, trace2, trace3, trace4]

layout = go.Layout(
    title="Comparing xGA gap between positions",
    xaxis={'title': 'Year'},
    yaxis={'title': "xGA difference",
    }
)

fig = go.Figure(data=data, layout=layout)
py.iplot(fig)
trace0 = go.Scatter(
    x = laliga['position'][laliga['year'] == 2014], 
    y = laliga['xpts_diff'][laliga['year'] == 2014],
    name = '2014',
    mode = 'lines+markers'
)

trace1 = go.Scatter(
    x = laliga['position'][laliga['year'] == 2015], 
    y = laliga['xpts_diff'][laliga['year'] == 2015],
    name='2015',
    mode = 'lines+markers'
)

trace2 = go.Scatter(
    x = laliga['position'][laliga['year'] == 2016], 
    y = laliga['xpts_diff'][laliga['year'] == 2016],
    name='2016',
    mode = 'lines+markers'
)

trace3 = go.Scatter(
    x = laliga['position'][laliga['year'] == 2017], 
    y = laliga['xpts_diff'][laliga['year'] == 2017],
    name='2017',
    mode = 'lines+markers'
)

trace4 = go.Scatter(
    x = laliga['position'][laliga['year'] == 2018], 
    y = laliga['xpts_diff'][laliga['year'] == 2018],
    name='2018',
    mode = 'lines+markers'
)

data = [trace0, trace1, trace2, trace3, trace4]

layout = go.Layout(
    title="Comparing xPTS gap between positions",
    xaxis={'title': 'Position'},
    yaxis={'title': "xPTS difference",
    }
)

fig = go.Figure(data=data, layout=layout)
py.iplot(fig)

Из приведенных выше диаграмм видно, что лучшие команды забивают больше, пропускают меньше и получают больше очков, чем ожидалось. Поэтому эти команды - топ-команды. И совсем противоположная ситуация с аутсайдерами. Команды среднего уровня средние. Вполне логично, здесь нет невероятных находок.

# Check mean differences
def get_diff_means(df):  
  dm = df.groupby('year')[['xG_diff', 'xGA_diff', 'xpts_diff']].mean()
  
  return dm

means = get_diff_means(laliga)
means
# Check median differences
def get_diff_medians(df):  
  dm = df.groupby('year')[['xG_diff', 'xGA_diff', 'xpts_diff']].median()
  
  return dm

medians = get_diff_medians(laliga)
medians

Выявление исключений

Z-Score

Z-Score - количество стандартных отклонений от средней точки данных. Мы можем использовать его для поиска других исключений в нашем наборе данных, считая, что | z-score | > 3 - исключение.

# Getting outliers for xG using zscore
from scipy.stats import zscore
# laliga[(np.abs(zscore(laliga[['xG_diff']])) > 2.0).all(axis=1)]
df_xg[(np.abs(zscore(df_xg[['xG_diff']])) > 3.0).all(axis=1)]
# outliers for xGA
# laliga[(np.abs(zscore(laliga[['xGA_diff']])) > 2.0).all(axis=1)]
df_xg[(np.abs(zscore(df_xg[['xGA_diff']])) > 3.0).all(axis=1)]
# Outliers for xPTS
# laliga[(np.abs(zscore(laliga[['xpts_diff']])) > 2.0).all(axis=1)]
df_xg[(np.abs(zscore(df_xg[['xpts_diff']])) > 3.0).all(axis=1)]

Всего выявлено 12 исключений. Бедная Осасуна в 2016 году - почти 30 незаслуженных голов.

Как мы видим из этих данных, пребывание в пространстве исключений, еще не делает вас победителем сезона. Но если вы теряете свои возможности или пропускаете гола там, где не следует, и делаете это слишком часто - вы заслуживаете на вылет. Проигрывать и быть средним гораздо проще, чем выигрывать.

Межквартильный диапазон (IQR)

IQR - разница между первым кварталом и третьим кварталом набора данных. Это один из способов описать распространение набора данных.

Общеупотребительное правило гласит, что точка данных является исключением, если она более 1,5 ⋅ IQR третьего квартала или на столько же ниже первого квартала. Иными словами, низкие исключения есть ниже Q1 - 1,5 ⋅ IQR, а высокие - более Q3 + 1,5 ⋅ IQR.

Давайте проверим это.

# Trying different method of outliers detection
df_xg.describe()
# using Interquartile Range Method to identify outliers
# xG_diff
iqr_xG = (df_xg.describe().loc['75%','xG_diff'] - df_xg.describe().loc['25%','xG_diff']) * 1.5
upper_xG = df_xg.describe().loc['75%','xG_diff'] + iqr_xG
lower_xG = df_xg.describe().loc['25%','xG_diff'] - iqr_xG

print('IQR for xG_diff: {:.2f}'.format(iqr_xG))
print('Upper border for xG_diff: {:.2f}'.format(upper_xG))
print('Lower border for xG_diff: {:.2f}'.format(lower_xG))

outliers_xG = df_xg[(df_xg['xG_diff'] > upper_xG) | (df_xg['xG_diff'] < lower_xG)]
print('='*50)

# xGA_diff
iqr_xGA = (df_xg.describe().loc['75%','xGA_diff'] - df_xg.describe().loc['25%','xGA_diff']) * 1.5
upper_xGA = df_xg.describe().loc['75%','xGA_diff'] + iqr_xGA
lower_xGA = df_xg.describe().loc['25%','xGA_diff'] - iqr_xGA

print('IQR for xGA_diff: {:.2f}'.format(iqr_xGA))
print('Upper border for xGA_diff: {:.2f}'.format(upper_xGA))
print('Lower border for xGA_diff: {:.2f}'.format(lower_xGA))

outliers_xGA = df_xg[(df_xg['xGA_diff'] > upper_xGA) | (df_xg['xGA_diff'] < lower_xGA)]
print('='*50)

# xpts_diff
iqr_xpts = (df_xg.describe().loc['75%','xpts_diff'] - df_xg.describe().loc['25%','xpts_diff']) * 1.5
upper_xpts = df_xg.describe().loc['75%','xpts_diff'] + iqr_xpts
lower_xpts = df_xg.describe().loc['25%','xpts_diff'] - iqr_xpts

print('IQR for xPTS_diff: {:.2f}'.format(iqr_xpts))
print('Upper border for xPTS_diff: {:.2f}'.format(upper_xpts))
print('Lower border for xPTS_diff: {:.2f}'.format(lower_xpts))

outliers_xpts = df_xg[(df_xg['xpts_diff'] > upper_xpts) | (df_xg['xpts_diff'] < lower_xpts)]
print('='*50)

outliers_full = pd.concat([outliers_xG, outliers_xGA, outliers_xpts])
outliers_full = outliers_full.drop_duplicates()
IQR для xG_diff: 13.16
Верхняя граница для xG_diff: 16.65
Нижняя граница для  xG_diff: -18.43
==================================================
IQR для xGA_diff: 13.95
Верхняя граница для  xGA_diff: 17.15
Нижняя граница для  xGA_diff: -20.05
==================================================
IQR для xPTS_diff: 13.93
Верхняя граница для  xPTS_diff: 18.73
Нижняя граница для  xPTS_diff: -18.41
==================================================
# Adding ratings bottom to up to find looser in each league (different amount of teams in every league so I can't do just n-20)
max_position = df_xg.groupby('league')['position'].max()
df_xg['position_reverse'] = np.nan
outliers_full['position_reverse'] = np.nan

for i, row in df_xg.iterrows():
  df_xg.at[i, 'position_reverse'] = np.abs(row['position'] - max_position[row['league']])+1
  
for i, row in outliers_full.iterrows():
  outliers_full.at[i, 'position_reverse'] = np.abs(row['position'] - max_position[row['league']])+1
total_count = df_xg[(df_xg['position'] <= 4) | (df_xg['position_reverse'] <= 3)].count()[0]
outlier_count = outliers_full[(outliers_full['position'] <= 4) | (outliers_full['position_reverse'] <= 3)].count()[0]
outlier_prob = outlier_count / total_count
print('Probability of outlier in top or bottom of the final table: {:.2%}'.format(outlier_prob))
Вероятность исключения в верхней или нижней части финальной таблицы: 8.10%

Поэтому можно сказать, что очень вероятно, что ежегодно в одной из 6 лиг будет команда, которая получает путевку в Лигу чемпионов или Лиги Европы с помощью удачи, кроме своих замечательных навыков, или есть неудачник, которая падает во второй дивизион, поскольку они не могут конвертировать свои моменты.

# 1-3 outliers among all leagues in a year
data = pd.DataFrame(outliers_full.groupby('league')['year'].count()).reset_index()
data = data.rename(index=int, columns={'year': 'outliers'})
sns.barplot(x='league', y='outliers', data=data)
# no outliers in Bundesliga

Наши победители и побежденные с блестящими данными и не слишком.

top_bottom = outliers_full[(outliers_full['position'] <= 4) | (outliers_full['position_reverse'] <= 3)].sort_values(by='league')
top_bottom
# Let's get back to our list of teams that suddenly got into top. Was that because of unbeliavable mix of luck and skill?
ot = [x for x  in outlier_teams if x in top_bottom['team'].drop_duplicates().tolist()]
ot
# The answer is absolutely no. They just played well during 1 season. Sometimes that happen.
[]

Выводы

Футбол - это игра с низким уровнем баллов, и один гол может изменить всю картину игры и даже конечные результаты. Вот почему долгосрочный анализ дает нам лучшую картину ситуации.

С внедрением метрики xG (и других, вытекающих из него) теперь мы можем реально оценить результаты работы команды в долгосрочной перспективе и понять разницу между топ-командами, командами среднего класса и абсолютными аутсайдерами.

xG вводит новые аргументы в дискуссии вокруг футбола, что делает его еще более интересным. И в то же время игра не теряет этот фактор неопределенности и возможности безумных вещей. На самом деле сейчас эти безумные вещи имеют шанс быть объяснены.

В конце концов мы обнаружили, что есть почти 100% шансы что в одной из лиг случится что-то странное. Это лишь вопрос времени, насколько эпическим это будет.


Оригинальная работа с интерактивными графиками может быть найдена тут.


Фото Vienna Reyes на Unsplash

Karma +1 when you share it:

Leave a Reply

Your email address will not be published. Required fields are marked *