Skip to contents

This vignette explores Washington’s public school enrollment data, surfacing key trends across 16 years of data (2010-2025).


1. Washington added 70,000 students over 16 years before COVID reversed the trend

The Evergreen State grew enrollment steadily from 1.03 million to nearly 1.15 million before the pandemic wiped out years of gains.

enr <- fetch_enr_multi(2010:2025, 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))
stopifnot(nrow(state_totals) > 0)

state_totals
#>    end_year n_students change pct_change
#> 1      2010    1034935     NA         NA
#> 2      2011    1045333  10398       1.00
#> 3      2012    1051404   6071       0.58
#> 4      2013    1058680   7276       0.69
#> 5      2014    1068234   9554       0.90
#> 6      2015    1086314  18080       1.69
#> 7      2016    1100849  14535       1.34
#> 8      2017    1115820  14971       1.36
#> 9      2018    1130714  14894       1.33
#> 10     2019    1137367   6653       0.59
#> 11     2020    1146882   9515       0.84
#> 12     2021    1093331 -53551      -4.67
#> 13     2022    1091343  -1988      -0.18
#> 14     2023    1096695   5352       0.49
#> 15     2024    1100059   3364       0.31
#> 16     2025    1105384   5325       0.48
print(state_totals)
#>    end_year n_students change pct_change
#> 1      2010    1034935     NA         NA
#> 2      2011    1045333  10398       1.00
#> 3      2012    1051404   6071       0.58
#> 4      2013    1058680   7276       0.69
#> 5      2014    1068234   9554       0.90
#> 6      2015    1086314  18080       1.69
#> 7      2016    1100849  14535       1.34
#> 8      2017    1115820  14971       1.36
#> 9      2018    1130714  14894       1.33
#> 10     2019    1137367   6653       0.59
#> 11     2020    1146882   9515       0.84
#> 12     2021    1093331 -53551      -4.67
#> 13     2022    1091343  -1988      -0.18
#> 14     2023    1096695   5352       0.49
#> 15     2024    1100059   3364       0.31
#> 16     2025    1105384   5325       0.48

ggplot(state_totals, aes(x = end_year, y = n_students)) +
  geom_line(linewidth = 1.2, color = "#4B2E83") +
  geom_point(size = 3, color = "#4B2E83") +
  geom_vline(xintercept = 2020.5, linetype = "dashed", color = "red", alpha = 0.5) +
  annotate("text", x = 2020.8, y = max(state_totals$n_students, na.rm = TRUE),
           label = "COVID", hjust = 0, color = "red", size = 3) +
  scale_y_continuous(labels = scales::comma) +
  labs(
    title = "Washington Public School Enrollment (2010-2025)",
    subtitle = "A decade of growth disrupted by the pandemic",
    x = "School Year (ending)",
    y = "Total Enrollment"
  )


2. Seattle peaked at 56,000 students then lost 9% in five years

Washington’s largest district peaked in 2020 and has lost nearly 5,000 students since, even as tech industry growth transformed the region.

seattle <- enr |>
  filter(is_district, subgroup == "total_enrollment", grade_level == "TOTAL",
         grepl("Seattle", district_name, ignore.case = TRUE)) |>
  select(end_year, district_name, n_students) |>
  mutate(pct_of_peak = round(n_students / max(n_students) * 100, 1))
stopifnot(nrow(seattle) > 0)

seattle
#>    end_year                 district_name n_students pct_of_peak
#> 1      2010 Seattle School District No. 1      47058        84.0
#> 2      2011 Seattle School District No. 1      48299        86.2
#> 3      2012 Seattle School District No. 1      49851        88.9
#> 4      2013 Seattle School District No. 1      51201        91.3
#> 5      2014 Seattle School District No. 1      52181        93.1
#> 6      2015 Seattle School District No. 1      53361        95.2
#> 7      2016 Seattle School District No. 1      53767        95.9
#> 8      2017 Seattle School District No. 1      54722        97.6
#> 9      2018 Seattle School District No. 1      55321        98.7
#> 10     2019 Seattle School District No. 1      55325        98.7
#> 11     2020 Seattle School District No. 1      56051       100.0
#> 12     2021 Seattle School District No. 1      54021        96.4
#> 13     2022 Seattle School District No. 1      51653        92.2
#> 14     2023 Seattle School District No. 1      51528        91.9
#> 15     2024 Seattle School District No. 1      50968        90.9
#> 16     2025 Seattle School District No. 1      51200        91.3
top_districts <- enr |>
  filter(is_district, subgroup == "total_enrollment", grade_level == "TOTAL",
         end_year == 2025) |>
  arrange(desc(n_students)) |>
  head(5) |>
  pull(district_id)
stopifnot(length(top_districts) > 0)

top5_data <- enr |>
  filter(is_district, subgroup == "total_enrollment", grade_level == "TOTAL",
         district_id %in% top_districts)
stopifnot(nrow(top5_data) > 0)

print(top5_data |> filter(end_year == 2025) |> select(district_name, n_students))
#>                     district_name n_students
#> 1         Spokane School District      29690
#> 2 Lake Washington School District      31146
#> 3            Kent School District      25455
#> 4          Tacoma School District      29014
#> 5   Seattle School District No. 1      51200

ggplot(top5_data, aes(x = end_year, y = n_students, color = district_name)) +
  geom_line(linewidth = 1.2) +
  geom_point(size = 2) +
  geom_vline(xintercept = 2020.5, linetype = "dashed", color = "red", alpha = 0.5) +
  scale_y_continuous(labels = scales::comma) +
  labs(
    title = "Washington's Top 5 Districts: Enrollment Trends",
    subtitle = "Seattle peaked in 2020 while suburban districts continue growing",
    x = "School Year",
    y = "Enrollment",
    color = "District"
  ) +
  theme(legend.position = "bottom") +
  guides(color = guide_legend(nrow = 2))


3. White students dropped from 64% to 48% as Hispanic enrollment nearly doubled

The state’s demographics reflect Pacific Rim immigration and a growing Hispanic population that rose from 167K to 295K students.

demographics <- enr |>
  filter(is_state, grade_level == "TOTAL",
         subgroup %in% c("white", "hispanic", "asian", "black", "multiracial"),
         end_year %in% c(2010, 2015, 2020, 2025)) |>
  select(end_year, subgroup, n_students, pct) |>
  mutate(pct = round(pct * 100, 1))
stopifnot(nrow(demographics) > 0)

demographics |>
  pivot_wider(names_from = end_year, values_from = c(n_students, pct))
#> # A tibble: 5 × 9
#>   subgroup    n_students_2010 n_students_2015 n_students_2020 n_students_2025
#>   <chr>                 <dbl>           <dbl>           <dbl>           <dbl>
#> 1 white                657143          615697          601749          526102
#> 2 black                 56515           48578           50251           53176
#> 3 hispanic             167426          235730          273842          294985
#> 4 asian                 80375           77981           91377          100676
#> 5 multiracial           35867           81757          101807          101068
#> # ℹ 4 more variables: pct_2010 <dbl>, pct_2015 <dbl>, pct_2020 <dbl>,
#> #   pct_2025 <dbl>
demo_trend <- enr |>
  filter(is_state, grade_level == "TOTAL",
         subgroup %in% c("white", "hispanic", "asian", "black", "multiracial"))
stopifnot(nrow(demo_trend) > 0)

print(demo_trend |> filter(end_year == 2025) |> select(subgroup, n_students, pct))
#>      subgroup n_students        pct
#> 1       white     526102 0.47594501
#> 2       black      53176 0.04810636
#> 3    hispanic     294985 0.26686201
#> 4       asian     100676 0.09107785
#> 5 multiracial     101068 0.09143248

ggplot(demo_trend, aes(x = end_year, y = pct * 100, color = subgroup)) +
  geom_line(linewidth = 1.2) +
  geom_point(size = 2) +
  scale_y_continuous(labels = function(x) paste0(x, "%")) +
  scale_color_brewer(palette = "Set2",
                     labels = c("Asian", "Black", "Hispanic", "Multiracial", "White")) +
  labs(
    title = "Washington's Shifting Demographics (2010-2025)",
    subtitle = "White enrollment declining as Hispanic and multiracial populations grow",
    x = "School Year",
    y = "Percent of Students",
    color = "Race/Ethnicity"
  ) +
  theme(legend.position = "bottom")


4. Puget Sound holds 427K students – nearly 4x Eastern Washington

The I-5 corridor dominates enrollment, but Eastern Washington’s districts face different challenges.

esd_enrollment <- enr |>
  filter(is_district, subgroup == "total_enrollment", grade_level == "TOTAL",
         end_year == 2025, !is.na(esd_name)) |>
  group_by(esd_name) |>
  summarize(
    districts = n(),
    students = sum(n_students, na.rm = TRUE),
    .groups = "drop"
  ) |>
  arrange(desc(students))
stopifnot(nrow(esd_enrollment) > 0)

esd_enrollment
#> # A tibble: 11 × 3
#>    esd_name                                       districts students
#>    <chr>                                              <int>    <dbl>
#>  1 Puget Sound Educational Service District 121          35   427222
#>  2 Northwest Educational Service District 189            35   166971
#>  3 Educational Service District 112                      30    97301
#>  4 Educational Service District 101                      59    94984
#>  5 Educational Service District 123                      22    77778
#>  6 Capital Region ESD 113                                44    75121
#>  7 Educational Service District 105                      25    65229
#>  8 North Central Educational Service District 171        29    48048
#>  9 Olympic Educational Service District 114              15    46477
#> 10 Washington State Charter School Commission            15     4600
#> 11 Spokane Public Schools Charter Authorizer              2      268
print(esd_enrollment)
#> # A tibble: 11 × 3
#>    esd_name                                       districts students
#>    <chr>                                              <int>    <dbl>
#>  1 Puget Sound Educational Service District 121          35   427222
#>  2 Northwest Educational Service District 189            35   166971
#>  3 Educational Service District 112                      30    97301
#>  4 Educational Service District 101                      59    94984
#>  5 Educational Service District 123                      22    77778
#>  6 Capital Region ESD 113                                44    75121
#>  7 Educational Service District 105                      25    65229
#>  8 North Central Educational Service District 171        29    48048
#>  9 Olympic Educational Service District 114              15    46477
#> 10 Washington State Charter School Commission            15     4600
#> 11 Spokane Public Schools Charter Authorizer              2      268

