Agrupación jerárquica de datos categóricos en R

La traducción fue preparada para los estudiantes del curso "Análisis aplicado en R" .





Este fue mi primer intento de agrupar clientes basados ​​en datos reales, y me dio una experiencia valiosa. Hay muchos artículos en Internet sobre la agrupación en clúster utilizando variables numéricas, pero encontrar soluciones para datos categóricos, que es algo más difícil, no fue tan simple. Los métodos de agrupamiento para datos categóricos todavía están en desarrollo, y en otra publicación voy a probar con otro.

Por otro lado, muchas personas piensan que agrupar datos categóricos puede no producir resultados significativos, y esto es en parte cierto (ver la excelente discusión sobre CrossValidated ). En un momento, pensé: “¿Qué estoy haciendo? Simplemente se pueden dividir en cohortes ". Sin embargo, el análisis de cohortes tampoco siempre es aconsejable, especialmente con un número significativo de variables categóricas con una gran cantidad de niveles: puede manejar fácilmente de 5 a 7 cohortes, pero si tiene 22 variables y cada una tiene 5 niveles (por ejemplo, una encuesta de clientes con estimaciones discretas 1 , 2, 3, 4 y 5), y necesita comprender con qué grupos característicos de clientes está tratando: obtendrá cohortes de 22x5. Nadie quiere molestarse con tal tarea. Y aquí la agrupación podría ayudar. Entonces, en esta publicación, hablaré sobre lo que a mí mismo me gustaría saber tan pronto como comience a agrupar.

El proceso de agrupación en sí consta de tres pasos:

  1. Construir una matriz de disimilitud es, sin duda, la decisión más importante en la agrupación. Todos los pasos posteriores se basarán en la matriz de disimilitud que creó.
  2. La elección del método de agrupamiento.
  3. Evaluación de clúster.

Esta publicación será una especie de introducción que describe los principios básicos de la agrupación y su implementación en el entorno R.

Matriz de disimilitud


La base para la agrupación será la matriz de disimilitud, que en términos matemáticos describe la diferencia entre los puntos del conjunto de datos. Le permite combinar aún más en los grupos aquellos puntos que están más cercanos entre sí, o separar los más distantes entre sí; esta es la idea principal de la agrupación.

En esta etapa, las diferencias entre los tipos de datos son importantes, ya que la matriz de disimilitud se basa en las distancias entre puntos de datos individuales. Es fácil imaginar las distancias entre los puntos de datos numéricos (un ejemplo bien conocido son las distancias euclidianas ), pero en el caso de los datos categóricos (factores en R), no todo es tan obvio.

Para construir una matriz de disimilitud en este caso, se debe utilizar la llamada distancia de Gover. No profundizaré en la parte matemática de este concepto, simplemente proporcionaré enlaces: aquí y allá . Para esto, prefiero usar daisy() con la metric = c("gower") del paquete del cluster .

 #-----   -----# #    ,       ,     ,   ,    library(dplyr) #     set.seed(40) #     #    ;   data.frame()     #    ,   200   1  200 id.s <- c(1:200) %>% factor() budget.s <- sample(c("small", "med", "large"), 200, replace = T) %>% factor(levels=c("small", "med", "large"), ordered = TRUE) origins.s <- sample(c("x", "y", "z"), 200, replace = T, prob = c(0.7, 0.15, 0.15)) area.s <- sample(c("area1", "area2", "area3", "area4"), 200, replace = T, prob = c(0.3, 0.1, 0.5, 0.2)) source.s <- sample(c("facebook", "email", "link", "app"), 200, replace = T, prob = c(0.1,0.2, 0.3, 0.4)) ##   —      dow.s <- sample(c("mon", "tue", "wed", "thu", "fri", "sat", "sun"), 200, replace = T, prob = c(0.1, 0.1, 0.2, 0.2, 0.1, 0.1, 0.2)) %>% factor(levels=c("mon", "tue", "wed", "thu", "fri", "sat", "sun"), ordered = TRUE) #  dish.s <- sample(c("delicious", "the one you don't like", "pizza"), 200, replace = T) #   data.frame()      synthetic.customers <- data.frame(id.s, budget.s, origins.s, area.s, source.s, dow.s, dish.s) #-----   -----# library(cluster) #       #   : daisy(), diana(), clusplot() gower.dist <- daisy(synthetic.customers[ ,2:7], metric = c("gower")) # class(gower.dist) ## ,  

