15 Insights from Vermont School Enrollment Data
Source:vignettes/enrollment_hooks.Rmd
enrollment_hooks.Rmd
library(vtschooldata)
library(dplyr)
library(tidyr)
library(ggplot2)
theme_set(theme_minimal(base_size = 14))Vermont Lost 14% of Its Students Since 2004
The Green Mountain State has seen steady enrollment decline over two decades. Vermont now educates fewer than 80,000 students, down 14% from over 92,000 in 2004 – a loss of more than 13,000 students.
enr <- fetch_enr_multi(c(2004, 2008, 2012, 2016, 2020, 2024), use_cache = TRUE)
statewide <- enr |>
filter(is_state, subgroup == "total_enrollment", grade_level == "TOTAL") |>
select(end_year, n_students)
stopifnot(nrow(statewide) > 0)
statewide
#> end_year n_students
#> 1 2004 92334
#> 2 2008 87777
#> 3 2012 82014
#> 4 2016 78472
#> 5 2020 83503
#> 6 2024 79288
ggplot(statewide, aes(x = end_year, y = n_students)) +
geom_line(color = "#006837", linewidth = 1.2) +
geom_point(color = "#006837", size = 3) +
geom_vline(xintercept = 2020, linetype = "dashed", color = "gray50") +
annotate("text", x = 2020.3, y = max(statewide$n_students),
label = "COVID", hjust = 0, size = 3.5, color = "gray40") +
scale_y_continuous(labels = scales::comma, limits = c(0, NA)) +
labs(
title = "Vermont Public School Enrollment (2004-2024)",
subtitle = "Steady decline reflects aging population and outmigration",
x = "Year",
y = "Students"
)
Vermont statewide enrollment has declined steadily since 2004
Top Supervisory Unions: Burlington Leads a Small State
Vermont organizes schools into Supervisory Unions (SUs) and Supervisory Districts (SDs). Burlington is among the largest, but even the biggest districts are small by national standards.
Note: Vermont data reports at the school (campus) level. To see supervisory union totals, we aggregate campus data by district name.
enr_2024 <- fetch_enr(2024, use_cache = TRUE)
# Aggregate campus data to get district totals
top_districts <- enr_2024 |>
filter(is_campus, subgroup == "total_enrollment", grade_level == "TOTAL") |>
group_by(district_name) |>
summarize(n_students = sum(n_students, na.rm = TRUE), .groups = "drop") |>
arrange(desc(n_students)) |>
head(10)
stopifnot(nrow(top_districts) > 0)
top_districts
#> # A tibble: 10 × 2
#> district_name n_students
#> <chr> <dbl>
#> 1 CHAMPLAIN VALLEY SUPERVISORY DISTRICT 4150
#> 2 ESSEX-WESTFORD SUPERVISORY DISTRICT 3703
#> 3 BURLINGTON SUPERVISORY DISTRICT 3506
#> 4 SOUTHWEST VERMONT SUPERVISORY UNION 3166
#> 5 SOUTH BURLINGTON SUPERVISORY DISTRICT 2693
#> 6 MAPLE RUN SUPERVISORY DISTRICT 2608
#> 7 NORTH COUNTRY SUPERVISORY UNION 2598
#> 8 MOUNT MANSFIELD UNIFIED UNION SCHOOL DISTRICT 2591
#> 9 WINDHAM SOUTHEAST SUPERVISORY UNION 2495
#> 10 COLCHESTER SUPERVISORY DISTRICT 2417
# Shorten district names for display
su_pattern <- " SUPERVISORY UNION| SUPERVISORY DISTRICT| SCHOOL DISTRICT| SD| SU"
top_districts |>
mutate(district_name = gsub(su_pattern, "", district_name)) |>
mutate(district_name = factor(district_name, levels = rev(district_name))) |>
ggplot(aes(x = n_students, y = district_name)) +
geom_col(fill = "#006837") +
geom_text(aes(label = scales::comma(n_students)), hjust = -0.1, size = 3.5) +
scale_x_continuous(
labels = scales::comma,
expand = expansion(mult = c(0, 0.15))
) +
labs(
title = "Largest Supervisory Unions in Vermont (2024)",
subtitle = "Even the largest districts serve fewer than 5,000 students",
x = "Students",
y = NULL
)
Top 10 Vermont supervisory unions by enrollment
Grade-Level Distribution: Elementary Dominates
Vermont’s grade distribution shows where students are concentrated. The K-5 grades represent the largest share of enrollment, with Pre-K accounting for a notable 10% of all students.
# Vermont data focuses on grade levels rather than demographic subgroups
grade_levels <- c("PK", "K", "01", "02", "03", "04", "05",
"06", "07", "08", "09", "10", "11", "12")
grade_dist <- enr_2024 |>
filter(is_state, subgroup == "total_enrollment",
grade_level %in% grade_levels) |>
mutate(level = case_when(
grade_level == "PK" ~ "Pre-K",
grade_level %in% c("K", "01", "02", "03", "04", "05") ~ "Elementary",
grade_level %in% c("06", "07", "08") ~ "Middle",
TRUE ~ "High School"
)) |>
group_by(level) |>
summarize(n_students = sum(n_students, na.rm = TRUE), .groups = "drop") |>
mutate(pct = n_students / sum(n_students) * 100)
stopifnot(nrow(grade_dist) > 0)
grade_dist
#> # A tibble: 4 × 3
#> level n_students pct
#> <chr> <dbl> <dbl>
#> 1 Elementary 33036 41.7
#> 2 High School 21250 26.8
#> 3 Middle 16849 21.3
#> 4 Pre-K 8108 10.2
level_order <- c("Pre-K", "Elementary", "Middle", "High School")
level_colors <- c("Pre-K" = "#78c679", "Elementary" = "#31a354",
"Middle" = "#006837", "High School" = "#00441b")
grade_dist |>
mutate(level = factor(level, levels = level_order)) |>
ggplot(aes(x = level, y = n_students, fill = level)) +
geom_col() +
geom_text(
aes(label = paste0(scales::comma(n_students), "\n(", round(pct), "%)")),
vjust = -0.2, size = 3.5
) +
scale_fill_manual(values = level_colors) +
scale_y_continuous(
labels = scales::comma,
expand = expansion(mult = c(0, 0.15))
) +
labs(
title = "Vermont Enrollment by Grade Level (2024)",
subtitle = "Elementary grades (K-5) make up the largest share",
x = "Grade Level",
y = "Students"
) +
theme(legend.position = "none")
Enrollment by grade level in Vermont
Top SUs Hold Steady From 2022 to 2024
Vermont’s largest Supervisory Unions show modest enrollment changes over recent years. District name formats changed across years, so we normalize names by stripping suffixes to compare 2022 and 2024.
# Strip SU/SD suffixes for cross-year matching (names changed between years)
strip_suffix <- function(x) {
x |>
gsub(" SUPERVISORY UNION$| SUPERVISORY DISTRICT$| SCHOOL DISTRICT$", "", x = _) |>
gsub(" SU$| SD$", "", x = _) |>
gsub("-", " ", x = _) |>
trimws()
}
enr_regional <- fetch_enr_multi(c(2022, 2024), use_cache = TRUE)
# Aggregate to district level using normalized names
district_totals <- enr_regional |>
filter(is_campus, subgroup == "total_enrollment", grade_level == "TOTAL") |>
mutate(district_short = strip_suffix(district_name)) |>
group_by(end_year, district_short) |>
summarize(n_students = sum(n_students, na.rm = TRUE), .groups = "drop")
# Identify the largest SUs in 2024
top_sus <- district_totals |>
filter(end_year == 2024) |>
arrange(desc(n_students)) |>
head(6) |>
pull(district_short)
regional_top <- district_totals |>
filter(district_short %in% top_sus)
stopifnot(nrow(regional_top) > 0)
regional_top |>
pivot_wider(names_from = end_year, values_from = n_students)
#> # A tibble: 6 × 3
#> district_short `2022` `2024`
#> <chr> <dbl> <dbl>
#> 1 BURLINGTON 3486 3506
#> 2 CHAMPLAIN VALLEY 4215 4150
#> 3 ESSEX WESTFORD 3810 3703
#> 4 MAPLE RUN 2632 2608
#> 5 SOUTH BURLINGTON 2702 2693
#> 6 SOUTHWEST VERMONT 3277 3166
ggplot(regional_top, aes(x = end_year, y = n_students, color = district_short)) +
geom_line(linewidth = 1.2) +
geom_point(size = 3) +
scale_y_continuous(labels = scales::comma) +
scale_color_brewer(palette = "Dark2") +
labs(
title = "Enrollment by Top 6 Vermont Supervisory Unions (2022-2024)",
subtitle = "Most top SUs saw modest declines; Burlington held steady",
x = "Year",
y = "Students",
color = "SU"
) +
theme(legend.position = "right")
Enrollment trends for top Vermont SUs
Most SUs Serve 1,000-2,000 Students
Vermont is a state of small schools. The plurality of supervisory unions serve between 1,000 and 2,000 students, creating challenges for specialized programs and efficiency.
size_levels <- c("Tiny (<500)", "Small (500-999)",
"Medium (1,000-1,999)", "Large (2,000+)")
# Aggregate campus to district level
district_sizes <- enr_2024 |>
filter(is_campus, subgroup == "total_enrollment", grade_level == "TOTAL") |>
group_by(district_name) |>
summarize(n_students = sum(n_students, na.rm = TRUE), .groups = "drop") |>
mutate(size = case_when(
n_students >= 2000 ~ "Large (2,000+)",
n_students >= 1000 ~ "Medium (1,000-1,999)",
n_students >= 500 ~ "Small (500-999)",
TRUE ~ "Tiny (<500)"
)) |>
count(size) |>
mutate(size = factor(size, levels = size_levels))
stopifnot(nrow(district_sizes) > 0)
district_sizes
#> # A tibble: 4 × 2
#> size n
#> <fct> <int>
#> 1 Large (2,000+) 11
#> 2 Medium (1,000-1,999) 27
#> 3 Small (500-999) 10
#> 4 Tiny (<500) 4
size_colors <- c(
"Tiny (<500)" = "#f03b20", "Small (500-999)" = "#feb24c",
"Medium (1,000-1,999)" = "#31a354", "Large (2,000+)" = "#006837"
)
ggplot(district_sizes, aes(x = size, y = n, fill = size)) +
geom_col() +
geom_text(aes(label = n), vjust = -0.5, size = 4) +
scale_fill_manual(values = size_colors) +
scale_y_continuous(expand = expansion(mult = c(0, 0.15))) +
labs(
title = "Vermont Supervisory Unions by Size (2024)",
subtitle = "Most SUs serve between 1,000 and 2,000 students",
x = "District Size",
y = "Number of SUs/SDs"
) +
theme(legend.position = "none",
axis.text.x = element_text(angle = 15, hjust = 1))
Distribution of supervisory union sizes in Vermont
Kindergarten Dropped 11% From 2019 to 2021
Vermont’s kindergarten enrollment dropped 11% from 2019 to 2021, as families delayed school entry during the pandemic. That smaller cohort is now moving through the elementary grades.
k_trend <- fetch_enr_multi(2017:2024, use_cache = TRUE)
k_enrollment <- k_trend |>
filter(is_state, subgroup == "total_enrollment", grade_level == "K") |>
select(end_year, n_students)
stopifnot(nrow(k_enrollment) > 0)
k_enrollment
#> end_year n_students
#> 1 2017 5786
#> 2 2018 5975
#> 3 2019 5826
#> 4 2020 5879
#> 5 2021 5157
#> 6 2022 5699
#> 7 2023 5404
#> 8 2024 5191
ggplot(k_enrollment, aes(x = end_year, y = n_students)) +
geom_line(color = "#006837", linewidth = 1.2) +
geom_point(color = "#006837", size = 3) +
geom_vline(xintercept = 2020, linetype = "dashed", color = "gray50") +
annotate("text", x = 2020.1, y = max(k_enrollment$n_students),
label = "COVID", hjust = 0, size = 3.5, color = "gray40") +
scale_y_continuous(labels = scales::comma, limits = c(0, NA)) +
labs(
title = "Vermont Kindergarten Enrollment (2017-2024)",
subtitle = "Pandemic delayed kindergarten entry for many families",
x = "Year",
y = "Kindergarten Students"
)
Vermont kindergarten enrollment dropped sharply during COVID
The Northeast Kingdom Serves Under 5,000 Students
Vermont’s remote Northeast Kingdom (Essex, Orleans, and Caledonia counties) enrolled fewer than 5,000 students in 2024 – about 6% of the state total. The NEK lost 6% of its students in just two years.
Note: Vermont district names changed format between years and some years have missing district data, so we compare only 2022 and 2024 for consistency. We exclude Essex-Westford (a Chittenden County district) from the NEK match.
# NEK districts: Kingdom East, Caledonia Central, Orleans Central/Southwest, Essex North
# Exclude Essex-Westford (Chittenden County, not NEK)
nek_pattern <- "KINGDOM|CALEDONIA|ORLEANS|ESSEX NORTH"
nek_trend <- fetch_enr_multi(c(2022, 2024), use_cache = TRUE)
nek_districts <- nek_trend |>
filter(is_campus, subgroup == "total_enrollment", grade_level == "TOTAL") |>
mutate(district_short = strip_suffix(district_name)) |>
filter(grepl(nek_pattern, district_short, ignore.case = TRUE)) |>
group_by(end_year) |>
summarize(n_students = sum(n_students, na.rm = TRUE), .groups = "drop")
stopifnot(nrow(nek_districts) > 0)
nek_districts
#> # A tibble: 2 × 2
#> end_year n_students
#> <dbl> <dbl>
#> 1 2022 4896
#> 2 2024 4599
ggplot(nek_districts, aes(x = end_year, y = n_students)) +
geom_line(color = "#d95f02", linewidth = 1.2) +
geom_point(color = "#d95f02", size = 3) +
scale_y_continuous(labels = scales::comma, limits = c(0, NA)) +
labs(
title = "Northeast Kingdom Enrollment (2022-2024)",
subtitle = "Rural Vermont's population challenge continues",
x = "Year",
y = "Students"
)
Northeast Kingdom enrollment has declined faster than the state average
High School Holds Steadier Than Elementary
While elementary enrollment has dropped sharply, high school grades have been more stable. Grade 12 enrollment has declined less than kindergarten, reflecting smaller incoming cohorts replacing larger graduating classes.
hs_compare <- fetch_enr_multi(c(2010, 2015, 2020, 2024), use_cache = TRUE)
grade_compare <- hs_compare |>
filter(is_state, subgroup == "total_enrollment",
grade_level %in% c("K", "12")) |>
select(end_year, grade_level, n_students) |>
mutate(grade_level = factor(grade_level, levels = c("K", "12"),
labels = c("Kindergarten", "Grade 12")))
stopifnot(nrow(grade_compare) > 0)
grade_compare
#> end_year grade_level n_students
#> 1 2010 Kindergarten 6205
#> 2 2010 Grade 12 6683
#> 3 2015 Kindergarten 5763
#> 4 2015 Grade 12 5727
#> 5 2020 Kindergarten 5879
#> 6 2020 Grade 12 5088
#> 7 2024 Kindergarten 5191
#> 8 2024 Grade 12 4823
ggplot(grade_compare, aes(x = end_year, y = n_students, color = grade_level)) +
geom_line(linewidth = 1.2) +
geom_point(size = 3) +
geom_vline(xintercept = 2020, linetype = "dashed", color = "gray50") +
annotate("text", x = 2020.2, y = max(grade_compare$n_students),
label = "COVID", hjust = 0, size = 3.5, color = "gray40") +
scale_color_manual(values = c("Kindergarten" = "#1b9e77", "Grade 12" = "#7570b3")) +
scale_y_continuous(labels = scales::comma, limits = c(0, NA)) +
labs(
title = "Kindergarten vs. Grade 12 Enrollment in Vermont",
subtitle = "Elementary grades feel the demographic decline first",
x = "Year",
y = "Students",
color = "Grade"
)
High school enrollment has declined less than kindergarten
The Tiniest Schools in America
Vermont is home to some of the smallest public schools in the nation. Dozens of schools enroll fewer than 100 students, and some serve only a handful of children.
tiny_schools <- enr_2024 |>
filter(is_campus, subgroup == "total_enrollment", grade_level == "TOTAL") |>
filter(n_students > 0, n_students < 100) |>
mutate(size_bin = cut(n_students, breaks = c(0, 25, 50, 75, 100),
labels = c("1-25", "26-50", "51-75", "76-99"))) |>
count(size_bin)
stopifnot(nrow(tiny_schools) > 0)
tiny_schools
#> size_bin n
#> 1 1-25 3
#> 2 26-50 10
#> 3 51-75 21
#> 4 76-99 25
ggplot(tiny_schools, aes(x = size_bin, y = n)) +
geom_col(fill = "#7570b3") +
geom_text(aes(label = n), vjust = -0.5, size = 4) +
scale_y_continuous(expand = expansion(mult = c(0, 0.15))) +
labs(
title = "Vermont's Tiny Schools (Under 100 Students)",
subtitle = "Small rural communities maintain neighborhood schools",
x = "Enrollment Size",
y = "Number of Schools"
)
Many Vermont schools serve fewer than 100 students
Chittenden County Enrolls Nearly a Quarter of Vermont Students
Chittenden County (the Burlington metro area) enrolls about 24% of Vermont’s students. The seven Chittenden County supervisory unions – Burlington, Champlain Valley, Colchester, Essex-Westford, Milton, South Burlington, and Winooski – anchor the state’s enrollment.
Note: Vermont district name formats changed across data years and 2018-2020 data lacks district names, so we compare 2022 and 2024 for consistency.
# Chittenden County SUs: Burlington, Champlain Valley, Colchester,
# Essex-Westford, Milton, South Burlington, Winooski
chittenden_pattern <- "BURLINGTON|SOUTH BURLINGTON|COLCHESTER|ESSEX.WESTFORD|CHAMPLAIN VALLEY|WINOOSKI|MILTON"
chittenden_trend <- fetch_enr_multi(c(2022, 2024), use_cache = TRUE)
chittenden_area <- chittenden_trend |>
filter(is_campus, subgroup == "total_enrollment", grade_level == "TOTAL") |>
mutate(district_short = strip_suffix(district_name)) |>
mutate(region = if_else(
grepl(chittenden_pattern, district_short, ignore.case = TRUE),
"Chittenden County", "Rest of Vermont"
)) |>
group_by(end_year, region) |>
summarize(n_students = sum(n_students, na.rm = TRUE), .groups = "drop")
stopifnot(nrow(chittenden_area) > 0)
chittenden_area
#> # A tibble: 4 × 3
#> end_year region n_students
#> <dbl> <chr> <dbl>
#> 1 2022 Chittenden County 18926
#> 2 2022 Rest of Vermont 61831
#> 3 2024 Chittenden County 18718
#> 4 2024 Rest of Vermont 60570
ggplot(chittenden_area, aes(x = end_year, y = n_students, fill = region)) +
geom_col(position = "stack") +
scale_fill_manual(values = c("Chittenden County" = "#006837",
"Rest of Vermont" = "#a1d99b")) +
scale_y_continuous(labels = scales::comma) +
labs(
title = "Vermont Enrollment: Chittenden vs. Rest of State",
subtitle = "Chittenden County accounts for about 24% of state enrollment",
x = "Year",
y = "Students",
fill = "Region"
)
Chittenden County holds a substantial share of Vermont students
59 Tiny Schools Serve Under 100 Students
Vermont maintains a remarkable number of extremely small schools. These neighborhood schools serve tight-knit rural communities but face efficiency challenges.
Half SUs and Half SDs, Plus One Oddity
Vermont’s school governance differs from most states. Schools are organized into Supervisory Unions (SUs) or Supervisory Districts (SDs) that share administrative services, allowing small communities to maintain local schools while pooling resources. One district (SCHOOL ADMINISTRATIVE UNIT #70) fits neither category.
# Count unique district types from campus data
su_types <- enr_2024 |>
filter(is_campus, subgroup == "total_enrollment", grade_level == "TOTAL") |>
distinct(district_name) |>
mutate(type = case_when(
grepl("SUPERVISORY UNION", district_name) ~ "Supervisory Union",
grepl("SUPERVISORY DISTRICT|SCHOOL DISTRICT", district_name) ~ "Supervisory District",
TRUE ~ "Other"
)) |>
count(type)
stopifnot(nrow(su_types) > 0)
su_types
#> type n
#> 1 Other 1
#> 2 Supervisory District 26
#> 3 Supervisory Union 25Pre-K Enrollment Reaches 8,100 Students
Vermont’s public Pre-K program serves over 8,000 students, accounting for about 10% of total enrollment. Vermont has been a leader in universal pre-K access.
Both Regions Declined Slightly From 2022 to 2024
Both Chittenden County and the rest of Vermont saw modest enrollment declines between 2022 and 2024, with the rest of the state losing a slightly larger share.
# Compare 2022 to 2024 by region using chittenden_area data
chittenden_area |>
pivot_wider(names_from = end_year, values_from = n_students) |>
mutate(pct_change = round((`2024` - `2022`) / `2022` * 100, 1))
#> # A tibble: 2 × 4
#> region `2022` `2024` pct_change
#> <chr> <dbl> <dbl> <dbl>
#> 1 Chittenden County 18926 18718 -1.1
#> 2 Rest of Vermont 61831 60570 -2Data Only Shows Total Enrollment by Grade
Unlike most states, Vermont’s enrollment data focuses on total counts by grade level. Race/ethnicity and special population breakdowns require separate datasets from the Vermont Education Dashboard.
enr_2024 |>
distinct(subgroup)
#> subgroup
#> 1 total_enrollmentSummary
Vermont’s public school enrollment tells a story of rural challenges and demographic transition:
- Steady decline: Lost 14% of students since 2004
- Small scale: Even the largest SUs serve fewer than 5,000 students
- Supervisory structure: 26 SDs, 25 SUs, and one administrative unit
- Chittenden concentration: Burlington area enrolls 24% of the state
- Many mid-size districts: Most SUs serve 1,000-2,000 students
- COVID kindergarten shock: K enrollment dropped 11% from 2019 to 2021
These patterns reflect Vermont’s aging population, low birth rates, and the ongoing challenge of providing quality education in a small, rural state.
sessionInfo()
#> R version 4.5.2 (2025-10-31)
#> Platform: x86_64-pc-linux-gnu
#> Running under: Ubuntu 24.04.3 LTS
#>
#> Matrix products: default
#> BLAS: /usr/lib/x86_64-linux-gnu/openblas-pthread/libblas.so.3
#> LAPACK: /usr/lib/x86_64-linux-gnu/openblas-pthread/libopenblasp-r0.3.26.so; LAPACK version 3.12.0
#>
#> locale:
#> [1] LC_CTYPE=C.UTF-8 LC_NUMERIC=C LC_TIME=C.UTF-8
#> [4] LC_COLLATE=C.UTF-8 LC_MONETARY=C.UTF-8 LC_MESSAGES=C.UTF-8
#> [7] LC_PAPER=C.UTF-8 LC_NAME=C LC_ADDRESS=C
#> [10] LC_TELEPHONE=C LC_MEASUREMENT=C.UTF-8 LC_IDENTIFICATION=C
#>
#> time zone: UTC
#> tzcode source: system (glibc)
#>
#> attached base packages:
#> [1] stats graphics grDevices utils datasets methods base
#>
#> other attached packages:
#> [1] ggplot2_4.0.2 tidyr_1.3.2 dplyr_1.2.0 vtschooldata_0.1.0
#>
#> loaded via a namespace (and not attached):
#> [1] gtable_0.3.6 jsonlite_2.0.0 compiler_4.5.2 tidyselect_1.2.1
#> [5] jquerylib_0.1.4 systemfonts_1.3.2 scales_1.4.0 textshaping_1.0.5
#> [9] readxl_1.4.5 yaml_2.3.12 fastmap_1.2.0 R6_2.6.1
#> [13] labeling_0.4.3 generics_0.1.4 curl_7.0.0 knitr_1.51
#> [17] tibble_3.3.1 desc_1.4.3 bslib_0.10.0 pillar_1.11.1
#> [21] RColorBrewer_1.1-3 rlang_1.1.7 utf8_1.2.6 cachem_1.1.0
#> [25] xfun_0.56 S7_0.2.1 fs_1.6.7 sass_0.4.10
#> [29] cli_3.6.5 withr_3.0.2 pkgdown_2.2.0 magrittr_2.0.4
#> [33] digest_0.6.39 grid_4.5.2 rappdirs_0.3.4 lifecycle_1.0.5
#> [37] vctrs_0.7.1 evaluate_1.0.5 glue_1.8.0 cellranger_1.1.0
#> [41] farver_2.1.2 codetools_0.2-20 ragg_1.5.1 httr_1.4.8
#> [45] rmarkdown_2.30 purrr_1.2.1 tools_4.5.2 pkgconfig_2.0.3
#> [49] htmltools_0.5.9