From f4f9fc5b199d1e3c46609a82d3e7c90a5ff726af Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Wed, 4 Feb 2026 18:34:59 +0000 Subject: [PATCH] beginning to assembly the parity class --- backend/onboarders/base.py | 29 ++- backend/onboarders/epc/placeholder.py | 57 ----- backend/onboarders/epc_descriptions.py | 216 +------------------ backend/onboarders/mappings/built_form.py | 23 +- backend/onboarders/mappings/property_type.py | 10 +- backend/onboarders/parity.py | 58 +++-- datatypes/epc/__init__.py | 0 datatypes/epc/construction_age_band.py | 45 ++++ datatypes/epc/efficiency.py | 10 + datatypes/epc/fuel.py | 10 + datatypes/epc/heating_controls.py | 18 ++ datatypes/epc/hotwater.py | 8 + datatypes/epc/main_heating.py | 24 +++ datatypes/epc/property_type_built_form.py | 17 ++ datatypes/epc/roof.py | 86 ++++++++ datatypes/epc/walls.py | 74 +++++++ 16 files changed, 374 insertions(+), 311 deletions(-) delete mode 100644 backend/onboarders/epc/placeholder.py create mode 100644 datatypes/epc/__init__.py create mode 100644 datatypes/epc/construction_age_band.py create mode 100644 datatypes/epc/efficiency.py create mode 100644 datatypes/epc/fuel.py create mode 100644 datatypes/epc/heating_controls.py create mode 100644 datatypes/epc/hotwater.py create mode 100644 datatypes/epc/main_heating.py create mode 100644 datatypes/epc/property_type_built_form.py create mode 100644 datatypes/epc/roof.py create mode 100644 datatypes/epc/walls.py diff --git a/backend/onboarders/base.py b/backend/onboarders/base.py index 12ef9c94..258784f1 100644 --- a/backend/onboarders/base.py +++ b/backend/onboarders/base.py @@ -1,7 +1,30 @@ -class OnboarderBase: +import pandas as pd +from utils.s3 import read_from_s3 - def read(self): - pass + +class OnboarderBase: + data: pd.DataFrame | None = None + + def read_s3(self, bucket_name: str, file_name: str): + self.data = read_from_s3(bucket_name=bucket_name, s3_file_name=file_name) def write(self): pass + + @staticmethod + def assert_nulls_only_from_source_nulls(data: pd.DataFrame, original_column: str, mapped_column: str) -> bool: + # We only allow nulls if the original value was null + null_vals = data[pd.isnull(data[mapped_column])] + if null_vals.empty: + return True + # We make sure all original values were null + assert pd.isnull(null_vals[original_column]).all(), ( + f"Some values in {mapped_column} were not mapped, but original values were not null" + ) + + @staticmethod + def assert_no_nulls(data: pd.DataFrame, column: str): + assert pd.isnull(data[column]).sum() == 0, f"column {column} contains null values, but should not" + + def map_construction_age_band(self): + pass diff --git a/backend/onboarders/epc/placeholder.py b/backend/onboarders/epc/placeholder.py deleted file mode 100644 index 64807c49..00000000 --- a/backend/onboarders/epc/placeholder.py +++ /dev/null @@ -1,57 +0,0 @@ -from enum import Enum - - -class EpcFuel(Enum): - electricity_not_community = "electricity (not community)" - lpg_not_community = "LPG (not community)" - mains_gas_not_community = "mains gas (not community)" - oil_not_community = "oil (not community)" - manufactured_smokeless_fuel = "Solid fuel: manufactured smokeless fuel" - smokeless_coal = "smokeless coal" - - -class EpcHeatingControls(Enum): - programmer_room_thermostat_trvs = "Programmer, room thermostat and TRVs" - programmers_trvs_bypass = "Programmer, TRVs and bypass" - time_and_temperature_zone_control = "Time and temperature zone control" - - # Room heaters - programmer_and_appliance_thermostats = "Programmer and appliance thermostats" - appliance_thermostats = "Appliance thermostats" - - # Storage heaters - automatic_charge_control = "Automatic charge control" - manual_charge_control = "Manual charge control" - - # Warm air - programmer_and_atleast_two_room_thermostats = "Programmer and at least two room thermostats" - - -class EpcHeatingSystems(Enum): - # boiler and radiators - boiler_and_radiators_electric = "Boiler and radiators, electric" - boiler_and_radiators_lpg = "Boiler and radiators, LPG" - boiler_radiators_mains_gas = "Boiler and radiators, mains gas" - boiler_radiators_oil = "Boiler and radiators, oil" - # underfloor - electric_underfloor_heating = "Electric underfloor heating" - # ashp - air_to_air_ashp = "Air source heat pump, warm air, electric" - ashp_radiators_electric = "Air source heat pump, radiators, electric" - # Room heaters - room_heaters_electric = "Room heaters, electric" - room_heaters_mains_gas = "Room heaters, mains gas" - room_heaters_smokeless_fuel = "Room heaters, smokeless fuel" - room_heaters_coal = "Room heaters, coal" - # Storage heaters - electric_storage_heaters = "Electric storage heaters" - # Warm air - warm_air_electricaire = "Warm air, Electricaire" - warm_air_mains_gas = "Warm air, mains gas" - - -class EpcHotWaterSystems(Enum): - # from primary heating system - from_main_system = "From main system" - # Common for heater-based systems, e.g. room heaters or storage heaters - electric_immersion_off_peak = "Electric immersion, off-peak" diff --git a/backend/onboarders/epc_descriptions.py b/backend/onboarders/epc_descriptions.py index 57b4ab89..bfe6b07f 100644 --- a/backend/onboarders/epc_descriptions.py +++ b/backend/onboarders/epc_descriptions.py @@ -1,215 +1,11 @@ import pandas as pd -import re -from collections.abc import Mapping from enum import Enum -from typing import Callable, Union, List - - -class EpcConstructionAgeBand(Enum): - before_1900: str = 'England and Wales: before 1900' - from_1900_to_1929: str = 'England and Wales: 1900-1929' - from_1930_to_1949: str = 'England and Wales: 1930-1949' - from_1950_to_1966: str = 'England and Wales: 1950-1966' - from_1967_to_1975: str = 'England and Wales: 1967-1975' - from_1976_to_1982: str = 'England and Wales: 1976-1982' - from_1983_to_1990: str = 'England and Wales: 1983-1990' - from_1991_to_1995: str = 'England and Wales: 1991-1995' - from_1996_to_2002: str = 'England and Wales: 1996-2002' - from_2003_to_2006: str = 'England and Wales: 2003-2006' - from_2007_to_2011: str = 'England and Wales: 2007-2011' - from_2012_onwards: str = 'England and Wales: 2012-onwards' - from_2012_to_2022: str = 'England and Wales: 2012-2022' - from_2023_onwards: str = 'England and Wales: 2023 onwards' - - def start_year(self) -> int: - """ - Extract the starting year of the age band. - """ - value = self.value.lower() - - if 'before' in value: - return 0 - match = re.search(r'(\d{4})', value) - if not match: - raise ValueError(f"Cannot determine start year from '{self.value}'") - - return int(match.group(1)) - - @classmethod - def from_year_onwards(cls, year: int) -> List["EpcConstructionAgeBand"]: - """ - Return all age bands whose starting year is >= the given year. - """ - return [ - band - for band in cls - if band.start_year() >= year - ] - - -class EpcWallDescriptions(Enum): - # Cavity wall descriptions - cavity_insulated_assumed: str = "Cavity wall, as built, insulated (assumed)" - cavity_partial_insulated_assumed: str = "Cavity wall, as built, partial insulation (assumed)" - cavity_no_insulation_assumed: str = "Cavity wall, as built, no insulation (assumed)" - cavity_filled_cavity: str = "Cavity wall, filled cavity" - cavity_internal_insulation: str = "Cavity wall, with internal insulation" - cavity_external_insulation: str = "Cavity wall, with external insulation" - cavity_filled_plus_internal: str = "Cavity wall, filled cavity and internal insulation" - cavity_filled_plus_external: str = "Cavity wall, filled cavity and external insulation" - - # Solid wall descriptions - solid_brick_internal_insulation: str = "Solid brick, with internal insulation" - solid_brick_external_insulation: str = "Solid brick, with external insulation" - solid_brick_no_insulation_assumed: str = 'Solid brick, as built, no insulation (assumed)' - solid_brick_partial_insulated_assumed: str = 'Solid brick, as built, partial insulation (assumed)' - solid_brick_insulated_assumed: str = 'Solid brick, as built, insulated (assumed)' - - # System - system_external_insulation: str = "System built, with external insulation" - system_internal_insulation: str = "System built, with internal insulation" - system_no_insulation_assumed: str = "System built, as built, no insulation (assumed)" - system_partial_insulated_assumed: str = "System built, as built, partial insulation (assumed)" - system_insulated_assumed: str = "System built, as built, insulated (assumed)" - - # Timber - timber_frame_internal_insulation: str = "Timber frame, with internal insulation" - timber_frame_external_insulation: str = "Timber frame, with external insulation" - timber_frame_no_insulation_assumed: str = "Timber frame, as built, no insulation (assumed)" - timber_frame_partial_insulated_assumed: str = "Timber frame, as built, partial insulation (assumed)" - timber_frame_insulated_assumed: str = "Timber frame, as built, insulated (assumed)" - - # Granite/whinstone - granite_whinstone_external_insulation: str = "Granite or whin, with external insulation" - granite_whinstone_internal_insulation: str = "Granite or whin, with internal insulation" - granite_whinstone_no_insulation_assumed: str = "Granite or whin, as built, no insulation (assumed)" - granite_whinstone_partial_insulated_assumed: str = "Granite or whin, as built, partial insulation (assumed)" - granite_whinestone_insulated_assumed: str = "Granite or whin, as built, insulated (assumed)" - - # Sandstone/limestone - sandstone_limestone_internal_insulation: str = "Sandstone, with internal insulation" - sandstone_limestone_external_insulation: str = "Sandstone, with external insulation" - sandstone_limestone_no_insulation_assumed: str = "Sandstone, as built, no insulation (assumed)" - sandstone_limestone_partial_insulated_assumed: str = "Sandstone, as built, partial insulation (assumed)" - sandstone_limestone_insulated_assumed: str = "Sandstone, as built, insulated (assumed)" - - # Cob - cob_as_built_average = "Cob, as built" - cob_as_built_good = "Cob, as built" - - # unknown descriptions which may get mapped later or handled via fallback - cavity_as_built_unknown = "Cavity wall, as built, unknown insulation" - solid_brick_as_built_unknown = "Solid brick, as built, unknown insulation" - system_as_built_unknown = "System built, as built, unknown insulation" - timber_frame_as_built_unknown = "Timber frame, as built, unknown insulation" - granite_as_built_unknown = "Granite or whin, as built, unknown insulation" - sandstone_as_built_unknown = "Sandstone, as built, unknown insulation" - cob_as_built_unknown = "Cob, as built, unknown insulation" - - @property - def unknown_descriptions(self) -> List["EpcWallDescriptions"]: - return [ - EpcWallDescriptions.cavity_as_built_unknown, - EpcWallDescriptions.solid_brick_as_built_unknown, - EpcWallDescriptions.system_as_built_unknown, - EpcWallDescriptions.timber_frame_as_built_unknown, - EpcWallDescriptions.granite_as_built_unknown, - EpcWallDescriptions.sandstone_as_built_unknown, - EpcWallDescriptions.cob_as_built_unknown, - ] - - -class EpcRoofDescriptions(Enum): - # Loft - # Assumed options - pitched_insulated_assumed = "Pitched, insulated (assumed)" - pitched_no_insulation = "Pitched, no insulation" - # Insulation thickness options - loft_12mm_insulation: str = "Pitched, 12 mm loft insulation" - loft_25mm_insulation: str = "Pitched, 25 mm loft insulation" - loft_50mm_insulation: str = "Pitched, 50 mm loft insulation" - loft_75mm_insulation: str = "Pitched, 75 mm loft insulation" - loft_100mm_insulation: str = "Pitched, 100 mm loft insulation" - loft_125mm_insulation: str = "Pitched, 125 mm loft insulation" - loft_150mm_insulation: str = "Pitched, 150 mm loft insulation" - loft_175mm_insulation: str = "Pitched, 175 mm loft insulation" - loft_200mm_insulation: str = "Pitched, 200 mm loft insulation" - loft_250mm_insulation: str = "Pitched, 250 mm loft insulation" - loft_270mm_insulation: str = "Pitched, 270 mm loft insulation" - loft_300mm_insulation: str = "Pitched, 300 mm loft insulation" - loft_350mm_insulation: str = "Pitched, 350 mm loft insulation" - loft_400mm_plus_insulation: str = "Pitched, 400+ mm loft insulation" - # Insulated at rafters "Pitched, insulated at rafters" - # Rafters - # 400mm, 350mm = very good - # 200-300mm = good - # 125-175 = average - # 50-100 = poor - # 25 and below= very poor - loft_insulated_at_rafters: str = "Pitched, insulated at rafters" - # another dwelling above - another_dwelling_above: str = "(another dwelling above)" - # flat roof, which if there is observed insulation is just "flat, insulated", however there is a - # different efficiency rating depending on insulation thickness - # categories: - # 12mm = very poor & has limited insulation description - # 25, 50 = poor & has limited insulation description - # 75, 100, 125mm = average (Flat, insulated) - # 150, 175, 200, 225, 250mm = good (Flat, insulated) - # 270mm+ = very good (Flat, insulated) - # As built 2023 = Flat, insulated, Very good - # 2003 - 2006, up to 2012-2022 = Flat insulated, Good - # 1983-1990, 1996-2002 = Flat, insulated, Average - # 1976-1982 = Flat, limited insulation, poor - # 1967 - 1975 = Flat, limited insulation, Very Poor - # 1950-1966 and earlier bands = flat, no insulation, very poor - - flat_insulated = "Flat, insulated" - flat_limited_insulation = "Flat, limited insulation" - flat_no_insulation = "Flat, no insulation" - - # Thatched roof descriptions - # With Loft insulation at joists - # Thatched + 12mm = thatched, with additional insulation, average - # Thatched + 25, 50, 100, 150mm = thatched, with additional insulation, good - # Thatched + 175mm+ = thatched, with additional insulation, very good - # With loft insulation at rafters [out of scope atm] - # Unknown insulation - # Pre 1900, 1930-1949, 1967-1975, 1983-1990, 1996-2002 = "Thatched", Average - # 2003-2006, 2012-2022 = "Thatched", Good - # 2023 onwards = "Thatched", Very Good - thatched = "Thatched" # We see this for no insulation, has average performance - thatched_with_additional_insulation: str = "Thatched, with additional insulation" - - # Sloping ceiling - # For sloping ceiling tags, we don't use any (assumed) tags so that it's unambiguous that the roof is sloped - sloping_pitched_no_insulation: str = "Pitched, no insulation" - sloping_pitched_limited_insulation: str = "Pitched, limited insulation" - sloping_pitched_insulated: str = "Pitched, insulated" - - # Unknown descriptions which may get mapped later or handled via fallback - flat_as_built_unknown: str = "Flat, as built, unknown insulation" - loft_as_built_unknown: str = "Loft, as built, unknown insulation" - thatched_as_built_unknown: str = "Thatched, as built, unknown insulation" - sloping_pitched_as_built_unknown: str = "Pitched, as built, unknown insulation" - - @property - def unknown_descriptions(self) -> List["EpcRoofDescriptions"]: - return [ - EpcRoofDescriptions.flat_as_built_unknown, - EpcRoofDescriptions.loft_as_built_unknown, - EpcRoofDescriptions.thatched_as_built_unknown, - EpcRoofDescriptions.sloping_pitched_as_built_unknown, - ] - - -class EpcEfficiency(Enum): - VERY_POOR = "Very Poor" - POOR = "Poor" - AVERAGE = "Average" - GOOD = "Good" - VERY_GOOD = "Very Good" - NA = "N/A" +from collections.abc import Mapping +from typing import Callable, Union +from datatypes.epc.construction_age_band import EpcConstructionAgeBand +from datatypes.epc.efficiency import EpcEfficiency +from datatypes.epc.walls import EpcWallDescriptions +from datatypes.epc.roof import EpcRoofDescriptions def cavity_filled_efficiency(age_band: EpcConstructionAgeBand) -> EpcEfficiency: diff --git a/backend/onboarders/mappings/built_form.py b/backend/onboarders/mappings/built_form.py index 23901fc6..12ae6360 100644 --- a/backend/onboarders/mappings/built_form.py +++ b/backend/onboarders/mappings/built_form.py @@ -1,15 +1,10 @@ -parity_map = { - "MidTerrace": "Mid-Terrace", - "EndTerrace": "End-Terrace", - "Detached": "Detached", - "SemiDetached": "Semi-Detached", - "EnclosedMidTerrace": "Enclosed Mid-Terrace", - "EnclosedEndTerrace": "Enclosed End-Terrace", -} +from datatypes.epc.property_type_built_form import BuiltForm -# MidTerrace 41462 -# EndTerrace 20910 -# Detached 16875 -# SemiDetached 14725 -# EnclosedMidTerrace 3176 -# EnclosedEndTerrace 2393 +parity_map = { + "MidTerrace": BuiltForm.mid_terrace, + "EndTerrace": BuiltForm.end_terrace, + "Detached": BuiltForm.detached, + "SemiDetached": BuiltForm.semi_detached, + "EnclosedMidTerrace": BuiltForm.enclosed_mid_terrace, + "EnclosedEndTerrace": BuiltForm.enclosed_end_terrace, +} diff --git a/backend/onboarders/mappings/property_type.py b/backend/onboarders/mappings/property_type.py index 75deef04..f91c0c88 100644 --- a/backend/onboarders/mappings/property_type.py +++ b/backend/onboarders/mappings/property_type.py @@ -1,6 +1,8 @@ +from datatypes.epc.property_type_built_form import PropertyType + parity_map = { - "Flat": "Flat", - "Maisonette": "Maisonette", - "Bungalow": "Bungalow", - "House": "House", + "Flat": PropertyType.flat, + "Maisonette": PropertyType.maisonette, + "Bungalow": PropertyType.bungalow, + "House": PropertyType.house, } diff --git a/backend/onboarders/parity.py b/backend/onboarders/parity.py index 88e548c2..c1931437 100644 --- a/backend/onboarders/parity.py +++ b/backend/onboarders/parity.py @@ -2,12 +2,16 @@ import re from numpy import nan from tqdm import tqdm import pandas as pd +from backend.onboarders.base import OnboarderBase from backend.onboarders.mappings.property_type import parity_map as property_map from backend.onboarders.mappings.age_band import parity_map as age_band_map from backend.onboarders.mappings.built_form import parity_map as built_form_map from backend.onboarders.epc_descriptions import EpcWallDescriptions, EpcConstructionAgeBand, EpcEfficiency, \ WALL_DESCRIPTION_EFFICIENCIES, EpcRoofDescriptions, resolve_roof_efficiency, EpcFloorDescriptions -from backend.onboarders.epc.placeholder import EpcFuel, EpcHeatingControls, EpcHeatingSystems, EpcHotWaterSystems +from datatypes.epc.fuel import EpcFuel +from datatypes.epc.heating_controls import EpcHeatingControls +from datatypes.epc.main_heating import EpcHeatingSystems +from datatypes.epc.hotwater import EpcHotWaterSystems from backend.onboarders.mappings.as_built_wall_classifiers import AS_BUILT_WALL_CLASSIFIERS from backend.onboarders.mappings.as_built_roof_classifiers import AS_BUILT_ROOF_CLASSIFIERS from backend.onboarders.mappings.as_built_floor_classifiers import unknown_floor_as_built, unknown_floor_retrofitted, \ @@ -15,18 +19,6 @@ from backend.onboarders.mappings.as_built_floor_classifiers import unknown_floor tqdm.pandas() - -def check_nulls(data, original_column, mapped_column): - # We only allow nulls if the oroginal value was null - null_vals = data[pd.isnull(data[mapped_column])] - if null_vals.empty: - return True - # We make sure all original values were null - assert pd.isnull(null_vals[original_column]).all(), ( - f"Some values in {mapped_column} were not mapped, but original values were not null" - ) - - # Sample input data data = pd.read_excel( "/Users/khalimconn-kowlessar/Documents/hestia/Customers/Peabody/Nov 2025 Consulting Project/2025_11_11 - Peabody " @@ -34,21 +26,41 @@ data = pd.read_excel( sheet_name="Sustainability" ) + +class ParityOnboarder(OnboarderBase): + + def __init__( + self, + fileuri: str, + ): + # Extract bucket, and filekey; Will be in the format s3://bucket/key + bucket_name = fileuri.split("/")[2] + file_name = "/".join(fileuri.split("/")[3:]) + + self.read_s3(bucket_name=bucket_name, file_name=file_name) + pass + + def map_construction_age_band(self): + data["construction_age_band"] = data["Construction Years"].map(age_band_map) + self.assert_nulls_only_from_source_nulls(data, "Construction Years", "construction_age_band") + + def map_property_type(self): + data["property_type"] = data["Type"].map(property_map) + self.assert_no_nulls(data, "property_type") + + def process(self): + # ------------ construction_age_band ------------ + self.map_construction_age_band() + + # ------------ property_type ------------ + self.map_property_type() + + # We want to map the parity fields to standard EPC references. This will allow us to # 1) Estimate EPCs, more accurately # 2) Patch incorrect EPCs with ease # 3) Indicate already installed measures -# ------------ construction_age_band ------------ - -data["construction_age_band"] = data["Construction Years"].map(age_band_map) - -check_nulls(data, "Construction Years", "construction_age_band") - -# ------------ property_type ------------ -data["property_type"] = data["Type"].map(property_map) - -assert pd.isnull(data["property_type"]).sum() == 0, "Some property types were not mapped" # ------------ built_form ------------ data["built_form"] = data["Attachment"].map(built_form_map) diff --git a/datatypes/epc/__init__.py b/datatypes/epc/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/datatypes/epc/construction_age_band.py b/datatypes/epc/construction_age_band.py new file mode 100644 index 00000000..c5e7a03b --- /dev/null +++ b/datatypes/epc/construction_age_band.py @@ -0,0 +1,45 @@ +import re +from enum import Enum +from typing import List + + +class EpcConstructionAgeBand(Enum): + before_1900: str = 'England and Wales: before 1900' + from_1900_to_1929: str = 'England and Wales: 1900-1929' + from_1930_to_1949: str = 'England and Wales: 1930-1949' + from_1950_to_1966: str = 'England and Wales: 1950-1966' + from_1967_to_1975: str = 'England and Wales: 1967-1975' + from_1976_to_1982: str = 'England and Wales: 1976-1982' + from_1983_to_1990: str = 'England and Wales: 1983-1990' + from_1991_to_1995: str = 'England and Wales: 1991-1995' + from_1996_to_2002: str = 'England and Wales: 1996-2002' + from_2003_to_2006: str = 'England and Wales: 2003-2006' + from_2007_to_2011: str = 'England and Wales: 2007-2011' + from_2012_onwards: str = 'England and Wales: 2012-onwards' + from_2012_to_2022: str = 'England and Wales: 2012-2022' + from_2023_onwards: str = 'England and Wales: 2023 onwards' + + def start_year(self) -> int: + """ + Extract the starting year of the age band. + """ + value = self.value.lower() + + if 'before' in value: + return 0 + match = re.search(r'(\d{4})', value) + if not match: + raise ValueError(f"Cannot determine start year from '{self.value}'") + + return int(match.group(1)) + + @classmethod + def from_year_onwards(cls, year: int) -> List["EpcConstructionAgeBand"]: + """ + Return all age bands whose starting year is >= the given year. + """ + return [ + band + for band in cls + if band.start_year() >= year + ] diff --git a/datatypes/epc/efficiency.py b/datatypes/epc/efficiency.py new file mode 100644 index 00000000..0417f49e --- /dev/null +++ b/datatypes/epc/efficiency.py @@ -0,0 +1,10 @@ +from enum import Enum + + +class EpcEfficiency(Enum): + VERY_POOR: str = "Very Poor" + POOR: str = "Poor" + AVERAGE: str = "Average" + GOOD: str = "Good" + VERY_GOOD: str = "Very Good" + NA: str = "N/A" diff --git a/datatypes/epc/fuel.py b/datatypes/epc/fuel.py new file mode 100644 index 00000000..0d1e455c --- /dev/null +++ b/datatypes/epc/fuel.py @@ -0,0 +1,10 @@ +from enum import Enum + + +class EpcFuel(Enum): + electricity_not_community = "electricity (not community)" + lpg_not_community = "LPG (not community)" + mains_gas_not_community = "mains gas (not community)" + oil_not_community = "oil (not community)" + manufactured_smokeless_fuel = "Solid fuel: manufactured smokeless fuel" + smokeless_coal = "smokeless coal" diff --git a/datatypes/epc/heating_controls.py b/datatypes/epc/heating_controls.py new file mode 100644 index 00000000..48538bff --- /dev/null +++ b/datatypes/epc/heating_controls.py @@ -0,0 +1,18 @@ +from enum import Enum + + +class EpcHeatingControls(Enum): + programmer_room_thermostat_trvs = "Programmer, room thermostat and TRVs" + programmers_trvs_bypass = "Programmer, TRVs and bypass" + time_and_temperature_zone_control = "Time and temperature zone control" + + # Room heaters + programmer_and_appliance_thermostats = "Programmer and appliance thermostats" + appliance_thermostats = "Appliance thermostats" + + # Storage heaters + automatic_charge_control = "Automatic charge control" + manual_charge_control = "Manual charge control" + + # Warm air + programmer_and_atleast_two_room_thermostats = "Programmer and at least two room thermostats" diff --git a/datatypes/epc/hotwater.py b/datatypes/epc/hotwater.py new file mode 100644 index 00000000..96af2be3 --- /dev/null +++ b/datatypes/epc/hotwater.py @@ -0,0 +1,8 @@ +from enum import Enum + + +class EpcHotWaterSystems(Enum): + # from primary heating system + from_main_system = "From main system" + # Common for heater-based systems, e.g. room heaters or storage heaters + electric_immersion_off_peak = "Electric immersion, off-peak" diff --git a/datatypes/epc/main_heating.py b/datatypes/epc/main_heating.py new file mode 100644 index 00000000..663ada99 --- /dev/null +++ b/datatypes/epc/main_heating.py @@ -0,0 +1,24 @@ +from enum import Enum + + +class EpcHeatingSystems(Enum): + # boiler and radiators + boiler_and_radiators_electric = "Boiler and radiators, electric" + boiler_and_radiators_lpg = "Boiler and radiators, LPG" + boiler_radiators_mains_gas = "Boiler and radiators, mains gas" + boiler_radiators_oil = "Boiler and radiators, oil" + # underfloor + electric_underfloor_heating = "Electric underfloor heating" + # ashp + air_to_air_ashp = "Air source heat pump, warm air, electric" + ashp_radiators_electric = "Air source heat pump, radiators, electric" + # Room heaters + room_heaters_electric = "Room heaters, electric" + room_heaters_mains_gas = "Room heaters, mains gas" + room_heaters_smokeless_fuel = "Room heaters, smokeless fuel" + room_heaters_coal = "Room heaters, coal" + # Storage heaters + electric_storage_heaters = "Electric storage heaters" + # Warm air + warm_air_electricaire = "Warm air, Electricaire" + warm_air_mains_gas = "Warm air, mains gas" diff --git a/datatypes/epc/property_type_built_form.py b/datatypes/epc/property_type_built_form.py new file mode 100644 index 00000000..2fd59ddf --- /dev/null +++ b/datatypes/epc/property_type_built_form.py @@ -0,0 +1,17 @@ +from enum import Enum + + +class PropertyType(Enum): + flat = "Flat" + maisonette = "Maisonette" + bungalow = "Bungalow" + house = "House" + + +class BuiltForm(Enum): + mid_terrace = "Mid-Terrace" + end_terrace = "End-Terrace" + detached = "Detached" + semi_detached = "Semi-Detached" + enclosed_mid_terrace = "Enclosed Mid-Terrace" + enclosed_end_terrace = "Enclosed End-Terrace" diff --git a/datatypes/epc/roof.py b/datatypes/epc/roof.py new file mode 100644 index 00000000..9cdaac96 --- /dev/null +++ b/datatypes/epc/roof.py @@ -0,0 +1,86 @@ +from enum import Enum +from typing import List + + +class EpcRoofDescriptions(Enum): + # Loft + # Assumed options + pitched_insulated_assumed: str = "Pitched, insulated (assumed)" + pitched_no_insulation: str = "Pitched, no insulation" + # Insulation thickness options + loft_12mm_insulation: str = "Pitched, 12 mm loft insulation" + loft_25mm_insulation: str = "Pitched, 25 mm loft insulation" + loft_50mm_insulation: str = "Pitched, 50 mm loft insulation" + loft_75mm_insulation: str = "Pitched, 75 mm loft insulation" + loft_100mm_insulation: str = "Pitched, 100 mm loft insulation" + loft_125mm_insulation: str = "Pitched, 125 mm loft insulation" + loft_150mm_insulation: str = "Pitched, 150 mm loft insulation" + loft_175mm_insulation: str = "Pitched, 175 mm loft insulation" + loft_200mm_insulation: str = "Pitched, 200 mm loft insulation" + loft_250mm_insulation: str = "Pitched, 250 mm loft insulation" + loft_270mm_insulation: str = "Pitched, 270 mm loft insulation" + loft_300mm_insulation: str = "Pitched, 300 mm loft insulation" + loft_350mm_insulation: str = "Pitched, 350 mm loft insulation" + loft_400mm_plus_insulation: str = "Pitched, 400+ mm loft insulation" + # Insulated at rafters "Pitched, insulated at rafters" + # Rafters + # 400mm, 350mm = very good + # 200-300mm = good + # 125-175 = average + # 50-100 = poor + # 25 and below= very poor + loft_insulated_at_rafters: str = "Pitched, insulated at rafters" + # another dwelling above + another_dwelling_above: str = "(another dwelling above)" + # flat roof, which if there is observed insulation is just "flat, insulated", however there is a + # different efficiency rating depending on insulation thickness + # categories: + # 12mm = very poor & has limited insulation description + # 25, 50 = poor & has limited insulation description + # 75, 100, 125mm = average (Flat, insulated) + # 150, 175, 200, 225, 250mm = good (Flat, insulated) + # 270mm+ = very good (Flat, insulated) + # As built 2023 = Flat, insulated, Very good + # 2003 - 2006, up to 2012-2022 = Flat insulated, Good + # 1983-1990, 1996-2002 = Flat, insulated, Average + # 1976-1982 = Flat, limited insulation, poor + # 1967 - 1975 = Flat, limited insulation, Very Poor + # 1950-1966 and earlier bands = flat, no insulation, very poor + + flat_insulated: str = "Flat, insulated" + flat_limited_insulation: str = "Flat, limited insulation" + flat_no_insulation: str = "Flat, no insulation" + + # Thatched roof descriptions + # With Loft insulation at joists + # Thatched + 12mm = thatched, with additional insulation, average + # Thatched + 25, 50, 100, 150mm = thatched, with additional insulation, good + # Thatched + 175mm+ = thatched, with additional insulation, very good + # With loft insulation at rafters [out of scope atm] + # Unknown insulation + # Pre 1900, 1930-1949, 1967-1975, 1983-1990, 1996-2002 = "Thatched", Average + # 2003-2006, 2012-2022 = "Thatched", Good + # 2023 onwards = "Thatched", Very Good + thatched: str = "Thatched" # We see this for no insulation, has average performance + thatched_with_additional_insulation: str = "Thatched, with additional insulation" + + # Sloping ceiling + # For sloping ceiling tags, we don't use any (assumed) tags so that it's unambiguous that the roof is sloped + sloping_pitched_no_insulation: str = "Pitched, no insulation" + sloping_pitched_limited_insulation: str = "Pitched, limited insulation" + sloping_pitched_insulated: str = "Pitched, insulated" + + # Unknown descriptions which may get mapped later or handled via fallback + flat_as_built_unknown: str = "Flat, as built, unknown insulation" + loft_as_built_unknown: str = "Loft, as built, unknown insulation" + thatched_as_built_unknown: str = "Thatched, as built, unknown insulation" + sloping_pitched_as_built_unknown: str = "Pitched, as built, unknown insulation" + + @property + def unknown_descriptions(self) -> List["EpcRoofDescriptions"]: + return [ + EpcRoofDescriptions.flat_as_built_unknown, + EpcRoofDescriptions.loft_as_built_unknown, + EpcRoofDescriptions.thatched_as_built_unknown, + EpcRoofDescriptions.sloping_pitched_as_built_unknown, + ] diff --git a/datatypes/epc/walls.py b/datatypes/epc/walls.py new file mode 100644 index 00000000..44ca7e49 --- /dev/null +++ b/datatypes/epc/walls.py @@ -0,0 +1,74 @@ +from enum import Enum +from typing import List + + +class EpcWallDescriptions(Enum): + # Cavity wall descriptions + cavity_insulated_assumed: str = "Cavity wall, as built, insulated (assumed)" + cavity_partial_insulated_assumed: str = "Cavity wall, as built, partial insulation (assumed)" + cavity_no_insulation_assumed: str = "Cavity wall, as built, no insulation (assumed)" + cavity_filled_cavity: str = "Cavity wall, filled cavity" + cavity_internal_insulation: str = "Cavity wall, with internal insulation" + cavity_external_insulation: str = "Cavity wall, with external insulation" + cavity_filled_plus_internal: str = "Cavity wall, filled cavity and internal insulation" + cavity_filled_plus_external: str = "Cavity wall, filled cavity and external insulation" + + # Solid wall descriptions + solid_brick_internal_insulation: str = "Solid brick, with internal insulation" + solid_brick_external_insulation: str = "Solid brick, with external insulation" + solid_brick_no_insulation_assumed: str = 'Solid brick, as built, no insulation (assumed)' + solid_brick_partial_insulated_assumed: str = 'Solid brick, as built, partial insulation (assumed)' + solid_brick_insulated_assumed: str = 'Solid brick, as built, insulated (assumed)' + + # System + system_external_insulation: str = "System built, with external insulation" + system_internal_insulation: str = "System built, with internal insulation" + system_no_insulation_assumed: str = "System built, as built, no insulation (assumed)" + system_partial_insulated_assumed: str = "System built, as built, partial insulation (assumed)" + system_insulated_assumed: str = "System built, as built, insulated (assumed)" + + # Timber + timber_frame_internal_insulation: str = "Timber frame, with internal insulation" + timber_frame_external_insulation: str = "Timber frame, with external insulation" + timber_frame_no_insulation_assumed: str = "Timber frame, as built, no insulation (assumed)" + timber_frame_partial_insulated_assumed: str = "Timber frame, as built, partial insulation (assumed)" + timber_frame_insulated_assumed: str = "Timber frame, as built, insulated (assumed)" + + # Granite/whinstone + granite_whinstone_external_insulation: str = "Granite or whin, with external insulation" + granite_whinstone_internal_insulation: str = "Granite or whin, with internal insulation" + granite_whinstone_no_insulation_assumed: str = "Granite or whin, as built, no insulation (assumed)" + granite_whinstone_partial_insulated_assumed: str = "Granite or whin, as built, partial insulation (assumed)" + granite_whinestone_insulated_assumed: str = "Granite or whin, as built, insulated (assumed)" + + # Sandstone/limestone + sandstone_limestone_internal_insulation: str = "Sandstone, with internal insulation" + sandstone_limestone_external_insulation: str = "Sandstone, with external insulation" + sandstone_limestone_no_insulation_assumed: str = "Sandstone, as built, no insulation (assumed)" + sandstone_limestone_partial_insulated_assumed: str = "Sandstone, as built, partial insulation (assumed)" + sandstone_limestone_insulated_assumed: str = "Sandstone, as built, insulated (assumed)" + + # Cob + cob_as_built_average: str = "Cob, as built" + cob_as_built_good: str = "Cob, as built" + + # unknown descriptions which may get mapped later or handled via fallback + cavity_as_built_unknown: str = "Cavity wall, as built, unknown insulation" + solid_brick_as_built_unknown: str = "Solid brick, as built, unknown insulation" + system_as_built_unknown: str = "System built, as built, unknown insulation" + timber_frame_as_built_unknown: str = "Timber frame, as built, unknown insulation" + granite_as_built_unknown: str = "Granite or whin, as built, unknown insulation" + sandstone_as_built_unknown: str = "Sandstone, as built, unknown insulation" + cob_as_built_unknown: str = "Cob, as built, unknown insulation" + + @property + def unknown_descriptions(self) -> List["EpcWallDescriptions"]: + return [ + EpcWallDescriptions.cavity_as_built_unknown, + EpcWallDescriptions.solid_brick_as_built_unknown, + EpcWallDescriptions.system_as_built_unknown, + EpcWallDescriptions.timber_frame_as_built_unknown, + EpcWallDescriptions.granite_as_built_unknown, + EpcWallDescriptions.sandstone_as_built_unknown, + EpcWallDescriptions.cob_as_built_unknown, + ]