Analyse de regroupements

Exercice 6.1

Les données fictives regroupements1 sont inspirées de Hsu & Lee (2002). Ces données contiennent des échelles pour certains éléments d’un questionnaire. Ce dernier a été élaboré afin d’évaluer l’importance de 55 caractéristiques des opérateurs de voyages organisés en autobus et des voyages eux-mêmes à l’aide d’une échelle de Likert à cinq points, allant de extrêmement important (5) à pas du tout important (1).

Les variables représentent les activités sociales, les politiques de l’opérateur et références, la flexibilité des horaires, la santé et sécurité, le matériel publicitaire et la réputation.

  1. Doit-on standardiser les données avant d’effectuer l’analyse?
Code
data(regroupements1, package = "hecmulti")
str(regroupements1)
Classes 'tbl_df', 'tbl' and 'data.frame':   150 obs. of  6 variables:
 $ x1: num  3.9 2.8 3.6 2.8 2.5 2.5 2.5 2.2 3.1 3.1 ...
 $ x2: num  4.1 4.6 5 5 3.9 3.3 4.3 5 3.6 5 ...
 $ x3: num  3.6 5 4.3 4.4 3.6 3.6 2.6 2.5 4.5 3.7 ...
 $ x4: num  4.4 4.6 3.6 3.7 4.2 2.6 3.9 3.9 3.9 2.6 ...
 $ x5: num  4 4.5 3.5 3.2 3.2 3.9 4.2 5 4.9 5 ...
 $ x6: num  5 4 4.4 3.5 4 4.5 5 4.9 5 3.9 ...
Code
summary(regroupements1)
       x1              x2              x3              x4       
 Min.   :1.000   Min.   :1.000   Min.   :1.000   Min.   :1.000  
 1st Qu.:2.100   1st Qu.:2.400   1st Qu.:1.925   1st Qu.:2.525  
 Median :2.800   Median :3.200   Median :3.000   Median :3.150  
 Mean   :2.933   Mean   :3.218   Mean   :2.957   Mean   :3.197  
 3rd Qu.:3.775   3rd Qu.:4.100   3rd Qu.:3.900   3rd Qu.:3.900  
 Max.   :5.000   Max.   :5.000   Max.   :5.000   Max.   :5.000  
       x5              x6       
 Min.   :1.000   Min.   :1.000  
 1st Qu.:2.400   1st Qu.:1.925  
 Median :3.450   Median :3.400  
 Mean   :3.321   Mean   :3.170  
 3rd Qu.:4.200   3rd Qu.:4.400  
 Max.   :5.000   Max.   :5.000  

Les données sont toutes sur la même échelle de Likert donc la standardisation n’est pas nécessaire apriori.

  1. Faites une analyse en composantes principales et projetez les observations sur un nuage de points avec les deux premières composantes principales.
Code
acp <- princomp(regroupements1, cor = FALSE)
hecmulti::eboulis(acp)

Code
# 2 à 3 composantes principales suffisantes
library(ggplot2)
ggplot(data = data.frame(acp$scores[,1:2]),
       mapping = aes(x = Comp.1,
                     y = Comp.2)) +
  geom_point() +
  labs(x = "composante principale 1",
       y = "composante principale 2") +
  theme_classic()

Code
# Chargements
acp$loadings

Loadings:
   Comp.1 Comp.2 Comp.3 Comp.4 Comp.5 Comp.6
x1  0.247  0.918  0.202                0.229
x2  0.410        -0.498  0.758              
x3  0.459         0.187         0.616 -0.609
x4  0.343  0.140 -0.505 -0.568 -0.440 -0.304
x5  0.410 -0.213  0.648  0.119 -0.588       
x6  0.525 -0.300        -0.292  0.266  0.690

               Comp.1 Comp.2 Comp.3 Comp.4 Comp.5 Comp.6
SS loadings     1.000  1.000  1.000  1.000  1.000  1.000
Proportion Var  0.167  0.167  0.167  0.167  0.167  0.167
Cumulative Var  0.167  0.333  0.500  0.667  0.833  1.000

