mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
717 lines
28 KiB
Python
717 lines
28 KiB
Python
import pandas as pd
|
|
import re
|
|
from collections.abc import Mapping
|
|
from enum import Enum
|
|
from typing import Callable, Union, List
|
|
|
|
|
|
class EpcConstructionAgeBand(Enum):
|
|
before_1900: str = 'England and Wales: before 1900'
|
|
from_1900_to_1929: str = 'England and Wales: 1900-1929'
|
|
from_1930_to_1949: str = 'England and Wales: 1930-1949'
|
|
from_1950_to_1966: str = 'England and Wales: 1950-1966'
|
|
from_1967_to_1975: str = 'England and Wales: 1967-1975'
|
|
from_1976_to_1982: str = 'England and Wales: 1976-1982'
|
|
from_1983_to_1990: str = 'England and Wales: 1983-1990'
|
|
from_1991_to_1995: str = 'England and Wales: 1991-1995'
|
|
from_1996_to_2002: str = 'England and Wales: 1996-2002'
|
|
from_2003_to_2006: str = 'England and Wales: 2003-2006'
|
|
from_2007_to_2011: str = 'England and Wales: 2007-2011'
|
|
from_2012_onwards: str = 'England and Wales: 2012-onwards'
|
|
from_2012_to_2022: str = 'England and Wales: 2012-2022'
|
|
from_2023_onwards: str = 'England and Wales: 2023 onwards'
|
|
|
|
def start_year(self) -> int:
|
|
"""
|
|
Extract the starting year of the age band.
|
|
"""
|
|
value = self.value.lower()
|
|
|
|
if 'before' in value:
|
|
return 0
|
|
match = re.search(r'(\d{4})', value)
|
|
if not match:
|
|
raise ValueError(f"Cannot determine start year from '{self.value}'")
|
|
|
|
return int(match.group(1))
|
|
|
|
@classmethod
|
|
def from_year_onwards(cls, year: int) -> List["EpcConstructionAgeBand"]:
|
|
"""
|
|
Return all age bands whose starting year is >= the given year.
|
|
"""
|
|
return [
|
|
band
|
|
for band in cls
|
|
if band.start_year() >= year
|
|
]
|
|
|
|
|
|
class EpcWallDescriptions(Enum):
|
|
# Cavity wall descriptions
|
|
cavity_insulated_assumed: str = "Cavity wall, as built, insulated (assumed)"
|
|
cavity_partial_insulated_assumed: str = "Cavity wall, as built, partial insulation (assumed)"
|
|
cavity_no_insulation_assumed: str = "Cavity wall, as built, no insulation (assumed)"
|
|
cavity_filled_cavity: str = "Cavity wall, filled cavity"
|
|
cavity_internal_insulation: str = "Cavity wall, with internal insulation"
|
|
cavity_external_insulation: str = "Cavity wall, with external insulation"
|
|
cavity_filled_plus_internal: str = "Cavity wall, filled cavity and internal insulation"
|
|
cavity_filled_plus_external: str = "Cavity wall, filled cavity and external insulation"
|
|
|
|
# Solid wall descriptions
|
|
solid_brick_internal_insulation: str = "Solid brick, with internal insulation"
|
|
solid_brick_external_insulation: str = "Solid brick, with external insulation"
|
|
solid_brick_no_insulation_assumed: str = 'Solid brick, as built, no insulation (assumed)'
|
|
solid_brick_partial_insulated_assumed: str = 'Solid brick, as built, partial insulation (assumed)'
|
|
solid_brick_insulated_assumed: str = 'Solid brick, as built, insulated (assumed)'
|
|
|
|
# System
|
|
system_external_insulation: str = "System built, with external insulation"
|
|
system_internal_insulation: str = "System built, with internal insulation"
|
|
system_no_insulation_assumed: str = "System built, as built, no insulation (assumed)"
|
|
system_partial_insulated_assumed: str = "System built, as built, partial insulation (assumed)"
|
|
system_insulated_assumed: str = "System built, as built, insulated (assumed)"
|
|
|
|
# Timber
|
|
timber_frame_internal_insulation: str = "Timber frame, with internal insulation"
|
|
timber_frame_external_insulation: str = "Timber frame, with external insulation"
|
|
timber_frame_no_insulation_assumed: str = "Timber frame, as built, no insulation (assumed)"
|
|
timber_frame_partial_insulated_assumed: str = "Timber frame, as built, partial insulation (assumed)"
|
|
timber_frame_insulated_assumed: str = "Timber frame, as built, insulated (assumed)"
|
|
|
|
# Granite/whinstone
|
|
granite_whinstone_external_insulation: str = "Granite or whin, with external insulation"
|
|
granite_whinstone_internal_insulation: str = "Granite or whin, with internal insulation"
|
|
granite_whinstone_no_insulation_assumed: str = "Granite or whin, as built, no insulation (assumed)"
|
|
granite_whinstone_partial_insulated_assumed: str = "Granite or whin, as built, partial insulation (assumed)"
|
|
granite_whinestone_insulated_assumed: str = "Granite or whin, as built, insulated (assumed)"
|
|
|
|
# Sandstone/limestone
|
|
sandstone_limestone_internal_insulation: str = "Sandstone, with internal insulation"
|
|
sandstone_limestone_external_insulation: str = "Sandstone, with external insulation"
|
|
sandstone_limestone_no_insulation_assumed: str = "Sandstone, as built, no insulation (assumed)"
|
|
sandstone_limestone_partial_insulated_assumed: str = "Sandstone, as built, partial insulation (assumed)"
|
|
sandstone_limestone_insulated_assumed: str = "Sandstone, as built, insulated (assumed)"
|
|
|
|
# Cob
|
|
cob_as_built_average = "Cob, as built"
|
|
cob_as_built_good = "Cob, as built"
|
|
|
|
# unknown descriptions which may get mapped later or handled via fallback
|
|
cavity_as_built_unknown = "Cavity wall, as built, unknown insulation"
|
|
solid_brick_as_built_unknown = "Solid brick, as built, unknown insulation"
|
|
system_as_built_unknown = "System built, as built, unknown insulation"
|
|
timber_frame_as_built_unknown = "Timber frame, as built, unknown insulation"
|
|
granite_as_built_unknown = "Granite or whin, as built, unknown insulation"
|
|
sandstone_as_built_unknown = "Sandstone, as built, unknown insulation"
|
|
cob_as_built_unknown = "Cob, as built, unknown insulation"
|
|
|
|
@property
|
|
def unknown_descriptions(self) -> List["EpcWallDescriptions"]:
|
|
return [
|
|
EpcWallDescriptions.cavity_as_built_unknown,
|
|
EpcWallDescriptions.solid_brick_as_built_unknown,
|
|
EpcWallDescriptions.system_as_built_unknown,
|
|
EpcWallDescriptions.timber_frame_as_built_unknown,
|
|
EpcWallDescriptions.granite_as_built_unknown,
|
|
EpcWallDescriptions.sandstone_as_built_unknown,
|
|
EpcWallDescriptions.cob_as_built_unknown,
|
|
]
|
|
|
|
|
|
class EpcRoofDescriptions(Enum):
|
|
# Loft
|
|
# Assumed options
|
|
pitched_insulated_assumed = "Pitched, insulated (assumed)"
|
|
pitched_no_insulation = "Pitched, no insulation"
|
|
# Insulation thickness options
|
|
loft_12mm_insulation: str = "Pitched, 12 mm loft insulation"
|
|
loft_25mm_insulation: str = "Pitched, 25 mm loft insulation"
|
|
loft_50mm_insulation: str = "Pitched, 50 mm loft insulation"
|
|
loft_75mm_insulation: str = "Pitched, 75 mm loft insulation"
|
|
loft_100mm_insulation: str = "Pitched, 100 mm loft insulation"
|
|
loft_125mm_insulation: str = "Pitched, 125 mm loft insulation"
|
|
loft_150mm_insulation: str = "Pitched, 150 mm loft insulation"
|
|
loft_175mm_insulation: str = "Pitched, 175 mm loft insulation"
|
|
loft_200mm_insulation: str = "Pitched, 200 mm loft insulation"
|
|
loft_250mm_insulation: str = "Pitched, 250 mm loft insulation"
|
|
loft_270mm_insulation: str = "Pitched, 270 mm loft insulation"
|
|
loft_300mm_insulation: str = "Pitched, 300 mm loft insulation"
|
|
loft_350mm_insulation: str = "Pitched, 350 mm loft insulation"
|
|
loft_400mm_plus_insulation: str = "Pitched, 400+ mm loft insulation"
|
|
# Insulated at rafters "Pitched, insulated at rafters"
|
|
# Rafters
|
|
# 400mm, 350mm = very good
|
|
# 200-300mm = good
|
|
# 125-175 = average
|
|
# 50-100 = poor
|
|
# 25 and below= very poor
|
|
loft_insulated_at_rafters: str = "Pitched, insulated at rafters"
|
|
# another dwelling above
|
|
another_dwelling_above: str = "(another dwelling above)"
|
|
# flat roof, which if there is observed insulation is just "flat, insulated", however there is a
|
|
# different efficiency rating depending on insulation thickness
|
|
# categories:
|
|
# 12mm = very poor & has limited insulation description
|
|
# 25, 50 = poor & has limited insulation description
|
|
# 75, 100, 125mm = average (Flat, insulated)
|
|
# 150, 175, 200, 225, 250mm = good (Flat, insulated)
|
|
# 270mm+ = very good (Flat, insulated)
|
|
# As built 2023 = Flat, insulated, Very good
|
|
# 2003 - 2006, up to 2012-2022 = Flat insulated, Good
|
|
# 1983-1990, 1996-2002 = Flat, insulated, Average
|
|
# 1976-1982 = Flat, limited insulation, poor
|
|
# 1967 - 1975 = Flat, limited insulation, Very Poor
|
|
# 1950-1966 and earlier bands = flat, no insulation, very poor
|
|
|
|
flat_insulated = "Flat, insulated"
|
|
flat_limited_insulation = "Flat, limited insulation"
|
|
flat_no_insulation = "Flat, no insulation"
|
|
|
|
# Thatched roof descriptions
|
|
# With Loft insulation at joists
|
|
# Thatched + 12mm = thatched, with additional insulation, average
|
|
# Thatched + 25, 50, 100, 150mm = thatched, with additional insulation, good
|
|
# Thatched + 175mm+ = thatched, with additional insulation, very good
|
|
# With loft insulation at rafters [out of scope atm]
|
|
# Unknown insulation
|
|
# Pre 1900, 1930-1949, 1967-1975, 1983-1990, 1996-2002 = "Thatched", Average
|
|
# 2003-2006, 2012-2022 = "Thatched", Good
|
|
# 2023 onwards = "Thatched", Very Good
|
|
thatched = "Thatched" # We see this for no insulation, has average performance
|
|
thatched_with_additional_insulation: str = "Thatched, with additional insulation"
|
|
|
|
# Sloping ceiling
|
|
# For sloping ceiling tags, we don't use any (assumed) tags so that it's unambiguous that the roof is sloped
|
|
sloping_pitched_no_insulation: str = "Pitched, no insulation"
|
|
sloping_pitched_limited_insulation: str = "Pitched, limited insulation"
|
|
sloping_pitched_insulated: str = "Pitched, insulated"
|
|
|
|
# Unknown descriptions which may get mapped later or handled via fallback
|
|
flat_as_built_unknown: str = "Flat, as built, unknown insulation"
|
|
loft_as_built_unknown: str = "Loft, as built, unknown insulation"
|
|
thatched_as_built_unknown: str = "Thatched, as built, unknown insulation"
|
|
sloping_pitched_as_built_unknown: str = "Pitched, as built, unknown insulation"
|
|
|
|
@property
|
|
def unknown_descriptions(self) -> List["EpcRoofDescriptions"]:
|
|
return [
|
|
EpcRoofDescriptions.flat_as_built_unknown,
|
|
EpcRoofDescriptions.loft_as_built_unknown,
|
|
EpcRoofDescriptions.thatched_as_built_unknown,
|
|
EpcRoofDescriptions.sloping_pitched_as_built_unknown,
|
|
]
|
|
|
|
|
|
class EpcEfficiency(Enum):
|
|
VERY_POOR = "Very Poor"
|
|
POOR = "Poor"
|
|
AVERAGE = "Average"
|
|
GOOD = "Good"
|
|
VERY_GOOD = "Very Good"
|
|
NA = "N/A"
|
|
|
|
|
|
def cavity_filled_efficiency(age_band: EpcConstructionAgeBand) -> EpcEfficiency:
|
|
""""
|
|
Maps cavity filled to efficiency based on construction age band.
|
|
:param age_band: EpcConstructionAgeBand
|
|
:return: EpcEfficiency
|
|
"""
|
|
if age_band in {
|
|
EpcConstructionAgeBand.from_2023_onwards
|
|
}:
|
|
return EpcEfficiency.VERY_GOOD
|
|
|
|
return EpcEfficiency.GOOD
|
|
|
|
|
|
def internal_external_insulation_efficiency(
|
|
age_band: EpcConstructionAgeBand,
|
|
) -> EpcEfficiency:
|
|
"""
|
|
Maps:
|
|
- cavity unfilled with internal/external insulation to efficiency based on construction age band. We assumed
|
|
based on 100mm insulation
|
|
- solid brick with internal/external insulation to efficiency based on construction age band. We assumed
|
|
based on 100mm insulation
|
|
- system built with internal/external insulation to efficiency based on construction age band. We assumed
|
|
based on 100mm insulation
|
|
|
|
All of these wall types have the same behaviour in elmhurst
|
|
:param age_band: EpcConstructionAgeBand
|
|
:return: EpcEfficiency
|
|
"""
|
|
if age_band in {
|
|
EpcConstructionAgeBand.from_1983_to_1990,
|
|
EpcConstructionAgeBand.from_1991_to_1995,
|
|
EpcConstructionAgeBand.from_1996_to_2002,
|
|
EpcConstructionAgeBand.from_2003_to_2006,
|
|
EpcConstructionAgeBand.from_2007_to_2011,
|
|
EpcConstructionAgeBand.from_2012_to_2022,
|
|
EpcConstructionAgeBand.from_2023_onwards,
|
|
}:
|
|
return EpcEfficiency.VERY_GOOD
|
|
|
|
return EpcEfficiency.GOOD
|
|
|
|
|
|
def timber_granite_sandstone_internal_external_efficiency(age_band: EpcConstructionAgeBand) -> EpcEfficiency:
|
|
""""
|
|
Maps:
|
|
- timber frame with internal/external wall insulation to efficiency based on construction age band.
|
|
- sandstone/limestone with internal/external wall insulation to efficiency based on construction age band.
|
|
- granite/whinstone with internal/external wall insulation to efficiency based on construction age band.
|
|
:param age_band: EpcConstructionAgeBand
|
|
:return: EpcEfficiency
|
|
"""
|
|
if age_band in {
|
|
EpcConstructionAgeBand.from_2023_onwards
|
|
}:
|
|
return EpcEfficiency.VERY_GOOD
|
|
|
|
return EpcEfficiency.GOOD
|
|
|
|
|
|
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
|
|
EpcWallDescriptions.cavity_no_insulation_assumed: EpcEfficiency.POOR,
|
|
EpcWallDescriptions.cavity_partial_insulated_assumed: EpcEfficiency.AVERAGE,
|
|
EpcWallDescriptions.cavity_insulated_assumed: EpcEfficiency.GOOD,
|
|
EpcWallDescriptions.cavity_filled_plus_internal: EpcEfficiency.VERY_GOOD,
|
|
EpcWallDescriptions.cavity_filled_plus_external: EpcEfficiency.VERY_GOOD,
|
|
# function mappings
|
|
EpcWallDescriptions.cavity_filled_cavity: cavity_filled_efficiency,
|
|
EpcWallDescriptions.cavity_internal_insulation: internal_external_insulation_efficiency,
|
|
EpcWallDescriptions.cavity_external_insulation: internal_external_insulation_efficiency,
|
|
|
|
# Solid brick
|
|
# value mappings
|
|
EpcWallDescriptions.solid_brick_no_insulation_assumed: EpcEfficiency.POOR,
|
|
EpcWallDescriptions.solid_brick_partial_insulated_assumed: EpcEfficiency.AVERAGE,
|
|
EpcWallDescriptions.solid_brick_insulated_assumed: EpcEfficiency.GOOD,
|
|
# function mappings
|
|
EpcWallDescriptions.solid_brick_internal_insulation: internal_external_insulation_efficiency,
|
|
EpcWallDescriptions.solid_brick_external_insulation: internal_external_insulation_efficiency,
|
|
|
|
# System
|
|
# value mappings
|
|
EpcWallDescriptions.system_no_insulation_assumed: EpcEfficiency.POOR,
|
|
EpcWallDescriptions.system_partial_insulated_assumed: EpcEfficiency.AVERAGE,
|
|
EpcWallDescriptions.system_insulated_assumed: EpcEfficiency.GOOD,
|
|
# function mappings
|
|
EpcWallDescriptions.system_internal_insulation: internal_external_insulation_efficiency,
|
|
EpcWallDescriptions.system_external_insulation: internal_external_insulation_efficiency,
|
|
|
|
# Timber frame
|
|
# value mappings
|
|
EpcWallDescriptions.timber_frame_no_insulation_assumed: EpcEfficiency.POOR,
|
|
EpcWallDescriptions.timber_frame_partial_insulated_assumed: EpcEfficiency.AVERAGE,
|
|
EpcWallDescriptions.timber_frame_insulated_assumed: EpcEfficiency.GOOD,
|
|
# function mappings
|
|
EpcWallDescriptions.timber_frame_internal_insulation: timber_granite_sandstone_internal_external_efficiency,
|
|
EpcWallDescriptions.timber_frame_external_insulation: timber_granite_sandstone_internal_external_efficiency,
|
|
|
|
# Granite / whinstone
|
|
EpcWallDescriptions.granite_whinstone_no_insulation_assumed: EpcEfficiency.VERY_POOR,
|
|
EpcWallDescriptions.granite_whinstone_partial_insulated_assumed: EpcEfficiency.AVERAGE,
|
|
EpcWallDescriptions.granite_whinestone_insulated_assumed: EpcEfficiency.GOOD,
|
|
# function mappings
|
|
EpcWallDescriptions.granite_whinstone_internal_insulation: timber_granite_sandstone_internal_external_efficiency,
|
|
EpcWallDescriptions.granite_whinstone_external_insulation: timber_granite_sandstone_internal_external_efficiency,
|
|
|
|
# Sandstone / limestone
|
|
EpcWallDescriptions.sandstone_limestone_no_insulation_assumed: EpcEfficiency.VERY_POOR,
|
|
EpcWallDescriptions.sandstone_limestone_partial_insulated_assumed: EpcEfficiency.AVERAGE,
|
|
EpcWallDescriptions.sandstone_limestone_insulated_assumed: EpcEfficiency.GOOD,
|
|
# function mappings
|
|
EpcWallDescriptions.sandstone_limestone_internal_insulation: timber_granite_sandstone_internal_external_efficiency,
|
|
EpcWallDescriptions.sandstone_limestone_external_insulation: timber_granite_sandstone_internal_external_efficiency,
|
|
|
|
# Cob (special case)
|
|
EpcWallDescriptions.cob_as_built_average: EpcEfficiency.AVERAGE,
|
|
EpcWallDescriptions.cob_as_built_good: EpcEfficiency.GOOD,
|
|
|
|
# Unknown mappings which are unhandled
|
|
EpcWallDescriptions.cavity_as_built_unknown: EpcEfficiency.NA,
|
|
EpcWallDescriptions.solid_brick_as_built_unknown: EpcEfficiency.NA,
|
|
EpcWallDescriptions.system_as_built_unknown: EpcEfficiency.NA,
|
|
EpcWallDescriptions.timber_frame_as_built_unknown: EpcEfficiency.NA,
|
|
EpcWallDescriptions.granite_as_built_unknown: EpcEfficiency.NA,
|
|
EpcWallDescriptions.sandstone_as_built_unknown: EpcEfficiency.NA,
|
|
EpcWallDescriptions.cob_as_built_unknown: EpcEfficiency.NA,
|
|
|
|
}
|
|
|
|
|
|
def resolve_wall_efficiency(
|
|
description: EpcWallDescriptions,
|
|
age_band: EpcConstructionAgeBand,
|
|
) -> EpcEfficiency:
|
|
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_insulated_efficiency_age_band(age_band: EpcConstructionAgeBand) -> EpcEfficiency:
|
|
"""
|
|
before 1900, 1900-1929, 1930-1949, 1950-1966, 1967-1975 -> Pitched, no insulation, Very Poor
|
|
1976-1982 -> Pitched, limited insulation, Poor
|
|
1983-1990, to 1996-2002 Pitched, insulated, Average
|
|
2003 - 2006, 2012-2022 -> Pitched, insulated, Good
|
|
2023 onwards -> Pitched, insulated, Very Good
|
|
:param age_band: EpcConstructionAgeBand
|
|
:return: EpcEfficiency
|
|
"""
|
|
|
|
start_year = age_band.start_year()
|
|
if start_year >= 2023:
|
|
return EpcEfficiency.VERY_GOOD
|
|
|
|
if start_year >= 2003:
|
|
return EpcEfficiency.GOOD
|
|
|
|
if start_year >= 1983:
|
|
return EpcEfficiency.AVERAGE
|
|
|
|
if start_year >= 1976:
|
|
return EpcEfficiency.POOR
|
|
|
|
return EpcEfficiency.VERY_POOR
|
|
|
|
|
|
def flat_insulated_efficiency_thickness(insulation_thickness: int | None) -> EpcEfficiency:
|
|
"""
|
|
12mm -> Very Poor
|
|
25mm - 50mm -> Poor
|
|
75mm - 125mm -> Pitched, insulated, average
|
|
150mm - 250mm -> good
|
|
270mm+ -> very good
|
|
:param insulation_thickness: Insulation thickness in mm
|
|
:return: EpcEfficiency
|
|
"""
|
|
|
|
if insulation_thickness is None:
|
|
raise ValueError("Insulation thickness is required for flat insulated efficiency calculation")
|
|
|
|
if insulation_thickness >= 270:
|
|
return EpcEfficiency.VERY_GOOD
|
|
|
|
if 150 <= insulation_thickness <= 250:
|
|
return EpcEfficiency.GOOD
|
|
|
|
if 75 <= insulation_thickness <= 125:
|
|
return EpcEfficiency.AVERAGE
|
|
|
|
if 25 <= insulation_thickness <= 50:
|
|
return EpcEfficiency.POOR
|
|
|
|
return EpcEfficiency.VERY_POOR
|
|
|
|
|
|
def flat_efficiency(insulation_thickness: int | None, age_band: EpcConstructionAgeBand) -> EpcEfficiency:
|
|
"""
|
|
Combines both age band and insulation thickness to determine flat roof efficiency.
|
|
:param insulation_thickness: Insulation thickness in mm
|
|
:param age_band: EpcConstructionAgeBand
|
|
:return: EpcEfficiency
|
|
"""
|
|
if insulation_thickness is not None:
|
|
return flat_insulated_efficiency_thickness(insulation_thickness)
|
|
|
|
return flat_insulated_efficiency_age_band(age_band)
|
|
|
|
|
|
def loft_insulated_efficiency(age_band: EpcConstructionAgeBand) -> EpcEfficiency:
|
|
"""
|
|
2023 onwards -> Very Good
|
|
2012-2022 -> Very Good
|
|
2007-2011 -> Very Good
|
|
2003-2006 -> Very Good
|
|
1996-2002 -> Good
|
|
1991-1995 -> Good
|
|
1983-1990 -> Average
|
|
1976-1982 -> Average
|
|
1967-1975 -> Average
|
|
1950-1966 -> Average
|
|
1930-1949 -> Average
|
|
1900-1929 -> Average
|
|
before 1900 -> Average
|
|
:param age_band: Input age band, EpcConstructionAgeBand
|
|
:return: EpcEfficiency
|
|
"""
|
|
year = age_band.start_year()
|
|
if year >= 2003:
|
|
return EpcEfficiency.VERY_GOOD
|
|
if year >= 1991:
|
|
return EpcEfficiency.GOOD
|
|
|
|
return EpcEfficiency.AVERAGE
|
|
|
|
|
|
def thatched_efficiency_age_band(age_band: EpcConstructionAgeBand) -> EpcEfficiency:
|
|
"""
|
|
Maps thatched roof efficiency based on construction age band.
|
|
:param age_band: EpcConstructionAgeBand
|
|
:return: EpcEfficiency
|
|
"""
|
|
year = age_band.start_year()
|
|
if year >= 2023:
|
|
return EpcEfficiency.VERY_GOOD
|
|
if year >= 2003:
|
|
return EpcEfficiency.GOOD
|
|
|
|
return EpcEfficiency.AVERAGE
|
|
|
|
|
|
def thatched_efficiency_thickness(insulation_thickness: int | None) -> EpcEfficiency:
|
|
"""
|
|
Maps thatched roof efficiency based on insulation thickness.
|
|
:param insulation_thickness: Insulation thickness in mm
|
|
:return: EpcEfficiency
|
|
"""
|
|
if insulation_thickness is None:
|
|
raise ValueError("Insulation thickness is required for thatched efficiency calculation")
|
|
|
|
if insulation_thickness >= 175:
|
|
return EpcEfficiency.VERY_GOOD
|
|
|
|
if insulation_thickness >= 25:
|
|
return EpcEfficiency.GOOD
|
|
|
|
return EpcEfficiency.AVERAGE
|
|
|
|
|
|
def thatched_efficiency(
|
|
insulation_thickness: int | None,
|
|
age_band: EpcConstructionAgeBand,
|
|
) -> EpcEfficiency:
|
|
"""
|
|
Combines both age band and insulation thickness to determine thatched roof efficiency.
|
|
:param insulation_thickness: Insulation thickness in mm
|
|
:param age_band: EpcConstructionAgeBand
|
|
:return: EpcEfficiency
|
|
"""
|
|
if insulation_thickness is not None:
|
|
return thatched_efficiency_thickness(insulation_thickness)
|
|
|
|
return thatched_efficiency_age_band(age_band)
|
|
|
|
|
|
def sloping_ceiling_efficiency_age_band(age_band: EpcConstructionAgeBand) -> EpcEfficiency:
|
|
"""
|
|
Maps sloping ceiling roof efficiency based on construction age band.
|
|
:param age_band: EpcConstructionAgeBand
|
|
:return: EpcEfficiency
|
|
"""
|
|
year = age_band.start_year()
|
|
if year >= 2023:
|
|
return EpcEfficiency.VERY_GOOD
|
|
if year >= 2003:
|
|
return EpcEfficiency.GOOD
|
|
if year >= 1983:
|
|
return EpcEfficiency.AVERAGE
|
|
if year >= 1976:
|
|
return EpcEfficiency.POOR
|
|
|
|
return EpcEfficiency.VERY_POOR
|
|
|
|
|
|
def sloping_ceiling_efficiency_thickness(insulation_thickness: int | None) -> EpcEfficiency:
|
|
"""
|
|
Maps sloping ceiling roof efficiency based on insulation thickness.
|
|
:param insulation_thickness: Insulation thickness in mm
|
|
:return: EpcEfficiency
|
|
"""
|
|
if insulation_thickness is None:
|
|
raise ValueError("Insulation thickness is required for sloping ceiling efficiency calculation")
|
|
|
|
if insulation_thickness >= 270:
|
|
return EpcEfficiency.VERY_GOOD
|
|
|
|
if insulation_thickness >= 150:
|
|
return EpcEfficiency.GOOD
|
|
|
|
if insulation_thickness >= 75:
|
|
return EpcEfficiency.AVERAGE
|
|
|
|
if insulation_thickness >= 25:
|
|
return EpcEfficiency.POOR
|
|
|
|
return EpcEfficiency.VERY_POOR
|
|
|
|
|
|
def sloping_ceiling_efficiency(
|
|
insulation_thickness: int | None,
|
|
age_band: EpcConstructionAgeBand,
|
|
) -> EpcEfficiency:
|
|
"""
|
|
Combines both age band and insulation thickness to determine sloping ceiling roof efficiency.
|
|
:param insulation_thickness: Insulation thickness in mm
|
|
:param age_band: EpcConstructionAgeBand
|
|
:return: EpcEfficiency
|
|
"""
|
|
if insulation_thickness is not None:
|
|
return sloping_ceiling_efficiency_thickness(insulation_thickness)
|
|
|
|
return sloping_ceiling_efficiency_age_band(age_band)
|
|
|
|
|
|
def loft_insulated_at_rafters_efficiency_thickness(insulation_thickness: int | None) -> EpcEfficiency:
|
|
"""
|
|
400mm, 350mm = very good
|
|
200-300mm = good
|
|
125-175 = average
|
|
50-100 = poor
|
|
25 and below= very poor
|
|
:return:
|
|
"""
|
|
if insulation_thickness is None:
|
|
raise ValueError("Insulation thickness is required for loft insulated at rafters efficiency calculation")
|
|
|
|
if insulation_thickness >= 350:
|
|
return EpcEfficiency.VERY_GOOD
|
|
|
|
if insulation_thickness >= 200:
|
|
return EpcEfficiency.GOOD
|
|
|
|
if insulation_thickness >= 125:
|
|
return EpcEfficiency.AVERAGE
|
|
|
|
if insulation_thickness >= 50:
|
|
return EpcEfficiency.POOR
|
|
|
|
return EpcEfficiency.VERY_POOR
|
|
|
|
|
|
def loft_insulated_at_rafters_efficiency_age_band(age_band: EpcConstructionAgeBand) -> EpcEfficiency:
|
|
"""
|
|
# 2023 onwards -> Very Good
|
|
# 2003-2006, 2012-2022 -> Good
|
|
# 1983 - 1990, 1996-2002 -> Average
|
|
# 1976-1982 -> Poor
|
|
# 1967-1975 and earlier bands -> Very Poor
|
|
:param age_band: EpcConstructionAgeBand
|
|
:return: EpcEfficiency
|
|
"""
|
|
year = age_band.start_year()
|
|
if year >= 2023:
|
|
return EpcEfficiency.VERY_GOOD
|
|
if year >= 2003:
|
|
return EpcEfficiency.GOOD
|
|
if year >= 1983:
|
|
return EpcEfficiency.AVERAGE
|
|
if year >= 1976:
|
|
return EpcEfficiency.POOR
|
|
|
|
return EpcEfficiency.VERY_POOR
|
|
|
|
|
|
def loft_insulated_at_rafters_efficiency(
|
|
insulation_thickness: int | None,
|
|
age_band: EpcConstructionAgeBand,
|
|
) -> EpcEfficiency:
|
|
"""
|
|
Combines both age band and insulation thickness to determine loft insulated at rafters roof efficiency.
|
|
:param insulation_thickness: Insulation thickness in mm
|
|
:param age_band: EpcConstructionAgeBand
|
|
:return: EpcEfficiency
|
|
"""
|
|
if insulation_thickness is not None:
|
|
return loft_insulated_at_rafters_efficiency_thickness(insulation_thickness)
|
|
|
|
return loft_insulated_at_rafters_efficiency_age_band(age_band)
|
|
|
|
|
|
ROOF_DESCRIPTION_EFFICIENCIES: Mapping[EpcRoofDescriptions, RoofEfficiencyRule] = {
|
|
# Flat roof
|
|
EpcRoofDescriptions.flat_no_insulation: EpcEfficiency.VERY_POOR,
|
|
EpcRoofDescriptions.flat_limited_insulation: flat_efficiency,
|
|
EpcRoofDescriptions.flat_insulated: flat_efficiency,
|
|
|
|
# Loft:
|
|
# value mappings
|
|
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,
|
|
EpcRoofDescriptions.pitched_no_insulation: EpcEfficiency.VERY_POOR,
|
|
# function mappings
|
|
EpcRoofDescriptions.pitched_insulated_assumed: loft_insulated_efficiency,
|
|
|
|
# Loft af rafters
|
|
EpcRoofDescriptions.loft_insulated_at_rafters: loft_insulated_at_rafters_efficiency,
|
|
|
|
# Another dwelling above
|
|
EpcRoofDescriptions.another_dwelling_above: EpcEfficiency.NA,
|
|
|
|
# Thatched
|
|
EpcRoofDescriptions.thatched: thatched_efficiency,
|
|
EpcRoofDescriptions.thatched_with_additional_insulation: thatched_efficiency,
|
|
|
|
# Sloping ceiling
|
|
EpcRoofDescriptions.sloping_pitched_insulated: sloping_ceiling_efficiency,
|
|
EpcRoofDescriptions.sloping_pitched_limited_insulation: sloping_ceiling_efficiency,
|
|
EpcRoofDescriptions.sloping_pitched_no_insulation: EpcEfficiency.VERY_POOR,
|
|
|
|
}
|
|
|
|
|
|
def resolve_roof_efficiency(
|
|
description: EpcRoofDescriptions,
|
|
age_band: EpcConstructionAgeBand | None,
|
|
insulation_thickness: int | None,
|
|
) -> EpcEfficiency:
|
|
"""
|
|
Resolve roof efficiency from description + age band + insulation thickness.
|
|
"""
|
|
|
|
# Unknown / holding descriptions → efficiency unknown
|
|
if description in description.unknown_descriptions:
|
|
return EpcEfficiency.NA
|
|
|
|
rule = ROOF_DESCRIPTION_EFFICIENCIES.get(description)
|
|
|
|
if rule is None:
|
|
return EpcEfficiency.NA
|
|
|
|
# Fixed efficiency
|
|
if isinstance(rule, EpcEfficiency):
|
|
return rule
|
|
|
|
# Callable rule
|
|
if age_band is None or pd.isnull(age_band):
|
|
return EpcEfficiency.NA
|
|
|
|
try:
|
|
# Try (thickness, age_band)
|
|
return rule(insulation_thickness, age_band)
|
|
except TypeError:
|
|
# Fallback to (age_band)
|
|
return rule(age_band)
|