Predict ventilation kind from the cohort mode 🟩

Add _apply_ventilation_mode: set the predicted mechanical_ventilation_kind to
the recency/geo-weighted cohort mode (mirrors _apply_glazing_mode — MEV/MVHR is
a new-build/retrofit feature clustering by era + street). Only the kind moves;
the template's sheltered_sides etc. stay. Natural cohorts mode to None and stay
natural (§2 default), so this only moves genuine MEV/MVHR neighbourhoods.
Display-only for the calc gate: component-accuracy (26) + corpus (6) + e2e (1)
all green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-06-26 10:54:11 +00:00
parent 80c5ad0c6c
commit a88b550234

View file

@ -70,6 +70,7 @@ class EpcPrediction:
)
self._apply_categorical_modes(predicted, comparables, target.coordinates)
self._apply_glazing_mode(predicted, comparables, target.coordinates)
self._apply_ventilation_mode(predicted, comparables, target.coordinates)
self._apply_heating_donor(predicted, comparables)
self._apply_overrides(predicted, target)
return predicted
@ -139,6 +140,36 @@ class EpcPrediction:
for window in predicted.sap_windows:
window.glazing_type = glazing
@staticmethod
def _apply_ventilation_mode(
predicted: EpcPropertyData,
comparables: ComparableProperties,
target_coordinates: Optional[Coordinates],
) -> None:
"""Set the predicted mechanical-ventilation kind to the recency- and
geo-weighted cohort mode. A mechanical system (MEV/MVHR) is a new-build /
retrofit feature that clusters by era and street (like glazing), so a
recent, near neighbour is the best signal for the target's system; the
size-representative structural template is not (it just happens to carry
whatever its own cert lodged). Only the kind moves the rest of the
template's `sap_ventilation` (e.g. `sheltered_sides`, derived from
built_form) stays. A natural-vent cohort modes to None and is left
natural the §2 cascade default so this only moves genuine MEV/MVHR
neighbourhoods (ADR-0029 follow-up; predicted property 721167)."""
ventilation = predicted.sap_ventilation
if ventilation is None:
return
members = comparables.members
weights = _combine(
_recency_weights(members), _geo_weights(target_coordinates, members)
)
kind = _weighted_mode(
(_comparable_ventilation_kind(c) for c in members), weights
)
if kind is None:
return
ventilation.mechanical_ventilation_kind = kind
def confidence(
self, comparables: ComparableProperties
) -> PredictionConfidence:
@ -551,6 +582,15 @@ def _comparable_modal_glazing(
return Counter(types).most_common(1)[0][0] if types else None
def _comparable_ventilation_kind(
comparable: ComparableProperty,
) -> Optional[str]:
"""A comparable's mechanical-ventilation kind (the `MechanicalVentilationKind`
enum name, e.g. "MVHR"), or None when it lodges no system (natural)."""
ventilation = comparable.epc.sap_ventilation
return ventilation.mechanical_ventilation_kind if ventilation is not None else None
def _main_heating_detail(comparable: ComparableProperty) -> Optional[MainHeatingDetail]:
"""The primary heating system's detail row, or None when none is lodged."""
details = comparable.epc.sap_heating.main_heating_details