esd_enrollment |>
  mutate(esd_short = gsub("Educational Service District|ESD", "ESD", esd_name)) |>
  mutate(esd_short = forcats::fct_reorder(esd_short, students)) |>
  ggplot(aes(x = students, y = esd_short, fill = students)) +
  geom_col(show.legend = FALSE) +
  geom_text(aes(label = scales::comma(students)), hjust = -0.1, size = 3) +
  scale_x_continuous(labels = scales::comma, expand = expansion(mult = c(0, 0.2))) +
  scale_fill_gradient(low = "#E8E3D3", high = "#4B2E83") +
  labs(
    title = "Washington Enrollment by Educational Service District",
    subtitle = "Puget Sound ESD 121 dominates with 427K students",
    x = "Total Students",
    y = NULL
  )


5. Suburban districts are booming: Sumner-Bonney Lake up 23% since 2015

The fastest-growing large districts are spread across the state, from Sumner-Bonney Lake south of Seattle to Central Valley near Spokane.

growth_rates <- enr |>
  filter(is_district, subgroup == "total_enrollment", grade_level == "TOTAL",
         end_year %in% c(2015, 2025)) |>
  select(district_id, district_name, end_year, n_students) |>
  pivot_wider(names_from = end_year, values_from = n_students, names_prefix = "yr_") |>
  filter(yr_2015 > 5000) |>
  mutate(
    change = yr_2025 - yr_2015,
    pct_change = round((yr_2025 - yr_2015) / yr_2015 * 100, 1)
  ) |>
  arrange(desc(pct_change))
stopifnot(nrow(growth_rates) > 0)

head(growth_rates, 10)
#> # A tibble: 10 × 6
#>    district_id district_name                   yr_2015 yr_2025 change pct_change
#>    <chr>       <chr>                             <dbl>   <dbl>  <dbl>      <dbl>
#>  1 100259      Sumner-Bonney Lake School Dist…    8988   11048   2060       22.9
#>  2 100126      Lake Stevens School District       8515   10215   1700       20  
#>  3 100016      Auburn School District            15722   18234   2512       16  
#>  4 100183      Omak School District               5257    6071    814       15.5
#>  5 100022      Bethel School District            18678   21538   2860       15.3
#>  6 100263      Tahoma School District             8118    9286   1168       14.4
#>  7 100127      Lake Washington School District   27293   31146   3853       14.1
#>  8 100218      Richland School District          12729   14499   1770       13.9
#>  9 100039      Central Valley School District    13396   15102   1706       12.7
#> 10 100195      Pasco School District             17182   19001   1819       10.6
growth_top10 <- head(growth_rates, 10)
print(growth_top10)
#> # A tibble: 10 × 6
#>    district_id district_name                   yr_2015 yr_2025 change pct_change
#>    <chr>       <chr>                             <dbl>   <dbl>  <dbl>      <dbl>
#>  1 100259      Sumner-Bonney Lake School Dist…    8988   11048   2060       22.9
#>  2 100126      Lake Stevens School District       8515   10215   1700       20  
#>  3 100016      Auburn School District            15722   18234   2512       16  
#>  4 100183      Omak School District               5257    6071    814       15.5
#>  5 100022      Bethel School District            18678   21538   2860       15.3
#>  6 100263      Tahoma School District             8118    9286   1168       14.4
#>  7 100127      Lake Washington School District   27293   31146   3853       14.1
#>  8 100218      Richland School District          12729   14499   1770       13.9
#>  9 100039      Central Valley School District    13396   15102   1706       12.7
#> 10 100195      Pasco School District             17182   19001   1819       10.6

growth_top10 |>
  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_text(aes(label = paste0("+", pct_change, "%")),
            hjust = -0.1, size = 3) +
  scale_x_continuous(expand = expansion(mult = c(0.1, 0.15))) +
  scale_fill_manual(values = c("TRUE" = "#4B2E83", "FALSE" = "#C4A055")) +
  labs(
    title = "Fastest Growing Large Districts (2015-2025)",
    subtitle = "Among districts with 5,000+ students in 2015",
    x = "Percent Change",
    y = NULL
  )


6. The kindergarten cliff is real: K enrollment down 14% from 2020 peak

Washington’s kindergarten enrollment dropped during COVID and hasn’t recovered, signaling smaller cohorts ahead.

k_trend <- enr |>
  filter(is_state, subgroup == "total_enrollment", grade_level == "K") |>
  select(end_year, n_students) |>
  mutate(pct_of_peak = round(n_students / max(n_students) * 100, 1))
stopifnot(nrow(k_trend) > 0)

k_trend
#>    end_year n_students pct_of_peak
#> 1      2010      73735        88.9
#> 2      2011      75364        90.9
#> 3      2012      77896        93.9
#> 4      2013      80426        97.0
#> 5      2014      81286        98.0
#> 6      2015      81348        98.1
#> 7      2016      79874        96.3
#> 8      2017      81151        97.8
#> 9      2018      81428        98.2
#> 10     2019      82130        99.0
#> 11     2020      82947       100.0
#> 12     2021      70977        85.6
#> 13     2022      78640        94.8
#> 14     2023      78406        94.5
#> 15     2024      76359        92.1
#> 16     2025      71443        86.1
print(k_trend)
#>    end_year n_students pct_of_peak
#> 1      2010      73735        88.9
#> 2      2011      75364        90.9
#> 3      2012      77896        93.9
#> 4      2013      80426        97.0
#> 5      2014      81286        98.0
#> 6      2015      81348        98.1
#> 7      2016      79874        96.3
#> 8      2017      81151        97.8
#> 9      2018      81428        98.2
#> 10     2019      82130        99.0
#> 11     2020      82947       100.0
#> 12     2021      70977        85.6
#> 13     2022      78640        94.8
#> 14     2023      78406        94.5
#> 15     2024      76359        92.1
#> 16     2025      71443        86.1

