Fútbol: por qué los ganadores ganan y los perdedores pierden

Explorando 5 años de fútbol europeo

Intro

En este cuaderno exploraremos métricas modernas en el fútbol (xG, xGA y xPTS) y su influencia en la analítica deportiva.

  • Expected Goals (xG) – mide la calidad de un disparo en función de varias variables, como el tipo de asistencia, el ángulo de disparo y la distancia desde la portería, si fue un disparo a la cabeza y si se definió como una gran oportunidad.
  • Expected Assits (xGA) – mide la probabilidad de que un pase dado se convierta en un gol de asistencia. Considera varios factores, incluido el tipo de pase, el punto final del pase y la longitud del pase.
  • Expected Points (xPTS) – mide la probabilidad de que cierto juego traiga puntos al equipo.

Estas métricas nos permiten profundizar mucho más en las estadísticas de fútbol y comprender el rendimiento de los jugadores y los equipos en general y darnos cuenta del papel de la suerte y la habilidad en él. Descargo de responsabilidad: ambos son importantes.

El proceso de recopilación de datos para este cuaderno se describe en este núcleo de Kaggle: 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

Import de datos y EDA visual

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

En la siguiente visualización, comprobaremos cuántos equipos de cada liga estuvieron entre los 4 mejores durante los últimos 5 años. Puede brindarnos información sobre la estabilidad de los mejores equipos de diferentes países.

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)

Como podemos ver en estos gráficos de barras, hay equipos que en los últimos 5 años estuvieron en el top 4 solo una vez, lo que significa que no es algo común, lo que significa que si profundizamos, podemos encontrar que hay un factor de suerte que podría haber jugado a favor de estos equipos. Es solo una teoría, así que veamos más de cerca esos valores atípicos.

Los equipos que estuvieron en el top 4 solo una vez durante las últimas 5 temporadas son:

  • Wolfsburg (2014) y Schalke 04 (2017) de la Bundesliga
  • Leicester (2015) de EPL
  • Villareal (2015) y Sevilla (2016) de La Liga
  • Lazio (2014) y Fiorentina (2014) de Serie A
  • Lille (2018) y Saint-Etienne (2018) de Ligue 1
  • FC Rostov (2015) y Dinamo Moscow (2014) de RFPL

Vamos a guardar estos equipos.

# 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()

Comprender cómo ganan los ganadores

In this section we will try to find some patterns that can help us understand what are some of the ingredients of the victory soup :D. Starting with Bundesliga.

Bundesliga

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)

Al mirar la tabla y el gráfico de barras, vemos que el Bayern cada año obtuvo más puntos de los que deberían tener, anotaron más de lo esperado y perdieron menos de lo esperado (excepto en 2018, que no rompió su plan de ganar la temporada, pero da algunas pistas de que el Bayern jugó peor este año, aunque los competidores no lo aprovecharon).

# 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)

La Liga

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)

Como podemos ver en la tabla anterior, en 2014 y 2015, Barcelona estaba creando suficientes momentos para ganar el título y no confiar en las habilidades personales o la suerte, de estos números podemos decir que THE Team estaba jugando allí.

En 2016 hubo mucha competencia entre Madrid y Barcelona y al final Madrid tuvo más suerte / tuvo más agallas en un juego en particular (o Barcelona tuvo mala suerte / no tenía bolas) y fue el costo del título. Estoy seguro de que si profundizamos esa temporada podemos encontrar ese partido en particular.

En 2017 y 2018, el éxito de Barcelona se debió principalmente a las acciones de Lionel Messi, quien estaba anotando o haciendo asistencias en situaciones en las que los jugadores normales no harían eso. Lo que llevó a tal salto en la diferencia de xPTS. Lo que me hace pensar (teniendo el contexto de que el Real Madrid es muy activo en el mercado de transferencias esta temporada) puede terminar mal. Solo opinión subjetiva basada en números y viendo partidos de Barcelona. Realmente espero estar equivocado.

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

EPL

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)

En EPL vemos la clara tendencia que te dice: "Para ganar tienes que ser mejor que las estadísticas". Un caso interesante aquí es la historia de victoria de Leicester en 2015: ¡obtuvieron 12 puntos más de lo que deberían y al mismo tiempo el Arsenal obtuvo 6 puntos menos de lo esperado! Por eso amamos el fútbol, porque suceden cosas tan inexplicables. No estoy diciendo que sea una suerte total, pero jugó su papel aquí.

Otra cosa interesante es el Manchester City de 2018: ¡son súper estables! Anotaron solo un gol más de lo esperado, fallaron 2 menos y obtuvieron 7 puntos adicionales, mientras que Liverpool luchó realmente bien, tuvo un poco más de suerte de su lado, pero no pudo ganar a pesar de estar 13 puntos por delante de lo esperado.

Pep está terminando de construir la máquina de destrucción. Man City crea y convierte sus momentos en función de la habilidad y no confía en la suerte; los hace muy peligrosos en la próxima temporada.

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

Ligue 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)

En la Ligue 1 francesa seguimos viendo la tendencia "para ganar hay que ejecutar el 110%, porque el 100% no es suficiente". Aquí Paris Saint Germain domina totalmente. ¡Solo en 2016 tenemos un caso atípico frente a Mónaco que anotó 30 goles más de lo esperado! y obtuve casi 17 puntos más de lo esperado! ¿Suerte? Muy buena parte de eso. El PSG fue bueno ese año, pero Mónaco fue extraordinario. Nuevamente, no podemos afirmar que es pura suerte o pura habilidad, sino una combinación perfecta de ambos en el lugar y el tiempo correctos.

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

Serie A

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)

En la Serie A italiana, la Juventus domina 8 años seguidos, aunque no puede mostrar ningún gran éxito en la Liga de Campeones. Creo que al revisar este cuadro y los números podemos entender que la Juve no tiene una competencia lo suficientemente fuerte dentro del país y obtiene muchos puntos "afortunados", lo que nuevamente se deriva de múltiples factores y podemos ver que Napoli superó a la Juventus en xPTS dos veces, pero es una vida real y, por ejemplo, en 2017, la Juve estaba loca y anotó 26 goles adicionales (o creó goles de la nada), mientras que Napoli perdió 3 más de lo esperado (debido a un error del portero o tal vez la excelencia de algún equipo en 1 o 2 partidos particulares). Al igual que con la situación en La Liga cuando el Real Madrid se convirtió en campeón, estoy seguro de que podemos encontrar 1 o 2 juegos que fueron clave ese año.

Los detalles importan en el fútbol. Ves, un error aquí, una carpintería allá y has perdido el título.

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

RFPL

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)

No sigo a la Premier League rusa, así que solo mirando fríamente los datos vemos el mismo patrón que anotar más de lo que mereces y también una situación interesante con CSKA Moscú de 2015 a 2017. Durante estos años, estos muchachos fueron buenos, pero convirtieron sus ventajas solo una vez, los otros dos: si no se convierte, se lo castiga o su principal competidor simplemente se convierte mejor.

No hay justicia en el fútbol :D. Aunque, creo que con VAR los números serán más estables en las próximas temporadas. Porque una de las razones de esos objetivos y puntos adicionales son los errores de los árbitros.

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

Resumen estadístico

Como hay 6 ligas con diferentes equipos y estadísticas, decidí centrarme en una al principio para probar diferentes enfoques y luego replicar el modelo de análisis final en otras 5. Y como veo principalmente La Liga, comenzaré con esta competencia mientras saber más al respecto.

# 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)
Presentando algunos records y antirecords:
El valor más bajo de SCORED tuvo Córdoba en 2014 y es igual a 22.00
El valor más alto de SCORED tuvo Real Madrid en 2014 y es igual a 118.00
================================================================
El valor más bajo de XG tuvo Eibar en 2014 y es igual a 29.56
El valor más alto de XG tuvo Barcelona en 2015 y es igual a 113.60
================================================================
El valor más bajo de XG_DIFF tuvo Barcelona en 2016 y es igual a -22.45
El valor más alto de XG_DIFF tuvo Las Palmas en 2017 y es igual a 13.88
================================================================
El valor más bajo de MISSED tuvo el Atlético de Madrid en 2015 y es igual a 18.00
El valor más alto de MISSED tuvo Osasuna en 2016 y es igual a 94.00
================================================================
 El valor más bajo de XGA tuvo el Atlético de Madrid en 2015 y es igual a 27.80
 El valor más alto de XGA tuvo Levante en 2018 y es igual a 78.86
 ================================================== ==============
 El valor más bajo de XGA_DIFF tenía Osasuna en 2016 y es igual a -29.18
 El valor más alto de XGA_DIFF tuvo Valencia en 2015 y es igual a 13.69
 ================================================== ==============
 El valor más bajo de PTS tuvo Córdoba en 2014 y es igual a 20.00
 El valor más alto de PTS tuvo Barcelona en 2014 y es igual a 94.00
 ================================================== ==============
 El valor más bajo de XPTS tuvo Granada en 2016 y es igual a 26.50
 El valor más alto de XPTS tuvo Barcelona en 2015 y es igual a 94.38
 ================================================== ==============
 El valor más bajo de XPTS_DIFF tuvo el Atlético de Madrid en 2017 y es igual a -17,40
 El valor más alto de XPTS_DIFF tuvo el Deportivo La Coruña en 2017 y es igual a 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)

