cassionAnalyse de données

Leçon 6 sur 8

Unité · Où les comparaisons cèdent

Mille deux cents élèves, vingt-quatre décisions

La cantine a été attribuée par école, non par enfant. Testez-la sur 1 200 élèves et t vaut 5,41 ; testez-la sur les 24 écoles réellement attribuées et t vaut 3,11. L'effet a la même taille. Une seule des deux erreurs-types est honnête.

PythonR180 minEnquête SMARTDéfinitions d'indicateurs de l'UNICEFCritères d'évaluation du CAD de l'OCDE

Vérifiez qui a été attribué avant de vérifier qui a été mesuré

import pandas as pd
import numpy as np
from scipy import stats

roster = pd.read_csv("school-roster-2024.v1.csv")
print(f"roster rows {len(roster)}, students {roster['student_id'].nunique()}")

# Two students were transferred and never de-registered, so they appear twice
# with different schools. Keep the later row: that is the school they moved to.
roster = roster.drop_duplicates("student_id", keep="last")

mixed = roster.groupby("school_id")["feeding_programme"].nunique()
print(f"schools with both fed and unfed students: {(mixed > 1).sum()}")
print(roster.groupby("school_id")["feeding_programme"].first().value_counts())
library(dplyr)

roster |> summarise(variants = n_distinct(feeding_programme), .by = school_id) |>
  count(variants)

Pas une école sur les vingt-quatre n’a à la fois des élèves nourris et non nourris. Quinze écoles font tourner le programme et neuf non, et chaque enfant d’une école partage son statut.

Le registre compte 1 202 lignes pour 1 200 élèves, et il faut trancher cela d’abord : deux enfants ont été transférés sans être radiés, si bien que chacun figure sous les deux écoles avec des statuts de cantine opposés. Joindre avant de dédoublonner leur donne deux lignes de présence chacun et place le même enfant des deux côtés de la comparaison.

Cette seule ligne décide de toute l’analyse. Le programme a été attribué à 24 écoles, donc l’analyse a 24 observations indépendantes et non 1 200 — et les 1 200 élèves sont 24 groupes d’une cinquantaine d’enfants qui partagent un directeur, un bassin de recrutement, une route et un calendrier scolaire.

Faites cette vérification avant chaque comparaison. Elle tient sur une ligne, et c’est la différence entre un t de 5,41 et un t de 3,11.

Les deux analyses

attendance = pd.read_csv("school-attendance-2024.v1.csv")
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"))

fed = per_student[per_student["feeding_programme"]]["rate"]
unfed = per_student[~per_student["feeding_programme"]]["rate"]
print(stats.ttest_ind(fed, unfed, equal_var=False))
per_student |>
  t.test(rate ~ feeding_programme, data = _)

Au niveau élève : 90,14 % contre 85,21 %, différence 4,93 points, t = 5,41 sur 1 200 élèves.

per_school = (per_student.groupby(["school_id", "feeding_programme"])["rate"]
              .mean().reset_index())

fed_s = per_school[per_school["feeding_programme"]]["rate"]
unfed_s = per_school[~per_school["feeding_programme"]]["rate"]
result = stats.ttest_ind(fed_s, unfed_s, equal_var=False)
print(f"n = {len(fed_s)} fed, {len(unfed_s)} unfed")
print(f"difference {fed_s.mean() - unfed_s.mean():+.2%}, t = {result.statistic:.2f}")
per_school |> t.test(rate ~ feeding_programme, data = _)

Au niveau école : 90,25 % contre 85,04 %, différence 5,21 points, t = 3,11 sur 24 écoles.

Analyse n Différence Erreur-type t
Niveau élève 1 200 +4,93 pts 0,91 pt 5,41
Niveau école 24 +5,21 pts 1,68 pt 3,11

L’effet a à peine bougé. L’erreur-type a presque doublé. C’est tout le phénomène : traiter des observations en grappes comme indépendantes ne biaise pas l’estimation, cela sous-estime son incertitude, et chaque p et chaque intervalle bâtis dessus sont trop étroits.

Pourquoi le rapport vaut 1,8

Le multiplicateur n’est pas arbitraire — il vient de la part de variation qui se situe entre les écoles plutôt qu’à l’intérieur.

groups = [g["rate"].values for _, g in per_student.groupby("school_id")]
k, N = len(groups), len(per_student)
grand = per_student["rate"].mean()

msb = sum(len(g) * (g.mean() - grand) ** 2 for g in groups) / (k - 1)
msw = sum(((g - g.mean()) ** 2).sum() for g in groups) / (N - k)
n0 = (N - sum(len(g) ** 2 for g in groups) / N) / (k - 1)

icc = (msb - msw) / (msb + (n0 - 1) * msw)
deff = 1 + (n0 - 1) * icc
print(f"ICC {icc:.3f}, average cluster {n0:.0f}, design effect {deff:.2f}")
print(f"effective sample size {N / deff:.0f} of {N}")
# lme4::lmer(rate ~ 1 + (1 | school_id)) gives the same variance components.

Coefficient de corrélation intra-classe 0,060, grappe moyenne 50, effet de plan 3,94. Six pour cent de la variation de présence se situe entre écoles, ce qui paraît négligeable et ne l’est pas : avec cinquante enfants par école, cela multiplie la variance par près de quatre.

Taille d’échantillon efficace 304, non 1 200. L’erreur-type croît comme la racine de l’effet de plan, et la racine de 3,94 vaut 1,99 — ce qui donne le 1,8 observé plus haut.

