working on roof efficiency rules

This commit is contained in:
Khalim Conn-Kowlessar 2026-02-02 12:25:54 +00:00
parent 002dc3695b
commit 63c6c32e22
13 changed files with 459 additions and 215 deletions

2
.idea/Model.iml generated
View file

@ -7,7 +7,7 @@
<sourceFolder url="file://$MODULE_DIR$/open_uprn" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/recommendations" isTestSource="false" />
</content>
<orderEntry type="jdk" jdkName="Fastapi-backend" jdkType="Python SDK" />
<orderEntry type="jdk" jdkName="AssetList" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

2
.idea/misc.xml generated
View file

@ -3,7 +3,7 @@
<component name="Black">
<option name="sdkName" value="Python 3.10 (backend)" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Fastapi-backend" project-jdk-type="Python SDK" />
<component name="ProjectRootManager" version="2" project-jdk-name="AssetList" project-jdk-type="Python SDK" />
<component name="PyCharmProfessionalAdvertiser">
<option name="shown" value="true" />
</component>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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