diff --git a/backend/Property.py b/backend/Property.py index 433bebe7..9eb8ef99 100644 --- a/backend/Property.py +++ b/backend/Property.py @@ -364,10 +364,6 @@ class Property: simulation_epc = self.epc_record.prepared_epc.copy() # Insert static values simulation_epc["lodgement_date"] = simulation_lodgment_date - # Insert today's costs, unadjusted (i.e. in line with what we expect the EPC would say today) - simulation_epc["heating-cost-current"] = round(self.energy_cost_estimates["unadjusted"]["heating"]) - simulation_epc["lighting-cost-current"] = round(self.energy_cost_estimates["unadjusted"]["lighting"]) - simulation_epc["hot-water-cost-current"] = round(self.energy_cost_estimates["unadjusted"]["hot_water"]) # Replace the understores with hyphens simulation_epc = {k.replace("_", "-"): v for k, v in simulation_epc.items()} @@ -698,44 +694,44 @@ class Property: appliances_kwh = AnnualBillSavings.estimate_appliances_energy_use(total_floor_area=self.floor_area) - adjusted_heating_kwh = AnnualBillSavings.adjust_energy_cost_to_metered( - epc_energy_cost=heating_prediction, + adjusted_heating_kwh = AnnualBillSavings.adjust_energy_to_metered( + epc_energy=heating_prediction, current_epc_rating=self.data["current-energy-rating"], ) - adjusted_hot_water_kwh = AnnualBillSavings.adjust_energy_cost_to_metered( - epc_energy_cost=hot_water_prediction, + adjusted_hot_water_kwh = AnnualBillSavings.adjust_energy_to_metered( + epc_energy=hot_water_prediction, current_epc_rating=self.data["current-energy-rating"], ) - adjusted_lighting_kwh = AnnualBillSavings.adjust_energy_cost_to_metered( - epc_energy_cost=lighting_kwh, + adjusted_lighting_kwh = AnnualBillSavings.adjust_energy_to_metered( + epc_energy=lighting_kwh, current_epc_rating=self.data["current-energy-rating"], ) - adjusted_applicances_kwh = AnnualBillSavings.adjust_energy_cost_to_metered( - epc_energy_cost=appliances_kwh, + adjusted_applicances_kwh = AnnualBillSavings.adjust_energy_to_metered( + epc_energy=appliances_kwh, current_epc_rating=self.data["current-energy-rating"], ) # Adjust today's cost figures with the UCL model - adjusted_heating_cost = AnnualBillSavings.adjust_energy_cost_to_metered( - epc_energy_cost=todays_heating_cost, + adjusted_heating_cost = AnnualBillSavings.adjust_energy_to_metered( + epc_energy=todays_heating_cost, current_epc_rating=self.data["current-energy-rating"], ) - adjusted_hot_water_cost = AnnualBillSavings.adjust_energy_cost_to_metered( - epc_energy_cost=todays_hot_water_cost, + adjusted_hot_water_cost = AnnualBillSavings.adjust_energy_to_metered( + epc_energy=todays_hot_water_cost, current_epc_rating=self.data["current-energy-rating"], ) - adjusted_lighting_cost = AnnualBillSavings.adjust_energy_cost_to_metered( - epc_energy_cost=todays_lighting_cost, + adjusted_lighting_cost = AnnualBillSavings.adjust_energy_to_metered( + epc_energy=todays_lighting_cost, current_epc_rating=self.data["current-energy-rating"], ) - adjusted_appliances_cost = AnnualBillSavings.adjust_energy_cost_to_metered( - epc_energy_cost=appliances_kwh * AnnualBillSavings.ELECTRICITY_PRICE_CAP, + adjusted_appliances_cost = AnnualBillSavings.adjust_energy_to_metered( + epc_energy=appliances_kwh * AnnualBillSavings.ELECTRICITY_PRICE_CAP, current_epc_rating=self.data["current-energy-rating"], ) diff --git a/backend/ml_models/AnnualBillSavings.py b/backend/ml_models/AnnualBillSavings.py index 4747e587..a0c426bb 100644 --- a/backend/ml_models/AnnualBillSavings.py +++ b/backend/ml_models/AnnualBillSavings.py @@ -133,53 +133,7 @@ class AnnualBillSavings: return appliances_energy_use @classmethod - def adjust_energy_to_metered( - cls, epc_energy_consumption, current_epc_rating - ): - """ - The over-prediction of energy use by EPCs in Great Britain: A comparison - of EPC-modelled and metered primary energy use intensity - - Which can be found here: https://www.sciencedirect.com/science/article/pii/S0378778823002542 - We implement the results on page 10 - - :return: - """ - - gradients = { - "A": -0.1, - "B": -0.1, - "C": -0.43, - "D": -0.52, - "E": -0.7, - "F": -0.76, - "G": -0.76 - } - - intercepts = { - "A": 28, - "B": 28, - "C": 97, - "D": 119, - "E": 160, - "F": 157, - "G": 157 - } - - gradient = gradients[current_epc_rating] - intercept = intercepts[current_epc_rating] - - # This should be negative - consumption_difference = gradient * epc_energy_consumption + intercept - - adjusted_consumption = (epc_energy_consumption + consumption_difference) - if adjusted_consumption < 0: - raise ValueError("consumption_difference should be negative") - - return adjusted_consumption - - @classmethod - def adjust_energy_cost_to_metered(cls, epc_energy_cost, current_epc_rating): + def adjust_energy_to_metered(cls, epc_energy, current_epc_rating): """ The over-prediction of energy use by EPCs in Great Britain: A comparison of EPC-modelled and metered primary energy use intensity @@ -188,6 +142,7 @@ class AnnualBillSavings: We implement the results on page 10 This is used to just re-map the cost from the EPC to the metered cost + epc_energy could be cost or kwh :return: """ @@ -215,10 +170,10 @@ class AnnualBillSavings: intercept = intercepts[current_epc_rating] # This should be negative - consumption_difference = gradient * epc_energy_cost + intercept + consumption_difference = gradient * epc_energy + intercept consumption_difference = 0 if consumption_difference > 0 else consumption_difference - adjusted_consumption = (epc_energy_cost + consumption_difference) + adjusted_consumption = (epc_energy + consumption_difference) if adjusted_consumption < 0: raise ValueError("consumption_difference should be negative") diff --git a/recommendations/Recommendations.py b/recommendations/Recommendations.py index 1541246a..470b0554 100644 --- a/recommendations/Recommendations.py +++ b/recommendations/Recommendations.py @@ -1,3 +1,4 @@ +import pandas as pd from backend.Property import Property from typing import List from itertools import groupby @@ -276,7 +277,9 @@ class Recommendations: return property_recommendations @classmethod - def calculate_recommendation_impact(cls, property_instance, all_predictions, recommendations): + def calculate_recommendation_impact( + cls, property_instance, all_predictions, recommendations, energy_consumption_client + ): """ Given predictions from the model apis, with method will update the recommendations with the predicted @@ -285,6 +288,7 @@ class Recommendations: :param property_instance: Instance of the Property class, for the home associated to property_id :param all_predictions: dictionary of predictions from the model apis :param recommendations: dictionary of recommendations for the property + :param energy_consumption_client: Instance of the EnergyConsumptionClient class :return: """ @@ -297,6 +301,34 @@ class Recommendations: property_carbon_predictions = all_predictions["carbon_change_predictions"][ all_predictions["carbon_change_predictions"]["property_id"] == str(property_instance.id) ].copy() + property_lighting_cost_predictions = all_predictions["lighting_cost_predictions"][ + all_predictions["lighting_cost_predictions"]["property_id"] == str(property_instance.id) + ].copy() + property_heating_cost_predictions = all_predictions["heating_cost_predictions"][ + all_predictions["heating_cost_predictions"]["property_id"] == str(property_instance.id) + ].copy() + property_hot_water_cost_predictions = all_predictions["hot_water_cost_predictions"][ + all_predictions["hot_water_cost_predictions"]["property_id"] == str(property_instance.id) + ].copy() + + # We apply adjustments to each of the heating costs + property_lighting_cost_predictions["adjusted_cost"] = property_lighting_cost_predictions["predictions"].apply( + lambda x: AnnualBillSavings.adjust_energy_to_metered( + x, current_epc_rating=property_instance.data["current-energy-rating"] + ) + ) + + property_heating_cost_predictions["adjusted_cost"] = property_heating_cost_predictions["predictions"].apply( + lambda x: AnnualBillSavings.adjust_energy_to_metered( + x, current_epc_rating=property_instance.data["current-energy-rating"] + ) + ) + + property_hot_water_cost_predictions["adjusted_cost"] = property_hot_water_cost_predictions["predictions"].apply( + lambda x: AnnualBillSavings.adjust_energy_to_metered( + x, current_epc_rating=property_instance.data["current-energy-rating"] + ) + ) property_recommendations = recommendations[property_instance.id].copy() @@ -304,32 +336,43 @@ class Recommendations: sap_phase_impact = property_sap_predictions.groupby("phase")["predictions"].median().reset_index() heat_phase_impact = property_heat_predictions.groupby("phase")["predictions"].median().reset_index() carbon_phase_impact = property_carbon_predictions.groupby("phase")["predictions"].median().reset_index() + lighting_cost_phase_impact = ( + property_lighting_cost_predictions.groupby("phase")[["adjusted_cost", "predictions"]].median().reset_index() + ) + heating_cost_phase_impact = ( + property_heating_cost_predictions.groupby("phase")[["adjusted_cost", "predictions"]].median().reset_index() + ) + hot_water_cost_phase_impact = ( + property_hot_water_cost_predictions.groupby("phase")[ + ["adjusted_cost", "predictions"] + ].median().reset_index() + ) # The heat demand change is the difference between the starting heat demand and the value at the final phase - expected_heat_demand = property_instance.floor_area * ( - heat_phase_impact[heat_phase_impact["phase"] == max(heat_phase_impact["phase"])]["predictions"].values[0] - ) - starting_heat_demand = ( - float(property_instance.data["energy-consumption-current"]) * property_instance.floor_area - ) - - # This is the unadjusted resulting heat demand - predicted_heat_demand_change = starting_heat_demand - expected_heat_demand - - # TODO: This isn't quite right as this is based on EVERY possible measure, not just the ones that are - # actually implemented - expected_adjusted_energy = AnnualBillSavings.adjust_energy_to_metered( - epc_energy_consumption=expected_heat_demand, - current_epc_rating=property_instance.data["current-energy-rating"], - total_floor_area=property_instance.floor_area - ) - - adjusted_heat_demand_change = ( - property_instance.current_adjusted_energy - expected_adjusted_energy - ) - - # TODO: We should determine if the home is gas & electricity or just electricity - expected_energy_bill = AnnualBillSavings.calculate_annual_bill(expected_adjusted_energy) + # expected_heat_demand = property_instance.floor_area * ( + # heat_phase_impact[heat_phase_impact["phase"] == max(heat_phase_impact["phase"])]["predictions"].values[0] + # ) + # starting_heat_demand = ( + # float(property_instance.data["energy-consumption-current"]) * property_instance.floor_area + # ) + # + # # This is the unadjusted resulting heat demand + # predicted_heat_demand_change = starting_heat_demand - expected_heat_demand + # + # # TODO: This isn't quite right as this is based on EVERY possible measure, not just the ones that are + # # actually implemented + # expected_adjusted_energy = AnnualBillSavings.adjust_energy_to_metered( + # epc_energy_consumption=expected_heat_demand, + # current_epc_rating=property_instance.data["current-energy-rating"], + # total_floor_area=property_instance.floor_area + # ) + # + # adjusted_heat_demand_change = ( + # property_instance.current_adjusted_energy - expected_adjusted_energy + # ) + # + # # TODO: We should determine if the home is gas & electricity or just electricity + # expected_energy_bill = AnnualBillSavings.calculate_annual_bill(expected_adjusted_energy) for recommendations_by_type in property_recommendations: for rec in recommendations_by_type: @@ -350,12 +393,126 @@ class Recommendations: rec["recommendation_id"] )]["predictions"].values[0] + # Lighting costs won't change unless we have a lighting recommendation + new_lighting_cost_data = property_lighting_cost_predictions[ + property_lighting_cost_predictions["recommendation_id"] == str(rec["recommendation_id"]) + ] + + new_lighting_cost = new_lighting_cost_data["adjusted_cost"].values[0] + new_lighting_cost_unadjusted = new_lighting_cost_data["predictions"].values[0] + + new_heating_cost_data = property_heating_cost_predictions[ + property_heating_cost_predictions["recommendation_id"] == str(rec["recommendation_id"]) + ] + + new_heating_cost = new_heating_cost_data["adjusted_cost"].values[0] + new_heating_cost_unadjusted = new_heating_cost_data["predictions"].values[0] + + new_hot_water_cost_data = property_hot_water_cost_predictions[ + property_hot_water_cost_predictions["recommendation_id"] == str(rec["recommendation_id"]) + ] + + new_hot_water_cost = new_hot_water_cost_data["adjusted_cost"].values[0] + new_hot_water_cost_unadjusted = new_hot_water_cost_data["predictions"].values[0] + if rec["phase"] == 0: predicted_sap_points = new_sap - float(property_instance.data["current-energy-efficiency"]) predicted_co2_savings = float(property_instance.data["co2-emissions-current"]) - new_carbon predicted_heat_demand = property_instance.floor_area * ( float(property_instance.data["energy-consumption-current"]) - new_heat_demand ) + + predicted_heating_cost_reduction = ( + float(property_instance.energy_cost_estimates["adjusted"]["heating"]) - new_heating_cost + ) + predicted_hot_water_cost_reduction = ( + float(property_instance.energy_cost_estimates["adjusted"]["hot_water"]) - new_hot_water_cost + ) + predicted_heating_cost_reduction = ( + 0 if predicted_heating_cost_reduction < 0 else predicted_heating_cost_reduction + ) + predicted_hot_water_cost_reduction = ( + 0 if predicted_hot_water_cost_reduction < 0 else predicted_hot_water_cost_reduction + ) + + # Only lighting recommendations can have an impact here + predicted_lighting_cost_reduction = 0 if rec["type"] != "lighting" else ( + float(property_instance.energy_cost_estimates["adjusted"]["lighting"]) - new_lighting_cost + ) + + # This is the total bill savings for the recommendation + if rec["type"] == "solar_pv": + # We need to calculate the predicted bill savings for the solar pv recommendation + # where we will get some savings from the cost of appliances but it depends on the amount + # of energy generated by the solar panels + # We can assume that 50% of the energy generated will be used by the property without + # a battery, to be conservative. + # SIMILARLY: We need to handle kwh savings + raise Exception("Handle me") + else: + predicted_bill_savings = ( + predicted_heating_cost_reduction + predicted_hot_water_cost_reduction + + predicted_lighting_cost_reduction + ) + + # We now predict the kwh savings using the xgb model + scoring_heating_cost = min( + property_instance.energy_cost_estimates["unadjusted"]["heating"], new_heating_cost_unadjusted + ) + scoring_hot_water_cost = min( + property_instance.energy_cost_estimates["unadjusted"]["hot_water"], + new_hot_water_cost_unadjusted + ) + scoring_lighting_cost = min( + property_instance.energy_cost_estimates["unadjusted"]["lighting"], new_lighting_cost_unadjusted + ) if rec["type"] == "lighting" \ + else property_instance.energy_cost_estimates["unadjusted"]["lighting"] + + simulation_epc = property_instance.simulation_epcs[rec["phase"]].copy() + # The current heating, hot water and energy kwh should be based on the new, unadjusted + # costs for lighting, heating, hot water + simulation_epc["heating-cost-current"] = int(scoring_heating_cost) + simulation_epc["hot-water-cost-current"] = int(scoring_hot_water_cost) + simulation_epc["lighting-cost-current"] = int(scoring_lighting_cost) + # We predict with the energy consumption model + scoring_df = pd.DataFrame([simulation_epc]) + # Change columns from underscores to hyphens + scoring_df.columns = [ + x.lower().replace("_", "-") for x in scoring_df.columns + ] + for col in ["heating_kwh", "hot_water_kwh"]: + scoring_df[col] = None + + energy_consumption_client.data = None + new_heating_kwh = energy_consumption_client.score_new_data( + new_data=scoring_df, target="heating_kwh" + )[0] + + new_hot_water_kwh = energy_consumption_client.score_new_data( + new_data=scoring_df, target="hot_water_kwh" + )[0] + + # Adjust these figures + new_heating_kwh_adjusted = AnnualBillSavings.adjust_energy_to_metered( + new_heating_kwh, current_epc_rating=property_instance.data["current-energy-rating"] + ) + new_hot_water_kwh_adjusted = AnnualBillSavings.adjust_energy_to_metered( + new_hot_water_kwh, current_epc_rating=property_instance.data["current-energy-rating"] + ) + + heating_kwh_reduction = 0 if predicted_heating_cost_reduction == 0 else ( + property_instance.energy_consumption_estimates["adjusted"]["heating"] - new_heating_kwh_adjusted + ) + + hot_water_kwh_reduction = 0 if predicted_hot_water_cost_reduction == 0 else ( + property_instance.energy_consumption_estimates["adjusted"]["hot_water"] - + new_hot_water_kwh_adjusted + ) + + lighting_kwh_reduction = predicted_lighting_cost_reduction / AnnualBillSavings.ELECTRICITY_PRICE_CAP + + kwh_reduction = heating_kwh_reduction + hot_water_kwh_reduction + lighting_kwh_reduction + else: previous_phase = rec["phase"] - 1 predicted_sap_points = ( @@ -383,23 +540,8 @@ class Recommendations: # Round to 2 decimal places rec["sap_points"] = round(rec["sap_points"], 2) - # We now calculate the adjusted heat demand for this recommendation, which is simply the percentage - # of the total adjusted heat demand change. The percentage we use is this recommendation's percentage - # of the total heat demand per square meter change - - rec["adjusted_heat_demand"] = adjusted_heat_demand_change * ( - rec["heat_demand"] / predicted_heat_demand_change - ) - # We make sure this is NOT below 0 - rec["adjusted_heat_demand"] = max(0, rec["adjusted_heat_demand"]) - - # Depending on the property's tarriff, we calculate the amount of energy savings this measure will bring - if property_instance.energy_source == "electricity": - rec["energy_cost_savings"] = AnnualBillSavings.estimate_electric(rec["adjusted_heat_demand"]) - elif property_instance.energy_source == "electricity_and_gas": - rec["energy_cost_savings"] = AnnualBillSavings.estimate(rec["adjusted_heat_demand"]) - else: - raise ValueError("Invalid value for energy source") + rec["kwh_savings"] = kwh_reduction + rec["energy_cost_savings"] = predicted_bill_savings if (rec["sap_points"] is None) and (rec["co2_equivalent_savings"] is None) or ( rec["heat_demand"] is None) or (rec["energy_cost_savings"] is None):