Leçon 6 sur 8
Unité · Comparer des lieux
La moitié de l'écart tenait à qui habite là
Le taux d'attaque brut de Nord vaut 7,94 pour 1 000 et celui de Sud 5,51. Standardisés sur une population commune ils valent 7,25 et 5,95, si bien que l'écart tombe de 2,43 à 1,30 — et la moitié disparue était la structure par âge.
Deux districts ne sont pas comparables tels quels
La leçon 4 a posé trois taux d’attaque bruts côte à côte et la comparaison paraissait simple. Elle ne l’est pas, et la raison est dans le fichier de population.
import pandas as pd
population = pd.read_csv("district-population-2024.v1.csv")
structure = (
population.pivot(index="district", columns="age_band", values="population")
)
shares = structure.div(structure.sum(axis=1), axis=0)
print((shares * 100).round(1))
library(dplyr)
population |>
mutate(share = population / sum(population), .by = district) |>
tidyr::pivot_wider(id_cols = district, names_from = age_band, values_from = share)
| District | 0-4 | 5-14 | 15-44 | 45+ |
|---|---|---|---|---|
| Nord | 22 % | 30 % | 35 % | 13 % |
| Centre | 15 % | 25 % | 42 % | 18 % |
| Sud | 11 % | 20 % | 44 % | 25 % |
Nord compte deux fois plus de moins de cinq ans que Sud, et la leçon 4 a établi que les moins de cinq ans ont trois à quatre fois le taux d’attaque des adultes en âge de travailler.
Nord aurait donc un taux brut plus élevé que Sud même si chaque taux par âge des deux districts était identique. La comparaison brute mélange deux choses — à quel point chaque district est risqué, et qui y habite.
La standardisation directe
La méthode tient en une ligne d’arithmétique appliquée systématiquement — calculez les taux par âge de chaque district, puis appliquez-les à une seule population commune.
cases = pd.read_csv("cholera-line-list-2024.v1.csv")
observed = (
cases.groupby(["district", "age_band"]).size().rename("cases").to_frame()
.join(population.set_index(["district", "age_band"])["population"])
)
observed["rate"] = observed["cases"] / observed["population"]
standard = population.groupby("age_band")["population"].sum()
standard_share = standard / standard.sum()
standardised = (
observed["rate"].unstack() # district x age_band
.mul(standard_share, axis=1).sum(axis=1)
)
crude = (
cases.groupby("district").size()
/ population.groupby("district")["population"].sum()
)
comparison = pd.DataFrame({
"crude_per_1000": 1000 * crude,
"standardised_per_1000": 1000 * standardised,
})
comparison["difference"] = (
comparison["standardised_per_1000"] - comparison["crude_per_1000"]
)
print(comparison.round(2))
observed <- cases |> count(district, age_band, name = "cases") |>
left_join(population, by = c("district", "age_band")) |>
mutate(rate = cases / population)
standard <- population |> summarise(pop = sum(population), .by = age_band) |>
mutate(share = pop / sum(pop))
observed |>
left_join(standard, by = "age_band") |>
summarise(standardised = sum(rate * share), .by = district)
| District | Brut | Standardisé | Écart |
|---|---|---|---|
| Nord | 7,94 | 7,25 | −0,69 |
| Centre | 6,47 | 6,60 | +0,13 |
| Sud | 5,51 | 5,95 | +0,44 |
Nord baisse, Sud monte, et l’écart entre eux se resserre de 2,43 à 1,30 pour 1 000. Environ la moitié de la différence apparente entre le pire et le meilleur district était une structure par âge et non un risque.
Les 1,30 restants sont réels. Nord est réellement plus touché — ses taux par âge sont plus élevés dans trois bandes sur quatre — et c’est la standardisation qui permet de le dire plutôt que de l’affirmer.
Ce qu’est la population standard, et pourquoi elle compte
Le standard est la population à laquelle vous appliquez les taux de chaque district. Trois choix courants.
- La population combinée de l’étude, comme ci-dessus. Simple, défendable, et interne — les taux standardisés sont comparables entre eux et à rien d’autre.
- Une population nationale. Permet la comparaison avec d’autres districts standardisés de la même façon.
- Un standard mondial publié — la population type de l’OMS ou de Segi. Permet la comparaison internationale, et produit des nombres qui ne ressemblent en rien aux taux bruts.
Énoncez le standard. Un taux standardisé ne s’interprète que face à d’autres standardisés sur la même population, et deux rapports employant des standards différents produisent des nombres incomparables qui ont tous deux l’air officiel.
print("Standard: combined population of the three districts, 145,000")
# Say it in the table caption, every time.
La standardisation indirecte, et quand elle est nécessaire
La standardisation directe exige des taux par âge pour chaque district, ce qui exige assez de cas dans chaque cellule. Là où un district compte quatre cas dans une bande, son taux par âge est instable et la méthode directe propage cette instabilité.
La méthode indirecte inverse le problème — appliquer un jeu de taux standard à la population propre de chaque district, et comparer les cas observés aux attendus.
standard_rates = observed.groupby("age_band").apply(
lambda g: g["cases"].sum() / g["population"].sum()
)
expected = (
population.assign(rate=population["age_band"].map(standard_rates))
.assign(expected=lambda d: d["population"] * d["rate"])
.groupby("district")["expected"].sum()
)
smr = cases.groupby("district").size() / expected
print(smr.round(3))
standard_rates <- observed |>
summarise(rate = sum(cases) / sum(population), .by = age_band)
population |>
left_join(standard_rates, by = "age_band") |>
summarise(expected = sum(population * rate), .by = district) |>
left_join(count(cases, district, name = "observed"), by = "district") |>
mutate(smr = observed / expected)
Le résultat est un rapport standardisé de morbidité — observés sur attendus, où 1,0 signifie que le district a exactement les cas que sa structure par âge prédit. Au-dessus de 1 c’est pire qu’attendu, en dessous c’est mieux.
Employez la standardisation indirecte quand les cellules sont petites, et dites quelle méthode vous avez employée — les deux répondent à des questions légèrement différentes et leurs nombres ne sont pas interchangeables.
L’âge n’est pas le seul facteur de confusion
La standardisation retire la variable sur laquelle vous standardisez, et rien d’autre. Les taux d’attaque du choléra varient aussi avec la source d’eau, la densité de population, la distance à un centre de traitement et le statut de déplacement, et aucun de ces facteurs n’est dans le fichier de population.
Standardiser sur l’âge puis affirmer que la différence restante est la performance du programme est l’erreur que cette leçon rend possible, et la leçon suivante lui est entièrement consacrée.
Rapportez la paire
Cholera attack rate by district, weeks 1-16
District Crude Age-standardised Population
Nord 7.94 7.25 48,000
Centre 6.47 6.60 62,000
Sud 5.51 5.95 35,000
Standardised directly to the combined population of the three districts.
About half the crude Nord-Sud difference is age structure: Nord has 22% of
its population under five against Sud's 11%, and under-fives have three to
four times the attack rate of adults aged 15-44.
Les deux colonnes, le standard nommé, et une phrase disant de combien cela a bougé et pourquoi. Un tableau qui n’a que la colonne standardisée cache qu’un ajustement a eu lieu ; un tableau qui n’a que la colonne brute invite à une comparaison à moitié démographique.
La suite
La standardisation a retiré une explication alternative. La leçon suivante liste les autres — et demande à quoi une différence entre deux districts a le droit d’être attribuée quand aucune d’elles ne peut être retirée.