Leçon 2 sur 8
Unité · L'échantillon n'est pas la population
Construire les pondérations à partir de la base
Probabilité de sélection à chaque degré, la pondération de base qu'elle implique, l'ajustement pour non-réponse, et le contrôle qui prouve l'ensemble — des pondérations totalisant 56 428, exactement ce que la base déclare.
Une pondération est un nombre avec un seul sens
Une pondération de sondage, c’est le nombre d’unités de la population que représente l’unité tirée.
Cette phrase est toute la leçon. Le reste n’est que l’arithmétique pour y parvenir, et chaque contrôle est une façon de demander si les pondérations veulent toujours dire cela.
La pondération est l’inverse de la probabilité de sélection. Un ménage ayant une chance sur 60 d’être retenu représente 60 ménages ; un ménage ayant une chance sur 18 en représente 18. Rien de plus mystérieux.
Premier degré — probabilité proportionnelle à la taille
Vingt-cinq zones de dénombrement ont été tirées dans chaque strate, avec une probabilité proportionnelle aux ménages que chaque zone contient.
P(area i selected) = n_h * M_i / M_h
où n_h est le nombre de zones tirées dans la strate h (25), M_i les ménages
de la zone i, et M_h les ménages de toute la strate.
import pandas as pd
frame = pd.read_csv("household-survey-frame-2025.v1.csv")
stratum_households = frame.groupby("stratum")["households"].sum()
selected = frame[frame["selected"] == True].copy()
selected["p_stage1"] = (
25 * selected["households"] / selected["stratum"].map(stratum_households)
)
print(selected[["ea_id", "stratum", "households", "p_stage1"]].head())
library(dplyr)
stratum_households <- frame |> summarise(M_h = sum(households), .by = stratum)
selected <- frame |>
filter(selected) |>
left_join(stratum_households, by = "stratum") |>
mutate(p_stage1 = 25 * households / M_h)
Une zone plus grande a plus de chances d’être retenue. C’est tout l’objet du tirage proportionnel à la taille — il place les entretiens là où sont les gens, ce qui évite qu’une enquête dépense un tiers de son budget dans des hameaux.
Second degré — un tirage fixe
Quatorze ménages ont été tirés dans chaque zone retenue, à partir de la liste de cette zone.
P(household selected | area selected) = m / M_i
avec m = 14.
selected["p_stage2"] = 14 / selected["households"]
selected["p_overall"] = selected["p_stage1"] * selected["p_stage2"]
print(selected.groupby("stratum")["p_overall"].describe()[["min", "max"]])
selected <- selected |>
mutate(p_stage2 = 14 / households,
p_overall = p_stage1 * p_stage2)
selected |> summarise(min = min(p_overall), max = max(p_overall), .by = stratum)
Les deux probabilités se compensent
Développez le produit et M_i disparaît.
P(household) = (n_h * M_i / M_h) * (m / M_i) = n_h * m / M_h
Tout ménage d’une strate a la même probabilité globale, quelle que soit la taille de sa zone. Le plan est autopondéré à l’intérieur d’une strate.
Exécutez le code et le minimum et le maximum de p_overall sont identiques dans
chaque strate, ce qui est l’arithmétique qui le confirme. Ce n’est ni une
coïncidence ni une commodité — c’est précisément pourquoi le tirage proportionnel
à la taille va de pair avec un tirage fixe, dans les EDS, les MICS et la
méthodologie SMART. La mesure de taille qui décide des zones visitées est
annulée par celle qui décide du nombre de ménages tirés.
Deux conséquences à retenir. Une pondération qui varie à l’intérieur d’une strate dans un plan proportionnel à la taille signale un problème — en général une base dont la mesure de taille est périmée. Et la pondération de base est une propriété de la strate, non du ménage, si bien qu’elle se calcule en trois nombres.
base_weight = stratum_households / (25 * 14)
print(base_weight.round(1))
stratum_households |> mutate(base_weight = M_h / (25 * 14))
| Strate | Ménages dans la base | Pondération de base |
|---|---|---|
| Urbain | 21 270 | 60,8 |
| Rural accessible | 28 958 | 82,7 |
| Rural reculé | 6 200 | 17,7 |
Un ménage rural reculé en représente dix-huit ; un ménage rural accessible en représente quatre-vingt-trois. Ce rapport est toute la différence entre 32,8 % et 29,1 %.
L’ajustement pour non-réponse
Quatorze ménages ont été tirés par zone. Moins ont été enquêtés — 996 des 1 050 tirés. Les ménages enquêtés doivent porter ceux qui ne l’ont pas été.
survey = pd.read_csv("household-survey-2025.v1.csv")
interviewed = selected.set_index("ea_id")["households_interviewed"]
survey["base_weight"] = survey["stratum"].map(base_weight)
survey["nr_adjustment"] = 14 / survey["ea_id"].map(interviewed)
survey["weight"] = survey["base_weight"] * survey["nr_adjustment"]
print(survey["weight"].describe()[["min", "max"]].round(1))
survey <- survey |>
left_join(select(selected, ea_id, households_interviewed), by = "ea_id") |>
left_join(mutate(stratum_households, base_weight = M_h / (25 * 14)), by = "stratum") |>
mutate(weight = base_weight * 14 / households_interviewed)
range(survey$weight)
Les pondérations vont désormais de 17,7 à 96,5.
Ajustez à l’intérieur de la zone, pas de la strate. Les ménages qui n’ont pas répondu dans une zone donnée ressemblent bien plus aux ménages ayant répondu dans cette zone qu’à la moyenne de la strate. Ajuster au niveau de la strate est plus simple et jette précisément l’information qui rend l’ajustement utile.
L’hypothèse est énoncée et elle n’est pas gratuite — on suppose que les non-répondants ressemblent aux répondants de la même zone. Là où vous avez des raisons d’en douter — des concessions fermées dans un quartier, un incident de sécurité un jour donné — dites-le dans les limites, car aucune pondération n’y remédie.
Le contrôle qui prouve toute la construction
total = survey["weight"].sum()
frame_total = frame["households"].sum()
print(f"weights sum to {total:,.0f}; frame holds {frame_total:,}")
assert abs(total - frame_total) < 1
c(weights = sum(survey$weight), frame = sum(frame$households))
56 428 et 56 428. Les pondérations totalisent exactement le nombre de ménages que la base déclare, ce que « combien d’unités celle-ci représente » doit signifier pour signifier quelque chose.
Faites ce contrôle à chaque fois. Il attrape une strate dont la pondération de base a employé le mauvais dénominateur, un ajustement pour non-réponse appliqué deux fois, et une jointure qui a perdu des zones — trois erreurs autrement invisibles, puisque l’estimation obtenue ressemble toujours à un pourcentage.
Pondérations normalisées, et quand s’en servir
Certains logiciels veulent des pondérations de moyenne un plutôt que totalisant la population.
survey["weight_norm"] = survey["weight"] / survey["weight"].mean()
survey <- survey |> mutate(weight_norm = weight / mean(weight))
Proportions et moyennes sont identiques dans les deux cas — la mise à l’échelle s’annule. Les totaux, non. Une pondération normalisée ne peut pas estimer « combien de ménages sont en insécurité alimentaire », seulement « quelle part ». Gardez la pondération à l’échelle de la population comme référence et dérivez la normalisée là où un outil l’exige.
Pondérations par personne et de sous-échantillon
Deux autres pondérations découlent de la même logique, et les deux sont régulièrement fausses.
Une pondération par personne est la pondération du ménage multipliée par sa taille, car un ménage de huit représente huit fois plus de personnes que de ménages.
people = survey[survey["household_size"].notna()].copy()
people["person_weight"] = people["weight"] * people["household_size"]
print(f"estimated population {people['person_weight'].sum():,.0f}")
people <- survey |>
filter(!is.na(household_size)) |>
mutate(person_weight = weight * household_size)
sum(people$person_weight)
Cela donne 333 336 contre 328 127 dans la liste de base — 1,6 % d’écart. Vingt ménages n’ont pas de taille enregistrée et sortent, et une liste de base est elle-même une estimation faite des mois plus tôt. Un écart de cet ordre est normal et mérite d’être énoncé ; un écart de 20 % signale un problème.
Une pondération de sous-échantillon s’applique quand une unité est choisie parmi plusieurs. Un enfant de moins de cinq ans a été mesuré par ménage, si bien qu’un enfant mesuré dans un ménage comptant trois enfants éligibles en représente trois.
children = survey[survey["child_muac_mm"].notna()].copy()
children["child_weight"] = children["weight"] * children["children_under5"]
children <- survey |>
filter(!is.na(child_muac_mm)) |>
mutate(child_weight = weight * children_under5)
Sautez cette multiplication et les enfants des grands ménages sont sous-représentés — or les grands ménages sont systématiquement plus pauvres, donc le biais va dans un sens prévisible. Sur cette enquête, la malnutrition aiguë par PB est de 13,3 % parmi les enfants mesurés et de 11,4 % une fois les pondérations enfant appliquées.
Enregistrez les pondérations à côté des données
survey[["household_id", "ea_id", "stratum", "base_weight",
"nr_adjustment", "weight"]].to_csv("outputs/weights.csv", index=False)
survey |>
select(household_id, ea_id, stratum, base_weight, households_interviewed, weight) |>
readr::write_csv(here::here("outputs", "weights.csv"))
Conservez les composantes, pas seulement la pondération finale. Quand quelqu’un demandera dans un an pourquoi un ménage compte pour 96 et un autre pour 18, la réponse est dans les colonnes et non dans votre souvenir d’un script.
La suite
Vous savez désormais produire une estimation de population. L’unité suivante demande ce qu’elle a coûté — combien de ménages exige une précision donnée, et pourquoi la formule de taille d’échantillon des manuels donne à peu près la moitié de ce qu’une enquête en grappes réclame réellement.