1. Solicitudes de Tarjetas de Crédito

Los bancos comerciales reciben muchísimas solicitudes de tarjetas de crédito. Muchos de ellos son rechazados por diversas razones tales como altos saldos de préstamo, bajos niveles de ingresos, o tantas consultas sobre el informe de crédito de un individuo (solicitudes). Analizar manualmente estas solicitudes es mundano, propenso a errores y requiere mucho tiempo (y el tiempo es dinero!). Por suerte, esta tarea puede ser automatizada con el poder del aprendizaje automatico y casi todos los bancos comerciales lo hacen hoy en día. En este proyecto, construiremos un predictor automatizado de aprobación de tarjetas de crédito usando técnicas de Machine Learning, tal como los bancos reales lo hacen.

Credit card being held in hand

Usaremos el conjunto de datos de Credit Card Approval del repositorio UCI Machine Learning. La estructura del proyecto es el siguiente:

  • Primero, iniciaremos cargando y viendo el conjunto de datos.
  • Veremos que el conjunto de datos tiene una mixtura de variables numéricas y no numéricas, que contienen valores de diferentes rangos y que además contiene un número de datos vacíos.
  • Tendremos que preprocesar el conjunto de datos para asegurarnos de que el modelo de Machine Learning que escojamos pueda hacer buenas predicciones.
  • Después de que nuestros datos estén en buena forma, haremos un análisis exploratorio de datos para construir nuestras intuiciones.
  • Finalmente, crearemos un modelo de aprendizaje automático que pueda predecir si se aceptará la solicitud de una persona para una tarjeta de crédito.

Primero, cargamos y vemos el conjunto de datos. Descubriremos que el contribuyente del conjunto de datos ha anonimizado los nombres de las variables puesto que estos datos son confidencionales

# Import pandas
import pandas as pd
# Load dataset
cc_apps = pd.read_csv("http://archive.ics.uci.edu/ml/machine-learning-databases/credit-screening/crx.data", header=None)

# Inspect data
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 +

2. Analizando las solicitudes

El output puede parecer un poco confuso a primera vista, pero intentemos encontrar las variables más importantes de las solicitudes de tarjetas de crédito. Las variables han sido anonimizadas para proteger la privacidad, pero este blog nos da una muy buena aproximación de las probables variables. Las variables en una solicitud de tarjeta de crédito podrían ser Gender, Age, Debt, Married, BankCustomer, EducationLevel, Ethnicity, YearsEmployed, PriorDefault, Employed, CreditScore, DriversLicense, Citizen, ZipCode, Income y finalmente ApprovalStatus. Esto nos da un muy buen punto de inicio, y podemos podemos relacionar estas variables con respecto a las columnas en el output.

Como podemos ver desde nuestro primer vistazo a los datos, el conjunto de datos tiene una combinación de características numéricas y no numéricas. Esto se puede solucionar con algo de preprocesamiento, pero antes de hacerlo, aprendamos un poco más sobre el conjunto de datos para ver si hay otros problemas del conjunto de datos que deben corregirse.

# Print summary statistics
cc_apps_description = cc_apps.describe()
print(cc_apps_description)

print("\n")

# Print DataFrame information
cc_apps_info = cc_apps.info()
print(cc_apps_info)

print("\n")

# Inspect missing values in the dataset
cc_apps.tail()
               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):
0     690 non-null object
1     690 non-null object
2     690 non-null float64
3     690 non-null object
4     690 non-null object
5     690 non-null object
6     690 non-null object
7     690 non-null float64
8     690 non-null object
9     690 non-null object
10    690 non-null int64
11    690 non-null object
12    690 non-null object
13    690 non-null object
14    690 non-null int64
15    690 non-null object
dtypes: float64(2), int64(2), object(12)
memory usage: 86.3+ KB
None
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
685 b 21.08 10.085 y p e h 1.25 f f 0 f g 00260 0 -
686 a 22.67 0.750 u g c v 2.00 f t 2 t g 00200 394 -
687 a 25.25 13.500 y p ff ff 2.00 f t 1 t g 00200 1 -
688 b 17.92 0.205 u g aa v 0.04 f f 0 f g 00280 750 -
689 b 35.00 3.375 u g c h 8.29 f f 0 t g 00000 0 -

3. Manejando los datos vacíos (part I)

Hemos descubierto algunos problemas que afectarán el rendimiento de nuestros modelos de aprendizaje automático si no cambian:

  • Nuestro conjunto de datos contiene datos numéricos y no numéricos (específicamente datos que son float64, int64 y object types). Específicamente, las columnass 2, 7, 10 y 14 contienen valores numéricos (de tipos float64, float64, int64 y int64 respectivamente) y todas las otras columnas contienen valores no-numéricos.
  • El conjunto de datos también contiene valores de varios rangos. Algunas columnas tienen un rango de valores de 0 a 28, algunas tienen un rango de 2 a 67 y otras tienen un rango de 1017 a 100000. Aparte de estas, podemos obtener información estadística útil (como mean, max, y min) sobre las características que tienen valores numéricos.
  • Finalmente, el conjunto de datos tiene valores faltantes, de los cuales nos ocuparemos en esta tarea. Los valores que faltan en el conjunto de datos están etiquetados con '?', Que se puede ver en la salida de la última celda.

Ahora, reemplacemos temporalmente estos signos de interrogación de valor perdido con NaN.

# Import numpy
import numpy as np
# Inspect missing values in the dataset
print(cc_apps.tail(17))

# Replace the '?'s with NaN
cc_apps = cc_apps.replace('?', np.nan)

# Inspect the missing values again
cc_apps.tail(17)
    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  -
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 -

4. Manejando los datos vacíos (part II)

Reemplazamos todos los signos de interrogación con NaNs. Esto nos ayudará en el próximo tratamiento de los valores perdidos que vamos a realizar.

Una pregunta importante que surge aquí es ¿por qué le damos tanta importancia a los valores perdidos ? ¿No pueden ser ignorados? Ignorar los valores perdidos puede afectar en gran medida el rendimiento de un modelo de aprendizaje automático. Si ignoramos los valores faltantes, nuestro modelo de aprendizaje automático puede perder información sobre el conjunto de datos que puede ser útil para su capacitación.

Entonces, para evitar este problema, vamos a imputar los valores faltantes con una estrategia llamada imputación media.

# Impute the missing values with mean imputation
cc_apps.fillna(cc_apps.mean(), inplace=True)

# Count the number of NaNs in the dataset to verify
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

5. Manejando los datos vacíos (part III)

Nos hemos ocupado con éxito de los valores perdidos que están en las columnas numéricas. Todavía hay algunos datos vacíos para ser imputados en las columnas 0, 1, 3, 4, 5, 6 y 13. Todas estas columnas contienen datos no numéricos y es por eso que la estrategia de imputación media no funcionaría aquí. Esto necesita un tratamiento diferente.

Vamos a imputar estos valores perdidos con los valores más frecuentes que están presentes en las columnas respectivas. Esto es una buena práctica cuando se trata de imputar valores perdidos para datos categóricos en general.

# Iterate over each column of cc_apps
for col in cc_apps:
    # Check if the column is of object type
    if cc_apps[col].dtype == 'object':
        # Impute with the most frequent value
        cc_apps = cc_apps.fillna(cc_apps[col].value_counts()[0])

# Count the number of NaNs in the dataset and print the counts to verify
cc_apps.isna().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

6. Preprocesando los datos (part I)

Los valores perdidos ahora son manejados con éxito.

Todavía es necesario un preprocesamiento de datos menor pero esencial antes de proceder a construir nuestro modelo de aprendizaje automático. Vamos a dividir estos pasos de preprocesamiento restantes en tres tareas principales:

  1. Convertir los datos no numéricos en numéricos.
  2. Dividir los datos en dos conjuntos de entrenamiento y prueba.
  3. Escalar los valores de la característica a un rango uniforme.

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 aprendizaje automático (como XGBoost) (y especialmente los que son desarrollados usando scikit-learn) requieren que los datos estén en un formato estrictamente numérico. Lo haremos utilizando una técnica llamada label encoding .

# Import LabelEncoder
from sklearn.preprocessing import LabelEncoder

# Instantiate LabelEncoder

le = LabelEncoder()

# Iterate over all the values of each column and extract their dtypes
for col in cc_apps.columns.values:
    # Compare if the dtype is object
    if cc_apps[col].dtype=='object':
    # Use LabelEncoder to do the numeric transformation
        cc_apps[col]=le.fit_transform(cc_apps[col].astype(str))
        
