mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-19 17:03:02 +00:00
Historic stable attributes condition cohort selection on the relax ladder 🟥
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
bbfcf1385b
commit
248aa5ad6a
2 changed files with 128 additions and 1 deletions
|
|
@ -34,6 +34,12 @@ class PredictionTarget:
|
|||
# The target Property's own coordinates (resolved from its UPRN), against
|
||||
# which neighbours are distance-weighted. None disables geo-weighting.
|
||||
coordinates: Optional[Coordinates] = None
|
||||
# Stable attributes observed on an expired Historic EPC condition selection
|
||||
# the same way (ADR-0054): RdSAP Table S1 band letter, modern main_fuel
|
||||
# code, and a ±5% floor-area band. None leaves each filter inactive.
|
||||
construction_age_band: Optional[str] = None
|
||||
main_fuel: Optional[int] = None
|
||||
total_floor_area_m2: Optional[float] = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
|
|
|
|||
|
|
@ -8,7 +8,12 @@ enough remain, weighted by recency × similarity. Pure domain logic.
|
|||
from datetime import date
|
||||
from typing import Optional, Union
|
||||
|
||||
from datatypes.epc.domain.epc_property_data import EpcPropertyData, SapBuildingPart
|
||||
from datatypes.epc.domain.epc_property_data import (
|
||||
EpcPropertyData,
|
||||
MainHeatingDetail,
|
||||
SapBuildingPart,
|
||||
SapHeating,
|
||||
)
|
||||
from domain.epc_prediction.comparable_properties import (
|
||||
ComparableProperty,
|
||||
ComparableProperties,
|
||||
|
|
@ -25,6 +30,9 @@ def _comparable(
|
|||
wall_construction: Optional[Union[int, str]] = None,
|
||||
address: Optional[str] = None,
|
||||
registration_date: Optional[date] = None,
|
||||
construction_age_band: Optional[str] = None,
|
||||
main_fuel: Optional[int] = None,
|
||||
total_floor_area_m2: Optional[float] = None,
|
||||
) -> ComparableProperty:
|
||||
"""A ComparableProperty carrying only the fields under test (opaque EpcPropertyData
|
||||
with property_type / built_form / main wall set — the partial-instance idiom)."""
|
||||
|
|
@ -34,7 +42,17 @@ def _comparable(
|
|||
main: SapBuildingPart = object.__new__(SapBuildingPart)
|
||||
if wall_construction is not None:
|
||||
main.wall_construction = wall_construction
|
||||
if construction_age_band is not None:
|
||||
main.construction_age_band = construction_age_band
|
||||
epc.sap_building_parts = [main]
|
||||
if main_fuel is not None:
|
||||
detail: MainHeatingDetail = object.__new__(MainHeatingDetail)
|
||||
detail.main_fuel_type = main_fuel
|
||||
heating: SapHeating = object.__new__(SapHeating)
|
||||
heating.main_heating_details = [detail]
|
||||
epc.sap_heating = heating
|
||||
if total_floor_area_m2 is not None:
|
||||
epc.total_floor_area_m2 = total_floor_area_m2
|
||||
return ComparableProperty(
|
||||
epc=epc,
|
||||
certificate_number=certificate_number,
|
||||
|
|
@ -171,3 +189,106 @@ def test_known_wall_override_relaxes_when_too_few_match() -> None:
|
|||
|
||||
# Assert — relaxed: all eight houses retained.
|
||||
assert len(result.members) == 8
|
||||
|
||||
|
||||
def test_historic_age_band_conditions_the_cohort() -> None:
|
||||
# Arrange — the expired cert observed band C (1930-1949); 5 band-C houses +
|
||||
# 2 band-G houses in the cohort (ADR-0054).
|
||||
target = PredictionTarget(
|
||||
postcode="LS6 1AA", property_type="2", construction_age_band="C"
|
||||
)
|
||||
candidates = [
|
||||
_comparable(
|
||||
property_type="2", construction_age_band="C", certificate_number=f"C{i}"
|
||||
)
|
||||
for i in range(5)
|
||||
] + [
|
||||
_comparable(
|
||||
property_type="2", construction_age_band="G", certificate_number=f"G{i}"
|
||||
)
|
||||
for i in range(2)
|
||||
]
|
||||
|
||||
# Act
|
||||
result: ComparableProperties = select_comparables(
|
||||
target, candidates, minimum_cohort=5
|
||||
)
|
||||
|
||||
# Assert — only the same-age-band comparables remain.
|
||||
assert {c.certificate_number for c in result.members} == {
|
||||
"C0", "C1", "C2", "C3", "C4"
|
||||
}
|
||||
|
||||
|
||||
def test_historic_main_fuel_conditions_the_cohort() -> None:
|
||||
# Arrange — the expired cert observed mains gas (26); 5 gas + 2 electric
|
||||
# (29) houses in the cohort (ADR-0054).
|
||||
target = PredictionTarget(postcode="LS6 1AA", property_type="2", main_fuel=26)
|
||||
candidates = [
|
||||
_comparable(property_type="2", main_fuel=26, certificate_number=f"G{i}")
|
||||
for i in range(5)
|
||||
] + [
|
||||
_comparable(property_type="2", main_fuel=29, certificate_number=f"E{i}")
|
||||
for i in range(2)
|
||||
]
|
||||
|
||||
# Act
|
||||
result: ComparableProperties = select_comparables(
|
||||
target, candidates, minimum_cohort=5
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert {c.certificate_number for c in result.members} == {
|
||||
"G0", "G1", "G2", "G3", "G4"
|
||||
}
|
||||
|
||||
|
||||
def test_floor_area_band_keeps_comparables_within_5_percent() -> None:
|
||||
# Arrange — the expired cert observed 100 m²; the band is ±5% (ADR-0054):
|
||||
# 96 and 104.9 are in, 90 and 111 are out.
|
||||
target = PredictionTarget(
|
||||
postcode="LS6 1AA", property_type="2", total_floor_area_m2=100.0
|
||||
)
|
||||
candidates = [
|
||||
_comparable(property_type="2", total_floor_area_m2=96.0, certificate_number="A"),
|
||||
_comparable(
|
||||
property_type="2", total_floor_area_m2=104.9, certificate_number="B"
|
||||
),
|
||||
_comparable(property_type="2", total_floor_area_m2=90.0, certificate_number="X"),
|
||||
_comparable(
|
||||
property_type="2", total_floor_area_m2=111.0, certificate_number="Y"
|
||||
),
|
||||
]
|
||||
|
||||
# Act
|
||||
result: ComparableProperties = select_comparables(
|
||||
target, candidates, minimum_cohort=2
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert {c.certificate_number for c in result.members} == {"A", "B"}
|
||||
|
||||
|
||||
def test_floor_area_band_relaxes_when_too_few_match() -> None:
|
||||
# Arrange — only one comparable inside the ±5% band (< k=2): the band must
|
||||
# relax rather than starve the cohort (graceful degradation, ADR-0029).
|
||||
target = PredictionTarget(
|
||||
postcode="LS6 1AA", property_type="2", total_floor_area_m2=100.0
|
||||
)
|
||||
candidates = [
|
||||
_comparable(property_type="2", total_floor_area_m2=99.0, certificate_number="A"),
|
||||
_comparable(
|
||||
property_type="2", total_floor_area_m2=140.0, certificate_number="X"
|
||||
),
|
||||
_comparable(
|
||||
property_type="2", total_floor_area_m2=150.0, certificate_number="Y"
|
||||
),
|
||||
]
|
||||
|
||||
# Act
|
||||
result: ComparableProperties = select_comparables(
|
||||
target, candidates, minimum_cohort=2
|
||||
)
|
||||
|
||||
# Assert — relaxed: all three retained.
|
||||
assert len(result.members) == 3
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue