chore(scripts): triage PE/CO2 on the demand cascade in the corpus profilers

`profile_corpus_error.py` and `dive_cert.py` compared our PE/CO2 against
the lodged EPC figures using the UK-average RATING cascade, but the EPC
lodges CO2/PE on the postcode DEMAND cascade (SAP 10.2 Appendix U p.124,
now wired into Sap10Calculator.calculate in fc7c4d2d). That confounded the
DEMAND-vs-COST triage: a cert whose demand actually reproduced on local
weather looked "PE off" purely from the climate difference and was
mislabelled DEMAND-side. Switching the PE/CO2 lens to `cert_to_demand_
inputs` (SAP still from the rating cascade) re-classifies the corpus
outside-0.5 set 261/42 -> 211/92 DEMAND/COST — ~50 certs are genuinely
cost-side (e.g. 10091578598: SAP +7.81 but PE +1.6 / CO2 -0.04). Sharpens
the hunt for the subtle widespread SAP term.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-06-18 14:21:00 +00:00
parent fc7c4d2d3b
commit 6950deae06
2 changed files with 21 additions and 7 deletions

View file

@ -17,6 +17,7 @@ from datatypes.epc.domain.mapper import EpcPropertyDataMapper
from domain.sap10_calculator.calculator import calculate_sap_from_inputs
from domain.sap10_calculator.rdsap.cert_to_inputs import (
SAP_10_2_SPEC_PRICES,
cert_to_demand_inputs,
cert_to_inputs,
)
from scripts.profile_api_error import features
@ -40,6 +41,10 @@ def _dump(doc: dict[str, Any]) -> None:
lodged_pe = doc.get("energy_consumption_current")
epc = EpcPropertyDataMapper.from_api_response(doc)
r = calculate_sap_from_inputs(cert_to_inputs(epc, prices=SAP_10_2_SPEC_PRICES))
# SAP/EI rating is the UK-average rating cascade (`r`); EPC CO2/PE use the
# postcode demand cascade (SAP 10.2 Appendix U p.124). Display CO2/PE from
# the demand cascade so they compare like-for-like with the lodged EPC.
d = calculate_sap_from_inputs(cert_to_demand_inputs(epc, prices=SAP_10_2_SPEC_PRICES))
print("=" * 90)
print(f"CERT {cert}")
print(
@ -48,13 +53,13 @@ def _dump(doc: dict[str, Any]) -> None:
)
if lodged_co2 is not None:
print(
f" CO2 lodged={lodged_co2:.3f} ours={r.co2_kg_per_yr / 1000:.3f} t "
f"d={r.co2_kg_per_yr / 1000 - lodged_co2:+.3f}"
f" CO2 lodged={lodged_co2:.3f} ours={d.co2_kg_per_yr / 1000:.3f} t "
f"d={d.co2_kg_per_yr / 1000 - lodged_co2:+.3f} (demand cascade)"
)
if lodged_pe is not None:
print(
f" PE lodged={lodged_pe:.1f} ours={r.primary_energy_kwh_per_m2:.1f} "
f"d={r.primary_energy_kwh_per_m2 - lodged_pe:+.1f} kWh/m2"
f" PE lodged={lodged_pe:.1f} ours={d.primary_energy_kwh_per_m2:.1f} "
f"d={d.primary_energy_kwh_per_m2 - lodged_pe:+.1f} kWh/m2 (demand cascade)"
)
print(
f" energy kWh/yr: spaceheat={r.space_heating_kwh_per_yr:.0f} "

View file

@ -37,6 +37,7 @@ from typing import Any, Optional
from datatypes.epc.domain.mapper import EpcPropertyDataMapper
from domain.sap10_calculator.calculator import calculate_sap_from_inputs
from domain.sap10_calculator.rdsap.cert_to_inputs import (
cert_to_demand_inputs,
SAP_10_2_SPEC_PRICES,
cert_to_inputs,
)
@ -96,6 +97,14 @@ def _compute(corpus: list[dict[str, Any]]) -> tuple[list[Row], int, int]:
result = calculate_sap_from_inputs(
cert_to_inputs(epc, prices=SAP_10_2_SPEC_PRICES)
)
# SAP/EI rating is the UK-average rating cascade (`result`);
# the EPC-displayed CO2/PE use the postcode demand cascade
# (SAP 10.2 Appendix U p.124). Use the demand cascade for the
# PE/CO2-vs-cost triage so it is not confounded by the climate
# difference (UK-average vs local weather).
demand = calculate_sap_from_inputs(
cert_to_demand_inputs(epc, prices=SAP_10_2_SPEC_PRICES)
)
except Exception:
raised += 1
continue
@ -110,14 +119,14 @@ def _compute(corpus: list[dict[str, Any]]) -> tuple[list[Row], int, int]:
rows.append(Row(
cert=cert,
sap_err=result.sap_score_continuous - lodged_sap,
co2_err_t=(result.co2_kg_per_yr / 1000.0 - lodged_co2_t)
co2_err_t=(demand.co2_kg_per_yr / 1000.0 - lodged_co2_t)
if lodged_co2_t is not None else None,
pe_err=(result.primary_energy_kwh_per_m2 - lodged_pe)
pe_err=(demand.primary_energy_kwh_per_m2 - lodged_pe)
if lodged_pe is not None else None,
lodged_sap=lodged_sap,
our_sap=result.sap_score_continuous,
lodged_pe=lodged_pe,
our_pe=result.primary_energy_kwh_per_m2,
our_pe=demand.primary_energy_kwh_per_m2,
feats=features(doc),
))
return rows, skipped, raised