On voit plus ou moins clairement trois regroupements. Les chargements révèlent que la première composante principale est grosso modo une moyenne globale des scores, mais que le score pour les activités sociales ressort davantage dans la deuxième composante, comparativement au matériel publicitaire et à la réputation.

  1. Utilisez l’algorithme des \(K\)-moyennes en faisant varier le nombre de groupes de 1 à 10. Utilisez une dizaine d’initialisations aléatoires.
    1. Sélectionnez un nombre de regroupement adéquat
    2. Retournez le nombre d’observation par groupe pour la valeur de \(K\) choisie.
    3. Rapportez les statistiques descriptives (moyennes, etc.) de chaque segment
    4. Interprétez les profils obtenus.
Code
set.seed(60602)
Kmax <- 10L # nombre maximum de groupes
kmoy <- list() # liste pour stocker les résultats des segmentations
for(k in seq_len(Kmax)){
 kmoy[[k]] <- kmeans(x = regroupements1, 
                     centers = k,  # nombre de regroupements initiaux
                     nstart = 10L) # nombre d'initialisation aléatoires
}

On peut extraire le critère de la fonction objective (SCD): l’homogénéité est calculée comme la somme des carrés des distances intra-groupes. Cela nous permet d’utiliser les diagnostics pour la sélection avec le \(R^2\) en cherchant le coude à partir duquel aucun changement n’est visible.

Code
scd_intra <- sapply(kmoy, function(x){x$tot.withinss})
# Diagnostics pour sélection - clairement trois groupes
hecmulti::homogeneite(scd = scd_intra)

Les graphiques de \(R^2\) et le \(R^2\) semi-partiel indiquent tous deux clairement que trois regroupements est le choix le plus logique. On peut étiqueter les observations et visualiser les regroupements obtenus en projetant sur les deux premières composantes principales. Les groupes sont bien démarqués et il semble que l’initialisation aléatoire ait été adéquate.

Code
# Extraire les identifiants des regroupements
etiquettes_kmoy3 <- kmoy[[3]]$cluster
# Faire un graphique avec les composantes principales
ggplot(data = data.frame(acp$scores[,1:2]),
       mapping = aes(x = Comp.1,
                     y = Comp.2, 
                     col = factor(etiquettes_kmoy3))) +
  geom_point() +
  labs(x = "composante principale 1",
       y = "composante principale 2",
       col = "regroupement") +
  theme_classic() +
  theme(legend.position = "bottom")
Figure 1: Représentation des regroupements des \(K\)-moyennes projetés sur les deux premières composantes principales des données.

Le nombre peut être extrait de la liste avec $size. On a 45 observations dans le groupe 1, 28 dans le groupe 2 et 77 dans le groupe 3. Au vu de la taille de la base de donnée, ces nombres sont relativement équilibrés.

On peut maintenant extraire les moyennes des trois regroupements. Le Tableau 1 montre le résultat arrondi à un chiffre après la virgule.

Code
knitr::kable(kmoy[[3]]$centers, digits = 1)
Tableau 1: Moyennes des trois regroupements obtenus avec les \(K\)-moyennes.
x1 x2 x3 x4 x5 x6
1.7 2.0 1.6 2.1 2.1 1.6
4.5 2.9 2.6 3.1 3.0 2.6
3.0 4.0 3.9 3.9 4.2 4.3

Le groupe 1 est le groupe où les sujets ont les valeurs, en moyenne, les plus faibles pour les six variables. Le groupe 2 est celui où les sujets ont les valeurs, en moyenne, les plus élevées pour les 6 variables sauf pour la variable X1 (activité sociale). Le groupe 3 est celui où les sujets ont, en moyenne, la valeur la plus élevée de la variable X1 et des valeurs moyennes inférieures au groupe 3 mais supérieures au groupe 2 pour les cinq autres variables.

Dans l’article, les auteurs ont baptisé les sujets du groupe 1, les « indépendants », ceux du groupe 2, les « dépendants » et ceux du groupe 3, les « sociables ».

  1. Faites une segmentation avec d’autres méthodes. Est-ce que la segmentation est plus satisfaisante? Justifiez votre raisonnement.

La segmentation obtenue est très satisfaisante et interprétable, mais nous essayerons d’autres méthodes pour démontrer leur utilisation.

Les méthodes basées sur la densité ne fonctionnent pas bien parce que les scores sont relativement rapprochés.

Code
# Regarder les plus proches voisins
dbscan::kNNdistplot(regroupements1, 
                    minPts = 5)
abline(h = 1.7, col = "red")

Code
dbscan_regroup <- dbscan::dbscan(
  x = regroupements1,
  minPts = 5,
  eps = 1.7)
