Exploration et visualisation#

Le but de la visualisation est la compréhension, pas les images.

Ben Shneiderman

Introduction#

L”analyse exploratoire des données (Exploratory Data Analysis, EDA) est l’étape fondamentale qui précède toute modélisation. Avant de construire un modèle, il faut comprendre les données : leur structure, leurs distributions, leurs relations, leurs anomalies. L’EDA, popularisée par John Tukey dans les années 1970, repose sur deux piliers : les statistiques descriptives et la visualisation.

L’exploration permet de :

  • vérifier la qualité des données (valeurs manquantes, incohérences, doublons)

  • comprendre la distribution de chaque variable

  • identifier les relations entre variables

  • détecter les valeurs aberrantes (outliers)

  • formuler des hypothèses pour guider la modélisation

  • choisir les transformations et prétraitements appropriés

Remarque 22

L’EDA n’est pas une étape linéaire : c’est un processus itératif. Chaque visualisation peut soulever de nouvelles questions, qui appellent de nouvelles explorations. Il ne faut jamais sous-estimer le temps consacré à cette phase : un modèle construit sur des données mal comprises sera, au mieux, médiocre.

Chargeons les bibliothèques et les données que nous utiliserons tout au long de ce chapitre.

Hide code cell source

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.datasets import load_iris, fetch_california_housing

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)

Hide code cell source

# Jeu de données Iris (classification)
iris = load_iris(as_frame=True)
df_iris = iris.frame

# Jeu de données California Housing (régression)
housing = fetch_california_housing(as_frame=True)
df_housing = housing.frame

print(f"Iris : {df_iris.shape[0]} observations, {df_iris.shape[1]} variables")
print(f"California Housing : {df_housing.shape[0]} observations, {df_housing.shape[1]} variables")
Iris : 150 observations, 5 variables
California Housing : 20640 observations, 9 variables

Statistiques descriptives#

Les statistiques descriptives résument un jeu de données par quelques nombres caractéristiques. On distingue les mesures de tendance centrale (où se situe le « centre » des données), les mesures de dispersion (à quel point les données s’étalent) et les mesures de forme (la géométrie de la distribution).

Mesures de tendance centrale#

Définition 16 (Moyenne arithmétique)

Soit un échantillon \((x_1, \ldots, x_n)\). La moyenne arithmétique est

\[\bar{x} = \frac{1}{n} \sum_{i=1}^n x_i\]

Définition 17 (Médiane)

La médiane est la valeur qui sépare l’échantillon ordonné en deux moitiés égales. Si l’on note \(x_{(1)} \leq x_{(2)} \leq \cdots \leq x_{(n)}\) les statistiques d’ordre :

\[\begin{split}\text{Med} = \begin{cases} x_{((n+1)/2)} & \text{si } n \text{ est impair} \\ \frac{1}{2}\left(x_{(n/2)} + x_{(n/2+1)}\right) & \text{si } n \text{ est pair} \end{cases}\end{split}\]

Définition 18 (Mode)

Le mode est la valeur (ou les valeurs) la plus fréquente dans l’échantillon. Pour des données continues, on parle du mode de la distribution estimée (le sommet de la densité).

Remarque 23

La moyenne est sensible aux valeurs extrêmes ; la médiane est robuste. Si la distribution est symétrique, moyenne et médiane coïncident. Si la distribution est asymétrique à droite (right-skewed), on a typiquement \(\text{mode} < \text{médiane} < \text{moyenne}\).

Hide code cell source

# Statistiques de tendance centrale pour une variable
col = "MedHouseVal"
print(f"Moyenne  : {df_housing[col].mean():.4f}")
print(f"Médiane  : {df_housing[col].median():.4f}")
print(f"Mode     : {df_housing[col].mode().values[0]:.4f}")
Moyenne  : 2.0686
Médiane  : 1.7970
Mode     : 5.0000

Mesures de dispersion#

Définition 19 (Variance et écart-type)

La variance empirique (non biaisée) et l”écart-type d’un échantillon sont

\[s^2 = \frac{1}{n-1} \sum_{i=1}^n (x_i - \bar{x})^2, \qquad s = \sqrt{s^2}\]

La division par \(n - 1\) (correction de Bessel) assure que \(\mathbb{E}[s^2] = \sigma^2\).

Définition 20 (Quantiles)

Le quantile d’ordre \(p\) (avec \(0 < p < 1\)) est la valeur \(q_p\) telle que

\[\frac{|\{i : x_i \leq q_p\}|}{n} \approx p\]

Les quantiles d’ordre \(1/4\), \(1/2\) et \(3/4\) sont les quartiles \(Q_1\), \(Q_2\) (médiane) et \(Q_3\). L”écart interquartile est \(\text{IQR} = Q_3 - Q_1\).

Remarque 24

L’écart interquartile est une mesure de dispersion robuste : il n’est pas affecté par les valeurs extrêmes. C’est la mesure utilisée par les boxplots pour définir les « moustaches » et détecter les outliers.

Hide code cell source

col = "MedHouseVal"
print(f"Variance     : {df_housing[col].var():.4f}")
print(f"Écart-type   : {df_housing[col].std():.4f}")
print(f"Q1 (25%)     : {df_housing[col].quantile(0.25):.4f}")
print(f"Q2 (50%)     : {df_housing[col].quantile(0.50):.4f}")
print(f"Q3 (75%)     : {df_housing[col].quantile(0.75):.4f}")
print(f"IQR          : {df_housing[col].quantile(0.75) - df_housing[col].quantile(0.25):.4f}")
Variance     : 1.3316
Écart-type   : 1.1540
Q1 (25%)     : 1.1960
Q2 (50%)     : 1.7970
Q3 (75%)     : 2.6472
IQR          : 1.4512

Mesures de forme#

Définition 21 (Coefficient d’asymétrie (skewness))

Le coefficient d’asymétrie (ou skewness) mesure le degré d’asymétrie d’une distribution :

\[\gamma_1 = \frac{1}{n} \sum_{i=1}^n \left(\frac{x_i - \bar{x}}{s}\right)^3\]
  • \(\gamma_1 = 0\) : distribution symétrique

  • \(\gamma_1 > 0\) : queue à droite plus longue (right-skewed)

  • \(\gamma_1 < 0\) : queue à gauche plus longue (left-skewed)

Définition 22 (Coefficient d’aplatissement (kurtosis))

Le coefficient d’aplatissement (ou kurtosis) mesure l’épaisseur des queues :

\[\gamma_2 = \frac{1}{n} \sum_{i=1}^n \left(\frac{x_i - \bar{x}}{s}\right)^4 - 3\]

La soustraction de \(3\) (excess kurtosis) normalise par rapport à la loi normale (\(\gamma_2 = 0\)).

  • \(\gamma_2 > 0\) : queues plus épaisses que la normale (leptokurtique)

  • \(\gamma_2 < 0\) : queues plus fines que la normale (platykurtique)

Hide code cell source

print("Skewness et kurtosis des variables numériques (California Housing) :\n")
for col in df_housing.columns:
    sk = df_housing[col].skew()
    ku = df_housing[col].kurtosis()
    print(f"  {col:20s}  skewness = {sk:+.3f}   kurtosis = {ku:+.3f}")
Skewness et kurtosis des variables numériques (California Housing) :

  MedInc                skewness = +1.647   kurtosis = +4.953
  HouseAge              skewness = +0.060   kurtosis = -0.801
  AveRooms              skewness = +20.698   kurtosis = +879.353
  AveBedrms             skewness = +31.317   kurtosis = +1636.712
  Population            skewness = +4.936   kurtosis = +73.553
  AveOccup              skewness = +97.640   kurtosis = +10651.011
  Latitude              skewness = +0.466   kurtosis = -1.118
  Longitude             skewness = -0.298   kurtosis = -1.330
  MedHouseVal           skewness = +0.978   kurtosis = +0.328

Résumé avec pandas#

La méthode describe() de pandas fournit un résumé complet en une seule ligne.

Hide code cell source

df_housing.describe().round(3)
MedInc HouseAge AveRooms AveBedrms Population AveOccup Latitude Longitude MedHouseVal
count 20640.000 20640.000 20640.000 20640.000 20640.000 20640.000 20640.000 20640.000 20640.000
mean 3.871 28.639 5.429 1.097 1425.477 3.071 35.632 -119.570 2.069
std 1.900 12.586 2.474 0.474 1132.462 10.386 2.136 2.004 1.154
min 0.500 1.000 0.846 0.333 3.000 0.692 32.540 -124.350 0.150
25% 2.563 18.000 4.441 1.006 787.000 2.430 33.930 -121.800 1.196
50% 3.535 29.000 5.229 1.049 1166.000 2.818 34.260 -118.490 1.797
75% 4.743 37.000 6.052 1.100 1725.000 3.282 37.710 -118.010 2.647
max 15.000 52.000 141.909 34.067 35682.000 1243.333 41.950 -114.310 5.000

Distributions#

L’étude de la distribution de chaque variable est la première étape de l’EDA. On cherche à répondre aux questions : la variable est-elle symétrique ? unimodale ? la loi normale est-elle une approximation raisonnable ?

Histogrammes#

L’histogramme est la visualisation la plus élémentaire d’une distribution. Il discrétise l’axe en bins et compte le nombre d’observations dans chaque intervalle.

Hide code cell source

