Parity comparison investiagtion, stonewater wip

This commit is contained in:
Khalim Conn-Kowlessar 2024-06-11 18:23:19 +01:00
parent 09a3d01e90
commit 743422e8fe
12 changed files with 666 additions and 63 deletions

View file

@ -162,6 +162,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)
@ -585,6 +588,7 @@ class Property:
floor_area_decile_thresholds=floor_area_decile_thresholds,
)
self.set_energy_source()
self.find_energy_sources()
def set_spatial(self, spatial: pd.DataFrame):
"""
@ -993,3 +997,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")

View file

@ -434,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
@ -453,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:
"""
@ -483,6 +485,13 @@ class SearchEpc:
if not epc_data.empty:
# Further processing of the EPC data
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))
]
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(
@ -583,7 +592,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
@ -591,9 +601,6 @@ class SearchEpc:
# If we still have missing dates, we set it to the mean of the non NA dates
epc_data["lodgement-datetime"] = epc_data["lodgement-datetime"].fillna(epc_data["lodgement-datetime"].mean())
if exclude_old:
epc_data = epc_data[epc_data["lodgement-datetime"] > pd.Timestamp.now() - pd.DateOffset(years=10)]
# For each attribute, we need to determine the datatype and use an appropriate method
# to estimate.
estimated_epc = {}

View file

@ -1,10 +1,15 @@
import pandas as pd
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
from utils.s3 import read_dataframe_from_s3_parquet, read_from_s3
import os
import requests
import msgpack
from functools import lru_cache
import time
load_dotenv(dotenv_path="backend/.env")
EPC_AUTH_TOKEN = os.getenv("EPC_AUTH_TOKEN")
@ -13,6 +18,8 @@ EPC_AUTH_TOKEN = os.getenv("EPC_AUTH_TOKEN")
uprn = 100040099104
# This is for 353A, Hermitage Lane, ME16 9NT (one of the e.on properties)
uprn = 200000964454
# This is for 14 Victoria Road, Cross Hills, KEIGHLEY, North Yorkshire, ENGLAND, BD20 8SY
uprn = 100050346517
cleaning_data = read_dataframe_from_s3_parquet(
bucket_name="retrofit-data-dev", file_key="sap_change_model/cleaning_dataset.parquet",
@ -49,6 +56,25 @@ p = Property(
p.get_spatial_data(uprn_filenames)
cleaned = read_from_s3(
s3_file_name="cleaned_epc_data/cleaned.bson",
bucket_name="retrofit-data-dev"
)
cleaned = msgpack.unpackb(cleaned, raw=False)
from etl.solar.SolarPhotoSupply import SolarPhotoSupply
photo_supply_lookup, floor_area_decile_thresholds = SolarPhotoSupply.load(bucket="retrofit-data-dev")
p.get_components(
cleaned=cleaned,
photo_supply_lookup=photo_supply_lookup,
floor_area_decile_thresholds=floor_area_decile_thresholds
)
p.hot_water_energy_source
p.heating_energy_source
longitude = p.spatial["longitude"]
latitude = p.spatial["latitude"]
@ -73,14 +99,29 @@ from pprint import pprint
pprint(solar_potential)
# This is the maximum number of panels that can be installed
solar_potential["maxArrayPanelsCount"]
# 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"]
solar_potential["wholeRoofStats"]
# This is the maximum area that can be covered by the panels
solar_potential["maxArrayAreaMeters2"]
# This is the area of the roof
solar_potential["wholeRoofStats"]["areaMeters2"]
# This is the area of the floor
solar_potential["wholeRoofStats"]["groundAreaMeters2"]
solar_potential["solarPanelConfigs"][0]
solar_potential["solarPanelConfigs"][1]
# Copy of response for testing - 6 Laura Close, Tintagel, PL34 0EB
# {'name': 'buildings/ChIJ2yC6t4KEa0gRh2TIssogI7k', 'center': {'latitude': 50.667375, 'longitude': -4.7416833},
@ -334,3 +375,169 @@ solar_potential["wholeRoofStats"]
# '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 = GoogleSolarApi(api_key=api_key)
import numpy as np
from recommendations.Costs import MCS_SOLAR_PV_COST_DATA
class GoogleSolarApi:
NORTH_FACING_AZIMUTH_RANGE = (-30, 30)
def __init__(self, api_key, max_retries=5):
"""
Initialize the GoogleSolarApi class with the provided API key and maximum retries.
: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"
self.insights_data = None
self.roof_segments = []
# property attributes:
self.floor_area = None
self.roof_area = None
self.roof_segment_indexes = None
self.panel_area = None
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.
: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
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
}
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
@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.
: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.
"""
# TODO - can we make a request which includes the 30cm buffer from the edge of the roof?
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"]
)
# 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()
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_energy = segment["yearlyEnergyDcKwh"]
ratio = generated_energy / wattage
cost = MCS_SOLAR_PV_COST_DATA["average_cost_per_kwh"] * (generated_energy / 1000)
roi_summary.append(
{
"segmentIndex": segment["segmentIndex"],
"wattage": wattage,
"generatedEnergy": generated_energy,
"ratio": ratio,
"n_panels": segment["panelsCount"],
"cost": cost
}
)
roi_summary = pd.DataFrame(roi_summary)
weighted_ratio = np.average(
roi_summary["ratio"].values, weights=roi_summary["generatedEnergy"].values
)
total_cost = roi_summary["cost"].sum()
total_energy = roi_summary["generatedEnergy"].sum()
panel_performance.append(
{
"n_panels": roi_summary["n_panels"].sum(),
"total_energy": total_energy,
"total_cost": total_cost,
"weighted_ratio": weighted_ratio
}
)
panel_performance = pd.DataFrame(panel_performance)
panel_performance = panel_performance.sort_values("weighted_ratio", ascending=False)
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

View file

@ -1206,41 +1206,3 @@ def check_mds(results, input_properties, recommendations, optimise_measures):
hhr_check = pd.DataFrame(hhr_check)
return walls_check, hhr_check
from utils.s3 import read_dataframe_from_s3_parquet
z = read_dataframe_from_s3_parquet(
bucket_name="retrofit-data-dev",
file_key="sap_change_model/2024-05-28-19-08-25/dataset_rooms.parquet"
)
k = z[z["heat_demand_ending"] != z["heat_demand_starting"]]
k = k[k["walls_thermal_transmittance"] == k["walls_thermal_transmittance_ending"]]
k = k[k["roof_thermal_transmittance"] == k["roof_thermal_transmittance_ending"]]
k = k[k["floor_thermal_transmittance"] == k["floor_thermal_transmittance_ending"]]
ending_cols = [c for c in k.columns if "_ending" in c]
eg = k.head(2).tail(1).squeeze()
diff = []
for c in ending_cols:
split = c.split("_ending")[0]
if split + "_starting" in k.columns:
starting_col = split + "_starting"
else:
starting_col = split
b4 = eg[starting_col]
after = eg[c]
if b4 != after:
diff.append(
{
"measure": split,
"starting": b4,
"ending": after
}
)
diff = pd.DataFrame(diff)
eg["heat_demand_starting"]
eg["heat_demand_ending"]
eg["uprn"]

View file

@ -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

View file

@ -527,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)

View 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)

