Los bancos comerciales reciben infinidad de solicitudes de tarjetas de crédito. Muchos de ellos son rechazados por diversas razones, como saldos elevados de préstamos, bajos niveles de ingresos o demasiadas consultas sobre el informe crediticio de una persona, por ejemplo. El análisis manual de estas aplicaciones es tedioso, propenso a errores y requiere mucho tiempo. Afortunadamente, esta tarea se puede automatizar con el poder del machine learning o aprendizaje automático y casi todos los bancos comerciales lo hacen hoy en día. En este post, crearemos un predictor automático de aprobación de tarjetas de crédito utilizando técnicas de aprendizaje automático, tal como lo hacen los bancos reales.
Usaremos el conjunto de datos Credit Card Approval del Repositorio de Machine Learning de UCI. La estructura de este trabajo será la siguiente:
Dado que estos datos son confidenciales, el contribuyente de este dataset ha anonimizado los nombres de las funciones.
import pandas as pd
# Cargamos dataset
cc_apps = pd.read_csv("datasets/cc_approvals.data", header=None)
# Un vistazo a los datos
cc_apps.head()
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | b | 30.83 | 0.000 | u | g | w | v | 1.25 | t | t | 1 | f | g | 00202 | 0 | + |
1 | a | 58.67 | 4.460 | u | g | q | h | 3.04 | t | t | 6 | f | g | 00043 | 560 | + |
2 | a | 24.50 | 0.500 | u | g | q | h | 1.50 | t | f | 0 | f | g | 00280 | 824 | + |
3 | b | 27.83 | 1.540 | u | g | w | v | 3.75 | t | t | 5 | t | g | 00100 | 3 | + |
4 | b | 20.17 | 5.625 | u | g | w | v | 1.71 | t | f | 0 | f | s | 00120 | 0 | + |
El resultado puede parecer un poco confuso a primera vista, pero intentemos descubrir las características más importantes de una aplicación de tarjeta de crédito.
Como dijimos, los predictores de este conjunto de datos se han anonimizado para proteger la privacidad, pero este blog nos brinda una descripción general bastante buena de cuales pueden ser los probables predictores. Las características probables en una solicitud de tarjeta de crédito típica podrían ser Género
, Edad
, Deuda
, Estado Civil
, Cliente bancario
, Nivel de educación
, Etnia
, Años de empleo
, Incumplimiento previo
, Empleado
, Puntuación de crédito
, Licencia de conductor
, Ciudadano
, Código postal
, Ingresos
y finalmente el Estado de aprobación
. Esto nos da un buen punto de partida y podemos mapear estas características con respecto a las columnas en nuestro dataset.
Como podemos ver, el dataset tiene una combinación de predictores numéricos y no numéricos. Esto se puede solucionar con un poco de preprocesamiento, pero antes de hacerlo, investiguemos un poco más para ver si hay otros problemas del conjunto de datos que deban solucionarse.
# Resumen estadístico
cc_apps_description = cc_apps.describe()
print(cc_apps_description)
print("\n")
# Características del dataset
cc_apps_info = cc_apps.info()
print(cc_apps_info)
print("\n")
# Un vistazo a los últimos registros
print(cc_apps.tail(17))
2 7 10 14 count 690.000000 690.000000 690.00000 690.000000 mean 4.758725 2.223406 2.40000 1017.385507 std 4.978163 3.346513 4.86294 5210.102598 min 0.000000 0.000000 0.00000 0.000000 25% 1.000000 0.165000 0.00000 0.000000 50% 2.750000 1.000000 0.00000 5.000000 75% 7.207500 2.625000 3.00000 395.500000 max 28.000000 28.500000 67.00000 100000.000000 <class 'pandas.core.frame.DataFrame'> RangeIndex: 690 entries, 0 to 689 Data columns (total 16 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 0 690 non-null object 1 1 690 non-null object 2 2 690 non-null float64 3 3 690 non-null object 4 4 690 non-null object 5 5 690 non-null object 6 6 690 non-null object 7 7 690 non-null float64 8 8 690 non-null object 9 9 690 non-null object 10 10 690 non-null int64 11 11 690 non-null object 12 12 690 non-null object 13 13 690 non-null object 14 14 690 non-null int64 15 15 690 non-null object dtypes: float64(2), int64(2), object(12) memory usage: 86.4+ KB None 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 673 ? 29.50 2.000 y p e h 2.000 f f 0 f g 00256 17 - 674 a 37.33 2.500 u g i h 0.210 f f 0 f g 00260 246 - 675 a 41.58 1.040 u g aa v 0.665 f f 0 f g 00240 237 - 676 a 30.58 10.665 u g q h 0.085 f t 12 t g 00129 3 - 677 b 19.42 7.250 u g m v 0.040 f t 1 f g 00100 1 - 678 a 17.92 10.210 u g ff ff 0.000 f f 0 f g 00000 50 - 679 a 20.08 1.250 u g c v 0.000 f f 0 f g 00000 0 - 680 b 19.50 0.290 u g k v 0.290 f f 0 f g 00280 364 - 681 b 27.83 1.000 y p d h 3.000 f f 0 f g 00176 537 - 682 b 17.08 3.290 u g i v 0.335 f f 0 t g 00140 2 - 683 b 36.42 0.750 y p d v 0.585 f f 0 f g 00240 3 - 684 b 40.58 3.290 u g m v 3.500 f f 0 t s 00400 0 - 685 b 21.08 10.085 y p e h 1.250 f f 0 f g 00260 0 - 686 a 22.67 0.750 u g c v 2.000 f t 2 t g 00200 394 - 687 a 25.25 13.500 y p ff ff 2.000 f t 1 t g 00200 1 - 688 b 17.92 0.205 u g aa v 0.040 f f 0 f g 00280 750 - 689 b 35.00 3.375 u g c h 8.290 f f 0 t g 00000 0 -
Mediante las observaciones anteriores, hemos descubierto algunos problemas en el dataset que afectarán el rendimiento de nuestros modelos de machine learning si no se modifican:
float64
o int64
, y categóricos o no numéricos, de tipo object
para las características restantes.Vamos a ocuparnos ahora de estos valores faltantes. Comencemos reemplazando temporalmente estos signos de interrogación con valores nulos NaN
.
import numpy as np
# Reemplazamos los '?'s con NaN
cc_apps = cc_apps.replace('?', np.nan)
# Observemos nuevamente el valor de la feature 0 para la fila 673
print(cc_apps.tail(17))
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 673 NaN 29.50 2.000 y p e h 2.000 f f 0 f g 00256 17 - 674 a 37.33 2.500 u g i h 0.210 f f 0 f g 00260 246 - 675 a 41.58 1.040 u g aa v 0.665 f f 0 f g 00240 237 - 676 a 30.58 10.665 u g q h 0.085 f t 12 t g 00129 3 - 677 b 19.42 7.250 u g m v 0.040 f t 1 f g 00100 1 - 678 a 17.92 10.210 u g ff ff 0.000 f f 0 f g 00000 50 - 679 a 20.08 1.250 u g c v 0.000 f f 0 f g 00000 0 - 680 b 19.50 0.290 u g k v 0.290 f f 0 f g 00280 364 - 681 b 27.83 1.000 y p d h 3.000 f f 0 f g 00176 537 - 682 b 17.08 3.290 u g i v 0.335 f f 0 t g 00140 2 - 683 b 36.42 0.750 y p d v 0.585 f f 0 f g 00240 3 - 684 b 40.58 3.290 u g m v 3.500 f f 0 t s 00400 0 - 685 b 21.08 10.085 y p e h 1.250 f f 0 f g 00260 0 - 686 a 22.67 0.750 u g c v 2.000 f t 2 t g 00200 394 - 687 a 25.25 13.500 y p ff ff 2.000 f t 1 t g 00200 1 - 688 b 17.92 0.205 u g aa v 0.040 f f 0 f g 00280 750 - 689 b 35.00 3.375 u g c h 8.290 f f 0 t g 00000 0 -
Si deseamos observarlo más claro, hay 12 valores faltantes para la feature 0.
# Filtramos los valores NaN para la columna 0
cc_apps[cc_apps[0].isna()]
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
248 | NaN | 24.50 | 12.750 | u | g | c | bb | 4.750 | t | t | 2 | f | g | 00073 | 444 | + |
327 | NaN | 40.83 | 3.500 | u | g | i | bb | 0.500 | f | f | 0 | f | s | 01160 | 0 | - |
346 | NaN | 32.25 | 1.500 | u | g | c | v | 0.250 | f | f | 0 | t | g | 00372 | 122 | - |
374 | NaN | 28.17 | 0.585 | u | g | aa | v | 0.040 | f | f | 0 | f | g | 00260 | 1004 | - |
453 | NaN | 29.75 | 0.665 | u | g | w | v | 0.250 | f | f | 0 | t | g | 00300 | 0 | - |
479 | NaN | 26.50 | 2.710 | y | p | NaN | NaN | 0.085 | f | f | 0 | f | s | 00080 | 0 | - |
489 | NaN | 45.33 | 1.000 | u | g | q | v | 0.125 | f | f | 0 | t | g | 00263 | 0 | - |
520 | NaN | 20.42 | 7.500 | u | g | k | v | 1.500 | t | t | 1 | f | g | 00160 | 234 | + |
598 | NaN | 20.08 | 0.125 | u | g | q | v | 1.000 | f | t | 1 | f | g | 00240 | 768 | + |
601 | NaN | 42.25 | 1.750 | y | p | NaN | NaN | 0.000 | f | f | 0 | t | g | 00150 | 1 | - |
641 | NaN | 33.17 | 2.250 | y | p | cc | v | 3.500 | f | f | 0 | t | g | 00200 | 141 | - |
673 | NaN | 29.50 | 2.000 | y | p | e | h | 2.000 | f | f | 0 | f | g | 00256 | 17 | - |
Hemos reemplazado todos los signos de interrogación con NaN. Esto nos ayudará al momento de aplicar la estrategia de valores faltantes que vamos a realizar.
Una pregunta importante que surge aquí es ¿por qué le damos tanta importancia a los valores perdidos? ¿No se pueden simplemente ignorar?
Ignorar los valores perdidos puede afectar en gran medida el rendimiento del modelo de machine learning. Si bien podría ignorar los valores faltantes, nuestro modelo también perdería información potencialmente útil del dataset para su entrenamiento. Debido a esto, hay muchos modelos que no pueden manejar valores faltantes implícitamente.
Entonces, para evitar este problema, vamos a imputar o "llenar" los valores faltantes con una estrategia llamada mean imputation. Esta estrategia lo que hace es reemplazar los valores faltantes con el valor de la media para todos los valores de esa característica en el dataset. Obviamente, esto aplica solo para las features de tipo numéricas.
# Imputamos los valores faltantes con la media
cc_apps.fillna(cc_apps.mean(), inplace=True)
# Contamos el número de NaNs para verificar
cc_apps.isnull().sum()
0 12 1 12 2 0 3 6 4 6 5 9 6 9 7 0 8 0 9 0 10 0 11 0 12 0 13 13 14 0 15 0 dtype: int64
Como vemos, nos hemos ocupado con éxito de los valores faltantes presentes en las columnas numéricas. Todavía hay algunos valores faltantes que imputar para las columnas 0, 1, 3, 4, 5, 6 y 13. Todas estas columnas contienen datos categóricos (no numéricos) y por eso la estrategia de imputación media no funcionaría aquí. Esto necesita un tratamiento diferente.
Vamos a imputar estos valores faltantes con los valores más frecuentes presentes en sus respectivas columnas. Esta es una buena práctica cuando se trata de imputar valores faltantes para datos categóricos en general.
Para hacerlo, recorreremos cada una de las columnas del DataFrame y sólo en aquellas con valores categóricos imputaremos el valor que mayor recuento tenga para dicha columna.
# Recorremos cada columna de cc_apps
for col in cc_apps.columns:
# Chequeamos si la columna es de tipo 'object'
if cc_apps[col].dtypes == 'object':
# Imputamos con el valor más frecuente
cc_apps = cc_apps.fillna(cc_apps[col].value_counts().index[0])
# Volvemos a contar el número de NaNs en el dataset para verificar
cc_apps.isnull().sum()
0 0 1 0 2 0 3 0 4 0 5 0 6 0 7 0 8 0 9 0 10 0 11 0 12 0 13 0 14 0 15 0 dtype: int64
Hemos solucionado el problema de los valores faltantes.
Todavía se necesita un preprocesamiento de datos menor pero esencial antes de continuar con la construcción de nuestro modelo. Vamos a dividir estos pasos de preprocesamiento restantes en tres tareas principales:
Primero, convertiremos todos los valores no numéricos en valores numéricos. Hacemos esto porque no solo da como resultado un cálculo más rápido, sino que también muchos modelos de machine learning (especialmente los desarrollados con scikit-learn) requieren que los datos estén en un formato estrictamente numérico. Haremos esto utilizando una técnica llamada label encoding.
from sklearn.preprocessing import LabelEncoder
# Instanciamos LabelEncoder
le = LabelEncoder()
# Recorremos todos los valores de cada columna y extraemos su tipo de dato
for col in cc_apps.columns:
# Chequeamos si la columna es de tipo 'object'
if cc_apps[col].dtypes == 'object':
# Usamos LabelEncoder para realizar la transformación numérica
cc_apps[col]=le.fit_transform(cc_apps[col])
Hemos convertido todos los valores categóricos en valores numéricos.
Ahora, dividiremos nuestro dataset en un conjunto de entrenamiento y otro de pruebas que utilizaremos en esas dos fases diferentes del modelado respectivamente.
Idealmente, no se debe utilizar ninguna información de los datos del conjunto de pruebas para escalar los datos de entrenamiento ni mucho menos se deben utilizar durante el proceso de entrenamiento del modelo de machine learning. Por lo tanto, primero dividiremos los datos y luego aplicaremos el proceso de reescalamiento.
Además, podemos intuír que algunos datos como la Licencia de conductor
y el Código Postal
no son tan significativos al momento a predecir las aprobaciones de tarjetas de crédito como sí lo son otros datos de este dataset. Por lo tanto, deberíamos descartarlos para diseñar nuestro modelo de machine learning con el mejor conjunto de características. En la literatura sobre ciencia de datos, esto a menudo se denomina selección de características (feature selection).
from sklearn.model_selection import train_test_split
# Eliminamos las features 11 y 13 y convertimos el DataFrame en un NumPy array
cc_apps = cc_apps.drop([11, 13], axis=1)
cc_apps = cc_apps.to_numpy()
# Separamos características y etiquetas en variables distintas
X, y = cc_apps[:,0:12] , cc_apps[:,13]
# Dividimos el dataset en conjuntos de entrenamiento y prueba
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.33, random_state=42)
Los datos ya fueron divididos en dos conjuntos separados. Sólo nos queda un paso final antes de que podamos entrenar nuesto modelo, el escalado de las variables.
Cuando un dataset tiene rangos variables, como en este caso, es posible que un pequeño cambio en una característica en particular no genere un efecto significativo en otra, lo que puede causar muchos problemas en el modelado predictivo. De aquí la necesidad de llevar todas las características a una escala similar.
Intentemos comprender qué significan estos valores escalados en el mundo real. Usemos Puntuación de Crédito
como ejemplo. El puntaje crediticio de una persona es su solvencia basada en su historial crediticio. Cuanto mayor sea este número, se considera que una persona es más confiable desde el punto de vista financiero. Por lo tanto, un puntaje crediticio de 1 será el más alto, ya que estamos escalando todos los valores al rango entre 0 y 1.
from sklearn.preprocessing import MinMaxScaler
# Instanciamos MinMaxScaler y lo utilizamos para escalar X_train y X_test
scaler = MinMaxScaler(feature_range=(0, 1))
rescaledX_train = scaler.fit_transform(X_train)
rescaledX_test = scaler.fit_transform(X_test)
Esencialmente, predecir si una solicitud de tarjeta de crédito será aprobada o no es una tarea de clasificación. Según UCI, nuestro conjunto de datos contiene más instancias que corresponden al estado "Denegado" que las instancias correspondientes al estado "Aprobado". Específicamente, de 690 casos, hay 383 (55,5%) aplicaciones que fueron denegadas y 307 (44,5%) aplicaciones que fueron aprobadas.
Esto nos da un punto de referencia. Un buen modelo de aprendizaje automático debería poder predecir con precisión el estado de las aplicaciones con respecto a estas estadísticas.
¿Qué modelo deberíamos elegir?
Una pregunta que debe hacerse es: ¿las características que afectan el proceso de decisión de aprobación de la tarjeta de crédito están correlacionadas entre sí? Aunque podemos medir la correlación, en este caso nos limitaremos a confiar en nuestra intuición de que, de hecho, están correlacionados por ahora. Debido a esta correlación, aprovecharemos el hecho de que los modelos lineales generalizados funcionan bien en estos casos. Comencemos nuestro modelado de machine learning con un modelo de logistic regression (modelo lineal generalizado).
from sklearn.linear_model import LogisticRegression
# Instanciamos el clasificador LogisticRegression con sus parámetros por defecto
logreg = LogisticRegression()
# Entrenamos logreg con los datos escalados
logreg.fit(rescaledX_train, y_train)
LogisticRegression()
Ya tenemos nuestro modelo entrenado, pero ¿qué tan bien funciona?
Evaluaremos nuestro modelo con el conjunto de prueba respecto a la exactitud de la clasificación, es decir, evaluando la métrica accuracy
, pero también echaremos un vistazo a la matriz de confusión del modelo.
En nuestro caso de estudio, es igualmente importante ver si nuestro modelo es capaz de predecir como aprobadas aquellas solicitudes realmente aprobadas tanto como predecir como denegadas aquellas originalmente rechazadas. Si nuestro modelo no está funcionando bien en este aspecto, entonces podría terminar aprobando solicitudes que deberían haber sido rechazadas. La matriz de confusión nos ayuda a ver el desempeño de nuestro modelo desde estos aspectos.
from sklearn.metrics import confusion_matrix
# Utilizamos el estimador logreg para predecir instancias sobre el test set y las almacenamos
y_pred = logreg.predict(rescaledX_test)
# Obtenemos la puntuación "accuracy score"
print("Accuracy: ", logreg.score(rescaledX_test, y_test))
# Mostramos la matriz de confusión del modelo
print(confusion_matrix(y_test, y_pred))
Accuracy: 0.8377192982456141 [[93 10] [27 98]]
¡Nuestro modelo fue bastante bueno! Pudo producir un accuracy de casi el 84%.
En la matriz de confusión, el primer elemento de la primera fila representa los verdaderos negativos, es decir, el número de instancias negativas (solicitudes denegadas) predichas correctamente por el modelo. El último elemento de la segunda fila representa los verdaderos positivos, es decir, el número de instancias positivas (solicitudes aprobadas) predichas correctamente por el modelo.
Veamos si podemos mejorarlo. Podemos realizar una búsqueda en cuadrícula -grid search- de los parámetros del modelo para mejorar su capacidad para predecir las solicitudes de tarjetas de crédito.
La implementación de scikit-learn de logistic regression consta de diferentes hiperparámetros, pero en este caso buscaremos en la cuadrícula sólo los siguientes:
tol
max_iter
from sklearn.model_selection import GridSearchCV
# Definimos la grilla de valores para 'tol' y 'max_iter'
tol = [0.01, 0.001, 0.0001]
max_iter = [100, 150, 200]
# Creamos un diccionario con 'tol' y 'max_iter' como claves y las listas anteriores como sus valores
param_grid = dict(tol=tol, max_iter=max_iter)
Hemos definido la cuadrícula de valores de hiperparámetros y los hemos convertido en un formato de diccionario único que GridSearchCV()
espera como uno de sus parámetros. Ahora, comenzaremos la búsqueda en la cuadrícula para ver qué valores funcionan mejor.
Crearemos una instancia de GridSearchCV()
con nuestro modelo logreg anterior y todos los datos que tenemos. En lugar de pasar X_train
y X_test
por separado, proporcionaremos X
(versión escalada) e y
. También indicaremos a GridSearchCV()
que realice cross-validation de cinco pliegues.
Finalizaremos este proyecto almacenando la puntuación mejor lograda y los mejores parámetros respectivos.
# Instanciamos GridSearchCV con los parámetros requeridos
grid_model = GridSearchCV(estimator=logreg, param_grid=param_grid, cv=5)
# Utilizamos nuevamente 'scaler' para escalar X
rescaledX = scaler.fit_transform(X)
# Entrenamos el modelo
grid_model_result = grid_model.fit(rescaledX, y)
# Obtenemos los valores de los hiperparámetros que mejores resultados arrojan
best_score, best_params = grid_model_result.best_score_, grid_model_result.best_params_
print("Mejor puntuación: %f , utilizando %s" % (best_score, best_params))
Mejor puntuación: 0.850725 , utilizando {'max_iter': 100, 'tol': 0.01}
Al crear este predictor de solicitudes de tarjetas de crédito, abordamos algunos de los pasos de preprocesamiento más conocidos, como el escalado, la codificación de etiquetas y la imputación de valores faltantes. Terminamos con algo de machine learning para predecir si la solicitud de una persona para una tarjeta de crédito se aprobaría o no, dada cierta información sobre esa persona.
La idea es que fuera algo introductorio. Más adelante podríamos retomarlo para evaluar el modelo con otras métricas más significativas e incluso probar y comparar con otros modelos de clasificación diferentes.