mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
commit
68f3911ff8
35 changed files with 4563 additions and 545 deletions
2
.idea/Model.iml
generated
2
.idea/Model.iml
generated
|
|
@ -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 (model_data)" jdkType="Python SDK" />
|
||||
<orderEntry type="jdk" jdkName="Python 3.10 (backend)" jdkType="Python SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
<component name="PyNamespacePackagesService">
|
||||
|
|
|
|||
2
.idea/misc.xml
generated
2
.idea/misc.xml
generated
|
|
@ -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 (model_data)" project-jdk-type="Python SDK" />
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.10 (backend)" project-jdk-type="Python SDK" />
|
||||
<component name="PythonCompatibilityInspectionAdvertiser">
|
||||
<option name="version" value="3" />
|
||||
</component>
|
||||
|
|
|
|||
|
|
@ -38,7 +38,11 @@ class OrdnanceSuveyClient:
|
|||
raise ValueError("No results found - run get_places_api first")
|
||||
|
||||
self.address_os = self.most_relevant_result["ADDRESS"]
|
||||
self.postcode_os = self.most_relevant_result["POSTCODE"]
|
||||
|
||||
if "POSTCODE" in self.most_relevant_result:
|
||||
self.postcode_os = self.most_relevant_result["POSTCODE"]
|
||||
else:
|
||||
self.postcode_os = self.most_relevant_result["POSTCODE_LOCATOR"]
|
||||
# We strip out the postcode from the address as this is already stored separately
|
||||
self.address_os = self.address_os.replace(self.postcode_os, "").strip()
|
||||
# Remove trailing comma
|
||||
|
|
@ -49,7 +53,7 @@ class OrdnanceSuveyClient:
|
|||
self.postcode_os = self.postcode_os.upper()
|
||||
|
||||
@lru_cache(maxsize=128)
|
||||
def get_places_api(self):
|
||||
def get_places_api(self, filter_by_postcode=False):
|
||||
"""
|
||||
This method is tasked with getting the places api from the Ordnance Survey.
|
||||
"""
|
||||
|
|
@ -58,16 +62,35 @@ class OrdnanceSuveyClient:
|
|||
raise ValueError("Ordnance Survey API key not specified")
|
||||
|
||||
encoded_address_query = urllib.parse.quote(self.full_address)
|
||||
url = (f"https://api.os.uk/search/places/v1/find?query={encoded_address_query}&key="
|
||||
f"{self.api_key}")
|
||||
|
||||
url = (
|
||||
f"https://api.os.uk/search/places/v1/find?query={encoded_address_query}&dataset=DPA,LPI&matchprecision=10"
|
||||
f"&key={self.api_key}"
|
||||
)
|
||||
|
||||
response = requests.get(url)
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
results = data['results']
|
||||
res = data["results"]
|
||||
|
||||
if filter_by_postcode:
|
||||
results = []
|
||||
for r in res:
|
||||
if "DPA" in r:
|
||||
if r["DPA"]["POSTCODE"] == self.postcode:
|
||||
results.append(r)
|
||||
elif "LPI" in r:
|
||||
if r["LPI"]["POSTCODE_LOCATOR"] == self.postcode:
|
||||
results.append(r)
|
||||
else:
|
||||
raise ValueError("Could not find postcode in either DPA or LPI")
|
||||
else:
|
||||
results = res
|
||||
|
||||
self.results = results
|
||||
|
||||
# Extract some details about the best match
|
||||
self.most_relevant_result = self.results[0]["DPA"]
|
||||
self.most_relevant_result = self.results[0]["DPA"] if "DPA" in self.results[0] else self.results[0]["LPI"]
|
||||
|
||||
self.parse_classification_code(self.most_relevant_result["CLASSIFICATION_CODE"])
|
||||
self.set_places_address()
|
||||
|
|
@ -94,11 +117,14 @@ class OrdnanceSuveyClient:
|
|||
value_map = {
|
||||
# In the OS api, "RD" is a "Dwelling" however this is not valid property type in the EPC database
|
||||
'RD': {},
|
||||
'RD02': {'property_type': 'House', 'built_form': 'Detatched'},
|
||||
'RD03': {'property_type': 'House', 'built_form': 'Semi-Detatched'},
|
||||
'RD02': {'property_type': 'House', 'built_form': 'Detached'},
|
||||
'RD03': {'property_type': 'House', 'built_form': 'Semi-Detached'},
|
||||
'RD04': {'property_type': 'House', 'built_form': 'Mid-Terrace'},
|
||||
'RD06': {'property_type': 'Flat'},
|
||||
}
|
||||
# Other classifications can be found in here:
|
||||
# https://osdatahub.os.uk/docs/places/technicalSpecification in the CLASSIFICATION_CODE description.
|
||||
# A lookup table csv can be downloaded which contains all of the codes
|
||||
|
||||
mapped = value_map.get(classification_code, {})
|
||||
self.property_type = mapped.get("property_type", "")
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
@ -93,7 +94,10 @@ class Property:
|
|||
non_invasive_recommendations else []
|
||||
)
|
||||
# This is a list of measures that have been recommended for the property
|
||||
self.measures = ast.literal_eval(measures) if measures else None
|
||||
if isinstance(measures, list):
|
||||
self.measures = measures
|
||||
else:
|
||||
self.measures = ast.literal_eval(measures) if measures else None
|
||||
|
||||
self.uprn = epc_record.get("uprn")
|
||||
self.full_sap_epc = epc_record.get("full_sap_epc")
|
||||
|
|
@ -159,6 +163,9 @@ class Property:
|
|||
self.current_energy_bill = None
|
||||
self.expected_energy_bill = None
|
||||
|
||||
self.heating_energy_source = None
|
||||
self.hot_water_energy_source = None
|
||||
|
||||
self.recommendations_scoring_data = []
|
||||
|
||||
self.parse_kwargs(kwargs)
|
||||
|
|
@ -200,11 +207,11 @@ class Property:
|
|||
# difference_record = self.epc_record - self.epc_record
|
||||
|
||||
# TODO: change these lower and replace in the settings file
|
||||
print(
|
||||
"CHANGE THE LATEST FIELD TO REMOVE NUMBER HABITABLE ROOMS IF WE WANT TO USE STARTING/ENDING"
|
||||
)
|
||||
# print(
|
||||
# "CHANGE THE LATEST FIELD TO REMOVE NUMBER HABITABLE ROOMS IF WE WANT TO USE STARTING/ENDING"
|
||||
# )
|
||||
fixed_data_col_names = MANDATORY_FIXED_FEATURES + LATEST_FIELD
|
||||
print("NEED TO CHANGE THE DASH TO LOWER CASE")
|
||||
# print("NEED TO CHANGE THE DASH TO LOWER CASE")
|
||||
fixed_data_col_names = [
|
||||
x.lower().replace("_", "-") for x in fixed_data_col_names
|
||||
]
|
||||
|
|
@ -582,6 +589,26 @@ class Property:
|
|||
floor_area_decile_thresholds=floor_area_decile_thresholds,
|
||||
)
|
||||
self.set_energy_source()
|
||||
self.find_energy_sources()
|
||||
self.set_current_energy_bill()
|
||||
|
||||
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"],
|
||||
total_floor_area=self.floor_area
|
||||
)
|
||||
|
||||
self.current_energy_bill = AnnualBillSavings.calculate_annual_bill(self.current_adjusted_energy)
|
||||
|
||||
def set_spatial(self, spatial: pd.DataFrame):
|
||||
"""
|
||||
|
|
@ -844,8 +871,8 @@ class Property:
|
|||
# where a property is marked as being on the first floor
|
||||
if self.floor_level > 0:
|
||||
|
||||
# We check if there is another property below
|
||||
if not self.floor["another_property_below"]:
|
||||
# We check if there is another property below (for a non-sap assessment)
|
||||
if not self.floor["another_property_below"] and self.floor["thermal_transmittance_unit"] is None:
|
||||
self.floor_level = 0
|
||||
return
|
||||
|
||||
|
|
@ -902,14 +929,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):
|
||||
|
|
@ -990,3 +1016,66 @@ class Property:
|
|||
|
||||
# Set the energy source based on the conditions above
|
||||
self.energy_source = energy_source
|
||||
|
||||
def find_energy_sources(self):
|
||||
# Based on the heating and the hot water
|
||||
heating_fuel_mapping = {
|
||||
'has_mains_gas': 'Natural Gas',
|
||||
'has_electric': 'Electricity',
|
||||
'has_oil': 'Oil',
|
||||
'has_wood_logs': 'Wood Logs',
|
||||
'has_coal': 'Coal',
|
||||
'has_anthracite': 'Anthracite',
|
||||
'has_smokeless_fuel': 'Smokeless Fuel',
|
||||
'has_lpg': 'LPG',
|
||||
'has_b30k': 'B30K Biofuel',
|
||||
'has_air_source_heat_pump': 'Electricity',
|
||||
'has_ground_source_heat_pump': 'Electricity',
|
||||
'has_water_source_heat_pump': 'Electricity',
|
||||
'has_electric_heat_pump': 'Electricity',
|
||||
'has_solar_assisted_heat_pump': 'Electricity',
|
||||
'has_exhaust_source_heat_pump': 'Electricity',
|
||||
'has_community_heat_pump': 'Electricity',
|
||||
'has_wood_pellets': 'Wood Pellets',
|
||||
'has_community_scheme': 'Varied (Community Scheme)'
|
||||
}
|
||||
|
||||
# Hot water
|
||||
heater_type_to_fuel = {
|
||||
'gas instantaneous': 'Natural Gas',
|
||||
'electric heat pump': 'Electricity',
|
||||
'electric immersion': 'Electricity',
|
||||
'gas boiler': 'Natural Gas',
|
||||
'oil boiler': 'Oil',
|
||||
'electric instantaneous': 'Electricity',
|
||||
'gas multipoint': 'Natural Gas',
|
||||
'heat pump': 'Electricity',
|
||||
'solid fuel boiler': 'Solid Fuel',
|
||||
'solid fuel range cooker': 'Solid Fuel',
|
||||
'room heaters': 'Varied' # Could be any fuel, further specifics needed based on context
|
||||
}
|
||||
|
||||
# Define a mapping from system types to general categories or modifications of fuel types
|
||||
system_type_modification = {
|
||||
'from main system': 'Main System',
|
||||
'from secondary system': 'Secondary System',
|
||||
'from second main heating system': 'Secondary System',
|
||||
'community scheme': 'Community Scheme'
|
||||
}
|
||||
|
||||
self.heating_energy_source = [
|
||||
fuel for key, fuel in heating_fuel_mapping.items() if self.main_heating.get(key, False)
|
||||
]
|
||||
if len(self.heating_energy_source) == 0 or len(self.heating_energy_source) > 1:
|
||||
raise Exception("Investigate em")
|
||||
|
||||
self.heating_energy_source = self.heating_energy_source[0]
|
||||
|
||||
if self.hotwater["heater_type"] is not None:
|
||||
self.hot_water_energy_source = heater_type_to_fuel[self.hotwater["heater_type"]]
|
||||
else:
|
||||
fuel = system_type_modification[self.hotwater["system_type"]]
|
||||
if fuel == 'Main System':
|
||||
self.hot_water_energy_source = self.heating_energy_source
|
||||
else:
|
||||
raise Exception("Investiage me")
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ from BaseUtility import Definitions
|
|||
from utils.logger import setup_logger
|
||||
from typing import List
|
||||
from fuzzywuzzy import process
|
||||
from backend.app.utils import sap_to_epc
|
||||
|
||||
logger = setup_logger()
|
||||
|
||||
|
|
@ -190,15 +191,15 @@ class SearchEpc:
|
|||
self.property_type = property_type
|
||||
self.fast = fast
|
||||
|
||||
@classmethod
|
||||
def get_house_number(cls, address: str) -> str | None:
|
||||
@staticmethod
|
||||
def get_house_number(address: str, postcode=None) -> str | None:
|
||||
"""
|
||||
This method uses the usaddress library to parse an address and extract the primary house or flat number.
|
||||
"""
|
||||
try:
|
||||
|
||||
# Custom regex to catch a broad range of cases
|
||||
pattern = r'(?i)(?:flat|apartment)\s*(\d+)|^\s*(\d+)'
|
||||
try:
|
||||
# Updated regex to catch house numbers including alphanumeric ones
|
||||
pattern = r'(?i)(?:flat|apartment)\s*(\d+\w*)|^\s*(\d+\w*)'
|
||||
match = re.search(pattern, address)
|
||||
if match:
|
||||
return next(g for g in match.groups() if g is not None)
|
||||
|
|
@ -207,6 +208,11 @@ class SearchEpc:
|
|||
# First, try to get the 'OccupancyIdentifier' if 'OccupancyType' is detected
|
||||
for part, type_ in parsed:
|
||||
if type_ == 'OccupancyIdentifier':
|
||||
if postcode is not None:
|
||||
if part == postcode.split(" ")[0]:
|
||||
continue
|
||||
if part == postcode.split(" ")[1]:
|
||||
continue
|
||||
return part # This assumes the first 'OccupancyIdentifier' after 'OccupancyType' is the primary
|
||||
# number
|
||||
|
||||
|
|
@ -216,7 +222,7 @@ class SearchEpc:
|
|||
return address_number.replace(",", "") # Remove any trailing commas
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error parsing address: {e}")
|
||||
raise Exception(f"Error parsing address: {e}")
|
||||
|
||||
return None
|
||||
|
||||
|
|
@ -428,7 +434,8 @@ class SearchEpc:
|
|||
self, initial_postcode: str,
|
||||
lmks_to_drop: list[str] | None = None,
|
||||
built_form: str = "",
|
||||
property_type: str = ""
|
||||
property_type: str = "",
|
||||
exclude_old: bool = False
|
||||
):
|
||||
"""
|
||||
Fetches and processes EPC data for a given initial postcode, applying successive trimming
|
||||
|
|
@ -447,6 +454,7 @@ class SearchEpc:
|
|||
:param lmks_to_drop: List of 'lmk-key' values to be excluded from the EPC data.
|
||||
:param built_form: The 'built-form' value to be used for filtering the EPC data.
|
||||
:param property_type: The 'property-type' value to be used for filtering the EPC data.
|
||||
:param exclude_old: Flag to exclude EPC data older than 10 years.
|
||||
:return:
|
||||
"""
|
||||
|
||||
|
|
@ -474,9 +482,23 @@ class SearchEpc:
|
|||
if lmks_to_drop is not None:
|
||||
epc_data = epc_data[~epc_data["lmk-key"].isin(lmks_to_drop)]
|
||||
|
||||
try:
|
||||
epc_data['lodgement-datetime'] = pd.to_datetime(
|
||||
epc_data['lodgement-datetime'], format='%Y-%m-%d %H:%M:%S', errors='coerce'
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error("Problem formatting lodgement-datime, appling fallback: " + str(e))
|
||||
epc_data['lodgement-datetime'] = pd.to_datetime(epc_data['lodgement-datetime'], errors='coerce')
|
||||
|
||||
if exclude_old:
|
||||
# Exclude EPC data older than 10 years
|
||||
epc_data = epc_data[
|
||||
epc_data["lodgement-datetime"] > (pd.Timestamp.now() - pd.DateOffset(years=10))
|
||||
]
|
||||
|
||||
if not epc_data.empty:
|
||||
# Further processing of the EPC data
|
||||
epc_data['lodgement-datetime'] = pd.to_datetime(epc_data['lodgement-datetime'], errors='coerce')
|
||||
|
||||
epc_data = epc_data.sort_values("lodgement-datetime", ascending=False).groupby("uprn").head(1)
|
||||
epc_data["house_number"] = epc_data["address"].apply(lambda add1: self.get_house_number(add1))
|
||||
epc_data["numeric_house_number"] = epc_data["house_number"].apply(
|
||||
|
|
@ -554,7 +576,7 @@ class SearchEpc:
|
|||
# If loop finishes without a valid response, raise an exception
|
||||
raise Exception("Unable to find postcode data after trimming - investigate me")
|
||||
|
||||
def estimate_epc(self, property_type, built_form, lmks_to_drop=None):
|
||||
def estimate_epc(self, property_type, built_form, lmks_to_drop=None, exclude_old=False):
|
||||
"""
|
||||
For a property that does not have an EPC, we retrieve the EPC data for the closest properties
|
||||
and estimate the EPC for the property in question.
|
||||
|
|
@ -567,6 +589,7 @@ class SearchEpc:
|
|||
the ordnance survey api
|
||||
:param lmks_to_drop: This is a list of LMK keys that should be dropped from the estimation process. This
|
||||
is used as an override for testing, to drop EPCs for the property we are testing
|
||||
:param exclude_old: Used to drop any expired EPCs (more than 10 years old)
|
||||
:return:
|
||||
"""
|
||||
|
||||
|
|
@ -576,7 +599,8 @@ class SearchEpc:
|
|||
initial_postcode=self.postcode,
|
||||
lmks_to_drop=lmks_to_drop,
|
||||
built_form=built_form,
|
||||
property_type=property_type
|
||||
property_type=property_type,
|
||||
exclude_old=exclude_old
|
||||
)
|
||||
|
||||
# If we have missing lodgment date, we fill it with inspection-date
|
||||
|
|
@ -624,6 +648,8 @@ class SearchEpc:
|
|||
else:
|
||||
estimated_epc["lodgement-date"] = estimated_epc["lodgement-datetime"].strftime("%Y-%m-%d")
|
||||
|
||||
estimated_epc["current-energy-rating"] = sap_to_epc(estimated_epc["current-energy-efficiency"])
|
||||
|
||||
estimated_epc["postcode"] = self.postcode
|
||||
estimated_epc["uprn"] = self.uprn
|
||||
estimated_epc["address"] = self.full_address
|
||||
|
|
|
|||
|
|
@ -1,336 +1,329 @@
|
|||
from backend.Property import Property
|
||||
from backend.SearchEpc import SearchEpc
|
||||
from etl.epc.Record import EPCRecord
|
||||
from dotenv import load_dotenv
|
||||
from utils.s3 import read_dataframe_from_s3_parquet
|
||||
import os
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
from recommendations.Costs import MCS_SOLAR_PV_COST_DATA
|
||||
from backend.ml_models.AnnualBillSavings import AnnualBillSavings
|
||||
import requests
|
||||
from functools import lru_cache
|
||||
import time
|
||||
|
||||
load_dotenv(dotenv_path="backend/.env")
|
||||
EPC_AUTH_TOKEN = os.getenv("EPC_AUTH_TOKEN")
|
||||
|
||||
# This is for 6 Laura Close, Tintagel, PL34 0EB (same property that Cotswolrd energy used)
|
||||
uprn = 100040099104
|
||||
# This is for 353A, Hermitage Lane, ME16 9NT (one of the e.on properties)
|
||||
uprn = 200000964454
|
||||
class GoogleSolarApi:
|
||||
NORTH_FACING_AZIMUTH_RANGE = (-30, 30)
|
||||
|
||||
cleaning_data = read_dataframe_from_s3_parquet(
|
||||
bucket_name="retrofit-data-dev", file_key="sap_change_model/cleaning_dataset.parquet",
|
||||
)
|
||||
# Conservative estimate of the proportion of electricity that will be consumed, whereas the rest will
|
||||
# be exported
|
||||
SOLAR_CONSUMPTION_PROPORTION = 0.5
|
||||
|
||||
searcher = SearchEpc(address1="", postcode="", uprn=uprn, auth_token=EPC_AUTH_TOKEN, os_api_key="")
|
||||
# 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
|
||||
|
||||
searcher.find_property(skip_os=True)
|
||||
# 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
|
||||
|
||||
epc_records = {
|
||||
'original_epc': searcher.newest_epc.copy(),
|
||||
'full_sap_epc': searcher.full_sap_epc.copy(),
|
||||
'old_data': searcher.older_epcs.copy(),
|
||||
}
|
||||
# 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
|
||||
|
||||
epc = EPCRecord(
|
||||
epc_records=epc_records,
|
||||
run_mode="newdata",
|
||||
cleaning_data=cleaning_data
|
||||
)
|
||||
# The Solar API uses 1.04 (4% annual increase) for US locations
|
||||
discount_rate = 1.04
|
||||
|
||||
uprn_filenames = read_dataframe_from_s3_parquet(
|
||||
bucket_name="retrofit-data-dev", file_key="spatial/filename_meta.parquet"
|
||||
)
|
||||
# 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
|
||||
|
||||
p = Property(
|
||||
id=0,
|
||||
address=searcher.address_clean,
|
||||
postcode=searcher.postcode_clean,
|
||||
epc_record=epc,
|
||||
already_installed={},
|
||||
non_invasive_recommendations={},
|
||||
)
|
||||
# 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
|
||||
|
||||
p.get_spatial_data(uprn_filenames)
|
||||
def __init__(self, api_key, max_retries=5):
|
||||
"""
|
||||
Initialize the GoogleSolarApi class with the provided API key and maximum retries.
|
||||
|
||||
longitude = p.spatial["longitude"]
|
||||
latitude = p.spatial["latitude"]
|
||||
:param api_key: The API key to authenticate requests to the Google Solar API.
|
||||
:param max_retries: The maximum number of retries for the API request (default is 5).
|
||||
"""
|
||||
self.api_key = api_key
|
||||
self.max_retries = max_retries
|
||||
self.base_url = "https://solar.googleapis.com/v1"
|
||||
|
||||
api_key = "AIzaSyCIz8Psu5h-1txuDX0rQpUTgkvdj8yohqU"
|
||||
url = 'https://solar.googleapis.com/v1/solarPotential'
|
||||
params = {
|
||||
'location.latitude': f'{latitude:.5f}',
|
||||
'location.longitude': f'{longitude:.5f}',
|
||||
'requiredQuality': "MEDIUM",
|
||||
'key': api_key
|
||||
}
|
||||
self.insights_data = None
|
||||
self.roof_segments = []
|
||||
|
||||
insights_url = 'https://solar.googleapis.com/v1/buildingInsights:findClosest'
|
||||
# property attributes:
|
||||
self.floor_area = None
|
||||
self.roof_area = None
|
||||
self.roof_segment_indexes = None
|
||||
self.panel_area = None
|
||||
self.panel_wattage = None
|
||||
self.panel_performance = None
|
||||
|
||||
# Make the GET request to the Solar API
|
||||
insights_response = requests.get(insights_url, params=params)
|
||||
insights_data = insights_response.json()
|
||||
def get_building_insights(self, longitude, latitude, required_quality="MEDIUM", max_retries=None):
|
||||
"""
|
||||
Make an API request to retrieve building insights based on the given longitude and latitude, with retry
|
||||
mechanism.
|
||||
|
||||
solar_potential = insights_data["solarPotential"]
|
||||
:param longitude: The longitude of the location.
|
||||
:param latitude: The latitude of the location.
|
||||
:param required_quality: The required quality of the data (default is "MEDIUM").
|
||||
:param max_retries: The maximum number of retries for the API request (default is None, which uses the
|
||||
instance's max_retries).
|
||||
:return: The JSON response containing the building insights data.
|
||||
"""
|
||||
if max_retries is None:
|
||||
max_retries = self.max_retries
|
||||
|
||||
from pprint import pprint
|
||||
insights_url = f"{self.base_url}/buildingInsights:findClosest"
|
||||
params = {
|
||||
'location.latitude': f'{latitude:.5f}',
|
||||
'location.longitude': f'{longitude:.5f}',
|
||||
'requiredQuality': required_quality,
|
||||
'key': self.api_key
|
||||
}
|
||||
|
||||
pprint(solar_potential)
|
||||
attempt = 0
|
||||
while attempt < max_retries:
|
||||
try:
|
||||
response = requests.get(insights_url, params=params)
|
||||
response.raise_for_status() # Raise an error for bad status codes
|
||||
return response.json()
|
||||
except requests.exceptions.RequestException as e:
|
||||
attempt += 1
|
||||
print(f"Attempt {attempt} failed: {e}")
|
||||
time.sleep(2 ** attempt) # Exponential backoff
|
||||
if attempt >= max_retries:
|
||||
raise
|
||||
|
||||
# This is the size of the panels used in the calculation - 400 watt
|
||||
solar_potential["panelCapacityWatts"]
|
||||
# Height of the panels used
|
||||
solar_potential["panelHeightMeters"]
|
||||
# Width of the panels used
|
||||
solar_potential["panelWidthMeters"]
|
||||
@lru_cache(maxsize=128)
|
||||
def get(self, longitude, latitude, required_quality="MEDIUM"):
|
||||
"""
|
||||
Wrapper function that calls get_building_insights and extracts roof segments, with caching.
|
||||
|
||||
solar_potential["wholeRoofStats"]
|
||||
:param longitude: The longitude of the location.
|
||||
:param latitude: The latitude of the location.
|
||||
:param required_quality: The required quality of the data (default is "MEDIUM").
|
||||
:return: The JSON response containing the building insights data.
|
||||
"""
|
||||
|
||||
# Copy of response for testing - 6 Laura Close, Tintagel, PL34 0EB
|
||||
# {'name': 'buildings/ChIJ2yC6t4KEa0gRh2TIssogI7k', 'center': {'latitude': 50.667375, 'longitude': -4.7416833},
|
||||
# 'imageryDate': {'year': 2021, 'month': 7, 'day': 19}, 'regionCode': 'GB', 'solarPotential': {'maxArrayPanelsCount':
|
||||
# 39, 'maxArrayAreaMeters2': 76.578636, 'maxSunshineHoursPerYear': 1172.0627, 'carbonOffsetFactorKgPerMwh':
|
||||
# 478.99942, 'wholeRoofStats': {'areaMeters2': 129.65686, 'sunshineQuantiles': [537, 738.3836, 805.62445, 842.6802,
|
||||
# 909.8431, 972.15234, 1036.1013, 1092.051, 1135.8192, 1163.1444, 1193.6012], 'groundAreaMeters2': 112.33},
|
||||
# 'roofSegmentStats': [{'pitchDegrees': 31.443022, 'azimuthDegrees': 218.25331, 'stats': {'areaMeters2': 44.08321,
|
||||
# 'sunshineQuantiles': [614, 940.86975, 982.39124, 1057.0664, 1109.6869, 1137.5837, 1152.9211, 1163.1106, 1168.2212,
|
||||
# 1170.8883, 1193.6012], 'groundAreaMeters2': 37.61}, 'center': {'latitude': 50.6673664, 'longitude':
|
||||
# -4.741714099999999}, 'boundingBox': {'sw': {'latitude': 50.6673354, 'longitude': -4.741777}, 'ne': {'latitude':
|
||||
# 50.6674029, 'longitude': -4.7416472}}, 'planeHeightAtCenterMeters': 93.0221}, {'pitchDegrees': 34.39779,
|
||||
# 'azimuthDegrees': 31.74401, 'stats': {'areaMeters2': 44.622986, 'sunshineQuantiles': [537, 671.49774, 733.84985,
|
||||
# 780.82733, 801.4026, 814.0189, 824.0077, 847.77484, 895.08295, 950.1469, 1123.3503], 'groundAreaMeters2': 36.82},
|
||||
# 'center': {'latitude': 50.6673966, 'longitude': -4.7416813}, 'boundingBox': {'sw': {'latitude': 50.667361,
|
||||
# 'longitude': -4.7417497}, 'ne': {'latitude': 50.6674303, 'longitude': -4.741615599999999}},
|
||||
# 'planeHeightAtCenterMeters': 92.87593}, {'pitchDegrees': 3.0681775, 'azimuthDegrees': 301.1099, 'stats': {
|
||||
# 'areaMeters2': 17.074476, 'sunshineQuantiles': [644.71136, 731.0546, 782.89813, 842.7107, 908.55585, 966.6212,
|
||||
# 1010.6367, 1038.2543, 1053.2788, 1090.6831, 1128.0178], 'groundAreaMeters2': 17.050001}, 'center': {'latitude':
|
||||
# 50.66740850000001, 'longitude': -4.7416025}, 'boundingBox': {'sw': {'latitude': 50.6673895, 'longitude':
|
||||
# -4.7416436}, 'ne': {'latitude': 50.667431199999996, 'longitude': -4.7415572}}, 'planeHeightAtCenterMeters':
|
||||
# 90.630356}, {'pitchDegrees': 27.093596, 'azimuthDegrees': 132.60162, 'stats': {'areaMeters2': 13.501617,
|
||||
# 'sunshineQuantiles': [749, 976.85345, 1059.0062, 1081.6173, 1097.4441, 1110.3171, 1128.2186, 1133.9421, 1142.068,
|
||||
# 1148.2168, 1157.632], 'groundAreaMeters2': 12.02}, 'center': {'latitude': 50.667315699999996, 'longitude':
|
||||
# -4.741675400000001}, 'boundingBox': {'sw': {'latitude': 50.667291399999996, 'longitude': -4.7417066},
|
||||
# 'ne': {'latitude': 50.6673372, 'longitude': -4.741648400000001}}, 'planeHeightAtCenterMeters': 92.36334},
|
||||
# {'pitchDegrees': 31.666294, 'azimuthDegrees': 308.42334, 'stats': {'areaMeters2': 10.374564, 'sunshineQuantiles': [
|
||||
# 617.9507, 752.2504, 847.66315, 872.0505, 881.26227, 900.9639, 933.3188, 967.4747, 1000.8129, 1038.3002, 1105.545],
|
||||
# 'groundAreaMeters2': 8.83}, 'center': {'latitude': 50.6673295, 'longitude': -4.7417128}, 'boundingBox': {'sw': {
|
||||
# 'latitude': 50.6673134, 'longitude': -4.7417422}, 'ne': {'latitude': 50.6673413, 'longitude': -4.7416775}},
|
||||
# 'planeHeightAtCenterMeters': 92.31146}], 'solarPanelConfigs': [{'panelsCount': 4, 'yearlyEnergyDcKwh': 1867.1516,
|
||||
# 'roofSegmentSummaries': [{'pitchDegrees': 31.443022, 'azimuthDegrees': 218.25331, 'panelsCount': 4,
|
||||
# 'yearlyEnergyDcKwh': 1867.1515, 'segmentIndex': 0}]}, {'panelsCount': 5, 'yearlyEnergyDcKwh': 2335.0068,
|
||||
# 'roofSegmentSummaries': [{'pitchDegrees': 31.443022, 'azimuthDegrees': 218.25331, 'panelsCount': 5,
|
||||
# 'yearlyEnergyDcKwh': 2335.0068, 'segmentIndex': 0}]}, {'panelsCount': 6, 'yearlyEnergyDcKwh': 2799.8508,
|
||||
# 'roofSegmentSummaries': [{'pitchDegrees': 31.443022, 'azimuthDegrees': 218.25331, 'panelsCount': 6,
|
||||
# 'yearlyEnergyDcKwh': 2799.8508, 'segmentIndex': 0}]}, {'panelsCount': 7, 'yearlyEnergyDcKwh': 3264.6506,
|
||||
# 'roofSegmentSummaries': [{'pitchDegrees': 31.443022, 'azimuthDegrees': 218.25331, 'panelsCount': 7,
|
||||
# 'yearlyEnergyDcKwh': 3264.6506, 'segmentIndex': 0}]}, {'panelsCount': 8, 'yearlyEnergyDcKwh': 3726.2405,
|
||||
# 'roofSegmentSummaries': [{'pitchDegrees': 31.443022, 'azimuthDegrees': 218.25331, 'panelsCount': 8,
|
||||
# 'yearlyEnergyDcKwh': 3726.2405, 'segmentIndex': 0}]}, {'panelsCount': 9, 'yearlyEnergyDcKwh': 4187.721,
|
||||
# 'roofSegmentSummaries': [{'pitchDegrees': 31.443022, 'azimuthDegrees': 218.25331, 'panelsCount': 9,
|
||||
# 'yearlyEnergyDcKwh': 4187.721, 'segmentIndex': 0}]}, {'panelsCount': 10, 'yearlyEnergyDcKwh': 4646.094,
|
||||
# 'roofSegmentSummaries': [{'pitchDegrees': 31.443022, 'azimuthDegrees': 218.25331, 'panelsCount': 10,
|
||||
# 'yearlyEnergyDcKwh': 4646.094, 'segmentIndex': 0}]}, {'panelsCount': 11, 'yearlyEnergyDcKwh': 5103.777,
|
||||
# 'roofSegmentSummaries': [{'pitchDegrees': 31.443022, 'azimuthDegrees': 218.25331, 'panelsCount': 10,
|
||||
# 'yearlyEnergyDcKwh': 4646.094, 'segmentIndex': 0}, {'pitchDegrees': 27.093596, 'azimuthDegrees': 132.60162,
|
||||
# 'panelsCount': 1, 'yearlyEnergyDcKwh': 457.68268, 'segmentIndex': 3}]}, {'panelsCount': 12, 'yearlyEnergyDcKwh':
|
||||
# 5559.845, 'roofSegmentSummaries': [{'pitchDegrees': 31.443022, 'azimuthDegrees': 218.25331, 'panelsCount': 10,
|
||||
# 'yearlyEnergyDcKwh': 4646.094, 'segmentIndex': 0}, {'pitchDegrees': 27.093596, 'azimuthDegrees': 132.60162,
|
||||
# 'panelsCount': 2, 'yearlyEnergyDcKwh': 913.7509, 'segmentIndex': 3}]}, {'panelsCount': 13, 'yearlyEnergyDcKwh':
|
||||
# 6013.053, 'roofSegmentSummaries': [{'pitchDegrees': 31.443022, 'azimuthDegrees': 218.25331, 'panelsCount': 11,
|
||||
# 'yearlyEnergyDcKwh': 5099.302, 'segmentIndex': 0}, {'pitchDegrees': 27.093596, 'azimuthDegrees': 132.60162,
|
||||
# 'panelsCount': 2, 'yearlyEnergyDcKwh': 913.7509, 'segmentIndex': 3}]}, {'panelsCount': 14, 'yearlyEnergyDcKwh':
|
||||
# 6461.664, 'roofSegmentSummaries': [{'pitchDegrees': 31.443022, 'azimuthDegrees': 218.25331, 'panelsCount': 12,
|
||||
# 'yearlyEnergyDcKwh': 5547.9126, 'segmentIndex': 0}, {'pitchDegrees': 27.093596, 'azimuthDegrees': 132.60162,
|
||||
# 'panelsCount': 2, 'yearlyEnergyDcKwh': 913.7509, 'segmentIndex': 3}]}, {'panelsCount': 15, 'yearlyEnergyDcKwh':
|
||||
# 6902.33, 'roofSegmentSummaries': [{'pitchDegrees': 31.443022, 'azimuthDegrees': 218.25331, 'panelsCount': 12,
|
||||
# 'yearlyEnergyDcKwh': 5547.9126, 'segmentIndex': 0}, {'pitchDegrees': 27.093596, 'azimuthDegrees': 132.60162,
|
||||
# 'panelsCount': 3, 'yearlyEnergyDcKwh': 1354.4171, 'segmentIndex': 3}]}, {'panelsCount': 16, 'yearlyEnergyDcKwh':
|
||||
# 7321.6436, 'roofSegmentSummaries': [{'pitchDegrees': 31.443022, 'azimuthDegrees': 218.25331, 'panelsCount': 12,
|
||||
# 'yearlyEnergyDcKwh': 5547.9126, 'segmentIndex': 0}, {'pitchDegrees': 3.0681775, 'azimuthDegrees': 301.1099,
|
||||
# 'panelsCount': 1, 'yearlyEnergyDcKwh': 419.31348, 'segmentIndex': 2}, {'pitchDegrees': 27.093596, 'azimuthDegrees':
|
||||
# 132.60162, 'panelsCount': 3, 'yearlyEnergyDcKwh': 1354.4171, 'segmentIndex': 3}]}, {'panelsCount': 17,
|
||||
# 'yearlyEnergyDcKwh': 7740.388, 'roofSegmentSummaries': [{'pitchDegrees': 31.443022, 'azimuthDegrees': 218.25331,
|
||||
# 'panelsCount': 12, 'yearlyEnergyDcKwh': 5547.9126, 'segmentIndex': 0}, {'pitchDegrees': 3.0681775,
|
||||
# 'azimuthDegrees': 301.1099, 'panelsCount': 2, 'yearlyEnergyDcKwh': 838.0579, 'segmentIndex': 2}, {'pitchDegrees':
|
||||
# 27.093596, 'azimuthDegrees': 132.60162, 'panelsCount': 3, 'yearlyEnergyDcKwh': 1354.4171, 'segmentIndex': 3}]},
|
||||
# {'panelsCount': 18, 'yearlyEnergyDcKwh': 8154.265, 'roofSegmentSummaries': [{'pitchDegrees': 31.443022,
|
||||
# 'azimuthDegrees': 218.25331, 'panelsCount': 13, 'yearlyEnergyDcKwh': 5961.7896, 'segmentIndex': 0},
|
||||
# {'pitchDegrees': 3.0681775, 'azimuthDegrees': 301.1099, 'panelsCount': 2, 'yearlyEnergyDcKwh': 838.0579,
|
||||
# 'segmentIndex': 2}, {'pitchDegrees': 27.093596, 'azimuthDegrees': 132.60162, 'panelsCount': 3, 'yearlyEnergyDcKwh':
|
||||
# 1354.4171, 'segmentIndex': 3}]}, {'panelsCount': 19, 'yearlyEnergyDcKwh': 8566.032, 'roofSegmentSummaries': [{
|
||||
# 'pitchDegrees': 31.443022, 'azimuthDegrees': 218.25331, 'panelsCount': 14, 'yearlyEnergyDcKwh': 6373.556,
|
||||
# 'segmentIndex': 0}, {'pitchDegrees': 3.0681775, 'azimuthDegrees': 301.1099, 'panelsCount': 2, 'yearlyEnergyDcKwh':
|
||||
# 838.0579, 'segmentIndex': 2}, {'pitchDegrees': 27.093596, 'azimuthDegrees': 132.60162, 'panelsCount': 3,
|
||||
# 'yearlyEnergyDcKwh': 1354.4171, 'segmentIndex': 3}]}, {'panelsCount': 20, 'yearlyEnergyDcKwh': 8976.624,
|
||||
# 'roofSegmentSummaries': [{'pitchDegrees': 31.443022, 'azimuthDegrees': 218.25331, 'panelsCount': 15,
|
||||
# 'yearlyEnergyDcKwh': 6784.1484, 'segmentIndex': 0}, {'pitchDegrees': 3.0681775, 'azimuthDegrees': 301.1099,
|
||||
# 'panelsCount': 2, 'yearlyEnergyDcKwh': 838.0579, 'segmentIndex': 2}, {'pitchDegrees': 27.093596, 'azimuthDegrees':
|
||||
# 132.60162, 'panelsCount': 3, 'yearlyEnergyDcKwh': 1354.4171, 'segmentIndex': 3}]}, {'panelsCount': 21,
|
||||
# 'yearlyEnergyDcKwh': 9380.78, 'roofSegmentSummaries': [{'pitchDegrees': 31.443022, 'azimuthDegrees': 218.25331,
|
||||
# 'panelsCount': 15, 'yearlyEnergyDcKwh': 6784.1484, 'segmentIndex': 0}, {'pitchDegrees': 3.0681775,
|
||||
# 'azimuthDegrees': 301.1099, 'panelsCount': 3, 'yearlyEnergyDcKwh': 1242.214, 'segmentIndex': 2}, {'pitchDegrees':
|
||||
# 27.093596, 'azimuthDegrees': 132.60162, 'panelsCount': 3, 'yearlyEnergyDcKwh': 1354.4171, 'segmentIndex': 3}]},
|
||||
# {'panelsCount': 22, 'yearlyEnergyDcKwh': 9784.078, 'roofSegmentSummaries': [{'pitchDegrees': 31.443022,
|
||||
# 'azimuthDegrees': 218.25331, 'panelsCount': 15, 'yearlyEnergyDcKwh': 6784.1484, 'segmentIndex': 0},
|
||||
# {'pitchDegrees': 3.0681775, 'azimuthDegrees': 301.1099, 'panelsCount': 4, 'yearlyEnergyDcKwh': 1645.5122,
|
||||
# 'segmentIndex': 2}, {'pitchDegrees': 27.093596, 'azimuthDegrees': 132.60162, 'panelsCount': 3, 'yearlyEnergyDcKwh':
|
||||
# 1354.4171, 'segmentIndex': 3}]}, {'panelsCount': 23, 'yearlyEnergyDcKwh': 10162.354, 'roofSegmentSummaries': [{
|
||||
# 'pitchDegrees': 31.443022, 'azimuthDegrees': 218.25331, 'panelsCount': 15, 'yearlyEnergyDcKwh': 6784.1484,
|
||||
# 'segmentIndex': 0}, {'pitchDegrees': 3.0681775, 'azimuthDegrees': 301.1099, 'panelsCount': 4, 'yearlyEnergyDcKwh':
|
||||
# 1645.5122, 'segmentIndex': 2}, {'pitchDegrees': 27.093596, 'azimuthDegrees': 132.60162, 'panelsCount': 3,
|
||||
# 'yearlyEnergyDcKwh': 1354.4171, 'segmentIndex': 3}, {'pitchDegrees': 31.666294, 'azimuthDegrees': 308.42334,
|
||||
# 'panelsCount': 1, 'yearlyEnergyDcKwh': 378.2754, 'segmentIndex': 4}]}, {'panelsCount': 24, 'yearlyEnergyDcKwh':
|
||||
# 10535.894, 'roofSegmentSummaries': [{'pitchDegrees': 31.443022, 'azimuthDegrees': 218.25331, 'panelsCount': 15,
|
||||
# 'yearlyEnergyDcKwh': 6784.1484, 'segmentIndex': 0}, {'pitchDegrees': 3.0681775, 'azimuthDegrees': 301.1099,
|
||||
# 'panelsCount': 5, 'yearlyEnergyDcKwh': 2019.0519, 'segmentIndex': 2}, {'pitchDegrees': 27.093596, 'azimuthDegrees':
|
||||
# 132.60162, 'panelsCount': 3, 'yearlyEnergyDcKwh': 1354.4171, 'segmentIndex': 3}, {'pitchDegrees': 31.666294,
|
||||
# 'azimuthDegrees': 308.42334, 'panelsCount': 1, 'yearlyEnergyDcKwh': 378.2754, 'segmentIndex': 4}]}, {'panelsCount':
|
||||
# 25, 'yearlyEnergyDcKwh': 10901.273, 'roofSegmentSummaries': [{'pitchDegrees': 31.443022, 'azimuthDegrees':
|
||||
# 218.25331, 'panelsCount': 15, 'yearlyEnergyDcKwh': 6784.1484, 'segmentIndex': 0}, {'pitchDegrees': 3.0681775,
|
||||
# 'azimuthDegrees': 301.1099, 'panelsCount': 5, 'yearlyEnergyDcKwh': 2019.0519, 'segmentIndex': 2}, {'pitchDegrees':
|
||||
# 27.093596, 'azimuthDegrees': 132.60162, 'panelsCount': 3, 'yearlyEnergyDcKwh': 1354.4171, 'segmentIndex': 3},
|
||||
# {'pitchDegrees': 31.666294, 'azimuthDegrees': 308.42334, 'panelsCount': 2, 'yearlyEnergyDcKwh': 743.65497,
|
||||
# 'segmentIndex': 4}]}, {'panelsCount': 26, 'yearlyEnergyDcKwh': 11242.756, 'roofSegmentSummaries': [{'pitchDegrees':
|
||||
# 31.443022, 'azimuthDegrees': 218.25331, 'panelsCount': 15, 'yearlyEnergyDcKwh': 6784.1484, 'segmentIndex': 0},
|
||||
# {'pitchDegrees': 34.39779, 'azimuthDegrees': 31.74401, 'panelsCount': 1, 'yearlyEnergyDcKwh': 341.4827,
|
||||
# 'segmentIndex': 1}, {'pitchDegrees': 3.0681775, 'azimuthDegrees': 301.1099, 'panelsCount': 5, 'yearlyEnergyDcKwh':
|
||||
# 2019.0519, 'segmentIndex': 2}, {'pitchDegrees': 27.093596, 'azimuthDegrees': 132.60162, 'panelsCount': 3,
|
||||
# 'yearlyEnergyDcKwh': 1354.4171, 'segmentIndex': 3}, {'pitchDegrees': 31.666294, 'azimuthDegrees': 308.42334,
|
||||
# 'panelsCount': 2, 'yearlyEnergyDcKwh': 743.65497, 'segmentIndex': 4}]}, {'panelsCount': 27, 'yearlyEnergyDcKwh':
|
||||
# 11579.401, 'roofSegmentSummaries': [{'pitchDegrees': 31.443022, 'azimuthDegrees': 218.25331, 'panelsCount': 15,
|
||||
# 'yearlyEnergyDcKwh': 6784.1484, 'segmentIndex': 0}, {'pitchDegrees': 34.39779, 'azimuthDegrees': 31.74401,
|
||||
# 'panelsCount': 2, 'yearlyEnergyDcKwh': 678.1277, 'segmentIndex': 1}, {'pitchDegrees': 3.0681775, 'azimuthDegrees':
|
||||
# 301.1099, 'panelsCount': 5, 'yearlyEnergyDcKwh': 2019.0519, 'segmentIndex': 2}, {'pitchDegrees': 27.093596,
|
||||
# 'azimuthDegrees': 132.60162, 'panelsCount': 3, 'yearlyEnergyDcKwh': 1354.4171, 'segmentIndex': 3}, {'pitchDegrees':
|
||||
# 31.666294, 'azimuthDegrees': 308.42334, 'panelsCount': 2, 'yearlyEnergyDcKwh': 743.65497, 'segmentIndex': 4}]},
|
||||
# {'panelsCount': 28, 'yearlyEnergyDcKwh': 11919.106, 'roofSegmentSummaries': [{'pitchDegrees': 31.443022,
|
||||
# 'azimuthDegrees': 218.25331, 'panelsCount': 15, 'yearlyEnergyDcKwh': 6784.1484, 'segmentIndex': 0},
|
||||
# {'pitchDegrees': 34.39779, 'azimuthDegrees': 31.74401, 'panelsCount': 3, 'yearlyEnergyDcKwh': 1017.83356,
|
||||
# 'segmentIndex': 1}, {'pitchDegrees': 3.0681775, 'azimuthDegrees': 301.1099, 'panelsCount': 5, 'yearlyEnergyDcKwh':
|
||||
# 2019.0519, 'segmentIndex': 2}, {'pitchDegrees': 27.093596, 'azimuthDegrees': 132.60162, 'panelsCount': 3,
|
||||
# 'yearlyEnergyDcKwh': 1354.4171, 'segmentIndex': 3}, {'pitchDegrees': 31.666294, 'azimuthDegrees': 308.42334,
|
||||
# 'panelsCount': 2, 'yearlyEnergyDcKwh': 743.65497, 'segmentIndex': 4}]}, {'panelsCount': 29, 'yearlyEnergyDcKwh':
|
||||
# 12255.358, 'roofSegmentSummaries': [{'pitchDegrees': 31.443022, 'azimuthDegrees': 218.25331, 'panelsCount': 15,
|
||||
# 'yearlyEnergyDcKwh': 6784.1484, 'segmentIndex': 0}, {'pitchDegrees': 34.39779, 'azimuthDegrees': 31.74401,
|
||||
# 'panelsCount': 4, 'yearlyEnergyDcKwh': 1354.0854, 'segmentIndex': 1}, {'pitchDegrees': 3.0681775, 'azimuthDegrees':
|
||||
# 301.1099, 'panelsCount': 5, 'yearlyEnergyDcKwh': 2019.0519, 'segmentIndex': 2}, {'pitchDegrees': 27.093596,
|
||||
# 'azimuthDegrees': 132.60162, 'panelsCount': 3, 'yearlyEnergyDcKwh': 1354.4171, 'segmentIndex': 3}, {'pitchDegrees':
|
||||
# 31.666294, 'azimuthDegrees': 308.42334, 'panelsCount': 2, 'yearlyEnergyDcKwh': 743.65497, 'segmentIndex': 4}]},
|
||||
# {'panelsCount': 30, 'yearlyEnergyDcKwh': 12586.448, 'roofSegmentSummaries': [{'pitchDegrees': 31.443022,
|
||||
# 'azimuthDegrees': 218.25331, 'panelsCount': 15, 'yearlyEnergyDcKwh': 6784.1484, 'segmentIndex': 0},
|
||||
# {'pitchDegrees': 34.39779, 'azimuthDegrees': 31.74401, 'panelsCount': 5, 'yearlyEnergyDcKwh': 1685.1748,
|
||||
# 'segmentIndex': 1}, {'pitchDegrees': 3.0681775, 'azimuthDegrees': 301.1099, 'panelsCount': 5, 'yearlyEnergyDcKwh':
|
||||
# 2019.0519, 'segmentIndex': 2}, {'pitchDegrees': 27.093596, 'azimuthDegrees': 132.60162, 'panelsCount': 3,
|
||||
# 'yearlyEnergyDcKwh': 1354.4171, 'segmentIndex': 3}, {'pitchDegrees': 31.666294, 'azimuthDegrees': 308.42334,
|
||||
# 'panelsCount': 2, 'yearlyEnergyDcKwh': 743.65497, 'segmentIndex': 4}]}, {'panelsCount': 31, 'yearlyEnergyDcKwh':
|
||||
# 12911.502, 'roofSegmentSummaries': [{'pitchDegrees': 31.443022, 'azimuthDegrees': 218.25331, 'panelsCount': 15,
|
||||
# 'yearlyEnergyDcKwh': 6784.1484, 'segmentIndex': 0}, {'pitchDegrees': 34.39779, 'azimuthDegrees': 31.74401,
|
||||
# 'panelsCount': 6, 'yearlyEnergyDcKwh': 2010.2289, 'segmentIndex': 1}, {'pitchDegrees': 3.0681775, 'azimuthDegrees':
|
||||
# 301.1099, 'panelsCount': 5, 'yearlyEnergyDcKwh': 2019.0519, 'segmentIndex': 2}, {'pitchDegrees': 27.093596,
|
||||
# 'azimuthDegrees': 132.60162, 'panelsCount': 3, 'yearlyEnergyDcKwh': 1354.4171, 'segmentIndex': 3}, {'pitchDegrees':
|
||||
# 31.666294, 'azimuthDegrees': 308.42334, 'panelsCount': 2, 'yearlyEnergyDcKwh': 743.65497, 'segmentIndex': 4}]},
|
||||
# {'panelsCount': 32, 'yearlyEnergyDcKwh': 13233.139, 'roofSegmentSummaries': [{'pitchDegrees': 31.443022,
|
||||
# 'azimuthDegrees': 218.25331, 'panelsCount': 15, 'yearlyEnergyDcKwh': 6784.1484, 'segmentIndex': 0},
|
||||
# {'pitchDegrees': 34.39779, 'azimuthDegrees': 31.74401, 'panelsCount': 7, 'yearlyEnergyDcKwh': 2331.8652,
|
||||
# 'segmentIndex': 1}, {'pitchDegrees': 3.0681775, 'azimuthDegrees': 301.1099, 'panelsCount': 5, 'yearlyEnergyDcKwh':
|
||||
# 2019.0519, 'segmentIndex': 2}, {'pitchDegrees': 27.093596, 'azimuthDegrees': 132.60162, 'panelsCount': 3,
|
||||
# 'yearlyEnergyDcKwh': 1354.4171, 'segmentIndex': 3}, {'pitchDegrees': 31.666294, 'azimuthDegrees': 308.42334,
|
||||
# 'panelsCount': 2, 'yearlyEnergyDcKwh': 743.65497, 'segmentIndex': 4}]}, {'panelsCount': 33, 'yearlyEnergyDcKwh':
|
||||
# 13554.602, 'roofSegmentSummaries': [{'pitchDegrees': 31.443022, 'azimuthDegrees': 218.25331, 'panelsCount': 15,
|
||||
# 'yearlyEnergyDcKwh': 6784.1484, 'segmentIndex': 0}, {'pitchDegrees': 34.39779, 'azimuthDegrees': 31.74401,
|
||||
# 'panelsCount': 8, 'yearlyEnergyDcKwh': 2653.3286, 'segmentIndex': 1}, {'pitchDegrees': 3.0681775, 'azimuthDegrees':
|
||||
# 301.1099, 'panelsCount': 5, 'yearlyEnergyDcKwh': 2019.0519, 'segmentIndex': 2}, {'pitchDegrees': 27.093596,
|
||||
# 'azimuthDegrees': 132.60162, 'panelsCount': 3, 'yearlyEnergyDcKwh': 1354.4171, 'segmentIndex': 3}, {'pitchDegrees':
|
||||
# 31.666294, 'azimuthDegrees': 308.42334, 'panelsCount': 2, 'yearlyEnergyDcKwh': 743.65497, 'segmentIndex': 4}]},
|
||||
# {'panelsCount': 34, 'yearlyEnergyDcKwh': 13893.903, 'roofSegmentSummaries': [{'pitchDegrees': 31.443022,
|
||||
# 'azimuthDegrees': 218.25331, 'panelsCount': 15, 'yearlyEnergyDcKwh': 6784.1484, 'segmentIndex': 0},
|
||||
# {'pitchDegrees': 34.39779, 'azimuthDegrees': 31.74401, 'panelsCount': 9, 'yearlyEnergyDcKwh': 2992.6301,
|
||||
# 'segmentIndex': 1}, {'pitchDegrees': 3.0681775, 'azimuthDegrees': 301.1099, 'panelsCount': 5, 'yearlyEnergyDcKwh':
|
||||
# 2019.0519, 'segmentIndex': 2}, {'pitchDegrees': 27.093596, 'azimuthDegrees': 132.60162, 'panelsCount': 3,
|
||||
# 'yearlyEnergyDcKwh': 1354.4171, 'segmentIndex': 3}, {'pitchDegrees': 31.666294, 'azimuthDegrees': 308.42334,
|
||||
# 'panelsCount': 2, 'yearlyEnergyDcKwh': 743.65497, 'segmentIndex': 4}]}, {'panelsCount': 35, 'yearlyEnergyDcKwh':
|
||||
# 14221.166, 'roofSegmentSummaries': [{'pitchDegrees': 31.443022, 'azimuthDegrees': 218.25331, 'panelsCount': 15,
|
||||
# 'yearlyEnergyDcKwh': 6784.1484, 'segmentIndex': 0}, {'pitchDegrees': 34.39779, 'azimuthDegrees': 31.74401,
|
||||
# 'panelsCount': 10, 'yearlyEnergyDcKwh': 3319.893, 'segmentIndex': 1}, {'pitchDegrees': 3.0681775, 'azimuthDegrees':
|
||||
# 301.1099, 'panelsCount': 5, 'yearlyEnergyDcKwh': 2019.0519, 'segmentIndex': 2}, {'pitchDegrees': 27.093596,
|
||||
# 'azimuthDegrees': 132.60162, 'panelsCount': 3, 'yearlyEnergyDcKwh': 1354.4171, 'segmentIndex': 3}, {'pitchDegrees':
|
||||
# 31.666294, 'azimuthDegrees': 308.42334, 'panelsCount': 2, 'yearlyEnergyDcKwh': 743.65497, 'segmentIndex': 4}]},
|
||||
# {'panelsCount': 36, 'yearlyEnergyDcKwh': 14536.154, 'roofSegmentSummaries': [{'pitchDegrees': 31.443022,
|
||||
# 'azimuthDegrees': 218.25331, 'panelsCount': 15, 'yearlyEnergyDcKwh': 6784.1484, 'segmentIndex': 0},
|
||||
# {'pitchDegrees': 34.39779, 'azimuthDegrees': 31.74401, 'panelsCount': 11, 'yearlyEnergyDcKwh': 3634.8809,
|
||||
# 'segmentIndex': 1}, {'pitchDegrees': 3.0681775, 'azimuthDegrees': 301.1099, 'panelsCount': 5, 'yearlyEnergyDcKwh':
|
||||
# 2019.0519, 'segmentIndex': 2}, {'pitchDegrees': 27.093596, 'azimuthDegrees': 132.60162, 'panelsCount': 3,
|
||||
# 'yearlyEnergyDcKwh': 1354.4171, 'segmentIndex': 3}, {'pitchDegrees': 31.666294, 'azimuthDegrees': 308.42334,
|
||||
# 'panelsCount': 2, 'yearlyEnergyDcKwh': 743.65497, 'segmentIndex': 4}]}, {'panelsCount': 37, 'yearlyEnergyDcKwh':
|
||||
# 14850.317, 'roofSegmentSummaries': [{'pitchDegrees': 31.443022, 'azimuthDegrees': 218.25331, 'panelsCount': 15,
|
||||
# 'yearlyEnergyDcKwh': 6784.1484, 'segmentIndex': 0}, {'pitchDegrees': 34.39779, 'azimuthDegrees': 31.74401,
|
||||
# 'panelsCount': 12, 'yearlyEnergyDcKwh': 3949.0444, 'segmentIndex': 1}, {'pitchDegrees': 3.0681775,
|
||||
# 'azimuthDegrees': 301.1099, 'panelsCount': 5, 'yearlyEnergyDcKwh': 2019.0519, 'segmentIndex': 2}, {'pitchDegrees':
|
||||
# 27.093596, 'azimuthDegrees': 132.60162, 'panelsCount': 3, 'yearlyEnergyDcKwh': 1354.4171, 'segmentIndex': 3},
|
||||
# {'pitchDegrees': 31.666294, 'azimuthDegrees': 308.42334, 'panelsCount': 2, 'yearlyEnergyDcKwh': 743.65497,
|
||||
# 'segmentIndex': 4}]}, {'panelsCount': 38, 'yearlyEnergyDcKwh': 15160.658, 'roofSegmentSummaries': [{'pitchDegrees':
|
||||
# 31.443022, 'azimuthDegrees': 218.25331, 'panelsCount': 15, 'yearlyEnergyDcKwh': 6784.1484, 'segmentIndex': 0},
|
||||
# {'pitchDegrees': 34.39779, 'azimuthDegrees': 31.74401, 'panelsCount': 13, 'yearlyEnergyDcKwh': 4259.385,
|
||||
# 'segmentIndex': 1}, {'pitchDegrees': 3.0681775, 'azimuthDegrees': 301.1099, 'panelsCount': 5, 'yearlyEnergyDcKwh':
|
||||
# 2019.0519, 'segmentIndex': 2}, {'pitchDegrees': 27.093596, 'azimuthDegrees': 132.60162, 'panelsCount': 3,
|
||||
# 'yearlyEnergyDcKwh': 1354.4171, 'segmentIndex': 3}, {'pitchDegrees': 31.666294, 'azimuthDegrees': 308.42334,
|
||||
# 'panelsCount': 2, 'yearlyEnergyDcKwh': 743.65497, 'segmentIndex': 4}]}, {'panelsCount': 39, 'yearlyEnergyDcKwh':
|
||||
# 15438.986, 'roofSegmentSummaries': [{'pitchDegrees': 31.443022, 'azimuthDegrees': 218.25331, 'panelsCount': 15,
|
||||
# 'yearlyEnergyDcKwh': 6784.1484, 'segmentIndex': 0}, {'pitchDegrees': 34.39779, 'azimuthDegrees': 31.74401,
|
||||
# 'panelsCount': 14, 'yearlyEnergyDcKwh': 4537.713, 'segmentIndex': 1}, {'pitchDegrees': 3.0681775, 'azimuthDegrees':
|
||||
# 301.1099, 'panelsCount': 5, 'yearlyEnergyDcKwh': 2019.0519, 'segmentIndex': 2}, {'pitchDegrees': 27.093596,
|
||||
# 'azimuthDegrees': 132.60162, 'panelsCount': 3, 'yearlyEnergyDcKwh': 1354.4171, 'segmentIndex': 3}, {'pitchDegrees':
|
||||
# 31.666294, 'azimuthDegrees': 308.42334, 'panelsCount': 2, 'yearlyEnergyDcKwh': 743.65497, 'segmentIndex': 4}]}],
|
||||
# 'panelCapacityWatts': 400, 'panelHeightMeters': 1.879, 'panelWidthMeters': 1.045, 'panelLifetimeYears': 20,
|
||||
# 'buildingStats': {'areaMeters2': 138.38115, 'sunshineQuantiles': [537, 728.5604, 799.23975, 833.99713, 900.88086,
|
||||
# 959.65875, 1024.2743, 1086.1285, 1132.8774, 1162.1904, 1193.6012], 'groundAreaMeters2': 117.16}, 'solarPanels': [{
|
||||
# 'center': {'latitude': 50.667371499999994, 'longitude': -4.7417235}, 'orientation': 'LANDSCAPE',
|
||||
# 'yearlyEnergyDcKwh': 468.5037, 'segmentIndex': 0}, {'center': {'latitude': 50.6673614, 'longitude': -4.7417023},
|
||||
# 'orientation': 'LANDSCAPE', 'yearlyEnergyDcKwh': 467.61072, 'segmentIndex': 0}, {'center': {'latitude':
|
||||
# 50.667365100000005, 'longitude': -4.7417311}, 'orientation': 'LANDSCAPE', 'yearlyEnergyDcKwh': 465.55005,
|
||||
# 'segmentIndex': 0}, {'center': {'latitude': 50.6673512, 'longitude': -4.741681000000001}, 'orientation':
|
||||
# 'LANDSCAPE', 'yearlyEnergyDcKwh': 465.48712, 'segmentIndex': 0}, {'center': {'latitude': 50.667357599999995,
|
||||
# 'longitude': -4.7416734}, 'orientation': 'LANDSCAPE', 'yearlyEnergyDcKwh': 467.8553, 'segmentIndex': 0},
|
||||
# {'center': {'latitude': 50.6673779, 'longitude': -4.741715999999999}, 'orientation': 'LANDSCAPE',
|
||||
# 'yearlyEnergyDcKwh': 464.84396, 'segmentIndex': 0}, {'center': {'latitude': 50.6673678, 'longitude': -4.7416947},
|
||||
# 'orientation': 'LANDSCAPE', 'yearlyEnergyDcKwh': 464.79984, 'segmentIndex': 0}, {'center': {'latitude': 50.6673549,
|
||||
# 'longitude': -4.7417098}, 'orientation': 'LANDSCAPE', 'yearlyEnergyDcKwh': 461.58975, 'segmentIndex': 0},
|
||||
# {'center': {'latitude': 50.6673816, 'longitude': -4.7417448}, 'orientation': 'LANDSCAPE', 'yearlyEnergyDcKwh':
|
||||
# 461.48065, 'segmentIndex': 0}, {'center': {'latitude': 50.6673881, 'longitude': -4.7417372}, 'orientation':
|
||||
# 'LANDSCAPE', 'yearlyEnergyDcKwh': 458.3733, 'segmentIndex': 0}, {'center': {'latitude': 50.6673149, 'longitude':
|
||||
# -4.7416768}, 'orientation': 'LANDSCAPE', 'yearlyEnergyDcKwh': 457.68268, 'segmentIndex': 3}, {'center': {
|
||||
# 'latitude': 50.6673204, 'longitude': -4.7416867}, 'orientation': 'LANDSCAPE', 'yearlyEnergyDcKwh': 456.06827,
|
||||
# 'segmentIndex': 3}, {'center': {'latitude': 50.667375199999995, 'longitude': -4.7417524}, 'orientation':
|
||||
# 'LANDSCAPE', 'yearlyEnergyDcKwh': 453.20776, 'segmentIndex': 0}, {'center': {'latitude': 50.667364, 'longitude':
|
||||
# -4.7416659}, 'orientation': 'LANDSCAPE', 'yearlyEnergyDcKwh': 448.61087, 'segmentIndex': 0}, {'center': {
|
||||
# 'latitude': 50.6673094, 'longitude': -4.741666899999999}, 'orientation': 'LANDSCAPE', 'yearlyEnergyDcKwh':
|
||||
# 440.66626, 'segmentIndex': 3}, {'center': {'latitude': 50.667403799999995, 'longitude': -4.741588900000001},
|
||||
# 'orientation': 'LANDSCAPE', 'yearlyEnergyDcKwh': 419.31348, 'segmentIndex': 2}, {'center': {'latitude':
|
||||
# 50.66740850000001, 'longitude': -4.7416016999999995}, 'orientation': 'LANDSCAPE', 'yearlyEnergyDcKwh': 418.74448,
|
||||
# 'segmentIndex': 2}, {'center': {'latitude': 50.6673688, 'longitude': -4.7417599}, 'orientation': 'LANDSCAPE',
|
||||
# 'yearlyEnergyDcKwh': 413.877, 'segmentIndex': 0}, {'center': {'latitude': 50.667348499999996, 'longitude':
|
||||
# -4.7417174}, 'orientation': 'LANDSCAPE', 'yearlyEnergyDcKwh': 411.76657, 'segmentIndex': 0}, {'center': {
|
||||
# 'latitude': 50.6673587, 'longitude': -4.7417387}, 'orientation': 'LANDSCAPE', 'yearlyEnergyDcKwh': 410.5925,
|
||||
# 'segmentIndex': 0}, {'center': {'latitude': 50.6673992, 'longitude': -4.7415761}, 'orientation': 'LANDSCAPE',
|
||||
# 'yearlyEnergyDcKwh': 404.15607, 'segmentIndex': 2}, {'center': {'latitude': 50.6674132, 'longitude': -4.7416145},
|
||||
# 'orientation': 'LANDSCAPE', 'yearlyEnergyDcKwh': 403.29822, 'segmentIndex': 2}, {'center': {'latitude': 50.6673324,
|
||||
# 'longitude': -4.7417015}, 'orientation': 'PORTRAIT', 'yearlyEnergyDcKwh': 378.2754, 'segmentIndex': 4}, {'center':
|
||||
# {'latitude': 50.667417799999996, 'longitude': -4.7416273}, 'orientation': 'LANDSCAPE', 'yearlyEnergyDcKwh':
|
||||
# 373.53967, 'segmentIndex': 2}, {'center': {'latitude': 50.667324900000004, 'longitude': -4.7417104}, 'orientation':
|
||||
# 'PORTRAIT', 'yearlyEnergyDcKwh': 365.37958, 'segmentIndex': 4}, {'center': {'latitude': 50.6674043, 'longitude':
|
||||
# -4.741680800000001}, 'orientation': 'PORTRAIT', 'yearlyEnergyDcKwh': 341.4827, 'segmentIndex': 1}, {'center': {
|
||||
# 'latitude': 50.667392299999996, 'longitude': -4.7416919}, 'orientation': 'PORTRAIT', 'yearlyEnergyDcKwh':
|
||||
# 336.64502, 'segmentIndex': 1}, {'center': {'latitude': 50.667397, 'longitude': -4.741704599999999}, 'orientation':
|
||||
# 'PORTRAIT', 'yearlyEnergyDcKwh': 339.7059, 'segmentIndex': 1}, {'center': {'latitude': 50.6674018, 'longitude':
|
||||
# -4.7417174}, 'orientation': 'PORTRAIT', 'yearlyEnergyDcKwh': 336.25195, 'segmentIndex': 1}, {'center': {'latitude':
|
||||
# 50.6673875, 'longitude': -4.7416791}, 'orientation': 'PORTRAIT', 'yearlyEnergyDcKwh': 331.08936, 'segmentIndex':
|
||||
# 1}, {'center': {'latitude': 50.6674065, 'longitude': -4.7417301}, 'orientation': 'PORTRAIT', 'yearlyEnergyDcKwh':
|
||||
# 325.05405, 'segmentIndex': 1}, {'center': {'latitude': 50.6673828, 'longitude': -4.7416664}, 'orientation':
|
||||
# 'PORTRAIT', 'yearlyEnergyDcKwh': 321.63647, 'segmentIndex': 1}, {'center': {'latitude': 50.667378, 'longitude':
|
||||
# -4.741653599999999}, 'orientation': 'PORTRAIT', 'yearlyEnergyDcKwh': 321.46332, 'segmentIndex': 1}, {'center': {
|
||||
# 'latitude': 50.667373299999994, 'longitude': -4.7416409}, 'orientation': 'PORTRAIT', 'yearlyEnergyDcKwh': 339.3016,
|
||||
# 'segmentIndex': 1}, {'center': {'latitude': 50.6673853, 'longitude': -4.7416298}, 'orientation': 'PORTRAIT',
|
||||
# 'yearlyEnergyDcKwh': 327.26282, 'segmentIndex': 1}, {'center': {'latitude': 50.667399499999995, 'longitude':
|
||||
# -4.741668}, 'orientation': 'PORTRAIT', 'yearlyEnergyDcKwh': 314.9878, 'segmentIndex': 1}, {'center': {'latitude':
|
||||
# 50.6673948, 'longitude': -4.7416553}, 'orientation': 'PORTRAIT', 'yearlyEnergyDcKwh': 314.16364, 'segmentIndex':
|
||||
# 1}, {'center': {'latitude': 50.667390000000005, 'longitude': -4.7416425}, 'orientation': 'PORTRAIT',
|
||||
# 'yearlyEnergyDcKwh': 310.3404, 'segmentIndex': 1}, {'center': {'latitude': 50.6674186, 'longitude': -4.7417191},
|
||||
# 'orientation': 'PORTRAIT', 'yearlyEnergyDcKwh': 278.3281, 'segmentIndex': 1}]}, 'boundingBox': {'sw': {'latitude':
|
||||
# 50.6672904, 'longitude': -4.741778}, 'ne': {'latitude': 50.667431199999996, 'longitude': -4.7415536}},
|
||||
# 'imageryQuality': 'MEDIUM', 'imageryProcessedDate': {'year': 2024, 'month': 4, 'day': 18}}
|
||||
self.insights_data = self.get_building_insights(longitude, latitude, required_quality)
|
||||
|
||||
# Extract key data from the insights response
|
||||
self.roof_segments = self.insights_data["solarPotential"].get('roofSegmentStats', [])
|
||||
self.floor_area = self.insights_data["solarPotential"]["wholeRoofStats"]['groundAreaMeters2']
|
||||
self.roof_area = self.insights_data["solarPotential"]["wholeRoofStats"]['areaMeters2']
|
||||
self.panel_area = (
|
||||
self.insights_data["solarPotential"]["panelHeightMeters"] *
|
||||
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()
|
||||
|
||||
self.roof_segment_indexes = [segment['segmentIndex'] for segment in self.roof_segments]
|
||||
|
||||
# We now start finding the solar panel configurations
|
||||
self.optimise_solar_configuration()
|
||||
|
||||
@staticmethod
|
||||
def lifetime_production_ac_kwh(
|
||||
row,
|
||||
efficiency_depreciation_factor,
|
||||
installation_life_span
|
||||
):
|
||||
"""
|
||||
Mimics the function described in the Google Solar API documentation, presenting the lifetime production
|
||||
AC KWH as a geometri sum
|
||||
"""
|
||||
|
||||
return (
|
||||
row["initial_ac_kwh_per_year"] *
|
||||
(1 - pow(
|
||||
efficiency_depreciation_factor,
|
||||
installation_life_span)) /
|
||||
(1 - efficiency_depreciation_factor))
|
||||
|
||||
@staticmethod
|
||||
def annualUtilityBillEstimate(
|
||||
yearlyKWhEnergyConsumption,
|
||||
initialAcKwhPerYear,
|
||||
efficiencyDepreciationFactor,
|
||||
year,
|
||||
costIncreaseFactor,
|
||||
discountRate):
|
||||
"""
|
||||
Implements the bill costing model for esimating annual bill
|
||||
:param yearlyKWhEnergyConsumption:
|
||||
:param initialAcKwhPerYear:
|
||||
:param efficiencyDepreciationFactor:
|
||||
:param year:
|
||||
:param costIncreaseFactor:
|
||||
:param discountRate:
|
||||
:return:
|
||||
"""
|
||||
|
||||
return (
|
||||
billCostModel(
|
||||
yearlyKWhEnergyConsumption -
|
||||
annualProduction(
|
||||
initialAcKwhPerYear,
|
||||
efficiencyDepreciationFactor,
|
||||
year)) *
|
||||
pow(costIncreaseFactor, year) /
|
||||
pow(discountRate, year))
|
||||
|
||||
def lifetimeUtilityBill(
|
||||
yearlyKWhEnergyConsumption,
|
||||
initialAcKwhPerYear,
|
||||
efficiencyDepreciationFactor,
|
||||
installationLifeSpan,
|
||||
costIncreaseFactor,
|
||||
discountRate):
|
||||
bill = [0] * installationLifeSpan
|
||||
for year in range(installationLifeSpan):
|
||||
bill[year] = annualUtilityBillEstimate(
|
||||
yearlyKWhEnergyConsumption,
|
||||
initialAcKwhPerYear,
|
||||
efficiencyDepreciationFactor,
|
||||
year,
|
||||
costIncreaseFactor,
|
||||
discountRate)
|
||||
return bill
|
||||
|
||||
def estimate_solar_costs(self, panel_performance):
|
||||
"""
|
||||
This method implements the recommended costing approach, to estimate the ROI of a solar panel
|
||||
configuration, as described in the Google Solar API documentation
|
||||
:param panel_performance: dataframe containing the solar panel array configuration and energy generation data
|
||||
:return:
|
||||
"""
|
||||
|
||||
# we now estiamte the financial benefits of solar panels for the household, using the framework described
|
||||
# by the Google Solar API
|
||||
# 1) Convert Solar Energy AD production from the DC production
|
||||
panel_performance["initial_ac_kwh_per_year"] = panel_performance["yearly_dc_energy"] * self.dc_to_ac_rate
|
||||
|
||||
# Remove anything where the total ac energy is less than half of the array wattage
|
||||
panel_performance = panel_performance[
|
||||
(panel_performance["initial_ac_kwh_per_year"] / panel_performance["array_warrage"]) >= 0.5
|
||||
]
|
||||
|
||||
# 2) Calculate the liftime solar energy production
|
||||
panel_performance['lifetime_ac_kwh'] = panel_performance.apply(
|
||||
self.lifetime_production_ac_kwh,
|
||||
axis=1,
|
||||
efficiency_depreciation_factor=self.efficiency_depreciation_factor,
|
||||
installation_life_span=self.installation_life_span
|
||||
)
|
||||
|
||||
# TODO: Complete the rest of the solar model
|
||||
|
||||
def optimise_solar_configuration(self):
|
||||
"""
|
||||
Optimise the solar panel configuration for the building.
|
||||
:return:
|
||||
"""
|
||||
|
||||
# Remove any north facing roof segments
|
||||
panel_performance = []
|
||||
for config in self.insights_data["solarPotential"]["solarPanelConfigs"]:
|
||||
roof_segment_summaries = config["roofSegmentSummaries"]
|
||||
# Filter on just the segments in self.roof_segment_indexes
|
||||
roof_segment_summaries = [
|
||||
segment for segment in roof_segment_summaries if segment["segmentIndex"] in self.roof_segment_indexes
|
||||
]
|
||||
|
||||
roi_summary = []
|
||||
for segment in roof_segment_summaries:
|
||||
wattage = segment["panelsCount"] * self.insights_data["solarPotential"]["panelCapacityWatts"]
|
||||
generated_dc_energy = segment["yearlyEnergyDcKwh"]
|
||||
ratio = generated_dc_energy / wattage
|
||||
cost = MCS_SOLAR_PV_COST_DATA["average_cost_per_kwh"] * (generated_dc_energy / 1000)
|
||||
roi_summary.append(
|
||||
{
|
||||
"segmentIndex": segment["segmentIndex"],
|
||||
"wattage": wattage,
|
||||
"generated_dc_energy": generated_dc_energy,
|
||||
"ratio": ratio,
|
||||
"n_panels": segment["panelsCount"],
|
||||
"cost": cost,
|
||||
"panneled_roof_area": self.panel_area * int(segment["panelsCount"])
|
||||
}
|
||||
)
|
||||
|
||||
roi_summary = pd.DataFrame(roi_summary)
|
||||
|
||||
weighted_ratio = np.average(
|
||||
roi_summary["ratio"].values, weights=roi_summary["generated_dc_energy"].values
|
||||
)
|
||||
total_cost = roi_summary["cost"].sum()
|
||||
yearly_dc_energy = roi_summary["generated_dc_energy"].sum()
|
||||
|
||||
panel_performance.append(
|
||||
{
|
||||
"n_panels": roi_summary["n_panels"].sum(),
|
||||
"yearly_dc_energy": yearly_dc_energy,
|
||||
"total_cost": total_cost,
|
||||
"weighted_ratio": weighted_ratio,
|
||||
"panneled_roof_area": roi_summary["panneled_roof_area"].sum(),
|
||||
"array_warrage": roi_summary["n_panels"].sum() * self.panel_wattage
|
||||
}
|
||||
)
|
||||
|
||||
panel_performance = pd.DataFrame(panel_performance)
|
||||
# We can have duplicate configurations
|
||||
panel_performance = panel_performance.drop_duplicates()
|
||||
# Ensure more than 4 panels
|
||||
panel_performance = panel_performance[panel_performance["n_panels"] >= 4]
|
||||
|
||||
self.estimate_solar_costs()
|
||||
|
||||
# This first bracket is the value of the energy bill savings
|
||||
panel_performance["bill_savings"] = (
|
||||
self.SOLAR_CONSUMPTION_PROPORTION *
|
||||
panel_performance["total_energy"] *
|
||||
AnnualBillSavings.ELECTRICITY_PRICE_CAP
|
||||
)
|
||||
# This is the amount of energy exported
|
||||
panel_performance["export_value"] = (
|
||||
(1 - self.SOLAR_CONSUMPTION_PROPORTION) *
|
||||
panel_performance["total_energy"] *
|
||||
AnnualBillSavings.ELECTRICITY_EXPORT_PAYMENT
|
||||
)
|
||||
panel_performance["energy_value"] = panel_performance["bill_savings"] + panel_performance["export_value"]
|
||||
panel_performance["payback_years"] = panel_performance["total_cost"] / panel_performance["energy_value"]
|
||||
|
||||
panel_performance = panel_performance.sort_values("weighted_ratio", ascending=False)
|
||||
# TODO: Finish this!!
|
||||
|
||||
panel_performance["roof_area_percentage"] = panel_performance["panneled_roof_area"] / self.roof_area
|
||||
|
||||
self.panel_performance = panel_performance
|
||||
|
||||
def exclude_north_facing_segments(self):
|
||||
"""
|
||||
Filter out any north-facing roof segments from the roof_segments attribute.
|
||||
|
||||
North-facing segments are defined as those with an azimuth between -30 and 30 degrees.
|
||||
"""
|
||||
|
||||
filtered_segments = []
|
||||
for segment_index, segment in enumerate(self.roof_segments):
|
||||
segment["segmentIndex"] = segment_index
|
||||
# Check if the segment is north-facing
|
||||
if self.NORTH_FACING_AZIMUTH_RANGE[0] <= segment['azimuthDegrees'] <= self.NORTH_FACING_AZIMUTH_RANGE[1]:
|
||||
continue
|
||||
|
||||
filtered_segments.append(segment)
|
||||
|
||||
self.roof_segments = filtered_segments
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ class Settings(BaseSettings):
|
|||
PLAN_TRIGGER_BUCKET: str
|
||||
EPC_AUTH_TOKEN: str
|
||||
ORDNANCE_SURVEY_API_KEY: str
|
||||
GOOGLE_SOLAR_API_KEY: str
|
||||
DB_HOST: str
|
||||
DB_PASSWORD: str
|
||||
DB_USERNAME: str
|
||||
|
|
|
|||
|
|
@ -23,12 +23,13 @@ from backend.app.db.functions.recommendations_functions import (
|
|||
)
|
||||
from backend.app.db.models.portfolio import rating_lookup
|
||||
from backend.app.dependencies import validate_token
|
||||
from backend.app.plan.schemas import PlanTriggerRequest
|
||||
from backend.app.plan.schemas import PlanTriggerRequest, MdsRequest
|
||||
from backend.app.plan.utils import get_cleaned
|
||||
from backend.app.utils import epc_to_sap_lower_bound, sap_to_epc
|
||||
|
||||
from backend.ml_models.api import ModelApi
|
||||
from backend.Property import Property
|
||||
from backend.apis.GoogleSolarApi import GoogleSolarApi
|
||||
from etl.solar.SolarPhotoSupply import SolarPhotoSupply
|
||||
|
||||
from recommendations.optimiser.CostOptimiser import CostOptimiser
|
||||
|
|
@ -347,10 +348,14 @@ async def trigger_plan(body: PlanTriggerRequest):
|
|||
bucket_name=get_settings().DATA_BUCKET, file_key="spatial/filename_meta.parquet"
|
||||
)
|
||||
photo_supply_lookup, floor_area_decile_thresholds = SolarPhotoSupply.load(bucket=get_settings().DATA_BUCKET)
|
||||
solar_api_client = GoogleSolarApi(api_key=get_settings().GOOGLE_SOLAR_API_KEY)
|
||||
|
||||
logger.info("Getting spatial data")
|
||||
for p in input_properties:
|
||||
p.get_components(cleaned, photo_supply_lookup, floor_area_decile_thresholds)
|
||||
p.get_spatial_data(uprn_filenames)
|
||||
# Call Google Solar API
|
||||
solar_performance = solar_api_client.get(longitude=p.spatial["longitude"], latitude=p.spatial["latitude"])
|
||||
|
||||
logger.info("Getting components and epc recommendations")
|
||||
recommendations = {}
|
||||
|
|
@ -358,9 +363,6 @@ async def trigger_plan(body: PlanTriggerRequest):
|
|||
representative_recommendations = {}
|
||||
for p in tqdm(input_properties):
|
||||
|
||||
# Property recommendations
|
||||
p.get_components(cleaned, photo_supply_lookup, floor_area_decile_thresholds)
|
||||
|
||||
recommender = Recommendations(property_instance=p, materials=materials, exclusions=body.exclusions)
|
||||
property_recommendations, property_representative_recommendations = recommender.recommend()
|
||||
|
||||
|
|
@ -422,9 +424,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(
|
||||
|
|
@ -436,9 +436,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
|
||||
)
|
||||
|
||||
|
|
@ -622,7 +620,7 @@ async def trigger_plan(body: PlanTriggerRequest):
|
|||
|
||||
|
||||
@router.post("/mds")
|
||||
async def build_mds(body: PlanTriggerRequest):
|
||||
async def build_mds(body: MdsRequest):
|
||||
# TODO: This is a placeholder location for the MDS endpoint, which this is being assembled
|
||||
|
||||
logger.info("Connecting to db")
|
||||
|
|
@ -633,6 +631,8 @@ async def build_mds(body: PlanTriggerRequest):
|
|||
session.begin()
|
||||
logger.info("Getting the inputs")
|
||||
plan_input = read_csv_from_s3(bucket_name=get_settings().PLAN_TRIGGER_BUCKET, filepath=body.trigger_file_path)
|
||||
measure_set = body.measures
|
||||
optimise_measures = measure_set is not None
|
||||
|
||||
cleaning_data = read_dataframe_from_s3_parquet(
|
||||
bucket_name=get_settings().DATA_BUCKET, file_key="sap_change_model/cleaning_dataset.parquet",
|
||||
|
|
@ -659,10 +659,14 @@ async def build_mds(body: PlanTriggerRequest):
|
|||
epc_searcher.find_property(skip_os=True)
|
||||
|
||||
if config["address"] == "35b High Street":
|
||||
print("Performing temporary patch")
|
||||
print("Performing temporary patch on 35b High Street")
|
||||
epc_searcher.newest_epc["uprn"] = 10002911892
|
||||
epc_searcher.full_sap_epc["uprn"] = 10002911892
|
||||
|
||||
if config["address"] == "Cobnut Barn":
|
||||
print("Performing temporary patch on Cobnut Barn")
|
||||
epc_searcher.newest_epc["uprn"] = 10013924689
|
||||
|
||||
# Create a record in db
|
||||
# TODO: If we productionise the creation of this mds report, we will need to store this in the db
|
||||
# property_id, is_new = create_property(
|
||||
|
|
@ -706,7 +710,10 @@ async def build_mds(body: PlanTriggerRequest):
|
|||
# (x["address"] == config["address"]) and (x["postcode"] == config["postcode"])
|
||||
# ), {})
|
||||
|
||||
measures = config["measures"] if "measures" in config else None
|
||||
if measure_set is None:
|
||||
measures = config["measures"] if "measures" in config else None
|
||||
else:
|
||||
measures = measure_set
|
||||
|
||||
input_properties.append(
|
||||
Property(
|
||||
|
|
@ -737,24 +744,49 @@ async def build_mds(body: PlanTriggerRequest):
|
|||
logger.info("Getting components and epc recommendations")
|
||||
recommendations_scoring_data = []
|
||||
representative_recommendations = {}
|
||||
recommendations = {}
|
||||
|
||||
for p in tqdm(input_properties):
|
||||
|
||||
p.get_components(cleaned, photo_supply_lookup, floor_area_decile_thresholds)
|
||||
|
||||
mds = Mds(property_instance=p, materials=materials)
|
||||
property_representative_recommendations, errors = mds.build()
|
||||
mds = Mds(property_instance=p, materials=materials, optimise_measures=optimise_measures)
|
||||
mds_recommendations, property_representative_recommendations, errors = mds.build()
|
||||
|
||||
if errors:
|
||||
logger.info("Errors occurred during MDS build")
|
||||
if isinstance(errors, list):
|
||||
if errors:
|
||||
raise Exception("Errors occurred during MDS build")
|
||||
else:
|
||||
if any([len(x) for x in errors.values()]):
|
||||
raise Exception("Errors occurred during MDS build")
|
||||
|
||||
recommendations[p.id] = mds_recommendations
|
||||
representative_recommendations[p.id] = property_representative_recommendations
|
||||
|
||||
# Build the scoring data
|
||||
p.create_base_difference_epc_record(cleaned_lookup=cleaned)
|
||||
recommendations_scoring_data.append(
|
||||
p.simulate_all_representative_recommendations(property_representative_recommendations)
|
||||
)
|
||||
if optimise_measures:
|
||||
for _id, mds_recs in mds_recommendations.items():
|
||||
representative_ids = [r["recommendation_id"] for r in property_representative_recommendations[_id]]
|
||||
simulation_mds_recs = []
|
||||
for recs in mds_recs:
|
||||
simulation_mds_recs.append(
|
||||
[r for r in recs if r["recommendation_id"] in representative_ids]
|
||||
)
|
||||
|
||||
p.adjust_difference_record_with_recommendations(
|
||||
simulation_mds_recs, property_representative_recommendations[_id]
|
||||
)
|
||||
|
||||
data = p.recommendations_scoring_data.copy()
|
||||
for d in data:
|
||||
d["id"] = d["id"] + "*" + _id
|
||||
|
||||
recommendations_scoring_data.extend(data)
|
||||
|
||||
else:
|
||||
recommendations_scoring_data.append(
|
||||
p.simulate_all_representative_recommendations(property_representative_recommendations)
|
||||
)
|
||||
|
||||
logger.info("Preparing data for scoring in sap change api")
|
||||
recommendations_scoring_data = pd.DataFrame(recommendations_scoring_data)
|
||||
|
|
@ -787,13 +819,198 @@ async def build_mds(body: PlanTriggerRequest):
|
|||
for key, scored in predictions_dict.items():
|
||||
all_predictions[key] = pd.concat([all_predictions[key], scored])
|
||||
|
||||
# We now produce a table of results for the mds report
|
||||
# TODO: 1) walls_insulation_thickness_ending is not being set in the recommendations_scoring_data,
|
||||
# insulation_thickness_ending is being set instead
|
||||
# 2)
|
||||
|
||||
# TODO: TEMP
|
||||
for p in plan_input:
|
||||
if p["uprn"]:
|
||||
p["uprn"] = str(int(float(p["uprn"])))
|
||||
|
||||
import re
|
||||
from backend.ml_models.AnnualBillSavings import AnnualBillSavings
|
||||
|
||||
if optimise_measures:
|
||||
results = []
|
||||
for p in input_properties:
|
||||
|
||||
sap_before = int(p.data["current-energy-efficiency"])
|
||||
epc_before = p.data["current-energy-rating"]
|
||||
heat_demand_before = p.data["energy-consumption-current"]
|
||||
carbon_before = p.data["co2-emissions-current"]
|
||||
current_adjusted_energy = AnnualBillSavings.adjust_energy_to_metered(
|
||||
epc_energy_consumption=heat_demand_before * p.floor_area,
|
||||
current_epc_rating=epc_before,
|
||||
)
|
||||
current_energy_bill = AnnualBillSavings.calculate_annual_bill(current_adjusted_energy)
|
||||
|
||||
package_comparison = []
|
||||
for _id in recommendations[p.id].keys():
|
||||
|
||||
sap_prediction = all_predictions["sap_change_predictions"][
|
||||
(all_predictions["sap_change_predictions"]["property_id"] == str(p.id)) &
|
||||
(all_predictions["sap_change_predictions"]["recommendation_id"].str.contains(re.escape(_id)))
|
||||
].copy().reset_index(drop=True)
|
||||
sap_prediction["row_id"] = sap_prediction.index
|
||||
|
||||
heat_demand_prediction = all_predictions["heat_demand_predictions"][
|
||||
(all_predictions["heat_demand_predictions"]["property_id"] == str(p.id)) &
|
||||
(all_predictions["heat_demand_predictions"]["recommendation_id"].str.contains(re.escape(_id)))
|
||||
].copy().reset_index(drop=True)
|
||||
heat_demand_prediction["row_id"] = heat_demand_prediction.index
|
||||
|
||||
carbon_prediction = all_predictions["carbon_change_predictions"][
|
||||
(all_predictions["carbon_change_predictions"]["property_id"] == str(p.id)) &
|
||||
(all_predictions["carbon_change_predictions"]["recommendation_id"].str.contains(re.escape(_id)))
|
||||
].copy().reset_index(drop=True)
|
||||
carbon_prediction["row_id"] = carbon_prediction.index
|
||||
|
||||
epc_target = body.goal_value
|
||||
if epc_before == epc_target:
|
||||
continue
|
||||
|
||||
sap_target = epc_to_sap_lower_bound(epc_target)
|
||||
# Define the measures
|
||||
sap_threshold_barrier = sap_prediction[sap_prediction["predictions"] >= sap_target]
|
||||
meets_threshold = True
|
||||
if sap_threshold_barrier.empty:
|
||||
sap_threshold_barrier = sap_prediction.tail(1)
|
||||
meets_threshold = False
|
||||
sap_threshold_barrier = sap_threshold_barrier.head(1)
|
||||
|
||||
sap_prediction = sap_prediction[
|
||||
sap_prediction["row_id"] <= sap_threshold_barrier["row_id"].values[0]
|
||||
]
|
||||
heat_demand_prediction = heat_demand_prediction[
|
||||
heat_demand_prediction["row_id"] <= sap_threshold_barrier["row_id"].values[0]
|
||||
]
|
||||
carbon_prediction = carbon_prediction[
|
||||
carbon_prediction["row_id"] <= sap_threshold_barrier["row_id"].values[0]
|
||||
]
|
||||
|
||||
reverse_map = {v: k for k, v in Mds.format_map.items()}
|
||||
|
||||
selected_measures = [
|
||||
reverse_map[x.split("-")[0]] for x in sap_prediction["recommendation_id"].values
|
||||
]
|
||||
selected_measure_ids = [x.split("*")[0] for x in sap_prediction["recommendation_id"].values]
|
||||
|
||||
costs = [
|
||||
r["total"] for r in representative_recommendations[p.id][_id] if
|
||||
r["recommendation_id"] in selected_measure_ids
|
||||
]
|
||||
costs = sum(costs)
|
||||
|
||||
sap_after = sap_prediction["predictions"].values[-1]
|
||||
epc_after = sap_to_epc(sap_after)
|
||||
heat_demand_after = heat_demand_prediction["predictions"].values[-1]
|
||||
carbon_after = carbon_prediction["predictions"].values[-1]
|
||||
|
||||
expected_adjusted_energy = AnnualBillSavings.adjust_energy_to_metered(
|
||||
epc_energy_consumption=heat_demand_after * p.floor_area,
|
||||
current_epc_rating=epc_before,
|
||||
)
|
||||
|
||||
expected_energy_bill = AnnualBillSavings.calculate_annual_bill(expected_adjusted_energy)
|
||||
|
||||
bill_savings = current_energy_bill - expected_energy_bill
|
||||
energy_savings = current_adjusted_energy - expected_adjusted_energy
|
||||
|
||||
package_comparison.append(
|
||||
{
|
||||
"id": _id,
|
||||
"cost": costs,
|
||||
"measures": selected_measures,
|
||||
"sap_before": sap_before,
|
||||
"sap_after": sap_after,
|
||||
"epc_before": epc_before,
|
||||
"epc_after": epc_after,
|
||||
"heat_demand_before": heat_demand_before,
|
||||
"heat_demand_after": heat_demand_after,
|
||||
"carbon_before": carbon_before,
|
||||
"carbon_after": carbon_after,
|
||||
"bill_savings": bill_savings,
|
||||
"energy_savings": energy_savings,
|
||||
"current_energy_bill": current_energy_bill,
|
||||
"meets_threshold": meets_threshold
|
||||
}
|
||||
)
|
||||
|
||||
package_comparison = pd.DataFrame(package_comparison)
|
||||
# Find the smallest cost package
|
||||
if not package_comparison.empty:
|
||||
|
||||
# We check if any of the packages meet the threshold
|
||||
# If none of them do, take the one that gets closest to the target
|
||||
if package_comparison["meets_threshold"].any():
|
||||
package_comparison = package_comparison[package_comparison["meets_threshold"]]
|
||||
package_comparison = package_comparison.sort_values("cost")
|
||||
else:
|
||||
package_comparison = package_comparison.sort_values("sap_after", ascending=False)
|
||||
|
||||
package_comparison = package_comparison.head(1).to_dict("records")[0]
|
||||
else:
|
||||
package_comparison = {
|
||||
"measures": [],
|
||||
"sap_before": sap_before,
|
||||
"sap_after": sap_before,
|
||||
"epc_before": epc_before,
|
||||
"epc_after": epc_before,
|
||||
"heat_demand_before": heat_demand_before,
|
||||
"heat_demand_after": heat_demand_before,
|
||||
"carbon_before": carbon_before,
|
||||
"carbon_after": carbon_before,
|
||||
"bill_savings": 0,
|
||||
"energy_savings": 0,
|
||||
"current_energy_bill": current_energy_bill,
|
||||
"meets_threshold": False
|
||||
}
|
||||
|
||||
config = [c for c in plan_input if c["uprn"] == str(p.uprn)]
|
||||
if not config:
|
||||
config = {"address": None, "postcode": None}
|
||||
else:
|
||||
config = config[0]
|
||||
|
||||
results.append({
|
||||
"config_address": config["address"],
|
||||
"config_postcode": config["postcode"],
|
||||
"uprn": p.uprn,
|
||||
"address": p.address,
|
||||
"postcode": p.postcode,
|
||||
"measures": package_comparison["measures"],
|
||||
"year_of_epc": p.data['lodgement-date'],
|
||||
"sap_before": package_comparison["sap_before"],
|
||||
"sap_after": package_comparison["sap_after"],
|
||||
"epc_before": package_comparison["epc_before"],
|
||||
"epc_after": package_comparison["epc_after"],
|
||||
"heat_demand_before": package_comparison["heat_demand_before"],
|
||||
"heat_demand_after": package_comparison["heat_demand_after"],
|
||||
"carbon_before": package_comparison["carbon_before"],
|
||||
"carbon_after": package_comparison["carbon_after"],
|
||||
"bill_savings": round(package_comparison["bill_savings"], 2),
|
||||
"energy_savings": round(package_comparison["energy_savings"], 2),
|
||||
"current_energy_bill": round(package_comparison["current_energy_bill"], 2),
|
||||
"EWI": "EWI" if "external_wall_insulation" in package_comparison["measures"] else None,
|
||||
"CWI": "CWI" if "cavity_wall_insulation" in package_comparison["measures"] else None,
|
||||
"LI": "LI" if "loft_insulation" in package_comparison["measures"] else None,
|
||||
"ASHP Htg": "ASHP Htg" if "air_source_heat_pump" in package_comparison["measures"] else None,
|
||||
"Elec Storage": (
|
||||
"Elec Storage Htrs (Out of scope -Prov sum only)" if "high_heat_retention_storage_heaters" in
|
||||
package_comparison["measures"] else None
|
||||
),
|
||||
"Solar PV": "Solar PV" if "solar_pv" in package_comparison["measures"] else None,
|
||||
})
|
||||
|
||||
results = pd.DataFrame(results)
|
||||
|
||||
# For the different measures, we check the impact with a few debugging functions
|
||||
|
||||
walls_check, hhr_check = check_mds(results, input_properties, recommendations, optimise_measures)
|
||||
|
||||
results.to_excel("optimised mds_results 5th June.xlsx")
|
||||
|
||||
results = []
|
||||
for p in input_properties:
|
||||
measures = p.measures
|
||||
|
|
@ -842,11 +1059,14 @@ async def build_mds(body: PlanTriggerRequest):
|
|||
)
|
||||
|
||||
# TODO: We should determine if the home is gas & electricity or just electricity
|
||||
|
||||
# Determine if the heating and hotwater was previously electric only or both
|
||||
|
||||
current_energy_bill = AnnualBillSavings.calculate_annual_bill(
|
||||
current_adjusted_energy,
|
||||
kwh=current_adjusted_energy,
|
||||
)
|
||||
expected_energy_bill = AnnualBillSavings.calculate_annual_bill(
|
||||
expected_adjusted_energy,
|
||||
kwh=expected_adjusted_energy,
|
||||
)
|
||||
|
||||
bill_savings = current_energy_bill - expected_energy_bill
|
||||
|
|
@ -861,6 +1081,7 @@ async def build_mds(body: PlanTriggerRequest):
|
|||
to_append = {
|
||||
"config_address": config["address"],
|
||||
"config_postcode": config["postcode"],
|
||||
"uprn": p.uprn,
|
||||
"address": p.address,
|
||||
"postcode": p.postcode,
|
||||
"measures": measures,
|
||||
|
|
@ -874,14 +1095,19 @@ async def build_mds(body: PlanTriggerRequest):
|
|||
"heat_demand_after": heat_demand_after,
|
||||
"carbon_before": carbon_before,
|
||||
"carbon_after": carbon_after,
|
||||
"bill_savings": bill_savings,
|
||||
"energy_savings": energy_savings,
|
||||
"bill_savings": round(bill_savings, 2),
|
||||
"energy_savings": round(energy_savings, 2),
|
||||
"current_energy_bill": round(current_energy_bill, 2),
|
||||
"fuel_type": p.main_fuel["fuel_type"],
|
||||
}
|
||||
results.append(to_append)
|
||||
|
||||
results = pd.DataFrame(results)
|
||||
results["sap_uplift"] = results["sap_after"] - results["sap_before"]
|
||||
|
||||
# results.to_excel("mds_results 5th June.xlsx")
|
||||
|
||||
walls_check, hhr_check = check_mds(results, input_properties, recommendations, optimise_measures)
|
||||
|
||||
except IntegrityError:
|
||||
logger.error("Database integrity error occurred", exc_info=True)
|
||||
|
|
@ -901,3 +1127,80 @@ async def build_mds(body: PlanTriggerRequest):
|
|||
return Response(status_code=500, content="An unexpected error occurred.")
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
|
||||
def check_mds(results, input_properties, recommendations, optimise_measures):
|
||||
import ast
|
||||
walls_check = []
|
||||
hhr_check = []
|
||||
for p in input_properties:
|
||||
res = results[results["uprn"] == p.uprn]
|
||||
wall = p.walls
|
||||
heating = p.main_heating
|
||||
heating_controls = p.main_heating_controls
|
||||
|
||||
if optimise_measures:
|
||||
measures = res["measures"].values[0]
|
||||
else:
|
||||
measures = [list(z.keys())[0] for z in res["measures"].values[0]]
|
||||
|
||||
wall_recommendation = [
|
||||
x for x in measures if
|
||||
x in ["internal_wall_insulation", "external_wall_insulation", "cavity_wall_insulation"]
|
||||
]
|
||||
|
||||
hhr_recommendation = [
|
||||
x for x in measures if
|
||||
x in ["high_heat_retention_storage_heaters"]
|
||||
]
|
||||
|
||||
if optimise_measures:
|
||||
possible_measures = [ast.literal_eval(x) for x in list(recommendations[p.id].keys())]
|
||||
# Unlist them
|
||||
possible_measures = [x for sublist in possible_measures for x in sublist]
|
||||
possible_measures = list(set(possible_measures))
|
||||
else:
|
||||
possible_measures = p.measures
|
||||
|
||||
if wall_recommendation:
|
||||
if len(wall_recommendation) > 1:
|
||||
raise Exception("something went wrong")
|
||||
wall_recommendation = wall_recommendation[0]
|
||||
else:
|
||||
wall_recommendation = None
|
||||
|
||||
hhr_recommendation = hhr_recommendation[0] if hhr_recommendation else None
|
||||
|
||||
walls_check.append(
|
||||
{
|
||||
"uprn": p.uprn,
|
||||
"address": p.address,
|
||||
"postcode": p.postcode,
|
||||
"property_type": p.data['property-type'],
|
||||
"conservation_status": p.spatial["conservation_status"],
|
||||
"is_listed_building": p.spatial["is_listed_building"],
|
||||
"is_heritage_building": p.spatial["is_heritage_building"],
|
||||
"wall": wall["clean_description"],
|
||||
"recommendation": wall_recommendation,
|
||||
"possible_measures": possible_measures,
|
||||
"selected_measures": res["measures"].values[0],
|
||||
}
|
||||
)
|
||||
|
||||
hhr_check.append(
|
||||
{
|
||||
"uprn": p.uprn,
|
||||
"address": p.address,
|
||||
"postcode": p.postcode,
|
||||
"heating": heating["clean_description"],
|
||||
"heating_controls": heating_controls["clean_description"],
|
||||
"recommendation": hhr_recommendation,
|
||||
"possible_measures": possible_measures,
|
||||
"selected_measures": res["measures"].values[0],
|
||||
}
|
||||
)
|
||||
|
||||
walls_check = pd.DataFrame(walls_check)
|
||||
hhr_check = pd.DataFrame(hhr_check)
|
||||
|
||||
return walls_check, hhr_check
|
||||
|
|
|
|||
|
|
@ -52,3 +52,9 @@ class PlanTriggerRequest(BaseModel):
|
|||
if v not in cls._allowed_housing_types:
|
||||
raise ValueError(f"{v} is not a valid housing type")
|
||||
return v
|
||||
|
||||
|
||||
class MdsRequest(PlanTriggerRequest):
|
||||
# When creating the mds report, we allow an optional list of measures to select from. If this is passed, it will
|
||||
# cause the service to select the optimal package from the list of measures
|
||||
measures: Optional[conlist(str, min_items=1)] = None
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -14,6 +17,8 @@ class AnnualBillSavings:
|
|||
# https://www.ofgem.gov.uk/publications/new-energy-price-cap-level-april-june-2024-starts-today
|
||||
ELECTRICITY_PRICE_CAP = 0.245
|
||||
GAS_PRICE_CAP = 0.0604
|
||||
# This is the most recent export payment figure, at 12p per kwh
|
||||
ELECTRICITY_EXPORT_PAYMENT = 0.12
|
||||
|
||||
# This is a weighted mean of the price caps, using the consumption figures above as weights
|
||||
PRICE_FACTOR = 0.09549999999999999
|
||||
|
|
@ -58,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
|
||||
|
|
@ -70,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,
|
||||
|
|
|
|||
|
|
@ -90,6 +90,9 @@ class PropertyValuation:
|
|||
41222760: 46_000, # Based on Zoopla
|
||||
41222761: 270_000, # Based on Zoopla
|
||||
41212534: 38_000, # Based on Zoopla
|
||||
# Northern Group Pilot - search by going to https://www.zoopla.co.uk/property/uprn/{uprn}/
|
||||
10070868263: 194_000, # Based on Zoopla
|
||||
10070868244: 195_000, # Based on Zoopla
|
||||
}
|
||||
|
||||
# We base our valuation uplifts on a number of sources
|
||||
|
|
|
|||
27
etl/customers/eon/deck_examples.py
Normal file
27
etl/customers/eon/deck_examples.py
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
"""
|
||||
This script contains bits of codes for examples to be included in the Deck
|
||||
"""
|
||||
|
||||
from backend.SearchEpc import SearchEpc
|
||||
from dotenv import load_dotenv
|
||||
import os
|
||||
|
||||
load_dotenv(dotenv_path="backend/.env")
|
||||
|
||||
EPC_AUTH_TOKEN = os.getenv("EPC_AUTH_TOKEN")
|
||||
|
||||
searcher = SearchEpc(
|
||||
address1="108 Blacklands",
|
||||
postcode="ME19 6DP",
|
||||
auth_token=EPC_AUTH_TOKEN,
|
||||
os_api_key="",
|
||||
property_type=None,
|
||||
fast=False,
|
||||
)
|
||||
|
||||
res = searcher.estimate_epc(
|
||||
property_type="Bungalow",
|
||||
built_form="Detached",
|
||||
lmks_to_drop=["849273656952012102323315196229804"],
|
||||
exclude_old=True
|
||||
)
|
||||
|
|
@ -64,7 +64,7 @@ def extract_mds_measures(config):
|
|||
measures.append({"district_heating_networks": "District heating networks"})
|
||||
|
||||
if not pd.isnull(config["Elec Storage Htrs (Out of scope -Prov sum only)"]):
|
||||
measures.append({"electric_storage_heaters": "Elec Storage Htrs (Out of scope -Prov sum only)"})
|
||||
measures.append({"high_heat_retention_storage_heaters": "Elec Storage Htrs (Out of scope -Prov sum only)"})
|
||||
|
||||
if not pd.isnull(config["Low Energy Bulbs"]):
|
||||
measures.append({"low_energy_lighting": "Low Energy Bulbs"})
|
||||
|
|
@ -229,7 +229,8 @@ def app():
|
|||
"35a High Street",
|
||||
"35b High Street",
|
||||
"Flat Over 20 Holborough Road",
|
||||
"Flat above 7 Malling Road"
|
||||
"Flat above 7 Malling Road",
|
||||
"Cobnut Barn",
|
||||
]:
|
||||
print(config["Address"])
|
||||
uprn = None
|
||||
|
|
@ -269,3 +270,33 @@ def app():
|
|||
"budget": None,
|
||||
}
|
||||
print(body)
|
||||
|
||||
# Optimised version where we specify the measures
|
||||
measures = [
|
||||
"external_wall_insulation",
|
||||
"cavity_wall_insulation",
|
||||
"loft_insulation",
|
||||
"air_source_heat_pump",
|
||||
"high_heat_retention_storage_heaters",
|
||||
"solar_pv"
|
||||
]
|
||||
|
||||
body = {
|
||||
"portfolio_id": str(PORTFOLIO_ID),
|
||||
"housing_type": "Social",
|
||||
"goal": "Increase EPC",
|
||||
"goal_value": "C",
|
||||
"trigger_file_path": filename,
|
||||
"already_installed_file_path": "",
|
||||
"patches_file_path": "",
|
||||
"non_invasive_recommendations_file_path": "",
|
||||
"measures": measures,
|
||||
"budget": None,
|
||||
}
|
||||
|
||||
|
||||
output = []
|
||||
for r in self.results:
|
||||
output.append(r["DPA"])
|
||||
|
||||
output = pd.DataFrame(output)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import pandas as pd
|
|||
from tqdm import tqdm
|
||||
import Levenshtein
|
||||
from backend.SearchEpc import SearchEpc
|
||||
from utils.s3 import read_dataframe_from_s3_parquet
|
||||
|
||||
# Average value of a property in the midlands in 2024 was £238,000. Since these are EPC F & G properties, we assume
|
||||
# £207,000 since they trade at a discount. This is based on the rightmove study where moving from an EPC F/G -> C has a
|
||||
|
|
@ -248,6 +249,13 @@ def app():
|
|||
"""
|
||||
This script is for scoping property ownership for EPC F & G rated properties in Birmingam, for Goldman Sachs
|
||||
"""
|
||||
|
||||
# TODO: This property:
|
||||
# https://epc.opendatacommunities.org/domestic/search?address=&postcode=&local-authority=&constituency
|
||||
# =&uprn=100031179243&from-month=1&from-year=2008&to-month=12&to-year=2024
|
||||
# is actually listed in two local authorities causing us to think it's an EPC F & G property, but it's
|
||||
# it's actually EPC E. Need to handle this, probably by reading in all of the EPC data, concatenating together
|
||||
# and performing a singular filter for most recent EPC by UPRN
|
||||
# paths = [
|
||||
# "local_data/all-domestic-certificates/domestic-E08000025-Birmingham/certificates.csv",
|
||||
# "local_data/all-domestic-certificates/domestic-E08000031-Wolverhampton/certificates.csv",
|
||||
|
|
@ -477,6 +485,35 @@ def app():
|
|||
portfolio_epc_data_50m.to_excel("portfolio_epc_data_50m 28th May.xlsx", index=False)
|
||||
portfolio_epc_data_20m.to_excel("portfolio_epc_data_20m 28th May.xlsx", index=False)
|
||||
|
||||
# We check if any of these properties are in a conservation area
|
||||
valuations = pd.read_excel("property value.xlsx")
|
||||
|
||||
uprn_filenames = read_dataframe_from_s3_parquet(
|
||||
bucket_name="retrofit-data-dev", file_key="spatial/filename_meta.parquet"
|
||||
)
|
||||
|
||||
geospatial_data = []
|
||||
for _, row in tqdm(valuations.iterrows(), total=len(valuations)):
|
||||
filtered_df = uprn_filenames[
|
||||
(uprn_filenames["lower"] <= row["UPRN"])
|
||||
& (uprn_filenames["upper"] >= row["UPRN"])
|
||||
]
|
||||
if filtered_df.empty:
|
||||
raise Exception("No match found")
|
||||
|
||||
filename = filtered_df.iloc[0]["filenames"]
|
||||
|
||||
spatial_data = read_dataframe_from_s3_parquet(
|
||||
bucket_name="retrofit-data-dev", file_key=f"spatial/{filename}"
|
||||
)
|
||||
spatial = spatial_data[
|
||||
spatial_data["UPRN"] == row["UPRN"]
|
||||
][["UPRN", "conservation_status", "is_listed_building", "is_heritage_building"]]
|
||||
geospatial_data.append(spatial.to_dict("records")[0])
|
||||
|
||||
geospatial_data = pd.DataFrame(geospatial_data)
|
||||
geospatial_data.to_excel("geospatial_data.xlsx", index=False)
|
||||
|
||||
|
||||
def company_aggregation():
|
||||
company_ownership = pd.read_csv("/Users/khalimconn-kowlessar/Downloads/CCOD_FULL_2024_04.csv")
|
||||
|
|
@ -490,3 +527,79 @@ def company_aggregation():
|
|||
aggregation = aggregation.sort_values("Number of Properties", ascending=False)
|
||||
|
||||
aggregation.to_excel("Company ownership aggregation.xlsx")
|
||||
|
||||
|
||||
def prepare_anonymised_data():
|
||||
investment_50m_properties = pd.read_excel("investment_50m_properties 28th May.xlsx", header=0)
|
||||
investment_epc_data = pd.read_excel("portfolio_epc_data_50m 28th May.xlsx", header=0)
|
||||
valuations = pd.read_excel("property value.xlsx", header=0)
|
||||
|
||||
# Merge these datasets
|
||||
df = investment_50m_properties.merge(
|
||||
investment_epc_data[
|
||||
["UPRN", "PROPERTY_TYPE", "BUILT_FORM", "TOTAL_FLOOR_AREA", "LODGEMENT_DATE", "POSTCODE"]
|
||||
].rename(
|
||||
columns={
|
||||
"PROPERTY_TYPE": "Property Type",
|
||||
"BUILT_FORM": "Property Archetype",
|
||||
"TOTAL_FLOOR_AREA": "Total Floor Area",
|
||||
"LODGEMENT_DATE": "Date EPC Lodged",
|
||||
"POSTCODE": "Postcode on EPC"
|
||||
}
|
||||
),
|
||||
how="inner",
|
||||
on="UPRN"
|
||||
).merge(
|
||||
valuations.drop(columns=["ADDRESS", "POSTCODE"]).rename(
|
||||
columns={
|
||||
"Zoopla Valuation": "Expected Valuation",
|
||||
"Zoopla Lower Bound": "Valuation - Lower Bound",
|
||||
"Zoopla Upper Bound": "Valuation - Upper Bound",
|
||||
}
|
||||
),
|
||||
how="inner",
|
||||
on="UPRN"
|
||||
).rename(
|
||||
columns={
|
||||
"CURRENT_ENERGY_RATING": "Current EPC",
|
||||
"CURRENT_ENERGY_EFFICIENCY": "Current SAP Score",
|
||||
"epc_address": "Address on EPC"
|
||||
}
|
||||
).drop(
|
||||
columns=["Title Number", "match_type", "UPRN"]
|
||||
)
|
||||
|
||||
redacted_owner_names = df[["Company Registration No. (1)"]].drop_duplicates()
|
||||
redacted_owner_names["Owner"] = ["Owner" + str(i) for i in range(1, len(redacted_owner_names) + 1)]
|
||||
|
||||
df = df.merge(
|
||||
redacted_owner_names, how="left", on="Company Registration No. (1)"
|
||||
)
|
||||
|
||||
df = df.drop(columns=["Company Registration No. (1)", "Proprietor Name (1)", "Property Address"])
|
||||
df = df.sort_values(["Owner", "Date EPC Lodged"], ascending=False)
|
||||
|
||||
redacted_index = []
|
||||
for _, owner_properties in df.groupby("Owner"):
|
||||
top_50_percent = round(owner_properties.shape[0] / 2 + 0.00001)
|
||||
indexes = owner_properties.tail(
|
||||
owner_properties.shape[0] - top_50_percent
|
||||
).index
|
||||
|
||||
redacted_index.extend(indexes.tolist())
|
||||
|
||||
import numpy as np
|
||||
# Redact addresses and postcodes
|
||||
df["Address on EPC"] = np.where(
|
||||
df.index.isin(redacted_index),
|
||||
"Redacted",
|
||||
df["Address on EPC"]
|
||||
)
|
||||
|
||||
df["Postcode on EPC"] = np.where(
|
||||
df.index.isin(redacted_index),
|
||||
"Redacted",
|
||||
df["Postcode on EPC"]
|
||||
)
|
||||
|
||||
df.to_excel("Property List - 50% redacted.xlsx", index=False)
|
||||
|
|
|
|||
148
etl/customers/lhp/30_may_2024_data_pull.py
Normal file
148
etl/customers/lhp/30_may_2024_data_pull.py
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
import os
|
||||
|
||||
import pandas as pd
|
||||
from tqdm import tqdm
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from utils.s3 import read_excel_from_s3
|
||||
from backend.SearchEpc import SearchEpc
|
||||
from etl.epc_clean.epc_attributes.RoofAttributes import RoofAttributes
|
||||
|
||||
from recommendations.recommendation_utils import (
|
||||
estimate_perimeter,
|
||||
estimate_external_wall_area,
|
||||
estimate_number_of_floors
|
||||
)
|
||||
|
||||
load_dotenv(dotenv_path="backend/.env")
|
||||
EPC_AUTH_TOKEN = os.getenv("EPC_AUTH_TOKEN")
|
||||
|
||||
|
||||
def app():
|
||||
"""
|
||||
This app is EPC pulling data for some properties owned by LHP
|
||||
:return:
|
||||
"""
|
||||
# asset_list = read_excel_from_s3(
|
||||
# bucket_name="retrofit-datalake-dev",
|
||||
# file_key="customers/guiness/TGP CW Properties PV.xlsx",
|
||||
# header_row=0
|
||||
# )
|
||||
asset_list = pd.read_excel("/Users/khalimconn-kowlessar/Downloads/Echo4 3.4.24.xlsx", header=0)
|
||||
|
||||
epc_data = []
|
||||
for _, home in tqdm(asset_list.iterrows(), total=len(asset_list)):
|
||||
|
||||
full_address = home["ADDRESS"]
|
||||
address_split = full_address.split(",")
|
||||
address1 = address_split[0].strip()
|
||||
postcode = address_split[-1].strip()
|
||||
|
||||
searcher = SearchEpc(
|
||||
address1=address1,
|
||||
postcode=postcode,
|
||||
auth_token=EPC_AUTH_TOKEN,
|
||||
os_api_key="",
|
||||
property_type=None,
|
||||
fast=True,
|
||||
full_address=full_address
|
||||
)
|
||||
# Force the skipping of estimating the EPC
|
||||
searcher.ordnance_survey_client.property_type = None
|
||||
searcher.ordnance_survey_client.built_form = None
|
||||
|
||||
searcher.find_property(skip_os=True)
|
||||
if searcher.newest_epc is None:
|
||||
continue
|
||||
|
||||
epc = {
|
||||
"asset_list_address": full_address,
|
||||
**searcher.newest_epc.copy()
|
||||
}
|
||||
|
||||
epc_data.append(epc)
|
||||
|
||||
epc_df = pd.DataFrame(epc_data)
|
||||
|
||||
# Retrieve just the data we need
|
||||
epc_df = epc_df[
|
||||
[
|
||||
"asset_list_address",
|
||||
"uprn",
|
||||
"property-type",
|
||||
"built-form",
|
||||
"inspection-date",
|
||||
"current-energy-rating",
|
||||
"current-energy-efficiency",
|
||||
"roof-description",
|
||||
"walls-description",
|
||||
"transaction-type",
|
||||
# New fields needed
|
||||
"secondheat-description",
|
||||
"total-floor-area",
|
||||
"construction-age-band",
|
||||
"floor-height",
|
||||
"number-habitable-rooms",
|
||||
"mainheat-description"
|
||||
]
|
||||
]
|
||||
|
||||
asset_list = asset_list.merge(
|
||||
epc_df,
|
||||
how="left",
|
||||
left_on=["ADDRESS"],
|
||||
right_on=["asset_list_address"]
|
||||
)
|
||||
|
||||
asset_list = asset_list.drop(columns=["asset_list_address"])
|
||||
|
||||
# Rename the columns
|
||||
asset_list = asset_list.rename(columns={
|
||||
"inspection-date": "Date of last EPC",
|
||||
"current-energy-efficiency": "SAP score on register",
|
||||
"current-energy-rating": "EPC rating on register",
|
||||
"property-type": "Property Type",
|
||||
"built-form": "Archetype",
|
||||
"total-floor-area": "Property Floor Area",
|
||||
"construction-age-band": "Property Age Band",
|
||||
"floor-height": "Property Floor Height",
|
||||
"number-habitable-rooms": "Number of Habitable Rooms",
|
||||
"walls-description": "Wall Construction",
|
||||
"roof-description": "Roof Construction",
|
||||
"mainheat-description": "Heating Type",
|
||||
"secondheat-description": "Secondary Heating",
|
||||
"transaction-type": "Reason for last EPC"
|
||||
})
|
||||
|
||||
asset_list["Estimated Number of Floors"] = asset_list.apply(
|
||||
lambda x: estimate_number_of_floors(property_type=x["Property Type"]), axis=1
|
||||
)
|
||||
|
||||
asset_list["Property Floor Area"] = asset_list["Property Floor Area"].astype(float)
|
||||
asset_list["Number of Habitable Rooms"] = asset_list["Number of Habitable Rooms"].astype(float)
|
||||
|
||||
asset_list["Estimated Perimeter (m)"] = asset_list.apply(
|
||||
lambda x: estimate_perimeter(
|
||||
floor_area=x["Property Floor Area"] / x["Estimated Number of Floors"],
|
||||
num_rooms=x["Number of Habitable Rooms"] / x["Estimated Number of Floors"],
|
||||
), axis=1
|
||||
)
|
||||
|
||||
asset_list["Estimated Heat Loss Perimeter (m)"] = asset_list.apply(
|
||||
lambda x: estimate_external_wall_area(
|
||||
num_floors=x["Estimated Number of Floors"],
|
||||
floor_height=float(x["Property Floor Height"]) if x["Property Floor Height"] else 2.5,
|
||||
perimeter=x["Estimated Perimeter (m)"],
|
||||
built_form=x["Archetype"]
|
||||
),
|
||||
axis=1
|
||||
)
|
||||
|
||||
asset_list["Roof Insulation Thickness"] = asset_list.apply(
|
||||
lambda x: RoofAttributes(description=x["Roof Construction"]).process()["insulation_thickness"],
|
||||
axis=1
|
||||
)
|
||||
|
||||
# Store as an excel
|
||||
filename = "LHP EPC Data pull.xlsx"
|
||||
asset_list.to_excel(filename, index=False)
|
||||
43
etl/customers/northern_gorup/test_asset_list.py
Normal file
43
etl/customers/northern_gorup/test_asset_list.py
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import pandas as pd
|
||||
from utils.s3 import save_csv_to_s3
|
||||
|
||||
USER_ID = 8
|
||||
PORTFOLIO_ID = 81
|
||||
|
||||
|
||||
def app():
|
||||
asset_list = [
|
||||
{
|
||||
'uprn': 10070868263,
|
||||
"address": "Apartment 307, Flint Glass Wharf",
|
||||
"postcode": "M4 6AD",
|
||||
},
|
||||
{
|
||||
'uprn': 10070868244,
|
||||
"address": "Apartment 106, Flint Glass Wharf",
|
||||
"postcode": "M4 6AD",
|
||||
}
|
||||
]
|
||||
|
||||
asset_list = pd.DataFrame(asset_list)
|
||||
|
||||
# Store the asset list in s3
|
||||
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": "Increase EPC",
|
||||
"goal_value": "B",
|
||||
"trigger_file_path": filename,
|
||||
"already_installed_file_path": "",
|
||||
"patches_file_path": "",
|
||||
"non_invasive_recommendations_file_path": "",
|
||||
"budget": None,
|
||||
}
|
||||
print(body)
|
||||
156
etl/customers/places_for_people/EPC data pull - 12th June.py
Normal file
156
etl/customers/places_for_people/EPC data pull - 12th June.py
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
import os
|
||||
|
||||
import pandas as pd
|
||||
from tqdm import tqdm
|
||||
import numpy as np
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from backend.SearchEpc import SearchEpc
|
||||
from etl.epc_clean.epc_attributes.RoofAttributes import RoofAttributes
|
||||
|
||||
from recommendations.recommendation_utils import (
|
||||
estimate_perimeter,
|
||||
estimate_external_wall_area,
|
||||
estimate_number_of_floors
|
||||
)
|
||||
|
||||
load_dotenv(dotenv_path="backend/.env")
|
||||
EPC_AUTH_TOKEN = os.getenv("EPC_AUTH_TOKEN")
|
||||
|
||||
|
||||
def app():
|
||||
"""
|
||||
This app is EPC pulling data for some properties owned by LHP
|
||||
:return:
|
||||
"""
|
||||
|
||||
asset_list = pd.read_excel(
|
||||
"/Users/khalimconn-kowlessar/Downloads/Places for People NORTH WEST - EPC DATA PULL REQUEST.xlsx", header=0
|
||||
)
|
||||
|
||||
epc_data = []
|
||||
for _, home in tqdm(asset_list.iterrows(), total=len(asset_list)):
|
||||
|
||||
full_address = home["Address"]
|
||||
|
||||
address1 = home["AddressLine1"]
|
||||
postcode = home["Postcode"]
|
||||
|
||||
searcher = SearchEpc(
|
||||
address1=address1,
|
||||
postcode=postcode,
|
||||
auth_token=EPC_AUTH_TOKEN,
|
||||
os_api_key="",
|
||||
property_type=None,
|
||||
fast=True,
|
||||
full_address=full_address
|
||||
)
|
||||
# Force the skipping of estimating the EPC
|
||||
searcher.ordnance_survey_client.property_type = None
|
||||
searcher.ordnance_survey_client.built_form = None
|
||||
|
||||
searcher.find_property(skip_os=True)
|
||||
if searcher.newest_epc is None:
|
||||
continue
|
||||
|
||||
epc = {
|
||||
"asset_list_address": full_address,
|
||||
**searcher.newest_epc.copy()
|
||||
}
|
||||
|
||||
epc_data.append(epc)
|
||||
|
||||
epc_df = pd.DataFrame(epc_data)
|
||||
|
||||
# Retrieve just the data we need
|
||||
epc_df = epc_df[
|
||||
[
|
||||
"asset_list_address",
|
||||
"uprn",
|
||||
"property-type",
|
||||
"built-form",
|
||||
"inspection-date",
|
||||
"current-energy-rating",
|
||||
"current-energy-efficiency",
|
||||
"roof-description",
|
||||
"walls-description",
|
||||
"transaction-type",
|
||||
# New fields needed
|
||||
"secondheat-description",
|
||||
"total-floor-area",
|
||||
"construction-age-band",
|
||||
"floor-height",
|
||||
"number-habitable-rooms",
|
||||
"mainheat-description"
|
||||
]
|
||||
]
|
||||
|
||||
# epc_df.to_csv("pfp sales data.csv", index=False)
|
||||
|
||||
asset_list = asset_list.merge(
|
||||
epc_df,
|
||||
how="left",
|
||||
left_on=["Address"],
|
||||
right_on=["asset_list_address"]
|
||||
)
|
||||
|
||||
asset_list = asset_list.drop(columns=["asset_list_address"])
|
||||
|
||||
# Rename the columns
|
||||
asset_list = asset_list.rename(columns={
|
||||
"inspection-date": "Date of last EPC",
|
||||
"current-energy-efficiency": "SAP score on register",
|
||||
"current-energy-rating": "EPC rating on register",
|
||||
"property-type": "EPC Property Type",
|
||||
"built-form": "EPC Archetype",
|
||||
"total-floor-area": "EPC Property Floor Area",
|
||||
"construction-age-band": "EPC Property Age Band",
|
||||
"floor-height": "EPC Property Floor Height",
|
||||
"number-habitable-rooms": "EPC Number of Habitable Rooms",
|
||||
"walls-description": "EPC Wall Construction",
|
||||
"roof-description": "EPC Roof Construction",
|
||||
"mainheat-description": "EPC Heating Type",
|
||||
"secondheat-description": "EPC Secondary Heating",
|
||||
"transaction-type": "Reason for last EPC"
|
||||
})
|
||||
|
||||
asset_list["Estimated Number of Floors"] = asset_list.apply(
|
||||
lambda x: estimate_number_of_floors(
|
||||
property_type=x["EPC Property Type"]
|
||||
) if not pd.isnull(x["EPC Property Type"]) else None, axis=1
|
||||
)
|
||||
|
||||
asset_list["EPC Property Floor Area"] = asset_list["EPC Property Floor Area"].astype(float)
|
||||
asset_list["EPC Number of Habitable Rooms"] = np.where(
|
||||
asset_list["EPC Number of Habitable Rooms"] == "",
|
||||
None,
|
||||
asset_list["EPC Number of Habitable Rooms"]
|
||||
)
|
||||
asset_list["EPC Number of Habitable Rooms"] = asset_list["EPC Number of Habitable Rooms"].astype(float)
|
||||
|
||||
asset_list["Estimated Perimeter (m)"] = asset_list.apply(
|
||||
lambda x: estimate_perimeter(
|
||||
floor_area=x["EPC Property Floor Area"] / x["Estimated Number of Floors"],
|
||||
num_rooms=x["EPC Number of Habitable Rooms"] / x["Estimated Number of Floors"],
|
||||
), axis=1
|
||||
)
|
||||
|
||||
asset_list["Estimated Heat Loss Perimeter (m)"] = asset_list.apply(
|
||||
lambda x: estimate_external_wall_area(
|
||||
num_floors=x["Estimated Number of Floors"],
|
||||
floor_height=float(x["EPC Property Floor Height"]) if x["EPC Property Floor Height"] else 2.5,
|
||||
perimeter=x["Estimated Perimeter (m)"],
|
||||
built_form=x["EPC Archetype"]
|
||||
),
|
||||
axis=1
|
||||
)
|
||||
|
||||
asset_list["Roof Insulation Thickness"] = asset_list.apply(
|
||||
lambda x: RoofAttributes(description=x["EPC Roof Construction"]).process()[
|
||||
"insulation_thickness"] if not pd.isnull(x["EPC Roof Construction"]) else None,
|
||||
axis=1
|
||||
)
|
||||
|
||||
# Store as an excel
|
||||
filename = "Places for People NORTH WEST - EPC DATA PULL.xlsx"
|
||||
asset_list.to_excel(filename, index=False)
|
||||
164
etl/customers/places_for_people/parity_comparison.py
Normal file
164
etl/customers/places_for_people/parity_comparison.py
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
"""
|
||||
This script is used to pull together some case studies for the Parity Projects comparison
|
||||
"""
|
||||
|
||||
import pandas as pd
|
||||
from backend.SearchEpc import SearchEpc
|
||||
from dotenv import load_dotenv
|
||||
import os
|
||||
|
||||
load_dotenv("backend/.env")
|
||||
EPC_AUTH_TOKEN = os.getenv("EPC_AUTH_TOKEN")
|
||||
|
||||
parity_measures = pd.read_excel(
|
||||
"/Users/khalimconn-kowlessar/Documents/hestia/Places For People/Parity Sample All Addresses and Measures.xlsx",
|
||||
sheet_name="Total Measures"
|
||||
)
|
||||
|
||||
solar_measures = parity_measures[parity_measures["Category"] == "SolarPV"]
|
||||
|
||||
example_1 = parity_measures[
|
||||
parity_measures["Address Id (used by website)"] == 6125299
|
||||
].copy()
|
||||
|
||||
config = {
|
||||
"address": "14 Victoria Road",
|
||||
"postcode": "BD20 8SY",
|
||||
"uprn": 100050346517
|
||||
}
|
||||
|
||||
# Point 1:
|
||||
# Parity tends to re-score the EPCs, even if they're extrememly recent.
|
||||
# For example for '14, Victoria Road, Cross Hills, KEIGHLEY, North Yorkshire, ENGLAND, BD20 8SY'
|
||||
# The most recent EPC was done 15 May 2023, and landed at a 66D, however for some reason, parity re-score this
|
||||
# home to be a 63.91. It's unclear why this is done
|
||||
|
||||
example_1_measures = example_1[["MeasureGroupName", "Individual SAP increase"]].copy()
|
||||
# - LEDS: 0.25 SAP points
|
||||
# - 300mm of loft insulation from 200mm: 0.43 SAP points - where is this deduced from? Since the latest survey
|
||||
# indicates 250mm insulation in place
|
||||
# - Check construction of unknown party wall and fill cavity if appropriate: 0.12 SAP points (highly speculative,
|
||||
# not based on any data)
|
||||
# - Block open chimneys: 1.61 SAP points - latest survey showed 0 open fireplaces
|
||||
# - ASHP (45 degree emitters) with enhanced existing radiator central heating and hot water, from E rated gas boiler
|
||||
# 6.38 SAP points
|
||||
# - 4kWp PV array south and 30 degree pitch with no shading: 30.24 SAP points
|
||||
|
||||
# Notes on solar - 30.34 seems like a lot
|
||||
# 400 watt is the solar panel output
|
||||
# Let's do a test for this property
|
||||
# This would be 10 solar panels
|
||||
# Using typical solar panel dimensions, this would be 19.63555m2 of roof space
|
||||
# The area of the roof is between 60 - 64.5 m2 (we use a API to get the roof data), implying only
|
||||
# around 30% of the roof is covered by solar panels
|
||||
# Using our machine learning model to simulate the impact of this on SAP, this would more likely result in
|
||||
# a
|
||||
|
||||
from utils.s3 import read_dataframe_from_s3_parquet
|
||||
|
||||
training_data = read_dataframe_from_s3_parquet(
|
||||
bucket_name="retrofit-data-dev",
|
||||
file_key="sap_change_model/2024-06-09-10-36-53/dataset_rooms.parquet"
|
||||
)
|
||||
# Look for properties where the only difference is solar
|
||||
ending_cols = [
|
||||
c for c in training_data.columns if "_ending" in c and "photo_supply" not in c
|
||||
]
|
||||
ending_cols = [
|
||||
c for c in ending_cols if
|
||||
c not in ["sap_ending", "heat_demand_ending", "carbon_ending", "transaction_type_ending", "days_to_ending"]
|
||||
]
|
||||
|
||||
column_pairs = {}
|
||||
for col in ending_cols:
|
||||
starting = col.split("_ending")[0]
|
||||
if starting + "_starting" in training_data.columns:
|
||||
starting_col = starting + "_starting"
|
||||
else:
|
||||
starting_col = starting
|
||||
|
||||
column_pairs[col] = starting_col
|
||||
|
||||
filtered = training_data.copy()
|
||||
# Take rows that had solar installs
|
||||
filtered = filtered[filtered["photo_supply_ending"] != filtered["photo_supply_starting"]]
|
||||
for ending_col, starting_col in column_pairs.items():
|
||||
filtered = filtered[filtered[ending_col] == filtered[starting_col]]
|
||||
print(f"ending_col: {ending_col}, filtered shape: {filtered.shape}")
|
||||
|
||||
avg_change = filtered.groupby("photo_supply_ending")["rdsap_change"].mean().reset_index()
|
||||
|
||||
# I've take every single case of there being two EPCs for a property, where the only difference between the first
|
||||
# and second is the solar installation. This is 2692 properties, across the UK. In only 4 instances has this resulted in
|
||||
# 30 or more SAP points
|
||||
|
||||
|
||||
# Some functions based on the SAP methodology:
|
||||
import numpy as np
|
||||
|
||||
total_floor_area = 50
|
||||
occupants = calculate_occupants(total_floor_area)
|
||||
appliances_energy_use = estimate_electrical_appliances(occupants, total_floor_area)
|
||||
cooking_energy_use = estimate_cooking(occupants)
|
||||
|
||||
|
||||
def calculate_occupants(total_floor_area):
|
||||
"""
|
||||
From Table 1b
|
||||
:param total_floor_area:
|
||||
:return:
|
||||
"""
|
||||
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))
|
||||
|
||||
|
||||
def estimate_electrical_appliances(occupants, total_floor_area):
|
||||
"""
|
||||
From seciont L2 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
|
||||
|
||||
|
||||
def estimate_cooking(occupants):
|
||||
"""
|
||||
From section L3 Cooking
|
||||
:param occupants:
|
||||
:return:
|
||||
"""
|
||||
|
||||
return 35 + 7 * occupants
|
||||
|
||||
|
||||
primary_energy_per_m2 = 288 # kWh/m2 per year
|
||||
primary_energy_regulated = primary_energy_per_m2 * total_floor_area
|
||||
|
||||
primary_energy_factor_electricity = 1.1 # Example factor
|
||||
primary_energy_appliances = appliances_energy_use * primary_energy_factor_electricity
|
||||
primary_energy_cooking = cooking_energy_use * primary_energy_factor_electricity * 365 # Annualize cooking energy
|
||||
|
||||
total_primary_energy_use = primary_energy_regulated + primary_energy_appliances
|
||||
|
|
@ -295,6 +295,49 @@ def main():
|
|||
|
||||
addresses_df2.to_excel("Places For People EPC data with surveyor.xlsx", index=False)
|
||||
|
||||
# Read in
|
||||
df = pd.read_excel("Places For People EPC data with surveyor.xlsx")
|
||||
df = df[
|
||||
df["assessor_name"].isin(
|
||||
[
|
||||
"Arsalan Khalid", "Kieran Bradnock", "Wayne Davies", "Lindsay Sands", "Bruce Nethercot",
|
||||
"Christopher Hearn", "Robert Sigerson", "Daniel Riddle", "Leroy Sands",
|
||||
]
|
||||
)
|
||||
]
|
||||
|
||||
# Get the EPC
|
||||
heights = []
|
||||
for _, row in tqdm(df.iterrows(), total=len(df)):
|
||||
searcher = SearchEpc(
|
||||
address1=str(row["Matched EPC Address"]),
|
||||
postcode=str(row["POSTCODE"]),
|
||||
uprn=str(int(row["uprn"])),
|
||||
auth_token=EPC_AUTH_TOKEN,
|
||||
os_api_key="",
|
||||
property_type=None,
|
||||
fast=True,
|
||||
)
|
||||
# Force the skipping of estimating the EPC
|
||||
searcher.ordnance_survey_client.property_type = None
|
||||
searcher.ordnance_survey_client.built_form = None
|
||||
|
||||
searcher.find_property(skip_os=True)
|
||||
|
||||
height = {
|
||||
"uprn": row["uprn"],
|
||||
"floor_height": searcher.newest_epc["floor-height"]
|
||||
}
|
||||
heights.append(height)
|
||||
|
||||
df = df.merge(
|
||||
pd.DataFrame(heights),
|
||||
how="left",
|
||||
on="uprn"
|
||||
)
|
||||
|
||||
df.to_excel("WF surveyors with floor heights.xlsx", index=False)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
|
|||
165
etl/customers/stonewater/no_matches.py
Normal file
165
etl/customers/stonewater/no_matches.py
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
no_matches = [
|
||||
{
|
||||
'internal_id': 4626, 'full_address': '1 Dean Lane, Sixpenny Handley, Salisbury, SP5 5AS', 'postcode': 'SP5 5AS',
|
||||
'Note': 'No match found - all addresses in this postcode are for Mulberry Court, Sixpenny Handley, Salisbury, '
|
||||
'SP5 5AS, addresses not recognised by Zoopla - possibly the postcode is incorrect and this could be'
|
||||
'Handley Enterprises Ltd, Unit 1 Dean Lane, Sixpenny Handley, Salisbury, SP5 5PA.'
|
||||
'Or this could be 1 Mulberry Court Sixpenny Handley, Salisbury SP5 5AS'
|
||||
},
|
||||
{
|
||||
'internal_id': 4627, 'full_address': '3 Dean Lane, Sixpenny Handley, Salisbury, SP5 5AS', 'postcode': 'SP5 5AS',
|
||||
'Note': 'No match found - all addresses in this postcode are for Mulberry Court, Sixpenny Handley, Salisbury, '
|
||||
'SP5 5AS, addresses not recognised by Zoopla - possibly the postcode is incorrect and this could be'
|
||||
'2 Town Farm House, Dean Lane, Sixpenny Handley, Salisbury, SP5 5PA'
|
||||
'Or this could be 3 Mulberry Court Sixpenny Handley, Salisbury SP5 5AS'
|
||||
},
|
||||
{
|
||||
'internal_id': 4628, 'full_address': '5 Dean Lane, Sixpenny Handley, Salisbury, SP5 5AS', 'postcode': 'SP5 5AS',
|
||||
'Note': 'No match found - all addresses in this postcode are for Mulberry Court, Sixpenny Handley, Salisbury, '
|
||||
'SP5 5AS, addresses not recognised by Zoopla - possibly the postcode is incorrect and this could be'
|
||||
'4 Town Farm House, Dean Lane, Sixpenny Handley, Salisbury, SP5 5PA'
|
||||
'Or this could be 5 Mulberry Court Sixpenny Handley, Salisbury SP5 5AS'
|
||||
},
|
||||
{
|
||||
'internal_id': 544, 'full_address': 'Room 1, Sawr, PO Box 1354, Bedford, MK41 5AB', 'postcode': 'MK41 5AB',
|
||||
"Note": "Postcode deleted in April 2024: https://checkmypostcode.uk/mk415ab"
|
||||
},
|
||||
{
|
||||
'internal_id': 5116, 'full_address': '3 Huntspond Road, Titchfield, Fareham, PO14 4SS', 'postcode': 'PO14 4SS',
|
||||
'Note': 'Is this 3 St Francis Court, 195 Hunts Pond Road, Fareham, PO14 4SS, uprn: 100061988896'
|
||||
},
|
||||
{
|
||||
'internal_id': 5114, 'full_address': '4 Huntspond Road, Titchfield, Fareham, PO14 4SS', 'postcode': 'PO14 4SS',
|
||||
'Note': 'Is this 4 St Francis Court, 195 Hunts Pond Road, Fareham, PO14 4SS, uprn: 100061988897'
|
||||
},
|
||||
{
|
||||
'internal_id': 5115, 'full_address': '2 Huntspond Road, Titchfield, Fareham, PO14 4SS', 'postcode': 'PO14 4SS',
|
||||
'Note': 'Is this 2 St Francis Court, 195 Hunts Pond Road, Fareham, PO14 4SS, uprn: 100061988895'
|
||||
},
|
||||
{
|
||||
'internal_id': 5113, 'full_address': '6 Huntspond Road, Titchfield, Fareham, PO14 4SS', 'postcode': 'PO14 4SS',
|
||||
'Note': 'Is this 6 St Francis Court, 195 Hunts Pond Road, Fareham, PO14 4SS, uprn: 100061988899'
|
||||
},
|
||||
{
|
||||
'internal_id': 5112, 'full_address': '1 Huntspond Road, Titchfield, Fareham, PO14 4SS', 'postcode': 'PO14 4SS',
|
||||
'Note': 'Is this 1 St Francis Court, 195 Hunts Pond Road, Fareham, PO14 4SS, uprn: 100061988894'
|
||||
},
|
||||
{
|
||||
'internal_id': 3846, 'full_address': '2 Beaufort Road, Southbourne, Bournemouth, BH6 5BD',
|
||||
'postcode': 'BH6 5BD',
|
||||
'Note': "2 Beaufort Road, Southbourne, Bournemouth is listed under the postcode BH6 5AL - is there a typo in "
|
||||
"the postcode?"
|
||||
},
|
||||
{
|
||||
'internal_id': 4497, 'full_address': '11 Brokenford Lane, Totton, Southampton, SO40 9LZ',
|
||||
'postcode': 'SO40 9LZ',
|
||||
'Note': "This postcode doesn't appear to exist, closest is 10 brokenford lane, Totton, Southampton, SO40 9DW."
|
||||
"What should this be?"
|
||||
},
|
||||
{
|
||||
'internal_id': 4181, 'full_address': '25a Eastcott Road, Old Town, Swindon, SN1 3PA', 'postcode': 'SN1 3PA',
|
||||
'Note': 'All addresses at this postcode are for Bow Court. '
|
||||
'Closest match is 25 Eastcott Road, Swindon, SN1 3LT, but there is no 25A'
|
||||
},
|
||||
{
|
||||
'internal_id': 5447, 'full_address': '3 Send Road, Send Road, Reading, RG4 8EP', 'postcode': 'RG4 8EP',
|
||||
"Note": "These is no 'Send Road' at this postcode. There are a few possible matches, e.g. Flat 3, "
|
||||
"1 Send Road, RG4 8EH"
|
||||
},
|
||||
{
|
||||
'internal_id': 5449, 'full_address': '5 Send Road, Send Road, Reading, RG4 8EP', 'postcode': 'RG4 8EP',
|
||||
"Note": "Same as for 3 Send Road"
|
||||
},
|
||||
{
|
||||
'internal_id': 5450, 'full_address': '6 Send Road, Send Road, Reading, RG4 8EP', 'postcode': 'RG4 8EP',
|
||||
"Note": "Same as for 3 Send Road"
|
||||
},
|
||||
{
|
||||
'internal_id': 5446, 'full_address': '1 Send Road, Send Road, Reading, RG4 8EP', 'postcode': 'RG4 8EP',
|
||||
"Note": "Same as for 3 Send Road"
|
||||
},
|
||||
{
|
||||
'internal_id': 5448, 'full_address': '4 Send Road, Send Road, Reading, RG4 8EP', 'postcode': 'RG4 8EP',
|
||||
"Note": "Same as for 3 Send Road"
|
||||
},
|
||||
{
|
||||
'internal_id': 5451, 'full_address': '7 Send Road, Send Road, Reading, RG4 8EP', 'postcode': 'RG4 8EP',
|
||||
"Note": "Same as for 3 Send Road"
|
||||
},
|
||||
{
|
||||
'internal_id': 4547, 'full_address': '2 Cecil Terrace, Bemerton, Salisbury, SP2 9NE', 'postcode': 'SP2 9NE',
|
||||
"Note": "Addresses for this postcode are for The Croft, SP2 9NE. Should this be 2 Cecil Terrace SP2 9ND, with"
|
||||
"uprn: 100121039798 ?"
|
||||
},
|
||||
{
|
||||
'internal_id': 4549, 'full_address': '4 Cecil Terrace, Bemerton, Salisbury, SP2 9NE', 'postcode': 'SP2 9NE',
|
||||
"Note": "Addresses for this postcode are for The Croft, SP2 9NE. Should this be 4 Cecil Terrace SP2 9ND?"
|
||||
},
|
||||
{
|
||||
'internal_id': 3601, 'full_address': '20 Constitution Hill, Parkstone, Poole, BH14 0PX', 'postcode': 'BH14 0PX',
|
||||
"Note": "Should this be 20 Constitution Hill Gardens, Poole, BH14 0PY? (i.e. postcode is wrong) "
|
||||
"uprn: 10001086693"
|
||||
},
|
||||
{
|
||||
'internal_id': 3592, 'full_address': '7 Constitution Hill, Parkstone, Poole, BH14 0PX', 'postcode': 'BH14 0PX',
|
||||
"Note": "Should the postcode be BH14 0PY ?"
|
||||
},
|
||||
{
|
||||
'internal_id': 3594, 'full_address': '9 Constitution Hill, Parkstone, Poole, BH14 0PX', 'postcode': 'BH14 0PX',
|
||||
"Note": "Should the postcode be BH14 0PY ?"
|
||||
},
|
||||
{
|
||||
'internal_id': 3591, 'full_address': '6 Constitution Hill, Parkstone, Poole, BH14 0PX', 'postcode': 'BH14 0PX',
|
||||
"Note": "Should the postcode be BH14 0PY ?"
|
||||
},
|
||||
{
|
||||
'internal_id': 3593, 'full_address': '8 Constitution Hill, Parkstone, Poole, BH14 0PX', 'postcode': 'BH14 0PX',
|
||||
"Note": "Should the postcode be BH14 0PY ?"},
|
||||
{
|
||||
'internal_id': 3590, 'full_address': '5 Constitution Hill, Parkstone, Poole, BH14 0PX', 'postcode': 'BH14 0PX',
|
||||
"Note": "Should the postcode be BH14 0PY ?"},
|
||||
{
|
||||
'internal_id': 3589, 'full_address': '3 Constitution Hill, Parkstone, Poole, BH14 0PX', 'postcode': 'BH14 0PX',
|
||||
"Note": "Should the postcode be BH14 0PY ?"},
|
||||
{
|
||||
'internal_id': 3600, 'full_address': '18 Constitution Hill, Parkstone, Poole, BH14 0PX',
|
||||
'postcode': 'BH14 0PX', "Note": "Should the postcode be BH14 0PY ?"},
|
||||
{
|
||||
'internal_id': 3599, 'full_address': '17 Constitution Hill, Parkstone, Poole, BH14 0PX',
|
||||
'postcode': 'BH14 0PX', "Note": "Should the postcode be BH14 0PY ?"},
|
||||
{'internal_id': 3598, 'full_address': '15 Constitution Hill, Parkstone, Poole, BH14 0PX', 'postcode': 'BH14 0PX',
|
||||
"Note": "Should the postcode be BH14 0PY ?"},
|
||||
{'internal_id': 3608, 'full_address': '26 Constitution Hill, Parkstone, Poole, BH14 0PX', 'postcode': 'BH14 0PX',
|
||||
"Note": "Should the postcode be BH14 0PY ?"},
|
||||
{'internal_id': 3610, 'full_address': '30 Constitution Hill, Parkstone, Poole, BH14 0PX', 'postcode': 'BH14 0PX',
|
||||
"Note": "Should the postcode be BH14 0PY ?"},
|
||||
{'internal_id': 3603, 'full_address': '22 Constitution Hill, Parkstone, Poole, BH14 0PX', 'postcode': 'BH14 0PX',
|
||||
"Note": "Should the postcode be BH14 0PY ?"},
|
||||
{'internal_id': 3612, 'full_address': '32 Constitution Hill, Parkstone, Poole, BH14 0PX', 'postcode': 'BH14 0PX',
|
||||
"Note": "Should the postcode be BH14 0PY ?"},
|
||||
{'internal_id': 3595, 'full_address': '10 Constitution Hill, Parkstone, Poole, BH14 0PX', 'postcode': 'BH14 0PX',
|
||||
"Note": "Should the postcode be BH14 0PY ?"},
|
||||
{'internal_id': 3613, 'full_address': '34 Constitution Hill, Parkstone, Poole, BH14 0PX', 'postcode': 'BH14 0PX',
|
||||
"Note": "Should the postcode be BH14 0PY ?"},
|
||||
|
||||
{'internal_id': 3597, 'full_address': '12 Constitution Hill, Parkstone, Poole, BH14 0PX', 'postcode': 'BH14 0PX',
|
||||
"Note": "Should the postcode be BH14 0QB ?"},
|
||||
{'internal_id': 3602, 'full_address': '21 Constitution Hill, Parkstone, Poole, BH14 0PX', 'postcode': 'BH14 0PX',
|
||||
"Note": "Should the postcode be BH14 0QB ?"},
|
||||
{'internal_id': 3606, 'full_address': '19 Constitution Hill, Parkstone, Poole, BH14 0PX', 'postcode': 'BH14 0PX',
|
||||
"Note": "Should the postcode be BH14 0QB ?"},
|
||||
{'internal_id': 3604, 'full_address': '23 Constitution Hill, Parkstone, Poole, BH14 0PX', 'postcode': 'BH14 0PX',
|
||||
"Note": "Should the postcode be BH14 0QB ?"},
|
||||
{'internal_id': 3605, 'full_address': '25 Constitution Hill, Parkstone, Poole, BH14 0PX', 'postcode': 'BH14 0PX',
|
||||
"Note": "Should the postcode be BH14 0QB ?"},
|
||||
{'internal_id': 3609, 'full_address': '29 Constitution Hill, Parkstone, Poole, BH14 0PX', 'postcode': 'BH14 0PX',
|
||||
"Note": "Should the postcode be BH14 0QB ?"},
|
||||
{'internal_id': 3596, 'full_address': '11 Constitution Hill, Parkstone, Poole, BH14 0PX', 'postcode': 'BH14 0PX',
|
||||
"Note": "Should the postcode be BH14 0QB ?"},
|
||||
{'internal_id': 3607, 'full_address': '27 Constitution Hill, Parkstone, Poole, BH14 0PX', 'postcode': 'BH14 0PX',
|
||||
"Note": "Should the postcode be BH14 0QB ?"},
|
||||
{'internal_id': 3611, 'full_address': '31 Constitution Hill, Parkstone, Poole, BH14 0PX', 'postcode': 'BH14 0PX',
|
||||
"Note": "Should the postcode be BH14 0QB ?"},
|
||||
{'internal_id': 5622, 'full_address': '26 Roman Way, Andover, SP10 5HZ', 'postcode': 'SP10 5HZ',
|
||||
'Note': 'Shoul this postcode be SP10 5JU ?'}
|
||||
]
|
||||
1917
etl/customers/stonewater/shdf_3_clustering.py
Normal file
1917
etl/customers/stonewater/shdf_3_clustering.py
Normal file
File diff suppressed because it is too large
Load diff
148
etl/customers/unitas/20_may_2024_data_pull.py
Normal file
148
etl/customers/unitas/20_may_2024_data_pull.py
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
import os
|
||||
|
||||
import pandas as pd
|
||||
from tqdm import tqdm
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from backend.SearchEpc import SearchEpc
|
||||
from etl.epc_clean.epc_attributes.RoofAttributes import RoofAttributes
|
||||
|
||||
from recommendations.recommendation_utils import (
|
||||
estimate_perimeter,
|
||||
estimate_external_wall_area,
|
||||
estimate_number_of_floors
|
||||
)
|
||||
|
||||
load_dotenv(dotenv_path="backend/.env")
|
||||
EPC_AUTH_TOKEN = os.getenv("EPC_AUTH_TOKEN")
|
||||
|
||||
|
||||
def app():
|
||||
"""
|
||||
This app is EPC pulling data for some properties owned by Unitas
|
||||
:return:
|
||||
"""
|
||||
# asset_list = read_excel_from_s3(
|
||||
# bucket_name="retrofit-datalake-dev",
|
||||
# file_key="customers/guiness/TGP CW Properties PV.xlsx",
|
||||
# header_row=0
|
||||
# )
|
||||
asset_list = pd.read_excel(
|
||||
"/Users/khalimconn-kowlessar/Downloads/UNITAS BUNGALOWS - EPC DATA PULL.xlsx", header=0
|
||||
)
|
||||
|
||||
epc_data = []
|
||||
for _, home in tqdm(asset_list.iterrows(), total=len(asset_list)):
|
||||
|
||||
searcher = SearchEpc(
|
||||
address1=str(home["Address Line 1"]),
|
||||
postcode=home["Post Code"],
|
||||
uprn=home["Property Reference"],
|
||||
auth_token=EPC_AUTH_TOKEN,
|
||||
os_api_key="",
|
||||
property_type=None,
|
||||
fast=True
|
||||
)
|
||||
# Force the skipping of estimating the EPC
|
||||
searcher.ordnance_survey_client.property_type = None
|
||||
searcher.ordnance_survey_client.built_form = None
|
||||
|
||||
searcher.find_property(skip_os=True)
|
||||
if searcher.newest_epc is None:
|
||||
continue
|
||||
|
||||
epc = {
|
||||
"asset_list_address": home["Address Line 1"],
|
||||
"asset_list_postcode": home["Post Code"],
|
||||
**searcher.newest_epc.copy()
|
||||
}
|
||||
|
||||
epc_data.append(epc)
|
||||
|
||||
epc_df = pd.DataFrame(epc_data)
|
||||
|
||||
# Retrieve just the data we need
|
||||
epc_df = epc_df[
|
||||
[
|
||||
"asset_list_address",
|
||||
"uprn",
|
||||
"property-type",
|
||||
"built-form",
|
||||
"inspection-date",
|
||||
"current-energy-rating",
|
||||
"current-energy-efficiency",
|
||||
"roof-description",
|
||||
"walls-description",
|
||||
"transaction-type",
|
||||
# New fields needed
|
||||
"secondheat-description",
|
||||
"total-floor-area",
|
||||
"construction-age-band",
|
||||
"floor-height",
|
||||
"number-habitable-rooms",
|
||||
"mainheat-description"
|
||||
]
|
||||
]
|
||||
|
||||
asset_list = asset_list.merge(
|
||||
epc_df,
|
||||
how="left",
|
||||
left_on=["Address Line 1"],
|
||||
right_on=["asset_list_address"]
|
||||
)
|
||||
|
||||
asset_list = asset_list.drop(columns=["asset_list_address"])
|
||||
|
||||
# Rename the columns
|
||||
asset_list = asset_list.rename(columns={
|
||||
"inspection-date": "Date of last EPC",
|
||||
"current-energy-efficiency": "SAP score on register",
|
||||
"current-energy-rating": "EPC rating on register",
|
||||
"property-type": "EPC Property Type",
|
||||
"built-form": "Archetype",
|
||||
"total-floor-area": "Property Floor Area",
|
||||
"construction-age-band": "Property Age Band",
|
||||
"floor-height": "Property Floor Height",
|
||||
"number-habitable-rooms": "Number of Habitable Rooms",
|
||||
"walls-description": "Wall Construction",
|
||||
"roof-description": "Roof Construction",
|
||||
"mainheat-description": "Heating Type",
|
||||
"secondheat-description": "Secondary Heating",
|
||||
"transaction-type": "Reason for last EPC"
|
||||
})
|
||||
|
||||
asset_list["Estimated Number of Floors"] = asset_list.apply(
|
||||
lambda x: estimate_number_of_floors(property_type=x["EPC Property Type"]) if not pd.isnull(
|
||||
x["EPC Property Type"]) else None,
|
||||
axis=1
|
||||
)
|
||||
|
||||
asset_list["Property Floor Area"] = asset_list["Property Floor Area"].astype(float)
|
||||
asset_list["Number of Habitable Rooms"] = asset_list["Number of Habitable Rooms"].astype(float)
|
||||
|
||||
asset_list["Estimated Perimeter (m)"] = asset_list.apply(
|
||||
lambda x: estimate_perimeter(
|
||||
floor_area=x["Property Floor Area"] / x["Estimated Number of Floors"],
|
||||
num_rooms=x["Number of Habitable Rooms"] / x["Estimated Number of Floors"],
|
||||
) if not pd.isnull(x["uprn"]) else None, axis=1
|
||||
)
|
||||
|
||||
asset_list["Estimated Heat Loss Perimeter (m)"] = asset_list.apply(
|
||||
lambda x: estimate_external_wall_area(
|
||||
num_floors=x["Estimated Number of Floors"],
|
||||
floor_height=float(x["Property Floor Height"]) if x["Property Floor Height"] else 2.5,
|
||||
perimeter=x["Estimated Perimeter (m)"],
|
||||
built_form=x["Archetype"]
|
||||
) if not pd.isnull(x["uprn"]) else None,
|
||||
axis=1
|
||||
)
|
||||
|
||||
asset_list["Roof Insulation Thickness"] = asset_list.apply(
|
||||
lambda x: RoofAttributes(description=x["Roof Construction"]).process()["insulation_thickness"] if not pd.isnull(
|
||||
x["uprn"]) else None,
|
||||
axis=1
|
||||
)
|
||||
|
||||
# Store as an excel
|
||||
filename = "UNITAS BUNGALOWS - EPC DATA PULL - May 30tg 2024.xlsx"
|
||||
asset_list.to_excel(filename, index=False)
|
||||
182
etl/customers/unitas/Audit_check.py
Normal file
182
etl/customers/unitas/Audit_check.py
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
import pandas as pd
|
||||
import os
|
||||
|
||||
from tqdm import tqdm
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from backend.SearchEpc import SearchEpc
|
||||
|
||||
load_dotenv(dotenv_path="backend/.env")
|
||||
EPC_AUTH_TOKEN = os.getenv("EPC_AUTH_TOKEN")
|
||||
|
||||
|
||||
def app():
|
||||
# Read in rolling master
|
||||
master = pd.read_csv(
|
||||
"/Users/khalimconn-kowlessar/Downloads/UNITAS ( STOKE) MASTER ROLLING SHEET UPDATED 16.5.24 K - PASSWORD "
|
||||
"PROTECTED/ECO 4 - PHASE 1-Table 1.csv"
|
||||
)
|
||||
|
||||
master = master[master["INSTALLER"] == "SCIS"]
|
||||
|
||||
master = master[
|
||||
[
|
||||
'UPRN', 'NO.', 'Street / Block Name', 'Town/Area', 'Post Code', 'Surveyor', "SUBMISSION DATE"
|
||||
]
|
||||
]
|
||||
|
||||
master = master[~pd.isnull(master["UPRN"])]
|
||||
master = master[master["UPRN"] != "NOT ON ASSET LIST"]
|
||||
|
||||
heights = []
|
||||
eco_assessment_epcs = []
|
||||
for _, row in tqdm(master.iterrows(), total=len(master)):
|
||||
searcher = SearchEpc(
|
||||
address1="",
|
||||
postcode="",
|
||||
uprn=str(int(row["UPRN"])),
|
||||
auth_token=EPC_AUTH_TOKEN,
|
||||
os_api_key="",
|
||||
property_type=None,
|
||||
fast=False,
|
||||
)
|
||||
# Force the skipping of estimating the EPC
|
||||
searcher.ordnance_survey_client.property_type = None
|
||||
searcher.ordnance_survey_client.built_form = None
|
||||
|
||||
searcher.find_property(skip_os=True)
|
||||
if searcher.newest_epc is None:
|
||||
continue
|
||||
|
||||
# Look for eco assessment epcs
|
||||
eco_epc = [x for x in [searcher.newest_epc] + searcher.older_epcs if x['transaction-type'] == 'ECO assessment']
|
||||
# Take the newest
|
||||
eco_epc = sorted(eco_epc, key=lambda x: x['inspection-date'], reverse=True)
|
||||
if eco_epc:
|
||||
eco_assessment_epcs.append(eco_epc[0])
|
||||
|
||||
height = {
|
||||
"uprn": row["UPRN"],
|
||||
"floor_height": searcher.newest_epc["floor-height"]
|
||||
}
|
||||
heights.append(height)
|
||||
|
||||
heights_df = pd.DataFrame(heights)
|
||||
|
||||
eco_assessment_epcs_df = pd.DataFrame(eco_assessment_epcs)
|
||||
|
||||
merged_heights_df = master.merge(heights_df, left_on="UPRN", right_on="uprn", how="inner")
|
||||
merged_heights_df = merged_heights_df[merged_heights_df["floor_height"] != ""]
|
||||
merged_eco_assessment_epcs_df = master.merge(eco_assessment_epcs_df[["uprn", "floor-height"]], left_on="UPRN",
|
||||
right_on="uprn", how="inner")
|
||||
merged_eco_assessment_epcs_df["floor-height"] = merged_eco_assessment_epcs_df["floor-height"].astype(float)
|
||||
|
||||
merged_eco_assessment_epcs_df.groupby("Surveyor")["floor-height"].mean()
|
||||
|
||||
# Store
|
||||
merged_heights_df.to_csv("Unitas 2022 heights - based on newest EPC.csv", index=False)
|
||||
merged_eco_assessment_epcs_df.to_csv("Unitas 2022 heights - based on ECO assessment EPC.csv", index=False)
|
||||
|
||||
# Read in a diferent sheet
|
||||
master = pd.read_excel(
|
||||
"/Users/khalimconn-kowlessar/Downloads/COMMUNITY HOUSING SURVEYS WITH A POST EPC.xlsx"
|
||||
)
|
||||
|
||||
master["row_number"] = master.index
|
||||
|
||||
heights = []
|
||||
eco_assessment_epcs = []
|
||||
expected_pre = []
|
||||
expected_post = []
|
||||
biggest_floor_height = []
|
||||
for _, row in tqdm(master.iterrows(), total=len(master)):
|
||||
|
||||
full_address = ", ".join([
|
||||
str(row["NO."]), row["Street / Block Name"], row["Town/Area"], row["Post Code"]
|
||||
])
|
||||
searcher = SearchEpc(
|
||||
address1=str(row["NO."]),
|
||||
postcode=str(row["Post Code"]),
|
||||
auth_token=EPC_AUTH_TOKEN,
|
||||
os_api_key="",
|
||||
property_type=None,
|
||||
fast=False,
|
||||
full_address=full_address
|
||||
)
|
||||
|
||||
# Force the skipping of estimating the EPC
|
||||
searcher.ordnance_survey_client.property_type = None
|
||||
searcher.ordnance_survey_client.built_form = None
|
||||
|
||||
searcher.find_property(skip_os=True)
|
||||
|
||||
if searcher.newest_epc is None:
|
||||
continue
|
||||
|
||||
all_epcs = [searcher.newest_epc] + searcher.older_epcs
|
||||
# Search for SAP 54s
|
||||
sap_54s = [x for x in all_epcs if x["current-energy-efficiency"] == "54"]
|
||||
sap_69s = [x for x in all_epcs if x["current-energy-efficiency"] == "69"]
|
||||
heights = [float(x["floor-height"]) for x in all_epcs if x["floor-height"] != ""]
|
||||
|
||||
# Look for eco assessment epcs
|
||||
eco_epc = [x for x in [searcher.newest_epc] + searcher.older_epcs if x['transaction-type'] == 'ECO assessment']
|
||||
# Take the newest
|
||||
eco_epc = sorted(eco_epc, key=lambda x: x['inspection-date'], reverse=True)
|
||||
if eco_epc:
|
||||
eco_assessment_epcs.append(
|
||||
{
|
||||
"row_number": row["row_number"],
|
||||
**eco_epc[0]
|
||||
}
|
||||
)
|
||||
|
||||
if heights:
|
||||
floor_height_max = max(heights)
|
||||
biggest_floor_height.append(
|
||||
{
|
||||
"row_number": row["row_number"],
|
||||
"floor_height": floor_height_max
|
||||
}
|
||||
)
|
||||
|
||||
if sap_54s:
|
||||
expected_pre.append(
|
||||
{
|
||||
"row_number": row["row_number"],
|
||||
**sap_54s[0]
|
||||
}
|
||||
)
|
||||
|
||||
if sap_69s:
|
||||
expected_post.append(
|
||||
{
|
||||
"row_number": row["row_number"],
|
||||
**sap_69s[0]
|
||||
}
|
||||
)
|
||||
|
||||
expected_pre_df = pd.DataFrame(expected_pre)
|
||||
expected_post_df = pd.DataFrame(expected_post)
|
||||
|
||||
heights_df = pd.DataFrame(biggest_floor_height)
|
||||
eco_assessment_epcs_df = pd.DataFrame(eco_assessment_epcs)
|
||||
|
||||
merged_heights_df = master.merge(heights_df, on="row_number", how="inner")
|
||||
merged_heights_df = merged_heights_df[merged_heights_df["floor_height"] != ""]
|
||||
|
||||
merged_eco_assessment_epcs_df = master.merge(
|
||||
eco_assessment_epcs_df[["row_number", "floor-height"]], on="row_number", how="inner"
|
||||
)
|
||||
merged_eco_assessment_epcs_df["floor-height"] = merged_eco_assessment_epcs_df["floor-height"].astype(float)
|
||||
|
||||
merged_eco_assessment_epcs_df.groupby("Surveyor")["floor-height"].mean()
|
||||
|
||||
# Check average floor height for social housing properties with ECO assessment EPCs in Birmingham
|
||||
sample = pd.read_csv("local_data/all-domestic-certificates/domestic-E08000025-Birmingham/certificates.csv")
|
||||
sample = sample[sample["TRANSACTION_TYPE"] == "ECO assessment"]
|
||||
sample = sample[sample["TENURE"].isin(["rental (social)", "Rented (social)"])]
|
||||
sample["FLOOR_HEIGHT"] = sample["FLOOR_HEIGHT"].astype(float)
|
||||
sample["FLOOR_HEIGHT"].mean()
|
||||
|
||||
sample[pd.to_datetime(sample["LODGEMENT_DATE"]) >= "2022-01-01"]["FLOOR_HEIGHT"].mean()
|
||||
|
|
@ -203,11 +203,11 @@ class TrainingDataset(BaseDataset):
|
|||
common_cols = [[col + "_starting", col + "_ending"] for col in common_cols]
|
||||
|
||||
self.df = self.df.loc[
|
||||
:,
|
||||
no_suffix_cols
|
||||
+ only_ending_cols
|
||||
+ [col for cols in common_cols for col in cols],
|
||||
]
|
||||
:,
|
||||
no_suffix_cols
|
||||
+ only_ending_cols
|
||||
+ [col for cols in common_cols for col in cols],
|
||||
]
|
||||
|
||||
def _remove_abnormal_change_in_floor_area(self):
|
||||
"""
|
||||
|
|
@ -511,7 +511,7 @@ class TrainingDataset(BaseDataset):
|
|||
expanded_df["is_sandstone_or_limestone"]
|
||||
== expanded_df["is_sandstone_or_limestone_ending"]
|
||||
)
|
||||
]
|
||||
]
|
||||
elif component == "floor":
|
||||
expanded_df = expanded_df[
|
||||
(expanded_df["is_suspended"] == expanded_df["is_suspended_ending"])
|
||||
|
|
@ -528,7 +528,7 @@ class TrainingDataset(BaseDataset):
|
|||
expanded_df["is_to_external_air"]
|
||||
== expanded_df["is_to_external_air_ending"]
|
||||
)
|
||||
]
|
||||
]
|
||||
elif component == "roof":
|
||||
expanded_df = expanded_df[
|
||||
(expanded_df["is_pitched"] == expanded_df["is_pitched_ending"])
|
||||
|
|
@ -541,7 +541,7 @@ class TrainingDataset(BaseDataset):
|
|||
expanded_df["has_dwelling_above"]
|
||||
== expanded_df["has_dwelling_above_ending"]
|
||||
)
|
||||
]
|
||||
]
|
||||
|
||||
return expanded_df
|
||||
|
||||
|
|
@ -742,7 +742,7 @@ class TrainingDataset(BaseDataset):
|
|||
self.df[col] = self.df[col].fillna("Unknown")
|
||||
|
||||
def _null_validation(self, information: str):
|
||||
print(f"Null validation after {information}")
|
||||
# print(f"Null validation after {information}")
|
||||
if pd.isnull(self.df).sum().sum():
|
||||
raise ValueError(f"Null values found in dataset, after step {information}")
|
||||
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ class RoofAttributes(Definitions):
|
|||
"""
|
||||
|
||||
self.description: str = description.lower().strip()
|
||||
self.nodata = not description or description in self.DATA_ANOMALY_MATCHES
|
||||
self.nodata = not description or description in self.DATA_ANOMALY_MATCHES or self.description == "sap05:roof"
|
||||
|
||||
self.welsh_translation_search()
|
||||
|
||||
|
|
|
|||
7
etl/property_valuation/requirements.txt
Normal file
7
etl/property_valuation/requirements.txt
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
seleniumbase
|
||||
beautifulsoup4
|
||||
requests
|
||||
pandas
|
||||
tqdm
|
||||
openpyxl
|
||||
undetected_chromedriver
|
||||
88
etl/property_valuation/scrape_valuations.py
Normal file
88
etl/property_valuation/scrape_valuations.py
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
import requests
|
||||
import random
|
||||
import time
|
||||
import pandas as pd
|
||||
from bs4 import BeautifulSoup
|
||||
from tqdm import tqdm
|
||||
from seleniumbase import Driver
|
||||
from seleniumbase import page_actions
|
||||
|
||||
import undetected_chromedriver as webdriver
|
||||
from selenium.webdriver.chrome.service import Service
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
import time
|
||||
import pandas as pd
|
||||
|
||||
BASE_URL = "https://www.zoopla.co.uk/property/uprn/{uprn}/"
|
||||
|
||||
|
||||
def initialize_driver():
|
||||
driver = Driver(headless=True, uc=True) # Set headless to True if you want headless mode
|
||||
return driver
|
||||
|
||||
|
||||
def app():
|
||||
# Read in the starting asset list
|
||||
asset_list = pd.read_excel("portfolio_epc_data_50m 28th May.xlsx")
|
||||
asset_list = asset_list[["UPRN", "ADDRESS", "POSTCODE"]]
|
||||
|
||||
# asset_list.to_excel("property value.xlsx", index=False)
|
||||
|
||||
# Generate the list of urls
|
||||
urls = [BASE_URL.format(uprn=uprn) for uprn in asset_list["UPRN"]]
|
||||
|
||||
driver = webdriver.Chrome()
|
||||
|
||||
driver = initialize_driver()
|
||||
driver.set_page_load_timeout(30) # Increase page load timeout
|
||||
|
||||
result = []
|
||||
for i, (url, uprn) in tqdm(enumerate(zip(urls, asset_list["UPRN"].tolist())), total=len(urls)):
|
||||
|
||||
# Every 10 requests sleep for an extra 7 seconds
|
||||
if len(result) % 10 == 0 and len(result) != 0:
|
||||
time.sleep(7)
|
||||
|
||||
try:
|
||||
|
||||
driver.get(url)
|
||||
page_actions.wait_for_element_visible(driver, "p[data-testid='estimate-blurred']", timeout=30)
|
||||
|
||||
price_element = driver.find_element("css selector", "p[data-testid='estimate-blurred']")
|
||||
price = price_element.get_text(strip=True)
|
||||
|
||||
low_price_element = driver.find_element("css selector", "span[data-testid='low-estimate-blurred']")
|
||||
low_price = low_price_element.get_text(strip=True)
|
||||
|
||||
high_price_element = driver.find_element("css selector", "span[data-testid='high-estimate-blurred']")
|
||||
high_price = high_price_element.get_text(strip=True)
|
||||
|
||||
result.append(
|
||||
{
|
||||
"UPRN": uprn,
|
||||
"price": price,
|
||||
"lower_estimate": low_price,
|
||||
"upper_estimate": high_price
|
||||
}
|
||||
)
|
||||
|
||||
# Sleep a random amount of time between 5 and 20 seconds
|
||||
sleep_time = 5 + (15 * random.random())
|
||||
time.sleep(sleep_time)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Failed to retrieve data for UPRN {uprn} at iteration {i}: {e}")
|
||||
|
||||
# Store the result depending on where we are
|
||||
savepoint = pd.DataFrame(result)
|
||||
savepoint.to_csv(f"savepoint_index_{i}.csv", index=False)
|
||||
|
||||
# TODO: Testing Jina AI - didn't work but maybe one of the alternatives might work:
|
||||
# https://www.youtube.com/watch?v=QxHE4af5BQE
|
||||
response = requests.get("https://r.jina.ai/https://www.zoopla.co.uk/property/uprn/41222761/")
|
||||
response.text
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app()
|
||||
|
|
@ -20,21 +20,21 @@ regional_labour_variations = [
|
|||
|
||||
# This data is based on the MCS database
|
||||
MCS_SOLAR_PV_COST_DATA = {
|
||||
"last_updated": "2024-01-04",
|
||||
"average_cost_per_kwh": 2013.94,
|
||||
"average_cost_per_kwh-Outer London": 2618.75,
|
||||
"average_cost_per_kwh-Inner London": 2618.75,
|
||||
"average_cost_per_kwh-South East England": 2083.33,
|
||||
"average_cost_per_kwh-South West England": 2113,
|
||||
"average_cost_per_kwh-East of England": 1973.86,
|
||||
"average_cost_per_kwh-East Midlands": 1981.86,
|
||||
"average_cost_per_kwh-West Midlands": 1926.55,
|
||||
"average_cost_per_kwh-North East England": 2028.49,
|
||||
"average_cost_per_kwh-North West England": 1620.42,
|
||||
"average_cost_per_kwh-Yorkshire and the Humber": 2060.9,
|
||||
"average_cost_per_kwh-Wales": 1898.83,
|
||||
"average_cost_per_kwh-Scotland": 1967.97,
|
||||
"average_cost_per_kwh-Northern Ireland": 2126.09,
|
||||
"last_updated": "2024-06-10",
|
||||
"average_cost_per_kwh": 1750,
|
||||
"average_cost_per_kwh-Outer London": 1776,
|
||||
"average_cost_per_kwh-Inner London": 1776,
|
||||
"average_cost_per_kwh-South East England": 1672,
|
||||
"average_cost_per_kwh-South West England": 1732,
|
||||
"average_cost_per_kwh-East of England": 1721,
|
||||
"average_cost_per_kwh-East Midlands": 1730,
|
||||
"average_cost_per_kwh-West Midlands": 1761,
|
||||
"average_cost_per_kwh-North East England": 1669,
|
||||
"average_cost_per_kwh-North West England": 1764,
|
||||
"average_cost_per_kwh-Yorkshire and the Humber": 1705,
|
||||
"average_cost_per_kwh-Wales": 1896,
|
||||
"average_cost_per_kwh-Scotland": 1767,
|
||||
"average_cost_per_kwh-Northern Ireland": 1767,
|
||||
}
|
||||
|
||||
# This data is based on the MCS database, We use the larger figure between the 2023 and 2024 average,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,14 @@ from recommendations.HeatingControlRecommender import HeatingControlRecommender
|
|||
|
||||
|
||||
class HeatingRecommender:
|
||||
ELECTRIC_HEATING_DESCRIPTIONS = [
|
||||
"Room heaters, electric",
|
||||
"Electric storage heaters",
|
||||
"Electric storage heaters, radiators",
|
||||
"Portable electric heaters assumed for most rooms",
|
||||
]
|
||||
|
||||
high_heat_retention_contols_desc = "Controls for high heat retention storage heaters"
|
||||
|
||||
def __init__(self, property_instance: Property):
|
||||
self.property = property_instance
|
||||
|
|
@ -16,6 +24,24 @@ class HeatingRecommender:
|
|||
self.heating_recommendations = []
|
||||
self.heating_control_recommendations = []
|
||||
|
||||
self.has_electric_heating_description = (
|
||||
self.property.main_heating["clean_description"] in self.ELECTRIC_HEATING_DESCRIPTIONS
|
||||
)
|
||||
|
||||
def is_high_heat_retention_valid(self):
|
||||
"""
|
||||
Check conditions if high heat retention storage is valid
|
||||
:return:
|
||||
"""
|
||||
|
||||
# If the property has assumed electric heating, regardless of whether or not it has a mains connection, we
|
||||
# can consider hhr storage heaters
|
||||
electric_heating_assumed = (
|
||||
self.property.main_heating["clean_description"] in ["No system present, electric heaters assumed"]
|
||||
)
|
||||
|
||||
return self.has_electric_heating_description or electric_heating_assumed
|
||||
|
||||
def recommend(self, has_cavity_or_loft_recommendations, phase=0):
|
||||
"""
|
||||
Produces heating recommendations
|
||||
|
|
@ -34,17 +60,10 @@ class HeatingRecommender:
|
|||
# This first iteration of the recommender will provide very basic recommendation
|
||||
# We recommend heating controls based on the main heating system
|
||||
|
||||
has_electric_heating_description = self.property.main_heating["clean_description"] in [
|
||||
"Room heaters, electric", "Electric storage heaters", "Electric storage heaters, radiators"
|
||||
]
|
||||
|
||||
no_heating_no_mains = (
|
||||
self.property.main_heating["clean_description"] in ["No system present, electric heaters assumed"] and
|
||||
not self.property.data["mains-gas-flag"]
|
||||
)
|
||||
|
||||
if has_electric_heating_description or no_heating_no_mains:
|
||||
if self.is_high_heat_retention_valid():
|
||||
# Recommend high heat retention storage heaters
|
||||
# TODO: We need to allow for the possibility that the property aleady has storage heaters, but just
|
||||
# needs the controls
|
||||
self.recommend_hhr_storage_heaters(phase=phase, system_change=True, heating_controls_only=False)
|
||||
|
||||
# if the property has mains heating with boiler and radiators, we recommend optimal heating controls
|
||||
|
|
@ -61,7 +80,7 @@ class HeatingRecommender:
|
|||
)
|
||||
|
||||
# We also check if the property has electric heating, but it has access to the mains gas
|
||||
electic_heating_has_mains = has_electric_heating_description and self.property.data["mains-gas-flag"]
|
||||
electic_heating_has_mains = self.has_electric_heating_description and self.property.data["mains-gas-flag"]
|
||||
|
||||
portable_heaters_has_mains = (
|
||||
self.property.main_heating["clean_description"] in ["Portable electric heaters assumed for most rooms"] and
|
||||
|
|
@ -93,16 +112,19 @@ class HeatingRecommender:
|
|||
# In the future, we'll allow overrides, so that non-intrusive surveys can contradict these conditions
|
||||
# and either allow or prevent the recommendation of an air source heat pump
|
||||
|
||||
suitable_property_type = self.property.data["property-type"] in ["House", "Bungalow"]
|
||||
has_air_source_heat_pump = self.property.main_heating["has_air_source_heat_pump"]
|
||||
|
||||
if suitable_property_type and not has_air_source_heat_pump:
|
||||
if self.is_ashp_valid():
|
||||
self.recommend_air_source_heat_pump(
|
||||
phase=phase, has_cavity_or_loft_recommendations=has_cavity_or_loft_recommendations
|
||||
)
|
||||
|
||||
return
|
||||
|
||||
def is_ashp_valid(self):
|
||||
suitable_property_type = self.property.data["property-type"] in ["House", "Bungalow"]
|
||||
has_air_source_heat_pump = self.property.main_heating["has_air_source_heat_pump"]
|
||||
|
||||
return suitable_property_type and not has_air_source_heat_pump
|
||||
|
||||
def recommend_air_source_heat_pump(self, phase, has_cavity_or_loft_recommendations, _return=False):
|
||||
"""
|
||||
This method will implement the recommendation for an air source heat pump
|
||||
|
|
@ -314,6 +336,27 @@ class HeatingRecommender:
|
|||
|
||||
return output
|
||||
|
||||
def is_hhr_already_installed(self):
|
||||
"""
|
||||
Check if the property already has high heat retention storage heaters
|
||||
:return:
|
||||
"""
|
||||
|
||||
already_has_hhr = "Electric storage heaters" in self.property.main_heating["clean_description"]
|
||||
|
||||
# Some electric storage heaters will show that the controls are "Manual charge controls" which are indicative
|
||||
# of the old model of electric storage heaters, originating from 1961.
|
||||
# Newer HHR storage heaters will charge up over night but will retain the heat durin the day for when warmth
|
||||
# is actually needed, unlike traditional storage heaters that charge up at night and release heat during the day
|
||||
# which isn't always ideal for the occupants.
|
||||
already_has_hhr_contols = (
|
||||
self.property.main_heating_controls[
|
||||
"clean_description"
|
||||
].lower() == self.high_heat_retention_contols_desc.lower()
|
||||
)
|
||||
|
||||
return already_has_hhr and already_has_hhr_contols
|
||||
|
||||
def recommend_hhr_storage_heaters(self, phase, system_change, heating_controls_only, _return=False):
|
||||
"""
|
||||
We will recommend upgrading to a high heat retention storage system, if the current system is not already
|
||||
|
|
@ -330,19 +373,14 @@ class HeatingRecommender:
|
|||
|
||||
controls_recommender = HeatingControlRecommender(self.property)
|
||||
# The heating controls we're recommending for are based on the recommended heating system
|
||||
high_heat_retention_contols_desc = "Controls for high heat retention storage heaters"
|
||||
|
||||
# We only recommend Celect-type controls if the current heating system is not Celect-type controls
|
||||
if self.property.main_heating_controls["clean_description"] != high_heat_retention_contols_desc:
|
||||
if self.property.main_heating_controls["clean_description"] != self.high_heat_retention_contols_desc:
|
||||
controls_recommender.recommend(heating_description="Electric storage heaters, radiators")
|
||||
|
||||
# Conditions for not needing this recommendation
|
||||
already_installed_hh_retention = (
|
||||
"Electric storage heaters" in self.property.main_heating["clean_description"] and
|
||||
self.property.main_heating_controls["clean_description"].lower() == high_heat_retention_contols_desc.lower()
|
||||
)
|
||||
|
||||
has_hhr = self.is_hhr_already_installed()
|
||||
# Conditions for not recommending electric storage heaters
|
||||
if already_installed_hh_retention:
|
||||
if has_hhr:
|
||||
# No recommendation needed
|
||||
return
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import itertools
|
||||
from utils.logger import setup_logger
|
||||
from backend.Property import Property
|
||||
from recommendations.FloorRecommendations import FloorRecommendations
|
||||
from recommendations.WallRecommendations import WallRecommendations
|
||||
|
|
@ -12,13 +14,25 @@ from recommendations.HotwaterRecommendations import HotwaterRecommendations
|
|||
from recommendations.SecondaryHeating import SecondaryHeating
|
||||
from recommendations.Recommendations import Recommendations
|
||||
|
||||
logger = setup_logger()
|
||||
|
||||
|
||||
class Mds:
|
||||
"""
|
||||
Handles the contruction of the MDS report
|
||||
"""
|
||||
|
||||
def __init__(self, property_instance: Property, materials):
|
||||
format_map = {
|
||||
"external_wall_insulation": "EWI (Trad Const)",
|
||||
"internal_wall_insualtion": "IWI",
|
||||
"cavity_wall_insulation": "CWI",
|
||||
"loft_insulation": "LI",
|
||||
"air_source_heat_pump": "ASHP Htg",
|
||||
"high_heat_retention_storage_heaters": "High Heat Retention Storage Heaters",
|
||||
"solar_pv": "Solar PV",
|
||||
}
|
||||
|
||||
def __init__(self, property_instance: Property, materials, optimise_measures: bool = False):
|
||||
self.property_instance = property_instance
|
||||
|
||||
self.floor_recommender = FloorRecommendations(property_instance=property_instance, materials=materials)
|
||||
|
|
@ -35,14 +49,169 @@ class Mds:
|
|||
self.hotwater_recommender = HotwaterRecommendations(property_instance=property_instance)
|
||||
self.secondary_heating_recommender = SecondaryHeating(property_instance=property_instance)
|
||||
|
||||
def build(self):
|
||||
if self.property_instance.measures is None:
|
||||
raise NotImplementedError("No measures in the property - implement me")
|
||||
# This flag indicates that we wish to optimise the measures, to the property, depending on the set of measures
|
||||
# we have been provided
|
||||
self.optimise_measures = optimise_measures
|
||||
|
||||
measures = self.property_instance.measures
|
||||
def select_optimal_measure_set(self, measures):
|
||||
|
||||
measure_config_list = [list(m.keys())[0] for m in measures]
|
||||
# This is the set
|
||||
all_considered_measures = [
|
||||
'external_wall_insulation',
|
||||
'cavity_wall_insulation',
|
||||
'loft_insulation',
|
||||
'air_source_heat_pump',
|
||||
'high_heat_retention_storage_heaters',
|
||||
'solar_pv'
|
||||
]
|
||||
|
||||
# Check if our measures are within the ones we've handled
|
||||
new = [m for m in measures if m not in all_considered_measures]
|
||||
if new:
|
||||
raise NotImplementedError("New measures - handle me")
|
||||
|
||||
def prune_options(options, measures):
|
||||
options_pruned = []
|
||||
for _group in options:
|
||||
group_pruned = [m for m in _group if m in measures]
|
||||
if not group_pruned:
|
||||
continue
|
||||
options_pruned.append(group_pruned)
|
||||
|
||||
return options_pruned
|
||||
|
||||
# For options in here, a property could only possibly have one of these
|
||||
one_choice_options = [
|
||||
["external_wall_insulation", "cavity_wall_insulation", "internal_wall_insulation"],
|
||||
["loft_insulation", "flat_roof_insulation", "room_in_roof_insulation"],
|
||||
["solid_floor_insulation", "suspended_floor_insulation"],
|
||||
]
|
||||
# prune one_choice_options based on the measure set considered for this property
|
||||
one_choice_options_pruned = prune_options(one_choice_options, measures)
|
||||
|
||||
# For options in here, a property could have one or the other so all should be considered
|
||||
multi_path_options = [
|
||||
["air_source_heat_pump", "high_heat_retention_storage_heaters", "gas_boiler"]
|
||||
]
|
||||
|
||||
multi_path_options_pruned = prune_options(multi_path_options, measures)
|
||||
|
||||
one_choice_combinations = [list(itertools.product(*one_choice_options_pruned))]
|
||||
one_choice_combinations = [list(x) for sublist in one_choice_combinations for x in sublist]
|
||||
multi_path_combinations = [list(itertools.product(*multi_path_options_pruned))]
|
||||
multi_path_combinations = [list(x) for sublist in multi_path_combinations for x in sublist]
|
||||
|
||||
one_choice_flat = [item for sublist in one_choice_options_pruned for item in sublist]
|
||||
multi_path_flat = [item for sublist in multi_path_options_pruned for item in sublist]
|
||||
|
||||
remaining_measures = [
|
||||
measure for measure in measures
|
||||
if measure not in one_choice_flat and measure not in multi_path_flat
|
||||
]
|
||||
|
||||
# Combine one_choice and multi_path combinations with remaining measures
|
||||
final_combinations = []
|
||||
for one_choice in one_choice_combinations:
|
||||
for multi_path in multi_path_combinations:
|
||||
final_combinations.append([m for m in one_choice + multi_path + remaining_measures])
|
||||
|
||||
pruned_combinations = []
|
||||
# TODO: We can do these checks once, outside of the loop and prune the combinations
|
||||
for combination in final_combinations:
|
||||
pruned_measures = []
|
||||
for measure in combination:
|
||||
if measure not in measures:
|
||||
continue
|
||||
# There are certain measures where we need to
|
||||
if measure == "external_wall_insulation":
|
||||
# Check if the wall is not cavity since the other wall types can take external wall insulation
|
||||
if (
|
||||
self.wall_recommender.ewi_valid() and
|
||||
not self.property_instance.walls["insulation_thickness"] in ["average", "above average"]
|
||||
):
|
||||
pruned_measures.append(measure)
|
||||
continue
|
||||
|
||||
if measure == "cavity_wall_insulation":
|
||||
# Check if the wall is cavity
|
||||
if (
|
||||
self.property_instance.walls['is_cavity_wall'] and
|
||||
not self.property_instance.walls['is_filled_cavity']
|
||||
):
|
||||
pruned_measures.append(measure)
|
||||
continue
|
||||
|
||||
if measure == "loft_insulation":
|
||||
# Check if the roof is suitable for loft insulation and the loft isn't already done
|
||||
# Or, if the home had a u-value for the roof, we don't recommend loft insulation
|
||||
if (
|
||||
self.property_instance.roof["is_pitched"] and
|
||||
not self.roof_recommender.is_loft_already_insulated() and
|
||||
self.property_instance.roof["thermal_transmittance_unit"] is None
|
||||
):
|
||||
pruned_measures.append(measure)
|
||||
continue
|
||||
|
||||
if measure == "solid_floor_insulation":
|
||||
# Check if the floor is solid
|
||||
if (
|
||||
self.property_instance.floor["is_solid"] and
|
||||
self.property_instance.floor["insulation_thickness"] not in ["average", "above average"] and
|
||||
self.property_instance.floor["thermal_transmittance_unit"] is not None
|
||||
):
|
||||
pruned_measures.append(measure)
|
||||
continue
|
||||
|
||||
if measure == "suspended_floor_insulation":
|
||||
# Check if the floor is suspended
|
||||
if (
|
||||
self.property_instance.floor["is_suspended"] and
|
||||
self.property_instance.floor["insulation_thickness"] not in ["average", "above average"] and
|
||||
self.property_instance.floor["thermal_transmittance_unit"] is not None
|
||||
):
|
||||
pruned_measures.append(measure)
|
||||
continue
|
||||
|
||||
if measure == "high_heat_retention_storage_heaters":
|
||||
|
||||
# For the moment, we recommend storage heaters if the property doesn't already
|
||||
# and don't make it contngent on controls
|
||||
already_has_hhr = self.heating_recommender.is_hhr_already_installed()
|
||||
|
||||
if (
|
||||
self.heating_recommender.is_high_heat_retention_valid() and
|
||||
not already_has_hhr
|
||||
):
|
||||
pruned_measures.append(measure)
|
||||
continue
|
||||
|
||||
if measure == "air_source_heat_pump":
|
||||
if self.heating_recommender.is_ashp_valid():
|
||||
pruned_measures.append(measure)
|
||||
continue
|
||||
|
||||
if measure == "solar_pv":
|
||||
if self.solar_recommender.is_solar_pv_valid():
|
||||
pruned_measures.append(measure)
|
||||
continue
|
||||
|
||||
raise NotImplementedError("Implement me")
|
||||
|
||||
if not pruned_measures:
|
||||
continue
|
||||
|
||||
pruned_measures_formatted = []
|
||||
for pm in pruned_measures:
|
||||
pruned_measures_formatted.append({pm: self.format_map[pm]})
|
||||
|
||||
pruned_combinations.append(pruned_measures_formatted)
|
||||
|
||||
# We're left with the subset of measures that are possible for this property
|
||||
# These are the possible groups of measures that could be applied to this home
|
||||
|
||||
return pruned_combinations
|
||||
|
||||
def _build(self, measure_config_list, measures):
|
||||
not_implemented_measures = [
|
||||
"party_wall_insulation",
|
||||
"ground_source_heat_pump",
|
||||
|
|
@ -60,114 +229,164 @@ class Mds:
|
|||
|
||||
mds_recommendations = []
|
||||
errors = []
|
||||
phase = 0
|
||||
|
||||
# TODO: Could use a decarator to reduce the boilerplate code - insert_recommendation_id and then the append
|
||||
|
||||
if "external_wall_insulation" in measure_config_list:
|
||||
recs = self.wall_recommender.mds_recommend_ewi(phase=0)
|
||||
recs = self.wall_recommender.mds_recommend_ewi(phase=phase)
|
||||
if not recs:
|
||||
raise Exception("No recommendations for external wall insulation")
|
||||
recs = self.insert_recommendation_id(recs, measures, "external_wall_insulation")
|
||||
mds_recommendations.append(recs)
|
||||
if self.optimise_measures and len(recs):
|
||||
phase += 1
|
||||
|
||||
if "cavity_wall_insulation" in measure_config_list:
|
||||
recs = self.wall_recommender.mds_recommend_cavity_wall_insulation(phase=0)
|
||||
recs = self.wall_recommender.mds_recommend_cavity_wall_insulation(phase=phase)
|
||||
recs = self.insert_recommendation_id(recs, measures, "cavity_wall_insulation")
|
||||
mds_recommendations.append(recs)
|
||||
if self.optimise_measures and len(recs):
|
||||
phase += 1
|
||||
|
||||
if "loft_insulation" in measure_config_list:
|
||||
# Check if the roof is suitable for loft insulation
|
||||
if self.property_instance.roof['is_roof_room']:
|
||||
errors.append("Roof is a room")
|
||||
else:
|
||||
recs = self.roof_recommender.mds_loft_insulation(phase=0)
|
||||
recs = self.roof_recommender.mds_loft_insulation(phase=phase)
|
||||
if not recs:
|
||||
raise Exception("No recommendations for loft insulation")
|
||||
recs = self.insert_recommendation_id(recs, measures, "loft_insulation")
|
||||
mds_recommendations.append(recs)
|
||||
if self.optimise_measures and len(recs):
|
||||
phase += 1
|
||||
|
||||
if "internal_wall_insulation" in measure_config_list:
|
||||
raise Exception("check me out 4")
|
||||
self.wall_recommender.recommend(phase=0)
|
||||
self.wall_recommender.recommend(phase=phase)
|
||||
|
||||
if "suspended_floor_insulation" in measure_config_list:
|
||||
raise Exception("check me out 5")
|
||||
self.floor_recommender.recommend(phase=0)
|
||||
self.floor_recommender.recommend(phase=phase)
|
||||
|
||||
if "solid_floor_insulation" in measure_config_list:
|
||||
raise Exception("check me out 6")
|
||||
self.floor_recommender.recommend(phase=0)
|
||||
self.floor_recommender.recommend(phase=phase)
|
||||
|
||||
if "air_source_heat_pump" in measure_config_list:
|
||||
recs = self.heating_recommender.recommend_air_source_heat_pump(
|
||||
phase=0, has_cavity_or_loft_recommendations=False, _return=True
|
||||
phase=phase, has_cavity_or_loft_recommendations=False, _return=True
|
||||
)
|
||||
recs = self.insert_recommendation_id(recs, measures, "air_source_heat_pump")
|
||||
mds_recommendations.append(recs)
|
||||
if self.optimise_measures and len(recs):
|
||||
phase += 1
|
||||
|
||||
if "electric_storage_heaters" in measure_config_list:
|
||||
if "high_heat_retention_storage_heaters" in measure_config_list:
|
||||
recs = self.heating_recommender.recommend_hhr_storage_heaters(
|
||||
phase=0, system_change=True, heating_controls_only=False, _return=True
|
||||
phase=phase, system_change=True, heating_controls_only=False, _return=True
|
||||
)
|
||||
recs = self.insert_recommendation_id(recs, measures, "electric_storage_heaters")
|
||||
mds_recommendations.append(recs)
|
||||
if recs is None:
|
||||
logger.info(
|
||||
f"No recommendations for high heat retention storage heaters, current heating "
|
||||
f"{self.property_instance.main_heating['clean_description']}"
|
||||
)
|
||||
else:
|
||||
recs = self.insert_recommendation_id(recs, measures, "high_heat_retention_storage_heaters")
|
||||
mds_recommendations.append(recs)
|
||||
if self.optimise_measures and len(recs):
|
||||
phase += 1
|
||||
|
||||
if "low_energy_lighting" in measure_config_list:
|
||||
raise Exception("check me out 9")
|
||||
self.lighting_recommender.recommend(phase=0)
|
||||
self.lighting_recommender.recommend(phase=phase)
|
||||
|
||||
if "cylinder_insulation" in measure_config_list:
|
||||
raise Exception("check me out 10")
|
||||
self.hotwater_recommender.recommend(phase=0)
|
||||
self.hotwater_recommender.recommend(phase=phase)
|
||||
|
||||
if "smart_controls" in measure_config_list:
|
||||
raise Exception("check me out 11")
|
||||
self.heating_recommender.recommend(phase=0)
|
||||
self.heating_recommender.recommend(phase=phase)
|
||||
|
||||
if "zone_controls" in measure_config_list:
|
||||
raise Exception("check me out 12")
|
||||
self.heating_recommender.recommend(phase=0)
|
||||
self.heating_recommender.recommend(phase=phase)
|
||||
|
||||
if "trvs" in measure_config_list:
|
||||
raise Exception("check me out 13")
|
||||
self.heating_recommender.recommend(phase=0)
|
||||
self.heating_recommender.recommend(phase=phase)
|
||||
|
||||
if "solar_pv" in measure_config_list:
|
||||
recs = self.solar_recommender.mds_recommend(phase=0, solar_pv_percentage=0.5)
|
||||
recs = self.solar_recommender.mds_recommend(phase=phase, solar_pv_percentage=0.5)
|
||||
recs = self.insert_recommendation_id(recs, measures, "solar_pv")
|
||||
mds_recommendations.append(recs)
|
||||
if self.optimise_measures and len(recs):
|
||||
phase += 1
|
||||
|
||||
if "double_glazing" in measure_config_list:
|
||||
raise Exception("check me out 15")
|
||||
self.windows_recommender.recommend(phase=0)
|
||||
self.windows_recommender.recommend(phase=phase)
|
||||
|
||||
if "mechanical_ventilation" in measure_config_list:
|
||||
raise Exception("check me out 16")
|
||||
self.ventilation_recomender.recommend(phase=0)
|
||||
self.ventilation_recomender.recommend(phase=phase)
|
||||
|
||||
if "gas_boiler" in measure_config_list:
|
||||
raise Exception("check me out 17")
|
||||
self.heating_recommender.recommend(phase=0)
|
||||
self.heating_recommender.recommend(phase=phase)
|
||||
|
||||
if "flat_roof_insulation" in measure_config_list:
|
||||
raise Exception("check me out 18")
|
||||
self.roof_recommender.recommend(phase=0)
|
||||
self.roof_recommender.recommend(phase=phase)
|
||||
|
||||
if "room_in_roof_insulation" in measure_config_list:
|
||||
raise Exception("check me out 19")
|
||||
self.roof_recommender.recommend(phase=0)
|
||||
self.roof_recommender.recommend(phase=phase)
|
||||
|
||||
property_representative_recommendations = Recommendations.create_representative_recommendations(
|
||||
mds_recommendations, non_invasive_recommendations=[]
|
||||
)
|
||||
|
||||
return property_representative_recommendations, errors
|
||||
return mds_recommendations, property_representative_recommendations, errors
|
||||
|
||||
def build(self):
|
||||
if self.property_instance.measures is None:
|
||||
raise NotImplementedError("No measures in the property - implement me")
|
||||
|
||||
if self.optimise_measures:
|
||||
measures_set = self.select_optimal_measure_set(self.property_instance.measures)
|
||||
mds_recommendations_map = {}
|
||||
representative_recommendations_map = {}
|
||||
errors_map = {}
|
||||
for measures in measures_set:
|
||||
measure_config_list = [list(x.keys())[0] for x in measures]
|
||||
mds_recommendations, rep_recommendations, errors = self._build(
|
||||
measure_config_list=measure_config_list,
|
||||
measures=measures
|
||||
)
|
||||
if errors:
|
||||
logger.info(f"Errors: {errors}")
|
||||
|
||||
mds_recommendations_map[str(measure_config_list)] = mds_recommendations
|
||||
representative_recommendations_map[str(measure_config_list)] = rep_recommendations
|
||||
errors_map[str(measure_config_list)] = errors
|
||||
|
||||
return mds_recommendations_map, representative_recommendations_map, errors_map
|
||||
|
||||
else:
|
||||
measure_config_list = [list(m.keys())[0] for m in self.property_instance.measures]
|
||||
return self._build(measure_config_list=measure_config_list, measures=self.property_instance.measures)
|
||||
|
||||
@staticmethod
|
||||
def insert_recommendation_id(recommendations, measures, measure_name):
|
||||
# Insert the recommendation identifier into this recommendation
|
||||
measure_config = [m for m in measures if measure_name in m][0]
|
||||
|
||||
idx = 0
|
||||
for r in recommendations:
|
||||
r["recommendation_id"] = list(measure_config.values())[0]
|
||||
r["recommendation_id"] = list(measure_config.values())[0] + "-" + str(idx)
|
||||
idx += 1
|
||||
|
||||
return recommendations
|
||||
|
|
|
|||
|
|
@ -227,7 +227,7 @@ class Recommendations:
|
|||
|
||||
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"]):
|
||||
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:
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -54,6 +54,13 @@ class RoofRecommendations:
|
|||
]
|
||||
]
|
||||
|
||||
# Extract the insulation thickness from the roof, which is used throughout this method
|
||||
self.insulation_thickness = convert_thickness_to_numeric(
|
||||
self.property.roof["insulation_thickness"],
|
||||
self.property.roof["is_pitched"],
|
||||
self.property.roof["is_flat"]
|
||||
)
|
||||
|
||||
def mds_loft_insulation(self, phase):
|
||||
"""
|
||||
For usages within the mds report
|
||||
|
|
@ -62,18 +69,18 @@ class RoofRecommendations:
|
|||
"""
|
||||
self.recommendations = []
|
||||
|
||||
insulation_thickness = convert_thickness_to_numeric(
|
||||
self.property.roof["insulation_thickness"],
|
||||
self.property.roof["is_pitched"],
|
||||
self.property.roof["is_flat"]
|
||||
)
|
||||
|
||||
u_value = get_roof_u_value(**{**self.property.roof, "age_band": self.property.age_band})
|
||||
|
||||
self.recommend_roof_insulation(u_value, insulation_thickness, self.property.roof, phase)
|
||||
self.recommend_roof_insulation(u_value, self.insulation_thickness, self.property.roof, phase)
|
||||
|
||||
return self.recommendations
|
||||
|
||||
def is_loft_already_insulated(self):
|
||||
"""
|
||||
Check if the loft is already insulated
|
||||
"""
|
||||
return (self.insulation_thickness > self.MINIMUM_LOFT_ISULATION_MM) and self.property.roof["is_pitched"]
|
||||
|
||||
def recommend(self, phase):
|
||||
|
||||
if self.property.roof["has_dwelling_above"]:
|
||||
|
|
@ -81,21 +88,15 @@ class RoofRecommendations:
|
|||
|
||||
u_value = self.property.roof["thermal_transmittance"]
|
||||
|
||||
insulation_thickness = convert_thickness_to_numeric(
|
||||
self.property.roof["insulation_thickness"],
|
||||
self.property.roof["is_pitched"],
|
||||
self.property.roof["is_flat"]
|
||||
)
|
||||
|
||||
# We check if the roof is already insulated and if so, we exit
|
||||
|
||||
# Building regulations part L recommend installing at least 270mm of insulation, however generally we
|
||||
# experience diminishing returns in terms of SAP once we go beyond around 150mm of insulation
|
||||
# This only holds true for pitched roofs.
|
||||
if (insulation_thickness > self.MINIMUM_LOFT_ISULATION_MM) and self.property.roof["is_pitched"]:
|
||||
if self.is_loft_already_insulated():
|
||||
return
|
||||
|
||||
if (insulation_thickness >= self.MINIMUM_FLAT_ROOF_ISULATION_MM) and self.property.roof["is_flat"]:
|
||||
if (self.insulation_thickness >= self.MINIMUM_FLAT_ROOF_ISULATION_MM) and self.property.roof["is_flat"]:
|
||||
return
|
||||
|
||||
if self.property.roof["is_roof_room"]:
|
||||
|
|
@ -119,7 +120,7 @@ class RoofRecommendations:
|
|||
return
|
||||
|
||||
if self.property.roof["is_pitched"] or self.property.roof["is_flat"]:
|
||||
self.recommend_roof_insulation(u_value, insulation_thickness, self.property.roof, phase)
|
||||
self.recommend_roof_insulation(u_value, self.insulation_thickness, self.property.roof, phase)
|
||||
return
|
||||
|
||||
if self.property.roof["is_roof_room"]:
|
||||
|
|
|
|||
|
|
@ -4,10 +4,13 @@ from recommendations.recommendation_utils import override_costs
|
|||
|
||||
|
||||
class SolarPvRecommendations:
|
||||
# Solar panel specs based on Eurener 400s solar panels
|
||||
# https://midsummerwholesale.co.uk/buy/eurener/eurener-400w-mepv-zebra-ab-half-cut-mono
|
||||
# Approximate area of the solar panels
|
||||
SOLAR_PANEL_AREA = 1.6
|
||||
SOLAR_PANEL_AREA = 1.79
|
||||
# Wattage per panel - this is based on the average wattage of a solar panel being between 250w and 420w
|
||||
SOLAR_PANEL_WATTAGE = 250
|
||||
# This was previously set to 250w, but has been upped to 400 based on the systems used by Cotswolrd Energy Group
|
||||
SOLAR_PANEL_WATTAGE = 400
|
||||
|
||||
MAX_SYSTEM_WATTAGE = 6000
|
||||
MIN_SYSTEM_WATTAGE = 1000
|
||||
|
|
@ -75,15 +78,7 @@ class SolarPvRecommendations:
|
|||
}
|
||||
]
|
||||
|
||||
def recommend(self, phase):
|
||||
"""
|
||||
We check if a property is potentially suitable for solar PV based on the following criteria:
|
||||
- The property is a house or bungalow
|
||||
- The property has a flat or pitched roof
|
||||
- The property does not have existing solar pv
|
||||
:return:
|
||||
"""
|
||||
|
||||
def is_solar_pv_valid(self):
|
||||
is_valid_property_type = self.property.data["property-type"] in ["House", "Bungalow", "Maisonette"]
|
||||
is_valid_roof_type = (
|
||||
self.property.roof["is_flat"] or self.property.roof["is_pitched"] or self.property.roof["is_roof_room"]
|
||||
|
|
@ -93,7 +88,18 @@ class SolarPvRecommendations:
|
|||
None, 0, self.property.DATA_ANOMALY_MATCHES
|
||||
]
|
||||
|
||||
if not is_valid_property_type or not is_valid_roof_type or not has_no_existing_solar_pv:
|
||||
return is_valid_property_type and is_valid_roof_type and has_no_existing_solar_pv
|
||||
|
||||
def recommend(self, phase):
|
||||
"""
|
||||
We check if a property is potentially suitable for solar PV based on the following criteria:
|
||||
- The property is a house or bungalow
|
||||
- The property has a flat or pitched roof
|
||||
- The property does not have existing solar pv
|
||||
:return:
|
||||
"""
|
||||
|
||||
if not self.is_solar_pv_valid():
|
||||
return
|
||||
|
||||
solar_pv_percentage = self.property.solar_pv_percentage
|
||||
|
|
|
|||
|
|
@ -55,23 +55,26 @@ class WallRecommendations(Definitions):
|
|||
NEW_BUILD_INSULATED = 0.75
|
||||
|
||||
# These are the ending descriptions we consider for walls with external insulation
|
||||
# This maps the clean descriptions to the ending descriptions
|
||||
EXTERNALLY_INSULATED_WALL_DESCRIPTIONS = {
|
||||
"solid_brick": "Solid brick, with external insulation",
|
||||
"cob": "Cob, with external insulation",
|
||||
"system_built": "System built, with external insulation",
|
||||
"granite_or_whinstone": 'Granite or whinstone, with external insulation',
|
||||
"sandstone_or_limestone": 'Sandstone or limestone, with external insulation',
|
||||
"timber_frame": "Timber frame, with external insulation"
|
||||
"Cavity wall, as built, insulated": "Cavity wall, filled cavity and external insulation",
|
||||
"Solid brick, as built, no insulation": "Solid brick, with external insulation",
|
||||
"Solid brick, as built, insulated": "Solid brick, with external insulation",
|
||||
"Cob, as built": "Cob, with external insulation",
|
||||
"System built, as built, no insulation": "System built, with external insulation",
|
||||
"Granite or whinstone, as built, no insulation": 'Granite or whinstone, with external insulation',
|
||||
"Timber frame, as built, no insulation": "Timber frame, with external insulation",
|
||||
}
|
||||
|
||||
# These are the ending descriptions we consider for walls with internal insulation
|
||||
INTERNALLY_INSULATED_WALL_DESCRIPTIONS = {
|
||||
"solid_brick": "Solid brick, with internal insulation",
|
||||
"cob": "Cob, with internal insulation",
|
||||
"system_built": "System built, with internal insulation",
|
||||
"granite_or_whinstone": 'Granite or whinstone, with internal insulation',
|
||||
"sandstone_or_limestone": 'Sandstone or limestone, with internal insulation',
|
||||
"timber_frame": "Timber frame, with internal insulation"
|
||||
"Cavity wall, as built, insulated": "Cavity wall, filled cavity and internal insulation",
|
||||
"Solid brick, as built, no insulation": "Solid brick, with internal insulation",
|
||||
"Solid brick, as built, insulated": "Solid brick, with internal insulation",
|
||||
"Cob, as built": "Cob, with internal insulation",
|
||||
"System built, as built, no insulation": "System built, with internal insulation",
|
||||
"Granite or whinstone, as built, no insulation": 'Granite or whinstone, with internal insulation',
|
||||
"Timber frame, as built, no insulation": "Timber frame, with internal insulation",
|
||||
}
|
||||
|
||||
def __init__(
|
||||
|
|
@ -99,7 +102,7 @@ class WallRecommendations(Definitions):
|
|||
part
|
||||
for part in materials
|
||||
if part["type"]
|
||||
in ["iwi_wall_demolition", "iwi_vapour_barrier", "iwi_redecoration"]
|
||||
in ["iwi_wall_demolition", "iwi_vapour_barrier", "iwi_redecoration"]
|
||||
]
|
||||
|
||||
self.external_wall_insulation_materials = [
|
||||
|
|
@ -109,11 +112,9 @@ class WallRecommendations(Definitions):
|
|||
self.external_wall_non_insulation_materials = [
|
||||
part
|
||||
for part in materials
|
||||
if part["type"]
|
||||
in ["ewi_wall_demolition", "ewi_wall_preparation", "ewi_wall_redecoration"]
|
||||
if part["type"] in ["ewi_wall_demolition", "ewi_wall_preparation", "ewi_wall_redecoration"]
|
||||
]
|
||||
|
||||
@property
|
||||
def ewi_valid(self):
|
||||
"""
|
||||
This method check available data, to determine if a property is suitable for external wall insulation
|
||||
|
|
@ -123,11 +124,24 @@ class WallRecommendations(Definitions):
|
|||
# it is not suitable for EWI
|
||||
if self.property.restricted_measures or (
|
||||
self.property.data["property-type"].lower() == "flat"
|
||||
) or (
|
||||
self.property.walls['is_cob'] or
|
||||
self.property.walls['is_sandstone_or_limestone'] or
|
||||
self.property.walls["is_cavity_wall"]
|
||||
):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def is_suitable_for_solid_insulation(self):
|
||||
"""
|
||||
Checks if the wall is of a suitable type for internal/external wall insulation
|
||||
"""
|
||||
if self.property.walls["is_cavity_wall"] or self.property.walls["is_cob"]:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def mds_recommend_cavity_wall_insulation(self, phase=None):
|
||||
# Function specifically for cavity wall insulation, for usage in the mds report
|
||||
self.recommendations = []
|
||||
|
|
@ -175,7 +189,7 @@ class WallRecommendations(Definitions):
|
|||
# recommend internal wall insulation as a possible measure
|
||||
|
||||
u_value = self.property.walls["thermal_transmittance"]
|
||||
u_value = None if math.isnan(u_value) else u_value
|
||||
u_value = None if pd.isnull(u_value) else u_value
|
||||
|
||||
is_cavity_wall = self.property.walls["is_cavity_wall"]
|
||||
insulation_thickness = self.property.walls["insulation_thickness"]
|
||||
|
|
@ -246,7 +260,7 @@ class WallRecommendations(Definitions):
|
|||
return
|
||||
|
||||
# Remaining wall types are treated with IWI or EWI
|
||||
if u_value >= self.BUILDING_REGULATIONS_PART_L_MAX_U_VALUE:
|
||||
if (u_value >= self.BUILDING_REGULATIONS_PART_L_MAX_U_VALUE) and self.is_suitable_for_solid_insulation():
|
||||
self.find_insulation(u_value, phase)
|
||||
return
|
||||
|
||||
|
|
@ -332,18 +346,19 @@ class WallRecommendations(Definitions):
|
|||
|
||||
wall_ending_config = WallAttributes("Cavity wall, filled cavity").process()
|
||||
|
||||
simulation_config = {}
|
||||
if self.property.data["walls-energy-eff"] not in ["Good", "Very Good"]:
|
||||
simulation_config = {
|
||||
"walls_energy_eff_ending": "Good",
|
||||
"walls_thermal_transmittance_ending": new_u_value
|
||||
}
|
||||
|
||||
walls_simulation_config = check_simulation_difference(
|
||||
new_config=wall_ending_config, old_config=self.property.walls, prefix="walls_"
|
||||
)
|
||||
|
||||
simulation_config = {**simulation_config, **walls_simulation_config}
|
||||
simulation_config = self.set_starting_simulation_config(
|
||||
wall_ending_config=wall_ending_config
|
||||
)
|
||||
|
||||
simulation_config = {
|
||||
**simulation_config,
|
||||
**walls_simulation_config,
|
||||
"walls_thermal_transmittance_ending": new_u_value
|
||||
}
|
||||
|
||||
recommendations.append(
|
||||
{
|
||||
|
|
@ -370,30 +385,35 @@ class WallRecommendations(Definitions):
|
|||
self.recommendations = recommendations
|
||||
|
||||
def get_internal_external_wall_description(self, description_map, new_u_value):
|
||||
if self.property.walls["is_solid_brick"]:
|
||||
return description_map["solid_brick"]
|
||||
|
||||
if self.property.walls["is_cob"]:
|
||||
return description_map["cob"]
|
||||
|
||||
if self.property.walls["is_system_built"]:
|
||||
return description_map["system_built"]
|
||||
|
||||
if self.property.walls["is_granite_or_whinstone"]:
|
||||
return description_map["granite_or_whinstone"]
|
||||
|
||||
if self.property.walls["is_sandstone_or_limestone"]:
|
||||
return description_map["sandstone_or_limestone"]
|
||||
|
||||
if self.property.walls["is_timber_frame"]:
|
||||
return description_map["timber_frame"]
|
||||
|
||||
if "Average thermal transmittance" in self.property.walls["clean_description"]:
|
||||
if new_u_value is None:
|
||||
raise ValueError("New u value is None")
|
||||
return f'Average thermal transmittance {new_u_value} W/m-¦K'
|
||||
|
||||
raise NotImplementedError("Not implemented yet")
|
||||
return description_map[self.property.walls["clean_description"]]
|
||||
|
||||
def set_starting_simulation_config(self, wall_ending_config):
|
||||
"""
|
||||
Helper function to set the starting simulation config
|
||||
"""
|
||||
|
||||
simulation_config = {}
|
||||
if self.property.data["walls-energy-eff"] not in ["Good", "Very Good"]:
|
||||
simulation_config = {
|
||||
"walls_energy_eff_ending": "Good"
|
||||
}
|
||||
|
||||
# We check if we have double insulation in any instances
|
||||
double_insulation = (
|
||||
(wall_ending_config["is_filled_cavity"] and wall_ending_config["external_insulation"]) or
|
||||
(wall_ending_config["is_filled_cavity"] and wall_ending_config["internal_insulation"]) or
|
||||
(wall_ending_config["external_insulation"] and wall_ending_config["internal_insulation"])
|
||||
)
|
||||
if double_insulation:
|
||||
simulation_config["walls_energy_eff_ending"] = "Very Good"
|
||||
|
||||
return simulation_config
|
||||
|
||||
def _find_insulation(self, u_value, insulation_materials, non_insulation_materials, phase):
|
||||
|
||||
|
|
@ -468,16 +488,14 @@ class WallRecommendations(Definitions):
|
|||
|
||||
wall_ending_config = WallAttributes(new_description).process()
|
||||
|
||||
simulation_config = {}
|
||||
if self.property.data["walls-energy-eff"] not in ["Good", "Very Good"]:
|
||||
simulation_config = {
|
||||
"walls_energy_eff_ending": "Good"
|
||||
}
|
||||
|
||||
walls_simulation_config = check_simulation_difference(
|
||||
new_config=wall_ending_config, old_config=self.property.walls, prefix="walls_"
|
||||
)
|
||||
|
||||
simulation_config = self.set_starting_simulation_config(
|
||||
wall_ending_config=wall_ending_config
|
||||
)
|
||||
|
||||
simulation_config = {
|
||||
**walls_simulation_config,
|
||||
**simulation_config,
|
||||
|
|
@ -521,7 +539,7 @@ class WallRecommendations(Definitions):
|
|||
# consider diminishing returns between the two as they are considered to be separate measures
|
||||
|
||||
ewi_recommendations = []
|
||||
if self.ewi_valid:
|
||||
if self.ewi_valid():
|
||||
ewi_recommendations = self._find_insulation(
|
||||
u_value=u_value,
|
||||
insulation_materials=pd.DataFrame(
|
||||
|
|
|
|||
|
|
@ -756,17 +756,23 @@ def calculate_cavity_age(newest_epc, older_epcs, cleaned):
|
|||
return cavity_age
|
||||
|
||||
|
||||
def check_simulation_difference(old_config, new_config, prefix=""):
|
||||
def check_simulation_difference(old_config, new_config, prefix="", keys_with_prefix=None):
|
||||
"""
|
||||
Given two dictionaries, that describe the heating control configurations, this method will compare the two
|
||||
and pick out the differences. These differences will be things that have been added and things that have been
|
||||
removed. This will be used to determine how we should be updating the configuration in the simulation
|
||||
:return:
|
||||
"""
|
||||
|
||||
keys_with_prefix = (
|
||||
["is_assumed", "thermal_transmittance", "insulation_thickness"] if keys_with_prefix is None
|
||||
else keys_with_prefix
|
||||
)
|
||||
|
||||
differences = {}
|
||||
for key in new_config:
|
||||
if old_config[key] != new_config[key]:
|
||||
new_key = prefix + key + "_ending" if key in ["is_assumed", "thermal_transmittance"] else key + "_ending"
|
||||
new_key = prefix + key + "_ending" if key in keys_with_prefix else key + "_ending"
|
||||
differences[new_key] = new_config[key]
|
||||
|
||||
return differences
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue