El objetivo principal de este proyecto es desarrollar un modelo de riesgo crediticio para Bankaya que permita diferenciar eficazmente entre clientes de “buen” y “mal” comportamiento crediticio. Al integrar información transaccional interna de los solicitantes con su historial crediticio externo, buscaremos generar un riesgo_score predictivo y, con base en él, proponer un sistema de tasas de interés dinámicas individualizadas. Esto facilitará la toma de decisiones de aprobación y pricing de préstamos para la compra de smartphones, optimizando la gestión de riesgos y promoviendo un crecimiento sostenible de la cartera de clientes.
Dataset Principal (main_dataset.parquet) Contiene información detallada de las solicitudes de préstamos de Bankaya, incluyendo datos del cliente y su interacción con la plataforma.
-customer_id: Identificador único del cliente.
-loan_id: Identificador único del préstamo.
-ACC_CREATION_DATETIME Fecha de creación de la cuenta del cliente.
-APPLICATION_DATETIME Fecha en la que se solicitó el préstamo.
LOAN_ORIGINATION_DATETIME: Fecha en que el préstamo fue aprobado o iniciado.
max_days_late: Máximo número de días que el cliente se atrasó en un pago.
target: Variable objetivo original (0: buen comportamiento, 1: mal comportamiento).
account_to_application_days: Días entre la creación de cuenta y la solicitud del préstamo.
n_sf_apps: Número de solicitudes previas en la plataforma “SF” (no siempre presente).
first_app_date: Fecha de la primera solicitud de crédito registrada.
last_app_date: Fecha de la última solicitud de crédito registrada.
n_bnpl_apps: Número de aplicaciones tipo “Buy Now Pay Later” hechas por el cliente.
n_bnpl_approved_apps: Número de esas aplicaciones que fueron aprobadas.
first_bnpl_app_date: Fecha de la primera solicitud BNPL.
last_bnpl_app_date: Fecha de la última solicitud BNPL.
n_inquiries_l3m: Número de consultas de crédito en los últimos 3 meses.
n_inquiries_l6m: Número de consultas de crédito en los últimos 6 meses.
Dataset de Reportes de Crédito (credit_reports.parquet) Este dataset contiene el historial crediticio externo de los clientes, donde cada fila representa un registro de crédito específico del cliente con diversas entidades financieras (ej. préstamos, tarjetas de crédito). Un mismo customer_id puede tener múltiples entradas en este dataset.
-customer_id: Identificador único del cliente (clave de unión con main_dataset).
-REPORT_DATE: Fecha de generación o actualización del reporte de crédito.
-LOAN_OPENING_DATE: Fecha de apertura del crédito externo.
-LOAN_CLOSING_DATE: Fecha de cierre o terminación del crédito externo.
-CREDIT_TYPE: Tipo de crédito (ej., tarjeta de crédito, préstamo personal, hipoteca).
-PAYMENT_FREQUENCY: Frecuencia de los pagos de este crédito (ej., mensual, semanal).
-MAX_CREDIT: Monto máximo de crédito aprobado para esta línea de crédito.
-CREDIT_LIMIT: Límite de crédito asignado para esta línea de crédito.
-PAYMENT_AMOUNT: Monto del pago más reciente registrado.
-CURRENT_BALANCE: Saldo actual pendiente de pago en esta línea de crédito.
-BALANCE_DUE: Monto total vencido o adeudado.
-BALANCE_DUE_WORST_DELAY: Monto máximo que estuvo vencido o adeudado en el peor momento de atraso.
-DELAYED_PAYMENTS: Número de pagos que el cliente ha atrasado en esta cuenta.
-WORST_DELAY: El peor número de días de atraso registrado para este crédito.
-WORST_DELAY_DATE: Fecha en que se registró el peor atraso.
-TOTAL_PAYMENTS: Número total de pagos realizados para esta cuenta.
-TOTAL_REPORTED_PAYMENTS: Número total de pagos reportados a las agencias de crédito.
-UPDATE_DATE: Fecha de la última actualización de este registro de crédito.
-LAST_PURCHASE_DATE: Fecha de la última compra o disposición de crédito.
-LAST_PAYMENT_DATE: Fecha del último pago registrado.
# Librerías necesarias
library(arrow)
library(rpart) # Para Árboles de Decisión
library(rpart.plot)# Para visualizar Árboles de Decisión
library(dplyr)
library(tidyr)
library(ggplot2)
library(corrplot)
library(DT)
library(caret)
library(ROSE) # Para balancear clases
library(randomForest)
library(xgboost)
library(GGally)
library(e1071) # Para SVM
library(nnet) # Para Red Neuronal
library(MLmetrics)
library(knitr)
library(kableExtra)
library(scales)
library(lubridate) # Para manejo de fechas en df2
library(janitor) # Para limpieza de nombres de columnas en df2
library(tidyverse) # Colección de paquetes, ya tienes algunos individuales pero lo incluyo por si acaso
Importamos la base de datos con su respectivo summary y visualización de las primeras filas
df <- read_parquet("C:/Users/DELL/Downloads/main_dataset.parquet")
print(head(df))
## # A tibble: 6 × 17
## customer_id loan_id ACC_CREATION_DATETIME APPLICATION_DATETIME
## <int> <int> <dttm> <dttm>
## 1 1223 1 2021-08-23 08:57:56 2022-04-26 02:00:00
## 2 5190 2 2022-04-26 04:57:25 2022-04-26 02:00:00
## 3 5194 3 2022-04-26 07:22:35 2022-04-26 02:00:00
## 4 3978 4 2022-03-09 05:26:55 2022-04-26 02:00:00
## 5 4535 5 2022-04-01 08:28:42 2022-04-26 02:00:00
## 6 3604 6 2022-02-21 05:55:32 2022-05-05 02:00:00
## # ℹ 13 more variables: LOAN_ORIGINATION_DATETIME <dttm>, max_days_late <int>,
## # target <int>, account_to_application_days <int>, n_sf_apps <dbl>,
## # first_app_date <dttm>, last_app_date <dttm>, n_bnpl_apps <dbl>,
## # n_bnpl_approved_apps <dbl>, first_bnpl_app_date <dttm>,
## # last_bnpl_app_date <dttm>, n_inquiries_l3m <dbl>, n_inquiries_l6m <dbl>
print(summary(df))
## customer_id loan_id ACC_CREATION_DATETIME
## Min. : 1 Min. : 1 Min. :2020-10-14 13:22:10.00
## 1st Qu.: 3614 1st Qu.: 3614 1st Qu.:2022-02-21 12:46:22.25
## Median : 7228 Median : 7228 Median :2022-07-19 15:29:43.50
## Mean : 7228 Mean : 7228 Mean :2022-06-17 02:24:49.44
## 3rd Qu.:10841 3rd Qu.:10841 3rd Qu.:2022-11-13 01:37:39.25
## Max. :14454 Max. :14454 Max. :2023-05-19 13:55:04.00
##
## APPLICATION_DATETIME LOAN_ORIGINATION_DATETIME
## Min. :2022-04-26 02:00:00.0 Min. :2022-07-01 04:03:20.00
## 1st Qu.:2022-09-15 08:00:00.0 1st Qu.:2022-10-27 16:15:58.25
## Median :2022-12-20 02:00:00.0 Median :2023-01-11 04:05:49.50
## Mean :2022-11-27 21:42:40.9 Mean :2022-12-28 00:04:09.50
## 3rd Qu.:2023-02-04 02:00:00.0 3rd Qu.:2023-03-06 12:07:46.25
## Max. :2023-05-26 01:00:00.0 Max. :2023-05-29 06:18:28.00
##
## max_days_late target account_to_application_days n_sf_apps
## Min. :-7.00 Min. :0.0000 Min. : 0.0 Min. : 1.000
## 1st Qu.: 0.00 1st Qu.:0.0000 1st Qu.: 0.0 1st Qu.: 1.000
## Median : 2.00 Median :0.0000 Median :103.0 Median : 1.000
## Mean :14.23 Mean :0.1868 Mean :163.5 Mean : 1.654
## 3rd Qu.:20.00 3rd Qu.:0.0000 3rd Qu.:271.8 3rd Qu.: 2.000
## Max. :70.00 Max. :1.0000 Max. :901.0 Max. :42.000
## NA's :7648
## first_app_date last_app_date
## Min. :2021-04-26 19:00:00.00 Min. :2021-04-24 19:00:00.00
## 1st Qu.:2022-02-26 18:00:00.00 1st Qu.:2022-02-24 18:00:00.00
## Median :2022-07-14 19:00:00.00 Median :2022-07-15 19:00:00.00
## Mean :2022-06-15 23:31:39.97 Mean :2022-06-15 20:42:11.52
## 3rd Qu.:2022-10-20 19:00:00.00 3rd Qu.:2022-10-21 19:00:00.00
## Max. :2023-05-11 18:00:00.00 Max. :2023-05-11 18:00:00.00
## NA's :7648 NA's :7648
## n_bnpl_apps n_bnpl_approved_apps first_bnpl_app_date
## Min. : 1.000 Min. : 0.000 Min. :2022-01-06 15:17:08.19
## 1st Qu.: 1.000 1st Qu.: 0.000 1st Qu.:2022-05-01 16:03:56.96
## Median : 1.000 Median : 0.000 Median :2022-08-18 08:36:14.26
## Mean : 1.222 Mean : 0.265 Mean :2022-08-13 04:30:46.84
## 3rd Qu.: 1.000 3rd Qu.: 0.000 3rd Qu.:2022-11-06 13:24:55.19
## Max. :18.000 Max. :15.000 Max. :2023-05-20 11:15:47.00
## NA's :5715 NA's :5715 NA's :5715
## last_bnpl_app_date n_inquiries_l3m n_inquiries_l6m
## Min. :2022-01-06 15:17:08.19 Min. : 0.00 Min. : 0.00
## 1st Qu.:2022-04-20 00:33:33.58 1st Qu.: 0.00 1st Qu.: 0.00
## Median :2022-07-28 12:37:41.68 Median : 0.00 Median : 8.00
## Mean :2022-08-03 04:11:07.58 Mean : 10.35 Mean : 17.11
## 3rd Qu.:2022-11-05 19:50:47.64 3rd Qu.: 14.00 3rd Qu.: 26.00
## Max. :2023-05-17 09:20:48.00 Max. :170.00 Max. :213.00
## NA's :5715 NA's :5371 NA's :5371
En el resumen podemos observar que la variable max_days_late tiene valores negativos, dado que tener un valor negativo representa que se pagó antes de llegar al retraso se modificarán por 0 (No hay existencia de retraso).
Revisamos la cantidad de valores faltantes por columna
colSums(is.na(df))
## customer_id loan_id
## 0 0
## ACC_CREATION_DATETIME APPLICATION_DATETIME
## 0 0
## LOAN_ORIGINATION_DATETIME max_days_late
## 0 0
## target account_to_application_days
## 0 0
## n_sf_apps first_app_date
## 7648 7648
## last_app_date n_bnpl_apps
## 7648 5715
## n_bnpl_approved_apps first_bnpl_app_date
## 5715 5715
## last_bnpl_app_date n_inquiries_l3m
## 5715 5371
## n_inquiries_l6m
## 5371
Tenemos que para:
Valores faltantes: 7,648 representa más del 50% del total.
Interpretación: Estos campos se relacionan con las solicitudes previas en la plataforma “SF”. Los valores faltantes indican que esas personas nunca han realizado una solicitud de crédito previa en esa plataforma. Por tanto:
n_sf_apps = NA significa cero solicitudes previas.
first_app_date y last_app_date son NA porque no hay fechas que registrar.
Se reemplazarán los NA por 0 en n_sf_apps
Valores faltantes: 5,715 casos.
Interpretación: Las personas con valores faltantes no han solicitado ni han sido aprobadas en ningún esquema BNPL anteriormente. Se rremplazarán los NA de n_bnpl_apps y n_bnpl_approved_apps con 0.
Interpretación: Estas variables registran cuántas veces se ha consultado el historial crediticio del cliente en los últimos 3 y 6 meses. El valor faltante probablemente indica que no existen consultas registradas para ese cliente en ese período, lo que puede ser porque:
Es un cliente nuevo (sin historial). Nunca ha solicitado ningún crédito anteriormente. También se reemplazarán con 0 los valores faltantes
# Tratamiento de valores negativos y NA en df
# Valores negativos
df <- df %>%
mutate(max_days_late = ifelse(max_days_late < 0, 0, max_days_late))
# Columnas donde NA significa 'cero actividad/información'
cols_na_0 <- c("n_sf_apps", "n_bnpl_apps", "n_bnpl_approved_apps",
"n_inquiries_l3m", "n_inquiries_l6m")
df <- df %>%
mutate(across(all_of(cols_na_0), ~replace_na(., 0)))
# Codificación
# Convertir enteros a numéricos y target a factor
df <- df %>%
mutate(across(where(is.integer), as.numeric), # Convertir todas las columnas enteras a numéricas
target = as.factor(target)) # Variable objetivo a factor
print(colSums(is.na(df))) # Verificamos que no queden NAs en las columnas relevantes
## customer_id loan_id
## 0 0
## ACC_CREATION_DATETIME APPLICATION_DATETIME
## 0 0
## LOAN_ORIGINATION_DATETIME max_days_late
## 0 0
## target account_to_application_days
## 0 0
## n_sf_apps first_app_date
## 0 7648
## last_app_date n_bnpl_apps
## 7648 0
## n_bnpl_approved_apps first_bnpl_app_date
## 0 5715
## last_bnpl_app_date n_inquiries_l3m
## 5715 0
## n_inquiries_l6m
## 0
Se ha verificado que no exista ningún valor faltante en las columnas necesarias para nuestra modelación
Aquí realizaremos gráficos con su interpretación para las distribuciones, diagrama de correlación, etc.
# Distribución de la variable target
ggplot(df, aes(x = target, fill = target)) +
geom_bar() +
scale_y_continuous(labels = comma) +
labs(title = "Distribución de la Variable Target", x = "Target", y = "Frecuencia")
La variable target es binaria. por lo tanto sus valores solo son 0 y 1,
tenemos casi 4 veces mas personas clasificadas con 0 que con 1, esto
puede hablarnos de un desbalanceo.
# Distribución de días de atraso
ggplot(df, aes(x = max_days_late)) +
geom_histogram(bins = 50, fill = "steelblue", color = "black") +
labs(title = "Distribución de días de atraso", x = "Días de atraso", y = "Frecuencia")
Por lo general nuestros clientes no se atrasan, tenemos mas datos en
primeros días de retraso que en los posteriores, aunque no sigue una
distribución convencional y posiblemente existan valores considerados
atípicos por la cantidad de días que tardaron, sin embargo es
información relevante para nuestros modelos.
# Boxplot de max_days_late por target
ggplot(df, aes(x = target, y = max_days_late, fill = target)) +
geom_boxplot() +
labs(title = "Días de atraso por target", x = "Target", y = "Días de atraso")
Para la clasificación de cero encontramos valores atípicos, estos se encuentran entre las dos categorías, fácilmente son los que podrían generar errores de clasificación a futuro, tambien vemos que la mediana es mucho mas pequeña que la media por lo que existe asimetría fuerte en la categoría 0 (muchos valores en primeros días), a diferencia de la categoría 1 que es casi simétrica.
# Matriz de dispersión y correlación
ggpairs(df %>% dplyr::select(max_days_late, n_sf_apps, n_bnpl_apps, n_inquiries_l6m, target),
mapping = aes(color = target), title = "Matriz de dispersión y correlación")
## `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.
## `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.
## `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.
## `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.
En el pairplot observamos que la variable que mejor separa las categorías es la variable max_days_late, por lo que será la variable mas importante cuando creemos nuestros modelos
Dado que la variable mas importate es la mencionada anteriormente, utilizaremos dicha variable para clasificar a las personas como: - bajo riesgo: si han tenido un retraso máximo de 3 días - medio: si han retrasado entre 4 y 14 días - alto: si se han retrasado 15 días o mas
Creamos la categoría de riesgo sólo para análisis interpretativo
# Creación de la categoría de riesgo
# Esta variable es útil para la parte de tasas fijas, pero no se usará directamente en el modelo predictivo.
df <- df %>%
mutate(
riesgo_cat = case_when( # Renombrado a riesgo_cat para evitar confusión con el score numérico
between(max_days_late, 0, 3) ~ "bajo",
between(max_days_late, 4, 14) ~ "medio",
max_days_late >= 15 ~ "alto"
),
riesgo_cat = factor(riesgo_cat, levels = c("bajo", "medio", "alto"))
)
Visualización de categorías creadas
df %>%
count(riesgo_cat) %>%
ggplot(aes(x = riesgo_cat, y = n, fill = riesgo_cat)) +
geom_col() +
labs(title = "Distribución de Riesgo por Cliente (Categoría Fija)", y = "Clientes", x = "Riesgo") +
theme_minimal()
Según nuestras métricas asignadas arbitrariamente esta sería la
distribución de las categorías de riesgo
En esta sección importaremos los datos de crédito externos, los limpiaremos, codificaremos, crearemos nuevas variables útiles con base en esta base de datos y las agruparemos para agregarlas anuestro main_dataset posteriormente, teniendo en él toda la información por cliente
Importamos los datos de crédito
df2 <- read_parquet("C:/Users/DELL/Downloads/credit_reports.parquet")
print(head(df2))
## # A tibble: 6 × 29
## customer_id INQUIRY_TIME CDC_INQUIRY_ID INQUIRY_DATE
## <int> <dttm> <chr> <dttm>
## 1 4223 2022-04-01 00:32:36 710278-27993a6e-2885-48d4… 2022-03-31 18:00:00
## 2 4223 2022-04-01 00:32:36 710278-27993a6e-2885-48d4… 2022-03-31 18:00:00
## 3 4223 2022-04-01 00:32:36 710278-27993a6e-2885-48d4… 2022-03-31 18:00:00
## 4 3490 2022-02-15 02:30:22 622857-6b4e9d95-7491-40c3… 2022-02-14 18:00:00
## 5 6486 2022-06-25 01:57:14 875073-46a5f149-19db-4193… 2022-06-24 19:00:00
## 6 6486 2022-06-25 01:57:14 875073-46a5f149-19db-4193… 2022-06-24 19:00:00
## # ℹ 25 more variables: PREVENTION_KEY <chr>, CURRENCY <chr>, MAX_CREDIT <dbl>,
## # CREDIT_LIMIT <dbl>, PAYMENT_AMOUNT <dbl>, UPDATE_DATE <dttm>,
## # LOAN_OPENING_DATE <dttm>, LOAN_CLOSING_DATE <dttm>,
## # WORST_DELAY_DATE <dttm>, REPORT_DATE <dttm>, LAST_PURCHASE_DATE <dttm>,
## # LAST_PAYMENT_DATE <dttm>, PAYMENT_FREQUENCY <chr>, BUSINESS_TYPE <chr>,
## # CREDIT_TYPE <chr>, ACCOUNT_TYPE <chr>, RESPONSABILITY_TYPE <chr>,
## # TOTAL_PAYMENTS <dbl>, DELAYED_PAYMENTS <dbl>, CURRENT_PAYMENT <chr>, …
print(summary(df2))
## customer_id INQUIRY_TIME CDC_INQUIRY_ID
## Min. : 1 Min. :2021-04-29 22:50:03.74 Length:287356
## 1st Qu.: 2776 1st Qu.:2022-03-22 21:03:45.14 Class :character
## Median : 6187 Median :2022-07-18 18:09:13.48 Mode :character
## Mean : 6334 Mean :2022-07-02 13:56:59.07
## 3rd Qu.: 9760 3rd Qu.:2022-10-27 00:27:07.93
## Max. :14416 Max. :2023-05-17 15:20:36.89
##
## INQUIRY_DATE PREVENTION_KEY CURRENCY
## Min. :2021-04-28 19:00:00.00 Length:287356 Length:287356
## 1st Qu.:2022-03-21 18:00:00.00 Class :character Class :character
## Median :2022-07-17 19:00:00.00 Mode :character Mode :character
## Mean :2022-07-01 19:44:25.38
## 3rd Qu.:2022-10-26 19:00:00.00
## Max. :2023-05-16 18:00:00.00
## NA's :89
## MAX_CREDIT CREDIT_LIMIT PAYMENT_AMOUNT
## Min. : 0 Min. : 0 Min. : 0
## 1st Qu.: 548 1st Qu.: 0 1st Qu.: 0
## Median : 2300 Median : 400 Median : 0
## Mean : 15280 Mean : 5526 Mean : 1671
## 3rd Qu.: 7483 3rd Qu.: 3000 3rd Qu.: 149
## Max. :404040416 Max. :1900000 Max. :1800000
## NA's :182 NA's :8357 NA's :89
## UPDATE_DATE LOAN_OPENING_DATE
## Min. :2001-04-24 18:00:00 Min. :1949-12-31 18:00:00.00
## 1st Qu.:2019-03-12 18:00:00 1st Qu.:2017-03-20 18:00:00.00
## Median :2021-11-21 18:00:00 Median :2020-04-01 18:00:00.00
## Mean :2020-03-25 22:35:03 Mean :2018-11-22 09:24:26.86
## 3rd Qu.:2022-06-14 19:00:00 3rd Qu.:2021-10-13 19:00:00.00
## Max. :2023-05-06 18:00:00 Max. :2023-03-30 18:00:00.00
## NA's :89 NA's :89
## LOAN_CLOSING_DATE WORST_DELAY_DATE
## Min. :2000-08-27 19:00:00.00 Min. :1998-10-09 19:00:00.00
## 1st Qu.:2017-01-29 18:00:00.00 1st Qu.:2018-04-29 19:00:00.00
## Median :2020-01-26 18:00:00.00 Median :2021-05-30 19:00:00.00
## Mean :2018-10-24 16:45:09.91 Mean :2019-08-10 21:14:42.97
## 3rd Qu.:2021-09-19 19:00:00.00 3rd Qu.:2022-04-19 19:00:00.00
## Max. :2023-03-30 18:00:00.00 Max. :2023-04-11 18:00:00.00
## NA's :94747 NA's :202698
## REPORT_DATE LAST_PURCHASE_DATE
## Min. :2001-04-24 18:00:00.00 Min. :1949-12-31 18:00:00.00
## 1st Qu.:2019-02-27 18:00:00.00 1st Qu.:2017-07-28 19:00:00.00
## Median :2021-11-09 18:00:00.00 Median :2020-08-02 19:00:00.00
## Mean :2020-03-21 23:43:32.96 Mean :2019-02-17 02:26:14.56
## 3rd Qu.:2022-06-11 19:00:00.00 3rd Qu.:2021-11-15 18:00:00.00
## Max. :2023-05-06 18:00:00.00 Max. :2023-04-06 18:00:00.00
## NA's :89 NA's :4142
## LAST_PAYMENT_DATE PAYMENT_FREQUENCY BUSINESS_TYPE
## Min. :1949-12-31 18:00:00.00 Length:287356 Length:287356
## 1st Qu.:2017-09-01 19:00:00.00 Class :character Class :character
## Median :2020-09-05 19:00:00.00 Mode :character Mode :character
## Mean :2019-04-08 19:51:00.57
## 3rd Qu.:2021-12-26 18:00:00.00
## Max. :2023-05-06 18:00:00.00
## NA's :31677
## CREDIT_TYPE ACCOUNT_TYPE RESPONSABILITY_TYPE TOTAL_PAYMENTS
## Length:287356 Length:287356 Length:287356 Min. : 0.00
## Class :character Class :character Class :character 1st Qu.: 1.00
## Mode :character Mode :character Mode :character Median : 3.00
## Mean : 22.77
## 3rd Qu.: 16.00
## Max. :1800.00
## NA's :18645
## DELAYED_PAYMENTS CURRENT_PAYMENT WORST_DELAY TOTAL_REPORTED_PAYMENTS
## Min. : 0.000 Length:287356 Min. : 0.000 Min. :0
## 1st Qu.: 0.000 Class :character 1st Qu.: 0.000 1st Qu.:0
## Median : 0.000 Mode :character Median : 0.000 Median :0
## Mean : 3.818 Mean : 4.279 Mean :0
## 3rd Qu.: 1.000 3rd Qu.: 2.000 3rd Qu.:0
## Max. :96.000 Max. :84.000 Max. :0
## NA's :89 NA's :3210 NA's :41941
## CURRENT_BALANCE BALANCE_DUE BALANCE_DUE_WORST_DELAY
## Min. : 0 Min. : 0 Min. : 0
## 1st Qu.: 0 1st Qu.: 0 1st Qu.: 0
## Median : 0 Median : 0 Median : 0
## Mean : 4578 Mean : 2090 Mean : 1672
## 3rd Qu.: 273 3rd Qu.: 0 3rd Qu.: 159
## Max. :3469743 Max. :1800000 Max. :1800000
## NA's :89 NA's :89 NA's :89
print(colSums(is.na(df2)))
## customer_id INQUIRY_TIME CDC_INQUIRY_ID
## 0 0 89
## INQUIRY_DATE PREVENTION_KEY CURRENCY
## 89 89 89
## MAX_CREDIT CREDIT_LIMIT PAYMENT_AMOUNT
## 182 8357 89
## UPDATE_DATE LOAN_OPENING_DATE LOAN_CLOSING_DATE
## 89 89 94747
## WORST_DELAY_DATE REPORT_DATE LAST_PURCHASE_DATE
## 202698 89 4142
## LAST_PAYMENT_DATE PAYMENT_FREQUENCY BUSINESS_TYPE
## 31677 89 89
## CREDIT_TYPE ACCOUNT_TYPE RESPONSABILITY_TYPE
## 89 89 89
## TOTAL_PAYMENTS DELAYED_PAYMENTS CURRENT_PAYMENT
## 18645 89 89
## WORST_DELAY TOTAL_REPORTED_PAYMENTS CURRENT_BALANCE
## 3210 41941 89
## BALANCE_DUE BALANCE_DUE_WORST_DELAY
## 89 89
Observamos que muchas variables tienen 89 valores faltantes, lo que nos dice que son personas sin crédito registrado, imputaremos los valores faltantes para las columnas numéricas con cero Nos aseguraremos que las columnas de fechas estén guardadas de esa manera
# Eliminar observaciones sin crédito registrado (PAYMENT_FREQUENCY es un buen indicador de registro válido)
df2 <- df2 %>%
filter(!is.na(PAYMENT_FREQUENCY))
# Imputar valores faltantes en columnas numéricas con 0
cols_to_impute_0_df2 <- c("MAX_CREDIT", "CREDIT_LIMIT", "PAYMENT_AMOUNT", "CURRENT_BALANCE",
"BALANCE_DUE", "BALANCE_DUE_WORST_DELAY", "DELAYED_PAYMENTS",
"WORST_DELAY", "TOTAL_PAYMENTS", "TOTAL_REPORTED_PAYMENTS")
df2 <- df2 %>%
mutate(across(all_of(cols_to_impute_0_df2), ~replace_na(., 0)))
# Convertir columnas de fecha
date_cols_df2 <- c("LOAN_OPENING_DATE", "LOAN_CLOSING_DATE", "UPDATE_DATE",
"WORST_DELAY_DATE", "REPORT_DATE", "LAST_PURCHASE_DATE",
"LAST_PAYMENT_DATE")
df2 <- df2 %>%
mutate(across(all_of(date_cols_df2), ~as_date(.)))
print(colSums(is.na(df2)))
## customer_id INQUIRY_TIME CDC_INQUIRY_ID
## 0 0 0
## INQUIRY_DATE PREVENTION_KEY CURRENCY
## 0 0 0
## MAX_CREDIT CREDIT_LIMIT PAYMENT_AMOUNT
## 0 0 0
## UPDATE_DATE LOAN_OPENING_DATE LOAN_CLOSING_DATE
## 0 0 94658
## WORST_DELAY_DATE REPORT_DATE LAST_PURCHASE_DATE
## 202609 0 4053
## LAST_PAYMENT_DATE PAYMENT_FREQUENCY BUSINESS_TYPE
## 31588 0 0
## CREDIT_TYPE ACCOUNT_TYPE RESPONSABILITY_TYPE
## 0 0 0
## TOTAL_PAYMENTS DELAYED_PAYMENTS CURRENT_PAYMENT
## 0 0 0
## WORST_DELAY TOTAL_REPORTED_PAYMENTS CURRENT_BALANCE
## 0 0 0
## BALANCE_DUE BALANCE_DUE_WORST_DELAY
## 0 0
Únicamente nos quedan valores faltantes en las columnas de fecha, cada una con diferente cantidad de valores pero no podemos borrar las filas con esas ausencias, ya que perderíamos mucha información relevante
En esta sección crearemos nuevas variables útiles para las características de riesgo externo, en caso de crear algún NA reemplazará por cero
# Ingeniería de Variables en df2
df2 <- df2 %>%
mutate(
# Tiempo de vida del crédito en meses
tiempo_credito_meses = as.numeric(interval(LOAN_OPENING_DATE, UPDATE_DATE), "months"),
# Uso de crédito: Saldo actual / Límite de crédito. Manejar división por cero.
uso_credito_pct = if_else(CREDIT_LIMIT > 0, CURRENT_BALANCE / CREDIT_LIMIT, 0),
# Porcentaje de pagos atrasados: Pagos atrasados / Total de pagos.
pagos_tarde_pct = if_else(TOTAL_PAYMENTS > 0, DELAYED_PAYMENTS / TOTAL_PAYMENTS, 0)
) %>%
# Limitar uso_credito_pct a 1 (no puede ser más del 100%)
mutate(uso_credito_pct = pmin(uso_credito_pct, 1))
# Reemplazamos posibles NAs
df2 <- df2 %>%
mutate(across(c(tiempo_credito_meses, uso_credito_pct, pagos_tarde_pct), ~replace_na(., 0)))
NOTA CLAVE DEL PROBLEMA: La relación es de uno a muchos (un customer_id en main_dataset puede tener múltiples registros en credit_reports). Para usar esta información en el modelo del main_dataset (que es por loan_id, y cada loan_id tiene un customer_id), necesitamos resumir el historial de credit_reports por customer_id.
df2_aggregated <- df2 %>%
group_by(customer_id) %>%
summarise(
# Métricas de uso de crédito
mean_uso_credito_ext = mean(uso_credito_pct, na.rm = TRUE),
max_uso_credito_ext = max(uso_credito_pct, na.rm = TRUE),
# Métricas de atraso
max_worst_delay_ext = max(WORST_DELAY, na.rm = TRUE), # Peor atraso en cualquier crédito externo
mean_delayed_payments_pct_ext = mean(pagos_tarde_pct, na.rm = TRUE),
# Cantidad y tipo de créditos
n_external_credits = n(), # Número total de créditos externos reportados
n_distinct_credit_types = n_distinct(CREDIT_TYPE),
# Historial de crédito
mean_tiempo_credito_meses_ext = mean(tiempo_credito_meses, na.rm = TRUE),
max_total_payments_ext = max(TOTAL_PAYMENTS, na.rm = TRUE),
) %>%
ungroup()
# Verificamos que no haya ningún NA en la agrupación que creamos
colSums(is.na(df2_aggregated))
## customer_id mean_uso_credito_ext
## 0 0
## max_uso_credito_ext max_worst_delay_ext
## 0 0
## mean_delayed_payments_pct_ext n_external_credits
## 0 0
## n_distinct_credit_types mean_tiempo_credito_meses_ext
## 0 0
## max_total_payments_ext
## 0
# Resumen
summary(df2_aggregated)
## customer_id mean_uso_credito_ext max_uso_credito_ext max_worst_delay_ext
## Min. : 1 Min. :0.00000 Min. :0.0000 Min. : 0.00
## 1st Qu.: 2698 1st Qu.:0.09056 1st Qu.:1.0000 1st Qu.:13.00
## Median : 5694 Median :0.17677 Median :1.0000 Median :34.00
## Mean : 6114 Mean :0.21368 Mean :0.8974 Mean :42.02
## 3rd Qu.: 9429 3rd Qu.:0.29917 3rd Qu.:1.0000 3rd Qu.:84.00
## Max. :14416 Max. :1.00000 Max. :1.0000 Max. :84.00
## mean_delayed_payments_pct_ext n_external_credits n_distinct_credit_types
## Min. : 0.00000 Min. : 1.00 Min. : 1.000
## 1st Qu.: 0.09635 1st Qu.: 11.00 1st Qu.: 4.000
## Median : 0.43310 Median : 22.00 Median : 5.000
## Mean : 0.89468 Mean : 31.32 Mean : 5.184
## 3rd Qu.: 1.18198 3rd Qu.: 41.00 3rd Qu.: 7.000
## Max. :25.00000 Max. :269.00 Max. :14.000
## mean_tiempo_credito_meses_ext max_total_payments_ext
## Min. : 0.4415 Min. : 0.0
## 1st Qu.: 10.8016 1st Qu.: 33.0
## Median : 16.8158 Median : 96.0
## Mean : 20.5255 Mean : 234.1
## 3rd Qu.: 25.6553 3rd Qu.: 360.0
## Max. :183.0418 Max. :1800.0
En el summary podemos ver que tenemos 14416 clientes, el promedio de uso de crédito externo es de 21%, el promedio de máximo uso de crédito externo es de casi 90%, la mayor parte de los clientes se atrasan en los primeros días, etc.
En esta sección se unirá la información de las bases de datos y las variables creadas por cliente según su id, crearemos unos modelos de aprendizaje automático, finalmente compararemos las métricas por modelo.
# Unir por costumer_id
df_final_model <- left_join(df, df2_aggregated, by = "customer_id")
colSums(is.na(df_final_model))
## customer_id loan_id
## 0 0
## ACC_CREATION_DATETIME APPLICATION_DATETIME
## 0 0
## LOAN_ORIGINATION_DATETIME max_days_late
## 0 0
## target account_to_application_days
## 0 0
## n_sf_apps first_app_date
## 0 7648
## last_app_date n_bnpl_apps
## 7648 0
## n_bnpl_approved_apps first_bnpl_app_date
## 0 5715
## last_bnpl_app_date n_inquiries_l3m
## 5715 0
## n_inquiries_l6m riesgo_cat
## 0 0
## mean_uso_credito_ext max_uso_credito_ext
## 5282 5282
## max_worst_delay_ext mean_delayed_payments_pct_ext
## 5282 5282
## n_external_credits n_distinct_credit_types
## 5282 5282
## mean_tiempo_credito_meses_ext max_total_payments_ext
## 5282 5282
Los NA significa que el cliente con ese ID no tiene datos externos, posibliemente no ha solicitado ningún crédito fuera de este negocio. Los NA serán reemplazados por cero en las variables que provienen de la base de datos de crédito
# Reemplazamos NAs
df_final_model <- df_final_model %>%
mutate(across(c(mean_uso_credito_ext, max_uso_credito_ext,
max_worst_delay_ext, mean_delayed_payments_pct_ext,
n_external_credits, n_distinct_credit_types,
mean_tiempo_credito_meses_ext, max_total_payments_ext),
~replace_na(., 0)))
Selección de variables para nuestro modelo de aprendizaje automático, omitiremos las variables de fecha de nuestro main_dataset
# Incluimos las variables numéricas y la variable objetivo de nuetro nuevo dataset
vars_usar_final <- c("target", "max_days_late", "n_sf_apps", "n_bnpl_apps",
"n_bnpl_approved_apps", "n_inquiries_l3m", "n_inquiries_l6m",
"account_to_application_days",
# Nuevas variables de credit_reports agregadas
"mean_uso_credito_ext", "max_uso_credito_ext",
"max_worst_delay_ext", "mean_delayed_payments_pct_ext",
"n_external_credits", "n_distinct_credit_types",
"mean_tiempo_credito_meses_ext", "max_total_payments_ext"
)
df_model_final <- df_final_model %>% dplyr::select(all_of(vars_usar_final))
df_model_final <- df_model_final %>%
mutate(across(where(is.integer), as.numeric))
summary(df_model_final)
## target max_days_late n_sf_apps n_bnpl_apps
## 0:11754 Min. : 0.00 Min. : 0.0000 Min. : 0.0000
## 1: 2700 1st Qu.: 0.00 1st Qu.: 0.0000 1st Qu.: 0.0000
## Median : 2.00 Median : 0.0000 Median : 1.0000
## Mean :14.28 Mean : 0.7788 Mean : 0.7387
## 3rd Qu.:20.00 3rd Qu.: 1.0000 3rd Qu.: 1.0000
## Max. :70.00 Max. :42.0000 Max. :18.0000
## n_bnpl_approved_apps n_inquiries_l3m n_inquiries_l6m
## Min. : 0.0000 Min. : 0.000 Min. : 0.00
## 1st Qu.: 0.0000 1st Qu.: 0.000 1st Qu.: 0.00
## Median : 0.0000 Median : 0.000 Median : 0.00
## Mean : 0.1602 Mean : 6.504 Mean : 10.76
## 3rd Qu.: 0.0000 3rd Qu.: 0.000 3rd Qu.: 15.00
## Max. :15.0000 Max. :170.000 Max. :213.00
## account_to_application_days mean_uso_credito_ext max_uso_credito_ext
## Min. : 0.0 Min. :0.0000 Min. :0.0000
## 1st Qu.: 0.0 1st Qu.:0.0000 1st Qu.:0.0000
## Median :103.0 Median :0.0771 Median :1.0000
## Mean :163.5 Mean :0.1356 Mean :0.5694
## 3rd Qu.:271.8 3rd Qu.:0.2207 3rd Qu.:1.0000
## Max. :901.0 Max. :1.0000 Max. :1.0000
## max_worst_delay_ext mean_delayed_payments_pct_ext n_external_credits
## Min. : 0.00 Min. : 0.00000 Min. : 0.00
## 1st Qu.: 0.00 1st Qu.: 0.00000 1st Qu.: 0.00
## Median :13.00 Median : 0.06919 Median : 9.00
## Mean :26.66 Mean : 0.56773 Mean : 19.87
## 3rd Qu.:53.00 3rd Qu.: 0.68072 3rd Qu.: 28.00
## Max. :84.00 Max. :25.00000 Max. :269.00
## n_distinct_credit_types mean_tiempo_credito_meses_ext max_total_payments_ext
## Min. : 0.00 Min. : 0.000 Min. : 0.0
## 1st Qu.: 0.00 1st Qu.: 0.000 1st Qu.: 0.0
## Median : 4.00 Median : 9.963 Median : 26.0
## Mean : 3.29 Mean : 13.025 Mean : 148.5
## 3rd Qu.: 6.00 3rd Qu.: 20.011 3rd Qu.: 102.0
## Max. :14.00 Max. :183.042 Max. :1800.0
Del summary anterior llama la atención la proporción de datos en target, cero tiene una proporción mucho mayor, de nuevo hablamos de un desbalance para la costrucción del modelo de aprendizaje automático
Realizamos un diagrama de correlación para evitar tener multicolinealidad perfecta, que las variables aporten lo mismo y/o afecten a nuestro modelo
# Correlación de las variables del modelo final
cor_matrix_final <- cor(dplyr::select(df_model_final, where(is.numeric)), use = "pairwise.complete.obs")
corrplot(cor_matrix_final, method = "color", type = "upper", order = "hclust",
addCoef.col = "black", tl.col = "black", tl.srt = 45, number.cex = 0.7)
Las únicas variables correlacionadas fuertemente son:
max_uso_crédito_ext y n_distinct_credit_types Sin embargo no es tan alto
como para preocuparnos, se mantendrán todas las variables.
Realizamos la partición en datos de entrenamiento y prueba, utilizando el 80% de los datos para entrenar el modelo
# Partición en datos de entrenamiento y prueba
set.seed(42)
part <- createDataPartition(df_model_final$target, p = 0.8, list = FALSE)
train <- df_model_final[part, ]
test <- df_model_final[-part, ]
table(train$target)
##
## 0 1
## 9404 2160
Como tenemos muchos mas datos en la clase 0 realizaremos un balanceo en nuestros datos de entrenamiento
# Balanceo con ROSE
train_bal <- ROSE(target ~ ., data = train, seed = 42)$data
# Verificamos el balanceo
table(train_bal$target)
##
## 0 1
## 5761 5803
Creamos una función para los resultados de las métricas de nuestros modelos y poderlos comparar posteriormente con una tabla
# Función para las métricas
eval_metrics <- function(pred, obs, modelo) {
pred <- factor(pred, levels = c("0", "1"))
obs <- factor(obs, levels = c("0", "1"))
cm <- confusionMatrix(pred, obs, positive = "1")
f1 <- F1_Score(pred, obs, positive = "1")
prec <- cm$byClass["Precision"]
rec <- cm$byClass["Sensitivity"]
acc <- cm$overall["Accuracy"]
kappa <- cm$overall["Kappa"]
data.frame(Modelo = modelo, Accuracy = acc, Kappa = kappa,
F1 = f1, Precision = prec, Recall = rec)
}
resultados <- list()
modelo_glm <- glm(target ~ ., data = train_bal, family = "binomial")
pred_prob_glm <- predict(modelo_glm, newdata = test, type = "response")
pred_glm_class <- as.factor(ifelse(pred_prob_glm > 0.7, 1, 0)) # Clasificación binaria con umbral 0.7
res_glm <- eval_metrics(pred_glm_class, test$target, "Regresión Logística")
print(confusionMatrix(pred_glm_class, test$target, positive = "1"))
## Confusion Matrix and Statistics
##
## Reference
## Prediction 0 1
## 0 2329 0
## 1 21 540
##
## Accuracy : 0.9927
## 95% CI : (0.9889, 0.9955)
## No Information Rate : 0.8131
## P-Value [Acc > NIR] : < 2.2e-16
##
## Kappa : 0.9764
##
## Mcnemar's Test P-Value : 1.275e-05
##
## Sensitivity : 1.0000
## Specificity : 0.9911
## Pos Pred Value : 0.9626
## Neg Pred Value : 1.0000
## Prevalence : 0.1869
## Detection Rate : 0.1869
## Detection Prevalence : 0.1941
## Balanced Accuracy : 0.9955
##
## 'Positive' Class : 1
##
resultados[["Regresión Logística"]] <- res_glm
# Usamos rpart para árboles de clasificación
modelo_dt <- rpart(target ~ ., data = train_bal, method = "class",
control = rpart.control(minsplit = 20, cp = 0.01)) # minsplit y cp para controlar complejidad
pred_dt <- predict(modelo_dt, newdata = test, type = "class")
res_dt <- eval_metrics(pred_dt, test$target, "Árbol de Decisión")
print(confusionMatrix(pred_dt, test$target, positive = "1"))
## Confusion Matrix and Statistics
##
## Reference
## Prediction 0 1
## 0 2325 0
## 1 25 540
##
## Accuracy : 0.9913
## 95% CI : (0.9873, 0.9944)
## No Information Rate : 0.8131
## P-Value [Acc > NIR] : < 2.2e-16
##
## Kappa : 0.972
##
## Mcnemar's Test P-Value : 1.587e-06
##
## Sensitivity : 1.0000
## Specificity : 0.9894
## Pos Pred Value : 0.9558
## Neg Pred Value : 1.0000
## Prevalence : 0.1869
## Detection Rate : 0.1869
## Detection Prevalence : 0.1955
## Balanced Accuracy : 0.9947
##
## 'Positive' Class : 1
##
resultados[["Árbol de Decisión"]] <- res_dt
# Visualización del Árbol de Decisión (opcional, puede ser grande)
rpart.plot(modelo_dt, type = 4, extra = 101, fallen.leaves = TRUE, cex = 0.7,
main = "Árbol de Decisión para Predicción de Riesgo")
Con el árbol de decisión verificamos que la variable más importante es
mas_day_late
train_x <- as.matrix(dplyr::select(train_bal, -target))
train_y <- as.numeric(train_bal$target) - 1 # 0 y 1 para XGBoost
dtrain <- xgb.DMatrix(data = train_x, label = train_y)
# Parámetros optimizados para XGBoost (puedes ajustar más si es necesario)
params <- list(objective = "binary:logistic",
eval_metric = "auc",
eta = 0.1, # Tasa de aprendizaje
max_depth = 4, # Profundidad máxima del árbol
subsample = 0.8, # Submuestreo de filas
colsample_bytree = 0.8 # Submuestreo de columnas
)
xgb_model <- xgb.train(params = params, dtrain, nrounds = 150, verbose = 0) # Aumentar nrounds para mejor rendimiento
# Predicciones de probabilidad en el conjunto de prueba
test_x <- as.matrix(dplyr::select(test, -target))
pred_prob_xgb <- predict(xgb_model, test_x)
pred_xgb_class <- as.factor(ifelse(pred_prob_xgb > 0.5, 1, 0)) # Clasificación binaria con umbral 0.5
res_xgb <- eval_metrics(pred_xgb_class, test$target, "XGBoost")
print(confusionMatrix(pred_xgb_class, test$target, positive = "1"))
## Confusion Matrix and Statistics
##
## Reference
## Prediction 0 1
## 0 2318 0
## 1 32 540
##
## Accuracy : 0.9889
## 95% CI : (0.9844, 0.9924)
## No Information Rate : 0.8131
## P-Value [Acc > NIR] : < 2.2e-16
##
## Kappa : 0.9644
##
## Mcnemar's Test P-Value : 4.251e-08
##
## Sensitivity : 1.0000
## Specificity : 0.9864
## Pos Pred Value : 0.9441
## Neg Pred Value : 1.0000
## Prevalence : 0.1869
## Detection Rate : 0.1869
## Detection Prevalence : 0.1979
## Balanced Accuracy : 0.9932
##
## 'Positive' Class : 1
##
resultados[["XGBoost"]] <- res_xgb
set.seed(42)
modelo_rf <- randomForest(target ~ ., data = train_bal, ntree = 100, maxnodes = 80)
pred_rf <- predict(modelo_rf, newdata = test)
res_rf <- eval_metrics(pred_rf, test$target, "Random Forest")
print(confusionMatrix(pred_rf, test$target, positive = "1"))
## Confusion Matrix and Statistics
##
## Reference
## Prediction 0 1
## 0 2310 0
## 1 40 540
##
## Accuracy : 0.9862
## 95% CI : (0.9812, 0.9901)
## No Information Rate : 0.8131
## P-Value [Acc > NIR] : < 2.2e-16
##
## Kappa : 0.9557
##
## Mcnemar's Test P-Value : 6.984e-10
##
## Sensitivity : 1.0000
## Specificity : 0.9830
## Pos Pred Value : 0.9310
## Neg Pred Value : 1.0000
## Prevalence : 0.1869
## Detection Rate : 0.1869
## Detection Prevalence : 0.2007
## Balanced Accuracy : 0.9915
##
## 'Positive' Class : 1
##
resultados[["Random Forest"]] <- res_rf
Error OOB para bosque aleatorio
# Tabla error OOB
oob_error <- modelo_rf$err.rate[modelo_rf$ntree, "OOB"]
cat("Error OOB final:", round(oob_error * 100, 2), "%\n")
## Error OOB final: 2.53 %
plot(modelo_rf,
main = "Error OOB vs Número de Árboles",
col = c("black", "red"))
legend("topright",
legend = colnames(modelo_rf$err.rate),
col = c("black", "red"),
lty = 1)
oob_error <- modelo_rf$err.rate[modelo_rf$ntree, "OOB"]
cat("Error OOB final:", round(oob_error * 100, 2), "%\n")
## Error OOB final: 2.53 %
# F1 vs Número de árboles
n_arboles <- seq(10, 120, by = 10)
f1_scores <- sapply(n_arboles, function(n) {
m <- randomForest(target ~ ., data = train_bal, ntree = n)
pred <- predict(m, newdata = test)
F1_Score(pred, test$target, positive = "1")
})
plot(n_arboles, f1_scores, type = "b", pch = 19, col = "steelblue",
main = "F1 Score vs Número de Árboles", xlab = "ntree", ylab = "F1 Score")
# F1 vs Número de nodos
max_nodes <- seq(5, 100, by = 10)
f1_nodes <- sapply(max_nodes, function(depth) {
m <- randomForest(target ~ ., data = train_bal, ntree = 90, maxnodes = depth)
pred <- predict(m, newdata = test)
F1_Score(pred, test$target, positive = "1")
})
plot(max_nodes, f1_nodes, type = "b", pch = 19, col = "tomato",
main = "F1 Score vs Nodos Máximos", xlab = "Max Nodes", ylab = "F1 Score")
## Red Neuronal
set.seed(42)
nn_model <- nnet(target ~ ., data = train_bal, size = 5, decay = 0.01, maxit = 100, trace = FALSE)
pred_nn <- predict(nn_model, test, type = "class")
pred_nn <- factor(pred_nn, levels = c("0", "1"))
res_nn <- eval_metrics(pred_nn, test$target, "Neural Net")
print(confusionMatrix(pred_nn, test$target, positive = "1"))
## Confusion Matrix and Statistics
##
## Reference
## Prediction 0 1
## 0 2300 0
## 1 50 540
##
## Accuracy : 0.9827
## 95% CI : (0.9773, 0.9871)
## No Information Rate : 0.8131
## P-Value [Acc > NIR] : < 2.2e-16
##
## Kappa : 0.945
##
## Mcnemar's Test P-Value : 4.219e-12
##
## Sensitivity : 1.0000
## Specificity : 0.9787
## Pos Pred Value : 0.9153
## Neg Pred Value : 1.0000
## Prevalence : 0.1869
## Detection Rate : 0.1869
## Detection Prevalence : 0.2042
## Balanced Accuracy : 0.9894
##
## 'Positive' Class : 1
##
resultados[["NeuralNet"]] <- res_nn
resumen <- do.call(rbind, resultados)
DT::datatable(resumen,
caption = "Tabla Comparativa de Modelos (con características de external credit)",
options = list(pageLength = 10, # Mostrar 10 filas por página
dom = 'tip'))
Todos los modelos tienen excelentes métricas, acertando el 100% de las predicciones para cero y fallando en un porcentaje mínimo para 1
Calcularemos el score de riesgo y una tasa de interés dinámica (por persona), donde las personas de mayor riesgo tendrán una tasa de interés superior. Utilizaremos el modelo XGBoost
Comenzamos calculando el score de riesgo bajo score = bajo riesgo alto score = alto riesgo
# El riesgo_score será la probabilidad de incumplimiento (target=1)
features_for_prediction <- xgb_model$feature_names
# Filtrar df_final_model
data_for_prediction <- as.matrix(df_final_model %>% dplyr::select(all_of(features_for_prediction)))
probabilidades_final_score <- predict(xgb_model, newdata = xgb.DMatrix(data = data_for_prediction))
df_final_model$riesgo_score_final <- round(probabilidades_final_score, 4)
head(df_final_model)
## # A tibble: 6 × 27
## customer_id loan_id ACC_CREATION_DATETIME APPLICATION_DATETIME
## <dbl> <dbl> <dttm> <dttm>
## 1 1223 1 2021-08-23 08:57:56 2022-04-26 02:00:00
## 2 5190 2 2022-04-26 04:57:25 2022-04-26 02:00:00
## 3 5194 3 2022-04-26 07:22:35 2022-04-26 02:00:00
## 4 3978 4 2022-03-09 05:26:55 2022-04-26 02:00:00
## 5 4535 5 2022-04-01 08:28:42 2022-04-26 02:00:00
## 6 3604 6 2022-02-21 05:55:32 2022-05-05 02:00:00
## # ℹ 23 more variables: LOAN_ORIGINATION_DATETIME <dttm>, max_days_late <dbl>,
## # target <fct>, account_to_application_days <dbl>, n_sf_apps <dbl>,
## # first_app_date <dttm>, last_app_date <dttm>, n_bnpl_apps <dbl>,
## # n_bnpl_approved_apps <dbl>, first_bnpl_app_date <dttm>,
## # last_bnpl_app_date <dttm>, n_inquiries_l3m <dbl>, n_inquiries_l6m <dbl>,
## # riesgo_cat <fct>, mean_uso_credito_ext <dbl>, max_uso_credito_ext <dbl>,
## # max_worst_delay_ext <dbl>, mean_delayed_payments_pct_ext <dbl>, …
Seleccionamos los límites de las tasas de interés
Mínimo 10% = Para bajo riesgo Máximo 50% = Para alto riesgo
# Tasa de interés dinámica
tasa_base <- 0.10 # Mínimo 10%
tasa_max <- 0.50 # Máximo 50%
# Cálculo dinámico de la tasa individual basado en el riesgo_score_final (probabilidad de incumplimiento)
df_final_model <- df_final_model %>%
mutate(tasa_interes_dinamica = tasa_base + riesgo_score_final * (tasa_max - tasa_base))
Graficamos
# Visualización: tasa vs probabilidad de riesgo
ggplot(df_final_model, aes(x = riesgo_score_final, y = tasa_interes_dinamica)) +
geom_point(alpha = 0.3, color = "darkblue") +
geom_smooth(method = "lm", se = FALSE, color = "red") +
labs(title = "Tasa de interés dinámica según probabilidad de incumplimiento (Modelo Mejorado)",
x = "Probabilidad estimada de incumplimiento (Riesgo Score Final)", y = "Tasa de interés asignada") +
scale_y_continuous(labels = percent) +
scale_x_continuous(labels = percent) +
theme_minimal()
## `geom_smooth()` using formula = 'y ~ x'
Podemos como se va asignando una tasa de interés de forma lineal
dependiendo del score de riesgo
Creamos una tabla final con los ID correspondientes, el riesgo calculado con XGBoost y la tasa de interés dinámica asignada
# Tabla final
tabla <- df_final_model %>%
dplyr::select(customer_id, loan_id, riesgo_score_final, tasa_interes_dinamica) %>%
distinct() # Asegura que no haya duplicados de customer_id-loan_id
# Renombrar riesgo_score_final a riesgo_score como lo pide el entregable
tabla <- tabla %>%
rename(riesgo_score = riesgo_score_final)
# Mostramos un ejemplo de tabla con las primeras 100 observaciones que otorga esa semilla
set.seed(42)
DT::datatable(tabla %>%
head(100) %>%
mutate(across(c(riesgo_score, tasa_interes_dinamica), ~ round(.x, 3))),
caption = "Ejemplos de Riesgo Score y Tasa de Interés Dinámica por Préstamo (Árbol de Decisión)",
options = list(pageLength = 10,
dom = 'tip'))
En los modelos de aprendizaje automático se ve un error en la clasificación de las variables de tipo 1, mientras que para 0 prácticamente todos tienen el 100% de efectividad. Seleccionamos el modelo XGBoost para continuar con los cálculos y asignación de tasas de interés
Este modelo proporciona a Bankaya una herramienta analítica poderosa para automatizar y mejorar las decisiones crediticias. La capacidad de discernir el riesgo con mayor precisión se traducirá en:
-Reducción de Morosidad: Al identificar mejor a los clientes riesgosos, se pueden rechazar solicitudes o asignar tasas que compensen el riesgo. -Expansión Cautelosa del Mercado: Permite aprobar clientes con un riesgo moderado a una tasa adecuada, ampliando la base de clientes de forma controlada. -Mejora de la Experiencia del Cliente: Los clientes de bajo riesgo se benefician de tasas más bajas.