Predicción de riesgo crediticio con regresión logística
Predicción de riesgo crediticio con regresión logística
El origen de los datos es desconocido; los propietarios no describen su procedencia ni brindan una descripción, pero el conjunto de datos es adecuado para crear un modelo de práctica. Dataset de Kaggle
El objetivo principal es crear un modelo que clasifique el riesgo crediticio de las personas que solicitan un préstamo con el banco, en función de variables como: la edad, los ingresos anuales, el tipo de tenencia de vivienda, los años de empleo, el propósito del préstamo, la calificación del préstamo, el monto solicitado, la tasa de interés, el porcentaje del ingreso que representa el préstamo, si ha tenido incumplimientos en el pasado y la duración de su historial crediticio.
Método de Machine Learning
- Escalado de datos para mejorar el rendimiento.
- Aplicación de PCA para reducción de dimensionalidad antes del modelado.
- El modelo utilizado será Regresión Logística.
Atributos del conjunto de datos
- person_age: Edad de la persona que solicita el préstamo.
- person_income: Ingresos anuales de la persona.
- person_home_ownership: Tipo de tenencia de vivienda (RENT = Arriendo, OWN = Propia, MORTGAGE = Hipoteca).
- person_emp_length: Años de empleo de la persona.
- loan_intent: Propósito del préstamo (PERSONAL = Personal, EDUCATION = Educación, MEDICAL = Médico, VENTURE = Emprendimiento, HOMEIMPROVEMENT = Mejoras del hogar, DEBTCONSOLIDATION = Consolidación de deudas).
- loan_grade: Calificación del préstamo (evalúa el riesgo crediticio del solicitante).
- loan_amnt: Monto solicitado en el préstamo.
- loan_int_rate: Tasa de interés del préstamo.
- loan_status: Estado del préstamo (1 = Incumplido, 0 = Sin incumplimiento).
- loan_percent_income: Porcentaje del ingreso anual que representa el préstamo.
- cb_person_default_on_file: Si la persona ha tenido un incumplimiento en el pasado (Y = Sí, N = No).
- cb_person_cred_hist_length: Duración del historial crediticio en años.
1# Librerías necesarias
2import numpy as np
3import pandas as pd
4import matplotlib.pyplot as plt
5from matplotlib.colors import ListedColormap # Crear paletas personalizadas
6import seaborn as sns
7sns.set(style='whitegrid') # Estilo visual para seaborn
8
9# Preprocesamiento de datos y reducción de dimensionalidad
10from sklearn.preprocessing import StandardScaler, MinMaxScaler
11from sklearn.decomposition import PCA
12from sklearn.preprocessing import OneHotEncoder, LabelEncoder
13
14# Librerías para modelado
15from imblearn.over_sampling import SMOTE
16from imblearn.under_sampling import RandomUnderSampler
17import tensorflow as tf
18from tensorflow import keras
19from tensorflow.keras import layers
20from tensorflow.keras import backend as K
21
22# Evaluación y métricas
23from sklearn.model_selection import train_test_split
24from sklearn.metrics import (roc_curve, auc, precision_recall_curve,
25 confusion_matrix, f1_score)
26
27# Verificar versión de TensorFlow
28print(tf.__version__)
29
30# Montar Google Drive
31from google.colab import drive
32drive.mount('/content/drive')
33
34# Ruta del archivo CSV
35ruta_archivo = '/content/drive/My Drive/db/credit_risk_dataset.csv'
36
37# Cargar el archivo CSV
38df = pd.read_csv(ruta_archivo)
ANÁLISIS EXPLORATORIO DE DATOS (AED)
1# Información básica del dataset
2df.info() # Muestra el resumen de columnas, tipos de datos y valores nulos
3
4# Revisión de variables categóricas
5
6# person_home_ownership
7df['person_home_ownership'].value_counts()
8# RENT 16446
9# MORTGAGE 13444
10# OWN 2584
11# OTHER 107
12
13# loan_intent
14df['loan_intent'].value_counts()
15# EDUCATION 6453
16# MEDICAL 6071
17# VENTURE 5719
18# PERSONAL 5521
19# DEBTCONSOLIDATION 5212
20# HOMEIMPROVEMENT 3605
21
22# loan_grade
23df['loan_grade'].value_counts()
24# A 10777
25# B 10451
26# C 6458
27# D 3626
28# E 964
29# F 241
30# G 64
31
32# cb_person_default_on_file
33df['cb_person_default_on_file'].value_counts()
34# N 26836
35# Y 5745
36
37# loan_status
38df['loan_status'].value_counts()
39# 0 25473
40# 1 7108
1# Verificar valores nulos en el dataset
2df.isnull().sum()
3# person_age 0
4# person_income 0
5# person_home_ownership 0
6# person_emp_length 895
7# loan_intent 0
8# loan_grade 0
9# loan_amnt 0
10# loan_int_rate 3116
11# loan_status 0
12# loan_percent_income 0
13# cb_person_default_on_file 0
14# cb_person_cred_hist_length 0
15
16# Verificar si existen registros duplicados
17df.duplicated().sum()
18# 165 duplicados encontrados
- Hay varios valores atípicos según el valor máximo, y hay valores faltantes en loan_int_rate
- Hay 895 valores N/A en person_emp_length.
- Hay 3116 valores N/A en loan_int_rate
- La variable loan_grade no aporta información porque no tenemos contexto sobre ella.
- Hay 165 valores repetidos.
Limpieza de datos y nuevas funciones
1# Eliminar valores nulos
2df.dropna(inplace=True)
3
4# Eliminar registros duplicados
5df.drop_duplicates(inplace=True)
6
7# Eliminar la columna 'loan_grade' (posiblemente redundante o no necesaria)
8df.drop(['loan_grade'], axis=1, inplace=True)
Características discretas y continuas
1# Dividir variables en continuas y categóricas (esto no aplica después de aplicar PCA)
2col_cont = []
3col_dis = []
4
5# Función para separar tipos de variables según sus nombres o tipo de dato
6def divide_feature_types(data):
7 '''
8 Recibe un DataFrame y devuelve dos listas: una con columnas numéricas continuas
9 y otra con columnas categóricas o discretas.
10 '''
11 col_cont = []
12 col_dis = []
13 for c in data.columns:
14 if ('person_home_ownership' in c) or ('loan_intent' in c) or ('loan_status' in c) or ('cb_person_default_on_file' in c):
15 col_dis.append(c)
16 elif (data[c].dtype == 'O'):
17 col_dis.append(c)
18 else:
19 col_cont.append(c)
20 return col_cont, col_dis
21
22# Aplicar la función al DataFrame
23col_cont, col_dis = divide_feature_types(df)
24
25print('Continuous numerical features: ', col_cont)
26print('Categorical or discrete features: ', col_dis)
27
28# Crear un pairplot con las variables continuas menos 'loan_int_rate', coloreado por 'person_home_ownership'
29col_cont_minus_loan_int_rate = [col for col in col_cont if col != 'loan_int_rate']
30to_plot = col_cont_minus_loan_int_rate + ['person_home_ownership']
31sns.pairplot(df[to_plot], hue='person_home_ownership', palette=["#682F2F", "yellow", 'Skyblue', 'orange'])
1# Crear un pairplot con las variables continuas (excluyendo 'loan_int_rate')
2# y relacionarlas con la variable categórica 'loan_intent'
3col_cont_minus_loan_int_rate = [col for col in col_cont if col != 'loan_int_rate']
4to_plot = col_cont_minus_loan_int_rate + ['loan_intent']
5sns.pairplot(df[to_plot], hue='loan_intent', palette='colorblind')
1# Crear un pairplot con las variables continuas (excluyendo 'loan_int_rate')
2# y relacionarlas con la variable categórica 'cb_person_default_on_file'
3col_cont_minus_loan_int_rate = [col for col in col_cont if col != 'loan_int_rate']
4to_plot = col_cont_minus_loan_int_rate + ['cb_person_default_on_file']
5sns.pairplot(df[to_plot], hue='cb_person_default_on_file', palette=["#682F2F", "#F3AB60"])
1# Crear la matriz de correlación para analizar la relación entre variables numéricas
2plt.figure(figsize=(12, 7))
3sns.heatmap(df.corr(), annot=True, cmap='coolwarm')
Creamos un pairplot analizando las variables continuas en relación con variables discretas. Podemos observar que hay valores atípicos en los años, en los ingresos y en los años de empleo de la persona. Tendremos que realizar nuevamente una limpieza de datos. En cuanto a la correlación, podemos ver que no hay presencia de multicolinealidad.
Limpieza de datos y nuevas características (repetición del paso)
1# Eliminar outliers en la edad (person_age < 90)
2df = df[df['person_age'] < 90]
3
4# Eliminar outliers en la duración del empleo (person_emp_length < 60)
5df = df[df['person_emp_length'] < 60]
6
7# Crear un pairplot con las variables continuas (excepto 'loan_int_rate')
8# y relacionarlas con la variable categórica 'cb_person_default_on_file'
9col_cont_minus_loan_int_rate = [col for col in col_cont if col != 'loan_int_rate']
10to_plot = col_cont_minus_loan_int_rate + ['cb_person_default_on_file']
11sns.pairplot(df[to_plot], hue='cb_person_default_on_file', palette=["#F3AB60", "#682F2F"])
PREPROCESAMIENTO DE DATOS
En esta sección escalaremos los datos para mejorar el rendimiento y también generaremos variables dummy para las variables categóricas.
1# Codificar variables categóricas con LabelEncoder
2le = LabelEncoder()
3
4# Transformar person_home_ownership
5df['person_home_ownership'] = le.fit_transform(df['person_home_ownership'])
6
7# Transformar loan_intent
8df['loan_intent'] = le.fit_transform(df['loan_intent'])
9
10# Transformar cb_person_default_on_file
11df['cb_person_default_on_file'] = le.fit_transform(df['cb_person_default_on_file'])
12
13# Crear copia del dataframe
14dt = df.copy()
15
16# Separar variables dependientes e independientes
17dumi = dt['cb_person_default_on_file'].values
18y = dt['loan_status'] # Variable dependiente binaria
19x = dt.drop(['cb_person_default_on_file', 'loan_status'], axis=1)
20
21# Escalar variables numéricas (sin incluir las binarias)
22x = x.copy()
23scaler = StandardScaler()
24scaler.fit(x)
25x_scaler = pd.DataFrame(scaler.transform(x), columns=x.columns)
26
27# Volver a unir la variable binaria
28x_scaler['cb_person_default_on_file'] = dumi
PCA - ANÁLISIS DE COMPONENTES PRINCIPALES
El Análisis de Componentes Principales (PCA) es un procedimiento estadístico que utiliza una transformación ortogonal para convertir un conjunto de observaciones de variables posiblemente correlacionadas en un conjunto de valores de variables linealmente no correlacionadas llamadas componentes principales. Esta técnica se utiliza para simplificar la complejidad de datos de alta dimensión, conservando las tendencias y patrones. Lo logra transformando los datos a un número menor de dimensiones, lo que puede facilitar su interpretación y visualización.
1# Aplicar PCA
2pca = PCA()
3
4# Entrenar el modelo PCA
5pca.fit(x_scaler)
6
7# Transformar los datos con PCA
8x_scaler_pca = pca.transform(x_scaler)
9
10# Convertir a DataFrame
11x_scaler_pca = pd.DataFrame(x_scaler_pca)
12
13# Analizar la varianza explicada por cada componente
14var = pca.explained_variance_ratio_
15
16# Imprimir número de componentes y varianza
17print(len(var))
18print(var)
1# Calcular la varianza acumulada
2cum_var = np.cumsum(np.round(var, decimals=4) * 100)
3
4# Mostrar resultados por componente
5for i, acumulado in enumerate(cum_var, start=1):
6 print(f"{i} componente: {acumulado:.2f}%")
7
8# Graficar la varianza acumulada
9plt.figure(figsize=(10, 6))
10plt.plot(cum_var, 'r-x', marker='X')
11plt.xlabel('Número de Componentes')
12plt.ylabel('Varianza Acumulada (%)')
13plt.title('Varianza Acumulada vs. Número de Componentes')
14plt.grid(True)
15plt.show()
1# Eliminar componentes que no aportan información
2x_scaler_pca.drop([7, 8, 9], axis=1, inplace=True)
CONSTRUCCIÓN DEL MODELO - REGRESIÓN LOGÍSTICA
1# Separar los datos en entrenamiento y prueba
2x_train, x_test, y_train, y_test = train_test_split(x_scaler_pca, y, test_size=0.2)
3
4# Ver distribución original de la variable objetivo
5print(df['loan_status'].value_counts())
6
7# Balanceo de clases en loan_status - Oversampling con SMOTE
8smote = SMOTE(sampling_strategy='minority', k_neighbors=6)
9x_train_sm, y_train_sm = smote.fit_resample(x_train, y_train)
10print(len(x_train_sm))
11print(len(y_train_sm))
12
13# Balanceo de clases en loan_status - Undersampling con RandomUnderSampler
14rus = RandomUnderSampler(sampling_strategy='majority')
15x_train_us, y_train_us = rus.fit_resample(x_train, y_train)
16print(len(x_train_us))
17print(len(y_train_us))
1# Modelo de regresión logística con Keras
2de tf.keras.models import Sequential
3de tf.keras.layers import Dense
4
5# Definir el modelo secuencial con una sola capa y función sigmoide
6model = Sequential([
7 Dense(1, activation='sigmoid') # Ideal para clasificación binaria, entrega una probabilidad entre 0 y 1
8])
9
10# Compilar el modelo especificando la función de pérdida, optimizador y métrica
11model.compile(
12 loss='binary_crossentropy',
13 optimizer='adam',
14 metrics=['accuracy']
15)
16
17# Entrenar el modelo con datos balanceados por oversampling
18model.fit(
19 x_train_sm,
20 y_train_sm,
21 epochs=100,
22 batch_size=32,
23 verbose=0 # No mostrar logs de entrenamiento
24)
25
26# Evaluar el modelo con el conjunto de prueba
27loss, accuracy = model.evaluate(x_test, y_test)
28print(f'Model accuracy : {accuracy*100:.2f}%')
29print(f'Average error committed by the model: {loss:.3f}')
30
31# Realizar predicciones con el conjunto de entrenamiento balanceado
32y_pred_train = model.predict(x_train_sm)
33
34# Convertir y_train_sm a arreglo de NumPy
35y_train_sm_original = np.array(y_train_sm)
1# Curva ROC (Receiver Operating Characteristic)
2# Esta curva nos permite visualizar la capacidad del modelo para distinguir entre clases
3
4# Calcular la TPR y FPR a partir de las predicciones del modelo y los datos reales
5fpr, tpr, _ = roc_curve(y_train_sm_original, y_pred_train)
6
7# Calcular el área bajo la curva ROC (AUC)
8auc_score = auc(fpr, tpr)
9
10# Graficar la curva ROC
11plt.plot(fpr, tpr, label='Curva ROC (AUC = {:.3f})'.format(auc_score))
12plt.xlabel('Tasa de Falsos Positivos (FPR)')
13plt.ylabel('Tasa de Verdaderos Positivos (TPR)')
14plt.title('Curva ROC')
15plt.legend()
16plt.grid(True)
17plt.show()
18
19# Interpretación del resultado del AUC
20print('Un AUC de 0.5 indica que el modelo no es mejor que el azar para distinguir entre clases.')
21print('Un AUC de 1 indica que el modelo es perfecto para distinguir entre clases.')
Un AUC de 0.5 indica que el modelo no es mejor que el azar para distinguir entre clases. Un AUC de 1 indica que el modelo distingue perfectamente entre las clases.
1# Calcular precisión y recall a partir de las predicciones del modelo
2precision, recall, _ = precision_recall_curve(y_train_sm_original, y_pred_train)
3
4# Graficar la curva precisión-recall
5plt.plot(precision, recall, label='Curva Precisión-Recall')
6plt.xlabel('Precisión')
7plt.ylabel('Recall (Sensibilidad)')
8plt.title('Curva Precisión vs. Recall')
9plt.legend()
10plt.grid(True)
11plt.show()
12
13# Explicación de las métricas
14# Precisión: Mide cuántas de las predicciones positivas fueron correctas.
15# Recall: Mide cuántos de los casos positivos reales fueron correctamente identificados.
16# En resumen:
17# - Precisión se enfoca en la calidad de las predicciones positivas.
18# - Recall se enfoca en la cobertura de los positivos reales.
19
20print('Un modelo ideal tendría una curva precisión-recall que se acerque a la esquina superior derecha.')
Un modelo ideal tendría una curva de precisión-recall que se acerque a la esquina superior derecha.
1# Crear la matriz de confusión a partir de las predicciones del modelo
2confusion_matrix = confusion_matrix(y_train_sm, y_pred_train > 0.5)
3
4# Imprimir la matriz de confusión
5print(confusion_matrix)
6# Salida esperada:
7# [[13458 4410]
8# [ 4062 13806]]
- 13,377: Casos que el modelo predijo como positivos y en realidad son positivos (verdaderos positivos)
- 4,440: Casos que el modelo predijo como positivos pero en realidad son negativos (falsos positivos)
- 3,941: Casos que el modelo predijo como negativos pero en realidad son positivos (falsos negativos)
- 13,876: Casos que el modelo predijo como negativos y en realidad son negativos (verdaderos negativos)
1# Predecir sobre el conjunto de prueba y convertir a valores binarios
2y_pred_binario = model.predict(x_test) > 0.5
3
4# Calcular el F1-score para evaluar el balance entre precisión y recall en un conjunto desequilibrado
5f1 = f1_score(y_test, y_pred_binario, average='binary') # 'binary' para clases desequilibradas
6
7# Mostrar el F1-score
8print(f"F1-score: {f1}")
9# Salida esperada:
10# F1-score: 0.580875781948168
El modelo tiene un desempeño aceptable para el riesgo crediticio
Comentarios
Cargando comentarios...