Files
school_compare/backend/school_groups.py
T

120 lines
5.4 KiB
Python
Raw Normal View History

"""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), ())