PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m13s
PR Checks / Backend Smoke (pull_request) Successful in 9s
PR Checks / Build Backend (no push) (pull_request) Successful in 19s
PR Checks / Build Frontend (no push) (pull_request) Successful in 1m21s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 11s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 20s
The School type filter split state schools into "academy or free school" and "council-run". The two were near-halves of one pool (11,186 and 9,316 schools), so choosing one rarely narrowed anything, and the split did not follow the difference a parent feels most, admissions: voluntary aided and foundation schools set their own, as academies do. Faith, which voluntary aided mostly meant, has its own filter. They are now one group, "State school (free)", leaving five. The old keys academy and council resolve to state, so a link made with them keeps working instead of falling through to the raw-label path and returning nothing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
121 lines
5.4 KiB
Python
121 lines
5.4 KiB
Python
"""Parent-facing groups over GIAS establishment types and religious characters.
|
|
|
|
The 34 GIAS establishment types describe governance and funding, which for a
|
|
mainstream state school barely changes what a parent experiences. The search
|
|
filter offers five groups a parent recognises instead, and a faith filter in
|
|
place of the faith signal that "Voluntary aided" and "Voluntary controlled"
|
|
only half carry. See
|
|
docs/superpowers/specs/2026-10-02-school-type-groups-and-faith-filter-design.md.
|
|
|
|
Groups are defined over GIAS codes and looked up by the translated name,
|
|
because the DataFrame the API filters carries names only: the codes are
|
|
replaced at load (data_loader.translate_gias_code_columns), the legacy-name
|
|
mart fallback never had them, and test fixtures are written in names.
|
|
"""
|
|
|
|
from typing import Optional
|
|
|
|
import pandas as pd
|
|
|
|
from .gias_codes import RELIGIOUS_CHARACTER, SCHOOL_TYPE
|
|
|
|
# (key, label, GIAS TypeOfEstablishment codes), in the order shown.
|
|
TYPE_GROUPS: tuple[tuple[str, str, frozenset[int]], ...] = (
|
|
# Every mainstream state school, academy or council-run: Academy sponsor
|
|
# led, Academy converter, Free schools, University technical college,
|
|
# Studio schools, City technology college, Community, Voluntary aided,
|
|
# Voluntary controlled, Foundation, LA nursery. Academy against council-run
|
|
# was two near-halves of one pool, and did not follow the difference a
|
|
# parent feels most, admissions: voluntary aided and foundation schools
|
|
# set their own, as academies do. Faith, which voluntary aided mostly
|
|
# meant, has its own filter.
|
|
("state", "State school (free)",
|
|
frozenset({28, 34, 35, 40, 41, 6, 1, 2, 3, 5, 15})),
|
|
("independent", "Independent (fee-paying)", frozenset({11})),
|
|
# Every special type, independent ones included (usually funded by the
|
|
# council through an EHCP, so SEND provision to a parent, not private
|
|
# school), and Special post 16 institutions.
|
|
("special", "Special school (SEND)", frozenset({7, 8, 10, 12, 32, 33, 36, 44})),
|
|
# Further education, Sixth form centres, the 16-19 academies and free schools.
|
|
("post16", "Sixth form or college", frozenset({18, 31, 39, 45, 46})),
|
|
# Pupil referral units and AP academies and free schools. Last: parents do
|
|
# not apply to these; the local authority places children there.
|
|
("alternative", "Alternative provision", frozenset({14, 38, 42, 43})),
|
|
)
|
|
|
|
# In no group, so reachable only under "Any school type": Secure units,
|
|
# Miscellaneous, Higher education institutions, Online provider, Institution
|
|
# funded by other government department, Academy secure 16 to 19.
|
|
UNOFFERED_TYPE_CODES: frozenset[int] = frozenset({24, 27, 29, 49, 56, 57})
|
|
|
|
# (key, label, GIAS ReligiousCharacter codes), in the order shown. A joint
|
|
# school is in every faith its label names; a generic "Christian" beside a
|
|
# named church adds nothing.
|
|
FAITH_GROUPS: tuple[tuple[str, str, frozenset[int]], ...] = (
|
|
# Does not apply, None, and 99 (a blank label).
|
|
("none", "No religious character", frozenset({0, 6, 99})),
|
|
("church_of_england", "Church of England",
|
|
frozenset({2, 9, 10, 11, 12, 13, 19, 20, 30, 31, 32, 33, 34, 41, 48})),
|
|
("roman_catholic", "Roman Catholic", frozenset({3, 11, 13, 35, 48})),
|
|
# 28 "Inter- / non- denominational" is how GIAS files Christian schools
|
|
# tied to no one church.
|
|
("other_christian", "Other Christian",
|
|
frozenset({4, 8, 9, 10, 12, 14, 15, 16, 17, 18, 19, 22, 26, 28, 30, 33,
|
|
37, 38, 39, 40, 41, 44, 45, 46, 47})),
|
|
("jewish", "Jewish", frozenset({5, 36, 43})),
|
|
("muslim", "Muslim", frozenset({7, 42, 49})),
|
|
("other_faith", "Other faith", frozenset({21, 24, 25, 29})),
|
|
)
|
|
|
|
TYPE_GROUP_KEYS: frozenset[str] = frozenset(k for k, _, _ in TYPE_GROUPS)
|
|
|
|
# Keys a group was offered under before, so their links keep working: "state"
|
|
# was "academy" and "council" until 2026-10-02.
|
|
TYPE_GROUP_ALIASES: dict[str, str] = {"academy": "state", "council": "state"}
|
|
FAITH_KEYS: frozenset[str] = frozenset(k for k, _, _ in FAITH_GROUPS)
|
|
|
|
|
|
def _key(name: str) -> str:
|
|
return name.strip().lower()
|
|
|
|
|
|
_TYPE_GROUP_BY_NAME: dict[str, str] = {
|
|
_key(SCHOOL_TYPE[code]): key
|
|
for key, _, codes in TYPE_GROUPS
|
|
for code in codes
|
|
if code in SCHOOL_TYPE
|
|
}
|
|
|
|
_FAITHS_BY_NAME: dict[str, tuple[str, ...]] = {}
|
|
for _faith, _, _codes in FAITH_GROUPS:
|
|
for _code in sorted(_codes):
|
|
if _code in RELIGIOUS_CHARACTER:
|
|
_name = _key(RELIGIOUS_CHARACTER[_code])
|
|
_FAITHS_BY_NAME[_name] = _FAITHS_BY_NAME.get(_name, ()) + (_faith,)
|
|
|
|
|
|
def type_group_key(value: str) -> Optional[str]:
|
|
"""The type group a school_type URL value names, old keys included, or
|
|
None when it names no group (an old link's raw GIAS type)."""
|
|
v = value.strip().lower()
|
|
v = TYPE_GROUP_ALIASES.get(v, v)
|
|
return v if v in TYPE_GROUP_KEYS else None
|
|
|
|
|
|
def type_group_for(name: object) -> Optional[str]:
|
|
"""The type group of a GIAS establishment type name, or None."""
|
|
if not isinstance(name, str):
|
|
return None
|
|
return _TYPE_GROUP_BY_NAME.get(_key(name))
|
|
|
|
|
|
def faith_groups_for(name: object) -> tuple[str, ...]:
|
|
"""The faith groups of a GIAS religious character name.
|
|
|
|
A missing or blank name is "No religious character". A name the
|
|
dictionary does not know has no faith, so it matches no faith option.
|
|
"""
|
|
if not isinstance(name, str):
|
|
return ("none",) if name is None or pd.isna(name) else ()
|
|
return _FAITHS_BY_NAME.get(_key(name), ())
|