Descripción del Problema

Se suele decir que el origen de los deportes yace en los juegos olímpicos originales de Grecia, y desde aquellos tiempos hay evidencia que muestra la existencia de apuestas por los eventos. Ahora mas que nunca la tecnología ha impactado todo ámbito de los deportes, y con el desarrollo de técnicas de minería de datos y el fácil acceso a grandes registros históricos es posible pensar en buscar una manera moderna de predecir resultados.

Nuestro proyecto está enfocado en la predicción de resultados de partidos de fútbol dados resultados históricos y las cuotas o multiplicadores que ofrecen las casas de apuestas.

De manera inicial se trabaja con esta idea en mente, frente a lo cual se toman diversas consideraciones:

  1. Se decide trabajar con los datos existentes de las ligas Europeas, dado que por el alto interés que existe por este deporte en dicho Continente, el nivel de datos presente es mayor que al de otras ligas.

  2. Se busca predecir un resultado de tipo Victoria/Derrota para el equipo local dado los equipos Local y Visita. Dado esto, se considera el resultado Empate como Derrota para el equipo Local Dado este último punto, se busca obtener una probabilidad superior al 50% para poder conseguir una mejora frente a un sistema básico de predicción (Como lo es ZeroR, por ejemplo).

Posteriormente, dada la incorporación de datos a explicar en los puntos siguientes, se considera interesante el observar los partidos de fútbol que posean probabilidades inusuales, además de investigar las razones de la ocurrencia de estos hechos.

Descripción de los datos

Una gran cantidad de los datos usados provienen de http://www.football-data.co.uk, un sitio que tiene archivos en csv por cada año y cada liga o división de varios países europeos. Dentro de los csv cada fila corresponde a un partido y cada columna a un atributo del partido, donde la mayoría son datos de casas de apuestas.

Finalmente, se busca añadir datos estadísticos de los equipos de Fútbol, es decir los datos promedios de temporadas previas, tales como los Tiros promedio por juego, cuántos de estos poseen acierto, tarjetas Amarillas y Rojas promedio, etc.

Finalmente, se busca añadir datos estadísticos de los equipos de Fútbol, es decir los datos promedios de temporadas previas, tales como los Tiros promedio por juego, cuántos de estos poseen acierto, tarjetas Amarillas y Rojas promedio, etc.

Para esto, se recolectan datos de diversos sitios dedicados a tal tarea, tales como WhoScored, FootStats, Football-Data, etc., pero pronto se hace notorio que existe una flagrante inconsistencia en los datos, desde incongruencia de datos entre sitios hasta la falta de los mismos en otros.

Dado esto, se rescatan las estadísticas de FootStats, la cual posee mayoritariamente datos de las ligas Inglesa, Francesa, Italiana, Alemana y Española de Alta y Media división, frente a sus sitios competidores que poseen sólo información de las ligas menores. Finalmente, estos datos se añaden a los dataset por países en los cuales se tiene información, y se evita añadir datos de otros sitios para evitar la inconsistencia y poco porcentaje de datos estadísticos.

Los datos usados pueden pueden ser descargados de aquí.

Limpieza de datos

Para realizar una buena exploración de datos fue necesario combinar de diferentes maneras los csvs disponibles en el sitio, y hacer una limpieza para eliminar las pocas filas con datos faltantes. Para todos las combinaciones se utilizó el siguiente script con pequeñas modificaciones:

import csv

fnms = []

datos = ["B1.csv", "D1.csv", "E0.csv", "I1.csv", "N1.csv", "P1.csv", "SC0.csv", "SP1-1.csv", "T1.csv", "E0(1).csv", "E0(2).csv", "E0(3).csv", "E0(4).csv", "E0(5).csv", "E0(6).csv", "E0(7).csv", "E0(8).csv", "E0(9).csv", "E0(10).csv", "SC0(1).csv", "SC0(2).csv", "SC0(3).csv", "SC0(4).csv", "SC0(5).csv", "SC0(6).csv", "SC0(7).csv", "SC0(8).csv", "SC0(9).csv", "SC0(10).csv", "SC0(11).csv", "D1(1).csv", "D1(2).csv", "D1(3).csv", "D1(4).csv", "D1(5).csv", "D1(6).csv", "D1(7).csv", "D1(8).csv", "D1(9).csv", "D1(10).csv", "I1(1).csv", "I1(2).csv", "I1(3).csv", "I1(4).csv", "I1(5).csv", "I1(6).csv", "I1(7).csv", "I1(8).csv", "I1(9).csv", "I1(10).csv", "SP1(1).csv", "SP1(2).csv", "SP1(3).csv", "SP1(4).csv", "SP1(5).csv", "SP1(6).csv", "SP1(7).csv", "SP1(8).csv", "SP1(9).csv", "SP1(10).csv", "N1(1).csv", "N1(2).csv", "N1(3).csv", "N1(4).csv", "N1(5).csv", "N1(6).csv", "N1(7).csv", "N1(8).csv", "N1(9).csv", "N1(10).csv", "N1(11).csv", "B1(1).csv", "B1(2).csv", "B1(3).csv", "B1(4).csv", "B1(5).csv", "B1(6).csv", "B1(7).csv", "B1(8).csv", "B1(9).csv", "B1(10).csv", "B1(11).csv", "P1(1).csv", "P1(2).csv", "P1(3).csv", "P1(4).csv", "P1(5).csv", "P1(6).csv", "P1(7).csv", "P1(8).csv", "P1(9).csv", "P1(10).csv", "T1(1).csv", "T1(2).csv", "T1(3).csv", "T1(4).csv", "T1(5).csv", "T1(6).csv", "T1(7).csv", "T1(8).csv", "T1(9).csv", "T1(10).csv", "G1.csv", "G1(1).csv", "G1(2).csv", "G1(3).csv", "G1(4).csv", "G1(5).csv", "G1(6).csv", "G1(7).csv", "G1(8).csv", "G1(9).csv", "G1(10).csv", "F1.csv", "F1(1).csv", "F1(2).csv", "F1(3).csv", "F1(4).csv", "F1(5).csv", "F1(6).csv", "F1(7).csv", "F1(8).csv", "F1(9).csv", "F1(10).csv"]#remplazar por csvs relevantes

for nom in datos:
  with open(nom, "rb") as csvextra:
        reader = csv.DictReader(csvextra)
        for row in reader:
            print row
            if len(fnms) == 0:
                fnms = list(row.keys())
            else:
                fnms = list(set(fnms).intersection(set(list(row.keys()))))
            break
            
for p in fnms:
    print (p)
    
print("inter: " + str(len(fnms)) + " elementos")

count = 0
empty = 0
with open("europa_global.csv", "w") as nuevo:
    writer = csv.DictWriter(nuevo, fieldnames=fnms)
    writer.writeheader()
    for nom in datos:
        with open(nom, "rb") as csvextra:
            reader = csv.DictReader(csvextra)
            for row in reader:
                new_row = {}
                write = True
                for fn in fnms:
                    new_row[fn] = row[fn].replace("'", "") #por problemas de formato con weka
                    if row[fn] == "":
                        print count, fn, "empty"
                        write = False #comentar esta linea para incluir filas con espacios
                        empty += 1
                        break
                
                if write:
                    writer.writerow(new_row)
                    print count, "DONE"
                    count += 1

print "FILE DONE"

print("inter: " + str(len(fnms)) + " columnas de " + str(len(datos)) + " archivos")
print "empty", empty

Los datos limpios pueden pueden ser descargados de aquí.

Exploración de datos

Primer vistazo a los datos

Para este trabajo se buscaron distintos dataset de futbol de ligas europeas. En un inicio partimos con un dataset educativo de kraggle

https://inclass.kaggle.com/c/football-data-challenge

Los datos que se encuentran en esta página corresponden a vectores de este tipo

Donde aparecen datos como fecha, Equipo Home, Equipo Away, cantidad de goles, resultado del partido, resultado de medio tiempo y luego una gran cantidad de numeros que representan las distintas casas de apuestas.

Como primera cosa realizamos una clasificación de cada partido por su variable FTR (full time result) que posee 3 clases H cuando gana el equipo local, A cuando gana el equipo visitante y D cuando existe un empate. Se utilizó el clasificador BayesNetwork ya que en los talleres del curso parecida tener buen desempeño (también ya que soportan múltiples clases) los resultados fueron los siguientes:

Es decir nuestro clasificador logra clasificar correctamente el 100% de los datos. Por lo tanto terminamos, ya no hay nada que mejorar y se acabo el proyecto…. No, en realidad sin darnos cuenta colocamos en los datos un valor que decidía el partido. Estos datos eran FTHG y FTAG (full time home goals y full time away goals) cantidades de goles al final del partido. Es decir nuestro clasificador podría decirnos quién ganó, pero tendríamos que esperar hasta el final del partido para que nos dijera.

Revisando el vector de características con mayor cuidado encontramos que poseía muchos valores que solo serian conocidos una vez terminado el partido tales como resultado de medio tiempo, goles medio tiempo, tarjetas rojas, tarjetas amarillas y corners. De esta forma solamente queda [resultado Partido .. valores de las distintas casa de apuestas] donde resultado de partido es la variable a clasificar.

Una vez se creó este vector de características se calculó nuevamente para el modelo BayesNetwork

Estos resultados eran algo más realistas donde se tenía 44% de precisión promedio entre las 3 clases. Es un resultado más que mejorable y esto era el objetivo de nuestro proyecto.

Sobre las técnicas de entrenamiento y testeo

Sobre los datos en sí son solamente 1500 datos para ese dataset, lo cual es un dataset algo pequeño. Debido a esto no se realizó una partición en train y test data se entrena y testea con cross validation lo cual no es ideal en el sentido de que nuestras mediciones de la efectividad del clasificador estarán sesgadas, pero tenemos más datos para entrenar.

Primer intento de mejora. Más columnas

Como primera idea para mejorar nuestros resultados fue agregar aún más columnas de casas de apuestas y conseguir más partidos para complementar nuestros 1500 datos. Se logró encontrar más datos de apuestas para la serie A italiana en la página http://www.football-data.co.uk/italym.php La cual contenía variados datos de apuestas para incluso años anteriores a la temporada 2014-2015 (que era nuestro dataset inicial), pero se decidió acotarlo a 2015-2014 para utilizar los vectores que ya teníamos. Con estos nuevas columnas se agregan tambien informacion de los partidos anteriores a la fecha, es decir % victorias últimos 5 partidos, % victorias local temporada anterior, % victorias visitante temporada anterior. Con este nuevo modelo se consiguen los siguientes resultados

Modelo DecisionTable

Modelo BayesNet

Se observa que los resultados con BayesNet no cambiaron prácticamente en el sentido de la precisión promedio, mientras que el nuevo método de decisionTable consiguió un resultado algo mejor clasificando correctamente el 53% de los datos.

Aun asi nos pareció que los % de precision eran muy bajos asi que planeamos los siguientes pasos a seguir.

La última iteración

Dentro de las ideas para mejorar el rendimiento del algoritmo surgieron las siguientes

Agregar más datos acerca de las características de un equipo tales como % de posesión en temporada pasada, % tiros en el blanco, % de corners, % amarillas etc. Probar mas algoritmos de clasificación como árboles (J48, random tree, random forest) y comprar su efectividad con zeroR Probar el caso de clasificación binaria Win-NoWin Normalizar los datos para ver si existe mejora en los resultados Normalizar respecto a columnas Intentar analizar otros dataset (equipos de europa) con las mismas casas de apuestas para observar que tan buenos comparativamente eran nuestros resultados *Intentar reducir la dimensión de las columna para no ahogarnos en vectores de alta dimensionalidad

Analizando nuevos dataset