La matriz de disimilitud está lista. Para 200 observaciones, se construye rápidamente, pero puede requerir una gran cantidad de cómputo si se trata de un conjunto de datos grande.

En la práctica, es muy probable que primero tenga que limpiar el conjunto de datos, realizar las transformaciones necesarias de las filas en factores y rastrear los valores faltantes. En mi caso, el conjunto de datos también contenía filas de valores perdidos que se agrupaban maravillosamente cada vez, por lo que parecía que era un tesoro, hasta que miré los valores (¡ay!).

Algoritmos de agrupamiento


Es posible que ya sepa que la agrupación es k-means y jerárquica . En esta publicación, me concentro en el segundo método, ya que es más flexible y permite varios enfoques: puede elegir un algoritmo de agrupamiento aglomerativo (de abajo hacia arriba) o divisional (de arriba hacia abajo).


Fuente: Guía de programación de UC Business Analytics R

La agrupación aglomerativa comienza con n agrupaciones, donde n es el número de observaciones: se supone que cada una de ellas es una agrupación separada. Luego, el algoritmo intenta encontrar y agrupar los puntos de datos más similares entre ellos; así es como comienza la formación de conglomerados.

La agrupación divisional se realiza de manera opuesta: inicialmente se supone que todos los n puntos de datos que tenemos son un grupo grande, y luego los menos similares se dividen en grupos separados.

Al decidir cuál de estos métodos elegir, siempre tiene sentido probar todas las opciones, sin embargo, en general, la agrupación aglomerativa es mejor para identificar grupos pequeños y es utilizada por la mayoría de los programas de computadora, y la agrupación divisional es más apropiada para identificar grupos grandes .

Personalmente, antes de decidir qué método usar, prefiero mirar los dendrogramas, una representación gráfica de la agrupación. Como verá más adelante, algunos dendrogramas están bien equilibrados, mientras que otros son muy caóticos.

# La entrada principal para el siguiente código es la disimilitud (matriz de distancia)
 #             #            —         —    #------------  ------------# divisive.clust <- diana(as.matrix(gower.dist), diss = TRUE, keep.diss = TRUE) plot(divisive.clust, main = "Divisive") 



 #------------   ------------# #      #         —     ,      #    (complete linkages) aggl.clust.c <- hclust(gower.dist, method = "complete") plot(aggl.clust.c, main = "Agglomerative, complete linkages") 

Evaluación de calidad de agrupamiento


En esta etapa, es necesario elegir entre diferentes algoritmos de agrupación y un número diferente de agrupaciones. Puede usar diferentes métodos de evaluación, sin olvidar dejarse guiar por el sentido común . Destaqué estas palabras en negrita y cursiva, porque el significado de la elección es muy importante : el número de grupos y el método de dividir datos en grupos debería ser práctico desde un punto de vista práctico. El número de combinaciones de valores de variables categóricas es finito (ya que son discretas), pero no será significativo ningún desglose basado en ellas. Es posible que tampoco desee tener muy pocos grupos; en este caso, serán demasiado generalizados. Al final, todo depende de su objetivo y las tareas del análisis.

En general, al crear grupos, le interesa obtener grupos de puntos de datos claramente definidos, de modo que la distancia entre dichos puntos dentro del grupo ( o compacidad ) sea mínima, y ​​la distancia entre grupos ( separabilidad ) sea la máxima posible. Esto es fácil de entender intuitivamente: la distancia entre puntos es una medida de su disimilitud, obtenida en base a la matriz de disimilitud. Por lo tanto, la evaluación de la calidad de la agrupación se basa en la evaluación de la compacidad y la separabilidad.

A continuación, demostraré dos enfoques y mostraré que uno de ellos puede dar resultados sin sentido.

  • Método de codo : comience con él si el factor más importante para su análisis es la compacidad de los grupos, es decir, la similitud dentro de los grupos.
  • Método de evaluación de siluetas : el gráfico de silueta utilizado como una medida de consistencia de datos muestra qué tan cerca está cada uno de los puntos dentro de un grupo a los puntos en los grupos vecinos.

En la práctica, estos dos métodos a menudo dan resultados diferentes, lo que puede generar cierta confusión: la compactación máxima y la separación más clara se lograrán con un número diferente de grupos, por lo que el sentido común y la comprensión de lo que realmente significan sus datos jugarán un papel importante Al tomar una decisión final.

