Introducción

El objetivo de este trabajo es aplicar el método de clustering no jerárquico K-Medianas (PAM — Partitioning Around Medoids) a un conjunto de indicadores macroeconómicos obtenidos directamente de la API del Fondo Monetario Internacional (FMI) mediante el paquete imf.data.

Se utilizan tres variables del World Economic Outlook (WEO), todas expresadas en porcentajes y en su versión de proyecciones 2027-2031:

  • Crecimiento del PIB real (NGDP_RPCH).
  • Inflación, IPC promedio (PCPIPCH).
  • Tasa de desempleo (LUR).

Con ellas se agrupan 18 países según su perfil macroeconómico proyectado. Es importante precisar que el resultado describe similitudes esperadas según el FMI; no constituye un pronóstico de cómo evolucionarán los grupos.

A diferencia del clustering jerárquico (p. ej. Ward), en el clustering no jerárquico el número de grupos (k) se define antes de correr el algoritmo, y cada observación se asigna al grupo cuyo representante le resulte más parecido. Dentro de los métodos particionales existen dos variantes principales:

  • K-Medias (K-Means): el representante es un centroide, un promedio artificial que puede no coincidir con ningún país real. Es sensible a valores atípicos porque la media se distorsiona con outliers.
  • K-Medianas / PAM: el representante es un medoide, la observación real más central del grupo. Al elegir un representante existente en lugar de promediar, es mucho más robusto ante valores atípicos.

En la sección 8 se agrega el método jerárquico aglomerativo de Ward, aplicado a cinco indicadores del Banco Mundial (paquete wbstats) para los mismos 18 países, de modo que ambos enfoques (particional y jerárquico) puedan contrastarse.

1. Clasificación Lógica Económica

1.1 ¿En qué consiste el método?

PAM es un algoritmo de clustering particional (no jerárquico): parte de un número fijo k de grupos definido de antemano y asigna cada observación a uno de ellos, sin construir árbol ni dendrograma. El procedimiento es:

  1. Se seleccionan k observaciones reales como medoides iniciales (fase BUILD).
  2. Cada observación restante se asigna al medoide más cercano (distancia Manhattan).
  3. Para cada grupo se evalúa si intercambiar el medoide por otra observación reduce la suma total de disimilitudes (fase SWAP).
  4. Se repiten los pasos 2 y 3 hasta que ningún intercambio mejore la solución (convergencia).

Al ser particional, no es jerárquico: no hay una secuencia de fusiones ni dendrograma, y una observación puede cambiar de grupo entre iteraciones (en los métodos jerárquicos, una fusión es irrevocable).

1.2 ¿Por qué PAM es idóneo para los datos del FMI?

Los indicadores del FMI (crecimiento, inflación, desempleo) son series volátiles y suelen incluir valores atípicos genuinos: economías con inflación muy alta o con desempleo estructural elevado que se apartan mucho del resto sin que sea un error de medición.

  • K-Means usa la media como representante. Un solo país extremo puede “arrastrar” el centroide y distorsionar la asignación de los demás.
  • PAM usa un medoide real. Su posición no se ve arrastrada por un valor extremo y el resultado es más interpretable: “este grupo se parece al perfil de tal país”, en lugar de un promedio artificial.

Además, al estar las tres variables en porcentajes, son comparables e interpretables directamente. Aun así se aplica scale(), porque tienen dispersiones muy distintas (la inflación puede variar mucho más que el desempleo) y sin estandarizar dominaría el cálculo de distancias.

2. Explicación Matemática (intuitiva)

PAM minimiza la suma total de disimilitudes entre cada observación y el medoide de su grupo:

\[\text{TD} = \sum_{i=1}^{k} \sum_{x \in C_i} d(x, m_i)\]

donde \(C_i\) es el grupo \(i\), \(m_i\) su medoide (observación real) y \(d(x, m_i)\) la distancia entre \(x\) y su medoide.

Distancia Manhattan: PAM suele emplear la distancia de bloques en lugar de la euclídea:

\[d(x_i, x_j) = \sum_{l=1}^{p} |x_{il} - x_{jl}|\]

En lugar de elevar las diferencias al cuadrado (lo que penaliza fuertemente las grandes), suma sus valores absolutos, por lo que un valor extremo en una variable no domina la distancia total. Aquí \(p = 3\): PIB, inflación y desempleo.

En términos económicos: en cada iteración el algoritmo se pregunta “si en vez de este país uso otro país real como representante del grupo, ¿se reduce la distancia total de los países del grupo hacia su representante?”. Si la respuesta es sí, cambia el medoide. Converge cuando ningún cambio mejora la cohesión del grupo.

3. Ventajas y Desventajas Comparativas

Método Tipo Ventajas frente a PAM Desventajas frente a PAM
Ward (Grupo 1) Jerárquico aglomerativo No requiere fijar k de antemano; dendrograma muy interpretable Una asignación no puede revertirse; más costoso con muestras grandes
DBSCAN (Grupo 2) No jerárquico, basado en densidad Detecta ruido/outliers como categoría propia; no requiere fijar k Sensible a Épsilon y MinPts; falla si los grupos tienen densidades muy distintas
K-Means (Grupo 4) No jerárquico, particional Más rápido y escalable a muestras muy grandes Usa centroides (promedios): muy sensible a outliers; los centros pueden no ser países reales
PAM / K-Medianas (Grupo 3) No jerárquico, particional Robusto ante outliers; el representante es un país real e interpretable Más costoso computacionalmente que K-Means; requiere fijar k

Síntesis: PAM combina la lógica de un método particional con una robustez ante valores atípicos que ni K-Means ni Ward ofrecen, lo que lo hace adecuado para indicadores del FMI, donde la volatilidad y los casos extremos son frecuentes.

4. Selección del Número Óptimo de Clústeres

Entre las métricas generales (Método del Codo, Coeficiente de Silueta, Dendrograma, Gráfico de K-Vecinos), la que corresponde a PAM es el Coeficiente de Silueta:

  • El método del codo se basa en la suma de cuadrados intra-grupo, medida pensada para centroides (K-Means).
  • El dendrograma es propio de los métodos jerárquicos (Ward).
  • El gráfico de k-vecinos es la herramienta de DBSCAN para elegir Épsilon.
  • El coeficiente de silueta usa directamente las disimilitudes entre observaciones (compatible con Manhattan) y es la métrica estándar para particiones basadas en medoides.

Para cada observación \(i\):

\[s(i) = \frac{b(i) - a(i)}{\max\{a(i), b(i)\}}\]

donde \(a(i)\) es la distancia promedio de \(i\) a los demás miembros de su clúster (cohesión) y \(b(i)\) la distancia promedio al clúster vecino más cercano (separación). Valores cercanos a 1 indican buena asignación; cercanos a 0 o negativos, que la observación podría pertenecer a otro grupo. Se elige el k con mayor silueta promedio.

5. Ejemplo Práctico en R

5.1 Selección de países

Se seleccionaron 18 países que combinan economías latinoamericanas con economías avanzadas, para que el algoritmo tenga suficiente variabilidad y se pueda observar el efecto de posibles outliers.

library(dplyr)
library(tibble)
library(kableExtra)

paises_iso3 <- c(
  "SLV", "GTM", "HND", "NIC", "CRI", "PAN",   # Centroamérica
  "MEX", "COL", "PER", "CHL", "ARG", "BRA",   # Resto de Latinoamérica
  "USA", "CAN",                                # Norteamérica
  "ESP", "DEU", "FRA", "ITA"                   # Economías avanzadas (referencia)
)

nombres_paises <- c(
  "El Salvador", "Guatemala", "Honduras", "Nicaragua", "Costa Rica", "Panamá",
  "México", "Colombia", "Perú", "Chile", "Argentina", "Brasil",
  "Estados Unidos", "Canadá",
  "España", "Alemania", "Francia", "Italia"
)

Tabla_paises <- tibble(ISO3 = paises_iso3, Pais = nombres_paises)

Tabla_paises %>%
  kable(caption = "Países incluidos en el análisis de clustering") %>%
  kable_classic(html_font = "Times New Roman", font_size = 14) %>%
  add_footnote(label = "Elaboración propia", notation = "symbol") %>%
  kable_styling(full_width = TRUE)
Países incluidos en el análisis de clustering
ISO3 Pais
SLV El Salvador
GTM Guatemala
HND Honduras
NIC Nicaragua
CRI Costa Rica
PAN Panamá
MEX México
COL Colombia
PER Perú
CHL Chile
ARG Argentina
BRA Brasil
USA Estados Unidos
CAN Canadá
ESP España
DEU Alemania
FRA Francia
ITA Italia
* Elaboración propia

5.2 Carga de librerías

library(imf.data)
library(tidyr)
library(ggplot2)
library(cluster)      # PAM y silueta
library(factoextra)   # visualización de clusters y silueta

5.3 Estructura del dataset WEO e indicadores

Estructura_WEO <- list_dimensions("WEO", agency_id = "IMF.RES")

Estructura_WEO %>%
  kable(caption = "Estructura del dataset WEO en la API del FMI") %>%
  kable_classic(html_font = "Times New Roman", font_size = 14) %>%
  add_footnote(label = "Tomado de la API del FMI", notation = "symbol") %>%
  kable_styling()
Estructura del dataset WEO en la API del FMI
position id type name codelist_agency codelist_id codelist_version
0 COUNTRY Dimension Country IMF.RES CL_WEO_COUNTRY 1.0+.0
1 INDICATOR Dimension Indicator IMF.RES CL_WEO_INDICATOR 2.0+.0
2 FREQUENCY Dimension FREQUENCY NA NA NA
3 TIME_PERIOD TimeDimension TIME_PERIOD NA NA NA
* Tomado de la API del FMI
Indicadores_WEO <- list_dimension_values("WEO", "INDICATOR", agency_id = "IMF.RES")

Indicadores_WEO %>%
  filter(if_any(everything(), ~ .x %in% c("NGDP_RPCH", "PCPIPCH", "LUR"))) %>%
  kable(caption = "Indicadores del WEO utilizados") %>%
  kable_classic(html_font = "Times New Roman", font_size = 14) %>%
  kable_styling()
Indicadores del WEO utilizados
code name description
LUR Unemployment rate The percentage of the labor force that is unemployed and actively seeking employment. It is calculated as the number of unemployed persons divided by the total labor force. A measure that expresses the frequency of occurrence of an event or phenomenon in relation to another quantity.
NGDP_RPCH Gross domestic product (GDP), Constant prices, Percent change Gross domestic product reflects the total income earned through the production of goods and services in an economic territory during an accounting period. It can be measured in three different ways: using either the expenditure approach, the income approach, or the production approach. Values that have had the effect of price changes removed, expressed as time series in constant prices (fixed base) or chain-linked volume terms, or as non-time series in previous year’s prices This measures the relative change in a value over time, expressed as a percentage. It is calculated as the difference between the new value and the old value, divided by the old value, and multiplied by 100. 1 A ratio expressed as a fraction of 100.
PCPIPCH All Items, Consumer price index (CPI), Period average, percent change All items in the context of the consumer price index indicates that the index includes prices for all items included in the consumer price index basket. This includes prices related to food, beverages, clothing, housing, health, transport, communications, recreation, education, restaurants and hotels and other miscellanoues goods and services. The Consumer Price Index (CPI) is an index of the prices of consumption goods and services, as compared to a certain reference period. This calculates the change relative to the average value over the entire period, not just the starting point. This is often used when the data fluctuates significantly within the period, and the average provides a more representative value for comparison. A ratio expressed as a fraction of 100.

5.4 Variable 1: Crecimiento del PIB real (WEO)

Se descargan las tres variables del WEO en el mismo formato: un indicador, los 18 países, frecuencia anual, y se conserva el período de proyección 2027-2031 (ventana común a las tres series). Luego se calcula el promedio 2027-2031 por país, que resume su perfil proyectado.

Producto Interno Bruto a precios constantes, cambio porcentual (NGDP_RPCH).

PIB_WEO <- get_data(
  "WEO",
  agency_id = "IMF.RES",
  filters = list(
    INDICATOR = "NGDP_RPCH",
    COUNTRY = paises_iso3,
    FREQUENCY = "A"
  )
)

# Se filtran las proyecciones 2027-2031 y se calcula el promedio por país
PIB_prom <- PIB_WEO %>%
  mutate(TIME_PERIOD = as.numeric(TIME_PERIOD),
         OBS_VALUE = as.numeric(OBS_VALUE)) %>%
  filter(TIME_PERIOD >= 2027 & TIME_PERIOD <= 2031) %>%
  group_by(COUNTRY) %>%
  summarise(Crecimiento_PIB = mean(OBS_VALUE, na.rm = TRUE), .groups = "drop") %>%
  mutate(Crecimiento_PIB = ifelse(is.nan(Crecimiento_PIB), NA_real_, Crecimiento_PIB))

PIB_prom %>%
  mutate(Crecimiento_PIB = round(Crecimiento_PIB, 2)) %>%
  kable(caption = "PIB (crecimiento real, %): promedio proyectado 2027-2031") %>%
  kable_classic(html_font = "Times New Roman", font_size = 14) %>%
  add_footnote(label = "Tomado de la API del FMI (WEO)", notation = "symbol") %>%
  kable_styling()
PIB (crecimiento real, %): promedio proyectado 2027-2031
COUNTRY Crecimiento_PIB
ARG 3.50
BRA 2.36
CAN 1.74
CHL 2.35
COL 2.68
CRI 3.51
DEU 0.92
ESP 1.75
FRA 1.08
GTM 3.91
HND 3.78
ITA 0.70
MEX 2.07
NIC 3.58
PAN 4.58
PER 2.80
SLV 3.00
USA 1.92
* Tomado de la API del FMI (WEO)

5.5 Variable 2: Inflación (WEO)

Variación porcentual del Índice de Precios al Consumidor, promedio del período (PCPIPCH).

Inflacion_WEO <- get_data(
  "WEO",
  agency_id = "IMF.RES",
  filters = list(
    INDICATOR = "PCPIPCH",
    COUNTRY = paises_iso3,
    FREQUENCY = "A"
  )
)

# Se filtran las proyecciones 2027-2031 y se calcula el promedio por país
Inflacion_prom <- Inflacion_WEO %>%
  mutate(TIME_PERIOD = as.numeric(TIME_PERIOD),
         OBS_VALUE = as.numeric(OBS_VALUE)) %>%
  filter(TIME_PERIOD >= 2027 & TIME_PERIOD <= 2031) %>%
  group_by(COUNTRY) %>%
  summarise(Inflacion = mean(OBS_VALUE, na.rm = TRUE), .groups = "drop") %>%
  mutate(Inflacion = ifelse(is.nan(Inflacion), NA_real_, Inflacion))

Inflacion_prom %>%
  mutate(Inflacion = round(Inflacion, 2)) %>%
  kable(caption = "Inflación (IPC, %): promedio proyectado 2027-2031") %>%
  kable_classic(html_font = "Times New Roman", font_size = 14) %>%
  add_footnote(label = "Tomado de la API del FMI (WEO)", notation = "symbol") %>%
  kable_styling()
Inflación (IPC, %): promedio proyectado 2027-2031
COUNTRY Inflacion
ARG 9.54
BRA 3.07
CAN 2.02
CHL 3.07
COL 3.51
CRI 2.57
DEU 2.14
ESP 2.15
FRA 1.83
GTM 3.91
HND 4.10
ITA 2.13
MEX 3.08
NIC 3.00
PAN 2.00
PER 1.94
SLV 1.84
USA 2.18
* Tomado de la API del FMI (WEO)

5.6 Variable 3: Tasa de desempleo (WEO)

Tasa de desempleo como porcentaje de la fuerza laboral (LUR).

Desempleo_WEO <- get_data(
  "WEO",
  agency_id = "IMF.RES",
  filters = list(
    INDICATOR = "LUR",
    COUNTRY = paises_iso3,
    FREQUENCY = "A"
  )
)

# Se filtran las proyecciones 2027-2031 y se calcula el promedio por país
Desempleo_prom <- Desempleo_WEO %>%
  mutate(TIME_PERIOD = as.numeric(TIME_PERIOD),
         OBS_VALUE = as.numeric(OBS_VALUE)) %>%
  filter(TIME_PERIOD >= 2027 & TIME_PERIOD <= 2031) %>%
  group_by(COUNTRY) %>%
  summarise(Desempleo = mean(OBS_VALUE, na.rm = TRUE), .groups = "drop") %>%
  mutate(Desempleo = ifelse(is.nan(Desempleo), NA_real_, Desempleo))

Desempleo_prom %>%
  mutate(Desempleo = round(Desempleo, 2)) %>%
  kable(caption = "Desempleo (tasa, %): promedio proyectado 2027-2031") %>%
  kable_classic(html_font = "Times New Roman", font_size = 14) %>%
  add_footnote(label = "Tomado de la API del FMI (WEO)", notation = "symbol") %>%
  kable_styling()
Desempleo (tasa, %): promedio proyectado 2027-2031
COUNTRY Desempleo
ARG 6.58
BRA 7.39
CAN 6.14
CHL 7.42
COL 10.00
CRI 7.74
DEU 3.07
ESP 9.91
FRA 7.57
HND 6.56
ITA 6.02
MEX 2.78
NIC 2.76
PAN 7.76
PER 6.46
SLV 4.64
USA 4.00
* Tomado de la API del FMI (WEO)

5.7 Chequeo de los datos descargados

# Cada país debe aparecer una sola vez por variable
stopifnot(!anyDuplicated(PIB_prom$COUNTRY),
          !anyDuplicated(Inflacion_prom$COUNTRY),
          !anyDuplicated(Desempleo_prom$COUNTRY))

# Países sin dato proyectado en cada variable (se imputarán con la media)
faltantes <- tibble(
  Variable = c("Crecimiento_PIB", "Inflacion", "Desempleo"),
  `Países sin dato` = c(
    paste(setdiff(paises_iso3, PIB_prom$COUNTRY[!is.na(PIB_prom$Crecimiento_PIB)]), collapse = ", "),
    paste(setdiff(paises_iso3, Inflacion_prom$COUNTRY[!is.na(Inflacion_prom$Inflacion)]), collapse = ", "),
    paste(setdiff(paises_iso3, Desempleo_prom$COUNTRY[!is.na(Desempleo_prom$Desempleo)]), collapse = ", ")
  )
)

faltantes %>%
  kable(caption = "Países sin dato proyectado por variable") %>%
  kable_classic(html_font = "Times New Roman", font_size = 14) %>%
  kable_styling()
Países sin dato proyectado por variable
Variable Países sin dato
Crecimiento_PIB
Inflacion
Desempleo GTM

5.8 Construcción de la matriz de clustering

Siguiendo las recomendaciones técnicas de la tarea: los datos faltantes (NA) se sustituyen por la media de la variable y luego las tres variables se estandarizan con scale().

Datos_cluster <- Tabla_paises %>%
  left_join(PIB_prom,       by = c("ISO3" = "COUNTRY")) %>%
  left_join(Inflacion_prom, by = c("ISO3" = "COUNTRY")) %>%
  left_join(Desempleo_prom, by = c("ISO3" = "COUNTRY")) %>%
  select(Pais, Crecimiento_PIB, Inflacion, Desempleo)

# Tratamiento de datos faltantes: sustitución por la media de cada variable
Datos_cluster <- Datos_cluster %>%
  mutate(across(c(Crecimiento_PIB, Inflacion, Desempleo),
                ~ ifelse(is.na(.x), mean(.x, na.rm = TRUE), .x)))

# Un solo registro por país (evita el error de row.names duplicados)
stopifnot(!anyDuplicated(Datos_cluster$Pais))

Datos_cluster %>%
  mutate(across(where(is.numeric), ~ round(.x, 2))) %>%
  kable(caption = "Matriz de variables para clustering: promedio proyectado 2027-2031 (%)") %>%
  kable_classic(html_font = "Times New Roman", font_size = 14) %>%
  add_footnote(label = "Elaboración propia con datos del WEO (FMI)", notation = "symbol") %>%
  kable_styling()
Matriz de variables para clustering: promedio proyectado 2027-2031 (%)
Pais Crecimiento_PIB Inflacion Desempleo
El Salvador 3.00 1.84 4.64
Guatemala 3.91 3.91 6.28
Honduras 3.78 4.10 6.56
Nicaragua 3.58 3.00 2.76
Costa Rica 3.51 2.57 7.74
Panamá 4.58 2.00 7.76
México 2.07 3.08 2.78
Colombia 2.68 3.51 10.00
Perú 2.80 1.94 6.46
Chile 2.35 3.07 7.42
Argentina 3.50 9.54 6.58
Brasil 2.36 3.07 7.39
Estados Unidos 1.92 2.18 4.00
Canadá 1.74 2.02 6.14
España 1.75 2.15 9.91
Alemania 0.92 2.14 3.07
Francia 1.08 1.83 7.57
Italia 0.70 2.13 6.02
* Elaboración propia con datos del WEO (FMI)
# El clustering se basa en distancias: se estandarizan las variables con scale()
Datos_matriz <- Datos_cluster %>%
  select(Pais, Crecimiento_PIB, Inflacion, Desempleo) %>%
  column_to_rownames("Pais")

Datos_escalados <- scale(Datos_matriz)

# Comprobación: media ≈ 0 y desviación estándar = 1 en cada variable
round(colMeans(Datos_escalados), 6)
## Crecimiento_PIB       Inflacion       Desempleo 
##               0               0               0
apply(Datos_escalados, 2, sd)
## Crecimiento_PIB       Inflacion       Desempleo 
##               1               1               1

5.9 Número óptimo de clusters (coeficiente de silueta)

Tal como se justificó en la sección 4, el método correcto para PAM es el coeficiente de silueta. Se calcula la silueta promedio para k = 2, …, 8 y se elige el k con el valor máximo.

k_valores <- 2:8

silueta_k <- sapply(k_valores, function(k) {
  pam(Datos_escalados, k = k, metric = "manhattan")$silinfo$avg.width
})

Tabla_silueta <- tibble(k = k_valores, Silueta_promedio = silueta_k)

k_optimo <- Tabla_silueta$k[which.max(Tabla_silueta$Silueta_promedio)]

ggplot(Tabla_silueta, aes(x = k, y = Silueta_promedio)) +
  geom_line(color = "steelblue", linewidth = 1) +
  geom_point(size = 3, color = "steelblue") +
  geom_vline(xintercept = k_optimo, linetype = "dashed", color = "red") +
  scale_x_continuous(breaks = k_valores) +
  labs(title = "Coeficiente de silueta - número óptimo de clusters para PAM",
       subtitle = paste("k óptimo =", k_optimo),
       x = "Número de clusters (k)", y = "Silueta promedio") +
  theme_minimal()

“El análisis del coeficiente de silueta promedio para el algoritmo K-Medianas (PAM) muestra que la estructura de agrupación alcanza su máxima separación y cohesión en \(k = 8\) clusters (con un valor promedio de silueta de 0.36). Por lo tanto, k = 8 es el número óptimo de grupos para segmentar a los 18 países analizados según su perfil macroeconómico proyectado.”

5.10 K-Medianas / PAM

modelo_pam <- pam(Datos_escalados, k = k_optimo, metric = "manhattan")

Datos_cluster$Cluster_PAM <- as.factor(modelo_pam$clustering)

# Países que actúan como medoides (representantes reales de cada grupo)
Medoides <- rownames(modelo_pam$medoids)
Medoides
## [1] "Perú"      "Honduras"  "México"    "Panamá"    "España"    "Chile"    
## [7] "Argentina" "Italia"

El grupo 1 se representa con Perú: es el país más típico de ese grupo, el que mejor resume a los demás

# Con 3 variables, fviz_cluster proyecta los datos sobre los dos primeros
# componentes principales para poder graficar los grupos.
fviz_cluster(modelo_pam,
             data = Datos_escalados,
             ellipse.type = "convex",
             repel = TRUE,
             ggtheme = theme_minimal()) +
  labs(title = "Clustering PAM (K-Medianas) - Perfil macroeconómico proyectado 2027-2031",
       subtitle = paste("Medoides:", paste(Medoides, collapse = ", ")))

# Gráfico de silueta: qué tan bien encaja cada país en su clúster
fviz_silhouette(modelo_pam) +
  labs(title = "Gráfico de silueta - calidad de la partición PAM")
##   cluster size ave.sil.width
## 1       1    2          0.36
## 2       2    2          0.83
## 3       3    3          0.20
## 4       4    2          0.29
## 5       5    2          0.16
## 6       6    2          0.99
## 7       7    1          0.00
## 8       8    4          0.15

Este gráfico responde una pregunta: ¿cada país quedó bien ubicado en su grupo? Cada barra es un país. Si la barra es alta, el país encaja muy bien con sus compañeros. Si es baja, está en la frontera entre dos grupos. Si es negativa, probablemente estaría mejor en otro grupo

Perfil de cada clúster

Perfil <- Datos_cluster %>%
  group_by(Cluster_PAM) %>%
  summarise(N_paises = n(),
            across(c(Crecimiento_PIB, Inflacion, Desempleo), ~ round(median(.x), 2)),
            Paises = paste(Pais, collapse = ", "),
            .groups = "drop")

Perfil %>%
  kable(caption = "Perfil de cada clúster (mediana de cada variable, %)") %>%
  kable_classic(html_font = "Times New Roman", font_size = 14) %>%
  kable_styling()
Perfil de cada clúster (mediana de cada variable, %)
Cluster_PAM N_paises Crecimiento_PIB Inflacion Desempleo Paises
1 2 2.90 1.89 5.55 El Salvador, Perú
2 2 3.84 4.00 6.42 Guatemala, Honduras
3 3 2.07 3.00 2.78 Nicaragua, México, Estados Unidos
4 2 4.05 2.28 7.75 Costa Rica, Panamá
5 2 2.22 2.83 9.96 Colombia, España
6 2 2.36 3.07 7.41 Chile, Brasil
7 1 3.50 9.54 6.58 Argentina
8 4 1.00 2.07 6.08 Canadá, Alemania, Francia, Italia
Datos_cluster %>%
  pivot_longer(c(Crecimiento_PIB, Inflacion, Desempleo),
               names_to = "Variable", values_to = "Valor") %>%
  ggplot(aes(x = Cluster_PAM, y = Valor, fill = Cluster_PAM)) +
  geom_boxplot(alpha = 0.6, outlier.shape = 21) +
  facet_wrap(~ Variable, scales = "free_y") +
  labs(title = "Distribución de cada variable por clúster (proyección 2027-2031, %)",
       x = "Clúster", y = "%") +
  theme_minimal() +
  theme(legend.position = "none")

Este gráfico nos muestra cómo se comporta cada variable dentro de cada grupo. La línea gruesa es el valor típico y la caja muestra qué tan parecidos son los países entre sí. Se ve claro que el grupo 7, Argentina, se separa por su inflación, que el grupo 5 tiene el desempleo más alto y que el grupo 8 crece menos. Confirma que PAM formó los grupos según diferencias reales en las variables. Las cajas de los grupos pequeños son casi líneas porque tienen solo uno o dos países.

Crecimiento: el grupo 4 (Costa Rica, Panamá) es el más alto, cerca de 4%, y el grupo 8 (economías avanzadas) el más bajo, cerca de 1%. Desempleo: el grupo 5 (Colombia, España) es el más alto, casi 10%, y el grupo 3 (Nicaragua, México, EE. UU.) el más bajo, cerca de 3%. Inflación: el grupo 7 (Argentina) se separa del resto, con más de 9%. Los demás quedan entre 2% y 4%.

5.11 Medoides en la escala original

Los medoides seleccionados por PAM, expresados de vuelta en la escala original (%) en lugar de valores estandarizados:

media_x <- attr(Datos_escalados, "scaled:center")
sd_x    <- attr(Datos_escalados, "scaled:scale")

medoides_orig <- sweep(sweep(modelo_pam$medoids, 2, sd_x, "*"), 2, media_x, "+")

Representantes_PAM <- as.data.frame(round(medoides_orig, 2)) %>%
  rownames_to_column("Medoide (país real)")

Representantes_PAM %>%
  kable(caption = "PAM: medoides (países reales), en % de la escala original") %>%
  kable_classic(html_font = "Times New Roman", font_size = 14) %>%
  kable_styling()
PAM: medoides (países reales), en % de la escala original
Medoide (país real) Crecimiento_PIB Inflacion Desempleo
Perú 2.80 1.94 6.46
Honduras 3.78 4.10 6.56
México 2.07 3.08 2.78
Panamá 4.58 2.00 7.76
España 1.75 2.15 9.91
Chile 2.35 3.07 7.42
Argentina 3.50 9.54 6.58
Italia 0.70 2.13 6.02

Los ocho medoides, expresados en su escala original, muestran perfiles macroeconómicos claramente distintos: desde economías de alto crecimiento y baja inflación como Panamá, hasta casos extremos como Argentina por su inflación cercana al 10%, España por su desempleo cercano al 10% e Italia por su crecimiento casi nulo.

5.12 Sensibilidad: ¿qué pasa sin el país más extremo?

Se identifica el país con el valor estandarizado más extremo en cualquiera de las tres variables y se repite el análisis sin él, para ver si la partición cambia.

pais_extremo <- rownames(Datos_escalados)[which.max(apply(abs(Datos_escalados), 1, max))]

Datos_sin <- Datos_matriz[rownames(Datos_matriz) != pais_extremo, , drop = FALSE]
Datos_sin_esc <- scale(Datos_sin)

silueta_sin <- sapply(2:8, function(k) {
  pam(Datos_sin_esc, k = k, metric = "manhattan")$silinfo$avg.width
})
k_sin <- (2:8)[which.max(silueta_sin)]

pam_sin <- pam(Datos_sin_esc, k = k_sin, metric = "manhattan")

cat("País excluido:", pais_extremo, "| k óptimo sin ese país:", k_sin,
    "| Medoides:", paste(rownames(pam_sin$medoids), collapse = ", "))
## País excluido: Argentina | k óptimo sin ese país: 5 | Medoides: Canadá, Guatemala, México, Costa Rica, Brasil
Resultado_sin <- Datos_sin %>%
  rownames_to_column("Pais") %>%
  mutate(across(where(is.numeric), ~ round(.x, 2)),
         Cluster = as.factor(pam_sin$clustering)) %>%
  arrange(Cluster)

Resultado_sin %>%
  kable(caption = paste("PAM sin", pais_extremo)) %>%
  kable_classic(html_font = "Times New Roman", font_size = 14) %>%
  kable_styling()
PAM sin Argentina
Pais Crecimiento_PIB Inflacion Desempleo Cluster
El Salvador 3.00 1.84 4.64 1
Perú 2.80 1.94 6.46 1
Estados Unidos 1.92 2.18 4.00 1
Canadá 1.74 2.02 6.14 1
España 1.75 2.15 9.91 1
Alemania 0.92 2.14 3.07 1
Francia 1.08 1.83 7.57 1
Italia 0.70 2.13 6.02 1
Guatemala 3.91 3.91 6.28 2
Honduras 3.78 4.10 6.56 2
Nicaragua 3.58 3.00 2.76 3
México 2.07 3.08 2.78 3
Costa Rica 3.51 2.57 7.74 4
Panamá 4.58 2.00 7.76 4
Colombia 2.68 3.51 10.00 5
Chile 2.35 3.07 7.42 5
Brasil 2.36 3.07 7.39 5

5.13 Limitaciones por tratarse de proyecciones

  • Las proyecciones tienden a ser más suaves que los datos observados y rara vez anticipan crisis, lo que puede reducir la presencia de outliers.
  • Se usó el promedio 2027-2031 como resumen; usar un solo año podría arrojar agrupaciones distintas.

6. Interpretación

for (g in sort(unique(Datos_cluster$Cluster_PAM))) {
  m <- Datos_cluster %>% filter(Cluster_PAM == g)
  cat(sprintf(
    "- **Clúster %s** (medoide: **%s**): %d país(es) — %s. Medianas: PIB %.1f%%, inflación %.1f%%, desempleo %.1f%%.\n",
    g, Medoides[as.integer(g)], nrow(m), paste(m$Pais, collapse = ", "),
    median(m$Crecimiento_PIB), median(m$Inflacion), median(m$Desempleo)))
}
  • Clúster 1 (medoide: Perú): 2 país(es) — El Salvador, Perú. Medianas: PIB 2.9%, inflación 1.9%, desempleo 5.6%.
  • Clúster 2 (medoide: Honduras): 2 país(es) — Guatemala, Honduras. Medianas: PIB 3.8%, inflación 4.0%, desempleo 6.4%.
  • Clúster 3 (medoide: México): 3 país(es) — Nicaragua, México, Estados Unidos. Medianas: PIB 2.1%, inflación 3.0%, desempleo 2.8%.
  • Clúster 4 (medoide: Panamá): 2 país(es) — Costa Rica, Panamá. Medianas: PIB 4.0%, inflación 2.3%, desempleo 7.8%.
  • Clúster 5 (medoide: España): 2 país(es) — Colombia, España. Medianas: PIB 2.2%, inflación 2.8%, desempleo 10.0%.
  • Clúster 6 (medoide: Chile): 2 país(es) — Chile, Brasil. Medianas: PIB 2.4%, inflación 3.1%, desempleo 7.4%.
  • Clúster 7 (medoide: Argentina): 1 país(es) — Argentina. Medianas: PIB 3.5%, inflación 9.5%, desempleo 6.6%.
  • Clúster 8 (medoide: Italia): 4 país(es) — Canadá, Alemania, Francia, Italia. Medianas: PIB 1.0%, inflación 2.1%, desempleo 6.1%.
cat(sprintf("\n- **Silueta promedio** de la partición PAM: **%.3f** (cercana a 1 = grupos bien definidos; cercana a 0 = grupos poco diferenciados).\n",
            modelo_pam$silinfo$avg.width))
  • Silueta promedio de la partición PAM: 0.358 (cercana a 1 = grupos bien definidos; cercana a 0 = grupos poco diferenciados).
cat(sprintf("- **País más extremo** en las variables estandarizadas: %s.\n", pais_extremo))
  • País más extremo en las variables estandarizadas: Argentina.

Con \(k = 8\) (óptimo según la silueta) se observan ocho perfiles, varios de ellos con solo 1-2 países, lo cual es esperable con una muestra de 18 observaciones:

  • Clúster 1 (medoide: Perú) — El Salvador y Perú: crecimiento moderado (~2.9%), la inflación más baja del grupo (~1.9%) y desempleo moderado (~5.6%). Es el perfil de “estabilidad de precios” dentro de la muestra.
  • Clúster 2 (medoide: Honduras) — Guatemala y Honduras: crecimiento relativamente alto (~3.8%) pero también la inflación más alta entre los países centroamericanos “normales” (~4.0%), con desempleo medio (~6.4%).
  • Clúster 3 (medoide: México) — Nicaragua, México y Estados Unidos: el rasgo que los une es el desempleo muy bajo (~2.8%), pese a ser economías muy distintas en tamaño; el algoritmo los agrupa por ese único rasgo compartido, no por similitud estructural completa.
  • Clúster 4 (medoide: Panamá) — Costa Rica y Panamá: el mayor crecimiento proyectado (~4.0%) combinado con el desempleo más alto de Centroamérica (~7.8%) — un perfil de “crecimiento sin absorción de empleo”.
  • Clúster 5 (medoide: España) — Colombia y España: desempleo estructural muy alto (~10%), el más elevado de toda la muestra, con crecimiento e inflación moderados.
  • Clúster 6 (medoide: Chile) — Chile y Brasil: perfil intermedio en las tres variables, sin rasgos extremos.
  • Clúster 7 (medoide: Argentina) — Argentina sola: aislada por su inflación proyectada de 9.5%, muy por encima de cualquier otro país de la muestra. Es el clúster que confirma empíricamente la justificación de la sección 1.2: PAM no distorsiona a los demás grupos por este valor extremo, simplemente lo aísla en su propio clúster.
  • Clúster 8 (medoide: Italia) — Canadá, Alemania, Francia e Italia: el grupo de economías avanzadas de bajo crecimiento (~1.0%), con inflación y desempleo moderados — el perfil típico de economía madura en el WEO.

¿Dónde queda El Salvador? Se agrupa con Perú (Clúster 1): ambos comparten el rasgo de tener la inflación proyectada más baja de la muestra, más que un parecido en crecimiento o desempleo.

Silueta promedio de 0.358: según la escala habitual de Kaufman y Rousseeuw, esto indica una estructura moderada/débil — los clústeres son identificables, pero no están fuertemente separados. Esto es consistente con que \(k=8\) sobre 18 países produce grupos muy pequeños (varios de 1-2 miembros), lo que infla artificialmente la silueta de esos pares muy parecidos sin que necesariamente exista una separación económica profunda entre todos los ocho grupos.

¿Por qué el medoide es un buen representante? El caso de Argentina lo demuestra directamente: su inflación de 9.5% es casi el triple que la del país más cercano. Un centroide (promedio artificial) se habría desplazado hacia ese valor extremo, distorsionando la posición de todo el grupo. Con PAM, Argentina simplemente es su propio medoide — un país real, no un promedio inventado — por lo que ningún otro clúster absorbe esa distorsión.

Sensibilidad al quitar el país más extremo (sección 5.12): al excluir a Argentina, el \(k\) óptimo según la silueta baja de 8 a 5, con medoides Canadá, Guatemala, México, Costa Rica y Brasil. Esto confirma que buena parte de la fragmentación en 8 grupos pequeños estaba ligada a la presencia de ese outlier: al quitarlo, la estructura “natural” de la muestra es más compacta (5 grupos), reforzando la idea de que Argentina no solo es un caso aislado dentro de la partición con \(k=8\), sino que también afecta cuántos grupos parecen “óptimos” en primer lugar.

7. Conclusiones

La aplicación de PAM a los datos del WEO (FMI) confirma, con evidencia empírica y no solo teórica, las ventajas que se argumentaron en la sección 1.2:

  1. Robustez ante outliers, comprobada: Argentina —con una inflación proyectada de 9.5%, muy por encima del resto— quedó aislada en su propio clúster sin deformar a los demás grupos, pero el análisis de sensibilidad (5.12) mostró que su sola presencia alteraba el número “óptimo” de clústeres de 5 a 8. Esto ilustra un matiz importante: PAM es robusto en cómo asigna las observaciones (el medoide no se deforma), pero un outlier fuerte puede seguir afectando cuántos clústeres sugiere el coeficiente de silueta. Vale la pena que el analista revise ambos escenarios (con y sin el caso extremo) antes de fijar un \(k\) definitivo.

  2. El número de clústeres elegido (k = 8) es estadísticamente óptimo, pero económicamente sobredimensionado para 18 países: produce grupos de 1 a 4 países y una silueta promedio moderada (0.358). La versión sin Argentina, con \(k=5\), probablemente describe mejor la estructura económica real de la región (centroamericanos de mayor crecimiento, sudamericanos de mayor inflación/desempleo, economías avanzadas de bajo crecimiento), y sería la partición recomendada para presentar en la exposición si se busca una interpretación económica más clara y comunicable.

  3. Consistencia con la intuición económica: el resultado sí tiene sentido económico — las economías avanzadas (Canadá, Alemania, Francia, Italia) se agrupan por su bajo crecimiento estructural; Costa Rica y Panamá comparten el patrón de “crecer sin generar suficiente empleo”; y España y Colombia comparten desempleo estructural elevado pese a estar en continentes distintos, lo que confirma que el clustering captura similitud macroeconómica, no geográfica.

Advertencia necesaria: estos resultados describen perfiles proyectados por el FMI (WEO, promedio 2027-2031), no datos observados ni un pronóstico de qué países convergerán económicamente. Los grupos deben leerse como “estos países comparten un perfil macroeconómico esperado según el FMI a esa fecha”, sujeto a revisión en cada nueva edición del WEO.

8. Clustering Jerárquico Aglomerativo (Método de Ward) - API del Banco Mundial

Hasta aquí se aplicó un método no jerárquico (PAM) a proyecciones macroeconómicas del FMI. En esta segunda parte se aplica un método jerárquico aglomerativo (Ward) a una batería de cinco indicadores estructurales (económico, ambiental, desigualdad, salud y educación) obtenidos de la API del Banco Mundial mediante el paquete wbstats, para los mismos 18 países. Ambos enfoques se consideran complementarios más que competitivos: el jerárquico revela la estructura anidada de similitudes y sugiere cuántos grupos hay, y el particional los ajusta y los representa con países reales.

8.1 ¿En qué consiste el método de Ward y por qué encaja con estos datos?

Ward es un método jerárquico aglomerativo: parte con cada país como un clúster individual y, en cada paso, fusiona los dos clústeres cuya unión provoca el menor aumento de la varianza intra-grupo. El resultado es un árbol (dendrograma) donde los países más parecidos se unen primero y los más distintos se unen al final. A diferencia de PAM, el número de grupos no se fija de antemano: se decide después, cortando el árbol. Y una fusión es irrevocable: un país que se unió a un grupo no puede reasignarse.

Encaja con los indicadores del Banco Mundial porque son variables continuas que describen una gradación de desarrollo (esperanza de vida, gasto en educación, desigualdad, emisiones, deuda). El dendrograma muestra esa gradación como una jerarquía de niveles de similitud, y Ward tiende a producir grupos compactos y de tamaño equilibrado. Su contrapartida frente a PAM es que, al basarse en varianzas, es más sensible a valores atípicos.

8.2 Explicación matemática (intuitiva)

Ward trabaja con la suma de cuadrados intra-grupo (ESS), es decir, cuánto se dispersan los países alrededor del promedio de su clúster:

\[\text{ESS} = \sum_{i=1}^{k} \sum_{x \in C_i} \lVert x - \bar{x}_i \rVert^2\]

Al fusionar dos clústeres \(A\) y \(B\), el ESS aumenta en:

\[\Delta(A,B) = \frac{n_A \, n_B}{n_A + n_B} \, \lVert \bar{x}_A - \bar{x}_B \rVert^2\]

donde \(n_A, n_B\) son los tamaños y \(\bar{x}_A, \bar{x}_B\) los promedios de cada clúster. En cada paso se fusiona el par con el \(\Delta\) más pequeño. La altura en el dendrograma es el costo de esa fusión: fusiones a baja altura unen países muy parecidos, y saltos grandes indican que se están juntando grupos muy distintos.

Nota de implementación en R: hclust(..., method = "ward.D2") aplica este criterio directamente sobre la distancia euclídea. Es equivalente a la forma clásica hclust(dist(x)^2, method = "ward.D").

8.3 Selección del número de clústeres

Para Ward el método que corresponde es el dendrograma: se busca la altura de corte donde comienzan a aparecer saltos bruscos entre fusiones sucesivas. Como el mayor salto casi siempre ocurre en la última fusión (que separa solo 2 grupos), se busca el mayor salto entre 3 y 6 grupos. Como validación complementaria se reporta el coeficiente de silueta y la correlación cofenética.

8.4 Indicadores del Banco Mundial

Se utilizan los cinco indicadores de la sección de indicadores del Banco Mundial:

Dimensión Indicador Código
Económico Deuda del gobierno central (% del PIB) GC.DOD.TOTL.GD.ZS
Ambiental Emisiones de CO2 per cápita (t) EN.GHG.CO2.PC.CE.AR5
Desigualdad Índice de Gini SI.POV.GINI
Salud Esperanza de vida al nacer (años) SP.DYN.LE00.IN
Educación Gasto en educación (% del PIB) SE.XPD.TOTL.GD.ZS
library(wbstats)

ind_bm <- c(
  Deuda_gobierno  = "GC.DOD.TOTL.GD.ZS",
  CO2_percapita   = "EN.GHG.CO2.PC.CE.AR5",
  Gini            = "SI.POV.GINI",
  Esperanza_vida  = "SP.DYN.LE00.IN",
  Gasto_educacion = "SE.XPD.TOTL.GD.ZS"
)
vars_bm <- names(ind_bm)

# Los nombres del vector pasan a ser los nombres de las columnas
BM_datos <- wb_data(
  indicator = ind_bm,
  country = paises_iso3,
  start_date = 2000,
  end_date = 2023,
  return_wide = TRUE
)

8.5 Selección de un año común

El Gini y el gasto en educación no se publican todos los años para todos los países. Para que la comparación sea consistente se usa un único año en el que los cinco indicadores estén disponibles para los países. Primero se revisa cuántos indicadores tiene cada año, y luego se busca, por código, el año más reciente con datos completos.

# Cuántos países tienen dato en cada indicador, por año (años más recientes)
BM_datos %>%
  group_by(date) %>%
  summarise(across(all_of(vars_bm), ~ sum(!is.na(.x))), .groups = "drop") %>%
  arrange(desc(date)) %>%
  head(10) %>%
  kable(caption = "Países con dato por indicador y año (máximo = 18)") %>%
  kable_classic(html_font = "Times New Roman", font_size = 14) %>%
  add_footnote(label = "Tomado de la API del Banco Mundial", notation = "symbol") %>%
  kable_styling()
Países con dato por indicador y año (máximo = 18)
date Deuda_gobierno CO2_percapita Gini Esperanza_vida Gasto_educacion
2023 7 18 13 18 8
2022 7 18 14 18 16
2021 8 18 13 18 17
2020 8 18 13 18 18
2019 8 18 14 18 17
2018 7 18 15 18 18
2017 7 18 15 18 18
2016 7 18 15 18 17
2015 7 18 14 18 17
2014 7 18 17 18 16
* Tomado de la API del Banco Mundial
# Para cada año: cuántos países tienen los 5 indicadores a la vez
Disponibilidad <- BM_datos %>%
  filter(if_all(all_of(vars_bm), ~ !is.na(.x))) %>%
  count(date, name = "Paises_completos") %>%
  arrange(desc(date))

stopifnot(nrow(Disponibilidad) > 0)

n_paises <- length(paises_iso3)
anios_completos <- Disponibilidad %>% filter(Paises_completos == n_paises)

if (nrow(anios_completos) > 0) {
  anio_bm <- max(anios_completos$date)
  cat("Año común con los 5 indicadores disponibles para los", n_paises, "países:", anio_bm)
} else {
  anio_bm <- Disponibilidad$date[which.max(Disponibilidad$Paises_completos)]
  cat("Ningún año tiene los 5 indicadores para los", n_paises, "países.\n",
      "Se usa el año", anio_bm, "con más países completos:",
      max(Disponibilidad$Paises_completos), "de", n_paises)
}
## Ningún año tiene los 5 indicadores para los 18 países.
##  Se usa el año 2020 con más países completos: 7 de 18
BM_anio <- BM_datos %>% filter(date == anio_bm)

# Países a los que les falta algún indicador en el año elegido
Faltantes_bm <- BM_anio %>%
  filter(if_any(all_of(vars_bm), is.na)) %>%
  select(iso3c, all_of(vars_bm))

