"""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, }