diff --git a/.idea/Model.iml b/.idea/Model.iml
index c6561970..09f2e496 100644
--- a/.idea/Model.iml
+++ b/.idea/Model.iml
@@ -7,7 +7,7 @@
-
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
index 50cad4ca..fb10c6b0 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -3,7 +3,7 @@
-
+
diff --git a/asset_list/app.py b/asset_list/app.py
index 969b0184..63dc0601 100644
--- a/asset_list/app.py
+++ b/asset_list/app.py
@@ -59,6 +59,74 @@ def app():
Property UPRN
"""
+ # Fairhive
+ data_folder = "/Users/khalimconn-kowlessar/Documents/hestia/Customers/Fairhive"
+ data_filename = "Fairhive Asset list.xlsx"
+ sheet_name = "Sheet1"
+ postcode_column = 'POSTCODE'
+ address1_column = "ADDRESS"
+ address1_method = None
+ fulladdress_column = 'ADDRESS'
+ address_cols_to_concat = []
+ missing_postcodes_method = None
+ landlord_year_built = None
+ landlord_os_uprn = None
+ landlord_property_type = "PROPERTY TYPE"
+ landlord_built_form = None
+ landlord_wall_construction = None
+ landlord_roof_construction = None
+ landlord_heating_system = None
+ landlord_existing_pv = None
+ landlord_property_id = "Row ID"
+ landlord_sap = None
+ outcomes_filename = None
+ outcomes_sheetname = None
+ outcomes_postcode = None
+ outcomes_houseno = None
+ outcomes_id = None
+ outcomes_address = None
+ master_filepaths = []
+ master_id_colnames = []
+ master_to_asset_list_filepath = None
+ phase = False
+ ecosurv_landlords = None
+ asset_list_header = 0
+ landlord_block_reference = None
+
+ # Hyde
+ data_folder = "/Users/khalimconn-kowlessar/Documents/hestia/Customers/Hyde/Minor Works"
+ data_filename = "Hyde Group - Domna Minor Works Programme List.xlsx"
+ sheet_name = "Sheet1"
+ 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 = "Age"
+ landlord_os_uprn = None
+ landlord_property_type = "Property Type"
+ landlord_built_form = "Property Type"
+ landlord_wall_construction = "Walls"
+ landlord_roof_construction = "Roofs"
+ landlord_heating_system = "Heating"
+ landlord_existing_pv = "Renewables"
+ landlord_property_id = "Organisation Reference"
+ landlord_sap = "SAP (10)"
+ outcomes_filename = None
+ outcomes_sheetname = None
+ outcomes_postcode = None
+ outcomes_houseno = None
+ outcomes_id = None
+ outcomes_address = None
+ master_filepaths = []
+ master_id_colnames = []
+ master_to_asset_list_filepath = None
+ phase = False
+ ecosurv_landlords = None
+ asset_list_header = 0
+ landlord_block_reference = None
+
data_folder = "/Users/khalimconn-kowlessar/Documents/hestia/Customers/NCHA/20260129 SAL"
data_filename = "NCHA ASSET LIST 1.xlsx"
sheet_name = "NCHA ASSET LIST"
diff --git a/asset_list/mappings/built_form.py b/asset_list/mappings/built_form.py
index a9defdef..d6466539 100644
--- a/asset_list/mappings/built_form.py
+++ b/asset_list/mappings/built_form.py
@@ -520,4 +520,14 @@ BUILT_FORM_MAPPINGS = {
'2.EXT.WALL FLAT': 'mid-terrace',
'2 EXT. WALL FLAT': 'mid-terrace',
+ 'Maisonette: Detached: Ground Floor': 'detached',
+ 'Maisonette: Enclosed End Terrace: Top Floor': 'enclosed end-terrace',
+ 'Flat: End Terrace: Basement': 'end-terrace',
+ 'Flat: Mid Terrace: Basement': 'mid-terrace',
+ 'Flat: Enclosed Mid Terrace: Basement': 'enclosed mid-terrace',
+ 'House: Semi Detached: Top Floor': 'semi-detached',
+ 'House: End Terrace: Ground Floor': 'end-terrace',
+ 'Maisonette: Enclosed End Terrace: Mid Floor': 'enclosed end-terrace',
+ 'Bungalow: EnclosedEndTerrace': 'enclosed end-terrace'
+
}
diff --git a/asset_list/mappings/exising_pv.py b/asset_list/mappings/exising_pv.py
index e67fafb4..defce35f 100644
--- a/asset_list/mappings/exising_pv.py
+++ b/asset_list/mappings/exising_pv.py
@@ -17,5 +17,10 @@ EXISTING_PV_MAPPINGS = {
'PV: 10% roof area, PV: 2kWp array': 'already has PV',
'PV: 50% roof area': 'already has PV',
'Solar PV': 'already has PV',
- 'SOLAR PV': 'already has PV'
+ 'SOLAR PV': 'already has PV',
+
+ 'PV: 40% roof area, PV: 2kWp array': 'already has PV',
+ 'PV: 33% roof area, PV: 2kWp array': 'already has PV',
+ 'PV: 30% roof area': 'already has PV'
+
}
diff --git a/asset_list/mappings/heating_systems.py b/asset_list/mappings/heating_systems.py
index ffd1b198..272d6279 100644
--- a/asset_list/mappings/heating_systems.py
+++ b/asset_list/mappings/heating_systems.py
@@ -494,6 +494,10 @@ HEATING_MAPPINGS = {
'Gas (including LPG) room heaters: Gas fire, open flue, 1980 or later (open fronted), sitting proud of, '
'and sealed to, fireplace opening': 'room heaters',
'Boiler: A rated Regular Boiler, System 2: Boiler: C rated Regular Boiler': 'boiler - other fuel',
- 'Boiler: G rated Combi': 'gas condensing combi'
+ 'Boiler: G rated Combi': 'gas condensing combi',
+
+ 'Boiler: A rated Combi, System 2: Boiler: A rated Combi': 'gas combi boiler',
+ 'System 2: Boiler: A rated Regular Boiler, Boiler: A rated Regular Boiler': 'gas boiler, radiators',
+ 'Boiler: A rated Combi, System 2: Boiler: C rated Combi': 'gas combi boiler'
}
diff --git a/asset_list/mappings/property_type.py b/asset_list/mappings/property_type.py
index 703cb8ef..6f808c9a 100644
--- a/asset_list/mappings/property_type.py
+++ b/asset_list/mappings/property_type.py
@@ -429,6 +429,16 @@ PROPERTY_MAPPING = {
'Mid-terrace': 'unknown',
'MID - TERRACE': 'unknown',
'COMOFF': 'unknown',
- 'LOTS': 'unknown'
+ 'LOTS': 'unknown',
+
+ 'Maisonette: Detached: Ground Floor': 'maisonette',
+ 'Maisonette: Enclosed End Terrace: Top Floor': 'maisonette',
+ 'Flat: End Terrace: Basement': 'flat',
+ 'Bungalow: EnclosedEndTerrace': 'bungalow',
+ 'Flat: Mid Terrace: Basement': 'flat',
+ 'House: Semi Detached: Top Floor': 'house',
+ 'House: End Terrace: Ground Floor': 'house',
+ 'Maisonette: Enclosed End Terrace: Mid Floor': 'maisonette',
+ 'Flat: Enclosed Mid Terrace: Basement': 'flat'
}
diff --git a/asset_list/mappings/roof.py b/asset_list/mappings/roof.py
index 0857b046..cf829a5f 100644
--- a/asset_list/mappings/roof.py
+++ b/asset_list/mappings/roof.py
@@ -301,4 +301,13 @@ ROOF_CONSTRUCTION_MAPPINGS = {
'PitchedWithSlopingCeiling: As Built': 'pitched insulated',
'PitchedNormalLoftAccess: As Built': 'pitched unknown insulation',
+ 'Flat: 150mm, Flat: Unknown': 'flat insulated',
+ 'AnotherDwellingAbove: Unknown, Flat: Unknown': 'another dwelling above',
+ 'AnotherDwellingAbove, AnotherDwellingAbove: Unknown': 'another dwelling above',
+ 'PitchedNormalNoLoftAccess: Unknown, PitchedWithSlopingCeiling: As Built': 'pitched unknown access to loft',
+ 'Flat: No Insulation': 'flat uninsulated',
+ 'AnotherDwellingAbove: Unknown, PitchedNormalLoftAccess: 250mm': 'another dwelling above',
+ 'PitchedNormalLoftAccess: 175mm': 'pitched insulated',
+ 'AnotherDwellingAbove: 300mm': 'another dwelling above'
+
}
diff --git a/backend/onboarders/epc_descriptions.py b/backend/onboarders/epc_descriptions.py
index be704308..37f70df8 100644
--- a/backend/onboarders/epc_descriptions.py
+++ b/backend/onboarders/epc_descriptions.py
@@ -1,4 +1,5 @@
import re
+from collections.abc import Mapping
from enum import Enum
from typing import Callable, Union, List
@@ -105,6 +106,71 @@ class EpcWallDescriptions(Enum):
cob_as_built_unknown = "Cob, as built, unknown insulation"
+class EpcRoofDescriptions(Enum):
+ # Loft
+ # Known insulation at joists - we have 12, 25, 50, 75, 100, 125, 150, 175, 200, 225, 250, 270, 300, 350,
+ # 400+ as 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"
+
+ # TODO:
+ # Sloping ceiling
+ # Pitched, as built
+
+
class EpcEfficiency(Enum):
VERY_POOR = "Very Poor"
POOR = "Poor"
@@ -181,7 +247,12 @@ def timber_granite_sandstone_internal_external_efficiency(age_band: EpcConstruct
return EpcEfficiency.GOOD
-WALL_DESCRIPTION_METADATA = {
+WallEfficiencyRule = Union[
+ EpcEfficiency,
+ Callable[[EpcConstructionAgeBand, int | None], EpcEfficiency],
+]
+
+WALL_DESCRIPTION_EFFICIENCIES: Mapping[EpcWallDescriptions, WallEfficiencyRule] = {
# Note: all function mappings have been defined based on Elmhurst
# Cavity
# value mappings
@@ -248,9 +319,75 @@ def resolve_wall_efficiency(
description: EpcWallDescriptions,
age_band: EpcConstructionAgeBand,
) -> EpcEfficiency:
- rule = WALL_DESCRIPTION_METADATA[description]
+ rule = WALL_DESCRIPTION_EFFICIENCIES[description]
if isinstance(rule, EpcEfficiency):
return rule
return rule(age_band)
+
+
+RoofEfficiencyRule = Union[
+ EpcEfficiency,
+ Callable[[EpcConstructionAgeBand, int | None], EpcEfficiency],
+]
+
+
+def flat_limited_insulation_efficiency(age_band: EpcConstructionAgeBand) -> EpcEfficiency:
+ pass
+
+
+def flat_insulated_efficiency(age_band: EpcConstructionAgeBand) -> EpcEfficiency:
+ pass
+
+
+def flat_limited_efficiency(
+ age_band: EpcConstructionAgeBand,
+ insulation_thickness: int | None,
+) -> EpcEfficiency:
+ """
+ If we have an insulation thickness, 12mm results in a very poor rating. 25mm or above results in a poor rating.
+ If we don't have an insulation thickness, we fall back to age band, where
+ - 1976 - 1982 = Flat, limited insulation, poor efficiency
+ - 1967 - 1975 = Flat, limited insulation, Very Poor
+ :param age_band:
+ :param insulation_thickness:
+ :return:
+ """
+
+ if insulation_thickness is not None:
+ if insulation_thickness >= 25:
+ return EpcEfficiency.POOR
+ return EpcEfficiency.VERY_POOR
+
+ if age_band == EpcConstructionAgeBand.from_1976_to_1982:
+ return EpcEfficiency.POOR
+
+ if age_band == EpcConstructionAgeBand.from_1967_to_1975:
+ return EpcEfficiency.VERY_POOR
+
+ raise ValueError("Cannot determine flat limited insulation efficiency without insulation thickness or age band")
+
+
+ROOF_DESCRIPTION_EFFICIENCIES: Mapping[EpcRoofDescriptions, RoofEfficiencyRule] = {
+ # Flat roof
+ EpcRoofDescriptions.flat_no_insulation: EpcEfficiency.VERY_POOR,
+ EpcRoofDescriptions.flat_limited_insulation: flat_limited_insulation_efficiency,
+ EpcRoofDescriptions.flat_insulated: flat_insulated_efficiency,
+
+ # Loft:
+ EpcRoofDescriptions.loft_12mm_insulation: EpcEfficiency.VERY_POOR,
+ EpcRoofDescriptions.loft_25mm_insulation: EpcEfficiency.POOR,
+ EpcRoofDescriptions.loft_50mm_insulation: EpcEfficiency.POOR,
+ EpcRoofDescriptions.loft_75mm_insulation: EpcEfficiency.AVERAGE,
+ EpcRoofDescriptions.loft_100mm_insulation: EpcEfficiency.AVERAGE,
+ EpcRoofDescriptions.loft_125mm_insulation: EpcEfficiency.AVERAGE,
+ EpcRoofDescriptions.loft_150mm_insulation: EpcEfficiency.GOOD,
+ EpcRoofDescriptions.loft_175mm_insulation: EpcEfficiency.GOOD,
+ EpcRoofDescriptions.loft_200mm_insulation: EpcEfficiency.GOOD,
+ EpcRoofDescriptions.loft_250mm_insulation: EpcEfficiency.GOOD,
+ EpcRoofDescriptions.loft_270mm_insulation: EpcEfficiency.VERY_GOOD,
+ EpcRoofDescriptions.loft_300mm_insulation: EpcEfficiency.VERY_GOOD,
+ EpcRoofDescriptions.loft_350mm_insulation: EpcEfficiency.VERY_GOOD,
+ EpcRoofDescriptions.loft_400mm_plus_insulation: EpcEfficiency.VERY_GOOD,
+}
diff --git a/backend/onboarders/mappings/as_built_wall_classifiers.py b/backend/onboarders/mappings/as_built_wall_classifiers.py
index e0ef193f..e69de29b 100644
--- a/backend/onboarders/mappings/as_built_wall_classifiers.py
+++ b/backend/onboarders/mappings/as_built_wall_classifiers.py
@@ -1,204 +0,0 @@
-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 3e17ecce..a77e76a8 100644
--- a/backend/onboarders/parity.py
+++ b/backend/onboarders/parity.py
@@ -3,7 +3,9 @@ import pandas as pd
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
+from backend.onboarders.epc_descriptions import EpcWallDescriptions, EpcConstructionAgeBand, EpcEfficiency, \
+ WALL_DESCRIPTION_EFFICIENCIES
+from onboarders.epc_descriptions import EpcRoofDescriptions
tqdm.pandas()
@@ -49,7 +51,6 @@ assert pd.isnull(data["built_form"]).sum() == 0, "Some built forms were not mapp
# ------------ Wall Construction ------------
-
# Unique combindations
wall_mapping = {
# Cavity walls
@@ -241,16 +242,220 @@ def fill_as_built(row):
return classifier(row.construction_age_band)
+def resolve_wall_efficiency(
+ description: EpcWallDescriptions,
+ age_band: EpcConstructionAgeBand | None,
+) -> EpcEfficiency:
+ # Unknown / holding descriptions → efficiency unknown
+ if "unknown insulation" in description.value.lower():
+ return EpcEfficiency.NA
+
+ rule = WALL_DESCRIPTION_EFFICIENCIES.get(description)
+
+ if rule is None:
+ return EpcEfficiency.NA
+
+ if isinstance(rule, EpcEfficiency):
+ return rule
+
+ # Rule needs age band but we don't have one
+ if age_band is None or pd.isnull(age_band):
+ return EpcEfficiency.NA
+
+ return rule(age_band)
+
+
data["landlord_wall_description"] = data.progress_apply(fill_as_built, axis=1)
assert data["landlord_wall_description"].isnull().sum() == 0, (
"Some wall descriptions could not be resolved"
)
+data["landlord_wall_efficiency"] = data.progress_apply(
+ lambda row: resolve_wall_efficiency(
+ row.landlord_wall_description,
+ row.construction_age_band,
+ ),
+ axis=1,
+)
+# Sanity check
+assert data["landlord_wall_efficiency"].isnull().sum() == 0
+
+# ------------ Roof Construction ------------
+
+roof_aggs = data[["Roof Construction", "Roof Insulation"]].drop_duplicates().to_dict("records")
+
+[
+ # Dwelling above
+
+ # Pitched, loft
+
+ {'Roof Construction': 'PitchedNormalLoftAccess', 'Roof Insulation': nan},
+ {'Roof Construction': 'PitchedNormalLoftAccess', 'Roof Insulation': 'AsBuilt'},
+ {'Roof Construction': 'PitchedNormalLoftAccess', 'Roof Insulation': 'Unknown'},
+
+ # Flat
+ {'Roof Construction': 'Flat', 'Roof Insulation': 'AsBuilt'},
+ {'Roof Construction': 'Flat', 'Roof Insulation': 'mm100'},
+ {'Roof Construction': 'Flat', 'Roof Insulation': 'mm150'},
+ {'Roof Construction': 'Flat', 'Roof Insulation': nan},
+
+ {'Roof Construction': 'Flat', 'Roof Insulation': 'Unknown'},
+
+ # Thatched
+ {'Roof Construction': 'PitchedThatched', 'Roof Insulation': 'mm150'},
+ {'Roof Construction': 'PitchedThatched', 'Roof Insulation': 'Unknown'},
+ {'Roof Construction': 'PitchedThatched', 'Roof Insulation': 'mm50'},
+ {'Roof Construction': 'PitchedThatched', 'Roof Insulation': 'mm300'},
+
+ # Sloping
+ {'Roof Construction': 'PitchedWithSlopingCeiling', 'Roof Insulation': 'AsBuilt'},
+ {'Roof Construction': 'PitchedWithSlopingCeiling', 'Roof Insulation': 'mm150'},
+ {'Roof Construction': 'PitchedWithSlopingCeiling', 'Roof Insulation': 'mm100'},
+ {'Roof Construction': 'PitchedWithSlopingCeiling', 'Roof Insulation': nan},
+ {'Roof Construction': 'PitchedWithSlopingCeiling', 'Roof Insulation': 'mm50'},
+ {'Roof Construction': 'PitchedWithSlopingCeiling', 'Roof Insulation': 'NoInsulation'},
+ {'Roof Construction': 'PitchedWithSlopingCeiling', 'Roof Insulation': 'Unknown'},
+
+ # Pitched no loft access
+ {'Roof Construction': 'PitchedNormalNoLoftAccess', 'Roof Insulation': nan},
+ {'Roof Construction': 'PitchedNormalNoLoftAccess', 'Roof Insulation': 'Unknown'},
+ {'Roof Construction': 'PitchedNormalNoLoftAccess', 'Roof Insulation': 'AsBuilt'}
+]
+
+roof_mapping = {
+ # Dwelling above
+ ('AnotherDwellingAbove', 'Another Dwelling Above'): EpcRoofDescriptions.another_dwelling_above,
+ ('SameDwellingAbove', 'Same Dwelling Above'): EpcRoofDescriptions.another_dwelling_above,
+ # Pitched, normal loft access, with a loft thickness
+ ('PitchedNormalLoftAccess', 'mm25'): EpcRoofDescriptions.loft_25mm_insulation,
+ ('PitchedNormalLoftAccess', 'mm50'): EpcRoofDescriptions.loft_50mm_insulation,
+ ('PitchedNormalLoftAccess', 'mm75'): EpcRoofDescriptions.loft_75mm_insulation,
+ ('PitchedNormalLoftAccess', 'mm100'): EpcRoofDescriptions.loft_100mm_insulation,
+ ('PitchedNormalLoftAccess', 'mm150'): EpcRoofDescriptions.loft_150mm_insulation,
+ ('PitchedNormalLoftAccess', 'mm200'): EpcRoofDescriptions.loft_200mm_insulation,
+ ('PitchedNormalLoftAccess', 'mm250'): EpcRoofDescriptions.loft_250mm_insulation,
+ ('PitchedNormalLoftAccess', 'mm270'): EpcRoofDescriptions.loft_270mm_insulation,
+ ('PitchedNormalLoftAccess', 'mm300'): EpcRoofDescriptions.loft_300mm_insulation,
+ ('PitchedNormalLoftAccess', 'mm350'): EpcRoofDescriptions.loft_350mm_insulation,
+ ('PitchedNormalLoftAccess', 'mm400'): EpcRoofDescriptions.loft_400mm_plus_insulation,
+
+ # Pitched, no loft access, with a loft thickness
+ ('PitchedNormalNoLoftAccess', 'mm25'): EpcRoofDescriptions.loft_25mm_insulation,
+ ('PitchedNormalNoLoftAccess', 'mm50'): EpcRoofDescriptions.loft_50mm_insulation,
+ ('PitchedNormalNoLoftAccess', 'mm75'): EpcRoofDescriptions.loft_75mm_insulation,
+ ('PitchedNormalNoLoftAccess', 'mm100'): EpcRoofDescriptions.loft_100mm_insulation,
+ ('PitchedNormalNoLoftAccess', 'mm150'): EpcRoofDescriptions.loft_150mm_insulation,
+ ('PitchedNormalNoLoftAccess', 'mm200'): EpcRoofDescriptions.loft_200mm_insulation,
+ ('PitchedNormalNoLoftAccess', 'mm250'): EpcRoofDescriptions.loft_250mm_insulation,
+ ('PitchedNormalNoLoftAccess', 'mm270'): EpcRoofDescriptions.loft_270mm_insulation,
+ ('PitchedNormalNoLoftAccess', 'mm300'): EpcRoofDescriptions.loft_300mm_insulation,
+ ('PitchedNormalNoLoftAccess', 'mm350'): EpcRoofDescriptions.loft_350mm_insulation,
+ ('PitchedNormalNoLoftAccess', 'mm400'): EpcRoofDescriptions.loft_400mm_plus_insulation,
+
+ # Flat
+ ('Flat', 'NoInsulation'): EpcRoofDescriptions.flat_no_insulation,
+ # Flat - limited insulation
+ ('Flat', '12mm'): EpcRoofDescriptions.flat_limited_insulation,
+ ('Flat', 'mm25'): EpcRoofDescriptions.flat_limited_insulation,
+ ('Flat', 'mm50'): EpcRoofDescriptions.flat_limited_insulation,
+ # Flat insulated
+ ('Flat', 'mm75'): EpcRoofDescriptions.flat_insulated,
+ ('Flat', 'mm100'): EpcRoofDescriptions.flat_insulated,
+ ('Flat', 'mm150'): EpcRoofDescriptions.flat_insulated,
+ ('Flat', 'mm200'): EpcRoofDescriptions.flat_insulated,
+ ('Flat', 'mm250'): EpcRoofDescriptions.flat_insulated,
+ ('Flat', 'mm300'): EpcRoofDescriptions.flat_insulated,
+ ('Flat', 'mm350'): EpcRoofDescriptions.flat_insulated,
+ ('Flat', 'mm400'): EpcRoofDescriptions.flat_insulated,
+
+ # 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)
+
+ # {'Roof Construction': 'Flat', 'Roof Insulation': 'mm50'},
+
+}
+
+
+def classify_flat_roof(age_band: EpcConstructionAgeBand):
+ # # 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
+ raise NotImplementedError("Flat roof classification not implemented yet")
+
+
+def classify_pitched_loft_unknown(age_band: EpcConstructionAgeBand):
+ raise NotImplementedError("Pitched loft (unknown insulation) not implemented yet")
+
+
+def classify_thatched_roof(age_band: EpcConstructionAgeBand):
+ raise NotImplementedError("Thatched roof classification not implemented yet")
+
+
+def classify_sloping_ceiling_roof(age_band: EpcConstructionAgeBand):
+ raise NotImplementedError("Sloping ceiling roof classification not implemented yet")
+
+
+AS_BUILT_ROOF_CLASSIFIERS = {
+ "Flat": classify_flat_roof,
+ "PitchedNormalLoftAccess": classify_pitched_loft_unknown,
+ "PitchedNormalNoLoftAccess": classify_pitched_loft_unknown,
+ "PitchedThatched": classify_thatched_roof,
+ "PitchedWithSlopingCeiling": classify_sloping_ceiling_roof,
+}
+
+
+def fill_roof_as_built(row):
+ # Already resolved
+ if row.landlord_roof_description is not None:
+ return row.landlord_roof_description
+
+ roof_type = row["Roof Construction"]
+
+ classifier = AS_BUILT_ROOF_CLASSIFIERS.get(roof_type)
+ if classifier is None:
+ raise NotImplementedError(f"No roof classifier for roof type '{roof_type}'")
+
+ if pd.isnull(row.construction_age_band):
+ raise NotImplementedError(
+ f"Missing age band for roof classification ({roof_type})"
+ )
+
+ return classifier(row.construction_age_band)
+
+
+data["landlord_roof_description"] = (
+ data[["Roof Construction", "Roof Insulation"]]
+ .progress_apply(tuple, axis=1)
+ .map(roof_mapping)
+)
+
+data["landlord_roof_description"] = data.progress_apply(
+ fill_roof_as_built,
+ axis=1,
+)
+
+for _, row in data.iterrows():
+ fill_roof_as_built(row)
+
# Variables we want to map
# 'Org Ref', 'Address 1', 'Address 2', 'Address 3', 'Postcode', 'Type',
-# 'Attachment', 'Construction Years', 'Wall Construction',
-# 'Wall Insulation', 'Roof Construction', 'Roof Insulation',
+# 'Attachment', 'Construction Years',
+# 'Roof Construction', 'Roof Insulation',
# 'Floor Construction', 'Floor Insulation', 'Glazing', 'Heating',
# 'Boiler Efficiency', 'Main Fuel', 'Controls Adequacy', 'UPRN',
# 'Total Floor Area (m2)'
diff --git a/backend/onboarders/tests/test_roof_remapping.py b/backend/onboarders/tests/test_roof_remapping.py
new file mode 100644
index 00000000..e69de29b
diff --git a/backend/onboarders/tests/test_wall_remapping.py b/backend/onboarders/tests/test_wall_remapping.py
new file mode 100644
index 00000000..e69de29b