{"cells":[{"metadata":{},"cell_type":"markdown","source":"# Selección de features\n\nEl propósito de este notebook es indagar en el proceso de **selección de features** y sus efectos en **modelos lineales** y en **modelos basados en árboles de decisión**. A diferencia de los modelos basados en árboles de decisión, el desempeño de los modelos lineales se ve significativamente afectado por la presencia de features redundantes (**multicolinearidad**). Por este motivo, existen numerosos procedimientos para la identificación y eliminación de estas features. \n\nEn particular, el método empleado para la selección es simple y artesanal pero muy ilustrativo, con el objetivo de tener el máximo control del proceso de selección y, a la vez, familiarizarnos con el dominio muestral, lo que nos facilitará el proceso de creación de features.  "},{"metadata":{"trusted":true,"collapsed":true},"cell_type":"code","source":"import pandas as pd\nimport numpy as np\n\n# métrica y datasets\nfrom sklearn.metrics import roc_auc_score\nfrom sklearn.model_selection import train_test_split\n\n# modelos\nfrom xgboost import XGBClassifier\nfrom sklearn.linear_model import LogisticRegression\n\n# feature engineering\nfrom sklearn.feature_selection import f_classif\nfrom sklearn.preprocessing import StandardScaler\n\n# visualización\nfrom seaborn import heatmap\n\nimport os\nprint(os.listdir(\"../input\"))","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"## Creación de datasets \n\nLos datasets serán creados de la misma forma que en el notebook del benchmark"},{"metadata":{"trusted":true,"collapsed":true},"cell_type":"code","source":"data = pd.concat([\n       pd.read_csv(\"../input/banco-galicia-dataton-2019/pageviews/pageviews.csv\", parse_dates=[\"FEC_EVENT\"]),\n       pd.read_csv(\"../input/banco-galicia-dataton-2019/pageviews_complemento/pageviews_complemento.csv\", parse_dates=[\"FEC_EVENT\"])\n])\n\n# Test\nX_test = []\nfor c in data.drop([\"USER_ID\", \"FEC_EVENT\"], axis=1).columns:\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_test.append(temp.apply(lambda x: x / x.sum(), axis=1))\nX_test = pd.concat(X_test, axis=1)\n\n# Train\ndata = data[data.FEC_EVENT.dt.month < 10]\nX_train = []\nfor c in data.drop([\"USER_ID\", \"FEC_EVENT\"], axis=1).columns:\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)\n\nfeatures = list(set(X_train.columns).intersection(set(X_test.columns)))\nX_train = X_train[features]\nX_test = X_test[features]\n\ny_prev = pd.read_csv(\"../input/banco-galicia-dataton-2019/conversiones/conversiones.csv\")\ny_train = pd.Series(0, index=X_train.index)\nidx = set(y_prev[y_prev.mes >= 10].USER_ID.unique()).intersection(\n        set(X_train.index))\ny_train.loc[list(idx)] = 1","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"## Definición de funciones útiles"},{"metadata":{"trusted":true},"cell_type":"code","source":"def model_auc_score(model, X_t, y_t, X_v, y_v):\n    model.fit(X_t, y_t)\n    y_pred_t = model.predict_proba(X_t)[:,1]\n    y_pred_v = model.predict_proba(X_v)[:,1]\n    auc_t = roc_auc_score(y_t, y_pred_t)\n    auc_v = roc_auc_score(y_v, y_pred_v)\n    return auc_t, auc_v\n\ndef report_model(label, auc_t, auc_v):\n    print('------- ' + label + '  --------')\n    print('Training auc:   ' + str(auc_t))\n    print('Validation auc: ' + str(auc_v))\n    \ndef normalize(x_train, x_test):\n    scaler = StandardScaler()\n    x_train = scaler.fit_transform(x_train)\n    x_test = scaler.transform(x_test)\n    return x_train, x_test","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"## Evaluación sin selección de features\n\nEn esta etapa inicial evaluaremos los modelos directamente sobre el dataset, la única operación que realizaremos es la de **estandarización**.\n* Modelo lineal: **LogisitcRegression**\n* Modelo basado en árboles de decisión: **XGBClassifier(booster = 'gbtree')**\n\n*Hay que poner especial cuidado al hacer la estandarización, ya que tiene que ajustar sus parámetros con el training (**fit_transform**) y luego transformar (**transform**) el test. Por lo que hay que dividir nuestro dataset antes.*"},{"metadata":{"trusted":true},"cell_type":"code","source":"SEED = 18 # mi número de la suerte :)\nx_train = X_train.values\ny_tr = y_train.values\n\n# dividiendo el dataset\nX_t, X_v, y_t, y_v = train_test_split(x_train, y_tr, test_size=0.20, random_state=SEED)\n# normalizando\nX_t, X_v = normalize(X_t, X_v)\n\n# logistic regression\nlr = LogisticRegression(random_state=SEED)\nauc_t, auc_v = model_auc_score(lr, X_t, y_t, X_v, y_v)\nreport_model('Logistic Regression', auc_t, auc_v)\n\n\n# xgb\nxgb = XGBClassifier(random_state=SEED, booster='gbtree')\nauc_t, auc_v = model_auc_score(xgb, X_t, y_t, X_v, y_v)\nreport_model('XGB', auc_t, auc_v)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Los resultados hablan por si solos!\n* **XGB** tuvo buen score de validación (comparándolo con el public leaderboard, podés probarlo ;) ) \n* **Logistic Regression** tuvo pésimo score de validación y muy bueno de training, lo cuál es un claro signo de **overfitting**\n\nPara mitigar este efecto hay varias estrategias: agregar **regularización** (C), tunear los **hyperparámetros** del modelo o **eliminar features** redundantes y/o irrelevantes. En este kernel se explora el efecto de esta última a través de un método artesanal y simple. "},{"metadata":{},"cell_type":"markdown","source":"## Selección de Features"},{"metadata":{},"cell_type":"markdown","source":"Bien, nuestro objetivo es eliminar aquellas features que sean **irrelevantes** y/o **redundantes**. Una feature es irrelevante cuando está pobremente correlacionada (asociada, para features categóricas) con el **target**. Por otro lado, una feature es redundante cuando su información es expresada, parcial o completamente, por otra(s) feature(s) lo que se traduce en una alta **correlación** entre ellas."},{"metadata":{},"cell_type":"markdown","source":"### Correlación con el target ANOVA f-test"},{"metadata":{},"cell_type":"markdown","source":"Ahora, cómo medimos la correlación entre una variable **numérica** (nuestras features) y una variable **categórica** (el target)? \n\nEn este notebook, usaremos el test estadísitco-f para análisis de varianza **ANOVA**. Este test, básicamente, asigna un score a la variación entre las medias obtenidas al agrupar los valores numéricos en las distintas categorías de la variable categórica. A mayor score mayor **\"correlación\"** entre la feature numérica y la categórica."},{"metadata":{"trusted":true},"cell_type":"code","source":"features = X_train.columns\nf_scores = f_classif(X_train, y_train)[0] # el [1] son los p-values.\n\ndf_fscores = pd.DataFrame({'features':features,'score':f_scores})\ndf_fscores = df_fscores.sort_values('score', ascending = False)\ndf_fscores.head(10)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Veamos cuáles son esas features que están más correlacionadas con el target?\n* **PAGE_110: /PRESTAMOS/SOLICITAR**\n* **PAGE_109: /PRESTAMOS/SOLICITAR-SELECCIONAR-SUBTIPO-PRESTAMO-PER**\n* **PAGE_41 : /PRESTAMOS/INICIO**\n* **PAGE_108: /PRESTAMOS/SOLICITAR-SELECCIONAR-TIPO-PRESTAMO**\n* **PAGE_345: /PRESTAMOS/SOLICITAR-EXITO**\n* **PAGE_285: /PRESTAMOS/SOLICITAR-SELECCIONAR-DESTINO**\n* **PAGE_287: /PRESTAMOS/SOLICITAR-PRECONFIRMACION**\n* **PAGE_1259: /PRESTAMOS/SOLICITAR_SELECCIONAR_SUBTIPO_PRESTAMO_PER**\n* **CONTENT_CATEGORY_16: NO CATEGORY ASSIGNED > HB : PP** \n\nInteresante! La mayoría de las features con mayor correlación son urls con el prefijo: **/PRESTAMOS/**. Seguramente habrá **colinearidad** entre ellas, ya que es muy probable que para llegar a una el usuario tenga que haber pasado por la otra antes. En particular, la **PAGE_110** es la misma que la **PAGE_1259**, la diferencia está en los separadores... puede ser interesante tomar la suma de las visitas a esas dos y expresarla como una... animate :)"},{"metadata":{"trusted":true,"collapsed":true},"cell_type":"code","source":"# eliminando las features que no son relevantes\nLIM_IRR = 10 # ajustable\ndf_fscores = df_fscores[df_fscores['score'] > LIM_IRR]\nX_sel = X_train[df_fscores['features']]\nX_sel.shape","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"### Correlación lineal entre features (Multicolinearidad)"},{"metadata":{"trusted":true},"cell_type":"code","source":"xcorr = X_sel.corr().abs()\nheatmap(xcorr)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"De la gráfica vemos que hay alta correlación entre algunas features (zonas blancas), lo que significa que hay muchas features **redundantes** que brindan la misma información. Entonces, cómo las eliminamos? Se pueden probar varias estrategias, como agruparlas, en este kernel eliminaremos aquellas features redundantes y con menor relevancia, o sea con menor correlación con el target."},{"metadata":{"trusted":true},"cell_type":"code","source":"# seleccionando las features redundantes\nLIM_COR = 0.9 # ajustable\nxcorr = xcorr[xcorr > LIM_COR].fillna(0)\nindex = []\ncolumn = []\nfor idx in list(xcorr.index):\n    for col in list(xcorr.columns):\n        # la matriz es diagonal\n        if idx == col:\n            break\n        if (xcorr.loc[idx,col] != 0):\n            index = index + [idx]\n            column = column + [col]\ndf_fcorr = pd.DataFrame({'index':index, 'col':column})\ndf_fcorr.head(10)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"collapsed":true},"cell_type":"code","source":"# dropeamos las features correlacionadas que estan menos correlacionadas con el target\nfor idx in df_fcorr.index:\n    f_idx = df_fcorr.loc[idx,'index']\n    f_col = df_fcorr.loc[idx,'col']\n    score_idx = df_fscores.loc[df_fscores['features'] == f_idx, 'score'].ravel()\n    score_col = df_fscores.loc[df_fscores['features'] == f_col, 'score'].ravel()\n    if score_idx > score_col:\n        df_fcorr.loc[idx, 'drop'] = f_col\n    else:\n        df_fcorr.loc[idx, 'drop'] = f_idx \ndrop_features = list(df_fcorr['drop'].unique())\n\nX_sel.drop(columns=drop_features, inplace=True)\nX_sel.shape","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"## Evaluando de nuevo los modelos"},{"metadata":{"trusted":true},"cell_type":"code","source":"x_sel = X_sel.values\nX_t, X_v, y_t, y_v = train_test_split(x_sel, y_tr, test_size=0.20, random_state=SEED)\nX_t, X_v = normalize(X_t, X_v)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"lr = LogisticRegression(random_state=SEED)\n[auc_t, auc_v] = model_auc_score(lr, X_t, y_t, X_v, y_v)\nreport_model('Logistic Regression', auc_t, auc_v)\n\nxgb = XGBClassifier(random_state=SEED)\n[auc_t, auc_v] = model_auc_score(xgb, X_t, y_t, X_v, y_v)\nreport_model('XGB', auc_t, auc_v)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"## Conclusiones\n\nCon este proceso simple de selección de features, logramos reducir la dimensionalidad del dataset a la vez que mejoramos drásticamente el desempeño de Logistic Regression. Por otro lado, aunque el desempeño de XGB no mejoró significativamente, ahora se pueden tunear sus parámetros de forma rápida y efectiva.\n\n* El desempeño de **Logistic Regression** mejoró significativamente (0.66 -> 0.82).\n* El desempeño de **XGB** también mejoró pero en menor medida (0.85 -> 0.86), esto se debe a que al ser un modelo basado en árboles de decisión la multicolinearidad no lo afectaba.\n\n## Próximos Pasos\n\n* Tratar el **skewness** de las features para Logistic Regression. \n* Añadir **regularización** al Logistic Regression.\n* Tunear los **hyperparámetros** de ambos modelos.\n* **Crear features** con la información del dominio ganada.\n* Hacer **stack ensemble** con estos y otros modelos."}],"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"codemirror_mode":{"name":"ipython","version":3},"file_extension":".py","mimetype":"text/x-python","name":"python","nbconvert_exporter":"python","pygments_lexer":"ipython3","version":"3.7.1"}},"nbformat":4,"nbformat_minor":1}