if (nrow(Faltantes_bm) > 0) {
  Faltantes_bm %>%
    kable(caption = paste("Países con algún indicador faltante en", anio_bm)) %>%
    kable_classic(html_font = "Times New Roman", font_size = 14) %>%
    kable_styling()
} else {
  cat("Todos los países tienen los 5 indicadores en", anio_bm)
}
Países con algún indicador faltante en 2020
iso3c Deuda_gobierno CO2_percapita Gini Esperanza_vida Gasto_educacion
ARG NA 3.7200285 42.7 75.87800 5.276900
CHL NA 4.5177068 47.0 79.34900 5.629920
CRI NA 1.4252968 49.2 79.72500 6.628990
DEU NA 7.7210326 32.5 81.04146 5.516010
FRA NA 4.2467764 30.7 82.17561 5.658710
GTM NA 1.0150585 NA 69.97100 3.294850
HND NA 0.9112972 NA 70.86400 6.098013
ITA NA 4.9666741 35.2 82.19512 4.418040
NIC NA 0.7229257 NA 70.76600 4.580341
PAN NA 2.2623130 NA 76.33000 4.304870
SLV 70.38428 1.0671771 NA 70.24400 3.660880

8.6 Construcción y estandarización de la matriz

Si algún país no tiene los cinco indicadores en el año elegido, por defecto se excluye (na.omit). Si se prefiere conservar los 18 países, se puede cambiar imputar_faltantes a TRUE para sustituir los faltantes por la media, como permite el PDF de la tarea.

imputar_faltantes <- FALSE

Datos_bm <- BM_anio %>%
  select(iso3c, all_of(vars_bm)) %>%
  left_join(Tabla_paises, by = c("iso3c" = "ISO3")) %>%
  select(Pais, all_of(vars_bm))

if (imputar_faltantes) {
  Datos_bm <- Datos_bm %>%
    mutate(across(all_of(vars_bm), ~ ifelse(is.na(.x), mean(.x, na.rm = TRUE), .x)))
} else {
  Datos_bm <- na.omit(Datos_bm)
}

stopifnot(!anyDuplicated(Datos_bm$Pais), nrow(Datos_bm) >= 6)

Datos_bm %>%
  mutate(across(where(is.numeric), ~ round(.x, 2))) %>%
  kable(caption = paste("Indicadores del Banco Mundial por país, año", anio_bm, "-", nrow(Datos_bm), "países")) %>%
  kable_classic(html_font = "Times New Roman", font_size = 14) %>%
  add_footnote(label = "Elaboración propia con datos del Banco Mundial", notation = "symbol") %>%
  kable_styling()
Indicadores del Banco Mundial por país, año 2020 - 7 países
Pais Deuda_gobierno CO2_percapita Gini Esperanza_vida Gasto_educacion
Brasil 98.71 2.15 48.8 74.51 5.77
Canadá 74.55 14.42 29.9 81.53 4.89
Colombia 91.16 1.67 54.4 74.76 5.26
España 138.96 4.53 34.9 82.23 4.85
México 45.84 3.46 44.6 70.45 4.50
Perú 34.76 1.45 43.7 73.83 4.26
Estados Unidos 124.51 13.47 40.0 76.98 5.40
* Elaboración propia con datos del Banco Mundial

Los cinco indicadores tienen unidades distintas (porcentajes, toneladas, años, un índice), por lo que estandarizar con scale() es aquí imprescindible:

Datos_bm_mat <- Datos_bm %>% column_to_rownames("Pais")
Datos_bm_esc <- scale(Datos_bm_mat)

round(colMeans(Datos_bm_esc), 6)
##  Deuda_gobierno   CO2_percapita            Gini  Esperanza_vida Gasto_educacion 
##               0               0               0               0               0
apply(Datos_bm_esc, 2, sd)
##  Deuda_gobierno   CO2_percapita            Gini  Esperanza_vida Gasto_educacion 
##               1               1               1               1               1

8.7 Matriz de distancias y método de Ward

dist_bm <- dist(Datos_bm_esc, method = "euclidean")
hc_ward <- hclust(dist_bm, method = "ward.D2")
hc_ward
## 
## Call:
## hclust(d = dist_bm, method = "ward.D2")
## 
## Cluster method   : ward.D2 
## Distance         : euclidean 
## Number of objects: 7

Historial de fusiones (los números negativos son países individuales y los positivos son clústeres formados en pasos anteriores):

head(hc_ward$merge, 8)
##      [,1] [,2]
## [1,]   -5   -6
## [2,]   -1   -3
## [3,]   -2   -7
## [4,]   -4    3
## [5,]    1    2
## [6,]    4    5

8.8 Dendrograma y número de clústeres

# Alturas de fusión ordenadas de mayor a menor: alturas[i] es el costo de
# pasar de i+1 a i grupos. saltos[k-1] = alturas[k-1] - alturas[k] mide qué tan
# "costoso" es pasar de k a k-1 grupos frente a pasar de k+1 a k.
alturas <- sort(hc_ward$height, decreasing = TRUE)
saltos  <- alturas[-length(alturas)] - alturas[-1]

# Se busca entre 3 y 6 grupos para evitar la partición trivial en 2
k_candidatos <- 3:min(6, length(saltos) + 1)
k_ward <- k_candidatos[which.max(saltos[k_candidatos - 1])]

k_graf <- 2:min(8, length(saltos) + 1)
Tabla_saltos <- tibble(k = k_graf, Salto = saltos[k_graf - 1])

ggplot(Tabla_saltos, aes(x = k, y = Salto, fill = k == k_ward)) +
  geom_col() +
  scale_x_continuous(breaks = k_graf) +
  scale_fill_manual(values = c(`TRUE` = "firebrick", `FALSE` = "grey70"), guide = "none") +
  labs(title = "Saltos entre fusiones sucesivas (gráfico de paso o distancia)",
       subtitle = paste("Mayor salto entre 3 y 6 grupos: k =", k_ward),
       x = "Número de clústeres (k)", y = "Salto de altura") +
  theme_minimal()

plot(hc_ward, hang = -1, cex = 0.8,
     main = paste("Dendrograma - Método de Ward, indicadores del Banco Mundial", anio_bm),
     xlab = "", sub = "")
rect.hclust(hc_ward, k = k_ward, border = 2:(k_ward + 1))

8.9 Asignación de países y perfil de cada clúster

grupos_ward <- cutree(hc_ward, k = k_ward)

Datos_bm_cl <- Datos_bm %>%
  mutate(Cluster_Ward = factor(grupos_ward[Pais]))

Perfil_ward <- Datos_bm_cl %>%
  group_by(Cluster_Ward) %>%
  summarise(N_paises = n(),
            across(all_of(vars_bm), ~ round(median(.x), 2)),
            Paises = paste(Pais, collapse = ", "),
            .groups = "drop")

Perfil_ward %>%
  kable(caption = "Perfil de cada clúster de Ward (mediana de cada indicador)") %>%
  kable_classic(html_font = "Times New Roman", font_size = 14) %>%
  kable_styling()
Perfil de cada clúster de Ward (mediana de cada indicador)
Cluster_Ward N_paises Deuda_gobierno CO2_percapita Gini Esperanza_vida Gasto_educacion Paises
1 2 94.93 1.91 51.60 74.63 5.52 Brasil, Colombia
2 3 124.51 13.47 34.90 81.53 4.89 Canadá, España, Estados Unidos
3 2 40.30 2.45 44.15 72.14 4.38 México, Perú
Datos_bm_cl %>%
  pivot_longer(all_of(vars_bm), names_to = "Indicador", values_to = "Valor") %>%
  ggplot(aes(x = Cluster_Ward, y = Valor, fill = Cluster_Ward)) +
  geom_boxplot(alpha = 0.6, outlier.shape = 21) +
  facet_wrap(~ Indicador, scales = "free_y") +
  labs(title = paste("Distribución de cada indicador por clúster de Ward,", anio_bm),
       x = "Clúster", y = "Valor") +
  theme_minimal() +
  theme(legend.position = "none")

