Inférence causale : mesurer l'effet d'une action
Le module 9 a montré comment décider si une différence observée est réelle, et il s'est arrêté à une phrase importante : quand on ne peut pas randomiser, il existe des méthodes quasi-expérimentales, qui reposent sur des hypothèses à argumenter. Ce module développe cette phrase. C'est probablement le savoir-faire qui distingue le plus nettement quelqu'un qui sait entraîner un modèle de quelqu'un dont l'analyse fonde une décision : la question « quel est l'effet de cette action ? » n'est pas la question « que va-t-il se passer ? », et aucun modèle prédictif, si bon soit-il, ne répond à la première.
📋 Au programme
- Le problème : prédire n'est pas agir
- Les résultats potentiels : écrire ce qu'on cherche
- ATE, ATT, CATE : de quel effet parle-t-on ?
- Les trois hypothèses qu'on ne peut pas tester
- L'expérience randomisée, et CUPED
- Le score de propension : pondérer ou apparier
- Différences de différences
- Régression sur discontinuité
- Variables instrumentales
- Double machine learning et effets hétérogènes
- Et si un confondant manquait ?
- Récapitulatif et erreurs fréquentes
1. Le problème : prédire n'est pas agir
Un exemple auquel on ne coupe pas. Une entreprise constate que les clients ayant contacté le support résilient deux fois moins que les autres. Un modèle de classification le confirme : « a contacté le support » est la variable la plus importante. Conclusion tentante : appelons tout le monde.
Elle est probablement fausse, et pour une raison structurelle. Les gens qui contactent le support sont ceux qui tiennent assez au service pour prendre leur téléphone ; les autres sont déjà partis mentalement. La corrélation est réelle, le modèle n'a pas menti, mais l'intervention — appeler quelqu'un qui n'aurait pas appelé — n'est pas la même chose que l'observation — constater que quelqu'un a appelé.
Deux questions que tout oppose
- Prédictive — « sachant que ce client a appelé le support, quelle est sa probabilité de résilier ? » Un modèle de ML y répond très bien. On note cela \(P(Y \mid X = x)\) : on conditionne sur ce qu'on observe.
- Causale — « si j'appelle ce client, de combien sa probabilité de résilier change-t-elle ? » On note cela \(P(Y \mid do(X = x))\) : on intervient, on force la valeur au lieu de la constater.
Les deux quantités coïncident dans un seul cas : quand l'affectation au traitement est indépendante de tout le reste, c'est-à-dire quand on a randomisé. Partout ailleurs, elles diffèrent, et parfois jusqu'à changer de signe — c'est le paradoxe de Simpson du module 9.
La difficulté de fond est qu'aucune donnée, si abondante soit-elle, ne tranche seule. Deux mondes qui produisent exactement les mêmes données peuvent réclamer deux décisions opposées. Ce qui tranche, c'est une hypothèse sur le mécanisme, apportée par la connaissance du domaine, et que l'on écrit explicitement pour pouvoir en discuter. L'inférence causale est l'ensemble des méthodes qui rendent cette hypothèse visible et ses conséquences calculables.
2. Les résultats potentiels : écrire ce qu'on cherche
L'intuition
Le cadre dominant, dû à Rubin, part d'une idée simple. Pour chaque individu \(i\), imaginons deux versions du monde :
- \(Y_i(1)\) — ce qui lui serait arrivé s'il avait été traité ;
- \(Y_i(0)\) — ce qui lui serait arrivé s'il ne l'avait pas été.
L'effet du traitement sur cet individu est la différence \(\tau_i = Y_i(1) - Y_i(0)\). C'est exactement ce qu'on voudrait connaître, et c'est exactement ce qu'on ne peut jamais observer : un individu est traité ou ne l'est pas, jamais les deux. On n'observe que
\[ Y_i = T_i\,Y_i(1) + (1 - T_i)\,Y_i(0) \]où \(T_i \in \{0,1\}\) indique le traitement. L'autre terme est manquant, définitivement. C'est ce qu'on appelle le problème fondamental de l'inférence causale, et la formulation la plus utile est celle-ci : l'inférence causale est un problème de données manquantes, où ce qui manque est la moitié du tableau.
individu T Y(0) Y(1) τ = Y(1)-Y(0) observé
────────────────────────────────────────────────────────
1 1 ? 420 ? 420
2 0 310 ? ? 310
3 1 ? 380 ? 380
4 0 295 ? ? 295
· la colonne τ est entièrement inconnue, ligne par ligne
· MAIS sa MOYENNE peut être estimée, si les groupes sont comparables
→ tout le reste du module consiste à rendre les groupes comparablesCe déplacement — renoncer à l'effet individuel, viser une moyenne — est la clé. On ne saura jamais si ce client aurait résilié ; on peut savoir de combien le taux de résiliation change en moyenne sur une population.
3. ATE, ATT, CATE : de quel effet parle-t-on ?
« L'effet du traitement » recouvre plusieurs quantités distinctes, qu'il faut nommer avant d'estimer, parce qu'elles n'ont ni la même valeur ni la même utilité.
ATE — Average Treatment Effect E[Y(1) - Y(0)]
« et si on traitait TOUT LE MONDE ? »
la bonne cible pour une politique générale
ATT — ... on the Treated E[Y(1) - Y(0) | T = 1]
« le programme a-t-il servi à CEUX QUI Y ONT PARTICIPÉ ? »
la bonne cible pour évaluer un dispositif existant
CATE — Conditional ATE E[Y(1) - Y(0) | X = x]
« l'effet pour un individu DE PROFIL x »
la bonne cible pour cibler — c'est l'uplift du module 24
LATE — Local ATE : l'effet sur le sous-groupe que l'instrument
a réellement fait changer de comportement (section 9)La distinction n'est pas académique. Un dispositif de formation professionnelle auquel s'inscrivent surtout les personnes déjà motivées peut avoir un ATT élevé — il aide ceux qui viennent — et un ATE faible — il n'aiderait guère si on l'imposait à tous. Publier l'un pour l'autre est une faute courante, et coûteuse en décision publique.
4. Les trois hypothèses qu'on ne peut pas tester
Estimer un effet causal à partir de données observationnelles exige trois hypothèses. Elles ne se vérifient pas dans les données — c'est leur nature — mais elles se discutent, et une analyse honnête les énonce.
1. Ignorabilité (absence de confondant non mesuré)
À profil \(X\) identique, recevoir le traitement est indépendant des résultats potentiels :
\[ \big(Y(0),\, Y(1)\big) \perp\!\!\!\perp T \;\mid\; X \]Autrement dit : parmi les gens qui se ressemblent sur \(X\), qui a été traité est essentiellement une affaire de hasard. C'est l'hypothèse la plus forte et la plus souvent fausse. Si la motivation détermine à la fois la participation et le résultat, et qu'on ne la mesure pas, tout s'effondre.
2. Positivité (chevauchement)
Pour tout profil observé, les deux issues doivent être possibles : \(0 < P(T=1 \mid X=x) < 1\). Si aucun client de plus de 70 ans n'a jamais été appelé, on ne peut rien dire de l'effet d'un appel sur eux — il n'existe aucun terme de comparaison. On peut, elle, la vérifier empiriquement : c'est le diagnostic de chevauchement de la section 6.
3. SUTVA (pas d'interférence, un seul traitement)
Le résultat d'un individu ne dépend pas du traitement des autres, et « être traité » signifie la même chose pour tous. Deux violations très concrètes :
- Effets de réseau — offrir une réduction à la moitié des utilisateurs d'un réseau social : les non-traités en entendent parler. Le groupe témoin est contaminé, l'effet mesuré est sous-estimé.
- Équilibre général — un site de covoiturage qui teste une prime conducteur sur 10 % des trajets : ces conducteurs prennent les courses que les autres auraient prises. L'effet mesuré sur le petit groupe ne se reproduira pas à l'échelle.
La parade usuelle est de randomiser par grappes (une ville entière, un réseau entier) plutôt que par individu — au prix d'une forte perte de puissance statistique.
5. L'expérience randomisée, et CUPED
Randomiser rend l'affectation indépendante de tout, y compris de ce qu'on n'a pas mesuré : l'ignorabilité devient vraie par construction, sans hypothèse. C'est pour cela que l'essai contrôlé randomisé — l'A/B test du module 9 — reste l'étalon-or, et que la première question devant tout problème causal est : puis-je randomiser ?
Le problème : la puissance
La randomisation garantit l'absence de biais, pas la précision. Sur une métrique très dispersée — le chiffre d'affaires par utilisateur, où 1 % des utilisateurs font 40 % du total — l'intervalle de confiance reste large même avec un million d'observations. Or l'écart-type entre individus est en grande partie stable dans le temps : un gros acheteur avant l'expérience sera un gros acheteur pendant. Cette part de variance est du bruit prévisible, donc soustrayable.
CUPED : retirer le bruit qu'on peut prédire
L'idée tient en une ligne. Soit \(X\) la valeur de la métrique avant l'expérience. On remplace \(Y\) par
\[ Y^{\mathrm{cuped}} = Y - \theta\,(X - \bar{X}), \qquad \theta = \frac{\operatorname{Cov}(Y, X)}{\operatorname{Var}(X)} \]Comme \(X\) est mesuré avant l'affectation, il ne peut pas être affecté par le traitement : soustraire sa contribution ne déplace pas la moyenne du traitement, mais réduit la variance d'un facteur \(1 - \rho^2\), où \(\rho\) est la corrélation entre \(X\) et \(Y\). Avec \(\rho = 0{,}7\), la variance tombe de moitié : on obtient la même précision avec deux fois moins d'utilisateurs, ou le même résultat deux fois plus vite.
import numpy as np
from scipy import stats
rng = np.random.default_rng(0)
n = 20_000
base = rng.lognormal(3, 1, n) # dépense AVANT l'expérience
groupe = rng.integers(0, 2, n) # 0 = témoin, 1 = traitement
effet_vrai = 1.5
apres = 0.8 * base + rng.lognormal(3, 1, n) * 0.4 + effet_vrai * groupe
def ic(y, g):
d = y[g == 1].mean() - y[g == 0].mean()
se = np.sqrt(y[g == 1].var(ddof=1) / (g == 1).sum()
+ y[g == 0].var(ddof=1) / (g == 0).sum())
return d, 1.96 * se
theta = np.cov(apres, base)[0, 1] / base.var(ddof=1)
apres_cuped = apres - theta * (base - base.mean())
d0, m0 = ic(apres, groupe)
d1, m1 = ic(apres_cuped, groupe)
print(f"brut : effet {d0:5.2f} ± {m0:4.2f}") # effet 1.62 ± 0.94
print(f"CUPED : effet {d1:5.2f} ± {m1:4.2f}") # effet 1.55 ± 0.42
print(f"largeur divisée par {m0 / m1:.1f}") # largeur divisée par 2.36. Le score de propension : pondérer ou apparier
Le problème
On n'a pas pu randomiser. Les traités et les non-traités diffèrent sur dix variables : âge, ancienneté, dépense passée, canal d'acquisition. Comparer les moyennes brutes revient à comparer des populations différentes. On voudrait comparer chaque traité à des non-traités « qui lui ressemblent », mais avec dix variables, personne ne ressemble exactement à personne — c'est la malédiction de la dimension.
L'intuition : résumer en un seul nombre
Le résultat de Rosenbaum et Rubin est élégant. Définissons le score de propension
\[ e(x) = P(T = 1 \mid X = x) \]c'est-à-dire la probabilité d'être traité compte tenu du profil. Alors, sous l'hypothèse d'ignorabilité, conditionner sur ce seul nombre suffit : deux individus de même score de propension sont comparables, même si leurs profils diffèrent. Dix dimensions se réduisent à une.
On estime \(e(x)\) par un modèle de classification ordinaire — régression logistique ou gradient boosting du module 14 —, puis on s'en sert de deux façons :
Pondération (IPW) — reconstruire une population équilibrée
On pondère chaque individu par l'inverse de sa probabilité d'avoir reçu le traitement qu'il a effectivement reçu :
\[ \widehat{\mathrm{ATE}} = \frac{1}{n}\sum_{i} \left( \frac{T_i\,Y_i}{e(X_i)} - \frac{(1-T_i)\,Y_i}{1-e(X_i)} \right) \]L'intuition : un traité qui avait peu de chances de l'être est rare, donc il représente beaucoup de gens comme lui — on lui donne un grand poids. On fabrique ainsi une pseudo-population où traitement et profil sont indépendants.
Appariement — constituer des paires
À chaque traité on associe un ou plusieurs témoins de score voisin, et on compare à l'intérieur des paires. Plus intuitif à expliquer, plus facile à auditer, mais on écarte les individus sans correspondant — ce qui change silencieusement la population sur laquelle porte le résultat.
import numpy as np
from sklearn.linear_model import LogisticRegression
from sklearn.calibration import CalibratedClassifierCV
# X : covariables mesurées AVANT le traitement ; T : 0/1 ; Y : résultat
modele = CalibratedClassifierCV(LogisticRegression(max_iter=1000), cv=5)
modele.fit(X, T)
e = modele.predict_proba(X)[:, 1]
# 1) positivité : les deux groupes doivent se recouvrir
print("traités e ∈", np.percentile(e[T == 1], [1, 99]).round(3))
print("témoins e ∈", np.percentile(e[T == 0], [1, 99]).round(3))
# si les plages ne se recouvrent pas, AUCUNE méthode ne sauvera l'analyse
# 2) écrêtage : un score de 0.001 donnerait un poids de 1000
e = np.clip(e, 0.05, 0.95)
w = np.where(T == 1, 1 / e, 1 / (1 - e))
# 3) vérifier que la pondération a bien équilibré les covariables
def diff_standardisee(x, T, w):
m1 = np.average(x[T == 1], weights=w[T == 1])
m0 = np.average(x[T == 0], weights=w[T == 0])
return (m1 - m0) / np.sqrt((x[T == 1].var() + x[T == 0].var()) / 2)
for j, nom in enumerate(noms_colonnes):
d = diff_standardisee(X[:, j], T, w)
drapeau = " ⚠ déséquilibré" if abs(d) > 0.1 else ""
print(f"{nom:20s} {d:+.3f}{drapeau}")
ate = np.average(Y[T == 1], weights=w[T == 1]) - \
np.average(Y[T == 0], weights=w[T == 0])
print(f"ATE estimé : {ate:.3f}")7. Différences de différences
Le problème
Une région applique une nouvelle réglementation, l'autre non. Comparer les deux régions après coup ne dit rien : elles différaient déjà avant. Comparer la région traitée avant et après ne dit rien non plus : entre-temps, la conjoncture a bougé pour tout le monde.
L'intuition : soustraire deux fois
On combine les deux comparaisons pour éliminer les deux biais. La différence avant/après dans le groupe témoin mesure ce qui se serait passé sans traitement — la tendance commune. On la retranche de la différence avant/après du groupe traité ; ce qui reste est l'effet.
\[ \widehat{\mathrm{ATT}} = \underbrace{(\bar Y_{\text{tr,apr}} - \bar Y_{\text{tr,av}})}_{\text{évolution des traités}} - \underbrace{(\bar Y_{\text{té,apr}} - \bar Y_{\text{té,av}})}_{\text{évolution des témoins}} \] Y
│ ● traités (observé)
│ ●──────────
│ ●───────╱ ↕ EFFET ESTIMÉ
│ ●──────╱ ○ ─ ─ ─ ─ ─ contrefactuel : les traités
│ ╱ s'ils avaient suivi la tendance
│ ○──────○──────○────╱ des témoins
│ ○ témoins (observé)
└────────────────────────┬──────────────────────────► temps
intervention
L'hypothèse, INVÉRIFIABLE : sans traitement, les deux courbes seraient
restées PARALLÈLES. On ne peut pas la tester après l'intervention —
mais on peut regarder si elles l'étaient AVANT. C'est le test de
tendances pré-traitement, et c'est le premier graphique à produire.import statsmodels.formula.api as smf
# df : une ligne par (unité, période), colonnes traite (0/1), apres (0/1)
# L'interaction porte TOUT l'effet causal recherché.
m = smf.ols("y ~ traite * apres", data=df).fit(
cov_type="cluster", cov_kwds={"groups": df["unite"]})
print(m.summary().tables[1])
# coef std err P>|t|
# Intercept 12.400 0.210 0.000 niveau témoins, avant
# traite 3.100 0.290 0.000 écart initial (biais de niveau)
# apres 1.800 0.240 0.000 tendance commune
# traite:apres 2.450 0.330 0.000 ← L'EFFET
# forme plus robuste : effets fixes unité + période, qui absorbent
# toute différence stable entre unités et tout choc commun aux unités
m2 = smf.ols("y ~ traite:apres + C(unite) + C(periode)", data=df).fit(
cov_type="cluster", cov_kwds={"groups": df["unite"]})8. Régression sur discontinuité
L'intuition
Certaines décisions reposent sur un seuil arbitraire : une bourse attribuée en dessous d'un revenu, un rattrapage déclenché sous 10 de moyenne, une remise commerciale au-delà d'un montant de panier. Un élève à 9,9 et un élève à 10,1 sont, à toutes fins pratiques, identiques — sauf que l'un reçoit le dispositif et l'autre non. Au voisinage immédiat du seuil, on dispose donc d'une quasi-randomisation offerte par la règle.
résultat
│ ╭─── ajustement à droite
│ ╭────╯
│ ╭────╯
│ ╭────╯ ↕ EFFET = saut à la discontinuité
│ ────────╯
│ ──────
│ ──────
└──────────────────────┬───────────────────────► variable de forçage
seuil
· on ajuste une courbe DE CHAQUE CÔTÉ, séparément
· l'effet est l'écart des deux ajustements AU seuil, rien d'autre
· la validité décroît vite quand on s'éloigne : l'effet estimé est LOCALLes vérifications qui font la différence
- Pas de manipulation du seuil. Si les enseignants remontent les 9,7 à 10, la densité de la variable de forçage présente un trou juste avant et une bosse juste après. Le test de McCrary consiste à tracer cet histogramme : un pic au seuil invalide l'analyse.
- Continuité des covariables. L'âge, le sexe, l'origine sociale ne doivent pas sauter au seuil. S'ils sautent, quelque chose d'autre change en même temps.
- Choix de la fenêtre. Trop large, on mélange des individus dissemblables ; trop étroite, il ne reste rien. On reporte la sensibilité du résultat à ce choix — pas une seule valeur.
- Degré du polynôme. Linéaire ou quadratique local ; au-delà, les degrés élevés fabriquent des sauts artificiels aux bords.
import numpy as np, statsmodels.formula.api as smf
seuil, fenetre = 10.0, 1.5
d = df[np.abs(df["note"] - seuil) <= fenetre].copy()
d["centre"] = d["note"] - seuil
d["droite"] = (d["centre"] >= 0).astype(int)
# pentes séparées de part et d'autre : centre * droite
m = smf.ols("resultat ~ centre * droite", data=d).fit(cov_type="HC1")
print(f"effet au seuil : {m.params['droite']:.3f} "
f"[{m.conf_int().loc['droite', 0]:.3f}, "
f"{m.conf_int().loc['droite', 1]:.3f}]")
# sensibilité à la fenêtre : un résultat solide bouge peu
for h in (0.5, 1.0, 1.5, 2.0, 3.0):
dd = df[np.abs(df["note"] - seuil) <= h].copy()
dd["centre"] = dd["note"] - seuil
dd["droite"] = (dd["centre"] >= 0).astype(int)
r = smf.ols("resultat ~ centre * droite", data=dd).fit()
print(f" fenêtre {h}: {r.params['droite']:+.3f} (n = {len(dd)})")9. Variables instrumentales
Le problème
Un confondant non mesuré subsiste, et aucun ajustement ne le corrigera puisqu'on ne l'observe pas. Exemple canonique : l'effet d'une année d'études supplémentaire sur le salaire. La capacité individuelle influence à la fois la poursuite d'études et le salaire, et elle n'est pas dans les données.
L'intuition : une source de variation innocente
On cherche une troisième variable \(Z\), l'instrument, qui pousse à être traité sans avoir d'effet propre sur le résultat. Elle joue le rôle d'un tirage au sort partiel, fourni par la nature ou l'administration. On n'utilise alors que la part de variation du traitement due à l'instrument — une part dont on sait qu'elle n'est pas contaminée.
Z ─────────► T ─────────► Y
│ ▲
│ │
└──── ✗ ────► U (confondant non observé)
│
▼ (U influence T ET Y)
1. PERTINENCE Z influence réellement T → TESTABLE (statistique F)
2. EXCLUSION Z n'agit sur Y QUE via T → NON testable, à argumenter
3. INDÉPENDANCE Z n'est lié à aucun confondant → NON testable
Exemples utilisés en économie :
· distance au lycée le plus proche → poursuite d'études → salaire
· loterie d'affectation scolaire → école fréquentée → résultats
· mois de naissance (âge légal de sortie) → années d'études → salaire
· en entreprise : une INCITATION randomisée à utiliser une fonction,
quand on ne peut pas forcer l'usage → c'est le cas le plus utilefrom linearmodels.iv import IV2SLS
# y ~ 1 + controles + [traitement ~ instrument]
res = IV2SLS.from_formula(
"salaire ~ 1 + experience + [annees_etudes ~ distance_lycee]",
data=df).fit(cov_type="robust")
print(res.summary)
# LE diagnostic à lire en premier : la première étape
print(res.first_stage)
# F < 10 → instrument FAIBLE : l'estimation est biaisée et son
# intervalle de confiance n'a pas la couverture annoncée.
# Mieux vaut ne rien conclure que publier ce chiffre.10. Double machine learning et effets hétérogènes
Le problème
On voudrait ajuster sur beaucoup de covariables, avec des relations non linéaires — exactement ce que le gradient boosting du module 14 fait bien. Mais brancher un modèle de ML dans une estimation causale échoue de deux façons. D'abord, la régularisation qui aide à prédire biaise l'estimation de l'effet : elle rétrécit tous les coefficients, y compris celui qui nous intéresse. Ensuite, le surapprentissage : un modèle qui a vu un individu à l'entraînement produit sur lui un résidu artificiellement petit.
L'intuition : régresser le bruit sur le bruit
La méthode de Chernozhukov et coauteurs repose sur une idée ancienne — le théorème de Frisch–Waugh — appliquée avec du ML :
- prédire le résultat à partir des covariables, \(\hat{m}(X) \approx E[Y \mid X]\), et garder le résidu \(\tilde{Y} = Y - \hat{m}(X)\) ;
- prédire le traitement à partir des covariables, \(\hat{e}(X) \approx E[T \mid X]\), et garder le résidu \(\tilde{T} = T - \hat{e}(X)\) ;
- régresser \(\tilde{Y}\) sur \(\tilde{T}\) : la pente est l'effet causal.
Les deux premières étapes retirent tout ce que les covariables expliquent ; ce qui reste dans \(\tilde{T}\) est la part d'affectation au traitement qui ne s'explique pas par le profil — la part « comme si » elle avait été tirée au sort. Deux ingrédients rendent la construction valide :
- L'orthogonalisation — résidualiser des deux côtés rend l'estimation insensible au premier ordre aux erreurs des modèles auxiliaires. Ceux-ci peuvent être imparfaits sans invalider l'effet.
- Le découpage croisé (cross-fitting) — on prédit toujours un individu avec un modèle entraîné sans lui, comme en validation croisée (module 12). C'est ce qui neutralise le surapprentissage.
from econml.dml import LinearDML, CausalForestDML
from sklearn.ensemble import HistGradientBoostingRegressor as HGB
from sklearn.ensemble import HistGradientBoostingClassifier as HGC
# ---- effet moyen, avec ajustement non linéaire sur X
est = LinearDML(model_y=HGB(), model_t=HGC(),
discrete_treatment=True, cv=5) # cv = découpage croisé
est.fit(Y, T, X=X, W=W) # X : modulateurs d'effet ; W : contrôles
print("ATE :", est.ate(X), est.ate_interval(X, alpha=0.05))
# ---- effet PAR INDIVIDU : qui gagne à être traité ?
foret = CausalForestDML(model_y=HGB(), model_t=HGC(),
discrete_treatment=True, cv=5, n_estimators=500)
foret.fit(Y, T, X=X, W=W)
cate = foret.effect(X) # un effet estimé par individu
print(f"effet médian : {np.median(cate):+.3f}")
print(f"part d'effets négatifs : {(cate < 0).mean():.1%}")
# → si 30 % des individus ont un effet négatif, traiter tout le monde
# est une mauvaise politique même si l'ATE est positif.
# quelles variables portent l'hétérogénéité ?
print(foret.feature_importances())Cette dernière sortie est le pont vers l'action : un effet moyen positif peut masquer un sous-groupe que le traitement dégrade. C'est précisément l'uplift du module 24, qui reprend ce résultat pour décider qui traiter sous contrainte de budget.
11. Et si un confondant manquait ?
Toutes les méthodes observationnelles reposent sur une hypothèse invérifiable. Plutôt que de l'affirmer, on peut mesurer sa fragilité : quelle force devrait avoir un confondant caché pour annuler la conclusion ?
La valeur E, en une phrase
C'est la force d'association minimale — avec le traitement et avec le résultat — que devrait avoir un confondant non mesuré pour ramener l'effet observé à zéro. Une valeur E de 1,3 signifie qu'une variable oubliée assez banale suffirait : la conclusion est fragile. Une valeur E de 4 signifie qu'il faudrait un facteur très puissant, que l'on aurait probablement remarqué : la conclusion est robuste.
L'exemple historique est éclairant. Pour le lien tabac–cancer, la valeur E dépassait 10 : aucun confondant plausible ne pouvait produire une association aussi forte. C'est ce raisonnement, et non un essai randomisé — impossible à mener —, qui a emporté la conviction.
import numpy as np
def valeur_e(rr: float) -> float:
"""Valeur E d'un risque relatif (ou de sa borne de confiance)."""
if rr < 1:
rr = 1 / rr
return rr + np.sqrt(rr * (rr - 1))
print(f"RR = 1.2 → E = {valeur_e(1.2):.2f}") # 1.69 fragile
print(f"RR = 2.0 → E = {valeur_e(2.0):.2f}") # 3.41 solide
print(f"RR = 5.0 → E = {valeur_e(5.0):.2f}") # 9.47 très solide
# On reporte TOUJOURS la valeur E de la borne de confiance la plus
# proche de 1 : c'est le scénario le moins favorable compatible
# avec les données.Reporter une valeur E change la nature du débat : on ne discute plus de savoir si l'hypothèse est vraie — question sans réponse —, mais de savoir si un confondant de cette force est plausible dans ce domaine. C'est une question à laquelle un expert métier peut répondre.
12. Récapitulatif et erreurs fréquentes
Puis-je randomiser ?
├─ OUI → A/B test (module 9) + CUPED pour la puissance
│ randomiser par grappes si les unités interagissent
└─ NON → quelle structure les données offrent-elles ?
│
├─ Un seuil administratif décide du traitement
│ → RÉGRESSION SUR DISCONTINUITÉ effet local, très crédible
│
├─ Deux groupes observés avant ET après
│ → DIFFÉRENCES DE DIFFÉRENCES hypothèse : tendances parallèles
│
├─ Une variable pousse au traitement sans agir sur le résultat
│ → VARIABLES INSTRUMENTALES estime le LATE ; vérifier F > 10
│
├─ Je crois avoir mesuré tous les confondants
│ → SCORE DE PROPENSION / DOUBLE ML hypothèse la plus forte
│ + diagnostic de chevauchement OBLIGATOIRE
│
└─ Rien de tout cela
→ décrire, ne pas conclure. Une corrélation présentée comme
telle vaut mieux qu'un effet causal inventé.Les six erreurs qui reviennent
- Contrôler par tout ce qu'on a. Ajouter des variables n'est pas neutre : conditionner sur un médiateur efface l'effet, conditionner sur un collisionneur en crée un (module 9). Le choix des variables d'ajustement se déduit d'un graphe causal, pas de la disponibilité des colonnes.
- Ajuster sur une variable postérieure au traitement. Même règle que la fuite de données du module 12 : tout ce qui est mesuré après l'affectation est suspect.
- Lire l'importance des variables comme un effet causal. Un SHAP élevé (module 14) signifie « utile pour prédire », jamais « agir dessus produira un effet ».
- Oublier le regroupement des écarts-types. Quand la randomisation porte sur des villes et l'analyse sur des individus, l'erreur type est sous-estimée d'un facteur trois ou quatre, et tout devient « significatif ».
- Publier l'ATT pour l'ATE. « Le dispositif a aidé ses participants » n'est pas « le dispositif aiderait tout le monde ».
- Traiter tout le monde parce que l'effet moyen est positif. S'il existe un sous-groupe où l'effet est négatif, la bonne politique est de cibler — c'est l'objet du module 24.
Une dernière remarque, qui vaut pour tout le reste du cours. Un modèle prédictif et un modèle causal ne s'évaluent pas de la même manière : le premier se juge sur des données de test, le second ne se juge sur rien — il n'existe pas de vérité terrain pour un effet causal, puisque le contrefactuel est manquant par construction. C'est pourquoi la crédibilité repose ici sur la conception de l'étude, sur les diagnostics, et sur l'énoncé explicite des hypothèses. Le module 12 reprend maintenant le fil de la pratique : comment passer d'un problème métier à un jeu de données exploitable.