Model/docs/baseline-downgrade-followups.md
Khalim Conn-Kowlessar 5fc8bd3b3d docs: record predicted-property display fixes (item 4) 🟪
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 10:54:53 +00:00

99 lines
5.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Follow-ups from the full-SAP baseline-downgrade work (ADR-0037)
Open items surfaced while fixing the full-SAP mapper / portfolio-796 downgrades.
Each is **separate from** the full-SAP fix (`fix/baseline-downgrades`).
## 1. RdSAP ventilation mapping is inconsistent across schemas
**Severity: accuracy bug for MEV/MVHR dwellings.** The mappers handle
`sap_ventilation` four different ways:
- `17_0` + full-SAP — build via `_sap_17_1_ventilation`, which **maps**
`mechanical_ventilation_kind`. ✅
- `21_0_1` — rich inline `SapVentilation(...)` incl. the kind. ✅
- **`17_1` / `18_0` / `20_0_0`** — built `SapVentilation(sheltered_sides=…)` and
**dropped** the kind. **Fixed in #1333** (mirror `_api_mechanical_ventilation_kind`).
- **`19_0` / `21_0_0`** — set **no `sap_ventilation` at all** → the
`EpcPropertyData` default (`None`). They dropped the **entire** ventilation
block (sheltered sides + kind), not just the kind. **Fixed in
`feat/rdsap-19-21-ventilation`**: both now construct `SapVentilation(...)`
mirroring 21.0.1, but only for the fields each schema lodges — the top-level
`mechanical_ventilation` enum (→ kind) and `sheltered_sides` (from
`built_form`). Neither lodges the fan/flue/vent counts, so those stay unset
(RdSAP Table-5 age defaults — see counts note below).
Either way, an MEV/MVHR cert (`mechanical_ventilation ≠ 0`) is treated as
**natural** by the affected mappers — wrong §2 ventilation cascade (and heat
recovery). Natural certs (code `0`/`5``None`) are unaffected.
- For **natural-ventilation** certs (`mechanical_ventilation = 0`, e.g. UPRN
100020603823 / property 726605) it's **benign** — unmapped → `None` defaults to
natural in the §2 cascade.
- For **MEV/MVHR** certs (`mechanical_ventilation ≠ 0`) the calculator treats the
dwelling as **naturally ventilated**, getting the SAP §2 ventilation cascade
(and any heat recovery) wrong → mis-scored SAP.
The granular **counts** (fans/flues/vents) are *not* a bug: older RdSAP open-data
certs don't lodge them, and the calc correctly uses RdSAP Table-5 age defaults.
`percent_draughtproofed` is mapped (top-level) and read by the calc.
**19.0 / 21.0.0 fix (done, `feat/rdsap-19-21-ventilation`):** both build
`SapVentilation(sheltered_sides=…, mechanical_ventilation_kind=…)` mirroring
21.0.1. Guarded by the extended mapper-level MVHR test + a `sheltered_sides`
regression, with the corpus (held 73.3% / MAE 0.774 — it's 21.0.1-only so the
change isn't exercised by it) and the 6002-case mapper-corpus staying green.
**Outstanding SAP-accuracy fast-follow:** an **Elmhurst-anchored MEV/MVHR
`RealCertExpectation`** (the corpus is natural-vent-dominated, so the kind
change isn't exercised by it). **Blast radius (still to quantify):** count
portfolio-796 certs on schema 19.0 / 21.0.0 with `mechanical_ventilation ≠ 0`.
## 2. FE "Main Fuel: Unknown" is FE-side, not a Model mapper gap
`main_fuel_type` (the gov fuel **code**) **is** populated Model-side — UPRN
10093412452 (709791) → `1`, UPRN 100020603823 (726605) → `26` — and is persisted
(`epc_main_heating_detail.main_fuel_type`). So the panel's "Main Fuel: Unknown"
is the **front-end** either not mapping the code → fuel name or reading a field we
don't populate. Needs an FE-repo (Drizzle/Next) check, not a Model change.
## 3. Survivor clusters from the post-fix population sweep
`scripts/hyde/case_a_population_sweep.py`: the representative sample rebaselined
cleanly (0 survivors), but the worst-old-drop sample held **28 survivors**
(lodged A/B → new C/D, 1525 SAP) in tight UPRN clusters (new-build blocks),
spanning multiple schemas (16.0/16.1/17.1/18.0.0) and heating types. No single
mapper-gap signature → likely genuine SAP-2012→10.2 drops for very-high-lodged
new-builds, but **triage one in Elmhurst** to confirm genuine vs a residual calc
issue before trusting the cohort.
## 4. Predicted-property display path (e.g. property 721167)
721167 has **no lodged EPC** (predicted). Its Heating-Control / Main-Fuel /
Ventilation Unknowns come from the prediction synthesis path
(`domain/epc_prediction/epc_prediction.py`), **not** the lodged-cert mappers and
**not** the landlord-override overlay (`overlays_from` is simulation-time only,
ADR-0032 — it never writes the persisted/displayed EPC). Investigated and fixed
on `fix/predicted-property-display-unknowns`:
- **Heating Control — FIXED (Model).** `_apply_heating_donor` copied the donor's
*calc* `sap_heating` cluster but left the *display* rows (`main_heating`,
`main_heating_controls`) on the size-representative structural template —
incoherent with the donated system, and "Unknown" whenever the template lodged
no control row (exactly 721167: no `main_heating_controls` energy element
persisted). Now carries the donor's display heating + control alongside the
calc cluster.
- **Ventilation — FIXED (Model).** The prediction never synthesised ventilation
— it kept the template's `sap_ventilation`, so a predicted dwelling in an
MEV/MVHR neighbourhood was scored + displayed as natural. New
`_apply_ventilation_mode` modes `mechanical_ventilation_kind` across the cohort
(recency/geo-weighted, mirroring glazing). Natural-vent cohorts mode to None
and stay natural (§2 default) — so a genuinely natural dwelling like 721167
correctly stays natural; the fix moves only real MEV/MVHR neighbourhoods.
- **Main Fuel — NOT a prediction gap.** `epc_main_heating_detail.main_fuel_type`
*is* persisted for predicted properties (721167 → `26` = mains gas, carried
from the heating donor). The "Unknown" is the **same FE code→name gap as item
2** (the Drizzle/Next repo not mapping the fuel code), not a Model issue.
Both fixes are display-only for the calculator (component-accuracy gate, corpus
harness, and prediction e2e all green); persistence already carries
`main_heating_controls` (energy element) and `ventilation_mechanical_ventilation_kind`,
so the synthesised values reach the passport on the next prediction run.