feat: GIAS classification fields stored as codes, translated in code #24
+39
-5
@@ -21,6 +21,38 @@ from .models import (
|
||||
FactDeprivation, FactFinance, FactPupilCharacteristics,
|
||||
)
|
||||
from .schemas import SCHOOL_TYPE_MAP
|
||||
from .gias_codes import (
|
||||
ADMISSIONS_POLICY,
|
||||
ESTABLISHMENT_STATUS,
|
||||
PHASE_OF_EDUCATION,
|
||||
RELIGIOUS_CHARACTER,
|
||||
SCHOOL_TYPE,
|
||||
translate,
|
||||
)
|
||||
|
||||
# mart code column -> (API name column, dictionary)
|
||||
_GIAS_CODE_COLUMNS = {
|
||||
"phase_code": ("phase", PHASE_OF_EDUCATION),
|
||||
"school_type_code": ("school_type", SCHOOL_TYPE),
|
||||
"status_code": ("status", ESTABLISHMENT_STATUS),
|
||||
"religious_character_code": ("religious_denomination", RELIGIOUS_CHARACTER),
|
||||
"admissions_policy_code": ("admissions_policy", ADMISSIONS_POLICY),
|
||||
}
|
||||
|
||||
|
||||
def translate_gias_code_columns(df: pd.DataFrame) -> pd.DataFrame:
|
||||
"""Map GIAS code columns to today's name columns (API contract).
|
||||
|
||||
Runs immediately after pd.read_sql so every downstream consumer —
|
||||
filters, PHASE_GROUPS, payloads, /api/filters — keeps seeing names.
|
||||
DataFrames without the code columns (old schema, test fixtures) pass
|
||||
through unchanged.
|
||||
"""
|
||||
for code_col, (name_col, mapping) in _GIAS_CODE_COLUMNS.items():
|
||||
if code_col in df.columns:
|
||||
df[name_col] = df[code_col].map(lambda c: translate(c, mapping))
|
||||
return df
|
||||
|
||||
|
||||
_postcode_cache: Dict[str, Tuple[float, float]] = {}
|
||||
_typesense_client = None
|
||||
@@ -121,16 +153,16 @@ _MAIN_QUERY = text("""
|
||||
SELECT
|
||||
s.urn,
|
||||
s.school_name,
|
||||
s.phase,
|
||||
s.school_type,
|
||||
s.phase_code,
|
||||
s.school_type_code,
|
||||
s.academy_trust_name AS trust_name,
|
||||
s.academy_trust_uid AS trust_uid,
|
||||
s.religious_character AS religious_denomination,
|
||||
s.religious_character_code,
|
||||
s.gender,
|
||||
s.age_range,
|
||||
s.has_sixth_form,
|
||||
s.status,
|
||||
s.admissions_policy,
|
||||
s.status_code,
|
||||
s.admissions_policy_code,
|
||||
s.capacity,
|
||||
s.total_pupils AS gias_total_pupils,
|
||||
s.headteacher_name,
|
||||
@@ -256,6 +288,8 @@ def load_school_data_as_dataframe() -> pd.DataFrame:
|
||||
if df.empty:
|
||||
return df
|
||||
|
||||
df = translate_gias_code_columns(df)
|
||||
|
||||
# Build address string
|
||||
df["address"] = df.apply(
|
||||
lambda r: ", ".join(
|
||||
|
||||
+5
-5
@@ -17,11 +17,11 @@ class DimSchool(Base):
|
||||
|
||||
urn = Column(Integer, primary_key=True)
|
||||
school_name = Column(String(255), nullable=False)
|
||||
phase = Column(String(100))
|
||||
school_type = Column(String(100))
|
||||
phase_code = Column(Integer)
|
||||
school_type_code = Column(Integer)
|
||||
academy_trust_name = Column(String(255))
|
||||
academy_trust_uid = Column(String(20))
|
||||
religious_character = Column(String(100))
|
||||
religious_character_code = Column(Integer)
|
||||
gender = Column(String(20))
|
||||
age_range = Column(String(20))
|
||||
has_sixth_form = Column(Boolean)
|
||||
@@ -30,9 +30,9 @@ class DimSchool(Base):
|
||||
headteacher_name = Column(String(200))
|
||||
website = Column(String(255))
|
||||
telephone = Column(String(30))
|
||||
status = Column(String(50))
|
||||
status_code = Column(Integer)
|
||||
nursery_provision = Column(Boolean)
|
||||
admissions_policy = Column(String(50))
|
||||
admissions_policy_code = Column(Integer)
|
||||
# Denormalised Ofsted summary (updated by monthly pipeline)
|
||||
ofsted_grade = Column(Integer)
|
||||
ofsted_date = Column(Date)
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
"""API-boundary translation: marts now carry GIAS codes; the DataFrame the
|
||||
rest of the backend sees must carry today's name strings."""
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from backend.data_loader import translate_gias_code_columns
|
||||
from backend.gias_codes import ESTABLISHMENT_STATUS, PHASE_OF_EDUCATION
|
||||
|
||||
|
||||
def _code_for(mapping, name):
|
||||
return next(c for c, n in mapping.items() if n == name)
|
||||
|
||||
|
||||
def test_codes_become_todays_names():
|
||||
df = pd.DataFrame([{
|
||||
"urn": 1,
|
||||
"phase_code": float(_code_for(PHASE_OF_EDUCATION, "Primary")),
|
||||
"school_type_code": np.nan,
|
||||
"status_code": float(_code_for(ESTABLISHMENT_STATUS, "Open, but proposed to close")),
|
||||
"religious_character_code": np.nan,
|
||||
"admissions_policy_code": np.nan,
|
||||
}])
|
||||
out = translate_gias_code_columns(df)
|
||||
row = out.iloc[0]
|
||||
assert row["phase"] == "Primary"
|
||||
assert row["status"] == "Open, but proposed to close"
|
||||
assert row["school_type"] is None
|
||||
assert row["religious_denomination"] is None
|
||||
assert row["admissions_policy"] is None
|
||||
|
||||
|
||||
def test_unknown_code_degrades_not_blanks():
|
||||
df = pd.DataFrame([{"urn": 1, "phase_code": 9999.0}])
|
||||
out = translate_gias_code_columns(df)
|
||||
assert out.iloc[0]["phase"] == "Unknown (9999)"
|
||||
|
||||
|
||||
def test_missing_code_columns_are_a_noop():
|
||||
"""Old-schema DataFrames (tests, pre-pipeline DBs) pass through untouched."""
|
||||
df = pd.DataFrame([{"urn": 1, "phase": "Primary", "status": "Open"}])
|
||||
out = translate_gias_code_columns(df)
|
||||
assert out.iloc[0]["phase"] == "Primary"
|
||||
assert out.iloc[0]["status"] == "Open"
|
||||
Reference in New Issue
Block a user