mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-27 23:35:01 +00:00
Only main wall_construction was set to the cohort mode; the other homogeneous categoricals (wall insulation, construction age band, roof construction, floor construction) were left as template-copied, so one median-size template's quirks set them. Apply the same cohort-mode mechanism to all of them per ADR-0029 decision 4 — the template still supplies geometry, only the categorical codes move to the mode. Verified mode beats (or ties) template-copy per categorical before applying. Smoke corpus (29 leave-one-out) classification rates: construction_age_band 55.2% -> 65.5% roof_construction 72.4% -> 79.3% floor_construction 46.2% -> 84.6% wall_insulation_type 93.1% (tie — already template-strong) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
175 lines
6.3 KiB
Python
175 lines
6.3 KiB
Python
"""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 Optional, Union
|
|
|
|
from datatypes.epc.domain.epc_property_data import (
|
|
EpcPropertyData,
|
|
SapBuildingPart,
|
|
SapFloorDimension,
|
|
)
|
|
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,
|
|
wall_insulation_type: Union[int, str] = 1,
|
|
construction_age_band: str = "K",
|
|
roof_construction: Optional[int] = 1,
|
|
floor_construction: Optional[int] = 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
|
|
part.wall_insulation_type = wall_insulation_type
|
|
part.construction_age_band = construction_age_band
|
|
part.roof_construction = roof_construction
|
|
floor_dim: SapFloorDimension = object.__new__(SapFloorDimension)
|
|
floor_dim.floor_construction = floor_construction
|
|
part.sap_floor_dimensions = [floor_dim]
|
|
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_template_is_the_member_closest_to_the_cohort_median_size() -> None:
|
|
# Arrange — the cohort spans a wide range of sizes; members[0] is an atypical
|
|
# tiny 20 m² outlier. A single neighbour's geometry is copied wholesale, so
|
|
# the template must be the size-representative member (closest to the median),
|
|
# not whoever happens to come first (ADR-0029 decision 4: closest on size).
|
|
cohort = _cohort(
|
|
_epc(floor_area=20.0),
|
|
_epc(floor_area=80.0),
|
|
_epc(floor_area=200.0),
|
|
)
|
|
|
|
# Act
|
|
predicted: EpcPropertyData = EpcPrediction().predict(
|
|
PredictionTarget(postcode="LS6 1AA", property_type="2"), cohort
|
|
)
|
|
|
|
# Assert — the 80 m² member (the median) seeds the structure, not the 20 m²
|
|
# outlier sitting at members[0].
|
|
assert predicted.total_floor_area_m2 == 80.0
|
|
|
|
|
|
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_sets_the_other_homogeneous_categoricals_to_the_cohort_mode() -> None:
|
|
# Arrange — the median-size template (members[0], 80 m²) is an atypical
|
|
# outlier on every categorical; the cohort majority disagrees. Age band,
|
|
# wall insulation, roof construction and floor construction are all
|
|
# homogeneous categoricals, so each should follow its mode, not the one
|
|
# template (ADR-0029 decision 4).
|
|
cohort = _cohort(
|
|
_epc(
|
|
floor_area=80.0,
|
|
construction_age_band="A",
|
|
wall_insulation_type=9,
|
|
roof_construction=7,
|
|
floor_construction=7,
|
|
),
|
|
_epc(
|
|
construction_age_band="K",
|
|
wall_insulation_type=1,
|
|
roof_construction=2,
|
|
floor_construction=3,
|
|
),
|
|
_epc(
|
|
construction_age_band="K",
|
|
wall_insulation_type=1,
|
|
roof_construction=2,
|
|
floor_construction=3,
|
|
),
|
|
)
|
|
|
|
# Act
|
|
predicted: EpcPropertyData = EpcPrediction().predict(
|
|
PredictionTarget(postcode="LS6 1AA", property_type="2"), cohort
|
|
)
|
|
|
|
# Assert — every categorical follows the cohort mode over the outlier
|
|
# template.
|
|
main = predicted.sap_building_parts[0]
|
|
assert main.construction_age_band == "K"
|
|
assert main.wall_insulation_type == 1
|
|
assert main.roof_construction == 2
|
|
assert main.sap_floor_dimensions[0].floor_construction == 3
|
|
|
|
|
|
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
|