# 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](../../docs/adr/0029-epc-prediction-from-comparable-properties.md) (algorithm), [ADR-0030](../../docs/adr/0030-epc-prediction-validation-is-sap-version-aware-and-component-first.md) (validation), [ADR-0031](../../docs/adr/0031-epc-prediction-production-wiring.md) (production wiring). - **Glossary**: see *EPC Prediction*, *Comparable Properties*, *Component Accuracy*, *EPC Anomaly Flag* in [CONTEXT.md](../../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](../../docs/HANDOVER_EPC_PREDICTION_WIRING.md) and [the migration note](../../docs/MIGRATION_NOTE_predicted_epc_source.md). **EPC Anomaly Flags** (predict for *every* home, compare to lodged) is the designed next step the storage already supports. ## Run the tests ```bash 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 ```