import pandas as pd from numpy import nan from typing import Union, Callable from collections.abc import Mapping from datatypes.epc.roof import EpcRoofDescriptions from datatypes.epc.efficiency import EpcEfficiency from datatypes.epc.construction_age_band import EpcConstructionAgeBand roof_map = { # 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, # All pitched options with asbuilt or unknown got to EpcRoofDescriptions.pitched_insulated_assumed # With access ('PitchedNormalLoftAccess', nan): EpcRoofDescriptions.pitched_insulated_assumed, ('PitchedNormalLoftAccess', 'AsBuilt'): EpcRoofDescriptions.pitched_insulated_assumed, ('PitchedNormalLoftAccess', 'Unknown'): EpcRoofDescriptions.pitched_insulated_assumed, # No access ('PitchedNormalNoLoftAccess', nan): EpcRoofDescriptions.pitched_insulated_assumed, ('PitchedNormalNoLoftAccess', 'AsBuilt'): EpcRoofDescriptions.pitched_insulated_assumed, ('PitchedNormalNoLoftAccess', 'Unknown'): EpcRoofDescriptions.pitched_insulated_assumed, # 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, # Flat - as built or unknown ('Flat', 'AsBuilt'): None, # To be classified ('Flat', nan): None, # To be classified ('Flat', 'Unknown'): None, # To be classified # 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) # Thatched ('PitchedThatched', 'mm50'): EpcRoofDescriptions.thatched_with_additional_insulation, ('PitchedThatched', 'mm150'): EpcRoofDescriptions.thatched_with_additional_insulation, ('PitchedThatched', 'mm300'): EpcRoofDescriptions.thatched_with_additional_insulation, ('PitchedThatched', 'Unknown'): EpcRoofDescriptions.thatched, # efficiency classified based on age # Sloping: # Limited (12 very poor, 25-50 poor) ('PitchedWithSlopingCeiling', 'mm12'): EpcRoofDescriptions.sloping_pitched_limited_insulation, ('PitchedWithSlopingCeiling', 'mm25'): EpcRoofDescriptions.sloping_pitched_limited_insulation, ('PitchedWithSlopingCeiling', 'mm50'): EpcRoofDescriptions.sloping_pitched_limited_insulation, # Insulated 75mm+ (75 - 125 average, 150 - 250 good, 270+ very good) ('PitchedWithSlopingCeiling', 'mm75'): EpcRoofDescriptions.sloping_pitched_insulated, ('PitchedWithSlopingCeiling', 'mm100'): EpcRoofDescriptions.sloping_pitched_insulated, ('PitchedWithSlopingCeiling', 'mm150'): EpcRoofDescriptions.sloping_pitched_insulated, ('PitchedWithSlopingCeiling', 'mm200'): EpcRoofDescriptions.sloping_pitched_insulated, ('PitchedWithSlopingCeiling', 'mm250'): EpcRoofDescriptions.sloping_pitched_insulated, ('PitchedWithSlopingCeiling', 'mm270'): EpcRoofDescriptions.sloping_pitched_insulated, ('PitchedWithSlopingCeiling', 'mm300'): EpcRoofDescriptions.sloping_pitched_insulated, ('PitchedWithSlopingCeiling', 'mm350'): EpcRoofDescriptions.sloping_pitched_insulated, ('PitchedWithSlopingCeiling', 'mm400'): EpcRoofDescriptions.sloping_pitched_insulated, # As built/unknown ('PitchedWithSlopingCeiling', 'AsBuilt'): None, # To be classified ('PitchedWithSlopingCeiling', nan): None, # To be classified ('PitchedWithSlopingCeiling', 'Unknown'): None, # } roof_unknown_age_fallback = { "Flat": EpcRoofDescriptions.flat_as_built_unknown, "PitchedWithSlopingCeiling": EpcRoofDescriptions.sloping_pitched_as_built_unknown, "PitchedThatched": EpcRoofDescriptions.thatched_as_built_unknown, "PitchedNormalLoftAccess": EpcRoofDescriptions.loft_as_built_unknown, "PitchedNormalNoLoftAccess": EpcRoofDescriptions.loft_as_built_unknown, } 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)