Live site: ppsdata.info
A data-driven explorer for the 74 Portland Public Schools (PPS) elementary, K-8, middle, and alternative schools that the district plans to narrow to a closure shortlist in November 2026.
The dashboard gathers enrollment, building, demographic, academic, and neighborhood-housing data for every in-scope school.
web/index.html is a single-page static dashboard (vanilla JS + Plotly.js + Leaflet). It reads from web/data.json, which is generated by the pipeline below.
Serve locally:
python3 -m http.server -d web 8000
# open http://localhost:8000- Map of every in-scope school (click to enable scroll-zoom).
- Trends chart showing 7-year enrollment change (2018 → 2025) with per-school toggles.
- Utilization ranking (enrollment vs. 2021 LRFP functional capacity).
- Long-term sustainability ranking (7-year enrollment change 2018 → 2025).
- 10-year enrollment forecast ranking — per-school projected 2034-35 enrollment from PSU Population Research Center (Table 5.5, medium scenario), with low/high scenario bands derived by scaling each school's medium by the district-and-grade-band Low/Medium and High/Medium ratios.
- Housing growth forecast ranking — projected new residential units within each school's catchment by ~2035 (Metro BLI 2045 grid, area-weighted to catchment polygons).
- Seismic ranking (remaining retrofit cost per campus from Holmes 2024, colored by funding status; URM buildings flagged).
- Transportation ranking (distance to nearest same-level school for the 15 lowest-enrollment schools).
- Ratios ranking (students per teacher FTE, Fall 2023 CCD).
- DLI & focus-option section with strand-vs-non-strand headcounts and a collapsible "why this matters" note.
- Scatter plots: enrollment vs. utilization, math/avg proficiency vs. poverty, enrollment vs. % BIPOC, chronic absenteeism vs. proficiency, support-staff FTE equity, 7-year change vs. current size, students-per-teacher vs. enrollment.
- Sortable table of all 74 schools with column descriptions and source citations; CSV export of the filtered rows.
- Charter reference table of the 5 PPS-sponsored charters (Emerson, Kairos PDX, Le Monde, Portland Arthur Academy, Portland Village). Kept separate from the in-scope 74 since PPS can't close a charter through its consolidation process. Shows only the state/federal measures that cover charters (enrollment, demographics, OSAS proficiency, attendance, Title I); building, seismic, capacity, ventilation, and forecast columns are omitted because no source publishes them for charters.
- Methodology section documenting every source with vintage.
- Feeder trees on a standalone page at
/feeders.html— elementary → middle → high school feeder chains, one tree per high school (Mermaid diagrams).
Ranking charts render vertically on desktop and swap to horizontal bars on narrow screens (<700 px) so every school label stays legible.
| Source | What it provides | Vintage |
|---|---|---|
| Oregon ODE Fall Membership | Enrollment by school (2024-25, 2025-26), race/ethnicity | 2025-26 |
| Oregon OSAS (PAGR) | ELA & math state-test proficiency | 2023-24, 2024-25 |
| Oregon ODE At-A-Glance source file | Regular attenders %, experienced-teacher %, teacher retention %, class size | 2024-25 |
| NCES Common Core of Data | Free/reduced lunch counts, geocoded addresses, 2018-2023 enrollment history, 2023 teacher FTE (for students-per-teacher ratios) | 2018-2023 |
| US Dept of Ed CRDC | English learners, IDEA/SPED, chronic absenteeism (2020); counselor / social worker / psychologist / nurse FTE, OSS suspension instances, post-COVID chronic absenteeism (2021) | 2020, 2021 (both COVID-era) |
| KPFF Seismic Report 2009 | Year built, square footage, construction type | 2009 |
| Holmes Engineering 2024 PPS Seismic Assessment | Per-campus ROM retrofit cost (all ~80 sites), URM-only partial-retrofit cost, URM classification | 2024 |
| PPS Long-Range Facility Plan 2021 (Vol 1) | Functional capacity per school (classrooms × station size minus set-asides, with PPS-defined utilization rates). Used for the building-utilization metric. | 2021 |
| PPS Bond page | Seismic retrofit status | 2025 |
| PPS Indoor Air Quality | Per-room airflow tests (Amerseco + Neudorfer Engineers, NEBB-certified) — ACH_e medians, % rooms below Lancet 3/6 ACH, MERV-13 filter status | 2021 |
| Oregon Affordable Housing Inventory (OAHI) | Existing + in-development subsidized housing | 2024 |
| Metro RLIS | Supplementary regional housing | 2024 |
| Portland BDS / PortlandMaps | Residential building permits (2022+) | 2022 – present |
| City of Portland — School Boundaries | PPS attendance polygons (ArcGIS FeatureServer) | 2024-25 |
| PPS Dual Language Immersion + focus-option pages | Per-school DLI languages (5) + K-8 focus options (Arts, Environmental, STEAM, TAG, Alt) | 2026-04 |
| PPS Language Immersion Enrollment Report | Per-school DLI-strand vs. non-strand headcounts (SIS Synergy, annual PDF) | 2025-26 |
| Metro 2045 Distributed Forecast (Ord. 21-1457) | Regional growth context (city/county only) | 2021 |
| Portland Building Land Inventory — Metro BLI 2045 Housing Allocation | Grid-cell projected new residential units by ~2035 under current zoning (area-weighted to each school's catchment) | 2024 |
| PSU Population Research Center — PPS Enrollment Forecasts 2025-26 to 2034-35 | Per-school 10-year enrollment forecast (Table 5.5, medium scenario); district-and-grade low/high scenarios (Tables 5.3, 5.4) used to derive per-school scenario bands | July 2025 |
data/
pps_schools.csv master table (88 rows; 74 in-scope + 9 high schools + 5 PPS-sponsored charters)
raw/ source CSVs, JSONs, XLSXs, PDFs, GeoJSONs
scripts/
fetch_bds_permits.py → data/raw/portland_bds_permits.csv
fetch_affordable_pipeline.py → data/raw/portland_affordable_pipeline.csv
fetch_boundaries.py → data/raw/pps_boundaries_{level}.geojson
fetch_crdc_2021.py → data/raw/pps_crdc_2021_agg.json
fetch_enrollment_history.py → data/raw/pps_ccd_enrollment_history.json
fetch_ode_aag.py → data/raw/ode_aag_schools_2425.csv
fetch_dli_report.py → data/raw/pps_immersion_details_2526.{pdf,json}
fetch_metro_bli.py → data/raw/metro_bli_housing_allocation.geojson
fetch_pps_enrollment_forecast.py → data/raw/pps_enrollment_forecast_2025.pdf
parse_pps_enrollment_forecast.py → data/raw/pps_enrollment_forecast.csv (Table 5.5 per-school)
parse_lrfp_capacity.py → data/raw/pps_functional_capacity_2021.json
fetch_pps_airflow.py → data/raw/pps_airflow_pdfs/*.pdf + pps_airflow_index.json
parse_pps_airflow.py → data/raw/pps_airflow_stats.json
parse_holmes_costs.py → data/raw/pps_holmes_2024_costs.json
boundary_join.py shared BoundaryIndex (point-in-polygon w/ haversine fallback)
build_master.py assemble data/pps_schools.csv
merge_housing.py + affordable_units / pipeline_* columns
merge_permits.py + permit columns
merge_bli_forecast.py + bli_forecast_units_within_catchment (area-weighted from BLI grid)
merge_pps_enrollment_forecast.py + enrollment_forecast_2025_26..2034_35, _2034_35_low/high, pct_change_10yr (PRC medium + derived scenario bands)
export_web.py → web/data.json (filters to the 74 in-scope schools)
web/
index.html single-page dashboard
feeders.html standalone elem→middle→HS feeder-tree diagrams (Mermaid)
data.json dashboard payload (generated)
Requires Python 3.11+ and the packages in pandas, requests, openpyxl, shapely, pdfplumber.
python3 -m venv .venv
source .venv/bin/activate
pip install pandas requests openpyxl shapely pdfplumber
# 1. Fetch raw sources (network):
python scripts/fetch_bds_permits.py
python scripts/fetch_affordable_pipeline.py
python scripts/fetch_boundaries.py
python scripts/fetch_crdc_2021.py
python scripts/fetch_enrollment_history.py
python scripts/fetch_ode_aag.py
python scripts/fetch_dli_report.py
python scripts/fetch_metro_bli.py # Metro BLI 2045 housing-allocation grid (~30 MB GeoJSON)
python scripts/fetch_pps_enrollment_forecast.py # PRC 10-year enrollment forecast PDF
python scripts/parse_pps_enrollment_forecast.py # Table 5.5 per-school → CSV
python scripts/parse_lrfp_capacity.py # reads data/raw/LRFP_Vol1_2021.pdf (checked in)
python scripts/fetch_pps_airflow.py # ~270 MB of per-building airflow PDFs
python scripts/parse_pps_airflow.py # ~6 min: pdfplumber table extraction
python scripts/parse_holmes_costs.py # reads data/raw/holmes_2024_seismic.pdf (download separately, see below)
# 2. Build master CSV:
python scripts/build_master.py
# 3. Spatial joins + per-school forecasts into the master:
python scripts/merge_housing.py
python scripts/merge_permits.py
python scripts/merge_bli_forecast.py
python scripts/merge_pps_enrollment_forecast.py
# 4. Export dashboard payload:
python scripts/export_web.pyEach step is idempotent; rerun freely as sources refresh.
Every "nearby" metric (affordable units, pipeline family units, residential permits, BLI forecast units) is aggregated over the school's PPS attendance polygon rather than a 1-mile circle. Schools without a published catchment (ACCESS Academy, focus-option programs, some alternatives) fall back to a 1-mile haversine radius. boundary_join.BoundaryIndex handles the lookup and the name-alias table between ODE and the City's boundary feed.
The BLI 2045 forecast grid is attributed by area-weighted overlap: for each grid cell intersecting a catchment, the fraction intersection.area / cell.area of that cell's Forecast_Units_Prop contributes to the school's total. See scripts/merge_bli_forecast.py.
- Academic performance is included for context but is notably absent from the criteria PPS has discussed publicly.
- CRDC (LEP, SPED, chronic absenteeism) is from 2020 — a COVID-suppressed reporting year. Percentages are CRDC counts ÷ current enrollment.
- Square footage is from a 2009 inventory and may predate bond-funded expansions.
- Building permits ≠ completions.
permits_units_within_1mi_since_2022counts approved units from 2022-01-01 forward, not occupied units. - Forecasts are not commitments. The PRC 10-year enrollment forecast encodes July-2025 assumptions (net migration, K/1 capture rate, birth rates). PRC reports district-level accuracy only — 2024 district MAPE was 1.65% at one year, but Kindergarten alone was 8.4% off that year, and pandemic-era multi-year forecasts missed by 8-20% at 4-5 years. No per-school error analysis is published; Low/High bands in this dashboard are derived (PRC only publishes them at the district-and-grade level).
- Ventilation data is from 2021 and reflects ASHRAE Total Effective ACH with HVAC only; portable HEPA units deployed in response to COVID are excluded. Some older buildings genuinely have rooms with near-zero mechanical ventilation (no rooftop supply, operable windows only).
- High schools are out of scope. PPS's announced closure process covers elementary / K-8 / middle / alternative only. The master CSV keeps high schools for reference but
export_web.pyfilters them out.
.venv/— Python virtualenv, regenerate locally.data/raw/LRFP_Vol2_2021.pdf(134 MB) — PPS Long-Range Facility Plan Vol. 2, available on bond.pps.net.data/raw/holmes_2024_seismic.pdf(51 MB) — Holmes 2024 PPS Seismic Assessment, available on Google Drive.data/raw/metro/(336 MB) — Metro 2045 forecast .ppkx + extracted GDB, available on oregonmetro.gov.data/raw/metro_bli_housing_allocation.geojson(~30 MB) — Metro BLI 2045 housing-allocation grid, regenerated viapython scripts/fetch_metro_bli.py.
Built by Alex Meub with help from Claude Code.
Released under the MIT License. Copyright © 2026 Alex Meub. The code is freely reusable; data compiled in this repository comes from public sources cited in the Data sources table and remains subject to the terms of its original publishers.