mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
Adding costs for ttzc
This commit is contained in:
parent
053218b3fd
commit
08a657eb9f
7 changed files with 338 additions and 18 deletions
|
|
@ -18,6 +18,9 @@ class AnnualBillSavings:
|
|||
# This is a weighted mean of the price caps, using the consumption figures above as weights
|
||||
PRICE_FACTOR = 0.09549999999999999
|
||||
|
||||
# Daily standard charge, based on average across England, Scotland and Wales, and includes VAT
|
||||
DAILY_STANDARD_CHARGE = 0.3143
|
||||
|
||||
EPC_BANDS = ["G", "F", "E", "D", "C", "B", "A"]
|
||||
|
||||
@classmethod
|
||||
|
|
@ -38,6 +41,16 @@ class AnnualBillSavings:
|
|||
"""
|
||||
return cls.ELECTRICITY_PRICE_CAP * kwh
|
||||
|
||||
@classmethod
|
||||
def calculate_annual_bill(cls, kwh):
|
||||
"""
|
||||
This method will estimate the total annual bill for a property
|
||||
:param kwh: The total kwh consumption
|
||||
:return: An estimate for annual bill
|
||||
"""
|
||||
|
||||
return cls.PRICE_FACTOR * kwh + cls.DAILY_STANDARD_CHARGE * 365
|
||||
|
||||
@classmethod
|
||||
def adjust_energy_to_metered(cls, epc_energy_consumption, current_epc_rating):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -140,6 +140,19 @@ def app():
|
|||
|
||||
asset_list["uprn"] = asset_list["uprn"].astype(int)
|
||||
|
||||
# We end up with some properties that are currently an EPC C, but we do not have this data in the download, so we
|
||||
# manually remove
|
||||
# 1) 3 Reid Close, CR5 3BL
|
||||
# 2) Flat 6, Collier Court 2A, St. Peters Road CR0 1HD
|
||||
asset_list = asset_list[
|
||||
~asset_list["uprn"].isin(
|
||||
[
|
||||
100020576460,
|
||||
100020624352,
|
||||
]
|
||||
)
|
||||
]
|
||||
|
||||
filename = f"{USER_ID}/{PORTFOLIO_ID}/inputs.csv"
|
||||
save_csv_to_s3(
|
||||
dataframe=asset_list,
|
||||
|
|
|
|||
|
|
@ -16,11 +16,15 @@ from etl.customers.slide_utils import (
|
|||
create_powerpoint,
|
||||
create_recommendations_summary
|
||||
)
|
||||
from backend.ml_models.AnnualBillSavings import AnnualBillSavings
|
||||
|
||||
USER_ID = 8
|
||||
PORTFOLIO_ID_1 = 67
|
||||
PORTFOLIO_ID_2 = 68
|
||||
EPC_TARGET_1 = "C"
|
||||
EPC_TARGET_2 = "A"
|
||||
SAP_TARGET_1 = 69
|
||||
SAP_TARGET_2 = 100
|
||||
CUSTOMER_KEY = "gla-demo"
|
||||
|
||||
|
||||
|
|
@ -32,11 +36,13 @@ def app():
|
|||
# Get the data we need
|
||||
########################################################################
|
||||
|
||||
portfolio_id = PORTFOLIO_ID_1
|
||||
# TODO: Update to portfolio desired
|
||||
# portfolio_id = PORTFOLIO_ID_1
|
||||
portfolio_id = PORTFOLIO_ID_2
|
||||
|
||||
# Get the asset list
|
||||
asset_list = read_csv_from_s3(
|
||||
"retrofit-plan-inputs-dev", f"{USER_ID}/{portfolio_id}/inputs.csv"
|
||||
"retrofit-plan-inputs-dev", f"{USER_ID}/67/inputs.csv"
|
||||
)
|
||||
asset_list = pd.DataFrame(asset_list)
|
||||
|
||||
|
|
@ -47,6 +53,10 @@ def app():
|
|||
# We now pull the data for the property details
|
||||
property_details = get_property_details_by_portfolio_id(session, portfolio_id)
|
||||
property_details_df = pd.DataFrame(property_details)
|
||||
# We estimate bills based on the adjusted_energy_consumption
|
||||
property_details_df["energy_bill"] = property_details_df["adjusted_energy_consumption"].apply(
|
||||
lambda x: AnnualBillSavings.calculate_annual_bill(x)
|
||||
)
|
||||
# Merge on uprn
|
||||
property_details_df = property_details_df.merge(
|
||||
properties_df[["uprn", "id"]].rename(columns={"id": "property_id"}),
|
||||
|
|
@ -66,22 +76,84 @@ def app():
|
|||
on="property_id"
|
||||
)
|
||||
|
||||
# Summary information by each archetype
|
||||
archetype_1 = asset_list[asset_list["archetype"] == "Archetype 1"]
|
||||
|
||||
recommendations_arch_1_summary = create_recommendations_summary(
|
||||
recommendations_df[recommendations_df["uprn"].astype(str).isin(archetype_1["uprn"].values)],
|
||||
properties_df[properties_df["uprn"].astype(str).isin(archetype_1["uprn"].values)],
|
||||
recommendations_summary = create_recommendations_summary(
|
||||
recommendations_df,
|
||||
properties_df,
|
||||
property_details_df,
|
||||
SAP_TARGET_1
|
||||
)
|
||||
|
||||
# Take the mean, median and maximum of each value
|
||||
arch_1_recommendation_means = recommendations_arch_1_summary.mean()
|
||||
# Calculate % changes of energ, co2 and abs
|
||||
recommendations_summary["carbon_percent_change"] = (
|
||||
recommendations_summary["total_carbon"] / recommendations_summary["current_co2"]
|
||||
)
|
||||
|
||||
arch_1_property_details = property_details_df[
|
||||
property_details_df["uprn"].astype(str).isin(archetype_1["uprn"].values)
|
||||
recommendations_summary["energy_percent_change"] = (
|
||||
recommendations_summary["adjusted_heat_demand"] / recommendations_summary["current_energy"]
|
||||
)
|
||||
|
||||
recommendations_summary["bills_percent_change"] = (
|
||||
recommendations_summary["total_bill_savings"] / recommendations_summary["current_energy_bill"]
|
||||
)
|
||||
|
||||
# Summary information by each archetype
|
||||
########################
|
||||
# Archetype 1
|
||||
########################
|
||||
archetype_1 = asset_list[asset_list["archetype"] == "Archetype 1"]
|
||||
recommendations_arch_1_summary = recommendations_summary[
|
||||
recommendations_summary["uprn"].astype(str).isin(archetype_1["uprn"].values)
|
||||
]
|
||||
|
||||
arch_1_property_details_means = arch_1_property_details.mean()
|
||||
# Take the mean, median and maximum of each value
|
||||
arch_1_recommendation_min = recommendations_arch_1_summary.min()
|
||||
arch_1_recommendation_max = recommendations_arch_1_summary.max()
|
||||
arch_1_recommendation_means = recommendations_arch_1_summary.mean()
|
||||
|
||||
arch_1_recommendation_means["total_bill_savings"] / arch_1_property_details_means["adjusted_energy_consumption"]
|
||||
########################
|
||||
# Archetype 2
|
||||
########################
|
||||
archetype_2 = asset_list[asset_list["archetype"] == "Archetype 2"]
|
||||
recommendations_arch_2_summary = recommendations_summary[
|
||||
recommendations_summary["uprn"].astype(str).isin(archetype_2["uprn"].values)
|
||||
]
|
||||
|
||||
# Take the mean, median and maximum of each value
|
||||
arch_2_recommendation_min = recommendations_arch_2_summary.min()
|
||||
arch_2_recommendation_max = recommendations_arch_2_summary.max()
|
||||
arch_2_recommendation_means = recommendations_arch_2_summary.mean().round(2)
|
||||
|
||||
########################
|
||||
# Archetype 3
|
||||
########################
|
||||
archetype_3 = asset_list[asset_list["archetype"] == "Archetype 3"]
|
||||
recommendations_arch_3_summary = recommendations_summary[
|
||||
recommendations_summary["uprn"].astype(str).isin(archetype_3["uprn"].values)
|
||||
]
|
||||
|
||||
# Take the mean, median and maximum of each value
|
||||
arch_3_recommendation_min = recommendations_arch_3_summary.min()
|
||||
arch_3_recommendation_max = recommendations_arch_3_summary.max()
|
||||
arch_3_recommendation_means = recommendations_arch_3_summary.mean()
|
||||
|
||||
########################
|
||||
# Archetype 4
|
||||
########################
|
||||
archetype_4 = asset_list[asset_list["archetype"] == "Archetype 4"]
|
||||
recommendations_arch_4_summary = recommendations_summary[
|
||||
recommendations_summary["uprn"].astype(str).isin(archetype_4["uprn"].values)
|
||||
]
|
||||
|
||||
# Take the mean, median and maximum of each value
|
||||
arch_4_recommendation_min = recommendations_arch_4_summary.min()
|
||||
arch_4_recommendation_max = recommendations_arch_4_summary.max()
|
||||
arch_4_recommendation_means = recommendations_arch_4_summary.mean()
|
||||
|
||||
property_details_df[
|
||||
property_details_df["uprn"].astype(str).isin(archetype_4["uprn"].values)
|
||||
]["total_floor_area"].mean()
|
||||
|
||||
########################
|
||||
# Overview
|
||||
########################
|
||||
overview_totals = recommendations_summary.sum()
|
||||
|
|
|
|||
|
|
@ -246,7 +246,7 @@ def create_powerpoint(data, save_location):
|
|||
prs.save(save_location)
|
||||
|
||||
|
||||
def create_recommendations_summary(recommendations_df, properties_df, sap_target):
|
||||
def create_recommendations_summary(recommendations_df, properties_df, property_details_df, sap_target):
|
||||
# Aggregate the impact of the recommendations
|
||||
# We want:
|
||||
# Total number of sap points
|
||||
|
|
@ -259,13 +259,15 @@ def create_recommendations_summary(recommendations_df, properties_df, sap_target
|
|||
total_valuation_impact=("property_valuation_increase", "sum"),
|
||||
total_bill_savings=("energy_cost_savings", "sum"),
|
||||
total_cost=("estimated_cost", "sum"),
|
||||
total_carbon=("co2_equivalent_savings", "sum")
|
||||
total_carbon=("co2_equivalent_savings", "sum"),
|
||||
adjusted_heat_demand=("adjusted_heat_demand", "sum")
|
||||
).reset_index()
|
||||
# Merge on current sap points
|
||||
# Merge on current sap points, current CO2, current adjusted_heat_demand, current annual bill
|
||||
recommendations_summary = recommendations_summary.merge(
|
||||
properties_df[["id", "uprn", "current_sap_points"]].rename(columns={"id": "property_id"}), on="property_id",
|
||||
how="left"
|
||||
)
|
||||
|
||||
recommendations_summary["expected_sap_points"] = (
|
||||
recommendations_summary["current_sap_points"] + recommendations_summary["total_sap_points"]
|
||||
)
|
||||
|
|
@ -274,4 +276,18 @@ def create_recommendations_summary(recommendations_df, properties_df, sap_target
|
|||
)
|
||||
recommendations_summary["sap_difference"] = sap_target - recommendations_summary["expected_sap_points"]
|
||||
|
||||
if property_details_df is not None:
|
||||
recommendations_summary = recommendations_summary.merge(
|
||||
property_details_df[["uprn", "co2_emissions", "adjusted_energy_consumption", "energy_bill"]].rename(
|
||||
columns={
|
||||
"id": "property_id",
|
||||
"co2_emissions": "current_co2",
|
||||
"adjusted_energy_consumption": "current_energy",
|
||||
"energy_bill": "current_energy_bill"
|
||||
}
|
||||
),
|
||||
on="uprn",
|
||||
how="left"
|
||||
)
|
||||
|
||||
return recommendations_summary
|
||||
|
|
|
|||
|
|
@ -42,7 +42,22 @@ BATTERY_COST = 3500
|
|||
|
||||
# This is based on https://www.checkatrade.com/blog/cost-guides/cost-smart-thermostat/
|
||||
SMART_APPLIANCE_THERMOSTAT_COST = 400
|
||||
PROGRAMMER_COST = 200
|
||||
PROGRAMMER_COST = 120
|
||||
ROOM_THERMOSTAT_COST = 150
|
||||
TRVS_COST = 35
|
||||
|
||||
# Cost for TTZC
|
||||
# Smart thermostat based on checkatrade https://www.checkatrade.com/blog/cost-guides/cost-smart-thermostat/
|
||||
# Based on the Nest system
|
||||
TTZC_SMART_THERMOSTAT_COST = 205
|
||||
TTZC_SMART_THERMOSTAT_LABOUR_HOURS = 2
|
||||
TTZC_ELECTRICIAN_HOURLY_RATE = 45
|
||||
# Based on cost of a Nest temperature sensor
|
||||
TTZC_ROOM_TEMPERATURE_SENSOR_COST = 50
|
||||
TTZC_ROOM_TEMPERATURE_SENSOR_LABOUR_HOURS = 0.17 # (Assume ~ 10 mins install per sensor)
|
||||
# Basedon an average cost of smart radiator values
|
||||
TTZC_SMART_RADIATOR_VALUES = 50
|
||||
TTZC_SMART_RADIATOR_VALUES_LABOUR_HOURS = 0.37 # (Assume ~ 15-30 mins install per valve)
|
||||
|
||||
|
||||
class Costs:
|
||||
|
|
@ -998,3 +1013,69 @@ class Costs:
|
|||
"labour_hours": 0,
|
||||
"labour_days": 0,
|
||||
}
|
||||
|
||||
def roomstat_programmer_trvs(
|
||||
self, number_heated_rooms, has_programmer, has_trvs, has_room_thermostat
|
||||
):
|
||||
"""
|
||||
|
||||
:return:
|
||||
"""
|
||||
|
||||
total_cost = 0
|
||||
labour_hours = 0
|
||||
|
||||
if not has_programmer:
|
||||
total_cost += PROGRAMMER_COST
|
||||
labour_hours += 1
|
||||
|
||||
if not has_trvs:
|
||||
total_cost += TRVS_COST * number_heated_rooms
|
||||
labour_hours += 0.25 * number_heated_rooms
|
||||
|
||||
if not has_room_thermostat:
|
||||
total_cost += ROOM_THERMOSTAT_COST
|
||||
labour_hours += 0.5
|
||||
|
||||
subtotal_before_vat = total_cost / (1 + self.VAT_RATE)
|
||||
vat = total_cost - subtotal_before_vat
|
||||
|
||||
return {
|
||||
"total": total_cost,
|
||||
"subtotal": subtotal_before_vat,
|
||||
"vat": vat,
|
||||
"labour_hours": labour_hours,
|
||||
"labour_days": 1,
|
||||
}
|
||||
|
||||
def time_and_temperature_zone_control(self, number_heated_rooms):
|
||||
|
||||
# The product costs are inclusive of VAT
|
||||
product_costs = (
|
||||
TTZC_SMART_THERMOSTAT_COST +
|
||||
TTZC_ROOM_TEMPERATURE_SENSOR_COST * number_heated_rooms +
|
||||
TTZC_SMART_RADIATOR_VALUES * number_heated_rooms
|
||||
)
|
||||
labour_hours = (
|
||||
TTZC_SMART_THERMOSTAT_LABOUR_HOURS +
|
||||
TTZC_ROOM_TEMPERATURE_SENSOR_LABOUR_HOURS * number_heated_rooms +
|
||||
TTZC_SMART_RADIATOR_VALUES_LABOUR_HOURS * number_heated_rooms
|
||||
)
|
||||
labour_costs = TTZC_ELECTRICIAN_HOURLY_RATE * labour_hours
|
||||
# Add continency and preliminaries to the labour to account for the complexity of the job
|
||||
labour_costs = labour_costs * (1 + self.CONTINGENCY + self.PRELIMINARIES)
|
||||
|
||||
vat = labour_costs * self.VAT_RATE
|
||||
|
||||
subtotal_before_vat = product_costs + labour_costs
|
||||
total_cost = subtotal_before_vat + vat
|
||||
|
||||
labour_days = np.ceil(labour_hours / 8)
|
||||
|
||||
return {
|
||||
"total": total_cost,
|
||||
"subtotal": subtotal_before_vat,
|
||||
"vat": vat,
|
||||
"labour_hours": labour_hours,
|
||||
"labour_days": labour_days,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,14 @@ class HeatingControlRecommender:
|
|||
self.recommend_high_heat_retention_controls()
|
||||
return
|
||||
|
||||
if heating_description in ["Boiler and radiators, mains gas"]:
|
||||
# We can recommend roomstat programmer trvs
|
||||
self.recommend_roomstat_programmer_trvs()
|
||||
# We can also recommend time and temperature zone controls
|
||||
self.recommend_time_temperature_zone_controls()
|
||||
|
||||
return
|
||||
|
||||
def recommend_room_heaters_electric_controls(self):
|
||||
"""
|
||||
If the home has Room heaters, electric, we start by identifying potential heating controls that could
|
||||
|
|
@ -105,3 +113,103 @@ class HeatingControlRecommender:
|
|||
|
||||
# We don't implement any other recommendations right now
|
||||
return
|
||||
|
||||
def recommend_roomstat_programmer_trvs(self):
|
||||
"""
|
||||
If the home has a boiler and radiators, mains gas, we start by identifying potential heating controls that could
|
||||
be upgraded, that would provide a practical impact.
|
||||
|
||||
The criteria for recommending an upgrade to heating controls are (one of these must be true)
|
||||
1) There are no controls
|
||||
2) No programmer
|
||||
3) No room thermostat
|
||||
4) No TRVs
|
||||
|
||||
|
||||
:return:
|
||||
"""
|
||||
|
||||
# We check if we have the conditions to recommend this upgrade
|
||||
|
||||
needs_programmer = self.property.main_heating_controls["switch_system"] is None
|
||||
needs_room_thermostat = self.property.main_heating_controls["thermostatic_control"] is None
|
||||
needs_trvs = self.property.main_heating_controls["trvs"] is None
|
||||
|
||||
can_recommend = (
|
||||
(self.property.main_heating_controls["no_control"] is not None) or
|
||||
needs_programmer or
|
||||
needs_room_thermostat or
|
||||
needs_trvs
|
||||
)
|
||||
|
||||
if not can_recommend:
|
||||
return
|
||||
|
||||
ending_config = MainheatControlAttributes("Programmer, room thermostat and TRVS").process()
|
||||
# We use this to determine how we should be updating the config
|
||||
simulation_config = check_simulation_difference(
|
||||
new_config=ending_config, old_config=self.property.main_heating_controls
|
||||
)
|
||||
# This upgrade will only take the heating system to average energy efficiency
|
||||
# If the current system is below good, we make it good
|
||||
if self.property.data["mainheatc-energy-eff"] in ["Poor", "Very Poor", "Average"]:
|
||||
simulation_config["mainheatc_energy_eff_ending"] = "Good"
|
||||
|
||||
has_programmer = not needs_programmer
|
||||
has_room_thermostat = not needs_room_thermostat
|
||||
has_trvs = not needs_trvs
|
||||
|
||||
self.recommendation.append(
|
||||
{
|
||||
"description": "upgrade heating controls to Room thermostat, programmer and TRVs",
|
||||
**self.costs.roomstat_programmer_trvs(
|
||||
number_heated_rooms=int(self.property.data["number-heated-rooms"]),
|
||||
has_programmer=has_programmer,
|
||||
has_room_thermostat=has_room_thermostat,
|
||||
has_trvs=has_trvs
|
||||
),
|
||||
"simulation_config": simulation_config
|
||||
}
|
||||
)
|
||||
|
||||
return
|
||||
|
||||
def recommend_time_temperature_zone_controls(self):
|
||||
"""
|
||||
If the home has a boiler, we can recommend time and temperature zone controls. This is a more advanced
|
||||
and more efficient control system than the standard controls that come with a boiler. However, it may come
|
||||
with a higher cost and more involved usage
|
||||
:return:
|
||||
"""
|
||||
|
||||
# We check if the efficiency of the current heating controls is good or below, and
|
||||
|
||||
# Conditions for installation are as follows:
|
||||
# 1) The current heating controls are not time and temperature zone controls
|
||||
# 2) The current heating controls are not already at 'Very Good' or above
|
||||
|
||||
if (
|
||||
(self.property["thermostatic_control"] == "time and temperature zone control") or
|
||||
(self.property.data["mainheatc-energy-eff"] in ["Very Good"])
|
||||
):
|
||||
# No recommendation needed
|
||||
return
|
||||
|
||||
ending_config = MainheatControlAttributes("Time and temperature zone control").process()
|
||||
|
||||
# We use this to determine how we should be updating the config
|
||||
simulation_config = check_simulation_difference(
|
||||
new_config=ending_config, old_config=self.property.main_heating_controls
|
||||
)
|
||||
|
||||
# If the current system is below very good, we make it very good
|
||||
if self.property.data["mainheatc-energy-eff"] in ["Poor", "Very Poor", "Average", "Good"]:
|
||||
simulation_config["mainheatc_energy_eff_ending"] = "Very Good"
|
||||
|
||||
self.recommendation.append(
|
||||
{
|
||||
"description": "upgrade heating controls to Room thermostat, programmer and TRVs",
|
||||
**self.costs.time_and_temperature_zone_control(),
|
||||
"simulation_config": simulation_config
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -26,6 +26,11 @@ class HeatingRecommender:
|
|||
self.recommend_electric_storage_heaters(phase=phase, system_change=True, heating_controls_only=False)
|
||||
return
|
||||
|
||||
# if the property has mains heating with boiler and radiators, we recommend optimal heating controls
|
||||
if self.property.main_heating["clean_description"] in ["Boiler and radiators, mains gas"]:
|
||||
self.recommend_roomstat_programmer_trvs(phase=phase)
|
||||
return
|
||||
|
||||
@staticmethod
|
||||
def check_simulation_difference(old_config, new_config):
|
||||
"""
|
||||
|
|
@ -182,3 +187,15 @@ class HeatingRecommender:
|
|||
)
|
||||
|
||||
self.recommendations.extend(recommendations)
|
||||
|
||||
def recommend_roomstat_programmer_trvs(self, phase):
|
||||
"""
|
||||
|
||||
:param phase:
|
||||
:return:
|
||||
"""
|
||||
# We recommend the heating controls
|
||||
controls_recommender = HeatingControlRecommender(self.property)
|
||||
controls_recommender.recommend(heating_description="Boiler and radiators, mains gas")
|
||||
|
||||
controls_recommender.recommendation
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue