Model/tests/scripts/test_expired_prediction_pairs_harness.py
Khalim Conn-Kowlessar 817c00720e The historic roof description conditions the cohort by form family 🟩
roof_construction codes group by FORM (empirical: 1=Flat 98%, 4/5/8=
Pitched 88-99%, 3=dwelling-above 100% over 7,974 certs; 7/9=premises-
above per #1452), so the filter matches families — an exact-code filter
would wrongly drop pitched neighbours lodged as 5/8. Historic prefixes
map to the same families; roof rooms and thatch stay unconditioned.
Harness ladder replay and telemetry mirror the new filter.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 12:05:56 +00:00

149 lines
5.1 KiB
Python

"""Pure pair-selection and aggregation logic of the ADR-0054 pairs harness."""
from __future__ import annotations
import dataclasses
from typing import Optional
from datatypes.epc.domain.historic_epc import HistoricEpc
from domain.epc_prediction.prediction_comparison import PredictionComparison
from scripts.expired_prediction_pairs_harness import (
PairScore,
aggregate,
format_report,
latest_pre_2012_by_uprn,
)
def _hist(uprn: str, lodgement_date: str) -> HistoricEpc:
fields = {f.name: "" for f in dataclasses.fields(HistoricEpc)}
fields["uprn"] = uprn
fields["lodgement_date"] = lodgement_date
return HistoricEpc(**fields)
def _score(
hits: dict[str, Optional[bool]], sap_residual: Optional[float] = None
) -> PairScore:
return PairScore(
comparison=PredictionComparison(
categorical_hits=hits,
floor_area_residual=-4.0,
building_parts_residual=1,
window_count_residual=-2,
total_window_area_residual=3.5,
door_count_residual=0,
),
sap_residual=sap_residual,
)
def test_pairs_keep_only_the_latest_pre_2012_cert_per_uprn():
# Arrange — one UPRN lodged twice pre-2012, once post-2012; one UPRN-less row.
records = [
_hist("100", "2008-05-01"),
_hist("100", "2010-11-30"),
_hist("100", "2013-01-01"),
_hist("", "2009-01-01"),
]
# Act
by_uprn = latest_pre_2012_by_uprn(records)
# Assert — the 2010 cert wins; the 2013 one can never seed an expired pair.
assert set(by_uprn) == {"100"}
assert by_uprn["100"].lodgement_date == "2010-11-30"
def test_aggregate_matches_the_component_accuracy_shape():
# Arrange — two pairs; wall scored twice (1 hit), roof scored once (None
# means the actual lodges no value — out of the denominator, per ADR-0030).
scores = [
_score({"wall_construction": True, "roof_construction": None}, sap_residual=6.0),
_score({"wall_construction": False, "roof_construction": True}),
]
# Act
arm = aggregate(scores)
# Assert — classification (hits, applicable), all five residual components,
# and the SAP residual list (only the scored pair contributes).
assert arm.classification["wall_construction"] == (1, 2)
assert arm.classification["roof_construction"] == (1, 1)
assert arm.residuals["floor_area_m2"] == [-4.0, -4.0]
assert arm.residuals["building_parts"] == [1.0, 1.0]
assert arm.residuals["window_count"] == [-2.0, -2.0]
assert arm.residuals["total_window_area_m2"] == [3.5, 3.5]
assert arm.residuals["door_count"] == [0.0, 0.0]
assert arm.sap_residuals == [6.0]
def test_report_prints_both_arms_side_by_side():
# Arrange
plain = aggregate([_score({"wall_construction": False}, sap_residual=-8.0)])
conditioned = aggregate([_score({"wall_construction": True}, sap_residual=2.0)])
# Act
report = format_report(plain, conditioned, pairs=1)
# Assert — hit-rates, residuals and the SAP arm all present, side by side.
assert "| wall_construction | 0/1 (0%) | 1/1 (100%) |" in report
assert "| floor_area_m2 | 4.0 | 4.0 |" in report
assert "| mean abs | 8.0 | 2.0 |" in report
assert "1 pairs" in report
def test_ladder_simulation_engages_only_with_enough_matches():
# Arrange — a 6-strong base cohort: 5 band-C (engages at k=5), then within
# the band-C survivors only 2 on fuel 26 (relaxes), and 5 within ±5% of
# 100 m² (engages on the un-shrunk cohort).
from scripts.expired_prediction_pairs_harness import simulate_conditioning_ladder
from tests.domain.epc_prediction.test_comparable_properties import _comparable
base = [
_comparable(
property_type="0",
certificate_number=f"C{i}",
construction_age_band="C",
main_fuel=26 if i < 2 else 29,
total_floor_area_m2=100.0 + i,
)
for i in range(5)
] + [
_comparable(
property_type="0",
certificate_number="G0",
construction_age_band="G",
main_fuel=26,
total_floor_area_m2=100.0,
)
]
# Act
steps = simulate_conditioning_ladder(
base, age_band="C", main_fuel=26, total_floor_area_m2=100.0
)
# Assert — age engaged (5 matches), fuel relaxed (2 < 5 within band-C
# survivors), TFA engaged (all 5 survivors within the band).
age = steps["construction_age_band"]
fuel = steps["main_fuel"]
tfa = steps["total_floor_area"]
assert age is not None and age.engaged and age.matches == 5
assert fuel is not None and not fuel.engaged and fuel.matches == 2
assert tfa is not None and tfa.engaged and tfa.matches == 5
def test_ladder_simulation_skips_unresolved_attributes():
from scripts.expired_prediction_pairs_harness import simulate_conditioning_ladder
steps = simulate_conditioning_ladder(
[], age_band=None, main_fuel=None, total_floor_area_m2=None
)
assert steps == {
"roof_form": None,
"construction_age_band": None,
"main_fuel": None,
"total_floor_area": None,
}