Environnement et packages¶
In [34]:
# activer l'environnement
using Pkg
Pkg.activate("env_julia_cah")
Activating project at `c:\Users\ricco\Desktop\demo\env_julia_cah`
In [35]:
# liste des packages installés
Pkg.status()
Status `C:\Users\ricco\Desktop\demo\env_julia_cah\Project.toml` [aaaa29a8] Clustering v0.15.8 [a93c6f00] DataFrames v1.8.2 [7073ff75] IJulia v1.34.4 [f0f68f2c] PlotlyJS v0.18.18 [2913bbd2] StatsBase v0.34.10 [f3b207a7] StatsPlots v0.15.8 [fdbf4ff8] XLSX v0.11.7
Importation et préparation des données¶
Importation¶
In [36]:
# packages
import DataFrames as DFR
import XLSX
# lecture des données
df = DFR.DataFrame(XLSX.readtable("./autos_cah.xlsx"))
# premières lignes
println(DFR.size(df))
(26, 8)
In [37]:
# premières lignes
DFR.first(df,10)
10×8 DataFrame
| Row | Modele | puissance | cylindree | vitesse | longueur | largeur | poids | hauteur |
|---|---|---|---|---|---|---|---|---|
| String | Int64 | Int64 | Int64 | Int64 | Int64 | Int64 | Int64 | |
| 1 | LAGUNA | 165 | 1998 | 218 | 458 | 178 | 1320 | 143 |
| 2 | BMW530 | 231 | 2979 | 250 | 485 | 185 | 1495 | 147 |
| 3 | TWINGO | 60 | 1149 | 151 | 344 | 163 | 840 | 143 |
| 4 | FIESTA | 68 | 1399 | 164 | 392 | 168 | 1138 | 144 |
| 5 | CLIO | 100 | 1461 | 185 | 382 | 164 | 980 | 142 |
| 6 | CITRONC4 | 138 | 1997 | 207 | 426 | 178 | 1381 | 146 |
| 7 | PASSAT | 150 | 1781 | 221 | 471 | 175 | 1360 | 147 |
| 8 | MUSA | 100 | 1910 | 179 | 399 | 170 | 1275 | 169 |
| 9 | MAZDARX8 | 231 | 1308 | 235 | 443 | 177 | 1390 | 134 |
| 10 | ALFA 156 | 250 | 3179 | 250 | 443 | 175 | 1410 | 141 |
In [38]:
# description
DFR.describe(df)
8×7 DataFrame
| Row | variable | mean | min | median | max | nmissing | eltype |
|---|---|---|---|---|---|---|---|
| Symbol | Union… | Any | Union… | Any | Int64 | DataType | |
| 1 | Modele | ALFA 156 | YARIS | 0 | String | ||
| 2 | puissance | 135.962 | 54 | 139.0 | 250 | 0 | Int64 |
| 3 | cylindree | 1885.69 | 998 | 1939.0 | 3222 | 0 | Int64 |
| 4 | vitesse | 198.077 | 150 | 200.0 | 250 | 0 | Int64 |
| 5 | longueur | 423.962 | 344 | 427.5 | 486 | 0 | Int64 |
| 6 | largeur | 174.423 | 159 | 176.0 | 194 | 0 | Int64 |
| 7 | poids | 1288.58 | 840 | 1350.0 | 1735 | 0 | Int64 |
| 8 | hauteur | 147.923 | 134 | 146.5 | 169 | 0 | Int64 |
In [39]:
# récupération des variables actives
X = df[:,2:end]
DFR.describe(X)
7×7 DataFrame
| Row | variable | mean | min | median | max | nmissing | eltype |
|---|---|---|---|---|---|---|---|
| Symbol | Float64 | Int64 | Float64 | Int64 | Int64 | DataType | |
| 1 | puissance | 135.962 | 54 | 139.0 | 250 | 0 | Int64 |
| 2 | cylindree | 1885.69 | 998 | 1939.0 | 3222 | 0 | Int64 |
| 3 | vitesse | 198.077 | 150 | 200.0 | 250 | 0 | Int64 |
| 4 | longueur | 423.962 | 344 | 427.5 | 486 | 0 | Int64 |
| 5 | largeur | 174.423 | 159 | 176.0 | 194 | 0 | Int64 |
| 6 | poids | 1288.58 | 840 | 1350.0 | 1735 | 0 | Int64 |
| 7 | hauteur | 147.923 | 134 | 146.5 | 169 | 0 | Int64 |
Standardisation¶
In [40]:
# dimensions
n, p = size(X)
println("Obsservation = $n, variables actives = $p")
Obsservation = 26, variables actives = 7
In [41]:
# moyennes par colonne
import Statistics
mean_actifs = Statistics.mean(Matrix(X),dims=1)
println(mean_actifs)
[135.96153846153845 1885.6923076923076 198.07692307692307 423.96153846153845 174.42307692307693 1288.576923076923 147.92307692307693]
In [42]:
# écarts-type par colonne -- avec 1/n (corrected = false)
etype_actifs = Statistics.std(Matrix(X),dims=1,corrected=false)
println(etype_actifs)
[59.37849958543708 599.542054034755 30.67938806545864 43.775195618972276 7.869967632242485 248.92185568303637 7.231996622505364]
In [43]:
# centrage-réduction
Z = (X .- mean_actifs) ./ etype_actifs
DFR.describe(Z)
7×7 DataFrame
| Row | variable | mean | min | median | max | nmissing | eltype |
|---|---|---|---|---|---|---|---|
| Symbol | Float64 | Float64 | Float64 | Float64 | Int64 | DataType | |
| 1 | puissance | 1.43048e-16 | -1.38032 | 0.0511711 | 1.92053 | 0 | Float64 |
| 2 | cylindree | 1.06752e-16 | -1.48062 | 0.088914 | 2.22888 | 0 | Float64 |
| 3 | vitesse | 3.54951e-16 | -1.56708 | 0.062683 | 1.69244 | 0 | Float64 |
| 4 | longueur | 2.09234e-16 | -1.82664 | 0.0808326 | 1.41721 | 0 | Float64 |
| 5 | largeur | -1.4134e-15 | -1.95974 | 0.200372 | 2.48755 | 0 | Float64 |
| 6 | poids | -9.07394e-17 | -1.80208 | 0.246756 | 1.79343 | 0 | Float64 |
| 7 | hauteur | -1.51161e-15 | -1.92521 | -0.196775 | 2.9144 | 0 | Float64 |
CAH avec Clustering.jl¶
Distances par paires d'individus¶
In [44]:
# calculer les distances entre paires d'individus (dims=1)
# avec instanciation de la distance euclidienne
import Clustering
D = Clustering.pairwise(Clustering.Euclidean(),Matrix(Z),dims=1)
typeof(D)
Matrix{Float64} (alias for Array{Float64, 2})
In [45]:
# dimensions => (n x n)
size(D)
(26, 26)
In [46]:
# premières valeurs
# matrice symatrique, diagonale = 0
D[begin:5,begin:5]
5×5 Matrix{Float64}:
0.0 2.64054 4.90251 3.34759 3.34979
2.64054 0.0 7.31043 5.80921 5.76036
4.90251 7.31043 0.0 1.85199 1.74869
3.34759 5.80921 1.85199 0.0 1.24882
3.34979 5.76036 1.74869 1.24882 0.0
Implémentation de la CAH sur la matrice des distances¶
In [47]:
# lancement de la CAH
cah = Clustering.hclust(D,linkage=:ward)
dump(cah)
Clustering.Hclust{Float64}
merges: Array{Int64}((25, 2)) [-14 -17; -4 -24; … ; 15 17; 23 24]
heights: Array{Float64}((25,)) [0.5279032851794709, 0.6446363808450062, 0.7165290115805375, 0.8705320823455865, 1.0487291832063506, 1.2004566640679157, 1.2063417746292215, 1.238751656644204, 1.252065630244888, 1.3471553752332828 … 2.289173300170597, 2.717688597087221, 2.761067199325508, 2.8509850018442875, 3.2684784794457795, 3.8412635963250557, 4.012660514062123, 6.494212034049581, 6.629315078154999, 13.57028520152864]
order: Array{Int64}((26,)) [15, 22, 9, 18, 16, 7, 1, 19, 6, 25 … 8, 20, 26, 5, 4, 24, 12, 3, 14, 17]
linkage: Symbol ward
Dendrogramme - Visualisation, inspection¶
In [48]:
# affichage du dendrogramme
import StatsPlots as SPS
SPS.plot(cah)
In [49]:
# merges
# attention : indices négatifs = points existants initialement
# indices positifs : points issus des fusions
cah.merges
25×2 Matrix{Int64}:
-14 -17
-4 -24
-1 -19
-7 3
-6 -25
-2 -21
-3 1
-5 2
4 5
-15 -22
⋮
8 13
-11 -23
-18 12
-9 19
10 20
16 18
21 22
15 17
23 24
In [50]:
# avec la hauteur des niveaux d'agrégation
# => distance (ou proportionnel à) entre les groupes généralement
hcat(cah.merges,cah.heights)
25×3 Matrix{Float64}:
-14.0 -17.0 0.527903
-4.0 -24.0 0.644636
-1.0 -19.0 0.716529
-7.0 3.0 0.870532
-6.0 -25.0 1.04873
-2.0 -21.0 1.20046
-3.0 1.0 1.20634
-5.0 2.0 1.23875
4.0 5.0 1.25207
-15.0 -22.0 1.34716
⋮
8.0 13.0 2.71769
-11.0 -23.0 2.76107
-18.0 12.0 2.85099
-9.0 19.0 3.26848
10.0 20.0 3.84126
16.0 18.0 4.01266
21.0 22.0 6.49421
15.0 17.0 6.62932
23.0 24.0 13.5703
In [51]:
# indiquer les labels pour identifier les individus
# quand ils sont étiquetés, et petits effectifs seulement sinon illisibles
# /!\ attention /!\, il faut bien respecter l'ordre induit par la CAH
# indexer donc convenablement le vecteur des modèles
SPS.plot(cah,
xrotation=90, xticks=(1:n,df.Modele[cah.order]),
bottom_margin=10SPS.mm) # plus de marge en abscisse pour les étiquettes
Découpage en classes¶
In [52]:
# découpage en 4 classes
# k => nb de clusters, h => hauteur
nb_groupes = 4
groupes = Clustering.cutree(cah,k=nb_groupes)
print(groupes)
[1, 2, 3, 3, 3, 1, 1, 4, 1, 2, 2, 3, 2, 3, 1, 1, 3, 1, 1, 4, 2, 1, 2, 3, 1, 4]
In [53]:
# avec le nom des véhicules (modèles)
DFR.DataFrame(modele=df.Modele,cluster=groupes)
26×2 DataFrame
| Row | modele | cluster |
|---|---|---|
| String | Int64 | |
| 1 | LAGUNA | 1 |
| 2 | BMW530 | 2 |
| 3 | TWINGO | 3 |
| 4 | FIESTA | 3 |
| 5 | CLIO | 3 |
| 6 | CITRONC4 | 1 |
| 7 | PASSAT | 1 |
| 8 | MUSA | 4 |
| 9 | MAZDARX8 | 1 |
| 10 | ALFA 156 | 2 |
| 11 | PTCRUISER | 2 |
| 12 | PANDA | 3 |
| 13 | CITRONC5 | 2 |
| 14 | YARIS | 3 |
| 15 | GOLF | 1 |
| 16 | AVENSIS | 1 |
| 17 | CITRONC2 | 3 |
| 18 | MONDEO | 1 |
| 19 | VECTRA | 1 |
| 20 | MODUS | 4 |
| 21 | MERC_E | 2 |
| 22 | AUDIA3 | 1 |
| 23 | VELSATIS | 2 |
| 24 | CORSA | 3 |
| 25 | MEGANECC | 1 |
| 26 | MERC_A | 4 |
In [54]:
# trier par n° de cluster
DFR.sort(DFR.DataFrame(modele=df.Modele,cluster=groupes),:cluster)
26×2 DataFrame
| Row | modele | cluster |
|---|---|---|
| String | Int64 | |
| 1 | LAGUNA | 1 |
| 2 | CITRONC4 | 1 |
| 3 | PASSAT | 1 |
| 4 | MAZDARX8 | 1 |
| 5 | GOLF | 1 |
| 6 | AVENSIS | 1 |
| 7 | MONDEO | 1 |
| 8 | VECTRA | 1 |
| 9 | AUDIA3 | 1 |
| 10 | MEGANECC | 1 |
| 11 | BMW530 | 2 |
| 12 | ALFA 156 | 2 |
| 13 | PTCRUISER | 2 |
| 14 | CITRONC5 | 2 |
| 15 | MERC_E | 2 |
| 16 | VELSATIS | 2 |
| 17 | TWINGO | 3 |
| 18 | FIESTA | 3 |
| 19 | CLIO | 3 |
| 20 | PANDA | 3 |
| 21 | YARIS | 3 |
| 22 | CITRONC2 | 3 |
| 23 | CORSA | 3 |
| 24 | MUSA | 4 |
| 25 | MODUS | 4 |
| 26 | MERC_A | 4 |
In [55]:
# effectifs par cluster
import StatsBase
StatsBase.countmap(groupes)
Dict{Int64, Int64} with 4 entries:
4 => 3
2 => 6
3 => 7
1 => 10
Visualisation des groupes dans le dendrogramme¶
In [56]:
# couleurs associés à chaque groupe
couleurs = [:red,:green,:blue,:orange]
4-element Vector{Symbol}:
:red
:green
:blue
:orange
In [57]:
# couleur associé à chaque véhicule
couleur_id = couleurs[groupes]
26-element Vector{Symbol}:
:red
:green
:blue
:blue
:blue
:red
:red
:orange
:red
:green
⋮
:red
:red
:orange
:green
:red
:green
:blue
:red
:orange
In [58]:
# dendrogramme -- rajouter de la marge en bas
plot_cah = SPS.plot(cah,xticks=(1:n,fill("",n)),bottom_margin=20SPS.mm)
# pour chaque individu
for i in 1:n
# son identifiant dans le dendrogramme
id = cah.order[i]
# ajouter en annotation le modèle et sa couleur
SPS.annotate!(plot_cah,i,-1.4,
SPS.text(df.Modele[id],couleur_id[id],8,"DejaVu Sans",rotation=90))
end
# afficher
display(plot_cah)
Interprétation (cf. K-Means)¶
Après, on retrouve les mêmes traitements que pour les K-Means (importance des variables, caractérisation des groupes, etc.)
Pour le "fun" -- Graphique radar avec "PlotlyJS.jl"¶
In [59]:
# par exemple, pour un graphique radar
# calculer les moyennes conditionnelles
# nouveau dataframe
dfZ = deepcopy(Z)
dfZ.cluster = groupes
#moyennes conditionnelles
moy_cond = DFR.combine(DFR.groupby(dfZ,:cluster), names(Z) .=> Statistics.mean)
moy_cond
4×8 DataFrame
| Row | cluster | puissance_mean | cylindree_mean | vitesse_mean | longueur_mean | largeur_mean | poids_mean | hauteur_mean |
|---|---|---|---|---|---|---|---|---|
| Int64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | |
| 1 | 1 | 0.12864 | -0.0513597 | 0.326704 | 0.533144 | 0.568862 | 0.244346 | -0.542461 |
| 2 | 2 | 1.26934 | 1.43967 | 1.00251 | 0.975555 | 0.666278 | 1.22364 | 0.148911 |
| 3 | 3 | -1.13974 | -1.12296 | -1.20387 | -1.24249 | -1.26996 | -1.35156 | -0.226405 |
| 4 | 4 | -0.308106 | -0.0878876 | -0.284999 | -0.82912 | -0.265534 | -0.108107 | 2.03866 |
In [60]:
# importation de la librairie
import PlotlyJS as PJS
# noms des variables
vars = names(Z)
#dupliquer la première pour "fermer" le polygone
vars = vcat(vars,vars[1])
# layout du radarplot
layout = PJS.Layout(
polar = PJS.attr(
radialaxis = PJS.attr(
visible = true,
range = [-2.1, 2.1]
)
)
)
# liste des tracés à dessiner dans le radarplot
ligne_s = PJS.GenericTrace[]
# itérer sur les clusters
for i in 1:nb_groupes
# valeurs
row = collect(moy_cond[i,2:end])
row = vcat(row,row[1]) # pour "fermer" le polygone
# définir la ligne
# en respectant les couleurs utilisées dans le dendrogramme
ligne = PJS.scatterpolar(r=row,theta=vars,
fill="toself",name="Cluster_$i",
line = PJS.attr(color=couleurs[i]),#cf. dendrogramme
opacity=0.5)
# ajouter la ligne
push!(ligne_s,ligne)
end
# dessiner l'ensemble
PJS.Plot(ligne_s, layout)
Clustering de variables¶
Corrélations entre variables¶
In [61]:
# corrélations entre les variables
R = Statistics.cor(Matrix(X),dims=1)
size(R)
(7, 7)
In [62]:
# valeurs
R
7×7 Matrix{Float64}:
1.0 0.788353 0.93362 0.734486 0.605386 0.802675 -0.216307
0.788353 1.0 0.789052 0.731398 0.639516 0.817845 0.0413844
0.93362 0.789052 1.0 0.819983 0.72132 0.798444 -0.284439
0.734486 0.731398 0.819983 1.0 0.86125 0.881808 -0.191113
0.605386 0.639516 0.72132 0.86125 1.0 0.791583 -0.116335
0.802675 0.817845 0.798444 0.881808 0.791583 1.0 0.0632868
-0.216307 0.0413844 -0.284439 -0.191113 -0.116335 0.0632868 1.0
In [63]:
# ou sous forme de Heatmap
# défini sur (-1,+1), avec valeur centrale 0
SPS.heatmap(R,
xticks = (1:p,names(X)),
yticks = (1:p,names(X)),
c = :RdBu,
clims = (-1.0, 1.0),
yflip=true) #pour ne pas inverser les lignes
Distances entre variables¶
In [64]:
# transformer en matrice de distances
# en ne tenant compte de l'intensité de la corrélation (R^2)
DR = sqrt.(1 .- R.^2)
DR
7×7 Matrix{Float64}:
0.0 0.615223 0.358264 0.678624 0.795932 0.596417 0.976325
0.615223 0.0 0.614327 0.68195 0.768778 0.575439 0.999143
0.358264 0.614327 0.0 0.572388 0.692602 0.602069 0.958694
0.678624 0.68195 0.572388 0.0 0.508182 0.471608 0.981568
0.795932 0.768778 0.692602 0.508182 0.0 0.611061 0.99321
0.596417 0.575439 0.602069 0.471608 0.611061 0.0 0.997995
0.976325 0.999143 0.958694 0.981568 0.99321 0.997995 0.0
CAH sur les variables¶
In [65]:
# lancement de la CAH sur les variables
cah_var = Clustering.hclust(DR,linkage=:ward)
dump(cah_var)
Clustering.Hclust{Float64}
merges: Array{Int64}((6, 2)) [-1 -3; -4 -6; … ; 3 4; -7 5]
heights: Array{Float64}((6,)) [0.3582643530889661, 0.47160808690853384, 0.5890317420515444, 0.6790780326699423, 0.8695710419434539, 1.178257342607103]
order: Array{Int64}((7,)) [7, 5, 4, 6, 2, 1, 3]
linkage: Symbol ward
Une autre manière de voir la structuration de l'information.
In [66]:
# dendrogramme
SPS.plot(cah_var,xrotation=90,xticks=(1:p,names(X)[cah_var.order]))