diff --git a/asset_list/app.py b/asset_list/app.py index 01906c5f..969b0184 100644 --- a/asset_list/app.py +++ b/asset_list/app.py @@ -59,24 +59,24 @@ def app(): Property UPRN """ - data_folder = "/Users/khalimconn-kowlessar/Documents/hestia/Customers/Hackney" - data_filename = "Domna SHF Wave 3 (3).xlsx" - sheet_name = "Domna Wave 3" - postcode_column = 'Postcode' - address1_column = "Address 1" - address1_method = None - fulladdress_column = None - address_cols_to_concat = ["Address 1"] + data_folder = "/Users/khalimconn-kowlessar/Documents/hestia/Customers/NCHA/20260129 SAL" + data_filename = "NCHA ASSET LIST 1.xlsx" + sheet_name = "NCHA ASSET LIST" + postcode_column = 'POSTCODE' + address1_column = None + address1_method = "house_number_extraction" + fulladdress_column = 'ADDRESS' + address_cols_to_concat = [] missing_postcodes_method = None - landlord_year_built = "Construction Years" - landlord_os_uprn = "UPRN" - landlord_property_type = "Type" - landlord_built_form = "Attachment" - landlord_wall_construction = "Wall type" + landlord_year_built = None + landlord_os_uprn = None + landlord_property_type = "PROPERTY TYPE" + landlord_built_form = "BUILD FORM" + landlord_wall_construction = "wall combined" landlord_roof_construction = None landlord_heating_system = None landlord_existing_pv = None - landlord_property_id = "Row ID" + landlord_property_id = "UPRN" landlord_sap = None outcomes_filename = None outcomes_sheetname = None diff --git a/asset_list/mappings/property_type.py b/asset_list/mappings/property_type.py index 1f251598..703cb8ef 100644 --- a/asset_list/mappings/property_type.py +++ b/asset_list/mappings/property_type.py @@ -427,6 +427,8 @@ PROPERTY_MAPPING = { 'End Terrace': 'unknown', 'Detached': 'unknown', 'Mid-terrace': 'unknown', - 'MID - TERRACE': 'unknown' + 'MID - TERRACE': 'unknown', + 'COMOFF': 'unknown', + 'LOTS': 'unknown' } diff --git a/asset_list/mappings/walls.py b/asset_list/mappings/walls.py index 418ae9f8..1bb02a9a 100644 --- a/asset_list/mappings/walls.py +++ b/asset_list/mappings/walls.py @@ -354,6 +354,15 @@ WALL_CONSTRUCTION_MAPPINGS = { 'System built Internal': 'insulated system built', 'Cavity: AsBuilt (1976-1982), TimberFrame: AsBuilt': 'cavity unknown insulation', - 'Cavity: FilledCavityPlusExternal': 'filled cavity' + 'Cavity: FilledCavityPlusExternal': 'filled cavity', + + 'Cavity, Filled Cavity': 'filled cavity', + 'Solid Brick, As Built': 'solid brick unknown insulation', + 'Cavity, As Built': 'cavity unknown insulation', + 'Sandstone, As Built': 'sandstone or limestone unknown insulation', + 'Timber Frame, As Built': 'timber frame unknown insulation', + 'Solid Brick, Internal Insulation': 'insulated solid brick', + 'Granite or Whinstone, As Built': 'granite or whinstone unknown insulation', + 'Solid Brick, External': 'insulated solid brick' } diff --git a/backend/onboarders/base.py b/backend/onboarders/base.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/onboarders/epc_descriptions.py b/backend/onboarders/epc_descriptions.py new file mode 100644 index 00000000..a674e332 --- /dev/null +++ b/backend/onboarders/epc_descriptions.py @@ -0,0 +1,247 @@ +import re +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" + + +class EpcEfficiency(Enum): + VERY_POOR = "Very Poor" + POOR = "Poor" + AVERAGE = "Average" + GOOD = "Good" + VERY_GOOD = "Very Good" + NA = "N/A" + + +EfficiencyRule = Union[ + EpcEfficiency, + Callable[[EpcConstructionAgeBand], EpcEfficiency], +] + + +def cavity_filled_efficiency(age_band: EpcConstructionAgeBand) -> EpcEfficiency: + """" + Maps cavity filled to efficiency based on construction age band. + :param age_band: EpcConstructionAgeBand + :return: EpcEfficiency + """ + if age_band in { + EpcConstructionAgeBand.from_2023_onwards + }: + return EpcEfficiency.VERY_GOOD + + return EpcEfficiency.GOOD + + +def internal_external_insulation_efficiency( + age_band: EpcConstructionAgeBand, +) -> EpcEfficiency: + """ + Maps: + - cavity unfilled with internal/external insulation to efficiency based on construction age band. We assumed + based on 100mm insulation + - solid brick with internal/external insulation to efficiency based on construction age band. We assumed + based on 100mm insulation + - system built with internal/external insulation to efficiency based on construction age band. We assumed + based on 100mm insulation + + All of these wall types have the same behaviour in elmhurst + :param age_band: EpcConstructionAgeBand + :return: EpcEfficiency + """ + if age_band in { + EpcConstructionAgeBand.from_1983_to_1990, + EpcConstructionAgeBand.from_1991_to_1995, + EpcConstructionAgeBand.from_1996_to_2002, + EpcConstructionAgeBand.from_2003_to_2006, + EpcConstructionAgeBand.from_2007_to_2011, + EpcConstructionAgeBand.from_2012_to_2022, + EpcConstructionAgeBand.from_2023_onwards, + }: + return EpcEfficiency.VERY_GOOD + + return EpcEfficiency.GOOD + + +def timber_granite_sandstone_internal_external_efficiency(age_band: EpcConstructionAgeBand) -> EpcEfficiency: + """" + Maps: + - timber frame with internal/external wall insulation to efficiency based on construction age band. + - sandstone/limestone with internal/external wall insulation to efficiency based on construction age band. + - granite/whinstone with internal/external wall insulation to efficiency based on construction age band. + :param age_band: EpcConstructionAgeBand + :return: EpcEfficiency + """ + if age_band in { + EpcConstructionAgeBand.from_2023_onwards + }: + return EpcEfficiency.VERY_GOOD + + return EpcEfficiency.GOOD + + +WALL_DESCRIPTION_METADATA = { + # Note: all function mappings have been defined based on Elmhurst + # Cavity + # value mappings + EpcWallDescriptions.cavity_no_insulation_assumed: EpcEfficiency.POOR, + EpcWallDescriptions.cavity_partial_insulated_assumed: EpcEfficiency.AVERAGE, + EpcWallDescriptions.cavity_insulated_assumed: EpcEfficiency.GOOD, + EpcWallDescriptions.cavity_filled_plus_internal: EpcEfficiency.VERY_GOOD, + EpcWallDescriptions.cavity_filled_plus_external: EpcEfficiency.VERY_GOOD, + # function mappings + EpcWallDescriptions.cavity_filled_cavity: cavity_filled_efficiency, + EpcWallDescriptions.cavity_internal_insulation: internal_external_insulation_efficiency, + EpcWallDescriptions.cavity_external_insulation: internal_external_insulation_efficiency, + + # Solid brick + # value mappings + EpcWallDescriptions.solid_brick_no_insulation_assumed: EpcEfficiency.POOR, + EpcWallDescriptions.solid_brick_partial_insulated_assumed: EpcEfficiency.AVERAGE, + EpcWallDescriptions.solid_brick_insulated_assumed: EpcEfficiency.GOOD, + # function mappings + EpcWallDescriptions.solid_brick_internal_insulation: internal_external_insulation_efficiency, + EpcWallDescriptions.solid_brick_external_insulation: internal_external_insulation_efficiency, + + # System + # value mappings + EpcWallDescriptions.system_no_insulation_assumed: EpcEfficiency.POOR, + EpcWallDescriptions.system_partial_insulated_assumed: EpcEfficiency.AVERAGE, + EpcWallDescriptions.system_insulated_assumed: EpcEfficiency.GOOD, + # function mappings + EpcWallDescriptions.system_internal_insulation: internal_external_insulation_efficiency, + EpcWallDescriptions.system_external_insulation: internal_external_insulation_efficiency, + + # Timber frame + # value mappings + EpcWallDescriptions.timber_frame_no_insulation_assumed: EpcEfficiency.POOR, + EpcWallDescriptions.timber_frame_partial_insulated_assumed: EpcEfficiency.AVERAGE, + EpcWallDescriptions.timber_frame_insulated_assumed: EpcEfficiency.GOOD, + # function mappings + EpcWallDescriptions.timber_frame_internal_insulation: timber_granite_sandstone_internal_external_efficiency, + EpcWallDescriptions.timber_frame_external_insulation: timber_granite_sandstone_internal_external_efficiency, + + # Granite / whinstone + EpcWallDescriptions.granite_whinstone_no_insulation_assumed: EpcEfficiency.VERY_POOR, + EpcWallDescriptions.granite_whinstone_partial_insulated_assumed: EpcEfficiency.AVERAGE, + EpcWallDescriptions.granite_whinestone_insulated_assumed: EpcEfficiency.GOOD, + # function mappings + EpcWallDescriptions.granite_whinstone_internal_insulation: timber_granite_sandstone_internal_external_efficiency, + EpcWallDescriptions.granite_whinstone_external_insulation: timber_granite_sandstone_internal_external_efficiency, + + # Sandstone / limestone + EpcWallDescriptions.sandstone_limestone_no_insulation_assumed: EpcEfficiency.VERY_POOR, + EpcWallDescriptions.sandstone_limestone_partial_insulated_assumed: EpcEfficiency.AVERAGE, + EpcWallDescriptions.sandstone_limestone_insulated_assumed: EpcEfficiency.GOOD, + # function mappings + EpcWallDescriptions.sandstone_limestone_internal_insulation: timber_granite_sandstone_internal_external_efficiency, + EpcWallDescriptions.sandstone_limestone_external_insulation: timber_granite_sandstone_internal_external_efficiency, + + # Cob (special case) + EpcWallDescriptions.cob_as_built_average: EpcEfficiency.AVERAGE, + EpcWallDescriptions.cob_as_built_good: EpcEfficiency.GOOD, +} + + +def resolve_wall_efficiency( + description: EpcWallDescriptions, + age_band: EpcConstructionAgeBand, +) -> EpcEfficiency: + rule = WALL_DESCRIPTION_METADATA[description] + + if isinstance(rule, EpcEfficiency): + return rule + + return rule(age_band) diff --git a/backend/onboarders/mappings/age_band.py b/backend/onboarders/mappings/age_band.py index 2487c921..5106a3fe 100644 --- a/backend/onboarders/mappings/age_band.py +++ b/backend/onboarders/mappings/age_band.py @@ -1,14 +1,19 @@ -party_map = { - "Before 1900": 'England and Wales: before 1900', - "1900-1929": 'England and Wales: 1900-1929', - "1930-1949": 'England and Wales: 1930-1949', - "1950-1966": 'England and Wales: 1950-1966', - "1967-1975": 'England and Wales: 1967-1975', - "1976-1982": 'England and Wales: 1976-1982', - "1983-1990": 'England and Wales: 1983-1990', - "1991-1995": 'England and Wales: 1991-1995', - "1996-2002": 'England and Wales: 1996-2002', - "2003-2006": 'England and Wales: 2003-2006', - "2007-2011": 'England and Wales: 2007-2011', - "2012 onwards": 'England and Wales: 2012-2021', +from backend.onboarders.epc_descriptions import EpcConstructionAgeBand + +parity_map = { + "Before 1900": EpcConstructionAgeBand.before_1900.value, + "1900-1929": EpcConstructionAgeBand.from_1900_to_1929.value, + "1930-1949": EpcConstructionAgeBand.from_1930_to_1949.value, + "1950-1966": EpcConstructionAgeBand.from_1950_to_1966.value, + "1967-1975": EpcConstructionAgeBand.from_1967_to_1975.value, + "1976-1982": EpcConstructionAgeBand.from_1976_to_1982.value, + "1983-1990": EpcConstructionAgeBand.from_1983_to_1990.value, + "1991-1995": EpcConstructionAgeBand.from_1991_to_1995.value, + "1996-2002": EpcConstructionAgeBand.from_1996_to_2002.value, + "2003-2006": EpcConstructionAgeBand.from_2003_to_2006.value, + "2007-2011": EpcConstructionAgeBand.from_2007_to_2011.value, + "2012 onwards": EpcConstructionAgeBand.from_2012_onwards.value, + # Newer age bands, under SAP10 + "2012-2022": EpcConstructionAgeBand.from_2012_to_2022.value, + "2023 onwards": EpcConstructionAgeBand.from_2023_onwards.value, } diff --git a/backend/onboarders/mappings/as_built_wall_classifiers.py b/backend/onboarders/mappings/as_built_wall_classifiers.py new file mode 100644 index 00000000..e0ef193f --- /dev/null +++ b/backend/onboarders/mappings/as_built_wall_classifiers.py @@ -0,0 +1,204 @@ +def map_cavity_wall_insulation(age_band): + if age_band in [ + 'England and Wales: before 1900', + 'England and Wales: 1900-1929', + 'England and Wales: 1930-1949', + 'England and Wales: 1950-1966', + 'England and Wales: 1967-1975' + ]: + return EpcWallDescriptions.cavity_no_insulation_assumed + + if age_band in [ + 'England and Wales: 1976-1982' + ]: + return EpcWallDescriptions.cavity_partial_insulated_assumed + + if age_band in [ + 'England and Wales: 1983-1990', + 'England and Wales: 1991-1995', + 'England and Wales: 1996-2002', + 'England and Wales: 2003-2006', + 'England and Wales: 2007-2011', + 'England and Wales: 2012-2022', + 'England and Wales: 2023 onwards', + ]: + return EpcWallDescriptions.cavity_insulated_assumed + + raise NotImplementedError(f"Age band {age_band} not handled for cavity wall as built insulation mapping") + + +def map_solid_wall_insulation(age_band): + if age_band in [ + 'England and Wales: before 1900', 'England and Wales: 1900-1929', 'England and Wales: 1930-1949', + 'England and Wales: 1967-1975' + ]: + return EpcWallDescriptions.solid_brick_no_insulation_assumed + + if age_band in [ + 'England and Wales: 1976-1982' + ]: + return EpcWallDescriptions.solid_brick_partial_insulated_assumed + + if age_band in [ + 'England and Wales: 1983-1990', 'England and Wales: 1991-1995', 'England and Wales: 1996-2002', + 'England and Wales: 2003-2006', 'England and Wales: 2007-2011', 'England and Wales: 2012-2022', + 'England and Wales: 2023 onwards', + ]: + return EpcWallDescriptions.solid_brick_insulated_assumed + + +def map_timber_frame_wall_insulation(age_band): + # No insulation (Poor) + if age_band in [ + 'England and Wales: before 1900', + 'England and Wales: 1900-1929', + 'England and Wales: 1930-1949', + ]: + return EpcWallDescriptions.timber_frame_no_insulation_assumed + + # Partial insulation (Average) + if age_band in [ + 'England and Wales: 1950-1966', + 'England and Wales: 1967-1975', + ]: + return EpcWallDescriptions.timber_frame_partial_insulated_assumed + + # Insulated (Good) + if age_band in [ + 'England and Wales: 1976-1982', + 'England and Wales: 1983-1990', + 'England and Wales: 1991-1995', + 'England and Wales: 1996-2002', + 'England and Wales: 2003-2006', + 'England and Wales: 2007-2011', + 'England and Wales: 2012-2022', + 'England and Wales: 2023 onwards', + ]: + return EpcWallDescriptions.timber_frame_insulated_assumed + + # TODO: Unknown / pre-1930 handling + raise NotImplementedError(f"Age band {age_band} not handled for timber frame wall insulation mapping") + + +def map_system_build_wall_insulation(age_band): + # No insulation (Poor) + if age_band in [ + 'England and Wales: before 1900', + 'England and Wales: 1900-1929', + 'England and Wales: 1930-1949', + 'England and Wales: 1950-1966', + 'England and Wales: 1967-1975', + ]: + return EpcWallDescriptions.system_no_insulation_assumed + + # Partial insulation (Average) + if age_band in [ + 'England and Wales: 1976-1982', + ]: + return EpcWallDescriptions.system_partial_insulated_assumed + + # Insulated (Good) + if age_band in [ + 'England and Wales: 1983-1990', + 'England and Wales: 1991-1995', + 'England and Wales: 1996-2002', + 'England and Wales: 2003-2006', + 'England and Wales: 2007-2011', + 'England and Wales: 2012-2022', + 'England and Wales: 2023 onwards', + ]: + return EpcWallDescriptions.system_insulated_assumed + + # TODO: Unknown / early system build handling + raise NotImplementedError(f"Age band {age_band} not handled for system build wall insulation mapping") + + +def map_granite_wall_insulation(age_band): + # No insulation (Very Poor) + if age_band in [ + 'England and Wales: before 1900', + 'England and Wales: 1900-1929', + 'England and Wales: 1930-1949', + 'England and Wales: 1950-1966', + 'England and Wales: 1967-1975', + ]: + return EpcWallDescriptions.granite_whinstone_no_insulation_assumed + + # Partial insulation (Average) + if age_band in [ + 'England and Wales: 1976-1982', + ]: + return EpcWallDescriptions.granite_whinstone_partial_insulated_assumed + + # Insulated (Good) + if age_band in [ + 'England and Wales: 1983-1990', + 'England and Wales: 1991-1995', + 'England and Wales: 1996-2002', + 'England and Wales: 2003-2006', + 'England and Wales: 2007-2011', + 'England and Wales: 2012-2022', + 'England and Wales: 2023 onwards', + ]: + return EpcWallDescriptions.granite_whinestone_insulated_assumed + + raise NotImplementedError(f"Age band {age_band} not handled for granite wall insulation mapping") + + +def map_sandstone_wall_insulation(age_band): + # No insulation (Very Poor) + if age_band in [ + 'England and Wales: before 1900', + 'England and Wales: 1900-1929', + 'England and Wales: 1930-1949', + 'England and Wales: 1950-1966', + 'England and Wales: 1967-1975', + ]: + return EpcWallDescriptions.sandstone_limestone_no_insulation_assumed + + # Partial insulation (Average) + if age_band in [ + 'England and Wales: 1976-1982', + ]: + return EpcWallDescriptions.sandstone_limestone_partial_insulated_assumed + + # Insulated (Good) + if age_band in [ + 'England and Wales: 1983-1990', + 'England and Wales: 1991-1995', + 'England and Wales: 1996-2002', + 'England and Wales: 2003-2006', + 'England and Wales: 2007-2011', + 'England and Wales: 2012-2022', + 'England and Wales: 2023 onwards', + ]: + return EpcWallDescriptions.sandstone_limestone_insulated_assumed + + raise NotImplementedError(f"Age band {age_band} not handled for sandstone wall insulation mapping") + + +def map_cob_wall_insulation(age_band): + # Cob, as built (Average) + if age_band in [ + 'England and Wales: before 1900', + 'England and Wales: 1900-1929', + 'England and Wales: 1930-1949', + 'England and Wales: 1950-1966', + 'England and Wales: 1967-1975', + 'England and Wales: 1976-1982', + ]: + return EpcWallDescriptions.cob_as_built_average + + # Cob, as built (Good) + if age_band in [ + 'England and Wales: 1983-1990', + 'England and Wales: 1991-1995', + 'England and Wales: 1996-2002', + 'England and Wales: 2003-2006', + 'England and Wales: 2007-2011', + 'England and Wales: 2012-2022', + 'England and Wales: 2023 onwards', + ]: + return EpcWallDescriptions.cob_as_built_good + + raise NotImplementedError(f"Age band {age_band} not handled for cob wall insulation mapping") diff --git a/backend/onboarders/parity.py b/backend/onboarders/parity.py index 27244777..ca84fbec 100644 --- a/backend/onboarders/parity.py +++ b/backend/onboarders/parity.py @@ -1,8 +1,9 @@ import pandas as pd from etl.epc.DataProcessor import construction_age_bounds_map from backend.onboarders.mappings.property_type import parity_map as property_map -from backend.onboarders.mappings.age_band import party_map as age_band_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 onboarders.epc_descriptions import EpcWallDescriptions, EpcConstructionAgeBand def check_nulls(data, original_column, mapped_column): @@ -17,7 +18,6 @@ def check_nulls(data, original_column, mapped_column): # Sample input data - data = pd.read_excel( "/Users/khalimconn-kowlessar/Documents/hestia/Customers/Peabody/Nov 2025 Consulting Project/2025_11_11 - Peabody " "- Data Extracts for Domna.xlsx", @@ -30,18 +30,6 @@ data = pd.read_excel( # 3) Indicate already installed measures # ------------ construction_age_band ------------ -# Map to EPC age bands -# def construction_date_to_band(year): -# if pd.isnull(year): -# return None -# # Get the year from the date which is numpy datetime format -# for label, ranges in construction_age_bounds_map.items(): -# if ranges["l"] <= year <= ranges["u"]: -# return label -# raise NotImplementedError("year out of bounds") -# -# -# data["construction_age_band"] = pd.to_datetime(data["Construction Date"]).dt.year.apply(construction_date_to_band) data["construction_age_band"] = data["Construction Years"].map(age_band_map) @@ -59,30 +47,186 @@ assert pd.isnull(data["built_form"]).sum() == 0, "Some built forms were not mapp # ------------ Wall Construction ------------ -data["walls_combined"] = data["Wall Construction"] + "+" + data["Wall Insulation"].fillna("Unknown Insulation") -data["Wall Insulation"].value_counts() -data["Wall Construction"].value_counts() +# Unique combindations +wall_mapping = { + # Cavity walls + ('Cavity', 'FilledCavity'): EpcWallDescriptions.cavity_filled_cavity.value, + ('Cavity', 'Internal'): EpcWallDescriptions.cavity_internal_insulation.value, + ('Cavity', 'External'): EpcWallDescriptions.cavity_external_insulation.value, + ('Cavity', 'FilledCavityPlusInternal'): EpcWallDescriptions.cavity_filled_plus_internal.value, + ('Cavity', 'FilledCavityPlusExternal'): EpcWallDescriptions.cavity_filled_plus_external.value, + ('Cavity', 'AsBuilt'): None, # To be classified + ('Cavity', 'Unknown'): None, # To be classified -as_built_map = { - "Cavity": {"insulated_age_bands": [], "partial_insulated_age_bands": []}, - "Solid Brick": {"insulated_age_bands": [], "partial_insulated_age_bands": []}, - "System": {"insulated_age_bands": [], "partial_insulated_age_bands": []}, - "Timber Frame": {"insulated_age_bands": [], "partial_insulated_age_bands": []}, - "Sandstone": {"insulated_age_bands": [], "partial_insulated_age_bands": []}, - "Granite": {"insulated_age_bands": [], "partial_insulated_age_bands": []}, - "Cob": {"insulated_age_bands": [], "partial_insulated_age_bands": []}, + # System built walls + ('System', 'External'): EpcWallDescriptions.system_external_insulation.value, + ('System', 'Internal'): EpcWallDescriptions.system_internal_insulation.value, + ('System', 'AsBuilt'): None, # To be classified + ('System', 'Unknown'): None, + + # Timber Frame walls + ('Timber Frame', 'Internal'): EpcWallDescriptions.timber_frame_internal_insulation.value, + ('Timber Frame', 'External'): EpcWallDescriptions.timber_frame_external_insulation.value, + ('Timber Frame', 'AsBuilt'): None, # To be classified + ('Timber Frame', 'Unknown'): None, + + # Solid Brick walls + ('Solid Brick', 'External'): EpcWallDescriptions.solid_brick_external_insulation.value, + ('Solid Brick', 'Internal'): EpcWallDescriptions.solid_brick_internal_insulation.value, + ('Solid Brick', 'AsBuilt'): None, # To be classified + ('Solid Brick', 'Unknown'): None, + + # Granite walls + ('Granite', 'External'): EpcWallDescriptions.granite_whinstone_external_insulation.value, + ("Granite", 'Internal'): EpcWallDescriptions.granite_whinstone_internal_insulation.value, + ('Granite', 'AsBuilt'): None, + ('Granite', 'Unknown'): None, + + # Sandstone walls + ('Sandstone', 'Internal'): EpcWallDescriptions.sandstone_limestone_internal_insulation.value, + ('Sandstone', 'External'): EpcWallDescriptions.sandstone_limestone_external_insulation.value, + ('Sandstone', 'Unknown'): None, + ('Sandstone', 'AsBuilt'): None, + + # Cob walls + ('Cob', 'AsBuilt'): None, } -def map_wall_construction(wall_constuction, wall_insulation, construction_age_band): - if wall_insulation == "AsBuilt": - # Deduce based on wall construction and age band - bands = as_built_map.get(wall_constuction, None) - if bands is None: - raise NotImplementedError(f"Wall construction {wall_constuction} not in as built map") +def map_cavity_wall_insulation(age_band: EpcConstructionAgeBand): + if age_band.start_year() < 1976: + return EpcWallDescriptions.cavity_no_insulation_assumed - # We check if the age band is in insulated or partial insulated, and if neither, we assume uninsulated + if age_band == EpcConstructionAgeBand.from_1976_to_1982: + return EpcWallDescriptions.cavity_partial_insulated_assumed + + if age_band in EpcConstructionAgeBand.from_year_onwards(1983): + return EpcWallDescriptions.cavity_insulated_assumed + + raise NotImplementedError(f"Age band {age_band} not handled for cavity wall as built insulation mapping") + + +def map_solid_wall_insulation(age_band: EpcConstructionAgeBand): + if age_band.start_year() < 1976: + return EpcWallDescriptions.solid_brick_no_insulation_assumed + + if age_band == EpcConstructionAgeBand.from_1976_to_1982: + return EpcWallDescriptions.solid_brick_partial_insulated_assumed + + if age_band in EpcConstructionAgeBand.from_year_onwards(1983): + return EpcWallDescriptions.solid_brick_insulated_assumed + + raise NotImplementedError( + f"Age band {age_band.value} not handled for solid wall insulation mapping" + ) + + +def map_timber_frame_wall_insulation(age_band: EpcConstructionAgeBand): + if age_band.start_year() < 1950: + return EpcWallDescriptions.timber_frame_no_insulation_assumed + + if age_band.start_year() < 1976: + return EpcWallDescriptions.timber_frame_partial_insulated_assumed + + if age_band in EpcConstructionAgeBand.from_year_onwards(1976): + return EpcWallDescriptions.timber_frame_insulated_assumed + + raise NotImplementedError( + f"Age band {age_band.value} not handled for timber frame wall insulation mapping" + ) + + +def map_system_build_wall_insulation(age_band: EpcConstructionAgeBand): + if age_band.start_year() < 1976: + return EpcWallDescriptions.system_no_insulation_assumed + + if age_band == EpcConstructionAgeBand.from_1976_to_1982: + return EpcWallDescriptions.system_partial_insulated_assumed + + if age_band in EpcConstructionAgeBand.from_year_onwards(1983): + return EpcWallDescriptions.system_insulated_assumed + + raise NotImplementedError( + f"Age band {age_band.value} not handled for system build wall insulation mapping" + ) + + +def map_granite_wall_insulation(age_band: EpcConstructionAgeBand): + if age_band.start_year() < 1976: + return EpcWallDescriptions.granite_whinstone_no_insulation_assumed + + if age_band == EpcConstructionAgeBand.from_1976_to_1982: + return EpcWallDescriptions.granite_whinstone_partial_insulated_assumed + + if age_band in EpcConstructionAgeBand.from_year_onwards(1983): + return EpcWallDescriptions.granite_whinestone_insulated_assumed + + raise NotImplementedError( + f"Age band {age_band.value} not handled for granite wall insulation mapping" + ) + + +def map_sandstone_wall_insulation(age_band: EpcConstructionAgeBand): + if age_band.start_year() < 1976: + return EpcWallDescriptions.sandstone_limestone_no_insulation_assumed + + if age_band == EpcConstructionAgeBand.from_1976_to_1982: + return EpcWallDescriptions.sandstone_limestone_partial_insulated_assumed + + if age_band in EpcConstructionAgeBand.from_year_onwards(1983): + return EpcWallDescriptions.sandstone_limestone_insulated_assumed + + raise NotImplementedError( + f"Age band {age_band.value} not handled for sandstone wall insulation mapping" + ) + + +def map_cob_wall_insulation(age_band: EpcConstructionAgeBand): + if age_band.start_year() < 1983: + return EpcWallDescriptions.cob_as_built_average + + if age_band in EpcConstructionAgeBand.from_year_onwards(1983): + return EpcWallDescriptions.cob_as_built_good + + raise NotImplementedError( + f"Age band {age_band.value} not handled for cob wall insulation mapping" + ) + + +AS_BUILT_WALL_CLASSIFIERS = { + "Cavity": map_cavity_wall_insulation, + "Solid Brick": map_solid_wall_insulation, + "Timber Frame": map_timber_frame_wall_insulation, + "System": map_system_build_wall_insulation, + "Granite": map_granite_wall_insulation, + "Sandstone": map_sandstone_wall_insulation, + "Cob": map_cob_wall_insulation, +} + +data["landlord_wall_description"] = ( + data[["Wall Construction", "Wall Insulation"]] + .apply(tuple, axis=1) + .map(wall_mapping) +) + + +def fill_as_built(row): + if row.landlord_wall_description is not None: + return row.landlord_wall_description + + classifier = AS_BUILT_WALL_CLASSIFIERS.get(row["Wall Construction"]) + if classifier is None: + return None + + return classifier(row.construction_age_band) + + +data["landlord_wall_description"] = data.apply(fill_as_built, axis=1) + +for _, x in data.iterrows(): + if x["construction_age_band"] == "England and Wales: 2012-2021": + de # Variables we want to map # 'Org Ref', 'Address 1', 'Address 2', 'Address 3', 'Postcode', 'Type',