Introducción

El análisis de clúster busca identificar grupos naturales dentro de los datos para entender mejor su estructura y comportamiento.
Es útil para:

  • Segmentación (clientes, predios, regiones, perfiles productivos)
  • Descubrimiento de patrones (aprendizaje no supervisado)
  • Reducción de complejidad (resumir poblaciones grandes)
  • Apoyo a decisiones (manejo, intervención, focalización de recursos)

Requisitos, buenas prácticas y supuestos

Requisitos

  • Variables relevantes y suficientes observaciones
  • Estandarización si hay escalas distintas
  • Distancia/algoritmo coherentes con el tipo de datos
  • Elección de k con criterio + validación (silhouette, gap, interpretabilidad)

Buenas prácticas

  • EDA antes de agrupar (outliers, colinealidad, distribuciones)
  • Probar más de un método (k-means, jerárquico, PAM/DBSCAN)
  • Validar estabilidad (re-muestreo o re-ejecución)
  • Interpretar y comunicar perfiles con tablas/gráficos

Supuestos (implícitos)

  • Existe estructura (no todo es continuo/ruido)
  • La similitud (distancia) representa bien el fenómeno
  • Variables comparables (o escaladas)
  • La forma de clúster depende del método (k-means ~ “esférico”; DBSCAN ~ densidad)

Librerías

estas son las usadas para aplicar al ejemplo a continuación.

library(tidyverse)
library(janitor)
library(skimr)
library(GGally)
library(cluster)
library(factoextra)
library(dendextend)
library(PerformanceAnalytics)

1. EJEMPLO

Contexto y objetivo

El avellano europeo (Corylus avellana) se concentra en el centro-sur de Chile.
Objetivo: segmentar huertos por condiciones agroclimáticas/suelo/manejo para identificar perfiles productivos.
Trabajaremos con datos simulados plausibles (no reales).

2. Simulación de datos

Diseñamos variables que suelen influir en el desempeño del cultivo:

  • Región (centro-sur), latitud/longitud aproximadas
  • Clima: lluvia anual (mm), grados-día (GDD)
  • Suelo: pH, materia orgánica (%) y textura (factor)
  • Manejo: edad (años), densidad (árboles/ha), riego (factor)
  • Cultivar: Tonda di Giffoni, Barcelona, Giresun/Tombul, Tonda Gentile
  • Sanidad: incidencia de enfermedades (%)
  • Calidad/Producción: diámetro (mm), % almendra, rendimiento (kg/ha)

3. Exploración inicial (EDA)

skimr::skim(d_avellanos)
Data summary
Name d_avellanos
Number of rows 200
Number of columns 17
_______________________
Column type frequency:
character 4
numeric 13
________________________
Group variables None

Variable type: character

skim_variable n_missing complete_rate min max empty n_unique whitespace
region 0 1 5 12 0 7 0
textura 0 1 6 16 0 3 0
riego 0 1 5 14 0 3 0
cultivar 0 1 9 16 0 4 0

Variable type: numeric

skim_variable n_missing complete_rate mean sd p0 p25 p50 p75 p100 hist
id_huerto 0 1 100.50 57.88 1.00 50.75 100.50 150.25 200.00 ▇▇▇▇▇
lat 0 1 -37.19 1.86 -41.64 -38.96 -36.72 -35.60 -34.04 ▁▆▂▇▅
lon 0 1 -72.03 0.54 -72.97 -72.49 -72.05 -71.57 -71.02 ▆▆▇▆▅
lluvia_mm 0 1 1203.60 200.66 635.00 1067.25 1197.50 1338.25 1786.00 ▁▅▇▅▁
gdd 0 1 1690.32 150.30 1223.00 1586.50 1705.50 1797.25 2017.00 ▁▂▇▇▃
ph 0 1 6.42 0.37 5.25 6.21 6.43 6.65 7.48 ▁▂▇▃▁
materia_organica 0 1 3.93 0.84 1.78 3.42 3.87 4.48 6.25 ▂▅▇▅▁
edad 0 1 9.32 3.48 2.00 7.00 9.00 11.00 18.00 ▃▇▇▃▂
densidad 0 1 602.63 124.40 316.00 510.50 608.50 689.50 935.00 ▂▆▇▅▁
incidencia_enf 0 1 17.37 4.76 3.10 14.51 17.47 20.57 32.09 ▁▅▇▅▁
diam_mm 0 1 19.01 1.16 16.62 18.08 18.96 19.75 22.64 ▃▇▆▃▁
pct_almendra 0 1 45.32 3.58 35.86 42.91 45.40 47.89 56.03 ▂▅▇▅▁
rendimiento_kg_ha 0 1 989.74 318.08 300.00 791.75 1017.50 1226.00 1697.00 ▃▃▇▆▂
nums <- d_avellanos %>%
  select(lluvia_mm, gdd, ph, materia_organica, edad, densidad,
         incidencia_enf, diam_mm, pct_almendra, rendimiento_kg_ha)

chart.Correlation(nums)

Comentario: revisar las relaciones esperadas es esencial para no llevarse sorpresas de resultados no esperados (lluvia–sanidad, rendimiento con edad/pH/sanidad, etc.).
Si aparecen outliers severos, considerar transformar (p. ej. log1p) o tratar extremos antes de “procesar conglomerados (clusters)”.

4. Preparación para clustering

4.1 k-means (solo numéricas estandarizadas)

Se busca que todas las variables numéricas contribuyan de forma comparable al cálculo de distancias, ya que k-means se basa en distancias (normalmente euclidianas).

¿Por qué es necesaria esta preparación?

Las variables numéricas pueden estar en escalas muy distintas (ej.: edad vs. ingresos).

Sin estandarizar, las variables con valores más grandes dominan el clustering.

k-means no maneja bien variables categóricas ni escalas heterogéneas.

¿Qué implica la preparación?

  • Seleccionar solo variables numéricas
  • Eliminar categóricas o codificarlas solo si tiene sentido numérico real.

Estandarizar las variables

Resultado: media 0, desviación estándar 1.

Eliminar outliers (opcional pero recomendado)

k-means es sensible a valores extremos.

Verificar correlaciones

Variables muy correlacionadas pueden sesgar los centroides.

vars_num <- c("lluvia_mm","gdd","ph","materia_organica","edad","densidad",
              "incidencia_enf","diam_mm","pct_almendra","rendimiento_kg_ha")

x_num <- d_avellanos %>%
  select(all_of(vars_num)) %>%
  scale() %>%
  as.data.frame()

rownames(x_num) <- d_avellanos$id_huerto
colMeans(x_num) ## debería tender a 0
##         lluvia_mm               gdd                ph  materia_organica 
##     -1.344823e-16      4.191092e-16      7.761543e-16     -2.064278e-16 
##              edad          densidad    incidencia_enf           diam_mm 
##     -9.166279e-17      3.608225e-17     -3.186687e-17     -9.478616e-16 
##      pct_almendra rendimiento_kg_ha 
##      8.036974e-16     -3.112094e-17

5. Elección de k (k-means)

La elección de k en k-means consiste en determinar cuántos clusters representan mejor la estructura de los datos sin sobreajustar ni simplificar en exceso.

Métodos más usados para elegir k

  • Método del codo (Elbow)

Se calcula la inercia (suma de distancias cuadradas al centroide) para distintos valores de k.

Se grafica k vs. inercia.

Se elige el k donde la reducción de inercia deja de ser significativa (el “codo”).

Intuición: agregar más clusters ya no mejora mucho el modelo.

  • Coeficiente de Silueta

Mide qué tan bien está cada punto dentro de su cluster.

Valores entre −1 y 1.

Cerca de 1, implica un buen clustering

Cerca de 0 hay clusters superpuestos

Negativo, mala asignación

Se elige el k con mayor silueta promedio.

Muy útil cuando el “codo” no es claro.

  • Gap Statistic

Compara la dispersión real con la esperada bajo una distribución aleatoria.

Se elige el k donde la diferencia (gap) es mayor.

Más robusto, pero computacionalmente más costoso.

  • Criterio práctico / conocimiento del dominio

A veces k se fija por:

  • Requisitos del negocio
  • Interpretabilidad
  • Conocimiento previo del problema

Clustering no es solo matemático: el contexto importa.

set.seed(123)

p_wss <- fviz_nbclust(x_num, kmeans, method = "wss") +
  ggtitle("Método del codo (WSS)")

p_sil <- fviz_nbclust(x_num, kmeans, method = "silhouette") +
  ggtitle("Silhouette promedio")

print(p_wss)

print(p_sil)

ks <- 2:8
sil_tab_km <- sapply(ks, function(k){
  fit <- kmeans(x_num, centers = k, nstart = 50)
  mean(cluster::silhouette(fit$cluster, dist(x_num))[, "sil_width"])
})

