From e9b4dbbfe58d80061e61e0cce933d0d75cb0b180 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Sat, 16 May 2026 15:03:58 +0000 Subject: [PATCH] slice 5: room, door and lighting count features MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ten flat int counts added to the transform — door_count, habitable/heated/wet/insulated_door counts, extensions, open chimneys, and the three fixed-lighting bulb counts (CFL/LED/ incandescent). All non-nullable; direct EpcPropertyData field reads. Co-Authored-By: Claude Opus 4.7 --- .../domain/src/domain/ml/tests/_fixtures.py | 33 ++++++---- .../src/domain/ml/tests/test_transform.py | 63 +++++++++++++++++++ packages/domain/src/domain/ml/transform.py | 55 +++++++++++++++- 3 files changed, 139 insertions(+), 12 deletions(-) diff --git a/packages/domain/src/domain/ml/tests/_fixtures.py b/packages/domain/src/domain/ml/tests/_fixtures.py index 20d0dafb..fe3b214c 100644 --- a/packages/domain/src/domain/ml/tests/_fixtures.py +++ b/packages/domain/src/domain/ml/tests/_fixtures.py @@ -24,6 +24,17 @@ def make_minimal_sap10_epc( energy_consumption_current: Optional[int] = None, space_heating_kwh: float = 0.0, water_heating_kwh: float = 0.0, + total_floor_area_m2: float = 70.0, + door_count: int = 0, + habitable_rooms_count: int = 0, + heated_rooms_count: int = 0, + wet_rooms_count: int = 0, + extensions_count: int = 0, + open_chimneys_count: int = 0, + insulated_door_count: int = 0, + cfl_fixed_lighting_bulbs_count: int = 0, + led_fixed_lighting_bulbs_count: int = 0, + incandescent_fixed_lighting_bulbs_count: int = 0, ) -> EpcPropertyData: """Construct a minimal valid SAP10 EpcPropertyData with parametrisable targets.""" return EpcPropertyData( @@ -38,7 +49,7 @@ def make_minimal_sap10_epc( walls=[], floors=[], main_heating=[], - door_count=0, + door_count=door_count, sap_heating=SapHeating( instantaneous_wwhrs=InstantaneousWwhrs(), main_heating_details=[], @@ -59,16 +70,16 @@ def make_minimal_sap10_epc( solar_water_heating=False, has_hot_water_cylinder=False, has_fixed_air_conditioning=False, - wet_rooms_count=0, - extensions_count=0, - heated_rooms_count=0, - open_chimneys_count=0, - habitable_rooms_count=0, - insulated_door_count=0, - cfl_fixed_lighting_bulbs_count=0, - led_fixed_lighting_bulbs_count=0, - incandescent_fixed_lighting_bulbs_count=0, - total_floor_area_m2=70.0, + wet_rooms_count=wet_rooms_count, + extensions_count=extensions_count, + heated_rooms_count=heated_rooms_count, + open_chimneys_count=open_chimneys_count, + habitable_rooms_count=habitable_rooms_count, + insulated_door_count=insulated_door_count, + cfl_fixed_lighting_bulbs_count=cfl_fixed_lighting_bulbs_count, + led_fixed_lighting_bulbs_count=led_fixed_lighting_bulbs_count, + incandescent_fixed_lighting_bulbs_count=incandescent_fixed_lighting_bulbs_count, + total_floor_area_m2=total_floor_area_m2, sap_version=10.2, energy_rating_current=energy_rating_current, co2_emissions_current=co2_emissions_current, diff --git a/packages/domain/src/domain/ml/tests/test_transform.py b/packages/domain/src/domain/ml/tests/test_transform.py index 2c888f8f..5f447c3d 100644 --- a/packages/domain/src/domain/ml/tests/test_transform.py +++ b/packages/domain/src/domain/ml/tests/test_transform.py @@ -117,3 +117,66 @@ def test_to_row_extracts_total_floor_area_m2() -> None: # Assert # make_minimal_sap10_epc sets total_floor_area_m2=70.0 by default assert row["total_floor_area_m2"] == 70.0 + + +_EXPECTED_COUNT_FEATURES: dict[str, type] = { + "door_count": int, + "habitable_rooms_count": int, + "heated_rooms_count": int, + "wet_rooms_count": int, + "extensions_count": int, + "open_chimneys_count": int, + "insulated_door_count": int, + "cfl_fixed_lighting_bulbs_count": int, + "led_fixed_lighting_bulbs_count": int, + "incandescent_fixed_lighting_bulbs_count": int, +} + + +def test_schema_advertises_count_features() -> None: + # Arrange + transform = EpcMlTransform() + + # Act + schema = transform.schema() + + # Assert + for feature_name, expected_dtype in _EXPECTED_COUNT_FEATURES.items(): + assert feature_name in schema.feature_columns, feature_name + column = schema.feature_columns[feature_name] + assert isinstance(column, ColumnSpec) + assert column.dtype is expected_dtype + assert column.nullable is False + + +def test_to_row_extracts_count_features() -> None: + # Arrange + epc = make_minimal_sap10_epc( + energy_rating_current=82, + door_count=3, + habitable_rooms_count=5, + heated_rooms_count=4, + wet_rooms_count=1, + extensions_count=1, + open_chimneys_count=0, + insulated_door_count=2, + cfl_fixed_lighting_bulbs_count=0, + led_fixed_lighting_bulbs_count=8, + incandescent_fixed_lighting_bulbs_count=2, + ) + transform = EpcMlTransform() + + # Act + row = transform.to_row(epc) + + # Assert + assert row["door_count"] == 3 + assert row["habitable_rooms_count"] == 5 + assert row["heated_rooms_count"] == 4 + assert row["wet_rooms_count"] == 1 + assert row["extensions_count"] == 1 + assert row["open_chimneys_count"] == 0 + assert row["insulated_door_count"] == 2 + assert row["cfl_fixed_lighting_bulbs_count"] == 0 + assert row["led_fixed_lighting_bulbs_count"] == 8 + assert row["incandescent_fixed_lighting_bulbs_count"] == 2 diff --git a/packages/domain/src/domain/ml/transform.py b/packages/domain/src/domain/ml/transform.py index 2f1b89d8..907380a5 100644 --- a/packages/domain/src/domain/ml/transform.py +++ b/packages/domain/src/domain/ml/transform.py @@ -19,11 +19,53 @@ from domain.ml.ucl import apply_ucl_correction _FEATURE_COLUMNS: dict[str, ColumnSpec] = { + # Geometry "total_floor_area_m2": ColumnSpec( dtype=float, nullable=False, description="Total floor area in square metres, from `total_floor_area`.", ), + # Counts — directly populated by all SAP10 EPCs + "door_count": ColumnSpec( + dtype=int, nullable=False, description="Number of external doors." + ), + "habitable_rooms_count": ColumnSpec( + dtype=int, nullable=False, description="Number of habitable rooms." + ), + "heated_rooms_count": ColumnSpec( + dtype=int, nullable=False, description="Number of heated rooms." + ), + "wet_rooms_count": ColumnSpec( + dtype=int, nullable=False, description="Number of wet rooms (bathrooms / WCs)." + ), + "extensions_count": ColumnSpec( + dtype=int, + nullable=False, + description="Number of extensions beyond the main dwelling.", + ), + "open_chimneys_count": ColumnSpec( + dtype=int, nullable=False, description="Number of open chimneys." + ), + "insulated_door_count": ColumnSpec( + dtype=int, + nullable=False, + description="Number of external doors classed as insulated.", + ), + "cfl_fixed_lighting_bulbs_count": ColumnSpec( + dtype=int, + nullable=False, + description="Number of CFL bulbs in fixed lighting outlets.", + ), + "led_fixed_lighting_bulbs_count": ColumnSpec( + dtype=int, + nullable=False, + description="Number of LED bulbs in fixed lighting outlets.", + ), + "incandescent_fixed_lighting_bulbs_count": ColumnSpec( + dtype=int, + nullable=False, + description="Number of incandescent bulbs in fixed lighting outlets.", + ), } @@ -99,8 +141,19 @@ class EpcMlTransform: """ rhi = epc.renewable_heat_incentive return { - # Features + # Features — geometry "total_floor_area_m2": epc.total_floor_area_m2, + # Features — counts + "door_count": epc.door_count, + "habitable_rooms_count": epc.habitable_rooms_count, + "heated_rooms_count": epc.heated_rooms_count, + "wet_rooms_count": epc.wet_rooms_count, + "extensions_count": epc.extensions_count, + "open_chimneys_count": epc.open_chimneys_count, + "insulated_door_count": epc.insulated_door_count, + "cfl_fixed_lighting_bulbs_count": epc.cfl_fixed_lighting_bulbs_count, + "led_fixed_lighting_bulbs_count": epc.led_fixed_lighting_bulbs_count, + "incandescent_fixed_lighting_bulbs_count": epc.incandescent_fixed_lighting_bulbs_count, # Targets "sap_score": epc.energy_rating_current, "co2_emissions": epc.co2_emissions_current,