Volver a proyectos
Volver
Aprendizaje no supervisadoArtículo técnico

Clusterización, perfiles de riesgo y desempeño

Clasificación de las entidades que conforman la cartera institucional del BANHVI mediante K-means, con el propósito de identificar perfiles relativos de riesgo, solidez financiera y desempeño.

Autores: Henry Fabian Alvarado Vargas, José Andrés Castillo Azofeifa y Mario Alejandro Paniagua BarrantesUniversidad de Costa Rica2026
01

Objetivo y metodología

El análisis tiene como objetivo clasificar las entidades que conforman la cartera institucional del BANHVI según sus características financieras, con el propósito de identificar perfiles relativos de riesgo y desempeño.

La segmentación se realizó mediante el algoritmo de aprendizaje no supervisado K-means, utilizando indicadores financieros empleados por la Superintendencia General de Entidades Financieras (SUGEF) para evaluar la liquidez, el riesgo crediticio, la rentabilidad y la eficiencia operativa de las entidades del sistema financiero costarricense.

Para estimar el modelo se utilizaron datos históricos de 2024. Los valores anuales fueron tratados mediante una media geométrica, con el propósito de reducir el efecto de las variaciones estacionales observadas durante el período.

Los cinco indicadores seleccionados representan dimensiones centrales de la solidez financiera: intermediación, cobertura crediticia, morosidad, rentabilidad y eficiencia operativa.
02

Segmentación inicial de la cartera

Como primera etapa, la cartera se dividió en dos grupos de acuerdo con la naturaleza operativa de las entidades.

Tabla 1. Segregación de la cartera por clúster

Clúster A — Bancos y otrosClúster B — Cooperativas
CathayCoocique
Grupo MutualCoopeande
MUCAPCoopecaja
PopularCoopemep
ScotiabankCoopenae
BAC San JoséCoopealianza

Fuente: elaboración propia con datos del Informe de Gestión FONAVI (2025).

03

Indicadores financieros analizados

Los indicadores seleccionados buscan representar las principales dimensiones de la solidez financiera de las entidades.

Tabla 2. Códigos y denominaciones de los indicadores financieros

IndicadorCódigo
Activo Productivo de Intermediación Financiera / Pasivo con Costo 1I3
Estimaciones sobre cartera de créditos / Cartera con atraso mayor a 90 días y cobro judicialI8
Morosidad mayor a 90 días y cobro judicial / Cartera DirectaI10
Rentabilidad nominal sobre Patrimonio PromedioI14
Utilidad Operacional Bruta / Gastos de Administración 1I15

Fuente: elaboración propia con datos de la SUGEF (Costa Rica).

04

Selección del número de subclústeres

La selección de tres subclústeres para cada segmento se fundamentó en la aplicación conjunta del método del codo y el coeficiente de silueta o silhouette.

El método del codo evalúa la reducción de la variación interna conforme aumenta el número de grupos, mientras que el coeficiente de silueta permite valorar qué tan bien separadas y cohesionadas se encuentran las observaciones dentro de cada solución.

Figura 1. Método del codo y Silhouette para Cooperativas

Método del codo aplicado al segmento de cooperativas

Método del codo

Método Silhouette aplicado al segmento de cooperativas

Coeficiente Silhouette

Fuente: elaboración propia con datos de la SUGEF (Costa Rica).

Anexo 1. Método del codo y Silhouette para Otras Instituciones

Método del codo aplicado al segmento de bancos y otras instituciones

Método del codo

Método Silhouette aplicado al segmento de bancos y otras instituciones

Coeficiente Silhouette

Fuente: elaboración propia con datos de la SUGEF (Costa Rica).
05

Perfiles de riesgo y desempeño: Clúster A

Tabla 3. Clusterización de Bancos y Otras Instituciones

EntidadI10I14I15I3I8Clúster
CATHAY2.306.091.670.9001.9201
GRUPO MUTUAL2.9510.101.210.9210.8651
MUCAP2.814.211.040.9010.6141
POPULAR2.201.871.241.2601.6602
SCOTIABANK1.312.591.280.9942.0502
BAC SAN JOSÉ1.2412.801.911.0803.4803

Fuente: elaboración propia con datos de la SUGEF (Costa Rica).

Subclúster 1

Cathay, Grupo Mutual y MUCAP

Alto riesgo relativo

Las entidades presentan una rentabilidad relativamente favorable y un nivel moderado de morosidad. Su desempeño evidencia una capacidad adecuada para generar utilidades a partir del patrimonio y utilizar eficientemente sus recursos.

El principal elemento de vulnerabilidad corresponde a la baja cobertura de la cartera riesgosa, representada por el indicador I8. Esto limita su capacidad para absorber aumentos inesperados de la morosidad o pérdidas crediticias durante escenarios económicos adversos.

Aunque el grupo mantiene un equilibrio razonable entre rentabilidad y riesgo, una parte importante de su fortaleza depende de que se mantenga un entorno crediticio favorable. Por esta razón, se clasifica como el subclúster de mayor riesgo relativo entre los bancos y otras entidades.

Subclúster 2

Banco Popular y Scotiabank

Riesgo relativo medio

Este grupo presenta un perfil más conservador. Su rentabilidad sobre el patrimonio es inferior a la del primer subclúster, pero mantiene una menor morosidad promedio y una cobertura crediticia más favorable.

Los resultados reflejan una preferencia por la estabilidad financiera frente a la maximización del retorno. Las entidades parecen mantener políticas crediticias más prudentes y estructuras patrimoniales orientadas a limitar la exposición a pérdidas.

Debido a que su mayor estabilidad se acompaña de una rentabilidad limitada, el grupo se clasifica con un nivel medio de riesgo relativo.

Subclúster 3

BAC San José

Bajo riesgo relativo y alto retorno

BAC San José constituye un clúster individual debido a que presenta indicadores significativamente distintos a los de sus pares. La entidad puede considerarse un outlier estructural dentro del segmento bancario.

  • Mayor rentabilidad sobre el patrimonio: I14 de 12,80.
  • Menor morosidad del segmento: I10 de 1,24.
  • Elevada cobertura de la cartera riesgosa: I8 de 3,48.
  • Mayor eficiencia operativa: I15 de 1,91.

La combinación de rentabilidad elevada, baja morosidad, eficiencia operativa y cobertura crediticia favorable ubica a BAC San José en una posición de bajo riesgo relativo y alto retorno.

06

Perfiles de riesgo y desempeño: Clúster B

Tabla 4. Clusterización de Cooperativas

EntidadI10I14I15I3I8Clúster
COOCIQUE2.712.971.271.121.491
COOPEANDE2.892.711.431.211.381
COOPECAJA1.996.011.531.463.822
COOPEMEP1.673.901.471.321.892
COOPENAE2.135.611.621.201.922
COOPEALIANZA2.477.292.541.255.683

Fuente: elaboración propia con datos de la SUGEF (Costa Rica).

Subclúster 1

Coocique y Coopeande

Alto riesgo relativo

Este grupo presenta la morosidad promedio más elevada y la rentabilidad sobre el patrimonio más baja entre las cooperativas analizadas. Aunque mantiene una eficiencia operativa razonable, esta es inferior a la registrada por los otros subclústeres.

La cobertura de la cartera riesgosa también se encuentra en niveles bajos. La combinación de una morosidad alta, una cobertura crediticia limitada y una rentabilidad reducida incrementa la vulnerabilidad de estas entidades ante un deterioro de la economía.

En un escenario de pérdidas inesperadas, las utilidades podrían resultar insuficientes para absorber completamente los efectos de un incremento de la morosidad. Por ello, este grupo representa el subclúster de mayor riesgo relativo dentro de las cooperativas.

Subclúster 2

Coopecaja, Coopemep y Coopenae

Riesgo relativo medio

Las entidades de este grupo presentan carteras relativamente más sanas, con una morosidad promedio inferior a la del primer subclúster. Además, registran una rentabilidad, eficiencia operativa y cobertura crediticia moderadas.

El grupo muestra un balance más favorable entre generación de retornos y control del riesgo. Su cobertura promedio permite reducir la vulnerabilidad ante eventos adversos, aunque su capacidad de absorción de pérdidas todavía es intermedia.

Por esta razón, el subclúster se clasifica con un nivel medio de riesgo relativo, al encontrarse en una mejor posición que el primer grupo, pero sin alcanzar la fortaleza financiera de Coopealianza.

Subclúster 3

Coopealianza

Perfil más robusto

Coopealianza constituye un clúster individual debido a que sus indicadores se encuentran alejados de los valores observados en las demás cooperativas.

  • Mayor rentabilidad entre las cooperativas: I14 de 7,29.
  • Mayor eficiencia operativa: I15 de 2,54.
  • Cobertura crediticia más elevada: I8 de 5,68.
  • Morosidad intermedia: I10 de 2,47.

Aunque su morosidad no es la más baja del segmento, la elevada cobertura crediticia, la rentabilidad y el margen operativo proporcionan una mayor capacidad para enfrentar pérdidas o escenarios adversos.

