Hernan Perci Nuñez / Modelo de riesgo de crédito con P… Published at Apr 8,
Palomino 2022
Información de contacto
Tutor: Michael Crabtree
Student: Hernan Perci Nuñez Palomino
Email: hernanperci@[Link]
Exploración y preparación de datos de
préstamo
En esta primera parte, se discutira el concepto de riesgo de crédito. Usando tablas cruzadas y diagramas, se
explorara un conjunto de datos reales. Antes de aplicar el aprendizaje automático se procesara estos datos.
Explorando los datos de crédito
# Libreria pandas
import pandas as pd
# Importación de los datos crediticios desde un archivo csv
cr_loan = pd.read_csv("[Link]")
# Tabla interactiva de los datos de crédito en deepnote
cr_loan
person_age int64 person_income in… person_home_o… person_emp_len… loan_intent object loan_grade object
20 - 144 4000 - 6000000 RENT 50.5%
MORTGAGE 41.3%
2 others 8.3%
0 22 59000 RENT 123 PERSONAL D
1 21 9600 OWN 5 EDUCATION B
2 25 9600 MORTGAGE 1 MEDICAL C
3 23 65500 RENT 4 MEDICAL C
4 24 54400 RENT 8 MEDICAL C
5 21 9900 OWN 2 VENTURE A
6 26 77100 RENT 8 EDUCATION B
7 24 78956 RENT 5 MEDICAL B
8 24 83000 RENT 8 PERSONAL A
9 21 10000 OWN 6 VENTURE D
# Libreria pyplot de matplotlib
import [Link] as plt
# Distribución de los montos de préstamo
patches = [Link](x=cr_loan['loan_amnt'], bins='auto', color='blue',alpha=0.7, rwidth=0.85)
[Link]("Montos de préstamo")
[Link]()
# Diagrama de dispersión de ingresos vs edad
[Link](cr_loan['person_income'], cr_loan['person_age'],c='blue', alpha=0.5)
[Link]('Ingreso personal')
[Link]('Edad')
[Link]()
Podemos ver una correlación positiva con la edad y los ingresos, lo que podría significar que estos beneficiarios
mayores están más avanzados en su carrera y, por lo tanto, ganan salarios más altos. También parece haber un
valor atípico en los datos.
Tabulaciones cruzadas y tablas dinámicas
# Tabla cruzada de la intención del préstamo y el estado del préstamo
print([Link](cr_loan['loan_intent'], cr_loan['loan_status'], margins = True))
loan_status 0 1 All
loan_intent
DEBTCONSOLIDATION 3722 1490 5212
EDUCATION 5342 1111 6453
HOMEIMPROVEMENT 2664 941 3605
MEDICAL 4450 1621 6071
PERSONAL 4423 1098 5521
VENTURE 4872 847 5719
All 25473 7108 32581
# Tabla cruzada de la propiedad de la vivienda, el estado del préstamo y el grado
print([Link](cr_loan['person_home_ownership'],[cr_loan['loan_status'],cr_loan['loan_grade']]))
loan_status 0 1 \
loan_grade A B C D E F G A B C
person_home_ownership
MORTGAGE 5219 3729 1934 658 178 36 0 239 324 321
OTHER 23 29 11 9 2 0 0 3 5 6
OWN 860 770 464 264 26 7 0 66 34 31
RENT 3602 4222 2710 554 137 28 1 765 1338 981
loan_status
loan_grade D E F G
person_home_ownership
MORTGAGE 553 161 61 31
OTHER 11 6 2 0
OWN 18 31 8 5
RENT 1559 423 99 27
# Tabla cruzada de la propiedad de la vivienda, el estado del préstamo y el porcentaje de ingreso pro
print([Link](cr_loan['person_home_ownership'], cr_loan['loan_status'], values=cr_loan['loan_perc
loan_status 0 1
person_home_ownership
MORTGAGE 0.146504 0.184882
OTHER 0.143784 0.300000
OWN 0.180013 0.297358
RENT 0.144611 0.264859
# Create a box plot of percentage income by loan status
cr_loan.boxplot(column = ['loan_percent_income'], by = 'loan_status')
[Link]('Porcentaje del ingreso promedio por estado del préstamo')
[Link]('')
[Link]()
Parece que el porcentaje medio de ingresos por impago es mayor. Esto podría indicar que esos destinatarios tienen
una relación deuda-ingreso que ya es demasiado alta.
Encontrar valores atípicos con tablas cruzadas
# Tabla cruzada para el estado del préstamo, la propiedad de la vivienda y la duración máxima del emp
print([Link](cr_loan['loan_status'],cr_loan['person_home_ownership'],
values=cr_loan['person_emp_length'], aggfunc='max'))
# Matriz de índices donde la duración del empleo sea superior a 60
indices = cr_loan[cr_loan['person_emp_length'] > 60].index
# Nuevo marco de datos sin registros atípicos
cr_loan_new = cr_loan.drop(indices)
# Tabla cruzada del nuevo marco de datos incluyendo la duración mínima del empleo
print([Link](cr_loan_new['loan_status'],cr_loan_new['person_home_ownership'],values=cr_loan_new
person_home_ownership MORTGAGE OTHER OWN RENT
loan_status
0 123.0 24.0 31.0 41.0
1 34.0 11.0 17.0 123.0
max min
person_home_ownership MORTGAGE OTHER OWN RENT MORTGAGE OTHER OWN RENT
loan_status
0 38.0 24.0 31.0 41.0 0.0 0.0 0.0 0.0
1 34.0 11.0 17.0 27.0 0.0 0.0 0.0 0.0
En general, con los datos de crédito, las columnas clave como person_emp_length, que corresponde a la duración
del empleo de la persona, son de alta calidad, pero siempre hay margen para el error. Con esto en mente,
construimos nuestra intuición para detectar valores atípicos.
Visualización de valores atípicos de crédito
# Diagrama de dispersión para la edad y la cantidad
[Link](cr_loan['person_age'], cr_loan['loan_amnt'], c='blue', alpha=0.5)
[Link]("Person Age")
[Link]("Loan Amount")
[Link]()
import matplotlib
# Marco de datos con personas de menos de 100 años de edad
cr_loan_new = cr_loan.drop(cr_loan[cr_loan['person_age'] > 100].index)
# Diagrama de dispersión de la edad y la tasa de interés
colors = ["blue","red"]
[Link](cr_loan_new['person_age'], cr_loan_new['loan_int_rate'],
c = cr_loan_new['loan_status'],
cmap = [Link](colors),
alpha=0.5)
[Link]("Person Age")
[Link]("Loan Interest Rate")
[Link]()
Observe que en el último gráfico tenemos loan_status, el cual representa el estado de préstamo, como etiqueta para
los colores. Esto muestra un color diferente dependiendo de la clase. En este caso, se trata de incumplimiento y no
incumplimiento de préstamos, y parece que hay más incumplimientos con tasas de interés altas.
Reemplazo de datos de crédito faltantes
# Matriz de columnas con valores nulos
print(cr_loan.columns[cr_loan.isnull().any()])
# Cinco filas superiores con valores nulos para la duración del empleo
print(cr_loan[cr_loan['person_emp_length'].isnull()].head())
# Reemplazo de los valores nulos con la media para la variable duraciones de empleo
cr_loan['person_emp_length'].fillna((cr_loan['person_emp_length'].median()), inplace=True)
# Histograma de la duración del empleo
n, bins, patches = [Link](cr_loan['person_emp_length'], bins='auto', color='blue')
[Link]("Duración del empleo")
[Link]()
Index(['person_emp_length', 'loan_int_rate'], dtype='object')
person_age person_income person_home_ownership person_emp_length \
105 22 12600 MORTGAGE NaN
222 24 185000 MORTGAGE NaN
379 24 16800 MORTGAGE NaN
407 25 52000 RENT NaN
408 22 17352 MORTGAGE NaN
loan_intent loan_grade loan_amnt loan_int_rate loan_status \
105 PERSONAL A 2000 5.42 1
222 EDUCATION B 35000 12.42 0
379 DEBTCONSOLIDATION A 3900 NaN 1
407 PERSONAL B 24000 10.74 1
408 EDUCATION C 2250 15.27 0
loan_percent_income cb_person_default_on_file cb_person_cred_hist_length
105 0.16 N 4
222 0.19 N 2
379 0.23 N 3
407 0.46 N 2
408 0.13 Y 3
El objetivo aquí es mantener la mayor cantidad de datos posible. También es importante verificar la distribución de
esa característica para ver si cambió.
Eliminación de los datos faltantes
# Número de vacios
print(cr_loan['loan_int_rate'].isnull().sum())
# Índices nulos
indices = cr_loan[cr_loan['loan_int_rate'].isnull()].index
# Nueva base de datos sin valores nulos
cr_loan_clean = cr_loan.drop(indices)
3116
Ahora que se han procesado los datos faltantes y los valores atípicos, los datos están listos para el modelado. En el
caso de variables cualitativas, se podría contar si son pocas para asignarle a una categoría otros.
Regresión logística para probabilidad de
incumplimiento o default
Con los datos del préstamo completamente preparados, se analizará el modelo de regresión logística, que es un
estándar en el modelado de riesgos. Se calculará los componentes de este modelo y cómo calificar su rendimiento.
Una vez se hayam creado predicciones, podemos explorar el impacto financiero de utilizar este modelo.
Modelo de regresión logística
from sklearn.linear_model import LogisticRegression
import numpy as np
# Importación de los datos crediticios sin vacios desde un archivo csv
cr_loan_clean = pd.read_csv("DatosCrediticios_SinVaciosNiOutliers.csv")
# Variables independiente y dependiente
X = cr_loan_clean[['loan_int_rate']]
y = cr_loan_clean[['loan_status']]
# Modelo de regresión logística y su ajuste
clf_logistic_single = LogisticRegression()
clf_logistic_single.fit(X, [Link](y))
# Parametros del modelo
print(clf_logistic_single.get_params())
{'C': 1.0, 'class_weight': None, 'dual': False, 'fit_intercept': True, 'intercept_scaling': 1, 'l1_ratio': None, 'max
# Interceptos del modelo
print(clf_logistic_single.intercept_)
[-4.45785901]
El modelo pudo ajustarse a los datos y establecer algunos parámetros internamente. Se verá que pasa si usamos
más de una variable.
Regresión logística multivariada
# Variables independientes
X_multi = cr_loan_clean[['loan_int_rate','person_emp_length']]
# Estatus del préstamo como variable a predecir
y = cr_loan_clean[['loan_status']]
# Modelo de regresión logística multivariada
clf_logistic_multi = LogisticRegression(solver='lbfgs').fit(X_multi, [Link](y))
# Print the intercept of the model
print(clf_logistic_multi.intercept_)
[-4.21645549]
El nuevo modelo tiene un intercepto más cercano a cero. Esto significa que las probabilidades logarítmicas de un no
incumplimiento se acercan a cero.
Creación de conjuntos de entrenamiento y prueba
from sklearn.model_selection import train_test_split
# Variables independientes y dependiente
# X = cr_loan_clean[['loan_int_rate','person_emp_length','person_income']]
X = cr_loan_clean.loc[:,['loan_int_rate','person_emp_length','person_income']]
y = cr_loan_clean[['loan_status']]
# Conjuntos de entrenamiento y prueba
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=.4, random_state=123)
# Ajuste del modelo de regresión logística
clf_logistic = LogisticRegression(solver='lbfgs').fit(X_train, [Link](y_train))
# Coeficientes del modelo
print(clf_logistic.coef_)
[[ 1.28517496e-09 -2.27622202e-09 -2.17211991e-05]]
Los coeficientes indican la importancia de cada columna o característica para la predicción. Cuanto más positivo es
el valor, más predice los incumplimientos.
Codificación dummie
# Conjutos de datos numéricos y no numéricos
cred_num = cr_loan_clean.select_dtypes(exclude=['object'])
cred_str = cr_loan_clean.select_dtypes(include=['object'])
# Codificación dummie al conjunto de variables categóricas
cred_str_onehot = pd.get_dummies(cred_str)
# Unión de las variables numéricas con las codificadas
cr_loan_prep = [Link]([cred_num, cred_str_onehot], axis=1)
# Lista de columnas del nuevo cnjunto de datos
print(cr_loan_prep.columns)
Index(['person_age', 'person_income', 'person_emp_length', 'loan_amnt',
'loan_int_rate', 'loan_status', 'loan_percent_income',
'cb_person_cred_hist_length', 'person_home_ownership_MORTGAGE',
'person_home_ownership_OTHER', 'person_home_ownership_OWN',
'person_home_ownership_RENT', 'loan_intent_DEBTCONSOLIDATION',
'loan_intent_EDUCATION', 'loan_intent_HOMEIMPROVEMENT',
'loan_intent_MEDICAL', 'loan_intent_PERSONAL', 'loan_intent_VENTURE',
'loan_grade_A', 'loan_grade_B', 'loan_grade_C', 'loan_grade_D',
'loan_grade_E', 'loan_grade_F', 'loan_grade_G',
'cb_person_default_on_file_N', 'cb_person_default_on_file_Y'],
dtype='object')
Predicción de probabilidades de incumplimiento
# Variables independientes y dependiente
X = cr_loan_prep.loc[:,cr_loan_prep.columns != 'loan_status']
y = cr_loan_prep[['loan_status']]
# Conjuntos de entrenamiento y prueba
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=.4, random_state=123)
# Entrenamiento del modelo de regresión logística
clf_logistic = LogisticRegression(solver='lbfgs').fit(X_train, [Link](y_train))
# Predicciones de probabilidad para el estado del préstamo usando datos de prueba
preds = clf_logistic.predict_proba(X_test)
# Marcos de datos de las primeras cinco predicciones y las primeras cinco etiquetas verdaderas
preds_df = [Link](preds[:,1][0:20], columns = ['prob_default'])
true_df = y_test.head(20)
# Comparación de los dos marcos de datos
print([Link]([true_df.reset_index(drop = True), preds_df], axis = 1))
loan_status prob_default
0 1 0.445779
1 1 0.223447
2 0 0.288558
3 0 0.169358
4 1 0.114182
5 1 0.490257
6 0 0.162057
7 0 0.396211
8 1 0.217428
9 1 0.481440
10 1 0.243327
11 1 0.404546
12 0 0.063662
13 0 0.254207
14 1 0.416166
15 0 0.367900
16 0 0.246369
17 1 0.027882
18 0 0.106589
19 0 0.347888
Tenemos algunas predicciones ahora, pero no parecen muy precisas. Parece que la mayoría de las filas con estado
de préstamo en 1 tienen una baja probabilidad de incumplimiento. Veremos si podemos determinar qué tan preciso
es el modelo completo.
Informe de clasificación de default
# Dataframe de las probabilidades de incumplimiento
preds_df = [Link](preds[:,1], columns = ['prob_default'])
# Estado del préstamo según la probabilidad asignada
preds_df['loan_status'] = preds_df['prob_default'].apply(lambda X: 1 if X > .5 else 0)
# Recuento de filas para cada estado de préstamo
print(preds_df['loan_status'].value_counts())
0 11175
1 609
Name: loan_status, dtype: int64
from [Link] import classification_report
# Reporte de clasificación
target_names = ['Non-Default', 'Default']
print(classification_report(y_test, preds_df['loan_status'], target_names=target_names))
precision recall f1-score support
Non-Default 0.81 0.98 0.89 9198
Default 0.71 0.17 0.27 2586
accuracy 0.80 11784
macro avg 0.76 0.57 0.58 11784
weighted avg 0.79 0.80 0.75 11784
Se predijo que casi todo nuestro conjunto de prueba no sería default. El 17 % de los defaults reales se predijeron
correctamente.
Selección de métricas del informe
from [Link] import precision_recall_fscore_support
# Valores no promedio del informe
print(precision_recall_fscore_support(y_test,preds_df['loan_status']))
(array([0.80742729, 0.71264368]), array([0.98097412, 0.16782676]), array([0.8857802 , 0.27167449]), array([9198, 2586
# Dos primeros números del informe
print(precision_recall_fscore_support(y_test,preds_df['loan_status'])[0:2])
(array([0.80742729, 0.71264368]), array([0.98097412, 0.16782676]))
De esta forma se extrae valores específicos del informe para almacenarlos más tarde para compararlos o usarlos
para verificar el rendimiento de la cartera u otros cálculos posteriores.
Gráfico del modelo de puntaje crediticio
# Predicciones para la probabilidad de incumplimiento
preds = clf_logistic.predict_proba(X_test)
# Puntuación de precisión del modelo
print(clf_logistic.score(X_test, y_test))
0.8025288526816021
from sklearn import metrics
# Gáfico de la curva ROC de las probabilidades de incumplimiento
prob_default = preds[:, 1]
fallout, sensitivity, thresholds = metrics.roc_curve(y_test, prob_default)
[Link](fallout, sensitivity, color = 'darkorange')
[Link]([0, 1], [0, 1], linestyle='--')
[Link]()
from [Link] import roc_auc_score
# Área debajo de la curva
auc = roc_auc_score(y_test, prob_default)
auc
0.7643248801355148
La precisión de este modelo es de alrededor del 80 % y la puntuación AUC es del 76%. Lo que nos muestra el
gráfico ROC es la tasa de verdaderos positivos frente a la tasa de falsos positivos.
Umbrales y matrices de confusión
# Estado del préstamo según el umbral 0.4
preds_df['loan_status'] = preds_df['prob_default'].apply(lambda x: 1 if x > 0.4 else 0)
from [Link] import confusion_matrix
# Matriz de confusión
tn, fp, fn, tp = confusion_matrix(y_test,preds_df['loan_status']).ravel()
default_recall = tp / (tp + fn)
default_recall
0.46403712296983757
# Estado del prestamo según el umbral 0.5
preds_df['loan_status'] = preds_df['prob_default'].apply(lambda x: 1 if x > 0.5 else 0)
# Matriz de confusión
tn, fp, fn, tp = confusion_matrix(y_test,preds_df['loan_status']).ravel()
default_recall = tp / (tp + fn)
default_recall
0.16782675947409126
El valor del umbral de 0.4 nos da un recall para el incumplimiento de aproximadamente 0.46.
El valor del umbral de 0.5 nos da un recall para el incumplimiento de aproximadamente 0.17.
Performance según el umbral elegido
avg_loan_amnt = cr_loan_clean['loan_amnt'].mean()
# Valores del estado del préstamo en función del nuevo umbral
preds_df['loan_status'] = preds_df['prob_default'].apply(lambda x: 1 if x > 0.4 else 0)
# Número de impagos de préstamos a partir de los datos de predicción
num_defaults = preds_df['loan_status'].value_counts()[1]
# Default recall del reporte de clasificación
default_recall = precision_recall_fscore_support(y_test,preds_df['loan_status'])[1][1]
# Impacto estimado de la nueva tasa de recuperación predeterminada
print(avg_loan_amnt * num_defaults * (1 - default_recall))
9872265.223119883
Según nuestras estimaciones, esta pérdida rondaría los 9.8 millones de dólares.
Selección del umbral
# Conjuntos de umbrales y respectivas métricas
i = 0.2
# Umbrales
thresh = []
# recall de incumplimiento
def_recalls = []
# recall de no incumplimiento
nondef_recalls = []
# accuracy
accs = []
while i < 0.65 :
[Link](i)
preds_df['loan_status'] = preds_df['prob_default'].apply(lambda x: 1 if x > i else 0)
def_recalls.append(precision_recall_fscore_support(y_test,preds_df['loan_status'])[1][1])
nondef_recalls.append(precision_recall_fscore_support(y_test,preds_df['loan_status'])[1][0])
[Link](precision_recall_fscore_support(y_test,preds_df['loan_status'])[0][1])
i = i + 0.025
[Link](thresh,def_recalls)
[Link](thresh,nondef_recalls)
[Link](thresh,accs)
[Link]("Probability Threshold")
[Link](["Default Recall","Non-default Recall","Model Accuracy"])
[Link]()
El punto donde deberían converger las tres líneas sería un excelente punto de partida, pero aclarar que todos los
préstamos alrededor de 0.275 como predeterminados probablemente no sea práctico.
Gradient Boosted Trees usando
XGBoost
Los árboles de decisión son otro modelo estándar de riesgo de crédito. Se usará el paquete XGBoost en Python
para crear árboles potenciados por gradientes. Después de desarrollar estos modelos, haremos una prueba de
estrés de su desempeño y seleccionaremos las columnas en datos no balanceados.
Árboles para el incumplimiento de pago de la deuda
# Importación de los datos crediticios con variables dummies listo para modelar csv
cr_loan_prep = pd.read_csv("DatosCrediticios_ListoParaModelar.csv")
# Variables independientes y dependiente
X = cr_loan_prep.loc[:,cr_loan_prep.columns != 'loan_status']
y = cr_loan_prep[['loan_status']]
# Conjuntos de entrenamiento y prueba
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=.4, random_state=123)
# Ajuste del modelo
import xgboost as xgb
clf_gbt = [Link]().fit(X_train, [Link](y_train))
# Predicciones de la probabilidad de default con el modelo
gbt_preds = clf_gbt.predict_proba(X_test)
# Dataframes con probabilidades de dafault predicha y datos reales
preds_df = [Link](gbt_preds[:,1][0:20], columns = ['prob_default'])
true_df = y_test.head(20)
# Comparación de los dos marcos de datos
print([Link]([true_df.reset_index(drop = True), preds_df], axis = 1))
/root/venv/lib/python3.7/site-packages/xgboost/[Link]: UserWarning: The use of label encoder in XGBClassifie
[Link](label_encoder_deprecation_msg, UserWarning)
[22:49:05] WARNING: ../src/[Link]: Starting in XGBoost 1.3.0, the default evaluation metric used with the ob
loan_status prob_default
0 1 0.990942
1 1 0.983987
2 0 0.000807
3 0 0.001239
4 1 0.084892
5 1 0.021722
6 0 0.010304
7 0 0.000375
8 1 0.976663
9 1 0.992004
10 1 0.173216
11 1 0.997188
12 0 0.002653
13 0 0.001659
14 1 0.992790
15 0 0.004816
16 0 0.051240
17 1 0.998005
18 0 0.413108
19 0 0.147902
Las predicciones no tienen el mismo aspecto que con LogisticRegression(). Este modelo ya está prediciendo con
precisión la probabilidad de incumplimiento para algunos préstamos con un valor real de 1 en estado del préstamo.
Desempeño de la cartera impulsado por el gradiente
# Marco de datos llamado cartera para combinar las probabilidades de incumplimiento para
# ambos modelos (XGBoost y regresión logistica), la pérdida en caso de incumplimiento (20%
# por ahora) y el préstamo que se asumirá como la exposición en caso de incumplimiento.
portfolio = [Link]({'gbt_prob_default': gbt_preds[:,1], 'lr_prob_default': preds[:,1], 'lgd':
portfolio
gbt_prob_default f… lr_prob_default fl… lgd float64 loan_amnt int64
1.43470913371857… 5.26111040465612… 0.2 - 0.2 500 - 35000
28… 0.9909417033 0.4457786018 0.2 15000
22… 0.9839872122 0.223446533 0.2 11200
138… 0.0008073628414 0.288558257 0.2 15000
3145 0.001239418169 0.1693575271 0.2 10800
148… 0.08489220589 0.1141819733 0.2 3000
166… 0.02172206342 0.4902568814 0.2 7750
216… 0.01030396204 0.1620574551 0.2 10000
198… 0.0003745816357 0.3962111956 0.2 15000
125… 0.9766631722 0.2174275844 0.2 15000
8873 0.9920044541 0.4814399584 0.2 9000
# Creación de dos columnas usando la fórmula de la perdida esperada para cada modelo
portfolio['gbt_expected_loss'] = portfolio['gbt_prob_default'] * portfolio['lgd'] * portfolio['loan_a
portfolio['lr_expected_loss'] = portfolio['lr_prob_default'] * portfolio['lgd'] * portfolio['loan_amn
# Suma de la perdida esperada para el modelo de regresión lineal
print('LR perdida esperada: ', [Link](portfolio['lr_expected_loss']))
# Suma de la perdida esperada para el modelo de XGBoost
print('GBT perdida esperada: ', [Link](portfolio['gbt_expected_loss']))
LR perdida esperada: 5596776.979852879
GBT perdida esperada: 5383982.809227714
La pérdida total esperada para el modelo XGBClassifier() debería ser menor pero en este caso sucede lo contrario.
Cuando hablamos de exactitud y precisión, el objetivo es generar modelos que tengan una pérdida esperada baja.
Evaluación de árboles potenciados por gradiente
# Predicción del estado de préstamo
gbt_preds = clf_gbt.predict(X_test)
# Cadena de valores de los resulta
print(gbt_preds)
# Reporte de clasificación del modelo
target_names = ['Non-Default', 'Default']
print(classification_report(y_test, gbt_preds, target_names=target_names))
[1 1 0 ... 0 0 0]
precision recall f1-score support
Non-Default 0.93 0.99 0.96 9198
Default 0.94 0.74 0.83 2586
accuracy 0.93 11784
macro avg 0.94 0.86 0.89 11784
weighted avg 0.93 0.93 0.93 11784
A comparación del modelo logistico, este modelo parece tener buen potencial ya que presenta un default recall y
accuracy más alto.
Importancia de seleccionar variables en la predicción
de incumplimiento
# Selección de algunas variables independientes de ejemplo
X = cr_loan_prep[['person_income','loan_int_rate','loan_percent_income','loan_amnt','person_home_owne
y = cr_loan_prep[['loan_status']]
# Conjuntos de entrenamiento y prueba
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=.4, random_state=123)
# Entrenamiento del modelo
clf_gbt = [Link]().fit(X_train,[Link](y_train))
# Importancias de las variables en el modelo
print(clf_gbt.get_booster().get_score(importance_type = 'weight'))
[22:49:08] WARNING: ../src/[Link]: Starting in XGBoost 1.3.0, the default evaluation metric used with the ob
/root/venv/lib/python3.7/site-packages/xgboost/[Link]: UserWarning: The use of label encoder in XGBClassifie
[Link](label_encoder_deprecation_msg, UserWarning)
{'person_income': 1299.0, 'loan_int_rate': 1001.0, 'loan_percent_income': 515.0, 'loan_amnt': 519.0, 'person_home_own
La importancia para el préstamo de grado F es solo 9 en este caso. Esto podría deberse a que hay muy pocos
préstamos de grado F. Si bien los préstamos de grado F no agregan mucho a las predicciones aquí, pueden afectar
la importancia de otras variables de entrenamiento.
Visualización de la importancia de la variable
# Selección de algunas variables independientes de ejemplo
X2 = cr_loan_prep[['loan_int_rate','person_emp_length']]
y = cr_loan_prep[['loan_status']]
# Conjuntos de entrenamiento y prueba
X2_train, X2_test, y_train, y_test = train_test_split(X2, y, test_size=.4, random_state=123)
# Entrenamiento de un modelo con las 2 variables seleccionadas
clf_gbt2 = [Link]().fit(X2_train,[Link](y_train))
# Importancia de las variables para este modelo
xgb.plot_importance(clf_gbt2, importance_type = 'weight')
[Link]()
[22:49:10] WARNING: ../src/[Link]: Starting in XGBoost 1.3.0, the default evaluation metric used with the ob
X3 = cr_loan_prep[['person_income','loan_int_rate','loan_percent_income']]
y = cr_loan_prep[['loan_status']]
# Conjuntos de entrenamiento y prueba
X3_train, X3_test, y_train, y_test = train_test_split(X3, y, test_size=.4, random_state=123)
# Entrenamiento de un modelo con las 3 variables seleccionadas
clf_gbt3 = [Link]().fit(X3_train,[Link](y_train))
# Importancia de las variables para este modelo
xgb.plot_importance(clf_gbt3, importance_type = 'weight')
[Link]()
[22:49:11] WARNING: ../src/[Link]: Starting in XGBoost 1.3.0, the default evaluation metric used with the ob
La importancia de la tasa de interés pasó de 1490 a 1013. Inicialmente, esta era la columna más importante, pero el
ingreso de la persona terminó ocupando el primer lugar aquí.
Selección de variables y rendimiento del modelo
# Selección de algunas variables independientes de ejemplo
X = cr_loan_prep[['person_income','loan_int_rate','loan_percent_income','loan_amnt','person_home_owne
y = cr_loan_prep[['loan_status']]
# Conjuntos de entrenamiento y prueba
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=.4, random_state=123)
# Modelo XGBoost 1
gbt = [Link]().fit(X_train,[Link](y_train))
# Predicción del estado del prestamo usando el modelo 1
gbt_preds = [Link](X_test)
# Reporte de clasificación del primer modelo
target_names = ['Non-Default', 'Default']
print(classification_report(y_test, gbt_preds, target_names=target_names))
[22:49:12] WARNING: ../src/[Link]: Starting in XGBoost 1.3.0, the default evaluation metric used with the ob
precision recall f1-score support
Non-Default 0.91 0.95 0.93 9198
Default 0.79 0.66 0.72 2586
accuracy 0.89 11784
macro avg 0.85 0.81 0.83 11784
weighted avg 0.88 0.89 0.88 11784
# Selección de algunas variables independientes de ejemplo
X2 = cr_loan_prep[['person_income','loan_int_rate','loan_percent_income','person_emp_length', 'person
y = cr_loan_prep[['loan_status']]
# Conjuntos de entrenamiento y prueba
X2_train, X2_test, y_train, y_test = train_test_split(X2, y, test_size=.4, random_state=123)
# Modelo XGBoost 2
gbt2 = [Link]().fit(X2_train,[Link](y_train))
# Predicción del estado del prestamo usando el modelo 2
gbt2_preds = [Link](X2_test)
# Reporte de clasificación del segundo modelo
print(classification_report(y_test, gbt2_preds, target_names=target_names))
[22:49:14] WARNING: ../src/[Link]: Starting in XGBoost 1.3.0, the default evaluation metric used with the ob
/root/venv/lib/python3.7/site-packages/xgboost/[Link]: UserWarning: The use of label encoder in XGBClassifie
[Link](label_encoder_deprecation_msg, UserWarning)
precision recall f1-score support
Non-Default 0.91 0.97 0.94 9198
Default 0.88 0.66 0.75 2586
accuracy 0.90 11784
macro avg 0.89 0.82 0.85 11784
weighted avg 0.90 0.90 0.90 11784
Usamos la f1-score para defaults para ver cómo los modelos predecirán con precisión estos. En este caso el
segundo modelo presenta la puntuación f1 más alta. La selección de variables afectan bastante el accuracy y el
recall.
Validación cruzada
# Variables independientes y dependiente
X = cr_loan_prep.loc[:,cr_loan_prep.columns != 'loan_status']
y = cr_loan_prep[['loan_status']]
# Conjuntos de entrenamiento y prueba
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=.4, random_state=123)
# Ajuste del modelo
gbt = [Link]().fit(X_train, [Link](y_train))
# Parametros
params = {'eval_metric': 'auc', 'objective': 'binary:logistic', 'seed': 123}
# Número de pliegues y interacciones
n_folds = 5
early_stopping = 10
# Matriz de entrenamiento para XGBoost
DTrain = [Link](X_train, label = y_train)
# Dataframe de la validación cruzada
cv_df = [Link](params, DTrain, num_boost_round = 5, nfold=n_folds, early_stopping_rounds=early_stoppi
cv_df
[22:49:16] WARNING: ../src/[Link]: Starting in XGBoost 1.3.0, the default evaluation metric used with the ob
/root/venv/lib/python3.7/site-packages/xgboost/[Link]: UserWarning: The use of label encoder in XGBClassifie
[Link](label_encoder_deprecation_msg, UserWarning)
train-auc-mean fl… train-auc-std floa… test-auc-mean fl… test-auc-std float…
0 0.898182 0.001318178288 0.8925194 0.004649966649
1 0.9092564 0.002052300037 0.9027796 0.005052514645
2 0.9136208 0.002204611295 0.9068338 0.004423009039
3 0.9185998 0.001092324201 0.910779 0.005221253987
4 0.9222516 0.001818467553 0.9141926 0.004422394899
A medida que avanzan las iteraciones de la validacion cruzada, las puntuaciones para train-auc-mean y test-auc-
mean mejoran.
Límites de las pruebas de validación cruzada
# Validación cruzada con más pliegues e interacciones
cv_results_big = [Link](params, DTrain, num_boost_round = 600, nfold=10, shuffle = True)
cv_results_big
train-auc-mean fl… train-auc-std floa… test-auc-mean fl… test-auc-std float…
0.8975683 - 1.000… 9.21371260676291… 0.8928996 - 0.945… 0.0044207345871…
0 0.8975683 0.0009093306384 0.8928996 0.008258239476
1 0.906985 0.002611366883 0.9011617 0.00751884567
2 0.9136785 0.001403479052 0.9079096 0.007752611253
3 0.9191233 0.0009208806709 0.91213 0.007145408372
4 0.9228642 0.001096840171 0.9149616 0.006706011068
5 0.926411 0.001299419255 0.9177445 0.006494181477
6 0.9304613 0.001149175709 0.9208305 0.00655644523
7 0.933292 0.001214780474 0.922861 0.006997531536
8 0.9363102 0.001096736231 0.9247704 0.006658755517
9 0.9392984 0.001157245799 0.9267467 0.005896659971
# Media de los puntajes AUC del test
print([Link](cv_results_big['test-auc-mean']).round(2))
0.94
# Gráfico de las puntuaciones AUC del test para cada iteración
[Link](cv_results_big['test-auc-mean'])
[Link]('Puntaje Test AUC sobre 600 iteraciones')
[Link]('Iteration Number')
[Link]('Test AUC Score')
[Link]()
La puntuación AUC de la prueba nunca llega a 1,0 y comienza a disminuir ligeramente después de 100 iteraciones.
Esto se debe a que tanta validación cruzada puede hacer que el modelo se sobreajuste. Por lo tanto, hay un límite
para la cantidad de validación cruzada que debe realizar.
Puntuación de validación cruzada
# Modelo de árbol potenciado por gradiente usando dos hiperparámetros
gbt = [Link](learning_rate = 0.1, max_depth = 7)
from sklearn.model_selection import cross_val_score
# Puntajes de validación cruzada para 4 pliegues
cv_scores = cross_val_score(gbt, X_train, [Link](y_train), cv = 4)
# Puntuaciones de validación cruzada
print(cv_scores)
/root/venv/lib/python3.7/site-packages/xgboost/[Link]: UserWarning: The use of label encoder in XGBClassifie
[Link](label_encoder_deprecation_msg, UserWarning)
[22:52:49] WARNING: ../src/[Link]: Starting in XGBoost 1.3.0, the default evaluation metric used with the ob
/root/venv/lib/python3.7/site-packages/xgboost/[Link]: UserWarning: The use of label encoder in XGBClassifie
[Link](label_encoder_deprecation_msg, UserWarning)
[22:52:52] WARNING: ../src/[Link]: Starting in XGBoost 1.3.0, the default evaluation metric used with the ob
/root/venv/lib/python3.7/site-packages/xgboost/[Link]: UserWarning: The use of label encoder in XGBClassifie
[Link](label_encoder_deprecation_msg, UserWarning)
[22:52:55] WARNING: ../src/[Link]: Starting in XGBoost 1.3.0, the default evaluation metric used with the ob
/root/venv/lib/python3.7/site-packages/xgboost/[Link]: UserWarning: The use of label encoder in XGBClassifie
[Link](label_encoder_deprecation_msg, UserWarning)
[22:52:58] WARNING: ../src/[Link]: Starting in XGBoost 1.3.0, the default evaluation metric used with the ob
[0.94048427 0.93256393 0.93324282 0.92462653]
# Precisión promedio y la desviación estándar de las puntuaciones
print("Precisión promedio: %0.2f (+/- %0.2f)" % (cv_scores.mean(), cv_scores.std() * 2))
Precisión promedio: 0.93 (+/- 0.01)
Con solo un par de hiperparámetros y validación cruzada, podemos obtener una precisión promedio de hasta el 93
%. Esta es una excelente manera de validar cuán robusto es el modelo.
Submuestreo de datos de entrenamiento
# Concatenación de las muestras de entrenamiento
X_y_train = [Link]([X_train.reset_index(drop = True), y_train.reset_index(drop = True)], axis = 1)
# Cuenta de incumplidos y no incumplidos
count_nondefault, count_default = X_y_train['loan_status'].value_counts()
# Conjunto de datos para defaults and non-defaults
nondefaults = X_y_train[X_y_train['loan_status'] == 0]
defaults = X_y_train[X_y_train['loan_status'] == 1]
# Submuestreo de non-defaults
nondefaults_under = [Link](count_default)
# Concatenación de submuestreo de nondefaults with defaults
X_y_train_under = [Link]([nondefaults_under.reset_index(drop = True), defaults.reset_index(drop =
# Conteo del estado del préstamo
print(X_y_train_under['loan_status'].value_counts())
0 3877
1 3877
Name: loan_status, dtype: int64
Nuestro conjunto de entrenamiento tiene un número igual de default y non-default. Probaremos algunos modelos de
aprendizaje automático en este nuevo conjunto de datos submuestreados y comparemos su rendimiento con los
modelos entrenados en el conjunto de datos normal.
Rendimiento del árbol submuestreado
# Variables independientes y dependiente
X = cr_loan_prep.loc[:,cr_loan_prep.columns != 'loan_status']
y = cr_loan_prep[['loan_status']]
# Conjuntos de entrenamiento y prueba
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=.4, random_state=123)
# Ajuste del modelo
clf_gbt = [Link]().fit(X_train, [Link](y_train))
# Predicciones de la probabilidad de default con el modelo
gbt_preds = clf_gbt.predict(X_test)
[22:53:01] WARNING: ../src/[Link]: Starting in XGBoost 1.3.0, the default evaluation metric used with the ob
/root/venv/lib/python3.7/site-packages/xgboost/[Link]: UserWarning: The use of label encoder in XGBClassifie
[Link](label_encoder_deprecation_msg, UserWarning)
# Variables independientes y dependiente
X_train_under = X_y_train_under.loc[:,X_y_train_under.columns != 'loan_status']
y_train_under = X_y_train_under[['loan_status']]
# Ajuste del modelo
clf_gbt2 = [Link]().fit(X_train_under, [Link](y_train_under))
# Predicciones de la probabilidad de default con el modelo
gbt2_preds = clf_gbt2.predict(X_test)
[22:53:04] WARNING: ../src/[Link]: Starting in XGBoost 1.3.0, the default evaluation metric used with the ob
# Reportes de clasificación
target_names = ['Non-Default', 'Default']
print(classification_report(y_test, gbt_preds, target_names=target_names))
print(classification_report(y_test, gbt2_preds, target_names=target_names))
precision recall f1-score support
Non-Default 0.93 0.99 0.96 9198
Default 0.94 0.74 0.83 2586
accuracy 0.93 11784
macro avg 0.94 0.86 0.89 11784
weighted avg 0.93 0.93 0.93 11784
precision recall f1-score support
Non-Default 0.95 0.91 0.93 9198
Default 0.73 0.82 0.77 2586
accuracy 0.89 11784
macro avg 0.84 0.87 0.85 11784
weighted avg 0.90 0.89 0.90 11784
# Matriz de confusión de ambos modelos
print(confusion_matrix(y_test,gbt_preds))
print(confusion_matrix(y_test,gbt2_preds))
[[9085 113]
[ 677 1909]]
[[8416 782]
[ 469 2117]]
# Comparación de los puntajes AUC de los modelos
print(roc_auc_score(y_test, gbt_preds))
print(roc_auc_score(y_test, gbt2_preds))
0.8629602218579747
0.8668101710802659
El submuestreo de los datos de entrenamiento da como resultado más falsos positivos, pero el recall de los valores
default y la puntuación de AUC son más altos que el modelo original. Esto significa que, en general, predice los
valores default con mucha más precisión.
Algo que uno debe acordarse es que el conjunto de prueba no se submuestrea ya que representa el tipo de datos
que verá el modelo en el mundo real, por lo que cambiarlo probaría el modelo con datos poco realistas"
Evaluación e Implementación del
Modelo
Después de desarrollar y probar dos potentes modelos de aprendizaje automático, se utilizará métricas de
rendimiento clave para compararlos. Usando técnicas avanzadas de selección de modelos específicamente para el
modelado financiero, se seleccionara un modelo. Con ese modelo: se desarrollara una estrategia comercial,
estimara el valor de la cartera y minimizara la pérdida esperada.
Comparación de reportes de modelos
# Variables independientes y dependiente
X = cr_loan_prep.loc[:,cr_loan_prep.columns != 'loan_status']
y = cr_loan_prep[['loan_status']]
# Conjuntos de entrenamiento y prueba
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=.4, random_state=123)
# Ajuste del modelo de regresión logística
clf_logistic = LogisticRegression(solver='lbfgs').fit(X_train, [Link](y_train))
# Predicciones de probabilidad para el estado del préstamo usando datos de prueba
preds_df_lr = clf_logistic.predict_proba(X_test)
preds_df_lr
# Dataframe de las probabilidades de incumplimiento
preds_df_lr = [Link](preds_df_lr[:,1], columns = ['prob_default'])
# Estado del préstamo según la probabilidad asignada
preds_df_lr['loan_status'] = preds_df_lr['prob_default'].apply(lambda X: 1 if X > .4 else 0)
preds_df_lr
prob_default float… loan_status int64
5.26111040465612… 0 - 1
0 0.4457786018 1
1 0.223446533 0
2 0.288558257 0
3 0.1693575271 0
4 0.1141819733 0
5 0.4902568814 1
6 0.1620574551 0
7 0.3962111956 0
8 0.2174275844 0
9 0.4814399584 1
# Ajuste del modelo XGBoost
clf_gbt = [Link](base_score=0.5, colsample_bylevel=1, colsample_bytree=1, gamma=0, learnin
# Predicciones de la probabilidad de default con el modelo
preds_df_gbt = clf_gbt.predict_proba(X_test)
# Dataframe de las probabilidades de incumplimiento
preds_df_gbt = [Link](preds_df_gbt[:,1], columns = ['prob_default'])
# Estado del préstamo según la probabilidad asignada
preds_df_gbt['loan_status'] = preds_df_gbt['prob_default'].apply(lambda X: 1 if X > .4 else 0)
preds_df_gbt
/root/venv/lib/python3.7/site-packages/xgboost/[Link]: UserWarning: The use of label encoder in XGBClassifie
[Link](label_encoder_deprecation_msg, UserWarning)
[22:53:06] WARNING: ../src/[Link]: Starting in XGBoost 1.3.0, the default evaluation metric used with the ob
prob_default float… loan_status int64
0.0003537827578… 0-1
0 0.9823871851 1
1 0.9751634002 1
2 0.003473574528 0
3 0.005457147025 0
4 0.119876273 0
5 0.1467240006 0
6 0.01178901456 0
7 0.002285469789 0
8 0.940839529 1
9 0.9881092906 1
# Reporte de de clasificación de regresión logística
target_names = ['Non-Default', 'Default']
print(classification_report(y_test, preds_df_lr['loan_status'], target_names=target_names))
precision recall f1-score support
Non-Default 0.86 0.92 0.89 9198
Default 0.62 0.46 0.53 2586
accuracy 0.82 11784
macro avg 0.74 0.69 0.71 11784
weighted avg 0.81 0.82 0.81 11784
# Reporte de clasificación de árboles potenciados por gradiente
print(classification_report(y_test, preds_df_gbt['loan_status'], target_names=target_names))
precision recall f1-score support
Non-Default 0.93 0.99 0.96 9198
Default 0.94 0.73 0.82 2586
accuracy 0.93 11784
macro avg 0.93 0.86 0.89 11784
weighted avg 0.93 0.93 0.93 11784
# Puntuaciones F-1 de incumplimiento para la regresión logística
print(precision_recall_fscore_support(y_test,preds_df_lr['loan_status'], average = 'macro')[2])
0.7108943782814463
# Puntuaciones F-1 de incumplimiento para el modelo de arboles poteciados por gradiente
print(precision_recall_fscore_support(y_test,preds_df_gbt['loan_status'], average = 'macro')[2])
0.8909014142736051
Hay una diferencia notable entre estos dos modelos. Los puntajes del reporte de clasificación son todos más altos
para el árbol potenciado por gradiente. Esto significa que el modelo de árbol es mejor en todos estos aspectos.
Comparación de ROCs
# Predicciones de probabilidad para el estado del préstamo usando datos de prueba
clf_logistic_preds = [Link](preds_df_lr['prob_default'])
clf_gbt_preds = [Link](preds_df_gbt['prob_default'])
# Componentes del gráfico ROC
fallout_lr, sensitivity_lr, thresholds_lr = metrics.roc_curve(y_test, clf_logistic_preds)
fallout_gbt, sensitivity_gbt, thresholds_gbt = metrics.roc_curve(y_test, clf_gbt_preds)
# Gráfico ROC con ambos componentes
[Link](fallout_lr, sensitivity_lr, color = 'blue', label='%s' % 'Logistic Regression')
[Link](fallout_gbt, sensitivity_gbt, color = 'green', label='%s' % 'Gradient Boosted Tree')
[Link]([0, 1], [0, 1], linestyle='--', label='%s' % 'Random Prediction')
[Link]("Gráfico ROC para LR y GBT sobre la probabilidad de incumplimiento")
[Link]('Fall-out')
[Link]('Sensitivity')
[Link]()
[Link]()
# Puntaje AUC de la regresión logística
print("Logistic Regression AUC Score: %0.2f" % roc_auc_score(y_test, clf_logistic_preds))
# Puntaje AUC de los arboles potenciados por gradiente
print("Gradient Boosted Tree AUC Score: %0.2f" % roc_auc_score(y_test, clf_gbt_preds))
Logistic Regression AUC Score: 0.76
Gradient Boosted Tree AUC Score: 0.94
En la curva ROC para el árbol potenciado por gradiente la elevación es mucho más alta y la puntuación AUC
calculada también es un poco más alta. Parece que el árbol potenciado por gradiente es el mejor. Comprobaremos
la calibración para estar seguros.
Curvas de calibración
from [Link] import calibration_curve
# Fracción de positivos
frac_of_pos_lr = calibration_curve(y_test, clf_logistic_preds, n_bins = 20)[0]
frac_of_pos_lr
array([0.07886231, 0.06610942, 0.10835913, 0.13505074, 0.16063348,
0.18333333, 0.21268657, 0.24099099, 0.48036649, 0.72677596,
0.73354232, 0.70547945, 0.68 , 0.73913043, 0.55555556,
0.4 ])
# Probabilidad media
mean_pred_val_lr = calibration_curve(y_test, clf_logistic_preds, n_bins = 20)[1]
mean_pred_val_lr
array([0.02111464, 0.07548788, 0.12582662, 0.17502903, 0.22449499,
0.27491676, 0.32488847, 0.37486698, 0.42302912, 0.47397249,
0.52304288, 0.57259508, 0.62200793, 0.67156702, 0.71909209,
0.77024859])
# Fracción de positivos
frac_of_pos_gbt = calibration_curve(y_test, clf_gbt_preds, n_bins = 20)[0]
frac_of_pos_gbt
array([0.01916168, 0.06385752, 0.12795793, 0.17460317, 0.21806854,
0.32620321, 0.32653061, 0.33333333, 0.40677966, 0.43181818,
0.6 , 0.42105263, 0.31578947, 0.6875 , 0.78571429,
0.83333333, 0.90697674, 0.95238095, 0.98850575, 1. ])
# Probabilidad media
mean_pred_val_gbt = calibration_curve(y_test, clf_gbt_preds, n_bins = 20)[1]
mean_pred_val_gbt
array([0.01937249, 0.07211534, 0.12178284, 0.17298488, 0.22318428,
0.2716055 , 0.32285183, 0.369344 , 0.42164062, 0.47158214,
0.52230485, 0.57041398, 0.62149714, 0.67234764, 0.72826275,
0.77567046, 0.82827961, 0.87636708, 0.92830987, 0.98579916])
# Curva de calibración
[Link]([0, 1], [0, 1], 'k:', label='Perfectly calibrated')
[Link](mean_pred_val_lr, frac_of_pos_lr, 's-', label='%s' % 'Logistic Regression')
[Link](mean_pred_val_gbt, frac_of_pos_gbt, 's-', label='%s' % 'Gradient Boosted tree')
[Link]('Fraction of positives')
[Link]('Average Predicted Probability')
[Link]()
[Link]('Calibration Curve')
[Link]()
Para la regresión logística, la calibración de las probabilidades comienza muy bien, pero luego se vuelve más
errática a medida que la probabilidad promedio se acerca a 0,4. Algo similar sucede con el árbol potenciado por
gradiente alrededor de 0.5, pero el modelo finalmente se estabiliza. Centraremos el análisis únicamente en el
modelo de arboles potenciados por gradiente.
Tasas de aceptación
# Estadísticas de las probabilidades de incumplimiento
true_list = [Link](y_test, columns = ['loan_status']).rename(columns={'loan_status':'true_loan_
test_pred_df = [Link]([true_list.reset_index(drop = True), preds_df_gbt['prob_default']], axis = 1
print(test_pred_df['prob_default'].describe())
count 11784.000000
mean 0.216866
std 0.333038
min 0.000354
25% 0.022246
50% 0.065633
75% 0.177804
max 0.999557
Name: prob_default, dtype: float64
# Umbral para una tasa de aceptación del 85 %
threshold_85 = [Link](test_pred_df['prob_default'], 0.85)
# Umbral de tasa de aceptación
test_pred_df['pred_loan_status'] = test_pred_df['prob_default'].apply(lambda x: 1 if x > threshold_85
# Recuentos del estado del préstamo después del umbral
print(test_pred_df['pred_loan_status'].value_counts())
0 10016
1 1768
Name: pred_loan_status, dtype: int64
En los resultados de las estadísticas, se ve que hasta el cuantil 75 se empieza a ver números de probabilidades de
incumplimiento de dos dígitos. Esto se debe a que la mayoría de nuestro conjunto de prueba son préstamos sin
incumplimiento. Veremos cómo la tasa de aceptación y el umbral dividen los datos.
Visualización de cuantiles de aceptación
# Histograma de las probabilidades previstas de incumplimiento
[Link](clf_gbt_preds, color = 'blue', bins = 40)
# Línea de referencia al gráfico para el umbral
[Link](x = threshold_85, color = 'red')
[Link]()
Aquí se puede ver dónde se encuentra el umbral en el rango de probabilidades previstas. No solo se puede ver
cuántos préstamos se aceptarán (lado izquierdo), sino también cuántos préstamos se rechazarán (lado derecho).
Bajas calificaciones
test_pred_df
true_loan_status i… prob_default float… pred_loan_status i…
0-1 0.0003537827578… 0-1
0 1 0.9823871851 1
1 1 0.9751634002 1
2 0 0.003473574528 0
3 0 0.005457147025 0
4 1 0.119876273 0
5 1 0.1467240006 0
6 0 0.01178901456 0
7 0 0.002285469789 0
8 1 0.940839529 1
9 1 0.9881092906 1
# Subconjunto de solo préstamos aceptados
accepted_loans = test_pred_df[test_pred_df['pred_loan_status'] == 0]
# Cálculo de la baja tasa
print([Link](accepted_loans['true_loan_status']) / accepted_loans['true_loan_status'].count())
0.08256789137380191
La baja tasa con el umbral establecido por el cuantil del 85 % es de alrededor del 8% el cual no esta nada mal. Esto
significa que de todos los préstamos que hemos decidido aceptar del conjunto de prueba, solo el 8% eran
incumplimientos reales. Si aceptáramos todos los préstamos, el porcentaje de morosidad rondaría el 22%.
Impacto en la tasa de aceptación
test_pred_df = test_pred_df.rename(columns={'pred_loan_status':'pred_loan_status_15'}, inplace=False)
test_pred_df['loan_amnt'] = X_test['loan_amnt'].reset_index(drop = True)
test_pred_df
true_loan_status i… prob_default float… pred_loan_statu… loan_amnt int64
0-1 0.0003537827578… 0-1 500 - 35000
0 1 0.9823871851 1 15000
1 1 0.9751634002 1 11200
2 0 0.003473574528 0 15000
3 0 0.005457147025 0 10800
4 1 0.119876273 0 3000
5 1 0.1467240006 0 7750
6 0 0.01178901456 0 10000
7 0 0.002285469789 0 15000
8 1 0.940839529 1 15000
9 1 0.9881092906 1 9000
# Estadísticas de la variable monto del préstamo
print(test_pred_df['loan_amnt'].describe())
count 11784.000000
mean 9556.283944
std 6238.005674
min 500.000000
25% 5000.000000
50% 8000.000000
75% 12000.000000
max 35000.000000
Name: loan_amnt, dtype: float64
# Almacenar el monto promedio del préstamo
avg_loan = [Link](test_pred_df['loan_amnt'])
# Formato de moneda y tabulación cruzada
[Link].float_format = '${:,.2f}'.format
print([Link](test_pred_df['true_loan_status'], test_pred_df['pred_loan_status_15']).apply(lambda
pred_loan_status_15 0 1
true_loan_status
0 $87,812,693.16 $86,006.56
1 $7,903,046.82 $16,809,503.46
Con esto, podemos ver que nuestra tasa de morosidad de alrededor del 8% representa un valor de préstamo
estimado de alrededor de 7.9 millones de dólares. Esto puede parecer mucho al principio, pero no es en
comparación con el valor total de los préstamos sin incumplimiento. Con esto, estamos listos para comenzar a
hablar sobre nuestra estrategia de aceptación en el futuro.
Tabla de la estrategia comercial
# Matriz de tazas de aceptación
accept_rates = [1.0,
0.95,
0.9,
0.85,
0.8,
0.75,
0.7,
0.65,
0.6,
0.55,
0.5,
0.45,
0.4,
0.35,
0.3,
0.25,
0.2,
0.15,
0.1,
0.05]
# Matriz de umbrales
thresholds = []
# Matriz de bajas tazas de aceptación
bad_rates = []
test_pred_df = test_pred_df.rename(columns={'pred_loan_status_15':'pred_loan_status'}, inplace=False)
test_pred_df
true_loan_status i… prob_default float… pred_loan_status i… loan_amnt int64
0-1 0.0003537827578… 0-1 500 - 35000
0 1 0.9823871851 1 15000
1 1 0.9751634002 1 11200
2 0 0.003473574528 0 15000
3 0 0.005457147025 0 10800
4 1 0.119876273 0 3000
5 1 0.1467240006 0 7750
6 0 0.01178901456 0 10000
7 0 0.002285469789 0 15000
8 1 0.940839529 1 15000
9 1 0.9881092906 1 9000
# Prestamos aceptados
accepted_loans = accepted_loans[0:0]
# Número de prestamos aceptados
num_accepted_loans = []
# Matrices de la tabla de estrategia con un bucle for
for rate in accept_rates:
# Umbral para la tasa de aceptación
thresh = [Link](preds_df_gbt['prob_default'], rate).round(3)
# Agregación del valor del umbral a la lista de umbrales
[Link]([Link](preds_df_gbt['prob_default'], rate).round(3))
# Reasignación del valor del estado del préstamo usando el umbral
test_pred_df['pred_loan_status'] = \
test_pred_df['prob_default'].apply(lambda x: 1 if x > thresh else 0)
# Conjunto de préstamos aceptados usando esta tasa de aceptación
accepted_loans = test_pred_df[test_pred_df['pred_loan_status'] == 0]
# Calculo y asignación de la tasa baja usando la tasa de aceptación
bad_rates.append([Link]((accepted_loans['true_loan_status'])
/ accepted_loans['true_loan_status'].count()).round(3))
# Numero de prestamos aceptados
num_accepted_loans.append(len(test_pred_df[test_pred_df['prob_default']<[Link](test_pred_df
# Marco de datos de la tabla de estrategia comercial
strat_df = [Link](zip(accept_rates, thresholds, bad_rates), columns = ['Acceptance Rate','Thres
strat_df
Acceptance Rate f… Threshold float64 Bad Rate float64
0.05 - 1.0 0.002 - 1.0 0.0 - 0.219
0 1 1 0.219
1 0.95 0.992 0.179
2 0.9 0.976 0.132
3 0.85 0.804 0.083
4 0.8 0.254 0.061
5 0.75 0.178 0.052
6 0.7 0.138 0.043
7 0.65 0.111 0.036
8 0.6 0.093 0.03
9 0.55 0.078 0.027
Esta tabla de estrategia utiliza nuestras predicciones específicas sobre los datos crediticios y se puede usar para
ver las tasas de aceptación, las tasas malas y el impacto financiero, todo a la vez. Uno de estos valores tiene el
valor estimado más alto.
Visualizando la estrategia comercial
# Diagrama de caja de la tabla de estrategia comercial
strat_df.boxplot()
[Link]()
# Gráfico de la curva de estrategia
[Link](strat_df['Acceptance Rate'], strat_df['Bad Rate'])
[Link]('Acceptance Rate')
[Link]('Bad Rate')
[Link]('Acceptance and Bad Rates')
[Link]()
El diagrama de caja nos muestra la distribución para cada columna. En la curva de estrategia las malas tasas son
muy bajas hasta la tasa de aceptación de 0,6, donde aumentan repentinamente. Esto sugiere que muchos de los
valores defaults aceptados pueden tener un valor prob_default entre 0,6 y 0,8.
Valor estimado del perfil
# Monto promedio del prestamo
mean_loan_amnt = [Link](test_pred_df['loan_amnt']).repeat(20)
# Marco de datos de la tabla de estrategia comercial con el monto promedio del prestamo
strat_df = [Link](zip(accept_rates, thresholds, bad_rates, num_accepted_loans, mean_loan_amnt),
strat_df
Acceptance Rate f… Threshold float64 Bad Rate float64 Num Accepted L… Avg Loan Amnt fl…
0.05 - 1.0 0.002 - 1.0 0.0 - 0.219 405 - 11784 9556.2839443312…
0 1 1 0.219 11784 9556.283944
1 0.95 0.992 0.179 11208 9556.283944
2 0.9 0.976 0.132 10597 9556.283944
3 0.85 0.804 0.083 10016 9556.283944
4 0.8 0.254 0.061 9425 9556.283944
5 0.75 0.178 0.052 8843 9556.283944
6 0.7 0.138 0.043 8248 9556.283944
7 0.65 0.111 0.036 7653 9556.283944
8 0.6 0.093 0.03 7058 9556.283944
9 0.55 0.078 0.027 6496 9556.283944
# El valor neto estimado de los no impagos menos los impagos
estimated_value = ((strat_df['Num Accepted Loans'] * (1 - strat_df['Bad Rate'])) * strat_df['Avg Loan
# Marco de datos de la tabla de estrategia comercial con el valor neto
strat_df = [Link](zip(accept_rates, thresholds, bad_rates, num_accepted_loans, mean_loan_amnt,
strat_df
Acceptance Rate f… Threshold float64 Bad Rate float64 Num Accepted L… Avg Loan Amnt fl… Estimated Value f
0.05 - 1.0 0.002 - 1.0 0.0 - 0.219 405 - 11784 9556.2839443312… 3870294.99745417…
0 1 1 0.219 11784 9556.283944 63287522.
1 0.95 0.992 0.179 11208 9556.283944 68762585.1
2 0.9 0.976 0.132 10597 9556.283944 74533204.5
3 0.85 0.804 0.083 10016 9556.283944 79826927.1
4 0.8 0.254 0.061 9425 9556.283944 79079683.0
5 0.75 0.178 0.052 8843 9556.283944 75717572.1
6 0.7 0.138 0.043 8248 9556.283944 72041690.
7 0.65 0.111 0.036 7653 9556.283944 67868575.6
8 0.6 0.093 0.03 7058 9556.283944 63401356.9
9 0 55 0 078 0 027 6496 9556 283944 5872542
# Gráfico con los valores estimados
[Link](strat_df['Acceptance Rate'],strat_df['Estimated Value'])
[Link]('Estimated Value by Acceptance Rate')
[Link]('Acceptance Rate')
[Link]('Estimated Value')
[Link]()
# Fila con el valor máximo estimado
print(strat_df.loc[strat_df['Estimated Value'] == [Link](strat_df['Estimated Value'])])
Acceptance Rate Threshold Bad Rate Num Accepted Loans Avg Loan Amnt \
3 $0.85 $0.80 $0.08 10016 $9,556.28
Estimated Value
3 $79,826,927.15
Con estos datos crediticios y el valor de préstamo promedio estimado, vemos claramente que la tasa de aceptación
de 0,85 tiene el valor estimado potencial más alto. Normalmente, se establece la tasa de errores permitida, pero
podemos usar análisis como este para explorar otras opciones.
Pérdida total esperada
# Suposición de que la exposición es el valor total del préstamo y que la pérdida en caso de incumpli
test_pred_df['pred_loan_status'] = 1
test_pred_df = test_pred_df.rename(columns={'pred_loan_status':'loss_given_default'}, inplace=False)
test_pred_df
true_loan_status i… prob_default float… loss_given_default loan_amnt int64
0-1 0.0003537827578… 1-1 500 - 35000
0 1 0.9823871851 1 15000
1 1 0.9751634002 1 11200
2 0 0.003473574528 1 15000
3 0 0.005457147025 1 10800
4 1 0.119876273 1 3000
5 1 0.1467240006 1 7750
6 0 0.01178901456 1 10000
7 0 0.002285469789 1 15000
8 1 0.940839529 1 15000
9 1 0.9881092906 1 9000
# Pérdida esperada del banco
test_pred_df['expected_loss'] = test_pred_df['prob_default'] * test_pred_df['loss_given_default'] * t
# Pérdida total esperada con dos decimales
tot_exp_loss = round([Link](test_pred_df['expected_loss']),2)
# Pérdida total esperada
print('Total expected loss: ', '${:,.2f}'.format(tot_exp_loss))
Total expected loss: $27,084,153.38
Esta es la pérdida esperada total para toda la cartera utilizando el árbol potenciado por gradiente. $27 millones
puede parecer mucho, pero la pérdida total esperada habría sido de más de $28 millones con la regresión logística.
Este trabajo demuestra que elegir un buen modelo podría haberle ahorrado al banco un millón de dólares.