Code
library(tidyverse)
library(faraway)
library(kableExtra)
library(CDR)
library(cluster)
library(factoextra)
library(ggrepel)
library(jsonlite)
library(tidyverse)
library(faraway)
library(kableExtra)
library(CDR)
library(cluster)
library(factoextra)
library(ggrepel)
library(jsonlite)
Fernández-Avilés and Montero (2024)
Rodrigo (2017)
Dada la utilidad del clustering en disciplinas muy distintas (genómica, marketing…), se han desarrollado multitud de variantes y adaptaciones de sus métodos y algoritmos. Pueden diferenciarse tres grupos principales:
Partitioning Clustering: Este tipo de algoritmos requieren que el usuario especifique de antemano el número de clusters que se van a crear (K-means, K-medoids, CLARA).
Hierarchical Clustering: Este tipo de algoritmos no requieren que el usuario especifique de antemano el número de clusters. (agglomerative clustering, divisive clusterig).
Kassambara (2017)
Necesitamos saber previamente el número de clústers: K
El método encuentra los K mejores clústers. Se entiende como mejor clúster como aquel cuya varianza interna (varianza de los elementos que lo componen), es la mínima posible.
Las medidas más comunes para definir la varianza son:
Suma de las distancias euclídeas al cuadrado entre cada observación y el centroide del clúster.
Suma de las distancias euclídeas al cuadrado entre todos los pares de observaciones que forman el clúster, dividida entre el número de observaciones del clúster.
Encontrar este mínimo es una función compleja, debido a la gran variedad de configuraciones que podría tener el clúster. Sin embargo, se aplica un algoritmo que es buena, sin ser necesariamente la óptima (mínimo local).
Asignar aleatoriamente un número entre 1 y K a cada observación. (Asignación inicial)
Iterar los siguientes pasos hasta que la asignación de las observaciones a los clusters no cambie o se alcance un número máximo de iteraciones predeterminado.
2.1. Para cada clúster se calcula el centroide, calculando la media de cada variable.
2.2. Se asigna a cada observación al clúster cuyo centroide está más próximo.
Otra alternativa:
Seleccionar de forma aleatoria k observaciones como centroides
Asignar cada observación al centroide más cercano
Para cada clúster, recalcular su centroide
Repetir los pasos 2 y 3 hasta que las asignaciones no cambien o se alcance un número máximo de iteraciones predeterminado
<- read.csv("https://raw.githubusercontent.com/jesusturpin/curintel2324/main/data/lqsa.csv")
lqsa <- lqsa$Nombre
nombres rownames(lqsa) <- nombres
<- lqsa %>%
lqsa2d select_if(is.numeric) %>%
select(Atractivo, Convivencia) %>%
mutate_all(scale)
<- lqsa %>%
lqsa5d select_if(is.numeric) %>%
mutate_all(scale)
set.seed(123)
<- kmeans(x = lqsa2d, centers = 4, nstart = 50)
km_clusters2d <- kmeans(x = lqsa5d, centers = 4, nstart = 50) km_clusters5d
# Objeto devuelto por kmeans: media de las variables en los clusters
# Vector con los elementos y el grupo al que pertenece
# La suma de los cuadrados interna de cada cluster
km_clusters2d
K-means clustering with 4 clusters of sizes 11, 5, 9, 5
Cluster means:
Atractivo Convivencia
1 0.05156841 -0.7050180
2 0.27285563 1.8336351
3 -1.09620945 0.1006702
4 1.58687088 -0.4638018
Clustering vector:
Ongombo Violeta Javi Estela Reynolds La chusa
2 1 1 4 1
Vicente Enrique Pastor Amador Doña Fina Berta
2 3 1 3 1
Judith Lola Nines Clara Leo
4 4 3 3 3
Maxi Los Cuquitos Padre Alejandro Yoli Coque
3 2 2 4 1
Menchu Alba Raquel Antonio Recio Bruno
3 3 4 1 1
Maite Fermín Araceli Patricio Teodoro
1 2 1 3 1
Within cluster sum of squares by cluster:
[1] 3.150152 4.213857 2.156246 1.226957
(between_SS / total_SS = 81.5 %)
Available components:
[1] "cluster" "centers" "totss" "withinss" "tot.withinss"
[6] "betweenss" "size" "iter" "ifault"
%>%
lqsa2d mutate(cluster = as.factor(km_clusters2d$cluster)) %>%
ggplot() +
geom_text_repel(aes(x = Atractivo, y = Convivencia, label = rownames(lqsa2d),
color = cluster), size = 3.5)+
geom_point(aes(x = Atractivo, y = Convivencia, color = cluster))+
theme_bw()
fviz_cluster(object = km_clusters2d, data = lqsa2d, ellipse.type = "t",
repel = TRUE) +
theme_bw() +
labs(title = "Resultados clustering PAM") +
theme(legend.position = "none")
<- fviz_nbclust(x = scale(lqsa2d),
codo FUNcluster = kmeans,
method = "wss",
k.max = 10,
nstart = 25,
diss = dist(lqsa2d, method = "euclidean"))+
ggtitle("Número óptimo de clusters")
codo
$data) (codo
clusters y
1 1 58.000000
2 2 34.525428
3 3 17.006213
4 4 10.747211
5 5 7.991157
6 6 5.248731
7 7 4.354529
8 8 3.493682
9 9 2.659938
10 10 2.273717
<- codo$data$y
wss_values
# Calcular las diferencias porcentuales en el decremento de WSS
$data$perc_dec <- c(0, diff(wss_values) / head(wss_values, -1) * -100) codo
$data codo
clusters y perc_dec
1 1 58.000000 0.00000
2 2 34.525428 40.47340
3 3 17.006213 50.74293
4 4 10.747211 36.80421
5 5 7.991157 25.64437
6 6 5.248731 34.31826
7 7 4.354529 17.03653
8 8 3.493682 19.76901
9 9 2.659938 23.86434
10 10 2.273717 14.51993
Para las 5 dimensiones:
<- fviz_nbclust(x = scale(lqsa5d),
codo5d FUNcluster = kmeans,
method = "wss",
k.max = 10,
nstart = 25,
diss = dist(lqsa2d, method = "euclidean"))+
ggtitle("Número óptimo de clusters 5 dimensiones")
codo5d
<- codo5d$data$y
wss_values
# Calcular las diferencias porcentuales en el decremento de WSS
$data$perc_dec <- c(0, diff(wss_values) / head(wss_values, -1) * -100)
codo5d$data codo5d
clusters y perc_dec
1 1 58.000000 0.000000
2 2 47.545284 18.025373
3 3 28.140956 40.812309
4 4 21.369922 24.061137
5 5 15.671777 26.664322
6 6 15.130104 3.456363
7 7 12.658218 16.337531
8 8 9.970953 21.229414
9 9 6.192435 37.895252
10 10 6.092544 1.613118
set.seed(123)
<- kmeans(x = lqsa5d, centers = 3, nstart = 50)
km_clusters5d
# Las funciones del paquete factoextra emplean el nombre de las filas del
# dataframe que contiene los datos como identificador de las observaciones.
# Esto permite añadir labels a los gráficos.
fviz_cluster(object = km_clusters5d, data = lqsa5d, show.clust.cent = TRUE,
ellipse.type = "euclid", star.plot = TRUE, repel = TRUE) +
labs(title = "Resultados clustering K-means") +
theme_bw() +
theme(legend.position = "none")
La idea es similar a K-Means, pero cada cluster está representado por una observación presente en el cluster. Medoide, en lugar de centroide. El medoide es el elemento dentro de un cluster cuya distancia promedio entre él y todos los demás es la mínima posible. Es el elemento central, y por lo tanto, el más representativo del cluster. El algoritmo más empleado para aplicar K-medoid se conoce como PAM (Partitioning Around Medoids) y se implementa siguiendo los siguientes pasos:
Mientras que k-means minimiza la suma de las distancias al cuadrado de cada observación respecto a su centroide, k-medoids minimiza la suma de las diferencias de cada observación respecto a su medoide.
K-medoids, se usa cuando hay outliers, por ello, se recomienda usar distancia de Manhattan en lugar de euclídea.
<- fviz_nbclust(x = lqsa2d,
codo2d_pam FUNcluster = pam,
method = "wss",
k.max = 10,
diss = dist(lqsa2d, method = "manhattan"))
codo2d_pam
<- codo2d_pam$data$y
wss_values
# Calcular las diferencias porcentuales en el decremento de WSS
$data$perc_dec <- c(0, diff(wss_values) / head(wss_values, -1) * -100)
codo2d_pam$data codo2d_pam
clusters y perc_dec
1 1 93.746380 0.00000
2 2 59.041758 37.01969
3 3 35.053993 40.62847
4 4 16.437568 53.10786
5 5 12.584631 23.43982
6 6 7.737153 38.51903
7 7 6.682680 13.62869
8 8 5.543018 17.05397
9 9 4.368617 21.18703
10 10 3.829048 12.35103
set.seed(123)
<- pam(x = lqsa2d, k = 4, metric = "manhattan")
pam_clusters pam_clusters
Medoids:
ID Atractivo Convivencia
Fermín 27 -0.2225927 1.5747690
Coque 20 0.2082319 -0.8521010
Judith 11 1.6084121 -0.5285184
Doña Fina 9 -1.1919483 0.2804383
Clustering vector:
Ongombo Violeta Javi Estela Reynolds La chusa
1 2 2 3 2
Vicente Enrique Pastor Amador Doña Fina Berta
1 4 2 4 2
Judith Lola Nines Clara Leo
3 3 4 4 4
Maxi Los Cuquitos Padre Alejandro Yoli Coque
4 1 1 3 2
Menchu Alba Raquel Antonio Recio Bruno
4 4 3 2 2
Maite Fermín Araceli Patricio Teodoro
2 1 2 4 2
Objective function:
build swap
0.7672129 0.6180105
Available components:
[1] "medoids" "id.med" "clustering" "objective" "isolation"
[6] "clusinfo" "silinfo" "diss" "call" "data"
fviz_cluster(object = pam_clusters, data = lqsa2d, ellipse.type = "t",
repel = TRUE) +
theme_bw() +
labs(title = "Resultados clustering PAM") +
theme(legend.position = "none")
<- fviz_nbclust(x = lqsa5d,
codo5d_pam FUNcluster = pam,
method = "wss",
k.max = 10,
diss = dist(lqsa5d, method = "manhattan"))
codo5d_pam
<- codo5d_pam$data$y
wss_values
# Calcular las diferencias porcentuales en el decremento de WSS
$data$perc_dec <- c(0, diff(wss_values) / head(wss_values, -1) * -100)
codo5d_pam$data codo5d_pam
clusters y perc_dec
1 1 495.01513 0.000000
2 2 374.89660 24.265628
3 3 223.68554 40.334071
4 4 175.00772 21.761718
5 5 140.85920 19.512582
6 6 125.86036 10.648107
7 7 95.59865 24.043876
8 8 76.03511 20.464247
9 9 66.90749 12.004480
10 10 60.67630 9.313141
set.seed(123)
<- pam(x = lqsa5d, k = 3, metric = "manhattan")
pam_clusters pam_clusters
Medoids:
ID Poder Convivencia Liante Atractivo Locura
Ongombo 1 0.9069029 1.25118630 -0.8426670 1.0698813 -1.1602019
Coque 20 -0.7174008 -0.85210102 -0.5238200 0.2082319 0.5916699
Nines 13 -0.3113249 -0.04314436 0.2960722 -1.1919483 0.6247241
Clustering vector:
Ongombo Violeta Javi Estela Reynolds La chusa
1 2 2 2 2
Vicente Enrique Pastor Amador Doña Fina Berta
1 1 2 3 1
Judith Lola Nines Clara Leo
2 2 3 3 3
Maxi Los Cuquitos Padre Alejandro Yoli Coque
3 3 1 2 2
Menchu Alba Raquel Antonio Recio Bruno
3 3 1 3 2
Maite Fermín Araceli Patricio Teodoro
2 3 2 3 2
Objective function:
build swap
2.791864 2.595406
Available components:
[1] "medoids" "id.med" "clustering" "objective" "isolation"
[6] "clusinfo" "silinfo" "diss" "call" "data"
fviz_cluster(object = pam_clusters, data = lqsa2d, ellipse.type = "t",
repel = TRUE) +
theme_bw() +
labs(title = "Resultados clustering PAM") +
theme(legend.position = "none")
# Como hay más de 2 variables, se están representando las 2 primeras componentes
# de un PCA. Se tienen que calcular el PCA y extraer las proyecciones almacenadas
# en el elemento x.
<- prcomp(lqsa5d)$x
medoids
# Se seleccionan únicamente las proyecciones de las observaciones que son medoids
<- medoids[rownames(pam_clusters$medoids), c("PC1", "PC2")]
medoids <- as.data.frame(medoids)
medoids
# Se emplean los mismos nombres que en el objeto ggplot
colnames(medoids) <- c("x", "y")
# Creación del gráfico
fviz_cluster(object = pam_clusters, data = lqsa5d, ellipse.type = "t",
repel = TRUE) +
theme_bw() +
# Se resaltan las observaciones que actúan como medoids
geom_point(data = medoids, color = "firebrick", size = 2) +
labs(title = "Resultados clustering PAM") +
theme(legend.position = "none")
Para resolver las limitaciones del método k-medoids, que para grandes datasets, requiere mucha RAM, se combina el algoritmo PAM, con remuestreo. No se buscan los medoides de todos los datos, sino que se extrae una muestra de un tamaño determinado y se le aplica el algoritmo PAM.
Pasos:
Dividir el conjunto en n partes de igual tamaño. n es un valor a determinar por el analista, dependerá de la capacidad de cómputo y el tamño del dataset.
Para cada una de las n partes: 2.1 Aplicar PAM e identificar los medoides 2.2 Usar los medoides para todo el set de datos 2.3 Calcular la suma total de las distancias entre las observaciones del set completo y su medoide
Seleccionar como clustering final aquel que haya conseguido menor suma total de distancias en el paso 2.3
<- "https://mapaescolar.murciaeduca.es/mapaescolar-api/api/centros"
url_map_school <- fromJSON(url_map_school)
col.df <- col.df %>%
col.simp.df select(codcen, 'geo-referencia') %>%
unnest_wider('geo-referencia') %>%
%>%
as.data.frame drop_na()
rownames(col.simp.df) <- col.simp.df$codcen
<- col.simp.df %>% select(-codcen)
col.simp.df <- as.matrix(col.simp.df) col.mat
Determinar el número de clusters:
<- fviz_nbclust(col.simp.df, FUNcluster = clara, method = "wss", k.max = 8)
codo_col codo_col
<- codo_col$data$y
wss_values
# Calcular las diferencias porcentuales en el decremento de WSS
$data$perc_dec <- c(0, diff(wss_values) / head(wss_values, -1) * -100)
codo_col$data codo_col
clusters y perc_dec
1 1 113.71311 0.000000
2 2 76.65825 32.586269
3 3 39.06879 49.035112
4 4 29.23678 25.165908
5 5 19.58450 33.014146
6 6 18.13019 7.425863
7 7 14.14346 21.989431
8 8 12.64734 10.578159
<- clara(col.simp.df, 5)
fit
# Crear un data frame con los resultados del clúster
$cluster <- factor(fit$clustering)
col.simp.df
# Usar ggplot2 para visualizar los datos en un mapa
ggplot(col.simp.df, aes(x = lon, y = lat, color = cluster)) +
geom_point(aes(size = 3)) +
ggtitle("Visualización de Clústeres Geográficos") +
theme_minimal() +
labs(color = "Cluster") +
borders("world", xlim = range(col.simp.df$lon) + c(-0.1, 0.1), ylim = range(col.simp.df$lat) + c(-0.1, 0.1)) +
theme(legend.position = "right")
# Asumiendo que el número óptimo de clústeres es 3 y que ya tenemos los datos de clúster en col.simp.df
library(ggplot2)
library(maps)
Warning: package 'maps' was built under R version 4.3.3
Attaching package: 'maps'
The following object is masked from 'package:cluster':
votes.repub
The following object is masked from 'package:faraway':
ozone
The following object is masked from 'package:purrr':
map
# Establecer los límites basándose en los datos proporcionados, centrados y con un pequeño margen alrededor
<- range(col.simp.df$lon) + c(-0.05, 0.05) # pequeño margen longitudinal
lon_range <- range(col.simp.df$lat) + c(-0.05, 0.05) # pequeño margen latitudinal
lat_range
# Crear el gráfico
ggplot(col.simp.df, aes(x = lon, y = lat, color = cluster)) +
geom_point(size = 2) + # puntos más grandes para mejor visualización
ggtitle("Visualización de Clústeres Geográficos con Zoom") +
theme_minimal() +
labs(color = "Cluster") +
borders("world", xlim = lon_range, ylim = lat_range) + # añadir bordes de mapa con el rango ajustado
coord_fixed(ratio = 1 / cos(mean(lat_range) * pi / 180)) + # mantener la proporción adecuada en el mapa
xlim(lon_range) + # establecer límites longitudinales
ylim(lat_range) + # establecer límites latitudinales
theme(legend.position = "right")