feat: GIAS code->name dictionaries generated from live bulk CSV
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
"""GIAS code -> name dictionaries.
|
||||
|
||||
GENERATED by pipeline/scripts/generate_gias_codes.py from the GIAS bulk CSV
|
||||
— do not edit by hand; rerun the script when the dbt drift test warns.
|
||||
The canonical file is backend/gias_codes.py; pipeline/scripts/gias_codes.py
|
||||
must be byte-identical (enforced by backend/tests/test_gias_codes.py).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import math
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
SCHOOL_TYPE: dict[int, str] = {
|
||||
1: "Community school",
|
||||
2: "Voluntary aided school",
|
||||
3: "Voluntary controlled school",
|
||||
5: "Foundation school",
|
||||
6: "City technology college",
|
||||
7: "Community special school",
|
||||
8: "Non-maintained special school",
|
||||
10: "Other independent special school",
|
||||
11: "Other independent school",
|
||||
12: "Foundation special school",
|
||||
14: "Pupil referral unit",
|
||||
15: "Local authority nursery school",
|
||||
18: "Further education",
|
||||
24: "Secure units",
|
||||
25: "Offshore schools",
|
||||
26: "Service children's education",
|
||||
27: "Miscellaneous",
|
||||
28: "Academy sponsor led",
|
||||
29: "Higher education institutions",
|
||||
30: "Welsh establishment",
|
||||
31: "Sixth form centres",
|
||||
32: "Special post 16 institution",
|
||||
33: "Academy special sponsor led",
|
||||
34: "Academy converter",
|
||||
35: "Free schools",
|
||||
36: "Free schools special",
|
||||
37: "British schools overseas",
|
||||
38: "Free schools alternative provision",
|
||||
39: "Free schools 16 to 19",
|
||||
40: "University technical college",
|
||||
41: "Studio schools",
|
||||
42: "Academy alternative provision converter",
|
||||
43: "Academy alternative provision sponsor led",
|
||||
44: "Academy special converter",
|
||||
45: "Academy 16-19 converter",
|
||||
46: "Academy 16 to 19 sponsor led",
|
||||
49: "Online provider",
|
||||
56: "Institution funded by other government department",
|
||||
57: "Academy secure 16 to 19",
|
||||
}
|
||||
|
||||
ESTABLISHMENT_STATUS: dict[int, str] = {
|
||||
1: "Open",
|
||||
2: "Closed",
|
||||
3: "Open, but proposed to close",
|
||||
4: "Proposed to open",
|
||||
}
|
||||
|
||||
PHASE_OF_EDUCATION: dict[int, str] = {
|
||||
0: "Not applicable",
|
||||
1: "Nursery",
|
||||
2: "Primary",
|
||||
3: "Middle deemed primary",
|
||||
4: "Secondary",
|
||||
5: "Middle deemed secondary",
|
||||
6: "16 plus",
|
||||
7: "All-through",
|
||||
}
|
||||
|
||||
OFFICIAL_SIXTH_FORM: dict[int, str] = {
|
||||
0: "Not applicable",
|
||||
1: "Has a sixth form",
|
||||
2: "Does not have a sixth form",
|
||||
}
|
||||
|
||||
RELIGIOUS_CHARACTER: dict[int, str] = {
|
||||
0: "Does not apply",
|
||||
2: "Church of England",
|
||||
3: "Roman Catholic",
|
||||
4: "Methodist",
|
||||
5: "Jewish",
|
||||
6: "None",
|
||||
7: "Muslim",
|
||||
8: "Seventh Day Adventist",
|
||||
9: "Church of England/Methodist",
|
||||
10: "Methodist/Church of England",
|
||||
11: "Church of England/Roman Catholic",
|
||||
12: "Church of England/United Reformed Church",
|
||||
13: "Roman Catholic/Church of England",
|
||||
14: "Quaker",
|
||||
15: "Christian",
|
||||
16: "United Reformed Church",
|
||||
17: "Congregational Church",
|
||||
18: "Free Church",
|
||||
19: "Church of England/Free Church",
|
||||
20: "Church of England/Christian",
|
||||
21: "Sikh",
|
||||
22: "Greek Orthodox",
|
||||
24: "Buddhist",
|
||||
25: "Hindu",
|
||||
26: "Moravian",
|
||||
28: "Inter- / non- denominational",
|
||||
29: "Multi-faith",
|
||||
30: "Church of England/Methodist/United Reform Church/Baptist",
|
||||
31: "Anglican",
|
||||
32: "Anglican/Christian",
|
||||
33: "Anglican/Evangelical",
|
||||
34: "Anglican/Church of England",
|
||||
35: "Catholic",
|
||||
36: "Charadi Jewish",
|
||||
37: "Christian/Evangelical",
|
||||
38: "Christian Science",
|
||||
39: "Christian/Methodist",
|
||||
40: "Christian/non-denominational",
|
||||
41: "Church of England/Evangelical",
|
||||
42: "Islam",
|
||||
43: "Orthodox Jewish",
|
||||
44: "Plymouth Brethren Christian Church",
|
||||
45: "Protestant",
|
||||
46: "Protestant/Evangelical",
|
||||
47: "Reformed Baptist",
|
||||
48: "Roman Catholic/Anglican",
|
||||
49: "Sunni Deobandi",
|
||||
}
|
||||
|
||||
ADMISSIONS_POLICY: dict[int, str] = {
|
||||
0: "Not applicable",
|
||||
2: "Selective",
|
||||
4: "Non-selective",
|
||||
}
|
||||
|
||||
|
||||
def translate(code, mapping: dict[int, str]) -> str | None:
|
||||
"""Translate a GIAS code to its display name.
|
||||
|
||||
None/NaN -> None (column absent or suppressed). Unknown codes degrade to
|
||||
"Unknown (<code>)" with a warning so a new DfE value never blanks the UI.
|
||||
"""
|
||||
if code is None or (isinstance(code, float) and math.isnan(code)):
|
||||
return None
|
||||
code = int(code)
|
||||
if code not in mapping:
|
||||
logger.warning("Unknown GIAS code %s (not in dictionary)", code)
|
||||
return f"Unknown ({code})"
|
||||
return mapping[code]
|
||||
@@ -0,0 +1,83 @@
|
||||
"""Tests for the GIAS code->name dictionaries (spec 2026-07-09).
|
||||
|
||||
The dictionaries are generated from the live GIAS bulk CSV by
|
||||
pipeline/scripts/generate_gias_codes.py — these tests assert the module's
|
||||
contract, key sentinel values the marts/UI depend on, and that the pipeline
|
||||
copy has not drifted from the canonical backend module.
|
||||
"""
|
||||
|
||||
import math
|
||||
from pathlib import Path
|
||||
|
||||
from backend.gias_codes import (
|
||||
ADMISSIONS_POLICY,
|
||||
ESTABLISHMENT_STATUS,
|
||||
OFFICIAL_SIXTH_FORM,
|
||||
PHASE_OF_EDUCATION,
|
||||
RELIGIOUS_CHARACTER,
|
||||
SCHOOL_TYPE,
|
||||
translate,
|
||||
)
|
||||
|
||||
REPO = Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
def test_translate_known_code():
|
||||
open_code = next(c for c, n in ESTABLISHMENT_STATUS.items() if n == "Open")
|
||||
assert translate(open_code, ESTABLISHMENT_STATUS) == "Open"
|
||||
|
||||
|
||||
def test_translate_unknown_code_degrades_gracefully():
|
||||
assert translate(9999, ESTABLISHMENT_STATUS) == "Unknown (9999)"
|
||||
|
||||
|
||||
def test_translate_none_and_nan_return_none():
|
||||
assert translate(None, ESTABLISHMENT_STATUS) is None
|
||||
assert translate(float("nan"), ESTABLISHMENT_STATUS) is None
|
||||
|
||||
|
||||
def test_translate_accepts_float_codes():
|
||||
# pd.read_sql yields float columns when NULLs are present
|
||||
open_code = next(c for c, n in ESTABLISHMENT_STATUS.items() if n == "Open")
|
||||
assert translate(float(open_code), ESTABLISHMENT_STATUS) == "Open"
|
||||
|
||||
|
||||
def test_sentinel_names_present():
|
||||
"""Names the marts/UI compare against must exist verbatim."""
|
||||
assert "Open" in ESTABLISHMENT_STATUS.values()
|
||||
assert "Open, but proposed to close" in ESTABLISHMENT_STATUS.values()
|
||||
assert "Has a sixth form" in OFFICIAL_SIXTH_FORM.values()
|
||||
assert "Primary" in PHASE_OF_EDUCATION.values()
|
||||
assert "Secondary" in PHASE_OF_EDUCATION.values()
|
||||
assert "Does not apply" in RELIGIOUS_CHARACTER.values()
|
||||
assert all(len(d) > 0 for d in (
|
||||
SCHOOL_TYPE, ESTABLISHMENT_STATUS, PHASE_OF_EDUCATION,
|
||||
OFFICIAL_SIXTH_FORM, RELIGIOUS_CHARACTER, ADMISSIONS_POLICY,
|
||||
))
|
||||
|
||||
|
||||
def test_pipeline_copy_is_identical():
|
||||
canonical = (REPO / "backend" / "gias_codes.py").read_text()
|
||||
copy = (REPO / "pipeline" / "scripts" / "gias_codes.py").read_text()
|
||||
assert canonical == copy, (
|
||||
"pipeline/scripts/gias_codes.py has drifted from backend/gias_codes.py — "
|
||||
"regenerate with pipeline/scripts/generate_gias_codes.py and copy the file"
|
||||
)
|
||||
|
||||
|
||||
def test_seed_matches_dictionaries():
|
||||
import csv
|
||||
fields = {
|
||||
"school_type": SCHOOL_TYPE,
|
||||
"establishment_status": ESTABLISHMENT_STATUS,
|
||||
"phase_of_education": PHASE_OF_EDUCATION,
|
||||
"official_sixth_form": OFFICIAL_SIXTH_FORM,
|
||||
"religious_character": RELIGIOUS_CHARACTER,
|
||||
"admissions_policy": ADMISSIONS_POLICY,
|
||||
}
|
||||
seed_path = REPO / "pipeline" / "transform" / "seeds" / "gias_code_names.csv"
|
||||
seed: dict[str, dict[int, str]] = {k: {} for k in fields}
|
||||
with open(seed_path, newline="") as fh:
|
||||
for row in csv.DictReader(fh):
|
||||
seed[row["field"]][int(row["code"])] = row["name"]
|
||||
assert seed == fields
|
||||
Reference in New Issue
Block a user