From d2d696a1c57ea7ef09bed71c04117d922e67234f Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Mon, 6 Jul 2026 08:26:00 +0000 Subject: [PATCH] =?UTF-8?q?Historic=20stable=20attributes=20condition=20co?= =?UTF-8?q?hort=20selection=20on=20the=20relax=20ladder=20=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Age band (Table S1 letter), main fuel code, and a ±5% floor-area band — the first numeric-tolerance filter — each ride _maybe_filter, so an unresolved attribute (None) is inactive and a starving filter relaxes. Existing callers pass no new fields and are behaviourally untouched. Co-Authored-By: Claude Opus 4.8 --- .../epc_prediction/comparable_properties.py | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/domain/epc_prediction/comparable_properties.py b/domain/epc_prediction/comparable_properties.py index 13e33cf73..47b762346 100644 --- a/domain/epc_prediction/comparable_properties.py +++ b/domain/epc_prediction/comparable_properties.py @@ -21,6 +21,10 @@ from domain.geospatial.coordinates import Coordinates # else it is relaxed (ADR-0029 filter-then-relax ladder). _DEFAULT_MINIMUM_COHORT = 5 +# Half-width of the floor-area conditioning band: an expired Historic EPC's +# observed floor area keeps comparables within ±5% of it (ADR-0054). +_FLOOR_AREA_TOLERANCE = 0.05 + @dataclass(frozen=True) class ComparableProperty: @@ -88,6 +92,27 @@ def select_comparables( active=target.wall_construction is not None, minimum_cohort=minimum_cohort, ) + cohort = _maybe_filter( + cohort, + lambda c: _main_construction_age_band(c) == target.construction_age_band, + active=target.construction_age_band is not None, + minimum_cohort=minimum_cohort, + ) + cohort = _maybe_filter( + cohort, + lambda c: _main_fuel_type(c) == target.main_fuel, + active=target.main_fuel is not None, + minimum_cohort=minimum_cohort, + ) + target_area = target.total_floor_area_m2 + cohort = _maybe_filter( + cohort, + lambda c: target_area is not None + and abs(c.epc.total_floor_area_m2 - target_area) + <= _FLOOR_AREA_TOLERANCE * target_area, + active=target_area is not None, + minimum_cohort=minimum_cohort, + ) return ComparableProperties(members=tuple(cohort)) @@ -124,3 +149,15 @@ def _main_wall_construction(comparable: ComparableProperty) -> object: """The main building part's wall construction, or None when no part lodged.""" parts = comparable.epc.sap_building_parts return parts[0].wall_construction if parts else None + + +def _main_construction_age_band(comparable: ComparableProperty) -> object: + """The main building part's Table S1 band letter, or None when no part lodged.""" + parts = comparable.epc.sap_building_parts + return parts[0].construction_age_band if parts else None + + +def _main_fuel_type(comparable: ComparableProperty) -> object: + """The primary heating fuel code, or None when no main heating lodged.""" + details = comparable.epc.sap_heating.main_heating_details + return details[0].main_fuel_type if details else None