Skip to contents

This vignette explores Wisconsin’s public school enrollment data, surfacing key trends and demographic patterns across the Badger State’s school system.


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.

# NOTE: 2017-18 file excluded because WI DPI server returns HTTP 503 for that year
enr <- fetch_enr_multi(2019:2024, use_cache = TRUE)

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
#>   end_year n_students change pct_change
#> 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
stopifnot(nrow(state_totals) > 0)
ggplot(state_totals, aes(x = end_year, y = n_students)) +
  geom_line(linewidth = 1.2, color = "#C5050C") +
  geom_point(size = 3, color = "#C5050C") +
  scale_y_continuous(labels = scales::comma, limits = c(0, NA)) +
  labs(
    title = "Wisconsin Public School Enrollment (2019-2024)",
    subtitle = "Tracking enrollment trends across the Badger State",
    x = "School Year (ending)",
    y = "Total Enrollment"
  )


2. 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.

enr_2024 <- fetch_enr(2024, use_cache = TRUE)

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
#>            district_name n_students
#> 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
stopifnot(nrow(top_10) > 0)
top_10 |>
  mutate(district_name = forcats::fct_reorder(district_name, n_students)) |>
  ggplot(aes(x = n_students, y = district_name)) +
  geom_col(fill = "#C5050C") +
  scale_x_continuous(labels = scales::comma) +
  labs(
    title = "Wisconsin's 10 Largest School Districts (2024)",
    x = "Total Enrollment",
    y = NULL
  )


3. 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
#>          subgroup n_students  pct
#> 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
stopifnot(nrow(demographics) > 0)
demographics |>
  mutate(subgroup = forcats::fct_reorder(subgroup, n_students)) |>
  ggplot(aes(x = n_students, y = subgroup, fill = subgroup)) +
  geom_col(show.legend = FALSE) +
  geom_text(aes(label = paste0(pct, "%")), hjust = -0.1) +
  scale_x_continuous(labels = scales::comma, expand = expansion(mult = c(0, 0.15))) +
  scale_fill_brewer(palette = "Set2") +
  labs(
    title = "Wisconsin Student Demographics (2024)",
    subtitle = "Statewide racial/ethnic composition",
    x = "Number of Students",
    y = NULL
  )


4. Wisconsin’s 12 CESAs organize regional services

Wisconsin divides into 12 Cooperative Educational Service Agencies (CESAs) that provide support services to districts. Enrollment varies widely by region.

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
#> # A tibble: 12 × 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
stopifnot(nrow(cesa_totals) > 0)
cesa_totals |>
  mutate(cesa = forcats::fct_reorder(as.factor(cesa), total_students)) |>
  ggplot(aes(x = total_students, y = cesa)) +
  geom_col(fill = "#282728") +
  scale_x_continuous(labels = scales::comma) +
  labs(
    title = "Enrollment by CESA Region (2024)",
    subtitle = "Wisconsin's 12 Cooperative Educational Service Agencies",
    x = "Total Enrollment",
    y = "CESA"
  )


5. 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
#> # A tibble: 10 × 5
#>    district_id district_name             y2019 y2024 pct_change
#>    <chr>       <chr>                     <dbl> <dbl>      <dbl>
#>  1 0714        Elmbrook                   7334  7863        7.2
#>  2 5901        Verona Area                5656  5794        2.4
#>  3 4970        D C Everest Area           6004  5954       -0.8
#>  4 4018        Oak Creek-Franklin Joint   6604  6527       -1.2
#>  5 5656        Sun Prairie Area           8521  8411       -1.3
#>  6 5607        Stevens Point Area Public  7144  6980       -2.3
#>  7 3822        Mukwonago                  5040  4918       -2.4
#>  8 2835        Kimberly Area              5190  5058       -2.5
#>  9 3892        Neenah Joint               6714  6497       -3.2
#> 10 1554        Eau Claire Area           11355 10866       -4.3
stopifnot(nrow(growth) > 0)
growth |>
  mutate(district_name = forcats::fct_reorder(district_name, pct_change)) |>
  ggplot(aes(x = pct_change, y = district_name, fill = pct_change > 0)) +
  geom_col(show.legend = FALSE) +
  geom_vline(xintercept = 0, linetype = "dashed", color = "gray40") +
  scale_fill_manual(values = c("TRUE" = "#0479A8", "FALSE" = "#C5050C")) +
  labs(
    title = "Enrollment Change in Large Districts (2019-2024)",
    subtitle = "Districts with 5,000+ students in 2019",
    x = "Percent Change",
    y = NULL
  )


