9 KiB
EPC Prediction — handover
Branch feature/epc-prediction @ d8f015fb (37 ahead of origin/main; local-only,
not pushed). Tree clean. All ranked backlog (#1222–1228) closed.
What this is
Deterministic neighbour synthesis that predicts a structured EpcPropertyData
for an EPC-less UK home from its postcode-cohort of neighbours, so it flows through
the modelling pipeline. NOT ML. Validation methodology + harness are built; the work
is a measurable accuracy backlog.
READ FIRST (hold the full state)
- Memory
project_epc_prediction— the spine: design, every commit, metrics, the open fronts, gotchas. Read it first. docs/adr/0029-…(design, 6 forks) anddocs/adr/0030-…component-first.md(validation methodology — internalise: predict components, SAP/carbon/PE are a calculator-floored secondary guard).- Memory
feedback_per_component_best_method— THE load-bearing principle this session established (see below). - Convention memories:
feedback_aaa_test_convention,feedback_abs_diff_over_pytest_approx,feedback_commit_per_slice,feedback_bigger_slices_for_uniform_work.
The methodology (ADR-0030)
- Component Accuracy is the PRIMARY signal — predicted vs API-actual components, calculator-free. SAP/CO₂/PE vs lodged is SECONDARY and calculator-floored.
- Source cohort keeps ALL cert vintages; only held-out validation TARGETS are
SAP 10.2 (
sap_version == 10.2). - The committed Tier-1 gate (
tests/domain/epc_prediction/test_component_accuracy_gate.py) runs the calculator-free scorer over the frozen anonymised fixture (tests/fixtures/epc_prediction/, 36 SAP-10.2 targets) and asserts per-component ratchet floors. Deterministic → exact. Tighten-only: when you improve a component, bump its floor in the same commit. A mapper or fixture change re-baselines floors (not a regression) — document it.
THE PRINCIPLE that drove this session
Give each component its own best-fit synthesis method; never force one global mechanism on all of them. Validated head-to-head on the harness:
- Permanent fabric categoricals (wall, age) → physical-similarity-weighted mode (size×age toward cohort centre).
- Time-varying components (roof insulation, glazing) → recency-weighted mode.
- Coherence-coupled cluster (heating) → coherent whole-cluster donor, NEVER field-moded.
- Point-estimate scalar (floor area) → cohort median (MAD-minimising).
- Geo-varying components (age, wall, floor, glazing) → additionally geo-proximity
weighted; roof showed no geo signal → excluded.
All live in
domain/epc_prediction/epc_prediction.pyas composable weight vectors (_similarity_weights×_recency_weights×_geo_weights, combined via_combine, fed to_weighted_mode).
Closed this session (#1222 was done before; #1223–1228 this session)
- #1226 per-prediction confidence (
PredictionConfidence, compute-only; agreement strongly predicts correctness, r=0.582). - #1224 physical-similarity-weighted categorical mode (wall_insul/roof/floor +1–3pp).
- #1223 per-component, NOT a global recency template: floor-area→cohort median + glazing→recency mode. (A global recency template was rejected — it disturbed the coherence-coupled heating cluster.)
- #1225 coherent heating donor (modal signature = fuel+category+cylinder, recency tie-break). Biggest SAP lever: control 66→74%, SAP MAE 7.08→6.00 pre-merge.
- #1228 PEI investigation — DISPROVED the unit-bug hypothesis (calc/lodged ratio 1.06); reframed as calc floor + prediction-sensitivity. Report now surfaces CO₂/PEI calc floors. (Open calc-branch remnant; largely closed by the main merge — see below.)
- #1227 geo-proximity weighting — grilled, signal-checked (STRONG GO, esp. age),
built per-component. Batch
GeospatialRepository.coordinates_for_uprns, coords threaded ontoComparable/PredictionTarget, haversine kernel (_GEO_SCALE_KM=0.1, gate-safe optimum). Intra-postcode lift modest (cohort = 1 postcode); the bigger prize is cross-postcode expansion (deferred, needs dense corpus). - Corpus grown 40→150 postcodes (
6e9f8312); roof-insulation ±1 reporting. - Merged
origin/main(96 commits of calculator/mapper gap fixes,0b2827e9).
Current metrics (post-merge, 150-pc corpus, 514 SAP-10.2 targets)
Component Accuracy (calculator-free): wall 91.2, wall_insul 79.0, age 57.2 (±1 84.7), roof_construction 78.2, floor_construction 79.6, heating_fuel 96.9, heating_category 95.7, heating_control 73.9, water_fuel 96.3, water_code 95.3, has_cylinder 89.7, cylinder_insul 52.4, secondary 42.0, roof_insul 49.3 (±1 53.7), floor_insul 94.7, room_in_roof 96.5, glazing 67.3, pv 98.8, solar 99.8.
Floor area: MAE 10.48 m² / MAPE 13.2% / typical (median actual) 61 m² (cohort median, unweighted).
End-to-end vs lodged (SECONDARY, calculator-floored): SAP pred MAE 6.25 / calc floor 0.95 (was 1.57 pre-merge, orig 3.25 — the calc fixes nearly validated the calculator, so the gap is now almost all prediction); CO₂ 0.61 / floor 0.18; PEI 39.6 / floor 13.7.
Key files
domain/epc_prediction/epc_prediction.py—EpcPrediction.predict: median floor area + per-component weighted modes + glazing + heating donor + overrides.domain/epc_prediction/comparable_properties.py—select_comparablesladder;Comparable/PredictionTarget(carrycoordinates).domain/epc_prediction/prediction_comparison.py—compare_prediction(25 signals).domain/epc_prediction/validation.py—iter_predictions+evaluate_component_accuracy(one scorer, calculator-free).harness/epc_prediction_corpus.py—load_corpus(+_coordinates.jsonsidecar),load_coordinates,anonymise_payload.repositories/geospatial/—GeospatialRepository.coordinates_for_uprns(batch).scripts/validate_epc_prediction.py(full report),build_epc_prediction_fixture.py,fetch_epc_prediction_corpus.py,fetch_corpus_coordinates.py.
Open fronts (ranked)
- Geo-weighted floor-area median — measured quick win: MAE 10.48→9.77,
MAPE 13.2→12.2%. Swap
_median_floor_areafor a geo-weighted median (reuse_geo_weights); gate-check + ratchet the floor_area ceiling. Smallest next slice. - Cross-postcode geo expansion — the real geo payoff (distance-weighted cohort beyond the single postcode). Needs a densely-sampled corpus (current 150 are scattered, so a target's true geo-neighbours aren't in-corpus). Design grilled; build a dense corpus first.
- Slice-5 production wiring —
ComparablePropertiesrepo + theModellingOrchestratorowning the EPC estimation + distance calcs (a deliberate shift from ADR-0029, which put the fallback in Ingestion). WRITE AN ADR when this lands (it reverses where the fallback lives). Add a provenance marker (EpcPropertyDatahas no predicted/source field yet). - Weak components with headroom only via NEW signals: age 57% / roof_insul 49% (method-exhausted — confirmed recency/similarity/plain all tie-or-worse); cylinder_insul / secondary are tiny-n.
How to run
- Token + S3 creds:
set -a; . backend/.env; set +a(AWS creds mounted at~/.aws). - Tests:
PYTHONPATH=. python -m pytest tests/domain/epc_prediction tests/harness/test_epc_prediction_corpus.py tests/repositories/geospatial -o addopts="" -p no:cacheprovider -q - Full report:
PYTHONPATH=. python scripts/validate_epc_prediction.py(corpus/tmp/epc_prediction_corpus). - Gate is just a pytest test (deterministic, calculator-free).
- pyright strict, zero new errors, on every touched file.
In-flight / gotchas
- Corpus lives in
/tmp/epc_prediction_corpus(gitignored; 150 pc / 3719 certs +_coordinates.json). Backed up to/workspaces/home/epc_prediction_corpus_backup(persistent host mount — survives container rebuild;/tmpdoes NOT). Coords backup at/workspaces/home/epc_prediction_corpus_coords_backup.json. If/tmpis wiped, restore from the backup before running the full report. - Coordinates: OS Open-UPRN parquet is
DATA_BUCKET/spatial/(boto3 — s3fs NOT installed; read viaget_object→BytesIO;boto3.clientneeds# pyright: ignore[reportUnknownMemberType, reportUnknownVariableType]). The cert payload carriesuprn(the join key). The committed fixture ships_coordinates.json(OGL OS OpenData) so the gate exercises geo without S3. - NEVER commit the API token,
/tmpcorpus, or the coords cache. Thetests/fixtures/epc_predictionone is anonymised + intentional. - Conventions: AAA test headers;
abs(x-y) <= tolnotpytest.approx; commit per slice (stage by name, watch untracked); ADR-cite in commit messages; class isEpcPrediction(no "Service"). - Per-item workflow: implement TDD red→green on this branch → run the harness →
record before/after → ratchet gate floors →
gh issue commentimpact → close. - The merge is local, not pushed — push only if asked.
- Update memory
project_epc_predictionas state changes.