Fetch and analyze Wisconsin school enrollment and graduation data from the Wisconsin Department of Public Instruction (DPI) in R or Python. 28 years of enrollment data (1997-2024) and 15 years of graduation rates (2010-2024) for every school, district, and the state via WISEdash.
Part of the State Schooldata Project, extending the original njschooldata package to all 50 states.
Full documentation — all 15 stories with interactive charts, getting-started guide, and complete function reference.
Highlights
library(wischooldata)
library(dplyr)
library(tidyr)
library(ggplot2)
theme_set(theme_minimal(base_size = 14))
# NOTE: 2017-18 file excluded because WI DPI server returns HTTP 503 for that year
enr <- fetch_enr_multi(2019:2024, use_cache = TRUE)1. Wisconsin lost nearly 45,000 students since 2019
Wisconsin enrollment has declined every year since 2019, accelerating after the COVID-19 pandemic. The state has not recovered.
state_totals <- enr |>
filter(is_state, subgroup == "total_enrollment", grade_level == "TOTAL") |>
select(end_year, n_students) |>
mutate(change = n_students - lag(n_students),
pct_change = round(change / lag(n_students) * 100, 2))
state_totals
stopifnot(nrow(state_totals) > 0)
#> # A tibble: 6 x 4
#> end_year n_students change pct_change
#> <dbl> <dbl> <dbl> <dbl>
#> 1 2019 858833 NA NA
#> 2 2020 854959 -3874 -0.45
#> 3 2021 829935 -25024 -2.93
#> 4 2022 829143 -792 -0.10
#> 5 2023 822804 -6339 -0.76
#> 6 2024 814002 -8802 -1.07
2. Hispanic enrollment is growing statewide
Hispanic students are the fastest-growing demographic group in Wisconsin, rising from 12.3% to 14.0% of enrollment since 2019 even as total enrollment fell.
hispanic_trend <- enr |>
filter(is_state, grade_level == "TOTAL", subgroup == "hispanic") |>
select(end_year, n_students, pct) |>
mutate(pct = round(pct * 100, 1))
hispanic_trend
stopifnot(nrow(hispanic_trend) > 0)
#> # A tibble: 6 x 3
#> end_year n_students pct
#> <dbl> <dbl> <dbl>
#> 1 2019 105863 12.3
#> 2 2020 107448 12.6
#> 3 2021 106239 12.8
#> 4 2022 109106 13.2
#> 5 2023 111830 13.6
#> 6 2024 114020 14.0
3. Special Education Varies by Region
Special education rates differ markedly across Wisconsin, with some districts serving nearly three times the proportion of students with disabilities as others.
enr_2024 <- fetch_enr(2024, use_cache = TRUE)
sped_rates <- enr_2024 |>
filter(is_district, grade_level == "TOTAL",
subgroup %in% c("total_enrollment", "special_ed")) |>
select(district_name, subgroup, n_students) |>
pivot_wider(names_from = subgroup, values_from = n_students) |>
filter(total_enrollment > 2000) |>
mutate(pct_sped = round(special_ed / total_enrollment * 100, 1)) |>
arrange(desc(pct_sped))
sped_summary <- bind_rows(
sped_rates |> head(5) |> mutate(group = "Highest"),
sped_rates |> tail(5) |> mutate(group = "Lowest")
)
sped_summary
stopifnot(nrow(sped_summary) > 0)
#> # A tibble: 10 x 5
#> district_name total_enrollment special_ed pct_sped group
#> <chr> <dbl> <dbl> <dbl> <chr>
#> 1 Sparta Area 2794 573 20.5 Highest
#> 2 Reedsburg 2597 529 20.4 Highest
#> 3 Cudahy 2054 415 20.2 Highest
#> 4 Tomah Area 3096 603 19.5 Highest
#> 5 Milwaukee 66864 12924 19.3 Highest
#> 6 Monona Grove 3696 352 9.5 Lowest
#> 7 Franklin Public 4721 428 9.1 Lowest
#> 8 Verona Area 5794 491 8.5 Lowest
#> 9 Slinger 3271 265 8.1 Lowest
#> 10 Arrowhead UHS 2038 143 7.0 Lowest
Data Taxonomy
| Category | Years | Function | Details |
|---|---|---|---|
| Enrollment | 1997-2024 |
fetch_enr() / fetch_enr_multi()
|
State, district, school. Race, gender, FRPL, SpEd, LEP |
| Assessments | — | — | Not yet available |
| Graduation | 2010-2024 |
fetch_graduation() / fetch_graduation_multi()
|
State, district, school. 4/5/6-year rates, subgroups |
| Directory | Current | fetch_directory() |
School name, address, grades, charter/virtual status, locale, county |
| Per-Pupil Spending | — | — | Not yet available |
| Accountability | — | — | Not yet available |
| Chronic Absence | — | — | Not yet available |
| EL Progress | — | — | Not yet available |
| Special Ed | — | — | Not yet available |
See the full data category taxonomy for what each category covers.
Quick Start
R
# install.packages("devtools")
devtools::install_github("almartin82/wischooldata")
library(wischooldata)
library(dplyr)
# Get 2024 enrollment data (2023-24 school year)
enr <- fetch_enr(2024)
# Statewide total
enr |>
filter(is_state, subgroup == "total_enrollment", grade_level == "TOTAL") |>
pull(n_students)
#> 814002
# Top 5 districts
enr |>
filter(is_district, subgroup == "total_enrollment", grade_level == "TOTAL") |>
arrange(desc(n_students)) |>
select(district_name, n_students) |>
head(5)Python
import pywischooldata as wi
# Get 2024 enrollment data (2023-24 school year)
df = wi.fetch_enr(2024)
# Statewide total
state_total = df[(df['is_state']) &
(df['subgroup'] == 'total_enrollment') &
(df['grade_level'] == 'TOTAL')]['n_students'].values[0]
print(state_total)
#> 814002
# Top 5 districts
districts = df[(df['is_district']) &
(df['subgroup'] == 'total_enrollment') &
(df['grade_level'] == 'TOTAL')]
print(districts.nlargest(5, 'n_students')[['district_name', 'n_students']])Explore More
Full analysis with 15 stories: - 15 Insights from Wisconsin School Enrollment Data — 15 stories - Function reference
Data Notes
Data Source: Wisconsin Department of Public Instruction (DPI)
Available Years: 1997-2024 (28 years of enrollment), 2010-2024 (15 years of graduation rates)
| Era | Years | Source |
|---|---|---|
| WINSS/Published | 1997-2005 | Published Excel files |
| WISEdash Early | 2006-2015 | Published Excel files |
| WISEdash Modern | 2016-2024 | WISEdash CSV downloads |
| Graduation | 2010-2024 | WISEdash HS Completion ZIP |
Census Day: Third Friday Count Day in September. This is Wisconsin’s official enrollment count date.
Suppression Rules: - Student counts less than 6 are suppressed and shown as “*” in source data - When a count is suppressed, the package returns NA - Suppression is applied to protect student privacy in small subgroups
Data Quality Notes: - District consolidations and name changes occur over time - Some historical years have fewer subgroup breakdowns than recent years - CESA boundaries occasionally change; use cesa column for regional analysis - Virtual and charter schools may be reported differently across years
28 years total across ~2,200 schools and 449 districts.
Deeper Dive
4. Milwaukee dominates the enrollment landscape
Milwaukee Public Schools is by far the largest district, serving nearly 67,000 students—more than the next two districts combined.
top_10 <- enr_2024 |>
filter(is_district, subgroup == "total_enrollment", grade_level == "TOTAL") |>
arrange(desc(n_students)) |>
head(10) |>
select(district_name, n_students)
top_10
stopifnot(nrow(top_10) > 0)
#> # A tibble: 10 x 2
#> district_name n_students
#> <chr> <dbl>
#> 1 Milwaukee 66864
#> 2 Madison Metropolitan 25247
#> 3 Kenosha 18719
#> 4 Green Bay Area Public 18579
#> 5 Racine Unified 15963
#> 6 Appleton Area 15230
#> 7 Waukesha 11318
#> 8 Eau Claire Area 10866
#> 9 Sheboygan Area 9427
#> 10 Janesville 9414
5. White students are two-thirds of statewide enrollment
Wisconsin remains predominantly white at 66.5%, but the student body is diversifying—Hispanic students now account for 14% and growing.
demographics <- enr_2024 |>
filter(is_state, grade_level == "TOTAL",
subgroup %in% c("hispanic", "white", "black", "asian", "multiracial", "native_american")) |>
mutate(pct = round(pct * 100, 1)) |>
select(subgroup, n_students, pct) |>
arrange(desc(n_students))
demographics
stopifnot(nrow(demographics) > 0)
#> # A tibble: 6 x 3
#> subgroup n_students pct
#> <chr> <dbl> <dbl>
#> 1 white 541411 66.5
#> 2 hispanic 114020 14.0
#> 3 black 71146 8.7
#> 4 multiracial 43621 5.4
#> 5 asian 34881 4.3
#> 6 native_american 8245 1.0
6. Wisconsin’s 12 CESAs organize regional services
Wisconsin divides into 12 Cooperative Educational Service Agencies (CESAs) that provide support services to districts. CESA 1 (southeastern Wisconsin, including Milwaukee) dwarfs all others with 236,000 students.
cesa_totals <- enr_2024 |>
filter(is_district, subgroup == "total_enrollment", grade_level == "TOTAL",
!is.na(cesa)) |>
group_by(cesa) |>
summarize(
n_districts = n_distinct(district_id),
total_students = sum(n_students, na.rm = TRUE),
.groups = "drop"
) |>
arrange(desc(total_students))
cesa_totals
stopifnot(nrow(cesa_totals) > 0)
#> # A tibble: 12 x 3
#> cesa n_districts total_students
#> <chr> <int> <dbl>
#> 1 01 66 236097
#> 2 02 78 149803
#> 3 06 39 95481
#> 4 07 38 82704
#> 5 05 36 49470
#> 6 11 39 47054
#> 7 10 29 35729
#> 8 04 26 34420
#> 9 09 22 32060
#> 10 08 27 19196
#> 11 03 31 18121
#> 12 12 18 13867
7. Most large districts are shrinking, with rare exceptions
Only Elmbrook and Verona bucked the trend among large districts—most lost students between 2019 and 2024, even in the suburbs.
growth <- enr |>
filter(is_district, subgroup == "total_enrollment", grade_level == "TOTAL",
end_year %in% c(2019, 2024)) |>
group_by(district_id, district_name) |>
filter(n() == 2) |>
summarize(
y2019 = n_students[end_year == 2019],
y2024 = n_students[end_year == 2024],
pct_change = round((y2024 / y2019 - 1) * 100, 1),
.groups = "drop"
) |>
filter(y2019 > 5000) |>
arrange(desc(pct_change)) |>
head(10)
growth
stopifnot(nrow(growth) > 0)
# Note: values below are from the last vignette build and may shift with data updates
#> # A tibble: 10 x 5
#> district_id district_name y2019 y2024 pct_change
#> <chr> <chr> <dbl> <dbl> <dbl>
#> 1 0714 Elmbrook 7271 7863 8.1
#> 2 5901 Verona Area 5642 5794 2.7
#> 3 5656 Sun Prairie Area 8611 8411 -2.3
#> 4 4018 Oak Creek-Franklin Joint 6587 6527 -0.9
#> 5 4970 D C Everest Area 5942 5954 0.2
#> 6 5607 Stevens Point Area Public 7114 6980 -1.9
#> 7 2835 Kimberly Area 5183 5058 -2.4
#> 8 3892 Neenah Joint 6694 6497 -2.9
#> 9 3549 Middleton-Cross Plains Area 7289 7059 -3.2
#> 10 1554 Eau Claire Area 11306 10866 -3.9
8. Milwaukee’s enrollment has declined significantly
Milwaukee Public Schools has lost thousands of students in recent years, driven by choice programs, charter schools, and population shifts.
milwaukee <- enr |>
filter(is_district, subgroup == "total_enrollment", grade_level == "TOTAL",
district_name == "Milwaukee")
milwaukee_summary <- milwaukee |>
select(end_year, district_name, n_students) |>
arrange(district_name, end_year)
milwaukee_summary
stopifnot(nrow(milwaukee_summary) > 0)
#> # A tibble: 6 x 3
#> end_year district_name n_students
#> <dbl> <chr> <dbl>
#> 1 2019 Milwaukee 75431
#> 2 2020 Milwaukee 74683
#> 3 2021 Milwaukee 71510
#> 4 2022 Milwaukee 69115
#> 5 2023 Milwaukee 67500
#> 6 2024 Milwaukee 66864
9. High school grades outpace elementary enrollment
Wisconsin enrolls more students in grades 9-12 than in early grades—a sign that high school retention is strong even as overall enrollment declines.
grade_breakdown <- enr_2024 |>
filter(is_state, subgroup == "total_enrollment",
grade_level %in% c("PK", "K", "01", "05", "09", "12")) |>
select(grade_level, n_students) |>
arrange(match(grade_level, c("PK", "K", "01", "05", "09", "12")))
grade_breakdown
stopifnot(nrow(grade_breakdown) > 0)
#> # A tibble: 5 x 2
#> grade_level n_students
#> <chr> <dbl>
#> 1 PK 6363
#> 2 K 51787
#> 3 01 53983
#> 4 05 56459
#> 5 09 65035
#> 6 12 64957
10. Rural dairy country districts are small but numerous
Wisconsin has hundreds of small rural districts, many in the state’s famous dairy farming regions. Nearly 58% of districts have fewer than 1,000 students.
size_distribution <- enr_2024 |>
filter(is_district, subgroup == "total_enrollment", grade_level == "TOTAL") |>
mutate(size_category = case_when(
n_students < 500 ~ "Under 500",
n_students < 1000 ~ "500-999",
n_students < 2500 ~ "1,000-2,499",
n_students < 5000 ~ "2,500-4,999",
n_students < 10000 ~ "5,000-9,999",
TRUE ~ "10,000+"
)) |>
mutate(size_category = factor(size_category,
levels = c("Under 500", "500-999", "1,000-2,499", "2,500-4,999", "5,000-9,999", "10,000+"))) |>
count(size_category) |>
mutate(pct = round(n / sum(n) * 100, 1))
size_distribution
stopifnot(nrow(size_distribution) > 0)
#> # A tibble: 6 x 3
#> size_category n pct
#> <fct> <int> <dbl>
#> 1 Under 500 136 30.3
#> 2 500-999 124 27.6
#> 3 1,000-2,499 108 24.1
#> 4 2,500-4,999 50 11.1
#> 5 5,000-9,999 23 5.1
#> 6 10,000+ 8 1.8
11. Green Bay anchors northeastern Wisconsin
Green Bay Area Public Schools is the largest district in northeastern Wisconsin, serving the region’s industrial and shipping hub.
fox_valley <- enr_2024 |>
filter(is_district, subgroup == "total_enrollment", grade_level == "TOTAL",
grepl("Green Bay|Appleton|Oshkosh|Fond du Lac", district_name, ignore.case = TRUE)) |>
select(district_name, n_students) |>
arrange(desc(n_students))
fox_valley
stopifnot(nrow(fox_valley) > 0)
#> # A tibble: 5 x 2
#> district_name n_students
#> <chr> <dbl>
#> 1 Green Bay Area Public 18579
#> 2 Appleton Area 15230
#> 3 Oshkosh Area 9113
#> 4 Fond du Lac 6419
#> 5 North Fond du Lac 1555
12. The WOW Counties: Suburban Milwaukee’s Demographic Mix
The WOW counties (Waukesha, Ozaukee, Washington) form an affluent suburban ring around Milwaukee with distinct demographic profiles—higher percentages of white students than the statewide average.
# Identify WOW-area districts by name patterns
wow_districts <- enr_2024 |>
filter(is_district, grade_level == "TOTAL",
subgroup %in% c("total_enrollment", "white")) |>
select(district_name, subgroup, n_students) |>
pivot_wider(names_from = subgroup, values_from = n_students) |>
filter(grepl("Waukesha|Germantown|Cedarburg|Mequon|Hartford|West Bend|Grafton|Slinger|Elmbrook|Kettle Moraine",
district_name, ignore.case = TRUE)) |>
mutate(pct_white = round(white / total_enrollment * 100, 1)) |>
filter(total_enrollment > 1000) |>
arrange(desc(pct_white))
wow_districts
stopifnot(nrow(wow_districts) > 0)
#> # A tibble: 11 x 4
#> district_name total_enrollment white pct_white
#> <chr> <dbl> <dbl> <dbl>
#> 1 Cedarburg 3101 2781 89.7
#> 2 Slinger 3271 2932 89.6
#> 3 Kettle Moraine 3421 3010 88.0
#> 4 Hartford UHS 1364 1158 84.9
#> 5 Grafton 2132 1750 82.1
#> 6 West Bend 5591 4495 80.4
#> 7 Germantown 3816 2999 78.6
#> 8 Hartford J1 1429 1120 78.4
#> 9 Mequon-Thiensville 3570 2667 74.7
#> 10 Elmbrook 7863 5452 69.3
#> 11 Waukesha 11318 6853 60.5
13. Madison vs. Milwaukee: A Tale of Two Cities
Wisconsin’s two largest cities are both losing students, but Milwaukee’s decline is far steeper—Milwaukee’s losses dwarf Madison’s over the same period.
two_cities <- enr |>
filter(is_district, subgroup == "total_enrollment", grade_level == "TOTAL",
grepl("^Milwaukee$|Madison Metropolitan", district_name)) |>
select(end_year, district_name, n_students)
two_cities
stopifnot(nrow(two_cities) > 0)
#> # A tibble: 12 x 3
#> end_year district_name n_students
#> <dbl> <chr> <dbl>
#> 1 2019 Madison Metropolitan 26917
#> 2 2019 Milwaukee 75431
#> 3 2020 Madison Metropolitan 26842
#> 4 2020 Milwaukee 74683
#> 5 2021 Madison Metropolitan 26151
#> 6 2021 Milwaukee 71510
#> 7 2022 Madison Metropolitan 25497
#> 8 2022 Milwaukee 69115
#> 9 2023 Madison Metropolitan 25237
#> 10 2023 Milwaukee 67500
#> 11 2024 Madison Metropolitan 25247
#> 12 2024 Milwaukee 66864
14. English Learners Concentrated in Urban Areas
Limited English Proficiency (LEP) students are heavily concentrated in a handful of urban districts, with Green Bay and Madison having the highest LEP rates among large districts.
lep_districts <- enr_2024 |>
filter(is_district, grade_level == "TOTAL",
subgroup %in% c("total_enrollment", "lep")) |>
select(district_name, subgroup, n_students) |>
pivot_wider(names_from = subgroup, values_from = n_students) |>
filter(lep > 200) |>
mutate(pct_lep = round(lep / total_enrollment * 100, 1)) |>
arrange(desc(lep)) |>
head(10)
lep_districts
stopifnot(nrow(lep_districts) > 0)
#> # A tibble: 10 x 4
#> district_name total_enrollment lep pct_lep
#> <chr> <dbl> <dbl> <dbl>
#> 1 Milwaukee 66864 10404 15.6
#> 2 Madison Metropolitan 25247 5377 21.3
#> 3 Green Bay Area Public 18579 4011 21.6
#> 4 Racine Unified 15963 1953 12.2
#> 5 Kenosha 18719 1760 9.4
#> 6 Appleton Area 15230 1653 10.9
#> 7 Sheboygan Area 9427 1646 17.5
#> 8 Beloit 5098 926 18.2
#> 9 Waukesha 11318 830 7.3
#> 10 Verona Area 5794 822 14.2
15. The Driftless Region’s Small School Districts
Southwestern Wisconsin’s Driftless Area—unglaciated terrain known for dairy farms and winding valleys—is home to dozens of tiny school districts.
# Driftless region includes Crawford, Grant, Iowa, Lafayette, Richland, Vernon, and parts of others
driftless_districts <- enr_2024 |>
filter(is_district, subgroup == "total_enrollment", grade_level == "TOTAL",
grepl("Prairie du Chien|Richland|Viroqua|Kickapoo|Westby|Cashton|La Farge|Hillsboro|Wonewoc|Necedah|Royall|Boscobel|Lancaster|Platteville|Fennimore|Potosi|Cassville|Seneca|River Ridge|Ithaca|Weston|De Soto|North Crawford|Riverdale|Pecatonica|Iowa-Grant|Highland|Mineral Point|Dodgeville",
district_name, ignore.case = TRUE)) |>
select(district_name, n_students) |>
arrange(n_students)
driftless_districts
stopifnot(nrow(driftless_districts) > 0)
#> # A tibble: 29 x 2
#> district_name n_students
#> <chr> <dbl>
#> 1 Cassville 169
#> 2 Weston 233
#> 3 La Farge 260
#> 4 Highland 268
#> 5 Seneca Area 270
#> 6 Potosi 327
#> 7 Ithaca 352
#> 8 Wonewoc-Union Center 384
#> 9 Pecatonica Area 393
#> 10 North Crawford 421
#> # ... with 19 more rows