6. 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
#>   end_year district_name n_students
#> 1     2019     Milwaukee      75431
#> 2     2020     Milwaukee      74683
#> 3     2021     Milwaukee      71510
#> 4     2022     Milwaukee      69115
#> 5     2023     Milwaukee      67500
#> 6     2024     Milwaukee      66864
stopifnot(nrow(milwaukee_summary) > 0)
milwaukee_summary |>
  filter(district_name == "Milwaukee") |>
  ggplot(aes(x = end_year, y = n_students)) +
  geom_line(linewidth = 1.2, color = "#C5050C") +
  geom_point(size = 3, color = "#C5050C") +
  scale_y_continuous(labels = scales::comma, limits = c(0, NA)) +
  labs(
    title = "Milwaukee Public Schools Enrollment Decline",
    subtitle = "Annual enrollment 2019-2024",
    x = "School Year (ending)",
    y = "Total Enrollment"
  )


7. 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
#>   grade_level n_students
#> 1          PK       6363
#> 2           K      51787
#> 3          01      53983
#> 4          05      56459
#> 5          09      65035
#> 6          12      64957
stopifnot(nrow(grade_breakdown) > 0)
grade_breakdown |>
  mutate(grade_level = factor(grade_level, levels = c("PK", "K", "01", "05", "09", "12"))) |>
  ggplot(aes(x = grade_level, y = n_students, fill = grade_level %in% c("09", "12"))) +
  geom_col(show.legend = FALSE) +
  scale_y_continuous(labels = scales::comma) +
  scale_fill_manual(values = c("TRUE" = "#C5050C", "FALSE" = "#282728")) +
  labs(
    title = "Wisconsin Enrollment by Grade Level (2024)",
    subtitle = "High school grades highlighted in red",
    x = "Grade Level",
    y = "Number of Students"
  )


8. Rural dairy country districts are small but numerous

Wisconsin has hundreds of small rural districts, many in the state’s famous dairy farming regions. Most 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
#>   size_category   n  pct
#> 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
stopifnot(nrow(size_distribution) > 0)
size_distribution |>
  ggplot(aes(x = size_category, y = n, fill = size_category %in% c("Under 500", "500-999"))) +
  geom_col(show.legend = FALSE) +
  geom_text(aes(label = paste0(n, " (", pct, "%)")), vjust = -0.3, size = 3.5) +
  scale_fill_manual(values = c("TRUE" = "#C5050C", "FALSE" = "#282728")) +
  scale_y_continuous(expand = expansion(mult = c(0, 0.15))) +
  labs(
    title = "Wisconsin Districts by Size (2024)",
    subtitle = "Small rural districts (under 1,000 students) highlighted",
    x = "District Size Category",
    y = "Number of Districts"
  ) +
  theme(axis.text.x = element_text(angle = 45, hjust = 1))


9. 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
#>           district_name n_students
#> 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
stopifnot(nrow(fox_valley) > 0)
fox_valley |>
  mutate(district_name = forcats::fct_reorder(district_name, n_students)) |>
  ggplot(aes(x = n_students, y = district_name, fill = grepl("Green Bay", district_name))) +
  geom_col(show.legend = FALSE) +
  scale_x_continuous(labels = scales::comma) +
  scale_fill_manual(values = c("TRUE" = "#203731", "FALSE" = "#FFB612")) +
  labs(
    title = "Northeastern Wisconsin's Largest Districts (2024)",
    subtitle = "Green Bay highlighted as regional anchor",
    x = "Total Enrollment",
    y = NULL
  )


