En este documento se presenta el trabajo desarrollado en el curso de Introducción a la Minería de datos de la Universidad de Chile.
El proyecto correspondió a estudiar el dataset del banco Santander, con el objetivo de predecir qué nuevos productos contratarían los clientes.
En la competencia propuesta por el banco, se entregó 1,5 años de historial y se esperaba la predicción para el mes siguiente.
Actualmente Santander ayuda a sus clientes en sus decisiones financieras a través de recomendaciones personalizadas de productos.
El problema reside en que hay un pequeño grupo, que recibe muchas recomendaciones, y otro grande que raramente recibe alguna. Generando una desigual experiencia de cliente.
Con esto, el desafío propuesto por el banco es predecir cuáles productos contratarán sus clientes el siguiente mes basándose en su historial y similitud a otros clientes.
Con una recomendación más efectiva, Santander puede asistir de mejor manera las necesidades individuales y asegurar su satisfacción. Así, el cliente y el banco se verían beneficiados.
Objetivo:
Predecir qué contratará un cliente además de lo que ya tenía el mes anterior.
Los datos del set de entrenamiento se pueden resumir en los puntos siguientes.
A continuación se presenta la información más relevante que se encontró en la exploración inicial.
Se tiene un total de ~950 mil clientes, de los cuales:
Relaciones entre atributos
Para poder visualizar la relación existente entre los diferentes atributos, se debió transformar los caracteres y strings a valores numéricos, para así poder generar una matriz de correlación.
Para esto, se asignaron números a las categorías definidas por caracteres, si alguna de ellas poseía algún orden natural se mantuvo numéricamente. También, para las fechas, se eliminaron los “-” que separaban año, mes y día, y luego se transformaron a entero.
Luego, se obtuvo la matriz siguiente.
Dentro de las observaciones que se hicieron, las más relevantes fueron:
Por otro lado, es importante notar que hay relaciones que no se observan en la matriz de correlación. Pues por ejemplo para la variable canal de entrada (categórica) y edad, no se nota correlación alguna. Pero gracias al gráfico[1] a la derecha, se puede ver claramente que si existe relación. Esto puede deberse a la conversión numérica que fue hecha.
na_count <-sapply(x, function(y) sum(length(which(is.na(y)))))
Atributo | Cantidad NA |
---|---|
fecha_dato | 0 |
ncodpers | 0 |
ind_empleado | 0 |
pais_residencia | 0 |
sexo | 0 |
age | 27734 |
fecha_alta | 0 |
ind_nuevo | 27734 |
antiguedad | 27734 |
indrel | 27734 |
ult_fec_cli_1t | 0 |
indrel_1mes | 0 |
tiprel_1mes | 0 |
indresi | 0 |
indext | 0 |
conyuemp | 0 |
canal_entrada | 0 |
indfall | 0 |
tipodom | 27735 |
cod_prov | 93591 |
nomprov | 0 |
ind_actividad_cliente | 27734 |
renta | 2794375 |
segmento | 0 |
ind (los primeros 21 productos) | 0 |
ind_nomina_ult1 | 16063 |
ind_nom_pens_ult1 | 16063 |
ind_recibo_ult1 | 0 |
Se cambió el formato de string a fecha, y se creó un atributo mes, porque se cree puede haber correlación entre el mes y la probabilidad de contratación de algún servicio (por ejemplo, por períodos de ingreso escolar, pago de seguros etc).
Además, los null de fecha_alta, correspondiente a la fecha en que el cliente se convirtió en titular de la cuenta, fueron rellenados con la mediana, de tal forma que no influyan en cálculos futuros, pero que tampoco interfieran por ser valores no numéricos.
> quantile(df$age, prob = seq(0, 1, length = 11), type = 5, na.rm = TRUE)
0% 10% 20% 30% 40% 50% 60% 70% 80% 90% 100%
2 22 23 26 32 39 43 48 54 64 164
Luego de observar que los menores de 18 años representan un 0.8% del dataset, y que los mayores de 100 un 0.09%, se decidió manejar estos outliers, asignándoles la media de 18-30 y 30-100 respectivamente.
Además, se asignó la mediana a los valores NA.
> months.active <- df[is.na(df$ind_nuevo),] %>%
+ group_by(ncodpers) %>%
+ summarise(months.active=n()) %>%
+ select(months.active)
> max(months.active)
[1] 6
Se consideraron las observaciones sin indicador de cliente nuevo o no. Éstas, fueron agrupadas por código de cliente, sumando la cantidad de apariciones. Luego, dado que se tiene la información de las acciones de cada mes por cliente, se concluye que todos corresponden a nuevos, pues el máximo es 6 meses. Por lo tanto, se les asigna el correspondiente valor 1.
Se obtuvo exactamente la misma cantidad de null que en ind_nuevo, por lo que al analizar el atributo ind_nuevo de estos, se observó que son todos 1, con lo que probablemente corresponden a las observaciones que se acaban de completar en el punto anterior. Se podría completar con valor de antigüedad 1, pero dado que el número 27735 se repite una y otra vez, seguramente corresponden a las mismas observaciones. Dado que además representan el 0.02% del dataset, se decidió eliminarlas.
> x<-df[df$ult_fec_cli_1t=="", ]
> nrow(x)/nrow(df)
[1] 0.9981796
Debido a que se desconoce el 99,8% de este atributo, se decidió eliminarlo.
> table(df$tipodom)
1
13619574
1 si es la dirección principal. Como se ve, todos los valores son 1, y los que no nulos. Por lo tanto, se decidió eliminar este atributo, ya que no entrega ninguna información.
> u <- df[is.na(df$cod_prov)]
> v <- u[u$nomprov == ""]
> nrow(u)
[1] 93591
> nrow(v)
[1] 93591
Se notó que los códigos que son nulos no tienen nombre tampoco, por lo cual no se pueden completar. Por lo que fueron completados con valores -1.
Dado que el código entrega la información de la provincia, se decidió eliminar el atributo nomprov (luego de ser utilizado para el gráfico de renta para mejor visualización).
> sum(is.na(df$renta))/nrow(df)
[1] 0.2047565
Los valores nulos representan el 20% del dataset. Por lo tanto no puede ser simplemente reemplazado por la mediana total, debemos encontrar algún valor más representativo para asignar.
Para esto, se decidió estudiar la renta por provincia. Se utilizó la función aggregate y se obtuvo lo siguiente.
> summary(aggregated_output$renta)
Min. 1st Qu. Median Mean 3rd Qu. Max.
62190 69110 82450 86480 95100 139100
Así, se observa que los valores de renta varían de forma considerable por región. Luego, asignar la renta media por región a los valores faltantes es representativo.
Menos de 0.0001% no tiene valor definido, por lo tanto se asignó 0 (no contratado) a esos valores.
Se cambiaron los strings vacíos por el string ”UNKNOWN”, otros con un valor por defecto, o por la clase mayoritaria (se muestran algunos ejemplos a continuación).
Además se formateó a enteros el atributo indrel_1mes que contenía strings de números y de caracteres.
df$indfall[df$indfall==""] <- "N" # no ha muerto
df$tiprel_1mes[df$tiprel_1mes==""] <- "A" # activo
df$indrel_1mes[df$indrel_1mes==""] <- "1" # mayoritaria
df$pais_residencia[df$pais_residencia==""] <- "UNKNOWN"
df$sexo[df$sexo==""] <- "UNKNOWN"
df$ult_fec_cli_1t[df$ult_fec_cli_1t==""] <- "UNKNOWN"
Debido a que el objetivo principal es predecir qué productos va a contratar una persona en Junio del 2016, notamos que esto puede ser resuelto con algoritmos de clasificación.
Primero se intentó probar una clasificación simple y ver qué resultaba. Para esto se debió transformar los atributos a factores (categóricos) o numéricos. Luego, se entrenó con los primeros 4 meses la predicción del atributo ind_nom_pens_ult1 (Pension), y se predijo el 5to mes. El modelo obtenido se muestra a continuación.
Su matriz de confusión fue la siguiente:
- | 0 | 1 |
---|---|---|
0 | 582941 | 282 |
1 | 2969 | 42168 |
Con lo cual se obtuvo un accuracy = 0.99. Dado que este valor es en realidad demasiado alto, se observaron los valores de algunos clientes, y se concluyó que podría estar pasando que el cliente contrató el servicio el primer mes y luego lo mantuvo durante todos los meses. Entonces el modelo está prediciendo bien, el cliente mantiene ese servicio el siguiente mes. Pero esto no es precisamente lo que deseamos predecir, ya que fue solicitado los productos adicionales, es decir que son contratdos por primera vez ese mes.
Luego, se decidió cambiar el formato en que estaban representados los servicios.
Nuevo formato:
Un servicio tiene un 1 si fue contratado en esa fecha, 0 si no (si se mantiene o elimina).
También, se decidió entrenar con subconjuntos cada vez mas grandes, para así evaluar la capacidad de predicción dado distintos tamaños de dataset. De esta forma, se verá cómo reacciona el modelo ante distinta carga de entrenamiento y visualizar posibles overfitting y underfitting.
Para preparar los datos, se hizo un mapeo desde strings a valores numéricos de forma manual[3], asegurando así el impacto de las variables categóricas que son catalogadas como strings en el dataset.
Luego, se dividió el dataframe en los conjuntos relevantes para nuestras hipótesis y se ejecutó Naive Bayes Gaussiano de la librería de Python, sklearn, entrenando la prediccion la columna ind_nomina_ult1
Los resultados son mostrados en las matrices de confusión siguientes, las cuales corresponden a un entrenamiento con los primeros 4 meses, los primeros 8 y solo del cuarto mes, respectivamente.
Luego, se calcularon los indicadores de cada una.
Meses de entrenamiento | 1-4 | 1-8 | Mes 4 |
---|---|---|---|
Mes predicho | Mes 5 | Mes 9 | Mes 5 |
Precision | 0.0079 | 0.0078 | 0.0158 |
Recall | 0.0026 | 0.0012 | 0.0148 |
Accuracy | 0.9928 | 0.9936 | 0.9825 |
Se obtuvo muy poca precisión, además la clase “compra”, la que nos interesa predecir, tiene una gran cantidad de falsos positivos y negativos. Esto podría deberse a la baja cantidad de observaciones que hay de la clase de interés, con respecto a la otra clase. Este tema será tratado en la sección que sigue.
Luego de los resultados obtenidos, se decidió observar los cambios en los servicios, es decir cuando contratan o eliminan un servicio. Para esto se debió modificar el cambio de formato, pues el anterior no diferencia entre alguien que mantiene un servicio o alguien que lo elimina. Por esto, se perdía información.
Cambio formato:
Un servicio tiene un 1 si fue contratado en ese mes, un -1 si fue dado de baja, o un 0 si mantuvo su estado.
Detalle: No se considera el primer mes como un 1
Este nuevo formato sigue siendo igual de cómodo para clasificar que el anterior, pero no se pierde información del dataset, ya que se podría correr un algoritmo que recupere los datos anteriores.
Entonces, fue posible realizar nuevos análisis a los datos. Que se muestran a continuación.
Tal como se creía, la cantidad de observaciones en que se adquiere un servicio que no se tenía en el mes previo, es bastante baja. De hecho, de la imagen 1 se observa que el servicio ind_recibo_ult1 (Direct debit), tiene el mayor porcentaje de contratación, con alrededor de un 1%. Por le contrario, hay otros productos como inde_pres_fin_ult1 (Préstamos), que tiene casi un 0% de contratación.
Además, en la segunda imagen se observa que el porcentaje de productos eliminados es bastante bajo, donde el máximo llega a un 0.0001%.
Además, se decidió observar la cantidad agregada por producto en cada mes. Ésta se visualiza en la siguiente imagen
Del gráfico, se pudo observar situaciones interesantes entre los meses, como por ejemplo que el producto ind_cco_fin_ult1 (cuenta corriente) de naranjo en la imagen, es contratado menos de 2500 veces durante los primeros meses, pero luego en el mes 6, es agregado por poco menos de 7500 clientes.
Esta observación es importante, pues sería esperable que algunos servicios se vean afectados por los meses, dado por las vacaciones, inicio período escolar etc. Entonces, será importante estudiar cómo se comporta el modelo en ese tipo de variaciones, considerando además que se pide la predicción del sexto mes del siguiente año.
Dado que no se obtuvieron buenos resultados con el algoritmo de Naive Bayes, probablemente porque las variables tenían bastante correlación, tal como se vio en la matriz anteriormente. Y dado que dicho algoritmo asume los atributos independientes, se puede haber visto perjudicado. Así, se decidió probar con randomForest. Se probó con distintos parámetros, y se eligieron empíricamente los que dieron mayor accuracy a la clase de interés, “contrata”, los parámetros finales fueron:
>RandomForestClassifier(
n_estimators=100,
max_depth=None,
min_samples_split=4,
class_weight='balanced',
min_samples_leaf=3,
random_state=0,
n_jobs=-1,
criterion='gini'
)
Con la configuración mostrada anteriormente se crearon clasificadores para cada uno de los productos, los cuales fueron entrenados con los 5 primeros meses del dataset.
A continuación se muestran solo algunos resultados de los clasificadores, dado que mostrar 24 matrices de confusión sería excesivo. Las tres imágenes mostradas a continuación corresponden a:
En general nuestro modelo de predicción es bastante bueno para la clase “compra”. Dado que en la matriz promedio:
Para crear la submission en Kaggle, para cada usuario se debe:
Predecir los productos que contratará el mes objetivo.
Si hay más de un producto, estos se deben ordenar por probabilidad de ser contratados en orden descendente.
Si hay más de 7 productos predichos como “contrata”, entonces solo deben escribirse los 7 con mayor probabilidad.
Para nuestra submission hicimos lo siguiente:
Dado que Random Forest entrega solamente la clasificación final del producto, en este caso -1,0,1. Para poder comparar las probabilidades se calculó la probabilidad de la clasificación por producto en base a las votaciones de los arboles por cada Random Forest, es decir cuántos árboles votaron por la clasificación/árboles totales.
Luego de obtener las probabilidades para todos los clientes, por cada uno de los productos, se creó un dataset ordenado de todos los usuarios con los 7 productos con la más alta probabilidad de ser comprados
En la submission se logró llegar al puesto número 1004/1400 de la competencia Santander-Kaggle. Con un puntaje de 0.01679, considerando que el puntaje máximo es ~0.035, y que al probar con predicciones al azar se obtuvo ~0.003. Con esto, se conluye que el clasificador es bueno, dado que es considerablemente mejor que responder de forma aleatoria.
En conclusión la metodología seguida fue:
Fue necesario un cambio de formato, principalmente para poder observar el comportamiento y la predicción de las clases más pequeñas de interés.
Luego de haber obtenido mejores resultados con random forest, se piensa que la correlación observada en la matriz podría haber provocado la mala predicción de naive bayes.
Como trabajo a futuro, se podría probar hacer un subsampling de la clase mayoritaria, de tal forma de reducir la alta cantidad de falsos negativos obtenida.