En esta iteración se logró conseguir variados dataset con muchas más columnas. Primero se observo la frecuencia de los diferentes parametros de las columnas (probabilidades de apuestas) Se observaron gráficos como el siguiente en el dataset ReducedEurope.csv

Se observa que la mayoría tiene una distribución que parte con la mayor de sus valores en 0 y va decreciendo sin embargo existen excepciones como BbOu o BbMx que tienen distribuciones centradas en diferentes valores. Estas distribuciones podrían generar problemas de escala al comparar los diversos valores por esto nace la necesidad de normalizar los valores primero.

Una vez aplicado normalización por columnas queda

Como estos valores están normalizados ahora es posibles compararlos. Y son bastante extraños ya que por ejemplo se observa esta distribución para la columna VCDN

Donde existe un valor de 20.02551881 es decir está 20 desviaciones estandares sobre el promedio. Lo cual es bastante extraño ya que ocurre en varias de las distribuciones de los datos. Quizás existió un error en el tipeo de los datos?? Quizás existió un cambio de escala en las apuestas en todo este tiempo?? De todas formas la cantidad de datos es ínfima y no debería afectar los resultados. Se comparan los resultados para 3 tipos de métodos con J48 uno sin nada, otro con normalizar (todo vector tiene norma 1) y otro con estandarizar (todo valor de la columna pasa a tener promedio 0 y valor de desviaciones estándar) Los resultados fueron los siguientes

(Ver experimentoNormalizacion.kf)

Sin preproceso

Con normalizacion

Con estandarización

Pruebas sobre reducir la dimensión de los datos

Debido a que se logró conseguir muchas más dimensiones o columnas en esta iteración junto con el siempre presente problema de la dimensionalidad aparece la pregunta ¿Podemos quitar algunas columnas? Cuales?

(ver experimentoDimensionalidad.kf) Para esto se experimento con dos métodos atributeSelection y un algoritmo de reducción de dimensionalidad (PCA principal component analysis) frente al método sin ninguno. Los resultados fueron los siguientes

Resultado sin reduccion

Resultado con AtributeSelection

Reduccion con reducción de dimensionalidad PCA

De estos resultados se observa que el algoritmo Atribute selection logra mejorar algo el resultado. Pero lo aun mas importante baja las columnas a procesar por el algoritmo lo cual lo hace mas rapido. PCA por el otro lado parece que empeora el resultado.

La metodología final

Ahora con los resultados de la exploración se armó lo siguiente. Probar todos los diversos dataset sin una metodología hubiera sido una pesadilla ya que sería casi imposible compararlas objetivamente. Afortunadamente Weka con su herramienta workflow permite armar redes de módulos para realizar tareas tales como preprocesar, filtrar, clasificar y medir los resultados. Se desarrolló la siguiente red.

Parece algo compleja, pero en realidad es bastante simple. Se puede entender con los siguientes bloques.

En verde se consigue el archivo .csv de entrada, en rojo se consigue que argumento debe utilizarse para clasificar (en este caso FTR full time result) además de realizar la partición de K-fold cross validation, en azul el clasificador. El resto de los nodos a la derecha consiguen el desempeño de un clasificador dado. Todas las demás branch corresponden a distintas opciones de métodos, pre procesos. Como son 3 métodos se agrupan en tripletas por cada pre procesos. Es decir se calculan todas las combinaciones de “sin preproceso”, “Normalización”, “Reducir atributos” y en cuando a método están los 3 métodos que dieron mejores resultados en la fase de pruebas de iteración 1, es decir BayesNet, árboles J48 y ZeroR para comparar.

Estos resultados salen a archivos de texto los cuales pueden consultarse fácilmente para verificar que combinacion de metodos-preproceso funciona mejor y además cambia entre un dataset y otro es simplemente configurar el nodo inicial lo que permite la comparación de varios dataset de forma rapida.

Resultados