Coopealianza se posiciona como el subclúster más estable y robusto entre las cooperativas, con una estructura de negocio eficiente y un perfil prudencial comparativamente maduro.

07

Conclusiones

  • La aplicación de K-means permitió identificar diferencias relevantes en los perfiles de riesgo y desempeño de las entidades de la cartera institucional del BANHVI.
  • Los indicadores de morosidad (I10), cobertura crediticia (I8) y rentabilidad (I14) resultaron determinantes para distinguir los perfiles financieros.
  • Cathay, Grupo Mutual y MUCAP presentan la mayor vulnerabilidad relativa entre bancos y otras entidades, principalmente por su menor cobertura de cartera.
  • Banco Popular y Scotiabank mantienen una estrategia comparativamente conservadora, caracterizada por mayor estabilidad y niveles reducidos de rentabilidad.
  • BAC San José combina la mayor rentabilidad, la menor morosidad y una cobertura elevada, por lo que presenta el mejor desempeño relativo de su segmento.
  • Coocique y Coopeande presentan el perfil más vulnerable entre las cooperativas por su morosidad elevada, rentabilidad reducida y baja cobertura crediticia.
  • Coopecaja, Coopemep y Coopenae mantienen una posición intermedia, con mejores condiciones de cartera y cobertura, aunque con capacidad moderada para absorber choques.
  • Coopealianza presenta el perfil más robusto entre las cooperativas debido a su cobertura crediticia, rentabilidad y eficiencia operativa.
  • Los clústeres individuales de BAC San José y Coopealianza evidencian outliers estructurales y demuestran la capacidad del modelo para detectar comportamientos diferenciados.
  • Los resultados constituyen una herramienta complementaria para orientar el seguimiento de la cartera, priorizar entidades y definir análisis específicos de riesgo.
08

Referencia

El presente análisis forma parte del Trabajo Final de Investigación Aplicada titulado Evaluación de la solidez financiera de los clientes de la cartera institucional del BANHVI dentro del sistema financiero costarricense y sensibilización de la cartera ante escenarios macroeconómicos adversos, desarrollado para optar por el grado y título de Maestría Profesional en Riesgo y Finanzas de la Universidad de Costa Rica.

El Trabajo Final de Graduación fue elaborado conjuntamente por:

  • Henry Fabian Alvarado Vargas.
  • José Andrés Castillo Azofeifa.
  • Mario Alejandro Paniagua Barrantes.

Universidad de Costa Rica

Sistema de Estudios de Posgrado

Programa de Posgrado en Economía

Ciudad Universitaria Rodrigo Facio, Costa Rica

2026

Descripción del proyecto

La investigación tuvo como objetivo evaluar la solidez financiera de las instituciones que conforman la cartera institucional del Banco Hipotecario de la Vivienda, BANHVI, así como analizar la sensibilidad de sus principales indicadores financieros ante cambios y escenarios macroeconómicos adversos.

Alvarado Vargas, H. F., Castillo Azofeifa, J. A., & Paniagua Barrantes, M. A. (2026). Evaluación de la solidez financiera de los clientes de la cartera institucional del BANHVI dentro del sistema financiero costarricense y sensibilización de la cartera ante escenarios macroeconómicos adversos [Trabajo final de investigación aplicada de maestría, Universidad de Costa Rica].
09

Código fuente

El apartado conserva la misma estructura del artículo anterior para que pueda incorporar posteriormente el script completo del análisis de clusterización.

