Model/domain/epc_prediction/prediction_target.py
Khalim Conn-Kowlessar 7ca1f815f6 refactor(epc-prediction): PR review — rename ComparableProperty, relocate PredictionTarget
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>
2026-06-16 13:34:44 +00:00

68 lines
2.7 KiB
Python

"""Assemble an EPC-less Property's PredictionTarget, with the eligibility gate
(ADR-0031 slice-5d).
A `PredictionTarget` needs the target's own known inputs: its postcode (to find
the cohort), coordinates (to distance-weight it), and the Landlord-Override
attributes that condition selection — `property_type` (the HARD cohort filter),
plus optional `built_form` / `wall_construction`. `property_type` is required: a
Property whose type is unknown is gated out (never sized from a mixed-type
cohort), so the builder returns None and the caller skips prediction.
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Optional, Union
from domain.geospatial.coordinates import Coordinates
from domain.property.property import PropertyIdentity
@dataclass(frozen=True)
class PredictionTarget:
"""The known inputs for the Property whose EPC we are predicting — the fields
guaranteed at ingestion (plus any Landlord Overrides, added as they're used).
`built_form` is often but not always known.
"""
postcode: str
property_type: str
built_form: Optional[str] = None
# A known Landlord Override (e.g. solid brick) conditions cohort selection —
# matching comparables are emphasised while enough remain (ADR-0029).
wall_construction: Optional[Union[int, str]] = None
# The target Property's own coordinates (resolved from its UPRN), against
# which neighbours are distance-weighted. None disables geo-weighting.
coordinates: Optional[Coordinates] = None
@dataclass(frozen=True)
class PredictionTargetAttributes:
"""The target Property's own attributes resolved from Landlord Overrides,
needed to find and condition its cohort. `property_type` is the code-space
value the cohort EPCs carry (e.g. "2"); None means it could not be resolved,
which gates the Property out of prediction."""
property_type: Optional[str]
built_form: Optional[str] = None
wall_construction: Optional[Union[int, str]] = None
def build_prediction_target(
identity: PropertyIdentity,
coordinates: Optional[Coordinates],
attributes: PredictionTargetAttributes,
) -> Optional[PredictionTarget]:
"""The PredictionTarget for an EPC-less Property, or None when ineligible —
`property_type` is the hard cohort filter, so a Property whose type is unknown
is gated out of prediction (ADR-0031) rather than sized from a mixed-type
cohort."""
if attributes.property_type is None:
return None
return PredictionTarget(
postcode=identity.postcode,
property_type=attributes.property_type,
built_form=attributes.built_form,
wall_construction=attributes.wall_construction,
coordinates=coordinates,
)