ggplot(k_trend, aes(x = end_year, y = n_students)) +
  geom_line(linewidth = 1.2, color = "#4B2E83") +
  geom_point(size = 3, color = "#4B2E83") +
  geom_vline(xintercept = 2020.5, linetype = "dashed", color = "red", alpha = 0.5) +
  annotate("text", x = 2020.8, y = max(k_trend$n_students, na.rm = TRUE),
           label = "COVID", hjust = 0, color = "red", size = 3) +
  scale_y_continuous(labels = scales::comma) +
  labs(
    title = "Washington Kindergarten Enrollment (2010-2025)",
    subtitle = "COVID-era losses persist: 71K in 2025 vs 83K peak in 2020",
    x = "School Year (ending)",
    y = "Kindergarten Enrollment"
  )


7. Spokane anchors Eastern Washington with nearly 30,000 students

Spokane Public Schools serves 29,690 students, making it the largest district east of the Cascades and the state’s second-largest overall.

spokane <- enr |>
  filter(is_district, subgroup == "total_enrollment", grade_level == "TOTAL",
         grepl("Spokane", district_name, ignore.case = TRUE),
         end_year == 2025) |>
  select(district_name, n_students) |>
  arrange(desc(n_students))
stopifnot(nrow(spokane) > 0)

spokane
#>                           district_name n_students
#> 1               Spokane School District      29690
#> 2 East Valley School District (Spokane)       3655
#> 3 West Valley School District (Spokane)       3501
#> 4         Spokane International Academy        830
#> 5            Innovation Spokane Schools        235
spokane_trend <- enr |>
  filter(is_district, subgroup == "total_enrollment", grade_level == "TOTAL",
         grepl("^Spokane School District$", district_name))
stopifnot(nrow(spokane_trend) > 0)

print(spokane_trend |> select(end_year, n_students))
#>    end_year n_students
#> 1      2010      28793
#> 2      2011      29124
#> 3      2012      29034
#> 4      2013      29075
#> 5      2014      29493
#> 6      2015      30307
#> 7      2016      30489
#> 8      2017      30876
#> 9      2018      30898
#> 10     2019      31079
#> 11     2020      31221
#> 12     2021      29019
#> 13     2022      29074
#> 14     2023      29399
#> 15     2024      29444
#> 16     2025      29690

ggplot(spokane_trend, aes(x = end_year, y = n_students)) +
  geom_line(linewidth = 1.2, color = "#4B2E83") +
  geom_point(size = 3, color = "#4B2E83") +
  geom_vline(xintercept = 2020.5, linetype = "dashed", color = "red", alpha = 0.5) +
  annotate("text", x = 2020.8, y = max(spokane_trend$n_students, na.rm = TRUE),
           label = "COVID", hjust = 0, color = "red", size = 3) +
  scale_y_continuous(labels = scales::comma) +
  labs(
    title = "Spokane School District Enrollment (2010-2025)",
    subtitle = "Eastern Washington's anchor district holding near 30K",
    x = "School Year (ending)",
    y = "Total Enrollment"
  )


8. 130 districts have fewer than 500 students

Nearly 40% of Washington’s districts serve fewer than 500 students, challenging their long-term viability.

district_sizes <- enr |>
  filter(is_district, subgroup == "total_enrollment", grade_level == "TOTAL",
         end_year == 2025) |>
  mutate(size_bucket = case_when(
    n_students < 500 ~ "Small (<500)",
    n_students < 2000 ~ "Medium (500-2K)",
    n_students < 10000 ~ "Large (2K-10K)",
    TRUE ~ "Very Large (10K+)"
  )) |>
  count(size_bucket)
stopifnot(nrow(district_sizes) > 0)

district_sizes
#>         size_bucket   n
#> 1    Large (2K-10K)  81
#> 2   Medium (500-2K)  87
#> 3      Small (<500) 130
#> 4 Very Large (10K+)  32
print(district_sizes)
#>         size_bucket   n
#> 1    Large (2K-10K)  81
#> 2   Medium (500-2K)  87
#> 3      Small (<500) 130
#> 4 Very Large (10K+)  32

district_sizes |>
  mutate(size_bucket = factor(size_bucket,
    levels = c("Small (<500)", "Medium (500-2K)", "Large (2K-10K)", "Very Large (10K+)"))) |>
  ggplot(aes(x = size_bucket, y = n, fill = size_bucket)) +
  geom_col(show.legend = FALSE) +
  geom_text(aes(label = n), vjust = -0.5, size = 4) +
  scale_fill_manual(values = c("#E8E3D3", "#C4A055", "#85754D", "#4B2E83")) +
  scale_y_continuous(expand = expansion(mult = c(0, 0.1))) +
  labs(
    title = "Washington Districts by Size (2025)",
    subtitle = "130 small districts serve fewer than 500 students each",
    x = "District Size",
    y = "Number of Districts"
  )


9. Tacoma held steady at ~30K for a decade before COVID

The state’s third-largest city maintained remarkably stable enrollment from 2010 to 2020 before losing 2,000 students.

tacoma <- enr |>
  filter(is_district, subgroup == "total_enrollment", grade_level == "TOTAL",
         grepl("Tacoma", district_name, ignore.case = TRUE)) |>
  select(end_year, district_name, n_students)
stopifnot(nrow(tacoma) > 0)

tacoma
#>    end_year          district_name n_students
#> 1      2010 Tacoma School District      29625
#> 2      2011 Tacoma School District      29856
#> 3      2012 Tacoma School District      29704
#> 4      2013 Tacoma School District      30122
#> 5      2014 Tacoma School District      30411
#> 6      2015 Tacoma School District      30606
#> 7      2016 Tacoma School District      30554
#> 8      2017 Tacoma School District      30326
#> 9      2018 Tacoma School District      30414
#> 10     2019 Tacoma School District      30320
#> 11     2020 Tacoma School District      30406
#> 12     2021 Tacoma School District      28734
#> 13     2022 Tacoma School District      28779
#> 14     2023 Tacoma School District      28457
#> 15     2024 Tacoma School District      28353
#> 16     2025 Tacoma School District      29014
print(tacoma)
#>    end_year          district_name n_students
#> 1      2010 Tacoma School District      29625
#> 2      2011 Tacoma School District      29856
#> 3      2012 Tacoma School District      29704
#> 4      2013 Tacoma School District      30122
#> 5      2014 Tacoma School District      30411
#> 6      2015 Tacoma School District      30606
#> 7      2016 Tacoma School District      30554
#> 8      2017 Tacoma School District      30326
#> 9      2018 Tacoma School District      30414
#> 10     2019 Tacoma School District      30320
#> 11     2020 Tacoma School District      30406
#> 12     2021 Tacoma School District      28734
#> 13     2022 Tacoma School District      28779
#> 14     2023 Tacoma School District      28457
#> 15     2024 Tacoma School District      28353
#> 16     2025 Tacoma School District      29014

ggplot(tacoma, aes(x = end_year, y = n_students)) +
  geom_line(linewidth = 1.2, color = "#4B2E83") +
  geom_point(size = 3, color = "#4B2E83") +
  geom_vline(xintercept = 2020.5, linetype = "dashed", color = "red", alpha = 0.5) +
  annotate("text", x = 2020.8, y = max(tacoma$n_students, na.rm = TRUE),
           label = "COVID", hjust = 0, color = "red", size = 3) +
  scale_y_continuous(labels = scales::comma) +
  labs(
    title = "Tacoma School District Enrollment (2010-2025)",
    subtitle = "Remarkably stable for a decade, then COVID disruption",
    x = "School Year (ending)",
    y = "Total Enrollment"
  )


10. From 1.03M to 1.1M: 16 years of enrollment milestones

Washington’s enrollment data spans 2010-2025, documenting the tech boom, pandemic disruption, and demographic transformation.

decade_summary <- enr |>
  filter(is_state, subgroup == "total_enrollment", grade_level == "TOTAL",
         end_year %in% c(2010, 2015, 2019, 2021, 2025)) |>
  select(end_year, n_students) |>
  mutate(label = case_when(
    end_year == 2010 ~ "Post-recession",
    end_year == 2015 ~ "Tech boom",
    end_year == 2019 ~ "Pre-COVID peak",
    end_year == 2021 ~ "COVID low",
    end_year == 2025 ~ "Current"
  ))
stopifnot(nrow(decade_summary) > 0)

decade_summary
#>   end_year n_students          label
#> 1     2010    1034935 Post-recession
#> 2     2015    1086314      Tech boom
#> 3     2019    1137367 Pre-COVID peak
#> 4     2021    1093331      COVID low
#> 5     2025    1105384        Current
print(decade_summary)
#>   end_year n_students          label
#> 1     2010    1034935 Post-recession
#> 2     2015    1086314      Tech boom
#> 3     2019    1137367 Pre-COVID peak
#> 4     2021    1093331      COVID low
#> 5     2025    1105384        Current

ggplot(decade_summary, aes(x = end_year, y = n_students)) +
  geom_col(fill = "#4B2E83", width = 2) +
  geom_text(aes(label = paste0(label, "\n", scales::comma(n_students))),
            vjust = -0.3, size = 3) +
  scale_y_continuous(labels = scales::comma, expand = expansion(mult = c(0, 0.15))) +
  labs(
    title = "Washington Enrollment Milestones",
    subtitle = "Key years from post-recession through COVID recovery",
    x = "School Year (ending)",
    y = "Total Enrollment"
  )


11. 1 in 6 Washington students now receives special education services

Special education identification rose from 13.2% to 16.4% over 16 years, with the rate accelerating since 2022.

sped_trend <- enr |>
  filter(is_state, grade_level == "TOTAL", subgroup == "special_ed") |>
  select(end_year, n_students, pct) |>
  mutate(pct = round(pct * 100, 1))
stopifnot(nrow(sped_trend) > 0)

sped_trend
#>    end_year n_students  pct
#> 1      2010     136129 13.2
#> 2      2011     141583 13.5
#> 3      2012     145177 13.8
#> 4      2013     146773 13.9
#> 5      2014     148462 13.9
#> 6      2015     149314 13.7
#> 7      2016     153648 14.0
#> 8      2017     157984 14.2
#> 9      2018     163839 14.5
#> 10     2019     169270 14.9
#> 11     2020     170961 14.9
#> 12     2021     158218 14.5
#> 13     2022     161967 14.8
#> 14     2023     168599 15.4
#> 15     2024     176801 16.1
#> 16     2025     181381 16.4
print(sped_trend)
#>    end_year n_students  pct
#> 1      2010     136129 13.2
#> 2      2011     141583 13.5
#> 3      2012     145177 13.8
#> 4      2013     146773 13.9
#> 5      2014     148462 13.9
#> 6      2015     149314 13.7
#> 7      2016     153648 14.0
#> 8      2017     157984 14.2
#> 9      2018     163839 14.5
#> 10     2019     169270 14.9
#> 11     2020     170961 14.9
#> 12     2021     158218 14.5
#> 13     2022     161967 14.8
#> 14     2023     168599 15.4
#> 15     2024     176801 16.1
#> 16     2025     181381 16.4

ggplot(sped_trend, aes(x = end_year, y = pct)) +
  geom_line(linewidth = 1.2, color = "#4B2E83") +
  geom_point(size = 3, color = "#4B2E83") +
  geom_vline(xintercept = 2020.5, linetype = "dashed", color = "red", alpha = 0.5) +
  annotate("text", x = 2020.8, y = max(sped_trend$pct, na.rm = TRUE),
           label = "COVID", hjust = 0, color = "red", size = 3) +
  scale_y_continuous(labels = function(x) paste0(x, "%"),
                     limits = c(0, NA)) +
  labs(
    title = "Special Education Students as Share of Enrollment",
    subtitle = "Washington state, 2010-2025 -- rate accelerating since 2022",
    x = "School Year",
    y = "Percent of Students"
  )


12. Yakima Valley: where 93% of students are Hispanic and half are English learners

Central Washington’s agricultural heartland has the state’s highest concentrations of English learners and economically disadvantaged students.

yakima_districts <- enr |>
  filter(is_district, grade_level == "TOTAL", end_year == 2025,
         grepl("Yakima|Sunnyside|Toppenish|Wapato|Grandview", district_name)) |>
  select(district_name, subgroup, n_students) |>
  pivot_wider(names_from = subgroup, values_from = n_students) |>
  mutate(
    pct_hispanic = round(hispanic / total_enrollment * 100, 1),
    pct_ell = round(lep / total_enrollment * 100, 1),
    pct_econ_disadv = round(econ_disadv / total_enrollment * 100, 1)
  ) |>
  select(district_name, total_enrollment, pct_hispanic, pct_ell, pct_econ_disadv) |>
  arrange(desc(total_enrollment))
stopifnot(nrow(yakima_districts) > 0)

yakima_districts
#> # A tibble: 7 × 5
#>   district_name            total_enrollment pct_hispanic pct_ell pct_econ_disadv
#>   <chr>                               <dbl>        <dbl>   <dbl>           <dbl>
#> 1 Yakima School District              15621         82.1    34.2            86.8
#> 2 Sunnyside School Distri…             6169         92.9    33.3            87.6
#> 3 West Valley School Dist…             5570         41.5     9.9            54.9
#> 4 Toppenish School Distri…             3670         86.9    36.3            89.9
#> 5 Grandview School Distri…             3586         93.4    32.7            85.5
#> 6 East Valley School Dist…             3408         58.5    15              65.7
#> 7 Wapato School District               3225         77.2    50.6            89.9
yakima_long <- enr |>
  filter(is_district, grade_level == "TOTAL", end_year == 2025,
         grepl("Yakima|Sunnyside|Toppenish|Wapato|Grandview", district_name),
         subgroup %in% c("hispanic", "lep", "econ_disadv")) |>
  select(district_name, subgroup, pct) |>
  mutate(pct = pct * 100,
         subgroup = case_when(
           subgroup == "hispanic" ~ "Hispanic",
           subgroup == "lep" ~ "English Learner",
           subgroup == "econ_disadv" ~ "Economically Disadvantaged"
         ))
stopifnot(nrow(yakima_long) > 0)

print(yakima_long)
#>                           district_name                   subgroup      pct
#> 1                Yakima School District                   Hispanic 82.14583
#> 2  East Valley School District (Yakima)                   Hispanic 58.45070
#> 3             Grandview School District                   Hispanic 93.41885
#> 4             Sunnyside School District                   Hispanic 92.89998
#> 5             Toppenish School District                   Hispanic 86.86649
#> 6                Wapato School District                   Hispanic 77.17829
#> 7  West Valley School District (Yakima)                   Hispanic 41.50808
#> 8                Yakima School District            English Learner 34.19115
#> 9  East Valley School District (Yakima)            English Learner 14.99413
#> 10            Grandview School District            English Learner 32.71054
#> 11            Sunnyside School District            English Learner 33.34414
#> 12            Toppenish School District            English Learner 36.32153
#> 13               Wapato School District            English Learner 50.57364
#> 14 West Valley School District (Yakima)            English Learner  9.94614
#> 15               Yakima School District Economically Disadvantaged 86.83183
#> 16 East Valley School District (Yakima) Economically Disadvantaged 65.66901
#> 17            Grandview School District Economically Disadvantaged 85.52705
#> 18            Sunnyside School District Economically Disadvantaged 87.59929
#> 19            Toppenish School District Economically Disadvantaged 89.89101
#> 20               Wapato School District Economically Disadvantaged 89.86047
#> 21 West Valley School District (Yakima) Economically Disadvantaged 54.93716

