cassionData Analysis

Back to the lessonLesson 6 of 8The cohort

A survival rate you should not publish

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

    What this lesson covers

    • The calculation everyone reaches for
    • Why it is wrong
    • What to compute instead
    • Who leaves
    • The missing data is not missing at random
    • Report it as a risk profile
    • What comes next
    Speaker notes
    Chain one year's promotion rates and 19.3% of children reach the final grade. Count repeaters as retained and it is 40.3%. Both come from the same file, and neither is a completion rate.
  2. Slide 2 / 30

    The calculation everyone reaches for — In Python (cont.)

    import pandas as pd
    
    enrolment = pd.read_csv("school-enrolment-2024.v1.csv")
    clean = enrolment[enrolment["age_years"] <= 20]
    cohort = clean[clean["school_year"] == 2023]
    
    survival, cumulative = {}, 1.0
    for grade in range(1, 7):
        grade_rows = cohort[cohort["grade"] == grade]
        promoted = grade_rows["end_of_year_status"].isin(
            ["promoted", "completed-final-grade"]).mean()
        survival[grade] = cumulative
        cumulative *= promoted
        print(f"grade {grade}: promotion {promoted:.1%}, "
              f"survival to here {survival[grade]:.1%}")
    
    Speaker notes
    You have promotion rates by grade for one year. Chain them and you have survival to the final grade — the SDG-style completion measure — without waiting six years.
  3. Slide 3 / 30

    The calculation everyone reaches for — In Python (cont.)

    print(f"\nsurvival to grade 6 entry: {cumulative:.1%}")
  4. Slide 4 / 30

    The calculation everyone reaches for — In R

    library(dplyr)
    
    cohort |>
      filter(between(grade, 1, 6)) |>
      summarise(promotion = mean(end_of_year_status %in%
                  c("promoted", "completed-final-grade")), .by = grade) |>
      arrange(grade) |>
      mutate(survival = cumprod(lag(promotion, default = 1)))
  5. Slide 5 / 30

    The calculation everyone reaches for

    GradePromotionCumulative survival
    189.3%100.0%
    283.0%89.3%
    371.7%74.1%
    476.2%53.1%
    570.9%40.5%
    667.3%28.7%
    ——19.3%
  6. Slide 6 / 30

    The calculation everyone reaches for

    • 19.3% of children entering grade 1 reach the end of primary school — It is a striking number, it is what the method…
    Speaker notes
    19.3% of children entering grade 1 reach the end of primary school. It is a striking number, it is what the method produces, and it should not leave your screen.
  7. Slide 7 / 30

    Why it is wrong

    • Repeaters are counted as failures — A child who repeats grade 2 is not out of school — they are in grade 2 again
    Speaker notes
    This is a reconstructed cohort: it assumes that this year's grade-5 promotion rate is what today's grade-1 children will face in four years. Four things break that, and here they break it in the same direction. Repeaters are counted as failures. A child who repeats grade 2 is not out of school — they are in grade 2 again. The chain treats non-promotion as exit, so 10.1% repetition compounds six times into a huge apparent loss.
  8. Slide 8 / 30

    Why it is wrong — In Python

    promotion_or_repeat = cohort[cohort["grade"].between(1, 6)].groupby("grade")[
        "end_of_year_status"].apply(
        lambda s: s.isin(["promoted", "completed-final-grade", "repeated"]).mean())
    
    still_enrolled = 1.0
    for grade, rate in promotion_or_repeat.items():
        still_enrolled *= rate
    print(f"survival counting repeaters as retained: {still_enrolled:.1%}")
  9. Slide 9 / 30

    Why it is wrong — In R

    # Retention, not promotion, is what a survival rate needs.
  10. Slide 10 / 30

    Why it is wrong

    • Transfers are counted as failures too — 2.9% left for another school and the chain reads them as leaving education
    • A single year is assumed to be six — The grade-6 promotion rate observed in 2023 belongs to children who entered in…
    • The grade sizes are not a cohort — Grade 1 has 149 students and grade 6 has 110, and some of that is genuine attrition…
    Speaker notes
    Transfers are counted as failures too. 2.9% left for another school and the chain reads them as leaving education. A single year is assumed to be six. The grade-6 promotion rate observed in 2023 belongs to children who entered in 2018, under different conditions. The grade sizes are not a cohort. Grade 1 has 149 students and grade 6 has 110, and some of that is genuine attrition while some is a changing population — a larger birth cohort arriving at grade 1 makes the pyramid look like dropout.
  11. Slide 11 / 30

    What to compute instead

    • Retention, not promotion — if you must use a single year
    Speaker notes
    Retention, not promotion, if you must use a single year. Count children who are still in school — promoted or repeating — as retained.
  12. Slide 12 / 30

    What to compute instead — In Python

    def chained(statuses):
        rate, product = {}, 1.0
        for grade in range(1, 7):
            rows = cohort[cohort["grade"] == grade]["end_of_year_status"]
            product *= rows.isin(statuses).mean()
            rate[grade] = product
        return product
    
    promotion_only = chained(["promoted", "completed-final-grade"])
    retained = chained(["promoted", "completed-final-grade", "repeated"])
    print(f"chained promotion: {promotion_only:.1%}")
    print(f"chained retention: {retained:.1%}")
  13. Slide 13 / 30

    What to compute instead — In R

    # Same chain, different numerator. The gap is repetition compounding.
  14. Slide 14 / 30

    What to compute instead

    • 19.3% against 40.3% — More than twenty points of the apparent loss was repetition being read as exit, and the retention…
    • Follow real students where you can — This register has two years and the same identifiers, so a one-year transition is…
    Speaker notes
    19.3% against 40.3%. More than twenty points of the apparent loss was repetition being read as exit, and the retention figure is the one closer to something meaningful — though still not a completion rate. Follow real students where you can. This register has two years and the same identifiers, so a one-year transition is directly observable rather than assumed.
  15. Slide 15 / 30

    What to compute instead — In Python

    years = clean.pivot_table(index="student_id", columns="school_year",
                              values="grade", aggfunc="first")
    both = years.dropna()
    print(f"students observed in both years: {len(both)}")
    print(f"  advanced a grade: {(both[2024] > both[2023]).mean():.1%}")
    print(f"  same grade:       {(both[2024] == both[2023]).mean():.1%}")
  16. Slide 16 / 30

    What to compute instead

    • 1,103 students appear in both years: 88.5% advanced a grade and 11.5% are in the same grade twice — That is a measured…
    Speaker notes
    1,103 students appear in both years: 88.5% advanced a grade and 11.5% are in the same grade twice. That is a measured transition, not an assumed one, and it is the number to put in a report.
  17. Slide 17 / 30

    What to compute instead — In R

    enrolment |>
      select(student_id, school_year, grade) |>
      tidyr::pivot_wider(names_from = school_year, values_from = grade) |>
      filter(!is.na(`2023`), !is.na(`2024`))
  18. Slide 18 / 30

    What to compute instead

    • A one-year transition observed on real students beats a six-year chain assembled from one year's rates — even though it…
    Speaker notes
    A one-year transition observed on real students beats a six-year chain assembled from one year's rates, even though it answers a smaller question. The smaller question is one you can defend.
  19. Slide 19 / 30

    Who leaves — In Python

    year23 = cohort.assign(
        over_age=cohort["age_years"] > cohort["grade"] + 5,
        dropped=cohort["end_of_year_status"].eq("dropped-out"),
    )
    for column in ("over_age", "disability_reported", "displacement_status", "sex"):
        table = year23.groupby(column, dropna=False)["dropped"].agg(["mean", "size"])
        print((table * [100, 1]).round(1), "\n")
    Speaker notes
    The disaggregation that does work, unlike the over-age cuts in lesson 2.
  20. Slide 20 / 30

    Who leaves — In R

    cohort |>
      mutate(dropped = end_of_year_status == "dropped-out") |>
      summarise(dropout = mean(dropped), n = n(), .by = displacement_status)
  21. Slide 21 / 30

    Who leaves

    CutDropoutn
    Over-age19.5%478
    In-age4.8%814
    Returnee19.7%71
    Disability reported17.9%112
    Internally displaced14.7%163
    Host community11.6%95
    Resident8.6%940
    Boys10.4%634

    …

  22. Slide 22 / 30

    Who leaves

    • Over-age is the strongest predictor and the largest group — Disability and displacement roughly double the rate on…
    • Sex shows nothing — 10.4% against 10.0% on 634 and 658 students
    Speaker notes
    Over-age is the strongest predictor and the largest group. Disability and displacement roughly double the rate on smaller denominators. Sex shows nothing — 10.4% against 10.0% on 634 and 658 students. That is a result worth stating, because a gender gap in dropout is the finding an education programme most expects to see and this register does not contain one.
  23. Slide 23 / 30

    The missing data is not missing at random — In Python

    print(year23["displacement_status"].isna().sum(), "students with no status")
    print(year23.loc[year23["displacement_status"].isna(), "dropped"].mean())
  24. Slide 24 / 30

    The missing data is not missing at random — In R

    cohort |> filter(is.na(displacement_status)) |> nrow()
  25. Slide 25 / 30

    The missing data is not missing at random

    • Be precise about what that does and does not bias — The blanks are removed roughly at random within those two…
    • the dropout rate for internally displaced and returnee students is unbiased — 14.7% and 19.7% are the right numbers…
    • their counts and population share are understated by about a tenth. 234 students are recorded as displaced or…
    Speaker notes
    Displacement status is blank for 42 students across the register, 23 of them in the 2023 cohort. The blanks are drawn entirely from displaced and returnee households — it is the field an enumerator skips when a family has just arrived. Be precise about what that does and does not bias. The blanks are removed roughly at random within those two categories, so: So "displaced children drop out at 14.7%" is defensible and "displaced children are 18% of enrolment" is not. A missing-value pattern can bias a count without biasing a rate, and which one your sentence relies on decides whether the pattern matters. The observed dropout among the blanks themselves is 8.7% on 23 students, which is too few to read anything into and should not be reported as a category.
  26. Slide 26 / 30

    Report it as a risk profile — Example (cont.)

    Dropout, 2023 cohort, 1,122 students
    
      Overall                      10.2%
    
      Over-age for grade           19.5%   478 students
      In-age                        4.8%   814
    
      Returnee                     19.7%    71 students
      Disability reported          17.9%   112
      Internally displaced         14.7%   163
      Resident                      8.6%   940
    
      No meaningful difference by sex: 10.4% boys, 10.0% girls.
    
      42 students have no displacement status, all drawn from displaced and
      returnee households. The rates above are unbiased for those groups; their
  27. Slide 27 / 30

    Report it as a risk profile — Example (cont.)

      counts are understated by about a tenth.
    
      Not reported: survival to the final grade. A reconstructed cohort from a
      single year gives 19.3% on promotion and 40.3% on retention, neither of
      which is a completion rate. A true cohort needs six years of the register
      or a household survey. The measured one-year transition is 88.5%.
  28. Slide 28 / 30

    Report it as a risk profile

    • "Not reported, and why" is the most useful line in that block — Somebody will ask for a completion rate, and the answer…
    Speaker notes
    "Not reported, and why" is the most useful line in that block. Somebody will ask for a completion rate, and the answer is a real one: not from this, from a household survey, and here is what this can tell you instead.
  29. Slide 29 / 30

    What comes next

    • Enrolment, attendance and retention describe whether children are in school.
    Speaker notes
    Enrolment, attendance and retention describe whether children are in school. None of them says whether they are learning, and the last unit is the instrument that does — where two rounds turn out not to be comparable.
  30. Slide 30 / 30

    Where this goes next

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