Introducción

Para esta segunda práctica nos encontrábamos frente a un problema de riesgo de crédito, el cual permite predecir mediante probabilidad la posibilidad de incurrir en una pérdida debido a un incumplimiento de un futuro crédito que se desee brindar.

Objetivo

El objetivo principal de la práctica fue el de realizar un modelo de probabilidad el cual permitiese predecir la probabilidad de que un individuo incumpla sus obligaciones financieras en los siguientes 12 meses desde que se genere el crédito.

También se debía representar este mismo modelo con un Scorecard. De igual forma se debía analizar qué variables hacen más riesgosa a una persona. Y finalmente, se debía desarrollar una aplicación web que le permitiera a los usuario ver su calificación de scorecard, de acuerdo a sus características, y cómo se encuentra respecto al resto de la población.

import pandas as pd
import numpy as np
import seaborn as sns
from scipy.stats import chi2_contingency
from sklearn.neighbors import KNeighborsClassifier
from sklearn.metrics import accuracy_score
from sklearn.model_selection import train_test_split
import matplotlib.pyplot as plt
import plotly.express as px
from sklearn.naive_bayes import GaussianNB
from tqdm import tqdm
from plotly.subplots import make_subplots
import plotly.graph_objects as go
from sklearn.linear_model import LogisticRegression
mes_name=['Dec', 'Nov', 'Oct', 'Sep', 'Aug', 'Jul', 'Jun', 'May', 'Apr', 'Mar', 'Feb', 'Jan']
mes_num=list(pd.Series(list((13-np.arange(1,13)))).astype("str"))
def format_replace(string_): # ajust formato de fecha
    for i in range(0,12):
        string_=str(string_).replace(mes_name[i],mes_num[i] )
    return string_
def dummy_creation(df_, columns_list): # creacion de variables dummy
    df_dummies = []
    for col in columns_list:
        df_dummies.append(pd.get_dummies(df_[col], prefix = col, prefix_sep = ':',drop_first=True))
    df_dummies = pd.concat(df_dummies, axis = 1)
    df_ = pd.concat([df_, df_dummies], axis = 1)
    df_=df_.drop(labels=columns_list,axis=1)
    return df_
def plot_barplot(df_temp, x, y,nrow,ncol,fig_temp,show_leg):
    df1 = df_temp.groupby(x)[y].value_counts(normalize=True)
    df1 = df1.mul(100)
    df1 = df1.rename('percent').reset_index()
    df_temp=df1[df1[y]==1].copy()
    fig_temp.add_trace(go.Bar(x=df_temp[x],
                y=df_temp["percent"],
                name="1",
                marker_color='rgb(55, 83, 109)',
                              hovertext =(x,"percent" ),
                              showlegend=show_leg
                ), row=nrow,col=ncol)
    df_temp=df1[df1[y]==0].copy()
    fig_temp.add_trace(go.Bar(x=df_temp[x],
                y=df_temp["percent"],
                name="0",
                marker_color='rgb(26, 118, 255)',
                              hovertext =(x,"percent" ),
                              showlegend=show_leg
                ),row=nrow,col=ncol)
    fig_temp.update_yaxes(range = [0,100])
    return fig_temp
# function to calculate WoE and IV of categorical features
# The function takes 3 arguments: a dataframe (X_train_prepr), a string (column name), and a dataframe (y_train_prepr).
def woe_discrete(df, cat_variabe_name, y_df):
    df = pd.concat([df[cat_variabe_name], y_df], axis = 1)
    df = pd.concat([df.groupby(df.columns.values[0], as_index = False)[df.columns.values[1]].count(),
                    df.groupby(df.columns.values[0], as_index = False)[df.columns.values[1]].mean()], axis = 1)
    df = df.iloc[:, [0, 1, 3]]
    df.columns = [df.columns.values[0], 'n_obs', 'prop_good']
    df['prop_n_obs'] = df['n_obs'] / df['n_obs'].sum()
    df['n_good'] = df['prop_good'] * df['n_obs']
    df['n_bad'] = (1 - df['prop_good']) * df['n_obs']
    df['prop_n_good'] = df['n_good'] / df['n_good'].sum()
    df['prop_n_bad'] = df['n_bad'] / df['n_bad'].sum()
    df['WoE'] = np.log(df['prop_n_good'] / df['prop_n_bad'])
    df = df.sort_values(['WoE'])
    df = df.reset_index(drop = True)
    df['diff_prop_good'] = df['prop_good'].diff().abs()
    df['diff_WoE'] = df['WoE'].diff().abs()
    df['IV'] = (df['prop_n_good'] - df['prop_n_bad']) * df['WoE']
    df['IV'] = df['IV'].sum()
    return df

'''
function to calculate WoE & IV of continuous variables
This is same as the function we defined earlier for discrete variables
The only difference are the 2 commented lines of code in the function that results in the df
being sorted by continuous variable values
'''
## '\nfunction to calculate WoE & IV of continuous variables\nThis is same as the function we defined earlier for discrete variables\nThe only difference are the 2 commented lines of code in the function that results in the df\nbeing sorted by continuous variable values\n'
def continua_categorica(df_,variable,y_df ):
    quantiles_=list(df_[variable].quantile([0.15,0.3,0.45,0.6,0.75,0.9]))
    new_var=[variable+str(quantiles_[0])]
    df_[variable+str(quantiles_[0])]=np.where(df_[variable]<quantiles_[0],1,0 )
    for i in range(1,len(quantiles_)-1):
        new_var.append(variable+str(quantiles_[i]))
        df_[variable+str(quantiles_[i]) ]=np.where((df_[variable]<=quantiles_[i]) & (df_[variable]>quantiles_[i-1]),1,0 )    
    df_[variable+str(quantiles_[5])]=np.where(df_[variable]>=quantiles_[5],1,0)
    new_var.append(variable+str(quantiles_[5]) )
    woes=[]
    for vars_ in new_var:
        woe_temp=[*list(woe_ordered_continuous(df_,vars_,y_df )["WoE"] ),vars_]
        woes.append(woe_temp)
    return woes
    
def woe_ordered_continuous(df, continuous_variabe_name, y_df):
    df = pd.concat([df[continuous_variabe_name], y_df], axis = 1)
    df = pd.concat([df.groupby(df.columns.values[0], as_index = False)[df.columns.values[1]].count(),
                    df.groupby(df.columns.values[0], as_index = False)[df.columns.values[1]].mean()], axis = 1)
    df = df.iloc[:, [0, 1, 3]]
    df.columns = [df.columns.values[0], 'n_obs', 'prop_good']
    df['prop_n_obs'] = df['n_obs'] / df['n_obs'].sum()
    df['n_good'] = df['prop_good'] * df['n_obs']
    df['n_bad'] = (1 - df['prop_good']) * df['n_obs']
    df['prop_n_good'] = df['n_good'] / df['n_good'].sum()
    df['prop_n_bad'] = df['n_bad'] / df['n_bad'].sum()
    df['WoE'] = np.log(df['prop_n_good'] / df['prop_n_bad'])
    #df = df.sort_values(['WoE'])
    #df = df.reset_index(drop = True)
    df['diff_prop_good'] = df['prop_good'].diff().abs()
    df['diff_WoE'] = df['WoE'].diff().abs()
    df['IV'] = (df['prop_n_good'] - df['prop_n_bad']) * df['WoE']
    df['IV'] = df['IV'].sum()
    return df

Depuración

Para este ejercicio, se contó con la base de datos loan_data_2007_2014.csv obtenida a través de kaggle. Esta contiene información perteneciente a usuarios entre los años 2007 y 2014 de lendingclub, una empresa que realiza préstamos digitales en Estados Unidos ( lendingclub ).

df=pd.read_csv("loan_data_2007_2014.csv")
## <string>:1: DtypeWarning:
## 
## Columns (19) have mixed types. Specify dtype option on import or set low_memory=False.

La base de datos loan_data_2007_2014.csv cuenta con 74 columnas y 466285 registros.

  • Las columnas id, member_id, url, title hacen referencia a información de identificación del usuario, que no es de interés para el estudio.

Variables primordiales

Para la creación del modelo se tienen:

  • issue_d: El mes en que se financió el préstamo (mes-año).

  • last_pymnt_d: El último mes de pago fue recibido.

  • loan_satus: Esta sera la variable objetivo, cuenta con 9 categorías que clasifican el ultimo estado registrado.

Creación de variables

Como el objetivo es crear un modelo para predecir si al cabo de 12 meses que se origina el credíto (issue_d) el usuario incumple sus obligaciones financieras, luego de analizar las variables fecha registradas se crea month_last: meses que han pasado desde el ultimo pago, que es la diferencia (last_pymnt_d-issue_d ) esto nos dará informción del tiempo que pago el usuario y con la variable loan_status se podrá saber si el usuario incumple entre el tiempo de interés (antes de 12 meses).

Se crea las variables.

  • good_status: Que tomara valores 0 (con morosidad) y 1 (Sin morosidad)
#results="asis"
table_frec=pd.DataFrame(df["loan_status"].value_counts())
status_mora=['Charged Off', 'Default', 'Late (31-120 days)','Does not meet the credit policy. Status:Charged Off']
table_frec["good_status"]=1
filtro=pd.Series(table_frec.index).isin( status_mora)
table_frec.loc[list(filtro), "good_status"]=0
table_frec=table_frec.reset_index()
table_frec.columns=["loan_status", "Frec", "good_status" ]
df_temp=table_frec[["loan_status", "good_status","Frec" ]]
df["good_status"]=1
df.loc[df["loan_status"].isin(status_mora),"good_status"]=0
Categorías good_status
loan_status good_status Frec
Current 1 224226
Fully Paid 1 184739
Charged Off 0 42475
Late (31-120 days) 0 6900
In Grace Period 1 3146
Does not meet the credit policy. Status:Fully Paid 1 1988
Late (16-30 days) 1 1218
Default 0 832
Does not meet the credit policy. Status:Charged Off 0 761

En la Table 1 se observan las categorías que tiene la variable de interés.

  • month_last: last_pymnt_d-issue_d en meses.
df[ 'issue_d']=pd.to_datetime(df['issue_d'], format = "%m-%y")
df['last_pymnt_d']=pd.to_datetime(df['last_pymnt_d'], format = "%m-%y")
df["month_last"]= ((df.last_pymnt_d - df.issue_d)/np.timedelta64(1, 'M'))
  • target_time: 1 si month_last \(\leq\) 12, 0 si >12.
df['target_time']=0
df.loc[df["month_last"]<=12,'target_time']=1

Estructura del modelo

El modelo general tendra la estructura:

\[ P(\text{good_status=1} )= f(\text{month_last}, {X },\theta ) \] Donde month_last define el tiempo en que queremos predecir, \(X\) es un vector de variables que puedan afectar la probabilidad y \(\theta\) son los parámetros que puede contener el modelo.

Valores NA

Asumiendo que se tolera al menos un 20 % de valores NA en los datos de las y omitiendo las columnas de identificación se cuenta con:

drop_columns=["id", "member_id", "url", "title"]
df=df.drop(labels=drop_columns,axis=1 )
total_na=df.isna().sum()
filtro=total_na< df.shape[0]*0.2
total_na=total_na[filtro]
result=pd.DataFrame({"Variables":["Menos del 20% NA"," Mas del 20% NA"],
"Total variables":[total_na.shape[0], 70-total_na.shape[0] ]} )
Resumen de NA
Variables Total variables
Menos del 20% NA 51
Mas del 20% NA 19

En la Table 2 se observa la cantidad % de NA que tienen las variables, según esto se considera omitir 22 variables por su alto porcentaje de valores faltantes, aunque 20% de valores faltantes es una cantidad alta, existen variables importantes que contienen alta cantidad de valores faltantes que se muestran a continuación.

Variables modelo

vars_=total_na.sort_values(ascending=False).head()
result=pd.DataFrame({"Variables":vars_.index,"Descripción":[" Total crédito rotativo alto entre límite de crédito. ","Saldo corriente en todas las cuentas ", " Montos totales de cobro adeudados. ", "Tipo de trabajo.","Años en el trabajo "   ], "Total NA":vars_ })
result=result.reset_index()
Variables candidatas al modelo
index Variables Descripción Total NA
total_rev_hi_lim total_rev_hi_lim Total crédito rotativo alto entre límite de crédito. 70276
tot_cur_bal tot_cur_bal Saldo corriente en todas las cuentas 70276
tot_coll_amt tot_coll_amt Montos totales de cobro adeudados. 70276
emp_title emp_title Tipo de trabajo. 27588
emp_length emp_length Años en el trabajo 21008

En la Tabla 3 se tiene una pequeña descripción de variables importantes con una cantidad de NA alta, de estas variables puede ser dificil que el usuario obtenga total_rev_hi_lim, emp_title tiene muchas categorías.

Es importante identificar que variables puede dar un usuario al momento del registro, pues existen variables donde se obtienen la información al pasar el tiempo o un usuario no puede identificar.

Variables que no se incluyen
funded_amnt_inv grade sub_grade emp_title
verification_status zip_code addr_state dti
delinq_2yrs inq_last_6mths revol_bal revol_util
total_acc out_prncp out_prncp_inv out_prncp_inv
total_pymnt total_rec_prncp total_rec_int total_rec_late_fee
recoveries collection_recovery_fee last_pymnt_amnt last_credit_pull_d
collections_12_mths_ex_med policy_code tot_coll_amt total_rev_hi_lim

La Table 4 contiene Las variables que no se consideran en el modelo porque son medidas que son proporcionadas por LC, información al pasar el tiempo después del prestamo o son extraidas de un externo, por ende, las variables a considerar como influyentes en el incumplimiento de las finanzas son:

Covariables candidatas
Variables Descripción
loan_amnt El monto indicado del préstamo solicitado por el prestatario. Si en algún momento, el departamento de crédito reduce el monto del préstamo, entonces se reflejará en este valor.
funded_amnt El monto total comprometido con ese préstamo en ese momento.
term El número de pagos del préstamo. Los valores son en meses y pueden ser 36 o 60.
int_rate tasa de interés del préstamo.
installment cuota El pago mensual adeudado por el prestatario si el préstamo se origina.
home_ownership El estado de propiedad de la vivienda proporcionado por el prestatario durante el registro. Nuestros valores son
annual_inc Los ingresos anuales autoinformados proporcionados por el prestatario durante el registro.
earliest_cr_line El mes en que se abrió la primera línea de crédito reportada del prestatario.
open_acc El número de líneas de crédito abiertas en el archivo de crédito del prestatario.
pub_rec numero de derogatory public records.
acc_now_delinq El número de cuentas en las que el prestatario está ahora en mora.
purpose Razón por la que se hace el prestamo.
tot_cur_bal Saldo corriente total de todas las cuentas
emp_length años trabajo
initial_list_status El estado inicial de listado del préstamo. Los valores posibles son – W, F
pymnt_plan indica si se a establecido un plan de pago.

En la Table 5 se tienen la cantidad de variables que se pueden usar en el modelo, con good_status y target_time.

variables_=pd.Series(variables_).apply(lambda x: x.replace("__","" ))
df["month_earliest_cr_line"]=((df.issue_d-df.earliest_cr_line )/np.timedelta64(1, 'M'))
df=df[ [*variables_, "good_status","target_time","month_earliest_cr_line" ]] 
df=df[~df.isna().any(axis=1)]
result=pd.DataFrame({"":["Filas", "Columnas"] ,"Cantidad":df.shape})
Dimensión de datos
Cantidad
Filas 377062
Columnas 19

En la Table 6 se observa las dimensiones del data frame que se usara para el modelo excluyendo los NA.

Analisis Descriptivo

¿Cuál es la distribución de good_estatus?