tibble(k = ks, silhouette_prom = sil_tab_km)
## # A tibble: 7 × 2
##       k silhouette_prom
##   <int>           <dbl>
## 1     2           0.120
## 2     3           0.113
## 3     4           0.110
## 4     5           0.105
## 5     6           0.108
## 6     7           0.107
## 7     8           0.105
set.seed(123)
gap_km <- clusGap(x_num, FUN = kmeans, K.max = 8, B = 30)
fviz_gap_stat(gap_km) + ggtitle("Gap statistic")

print(gap_km, method = "firstmax")
## Clustering Gap statistic ["clusGap"] from call:
## clusGap(x = x_num, FUNcluster = kmeans, K.max = 8, B = 30)
## B=30 simulated reference sets, k = 1..8; spaceH0="scaledPCA"
##  --> Number of clusters (method 'firstmax'): 1
##          logW   E.logW       gap     SE.sim
## [1,] 5.375059 5.829316 0.4542572 0.01387554
## [2,] 5.300956 5.745565 0.4446090 0.01378458
## [3,] 5.253893 5.698228 0.4443347 0.01257054
## [4,] 5.208399 5.659791 0.4513921 0.01147519
## [5,] 5.180377 5.629514 0.4491371 0.01245971
## [6,] 5.157442 5.601219 0.4437774 0.01398229
## [7,] 5.134679 5.575426 0.4407471 0.01461328
## [8,] 5.107922 5.552020 0.4440981 0.01313016

Interpretación mínima (para el informe): - Silhouette bajo (≈ 0.10–0.25) sugiere estructura débil o continuo. - Gap con k=1 indica ausencia de evidencia fuerte de agrupación clara. - En ese caso, k se elige por interpretabilidad y uso posterior.

Para continuar, tomaremos k = 3 por interpretabilidad (ajusta según tus gráficos/datos).

k_elegido <- 3

6. k-means: ajuste, calidad y visualización

set.seed(123)
km <- kmeans(x_num, centers = k_elegido, nstart = 50)

d_avellanos_km <- d_avellanos %>%
  mutate(cluster_km = factor(km$cluster))

table(d_avellanos_km$cluster_km)
## 
##  1  2  3 
## 65 82 53

Cluster 1 tiene 65 observaciones, mientras que C2 tiene 82 y C3 tiene 53

Los tamaños son relativamente balanceados (no hay clusters diminutos).

sil_km <- silhouette(km$cluster, dist(x_num))
fviz_silhouette(sil_km) + ggtitle("Silhouette por observación (k-means)")
##   cluster size ave.sil.width
## 1       1   65          0.15
## 2       2   82          0.10
## 3       3   53          0.09

Silhouette mide qué tan bien está cada observación dentro de su cluster.

Usa distancias entre observaciones (dist(x_num)).

El valor va de -1 a 1:

  • aproximado a 1, muy bien clasificado
  • aproximado a 0, en la frontera entre clusters
  • menor que 0, probablemente mal clasificado

Los valores promedio de silhouette son bajos: - Cluster 1: 0.15 - Cluster 2: 0.10 - Cluster 3: 0.09

Esto indica que:

  1. Los clusters no están muy bien separados

  2. Hay bastante solapamiento entre grupos

  3. La estructura existe, pero es débil

6.1 PCA para visualización

pca <- prcomp(x_num, center = TRUE, scale. = TRUE)

fviz_pca_biplot(
  pca, geom.ind = "point",
  habillage = d_avellanos_km$cluster_km,
  addEllipses = TRUE
) + ggtitle("PCA (k-means)")

EXPLICACION

El PCA, reduce las variables originales a dos dimensiones: Dim1 con un 21.3% y Dim2 con 15.2% de aporte de información. Esto implica, que en conjunto explican 36.5% de la variabilidad.

Puntos: parcelas/observaciones.

  1. Interpretación de los ejes
  • Dimensión 1 (horizontal)

Está dominada por: lluvia_mm, incidencia_enf️, materia_organic.

De acuerdo con esto, la Dim1 podría representar una gradiente ambiental-productivo, explicándonse como a la izquierda, condiciones más bajas / menos favorables, mientras que a la derecha, mayor lluvia, mayor MO y mayor incidencia de enfermedades.

  • Dimensión 2 (vertical)

Está asociada a: rendimiento, edad, productividad, reflejando un gradiente estructural/productiva, relacionado con: edad del cultivo, densidad, desempeño productivo

  1. Interpretación de los clusters

Cluster 1 (rojo,Derecha del gráfico)

Características:

  • Alta lluvia
  • Alta materia orgánica
  • Mayor incidencia de enfermedades
  • Valores intermedios de rendimiento

Posible Interpretación: Parcelas en ambientes más húmedos y fértiles, pero con mayor presión sanitaria.

Cluster 2 (verde, Zona central-superior)

Características:

  • Asociado a edad, densidad y rendimiento
  • Valores cercanos al centro con condiciones equilibradas

Posible Interpretación: Parcelas más maduras y estructuralmente estables, con mejor desempeño productivo relativo.

Nota: Este es el cluster más “productivo” según el PCA.

Cluster 3 (azul, Inferior-izquierda)

Características:

  • Asociado a gdd
  • Menor lluvia
  • Menor rendimiento
  • Condiciones más limitantes

Posible Interpretación:

Parcelas más jóvenes o en ambientes térmicos/secos, con menor productividad.

  1. Separación entre clusters (calidad)

Las elipses se solapan bastante, especialmente entre verde y rojo. Esto coincide con los valores bajos de silhouette que obtuviste antes (≈ 0.10–0.15).

Conclusión:

Hay tendencias claras, pero no fronteras nítidas. El sistema es continuo, no discretamente agrupado.

  1. Relaciones entre variables (flechas)

las variables lluvia_mm, materia_organica e incidencia_enf están correlacionadas entre sí, mientras que rendimiento se asocia más con edad y densidad que con lluvia y pH y gdd apuntan en direcciones opuestas a lluvia.

Nota: El rendimiento parece depender más de la estructura del cultivo que solo del ambiente.

Conclusión

El PCA muestra que los clusters obtenidos por k-means representan gradientes ambientales y productivos, más que grupos completamente separados. El cluster verde se asocia a mayores rendimientos y parcelas más maduras, mientras que el cluster rojo refleja ambientes húmedos con mayor incidencia de enfermedades y el azul condiciones más limitantes.

6.2 Perfiles de clústeres

perfil_raw <- d_avellanos_km %>%
  group_by(cluster_km) %>%
  summarise(across(all_of(vars_num), mean), .groups = "drop")

perfil_std <- as_tibble(x_num) %>%
  mutate(cluster_km = d_avellanos_km$cluster_km) %>%
  group_by(cluster_km) %>%
  summarise(across(everything(), mean), .groups = "drop")

perfil_raw
## # A tibble: 3 × 11
##   cluster_km lluvia_mm   gdd    ph materia_organica  edad densidad
##   <fct>          <dbl> <dbl> <dbl>            <dbl> <dbl>    <dbl>
## 1 1              1384. 1572.  6.32             4.36  8.83     632.
## 2 2              1104. 1715.  6.47             3.51 11.4      594.
## 3 3              1137. 1797.  6.47             4.06  6.74     580.
## # ℹ 4 more variables: incidencia_enf <dbl>, diam_mm <dbl>, pct_almendra <dbl>,
## #   rendimiento_kg_ha <dbl>
perfil_std
## # A tibble: 3 × 11
##   cluster_km lluvia_mm    gdd     ph materia_organica   edad densidad
##   <fct>          <dbl>  <dbl>  <dbl>            <dbl>  <dbl>    <dbl>
## 1 1              0.897 -0.785 -0.270            0.513 -0.140   0.238 
## 2 2             -0.498  0.165  0.129           -0.508  0.591  -0.0721
## 3 3             -0.331  0.708  0.132            0.157 -0.742  -0.180 
## # ℹ 4 more variables: incidencia_enf <dbl>, diam_mm <dbl>, pct_almendra <dbl>,
## #   rendimiento_kg_ha <dbl>
perfil_std_long <- perfil_std %>%
  pivot_longer(-cluster_km, names_to = "variable", values_to = "media_std")

ggplot(
  perfil_std_long,
  aes(
    x = reorder(variable, media_std),
    y = media_std,
    group = cluster_km,
    color = cluster_km
  )
) +
  geom_hline(yintercept = 0, linewidth = 0.4, color = "grey40") +
  geom_line(linewidth = 1) +
  geom_point(size = 2) +
  coord_flip() +
  labs(
    title = "Perfil estandarizado por clúster (k-means)",
    x = NULL,
    y = "Media (z)",
    color = "Clúster"
  ) +
  theme_minimal()

Cómo leer el gráfico

El eje X (Media Z) muestra valores estandarizados:

  • Cada línea/punto representa el promedio de una variable dentro de un clúster.
  • La línea vertical en 0 es la referencia global.

Nota: Cuanto más lejos de 0, más característica es esa variable para el clúster.

Variables que realmente separan los clústeres

Las que muestran mayor dispersión (más informativas):

  • rendimiento_kg_ha
  • edad
  • lluvia_mm
  • gdd
  • incidencia_enf
  • materia_organica

Otras (menos discriminantes):

  • pH
  • densidad
  • diámetro
  • % almendra

7. Variables mixtas: PAM + Gower

Para incluir variables categóricas sin dummies, usamos distancia Gower y PAM (medoides).

vars_mixtas <- c("lluvia_mm","gdd","ph","materia_organica","edad","densidad",
                 "incidencia_enf","diam_mm","pct_almendra",
                 "textura","riego","cultivar","region")

datos_mixtos <- d_avellanos %>%
  select(all_of(vars_mixtas)) %>%
  mutate(across(where(is.character), as.factor))

gower_dist <- daisy(datos_mixtos, metric = "gower")

pam_fit <- pam(gower_dist, k = k_elegido)

d_avellanos_pam <- d_avellanos %>%
  mutate(cluster_pam = factor(pam_fit$clustering))

sil_pam <- silhouette(pam_fit)
fviz_silhouette(sil_pam) + ggtitle("Silhouette (PAM + Gower)")
##   cluster size ave.sil.width
## 1       1   91          0.12
## 2       2   55          0.09
## 3       3   54          0.09

7.1 Perfiles PAM

perfil_pam_num <- d_avellanos_pam %>%
  group_by(cluster_pam) %>%
  summarise(across(all_of(vars_num), mean), .groups = "drop")

perfil_pam_cat <- d_avellanos_pam %>%
  group_by(cluster_pam) %>%
  summarise(
    n = n(),
    across(c(riego, textura, cultivar, region),
           ~ names(sort(table(.), decreasing = TRUE))[1],
           .names = "modo_{.col}"),
    .groups = "drop"
  )

perfil_pam_num
## # A tibble: 3 × 11
##   cluster_pam lluvia_mm   gdd    ph materia_organica  edad densidad
##   <fct>           <dbl> <dbl> <dbl>            <dbl> <dbl>    <dbl>
## 1 1               1267. 1652.  6.45             3.99  9.03     615.
## 2 2               1199. 1719.  6.49             3.95  9.95     621.
## 3 3               1102. 1725.  6.32             3.82  9.17     564.
## # ℹ 4 more variables: incidencia_enf <dbl>, diam_mm <dbl>, pct_almendra <dbl>,
## #   rendimiento_kg_ha <dbl>
perfil_pam_cat
## # A tibble: 3 × 6
##   cluster_pam     n modo_riego modo_textura     modo_cultivar    modo_region 
##   <fct>       <int> <chr>      <chr>            <chr>            <chr>       
## 1 1              91 Goteo      Franco           Tonda di Giffoni La Araucanía
## 2 2              55 Secano     Franco           Barcelona        Maule       
## 3 3              54 Goteo      Franco-arcilloso Tonda di Giffoni Ñuble

8. Comparación k-means vs PAM

comp <- tibble(
  id_huerto = d_avellanos$id_huerto,
  km = d_avellanos_km$cluster_km,
  pam = d_avellanos_pam$cluster_pam
)

tab_comp <- table(comp$km, comp$pam)
tab_comp
##    
##      1  2  3
##   1 42 14  9
##   2 34 22 26
##   3 15 19 19
acuerdo_simple <- sum(diag(tab_comp)) / sum(tab_comp)
acuerdo_simple
## [1] 0.415

9. Visualizaciones útiles para comunicar

9.1 Rendimiento por clúster (k-means)

ggplot(d_avellanos_km, aes(x = cluster_km, y = rendimiento_kg_ha)) +
  geom_boxplot() +
  labs(x = "Cluster (k-means)", y = "Rendimiento (kg/ha)", title = "Rendimiento por clúster")

9.2 Clima por clúster (k-means)

d_avellanos_km %>%
  pivot_longer(c(lluvia_mm, gdd), names_to = "variable", values_to = "valor") %>%
  ggplot(aes(x = cluster_km, y = valor)) +
  geom_boxplot() +
  facet_wrap(~ variable, scales = "free_y") +
  labs(x = "Cluster (k-means)", y = NULL, title = "Clima por clúster")

10. Interpretación (plantilla)

  • Clúster A (alto rendimiento): pH cercano a 6.5, edad intermedia, menor incidencia sanitaria y mayor frecuencia de goteo.
  • Clúster B (húmedo-sanidad): lluvia + MO altas → mayor incidencia → rendimiento más variable.
  • Clúster C (más cálido/seco): menor lluvia, GDD alto; depende más del riego y densidad.

Ajusta usando perfil_raw / perfil_pam_num / perfil_pam_cat.