Skip to contents

Where Did All the Students Go?

New York public schools have lost 295,521 students since 2012 - that’s the equivalent of emptying Buffalo, Rochester, Syracuse, and Yonkers combined. But this headline number hides a complex story of urban decline, Pre-K revolution, COVID disruption, and surprising pockets of growth.

This vignette explores 13 years of enrollment data to surface the trends shaping New York’s educational landscape.

# Fetch district-level data for all available years
enr <- fetch_enr_years(2012:2024, level = "district", tidy = TRUE, use_cache = TRUE)

1. The Vanishing 300,000

New York lost 295,521 students (11%) from 2012 to 2024 - equivalent to losing every student in Buffalo, Rochester, Syracuse, and Yonkers combined.

state_trend <- enr %>%
  filter(grade_level == "TOTAL") %>%
  group_by(end_year) %>%
  summarize(total = sum(n_students, na.rm = TRUE), .groups = "drop")

stopifnot(nrow(state_trend) > 0)

# Calculate loss
loss <- state_trend$total[state_trend$end_year == 2012] -
        state_trend$total[state_trend$end_year == 2024]

ggplot(state_trend, aes(x = end_year, y = total)) +
  geom_line(linewidth = 1.2, color = "#2E86AB") +
  geom_point(size = 3, color = "#2E86AB") +
  geom_hline(yintercept = state_trend$total[state_trend$end_year == 2024],
             linetype = "dashed", color = "darkred", alpha = 0.5) +
  annotate("text", x = 2016, y = 2350000,
           label = paste0("-", comma(loss), " students\n(-11%)"),
           color = "darkred", size = 4, fontface = "bold") +
  scale_y_continuous(labels = comma, limits = c(2200000, 2750000)) +
  scale_x_continuous(breaks = 2012:2024) +
  labs(
    title = "New York Lost 300,000 Students in 12 Years",
    subtitle = "Total public school enrollment, 2012-2024",
    x = "School Year (End Year)",
    y = "Total Enrollment",
    caption = "Source: NYSED IRS"
  ) +
  theme_minimal() +
  theme(
    axis.text.x = element_text(angle = 45, hjust = 1),
    plot.title = element_text(face = "bold", size = 14)
  )

2. The COVID Cliff

2021 saw an unprecedented 4.2% single-year drop (106,560 students) - by far the largest decline in recorded data. But 2024 shows the first positive year (+0.02%), suggesting possible stabilization.

state_yoy <- state_trend %>%
  mutate(
    change = total - lag(total),
    pct_change = round(change / lag(total) * 100, 2)
  )

stopifnot(nrow(state_yoy) > 0)

ggplot(state_yoy %>% filter(!is.na(pct_change)),
       aes(x = end_year, y = pct_change, fill = pct_change > 0)) +
  geom_col(show.legend = FALSE) +
  geom_hline(yintercept = 0, color = "black") +
  scale_fill_manual(values = c("TRUE" = "#28A745", "FALSE" = "#DC3545")) +
  scale_x_continuous(breaks = 2013:2024) +
  labs(
    title = "The COVID Cliff: 2021's Unprecedented 4.2% Drop",
    subtitle = "Year-over-year enrollment change (%)",
    x = "School Year",
    y = "Percent Change",
    caption = "2024 shows first positive growth in the dataset"
  ) +
  theme_minimal() +
  theme(axis.text.x = element_text(angle = 45, hjust = 1))

3. The Pre-K Revolution

Pre-K enrollment surged from 100K to 167K - a 67% increase driven by NYC’s Universal Pre-K program, which launched full-day Pre-K in 2015.

pk_trend <- enr %>%
  filter(grade_level == "PK") %>%
  group_by(end_year) %>%
  summarize(total = sum(n_students, na.rm = TRUE), .groups = "drop") %>%
  mutate(
    yoy_pct = round((total - lag(total)) / lag(total) * 100, 1)
  )

stopifnot(nrow(pk_trend) > 0)

ggplot(pk_trend, aes(x = end_year, y = total)) +
  geom_area(fill = "#17A2B8", alpha = 0.3) +
  geom_line(linewidth = 1.2, color = "#17A2B8") +
  geom_point(size = 3, color = "#17A2B8") +
  geom_vline(xintercept = 2015, linetype = "dashed", color = "darkgreen") +
  annotate("text", x = 2015.5, y = 140000,
           label = "NYC Universal Pre-K\ndrives surge",
           hjust = 0, color = "darkgreen", size = 3.5) +
  scale_y_continuous(labels = comma, limits = c(0, NA)) +
  scale_x_continuous(breaks = 2012:2024) +
  labs(
    title = "Pre-K Enrollment Surged 67% in 12 Years",
    subtitle = "NYC's Universal Pre-K program transformed early childhood education",
    x = "School Year",
    y = "Pre-K Enrollment",
    caption = "Source: NYSED IRS"
  ) +
  theme_minimal() +
  theme(axis.text.x = element_text(angle = 45, hjust = 1))

4. The Bronx Exodus

The Bronx lost 23.7% of its students - the worst percentage decline among major counties, losing nearly 50,000 students.

county_2012 <- enr %>% filter(end_year == 2012, grade_level == "TOTAL") %>%
  group_by(county) %>% summarize(enr_2012 = sum(n_students, na.rm = TRUE), .groups = "drop")
county_2024 <- enr %>% filter(end_year == 2024, grade_level == "TOTAL") %>%
  group_by(county) %>% summarize(enr_2024 = sum(n_students, na.rm = TRUE), .groups = "drop")

county_change <- county_2012 %>%
  inner_join(county_2024, by = "county") %>%
  filter(enr_2012 > 10000) %>%  # Major counties only
  mutate(
    change = enr_2024 - enr_2012,
    pct_change = round((enr_2024 - enr_2012) / enr_2012 * 100, 1)
  ) %>%
  arrange(pct_change)

stopifnot(nrow(county_change) > 0)

# Show top 10 declining and top 5 growing
county_display <- bind_rows(
  county_change %>% head(10),
  county_change %>% tail(5)
) %>%
  mutate(county = factor(county, levels = county))

ggplot(county_display, aes(x = reorder(county, pct_change), y = pct_change,
                           fill = pct_change > 0)) +
  geom_col(show.legend = FALSE) +
  geom_hline(yintercept = 0) +
  coord_flip() +
  scale_fill_manual(values = c("TRUE" = "#28A745", "FALSE" = "#DC3545")) +
  labs(
    title = "The Bronx Led the State in Enrollment Loss",
    subtitle = "Percent change in enrollment, 2012-2024 (counties with >10K students)",
    x = NULL,
    y = "Percent Change"
  ) +
  theme_minimal()

5. Rochester’s Collapse

Rochester City SD lost 30% of enrollment (32K to 23K) - the steepest decline among major urban districts.

# Calculate 2012-2024 change by district
dist_2012 <- enr %>% filter(end_year == 2012, grade_level == "TOTAL") %>%
  select(district_name, county, enr_2012 = n_students)
dist_2024 <- enr %>% filter(end_year == 2024, grade_level == "TOTAL") %>%
  select(district_name, enr_2024 = n_students)

change <- dist_2012 %>%
  inner_join(dist_2024, by = "district_name") %>%
  filter(!is.na(enr_2012), !is.na(enr_2024), enr_2012 >= 10000) %>%
  mutate(
    change = enr_2024 - enr_2012,
    pct_change = round((enr_2024 - enr_2012) / enr_2012 * 100, 1)
  )

stopifnot(nrow(change) > 0)

# Top decliners among big districts
big_declines <- change %>%
  arrange(pct_change) %>%
  head(12) %>%
  mutate(district_short = gsub(" - .*", "", district_name))

ggplot(big_declines, aes(x = reorder(district_short, pct_change), y = pct_change)) +
  geom_col(fill = "#DC3545") +
  geom_text(aes(label = paste0(pct_change, "%")), hjust = 1.1, color = "white", size = 3) +
  coord_flip() +
  labs(
    title = "Rochester Lost 30% - Worst Among Major Districts",
    subtitle = "Districts with 10,000+ students in 2012",
    x = NULL,
    y = "Percent Change (2012-2024)"
  ) +
  theme_minimal()

6. NYC’s Special Ed Surge

District 75 grew 40% while nearly every other NYC district shrank. NYC’s citywide special education district is one of very few that gained students.

# Find NYC District 75
nyc_districts <- change %>%
  filter(grepl("NYC", district_name)) %>%
  mutate(
    is_d75 = grepl("DIST 75", district_name)
  ) %>%
  arrange(pct_change)

stopifnot(nrow(nyc_districts) > 0)

# Top 10 NYC district changes
nyc_display <- bind_rows(
  nyc_districts %>% filter(is_d75),  # Always show District 75
  nyc_districts %>% filter(!is_d75) %>% head(5),  # Worst 5
  nyc_districts %>% filter(!is_d75) %>% tail(3)   # Best 3
) %>%
  mutate(district_short = gsub("NYC GEOG DIST ", "Dist ", district_name),
         district_short = gsub("NYC SPEC SCHOOLS - ", "", district_short))

ggplot(nyc_display, aes(x = reorder(district_short, pct_change), y = pct_change,
                        fill = pct_change > 0)) +
  geom_col(show.legend = FALSE) +
  geom_hline(yintercept = 0) +
  coord_flip() +
  scale_fill_manual(values = c("TRUE" = "#28A745", "FALSE" = "#DC3545")) +
  labs(
    title = "District 75 Bucked the Trend with 40% Growth",
    subtitle = "NYC geographic district enrollment change, 2012-2024",
    x = NULL,
    y = "Percent Change"
  ) +
  theme_minimal()

7. First Grade Cratering

Grade 1 enrollment fell 17.4% - the steepest decline by grade level, reflecting birth rate drops and family out-migration.

# Note: K grade only available for 2023-2024, so we use grades with full 2012-2024 coverage
grade_totals <- enr %>%
  filter(grade_level %in% c("01", "05", "08", "09", "12")) %>%
  group_by(end_year, grade_level) %>%
  summarize(total = sum(n_students, na.rm = TRUE), .groups = "drop")

stopifnot(nrow(grade_totals) > 0)

g_2012 <- grade_totals %>% filter(end_year == 2012) %>% rename(n_2012 = total)
g_2024 <- grade_totals %>% filter(end_year == 2024) %>% rename(n_2024 = total) %>%
  select(grade_level, n_2024)

grade_change <- g_2012 %>%
  inner_join(g_2024, by = "grade_level") %>%
  mutate(
    pct_change = round((n_2024 - n_2012) / n_2012 * 100, 1),
    grade_label = case_when(
      grade_level == "01" ~ "Grade 1",
      grade_level == "05" ~ "Grade 5",
      grade_level == "08" ~ "Grade 8",
      grade_level == "09" ~ "Grade 9",
      grade_level == "12" ~ "Grade 12"
    )
  )

stopifnot(nrow(grade_change) > 0)

ggplot(grade_change, aes(x = reorder(grade_label, pct_change), y = pct_change)) +
  geom_col(fill = "#6C757D") +
  geom_text(aes(label = paste0(pct_change, "%")), hjust = 1.1, color = "white", size = 4) +
  coord_flip() +
  labs(
    title = "First Grade Hit Hardest: -17.4%",
    subtitle = "Enrollment change by grade level, 2012-2024",
    x = NULL,
    y = "Percent Change"
  ) +
  theme_minimal()

8. Charter Schools’ Rising Market Share

# Need school-level data for charter information
enr_schools <- fetch_enr_years(2023:2024, level = "school", tidy = TRUE, use_cache = TRUE)

Charter schools now enroll ~181K students (7.5% of total) across 343 schools, growing even as traditional public school enrollment declines.

