mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-19 17:03:02 +00:00
Per pair, replay the age->fuel->TFA conditioning ladder over the plain arm's cohort to record ENGAGED vs RELAXED per filter, and record whether each historic stable attribute still agrees with the newly lodged cert (the direct staleness measurement). Emitted as JSONL (--telemetry) and aggregated into two diagnosis tables appended to the report. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
148 lines
5.1 KiB
Python
148 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 == {
|
|
"construction_age_band": None,
|
|
"main_fuel": None,
|
|
"total_floor_area": None,
|
|
}
|