{"cells":[{"metadata":{},"cell_type":"markdown","source":"# Script para generar la solución \"Benchmark\""},{"metadata":{},"cell_type":"markdown","source":"## Ejemplo de Procesamiento de los Datos, Modelado Simple y Generación de una Solución para la usar de referencia para nuevos participantes que inicien el desafío de este Dataton"},{"metadata":{},"cell_type":"markdown","source":"**Hola! **  \n  \nEste Script está basado en la solución \"Benchmark\" subida en un Kernel por **Rafael Crescenzi**.  \n  \nEs la misma ejecución agregando una pequeña explicación de lo que se hace en cada paso para ayudar a los que están un poco perdidos :-)"},{"metadata":{},"cell_type":"markdown","source":"### Importamos las librerías que vamos a utilizar"},{"metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true},"cell_type":"code","source":"import numpy as np # Biblioteca de funciones matemáticas de alto nivel para operar con esos vectores o matrices\nimport pandas as pd # Manipulación y análisis de datos, Data Frames, lectura de CSV \nfrom lightgbm import LGBMClassifier # Implementación de un algoritmo de Boosting de Arboles de Decisión con descenso de gradiente\nfrom sklearn import model_selection # Separación en Train y Test\nfrom sklearn.metrics import roc_auc_score # Calculo del Area bajo la Curva ROC\n","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"### Lectura de las Bases"},{"metadata":{},"cell_type":"markdown","source":"Observamos los datos que tenemos disponibles en https://www.kaggle.com/c/banco-galicia-dataton-2019/data\n\nVemos que tenemos, por un lado, dos archivos principales del comportamiento de los clientes:\n- pageviews.csv - Que nos trae los datos de navegación de 2018\n- device_data.csv - Que nos trae los datos de los dispositivos asociados a los usuarios\n\nCon estos 2 archivos deberemos trabajar para lograr generar variables explicativas que expliquen el comportamiento que estamos tratando de predecir: la propensión a la conversión\n\nPor otro lado tenemos 4 archivos que nos sirven para indexar variables que están codificadas en el archivo de **pageviews** y que se condicen los nombres de las variables con los archivos.\n- CONTENT_CATEGORY.csv\n- CONTENT_CATEGORY_TOP.csv\n- CONTENT_CATEGORY_BOTTOM.csv\n- SITE_ID.csv\n\nEstas variables de tipo \"texto\" no nos aportan valor en si mismas de forma directa, por lo que no se usan en el código del Benchmark, pero pueden ser útiles como complemento explicativo a la hora de construir variables entendiendo las distintas categorías y eventos relacionados que los disparan."},{"metadata":{"_cell_guid":"79c7e3d0-c299-4dcb-8224-4455121ee9b0","_uuid":"d629ff2d2480ee46fbb7e2d37f6b5fab8052498a","trusted":true},"cell_type":"code","source":"# Leemos el archivo \"pageviews\" y observamos las primeras filas\ndata = pd.read_csv(\"../input/pageviews/pageviews.csv\",\n                   parse_dates=[\"FEC_EVENT\"]) # Le indicamos que la columna FEC_EVENT debe leerse como tipo Fecha\ndata.head()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Como podemos ver, la primera columna corresponde a la fecha del evento, incluyendo  la hora, minuto y segundo en que se realizó.  \n  \nEn principio, esta variable va a ser la que utilicemos para separar nuestras bases de entrenamiento y testeo e intentaremos  construir un esquema similar a lo que luego vamos a tener que predecir en la submission.  \n  \nLas demás variables que tenemos son:\n- La codificación de las páginas que visitó el usuario\n- La categoría que corresponde\n- El ID del sitio\n- El termino buscado en la búsqueda si corresponde\n- El ID del usuario\n"},{"metadata":{},"cell_type":"markdown","source":"### Agrupamos la navegación por Usuario - Base de Predicción Final"},{"metadata":{},"cell_type":"markdown","source":"Ahora, como la predicción que tenemos que hacer es a nivel Usuario, vamos a realizar un agrupamiento de toda la navegación de los mismos, de forma que nos quede la misma cantidad de filas que los usuarios que tenemos en la base de entrenamiento.  \n  \nPara ello vamos a crear el objeto \"X_test\" que luego vamos a usar como base para generar nuestra predicción final, ya que contiene todas las filas del entrenamiento (y lógicamente no incluye al target).  \n  \nLuego, para cada una de las variables explicativas que tenemos (PAGE, CONTENT_CATEGORY, CONTENT_CATEGORY_TOP, CONTENT_CATEGORY_BOTTOM, SITE_ID, ON_SITE_SEARCH_TERM) vamos a :\n- Sumar su frecuencia de ocurrencia de cada valor de cada una de las variables \n- Y luego calcular la proporción de frecuencia de cada valor posible, en relación con todos los valores que puede tomar la variable (ej: para PAGE = 1, sumamos la cantidad de veces que el usuario visitó la PAGE 1 y luego lo dividimos por el total de visitas que hizo ese usuario a todas las PAGE)\n  \nPor ultimo, nos quedamos con una tabla que tiene 2.166 columnas, que corresponden a la proporción de frecuencia para cada usuario de cada uno de los valores de cada una de estas variables explicativas"},{"metadata":{"trusted":true},"cell_type":"code","source":"X_test = [] # Primero creamos el objeto vacío\nfor c in data.drop([\"USER_ID\", \"FEC_EVENT\"], axis=1).columns: # iteramos sobre todas las columnas de \"data\", menos la fecha y el Id de Usuario\n    print(\"haciendo\", c) # Mostramos en que variable está trabajando el loop\n    temp = pd.crosstab(data.USER_ID, data[c]) # * Realizamos una tabla cruzada de la Variable por Usuario colocando la frecuencia de cada valor posible como columna \n    temp.columns = [c + \"_\" + str(v) for v in temp.columns] # El nombre de cada columna lo renombramos como: Variable + \"_\" + Valor de la Variable\n    X_test.append(temp.apply(lambda x: x / x.sum(), axis=1)) # Aplicamos una función lambda para calcular la proporción de frecuencia de cada variable\nX_test = pd.concat(X_test, axis=1) # Concatenamos todas las variables en el mismo objeto","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# SOLO A MODO DE EXPLICACIÖN DEL CODIGO ANTERIOR\n# * Podemos ver un ejemplo con PAGE de como se ve la ejecución solo de esta linea, armando la tabla de contingencia y graficando las primeras 5 filas\npd.crosstab(data.USER_ID, data[\"PAGE\"]).head()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Podemos ver las primeras lineas del objeto X_test para comprobar como quedó construido"},{"metadata":{"trusted":true},"cell_type":"code","source":"X_test.head()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Si observamos el tamaño del objeto vemos que tiene:\n- 11.676 filas: Que corresponde a la cantidad de Usuarios de la base\n- 2.166 columnas: Que corresponde a la proporción de frecuencia para cada usuario de cada uno de los valores de cada una de estas variables"},{"metadata":{"trusted":true},"cell_type":"code","source":"X_test.shape","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Si sumamos los valores de, por ejemplo, las primeras 1725 columnas (correspondientes a PAGE) del primer registro, la suma debería dar **1**"},{"metadata":{"trusted":true},"cell_type":"code","source":"X_test.iloc[0,0:1725].sum()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"### Agrupamos la navegación por Usuario - Base de Entrenamiento"},{"metadata":{},"cell_type":"markdown","source":"Ahora hacemos el mismo agrupamiento, pero para armar una base de entrenamiento, donde solo vamos a permitirle al modelo observar las navegaciones hasta Octubre 2018 (no inclusive).  \n  \nEsto significa que nuestra variable dependientes (Y) van a ser las conversiones desde Octubre en adelante, para simular la situación que vamos a tener que predecir en la competencia, donde con datos hasta Diciembre 2018 vamos a predecir las conversiones en los primeros meses de 2019"},{"metadata":{"trusted":true},"cell_type":"code","source":"data = data[data.FEC_EVENT.dt.month < 10] # Limitamos los registros a eventos anteriores a Octubre (mes 10)\nX_train = [] # Creo un objeto vacío para Train\nfor c in data.drop([\"USER_ID\", \"FEC_EVENT\"], axis=1).columns: # Repito el proceso que vimos anteriormente\n    print(\"haciendo\", c)\n    temp = pd.crosstab(data.USER_ID, data[c])\n    temp.columns = [c + \"_\" + str(v) for v in temp.columns]\n    X_train.append(temp.apply(lambda x: x / x.sum(), axis=1))\nX_train = pd.concat(X_train, axis=1)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Ahora que tenemos ambas bases construidas, vamos a filtrarlas quedandonos con las columnas que existen en ambos datasets, para poder entrenar y predecir sobre los mismos atributos."},{"metadata":{"trusted":true},"cell_type":"code","source":"features = list(set(X_train.columns).intersection(set(X_test.columns))) # Creamos una lista con las columnas que se encuentran en ambos datasets\nX_train = X_train[features] # Filtramos en el dataset de Train las columnas que son comunes a ambos\nX_test = X_test[features] # Filtramos en el dataset de Test las columnas que son comunes a ambos","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Volvemos a mirar ahora las primeras filas de la base de entrenamiento y vemos que ahora tenemos muchas menos columnas, ya que solo quedaron las que existen en ambas bases.  \n  \nRecordemos que esto surge de una separación \"arbitraria\" que hicimos de seprar a partir de Octubre para Validar, si el corte fuera distinto (en otro mes) podrían ser otras las columnas \"coincidentes\"."},{"metadata":{"trusted":true},"cell_type":"code","source":"X_train.head()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Ahora importamos el archivo **conversiones.csv** que tiene la variable objetivo (Y) para el entrenamiento de nuestro modelo, que corresponde a las conversiones realizadas durante 2018  \n  \n  Luego creamos un objeto **y_train** con las conversiones que queremos predecir en nuestro modelo de entrenamiento (Oct-Dic 18)"},{"metadata":{"trusted":true},"cell_type":"code","source":"y_prev = pd.read_csv(\"../input/conversiones/conversiones.csv\") # Leemos el archivo CSV\ny_train = pd.Series(0, index=X_train.index) # Creamos un objeto para cada Usuario con valor cero en todos los casos\nidx = set(y_prev[y_prev.mes >= 10].USER_ID.unique()).intersection(\n        set(X_train.index)) # Buscamos a los Usuarios que hayan convertido de Octubre en adelante\ny_train.loc[list(idx)] = 1 # Asignamos el valor \"1\" a los casos que crucen con el objeto creado antes","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Podemos ver las primeras 23 lineas de este archivo y ver que el usuario con ID 22 convirtió (valor = 1) mientras que los demás no"},{"metadata":{"trusted":true},"cell_type":"code","source":"y_train.head(23)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"### Entrenamiento del Modelo"},{"metadata":{},"cell_type":"markdown","source":"Ahora si, llegó la hora de entrenar el modelo!  \n  \nVamos a separar la base de entrenamiento en 10 folds para poder hacer la validación, utilizando como target las conversiones de los Usuarios que están contenidos en ese fold determinado, en los meses que separamos para validar (Oct-Dic).  \n    \nPara entrenar el modelo vamos a utilizar LightGBM (gradient boosting de árboles de decisión) que es una librería desarrollada por Microsoft. podemos ver la documentación acá: https://lightgbm.readthedocs.io  \n  \nPor el momento no vamos cambiar los parámetros que utiliza LightGBM por default, por lo que solo vamos a entrenar un modelo para cada \"sub-base\" iterando hasta un máximo de 10.000 veces, pero deteniendo el modelo cuando no haya una mejora en el score de validación durante más de 10 iteraciones.  \n  \nFinalmente vamos a guardar las probabilidades medias estimadas y lo vamos a preparar para hacer la subida correspondiente en la plataforma"},{"metadata":{"trusted":true},"cell_type":"code","source":"# Entrenamos el modelo LGBM Classifier\n\nfi = [] # Creamos un objeto vacío para guardar la importancia de las variables de los modelos entrenados\ntest_probs = [] # Creamos un objeto vacío para guardar las probabilidades estimadas\ni = 0 \nfor train_idx, valid_idx in model_selection.KFold(n_splits=10, shuffle=True).split(X_train): # Iterams sobre 10 folds que creamos para entrenar y validar\n    i += 1\n    Xt = X_train.iloc[train_idx] # Definimos el set de entrenamiento a partir de los indices que coincidan con los 9 folds de entrenamiento\n    yt = y_train.loc[X_train.index].iloc[train_idx] # Definimos el Target de entrenamiento con los Usuarios que coincidan con los que se encuentran en estos folds\n\n    Xv = X_train.iloc[valid_idx] # Definimos el set de validación a partir los indices que coincidan con el fold de validación\n    yv = y_train.loc[X_train.index].iloc[valid_idx] # Definimos el Target de validación con los Usuarios que coincidan con los que se encuentran en este fold\n\n    learner = LGBMClassifier(n_estimators=10000) # Entrenamos un modelo lightgbm con los parámetros por default usando un máximo de 10.000 árboles\n    learner.fit(Xt, yt,  early_stopping_rounds=10, eval_metric=\"auc\",\n                eval_set=[(Xt, yt), (Xv, yv)]) # Definimos un early stop de 10 y el método de evaluación como AUC\n    \n    test_probs.append(pd.Series(learner.predict_proba(X_test)[:, -1],\n                                index=X_test.index, name=\"fold_\" + str(i))) # Predecimos sobre la base total y nos guardamos las probabilidades estimadas\n    fi.append(pd.Series(learner.feature_importances_ / learner.feature_importances_.sum(), index=Xt.columns)) # Guardamos la proporción de importancia de cada variable en el modelo\n\ntest_probs = pd.concat(test_probs, axis=1).mean(axis=1) # Caluclamos la media de las probabilidades\ntest_probs.index.name=\"USER_ID\" # Renombramos las columnas\ntest_probs.name=\"SCORE\" # Renombramos las columnas\ntest_probs.to_csv(\"benchmark.zip\", header=True, compression=\"zip\") # Guardamos la predicción final en un zip para subirlo a la plataforma\nfi = pd.concat(fi, axis=1).mean(axis=1) # Como explicación del modelo guardamos la importancia de las variables de los modelos entrenados","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Vemos finalmente las probabilidades que calculamos y vamos a subir en nuestro submition:"},{"metadata":{"trusted":true},"cell_type":"code","source":"test_probs","execution_count":null,"outputs":[]}],"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"name":"python","version":"3.6.4","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"}},"nbformat":4,"nbformat_minor":1}