Model/scripts/corpus_1000/why_diverge.py
Jun-te Kim 58125503f0 chore(corpus): why_diverge.py — categorize all 223 diverging certs by reason
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 05:19:20 +00:00

76 lines
3.7 KiB
Python

"""Categorize every corpus cert outside +-0.5 SAP by the REASON it diverges,
combining the computed cost-vs-demand signature, the electric heating code /
meter, and the ledger's RR/multipart tokens."""
from __future__ import annotations
import json, re
from pathlib import Path
from collections import defaultdict
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, _is_electric_main,
_first_main_heating)
CORPUS = Path("backend/epc_api/json_samples/RdSAP-Schema-21.0.1/corpus.jsonl")
# ledger signatures (for RR / multipart tokens)
sig_of={}; cur=""
for l in Path("scripts/corpus_1000/worklist.md").read_text().splitlines():
if l.startswith('### C'):
m=re.search(r'`([^`]*)`',l); cur=m.group(1) if m else ""
m=re.match(r'^- .*?`([^`]+)`',l)
if m: sig_of[m.group(1)]=cur
def bucket(doc, dsap, dpe, dco2, code, mt, sig):
rr = 'RR' in (sig or '')
elec = code in range(191,600) if code else False
demand = dpe is not None and abs(dpe) >= 4 and (dpe>0)==(dsap<0)
cost = dpe is not None and abs(dpe) < 4
# 1. electric storage-boiler / CPSU on off-peak (spec §12 Rule 2/1)
if code in (192,193,194,195,196) and dsap>0:
return "elec storage/CPSU boiler → §12 off-peak (spec-faithful; lodged over-billed)"
# 2. electric direct + storage heaters, tariff/cost
if elec and cost:
return "electric tariff/cost (meter-column & high-rate fraction; spec-faithful vs lodged)"
if elec and demand:
return "electric + fabric/demand mismatch"
if elec:
return "electric other"
# 3. RR
if rr:
return "room-in-roof surface geometry (spec-mandated, worksheet-validated; high variance)"
# 4. non-elec demand/fabric
if demand:
return "non-electric fabric/demand (per-cert: glazing area, lodged geometry, mixed walls)"
if cost:
return "non-electric cost (secondary fuel / cylinder / tariff)"
return "unclassified"
cnt=defaultdict(list)
band=defaultdict(int)
for line in CORPUS.read_text().splitlines():
doc=json.loads(line); L=doc.get('energy_rating_current')
if L is None: continue
try:
epc=EpcPropertyDataMapper.from_api_response(doc)
r=calculate_sap_from_inputs(cert_to_inputs(epc, prices=SAP_10_2_SPEC_PRICES))
d=calculate_sap_from_inputs(cert_to_demand_inputs(epc, prices=SAP_10_2_SPEC_PRICES))
except Exception: continue
dsap=r.sap_score_continuous-L
if abs(dsap)<0.5: continue
peL=doc.get('energy_consumption_current'); co2L=doc.get('co2_emissions_current')
dpe=(d.primary_energy_kwh_per_m2-peL) if peL is not None else None
dco2=(d.co2_kg_per_yr/1000-co2L) if co2L is not None else None
sh=doc.get('sap_heating') or {}; det=(sh.get('main_heating_details') or [{}])[0]
code=det.get('sap_main_heating_code'); mt=(doc.get('sap_energy_source') or {}).get('meter_type')
b=bucket(doc,dsap,dpe,dco2,code,mt,sig_of.get(str(doc.get('uprn')),''))
cnt[b].append((doc.get('uprn'),L,round(r.sap_score_continuous,1),round(dsap,1)))
ad=abs(dsap); band['0.5-1' if ad<1 else '1-2' if ad<2 else '2-4' if ad<4 else '4+']+=1
total=sum(len(v) for v in cnt.values())
print(f"TOTAL diverging (|Δ|≥0.5): {total}\n")
print("by |Δ| band:", dict(band), "\n")
print("=== by REASON (count, mean|Δ|, examples) ===")
for b,rows in sorted(cnt.items(), key=lambda x:-len(x[1])):
mabs=sum(abs(x[3]) for x in rows)/len(rows)
ex=", ".join(f"{x[0]}({x[3]:+.1f})" for x in sorted(rows,key=lambda x:-abs(x[3]))[:3])
print(f"\n[{len(rows):3d}] mean|Δ|={mabs:.1f} {b}\n e.g. {ex}")