mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-12 13:29:04 +00:00
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:
parent
355c1e7d2f
commit
38661c6bd7
2 changed files with 29 additions and 8 deletions
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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 += [
|
||||
"",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue