Historic stable attributes condition cohort selection on the relax ladder 🟩

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 <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-07-06 08:26:00 +00:00
parent 248aa5ad6a
commit d2d696a1c5

View file

@ -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