cassionData Analysis

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

Rate, ratio, proportion — and person-time

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 / 20

    What this lesson covers

    • Three shapes, one word
    • Prevalence and incidence
    • What person-time adds
    • Say the denominator and the multiplier in the name
    • The at-risk population is not always the whole population
    • The check to run on any rate
    • What comes next
    Speaker notes
    Three shapes that get called "rate" and only one of them is, plus the denominator that makes incidence mean something when people enter and leave a population at different times.
  2. Slide 2 / 20

    Three shapes, one word

    ShapeFormRangeExample
    ProportionPart over whole, both same units0 to 1Case fatality: deaths among cases
    RatioTwo quantities, not nestedanySex ratio of cases; odds ratio
    RateEvents over population-timeany positiveIncidence per 1,000 person-years
    Speaker notes
    The indicator design course established four families of measure. Epidemiology uses three of them constantly and calls all three "rate", which is where most misreading starts.
  3. Slide 3 / 20

    Three shapes, one word

    • Only the third is a rate — and the difference is the time in the denominator
    Speaker notes
    Only the third is a rate, and the difference is the time in the denominator. A proportion asks "what share"; a rate asks "how fast".
  4. Slide 4 / 20

    Prevalence and incidence

    • Prevalence — how many people have the condition now. A proportion, from a survey, a snapshot.
    • Incidence — how many new cases arise per unit of population-time. A rate, from surveillance, a flow.
    Speaker notes
    The pair this whole course turns on. They answer different questions and they move for different reasons. Prevalence rises if incidence rises or if people survive longer with the condition, which is why a successful treatment programme can raise HIV prevalence while lowering incidence — the most misread pair of numbers in this sector.
  5. Slide 5 / 20

    Prevalence and incidence — In Python

    import pandas as pd
    
    cases = pd.read_csv("cholera-line-list-2024.v1.csv", parse_dates=["onset_date"])
    population = pd.read_csv("district-population-2024.v1.csv")
    
    total_population = population["population"].sum()
    print(f"{len(cases)} cases in a population of {total_population:,}")
    print(f"attack rate over the outbreak: {1000 * len(cases) / total_population:.2f} per 1,000")
  6. Slide 6 / 20

    Prevalence and incidence — In R

    library(dplyr)
    
    cases <- readr::read_csv("cholera-line-list-2024.v1.csv")
    population <- readr::read_csv("district-population-2024.v1.csv")
    
    c(cases = nrow(cases),
      population = sum(population$population),
      per_1000 = 1000 * nrow(cases) / sum(population$population))
    Speaker notes
    975 cases in 145,000 people — 6.7 per 1,000 over the outbreak. Strictly that is an attack rate, which is a proportion despite its name: cases over the population at risk, over a defined outbreak period. Lesson 4 takes it seriously.
  7. Slide 7 / 20

    What person-time adds — In Python

    # One person followed for 6 months contributes 0.5 person-years
    follow_up = pd.DataFrame({
        "person_id": ["A", "B", "C"],
        "entered": pd.to_datetime(["2024-01-01", "2024-01-01", "2024-07-01"]),
        "exited": pd.to_datetime(["2024-12-31", "2024-06-30", "2024-12-31"]),
    })
    follow_up["person_years"] = (
        (follow_up["exited"] - follow_up["entered"]).dt.days / 365.25
    )
    
    events = 1
    print(f"{events / follow_up['person_years'].sum():.3f} events per person-year")
    print(f"naive headcount rate: {events / len(follow_up):.3f} per person")
    Speaker notes
    A headcount denominator assumes everyone was present and at risk for the whole period. In this sector they routinely were not — people arrive, leave, are born, die, or enter a programme mid-year. Person-time counts each person for as long as they were actually at risk.
  8. Slide 8 / 20

    What person-time adds — In R

    follow_up <- tibble::tibble(
      person_id = c("A", "B", "C"),
      entered = as.Date(c("2024-01-01", "2024-01-01", "2024-07-01")),
      exited  = as.Date(c("2024-12-31", "2024-06-30", "2024-12-31"))
    ) |>
      mutate(person_years = as.numeric(exited - entered) / 365.25)
    
    c(per_person_year = 1 / sum(follow_up$person_years),
      naive = 1 / nrow(follow_up))
  9. Slide 9 / 20

    What person-time adds

    • Follow-up varies — a treatment cohort where people enrol through the year.
    • The population changes — displacement, camp arrivals, a catchment that grows.
    • You are comparing periods of different length — a nine-month response against a twelve-month one.
    Speaker notes
    Three people, two of whom were present for half the year. The headcount denominator is 3; the person-time denominator is 2.0 person-years. The two answers differ by a third, and the second is the one that compares across periods and places with different amounts of follow-up. Use person-time whenever: Use a headcount denominator when everyone was present throughout and the period is fixed, which is what makes an outbreak attack rate legitimate.
  10. Slide 10 / 20

    Say the denominator and the multiplier in the name — In Python

    indicators = {
        "cholera_attack_rate_per_1000_outbreak": 1000 * len(cases) / total_population,
        "case_fatality_percent_of_cases_with_outcome": None,
        "incidence_per_1000_person_years": None,
    }
  11. Slide 11 / 20

    Say the denominator and the multiplier in the name — In R

    # The same discipline: the name carries the denominator and the multiplier
  12. Slide 12 / 20

    Say the denominator and the multiplier in the name

    • The multiplier — Per 100, per 1,000, per 100,000
    • The denominator's population — "Per 1,000 population" and "per 1,000 children under five" are different indicators
    Speaker notes
    Two things belong in the name and are usually missing. The multiplier. Per 100, per 1,000, per 100,000. Cholera attack rates are conventionally per 1,000; maternal mortality per 100,000; immunisation coverage per 100. Getting the convention wrong is a factor of a hundred and it happens. The denominator's population. "Per 1,000 population" and "per 1,000 children under five" are different indicators. The indicator course made this general point; here the conventions are published and departing from one silently is worse than inventing one.
  13. Slide 13 / 20

    The at-risk population is not always the whole population

    • Maternal mortality ratio — deaths per 100,000 live births, not per population, because only pregnancies are at…
    • Neonatal mortality — deaths in the first 28 days per 1,000 live births.
    • Cholera attack rate — cases per 1,000 population, because everyone is at risk.
    • Measles attack rate — arguably per 1,000 susceptible people, and the susceptible population is exactly what an…
    Speaker notes
    The denominator of a rate is the population at risk of the event.
  14. Slide 14 / 20

    The at-risk population is not always the whole population — In Python

    under_five = population.loc[population["age_band"] == "0-4", "population"].sum()
    u5_cases = (cases["age_band"] == "0-4").sum()
    
    print(f"under-five attack rate: {1000 * u5_cases / under_five:.2f} per 1,000")
    print(f"all-age attack rate:    {1000 * len(cases) / total_population:.2f} per 1,000")
  15. Slide 15 / 20

    The at-risk population is not always the whole population — In R

    under5 <- population |> filter(age_band == "0-4") |> summarise(sum(population)) |> pull()
    u5_cases <- sum(cases$age_band == "0-4")
    
    c(under_five = 1000 * u5_cases / under5,
      all_age = 1000 * nrow(cases) / sum(population$population))
    Speaker notes
    Under-fives have roughly twice the all-age attack rate here. That gap is the whole reason lesson 6 exists: two districts with different age structures will differ on the all-age rate even if every age-specific rate is identical.
  16. Slide 16 / 20

    The check to run on any rate

    • What is the numerator counting — events, people, or episodes?
    • Who is in the denominator, and were they all at risk?
    • Over what period, and was everyone present for it?
    • What is the multiplier, and is it the convention for this indicator?
    Speaker notes
    Four questions, and a rate that cannot answer all four is not usable.
  17. Slide 17 / 20

    The check to run on any rate — In Python

    def describe_rate(numerator, denominator, period, multiplier, at_risk_note):
        return {
            "value": multiplier * numerator / denominator,
            "numerator": numerator, "denominator": denominator,
            "period": period, "multiplier": multiplier, "at_risk": at_risk_note,
        }
  18. Slide 18 / 20

    The check to run on any rate — In R

    describe_rate <- function(numerator, denominator, period, multiplier, at_risk) {
      list(value = multiplier * numerator / denominator, numerator = numerator,
           denominator = denominator, period = period, at_risk = at_risk)
    }
    Speaker notes
    That is the indicator reference sheet from module 3, compressed to the five fields an epidemiological measure cannot do without.
  19. Slide 19 / 20

    What comes next

    • One denominator is straightforward.
    Speaker notes
    One denominator is straightforward. The next lesson chains several together — the HIV and TB care cascades, where each step's denominator is the previous step's numerator, and the interesting number is which link loses the most.
  20. Slide 20 / 20

    Where this goes next

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