diff --git a/.idea/Model.iml b/.idea/Model.iml
index c75af922..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 1f2c584d..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 a8ed9129..7b5a6bc3 100644
--- a/backend/Property.py
+++ b/backend/Property.py
@@ -142,6 +142,8 @@ class Property:
self.current_adjusted_energy = None
self.expected_adjusted_energy = None
+ self.current_energy_bill = None
+ self.expected_energy_bill = None
self.recommendations_scoring_data = []
@@ -892,12 +894,16 @@ class Property:
return component_data
- def set_adjusted_energy(self, current_adjusted_energy, expected_adjusted_energy):
+ def set_adjusted_energy(
+ self, current_adjusted_energy, expected_adjusted_energy, current_energy_bill, expected_energy_bill
+ ):
"""
Stores these values for usage later
"""
self.current_adjusted_energy = current_adjusted_energy
self.expected_adjusted_energy = expected_adjusted_energy
+ self.current_energy_bill = current_energy_bill
+ self.expected_energy_bill = expected_energy_bill
def set_windows_count(self):
"""
diff --git a/backend/app/db/functions/portfolio_functions.py b/backend/app/db/functions/portfolio_functions.py
index ead8280f..69203368 100644
--- a/backend/app/db/functions/portfolio_functions.py
+++ b/backend/app/db/functions/portfolio_functions.py
@@ -4,7 +4,7 @@ from backend.app.db.models.portfolio import Portfolio
def aggregate_portfolio_recommendations(
- session, portfolio_id: int, total_valuation_increase: float, labour_days: float
+ session, portfolio_id: int, total_valuation_increase: float, labour_days: float, aggregated_data: dict
):
# Aggregate multiple fields
aggregates = (
@@ -27,6 +27,7 @@ def aggregate_portfolio_recommendations(
"energy_savings": aggregates.energy_savings or 0,
"co2_equivalent_savings": aggregates.co2_equivalent_savings or 0,
"energy_cost_savings": aggregates.energy_cost_savings or 0,
+ **aggregated_data
}
# Get the portfolio and update the fields
diff --git a/backend/app/plan/router.py b/backend/app/plan/router.py
index 49e14872..b8b2d5c8 100644
--- a/backend/app/plan/router.py
+++ b/backend/app/plan/router.py
@@ -1,3 +1,4 @@
+import json
from datetime import datetime
from tqdm import tqdm
@@ -57,6 +58,109 @@ def patch_epc(patch, epc_records):
return epc_records
+def extract_portfolio_aggregation_data(
+ input_properties, total_valuation_increase, recommendations, new_epc_bands
+):
+ # We aggregate a number of metrics for the portfolio:
+ # 1) A breakdown of the number of properties in each EPC band
+ # a) before retrofit
+ # b) after retrofit
+ # 2) Number of units
+ # 3) Co2/unit
+ # a) before retrofit
+ # b) after retrofit
+ # 4) Energy bulls/unit
+ # a) before retrofit
+ # b) after retrofit
+ # 5) Average valuation improvement/unit
+ # 6) Total cost
+ # 7) Cost per unit
+ # 8) £ per CO2 saved
+ # 9) £ per SAP point
+
+ # We need to construct the underlyind data for this
+
+ # Helper function to reformat the EPC data
+ def reformat_epc_data(epc_counts):
+ # Define all possible EPC bands in the required order
+ epc_bands = ["G", "F", "E", "D", "C", "B", "A"]
+
+ # Create the formatted data list by checking each band in the order
+ formatted_data = []
+ for band in epc_bands:
+ # Get the count from the dictionary, defaulting to 0 if not present
+ count = epc_counts.get(band, 0)
+ # Append the formatted dictionary to the list
+ formatted_data.append({"name": band, band: count})
+
+ return formatted_data
+
+ n_units = len(input_properties)
+
+ agg_data = []
+ for p in input_properties:
+ # Get the recommendations for the property
+ property_recommendations = recommendations.get(p.id, [])
+ if not property_recommendations:
+ continue
+ # Get just the default recommendations
+ default_recommendations = [r for r in property_recommendations if r["default"]]
+
+ # We can now calculate multiple outputs based on default recommendations
+ carbon_savings = sum([r["co2_equivalent_savings"] for r in default_recommendations])
+
+ pre_retrofit_co2 = p.data["co2-emissions-current"]
+ post_retrofit_co2 = pre_retrofit_co2 - carbon_savings
+
+ pre_retrofit_energy_bill = p.current_energy_bill
+ post_retrofit_energy_bill = p.expected_energy_bill
+
+ cost = sum([r["total"] for r in default_recommendations])
+ sap_point_improvement = sum([r["sap_points"] for r in default_recommendations])
+
+ agg_data.append({
+ "pre_retrofit_epc": p.data["current-energy-rating"],
+ "post_retrofit_epc": new_epc_bands[p.id],
+ "pre_retrofit_co2": pre_retrofit_co2,
+ "post_retrofit_co2": post_retrofit_co2,
+ "pre_retrofit_energy_bill": pre_retrofit_energy_bill,
+ "post_retrofit_energy_bill": post_retrofit_energy_bill,
+ "cost": cost,
+ "sap_point_improvement": sap_point_improvement
+ })
+
+ agg_data = pd.DataFrame(agg_data)
+
+ n_units_to_retrofit = len(agg_data)
+
+ valuation_improvment_per_unit = total_valuation_increase / n_units_to_retrofit
+
+ total_carbon_saved = agg_data["pre_retrofit_co2"].sum() - agg_data["post_retrofit_co2"].sum()
+ total_sap_points = agg_data["sap_point_improvement"].sum()
+
+ aggregation_data = {
+ "epc_breakdown_pre_retrofit": json.dumps(
+ reformat_epc_data(agg_data["pre_retrofit_epc"].value_counts().to_dict())
+ ),
+ "epc_breakdown_post_retrofit": json.dumps(
+ reformat_epc_data(agg_data["post_retrofit_epc"].value_counts().to_dict())
+ ),
+ "number_of_properties": n_units,
+ "n_units_to_retrofit": n_units_to_retrofit,
+ "co2_per_unit_pre_retrofit": agg_data["pre_retrofit_co2"].mean(),
+ "co2_per_unit_post_retrofit": agg_data["post_retrofit_co2"].mean(),
+ "energy_bill_per_unit_pre_retrofit": agg_data["pre_retrofit_energy_bill"].mean(),
+ "energy_bill_per_unit_post_retrofit": agg_data["post_retrofit_energy_bill"].mean(),
+ "valuation_improvement_per_unit": valuation_improvment_per_unit,
+ "total_cost": agg_data["cost"].sum(),
+ "cost_per_unit": agg_data["cost"].mean(),
+ "cost_per_co2_saved": agg_data["cost"].sum() / total_carbon_saved,
+ "cost_per_sap_point": agg_data["cost"].sum() / total_sap_points
+ }
+
+ return aggregation_data
+
+
router = APIRouter(
prefix="/plan",
tags=["plan"],
@@ -243,7 +347,13 @@ async def trigger_plan(body: PlanTriggerRequest):
property_instance = [p for p in input_properties if p.id == property_id][0]
- recommendations_with_impact, current_adjusted_energy, expected_adjusted_energy = (
+ (
+ recommendations_with_impact,
+ current_adjusted_energy,
+ expected_adjusted_energy,
+ current_energy_bill,
+ expected_energy_bill
+ ) = (
Recommendations.calculate_recommendation_impact(
property_instance=property_instance,
all_predictions=all_predictions,
@@ -254,7 +364,9 @@ async def trigger_plan(body: PlanTriggerRequest):
# Store the resulting adjusted energy in the property instance
property_instance.set_adjusted_energy(
current_adjusted_energy=current_adjusted_energy,
- expected_adjusted_energy=expected_adjusted_energy
+ expected_adjusted_energy=expected_adjusted_energy,
+ current_energy_bill=current_energy_bill,
+ expected_energy_bill=expected_energy_bill
)
input_measures = prepare_input_measures(recommendations_with_impact, body.goal)
@@ -316,6 +428,7 @@ async def trigger_plan(body: PlanTriggerRequest):
logger.info("Uploading recommendations to the database")
property_valuation_increases = []
session.commit()
+ new_epc_bands = {}
for i in range(0, len(input_properties), BATCH_SIZE):
try:
# Take a slice of the input_properties list to make a batch
@@ -327,6 +440,7 @@ async def trigger_plan(body: PlanTriggerRequest):
total_sap_points = sum([r["sap_points"] for r in default_recommendations])
new_sap_points = float(p.data["current-energy-efficiency"]) + total_sap_points
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)
@@ -392,11 +506,19 @@ async def trigger_plan(body: PlanTriggerRequest):
[sum(r["labour_days"] for r in rec_group if r["default"]) for p_id, rec_group in recommendations.items()]
))
+ aggregated_data = extract_portfolio_aggregation_data(
+ input_properties=input_properties,
+ total_valuation_increase=total_valuation_increase,
+ recommendations=recommendations,
+ new_epc_bands=new_epc_bands
+ )
+
aggregate_portfolio_recommendations(
session,
portfolio_id=body.portfolio_id,
total_valuation_increase=total_valuation_increase,
- labour_days=labour_days
+ labour_days=labour_days,
+ aggregated_data=aggregated_data
)
# Commit final changes
diff --git a/recommendations/Recommendations.py b/recommendations/Recommendations.py
index 68fead16..659b41a8 100644
--- a/recommendations/Recommendations.py
+++ b/recommendations/Recommendations.py
@@ -281,6 +281,9 @@ class Recommendations:
current_adjusted_energy - expected_adjusted_energy
)
+ current_energy_bill = AnnualBillSavings.calculate_annual_bill(current_adjusted_energy)
+ expected_energy_bill = AnnualBillSavings.calculate_annual_bill(expected_adjusted_energy)
+
for recommendations_by_type in property_recommendations:
for rec in recommendations_by_type:
@@ -355,4 +358,10 @@ class Recommendations:
rec["heat_demand"] is None) or (rec["energy_cost_savings"] is None):
raise ValueError("sap points, co2 or heat demand is missing")
- return property_recommendations, current_adjusted_energy, expected_adjusted_energy
+ return (
+ property_recommendations,
+ current_adjusted_energy,
+ expected_adjusted_energy,
+ current_energy_bill,
+ expected_energy_bill
+ )