# Proyección sobre los dos primeros componentes principales
fviz_cluster(list(data = Datos_bm_esc, cluster = grupos_ward),
             ellipse.type = "convex", repel = TRUE, ggtheme = theme_minimal()) +
  labs(title = paste("Clustering jerárquico (Ward) - Banco Mundial", anio_bm))

8.10 Validación de la partición

# Silueta promedio para distintos k
k_vals <- 2:min(8, nrow(Datos_bm_esc) - 1)
Silueta_ward <- tibble(
  k = k_vals,
  Silueta_promedio = sapply(k_vals, function(k)
    mean(silhouette(cutree(hc_ward, k = k), dist_bm)[, "sil_width"]))
)

Silueta_ward %>%
  mutate(Silueta_promedio = round(Silueta_promedio, 3)) %>%
  kable(caption = "Silueta promedio de Ward según el número de clústeres") %>%
  kable_classic(html_font = "Times New Roman", font_size = 14) %>%
  kable_styling()
Silueta promedio de Ward según el número de clústeres
k Silueta_promedio
2 0.341
3 0.459
4 0.366
5 0.336
6 0.172
# Silueta de la partición elegida
sil_ward <- silhouette(grupos_ward, dist_bm)
fviz_silhouette(sil_ward) +
  labs(title = paste("Gráfico de silueta - Ward con k =", k_ward))
##   cluster size ave.sil.width
## 1       1    2          0.58
## 2       2    3          0.26
## 3       3    2          0.64

# Correlación cofenética: qué tan fiel es el dendrograma a las distancias originales
cor_cofenetica <- cor(dist_bm, cophenetic(hc_ward))

# Coeficiente aglomerativo (cerca de 1 = estructura de grupos marcada)
coef_aglomerativo <- agnes(Datos_bm_esc, method = "ward")$ac

tibble(Medida = c("Silueta promedio (k elegido)", "Correlación cofenética", "Coeficiente aglomerativo"),
       Valor = round(c(mean(sil_ward[, "sil_width"]), cor_cofenetica, coef_aglomerativo), 3)) %>%
  kable(caption = "Medidas de validación del clustering de Ward") %>%
  kable_classic(html_font = "Times New Roman", font_size = 14) %>%
  kable_styling()
Medidas de validación del clustering de Ward
Medida Valor
Silueta promedio (k elegido) 0.459
Correlación cofenética 0.877
Coeficiente aglomerativo 0.699

8.11 Contraste con PAM

Los grupos de PAM (variables proyectadas del FMI, 2027-2031) y los de Ward (indicadores estructurales del Banco Mundial) se construyen con variables distintas, por lo que no tienen por qué coincidir. Contrastarlos permite ver si los países se agrupan de forma parecida en el perfil macroeconómico esperado y en el perfil estructural.

Datos_bm_cl %>%
  select(Pais, Cluster_Ward) %>%
  left_join(Datos_cluster %>% select(Pais, Cluster_PAM), by = "Pais") %>%
  arrange(Cluster_Ward, Cluster_PAM) %>%
  kable(caption = "Clúster de cada país: Ward (Banco Mundial) vs. PAM (FMI)") %>%
  kable_classic(html_font = "Times New Roman", font_size = 14) %>%
  kable_styling()
Clúster de cada país: Ward (Banco Mundial) vs. PAM (FMI)
Pais Cluster_Ward Cluster_PAM
Colombia 1 5
Brasil 1 6
Estados Unidos 2 3
España 2 5
Canadá 2 8
Perú 3 1
México 3 3

8.12 Interpretación del clustering de Ward

cat(sprintf("Con los indicadores del Banco Mundial del año **%s** (%d países), el dendrograma sugiere **k = %d** clústeres.\n\n",
            anio_bm, nrow(Datos_bm_cl), k_ward))

Con los indicadores del Banco Mundial del año 2020 (7 países), el dendrograma sugiere k = 3 clústeres.

for (g in levels(Datos_bm_cl$Cluster_Ward)) {
  m <- Datos_bm_cl %>% filter(Cluster_Ward == g)
  cat(sprintf(
    "- **Clúster %s** (%d país(es)): %s. Medianas: deuda %.1f%% del PIB, CO2 %.1f t per cápita, Gini %.1f, esperanza de vida %.1f años, gasto en educación %.1f%% del PIB.\n",
    g, nrow(m), paste(m$Pais, collapse = ", "),
    median(m$Deuda_gobierno), median(m$CO2_percapita), median(m$Gini),
    median(m$Esperanza_vida), median(m$Gasto_educacion)))
}
  • Clúster 1 (2 país(es)): Brasil, Colombia. Medianas: deuda 94.9% del PIB, CO2 1.9 t per cápita, Gini 51.6, esperanza de vida 74.6 años, gasto en educación 5.5% del PIB.
  • Clúster 2 (3 país(es)): Canadá, España, Estados Unidos. Medianas: deuda 124.5% del PIB, CO2 13.5 t per cápita, Gini 34.9, esperanza de vida 81.5 años, gasto en educación 4.9% del PIB.
  • Clúster 3 (2 país(es)): México, Perú. Medianas: deuda 40.3% del PIB, CO2 2.5 t per cápita, Gini 44.2, esperanza de vida 72.1 años, gasto en educación 4.4% del PIB.
cat(sprintf("\n- **Silueta promedio:** %.3f. **Correlación cofenética:** %.3f. **Coeficiente aglomerativo:** %.3f.\n",
            mean(sil_ward[, "sil_width"]), cor_cofenetica, coef_aglomerativo))
  • Silueta promedio: 0.459. Correlación cofenética: 0.877. Coeficiente aglomerativo: 0.699.

(Completar con el análisis del equipo: ¿qué perfil de desarrollo caracteriza a cada grupo (desigualdad, salud, educación, deuda, emisiones)? ¿Dónde queda El Salvador y con qué países se agrupa? ¿Algún país queda aislado por un valor extremo, por ejemplo en emisiones o deuda? ¿Coinciden los grupos con los de PAM?)

8.13 Conclusiones del método jerárquico

(Completar: qué tan clara resultó la estructura jerárquica según el dendrograma y las medidas de validación, qué ventajas mostró Ward frente a PAM en este ejemplo (visión anidada de las similitudes, no requiere fijar k de antemano) y qué limitaciones (fusiones irrevocables, sensibilidad a valores atípicos), y qué indica el año elegido: los resultados describen la situación de ese año, no una tendencia.)

9. Tarea de Comprobación

Cinco preguntas para los compañeros de clase (3 teóricas/conceptuales y 2 prácticas):

  1. (Teórica) ¿Por qué PAM es un método de clustering no jerárquico y qué diferencia hay entre la asignación de grupos en un método jerárquico (como Ward) y en uno particional (como PAM)?
  2. (Teórica) ¿Cuál es la diferencia entre un centroide (K-Means) y un medoide (PAM), y por qué esa diferencia hace a PAM más robusto ante valores atípicos?
  3. (Conceptual) De las métricas para elegir el número de clústeres (codo, silueta, dendrograma, gráfico de k-vecinos), ¿cuál corresponde a PAM y por qué las otras tres no aplican de la misma manera?
  4. (Práctica) Aunque las tres variables están en porcentajes, ¿por qué es obligatorio aplicar scale() antes de correr PAM? ¿Qué variable dominaría las distancias si no se estandarizara?
  5. (Práctica) En el gráfico de silueta de la sección 5.10, ¿qué significa que un país tenga un ancho de silueta negativo o cercano a cero? ¿Qué podría hacer el analista al respecto?

Recursos y documentación

Paquete de R (CRAN): imf.data — https://cran.r-project.org/web/packages/imf.data/imf.data.pdf

Paquete de R (CRAN): wbstats (Banco Mundial) — https://cran.r-project.org/web/packages/wbstats/wbstats.pdf

Landa Baella, M. y Villagómez Véliz, D. Desarrollo de análisis clúster en R. Estadística Computacional (ejemplo del método de Ward).