De las tablas anteriores podemos ver claramente que los mejores equipos obtienen más puntos, conceden menos y obtienen más puntos de lo esperado. Es por eso que estos equipos son los mejores equipos. Y situación totalmente opuesta con extraños. Los equipos del medio juego promedio. Totalmente lógico, no hay grandes ideas aquí.

# 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

Detección de valores atípicos

Z-Score

Z-Score es el número de desviaciones estándar de la media de un punto de datos. Podemos usarlo para encontrar valores atípicos en nuestro conjunto de datos suponiendo que | z-score | > 3 es un valor atípico.

# 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 valores atípicos en total detectados con z-score. Pobre Osasuna en 2016: casi 30 goles no merecidos.

Como podemos ver en estos datos, estar en un espacio atípico aún no te hace ganar la temporada. Pero si pierde sus oportunidades o recibe objetivos donde no debería hacerlo y hace demasiado, merece la relegación. Perder y ser promedio es mucho más fácil que ganar.

Rango intercuartil (IQR)

IQR - es la diferencia entre el primer cuartil y el tercer cuartil de un conjunto de datos. Esta es una forma de describir la propagación de un conjunto de datos.

Una regla de uso común dice que un punto de datos es un valor atípico si está a más de 1.5 ⋅RQ por encima del tercer cuartil o por debajo del primer cuartil. Dicho de otra manera, los valores atípicos bajos están por debajo de Q1 - 1.5 ⋅ IQR y los valores atípicos altos están por encima de Q3 + 1.5 ⋅ IQR.

Vamos a ver.

# 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 for xG_diff: 13.16
Upper border for xG_diff: 16.65
Lower border for xG_diff: -18.43
==================================================
IQR for xGA_diff: 13.95
Upper border for xGA_diff: 17.15
Lower border for xGA_diff: -20.05
==================================================
IQR for xPTS_diff: 13.93
Upper border for xPTS_diff: 18.73
Lower border for 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))
Probability of outlier in top or bottom of the final table: 8.10%

Entonces, podemos decir que es muy probable que cada año en una de las 6 ligas haya un equipo que obtenga un boleto para la Liga de Campeones o Europa Legue con la ayuda de la suerte además de sus grandes habilidades o haya un perdedor que obtenga a la segunda división, porque no pueden convertir sus momentos.

# 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

Nuestros ganadores y perdedores con un rendimiento brillante y un rendimiento inferior brillante.

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.
[]

Conclusiones

El fútbol es un juego de bajo puntaje y un gol puede cambiar la imagen completa del juego e incluso los resultados finales. Es por eso que el análisis a largo plazo le da una mejor idea de la situación.

Con la introducción de la métrica xG (y otras que se derivan de esto) ahora realmente podemos evaluar el rendimiento del equipo a largo plazo y comprender la diferencia entre los mejores equipos, los equipos de clase media y los extraños absolutos.

xG trae nuevos argumentos a las discusiones sobre el fútbol, lo que lo hace aún más interesante. Y al mismo tiempo, el juego no pierde este factor de incertidumbre y la posibilidad de que ocurran locuras. En realidad ahora, estas locuras tienen una oportunidad de ser explicadas.

Al final, hemos descubierto que hay casi un 100% de posibilidades de que ocurra algo extraño en una de las ligas. Es solo cuestión de tiempo lo épico que será.


Se puede encontrar trabajo original con gráficos interactivos aquí.


Foto de Vienna Reyes en Unsplash

Leave a Reply

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