Carga de librerías y datos

library(tidyverse)
library(scales)

Carga de Insumos

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

1. Cartera por atraso por Bucket

(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.

2. Tasa de morosidad por región geográfica

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%.

3. Distribución de la cartera por calificación de riesgo

#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.

4. Estado de las cuotas

#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.

5. Distribución de cartera por tipo de préstamo

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.

6. Días de retraso promedio e intereses moratorios por sector económico

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.

Conclusiones Generales

  • 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.