mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-30 13:10:47 +00:00
integrating the non-intrusive recommendations for ashp and solarpv into backend engine
This commit is contained in:
parent
19c471f614
commit
92fcd080a8
12 changed files with 613 additions and 146 deletions
|
|
@ -2,6 +2,7 @@ import os
|
||||||
import ast
|
import ast
|
||||||
from itertools import groupby
|
from itertools import groupby
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
import numpy as np
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from etl.epc.Dataset import TrainingDataset
|
from etl.epc.Dataset import TrainingDataset
|
||||||
|
|
@ -211,9 +212,24 @@ class Property:
|
||||||
if n_bedrooms not in [None, ""]:
|
if n_bedrooms not in [None, ""]:
|
||||||
n_bedrooms = int(round(float(n_bedrooms) + 1e-5))
|
n_bedrooms = int(round(float(n_bedrooms) + 1e-5))
|
||||||
|
|
||||||
|
number_of_floors = kwargs.get("number_of_floors", None)
|
||||||
|
if number_of_floors not in [None, ""]:
|
||||||
|
number_of_floors = int(round(float(number_of_floors) + 1e-5))
|
||||||
|
|
||||||
|
insulation_floor_area = kwargs.get("insulation_floor_area", None)
|
||||||
|
if insulation_floor_area not in [None, ""]:
|
||||||
|
insulation_floor_area = float(insulation_floor_area)
|
||||||
|
|
||||||
|
insulation_wall_area = kwargs.get("insulation_wall_area", None)
|
||||||
|
if insulation_wall_area not in [None, ""]:
|
||||||
|
insulation_wall_area = float(insulation_wall_area)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"n_bathrooms": n_bathrooms,
|
"n_bathrooms": n_bathrooms,
|
||||||
"n_bedrooms": n_bedrooms,
|
"n_bedrooms": n_bedrooms,
|
||||||
|
"number_of_floors": number_of_floors,
|
||||||
|
"insulation_floor_area": insulation_floor_area,
|
||||||
|
"insulation_wall_area": insulation_wall_area,
|
||||||
"building_id": kwargs.get("building_id", None),
|
"building_id": kwargs.get("building_id", None),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -222,6 +238,9 @@ class Property:
|
||||||
self.n_bathrooms = kwargs.get("n_bathrooms", None)
|
self.n_bathrooms = kwargs.get("n_bathrooms", None)
|
||||||
self.n_bedrooms = kwargs.get("n_bedrooms", None)
|
self.n_bedrooms = kwargs.get("n_bedrooms", None)
|
||||||
self.building_id = kwargs.get("building_id", None)
|
self.building_id = kwargs.get("building_id", None)
|
||||||
|
self.number_of_floors = kwargs.get("number_of_floors", None)
|
||||||
|
self.insulation_floor_area = kwargs.get("insulation_floor_area", None)
|
||||||
|
self.insulation_wall_area = kwargs.get("insulation_wall_area", None)
|
||||||
|
|
||||||
def create_base_difference_epc_record(self, cleaned_lookup: dict):
|
def create_base_difference_epc_record(self, cleaned_lookup: dict):
|
||||||
"""
|
"""
|
||||||
|
|
@ -1060,18 +1079,22 @@ class Property:
|
||||||
|
|
||||||
# We can update the number of floors if we have this information in the condition data
|
# We can update the number of floors if we have this information in the condition data
|
||||||
self.number_of_floors = int(self.energy_assessment_condition_data["number_of_floors"]) \
|
self.number_of_floors = int(self.energy_assessment_condition_data["number_of_floors"]) \
|
||||||
if condition_data.get("number_of_floors") is not None \
|
if (condition_data.get("number_of_floors") is not None) and (self.number_of_floors is not None) \
|
||||||
else self.number_of_floors
|
else self.number_of_floors
|
||||||
|
|
||||||
self.perimeter = float(self.energy_assessment_condition_data["perimeter"]) \
|
# If we already have this, we re-engineer the perimeter
|
||||||
if condition_data.get("perimeter") is not None \
|
if self.insulation_floor_area is not None:
|
||||||
else estimate_perimeter(
|
self.perimeter = np.sqrt(self.insulation_floor_area) * 4
|
||||||
floor_area=self.floor_area / self.number_of_floors,
|
else:
|
||||||
num_rooms=self.number_of_rooms / self.number_of_floors
|
self.perimeter = float(self.energy_assessment_condition_data["perimeter"]) \
|
||||||
)
|
if condition_data.get("perimeter") is not None \
|
||||||
|
else estimate_perimeter(
|
||||||
|
floor_area=self.floor_area / self.number_of_floors,
|
||||||
|
num_rooms=self.number_of_rooms / self.number_of_floors
|
||||||
|
)
|
||||||
|
|
||||||
self.insulation_wall_area = float(self.energy_assessment_condition_data["insulation_wall_area"]) \
|
self.insulation_wall_area = float(self.energy_assessment_condition_data["insulation_wall_area"]) \
|
||||||
if condition_data.get("insulation_wall_area") is not None \
|
if (condition_data.get("insulation_wall_area") is not None) and (self.insulation_wall_area is not None) \
|
||||||
else estimate_external_wall_area(
|
else estimate_external_wall_area(
|
||||||
num_floors=self.number_of_floors,
|
num_floors=self.number_of_floors,
|
||||||
floor_height=self.floor_height,
|
floor_height=self.floor_height,
|
||||||
|
|
@ -1079,9 +1102,12 @@ class Property:
|
||||||
built_form=self.data["built-form"],
|
built_form=self.data["built-form"],
|
||||||
)
|
)
|
||||||
|
|
||||||
self.insulation_floor_area = float(self.energy_assessment_condition_data["main_dwelling_ground_floor_area"]) \
|
if self.insulation_floor_area is not None:
|
||||||
if condition_data.get("main_dwelling_ground_floor_area") is not None \
|
self.insulation_floor_area = float(
|
||||||
else self.floor_area / self.number_of_floors
|
self.energy_assessment_condition_data["main_dwelling_ground_floor_area"]
|
||||||
|
) if (condition_data.get("main_dwelling_ground_floor_area") is not None) else (
|
||||||
|
self.floor_area / self.number_of_floors
|
||||||
|
)
|
||||||
|
|
||||||
def set_floor_level(self):
|
def set_floor_level(self):
|
||||||
self.floor_level = (
|
self.floor_level = (
|
||||||
|
|
|
||||||
|
|
@ -388,7 +388,7 @@ async def trigger_plan(body: PlanTriggerRequest):
|
||||||
|
|
||||||
property_non_invasive_recommendations = next((
|
property_non_invasive_recommendations = next((
|
||||||
x for x in non_invasive_recommendations if
|
x for x in non_invasive_recommendations if
|
||||||
(x["address"] == config["address"]) and (x["postcode"] == config["postcode"])
|
(x["uprn"] == config["uprn"])
|
||||||
), {})
|
), {})
|
||||||
|
|
||||||
input_properties.append(
|
input_properties.append(
|
||||||
|
|
@ -432,6 +432,25 @@ async def trigger_plan(body: PlanTriggerRequest):
|
||||||
environment=get_settings().ENVIRONMENT
|
environment=get_settings().ENVIRONMENT
|
||||||
)
|
)
|
||||||
|
|
||||||
|
epcs_for_scoring = pd.DataFrame([energy_consumption_client.prepare_new_data(p) for p in input_properties])
|
||||||
|
# What do we need?
|
||||||
|
# We need an estimate of each properties energy consumption now, as well as the cost of heating and hot water
|
||||||
|
# The newest EPC may have been done quite some time ago, and so we should take this into consideration when
|
||||||
|
# producing the estimate for cost. With that said, we already have a methodology which will re-map the cost
|
||||||
|
# when the EPC was produced to a cost for today, however could we use the ML models.
|
||||||
|
# In theory, we could just score the kwh models via the API, pass the results into the get_components function
|
||||||
|
# and insert the kwh figures into the property and we're done
|
||||||
|
# TODO: Need to check if we need to re-map when scoring new data or not
|
||||||
|
|
||||||
|
# We need to prepare the EPC so it's in the same format as the training data
|
||||||
|
# TODO: DELETE ME
|
||||||
|
# from utils.s3 import read_dataframe_from_s3_parquet
|
||||||
|
# train = read_dataframe_from_s3_parquet(
|
||||||
|
# bucket_name="retrofit-data-dev",
|
||||||
|
# file_key="energy_consumption/2024-07-08/energy_consumption_dataset.parquet"
|
||||||
|
# )
|
||||||
|
# We need to prepare the EPC so it's in the same format as the training data
|
||||||
|
|
||||||
logger.info("Getting spatial data")
|
logger.info("Getting spatial data")
|
||||||
for p in input_properties:
|
for p in input_properties:
|
||||||
p.get_components(cleaned=cleaned, energy_consumption_client=energy_consumption_client)
|
p.get_components(cleaned=cleaned, energy_consumption_client=energy_consumption_client)
|
||||||
|
|
@ -444,6 +463,7 @@ async def trigger_plan(body: PlanTriggerRequest):
|
||||||
# extensions, since it doesn't seem to do a great job
|
# extensions, since it doesn't seem to do a great job
|
||||||
# TODO: For simple properties, we should do a comparison/check between the solar API's roof area and the
|
# TODO: For simple properties, we should do a comparison/check between the solar API's roof area and the
|
||||||
# basic estimate of roof area
|
# basic estimate of roof area
|
||||||
|
# TODO: TEMP SWITCHED OFF
|
||||||
building_ids = [
|
building_ids = [
|
||||||
{
|
{
|
||||||
"building_id": p.building_id,
|
"building_id": p.building_id,
|
||||||
|
|
@ -481,109 +501,112 @@ async def trigger_plan(body: PlanTriggerRequest):
|
||||||
"uprn": p.uprn
|
"uprn": p.uprn
|
||||||
} for p in input_properties if p.building_id is None
|
} for p in input_properties if p.building_id is None
|
||||||
]
|
]
|
||||||
if building_ids:
|
if False:
|
||||||
# Find the unique longitude and latitude pairs for each building id
|
if building_ids:
|
||||||
unique_coordinates = {}
|
# Find the unique longitude and latitude pairs for each building id
|
||||||
building_uprns = {}
|
unique_coordinates = {}
|
||||||
for entry in building_ids:
|
building_uprns = {}
|
||||||
building_id = entry['building_id']
|
for entry in building_ids:
|
||||||
coordinate_pair = {'longitude': entry['longitude'], 'latitude': entry['latitude']}
|
building_id = entry['building_id']
|
||||||
|
coordinate_pair = {'longitude': entry['longitude'], 'latitude': entry['latitude']}
|
||||||
|
|
||||||
if building_id not in unique_coordinates:
|
if building_id not in unique_coordinates:
|
||||||
unique_coordinates[building_id] = []
|
unique_coordinates[building_id] = []
|
||||||
|
|
||||||
if coordinate_pair not in unique_coordinates[building_id]:
|
if coordinate_pair not in unique_coordinates[building_id]:
|
||||||
unique_coordinates[building_id].append(coordinate_pair)
|
unique_coordinates[building_id].append(coordinate_pair)
|
||||||
|
|
||||||
if building_id not in building_uprns:
|
if building_id not in building_uprns:
|
||||||
building_uprns[building_id] = []
|
building_uprns[building_id] = []
|
||||||
|
|
||||||
if entry['uprn'] not in building_uprns[building_id]:
|
if entry['uprn'] not in building_uprns[building_id]:
|
||||||
building_uprns[building_id].append(
|
building_uprns[building_id].append(
|
||||||
{
|
{
|
||||||
"uprn": entry['uprn'], "longitude": entry['longitude'], "latitude": entry['latitude']
|
"uprn": entry['uprn'], "longitude": entry['longitude'], "latitude": entry['latitude']
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
solar_panel_configuration = {}
|
|
||||||
for building_id, coordinates in unique_coordinates.items():
|
|
||||||
if len(coordinates) > 1:
|
|
||||||
raise NotImplementedError("more than one coordinate for a building - handle me")
|
|
||||||
|
|
||||||
coordinates = coordinates[0]
|
|
||||||
energy_consumption = sum(
|
|
||||||
[entry['energy_consumption'] for entry in building_ids if entry['building_id'] == building_id]
|
|
||||||
)
|
|
||||||
solar_api_client.get(
|
|
||||||
longitude=coordinates["longitude"],
|
|
||||||
latitude=coordinates["latitude"],
|
|
||||||
energy_consumption=energy_consumption,
|
|
||||||
is_building=True,
|
|
||||||
session=session
|
|
||||||
)
|
|
||||||
solar_panel_configuration[building_id] = {
|
|
||||||
"insights_data": solar_api_client.insights_data,
|
|
||||||
"panel_performance": solar_api_client.panel_performance,
|
|
||||||
"n_units": len([entry for entry in building_ids if entry['building_id'] == building_id])
|
|
||||||
}
|
|
||||||
|
|
||||||
# Store the data in the database
|
|
||||||
# TODO: Rather than just doing a straight insert, we should overwrite what's already there if it exists
|
|
||||||
solar_api_client.save_to_db(
|
|
||||||
session=session, uprns_to_location=building_uprns[building_id], scenario_type="building"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Insert this into the properties that have this building id
|
|
||||||
for p in input_properties:
|
|
||||||
if p.building_id == building_id:
|
|
||||||
unit_solar_panel_configuration = solar_panel_configuration[building_id].copy()
|
|
||||||
|
|
||||||
unit_solar_panel_configuration["unit_share_of_energy"] = (
|
|
||||||
[x for x in building_ids if x["property_id"] == p.id][0]["energy_consumption"] /
|
|
||||||
energy_consumption
|
|
||||||
)
|
)
|
||||||
p.set_solar_panel_configuration(unit_solar_panel_configuration)
|
|
||||||
|
|
||||||
if individual_units:
|
solar_panel_configuration = {}
|
||||||
# Model the solar potential at the property level
|
for building_id, coordinates in unique_coordinates.items():
|
||||||
for unit in individual_units:
|
if len(coordinates) > 1:
|
||||||
property_instance = [p for p in input_properties if p.id == unit["property_id"]][0]
|
raise NotImplementedError("more than one coordinate for a building - handle me")
|
||||||
# At this level, we check if the property is suitable for solar and if now, skip
|
|
||||||
if not property_instance.is_solar_pv_valid():
|
|
||||||
continue
|
|
||||||
|
|
||||||
solar_api_client.get(
|
coordinates = coordinates[0]
|
||||||
longitude=unit["longitude"],
|
energy_consumption = sum(
|
||||||
latitude=unit["latitude"],
|
[entry['energy_consumption'] for entry in building_ids if entry['building_id'] == building_id]
|
||||||
energy_consumption=unit["energy_consumption"],
|
)
|
||||||
is_building=False,
|
solar_api_client.get(
|
||||||
session=session,
|
longitude=coordinates["longitude"],
|
||||||
uprn=unit["uprn"],
|
latitude=coordinates["latitude"],
|
||||||
property_instance=property_instance
|
energy_consumption=energy_consumption,
|
||||||
)
|
is_building=True,
|
||||||
|
session=session
|
||||||
# Store the data in the database
|
)
|
||||||
# TODO: Rather than just doing a straight insert, we should overwrite what's already there if it exists
|
solar_panel_configuration[building_id] = {
|
||||||
solar_api_client.save_to_db(
|
|
||||||
session=session,
|
|
||||||
uprns_to_location=[
|
|
||||||
{
|
|
||||||
"uprn": property_instance.uprn,
|
|
||||||
"longitude": property_instance.spatial["longitude"],
|
|
||||||
"latitude": property_instance.spatial["latitude"]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
scenario_type="unit"
|
|
||||||
)
|
|
||||||
|
|
||||||
property_instance.set_solar_panel_configuration(
|
|
||||||
solar_panel_configuration={
|
|
||||||
"insights_data": solar_api_client.insights_data,
|
"insights_data": solar_api_client.insights_data,
|
||||||
"panel_performance": solar_api_client.panel_performance,
|
"panel_performance": solar_api_client.panel_performance,
|
||||||
"unit_share_of_energy": 1
|
"n_units": len([entry for entry in building_ids if entry['building_id'] == building_id])
|
||||||
},
|
}
|
||||||
roof_area=solar_api_client.roof_area
|
|
||||||
)
|
# Store the data in the database
|
||||||
|
# TODO: Rather than just doing a straight insert, we should overwrite what's already there if it
|
||||||
|
# exists
|
||||||
|
solar_api_client.save_to_db(
|
||||||
|
session=session, uprns_to_location=building_uprns[building_id], scenario_type="building"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Insert this into the properties that have this building id
|
||||||
|
for p in input_properties:
|
||||||
|
if p.building_id == building_id:
|
||||||
|
unit_solar_panel_configuration = solar_panel_configuration[building_id].copy()
|
||||||
|
|
||||||
|
unit_solar_panel_configuration["unit_share_of_energy"] = (
|
||||||
|
[x for x in building_ids if x["property_id"] == p.id][0]["energy_consumption"] /
|
||||||
|
energy_consumption
|
||||||
|
)
|
||||||
|
p.set_solar_panel_configuration(unit_solar_panel_configuration)
|
||||||
|
|
||||||
|
if individual_units:
|
||||||
|
# Model the solar potential at the property level
|
||||||
|
for unit in individual_units:
|
||||||
|
property_instance = [p for p in input_properties if p.id == unit["property_id"]][0]
|
||||||
|
# At this level, we check if the property is suitable for solar and if now, skip
|
||||||
|
if not property_instance.is_solar_pv_valid():
|
||||||
|
continue
|
||||||
|
|
||||||
|
solar_api_client.get(
|
||||||
|
longitude=unit["longitude"],
|
||||||
|
latitude=unit["latitude"],
|
||||||
|
energy_consumption=unit["energy_consumption"],
|
||||||
|
is_building=False,
|
||||||
|
session=session,
|
||||||
|
uprn=unit["uprn"],
|
||||||
|
property_instance=property_instance
|
||||||
|
)
|
||||||
|
|
||||||
|
# Store the data in the database
|
||||||
|
# TODO: Rather than just doing a straight insert, we should overwrite what's already there if it
|
||||||
|
# exists
|
||||||
|
solar_api_client.save_to_db(
|
||||||
|
session=session,
|
||||||
|
uprns_to_location=[
|
||||||
|
{
|
||||||
|
"uprn": property_instance.uprn,
|
||||||
|
"longitude": property_instance.spatial["longitude"],
|
||||||
|
"latitude": property_instance.spatial["latitude"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
scenario_type="unit"
|
||||||
|
)
|
||||||
|
|
||||||
|
property_instance.set_solar_panel_configuration(
|
||||||
|
solar_panel_configuration={
|
||||||
|
"insights_data": solar_api_client.insights_data,
|
||||||
|
"panel_performance": solar_api_client.panel_performance,
|
||||||
|
"unit_share_of_energy": 1
|
||||||
|
},
|
||||||
|
roof_area=solar_api_client.roof_area
|
||||||
|
)
|
||||||
|
|
||||||
logger.info("Getting components and epc recommendations")
|
logger.info("Getting components and epc recommendations")
|
||||||
recommendations = {}
|
recommendations = {}
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,8 @@ class PlanTriggerRequest(BaseModel):
|
||||||
"solar_pv",
|
"solar_pv",
|
||||||
# Specific measures
|
# Specific measures
|
||||||
"air_source_heat_pump",
|
"air_source_heat_pump",
|
||||||
|
"internal_wall_insulation",
|
||||||
|
"external_wall_insulation"
|
||||||
}
|
}
|
||||||
|
|
||||||
_allowed_goals = {"Increasing EPC"}
|
_allowed_goals = {"Increasing EPC"}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ from sklearn.metrics import mean_squared_error, r2_score, mean_absolute_percenta
|
||||||
from sklearn.feature_selection import RFECV
|
from sklearn.feature_selection import RFECV
|
||||||
from utils.s3 import save_pickle_to_s3, read_pickle_from_s3, read_dataframe_from_s3_parquet, read_csv_from_s3
|
from utils.s3 import save_pickle_to_s3, read_pickle_from_s3, read_dataframe_from_s3_parquet, read_csv_from_s3
|
||||||
from utils.logger import setup_logger
|
from utils.logger import setup_logger
|
||||||
|
from backend.Property import Property
|
||||||
|
|
||||||
logger = setup_logger()
|
logger = setup_logger()
|
||||||
|
|
||||||
|
|
@ -506,6 +507,57 @@ class EnergyConsumptionModel:
|
||||||
|
|
||||||
return prediction
|
return prediction
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def prepare_new_data(p: Property):
|
||||||
|
"""
|
||||||
|
Given an instance of the property class, this method will ensure that the EPC is ready for scoring with the
|
||||||
|
kwh models. In the backend, we perform some cleaning and transformation on an EPC so we just ensure that the
|
||||||
|
data is in the format required by the model
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
|
||||||
|
epc = p.data.copy()
|
||||||
|
numeric_cols = [
|
||||||
|
'current-energy-efficiency',
|
||||||
|
'potential-energy-efficiency', 'environment-impact-current',
|
||||||
|
'environment-impact-potential', 'energy-consumption-current',
|
||||||
|
'energy-consumption-potential', 'co2-emissions-current',
|
||||||
|
'co2-emiss-curr-per-floor-area', 'co2-emissions-potential',
|
||||||
|
'lighting-cost-current', 'lighting-cost-potential',
|
||||||
|
'heating-cost-current', 'heating-cost-potential',
|
||||||
|
'hot-water-cost-current', 'hot-water-cost-potential',
|
||||||
|
'total-floor-area', 'multi-glaze-proportion',
|
||||||
|
'extension-count', 'number-habitable-rooms', 'number-heated-rooms',
|
||||||
|
'low-energy-lighting', 'number-open-fireplaces',
|
||||||
|
'wind-turbine-count', 'unheated-corridor-length',
|
||||||
|
'floor-height', 'photo-supply', 'fixed-lighting-outlets-count',
|
||||||
|
'low-energy-fixed-light-count',
|
||||||
|
]
|
||||||
|
for v in numeric_cols:
|
||||||
|
if epc[v] is not None:
|
||||||
|
epc[v] = float(epc[v])
|
||||||
|
|
||||||
|
bools_to_remap = ['mains-gas-flag', 'flat-top-storey']
|
||||||
|
bool_map = {
|
||||||
|
True: "Y",
|
||||||
|
False: "N",
|
||||||
|
None: "N",
|
||||||
|
"Y": "Y",
|
||||||
|
"N": "N"
|
||||||
|
}
|
||||||
|
for v in bools_to_remap:
|
||||||
|
epc[v] = bool_map[epc[v]]
|
||||||
|
|
||||||
|
no_data = {
|
||||||
|
"floor-level": "NODATA!",
|
||||||
|
"floor-energy-eff": "NO DATA!"
|
||||||
|
}
|
||||||
|
for v, fill_val in no_data.items():
|
||||||
|
if pd.isnull(epc[v]):
|
||||||
|
epc[v] = fill_val
|
||||||
|
|
||||||
|
return epc
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def calculate_percentage_decrease(start_efficiency, end_efficiency, consumption_averages):
|
def calculate_percentage_decrease(start_efficiency, end_efficiency, consumption_averages):
|
||||||
|
|
||||||
|
|
|
||||||
0
etl/customers/newhaven/__init__.py
Normal file
0
etl/customers/newhaven/__init__.py
Normal file
249
etl/customers/newhaven/newhaven_study.py
Normal file
249
etl/customers/newhaven/newhaven_study.py
Normal file
|
|
@ -0,0 +1,249 @@
|
||||||
|
import inspect
|
||||||
|
import pandas as pd
|
||||||
|
from etl.epc.settings import EARLIEST_EPC_DATE
|
||||||
|
from pathlib import Path
|
||||||
|
import numpy as np
|
||||||
|
from utils.s3 import save_csv_to_s3
|
||||||
|
|
||||||
|
src_file_path = inspect.getfile(lambda: None)
|
||||||
|
|
||||||
|
EPC_DIRECTORY = Path(src_file_path).parent / "local_data" / "all-domestic-certificates"
|
||||||
|
CUSTOMER_DATA_DIRECTORY = "/Users/khalimconn-kowlessar/Documents/hestia/Customers/Newhaven/Data"
|
||||||
|
|
||||||
|
USER_ID = 8
|
||||||
|
PORTFOLIO_ID = 88
|
||||||
|
|
||||||
|
|
||||||
|
def make_asset_list():
|
||||||
|
"""
|
||||||
|
Set up a small asset list for the study
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Read in EPC data for Lewes
|
||||||
|
lewes_directory = EPC_DIRECTORY / "domestic-E07000063-Lewes/certificates.csv"
|
||||||
|
epc_data = pd.read_csv(lewes_directory, low_memory=False)
|
||||||
|
# Rename the columns to the same format as the api returns
|
||||||
|
epc_data.columns = [c.replace("_", "-").lower() for c in epc_data.columns]
|
||||||
|
|
||||||
|
# Take just date before the date threshold
|
||||||
|
epc_data = epc_data[epc_data["lodgement-date"] >= EARLIEST_EPC_DATE]
|
||||||
|
|
||||||
|
epc_data = epc_data[~pd.isnull(epc_data["uprn"])]
|
||||||
|
epc_data["uprn"] = epc_data["uprn"].astype(int).astype(str)
|
||||||
|
# /Users/khalimconn-kowlessar/Documents/hestia/Customers/Newhaven/Data/
|
||||||
|
# We read in the multiple data sources
|
||||||
|
address_base = pd.read_csv(
|
||||||
|
f"{CUSTOMER_DATA_DIRECTORY}/OS AddressBase Premium/OS AddressBase Premium.csv",
|
||||||
|
low_memory=False,
|
||||||
|
)
|
||||||
|
# Filter on resi
|
||||||
|
address_base = address_base[address_base["Primary Code Description"] == "Residential"]
|
||||||
|
address_base["UPRN"] = address_base["UPRN"].astype(int).astype(str)
|
||||||
|
|
||||||
|
pv_potential = pd.read_csv(
|
||||||
|
f"{CUSTOMER_DATA_DIRECTORY}/Domestic Rooftop PV Potential/Domestic Rooftop PV Potential.csv",
|
||||||
|
low_memory=False,
|
||||||
|
)
|
||||||
|
pv_potential["UPRN"] = pv_potential["UPRN"].astype(int).astype(str)
|
||||||
|
|
||||||
|
ashp_potential = pd.read_csv(
|
||||||
|
f"{CUSTOMER_DATA_DIRECTORY}/Air Source Heat Pump Potential/Air Source Heat Pump Potential.csv",
|
||||||
|
low_memory=False,
|
||||||
|
)
|
||||||
|
ashp_potential["UPRN"] = ashp_potential["UPRN"].astype(int).astype(str)
|
||||||
|
|
||||||
|
insulation_potential = pd.read_csv(
|
||||||
|
f"{CUSTOMER_DATA_DIRECTORY}/Insulation Potential/Insulation Potential.csv",
|
||||||
|
low_memory=False,
|
||||||
|
)
|
||||||
|
insulation_potential["UPRN"] = insulation_potential["UPRN"].astype(int).astype(str)
|
||||||
|
|
||||||
|
renewables_cost = pd.read_csv(
|
||||||
|
f"{CUSTOMER_DATA_DIRECTORY}/Low Carbon Technology Costs/Low Carbon Technology Costs.csv",
|
||||||
|
low_memory=False,
|
||||||
|
)
|
||||||
|
renewables_cost["UPRN"] = renewables_cost["UPRN"].astype(int).astype(str)
|
||||||
|
|
||||||
|
# Merge the EPC data onto address base
|
||||||
|
asset_list = address_base[
|
||||||
|
[
|
||||||
|
"UPRN", "Class Description", "Relative Height - Eaves",
|
||||||
|
]
|
||||||
|
].merge(
|
||||||
|
epc_data[
|
||||||
|
["uprn", "current-energy-efficiency", "current-energy-rating", "address1", "postcode", "floor-height",
|
||||||
|
"property-type", "built-form"]],
|
||||||
|
how="left",
|
||||||
|
left_on="UPRN",
|
||||||
|
right_on="uprn"
|
||||||
|
).drop(
|
||||||
|
columns=["uprn"]
|
||||||
|
).merge(
|
||||||
|
insulation_potential[["UPRN", "EPC Rating", "Wall Area [m^2]", "Building Area [m^2]"]],
|
||||||
|
how="left",
|
||||||
|
on="UPRN"
|
||||||
|
).rename(
|
||||||
|
columns={"Wall Area [m^2]": "insulation_wall_area", "Building Area [m^2]": "floor_area"}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Take properties below a B - there are 2844 units
|
||||||
|
asset_list = asset_list[asset_list["current-energy-efficiency"].astype(float) <= 80]
|
||||||
|
# Drop caravans
|
||||||
|
asset_list = asset_list[asset_list["Class Description"] != "Caravan"]
|
||||||
|
asset_list = asset_list[~pd.isnull(asset_list["current-energy-efficiency"])]
|
||||||
|
|
||||||
|
# Take a 10% sample, for properties that have an EPC, with a seed
|
||||||
|
asset_list = asset_list.sample(frac=0.1, random_state=42)
|
||||||
|
|
||||||
|
AVG_FLOOR_HEIGHT = asset_list["floor-height"].median()
|
||||||
|
|
||||||
|
def estimate_n_floors(
|
||||||
|
building_height, floor_height, address_base_property_description, epc_property_type,
|
||||||
|
):
|
||||||
|
|
||||||
|
if address_base_property_description == "Self Contained Flat (Includes Maisonette / Apartment)":
|
||||||
|
if epc_property_type == "Flat":
|
||||||
|
return 1
|
||||||
|
if epc_property_type == "House":
|
||||||
|
return 2
|
||||||
|
return NotImplementedError("Implement me")
|
||||||
|
|
||||||
|
if pd.isnull(floor_height):
|
||||||
|
return np.round(building_height / AVG_FLOOR_HEIGHT)
|
||||||
|
|
||||||
|
return np.round(building_height / floor_height)
|
||||||
|
|
||||||
|
# Estimate the number of floors
|
||||||
|
asset_list["number_of_floors"] = asset_list.apply(
|
||||||
|
lambda x: estimate_n_floors(
|
||||||
|
building_height=x["Relative Height - Eaves"],
|
||||||
|
floor_height=x["floor-height"],
|
||||||
|
address_base_property_description=x["Class Description"],
|
||||||
|
epc_property_type=x["property-type"],
|
||||||
|
),
|
||||||
|
axis=1
|
||||||
|
)
|
||||||
|
|
||||||
|
# D 0.419929
|
||||||
|
# C 0.391459
|
||||||
|
# E 0.160142
|
||||||
|
# F 0.017794
|
||||||
|
# G 0.010676
|
||||||
|
|
||||||
|
# Total asset list:
|
||||||
|
# D 0.450409
|
||||||
|
# C 0.412016
|
||||||
|
# E 0.110203
|
||||||
|
# F 0.020263
|
||||||
|
# G 0.007110
|
||||||
|
|
||||||
|
# We do the followings:
|
||||||
|
# 1) Create final asset list
|
||||||
|
# 2) Create Non-intrusive recommendations
|
||||||
|
# 3) Create a third party costing object
|
||||||
|
|
||||||
|
cost_testing = renewables_cost.merge(
|
||||||
|
insulation_potential, how="inner", on="UPRN"
|
||||||
|
)
|
||||||
|
|
||||||
|
cost_testing["cwi_cost_per_m2"] = cost_testing["Insulation - Cavity Wall - Total"] / cost_testing["Wall Area [m^2]"]
|
||||||
|
# Their cavity wall insulation is £8 per m^2
|
||||||
|
|
||||||
|
cost_testing["ewi_cost_per_m2"] = cost_testing["Insulation - External Wall - Total"] / cost_testing[
|
||||||
|
"Wall Area [m^2]"]
|
||||||
|
|
||||||
|
final_asset_list = asset_list.rename(
|
||||||
|
columns={"UPRN": "uprn", "address1": "address", "floor_area": "insulation_floor_area"}
|
||||||
|
)[["uprn", "address", "postcode", "insulation_wall_area", "insulation_floor_area", "number_of_floors"]]
|
||||||
|
|
||||||
|
# Create non-invasive recommendations, which come from the solar potential and ASHP potential data sources
|
||||||
|
non_invasive_recommendations = []
|
||||||
|
for _, row in final_asset_list.iterrows():
|
||||||
|
property_ashp_potential = ashp_potential[
|
||||||
|
(ashp_potential["UPRN"] == row["uprn"]) & ashp_potential["Overall Suitability Rating"]
|
||||||
|
]
|
||||||
|
property_pv_potential = pv_potential[
|
||||||
|
(pv_potential["UPRN"] == row["uprn"]) & pv_potential["Overall Suitability"]
|
||||||
|
]
|
||||||
|
property_costs = renewables_cost[renewables_cost["UPRN"] == row["uprn"]]
|
||||||
|
|
||||||
|
property_non_invasive_recs = []
|
||||||
|
if not property_ashp_potential.empty:
|
||||||
|
property_non_invasive_recs.append(
|
||||||
|
{
|
||||||
|
"type": "air_source_heat_pump",
|
||||||
|
"size": property_ashp_potential["Recommended Heat Pump Size [kW]"].values[0],
|
||||||
|
"cost": property_costs["Air Source Heat Pump - Total"].values[0],
|
||||||
|
"ashp_only_heating_recommendation": True
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if not property_pv_potential.empty:
|
||||||
|
property_non_invasive_recs.append(
|
||||||
|
{
|
||||||
|
"type": "solar_pv",
|
||||||
|
"array_wattage": property_pv_potential["Recommended Array Size [kW]"].values[0] * 1000,
|
||||||
|
"initial_ac_kwh_per_year": property_pv_potential["Annual Generation [kWh]"].values[0],
|
||||||
|
"panneled_roof_area": property_pv_potential["Roof area suitable for PV [m^2]"].values[0],
|
||||||
|
"cost": property_costs["Rooftop PV - Total"].values[0],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
non_invasive_recommendations.append(
|
||||||
|
{
|
||||||
|
"uprn": row["uprn"],
|
||||||
|
"recommendations": property_non_invasive_recs,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Save the asset list
|
||||||
|
|
||||||
|
# Store the asset list in s3
|
||||||
|
filename = f"{USER_ID}/{PORTFOLIO_ID}/pilot.csv"
|
||||||
|
save_csv_to_s3(
|
||||||
|
dataframe=final_asset_list,
|
||||||
|
bucket_name="retrofit-plan-inputs-dev",
|
||||||
|
file_name=filename
|
||||||
|
)
|
||||||
|
|
||||||
|
# Store non-invasive recommendations in S3
|
||||||
|
non_invasive_recommendations_filename = f"{USER_ID}/{PORTFOLIO_ID}/non_invasive_recommendations.csv"
|
||||||
|
save_csv_to_s3(
|
||||||
|
dataframe=pd.DataFrame(non_invasive_recommendations),
|
||||||
|
bucket_name="retrofit-plan-inputs-dev",
|
||||||
|
file_name=non_invasive_recommendations_filename
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create two scenarios
|
||||||
|
# Scenario A
|
||||||
|
body1 = {
|
||||||
|
"portfolio_id": str(PORTFOLIO_ID),
|
||||||
|
"housing_type": "Private",
|
||||||
|
"goal": "Increasing EPC",
|
||||||
|
"goal_value": "A",
|
||||||
|
"trigger_file_path": filename,
|
||||||
|
"already_installed_file_path": "",
|
||||||
|
"patches_file_path": "",
|
||||||
|
"non_invasive_recommendations_file_path": non_invasive_recommendations_filename,
|
||||||
|
"scenario_name": "Fabric - no solid wall",
|
||||||
|
"multi_plan": True,
|
||||||
|
"exclusions": ["internal_wall_insulation", "external_wall_insulation", "floor_insulation"],
|
||||||
|
"budget": None,
|
||||||
|
}
|
||||||
|
print(body1)
|
||||||
|
|
||||||
|
# Scenario B - deep fabric, no exclusions
|
||||||
|
body2 = {
|
||||||
|
"portfolio_id": str(PORTFOLIO_ID),
|
||||||
|
"housing_type": "Private",
|
||||||
|
"goal": "Increasing EPC",
|
||||||
|
"goal_value": "A",
|
||||||
|
"trigger_file_path": filename,
|
||||||
|
"already_installed_file_path": "",
|
||||||
|
"patches_file_path": "",
|
||||||
|
"non_invasive_recommendations_file_path": non_invasive_recommendations_filename,
|
||||||
|
"scenario_name": "Deep Fabric",
|
||||||
|
"multi_plan": True,
|
||||||
|
"budget": None,
|
||||||
|
}
|
||||||
|
print(body2)
|
||||||
60
etl/testing_data/bills_model_testing.py
Normal file
60
etl/testing_data/bills_model_testing.py
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
# We use some sample properties from Newhaven to use as a testing dataset for implementing the model fixes
|
||||||
|
|
||||||
|
|
||||||
|
import inspect
|
||||||
|
import pandas as pd
|
||||||
|
from etl.epc.settings import EARLIEST_EPC_DATE
|
||||||
|
from pathlib import Path
|
||||||
|
from utils.s3 import save_csv_to_s3
|
||||||
|
|
||||||
|
src_file_path = inspect.getfile(lambda: None)
|
||||||
|
|
||||||
|
EPC_DIRECTORY = Path(src_file_path).parent / "local_data" / "all-domestic-certificates"
|
||||||
|
|
||||||
|
USER_ID = 8
|
||||||
|
PORTFOLIO_ID = -1
|
||||||
|
|
||||||
|
|
||||||
|
def app():
|
||||||
|
"""
|
||||||
|
This application is tasked with pulling a large quantity of data from the find my epc website, containing the
|
||||||
|
estimated energy consumption for properties
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
|
||||||
|
lewes_directory = EPC_DIRECTORY / "domestic-E07000063-Lewes/certificates.csv"
|
||||||
|
|
||||||
|
data = pd.read_csv(lewes_directory, low_memory=False)
|
||||||
|
# Rename the columns to the same format as the api returns
|
||||||
|
data.columns = [c.replace("_", "-").lower() for c in data.columns]
|
||||||
|
|
||||||
|
# Take just date before the date threshold
|
||||||
|
data = data[data["lodgement-date"] >= EARLIEST_EPC_DATE]
|
||||||
|
|
||||||
|
data = data[~pd.isnull(data["uprn"])]
|
||||||
|
data = data[data["current-energy-efficiency"].astype(float) < 52]
|
||||||
|
data = data.sample(10)
|
||||||
|
|
||||||
|
# Create an asset list
|
||||||
|
asset_list = data[["uprn", "address1", "postcode"]].copy().rename(columns={"address1": "address"})
|
||||||
|
asset_list["uprn"] = asset_list["uprn"].astype(str)
|
||||||
|
|
||||||
|
filename = f"{USER_ID}/{PORTFOLIO_ID}/pilot.csv"
|
||||||
|
save_csv_to_s3(
|
||||||
|
dataframe=asset_list,
|
||||||
|
bucket_name="retrofit-plan-inputs-dev",
|
||||||
|
file_name=filename
|
||||||
|
)
|
||||||
|
|
||||||
|
body = {
|
||||||
|
"portfolio_id": str(PORTFOLIO_ID),
|
||||||
|
"housing_type": "Private",
|
||||||
|
"goal": "Increasing EPC",
|
||||||
|
"goal_value": "B",
|
||||||
|
"trigger_file_path": filename,
|
||||||
|
"already_installed_file_path": "",
|
||||||
|
"patches_file_path": "",
|
||||||
|
"non_invasive_recommendations_file_path": "",
|
||||||
|
"budget": None,
|
||||||
|
}
|
||||||
|
print(body)
|
||||||
|
|
@ -1014,7 +1014,7 @@ class Costs:
|
||||||
"labour_days": labour_days
|
"labour_days": labour_days
|
||||||
}
|
}
|
||||||
|
|
||||||
def solar_pv(self, wattage: float, has_battery: bool = False):
|
def solar_pv(self, wattage: float, has_battery: bool = False, array_cost=None):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Calculates the total cost for solar PV based data provided by the MCS dashboard, which contains
|
Calculates the total cost for solar PV based data provided by the MCS dashboard, which contains
|
||||||
|
|
@ -1028,13 +1028,17 @@ class Costs:
|
||||||
https://www.checkatrade.com/blog/cost-guides/cost-of-solar-panel-installation/
|
https://www.checkatrade.com/blog/cost-guides/cost-of-solar-panel-installation/
|
||||||
:param wattage: Peak wattage of the solar PV system]
|
:param wattage: Peak wattage of the solar PV system]
|
||||||
:param has_battery: Bool, whether the system includes a battery
|
:param has_battery: Bool, whether the system includes a battery
|
||||||
|
:param array_cost: float, containing the cost of the solar PV array
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Get the cost data relevant to the region
|
# Get the cost data relevant to the region
|
||||||
regional_cost = MCS_SOLAR_PV_COST_DATA["-".join(["average_cost_per_kwh", self.region])]
|
regional_cost = MCS_SOLAR_PV_COST_DATA["-".join(["average_cost_per_kwh", self.region])]
|
||||||
|
|
||||||
kw = wattage / 1000
|
if array_cost is not None:
|
||||||
total_cost = kw * regional_cost
|
total_cost = array_cost
|
||||||
|
else:
|
||||||
|
kw = wattage / 1000
|
||||||
|
total_cost = kw * regional_cost
|
||||||
|
|
||||||
if has_battery:
|
if has_battery:
|
||||||
# The battery cost is based on the £3500 quote, recieved from installers
|
# The battery cost is based on the £3500 quote, recieved from installers
|
||||||
|
|
|
||||||
|
|
@ -57,13 +57,20 @@ class HeatingRecommender:
|
||||||
# in the Costs class, stored as SYSTEM_FLUSH_COST
|
# in the Costs class, stored as SYSTEM_FLUSH_COST
|
||||||
|
|
||||||
exclusions = [] if exclusions is None else exclusions
|
exclusions = [] if exclusions is None else exclusions
|
||||||
|
non_invasive_ashp_recommendation = next(
|
||||||
|
(r for r in self.property.non_invasive_recommendations if r["type"] == "air_source_heat_pump"), {}
|
||||||
|
)
|
||||||
|
|
||||||
|
# This option will prevent other heating recommendations from being specified, other than an ASHP
|
||||||
|
ashp_only_heating_recommendation = non_invasive_ashp_recommendation.get(
|
||||||
|
"ashp_only_heating_recommendation", False
|
||||||
|
)
|
||||||
self.heating_recommendations = []
|
self.heating_recommendations = []
|
||||||
self.heating_control_recommendations = []
|
self.heating_control_recommendations = []
|
||||||
# This first iteration of the recommender will provide very basic recommendation
|
# This first iteration of the recommender will provide very basic recommendation
|
||||||
# We recommend heating controls based on the main heating system
|
# We recommend heating controls based on the main heating system
|
||||||
|
|
||||||
if self.is_high_heat_retention_valid():
|
if self.is_high_heat_retention_valid() and not ashp_only_heating_recommendation:
|
||||||
# Recommend high heat retention storage heaters
|
# Recommend high heat retention storage heaters
|
||||||
# TODO: We need to allow for the possibility that the property aleady has storage heaters, but just
|
# TODO: We need to allow for the possibility that the property aleady has storage heaters, but just
|
||||||
# needs the controls
|
# needs the controls
|
||||||
|
|
@ -91,13 +98,13 @@ class HeatingRecommender:
|
||||||
self.property.data["mains-gas-flag"]
|
self.property.data["mains-gas-flag"]
|
||||||
)
|
)
|
||||||
|
|
||||||
if (
|
if ((
|
||||||
has_boiler or
|
has_boiler or
|
||||||
no_heating_has_mains or
|
no_heating_has_mains or
|
||||||
electic_heating_has_mains or
|
electic_heating_has_mains or
|
||||||
has_gas_heaters or
|
has_gas_heaters or
|
||||||
portable_heaters_has_mains
|
portable_heaters_has_mains
|
||||||
):
|
) and not ashp_only_heating_recommendation):
|
||||||
# This indicates that the home previously did not have a boiler in place and so would require
|
# This indicates that the home previously did not have a boiler in place and so would require
|
||||||
# an overhaul to the system - right now, this is all reasons, apart from if there is an existing boiler
|
# an overhaul to the system - right now, this is all reasons, apart from if there is an existing boiler
|
||||||
system_change = not has_boiler
|
system_change = not has_boiler
|
||||||
|
|
@ -118,7 +125,9 @@ class HeatingRecommender:
|
||||||
|
|
||||||
if self.property.is_ashp_valid(exclusions=exclusions):
|
if self.property.is_ashp_valid(exclusions=exclusions):
|
||||||
self.recommend_air_source_heat_pump(
|
self.recommend_air_source_heat_pump(
|
||||||
phase=phase, has_cavity_or_loft_recommendations=has_cavity_or_loft_recommendations
|
phase=phase,
|
||||||
|
has_cavity_or_loft_recommendations=has_cavity_or_loft_recommendations,
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
@ -194,14 +203,21 @@ class HeatingRecommender:
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# Look for a non-intrusive recommendation
|
||||||
|
non_intrusive_recommendation = next((
|
||||||
|
r for r in self.property.non_invasive_recommendations if r["type"] == "air_source_heat_pump"
|
||||||
|
), {})
|
||||||
|
|
||||||
controls_recommender = HeatingControlRecommender(self.property)
|
controls_recommender = HeatingControlRecommender(self.property)
|
||||||
controls_recommender.recommend(heating_description="Air source heat pump, radiators, electric")
|
controls_recommender.recommend(heating_description="Air source heat pump, radiators, electric")
|
||||||
|
|
||||||
ashp_costs = self.costs.air_source_heat_pump()
|
ashp_costs = self.costs.air_source_heat_pump()
|
||||||
# We add the costs of the heating controls, onto each key in the costs dictionary
|
if non_intrusive_recommendation:
|
||||||
if controls_recommender.recommendation:
|
# Update with non-intrusive recommendation
|
||||||
for key in ashp_costs:
|
if non_intrusive_recommendation.get("cost"):
|
||||||
ashp_costs[key] += controls_recommender.recommendation[0][key]
|
ashp_costs.update(
|
||||||
|
{"total": non_intrusive_recommendation["cost"], "subtotal": None, "vat": None}
|
||||||
|
)
|
||||||
|
|
||||||
already_installed = "air_source_heat_pump" in self.property.already_installed
|
already_installed = "air_source_heat_pump" in self.property.already_installed
|
||||||
|
|
||||||
|
|
@ -213,6 +229,14 @@ class HeatingRecommender:
|
||||||
if already_installed:
|
if already_installed:
|
||||||
ashp_costs = override_costs(ashp_costs)
|
ashp_costs = override_costs(ashp_costs)
|
||||||
|
|
||||||
|
if non_intrusive_recommendation and not all([x is None for x in controls_recommendations]):
|
||||||
|
# We just use the ttzc control
|
||||||
|
controls_recommendations = [
|
||||||
|
x for x in controls_recommendations if (
|
||||||
|
x["description_simulation"]["mainheatcont-description"] == "Time and temperature zone control"
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
# This is a map from the heating controls description to the description of the air source heat pump set up
|
# This is a map from the heating controls description to the description of the air source heat pump set up
|
||||||
ashp_descriptions = {
|
ashp_descriptions = {
|
||||||
"Time and temperature zone control": (
|
"Time and temperature zone control": (
|
||||||
|
|
@ -233,7 +257,8 @@ class HeatingRecommender:
|
||||||
|
|
||||||
if controls_rec:
|
if controls_rec:
|
||||||
for key in ashp_costs_with_controls:
|
for key in ashp_costs_with_controls:
|
||||||
ashp_costs_with_controls[key] += controls_rec[key]
|
if ashp_costs_with_controls[key] is not None:
|
||||||
|
ashp_costs_with_controls[key] += controls_rec[key]
|
||||||
|
|
||||||
if controls_rec is None:
|
if controls_rec is None:
|
||||||
description = "Install an air source heat pump."
|
description = "Install an air source heat pump."
|
||||||
|
|
@ -245,19 +270,19 @@ class HeatingRecommender:
|
||||||
# If the property does not have existing cavity and loft insulation, we include a note that the cost
|
# If the property does not have existing cavity and loft insulation, we include a note that the cost
|
||||||
# includes the boiler upgrade scheme and that the cavity and loft need to be treated, to ensure access
|
# includes the boiler upgrade scheme and that the cavity and loft need to be treated, to ensure access
|
||||||
# to the funding
|
# to the funding
|
||||||
if has_cavity_or_loft_recommendations:
|
if not non_intrusive_recommendation:
|
||||||
description = description + (
|
if has_cavity_or_loft_recommendations:
|
||||||
f" The cost includes the £"
|
description = description + (
|
||||||
f"{BOILER_UPGRADE_SCHEME_ASHP_VALUE} boiler upgrade scheme grant. "
|
f" The cost includes the £"
|
||||||
f"You must ensure that the property has an insulated cavity and "
|
f"{BOILER_UPGRADE_SCHEME_ASHP_VALUE} boiler upgrade scheme grant. "
|
||||||
f"270mm+ loft insulation to qualify for the grant"
|
f"You must ensure that the property has an insulated cavity and "
|
||||||
)
|
f"270mm+ loft insulation to qualify for the grant"
|
||||||
else:
|
)
|
||||||
description = description + (
|
else:
|
||||||
f" The cost includes the £{BOILER_UPGRADE_SCHEME_ASHP_VALUE} boiler upgrade scheme grant"
|
description = description + (
|
||||||
)
|
f" The cost includes the £{BOILER_UPGRADE_SCHEME_ASHP_VALUE} boiler upgrade scheme grant"
|
||||||
|
)
|
||||||
|
|
||||||
print("TEMP UPDATED FOR 77 Perryn!!!!!")
|
|
||||||
simulation_config = {
|
simulation_config = {
|
||||||
"mainheat_energy_eff_ending": "Good",
|
"mainheat_energy_eff_ending": "Good",
|
||||||
"hot_water_energy_eff_ending": "Good"
|
"hot_water_energy_eff_ending": "Good"
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,7 @@ class Recommendations:
|
||||||
|
|
||||||
# Building Fabric
|
# Building Fabric
|
||||||
if "wall_insulation" not in self.exclusions:
|
if "wall_insulation" not in self.exclusions:
|
||||||
self.wall_recomender.recommend(phase=phase)
|
self.wall_recomender.recommend(phase=phase, exclusions=self.exclusions)
|
||||||
if self.wall_recomender.recommendations:
|
if self.wall_recomender.recommendations:
|
||||||
property_recommendations.append(self.wall_recomender.recommendations)
|
property_recommendations.append(self.wall_recomender.recommendations)
|
||||||
phase += 1
|
phase += 1
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
from recommendations.Costs import Costs
|
from recommendations.Costs import Costs
|
||||||
from recommendations.recommendation_utils import override_costs
|
from recommendations.recommendation_utils import override_costs, esimtate_pitched_roof_area
|
||||||
|
|
||||||
|
|
||||||
class SolarPvRecommendations:
|
class SolarPvRecommendations:
|
||||||
|
|
@ -150,17 +152,37 @@ class SolarPvRecommendations:
|
||||||
self.recommend_building_analysis(phase)
|
self.recommend_building_analysis(phase)
|
||||||
return
|
return
|
||||||
|
|
||||||
panel_performance = self.property.solar_panel_configuration["panel_performance"]
|
non_invasive_recommendation = next(
|
||||||
roof_area = self.property.roof_area
|
(r for r in self.property.non_invasive_recommendations if r["type"] == "solar_pv"), {}
|
||||||
|
)
|
||||||
|
|
||||||
solar_configurations = panel_performance.head(3).reset_index(drop=True)
|
if non_invasive_recommendation:
|
||||||
|
|
||||||
|
roof_area = esimtate_pitched_roof_area(
|
||||||
|
floor_area=self.property.insulation_floor_area, floor_height=self.property.data["floor-height"]
|
||||||
|
)
|
||||||
|
solar_configurations = pd.DataFrame(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"array_wattage": non_invasive_recommendation["array_wattage"],
|
||||||
|
"initial_ac_kwh_per_year": non_invasive_recommendation["initial_ac_kwh_per_year"],
|
||||||
|
"panneled_roof_area": non_invasive_recommendation["panneled_roof_area"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
panel_performance = self.property.solar_panel_configuration["panel_performance"]
|
||||||
|
roof_area = self.property.roof_area
|
||||||
|
solar_configurations = panel_performance.head(3).reset_index(drop=True)
|
||||||
|
|
||||||
# We combine each of these configurations with estimates with and without a battery
|
# We combine each of these configurations with estimates with and without a battery
|
||||||
for rank, recommendation_config in solar_configurations.iterrows():
|
for rank, recommendation_config in solar_configurations.iterrows():
|
||||||
roof_coverage_percent = round(recommendation_config["panneled_roof_area"] / roof_area * 100)
|
roof_coverage_percent = round(recommendation_config["panneled_roof_area"] / roof_area * 100)
|
||||||
for has_battery in [False, True]:
|
for has_battery in [False, True]:
|
||||||
cost_result = self.costs.solar_pv(
|
cost_result = self.costs.solar_pv(
|
||||||
wattage=recommendation_config["array_wattage"], has_battery=has_battery
|
wattage=recommendation_config["array_wattage"],
|
||||||
|
has_battery=has_battery,
|
||||||
|
array_cost=non_invasive_recommendation["cost"] if non_invasive_recommendation else None
|
||||||
)
|
)
|
||||||
kw = np.floor(recommendation_config["array_wattage"] / 100) / 10
|
kw = np.floor(recommendation_config["array_wattage"] / 100) / 10
|
||||||
if has_battery:
|
if has_battery:
|
||||||
|
|
|
||||||
|
|
@ -184,7 +184,7 @@ class WallRecommendations(Definitions):
|
||||||
|
|
||||||
return ewi_recommendations
|
return ewi_recommendations
|
||||||
|
|
||||||
def recommend(self, phase=0):
|
def recommend(self, phase=0, exclusions=None):
|
||||||
# if building built after 1990 + we're able to identify U-value +
|
# if building built after 1990 + we're able to identify U-value +
|
||||||
# U-value less than 0.18 and if in or close to a conversation area,
|
# U-value less than 0.18 and if in or close to a conversation area,
|
||||||
# recommend internal wall insulation as a possible measure
|
# recommend internal wall insulation as a possible measure
|
||||||
|
|
@ -262,7 +262,7 @@ class WallRecommendations(Definitions):
|
||||||
|
|
||||||
# Remaining wall types are treated with IWI or EWI
|
# Remaining wall types are treated with IWI or EWI
|
||||||
if (u_value >= self.BUILDING_REGULATIONS_PART_L_MAX_U_VALUE) and self.is_suitable_for_solid_insulation():
|
if (u_value >= self.BUILDING_REGULATIONS_PART_L_MAX_U_VALUE) and self.is_suitable_for_solid_insulation():
|
||||||
self.find_insulation(u_value, phase)
|
self.find_insulation(u_value, phase, exclusions=exclusions)
|
||||||
return
|
return
|
||||||
|
|
||||||
# If the u-value is within regulations, we don't do anything
|
# If the u-value is within regulations, we don't do anything
|
||||||
|
|
@ -552,7 +552,7 @@ class WallRecommendations(Definitions):
|
||||||
|
|
||||||
return recommendations
|
return recommendations
|
||||||
|
|
||||||
def find_insulation(self, u_value, phase):
|
def find_insulation(self, u_value, phase, exclusions=None):
|
||||||
"""
|
"""
|
||||||
This function contains the logic for finding potential insulation measures for a property, depending
|
This function contains the logic for finding potential insulation measures for a property, depending
|
||||||
on the parts available and whether the property can have external wall insulation installed
|
on the parts available and whether the property can have external wall insulation installed
|
||||||
|
|
@ -564,8 +564,10 @@ class WallRecommendations(Definitions):
|
||||||
# we separate the logic for for recommending them, therefore we don't
|
# we separate the logic for for recommending them, therefore we don't
|
||||||
# consider diminishing returns between the two as they are considered to be separate measures
|
# consider diminishing returns between the two as they are considered to be separate measures
|
||||||
|
|
||||||
|
exclusions = [] if exclusions is None else exclusions
|
||||||
|
|
||||||
ewi_recommendations = []
|
ewi_recommendations = []
|
||||||
if self.ewi_valid():
|
if self.ewi_valid() and "external_wall_insulation" not in exclusions:
|
||||||
ewi_recommendations = self._find_insulation(
|
ewi_recommendations = self._find_insulation(
|
||||||
u_value=u_value,
|
u_value=u_value,
|
||||||
insulation_materials=pd.DataFrame(
|
insulation_materials=pd.DataFrame(
|
||||||
|
|
@ -575,12 +577,14 @@ class WallRecommendations(Definitions):
|
||||||
phase=phase,
|
phase=phase,
|
||||||
)
|
)
|
||||||
|
|
||||||
iwi_recommendations = self._find_insulation(
|
iwi_recommendations = []
|
||||||
u_value=u_value,
|
if "internal_wall_insulation" not in exclusions:
|
||||||
insulation_materials=pd.DataFrame(self.internal_wall_insulation_materials),
|
iwi_recommendations = self._find_insulation(
|
||||||
non_insulation_materials=self.internal_wall_non_insulation_materials,
|
u_value=u_value,
|
||||||
phase=phase,
|
insulation_materials=pd.DataFrame(self.internal_wall_insulation_materials),
|
||||||
)
|
non_insulation_materials=self.internal_wall_non_insulation_materials,
|
||||||
|
phase=phase,
|
||||||
|
)
|
||||||
|
|
||||||
self.recommendations += ewi_recommendations + iwi_recommendations
|
self.recommendations += ewi_recommendations + iwi_recommendations
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue