Introducción

El análisis de clustering es una técnica estadística no supervisada utilizada para identificar grupos naturales dentro de un conjunto de datos. Cada subconjunto es un clúster, de modo que los objetos en un clúster son similares entre sí, pero diferentes a los objetos en otros clústeres. En el contexto de la biología, estos métodos permiten explorar patrones morfológicos, clasificar organismos según similitudes o ragos funcionales sin necesidad de etiquetas previas.

En este trabajo se emplea el algoritmo K-Means, uno de los métodos de clustering más utilizados debido a su eficiencia y facilidad de interpretación (Jain, 2010). El análisis se realiza sobre la base de datos de pingüinos del archipiélago Palmer (Horst et al., 2020), la cual contiene medidas morfométricas clave como longitud y profundidad del pico, longitud de la aleta y masa corporal.

Objetivo

El objetivo principal es determinar si las características morfológicas permiten agrupar a los individuos de manera coherente, evaluando si estos grupos coinciden o no con las especies conocidas.

Desarrollo

1. Instalar y cargar paquetes

library(palmerpenguins)
library(dplyr)
library(ggplot2)
library(factoextra)
library(table1)
library(GGally)
library(patchwork)
library(knitr)

2. Cargar datos

data("penguins")

Los datos fueron recopilados y puestos a disposición por la Dra. Kristen Gorman y la Estación Palmer, Antártida LTER, miembro de Long Term Ecological Research Network (LTER). El paquete palmerpenguins contiene dos conjuntos de datos: Penguins_raw y Penguins. Ambos conjuntos contienen información sobre 344 pingüinos. Se incluyen tres especies de pingüinos del genero Pygoscelis (Adelidae: Pygoscelis adeliae; Gentoo; Pygoscelis papua y Chinstrap: Pygoscelis antarcticus), recopiladas en tres islas del archipiélago Palmer, Antártida, entre 2007 y 2009.

3. Seleccionar las columnas numéricas necesarias y eliminar NA

df <- penguins %>%
  select(species, bill_length_mm, bill_depth_mm, flipper_length_mm, body_mass_g) %>%
  na.omit()   # Esto asegura que todas las filas sean completas

Exploración inicial de los datos

# Tabla resumen por especie
table1(~ bill_length_mm + bill_depth_mm + flipper_length_mm + body_mass_g | species, data = df)
Adelie
(N=151)
Chinstrap
(N=68)
Gentoo
(N=123)
Overall
(N=342)
bill_length_mm
Mean (SD) 38.8 (2.66) 48.8 (3.34) 47.5 (3.08) 43.9 (5.46)
Median [Min, Max] 38.8 [32.1, 46.0] 49.6 [40.9, 58.0] 47.3 [40.9, 59.6] 44.5 [32.1, 59.6]
bill_depth_mm
Mean (SD) 18.3 (1.22) 18.4 (1.14) 15.0 (0.981) 17.2 (1.97)
Median [Min, Max] 18.4 [15.5, 21.5] 18.5 [16.4, 20.8] 15.0 [13.1, 17.3] 17.3 [13.1, 21.5]
flipper_length_mm
Mean (SD) 190 (6.54) 196 (7.13) 217 (6.48) 201 (14.1)
Median [Min, Max] 190 [172, 210] 196 [178, 212] 216 [203, 231] 197 [172, 231]
body_mass_g
Mean (SD) 3700 (459) 3730 (384) 5080 (504) 4200 (802)
Median [Min, Max] 3700 [2850, 4780] 3700 [2700, 4800] 5000 [3950, 6300] 4050 [2700, 6300]
# Boxplots por especie

p1 <- ggplot(df, aes(x = species, y = bill_length_mm, fill = species)) +
  geom_boxplot() +
  theme_minimal() +
  labs(title = "Longitud del pico por especie",
       x = "Especie", y = "Longitud del pico (mm)") +
  theme(legend.position = "none")

