# 06 — Philadelphia enrollment, capacity and utilization

**Question asked:** the same one answered for NYC in
`docs/design/10_nyc_enrollment_capacity_utilization.md` — does Philadelphia
have a Miami-style capacity/utilization surface, and what would it take to
display it?

**Answer:** yes. Philadelphia publishes per-site capacity, October enrollment
and a utilization score **and its own close / co-locate / modernize
recommendation for every site**. It is the only one of the three cities to
publish such a list: Miami has to hand-build its decision list from board
PDFs, and NYC does not publish one. It joins to `PhillySchool.ulcsCode` with no
crosswalk. It also has the shortest history of the three, **one snapshot
(SY2024-25), not a panel**, and it is published only through a Qlik Sense
dashboard, not as a file.

Philadelphia has the largest under-half-full share of the three districts
measured: **24.7% of 215 district sites** under 50% in SY2024-25, against 21.2%
of Miami-Dade's 316 district-run regular schools and 13.3% of NYC's 1,702
schools in the same school year. Its median site (67.1%) is level with
Miami-Dade's median school (66.7%) and 13 points below NYC's (80%). That figure
is on the district's own published denominator, SDP site capacity (the app's
older, 2017-labelled capacity is described in §6.5).

The figures below are reproduced by `probe/facilities.py`
(`python docs/cities/philly/probe/facilities.py`), live against the app, and by
`scripts/analysis/philly-capacity-packet.ts` from the committed extract. The
§4.1–4.3 site-grain figures come from the packet; §6.3 and §6.5 are printed by
the probe, and the packet carries the §6.5 comparison as
`denominator_sensitivity`; the §3.3 building counts are in the packet's
`building_extract` block and in the fetcher's dry run
(`python -m pipeline_philly.fetch_facilities --dry-run`). The §3.2 load checks
are printed by the loader (`npx tsx scripts/loaders/philly/facilities.ts
--dry-run`, or a full load). Three sets of §3.2 figures are one-off
measurements that no committed script reprints: the comparison with the
dashboard's per-school `Site Utilization (MAP)` measure, the NCES comparison of
the loaded school figures, and the distribution of those figures.

## 1. The source

The School District of Philadelphia's **Facilities Data Dashboard**, behind
`philasd.org/fpp`, published as part of *Accelerating Opportunity: Facilities
Master Plan*. It is a Qlik Sense app:

| | |
|---|---|
| host | `dashboards.philasd.org` |
| app id | `2d47abaf-02e1-4671-adba-ea661203eda5` |
| endpoint | `wss://dashboards.philasd.org/embed/app/engineData` (Qlik Engine JSON API) |
| auth | **anonymous** — the engine returns `mustAuthenticate: false` |
| gotcha | the load balancer returns **403 without a browser `User-Agent`**, before the engine sees the request. That header is the only trick. |

The app carries 341 fields. The ones this track needs:

| field | what it is |
|---|---|
| `ULCS Code` | SDP's canonical school code — already `PhillySchool.ulcsCode` |
| `id_site_eos`, `site_name` | site identity — the grain capacity is reported at. The id is the join key; the name is not unique (Lewis Cassidy's is on two sites) |
| `id_bldg_eos`, `bldg_name`, `Building Use Type` | building identity — a **different grain** (§3.3) |
| `Site Capacity` | the denominator |
| `Site Enrollment`, `Site Pre-K Allotment`, `Site Enroll w/ Allotment` | the numerator and its parts |
| `Site Utilization`, `Site Utilization Range Label (NEW)` | the published score and the district's own bands |
| **`Program Change`** | the district's published recommendation per site |
| `Project Recommendation` | the prose version, naming years and receiving schools |
| `FCI Score`, `total_replacement_value_cur` | building condition (building grain) |
| `BLDGFCI` | an older condition index, in the app's `BuildingsOld` table beside `BLDGCAPACITY` (gaps §8) |
| `BLDGCAPACITY` | an older capacity figure, per building, which the app labels 2017 ("OLD"); no figure here uses it (§6.5) |
| `gross_square_footage`, `lot_size`, `year_built`, `floor_count` | physical plant (building grain) |
| `sch_bldg_score_avg`, `program_alignment_score`, `Neighborhood Vulnerability Score` | the plan's other three scoring dimensions |

That is a wider facilities surface than either NYC or Miami publishes —
condition, replacement value, floor area and program fit alongside capacity.

### The district's own definition and cut points

The dashboard states it: *"The Capacity/Utilization Score is calculated by
dividing the total Enrollment (from the October 1 enrollment snapshot) at the
school sites by the total Capacity of the school sites."* Its published bands
are `<50%`, `>=50–74%`, `>=75–99%`, `>=100–104%`, `>=105%` — measured cut
points, not ones we chose.

## 2. Vintage — the binding constraint

**NEGATIVE RESULT, and it changes what can be built.** The app exposes a
`School Year` field with two values (2016-2017, 2024-2025), which looks like a
two-point panel. It is not one. `School Year` lives on the student/catchment
table; the facilities table is cross-joined to it, so every site's capacity
appears under both labels with identical numbers:

- distinct (ULCS, site) carrying a capacity: **342**
- of those, present under more than one `School Year` label: **335**
- of those, with *any* of capacity / enrollment / utilization differing between
  labels: **0**

So the year dimension carries no information about capacity. Philadelphia
publishes **one vintage, SY2024-25**. There is no decomposition of the NYC kind
(§4.3 of the NYC doc: enrollment effect vs capacity effect) because there is no
second point to decompose against. The app does keep a separate, older
per-building capacity it labels 2017, and a SY2016-17 enrollment count beside
it (§6.5); whether that pair is usable as a second vintage is unmeasured
(gaps §4, §5).

Coverage of that vintage, after deduplicating to one row per (ULCS, site):

| sector | sites | with capacity | with enrollment | with utilization |
|---|---|---|---|---|
| District | 249 | 219 | 249 | 219 |
| Charter | 22 | 22 | 22 | 22 |
| Alternative | 34 | 18 | 34 | 18 |
| Pre-K | 80 | 78 | 75 | 78 |

Charter coverage is thin — 22 sites, so charters in their own buildings are
largely absent, the same limitation NYC's Blue Book has.

## 3. Three things that will break a naive loader

### 3.1 The identifier join is free; the *grain* join is not

`ULCS Code` is already the primary key of `PhillySchool`. No crosswalk, no
derivation, no ambiguity — the opposite of NYC, where the Blue Book's `org_id`
needs an explicit rule, and four of its org codes map to two DBNs each across
NYC's directory and Snapshot years (NYC doc §3.1).

But school and site are **many-to-many**, as in NYC. The extract's 428
rows resolve to **328 distinct sites** and 350 ULCS codes: 32 schools sit on
more than one site, and **91 sites host more than one program** — a district
school plus its Pre-K program, or a district school plus a co-located charter.
John Turner (site `S238`) carries three: the district school, Motivation High
School, and KIPP West Philadelphia.

Among district sites with a capacity, **80 of 215 (37%) host more than one
program** — the direct analogue of NYC's 42% of buildings holding more than one
organization.

### 3.2 A multi-site school's enrollment is counted once

32 schools occupy more than one site. On those, the dashboard **repeats the
whole school's enrollment on every site row** while splitting capacity per
site. (NYC's Blue Book splits both, so NYC sums both; the two cities differ on
enrollment only.) A site's enrollment is the full October enrollment of every
programme linked to it, so a school linked to several sites carries its whole
enrollment on each. Of the 32 multi-site schools carrying an enrollment on more
than one site, **29 carry an identical enrollment on every one**. The other
three (3850, 4140, 7060) each have one site shared with other programmes, whose
figure is the total of everything on it: S172 reads 3,644 = 3,467 (Northeast
High) + 177 (3850); S109 reads 542 = 307 (4140) + 147 + 88 (two One Bright Ray
programmes); S173 reads 1,198 = 1,148 (7060) + 25 + 25 (two continuation
academies).

A school's figure is therefore built in three steps, in this order:

1. **Shared sites first.** A school with any site that also hosts another
   directory school gets no school figure. A site shared only with a Pre-K
   programme the Master School List does not carry is attributed to the school,
   and its allotted seats count in the school's numerator.
2. **Enrollment** is the school's own October enrollment, **counted once**,
   plus the Pre-K seats allotted across its sites, **summed**.
3. **Capacity** is **summed** across its sites; utilization is enrollment over
   capacity.

A naive sum or maximum gives a different answer. Summing enrollment multiplies
a school by its site count: Walter Saul (ULCS 6040) has four site rows of 416
that sum to 1,664 (NCES counts 417); Overbrook (4020) has three of 441 that sum
to 1,323 (NCES counts 440). Taking the largest enrollment gives the rule's
answer only after step 1; on the three shared sites above it would take 3,644
for 3850, 542 for 4140 and 1,198 for 7060. The dashboard's own per-school
measure, `Site Utilization (MAP)`, follows this rule: its numerator and
capacity equal the rule's for all 23 multi-site District schools that get a
school figure (a one-off comparison against that measure pulled per ULCS Code
from the app).

The loader (`schoolFacts` in `scripts/loaders/philly/_facilities.ts`) applies
the rule, and three checks guard it, each of which aborts the whole load.
Checks (v) and (vi) are Philadelphia additions to the (i)–(iv) validation
taxonomy of [`05_validation_spec.md`](05_validation_spec.md):

- **check (v)** — on every site, enrollment with allotment equals enrollment
  plus the Pre-K allotment (285 of 285 sites);
- **check (vi)** — a school whose capacity sits on exactly one of its sites
  reproduces that site's published utilization within 0.005 (220 of 220
  schools on a full load; 156 of 156 on the dry run, which runs over every
  link in the extract because it cannot read the directory);
- **a refusal** if any school's own sites disagree on enrollment (0 do).

Check (iv), the taxonomy's computed-value check, recomputes every published
site utilization as (enrollment + Pre-K allotment) ÷ capacity (§6.1, §8). It
compares figures site by site and cannot see a roll-up error; check (vi)
compares at school grain. Against the NCES 2024 enrollment count, joined
through the Master School List as in §6.2, the loaded school figures correlate
at r 0.9993, median difference 2 students, over 222 schools; on the 23
multi-site schools alone the median difference is 1 student (one-off
measurements against the database).

Across the 223 schools with a school figure (§7), the median utilization is
68.1%; 51 schools are under 50% and 26 over 100%, with a maximum of 198.6%;
summed enrollment over summed capacity is 68.6% (a one-off query against the
loaded values). This is a school universe; the §4.1 distribution is over 215
district sites.

**Three school figures may have a short denominator.** Each school has a
linked site with no or zero published capacity that holds classroom buildings:

- **Olney Elementary (7400):** S314 holds two auxiliary classroom buildings,
  W403 (11,000 sq ft) and W442 (area not published), and publishes no site
  capacity. School figure 99.4%.
- **The Workshop School (8560):** S264, Daroff, is W338, 85,080 sq ft, with a
  building capacity of 795 on the app's 2017 basis (§6.5) and a blank site
  capacity. School figure 75.4%.
- **Walter Saul (6040):** S212, the farm, holds classroom buildings W268
  (1,200 sq ft) and W273 (1,900 sq ft) and publishes a site capacity of 0.
  School figure 50.7%.

If those buildings hold seats, the three figures overstate utilization. They
are flagged here, not adjusted: the dashboard publishes no positive capacity
for those sites.

### 3.3 There are three grains, and mixing them corrupts the data

Capacity, enrollment and utilization are per **site**. Floor area, year built,
condition index, use type and the older capacity figure (§6.5) are per
**building** — and 47 of the 285 sites with a building hold more than one. John
Bartram High (ULCS 1010) has a Garage (16,000 sq ft) and a Main Classroom
Building (270,000) on its site and a Fieldhouse on a separate athletic-complex
site; the 8,996 sq ft Bartram Annex sits on a site of its own and hosts a
different programme (ULCS 3810).

Adding a building dimension to the site cube silently changes its grain: a
Bartram row then reads `building_name = "Bartram, John Site 1 - Garage 1",
gross_sqft = 16000` as if that were the school. Pulling
`Only([gross_square_footage])` at site grain is the mirror error — Qlik returns
null when a site has more than one building, so a statistic on it silently runs
on the simplest sites only (§6.3).

The extract therefore writes **two** tables,
`sdp_facilities_sites.csv` (428 rows) and `sdp_facilities_buildings.csv` (502
rows), joined on `site_id`. Both row counts are larger than the thing they
count: the sites cube repeats a site once per programme (428 rows → 328 sites)
and the buildings cube does the same one grain down (502 rows → **399 distinct
buildings**, 94 ids repeating up to four times; 362 of them sit on 285 sites,
and the other 37 ids carry nothing but the id). Dedupe both before counting or
summing anything.

