mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-12 13:29:04 +00:00
Added scripts/corpus_1000/demand_cohort.py (isolates fabric/demand-driven divergences from cost noise). Investigation conclusion: the demand-side tail is one-offs, not a cluster. 12109191 (+13.1) is a LODGED decimal error (365.67 m² floor, 10x its ground floor). 217060420 (-4.4) is spec-faithful (36.6 m2 lodged glazing at spec U). The only >=3 structural clusters among diverging certs are electric flats (already diagnosed spec-faithful). No generic fabric bug remains; gauge 77.7% is near its structural ceiling. Marked 12109191 unbuildable in ledger. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
58 lines
2.9 KiB
Python
58 lines
2.9 KiB
Python
"""Demand-driven divergence analyzer. Flags corpus certs where the SAP miss is
|
|
driven by FABRIC/DEMAND (PE & CO2 move with SAP), not cost — the fixable class.
|
|
Tags nbp (building parts) and room-in-roof so multi-part / RR bugs surface."""
|
|
from __future__ import annotations
|
|
import json
|
|
from pathlib import Path
|
|
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)
|
|
|
|
CORPUS = Path("backend/epc_api/json_samples/RdSAP-Schema-21.0.1/corpus.jsonl")
|
|
|
|
def has_rr(doc):
|
|
for bp in doc.get("sap_building_parts") or []:
|
|
r = bp.get("room_in_roof") or bp.get("rooms_in_roof")
|
|
if r: return True
|
|
for r in doc.get("roofs") or []:
|
|
if "room in roof" in (r.get("description") or "").lower(): return True
|
|
return False
|
|
|
|
rows = []
|
|
for line in CORPUS.read_text().splitlines():
|
|
doc = json.loads(line)
|
|
L = doc.get("energy_rating_current"); peL = doc.get("energy_consumption_current")
|
|
co2L = doc.get("co2_emissions_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
|
|
peE = getattr(d, "primary_energy_kwh_per_m2", None)
|
|
dpe = (peE - peL) if (peE is not None and peL is not None) else None
|
|
co2E = d.co2_kg_per_yr/1000 if d.co2_kg_per_yr is not None else None
|
|
dco2 = (co2E - co2L) if (co2E is not None and co2L is not None) else None
|
|
nbp = len(doc.get("sap_building_parts") or [])
|
|
rr = has_rr(doc)
|
|
# demand-driven: PE moves same direction as SAP-miss inverse (PE up -> SAP down)
|
|
# i.e. dpe and dsap have OPPOSITE signs, and |dpe| is material
|
|
demand = (dpe is not None and abs(dpe) >= 4 and (dpe > 0) == (dsap < 0))
|
|
rows.append((doc.get("uprn"), L, round(r.sap_score_continuous,1), round(dsap,2),
|
|
round(dpe,1) if dpe is not None else None,
|
|
round(dco2,3) if dco2 is not None else None, nbp, rr, demand))
|
|
|
|
demand_rows = [r for r in rows if r[8]]
|
|
demand_rows.sort(key=lambda x:-abs(x[3]))
|
|
print(f"diverging certs: {len(rows)} | demand-driven: {len(demand_rows)}")
|
|
mp = [r for r in demand_rows if r[6] > 1]
|
|
rr = [r for r in demand_rows if r[7]]
|
|
print(f" demand+multipart(nbp>1): {len(mp)} | demand+RR: {len(rr)}")
|
|
print(f"\n=== top demand-driven divergences ===")
|
|
print(f"{'uprn':13s} {'L':>3} {'E':>5} {'dSAP':>6} {'dPE':>6} {'dCO2':>6} {'nbp':>3} {'RR':>3}")
|
|
for r in demand_rows[:32]:
|
|
print(f"{str(r[0]):13s} {r[1]:3d} {r[2]:5.1f} {r[3]:+6.2f} {str(r[4]):>6} {str(r[5]):>6} {r[6]:3d} {'Y' if r[7] else '':>3}")
|