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.
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
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.
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.
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.
#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
| 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.
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'))
df['target_time']=0
df.loc[df["month_last"]<=12,'target_time']=1
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.
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] ]} )
| 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.
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()
| 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.
| 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:
| 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})
| 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.
¿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 } )
| 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)
| 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)
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)
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
| 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.
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.
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.
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 })
| Trian | Test | |
|---|---|---|
| Filas | 301649 | 75413 |
| Columnas | 37 | 37 |
En la Table 9 se observa las dimenciones de los datos luego de la partición.
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.
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
)
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%.
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
)
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.
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()
| 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.
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.