dbscan_regroup
DBSCAN clustering for 150 objects.
Parameters: eps = 1.7, minPts = 5
Using euclidean distances and borderpoints = TRUE
The clustering contains 2 cluster(s) and 6 noise points.

 0  1  2 
 6 99 45 

Available fields: cluster, eps, minPts, dist, borderPoints

La solution retournée fusionne deux regroupements et plusieurs valeurs sont traitées comme des points isolés.

Code
ggplot(data = data.frame(acp$scores[,1:2]),
       mapping = aes(x = Comp.1,
                     y = Comp.2, 
                     col = factor(dbscan_regroup$cluster + 1))) +
  geom_point() +
  labs(x = "composante principale 1",
       y = "composante principale 2",
       col = "regroupement") +
  theme_classic() +
  theme(legend.position = "bottom")
Figure 2: Représentation des regroupements DBSCAN projetés sur les deux premières composantes principales des données.

Avec une petite taille d’échantillons, les méthodes de regroupements hiérarchiques sont compétitives et on a le luxe de pouvoir en essayer plusieurs.

Code
dist_euc <- dist(regroupements1, method = "euclidean")
rh_simple <- fastcluster::hclust(dist_euc, method = "single")
rh_complet <- fastcluster::hclust(dist_euc, method = "complete")
rh_ward <- fastcluster::hclust(dist_euc, method = "ward.D2")
rh_bary <- fastcluster::hclust(dist_euc, method = "centroid")
rh_moy <- fastcluster::hclust(dist_euc, method = "average")
rh_med <- fastcluster::hclust(dist_euc, method = "median")
rh_genie <- genieclust::gclust(d = regroupements1)
rh_energie <- energy::energy.hclust(d = dist_euc)

Pour visualiser le dendrogramme, la méthode plot permet de tracer l’arborescence. Il suffit ensuite d’encadrer avec rect.hclust() en spécifiant le nombre de groupes. Par exemple,

Code
plot(rh_ward, labels = FALSE)
rect.hclust(rh_ward, k = 3)

On peut tracer les dendrogrammes (Figure 3) pour essayer de voir combien de groupes sont raisonnables. Les plus proches voisins (liaison simple) et le barycentre retournent deux groupes, les autres critères trois et en l’apparence tous de taille similaire (à l’exclusion de la distance médiane qui donne une segmentation inacceptable avec plusieurs observations isolées).

Figure 3: Dendrogrammes des regroupements hiérarchiques selon les mesures de distance et de liaison entre groupes (plus proches voisins, voisins les plus éloignées, distances moyenne et médiane, distance entre barycentres, critère de Ward, distance d’énergie et GENIE).

Une fois qu’on a sélectionné le nombre de groupes, on utilise cutree pour élaguer l’arbre et obtenir la segmentation. La procédure retourne le vecteur avec les étiquettes.

Puisque les étiquettes peuvent être permutées, on peut calculer l’indice de Rand pour voir la concordance entre les regroupements en fournissant à la fonction flexclust::randIndex() les deux vecteurs d’étiquettes.

Les regroupements obtenus avec les \(K\)-moyennes coïncident à 95% pour la distance énergie, 93% pour la méthode de Ward et 93% pour GENIE. Le résultat est cohérent.

Code
moy_ward <- cbind(regroupements1, 
                  etiquette = cutree(rh_ward, k = 3)) |>
  dplyr::group_by(etiquette) |>
  dplyr::summarize_all(.funs = mean)
# Initialisation avec regroupements pour K-moyennes
# kmeans(x = regroupements1, centers = moy_ward[,-1])

Avec la méthode de Ward, les fonctions objectives sont similaires et il serait envisageable, seulement si la taille de l’échantillon est petite, d’utiliser la solution de Ward pour initialiser les \(K\)-moyennes. Ces dernières permettent une réassignation et ne peuvent qu’améliorer le critère objectif.

On pourrait aussi envisager d’autres mesures de dissemblance, ici avec la distance de Manhattan, ce qui donne les \(K\)-médianes.

Code
library(flexclust, quietly = TRUE)
kmedianes <- kcca(x = regroupements1,
                  family = kccaFamily("kmedians"),
                  control = list(initcent = "kmeanspp"),
                  k = 3)
kmedianes@clusinfo # décompte par groupe
  size  av_dist max_dist separation
1   31 3.416129     5.60        4.5
2   73 3.293151     5.10        4.8
3   46 3.223913     5.95        4.4
Code
kmedianes@centers  # médianes des groupes
      x1  x2  x3  x4   x5  x6
