library(tidyverse) # manipulación de datos y visualización
library(cluster) # silhouette(), distancias
library(factoextra) # fviz_cluster, fviz_pca, fviz_nbclust, fviz_eig
library(dendextend) # dendrogramas coloreados
library(corrplot) # matrices de correlación
set.seed(2026) # fijar semilla para reproducibilidadReferencia rápida: aprendizaje no supervisado en R
Esta página es una referencia rápida de los comandos de tidyverse, cluster y factoextra que usamos para clustering y PCA en la Sesión 3.1 y en el Laboratorio 5. Todos los ejemplos son ejecutables con datos simulados. Pueden copiar y pegar el código en RStudio para practicar.
El flujo típico de un análisis no supervisado es:
- Seleccionar variables numéricas y escalarlas
- Explorar correlaciones
- Aplicar K-means (o clustering jerárquico) y elegir K
- Evaluar la calidad de los clusters con la silueta
- Reducir dimensionalidad con PCA
- Interpretar los componentes y visualizar con un biplot
- Caracterizar los grupos con variables sustantivas
1 Paquetes y datos simulados
1.1 Cargar paquetes
1.2 Crear datos simulados
Simulamos 90 “países” distribuidos en 3 niveles de desarrollo (alto, medio, bajo) con 5 indicadores correlacionados. Usaremos este mismo tibble en toda la página.
Sigma <- matrix(0.3, 5, 5); diag(Sigma) <- 1
sim_grupo <- function(n, mu, g) {
MASS::mvrnorm(n, mu, Sigma) |>
as_tibble(.name_repair = ~ c("pib", "educacion", "internet",
"salud", "desigualdad")) |>
mutate(grupo_real = g)
}
datos <- bind_rows(
sim_grupo(30, c( 2.5, 2.5, 2.5, 2.0, -1.0), "alto"),
sim_grupo(30, c( 0.0, 0.0, 0.0, 0.5, 2.0), "medio"),
sim_grupo(30, c(-2.5, -2.5, -2.5, -1.5, -1.0), "bajo")
)
glimpse(datos)Rows: 90
Columns: 6
$ pib <dbl> 3.5811529, 4.8114772, 2.5808460, 3.2274287, 1.6410054, 4.5…
$ educacion <dbl> 1.414789, 2.937925, 2.179774, 2.629537, 3.399682, 4.925100…
$ internet <dbl> 0.5879008, 3.7838290, 2.1014524, 2.9783056, 1.9771634, 3.6…
$ salud <dbl> 2.1784744, 1.6748550, 2.2056686, 1.6451850, 4.3726544, 3.5…
$ desigualdad <dbl> -0.9889160, -1.1271576, -1.0295412, -1.6993760, -0.6795119…
$ grupo_real <chr> "alto", "alto", "alto", "alto", "alto", "alto", "alto", "a…
1.3 Preparar la matriz numérica
K-means y PCA requieren una matriz numérica escalada. Guardamos los nombres de grupo aparte para usarlos en la validación.
# Separar la etiqueta sustantiva (grupo_real) de las variables
grupo_real <- datos$grupo_real
datos_scaled <- datos |>
select(where(is.numeric)) |>
scale()
head(datos_scaled, 3) pib educacion internet salud desigualdad
[1,] 1.442561 0.6452436 0.2795019 1.0498135 -0.5833199
[2,] 1.951106 1.3189628 1.7146198 0.7655716 -0.6652105
[3,] 1.029093 0.9836143 0.9591557 1.0651618 -0.6073852
2 Exploración previa
2.1 Matriz de correlación
Antes de aplicar cualquier técnica, conviene ver qué variables están correlacionadas. Las correlaciones altas anticipan buenos resultados de PCA.
cor_matrix <- datos |>
select(where(is.numeric)) |>
cor()
corrplot(cor_matrix, method = "color", type = "upper",
addCoef.col = "black", number.cex = 0.8,
tl.col = "black", tl.srt = 45)
2.2 Distancias entre observaciones
La distancia euclidiana es la métrica por defecto de K-means y clustering jerárquico. También existen manhattan, maximum, canberra, binary y minkowski.
d <- dist(datos_scaled, method = "euclidean")
# Ver las primeras 5 observaciones
as.matrix(d)[1:5, 1:5] |> round(2) 1 2 3 4 5
1 0.00 1.69 0.86 1.32 1.84
2 1.69 0.00 1.28 0.83 2.19
3 0.86 1.28 0.00 0.72 1.41
4 1.32 0.83 0.72 0.00 1.87
5 1.84 2.19 1.41 1.87 0.00
3 K-means
3.1 Ajustar K-means
nstart = 25 ejecuta 25 inicializaciones aleatorias y devuelve la mejor. Protege contra mínimos locales. Siempre escalar las variables antes de llamar a kmeans().
km <- kmeans(datos_scaled, centers = 3, nstart = 25)
# Estructura del objeto
names(km)[1] "cluster" "centers" "totss" "withinss" "tot.withinss"
[6] "betweenss" "size" "iter" "ifault"
km$cluster: vector de asignación (1, 2, 3, …)km$centers: centros de cada cluster (en escala escalada)km$totss,km$withinss,km$tot.withinss,km$betweenss: descomposición de varianza
# Asignación de cada observación
head(km$cluster, 15) [1] 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
# Centros de los clusters
round(km$centers, 2) pib educacion internet salud desigualdad
1 -0.07 0.03 -0.02 -0.04 1.08
2 1.14 1.10 1.10 1.03 -0.54
3 -1.14 -1.21 -1.16 -1.06 -0.78
# Tamaño de cada cluster
km$size[1] 34 29 27
3.2 Elegir K: método del codo
El “codo” aparece donde la suma de distancias intra-cluster (wss) deja de caer bruscamente. Se busca el punto donde añadir un cluster más ya no mejora mucho.
fviz_nbclust(datos_scaled, kmeans, method = "wss",
k.max = 8, nstart = 25) +
labs(title = "Método del codo",
subtitle = NULL,
y = "Suma de distancias intra-cluster (WSS)")
3.3 Elegir K: método de la silueta
La silueta mide qué tan bien cada punto encaja en su cluster. El K óptimo maximiza la silueta promedio.
fviz_nbclust(datos_scaled, kmeans, method = "silhouette",
k.max = 8, nstart = 25) +
labs(title = "Método de la silueta", subtitle = NULL)
3.4 Silueta detallada por observación
Una vez elegido K, podemos inspeccionar la silueta de cada punto. Valores negativos indican puntos mal clasificados.
sil <- silhouette(km$cluster, dist(datos_scaled))
# Silueta promedio
round(mean(sil[, 3]), 3)[1] 0.502
# Visualizar por observación
fviz_silhouette(sil) +
theme(axis.text.x = element_blank()) cluster size ave.sil.width
1 1 34 0.49
2 2 29 0.50
3 3 27 0.51

| Valor | Interpretación |
|---|---|
| 0.71 – 1.00 | Estructura fuerte |
| 0.51 – 0.70 | Estructura razonable |
| 0.26 – 0.50 | Estructura débil |
| < 0.25 | Sin estructura clara |
3.5 Visualizar los clusters con fviz_cluster()
factoextra proyecta automáticamente los datos en los dos primeros componentes principales para visualizar en 2D.
fviz_cluster(km, data = datos_scaled,
geom = "point",
ellipse.type = "convex",
palette = c("#2d4563", "#b85450", "#6b8e23"),
ggtheme = theme_minimal())
3.6 Validar contra una etiqueta conocida
Si tenemos una etiqueta sustantiva (como grupo_real), podemos cruzarla con la asignación del modelo. Los números de cluster son arbitrarios: lo que importa es si cada grupo real cae en un solo cluster.
table(real = grupo_real, cluster = km$cluster) cluster
real 1 2 3
alto 1 29 0
bajo 3 0 27
medio 30 0 0
3.7 Perfil de cada cluster
Para dar sentido sustantivo a los clusters, calculamos la media de cada variable dentro de cada grupo.
datos |>
mutate(cluster = km$cluster) |>
group_by(cluster) |>
summarise(across(where(is.numeric),
\(x) round(mean(x), 2)))# A tibble: 3 × 6
cluster pib educacion internet salud desigualdad
<int> <dbl> <dbl> <dbl> <dbl> <dbl>
1 1 -0.08 0.02 -0.08 0.25 1.82
2 2 2.86 2.44 2.41 2.15 -0.91
3 3 -2.67 -2.79 -2.61 -1.56 -1.32
4 Clustering jerárquico
4.1 Ajustar con distintos métodos de enlace
| Método | Cómo mide la distancia entre clusters |
|---|---|
single |
Puntos más cercanos (puede crear cadenas) |
complete |
Puntos más lejanos (clusters compactos) |
average |
Promedio de todas las distancias |
ward.D2 |
Minimiza la varianza interna (muy usado) |
hc <- hclust(d, method = "ward.D2")4.2 Cortar el dendrograma
cutree() corta el dendrograma a una altura que produce K clusters.
grupos_hc <- cutree(hc, k = 3)
table(real = grupo_real, jerarquico = grupos_hc) jerarquico
real 1 2 3
alto 28 2 0
bajo 0 4 26
medio 0 30 0
4.3 Visualizar el dendrograma
plot(hc, hang = -1, labels = FALSE,
main = "", xlab = "", sub = "", ylab = "Altura")
rect.hclust(hc, k = 3,
border = c("#2d4563", "#b85450", "#6b8e23"))
4.4 Dendrograma coloreado con dendextend
dend <- as.dendrogram(hc) |>
dendextend::color_branches(k = 3,
col = c("#2d4563", "#b85450", "#6b8e23")) |>
dendextend::set("labels_cex", 0.6)
plot(dend, main = "", ylab = "Altura")
4.5 K-means vs. jerárquico
| Criterio | K-means | Jerárquico |
|---|---|---|
| Elegir K | Antes de ejecutar | Después, cortando |
| Forma de clusters | Esféricos | Cualquier forma |
| Escalabilidad | Muy rápido | Lento con n grande |
| Reproducibilidad | Depende de la semilla | Determinístico |
| Visualización | Scatter plot | Dendrograma |
5 Análisis de componentes principales (PCA)
5.1 Ajustar PCA con prcomp()
prcomp() espera datos numéricos. Como ya escalamos antes, no necesitamos scale. = TRUE. Si los datos no estuvieran escalados, sí habría que pasarlo.
pca <- prcomp(datos_scaled)
# Resumen: varianza explicada y acumulada
summary(pca)Importance of components:
PC1 PC2 PC3 PC4 PC5
Standard deviation 1.8906 0.9897 0.44368 0.36372 0.34200
Proportion of Variance 0.7149 0.1959 0.03937 0.02646 0.02339
Cumulative Proportion 0.7149 0.9108 0.95015 0.97661 1.00000
5.2 Elegir cuántos componentes conservar
Tres criterios comunes:
- Varianza acumulada cerca del 70-80%
- Método del codo en el scree plot
- Regla de Kaiser: conservar componentes con eigenvalue > 1
fviz_eig(pca, addlabels = TRUE,
barfill = "#2d4563", barcolor = "#2d4563") +
labs(title = NULL, y = "Varianza explicada (%)") +
theme_minimal()
5.3 Loadings: ¿qué mide cada componente?
Los loadings (matriz de rotación) indican cuánto pesa cada variable en cada componente. Un loading grande (positivo o negativo) significa que la variable contribuye mucho al componente.
round(pca$rotation[, 1:3], 2) PC1 PC2 PC3
pib -0.50 0.06 -0.33
educacion -0.50 0.01 -0.19
internet -0.50 0.08 -0.32
salud -0.49 0.05 0.87
desigualdad -0.09 -0.99 0.00
Para ordenar por valor absoluto del primer componente:
as.data.frame(pca$rotation[, 1:3]) |>
rownames_to_column("variable") |>
arrange(desc(abs(PC1))) variable PC1 PC2 PC3
1 educacion -0.50452430 0.005298435 -0.186995940
2 pib -0.50010880 0.057165985 -0.334692681
3 internet -0.49909235 0.075955937 -0.320860879
4 salud -0.48730811 0.049274174 0.866059045
5 desigualdad -0.09372328 -0.994236757 -0.001831322
5.4 Contribución de variables
fviz_contrib() muestra qué variables contribuyen más a un componente específico. La línea roja es el umbral “esperado” bajo una contribución uniforme.
fviz_contrib(pca, choice = "var", axes = 1,
fill = "#2d4563", color = "#2d4563") +
labs(title = "Contribución de variables a PC1") +
theme_minimal()
5.5 Scores: posición de cada observación
Los scores (pca$x) son las coordenadas de cada observación en el nuevo espacio de componentes. Sirven para ranking y para visualización.
pca$x[, 1:2] |>
as.data.frame() |>
mutate(PC1 = round(PC1, 2),
PC2 = round(PC2, 2),
grupo_real = grupo_real) |>
slice_head(n = 6) PC1 PC2 grupo_real
1 -1.64 0.74 alto
2 -2.81 0.95 alto
3 -1.95 0.79 alto
4 -2.19 1.22 alto
5 -2.62 0.62 alto
6 -3.75 0.19 alto
5.6 Biplot: observaciones y variables juntos
Un biplot muestra las observaciones (puntos) y las variables (flechas) en el mismo plano. Flechas cercanas indican variables correlacionadas; observaciones cercanas a una flecha tienen valores altos en esa variable.
fviz_pca_biplot(pca,
geom.ind = "point",
habillage = grupo_real,
addEllipses = TRUE,
palette = c("#2d4563", "#b85450", "#6b8e23"),
col.var = "black",
repel = TRUE,
legend.title = "Grupo real") +
labs(title = NULL) +
theme_minimal()
6 Combinar PCA y clustering
Una práctica común es ejecutar K-means en las variables originales y visualizar los clusters en el plano PC1-PC2.
pca_coords <- as.data.frame(pca$x[, 1:2]) |>
mutate(cluster = factor(km$cluster),
grupo_real = grupo_real)
ggplot(pca_coords, aes(x = PC1, y = PC2,
colour = cluster, shape = grupo_real)) +
geom_point(size = 3) +
scale_colour_manual(values = c("#2d4563", "#b85450", "#6b8e23")) +
labs(title = "Clusters de K-means sobre PC1-PC2",
colour = "Cluster", shape = "Grupo real") +
theme_minimal()
Otra opción es ejecutar K-means directamente sobre los primeros componentes (útil cuando hay muchas variables correlacionadas).
km_pca <- kmeans(pca$x[, 1:2], centers = 3, nstart = 25)
table(real = grupo_real, cluster = km_pca$cluster) cluster
real 1 2 3
alto 1 0 29
bajo 3 27 0
medio 30 0 0
7 Recomendaciones prácticas
- Siempre escalar antes de K-means o PCA. Las variables con mayor varianza dominan el resultado si no lo hacemos.
- Fijar la semilla con
set.seed(). K-means depende de inicialización aleatoria. - Usar
nstart = 25o más enkmeans()para evitar mínimos locales. - Combinar métricas: codo, silueta y conocimiento del dominio. Si los tres coinciden, el K elegido es defendible.
- Nombrar los clusters con etiquetas sustantivas (“alto desarrollo”) en lugar de dejar “1, 2, 3”.
- No reificar: los clusters son un resumen útil, no categorías naturales. Dependen de K, de las variables elegidas y del escalado.
- PCA sólo captura relaciones lineales. Si sospechan no linealidad, considerar UMAP o t-SNE.
8 Resumen de funciones
| Función | Paquete | Para qué sirve |
|---|---|---|
scale() |
base | Estandariza variables (media 0, sd 1) |
dist() |
base | Matriz de distancias entre observaciones |
kmeans() |
base | Ajusta K-means |
hclust() |
base | Ajusta clustering jerárquico |
cutree() |
base | Corta un dendrograma en K grupos |
rect.hclust() |
base | Dibuja rectángulos sobre el dendrograma |
prcomp() |
base | Ajusta PCA |
silhouette() |
cluster | Calcula la silueta por observación |
fviz_cluster() |
factoextra | Visualiza clusters en 2D |
fviz_nbclust() |
factoextra | Elige K con codo o silueta |
fviz_silhouette() |
factoextra | Silueta por observación |
fviz_eig() |
factoextra | Scree plot de PCA |
fviz_contrib() |
factoextra | Contribución de variables a un PC |
fviz_pca_biplot() |
factoextra | Biplot de PCA |
color_branches() |
dendextend | Colorea ramas del dendrograma |
corrplot() |
corrplot | Matriz de correlaciones visual |
Para la referencia de aprendizaje supervisado (clasificación y regresión con tidymodels), ver Referencia: tidymodels. Para análisis de texto, ver Referencia: análisis de texto.