diff --git a/datatypes/epc/domain/epc_property_data.py b/datatypes/epc/domain/epc_property_data.py index 8795b389..4d9b4e24 100644 --- a/datatypes/epc/domain/epc_property_data.py +++ b/datatypes/epc/domain/epc_property_data.py @@ -250,6 +250,22 @@ class SapFlatDetails: unheated_corridor_length_m: Optional[int] = None +@dataclass +class RenewableHeatIncentive: + """The RHI block on the EPC — annual baseline kWh per end-use, plus SAP-estimated + impact of common insulation measures. + + Mapped 1:1 from the gov EPC API's `renewable_heat_incentive` object. Source of + baseline `space_heating_kwh` and `hot_water_kwh` for SAP10 properties (used as ML + training targets per ADR-0007). + """ + space_heating_kwh: float + water_heating_kwh: float + impact_of_loft_insulation_kwh: Optional[float] = None + impact_of_cavity_insulation_kwh: Optional[float] = None + impact_of_solid_wall_insulation_kwh: Optional[float] = None + + @dataclass class EpcPropertyData: # General @@ -352,7 +368,7 @@ class EpcPropertyData: potential_energy_efficiency_band: Optional[Epc] = ( None # not available in site notes ) - # renewable_heat_incentive: Optional[Any] = None # Not sure what this is, skip for now + renewable_heat_incentive: Optional[RenewableHeatIncentive] = None draughtproofed_door_count: Optional[int] = None mechanical_vent_duct_type: Optional[int] = None windows_transmission_details: Optional[WindowsTransmissionDetails] = None diff --git a/datatypes/epc/domain/mapper.py b/datatypes/epc/domain/mapper.py index ed20f367..18cefa35 100644 --- a/datatypes/epc/domain/mapper.py +++ b/datatypes/epc/domain/mapper.py @@ -11,6 +11,7 @@ from datatypes.epc.domain.epc_property_data import ( PhotovoltaicSupplyNoneOrNoDetails, PvBatteries, PvBattery, + RenewableHeatIncentive, SapAlternativeWall, SapBuildingPart, SapEnergySource, @@ -1249,6 +1250,27 @@ class EpcPropertyDataMapper: ) for bp in schema.sap_building_parts ], + renewable_heat_incentive=RenewableHeatIncentive( + space_heating_kwh=float( + schema.renewable_heat_incentive.space_heating_existing_dwelling + ), + water_heating_kwh=float(schema.renewable_heat_incentive.water_heating), + impact_of_loft_insulation_kwh=( + float(schema.renewable_heat_incentive.impact_of_loft_insulation) + if schema.renewable_heat_incentive.impact_of_loft_insulation is not None + else None + ), + impact_of_cavity_insulation_kwh=( + float(schema.renewable_heat_incentive.impact_of_cavity_insulation) + if schema.renewable_heat_incentive.impact_of_cavity_insulation is not None + else None + ), + impact_of_solid_wall_insulation_kwh=( + float(schema.renewable_heat_incentive.impact_of_solid_wall_insulation) + if schema.renewable_heat_incentive.impact_of_solid_wall_insulation is not None + else None + ), + ), ) @staticmethod @@ -1485,6 +1507,27 @@ class EpcPropertyDataMapper: ) for bp in schema.sap_building_parts ], + renewable_heat_incentive=RenewableHeatIncentive( + space_heating_kwh=float( + schema.renewable_heat_incentive.space_heating_existing_dwelling + ), + water_heating_kwh=float(schema.renewable_heat_incentive.water_heating), + impact_of_loft_insulation_kwh=( + float(schema.renewable_heat_incentive.impact_of_loft_insulation) + if schema.renewable_heat_incentive.impact_of_loft_insulation is not None + else None + ), + impact_of_cavity_insulation_kwh=( + float(schema.renewable_heat_incentive.impact_of_cavity_insulation) + if schema.renewable_heat_incentive.impact_of_cavity_insulation is not None + else None + ), + impact_of_solid_wall_insulation_kwh=( + float(schema.renewable_heat_incentive.impact_of_solid_wall_insulation) + if schema.renewable_heat_incentive.impact_of_solid_wall_insulation is not None + else None + ), + ), ) @staticmethod diff --git a/datatypes/epc/domain/tests/test_from_rdsap_schema.py b/datatypes/epc/domain/tests/test_from_rdsap_schema.py index 9e86ae42..a9d0119c 100644 --- a/datatypes/epc/domain/tests/test_from_rdsap_schema.py +++ b/datatypes/epc/domain/tests/test_from_rdsap_schema.py @@ -253,6 +253,20 @@ class TestFromRdSapSchema21_0_0: def test_property_type(self, result: EpcPropertyData) -> None: assert result.property_type == "0" + def test_renewable_heat_incentive(self, result: EpcPropertyData) -> None: + # Arrange — schema-21.0.0 sample JSON loaded via fixture + + # Act + rhi = result.renewable_heat_incentive + + # Assert + assert rhi is not None + assert rhi.space_heating_kwh == 13120.0 + assert rhi.water_heating_kwh == 2285.0 + assert rhi.impact_of_loft_insulation_kwh == -2114.0 + assert rhi.impact_of_cavity_insulation_kwh == -122.0 + assert rhi.impact_of_solid_wall_insulation_kwh == -3560.0 + # --------------------------------------------------------------------------- # Schema 21.0.1 (most comprehensive — full field coverage) @@ -532,3 +546,20 @@ class TestFromRdSapSchema21_0_1: def test_party_wall_length(self, result: EpcPropertyData) -> None: assert result.sap_building_parts[0].sap_floor_dimensions[0].party_wall_length_m == 7.9 + + # --- renewable heat incentive (RHI) --- + + def test_renewable_heat_incentive(self, result: EpcPropertyData) -> None: + # Arrange — schema-21.0.1 sample JSON loaded via fixture + + # Act + rhi = result.renewable_heat_incentive + + # Assert + assert rhi is not None + assert rhi.space_heating_kwh == 13120.0 + assert rhi.water_heating_kwh == 2285.0 + assert rhi.impact_of_loft_insulation_kwh == -2114.0 + assert rhi.impact_of_cavity_insulation_kwh == -122.0 + assert rhi.impact_of_solid_wall_insulation_kwh == -3560.0 + diff --git a/packages/domain/src/domain/ml/tests/test_transform.py b/packages/domain/src/domain/ml/tests/test_transform.py index ed828ef2..88538d47 100644 --- a/packages/domain/src/domain/ml/tests/test_transform.py +++ b/packages/domain/src/domain/ml/tests/test_transform.py @@ -1,6 +1,7 @@ -"""Tests for EpcMlTransform v0.1.0 — schema-contract surface.""" +"""Tests for EpcMlTransform v0.1.0 — schema-contract surface and target extraction.""" from domain.ml.schema import ColumnSpec, TransformSchema +from domain.ml.tests._fixtures import make_minimal_sap10_epc from domain.ml.transform import EpcMlTransform @@ -31,3 +32,25 @@ def test_transform_advertises_version_and_target_columns() -> None: assert isinstance(column, ColumnSpec) assert column.dtype is expected_dtype assert schema.feature_columns == {} + + +def test_to_row_extracts_targets_from_epc_property_data() -> None: + # Arrange + epc = make_minimal_sap10_epc( + energy_rating_current=82, + co2_emissions_current=2.7, + energy_consumption_current=232, + space_heating_kwh=10128.81, + water_heating_kwh=2166.19, + ) + transform = EpcMlTransform() + + # Act + row = transform.to_row(epc) + + # Assert + assert row["sap_score"] == 82 + assert row["co2_emissions"] == 2.7 + assert row["peui_raw"] == 232 + assert row["space_heating_kwh"] == 10128.81 + assert row["hot_water_kwh"] == 2166.19 diff --git a/packages/domain/src/domain/ml/transform.py b/packages/domain/src/domain/ml/transform.py index 7146bae3..5a28bc94 100644 --- a/packages/domain/src/domain/ml/transform.py +++ b/packages/domain/src/domain/ml/transform.py @@ -3,12 +3,16 @@ The single ML-data contract between this repo and the AutoGluon training repo. Versioned semver-style: MAJOR on removing/renaming columns, MINOR on adding. -At v0.1.0 only the schema contract is implemented — no feature columns yet. -Features are added incrementally per subsequent vertical slices. +At v0.1.0 the schema contract is fixed and the five directly-extractable targets +are populated by `to_row()`. The UCL-corrected PEUI target and all feature columns +are added in subsequent slices. See docs/adr/0007-kwh-as-ml-target.md for the target set and rationale. """ +from typing import Any + +from datatypes.epc.domain.epc_property_data import EpcPropertyData from domain.ml.schema import ColumnSpec, TransformSchema @@ -76,3 +80,18 @@ class EpcMlTransform: feature_columns={}, target_columns=dict(_TARGET_COLUMNS), ) + + def to_row(self, epc: EpcPropertyData) -> dict[str, Any]: + """Map an EpcPropertyData to a single row of features + targets. + + v0.1.0 populates the five directly-extractable targets. The UCL-corrected + PEUI target and all feature columns land in later slices. + """ + rhi = epc.renewable_heat_incentive + return { + "sap_score": epc.energy_rating_current, + "co2_emissions": epc.co2_emissions_current, + "peui_raw": epc.energy_consumption_current, + "space_heating_kwh": rhi.space_heating_kwh if rhi is not None else None, + "hot_water_kwh": rhi.water_heating_kwh if rhi is not None else None, + }