cassionAnalyse de données

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.

PythonR75 minDéfinitions d'indicateurs de l'UNICEFObjectifs de développement durable (ODD)Norme humanitaire fondamentale (CHS)

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é.

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.