feat(api): translate GIAS codes to names at the query boundary

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Tudor
2026-07-09 10:48:53 +01:00
co-authored by Claude Fable 5
parent fa6c929a3a
commit f1a013ec01
3 changed files with 88 additions and 10 deletions
+39 -5
View File
@@ -21,6 +21,38 @@ from .models import (
FactDeprivation, FactFinance, FactPupilCharacteristics, FactDeprivation, FactFinance, FactPupilCharacteristics,
) )
from .schemas import SCHOOL_TYPE_MAP 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]] = {} _postcode_cache: Dict[str, Tuple[float, float]] = {}
_typesense_client = None _typesense_client = None
@@ -121,16 +153,16 @@ _MAIN_QUERY = text("""
SELECT SELECT
s.urn, s.urn,
s.school_name, s.school_name,
s.phase, s.phase_code,
s.school_type, s.school_type_code,
s.academy_trust_name AS trust_name, s.academy_trust_name AS trust_name,
s.academy_trust_uid AS trust_uid, s.academy_trust_uid AS trust_uid,
s.religious_character AS religious_denomination, s.religious_character_code,
s.gender, s.gender,
s.age_range, s.age_range,
s.has_sixth_form, s.has_sixth_form,
s.status, s.status_code,
s.admissions_policy, s.admissions_policy_code,
s.capacity, s.capacity,
s.total_pupils AS gias_total_pupils, s.total_pupils AS gias_total_pupils,
s.headteacher_name, s.headteacher_name,
@@ -256,6 +288,8 @@ def load_school_data_as_dataframe() -> pd.DataFrame:
if df.empty: if df.empty:
return df return df
df = translate_gias_code_columns(df)
# Build address string # Build address string
df["address"] = df.apply( df["address"] = df.apply(
lambda r: ", ".join( lambda r: ", ".join(
+5 -5
View File
@@ -17,11 +17,11 @@ class DimSchool(Base):
urn = Column(Integer, primary_key=True) urn = Column(Integer, primary_key=True)
school_name = Column(String(255), nullable=False) school_name = Column(String(255), nullable=False)
phase = Column(String(100)) phase_code = Column(Integer)
school_type = Column(String(100)) school_type_code = Column(Integer)
academy_trust_name = Column(String(255)) academy_trust_name = Column(String(255))
academy_trust_uid = Column(String(20)) academy_trust_uid = Column(String(20))
religious_character = Column(String(100)) religious_character_code = Column(Integer)
gender = Column(String(20)) gender = Column(String(20))
age_range = Column(String(20)) age_range = Column(String(20))
has_sixth_form = Column(Boolean) has_sixth_form = Column(Boolean)
@@ -30,9 +30,9 @@ class DimSchool(Base):
headteacher_name = Column(String(200)) headteacher_name = Column(String(200))
website = Column(String(255)) website = Column(String(255))
telephone = Column(String(30)) telephone = Column(String(30))
status = Column(String(50)) status_code = Column(Integer)
nursery_provision = Column(Boolean) nursery_provision = Column(Boolean)
admissions_policy = Column(String(50)) admissions_policy_code = Column(Integer)
# Denormalised Ofsted summary (updated by monthly pipeline) # Denormalised Ofsted summary (updated by monthly pipeline)
ofsted_grade = Column(Integer) ofsted_grade = Column(Integer)
ofsted_date = Column(Date) ofsted_date = Column(Date)
+44
View File
@@ -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"