From a88b5502344eaa5422d3d4a7d947ef3c77293b95 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Fri, 26 Jun 2026 10:54:11 +0000 Subject: [PATCH] =?UTF-8?q?Predict=20ventilation=20kind=20from=20the=20coh?= =?UTF-8?q?ort=20mode=20=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- domain/epc_prediction/epc_prediction.py | 40 +++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/domain/epc_prediction/epc_prediction.py b/domain/epc_prediction/epc_prediction.py index 072fcfc6..c582548c 100644 --- a/domain/epc_prediction/epc_prediction.py +++ b/domain/epc_prediction/epc_prediction.py @@ -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