---
jupytext:
  text_representation:
    extension: .md
    format_name: myst
    format_version: 0.13
    jupytext_version: 1.16.0
kernelspec:
  name: python3
  display_name: Python 3
---

# Corrélation et dépendance

```{code-cell} python
:tags: [hide-input]

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
from scipy.cluster import hierarchy
try:
    import pingouin as pg
    HAS_PINGOUIN = True
except ImportError:
    HAS_PINGOUIN = False
    print("pingouin non disponible — certains calculs utiliseront scipy")

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

La corrélation est l'une des notions les plus utilisées — et les plus mal comprises — de la statistique appliquée. Ce chapitre couvre les trois grandes mesures de corrélation, leurs hypothèses, leurs limites, et surtout les **pièges** dans lesquels tombent même les analystes expérimentés.

## Corrélation de Pearson

Le **coefficient de corrélation de Pearson** $r$ mesure la force et la direction d'une relation **linéaire** entre deux variables :

$$r = \frac{\sum_{i=1}^n (x_i - \bar{x})(y_i - \bar{y})}{\sqrt{\sum (x_i - \bar{x})^2 \sum (y_i - \bar{y})^2}} \in [-1, 1]$$

$r = 1$ : relation linéaire positive parfaite. $r = -1$ : relation linéaire négative parfaite. $r = 0$ : pas de relation **linéaire** (mais peut exister une relation non linéaire).

**Hypothèses pour l'inférence :** pour que le test de significativité ($H_0 : \rho = 0$) soit valide, les deux variables doivent être environ normalement distribuées (ou $n$ doit être suffisamment grand par le TCL). La corrélation elle-même peut être calculée sans hypothèse de normalité.

```{code-cell} python
# Jeu de données réaliste : caractéristiques de logements
n = 300
surface = rng.uniform(20, 150, n)
prix_base = 1500 * surface + rng.normal(0, 15000, n)  # prix lié à la surface
nb_pieces = np.round(surface / 25 + rng.normal(0, 0.5, n)).clip(1, 8).astype(int)
etage = rng.integers(0, 15, n)
annee = rng.integers(1950, 2020, n)

df = pd.DataFrame({
    "prix": prix_base + 500 * etage + rng.normal(0, 10000, n),
    "surface": surface,
    "nb_pieces": nb_pieces,
    "etage": etage,
    "annee_construction": annee,
    "distance_centre": rng.exponential(5, n),
})

r_pearson, p_pearson = stats.pearsonr(df["surface"], df["prix"])
print(f"Corrélation de Pearson (surface, prix) : r = {r_pearson:.4f}, p = {p_pearson:.2e}")
```

### Intervalle de confiance par bootstrap

La distribution asymptotique de $r$ est délicate (la transformation de Fisher $z = \text{arctanh}(r)$ la normalise). Le **bootstrap** offre une alternative non paramétrique robuste.

```{code-cell} python
def ic_bootstrap_pearson(x, y, n_boot=2000, alpha=0.05):
    """IC bootstrap percentile pour le coefficient de Pearson."""
    n = len(x)
    boots = []
    for _ in range(n_boot):
        idx = rng.integers(0, n, size=n)
        r_b, _ = stats.pearsonr(x[idx], y[idx])
        boots.append(r_b)
    boots = np.array(boots)
    ic_low = np.percentile(boots, 100 * alpha / 2)
    ic_high = np.percentile(boots, 100 * (1 - alpha / 2))
    return ic_low, ic_high, boots

# IC analytique via transformation de Fisher
def ic_fisher_pearson(r, n, alpha=0.05):
    """IC à 95% via transformation de Fisher."""
    z = np.arctanh(r)
    se = 1 / np.sqrt(n - 3)
    z_crit = stats.norm.ppf(1 - alpha / 2)
    z_low = z - z_crit * se
    z_high = z + z_crit * se
    return np.tanh(z_low), np.tanh(z_high)

r = r_pearson
n_obs = len(df)
ic_fish_low, ic_fish_high = ic_fisher_pearson(r, n_obs)
ic_boot_low, ic_boot_high, boots = ic_bootstrap_pearson(
    df["surface"].values, df["prix"].values
)

