Análisis de datos Aval Buró de Crédito

Introducción

En los últimos años surge un especial interés en las instituciones financieras en conocer los riesgos de financiamiento y liquidez, a los que están expuestas estas empresas; con el fin de proponer instrumentos que les permitan actuar oportunamente y de esta forma, tomar adecuadas decisiones se hace uso de ciertas herramientas que bajo un fundamento estadístico y matemático ayuda a poder estimar el riesgo del crédito y la recuperación de cuentas por cobrar. Se han diseñado modelos estadísticos, en base a un conjunto de atributos que permiten calificar objetivamente la calidad de un cliente.

En el presente trabajo se analiza una base de datos que contiene el detalle de clientes de una entidad financiera. Lo que se realizará es un análisis y procesamiento de los datos para una futura modelización de la probabilidad de caer en default (Mal pagador) dado un conjunto de variables exógenas. Nuestra variable objetivo es BM la cual clasifica a los clientes en “Buenos”(si no presenta atrasos) o “Malos”(si presenta atrasos superiores a 45 días en el pago de sus obligaciones de crédito).

1. Preanálisis y limpieza de datos

Librerías que serán usadas en éste trabajo.

library(readxl)
library(tidyverse)
library(data.table)
library(knitr)
library(kableExtra)
library(DataExplorer)
library(FNN)
library(PCAmixdata)
library(Information)
library(caret)
library(rpart)

Procedemos a la lectura de los datos:

datos <- setDT(read_excel("bases/base.xlsx"))
kable(head(datos), align = 'c')
Identificacion BM PorcMesesAlDia_6M LN_Sum_SalVenDjuCCaDSCE_36M r_numOpsVenc_36M Prom_NumDiasVencido_12M ANTIGUEDAD_TRABAJO ingresoNoComprometido EstadoCivil RelacionLaboral TipoVivienda REGION
1 0 1.0000000 0 0.00 0.0000000 23 0.8591550 DIVORCIADO Independiente Propia no hipotecada SIERRA
2 0 0.6666667 0 0.25 0.0000000 23 0.6435970 DIVORCIADO Independiente Propia no hipotecada SIERRA
3 0 1.0000000 0 0.00 0.0000000 120 0.3681624 CASADO Dependiente Propia no hipotecada SIERRA
4 0 0.8333333 0 0.00 0.0833333 47 0.0000000 SOLTERO Independiente Arrendado SIERRA
5 0 1.0000000 0 0.00 0.0000000 120 -0.5315200 CASADO Independiente Propia no hipotecada SIERRA
6 0 1.0000000 0 0.00 0.0000000 120 0.4452300 CASADO Dependiente Familiar SIERRA

Revisamos la posible existencia de valores faltantes:

plot_missing(datos, title = "Porcentaje de valores faltantes")

Notemos que no hay valores NA. Por tanto no hay necesidad de alguna imputación en el conjunto de datos.

Valores Atípicos

Para poder realizar una detección de datos atípicos vamos a separar las variables entre cuantitativas y cualitativas:

data.cut <- splitmix(datos)
data.quanti<-as.data.frame(data.cut$X.quanti[-1])#omitimos la columna identificación
data.quali<-as.data.frame(data.cut$X.quali)

Distribuciones de las variables cuantitativas

ggplot(datos) + 
  aes(x = PorcMesesAlDia_6M) + 
  geom_density() + 
  theme(legend.position="none") + 
  labs(title = "Densidad de la variable *PorcMesesAlDia_6M*")

Recordemos que esta variable representa el porcentaje de meses en los que el cliente pagó al día en el último semestre, así el gráfico de densidad tiene bastante sentido ya que los picos consisten en los 6 últimos meses y podemos ver que la gran mayoría de clientes han pagado los 6 últimos meses al día.

A continuación presentamos las densidades de todas las variables cuantitativas:

data.quanti %>% gather(metric, value) %>%ggplot(aes(value, fill = metric)) +
  geom_density(show.legend = FALSE) +
  facet_wrap(~ metric, scales = "free") + 
  labs(title = "Densidad de las variables cuantitativas")

