library(tidyverse)
library(scales)
ruta_general <- "C:/Users/Sara María Martinez/Downloads/files"
ruta_archivos <- list.files(
ruta_general,
pattern = "\\.csv$",
full.names = TRUE
)
bases <- lapply(ruta_archivos, read.csv)
archivos <- c("clientes" , "pagos", "prestamos", "provisiones")
names(bases) <- archivos
(Al día / 1-30 / 31-60 / 61-90 / 91-180 días)
bucket_cartera <- bases[["pagos"]] %>% mutate(
bucket = case_when(
dias_retraso == 0 ~ "Al día",
dias_retraso >= 1 & dias_retraso <= 30 ~ "1-30 días",
dias_retraso >= 31 & dias_retraso <= 60 ~ "31-60 días",
dias_retraso >= 61 & dias_retraso <= 90 ~ "61-90 días",
dias_retraso >= 91 & dias_retraso <= 180 ~ "91-180 días",
TRUE ~ "No definido"
),
bucket = factor (bucket, levels = c("Al día", "1-30 días", "31-60 días", "61-90 días", "91-180 días"))) %>%
group_by(bucket) %>%
summarise(
cantidad_cuotas = n(),
monto_total_usd = sum(monto_cuota_usd, na.rm = TRUE),
.groups = "drop" ) %>%
mutate(
monto_total_usd = round(monto_total_usd / 1e3, 2),
pct_cantidad = round(cantidad_cuotas / sum(cantidad_cuotas) * 100, 2),
pct_monto = round(monto_total_usd / sum(monto_total_usd) * 100, 2)
)
bucket_totales <- bucket_cartera %>%
summarise(
across(where(is.numeric), sum, na.rm = TRUE)
)
bucket_cartera_totales <- bind_rows(bucket_cartera, bucket_totales) %>%
mutate(
bucket = if_else(is.na(bucket), "Totales", bucket),
cantidad_cuotas = comma(cantidad_cuotas),
monto_total_usd = dollar(monto_total_usd, prefix = "$")
)
bucket_cartera_totales
## # A tibble: 6 × 5
## bucket cantidad_cuotas monto_total_usd pct_cantidad pct_monto
## <chr> <chr> <chr> <dbl> <dbl>
## 1 Al día 1,764 $4,454.00 58.8 58.9
## 2 1-30 días 625 $1,667.88 20.8 22.1
## 3 31-60 días 196 $455.48 6.53 6.03
## 4 61-90 días 168 $391.37 5.6 5.18
## 5 91-180 días 247 $588.18 8.23 7.78
## 6 Totales 3,000 $7,556.91 100.0 100
colores_bucket <- c(
"Al día" = "#2E7D32",
"1-30 días" = "#F9A825",
"31-60 días" = "#EF6C00",
"61-90 días" = "#C62828",
"91-180 días" = "#6A1B9A"
)
ggplot(bucket_cartera, aes(x = bucket, y = monto_total_usd, fill = bucket)) +
geom_bar(stat = "identity", color = "white", width = 0.7) +
geom_text(
aes(label = paste0(pct_monto, "%\n(", comma(round(monto_total_usd),2), ")")),
vjust = -0.4, size = 3.2, fontface = "bold"
) +
scale_fill_manual(values = colores_bucket) +
scale_y_continuous(
labels = comma,
expand = expansion(mult = c(0, 0.18))
) +
labs(
title = "Cartera en Atraso por Bucket de Mora",
subtitle = "Distribución de cuotas por rango de días de retraso",
x = "Bucket",
y = "Monto total en miles de dólares"
) +
theme_minimal() +
theme(
plot.title = element_text(face = "bold", hjust = 0.5),
plot.subtitle = element_text(hjust = 0.5, color = "gray25"),
legend.position = "none",
panel.grid.major.x = element_blank()
)
El 58.94% de las cuotas se encuentran al día,lo que indica que
la mayoría de la cartera mantiene un comportamiento saludable y
estable.
El 41.06% restante presenta algún nivel de atraso de 1 a 180 días, siendo el bucket de 1-30 días el más representativo con un 20.07% del total, el 7.78% de cuotas atrasadas estan concentradas en el rango de mora de 91-180 días aumentando la cartera suceptible a pertenecer a la presa de castigo.
pagos_region <- bases[["pagos"]] %>%
left_join(bases[["clientes"]] %>% select(cliente_id, region),
by = "cliente_id") %>%
filter(!is.na(region))
morosidad_region <- pagos_region %>%
mutate(es_mora = if_else(dias_retraso > 0, 1, 0)) %>%
group_by(region) %>%
summarise(
total_cuotas = n(),
cuotas_mora = sum(es_mora, na.rm = TRUE),
tasa_morosidad = round(cuotas_mora / total_cuotas * 100, 2),
.groups = "drop"
) %>%
arrange(desc(tasa_morosidad))
morosidad_region
## # A tibble: 5 × 4
## region total_cuotas cuotas_mora tasa_morosidad
## <chr> <int> <dbl> <dbl>
## 1 Sur 590 267 45.2
## 2 Centro 761 321 42.2
## 3 Norte 554 221 39.9
## 4 Oriente 628 246 39.2
## 5 Occidente 467 181 38.8
ggplot(morosidad_region,
aes(x = reorder(region, tasa_morosidad),
y = tasa_morosidad,
fill = tasa_morosidad)) +
geom_bar(stat = "identity", width = 0.65, color = "white") +
geom_text(
aes(label = paste0(tasa_morosidad, "%")),
hjust = -0.2, size = 3.5, fontface = "bold"
) +
scale_fill_gradient(low = "#f5d376", high = "#B71C1C") +
scale_y_continuous(
labels = function(x) paste0(x, "%"),
expand = expansion(mult = c(0, 0.18))
) +
coord_flip() +
labs(
title = "Tasa de Morosidad por Región Geográfica",
subtitle = "Porcentaje de cuotas con días de retraso > 0",
x = "Region",
y = "Tasa de morosidad (%)"
) +
theme_minimal(base_size = 12) +
theme(
plot.title = element_text(face = "bold", hjust = 0.5),
plot.subtitle = element_text(hjust = 0.5, color = "gray25"),
legend.position = "none",
panel.grid.major.y = element_blank()
)
La región Sur presenta la tasa de morosidad más alta con un 45.25%, seguida de la región Centro con 42.18%. La región Occidente registra la menor tasa con 38.76%.
Se recomienda realizar un análisis del perfil de los clientes en la región Sur y Centro para identificar factores macroeconomicos que expliquen su nivel de atraso comparado con otras regiones y ajustar las políticas de otorgamiento de ser necesario pues su tasa de morosidad esta por superar el 50%.
#Validación de nulos en calificación de riesgo
bases[["prestamos"]] %>% filter(is.na(calificacion_riesgo))
## [1] prestamo_id cliente_id tipo_prestamo
## [4] monto_original_usd saldo_capital_usd tasa_interes_anual_pct
## [7] plazo_meses fecha_desembolso fecha_vencimiento
## [10] estado_prestamo calificacion_riesgo garantia
## [13] valor_garantia_usd
## <0 rows> (o 0- extensión row.names)
cartera_calificacion <- bases[["prestamos"]] %>%
filter(!is.na(calificacion_riesgo)) %>%
mutate(
calificacion_riesgo = factor(calificacion_riesgo, levels = c("A", "B", "C", "D", "E"))) %>%
group_by(calificacion_riesgo) %>%
summarise(
cantidad_prestamos = n(),
saldo_total_usd = sum(saldo_capital_usd, na.rm = TRUE),
.groups = "drop"
) %>%
mutate(
pct_cantidad = round(cantidad_prestamos / sum(cantidad_prestamos) * 100, 1),
pct_saldo = round(saldo_total_usd / sum(saldo_total_usd) * 100, 1)
)
cartera_calif_totales <- cartera_calificacion %>%
summarise(
across(where(is.numeric), sum, na.rm = TRUE)
)
cartera_calificacion_format <- bind_rows(cartera_calificacion, cartera_calif_totales) %>%
mutate(
saldo_total_usd = dollar(saldo_total_usd, prefix = "$"),
calificacion_riesgo = if_else(is.na(calificacion_riesgo), "Totales", calificacion_riesgo)
)
cartera_calificacion_format
## # A tibble: 6 × 5
## calificacion_riesgo cantidad_prestamos saldo_total_usd pct_cantidad pct_saldo
## <chr> <int> <chr> <dbl> <dbl>
## 1 A 292 $4,639,762 36.5 35.8
## 2 B 232 $3,489,351 29 26.9
## 3 C 158 $2,649,573 19.8 20.5
## 4 D 67 $1,326,118 8.4 10.2
## 5 E 51 $846,599 6.4 6.5
## 6 Totales 800 $12,951,404 100. 99.9
colores_cal <- c(
"A" = "#2E7D32",
"B" = "#66BB6A",
"C" = "#F9A825",
"D" = "#EF6C00",
"E" = "#C62828"
)
ggplot(cartera_calificacion,
aes(x = calificacion_riesgo, y = saldo_total_usd, fill = calificacion_riesgo)) +
geom_bar(stat = "identity", width = 0.65, color = "white") +
geom_text(
aes(label = paste0(pct_saldo, "%\n($",
comma(round(saldo_total_usd / 1e6, 2)), "M)")),
vjust = -0.3, size = 3.2, fontface = "bold"
) +
scale_fill_manual(values = colores_cal) +
scale_y_continuous(
labels = function(x) paste0("$", comma(x / 1e6), "M"),
expand = expansion(mult = c(0, 0.2))
) +
labs(
title = "Distribución de la Cartera por Calificación de Riesgo",
x = "Calificación de riesgo",
y = "Saldo de capital (USD)" ) +
theme_minimal(base_size = 12) +
theme(
plot.title = element_text(face = "bold", hjust = 0.5),
legend.position= "none",
panel.grid.major.x = element_blank()
)
El 62.7% de la cartera (calificaciones A y B) se ubica en los segmentos de menor riesgo, lo que refleja que la mayoría de los clientes tienen un buen comportamiento de pago.
Sin embargo, el 37.2% restante (calificaciones C, D y E) representa exposición a riesgo medio-alto, concentrando $4.83 millones en saldo de capital require una mayor provisión. De las categorías en riesgo, la categoría C representan la mayor concentración de saldo de capital con $2.65 millones, se sugiere coordinar gestiones de cobro a este segmento para evitar que transicionen a un deterioro objetivo.
#Validación de categorias de la variable estado_pago
print(unique(bases[["pagos"]]$estado_pago))
## [1] "Parcial" "Completo" "No pagado"
estado_cuotas <- bases[["pagos"]] %>%
mutate(
estado_pago = factor(estado_pago, levels = c("Completo", "Parcial", "No pagado"))) %>%
group_by(estado_pago) %>%
summarise(
cantidad_cuotas = n(),
monto_cuota_usd = sum(monto_cuota_usd, na.rm = TRUE),
monto_pagado_usd = sum(monto_pagado_usd, na.rm = TRUE),
.groups = "drop"
) %>%
mutate(
pct_cantidad = round(cantidad_cuotas / sum(cantidad_cuotas) * 100, 1),
monto_pendiente = monto_cuota_usd - monto_pagado_usd
)
estado_cuotas
## # A tibble: 3 × 6
## estado_pago cantidad_cuotas monto_cuota_usd monto_pagado_usd pct_cantidad
## <fct> <int> <dbl> <dbl> <dbl>
## 1 Completo 2142 5415853. 5415853. 71.4
## 2 Parcial 504 1234145. 673502. 16.8
## 3 No pagado 354 906909. 0 11.8
## # ℹ 1 more variable: monto_pendiente <dbl>
estado_cuotas %>%
ggplot(aes(x = 2, y = pct_cantidad, fill = estado_pago)) +
geom_bar(stat = "identity", width = 1, color = "white") +
coord_polar(theta = "y") +
xlim(0.5, 2.5) +
geom_text(aes(label = paste0(pct_cantidad, "%")),
position = position_stack(vjust = 0.5),
fontface = "bold", size = 4.5, color = "white") +
scale_fill_manual(values = c("#2E7D32","#f5d376","#C62828")) +
labs(title = "Estado de las Cuotas",
subtitle = "Etiquetas: Proporción de Cantidad de Créditos",
fill = NULL) +
theme_void() +
theme(plot.title = element_text(face = "bold", hjust = 0.5),
plot.subtitle = element_text(hjust = 0.5, color = "gray25"),)
El 71.4% de las cuotas fueron cobradas de forma completa, representando $5.42 millones recuperados. El 16.8% corresponde a pagos parciales, lo que implica que los clientes están pagando pero no el monto total de la cuota, generando una monto no recuperado de$0.56 millones.
El 11.8% de las cuotas no fueron pagadas, equivalente a $0.91 millones no recuperado. En total, las cuotas parciales y no pagadas representan el 28.6% del total, esto refleja la necesidad de reforzar las estrategias de cobranza preventiva en la cartera para perfiles de alto riesgo.
cartera_tipo <- bases[["prestamos"]] %>%
group_by(tipo_prestamo) %>%
summarise(
cantidad = n(),
monto_original = sum(monto_original_usd, na.rm = TRUE),
saldo_capital = sum(saldo_capital_usd, na.rm = TRUE),
.groups = "drop"
) %>%
mutate(
pct_saldo = round(saldo_capital / sum(saldo_capital) * 100, 2)
) %>%
arrange(desc(saldo_capital))
cartera_tipo_totales <- cartera_tipo %>%
summarise(
across(where(is.numeric), sum, na.rm = TRUE)
) %>% mutate(
tipo_prestamo = "Totales"
)
cartera_tipo_totales <- bind_rows(cartera_tipo, cartera_tipo_totales) %>%
mutate(
monto_original = dollar(monto_original, prefix = "$"),
saldo_capital = dollar(saldo_capital, prefix = "$"),
)
cartera_tipo_totales
## # A tibble: 6 × 5
## tipo_prestamo cantidad monto_original saldo_capital pct_saldo
## <chr> <int> <chr> <chr> <dbl>
## 1 Consumo 167 $10,084,647 $2,963,780 22.9
## 2 Comercial 168 $9,112,768 $2,704,026 20.9
## 3 Microempresa 159 $9,774,331 $2,622,991 20.2
## 4 Vehículo 171 $10,619,051 $2,489,013 19.2
## 5 Hipotecario 135 $8,362,610 $2,171,594 16.8
## 6 Totales 800 $47,953,408 $12,951,404 100
ggplot(cartera_tipo,
aes(x = reorder(tipo_prestamo, saldo_capital),
y = saldo_capital,
fill = tipo_prestamo)) +
geom_bar(stat = "identity", width = 0.65, color = "white") +
geom_text(
aes(label = paste0(pct_saldo, "% ($",
comma(round(saldo_capital / 1e6, 2)), "M)")),
hjust = -0.08, size = 3.2, fontface = "bold"
) +
coord_flip() +
scale_y_continuous(
labels = function(x) paste0("$", comma(x / 1e6), "M"),
expand = expansion(mult = c(0, 0.22))
) +
scale_fill_brewer(palette = "Set2") +
labs(
title = "Distribución de la Cartera por Tipo de Préstamo",
subtitle = "Saldo de capital vigente en USD",
x = NULL,
y = "Saldo de capital (USD)"
) +
theme_minimal(base_size = 12) +
theme(
plot.title = element_text(face = "bold", hjust = 0.5),
plot.subtitle = element_text(hjust = 0.5, color = "gray50"),
legend.position = "none",
panel.grid.major.y = element_blank()
)
El 64.01% de la cartera esta compuesto por los segmentos de Consumo, Comercial y Microempresa, liderado por Consumo con un saldo capital de $2.96 millones, lo cual representa el 22.88% de la cartera. Es decir, que la empresa tiene la mayor parte de los créditos colocados en productos con garantía fiduciaria, los cuales son más reconocidos por el deterioro por la volatilidad en el comportamiento de pago. Se recomienda mantener un monitreo constante de los bucket en deterioro de los segmentos más grandes.
pagos_sector <- bases[["pagos"]] %>%
left_join(
bases[["clientes"]] %>% select(cliente_id, sector_economico),
by = "cliente_id"
) %>%
filter(!is.na(sector_economico))
sector_resumen <- pagos_sector %>%
group_by(sector_economico) %>%
summarise(
dias_retraso_promedio = round(mean(dias_retraso, na.rm = TRUE), 1),
intereses_moratorios_usd = round(sum(interes_moratorio_usd, na.rm = TRUE), 2),
total_cuotas = n(),
.groups = "drop"
) %>%
arrange(desc(dias_retraso_promedio))
sector_resumen
## # A tibble: 8 × 4
## sector_economico dias_retraso_promedio intereses_moratorios_usd total_cuotas
## <chr> <dbl> <dbl> <int>
## 1 Transporte 23.3 18265. 472
## 2 Salud 23.1 5672. 307
## 3 Manufactura 22.9 4745. 266
## 4 Construcción 22.7 10413. 367
## 5 Agropecuario 22.2 15249. 529
## 6 Retail 20.8 7614. 319
## 7 Tecnología 18.8 5028. 378
## 8 Servicios 18.4 5078. 362
#Días de atraso
ggplot(sector_resumen,
aes(x = reorder(sector_economico, dias_retraso_promedio),
y = dias_retraso_promedio,
fill = dias_retraso_promedio)) +
geom_bar(stat = "identity", width = 0.65, color = "white") +
geom_text(
aes(label = paste0(dias_retraso_promedio, " días")),
hjust = -0.2, size = 3.5, fontface = "bold"
) +
scale_fill_gradient(low = "#f5d376", high = "#B71C1C") +
scale_y_continuous(expand = expansion(mult = c(0, 0.2))) +
coord_flip() +
labs(
title = "Días de Retraso Promedio por Sector Económico",
subtitle = "Promedio de días de mora por cuota según sector del cliente",
x = NULL,
y = "Días de retraso promedio"
) +
theme_minimal(base_size = 12) +
theme(
plot.title = element_text(face = "bold", hjust = 0.5),
plot.subtitle = element_text(hjust = 0.5, color = "gray25" ),
legend.position = "none",
panel.grid.major.y = element_blank()
)
El sector que presenta un promedio de 23.3 días de atraso es Transporte,
seguido por Salud y Manufactura. Considerando la naturaleza de sus
operaciones, es probable que necesiten un mayor seguimiento en la
gestión de cobro. Al contrario, los sectores de Servicios, Tecnología y
Retail, son los que presentan el menor promedio de días de atraso.
# Intereses moratorios acumulados
ggplot(sector_resumen,
aes(x = reorder(sector_economico, intereses_moratorios_usd),
y = intereses_moratorios_usd,
fill = intereses_moratorios_usd)) +
geom_bar(stat = "identity", width = 0.65, color = "white") +
geom_text(
aes(label = paste0("$", comma(round(intereses_moratorios_usd)))),
hjust = -0.15, size = 3.5, fontface = "bold"
) +
scale_fill_gradient(low = "#E3F2FD", high = "#0D47A1") +
scale_y_continuous(
labels = function(x) paste0("$", comma(x)),
expand = expansion(mult = c(0, 0.2))
) +
coord_flip() +
labs(
title = "Intereses Moratorios Acumulados por Sector Económico",
subtitle = "Total de intereses por mora generados según sector del cliente",
x = NULL,
y = "Intereses moratorios (USD)"
) +
theme_minimal(base_size = 12) +
theme(
plot.title = element_text(face = "bold", hjust = 0.5),
plot.subtitle = element_text(hjust = 0.5, color = "gray25"),
legend.position = "none",
panel.grid.major.y = element_blank()
)
Considerando que el sector económico de Transporte es el sector
que presenta mayores días de atraso en promedio, es natural que lidere
en la cantidad de intereses moratorios. Seguido por Agropecuario y
Construcción, considerando la naturaleza de sus operaciones los
intereses moratorios cobrados deben ser mayores a los sectores que
pueden ofrecer mayor garantía sobre sus créditos como Manufactura y
Tecnología.
La cartera mantiene un comportamiento saludable con el 58.94% de cuotas al día y el 62.7% del saldo en calificaciones A y B.
Los principales focos de riesgo son la región Sur (45.25% de morosidad), el sector Transporte (mayor atraso e intereses moratorios) y el bucket 91-180 días (7.78% susceptible a castigo).
El 28.6% de las cuotas presentan cobro incompleto o nulo, representando una brecha de recuperación de $1.47M que requiere atención inmediata.
Se recomienda emplearestrategias de cobranza en los segmentos de Consumo y Comercial de las regiones Sur y Centro, priorizando clientes del sector Transporte.