Model/scripts/dive_cert.py
Khalim Conn-Kowlessar 6950deae06 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>
2026-06-18 14:21:00 +00:00

108 lines
4 KiB
Python

"""Deep-dive a single corpus cert: lodged vs computed SAP/CO2/PE + the full
intermediate line-ref dump + the mapped fabric/heat-loss inputs, so the
diverging line is visible WITHOUT an Elmhurst worksheet.
USAGE
PYTHONPATH=/workspaces/model python scripts/dive_cert.py <cert_number_or_substr>
PYTHONPATH=/workspaces/model python scripts/dive_cert.py --filter wall_insulation_type=3 [--n 8]
"""
from __future__ import annotations
import json
import sys
from pathlib import Path
from typing import Any
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
_CORPUS = Path("backend/epc_api/json_samples/RdSAP-Schema-21.0.1/corpus.jsonl")
def _cert_id(doc: dict[str, Any]) -> str:
return str(
doc.get("certificate_number")
or doc.get("lmk_key")
or doc.get("uprn")
or "?"
)
def _dump(doc: dict[str, Any]) -> None:
cert = _cert_id(doc)
lodged_sap = doc.get("energy_rating_current")
lodged_co2 = doc.get("co2_emissions_current")
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(
f" SAP lodged={lodged_sap} ours={r.sap_score_continuous:.2f} "
f"d={r.sap_score_continuous - (lodged_sap or 0):+.2f}"
)
if lodged_co2 is not None:
print(
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={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} "
f"main={r.main_heating_fuel_kwh_per_yr:.0f} "
f"sec={r.secondary_heating_fuel_kwh_per_yr:.0f} "
f"hw={r.hot_water_kwh_per_yr:.0f} light={r.lighting_kwh_per_yr:.0f} "
f"pumpfan={r.pumps_fans_kwh_per_yr:.0f}"
)
d = epc.__dict__
print(" --- key mapped inputs ---")
f = features(doc)
for k in (
"property_type", "built_form", "age_band", "main_sap_code",
"main_heat_cat", "main_fuel", "has_pcdb_main", "main_data_source",
"wall_construction", "wall_insulation_type", "roof_codes",
"roof_insulation_thickness", "whc", "water_fuel", "immersion_type",
"has_cylinder", "has_secondary", "has_pv", "mains_gas", "n_building_parts",
):
print(f" {k:26s}= {f.get(k)}")
print(" --- intermediate line refs ---")
inter = r.intermediate or {}
for k in sorted(inter):
print(f" {k:34s}= {inter[k]:.4f}")
def main() -> None:
docs = [json.loads(l) for l in _CORPUS.read_text().splitlines() if l.strip()]
if "--filter" in sys.argv:
spec = sys.argv[sys.argv.index("--filter") + 1]
key, _, val = spec.partition("=")
n = int(sys.argv[sys.argv.index("--n") + 1]) if "--n" in sys.argv else 6
hits = [d for d in docs if str(features(d).get(key)) == val]
print(f"{len(hits)} certs match {spec}; dumping first {n}")
for d in hits[:n]:
_dump(d)
return
target = sys.argv[1]
for d in docs:
if target in _cert_id(d):
_dump(d)
return
print(f"no cert matching {target}")
if __name__ == "__main__":
main()