mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-12 13:29:04 +00:00
The historic roof description conditions the cohort by form family 🟥
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
13b75bcb0a
commit
49c9f8181c
4 changed files with 62 additions and 0 deletions
|
|
@ -109,6 +109,7 @@ class HistoricConditioning:
|
|||
construction_age_band: Optional[str]
|
||||
main_fuel: Optional[int]
|
||||
total_floor_area_m2: Optional[float]
|
||||
roof_form: Optional[str] = None
|
||||
|
||||
|
||||
def _wall_construction(description: str) -> Optional[int]:
|
||||
|
|
|
|||
|
|
@ -40,6 +40,10 @@ class PredictionTarget:
|
|||
construction_age_band: Optional[str] = None
|
||||
main_fuel: Optional[int] = None
|
||||
total_floor_area_m2: Optional[float] = None
|
||||
# The roof FORM family ("pitched" / "flat" / "dwelling_above" /
|
||||
# "premises_above") — the API's roof_construction codes group by form,
|
||||
# so the filter matches families, never exact codes (ADR-0054 amendment).
|
||||
roof_form: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ def _comparable(
|
|||
construction_age_band: Optional[str] = None,
|
||||
main_fuel: Optional[int] = None,
|
||||
total_floor_area_m2: Optional[float] = None,
|
||||
roof_construction: Optional[int] = None,
|
||||
) -> ComparableProperty:
|
||||
"""A ComparableProperty carrying only the fields under test (opaque EpcPropertyData
|
||||
with property_type / built_form / main wall set — the partial-instance idiom)."""
|
||||
|
|
@ -44,6 +45,8 @@ def _comparable(
|
|||
main.wall_construction = wall_construction
|
||||
if construction_age_band is not None:
|
||||
main.construction_age_band = construction_age_band
|
||||
if roof_construction is not None:
|
||||
main.roof_construction = roof_construction
|
||||
epc.sap_building_parts = [main]
|
||||
if main_fuel is not None:
|
||||
detail: MainHeatingDetail = object.__new__(MainHeatingDetail)
|
||||
|
|
@ -306,6 +309,32 @@ def test_historic_age_band_conditions_within_one_band() -> None:
|
|||
}
|
||||
|
||||
|
||||
def test_historic_roof_form_conditions_the_cohort_by_family() -> None:
|
||||
# Arrange — the expired cert observed a pitched roof. The API's
|
||||
# roof_construction codes group into FORM families (empirical sweep:
|
||||
# 4/5/8 = pitched, 1 = flat), so all pitched-family comparables match and
|
||||
# the flat one drops (ADR-0054 as amended).
|
||||
target = PredictionTarget(postcode="LS6 1AA", property_type="2", roof_form="pitched")
|
||||
candidates = [
|
||||
_comparable(property_type="2", roof_construction=4, certificate_number="P4a"),
|
||||
_comparable(property_type="2", roof_construction=4, certificate_number="P4b"),
|
||||
_comparable(property_type="2", roof_construction=5, certificate_number="P5"),
|
||||
_comparable(property_type="2", roof_construction=8, certificate_number="P8"),
|
||||
_comparable(property_type="2", roof_construction=4, certificate_number="P4c"),
|
||||
_comparable(property_type="2", roof_construction=1, certificate_number="F1"),
|
||||
]
|
||||
|
||||
# Act
|
||||
result: ComparableProperties = select_comparables(
|
||||
target, candidates, minimum_cohort=5
|
||||
)
|
||||
|
||||
# Assert — the whole pitched family survives; the flat roof drops.
|
||||
assert {c.certificate_number for c in result.members} == {
|
||||
"P4a", "P4b", "P4c", "P5", "P8"
|
||||
}
|
||||
|
||||
|
||||
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).
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ def test_resolves_stable_attributes_into_cohort_code_spaces():
|
|||
property_type="House",
|
||||
built_form="Semi-Detached",
|
||||
walls_description="Cavity wall, as built, no insulation (assumed)",
|
||||
roof_description="Pitched, 100 mm loft insulation",
|
||||
construction_age_band="England and Wales: 1930-1949",
|
||||
main_fuel="mains gas (not community)",
|
||||
total_floor_area="84",
|
||||
|
|
@ -42,11 +43,37 @@ def test_resolves_stable_attributes_into_cohort_code_spaces():
|
|||
assert conditioning.property_type == "0"
|
||||
assert conditioning.built_form == "2"
|
||||
assert conditioning.wall_construction == 4
|
||||
assert conditioning.roof_form == "pitched"
|
||||
assert conditioning.construction_age_band == "C"
|
||||
assert conditioning.main_fuel == 26
|
||||
assert conditioning.total_floor_area_m2 == 84.0
|
||||
|
||||
|
||||
def test_roof_descriptions_resolve_to_form_families():
|
||||
# Arrange / Act / Assert — the FORM half of the description (before the
|
||||
# comma) maps to a family, because the API's roof_construction codes group
|
||||
# that way (7,974-cert co-occurrence sweep: 1=Flat 98%, 4/5/8=Pitched
|
||||
# 88-99%, 3=another dwelling above 100%; 7/9=another premises above per
|
||||
# the #1452 suppression fix). Insulation state is volatile and ignored.
|
||||
cases = {
|
||||
"Pitched, 250 mm loft insulation": "pitched",
|
||||
"Pitched, no insulation (assumed)": "pitched",
|
||||
"Flat, insulated (assumed)": "flat",
|
||||
"(another dwelling above)": "dwelling_above",
|
||||
"(another premises above)": "premises_above",
|
||||
}
|
||||
for text, family in cases.items():
|
||||
record = _hist(roof_description=text)
|
||||
assert conditioning_from_historic(record).roof_form == family, text
|
||||
# Unpinned forms (roof rooms, thatched) must not guess.
|
||||
assert conditioning_from_historic(
|
||||
_hist(roof_description="Roof room(s), insulated")
|
||||
).roof_form is None
|
||||
assert conditioning_from_historic(
|
||||
_hist(roof_description="Thatched, with additional insulation")
|
||||
).roof_form is None
|
||||
|
||||
|
||||
def test_legacy_register_fuel_descriptions_resolve():
|
||||
# Arrange / Act / Assert — the old register lodged pre-RdSAP-17 fuels with
|
||||
# a "backwards compatibility" rider or a SAP-style prefix; they name the
|
||||
|
|
@ -84,6 +111,7 @@ def test_unresolvable_values_degrade_to_none():
|
|||
assert conditioning.property_type is None
|
||||
assert conditioning.built_form is None
|
||||
assert conditioning.wall_construction is None
|
||||
assert conditioning.roof_form is None
|
||||
assert conditioning.construction_age_band is None
|
||||
assert conditioning.main_fuel is None
|
||||
assert conditioning.total_floor_area_m2 is None
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue