cassionData Analysis

Lesson 4 of 8

Unit · Enrolled is not attending

A strike that looks like an emergency

SCH07 attends at 88.2% and SCH18 at 85.2%. Build the register as a full calendar and count every missing day as an absence, and they read 66.1% and 63.9% — the worst schools in the district, on fifteen days nobody was meant to be there.

PythonR105 minUNICEF indicator definitionsCore Humanitarian Standard (CHS)

The rows that are not there

Attendance rows exist only for days a school was open. A date with no row is a closure, not an absence, and nothing in the file marks which is which.

import pandas as pd

attendance = pd.read_csv("school-attendance-2024.v1.csv")
roster = pd.read_csv("school-roster-2024.v1.csv")

joined = attendance.merge(roster[["student_id", "school_id"]], on="student_id")
calendar = sorted(attendance["attendance_date"].unique())

days_per_school = joined.groupby("school_id")["attendance_date"].nunique()
print(f"school days in the calendar: {len(calendar)}")
print(days_per_school.sort_values().head())
library(dplyr)

attendance |>
  left_join(roster, by = "student_id") |>
  summarise(days = n_distinct(attendance_date), .by = school_id) |>
  arrange(days)

Twenty-two schools have all sixty days. SCH07 and SCH18 have forty-five.

missing = {
    school: sorted(set(calendar) - set(group["attendance_date"]))
    for school, group in joined.groupby("school_id")
}
for school, dates in missing.items():
    if dates:
        print(f"{school}: {len(dates)} days, {dates[0]} to {dates[-1]}")
# Which dates, not just how many. The pattern is the diagnosis.

Both are missing exactly 11 to 29 March, and both are missing the same fifteen days. That is not two schools with an attendance problem; that is one event.

Consecutive missing days at multiple schools is a closure until proved otherwise. Scattered missing days at one school is a recording problem. The shape tells you which, and it takes one line to look.

What happens if you do not look

The natural way to build an attendance table is to construct the full grid of students by school days and fill in the marks. It is also the way to turn a strike into a crisis.

def attendance_rate(school, missing_counts_as_absent):
    students = roster.loc[roster["school_id"] == school, "student_id"]
    marks = attendance[attendance["student_id"].isin(students)]
    present = marks["present"].isin(["true", "Y"]).sum()
    if missing_counts_as_absent:
        denominator = len(students) * len(calendar)
    else:
        denominator = marks["present"].isin(["true", "Y", "false", "N"]).sum()
    return present / denominator

for school in ("SCH07", "SCH18", "SCH01"):
    observed = attendance_rate(school, False)
    filled = attendance_rate(school, True)
    print(f"{school}: observed {observed:.1%}, grid-filled {filled:.1%}")
# Two denominators: marks made, and student-days in the calendar.
School On marks made On a full grid
SCH07 88.2% 66.1%
SCH18 85.2% 63.9%
SCH01 91.3% 91.3%

Twenty-two points, invented. On the grid-filled figures SCH07 and SCH18 are the two worst schools in the district by a wide margin, and a programme reading that table would send a dropout response to two schools whose children attended normally on every day they were asked to.

The unaffected school is unchanged, which is what makes the error so hard to catch: the table looks fine, most of it is fine, and the two wrong rows are the two you act on.

The rule

Build the denominator from days the school was open, not from the calendar.

open_days = joined.groupby("school_id")["attendance_date"].nunique()
enrolled = roster.groupby("school_id")["student_id"].nunique()
expected = (open_days * enrolled).rename("student_days_expected")

actual = joined.groupby("school_id").size().rename("marks_made")
coverage = (actual / expected).rename("mark_coverage")
print(pd.concat([expected, actual, coverage.round(3)], axis=1).head())
# Expected student-days uses each school's own open days.

That gives a second, useful number: mark coverage, the share of expected student-days that carry a mark at all. A school at 100% attendance and 60% mark coverage is not a school with good attendance.

When a closure is the finding

Fifteen school days is a quarter of the term. The closure is more consequential than any attendance figure in this file, and an analysis that correctly excludes those days and then says nothing about them has removed the largest thing that happened.

Attendance, February to April 2024

  Average daily attendance, all schools    88.4%   on marks made
  SCH07                                    88.2%
  SCH18                                    85.2%

  SCH07 and SCH18 were closed for the fifteen school days from 11 to 29
  March, a quarter of the term. Their attendance figures are computed on the
  45 days they were open and are comparable with other schools; their
  instructional time is not.

  Counting the closure days as absences would report these two schools at
  66.1% and 63.9% and rank them worst in the district.

Report the closure as lost instructional days, separately from attendance. The two answer different questions: attendance is about whether children came, and instructional days are about whether school happened.

The general form

This is the third time this platform has met the same defect in a different file.

Course The absent thing What it looked like
Routine data and DHIS2 A facility that did not report A district with falling coverage
WASH analysis A monitoring round nobody drove A district with improving functionality
Education A day the school was closed Two schools with a dropout emergency

In all three, the absence is not a value and nothing in the file announces it. The habit that catches all three is the same: before computing any rate, construct what the denominator should be from something other than the rows you have, and compare.

What comes next

Attendance describes one term. Whether a child is still in school next year is a different question, and the next unit follows the 2023 cohort through promotion, repetition and dropout to find out.

Teach this lesson

The lesson as a slide deck, with the prose kept in the speaker notes rather than on the slide. Generated from this page, so it cannot fall out of step with it.

Start the slideshowRead the slides

The PDF needs no software and projects from any machine. The PowerPoint file is there to be edited — add your organisation's branding, cut a section for a shorter session, or merge two lessons into a workshop.