ggplot(yakima_long, aes(x = reorder(district_name, -pct), y = pct, fill = subgroup)) +
  geom_col(position = "dodge") +
  scale_fill_brewer(palette = "Set2") +
  labs(
    title = "Yakima Valley Districts: Demographics",
    subtitle = "High concentrations of Hispanic, ELL, and low-income students",
    x = NULL,
    y = "Percent of Students",
    fill = "Subgroup"
  ) +
  theme(axis.text.x = element_text(angle = 45, hjust = 1),
        legend.position = "bottom")


13. Ridgefield grew 84% while Vancouver lost 7% – Clark County’s diverging paths

The Portland metro spillover has reshaped Clark County, with small suburban districts booming while the urban core declines.

clark_districts <- enr |>
  filter(is_district, subgroup == "total_enrollment", grade_level == "TOTAL",
         grepl("Vancouver|Evergreen SD|Camas|Battle Ground|Ridgefield", district_name),
         end_year %in% c(2015, 2020, 2025)) |>
  select(district_name, end_year, n_students) |>
  pivot_wider(names_from = end_year, values_from = n_students, names_prefix = "yr_") |>
  mutate(
    growth_2015_2025 = round((yr_2025 - yr_2015) / yr_2015 * 100, 1)
  ) |>
  arrange(desc(growth_2015_2025))
stopifnot(nrow(clark_districts) > 0)

clark_districts
#> # A tibble: 4 × 5
#>   district_name                 yr_2015 yr_2020 yr_2025 growth_2015_2025
#>   <chr>                           <dbl>   <dbl>   <dbl>            <dbl>
#> 1 Ridgefield School District       2343    3499    4315             84.2
#> 2 Camas School District            6695    7654    7272              8.6
#> 3 Battle Ground School District   13589   13365   13080             -3.7
#> 4 Vancouver School District       23486   23404   21943             -6.6
clark_trend <- enr |>
  filter(is_district, subgroup == "total_enrollment", grade_level == "TOTAL",
         grepl("Vancouver|Evergreen SD|Camas|Battle Ground|Ridgefield", district_name))
stopifnot(nrow(clark_trend) > 0)

print(clark_trend |> filter(end_year == 2025) |> select(district_name, n_students))
#>                   district_name n_students
#> 1     Vancouver School District      21943
#> 2         Camas School District       7272
#> 3 Battle Ground School District      13080
#> 4    Ridgefield School District       4315

ggplot(clark_trend, aes(x = end_year, y = n_students, color = district_name)) +
  geom_line(linewidth = 1.2) +
  geom_point(size = 2) +
  geom_vline(xintercept = 2020.5, linetype = "dashed", color = "red", alpha = 0.5) +
  scale_y_continuous(labels = scales::comma) +
  scale_color_brewer(palette = "Set1") +
  labs(
    title = "Clark County District Enrollment Trends",
    subtitle = "Ridgefield booming while Vancouver declines",
    x = "School Year",
    y = "Enrollment",
    color = "District"
  ) +
  theme(legend.position = "bottom") +
  guides(color = guide_legend(nrow = 2))


14. Foster care enrollment dropped 53% from 2019 peak

Washington tracks foster care enrollment at every school, revealing the population peaked at 7,573 in 2019 and has since dropped to 3,560.

foster_trend <- enr |>
  filter(is_state, grade_level == "TOTAL", subgroup == "foster_care") |>
  select(end_year, n_students, pct) |>
  mutate(pct = round(pct * 100, 2))
stopifnot(nrow(foster_trend) > 0)

foster_trend
#>    end_year n_students  pct
#> 1      2010       6670 0.64
#> 2      2011       6715 0.64
#> 3      2012       6124 0.58
#> 4      2013       5627 0.53
#> 5      2014       5452 0.51
#> 6      2015       5268 0.48
#> 7      2016       5224 0.47
#> 8      2017       5873 0.53
#> 9      2018       6739 0.60
#> 10     2019       7573 0.67
#> 11     2020       6812 0.59
#> 12     2021       5598 0.51
#> 13     2022       4903 0.45
#> 14     2023       4112 0.37
#> 15     2024       3317 0.30
#> 16     2025       3560 0.32
print(foster_trend)
#>    end_year n_students  pct
#> 1      2010       6670 0.64
#> 2      2011       6715 0.64
#> 3      2012       6124 0.58
#> 4      2013       5627 0.53
#> 5      2014       5452 0.51
#> 6      2015       5268 0.48
#> 7      2016       5224 0.47
#> 8      2017       5873 0.53
#> 9      2018       6739 0.60
#> 10     2019       7573 0.67
#> 11     2020       6812 0.59
#> 12     2021       5598 0.51
#> 13     2022       4903 0.45
#> 14     2023       4112 0.37
#> 15     2024       3317 0.30
#> 16     2025       3560 0.32

