En esta entrada se va a explicar el proceso de cocinado de datos, análisis de las variables y cómo, gracias a un algoritmo de minería de datos, se estudia la viabilidad de predecir los sucesos del encierro estos sanfermines.
Cocinar los datos
Una vez conseguimos tener todos los datos introducidos a la base de datos, el siguiente paso fue darles forma para poder aprender de ellos y crear un modelo que nos predijera que va a suceder en estos sanfermines.
Pasos que se han seguido:
- Hemos descartado aquellos registros incompletos o que contenían demasiadas incertidumbres que más que aprenden ocasionaban ruido y hacían que la predicción fuera peor.
- Se han convertido las variables categóricas a columnas numéricas para que nuestro algoritmo pueda aprender.
- Crear un único dataset con toda la información pudiendo unir todos los datos por fecha.
- Normalización de los datos para que una variable como el tiempo o la temperatura máxima no distorsionen el peso de la variable.
- Hemos sacado nuevas columnas
- Comienzo de fiesta
- Final de fiesta
- Aceras en el recorrido
- Producto antideslizante en el suelo
- Día de la semana en la que se produjo el encierro
- Número de participación de la ganadería
- Datos de corneados, heridos, atendidos del día anterior
- Media de cornadas de la ganadería
- Máximo de cornadas
- Mínimo de cornadas
- Riesgo de la ganadería
Después de estos pasos hemos conseguido un conjunto de datos limpio donde teníamos claros los datos que queríamos usar.
Escoger que algoritmo usar
El siguiente paso fue escoger que algoritmo íbamos a usar para predecir el tiempo del encierro y los corneados que se producirían por tramo. Para ello acudimos a uno de los bancos que mayor sabiduría albergan en esto de la minería de datos, el señor Mikel Galar (doctor de la Universidad Pública de Navarra) el cual es un experto y un buen amigo de Ekinbe. Él fue quien nos propuso usar el XGBoost, dado que nos iba a permitir realizar el aprendizaje supervisado que necesitábamos y también usar uno de los métodos que mejores resultados están dando en la actualidad. Sí que nos aviso que se trata de un algoritmo con muchos parámetros y que deberíamos de adaptarlo a nuestros datos.
XGBoost significa eXtreme Gradient Boosting. Este algoritmo está siendo el gran dominador en materia de Machine learning y también en las competiciones de Kaggle (portal web de competiciones entorno a los modelos de predicción y analítica ) con datos estructurados o tabulares (que era nuestro caso). XGBoost es una implementación de árboles de decisión con Gradient boosting diseñada para minimizar la velocidad de ejecución y maximizar el rendimiento. Posee una interface para varios lenguajes de programación, entre los que se incluyen Python la cual fue nuestra opción a la hora de realizar el desarrollo.
Internamente, XGBoost representa todos los problemas como un caso de modelado predictivo de regresión que sólo toma valores numéricos como entrada. Si nuestros datos están en un formato diferente, primero vamos a tener que transformarlos para poder hacer uso de todo el poder de esta librería. El hecho de trabajar sólo con datos numéricos es lo que hace que esta librería sea tan eficiente. Por ese motivo hemos tenido que convertir los datos de tipo «String» a campos numéricos.
Comprobar el peso de las variables
Para este paso nos hemos creado una pequeña función que nos muestra de forma visual el peso de cada una de las variables en la predicción. De esta forma podemos saber que variables tienen mayor peso.
def modelfit(alg, dtrain, dtest, target, predictors, cv_folds=5, early_stopping_rounds=50): alg.fit(dtrain[predictors], dtrain[target], eval_metric='auc') dtrain_predictions = alg.predict(dtest[predictors]) print("\nModel Report") print("Accuracy : \n") print(metrics.accuracy_score(dtest[target].values, dtrain_predictions)) print('Datos de entreno') print(dtest[target].values) print('Datos predecidos') print(dtrain_predictions) datos = sorted( ((v,k) for k,v in alg.get_booster().get_score(importance_type='weight').iteritems()), reverse=True) x, y = zip(*datos) feat_imp = pd.Series(x, y) feat_imp.plot(kind='bar') plt.show()
Como se puede observar, para pronosticar la duración del tiempo de hoy el algoritmo considera como variable de peso la cantidad de atendido que hubo, el tiempo del día anterior (en muchos casos estará relacionado con la peligrosidad del día anterior), el número de participación de la ganadería, la media de cornadas que esta lleva y ya el resto de variables. Esto significa que al parecer si que afecta lo que sucedió el día anterior en el encierro así como la historia de una ganadería como se puede apreciar en las variables de número de participación y cornadas medias.
Por el contrario para predecir las cornadas del tramo de estafeta vemos la siguiente gráfica donde podemos ver que hay muchas más variables implicadas pero que siguen siendo las mismas aquellas que más peso tienen.
Esto nos hace ver lo que ya sabíamos, tanto el tiempo como la cantidad de cornadas van de la mano, a mayor tiempo mayor probabilidad de cornadas.
Adaptación de XGBoost
Una vez estructurados los datos, el siguiente paso es la selección de los parámetros de nuestro algoritmo. Los parámetros que debíamos de ajustar son:
- Learning_rate: como de rápido aprende. Un número muy alto puede llevar a una escasa generalización y un ajuste muy estricto a los datos de entrenamiento
Para esto hicimos la siguiente prueba en donde le dimos valores entre 0.01 y 0.25 con un resultado de 0.01 para la estimación de tiempo y 0.1 para las corneadas por tramo
param_test0 = { 'learning_rate':[i/100.0 for i in range(1,25,2)] } gsearch0 = GridSearchCV(estimator = XGBClassifier( learning_rate =0.1, n_estimators=300, max_depth=5, min_child_weight=5, gamma=0, subsample=0.9, colsample_bytree=0.7, objective= 'binary:logistic', nthread=4, scale_pos_weight=1, seed=27), param_grid = param_test0, scoring='accuracy', n_jobs=4,iid=False, cv=5) gsearch0.fit(train[predictors],train[target]) print('Results: \n') print(gsearch0.cv_results_) print('Best params: \n') print(gsearch0.best_params_) print('Best score: \n') print(gsearch0.best_score_)
- Max_depth y min_child_weight
La profundidad del árbol y la cantidad mínima de hojas que va a tener que valorar antes de tomar una decisión. El resultado es de 1 y 3 respectivamente en ambas predicciones.
param_test1 = { 'max_depth':range(1,10,1), 'min_child_weight':range(1,6,1) }
- Gamma
La mínima perdida de información que necesitaremos para crear nuevas hojas. El resultado fue de 0.0 en ambas.
param_test3 = { 'gamma':[i/10.0 for i in range(0,5)] }
- Subsample
Cantidad de muestras que se usarán para hacer crecer el árbol. Los datos son 0.9 para el tiempo y 0.8 para las cornadas
param_test4 = { 'subsample':[i/10.0 for i in range(6,10)], 'colsample_bytree':[i/10.0 for i in range(6,10)] }
- Colsample_bytree
Cantidad de subconjunto de columnas que usará para crear los diferentes árboles. Los valores son 0.7 para el tiempo y 0.6 para las cornadas
param_test5 = { 'subsample':[i/100.0 for i in range(75,90,5)], 'colsample_bytree':[i/100.0 for i in range(75,90,5)] }
- Reg_alpha
Parámetro que usa para regularizar los pesos. El resultado es de 1e-05 para el tiempo y 0 para las cornadas
param_test6 = { 'reg_alpha':[1e-5, 1e-2, 0.1, 1, 2, 3 , 4, 5, 6, 7, 8, 9, 10, 20 ,30 ,40, 50, 60, 100] }
Resultado
Después de los ajustes realizados, probamos a dividir los datos y ver que tal predecía nuestro modelo
Datos reales frente a predicción
Santo Domingo
[0 0 1 0 0 0 0 0 0 0 0 0 0 0 1 0 1 1 0 1]
[0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0]
Ayuntamiento
[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1]
[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
Mecaderes:
[1 0 1 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 0 0]
[0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
Estafeta:
[0 0 2 0 0 0 1 1 0 0 0 1 0 0 0 0 0 1 0 1]
[0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 1]
Telefónica
[2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0]
[1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
Bajada Callejón
[0 0 0 0 0 0 0 0 0 0 0 0 2 0 0 0 0 0 0 0]
[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
Callejón
[1 0 1 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0]
[1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
Plaza de toros
[0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0]
[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
Como podemos ver el modelo nunca predice más de una cornada y podríamos decir que es tan conservador que casi nunca considera que vayan a suceder heridas por asta.
Respecto al tiempo los resultados fueron los siguientes
Los datos reales
[268 172 300 143 141 143 210 383 173 182 137 134 262 150 173 246 145 143
148 178]
Datos predecidos
[143 172 143 143 141 166 150 166 172 143 141 141 158 150 158 166 150 158
166 150]
Vemos que no hay mucho acierto debido a que el modelo a penas varía su resultado y es incapaz de predecir un encierro largo de forma correcta. Probablemente esto se deba a que es muy difícil predecir cual va a ser el comportamiento de los astados en las calles de Pamplona y no siempre una ganadería o unas variables tienen como fin el mismo resultado.
Conclusión
Viendo el resultado y los problemas que hemos tenido podemos sacar varias conclusiones:
- La primera y más importante es que el encierro es incierto si solo tenemos en cuenta las variables que nosotros hemos considerado o hemos podido conseguir. A igualdad de valores de entrada dos encierros pueden tener resultados muy diferentes.
- La segunda sería que que una de las partes más importantes es la recolección de datos y variables que en este caso ha sido muy difícil de conseguir y la cantidad de datos que se han conseguido, son insuficientes para predecir que es lo que va a suceder.
.
.
.
Este artículo es parte de la serie relatando nuestra experiencia en la construcción del proyecto San Fermín en directo.