Statistiques descriptives#

Hide code cell source

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
import seaborn as sns
from scipy import stats

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)
rng = np.random.default_rng(42)

Les statistiques descriptives ont un objectif modeste en apparence — résumer un jeu de données en quelques chiffres ou graphiques — mais cet objectif est en réalité fondamental. Avant tout test, toute modélisation, toute inférence, il faut comprendre ses données : leur forme, leurs valeurs atypiques, leur dispersion, leur asymétrie. Un modèle appliqué à des données mal comprises produit des résultats mal compris. Ce chapitre couvre les outils essentiels pour explorer des données numériques, en partant d’un jeu de données concret et en progressant vers les diagnostics avancés.

Un jeu de données réaliste : salaires dans une entreprise#

Imaginons une entreprise de 500 employés. Les salaires annuels (en milliers d’euros) suivent une distribution réaliste : la grande majorité tourne autour de 35 à 60 k€, avec quelques cadres supérieurs et dirigeants qui tirent la queue droite vers le haut.

# Génération de salaires réalistes : mélange de deux populations
n_employes = 400
n_cadres = 80
n_dirigeants = 20

salaires_employes = rng.normal(loc=38, scale=8, size=n_employes)
salaires_cadres = rng.normal(loc=65, scale=12, size=n_cadres)
salaires_dirigeants = rng.normal(loc=130, scale=30, size=n_dirigeants)

salaires = np.concatenate([salaires_employes, salaires_cadres, salaires_dirigeants])
salaires = np.clip(salaires, 22, 350)  # salaires plausibles

departements = (
    ["Technique"] * 150 + ["Ventes"] * 120 + ["RH"] * 80 + ["Finance"] * 50 +
    ["Technique"] * 30 + ["Ventes"] * 20 + ["Finance"] * 20 + ["RH"] * 10 +
    ["Direction"] * 20
)

df = pd.DataFrame({
    "salaire": salaires,
    "departement": departements,
    "anciennete": rng.integers(1, 25, size=500),
    "age": np.clip(rng.normal(38, 9, 500).astype(int), 22, 62),
})

print(df.shape)
df.head()
(500, 4)
salaire departement anciennete age
0 40.437737 Technique 10 48
1 29.680127 Technique 9 28
2 44.003610 Technique 18 22
3 45.524518 Technique 4 38
4 22.391718 Technique 6 46

Tendance centrale : quelle est la valeur « typique » ?#

Trois mesures coexistent pour résumer le centre d’une distribution. Elles coïncident pour une distribution symétrique, mais divergent dès qu’apparaît de l’asymétrie ou des valeurs aberrantes.

La moyenne arithmétique \(\bar{x} = \frac{1}{n}\sum_{i=1}^n x_i\) est sensible à tous les points, y compris les extrêmes. Elle minimise la somme des carrés des écarts.

La médiane est la valeur qui partage l’échantillon en deux moitiés égales. Elle est robuste aux outliers : on peut multiplier le salaire du PDG par dix sans déplacer la médiane d’un centime.

Le mode est la valeur la plus fréquente. Utile pour des données catégorielles ou discrètes ; pour des données continues, on parle plutôt de pic de la densité estimée.

moyenne = df["salaire"].mean()
mediane = df["salaire"].median()
mode_val = float(stats.mode(df["salaire"].round(), keepdims=True).mode[0])

print(f"Moyenne  : {moyenne:.1f} k€")
print(f"Médiane  : {mediane:.1f} k€")
print(f"Mode (arrondi) : {mode_val:.0f} k€")
print(f"Écart moyenne-médiane : {moyenne - mediane:.1f} k€  → asymétrie droite")
Moyenne  : 45.8 k€
Médiane  : 40.2 k€
Mode (arrondi) : 35 k€
Écart moyenne-médiane : 5.6 k€  → asymétrie droite

Hide code cell source

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

# Histogramme avec KDE et indicateurs
ax = axes[0]
ax.hist(df["salaire"], bins=40, density=True, alpha=0.55, color="steelblue",
        label="Distribution observée")
kde_x = np.linspace(df["salaire"].min(), df["salaire"].max(), 300)
kde = stats.gaussian_kde(df["salaire"])
ax.plot(kde_x, kde(kde_x), color="steelblue", lw=2, label="Densité estimée (KDE)")
ax.axvline(moyenne, color="tomato", lw=2, linestyle="--", label=f"Moyenne = {moyenne:.0f}")
ax.axvline(mediane, color="seagreen", lw=2, linestyle="-.", label=f"Médiane = {mediane:.0f}")
ax.set_xlabel("Salaire (k€)")
ax.set_ylabel("Densité")
ax.set_title("Distribution des salaires — tendance centrale")
ax.legend(fontsize=9)

# Impact des outliers sur la moyenne
ax2 = axes[1]
n_sim = 200
moyennes_sans = []
moyennes_avec = []
for _ in range(n_sim):
    echantillon = rng.choice(salaires_employes, size=50, replace=False)
    moyennes_sans.append(echantillon.mean())
    avec_outlier = np.append(echantillon, 350)  # un dirigeant exceptionnel
    moyennes_avec.append(avec_outlier.mean())

ax2.hist(moyennes_sans, bins=25, alpha=0.6, color="seagreen", density=True,
         label="Sans outlier")
ax2.hist(moyennes_avec, bins=25, alpha=0.6, color="tomato", density=True,
         label="Avec outlier à 350 k€")
ax2.axvline(np.median(salaires_employes), color="gray", lw=2, linestyle=":",
            label=f"Médiane stable ≈ {np.median(salaires_employes):.0f}")
ax2.set_xlabel("Moyenne estimée (k€)")
ax2.set_ylabel("Densité")
ax2.set_title("Robustesse : médiane vs moyenne face aux outliers")
ax2.legend(fontsize=9)

plt.tight_layout()
plt.show()
_images/9677e07f878c13be4de10cb118b30da7327f83bdf895c25674a60fdca6a65d95.png

La figure de droite illustre un fait fondamental : la moyenne fluctue énormément en présence d’un seul outlier, alors que la médiane reste stable. Pour des données salariales, il est donc préférable de rapporter la médiane comme mesure de tendance centrale.

Règle pratique

Choisissez la médiane dès que la distribution est asymétrique ou contient des valeurs aberrantes probables : revenus, prix immobiliers, durées de vie, tailles de fichiers. Réservez la moyenne aux distributions approximativement symétriques et bornées.

Dispersion : à quel point les valeurs s’étalent-elles ?#

La dispersion mesure la variabilité. Connaître la tendance centrale sans la dispersion ne suffit pas : deux entreprises peuvent avoir la même masse salariale médiane avec des inégalités très différentes.

Variance et écart-type. La variance \(s^2 = \frac{1}{n-1}\sum (x_i - \bar{x})^2\) est la moyenne des carrés des écarts à la moyenne (divisée par \(n-1\) pour l’estimateur sans biais). L’écart-type \(s\) est sa racine carrée, dans la même unité que les données.

IQR (interquartile range). L’IQR = Q3 − Q1 est la longueur de la boîte du boxplot. Il mesure l’étendue des 50 % centraux et est insensible aux valeurs extrêmes.

MAD (median absolute deviation). \(\text{MAD} = \text{médiane}(|x_i - \text{médiane}(x)|)\). Encore plus robuste que l’IQR, c’est l’estimateur de dispersion de référence pour les données contaminées. On le normalise parfois par 1,4826 pour l’aligner sur l’écart-type d’une gaussienne.

s = df["salaire"]

variance = s.var()          # ddof=1 par défaut dans pandas
ecart_type = s.std()
iqr = s.quantile(0.75) - s.quantile(0.25)
mad = stats.median_abs_deviation(s)
mad_normalise = stats.median_abs_deviation(s, scale="normal")  # ≈ σ pour gaussienne

