Code a Manual-Entry electric "Direct acting" boiler to SAP 191 🟩

A PasHub Manual-Entry electric boiler lodges "Direct acting" in its "Heating
System (Boiler):" age-band cell. `_PASHUB_MANUAL_BOILER_AGE_BAND_TO_TABLE_4B`
covers only gas/liquid Table 4b rows, so the descriptor resolved to None and the
boiler stayed uncoded — taking the generic 0.80 gas-boiler seasonal efficiency
AND, on a Dual meter, `_table_12a_system_for_main`'s None-fallback that bills the
whole main-heating load at the off-peak LOW rate. The two errors pull opposite
ways, so an identical mis-mapping over-rated one dwelling and under-rated another
(fixture 497712825571 +9.5 on a Dual meter; 461386632387 -6.2 on a Single meter).

A direct-acting electric "boiler with radiators" is SAP 10.2 Table 4a code 191
(direct-acting electric boiler, efficiency 1.00, §12 Rule 3 tariff). Code it
directly in the manual-boiler resolver (gated on fuel Electricity + no PCDB
product), so the single code value drives the efficiency table, the Table 12a
pricing row and the tariff dispatch together. Both fixtures converge:
497712825571 → +0.6, 461386632387 → +0.4. LRHA within-0.5 52.4% → 53.4%,
MAE 0.835. Renamed `_pashub_manual_boiler_table_4b_code` → `_pashub_manual_
boiler_code` (it now also returns the Table 4a electric code).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-07-24 14:29:14 +00:00
parent 398692703f
commit 0012ad1625
2 changed files with 59 additions and 13 deletions

View file

@ -96,11 +96,17 @@ _MANIFEST_PATH = _FIXTURES_DIR / "manifest.json"
# code-691 dwellings. Routed the resolved `_rdsap_tariff` into the secondary cost
# path (mirroring the CO2 path, which already did this) → within-0.5 49.5% →
# 52.4%, MAE 0.977. Storage-heater dwellings (7-hour by §12) are unchanged.
# 2026-07-24 (accuracy tail audit): a Manual-Entry electric "Direct acting"
# boiler was left uncoded (Table 4b covers only gas/liquid), so it took the 0.80
# gas-boiler default AND, on a Dual meter, 100%-off-peak billing → ±6-9 mis-rate.
# Coded it to SAP Table 4a 191 (direct-acting electric boiler, eff 1.00): fixture
# 497712825571 +9.5 → +0.6, 461386632387 -6.2 → +0.4 → within-0.5 52.4% → 53.4%,
# MAE 0.835.
# Treat the constants as a regression tripwire, NOT a cohort accuracy figure;
# re-baseline (coverage growth is not loosening) as further mapper fixes unblock
# fixtures.
_MIN_WITHIN_HALF: float = 0.52
_MAX_SAP_MAE: float = 0.98
_MIN_WITHIN_HALF: float = 0.53
_MAX_SAP_MAE: float = 0.84
_KNOWN_GAP_REASON = (
"pashub `from_site_notes` mapper does not int-code a main-heating "
@ -161,6 +167,27 @@ def _evaluate(deal_id: str) -> Outcome:
)
@pytest.mark.skipif(
"497712825571" not in _BY_ID or "461386632387" not in _BY_ID,
reason="direct-acting electric boiler fixtures not in manifest",
)
@pytest.mark.parametrize("deal_id", ["497712825571", "461386632387"])
def test_direct_acting_electric_boiler_codes_to_191(deal_id: str) -> None:
"""Regression: a Manual-Entry electric "Direct acting" boiler is a
direct-acting electric wet system SAP Table 4a code 191 (efficiency 1.00,
§12 Rule 3 tariff). Left uncoded it took the generic 0.80 gas-boiler default
AND, on a Dual meter, billed its whole load at the off-peak low rate two
errors that pull opposite ways (fixture 497712825571 over-rated +9.5 on a
Dual meter; 461386632387 under-rated -6.2 on a Single meter). Coding it 191
fixes both (the code drives the efficiency table, the Table 12a pricing row
and the §12 tariff together): 497712825571 +9.5 +0.6, 461386632387
-6.2 +0.4. The bound is 1.0 (not 0.5) the fix removes the ±6-9 mis-code,
leaving only sub-point residuals within the cohort's normal spread."""
outcome = _evaluate(deal_id)
assert outcome.diff is not None
assert outcome.diff < 1.0, f"direct-acting electric boiler mis-coded: diff {outcome.diff:.2f}"
@pytest.mark.skipif(
"462024626384" not in _BY_ID, reason="fixture 462024626384 not in manifest"
)

