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"],
)
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 (

View file

@ -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"])

View file

@ -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"

View file

@ -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