diff --git a/.idea/Model.iml b/.idea/Model.iml index b0f9c00d..4413bb06 100644 --- a/.idea/Model.iml +++ b/.idea/Model.iml @@ -7,7 +7,7 @@ - + diff --git a/.idea/misc.xml b/.idea/misc.xml index 1122b380..6f308057 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -3,7 +3,7 @@ - + diff --git a/backend/Property.py b/backend/Property.py index c04a3ed9..d400d439 100644 --- a/backend/Property.py +++ b/backend/Property.py @@ -10,7 +10,7 @@ from utils.logger import setup_logger from utils.s3 import read_dataframe_from_s3_parquet from epc_api.client import EpcClient from BaseUtility import Definitions -from recommendations.rdsap_tables import england_wales_age_band_lookup +from recommendations.rdsap_tables import england_wales_age_band_lookup, FLOOR_LEVEL_MAP from recommendations.recommendation_utils import ( estimate_perimeter, get_wall_type, estimate_external_wall_area, esimtate_pitched_roof_area ) @@ -84,6 +84,7 @@ class Property(Definitions): self.pitched_roof_area = None self.insulation_floor_area = None self.number_lighting_outlets = None + self.floor_level = None self.current_adjusted_energy = None self.expected_adjusted_energy = None @@ -324,6 +325,7 @@ class Property(Definitions): self.set_wall_type() self.set_floor_type() + self.set_floor_level() def set_age_band(self): """ @@ -369,7 +371,8 @@ class Property(Definitions): self.is_listed = spatial["is_listed_building"].values[0] self.is_heritage = spatial["is_heritage_building"].values[0] - if self.in_conservation_area is True | self.is_listed is True | self.is_heritage is True: + # We do an equals True, in the case of one of these variables being True + if (self.in_conservation_area == True) | (self.is_listed == True) | (self.is_heritage == True): self.restricted_measures = True spatial_dict = spatial.to_dict("records")[0] @@ -641,6 +644,38 @@ class Property(Definitions): floor_area=self.insulation_floor_area, floor_height=self.floor_height ) + def set_floor_level(self): + self.floor_level = ( + FLOOR_LEVEL_MAP[self.data["floor-level"]] if + self.data["floor-level"] not in self.DATA_ANOMALY_MATCHES else None + ) + + if self.floor_level is None: + + if self.data["property-type"] != "Flat": + return + + if self.floor["another_property_below"]: + self.floor_level = 1 + else: + self.floor_level = 0 + return + + # We perform some extra checks, if the property is not on the ground floor, as we have found cases + # where a property is marked as being on the first floor + if self.floor_level > 0: + + # We check if there is another property below + if not self.floor["another_property_below"]: + self.floor_level = 0 + return + + if self.floor_level == 0: + # Check if another property below + if self.floor["another_property_below"]: + self.floor_level = 1 + return + def set_wall_type(self): """ This method sets the wall type of the property, using a simple approach based on the wall description diff --git a/backend/app/db/models/materials.py b/backend/app/db/models/materials.py index 1a41f14f..f887fc25 100644 --- a/backend/app/db/models/materials.py +++ b/backend/app/db/models/materials.py @@ -35,7 +35,7 @@ class MaterialType(enum.Enum): low_energy_lighting_installation = "low_energy_lighting_installation" flat_roof_preparation = "flat_roof_preparation" flat_roof_vapour_barrier = "flat_roof_vapour_barrier" - flat_roof_waterpoofing = "flat_roof_waterpoofing" + flat_roof_waterproofing = "flat_roof_waterproofing" class DepthUnit(enum.Enum): diff --git a/backend/app/plan/router.py b/backend/app/plan/router.py index 42014bb3..b5acb3c0 100644 --- a/backend/app/plan/router.py +++ b/backend/app/plan/router.py @@ -298,6 +298,17 @@ async def trigger_plan(body: PlanTriggerRequest): t for t in missing_types if t not in ["internal_wall_insulation", "external_wall_insulation"] ] + # We check if NO wall insulation was selected but iwi and ewi are available + # This condition will check + # 1) iwi and ewi are both in missing_types + # 2) iwi and ewi are not in default_types + # If both of these are true, it means that no wall insulation was selected via the optimisation routine + # but both are possible, so we need to select a default. We default to iwi because it's usually cheaper + if (("internal_wall_insulation" in missing_types) and ("external_wall_insulation" in missing_types)) and ( + ("internal_wall_insulation" not in default_types) and ("external_wall_insulation" not in default_types) + ): + missing_types = [t for t in missing_types if t != "external_wall_insulation"] + if missing_types: for missed_type in missing_types: missed = [r for r in property_recommendations if r["type"] == missed_type] @@ -404,12 +415,10 @@ async def trigger_plan(body: PlanTriggerRequest): # We sum up the SAP points of the default recommendations and calculate a new EPC category. This # category is then used to produce adjusted energy figures - total_sap_points = sum([x["sap_points"] for x in representative_recs[property_id]]) - expected_epc = sap_to_epc(float(property_instance.data["current-energy-efficiency"]) + total_sap_points) expected_adjusted_energy = AnnualBillSavings.adjust_energy_to_metered( epc_energy_consumption=expected_heat_demand, - current_epc_rating=expected_epc, + current_epc_rating=property_instance.data["current-energy-rating"], ) heat_demand_change = ( @@ -531,8 +540,10 @@ async def trigger_plan(body: PlanTriggerRequest): new_sap_points = float(p.data["current-energy-efficiency"]) + total_sap_points new_epc = sap_to_epc(new_sap_points) + valuations = PropertyValuation.estimate(property_instance=p, target_epc=new_epc) + property_valuation_increases.append( - PropertyValuation.estimate(property_instance=p, target_epc=new_epc) + valuations["average_increased_value"] - valuations["current_value"] ) # Commit the session after each batch diff --git a/backend/ml_models/AnnualBillSavings.py b/backend/ml_models/AnnualBillSavings.py index 1519a866..f3e5074a 100644 --- a/backend/ml_models/AnnualBillSavings.py +++ b/backend/ml_models/AnnualBillSavings.py @@ -18,6 +18,8 @@ class AnnualBillSavings: # This is a weighted mean of the price caps, using the consumption figures above as weights PRICE_FACTOR = 0.11183098591549295 + EPC_BANDS = ["G", "F", "E", "D", "C", "B", "A"] + @classmethod def estimate(cls, kwh: float): """ @@ -70,3 +72,22 @@ class AnnualBillSavings: adjusted_consumption = (epc_energy_consumption + consumption_difference) return adjusted_consumption + + @classmethod + def adjust_expected_band(cls, expected_epc_rating, current_epc_rating): + """ + Because of the differing intercepts and intercepts when adjusting, it's possible for + expected_adjusted_energy to be bigger than current_adjusted_energy. In this case, we'll + adjust, against at most 1 EPC band above the curent. This function performs the EPC adjustment + :param expected_epc_rating: The expected EPC rating + :param current_epc_rating: The current EPC rating + """ + + # Find index of expected EPC rating + expected_index = cls.EPC_BANDS.index(expected_epc_rating) + current_index = cls.EPC_BANDS.index(current_epc_rating) + + if expected_index - 1 < current_index: + return current_epc_rating + + return cls.EPC_BANDS[expected_index - 1] diff --git a/backend/ml_models/Valuation.py b/backend/ml_models/Valuation.py index ad296409..9e409b9f 100644 --- a/backend/ml_models/Valuation.py +++ b/backend/ml_models/Valuation.py @@ -1,22 +1,121 @@ +import numpy as np + + class PropertyValuation: """ This is a placeholder class for the property valuation model """ UPRN_VALUE_LOOKUP = { - 15038202: {"current_value": 202000, "increase_percentage": 0.05725}, - 37024763: {"current_value": 213000, "increase_percentage": 0.025}, + 15038202: 202000, + 37024763: 213000, + 100070478545: 212000, + 100070297696: 662000, # Based on Zoopla's estimation of nearby house, 8 bloomfield road + 100070476394: 222000, # Based on Zoopla's estimation of next door, 20 Parkside + 100071264896: 128000, + # Based on next door neighbour: https://themovemarket.com/tools/propertyprices/flat-2-queens-wood-house-219 + # -brandwood-road-birmingham-b14-6pu + 100070533688: 218000, # Based on Zoopla's estimation of 95 Tenby Road, which is also end terrace + 100070505235: 344000, # Based on Zoopla's estimation of 131 School road, which is also semi-detached + 100070513306: 182000, # Based on Zoopla's estimation of 61 Simmons Drive + 100071306896: 77000, # Based on Flat 2 of 44 Wedgewood Road on Zoopla } + # We base our valuation uplifts on a number of sources + # https://www.moneysupermarket.com/gas-and-electricity/value-of-efficiency/ + MSM_MAPPING = [ + {"start": "G", "end": "F", "increase_percentage": 0.06}, + {"start": "F", "end": "E", "increase_percentage": 0.01}, + {"start": "E", "end": "D", "increase_percentage": 0.01}, + {"start": "D", "end": "C", "increase_percentage": 0.02}, + {"start": "C", "end": "B", "increase_percentage": 0.04}, + {"start": "B", "end": "A", "increase_percentage": 0.0}, + ] + + # https://www.lloydsbankinggroup.com/media/press-releases/2021/halifax/homebuyers-pay-a-green-premium-of-40000 + # -for-the-most-energy-efficient-properties.html + LLOYDS_MAPPING = [ + {"start": "G", "end": "F", "increase_percentage": 0.038}, + {"start": "F", "end": "E", "increase_percentage": 0.029}, + {"start": "E", "end": "D", "increase_percentage": 0.024}, + {"start": "D", "end": "C", "increase_percentage": 0.02}, + {"start": "C", "end": "B", "increase_percentage": 0.02}, + {"start": "B", "end": "A", "increase_percentage": 0.018}, + ] + + KNIGHT_FRANK_MAPPING = [ + {"start": "D", "end": "C", "increase_percentage": 0.03}, + {"start": "D", "end": "B", "increase_percentage": 0.088}, + ] + + NATIONWIDE_MAPPING = [ + {"start": "G", "end": "D", "increase_percentage": 0.035}, + {"start": "F", "end": "D", "increase_percentage": 0.035}, + {"start": "D", "end": "B", "increase_percentage": 0.017}, + {"start": "D", "end": "A", "increase_percentage": 0.017}, + ] + + EPC_BANDS = ["G", "F", "E", "D", "C", "B", "A"] + + @classmethod + def get_increase(cls, epc_band_range): + + increases = [] + for i in range(len(epc_band_range)): + + if i == len(epc_band_range) - 1: + break + + current = epc_band_range[i] + next = epc_band_range[i + 1] + + msm_increase = [x for x in cls.MSM_MAPPING if x["start"] == current and x["end"] == next][0] + lloyds_increase = [x for x in cls.LLOYDS_MAPPING if x["start"] == current and x["end"] == next][0] + + increases.append( + { + "start": current, + "end": next, + "msm_increase": msm_increase["increase_percentage"], + "lloyds_increase": lloyds_increase["increase_percentage"], + } + ) + + # We now aggregate the increases. The should be compound increases so we multiply them together + msm_increase = np.prod([1 + x["msm_increase"] for x in increases]) - 1 + lloyds_increase = np.prod([1 + x["lloyds_increase"] for x in increases]) - 1 + + return msm_increase, lloyds_increase + @classmethod def estimate(cls, property_instance, target_epc): - data = cls.UPRN_VALUE_LOOKUP.get(property_instance.uprn) + value = cls.UPRN_VALUE_LOOKUP.get(property_instance.uprn) - if not data: + if not value: raise ValueError("Have not implemented valuation for this property") - new_valuation = (1 + data["increase_percentage"]) * data["current_value"] + current_epc = property_instance.data["current-energy-rating"] + # We get the spectrum of ratings between the current and target EPC + epc_band_range = cls.EPC_BANDS[cls.EPC_BANDS.index(current_epc): cls.EPC_BANDS.index(target_epc) + 1] - increase = round(new_valuation - data["current_value"], 2) + msm_increase, lloyds_increase = cls.get_increase(epc_band_range) - return increase + # We now use the knight frank and nationwide data to get further valuation evidence, if we have it + kf_increase = [x for x in cls.KNIGHT_FRANK_MAPPING if x["start"] == current_epc and x["end"] == target_epc] + nw_increase = [x for x in cls.NATIONWIDE_MAPPING if x["start"] == current_epc and x["end"] == target_epc] + + kf_increase = kf_increase[0]["increase_percentage"] if kf_increase else None + nw_increase = nw_increase[0]["increase_percentage"] if nw_increase else None + + all_increases = [x for x in [msm_increase, lloyds_increase, kf_increase, nw_increase] if x is not None] + + max_increase = max(all_increases) + min_increase = min(all_increases) + avg_increase = np.mean(all_increases) + + return { + "current_value": value, + "lower_bound_increased_value": value * (1 + min_increase), + "upper_bound_increased_value": value * (1 + max_increase), + "average_increased_value": value * (1 + avg_increase), + } diff --git a/backend/tests/test_property.py b/backend/tests/test_property.py index d8519b6b..871c9291 100644 --- a/backend/tests/test_property.py +++ b/backend/tests/test_property.py @@ -1,3 +1,4 @@ +import pandas as pd import pytest from unittest.mock import Mock from epc_api.client import EpcClient @@ -345,3 +346,95 @@ class TestProperty: # Verify that ValueError is raised when multiple attributes are found with pytest.raises(ValueError, match="Either No attributes or multiple found for roof-description"): property_instance.get_components(cleaned) + + def test_set_spatial(self, mock_epc_client): + prop = Property(1, "AB12CD", "Test Address", mock_epc_client) + + spatial1 = pd.DataFrame([{ + 'X_COORDINATE': 411143.0, 'Y_COORDINATE': 281701.0, 'LATITUDE': 52.4331896, 'LONGITUDE': -1.8375238, + 'conservation_status': True, 'is_listed_building': False, 'is_heritage_building': True + }]) + + prop.set_spatial(spatial1) + + assert prop.in_conservation_area + assert not prop.is_listed + assert prop.is_heritage + assert prop.restricted_measures + + prop2 = Property(1, "AB12CD", "Test Address", mock_epc_client) + + spatial2 = pd.DataFrame([{ + 'X_COORDINATE': 411143.0, 'Y_COORDINATE': 281701.0, 'LATITUDE': 52.4331896, 'LONGITUDE': -1.8375238, + 'conservation_status': None, 'is_listed_building': False, 'is_heritage_building': False + }]) + + prop2.set_spatial(spatial2) + + assert prop2.in_conservation_area is None + assert not prop2.is_listed + assert not prop2.is_heritage + assert not prop2.restricted_measures + + def test_set_floor_level(self, mock_epc_client): + # In this case, we have a flat which looks looks it's on the first floor, but it's actually on the ground + # floor, so we should set floor_level to 0 + prop = Property(1, "AB12CD", "Test Address", mock_epc_client) + prop.data = {'floor-level': '01', 'property-type': 'Flat'} + prop.floor = { + 'original_description': 'Solid, no insulation (assumed)', 'clean_description': 'Solid, no insulation', + 'thermal_transmittance': None, 'thermal_transmittance_unit': None, 'is_assumed': True, + 'is_to_unheated_space': False, 'is_to_external_air': False, 'is_suspended': False, 'is_solid': True, + 'another_property_below': False, 'insulation_thickness': 'none', 'floor_thermal_transmittance': None, + 'floor_insulation_thickness': 'none' + } + + prop.set_floor_level() + + assert prop.floor_level == 0 + + # This property is labelled as being on the ground floor but actually has another property below + # so we set floor level to 1 + prop2 = Property(1, "AB12CD", "Test Address", mock_epc_client) + prop2.data = {'floor-level': 'Ground', 'property-type': 'Flat'} + prop2.floor = { + 'original_description': '(Another dwelling below)', 'clean_description': 'Solid, no insulation', + 'thermal_transmittance': None, 'thermal_transmittance_unit': None, 'is_assumed': False, + 'is_to_unheated_space': False, 'is_to_external_air': False, 'is_suspended': False, 'is_solid': False, + 'another_property_below': True, 'insulation_thickness': 'none', 'floor_thermal_transmittance': None, + 'floor_insulation_thickness': 'none' + } + + prop2.set_floor_level() + + assert prop2.floor_level == 1 + + # this property is correctly labelled as being on the 2nd floor + prop3 = Property(1, "AB12CD", "Test Address", mock_epc_client) + prop3.data = {'floor-level': '02', 'property-type': 'Flat'} + prop3.floor = { + 'original_description': '(Another dwelling below)', 'clean_description': 'Solid, no insulation', + 'thermal_transmittance': None, 'thermal_transmittance_unit': None, 'is_assumed': False, + 'is_to_unheated_space': False, 'is_to_external_air': False, 'is_suspended': False, 'is_solid': False, + 'another_property_below': True, 'insulation_thickness': 'none', 'floor_thermal_transmittance': None, + 'floor_insulation_thickness': 'none' + } + + prop3.set_floor_level() + + assert prop3.floor_level == 2 + + # Example of a house + prop4 = Property(1, "AB12CD", "Test Address", mock_epc_client) + prop4.data = {'floor-level': '', 'property-type': 'House'} + prop4.floor = { + 'original_description': '(Another dwelling below)', 'clean_description': 'Solid, no insulation', + 'thermal_transmittance': None, 'thermal_transmittance_unit': None, 'is_assumed': False, + 'is_to_unheated_space': False, 'is_to_external_air': False, 'is_suspended': False, 'is_solid': False, + 'another_property_below': False, 'insulation_thickness': 'none', 'floor_thermal_transmittance': None, + 'floor_insulation_thickness': 'none' + } + + prop4.set_floor_level() + + assert prop4.floor_level is None diff --git a/etl/testing_data/birmingham_pilot.py b/etl/testing_data/birmingham_pilot.py index ab39df7e..a049e35e 100644 --- a/etl/testing_data/birmingham_pilot.py +++ b/etl/testing_data/birmingham_pilot.py @@ -22,49 +22,149 @@ def app(): # Birmingham has a Local Authority Code of E08000025 + # ~~~~~~~~~~~~~~~~~~~~ + # First example + # ~~~~~~~~~~~~~~~~~~~~ # Let's take an EPC D property example_1_reponse = epc_client.domestic.search( params={ "local-authority": "E08000025", "property-type": "house", - } + }, + size=1000 ) - - g_data = epc_client.domestic.search(params={"energy-band": "g"}, size=n_g) - f_data = epc_client.domestic.search(params={"energy-band": "f"}, size=n_f) - e_data = epc_client.domestic.search(params={"energy-band": "e"}, size=n_e) - d_data = epc_client.domestic.search(params={"energy-band": "d"}, size=n_d) - c_data = epc_client.domestic.search(params={"energy-band": "c"}, size=n_c) - b_data = epc_client.domestic.search(params={"energy-band": "b"}, size=n_b) - a_data = epc_client.domestic.search(params={"energy-band": "a"}, size=n_a) - - # Combine the final data - final_data = ( - g_data["rows"] + f_data["rows"] + e_data["rows"] + d_data["rows"] + c_data["rows"] + b_data["rows"] - + a_data["rows"] - ) - - # TODO: We also take homes with just a specific type of wall - - final_data = [ - x for x in final_data if ("cavity wall" in x["walls-description"].lower()) or ( - "solid brick" in x["walls-description"].lower() - ) or ("average thermal transmittance" in x["walls-description"].lower()) + example_1_reponse = example_1_reponse["rows"] + # Get a property with a cavity wall + example_1_reponse_filtered = [ + x for x in example_1_reponse if + "cavity wall, as built, no insulation (assumed)" in x["walls-description"].lower() + ] + example_1_reponse_filtered = [ + x for x in example_1_reponse_filtered if "pitched, no insulation (assumed)" in x["roof-description"].lower() + ] + # Get a social housing property + example_1_reponse_filtered = [ + x for x in example_1_reponse_filtered if x["tenure"] == "Rented (social)" ] - # TODO: For the moment, don't use park homes - final_csv_data = pd.DataFrame( - [{"address": x["address"], "postcode": x["postcode"], "Notes": None} for x - in final_data if - x["property-type"] not in ["Park home"]] - ) + print(example_1_reponse_filtered[0]["postcode"]) + # B13 9LT + print(example_1_reponse_filtered[0]["address1"]) + # 113 Tenby Road + print(example_1_reponse_filtered[0]["built-form"]) + # Mid-Terrace + print(example_1_reponse_filtered[0]["current-energy-rating"]) + # 'D' - final_csv_data = pd.concat([starting_csv, final_csv_data]).reset_index(drop=True) + # ~~~~~~~~~~~~~~~~~~~~ + # Second example + # ~~~~~~~~~~~~~~~~~~~~ + + # Let's take an EPC E property + example_2_reponse = epc_client.domestic.search( + params={ + "local-authority": "E08000025", + "property-type": "house", + "energy-band": "e" + }, + size=1000 + ) + example_2_reponse = example_2_reponse["rows"] + # Get a solid wall example + example_2_reponse_filtered = [ + x for x in example_2_reponse if + "solid brick, as built, no insulation (assumed)" in x["walls-description"].lower() + ] + # With some existing loft insulation + example_2_reponse_filtered = [ + x for x in example_2_reponse_filtered if "pitched, 100 mm loft insulation" in x["roof-description"].lower() + ] + # Get a social housing property + example_2_reponse_filtered = [ + x for x in example_2_reponse_filtered if x["tenure"] == "Rented (social)" + ] + + print(example_2_reponse_filtered[0]["postcode"]) + # B28 8JF + print(example_2_reponse_filtered[0]["address1"]) + # 139 School Road + print(example_2_reponse_filtered[0]["built-form"]) + # Semi-Detached + print(example_2_reponse_filtered[0]["current-energy-rating"]) + # E + + # ~~~~~~~~~~~~~~~~~~~~ + # Third example + # ~~~~~~~~~~~~~~~~~~~~ + example_3_reponse = epc_client.domestic.search( + params={ + "local-authority": "E08000025", + "property-type": "house", + "energy-band": "f" + }, + size=1000 + ) + example_3_reponse = example_3_reponse["rows"] + # Get a social housing property] + example_3_reponse_filtered = [ + x for x in example_3_reponse if x["tenure"] == "Rented (social)" + ] + + print(example_3_reponse_filtered[4]["walls-description"]) + print(example_3_reponse_filtered[4]["floor-description"]) + print(example_3_reponse_filtered[4]["roof-description"]) + print(example_3_reponse_filtered[4]["postcode"]) + # B32 1SL + print(example_3_reponse_filtered[4]["address1"]) + # 77 Simmons Drive + print(example_3_reponse_filtered[4]["built-form"]) + # Semi-Detached + + # ~~~~~~~~~~~~~~~~~~~~ + # Final example + # ~~~~~~~~~~~~~~~~~~~~ + # Let's take a flat that is a D + example_4_reponse = epc_client.domestic.search( + params={ + "local-authority": "E08000025", + "property-type": "flat", + "energy-band": "d" + }, + size=1000 + ) + example_4_reponse = example_4_reponse["rows"] + + example_4_reponse_filtered = [ + x for x in example_4_reponse if + "cavity wall, as built, no insulation (assumed)" in x["walls-description"].lower() + ] + # Get a social housing property + example_4_reponse_filtered = [ + x for x in example_4_reponse_filtered if x["tenure"] == "Rented (social)" + ] + print(example_4_reponse_filtered[0]["postcode"]) + # B32 1LS + print(example_4_reponse_filtered[0]["address1"]) + # Flat 2 + + print(example_4_reponse_filtered[0]["floor-description"]) + print(example_4_reponse_filtered[0]["property-type"]) + # Flat + + test_file = pd.DataFrame( + [ + # New properties + {"address": "113 Tenby Road", "postcode": "B13 9LT", "Notes": None}, + {"address": "139 School Road", "postcode": "B28 8JF", "Notes": None}, + {"address": "77 Simmons Drive", "postcode": "B32 1SL", "Notes": None}, + {"address": "Flat 2, 54 Wedgewood Road", "postcode": "B32 1LS", "Notes": None}, + ] + ) # Store the data in s3 filename = f"{USER_ID}/{PORTFOLIO_ID}/test_inputs.csv" save_csv_to_s3( - dataframe=final_csv_data, + dataframe=test_file, bucket_name="retrofit-plan-inputs-dev", file_name=filename ) @@ -73,7 +173,7 @@ def app(): "portfolio_id": str(PORTFOLIO_ID), "housing_type": "Social", "goal": "Increase EPC", - "goal_value": "B", + "goal_value": "C", "trigger_file_path": filename } print(body) diff --git a/recommendations/Costs.py b/recommendations/Costs.py index a57092f9..0d9031b2 100644 --- a/recommendations/Costs.py +++ b/recommendations/Costs.py @@ -315,7 +315,9 @@ class Costs: subtotal_before_profit = labour_costs + materials_costs - contingency_cost = subtotal_before_profit * self.CONTINGENCY + # Because of the possiblity of damage to the existing floor, or difficulties associated to moving fittings, + # we use a higher contingency rate + contingency_cost = subtotal_before_profit * self.HIGH_RISK_CONTINGENCY preliminaries_cost = subtotal_before_profit * self.PRELIMINARIES profit_cost = subtotal_before_profit * self.PROFIT_MARGIN diff --git a/recommendations/FloorRecommendations.py b/recommendations/FloorRecommendations.py index 48245554..a246c8cb 100644 --- a/recommendations/FloorRecommendations.py +++ b/recommendations/FloorRecommendations.py @@ -10,7 +10,6 @@ from recommendations.recommendation_utils import ( r_value_per_mm_to_u_value, calculate_u_value_uplift, is_diminishing_returns, update_lowest_selected_u_value, get_recommended_part, get_floor_u_value ) -from recommendations.rdsap_tables import FLOOR_LEVEL_MAP from recommendations.Costs import Costs @@ -73,10 +72,6 @@ class FloorRecommendations(Definitions): def recommend(self): u_value = self.property.floor["thermal_transmittance"] - floor_level = ( - FLOOR_LEVEL_MAP[self.property.data["floor-level"]] if - self.property.data["floor-level"] not in self.DATA_ANOMALY_MATCHES else None - ) property_type = self.property.data["property-type"] floor_area = self.property.insulation_floor_area @@ -90,7 +85,9 @@ class FloorRecommendations(Definitions): return # If the property is a flat that isn't at ground level, it's likely impractical to recommend a floor upgrade - if (floor_level != 0) and (property_type == "Flat"): + if (self.property.floor_level != 0) and (property_type == "Flat") and ( + self.property.floor["another_property_below"] + ): return if u_value: diff --git a/recommendations/tests/test_costs.py b/recommendations/tests/test_costs.py index 1d66ff47..1d519b91 100644 --- a/recommendations/tests/test_costs.py +++ b/recommendations/tests/test_costs.py @@ -240,7 +240,7 @@ class TestCosts: ) assert sus_floor_results == { - 'total': 3114.6027360000003, 'subtotal': 2595.50228, 'vat': 519.100456, 'contingency': 185.39302, + 'total': 3337.07436, 'subtotal': 2780.8953, 'vat': 556.17906, 'contingency': 370.78604, 'preliminaries': 185.39302, 'material': 483.405, 'profit': 370.78604, 'labour_hours': 54.940000000000005, 'labour_days': 2.289166666666667, 'labour_cost': 1370.5252 } diff --git a/recommendations/tests/test_floor_recommendations.py b/recommendations/tests/test_floor_recommendations.py index 01bd308e..43e98d60 100644 --- a/recommendations/tests/test_floor_recommendations.py +++ b/recommendations/tests/test_floor_recommendations.py @@ -81,7 +81,7 @@ class TestFloorRecommendations: assert types == {"suspended_floor_insulation"} assert len(recommender.recommendations) == 6 - assert recommender.recommendations[0]["total"] == 4596.858 + assert recommender.recommendations[0]["total"] == 4925.205 assert recommender.recommendations[0]["new_u_value"] == 0.21 def test_uvalue_0_12(self, input_properties):