Source document

docs/cities/philly/probe/README.md

Served verbatim from the project repository. Internal working document conventions apply: documents may reference file paths, branch names, and findings-ledger anchors from the repo.

probe/ — measurement scripts behind the Philly design docs

Read-only Python scripts that measure the numbers quoted in the numbered docs, kept as code so every figure is reproducible (house rule: derive quantities from the process, don't quote them). Same idiom as docs/cities/miami/probe/ and docs/design/probe/.

They fetch into a scratch directory — default data/cities/philly/sources/_probe/, git-ignored; override with PHILLY_PROBE_DIR — and cache every download, so a rerun is offline and the cache is the audit trail.

ScriptReachesProduces
facilities.pySDP Facilities Data Dashboard (Qlik Sense Engine API, anonymous) + SDP Master School List + NCES CCD via the Urban Institute APIthe measured figures in 06_enrollment_capacity_utilization.md: what the app holds, the single-vintage negative result, the ULCS join, the computed-value check, utilization distributions, the lift of the district's own Close recommendation over utilization, the CCD and floor-area checks (§6.2, §6.3), and the older, 2017-labelled capacity figure of §6.5 (BLDGCAPACITY counted once per building against Site Capacity, with the app's own table and labels for it). Sections H1 and I also print what a sum over the extract's repeated rows reads, so the size of that hazard is a printed number. The §4.1–4.3 site-grain figures are the packet's (scripts/analysis/philly-capacity-packet.ts), on its distinct-site universe; the §3.2 school roll-up checks are the loader's

Requires requests, websocket-client, scipy.

python docs/cities/philly/probe/facilities.py

Two things to know before changing it:

  • The Qlik endpoint returns 403 without a browser User-Agent. The rejection happens at the load balancer, before the engine sees the request, so it looks like an auth failure and is not one. Anonymous access is otherwise open (mustAuthenticate: false).
  • GetHyperCubeData caps a page at roughly 10,000 cells. PAGE is set to 250 rows; at the probe's 14 columns, raising it past ~700 returns Result too large (error 6001).

The app id (2d47abaf-…) is read out of the dashboard's env-config Nuxt plugin and is not documented anywhere; if the dashboard is rebuilt it may change.

The loader path is a different script

facilities.py measures; nothing it writes reaches the database. The extract the pipeline runs on is pipeline_philly/fetch_facilities.py, which talks to the same Qlik app and writes CSVs that are committed rather than cached:

pip install websocket-client                            # the Engine API is WebSocket, not HTTP
python -m pipeline_philly.fetch_facilities
python -m pipeline_philly.fetch_facilities --dry-run    # fetch, report, write nothing
probe/facilities.pypipeline_philly/fetch_facilities.py
Purposereproduce every figure in 06_…produce the tables the loader and the packet read
ReachesQlik + Master School List + NCES CCD (Urban Institute API)Qlik only
Writesa scratch cache under data/cities/philly/sources/_probe/, git-ignoreddata/cities/philly/reference/parsed/sdp_facilities_sites.csv and …_buildings.csv, plus sources/sdp_facilities_dashboard/<date>/provenance.json — all committed
Consumed bythe doc, by a reader rerunning itnpx tsx scripts/loaders/philly/facilities.ts (the sites CSV; its hash pin covers both), and scripts/analysis/philly-capacity-packet.ts (both)
Needsrequests, websocket-client, scipywebsocket-client (in pipeline_philly/requirements.txt)

The CSVs are committed because the source is an undocumented Qlik app that can change or disappear without notice, so the extract is the audit trail rather than the dashboard. The fetcher refuses to write a short table — a truncated pull exits non-zero instead of quietly shrinking a committed file.

Both scripts hit the two limits described above: the 403 without a browser User-Agent, and the ~10,000-cell page cap. They differ in how much of the app they read. facilities.py pulls a cube at site grain (with School Year), where FCI and gross_square_footage arrive as site measures. It caches the rest separately in fpp_building_basis.json:

  • the site cube on the fetcher's own dimensions;
  • a per-site cube that aggregates the building fields with Aggr(…, id_site_eos, id_bldg_eos), one value per building, for floor area (§6.3) and BLDGCAPACITY (§6.5);
  • a (site × programme) cube;
  • every record of the app table that holds BLDGCAPACITY, with the number of current buildings each record links to;
  • one row per current building, with no ULCS dimension, so no programme repeat;
  • the app's own measure definitions and labels.

The fetcher pulls the site cube and a second one at building grain, the source of year built, use type, per-building area and the older capacity figure (BLDGCAPACITY, §6.5). The analysis packet joins the two on site_id; the loader stores no building field. Adding a building dimension to the site cube instead would silently change its grain: capacity, enrollment and utilization are published on each site row, and a site can hold a garage and a fieldhouse alongside its classroom block. A school on several sites carries its whole enrollment on each of them (doc §3.2), so a school figure counts it once.

The building grain has a second trap one level down. ULCS Code is a field of the app's Buildings table, which holds one record per building per programme, so a building on a site hosting two programmes is emitted twice by the fetcher's building cube and is counted twice by a plain Sum of gross_square_footage inside Qlik itself. BLDGCAPACITY sits in a separate table keyed by building, so a plain Sum([BLDGCAPACITY]) is safe, but summing the fetcher's rows is not. Section I prints the row-summing reading (median, share under 50%, ratio spread, by level) beside the per-building one, labelled [counts a repeated building twice], and section H1 does the same for floor area.

make philly-facilities-fetch runs the fetcher through the pipeline_philly venv; make capacity-help prints where it sits in the load order.