refactoring calculate_recommendation_impact

This commit is contained in:
Khalim Conn-Kowlessar 2024-08-07 15:04:19 +01:00
parent 891545804e
commit db3ab9bb4a
4 changed files with 285 additions and 447 deletions

View file

@ -1025,7 +1025,7 @@ class Property:
built_form=self.data["built-form"], 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.insulation_floor_area = float(
self.energy_assessment_condition_data["main_dwelling_ground_floor_area"] self.energy_assessment_condition_data["main_dwelling_ground_floor_area"]
) if (condition_data.get("main_dwelling_ground_floor_area") is not None) else ( ) if (condition_data.get("main_dwelling_ground_floor_area") is not None) else (

View file

@ -438,7 +438,120 @@ async def trigger_plan(body: PlanTriggerRequest):
# prepare the data # 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( kwh_predictions = model_api.predict_all(
df=epcs_for_scoring, df=epcs_for_scoring,
bucket=get_settings().DATA_BUCKET, 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") raise Exception("Missed setting of spatial data for a property")
p.get_components( p.get_components(
cleaned=cleaned, 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 kwh_predictions=kwh_predictions
) )
@ -676,6 +789,12 @@ async def trigger_plan(body: PlanTriggerRequest):
for key, scored in predictions_dict.items(): for key, scored in predictions_dict.items():
all_predictions[key] = pd.concat([all_predictions[key], scored]) 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 # 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 # TODO: If a recommendation has a negative impact on SAP, we should remove it - this seems to have become a
# possibility with heating system # 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] property_instance = [p for p in input_properties if p.id == property_id][0]
( recommendations_with_impact, impact_summary = (
recommendations_with_impact,
expected_adjusted_energy,
expected_energy_bill
) = (
Recommendations.calculate_recommendation_impact( Recommendations.calculate_recommendation_impact(
property_instance=property_instance, property_instance=property_instance,
all_predictions=all_predictions, all_predictions=all_predictions,
recommendations=recommendations, 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) input_measures = prepare_input_measures(recommendations_with_impact, body.goal)
current_sap_points = int(property_instance.data["current-energy-efficiency"]) current_sap_points = int(property_instance.data["current-energy-efficiency"])

View file

@ -15,8 +15,6 @@ class ModelApi:
"lighting_cost_predictions", "lighting_cost_predictions",
"heating_cost_predictions", "heating_cost_predictions",
"hot_water_cost_predictions", "hot_water_cost_predictions",
"hotwater_kwh_predictions",
"heating_kwh_predictions",
] ]
MODEL_URLS = { MODEL_URLS = {
@ -72,8 +70,8 @@ class ModelApi:
:return: :return:
""" """
if model_prefix 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}") # raise ValueError(f"Model prefix specified is not in {self.MODEL_PREFIXES}")
# Store parquet file in s3 for scoring # Store parquet file in s3 for scoring
file_location = f"{model_prefix}/{self.portfolio_id}/{self.timestamp}.parquet" file_location = f"{model_prefix}/{self.portfolio_id}/{self.timestamp}.parquet"

View file

@ -359,477 +359,210 @@ class Recommendations:
property_instance, property_instance,
all_predictions, all_predictions,
recommendations, recommendations,
representative_recommendations,
energy_consumption_client
): ):
""" """
Given predictions from the model apis, with method will update the recommendations with the predicted Given predictions from the model apis, with method will update the recommendations with the predicted
impact of the recommendation on the property 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 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 all_predictions: dictionary of predictions from the model apis
:param recommendations: dictionary of recommendations for the property :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: :return:
""" """
property_sap_predictions = all_predictions["sap_change_predictions"][ property_predictions = {
all_predictions["sap_change_predictions"]["property_id"] == str(property_instance.id) prefix + "_predictions": all_predictions[prefix + "_predictions"][
].copy() all_predictions[prefix + "_predictions"]["property_id"] == str(property_instance.id)
property_heat_predictions = all_predictions["heat_demand_predictions"][ ].copy() for prefix in [
all_predictions["heat_demand_predictions"]["property_id"] == str(property_instance.id) "sap_change", "heat_demand", "carbon_change", "lighting_cost", "heating_cost", "hot_water_cost"
].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 # We apply adjustments to each of the heating costs
property_lighting_cost_predictions["adjusted_cost"] = property_lighting_cost_predictions["predictions"].apply( for prefix in ["lighting_cost", "heating_cost", "hot_water_cost"]:
lambda x: AnnualBillSavings.adjust_energy_to_metered( property_predictions[f"{prefix}_predictions"]["adjusted_cost"] = (
x, current_epc_rating=property_instance.data["current-energy-rating"] 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() property_recommendations = recommendations[property_instance.id].copy()
# We calculate the impact by phase # We calculate the impact by phase
sap_phase_impact = property_sap_predictions.groupby("phase")["predictions"].median().reset_index() phase_impact = {
heat_phase_impact = property_heat_predictions.groupby("phase")["predictions"].median().reset_index() prefix: property_predictions[prefix + "_predictions"].groupby("phase")["predictions"].median().reset_index()
carbon_phase_impact = property_carbon_predictions.groupby("phase")["predictions"].median().reset_index() for prefix in [
# lighting_cost_phase_impact = ( "sap_change", "heat_demand", "carbon_change", "lighting_cost", "heating_cost", "hot_water_cost"
# 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 = [ # TODO: should fabric upgrades have an impact on hot water costs/kwh?
rec["recommendation_id"] for rec in representative_recommendations[property_instance.id] # 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 = {} impact_summary = []
phase_kwh_figures = {}
bill_savings_list = []
kwh_savings_list = []
for recommendations_by_type in property_recommendations: for recommendations_by_type in property_recommendations:
for rec in recommendations_by_type: for rec in recommendations_by_type:
if rec["type"] == "mechanical_ventilation": if rec["type"] == "mechanical_ventilation":
# We don't have a percieved sap impact of mechanical ventilation # We don't have a percieved sap impact of mechanical ventilation
continue continue
new_heat_demand = property_heat_predictions[property_heat_predictions["recommendation_id"] == str( phase_energy_efficiency_metrics = {
rec["recommendation_id"] prefix: property_predictions[prefix + "_predictions"][
)]["predictions"].values[0] 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( # For phase costs, we need adusted and unadjusted values
rec["recommendation_id"] phase_cost = {
)]["predictions"].values[0] prefix: property_predictions[prefix + "_predictions"][
property_predictions[prefix + "_predictions"]["recommendation_id"] ==
new_sap = property_sap_predictions[property_sap_predictions["recommendation_id"] == str( str(rec["recommendation_id"])
rec["recommendation_id"] ] for prefix in ["lighting_cost", "heating_cost", "hot_water_cost"]
)]["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]
# 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: if rec["phase"] == 0:
predicted_sap_points = new_sap - float(property_instance.data["current-energy-efficiency"]) previous_phase_values = {
predicted_co2_savings = float(property_instance.data["co2-emissions-current"]) - new_carbon "sap": float(property_instance.data["current-energy-efficiency"]),
predicted_heat_demand = property_instance.floor_area * ( "carbon": float(property_instance.data["co2-emissions-current"]),
float(property_instance.data["energy-consumption-current"]) - new_heat_demand "heat_demand": float(property_instance.data["energy-consumption-current"]),
) }
if rec["type"] == "low_energy_lighting": if rec["type"] == "low_energy_lighting":
new_heating_cost = property_instance.energy_cost_estimates["adjusted"]["heating"] # In this instance, heating cost and hot water cost should not change so we set the previous
new_hot_water_cost = property_instance.energy_cost_estimates["adjusted"]["hot_water"] # value to the new one, so the difference is zero
new_lighting_cost = min( previous_phase_unadjusted_costs = {
new_lighting_cost, property_instance.energy_cost_estimates["adjusted"]["lighting"] "unadjusted_heating_cost": phase_cost["heating_cost"]["predictions"].values[0],
) "unadjusted_hot_water_cost": phase_cost["hot_water_cost"]["predictions"].values[0],
scoring_heating_cost = property_instance.energy_cost_estimates["unadjusted"]["heating"] "unadjusted_lighting_cost": (
scoring_hot_water_cost = property_instance.energy_cost_estimates["unadjusted"]["hot_water"] property_instance.energy_cost_estimates["unadjusted"]["lighting"]
scoring_lighting_cost = min( )
property_instance.energy_cost_estimates["unadjusted"]["lighting"], }
new_lighting_cost_unadjusted else:
) # If the recommendaiton is not for low energy lighting, we expect the heating/hot water
else: # costs to change but not te lighting
new_heating_cost = min( previous_phase_unadjusted_costs = {
new_heating_cost, property_instance.energy_cost_estimates["adjusted"]["heating"] "unadjusted_heating_cost": property_instance.energy_cost_estimates["adjusted"]["heating"],
) "unadjusted_hot_water_cost": (
new_hot_water_cost = min( property_instance.energy_cost_estimates["adjusted"]["hot_water"]
new_hot_water_cost, property_instance.energy_cost_estimates["adjusted"]["hot_water"] ),
) "unadjusted_lighting_cost": phase_cost["lighting_cost"]["predictions"].values[0]
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
} }
}
else: else:
previous_phase = rec["phase"] - 1 previous_phase_values = {
predicted_sap_points = ( "sap": (
new_sap - sap_phase_impact[sap_phase_impact["phase"] == previous_phase]["predictions"].values[0] phase_impact["sap_change"][phase_impact["sap_change"]["phase"] == (rec["phase"] - 1)]
) ["predictions"].values[0]
predicted_co2_savings = ( ),
carbon_phase_impact[carbon_phase_impact["phase"] == previous_phase]["predictions"].values[0] - "carbon": (
new_carbon phase_impact["carbon_change"][phase_impact["carbon_change"]["phase"] == (rec["phase"] - 1)]
) ["predictions"].values[0]
predicted_heat_demand = property_instance.floor_area * ( ),
heat_phase_impact[heat_phase_impact["phase"] == previous_phase]["predictions"].values[0] - "heat_demand": (
new_heat_demand phase_impact["heat_demand"][phase_impact["heat_demand"]["phase"] == (rec["phase"] - 1)]
) ["predictions"].values[0]
),
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"]] = { if rec["type"] == "low_energy_lighting":
"adjusted": { # Heating and hot water costs shouldn't change
"heating": new_heating_kwh_adjusted, # {'unadjusted_heating_cost': 501.8528134938132, 'unadjusted_hot_water_cost':
"hot_water": new_hot_water_kwh_adjusted # 171.22534405283452, 'unadjusted_lighting_cost': 127.2}
}, previous_phase_unadjusted_costs = {
"unadjusted": { "unadjusted_heating_cost": phase_cost["heating_cost"]["predictions"].values[0],
"heating": new_heating_kwh, "unadjusted_hot_water_cost": phase_cost["hot_water_cost"]["predictions"].values[0],
"hot_water": new_hot_water_kwh "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 # Prevent from being negative
predicted_sap_points = 0 if predicted_sap_points < 0 else predicted_sap_points for metric in ["sap", "carbon", "heat_demand"]:
predicted_co2_savings = 0 if predicted_co2_savings < 0 else predicted_co2_savings property_phase_impact[metric] = (
predicted_heat_demand = 0 if predicted_heat_demand < 0 else predicted_heat_demand 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": if rec["type"] == "low_energy_lighting":
# For the moment, we cap the number of SAP points that can be achieved by ventilation at 2 property_phase_impact["sap"] = min(property_phase_impact["sap"], LightingRecommendations.SAP_LIMIT)
rec["sap_points"] = min(predicted_sap_points, LightingRecommendations.SAP_LIMIT) property_phase_impact["carbon"] = min(
rec["co2_equivalent_savings"] = min(predicted_co2_savings, rec["co2_equivalent_savings"]) property_phase_impact["carbon"], 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 # Insert this information into the recommendation
rec["sap_points"] = round(rec["sap_points"], 2) rec["sap_points"] = property_phase_impact["sap"]
rec["co2_equivalent_savings"] = property_phase_impact["carbon"]
rec["kwh_savings"] = kwh_reduction rec["heat_demand"] = property_phase_impact["heat_demand"]
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 ( 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") 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) return property_recommendations, impact_summary
expected_adjusted_energy = property_instance.current_adjusted_energy - sum(kwh_savings_list)
return (
property_recommendations,
expected_adjusted_energy,
expected_energy_bill
)