cassionData Analysis

Back to the lessonLesson 2 of 8Enrolment and its denominator

Behind, and getting further behind

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

    What this lesson covers

    • The definition, and the range check that has to come first
    • It compounds with grade
    • Why it matters: the dropout link
    • The disaggregation that does not work
    • Report the profile, not the headline
    • What comes next
    Speaker notes
    Over-age enrolment runs 17.0% in grade 1 and 50.8% in grade 6. It rises because repetition creates it — 94.5% of the children who repeated in 2023 are over-age in 2024 — and because being over-age quadruples the chance of leaving.
  2. Slide 2 / 26

    The definition, and the range check that has to come first — In Python

    import pandas as pd
    
    enrolment = pd.read_csv("school-enrolment-2024.v1.csv")
    print(enrolment["age_years"].describe().round(1))
    print(f"ages above 20: {(enrolment['age_years'] > 20).sum()}")
    Speaker notes
    A student is over-age for their grade when their age exceeds the official age, which in this system is the grade number plus five.
  3. Slide 3 / 26

    The definition, and the range check that has to come first — In R

    library(dplyr)
    enrolment |> summarise(min = min(age_years), max = max(age_years),
                           impossible = sum(age_years > 20))
  4. Slide 4 / 26

    The definition, and the range check that has to come first

    • Twenty-one rows hold an age above 20 in a primary register — SCH04 records part of its intake in months, so a…
    Speaker notes
    Twenty-one rows hold an age above 20 in a primary register. SCH04 records part of its intake in months, so a nine-year-old appears as 112. A mean would absorb them; a range check finds them, and the correction is a division rather than a deletion.
  5. Slide 5 / 26

    The definition, and the range check that has to come first — In Python

    clean = enrolment[enrolment["age_years"] <= 20].copy()
    clean["official_age"] = clean["grade"] + 5
    clean["over_age"] = clean["age_years"] > clean["official_age"]
  6. Slide 6 / 26

    The definition, and the range check that has to come first — In R

    enrolment |> filter(age_years <= 20) |>
      mutate(official_age = grade + 5, over_age = age_years > official_age)
  7. Slide 7 / 26

    The definition, and the range check that has to come first

    • Do the range check before the flag, not after — An age of 112 is over-age for every grade, so a flag computed on the…
    Speaker notes
    Do the range check before the flag, not after. An age of 112 is over-age for every grade, so a flag computed on the raw column is technically correct about twenty-one children and wrong about what it means.
  8. Slide 8 / 26

    It compounds with grade — In Python

    primary = clean[(clean["school_year"] == 2024) & clean["grade"].between(1, 6)]
    by_grade = primary.groupby("grade").agg(
        students=("student_id", "size"),
        over_age=("over_age", "mean"),
    )
    print((by_grade * [1, 100]).round(1))
  9. Slide 9 / 26

    It compounds with grade — In R

    primary |> summarise(n = n(), over_age = mean(over_age), .by = grade)
  10. Slide 10 / 26

    It compounds with grade

    GradeOfficial ageStudentsOver-age
    165317.0%
    2715325.5%
    3824729.6%
    4925836.8%
    51020947.8%
    61113250.8%
  11. Slide 11 / 26

    It compounds with grade

    • Over-age triples between grade 1 and grade 6 — Two mechanisms produce that shape and they are not the same problem
    • Late entry — puts a child above age at grade 1 and keeps them there
    • Repetition — creates over-age during schooling
    Speaker notes
    Over-age triples between grade 1 and grade 6. Two mechanisms produce that shape and they are not the same problem. Late entry puts a child above age at grade 1 and keeps them there. It shows up as the 17.0% floor, and the intervention is early registration. Repetition creates over-age during schooling. It shows as the rise, and the intervention is entirely different.
  12. Slide 12 / 26

    It compounds with grade — In Python

    previous = clean[clean["school_year"] == 2023].set_index("student_id")
    current = clean[clean["school_year"] == 2024].copy()
    current["last_year"] = current["student_id"].map(previous["end_of_year_status"])
    
    print(current.groupby("last_year")["over_age"].agg(["mean", "size"]).round(3))
  13. Slide 13 / 26

    It compounds with grade — In R

    enrolment |>
      filter(age_years <= 20) |>
      select(student_id, school_year, grade, age_years, end_of_year_status) |>
      tidyr::pivot_wider(names_from = school_year,
                         values_from = c(grade, age_years, end_of_year_status))
  14. Slide 14 / 26

    It compounds with grade

    • 94.5% of the students who repeated in 2023 are over-age in 2024, against 30.6% of those who were promoted — Repetition…
    Speaker notes
    94.5% of the students who repeated in 2023 are over-age in 2024, against 30.6% of those who were promoted. Repetition does not correlate with over-age; it causes it, with a one-year lag, and the register lets you watch the mechanism rather than infer it. Note that the comparison has to be across years. Within 2023 a repeater is not yet over-age — they are in their own grade at the normal age, and the extra year appears the following September.
  15. Slide 15 / 26

    Why it matters: the dropout link — In Python

    year23 = clean[clean["school_year"] == 2023]
    dropout = year23.groupby("over_age")["end_of_year_status"].apply(
        lambda s: (s == "dropped-out").mean()
    )
    counts = year23.groupby("over_age").size()
    print(pd.DataFrame({"dropout": (dropout * 100).round(1), "n": counts}))
  16. Slide 16 / 26

    Why it matters: the dropout link — In R

    enrolment |> filter(school_year == 2023) |>
      summarise(dropout = mean(end_of_year_status == "dropped-out"),
                n = n(), .by = over_age)
  17. Slide 17 / 26

    Why it matters: the dropout link

    StudentsDropped out
    Over-age47819.5%
    In-age8144.8%
  18. Slide 18 / 26

    Why it matters: the dropout link

    • Being behind quadruples the chance of leaving — and that is what closes the loop: a child repeats, becomes over-age,…
    • So repetition is not a neutral intervention — It is offered as a second chance and it measurably raises the probability…
    Speaker notes
    Being behind quadruples the chance of leaving, and that is what closes the loop: a child repeats, becomes over-age, and is then far likelier to drop out than the repetition was intended to prevent. So repetition is not a neutral intervention. It is offered as a second chance and it measurably raises the probability of leaving altogether. That finding is available from two columns of one register, and it is the strongest argument this dataset supports.
  19. Slide 19 / 26

    The disaggregation that does not work — In Python

    for column in ("sex", "disability_reported", "displacement_status"):
        print(primary.groupby(column, dropna=False)["over_age"].agg(
            ["mean", "size"]).round(3), "\n")
    Speaker notes
    Over-age is unusual among education indicators in that the obvious cuts show very little.
  20. Slide 20 / 26

    The disaggregation that does not work — In R

    primary |> summarise(over_age = mean(over_age), n = n(), .by = sex)
  21. Slide 21 / 26

    The disaggregation that does not work

    CutOver-agen
    Boys39.2%510
    Girls33.8%542
    Disability reported33.7%89
    No disability reported36.7%963
  22. Slide 22 / 26

    The disaggregation that does not work

    • Five points between boys and girls, on 510 and 542 students — That is around the edge of what sampling variation…
    • Disability shows three points the other way on 89 students — which is noise on a denominator that small
    • A disaggregation that mostly shows nothing is a result — Over-age here is a system-level phenomenon driven by…
    Speaker notes
    Five points between boys and girls, on 510 and 542 students. That is around the edge of what sampling variation produces, so compute the interval before writing the sentence — the survey course's machinery, on exactly the kind of gap that gets published as a finding and disappears in the next round. Disability shows three points the other way on 89 students, which is noise on a denominator that small. A disaggregation that mostly shows nothing is a result. Over-age here is a system-level phenomenon driven by repetition and late entry, not a group-level inequity — and saying so is more useful than hunting for a subgroup until one turns up.
  23. Slide 23 / 26

    Report the profile, not the headline — Example

    Over-age enrolment, primary, 2024
    
      Overall                     36.4%   383 of 1,052 primary enrolees
      Grade 1                     17.0%   the late-entry floor
      Grade 6                     50.8%   after five years of repetition
      Repeated in 2023            94.5%   over-age in 2024, against 30.6% promoted
    
      Dropout, over-age           19.5%   against 4.8% in-age (2023 cohort)
    
      21 ages recorded in months at SCH04, corrected by division.
      Sex difference is 39.2% (boys) against 33.8% (girls); report with an
      interval or not at all.
  24. Slide 24 / 26

    Report the profile, not the headline

    • The grade profile is the deliverable — because 36.4% overall could mean uniform late entry or accumulating repetition,…
    Speaker notes
    The grade profile is the deliverable, because 36.4% overall could mean uniform late entry or accumulating repetition, and those need different programmes.
  25. Slide 25 / 26

    What comes next

    • Every child in this lesson is enrolled.
    Speaker notes
    Every child in this lesson is enrolled. Whether they are in the classroom is a different register and a different number — and the next lesson finds two of them, twenty-six points apart, in the same file.
  26. Slide 26 / 26

    Where this goes next

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