Dado que a simple vista podría decirse que hay una posible existencia de datos anómalos, es mejor realizar un gráfico de cajas y bigotes para una mejor precisión en la detección de estas anomalías:

data.quanti %>% gather(metric, value) %>%ggplot(aes(value, fill = metric)) +
  geom_boxplot(show.legend = FALSE) +
  facet_wrap(~ metric, scales = "free") + 
  labs(title = "Gráfico de cajas de las variables cuantitativas")

Para tener un criterio mas potente para determinar cuales serán las observaciones con anomalías, haremos uso de un algoritmo para la detección de los mismos.

KNN (K-Nearest-Neighbor), en sus siglas en inglés, es un algoritmo basado en instancia de tipo supervisado de Machine Learning. Se lo utilizará pues el supuesto fundamental en el vecino más cercano es que las observaciones similares están próximas entre sí y los valores atípicos suelen ser observaciones solitarias, que se mantienen más alejadas del grupo de observaciones similares.

Cuando se trata de la detección de anomalías, adopta un enfoque no supervisado. Esto se debe a que no hay un “aprendizaje” real involucrado en el proceso y no hay un etiquetado predeterminado de “valor atípico” o “no atípico” en el conjunto de datos, sino que se basa completamente en valores de umbral.

Para ello, se utilizará la función get.knn, que son algoritmos rápidos de búsqueda de vecino más cercano k incluyendo un árbol-kd (kd-tree), árbol de cobertura (cover-tree) y el algoritmo implementado en el paquete de clases, siendo k el número máximo de vecinos más cercanos para buscar. Se tomamará k=5 y el percentil 0.975 para declarar como cota y se obtendrá una lista de indices que superan la cota:

data.out <- get.knn(data.quanti,k=5)
kable(head(data.out$nn.dist,10))
0.0032145 0.0183678 0.0208720 0.0245709 0.0250641
0.0042738 0.2502265 0.2507925 0.2538469 0.2563851
0.0001298 0.0007253 0.0007476 0.0007745 0.0008838
0.0833333 0.0833333 0.0833333 0.0833333 0.0833333
0.0000495 0.0014557 0.0033983 0.0060723 0.0063014
0.0000586 0.0001889 0.0002327 0.0005695 0.0006589
0.0000000 0.0000000 0.0000000 0.0000000 0.0000000
0.0000000 0.0000000 0.0000000 0.0000000 0.0000000
0.0118629 0.0144475 0.0195458 0.0213165 0.0232702
0.0000942 0.0003315 0.0007561 0.0008789 0.0009339
## El total de datos atípicos en la base de datos es: 532
valores <- as.factor(ifelse(score>cota,"Eliminar","No eliminar"))
obs <- c(1:length(score))
pd <- data.frame(score,valores,obs) 
ggplot(pd, aes(x=obs,y=score, col=valores)) + 
  geom_point() + 
  labs(title = "Grafico de etiquetado de observaciones mediante KNN")

De este modo, estos \(532\) datos deben ser separados del conjunto de datos para un futuro análisis.

datos <- datos[-index,]
rm(data.cut, data.out, data.quali, data.quanti, pd)#removemos los datos del ambiente para no ocupar mucha memoria

Análisis de datos exploratorios (EDA)

datos %>% mutate(BM = ifelse(datos$BM == 0, "Buen Pagador", "Mal Pagador")) %>% 
ggplot() + 
  aes(x = BM, fill = BM) + 
  geom_bar() +
  geom_text(aes(label = scales::percent(((..count..)/sum(..count..))), y= ((..count..)/sum(..count..))), stat= "count", vjust = -.5) +
  labs(title = "Gráfico de barras de la variable BM (tipo de cliente)")

Notemos que existe una diferencia muy grande en cuanto a la proporción del tipo de cliente, lo cual podría generar futuros problemas a la hora de generar un posible modelo sobre esta variable.

ggplot(datos) +
  aes(x = RelacionLaboral, fill = RelacionLaboral) +
  geom_bar() +
  geom_text(aes(label = scales::percent(((..count..)/sum(..count..))), y= ((..count..)/sum(..count..))), stat= "count", vjust = -.5) +
  theme(axis.text.x = element_text(angle = 60, margin = margin(b = -0.5, unit = "cm"), vjust = 0.7)) + 
  labs(title = "Gráfico de barras del tipo de relación laboral de los clientes", y = "Número de clientes")

Notemos que en su mayoría los clientes son independientes laboralmente.

ggplot(datos) +
  aes(x = EstadoCivil, fill = EstadoCivil) +
  geom_bar() +
  geom_text(aes(label = scales::percent(((..count..)/sum(..count..))), y= ((..count..)/sum(..count..))), stat= "count", vjust = -.5) +
  theme(axis.text.x = element_text(angle = 60, margin = margin(b = -0.5, unit = "cm"), vjust = 0.7)) +
  labs(title = "Gráfico de barras del estado civil de los clientes", y = "Número de clientes")

Nótese que los perfiles mas predominantes son el de casado y de soltero, como era de esperarse. Otro gráfico que podría arrojar mayor información para el estudio sería cuál estado civil es el mejor pagador:

datos %>% mutate(BM = ifelse(datos$BM == 0, "Buen Pagador", "Mal Pagador")) %>% 
ggplot() +
  aes(x = BM, fill = BM) + 
  geom_bar() +
  theme(axis.text.x=element_blank(), axis.ticks.x=element_blank()) +
  facet_wrap(~EstadoCivil) +
  labs(title = "Gráfico de barras de tipo de cliente por estado civil")

Notemos que en casi todos los niveles de estado civil se tiene la misma relación entre buenos y malos pagadores.

datos %>% mutate(BM = ifelse(datos$BM == 0, "Buen Pagador", "Mal Pagador")) %>% 
ggplot() + 
  aes(x = ANTIGUEDAD_TRABAJO, fill = BM) + 
  geom_histogram(bins = 10, color = 'black') + 
  theme_minimal() + labs(title = "Histograma de antiguedad de trabajo con detalle de buen o mal pagador", y = "Número de observaciones")

Podría decirse que a media que crece la antiguedad en el trabajo se tiene una mayor cantidad de buenos clientes con respecto de los malos, aunque la diferencia no es muy marcada si puede observarse que en las dos barras extremas la de mayor antiguedad posee en proporción menos clientes malos en comparación con la barra de menor antiguedad.

2. Procesamiento

Primeramente vamos a realizar un cambio en la estructura de los datos, vamos a transformar nuestro conjunto de datos en un dataframe.

datos <- as.data.frame(datos)
str(datos)
## 'data.frame':    20725 obs. of  12 variables:
##  $ Identificacion             : num  1 2 3 4 5 6 7 8 9 10 ...
##  $ BM                         : num  0 0 0 0 0 0 0 0 0 0 ...
##  $ PorcMesesAlDia_6M          : num  1 0.667 1 0.833 1 ...
##  $ LN_Sum_SalVenDjuCCaDSCE_36M: num  0 0 0 0 0 0 0 0 0 0 ...
##  $ r_numOpsVenc_36M           : num  0 0.25 0 0 0 0 0 0 0 0 ...
##  $ Prom_NumDiasVencido_12M    : num  0 0 0 0.0833 0 ...
##  $ ANTIGUEDAD_TRABAJO         : num  23 23 120 47 120 120 120 24 36 120 ...
##  $ ingresoNoComprometido      : num  0.859 0.644 0.368 0 -0.532 ...
##  $ EstadoCivil                : chr  "DIVORCIADO" "DIVORCIADO" "CASADO" "SOLTERO" ...
##  $ RelacionLaboral            : chr  "Independiente" "Independiente" "Dependiente" "Independiente" ...
##  $ TipoVivienda               : chr  "Propia no hipotecada" "Propia no hipotecada" "Propia no hipotecada" "Arrendado" ...
##  $ REGION                     : chr  "SIERRA" "SIERRA" "SIERRA" "SIERRA" ...

Cambiamos las variables que son tipo character a tipo factor, para una mejor maipulación del dataset:

datos <- mutate_if(datos, is.character, as.factor)
datos$BM <- as.factor(datos$BM)
str(datos)
## 'data.frame':    20725 obs. of  12 variables:
##  $ Identificacion             : num  1 2 3 4 5 6 7 8 9 10 ...
##  $ BM                         : Factor w/ 2 levels "0","1": 1 1 1 1 1 1 1 1 1 1 ...
##  $ PorcMesesAlDia_6M          : num  1 0.667 1 0.833 1 ...
##  $ LN_Sum_SalVenDjuCCaDSCE_36M: num  0 0 0 0 0 0 0 0 0 0 ...
##  $ r_numOpsVenc_36M           : num  0 0.25 0 0 0 0 0 0 0 0 ...
##  $ Prom_NumDiasVencido_12M    : num  0 0 0 0.0833 0 ...
##  $ ANTIGUEDAD_TRABAJO         : num  23 23 120 47 120 120 120 24 36 120 ...
##  $ ingresoNoComprometido      : num  0.859 0.644 0.368 0 -0.532 ...
##  $ EstadoCivil                : Factor w/ 5 levels "CASADO","DIVORCIADO",..: 2 2 1 4 1 1 1 4 5 5 ...
##  $ RelacionLaboral            : Factor w/ 6 levels "Dependiente",..: 2 2 1 2 2 1 1 1 2 2 ...
##  $ TipoVivienda               : Factor w/ 7 levels "Arrendado","Familiar",..: 7 7 7 1 7 2 7 7 7 2 ...
##  $ REGION                     : Factor w/ 3 levels "AMAZONIA","COSTA",..: 3 3 3 3 3 3 1 3 3 3 ...

Selección de variables

Para realizar un buen modelo, es necesario introducir las variables que tengan el mayor poder predictivo con respecto a la variable objetivo.

En nuestro caso, la variable objetivo es BM la cual es dicotómica.

- Valor de información IV

Muy tradicionalmente se ha conciderado el IV como una regla discriminatoria para el poder predictivo de las variables. Este IV tiene las siguientes reglas.

Reglas:

  • IV < 0.02 sin poder predictivo.

  • 0.02 < IV < 0.1 poder predictivo bajo.

  • 0.1 < IV < 0.3 poder predictivo medio.

  • 0.3 < IV < 0.5 poder predictivo fuerte.

  • IV > 0.5 poder predictivo sospechosamente fuerte.

DatosIV<-datos[, -1]
DatosIV$BM<-as.numeric(DatosIV$BM)
DatosIV$BM<-ifelse(DatosIV$BM==1,0,1)
IV<- Information::create_infotables(data=DatosIV, y="BM", parallel = FALSE)
knitr::kable(head(IV$Summary))
Variable IV
1 PorcMesesAlDia_6M 0.6686113
4 Prom_NumDiasVencido_12M 0.6207141
5 ANTIGUEDAD_TRABAJO 0.2956649
8 RelacionLaboral 0.2651091
6 ingresoNoComprometido 0.2260130
2 LN_Sum_SalVenDjuCCaDSCE_36M 0.1327862

- Analisis de interacción de variables independientes con la variable dependiente

Otra forma de ver cuales variables están con mayor relación de dependencia con nuestra variable objetivo, lo podemos realizar mediante una matriz de correlación. La matriz de correlación nos explica como se encuentran relacionadas cada una de las variables con otra variable. Mientras el coeficiente de correlación se encuentre mas cercano a cero las variables son más independientes.

datos1 <- as.data.table(datos)
datos1 <- datos1 %>% mutate_if(is.factor, as.numeric) #convertimos las variable tipo factor a numericas
kable(cor(datos1[,-1]), caption = "Matriz de correlación")
Matriz de correlación
BM PorcMesesAlDia_6M LN_Sum_SalVenDjuCCaDSCE_36M r_numOpsVenc_36M Prom_NumDiasVencido_12M ANTIGUEDAD_TRABAJO ingresoNoComprometido EstadoCivil RelacionLaboral TipoVivienda REGION
BM 1.0000000 -0.2302612 0.1004755 0.1059263 0.2859499 -0.1236307 -0.0859131 0.0299427 0.0983453 -0.0422956 -0.0083119
PorcMesesAlDia_6M -0.2302612 1.0000000 -0.2606972 -0.2205515 -0.3871559 0.0682109 0.1655208 0.0448443 -0.0716597 -0.0253385 -0.0302654
LN_Sum_SalVenDjuCCaDSCE_36M 0.1004755 -0.2606972 1.0000000 0.6487830 0.0601568 -0.0089279 -0.0744798 0.0073283 0.0114426 -0.0432484 0.0199902
r_numOpsVenc_36M 0.1059263 -0.2205515 0.6487830 1.0000000 0.0893770 -0.0502344 -0.0266948 0.0355233 0.0194621 -0.0581990 -0.0023434
Prom_NumDiasVencido_12M 0.2859499 -0.3871559 0.0601568 0.0893770 1.0000000 -0.0690272 -0.0199390 -0.0118496 0.0798750 0.0097563 0.0093893
ANTIGUEDAD_TRABAJO -0.1236307 0.0682109 -0.0089279 -0.0502344 -0.0690272 1.0000000 0.0513511 -0.1322516 -0.3549288 0.1669440 -0.0447375
ingresoNoComprometido -0.0859131 0.1655208 -0.0744798 -0.0266948 -0.0199390 0.0513511 1.0000000 0.0421066 -0.0179946 -0.0681859 -0.0553075
EstadoCivil 0.0299427 0.0448443 0.0073283 0.0355233 -0.0118496 -0.1322516 0.0421066 1.0000000 -0.0285092 -0.2543081 -0.0856955
RelacionLaboral 0.0983453 -0.0716597 0.0114426 0.0194621 0.0798750 -0.3549288 -0.0179946 -0.0285092 1.0000000 0.0210349 0.0139629
TipoVivienda -0.0422956 -0.0253385 -0.0432484 -0.0581990 0.0097563 0.1669440 -0.0681859 -0.2543081 0.0210349 1.0000000 -0.0536148
REGION -0.0083119 -0.0302654 0.0199902 -0.0023434 0.0093893 -0.0447375 -0.0553075 -0.0856955 0.0139629 -0.0536148 1.0000000

Notemos que de esta matriz se puede observar en la primara fila, que las variables que guardan una mayor relación con la variable objetivo de mayor a menor son:

  1. Prom_NumDiasVencido_12M.
  2. PorcMesesAlDia_6M.
  3. ANTIGUEDAD_TRABAJO.
  4. r_numOpsVenc_36M.
  5. LN_Sum_SalVenDjuCCaDSCE_36M.
  6. RelacioLaboral.
  7. ingresoNoComprometido.
  8. TipoVivienda.
  9. EstadoCIvil.
  10. REGION.

Notemos que el ranking de correlación es bastante similar al arrojado por el criterio de información IV, por tanto podría realizarse un futuro modelo con las variables con mayor índice de relación con la variable objetivo.

Conclusiones

Como se vió en los gráficos uno de los posible problemas es lo desequilibrados que se encuentran los datos respecto a la variable objetivo (BM). Desde una perspectiva de modelado, un conjunto de datos desequilibrado presenta numerosos desafíos al desarrollar un modelo predictivo. Primero, un conjunto de datos desequilibrado tiende a producir un modelo con métricas de rendimiento infladas artificialmente.

Otro desafío de un conjunto de datos desequilibrado es que el modelo creado puede tener un sesgo hacia la clase representada más alta. El modelo tratará a la clase minoritaria como ruido que conduce a altos niveles de clasificación errónea.

Hay una serie de técnicas, que van de simples a complejas, que pueden ayudar a abordar los datos desequilibrados.