The building cube repeats because `ULCS Code` is a field of the app's Buildings
table, which holds one record per building per programme. `BLDGCAPACITY` sits
in a separate table keyed by building, so the app's own `Sum([BLDGCAPACITY])`
per site counts each building once; `gross_square_footage` sits in the
per-programme table, so even the app's own `Sum` of it counts a two-programme
building twice. Summing rows rather than buildings would inflate 79 of the 236
sites carrying a `BLDGCAPACITY` and 77 of the 213 sites in the floor-area
comparison (§6.3). The committed provenance record
(`data/cities/philly/sources/sdp_facilities_dashboard/2026-09-22/provenance.json`)
says in its grain note that the tables join on `site_name`; they join on
`site_id`, because a site name is not unique (§1).

## 4. What the data says

### 4.1 Distribution, SY2024-25

Counted on **distinct sites**, not on (site, program) rows — a site hosting a
school plus its Pre-K program publishes the same figures twice, and counting
rows would weight it double:

| universe | n | p10 | p25 | **median** | p75 | p90 | **<50%** | <33% | >100% | Σ/Σ |
|---|---|---|---|---|---|---|---|---|---|---|
| District sites | **215** | 37 | 51 | **67.1** | 85 | 106 | **24.7%** | 7.9% | 13.5% | 68.4% |

By level:

| level | n | median | <50% | >100% | Σ/Σ |
|---|---|---|---|---|---|
| Elementary | 46 | 72.2 | 6.5% | 21.7% | 78.0% |
| Elementary-Middle (K-8) | 106 | 65.5 | 27.4% | 9.4% | 68.2% |
| **Middle** | 12 | **45.8** | **50.0%** | 16.7% | 57.1% |
| High | 46 | 64.8 | 28.3% | 10.9% | 65.9% |

Two further levels, Middle-High (n = 4) and Elementary-Middle-High (n = 1),
are left out of this table as too small to summarize; they are in the packet's
`distribution.by_school_level` and on the page, and bring the rows to 215.

The emptiness is concentrated in the middle grades. Straight elementary schools
are the fullest group and a fifth are over capacity; standalone middle schools
are the emptiest group in the district — half of the twelve are under 50% — and
K-8 and high schools sit between. Twelve is a small group, so the middle-school
figure is indicative rather than settled.

**Three split campuses are counted as six sites.** Bache-Martin (2210: S016 and
S148), Bayard Taylor (7440: S234 and S278) and Hancock (8770: S100 and S130)
each have classroom capacity on two sites of their own. SDP publishes a
utilization for each site, and because the dashboard repeats a school's whole
enrollment on every one of its sites (§3.2), each of those six figures divides
the whole school by one site's capacity: Hancock reads 194.6% on S100 and
256.4% on S130, against 110.6% for the school. The figures here keep SDP's
published per-site basis, so the enrollment total counts 1,871 students twice.
Merged into one unit per school (enrollment once, Pre-K seats and capacity
summed):

| universe | n | median | <50% | <33% | >100% | Σ/Σ | `<66%` Close lift |
|---|---|---|---|---|---|---|---|
| District sites, as published | 215 | 67.1 | 24.7% | 7.9% | 13.5% | 68.4% | 7.47× on 104 sites (p = 0.0012) |
| Split campuses merged | 212 | 66.6 | 25.0% | 8.0% | 11.8% | 67.4% | 7.13× on 105 units (p = 0.0015) |

All six sites are recommended Modernize, so no Close count changes. The packet
carries this as `distribution.split_campus_sensitivity`.

### 4.2 Against the other two cities

| | n | median | <50% | <⅓ | >100% | overall | vintage |
|---|---|---|---|---|---|---|---|
| **Philadelphia** (district sites, 2024-25) | 215 | **67.1%** | **24.7%** | **7.9%** | 13.5% | 68.4% | 1 snapshot |
| Miami-Dade (district-run regular schools, 2024-25) | 316 | 66.7% | 21.2% | 4.7% | 7.9% (≥100) | 69.7% | 1 snapshot + FISH LOS series |
| NYC (schools, 2024-25) | 1,702 | 80% | 13.3% | 3.6% | 26.2% | 83.1% | 12 school years; latest 2025-26: 78%, 15.5% |

All three rows are SY2024-25, read from the three committed packets (the
Philadelphia packet's `cross_city` block). The under-a-third cuts are <33
(Philadelphia), <33.33 (Miami-Dade) and <33.3 on whole percents (NYC); Miami-Dade
publishes only ≥100 for the top cut. Philadelphia's per-site basis counts three
split campuses twice (§4.1).

Miami-Dade's terms are defined in its own doc
(`docs/cities/miami/06_enrollment_capacity_closures.md`). FISH is the Florida
Inventory of School Houses, the state's facility inventory; Miami-Dade's
denominator is its permanent capacity. The FISH LOS series is FLDOE's annual
FISH Level of Service report per district, fetched for 2022-23 to 2024-25.

**Philadelphia has the largest under-50% tail of the three — a quarter of its
district sites, 3.5 points above Miami-Dade and 11.4 above NYC — and more than
twice NYC's share under a third (7.9% against 3.6%).** Its median sits with
Miami-Dade's (67.1% against 66.7%), about 13 points below NYC's (80%).

Three caveats apply wherever this comparison appears:

1. **Different denominators.** Philadelphia's capacity is SDP site capacity, Miami's is permanent FISH capacity, NYC's is SCA target capacity. NYC's own two published capacity definitions differ enough to move its under-50% share by 2.4 points (NYC doc §4.4, on 2013-14), about a third of the 7.9-point NYC–Miami-Dade gap in 2024-25 (13.3% against 21.2%).
2. **All three numerators include Pre-K, on different bases.** Philadelphia adds the Pre-K seats allotted to a site's early-childhood programmes to its K-12 October enrollment; NYC's Blue Book enrollment is the audited register including 3-K and Pre-K students; Miami-Dade's is the October (FLDOE Survey 2) headcount, PK-12. Philadelphia counts Pre-K seats where NYC and Miami-Dade count Pre-K children. The denominators differ on Pre-K as well: NYC's target capacity gives each Pre-K room up to 18 seats (36 half-day) — the lower of that and the room's area ÷ 35 sq ft — with the PK Cap column restating that share rather than adding to it; Philadelphia's site capacity is assigned to Pre-K space too (its five Pre-K-only sites carry 60–221 seats); whether Miami-Dade's FISH capacity counts Pre-K stations is not established. Pre-K is 2.2% of Philadelphia's district-site numerator (215 sites; §6.1 shows the published figure is built this way); in NYC, 3-K and Pre-K together are 3.5% of the school-grain numerator (Pre-K alone 2.5%; 1,702 schools, 2024-25).
3. **Different universes.** Philadelphia's figure is district sites; Miami's is 316 district-run regular schools, including K-8, 6-12 and K-12; NYC's is the 1,702 organizations in DOE buildings whose org code resolves to exactly one DBN, co-located charters (about 110) and District 75 included.

### 4.3 Low utilization tracks the district's own closure recommendation

This is the analysis Miami's plan designs and NYC cannot run: the district
published its recommendation for every site, so the reference already exists.

On the 215 distinct district sites, **the base rate of a Close recommendation
is 7.4%**:

| cut point | flagged | close rate | rest | lift | Fisher p |
|---|---|---|---|---|---|
| utilization < 33% | 17 | 29.4% | 5.6% | 5.29× | 0.0043 |
| utilization < 50% | 53 | 18.9% | 3.7% | 5.09× | 0.00093 |
| **utilization < 66%** | 104 | 13.5% | 1.8% | **7.47×** | 0.0012 |
| utilization < 75% | 140 | 10.7% | 1.3% | 8.04× | 0.013 |

Not one site at or above 100% utilization is recommended for closure. Size
separates more strongly than utilization: on the same 215 sites, enrollment
(with allotted Pre-K seats) under 300 carries a **10.72×** lift — 12 of 47
sites (25.5%) recommended for closure against 4 of 168 (2.4%), p = 3.2 × 10⁻⁶ —
so the same caveat as NYC applies: utilization adds information, but school
size is the stronger single predictor.

**Ablation.** Within the 91 sites under 400 students, `<50%` utilization still
carries a 3.14× lift (9 of 38 against 4 of 53, p = 0.037); within the 124 sites
of 400+, no site at or above 66% is recommended for closure, so `<66%` carries
an unbounded lift (3 of 38, 7.9%, against 0 of 86, p = 0.027). Utilization is
not a restatement of size — but as in both other cities, **size is the stronger
single predictor** (enrollment < 300: 10.72×). The packet derives these counts
on distinct sites (`decision_lift.size`, `decision_lift.ablation`); a site
hosting two district schools enters once.

**What this is and is not.** Both sides come from the same publication, so this
measures how closely the district's decision tracks its own stated criterion —
not whether an outside observer could have predicted the list. That is a
weaker claim than Miami's (where the closure list and the enrollment predictor
come from different publishers), and the page says so. It is still the only
one of the three cities where the question can be asked against a published
decision list.

