Regroupement hiérarchique des données catégorielles dans R

La traduction a été préparée pour les étudiants du cours «Applied Analytics on R» .





C'était ma première tentative de regrouper des clients sur la base de données réelles, et cela m'a donné une expérience précieuse. Il existe de nombreux articles sur Internet sur le regroupement à l'aide de variables numériques, mais trouver des solutions pour les données catégorielles, qui est un peu plus difficile, n'était pas si simple. Les méthodes de regroupement des données catégorielles sont toujours en cours de développement, et dans un autre article, je vais en essayer un autre.

D'un autre côté, de nombreuses personnes pensent que le regroupement de données catégorielles peut ne pas produire de résultats significatifs - et cela est en partie vrai (voir l' excellente discussion sur CrossValidated ). À un moment donné, j'ai pensé: «Qu'est-ce que je fais? Ils peuvent simplement être divisés en cohortes. » Cependant, l'analyse de cohorte n'est également pas toujours recommandée, en particulier avec un nombre significatif de variables catégorielles avec un grand nombre de niveaux: vous pouvez facilement traiter 5-7 cohortes, mais si vous avez 22 variables et chacune a 5 niveaux (par exemple, une enquête client avec des estimations discrètes 1 , 2, 3, 4 et 5), et vous devez comprendre à quels groupes de clients caractéristiques vous avez affaire - vous obtiendrez des cohortes de 22x5. Personne ne veut s'embêter avec une telle tâche. Et ici, le clustering pourrait aider. Donc, dans cet article, je parlerai de ce que j'aimerais savoir moi-même dès que j'ai commencé à regrouper.

Le processus de clustering lui-même comprend trois étapes:

  1. Construire une matrice de dissimilarité est sans aucun doute la décision la plus importante dans le clustering. Toutes les étapes suivantes seront basées sur la matrice de dissimilarité que vous avez créée.
  2. Le choix de la méthode de clustering.
  3. Évaluation des grappes.

Ce billet sera une sorte d'introduction qui décrit les principes de base du clustering et sa mise en œuvre dans l'environnement R.

Matrice de dissimilarité


La base du clustering sera la matrice de dissimilarité qui, en termes mathématiques, décrit la différence (supprimée) entre les points de l'ensemble de données. Il vous permet de combiner davantage dans les groupes les points les plus proches les uns des autres, ou de séparer les plus éloignés les uns des autres - c'est l'idée principale du clustering.

À ce stade, les différences entre les types de données sont importantes, car la matrice de dissimilarité est basée sur les distances entre les points de données individuels. Il est facile d'imaginer les distances entre les points de données numériques (un exemple bien connu est les distances euclidiennes ), mais dans le cas de données catégorielles (facteurs en R), tout n'est pas si évident.

Afin de construire une matrice de dissimilarité dans ce cas, la distance dite de Gover doit être utilisée. Je ne m'attarderai pas sur la partie mathématique de ce concept, je fournirai simplement des liens: ici et . Pour cela, je préfère utiliser daisy() avec la metric = c("gower") du package de 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 matrice de dissimilarité est prête. Pour 200 observations, il est construit rapidement, mais peut nécessiter une très grande quantité de calcul si vous avez affaire à un grand ensemble de données.

Dans la pratique, il est très probable que vous deviez d'abord nettoyer l'ensemble de données, effectuer les transformations nécessaires des lignes en facteurs et suivre les valeurs manquantes. Dans mon cas, l'ensemble de données contenait également des lignes de valeurs manquantes qui étaient joliment regroupées à chaque fois, il semblait donc que c'était un trésor - jusqu'à ce que je regarde les valeurs (hélas!).

Algorithmes de clustering


Vous savez peut-être déjà que le clustering est k-means et hiérarchique . Dans cet article, je me concentre sur la deuxième méthode, car elle est plus flexible et permet différentes approches: vous pouvez choisir un algorithme de clustering agglomératif (de bas en haut) ou divisionnaire (de haut en bas).


Source: Guide de programmation UC Business Analytics R

Le clustering agglomératif commence par n clusters, où n est le nombre d'observations: on suppose que chacun d'eux est un cluster séparé. Ensuite, l'algorithme essaie de trouver et de regrouper les points de données les plus similaires entre eux - c'est ainsi que la formation des clusters commence.

Le regroupement divisionnaire est effectué de la manière opposée - il est initialement supposé que tous les n points de données que nous avons sont un grand cluster, puis les moins similaires sont divisés en groupes séparés.

Lorsque vous décidez laquelle de ces méthodes choisir, il est toujours judicieux d'essayer toutes les options.Cependant, en général, le clustering aggloméré est meilleur pour identifier les petits clusters et est utilisé par la plupart des programmes informatiques, et le clustering divisionnaire est plus approprié pour identifier les grands clusters .

