The grade progression ratio method
The cohort survival / grade progression ratio method projects
enrollment by asking: of the students in grade g this year, how
many appear in grade g+1 next year? That ratio captures net
retention, migration, and repetition. enrollcast estimates
these ratios from history and applies them forward with a matrix
projection.
A small district
history <- data.frame(
year = rep(2021:2023, each = 3),
grade = factor(rep(c("K", "1", "2"), 3), levels = c("K", "1", "2")),
enrollment = c(100, 90, 80, 110, 95, 88, 120, 99, 91)
)
history
#> year grade enrollment
#> 1 2021 K 100
#> 2 2021 1 90
#> 3 2021 2 80
#> 4 2022 K 110
#> 5 2022 1 95
#> 6 2022 2 88
#> 7 2023 K 120
#> 8 2023 1 99
#> 9 2023 2 91Step 1: progression ratios
ratios <- progression_ratios(history, method = "mean")
ratios
#> grade_from grade_to ratio
#> 1 K 1 0.9250000
#> 2 1 2 0.9678363Ratios are calculated only from observed adjacent calendar-year
pairs. If the history contains a gap, progression_ratios()
warns and uses the adjacent pairs on either side without constructing a
transition across the missing period. A history with no adjacent year
pair cannot produce progression ratios and is rejected. Gap detection
examines the complete supplied history before n_years
selects the most recent adjacent transitions, so an older gap still
warns even when it lies outside the selected transitions.
The ratios sit on the sub-diagonal of the projection matrix; the entry-grade row is zero because entry is supplied exogenously.
progression_matrix(ratios)
#> K 1 2
#> K 0.000 0.0000000 0
#> 1 0.925 0.0000000 0
#> 2 0.000 0.9678363 0Step 2: project forward
The entry grade (kindergarten here) has no feeder grade, so you supply its future values — for example from a birth-cohort or housing model.
base <- history[history$year == 2023, c("grade", "enrollment")]
projection <- project_enrollment(
base = base,
ratios = ratios,
horizon = 3,
entry = c(125, 130, 128),
start_year = 2023
)
projection
#> year grade enrollment
#> 1 2024 K 125.00000
#> 2 2024 1 111.00000
#> 3 2024 2 95.81579
#> 4 2025 K 130.00000
#> 5 2025 1 115.62500
#> 6 2025 2 107.42982
#> 7 2026 K 128.00000
#> 8 2026 1 120.25000
#> 9 2026 2 111.90607Stitching history and projection
project_enrollment() returns projected years only.
Combine with history for plotting:
observed <- data.frame(
year = history$year,
grade = as.character(history$grade),
enrollment = history$enrollment
)
combined <- rbind(observed, projection)
head(combined)
#> year grade enrollment
#> 1 2021 K 100
#> 2 2021 1 90
#> 3 2021 2 80
#> 4 2022 K 110
#> 5 2022 1 95
#> 6 2022 2 88Modeling a school modernization swing
A school temporarily relocated during modernization typically sees
depressed enrollment that recovers after it returns.
swing_schedule() builds a per-year projection schedule:
enrollment is held flat during the swing, scaled by recovery multipliers
for a few years, then projected normally.
Schedules may contain NA or NaN matrix
coefficients. These values are preserved and warned about rather than
imputed. A missing coefficient can make its output grade missing. If
that missing enrollment remains after the entry grade is replaced, the
next matrix multiplication spreads missingness to all grade results
because zero times a missing value is still missing. A
non-NULL entry value then restores only the entry
grade.
depressed <- c(K = 80, `1` = 66, `2` = 60)
schedule <- swing_schedule(ratios,
horizon = 6, swing_years = 2,
recovery = c(1.10, 1.10, 1.05), entry = 130
)
project_enrollment(depressed, schedule = schedule, start_year = 2023)
#> year grade enrollment
#> 1 2024 K 80.00000
#> 2 2024 1 66.00000
#> 3 2024 2 60.00000
#> 4 2025 K 80.00000
#> 5 2025 1 66.00000
#> 6 2025 2 60.00000
#> 7 2026 K 88.00000
#> 8 2026 1 72.60000
#> 9 2026 2 66.00000
#> 10 2027 K 96.80000
#> 11 2027 1 79.86000
#> 12 2027 2 72.60000
#> 13 2028 K 101.64000
#> 14 2028 1 83.85300
#> 15 2028 2 76.23000
#> 16 2029 K 130.00000
#> 17 2029 1 94.01700
#> 18 2029 2 81.15597