[1,] 4.6 3.0 2.9 3.1 3.20 2.6
[2,] 3.1 4.1 3.9 3.9 4.20 4.4
[3,] 1.7 2.0 1.5 2.2 1.95 1.4

Encore une fois, les regroupements sont à 91% les mêmes que pour les \(K\)-moyennes et l’interprétation est la même.

Code
ggplot(data = data.frame(acp$scores[,1:2]),
       mapping = aes(x = Comp.1,
                     y = Comp.2, 
                     col = factor(kmedianes@cluster))) +
  geom_point() +
  labs(x = "composante principale 1",
       y = "composante principale 2",
       col = "regroupement") +
  theme_classic() +
  theme(legend.position = "bottom")
Figure 4: Représentation des regroupements des \(K\)-médianes projetés sur les deux premières composantes principales des données.

On pourrait aussi utiliser la méthode des \(K\)-médoïdes.

Code
library(cluster)
kmedoides <- pam(regroupements1, k = 3)
# plot(kmedoides)
silhouette <- silhouette(x = kmedoides, dist = dist_euc)
plot(silhouette)

Code
kmedoides$medoids
     x1  x2  x3  x4  x5  x6
36  2.9 3.7 3.9 3.6 4.0 4.5
87  4.2 2.9 3.0 3.1 2.5 2.4
142 1.7 1.9 1.6 2.3 1.3 1.1

L’algorithme de partition autour des médoïdes (PAM) retourne un résultat similaire aux \(K\)-moyennes si on fixe le nombre de rgroupements à trois. La méthode plot par défaut retourne une projection sur les deux composantes principales. Les silhouettes montrent des profils globalement homogènes, sauf pour deux observations du groupe 2.

La plupart des méthodes employées retournent, peu ou prou, le même résultat que les \(K\)-moyennes. Ces données simulées sont un cas d’école; en pratique, il faudra davantage se fier à son jugement pratique pour voir si certains regroupements sont fortement débalancés avec plusieurs données isolées, difficilement interprétables ou voire trop similaires.

Si la solutions des \(K\)-moyennes et cie est satisfaisante, l’assignation probabiliste plutôt que rigide pourrait être souhaitable. Les mélanges de modèles Gaussiens offrent cette option. Ici, on ajuste tous les modèles avec \(K=1, \ldots, 10\) regroupements pour toutes les structures de covariance et on choisit le modèle avec le plus petit BIC.

Code
library(mclust)
Package 'mclust' version 6.0.1
Type 'citation("mclust")' for citing this R package in publications.
Code
mmg <- Mclust(regroupements1, G = 1:10)
summary(mmg)
---------------------------------------------------- 
Gaussian finite mixture model fitted by EM algorithm 
---------------------------------------------------- 

Mclust EII (spherical, equal volume) model with 3 components: 

 log-likelihood   n df       BIC       ICL
      -1092.714 150 21 -2290.651 -2293.147

Clustering table:
 1  2  3 
77 28 45 
Code
# Critère BIC selon le nombre de groupes 
# et la structure de covariance
plot(mmg, what = "BIC")

On peut clairement voir que trois regroupements est le nombre idéal. Les modèles plus complexes à coefficients variables sont pénalisés davantage et difficilement estimables vu la taille du jeu de données.

On voit clairement les points en bordure de l’espace qui pourraient réalistement être assignés à l’un ou l’autre des regroupements.

Code
## Nuages de points 2x2
plot(mmg, what = "uncertainty")

Les points plus larges sont ceux qui sont plus incertains. Ce degré d’incertitude vaut un moins la probabilité d’appartenir à la classe assignée. On peut extraire de la sortie les probabilités de chaque classe en bonus.

Code
mmg$uncertainty # très faible ici, regroupements bien déterminés
# Probabilité de chaque observation par classe
mmg$z

Les regroupements sont exactement les mêmes que ceux obtenus avec les \(K\)-moyennes.

Code
# Indice de Rand - adéquation entre partitions
flexclust::randIndex(x = mmg$classification, #nos regroupements
                     y = kmoy[[3]]$cluster)
ARI 
  1 

Références

Hsu, C. H. C., & Lee, E.-J. (2002). Segmentation of senior motorcoach travelers. Journal of Travel Research, 40(4), 364–373. https://doi.org/10.1177/004728750204000403