cc_apps.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 690 entries, 0 to 689
Data columns (total 16 columns):
0     690 non-null int64
1     690 non-null int64
2     690 non-null float64
3     690 non-null int64
4     690 non-null int64
5     690 non-null int64
6     690 non-null int64
7     690 non-null float64
8     690 non-null int64
9     690 non-null int64
10    690 non-null int64
11    690 non-null int64
12    690 non-null int64
13    690 non-null int64
14    690 non-null int64
15    690 non-null int64
dtypes: float64(2), int64(14)
memory usage: 86.3 KB

7. Separando el conjunto de datos en dos: entrenamiento y prueba

Hemos convertido con éxito todos los valores no numéricos en valores numéricos.

Ahora, dividiremos nuestros datos en un conjunto de entrenamiento y un conjunto de prueba para preparar nuestros datos para dos fases diferentes de modelado de aprendizaje automático: capacitación y pruebas. Idealmente, ninguna información de los datos de la prueba debería usarse para escalar los datos de entrenamiento o para dirigir el proceso de entrenamiento de un modelo de aprendizaje automático. Por lo tanto, primero dividimos los datos y luego aplicamos la escala.

Además, características como DriversLicense y ZipCode no son tan importantes como las otras características del conjunto de datos para predecir las aprobaciones de tarjetas de crédito. No deberíamos considerarlos para diseñar nuestro modelo de aprendizaje automático con el mejor conjunto de variables. En la literatura de Data Science, esto a menudo se conoce como feature selection .

# Import train_test_split
from sklearn.model_selection import train_test_split
# Drop the features 11 and 13 and convert the DataFrame to a NumPy array
cc_apps = cc_apps.drop([11, 13], axis=1)
cc_apps = cc_apps.values

# Segregate features and labels into separate variables
X,y = cc_apps[:,0:12] , cc_apps[:,13]

# Split into train and test sets
X_train, X_test, y_train, y_test = train_test_split(X,
                                y,
                                test_size=0.33,
                                random_state=42)

8. Preprocesando los datos (part II)

Los datos ahora se dividen en dos conjuntos separados: conjuntos de entrenamiento y prueba, respectivamente. Solo nos queda un paso final que es el escalado antes de que podamos aplicar un modelo de aprendizaje automático a los datos.

Ahora, intentemos comprender qué significan estos valores escalados en el mundo real. Usemos la variable CreditScore como ejemplo. La calificación crediticia de una persona es su solvencia en función de su historial crediticio. Cuanto mayor sea su solvencia, más confiable financieramente se considera que una persona es. Entonces, un CreditScore de 1 es el más alto ya que estamos reescalando todos los valores al rango de 0-1.

# Import MinMaxScaler
from sklearn.preprocessing import MinMaxScaler
# Instantiate MinMaxScaler and use it to rescale X_train and X_test
scaler = MinMaxScaler(feature_range=(0, 1))
rescaledX_train = scaler.fit_transform(X_train)
rescaledX_test = scaler.fit_transform(X_test)

9. Ajustando un modelo de regresión logística al conjunto de entrenamiento.

Esencialmente, predecir si una solicitud de tarjeta de crédito será aprobada o no es una tarea de clasificación . Según el 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 instancias, hay 383 (55.5%) solicitudes que fueron rechazadas y 307 (44.5%) solicitudes 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 solicitudes con respecto a estas estadísticas.

¿Qué modelo debemos elegir? Una pregunta que debe hacerse es: ¿Las variables 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, eso está fuera del alcance de este proyecto, por lo que confiaremos en nuestra intuición de que de hecho están correlacionadas por ahora. Debido a esta correlación, aprovecharemos el hecho de que los modelos lineales generalizados funcionan bien en estos casos. Comencemos nuestro proceso de modelación de aprendizaje automático con un modelo de Regresión Logística (un modelo lineal generalizado).

# Import LogisticRegression
from sklearn.linear_model import LogisticRegression

# Instantiate a LogisticRegression classifier with default parameter values
logreg = LogisticRegression()

# Fit logreg to the train set
logreg.fit(rescaledX_train, y_train)
LogisticRegression(C=1.0, class_weight=None, dual=False, fit_intercept=True,
          intercept_scaling=1, max_iter=100, multi_class='ovr', n_jobs=1,
          penalty='l2', random_state=None, solver='liblinear', tol=0.0001,
          verbose=0, warm_start=False)

10. Haciendo predicciones y evaluando el rendimiento

¿Pero qué tan bien funciona nuestro modelo?

Ahora evaluaremos nuestro modelo en el conjunto de prueba con la precisión de clasificación . Pero también analizaremos la matriz de confusión del modelo. En el caso de la predicción de solicitudes de tarjetas de crédito, es igualmente importante ver si nuestro modelo de aprendizaje automático puede predecir la aprobación de las solicitudes que originalmente fueron denegadas. Si nuestro modelo no funciona bien en este aspecto, entonces podría terminar denegando la solicitud que debería haber sido aprobada. La matriz de confusión nos ayuda a ver el rendimiento de nuestro modelo desde estos aspectos.

# Import confusion_matrix
from sklearn.metrics import confusion_matrix
# Use logreg to predict instances from the test set and store it
y_pred = logreg.predict(rescaledX_test)

# Get the accuracy score of logreg model and print it
print("Accuracy of logistic regression classifier: ", logreg.score(rescaledX_test,y_test))

# Print the confusion matrix of the logreg model
confusion_matrix(y_test, y_pred)
Accuracy of logistic regression classifier:  0.8377192982456141

array([[92, 11],
       [26, 99]])

11. Grid searching y haciendo que el modelo funcione mejor

¡Nuestro modelo es bastante bueno! Es capaz de obtener una precisión de casi el 84%.

Para la matriz de confusión, el primer elemento de la primera fila denota los verdaderos negativos que significan el número de instancias negativas (solicitudes denegadas) predichas por el modelo correctamente. Y el último elemento de la segunda fila de la matriz de confusión denota los verdaderos positivos que significan el número de instancias positivas (solicitudes aprobadas) predichas por el modelo correctamente.

Veamos si podemos hacerlo mejor. Podemos realizar un Grid searching de los parámetros del modelo para mejorar su capacidad de predecir aprobaciones de tarjetas de crédito.

La implementación de regresión logística de scikit-learn consta de diferentes hiperparámetros, pero nosotros buscaremos en el Grid Search los dos siguientes:

  • tol
  • max_iter
# Import GridSearchCV
from sklearn.model_selection import GridSearchCV
# Define the grid of values for tol and max_iter
tol = [0.01,0.001,0.0001]
max_iter = [100,150,200]

# Create a dictionary where tol and max_iter are keys and the lists of their values are corresponding values
param_grid = dict(tol=tol, max_iter=max_iter)

12. Encontrando el modelo con el mejor rendimiento

Hemos definido la matriz de valores de hiperparámetros y los hemos convertido a un único formato de diccionario que GridSearchCV () espera como uno de sus parámetros. Ahora, comenzaremos la búsqueda en la matriz para ver qué valores funcionan mejor.

Aplicaremos GridSearchCV () con nuestro modelo anterior logreg con todos los datos que tenemos. En lugar de pasar el entrenamiento y el conjunto de prueba por separado, suministraremos X (versión a escala) y la variable explicada y . También le indicaremos a GridSearchCV () que realice una validación cruzada de cinco instancias.

Terminaremos el proyecto almacenando el puntaje mejor logrado y los mejores parámetros respectivos.

Al construir este predictor de tarjeta de crédito, abordamos algunos de los pasos de preprocesamiento más conocidos, como scaling , label encoding y missing value imputation . Terminamos con algo de aprendizaje automático para predecir si la solicitud de una tarjeta de crédito de una persona sería aprobada o no dado la información sobre esa persona.

# Instantiate GridSearchCV with the required parameters
grid_model = GridSearchCV(estimator=logreg, param_grid=param_grid, cv=5)

# Use scaler to rescale X and assign it to rescaledX
rescaledX = scaler.fit_transform(X)

# Fit data to grid_model
grid_model_result = grid_model.fit(rescaledX, y)

# Summarize results
best_score, best_params = grid_model_result.best_score_, grid_model_result.best_params_
print("Best: %f using %s" % (best_score, best_params))
Best: 0.850725 using {'tol': 0.01, 'max_iter': 100}

Como vemos, terminamos con una precisión del 85% y que los mejores parámetros para tol y max_iter son 0.01 y 100, respectivamente