cassionData Analysis

Back to the lessonLesson 2 of 8The denominator is the epidemiology

Denominators chained together

The same deck as the downloads, rendered as a page. Start the slideshow to present it full screen — arrow keys or a click advance one slide, Escape leaves.

Slides · PDFSlides · PowerPoint

  1. Slide 1 / 22

    What this lesson covers

    • The shape
    • Two ways to express each step
    • The first denominator is the hard one
    • The immunisation cascade
    • Watch the direction of the dropout
    • Draw it, but report the table
    • What comes next
    Speaker notes
    A cascade is a sequence in which each step's denominator is the previous step's numerator. 1,850 protection cases become 757 reaching a service, and the useful number is not the 41% — it is which link lost the most.
  2. Slide 2 / 22

    The shape — Example

    people living with HIV
      -> diagnosed
        -> on treatment
          -> virally suppressed
    Speaker notes
    The HIV treatment cascade is the best-known example and the structure is general: Each arrow is a proportion, and each step's denominator is the step before it. That is what makes it a cascade rather than four separate indicators, and it is what makes the arithmetic worth doing carefully. The TB care cascade has the same shape — estimated incident cases, notified, started on treatment, successfully treated — and so does every referral pathway, every immunisation schedule, and every programme where people can leave between steps.
  3. Slide 3 / 22

    Two ways to express each step — In Python (cont.)

    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
  4. Slide 4 / 22

    Two ways to express each step — In Python (cont.)

    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
  5. Slide 5 / 22

    Two ways to express each step — In R

    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)
      )
  6. Slide 6 / 22

    Two ways to express each step

    StepnOf allOf previous
    Cases1,850100%—
    Consented to referral1,63888.5%88.5%
    Referral made1,14261.7%69.7%
    Reached a service75740.9%66.3%
  7. Slide 7 / 22

    Two ways to express each step

    • Both columns are needed and they say different things
    • Rank by conditional loss, act on the largest
    Speaker notes
    Both columns are needed and they say different things. The of all column is the cascade as usually drawn — a staircase descending from 100% to 41%. It tells a donor how much of the caseload completes. The of previous column is the one that identifies the problem. The largest single loss is at "referral made": 30.3% of people who consented never had a referral issued. That is a step inside the organisation's control, and it is invisible in the first column, where the drop from 88.5% to 61.7% looks similar to the drop from 61.7% to 40.9%. Rank by conditional loss, act on the largest.
  8. Slide 8 / 22

    Two ways to express each step — In Python

    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))
  9. Slide 9 / 22

    Two ways to express each step — In R

    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))
  10. Slide 10 / 22

    The first denominator is the hard one

    • HIV: people living with HIV. Nobody counts them; the figure comes from a model, and every "% diagnosed" inherits…
    • TB: estimated incident cases. Same, and the gap between estimated and notified is the headline finding of most…
    • Protection: people who experienced an incident. Unknowable, and the reason the referral cascade here starts at…
    Speaker notes
    Every cascade has one step that cannot be counted, only estimated, and it is always the first.
  11. Slide 11 / 22

    The first denominator is the hard one — In Python

    print("cascade base: cases known to the system, not people in need")
  12. Slide 12 / 22

    The first denominator is the hard one — In R

    # The base of this cascade is cases the system saw. It is not incidence.
  13. Slide 13 / 22

    The first denominator is the hard one

    • Say which base you used — A cascade starting from an estimated need and one starting from cases already in the system…
    Speaker notes
    Say which base you used. A cascade starting from an estimated need and one starting from cases already in the system look identical and mean completely different things — the first measures whether the system reaches people, the second only whether it processes them.
  14. Slide 14 / 22

    The immunisation cascade — In Python

    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%}")
    Speaker notes
    The vaccination extract carries one, and it needs no modelled denominator because every step is counted.
  15. Slide 15 / 22

    The immunisation cascade — In R

    doses <- vax |> filter(report_submitted) |>
      summarise(doses = sum(doses_administered), .by = antigen)
  16. Slide 16 / 22

    The immunisation cascade

    • Penta1 to penta3: 13.8% dropout. Measles first to second dose: 22.9%
    • It is robust to the denominator — Numerator and denominator come from the same facilities in the same months, so a…
    • It measures the mechanism — Coverage rises if more children start; dropout falls only if more children who start also…
    Speaker notes
    Penta1 to penta3: 13.8% dropout. Measles first to second dose: 22.9%. Two things that makes possible and a coverage figure does not. It is robust to the denominator. Numerator and denominator come from the same facilities in the same months, so a census projection nine years old cannot move it. The indicator design course made this argument; here it is the reason dropout is often the better indicator to report. It measures the mechanism. Coverage rises if more children start; dropout falls only if more children who start also finish, which is what a defaulter-tracing or reminder intervention actually changes.
  17. Slide 17 / 22

    Watch the direction of the dropout — In Python

    if doses["penta3"] > doses["penta1"]:
        print("negative dropout: check the denominator and the period")
  18. Slide 18 / 22

    Watch the direction of the dropout — In R

    if (doses$doses[doses$antigen == "penta3"] > doses$doses[doses$antigen == "penta1"])
      warning("negative dropout")
    Speaker notes
    A negative dropout — more third doses than first — is arithmetically possible and always means something specific: a catch-up campaign, a period boundary problem, or children vaccinated elsewhere for the earlier dose. It is not an error to be clipped to zero; it is a finding to be explained.
  19. Slide 19 / 22

    Draw it, but report the table — In Python

    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"
    Speaker notes
    A cascade chart is one of the few genuinely good default visualisations in this sector — the descending bars make the losses immediate. But the chart shows only the of all column, so publish the table beside it with both.
  20. Slide 20 / 22

    Draw it, but report the table — In R

    tibble::tribble(
      ~step,             ~n,
      "Cases",         1850,
      "Consented",     1638,
      "Referral made", 1142,
      "Reached",        757
    ) |> mutate(of_all = n / first(n), of_previous = n / lag(n))
    Speaker notes
    Four columns, and the last one — the base — is what stops a reader comparing your cascade to somebody else's that started somewhere different.
  21. Slide 21 / 22

    What comes next

    • A cascade is a set of counts already aggregated.
    Speaker notes
    A cascade is a set of counts already aggregated. The next unit goes back to individual records: an outbreak line list, where the analysis starts by putting 975 cases in time order and asking what shape they make.
  22. Slide 22 / 22

    Where this goes next

    Read the full lesson, with runnable code Back to the lesson