10. Hispanic enrollment is growing statewide

Hispanic students are the fastest-growing demographic group in Wisconsin, particularly in southeastern Wisconsin and agricultural communities.

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
#>   end_year n_students  pct
#> 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
stopifnot(nrow(hispanic_trend) > 0)
hispanic_trend |>
  ggplot(aes(x = end_year, y = n_students)) +
  geom_line(linewidth = 1.2, color = "#C5050C") +
  geom_point(size = 3, color = "#C5050C") +
  scale_y_continuous(labels = scales::comma, limits = c(0, NA)) +
  labs(
    title = "Hispanic Student Enrollment Growth in Wisconsin",
    subtitle = "Fastest-growing demographic group statewide",
    x = "School Year (ending)",
    y = "Hispanic Student Enrollment"
  )


11. 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
#> # A tibble: 11 × 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  
#>  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
stopifnot(nrow(wow_districts) > 0)
wow_districts |>
  mutate(district_name = forcats::fct_reorder(district_name, pct_white)) |>
  ggplot(aes(x = pct_white, y = district_name)) +
  geom_col(fill = "#0479A8") +
  geom_vline(xintercept = 70, linetype = "dashed", color = "gray40") +
  annotate("text", x = 72, y = 1, label = "State avg ~70%", hjust = 0, size = 3) +
  labs(
    title = "White Student Percentage in WOW County Districts",
    subtitle = "Waukesha-Ozaukee-Washington suburban ring (districts >1,000 students)",
    x = "Percent White Students",
    y = NULL
  )


12. Special Education Varies by Region

Special education rates differ markedly across Wisconsin, with some districts serving twice the proportion of students with disabilities as others.

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
#> # A tibble: 10 × 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   Lowest
stopifnot(nrow(sped_summary) > 0)
sped_summary |>
  mutate(district_name = forcats::fct_reorder(district_name, pct_sped)) |>
  ggplot(aes(x = pct_sped, y = district_name, fill = group)) +
  geom_col(show.legend = TRUE) +
  scale_fill_manual(values = c("Highest" = "#C5050C", "Lowest" = "#0479A8")) +
  labs(
    title = "Special Education Rates Vary Widely Across Wisconsin",
    subtitle = "Top 5 and bottom 5 districts with 2,000+ students",
    x = "Percent Special Education",
    y = NULL,
    fill = NULL
  )


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
#>    end_year        district_name n_students
#> 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
stopifnot(nrow(two_cities) > 0)
two_cities |>
  mutate(city = ifelse(grepl("Milwaukee", district_name), "Milwaukee", "Madison")) |>
  ggplot(aes(x = end_year, y = n_students, color = city)) +
  geom_line(linewidth = 1.2) +
  geom_point(size = 3) +
  scale_y_continuous(labels = scales::comma, limits = c(0, NA)) +
  scale_color_manual(values = c("Milwaukee" = "#C5050C", "Madison" = "#0479A8")) +
  labs(
    title = "Milwaukee vs. Madison Enrollment Trends",
    subtitle = "Wisconsin's two largest districts (2019-2024)",
    x = "School Year (ending)",
    y = "Total Enrollment",
    color = NULL
  )


14. English Learners Concentrated in Urban Areas

