mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-30 13:10:47 +00:00
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:
parent
bf6b6fac17
commit
5e6d2cff16
2 changed files with 186 additions and 0 deletions
88
domain/epc_prediction/epc_prediction.py
Normal file
88
domain/epc_prediction/epc_prediction.py
Normal 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]
|
||||
98
tests/domain/epc_prediction/test_epc_prediction.py
Normal file
98
tests/domain/epc_prediction/test_epc_prediction.py
Normal 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
|
||||
Loading…
Add table
Reference in a new issue