From 918e5fd8cea5433b466839bb49f9ca857b5be388 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Mon, 2 Feb 2026 19:45:08 +0000 Subject: [PATCH] applying floor transformations --- backend/onboarders/epc_descriptions.py | 16 ++++ backend/onboarders/parity.py | 128 ++++++++++++++++++++++++- 2 files changed, 143 insertions(+), 1 deletion(-) diff --git a/backend/onboarders/epc_descriptions.py b/backend/onboarders/epc_descriptions.py index c6fe9de9..57b4ab89 100644 --- a/backend/onboarders/epc_descriptions.py +++ b/backend/onboarders/epc_descriptions.py @@ -715,3 +715,19 @@ def resolve_roof_efficiency( except TypeError: # Fallback to (age_band) return rule(age_band) + + +class EpcFloorDescriptions(Enum): + # Solid floor + solid_insulated = "Solid, insulated" + solid_insulated_assumed = "Solid, insulated (assumed)" + solid_no_insulation_assumed = "Solid, no insulation (assumed)" + solid_limited_insulation_assumed = "Solid, limited insulation (assumed)" + + # Suspended floor + suspended_insulated = "Suspended, insulated" + suspended_insulated_assumed = "Suspended, insulated (assumed)" + suspended_no_insulation_assumed = "Suspended, no insulation (assumed)" + suspended_limited_insulation_assumed = "Suspended, limited insulation (assumed)" + + unknown = None # We don't resolve anything diff --git a/backend/onboarders/parity.py b/backend/onboarders/parity.py index 69a64a89..67e65115 100644 --- a/backend/onboarders/parity.py +++ b/backend/onboarders/parity.py @@ -6,7 +6,7 @@ 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, EpcEfficiency, \ - WALL_DESCRIPTION_EFFICIENCIES, EpcRoofDescriptions, resolve_roof_efficiency + WALL_DESCRIPTION_EFFICIENCIES, EpcRoofDescriptions, resolve_roof_efficiency, EpcFloorDescriptions from backend.onboarders.mappings.as_built_wall_classifiers import AS_BUILT_WALL_CLASSIFIERS from backend.onboarders.mappings.as_built_roof_classifiers import AS_BUILT_ROOF_CLASSIFIERS @@ -352,6 +352,132 @@ data["has_sloping_ceiling"] = data["Roof Construction"].apply( lambda x: x == "PitchedWithSlopingCeiling" ) +# ------------ Floor Construction ------------ + + +floor_mapping = { + # Solid floor + ('Solid', 'AsBuilt'): None, # Mapped + ('Solid', 'Unknown'): None, # Mapped + ('Solid', nan): None, # Mapped + ('Solid', 'RetroFitted'): EpcFloorDescriptions.solid_insulated, + + # Suspended floor + ('SuspendedTimber', nan): None, # Mapped suspended_floor_as_built + ('SuspendedTimber', 'AsBuilt'): None, # Mapped suspended_floor_as_built + ('SuspendedTimber', 'RetroFitted'): EpcFloorDescriptions.suspended_insulated, + ('SuspendedTimber', 'Unknown'): None, # Mapped suspended_floor_as_built + ('SuspendedNotTimber', 'RetroFitted'): EpcFloorDescriptions.suspended_insulated, + ('SuspendedNotTimber', nan): None, # Mapped suspended_floor_as_built + ('SuspendedNotTimber', 'Unknown'): None, # Mapped suspended_floor_as_built + ('SuspendedNotTimber', 'AsBuilt'): None, # Mapped suspended_floor_as_built + + # Unknown type - mapped on age + ('Unknown', 'Unknown'): None, # Mapped unknown_floor_as_built + ('Unknown', 'RetroFitted'): None, # Mapped unknown_floor_retrofitted + (nan, nan): None, # No actual information! + ('Unknown', 'AsBuilt'): None, # Mapped unknown_floor_as_built +} + + +# Unknown floor, as built +# Before 1900, 1900 - 1929 -> Suspended, no insulation (assumed) +# 1930-1949, 1950 - 1966, 1967 - 1975, 1976-1982, 1983-1990, 1991-1995, -> Solid, no insulation (assumed) +# 1996 - 2002, Solid, limited insulation (assumed) +# 2003 onwards -> Solid, insulated (assumed) + +def unknown_floor_as_built(age_band: EpcConstructionAgeBand) -> EpcFloorDescriptions: + year = age_band.start_year() + + if year >= 2003: + return EpcFloorDescriptions.solid_insulated_assumed + + if year >= 1930: + return EpcFloorDescriptions.solid_no_insulation_assumed + + return EpcFloorDescriptions.suspended_no_insulation_assumed + + +# before 1900, 1900-1929 -> Suspended, insulated +# Thereafter, 1930 onwards -> Solid, insulated +def unknown_floor_retrofitted(age_band: EpcConstructionAgeBand) -> EpcFloorDescriptions: + year = age_band.start_year() + + if year >= 1930: + return EpcFloorDescriptions.solid_insulated + + return EpcFloorDescriptions.suspended_insulated + + +# 2003 - 2006, 2023 onwards -> Solid, insulated (assumed) +# 1996 - 2022 -> Solid, limited insulation (assumed) +# 1983 - 1990, 1991 - 1995 -> Solid, no insulation (assumed) +def solid_floor_as_built(age_band: EpcConstructionAgeBand) -> EpcFloorDescriptions: + year = age_band.start_year() + + if year >= 2003: + return EpcFloorDescriptions.solid_insulated_assumed + if year >= 1996: + return EpcFloorDescriptions.solid_limited_insulation_assumed + return EpcFloorDescriptions.solid_no_insulation_assumed + + +# 2003 -> 2006 -> Suspended, insulated (assumed) +# 1996 - 2022 -> Suspended, limited insulation (assumed) +# 1983 - 1995 -> Suspended, no insulation (assumed) +def suspended_floor_as_built(age_band: EpcConstructionAgeBand) -> EpcFloorDescriptions: + year = age_band.start_year() + + if year >= 2003: + return EpcFloorDescriptions.suspended_insulated_assumed + if year >= 1996: + return EpcFloorDescriptions.suspended_limited_insulation_assumed + + return EpcFloorDescriptions.suspended_no_insulation_assumed + + +data["landlord_floor_description"] = ( + data[["Floor Construction", "Floor Insulation"]] + .progress_apply(tuple, axis=1) + .map(floor_mapping) +) + + +def fill_floor_as_built(row): + # 1. Already resolved + if row.landlord_floor_description is not None: + return row.landlord_floor_description + + age_band = row.construction_age_band + floor_type = row["Floor Construction"] + insulation = row["Floor Insulation"] + + # 2. Missing age band → conservative fallback + if pd.isnull(age_band): + return EpcFloorDescriptions.unknown + + # 3. Known floor types + if floor_type == "Solid": + return solid_floor_as_built(age_band) + + if floor_type in {"SuspendedTimber", "SuspendedNotTimber"}: + return suspended_floor_as_built(age_band) + + # 4. Unknown floor type + if floor_type == "Unknown": + if insulation == "RetroFitted": + return unknown_floor_retrofitted(age_band) + return unknown_floor_as_built(age_band) + + # 5. Truly missing / garbage input + return EpcFloorDescriptions.unknown + + +data["landlord_floor_description"] = data.progress_apply( + fill_floor_as_built, + axis=1, +) + # Variables we want to map # 'Org Ref', 'Address 1', 'Address 2', 'Address 3', 'Postcode', # 'Floor Construction', 'Floor Insulation', 'Glazing', 'Heating',