tabla_1=df.good_status.value_counts(normalize=True)*100
tabla_2=df.good_status.value_counts()
tabla_3=df.target_time.value_counts(normalize=True)*100
tabla_4=df.target_time.value_counts()
table_final=pd.DataFrame({"good status":[1,0],"Frecuencia":tabla_2, 
                          "Frec %":tabla_1, "target time":[0,1],
                         "Frecuencia ":tabla_4, "Frec % ":tabla_3 } )
Distribución global de good_status
good status Frecuencia Frec % target time Frecuencia Frec %
1 37709 10 0 305925 81.13
0 339353 90 1 71137 18.87

En la Table 7 se puede obserar la distribución frecuentista de good_status La variable acc_now_delinq se puede transformar en 1 si tiene al menos una cuenta en mora, 0 sino tiene cunetas en mora. También la variable pub_rec se transforma 1 si tiene al menos un derogatory public records, 0 sino. Se tomo la decisión ya que son variables conteos donde no parece ser necesario. Como esta es la fecha en que se hizo su primer prestamo earliest_cr_line, se debe calcular los meses que han pasado desde que solicito el prestamo month_earliest_cr_line

df=df.drop(labels="earliest_cr_line",axis=1)
df["acc_now_delinq"]=np.where(df["acc_now_delinq"]>0, 1,0)
df["pub_rec"]=np.where(df["pub_rec"]>0,1,0)
df_temp=df.copy()
df_temp["acc_now_delinq"]=df_temp["acc_now_delinq"].astype("str")
df_temp["target_time"]=df_temp["target_time"].astype("str")
df_temp["pub_rec"]=df_temp["pub_rec"].astype("str")
X=df_temp.drop(labels="good_status",axis=1)
Y=df_temp["good_status"]

X_train_cat = X.select_dtypes(include = 'object').copy()
X_train_num = X.select_dtypes(include = 'number').copy()
# define an empty dictionary to store chi-squared test results
chi2_check = {}

# loop over each column in the training set to calculate chi-statistic with the target variable
for column in X_train_cat:
    chi, p, dof, ex = chi2_contingency(pd.crosstab(Y, X_train_cat[column]))
    chi2_check.setdefault('Feature',[]).append(column)
    chi2_check.setdefault('p-value',[]).append(round(p, 10))

# convert the dictionary to a DF
chi2_result = pd.DataFrame(data = chi2_check)
chi2_result.sort_values(by = ['p-value'], ascending = True, ignore_index = True, inplace = True)
Prueba
Feature p-value
term 0.00
home_ownership 0.00
purpose 0.00
emp_length 0.00
initial_list_status 0.00
target_time 0.00
pub_rec 0.00
pymnt_plan 0.14
acc_now_delinq 0.51

En la Table 8 Se realizan pruebas \(\chi^2\) para las variables categoricas en contraste con la variable good_status y se observa 8 variables con un p-valor pequeño, esto significa que estas variables pueden influir en el incumplimiento de las finanzas. Se excluye pymnt_plan debido a un p-valor > 0.05.

corrmat = X_train_num.corr()
sns.heatmap(corrmat)

Figure 1: Correlación entre variables númericas.

En la Figure 1 se observa que hay 3 variables con una alta correlación entre si (\(\approx\) 1) que son: loan_amnt, funded_amnt, installment y según la table - 5 se opta por loan_amnt por ser el prestamo definitivo que dio LC.

vars_=['loan_amnt',  'int_rate', 'annual_inc',
       'open_acc', 'tot_cur_bal',"month_earliest_cr_line"]
fig, axs = plt.subplots(ncols=3,nrows=2,figsize=(18, 7))
sns.boxplot(data=df, x="good_status", y=vars_[0], ax=axs[0, 0],showfliers = False)
sns.boxplot(data=df, x="good_status", y=vars_[1], ax=axs[0, 1],showfliers = False)
sns.boxplot(data=df, x="good_status", y=vars_[2], ax=axs[0, 2],showfliers = False)
sns.boxplot(data=df, x="good_status", y=vars_[3], ax=axs[1, 1],showfliers = False)
sns.boxplot(data=df, x="good_status", y=vars_[4], ax=axs[1, 2],showfliers = False)
sns.boxplot(data=df, x="good_status", y=vars_[5], ax=axs[1, 0],showfliers = False)

Figure 2: Box plot comparativos dado good_status.

En la Figure 2 se observa que las variables con una tendencia de influir en good_status son int_rate, annual_inc pues aunque no se observa gran diferencia, parece existir una influencia.

Las variables a usar para crear el modelo son:

variables_final=list(chi2_result["Feature"].iloc[0:8])
variables_final=[*variables_final,*vars_,"good_status"]
result=np.array([*variables_final,""])
result.shape=(4,4)
result=pd.DataFrame(result)
result.columns=[""]*4
Covariables del modelo
term home_ownership purpose emp_length
initial_list_status target_time pub_rec pymnt_plan
loan_amnt int_rate annual_inc open_acc
tot_cur_bal month_earliest_cr_line good_status

En la Table 9 se encuentran las covariables que se tendran en cuenta en el modelo.

