Pasos para analizar los partidos ATP con Machine Learning
Importamos las librerías necesarias
Para poder realizar todas las tareas, necesitaremos los siguientes imports:
%load_ext autoreload %autoreload 2 import pandas as pd import sklearn from random import seed from random import randint import warnings from sklearn.compose import ColumnTransformer from sklearn.pipeline import Pipeline from sklearn.impute import SimpleImputer from sklearn.preprocessing import StandardScaler from category_encoders.target_encoder import TargetEncoder from sklearn.model_selection import train_test_split from sklearn.preprocessing import LabelEncoder import lightgbm as lgb from sklearn.metrics import accuracy_score import shap import matplotlib.pyplot as plt import seaborn as sns seed(1) warnings.simplefilter("ignore") # Ignoramos los warnings
Carga de datos
df_original = pd.read_csv('Data.csv', encoding = 'latin1') # leemos el csv
En este conjunto podemos encontrar variables como:
- Dónde se jugó el torneo
- Fecha
- Tipo de superficie
- Ganador
- Puntos del ganador (antes del torneo)
- Ranking del ganador (antes del torneo)
- Perdedor
- Puntos del perdedor (antes del torneo)
- Ranking del perdedor (antes del torneo)
- Información de las probabilidades de casas de apuestas
- Información del número de juegos ganados/perdidos por el ganador/perdedor en los diferentes matches del partido
df_original.head()
Usar datos que no estarán disponibles en el momento de la predicción para construir el modelo se conoce como data leakage, y es uno de los problemas que un data scientist debe evitar a toda costa.
Análisis exploratorio inicial
En primer lugar realizaremos un análisis exploratorio inicial de algunas de las variables que finalmente usaremos para la predicción.
# Función que extrae el año y el mes de la fecha y lo añade como dos columnas nuevas al dataset def date_wrangle(_df): _df['Year'] = pd.DatetimeIndex(_df['Date']).year _df['Month'] = pd.DatetimeIndex(_df['Date']).month return _df df_original = date_wrangle(df_original) # Creamos las columnas de año y mes matches_won = df_original[['Tournament', 'Winner','Surface', 'Round', 'Year', 'Month', 'WRank']]
Análisis de ganadores por superficie
surfaces = df_original['Surface'].unique() # Realizamos un group by por la superficie y el ganador y contamos el número de triunfos que ha conseguido y, # posteriormente, nos quedamos con el top 10 surfaces_gb = matches_won.groupby(['Surface','Winner'], as_index=False)['Tournament'].count() surfaces_gb.columns = ['Surface','Player', 'Wins'] t10_wins_surface = surfaces_gb.groupby(['Surface']).apply(lambda x: x.nlargest(10,['Wins'])).reset_index(drop=True) # Realizamos un bucle para mostrar el top 10 de ganadores para cada una de las superficies for s in surfaces: x = t10_wins_surface[t10_wins_surface['Surface']==s]['Player'] y = t10_wins_surface[t10_wins_surface['Surface']==s]['Wins'] fig = plt.figure() ax = fig.add_axes([0,0,2,2]) ax.bar(x, y) plt.ylabel('Wins') plt.xlabel('Players') plt.title("Top 10 winners in " + s) plt.show()
Top 10 jugadores respecto del número de torneos ganados
# Nos quedamos únicamente con aquellas filas del dataset cuya columna Round toma el valor de 'The Final', es decir, # el último partido de cada torneo el cuál marcará el ganador de éste. # Posteriormente nos quedamos con el top 10 de jugadres que más torneos ganados tienen wins_tournament = matches_won[matches_won['Round'] == 'The Final'][['Tournament', 'Winner']] t10_wins_tournament = wins_tournament.groupby('Winner', as_index=False)['Tournament'].count().sort_values(['Tournament'], ascending=False).head(10) t10_wins_tournament.columns = ['Player', 'Tournaments_won'] fig = plt.figure() ax = fig.add_axes([0,0,2,2]) ax.bar(t10_wins_tournament['Player'], t10_wins_tournament['Tournaments_won']) plt.ylabel('Wins') plt.xlabel('Players') plt.title("Top 10 winners in tournaments") plt.show()
Análisis del Big Three
Tal y como hemos visto en todas las gráficas anteriores, hay jugadores que se encuentran presentes en todas ellas. Pero, existen claramente tres cuya hegemonía es incuestionable. Estamos hablando de los “Big Three” o los “Tres Grandes”; Federer, Nadal y Djokovic. Y es que una década después siguen encabezando los rankings, habiendo conseguido 51 de los últimos 59 títulos de Grand Slam.
Es por ello por lo que realizaremos un análisis centrándonos en estos tres jugadores.
bt = ['Federer R.', 'Nadal R.', 'Djokovic N.'] matches_won_bt = matches_won[matches_won['Winner'].isin(bt)] # Nos quedamos con los partidos jugados por los big three
Triunfos por fecha
- Partidos ganados por año
# Igual que en el caso para todos los jugadores, miramos el número de partidos ganados para cada uno de los big three matches_won_bt_year = matches_won_bt.groupby(['Year', 'Winner'], as_index=False)['Tournament'].count() matches_won_bt_year.columns = ['Year', 'Player', 'Wins'] fig = plt.figure(figsize=(20,10)) for p in bt: sns.lineplot(x='Year', y='Wins', data=matches_won_bt_year[matches_won_bt_year['Player']==p]) fig.legend(labels=bt) fig.suptitle('Matches won by Year')
- Torneos ganados por año
tournaments_won_bt_year = matches_won_bt[matches_won_bt['Round']=='The Final'].groupby(['Year', 'Winner'], as_index=False)['Tournament'].count() tournaments_won_bt_year.columns = ['Year', 'Player', 'Wins'] fig = plt.figure(figsize=(20,10)) for p in bt: sns.lineplot(x='Year', y='Wins', data=tournaments_won_bt_year[tournaments_won_bt_year['Player']==p]) fig.legend(labels=bt) fig.suptitle('Matches won by Year')
- Ranking ATP por año
matches_won_bt['WRank'] = matches_won_bt['WRank'].astype('int') fig = plt.figure(figsize=(20,10)) for p in bt: sns.lineplot(x='Year', y='WRank', data=matches_won_bt[matches_won_bt['Winner']==p]) fig.legend(labels=bt) plt.gca().invert_yaxis() fig.suptitle('Ranking by Year')
En lugar de coger todo el histórico, vamos a ver a partir de 2007, pues sabemos que a partir del 2008 es cuando comenzaron las lesiones de Nadal.
# Filtramos los partidos ganados por los bigthree en años posteriores o iguales a 2007 matches_won_bt_filter = matches_won_bt[matches_won_bt['Year'] >= 2007] fig = plt.figure(figsize=(20,10)) for p in bt: sns.lineplot(x='Year', y='WRank', data=matches_won_bt_filter[matches_won_bt_filter['Winner']==p]) fig.legend(labels=bt) plt.gca().invert_yaxis() fig.suptitle('Ranking by Year')
Podemos ver cómo Nadal a partir de su lesión en la rodilla (que comenzó en el 2008) y se acentuó del 2011 en adelante, ha causado estragos en el manacorí. No obstante, si tuviéramos los datos de años posteriores veríamos como, a pesar de las vicisitudes, su dominio incuestionable le ha devuelto al top 1.
A diferencia de Djokovic que desde esa fecha pudo coger carrerilla y situarse en la cúspide del ranking. Probablemente podríamos ver su lesión de codo si tuviéramos los datos del 2017 en adelante, pues eso le supuso la retirada de los campeonatos.
Análisis sets por superficie
sets_won_bt_surface = matches_won_bt.groupby(['Surface', 'Winner'], as_index=False)['Tournament'].count() sets_won_bt_surface.columns = ['Surface', 'Player', 'Sets_won'] palette ={"Federer R.":"C0","Nadal R.":"C1","Djokovic N.":"C2"} # Mantenemos la misma paleta de colores sns.factorplot(x='Surface', y='Sets_won', hue='Player', data=sets_won_bt_surface, kind='bar', palette=palette)
Data wrangling
- Fecha
# Función para filtrar el dataset a partir del año pasado como parámetro year_from def date_filter(_df, year_from): _df = _df[_df['Year'] >= year_from] return _df
- Filtrado de columnas
columns_to_use = ['Location', 'Tournament', 'Date', 'Series', 'Court', 'Surface', 'Round', 'Best of', 'WRank', 'LRank', 'WPts', 'LPts', 'Winner', 'Loser', 'Month', 'Year' ] # Función que se quedará únicamente con las columnas deseadas def filter_columns(_df): return _df[columns_to_use]
- Computar player 1 / 2
# Funciones para computar player 1 y player 2 # Función que realiza un número aleatorio entero entre 1 y 2. En caso de ser 1, player1 será winner y player2, looser. def get_player1(winner, loser): random_number = randint(1, 2) if random_number == 1: return winner else: return loser def get_player2(winner, loser, player1): if player1 == winner: return loser else: return winner def get_player1_points(winner, loser, player1, WPts, LPts): if player1 == winner: return WPts else: return LPts def get_player2_points(winner, loser, player1, WPts, LPts): if player1 == winner: return LPts else: return WPts def get_player1_atprank(winner, loser, player1, WRank, LRank): if player1 == winner: return WRank else: return LRank def get_player2_atprank(winner, loser, player1, WRank, LRank): if player1 == winner: return LRank else: return WRank # Aplicamos todas las funciones lambda para cambiar winner y loser por player1 y player2 de forma aleatoria. # Asimismo, una vez player1 y player2 tome el valor de winner o loser indistintamente, las demás columnas tales como # WPts, LPTs, etc., deberán ser cambiadas para hacer referencia al player1/2 def create_player_1_2(_df): _df['Player1'] = _df.apply(lambda x: get_player1(x['Winner'], x['Loser']), axis=1) _df['Player2'] = _df.apply(lambda x: get_player2(x['Winner'], x['Loser'], x['Player1']), axis=1) _df['Player1_Points'] = _df.apply(lambda x: get_player1_points(x['Winner'], x['Loser'], x['Player1'], x['WPts'], x['LPts']), axis=1) _df['Player2_Points'] = _df.apply(lambda x: get_player2_points(x['Winner'], x['Loser'], x['Player1'], x['WPts'], x['LPts']), axis=1) _df['Player1_ATPRank'] = _df.apply(lambda x: get_player1_atprank(x['Winner'], x['Loser'], x['Player1'], x['WRank'], x['LRank']), axis=1) _df['Player2_ATPRank'] = _df.apply(lambda x: get_player2_atprank(x['Winner'], x['Loser'], x['Player1'], x['WRank'], x['LRank']), axis=1) return _df
En el dataset original podemos ver como tenemos una columna Winner y una columna Loser para indicar el ganador y el perdedor del partido respectivamente. Si queremos realizar una predicción de quién ganará el partido con estas columnas, sería directo ya que siempre Winner gana a Loser. Además de que partiríamos de la base de que ya conocemos previamente el resultado antes de realizar la predicción, por lo que estaríamos cometiendo data leakage.
Para solventar esto hemos distribuido Winner y Loser de forma aleatoria de manera que se distribuyan entre Player1 ó Player2, cambiando las variables de los puntos y el ranking del ganador y del perdedor acorde al valor nuevo que hayan tomado. De esta manera ya podemos construir nuestro label y realizar las primeras predicciones.
- Label
# Función que generará el label. En caso de que el player1 sea el winner, devolverá 1 y 0 en caso contrario. def build_label(_df): def _create_label(winner, player1): if winner == player1: return 1 else: return 0 _df['label'] = _df.apply(lambda x: _create_label(x['Winner'], x['Player1']), axis=1) return _df
Una vez hemos distribuido Winner y Loser de forma aleatoria entre Player1 y Player2, nuestro label será dicotómico y lo construiremos de tal manera que tome el valor de 1 cuando Player1 gane el partido a Player2 y 0 en caso contrario. Es decir, estaríamos hablando de una clasificación binaria.
La construcción del label es una de las tareas más importantes en un proyecto de Machine Learning. De hecho, definirlo al comienzo del proyecto con el mayor detalle posible es una de nuestras prioridades, pues marcará muchas de las actividades posteriores. El label debe tener un significado claro y conciso y debe ser interpretable por negocio, para que una vez el modelo se reejecute con la periodicidad deseada y sus resultados se integren con el sistema de información, los expertos/as puedan tomar las decisiones y acciones pertinentes.
- Eliminación de columnas
def drop_columns(_df): _df.drop(columns=['Winner', 'Loser', 'Date', 'WRank', 'LRank', 'WPts', 'LPts'], inplace=True) return _df
Eliminamos las columnas que hemos utilizado para el cambio a Player1 Player2 que ya no necesitamos.
- Función que encapsula todas las anteriores
def df_wrangle(_df, year_from=2010): _df = date_wrangle(_df) _df = date_filter(_df, year_from) _df = filter_columns(_df) _df = create_player_1_2(_df) _df = build_label(_df) _df = drop_columns(_df) return _df df = df_wrangle(df_original)
Para ayudarnos a que el proceso sea algo más cómodo para reejecutar y mantener un orden, hemos creado una función que encapsula todas las funciones de transformación que necesitamos.
EDA y limpieza de datos
df.columns
Una vez hemos realizado las transformaciones iniciales, este sería el aspecto de nuestro dataset.
Análisis del Label
df['label'].value_counts()