p2 <- ggplot(df, aes(x = species, y = flipper_length_mm, fill = species)) +
  geom_boxplot() +
  theme_minimal() +
  labs(title = "Longitud de la aleta por especie",
       x = "Especie", y = "Longitud de la aleta (mm)") +
  theme(legend.position = "none")

p3 <- ggplot(df, aes(x = species, y = bill_depth_mm, fill = species)) +
  geom_boxplot() +
  theme_minimal() +
  labs(title = "Profundidad del pico por especie",
       x = "Especie", y = "Profundidad del pico (mm)") +
  theme(legend.position = "none")

p4 <- ggplot(df, aes(x = species, y = body_mass_g, fill = species)) +
  geom_boxplot() +
  theme_minimal() +
  labs(title = "Masa corporal por especie",
       x = "Especie", y = "Masa corporal (g)") +
  theme(legend.position = "none")

# Combinar todos los gráficos en una cuadrícula 2x2
(p1 | p2) /
  (p3 | p4)

# --- Gráfico de correlación entre variables ---
ggpairs(df[, 2:5], title = "Relaciones entre variables morfológicas")

4. Escalar variables numéricas

Se estandarizan las variables (media = 0 y desviasion estandar = 1), para evitar que las variables con unidades grandes dominen el modelo

num_scaled <- scale(df[, 2:5])

5. Determinar número óptimo de clusters

fviz_nbclust(num_scaled, kmeans, method = "wss") +
  ggtitle("Método del Codo - Número óptimo de clusters")

El primer gráfico utilizando la suma de cuadrados dentro del intervalo (WSS), se busca encontrar el punto donde la reducción del WSS deja de ser significantiva, lo que se puede observar cuando la curva forma un “codo”, indicando que los clústeres adicionales proporcionan rendimientos y valor decrecientes. Con este grafico se puede interpretar que k=3 es probablemente apropiado.

fviz_nbclust(num_scaled, kmeans, method = "silhouette") +
  ggtitle("Método de la Silueta - Número óptimo de clusters")

En el segundo metodo dio k=2, pero teniendo en cuenta que ya se sabe que son tres especies de pingüinos, se puede inferir que k=3 podría ser un mejor ajuste.

6. Ajustar modelo K-Means (K = 3)

Su objetivo es agrupar los datos en K grupos distintos, de forma que los elementos dentro de cada grupo sean similares entre sí, y diferentes de los de otros grupos, según las variables numéricas.

K-Means comienza con “semillas” aleatorias (puntos iniciales). Este parámetro hace que el algoritmo se ejecute 25 veces con distintos puntos iniciales, y elija el mejor resultado (el de menor variabilidad interna). Así se evitan soluciones locales subóptimas.

set.seed(123)
km <- kmeans(num_scaled, centers = 3, nstart = 25)
print(km)
## K-means clustering with 3 clusters of sizes 123, 132, 87
## 
## Cluster means:
##   bill_length_mm bill_depth_mm flipper_length_mm body_mass_g
## 1      0.6562677    -1.0983711         1.1571696   1.0901639
## 2     -1.0465260     0.4858415        -0.8899121  -0.7694891
## 3      0.6600059     0.8157307        -0.2857869  -0.3737654
## 
## Clustering vector:
##   [1] 2 2 2 2 2 2 2 2 3 2 2 2 2 2 2 2 3 2 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
##  [38] 2 2 2 2 2 3 2 2 2 2 2 3 2 2 2 3 2 2 2 2 2 2 2 3 2 2 2 2 2 2 2 3 2 2 2 3 2
##  [75] 3 2 2 2 3 2 3 2 2 2 2 2 2 2 2 2 3 2 2 2 3 2 2 2 3 2 3 2 2 2 2 2 2 2 3 2 3
## [112] 2 3 2 3 2 2 2 2 2 2 2 3 2 2 2 2 2 3 2 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
## [149] 2 2 3 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [186] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [223] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## [260] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 3
## [297] 2 3 3 3 3 3 3 3 2 3 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 3 3 3 3
## [334] 3 3 3 3 3 3 3 3 3
## 
## Within cluster sum of squares by cluster:
## [1] 143.1502 122.1477 112.9852
##  (between_SS / total_SS =  72.3 %)
## 
## Available components:
## 
## [1] "cluster"      "centers"      "totss"        "withinss"     "tot.withinss"
## [6] "betweenss"    "size"         "iter"         "ifault"

