From 42dc635aa3e4f15400998da180d12a03dad0cbea Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Tue, 5 Nov 2024 17:37:04 +0000 Subject: [PATCH] ammended RIR conditions --- .idea/Model.iml | 2 +- .idea/misc.xml | 2 +- backend/app/plan/router.py | 6 +- backend/ml_models/Valuation.py | 22 +++- etl/customers/warwick/remote_assessments.py | 123 ++++++++++++++++++++ recommendations/Recommendations.py | 8 +- recommendations/RoofRecommendations.py | 13 ++- 7 files changed, 166 insertions(+), 10 deletions(-) create mode 100644 etl/customers/warwick/remote_assessments.py diff --git a/.idea/Model.iml b/.idea/Model.iml index 0e963140..df6c4faa 100644 --- a/.idea/Model.iml +++ b/.idea/Model.iml @@ -7,7 +7,7 @@ - + diff --git a/.idea/misc.xml b/.idea/misc.xml index 35513387..50cad4ca 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -3,7 +3,7 @@ - + diff --git a/backend/app/plan/router.py b/backend/app/plan/router.py index 119c2061..65a6c32c 100644 --- a/backend/app/plan/router.py +++ b/backend/app/plan/router.py @@ -759,7 +759,11 @@ async def trigger_plan(body: PlanTriggerRequest): new_epc = sap_to_epc(new_sap_points) new_epc_bands[p.id] = new_epc - valuations = PropertyValuation.estimate(property_instance=p, target_epc=new_epc) + total_cost = sum([r["total"] for r in default_recommendations]) + + valuations = PropertyValuation.estimate( + property_instance=p, target_epc=new_epc, total_cost=total_cost + ) property_value_increase_ranges[p.id] = valuations if p.is_new: diff --git a/backend/ml_models/Valuation.py b/backend/ml_models/Valuation.py index 92c55641..720005d3 100644 --- a/backend/ml_models/Valuation.py +++ b/backend/ml_models/Valuation.py @@ -203,7 +203,14 @@ class PropertyValuation: return msm_increase, lloyds_increase @classmethod - def estimate(cls, property_instance, target_epc): + def estimate(cls, property_instance, target_epc, total_cost=None): + """ + This function estimates the value of a property based on the current EPC rating and the target EPC rating + :param property_instance: An instance of the Property class + :param target_epc: The target EPC rating + :param total_cost: The total cost of the retrofit + :return: + """ current_value = ( property_instance.valuation if property_instance.valuation else cls.UPRN_VALUE_LOOKUP.get(property_instance.uprn) @@ -242,6 +249,19 @@ class PropertyValuation: avg_increase = np.mean(all_increases) + if total_cost is not None: + # We CAP the retrofit ROI at 2 + avg_increase_value = current_value * avg_increase + if avg_increase_value / total_cost > 2: + # We re-scale the % so that the average value increase is no more than 2 times the total cost + double_cost = 2 * total_cost + new_avg_increase = double_cost / current_value + scalar = new_avg_increase / avg_increase + # We scale the min and max increases by the same scalar + min_increase *= scalar + max_increase *= scalar + avg_increase = new_avg_increase + return { "current_value": current_value, "lower_bound_increased_value": float(current_value * (1 + min_increase)), diff --git a/etl/customers/warwick/remote_assessments.py b/etl/customers/warwick/remote_assessments.py new file mode 100644 index 00000000..a9b654b7 --- /dev/null +++ b/etl/customers/warwick/remote_assessments.py @@ -0,0 +1,123 @@ +import pandas as pd +from utils.s3 import save_csv_to_s3 + +PORTFOLIO_ID = 115 +USER_ID = 8 + + +def app(): + """ + Used to set up the remote assessments for Warwick + """ + + asset_list = [ + { + "uprn": 10033604792, + "address": "Flat 2, 3 Green Street", + "postcode": "W1K 6RN" + }, + { + "uprn": 10033604794, + "address": "Flat 4, 3 Green Street", + "postcode": "W1K 6RN" + }, + { + "uprn": 10033615515, + "address": "Apartment 4, 52 Green Street", + "postcode": "W1K 6RS" + } + ] + asset_list = pd.DataFrame(asset_list) + + # Store the asset list in s3 + filename = f"{USER_ID}/{PORTFOLIO_ID}/asset_list.csv" + save_csv_to_s3( + dataframe=asset_list, + bucket_name="retrofit-plan-inputs-dev", + file_name=filename + ) + + non_invasive_recommendations = [ + { + "uprn": 10033604792, + "recommendations": [ + { + "type": "internal_wall_insulation", + "sap_points": 16, + "survey": True + } + ] + }, + { + "uprn": 10033604794, + "recommendations": [ + { + "type": "internal_wall_insulation", + "sap_points": 14, + "survey": True + } + ] + }, + { + "uprn": 10033615515, + "recommendations": [ + { + "type": "room_roof_insulation", + "sap_points": 12, + "survey": True + }, + { + "type": "internal_wall_insulation", + "sap_points": 2, + "survey": True + } + ] + } + ] + + # Store non-invasive recommendations in S3 + non_invasive_recommendations_filename = f"{USER_ID}/{PORTFOLIO_ID}/non_invasive_recommendations.csv" + save_csv_to_s3( + dataframe=pd.DataFrame(non_invasive_recommendations), + bucket_name="retrofit-plan-inputs-dev", + file_name=non_invasive_recommendations_filename + ) + + valuation_data = [ + { + "uprn": 10033604792, + "value": 3_692_000 + }, + { + "uprn": 10033604794, + "value": 3_789_000 + }, + { + "uprn": 10033615515, + "value": 3_499_000 + } + ] + + # Store valuation data to s3 + valuation_filename = f"{USER_ID}/{PORTFOLIO_ID}/valuation.csv" + save_csv_to_s3( + dataframe=pd.DataFrame(valuation_data), + bucket_name="retrofit-plan-inputs-dev", + file_name=valuation_filename + ) + + body = { + "portfolio_id": str(PORTFOLIO_ID), + "housing_type": "Private", + "goal": "Increasing EPC", + "goal_value": "C", + "trigger_file_path": filename, + "already_installed_file_path": "", + "patches_file_path": "", + "non_invasive_recommendations_file_path": non_invasive_recommendations_filename, + "valuation_file_path": valuation_filename, + "scenario_name": "Full package remote assessment", + "multi_plan": True, + "budget": None, + } + print(body) diff --git a/recommendations/Recommendations.py b/recommendations/Recommendations.py index dd51b47d..a1183d33 100644 --- a/recommendations/Recommendations.py +++ b/recommendations/Recommendations.py @@ -519,6 +519,7 @@ class Recommendations: # heating_cost_starting and heating_cost_ending are just the values in the EPC. However, with # heating_cost_ending, we expect that the EPC will predict a heating cost based on what would happen # if we implemented the recommendation today, so our starting value is the EPC + previous_phase_values = { "sap": float(property_instance.data["current-energy-efficiency"]), "carbon": float(property_instance.data["co2-emissions-current"]), @@ -541,8 +542,13 @@ class Recommendations: previous_phase_values = previous_phase_values_multiple[0] # We extract the values for the current phase + if rec.get("survey", False): + current_phase_sap = rec["sap_points"] + previous_phase_values["sap"] + else: + current_phase_sap = phase_energy_efficiency_metrics["sap_change"] + current_phase_values = { - "sap": phase_energy_efficiency_metrics["sap_change"], + "sap": current_phase_sap, "carbon": phase_energy_efficiency_metrics["carbon_change"], "heat_demand": phase_energy_efficiency_metrics["heat_demand"], } diff --git a/recommendations/RoofRecommendations.py b/recommendations/RoofRecommendations.py index c0fa4eb2..acc78359 100644 --- a/recommendations/RoofRecommendations.py +++ b/recommendations/RoofRecommendations.py @@ -123,7 +123,11 @@ class RoofRecommendations: self.property.roof["insulation_thickness"] in ["average", "above_average"] ) - return full_insulated_room_roof or room_roof_insulated_at_rafters + has_non_invasive_recommendation = any( + x["type"] == "room_roof_insulation" for x in self.property.non_invasive_recommendations + ) + + return (full_insulated_room_roof or room_roof_insulated_at_rafters) and not has_non_invasive_recommendation def recommend(self, phase, measures=None, default_u_values=False): @@ -181,7 +185,8 @@ class RoofRecommendations: # We firstly handle non-intrusive recommendations, which may override the normal roof insulation recommendations if ("loft_insulation" in [x["type"] for x in non_invasive_recommendations]) or ( - self.property.roof["is_pitched"] and "loft_insulation" in measures + self.property.roof["is_pitched"] and "loft_insulation" in measures and + not self.property.roof["is_at_rafters"] ): self.recommend_roof_insulation( u_value=u_value, @@ -512,8 +517,6 @@ class RoofRecommendations: rir_non_invasive_recommendation.get("cost") ) - sap_points = rir_non_invasive_recommendation.get("sap_points", None) - # Could also be Roof room(s), ceiling insulated new_descriptin = "Roof room(s), insulated" roof_ending_config = RoofAttributes(new_descriptin).process() @@ -562,7 +565,7 @@ class RoofRecommendations: "description": "Insulate room in roof at rafters and re-decorate", "starting_u_value": u_value, "new_u_value": new_u_value, - "sap_points": sap_points, + "sap_points": rir_non_invasive_recommendation.get("sap_points", None), "simulation_config": simulation_config, "description_simulation": { "roof-description": new_descriptin,