Creando nuestro propio modelo xG

Para cerrar el ciclo de entradas referentes al xG, vamos a crear nuestro propio modelo de machine learning para predecir esta métrica. Explicaremos paso a paso cómo explorar y preparar los datos, aprenderemos a crear y entrenar nuestro modelo, y veremos cómo evaluar su rendimiento.

Resumen

Fuente de datos*: Vamos a utilizar un dataset con datos de más de 300000 tiros de varias temporadas de las principales ligas europeas. Este conjunto de datos fue un regalo que hizo el canal de Youtube Planeta Data Fútbol a sus suscriptores cuando llegaron a las 1000 suscripciones.

Problema: Estamos ante un problema de clasificación binaria, donde deberemos predecir si un tiro será gol. Para ser más exactos, predeciremos la probabilidad de que esto ocurra.

Algoritmo: XGBoost. A pesar de su nombre, no tiene nada que ver con la métrica xG. Es un algoritmo de boosting, los cuales se basan en árboles de decisión. Crean árboles de forma secuencial, de manera que cada uno va aprendiendo del error del anterior. El XGBoost, en particular, utiliza una función de pérdida o coste que va minimizando el error en cada iteración. Incorpora validación cruzada en el entrenamiento y tratamiento de valores faltantes, lo que le hace robusto ante ellos. Se puede utilizar tanto en problemas de regresión como de clasificación. Para más información sobre este algoritmo podéis consultar esta página.

Lenguaje de programación: R. Para las visualizaciones utilizamos los paquetes ggplot2 y ggsoccer. También usaremos dplyr y caret para el preprocesamiento de los datos, y xgboost para crear y entrenar nuestro modelo. Además, nos apoyaremos en el paquete LearnGeom para realizar algún cálculo matemático, en doParallel para entrenar el modelo más rápido y en pROC para evaluar su rendimiento.

Explorando los datos

Nuestro dataset dispone de 317521 filas que representan cada tiro con 21 características cada una. Para este estudio sólo vamos a tener en cuenta los disparos que se realizaron en juego abierto, dejando para otro experimento distinto el balón parado. Entonces, vamos a filtrar aquellas filas que tengan la columna situation igual a OpenPlay. Además, sólo vamos a utilizar las siguientes características:

  • X, Y: Coordenadas exactas desde donde el jugador realizó el disparo. En este ejemplo utilizaremos como referencia el sistema de coordenadas que utiliza el proveedor de datos Opta (100×100).
  • minute: Minuto del partido en el que se produjo el tiro.
  • h_a: Si el jugador que dispara es del equipo local (h) o visitante (a).
  • shotType: Parte del cuerpo con la que se realiza el tiro. Las opciones son: de cabeza (Head), con el pie (RightFoot, LeftFoot) o con otra parte del cuerpo (OtherBodyPart).
  • lastAction: Se refiere a la acción que precede al tiro. Entre sus posibles valores destacan: pase (Pass), centro (Cross) o rebote (Rebound).
  • result: Como finaliza el tiro. A nosotros sólo nos interesa saber si ha sido gol, por lo que la convertiremos en variable binaria (0: No gol, 1:Gol).

Veamos unos gráficos para la variable result:

Gráfico de barras realizado con ggplot2

Claramente se puede apreciar que estamos ante un problema de clasificación binaria desbalanceado. Tenemos muchos más registros de tiros fallados que de los que acaban en gol. Esto podría ser un problema, pero los algoritmos de boosting disponen de su propio sistema para compensar estas situaciones.

Gráfico de puntos realizado con ggplot2 y ggsoccer

Aunque la mayoría de goles corresponden a tiros desde dentro del área, vemos que también hay otros que se marcan casi desde el centro del campo, lo cual no es para nada habitual. Este tipo de registros podrían considerarse outliers que podrían perjudicar al desempeño de nuestro modelo. Los vamos a dejar, ya que requieren un estudio exhaustivo que se sale del propósito de este experimento.

Ingeniería de características

En este paso, más conocido como Feature Engineering, vamos a crear nuevas variables a partir de las que tenemos. En particular, vamos a seguir los consejos del canal de Youtube Friends Of Tracking y calcularemos la distancia desde donde se realiza el tiro hasta la portería y el ángulo de tiro que tiene el jugador en el momento del disparo:

  • distToGoal: Utilizaremos la función dist del paquete stats para calcular la distancia euclídea desde el punto donde se realiza el tiro (X, Y), al punto central de la línea de gol (100, 50).
  • angleToGoal: Con la función angle del paquete LearnGeom, calcularemos el ángulo formado por los puntos donde están los postes, (100, 55.8) y (100, 44.2), y el punto desde donde se realiza el disparo, el cual utilizaremos como vértice.

Echemos un vistazo al dataset final:

Muestra de los datos

Tenemos 232884 observaciones con 9 variables, siendo result la variable dependiente.

Preprocesamiento de los datos