fig, axes = plt.subplots(2, 1, figsize=(9, 7))

axes[0].hist(df_housing["MedHouseVal"], bins=50, edgecolor="white", alpha=0.8)
axes[0].set_xlabel("Valeur médiane des maisons")
axes[0].set_ylabel("Fréquence")
axes[0].set_title("Histogramme — MedHouseVal")

axes[1].hist(df_housing["HouseAge"], bins=40, edgecolor="white", alpha=0.8, color="coral")
axes[1].set_xlabel("Âge médian des maisons")
axes[1].set_ylabel("Fréquence")
axes[1].set_title("Histogramme — HouseAge")

plt.tight_layout()
plt.show()
_images/a31e1307ec4e8240615041d3295866b5192f07f152cc1214a24f6aaf031ff35b.png

Remarque 25

Le choix du nombre de bins est déterminant. Trop peu de bins lissent la distribution et masquent les détails ; trop de bins produisent un graphique bruité. Des règles comme celle de Sturges (\(k = 1 + \log_2 n\)) ou de Freedman-Diaconis (\(h = 2 \cdot \text{IQR} \cdot n^{-1/3}\)) fournissent des heuristiques utiles.

Estimation par noyau (KDE)#

Définition 23 (Estimation par noyau (KDE))

L”estimation par noyau (Kernel Density Estimation) estime la densité de probabilité \(f\) à partir d’un échantillon \((x_1, \ldots, x_n)\) :

\[\hat{f}_h(x) = \frac{1}{nh} \sum_{i=1}^n K\left(\frac{x - x_i}{h}\right)\]

\(K\) est un noyau (typiquement gaussien : \(K(u) = \frac{1}{\sqrt{2\pi}} e^{-u^2/2}\)) et \(h > 0\) est la largeur de bande (bandwidth).

Remarque 26

La largeur de bande \(h\) joue un rôle analogue au nombre de bins : trop petit, le KDE est bruité (surapprentissage) ; trop grand, il est trop lissé (sous-apprentissage). La règle de Silverman (\(h = 1.06 \cdot s \cdot n^{-1/5}\)) donne une bonne valeur par défaut pour des distributions unimodales.

Hide code cell source

fig, ax = plt.subplots(figsize=(8, 4))

for feature in ["sepal length (cm)", "petal length (cm)"]:
    sns.kdeplot(data=df_iris, x=feature, fill=True, alpha=0.4, label=feature, ax=ax)

ax.set_xlabel("Valeur (cm)")
ax.set_ylabel("Densité estimée")
ax.set_title("Estimation par noyau (KDE) — Iris")
ax.legend()
plt.tight_layout()
plt.show()
_images/60f8ad46627117a2b35eaf80dfe784cb472aa2f058c55c60c0ca2b4e9f921ece.png

QQ-plots#

Définition 24 (QQ-plot)

Un QQ-plot (Quantile-Quantile plot) compare les quantiles empiriques d’un échantillon aux quantiles théoriques d’une distribution de référence (souvent la loi normale). Si les données suivent la distribution de référence, les points s’alignent sur la diagonale \(y = x\).

Hide code cell source

from scipy import stats

fig, axes = plt.subplots(2, 1, figsize=(9, 9))

# Variable approximativement normale
stats.probplot(df_iris["sepal width (cm)"], dist="norm", plot=axes[0])
axes[0].set_title("QQ-plot — sepal width (cm)")
axes[0].get_lines()[0].set_color("steelblue")

# Variable non normale
stats.probplot(df_housing["MedHouseVal"], dist="norm", plot=axes[1])
axes[1].set_title("QQ-plot — MedHouseVal")
axes[1].get_lines()[0].set_color("coral")

plt.tight_layout()
plt.show()
_images/fd4d457798bc1de099551a30c152d48bd02cd0d939c9ad704ca88bdc31652458.png

Tests de normalité#

Proposition 4 (Test de Shapiro-Wilk)

Le test de Shapiro-Wilk teste l’hypothèse nulle \(H_0\) : « l’échantillon provient d’une population normalement distribuée ». La statistique de test est

\[W = \frac{\left(\sum_{i=1}^n a_i x_{(i)}\right)^2}{\sum_{i=1}^n (x_i - \bar{x})^2}\]

où les coefficients \(a_i\) dépendent des moments attendus des statistiques d’ordre de la loi normale. On rejette \(H_0\) si la \(p\)-valeur est inférieure au seuil \(\alpha\) (typiquement \(0.05\)).

Remarque 27

Le test de Shapiro-Wilk est l’un des plus puissants pour détecter la non-normalité, mais il est limité aux échantillons de taille \(n \leq 5000\). Pour de grands échantillons, on peut utiliser le test de D’Agostino-Pearson ou le test de Kolmogorov-Smirnov.

Hide code cell source

from scipy.stats import shapiro, normaltest

variables = ["sepal width (cm)", "petal length (cm)"]
for var in variables:
    stat_sw, p_sw = shapiro(df_iris[var])
    stat_dp, p_dp = normaltest(df_iris[var])
    print(f"{var}")
    print(f"  Shapiro-Wilk      : W = {stat_sw:.4f}, p = {p_sw:.4f}")
    print(f"  D'Agostino-Pearson: S = {stat_dp:.4f}, p = {p_dp:.4f}")
    print(f"  → {'Normal' if p_sw > 0.05 else 'Non normal'} (Shapiro-Wilk, α = 0.05)")
    print()
sepal width (cm)
  Shapiro-Wilk      : W = 0.9849, p = 0.1012
  D'Agostino-Pearson: S = 3.1238, p = 0.2097
  → Normal (Shapiro-Wilk, α = 0.05)

petal length (cm)
  Shapiro-Wilk      : W = 0.8763, p = 0.0000
  D'Agostino-Pearson: S = 221.6873, p = 0.0000
  → Non normal (Shapiro-Wilk, α = 0.05)

Corrélations#

Les corrélations mesurent la force et la direction de la relation linéaire (ou monotone) entre deux variables.

Coefficient de Pearson#

Définition 25 (Coefficient de corrélation de Pearson)

Le coefficient de corrélation linéaire de Pearson entre deux variables \(X\) et \(Y\) est

\[r_{XY} = \frac{\sum_{i=1}^n (x_i - \bar{x})(y_i - \bar{y})}{\sqrt{\sum_{i=1}^n (x_i - \bar{x})^2 \cdot \sum_{i=1}^n (y_i - \bar{y})^2}} = \frac{\text{Cov}(X, Y)}{s_X \cdot s_Y}\]

avec \(r \in [-1, 1]\).

  • \(r = +1\) : corrélation linéaire positive parfaite

  • \(r = -1\) : corrélation linéaire négative parfaite

  • \(r = 0\) : absence de corrélation linéaire (mais il peut exister une relation non linéaire)

Coefficient de Spearman#

Définition 26 (Coefficient de corrélation de Spearman)

Le coefficient de Spearman est le coefficient de Pearson appliqué aux rangs des observations. Si \(\text{rg}(x_i)\) et \(\text{rg}(y_i)\) sont les rangs :

\[\rho_s = r_{\text{rg}(X), \text{rg}(Y)} = 1 - \frac{6 \sum_{i=1}^n d_i^2}{n(n^2 - 1)}\]

\(d_i = \text{rg}(x_i) - \text{rg}(y_i)\), en l’absence d’ex aequo.

Remarque 28

Pearson mesure la linéarité de la relation ; Spearman mesure la monotonie. Si la relation est monotone mais non linéaire (par exemple exponentielle), \(|\rho_s|\) peut être proche de \(1\) alors que \(|r|\) est sensiblement plus faible. Spearman est également plus robuste aux valeurs aberrantes.

Matrice de corrélation et heatmaps#

Hide code cell source

corr_pearson = df_housing.corr(method="pearson")
corr_spearman = df_housing.corr(method="spearman")

fig, axes = plt.subplots(2, 1, figsize=(9, 11))

sns.heatmap(corr_pearson, annot=True, fmt=".2f", cmap="coolwarm", center=0,
            square=True, linewidths=0.5, ax=axes[0])
axes[0].set_title("Corrélation de Pearson")

sns.heatmap(corr_spearman, annot=True, fmt=".2f", cmap="coolwarm", center=0,
            square=True, linewidths=0.5, ax=axes[1])
axes[1].set_title("Corrélation de Spearman")

plt.tight_layout()
plt.show()
_images/33b098d4a5c316a02906fe18ec887dd4c79b0759b7117838dd49331060ad2301.png

Exemple 3

Sur le jeu California Housing, on observe que MedInc (revenu médian) est fortement corrélé à MedHouseVal (valeur médiane des maisons) avec \(r \approx 0.69\). C’est l’information la plus importante pour un modèle de régression du prix. En revanche, AveBedrms et AveRooms sont très corrélés entre eux (\(r \approx 0.85\)) : c’est un signal de multicolinéarité qu’il faudra traiter lors du prétraitement.

Visualisations univariées#

Les visualisations univariées décrivent la distribution d’une seule variable à la fois.

Histogrammes et KDE combinés#

Hide code cell source

fig, axes = plt.subplots(2, 2, figsize=(12, 9))
features = df_iris.columns[:4]

for ax, feature in zip(axes.flat, features):
    sns.histplot(data=df_iris, x=feature, kde=True, bins=25, ax=ax, color="steelblue")
    ax.set_title(feature)
    ax.set_ylabel("Fréquence")

