cassionAnalyse de données

Retour à la leçonLeçon 4 sur 8Une épidémie, un cas à la fois

Taux d'attaque et le 1 % que personne n'atteint

Le même diaporama que les téléchargements, rendu sous forme de page. Lancez le diaporama pour le présenter en plein écran — les flèches ou un clic avancent d'une diapositive, Échap quitte.

Diapositives · PDFDiapositives · PowerPoint

  1. Diapositive 1 / 23

    Ce que couvre cette leçon

    • Le taux d'attaque
    • Taux d'attaque par âge
    • La létalité
    • Le seuil
    • L'explication, et pourquoi elle est indisponible
    • Le bloc de rapportage
    • La suite
    Notes du présentateur
    Le taux d'attaque exige la population ; la létalité exige un dénominateur qui exclut le cas encore hospitalisé. 3,90 % contre une cible de 1 %, 6,30 % dans un district — et l'explication repose sur une statistique de délai que la leçon précédente a montrée cassée.
  2. Diapositive 2 / 23

    Le taux d'attaque — En Python

    import pandas as pd
    
    cases = pd.read_csv("cholera-line-list-2024.v1.csv")
    population = pd.read_csv("district-population-2024.v1.csv")
    
    by_district = (
        cases.groupby("district").size().rename("cases")
        .to_frame()
        .join(population.groupby("district")["population"].sum())
    )
    by_district["per_1000"] = 1000 * by_district["cases"] / by_district["population"]
    print(by_district.round(2))
    Notes du présentateur
    Le taux d'attaque, ce sont les cas sur la population à risque, sur la période de l'épidémie. Malgré son nom c'est une proportion, et il exige le fichier de population que la liste linéaire livre avec elle.
  3. Diapositive 3 / 23

    Le taux d'attaque — En R

    library(dplyr)
    
    cases |> count(district, name = "cases") |>
      left_join(summarise(population, population = sum(population), .by = district),
                by = "district") |>
      mutate(per_1000 = 1000 * cases / population)
  4. Diapositive 4 / 23

    Le taux d'attaque

    DistrictCasPopulationPour 1 000
    Nord38148 0007,94
    Centre40162 0006,47
    Sud19335 0005,51
    Notes du présentateur
    Nord est le plus touché à 7,94 et Sud le moins à 5,51, soit un écart de 2,43 pour 1 000. Retenez ce nombre : la leçon 6 montre que la moitié n'est pas ce qu'elle paraît. Remarquez aussi que les effectifs de cas ne se classent pas comme les taux — Centre a le plus de cas et n'est pas le plus touché. Un effectif répond à « où envoyer les intrants » ; un taux répond à « où le risque est le plus élevé ». Rapportez les deux, et ne laissez jamais un diagramme en barres d'effectifs se lire comme une carte du risque.
  5. Diapositive 5 / 23

    Taux d'attaque par âge — En Python

    by_band = (
        cases.groupby(["district", "age_band"]).size().rename("cases")
        .to_frame()
        .join(population.set_index(["district", "age_band"])["population"])
    )
    by_band["per_1000"] = 1000 * by_band["cases"] / by_band["population"]
    print(by_band["per_1000"].unstack().round(2))
  6. Diapositive 6 / 23

    Taux d'attaque par âge — En R

    cases |> count(district, age_band, name = "cases") |>
      left_join(population, by = c("district", "age_band")) |>
      mutate(per_1000 = 1000 * cases / population) |>
      tidyr::pivot_wider(id_cols = age_band, names_from = district, values_from = per_1000)
  7. Diapositive 7 / 23

    Taux d'attaque par âge

    Tranche d'âgeNordCentreSud
    0-414,7712,6911,69
    5-148,268,066,29
    15-443,934,033,51
    45+6,414,755,71
  8. Diapositive 8 / 23

    Taux d'attaque par âge

    • Les moins de cinq ans sont trois à quatre fois plus touchés que les adultes en âge de travailler — dans chaque district
    Notes du présentateur
    Les moins de cinq ans sont trois à quatre fois plus touchés que les adultes en âge de travailler, dans chaque district. Ce gradient est l'épidémiologie de fond, et c'est aussi la raison pour laquelle la comparaison brute ci-dessus induit en erreur — Nord compte deux fois plus de moins de cinq ans que Sud.
  9. Diapositive 9 / 23

    La létalité — En Python

    with_outcome = cases[cases["outcome"].notna()]
    
    print(f"cases: {len(cases)}, with an outcome: {len(with_outcome)}")
    print(f"CFR: {(with_outcome['outcome'] == 'died').mean():.2%}")
    Notes du présentateur
    La létalité, ce sont les décès parmi les cas. Deux décisions au dénominateur, et les deux ont déjà été prises dans ce cours.
  10. Diapositive 10 / 23

    La létalité — En R

    cases |> filter(!is.na(outcome)) |>
      summarise(n = n(), cfr = mean(outcome == "died"))
  11. Diapositive 11 / 23

    La létalité

    • 3,90 % sur 974 cas à issue enregistrée
    Notes du présentateur
    3,90 % sur 974 cas à issue enregistrée. Un cas était encore hospitalisé à la date d'arrêt et n'a pas d'issue. Ce n'est ni un décès ni une guérison, et le raisonnement est le même que pour les soixante et onze enfants encore en traitement PCIMA dans le cours nutrition — une issue en attente n'est pas une issue, et le dénominateur doit dire lequel il a employé. Ici cela ne déplace rien ; dans une épidémie encore en cours cela déplace beaucoup.
  12. Diapositive 12 / 23

    Le seuil — En Python

    TARGET = 0.01
    cfr = (with_outcome["outcome"] == "died").mean()
    print(f"CFR {cfr:.2%} against a target of {TARGET:.0%}: "
          f"{'meets' if cfr < TARGET else 'does not meet'} the standard")
    
    by_district_cfr = with_outcome.groupby("district")["outcome"].apply(
        lambda s: (s == "died").mean()
    )
    print((by_district_cfr * 100).round(2))
    Notes du présentateur
    Sphere et l'OMS retiennent une létalité sous 1 % comme marque d'une réponse choléra bien conduite. Le choléra non traité tue une grande part des cas sévères ; traité rapidement par réhydratation orale il ne tue presque personne. Le seuil est donc un énoncé sur l'accès au traitement plutôt que sur l'agent pathogène.
  13. Diapositive 13 / 23

    Le seuil — En R

    cases |> filter(!is.na(outcome)) |>
      summarise(cfr = mean(outcome == "died"), n = n(), .by = district)
  14. Diapositive 14 / 23

    Le seuil

    DistrictLétalitén
    Nord6,30 %381
    Sud3,11 %193
    Centre2,00 %400
    Notes du présentateur
    Tous les districts dépassent 1 %, et Nord de plus de six fois. C'est le constat de l'épidémie — non le taux d'attaque, qui est un fait sur l'exposition, mais la létalité, qui est un fait sur la réponse.
  15. Diapositive 15 / 23

    L'explication, et pourquoi elle est indisponible — En Python

    delay = (
        pd.to_datetime(cases["admission_date"], errors="coerce")
        - pd.to_datetime(cases["onset_date"], errors="coerce")
    ).dt.days
    
    testable = cases.assign(delay=delay).dropna(subset=["delay", "outcome"])
    banded = testable.assign(
        band=pd.cut(testable["delay"], [-1, 1, 3, 99], labels=["0-1", "2-3", "4+"])
    )
    print(banded.groupby("band")["outcome"].apply(lambda s: (s == "died").mean()).round(3))
    Notes du présentateur
    L'explication classique d'une létalité élevée du choléra est le délai d'accès au traitement, et la liste linéaire a les champs pour la tester.
  16. Diapositive 16 / 23

    L'explication, et pourquoi elle est indisponible — En R

    cases |>
      filter(!is.na(onset_date), !is.na(admission_date), !is.na(outcome)) |>
      mutate(delay = as.integer(admission_date - onset_date),
             band = cut(delay, c(-1, 1, 3, 99), labels = c("0-1", "2-3", "4+"))) |>
      summarise(cfr = mean(outcome == "died"), n = n(), .by = band)
  17. Diapositive 17 / 23

    L'explication, et pourquoi elle est indisponible

    • La raison de fond — l'admission elle-même est la protection — Les cas jamais admis portent l'essentiel de la mortalité,…
    • Celle qui n'en est pas — le délai de Nord est fabriqué par son registre — La leçon précédente a trouvé 80 % de ses cas…
    Notes du présentateur
    Parmi les cas admis, le gradient existe mais reste modeste — environ 1,9 % de zéro à trois jours et 3,3 % à quatre jours ou plus. Il est plus faible que l'écart entre districts, et il y a deux raisons à cela, l'une de fond et l'autre non. La raison de fond — l'admission elle-même est la protection. Les cas jamais admis portent l'essentiel de la mortalité, et la variable de délai n'existe que pour ceux qui ont été admis. Comparer des bandes de délai à l'intérieur des cas admis conditionne sur ce qui compte le plus. Celle qui n'en est pas — le délai de Nord est fabriqué par son registre. La leçon précédente a trouvé 80 % de ses cas avec début égal à admission. Le district ayant la plus forte létalité rapporte donc le délai le plus court, et la comparaison qui expliquerait sa mortalité est précisément celle que ses données ne peuvent pas soutenir.
  18. Diapositive 18 / 23

    L'explication, et pourquoi elle est indisponible — En Python

    print("Nord CFR 6.30% with a median recorded delay of 0 days.")
    print("The delay is a register artefact; the CFR is not.")
  19. Diapositive 19 / 23

    L'explication, et pourquoi elle est indisponible — En R

    # One of these two numbers is real. Say which, in the report.
  20. Diapositive 20 / 23

    L'explication, et pourquoi elle est indisponible

    • Consignez cela comme une limite, non comme un résultat — « La létalité est la plus élevée à Nord ; le délai entre début…
    Notes du présentateur
    Consignez cela comme une limite, non comme un résultat. « La létalité est la plus élevée à Nord ; le délai entre début et admission qui permettrait de tester l'explication habituelle n'y est pas fiable, car 80 % de ses cas enregistrent début et admission le même jour » est la phrase honnête, et elle engendre aussi l'action corrective — réparer le registre — qu'une explication fabriquée ne produirait pas.
  21. Diapositive 21 / 23

    Le bloc de rapportage — Exemple

    Cholera outbreak, weeks 1-16
    
      Cases                      975      attack rate 6.7 per 1,000 (145,000 population)
      Attack rate by district    7.94 / 6.47 / 5.51 per 1,000 (Nord / Centre / Sud)
                                 crude; see standardised rates before comparing
    
      Deaths                      38      case fatality 3.90%
      Denominator                974      one case still admitted at cut-off, excluded
      Against Sphere target       1%      not met in any district
    
      Highest CFR: Nord at 6.30% (n=381). Onset-to-admission delay is not usable
      in Nord (80% of cases record onset = admission), so the usual explanation
      cannot be tested there from this register.
    Notes du présentateur
    Dix lignes, et ce sont les trois dernières qui en font un constat plutôt qu'un tableau.
  22. Diapositive 22 / 23

    La suite

    • Les taux d'attaque par district ressemblent à une comparaison et n'en sont pas encore une, car les districts ne contiennent pas les mêmes gens.
    Notes du présentateur
    Les taux d'attaque par district ressemblent à une comparaison et n'en sont pas encore une, car les districts ne contiennent pas les mêmes gens. L'unité suivante y remédie, après avoir d'abord réglé une question de couverture ouverte depuis le module 2.
  23. Diapositive 23 / 23

    La suite

    Lire la leçon complète, avec le code exécutable Retour à la leçon