print(f"Variance          : {variance:.1f} k€²")
print(f"Écart-type        : {ecart_type:.1f} k€")
print(f"IQR               : {iqr:.1f} k€")
print(f"MAD               : {mad:.1f} k€")
print(f"MAD normalisé     : {mad_normalise:.1f} k€  (comparable à l'écart-type)")
print(f"\nRatio σ/MAD_norm  : {ecart_type/mad_normalise:.2f}  (> 1 → queue droite lourde)")
Variance          : 470.2 k€²
Écart-type        : 21.7 k€
IQR               : 14.4 k€
MAD               : 6.9 k€
MAD normalisé     : 10.3 k€  (comparable à l'écart-type)

Ratio σ/MAD_norm  : 2.12  (> 1 → queue droite lourde)

Le ratio écart-type / MAD normalisé supérieur à 1 indique que l’écart-type est gonflé par les valeurs extrêmes. C’est une signature d’asymétrie ou de queues lourdes.

Forme de la distribution : asymétrie et aplatissement#

Deux distributions peuvent avoir la même moyenne et le même écart-type tout en différant radicalement de forme. Le skewness (asymétrie) et le kurtosis (aplatissement) capturent ces différences.

Skewness. Le coefficient d’asymétrie \(\gamma_1 = \frac{\mu_3}{\sigma^3}\)\(\mu_3 = E[(X-\mu)^3]\) est le moment centré d’ordre 3. Un skewness positif signifie une queue étalée à droite (revenus, temps d’attente) ; négatif, à gauche (âge au décès dans un pays développé).

Kurtosis. Le coefficient d’aplatissement \(\gamma_2 = \frac{\mu_4}{\sigma^4} - 3\) (excès de kurtosis, nul pour la gaussienne). Un kurtosis positif (leptokurtique) indique des queues plus épaisses et un pic plus prononcé que la normale. Attention : scipy.stats.kurtosis retourne l’excès de kurtosis par défaut.

skewness = stats.skew(df["salaire"])
kurt = stats.kurtosis(df["salaire"])  # excès de kurtosis

print(f"Skewness  : {skewness:.3f}")
print(f"Kurtosis (excès) : {kurt:.3f}")
print()
print("Interprétation :")
print(f"  → Skewness {skewness:.2f} > 0 : queue droite étalée (salaires élevés rares mais présents)")
print(f"  → Kurtosis {kurt:.2f} > 0 : queues plus lourdes qu'une gaussienne")
Skewness  : 2.895
Kurtosis (excès) : 10.444

Interprétation :
  → Skewness 2.89 > 0 : queue droite étalée (salaires élevés rares mais présents)
  → Kurtosis 10.44 > 0 : queues plus lourdes qu'une gaussienne

Hide code cell source

fig, axes = plt.subplots(1, 3, figsize=(15, 4))

# Comparaison de skewness
ax = axes[0]
x = np.linspace(-4, 8, 300)
for skew_param, label, color in [
    (0, "Symétrique (skew=0)", "steelblue"),
    (4, "Asymétrie droite (skew>0)", "tomato"),
    (-4, "Asymétrie gauche (skew<0)", "seagreen"),
]:
    a_param = skew_param
    dist = stats.skewnorm(a=a_param, loc=2, scale=1.2)
    ax.plot(x, dist.pdf(x), lw=2, label=label, color=color)
ax.set_title("Effets du skewness")
ax.set_xlabel("Valeur")
ax.set_ylabel("Densité")
ax.legend(fontsize=8)

# Comparaison de kurtosis
ax2 = axes[1]
x2 = np.linspace(-6, 6, 300)
for df_t, label, color in [
    (2.1, "Queues lourdes (t, ν=2)", "tomato"),
    (10, "Queues légères (t, ν=10)", "steelblue"),
    (None, "Gaussienne (kurtosis=0)", "seagreen"),
]:
    if df_t is None:
        ax2.plot(x2, stats.norm.pdf(x2), lw=2, label=label, color=color)
    else:
        ax2.plot(x2, stats.t.pdf(x2, df=df_t), lw=2, label=label, color=color)
ax2.set_title("Effets du kurtosis (excès)")
ax2.set_xlabel("Valeur")
ax2.legend(fontsize=8)

# Salaires : skewness et kurtosis réels
ax3 = axes[2]
ax3.hist(df["salaire"], bins=35, density=True, alpha=0.5, color="steelblue")
kde_x = np.linspace(df["salaire"].min(), df["salaire"].max(), 300)
ax3.plot(kde_x, stats.gaussian_kde(df["salaire"])(kde_x), color="steelblue", lw=2)
# Gaussienne de référence
x_ref = np.linspace(10, 200, 300)
ax3.plot(x_ref,
         stats.norm.pdf(x_ref, df["salaire"].mean(), df["salaire"].std()),
         "r--", lw=1.5, label="Gaussienne de référence")
ax3.set_xlabel("Salaire (k€)")
ax3.set_title(f"Salaires : skew={skewness:.2f}, kurt={kurt:.2f}")
ax3.legend(fontsize=8)

plt.tight_layout()
plt.show()
_images/0532456631262aa86631af695904500b4f469ae149592e541915d339b961422c.png

Quantiles, percentiles et boxplot#

Les quantiles découpent la distribution en tranches d’égale probabilité. Le quantile d’ordre \(p\) est la valeur \(Q_p\) telle que \(P(X \leq Q_p) = p\). Les quartiles (\(Q_{0.25}\), \(Q_{0.5}\), \(Q_{0.75}\)) sont les plus utilisés.

Le boxplot (boîte à moustaches) résume une distribution en cinq chiffres : minimum, Q1, médiane, Q3, maximum — avec un traitement visuel des outliers. La moustache s’étend jusqu’à \(\text{Q1} - 1{,}5 \times \text{IQR}\) (en bas) et \(\text{Q3} + 1{,}5 \times \text{IQR}\) (en haut). Les points au-delà sont tracés individuellement.

quantiles = df["salaire"].quantile([0.1, 0.25, 0.5, 0.75, 0.9, 0.95, 0.99])
print("Quantiles des salaires :")
for q, v in quantiles.items():
    print(f"  P{int(q*100):2d} : {v:.1f} k€")
Quantiles des salaires :
  P10 : 29.1 k€
  P25 : 34.4 k€
  P50 : 40.2 k€
  P75 : 48.7 k€
  P90 : 68.8 k€
  P95 : 80.9 k€
  P99 : 143.7 k€

Hide code cell source

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

# Boxplot comparatif par département
ax = axes[0]
order = df.groupby("departement")["salaire"].median().sort_values(ascending=False).index
sns.boxplot(data=df, y="departement", x="salaire", order=order, hue="departement", palette="muted", legend=False, ax=ax)
ax.set_xlabel("Salaire (k€)")
ax.set_ylabel("")
ax.set_title("Distribution des salaires par département")
# Annoter IQR
for i, dept in enumerate(order):
    sub = df[df["departement"] == dept]["salaire"]
    q1, q3 = sub.quantile(0.25), sub.quantile(0.75)
    ax.annotate(f"IQR={q3-q1:.0f}", xy=(q3, i), xytext=(q3+5, i),
                fontsize=7.5, va="center", color="gray")

# Violin plot : densité complète + boxplot intégré
ax2 = axes[1]
sns.violinplot(data=df, y="departement", x="salaire", order=order,
               inner="box", hue="departement", palette="pastel", legend=False, ax=ax2)
ax2.set_xlabel("Salaire (k€)")
ax2.set_ylabel("")
ax2.set_title("Violin plot : densité + quartiles")

plt.tight_layout()
plt.show()
_images/81716cfcad004cfbbafdec75acb5977105144729697244266a4d30bb9de08353.png

Boxplot vs violin plot

Le boxplot est lisible et compact : il fonctionne bien pour comparer beaucoup de groupes. Le violin plot montre la forme de la distribution (multimodalité, asymétrie) mais est plus difficile à lire avec de nombreux groupes. Pour une distribution bimodale, le boxplot n’en révèle rien là où le violin plot la révèle immédiatement.

Détection d’outliers#

Un outlier n’est pas forcément une erreur de mesure — c’est une valeur qui se distingue du comportement typique. Sa détection dépend du contexte et de la méthode choisie.

Règle de l’IQR (Tukey)#

Un point \(x\) est déclaré outlier si \(x < Q_1 - 1{,}5 \times \text{IQR}\) ou \(x > Q_3 + 1{,}5 \times \text{IQR}\). Simple et non paramétrique, cette règle est celle utilisée par défaut dans les boxplots.

Z-score classique#

\(z = \frac{x - \bar{x}}{s}\). Convention : \(|z| > 3\) signale un outlier. Cette méthode suppose une distribution approximativement gaussienne et est sensible aux outliers eux-mêmes (le masquage).

Z-score modifié (Iglewicz & Hoaglin)#

\(z_{\text{mod}} = \frac{0{,}6745 \times (x - \text{médiane})}{\text{MAD}}\). Le seuil habituel est \(|z_{\text{mod}}| > 3{,}5\). Beaucoup plus robuste : ni la médiane ni la MAD ne sont affectées par les outliers.

def detecter_outliers(x):
    """Trois méthodes de détection d'outliers."""
    x = np.asarray(x)

    # IQR
    q1, q3 = np.percentile(x, 25), np.percentile(x, 75)
    iqr_val = q3 - q1
    outliers_iqr = (x < q1 - 1.5 * iqr_val) | (x > q3 + 1.5 * iqr_val)

    # Z-score classique
    z = np.abs(stats.zscore(x))
    outliers_z = z > 3

    # Z-score modifié
    med = np.median(x)
    mad_val = stats.median_abs_deviation(x)
    z_mod = np.abs(0.6745 * (x - med) / mad_val) if mad_val > 0 else np.zeros_like(x)
    outliers_zmod = z_mod > 3.5

    return outliers_iqr, outliers_z, outliers_zmod

oi, oz, ozm = detecter_outliers(df["salaire"])

print(f"Méthode IQR          : {oi.sum()} outliers ({oi.mean()*100:.1f}%)")
print(f"Z-score classique    : {oz.sum()} outliers ({oz.mean()*100:.1f}%)")
print(f"Z-score modifié (MAD): {ozm.sum()} outliers ({ozm.mean()*100:.1f}%)")
print()
print("Outliers IQR (salaires en k€) :")
print(np.sort(df["salaire"][oi].values)[:10], "...")
Méthode IQR          : 41 outliers (8.2%)
Z-score classique    : 15 outliers (3.0%)
Z-score modifié (MAD): 33 outliers (6.6%)

Outliers IQR (salaires en k€) :
[70.69636506 71.25400707 72.4300534  73.9932068  74.64302023 74.84634171
 74.9542285  75.05781747 76.1395639  77.76621839] ...

Hide code cell source

fig, axes = plt.subplots(1, 3, figsize=(15, 4))
methodes = [("IQR", oi), ("Z-score", oz), ("Z-score modifié", ozm)]

for ax, (nom, masque) in zip(axes, methodes):
    ax.scatter(range(len(df)), np.sort(df["salaire"]),
               c=["tomato" if m else "steelblue" for m in masque[np.argsort(df["salaire"])]],
               alpha=0.5, s=15)
    ax.set_title(f"{nom}{masque.sum()} outliers")
    ax.set_xlabel("Rang")
    ax.set_ylabel("Salaire (k€)")

plt.suptitle("Comparaison des méthodes de détection d'outliers", y=1.02)
plt.tight_layout()
plt.show()
_images/a58ddd09d309f5b37116f15edec3db885395558a7fdc9d5543718314c63b3d08.png

Isolation Forest et méthodes non paramétriques

Pour des données multivariées, les méthodes univariées sont insuffisantes : un point peut être normal sur chaque dimension séparément mais aberrant par leur combinaison. Isolation Forest (sklearn.ensemble.IsolationForest) isole les outliers en les coupant aléatoirement dans l’espace des features ; ils nécessitent moins de coupes que les points normaux. Local Outlier Factor (LOF) compare la densité locale d’un point à celle de ses voisins. Ces méthodes seront couvertes dans le livre Data Science.

Résumé complet avec describe() enrichi#

La méthode describe() de pandas donne un résumé rapide, mais elle peut être enrichie pour inclure skewness, kurtosis et MAD.

def describe_enrichi(series, nom="variable"):
    """Résumé statistique enrichi."""
    s = series.dropna()
    q = s.quantile
    mad_v = stats.median_abs_deviation(s)

    resume = {
        "n": len(s),
        "n_manquants": series.isna().sum(),
        "min": s.min(),
        "P5": q(0.05),
        "Q1 (P25)": q(0.25),
        "médiane": q(0.50),
        "Q3 (P75)": q(0.75),
        "P95": q(0.95),
        "max": s.max(),
        "moyenne": s.mean(),
        "écart-type": s.std(),
        "IQR": q(0.75) - q(0.25),
        "MAD": mad_v,
        "skewness": stats.skew(s),
        "kurtosis (excès)": stats.kurtosis(s),
    }
    return pd.Series(resume, name=nom)

desc = describe_enrichi(df["salaire"], "salaire (k€)")
print(desc.to_string())
n                   500.000000
n_manquants           0.000000
min                  22.000000
P5                   26.419083
Q1 (P25)             34.354700
médiane              40.186470
Q3 (P75)             48.742764
P95                  80.852155
max                 172.996435
moyenne              45.817865
écart-type           21.683401
IQR                  14.388064
MAD                   6.913711
skewness              2.894855
kurtosis (excès)     10.444140

Statistiques groupées#

L’analyse par groupe est une des opérations les plus fréquentes en data analyse. groupby de pandas permet de calculer n’importe quelle statistique par sous-groupe.

# Statistiques par département
stats_groupe = df.groupby("departement")["salaire"].agg([
    ("n", "count"),
    ("médiane", "median"),
    ("moyenne", "mean"),
    ("écart-type", "std"),
    ("IQR", lambda x: x.quantile(0.75) - x.quantile(0.25)),
    ("skewness", stats.skew),
    ("P90", lambda x: x.quantile(0.90)),
]).round(1)

print(stats_groupe.sort_values("médiane", ascending=False).to_string())
               n  médiane  moyenne  écart-type   IQR  skewness    P90
departement                                                          
Direction     20    130.5    127.2        28.0  28.0      -0.5  158.5
Finance       70     42.2     46.3        15.6  16.1       1.0   68.8
RH            90     40.5     41.9        12.7  13.0       1.3   55.3
Technique    180     39.8     41.9        12.4  10.6       1.4   61.3
Ventes       140     38.6     41.5        12.4  14.2       1.1   58.8

Hide code cell source

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

# Médiane et moyenne par département
ax = axes[0, 0]
x_pos = np.arange(len(stats_groupe))
dept_order = stats_groupe.sort_values("médiane", ascending=True).index
med_vals = stats_groupe.loc[dept_order, "médiane"]
moy_vals = stats_groupe.loc[dept_order, "moyenne"]
ax.barh(x_pos, med_vals.values, alpha=0.7, color="seagreen", label="Médiane")
ax.barh(x_pos, moy_vals.values, alpha=0.5, color="tomato", label="Moyenne")
ax.set_yticks(x_pos)
ax.set_yticklabels(dept_order)
ax.set_xlabel("Salaire (k€)")
ax.set_title("Médiane vs Moyenne par département")
ax.legend()

# Boxplot par département avec swarmplot
ax2 = axes[0, 1]
dept_order2 = df.groupby("departement")["salaire"].median().sort_values(ascending=False).index
sns.boxplot(data=df, x="departement", y="salaire", order=dept_order2,
            hue="departement", palette="Set2", legend=False, width=0.5, ax=ax2)
sns.stripplot(data=df, x="departement", y="salaire", order=dept_order2,
              color="black", alpha=0.2, size=3, jitter=True, ax=ax2)
ax2.tick_params(axis="x", rotation=20)
ax2.set_xlabel("")
ax2.set_ylabel("Salaire (k€)")
ax2.set_title("Distribution par département")

# Salaire vs ancienneté (corrélation brute)
ax3 = axes[1, 0]
scatter = ax3.scatter(df["anciennete"], df["salaire"],
                      c=df["age"], cmap="YlOrRd", alpha=0.5, s=20)
plt.colorbar(scatter, ax=ax3, label="Âge")
ax3.set_xlabel("Ancienneté (années)")
ax3.set_ylabel("Salaire (k€)")
ax3.set_title("Salaire vs ancienneté (couleur = âge)")
r, p = stats.pearsonr(df["anciennete"], df["salaire"])
ax3.annotate(f"r = {r:.2f} (p={p:.3f})", xy=(0.05, 0.92), xycoords="axes fraction",
             fontsize=9)

# Distribution cumulée (ECDF)
ax4 = axes[1, 1]
for dept in dept_order2[:4]:
    sub = df[df["departement"] == dept]["salaire"].sort_values()
    ecdf = np.arange(1, len(sub) + 1) / len(sub)
    ax4.step(sub, ecdf, where="post", lw=1.8, label=dept)
ax4.set_xlabel("Salaire (k€)")
ax4.set_ylabel("Fréquence cumulée")
ax4.set_title("ECDF par département")
ax4.legend(fontsize=9)
ax4.axhline(0.5, color="gray", lw=1, linestyle=":")
ax4.axhline(0.25, color="gray", lw=1, linestyle=":")
ax4.axhline(0.75, color="gray", lw=1, linestyle=":")

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

Tableau récapitulatif des mesures#

Hide code cell source

# Tableau synthétique des mesures et de leurs propriétés
synthese = pd.DataFrame({
    "Mesure": [
        "Moyenne", "Médiane", "Mode",
        "Variance", "Écart-type", "IQR", "MAD",
        "Skewness", "Kurtosis (excès)"
    ],
    "Catégorie": [
        "Tendance centrale", "Tendance centrale", "Tendance centrale",
        "Dispersion", "Dispersion", "Dispersion", "Dispersion",
        "Forme", "Forme"
    ],
    "Robustesse": [
        "Faible", "Forte", "Forte",
        "Faible", "Faible", "Forte", "Très forte",
        "Faible", "Faible"
    ],
    "Quand l'utiliser": [
        "Distribution symétrique, sans outliers extrêmes",
        "Distribution asymétrique, revenus, prix",
        "Variables discrètes, catégorielles",
        "Calculs théoriques, ANOVA",
        "Distribution ≈ normale",
        "Boxplots, détection d'outliers Tukey",
        "Données contaminées, outliers probables",
        "Diagnostic d'asymétrie",
        "Diagnostic de queues lourdes"
    ]
}).set_index("Mesure")

print(synthese.to_string())
                          Catégorie  Robustesse                                 Quand l'utiliser
Mesure                                                                                          
Moyenne           Tendance centrale      Faible  Distribution symétrique, sans outliers extrêmes
Médiane           Tendance centrale       Forte          Distribution asymétrique, revenus, prix
Mode              Tendance centrale       Forte               Variables discrètes, catégorielles
Variance                 Dispersion      Faible                        Calculs théoriques, ANOVA
Écart-type               Dispersion      Faible                           Distribution ≈ normale
IQR                      Dispersion       Forte             Boxplots, détection d'outliers Tukey
MAD                      Dispersion  Très forte          Données contaminées, outliers probables
Skewness                      Forme      Faible                           Diagnostic d'asymétrie
Kurtosis (excès)              Forme      Faible                     Diagnostic de queues lourdes

Le choix de la bonne mesure statistique n’est pas anodin. Rapporter la moyenne des salaires d’une entreprise peut être politiquement trompeur si quelques dirigeants très bien payés la tirent vers le haut — la médiane révèle alors une réalité plus fidèle. Comprendre ces nuances, c’est déjà une grande partie du travail de statisticien.