mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-19 17:03:02 +00:00
property_type/built_form reuse the override code mapping; wall material prefix -> RdSAP wall_construction; E&W age-band strings -> Table S1 letters; main_fuel display text (stripping " (not community)") -> modern RdSAP-20/21 codes; TFA -> float. Unresolvable -> None (filter inactive). Roof construction is deferred: its RdSAP code table isn't pinned in-repo. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
123 lines
4.3 KiB
Python
123 lines
4.3 KiB
Python
"""Resolve an expired Historic EPC's *stable* attributes into the cohort code
|
|
spaces that condition EPC Prediction (ADR-0054).
|
|
|
|
The historic dump carries display text ("Semi-Detached", "Cavity wall, as
|
|
built, no insulation (assumed)"); the Comparable Properties cohort carries
|
|
gov-EPC codes. This module is the anti-corruption layer between the two: one
|
|
resolver per whitelisted stable attribute, each returning None on an
|
|
unresolvable value so its cohort filter is simply inactive. Volatile
|
|
attributes (heating system, hot water, glazing, PV, insulation states,
|
|
lighting) are deliberately absent — a 14+-year-old observation of them is not
|
|
evidence about today.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from typing import Optional
|
|
|
|
from datatypes.epc.domain.historic_epc import HistoricEpc
|
|
from domain.epc.property_overrides.override_code_mapping import (
|
|
built_form_to_code,
|
|
property_type_to_code,
|
|
)
|
|
|
|
# RdSAP `wall_construction` codes by material prefix — the same table
|
|
# `wall_type_overlay.py` pins (source: domain/sap10_ml/rdsap_uvalues.py).
|
|
# The historic description's material is everything before the first comma.
|
|
_WALL_MATERIAL_CONSTRUCTION: dict[str, int] = {
|
|
"Granite or whin": 1,
|
|
"Sandstone": 2,
|
|
"Solid brick": 3,
|
|
"Cavity wall": 4,
|
|
"Timber frame": 5,
|
|
"System built": 6,
|
|
"Cob": 7,
|
|
"Park home wall": 8,
|
|
}
|
|
|
|
# RdSAP Table S1 band letters — the code space cohort building parts carry
|
|
# (`sap_building_parts[].construction_age_band` is "A".."M" from the API).
|
|
# The old register lodged the full England-and-Wales display strings; a
|
|
# pre-2012 cert cannot carry the post-2012 bands, but they cost nothing.
|
|
_AGE_BAND_LETTERS: dict[str, str] = {
|
|
"England and Wales: before 1900": "A",
|
|
"England and Wales: 1900-1929": "B",
|
|
"England and Wales: 1930-1949": "C",
|
|
"England and Wales: 1950-1966": "D",
|
|
"England and Wales: 1967-1975": "E",
|
|
"England and Wales: 1976-1982": "F",
|
|
"England and Wales: 1983-1990": "G",
|
|
"England and Wales: 1991-1995": "H",
|
|
"England and Wales: 1996-2002": "I",
|
|
"England and Wales: 2003-2006": "J",
|
|
"England and Wales: 2007 onwards": "K",
|
|
"England and Wales: 2007-2011": "K",
|
|
"England and Wales: 2012-2022": "L",
|
|
"England and Wales: 2023 onwards": "M",
|
|
}
|
|
|
|
# Modern RdSAP-20/21 `main_fuel` codes (epc_codes.csv), keyed by the base fuel
|
|
# description — the same family `main_fuel_overlay.py` pins. The old register
|
|
# suffixes private fuels with " (not community)", stripped before lookup.
|
|
_FUEL_CODES: dict[str, int] = {
|
|
"mains gas": 26,
|
|
"mains gas (community)": 20,
|
|
"LPG": 27,
|
|
"bottled LPG": 3,
|
|
"LPG special condition": 17,
|
|
"oil": 28,
|
|
"electricity": 29,
|
|
"electricity (community)": 25,
|
|
"house coal": 33,
|
|
"smokeless coal": 15,
|
|
"dual fuel (mineral and wood)": 10,
|
|
"wood logs": 6,
|
|
"bulk wood pellets": 7,
|
|
"wood chips": 8,
|
|
"biomass (community)": 31,
|
|
}
|
|
|
|
_NOT_COMMUNITY_SUFFIX = " (not community)"
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class HistoricConditioning:
|
|
"""The expired cert's stable attributes, in cohort code space; None means
|
|
"unresolved — do not condition on this"."""
|
|
|
|
property_type: Optional[str]
|
|
built_form: Optional[str]
|
|
wall_construction: Optional[int]
|
|
construction_age_band: Optional[str]
|
|
main_fuel: Optional[int]
|
|
total_floor_area_m2: Optional[float]
|
|
|
|
|
|
def _wall_construction(description: str) -> Optional[int]:
|
|
material = description.split(",", 1)[0].strip()
|
|
return _WALL_MATERIAL_CONSTRUCTION.get(material)
|
|
|
|
|
|
def _main_fuel(description: str) -> Optional[int]:
|
|
base = description.strip().removesuffix(_NOT_COMMUNITY_SUFFIX)
|
|
return _FUEL_CODES.get(base)
|
|
|
|
|
|
def _floor_area(raw: str) -> Optional[float]:
|
|
try:
|
|
area = float(raw)
|
|
except ValueError:
|
|
return None
|
|
return area if area > 0 else None
|
|
|
|
|
|
def conditioning_from_historic(record: HistoricEpc) -> HistoricConditioning:
|
|
return HistoricConditioning(
|
|
property_type=property_type_to_code(record.property_type),
|
|
built_form=built_form_to_code(record.built_form),
|
|
wall_construction=_wall_construction(record.walls_description),
|
|
construction_age_band=_AGE_BAND_LETTERS.get(record.construction_age_band),
|
|
main_fuel=_main_fuel(record.main_fuel),
|
|
total_floor_area_m2=_floor_area(record.total_floor_area),
|
|
)
|