## 5. Where Philadelphia sits against NYC and Miami

| | Philadelphia | NYC | Miami-Dade |
|---|---|---|---|
| utilization published? | yes, per site | yes, per school and building | no — must be derived, three numerators |
| history | **1 snapshot** | 12 school years, 2013-14 → 2025-26 (2020-21 never published) | FISH LOS 2022-23→2024-25 + GIS + ABC book |
| machine-readable? | Qlik Engine API, anonymous | **plain Socrata JSON/CSV** | PDF tables, two-parser gate |
| join to our schools | **free** (`ULCS Code` is the PK) | org-code rule; 4 ambiguous codes skipped; 99.6–99.7% | MSID/CCD/GIS join on the 4-digit number |
| closure/decision list | **published, machine-readable** | **none public** | hand-built from board PDFs |
| building condition | FCI, replacement value, per system | partial (room inventory) | not verified |
| co-location | **37% of district sites** | 42% of buildings | rare |

In the Miami-Dade column, the ABC book is M-DCPS's *Attendance Zone
Recommendations* document, which gives capacity blocks for the schools it
targets; MSID is FLDOE's Master School ID directory; GIS is the county's
Public School layer (Miami-Dade doc §1 and §7).

No city leads on every row. NYC has the history and the clean file;
Philadelphia has the decision list, the condition data and the free join;
Miami-Dade has a closure process of its own: nine schools closed or
consolidated for 2026-27, and 27 more flagged for reassessment or review in
August 2026, with a decision targeted for December 2026 (Miami-Dade doc §0).

## 6. Verification against other sources

### 6.1 Computed values

The published `Site Utilization` equals (K-12 enrollment + allotted Pre-K
seats) ÷ site capacity on **all 337 (site, programme) rows carrying a
utilization (245 distinct sites), maximum absolute difference 0.0000**.
Enrollment alone matches 192 rows (57%) — exactly the 192 whose allotment is
zero, so that is the share of rows without Pre-K, not independent evidence. The
dashboard's stated definition — "the total number of students enrolled (and
allotted seats for PreK) at the school sites divided by school site capacity" —
is the one it computes. Pre-K enters as allotted seats, not enrolled children.

### 6.2 Enrollment, against an independent publisher

SDP → PDE → NCES is a different chain from the dashboard. Joined through the
`ULCS Code → NCES Code` crosswalk in SDP's own Master School List (the source
this repo already registers as `sdp_master_school_list_2024-25`), not by name:

- dashboard schools 345, joined to CCD **241**
- **median absolute difference 2 students**, 68% within 2% of school size, 76%
  within 5%, **r = 0.9718**

The dashboard's October 1 count and CCD's membership are not the same count
date, so exact agreement is not expected. This is the only independent
publisher available for Philadelphia, and it confirms the numerator at
magnitude and coverage. The probe's roll-up takes the largest enrollment across
all of a school's sites, shared ones included, so a school on a shared site
carries the whole site's total here. The loader's school figures set those
schools aside (§3.2) and correlate with the NCES 2024 count at r 0.9993, median
difference 2 students, over 222 schools.

### 6.3 Capacity, against floor area

Classroom-use building area summed per site, each building counted once
(§3.3), against site capacity:

| | measure | n | median sq ft per seat | R² of capacity on area |
|---|---|---|---|---|
| Philadelphia | gross area of classroom-use buildings, each counted once | 213 | **122.0** (IQR 99.7–147.5) | **0.721** |
| NYC | instructional room area, Pre-K rooms excluded as registered¹ | 1,331 | 32.6 (IQR 28.8–36.6) | 0.922 |

¹ With Pre-K full- and half-day rooms added (2025-26): median 34.2 sq ft per
seat, R² 0.918 (NYC doc §5.2).

Each building has to be counted once. The building extract and the app's own
`Sum([gross_square_footage])` repeat a building once per programme on its site,
which would inflate 77 of these 213 sites; `Only([gross_square_footage])` at
site grain returns null wherever a site has more than one building, which would
leave only the 182 simplest.

The two cities' levels are not comparable — Philadelphia publishes gross
building area (structure, circulation, gyms, offices and all), NYC's comes from
a room-by-room inventory filtered to teaching space. Only the R² compares, and
Philadelphia's capacity tracks floor area less tightly: **0.721 against NYC's
0.922**, and some of that may reflect the coarser numerator — gross area
carries gyms, corridors and offices that NYC's room filter removes. It is a
within-publisher consistency check, not an independent confirmation of the
capacity figure.

### 6.4 What is *not* verified

- **No second publisher states Philadelphia school capacity.** PDE does not publish building capacity at school level; NCES does not publish capacity at all. §6.3 is a within-publisher consistency check (R² 0.721), not an independent one. The app's older, 2017-labelled per-building figure (§6.5) is a different year's capacity, not a check on the 2024-25 one.
- **No historical capacity in a usable form**, so no trend can be verified (§2). The 2017-labelled figure and its SY2016-17 enrollment count (§6.5) are unmeasured as a vintage.
- The figures shown under the app's `School Year` label 2016-17 are the 2024-25 figures under a second label — read without this check, the dashboard shows a flat trend that does not exist. (That is the `School Year` field; the app's separate "OLD" measures are a different thing, §6.5.)

### 6.5 An older, 2017-labelled capacity figure

The app also carries `BLDGCAPACITY`, a capacity per building, in its
`BuildingsOld` table. Its master measure "Current Capacity (OLD)" is
`sum(BLDGCAPACITY)`; the app labels its OLD view 2017 and pairs the figure, in
"Site Utilization (OLD)", with a SY2016-17 enrollment count. The latest year
built on the table's 268 capacity records is 2011. The app documents nothing
more about how it was measured, and no figure in this document uses it.

The extract repeats a building once per programme on its site (§3.3), so a
per-site sum has to count each building once, as the app's own
`Sum([BLDGCAPACITY])` does. On the 207 district sites carrying both figures:

| | site capacity | `BLDGCAPACITY`, each building once |
|---|---|---|
| seats | 169,551 | 164,931 (2.7% fewer) |
| 2024-25 utilization, median | 67.1% | 61.8% |
| share under 50% | 25.1% | 31.4% |

Site by site the two differ: building over site capacity runs 0.754 at p10,
0.989 at the median and 1.301 at p90, and 56 of the 207 differ by more than a
quarter (outside 0.8–1.25×). The utilization rows divide 2024-25 enrollment by
a 2017-labelled capacity, which pairs two different years. A further 19
`BLDGCAPACITY` records (11,536 seats) are linked to no current building.

## 7. Metrics and schema

Two tables hold the site grain (§3.3), the figures a school page lists beside
the school's own:

- **`PhillySite`** (`philly_sites`, 328 sites) — keyed on `id_site_eos` (`S238`), carrying the site's name, capacity, Pre-K allotment, enrollment with allotment and utilization as published. These are SITE facts, not school facts.
- **`PhillySiteProgram`** (`philly_site_programs`) — the join table `(siteId, ulcsCode)`. School and site are many-to-many (§3.1), so a foreign key on either side would drop rows. Links to a ULCS code the Master School List does not carry are skipped; on the 2024-25 extract those are Pre-K programmes.

The rest of the extract stays in the committed CSVs, which the analysis packet
reads, and is not stored: the whole building grain (floor area, condition,
`BLDGCAPACITY`; §3.3, §6.3, §6.5), and six site-grain fields that no page
reads from the database — sector, school level, K-12 enrollment, the utilization
band, and the district's `Program Change` and `Project Recommendation` (§4.3).
The gap register, `docs/capacity-utilization-gaps.md` §10, lists them and how
to add them.

The metric keys sit in `philly_metric_definitions` with subdomain `Facilities`
and direction `NEUTRAL`; the subdomain keeps them out of the percentile batch,
so they carry no percentile. There are four, all at school grain, with values
for the 223 directory schools that get a school figure (39 on a shared site get
none), all SY2024-25:

| metric_key | unit | values | notes |
|---|---|---|---|
| `site_capacity` | COUNT | 223 schools | summed across a school's own sites (§3.2) |
| `site_enrollment` | COUNT | 223 schools | October 1 enrollment counted **once**, plus the Pre-K seats allotted across the school's sites (§3.2) |
| `prek_allotment` | COUNT | 223 schools | allotted seats, inside the utilization numerator; summed across a school's sites |
| `utilization_pct` | PERCENT | 223 schools | `site_enrollment` ÷ `site_capacity`; published per site as (enrollment + Pre-K) ÷ capacity |

The plan's other three scoring dimensions (§1) are not loaded.

## 8. What is built

| Piece | What exists |
|---|---|
| Source | `sdp_facilities_dashboard` in `sources.yaml`, with the app id, the field list and the `User-Agent` gotcha. `python -m pipeline_philly.fetch_facilities` (`make philly-facilities-fetch`) pulls the extract and commits it as two CSVs plus provenance |
| Tables | `philly_sites`, `philly_site_programs` (§7), created by `scripts/sql/capacity-schema.sql` |
| Loader | `scripts/loaders/philly/facilities.ts` (`make philly-facilities-load`), which reads the committed site CSV, never the network, and applies the §3.2 roll-up; its hash pin covers both CSVs |
| Load checks | (iv) every published utilization recomputed, 245 of 245 sites within 0.005 (max difference 0.0000); (v) and (vi) and the refusal on conflicting enrollments (§3.2). Each aborts the load; a failure means the extract changed shape. The loader also refuses a CSV pair whose hash differs from the one pinned beside its school year, since the dashboard publishes no year of its own, and a reload removes the school figures and site links that the new extract no longer produces |
| Independent check | enrollment against NCES CCD (§6.2), in the probe |
| Packet and pages | `scripts/analysis/philly-capacity-packet.ts` → `data/analysis/philly-enrollment-capacity.json`, rendered at `/philly/methodology/analysis/enrollment-capacity`; each school page shows the school's figure, or its sites' figures where it shares a site |

Philadelphia is the cheapest of the three cities to load: the join is free and
the computed-value check holds on every site, and the decision list the packet
reads comes structured in the same extract. The cost is that the page shows a
cross-section only, until a historical capacity vintage turns up (§9).

## 9. Open items and caveats

- **One vintage.** Until a historical capacity source turns up, no Philadelphia trend figure is possible. Candidates not yet checked: the app's own 2017-labelled `BLDGCAPACITY` and SY2016-17 enrollment count (§6.5; unmeasured — check the enrollment against CCD 2016-17 first), the 2017 Comprehensive School Planning Review, the longitudinal school list, and PICA / City Controller reports. Checked and ruled out: SDP's Facilities Condition Assessment archive, which is condition only (gaps §4).
- **Qlik as the only access path.** There is no CSV, no Socrata dataset, no file on the SDP open-data CDN — the SDP open-data catalog has no facilities category at all. The fetcher depends on an undocumented engine API and an app id read out of a JavaScript bundle. Both could change without notice, so the pulled extract is committed, and the fetcher refuses to write a short table rather than silently serve a truncated one.
- **Anonymous access is a courtesy, not a contract.** It works today; the 403-without-User-Agent behaviour shows there is already a filter in front of it.
- **Charter coverage is 22 sites.** Any district-wide utilization figure is a district-sites figure.
- **Three school figures may have a short denominator** (§3.2): 7400, 8560 and 6040 hold classroom buildings on sites with no positive published capacity. They are flagged in this document, not adjusted, and not flagged on the school pages.
- **Pre-K is in the numerator as allotted seats** (§4.2, §6.1). NYC and Miami-Dade count Pre-K too, as enrolled children (NYC also 3-K), so the three numerators are not on one basis; name it wherever the cities appear together.
- **The decision-list analysis is same-publisher** (§4.3) and is labelled as measuring adherence to a stated criterion, not out-of-sample prediction.