ggplot(foster_trend, aes(x = end_year, y = n_students)) +
  geom_line(linewidth = 1.2, color = "#4B2E83") +
  geom_point(size = 3, color = "#4B2E83") +
  geom_vline(xintercept = 2020.5, linetype = "dashed", color = "red", alpha = 0.5) +
  annotate("text", x = 2020.8, y = max(foster_trend$n_students, na.rm = TRUE),
           label = "COVID", hjust = 0, color = "red", size = 3) +
  scale_y_continuous(labels = scales::comma) +
  labs(
    title = "Foster Care Students in Washington Schools",
    subtitle = "Peaked at 7,573 in 2019, now down to 3,560",
    x = "School Year (ending)",
    y = "Foster Care Students"
  )


15. The Tri-Cities boom: Pasco grew 31% while Kennewick and Richland added 19% and 32%

The Tri-Cities in southeastern Washington have seen sustained population growth driven by Hanford cleanup and agricultural expansion.

tri_cities <- enr |>
  filter(is_district, subgroup == "total_enrollment", grade_level == "TOTAL",
         grepl("Richland|Kennewick|Pasco", district_name)) |>
  select(end_year, district_name, n_students)
stopifnot(nrow(tri_cities) > 0)

tri_cities_wide <- tri_cities |>
  filter(end_year %in% c(2010, 2015, 2020, 2025)) |>
  pivot_wider(names_from = end_year, values_from = n_students, names_prefix = "yr_")

tri_cities_wide
#> # A tibble: 3 × 5
#>   district_name             yr_2010 yr_2015 yr_2020 yr_2025
#>   <chr>                       <dbl>   <dbl>   <dbl>   <dbl>
#> 1 Kennewick School District   16085   17611   19554   19109
#> 2 Pasco School District       14473   17182   19226   19001
#> 3 Richland School District    10965   12729   14295   14499
print(tri_cities_wide)
#> # A tibble: 3 × 5
#>   district_name             yr_2010 yr_2015 yr_2020 yr_2025
#>   <chr>                       <dbl>   <dbl>   <dbl>   <dbl>
#> 1 Kennewick School District   16085   17611   19554   19109
#> 2 Pasco School District       14473   17182   19226   19001
#> 3 Richland School District    10965   12729   14295   14499

ggplot(tri_cities, aes(x = end_year, y = n_students, color = district_name)) +
  geom_line(linewidth = 1.2) +
  geom_point(size = 2) +
  geom_vline(xintercept = 2020.5, linetype = "dashed", color = "red", alpha = 0.5) +
  scale_y_continuous(labels = scales::comma) +
  scale_color_manual(values = c("Kennewick School District" = "#4B2E83",
                                "Pasco School District" = "#C4A055",
                                "Richland School District" = "#85754D"),
                     labels = function(x) gsub(" School District", "", x)) +
  labs(
    title = "Tri-Cities Enrollment Growth",
    subtitle = "Southeastern Washington's sustained growth region",
    x = "School Year",
    y = "Enrollment",
    color = "District"
  ) +
  theme(legend.position = "bottom")


Summary

Washington’s school enrollment data reveals:

  • Steady growth: 1.03M to 1.15M students over a decade before COVID
  • Seattle paradox: The largest district lost 9% while suburban districts boomed
  • Demographic transformation: White students dropped from 64% to 48%, Hispanic nearly doubled
  • Kindergarten cliff: K enrollment down 14% from peak, signaling smaller future cohorts
  • Rural pressure: 130 districts serve fewer than 500 students each
  • Special education growth: Rising from 13% to 16% of all students
  • Agricultural communities: Yakima Valley districts are 80-93% Hispanic
  • Clark County divergence: Ridgefield up 84%, Vancouver down 7%
  • Foster care decline: Peaked at 7,573 in 2019, now down 53%
  • Tri-Cities expansion: All three districts grew 19-32% since 2010

These patterns shape school funding, facility planning, and staffing decisions across the Evergreen State.


Data sourced from the Washington Office of Superintendent of Public Instruction (OSPI) via the Washington State Report Card and data.wa.gov.


Session Info

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        waschooldata_0.1.0
#> [5] testthat_3.3.2    
#> 
#> loaded via a namespace (and not attached):
#>  [1] gtable_0.3.6       jsonlite_2.0.0     compiler_4.5.2     brio_1.1.5        
#>  [5] tidyselect_1.2.1   jquerylib_0.1.4    systemfonts_1.3.2  scales_1.4.0      
#>  [9] textshaping_1.0.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] forcats_1.0.1      tibble_3.3.1       desc_1.4.3         bslib_0.10.0      
#> [21] pillar_1.11.1      RColorBrewer_1.1-3 rlang_1.1.7        utf8_1.2.6        
#> [25] cachem_1.1.0       xfun_0.56          S7_0.2.1           fs_1.6.7          
#> [29] sass_0.4.10        cli_3.6.5          withr_3.0.2        pkgdown_2.2.0     
#> [33] magrittr_2.0.4     digest_0.6.39      grid_4.5.2         rappdirs_0.3.4    
#> [37] lifecycle_1.0.5    vctrs_0.7.1        evaluate_1.0.5     glue_1.8.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