mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-27 23:35:01 +00:00
Map PasHub walls_insulation_type labels to SAP10 codes
`from_site_notes` copied the raw `walls_insulation_type` survey label onto
the building part; the calculator's `_int_or_none` read it as `None`, so
the ~101 filled-cavity walls in the cohort lost their lower-U credit and
were over-counted.
Add `_pashub_wall_insulation_type_int` (mirroring the wall-construction
helper and the Elmhurst `_ELMHURST_WALL_INSULATION_TO_SAP10` sibling)
mapping the surveyed labels to the SAP10 wall-insulation codes `u_wall`
consumes ("As built" -> 4 assumed/default, "Filled Cavity" -> 2,
"External" -> 1), strict-raising `UnmappedPasHubLabel` on an unknown label.
Stacked on #1560, this is the fabric fix that removes the cohort's
systematic SAP bias: mean signed -0.79 -> -0.04, MAE 3.11 -> 2.79,
within-0.5 8.5% -> 11.4%.
Closes #1561
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
aa338dd152
commit
ed15bd13f7
2 changed files with 74 additions and 5 deletions
|
|
@ -5939,7 +5939,9 @@ def _map_main_building_part(
|
|||
wall_construction=_pashub_wall_construction_int(
|
||||
main.walls_construction_type
|
||||
),
|
||||
wall_insulation_type=main.walls_insulation_type,
|
||||
wall_insulation_type=_pashub_wall_insulation_type_int(
|
||||
main.walls_insulation_type
|
||||
),
|
||||
wall_thickness_measured=main.wall_thickness_mm is not None,
|
||||
party_wall_construction=_pashub_party_wall_construction_int(
|
||||
main.party_wall_construction_type
|
||||
|
|
@ -5967,7 +5969,9 @@ def _map_extension_building_part(
|
|||
wall_construction=_pashub_wall_construction_int(
|
||||
ext_c.walls_construction_type
|
||||
),
|
||||
wall_insulation_type=ext_c.walls_insulation_type,
|
||||
wall_insulation_type=_pashub_wall_insulation_type_int(
|
||||
ext_c.walls_insulation_type
|
||||
),
|
||||
wall_thickness_measured=ext_c.wall_thickness_mm is not None,
|
||||
party_wall_construction=_pashub_party_wall_construction_int(
|
||||
ext_c.party_wall_construction_type
|
||||
|
|
@ -7160,6 +7164,33 @@ def _pashub_wall_construction_int(label: Optional[str]) -> Union[int, str]:
|
|||
return _PASHUB_WALL_CONSTRUCTION_TO_SAP10[key]
|
||||
|
||||
|
||||
# PasHub surveyed `walls_insulation_type` label → SAP10 wall-insulation code that
|
||||
# `u_wall` consumes. Same target code-space as the Elmhurst sibling
|
||||
# (`_ELMHURST_WALL_INSULATION_TO_SAP10`) — PasHub just lodges the full human
|
||||
# label. "As built" is the assumed/default cascade code (Elmhurst "A"=4).
|
||||
_PASHUB_WALL_INSULATION_TYPE_TO_SAP10: Dict[str, int] = {
|
||||
"As built": 4, # as built / assumed (default cascade)
|
||||
"Filled Cavity": 2, # WALL_INSULATION_FILLED_CAVITY
|
||||
"External": 1, # external wall insulation
|
||||
}
|
||||
|
||||
|
||||
def _pashub_wall_insulation_type_int(label: Optional[str]) -> Union[int, str]:
|
||||
"""Resolve a PasHub surveyed `walls_insulation_type` label to its SAP10
|
||||
wall-insulation code at the mapper boundary. A blank label is "no lodging" and
|
||||
passes through as the empty string (the field is `Union[int, str]`; the
|
||||
calculator's `_int_or_none` resolves a non-numeric string to None); a non-empty
|
||||
label the lookup does not cover strict-raises `UnmappedPasHubLabel` so the
|
||||
coverage gap is fixed here rather than silently dropping the filled-cavity
|
||||
U-value credit downstream (#1561)."""
|
||||
if label is None or not label.strip():
|
||||
return ""
|
||||
key = label.strip()
|
||||
if key not in _PASHUB_WALL_INSULATION_TYPE_TO_SAP10:
|
||||
raise UnmappedPasHubLabel("wall insulation type", label)
|
||||
return _PASHUB_WALL_INSULATION_TYPE_TO_SAP10[key]
|
||||
|
||||
|
||||
def _resolve_elmhurst_underfloor_subtype(
|
||||
main_floor: ElmhurstFloorDetails,
|
||||
main_age_band: str,
|
||||
|
|
|
|||
|
|
@ -113,6 +113,44 @@ class TestWallConstructionCoding:
|
|||
EpcPropertyDataMapper.from_site_notes(survey)
|
||||
|
||||
|
||||
class TestWallInsulationTypeCoding:
|
||||
"""`from_site_notes` must int-code the PAS Hub `walls_insulation_type` label
|
||||
to the SAP10 wall-insulation code `u_wall` consumes (matching the Elmhurst
|
||||
`_ELMHURST_WALL_INSULATION_TO_SAP10` sibling), not copy the raw label. A raw
|
||||
label reads as `None`, so a filled cavity loses its lower-U credit and the
|
||||
wall is over-counted (Guinness cohort, issue #1561).
|
||||
"""
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"label, expected_code",
|
||||
[
|
||||
("As built", 4), # as built / assumed (default cascade)
|
||||
("Filled Cavity", 2), # WALL_INSULATION_FILLED_CAVITY
|
||||
("External", 1), # external wall insulation
|
||||
("", ""), # no lodging — passes through (calc nulls it via _int_or_none)
|
||||
],
|
||||
)
|
||||
def test_label_maps_to_sap_code(
|
||||
self, label: str, expected_code: "int | str"
|
||||
) -> None:
|
||||
data = load("pashub_rdsap_site_notes_example1.json")
|
||||
data["building_construction"]["main_building"][
|
||||
"walls_insulation_type"
|
||||
] = label
|
||||
survey = from_dict(PasHubRdSapSiteNotes, data)
|
||||
result = EpcPropertyDataMapper.from_site_notes(survey)
|
||||
assert result.sap_building_parts[0].wall_insulation_type == expected_code
|
||||
|
||||
def test_unknown_label_strict_raises(self) -> None:
|
||||
data = load("pashub_rdsap_site_notes_example1.json")
|
||||
data["building_construction"]["main_building"][
|
||||
"walls_insulation_type"
|
||||
] = "Aerogel Blanket"
|
||||
survey = from_dict(PasHubRdSapSiteNotes, data)
|
||||
with pytest.raises(UnmappedPasHubLabel):
|
||||
EpcPropertyDataMapper.from_site_notes(survey)
|
||||
|
||||
|
||||
class TestFromSiteNotesExample1:
|
||||
"""
|
||||
Fixture: pashub_rdsap_site_notes_example1.json
|
||||
|
|
@ -302,8 +340,8 @@ class TestFromSiteNotesExample1:
|
|||
assert result.sap_building_parts[0].wall_construction == 4
|
||||
|
||||
def test_wall_insulation_type(self, result: EpcPropertyData) -> None:
|
||||
# main_building.walls_insulation_type: "As built"
|
||||
assert result.sap_building_parts[0].wall_insulation_type == "As built"
|
||||
# main_building.walls_insulation_type: "As built" → SAP10 code 4 (as built/assumed)
|
||||
assert result.sap_building_parts[0].wall_insulation_type == 4
|
||||
|
||||
def test_wall_thickness_measured(self, result: EpcPropertyData) -> None:
|
||||
# main_building.wall_thickness_mm: 280 → thickness was measured
|
||||
|
|
@ -549,7 +587,7 @@ class TestFromSiteNotesExample1:
|
|||
identifier=BuildingPartIdentifier.MAIN,
|
||||
construction_age_band="I",
|
||||
wall_construction=4, # "Cavity" → SAP10 code 4 (WALL_CAVITY)
|
||||
wall_insulation_type="As built",
|
||||
wall_insulation_type=4, # "As built" → SAP10 code 4 (as built/assumed)
|
||||
wall_thickness_measured=True,
|
||||
party_wall_construction=4, # "Cavity Masonry, Unfilled" → SAP10 code 4 (U=0.5)
|
||||
sap_floor_dimensions=[
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue