Added unit tests for annual bill savings appliance consumption

This commit is contained in:
Khalim Conn-Kowlessar 2024-06-25 14:47:36 +01:00
parent 01c50eb5cb
commit 83339d2cbe
8 changed files with 123 additions and 23 deletions

2
.idea/Model.iml generated
View file

@ -7,7 +7,7 @@
<sourceFolder url="file://$MODULE_DIR$/open_uprn" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/recommendations" isTestSource="false" />
</content>
<orderEntry type="jdk" jdkName="Python 3.10 (backend)" jdkType="Python SDK" />
<orderEntry type="jdk" jdkName="Python 3.10 (model_data)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="PyNamespacePackagesService">

2
.idea/misc.xml generated
View file

@ -3,7 +3,7 @@
<component name="Black">
<option name="sdkName" value="Python 3.10 (backend)" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.10 (backend)" project-jdk-type="Python SDK" />
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.10 (model_data)" project-jdk-type="Python SDK" />
<component name="PythonCompatibilityInspectionAdvertiser">
<option name="version" value="3" />
</component>

View file

@ -18,6 +18,7 @@ from recommendations.recommendation_utils import (
esimtate_pitched_roof_area,
estimate_windows,
)
from backend.ml_models.AnnualBillSavings import AnnualBillSavings
ENVIRONMENT = os.environ.get("ENVIRONMENT", "dev")
DATA_BUCKET = os.environ.get(
@ -590,6 +591,23 @@ class Property:
self.set_energy_source()
self.find_energy_sources()
def set_current_energy_bill(self):
"""
Given what we know about the property now, estimates the current energy consumption using the UCL paper
https://www.sciencedirect.com/science/article/pii/S0378778823002542
:return:
"""
starting_heat_demand = (
float(self.data["energy-consumption-current"]) * self.floor_area
)
self.current_adjusted_energy = AnnualBillSavings.adjust_energy_to_metered(
epc_energy_consumption=starting_heat_demand,
current_epc_rating=self.data["current-energy-rating"],
)
self.current_energy_bill = AnnualBillSavings.calculate_annual_bill(self.current_adjusted_energy)
def set_spatial(self, spatial: pd.DataFrame):
"""
Sets whether the property is in a conservation area given the output of the ConservationAreaClient
@ -909,14 +927,13 @@ class Property:
return component_data
def set_adjusted_energy(
self, current_adjusted_energy, expected_adjusted_energy, current_energy_bill, expected_energy_bill
self, expected_adjusted_energy, expected_energy_bill
):
"""
Stores these values for usage later
"""
self.current_adjusted_energy = current_adjusted_energy
self.expected_adjusted_energy = expected_adjusted_energy
self.current_energy_bill = current_energy_bill
self.expected_energy_bill = expected_energy_bill
def set_windows_count(self):

View file

@ -14,6 +14,31 @@ class GoogleSolarApi:
# be exported
SOLAR_CONSUMPTION_PROPORTION = 0.5
# These are variables, described in the documentation for cost analysis for non-us locations, seen here
# https://developers.google.com/maps/documentation/solar/calculate-costs-non-us
# We use the default figures that the API uses for US locations
# The factor by which the cost of electricity increases annually. The Solar API uses 1.022 (2.2% annual increase)
# for US locations.
cost_increase_factor = 1.022
# The efficiency at which an inverter converts the DC electricity that is produced by the solar panels to the AC
# electricity that is used in a household. The Solar API uses 85% for US locations. We use 0.95.5 which is the
# middle value of the 93-98% range, cited by Sunsave:
# https://www.sunsave.energy/solar-panels-advice/system-size/inverters
dc_to_ac_rate = 0.955
# The Solar API uses 1.04 (4% annual increase) for US locations
discount_rate = 1.04
# How much the efficiency of the solar panels declines each year. The Solar API uses 0.995 (0.5% annual decrease)
# for US locations
efficiency_depreciation_factor = 0.995
# The expected lifespan of the solar installation. The Solar API uses 20 years. Adjust this value as needed for
# your area
installation_life_span = 20
def __init__(self, api_key, max_retries=5):
"""
Initialize the GoogleSolarApi class with the provided API key and maximum retries.
@ -94,6 +119,13 @@ class GoogleSolarApi:
self.insights_data["solarPotential"]["panelWidthMeters"]
)
self.panel_wattage = self.insights_data["solarPotential"]["panelCapacityWatts"]
if self.panel_wattage != 400:
# In the API documentation, it claims that the default output is 250W, however we've only seen 400W, so if
# we get anything other than 400W, we'll need to adjust the calculations in the output. For this, we should
# refer to https://developers.google.com/maps/documentation/solar/calculate-costs-non-us
# Where the documentation explains how to adjust the yearlyEnergyDcKwh figures.
# It should be straightforward, but I'd rather see an actual instance of this happening
raise NotImplementedError("Panel wattage is not 400W - implement me")
# Automatically exclude north-facing segments
self.exclude_north_facing_segments()

View file

