import pandas as pd from backend.Property import Property from typing import List from itertools import groupby from recommendations.FloorRecommendations import FloorRecommendations from recommendations.WallRecommendations import WallRecommendations from recommendations.RoofRecommendations import RoofRecommendations from recommendations.VentilationRecommendations import VentilationRecommendations from recommendations.FireplaceRecommendations import FireplaceRecommendations from recommendations.LightingRecommendations import LightingRecommendations from recommendations.SolarPvRecommendations import SolarPvRecommendations from recommendations.WindowsRecommendations import WindowsRecommendations from recommendations.HeatingRecommender import HeatingRecommender from recommendations.HotwaterRecommendations import HotwaterRecommendations from recommendations.SecondaryHeating import SecondaryHeating from backend.ml_models.AnnualBillSavings import AnnualBillSavings from backend.apis.GoogleSolarApi import GoogleSolarApi class Recommendations: """ High level recommendations class, which sits above the measure specific recommendation classes """ def __init__( self, property_instance: Property, materials: List, exclusions: List[str] = None, ): """ :param property_instance: Instance of the Property class, for the home associated to property_id :param materials: List of materials to be used in the recommendations """ self.property_instance = property_instance self.materials = materials self.exclusions = exclusions if exclusions else [] self.floor_recommender = FloorRecommendations(property_instance=property_instance, materials=materials) self.wall_recomender = WallRecommendations(property_instance=property_instance, materials=materials) self.roof_recommender = RoofRecommendations(property_instance=property_instance, materials=materials) self.ventilation_recomender = VentilationRecommendations( property_instance=property_instance, materials=materials ) self.fireplace_recommender = FireplaceRecommendations(property_instance=property_instance) self.lighting_recommender = LightingRecommendations(property_instance=property_instance, materials=materials) self.windows_recommender = WindowsRecommendations(property_instance=property_instance, materials=materials) self.solar_recommender = SolarPvRecommendations(property_instance=property_instance) self.heating_recommender = HeatingRecommender(property_instance=property_instance) self.hotwater_recommender = HotwaterRecommendations(property_instance=property_instance) self.secondary_heating_recommender = SecondaryHeating(property_instance=property_instance) def recommend(self): """ This method runs the recommendations for the individual measures and then appends them to a list for output The recommendations are implemented in order of suggested phase, from fabric first to heating systems, to renewables. :return: """ property_recommendations = [] phase = 0 # Building Fabric if "wall_insulation" not in self.exclusions: self.wall_recomender.recommend(phase=phase) if self.wall_recomender.recommendations: property_recommendations.append(self.wall_recomender.recommendations) phase += 1 if "roof_insulation" not in self.exclusions: self.roof_recommender.recommend(phase=phase) if self.roof_recommender.recommendations: property_recommendations.append(self.roof_recommender.recommendations) phase += 1 # Ventilation recommendations # We only produce a ventilation recommendation if the property is recommended to have wall or roof # insulation # We will not attribute a SAP impact to the ventilation recommendation, since we've seen that this # has no # real impact on the SAP score. Therefore, we don't need to include phasing for ventilation. If we # have any # wall or roof recommendations, we will ensure that ventilation is included in the simulation if "ventilation" not in self.exclusions: if self.wall_recomender.recommendations or self.roof_recommender.recommendations: self.ventilation_recomender.recommend() if self.ventilation_recomender.recommendation: property_recommendations.append(self.ventilation_recomender.recommendation) if "floor_insulation" not in self.exclusions: self.floor_recommender.recommend(phase=phase) if self.floor_recommender.recommendations: property_recommendations.append(self.floor_recommender.recommendations) phase += 1 if "windows" not in self.exclusions: self.windows_recommender.recommend(phase=phase) if self.windows_recommender.recommendation: property_recommendations.append(self.windows_recommender.recommendation) phase += 1 if "fireplace" not in self.exclusions: self.fireplace_recommender.recommend(phase=phase) if self.fireplace_recommender.recommendation: property_recommendations.append(self.fireplace_recommender.recommendation) phase += 1 # Heating and Electical systems if "heating" not in self.exclusions: cavity_or_loft_recommendations = [ r for r in self.wall_recomender.recommendations + self.roof_recommender.recommendations if r["type"] in ["cavity_wall_insulation", "loft_insulation"] ] has_cavity_or_loft_recommendations = len(cavity_or_loft_recommendations) > 0 self.heating_recommender.recommend( phase=phase, has_cavity_or_loft_recommendations=has_cavity_or_loft_recommendations, exclusions=self.exclusions ) if ( self.heating_recommender.heating_recommendations or self.heating_recommender.heating_control_recommendations ): # We split into first and second phase recommendations first_phase_recommendations = [ r for r in ( self.heating_recommender.heating_recommendations + self.heating_recommender.heating_control_recommendations ) if r["phase"] == phase ] second_phase_recommendations = [ r for r in ( self.heating_recommender.heating_recommendations + self.heating_recommender.heating_control_recommendations ) if r["phase"] == phase + 1 ] if first_phase_recommendations: property_recommendations.append(first_phase_recommendations) if second_phase_recommendations: property_recommendations.append(second_phase_recommendations) # We check if we have distinct heating and heating controls recommendations # If so, we increment by 2 (one of the heating system, one for the heating controls) # otherwise we incremenet by 1 max_used_phase = max( [rec["phase"] for rec in self.heating_recommender.heating_recommendations + self.heating_recommender.heating_control_recommendations] ) amount_to_increment = max_used_phase - phase + 1 phase += amount_to_increment # Hot water if "hot_water" not in self.exclusions: self.hotwater_recommender.recommend(phase=phase) if self.hotwater_recommender.recommendations: property_recommendations.append(self.hotwater_recommender.recommendations) phase += 1 if "lighting" not in self.exclusions: self.lighting_recommender.recommend(phase=phase) if self.lighting_recommender.recommendation: property_recommendations.append(self.lighting_recommender.recommendation) phase += 1 if "secondary_heating" not in self.exclusions: self.secondary_heating_recommender.recommend(phase=phase) if self.secondary_heating_recommender.recommendation: property_recommendations.append(self.secondary_heating_recommender.recommendation) phase += 1 # Renewables if "solar_pv" not in self.exclusions: self.solar_recommender.recommend(phase=phase) if self.solar_recommender.recommendation: property_recommendations.append(self.solar_recommender.recommendation) phase += 1 # We insert temporary ids into the recommendations which is important for the optimiser later property_recommendations = self.insert_temp_recommendation_id(property_recommendations) # We also need to create the representative recommendations for each recommendation type property_representative_recommendations = self.create_representative_recommendations( property_recommendations, non_invasive_recommendations=self.property_instance.non_invasive_recommendations ) return property_recommendations, property_representative_recommendations @staticmethod def create_representative_recommendations(property_recommendations, non_invasive_recommendations): """ This method will create a representative recommendation for each recommendation type In order to create a representative recommendation, we choose the recommendation that has: 1) Where a U-value is available, has the best U-value to cost ratio 2) Where SAP points are available, has the best SAP points to cost ratio We don't include mechanical ventilation in the representative recommendations, since we don't attribute a SAP impact to this recommendation :return: """ property_representative_recommendations = [] for recommendations_by_type in property_recommendations: # If the property was initially surveyed as filled, but the cavity was only partially filled, we don't # want to include the cavity wall insulation recommendation in the defaults # if (recommendations_by_type[0].get("type") == "cavity_wall_insulation") and ( # "cavity_surveyed_as_filled_is_partial" in non_invasive_recommendations # ): # continue if recommendations_by_type[0].get("type") == "mechanical_ventilation": continue has_u_value = recommendations_by_type[0].get("new_u_value") is not None has_sap_points = recommendations_by_type[0].get("sap_points") is not None has_rank = recommendations_by_type[0].get("rank") is not None # When check if these recommendations have two different types, such as solid wall insulation # If we have multiple types, we group by type and then select the best recommendation for each type recommendations_by_type = sorted(recommendations_by_type, key=lambda x: x["type"]) representative_recommendations = [] for _type, recommendations in groupby(recommendations_by_type, key=lambda x: x["type"]): recommendations = list(recommendations) # We also create an efficiency key, which is used to sort the recommendations if has_u_value: # We sort by the cost per U-value improvement - the lower the better for rec in recommendations: rec["efficiency"] = rec["total"] / rec["starting_u_value"] - rec["new_u_value"] elif not has_u_value and has_sap_points: # Sort the options by the cost per SAP point improvement - the lower the better for rec in recommendations: rec["efficiency"] = rec["total"] / rec["sap_points"] elif has_rank: # Sort the options by rank - the lower the better for rec in recommendations: rec["efficiency"] = rec["rank"] else: # Sort the options by cost - the lower the better for rec in recommendations: rec["efficiency"] = rec["total"] recommendations.sort( key=lambda x: x["efficiency"] ) representative_recommendations.append(recommendations[0]) property_representative_recommendations.extend(representative_recommendations) return property_representative_recommendations @staticmethod def insert_temp_recommendation_id(property_recommendations): """ Creates a temporary recommendation id which is needed for filtering recommendations between default and no, after the optimiser has been run :param property_recommendations: nested list of recommendations, grouped by data_types :return: Updated recommendations_to_upload, where where recommendation has a "recommendation_id" integer inserted """ idx = 0 for recs in property_recommendations: for rec in recs: rec["recommendation_id"] = f"{str(idx)}_phase={str(rec['phase'])}" idx += 1 return property_recommendations @staticmethod def _calculate_appliance_solar_savings( rec, property_instance, heating_kwh_reduction, hot_water_kwh_reduction, lighting_kwh_reduction ): """ Calculates the impact on kwh and cost of installing solar panels on appliances :param rec: The recommendation :param property_instance: Instance of the Property class :param heating_kwh_reduction: The kwh reduction from heating :param hot_water_kwh_reduction: The kwh reduction from hot water :param lighting_kwh_reduction: The kwh reduction from lighting :return: """ if rec["type"] != "solar_pv": return 0, 0 if property_instance.solar_panel_configuration is None: print("PLACEHOLDER ESTIMATES") # 50% reduction average kwh_reduction = property_instance.energy_consumption_estimates["adjusted"]["appliances"] * 0.5 predicted_appliances_cost_reduction = kwh_reduction * AnnualBillSavings.ELECTRICITY_PRICE_CAP return predicted_appliances_cost_reduction, kwh_reduction # Calulate the amount of energy the solar panel array will generate for this unit unit_energy_consumption = ( rec["initial_ac_kwh_per_year"] * property_instance.solar_panel_configuration["unit_share_of_energy"] ) unit_energy_utilised = unit_energy_consumption * GoogleSolarApi.SOLAR_CONSUMPTION_PROPORTION unit_energy_exported = unit_energy_consumption - unit_energy_utilised unit_energy_exported_value = unit_energy_exported * AnnualBillSavings.ELECTRICITY_EXPORT_PAYMENT # We assume that 50% of the energy generated will be used by the property without a battery # to be conservative # of the energy utilised, some of it is used by heating, hot water and lighting so we # remove that from the total unit_energy_utilised -= ( heating_kwh_reduction + hot_water_kwh_reduction + lighting_kwh_reduction ) unit_energy_utilised = 0 if unit_energy_utilised < 0 else unit_energy_utilised # This is how much energy the appliances will use after install post_install_appliance_kwh = ( property_instance.energy_consumption_estimates["adjusted"]["appliances"] - unit_energy_utilised ) post_install_appliance_kwh = ( 0 if post_install_appliance_kwh < 0 else post_install_appliance_kwh ) predicted_appliances_kwh_reduction = ( property_instance.energy_consumption_estimates["adjusted"]["appliances"] - post_install_appliance_kwh ) predicted_appliances_cost_reduction = unit_energy_exported_value + ( predicted_appliances_kwh_reduction * AnnualBillSavings.ELECTRICITY_PRICE_CAP ) return predicted_appliances_cost_reduction, predicted_appliances_kwh_reduction @classmethod def calculate_recommendation_impact( cls, property_instance, all_predictions, recommendations, representative_recommendations, energy_consumption_client ): """ Given predictions from the model apis, with method will update the recommendations with the predicted impact of the recommendation on the property :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 representative_recommendations: dictionary of representative recommendations for the property :param energy_consumption_client: Instance of the EnergyConsumptionClient class :return: """ property_sap_predictions = all_predictions["sap_change_predictions"][ all_predictions["sap_change_predictions"]["property_id"] == str(property_instance.id) ].copy() property_heat_predictions = all_predictions["heat_demand_predictions"][ all_predictions["heat_demand_predictions"]["property_id"] == str(property_instance.id) ].copy() 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() # We calculate the impact by phase 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() ) representative_rec_ids = [ rec["recommendation_id"] for rec in representative_recommendations[property_instance.id] ] phase_lighting_costs = {} phase_kwh_figures = {} bill_savings_list = [] kwh_savings_list = [] for recommendations_by_type in property_recommendations: for rec in recommendations_by_type: if rec["type"] == "mechanical_ventilation": # We don't have a percieved sap impact of mechanical ventilation continue new_heat_demand = property_heat_predictions[property_heat_predictions["recommendation_id"] == str( rec["recommendation_id"] )]["predictions"].values[0] new_carbon = property_carbon_predictions[property_carbon_predictions["recommendation_id"] == str( rec["recommendation_id"] )]["predictions"].values[0] new_sap = property_sap_predictions[property_sap_predictions["recommendation_id"] == str( 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 ) if rec["type"] == "lighting": new_heating_cost = property_instance.energy_cost_estimates["adjusted"]["heating"] new_hot_water_cost = property_instance.energy_cost_estimates["adjusted"]["hot_water"] new_lighting_cost = min( new_lighting_cost, property_instance.energy_cost_estimates["adjusted"]["lighting"] ) scoring_heating_cost = property_instance.energy_cost_estimates["unadjusted"]["heating"] scoring_hot_water_cost = property_instance.energy_cost_estimates["unadjusted"]["hot_water"] scoring_lighting_cost = min( property_instance.energy_cost_estimates["unadjusted"]["lighting"], new_lighting_cost_unadjusted ) else: new_heating_cost = min( new_heating_cost, property_instance.energy_cost_estimates["adjusted"]["heating"] ) new_hot_water_cost = min( new_hot_water_cost, property_instance.energy_cost_estimates["adjusted"]["hot_water"] ) new_lighting_cost = property_instance.energy_cost_estimates["adjusted"]["lighting"] 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 = property_instance.energy_cost_estimates["unadjusted"]["lighting"] predicted_heating_cost_reduction = ( property_instance.energy_cost_estimates["adjusted"]["heating"] - new_heating_cost ) predicted_hot_water_cost_reduction = ( property_instance.energy_cost_estimates["adjusted"]["hot_water"] - new_hot_water_cost ) predicted_lighting_cost_reduction = 0 if rec["type"] != "lighting" else ( property_instance.energy_cost_estimates["adjusted"]["lighting"] - new_lighting_cost ) # We store this value for later phase_lighting_costs[rec["phase"]] = { "adjusted": new_lighting_cost, "unadjusted": scoring_lighting_cost } # We now predict the kwh savings using the xgb model 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 ( predicted_appliances_cost_reduction, predicted_appliances_kwh_reduction ) = cls._calculate_appliance_solar_savings( rec=rec, property_instance=property_instance, heating_kwh_reduction=heating_kwh_reduction, hot_water_kwh_reduction=hot_water_kwh_reduction, lighting_kwh_reduction=lighting_kwh_reduction ) kwh_reduction = ( heating_kwh_reduction + hot_water_kwh_reduction + lighting_kwh_reduction + predicted_appliances_kwh_reduction ) predicted_bill_savings = ( predicted_heating_cost_reduction + predicted_hot_water_cost_reduction + predicted_lighting_cost_reduction + predicted_appliances_cost_reduction ) phase_kwh_figures[rec["phase"]] = { "adjusted": { "heating": new_heating_kwh_adjusted, "hot_water": new_hot_water_kwh_adjusted }, "unadjusted": { "heating": new_heating_kwh, "hot_water": new_hot_water_kwh } } else: previous_phase = rec["phase"] - 1 predicted_sap_points = ( new_sap - sap_phase_impact[sap_phase_impact["phase"] == previous_phase]["predictions"].values[0] ) predicted_co2_savings = ( carbon_phase_impact[carbon_phase_impact["phase"] == previous_phase]["predictions"].values[0] - new_carbon ) predicted_heat_demand = property_instance.floor_area * ( heat_phase_impact[heat_phase_impact["phase"] == previous_phase]["predictions"].values[0] - new_heat_demand ) if rec["type"] == "lighting": # If we have a lighting recommendation, the heating, hot water and lighting costs will # be from the previous phase - nothing will change new_heating_cost = heating_cost_phase_impact[ heating_cost_phase_impact["phase"] == previous_phase ]["adjusted_cost"].values[0] new_hot_water_cost = hot_water_cost_phase_impact[ hot_water_cost_phase_impact["phase"] == previous_phase ]["adjusted_cost"].values[0] new_lighting_cost = min( new_lighting_cost, phase_lighting_costs[previous_phase]["adjusted"] ) # We also use the unadjusted costs for the scoring from the previous phase scoring_heating_cost = heating_cost_phase_impact[ heating_cost_phase_impact["phase"] == previous_phase ]["predictions"].values[0] scoring_hot_water_cost = hot_water_cost_phase_impact[ hot_water_cost_phase_impact["phase"] == previous_phase ]["predictions"].values[0] scoring_lighting_cost = min( new_lighting_cost_unadjusted, phase_lighting_costs[previous_phase]["unadjusted"] ) else: # Whereas for other recommendations, we use the new costs new_heating_cost = min( new_heating_cost, heating_cost_phase_impact[ heating_cost_phase_impact["phase"] == previous_phase ]["adjusted_cost"].values[0] ) new_hot_water_cost = min( new_hot_water_cost, hot_water_cost_phase_impact[ hot_water_cost_phase_impact["phase"] == previous_phase ]["adjusted_cost"].values[0] ) new_lighting_cost = phase_lighting_costs[previous_phase]["adjusted"] scoring_heating_cost = min( new_heating_cost_unadjusted, heating_cost_phase_impact[ heating_cost_phase_impact["phase"] == previous_phase ]["predictions"].values[0] ) scoring_hot_water_cost = min( new_hot_water_cost_unadjusted, hot_water_cost_phase_impact[ hot_water_cost_phase_impact["phase"] == previous_phase ]["predictions"].values[0] ) scoring_lighting_cost = phase_lighting_costs[previous_phase]["unadjusted"] # We now estimate the adjusted cost savings for the recommendation predicted_heating_cost_reduction = ( heating_cost_phase_impact[heating_cost_phase_impact["phase"] == previous_phase][ "adjusted_cost" ].values[0] - new_heating_cost ) predicted_hot_water_cost_reduction = ( hot_water_cost_phase_impact[hot_water_cost_phase_impact["phase"] == previous_phase][ "adjusted_cost" ].values[0] - new_hot_water_cost ) # Only lighting recommendations can have an impact here predicted_lighting_cost_reduction = ( phase_lighting_costs[previous_phase]["adjusted"] - new_lighting_cost ) # We now predict the kwh savings using the xgb model - this is based on # the new costs at this phase 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 ( phase_kwh_figures[previous_phase]["adjusted"]["heating"] - new_heating_kwh_adjusted ) if heating_kwh_reduction < 0: heating_kwh_reduction = 0 hot_water_kwh_reduction = 0 if predicted_hot_water_cost_reduction == 0 else ( phase_kwh_figures[previous_phase]["adjusted"]["hot_water"] - new_hot_water_kwh_adjusted ) if hot_water_kwh_reduction < 0: hot_water_kwh_reduction = 0 lighting_kwh_reduction = predicted_lighting_cost_reduction / AnnualBillSavings.ELECTRICITY_PRICE_CAP ( predicted_appliances_cost_reduction, predicted_appliances_kwh_reduction ) = cls._calculate_appliance_solar_savings( rec=rec, property_instance=property_instance, heating_kwh_reduction=heating_kwh_reduction, hot_water_kwh_reduction=hot_water_kwh_reduction, lighting_kwh_reduction=lighting_kwh_reduction ) # We now calculate the predicted_bill_savings predicted_bill_savings = ( predicted_heating_cost_reduction + predicted_hot_water_cost_reduction + predicted_lighting_cost_reduction + predicted_appliances_cost_reduction ) kwh_reduction = ( heating_kwh_reduction + hot_water_kwh_reduction + lighting_kwh_reduction + predicted_appliances_kwh_reduction ) # We store this value for later phase_lighting_costs[rec["phase"]] = { "adjusted": new_lighting_cost, "unadjusted": scoring_lighting_cost } phase_kwh_figures[rec["phase"]] = { "adjusted": { "heating": new_heating_kwh_adjusted, "hot_water": new_hot_water_kwh_adjusted }, "unadjusted": { "heating": new_heating_kwh, "hot_water": new_hot_water_kwh } } # Prevent from being negative predicted_sap_points = 0 if predicted_sap_points < 0 else predicted_sap_points predicted_co2_savings = 0 if predicted_co2_savings < 0 else predicted_co2_savings predicted_heat_demand = 0 if predicted_heat_demand < 0 else predicted_heat_demand if rec["type"] == "low_energy_lighting": # For the moment, we cap the number of SAP points that can be achieved by ventilation at 2 rec["sap_points"] = min(predicted_sap_points, LightingRecommendations.SAP_LIMIT) rec["co2_equivalent_savings"] = min(predicted_co2_savings, rec["co2_equivalent_savings"]) rec["heat_demand"] = predicted_heat_demand else: rec["sap_points"] = predicted_sap_points rec["co2_equivalent_savings"] = predicted_co2_savings rec["heat_demand"] = predicted_heat_demand # Round to 2 decimal places rec["sap_points"] = round(rec["sap_points"], 2) rec["kwh_savings"] = kwh_reduction rec["energy_cost_savings"] = predicted_bill_savings if rec["recommendation_id"] in representative_rec_ids: bill_savings_list.append(predicted_bill_savings) kwh_savings_list.append(kwh_reduction) 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): raise ValueError("sap points, co2 or heat demand is missing") # We sum up the total savings for the property and that is our expected energy bill expected_energy_bill = property_instance.current_energy_bill - sum(bill_savings_list) expected_adjusted_energy = property_instance.current_adjusted_energy - sum(kwh_savings_list) return ( property_recommendations, expected_adjusted_energy, expected_energy_bill )