mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
working on roof efficiency rules
This commit is contained in:
parent
002dc3695b
commit
63c6c32e22
13 changed files with 459 additions and 215 deletions
2
.idea/Model.iml
generated
2
.idea/Model.iml
generated
|
|
@ -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
2
.idea/misc.xml
generated
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
@ -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)'
|
||||
|
|
|
|||
0
backend/onboarders/tests/test_roof_remapping.py
Normal file
0
backend/onboarders/tests/test_roof_remapping.py
Normal file
0
backend/onboarders/tests/test_wall_remapping.py
Normal file
0
backend/onboarders/tests/test_wall_remapping.py
Normal file
Loading…
Add table
Reference in a new issue