feat(places): name every authority a place sits in
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m2s
PR Checks / Backend Smoke (pull_request) Successful in 8s
PR Checks / Build Backend (no push) (pull_request) Successful in 18s
PR Checks / Build Frontend (no push) (pull_request) Successful in 44s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 10s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 2m33s
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m2s
PR Checks / Backend Smoke (pull_request) Successful in 8s
PR Checks / Build Backend (no push) (pull_request) Successful in 18s
PR Checks / Build Frontend (no push) (pull_request) Successful in 44s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 10s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 2m33s
SW19 is mostly Merton but partly Wandsworth, and the page said only Merton. The cause was one field doing two jobs: _parent_authority takes the modal authority, which is right for a 301 target and wrong as a statement about where a place is. This is not a corner case. A quarter of viable outcodes (425 of 1,760) and a third of viable towns (263 of 783) cross an authority boundary — Bedford the town spans Bedford and Central Bedfordshire. Place now carries `authorities`, every authority holding at least a tenth of the schools and at least two of them, largest first. parent_authority stays single and unchanged, because a redirect still needs one target. The share threshold exists because GIAS carries postcode errors: EN6 lists two Shropshire schools among fourteen in Hertfordshire, and a bare "any authority present" rule would print those as though they were real. A place too small or too fragmented to clear the threshold still names its largest, so the page never goes silent about where it is. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015mWQnpye9F299NVRCCSRvj
This commit is contained in:
1 parent
9545aec7f4
commit
93a0a3c26f
7 files changed
+214
-6
No files matched your search
@@ -30,6 +30,12 @@ class Place:
|
||||
name: str
|
||||
urns: tuple[int, ...]
|
||||
parent_authority: str | None # authority NAME, for the 301 target
|
||||
# Every authority the place meaningfully sits in, largest first. A quarter
|
||||
# of outcodes and a third of towns straddle a boundary — SW19 is mostly
|
||||
# Merton but partly Wandsworth — so naming only one asserts something
|
||||
# false. parent_authority stays single because a redirect needs one
|
||||
# target; this is what the page shows.
|
||||
authorities: tuple[tuple[str, int], ...] = ()
|
||||
# URNs per phase, so the per-phase threshold can be applied without
|
||||
# re-querying. A place with 30 primaries and 2 secondaries publishes a
|
||||
# primary variant and no secondary one.
|
||||
@@ -71,6 +77,42 @@ def _phase_urns(group, publishable: set[int]) -> dict[str, tuple[int, ...]]:
|
||||
return out
|
||||
|
||||
|
||||
# A place is described by an authority when it holds at least a tenth of the
|
||||
# schools, and at least two. GIAS carries occasional postcode errors — EN6
|
||||
# lists two Shropshire schools among fourteen in Hertfordshire — and a bare
|
||||
# "any authority present" rule would print those as though they were real.
|
||||
_AUTHORITY_MIN_SHARE = 0.10
|
||||
_AUTHORITY_MIN_SCHOOLS = 2
|
||||
_AUTHORITY_MAX_SHOWN = 3
|
||||
|
||||
|
||||
def _authorities(group) -> tuple[tuple[str, int], ...]:
|
||||
"""Authorities this place meaningfully sits in, largest first."""
|
||||
from backend.app import EXCLUDED_FILTER_VALUES
|
||||
|
||||
if "local_authority" not in group.columns:
|
||||
return ()
|
||||
counts = group["local_authority"].dropna().value_counts()
|
||||
total = int(counts.sum())
|
||||
if not total:
|
||||
return ()
|
||||
|
||||
kept = [
|
||||
(str(name), int(n)) for name, n in counts.items()
|
||||
if str(name) not in EXCLUDED_FILTER_VALUES
|
||||
and n >= _AUTHORITY_MIN_SCHOOLS
|
||||
and n / total >= _AUTHORITY_MIN_SHARE
|
||||
]
|
||||
# A place too small or too fragmented for the share rule still names its
|
||||
# largest authority, or the page would say nothing about where it is.
|
||||
if not kept:
|
||||
for name, n in counts.items():
|
||||
if str(name) not in EXCLUDED_FILTER_VALUES:
|
||||
return ((str(name), int(n)),)
|
||||
return ()
|
||||
return tuple(kept[:_AUTHORITY_MAX_SHOWN])
|
||||
|
||||
|
||||
def _parent_authority(group) -> str | None:
|
||||
"""The most common authority in a group — the useful 301 target.
|
||||
|
||||
@@ -104,6 +146,7 @@ def _group(df, column: str, kind: str, publishable: set[int]) -> dict[str, Place
|
||||
place = Place(
|
||||
kind=kind, slug=slug, name=name, urns=urns,
|
||||
parent_authority=_parent_authority(group) if kind == "town" else None,
|
||||
authorities=() if kind == "authority" else _authorities(group),
|
||||
phase_urns=_phase_urns(group, publishable),
|
||||
)
|
||||
out[place.key] = place
|
||||
@@ -138,6 +181,7 @@ def _outcode_places(df, publishable: set[int]) -> dict[str, Place]:
|
||||
continue
|
||||
place = Place(kind="outcode", slug=str(oc).lower(), name=str(oc),
|
||||
urns=urns, parent_authority=_parent_authority(group),
|
||||
authorities=_authorities(group),
|
||||
phase_urns=_phase_urns(group, publishable))
|
||||
out[place.key] = place
|
||||
return out
|
||||
@@ -181,6 +225,7 @@ def _locality_places(df, publishable: set[int],
|
||||
continue
|
||||
place = Place(kind="locality", slug=slug, name=name, urns=urns,
|
||||
parent_authority=_parent_authority(group),
|
||||
authorities=_authorities(group),
|
||||
phase_urns=_phase_urns(group, publishable))
|
||||
out[place.key] = place
|
||||
return out
|
||||
|
||||
Reference in new issue
Block a user