Limited English Proficiency (LEP) students are heavily concentrated in a handful of urban districts, with Milwaukee and Madison serving the vast majority.

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
#> # A tibble: 10 × 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
stopifnot(nrow(lep_districts) > 0)
lep_districts |>
  mutate(district_name = forcats::fct_reorder(district_name, lep)) |>
  ggplot(aes(x = lep, y = district_name)) +
  geom_col(fill = "#282728") +
  geom_text(aes(label = paste0(pct_lep, "%")), hjust = -0.1, size = 3) +
  scale_x_continuous(labels = scales::comma, expand = expansion(mult = c(0, 0.15))) +
  labs(
    title = "Districts with Most English Learner Students (2024)",
    subtitle = "Top 10 districts by LEP enrollment (>200 students)",
    x = "Number of LEP Students",
    y = NULL
  )


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
#>            district_name n_students
#> 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
#> 11                Royall        451
#> 12          De Soto Area        455
#> 13         Kickapoo Area        471
#> 14             Hillsboro        477
#> 15           River Ridge        505
#> 16               Cashton        619
#> 17             Riverdale        660
#> 18            Iowa-Grant        667
#> 19         Boscobel Area        672
#> 20          Necedah Area        706
#> 21 Mineral Point Unified        782
#> 22   Fennimore Community        836
#> 23 Prairie du Chien Area        971
#> 24           Westby Area        995
#> 25   Lancaster Community        999
#> 26          Viroqua Area       1030
#> 27            Dodgeville       1060
#> 28              Richland       1133
#> 29           Platteville       1544
stopifnot(nrow(driftless_districts) > 0)
driftless_districts |>
  mutate(district_name = forcats::fct_reorder(district_name, n_students)) |>
  ggplot(aes(x = n_students, y = district_name,
             fill = n_students < 500)) +
  geom_col(show.legend = FALSE) +
  scale_x_continuous(labels = scales::comma) +
  scale_fill_manual(values = c("TRUE" = "#C5050C", "FALSE" = "#282728")) +
  labs(
    title = "Driftless Region School Districts",
    subtitle = "Southwestern Wisconsin's small rural districts (under 500 highlighted)",
    x = "Total Enrollment",
    y = NULL
  )


Summary

Wisconsin’s school enrollment data reveals:

  • Statewide decline: Wisconsin has lost nearly 45,000 students since 2019, a trend that accelerated during and after COVID
  • Predominantly white: White students make up two-thirds of statewide enrollment, but diversity is increasing
  • Broad-based decline: Most large districts are shrinking, with only a few suburban exceptions like Elmbrook and Verona
  • Small district heritage: Hundreds of tiny rural districts serve dairy country communities
  • Demographic change: Hispanic enrollment is growing, reshaping the state’s student population
  • Suburban demographics: WOW counties have distinct demographic profiles from urban cores
  • Service variation: Special education and English learner rates vary significantly by region
  • Two cities decline: Both Madison and Milwaukee are losing students, but Milwaukee’s decline is far steeper

These patterns shape education policy across the Badger State.


Data sourced from the Wisconsin Department of Public Instruction.


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        wischooldata_0.1.0
#> 
#> loaded via a namespace (and not attached):
#>  [1] utf8_1.2.6         rappdirs_0.3.4     sass_0.4.10        generics_0.1.4    
#>  [5] hms_1.1.4          digest_0.6.39      magrittr_2.0.4     evaluate_1.0.5    
#>  [9] grid_4.5.2         RColorBrewer_1.1-3 fastmap_1.2.0      jsonlite_2.0.0    
#> [13] httr_1.4.8         purrr_1.2.1        scales_1.4.0       codetools_0.2-20  
#> [17] textshaping_1.0.4  jquerylib_0.1.4    cli_3.6.5          rlang_1.1.7       
#> [21] crayon_1.5.3       bit64_4.6.0-1      withr_3.0.2        cachem_1.1.0      
#> [25] yaml_2.3.12        tools_4.5.2        parallel_4.5.2     tzdb_0.5.0        
#> [29] forcats_1.0.1      curl_7.0.0         vctrs_0.7.1        R6_2.6.1          
#> [33] lifecycle_1.0.5    fs_1.6.6           bit_4.6.0          vroom_1.7.0       
#> [37] ragg_1.5.0         pkgconfig_2.0.3    desc_1.4.3         pkgdown_2.2.0     
#> [41] pillar_1.11.1      bslib_0.10.0       gtable_0.3.6       glue_1.8.0        
#> [45] systemfonts_1.3.1  xfun_0.56          tibble_3.3.1       tidyselect_1.2.1  
#> [49] knitr_1.51         farver_2.1.2       htmltools_0.5.9    rmarkdown_2.30    
#> [53] labeling_0.4.3     readr_2.2.0        compiler_4.5.2     S7_0.2.1