Skip to contents

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
Wisconsin statewide enrollment trends
Wisconsin statewide enrollment trends

(source)

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
Hispanic Student Enrollment Growth
Hispanic Student Enrollment Growth

(source)

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
Special Education by District
Special Education by District

(source)

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
Top Wisconsin districts
Top Wisconsin districts

(source)

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
Wisconsin Student Demographics
Wisconsin Student Demographics

(source)

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
Enrollment by CESA Region
Enrollment by CESA Region

(source)

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
Enrollment Change in Large Districts
Enrollment Change in Large Districts

(source)

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
Milwaukee Public Schools Enrollment Decline
Milwaukee Public Schools Enrollment Decline

(source)

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
Wisconsin Enrollment by Grade Level
Wisconsin Enrollment by Grade Level

(source)

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
Wisconsin Districts by Size
Wisconsin Districts by Size

(source)

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
Northeastern Wisconsin’s Largest Districts
Northeastern Wisconsin’s Largest Districts

(source)

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
WOW Counties Demographics
WOW Counties Demographics

(source)

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
Madison vs Milwaukee Enrollment
Madison vs Milwaukee Enrollment

(source)

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
English Learners by District
English Learners by District

(source)

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
Driftless Region Districts
Driftless Region Districts

(source)