adding kwh feidls to EpcPropertyData and testing to_row

This commit is contained in:
Khalim Conn-Kowlessar 2026-05-16 14:33:25 +00:00
parent 611ff24eb6
commit a64e7e74c5
5 changed files with 136 additions and 4 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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,
}