@ -426,9 +426,7 @@ async def trigger_plan(body: PlanTriggerRequest):
(
recommendations_with_impact,
current_adjusted_energy,
expected_adjusted_energy,
current_energy_bill,
expected_energy_bill
) = (
Recommendations.calculate_recommendation_impact(
@ -440,9 +438,7 @@ async def trigger_plan(body: PlanTriggerRequest):
# Store the resulting adjusted energy in the property instance
property_instance.set_adjusted_energy(
current_adjusted_energy=current_adjusted_energy,
expected_adjusted_energy=expected_adjusted_energy,
current_energy_bill=current_energy_bill,
expected_energy_bill=expected_energy_bill
)

View file

@ -1,3 +1,6 @@
import numpy as np
class AnnualBillSavings:
"""
This is a simple class which will estimate the annual bill savings, based on the kwh savings.
@ -60,8 +63,58 @@ class AnnualBillSavings:
return cls.ELECTRICITY_PRICE_CAP * kwh + (cls.DAILY_STANDARD_CHARGE_ELECTRICITY * 365)
@staticmethod
def calculate_occupants(total_floor_area):
"""
From Table 1b of the SAP 2012 documentation https://bregroup.com/documents/d/bre-group/sap-2012_9-92
Provides a methodology to estimate occupancy, based on floor area. This is used to calculate the amount of
electricity used be appliances and during cooking.
:param total_floor_area:
:return:
"""
if total_floor_area <= 13.9:
return 1
return 1 + (1.76 * (1 - np.exp(-0.000349 * (total_floor_area - 13.9) * (total_floor_area - 13.9))) + 0.0013 * (
total_floor_area - 13.9))
@staticmethod
def estimate_electrical_appliances(occupants, total_floor_area):
"""
From secion L2 of SAP2012 Electrical appliances
https://bregroup.com/documents/d/bre-group/sap-2012_9-92
Used to estimate the amount of energy used by electrical appliances
:param occupants:
:param total_floor_area:
:return:
"""
e_a = 207.8 * np.power(total_floor_area * occupants, 0.4717)
days_in_month = {
1: 31,
2: 28,
3: 31,
4: 30,
5: 31,
6: 30,
7: 31,
8: 31,
9: 30,
10: 31,
11: 30,
12: 31
}
eam = 0
for m in range(1, 13):
nm = days_in_month[m]
eam += e_a * (1 + 0.157 * np.cos(2 * np.pi * (m - 1.78) / 12)) * nm / 365
return eam
@classmethod
def adjust_energy_to_metered(cls, epc_energy_consumption, current_epc_rating):
def adjust_energy_to_metered(cls, epc_energy_consumption, current_epc_rating, total_floor_area):
"""
The over-prediction of energy use by EPCs in Great Britain: A comparison
of EPC-modelled and metered primary energy use intensity
@ -72,6 +125,13 @@ class AnnualBillSavings:
:return:
"""
# The EPC energy consumption does not factor in cooking and applicance use, so this is estimated using the
# methodology outlined in SAP, and is discussed in the UCL paper in section 3.1.1
estimated_occupants = cls.calculate_occupants(total_floor_area=total_floor_area)
appliances_energy_use = cls.estimate_electrical_appliances(estimated_occupants, total_floor_area)
epc_energy_consumption += appliances_energy_use
gradients = {
"A": -0.1,
"B": -0.1,

View file

@ -6,6 +6,7 @@ from backend.SearchEpc import SearchEpc
import urllib.parse
import requests
from datetime import datetime
from scipy import stats
from fuzzywuzzy import fuzz
import numpy as np
@ -1598,7 +1599,6 @@ def compile_data_final():
property_attributes[c] = property_attributes[c].fillna(0)
property_attributes[c] = property_attributes[c].astype(float)
from scipy import stats
for col in fill_with_mode:
property_attributes[col] = property_attributes[col].replace('', None)
mode_val = stats.mode([float(x) for x in property_attributes[col].values if x not in [None, "", np.nan]])[0]
@ -1632,6 +1632,12 @@ def compile_data_final():
# s3_file_name="customers/Stonewater/clustering/clustering_dataframe.pkl"
# )
# from utils.s3 import read_pickle_from_s3
# data = read_pickle_from_s3(
# bucket_name="retrofit-data-dev",
# s3_file_name="customers/Stonewater/clustering/clustering_dataframe.pkl"
# )
# CLUSTERING!!
# from sklearn.cluster import KMeans

View file

@ -311,14 +311,6 @@ class Recommendations:
# This is the unadjusted resulting heat demand
predicted_heat_demand_change = starting_heat_demand - expected_heat_demand
# We don't want to adjust the heat demand for mechanical ventilation so we add it back on
# We adjust the heat demand figures to align to the UCL paper
current_adjusted_energy = AnnualBillSavings.adjust_energy_to_metered(
epc_energy_consumption=starting_heat_demand,
current_epc_rating=property_instance.data["current-energy-rating"],
)
# TODO: This isn't quite right as this is based on EVERY possible measure, not just the ones that are
# actually implemented
expected_adjusted_energy = AnnualBillSavings.adjust_energy_to_metered(
@ -327,11 +319,10 @@ class Recommendations:
)
adjusted_heat_demand_change = (
current_adjusted_energy - expected_adjusted_energy
property_instance.current_adjusted_energy - expected_adjusted_energy
)
# TODO: We should determine if the home is gas & electricity or just electricity
current_energy_bill = AnnualBillSavings.calculate_annual_bill(current_adjusted_energy)
expected_energy_bill = AnnualBillSavings.calculate_annual_bill(expected_adjusted_energy)
for recommendations_by_type in property_recommendations:
@ -410,8 +401,6 @@ class Recommendations:
return (
property_recommendations,
current_adjusted_energy,
expected_adjusted_energy,
current_energy_bill,
expected_energy_bill
)