Leçon 2 sur 8
Unité · Le dénominateur, c'est l'épidémiologie
Des dénominateurs enchaînés
Une cascade est une suite où le dénominateur de chaque étape est le numérateur de la précédente. 1 850 cas de protection deviennent 757 atteignant un service, et le nombre utile n'est pas les 41 % — c'est le maillon qui a le plus perdu.
La forme
La cascade de traitement du VIH est l’exemple le plus connu et la structure est générale.
people living with HIV
-> diagnosed
-> on treatment
-> virally suppressed
Chaque flèche est une proportion, et le dénominateur de chaque étape est l’étape précédente. C’est ce qui en fait une cascade plutôt que quatre indicateurs séparés, et ce qui rend l’arithmétique digne d’attention.
La cascade de soins de la tuberculose a la même forme — cas incidents estimés, notifiés, mis sous traitement, traités avec succès — et c’est aussi celle de tout circuit de référencement, de tout calendrier vaccinal, et de tout programme dont on peut sortir entre deux étapes.
Deux façons d’exprimer chaque étape
import pandas as pd
referrals = pd.read_csv("protection-referrals-2024.v1.csv")
steps = [
("cases", len(referrals)),
("consented", (referrals["consent_to_refer"] == True).sum()),
("referral made", ((referrals["consent_to_refer"] == True)
& (referrals["referral_made"] == True)).sum()),
("reached a service", ((referrals["consent_to_refer"] == True)
& (referrals["referral_made"] == True)
& (referrals["referral_accepted"] == True)).sum()),
]
first = steps[0][1]
previous = None
for name, count in steps:
line = f"{name:20} {count:5} {count / first:6.1%} of all"
if previous:
line += f" {count / previous:6.1%} of previous"
print(line)
previous = count
library(dplyr)
referrals |>
summarise(
cases = n(),
consented = sum(consent_to_refer),
referred = sum(consent_to_refer & referral_made),
reached = sum(consent_to_refer & referral_made & referral_accepted)
)
| Étape | n | Sur le total | Sur la précédente |
|---|---|---|---|
| Cas | 1 850 | 100 % | — |
| Ont consenti au référencement | 1 638 | 88,5 % | 88,5 % |
| Référencement émis | 1 142 | 61,7 % | 69,7 % |
| Ont atteint un service | 757 | 40,9 % | 66,3 % |
Les deux colonnes sont nécessaires et elles disent des choses différentes.
La colonne sur le total est la cascade telle qu’on la dessine d’ordinaire — un escalier descendant de 100 % à 41 %. Elle dit à un bailleur quelle part de la charge de cas aboutit.
La colonne sur la précédente est celle qui identifie le problème. La plus grosse perte unique se situe à « référencement émis » : 30,3 % des personnes ayant consenti n’ont jamais vu de référencement émis. C’est une étape sous le contrôle de l’organisation, et elle est invisible dans la première colonne, où la chute de 88,5 % à 61,7 % ressemble à celle de 61,7 % à 40,9 %.
Classez par perte conditionnelle, agissez sur la plus grande.
losses = pd.DataFrame(steps, columns=["step", "n"])
losses["lost"] = losses["n"].shift(1) - losses["n"]
losses["conditional_loss"] = losses["lost"] / losses["n"].shift(1)
print(losses.dropna().sort_values("conditional_loss", ascending=False))
tibble::tibble(step = c("consented", "referred", "reached"),
n = c(1638, 1142, 757), previous = c(1850, 1638, 1142)) |>
mutate(conditional_loss = (previous - n) / previous) |>
arrange(desc(conditional_loss))
Le premier dénominateur est le difficile
Toute cascade a une étape qui ne se compte pas, seulement s’estime, et c’est toujours la première.
- VIH — les personnes vivant avec le VIH. Personne ne les compte ; le chiffre vient d’un modèle, et tout « % diagnostiqués » hérite de l’incertitude de ce modèle.
- Tuberculose — les cas incidents estimés. De même, et l’écart entre estimés et notifiés est le constat phare de la plupart des rapports nationaux.
- Protection — les personnes ayant subi un incident. Inconnaissable, et la raison pour laquelle la cascade de référencement démarre ici aux cas connus du système plutôt qu’au besoin.
print("cascade base: cases known to the system, not people in need")
# The base of this cascade is cases the system saw. It is not incidence.
Dites quelle base vous avez employée. Une cascade partant d’un besoin estimé et une partant des cas déjà dans le système se ressemblent et veulent dire des choses complètement différentes — la première mesure si le système atteint les gens, la seconde seulement s’il les traite.
La cascade vaccinale
L’extraction vaccinale en porte une, et elle n’exige aucun dénominateur modélisé puisque chaque étape est comptée.
vax = pd.read_csv("vaccination-coverage-2024.v1.csv")
reported = vax[vax["report_submitted"] == True]
doses = reported.groupby("antigen")["doses_administered"].sum()
for first_dose, last_dose in [("penta1", "penta3"), ("mcv1", "mcv2")]:
dropout = (doses[first_dose] - doses[last_dose]) / doses[first_dose]
print(f"{first_dose} -> {last_dose}: {doses[first_dose]:,} -> "
f"{doses[last_dose]:,}, dropout {dropout:.1%}")
doses <- vax |> filter(report_submitted) |>
summarise(doses = sum(doses_administered), .by = antigen)
Penta1 vers penta3 : 13,8 % d’abandon. Première vers seconde dose de rougeole : 22,9 %.
Deux choses que cela rend possible et qu’un taux de couverture ne permet pas.
C’est robuste au dénominateur. Numérateur et dénominateur viennent des mêmes formations aux mêmes mois, si bien qu’une projection de recensement vieille de neuf ans ne peut pas le déplacer. Le cours sur la conception d’indicateurs avançait cet argument ; ici c’est la raison pour laquelle l’abandon est souvent le meilleur indicateur à rapporter.
Cela mesure le mécanisme. La couverture monte si davantage d’enfants commencent ; l’abandon ne baisse que si davantage d’enfants qui commencent terminent aussi, ce que change réellement une intervention de rappel ou de recherche des perdus de vue.
Surveillez le sens de l’abandon
if doses["penta3"] > doses["penta1"]:
print("negative dropout: check the denominator and the period")
if (doses$doses[doses$antigen == "penta3"] > doses$doses[doses$antigen == "penta1"])
warning("negative dropout")
Un abandon négatif — plus de troisièmes doses que de premières — est arithmétiquement possible et signifie toujours quelque chose de précis : une campagne de rattrapage, un problème de bornes de période, ou des enfants vaccinés ailleurs pour la dose antérieure. Ce n’est pas une erreur à ramener à zéro, c’est un constat à expliquer.
Dessinez-la, mais rapportez le tableau
Un graphique en cascade est l’une des rares visualisations par défaut réellement bonnes de ce secteur — les barres descendantes rendent les pertes immédiates. Mais le graphique ne montre que la colonne sur le total, alors publiez le tableau à côté avec les deux.
cascade = pd.DataFrame({
"step": ["Cases", "Consented", "Referral made", "Reached service"],
"n": [1850, 1638, 1142, 757],
})
cascade["of_all"] = cascade["n"] / cascade["n"].iloc[0]
cascade["of_previous"] = cascade["n"] / cascade["n"].shift(1)
cascade["base"] = "cases known to the system"
tibble::tribble(
~step, ~n,
"Cases", 1850,
"Consented", 1638,
"Referral made", 1142,
"Reached", 757
) |> mutate(of_all = n / first(n), of_previous = n / lag(n))
Quatre colonnes, et la dernière — la base — est ce qui empêche un lecteur de comparer votre cascade à celle de quelqu’un d’autre partie d’ailleurs.
La suite
Une cascade est un ensemble de décomptes déjà agrégés. L’unité suivante revient aux enregistrements individuels — une liste linéaire d’épidémie, où l’analyse commence par mettre 975 cas dans l’ordre du temps et demander quelle forme ils dessinent.