Age band conditions within one band; the floor-area band widens to ±20% 🟩

Evidence (439-pair harness, PR #1466): historic-vs-new age band agrees
52% exactly but 90% within one band (assessors re-band, skewing newer);
TFA agrees 45% within ±5% but 82% within ±20%. Equality/±5% steered the
cohort toward stale values where they engaged and relaxed everywhere
else. Band definitions are public so the harness's ladder replay shares
one source of truth.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-07-06 11:08:03 +00:00
parent 355c1e7d2f
commit 38661c6bd7
2 changed files with 29 additions and 8 deletions

View file

@ -22,8 +22,25 @@ from domain.geospatial.coordinates import Coordinates
_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
# observed floor area keeps comparables within ±20% of it — a coarse
# dwelling-size filter, not a precision match. Evidence (ADR-0054 amendment,
# 439-pair harness): the historic TFA agrees with the newly lodged one within
# ±5% only 45% of the time (remeasurement + extensions) but within ±20% 82%.
FLOOR_AREA_TOLERANCE = 0.20
# RdSAP Table S1 band letters in chronological order, for band-distance checks.
_AGE_BAND_ORDER = "ABCDEFGHIJKLM"
def age_bands_within_one(candidate: object, target_band: object) -> bool:
"""Whether two Table S1 band letters are at most one band apart. Assessors
re-band constantly (harness: 52% exact agreement historic-vs-new, 90%
within one band), so the age filter keeps the band NEIGHBOURHOOD."""
if not (isinstance(candidate, str) and isinstance(target_band, str)):
return False
if candidate not in _AGE_BAND_ORDER or target_band not in _AGE_BAND_ORDER:
return False
return abs(_AGE_BAND_ORDER.index(candidate) - _AGE_BAND_ORDER.index(target_band)) <= 1
@dataclass(frozen=True)
@ -94,7 +111,9 @@ def select_comparables(
)
cohort = _maybe_filter(
cohort,
lambda c: _main_construction_age_band(c) == target.construction_age_band,
lambda c: age_bands_within_one(
_main_construction_age_band(c), target.construction_age_band
),
active=target.construction_age_band is not None,
minimum_cohort=minimum_cohort,
)
@ -109,7 +128,7 @@ def select_comparables(
cohort,
lambda c: target_area is not None
and abs(c.epc.total_floor_area_m2 - target_area)
<= _FLOOR_AREA_TOLERANCE * target_area,
<= FLOOR_AREA_TOLERANCE * target_area,
active=target_area is not None,
minimum_cohort=minimum_cohort,
)

View file

@ -43,7 +43,9 @@ sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from datatypes.epc.domain.epc_property_data import EpcPropertyData # noqa: E402
from datatypes.epc.domain.historic_epc import HistoricEpc # noqa: E402
from domain.epc_prediction.comparable_properties import ( # noqa: E402
FLOOR_AREA_TOLERANCE,
ComparableProperty,
age_bands_within_one,
select_comparables,
)
from domain.epc_prediction.epc_prediction import EpcPrediction # noqa: E402
@ -226,7 +228,7 @@ def _actual_fuel(epc: EpcPropertyData) -> Optional[object]:
def _tfa_within_band(actual_tfa: float, hist_tfa: Optional[float]) -> Optional[bool]:
if hist_tfa is None:
return None
return abs(actual_tfa - hist_tfa) <= 0.05 * hist_tfa
return abs(actual_tfa - hist_tfa) <= FLOOR_AREA_TOLERANCE * hist_tfa
@dataclass(frozen=True)
@ -267,7 +269,7 @@ def simulate_conditioning_ladder(
apply(
"construction_age_band",
age_band is not None,
[c for c in cohort if _actual_age_band(c.epc) == age_band],
[c for c in cohort if age_bands_within_one(_actual_age_band(c.epc), age_band)],
)
apply(
"main_fuel",
@ -282,7 +284,7 @@ def simulate_conditioning_ladder(
for c in cohort
if total_floor_area_m2 is not None
and abs(c.epc.total_floor_area_m2 - total_floor_area_m2)
<= 0.05 * total_floor_area_m2
<= FLOOR_AREA_TOLERANCE * total_floor_area_m2
],
)
return steps
@ -321,7 +323,7 @@ def format_diagnosis(rows: list[dict[str, object]]) -> str:
"wall_construction",
"construction_age_band",
"main_fuel",
"tfa_within_5pct",
"tfa_within_band",
)
lines += [
"",