Leçon 2 sur 8
Unité · Un modèle est une comparaison
L'ajustement déplace l'estimation ; les grappes déplacent l'erreur-type
Ajouter toutes les covariables individuelles du registre fait passer le coefficient de cantine de 4,47 à 4,37 et laisse l'erreur-type à 0,9. Ajouter un terme pour l'école laisse le coefficient tranquille et porte l'erreur-type à 1,5. Ce sont deux problèmes distincts et on les confond couramment.
Le tableau autour duquel ce cours est bâti
import pandas as pd
import statsmodels.formula.api as smf
attendance = pd.read_csv("school-attendance-2024.v1.csv")
roster = pd.read_csv("school-roster-2024.v1.csv").drop_duplicates(
"student_id", keep="last")
MARKS = {"true": True, "Y": True, "false": False, "N": False}
marked = attendance[attendance["present"].isin(MARKS)].copy()
marked["attended"] = marked["present"].map(MARKS)
per_student = (marked.groupby("student_id")["attended"].mean()
.rename("rate").reset_index().merge(roster, on="student_id"))
enrolment = pd.read_csv("school-enrolment-2024.v1.csv")
current = enrolment[enrolment["school_year"] == 2024]
d = per_student.merge(
current[["student_id", "sex", "age_years", "disability_reported",
"displacement_status", "admin2"]], on="student_id").dropna()
print(f"complete cases: {len(d)}")
m1 = smf.ols("rate ~ feeding_programme", data=d).fit()
m2 = smf.ols("rate ~ feeding_programme + sex + age_years"
" + disability_reported + displacement_status", data=d).fit()
m3 = smf.ols("rate ~ feeding_programme + sex + age_years"
" + disability_reported + displacement_status + admin2", data=d).fit()
m4 = m1.get_robustcov_results(cov_type="cluster", groups=d["school_id"])
for name, m in [("child covariates", m2), ("+ district", m3)]:
print(f"{name:18} {m.params['feeding_programme[T.True]']:+.4f}")
library(dplyr)
m1 <- lm(rate ~ feeding_programme, data = d)
m2 <- lm(rate ~ feeding_programme + sex + age_years +
disability_reported + displacement_status, data = d)
m3 <- update(m2, . ~ . + admin2)
| Modèle | Coefficient cantine | ET | t |
|---|---|---|---|
| M1 un prédicteur | +0,0447 | 0,0089 | 5,02 |
| M2 + sexe, âge, handicap, déplacement | +0,0437 | 0,0090 | 4,85 |
| M3 + district | +0,0360 | 0,0100 | 3,60 |
| M4 = M1 avec ET groupées par école | +0,0447 | 0,0154 | 2,90 |
Lisez les colonnes plutôt que les lignes, car elles disent deux choses différentes.
Ajouter des covariables a changé le coefficient et laissé l’erreur-type tranquille. De M1 à M3, l’estimation passe de 4,5 points à 3,6 tandis que l’erreur-type va de 0,9 à 1,0.
Le regroupement a changé l’erreur-type et laissé le coefficient intact. M4 est la même droite ajustée que M1 — coefficient identique à toutes les décimales — avec une erreur-type 1,7 fois plus grande.
Ce sont deux problèmes distincts. L’un porte sur le caractère comparable des groupes ; l’autre sur la quantité d’information que contient réellement l’échantillon. Résoudre l’un ne fait rien pour l’autre, et un modèle qui ne résout ni l’un ni l’autre est faux de deux façons indépendantes.
Ce que « ajuster sur » fait réellement
L’expression figure dans chaque rapport de programme et signifie quelque chose de plus étroit que ce que supposent les lecteurs.
Un coefficient avec des covariables dans le modèle est la comparaison à l’intérieur des niveaux de ces covariables, mise en commun sur ces niveaux. « Ajusté sur le district » signifie : comparer élèves nourris et non nourris à l’intérieur de chaque district, puis moyenner ces comparaisons.
within = (d.groupby("admin2")
.apply(lambda g: g[g["feeding_programme"]]["rate"].mean()
- g[~g["feeding_programme"]]["rate"].mean()))
print(within.round(4))
print(f"unadjusted: {m1.params['feeding_programme[T.True]']:.4f}")
print(f"adjusted: {m3.params['feeding_programme[T.True]']:.4f}")
d |> summarise(gap = mean(rate[feeding_programme]) -
mean(rate[!feeding_programme]), .by = admin2)
| District | Écart | Avec cantine | Sans |
|---|---|---|---|
| Artibonite | +3,8 pts | 146 | 141 |
| Centre | −4,1 pts | 244 | 42 |
| Nord-Ouest | +4,9 pts | 251 | 45 |
| Sud | +7,0 pts | 104 | 178 |
Le coefficient ajusté est une moyenne pondérée de ces quatre écarts, et les voir séparément vaut les deux lignes que cela coûte — parce que l’un d’eux va dans l’autre sens. Les écoles avec cantine du Centre sont présentes 4,1 points de moins que celles sans, contre une estimation groupée de +3,6.
Un coefficient unique moyenné sur un désaccord reste un nombre, et ce n’est plus un résumé. Avant de rapporter le 3,6 ajusté, dites que le Centre est en désaccord et que sa comparaison repose sur 42 élèves sans cantine — assez peu pour que le renversement puisse être du bruit, et assez peu pour qu’il faille le vérifier plutôt que le moyenner.
Pourquoi les covariables n’ont presque rien déplacé
M2 ajoute sexe, âge, handicap et statut de déplacement et déplace le coefficient d’un dixième de point. Ce n’est pas un échec des covariables — c’est un fait sur le protocole.
La cantine a été attribuée par école. Rien concernant un enfant n’a décidé s’il recevait des repas scolaires, si bien que les caractéristiques individuelles ne peuvent pas être des facteurs de confusion de cette comparaison, et ajuster dessus ne peut pas beaucoup déplacer l’estimation.
Le district, lui, l’a déplacée, de 4,5 à 3,6, et pour la raison inverse : les écoles avec cantine ne sont pas réparties uniformément entre districts, donc le district est un vrai facteur de confusion pour une exposition de niveau école.
La règle qu’implique le protocole : les facteurs de confusion vivent au niveau de l’attribution. Pour un programme de niveau école, cherchez les différences de niveau école et district ; ajouter des covariables individuelles n’est ici ni nuisible ni utile, et signale surtout que l’analyste a pris ce que le fichier contenait.
Lire un tableau de coefficients sans se faire tromper
Quatre habitudes, chacune prévenant une mélecture précise.
Rapportez la taille d’échantillon du modèle, non celle du fichier. L’ajustement sur cas complets a écarté 49 élèves ici — 1 200 lignes deviennent 1 151 — et l’écart est silencieux. Chaque modèle du tableau ci-dessus doit être ajusté sur les mêmes lignes, sans quoi la comparaison entre modèles est contaminée par les lignes que chacun a gardées.
print(f"file {len(per_student)}, model {int(m3.nobs)}")
c(file = nrow(per_student), model = nobs(m3))
Ne lisez pas les coefficients des covariables comme des constats. Ils sont ajustés sur un ensemble de variables choisi pour répondre à une question sur la cantine, non sur le sexe ou le déplacement. Un coefficient n’est interprétable que pour l’exposition autour de laquelle le modèle a été bâti — c’est le « sophisme du tableau 2 », et c’est ainsi qu’un coefficient de déplacement ajusté comme contrôle finit cité comme un constat sur le déplacement.
Vérifiez si une covariable ajoutée a changé l’échantillon. Une covariable avec des valeurs manquantes écarte silencieusement des lignes, donc un coefficient qui bouge quand vous l’ajoutez a peut-être bougé parce que l’échantillon a changé et non parce que l’ajustement a fait quelque chose.
Rapportez aussi l’estimation non ajustée. La paire est ce qui montre au lecteur le travail qu’a fait l’ajustement, et un rapport ne donnant que le nombre ajusté a caché la seule comparaison qui n’exige aucune hypothèse.
Quand l’ajustement ne peut pas aider
Trois cas où ajouter une covariable n’est pas la réponse, tous présents plus loin dans ce cours.
Le facteur de confusion n’est pas dans le fichier. Ajuster sur ce que vous avez mesuré ne dit rien de ce que vous n’avez pas mesuré, et « ajusté » n’est pas synonyme de « sans confusion ». Le cours d’épidémiologie faisait ce point avec la standardisation sur l’âge ; la régression n’y ajoute rien.
La covariable est sur le chemin causal. La leçon 5 montre une covariable qui retire 42 % de l’effet en se plaçant entre l’exposition et le résultat, et l’ajouter détériore la réponse d’une façon qu’aucune statistique d’ajustement ne révèle.
Le problème est l’erreur-type, non l’estimation. Aucune covariable ne corrige le regroupement en grappes. C’est la ligne M1 vers M4, et la leçon 6 y répond entièrement.
Rapportez-le en entier
Attendance and school feeding, adjusted analysis
Linear model, 1,151 students with complete covariates (49 dropped).
Unadjusted +4.47 points 95% CI +2.73 to +6.22
Adjusted for district +3.60 points 95% CI +1.64 to +5.56
Child-level covariates (sex, age, disability, displacement) change the
estimate by less than 0.1 points and are retained only to show that.
The district gaps are +3.8, -4.1, +4.9 and +7.0 points. Centre runs the
other way on 42 unfed students and is not averaged away silently.
Standard errors above assume independent students and are too small; the
school-clustered version of the unadjusted model gives +4.47 (95% CI
+1.45 to +7.49).
Observational. Schools were not randomised into the programme, and
district adjustment does not make the remaining comparison causal.
Les deux estimations, et l’intervalle groupé, en six lignes. Un lecteur qui ne voit que le nombre ajusté ne peut pas savoir si l’ajustement a compté, et un lecteur qui ne voit que l’intervalle naïf s’entend dire que le résultat est deux fois plus précis qu’il ne l’est.
La suite
Tout jusqu’ici avait un résultat continu. La leçon suivante prend un résultat en oui ou non — cette orientation a-t-elle atteint un service — et rencontre le nombre que ce public interprète de travers plus sûrement qu’aucun autre.