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