"""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}")