From 80c5ad0c6caf583766db816df3629291072b43a1 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Fri, 26 Jun 2026 10:49:37 +0000 Subject: [PATCH] =?UTF-8?q?Predict=20ventilation=20kind=20from=20the=20coh?= =?UTF-8?q?ort=20mode=20=F0=9F=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prediction never synthesises ventilation — it keeps the size-template's sap_ventilation, so a predicted dwelling in an MEV/MVHR neighbourhood is scored + displayed as natural (predicted property 721167 follow-up). Mode the mechanical_ventilation_kind across the cohort like glazing. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../epc_prediction/test_epc_prediction.py | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tests/domain/epc_prediction/test_epc_prediction.py b/tests/domain/epc_prediction/test_epc_prediction.py index 4b0ff149..c7ffbf52 100644 --- a/tests/domain/epc_prediction/test_epc_prediction.py +++ b/tests/domain/epc_prediction/test_epc_prediction.py @@ -17,6 +17,7 @@ from datatypes.epc.domain.epc_property_data import ( SapEnergySource, SapFloorDimension, SapHeating, + SapVentilation, SapWindow, ) from domain.geospatial.coordinates import Coordinates @@ -54,6 +55,7 @@ def _epc( meter_type: str = "2", main_heating_label: str = "Boiler and radiators, mains gas", main_heating_controls_label: Optional[str] = None, + mechanical_ventilation_kind: Optional[str] = None, ) -> EpcPropertyData: epc: EpcPropertyData = object.__new__(EpcPropertyData) epc.property_type = "2" @@ -108,6 +110,10 @@ def _epc( ) epc.has_hot_water_cylinder = has_hot_water_cylinder epc.solar_water_heating = solar_water_heating + epc.sap_ventilation = SapVentilation( + mechanical_ventilation_kind=mechanical_ventilation_kind, + sheltered_sides=1, + ) energy: SapEnergySource = object.__new__(SapEnergySource) energy.meter_type = meter_type epc.sap_energy_source = energy @@ -557,6 +563,31 @@ def test_heating_is_a_coherent_donor_not_the_structural_template() -> None: assert predicted.has_hot_water_cylinder is True +def test_ventilation_kind_follows_the_cohort_mode() -> None: + # Mechanical ventilation (MEV/MVHR) is a new-build / retrofit feature that + # clusters by era and street — like glazing — so the predicted ventilation + # kind takes the recency/geo-weighted cohort mode, not the size-template's. + # The size-closest template here is natural (None); the cohort is + # predominantly MVHR, so the prediction must reflect the MVHR neighbourhood + # rather than leave the template's empty ventilation (predicted property + # 721167 follow-up). Natural-vent cohorts mode to None and stay natural. + cohort = _cohort( + _epc(mechanical_ventilation_kind=None), # template (size tie → first) + _epc(mechanical_ventilation_kind="MVHR"), + _epc(mechanical_ventilation_kind="MVHR"), + _epc(mechanical_ventilation_kind="MVHR"), + ) + + # Act + predicted: EpcPropertyData = EpcPrediction().predict( + PredictionTarget(postcode="LS6 1AA", property_type="2"), cohort + ) + + # Assert — the predicted kind is the cohort's MVHR mode, not the template's None. + assert predicted.sap_ventilation is not None + assert predicted.sap_ventilation.mechanical_ventilation_kind == "MVHR" + + def test_glazing_follows_the_recency_weighted_cohort_mode() -> None: # Arrange — an old majority single-glazed (type 1, 2015) and a recent # minority double-glazed (type 3, 2025). Glazing is retrofitted over time