C’est le même effet de plan que le cours sur les enquêtes appliquait à l’échantillonnage en grappes. L’arithmétique se moque de savoir si les grappes viennent d’un plan de sondage ou de la façon dont un programme a été déployé — une cantine attribuée par école est un plan en grappes, que quelqu’un l’ait appelé ainsi ou non.

Trois façons de bien faire

Agréger à l’unité d’attribution. Calculez un nombre par école, testez les 24. Simple, transparent, et c’est ce que fait le tableau ci-dessus. Le coût est qu’une école de 90 enfants compte autant qu’une de 20.

sizes = per_student.groupby("school_id").size().rename("students")
sized = per_school.merge(sizes, on="school_id")
fed_rows = sized[sized["feeding_programme"]]
print(f"unweighted {fed_rows['rate'].mean():.2%}, "
      f"weighted by roster {np.average(fed_rows['rate'], weights=fed_rows['students']):.2%}")
# Weighting is a judgement about what the average school means, not a fix.

Employer un modèle mixte avec une constante aléatoire par école. Il garde chaque élève, estime explicitement la variance inter-écoles et gère des tailles d’école inégales. statsmodels.formula.api.mixedlm en Python, lme4::lmer en R.

Employer des erreurs-types robustes aux grappes. Elles gardent le modèle au niveau élève et corrigent l’erreur-type du regroupement, ce qui est l’approche standard en économétrie et tient dans un argument de statsmodels. Il leur faut un nombre raisonnable de grappes — 24 est bas, et en dessous d’une trentaine la correction devient elle-même peu fiable.

Avec 24 grappes, agrégez. Le modèle mixte apporte de la précision quand les grappes sont nombreuses et de tailles inégales ; ici il produirait une réponse voisine avec plus de machinerie et plus d’hypothèses.

Le même piège dans trois autres cours

La corrélation. La corrélation point-bisériale entre cantine et présence vaut 0,161 au niveau élève et 0,588 au niveau école. Les deux sont des réponses correctes à des questions différentes, et rapporter le chiffre au niveau élève comme « la corrélation entre cantine et présence » attribue à des enfants une relation qui est celle des écoles.

print(f"student level r = {np.corrcoef(per_student['feeding_programme'], per_student['rate'])[0,1]:.3f}")
print(f"school level  r = {np.corrcoef(per_school['feeding_programme'], per_school['rate'])[0,1]:.3f}")
cor(as.numeric(per_student$feeding_programme), per_student$rate)
cor(as.numeric(per_school$feeding_programme), per_school$rate)

Les points d’eau. La fonctionnalité est attribuée par point et mesurée par visite. Le cours EAH comptait un point une fois et non ses douze visites, ce qui est cette règle appliquée avant qu’elle ait un nom.

Les cas orientés. Un travailleur social suivant quarante cas est un travailleur social, et comparer des résultats entre travailleurs sur 1 600 cas a pour unité le nombre de travailleurs, quel qu’il soit.

Les mesures répétées sur le même ménage. Trois passages d’enquête sur un ménage font trois lignes et un ménage, et la même correction s’applique.

Lisez le rapport comme une affirmation sur la généralisation

Il y a une raison pour que l’analyse honnête paraisse plus faible, et ce n’est pas une subtilité technique.

Avec 24 écoles, « la cantine augmente-t-elle la présence » est répondu par quinze écoles qui en ont une contre neuf qui n’en ont pas. Tout ce qui diffère entre ces deux ensembles d’écoles — où elles sont, qui les dirige, pourquoi elles ont été choisies pour le programme — voyage avec la comparaison, et 1 200 élèves n’y changent rien.

Le t de 5,41 au niveau élève est une affirmation sur une étude qui n’a jamais eu lieu. C’est la réponse qu’on obtiendrait si 1 200 enfants avaient chacun été affectés indépendamment à des repas scolaires, ce qui aurait appris bien davantage et aurait été un autre programme.

Rapportez-le en entier

Attendance and school feeding, February to April 2024

  Schools with feeding      90.25%   15 schools
  Schools without           85.04%    9 schools
  Difference +5.21 points, 95% CI 1.6 to 8.8, t = 3.11, Welch df 12.7

  The programme is assigned by school: no school has both fed and unfed
  students, so the analysis has 24 independent units and not 1,200. The
  student-level comparison gives the same effect with t = 5.41; that
  statistic is not reported because it treats 50 children in one school as
  50 independent observations (ICC 0.060, design effect 3.94, effective
  sample size 304).

  The comparison is observational. Schools were not randomised into the
  programme and the difference should not be read as the effect of feeding
  alone.

Le dernier paragraphe coûte deux lignes et c’est celui qu’un relecteur cherchera. Bien choisir l’unité d’analyse rend l’intervalle honnête ; cela ne rend pas la comparaison causale, et le cours d’épidémiologie a établi ce qui le fait.

La suite

La leçon suivante porte sur la corrélation — là où le même problème de grappes fait passer un r de 0,16 à 0,59, et où c’est la phrase écrite sous le nombre qui fait l’essentiel des dégâts.

Animer cette leçon

La leçon en diaporama, la prose étant reléguée dans les notes du présentateur plutôt que projetée. Produit à partir de cette page, dont il ne peut donc pas s'écarter.

Lancer le diaporamaLire les diapositives

Le PDF ne requiert aucun logiciel et se projette depuis n'importe quel poste. Le fichier PowerPoint est fait pour être modifié : appliquez la charte de votre organisation, retirez une section pour une séance plus courte, ou fusionnez deux leçons en atelier.