implmenting innovation uplift

This commit is contained in:
Khalim Conn-Kowlessar 2025-08-08 18:41:02 +01:00
parent e2e2e8c71c
commit a55d2902ce
7 changed files with 391 additions and 8 deletions

View file

@ -949,9 +949,19 @@ class AssetList:
if self.phase:
# We filter on just the properties that have had an inspection
self.standardised_asset_list = self.standardised_asset_list[
~self.standardised_asset_list['Surveyors Name'].isin(["YET TO BE SURVEYED"])
]
if self.new_format_non_insturives_present_v2:
self.standardised_asset_list = self.standardised_asset_list[
~self.standardised_asset_list['NAME OF SURVEYOR'].isin(
["YET TO BE SURVEYED", "", None]
)
]
self.standardised_asset_list = self.standardised_asset_list[
~pd.isnull(self.standardised_asset_list["NAME OF SURVEYOR"])
]
else:
self.standardised_asset_list = self.standardised_asset_list[
~self.standardised_asset_list['Surveyors Name'].isin(["YET TO BE SURVEYED"])
]
if not self.variable_mappings and not override_empty_mappings:
raise ValueError("Please run init_standardise first")

View file

@ -58,6 +58,39 @@ def app():
EPC recommendations
Property UPRN
"""
# Freebridge
data_folder = "/Users/khalimconn-kowlessar/Documents/hestia/Customers/Freebridge"
data_filename = "Domna - FCH property data May 25 copy.xlsx"
sheet_name = "EPC Data"
postcode_column = 'Post Code'
address1_column = "Address 1"
address1_method = None
fulladdress_column = None
address_cols_to_concat = ["Address 1", "Address 4"]
missing_postcodes_method = None
landlord_year_built = "Build Date"
landlord_os_uprn = None
landlord_property_type = "Property Type"
landlord_built_form = None
landlord_wall_construction = "Walls Description"
landlord_heating_system = "Heating Type"
landlord_existing_pv = None
landlord_property_id = "Place Ref"
landlord_roof_construction = "Roof Description"
landlord_sap = "Current SAP"
outcomes_filename = []
outcomes_sheetname = []
outcomes_postcode = []
outcomes_houseno = []
outcomes_address = []
outcomes_id = []
master_filepaths = []
master_to_asset_list_filepath = None
asset_list_header = 0
landlord_block_reference = None
master_id_colnames = []
phase = True # Inspections not complete, produce a partial view
ecosurv_landlords = None
data_folder = "/Users/khalimconn-kowlessar/Documents/hestia/Customers/Broadlands"
data_filename = "Broadlands Asset List.xlsx"

View file

@ -431,6 +431,47 @@ HEATING_MAPPINGS = {
'Mains Electric': 'electric fuel',
'Unvented cylinder': 'other',
'MVHR & Heat Recovery': 'other',
'Solar': 'other'
'Solar': 'other',
'Electric storage heaters, Electric storage heaters': 'electric storage heaters',
'Room heaters, electric': 'room heaters',
'Room heaters, mains gas, Room heaters, electric': 'room heaters',
'Air source heat pump, underfloor, electric': 'air source heat pump',
'Air source heat pump, radiators, electric': 'air source heat pump',
'Air source heat pump, Systems with radiators, electric': 'air source heat pump',
'Electric storage heaters': 'electric storage heaters',
'Air source heat pump, Underfloor heating and radiators, pipes in screed above insulation, electric': 'air source '
'heat pump',
'Room heaters, coal': 'room heaters',
'Room heaters, electric, Electric storage heaters': 'electric storage heaters',
'Air source heat pump, fan coil units, electric': 'air source heat pump',
'Boiler and radiators, mains gas': 'gas boiler, radiators',
'Boiler and radiators, mains gas, Electric storage heaters': 'condensing boiler, radiators',
'Room heaters, mains gas': 'room heaters',
'Air source heat pump, radiators, electric, Air source heat pump, fan coil units, electric': 'air source heat pump',
'Air source heat pump, warm air, electric': 'air source heat pump',
'Electric ceiling heating, electric': 'electric ceiling',
'Electric storage heaters, Room heaters, electric': 'electric storage heaters',
'Room heaters, dual fuel (mineral and wood)': 'room heaters',
'Water source heat pump, radiators, electric': 'other',
'Warm air, electric': 'warm air heating',
'Boiler and radiators, wood logs': 'solid fuel',
'Boiler and radiators, dual fuel (mineral and wood)': 'solid fuel',
'Boiler & underfloor, mains gas': 'gas boiler, radiators',
'Boiler and underfloor heating, mains gas': 'gas boiler, radiators',
'Community scheme': 'communal heating',
'Warm air, Electricaire': 'warm air heating',
'Boiler and radiators, smokeless fuel': 'solid fuel',
'Warm air, mains gas': 'warm air heating',
'Warm air , electric': 'warm air heating',
'Boiler and radiators, LPG': 'boiler - other fuel',
'Boiler & underfloor, oil': 'oil boiler',
'Boiler and radiators, bottled LPG': 'boiler - other fuel',
'Boiler and underfloor heating, oil': 'oil boiler',
'SAP05:Main-Heating': 'unknown',
'Boiler and radiators, coal': 'solid fuel',
'Boiler and radiators, oil': 'oil boiler',
'Boiler and radiators, electric': 'electric boiler',
'No system present: electric heaters assumed': 'electric radiators',
'Boiler and radiators, anthracite': 'solid fuel'
}

View file

@ -10,8 +10,12 @@ STANDARD_ROOF_CONSTRUCTIONS = {
"another dwelling above",
"flat unknown insulation",
"flat insulated",
"flat uninsulated",
"unknown insulated",
"unknown",
"room roof insulated",
"room roof uninsulated",
"average thermal transmittance",
}
ROOF_CONSTRUCTION_MAPPINGS = {
@ -173,6 +177,73 @@ ROOF_CONSTRUCTION_MAPPINGS = {
'PitchedNormalNoLoftAccess: Unknown': 'pitched no access to loft',
'PitchedNormalLoftAccess: Unknown': 'pitched unknown insulation',
'AnotherDwellingAbove: Unknown': 'another dwelling above'
'AnotherDwellingAbove: Unknown': 'another dwelling above',
'Flat, insulated': 'flat insulated',
'Pitched, insulated (assumed)': 'pitched insulated',
'Flat, insulated (assumed)': 'flat insulated',
'(another dwelling above)': 'another dwelling above',
'Pitched, insulated at rafters': 'pitched insulated',
'(other premises above)': 'another dwelling above',
'Average thermal transmittance 0.15 W/m-¦K': 'average thermal transmittance',
'Pitched, 25 mm loft insulation': 'pitched less than 100mm insulation',
'Roof room(s), insulated (assumed)': 'room roof insulated',
'Pitched, limited insulation (assumed)': 'pitched less than 100mm insulation',
'Pitched, 270 mm loft insulation': 'pitched insulated',
'Pitched, 250 mm loft insulation': 'pitched insulated',
'Pitched, 200mm loft insulation': 'pitched insulated',
'Flat, no insulation': 'flat uninsulated',
'Pitched, 75 mm loft insulation': 'pitched less than 100mm insulation',
'Average thermal transmittance 0.09 W/m-¦K': 'average thermal transmittance',
'SAP05:Roof': 'unknown',
'Pitched, 400 mm loft insulation': 'pitched insulated',
'Pitched, 150mm loft insulation': 'pitched insulated',
'Average thermal transmittance 0.11 W/m-¦K': 'unknown',
'Pitched, 100 mm loft insulation': 'pitched less than 100mm insulation',
'Pitched, 300 mm loft insulation': 'pitched insulated',
'Pitched, 75mm loft insulation': 'pitched less than 100mm insulation',
'Pitched, 300+mm loft insulation': 'pitched insulated',
'Pitched, 300+ mm loft insulation': 'pitched insulated',
'Average thermal transmittance 0.11 W/m?K': 'average thermal transmittance',
'Average thermal transmittance 0.10 W/m?K': 'average thermal transmittance',
'Pitched, 250mm loft insulation': 'pitched insulated',
'Pitched, 300+ mm loft insulation': 'pitched insulated',
'Average thermal transmittance 0.1 W/m-¦K': 'average thermal transmittance',
'Pitched, *** INVALID INPUT Code : 57 *** loft insulation': 'unknown',
'Pitched, 100mm loft insulation': 'pitched less than 100mm insulation',
'Pitched, loft insulation': 'pitched less than 100mm insulation',
'Average thermal transmittance 0.20 W/m?K': 'average thermal transmittance',
'Average thermal transmittance 0.1 W/m?K': 'average thermal transmittance',
'Average thermal transmittance 0.16 W/m-¦K': 'average thermal transmittance',
'Average thermal transmittance 0.14 W/m?K': 'average thermal transmittance',
'Pitched, 50 mm loft insulation': 'pitched less than 100mm insulation',
'Flat, limited insulation': 'flat uninsulated',
'Average thermal transmittance 0.12 W/m?K': 'average thermal transmittance',
'Roof room(s), ceiling insulated': 'room roof insulated',
'Average thermal transmittance 0.18 W/m?K': 'average thermal transmittance',
'Average thermal transmittance 0.10 W/m-¦K': 'average thermal transmittance',
'Pitched, 400+ mm loft insulation': 'pitched insulated',
'Average thermal transmittance 0.14 W/m²K': 'average thermal transmittance',
'Pitched, no insulation (assumed)': 'pitched less than 100mm insulation',
'Average thermal transmittance 0.16 W/m?K': 'average thermal transmittance',
'Average thermal transmittance 0.21 W/m?K': 'average thermal transmittance',
'Flat, no insulation (assumed)': 'flat uninsulated',
'Pitched, no insulation': 'pitched less than 100mm insulation',
'Average thermal transmittance 0.12 W/m-¦K': 'average thermal transmittance',
'Pitched, 12 mm loft insulation': 'pitched less than 100mm insulation',
'Average thermal transmittance 0.07 W/m-¦K': 'average thermal transmittance',
'Roof room(s), no insulation (assumed)': 'room roof uninsulated',
'Pitched, no insulation(assumed)': 'pitched less than 100mm insulation',
'Average thermal transmittance 0.13 W/m-¦K': 'average thermal transmittance',
'Average thermal transmittance 0.08 W/m-¦K': 'average thermal transmittance',
'Average thermal transmittance 0.14 W/m-¦K': 'average thermal transmittance',
'Pitched, 350 mm loft insulation': 'pitched insulated',
'Average thermal transmittance 0 W/m-¦K': 'average thermal transmittance',
'Pitched, 200 mm loft insulation': 'pitched insulated',
'Pitched, 150 mm loft insulation': 'pitched insulated',
'Flat, limited insulation (assumed)': 'flat uninsulated',
}

View file

@ -334,4 +334,13 @@ WALL_CONSTRUCTION_MAPPINGS = {
'Cavity: FilledCavity, TimberFrame: AsBuilt': 'filled cavity',
'Cavity: FilledCavity, SolidBrick: AsBuilt, SolidBrick: Internal': 'filled cavity',
'Cavity: Internal, SolidBrick: AsBuilt': 'filled cavity',
'Timber frame, filled cavity': 'filled cavity',
'Cob, as built': 'cob',
'Cavity wall, filled cavity and internal insulation': 'filled cavity',
'SAP05:Walls': 'other',
'Solid brick, as built, partial insulation (assumed)': 'insulated solid brick',
'Sandstone, as built, no insulation (assumed)': 'uninsulated sandstone or limestone',
'System built, as built, partial insulation (assumed)': 'system built unknown insulation',
'Timber frame, with external insulation': 'insulated timber frame'
}

View file

@ -96,8 +96,9 @@ class Funding:
"""
measure_types = [m["type"] for m in measures]
innovation_flags = [m.get("is_innovation", False) for m in measures]
uplifts = [m["uplift"] for m in measures]
innovation_measures = [m["type"] for m in measures if m.get("is_innovation", False)]
return measure_types, innovation_flags, innovation_measures
return measure_types, uplifts, innovation_flags, innovation_measures
@staticmethod
def _meets_upgrade_target(starting_sap: int, ending_sap: int) -> bool:
@ -325,9 +326,104 @@ class Funding:
return starting_str, ending_uvalue
@staticmethod
def _map_to_pre_main_heating(mainheating, main_fuel, mainheat_energy_eff):
# We check most likely primary heating system. Because mixed systems are hard to break up, we
# check the larger, more prominent heating systems first and then the smaller ones. We aim
# to cover the case where properties have heating systems like
# "boiler radiators, mains gas, electric storage heaters" so mixed systems
if mainheating["has_air_source_heat_pump"]:
return 'Air to Water ASHP'
if mainheating["has_boiler"] and (main_fuel["fuel_type"] == "biomass"):
return 'Biomass Boiler'
if mainheating["has_boiler"] and (main_fuel["fuel_type"] == "lpg"):
return 'Bottled LPG Boiler'
if mainheating["has_boiler"] and (main_fuel["fuel_type"] == "mains gas") and (
mainheat_energy_eff in ["Good", "Very Good"]
):
# Assume higher efficiency condensing boiler
return 'Condensing Gas Boiler'
if mainheating["has_boiler"] and (main_fuel["fuel_type"] == "mains gas") and (
mainheat_energy_eff in ["Average", "Poor"]
):
return 'Non Condensing Gas Boiler'
if mainheating["has_boiler"] and (main_fuel["fuel_type"] == "mains gas") and (
mainheat_energy_eff in ["Very Poor"]
) and mainheating["has_radiators"]:
return 'Gas Back Boiler to Radiators'
if mainheating["has_boiler"] and (main_fuel["fuel_type"] == "mains gas") and (
mainheat_energy_eff in ["Very Poor"]
) and not mainheating["has_radiators"]:
# Doesnt have radiators
return 'Gas Fire with Back Boiler'
if mainheating["has_boiler"] and (main_fuel["fuel_type"] == "oil") and (
mainheat_energy_eff in ["Good", "Very Good"]
):
return 'Condensing Oil Boiler'
if mainheating["has_boiler"] and (main_fuel["fuel_type"] == "oil") and (
mainheat_energy_eff in ["Average", "Very Poor", "Poor"]
):
return 'Non Condensing Oil Boiler'
if mainheating["has_boiler"] and (main_fuel["fuel_type"] == "lpg") and (
mainheat_energy_eff in ["Good", "Very Good"]
):
return 'Condensing LPG Boiler'
if mainheating["has_boiler"] and (main_fuel["fuel_type"] == "lpg") and (
mainheat_energy_eff in ["Average", "Very Poor", "Poor"]
):
return 'Non Condensing LPG Boiler'
if mainheating["has_boiler"] and (
main_fuel["fuel_type"] in ["dual fuel appliance mineral and wood", "manufactured smokeless fuel"]
) and (mainheat_energy_eff in ["Average", "Very Poor", "Poor"]):
return 'Solid Fossil Boiler'
if mainheating["has_ground_source_heat_pump"]:
return 'GSHP'
if mainheating["has_boiler"] and (main_fuel["fuel_type"] == "electric"):
return 'Electric Boiler'
if mainheating["has_community_scheme"] and mainheat_energy_eff in ["Good", "Very Good"]:
return 'DHS CHP'
if mainheating["has_community_scheme"] and mainheat_energy_eff in ["Average", "Very Poor", "Poor"]:
return 'DHS non-CHP'
if mainheating["has_electric_storage_heaters"] and mainheat_energy_eff == "Very Poor":
return 'Electric Storage Heaters Responsiveness <=0.2'
if mainheating["has_electric_storage_heaters"] and mainheat_energy_eff in [
"Poor", "Average", "Good", "Very Good",
]:
return 'Electric Storage Heaters Responsiveness >0.2'
if mainheating["has_room_heaters"] and main_fuel["fuel_tye"] == "lpg":
return 'Bottled LPG Room Heaters'
if mainheating["has_room_heaters"] and main_fuel["fuel_tye"] == "electricity":
return 'Electric Room Heaters'
if mainheating["has_room_heaters"] and main_fuel["fuel_tye"] == "mains gas":
return 'Gas Room Heaters'
if mainheating["has_room_heaters"] and main_fuel["fuel_tye"] in [
"dual fuel appliance mineral and wood", "manufactured smokeless fuel"
]:
return 'Solid Fossil Room Heaters'
raise ValueError("Invalid pre heating system")
def calculate_partial_project_abs(
self,
measure_type: str,
mainheating: dict,
main_fuel: dict,
mainheat_energy_eff: str,
current_wall_uvalue: float = None,
is_partial: bool = False,
existing_li_thickness: float = None,
@ -411,6 +507,23 @@ class Funding:
raise ValueError("Invalid SFI category")
return pps.squeeze()["Cost Savings"]
if measure_type == "solar_pv":
pre_heating_system = self._map_to_pre_main_heating(mainheating, main_fuel, mainheat_energy_eff)
solar_pps_df = df[
(df["Measure_Type"] == "Solar_PV") & (df["Pre_Main_Heating_Source"] == pre_heating_system)
]
return solar_pps_df.squeeze()["Cost Savings"]
if measure_type == "air_source_heat_pump":
pre_heating_system = self._map_to_pre_main_heating(mainheating, main_fuel, mainheat_energy_eff)
pps = df[
(df["Pre_Main_Heating_Source"] == pre_heating_system) &
(df["Post_Main_Heating_Source"] == "Air to Water ASHP")
]
if pps.shape[0] != 1:
raise ValueError("something went wrong, more than one pps for ashp")
return pps.squeeze()["Cost Savings"]
raise ValueError(f"Invalid measure type for partial project ABS calculation: {measure_type}")
# -----------------------
@ -588,6 +701,9 @@ class Funding:
current_wall_uvalue: float,
is_partial: False,
existing_li_thickness: float,
mainheating: dict,
main_fuel: dict,
mainheat_energy_eff: str,
council_tax_band: str = None,
has_wall_insulation_recommendation: bool = False,
has_roof_insulation_recommendation: bool = False,
@ -602,7 +718,7 @@ class Funding:
"""
# Normalize measures
measure_types, innovation_flags, innovation_measures = self._split_measures(measures)
measure_types, uplifts, innovation_flags, innovation_measures = self._split_measures(measures)
# If we have a heating measure, we check if we meet the pre conditions
has_ftch = "first_time_central_heating" in measure_types
@ -666,6 +782,22 @@ class Funding:
if self.eco4_eligible:
# Calculate the full project ABS for ECO4
self.full_project_abs = self.calculate_full_project_abs()
# We calculate uplift innovation, where required
project_uplifts = []
for i, measure in enumerate(measure_types):
pps = self.calculate_partial_project_abs(
measure_type=measure,
mainheating=mainheating,
main_fuel=main_fuel,
mainheat_energy_eff=mainheat_energy_eff,
current_wall_uvalue=current_wall_uvalue,
is_partial=is_partial,
existing_li_thickness=existing_li_thickness,
)
project_uplifts.append(pps * uplifts[i])
total_uplift = sum(project_uplifts)
self.full_project_abs += total_uplift
self.eco4_funding = self.full_project_abs * (
self.social_cavity_abs_rate if is_cavity else self.social_solid_abs_rate
)
@ -673,7 +805,13 @@ class Funding:
if self.gbis_eligible:
# Calculate the partial project score - this is dependent on the measure
self.partial_project_abs = self.calculate_partial_project_abs(
measure_types[0], current_wall_uvalue, is_partial, existing_li_thickness,
measure_type=measure_types[0],
mainheating=mainheating,
main_fuel=main_fuel,
mainheat_energy_eff=mainheat_energy_eff,
current_wall_uvalue=current_wall_uvalue,
is_partial=is_partial,
existing_li_thickness=existing_li_thickness,
)

View file

@ -907,3 +907,84 @@ def test_custom_eco4_scenarios(
assert caveat in funding.eco4_eligibility_caveats, f"Missing caveat in: {scenario['description']}"
for caveat in funding.eco4_eligibility_caveats:
assert caveat in scenario.get("expected_caveats", []), f"Unexpected caveat in: {scenario['description']}"
### -------------------------
### Innovation uplift scenarios
### -------------------------
def test_uplift(
mock_project_scores_matrix,
mock_partial_scores_matrix,
mock_whlg_postcodes
):
funding = Funding(
project_scores_matrix=mock_project_scores_matrix,
partial_project_scores_matrix=mock_partial_scores_matrix,
whlg_eligible_postcodes=mock_whlg_postcodes,
social_cavity_abs_rate=13.5,
social_solid_abs_rate=17,
private_cavity_abs_rate=13.5,
private_solid_abs_rate=17,
tenure="Social"
)
# # TODO: Add a scenario with multiple measures, where some are innovation, some are not and we have
# TODO: Make sure private works too
measures = [
{"type": "solar_pv", "is_innovation": True, "uplift": 0.45},
{"type": "internal_wall_insulation", "is_innovation": False, "uplift": 0},
{"type": "loft_insulation", "is_innovation": False, "uplift": 0},
{"type": "air_source_heat_pump", "is_innovation": False, "uplift": 0},
{"type": "cavity_wall_insulation", "is_innovation": False, "uplift": 0.25},
]
mainheating = {
'original_description': 'Electric storage heaters', 'has_radiators': False,
'has_fan_coil_units': False,
'has_pipes_in_screed_above_insulation': False,
'has_pipes_in_insulated_timber_floor': False,
'has_pipes_in_concrete_slab': False, 'has_boiler': False,
'has_air_source_heat_pump': False,
'has_room_heaters': False, 'has_electric_storage_heaters': True, 'has_warm_air': False,
'has_electric_underfloor_heating': False, 'has_electric_ceiling_heating': False,
'has_community_scheme': False,
'has_ground_source_heat_pump': False, 'has_no_system_present': False,
'has_portable_electric_heaters': False,
'has_water_source_heat_pump': False, 'has_electric': True, 'has_mains_gas': False,
'has_wood_logs': False,
'has_coal': False, 'has_oil': False, 'has_wood_pellets': False, 'has_anthracite':
False,
'has_dual_fuel_mineral_and_wood': False, 'has_smokeless_fuel': False, 'has_lpg': False,
'has_assumed': False,
'has_electricaire': False, 'has_assumed_for_most_rooms': False,
'has_underfloor_heating': False,
"has_electric_heat_pumps": False,
"has_micro-cogeneration": False
}
main_fuel = {
'original_description': 'Electricity: electricity, unspecified tariff', 'fuel_type':
'electricity',
'tariff_type': 'unspecified tariff', 'is_community': False,
'no_individual_heating_or_community_network': False,
'complex_fuel_type': None
}
mainheat_energy_eff = "Good"
funding.check_funding(
measures=measures,
starting_sap=33,
ending_sap=69,
floor_area=71,
mainheat_description="Electic storage heaters",
heating_control_description="Manual charge control",
is_cavity=True,
current_wall_uvalue=2,
is_partial=False,
existing_li_thickness=0,
has_wall_insulation_recommendation=True,
has_roof_insulation_recommendation=True,
mainheating=mainheating,
main_fuel=main_fuel,
mainheat_energy_eff=mainheat_energy_eff,
)