df.loc[df["home_ownership"] == "ANY","home_ownership"] = 'NONE'
var_cat=['term','pub_rec','home_ownership','initial_list_status','purpose','emp_length']
fig_ = make_subplots(rows=4, cols=2,subplot_titles =var_cat)
fig_ =plot_barplot(df, var_cat[0],"good_status", 1,1,fig_,True)
fig_ =plot_barplot(df, var_cat[1],"good_status", 1,2,fig_, True)
fig_ =plot_barplot(df, var_cat[2],"good_status", 2,1,fig_, True)
fig_ =plot_barplot(df, var_cat[3],"good_status", 2,2,fig_, True)
fig_ =plot_barplot(df, var_cat[4],"good_status", 3,1,fig_, True)
fig_ =plot_barplot(df, var_cat[5],"good_status", 3,2,fig_, True)
# fig_ =plot_barplot(df, var_cat[6],"good_status", 4,1,fig_, True)
fig_.update_layout(
    title="Good status",
    xaxis_tickfont_size=14,
    yaxis=dict(
        title='Distribution percent ',
        titlefont_size=16,
        tickfont_size=14,
    ),
    legend=dict(
        x=1,
        y=1.0,
        bgcolor='rgba(255, 255, 255, 0)',
        bordercolor='rgba(255, 255, 255, 0)'
    ),
    barmode='group',
    bargap=0.15, # gap between bars of adjacent location coordinates.
    bargroupgap=0.1, # gap between bars of the same location coordinate.
    height=1000, width=900
    )

Figure 3: Bar plot comparativos.

En la Figure 3 se observa los bar plot con frecuencia relativa dado la categoría de good_status.

¿Cómo afectan las variables?

  • Cuando el número de pagos es \(>\) 36 meses la probabilidad de incumplimiento es mayor, es decir, un usuario puede inclumir si tiene mas número de pagos al inicio del prestamo.

  • Si el usuario marca NONE o OTHER o RENT influye negativamente, es decir, aumenta la probabilidad de que incumpla sus obligaciones financieras.

  • Si el proposito del prestamo es small_business, house, weddlng aumenta la probabilidad de que incumpla sus obligaciones financieras.

  • A medida que el usuario tiene menor tiempo de trabajo, aumenta la probabilidad de que incumpla sus obligaciones financieras.

Modelos

Se plantean diferentes modelos aplicando validación cruzada 80% prueba 20% test.

Luego de ensayar modelos se encontro que el valor de probabilidad de cambio mas adecuado es 0.8, es decir, si la probabilidad >=0.8 good_status 1, por el contrario 0.

Partición de datos

df_modelo=df[variables_final].copy()
df_modelo.loc[df_modelo["home_ownership"] == "ANY","home_ownership"] = 'NONE'
X = df_modelo.drop('good_status', axis = 1).copy()
X=dummy_creation(X, ["term","home_ownership", "purpose",'initial_list_status','emp_length' ,"pymnt_plan"])
y = df_modelo['good_status'].copy()
# y= np.where(y==1,0,1)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.2, random_state = 421)#, stratify = y)
X_train, X_test = X_train.copy(), X_test.copy()
result=pd.DataFrame({"":["Filas", "Columnas"] ,"Trian":X_train.shape,"Test":X_test.shape })
Dimensiones partición
Trian Test
Filas 301649 75413
Columnas 37 37

En la Table 9 se observa las dimenciones de los datos luego de la partición.

Probabilidad

De los modelos que se van a probar como variable de interés good_status, de los dos modelos se arroja una probabilidad \(P(\text{good_status}=1|\text{covariables} )\) ¿pero como elegir si good_status es 1 o 0 dado las covariables? definimos una proporción de particion \(p\) tal que:

good_status=1 si \(P(\text{good_status}=1|\text{covariables} )\geq p\)

good_status=0 si \(P(\text{good_status}=1|\text{covariables} )<p\)

Por lo general se utiliza una proporción de partición de 0.5 pero para los modelos se analizara la tasa de aciertos variando esta proporción.

Modelo Naive Bayes

De las variables que se escogieron se probo eliminando variables y para el modelo las variables selecionadas son: target_time, pub_rec, loan_amnt, int_rate, open_acc, home_ownership.

var_=[
    'target_time', 
      'pub_rec', 
      'loan_amnt', 
      'int_rate', 
       'open_acc', 
       'home_ownership:NONE', 'home_ownership:OTHER', 'home_ownership:OWN',
       'home_ownership:RENT', 
     ]
     
X_train_pca=X_train[var_].copy()
# X_train_pca=PCA_5.transform(X_train)
X_test_pca=X_test[var_].copy()
# X_test_pca=PCA_5.transform(X_test)
clf = GaussianNB()
clf.fit(X_train_pca, y_train)
## GaussianNB()
predict_train_nb= clf.predict_proba(X_train_pca)[:,1]
predict_test_nb= clf.predict_proba(X_test_pca)[:,1]
list_aciertos_1_train=[]
list_aciertos_0_train=[]
list_aciertos_train=[]
list_aciertos_1_test=[]
list_aciertos_0_test=[]
list_aciertos_test=[]
list_prop=np.arange(0.5,0.95,0.02)
for prop_ in list_prop:
  predict_train_nb_=np.where(predict_train_nb>=prop_,1,0  )
  validacion_train=pd.concat([y_train.reset_index().drop(labels="index", axis=1),pd.DataFrame({"pron":predict_train_nb_})],axis=1)
  tabla_validacion=validacion_train.value_counts()
  aciertos_=((tabla_validacion[1][1]+tabla_validacion[0][0])/tabla_validacion.sum()  )
  aciertos_1=tabla_validacion[1][1]/tabla_validacion[1].sum()
  aciertos_0=tabla_validacion[0][0]/tabla_validacion[0].sum()
  list_aciertos_train.append(aciertos_)
  list_aciertos_1_train.append(aciertos_1)
  list_aciertos_0_train.append(aciertos_0)
  predict_test_nb_=np.where(predict_test_nb>=prop_,1,0  )
  validacion_test=pd.concat([y_test.reset_index().drop(labels="index", axis=1),pd.DataFrame({"pron":predict_test_nb_})],axis=1)
  tabla_validacion_test=validacion_test.value_counts()
  aciertos_1_test=tabla_validacion_test[1][1]/tabla_validacion_test[1].sum()
  aciertos_0_test=tabla_validacion_test[0][0]/tabla_validacion_test[0].sum()
  aciertos_test=(tabla_validacion_test[0][0]+tabla_validacion_test[1][1])/tabla_validacion_test.sum()
  list_aciertos_test.append(aciertos_test)
  list_aciertos_1_test.append(aciertos_1_test)
  list_aciertos_0_test.append(aciertos_0_test)