charter_summary <- enr_schools %>%
  filter(grade_level == "TOTAL", is_school == TRUE) %>%
  group_by(end_year, is_charter) %>%
  summarize(
    total = sum(n_students, na.rm = TRUE),
    n_schools = n(),
    .groups = "drop"
  ) %>%
  mutate(
    type = ifelse(is_charter, "Charter", "Traditional"),
    avg_size = round(total / n_schools)
  )

stopifnot(nrow(charter_summary) > 0)

# Show as table
charter_summary %>%
  select(end_year, type, total, n_schools, avg_size) %>%
  arrange(end_year, desc(type)) %>%
  knitr::kable(
    col.names = c("Year", "School Type", "Total Students", "# Schools", "Avg Size"),
    format.args = list(big.mark = ","),
    caption = "Charter schools grew from 175K to 181K students in one year"
  )
Charter schools grew from 175K to 181K students in one year
Year School Type Total Students # Schools Avg Size
2,023 Traditional 2,308,189 4,402 524
2,023 Charter 175,396 342 513
2,024 Traditional 2,307,920 4,406 524
2,024 Charter 181,334 343 529

9. Only One County Grew

Saratoga County is the only county that GREW (+0.3%). Suburban counties held steady while urban and rural areas declined sharply.

county_all <- county_2012 %>%
  inner_join(county_2024, by = "county") %>%
  mutate(
    pct_change = round((enr_2024 - enr_2012) / enr_2012 * 100, 1),
    grew = pct_change > 0
  )

stopifnot(nrow(county_all) > 0)

# Summary stats
n_grew <- sum(county_all$grew)
n_declined <- sum(!county_all$grew)

cat(paste0("Counties that grew: ", n_grew, "\n"))
## Counties that grew: 1
cat(paste0("Counties that declined: ", n_declined, "\n"))
## Counties that declined: 61
# The one county that grew
county_all %>% filter(grew) %>%
  select(county, enr_2012, enr_2024, pct_change) %>%
  knitr::kable(caption = "The Only Growing County")
The Only Growing County
county enr_2012 enr_2024 pct_change
SARATOGA 31661 31746 0.3

10. The Pre-K Inversion

NYC Pre-K is now 99% full-day (103K of 104K), while rest of state is only 85% full-day. This represents a fundamental policy shift in early childhood education.

# Use wide format to access half/full day Pre-K detail columns
enr_2024_wide <- fetch_enr(2024, level = "district", tidy = FALSE, use_cache = TRUE)

pk_comparison <- enr_2024_wide %>%
  group_by(is_nyc) %>%
  summarize(
    pk_full = sum(grade_pk_full, na.rm = TRUE),
    pk_half = sum(grade_pk_half, na.rm = TRUE),
    .groups = "drop"
  ) %>%
  mutate(
    region = ifelse(is_nyc, "NYC", "Rest of NY"),
    total_pk = pk_full + pk_half,
    pct_full_day = round(pk_full / total_pk * 100, 1)
  )

stopifnot(nrow(pk_comparison) > 0)

ggplot(pk_comparison, aes(x = region, y = pct_full_day, fill = region)) +
  geom_col(show.legend = FALSE, width = 0.6) +
  geom_text(aes(label = paste0(pct_full_day, "%")), vjust = -0.5, size = 5, fontface = "bold") +
  scale_fill_manual(values = c("NYC" = "#2E86AB", "Rest of NY" = "#A23B72")) +
  scale_y_continuous(limits = c(0, 110)) +
  labs(
    title = "NYC Pre-K is Now 99% Full-Day",
    subtitle = "Percentage of Pre-K students in full-day programs, 2024",
    x = NULL,
    y = "Percent Full-Day"
  ) +
  theme_minimal() +
  theme(
    axis.text.x = element_text(size = 12, face = "bold"),
    plot.title = element_text(face = "bold", size = 14)
  )

11. NYC Enrolls 37% of the State - But Is Shrinking Faster

NYC accounts for 898K of 2.4 million students (37%), and lost 12.1% since 2012 compared to 10.2% for the rest of the state. The city’s outsized share means its decline drives statewide numbers.