plt.suptitle("Histogrammes avec KDE — Iris", fontsize=14, y=1.01)
plt.tight_layout()
plt.show()
_images/c0cce40c01247871858d65a1b6cf2eafb4565c6cb9126739a55047c3e6289846.png

Boxplots (diagrammes en boîte)#

Définition 27 (Boxplot)

Un boxplot (diagramme en boîte) représente la distribution d’une variable par :

  • la boîte : de \(Q_1\) à \(Q_3\) (contient 50 % des données)

  • la ligne centrale : la médiane \(Q_2\)

  • les moustaches : s’étendent jusqu’aux valeurs les plus extrêmes dans l’intervalle \([Q_1 - 1.5 \cdot \text{IQR},\ Q_3 + 1.5 \cdot \text{IQR}]\)

  • les points au-delà des moustaches : les valeurs aberrantes (outliers)

Hide code cell source

fig, axes = plt.subplots(2, 1, figsize=(9, 9))

# Boxplots par espèce
sns.boxplot(data=df_iris, x="target", y="petal length (cm)", hue="target", ax=axes[0], palette="Set2", legend=False)
axes[0].set_xlabel("Espèce")
axes[0].set_title("Boxplot — petal length par espèce")

# Boxplots de toutes les variables Iris
df_iris_features = df_iris.drop(columns="target")
sns.boxplot(data=df_iris_features, orient="h", ax=axes[1], palette="Set2")
axes[1].set_title("Boxplot — toutes les variables Iris")

plt.tight_layout()
plt.show()
_images/f4557a03e3e0f67c39cf993782447245070fe84b6659577c4089c589b66a19b0.png

Violin plots#

Définition 28 (Violin plot)

Un violin plot combine un boxplot et un KDE symétrique. La largeur du « violon » à chaque ordonnée représente la densité estimée de la distribution, offrant une vision plus riche qu’un simple boxplot.

Hide code cell source

fig, ax = plt.subplots(figsize=(10, 5))

sns.violinplot(data=df_iris, x="target", y="sepal length (cm)",
               inner="quart", hue="target", palette="pastel", ax=ax, legend=False)
ax.set_xlabel("Espèce")
ax.set_ylabel("Longueur du sépale (cm)")
ax.set_title("Violin plot — sepal length par espèce")
plt.tight_layout()
plt.show()
_images/9e382d607d2500ba6a3a011bce51fca8c471a3dfc1788f8e25fca6a42b9de83b.png

Remarque 29

Les violin plots sont particulièrement utiles lorsque la distribution est bimodale ou multimodale : le KDE révèle la forme complète, là où le boxplot ne montre que les quartiles.

Visualisations bivariées#

Les visualisations bivariées examinent la relation entre deux variables.

Scatter plots (nuages de points)#

Hide code cell source

fig, axes = plt.subplots(2, 1, figsize=(9, 9))

# Scatter plot simple
axes[0].scatter(df_housing["MedInc"], df_housing["MedHouseVal"],
                alpha=0.15, s=5, c="steelblue")
axes[0].set_xlabel("Revenu médian (MedInc)")
axes[0].set_ylabel("Valeur médiane (MedHouseVal)")
axes[0].set_title("Scatter plot — Revenu vs Prix")

# Scatter plot coloré par classe
for i, name in enumerate(iris.target_names):
    mask = df_iris["target"] == i
    axes[1].scatter(df_iris.loc[mask, "petal length (cm)"],
                    df_iris.loc[mask, "petal width (cm)"],
                    label=name, alpha=0.7, s=40)
axes[1].set_xlabel("Longueur du pétale (cm)")
axes[1].set_ylabel("Largeur du pétale (cm)")
axes[1].set_title("Scatter plot — Iris (petal)")
axes[1].legend()

plt.tight_layout()
plt.show()
_images/b7b3963d9efd93d87e1151852dbc6ed31b47256ef6da80c094992d6e5ae6e1ba.png

Pair plots#

Définition 29 (Pair plot)

Un pair plot (ou matrice de nuages de points) affiche les scatter plots de toutes les paires de variables, avec les distributions univariées sur la diagonale. C’est un outil puissant pour obtenir une vue d’ensemble rapide des relations entre variables.

Hide code cell source

g = sns.pairplot(df_iris, hue="target", palette="Set1",
                 diag_kind="kde", plot_kws={"alpha": 0.6, "s": 30})
g.figure.suptitle("Pair plot — Iris", y=1.01, fontsize=14)
plt.show()
_images/9b01f019f70b990b3ee0a3cd0f2b1445354642462628e08e7993d73b5f7768f5.png

Remarque 30

Le pair plot du jeu Iris est un classique de la visualisation en apprentissage automatique. On observe que la longueur et la largeur du pétale séparent nettement les trois espèces, ce qui indique que ces deux variables seront les plus discriminantes pour un classifieur.

Bar plots#

Hide code cell source

fig, axes = plt.subplots(2, 1, figsize=(9, 9))

# Comptage par classe
target_counts = df_iris["target"].value_counts().sort_index()
axes[0].bar(iris.target_names, target_counts.values, color=["#4C72B0", "#55A868", "#C44E52"])
axes[0].set_xlabel("Espèce")
axes[0].set_ylabel("Nombre d'observations")
axes[0].set_title("Distribution des classes — Iris")

# Moyennes par classe avec barres d'erreur
means = df_iris.groupby("target")["sepal length (cm)"].mean()
stds = df_iris.groupby("target")["sepal length (cm)"].std()
axes[1].bar(iris.target_names, means.values, yerr=stds.values,
            capsize=5, color=["#4C72B0", "#55A868", "#C44E52"], alpha=0.8)
axes[1].set_xlabel("Espèce")
axes[1].set_ylabel("Longueur du sépale (cm)")
axes[1].set_title("Moyenne (± écart-type) — sepal length")

plt.tight_layout()
plt.show()
_images/3a7630dc738c9bbe2de732b98ed89fc391261b470af51728816f89a8731b1708.png

Visualisations multivariées#

Lorsque le nombre de variables dépasse deux, il faut recourir à des techniques de visualisation plus élaborées.

Heatmaps#

Les heatmaps ne servent pas uniquement pour les matrices de corrélation. Elles peuvent représenter toute matrice numérique.

Hide code cell source

# Heatmap des moyennes par espèce
means_by_species = df_iris.groupby("target").mean()
means_by_species.index = iris.target_names

fig, ax = plt.subplots(figsize=(8, 4))
sns.heatmap(means_by_species, annot=True, fmt=".2f", cmap="YlOrRd",
            linewidths=1, ax=ax)
ax.set_title("Valeurs moyennes par espèce — Iris")
ax.set_ylabel("Espèce")
plt.tight_layout()
plt.show()
_images/6bf8f07fe643dda5182f79b36803dfca1ba85398599c0c58c2859465b9b7f547.png

Coordonnées parallèles#

Définition 30 (Coordonnées parallèles)

Un graphique en coordonnées parallèles représente chaque observation comme une ligne brisée traversant des axes verticaux parallèles (un axe par variable). C’est une technique efficace pour visualiser des données à plusieurs dimensions et identifier des groupes ou des tendances.

Hide code cell source

from pandas.plotting import parallel_coordinates

# Normalisation min-max pour la comparabilité des axes
df_iris_norm = df_iris.copy()
for col in df_iris.columns[:4]:
    df_iris_norm[col] = (df_iris[col] - df_iris[col].min()) / (df_iris[col].max() - df_iris[col].min())
df_iris_norm["species"] = df_iris["target"].map(dict(enumerate(iris.target_names)))

fig, ax = plt.subplots(figsize=(10, 5))
parallel_coordinates(df_iris_norm.drop(columns="target"), class_column="species",
                     colormap="Set1", alpha=0.4, ax=ax)
ax.set_title("Coordonnées parallèles — Iris (normalisé)")
ax.set_ylabel("Valeur normalisée [0, 1]")
ax.legend(loc="upper right")
plt.tight_layout()
plt.show()
_images/a4a2330428664eb818c11f837d98485065c5b6a22d002cc013ba54a2a3e92472.png

Radar charts (diagrammes en radar)#

Définition 31 (Radar chart)

Un radar chart (ou diagramme en toile d’araignée) représente les valeurs de plusieurs variables sur des axes partant d’un centre commun, les extrémités étant reliées pour former un polygone. Il est utile pour comparer des profils multivariés.

Hide code cell source

# Moyennes par espèce
categories = [c.replace(" (cm)", "") for c in df_iris.columns[:4]]
means = df_iris.groupby("target").mean().values

angles = np.linspace(0, 2 * np.pi, len(categories), endpoint=False).tolist()
angles += angles[:1]  # fermer le polygone

fig, ax = plt.subplots(figsize=(7, 7), subplot_kw=dict(polar=True))

colors = ["#4C72B0", "#55A868", "#C44E52"]
for i, name in enumerate(iris.target_names):
    values = means[i].tolist()
    values += values[:1]
    ax.plot(angles, values, "o-", label=name, color=colors[i], linewidth=2)
    ax.fill(angles, values, alpha=0.15, color=colors[i])