También hay una serie de métricas que puede analizar. Los agregaré directamente al código.

 #      ,        #      ,     ,   —   #     ,      ,         ,   ,     library(fpc) cstats.table <- function(dist, tree, k) { clust.assess <- c("cluster.number","n","within.cluster.ss","average.within","average.between", "wb.ratio","dunn2","avg.silwidth") clust.size <- c("cluster.size") stats.names <- c() row.clust <- c() output.stats <- matrix(ncol = k, nrow = length(clust.assess)) cluster.sizes <- matrix(ncol = k, nrow = k) for(i in c(1:k)){ row.clust[i] <- paste("Cluster-", i, " size") } for(i in c(2:k)){ stats.names[i] <- paste("Test", i-1) for(j in seq_along(clust.assess)){ output.stats[j, i] <- unlist(cluster.stats(d = dist, clustering = cutree(tree, k = i))[clust.assess])[j] } for(d in 1:k) { cluster.sizes[d, i] <- unlist(cluster.stats(d = dist, clustering = cutree(tree, k = i))[clust.size])[d] dim(cluster.sizes[d, i]) <- c(length(cluster.sizes[i]), 1) cluster.sizes[d, i] } } output.stats.df <- data.frame(output.stats) cluster.sizes <- data.frame(cluster.sizes) cluster.sizes[is.na(cluster.sizes)] <- 0 rows.all <- c(clust.assess, row.clust) # rownames(output.stats.df) <- clust.assess output <- rbind(output.stats.df, cluster.sizes)[ ,-1] colnames(output) <- stats.names[2:k] rownames(output) <- rows.all is.num <- sapply(output, is.numeric) output[is.num] <- lapply(output[is.num], round, 2) output } #     :      7 #     ,            stats.df.divisive <- cstats.table(gower.dist, divisive.clust, 7) stats.df.divisive 



Entonces, el indicador average.within, que representa la distancia promedio entre las observaciones dentro de los grupos, disminuye, al igual que dentro de.cluster.ss (la suma de los cuadrados de las distancias entre las observaciones en un grupo). El ancho promedio de la silueta (avg.silwidth) no cambia tan inequívocamente, sin embargo, todavía se puede notar una relación inversa.
Observe cuán desproporcionados son los tamaños de clúster. No me apresuraría a trabajar con un número incomparable de observaciones dentro de los grupos. Una de las razones es que el conjunto de datos puede estar desequilibrado, y algunos grupos de observaciones superarán a todos los demás en el análisis; esto no es bueno y probablemente conducirá a errores.

stats.df.aggl <-cstats.table(gower.dist, aggl.clust.c, 7) #

stats.df.aggl



Observe cuánto mejor se equilibra el número de observaciones por grupo mediante el agrupamiento jerárquico aglomerativo basado en el método de comunicación completo.

 # ---------    ---------# #   «»       #    ,     7  library(ggplot2) #  #   ggplot(data = data.frame(t(cstats.table(gower.dist, divisive.clust, 15))), aes(x=cluster.number, y=within.cluster.ss)) + geom_point()+ geom_line()+ ggtitle("Divisive clustering") + labs(x = "Num.of clusters", y = "Within clusters sum of squares (SS)") + theme(plot.title = element_text(hjust = 0.5)) 



Entonces, hemos creado un gráfico del "codo". Muestra cómo la suma de las distancias al cuadrado entre las observaciones (la usamos como una medida de la proximidad de las observaciones; cuanto más pequeña es, más cercanas están las mediciones dentro del grupo) varía para un número diferente de grupos. Idealmente, deberíamos ver una "curva de codo" distinta en el punto donde la agrupación adicional solo da una ligera disminución en la suma de cuadrados (SS). Para el gráfico a continuación, me detendría en aproximadamente 7. Aunque en este caso uno de los grupos consistirá en solo dos observaciones. Veamos qué sucede durante la agrupación aglomerativa.

 #       ggplot(data = data.frame(t(cstats.table(gower.dist, aggl.clust.c, 15))), aes(x=cluster.number, y=within.cluster.ss)) + geom_point()+ geom_line()+ ggtitle("Agglomerative clustering") + labs(x = "Num.of clusters", y = "Within clusters sum of squares (SS)") + theme(plot.title = element_text(hjust = 0.5)) 



El “codo” aglomerativo es similar al divisional, pero el gráfico se ve más suave: las curvas no son tan pronunciadas. Al igual que con la agrupación divisional, me centraría en 7 agrupaciones, sin embargo, al elegir entre estos dos métodos, me gustan más los tamaños de agrupación que se obtienen mediante el método aglomerativo; es mejor que sean comparables entre sí.

 #  ggplot(data = data.frame(t(cstats.table(gower.dist, divisive.clust, 15))), aes(x=cluster.number, y=avg.silwidth)) + geom_point()+ geom_line()+ ggtitle("Divisive clustering") + labs(x = "Num.of clusters", y = "Average silhouette width") + theme(plot.title = element_text(hjust = 0.5)) 



Al usar el método de estimación de silueta, debe elegir la cantidad que proporciona el coeficiente de silueta máximo, porque necesita grupos que estén lo suficientemente separados como para considerarse separados.

El coeficiente de silueta puede variar de –1 a 1, con 1 correspondiente a una buena consistencia dentro de los grupos, y –1 no muy bueno.
En el caso de la tabla anterior, elegiría 9 en lugar de 5 grupos.

A modo de comparación: en el caso "simple", el gráfico de silueta es similar al siguiente. No como el nuestro, pero casi.


Fuente: Marineros de datos

 ggplot(data = data.frame(t(cstats.table(gower.dist, aggl.clust.c, 15))), aes(x=cluster.number, y=avg.silwidth)) + geom_point()+ geom_line()+ ggtitle("Agglomerative clustering") + labs(x = "Num.of clusters", y = "Average silhouette width") + theme(plot.title = element_text(hjust = 0.5)) 



El gráfico de ancho de la silueta nos dice: cuanto más divide el conjunto de datos, más claros se vuelven los grupos. Sin embargo, al final alcanzarás puntos individuales, y no necesitas esto. Sin embargo, esto es exactamente lo que verá si comienza a aumentar el número de clústeres k . Por ejemplo, para k=30 obtuve el siguiente gráfico:



Para resumir: cuanto más divida el conjunto de datos, mejores serán los grupos, pero no podremos alcanzar puntos individuales (por ejemplo, en el gráfico anterior seleccionamos 30 grupos, y solo tenemos 200 puntos de datos).

Entonces, el agrupamiento aglomerativo en nuestro caso me parece mucho más equilibrado: los tamaños de los conglomerados son más o menos comparables (¡solo mire un conglomerado de solo dos observaciones al dividir por el método divisional!), Y me detendría en 7 conglomerados obtenidos por este método. Veamos cómo se ven y de qué están hechos.

El conjunto de datos consta de 6 variables que deben visualizarse en 2D o 3D, por lo que debe trabajar duro. La naturaleza de los datos categóricos también impone algunas limitaciones, por lo que las soluciones preparadas pueden no funcionar. Necesito: a) ver cómo se dividen las observaciones en grupos, b) entender cómo se clasifican las observaciones. Por lo tanto, creé a) un dendrograma de color, b) un mapa de calor del número de observaciones por variable dentro de cada grupo.

 library("ggplot2") library("reshape2") library("purrr") library("dplyr") #    library("dendextend") dendro <- as.dendrogram(aggl.clust.c) dendro.col <- dendro %>% set("branches_k_color", k = 7, value = c("darkslategray", "darkslategray4", "darkslategray3", "gold3", "darkcyan", "cyan3", "gold3")) %>% set("branches_lwd", 0.6) %>% set("labels_colors", value = c("darkslategray")) %>% set("labels_cex", 0.5) ggd1 <- as.ggdend(dendro.col) ggplot(ggd1, theme = theme_minimal()) + labs(x = "Num. observations", y = "Height", title = "Dendrogram, k = 7") 



 #     ( ) ggplot(ggd1, labels = T) + scale_y_reverse(expand = c(0.2, 0)) + coord_polar(theta="x") 



 #  —   #    —       #    ,      clust.num <- cutree(aggl.clust.c, k = 7) synthetic.customers.cl <- cbind(synthetic.customers, clust.num) cust.long <- melt(data.frame(lapply(synthetic.customers.cl, as.character), stringsAsFactors=FALSE), id = c("id.s", "clust.num"), factorsAsStrings=T) cust.long.q <- cust.long %>% group_by(clust.num, variable, value) %>% mutate(count = n_distinct(id.s)) %>% distinct(clust.num, variable, value, count) # heatmap.c ,      — ,   ,     heatmap.c <- ggplot(cust.long.q, aes(x = clust.num, y = factor(value, levels = c("x","y","z", "mon", "tue", "wed", "thu", "fri","sat","sun", "delicious", "the one you don't like", "pizza", "facebook", "email", "link", "app", "area1", "area2", "area3", "area4", "small", "med", "large"), ordered = T))) + geom_tile(aes(fill = count))+ scale_fill_gradient2(low = "darkslategray1", mid = "yellow", high = "turquoise4") #            cust.long.p <- cust.long.q %>% group_by(clust.num, variable) %>% mutate(perc = count / sum(count)) %>% arrange(clust.num) heatmap.p <- ggplot(cust.long.p, aes(x = clust.num, y = factor(value, levels = c("x","y","z", "mon", "tue", "wed", "thu", "fri","sat", "sun", "delicious", "the one you don't like", "pizza", "facebook", "email", "link", "app", "area1", "area2", "area3", "area4", "small", "med", "large"), ordered = T))) + geom_tile(aes(fill = perc), alpha = 0.85)+ labs(title = "Distribution of characteristics across clusters", x = "Cluster number", y = NULL) + geom_hline(yintercept = 3.5) + geom_hline(yintercept = 10.5) + geom_hline(yintercept = 13.5) + geom_hline(yintercept = 17.5) + geom_hline(yintercept = 21.5) + scale_fill_gradient2(low = "darkslategray1", mid = "yellow", high = "turquoise4") heatmap.p 



El mapa de calor muestra gráficamente cuántas observaciones se hacen para cada nivel de factor para los factores iniciales (las variables con las que comenzamos). El color azul oscuro corresponde a un número relativamente grande de observaciones dentro del grupo. Este mapa de calor también muestra que para el día de la semana (sol, sábado ...) y el tamaño de la canasta (grande, med, pequeño) el número de clientes en cada celda es casi el mismo, esto puede significar que estas categorías no son determinantes para el análisis, y Quizás no necesitan ser tomados en cuenta.

Conclusión


En este artículo, calculamos la matriz de disimilitud, probamos los métodos de aglomeración y división del agrupamiento jerárquico y nos familiarizamos con los métodos de codo y silueta para evaluar la calidad de los grupos.

La agrupación jerárquica divisional y aglomerativa es un buen comienzo para estudiar el tema, pero no se detenga allí si realmente desea dominar el análisis de agrupación. Existen muchos otros métodos y técnicas. La principal diferencia de agrupar datos numéricos es el cálculo de la matriz de disimilitud. Al evaluar la calidad de la agrupación, no todos los métodos estándar darán resultados confiables y significativos; es muy probable que el método de la silueta no sea adecuado.

Y finalmente, dado que ya ha pasado un tiempo desde que hice este ejemplo, ahora veo una serie de deficiencias en mi enfoque y me complacerá recibir cualquier comentario. Uno de los problemas importantes de mi análisis no estaba relacionado con la agrupación como tal: mi conjunto de datos estaba desequilibrado de muchas maneras, y este momento no se tuvo en cuenta. Esto tuvo un efecto notable en la agrupación: el 70% de los clientes pertenecían a un nivel del factor de "ciudadanía", y este grupo dominó la mayoría de los grupos obtenidos, por lo que fue difícil calcular las diferencias dentro de otros niveles del factor. La próxima vez intentaré equilibrar el conjunto de datos y comparar los resultados de la agrupación. Pero más sobre eso en otra publicación.

Finalmente, si desea clonar mi código, aquí está el enlace a github: https://github.com/khunreus/cluster-categorical
¡Espero que hayas disfrutado este artículo!

Fuentes que me ayudaron:


Guía de agrupación jerárquica (preparación de datos, agrupación, visualización): este blog será interesante para quienes estén interesados ​​en el análisis empresarial en el entorno R: http://uc-r.imtqy.com/hc_clustering y https: // uc-r. imtqy.com/kmeans_clustering

Agrupación: http://www.sthda.com/english/articles/29-cluster-validation-essentials/97-cluster-validation-statistics-must-know-methods/

( k-): https://eight2late.wordpress.com/2015/07/22/a-gentle-introduction-to-cluster-analysis-using-r/

denextend, : https://cran.r-project.org/web/packages/dendextend/vignettes/introduction.html#the-set-function

, : https://www.r-statistics.com/2010/06/clustergram-visualization-and-diagnostics-for-cluster-analysis-r-code/

: https://jcoliver.imtqy.com/learn-r/008-ggplot-dendrograms-and-heatmaps.html

, https://www.ncbi.nlm.nih.gov/pmc/articles/PMC5025633/ ( GitHub: https://github.com/khunreus/EnsCat ).

Source: https://habr.com/ru/post/461741/


All Articles