Personnellement, avant de décider quelle méthode utiliser, je préfère regarder les dendrogrammes - une représentation graphique du clustering. Comme vous le verrez plus tard, certains dendrogrammes sont bien équilibrés, tandis que d'autres sont très chaotiques.

# L'entrée principale pour le code ci-dessous est la dissimilarité (matrice de distance)
 #             #            —         —    #------------  ------------# 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") 

Évaluation de la qualité du clustering


A ce stade, il est nécessaire de choisir entre différents algorithmes de clustering et un nombre différent de clusters. Vous pouvez utiliser différentes méthodes d'évaluation, sans oublier de vous laisser guider par le bon sens . J'ai souligné ces mots en gras et en italique, car la signification du choix est très importante - le nombre de grappes et la méthode de division des données en groupes doivent être pratiques d'un point de vue pratique. Le nombre de combinaisons de valeurs de variables catégorielles est fini (car elles sont discrètes), mais aucune ventilation basée sur elles ne sera significative. Vous pouvez également ne pas vouloir avoir très peu de clusters - dans ce cas, ils seront trop généralisés. En fin de compte, tout dépend de votre objectif et des tâches de l'analyse.

En général, lors de la création de clusters, vous souhaitez obtenir des groupes de points de données clairement définis, de sorte que la distance entre ces points au sein du cluster ( ou compacité ) soit minimale, et la distance entre les groupes ( séparabilité ) soit le maximum possible. Ceci est facile à comprendre intuitivement: la distance entre les points est une mesure de leur dissimilarité, obtenue à partir de la matrice de dissimilarité. Ainsi, l'évaluation de la qualité du clustering est basée sur l'évaluation de la compacité et de la séparabilité.

Ensuite, je vais démontrer deux approches et montrer que l'une d'elles peut donner des résultats dénués de sens.

  • Méthode du coude : commencez par celle-ci si le facteur le plus important pour votre analyse est la compacité des grappes, c'est-à-dire la similitude au sein des groupes.
  • Méthode d'évaluation des silhouettes : Le graphique de silhouette utilisé comme mesure de la cohérence des données montre à quel point chacun des points d'un cluster est proche des points des clusters voisins.

Dans la pratique, ces deux méthodes donnent souvent des résultats différents, ce qui peut entraîner une certaine confusion - la compacité maximale et la séparation la plus claire seront obtenues avec un nombre différent de clusters, de sorte que le bon sens et la compréhension de ce que vos données signifient réellement joueront un rôle important lors de la prise de décision finale.

Il existe également un certain nombre de mesures que vous pouvez analyser. Je vais les ajouter directement au code.

 #      ,        #      ,     ,   —   #     ,      ,         ,   ,     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 



Ainsi, l'indicateur average .within, qui représente la distance moyenne entre les observations au sein des grappes, diminue, de même que within.cluster.ss (la somme des carrés des distances entre les observations dans une grappe). La largeur moyenne de la silhouette (largeur moyenne) ne change pas sans ambiguïté, cependant, une relation inverse peut encore être remarquée.
Remarquez à quel point les tailles de cluster sont disproportionnées. Je ne me précipiterais pas pour travailler avec un nombre incomparable d'observations au sein des clusters. L'une des raisons est que l'ensemble de données peut être déséquilibré, et certains groupes d'observations l'emporteront sur tous les autres dans l'analyse - ce n'est pas bon et conduira très probablement à des erreurs.

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

stats.df.aggl



Remarquez à quel point le nombre d'observations par groupe est mieux équilibré par le regroupement hiérarchique aggloméré basé sur la méthode de communication complète.

 # ---------    ---------# #   «»       #    ,     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)) 



Nous avons donc créé un graphique du «coude». Il montre comment la somme des distances au carré entre les observations (nous l'utilisons comme mesure de la proximité des observations - plus elle est petite, plus les mesures à l'intérieur du cluster sont proches les unes des autres) varie pour un nombre différent de clusters. Idéalement, nous devrions voir un «coude de coude» distinct au point où un regroupement supplémentaire ne donne qu'une légère diminution de la somme des carrés (SS). Pour le graphique ci-dessous, je m'arrêterais à environ 7. Bien que dans ce cas l'un des clusters ne comprendra que deux observations. Voyons ce qui se passe lors du clustering agglomératif.

 #       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)) 



Le «coude» aggloméré est similaire à la division, mais le graphique semble plus lisse - les courbes ne sont pas si prononcées. Comme pour le clustering divisionnaire, je me concentrerais sur 7 clusters, cependant, lorsque je choisis entre ces deux méthodes, j'aime davantage les tailles de cluster obtenues par la méthode agglomérative - il vaut mieux qu'elles soient comparables.

 #  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)) 