fig_ = make_subplots(rows=1, cols=2,subplot_titles =["Datos entrenamiento","Datos prueba"]
 )
fig_=fig_.add_trace(go.Scatter(x=list(list_prop), y=list_aciertos_train,
                    mode='lines',
                    name='Global'),row=1,col=1)
                    
fig_=fig_.add_trace(go.Scatter(x=list(list_prop), y=list_aciertos_1_train,
                    mode='lines',
                    name='good_status=1'),row=1,col=1)
fig_=fig_.add_trace(go.Scatter(x=list(list_prop), y=list_aciertos_0_train,
                    mode='lines',
                    name='goo_status=0'),row=1,col=1)                 
fig_=fig_.add_trace(go.Scatter(x=list(list_prop), y=list_aciertos_test,
                    mode='lines',
                    name='Global'),row=1,col=2)
                    
fig_=fig_.add_trace(go.Scatter(x=list(list_prop), y=list_aciertos_1_test,
                    mode='lines',
                    name='good_status=1'),row=1,col=2)
fig_=fig_.add_trace(go.Scatter(x=list(list_prop), y=list_aciertos_0_test,
                    mode='lines',
                    name='goo_status=0'),row=1,col=2)                 
#fig_.show()
fig_.update_layout(
    title="Modelo Bayes",
#    xaxis_tickfont_size=dict(title="Probabilidad de partición.",
#    titlefont_size=16,tickfont_size=14),
    yaxis=dict(
        title='Tasa de aciertos',
        titlefont_size=16,
        tickfont_size=14,
    )
    #,# gap between bars of the same location coordinate.
#  height=1000, width=900
    )
Figure 4: Tasa de aciertos para el modelo Bayes.

En la Figure 4 se observa que la tasa de aciertos es muy similar entre los datos de entrenamiento y los datos de prueba por lo que parece el modelo no sobre ajusta y si la proporción de partición es 0.5 se acierta cuando good_status=1, sin embargo para good_status=0 la tasa de aciertos es casi 0, si se aumenta la proporción de partición cuando good_status=0 esta aumenta mientras que cuando es 1 disminuye, y la proporción para este caso mas viable es en 0.92 consiguiendo una tasa de aciertos para good_status 0,1 y en general de al rededor del 67%.

Modelo logístico

Para este modelo se encontraron las variables apropiadas: target_time, pub_rec, loan_amnt, int_rate, open_acc, home_ownership, emp_length.

var_=[
    'target_time', 
      'pub_rec', 
      'loan_amnt', 
      'int_rate', 
       'open_acc', 
       'home_ownership:NONE', 'home_ownership:OTHER', 'home_ownership:OWN',
       'home_ownership:RENT', 
      'emp_length:10+ years', 'emp_length:2 years',
       'emp_length:3 years', 'emp_length:4 years', 'emp_length:5 years',
       'emp_length:6 years', 'emp_length:7 years', 'emp_length:8 years',
       'emp_length:9 years', 'emp_length:< 1 year'
     ]
X_train_pca=X_train[var_]
# X_train_pca=PCA_5.transform(X_train)
X_test_pca=X_test[var_]
# X_test_pca=PCA_5.transform(X_test)
Modelo = LogisticRegression()#C=1e-09,class_weight="balanced",solver="sag")
Modelo.fit(X_train_pca, y_train)
## LogisticRegression()
prop_=0.8
predict_train_log= Modelo.predict_proba(X_train_pca)[:,1]
predict_test_log= Modelo.predict_proba(X_test_pca)[:,1]

