Two review points from @dancafc: 1) Rename the `Comparable` dataclass → `ComparableProperty` (it models one comparable *property*; the collection stays `ComparableProperties`). Applied across domain, repositories, orchestration, harness, scripts, and tests with a word-boundary rename so `ComparableProperties` is untouched. 2) Move `PredictionTarget` out of comparable_properties.py into prediction_target.py (where `PredictionTargetAttributes` + `build_prediction_target` already live). comparable_properties.py now imports it; no import cycle (prediction_target no longer depends on comparable_properties). Importers updated. 92 tests pass across the touched suites; pyright strict clean. Co-Authored-By: Claude Opus 4.8 <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