print(f"r de Pearson       : {r:.4f}")
print(f"IC Fisher (95%)    : [{ic_fish_low:.4f}, {ic_fish_high:.4f}]")
print(f"IC Bootstrap (95%) : [{ic_boot_low:.4f}, {ic_boot_high:.4f}]")
```

```{code-cell} python
:tags: [hide-input]

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

# Scatter avec droite de régression
ax = axes[0]
ax.scatter(df["surface"], df["prix"] / 1000, alpha=0.4, s=20, color="steelblue")
m, b, r_val, p_val, _ = stats.linregress(df["surface"], df["prix"] / 1000)
x_line = np.linspace(df["surface"].min(), df["surface"].max(), 100)
ax.plot(x_line, m * x_line + b, "r-", lw=2, label=f"y = {m:.0f}x + {b:.0f}")
ax.set_xlabel("Surface (m²)")
ax.set_ylabel("Prix (k€)")
ax.set_title(f"Surface vs Prix — r = {r_val:.3f} (p={p_val:.1e})")
ax.legend()

# Distribution bootstrap de r
ax2 = axes[1]
ax2.hist(boots, bins=40, density=True, alpha=0.55, color="steelblue")
ax2.axvline(r, color="tomato", lw=2, label=f"r observé = {r:.3f}")
ax2.axvline(ic_boot_low, color="seagreen", lw=2, linestyle="--", label=f"IC [{ic_boot_low:.3f}, {ic_boot_high:.3f}]")
ax2.axvline(ic_boot_high, color="seagreen", lw=2, linestyle="--")
# Distribution théorique de Fisher
z_boots = np.arctanh(boots)
z_r = np.arctanh(r)
se = 1 / np.sqrt(n_obs - 3)
x_z = np.linspace(z_r - 4 * se, z_r + 4 * se, 200)
r_z = np.tanh(x_z)
ax2.plot(r_z, stats.norm.pdf(x_z, z_r, se) / (1 - r_z**2) * 0.5,
         "k--", lw=1.5, label="Théorique (Fisher)")
ax2.set_xlabel("r de Pearson")
ax2.set_ylabel("Densité")
ax2.set_title("Distribution bootstrap de r (n=2000 rééchantillons)")
ax2.legend(fontsize=8)

plt.tight_layout()
plt.show()
```

## Corrélation de Spearman

Le **coefficient de Spearman** $\rho$ est la corrélation de Pearson appliquée aux **rangs** des observations. Il mesure une relation **monotone** (croissante ou décroissante) sans supposer la linéarité.

$$\rho = 1 - \frac{6 \sum d_i^2}{n(n^2-1)}$$

où $d_i$ est la différence des rangs de $x_i$ et $y_i$.

**Avantages :** insensible aux transformations monotones ($\log$, racine carrée…), robuste aux outliers, adapté aux données ordinales.

```{code-cell} python
# Illustration : Spearman robuste aux outliers, Pearson non
x_base = rng.uniform(1, 10, 50)
y_base = 2 * x_base + rng.normal(0, 1, 50)

# Ajout d'un outlier extrême
x_avec = np.append(x_base, 50)
y_avec = np.append(y_base, 5)  # outlier incohérent

r_p_sans, _ = stats.pearsonr(x_base, y_base)
r_s_sans, _ = stats.spearmanr(x_base, y_base)
r_p_avec, _ = stats.pearsonr(x_avec, y_avec)
r_s_avec, _ = stats.spearmanr(x_avec, y_avec)

print("              Pearson    Spearman")
print(f"Sans outlier :  {r_p_sans:.3f}      {r_s_sans:.3f}")
print(f"Avec outlier :  {r_p_avec:.3f}      {r_s_avec:.3f}")
print(f"Variation    :  {abs(r_p_avec-r_p_sans):.3f}      {abs(r_s_avec-r_s_sans):.3f}")
```

## Corrélation de Kendall

Le **tau de Kendall** $\tau$ mesure la concordance entre les paires d'observations. Une paire $(x_i, x_j)$ est **concordante** si $x_i < x_j$ et $y_i < y_j$ (ou les deux plus grands), **discordante** sinon.

$$\tau = \frac{\text{concordantes} - \text{discordantes}}{\binom{n}{2}}$$

**Avantages :** plus robuste que Spearman pour les petits échantillons, interprétable directement comme différence de probabilités ($P(\text{concordante}) - P(\text{discordante})$), s'étend naturellement aux données avec ex-aequo.

```{code-cell} python
# Comparaison des trois corrélations sur notre jeu de données logements
print("=== Corrélations paires (logements) ===\n")
variables = ["prix", "surface", "nb_pieces", "etage", "annee_construction"]