list_aciertos_1_train=[]
list_aciertos_0_train=[]
list_aciertos_train=[]
list_aciertos_1_test=[]
list_aciertos_0_test=[]
list_aciertos_test=[]
list_prop=np.arange(0.5,0.95,0.02)
for prop_ in list_prop:
  predict_train_log_=np.where(predict_train_log>=prop_,1,0  )
  validacion_train=pd.concat([y_train.reset_index().drop(labels="index", axis=1),pd.DataFrame({"pron":predict_train_log_})],axis=1)
  tabla_validacion=validacion_train.value_counts()
  aciertos_=((tabla_validacion[1][1]+tabla_validacion[0][0])/tabla_validacion.sum()  )
  aciertos_1=tabla_validacion[1][1]/tabla_validacion[1].sum()
  aciertos_0=tabla_validacion[0][0]/tabla_validacion[0].sum()
  list_aciertos_train.append(aciertos_)
  list_aciertos_1_train.append(aciertos_1)
  list_aciertos_0_train.append(aciertos_0)
  predict_test_log_=np.where(predict_test_log>=prop_,1,0  )
  validacion_test=pd.concat([y_test.reset_index().drop(labels="index", axis=1),pd.DataFrame({"pron":predict_test_log_})],axis=1)
  tabla_validacion_test=validacion_test.value_counts()
  aciertos_1_test=tabla_validacion_test[1][1]/tabla_validacion_test[1].sum()
  aciertos_0_test=tabla_validacion_test[0][0]/tabla_validacion_test[0].sum()
  aciertos_test=(tabla_validacion_test[0][0]+tabla_validacion_test[1][1])/tabla_validacion_test.sum()
  list_aciertos_test.append(aciertos_test)
  list_aciertos_1_test.append(aciertos_1_test)
  list_aciertos_0_test.append(aciertos_0_test)

fig_ = make_subplots(rows=1, cols=2,subplot_titles =["Datos entrenamiento","Datos prueba"]
 )
fig_=fig_.add_trace(go.Scatter(x=list(list_prop), y=list_aciertos_train,
                    mode='lines',
                    name='Global'),row=1,col=1)
                    
fig_=fig_.add_trace(go.Scatter(x=list(list_prop), y=list_aciertos_1_train,
                    mode='lines',
                    name='good_status=1'),row=1,col=1)
fig_=fig_.add_trace(go.Scatter(x=list(list_prop), y=list_aciertos_0_train,
                    mode='lines',
                    name='goo_status=0'),row=1,col=1)                 
fig_=fig_.add_trace(go.Scatter(x=list(list_prop), y=list_aciertos_test,
                    mode='lines',
                    name='Global'),row=1,col=2)
                    
fig_=fig_.add_trace(go.Scatter(x=list(list_prop), y=list_aciertos_1_test,
                    mode='lines',
                    name='good_status=1'),row=1,col=2)
fig_=fig_.add_trace(go.Scatter(x=list(list_prop), y=list_aciertos_0_test,
                    mode='lines',
                    name='goo_status=0'),row=1,col=2)                 
#fig_.show()
fig_.update_layout(
    title="Modelo Logístico",
#    xaxis_tickfont_size=dict(title="Probabilidad de partición.",
#    titlefont_size=16,tickfont_size=14),
    yaxis=dict(
        title='Tasa de aciertos',
        titlefont_size=16,
        tickfont_size=14,
    )
    #,# gap between bars of the same location coordinate.
#  height=1000, width=900
    )
Figure 5: Tasa de aciertos para el modelo Logístico.

En la Figure 5 se observa que la tasa de aciertos es muy similar entre los datos de entrenamiento y los datos de prueba por lo que parece el modelo no sobre ajusta y si la proporción de partición es 0.5 se acierta cuando good_status=1, sin embargo para good_status=0 la tasa de aciertos es casi 0, si se aumenta la proporción de partición cuando good_status=0 esta aumenta mientras que cuando es 1 disminuye, si se compara la proporción de partición en 0.92 parece ser mejor el modelo de bayes, sin embargo algo importante con este modelo logístico es que cuando se tiene una proporción 0.8 la tasa de aciertos para el caso de good_status=0 es mayor que en el caso del modelo bayes y si se considera una proporción de 0.92 es un poco grande mientras que el logístico con una probabilidad más pequeña puede captar si cumple o no sus obligaciones financieras.

Por lo anterior se trabajara con el modelo logístico con dichas covariables.

Score

Con las variables seleccionadas se obta por crear un modelo con todos los datos, las variables numericas se dividen en 4 grupos (open_acc en 2 grupos) con pd.cut, se define un score de 300 a 850.

Para las variables el nivel de refencia son:

  • loan_amnt: (9500.0, 18000.0]

  • int_rate: (5.98, 11.015]

  • open_acc: (-0.084, 42.0]

  • home_ownership: MORTGAGE

  • emp_length: 1 year

X_model=X[var_ ].copy()
X_model=X_model.reset_index()
X_model=X_model.drop(labels="index", axis=1)
# varibles continuas
X_model["loan_amnt"]=pd.cut(X_model["loan_amnt"],4)
X_model["int_rate"]=pd.cut(X_model["int_rate"],4)
X_model["open_acc"]=pd.cut(X_model["open_acc"],2)
# dummy de variables
X_model=dummy_creation(X_model, ["loan_amnt","int_rate", "open_acc"])
# ajuiste modelo logistico
modelo_final = LogisticRegression()
modelo_final.fit(X_model, Y)
## LogisticRegression()
df_scorecard=pd.DataFrame({"variables_modelo":["(intercept)", *list(X_model.columns)],
                      "Coefficients":[modelo_final.intercept_[0],*modelo_final.coef_.tolist()[0]] }) 
df_scorecard["var_origen"]=df_scorecard["variables_modelo"].apply(lambda x: x.split(":")[0] )
min_score = 300
max_score = 850
def min_temp(vector):
    result=vector.min()
    if result>0 :
        result=0
    return result
def max_temp(vector):
    result=vector.max()
    if result<0:
        result=0
    return result
