TD1: Introduction et notions fondamentales
Table des matières
Figure 1 : Image générée par Midjourney
1. Préambule
1.1. Crédit
Ce TD reprend la trame du TD de classification non supervisée proposé par Mme. Arlette Antoni en
2019-2020. Le TD original était réalisé avec le logiciel R
tandis que nous allons utiliser le
logiciel Python
.
1.2. Environnement logiciel
Les travaux se dérouleront sous Python
. Dans ce TD, nous utiliserons en particulier les modules 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.
Ces modules ne sont pas forcément installés dans votre environnement. Vous pouvez
donc utiliser la commande !pip install <nom_module>
pour les installer :
!pip install pandas !pip install plotly !pip install scipy
Rappel : code d'import des modules :
import pandas as pd import numpy as np import plotly as pl import plotly.io as pio # Nécessaire avec Spyder pio.renderers.default = 'browser' # Nécessaire avec Spyder import plotly.express as px import scipy as sc import scipy.spatial.distance as scd import sklearn as sk # Vérification des versions des librairies utilisées {"plotly": pl.__version__, "pandas": pd.__version__, "numpy": np.__version__, "scipy": sc.__version__}
2. Introduction
L'objectif de ce TD est de mettre en pratique les principales notions vues en cours en les appliquant sur un jeu de données réel.
Pour ce faire, nous utiliserons une adaptation des données libres Wine Data Set ne conservant que les variables quantitatives pour la mise en oeuvre de méthodes de classification non supervisée.
Les données correspondent aux résultats d'analyses chimiques sur des vins issus d'une même région d'Italie mais produits par trois vignerons différents. L'analyse quantifie la présence des 13 éléments suivants :
- Alcohol
- Malic acid
- Ash
- Alcalinity of ash
- Magnesium
- Total phenols
- Flavanoids
- Nonflavanoid phenols
- Proanthocyanins
- Color intensity
- Hue
- OD280/OD315 of diluted wines
- Proline
Les données retravaillées à utiliser dans ce TD sont disponibles à ici.
3. Chargement des données et premières analyses
Charger les données dans votre environnement à l'aide de la fonction
pd.read_csv
.data_path = "https://roland-donat.github.io/cours-class-non-sup/td/td1/wine.csv" data_df = pd.read_csv(data_path, sep=",")
- Faire une rapide analyse exploratoire de vos données :
- de manière macroscopique avec les méthodes
.head
,.describe
et.info
; de manière visuelle en représentant la distribution de chaque variable en utilisant la fonction
px.box
. Que constatez-vous ?data_df.describe() data_df.info() px.box(data_df, title="Boxplot de chaque variable (donnée originale)").show()
- de manière macroscopique avec les méthodes
Centrez les données.
data_ctr_df = data_df - data_df.mean()
Réduisez les données centrées précédemment et stocker le résultat dans un nouveau
DataFrame
nommédata_scaled_df
. Aide : utilisez la méthode.std
desDataFrame
.# SOLUTION # -------- data_scaled_df = data_ctr_df/data_df.std()
Calculer la matrice des corrélations linéaires avec la méthode
.corr
desDataFrame
. Quelles sont les caractéristiques remarquables de cette matrice ?data_corr_df = data_df.corr()
Afficher un diagramme en paires et une carte de chaleur des corrélations linéaires.
# Diagramme en paires px.scatter_matrix(data_df, title="Diagramme en paires des données origiales").show() # Carte de chaleur px.imshow(data_df.corr(), color_continuous_midpoint=0, title="Corrélations linéaires des données sous forme de carte de chaleur (heatmap)").show()
4. Distance euclidienne
Dans la suite du TD, nous allons travailler avec les données centrées réduites calculées
précédemment dans le DataFrame
data_scaled_df
.
Calculer la distance euclidienne entre les deux premiers individus en utilisant la méthode
sum
desDataFrame
.d2_ind_01 = ((data_scaled_df.loc[0] - data_scaled_df.loc[1])**2).sum() d_ind_01 = d2_ind_01**(0.5) d_ind_01
Calculer la distance euclidienne entre le premier individu et le troisième, puis entre le deuxième et le troisième.
# SOLUTION # -------- d2_ind_02 = ((data_scaled_df.loc[0] - data_scaled_df.loc[2])**2).sum() d_ind_02 = d2_ind_02**(0.5) d_ind_02
Calculer la matrice des distances euclidiennes. Aide : utilisez les fonctions
pdist
etsquareform
du packagescipy.spatial.distance
. Quelles sont les dimensions de cette matrice, quelles sont ses propriétés remarquables ?dist_array = scd.pdist(data_scaled_df, metric="euclidean") dist_mat = scd.squareform(dist_array) dist_mat
Transformer la matrice de distances précédente sous la forme d'un
DataFrame
pour en améliorer la lisibilité. Vérifier que les distances calculées aux points 1 et 2 grâce à la matrice des distances calculée au point 3.# SOLUTION # -------- dist_mat_df = \ pd.DataFrame(dist_mat, index=data_scaled_df.index, columns=data_scaled_df.index) dist_mat_df
5. Inertie totale
Dans la suite du TD, les données seront considérées comme étant équipondérées avec un poids de 1 pour chaque observation. Par ailleurs, la distance utilisée sera la distance euclidienne.
Nous travaillons toujours avec les données centrées réduites du DataFrame
data_scaled_df
.
Calculer le centre de gravité du nuage d'observations. Que remarquez-vous ?
mu_data = data_scaled_df.mean() mu_data
Calculer les distances au carré entre les observations et le centre de gravité du nuage.
# SOLUTION # -------- d2_data_mu = ((data_scaled_df - mu_data)**2).sum(axis=1) d2_data_mu
En déduire l'inertie totale des données.
# SOLUTION # -------- I_T = d2_data_mu.sum() I_T
Calculer la somme des variances empiriques de chaque variable. Aide : utilisez la méthode
.var
desDataFrame
? Quels calculs fait exactement la méthode.var
? Aurait-on pu prévoir le résultat dans ce cas ?# SOLUTION # -------- S2_var = data_scaled_df.var().sum() S2_var
Calculer le rapport entre l'inertie totale et la somme des variances de chaque variable. Expliquer le résultat.
# SOLUTION # -------- I_T/S2_var
6. Première partition \(C_{\text{A}}\)
6.1. Construction des classes
Nous allons nous donner une première partition arbitraire, notée \(C_{\text{A}}\), consistant à affecter :
- la classe \(c_{1}\) aux individus d'indices 0-49 ;
- la classe \(c_{2}\) aux individus d'indices 50-99 ;
- la classe \(c_{3}\) aux individus d'indices 100-177.
Rappel : N'oubliez pas que Python
indexe les listes, vecteurs, tableaux en commençant à 0 !
Ajoutez une nouvelle variable
cls_A
contenant la classe de chaque individu suivant le schéma décrit précédemment.data_scaled_df["cls_A"] = "c1" data_scaled_df["cls_A"].loc[50:99] = "c2" data_scaled_df["cls_A"].loc[100:] = "c3"
Visualiser le nuage d'individus sur les variables
OD280
etAlcohol
en faisant apparaître votre partition. Pour ce faire, utiliser la fonctionpx.scatter
avec l'optioncolor
pour colorer les individus en fonction de leur classe.px.scatter(data_scaled_df, x="OD280", y="Alcohol", color="cls_A", title="Visualisation de la partition A sur les données", labels={"cls_A": "Partition"}).show()
Calculer l'effectif de chaque classe avec la méthode
.value_counts
.N_cls_A = data_scaled_df["cls_A"].value_counts() N_cls_A
Réaliser les boxplot de chaque variable en fonction de leur classe.
px.box(data_scaled_df.select_dtypes(include=np.number), color=data_scaled_df["cls_A"], title="Visualisation de la partition A sur les données", labels={"cls_A": "Partition"}).show()
6.2. Inertie intra-classe
Calculer le centre de gravité de chaque classe. Pour ce faire, utiliser la méthode
groupby
desDataFrame
afin de regrouper les données sur la variable de classe et réaliser les traitements appropriés sur chaque groupe.# Création des groupes selon la partition A data_cls_A_grp = data_scaled_df.groupby("cls_A") # Calcul des centres de chaque classe mu_cls_A = data_cls_A_grp.mean() mu_cls_A
Pour accéder au centre de la classe
"c1"
, utiliser l'accesseur.loc
de la façon suivantemu_cls_A.loc["c1"]
.Calculez l'inertie interne de la classe
"c1"
.# Récupération des observations de la classe "c1" data_c1_df = data_cls_A_grp.get_group("c1") d2_data_c1 = ((data_c1_df - mu_cls_A.loc["c1"])**2).sum(axis=1) I_W_c1 = d2_data_c1.sum()
Calculer l'inertie interne de chacune des classes de la partition A. Pour rendre votre code générique et réutilisable, utiliser une boucle
for
. Pour vous aider, inspirez-vous du code à compléter suivant :# On initialise un vecteur de trois élements nommés c1, c2 et c3 ayant pour valeur 0. # Ce vecteur servira à récupérer l'inertie interne des 3 groupes dans la boucle. I_W_cls_A = pd.Series(0, index=mu_cls_A.index) # Note : il est possible d'itérer sur un objet `groupby` avec un couple de variables. # Dans la boucle ci-dessous cls prendra successivement les valeurs "c1", "c2" et "c3" ; # et data_cls_df contiendra successivement les individus des classes "c1", "c2" et "c3". for cls, data_cls_df in data_cls_A_grp: # Calcul des distances au carré entre chaque individu de la classe courante avec le centre de cette classe. d2_data_cls = # À COMPLÉTER # Sommation des distances au carré pour obtenir l'inertie de la classe courante I_W_cls_A.loc[cls] = # À COMPLÉTER I_W_cls_A
# SOLUTION # -------- I_W_cls_A = pd.Series(0, index=mu_cls_A.index) for cls, data_cls_df in data_cls_A_grp: d2_data_cls = ((data_cls_df - mu_cls_A.loc[cls])**2).sum(axis=1) I_W_cls_A.loc[cls] = d2_data_cls.sum() I_W_cls_A
En déduire l'inertie intra-classe de la partition.
# SOLUTION # -------- I_W_A = I_W_cls_A.sum() I_W_A
En déduire l'inertie inter-classe de la partition.
# SOLUTION # -------- I_B_A = I_T - I_W_A I_B_A
Calculer le pourcentage d'inertie expliquée par la partition.
# SOLUTION # -------- PIE_A = 100*(1 - I_W_A/I_T) PIE_A_bis = 100*I_B_A/I_T (PIE_A, PIE_A_bis)
Calculer la somme des variances empiriques corrigées des variables au sein de chaque classe.
# SOLUTION # -------- S2d_cls_A = data_cls_A_grp.var() S2d_cls_A_sum = S2d_cls_A.sum(axis=1) S2d_cls_A_sum
Calculer le rapport des inerties internes et la somme des variances empiriques corrigées des variables au sein de chaque classe (résultat de la question précdente).
# SOLUTION # -------- I_W_cls_A/S2d_cls_A_sum
Comment calculer l'inertie intra-classe en utilisant la somme des variances empiriques corrigées des variables au sein de chaque classe et l'effectif de chaque classe ?
# SOLUTION # -------- (S2d_cls_A_sum*(N_cls_A - 1)).sum()
6.3. Inertie inter-classe
Calculer le carré des distances entre le centre de gravité des classes et le centre de gravité des données.
# SOLUTION # -------- d2_mu_cls_A = ((mu_cls_A - mu_data)**2).sum(axis=1) d2_mu_cls_A
En déduire l'inertie inter-classe de la partition.
# SOLUTION # -------- # - On oublie pas de pondérer le calcul par le poids de chaque classe # - Ici omega = 1 pour tous les individus, donc poids de la classe k = effectif de la classe k I_B_A = (N_cls_A*d2_mu_cls_A).sum() I_B_A
7. Seconde partition \(C_{\text{B}}\)
Reprendre les questions de la partie précédente en construisant une partition \(C_{\text{B}}\) au
hasard. Pour ce faire, utilisez la fonction np.ramdom.choice
pour affecter les classes aux
individus. N'hésitez pas à fixer la graine du générateur aléatoire avec la fonction np.random.seed
afin de reproduire le "même hasard" d'une exécution à l'autre de votre script.
# On fixe la graîne du générateur de nombre aléatoire pour # reproduire le même "hasard" d'une exécution à l'autre np.random.seed(56) # Initalisation au hasard de la partition data_scaled_df["cls_B"] = np.random.choice(["c1", "c2", "c3"], len(data_scaled_df)) N_cls_B = data_scaled_df["cls_B"].value_counts() N_cls_B
# SOLUTION # -------- # Dérouler tous les traitements de la partie précédente en changeant "cls_A" par "cls_B" ;)