ax.set_thetagrids(np.degrees(angles[:-1]), categories)
ax.set_title("Radar chart — Profil moyen par espèce", pad=20)
ax.legend(loc="upper right", bbox_to_anchor=(1.3, 1.0))
plt.tight_layout()
plt.show()
_images/32a2e58e3b680fa2dcec526c845ca833f34e2d019235a23b81df273dd464ca88.png

Remarque 31

Les radar charts sont visuellement attractifs mais doivent être utilisés avec prudence : la surface du polygone dépend de l’ordre des variables, et la comparaison des aires peut être trompeuse. Ils conviennent bien pour comparer un petit nombre de profils (2 à 5) sur un petit nombre de variables (4 à 8).

Détection visuelle d’anomalies#

Les valeurs aberrantes (outliers) sont des observations qui s’écartent fortement du reste des données. Leur détection est cruciale car elles peuvent :

  • résulter d’erreurs de mesure ou de saisie (à corriger ou supprimer)

  • représenter des phénomènes rares mais réels (à conserver et étudier)

  • dégrader les performances de certains modèles sensibles aux valeurs extrêmes

Détection par boxplot et IQR#

Proposition 5 (Règle de l’IQR)

Une observation \(x_i\) est considérée comme un outlier si

\[x_i < Q_1 - 1.5 \cdot \text{IQR} \quad \text{ou} \quad x_i > Q_3 + 1.5 \cdot \text{IQR}\]

Elle est considérée comme un outlier extrême si le facteur \(1.5\) est remplacé par \(3\).

Hide code cell source

def detect_outliers_iqr(series, factor=1.5):
    """Détecte les outliers selon la règle de l'IQR."""
    Q1 = series.quantile(0.25)
    Q3 = series.quantile(0.75)
    IQR = Q3 - Q1
    lower = Q1 - factor * IQR
    upper = Q3 + factor * IQR
    return (series < lower) | (series > upper)

print("Nombre d'outliers (IQR) par variable — California Housing :\n")
for col in df_housing.columns:
    n_out = detect_outliers_iqr(df_housing[col]).sum()
    pct = 100 * n_out / len(df_housing)
    print(f"  {col:20s} : {n_out:5d} outliers ({pct:.1f} %)")
Nombre d'outliers (IQR) par variable — California Housing :

  MedInc               :   681 outliers (3.3 %)
  HouseAge             :     0 outliers (0.0 %)
  AveRooms             :   511 outliers (2.5 %)
  AveBedrms            :  1424 outliers (6.9 %)
  Population           :  1196 outliers (5.8 %)
  AveOccup             :   711 outliers (3.4 %)
  Latitude             :     0 outliers (0.0 %)
  Longitude            :     0 outliers (0.0 %)
  MedHouseVal          :  1071 outliers (5.2 %)

Détection par scatter plot#

Hide code cell source

fig, axes = plt.subplots(2, 1, figsize=(9, 9))

# Scatter plot avec outliers mis en évidence
col_x, col_y = "AveRooms", "MedHouseVal"
outliers = detect_outliers_iqr(df_housing[col_x])

axes[0].scatter(df_housing.loc[~outliers, col_x], df_housing.loc[~outliers, col_y],
                alpha=0.2, s=5, c="steelblue", label="Normal")
axes[0].scatter(df_housing.loc[outliers, col_x], df_housing.loc[outliers, col_y],
                alpha=0.6, s=15, c="red", label="Outlier (IQR)")
axes[0].set_xlabel("AveRooms")
axes[0].set_ylabel("MedHouseVal")
axes[0].set_title("Détection d'outliers par IQR — scatter plot")
axes[0].legend()

# Boxplots côte à côte pour plusieurs variables
cols_to_check = ["MedInc", "AveRooms", "AveBedrms", "Population"]
df_subset = df_housing[cols_to_check]
df_normalized = (df_subset - df_subset.median()) / df_subset.std()
sns.boxplot(data=df_normalized, orient="h", ax=axes[1], palette="Set2")
axes[1].set_title("Boxplots normalisés — détection visuelle d'outliers")
axes[1].set_xlabel("Valeur standardisée")

plt.tight_layout()
plt.show()
_images/1d55eea0a96420b322549d5aa72ad4fceb966355f6575082fe78c48c7a08946c.png

Détection par Z-score#

Définition 32 (Z-score)

Le Z-score d’une observation est le nombre d’écarts-types qui la séparent de la moyenne :

\[z_i = \frac{x_i - \bar{x}}{s}\]

Une règle empirique classique considère comme aberrante toute observation avec \(|z_i| > 3\).

Hide code cell source

from scipy.stats import zscore

fig, ax = plt.subplots(figsize=(10, 5))

z_scores = np.abs(zscore(df_housing["Population"]))
threshold = 3

