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
RowModelepuissancecylindreevitesselongueurlargeurpoidshauteur
StringInt64Int64Int64Int64Int64Int64Int64
1LAGUNA 16519982184581781320143
2BMW530 23129792504851851495147
3TWINGO 601149151344163840143
4FIESTA 6813991643921681138144
5CLIO 1001461185382164980142
6CITRONC4 13819972074261781381146
7PASSAT 15017812214711751360147
8MUSA 10019101793991701275169
9MAZDARX8 23113082354431771390134
10ALFA 156 25031792504431751410141
In [38]:
# description
DFR.describe(df)
8×7 DataFrame
Rowvariablemeanminmedianmaxnmissingeltype
SymbolUnion…AnyUnion…AnyInt64DataType
1ModeleALFA 156 YARIS 0String
2puissance135.96254139.02500Int64
3cylindree1885.699981939.032220Int64
4vitesse198.077150200.02500Int64
5longueur423.962344427.54860Int64
6largeur174.423159176.01940Int64
7poids1288.588401350.017350Int64
8hauteur147.923134146.51690Int64
In [39]:
# récupération des variables actives
X = df[:,2:end]
DFR.describe(X)
7×7 DataFrame
Rowvariablemeanminmedianmaxnmissingeltype
SymbolFloat64Int64Float64Int64Int64DataType
1puissance135.96254139.02500Int64
2cylindree1885.699981939.032220Int64
3vitesse198.077150200.02500Int64
4longueur423.962344427.54860Int64
5largeur174.423159176.01940Int64
6poids1288.588401350.017350Int64
7hauteur147.923134146.51690Int64

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
Rowvariablemeanminmedianmaxnmissingeltype
SymbolFloat64Float64Float64Float64Int64DataType
1puissance1.43048e-16-1.380320.05117111.920530Float64
2cylindree1.06752e-16-1.480620.0889142.228880Float64
3vitesse3.54951e-16-1.567080.0626831.692440Float64
4longueur2.09234e-16-1.826640.08083261.417210Float64
5largeur-1.4134e-15-1.959740.2003722.487550Float64
6poids-9.07394e-17-1.802080.2467561.793430Float64
7hauteur-1.51161e-15-1.92521-0.1967752.91440Float64

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)
No description has been provided for this image
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
No description has been provided for this image

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
Rowmodelecluster
StringInt64
1LAGUNA 1
2BMW530 2
3TWINGO 3
4FIESTA 3
5CLIO 3
6CITRONC4 1
7PASSAT 1
8MUSA 4
9MAZDARX8 1
10ALFA 156 2
11PTCRUISER 2
12PANDA 3
13CITRONC5 2
14YARIS 3
15GOLF 1
16AVENSIS 1
17CITRONC2 3
18MONDEO 1
19VECTRA 1
20MODUS 4
21MERC_E 2
22AUDIA3 1
23VELSATIS 2
24CORSA 3
25MEGANECC 1
26MERC_A 4
In [54]:
# trier par n° de cluster
DFR.sort(DFR.DataFrame(modele=df.Modele,cluster=groupes),:cluster)
26×2 DataFrame
Rowmodelecluster
StringInt64
1LAGUNA 1
2CITRONC4 1
3PASSAT 1
4MAZDARX8 1
5GOLF 1
6AVENSIS 1
7MONDEO 1
8VECTRA 1
9AUDIA3 1
10MEGANECC 1
11BMW530 2
12ALFA 156 2
13PTCRUISER 2
14CITRONC5 2
15MERC_E 2
16VELSATIS 2
17TWINGO 3
18FIESTA 3
19CLIO 3
20PANDA 3
21YARIS 3
22CITRONC2 3
23CORSA 3
24MUSA 4
25MODUS 4
26MERC_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)
No description has been provided for this image

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
Rowclusterpuissance_meancylindree_meanvitesse_meanlongueur_meanlargeur_meanpoids_meanhauteur_mean
Int64Float64Float64Float64Float64Float64Float64Float64
110.12864-0.05135970.3267040.5331440.5688620.244346-0.542461
221.269341.439671.002510.9755550.6662781.223640.148911
33-1.13974-1.12296-1.20387-1.24249-1.26996-1.35156-0.226405
44-0.308106-0.0878876-0.284999-0.82912-0.265534-0.1081072.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
No description has been provided for this image

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]))
No description has been provided for this image