From 992950bd9f850136ccbfc72deb3293cc4df3030b Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Mon, 6 Jul 2026 08:21:41 +0000 Subject: [PATCH] =?UTF-8?q?Resolve=20a=20historic=20EPC's=20stable=20attri?= =?UTF-8?q?butes=20into=20cohort=20code=20spaces=20=F0=9F=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 --- .../epc_prediction/historic_conditioning.py | 43 ++++++++++++ .../test_historic_conditioning.py | 70 +++++++++++++++++++ 2 files changed, 113 insertions(+) create mode 100644 domain/epc_prediction/historic_conditioning.py create mode 100644 tests/domain/epc_prediction/test_historic_conditioning.py diff --git a/domain/epc_prediction/historic_conditioning.py b/domain/epc_prediction/historic_conditioning.py new file mode 100644 index 000000000..ca0d318f2 --- /dev/null +++ b/domain/epc_prediction/historic_conditioning.py @@ -0,0 +1,43 @@ +"""Resolve an expired Historic EPC's *stable* attributes into the cohort code +spaces that condition EPC Prediction (ADR-0054). + +The historic dump carries display text ("Semi-Detached", "Cavity wall, as +built, no insulation (assumed)"); the Comparable Properties cohort carries +gov-EPC codes. This module is the anti-corruption layer between the two: one +resolver per whitelisted stable attribute, each returning None on an +unresolvable value so its cohort filter is simply inactive. Volatile +attributes (heating system, hot water, glazing, PV, insulation states, +lighting) are deliberately absent — a 14+-year-old observation of them is not +evidence about today. +""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Optional + +from datatypes.epc.domain.historic_epc import HistoricEpc + + +@dataclass(frozen=True) +class HistoricConditioning: + """The expired cert's stable attributes, in cohort code space; None means + "unresolved — do not condition on this".""" + + property_type: Optional[str] + built_form: Optional[str] + wall_construction: Optional[int] + construction_age_band: Optional[str] + main_fuel: Optional[int] + total_floor_area_m2: Optional[float] + + +def conditioning_from_historic(record: HistoricEpc) -> HistoricConditioning: + return HistoricConditioning( + property_type=None, + built_form=None, + wall_construction=None, + construction_age_band=None, + main_fuel=None, + total_floor_area_m2=None, + ) diff --git a/tests/domain/epc_prediction/test_historic_conditioning.py b/tests/domain/epc_prediction/test_historic_conditioning.py new file mode 100644 index 000000000..8f26febb1 --- /dev/null +++ b/tests/domain/epc_prediction/test_historic_conditioning.py @@ -0,0 +1,70 @@ +"""HistoricConditioning resolves an expired Historic EPC's stable attributes +into the cohort's code spaces (ADR-0054). + +Only stable attributes are resolved — volatile ones (heating system, glazing, +PV, insulation states) never condition prediction. Every resolver degrades to +None on an unresolvable value, which leaves its cohort filter inactive. +""" + +from __future__ import annotations + +import dataclasses + +from datatypes.epc.domain.historic_epc import HistoricEpc +from domain.epc_prediction.historic_conditioning import ( + HistoricConditioning, + conditioning_from_historic, +) + + +def _hist(**overrides: str) -> HistoricEpc: + fields = {f.name: "" for f in dataclasses.fields(HistoricEpc)} + fields.update(overrides) + return HistoricEpc(**fields) + + +def test_resolves_stable_attributes_into_cohort_code_spaces(): + # Arrange — display-text values exactly as the old register lodged them. + record = _hist( + property_type="House", + built_form="Semi-Detached", + walls_description="Cavity wall, as built, no insulation (assumed)", + construction_age_band="England and Wales: 1930-1949", + main_fuel="mains gas (not community)", + total_floor_area="84", + ) + + # Act + conditioning = conditioning_from_historic(record) + + # Assert — each attribute lands in the code space its cohort filter compares. + assert isinstance(conditioning, HistoricConditioning) + assert conditioning.property_type == "0" + assert conditioning.built_form == "2" + assert conditioning.wall_construction == 4 + assert conditioning.construction_age_band == "C" + assert conditioning.main_fuel == 26 + assert conditioning.total_floor_area_m2 == 84.0 + + +def test_unresolvable_values_degrade_to_none(): + # Arrange — junk and blanks must never guess a code. + record = _hist( + property_type="Castle", + built_form="Not Recorded", + walls_description="Average thermal transmittance 0.3 W/m²K", + construction_age_band="INVALID!", + main_fuel="To be used only when there is no heating system", + total_floor_area="", + ) + + # Act + conditioning = conditioning_from_historic(record) + + # Assert + assert conditioning.property_type is None + assert conditioning.built_form is None + assert conditioning.wall_construction is None + assert conditioning.construction_age_band is None + assert conditioning.main_fuel is None + assert conditioning.total_floor_area_m2 is None