Model/tests/harness/test_epc_prediction_corpus.py
Khalim Conn-Kowlessar fdc314c857 feat(epc-prediction): thread coordinates onto Comparable + target (#1227)
Adds coordinates: Optional[Coordinates] to Comparable and PredictionTarget
(data carriers — the pure predictor stays IO-free), and wires load_corpus to
read an optional _coordinates.json sidecar ({uprn: [lon, lat]}) and populate
each Comparable from its cert's uprn; iter_predictions threads the held-out
target's coordinates through. Absent sidecar -> geo-weighting stays off (no
behaviour change yet — weighting lands next slice). fetch_corpus_coordinates
now writes the sidecar into the corpus dir. load_corpus populates 99% of
corpus comparables.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 14:46:01 +00:00

96 lines
3 KiB
Python

"""Behaviour of the EPC Prediction corpus anonymiser (ADR-0030): de-identify a
cert payload for the committed fixture without disturbing the component data the
scorer reads. Pure dict-in / dict-out.
"""
import json
from pathlib import Path
from domain.geospatial.coordinates import Coordinates
from harness.epc_prediction_corpus import load_coordinates, anonymise_payload
def _payload() -> dict[str, object]:
return {
"address_line_1": "12 Acacia Avenue",
"address_line_2": "Hyde Park",
"post_town": "LEEDS",
"certificate_number": "1234-5678-9012-3456-7890",
"postcode": "LS6 1AA",
"sap_version": 10.2,
"energy_rating_current": 72,
"sap_building_parts": [{"wall_construction": 1}],
}
def test_hashes_identifiers_and_blanks_address_lines() -> None:
# Arrange
raw = _payload()
# Act
anon = anonymise_payload(raw)
# Assert — the street address + cert number are replaced by opaque tokens,
# the secondary address lines blanked.
assert anon["address_line_1"] != "12 Acacia Avenue"
assert str(anon["address_line_1"]).startswith("addr-")
assert str(anon["certificate_number"]).startswith("cert-")
assert anon["address_line_2"] == ""
assert anon["post_town"] == ""
def test_keeps_postcode_and_component_fields() -> None:
# Arrange
raw = _payload()
# Act
anon = anonymise_payload(raw)
# Assert — postcode (coarse, the cohort key) and all component / lodged data
# survive untouched.
assert anon["postcode"] == "LS6 1AA"
assert anon["sap_version"] == 10.2
assert anon["energy_rating_current"] == 72
assert anon["sap_building_parts"] == [{"wall_construction": 1}]
def test_address_hash_is_stable_for_dedup() -> None:
# Arrange — the same address re-lodged with trivial whitespace/case noise.
a = anonymise_payload({"address_line_1": "Flat 3"})
b = anonymise_payload({"address_line_1": " FLAT 3 "})
# Act / Assert — both normalise to the same token, so the dedup-by-address
# leave-one-out still collapses re-lodgements in the fixture.
assert a["address_line_1"] == b["address_line_1"]
def test_does_not_mutate_the_input() -> None:
# Arrange
raw = _payload()
# Act
anonymise_payload(raw)
# Assert — the caller's payload is left intact.
assert raw["address_line_1"] == "12 Acacia Avenue"
def test_loads_the_coordinates_sidecar(tmp_path: Path) -> None:
# Arrange — a `_coordinates.json` sidecar mapping UPRN -> [lon, lat].
(tmp_path / "_coordinates.json").write_text(
json.dumps({"100024": [-1.5, 53.4]})
)
# Act
coordinates = load_coordinates(tmp_path)
# Assert — parsed into UPRN-keyed Coordinates.
assert coordinates == {100024: Coordinates(longitude=-1.5, latitude=53.4)}
def test_coordinates_sidecar_absent_yields_empty(tmp_path: Path) -> None:
# Arrange / Act — no sidecar present (a corpus without geo data).
coordinates = load_coordinates(tmp_path)
# Assert — geo-weighting simply stays off.
assert coordinates == {}