TD3: Classification hiérarchique ascendante
Table des matières
Figure 1 : A woman datascientist performing machine learning to bet on soccer teams, manga style (générée par DALL-E)
Figure 2 : A woman datascientist performing machine learning to bet on soccer teams, manga style (générée par MidJourney)
Introduction
Dans ce dernier TD, nous mettrons en pratique la méthode de classification ascendante hiérarchique (CAH) sur différents jeux de données afin d'en évaluer les propriétés.
Ce TD aura également pour objectif de comparer les approches de classification non supervisées vues en cours dans le cadre d'applications pratiques.
Enfin, une activité de programmation de distances entre classes est proposée afin de monter en
compétence sur le langage Python
.
Modules Python
utilisés dans ce TD
Dans ce TD, nous utiliserons les modules Python
suivants :
pandas
, pour la manipulation des données ;plotly
, pour les représentations graphiques ;numpy
, pour utiliser des fonctions de calculs numériques "bas niveau", e.g. génération de nombres aléatoires ;scipy
, pour utiliser d'autres fonctions de calculs numériques plus "haut niveau", e.g. calcul de distances ;sklearn
, pour les algorithmes de machine learning (k-Means, mélange gaussien, ou autres).
Ces modules ne sont pas forcément installés dans votre environnement local ou distant. Vous pouvez
donc utiliser la commande !pip install <nom_module>
pour les installer :
#!pip install pandas #!pip install plotly #!pip install scipy #!pip install scikit-learn
Rappel : code d'import des modules :
for k in range(2, 9): km = skc.KMeans(n_init=1, random_state=56000, n_clusters=k) km.fit(data_df) IB = IT - km.inertia_ print(k, IB) import pandas as pd # Manipulation des données import numpy as np # Calcul numérique import plotly as pl # Librairie principale pour avoir le n° de version import plotly.express as px # Package plotly pour utiliser les visualisations de haut niveau import plotly.figure_factory as pff # Package plotly pour utiliser d'autres visualisations de haut niveau plus exotiques (e.g. dendrogramme) import plotly.io as pio # Nécessaire avec Spyder pio.renderers.default = 'browser' # Nécessaire avec Spyder import sklearn as sk # Librairie principale pour avoir le n° de version import sklearn.cluster as skc # Package sklearn dédié au clustering import scipy as sc # Librairie principale de calcul numérique avancée import scipy.cluster.hierarchy as sch # Package scipy dédié au clustering hiérarchique import scipy.spatial.distance as scd # Package scipy dédié au clacul de distance # Vérification des versions des librairies utilisées {"plotly": pl.__version__, "pandas": pd.__version__, "numpy": np.__version__, "sklearn": sk.__version__, "scipy": sc.__version__}
Exercice 1 : CAH et données wine
Dans cet exercice, nous reprenons les données wine
afin d'y appliquer différentes CAH dans le but
d'identifier des profils de vignobles.
Chargement des données
Chargez les données
Wine
dans unDataFrame
nomméwine_ori_df
(ori
pour originales).wine_path = "https://roland-donat.github.io/cours-class-non-sup/td/td1/wine.csv" wine_ori_df = pd.read_csv(wine_path, sep=",")
Centrer et réduire les données afin d'obtenir un nouveau DataFrame
wine_cr_df
(cr
pour centré-réduit).wine_cr_df = (wine_ori_df - wine_ori_df.mean())/wine_ori_df.std()
CAH avec paramètres par défaut
Nous allons utiliser l'algorithme CAH afin de construire une hiérarchie sur nos vins à
partir de leurs caractéristiques physico-chimiques. Pour ce faire, le package sklearn.cluster
(ou
skc
pour nous) de la librairie sklearn
propose une implémentation de l'algorithme CAH dans
la classe skc.AgglomerativeClustering
.
Construire un modèle CAH avec la classe
skc.AgglomerativeClustering
en utilisant les paramètres par défaut.cah_cr_def = skc.AgglomerativeClustering()
Utiliser ensuite la méthode
fit
pour construire la hiérarchie sur les donnéeswine_cr_df
:cah_cr_def.fit(wine_cr_df)
Explorer l'objet
cah_cr_def
et déterminer la signification des attributs suivants :cah_cr_def.n_clusters_
cah_cr_def.labels_
cah_cr_def.n_leaves_
Décrire les profils obtenus avec la partition du modèle
cah_cr_def
(diagramme en paires, boxplot, profils statistiques détaillés).px.scatter_matrix(wine_ori_df, color=cah_cr_def.labels_.astype(str), title="Partition cah_cr_def").show() px.box(wine_ori_df, color=cah_cr_def.labels_.astype(str), title="Profils des groupes obtenus par la classification cah_cr_def").show() cah_def_ori_prof = \ wine_ori_df.groupby(cah_cr_def.labels_).describe()
- Que peut-on dire du comportement par défaut de la classe
skc.AgglomerativeClustering
en termes, de partitionnement réalisé, de distance entre individus et entre groupes utilisée ? Calculer l'inertie expliquée par la partition
cah_cr_def
. Aide : Utiliser la fonctioneval_partition
du TD2.def eval_partition(data_df, partition): """ Entrées : - data_df (pandas.DataFrame) : Données quantitatives - partition (list, numpy.array ou pandas.Series) : partition des données Sortie : - inertie_intra : inertie intra-classe de la partition - inertie_score : Inertie expliquée par la partition entre 0 et 1 """ # Calcul de l'inertie totale des données (cf. TD1) mu_data = data_df.mean() d2_data_mu = ((data_df - mu_data)**2).sum(axis=1) inertie_totale = d2_data_mu.sum() # Calcul de l'inertie interne aux classes (cf. TD1) inertie_intra = 0 for cls, data_cls_df in data_df.groupby(partition): # Centre de gravité de la classe cls mu_cls = data_cls_df.mean() # Distances au carré entre les données de la classe et le centre de la classe d2_data_cls = ((data_cls_df - mu_cls)**2).sum(axis=1) # Sommation pour obtenir l'inertie interne à la classe inertie_intra += d2_data_cls.sum() # Inertie expliquée par la partition inertie_score = 1 - inertie_intra/inertie_totale return inertie_intra, inertie_score cah_cr_def_iw, cah_cr_def_ie = eval_partition(wine_cr_df, cah_cr_def.labels_)
- Étudier l'aide de la classe
AgglomerativeClustering
afin de comprendre le rôle des principaux paramètres de la classe, à savoirn_clusters
,metric
etlinkage
. Construire une nouvelle hiérarchie
cah_cr_ward
sur les donnéeswine_cr_df
afin de produire une partition en 3 groupes en utilisant la distance de Ward.# SOLUTION # -------- cah_cr_ward = skc.AgglomerativeClustering( n_clusters=3, linkage="ward").fit(wine_cr_df)
Décrire les profils obtenus avec la partition du modèle
cah_cr_ward
(diagramme en paires, boxplot, profils statistiques détaillés).# SOLUTION # -------- px.scatter_matrix(wine_ori_df, color=cah_cr_ward.labels_.astype(str), title="Partition cah_cr_ward").show() px.box(wine_ori_df, color=cah_cr_ward.labels_.astype(str), title="Profils des groupes obtenus par la classification cah_cr_ward").show() cah_ward_ori_prof = \ wine_ori_df.groupby(cah_cr_ward.labels_).describe()
Construire le dendrogramme de la CAH sur les données
wine_cr_df
réalisée en utilisant la distance de Ward. Pour ce faire, utiliser la fonctioncreate_dendrogram
du packageplotly.figure_factory
(ouplf
pour nous).cah_cr_ward_dendro_fig = \ pff.create_dendrogram(wine_cr_df, labels=wine_cr_df.index, color_threshold=26, linkagefun=lambda x: sch.linkage(x, "ward")) cah_cr_ward_dendro_fig.update_layout( title_text="CAH avec distance d'aggrégation Ward et distance entre individus euclidienne", width=1600, height=900) cah_cr_ward_dendro_fig.show()
Influence de la distance entre groupe
Dans cette section, nous allons observer l'influence de la distance entre groupe dans le processus de construction de la CAH.
Construire une CAH
cah_cr_single
sur les donnéeswine_cr
afin de produire une partition en 3 groupes en utilisant la distance du lien minimum (single link).cah_cr_single = skc.AgglomerativeClustering( n_clusters=3, linkage="single").fit(wine_cr_df)
Calculer l'inertie expliquée par la partition
cah_cr_single
.# SOLUTION # -------- cah_cr_single_iw, cah_cr_single_ie = eval_partition(wine_cr_df, cah_cr_single.labels_)
Décrire les profils obtenus avec la partition du modèle
cah_cr_single
(diagramme en paires, boxplot, profils statistiques détaillés).# SOLUTION # -------- px.scatter_matrix(wine_ori_df, color=cah_cr_single.labels_.astype(str), title="Partition cah_cr_single").show() px.box(wine_ori_df, color=cah_cr_single.labels_.astype(str), title="Profils des groupes obtenus par la classification cah_cr_single").show() cah_single_ori_prof = \ wine_ori_df.groupby(cah_cr_single.labels_).describe()
Construire le dendrogramme de la CAH sur les données
wine_cr_df
réalisée en utilisant la distance du lien minimum.# SOLUTION # -------- cah_cr_single_dendro_fig = \ pff.create_dendrogram(wine_cr_df, labels=wine_cr_df.index, color_threshold=3, linkagefun=lambda x: sch.linkage(x, "single")) cah_cr_single_dendro_fig.update_layout( title_text="CAH avec distance d'aggrégation du lien minimum", width=1600, height=900) cah_cr_single_dendro_fig.show()
- Interpréter les résultats obtenus.
Faire une CAH sur les données
wine_cr_df
en réalisant les mêmes analyses que précédemment mais en utilisant la distance du lien maximum (complete link). Le modèle CAH sera nommécah_cr_complete
. Comparer les résultats obtenus avec les CAH précédentes.# SOLUTION # -------- cah_cr_complete = skc.AgglomerativeClustering( n_clusters=3, linkage="complete").fit(wine_cr_df) cah_cr_complete_iw, cah_cr_complete_ie = eval_partition(wine_cr_df, cah_cr_complete.labels_) px.scatter_matrix(wine_ori_df, color=cah_cr_complete.labels_.astype(str), title="Partition cah_cr_complete") px.box(wine_ori_df, color=cah_cr_complete.labels_.astype(str), title="Profils des groupes obtenus par la classification cah_cr_complete").show() cah_complete_ori_prof = \ wine_ori_df.groupby(cah_cr_complete.labels_).describe() cah_cr_complete_dendro_fig = \ pff.create_dendrogram(wine_cr_df, labels=wine_cr_df.index, color_threshold=15, linkagefun=lambda x: sch.linkage(x, "complete")) cah_cr_complete_dendro_fig.update_layout( title_text="CAH avec distance d'aggrégation du lien maximum", width=1600, height=900)
Faire une CAH sur les données
wine_cr_df
en réalisant les mêmes analyses que précédemment mais en utilisant la distance du lien moyen (average link). Le modèle CAH sera nommécah_cr_average
. Comparer les résultats obtenus avec les CAH précédentes.# SOLUTION # -------- cah_cr_average = skc.AgglomerativeClustering( n_clusters=3, linkage="average").fit(wine_cr_df) cah_cr_average_iw, cah_cr_average_ie = eval_partition(wine_cr_df, cah_cr_average.labels_) px.scatter_matrix(wine_cr_df, color=cah_cr_average.labels_.astype(str), title="Partition cah_cr_average") px.box(wine_cr_df, color=cah_cr_average.labels_.astype(str), title="Profils de la partition cah_cr_average") cah_cr_average_dendro_fig = \ pff.create_dendrogram(wine_cr_df, labels=wine_cr_df.index, color_threshold=15, linkagefun=lambda x: sch.linkage(x, "average")) cah_cr_average_dendro_fig.update_layout( title_text="CAH avec distance d'aggrégation du lien moyen", width=1600, height=900)
Exercice 2 : Analyse de profils d'équipes de football
Dans cet exercice, nous allons travailler sur des données historiques de matchs de football. Les données couvrent les deux premières divisions de différents championnats européens.
Le Tableau 1 donne une description des variables du jeu de données.
Colonne | Description |
---|---|
league_id | Identifiant du championnat |
season_id | Identifiant de la saison |
Date | Date du match au format %Y-%m-%d |
HomeTeam | Équipe à domicile (ED) |
AwayTeam | Équipe à l'exterieur (EE) |
FTHG | Nombre de buts de l'ED à la fin du match |
FTAG | Nombre de buts de l'EE à la fin du match |
FTR | Résultat à la fin du match (H = ED gagne, D = match nul, A = EE gagne) |
HTHG | Nombre de buts de l'ED à la mi-temps |
HTAG | Nombre de buts de l'EE à la mi-temps |
HTR | Résultat à la mi-temps (H = ED gagne, D = match nul, A = EE gagne) |
HS | Nombre de tirs tentés par l'ED |
AS | Nombre de tirs tentés par l'EE |
HST | Nombre de tirs cadrés par l'ED |
AST | Nombre de tirs cadrés par l'EE |
HC | Nombre de corners pour l'ED |
AC | Nombre de corners pour l'EE |
HF | Nombre de fautes commises par l'ED |
AF | Nombre de fautes commises par l'EE |
HY | Nombre de cartons jaunes reçus par l'ED |
AY | Nombre de cartons jaunes reçus par l'EE |
HR | Nombre de cartons rouges reçus par l'ED |
AR | Nombre de cartons rouges reçus par l'EE |
L'objectif est d'identifier des profils d'équipes à partir des méthodes de classification non supervisées vues en cours.
Préparation des données
Charger les données à partir de l'adresse suivante : https://roland-donat.github.io/cours-class-non-sup/td/td3/data_soccer_fixtures.csv. Le séparateur est ";".
soccer_path = "https://roland-donat.github.io/cours-class-non-sup/td/td3/data_soccer_fixtures.csv" soccer_df = pd.read_csv(soccer_path, sep=";") soccer_df.head()
Faire une extraction des données de la division 1 française (
league_id = fra_l1
) sur la saison 2018-2019.idx_selection = (soccer_df["league_id"] == "fra_l1") & (soccer_df["season_id"] == "2018-2019") soccer_sel_df = soccer_df.loc[idx_selection] soccer_sel_df.head()
Sélectionner les données des équipes à domicile, i.e.
['HomeTeam', 'FTHG', 'HTHG', 'HS', 'HST', 'HC', 'HF', 'HY', 'HR']
.col_selection = ['HomeTeam', 'FTHG', 'HTHG', 'HS', 'HST', 'HC', 'HF', 'HY', 'HR'] soccer_sel_df = soccer_sel_df[col_selection] soccer_sel_df.head()
Regrouper les données par équipe et calculer la moyenne des faits de jeu par équipe.
soccer_mean_df = soccer_sel_df.groupby("HomeTeam").mean() soccer_mean_df
Détection de profils
Réaliser une CAH sur les données
soccer_mean_df
avec la distance de Ward afin de partitionner les équipes en 4 groupes. Essayer d'interpréter les groupes obtenus.# SOLUTION # -------- cah_soccer_ward = skc.AgglomerativeClustering( n_clusters=4, linkage="ward").fit(soccer_mean_df) cah_soccer_ward_iw, cah_soccer_ward_ie = eval_partition(soccer_mean_df, cah_soccer_ward.labels_) var_num = ['FTHG', 'HTHG', 'HS', 'HST', 'HC', 'HF', 'HY', 'HR'] px.scatter_matrix(soccer_mean_df, dimensions=var_num, hover_name=soccer_mean_df.index, color=cah_soccer_ward.labels_.astype(str), title="Partition en 4 groupes des équipes de football obtenue par CAH.").show() px.box(soccer_mean_df[var_num], color=cah_soccer_ward.labels_.astype(str), title="Profils des groupes obtenus par la classification cah_soccer_ward").show() cah_soccer_ward_prof = \ soccer_mean_df.groupby(cah_soccer_ward.labels_)[var_num].describe() cah_soccer_ward_dendro_fig = \ pff.create_dendrogram(soccer_mean_df, labels=soccer_mean_df.index, color_threshold=6, linkagefun=lambda x: sch.linkage(x, "ward")) cah_soccer_ward_dendro_fig.update_layout( title_text="Dendrogramme de la CAH sur les équipes de football avec la distance de Ward", width=1600, height=900) cah_soccer_ward_dendro_fig.show()
Appliquer la méthode du coude afin d'évaluer un compromis entre nombre de classes et inertie expliquée.
# SOLUTION # -------- cah_soccer_ie = [] link_dist = "ward" K_list = range(2, 10) for k in K_list: cah_soccer_k = skc.AgglomerativeClustering( n_clusters=k, linkage=link_dist).fit(soccer_mean_df) iintra, iscore = eval_partition(soccer_mean_df, cah_soccer_k.labels_) cah_soccer_ie.append(iscore) px.line(x=K_list, y=cah_soccer_ie, title="CAH : Inertie expliquée vs nb de classes", markers=True)
Refaire les traitements précédents en utilisant la méthode des moyennes mobiles. Comparer les résultats obtenus.
# SOLUTION # -------- km_soccer = skc.KMeans( n_clusters=4, n_init="auto").fit(soccer_mean_df) km_soccer_iw, km_soccer_ie = eval_partition(soccer_mean_df, km_soccer.labels_) var_num = ['FTHG', 'HTHG', 'HS', 'HST', 'HC', 'HF', 'HY', 'HR'] px.scatter_matrix(soccer_mean_df, dimensions=var_num, hover_name=soccer_mean_df.index, color=cah_soccer_ward.labels_.astype(str), title="Partition en 4 groupes des équipes de football obtenue par moyenne mobile.") px.box(soccer_mean_df[var_num], color=km_soccer.labels_.astype(str), title="Profils des groupes obtenus par la classification km_soccer").show() km_soccer_ward_prof = \ soccer_mean_df.groupby(km_soccer.labels_)[var_num].describe()
# SOLUTION # -------- km_soccer_ie = [] K_list = range(2, 10) for k in K_list: km_soccer_k = skc.KMeans( n_clusters=k, n_init="auto").fit(soccer_mean_df) iintra, iscore = eval_partition(soccer_mean_df, km_soccer_k.labels_) km_soccer_ie.append(iscore) px.line(x=K_list, y=km_soccer_ie, title="KM : Inertie expliquée vs nb de classes", markers=True)
Exercice 3 : Programmation des distances entre classes
Cet exercice a pour objectif de vous faire programmer les différentes distances entre classes vues en cours. Vous développerez ainsi des fonctions ayant la forme suivante :
def calcul_<nom distance>(data_df1, data_df2): # Votre code à mettre ici return dist
Remarques :
<nom_distance>
est à remplacer par le nom de la distance programmée.data_df1
etdata_df2
sont deuxDataFrame
contenant les individus de deux classes disjointes.- La fonction retourne la valeur de la distance calculée.
- Vous supposerez que la distance entre individus est la distance euclidienne.
Le travail consiste alors à créer les quatre fonctions suivantes :
calcul_single
qui calcule la distance du lien minimum.calcul_complete
qui calcule la distance du lien maximum.calcul_average
qui calcule la distance moyenne.calcul_ward
qui calcule la distance de Ward.
Aide : vous pouvez utiliser la fonction cdist
du package scipy.spatial.distance
.
# SOLUTION # -------- import scipy.spatial.distance as scd # Package scipy dédié au clacul de distance # Distance du lien minimum def calcul_single(data_df1, data_df2): """Calcul du lien minimum (single in english) data_df1 : DataFrame contenant les données du premier groupe data_df2 : DataFrame contenant les données du second groupe Résultat : Distance du lien minimum """ # scd.cdist calcule les distances entre chaque pair d'individus des groupes 1 et 2 data_dist = scd.cdist(data_df1, data_df2) # Transformation en DataFrame pour avoir une jolie matrice de distance entre individus dist_mat_df = pd.DataFrame(data_dist, index=data_df1.index, columns=data_df2.index) # Conversion en vecteur pour préparer le calcul de la valeur min dist_s = dist_mat_df.stack() # Recherche de l'indice (pair d'individus) qui correspond à la distance min # entre individus des deux groupes idx_min = dist_s.idxmin() # Retour de la distance min return dist_s.loc[idx_min] # Distance de Ward def calcul_ward(data_df1, data_df2): """Calcul de la distance de Ward data_df1 : DataFrame contenant les données du premier groupe data_df2 : DataFrame contenant les données du second groupe Résultat : Distance de Ward """ mu_df1 = data_df1.mean() mu_df2 = data_df2.mean() d2 = ((mu_df1 - mu_df2)**2).sum() n1 = len(data_df1) n2 = len(data_df2) dist = (n1*n2/(n1 + n2))*d2 return dist
Données de test :
data_dict = {'life_expec': {'France': 81.4, 'Mozambique': 54.5, 'Guatemala': 71.3, 'Brazil': 74.2, 'Japan': 82.8, 'Argentina': 75.8, 'Senegal': 64.0}, 'total_fer': {'France': 2.03, 'Mozambique': 5.56, 'Guatemala': 3.38, 'Brazil': 1.8, 'Japan': 1.39, 'Argentina': 2.37, 'Senegal': 5.06}} data_df = pd.DataFrame(data_dict) data_df.head() ds = calcul_single(data_df.loc[["France", "Japan"]], data_df.loc[["Mozambique"]]) print(f"distance single : {ds}") # => 27.130626605369812 # On retrouve le résultat de la slide 25/39 du cours sur la CAH dw = calcul_ward(data_df.loc[["France", "Japan"]], data_df.loc[["Mozambique"]]) print(f"distance ward : {dw}") # => 517.7216666666664
dc = calcul_complete(data_df.loc[["France", "Japan"]], data_df.loc[["Mozambique"]]) print(f"distance complete : {dc}") # => 28.60557463152943 da = calcul_average(data_df.loc[["France", "Japan"]], data_df.loc[["Mozambique"]]) print(f"distance average : {da}") # => 27.86810061844962