# Note: is_nyc flag relies on district_code, which is missing for 2012-2022.
# Use grepl on district_name instead. Also exclude 2022 where district_name is NA.
nyc_vs_rest <- enr %>%
  filter(grade_level == "TOTAL", !is.na(district_name)) %>%
  mutate(nyc_flag = grepl("^NYC", district_name)) %>%
  group_by(end_year, nyc_flag) %>%
  summarize(total = sum(n_students, na.rm = TRUE), .groups = "drop") %>%
  mutate(region = ifelse(nyc_flag, "NYC", "Rest of State"))

stopifnot(nrow(nyc_vs_rest) > 0)

# Calculate shares
nyc_share <- nyc_vs_rest %>%
  group_by(end_year) %>%
  mutate(pct_share = round(total / sum(total) * 100, 1))

ggplot(nyc_vs_rest, aes(x = end_year, y = total, fill = region)) +
  geom_area(alpha = 0.7) +
  scale_y_continuous(labels = comma) +
  scale_x_continuous(breaks = 2012:2024) +
  scale_fill_manual(values = c("NYC" = "#2E86AB", "Rest of State" = "#A23B72")) +
  labs(
    title = "NYC Is 37% of All NY Students - And Shrinking Faster",
    subtitle = "Total enrollment by region, 2012-2024 (2022 excluded: missing district names)",
    x = "School Year",
    y = "Total Enrollment",
    fill = NULL,
    caption = "Source: NYSED IRS"
  ) +
  theme_minimal() +
  theme(
    axis.text.x = element_text(angle = 45, hjust = 1),
    legend.position = "top"
  )

12. Long Island Lost 35,000 Students Despite Stable Housing

Nassau and Suffolk counties lost 35,493 students combined despite being among the state’s most expensive housing markets. Nassau fell 3.1% and Suffolk 11.4%, suggesting birth rate declines even in affluent suburbs.

li_counties <- enr %>%
  filter(county %in% c("NASSAU", "SUFFOLK"), grade_level == "TOTAL") %>%
  group_by(end_year, county) %>%
  summarize(total = sum(n_students, na.rm = TRUE), .groups = "drop")

stopifnot(nrow(li_counties) > 0)

ggplot(li_counties, aes(x = end_year, y = total, color = county)) +
  geom_line(linewidth = 1.2) +
  geom_point(size = 2.5) +
  scale_y_continuous(labels = comma) +
  scale_x_continuous(breaks = 2012:2024) +
  scale_color_manual(values = c("NASSAU" = "#E85D75", "SUFFOLK" = "#6C5B7B")) +
  labs(
    title = "Long Island Lost 35,000 Students in 12 Years",
    subtitle = "Nassau and Suffolk county enrollment, 2012-2024",
    x = "School Year",
    y = "Total Enrollment",
    color = NULL,
    caption = "Source: NYSED IRS"
  ) +
  theme_minimal() +
  theme(
    axis.text.x = element_text(angle = 45, hjust = 1),
    legend.position = "top"
  )

13. Buffalo Fell 10% but Outpaced Rochester and Syracuse

Buffalo lost 9.8% of students, yet that’s the best outcome among upstate Big 3 cities. Rochester lost 29.8% and Syracuse 11.7% - all three lost more than the state average.

upstate_big3 <- enr %>%
  filter(
    district_name %in% c("BUFFALO", "ROCHESTER", "SYRACUSE"),
    grade_level == "TOTAL"
  ) %>%
  group_by(end_year, district_name) %>%
  summarize(total = sum(n_students, na.rm = TRUE), .groups = "drop")

stopifnot(nrow(upstate_big3) > 0)

ggplot(upstate_big3, aes(x = end_year, y = total, color = district_name)) +
  geom_line(linewidth = 1.2) +
  geom_point(size = 2.5) +
  scale_y_continuous(labels = comma) +
  scale_x_continuous(breaks = 2012:2024) +
  scale_color_manual(values = c("BUFFALO" = "#FF6B35", "ROCHESTER" = "#004E89", "SYRACUSE" = "#1A936F")) +
  labs(
    title = "Upstate Big 3: Buffalo, Rochester, Syracuse All Declining",
    subtitle = "Total enrollment in NY's three largest upstate districts",
    x = "School Year",
    y = "Total Enrollment",
    color = NULL,
    caption = "Source: NYSED IRS"
  ) +
  theme_minimal() +
  theme(
    axis.text.x = element_text(angle = 45, hjust = 1),
    legend.position = "top"
  )