Primero solamente se van a tomar en cuenta las siguientes métricas: porcCC: Porcentaje clasificados correctamente es una medida promedio de la cantidad de clasificados correctamente entre las 3 clases y el total de datos. Permite tener un estimado a groso modo de la exactitud del algoritmo. Precisión por clase: Debido a que tenemos una clasificación de 3 clases se decidió separar en 3 precisiones para tener una mejor visualización de los datos. La precisión es uno de los datos más importante ya que si necesitamos algo para apostar debe estar lo más seguro posible de su estimación. Recall: Corresponde a cuántos de los verdaderos clase X se lograron obtener. No es tan vital como la precisión en nuestro problema, pero es importante ver que tan lejos se está se reconocer a todos los de una cierta clase. Roc-area: Una forma de calificar un clasificador es mediante su área bajo la curva roc siendo 1 el área de un clasificador perfecto. En este caso al ser casos binarios se toma como 1 clase vs las demás.

Para aplicar esta metodología se utilizó un codigo en python que se entrega en los archivos adjuntos. Este código permite tomar el archivo de texto exportado por Weka y conseguir los atributos que se necesiten.

Resultados por dataset

En todos los graficos siguientes se tiene la siguiente convencion. Rojo conAtribSel, ConNormalize Azul, Standarize Amarillo y sin preproceso verde. KraggleReduced3Class: Este dataset corresponde al dataset inicial del proyecto menos las columnas que contienen información del partido terminado.

Se puede observar que la mejora es prácticamente nula entre uno u otro preproceso. De hecho solo destaca la diferencia con AtribSel(en rojo) y el sin preprocesos. El detalle de los preprocesos muestra lo siguiente (solo se muestran los diferentes los demas son iguales a sinPreproceso)

Caso outConAtribSel

Comparado con el peor no hay gran cambio Caso outSinPreproceso

Se observa que si bien existe una mejora esta es ínfima tan solo del 3%en j48 y extrañamente parece empeorar para el caso de bayes net.

**Los resultados completos pueden encontrarse en la carpeta plots tanto los gráficos como los archivos texto originales.

ReducedDesafio2Clases3

Este dataset fue el que se utilizó en el desafío 2. Entre las mejoras que posee es que ahora agrega más casas de apuestas, agrega % de victorias de los últimos 5 partidos, % victorias home temporada pasada, % victorias away temporada pasada etc.

Es muy interesante observar que ocurre lo contrario al primer dataset, el mejor clasificador resulta ser ahora bayesNet por sobre J48 el cual posee un rendimiento menor al del dataset original. Quizas los datos del porcentaje favorecen a un clasificador como bayesNet. De ser asi al agregar más datos de este tipo como el caso del siguiente dataset deberia tener un resultado similar.

Italia+DetallesEquipos

En este dataset se agregan al dataset del desafío 2 de la serie A italiana una gran cantidad de datos sobre el equipo home y Away. Lamentablemente solamente se encontro información sobre años 2015-2014 por lo que es posible que algunos datos no correspondan a esta información.

Se observa otra vez que el algoritmo BayerNet gana en % de correctamente clasificador, lo intentesante es que ahora el arbol J48 esta mucho mas a la par y bayes falla en el caso SinPreproceso. Bastante en metricas como porcCC, H recall o D roc area.

ReducedEngland

En algunas de las observaciones de la iteración anterior se nos pidió contrastar nuestros datos con un dataset de otra liga de futbol se eligió la liga inglesa de futbol (un mix de todas). Y se espera obtener resultados similares ya que los datos son los mismos (estadísticas equipos, casas de apuestas)

El comportamiento como se esperaba es bastante similar, lo único que cambia es que bayes, j48 parecen comportarse muy similarmente sea cual sea el preproceso.

ReducedEuropa3Classes

Por último como desafío final se decidió usar un dataset de toda europa (mixs de diferentes ligas) y observar el desempeño del modelo propuesto.

Se observa que el desempeño bajo algo está por el 47 %. Además los distintos métodos de preproceso no tienen una gran incidencia en el resultado al parecer es notoria la mayor dificultad de generalizar a todo el continente.