View file

@ -8578,14 +8578,24 @@ def _pashub_main_heating_control_code(
return code
# SAP 10.2 Table 4a code 191 = "direct-acting electric boiler" (efficiency 1.00,
# §12 Rule 3 tariff dispatch). A Manual-Entry electric boiler lodges "Direct
# acting" in its "Heating System (Boiler):" age-band cell; Table 4b (gas/liquid
# only) does not cover it, so without an explicit code it stayed uncoded → billed
# at the generic 0.80 gas default AND, on a Dual meter, 100% off-peak low-rate — a
# large mis-rate in BOTH directions (over-rate on Dual, under-rate on Single).
_PASHUB_DIRECT_ACTING_ELECTRIC_BOILER_AGE_BAND: Final[str] = "Direct acting"
_SAP_DIRECT_ACTING_ELECTRIC_BOILER_CODE: Final[int] = 191
# PasHub Manual-Entry boiler descriptor → SAP 10.2 Table 4b sub-row code
# (`table_4b.py`), keyed on the surveyed "Heating System (Boiler):" cell
# (`boiler_age_band`), which carries both the boiler type and the pre/post-1998
# age band. A Manual-Entry boiler has no PCDB `product_id`, so its efficiency
# must come from Table 4b, not the generic 0.80 gas default (#1649 A). Only the
# cohort's observed gas/liquid descriptors are mapped; an unmapped descriptor
# (or an electric "Direct acting" boiler, which Table 4b does not cover) resolves
# to None, preserving the pre-existing behaviour (no block, no regression).
# cohort's observed gas/liquid descriptors are mapped; an unmapped gas/liquid
# descriptor resolves to None, preserving the pre-existing behaviour (no block,
# no regression). The electric "Direct acting" boiler is handled separately
# (Table 4a code 191, above) since it is not a Table 4b row.
_PASHUB_MANUAL_BOILER_AGE_BAND_TO_TABLE_4B: Dict[str, int] = {
# Gas boilers 1998 or later (Table 4b 101-109).
"1998 or later - Condensing combi": 104, # 84/75 winter/summer
@ -8599,14 +8609,22 @@ _PASHUB_MANUAL_BOILER_AGE_BAND_TO_TABLE_4B: Dict[str, int] = {
}
def _pashub_manual_boiler_table_4b_code(main: PasHubMainHeating) -> Optional[int]:
def _pashub_manual_boiler_code(main: PasHubMainHeating) -> Optional[int]:
"""Resolve a Manual-Entry boiler's surveyed descriptor to its SAP 10.2
Table 4b code (#1649 A). Returns None for a PCDF-Search boiler (product_id
lodged efficiency is the SEDBUK lookup) and for any descriptor the Table
4b map does not cover (e.g. an electric boiler), leaving the calculator's
existing cascade untouched for those."""
main-heating code. Returns None for a PCDF-Search boiler (product_id lodged
efficiency is the SEDBUK lookup) and for any gas/liquid descriptor the
Table 4b map does not cover, leaving the calculator's existing cascade
untouched. An electric "Direct acting" boiler is a direct-acting electric
wet system Table 4a code 191 (efficiency 1.00), NOT a Table 4b gas/liquid
row; without this it stayed uncoded and was billed at the 0.80 gas default
and (on a Dual meter) the off-peak low rate."""
if main.product_id > 0:
return None
if (
main.boiler_age_band == _PASHUB_DIRECT_ACTING_ELECTRIC_BOILER_AGE_BAND
and main.fuel == "Electricity"
):
return _SAP_DIRECT_ACTING_ELECTRIC_BOILER_CODE
return _PASHUB_MANUAL_BOILER_AGE_BAND_TO_TABLE_4B.get(main.boiler_age_band)
@ -8645,9 +8663,10 @@ def _pashub_main_heating_system_code(main: PasHubMainHeating) -> Optional[int]:
return code
if system in _PASHUB_BOILER_SYSTEM_TYPES:
# PCDF-Search boiler → None (SEDBUK product efficiency); Manual-Entry
# boiler → its Table 4b code (or None for an uncovered/non-Table-4b
# descriptor, keeping the pre-existing cascade) (#1649 A).
return _pashub_manual_boiler_table_4b_code(main)
# boiler → its Table 4b code, Table 4a 191 for an electric direct-acting
# boiler, or None for an uncovered descriptor (keeping the pre-existing
# cascade) (#1649 A).
return _pashub_manual_boiler_code(main)
return None