14. Grade 1 Lost 33,600 Seats in 12 Years

Grade 1 enrollment dropped from 193K to 159K (-17.4%), a leading indicator that future grades will continue to shrink as smaller cohorts advance through the pipeline.

# Note: K grade only available for 2023-2024, so Grade 1 is the best proxy
# for pipeline trends across the full 2012-2024 period.
g1_trend <- enr %>%
  filter(grade_level == "01") %>%
  group_by(end_year) %>%
  summarize(total = sum(n_students, na.rm = TRUE), .groups = "drop")

stopifnot(nrow(g1_trend) > 0)

g1_loss <- g1_trend$total[g1_trend$end_year == 2012] - g1_trend$total[g1_trend$end_year == 2024]
g1_pct <- round(g1_loss / g1_trend$total[g1_trend$end_year == 2012] * 100, 1)

ggplot(g1_trend, aes(x = end_year, y = total)) +
  geom_line(linewidth = 1.2, color = "#E63946") +
  geom_point(size = 3, color = "#E63946") +
  geom_hline(yintercept = g1_trend$total[g1_trend$end_year == 2024],
             linetype = "dashed", color = "gray50", alpha = 0.5) +
  annotate("text", x = 2016, y = 185000,
           label = paste0("-", comma(g1_loss), " students\n(-", g1_pct, "%)"),
           color = "#E63946", size = 4, fontface = "bold") +
  scale_y_continuous(labels = comma, limits = c(140000, 210000)) +
  scale_x_continuous(breaks = 2012:2024) +
  labs(
    title = "Grade 1 Is a Leading Indicator of Future Decline",
    subtitle = "Total grade 1 enrollment, 2012-2024",
    x = "School Year",
    y = "Grade 1 Enrollment",
    caption = "Source: NYSED IRS"
  ) +
  theme_minimal() +
  theme(
    axis.text.x = element_text(angle = 45, hjust = 1),
    plot.title = element_text(face = "bold", size = 14)
  )

15. New York’s 47-Year View: Peak Was 1977

New York enrollment peaked around 3.2M in 1977 and has been on a long decline. The current 2.4M is roughly where enrollment was in the early 1990s, but the trajectory is downward.

# Fetch archive data for the long view (district-level for older years)
enr_archive <- fetch_enr_years(
  c(1977, 1980, 1985, 1990, 1995, 2000, 2005, 2010),
  level = "district", tidy = TRUE, use_cache = TRUE
)
archive_trend <- enr_archive %>%
  filter(grade_level == "TOTAL") %>%
  group_by(end_year) %>%
  summarize(total = sum(n_students, na.rm = TRUE), .groups = "drop")

long_trend <- bind_rows(archive_trend, state_trend) %>%
  arrange(end_year)

stopifnot(nrow(long_trend) > 0)

ggplot(long_trend, aes(x = end_year, y = total)) +
  geom_line(linewidth = 1.2, color = "#457B9D") +
  geom_point(size = 3, color = "#457B9D") +
  geom_vline(xintercept = 2020, linetype = "dashed", color = "red", alpha = 0.5) +
  annotate("text", x = 2020.5, y = 3100000, label = "COVID", color = "red", hjust = 0, size = 3.5) +
  scale_y_continuous(labels = comma, limits = c(2000000, 3500000)) +
  scale_x_continuous(breaks = seq(1977, 2024, by = 5)) +
  labs(
    title = "47 Years of NY Enrollment: From 3.2M Peak to 2.4M",
    subtitle = "Total public school enrollment, 1977-2024",
    x = "School Year",
    y = "Total Enrollment",
    caption = "Source: NYSED IRS | Note: Pre-K not included in pre-1995 totals"
  ) +
  theme_minimal() +
  theme(
    axis.text.x = element_text(angle = 45, hjust = 1),
    plot.title = element_text(face = "bold", size = 14)
  )

The Big Picture

New York’s public school enrollment data reveals a state in profound transformation:

Winners and Losers

Growing Shrinking
Pre-K enrollment (+67%) Total enrollment (-11%)
District 75 special ed (+40%) The Bronx (-24%)
Charter schools (7.5% share) Rochester (-30%)
Saratoga County (+0.3%) Grade 1 (-17%)

Key Statistics

Finding Key Statistic
Total enrollment loss -295,521 students (-11%)
COVID impact (2021) -106,560 students (-4.2%)
Pre-K growth +67% (driven by full-day expansion)
Bronx county loss -49,447 students (-23.7%)
Rochester district loss -9,621 students (-29.8%)
District 75 growth +7,784 students (+40.1%)
Grade 1 decline -33,643 students (-17.4%)
Charter market share 181K students (7.5%)
Growing counties 1 of 62 (Saratoga)
NYC full-day Pre-K 99% vs 85% rest of state
NYC share of state 37% (898K of 2.4M)
Long Island loss -35,493 students (Nassau + Suffolk)
Upstate Big 3 loss Buffalo -9.8%, Rochester -29.8%, Syracuse -11.7%
Grade 1 decline -33,643 students (-17.4%)
Historical peak 3.2M in 1977 vs 2.4M in 2024

What Does This Mean?

These patterns reflect three converging forces:

  1. Demographic decline: New York’s birth rate has fallen sharply, especially in urban areas. The pipeline of future students is shrinking - Grade 1’s 17% decline is a leading indicator.

  2. Policy success: Universal Pre-K transformed early childhood education in NYC. The 67% growth in Pre-K enrollment, driven primarily by full-day expansion, is one of the most dramatic policy-driven enrollment shifts in state history.

  3. COVID disruption: The pandemic accelerated pre-existing trends and may have permanently altered family choices about public vs. private schooling, remote work migration, and urban living.

  4. Geographic divergence: NYC lost students faster than the rest of the state (-12.1% vs -10.2%), Long Island lost 35,000 despite stable housing, and even upstate’s largest cities lost 10-30% each.

The 2024 data shows the first positive year-over-year change (+0.02%), suggesting possible stabilization - but with Grade 1 enrollment still declining, the long-term trajectory remains uncertain. The 47-year view shows that NY enrollment peaked in 1977 and the current level represents a generational low.

Session Info

## 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] tidyr_1.3.2        scales_1.4.0       ggplot2_4.0.2      dplyr_1.2.0       
## [5] nyschooldata_0.1.0
## 
## loaded via a namespace (and not attached):
##  [1] gtable_0.3.6       jsonlite_2.0.0     compiler_4.5.2     tidyselect_1.2.1  
##  [5] jquerylib_0.1.4    systemfonts_1.3.2  textshaping_1.0.5  readxl_1.4.5      
##  [9] yaml_2.3.12        fastmap_1.2.0      R6_2.6.1           labeling_0.4.3    
## [13] generics_0.1.4     knitr_1.51         tibble_3.3.1       desc_1.4.3        
## [17] downloader_0.4.1   bslib_0.10.0       pillar_1.11.1      RColorBrewer_1.1-3
## [21] rlang_1.1.7        cachem_1.1.0       xfun_0.56          fs_1.6.7          
## [25] sass_0.4.10        S7_0.2.1           cli_3.6.5          pkgdown_2.2.0     
## [29] withr_3.0.2        magrittr_2.0.4     digest_0.6.39      grid_4.5.2        
## [33] rappdirs_0.3.4     lifecycle_1.0.5    vctrs_0.7.1        evaluate_1.0.5    
## [37] glue_1.8.0         cellranger_1.1.0   farver_2.1.2       codetools_0.2-20  
## [41] ragg_1.5.1         purrr_1.2.1        rmarkdown_2.30     tools_4.5.2       
## [45] pkgconfig_2.0.3    htmltools_0.5.9