MatchBrain es un algoritmo de predicción de resultado y cantidad de goles de partidos de futbol escrito en Python, utilizando LightGBM (Light Gradient-Boosting Machine) el cual es un aloritmo de Machine Learning basado en arboles de decisión. El modelo está entrenado con un dataset que incluyen todos los partidos internacionales de naciones jugados, desde amistosos hasta mundiales, pero el modelo ya se presta para entrenarse con datos de Clubes*.
*(quizás)
MatchBrain actualmente realiza las siguientes predicciones:
- Ganador del partido
- Cantidad de goles totales
- Si el partido tendra 3 goles (OVER 2.5 GOALS)
- Marcador Exacto
Aparte de eso, calcula la forma actual ofensiva y defensiva, y les da a las Naciones un puntaje ELO calculado historico para todos los partidos que estén en el dataset.
Prueba realizada con partidos amistosos pre-mundialistas (El escenario más complicado para un modelo así):
| Partido | Marcador Real | Marcador Simulado | Prob. Resultado (Local/Empate/Visitante) | Goles Esperados | ¿Cumplió Over 2.5? | ¿Cumplió Resultado? | ¿Cumplió Marcador? |
|---|---|---|---|---|---|---|---|
| Ecuador vs Guatemala | 3 - 0 | 1 - 0 | 67% / 21% / 12% | 2.64 ✅ | ✅ Sí (3 goles) | ✅ Sí (L) | ❌ No |
| Greece vs Italy | 0 - 1 | 1 - 1 | 23% / 23% / 54% | 2.96 ✅ | ❌ No (1 gol) | ✅ Sí (V) | ❌ No |
| Colombia vs Jordan | 2 - 0 | 2 - 0 | 65% / 20% / 15% | 2.97 ✅ | ❌ No (2 goles) | ✅ Sí (L) | ✅ Sí |
| Denmark vs Ukraine | 2 - 1 | 1 - 0 | 56% / 23% / 21% | 2.79 ✅ | ✅ Sí (3 goles) | ✅ Sí (L) | ❌ No |
| Morocco vs Norway | 1 - 1 | 1 - 1 | 44% / 27% / 30% | 2.52 ✅ | ❌ No (2 goles) | ✅ Sí (E) | ✅ Sí |
| Kosovo vs Andorra | 3 - 0 | 3 - 0 | 89% / 8% / 3% | 3.62 ✅ | ✅ Sí (3 goles) | ✅ Sí (L) | ✅ Sí |
| Armenia vs Kazakhstan | 1 - 1 | 1 - 2 | 14% / 18% / 68% | 3.47 ✅ | ❌ Sí (2 goles) | ❌ No (E) | ❌ No |
| Estonia vs Faroe Islands | 1 - 0 | 1 - 1 | 37% / 28% / 35% | 2.41 ❌ | ❌ No (1 gol) | ✅ Sí (L) | ❌ No |
| Gibraltar vs Cayman Islands | 4 - 1 | 1 - 1 | 33% / 26% / 41% | 2.64 ✅ | ✅ Sí (5 goles) | ❌ No (L) | ❌ No |
| Portugal vs Chile | 2 - 1 | 1 - 0 | 60% / 22% / 18% | 2.73 ✅ | ✅ Sí (3 goles) | ✅ Sí (L) | ❌ No |
| Romania vs Wales | 2 - 1 | 1 - 1 | 29% / 27% / 44% | 2.42 ❌ | ✅ Sí (3 goles) | ❌ No (V) | ❌ No |
| Albania vs Luxembourg | 0 - 1 | 1 - 0 | 62% / 23% / 15% | 2.48 ❌ | ❌ No (1 gol) | ❌ No (V) | ❌ No |
| USA vs Germany | 1 - 2 | 1 - 1 | 23% / 22% / 55% | 3.18 ✅ | ✅ Sí (3 goles) | ✅ Sí (V) | ❌ No |
| Panama vs Bosnia and Herzegovina | 1 - 1 | 1 - 1 | 51% / 25% / 24% | 2.58 ✅ | ❌ No (2 goles) | ✅ Sí (E) | ✅ Sí |
| Switzerland vs Australia | 1 - 1 | 1 - 1 | 44% / 26% / 30% | 2.65 ✅ | ❌ No (2 goles) | ✅ Sí (E) | ✅ Sí |
| Bolivia vs Scotland | 0 - 4 | 1 - 1 | 29% / 27% / 43% | 2.42 ❌ | ✅ Sí (4 goles) | ✅ Sí (V) | ❌ No |
| England vs New Zealand | 1 - 0 | 2 - 0 | 75% / 17% / 8% | 2.80 ✅ | ❌ No (1 gol) | ✅ Sí (L) | ❌ No |
| Qatar vs El Salvador | 0 - 0 | 1 - 0 | 54% / 26% / 20% | 2.32 ❌ | ❌ No (0 goles) | ❌ No (E) | ❌ No |
| Brazil vs Egypt | 2 - 1 | 1 - 0 | 56% / 24% / 20% | 2.60 ✅ | ✅ Sí (3 goles) | ✅ Sí (L) | ❌ No |
| Argentina vs Honduras | 2 - 0 | 2 - 0 | 76% / 16% / 8% | 2.83 ✅ | ❌ No (2 goles) | ✅ Sí (L) | ✅ Sí |
| Métrica | Aciertos | Total | % Acierto |
|---|---|---|---|
| Resultado (L/E/V) | 13 | 20 | 65% |
| Marcador Exacto | 6 | 20 | 30% |
| Over 2.5 goles | 11 | 20 | 55% |
El Fútbol es un deporte caotico e impredecible, y eso lo hace particularmente interesante al verlo de un punto de vista analítico. Apesar del caos el fútbol es un deporte extremadamente analítico, es un deporte donde variables como el rendimiento histórico, las estadísticas de los jugadores, las condiciones del encuentro, las tácticas empleadas y otros factores contextuales pueden afectar significativamente el resultado de un partido. Como podrás imaginarte, esto produce muchos datos.
Debido a esto, se hace un deporte bastante ideal para ajustar a un modelo de Machine Learning, ya que ahora las computadoras son capaces de procesar millones de datos por segundo y encontrar patrones complejos en grandes cantidades de datos, cosa que a un humano puede tomarle mucho tiempo.
La idea con MatchBrain era combinar mi pasión por el fútbol con mi fascinación con la Ciencia de Datos y el Machine Learning. Mi objetivo final era aprender más sobre la parte analítca del fútbol y encontrar patrones que nunca se me habrían ocurrido por mi cuenta. Ningún modelo es perfecto, pero creo que he logrado hacer algo util, bastante liviano, y descubriendo algún que otro dato interesante.
El dataset (o dataframe) inicial contiene las filas:
datehome_teamaway_teamhome_scoreaway_scoretournamentcitycountry(país en el que se jugó el encuentro)neutral
El sistema utiliza LightGBM porque es de lo mejorcito que existe actualmente para un dataset estructurado como este, es muy rápido y consume mucha menos memoria que otros algoritmos de Gradient Boosting. Los modelos de Gradient Boosting construyen muchos árboles pequeños que corrigen los errores de los anteriores, permitiendo capturar patrones difíciles sin diseñarlos manualmente.
Uno de los mayores problemas del fútbol es que tiene una complicación matemática muy molesta en comparación con otros datos temporales, me explico: un equipo puede jugar hoy como home_team y el próximo partido como away_team, puede jugar mal por que a un defensa le dio dolor de estomago, o al delantero estrella se le olvido como jugar futbol y ganar mañana por goleada. No se puede simplemente agrupar por una sola columna, tenes que calcular el rendimiento histórico "global" de cada selección, sin importar si jugó en casa o de visita, y luego asociar ese pasado a cada fila del dataset.
El sistema predice a partir de cuatro dimensiones:
- La forma reciente de los equipos. (Como esta actualmente)
- El contexto (Que se juega, y donde se juega)
- El potencial de ataque de ambos equipos comparado la defensa del otro.
- El puntaje ELO Historico. (Como se comporta historicamente)
Entonces la mejor forma de comenzar es haciendo dos copias de los datasets, y dos modelos de predicción, uno para el que juega en casa y otro para el que juega de visita, y entrenarlos individualmente.
Para comenzar había que capturar la "forma reciente" de cada selección, se duplica el dataset en dos perspectivas (local y visitante), se crea un historial cronológico por equipo y se aplica .shift(1).rolling(10).mean() para evitar filtración de datos futuros (para que el modelo no haga trampa solo viendo el resultado real del partido que debe predecir.).
| Feature | Descripción |
|---|---|
goles_anotados_form_home |
Promedio de goles anotados por el local en sus últimos 10 partidos |
goles_recibidos_form_home |
Promedio de goles recibidos por el local en sus últimos 10 partidos |
goles_anotados_form_away |
Promedio de goles anotados por el visitante en sus últimos 10 partidos |
goles_recibidos_form_away |
Promedio de goles recibidos por el visitante en sus últimos 10 partidos |
El contexto en el que se juega el partido.
| Feature | Descripción |
|---|---|
is_home_advantage |
Binaria (1 si no es cancha neutral, 0 si es neutral) |
tipo_torneo |
Categoría del torneo: mundial, oficial_continental, amistoso, otros_torneos |
Como en el dataset vienen muchos torneos que nadie conoce, había que clasificarlos y ponerlos a niveles parejos sin importar el continente:
| Clasificación | Torneos |
|---|---|
mundial |
Copa Mundial de la FIFA |
oficial_continental |
Partido oficial que tenga en juego un titulo o clasificación (EURO, Copa América, Qualifiers para la Copa Mundial, etc.) |
amistoso |
Partidos amistosos, Nations League, etc. |
Ratios que combinan el ataque de un equipo con la defensa del rival, facilitando el aprendizaje del modelo:
df['potencial_ataque_home'] = df['goles_anotados_form_home'] / (df['goles_recibidos_form_away'] + 0.1)
df['potencial_ataque_away'] = df['goles_anotados_form_away'] / (df['goles_recibidos_form_home'] + 0.1)Este es el mismo sistema utilizado oficialmente por el ajedrez en sitios como chess.com, y se aproxima bastante al Ranking FIFA Oficial que estaba roto desde el inicio, hasta que decidieron arreglarlo ahí por 2018. Al menos tiene la misma base matemática.
Todos los países comienzan con un rating de referencia:
1500
Este valor es arbitrario pero comodo porque evita números negativos, ofrece rango suficiente por encima y por debajo, y el sistema es invariante a la escala (funcionaría igual con 0 o con 5000). Al ser un sistema de suma cero, los puntos no se crean ni se destruyen, justo como la materia, todo punto que gana un equipo se lo resta al rival y eso depende de que tan disparejo sea el partido (No es lo mismo ganarle a Brazil en un mundial, que ganarle a El Salvador en un amistoso en cualquier momento de su historia).
Antes de cada partido, el sistema calcula la probabilidad esperada de que el Equipo A gane, basándose en la diferencia de ratings actuales:
El divisor 400 es el parámetro de escala de la curva logística. Su significado concreto es:
Una diferencia de exactamente 400 puntos de ELO implica que el favorito tiene aproximadamente un 91% de probabilidad de ganar.
Demostración numérica: si
Una vez conocido el resultado real, ambos ratings se actualizan simultáneamente:
Donde
| Resultado | ||
|---|---|---|
| Gana el Equipo A | 1.0 | 0.0 |
| Empate | 0.5 | 0.5 |
| Gana el Equipo B | 0.0 | 1.0 |
El término
Tenemos que:
- Si el favorito (
$E_A = 0.91$ ) gana como se esperaba:$(1 - 0.91) = 0.09$ hay un ajuste mínimo.
PERO:
- Si el débil (
$E_A = 0.09$ ) da la sorpresa y gana:$(1 - 0.09) = 0.91$ al equipo débil le da una cantidad de puntos masiva.
En este sentido
Esto es igual que el estándar adoptado por la FIFA en 2018 (sistema SUM) despues que se dieran cuenta que su sistema actual era una basura. Este reconoce que una derrota en una final del mundo debe pesar más que una derrota en un partido de preparación. El ELO es matemáticamente inmune al problema del viejo Ranking FIFA (inflar el rating jugando muchos amistosos fáciles).
Por ejemplo: si un equipo fuerte (
Si gana (lo esperado):
Si empata (resultado inesperado):
El cálculo es estrictamente secuencial (cada partido depende del anterior), por lo que se implementa con un bucle iterativo sobre el dataframe ordenado cronológicamente. El ELO se le da al modelo a la hora de hacer una predicción es el rating vigente justo antes de que se dispute el partido
En codigo todo esto se ve así:
ELO_INICIAL = 1500
k = 30
elo_actual = {}
elo_home_antes = []
elo_away_antes = []
for idx, row in df_final.iterrows():
home = row['home_team']
away = row['away_team']
if row['tipo_torneo'] == 'mundial':
K = 60
elif row['tipo_torneo'] == 'oficial_continental':
K = 40
else:
K = 20
r_home = elo_actual.get(home, ELO_INICIAL)
r_away = elo_actual.get(away, ELO_INICIAL)
elo_home_antes.append(r_home)
elo_away_antes.append(r_away)
e_home = 1 / (1 + 10 ** ((r_away - r_home) / 400))
e_away = 1 / (1 + 10 ** ((r_home - r_away) / 400))
if row['home_score'] > row['away_score']:
s_home, s_away = 1.0, 0.0
elif row['home_score'] < row['away_score']:
s_home, s_away = 0.0, 1.0
else:
s_home, s_away = 0.5, 0.5
elo_actual[home] = r_home + k * (s_home - e_home)
elo_actual[away] = r_away + k * (s_away - e_away)
df_final['elo_home'] = elo_home_antes
df_final['elo_away'] = elo_away_antes
Curiosamente, aparte del ELO propio, el modelo encontró que importa más que tan débil es la defensa del rival, sobre que tan poderosa es la ofensa propia, de igual forma tienen un poco de influencia los factores del contexto (Terreno neutral, Tipo del torneo)
La arquitectura de dos modelos separados funciona excepcionalmente bien pero asume una regla que fútbol real viola constantemente: que los goles del equipo local y los del visitante son eventos independientes, En la práctica, un gol en el minuto 5 cambia por completo la postura táctica de ambos equipos. Aún así, el modelo ha probado ser bastante acertado y tiene suficientes datos para producir buenas predicciones.
Se requiere Python 3.12+ preferiblemente con venv. Despues de clonar el repositorio (asumiendo que estes en la carpeta raiz) puedes realizar:
/.venv/Scripts/activate
pip install -r requirements.txt
python app.pyEstaba aburrido.