diff --git a/backend/Property.py b/backend/Property.py index 600e9b03..25068f6c 100644 --- a/backend/Property.py +++ b/backend/Property.py @@ -1025,7 +1025,7 @@ class Property: built_form=self.data["built-form"], ) - if self.insulation_floor_area is not None: + if self.insulation_floor_area is None: self.insulation_floor_area = float( self.energy_assessment_condition_data["main_dwelling_ground_floor_area"] ) if (condition_data.get("main_dwelling_ground_floor_area") is not None) else ( diff --git a/backend/app/plan/router.py b/backend/app/plan/router.py index b4d5c774..fb4ffa14 100644 --- a/backend/app/plan/router.py +++ b/backend/app/plan/router.py @@ -438,7 +438,120 @@ async def trigger_plan(body: PlanTriggerRequest): # prepare the data - # TODO: Some junk is being returned by the heating kwh model! + # TODO - this needs to be moved to the etl process + import numpy as np + def add_features_from_code(df): + + FEATURES = { + "heating_kwh": [ + "lodgement-year", "lodgement-month", "current-energy-efficiency", "energy-consumption-current", + "heating-cost-current", "heating-cost-potential", "total-floor-area", "number-heated-rooms", + "mainheat-description", "mainheat-energy-eff", "main-fuel", "secondheat-description", + "property-type", + "built-form", "mainheatcont-description", "hotwater-description", "hot-water-energy-eff", + "walls-energy-eff", + "roof-energy-eff", "windows-description", "windows-energy-eff", "floor-description", + "flat-top-storey", + "flat-storey-count", "unheated-corridor-length", "solar-water-heating-flag", + "mechanical-ventilation", + "low-energy-lighting", "environment-impact-current", "energy-tariff", + "county", "construction-age-band", "co2-emissions-current", + ], + "hot_water_kwh": [ + "lodgement-year", "lodgement-month", + "current-energy-efficiency", + "energy-consumption-current", + "hot-water-cost-current", + "total-floor-area", "number-heated-rooms", + "hotwater-description", "hot-water-energy-eff", "main-fuel", "property-type", "built-form", + "co2-emissions-current", + ] + } + CATEGORICAL_COLUMNS = [ + "lodgement-year", "lodgement-month", "main-fuel", "mainheat-description", "number-heated-rooms", + "number-habitable-rooms", "mainheat-energy-eff", "mainheatcont-description", "property-type", + "built-form", + "construction-age-band", "secondheat-description", "hotwater-description", "hot-water-energy-eff", + "walls-description", "walls-energy-eff", "roof-description", "roof-energy-eff", "floor-description", + "county", + "windows-description", "windows-energy-eff", "flat-top-storey", + "flat-storey-count", "unheated-corridor-length", "solar-water-heating-flag", "mechanical-ventilation", + "low-energy-lighting", "environment-impact-current", "energy-tariff", "current-energy-rating" + ] + + NUMERICAL_COLUMNS = list({ + x for x in FEATURES["heating_kwh"] + FEATURES["hot_water_kwh"] + if x not in CATEGORICAL_COLUMNS + }) + + """Performs feature engineering on the dataset.""" + df["lodgement-date"] = pd.to_datetime(df["lodgement-date"]) + df["lodgement-year"] = df["lodgement-date"].dt.year + df["lodgement-month"] = df["lodgement-date"].dt.month + + # For walls, roof, floor description where we have average thermal transmittance, to avoid too many + # categories + # we group them + ranges = { + "lessthan 0.1": (0, 0.1), + "0.1 - 0.3": (0.1, 0.3), + "0.3 - 0.5": (0.3, 0.5), + "morethan 0.5": (0.5, 2.5), + } + + # Generate the lookup table + thermal_transmittance_lookup_table = [] + for i in range(1, 251): + value = i / 100 + for label, (low, high) in ranges.items(): + if low < value <= high: + thermal_transmittance_lookup_table.append({"from": value, "to": label}) + break + + # Convert to DataFrame for display + thermal_transmittance_lookup_table = pd.DataFrame(thermal_transmittance_lookup_table) + thermal_transmittance_lookup_table["from"] = thermal_transmittance_lookup_table["from"].astype(str) + + # Apply the lookup table to the data + for feature in ["walls-description", "roof-description", "floor-description"]: + cleaned_df = pd.DataFrame(cleaned[feature])[["original_description", "thermal_transmittance"]] + # Round to 2 decimal places and convert to string + cleaned_df["thermal_transmittance"] = cleaned_df["thermal_transmittance"].round(2).astype(str) + + df = df.merge( + cleaned_df, + how="left", + left_on=feature, + right_on="original_description", + ) + # We now have the thermal transmittance in the data, which we can use to group with the lookup table + df = df.merge( + thermal_transmittance_lookup_table, + how="left", + left_on="thermal_transmittance", + right_on="from", + ) + # Where "to" is populated, replace feature with to + df[feature] = np.where( + ~pd.isnull(df["to"]), + df["to"], + df[feature] + ) + df = df.drop(columns=["original_description", "thermal_transmittance", "from", "to"]) + + # Convert data types + df[NUMERICAL_COLUMNS] = df[NUMERICAL_COLUMNS].apply(pd.to_numeric) + df[CATEGORICAL_COLUMNS] = df[CATEGORICAL_COLUMNS].astype(str) + + return df + + def add_estimate_annual_kwh(df): + df['estimate_annual_kwh'] = df['energy-consumption-current'] * df['total-floor-area'] + return df + + epcs_for_scoring = add_features_from_code(epcs_for_scoring) + epcs_for_scoring = add_estimate_annual_kwh(epcs_for_scoring) + kwh_predictions = model_api.predict_all( df=epcs_for_scoring, bucket=get_settings().DATA_BUCKET, @@ -476,7 +589,7 @@ async def trigger_plan(body: PlanTriggerRequest): raise Exception("Missed setting of spatial data for a property") p.get_components( cleaned=cleaned, - # energy_consumption_client=energy_consumption_client # TODO: Full remove me + energy_consumption_client=energy_consumption_client, # TODO: Full remove me kwh_predictions=kwh_predictions ) @@ -676,6 +789,12 @@ async def trigger_plan(body: PlanTriggerRequest): for key, scored in predictions_dict.items(): all_predictions[key] = pd.concat([all_predictions[key], scored]) + # We now produce predictions for the kwh models + + # TODO!!!!! In order to score the kwh models, we need to insert the new SAP, heat demand, carbon, cost + # etc values, into the simulated EPC, otherwise it won't work. We might also want to drop all potential + # columns and env-efficiency columns (POTENTIAL COLUMNS ALREADY GONE, JUST NEED TO DROP ENV EFFICIENCY) + # Insert the predictions into the recommendations and run the optimiser # TODO: If a recommendation has a negative impact on SAP, we should remove it - this seems to have become a # possibility with heating system @@ -686,26 +805,14 @@ async def trigger_plan(body: PlanTriggerRequest): property_instance = [p for p in input_properties if p.id == property_id][0] - ( - recommendations_with_impact, - expected_adjusted_energy, - expected_energy_bill - ) = ( + recommendations_with_impact, impact_summary = ( Recommendations.calculate_recommendation_impact( property_instance=property_instance, all_predictions=all_predictions, recommendations=recommendations, - representative_recommendations=representative_recommendations, - energy_consumption_client=energy_consumption_client ) ) - # Store the resulting adjusted energy in the property instance - property_instance.set_adjusted_energy( - expected_adjusted_energy=expected_adjusted_energy, - expected_energy_bill=expected_energy_bill - ) - input_measures = prepare_input_measures(recommendations_with_impact, body.goal) current_sap_points = int(property_instance.data["current-energy-efficiency"]) diff --git a/backend/ml_models/api.py b/backend/ml_models/api.py index e4a0715f..0de7977f 100644 --- a/backend/ml_models/api.py +++ b/backend/ml_models/api.py @@ -15,8 +15,6 @@ class ModelApi: "lighting_cost_predictions", "heating_cost_predictions", "hot_water_cost_predictions", - "hotwater_kwh_predictions", - "heating_kwh_predictions", ] MODEL_URLS = { @@ -72,8 +70,8 @@ class ModelApi: :return: """ - if model_prefix not in self.MODEL_PREFIXES: - raise ValueError(f"Model prefix specified is not in {self.MODEL_PREFIXES}") + # if model_prefix not in self.MODEL_PREFIXES: + # raise ValueError(f"Model prefix specified is not in {self.MODEL_PREFIXES}") # Store parquet file in s3 for scoring file_location = f"{model_prefix}/{self.portfolio_id}/{self.timestamp}.parquet" diff --git a/recommendations/Recommendations.py b/recommendations/Recommendations.py index 1c12d5eb..0de8931a 100644 --- a/recommendations/Recommendations.py +++ b/recommendations/Recommendations.py @@ -359,477 +359,210 @@ class Recommendations: 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 + This function will return two objects: + 1) Updated recommendations with the predicted impact of the recommendation + 2) A list of impacts by phase, which will be used for the kwh model scoring + :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() + property_predictions = { + prefix + "_predictions": all_predictions[prefix + "_predictions"][ + all_predictions[prefix + "_predictions"]["property_id"] == str(property_instance.id) + ].copy() for prefix in [ + "sap_change", "heat_demand", "carbon_change", "lighting_cost", "heating_cost", "hot_water_cost" + ] + } # 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"] + for prefix in ["lighting_cost", "heating_cost", "hot_water_cost"]: + property_predictions[f"{prefix}_predictions"]["adjusted_cost"] = ( + property_predictions[f"{prefix}_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() - ) + phase_impact = { + prefix: property_predictions[prefix + "_predictions"].groupby("phase")["predictions"].median().reset_index() + for prefix in [ + "sap_change", "heat_demand", "carbon_change", "lighting_cost", "heating_cost", "hot_water_cost" + ] + } - representative_rec_ids = [ - rec["recommendation_id"] for rec in representative_recommendations[property_instance.id] - ] + # TODO: should fabric upgrades have an impact on hot water costs/kwh? + # TODO: Generally, the costing models are just increasing. Maybe they're including something in the model + # that they shouldn't e.g. SAP, carbon, heat demand etc? - phase_lighting_costs = {} - phase_kwh_figures = {} - bill_savings_list = [] - kwh_savings_list = [] + impact_summary = [] 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] + phase_energy_efficiency_metrics = { + prefix: property_predictions[prefix + "_predictions"][ + property_predictions[prefix + "_predictions"]["recommendation_id"] == str( + rec["recommendation_id"] + )]["predictions"].values[0] for prefix in ["sap_change", "heat_demand", "carbon_change"] + } - 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] + # For phase costs, we need adusted and unadjusted values + phase_cost = { + prefix: property_predictions[prefix + "_predictions"][ + property_predictions[prefix + "_predictions"]["recommendation_id"] == + str(rec["recommendation_id"]) + ] for prefix in ["lighting_cost", "heating_cost", "hot_water_cost"] + } + # We structure this so that depending on the phase, we capture the previous phase impacts and + # then just have one piece of code to calculate the difference 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 - ) + previous_phase_values = { + "sap": float(property_instance.data["current-energy-efficiency"]), + "carbon": float(property_instance.data["co2-emissions-current"]), + "heat_demand": float(property_instance.data["energy-consumption-current"]), + } if rec["type"] == "low_energy_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_heating_kwh = 0 if new_heating_kwh < 0 else new_heating_kwh - - new_hot_water_kwh = energy_consumption_client.score_new_data( - new_data=scoring_df, target="hot_water_kwh" - )[0] - new_hot_water_kwh = 0 if new_hot_water_kwh < 0 else new_hot_water_kwh - - # 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 + # In this instance, heating cost and hot water cost should not change so we set the previous + # value to the new one, so the difference is zero + previous_phase_unadjusted_costs = { + "unadjusted_heating_cost": phase_cost["heating_cost"]["predictions"].values[0], + "unadjusted_hot_water_cost": phase_cost["hot_water_cost"]["predictions"].values[0], + "unadjusted_lighting_cost": ( + property_instance.energy_cost_estimates["unadjusted"]["lighting"] + ) + } + else: + # If the recommendaiton is not for low energy lighting, we expect the heating/hot water + # costs to change but not te lighting + previous_phase_unadjusted_costs = { + "unadjusted_heating_cost": property_instance.energy_cost_estimates["adjusted"]["heating"], + "unadjusted_hot_water_cost": ( + property_instance.energy_cost_estimates["adjusted"]["hot_water"] + ), + "unadjusted_lighting_cost": phase_cost["lighting_cost"]["predictions"].values[0] } - } - 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 + previous_phase_values = { + "sap": ( + phase_impact["sap_change"][phase_impact["sap_change"]["phase"] == (rec["phase"] - 1)] + ["predictions"].values[0] + ), + "carbon": ( + phase_impact["carbon_change"][phase_impact["carbon_change"]["phase"] == (rec["phase"] - 1)] + ["predictions"].values[0] + ), + "heat_demand": ( + phase_impact["heat_demand"][phase_impact["heat_demand"]["phase"] == (rec["phase"] - 1)] + ["predictions"].values[0] + ), } - 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 + if rec["type"] == "low_energy_lighting": + # Heating and hot water costs shouldn't change + # {'unadjusted_heating_cost': 501.8528134938132, 'unadjusted_hot_water_cost': + # 171.22534405283452, 'unadjusted_lighting_cost': 127.2} + previous_phase_unadjusted_costs = { + "unadjusted_heating_cost": phase_cost["heating_cost"]["predictions"].values[0], + "unadjusted_hot_water_cost": phase_cost["hot_water_cost"]["predictions"].values[0], + "unadjusted_lighting_cost": phase_impact["lighting_cost"][ + phase_impact["lighting_cost"]["phase"] == (rec["phase"] - 1) + ]["predictions"].values[0] } - } + else: + # update heating and hot water costs + previous_phase_unadjusted_costs = { + "unadjusted_heating_cost": phase_impact["heating_cost"][ + phase_impact["heating_cost"]["phase"] == (rec["phase"] - 1) + ]["predictions"].values[0], + "unadjusted_hot_water_cost": phase_impact["hot_water_cost"][ + phase_impact["hot_water_cost"]["phase"] == (rec["phase"] - 1) + ]["predictions"].values[0], + "unadjusted_lighting_cost": phase_cost["lighting_cost"]["predictions"].values[0] + } + + previous_phase_values.update(previous_phase_unadjusted_costs) + + # We extract the values for the current phase + current_phase_values = { + "sap": phase_energy_efficiency_metrics["sap_change"], + "carbon": phase_energy_efficiency_metrics["carbon_change"], + "heat_demand": phase_energy_efficiency_metrics["heat_demand"], + "unadjusted_heating_cost": phase_cost["heating_cost"]["predictions"].values[0], + "unadjusted_hot_water_cost": phase_cost["hot_water_cost"]["predictions"].values[0], + "unadjusted_lighting_cost": phase_cost["lighting_cost"]["predictions"].values[0] + } + + property_phase_impact = { + # Increasing + "sap": current_phase_values["sap"] - previous_phase_values["sap"], + # Decreasing + "carbon": previous_phase_values["carbon"] - current_phase_values["carbon"], + # Decreasing + "heat_demand": previous_phase_values["heat_demand"] - current_phase_values["heat_demand"], + # Decreasing + "unadjusted_heating_cost": ( + previous_phase_values["unadjusted_heating_cost"] - + current_phase_values["unadjusted_heating_cost"] + ), + # Decreasing + "unadjusted_hot_water_cost": ( + previous_phase_values["unadjusted_hot_water_cost"] - + current_phase_values["unadjusted_hot_water_cost"] + ), + # Decreasing + "unadjusted_lighting_cost": ( + previous_phase_values["unadjusted_lighting_cost"] - + current_phase_values["unadjusted_lighting_cost"] + ) + } # 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 + for metric in ["sap", "carbon", "heat_demand"]: + property_phase_impact[metric] = ( + 0 if property_phase_impact[metric] < 0 else property_phase_impact[metric] + ) + if metric == "sap": + property_phase_impact[metric] = round(property_phase_impact[metric], 2) + # For the moment, we cap the number of SAP points that can be achieved by LEDs at 2 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 + property_phase_impact["sap"] = min(property_phase_impact["sap"], LightingRecommendations.SAP_LIMIT) + property_phase_impact["carbon"] = min( + property_phase_impact["carbon"], rec["co2_equivalent_savings"] + ) - # 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) + # Insert this information into the recommendation + rec["sap_points"] = property_phase_impact["sap"] + rec["co2_equivalent_savings"] = property_phase_impact["carbon"] + rec["heat_demand"] = property_phase_impact["heat_demand"] 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): + rec["heat_demand"] 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 + impact_summary.append( + { + "phase": rec["phase"], + "recommendation_id": rec["recommendation_id"], + **current_phase_values + } + ) - 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 - ) + return property_recommendations, impact_summary