feat(epc-prediction): EpcPrediction hybrid synthesis (ADR-0029)

predict() copies a representative template comparable's structure (coherent for
the calculator), overrides the homogeneous categorical with the cohort mode
(robust to an atypical template), then applies known Landlord Overrides on top
(a known value wins over the estimate). Proven on wall construction; roof/floor/
insulation/age extend on the same mode+override mechanism, driven next by the
validation harness metrics.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-06-13 23:50:07 +00:00
parent bf6b6fac17
commit 5e6d2cff16
2 changed files with 186 additions and 0 deletions

View file

@ -0,0 +1,88 @@
"""EPC Prediction synthesis (ADR-0029).
`EpcPrediction.predict` turns the selected `ComparableProperties` into a
predicted `EpcPropertyData`: copy a coherent representative template's structure
(building parts, windows, geometry), set the homogeneous categoricals to the
recency-weighted cohort mode, then apply Landlord Overrides on top. Pure domain
logic deterministic neighbour synthesis, not ML.
"""
from __future__ import annotations
import copy
from collections import Counter
from typing import Iterable, Optional, Union
from datatypes.epc.domain.epc_property_data import (
EpcPropertyData,
SapBuildingPart,
)
from domain.epc_prediction.comparable_properties import (
Comparable,
ComparableProperties,
PredictionTarget,
)
class EpcPrediction:
"""Synthesises a predicted `EpcPropertyData` from Comparable Properties."""
def predict(
self, target: PredictionTarget, comparables: ComparableProperties
) -> EpcPropertyData:
"""Predict the target's EPC picture: copy a representative template's
structure (coherent for the calculator), then set the homogeneous
categoricals to the cohort mode."""
template: Comparable = self._template(comparables)
predicted: EpcPropertyData = copy.deepcopy(template.epc)
self._apply_categorical_modes(predicted, comparables)
self._apply_overrides(predicted, target)
return predicted
@staticmethod
def _template(comparables: ComparableProperties) -> Comparable:
"""The representative comparable whose structure seeds the prediction."""
return comparables.members[0]
@staticmethod
def _apply_categorical_modes(
predicted: EpcPropertyData, comparables: ComparableProperties
) -> None:
"""Override the predicted picture's homogeneous categoricals with the
cohort mode (robust to an atypical template)."""
if not predicted.sap_building_parts:
return
main = predicted.sap_building_parts[0]
wall_mode = _mode(
_main_wall_construction(c) for c in comparables.members
)
if wall_mode is not None:
main.wall_construction = wall_mode
@staticmethod
def _apply_overrides(
predicted: EpcPropertyData, target: PredictionTarget
) -> None:
"""Apply the known Landlord Overrides on top of the estimate — a known
value always wins over the cohort mode (ADR-0029)."""
if not predicted.sap_building_parts:
return
if target.wall_construction is not None:
predicted.sap_building_parts[0].wall_construction = (
target.wall_construction
)
def _main_wall_construction(comparable: Comparable) -> Optional[Union[int, str]]:
parts: list[SapBuildingPart] = comparable.epc.sap_building_parts
return parts[0].wall_construction if parts else None
def _mode(
values: Iterable[Optional[Union[int, str]]],
) -> Optional[Union[int, str]]:
"""The most common non-None value, or None when there are none."""
present = [v for v in values if v is not None]
if not present:
return None
return Counter(present).most_common(1)[0][0]

View file

@ -0,0 +1,98 @@
"""Behaviour of EPC Prediction synthesis (ADR-0029): turn the selected
Comparable Properties into a predicted EpcPropertyData. Hybrid copy a coherent
representative template's structure (building parts, windows, geometry), set the
homogeneous categoricals to the recency-weighted cohort mode, apply Landlord
Overrides on top. Pure domain logic.
"""
from typing import Union
from datatypes.epc.domain.epc_property_data import EpcPropertyData, SapBuildingPart
from domain.epc_prediction.comparable_properties import (
Comparable,
ComparableProperties,
PredictionTarget,
)
from domain.epc_prediction.epc_prediction import EpcPrediction
def _epc(
*,
building_parts: int = 1,
floor_area: float = 80.0,
wall_construction: Union[int, str] = 1,
) -> EpcPropertyData:
epc: EpcPropertyData = object.__new__(EpcPropertyData)
epc.property_type = "2"
epc.built_form = "4"
epc.total_floor_area_m2 = floor_area
parts: list[SapBuildingPart] = []
for _ in range(building_parts):
part: SapBuildingPart = object.__new__(SapBuildingPart)
part.wall_construction = wall_construction
parts.append(part)
epc.sap_building_parts = parts
return epc
def _cohort(*epcs: EpcPropertyData) -> ComparableProperties:
return ComparableProperties(
members=tuple(
Comparable(epc=e, certificate_number=str(i)) for i, e in enumerate(epcs)
)
)
def test_predicts_a_picture_by_copying_a_representative_template() -> None:
# Arrange — a single comparable with a distinctive structure (2 building
# parts, 92 m²); with nothing else to go on it is the template.
template = _epc(building_parts=2, floor_area=92.0)
target = PredictionTarget(postcode="LS6 1AA", property_type="2")
# Act
predicted: EpcPropertyData = EpcPrediction().predict(target, _cohort(template))
# Assert — the structure is copied wholesale (and it is a copy, not the same
# object — the baseline must never be mutated).
assert len(predicted.sap_building_parts) == 2
assert predicted.total_floor_area_m2 == 92.0
assert predicted is not template
def test_sets_main_wall_construction_to_the_cohort_mode() -> None:
# Arrange — the template (members[0]) is solid brick (2), but the cohort
# majority is cavity (1). The homogeneous categorical should follow the mode,
# not the one template, so the prediction is robust to an atypical template.
cohort = _cohort(
_epc(wall_construction=2),
_epc(wall_construction=1),
_epc(wall_construction=1),
_epc(wall_construction=1),
)
# Act
predicted: EpcPropertyData = EpcPrediction().predict(
PredictionTarget(postcode="LS6 1AA", property_type="2"), cohort
)
# Assert — cavity (the mode) wins over the solid-brick template.
assert predicted.sap_building_parts[0].wall_construction == 1
def test_applies_a_known_wall_override_over_the_mode() -> None:
# Arrange — the cohort mode is cavity (1), but we KNOW the target is solid
# brick (2), a Landlord Override. The known value must win over the estimate.
cohort = _cohort(
_epc(wall_construction=1),
_epc(wall_construction=1),
_epc(wall_construction=1),
)
target = PredictionTarget(
postcode="LS6 1AA", property_type="2", wall_construction=2
)
# Act
predicted: EpcPropertyData = EpcPrediction().predict(target, cohort)
# Assert — the known override overrides the cohort mode.
assert predicted.sap_building_parts[0].wall_construction == 2