for v1, v2 in [("surface", "prix"), ("nb_pieces", "prix"), ("etage", "prix"),
                ("annee_construction", "prix"), ("surface", "nb_pieces")]:
    r_p, p_p = stats.pearsonr(df[v1], df[v2])
    r_s, p_s = stats.spearmanr(df[v1], df[v2])
    r_k, p_k = stats.kendalltau(df[v1], df[v2])
    print(f"{v1:25s} × {v2:20s}")
    print(f"  Pearson : r={r_p:.3f} (p={p_p:.3f})")
    print(f"  Spearman: ρ={r_s:.3f} (p={p_s:.3f})")
    print(f"  Kendall : τ={r_k:.3f} (p={p_k:.3f})")
    print()
```

## Matrice de corrélation et visualisation

```{code-cell} python
# Corrélations complètes avec significativité
def matrice_corr_avec_pvaleurs(df_vars, methode="pearson"):
    """Retourne la matrice de corrélation et les p-valeurs."""
    cols = df_vars.columns
    n = len(cols)
    corr = pd.DataFrame(index=cols, columns=cols, dtype=float)
    pvals = pd.DataFrame(index=cols, columns=cols, dtype=float)

    for i, c1 in enumerate(cols):
        for j, c2 in enumerate(cols):
            if i == j:
                corr.loc[c1, c2] = 1.0
                pvals.loc[c1, c2] = 0.0
            else:
                if methode == "pearson":
                    r, p = stats.pearsonr(df_vars[c1], df_vars[c2])
                elif methode == "spearman":
                    r, p = stats.spearmanr(df_vars[c1], df_vars[c2])
                corr.loc[c1, c2] = r
                pvals.loc[c1, c2] = p

    return corr.astype(float), pvals.astype(float)

df_vars = df[variables]
corr_mat, pval_mat = matrice_corr_avec_pvaleurs(df_vars)
print("Matrice de corrélation de Pearson :")
print(corr_mat.round(3).to_string())
```

```{code-cell} python
:tags: [hide-input]

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

# Heatmap annotée avec significativité
ax = axes[0]
mask = np.triu(np.ones_like(corr_mat, dtype=bool), k=1)  # triangle inférieur
annot_mat = corr_mat.copy().astype(str)
for i in range(len(variables)):
    for j in range(len(variables)):
        r = corr_mat.iloc[i, j]
        p = pval_mat.iloc[i, j]
        stars = "***" if p < 0.001 else ("**" if p < 0.01 else ("*" if p < 0.05 else ""))
        annot_mat.iloc[i, j] = f"{r:.2f}{stars}"

sns.heatmap(corr_mat, annot=annot_mat, fmt="s", cmap="RdBu_r", center=0,
            vmin=-1, vmax=1, ax=ax, linewidths=0.5, square=True,
            cbar_kws={"shrink": 0.8})
ax.set_title("Matrice de corrélation\n(* p<0.05, ** p<0.01, *** p<0.001)")
ax.tick_params(axis="x", rotation=30)

# Clustering hiérarchique sur les corrélations
ax2 = axes[1]
dist_mat = 1 - np.abs(corr_mat.values)
np.fill_diagonal(dist_mat, 0)
linkage = hierarchy.linkage(hierarchy.distance.squareform(dist_mat), method="average")
order = hierarchy.leaves_list(linkage)
corr_ordered = corr_mat.iloc[order, :].iloc[:, order]
cols_ordered = [variables[i] for i in order]

sns.heatmap(corr_ordered, annot=True, fmt=".2f", cmap="RdBu_r", center=0,
            vmin=-1, vmax=1, ax=ax2, linewidths=0.5, square=True,
            cbar_kws={"shrink": 0.8},
            xticklabels=cols_ordered, yticklabels=cols_ordered)
ax2.set_title("Corrélations regroupées\n(clustering hiérarchique)")
ax2.tick_params(axis="x", rotation=30)

