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,