cassionAnalyse de données

Leçon 3 sur 8

Unité · Une épidémie, un cas à la fois

La courbe n'est pas l'épidémie

975 cas, 47 sans date de début, et un district dont le registre est rempli après coup depuis le cahier d'admission — si bien que son délai médian d'admission affiche zéro alors que sa létalité est la plus élevée des trois.

PythonR135 minStandards SphèreDéfinitions d'indicateurs de l'UNICEF

À quoi sert la courbe

Une courbe épidémique, ce sont les cas par date de début des symptômes. C’est la première chose que l’on trace dans toute épidémie et elle répond à trois questions qu’aucun tableau ne règle aussi vite — cela croît-il, où est le pic, et la forme est-elle compatible avec une source ponctuelle ou avec une transmission interhumaine ?

C’est aussi l’analyse la plus facilement cassée par un champ manquant, ce qui est le véritable sujet de cette leçon.

Tracez-la

import pandas as pd

cases = pd.read_csv("cholera-line-list-2024.v1.csv", parse_dates=["onset_date"])

with_onset = cases[cases["onset_date"].notna()]
print(f"{len(with_onset)} of {len(cases)} cases have an onset date")

curve = (
    with_onset.assign(week=with_onset["onset_date"].dt.to_period("W").dt.start_time)
    .groupby("week").size()
)
print(curve)
print(f"peak week {curve.idxmax().date()} with {curve.max()} cases")
library(dplyr)

cases <- readr::read_csv("cholera-line-list-2024.v1.csv")

curve <- cases |>
  filter(!is.na(onset_date)) |>
  mutate(week = lubridate::floor_date(onset_date, "week")) |>
  count(week)

928 des 975 cas portent une date de début, et la courbe culmine à la septième semaine — celle qui s’achève le 23 juin — avec 111 cas. Par date de début, non d’admission, non de saisie — tout l’objet de la courbe est le moment où les gens sont tombés malades, et chacune des deux autres dates est décalée d’un montant différent et inconnu.

Employez des pas hebdomadaires pour une épidémie de cette longueur. Le journalier est trop bruité pour être lu ; le mensuel masque entièrement le pic.

Ce que font les dates manquantes

Quarante-sept cas n’ont pas de date de début, et ils ne sont pas dispersés au hasard.

missing = cases[cases["onset_date"].isna()]
print(missing.groupby("district").size())
print(f"{len(missing) / len(cases):.1%} of all cases")
cases |> filter(is.na(onset_date)) |> count(district)

Trois traitements possibles, et ils donnent trois courbes différentes.

Les exclure. Le défaut, et généralement le bon. La courbe porte alors sur 928 cas et le libellé doit le dire, car 975 apparaîtra ailleurs dans le même rapport.

Les imputer d’après la date d’admission. Tentant, et cela décale toute la courbe vers la droite du délai médian, ce qui aplatit la montée et déplace le pic apparent.

Les tracer en bande séparée. Honnête et rarement fait — une barre empilée avec « début inconnu » dans une teinte distincte, placée à la semaine d’admission, pour que le lecteur voie à la fois la courbe et son incertitude.

imputed = cases.copy()
fallback = pd.to_datetime(imputed["admission_date"], errors="coerce")
imputed["onset_or_admission"] = imputed["onset_date"].fillna(fallback)

both = pd.DataFrame({
    "excluded": curve,
    "imputed": (imputed.assign(
        week=imputed["onset_or_admission"].dt.to_period("W").dt.start_time)
        .groupby("week").size()),
}).fillna(0).astype(int)
print(both)
cases |>
  mutate(onset_or_admission = coalesce(onset_date, admission_date),
         week = lubridate::floor_date(onset_or_admission, "week")) |>
  count(week)

Quel que soit votre choix, énoncez le dénominateur sur le graphique. « n = 928 des 975 cas avec une date de début enregistrée » fait une ligne et c’est la différence entre une courbe et une affirmation.

Le district dont la courbe n’est pas une courbe

Vient maintenant le constat pour lequel ce jeu de données a été bâti.

delay = (
    pd.to_datetime(cases["admission_date"], errors="coerce") - cases["onset_date"]
).dt.days

by_district = cases.assign(delay=delay).dropna(subset=["delay"]).groupby("district")
print(by_district["delay"].median())
print((by_district["delay"].apply(lambda s: (s == 0).mean()) * 100).round(0))
cases |>
  filter(!is.na(onset_date), !is.na(admission_date)) |>
  mutate(delay = as.integer(admission_date - onset_date)) |>
  summarise(median_delay = median(delay), zero_day = mean(delay == 0), .by = district)
District Délai médian Même jour
Nord 0 jour 80 %
Centre 2 jours 13 %
Sud 2 jours 20 %

Les cas de Nord semblent atteindre le traitement le jour même où ils tombent malades, quatre fois plus souvent que partout ailleurs. Ce n’est pas un système de santé performant. C’est un registre rempli après coup depuis le cahier d’admission — quelqu’un saisit la date d’admission dans les deux champs parce que la date de début n’a jamais été demandée.

Deux conséquences, graves toutes deux.

La courbe épidémique de Nord est une courbe d’admissions. Elle est décalée vers la droite du temps que ses cas ont réellement mis à arriver, si bien que comparer le moment du pic entre districts compare deux quantités différentes.

La statistique de délai de Nord est inutilisable, et c’est celle que la leçon suivante emploie pour expliquer la létalité — où Nord est le pire à 6,30 % contre 2,00 % au Centre. Le district qui paraît avoir l’accès le plus rapide a la mortalité la plus élevée, et la résolution est que l’apparence est un artefact.

print("Nord: median delay 0 days, case fatality 6.30%")
print("Centre: median delay 2 days, case fatality 2.00%")
# The contradiction is the finding: the fastest-looking district dies most.

Une contradiction entre deux indicateurs d’un même registre est une question de données avant d’être une question d’épidémiologie. La règle du cours de nettoyage sur les colonnes qui doivent concorder, arrivant dans un décor où s’y tromper oriente mal une réponse d’épidémie.

Lire la forme

Une fois la courbe digne de confiance, trois lectures sont classiques.

  • Un pic unique et net suggère une source ponctuelle — un point d’eau contaminé, un événement — avec des cas apparaissant à moins d’une période d’incubation les uns des autres.
  • Une série de pics espacés d’environ une période d’incubation suggère une propagation interhumaine.
  • Un long plateau suggère une exposition à source commune persistante, ce qui pour le choléra signifie en général que l’approvisionnement en eau n’a pas été réparé.

Cette épidémie monte sur six semaines et décroît sur dix, ce qui est une forme propagée plutôt qu’une source ponctuelle. Dites quelle lecture vous faites et pourquoi, et soyez prudent — la forme est aussi affectée par le moment où la réponse a démarré, et une intervention efficace tronque la courbe d’une manière qui ressemble à une décroissance naturelle.

Tracez des cas, non des taux

Un dernier point pratique. La courbe épidémique est un décompte dans le temps, non un taux. Diviser par la population rend les districts comparables et détruit ce à quoi sert la courbe — la charge absolue arrivant chaque semaine dans les centres de traitement.

Tracez des effectifs. Mettez les taux dans le tableau à côté, ce qui est la leçon suivante.

La suite

La courbe dit quand. La leçon suivante dit combien et à quel point c’est grave — taux d’attaque avec le dénominateur de population que ce jeu de données livre, et létalité au regard du 1 % sur lequel la réponse est jugée.

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.