View 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

View file

@ -593,3 +593,74 @@ def app():
# "City/Town": "city_town",
# "County": "county",
# "Address ID": "external_address_id",
def compile_data():
"""
Various data sources have been produced to create the final data source for Stonewater.
This function combines them
:return:
"""
########################################################################
# Read in data
########################################################################
asset_list = read_excel_from_s3(
file_key="customers/Stonewater/Stonewater SHDF_3_0_Board Triage 22.05.24.xlsx",
bucket_name="retrofit-data-dev",
header_row=4
)
# TODO: Read in UPRNs
########################################################################
# Prepare asset list
########################################################################
# TODO: Merge on UPRNs
# Drop the bottom 4 rows, which are completely missing
asset_list = asset_list.head(-4)
# Keep just the columns we're interested in
asset_list = asset_list[
[
"Osm. ID",
"Org. ref.",
"Postcode",
"House no",
"Name",
"Address line 2",
"City/Town",
"County",
"Address ID", # This is not uprn
]
].rename(
columns={
"Osm. ID": "internal_id",
"Org. ref.": "customer_asset_id",
"Postcode": "postcode",
"House no": "house_number",
"Name": "address1",
"Address line 2": "address2",
"City/Town": "city_town",
"County": "county",
"Address ID": "external_address_id",
}
)
# Create full address
asset_list["full_address"] = np.where(
~pd.isnull(asset_list["address2"]),
(
asset_list["address1"] + ", " +
asset_list["address2"] + ", " +
asset_list["city_town"].str.title() + ", " +
# asset_list["county"] + ", " +
asset_list["postcode"]
),
asset_list["address1"] + ", " +
asset_list["city_town"].str.title() + ", " +
# asset_list["county"] + ", " +
asset_list["postcode"]
)
if pd.isnull(asset_list["full_address"]).sum():
raise ValueError("Missing full addresses")

View file

@ -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,

View file

@ -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

View file

@ -189,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"]