Source document

scripts/loaders/philly/metric_definitions_facilities.ts

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.

// Seed the Philadelphia facilities metric keys — the capacity, enrollment and
// utilization facts written by scripts/loaders/philly/facilities.ts out of the
// SDP Facilities Data Dashboard. Idempotent — re-running upserts.
//
// Kept apart from philly/metric_definitions.ts (the outcome, survey and
// demographic keys), as NYC's facilities keys are kept apart in
// scripts/loaders/metric-definitions-capacity.ts.
//
// Run:  npx tsx scripts/loaders/philly/metric_definitions_facilities.ts
//
// Source is the SDP Facilities Data Dashboard behind philasd.org/fpp, pulled by
// pipeline_philly/fetch_facilities.py and loaded by
// scripts/loaders/philly/facilities.ts. Seed these BEFORE that loader:
// philly_school_year_metrics.metric_key carries a foreign key to this table.
//
// Four properties bear on every key below, and each is the opposite of what a
// first reading of the dashboard suggests:
//   * The grain is the SITE, not the school. 80 of 215 district sites with a
//     capacity host more than one programme, and every programme's row repeats
//     the site's figures, so counting rows double-weights those sites.
//   * School ↔ site is many-to-many: 32 schools sit on more than one site.
//     School-grain facts are written only where attribution is unambiguous.
//   * PreK allotment (seats, not children) is INSIDE the utilization numerator here.
//     NYC's Blue Book counts enrolled 3-K/Pre-K students and gives Pre-K rooms target
//     capacity, so the two cities' utilization figures are on different bases as published.
//   * One vintage exists, SY2024-25. The dashboard's School Year field is a
//     cross-join across the facilities table and carries no capacity
//     information — the figures under its 2016-17 label are the 2024-25
//     figures under a second label, so no trend can be read off it. (The app's
//     separate "OLD" measures — BLDGCAPACITY, labelled 2017 — are a different
//     thing and are unmeasured as a vintage; doc §6.5.)
//
// All four keys are school grain. The building figures the dashboard also
// publishes (condition index, floor area, a second capacity) are not stored
// (gap register §10).
//
// Every key is NEUTRAL: none of them is better in a direction. What keeps them
// out of the percentile batch, so that they carry no percentile, is subdomain =
// 'Facilities' (philly/compute_percentiles.ts), not the direction; a new
// capacity key stays out only if it carries that subdomain.
//
// Evidence: docs/cities/philly/06_enrollment_capacity_utilization.md.

import { prisma } from "./_lib";
import type {
  MetricDomain,
  MetricUnit,
  MetricDirection,
} from "@prisma/client";

type Def = {
  key: string;
  displayName: string;
  domain: MetricDomain;
  subdomain?: string;
  sourceName: string;
  sourceUrl: string;
  unit: MetricUnit;
  direction: MetricDirection;
  description: string;
  derived?: boolean;
};

const SDP_FACILITIES_URL = "https://www.philasd.org/fpp/";

const FACILITIES: Def[] = [
  {
    key: "site_capacity",
    displayName: "Capacity",
    domain: "ACCESS",
    subdomain: "Facilities",
    sourceName: "SDP Facilities Master Plan — Facilities Data Dashboard",
    sourceUrl: SDP_FACILITIES_URL,
    unit: "COUNT",
    direction: "NEUTRAL",
    description:
      "Seats the district's Facilities Master Plan assigns the site, summed across a school's sites. This is the denominator of utilization_pct and the basis every published Philadelphia capacity figure in this repo uses. It is not the only capacity number the dashboard carries: an older, 2017-labelled per-building figure (BLDGCAPACITY, not stored) runs 0.754-1.301x it across the middle 80% of 207 district sites (method doc §6.5). A school sharing a site with another school in the directory (the Master School List) gets no school-grain value, because the site's capacity is the site's and not that school's alone; the site record still carries the number. A site shared only with programmes the directory does not carry is attributed to the school it hosts: in SY2024-25 those programmes are all Pre-K, 71 of the 223 schools with a value sit on such a site, and the Pre-K seats allotted there are in their numerator. SY2024-25 only.",
  },
  {
    key: "site_enrollment",
    displayName: "Enrollment (incl. Pre-K allotment)",
    domain: "ACCESS",
    subdomain: "Facilities",
    sourceName: "SDP Facilities Master Plan — Facilities Data Dashboard",
    sourceUrl: SDP_FACILITIES_URL,
    unit: "COUNT",
    direction: "NEUTRAL",
    description:
      "The October 1 enrollment snapshot INCLUDING the Pre-K seats allotted to the site (the dashboard's Site Enroll w/ Allotment), which is the numerator of the published utilization score. A school on several sites of its own carries its enrollment ONCE plus the Pre-K seats allotted across its sites: the dashboard repeats the whole school's enrollment on every one of its site rows while splitting capacity per site, so summing multiplies a school by its site count — Walter Saul's four rows of 416 sum to 1,664 (NCES counts 417). Counted once, the stored figures correlate with the NCES 2024 enrollment count at r 0.999 (222 schools joined through the Master School List). A school sharing a site with another school in the directory gets no school-grain value; a site shared only with Pre-K programmes, which the directory does not carry, is attributed to the school it hosts.",
  },
  {
    key: "prek_allotment",
    displayName: "Pre-K allotment",
    domain: "ACCESS",
    subdomain: "Facilities",
    sourceName: "SDP Facilities Master Plan — Facilities Data Dashboard",
    sourceUrl: SDP_FACILITIES_URL,
    unit: "COUNT",
    direction: "NEUTRAL",
    description:
      "Pre-K seats allotted to the site — seats, not enrolled children. Carried separately so the numerator can be decomposed, but it is already inside site_enrollment and inside utilization_pct: the published score matches enrollment-plus-allotment on all 337 (site, programme) rows, and enrollment alone only on the 192 rows whose allotment is zero. A school on several sites sums it across them (it is published on one site only). NYC's Blue Book counts enrolled 3-K and Pre-K students rather than allotted seats, so a cross-city comparison names its Pre-K basis before the figures are shown together.",
  },
  {
    key: "utilization_pct",
    displayName: "Utilization",
    domain: "ACCESS",
    subdomain: "Facilities",
    sourceName: "SDP Facilities Master Plan — Facilities Data Dashboard",
    sourceUrl: SDP_FACILITIES_URL,
    unit: "PERCENT",
    direction: "NEUTRAL",
    description:
      "(Enrollment + Pre-K allotment) ÷ site capacity, as the district publishes it per site — the dashboard's own definition, and it reconciles exactly (max |diff| 0.0000 across 337 (site, programme) rows). A school on several sites of its own reads its enrollment once, plus the Pre-K seats allotted across its sites, over its capacity summed across those sites — the rule the dashboard's per-school figure uses; each of those sites' own published figure divides the whole school by that one site's capacity. Pre-K enters this numerator as allotted seats; NYC's Blue Book utilization_pct counts enrolled 3-K and Pre-K students over a target capacity that includes Pre-K rooms. The two keys share a name across the two cities and do not share a definition. On 215 distinct district sites in SY2024-25 the median is 67.1%, 24.7% sit under 50% and 13.5% over 100%, concentrated in the middle grades (standalone middle schools: n=12, median 45.8, half under 50%). Against the district's own published Program Change recommendations (base rate 7.4% Close), sites under 66% utilization carry a 7.47x lift (n=104, p=0.0012), under 75% an 8.04x lift (n=140, p=0.013), under 50% 5.09x (n=53, p=0.00093) and under 33% 5.29x (n=17); both sides of that test come from the same publication, so it measures how closely the district's decisions track its own stated criterion rather than out-of-sample prediction. School size separates more strongly than utilization: enrollment (with allotted Pre-K seats) under 300 carries a 10.72x lift (n=47).",
  },
];

async function main() {
  console.log(`[seed] upserting ${FACILITIES.length} Philly facilities metric definitions`);
  for (const def of FACILITIES) {
    await prisma.phillyMetricDefinition.upsert({
      where: { key: def.key },
      update: def,
      create: def,
    });
  }
  console.log(`[seed] done — ${FACILITIES.length} facilities metrics in philly_metric_definitions`);
}

main()
  .catch((e) => {
    console.error(e);
    process.exit(1);
  })
  .finally(() => prisma.$disconnect());