plt.tight_layout()
plt.show()
```

## Pièges classiques

### Le quartet d'Anscombe

En 1973, Francis Anscombe construisit quatre jeux de données ayant exactement les mêmes statistiques descriptives (moyenne, variance, corrélation) mais des formes radicalement différentes. La leçon : **toujours tracer les données**.

```{code-cell} python
# Quartet d'Anscombe (données exactes de l'article original)
anscombe = {
    "I":   {"x": [10,8,13,9,11,14,6,4,12,7,5],
             "y": [8.04,6.95,7.58,8.81,8.33,9.96,7.24,4.26,10.84,4.82,5.68]},
    "II":  {"x": [10,8,13,9,11,14,6,4,12,7,5],
             "y": [9.14,8.14,8.74,8.77,9.26,8.10,6.13,3.10,9.13,7.26,4.74]},
    "III": {"x": [10,8,13,9,11,14,6,4,12,7,5],
             "y": [7.46,6.77,12.74,7.11,7.81,8.84,6.08,5.39,8.15,6.42,5.73]},
    "IV":  {"x": [8,8,8,8,8,8,8,19,8,8,8],
             "y": [6.58,5.76,7.71,8.84,8.47,7.04,5.25,12.50,5.56,7.91,6.89]},
}

print(f"{'':8s} {'Moyenne x':>10} {'Variance x':>10} {'Moyenne y':>10} {'Variance y':>10} {'Corrélation':>12}")
for nom, d in anscombe.items():
    x, y = np.array(d["x"]), np.array(d["y"])
    r, _ = stats.pearsonr(x, y)
    print(f"Dataset {nom}  {x.mean():>10.2f} {x.var():>10.2f} {y.mean():>10.2f} {y.var():>10.2f} {r:>12.4f}")
```

```{code-cell} python
:tags: [hide-input]

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

for ax, (nom, d) in zip(axes, anscombe.items()):
    x, y = np.array(d["x"]), np.array(d["y"])
    r, _ = stats.pearsonr(x, y)
    ax.scatter(x, y, color="steelblue", s=60, zorder=3)
    m, b, _, _, _ = stats.linregress(x, y)
    x_l = np.linspace(min(x) - 1, max(x) + 1, 100)
    ax.plot(x_l, m * x_l + b, "r-", lw=2)
    ax.set_title(f"Dataset {nom}\nr = {r:.3f}", fontweight="bold")
    ax.set_xlabel("x")
    ax.set_ylabel("y")
    ax.set_xlim(2, 22)
    ax.set_ylim(2, 14)

plt.suptitle("Quartet d'Anscombe — même corrélation, distributions très différentes",
             fontsize=12, y=1.02)
plt.tight_layout()
plt.show()
```

### Le Datasaurus

Le Datasaurus Dozen (Matejka & Fitzmaurice, 2017) étend l'idée d'Anscombe à l'extrême : des jeux de données qui dessinent des formes reconnaissables tout en ayant des statistiques identiques.

```{code-cell} python
:tags: [hide-input]

# Génération d'une version simplifiée du Datasaurus (dinosaure approximatif)
# On crée deux nuages de points avec la MÊME corrélation mais formes différentes

def generer_avec_correlation_cible(forme, r_cible, n=150):
    """Génère des données avec une corrélation cible via transformation de Cholesky."""
    cov = np.array([[1, r_cible], [r_cible, 1]])
    L = np.linalg.cholesky(cov)
    z = rng.standard_normal((n, 2))
    data = z @ L.T
    return data[:, 0], data[:, 1]

# Trois formes avec la même corrélation
r_cible = -0.06

fig, axes = plt.subplots(1, 3, figsize=(15, 4))
noms = ["Nuage aléatoire", "Deux groupes séparés", "Forme en V"]
couleurs = ["steelblue", "tomato", "seagreen"]

for ax, nom, color in zip(axes, noms, couleurs):
    if nom == "Nuage aléatoire":
        x, y = generer_avec_correlation_cible(nom, r_cible, 200)
    elif nom == "Deux groupes séparés":
        x1, y1 = generer_avec_correlation_cible(nom, 0.7, 100)
        x2, y2 = generer_avec_correlation_cible(nom, 0.7, 100)
        x = np.concatenate([x1 - 2, x2 + 2])
        y = np.concatenate([y1 + 1, y2 - 1])
        # Rescaler pour obtenir r ≈ r_cible
        # (approx — la corrélation globale dépend de la position des groupes)
    else:  # Forme en V
        t = np.linspace(-2, 2, 200)
        x = t + rng.normal(0, 0.1, 200)
        y = np.abs(t) - 1 + rng.normal(0, 0.15, 200)

    r_obs, p_obs = stats.pearsonr(x, y)
    ax.scatter(x, y, alpha=0.5, s=15, color=color)
    ax.set_title(f"{nom}\nr={r_obs:.3f} (p={p_obs:.3f})", fontsize=9)
    ax.set_xlabel("x")
    ax.set_ylabel("y")

plt.suptitle("Même corrélation ≈ 0, formes très différentes — toujours tracer !",
             y=1.02, fontsize=11)
plt.tight_layout()
plt.show()
```

### Corrélation nulle ≠ indépendance

```{code-cell} python
:tags: [hide-input]

# Relation non linéaire avec corrélation nulle
t = np.linspace(-np.pi, np.pi, 400)
x_circ = np.cos(t) + rng.normal(0, 0.05, 400)
y_circ = np.sin(t) + rng.normal(0, 0.05, 400)

x_quad = np.linspace(-3, 3, 400) + rng.normal(0, 0.1, 400)
y_quad = x_quad**2 + rng.normal(0, 0.3, 400)

r_circ, p_circ = stats.pearsonr(x_circ, y_circ)
r_quad, p_quad = stats.pearsonr(x_quad, y_quad)

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

axes[0].scatter(x_circ, y_circ, alpha=0.5, s=10, color="steelblue")
axes[0].set_aspect("equal")
axes[0].set_title(f"Cercle : r = {r_circ:.3f}\n→ corrélation nulle mais forte dépendance")

axes[1].scatter(x_quad, y_quad, alpha=0.5, s=10, color="tomato")
axes[1].set_title(f"Parabole : r = {r_quad:.3f}\n→ corrélation nulle, dépendance quadratique")

plt.tight_layout()
plt.show()
```

### Corrélations fallacieuses (spurious correlations)

La corrélation entre deux variables peut être entièrement due à une **variable confondante** — une troisième variable qui cause les deux.

```{code-cell} python
# Exemple classique : taille de la ville → nombre de pompiers ET nombre d'incendies
# (les deux sont corrélés mais se causent-ils ?)
n_villes = 80
population = rng.exponential(scale=100_000, size=n_villes)

pompiers = 0.008 * population + rng.normal(0, 500, n_villes)
incendies = 0.002 * population + rng.normal(0, 100, n_villes)
# corrélation entre pompiers et incendies ?

r_pi, p_pi = stats.pearsonr(pompiers, incendies)

print(f"Corrélation pompiers / incendies : r = {r_pi:.3f} (p = {p_pi:.4f})")
print("→ Conclusion naive : les pompiers causent les incendies !")
print("→ Réalité : tous deux dépendent de la taille de la ville (confondante)")

# Après contrôle de la population (corrélation partielle) :
# r(pompiers, incendies | population) devrait être proche de 0
```

## Dépendance non linéaire

Pour détecter des relations non linéaires, deux mesures s'imposent.

### Information mutuelle

L'**information mutuelle** $I(X; Y)$ mesure la réduction d'incertitude sur $Y$ apportée par la connaissance de $X$ :

$$I(X; Y) = \sum_{x,y} p(x,y) \log \frac{p(x,y)}{p(x)p(y)}$$

Elle est nulle si et seulement si $X$ et $Y$ sont indépendants, quel que soit le type de relation. `sklearn.feature_selection.mutual_info_regression` en donne une estimation.

### Distance correlation

La **distance correlation** (Székely, 2007) vaut 0 si et seulement si les variables sont indépendantes (sous hypothèse de distribution à variance finie). Elle détecte des dépendances linéaires **et** non linéaires.

```{code-cell} python
from sklearn.feature_selection import mutual_info_regression

# Comparaison sur plusieurs types de relations
n = 300
x_lin = rng.uniform(-3, 3, n)
relations = {
    "Linéaire (y=x)": x_lin + rng.normal(0, 0.5, n),
    "Quadratique (y=x²)": x_lin**2 + rng.normal(0, 0.5, n),
    "Sinusoïdale": np.sin(x_lin * 2) + rng.normal(0, 0.2, n),
    "Indépendante": rng.normal(0, 1, n),
}

print(f"{'Relation':30s} {'Pearson':>10} {'Spearman':>10} {'MI (est.)':>12}")
print("-" * 65)
for nom, y in relations.items():
    r_p, _ = stats.pearsonr(x_lin, y)
    r_s, _ = stats.spearmanr(x_lin, y)
    mi = mutual_info_regression(x_lin.reshape(-1, 1), y, random_state=0)[0]
    print(f"{nom:30s} {r_p:>10.3f} {r_s:>10.3f} {mi:>12.4f}")
```

## Corrélation partielle

La **corrélation partielle** entre $X$ et $Y$ contrôlant $Z$ mesure la relation entre $X$ et $Y$ une fois supprimée l'influence linéaire de $Z$ sur les deux.

$$r_{XY \cdot Z} = \frac{r_{XY} - r_{XZ} r_{YZ}}{\sqrt{(1-r_{XZ}^2)(1-r_{YZ}^2)}}$$

```{code-cell} python
if HAS_PINGOUIN:
    # Corrélation partielle logements : surface et prix en contrôlant nb_pieces
    result = pg.partial_corr(data=df, x="surface", y="prix", covar="nb_pieces")
    print("Corrélation partielle (surface, prix | nb_pieces) avec pingouin :")
    print(result.to_string())
    print()

# Version manuelle
r_sp, _ = stats.pearsonr(df["surface"], df["prix"])
r_sn, _ = stats.pearsonr(df["surface"], df["nb_pieces"])
r_pn, _ = stats.pearsonr(df["prix"], df["nb_pieces"])

r_partielle = (r_sp - r_sn * r_pn) / np.sqrt((1 - r_sn**2) * (1 - r_pn**2))
print(f"Corrélation brute (surface, prix)               : {r_sp:.4f}")
print(f"Corrélation partielle (surface, prix | nb_pièces) : {r_partielle:.4f}")
print()
print("Après contrôle du nombre de pièces, la corrélation surface-prix diminue,")
print("car une partie de la relation passait par l'intermédiaire des pièces.")
```

## Matrice de corrélation complète avec tests (pingouin)

```{code-cell} python
if HAS_PINGOUIN:
    # Matrice de corrélation avec p-valeurs via pingouin
    corr_pg = pg.pairwise_corr(df[variables], method="pearson")
    print("Corrélations paires avec pingouin (extrait) :")
    print(corr_pg[["X", "Y", "r", "CI95", "p_unc", "BF10"]].head(10).to_string())
```

```{code-cell} python
:tags: [hide-input]

# Scatter plot matrix (pair plot)
g = sns.pairplot(df[variables], diag_kind="kde", plot_kws={"alpha": 0.3, "s": 10},
                 diag_kws={"fill": True})
g.figure.suptitle("Matrice de scatter plots — logements", y=1.01, fontsize=12)

# Annoter les corrélations dans le triangle supérieur
for i, v1 in enumerate(variables):
    for j, v2 in enumerate(variables):
        if i < j:
            r, p = stats.pearsonr(df[v1], df[v2])
            stars = "***" if p < 0.001 else ("**" if p < 0.01 else ("*" if p < 0.05 else "ns"))
            g.axes[i, j].annotate(f"r={r:.2f}{stars}",
                                  xy=(0.5, 0.5), xycoords="axes fraction",
                                  ha="center", va="center", fontsize=8.5,
                                  color="tomato" if abs(r) > 0.3 else "gray")

plt.tight_layout()
plt.show()
```

```{admonition} Récapitulatif : quelle corrélation choisir ?
:class: tip
| Situation | Recommandation |
|-----------|---------------|
| Relation linéaire, données normales, pas d'outliers | Pearson |
| Relation monotone, données ordinales, outliers présents | Spearman |
| Petit échantillon, données ordinales | Kendall |
| Relation non linéaire quelconque | Information mutuelle, distance correlation |
| Contrôler une variable confondante | Corrélation partielle (Pearson ou Spearman) |

**Règle d'or :** calculer la corrélation est facile ; l'interpréter est difficile. Tracez toujours le scatter plot. Corrélation forte ≠ causalité. Corrélation nulle ≠ indépendance.
```

La corrélation est un outil de **description** d'une relation, pas d'une explication. Comprendre la causalité nécessite un cadre différent — expériences randomisées, études causales (graphes de causalité, potentiel d'outcome) — qui dépasse le cadre de ce chapitre mais dont les principes fondamentaux seront abordés dans le chapitre sur l'A/B testing.
