Examen:
Datos. Utilice la librerÃa wbstats para descargar, desde la API del Banco Mundial, los siguientes indicadores para el periodo 2015-2024:
Crecimiento del PIB (% anual): NY.GDP.MKTP.KD.ZG Desempleo (% de la fuerza laboral): SL.UEM.TOTL.ZS Remesas personales recibidas (% del PIB): BX.TRF.PWKR.DT.GD.ZS Exportaciones de bienes y servicios (% del PIB): NE.EXP.GNFS.ZS
Pregunta 1. Preparación de los datos
Seleccione un grupo de 20 paÃses de su elección y descargue los indicadores indicados.
Construya la matriz de datos tomando, para cada paÃs e indicador, el último dato disponible.
Trate los valores faltantes sustituyéndolos por la media de la columna, y verifique que no queden valores NA.
Estandarice las variables mediante
scale().
Pregunta 2. Número óptimo de clústeres
Sobre la matriz estandarizada obtenida en la pregunta 1, determine el número óptimo de clústeres (k) para cada método, usando el criterio indicado:
K-Means: método del codo.
K-Medoides (PAM): coeficiente de silueta.
Ward: criterio del salto de altura en el dendrograma.
Presente el gráfico de cada criterio.
Pregunta 3. Aplicación de los métodos con el k seleccionado
Sobre la matriz estandarizada de la pregunta 1, aplique los siguientes métodos de agrupamiento (DBSCAN se aplica sobre las variables de comercio exterior indicadas en el inciso d):
K-Means, usando el k obtenido con el método del codo en la pregunta 2.
K-Medoides (PAM), usando el k obtenido con el coeficiente de silueta en la pregunta 2.
Ward, usando el k obtenido con el criterio del salto de altura en la pregunta 2.
DBSCAN, usando las variables Exportaciones_USD (NE.EXP.GNFS.CD) e Importaciones_USD (NE.IMP.GNFS.CD). Como este método no requiere un k previo, determine sus parámetros minPts y epsilon.
Pregunta 4. Representación gráfica de los clústeres
Grafique los clústeres obtenidos en la pregunta 3 con los cuatro métodos (K-Means, K-Medoides PAM, DBSCAN y Ward).
Datos
Parte 1
a) Selección de 20 paÃses
Descarga de indicadores
b) matriz
ultimo_dato <- datos_wb %>%
select(iso3c, country, date, all_of(names(indicadores))) %>%
pivot_longer(cols = all_of(names(indicadores)),
names_to = "Indicador", values_to = "Valor") %>%
filter(!is.na(Valor)) %>%
group_by(iso3c, country, Indicador) %>%
slice_max(date, n = 1) %>%
ungroup()
data_cluster <- ultimo_dato %>%
select(iso3c, country, Indicador, Valor) %>%
pivot_wider(names_from = Indicador, values_from = Valor)
data_cluster %>%
kable(caption = "Matriz de variables por paÃs (último dato disponible)") %>%
kable_classic(html_font = "Times New Roman", font_size = 13)| iso3c | country | Crecimiento_PIB | Desempleo | Exportaciones | Remesas_personales |
|---|---|---|---|---|---|
| ARG | Argentina | -1.3429307 | 7.150 | 15.24975 | 0.1635999 |
| BRA | Brazil | 3.4193152 | 6.801 | 17.93858 | 0.2242818 |
| CAN | Canada | 2.0462661 | 6.351 | 32.46912 | 0.0375070 |
| CHL | Chile | 2.8051264 | 8.718 | 33.73207 | 0.0304727 |
| COL | Colombia | 1.4933073 | 9.619 | 16.08045 | 2.8235717 |
| CRI | Costa Rica | 4.0829244 | 6.936 | 37.52984 | 0.7494093 |
| DEU | Germany | -0.4958519 | 3.400 | 41.43406 | 0.4731113 |
| ECU | Ecuador | -1.9440902 | 3.453 | 30.49788 | 5.2861312 |
| ESP | Spain | 3.4552541 | 11.400 | 37.05588 | 0.3667485 |
| FRA | France | 1.1904680 | 7.400 | 33.88975 | 1.2268965 |
| GBR | United Kingdom | 1.0802772 | 4.361 | 31.05614 | 0.1308153 |
| ITA | Italy | 0.7830391 | 6.500 | 32.33507 | 0.5094347 |
| JPN | Japan | -0.2400842 | 2.500 | 21.97991 | 0.1108693 |
| KOR | Korea, Rep. | 2.0036110 | 2.784 | 44.35824 | 0.3883463 |
| MEX | Mexico | 1.3505763 | 2.678 | 37.28362 | 3.6950729 |
| PER | Peru | 3.5152749 | 5.199 | 28.88393 | 1.6910867 |
| POL | Poland | 3.0284163 | 2.807 | 52.17981 | 0.9468633 |
| SLV | El Salvador | 2.5904637 | 3.261 | 32.65986 | 24.3362061 |
| URY | Uruguay | 3.3257591 | 8.209 | 28.23490 | 0.1653514 |
| USA | United States | 2.7931872 | 4.022 | 10.97470 | 0.0297358 |
c) Tratamiento de datos faltantes (imputación por la media)
vars <- names(indicadores)
data_cluster_imputado <- data_cluster %>%
mutate(across(all_of(vars),
~ ifelse(is.na(.x), mean(.x, na.rm = TRUE), .x)))
# Verificación: no deben quedar NA
data_cluster_imputado %>%
summarise(across(all_of(vars), ~ sum(is.na(.x))))## # A tibble: 1 × 4
## Crecimiento_PIB Desempleo Remesas_personales Exportaciones
## <int> <int> <int> <int>
## 1 0 0 0 0
Parte 2
a) K-Means: método del codo.
set.seed(123)
fviz_nbclust(matriz_tipificada, kmeans, method = "wss", k.max = 10) +
labs(title = "Método del Codo para K-Means",
subtitle = "Crecimiento del PIB, Desempleo, Remesas personales y Exportaciones",
x = "Número de clústeres (k)", y = "WCSS (Suma de cuadrados intra-clúster)") +
theme_minimal()b) K-Medoides (PAM): coeficiente de silueta.
fviz_nbclust(matriz_tipificada, pam, method = "silhouette", k.max = 10,
diss = dist(matriz_tipificada, method = "manhattan")) +
labs(title = "Coeficiente de Silueta promedio por k (PAM, distancia Manhattan)") +
theme_minimal()c) Ward: criterio del salto de altura en el dendrograma.
distancias <- dist(matriz_tipificada, method = "euclidean")
modelo_ward <- hclust(distancias, method = "ward.D2")
alturas <- rev(modelo_ward$height)
saltos <- -diff(alturas)
n_candidatos <- min(10, length(alturas))
plot(1:n_candidatos, alturas[1:n_candidatos], type = "b", pch = 19, col = "steelblue",
xlab = "Número de clústeres", ylab = "Altura de fusión",
main = "Identificación del salto más grande - Ward")
k_ward <- which.max(saltos[1:(n_candidatos - 1)]) + 1
abline(v = k_ward, col = "red", lty = 2)
text(k_ward, max(alturas[1:n_candidatos]) * 0.9,
labels = paste0("k óptimo ≈ ", k_ward), col = "red", pos = 4)## [1] 3
Parte 3
a) Aplicación de K-Means con el k seleccionado
k_optimo <-5
set.seed(123)
resultado_kmeans <- kmeans(matriz_tipificada, centers = k_optimo, nstart = 25)
resultado_kmeans## K-means clustering with 5 clusters of sizes 5, 3, 3, 8, 1
##
## Cluster means:
## Crecimiento_PIB Desempleo Exportaciones Remesas_personales
## 1 -0.2067681 -0.9469142 1.0174696 -0.19297468
## 2 0.4804115 0.4354591 -1.5346069 -0.21166780
## 3 -1.7090163 -0.5018319 -0.7982709 -0.05844998
## 4 0.5283076 0.7324413 0.2162136 -0.29103768
## 5 0.4931941 -0.9258414 0.1815766 4.10352822
##
## Clustering vector:
## ARG BRA CAN CHL COL CRI DEU ECU ESP FRA GBR ITA JPN KOR MEX PER POL SLV URY USA
## 3 2 4 4 2 4 1 3 4 4 1 4 3 1 1 4 1 5 4 2
##
## Within cluster sum of squares by cluster:
## [1] 5.231273 3.371699 3.990399 7.898235 0.000000
## (between_SS / total_SS = 73.0 %)
##
## Available components:
##
## [1] "cluster" "centers" "totss" "withinss" "tot.withinss"
## [6] "betweenss" "size" "iter" "ifault"
b) Aplicación de K-Medoides (PAM) con el k seleccionado
k_op <- 2
set.seed(123)
modelo_pam <- pam(matriz_tipificada, k = k_op, metric = "manhattan")
modelo_pam## Medoids:
## ID Crecimiento_PIB Desempleo Exportaciones Remesas_personales
## CAN 3 0.1749824 0.2580647 0.1630425 -0.3946317
## MEX 15 -0.2318120 -1.1492127 0.6308600 0.2824546
## Clustering vector:
## ARG BRA CAN CHL COL CRI DEU ECU ESP FRA GBR ITA JPN KOR MEX PER POL SLV URY USA
## 1 1 1 1 1 1 2 2 1 1 1 1 2 2 2 1 2 2 1 1
## Objective function:
## build swap
## 2.319271 2.319271
##
## Available components:
## [1] "medoids" "id.med" "clustering" "objective" "isolation"
## [6] "clusinfo" "silinfo" "diss" "call" "data"
c) Aplicación de Ward con el k seleccionado
## grupos_ward
## 1 2 3
## 12 7 1
data.frame(
k_clusteres = 1:n_candidatos,
altura_fusion = round(alturas[1:n_candidatos], 2),
salto_siguiente = c(round(saltos[1:(n_candidatos - 1)], 2), NA)
) %>%
kable(
col.names = c("Fusión j (pasa de j + 1 a j grupos)", "Altura de fusión (ESS)", "Salto a la fusión siguiente"),
caption = "Alturas de fusión y saltos - Ward"
) %>%
kable_classic(html_font = "Times New Roman", font_size = 14) %>%
kable_styling()| Fusión j (pasa de j + 1 a j grupos) | Altura de fusión (ESS) | Salto a la fusión siguiente |
|---|---|---|
| 1 | 6.61 | 0.82 |
| 2 | 5.79 | 1.47 |
| 3 | 4.32 | 0.65 |
| 4 | 3.67 | 0.66 |
| 5 | 3.01 | 0.43 |
| 6 | 2.58 | 0.31 |
| 7 | 2.26 | 0.17 |
| 8 | 2.10 | 0.12 |
| 9 | 1.97 | 0.32 |
| 10 | 1.65 | NA |
d) Aplicación de DBSCAN
DBSCAN agrupa por densidad, por lo que funciona mejor con pocas
dimensiones. Se utilizan dos variables derivadas del comercio exterior,
construidas con las exportaciones (NE.EXP.GNFS.CD) y las
importaciones (NE.IMP.GNFS.CD) de bienes y servicios en
dólares corrientes:
log_total = ln(X + M + 1): tamaño del comercio exterior del paÃs.log_ratio = ln(X + 1) - ln(M + 1): balanza comercial relativa (positiva en exportadores netos, negativa en importadores netos).
Se aplican logaritmos porque el comercio en dólares tiene un fuerte sesgo a la derecha.
Descarga y preparación de los datos
ind_db <- c(
Exportaciones_USD = "NE.EXP.GNFS.CD",
Importaciones_USD = "NE.IMP.GNFS.CD"
)
datos_comercio <- wb_data(
indicator = ind_db,
country = paises_wb,
start_date = 2015,
end_date = 2024,
return_wide = TRUE
)
datos_db <- datos_comercio %>%
filter(!is.na(Exportaciones_USD), !is.na(Importaciones_USD)) %>%
group_by(iso3c, country) %>%
slice_max(date, n = 1) %>% # último año con ambos datos
ungroup()
# Tamaño total del comercio y balanza relativa
datos_db$log_total <- log(datos_db$Importaciones_USD + datos_db$Exportaciones_USD + 1)
datos_db$log_ratio <- log(datos_db$Exportaciones_USD + 1) - log(datos_db$Importaciones_USD + 1)
# Correlaciones para justificar la transformación
cor(log(datos_db$Exportaciones_USD + 1), log(datos_db$Importaciones_USD + 1))## [1] 0.9943941
## [1] -0.04656584
# Estandarización Z
datos_escalados <- as.data.frame(scale(datos_db[, c("log_total", "log_ratio")]))
rownames(datos_escalados) <- datos_db$iso3c
nrow(datos_escalados) # paÃses que quedaron## [1] 20
Parámetros y curva k-NN
Con 2 variables, la regla minPts ≥ 2 × m da
minPts = 4.
minPts_db <- 4
epsilon_db <- 0.7
kNNdistplot(datos_escalados, k = minPts_db - 1)
abline(h = epsilon_db, col = "red", lty = 2)
title(paste("Gráfico de k-vecinos - Epsilon elegido =", epsilon_db))set.seed(123)
resultado_db <- dbscan(datos_escalados, eps = epsilon_db, minPts = minPts_db)
datos_db$grupo <- factor(ifelse(resultado_db$cluster == 0, "Ruido",
paste("Grupo", resultado_db$cluster)))
table(datos_db$grupo)##
## Grupo 1 Grupo 2 Ruido
## 6 11 3
Prueba de sensibilidad
valores_eps <- seq(0.2, 1.2, by = 0.1)
sensibilidad <- do.call(rbind, lapply(valores_eps, function(e) {
r <- dbscan(datos_escalados, eps = e, minPts = minPts_db)
data.frame(
eps = e,
clusteres = max(r$cluster),
ruido = sum(r$cluster == 0),
pct_ruido = round(100 * sum(r$cluster == 0) / nrow(datos_escalados), 1)
)
}))
sensibilidad %>%
kable(
col.names = c("Epsilon", "Clústeres", "PaÃses en ruido", "% de ruido"),
caption = paste("Sensibilidad del resultado al valor de epsilon (minPts =", minPts_db, ")")
) %>%
kable_classic(html_font = "Times New Roman", font_size = 13)| Epsilon | Clústeres | PaÃses en ruido | % de ruido |
|---|---|---|---|
| 0.2 | 0 | 20 | 100 |
| 0.3 | 1 | 15 | 75 |
| 0.4 | 2 | 10 | 50 |
| 0.5 | 1 | 10 | 50 |
| 0.6 | 1 | 9 | 45 |
| 0.7 | 2 | 3 | 15 |
| 0.8 | 2 | 3 | 15 |
| 0.9 | 2 | 3 | 15 |
| 1.0 | 1 | 3 | 15 |
| 1.1 | 1 | 3 | 15 |
| 1.2 | 1 | 3 | 15 |
Parte 4
Grafica K-Means
fviz_cluster(
resultado_kmeans,
data = matriz_tipificada,
palette = c("#0072B2", "#D55E00", "#009E73", "#CC79A7", "#E69F00", "#56B4E9"),
ellipse.type = "norm",
star.plot = TRUE,
repel = TRUE,
labelsize = 8,
main = paste("K-Means: Clústeres de paÃses seleccionados (k =", k_optimo, ")"),
ggtheme = theme_minimal()
)Grafica K-Medoides (PAM)
fviz_cluster(
modelo_pam,
data = matriz_tipificada,
palette = c("#0072B2", "#D55E00", "#009E73", "#CC79A7", "#E69F00", "#56B4E9", "#555555"),
ellipse.type = "norm",
repel = TRUE,
labelsize = 8,
main = paste("K-Medoides (PAM): Clústeres de paÃses seleccionados (k =", k_op, ")"),
ggtheme = theme_minimal()
)Dendrograma con el corte de Método de Ward
paleta <- c("#0072B2", "#D55E00", "#009E73", "#CC79A7", "#E69F00", "#56B4E9", "#555555")
fviz_dend(
modelo_ward,
k = k_ward,
cex = 0.7,
k_colors = paleta[1:k_ward],
rect = TRUE,
rect_fill = TRUE,
main = paste0("Dendrograma - Método de Ward (k = ", k_ward, ")")
)Grafica DBSCAN
datos_grafico <- datos_escalados
datos_grafico$iso3c <- rownames(datos_escalados)
datos_grafico$grupo <- datos_db$grupo
# Valor Z que corresponde a exportaciones = importaciones (log_ratio = 0)
equilibrio <- (0 - mean(datos_db$log_ratio)) / sd(datos_db$log_ratio)
ggplot(datos_grafico, aes(x = log_total, y = log_ratio, color = grupo)) +
geom_point(alpha = 0.8, size = 2.8) +
geom_text_repel(aes(label = iso3c), size = 3, show.legend = FALSE) +
geom_hline(yintercept = equilibrio, linetype = "dashed", color = "gray50") +
scale_color_manual(values = c("Ruido" = "#555555",
"Grupo 1" = "#0072B2",
"Grupo 2" = "#D55E00",
"Grupo 3" = "#009E73",
"Grupo 4" = "#CC79A7")) +
labs(
title = "DBSCAN - Tamaño y balanza comercial de los paÃses seleccionados",
subtitle = paste("Parámetros: eps =", round(epsilon_db, 3), "| minPts =", minPts_db,
"| LÃnea punteada: exportaciones = importaciones"),
x = "Tamaño del Comercio (Estandarizado Z)",
y = "Balanza Relativa Exp-Imp (Estandarizada Z)",
color = "Clasificación"
) +
theme_minimal(base_size = 12) +
theme(
plot.title = element_text(face = "bold", size = 14),
panel.grid.minor = element_blank(),
legend.position = "right"
)