Model/docs/HANDOVER_EPC_PREDICTION_WIRING.md
Khalim Conn-Kowlessar a43c03ed94 feat(epc-prediction): thread prediction injection points through the composition root
build_first_run_pipeline now constructs epc_prediction=EpcPrediction() and accepts
comparables_repo + prediction_attributes_reader as optional params, threading them
into IngestionOrchestrator (ADR-0031). The on-switch is now just supplying those
two arguments — no orchestrator/handler edits — once they exist: the cohort repo
(its EPC client is the source client pending #1136) and the property_overrides
attributes reader (built separately). Both default None, so the feature stays OFF
and ingestion is unchanged until they're passed.

The epc_property.source migration is live, so the predicted-EPC persistence slot
(slice-5c) now works against the real DB. Handover updated to reflect the simpler
composition-root step.

pyright strict clean; handler + pipeline + ingestion-prediction tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 13:53:54 +00:00

5 KiB
Raw Blame History

EPC Prediction — production wiring handover (for Jun-te)

The EPC Prediction gap-fill is wired end-to-end behind seams, with one real dependency stubbed: reading an EPC-less Property's resolved Landlord Overrides. This note is what's needed to finish it once your property_overrides read path lands. Design is ADR-0031; terms in CONTEXT.md (EPC Prediction, Effective EPC, EPC Anomaly Flag).

What's already built (slices 5a5e, all on feature/epc-prediction)

  • 5a Property.predicted_epc slot + a "predicted" source_path / effective_epc branch — used only when there's no lodged EPC and no Site Notes (a real source always wins).
  • 5b ComparablePropertiesRepository.candidates_for(postcode) + EpcComparablePropertiesRepository adapter (postcode search → per-cert fetch → batched UPRN→coords). Composes with EpcClientService + GeospatialS3Repository.
  • 5c EPC store source discriminator (lodged | predicted) so the two coexist per property; get_predicted_for_property / _for_properties; PropertyPostgresRepository hydrates predicted_epc. Needs a DB migration — see docs/MIGRATION_NOTE_predicted_epc_source.md.
  • 5d build_prediction_target(identity, coords, attributes) + the eligibility gate (unknown property_type → not predicted). Override attributes come through the PredictionTargetAttributesReader port (the stub).
  • 5e IngestionOrchestrator wiring: when the three prediction collaborators are injected, an EPC-less Property is predicted from its cohort and persisted to the predicted slot. The collaborators are optional — unwired, ingestion is unchanged.

Your part — three things

1. Implement PredictionTargetAttributesReader (the stub)

repositories/property/prediction_target_attributes_reader.py defines the port: attributes_for(property_id) -> PredictionTargetAttributes (property_type, built_form, wall_construction). Build the adapter as a read over the property_overrides fact layer (the finaliser writes it via PropertyOverrideRepository.upsert_all; you're adding the read side).

Code-space gotcha. select_comparables filters comparable.epc.property_type == target.property_type, and the cohort EPCs carry gov API codes (e.g. "0"/"2"). Landlord Overrides resolve to enum value strings (e.g. "House"). Your adapter must map override value → the API-code space, or property_type will never match and every cohort comes back empty. Same for built_form. (domain/epc/property_type.py, built_form_type.py are the enums; datatypes/epc/domain/epc_codes.csv has the code table.) property_type unresolved → return PredictionTargetAttributes(property_type=None) so the gate skips the Property.

2. Run the Drizzle migration

epc_property.source column — full spec in docs/MIGRATION_NOTE_predicted_epc_source.md (column + default 'lodged' + relax any property_id uniqueness to (property_id, source)).

3. Pass the two collaborators at the composition root

This is now wired: build_first_run_pipeline (in applications/ara_first_run/handler.py) already constructs epc_prediction=EpcPrediction() and accepts the other two as optional params that it threads into the IngestionOrchestrator. So the on-switch is just supplying them once they exist:

build_first_run_pipeline(
    ...,
    comparables_repo=EpcComparablePropertiesRepository(epc_client, geospatial_repo),
    prediction_attributes_reader=<your property_overrides adapter>,  # task #1
)

epc_client is the same EPC source client behind epc_fetcher (the concrete EpcClientService exposes search_by_postcode + get_by_certificate_number), so build it alongside the other source clients in _source_clients_from_env (pending #1136). Until both are passed, ingestion ignores prediction — no orchestrator or handler edits needed, just the two arguments.

One open item — Validation Cohort exclusion

A predicted-source Property has no real lodged record, so it must not be scored as if it did (CONTEXT: Validation Cohort; ADR-0031 dec-3). There is no Validation-Cohort code path today to exclude it from — when one is built (or in any QA that compares calc(effective_epc) vs lodged), exclude on the structural signal:

if prop.source_path == "predicted":
    continue  # predicted EPC — no ground truth to validate against

Note too: PropertyBaselinePerformance.lodged is derived from effective_epc regardless of source (property_baseline_orchestratorlodged_performance), so for a predicted Property that "lodged" is synthesised, not real. Decide whether baseline should null/flag it for predicted properties when this lands.

Anomaly dual-use (later, not now)

Slice-5 is gap-fill only (epc is None). The slot model already supports predicting for every Property to compare predicted vs lodged (EPC Anomaly Flags) — see ADR-0031 dec-4. Reuses the same ComparableProperties repo + the predicted slot.