Tiene razón en que la agrupación de k-means no debe hacerse con datos de tipos mixtos. Dado que k-means es esencialmente un algoritmo de búsqueda simple para encontrar una partición que minimice las distancias euclidianas cuadradas dentro del clúster entre las observaciones agrupadas y el centroide del clúster, solo debe usarse con datos donde las distancias euclidianas cuadradas serían significativas.
yoyo′
En este punto, puede usar cualquier método de agrupación que pueda operar sobre una matriz de distancia en lugar de necesitar la matriz de datos original. (Tenga en cuenta que k-means necesita lo último). Las opciones más populares son la partición alrededor de medoides (PAM, que es esencialmente lo mismo que k-means, pero utiliza la observación más central en lugar del centroide), varios enfoques de agrupamiento jerárquico (por ejemplo , mediana, enlace único y enlace completo; con la agrupación jerárquica necesitará decidir dónde ' cortar el árbol ' para obtener las asignaciones finales del grupo) y DBSCAN, que permite formas de grupo mucho más flexibles.
Aquí hay una R
demostración simple (nb, en realidad hay 3 grupos, pero la mayoría de los datos parecen 2 grupos apropiados):
library(cluster) # we'll use these packages
library(fpc)
# here we're generating 45 data in 3 clusters:
set.seed(3296) # this makes the example exactly reproducible
n = 15
cont = c(rnorm(n, mean=0, sd=1),
rnorm(n, mean=1, sd=1),
rnorm(n, mean=2, sd=1) )
bin = c(rbinom(n, size=1, prob=.2),
rbinom(n, size=1, prob=.5),
rbinom(n, size=1, prob=.8) )
ord = c(rbinom(n, size=5, prob=.2),
rbinom(n, size=5, prob=.5),
rbinom(n, size=5, prob=.8) )
data = data.frame(cont=cont, bin=bin, ord=factor(ord, ordered=TRUE))
# this returns the distance matrix with Gower's distance:
g.dist = daisy(data, metric="gower", type=list(symm=2))
Podemos comenzar buscando en diferentes números de clústeres con PAM:
# we can start by searching over different numbers of clusters with PAM:
pc = pamk(g.dist, krange=1:5, criterion="asw")
pc[2:3]
# $nc
# [1] 2 # 2 clusters maximize the average silhouette width
#
# $crit
# [1] 0.0000000 0.6227580 0.5593053 0.5011497 0.4294626
pc = pc$pamobject; pc # this is the optimal PAM clustering
# Medoids:
# ID
# [1,] "29" "29"
# [2,] "33" "33"
# Clustering vector:
# 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
# 1 1 1 1 1 2 1 1 1 1 1 2 1 2 1 2 2 1 1 1 2 1 2 1 2 2
# 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
# 1 2 1 2 2 1 2 2 2 2 1 2 1 2 2 2 2 2 2
# Objective function:
# build swap
# 0.1500934 0.1461762
#
# Available components:
# [1] "medoids" "id.med" "clustering" "objective" "isolation"
# [6] "clusinfo" "silinfo" "diss" "call"
Esos resultados se pueden comparar con los resultados de la agrupación jerárquica:
hc.m = hclust(g.dist, method="median")
hc.s = hclust(g.dist, method="single")
hc.c = hclust(g.dist, method="complete")
windows(height=3.5, width=9)
layout(matrix(1:3, nrow=1))
plot(hc.m)
plot(hc.s)
plot(hc.c)
El método mediano sugiere 2 (posiblemente 3) grupos, el único solo admite 2, pero el método completo podría sugerir 2, 3 o 4 a mi ojo.
Finalmente, podemos probar DBSCAN. Esto requiere especificar dos parámetros: eps, la 'distancia de alcance' (qué tan cerca deben estar vinculadas dos observaciones) y minPts (el número mínimo de puntos que deben conectarse entre sí antes de que esté dispuesto a llamarlos un 'racimo'). Una regla general para minPts es usar una más que el número de dimensiones (en nuestro caso 3 + 1 = 4), pero no se recomienda tener un número que sea demasiado pequeño. El valor predeterminado para dbscan
es 5; nos quedaremos con eso. Una forma de pensar en la distancia de alcance es ver qué porcentaje de las distancias son menores que cualquier valor dado. Podemos hacerlo examinando la distribución de las distancias:
windows()
layout(matrix(1:2, nrow=1))
plot(density(na.omit(g.dist[upper.tri(g.dist)])), main="kernel density")
plot(ecdf(g.dist[upper.tri(g.dist)]), main="ECDF")
Las distancias mismas parecen agruparse en grupos visualmente discernibles de "más cerca" y "más lejos". Un valor de .3 parece distinguir más claramente entre los dos grupos de distancias. Para explorar la sensibilidad de la salida a diferentes opciones de eps, también podemos probar .2 y .4:
dbc3 = dbscan(g.dist, eps=.3, MinPts=5, method="dist"); dbc3
# dbscan Pts=45 MinPts=5 eps=0.3
# 1 2
# seed 22 23
# total 22 23
dbc2 = dbscan(g.dist, eps=.2, MinPts=5, method="dist"); dbc2
# dbscan Pts=45 MinPts=5 eps=0.2
# 1 2
# border 2 1
# seed 20 22
# total 22 23
dbc4 = dbscan(g.dist, eps=.4, MinPts=5, method="dist"); dbc4
# dbscan Pts=45 MinPts=5 eps=0.4
# 1
# seed 45
# total 45
El uso eps=.3
proporciona una solución muy limpia, que (al menos cualitativamente) está de acuerdo con lo que vimos de otros métodos anteriores.
Dado que no hay un grupo 1 significativo , debemos tener cuidado de tratar de hacer coincidir qué observaciones se llaman 'grupo 1' de diferentes grupos. En cambio, podemos formar tablas y si la mayoría de las observaciones llamadas 'grupo 1' en un ajuste se llaman 'grupo 2' en otro, veríamos que los resultados siguen siendo sustancialmente similares. En nuestro caso, los diferentes agrupamientos son en su mayoría muy estables y colocan las mismas observaciones en los mismos grupos cada vez; solo el agrupamiento jerárquico de enlace completo difiere:
# comparing the clusterings
table(cutree(hc.m, k=2), cutree(hc.s, k=2))
# 1 2
# 1 22 0
# 2 0 23
table(cutree(hc.m, k=2), pc$clustering)
# 1 2
# 1 22 0
# 2 0 23
table(pc$clustering, dbc3$cluster)
# 1 2
# 1 22 0
# 2 0 23
table(cutree(hc.m, k=2), cutree(hc.c, k=2))
# 1 2
# 1 14 8
# 2 7 16
Por supuesto, no hay garantía de que ningún análisis de clúster recupere los verdaderos clústeres latentes en sus datos. La ausencia de las verdaderas etiquetas de clúster (que estarían disponibles en, por ejemplo, una situación de regresión logística) significa que una enorme cantidad de información no está disponible. Incluso con conjuntos de datos muy grandes, los clústeres pueden no estar suficientemente bien separados para ser perfectamente recuperables. En nuestro caso, dado que conocemos la verdadera membresía del clúster, podemos comparar eso con la salida para ver qué tan bien funcionó. Como señalé anteriormente, en realidad hay 3 grupos latentes, pero los datos dan la apariencia de 2 grupos:
pc$clustering[1:15] # these were actually cluster 1 in the data generating process
# 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
# 1 1 1 1 1 2 1 1 1 1 1 2 1 2 1
pc$clustering[16:30] # these were actually cluster 2 in the data generating process
# 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
# 2 2 1 1 1 2 1 2 1 2 2 1 2 1 2
pc$clustering[31:45] # these were actually cluster 3 in the data generating process
# 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
# 2 1 2 2 2 2 1 2 1 2 2 2 2 2 2