Metodos de validación de clusters Por qué es importante validar un cluster? Los métodos de clustering siempre van a encontrar grupos, aun cuando no haya patrones. Puede ser un primer paso para determinar el número correcto de clusters. Es una herramienta para comparar diferentes métodos de clustering aplicados al mismo dataset. Una breve introducción y algunas definiciones En algunas situaciones antes de comenzar con las tareas de clustering es necesario determinar si los datos realmente presentan alguna tendencia al agrupamiento. Asumiendo que esta tendencia existe, una vez que se realizan los análisis de agrupamiento se continúa con la validación. Validar un agrupamiento significa determinar si los clusters obtenidos reflejen la presencia de grupos en los datos y la separación de dichos grupos en el espacio de variables. La concordancia entre número de clusters y de grupos "reales" se puede evaluar de dos formas diferentes. Si existe algún información sobre la existencia de clases dentro del conjunto de datos y esa información sobre clases no se utilizó para el clustering, se puede realizar la validación externa de los clusters. Este es, calcular alguna métrica que revele la concordancia entre las clases conocidas y los clusters obtenidos. De manera complementaria, o como alternativa, si no se cuenta con información de clases, se puede realizar una validación interna. Este tipo de validación consiste en determinar si los clusters encontrados tienen suficiente separación entre si, y cohesión dentro de cada uno de ellos. Existen diferentes medidas de validación externa e interna y no existe un criterio único para determinar cuál es la mejor. Hay que tener en cuenta que no hay una medida única que se pueda usar para todos los métodos de clustering y en algunos casos la eficiencia de estas medidas todavía no se estudió en profundidad.
Dos criterios importantes: Cohesión: mide las proximidades de los miembros de un cluster con respecto al prototipo (centroide o medoide). Separación: es la proximidad entre miembros de diferentes clusters o entre prototipos de grupos y el prototipo general Y algunas fórmulas derivadas: Suma de los errores al cuadrado: Donde K es el número total de clusters, ci es el centroide del cluster Ci. La suma total de cuadrados (TSE) es igual a la suma de errores al cuadrado (SSE) más la suma de cuadrados de separación (SSB): Como material de referencia para este tema se puede consultar: El capítulo 8 de "Introduction to Data Mining" de Tan, Steinbach & Kumar (este capítulo está * disponible gratis aqui). El capítuo 23 de "Data Clustering. Algorithms and Applications", editado por Vipin Kumar. Una versión casi idéntica a este capítulo se encuentra en este artículo Preparación de un dataset Para poder tener control sobre los datos vamos a crear tres datasets, cada uno con 200 casos y 3 variables. La diferencia entre ellos es que las variables del primero no tienen ruido, y el segundo y tercero van a tener ruido creciente. a <- c(rep(2,40), rep(6,40), rep(12,40), rep(19,40), rep(26,40)) b <- c(rep(3,40), rep(6,40), rep(14,120)) c <- c(rep(12,80), rep(18,40), rep(2,40), rep(30,40)) a.r1 <- a + rnorm(200, 0, 0.5) b.r1 <- b + rnorm(200, 0, 0.5) c.r1 <- c + rnorm(200, 0, 0.5) a.r2 <- a + rnorm(200, 0, 3)
b.r2 <- b + rnorm(200, 0, 3) c.r2 <- c + rnorm(200, 0, 4) Ahora les asignamos clases a los objetos. La elección de los valores que toma cada variable determina cinco clases. Hay que tener en cuenta que el ruido agregado va a afectar a las posiciones de los objetos en el espacio muestral del segundo dataset. Esto significa que algunas asignaciones de clase pueden resultar arbitrarias. dat.clase <- c(rep("a",40), rep("b",40), rep("c",40), rep("d", 40), rep("e",40)) Agrupamos las variables con y sin ruido en dos matrices diferentes: # aleatorizamos el orden de los registros (para hacerlos parecer más "reales", no es necesario) randord <- sample(1:200, 200) # dat es la matriz original: dat <- as.matrix(cbind(a,b,c)) row.names(dat) <- paste0("obj", 1:200) dat <- dat[randord,] # dat.r1 es la matriz con ruido: dat.r1 <- as.matrix(cbind(a.r1, b.r1, c.r1)) row.names(dat.r1) <- paste0("obj", 1:200) dat.r1 <- dat.r1[randord,] # dat.r2 es la matriz con más ruido: dat.r2 <- as.matrix(cbind(a.r2, b.r2, c.r2)) row.names(dat.r2) <- paste0("obj", 1:200) dat.r2 <- dat.r2[randord,] # ordenamos las etiquetas de clase dat.clase <- dat.clase[randord] # Escalamos todos los datos entre 0 y 1 para simplificar los pasos que siguen esc01 <- function(x) { (x - min(x)) / (max(x) - min(x))} dat.nrm <- apply(dat, 2, esc01) dat.r1.nrm <- apply(dat.r1, 2, esc01) dat.r2.nrm <- apply(dat.r2, 2, esc01) Podemos mirar rápidamente el efecto del agregado de ruido en los tres datasets: dat.nrm.dist <- dist(dat.nrm) dat.clus <- hclust(dat.nrm.dist) dat.r1.dist <- dist(dat.r1.nrm) dat.r1.clus <- hclust(dat.r1.dist)
dat.r2.dist <- dist(dat.r2.nrm) dat.r2.clus <- hclust(dat.r2.dist) par(mfrow=c(1,3)) plot( as.dendrogram( dat.clus ), leaflab="none", main="dat.nrm") plot( as.dendrogram( dat.r1.clus ), leaflab="none", main="dat.r1.nrm") plot( as.dendrogram( dat.r2.clus ), leaflab="none", main="dat.r2.nrm") par(mfrow=c(1,1)) Medición de la tendencia al clustering Podemos plantearnos si, como sugieren los gráficos anteriores, el agregado de ruido aleatorio no pudo haber destruido cualquier tendencia al agrupamiento, especialmente en dat.r2.nrm. Esto es, Los datos quedaron dispersos de manera aleatoria en el espacio muestral? Para averiguar esto podemos usar el estadístico de Hopkins de tendencia al clustering, (está descrito en el libro "Introduction to Data Mining" de Tan, Steinbach & Kumar, p547). El método consiste en generar p puntos distribuidos al azar en el espacio muestral, y además extraer p puntos reales del dataset. Luego se calculan las distancias a los vecinos más próximos de los datos generados al azar (u) y de los datos extraídos del dataset (w), con estos se calcula:
Usamos el paquete RANN de R para determinar la distancia al vecino más cercano y asignamos a la variable cant.muestras el número de puntos que se van a analizar. if (!require("rann")) install.packages("rann") ## Loading required package: RANN library(rann) Creamos 20 puntos al azar en el espacio de muestreo: cant.muestras <- 20 rnd.pts <- cbind(runif(cant.muestras, 0, 1), runif(cant.muestras, 0, 1), runif(cant.muestras, 0, 1)) Y ahora seleccionamos al azar 20 indices de las tres matrices smp <- sample(nrow(dat.nrm), 20) smp.dat <- dat.nrm[smp,] smp.r1 <- dat.r1.nrm[smp,] smp.r2 <- dat.r2.nrm[smp,] Calculamos la tendencia al clustering de los datos sin ruido del primer dataset: # calculo de las distancias al vecino más cercano para los datos reales smp.pts.dist <- nn2(as.data.frame(dat.nrm), as.data.frame(smp.dat), k=2)$nn.dist[,2] # calculo de las distancias al vecino más cercano para datos al azar rnd.pts.dist <- nn2(as.data.frame(dat.nrm), as.data.frame(rnd.pts), k=1)$nn.dists # calculo del estadistico sum(smp.pts.dist) / (sum(smp.pts.dist) + sum(rnd.pts.dist)) ## [1] 0 Ahora para los datos con ruido intermedio: smp.pts.dist <- nn2(as.data.frame(dat.r1.nrm), as.data.frame(smp.r1), k=2)$nn.dist[,2] rnd.pts.dist <- nn2(as.data.frame(dat.r1.nrm), as.data.frame(rnd.pts), k=1)$nn.dists sum(smp.pts.dist) / (sum(smp.pts.dist) + sum(rnd.pts.dist)) ## [1] 0.03196105
Ahora para los datos con ruido intermedio smp.pts.dist <- nn2(as.data.frame(dat.r2.nrm), as.data.frame(smp.r2), k=2)$nn.dist[,2] rnd.pts.dist <- nn2(as.data.frame(dat.r2.nrm), as.data.frame(rnd.pts), k=1)$nn.dists sum(smp.pts.dist) / (sum(smp.pts.dist) + sum(rnd.pts.dist)) ## [1] 0.2220164: En el primer caso la tendencia al clustering es cero, indicando que es la más mayor posible. Esto sucede porque para cada clase hay varios elementos idénticos, por lo que la distancia al vecino más cercano va a ser cero para cualquier punto. Al aumentar el ruido la tendencia al clustering baja, pero no pasa el umbral de 0.5. Validación externa de un cluster La discusión que sigue está enfocada sobre en todo la validación de clusters obtenidos por el método de k-medias. En la validación externa de un agrupamiento se utiliza información que no está presente en los datos con los que se hace el agrupamiento. En nuestro caso la variable dat.clase, que habíamos construido antes, registra la pertenencia a las diferentes clases de cada uno de los 200 registros del dataset de prueba. Para poder validar primero necesitamos algunos cluster: dat.kmeans <- kmeans(dat.nrm, centers=5) head(dat.kmeans$cluster) ## obj127 obj114 obj45 obj128 obj194 obj175 ## 3 1 2 3 4 4 dat.conf <- table(dat.kmeans$cluster, dat.clase, dnn = c("cluster", "clase")) dat.conf ## clase ## cluster a b c d e ## 1 0 0 40 0 0 ## 2 0 40 0 0 0 ## 3 0 0 0 40 0 ## 4 0 0 0 0 40 ## 5 40 0 0 0 0 # K = 5. OK dat.r2.kmeans <- kmeans(dat.r2.nrm, centers=5) dat.r2.conf <- table(dat.r2.kmeans$cluster, dat.clase, dnn =
c("cluster", "clase")) dat.r2.conf ## clase ## cluster a b c d e ## 1 0 0 36 0 0 ## 2 31 15 0 0 0 ## 3 0 0 0 38 0 ## 4 9 25 4 2 0 ## 5 0 0 0 0 40 # k = 2 dat.r2.kmeans.mal.1 <- kmeans(dat.r2.nrm, centers=2) dat.r2.mal.1.conf <- table(dat.r2.kmeans.mal.1$cluster, dat.clase, dnn = c("cluster", "clase")) dat.r2.mal.1.conf ## clase ## cluster a b c d e ## 1 0 0 28 8 40 ## 2 40 40 12 32 0 # k = 10 dat.r2.kmeans.mal.2 <- kmeans(dat.r2.nrm, centers=10) dat.r2.mal.2.conf <- table(dat.r2.kmeans.mal.2$cluster, dat.clase, dnn = c("cluster", "clase")) dat.r2.mal.2.conf ## clase ## cluster a b c d e ## 1 11 7 0 0 0 ## 2 6 16 0 0 0 ## 3 0 0 0 23 0 ## 4 0 0 0 0 40 ## 5 0 0 0 15 0 ## 6 0 0 21 0 0 ## 7 9 4 0 0 0 ## 8 14 3 0 0 0 ## 9 0 0 13 0 0 ## 10 0 10 6 2 0 En el capítulo recomendado de Xiong y Li (o el paper Wu, Xiong y Chen) se realiza una evaluación comparativa de diferentes medidas de validación externa y la que resulta mejor es la medida normalizada de van Dongen: A continuación, una función simplificada en R para calcular esta medida:
vdn <- function(matconf){ n2 <- 2 * sum(matconf) sum.i <- sum(apply(matconf,1,max)) sum.j <- sum(apply(matconf,2,max)) max.i <- max(rowsums(matconf)) max.j <- max(colsums(matconf)) vd.n <- (n2 - sum.i - sum.j) / (n2-max.i-max.j) return(vd.n) } Y la evaluación de la medida normalizada de van Dongen para los tres agrupamientos de k-medias anteriores (cuanto menor es el valor, mejor): vdn(dat.r2.conf) ## [1] 0.1910828 vdn(dat.r2.mal.1.conf) ## [1] 0.5932203 vdn(dat.r2.mal.2.conf) ## [1] 0.35625 Es importante hacer este tipo de evaluaciones porque el método de k-medias tiende a formar grupos de tamaño uniforme, aun cuando las clases sean claramente no balanceadas. Esto se llama el "efecto uniforme". Una forma rápida de evaluarlo es calcular el coeficiente de variación (CV = desvío estándar/media) de la distribución del tamaño de las clases. Por ejemplo supongamos dos datasets con 6 clases. En el primer dataset el tamaño de cada clase es bastante uniforme y el segundo es sesgado: ds1.n.clases <- c(55, 58, 51, 59, 49, 50) ds2.n.clases <- c(5, 17, 62, 30, 128, 80) cv.ds1.n.clases <- sd(ds1.n.clases) / mean(ds1.n.clases) cv.ds1.n.clases ## [1] 0.07963886 cv.ds2.n.clases <- sd(ds2.n.clases) / mean(ds2.n.clases) cv.ds2.n.clases ## [1] 0.8563864 En forma empírica se mostró que si las clases presentan un CV mayor que 0.85 es bastante posible que el método de k-medias vaya a introducir alguna distorsión en el resultado. Si se están probando variaciones de métodos o parámetros de
clustering, una forma rápida de evaluar el efecto uniforme es calcular diferencias entre el CV de las clases conocidas y las de los variantes probadas. El método de k-medias también puede ser sensible a diferencias en densidad de los grupos y a la presencia de grupos no esféricos. Validación interna: análisis con Silhouette en agrupamientos por partición El análisis de Silhouette es útil para analizar la cohesión y separación dentro de un grafo. Procedimiento: Para cada objeto i calcular su distancia promedio a todos los otros objetos de su cluster. Llamar a este valor ai. Para el objeto i y todos los otros clusters que no lo contienen, calcular las distancias promedio a todos los objetos de cada cluster. buscar el mínimo y llamarlo bi. El coeficiente Silhouette (si) del objeto i es: Algunos ejemplos en R: library(cluster) dat.r2.kmeans.sil <- silhouette(dat.r2.kmeans$cluster, dat.r2.dist) # salida tabulada: summary(dat.r2.kmeans.sil) ## Silhouette of 200 units in 5 clusters from silhouette.default(x = dat.r2.kmeans$cluster, dist = dat.r2.dist) : ## Cluster sizes and average silhouette widths: ## 36 46 38 40 40 ## 0.4888149 0.3341678 0.4606900 0.2159676 0.5762980 ## Individual silhouette widths: ## Min. 1st Qu. Median Mean 3rd Qu. Max. ## 0.0006365 0.2676000 0.4501000 0.4108000 0.5610000 0.7040000 summary(dat.r2.kmeans.sil)$avg.width ## [1] 0.4108295
summary(dat.r2.kmeans.sil)$clus.avg.widths ## 1 2 3 4 5 ## 0.4888149 0.3341678 0.4606900 0.2159676 0.5762980 # salida grafica plot(silhouette(dat.r2.kmeans$cluster, dat.r2.dist)) Calidad de clusters jerárquicos Anteriormente habíamos construido clusters jerárquicos para realizar un análisis exploratorio rápido. en esta sección vamos a profundizar la validación de este tipo de agrupamientos. Primero hacemos un gráfico de calor para estudiar las relaciones de distancia: library(lattice) levelplot(as.matrix(dat.nrm.dist))
Sirve? Qué pasó?. Es importante ordenar: # dat levelplot(as.matrix(dat.nrm.dist)[dat.clus$order, dat.clus$order], scales=list(y=list(at=c(1),labels=""), x=list(at=c(1),labels=""))) # dat.r1 levelplot(as.matrix(dat.r1.dist)[dat.r1.clus$order, dat.r1.clus$order], scales=list(y=list(at=c(1),labels=""), x=list(at=c(1),labels="")))
# dat.r1 levelplot(as.matrix(dat.r2.dist)[dat.r2.clus$order, dat.r2.clus$order], scales=list(y=list(at=c(1),labels=""), x=list(at=c(1),labels="")))
Cómo se interpretan estos resultados? Sobre la diagonal se ubican los clusters, que no deberían tener similitud con los otros clusters, que se observarían como zonas de color rosado alejados de la diagonal principal. Los dos clusters que se ubican en el extremo inferior izquierdo son clusters con datos relativamente similares entre sí (ver los dendrogramas de más arriba). A medida que aumenta el ruido, se pierde la separación clara entre clusters. El coeficiente de correlación cofenético Otra medida de calidad de los clusters jerárquicos es el cálculo del coeficiente de correlación cofenético, que mide la correlación entre la matris de distancia que dio origen al agrupamiento y los distancias extraidas del árbol. cor(dist(dat.nrm), cophenetic(dat.clus)) ## [1] 0.9250305 cor(dist(dat.r1.nrm), cophenetic(dat.r1.clus)) ## [1] 0.9030483 Técnicas de bootstraping Otra alternativa para evaluar la significación de las ramas de un agrupamiento jerárquicos es mediante la aplicación de técnicas de bootstraping. Este tema lo vamos a mirar con más atención un poco más adelante. Partición de un cluster jerárquico. Se pueden extraer grupos de un cluster jerárquico estableciendo un punto de corte, con esto se puede construir una matriz de confusión muy básica para realizar una validación externa de la calidad del cluster: dat.r.clus.gr <- cutree(dat.r1.clus, h=0.3) plot(dat.r1.clus) rect.hclust(dat.r1.clus, h=0.3, border="red")
table(dat.r.clus.gr, dat.clase) ## dat.clase ## dat.r.clus.gr a b c d e ## 1 0 0 0 40 0 ## 2 0 0 40 0 0 ## 3 0 40 0 0 0 ## 4 0 0 0 0 40 ## 5 40 0 0 0 0