mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-12 13:29:04 +00:00
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>
149 lines
5.1 KiB
Python
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,
|
|
}
|