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