Футбол: Чому переможці перемагають, а невдахи програють

Дивимось 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-оцінка | > 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

Leave a Reply

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