# calculate the sum of the minimum coefficients of each category within the original feature name
min_sum_coef = df_scorecard.groupby('var_origen')['Coefficients'].apply(min_temp).sum()+df_scorecard.loc[0,'Coefficients']
# calculate the sum of the maximum coefficients of each category within the original feature name
max_sum_coef = df_scorecard.groupby('var_origen')['Coefficients'].apply(max_temp).sum()#+df_scorecard.loc[0,'Coefficients']
df_scorecard['Score - Calculation'] = df_scorecard['Coefficients'] * (max_score - min_score) / (max_sum_coef - min_sum_coef)
# min_sum_coef = df_scorecard.groupby('var_origen')['Coefficients'].apply(min).sum()
# update the calculated score of the Intercept
df_scorecard.loc[0, 'Score - Calculation'] = ((df_scorecard.loc[0,'Coefficients'] - min_sum_coef) /
                                              (max_sum_coef - min_sum_coef
                                              )) * (max_score - min_score) + min_score
# # round the values of the 'Score - Calculation' column and store them in a new column
df_scorecard['Score - Final'] = df_scorecard['Score - Calculation'].round()

# check the min and max possible scores of our scorecard
min_sum_score_prel = df_scorecard.groupby('var_origen')['Score - Final'].apply(min_temp).sum()+df_scorecard.loc[0, 'Score - Final']
max_sum_score_prel = df_scorecard.groupby('var_origen')['Score - Final'].apply(max_temp).sum()
score de cada variable
variables_modelo Coefficients var_origen Score - Calculation Score - Final
(intercept) 3.90 (intercept) 773.17 773
target_time -1.67 target_time -170.63 -171
pub_rec 0.20 pub_rec 20.24 20
home_ownership:NONE -0.49 home_ownership -50.15 -50
home_ownership:OTHER -0.81 home_ownership -82.93 -83
home_ownership:OWN -0.08 home_ownership -8.40 -8
home_ownership:RENT -0.25 home_ownership -25.69 -26
emp_length:10+ years 0.08 emp_length 7.68 8
emp_length:2 years 0.00 emp_length -0.12 0
emp_length:3 years 0.00 emp_length -0.35 0
emp_length:4 years 0.02 emp_length 2.38 2
emp_length:5 years -0.06 emp_length -5.71 -6
emp_length:6 years -0.10 emp_length -9.79 -10
emp_length:7 years -0.02 emp_length -1.97 -2
emp_length:8 years -0.04 emp_length -3.84 -4
emp_length:9 years -0.06 emp_length -6.22 -6
emp_length:< 1 year -0.07 emp_length -6.72 -7
loan_amnt:(9500.0, 18000.0] -0.17 loan_amnt -17.06 -17
loan_amnt:(18000.0, 26500.0] -0.19 loan_amnt -19.34 -19
loan_amnt:(26500.0, 35000.0] -0.10 loan_amnt -9.82 -10
int_rate:(11.015, 16.03] -0.89 int_rate -90.80 -91
int_rate:(16.03, 21.045] -1.48 int_rate -151.28 -151
int_rate:(21.045, 26.06] -1.86 int_rate -190.47 -190
open_acc:(42.0, 84.0] 0.48 open_acc 48.91 49

En la Table 11 se puede observar que la población que desde el inicio del crédito al final del crédito con \(\leq 12\) meses afecta negativamente al score con 171 puntos menos, si se tiene el estado de la vivienda diferente a hipoteca, afecta negativamente, si se tiene varias lineas de crédito (\(\geq\) 42) afecta de manera positia, si la tasa de interes es alta afecta de manera negativa al score, si se pide un prestamo superior a 9.500 dolares afecta el score.

scores=np.array(df_scorecard["Score - Final"])
scores.shape=(24,1)
X_model_temp=pd.concat([ pd.DataFrame({"(intercept)" :list(np.repeat(1,X_model.shape[0]))} ),X_model],axis=1,ignore_index=True ).copy() 
score_poblacional=np.matmul(np.array(X_model_temp),scores)
score_poblacional=pd.DataFrame(np.transpose(score_poblacional).tolist()[0])
Y=Y.reset_index()
Y=Y.drop(labels="index",axis=1 )
score_poblacional["good_status"]=Y
fig = px.histogram(score_poblacional, x=0 ,color="good_status",
                   marginal="box",  #or violin, rug
                   hover_data=score_poblacional.columns)
fig.show()
#score_poblacional.to_csv("score_poblacional.csv")

Figure 6: Distribución de score | good_status.

En la Figure 6 se observa la distrubución del score para toda la población y se discrimina por good_status, se observa que aquellos usuarios con incumpliminetos financieros en promedio tienen un score menor a los que no tienen incumpliminetos financieros.

Conclusión

Hay varios factores, unos más determinantes que otros, en todo el problema de riesgo de crédito.

Dentro de los factores que más influyen se encuentran: la estabilidad laboral, conocida también como el tiempo que lleva cada individuo trabajando para la misma empresa. El número de líneas de crédito que tiene, que se puede interpretar como el nivel de endeudamiento del individuo. El si ha incumplido obligaciones financieras en el pasado. El número y tipo de bienes raíces que posee. También es vital la cantidad del préstamo solicitado, ya que si la cantidad es pequeña se puede ser más indulgente que en caso contrario. Otro factor importante es el interés al cual se presta, pues, a mayor interés es más poco probable que el crédito sea otorgado.