diff --git a/backend/data_loader.py b/backend/data_loader.py index 1cfbbc1..ddfac01 100644 --- a/backend/data_loader.py +++ b/backend/data_loader.py @@ -414,6 +414,7 @@ def get_supplementary_data(db: Session, urn: int) -> dict: "first_preference_applications": a.first_preference_applications, "first_preference_offers": a.first_preference_offers, "first_preference_offer_pct": a.first_preference_offer_pct, + "oversubscription_ratio": a.oversubscription_ratio, "oversubscribed": a.oversubscribed, } if a diff --git a/backend/models.py b/backend/models.py index 57f1586..c92999d 100644 --- a/backend/models.py +++ b/backend/models.py @@ -184,6 +184,7 @@ class FactAdmissions(Base): first_preference_applications = Column(Integer) first_preference_offers = Column(Integer) first_preference_offer_pct = Column(Float) + oversubscription_ratio = Column(Float) oversubscribed = Column(Boolean) admissions_policy = Column(String(100)) diff --git a/nextjs-app/components/SchoolDetailView.tsx b/nextjs-app/components/SchoolDetailView.tsx index 18d19d3..6dc8932 100644 --- a/nextjs-app/components/SchoolDetailView.tsx +++ b/nextjs-app/components/SchoolDetailView.tsx @@ -226,7 +226,7 @@ export function SchoolDetailView({
Oversubscribed
{admissions.first_preference_offer_pct != null - ? `${Math.round(admissions.first_preference_offer_pct)}% got first choice` + ? `${Math.round(admissions.first_preference_offer_pct)}% of first-choice applicants offered a place` : 'More applicants than places'}
diff --git a/nextjs-app/lib/types.ts b/nextjs-app/lib/types.ts index a3ba806..7ead4bf 100644 --- a/nextjs-app/lib/types.ts +++ b/nextjs-app/lib/types.ts @@ -134,7 +134,10 @@ export interface SchoolAdmissions { total_applications: number | null; first_preference_applications?: number | null; first_preference_offers?: number | null; + /** Of families who listed this school as 1st preference, the % that received an offer (0–100). */ first_preference_offer_pct: number | null; + /** 1st-preference applications per place offered (>1 means oversubscribed). */ + oversubscription_ratio?: number | null; oversubscribed: boolean | null; } diff --git a/nextjs-app/lib/utils.ts b/nextjs-app/lib/utils.ts index 2291399..8309100 100644 --- a/nextjs-app/lib/utils.ts +++ b/nextjs-app/lib/utils.ts @@ -557,11 +557,12 @@ export function buildSchoolSummary( // Admissions clause if (admissions?.oversubscribed) { if (admissions.first_preference_offer_pct != null) { + const pct = Math.round(admissions.first_preference_offer_pct); parts.push( - `heavily oversubscribed — just ${Math.round(admissions.first_preference_offer_pct)}% of applicants get a first-choice offer`, + `oversubscribed — ${pct}% of first-choice applicants are offered a place`, ); } else { - parts.push('heavily oversubscribed'); + parts.push('oversubscribed'); } } else if (admissions?.first_preference_offer_pct != null && admissions.first_preference_offer_pct >= 90) { parts.push('most families get their first-choice offer'); diff --git a/pipeline/transform/models/marts/fact_admissions.sql b/pipeline/transform/models/marts/fact_admissions.sql index 9ca5e84..36f18b2 100644 --- a/pipeline/transform/models/marts/fact_admissions.sql +++ b/pipeline/transform/models/marts/fact_admissions.sql @@ -9,6 +9,7 @@ select first_preference_applications, first_preference_offers, first_preference_offer_pct, + oversubscription_ratio, oversubscribed, admissions_policy from {{ ref('stg_ees_admissions') }} diff --git a/pipeline/transform/models/staging/stg_ees_admissions.sql b/pipeline/transform/models/staging/stg_ees_admissions.sql index 3ca26aa..060604a 100644 --- a/pipeline/transform/models/staging/stg_ees_admissions.sql +++ b/pipeline/transform/models/staging/stg_ees_admissions.sql @@ -29,7 +29,25 @@ renamed as ( {{ safe_numeric('times_put_as_1st_preference') }}::integer as first_preference_applications, -- Proportions - {{ safe_numeric('proportion_1stprefs_v_totaloffers') }} as first_preference_offer_pct, + -- first_preference_offer_pct: of families who listed this school FIRST, + -- the percentage that received an offer. 0–100 scale. + -- The raw EES column `proportion_1stprefs_v_totaloffers` stores a + -- different, misleading figure (first-preference applications divided + -- by total offers, i.e. an oversubscription ratio) — do not use it. + case + when {{ safe_numeric('times_put_as_1st_preference') }} > 0 + then 100.0 + * {{ safe_numeric('number_1st_preference_offers') }} + / {{ safe_numeric('times_put_as_1st_preference') }} + end as first_preference_offer_pct, + + -- Oversubscription ratio: 1st-preference applications per place offered. + -- >1 means the school is oversubscribed on first preferences. + case + when {{ safe_numeric('total_number_places_offered') }} > 0 + then {{ safe_numeric('times_put_as_1st_preference') }}::numeric + / {{ safe_numeric('total_number_places_offered') }} + end as oversubscription_ratio, -- Derived: oversubscribed if 1st-preference applications > places offered -- Use already-cast columns to avoid repeating the regex expression