El 72.3% hace referencia a que ese porcentaje de la variabilidad total de los datos está explicado por la diferencia entre clusters y explica la mayor parte de la variación de los datos (> 70% indica que los clusters están bien definidos y alejados entre sí.)

7. Añadir resultado de clustering al dataset

df$Cluster <- as.factor(km$cluster)

8. Visualización PCA de los clusters

La función fviz_cluster() toma el resultado de un modelo de clustering (km) y lo proyecta visualmente en dos dimensiones. Esta función utiliza un Análisis de Componentes Principales (PCA) para reducir la dimensionalidad. En otras palabras, reduce el número de variables, conservando la mayor parte de la información y la varianza de los datos originales, facilitando la visualización y el análisis. De esta manera, acelera el procesamiento y minimiza la pérdida de información.

fviz_cluster(km, data = num_scaled,
             geom = "point",
             ellipse.type = "norm",
             palette = "viridis",
             ggtheme = theme_minimal()) +
  ggtitle("Clustering de Pingüinos (K-Means, K=3)")

Las medidas morfológicas separan bien a los pingüinos en tres grupos. El cluster 1 agrupa individuos más grandes y los clusters 2 y 3 a grupos más pequeños.

9. Resumen promedio por cluster

df_summary <- df %>%
  group_by(Cluster) %>%
  summarise(across(c(bill_length_mm, bill_depth_mm, flipper_length_mm, body_mass_g),
                   mean, na.rm = TRUE))

kable(df_summary, align = "l")
Cluster bill_length_mm bill_depth_mm flipper_length_mm body_mass_g
1 47.50488 14.98211 217.1870 5076.016
2 38.20833 18.11061 188.4015 3584.659
3 47.52529 18.76207 196.8966 3902.011
Cluster Descripción
Cluster 1 El cluster 1 es el de mayor tamaño corporal (5076 g) y las aletas más largas (217 mm).
Cluster 2 El cluster 2 es el más pequeño (3585 g) con un pico corto (38.2 mm) y aletas cortas (188 mm).
Cluster 3 El cluster 3 tiene el pico largo (47.5 mm) y profundo (18.8 mm), con un tamaño corporal intermedio (3902 g).

10. Comparar clusters con especies reales

table(df$Cluster, df$species)
##    
##     Adelie Chinstrap Gentoo
##   1      0         0    123
##   2    127         5      0
##   3     24        63      0

Esta tabla que muestra cuántos individuos de cada especie real cayeron dentro de cada cluster del modelo:

El primer clúster agrupó a los individuos de mayor tamaño corporal y aletas más largas (Gentoo), el segundo reunió a los individuos de menor tamaño y pico más corto (Adelie), mientras que el tercero representó una combinación intermedia con picos más largos y profundos (Chinstrap). El modelo agrupo el 100% de los datos para el primer cluster, el 96% para el segundo cluster y el 72% para el tercero.

Conclusión

El análisis de clustering mediante K-means permitió identificar tres grupos morfológicamente diferenciados dentro de la población de pingüinos analizada. Cada clúster reflejó combinaciones específicas de rasgos corporales, lo que evidencia patrones naturales de variación asociados, posiblemente, a diferencias entre especies.

Referencias

Gorman, K. B., Williams, T. D., & Fraser, W. R. (2014). Ecological sexual dimorphism and environmental variability within a community of Antarctic penguins (genus Pygoscelis). PloS one, 9(3), e90081.

Jain, A. K. (2010). Data clustering: 50 years beyond K-means. Pattern recognition letters, 31(8), 651-666.