Ahora vamos a preparar los datos para la creación y entrenamiento de nuestro modelo. Estos son los pasos a seguir:

  • División del conjunto de datos: Dividiremos nuestros datos en dos partes, entrenamiento y validación. Cogeremos el 70% de las observaciones para la primera y el 30% para la segunda. Lo haremos con la función createDataPartition del paquete caret.
  • Centrado y escalado de variables: En la mayoría de algoritmos es necesario que los datos numéricos estén en la misma escala y rango de valores, pero en los que utilizan árboles de decisión no es necesario.
  • Transformación de variables categóricas: El algoritmo XGBoost sólo admite variables numéricas, por lo que necesitamos convertir nuestras variables categóricas en variables binarias. Se creará una nueva variable por cada categoría. Utilizaremos la función dummyVars del paquete caret.
  • Eliminación de variables con varianza cero: Las variables que tienen varianza cero o cercana a cero no aportan información al modelo, ya que tienen (casi) siempre el mismo valor. Las detectamos con la función nearZeroVar del paquete caret y las eliminamos.

Finalmente, nos quedan 2 conjuntos de datos con las siguientes variables:

Muestra de los datos procesados

Creando y entrenando el modelo

Para crear nuestro modelo XGBoost tenemos que definir una serie de parámetros, más conocidos como hiperparámetros del modelo. Con los valores por defecto, este algoritmo suele dar buenos resultados, pero nosotros vamos a tratar de ajustarlos para obtener mejores predicciones. Veamos cuál es cada uno:

  • objective: Aquí tenemos que definir que tipo de problema tenemos. En nuestro caso tenemos que indicar binary:logistic para que nos devuelva valores entre 0 y 1.
  • eval_metric: Para definir la métrica que usará el modelo para evaluar su desempeño. Elegimos AUC, métrica que explicaremos en el siguiente apartado.
  • nrounds: Número máximo de iteraciones del modelo. Por defecto es 100, pero vamos a buscar el número óptimo (puede ir desde 1 hasta el número que queramos).
  • eta: Tasa de aprendizaje de nuestro modelo. Valores bajos harán que el modelo aprenda lentamente, pero se adaptará mejor a los datos de entrenamiento. Puede producir overfitting. Con valores altos, el modelo aprenderá más rápido, pero podría hacer que nos saltemos el mínimo global del error. Por defecto es 0.3 (puede ir entre 0 y 1), pero también lo ajustaremos.
  • max_depth: Profundidad máxima del árbol. El valor por defecto es 6, pero buscaremos el valor óptimo (entre 0 y el número que queramos).
  • col_sample_by_tree: Porcentaje del número de variables que evaluará cada árbol. Buscaremos ajustarlo y su valor por defecto es 1, pero puede ser cualquier valor entre 0 y 1.
  • subsample: Es el porcentaje del número de observaciones que evalúa cada árbol. También es 1 por defecto y puede tomar valores entre 0 y 1.

Para buscar los valores óptimos utilizaremos la técnica de búsqueda en rejilla, más conocida como grid search, utilizando el paquete caret. Para encontrar el mejor número de iteraciones para el modelo (nrounds), utilizaremos la función xgb.cv del paquete xgboost.

Finalmente, con los valores obtenidos en los pasos anteriores, creamos y entrenamos nuestro modelo con la función xgb.train del paquete xgboost.

Predicción del xG y evaluación

Una vez tenemos nuestro modelo, vamos a ponerlo a prueba. Lo utilizaremos para predecir sobre los datos de validación con la función predict del paquete stats. Veamos el resultado gráficamente:

Gráfico creado con ggplot2 y ggsoccer

Obtenemos unos resultados bastante coherentes. Cuanto más cerca de la línea de gol y mayor ángulo de tiro, mayor probabilidad de marcar gol.

Para medir el rendimiento de nuestro modelo vamos a utilizar la métrica AUC (Area Under the Curve), que mide el área bajo la curva ROC. Esta curva es una representación gráfica de la sensibilidad frente a la especificidad para un modelo clasificador binario. Para más información sobre la curva ROC o el AUC podéis visitar esta entrada de Wikipedia.

Para calcular el AUC utilizaremos la función auc del paquete pROC. Con nuestro modelo obtenemos un resultado de 0.795. Podemos considerar un buen resultado valores entre 0.75 y 0.9 de AUC, por lo que nuestro modelo presenta un rendimiento satisfactorio, teniendo en cuenta además que es nuestro primer modelo. Podríamos intentar mejorarlo creando más variables o aumentando el rango de valores en la búsqueda de los hiperparámetros óptimos.

Variables más importantes

Por último, vamos a ver cuáles son las variables que más información han aportado al modelo. Utilizaremos la función xgb.importance del paquete xgboost:

Gráfico realizado con xgboost

Para nuestro modelo, las variables más importantes han sido distToGoal y angleToGoal. Algo bastante esperado viendo el gráfico de resultados anterior.

Podéis seguir el desarrollo de esta entrada viendo el código al completo en nuestro repositorio de GitHub.

Si os gusta este contenido podéis dejar vuestros comentarios en nuestra cuenta en Twitter.

*Por respeto a las personas que crearon este dataset, en el repositorio de GitHub sólo dejaremos una muestra con unas pocas observaciones de ejemplo.

Política de Privacidad