A dwelling's heating is one conceptual system, but its fields are scattered across EpcPropertyData (a gov-API schema mirror): the cluster on sap_heating, the electricity tariff on sap_energy_source.meter_type, hot-water flags loose at top level. Three places synthesise a heating system — Measure Options, Landlord Overrides, EPC Prediction's donor — and each hand-copied a different ad-hoc subset. The override and donor both dropped meter_type, so an electric-storage system landed on the template's single-rate meter and billed overnight heat at the peak rate: property 713406 scored SAP 13 (G) vs ~50 (E), inflating the HHRSH measure to +45.8 and overshooting the plan to band A. Establish a single Coherent Heating System boundary (CONTEXT.md) that every synthesiser must cover, with a source-appropriate fill policy (ADR-0035): - Override overlay *completes* the partial system the landlord named. Companion fields are now DERIVED from the SAP code, not hand-attached per archetype: the off-peak meter from the calculator's single off-peak classification (new OFF_PEAK_IMPLYING_HEATING_CODES = SAP §12 Rules 1-2), and an unobserved storage charge control defaults to the conservative manual control (Table 4e 2401). So adding a heating archetype is just adding its code — companions can't be forgotten. A contract test guards it (every off-peak code drags a Dual meter). - Prediction's heating donor now *carries* the donor's meter_type alongside its sap_heating cluster — the donor is already coherent. Coherence is a synthesis-time obligation only; the calculator still scores a real lodged cert exactly as lodged. Verified on 713406: baseline 13 -> 47.8 (E), matching its recorded rating; the phantom HHRSH recommendation is gone and the plan no longer overshoots to A. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|---|---|---|
| .. | ||
| __init__.py | ||
| comparable_properties.py | ||
| epc_prediction.py | ||
| prediction_comparison.py | ||
| prediction_target.py | ||
| README.md | ||
| validation.py | ||
EPC Prediction
Predict a structured EpcPropertyData for an EPC-less UK home from its
postcode neighbours, so it flows through the rest of the pipeline (Baseline, Bill
Derivation, Modelling) exactly like a home that has an EPC. It is deterministic
neighbour synthesis — cohort modes + a coherent template + per-component
weighting — not ML. ~30% of UK homes (typically long-tenure) have no EPC.
- Design: ADR-0029 (algorithm), ADR-0030 (validation), ADR-0031 (production wiring).
- Glossary: see EPC Prediction, Comparable Properties, Component Accuracy, EPC Anomaly Flag in CONTEXT.md.
The flow (gap-fill)
Ingestion: a Property has no lodged EPC (epc_fetcher.get_by_uprn → None)
│
├─ resolve its attributes (property_type/built_form/wall) from Landlord Overrides
│ └─ property_type unknown? → GATED OUT, not predicted (no national defaults)
├─ build a PredictionTarget (postcode + coordinates + attributes)
├─ ComparableProperties repo: fetch the postcode cohort (search → per-cert → coords)
├─ select_comparables(): filter to the reference cohort (type-hard, built-form-soft)
├─ EpcPrediction.predict(): synthesise the picture (modes + template + donor + weights)
└─ persist to the Property's PREDICTED slot (source = "predicted")
│
Modelling/Baseline: Property.effective_epc returns the predicted picture
(source_path == "predicted"), scored like any other Effective EPC.
A lodged EPC always wins — prediction is last-resort gap-fill.
Where the pieces live
| Concern | File |
|---|---|
| Synthesis (modes, template, heating donor, geo/recency/similarity weights) | epc_prediction.py |
| Cohort selection (filter-then-relax ladder) | comparable_properties.py |
| Target assembly + eligibility gate | prediction_target.py |
| Cohort IO port + EPC-API/geospatial adapter | repositories/comparable_properties/ |
Predicted-EPC persistence (source discriminator) |
repositories/epc/ |
predicted source path on the aggregate |
domain/property/property.py |
| Ingestion wiring (gate → predict → persist) | orchestration/ingestion_orchestrator.py |
| Validation (leave-one-out, component-first) + ratcheting gate | validation.py, tests/domain/epc_prediction/test_component_accuracy_gate.py |
See it run
tests/e2e/test_epc_prediction_e2e.py — the whole flow against the real DB +
repos, only the external HTTP clients faked. Start there.
Status
Algorithm + validation: built. Production gap-fill wiring: built behind
seams (slices 5a–5e). Two things finish it — a DB migration and the
property_overrides read adapter — see
the wiring handover and
the migration note.
EPC Anomaly Flags (predict for every home, compare to lodged) is the
designed next step the storage already supports.
Run the tests
PYTHONPATH=. python -m pytest tests/e2e/test_epc_prediction_e2e.py \
tests/domain/epc_prediction tests/orchestration/test_ingestion_prediction.py \
tests/repositories/comparable_properties tests/repositories/epc/test_epc_predicted_slot.py \
-o addopts="" -q