New ees_ks4_national stream ingests the EES 'National characteristics summary data' series (England, state-funded, all pupils). The old mart's unweighted school means were 7-15 points off every headline measure and produced an impossible national Progress 8 (-0.27). The API's computed fallback is gone too: the footnote calls these figures official, so an unbuilt mart now yields an empty series, never a stand-in. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0146VHeLAWjDVE2B5uU67jCB
293 lines
9.9 KiB
Python
293 lines
9.9 KiB
Python
"""
|
|
SQLAlchemy models — all tables live in the marts schema, built by dbt.
|
|
Read-only: the pipeline writes to these tables; the backend only reads.
|
|
"""
|
|
|
|
from sqlalchemy import Column, Integer, String, Float, Boolean, Date, Text, Index
|
|
|
|
from .database import Base
|
|
|
|
MARTS = {"schema": "marts"}
|
|
|
|
|
|
class DimSchool(Base):
|
|
"""Canonical school dimension — one row per active URN."""
|
|
__tablename__ = "dim_school"
|
|
__table_args__ = MARTS
|
|
|
|
urn = Column(Integer, primary_key=True)
|
|
school_name = Column(String(255), nullable=False)
|
|
phase_code = Column(Integer)
|
|
school_type_code = Column(Integer)
|
|
academy_trust_name = Column(String(255))
|
|
academy_trust_uid = Column(String(20))
|
|
religious_character_code = Column(Integer)
|
|
gender = Column(String(20))
|
|
age_range = Column(String(20))
|
|
has_sixth_form = Column(Boolean)
|
|
capacity = Column(Integer)
|
|
total_pupils = Column(Integer)
|
|
headteacher_name = Column(String(200))
|
|
website = Column(String(255))
|
|
telephone = Column(String(30))
|
|
status_code = Column(Integer)
|
|
nursery_provision = Column(Boolean)
|
|
admissions_policy_code = Column(Integer)
|
|
# Denormalised Ofsted summary (updated by monthly pipeline)
|
|
ofsted_grade = Column(Integer)
|
|
ofsted_date = Column(Date)
|
|
ofsted_framework = Column(String(20))
|
|
|
|
|
|
class DimLocation(Base):
|
|
"""School location — address, lat/lng from easting/northing (BNG→WGS84)."""
|
|
__tablename__ = "dim_location"
|
|
__table_args__ = MARTS
|
|
|
|
urn = Column(Integer, primary_key=True)
|
|
address_line1 = Column(String(255))
|
|
address_line2 = Column(String(255))
|
|
town = Column(String(100))
|
|
county = Column(String(100))
|
|
postcode = Column(String(20))
|
|
local_authority_code = Column(Integer)
|
|
local_authority_name = Column(String(100))
|
|
parliamentary_constituency = Column(String(100))
|
|
urban_rural = Column(String(50))
|
|
easting = Column(Integer)
|
|
northing = Column(Integer)
|
|
latitude = Column(Float)
|
|
longitude = Column(Float)
|
|
# geom is a PostGIS geometry — not mapped to SQLAlchemy (accessed via raw SQL)
|
|
|
|
|
|
class KS2Performance(Base):
|
|
"""KS2 attainment — one row per URN per year (includes predecessor data)."""
|
|
__tablename__ = "fact_ks2_performance"
|
|
__table_args__ = (
|
|
Index("ix_ks2_urn_year", "urn", "year"),
|
|
MARTS,
|
|
)
|
|
|
|
urn = Column(Integer, primary_key=True)
|
|
year = Column(Integer, primary_key=True)
|
|
source_urn = Column(Integer)
|
|
total_pupils = Column(Integer)
|
|
eligible_pupils = Column(Integer)
|
|
# Core attainment
|
|
rwm_expected_pct = Column(Float)
|
|
rwm_high_pct = Column(Float)
|
|
reading_expected_pct = Column(Float)
|
|
reading_high_pct = Column(Float)
|
|
reading_avg_score = Column(Float)
|
|
reading_progress = Column(Float)
|
|
writing_expected_pct = Column(Float)
|
|
writing_high_pct = Column(Float)
|
|
writing_progress = Column(Float)
|
|
maths_expected_pct = Column(Float)
|
|
maths_high_pct = Column(Float)
|
|
maths_avg_score = Column(Float)
|
|
maths_progress = Column(Float)
|
|
# Progress confidence intervals + writing working-towards (published
|
|
# for years with progress measures, i.e. up to 2022/23)
|
|
reading_progress_lower_ci = Column(Float)
|
|
reading_progress_upper_ci = Column(Float)
|
|
writing_progress_lower_ci = Column(Float)
|
|
writing_progress_upper_ci = Column(Float)
|
|
writing_working_towards_pct = Column(Float)
|
|
maths_progress_lower_ci = Column(Float)
|
|
maths_progress_upper_ci = Column(Float)
|
|
gps_expected_pct = Column(Float)
|
|
gps_high_pct = Column(Float)
|
|
gps_avg_score = Column(Float)
|
|
science_expected_pct = Column(Float)
|
|
# Absence
|
|
reading_absence_pct = Column(Float)
|
|
writing_absence_pct = Column(Float)
|
|
maths_absence_pct = Column(Float)
|
|
gps_absence_pct = Column(Float)
|
|
science_absence_pct = Column(Float)
|
|
# Gender
|
|
rwm_expected_boys_pct = Column(Float)
|
|
rwm_high_boys_pct = Column(Float)
|
|
rwm_expected_girls_pct = Column(Float)
|
|
rwm_high_girls_pct = Column(Float)
|
|
# Disadvantaged
|
|
rwm_expected_disadvantaged_pct = Column(Float)
|
|
rwm_expected_non_disadvantaged_pct = Column(Float)
|
|
disadvantaged_gap = Column(Float)
|
|
# Context
|
|
disadvantaged_pct = Column(Float)
|
|
eal_pct = Column(Float)
|
|
sen_support_pct = Column(Float)
|
|
sen_ehcp_pct = Column(Float)
|
|
stability_pct = Column(Float)
|
|
|
|
|
|
class FactOfstedInspection(Base):
|
|
"""Full Ofsted inspection history — one row per inspection."""
|
|
__tablename__ = "fact_ofsted_inspection"
|
|
__table_args__ = (
|
|
Index("ix_ofsted_urn_date", "urn", "inspection_date"),
|
|
MARTS,
|
|
)
|
|
|
|
urn = Column(Integer, primary_key=True)
|
|
inspection_date = Column(Date, primary_key=True)
|
|
inspection_type = Column(String(100))
|
|
framework = Column(String(20))
|
|
overall_effectiveness = Column(Integer)
|
|
quality_of_education = Column(Integer)
|
|
behaviour_attitudes = Column(Integer)
|
|
personal_development = Column(Integer)
|
|
leadership_management = Column(Integer)
|
|
early_years_provision = Column(Integer)
|
|
sixth_form_provision = Column(Integer)
|
|
# Ungraded (Section 8) inspection: raw outcome text and the grade parsed from
|
|
# it (fallback for schools with no graded overall effectiveness).
|
|
ungraded_outcome = Column(String(100))
|
|
ungraded_grade = Column(Integer)
|
|
rc_safeguarding_met = Column(Boolean)
|
|
rc_inclusion = Column(Integer)
|
|
rc_curriculum_teaching = Column(Integer)
|
|
rc_achievement = Column(Integer)
|
|
rc_attendance_behaviour = Column(Integer)
|
|
rc_personal_development = Column(Integer)
|
|
rc_leadership_governance = Column(Integer)
|
|
rc_early_years = Column(Integer)
|
|
rc_sixth_form = Column(Integer)
|
|
# Start date of the report-card inspection itself (renewed framework,
|
|
# Nov 2025+). Null for rows without report-card grades.
|
|
rc_inspection_date = Column(Date)
|
|
report_url = Column(Text)
|
|
|
|
|
|
class FactAdmissions(Base):
|
|
"""School admissions — one row per URN per year."""
|
|
__tablename__ = "fact_admissions"
|
|
__table_args__ = (
|
|
Index("ix_admissions_urn_year", "urn", "year"),
|
|
MARTS,
|
|
)
|
|
|
|
urn = Column(Integer, primary_key=True)
|
|
year = Column(Integer, primary_key=True)
|
|
school_phase = Column(String(50))
|
|
places_offered = Column(Integer)
|
|
total_applications = Column(Integer)
|
|
first_preference_applications = Column(Integer)
|
|
first_preference_offers = Column(Integer)
|
|
total_offers = Column(Integer)
|
|
second_preference_offers = Column(Integer)
|
|
third_preference_offers = Column(Integer)
|
|
cross_la_applications = Column(Integer)
|
|
cross_la_offers = Column(Integer)
|
|
first_preference_offer_pct = Column(Float)
|
|
oversubscription_ratio = Column(Float)
|
|
oversubscribed = Column(Boolean)
|
|
admissions_policy = Column(String(100))
|
|
|
|
|
|
class FactPupilCharacteristics(Base):
|
|
"""School pupil composition from EES census — one row per URN per year."""
|
|
__tablename__ = "fact_pupil_characteristics"
|
|
__table_args__ = (
|
|
Index("ix_pupil_chars_urn_year", "urn", "year"),
|
|
MARTS,
|
|
)
|
|
|
|
urn = Column(Integer, primary_key=True)
|
|
year = Column(Integer, primary_key=True)
|
|
phase_type_grouping = Column(String(50))
|
|
total_pupils = Column(Integer)
|
|
female_pupils = Column(Integer)
|
|
male_pupils = Column(Integer)
|
|
fsm_pct = Column(Float)
|
|
eal_pct = Column(Float)
|
|
|
|
|
|
class FactDeprivation(Base):
|
|
"""IDACI deprivation index — one row per URN."""
|
|
__tablename__ = "fact_deprivation"
|
|
__table_args__ = MARTS
|
|
|
|
urn = Column(Integer, primary_key=True)
|
|
lsoa_code = Column(String(20))
|
|
idaci_score = Column(Float)
|
|
idaci_decile = Column(Integer)
|
|
|
|
|
|
class FactFinance(Base):
|
|
"""FBIT financial benchmarking — one row per URN per year."""
|
|
__tablename__ = "fact_finance"
|
|
__table_args__ = (
|
|
Index("ix_finance_urn_year", "urn", "year"),
|
|
MARTS,
|
|
)
|
|
|
|
urn = Column(Integer, primary_key=True)
|
|
year = Column(Integer, primary_key=True)
|
|
per_pupil_spend = Column(Float)
|
|
staff_cost_pct = Column(Float)
|
|
teacher_cost_pct = Column(Float)
|
|
support_staff_cost_pct = Column(Float)
|
|
premises_cost_pct = Column(Float)
|
|
|
|
|
|
class CensusBenchmark(Base):
|
|
"""State-school context benchmarks from the pupil census — one row per phase.
|
|
|
|
fsm_pct / eal_pct are pupil-weighted means. Computed at import time;
|
|
consumers label them "state-school average (computed from our dataset)".
|
|
"""
|
|
__tablename__ = "fact_census_benchmarks"
|
|
__table_args__ = MARTS
|
|
|
|
phase = Column(String(20), primary_key=True)
|
|
year = Column(Integer)
|
|
fsm_pct = Column(Float)
|
|
eal_pct = Column(Float)
|
|
median_pupils = Column(Integer)
|
|
|
|
|
|
class Ks4NationalAverage(Base):
|
|
"""Official DfE KS4 national headline averages — one row per academic year.
|
|
|
|
gcse_grade_91_pct has no official national series and is always NULL.
|
|
"""
|
|
__tablename__ = "fact_ks4_national_averages"
|
|
__table_args__ = MARTS
|
|
|
|
year = Column(Integer, primary_key=True)
|
|
attainment_8_score = Column(Float)
|
|
progress_8_score = Column(Float)
|
|
english_maths_standard_pass_pct = Column(Float)
|
|
english_maths_strong_pass_pct = Column(Float)
|
|
ebacc_entry_pct = Column(Float)
|
|
ebacc_standard_pass_pct = Column(Float)
|
|
ebacc_strong_pass_pct = Column(Float)
|
|
ebacc_avg_score = Column(Float)
|
|
gcse_grade_91_pct = Column(Float)
|
|
|
|
|
|
class Ks2NationalAverage(Base):
|
|
"""Official DfE KS2 national headline averages — one row per academic year."""
|
|
__tablename__ = "fact_ks2_national_averages"
|
|
__table_args__ = MARTS
|
|
|
|
year = Column(Integer, primary_key=True)
|
|
rwm_expected_pct = Column(Float)
|
|
rwm_high_pct = Column(Float)
|
|
reading_expected_pct = Column(Float)
|
|
reading_high_pct = Column(Float)
|
|
reading_avg_score = Column(Float)
|
|
writing_expected_pct = Column(Float)
|
|
writing_gd_pct = Column(Float)
|
|
maths_expected_pct = Column(Float)
|
|
maths_high_pct = Column(Float)
|
|
maths_avg_score = Column(Float)
|
|
gps_expected_pct = Column(Float)
|
|
gps_high_pct = Column(Float)
|
|
gps_avg_score = Column(Float)
|
|
science_expected_pct = Column(Float)
|