ax.scatter(range(len(z_scores)), z_scores, alpha=0.3, s=3, c="steelblue")
ax.axhline(y=threshold, color="red", linestyle="--", label=f"Seuil |z| = {threshold}")
ax.set_xlabel("Index de l'observation")
ax.set_ylabel("|Z-score|")
ax.set_title("Z-scores — Population (California Housing)")
ax.legend()

n_outliers_z = (z_scores > threshold).sum()
print(f"Outliers (|z| > {threshold}) dans Population : {n_outliers_z} ({100*n_outliers_z/len(z_scores):.1f} %)")

plt.tight_layout()
plt.show()
Outliers (|z| > 3) dans Population : 342 (1.7 %)
_images/b0792d4f929cb2747327f1187f9fc7514c846b7234bf1de01b2c67bc90c43f7f.png

Remarque 32

Le Z-score suppose implicitement que les données suivent une distribution approximativement normale. Pour des distributions fortement asymétriques, on préfère le Z-score modifié basé sur la médiane et le MAD (Median Absolute Deviation) :

\[z_i^{\text{mod}} = \frac{0.6745 \cdot (x_i - \text{Med}))}{\text{MAD}}, \quad \text{MAD} = \text{Med}(|x_i - \text{Med}|)\]

Le facteur \(0.6745\) normalise le MAD pour qu’il coïncide avec l’écart-type dans le cas gaussien.

Synthèse visuelle#

Hide code cell source

fig, axes = plt.subplots(2, 2, figsize=(14, 10))

# 1. Histogramme avec zones d'outliers
col = "AveOccup"
Q1 = df_housing[col].quantile(0.25)
Q3 = df_housing[col].quantile(0.75)
IQR = Q3 - Q1
lower, upper = Q1 - 1.5 * IQR, Q3 + 1.5 * IQR

axes[0, 0].hist(df_housing[col], bins=100, edgecolor="white", alpha=0.8)
axes[0, 0].axvline(lower, color="red", linestyle="--", label=f"Borne inf ({lower:.1f})")
axes[0, 0].axvline(upper, color="red", linestyle="--", label=f"Borne sup ({upper:.1f})")
axes[0, 0].set_xlim(df_housing[col].quantile(0.001), df_housing[col].quantile(0.999))
axes[0, 0].set_title(f"Histogramme avec bornes IQR — {col}")
axes[0, 0].legend(fontsize=9)

# 2. Violin plot par espèce
sns.violinplot(data=df_iris, x="target", y="sepal width (cm)",
               inner="box", hue="target", palette="Set2", ax=axes[0, 1], legend=False)
axes[0, 1].set_title("Violin plot — sepal width par espèce")

# 3. KDE comparant distribution avec et sans outliers
col = "AveRooms"
mask_normal = ~detect_outliers_iqr(df_housing[col])
sns.kdeplot(df_housing[col], label="Toutes les données", ax=axes[1, 0], fill=True, alpha=0.3)
sns.kdeplot(df_housing.loc[mask_normal, col], label="Sans outliers", ax=axes[1, 0], fill=True, alpha=0.3)
axes[1, 0].set_title(f"Impact des outliers sur la KDE — {col}")
axes[1, 0].legend()

# 4. Scatter plot bivariée avec régression
sns.regplot(data=df_housing.sample(2000, random_state=42),
            x="MedInc", y="MedHouseVal", scatter_kws={"alpha": 0.3, "s": 10},
            line_kws={"color": "red"}, ax=axes[1, 1])
axes[1, 1].set_title("Régression linéaire — MedInc vs MedHouseVal")

plt.tight_layout()
plt.show()
_images/40fd488ab063f54f46e52d9e4227a7c154d8ce053c0c5eae2f19530aa273fd19.png

Résumé#

Remarque 33

L’analyse exploratoire des données est une discipline à part entière. Ce chapitre a présenté les outils fondamentaux, mais la pratique de l’EDA s’enrichit avec l’expérience. Voici quelques principes directeurs :

  1. Toujours commencer par describe() et info() pour avoir une vue d’ensemble rapide.

  2. Visualiser chaque variable individuellement (histogramme, KDE, boxplot) avant d’étudier les relations.

  3. Examiner les corrélations pour identifier les variables redondantes et les prédicteurs potentiels.

  4. Chercher les outliers et décider de leur traitement au cas par cas.

  5. Adapter la visualisation au message : un bon graphique raconte une histoire claire.

  6. Itérer : chaque observation soulève de nouvelles questions.

Les outils présentés ici — statistiques descriptives, tests de normalité, corrélations, et la gamme complète des visualisations univariées, bivariées et multivariées — constituent le socle sur lequel reposent toutes les étapes suivantes du pipeline d’apprentissage automatique : le prétraitement, la sélection de variables et la modélisation.