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 + )