mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-12 13:29:04 +00:00
One-time script (dry-run default, --apply in a transaction, idempotent) that NULLs the four lodged_* columns on every predicted-source baseline row — a predicted EPC exists and no lodged one does — leaving Effective, the bill block, and rebaseline_reason intact. Dry-run against the audited DB reports 12,236 rows. Must run after the FE-owned Drizzle ALTER ... DROP NOT NULL lands. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
111 lines
4 KiB
Python
111 lines
4 KiB
Python
"""One-time backfill: NULL the phantom Lodged Performance on predicted Properties.
|
|
|
|
#1361 Class B: ``PropertyBaselineOrchestrator`` used to read Lodged Performance
|
|
off a predicted Property's neighbour-synthesised EPC, persisting a phantom set of
|
|
``lodged_*`` figures on ``property_baseline_performance`` — a *different
|
|
dwelling's* SAP / band / carbon / Primary Energy Intensity presented as this
|
|
Property's government-register record. The orchestrator no longer does this
|
|
(``lodged`` is ``None`` when ``source_path == "predicted"``, ADR-0004 amendment),
|
|
but the ~12k rows written before the fix still carry the phantom.
|
|
|
|
This NULLs the four ``lodged_*`` columns on every predicted-source baseline row,
|
|
leaving the **Effective** half, the **bill** block, and ``rebaseline_reason``
|
|
untouched (a predicted Property's Effective Performance is correct — it is a
|
|
first-class modelled output).
|
|
|
|
A predicted-source Property is identified exactly as the orchestrator's
|
|
``source_path == "predicted"``: it has a **predicted** EPC and **no lodged** EPC.
|
|
Site Notes keep their as-surveyed Lodged Performance and so are excluded — though
|
|
no Site-Notes-sourced EPC exists in ``epc_property`` today, the predicate (a
|
|
predicted EPC, no lodged EPC) holds regardless.
|
|
|
|
DRY-RUN BY DEFAULT: prints the count it would change and writes nothing. Pass
|
|
``--apply`` to execute inside a transaction. **Idempotent** — only rows whose
|
|
``lodged_sap_score`` is still non-NULL are touched, so a second run is a no-op.
|
|
|
|
Requires the FE-owned Drizzle ``ALTER ... DROP NOT NULL`` on the four ``lodged_*``
|
|
columns to have landed first; without it the UPDATE to NULL violates the
|
|
constraint.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
|
|
from sqlalchemy import Connection, text
|
|
|
|
from scripts.e2e_common import build_engine, load_env
|
|
|
|
# A predicted-source baseline row: a predicted EPC exists for the property and no
|
|
# lodged one does (``source_path == "predicted"``). ``lodged_sap_score IS NOT
|
|
# NULL`` makes it idempotent — a row already nulled is skipped on a re-run.
|
|
_PREDICTED_PHANTOM_PREDICATE = """
|
|
pbp.lodged_sap_score IS NOT NULL
|
|
AND EXISTS (
|
|
SELECT 1 FROM epc_property e
|
|
WHERE e.property_id = pbp.property_id AND e.source = 'predicted'
|
|
)
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM epc_property e
|
|
WHERE e.property_id = pbp.property_id AND e.source = 'lodged'
|
|
)
|
|
"""
|
|
|
|
_COUNT = text(
|
|
f"""
|
|
SELECT count(*) FROM property_baseline_performance pbp
|
|
WHERE {_PREDICTED_PHANTOM_PREDICATE}
|
|
"""
|
|
)
|
|
|
|
_NULL_LODGED = text(
|
|
f"""
|
|
UPDATE property_baseline_performance AS pbp
|
|
SET lodged_sap_score = NULL,
|
|
lodged_epc_band = NULL,
|
|
lodged_co2_emissions_t_per_yr = NULL,
|
|
lodged_primary_energy_intensity_kwh_per_m2_yr = NULL
|
|
WHERE {_PREDICTED_PHANTOM_PREDICATE}
|
|
"""
|
|
)
|
|
|
|
|
|
def backfill(conn: Connection, *, apply: bool) -> int:
|
|
"""NULL the four ``lodged_*`` columns on predicted-source baseline rows.
|
|
|
|
Returns the number of phantom rows found (those that ``--apply`` would / did
|
|
null). Reads the count first so the dry-run reports it without writing.
|
|
"""
|
|
found = conn.execute(_COUNT).scalar() or 0
|
|
if apply:
|
|
conn.execute(_NULL_LODGED)
|
|
return found
|
|
|
|
|
|
def main() -> None:
|
|
load_env()
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument(
|
|
"--apply",
|
|
action="store_true",
|
|
help="execute the update (default: dry-run, writes nothing)",
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
engine = build_engine()
|
|
with engine.begin() as conn:
|
|
conn.execute(text("SET statement_timeout = 120000"))
|
|
found = backfill(conn, apply=args.apply)
|
|
|
|
verb = "NULLed" if args.apply else "would NULL"
|
|
print(
|
|
f"{verb} the Lodged Performance (lodged_* → NULL) on {found} "
|
|
"predicted-source baseline row(s); Effective / bill / rebaseline_reason "
|
|
"left intact."
|
|
)
|
|
if not args.apply:
|
|
print("\nDRY-RUN — nothing written. Re-run with --apply to execute.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|