Leçon 4 sur 8
Unité · Périodes et complétude
Le taux de rapportage est un dénominateur, pas une note de bas de page
Le système calcule quatre mesures de complétude et elles divergent. Celle que cite votre rapport décide si un taux de couverture est une estimation ou une borne inférieure, et août vaut 29 % dans les deux cas.
Quatre mesures, un seul mot
Le cours Évaluation de la qualité des données a établi que la complétude du rapportage est un indicateur à part entière. Cette leçon porte sur ce que le système entend par là, car DHIS2 expose quatre mesures voisines sous des noms employés indifféremment.
| Mesure | Numérateur | Dénominateur |
|---|---|---|
| Taux de rapportage | Ensembles marqués complets | Ensembles attendus |
| Rapports effectifs | Ensembles marqués complets | — (un décompte) |
| Rapports attendus | — | Unités affectées × périodes |
| Taux de rapportage à temps | Complétés avant l’échéance | Ensembles attendus |
Le premier et le dernier ne diffèrent que par la promptitude, et un rapport citant « le taux de rapportage » sans préciser lequel laisse le lecteur incapable de dire si 82 % veut dire « transmis » ou « transmis à temps ».
import pandas as pd
vax = pd.read_csv("vaccination-coverage-2024.v1.csv", parse_dates=["period"])
vax["reported"] = vax["report_submitted"] == True
facility_months = vax.drop_duplicates(["facility_id", "period"])
print(f"expected: {len(facility_months)}")
print(f"actual: {facility_months['reported'].sum()}")
print(f"rate: {facility_months['reported'].mean():.1%}")
library(dplyr)
vax |>
distinct(facility_id, period, .keep_all = TRUE) |>
summarise(expected = n(), actual = sum(report_submitted),
rate = mean(report_submitted))
456 couples formation-mois attendus, 349 transmis, 76,5 %. Notez le dédoublonnage — la complétude est enregistrée par ensemble de données, unité et période, non par élément de données, si bien que compter les 2 736 lignes au niveau élément répondrait à une autre question et donnerait le même pourcentage par coïncidence.
Là où la réponse du système diffère de la vôtre
Deux différences, et les deux ont pris des gens en défaut.
Les rapports attendus comptent l’affectation, non l’existence. Une formation fermée en mars reste au dénominateur le reste de l’année tant que personne ne désaffecte l’ensemble de données ou ne ferme l’unité. Les taux de rapportage dérivent donc vers le bas à mesure que des formations ferment sans mise à jour des affectations, et cette dérive ressemble à une performance qui se dégrade.
La complétude est un clic, non un calcul. Une formation peut saisir toutes ses valeurs sans jamais marquer le formulaire complet ; le système la compte comme non déclarante alors que les données sont là. L’inverse arrive aussi. Le taux de rapportage et la présence de données sont donc deux questions différentes.
has_data = (
vax.groupby(["facility_id", "period"])["doses_administered"]
.sum().gt(0).rename("has_data")
)
flags = facility_months.set_index(["facility_id", "period"])["reported"]
crosstab = pd.crosstab(flags, has_data.reindex(flags.index))
print(crosstab)
vax |>
summarise(reported = first(report_submitted),
has_data = sum(doses_administered) > 0,
.by = c(facility_id, period)) |>
count(reported, has_data)
Sur cette extraction les deux concordent exactement — tout mois rapporté a des doses et tout mois non rapporté n’en a aucune — ce qui est le cas net. Faites le tableau croisé quand même. Les cellules hors diagonale sont là où une instance réelle garde ses problèmes les plus intéressants, et une cellule « marqué complet, aucune donnée » est une formation qui transmet des formulaires vides pour atteindre une cible.
Le taux de rapportage comme dénominateur de tout le reste
Le point opérationnel, et la raison pour laquelle cette leçon figure dans un cours DHIS2 et pas seulement dans celui d’évaluation.
monthly = (
vax[vax["antigen"] == "penta3"]
.groupby(vax["period"].dt.strftime("%Y-%m"))
.apply(lambda g: pd.Series({
"reporting_rate": g["reported"].mean(),
"coverage_reporting": (g.loc[g["reported"], "doses_administered"].sum()
/ g.loc[g["reported"], "target_population"].sum()),
}))
)
print((monthly * 100).round(1))
vax |>
filter(antigen == "penta3") |>
mutate(month = format(period, "%Y-%m")) |>
summarise(
reporting_rate = mean(report_submitted),
coverage = sum(doses_administered[report_submitted]) /
sum(target_population[report_submitted]),
.by = month
)
| Mois | Taux de rapportage | Couverture parmi les déclarants |
|---|---|---|
| Juillet | 82 % | … |
| Août | 29 % | … |
| Septembre | 45 % | … |
| Octobre | 95 % | … |
Deux colonnes, toujours adjacentes. Le taux de couverture d’août est calculé sur onze des trente-huit formations, et quoi qu’il dise, il le dit de ces onze-là.
Trois règles en découlent, et elles sont le livrable de cette leçon.
- En dessous de 90 % de rapportage, un taux de couverture est une borne inférieure, non une estimation. Étiquetez-le « parmi les formations ayant rapporté », à chaque fois.
- Ne comparez pas deux périodes aux taux de rapportage sensiblement différents sans le dire. Août face à juillet compare onze formations à trente et une.
- Une courbe de tendance exige le taux de rapportage sur le même graphique. Tracée seule, une série de couverture traversant août ressemble à un effondrement de programme ; tracée avec la complétude, l’explication est immédiate.
Les deux façons de traiter une formation muette
Aucune n’est fausse. Elles répondent à des questions différentes et doivent être étiquetées.
L’exclure du numérateur et du dénominateur. Couverture parmi les formations ayant rapporté. Honnête, et c’est ce que fait le code ci-dessus.
Imputer ses doses. Remplir à partir des mois récents de la formation, en le disant. C’est ce que font les estimations nationales — WUENIC et exercices similaires imputent plutôt que de laisser des trous, car la question porte sur des enfants et non sur de la paperasse.
by_facility = (
vax[(vax["antigen"] == "penta3") & vax["reported"]]
.groupby("facility_id")["doses_administered"].median()
)
missing = (vax["antigen"] == "penta3") & ~vax["reported"]
imputed_total = (vax.loc[missing, "facility_id"].map(by_facility).sum()
+ vax.loc[(vax["antigen"] == "penta3") & vax["reported"],
"doses_administered"].sum())
annual_target = (vax[vax["antigen"] == "penta3"]
.groupby("facility_id")["target_population"].first().sum())
print(f"imputed annual coverage: {imputed_total / annual_target:.1%}")
median_by_facility <- vax |>
filter(antigen == "penta3", report_submitted) |>
summarise(m = median(doses_administered), .by = facility_id)
N’imputez jamais en silence. Un chiffre imputé et un chiffre rapporté dans la même colonne sans marquage sont le défaut que le cours de nettoyage a nommé — une estimation qu’on ne distingue plus d’une mesure.
Rapportez la paire
summary = pd.DataFrame({
"period": ["2024-08", "2024-09", "2024-10"],
"facilities_reporting": [11, 17, 36],
"facilities_expected": [38, 38, 38],
"reporting_rate": ["29%", "45%", "95%"],
"coverage_basis": ["among reporting facilities"] * 3,
})
tibble::tribble(
~period, ~reporting, ~expected, ~basis,
"2024-08", 11, 38, "among reporting facilities",
"2024-09", 17, 38, "among reporting facilities",
"2024-10", 36, 38, "among reporting facilities"
)
Cinq colonnes, et aucun taux de couverture ne circule sans elles. C’est tout l’argument, et c’est moins cher que la réunion où quelqu’un demande pourquoi août s’est effondré.
La suite
Tout ce qui précède a tourné sur un CSV exporté à la main. L’unité suivante le remplace — l’API Web, ce qu’il faut lui demander, et un script qui fait la même requête chaque mois en consignant ce qu’il a demandé.