mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-12 13:29:04 +00:00
Captures the calc-facing ventilation-type gap (older RdSAP mappers drop mechanical_ventilation_kind), the FE-side Main Fuel display, the sweep survivor clusters, and the predicted-property display path — all separate from ADR-0037. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
125 lines
5 KiB
Python
125 lines
5 KiB
Python
"""Local population sanity sweep for the full-SAP baseline-downgrade fix (ADR-0037).
|
|
|
|
For a sample of Case A properties (portfolio 796, full-SAP path: epc_property
|
|
assessment_type+sap_version NULL), re-map the real cert with the NEW mapper, run
|
|
the SAP-10.2 calculator, and compute the post-fix Effective SAP/band. Compare to
|
|
the lodged figures to (a) confirm the fix flips Effective off the stale lodged
|
|
value and (b) flag survivors — drops too large to be a 2012→10.2 methodology
|
|
shift (>15 SAP or >=2 bands below lodged), candidate deeper-calc/cert bugs.
|
|
|
|
Read-only. No DB writes.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import random # noqa: F401 — only used with a fixed seed below
|
|
|
|
from scripts.e2e_common import load_env, build_engine
|
|
from sqlalchemy import text
|
|
|
|
load_env()
|
|
|
|
from infrastructure.epc_client.epc_client_service import EpcClientService
|
|
from datatypes.epc.domain.mapper import EpcPropertyDataMapper
|
|
from domain.sap10_calculator.calculator import Sap10Calculator
|
|
from datatypes.epc.domain.epc import Epc
|
|
|
|
_BAND_IDX = {b: i for i, b in enumerate(["G", "F", "E", "D", "C", "B", "A"])}
|
|
|
|
|
|
def _band_letter(epc_band: object) -> str:
|
|
s = str(epc_band)
|
|
return s.split(".")[-1] if "." in s else s
|
|
|
|
|
|
def main() -> None:
|
|
eng = build_engine()
|
|
with eng.connect() as c:
|
|
# 30 worst old-drops + 30 spread across the cohort (deterministic order).
|
|
worst = c.execute(text("""
|
|
SELECT p.uprn, pbp.lodged_sap_score, pbp.lodged_epc_band,
|
|
pbp.effective_sap_score AS old_eff, pl.post_sap_points AS old_post
|
|
FROM property p
|
|
JOIN epc_property ep ON ep.uprn=p.uprn
|
|
JOIN property_baseline_performance pbp ON pbp.property_id=p.id
|
|
JOIN plan pl ON pl.property_id=p.id
|
|
WHERE p.portfolio_id=796 AND ep.assessment_type IS NULL
|
|
AND ep.sap_version IS NULL
|
|
ORDER BY (pbp.effective_sap_score - pl.post_sap_points) DESC
|
|
LIMIT 30
|
|
""")).fetchall()
|
|
spread = c.execute(text("""
|
|
SELECT p.uprn, pbp.lodged_sap_score, pbp.lodged_epc_band,
|
|
pbp.effective_sap_score AS old_eff, pl.post_sap_points AS old_post
|
|
FROM property p
|
|
JOIN epc_property ep ON ep.uprn=p.uprn
|
|
JOIN property_baseline_performance pbp ON pbp.property_id=p.id
|
|
JOIN plan pl ON pl.property_id=p.id
|
|
WHERE p.portfolio_id=796 AND ep.assessment_type IS NULL
|
|
AND ep.sap_version IS NULL AND (p.uprn % 7) = 0
|
|
ORDER BY p.uprn LIMIT 30
|
|
""")).fetchall()
|
|
|
|
rows = {r._mapping["uprn"]: dict(r._mapping) for r in worst}
|
|
for r in spread:
|
|
rows.setdefault(r._mapping["uprn"], dict(r._mapping))
|
|
|
|
svc = EpcClientService(os.environ["OPEN_EPC_API_TOKEN"])
|
|
calc = Sap10Calculator()
|
|
|
|
n = 0
|
|
flipped = 0
|
|
calc_errors = 0
|
|
survivors = []
|
|
drops = []
|
|
for uprn, row in rows.items():
|
|
n += 1
|
|
try:
|
|
results = svc._search(uprn=int(uprn))
|
|
if not results:
|
|
print(f" uprn={uprn} NO_CERT")
|
|
continue
|
|
latest = max(results, key=lambda x: x.registration_date)
|
|
raw = svc._fetch_certificate(latest.certificate_number)
|
|
epc = EpcPropertyDataMapper.from_api_response(raw)
|
|
new_sap = calc.calculate(epc).sap_score
|
|
except Exception as e: # noqa: BLE001 — sweep tolerates per-cert failure
|
|
calc_errors += 1
|
|
print(f" uprn={uprn} CALC_ERROR {type(e).__name__}: {str(e)[:90]}")
|
|
continue
|
|
new_band = _band_letter(Epc.from_sap_score(new_sap))
|
|
lodged = row["lodged_sap_score"]
|
|
lodged_band = row["lodged_epc_band"]
|
|
drop = lodged - new_sap
|
|
drops.append(drop)
|
|
if epc.sap_version is not None and epc.sap_version < 10.2:
|
|
flipped += 1
|
|
band_drop = _BAND_IDX.get(lodged_band, 0) - _BAND_IDX.get(new_band, 0)
|
|
survivor = drop > 15 or band_drop >= 2
|
|
tag = " *** SURVIVOR" if survivor else ""
|
|
if survivor:
|
|
survivors.append((uprn, lodged, lodged_band, new_sap, new_band, drop))
|
|
print(
|
|
f" uprn={uprn} lodged={lodged}/{lodged_band} "
|
|
f"new_eff={new_sap}/{new_band} drop={drop} "
|
|
f"(old_eff={row['old_eff']} old_post={round(float(row['old_post']),1)}){tag}"
|
|
)
|
|
|
|
computed = len(drops)
|
|
print("\n==== SUMMARY ====")
|
|
print(f"sampled={n} computed={computed} calc_errors={calc_errors}")
|
|
print(f"rebaseline flipped (sap_version<10.2 now set) = {flipped}/{computed}")
|
|
if drops:
|
|
drops_sorted = sorted(drops)
|
|
print(
|
|
f"lodged-new_eff drop: min={min(drops)} median="
|
|
f"{drops_sorted[len(drops_sorted)//2]} max={max(drops)} "
|
|
f"mean={round(sum(drops)/len(drops),1)}"
|
|
)
|
|
print(f"survivors (>15 SAP or >=2 bands below lodged) = {len(survivors)}")
|
|
for s in survivors:
|
|
print(f" SURVIVOR uprn={s[0]} lodged={s[1]}/{s[2]} new={s[3]}/{s[4]} drop={s[5]}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|