Lorsque vous utilisez la méthode d'estimation de la silhouette, vous devez choisir la quantité qui donne le coefficient de silhouette maximal, car vous avez besoin de grappes suffisamment éloignées pour être considérées comme distinctes.

Le coefficient de silhouette peut aller de –1 à 1, 1 correspondant à une bonne cohérence au sein des grappes et –1 pas très bon.
Dans le cas du graphique ci-dessus, vous choisiriez 9 plutôt que 5 grappes.

A titre de comparaison: dans le cas «simple», le graphique de silhouette est similaire à celui ci-dessous. Pas tout à fait comme le nôtre, mais presque.


Source: Data Sailors

 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)) 



Le graphique de largeur de silhouette nous dit: plus vous divisez l'ensemble de données, plus les clusters deviennent clairs. Cependant, à la fin, vous atteindrez des points individuels et vous n'en aurez pas besoin. Cependant, c'est exactement ce que vous verrez si vous commencez à augmenter le nombre de clusters k . Par exemple, pour k=30 j'ai obtenu le graphique suivant:



Pour résumer: plus vous divisez l'ensemble de données, meilleurs sont les clusters, mais nous ne pouvons pas atteindre des points individuels (par exemple, dans le graphique ci-dessus, nous avons sélectionné 30 clusters et nous n'avons que 200 points de données).

Ainsi, le clustering aggloméré dans notre cas me semble beaucoup plus équilibré: les tailles de cluster sont plus ou moins comparables (il suffit de regarder un cluster de seulement deux observations lors de la division par la méthode divisionnaire!), Et je m'arrêterais à 7 clusters obtenus par cette méthode. Voyons à quoi ils ressemblent et de quoi ils sont faits.

L'ensemble de données se compose de 6 variables qui doivent être visualisées en 2D ou 3D, vous devez donc travailler dur! La nature des données catégorielles impose également certaines limites, de sorte que les solutions toutes faites peuvent ne pas fonctionner. Je dois: a) voir comment les observations sont divisées en grappes, b) comprendre comment les observations sont classées. Par conséquent, j'ai créé a) un dendrogramme en couleurs, b) une carte thermique du nombre d'observations par variable à l'intérieur de chaque groupe.

 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 



La carte thermique montre graphiquement combien d'observations sont faites pour chaque niveau de facteur pour les facteurs initiaux (les variables avec lesquelles nous avons commencé). La couleur bleu foncé correspond à un nombre relativement important d'observations au sein de l'amas. Cette carte de chaleur montre également que pour le jour de la semaine (soleil, sam ... lun) et la taille du panier (grand, moyen, petit), le nombre de clients dans chaque cellule est presque le même - cela peut signifier que ces catégories ne sont pas déterminantes pour l'analyse, et Il n'est peut-être pas nécessaire d'en tenir compte.

Conclusion


Dans cet article, nous avons calculé la matrice de dissimilarité, testé les méthodes d'agglomération et de division du clustering hiérarchique et nous sommes familiarisés avec les méthodes du coude et de la silhouette pour évaluer la qualité des clusters.

Le clustering hiérarchique divisionnaire et aggloméré est un bon début pour étudier le sujet, mais ne vous arrêtez pas là si vous voulez vraiment maîtriser l'analyse de cluster. Il existe de nombreuses autres méthodes et techniques. La principale différence avec le regroupement des données numériques est le calcul de la matrice de dissimilarité. Lors de l'évaluation de la qualité du regroupement, toutes les méthodes standard ne donneront pas des résultats fiables et significatifs - la méthode de la silhouette n'est probablement pas appropriée.

Et enfin, comme un certain temps s'est déjà écoulé depuis que j'ai fait cet exemple, je vois maintenant un certain nombre de lacunes dans mon approche et je serai heureux de tout commentaire. L'un des problèmes importants de mon analyse n'était pas lié au clustering en tant que tel - mon ensemble de données était déséquilibré à bien des égards, et ce moment restait inexpliqué. Cela a eu un effet notable sur le regroupement: 70% des clients appartenaient à un niveau du facteur «citoyenneté» et ce groupe dominait la plupart des grappes obtenues, il était donc difficile de calculer les différences entre les autres niveaux du facteur. La prochaine fois, j'essaierai d'équilibrer l'ensemble de données et de comparer les résultats du clustering. Mais plus à ce sujet dans un autre post.

Enfin, si vous souhaitez cloner mon code, voici le lien vers github: https://github.com/khunreus/cluster-categorical
J'espère que cet article vous a plu!

Sources qui m'ont aidé:


Guide de clustering hiérarchique (préparation des données, clustering, visualisation) - ce blog sera intéressant pour ceux qui sont intéressés par l'analyse commerciale dans l'environnement R: http://uc-r.imtqy.com/hc_clustering et https: // uc-r. imtqy.com/kmeans_clustering

Clustering: 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/fr461741/


All Articles