clusterizacion_banhvi.R
##Paquetes

  # install.packages(c("readxl","tibble","dplyr","tidyr","cluster","psych","stringr")) ###Instalar librerias

  ##Librerías
  library(readxl)
  library(tibble)
  library(dplyr)
  library(tidyr)
  library(cluster)
  library(psych)
  library(stringr)

  ##Cargar datos
  ruta_archivo = "C:\\Users\\usuario\\Desktop\\TFG\\Datos\\Base de datos-Para Panel.xlsx"
  data_total   = read_excel(path = ruta_archivo, sheet = "Data")

  ##Filtros base
  entidades_filtrar = c("BAC SAN JOSE","CATHAY","GRUPO MUTUAL","MUCAP","SCOTIABANK","POPULAR")
  data_total$Periodo = as.Date(data_total$Periodo)

  # Filtra por entidades y año (ajusta si deseas más)
  data_filtrada <- data_total[
    data_total$Entidad %in% entidades_filtrar &
      format(data_total$Periodo, "%Y") %in% c("2024"),
  ]

  ##Indicador  Código (I1–I15) 
  #Nota: Aca se el asigna a cada indicar de la SUGEF un codigo de indicar, para fines de simplicidad 
  data_filtrada <- data_filtrada %>%
    mutate(
      Indicador = str_squish(Indicador),
      Indicador = case_when(
        Indicador == "Activo Productivo / Activo total" ~ "I1",
        Indicador == "Activo Productivo de Intermediación Financiera / Activo Productivo" ~ "I2",
        Indicador == "Activo Productivo de Intermediación Financiera/ Pasivo con Costo  1" ~ "I3",
        Indicador == "Captaciones a plazo con el público / Pasivo con costo" ~ "I4",
        Indicador == "Cartera al día y con atraso de hasta 90 días (excepto cobro judicial) / Activo Productivo" ~ "I5",
        Indicador == "Cartera al día y con atraso hasta 90 días/Cartera total" ~ "I6",
        Indicador %in% c("Compromiso patrimonial","Solvencia y Capitalización","Solvencia y Capitalizacion") ~ "I7",
        Indicador == "Estimaciones sobre cartera de créditos / Cartera con atraso mayor a 90 días y cobro judicial" ~ "I8",
        Indicador == "Inversiones en títulos valores / Activo Productivo de Intermediación Financiera" ~ "I9",
        Indicador == "Morosidad mayor a 90 días y cobro judicial / Cartera Directa" ~ "I10",
        Indicador == "Obligaciones con entidades financieras del exterior / Pasivo con costo" ~ "I11",
        Indicador == "Obligaciones con entidades financieras del país / Pasivo con costo" ~ "I12",
        Indicador == "Pasivo con costo / Pasivo Total" ~ "I13",
        Indicador == "Rentabilidad nominal sobre Patrimonio Promedio" ~ "I14",
        Indicador == "Utilidad Operacional Bruta / Gastos de Administración  1" ~ "I15",
        TRUE ~ Indicador
      )
    )

  ##Indicadores a usar en la clusterizacion (k-means)
  #Nota: estos se selecionaron dado temas de liquidez, mora, riesgo etc.
  indicadores_seleccionados <- c("I8","I14","I3","I10","I15")

  ## Media geométrica por Entidad-Indicador
  #Nota: se aplica una media geometrica de los datos por indicador, se usa esta media por su solidez matematica 
  media_geom <- function(x) {
    x <- suppressWarnings(as.numeric(x))
    x <- x[is.finite(x) & x > 0]    # evita log de no-positivos
    if (length(x) == 0) return(NA_real_)
    exp(mean(log(x)))}

  media_geometrica <- data_filtrada %>%
    group_by(Entidad, Indicador) %>%
    summarise(Media_Geometrica = media_geom(Valor), .groups = "drop") %>%
    filter(Indicador %in% indicadores_seleccionados)

  ##k-means (una fila por Entidad)
  df_wide <- media_geometrica %>%
    pivot_wider(names_from = Indicador, values_from = Media_Geometrica)

  if (!"Entidad" %in% names(df_wide)) {
    stop("No hay columna 'Entidad' después del pivot_wider. Verifica datos/indicadores.")
  }

  # Matriz numérica con Entidad 
  mat <- df_wide %>%
    select(Entidad, all_of(indicadores_seleccionados)) %>%  # asegura solo indicadores deseados
    column_to_rownames("Entidad") %>%
    mutate(across(everything(), as.numeric)) %>%
    as.data.frame()

  if (nrow(mat) < 2) stop("Se necesitan al menos 2 entidades para ejecutar k-means/silhouette.")

  # Imputar NAs por media de columna
  mat[is.na(mat)] <- apply(mat, 2, function(x) mean(x, na.rm = TRUE))[col(mat)][is.na(mat)]

  # Eliminar columnas constantes (sd = 0) que rompen el escalado
  sds <- apply(mat, 2, sd, na.rm = TRUE)
  const_cols <- names(sds)[which(!is.finite(sds) | sds == 0)]
  if (length(const_cols) > 0) {
    message("Eliminando columnas sin varianza: ", paste(const_cols, collapse = ", "))
    mat <- mat[, setdiff(colnames(mat), const_cols), drop = FALSE]
  }

  if (ncol(mat) == 0) stop("No quedan indicadores con varianza para clusterizar.")

  # Estandarizar
  mat_num    <- as.matrix(mat)
  mat_scaled <- scale(mat_num)

  if (!all(is.finite(mat_scaled))) {
    stop("Hay NA/NaN/Inf después de escalar. Revisa datos o columnas sin varianza.")
  }

  # Filas útiles y distintas (para limitar k)
  n_rows <- nrow(mat_scaled)
  n_dist <- nrow(unique(as.data.frame(mat_scaled)))
  if (is.null(n_rows) || n_rows < 1) stop("mat_scaled no tiene filas válidas.")

  ##MÉTODO DEL CODO
  set.seed(123)
  max_k <- min(10, n_rows, n_dist)   # tope seguro
  wss <- sapply(1:max_k, function(k){
    tryCatch(
      kmeans(mat_scaled, centers = k, nstart = 25)$tot.withinss,
      error = function(e) NA_real_
    )
  })
  if (all(is.na(wss))) stop("No fue posible calcular WSS para ningún k. Verifica los datos.")

  plot(1:max_k, wss, type = "b", pch = 19,
      xlab = "Número de clusters (k)", ylab = "WSS",
      main = "Método del codo")

  ## MÉTODO SILHOUETTE 
  if (n_rows >= 3 && max_k >= 2) {
    ks_sil <- 2:max_k
    sil_width <- sapply(ks_sil, function(k){
      tryCatch({
        km <- kmeans(mat_scaled, centers = k, nstart = 25)
        mean(silhouette(km$cluster, dist(mat_scaled))[, 3])
      }, error = function(e) NA_real_)
    })
    if (all(is.na(sil_width))) {
      message("No se pudo calcular silhouette para los k probados.")
    } else {
      plot(ks_sil, sil_width, type = "b", pch = 19,
          xlab = "k", ylab = "Silhouette promedio",
          main = "Método Silhouette")
    }
  } else {
    message("Silhouette requiere al menos 3 entidades y k >= 2.")
  }

  #Nota: se incorporan los 2 metodos para veridicar la cnatidad de clusteres


  ##Otros paquetes
  suppressPackageStartupMessages({
    library(dplyr)
    library(tibble)
    library(psych)   # para usar describeBy
  })

  ## Selección k óptimo 
  set.seed(123)

  usar_codo = TRUE   # si quiero usar silhouette poner falso


  if (usar_codo) {
    k_opt =  3   # valor elegido según el gráfico del codo
  } else if (exists("sil_width") && any(is.finite(sil_width))) {
    k_opt = ks_sil[ which.max(sil_width) ]   # mejor k por silhouette
  } else {
    k_opt = min(3, if (exists("max_k")) max_k else 3)  # fallback seguro
  }

  ## Ajuste final del modelo K-means 
  # mat_scaled: matriz/df numérica estandarizada (filas = Entidad)
  stopifnot(exists("mat_scaled"))

  km_fit <- kmeans(
    x         = mat_scaled,
    centers   = k_opt,
    nstart    = 50,
    iter.max  = 100)

  ## RESULTADO PRINCIPAL: Entidad - Cluster 
  # Resolver nombres de entidad de forma robusta
  entidades <- rownames(mat_scaled)
  if (is.null(entidades) || any(is.na(entidades)) || any(entidades == "")) {
    stopifnot(exists("df_wide"), "Entidad" %in% names(df_wide))
    entidades <- df_wide$Entidad}

  cluster_assignments <- tibble(
    Entidad = entidades,
    Cluster = factor(km_fit$cluster, levels = sort(unique(km_fit$cluster))))

  ## Tabla con indicadores + cluster 
  stopifnot(exists("df_wide"), "Entidad" %in% names(df_wide))

  df_clusters <- df_wide %>%
    select(Entidad, everything()) %>%
    left_join(cluster_assignments, by = "Entidad") %>%
    arrange(Cluster, Entidad)

  print(df_clusters, n = Inf)

  ##Conteo por cluster 
  conteo_clusters = df_clusters %>% count(Cluster, name = "Num_Entidades")
  print(conteo_clusters)

  ## Perfil promedio por cluster 
  cluster_profile <- df_clusters %>%
    select(-Entidad) %>%
    group_by(Cluster) %>%
    summarise(
      across(where(is.numeric), mean, na.rm = TRUE, .names = "avg_{.col}"),
      .groups = "drop"
    )

  print(cluster_profile, n = Inf)

  ## (Opcional) Descriptivos por cluster en datos escalados 
  mat_scaled_m <- if (is.data.frame(mat_scaled)) as.matrix(mat_scaled) else mat_scaled
  desc_clusters <- describeBy(mat_scaled_m, group = km_fit$cluster, mat = TRUE)
  print(head(desc_clusters, 10))
Desplácese dentro del recuadro para consultar el script completo.