Merge pull request #234 from Hestia-Homes/main

Restructuring rdsap data, deploying due considerations buckets
This commit is contained in:
KhalimCK 2023-09-24 17:51:12 +01:00 committed by GitHub
commit c9113f8250
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 2116 additions and 463 deletions

View file

@ -3,6 +3,7 @@ import re
from epc_api.client import EpcClient
from model_data.config import EPC_AUTH_TOKEN
from model_data.BaseUtility import Definitions
from recommendations.rdsap_tables import england_wales_age_band_lookup
class Property(Definitions):
@ -29,6 +30,7 @@ class Property(Definitions):
lighting = None
coordinates = None
age_band = None
def __init__(self, id, postcode, address1, epc_client=None, data=None):
self.id = id
@ -245,6 +247,7 @@ class Property(Definitions):
self.set_floor_height()
self.set_wall_area()
self.set_floor_area()
self.set_age_band()
for description, attribute in cleaned.items():
@ -263,6 +266,17 @@ class Property(Definitions):
raise ValueError("Either No attributes or multiple found for %s" % description)
setattr(self, self.ATTRIBUTE_MAP[description], attributes[0])
def set_age_band(self):
"""
Sets a cleaned version of the age band of the property given the EPC data
:return:
"""
if not self.data:
raise ValueError("Property does not contain data")
self.age_band = england_wales_age_band_lookup[self.data["construction-age-band"]]
def set_is_in_conservation_area(self, in_conservation_area):
"""
Sets whether the property is in a conservation area given the output of the ConservationAreaClient

View file

@ -30,7 +30,8 @@ mock_epc_response = {
"unheated-corridor-length": 0,
"mains-gas-flag": "Y",
"floor-height": 2.5,
"total-floor-area": 100
"total-floor-area": 100,
"construction-age-band": "England and Wales: 1967-1975"
},
{
"inspection-date": "2023-05-01",
@ -53,7 +54,8 @@ mock_epc_response = {
"unheated-corridor-length": 0,
"mains-gas-flag": "Y",
"floor-height": 2.5,
"total-floor-area": 100
"total-floor-area": 100,
"construction-age-band": "England and Wales: 1967-1975"
}
]
}
@ -77,7 +79,8 @@ mock_epc_response_dupe = {
"unheated-corridor-length": 0,
"mains-gas-flag": "Y",
"floor-height": 2.5,
"total-floor-area": 100
"total-floor-area": 100,
"construction-age-band": "England and Wales: 1967-1975"
},
{
'inspection-date': '2023-05-01', 'some-other-key': 'some-other-value',
@ -97,7 +100,8 @@ mock_epc_response_dupe = {
"unheated-corridor-length": 0,
"mains-gas-flag": "Y",
"floor-height": 2.5,
"total-floor-area": 100
"total-floor-area": 100,
"construction-age-band": "England and Wales: 1967-1975"
},
{
'inspection-date': '2023-06-01', 'some-other-key': 'duplicate-date',
@ -117,7 +121,8 @@ mock_epc_response_dupe = {
"unheated-corridor-length": 0,
"mains-gas-flag": "Y",
"floor-height": 2.5,
"total-floor-area": 100
"total-floor-area": 100,
"construction-age-band": "England and Wales: 1967-1975"
}
]
}
@ -126,11 +131,13 @@ mock_epc_response_dupe = {
class TestProperty:
@pytest.fixture(autouse=True)
def property_instance(self, mock_epc_client, mock_open_uprn_client, mock_cleaner):
return Property(1, "AB12CD", "Test Address", epc_client=mock_epc_client)
property_instance = Property(1, "AB12CD", "Test Address", epc_client=mock_epc_client)
return property_instance
@pytest.fixture(autouse=True)
def property_instance_dupe_data(self, mock_epc_client_dupe_data):
return Property(2, "AB12CD", "Test Address", epc_client=mock_epc_client_dupe_data)
property_instance_dupe_data = Property(2, "AB12CD", "Test Address", epc_client=mock_epc_client_dupe_data)
return property_instance_dupe_data
@pytest.fixture
def mock_epc_client(self):

View file

@ -4,7 +4,6 @@ import pandas as pd
import msgpack
from model_data.EpcClean import EpcClean
from model_data.analysis.UvalueEstimations import UvalueEstimations
from model_data.simulation_system.core.Settings import EARLIEST_EPC_DATE
from pathlib import Path
from utils.s3 import save_data_to_s3

View file

@ -65,7 +65,7 @@ class FloorAttributes(Definitions):
uvalue = uvalue_match.group(1)
else:
uvalue = uvalue_match2.group(1)
self.description = f"average thermal transmittance {uvalue} w/m-¦K"
self.description = f'average thermal transmittance {uvalue} w/m-¦k'
else:
translation = self.WELSH_TEXT.get(self.description)

View file

@ -24,15 +24,16 @@ class LightingAttributes:
expression and perform the translation
"""
lel_match = re.search(r"goleuadau ynni-isel mewn (\d+)%? ogçör mannau gosod", self.description)
lel_match2 = re.search(r"goleuadau ynni-isel mewn (\d+)%? o'r mannau gosod", self.description)
if lel_match:
if lel_match is not None or lel_match2 is not None:
# Perform the actual translation
percentage = lel_match.group(1)
percentage = lel_match.group(1) if lel_match is not None else lel_match2.group(1)
self.description = f"low energy lighting in {percentage}% of fixed outlets"
else:
translation = self.WELSH_TEXT.get(self.description)
if translation:
self.nodata = False
self.description = translation
def process(self):

View file

@ -54,7 +54,8 @@ class MainHeatAttributes(Definitions):
"gwresogyddion ystafell, pelenni coed": "room heaters, wood pellets",
"gwresogyddion ystafell, glo": "room heaters, coal",
"bwyler a gwres dan y llawr, lpg": "boiler and underfloor heating, lpg",
"bwyler a gwres dan y llawr, trydan": "boiler and underfloor heating, electric"
"bwyler a gwres dan y llawr, trydan": "boiler and underfloor heating, electric",
"boiler and radiators, nwy prif gyflenwad, mains gas": "boiler and radiators, mains gas",
}
REMAP = {

View file

@ -102,9 +102,16 @@ class MainheatControlAttributes(Definitions):
"dim rheolaeth thermostatig ar dymheredd yr ystafell": "no thermostatic control of room temperature",
"thermostatau ar y cyfarpar": "appliance thermostats",
"rhaglennydd a thermostatau ystafell": "programmer and room thermostats",
"system dalu wedigçöi chysylltu +ó defnyddio gwres cymunedol, rhaglennydd a thermostat ystafell": (
"charging system linked to use of community heating, programmer and room thermostat"
),
"system dalu wedigçöi chysylltu +ó defnyddio gwres cymunedol, rhaglennydd a thermostat ystafell":
"charging system linked to use of community heating, programmer and room thermostat",
'system dalu wedigçöi chysylltu +ó defnyddio gwres cymunedol, rhaglennydd a trvs':
"charging system linked to use of community heating, programmer and trvs",
'system dalu wedigçöi chysylltu +ó defnyddio gwres cymunedol, trvs':
'charging system linked to use of community heating, trvs',
't+-ól un gyfradd, trvs': 'single rate heating, trvs',
't+ól un gyfradd, rhaglennydd a trvs': 'single rate heating, programmer, trvs',
't+ól un gyfradd, trvs': 'single rate heating, trvs',
'trvs a falf osgoi': 'trvs and bypass'
}
def __init__(self, description: str):

View file

@ -1,15 +1,71 @@
import re
from typing import Dict, Union
from model_data.BaseUtility import Definitions
from model_data.epc_attributes.attribute_utils import extract_component_types, extract_thermal_transmittance
from model_data.epc_attributes.attribute_utils import (
extract_component_types,
extract_thermal_transmittance
)
class WallAttributes(Definitions):
WALL_TYPES = ['cavity wall', 'filled cavity', 'solid brick', 'system built', 'timber frame', 'granite or whinstone',
'as built', 'cob', 'assumed', 'sandstone or limestone']
'as built', 'cob', 'assumed', 'sandstone or limestone', "park home"]
WELSH_TEXT = {
"Briciau solet, fel yGÇÖu hadeiladwyd, dim inswleiddio (rhagdybiaeth)":
"Solid brick, as built, no insulation (assumed)",
'Waliau ceudod, fel yGÇÖu hadeiladwyd, inswleiddio rhannol (rhagdybiaeth)':
'Cavity wall, as built, partial insulation (assumed)',
'Waliau ceudod, fel yGÇÖu hadeiladwyd, inswleiddio rhannol':
'Cavity wall, as built, partial insulation',
'Waliau ceudod, fel yGÇÖu hadeiladwyd, dim inswleiddio (rhagdybiaeth)':
'Cavity wall, as built, no insulation (assumed)',
'Waliau ceudod, fel yGÇÖu hadeiladwyd, dim inswleiddio':
'Cavity wall, as built, no insulation',
'Tywodfaen, fel yGÇÖu hadeiladwyd, dim inswleiddio (rhagdybiaeth)':
'Sandstone or limestone, as built, no insulation (assumed)',
'Tywodfaen, fel yGÇÖu hadeiladwyd, dim inswleiddio':
'Sandstone or limestone, as built, no insulation',
'Waliau ceudod, ceudod wediGÇÖi lenwi': 'Cavity wall, filled cavity',
'Waliau ceudod, fel yGÇÖu hadeiladwyd, wediGÇÖu hinswleiddio (rhagdybiaeth)':
'Cavity wall, as built, insulated (assumed)',
'Waliau ceudod, fel yGÇÖu hadeiladwyd, wediGÇÖu hinswleiddio':
'Cavity wall, as built, insulated',
'Gwenithfaen neu risgraig, fel yGÇÖu hadeiladwyd, dim inswleiddio (rhagdybiaeth)':
'Granite or whinstone, as built, no insulation (assumed)',
'Waliau ceudod,': 'Cavity wall, as built, no insulation',
'Ffr+óm bren, fel yGÇÖu hadeiladwyd, wediGÇÖu hinswleiddio (rhagdybiaeth)':
'Timber frame, as built, insulated (assumed)',
'Ffr+óm bren, fel yGÇÖu hadeiladwyd, wediGÇÖu hinswleiddio':
'Timber frame, as built, insulated',
'Gwenithfaen neu risgraig, gydag inswleiddio allanol': 'Granite or whinstone, with external insulation',
'WediGÇÖu hadeiladu yn +¦l system, fel yGÇÖu hadeiladwyd, dim inswleiddio (rhagdybiaeth)':
'System built, as built, no insulation (assumed)',
'Tywodfaen, gydag inswleiddio mewnol': 'Sandstone or limestone, with internal insulation',
'Waliau ceudod, ynysydd allanol a llenwi ceudod': 'Cavity wall, filled cavity and external insulation',
'Gwenithfaen neu risgraig, gydag inswleiddio mewnol': 'Granite or whinstone, with internal insulation',
'Ffr+óm bren, fel yGÇÖu hadeiladwyd, inswleiddio rhannol (rhagdybiaeth)':
'Timber frame, as built, partial insulation (assumed)',
'WediGÇÖu hadeiladu yn +¦l system, fel yGÇÖu hadeiladwyd, wediGÇÖu hinswleiddio (rhagdybiaeth)':
'System built, as built, insulated (assumed)',
'WediGÇÖu hadeiladu yn +¦l system, fel yGÇÖu hadeiladwyd, wediGÇÖu hinswleiddio':
'System built, as built, insulated',
'WediGÇÖu hadeiladu yn +¦l system, gydag inswleiddio allanol': 'System built, with external insulation',
'Briciau solet, gydag inswleiddio mewnol': 'Solid brick, with internal insulation',
'WediGÇÖu hadeiladu yn +¦l system, fel yGÇÖu hadeiladwyd, inswleiddio rhannol (rhagdybiaeth)':
'System built, as built, partial insulation (assumed)',
'WediGÇÖu hadeiladu yn +¦l system, fel yGÇÖu hadeiladwyd, inswleiddio rhannol':
'System built, as built, partial insulation',
'Ffr+óm bren, fel yGÇÖu hadeiladwyd, dim inswleiddio (rhagdybiaeth)':
'Timber frame, as built, no insulation (assumed)',
'Ffr+óm bren, fel yGÇÖu hadeiladwyd, dim inswleiddio':
'Timber frame, as built, no insulation',
'Tywodfaen, gydag inswleiddio allanol': 'Sandstone or limestone, with external insulation',
'Waliau ceudod, gydag inswleiddio allanol': 'Cavity wall, with external insulation',
'Briciau solet, gydag inswleiddio allanol': 'Solid brick, with external insulation',
# Add in some corrections:
'Co with external insulation': 'Cob, with external insulation',
'Cowith external insulation': 'Cob, with external insulation',
}
def __init__(self, description: str):
@ -18,13 +74,27 @@ class WallAttributes(Definitions):
"""
self.description: str = description
translation = self.WELSH_TEXT.get(self.description)
if translation:
self.nodata = False
self.description = translation
self.welsh_translation_search()
self.nodata = not description or description in self.DATA_ANOMALY_MATCHES
def welsh_translation_search(self):
"""
For some descriptions, we need to translate from Welsh to English
:return:
"""
uvalue_search = re.search(r"Trawsyriannedd thermol cyfartalog (\d+\.?\d*)", self.description)
if uvalue_search:
uvalue = uvalue_search.group(1)
self.description = f"Average thermal transmittance {uvalue} W/m-¦K"
else:
translation = self.WELSH_TEXT.get(self.description)
if translation:
self.nodata = False
self.description = translation
def process(self) -> Dict[str, Union[float, str, bool, None]]:
result: Dict[str, Union[float, str, bool, None]] = {}
if self.nodata:
@ -38,6 +108,10 @@ class WallAttributes(Definitions):
# wall type
result, description = extract_component_types(result, description, list_of_components=self.WALL_TYPES)
# Handle some edge cases
if "sandstone" in description and not result["is_sandstone_or_limestone"]:
result["is_sandstone_or_limestone"] = True
# insulation thickness - this is far from a perfect approach and we'd likely need to use nlp to do this
# generally however this is sufficient for mvp
thickness_map = {

View file

@ -2,8 +2,9 @@ import re
import string
from typing import Tuple, Union, Dict, List
THERMAL_TRANSMITTENCE_STR = r"average thermal transmittance (-?\d+\.\d+)\s(w/m-¦k)"
THERMAL_TRANSMITTANCE_REGEX = re.compile(THERMAL_TRANSMITTENCE_STR)
THERMAL_TRANSMITTANCE_STR = r"average thermal transmittance (-?\d+(\.\d+)?)\s(w/m\S+k)"
THERMAL_TRANSMITTANCE_REGEX = re.compile(THERMAL_TRANSMITTANCE_STR)
DOUBLE_SPACE_PATTERN = re.compile(r"\s+")
@ -20,11 +21,12 @@ def extract_thermal_transmittance(result: dict, description: str) -> Tuple[
"""
match = THERMAL_TRANSMITTANCE_REGEX.search(description)
if match:
result['thermal_transmittance'] = float(match.group(1))
result['thermal_transmittance_unit'] = match.group(2)
result['thermal_transmittance_unit'] = match.group(3)
# Remove the match from the description
description = re.sub(THERMAL_TRANSMITTENCE_STR, "", description)
description = re.sub(THERMAL_TRANSMITTANCE_STR, "", description)
else:
result['thermal_transmittance'] = None
result['thermal_transmittance_unit'] = None

View file

@ -7,15 +7,18 @@ from model_data.simulation_system.core.Settings import (
EARLIEST_EPC_DATE,
FULLY_GLAZED_DESCRIPTIONS,
AVERAGE_FIXED_FEATURES,
FLOOR_HEIGHT_NATIONAL_AVERAGE,
TOTAL_FLOOR_AREA_NATIONAL_AVERAGE,
FLOOR_LEVEL_MAP,
BUILT_FORM_REMAP,
COLUMNS_TO_MERGE_ON,
COMPONENT_FEATURES,
FIXED_FEATURES,
COLUMNTYPES
COLUMNTYPES,
RDSAP_RESPONSE,
MAX_SAP_SCORE,
fill_na_map,
FIXED_DESCRIPTON_MAPPED_FEATURES
)
from typing import List
@ -101,10 +104,14 @@ class DataProcessor:
raise NotImplementedError("Not handled the case for value %s" % x)
self.data["CONSTRUCTION_AGE_BAND_CLEANED"] = self.data["CONSTRUCTION_AGE_BAND"].apply(
self.data["CONSTRUCTION_AGE_BAND"] = self.data["CONSTRUCTION_AGE_BAND"].apply(
lambda x: clean_construction_age_band(x)
)
self.data = self.data[
~pd.isnull(self.data["CONSTRUCTION_AGE_BAND"])
]
def clean_missing_rooms(self):
"""
For the number of heated rooms and number of habitable rooms, we clean these values up front,
@ -132,7 +139,7 @@ class DataProcessor:
for col in ["NUMBER_HEATED_ROOMS", "NUMBER_HABITABLE_ROOMS"]:
to_index = 3
matching_columns = ["PROPERTY_TYPE", "BUILT_FORM", "CONSTRUCTION_AGE_BAND_CLEANED", "POSTAL_AREA"]
matching_columns = ["PROPERTY_TYPE", "BUILT_FORM", "CONSTRUCTION_AGE_BAND", "POSTAL_AREA"]
has_missings = pd.isnull(self.data[col]).sum()
while has_missings:
self.data = apply_clean(
@ -175,6 +182,8 @@ class DataProcessor:
if not self.newdata:
self.confine_data()
self.remap_columns()
# We have some non-standard construction age bands which we'll clean for matching
self.standardise_construction_age_band()
self.clean_missing_rooms()
@ -189,7 +198,6 @@ class DataProcessor:
self.retain_multiple_epc_properties(
epc_minimum_count=DATA_PROCESSOR_SETTINGS["epc_minimum_count"]
)
self.remap_columns()
if DATA_PROCESSOR_SETTINGS["epc_minimum_count"] >= 1:
# If we have multiple EPC records, we can try and do filling
@ -199,8 +207,14 @@ class DataProcessor:
# Final re-casting after data transformed and prepared
self.data = self.data.astype(COLUMNTYPES)
self.na_remapping()
return self.data
def na_remapping(self):
for column, fill_value in fill_na_map.items():
self.data[column] = self.data[column].fillna(fill_value)
def fill_na_fields(self, columns_to_fill: List = COLUMNS_TO_MERGE_ON):
"""
If we have a minimum of 2 epcs, we can do back fill and forward fill on certain data fields
@ -244,6 +258,10 @@ class DataProcessor:
data["FLOOR_LEVEL"] = data["FLOOR_LEVEL"].replace(FLOOR_LEVEL_MAP)
data["BUILT_FORM"] = data["BUILT_FORM"].replace(BUILT_FORM_REMAP)
convert_to_lower = ["TRANSACTION_TYPE"]
for col in convert_to_lower:
data[col] = data[col].str.lower()
self.data = data
def make_cleaning_averages(self) -> pd.DataFrame:
@ -305,62 +323,43 @@ class DataProcessor:
suffixes=["", "_BUILT_FORM_AVERAGE"],
)
# Replace any missing NAN values with averages for the same Property type and built form
cleaning_averages_filled["TOTAL_FLOOR_AREA"] = cleaning_averages_filled[
"TOTAL_FLOOR_AREA"
].fillna(cleaning_averages_filled["TOTAL_FLOOR_AREA_AVERAGE"])
cleaning_averages_filled["FLOOR_HEIGHT"] = cleaning_averages_filled[
"FLOOR_HEIGHT"
].fillna(cleaning_averages_filled["FLOOR_HEIGHT_AVERAGE"])
cleaning_averages_filled = cleaning_averages_filled.drop(
columns=["TOTAL_FLOOR_AREA_AVERAGE", "FLOOR_HEIGHT_AVERAGE"]
)
for variable in AVERAGE_FIXED_FEATURES:
# Replace any missing NAN values with averages for the same Property type and built form
cleaning_averages_filled[variable] = cleaning_averages_filled[variable].fillna(
cleaning_averages_filled[f"{variable}_AVERAGE"]
)
# If there are still NA values i.e. the averages do not have values for a speicifc group of property tyope
# and built form
# We can use just the property type average and replace
cleaning_averages_filled["TOTAL_FLOOR_AREA"] = cleaning_averages_filled[
"TOTAL_FLOOR_AREA"
].fillna(cleaning_averages_filled["TOTAL_FLOOR_AREA_PROPERTY_AVERAGE"])
cleaning_averages_filled["FLOOR_HEIGHT"] = cleaning_averages_filled[
"FLOOR_HEIGHT"
].fillna(cleaning_averages_filled["FLOOR_HEIGHT_PROPERTY_AVERAGE"])
cleaning_averages_filled = cleaning_averages_filled.drop(
columns=[
"TOTAL_FLOOR_AREA_PROPERTY_AVERAGE",
"FLOOR_HEIGHT_PROPERTY_AVERAGE",
]
)
cleaning_averages_filled = cleaning_averages_filled.drop(columns=f"{variable}_AVERAGE")
# If there are still NA values, use BUILT FORM averages
cleaning_averages_filled["TOTAL_FLOOR_AREA"] = cleaning_averages_filled[
"TOTAL_FLOOR_AREA"
].fillna(cleaning_averages_filled["TOTAL_FLOOR_AREA_BUILT_FORM_AVERAGE"])
cleaning_averages_filled["FLOOR_HEIGHT"] = cleaning_averages_filled[
"FLOOR_HEIGHT"
].fillna(cleaning_averages_filled["FLOOR_HEIGHT_BUILT_FORM_AVERAGE"])
cleaning_averages_filled = cleaning_averages_filled.drop(
columns=[
"TOTAL_FLOOR_AREA_BUILT_FORM_AVERAGE",
"FLOOR_HEIGHT_BUILT_FORM_AVERAGE",
]
)
# If there are still NA values i.e. the averages do not have values for a speicifc group of property tyope
# and built form
# We can use just the property type average and replace
# If there still is na values, use average across all properties in consituecy
cleaning_averages_filled["TOTAL_FLOOR_AREA"] = cleaning_averages_filled[
"TOTAL_FLOOR_AREA"
].fillna(cleaning_averages_filled["TOTAL_FLOOR_AREA"].mean())
cleaning_averages_filled["FLOOR_HEIGHT"] = cleaning_averages_filled[
"FLOOR_HEIGHT"
].fillna(cleaning_averages_filled["FLOOR_HEIGHT"].mean())
cleaning_averages_filled[variable] = cleaning_averages_filled[variable].fillna(
cleaning_averages_filled[f"{variable}_PROPERTY_AVERAGE"]
)
cleaning_averages_filled = cleaning_averages_filled.drop(columns=f"{variable}_PROPERTY_AVERAGE")
# If there are still NA values, use BUILT FORM averages
cleaning_averages_filled["variable"] = cleaning_averages_filled[variable].fillna(
cleaning_averages_filled[f"{variable}_BUILT_FORM_AVERAGE"]
)
cleaning_averages_filled = cleaning_averages_filled.drop(columns=f"{variable}_BUILT_FORM_AVERAGE")
# If there still is na values, use average across all properties in consituecy
cleaning_averages_filled[variable] = cleaning_averages_filled[
variable
].fillna(cleaning_averages_filled[variable].mean())
# If the consituency is all NA values, then take UK AVERAGE VALUES
cleaning_averages_filled["TOTAL_FLOOR_AREA"] = cleaning_averages_filled[
"TOTAL_FLOOR_AREA"
].fillna(TOTAL_FLOOR_AREA_NATIONAL_AVERAGE)
cleaning_averages_filled["FLOOR_HEIGHT"] = cleaning_averages_filled[
"FLOOR_HEIGHT"
].fillna(FLOOR_HEIGHT_NATIONAL_AVERAGE)
# cleaning_averages_filled["TOTAL_FLOOR_AREA"] = cleaning_averages_filled[
# "TOTAL_FLOOR_AREA"
# ].fillna(TOTAL_FLOOR_AREA_NATIONAL_AVERAGE)
# cleaning_averages_filled["FLOOR_HEIGHT"] = cleaning_averages_filled[
# "FLOOR_HEIGHT"
# ].fillna(FLOOR_HEIGHT_NATIONAL_AVERAGE)
return cleaning_averages_filled
@ -402,12 +401,29 @@ class DataProcessor:
# Filter 4: We remove floor level in top floor or mid floor since this is ambiguous
# Filter 5: Remove any EPCs with a SAP score above 100
# Filter 6: We found a small number of cases that have missing window description so we drop these
# Filter 7: We found a small number of cases that have missing hotwater description so we drop these
self.data = self.data[~pd.isnull(self.data["UPRN"])]
self.data = self.data[self.data["LODGEMENT_DATE"] >= EARLIEST_EPC_DATE]
self.data = self.data[self.data["TRANSACTION_TYPE"] != "new dwelling"]
self.data = self.data[
~self.data["FLOOR_LEVEL"].isin(["top floor", "mid floor"])
]
self.data = self.data[self.data[RDSAP_RESPONSE] <= MAX_SAP_SCORE]
# We observed 7 final records with missing windows and 2 records with missing hot water so we shall remove them
self.data = self.data[~pd.isnull(self.data["WINDOWS_DESCRIPTION"])]
self.data = self.data[~pd.isnull(self.data["HOTWATER_DESCRIPTION"])]
self.data = self.data[~pd.isnull(self.data["ROOF_DESCRIPTION"])]
# Because park homes are surveyed unusually (for example, we don't have u-values to
# look up for their different components, they need to be collected in survey and aren't reflected in
# EPCs) we'll ignore them from the model
self.data = self.data[self.data["PROPERTY_TYPE"] != "Park home"]
def clean_multi_glaze_proportion(self) -> None:
"""
@ -437,6 +453,12 @@ class DataProcessor:
differs depending on where the function is being used.
:return: Cleaned DataFrame.
"""
cols_to_clean = [
c for c in ["TOTAL_FLOOR_AREA", "FLOOR_HEIGHT", "FIXED_LIGHTING_OUTLETS_COUNT"] if
c in data_to_clean.columns
]
# Enforce data types
for col in ["NUMBER_HABITABLE_ROOMS", "NUMBER_HEATED_ROOMS"]:
data_to_clean[col] = data_to_clean[col].astype(float)
@ -445,10 +467,9 @@ class DataProcessor:
columns_to_merge_on = data_to_clean[cols_to_merge_on].dropna().columns.tolist()
# Calculate averages
cleaning_averages_to_merge = cleaning_data.groupby(columns_to_merge_on).agg({
"TOTAL_FLOOR_AREA": "mean",
"FLOOR_HEIGHT": "mean"
})
cleaning_averages_to_merge = cleaning_data.groupby(columns_to_merge_on).agg(
dict(zip(cols_to_clean, ["mean", ] * len(cols_to_clean)))
)
# Merge with the original data
data_to_clean = pd.merge(
@ -460,7 +481,7 @@ class DataProcessor:
)
# Fill NaN values with averages
for col in ["TOTAL_FLOOR_AREA", "FLOOR_HEIGHT"]:
for col in cols_to_clean:
data_to_clean[col].fillna(data_to_clean[f"{col}_AVERAGE"], inplace=True)
data_to_clean.drop(columns=[f"{col}_AVERAGE"], inplace=True)
@ -486,3 +507,144 @@ class DataProcessor:
:return: Pandas dataframe containing the columns defined in FIXED_FEATURES
"""
return self.data[FIXED_FEATURES]
@staticmethod
def coerce_boolean_columns(df: pd.DataFrame, cols_to_ignore: List | None = None):
"""
Coerce columns with string 'True'/'False' values to boolean columns.
:param df: Input DataFrame.
:param cols_to_ignore: If specified, is a list of columns to ignore, e.g. uuids
:return: DataFrame with coerced columns.
"""
object_columns = df.select_dtypes(include=['object']).columns
if cols_to_ignore:
object_columns = [c for c in object_columns if c not in cols_to_ignore]
for column in object_columns:
unique_values = df[column].dropna().unique()
# If the unique values in the column are 'True' and 'False', convert the column to boolean
if set(unique_values) == {'True', 'False'} or set(unique_values) == {True, False}:
df[column] = df[column].astype(bool)
return df
@classmethod
def difference_data(cls, df: pd.DataFrame):
"""
Given a dataframe and starting and ending columns, this function will convert the features to
differenced the ending subtract the starting value, which is useful for modelling the difference responces
"""
# We ensure that the u value columns are co-erced to a numerical format
uvalue_columns = [col for col in df.columns if "thermal_transmittance" in col]
for uvalue_col in uvalue_columns:
df[uvalue_col] = pd.to_numeric(df[uvalue_col])
key_columns = [
"RDSAP_CHANGE", "HEAT_DEMAND_CHANGE", "CARBON_CHANGE", "SAP_STARTING", "HEAT_DEMAND_STARTING",
"CARBON_STARTING", "UPRN", "CONSTITUENCY",
]
ignore_cols = FIXED_FEATURES + FIXED_DESCRIPTON_MAPPED_FEATURES + key_columns
columns = {x for x in df.columns if x not in ignore_cols}
non_numerical_columns = df.select_dtypes(exclude=['number']).columns.tolist()
non_numerical_columns = [col for col in non_numerical_columns if col in columns]
levels = {col: df[col].unique().tolist() for col in non_numerical_columns}
df = pd.get_dummies(df, columns=non_numerical_columns)
# We make sure there is a starting and ending version of the column
diff_columns = []
no_diff_columns = [] # Store for debugging
for col in columns:
if "_ENDING" in col:
# Don't keep the endings
continue
else:
# We have a starting column so check if we have an ending
if col.replace("_STARTING", "") + "_ENDING" in columns:
diff_columns.append(col)
else:
no_diff_columns.append(col)
if any(c not in FIXED_DESCRIPTON_MAPPED_FEATURES for c in no_diff_columns):
raise Exception("Something went wrong, potentially missed a differencing column")
datatypes = df.dtypes
# Note: We also difference columns like floor area and floor height. We should experiement with this.
# Starting floor area will heavily impact the starting sap value so that feature may be encapsulated by
# the starting value, therefore to explain any differences in the new floor area, it may be enough to
# just consider the difference however we can play around with this.
# Do the differencing
cols_to_append = {}
for starting_col in diff_columns:
base_col = starting_col.replace("_STARTING", "")
if "_STARTING" in starting_col:
ending_col = starting_col.replace("_STARTING", "_ENDING")
else:
ending_col = starting_col + "_ENDING"
if starting_col not in non_numerical_columns:
cols_to_append[f"{base_col}_DIFF"] = df[ending_col] - df[starting_col]
df = df.drop(columns=[starting_col, ending_col])
continue
level_values = list(set(levels[starting_col] + levels[ending_col]))
level_cols = []
for level in level_values:
starting_level_col = "_".join([starting_col, str(level)])
ending_level_col = "_".join([ending_col, str(level)])
if starting_level_col not in df.columns:
# We have no starting, just ending
col_type = datatypes[ending_level_col].name
if col_type == "bool":
cols_to_append[f"{base_col}_{level}_DIFF"] = df[ending_level_col].astype(int)
else:
cols_to_append[f"{base_col}_{level}_DIFF"] = df[ending_level_col]
level_cols.append(ending_level_col)
elif ending_level_col not in df.columns:
# We have no ending, just starting
col_type = datatypes[starting_level_col].name
if col_type == "bool":
cols_to_append[f"{base_col}_{level}_DIFF"] = -1 * df[starting_level_col].astype(int)
else:
cols_to_append[f"{base_col}_{level}_DIFF"] = -1 * df[ending_level_col]
level_cols.append(starting_level_col)
else:
col_type = datatypes[starting_level_col].name
if col_type == "bool":
cols_to_append[f"{base_col}_{level}_DIFF"] = (
df[ending_level_col].astype(int) - df[starting_level_col].astype(int)
)
else:
cols_to_append[f"{base_col}_{level}_DIFF"] = df[ending_level_col] - df[starting_level_col]
level_cols.extend([starting_level_col, ending_level_col])
# Drop the columns
df = df.drop(columns=level_cols)
cols_to_append = pd.DataFrame(cols_to_append)
df = pd.concat([df, cols_to_append], axis=1)
# Perform a final coercing of string True/False columns to boolean
df = cls.coerce_boolean_columns(df, cols_to_ignore=key_columns)
return df

View file

@ -55,7 +55,8 @@ FLOOR_HEIGHT_NATIONAL_AVERAGE = 2.45
AVERAGE_FIXED_FEATURES = [
"TOTAL_FLOOR_AREA",
"FLOOR_HEIGHT"
"FLOOR_HEIGHT",
"FIXED_LIGHTING_OUTLETS_COUNT",
]
COLUMNS_TO_MERGE_ON = [
@ -82,9 +83,6 @@ FIXED_FEATURES = [
"CONSTITUENCY",
"NUMBER_HEATED_ROOMS",
"FIXED_LIGHTING_OUTLETS_COUNT",
"FLOOR_HEIGHT",
"FLOOR_LEVEL",
"TOTAL_FLOOR_AREA",
]
COMPONENT_FEATURES = [
@ -120,7 +118,6 @@ LATEST_FIELD = [
"NUMBER_HABITABLE_ROOMS",
"NUMBER_HEATED_ROOMS",
"FIXED_LIGHTING_OUTLETS_COUNT",
"FLOOR_LEVEL",
"CONSTRUCTION_AGE_BAND", # This is a field we're probably want to use verisk data for
]
@ -173,7 +170,7 @@ DATA_PROCESSOR_SETTINGS = {
COLUMNTYPES = {
'UPRN': 'object', 'TOTAL_FLOOR_AREA': 'float64', 'FLOOR_HEIGHT': 'float64', 'PROPERTY_TYPE': 'object',
'BUILT_FORM': 'object', 'CONSTITUENCY': 'object', 'NUMBER_HABITABLE_ROOMS': 'float64',
'NUMBER_HEATED_ROOMS': 'float64', 'FIXED_LIGHTING_OUTLETS_COUNT': 'float64', 'FLOOR_LEVEL': 'float64',
'NUMBER_HEATED_ROOMS': 'float64', 'FIXED_LIGHTING_OUTLETS_COUNT': 'float64',
'CONSTRUCTION_AGE_BAND': 'object',
'TRANSACTION_TYPE': 'object',
'WALLS_DESCRIPTION': 'object',
@ -194,3 +191,31 @@ COLUMNTYPES = {
'EXTENSION_COUNT': 'float64',
'LODGEMENT_DATE': 'object',
}
# For modelling, we don't allow records with more than 100 SAP points
MAX_SAP_SCORE = 100
fill_na_map = {
# There are some descriptions, such as "To be used only when there is no heating/hot-water system or data is from
# a community network" that could be clustered with unknown fuel
"MAIN_FUEL": "UNKNOWN",
"MECHANICAL_VENTILATION": "Unknown",
"SECONDHEAT_DESCRIPTION": "None",
"ENERGY_TARIFF": "Unknown",
# We set solar water heating flag to N - we could investigate using a different category entirely
"SOLAR_WATER_HEATING_FLAG": "N",
"GLAZED_TYPE": "not defined",
"MULTI_GLAZE_PROPORTION": 0,
"LOW_ENERGY_LIGHTING": 0,
"MAINHEATCONT_DESCRIPTION": "Unknown",
"EXTENSION_COUNT": 0,
"NUMBER_OPEN_FIREPLACES": 0
}
# After the property descriptions have been re-remapped, we expect these features to be fixed
FIXED_DESCRIPTON_MAPPED_FEATURES = [
'another_property_below', 'is_roof_room', 'is_granite_or_whinstone', 'is_flat', 'is_suspended',
'has_dwelling_above', 'is_as_built', 'is_to_external_air', 'is_cob', 'is_pitched', 'is_solid', 'is_at_rafters',
'is_solid_brick', 'is_loft', 'is_system_built', 'is_timber_frame', 'is_sandstone_or_limestone', 'is_filled_cavity',
'is_cavity_wall', 'is_thatched', 'is_to_unheated_space'
]

View file

@ -1,4 +1,5 @@
import pandas as pd
import numpy as np
from tqdm import tqdm
import msgpack
@ -14,7 +15,12 @@ from model_data.simulation_system.core.Settings import (
CARBON_RESPONSE,
)
from model_data.simulation_system.core.DataProcessor import DataProcessor
from utils.s3 import save_dataframe_to_s3_parquet, read_from_s3
from utils.s3 import save_dataframe_to_s3_parquet, read_from_s3, read_dataframe_from_s3_parquet
from recommendations.rdsap_tables import england_wales_age_band_lookup
from recommendations.recommendation_utils import (
get_wall_u_value, get_roof_u_value, get_floor_u_value, estimate_perimeter,
get_wall_type
)
DATA_DIRECTORY = Path(__file__).parent / "model_data" / "simulation_system" / "data" / "all-domestic-certificates"
@ -48,65 +54,81 @@ def process_and_prune_desriptions(df, cleaned_lookup):
:return:
"""
# TODO: In a future iteration, we can test using the binary features and the insulation thickness
# estimates, we well as estimated U-values
cols_to_drop = {
"walls": [
'original_description', 'thermal_transmittance',
'thermal_transmittance_unit', 'is_cavity_wall', 'is_filled_cavity',
'is_solid_brick', 'is_system_built', 'is_timber_frame',
'is_granite_or_whinstone', 'is_as_built', 'is_cob', 'is_assumed',
'is_sandstone_or_limestone', 'insulation_thickness',
'external_insulation', 'internal_insulation',
# We need to cleaned descriptions for pulling out u-values
'original_description', 'thermal_transmittance_unit',
'original_description_ENDING',
'thermal_transmittance_ENDING', 'thermal_transmittance_unit_ENDING',
'thermal_transmittance_unit_ENDING',
'is_cavity_wall_ENDING', 'is_filled_cavity_ENDING',
'is_solid_brick_ENDING', 'is_system_built_ENDING',
'is_timber_frame_ENDING', 'is_granite_or_whinstone_ENDING',
'is_as_built_ENDING', 'is_cob_ENDING', 'is_assumed_ENDING',
'is_sandstone_or_limestone_ENDING', 'insulation_thickness_ENDING',
'external_insulation_ENDING', 'internal_insulation_ENDING',
'is_sandstone_or_limestone_ENDING',
# Re remove the is_assumed columns
"is_assumed", "is_assumed_ENDING"
],
"floor": [
'original_description', 'thermal_transmittance',
'thermal_transmittance_unit', 'is_assumed', 'is_to_unheated_space',
'is_to_external_air', 'is_suspended', 'is_solid',
'another_property_below', 'insulation_thickness', 'no_data',
'original_description_ENDING',
'thermal_transmittance_ENDING', 'thermal_transmittance_unit_ENDING',
'is_assumed_ENDING', 'is_to_unheated_space_ENDING',
'is_to_external_air_ENDING', 'is_suspended_ENDING', 'is_solid_ENDING',
'another_property_below_ENDING', 'insulation_thickness_ENDING',
'no_data_ENDING',
"original_description", "clean_description", "thermal_transmittance_unit",
"no_data", "no_data_ENDING", "original_description_ENDING",
"clean_description_ENDING", "thermal_transmittance_unit_ENDING",
"is_suspended_ENDING", "is_solid_ENDING", "another_property_below_ENDING",
"is_to_unheated_space_ENDING", "is_to_external_air_ENDING", "is_assumed",
"is_assumed_ENDING"
],
"roof": [
'original_description', 'clean_description', 'thermal_transmittance',
'thermal_transmittance_unit', 'is_pitched', 'is_roof_room', 'is_loft',
'is_flat', 'is_thatched', 'is_at_rafters', 'is_assumed',
'has_dwelling_above', 'is_valid', 'insulation_thickness',
'original_description_ENDING', 'clean_description_ENDING',
'thermal_transmittance_ENDING', 'thermal_transmittance_unit_ENDING',
'is_pitched_ENDING', 'is_roof_room_ENDING', 'is_loft_ENDING',
'is_flat_ENDING', 'is_thatched_ENDING', 'is_at_rafters_ENDING',
'is_assumed_ENDING', 'has_dwelling_above_ENDING', 'is_valid_ENDING',
'insulation_thickness_ENDING',
]
"original_description", "clean_description", "thermal_transmittance_unit",
"is_assumed", "is_valid", "original_description_ENDING", "clean_description_ENDING",
"thermal_transmittance_unit_ENDING", "is_pitched_ENDING", "is_roof_room_ENDING",
"is_loft_ENDING", "is_flat_ENDING", "is_thatched_ENDING", "is_at_rafters_ENDING",
"has_dwelling_above_ENDING", "is_assumed_ENDING", "is_valid_ENDING"
],
"hotwater": [
"original_description", "clean_description", "assumed", "original_description_ENDING",
"clean_description_ENDING", "assumed_ENDING"
],
"mainheat": [
"original_description", "clean_description", "original_description_ENDING",
"has_assumed", "original_description_ENDING", "clean_description_ENDING",
"has_assumed_ENDING",
],
"mainheatcont": [
"original_description", "clean_description", "original_description_ENDING", "clean_description_ENDING"
],
"windows": [
"original_description", "clean_description", "original_description_ENDING", "clean_description_ENDING",
# We don't need many of the glazing coverage features because we have the multi_glaze_proportion feature
"has_glazing", "glazing_coverage", "no_data", "has_glazing_ENDING", "glazing_coverage_ENDING",
"no_data_ENDING"
],
"main-fuel": [
"original_description", "clean_description", "original_description_ENDING", "clean_description_ENDING"
],
}
for component in ["walls", "floor", "roof"]:
for component in ["walls", "floor", "roof", "hotwater", "mainheat", "mainheatcont", "windows", "main-fuel"]:
component_upper = component.upper()
if component == "main-fuel":
component_upper = component_upper.replace("-", "_")
cleaned_key = "main-fuel" if component == "main-fuel" else f"{component}-description"
left_on_starting = (
f"{component_upper}_STARTING" if component == "main-fuel" else f"{component_upper}_DESCRIPTION_STARTING"
)
left_on_ending = (
f"{component_upper}_ENDING" if component == "main-fuel" else f"{component_upper}_DESCRIPTION_ENDING"
)
df = df.merge(
pd.DataFrame(cleaned_lookup[f"{component}-description"]),
pd.DataFrame(cleaned_lookup[cleaned_key]),
how="left",
left_on=f"{component_upper}_DESCRIPTION_STARTING",
left_on=left_on_starting,
right_on="original_description",
).merge(
pd.DataFrame(cleaned_lookup[f"{component}-description"]),
pd.DataFrame(cleaned_lookup[cleaned_key]),
how="left",
left_on=f"{component_upper}_DESCRIPTION_ENDING",
left_on=left_on_ending,
right_on="original_description",
suffixes=("", "_ENDING")
)
@ -126,9 +148,10 @@ def process_and_prune_desriptions(df, cleaned_lookup):
(df["is_suspended"] == df["is_suspended_ENDING"]) &
(df["is_solid"] == df["is_solid_ENDING"]) &
(df["another_property_below"] == df["another_property_below_ENDING"]) &
(df["is_to_unheated_space"] == df["is_to_unheated_space_ENDING"])
(df["is_to_unheated_space"] == df["is_to_unheated_space_ENDING"]) &
(df["is_to_external_air"] == df["is_to_external_air_ENDING"])
]
else:
elif component == "roof":
df = df[
(df["is_pitched"] == df["is_pitched_ENDING"]) &
(df["is_roof_room"] == df["is_roof_room_ENDING"]) &
@ -144,17 +167,215 @@ def process_and_prune_desriptions(df, cleaned_lookup):
# Drop original cols
original_cols = [
f"{component_upper}_DESCRIPTION_STARTING", f"{component_upper}_DESCRIPTION_ENDING"
] if component != "main-fuel" else [
f"{component_upper}_STARTING", f"{component_upper}_ENDING"
]
df = df.drop(
columns=cols_to_drop[component] + original_cols
).rename(
columns={
"clean_description": f"{component_upper}_DESCRIPTION_STARTING",
"clean_description_ENDING": f"{component_upper}_DESCRIPTION_ENDING",
df = df.drop(columns=cols_to_drop[component] + original_cols)
# If we have an insulation_thickness column, rename it
if "insulation_thickness" in cleaned_lookup[cleaned_key][0]:
df = df.rename(
columns={
"insulation_thickness": f"{component}_insulation_thickness",
"insulation_thickness_ENDING": f"{component}_insulation_thickness_ENDING",
}
)
# If we have thermal transmittance, rename it
if "thermal_transmittance" in cleaned_lookup[cleaned_key][0]:
df = df.rename(
columns={
"thermal_transmittance": f"{component}_thermal_transmittance",
"thermal_transmittance_ENDING": f"{component}_thermal_transmittance_ENDING",
}
)
# If we have tarrif, rename it
if "tariff_type" in cleaned_lookup[cleaned_key][0]:
df = df.rename(
columns={
"tariff_type": f"{component}_tariff_type",
"tariff_type_ENDING": f"{component}_tariff_type_ENDING",
}
)
# We need the walls descriptions so we rename them to distinguish them
if component == "walls":
df = df.rename(
columns={
"clean_description": f"{component}_clean_description",
"clean_description_ENDING": f"{component}_clean_description_ENDING",
}
)
# We don't need any lighting specific cleaning, we just drop the original description as we use
# LOW_ENERGY_LIGHTING_STARTING, LOW_ENERGY_LIGHTING_ENDING
df = df.drop(columns=["LIGHTING_DESCRIPTION_STARTING", "LIGHTING_DESCRIPTION_ENDING"])
return df
def make_uvalues(df):
df["row_index"] = df.index
uvalues = []
for _, x in df.iterrows():
uprn = x["UPRN"]
row_index = x["row_index"]
age_band = england_wales_age_band_lookup[x["CONSTRUCTION_AGE_BAND"]]
# ~~~~~~~~~~~~~~~~~~
# Walls
# ~~~~~~~~~~~~~~~~~~
starting_wall_uvalue = x["walls_thermal_transmittance"]
if pd.isnull(starting_wall_uvalue):
starting_wall_uvalue = get_wall_u_value(
clean_description=x["walls_clean_description"],
age_band=age_band,
is_granite_or_whinstone=x["is_granite_or_whinstone"],
is_sandstone_or_limestone=x["is_sandstone_or_limestone"],
)
ending_wall_uvalue = x["walls_thermal_transmittance_ENDING"]
if pd.isnull(ending_wall_uvalue):
if x["walls_clean_description"] != x["walls_clean_description_ENDING"]:
ending_wall_uvalue = get_wall_u_value(
clean_description=x["walls_clean_description_ENDING"],
age_band=age_band,
is_granite_or_whinstone=x["is_granite_or_whinstone"],
is_sandstone_or_limestone=x["is_sandstone_or_limestone"],
)
else:
ending_wall_uvalue = starting_wall_uvalue
# ~~~~~~~~~~~~~~~~~~
# Roof
# ~~~~~~~~~~~~~~~~~~
starting_roof_uvalue = x["roof_thermal_transmittance"]
if pd.isnull(starting_roof_uvalue):
starting_roof_uvalue = get_roof_u_value(
insulation_thickness=x["roof_insulation_thickness"],
has_dwelling_above=x["has_dwelling_above"],
is_loft=x["is_loft"],
is_roof_room=x["is_roof_room"],
is_thatched=x["is_thatched"],
is_flat=x["is_flat"],
is_pitched=x["is_pitched"],
is_at_rafters=x["is_at_rafters"],
age_band=age_band
)
ending_roof_uvalue = x["roof_thermal_transmittance_ENDING"]
if pd.isnull(ending_roof_uvalue):
ending_roof_uvalue = get_roof_u_value(
insulation_thickness=x["roof_insulation_thickness_ENDING"],
has_dwelling_above=x["has_dwelling_above"],
is_loft=x["is_loft"],
is_roof_room=x["is_roof_room"],
is_thatched=x["is_thatched"],
is_flat=x["is_flat"],
is_pitched=x["is_pitched"],
is_at_rafters=x["is_at_rafters"],
age_band=age_band
)
# ~~~~~~~~~~~~~~~~~~
# Floor
# ~~~~~~~~~~~~~~~~~~
perimeters = {}
for suffix in ["_STARTING", "_ENDING"]:
floor_area = x[f"TOTAL_FLOOR_AREA{suffix}"]
n_rooms = x["NUMBER_HABITABLE_ROOMS"]
perimeters[f"estimated_perimeter{suffix}"] = estimate_perimeter(floor_area, n_rooms)
floor_type = "suspended" if x["is_suspended"] else "solid"
wall_type = get_wall_type(**x)
if x["another_property_below"]:
starting_floor_uvalue, ending_floor_uvalue = 0, 0
else:
starting_floor_uvalue = x["floor_thermal_transmittance"]
ending_floor_uvalue = x["floor_thermal_transmittance_ENDING"]
if pd.isnull(starting_floor_uvalue):
starting_floor_uvalue = get_floor_u_value(
floor_type=floor_type,
perimeter=perimeters["estimated_perimeter_STARTING"],
area=x[f"TOTAL_FLOOR_AREA_STARTING"],
insulation_thickness=x["floor_insulation_thickness"],
wall_type=wall_type,
age_band=age_band
)
if pd.isnull(ending_floor_uvalue):
ending_floor_uvalue = get_floor_u_value(
floor_type=floor_type,
perimeter=perimeters["estimated_perimeter_ENDING"],
area=x[f"TOTAL_FLOOR_AREA_ENDING"],
insulation_thickness=x["floor_insulation_thickness_ENDING"],
wall_type=wall_type,
age_band=age_band
)
uvalues.append(
{
"UPRN": uprn,
"row_index": row_index,
"starting_walls_uvalue": starting_wall_uvalue,
"ending_walls_uvalue": ending_wall_uvalue,
"starting_roof_uvalue": starting_roof_uvalue,
"ending_roof_uvalue": ending_roof_uvalue,
"starting_floor_uvalue": starting_floor_uvalue,
"ending_floor_uvalue": ending_floor_uvalue,
**perimeters
}
)
uvalues = pd.DataFrame(uvalues)
df = df.merge(
uvalues, how="left", on=["UPRN", "row_index"]
).drop(columns="row_index")
# Fill missings
for component in ["walls", "floor", "roof"]:
for suffix in ["", "_ENDING"]:
fill_col = f"starting_{component}_uvalue" if suffix == "" else f"ending_{component}_uvalue"
df[f"{component}_thermal_transmittance{suffix}"] = np.where(
pd.isnull(df[f"{component}_thermal_transmittance{suffix}"]),
df[fill_col],
df[f"{component}_thermal_transmittance{suffix}"]
)
df = df.drop(
columns=[
"starting_walls_uvalue", "ending_walls_uvalue", "starting_roof_uvalue",
"ending_roof_uvalue", "starting_floor_uvalue", "ending_floor_uvalue"
]
)
return df
def clean_missings_after_description_process(df):
missings = pd.isnull(df).sum()
missings = missings[missings > 0]
for col in missings.index:
unique_values = df[col].unique()
if True in unique_values or False in unique_values:
df[col] = df[col].fillna(False)
if "none" in unique_values:
df[col] = df[col].fillna("none")
else:
df[col] = df[col].fillna("Unknown")
return df
@ -172,22 +393,6 @@ def app():
dataset = []
cleaning_dataset = []
# TODO [x] : Does energy tariff make a difference
# - leave for now but it may not
# TODO: [x] : Add starting SAP and head demand as a feature
# TODO [x] : If SAP hasn't changed, we don't include the record
# TODO [x]: If SAP gets worse, it genuinely looks like in the vast majority of cases that the building looks
# worse in the newer epc, so we can switch the orders
# TODO [x] : Have a look at temporal features
# TODO [x] : Floor area will impact the EPC so instead of averaging, we should have a starting and ending value.
# TODO [x]: Same as floor area for floor height
# TODO [x]: If fundamental building fabric changes, we should proabably discard the record
# TODO [x]: Should we prune records that have an exceptionally large amount of time between them?
# - leave for now and check performance after temporal features
# TODO [x]: If we have multiple EPCs lodged on the same day, should we remove them? Could be corrections?
# - Leave for now
#
for directory in tqdm(directories):
filepath = directory / "certificates.csv"
@ -197,6 +402,15 @@ def app():
df = data_processor.pre_process()
cleaning_averages = data_processor.make_cleaning_averages()
# We have some odd cases with missing constituency so we fill
df = df.fillna({"CONSTITUENCY": df["CONSTITUENCY"].mode().values[0]})
df = DataProcessor.apply_averages_cleaning(
data_to_clean=df,
cleaning_data=cleaning_averages,
cols_to_merge_on=COLUMNS_TO_MERGE_ON
)
data_by_urpn = []
for uprn, property_data in df.groupby("UPRN", observed=True):
@ -204,7 +418,9 @@ def app():
fixed_data = {}
# If a property has changed building type, we can ignore the epc rating i.e. this should be 1 unique row
if any(property_data[MANDATORY_FIXED_FEATURES].nunique() > 1):
if any(property_data[MANDATORY_FIXED_FEATURES].nunique() > 1) or (
pd.isnull(property_data[MANDATORY_FIXED_FEATURES]).sum().sum() > 0
):
continue
# Take the latest row for both the LATEST_FEILDS and MANDATORY FIELDS
@ -213,29 +429,22 @@ def app():
property_data[MANDATORY_FIXED_FEATURES].iloc[-1].to_dict()
)
# Extract the columns that are not all None
modified_property_data = DataProcessor.apply_averages_cleaning(
data_to_clean=property_data,
cleaning_data=cleaning_averages,
cols_to_merge_on=COLUMNS_TO_MERGE_ON
)
# Combine all fields together
fixed_data.update(mandatory_field_data)
fixed_data.update(latest_field_data)
# We include the lodgement date here as we probably need to factor time into the
# model, since EPC standards and rigour have changed over time
variable_data = modified_property_data[
variable_data = property_data[
COMPONENT_FEATURES + ["LODGEMENT_DATE", RDSAP_RESPONSE, HEAT_DEMAND_RESPONSE, CARBON_RESPONSE]
]
# Note: we look at changes between subsequent EPCS, however we could look at other permutations
# e.g. first vs second, second vs third and also first vs third
property_model_data = []
for idx in range(0, modified_property_data.shape[0] - 1):
for idx in range(0, property_data.shape[0] - 1):
if idx >= modified_property_data.shape[0] - 1:
if idx >= property_data.shape[0] - 1:
break
earliest_record = variable_data.iloc[idx]
@ -289,6 +498,7 @@ def app():
data_by_urpn.extend(property_model_data)
data_by_urpn_df = pd.DataFrame(data_by_urpn)
# Add some temporal features - we look at the days from the standard starting point in time
# for the starting and ending date so all records are from a fixed point
data_by_urpn_df["DAYS_TO_STARTING"] = (
@ -298,9 +508,7 @@ def app():
pd.to_datetime(data_by_urpn_df["LODGEMENT_DATE_ENDING"]) - pd.to_datetime(EARLIEST_EPC_DATE)
).dt.days
# TODO: We need to pre-process the data. For instance, rather than using static for roofs, walls and
# floors, we may want to use the U-value. We may also want to handle the (assumed) tags
# within descriptions
data_by_urpn_df = data_by_urpn_df.drop(columns=["LODGEMENT_DATE_STARTING", "LODGEMENT_DATE_ENDING"])
# We look for key building fabric features that have changed from one EPC to the next.
# if, for example, we see that a home has gone from being a cavity wall to a solid wall, we
@ -308,8 +516,28 @@ def app():
# is low
# We also replace descriptions with their cleaned variants
if pd.isnull(data_by_urpn_df).sum().sum():
raise ValueError("Null values found in dataset")
data_by_urpn_df = process_and_prune_desriptions(data_by_urpn_df, cleaned_lookup)
# Apply u-values
for col in ["walls_clean_description", "walls_clean_description_ENDING"]:
data_by_urpn_df[col] = data_by_urpn_df[col].str.replace("(assumed)", "").str.rstrip()
data_by_urpn_df = make_uvalues(data_by_urpn_df).drop(
columns=["walls_clean_description", "walls_clean_description_ENDING"]
)
# TODO: For some of the features that we clean, we have either a true, false or possibly null value
# Those nulls should be False. clean_missings_after_description_process handles this but shouldn't
# need to
data_by_urpn_df = clean_missings_after_description_process(data_by_urpn_df)
if pd.isnull(data_by_urpn_df).sum().sum():
raise ValueError("Null values found in dataset after process_and_prune_desriptions")
dataset.append(data_by_urpn_df)
cleaning_averages["LOCAL_AUTHORITY"] = df["LOCAL_AUTHORITY"].values[0]
@ -324,10 +552,13 @@ def app():
)
output = pd.concat(dataset)
output = DataProcessor.difference_data(output)
save_dataframe_to_s3_parquet(
df=output,
bucket_name="retrofit-data-dev",
file_key="sap_change_model/dataset.parquet",
file_key="sap_change_model/dataset_new_not_diff.parquet",
)

View file

@ -1,8 +1,8 @@
from core.Logger import logger
from model_data.simulation_system.core.Logger import logger
import argparse
import pandas as pd
from pathlib import Path
from core.Settings import RANDOM_SEED, TRAIN_AND_VALIDATION_DATA_NAME, TEST_DATA_NAME
from model_data.simulation_system.core.Settings import RANDOM_SEED, TRAIN_AND_VALIDATION_DATA_NAME, TEST_DATA_NAME
def ingest_arguments() -> argparse.Namespace:
@ -96,7 +96,6 @@ def main(
if __name__ == "__main__":
logger.info("--- Generate test data pipeline ---")
args = ingest_arguments()

View file

@ -366,4 +366,15 @@ clean_floor_cases = [
{'original_description': 'I ofod heb ei wresogi, dim inswleiddio (rhagdybiaeth)', 'thermal_transmittance': None,
'thermal_transmittance_unit': None, 'is_assumed': True, 'is_to_unheated_space': True, 'is_to_external_air': False,
'is_suspended': False, 'is_solid': False, 'insulation_thickness': 'none', "another_property_below": False},
{'original_description': "Average thermal transmittance 1.10 W/m+é-¦K", 'thermal_transmittance': 1.1,
'thermal_transmittance_unit': 'w/m+é-¦k', 'is_assumed': False,
'is_to_unheated_space': False, 'is_to_external_air': False, 'is_suspended': False, 'is_solid': False,
'another_property_below': False, 'insulation_thickness': None},
{
"original_description": "Trawsyriannedd thermol cyfartalog 0.27 W/m-¦K", 'thermal_transmittance': 0.27,
'thermal_transmittance_unit': 'w/m-¦k', 'is_assumed': False,
'is_to_unheated_space': False, 'is_to_external_air': False, 'is_suspended': False, 'is_solid': False,
'another_property_below': False, 'insulation_thickness': None
}
]

View file

@ -35,4 +35,5 @@ test_cases = [
{'original_description': 'Dim goleuadau ynni-isel', 'low_energy_proportion': 0},
{'original_description': 'Excellent lighting efficiency', 'low_energy_proportion': 1},
{'original_description': "Goleuadau ynni-isel ym mhob un o'r mannau gosod", 'low_energy_proportion': 1},
{'original_description': "Goleuadau ynni-isel mewn 17% o'r mannau gosod", 'low_energy_proportion': 0.17},
]

View file

@ -1639,4 +1639,17 @@ mainheat_cases = [
'has_electricaire': False, 'has_assumed_for_most_rooms': False, 'has_underfloor_heating': False,
"has_electric_heat_pumps": False,
"has_micro-cogeneration": False},
{'original_description': 'Boiler and radiators, nwy prif gyflenwad, mains gas', 'has_radiators': True,
'has_fan_coil_units': False,
'has_pipes_in_screed_above_insulation': False, 'has_pipes_in_insulated_timber_floor': False,
'has_pipes_in_concrete_slab': False, 'has_boiler': True, 'has_air_source_heat_pump': False,
'has_room_heaters': False, 'has_electric_storage_heaters': False, 'has_warm_air': False,
'has_electric_underfloor_heating': False, 'has_electric_ceiling_heating': False, 'has_community_scheme': False,
'has_ground_source_heat_pump': False, 'has_no_system_present': False, 'has_portable_electric_heaters': False,
'has_water_source_heat_pump': False, 'has_electric': False, 'has_mains_gas': True, 'has_wood_logs': False,
'has_coal': False, 'has_oil': False, 'has_wood_pellets': False, 'has_anthracite': False,
'has_dual_fuel_mineral_and_wood': False, 'has_smokeless_fuel': False, 'has_lpg': False, 'has_assumed': False,
'has_electricaire': False, 'has_assumed_for_most_rooms': False, 'has_underfloor_heating': False,
"has_electric_heat_pumps": False,
"has_micro-cogeneration": False},
]

View file

@ -16,7 +16,8 @@ mainheat_control_cases = [
{'original_description': 'Charging system linked to use of community heating, programmer and TRVs',
'thermostatic_control': None, 'charging_system': 'charging system', 'switch_system': 'programmer',
'no_control': None, 'dhw_control': None, 'community_heating': 'use of community heating',
'multiple_room_thermostats': False, 'auxiliary_systems': None, 'trvs': 'trvs'}, {
'multiple_room_thermostats': False, 'auxiliary_systems': None, 'trvs': 'trvs'},
{
'original_description': 'Charging system linked to use of community heating, programmer and at least two room '
'stats',
'thermostatic_control': None, 'charging_system': 'charging system', 'switch_system': 'programmer',
@ -38,10 +39,11 @@ mainheat_control_cases = [
{'original_description': 'Controls for high heat retention storage heaters', 'thermostatic_control': None,
'charging_system': 'high heat retention storage heaters', 'switch_system': None, 'no_control': None,
'dhw_control': None, 'community_heating': None, 'multiple_room_thermostats': False, 'auxiliary_systems': None,
'trvs': None}, {'original_description': 'Flat rate charging, TRVs', 'thermostatic_control': None,
'charging_system': 'flat rate charging', 'switch_system': None, 'no_control': None,
'dhw_control': None, 'community_heating': None, 'multiple_room_thermostats': False,
'auxiliary_systems': None, 'trvs': 'trvs'},
'trvs': None},
{'original_description': 'Flat rate charging, TRVs', 'thermostatic_control': None,
'charging_system': 'flat rate charging', 'switch_system': None, 'no_control': None,
'dhw_control': None, 'community_heating': None, 'multiple_room_thermostats': False,
'auxiliary_systems': None, 'trvs': 'trvs'},
{'original_description': 'Flat rate charging, no thermostatic control of room temperature',
'thermostatic_control': None, 'charging_system': 'flat rate charging', 'switch_system': None,
'no_control': 'no thermostatic control', 'dhw_control': None, 'community_heating': None,
@ -213,4 +215,42 @@ mainheat_control_cases = [
'thermostatic_control': 'room thermostat', 'charging_system': 'charging system', 'switch_system': 'programmer',
'no_control': None, 'dhw_control': None, 'community_heating': 'use of community heating',
'multiple_room_thermostats': False, 'auxiliary_systems': None, 'trvs': None},
{'original_description': 'System dalu wediGÇÖi chysylltu +ó defnyddio gwres cymunedol, rhaglennydd a TRVs',
'thermostatic_control': None, 'charging_system': 'charging system', 'switch_system': 'programmer',
'no_control': None, 'dhw_control': None, 'community_heating': 'use of community heating',
'multiple_room_thermostats': False, 'auxiliary_systems': None, 'trvs': 'trvs'},
{'original_description': 'System dalu wediGÇÖi chysylltu +ó defnyddio gwres cymunedol, TRVs',
'thermostatic_control': None,
'charging_system': 'charging system', 'switch_system': None, 'no_control': None, 'dhw_control': None,
'community_heating': 'use of community heating', 'multiple_room_thermostats': False, 'auxiliary_systems': None,
'trvs': 'trvs'},
{'original_description': 'Single rate heating, TRVs',
'thermostatic_control': None, 'charging_system': None,
'switch_system': None,
'no_control': None, 'dhw_control': None, 'community_heating': None, 'multiple_room_thermostats': False,
'auxiliary_systems': None, 'trvs': 'trvs', 'rate_control': 'single rate heating'},
{'original_description': 'T+-ól un gyfradd, TRVs',
'thermostatic_control': None, 'charging_system': None,
'switch_system': None,
'no_control': None, 'dhw_control': None, 'community_heating': None, 'multiple_room_thermostats': False,
'auxiliary_systems': None, 'trvs': 'trvs', 'rate_control': 'single rate heating'},
{'original_description': 'Single rate heating, programmer, TRVs',
'thermostatic_control': None, 'charging_system': None,
'switch_system': 'programmer',
'no_control': None, 'dhw_control': None, 'community_heating': None, 'multiple_room_thermostats': False,
'auxiliary_systems': None, 'trvs': 'trvs', 'rate_control': 'single rate heating'},
{'original_description': 'T+ól un gyfradd, rhaglennydd a TRVs',
'thermostatic_control': None, 'charging_system': None,
'switch_system': 'programmer',
'no_control': None, 'dhw_control': None, 'community_heating': None, 'multiple_room_thermostats': False,
'auxiliary_systems': None, 'trvs': 'trvs', 'rate_control': 'single rate heating'},
{'original_description': 'T+ól un gyfradd, TRVs',
'thermostatic_control': None, 'charging_system': None,
'switch_system': None,
'no_control': None, 'dhw_control': None, 'community_heating': None, 'multiple_room_thermostats': False,
'auxiliary_systems': None, 'trvs': 'trvs', 'rate_control': 'single rate heating'},
{'original_description': 'TRVs a falf osgoi', 'thermostatic_control': None,
'charging_system': None,
'switch_system': None, 'no_control': None, 'dhw_control': None, 'community_heating': None,
'multiple_room_thermostats': False, 'auxiliary_systems': 'bypass', 'trvs': 'trvs'},
]

View file

@ -394,4 +394,8 @@ clean_roof_test_cases = [
'thermal_transmittance_unit': None, 'is_pitched': False, 'is_roof_room': True, 'is_loft': False, 'is_flat': False,
'is_thatched': False, 'is_at_rafters': False, 'is_assumed': True, 'has_dwelling_above': False, 'is_valid': True,
'insulation_thickness': 'none'},
{'original_description': 'Average thermal transmittance 0.80 W/m+é-¦K', 'thermal_transmittance': 0.8,
'thermal_transmittance_unit': 'w/m+é-¦k', 'is_pitched': False, 'is_roof_room': False,
'is_loft': False, 'is_flat': False, 'is_thatched': False, 'is_at_rafters': False, 'is_assumed': False,
'has_dwelling_above': False, 'is_valid': True, 'insulation_thickness': None}
]

View file

@ -690,5 +690,194 @@ wall_cases = [
'thermal_transmittance_unit': None, 'is_cavity_wall': False, 'is_filled_cavity': False, 'is_solid_brick': True,
'is_system_built': False, 'is_timber_frame': False, 'is_granite_or_whinstone': False, 'is_as_built': True,
'is_cob': False, 'is_assumed': True, 'is_sandstone_or_limestone': False, 'insulation_thickness': 'none',
'external_insulation': False, 'internal_insulation': False}
'external_insulation': False, 'internal_insulation': False},
{'original_description': 'Average thermal transmittance 1.60 W/m+é-¦K',
'thermal_transmittance': 1.6, 'thermal_transmittance_unit': 'w/m+é-¦k', 'is_cavity_wall': False,
'is_filled_cavity': False, 'is_solid_brick': False, 'is_system_built': False, 'is_timber_frame': False,
'is_granite_or_whinstone': False, 'is_as_built': False, 'is_cob': False, 'is_assumed': False,
'is_sandstone_or_limestone': False, 'insulation_thickness': None, 'external_insulation': False,
'internal_insulation': False},
{'original_description': 'Waliau ceudod, fel yGÇÖu hadeiladwyd, inswleiddio rhannol (rhagdybiaeth)',
'thermal_transmittance': None,
'thermal_transmittance_unit': None, 'is_cavity_wall': True, 'is_filled_cavity': False, 'is_solid_brick': False,
'is_system_built': False, 'is_timber_frame': False, 'is_granite_or_whinstone': False, 'is_as_built': True,
'is_cob': False, 'is_assumed': True, 'is_sandstone_or_limestone': False, 'insulation_thickness': 'below average',
'external_insulation': False, 'internal_insulation': False},
{'original_description': 'Waliau ceudod, fel yGÇÖu hadeiladwyd, dim inswleiddio (rhagdybiaeth)',
'thermal_transmittance': None,
'thermal_transmittance_unit': None, 'is_cavity_wall': True, 'is_filled_cavity': False, 'is_solid_brick': False,
'is_system_built': False, 'is_timber_frame': False, 'is_granite_or_whinstone': False, 'is_as_built': True,
'is_cob': False, 'is_assumed': True, 'is_sandstone_or_limestone': False, 'insulation_thickness': 'none',
'external_insulation': False, 'internal_insulation': False},
{'original_description': 'Tywodfaen, fel yGÇÖu hadeiladwyd, dim inswleiddio (rhagdybiaeth)',
'thermal_transmittance': None,
'thermal_transmittance_unit': None, 'is_cavity_wall': False, 'is_filled_cavity': False, 'is_solid_brick': False,
'is_system_built': False, 'is_timber_frame': False, 'is_granite_or_whinstone': False, 'is_as_built': True,
'is_cob': False, 'is_assumed': True, 'is_sandstone_or_limestone': True, 'insulation_thickness': 'none',
'external_insulation': False, 'internal_insulation': False},
{'original_description': 'Average thermal transmittance 1 W/m-¦K', 'thermal_transmittance': 1,
'thermal_transmittance_unit': 'w/m-¦k', 'is_cavity_wall': False, 'is_filled_cavity': False,
'is_solid_brick': False, 'is_system_built': False, 'is_timber_frame': False, 'is_granite_or_whinstone': False,
'is_as_built': False, 'is_cob': False, 'is_assumed': False, 'is_sandstone_or_limestone': False,
'insulation_thickness': None, 'external_insulation': False, 'internal_insulation': False},
{'original_description': 'Waliau ceudod, ceudod wediGÇÖi lenwi', 'thermal_transmittance': None,
'thermal_transmittance_unit': None, 'is_cavity_wall': True, 'is_filled_cavity': True, 'is_solid_brick': False,
'is_system_built': False, 'is_timber_frame': False, 'is_granite_or_whinstone': False, 'is_as_built': False,
'is_cob': False, 'is_assumed': False, 'is_sandstone_or_limestone': False, 'insulation_thickness': None,
'external_insulation': False, 'internal_insulation': False},
{'original_description': 'Waliau ceudod, fel yGÇÖu hadeiladwyd, wediGÇÖu hinswleiddio (rhagdybiaeth)',
'thermal_transmittance': None,
'thermal_transmittance_unit': None, 'is_cavity_wall': True, 'is_filled_cavity': False, 'is_solid_brick': False,
'is_system_built': False, 'is_timber_frame': False, 'is_granite_or_whinstone': False, 'is_as_built': True,
'is_cob': False, 'is_assumed': True, 'is_sandstone_or_limestone': False, 'insulation_thickness': 'average',
'external_insulation': False, 'internal_insulation': False},
{'original_description': 'Gwenithfaen neu risgraig, fel yGÇÖu hadeiladwyd, dim inswleiddio (rhagdybiaeth)',
'thermal_transmittance': None,
'thermal_transmittance_unit': None, 'is_cavity_wall': False, 'is_filled_cavity': False, 'is_solid_brick': False,
'is_system_built': False, 'is_timber_frame': False, 'is_granite_or_whinstone': True, 'is_as_built': True,
'is_cob': False, 'is_assumed': True, 'is_sandstone_or_limestone': False, 'insulation_thickness': 'none',
'external_insulation': False, 'internal_insulation': False},
{'original_description': 'Waliau ceudod,', 'thermal_transmittance': None, 'thermal_transmittance_unit': None,
'is_cavity_wall': True,
'is_filled_cavity': False, 'is_solid_brick': False, 'is_system_built': False, 'is_timber_frame': False,
'is_granite_or_whinstone': False, 'is_as_built': True, 'is_cob': False, 'is_assumed': False,
'is_sandstone_or_limestone': False, 'insulation_thickness': 'none', 'external_insulation': False,
'internal_insulation': False},
{'original_description': 'Ffr+óm bren, fel yGÇÖu hadeiladwyd, wediGÇÖu hinswleiddio (rhagdybiaeth)',
'thermal_transmittance': None,
'thermal_transmittance_unit': None, 'is_cavity_wall': False, 'is_filled_cavity': False, 'is_solid_brick': False,
'is_system_built': False, 'is_timber_frame': True, 'is_granite_or_whinstone': False, 'is_as_built': True,
'is_cob': False, 'is_assumed': True, 'is_sandstone_or_limestone': False, 'insulation_thickness': 'average',
'external_insulation': False, 'internal_insulation': False},
{'original_description': 'Granite or whinstone, with external insulation', 'thermal_transmittance': None,
'thermal_transmittance_unit': None, 'is_cavity_wall': False, 'is_filled_cavity': False, 'is_solid_brick': False,
'is_system_built': False, 'is_timber_frame': False, 'is_granite_or_whinstone': True, 'is_as_built': False,
'is_cob': False, 'is_assumed': False, 'is_sandstone_or_limestone': False, 'insulation_thickness': 'average',
'external_insulation': True, 'internal_insulation': False},
{'original_description': 'Gwenithfaen neu risgraig, gydag inswleiddio allanol', 'thermal_transmittance': None,
'thermal_transmittance_unit': None, 'is_cavity_wall': False, 'is_filled_cavity': False, 'is_solid_brick': False,
'is_system_built': False, 'is_timber_frame': False, 'is_granite_or_whinstone': True, 'is_as_built': False,
'is_cob': False, 'is_assumed': False, 'is_sandstone_or_limestone': False, 'insulation_thickness': 'average',
'external_insulation': True, 'internal_insulation': False},
{'original_description': 'WediGÇÖu hadeiladu yn +¦l system, fel yGÇÖu hadeiladwyd, dim inswleiddio (rhagdybiaeth)',
'thermal_transmittance': None,
'thermal_transmittance_unit': None, 'is_cavity_wall': False, 'is_filled_cavity': False, 'is_solid_brick': False,
'is_system_built': True, 'is_timber_frame': False, 'is_granite_or_whinstone': False, 'is_as_built': True,
'is_cob': False, 'is_assumed': True, 'is_sandstone_or_limestone': False, 'insulation_thickness': 'none',
'external_insulation': False, 'internal_insulation': False},
{'original_description': 'Sandstone or limestone, with internal insulation', 'thermal_transmittance': None,
'thermal_transmittance_unit': None, 'is_cavity_wall': False, 'is_filled_cavity': False, 'is_solid_brick': False,
'is_system_built': False, 'is_timber_frame': False, 'is_granite_or_whinstone': False, 'is_as_built': False,
'is_cob': False, 'is_assumed': False, 'is_sandstone_or_limestone': True, 'insulation_thickness': 'average',
'external_insulation': False, 'internal_insulation': True},
{'original_description': 'Sandstone or limestone, with external insulation', 'thermal_transmittance': None,
'thermal_transmittance_unit': None, 'is_cavity_wall': False, 'is_filled_cavity': False, 'is_solid_brick': False,
'is_system_built': False, 'is_timber_frame': False, 'is_granite_or_whinstone': False, 'is_as_built': False,
'is_cob': False, 'is_assumed': False, 'is_sandstone_or_limestone': True, 'insulation_thickness': 'average',
'external_insulation': True, 'internal_insulation': False},
{'original_description': 'Waliau ceudod, ynysydd allanol a llenwi ceudod', 'thermal_transmittance': None,
'thermal_transmittance_unit': None, 'is_cavity_wall': True, 'is_filled_cavity': True, 'is_solid_brick': False,
'is_system_built': False, 'is_timber_frame': False, 'is_granite_or_whinstone': False, 'is_as_built': False,
'is_cob': False, 'is_assumed': False, 'is_sandstone_or_limestone': False, 'insulation_thickness': 'average',
'external_insulation': True, 'internal_insulation': False},
{'original_description': 'Gwenithfaen neu risgraig, gydag inswleiddio mewnol', 'thermal_transmittance': None,
'thermal_transmittance_unit': None, 'is_cavity_wall': False, 'is_filled_cavity': False, 'is_solid_brick': False,
'is_system_built': False, 'is_timber_frame': False, 'is_granite_or_whinstone': True, 'is_as_built': False,
'is_cob': False, 'is_assumed': False, 'is_sandstone_or_limestone': False, 'insulation_thickness': 'average',
'external_insulation': False, 'internal_insulation': True},
{'original_description': 'Ffr+óm bren, fel yGÇÖu hadeiladwyd, inswleiddio rhannol (rhagdybiaeth)',
'thermal_transmittance': None,
'thermal_transmittance_unit': None, 'is_cavity_wall': False, 'is_filled_cavity': False, 'is_solid_brick': False,
'is_system_built': False, 'is_timber_frame': True, 'is_granite_or_whinstone': False, 'is_as_built': True,
'is_cob': False, 'is_assumed': True, 'is_sandstone_or_limestone': False, 'insulation_thickness': 'below average',
'external_insulation': False, 'internal_insulation': False},
{
'original_description': 'WediGÇÖu hadeiladu yn +¦l system, fel yGÇÖu hadeiladwyd, wediGÇÖu hinswleiddio ('
'rhagdybiaeth)',
'thermal_transmittance': None,
'thermal_transmittance_unit': None, 'is_cavity_wall': False, 'is_filled_cavity': False, 'is_solid_brick': False,
'is_system_built': True, 'is_timber_frame': False, 'is_granite_or_whinstone': False, 'is_as_built': True,
'is_cob': False, 'is_assumed': True, 'is_sandstone_or_limestone': False, 'insulation_thickness': 'average',
'external_insulation': False, 'internal_insulation': False},
{'original_description': 'WediGÇÖu hadeiladu yn +¦l system, gydag inswleiddio allanol',
'thermal_transmittance': None,
'thermal_transmittance_unit': None, 'is_cavity_wall': False, 'is_filled_cavity': False, 'is_solid_brick': False,
'is_system_built': True, 'is_timber_frame': False, 'is_granite_or_whinstone': False, 'is_as_built': False,
'is_cob': False, 'is_assumed': False, 'is_sandstone_or_limestone': False, 'insulation_thickness': 'average',
'external_insulation': True, 'internal_insulation': False},
{'original_description': 'Briciau solet, gydag inswleiddio mewnol', 'thermal_transmittance': None,
'thermal_transmittance_unit': None, 'is_cavity_wall': False, 'is_filled_cavity': False, 'is_solid_brick': True,
'is_system_built': False, 'is_timber_frame': False, 'is_granite_or_whinstone': False, 'is_as_built': False,
'is_cob': False, 'is_assumed': False, 'is_sandstone_or_limestone': False, 'insulation_thickness': 'average',
'external_insulation': False, 'internal_insulation': True},
{
'original_description': 'WediGÇÖu hadeiladu yn +¦l system, fel yGÇÖu hadeiladwyd, inswleiddio rhannol ('
'rhagdybiaeth)',
'thermal_transmittance': None,
'thermal_transmittance_unit': None, 'is_cavity_wall': False, 'is_filled_cavity': False, 'is_solid_brick': False,
'is_system_built': True, 'is_timber_frame': False, 'is_granite_or_whinstone': False, 'is_as_built': True,
'is_cob': False, 'is_assumed': True, 'is_sandstone_or_limestone': False,
'insulation_thickness': 'below average',
'external_insulation': False, 'internal_insulation': False},
{'original_description': 'Ffr+óm bren, fel yGÇÖu hadeiladwyd, dim inswleiddio (rhagdybiaeth)',
'thermal_transmittance': None,
'thermal_transmittance_unit': None, 'is_cavity_wall': False, 'is_filled_cavity': False, 'is_solid_brick': False,
'is_system_built': False, 'is_timber_frame': True, 'is_granite_or_whinstone': False, 'is_as_built': True,
'is_cob': False, 'is_assumed': True, 'is_sandstone_or_limestone': False, 'insulation_thickness': 'none',
'external_insulation': False, 'internal_insulation': False},
{'original_description': 'Tywodfaen, gydag inswleiddio allanol', 'thermal_transmittance': None,
'thermal_transmittance_unit': None, 'is_cavity_wall': False, 'is_filled_cavity': False, 'is_solid_brick': False,
'is_system_built': False, 'is_timber_frame': False, 'is_granite_or_whinstone': False, 'is_as_built': False,
'is_cob': False, 'is_assumed': False, 'is_sandstone_or_limestone': True, 'insulation_thickness': 'average',
'external_insulation': True, 'internal_insulation': False},
{'original_description': 'Waliau ceudod, gydag inswleiddio allanol', 'thermal_transmittance': None,
'thermal_transmittance_unit': None, 'is_cavity_wall': True, 'is_filled_cavity': False, 'is_solid_brick': False,
'is_system_built': False, 'is_timber_frame': False, 'is_granite_or_whinstone': False, 'is_as_built': False,
'is_cob': False, 'is_assumed': False, 'is_sandstone_or_limestone': False, 'insulation_thickness': 'average',
'external_insulation': True, 'internal_insulation': False},
{'original_description': 'Briciau solet, gydag inswleiddio allanol', 'thermal_transmittance': None,
'thermal_transmittance_unit': None, 'is_cavity_wall': False, 'is_filled_cavity': False, 'is_solid_brick': True,
'is_system_built': False, 'is_timber_frame': False, 'is_granite_or_whinstone': False, 'is_as_built': False,
'is_cob': False, 'is_assumed': False, 'is_sandstone_or_limestone': False, 'insulation_thickness': 'average',
'external_insulation': True, 'internal_insulation': False},
{'original_description': 'Cob, with external insulation', 'thermal_transmittance': None,
'thermal_transmittance_unit': None,
'is_cavity_wall': False, 'is_filled_cavity': False, 'is_solid_brick': False, 'is_system_built': False,
'is_timber_frame': False, 'is_granite_or_whinstone': False, 'is_as_built': False, 'is_cob': True,
'is_assumed': False,
'is_sandstone_or_limestone': False, 'insulation_thickness': 'average', 'external_insulation': True,
'internal_insulation': False},
{'original_description': 'Co with external insulation', 'thermal_transmittance': None,
'thermal_transmittance_unit': None,
'is_cavity_wall': False, 'is_filled_cavity': False, 'is_solid_brick': False, 'is_system_built': False,
'is_timber_frame': False, 'is_granite_or_whinstone': False, 'is_as_built': False, 'is_cob': True,
'is_assumed': False,
'is_sandstone_or_limestone': False, 'insulation_thickness': 'average', 'external_insulation': True,
'internal_insulation': False},
{'original_description': 'Cowith external insulation', 'thermal_transmittance': None,
'thermal_transmittance_unit': None,
'is_cavity_wall': False, 'is_filled_cavity': False, 'is_solid_brick': False, 'is_system_built': False,
'is_timber_frame': False, 'is_granite_or_whinstone': False, 'is_as_built': False, 'is_cob': True,
'is_assumed': False,
'is_sandstone_or_limestone': False, 'insulation_thickness': 'average', 'external_insulation': True,
'internal_insulation': False},
{'original_description': 'Sandstone, as built, no insulation (assumed)', 'thermal_transmittance': None,
'thermal_transmittance_unit': None, 'is_cavity_wall': False, 'is_filled_cavity': False, 'is_solid_brick': False,
'is_system_built': False, 'is_timber_frame': False, 'is_granite_or_whinstone': False, 'is_as_built': True,
'is_cob': False, 'is_assumed': True, 'is_sandstone_or_limestone': True, 'insulation_thickness': 'none',
'external_insulation': False, 'internal_insulation': False},
{'original_description': 'Sandstone or limestone, as built, insulated (assumed)', 'thermal_transmittance': None,
'thermal_transmittance_unit': None, 'is_cavity_wall': False, 'is_filled_cavity': False, 'is_solid_brick': False,
'is_system_built': False, 'is_timber_frame': False, 'is_granite_or_whinstone': False, 'is_as_built': True,
'is_cob': False, 'is_assumed': True, 'is_sandstone_or_limestone': True, 'insulation_thickness': 'average',
'external_insulation': False, 'internal_insulation': False},
{
'original_description': 'Park home wall, as built', 'thermal_transmittance': None,
'thermal_transmittance_unit': None, 'is_cavity_wall': False, 'is_filled_cavity': False,
'is_solid_brick': False, 'is_system_built': False, 'is_timber_frame': False, 'is_granite_or_whinstone': False,
'is_as_built': True, 'is_cob': False, 'is_assumed': False, 'is_sandstone_or_limestone': False,
'insulation_thickness': None, 'external_insulation': False, 'internal_insulation': False,
'is_park_home': True
}
]

View file

@ -56,3 +56,17 @@ class TestLightingAttributes:
del expected_result["original_description"]
result = LightingAttributes(test_case['original_description'], averages).process()
assert sorted(result.items()) == sorted(expected_result.items())
def test_regex_translations(self):
"""
Some of the regex translations were falling through the net, though we were pulling out the percentages
so we just make sure we're translating correctly
"""
init1 = LightingAttributes("Goleuadau ynni-isel mewn 17% o'r mannau gosod", [])
assert init1.description == 'low energy lighting in 17% of fixed outlets'
init2 = LightingAttributes("Goleuadau ynni-isel mewn 60% oGÇÖr mannau gosod", [])
assert init2.description == 'low energy lighting in 60% of fixed outlets'

View file

@ -1,6 +1,4 @@
import pytest
import pickle
from model_data.EpcClean import EpcClean
from pathlib import Path
from model_data.tests.test_data.test_roof_attributes_cases import clean_roof_test_cases
from model_data.epc_attributes.RoofAttributes import RoofAttributes

View file

@ -48,5 +48,11 @@ class TestWallAttributes:
expected_result = test_case.copy()
del expected_result["original_description"]
result = WallAttributes(test_case['original_description']).process()
# Some of the expected_result test data was produced before some attributes were added to the code
# base so we need to filter out some of the keys. The test is still valid
result = {k: v for k, v in result.items() if v}
expected_result = {k: v for k, v in expected_result.items() if v}
if not result:
raise Exception("Something went wong")
# Ensure the output ordering is correct
assert sorted(result.items()) == sorted(expected_result.items())

View file

@ -3,10 +3,10 @@ from typing import List
from model_data.BaseUtility import Definitions
from datatypes.enums import QuantityUnits
from backend.Property import Property
from recommendations.rdsap_tables import default_wall_thickness, age_band_data
from recommendations.recommendation_utils import (
r_value_per_mm_to_u_value, calculate_u_value_uplift, is_diminishing_returns, update_lowest_selected_u_value,
get_recommended_part, get_uvalue_estimate
get_recommended_part, estimate_perimeter, get_wall_type,
get_floor_u_value
)
@ -39,12 +39,10 @@ class FloorRecommendations(Definitions):
def __init__(
self,
property_instance: Property,
uvalue_estimates: List,
total_floor_area_group_decile: str,
materials: List,
):
self.property = property_instance
self.uvalue_estimates = uvalue_estimates
self.total_floor_area_group_decile = total_floor_area_group_decile
# For audit purposes, when estimating u values we'll store it
self.estimated_u_value = None
@ -61,80 +59,6 @@ class FloorRecommendations(Definitions):
part for part in self.materials if part["type"] == "solid_floor_insulation"
]
@staticmethod
def _estimate_perimeter(floor_area, num_rooms):
# Compute average room size based on total floor area and number of rooms
avg_room_size = floor_area / num_rooms
# Estimate total side length for square layout
total_side_length = math.sqrt(avg_room_size * num_rooms)
# Compute the perimeter
perimeter = total_side_length * 4
return perimeter
def _estimate_suspended_floor_u_value(
self, floor_area, number_of_rooms, insulation_thickness, wall_type, region, age_band
):
"""
Estimate the u-value of a suspended floor, based on RdSap methodology
Default U-value for UNINSULATED suspended floor, based on RdSAP methodology
https://files.bregroup.com/bre-co-uk-file-library-copy/filelibrary/SAP/2012/RdSAP-9.93/RdSAP_2012_9.93.pdf
w = wall thickness, where these estimates are based on the RD SAP methodology, as in table S3
A = floor area
Exposed perimeter = P
soil type clas thermal conductivity lambda_g = 1.5 W/mK
Rsi = 0.17m^2K/W
Rse = 0.04m^2K/W
Rf = 0.001 * d_ins / 0.035 where d_ins is the insulation thickness in mm
height above external ground h = 0.3m
average wind speed at 10m height v=5m/s
wind sheilding factor fw = 0.05
vantilation factor E = 0.003 m^2/m
U-value of walls to underfloor space Uw = 1.5 W/m^2K
# Calulations for suspended ground floors, example for 5 bedroom house with permiter estimated at
44.36214602563767
1) dg = w + lambda_g x (Rsi + Rse) = 0.5 + 1.5 * (0.17 + 0.04) = 0.615
2) B = 2 * A/P = 2 * 123.0 / 44.36214602563767 = 5.545268253204708
3) Ug = 2 * lambda_g * log(pi * B/dg + 1)/(pi * B + dg) =
2 * 1.5 * log(3.141592653589793 * 5.545268253204708/0.615 + 1) / (3.141592653589793 * 5.545268253204708
+ 0.615) = 0.5619604457160708
4) Ux = (2 * h * Uw /B) + (1450 * E * v * fw/B) = (2 * 0.3 * 1.5 / 5.545268253204708) + (1450 * 0.003 * 5 *
0.05/5.545268253204708) = 0.35841367978030436
5) U = 1/ (2 * Rsi + Rf + 1/(Ug + Ux)) = 1 / (2 * 0.17 + 0 + 1/(0.5619604457160708 + 0.35841367978030436)) =
0.701
"""
age_band_letter = [x for x in age_band_data if x[region] == age_band][0]["age_band"]
defaults = {
# We need width in meters
"w": [x[age_band_letter] for x in default_wall_thickness if x["type"] == wall_type][0] / 1000,
"lambda_g": 1.5,
"Rsi": 0.17,
"Rse": 0.04,
"Rf": 0.001 * insulation_thickness / 0.035,
"h": 0.3,
"v": 5,
"fw": 0.05,
"E": 0.003,
"Uw": 1.5,
}
dg = defaults["w"] + defaults["lambda_g"] * (defaults["Rsi"] + defaults["Rse"])
# P is the exposed perimeter, which we estimate as we not have this data
p = self._estimate_perimeter(floor_area=floor_area, num_rooms=number_of_rooms)
b = 2 * floor_area / p
u_g = 2 * defaults["lambda_g"] * math.log(math.pi * b / dg + 1) / (math.pi * b + dg)
u_x = (2 * defaults["h"] * defaults["Uw"] / b) + (1450 * defaults["E"] * defaults["v"] * defaults["fw"] / b)
# This is the final estimated U-value
u = 1 / (2 * defaults["Rsi"] + defaults["Rf"] + 1 / (u_g + u_x))
return u
def recommend(self):
u_value = self.property.floor["thermal_transmittance"]
is_suspended = self.property.floor["is_suspended"]
@ -169,12 +93,6 @@ class FloorRecommendations(Definitions):
# The floor is already compliant
return
# For these methods, we need to know the additional details about the property
if self.property.walls["is_solid_brick"]:
wall_type = "solid brick"
else:
raise NotImplementedError("Implement me")
total_floor_area = float(self.property.data["total-floor-area"])
number_of_rooms = float(self.property.data["number-habitable-rooms"])
@ -185,28 +103,18 @@ class FloorRecommendations(Definitions):
else:
raise NotImplementedError("Implement me")
if insulation_thickness == "none":
estimated_perimeter = estimate_perimeter(total_floor_area / num_floors, number_of_rooms / num_floors)
region_str, age_band = self.property.data["construction-age-band"].split(":")
region_str = region_str.strip()
age_band = age_band.strip()
region = self.REGION_LOOKUP[region_str]
u_value = self._estimate_suspended_floor_u_value(
floor_area=total_floor_area / num_floors,
number_of_rooms=number_of_rooms / num_floors,
insulation_thickness=0,
wall_type=wall_type,
region=region,
age_band=age_band,
)
else:
u_value = get_uvalue_estimate(
uvalue_estimates=self.uvalue_estimates,
property=self.property,
total_floor_area_group_decile=self.total_floor_area_group_decile
)
wall_type = get_wall_type(**self.property.walls)
u_value = get_floor_u_value(
floor_type="suspended" if is_suspended else "solid",
area=total_floor_area,
perimeter=estimated_perimeter,
age_band=self.property.age_band,
insulation_thickness=insulation_thickness,
wall_type=wall_type
)
self.estimated_u_value = u_value
if is_suspended:

View file

@ -7,7 +7,7 @@ from backend.Property import Property
from model_data.BaseUtility import Definitions
from recommendations.recommendation_utils import (
r_value_per_mm_to_u_value, calculate_u_value_uplift, is_diminishing_returns, update_lowest_selected_u_value,
get_recommended_part, get_uvalue_estimate
get_recommended_part, get_wall_u_value
)
@ -44,13 +44,12 @@ class WallRecommendations(Definitions):
}
def __init__(
self, property_instance: Property,
uvalue_estimates: List,
self,
property_instance: Property,
total_floor_area_group_decile: str,
materials: List
):
self.property = property_instance
self.uvalue_estimates = uvalue_estimates
self.total_floor_area_group_decile = total_floor_area_group_decile
# For audit purposes, when estimating u values we'll store it
self.estimated_u_value = None
@ -116,18 +115,15 @@ class WallRecommendations(Definitions):
raise NotImplementedError("Not implemented yet")
if is_solid_brick:
u_value = get_wall_u_value(
clean_description=self.property.walls["clean_description"],
age_band=self.property.age_band,
is_granite_or_whinstone=self.property.walls["is_granite_or_whinstone"],
is_sandstone_or_limestone=self.property.walls["is_sandstone_or_limestone"],
)
self.estimated_u_value = u_value
if insulation_thickness == "none":
# This is an estimated figure based on industry standards
u_value = self.DEFAULT_U_VALUES["solid_brick"]
else:
u_value = get_uvalue_estimate(
uvalue_estimates=self.uvalue_estimates,
property=self.property,
total_floor_area_group_decile=self.total_floor_area_group_decile
)
self.estimated_u_value = u_value
if is_solid_brick:
if u_value >= self.BUILDING_REGULATIONS_PART_L_MAX_U_VALUE:
self.find_insulation(u_value)

View file

@ -3,6 +3,7 @@ This script contains standard tables which are defined in rdsap. The most recent
based on the 2012 version, however the government is currently working on releasing a new version, and there
we will need to re-visit this
"""
import pandas as pd
age_band_data = [
{
@ -91,33 +92,373 @@ age_band_data = [
},
]
england_wales_age_band_lookup = {
f"England and Wales: %s" % x["England_Wales"]: x["age_band"] for x in age_band_data
}
########################################################################################################################
# As defined in the rdsap documentation on page 9
# https://bregroup.com/wp-content/uploads/2019/09/RdSAP_2012_9.94-20-09-2019.pdf
########################################################################################################################
default_wall_thickness = [
{
"type": "stone", "A": 500, "B": 500, "C": 500, "D": 500, "E": 450, "F": 420, "G": 420, "H": 420,
"I": 450, "J_K_L": 450
"I": 450, "J": 450, "K": 450, "L": 450
},
{
"type": "solid brick", "A": 220, "B": 220, "C": 220, "D": 220, "E": 240, "F": 250, "G": 270, "H": 270,
"I": 300, "J_K_L": 300
"I": 300, "J": 300, "K": 300, "L": 300
},
{
"type": "cavity", "A": 250, "B": 250, "C": 250, "D": 250, "E": 250, "F": 260, "G": 270, "H": 270,
"I": 300, "J_K_L": 300
"I": 300, "J": 300, "K": 300, "L": 300
},
{
"type": "timber frame", "A": 150, "B": 150, "C": 150, "D": 250, "E": 270, "F": 270, "G": 270, "H": 270,
"I": 300, "J_K_L": 300
"I": 300, "J": 300, "K": 300, "L": 300
},
{
"type": "cob", "A": 540, "B": 540, "C": 540, "D": 540, "E": 540, "F": 540, "G": 560, "H": 560, "I": 590,
"J_K_L": 590
"J": 590, "K": 590, "L": 590
},
{
"type": "system build", "A": 250, "B": 250, "C": 250, "D": 250, "E": 250, "F": 300, "G": 300, "H": 300,
"I": 300, "J_K_L": 300
"I": 300, "J": 300, "K": 300, "L": 300
},
{
"type": "park home", "A": None, "B": None, "C": None, "D": None, "E": None, "F": 50, "G": None,
"H": None, "I": 50, "J_K_L": 100
"type": "park home", "A": None, "B": None, "C": None, "D": None, "E": None, "F": 50, "G": 50,
"H": None, "I": 75, "J": 100, "K": 100, "L": 100
},
]
########################################################################################################################
# This wall u-value table is defined in the rdsap documentation on page 19
# https://bregroup.com/wp-content/uploads/2019/09/RdSAP_2012_9.94-20-09-2019.pdf
########################################################################################################################
wall_types = [
"Stone: granite or whinstone as built",
"Stone: sandstone or limestone as built",
"Solid brick as built",
"Stone/solid brick with 50 mm external or internal insulation",
"Stone/solid brick with 100 mm external or internal insulation",
"Stone/solid brick with 150 mm external or internal insulation",
"Stone/solid brick with 200 mm external or internal insulation",
"Cob as built",
"Cob with 50 mm external or internal insulation",
"Cob with 100 mm external or internal insulation",
"Cob with 150 mm external or internal insulation",
"Cob with 200 mm external or internal insulation",
"Cavity as built",
"Unfilled cavity with 50 mm external or internal insulation",
"Unfilled cavity with 100 mm external or internal insulation",
"Unfilled cavity with 150 mm external or internal insulation",
"Unfilled cavity with 200 mm external or internal insulation",
"Filled cavity",
"Filled cavity with 50 mm external or internal insulation",
"Filled cavity with 100 mm external or internal insulation",
"Filled cavity with 150 mm external or internal insulation",
"Filled cavity with 200 mm external or internal insulation",
"Timber frame as built",
"Timber frame with internal insulation",
"System build as built",
"System build with 50 mm external or internal insulation",
"System build with 100 mm external or internal insulation",
"System build with 150 mm external or internal insulation",
"System build with 200 mm external or internal insulation",
]
u_values = [
["a", "a", "a", "a", "1.7b", "1.0", "0.6", "0.60", "0.45", "0.35", "0.30", "0.28"],
["a", "a", "a", "a", "1.7b", "1.0", "0.6", "0.60", "0.45", "0.35", "0.30", "0.28"],
["1.7", "1.7", "1.7", "1.7", "1.7", "1.0", "0.60", "0.60", "0.45", "0.35", "0.30", "0.28"],
["0.55", "0.55", "0.55", "0.55", "0.55", "0.45", "0.35", "0.35", "0.30", "0.25", "0.21", "0.21"],
["0.32", "0.32", "0.32", "0.32", "0.32", "0.28", "0.24", "0.24", "0.21", "0.19", "0.17", "0.16"],
["0.23", "0.23", "0.23", "0.23", "0.23", "0.21", "0.18", "0.18", "0.17", "0.15", "0.14", "0.14"],
["0.18", "0.18", "0.18", "0.18", "0.18", "0.17", "0.15", "0.15", "0.14", "0.13", "0.12", "0.12"],
["0.80", "0.80", "0.80", "0.80", "0.80", "0.80", "0.60", "0.60", "0.45", "0.35", "0.30", "0.28"],
["0.40", "0.40", "0.40", "0.40", "0.40", "0.40", "0.35", "0.35", "0.30", "0.25", "0.21", "0.21"],
["0.26", "0.26", "0.26", "0.26", "0.26", "0.26", "0.24", "0.24", "0.21", "0.19", "0.17", "0.16"],
["0.20", "0.20", "0.20", "0.20", "0.20", "0.20", "0.18", "0.18", "0.17", "0.15", "0.14", "0.14"],
["0.16", "0.16", "0.16", "0.16", "0.16", "0.16", "0.15", "0.15", "0.14", "0.13", "0.12", "0.12"],
["1.5", "1.5", "1.5", "1.5", "1.5", "1.0", "0.60", "0.60", "0.45", "0.35", "0.30", "0.28"],
["0.53", "0.53", "0.53", "0.53", "0.53", "0.45", "0.35", "0.35", "0.30", "0.25", "0.21", "0.21"],
["0.32", "0.32", "0.32", "0.32", "0.32", "0.30", "0.24", "0.24", "0.21", "0.19", "0.17", "0.16"],
["0.23", "0.23", "0.23", "0.23", "0.23", "0.21", "0.18", "0.18", "0.17", "0.15", "0.14", "0.14"],
["0.18", "0.18", "0.18", "0.18", "0.18", "0.17", "0.15", "0.15", "0.14", "0.13", "0.12", "0.12"],
["0.7", "0.7", "0.7", "0.7", "0.7", "0.40", "0.35", "0.35", "0.45", "0.35", "0.30", "0.28"],
["0.37", "0.37", "0.37", "0.37", "0.37", "0.27", "0.25", "0.25", "0.25", "0.25", "0.21", "0.21"],
["0.25", "0.25", "0.25", "0.25", "0.25", "0.20", "0.19", "0.19", "0.19", "0.19", "0.17", "0.16"],
["0.19", "0.19", "0.19", "0.19", "0.19", "0.16", "0.15", "0.15", "0.15", "0.15", "0.14", "0.14"],
["0.16", "0.16", "0.16", "0.16", "0.16", "0.13", "0.13", "0.13", "0.13", "0.13", "0.12", "0.12"],
["2.5", "1.9", "1.9", "1.0", "0.80", "0.45", "0.40", "0.40", "0.40", "0.35", "0.30", "0.28"],
["0.60", "0.55", "0.55", "0.40", "0.40", "0.40", "0.40", "0.40", "0.40", "0.35", "0.30", "0.28"],
["2.0", "2.0", "2.0", "2.0", "1.7", "1.0", "0.60", "0.60", "0.45", "0.35", "0.30", "0.28"],
["0.60", "0.60", "0.60", "0.60", "0.55", "0.45", "0.35", "0.35", "0.30", "0.25", "0.21", "0.21"],
["0.35", "0.35", "0.35", "0.35", "0.35", "0.32", "0.24", "0.24", "0.21", "0.19", "0.17", "0.16"],
["0.25", "0.25", "0.25", "0.25", "0.25", "0.21", "0.18", "0.18", "0.17", "0.15", "0.14", "0.14"],
["0.18", "0.18", "0.18", "0.18", "0.18", "0.17", "0.15", "0.15", "0.14", "0.13", "0.12", "0.12"],
]
age_bands = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L"]
wall_uvalues = []
for i, wall_type in enumerate(wall_types):
row = {"Wall_type": wall_type}
for j, age_band in enumerate(age_bands):
row[age_band] = u_values[i][j]
wall_uvalues.append(row)
parkhome_wall_uvalues = [
{"Wall_type": "Park home as built", "F": "1.7", "G": "1.2", "I": "0.7", "K": "0.6"},
{"Wall_type": "Park home with additional insulation", "F": "s1.1.2", "G": "s1.1.2", "I": "s1.1.2",
"K": "s1.1.2"}
]
wall_uvalues.extend(parkhome_wall_uvalues)
wall_uvalues_df = pd.DataFrame(wall_uvalues)
# This maps the descriptions in the EPC data to the descriptions in the RdSAP table
epc_wall_description_map = {
############################
# Cavity wall mappings
############################
"Cavity wall, as built, partial insulation": "Filled cavity",
"Cavity wall, filled cavity": "Filled cavity",
"Cavity wall, as built, no insulation": "Cavity as built",
"Cavity wall, as built, insulated": "Unfilled cavity with 100 mm external or internal insulation",
"Cavity wall, with external insulation": "Unfilled cavity with 100 mm external or internal insulation",
"Cavity wall, insulated": "Unfilled cavity with 100 mm external or internal insulation",
'Cavity wall, partial insulation': "Unfilled cavity with 50 mm external or internal insulation",
"Cavity wall,": "Cavity as built", # General case of cavity wall without further details
"Cavity wall, filled cavity and external insulation":
"Filled cavity with 100 mm external or internal insulation",
"Cavity wall, filled cavity and internal insulation":
"Filled cavity with 100 mm external or internal insulation",
"Cavity wall, with internal insulation": "Unfilled cavity with 100 mm external or internal insulation",
"Cavity wall, no insulation": "Cavity as built",
############################
# Solid brick wall mappings
############################
"Solid brick, as built, no insulation": "Solid brick as built",
"Solid brick, with internal insulation": "Stone/solid brick with 100 mm external or internal insulation",
"Solid brick, as built, insulated": "Stone/solid brick with 100 mm external or internal insulation",
"Solid brick, with external insulation": "Stone/solid brick with 100 mm external or internal insulation",
"Solid brick, as built, partial insulation": "Stone/solid brick with 50 mm external or internal insulation",
############################
# Timber frame wall mappings
############################
# These mappings are perhaps the most dubious due to the lack of timber options in the RdSAP table
"Timber frame, as built, insulated": "Timber frame with internal insulation",
"Timber frame, with additional insulation": "Timber frame with internal insulation",
"Timber frame, as built, partial insulation": "Timber frame as built",
"Timber frame, as built, no insulation": "Timber frame as built",
"Timber frame, with external insulation": "Timber frame with internal insulation",
############################
# Sandstone/limestones wall mappings
############################
"Sandstone or limestone, as built, no insulation": "Stone: sandstone or limestone as built",
"Sandstone or limestone, with internal insulation":
"Stone/solid brick with 100 mm external or internal insulation",
"Sandstone or limestone, as built, partial insulation": "Stone/solid brick with 50 mm external or internal "
"insulation",
"Sandstone, as built, no insulation": "Stone: sandstone or limestone as built",
"Sandstone or limestone, as built, insulated":
"Stone/solid brick with 100 mm external or internal insulation",
"Sandstone, as built, insulated": "Stone/solid brick with 100 mm external or internal insulation",
"Sandstone, with internal insulation": "Stone/solid brick with 100 mm external or internal insulation",
"Sandstone or limestone, with external insulation": "Stone/solid brick with 100 mm external or internal "
"insulation",
"Sandstone, with external insulation": "Stone/solid brick with 100 mm external or internal insulation",
"Sandstone, as built, partial insulation": "Stone/solid brick with 50 mm external or internal insulation",
############################
# Granite/whinstone wall mappings
############################
"Granite or whinstone, as built, no insulation": "Stone: granite or whinstone as built",
"Granite or whinstone, with internal insulation": "Stone/solid brick with 100 mm external or internal "
"insulation",
"Granite or whinstone, as built, partial insulation": "Stone/solid brick with 50 mm external or internal "
"insulation",
"Granite or whinstone, as built, insulated": "Stone/solid brick with 100 mm external or internal "
"insulation",
"Granite or whinstone, with external insulation": "Stone/solid brick with 100 mm external or internal "
"insulation",
############################
# System built wall mappings
############################
"System built, as built, no insulation": "System build as built",
"System built, as built, partial insulation": "System build with 50 mm external or internal insulation",
"System built, with internal insulation": "System build with 100 mm external or internal insulation",
"System built, with external insulation": "System build with 100 mm external or internal insulation",
"System built, as built, insulated": "System build with 100 mm external or internal insulation",
############################
# Cob wall mappings
############################
"Cob, as built": "Cob as built",
"Cob, with external insulation": "Cob with 100 mm external or internal insulation",
"Cob, with internal insulation": "Cob with 100 mm external or internal insulation",
'Cob,': "Cob as built",
############################
# Park home mappings
############################
"Park home wall, as built": "Park home as built",
"Park home wall, with external insulation": "Park home with additional insulation",
"Park home wall, with internal insulation": "Park home with additional insulation",
}
########################################################################################################################
# These following tables define table s9 and s10 which are used to assign roofs with their assumed u-values.
# The tables can be found on pages 23 and 24 of the BRE document
# https://bregroup.com/wp-content/uploads/2019/09/RdSAP_2012_9.94-20-09-2019.pdf
########################################################################################################################
s9_list = [
{"Insulation_thickness_mm": None, "Slates_or_tiles_U_value_W_m2K": 2.3, "Thatched_roof_U_value_W_m2K": 0.35},
{"Insulation_thickness_mm": 12, "Slates_or_tiles_U_value_W_m2K": 1.5, "Thatched_roof_U_value_W_m2K": 0.32},
{"Insulation_thickness_mm": 25, "Slates_or_tiles_U_value_W_m2K": 1.0, "Thatched_roof_U_value_W_m2K": 0.30},
{"Insulation_thickness_mm": 50, "Slates_or_tiles_U_value_W_m2K": 0.68, "Thatched_roof_U_value_W_m2K": 0.25},
{"Insulation_thickness_mm": 75, "Slates_or_tiles_U_value_W_m2K": 0.50, "Thatched_roof_U_value_W_m2K": 0.22},
{"Insulation_thickness_mm": 100, "Slates_or_tiles_U_value_W_m2K": 0.40, "Thatched_roof_U_value_W_m2K": 0.20},
{"Insulation_thickness_mm": 150, "Slates_or_tiles_U_value_W_m2K": 0.30, "Thatched_roof_U_value_W_m2K": 0.17},
{"Insulation_thickness_mm": 200, "Slates_or_tiles_U_value_W_m2K": 0.21, "Thatched_roof_U_value_W_m2K": 0.14},
{"Insulation_thickness_mm": 250, "Slates_or_tiles_U_value_W_m2K": 0.17, "Thatched_roof_U_value_W_m2K": 0.12},
{"Insulation_thickness_mm": 270, "Slates_or_tiles_U_value_W_m2K": 0.16, "Thatched_roof_U_value_W_m2K": 0.12},
{"Insulation_thickness_mm": 300, "Slates_or_tiles_U_value_W_m2K": 0.14, "Thatched_roof_U_value_W_m2K": 0.11},
{"Insulation_thickness_mm": 350, "Slates_or_tiles_U_value_W_m2K": 0.12, "Thatched_roof_U_value_W_m2K": 0.10},
{"Insulation_thickness_mm": 400, "Slates_or_tiles_U_value_W_m2K": 0.11,
"Thatched_roof_U_value_W_m2K": 0.09},
]
s10_list = [
{
"Age_band": "A, B, C, D",
"Pitched_slates_or_tiles_insulation_between_joists_or_unknown": 2.3,
"Pitched_slates_or_tiles_insulation_at_rafters": 2.3,
"Flat_roof": 2.3,
"Room_in_roof_slates_or_tiles": 2.3,
"Thatched_roof": 0.35,
"Thatched_roof_room_in_roof": 0.25,
"Park_home": None
},
{
"Age_band": "E",
"Pitched_slates_or_tiles_insulation_between_joists_or_unknown": 1.5,
"Pitched_slates_or_tiles_insulation_at_rafters": 1.5,
"Flat_roof": 1.5,
"Room_in_roof_slates_or_tiles": 1.5,
"Thatched_roof": 0.35,
"Thatched_roof_room_in_roof": 0.25,
"Park_home": None
},
{
"Age_band": "F",
"Pitched_slates_or_tiles_insulation_between_joists_or_unknown": 0.68,
"Pitched_slates_or_tiles_insulation_at_rafters": 0.68,
"Flat_roof": 0.68,
"Room_in_roof_slates_or_tiles": 0.80,
"Thatched_roof": 0.35,
"Thatched_roof_room_in_roof": 0.25,
"Park_home": 1.7
},
{
"Age_band": "G",
"Pitched_slates_or_tiles_insulation_between_joists_or_unknown": 0.40,
"Pitched_slates_or_tiles_insulation_at_rafters": 0.40,
"Flat_roof": 0.40,
"Room_in_roof_slates_or_tiles": "0.50",
"Thatched_roof": 0.35,
"Thatched_roof_room_in_roof": 0.25,
"Park_home": 0.6
},
{
"Age_band": "H",
"Pitched_slates_or_tiles_insulation_between_joists_or_unknown": 0.30,
"Pitched_slates_or_tiles_insulation_at_rafters": 0.35,
"Flat_roof": 0.35,
"Room_in_roof_slates_or_tiles": 0.35,
"Thatched_roof": 0.35,
"Thatched_roof_room_in_roof": 0.25,
"Park_home": None
},
{
"Age_band": "I",
"Pitched_slates_or_tiles_insulation_between_joists_or_unknown": 0.26,
"Pitched_slates_or_tiles_insulation_at_rafters": 0.35,
"Flat_roof": 0.35,
"Room_in_roof_slates_or_tiles": 0.35,
"Thatched_roof": 0.35,
"Thatched_roof_room_in_roof": 0.25,
"Park_home": 0.35
},
{
"Age_band": "J",
"Pitched_slates_or_tiles_insulation_between_joists_or_unknown": 0.16,
"Pitched_slates_or_tiles_insulation_at_rafters": 0.20,
"Flat_roof": 0.25,
"Room_in_roof_slates_or_tiles": 0.30,
"Thatched_roof": 0.30,
"Thatched_roof_room_in_roof": 0.25,
"Park_home": None
},
{
"Age_band": "K",
"Pitched_slates_or_tiles_insulation_between_joists_or_unknown": 0.16,
"Pitched_slates_or_tiles_insulation_at_rafters": 0.20,
"Flat_roof": 0.25,
"Room_in_roof_slates_or_tiles": 0.25,
"Thatched_roof": 0.25,
"Thatched_roof_room_in_roof": 0.25,
"Park_home": 0.30
},
{
"Age_band": "L",
"Pitched_slates_or_tiles_insulation_between_joists_or_unknown": 0.16,
"Pitched_slates_or_tiles_insulation_at_rafters": 0.18,
"Flat_roof": 0.18,
"Room_in_roof_slates_or_tiles": 0.18,
"Thatched_roof": 0.18,
"Thatched_roof_room_in_roof": 0.18,
"Park_home": None
}
]
table_s9 = pd.DataFrame(s9_list)
table_s10 = pd.DataFrame(s10_list)
########################################################################################################################
# Table s11 is used for assigning the u-values of floors when the insulation thickness is unknown
# which can be found on page 25 of the BRE document
# https://bregroup.com/wp-content/uploads/2019/09/RdSAP_2012_9.94-20-09-2019.pdf
#
# The thickness values are in mm
########################################################################################################################
s11_list = [
{"Age_band": "A, B", "Floor_construction": "suspended timber", "England_Wales": 0, "Scotland": 0,
"Northern_Ireland": 0, "Park_home": 0},
{"Age_band": "C to F", "Floor_construction": "solid", "England_Wales": 0, "Scotland": 0,
"Northern_Ireland": 0, "Park_home": 0},
{"Age_band": "G", "Floor_construction": "solid", "England_Wales": 0, "Scotland": 0,
"Northern_Ireland": 0, "Park_home": 25},
{"Age_band": "H", "Floor_construction": "solid", "England_Wales": 0, "Scotland": 25,
"Northern_Ireland": 25, "Park_home": 0},
{"Age_band": "I", "Floor_construction": "solid", "England_Wales": 25, "Scotland": 50,
"Northern_Ireland": 50, "Park_home": 50},
{"Age_band": "J", "Floor_construction": "solid", "England_Wales": 75, "Scotland": 75,
"Northern_Ireland": 0, "Park_home": 0},
{"Age_band": "K", "Floor_construction": "solid", "England_Wales": 100, "Scotland": 100,
"Northern_Ireland": 100, "Park_home": 70},
{"Age_band": "L", "Floor_construction": "solid", "England_Wales": 100, "Scotland": 120,
"Northern_Ireland": 100, "Park_home": 0},
]
table_s11 = pd.DataFrame(s11_list)

View file

@ -1,7 +1,14 @@
import math
from copy import deepcopy
import pandas as pd
from backend.Property import Property
from statistics import mean
import random
from recommendations.rdsap_tables import (
epc_wall_description_map, wall_uvalues_df, default_wall_thickness, table_s9 as s9, table_s10 as s10,
table_s11 as s11
)
def r_value_per_mm_to_u_value(depth_mm: int, r_value_per_mm: float):
@ -121,54 +128,351 @@ def get_recommended_part(part, selected_depth, selected_total_cost, quantity, qu
return recommended_part
def get_uvalue_estimate(uvalue_estimates, property: Property, total_floor_area_group_decile):
def apply_formula_s_5_1_1(is_granite_or_whinstone, is_sandstone_or_limestone, age_band):
"""
Wrapper function which contains the methodology to extract a property's walls u-value estimate
when we don't have a true value and if we can't base our assumption off of the material
As the u-value table in https://bregroup.com/wp-content/uploads/2019/09/RdSAP_2012_9.94-20-09-2019.pdf
on page 19, certain u-values as indicated by an "a", should be populated using a formula as defined in section
S.5.1.1
"""
stone_wall_thickness = [x for x in default_wall_thickness if x["type"] == "stone"][0]
thickness = stone_wall_thickness["J_K_L"] if age_band in ["J", "L", "L"] else stone_wall_thickness[age_band]
if is_granite_or_whinstone:
return 3.3 - 0.002 * thickness
if is_sandstone_or_limestone:
return 3 - 0.002 * thickness
raise ValueError("This should only be called when is_granite_or_whinstone or is_sandstone_or_limestone is True")
def get_wall_u_value(clean_description, age_band, is_granite_or_whinstone, is_sandstone_or_limestone):
"""
Given some features about a wall, this function will query the wall u-value table and return the u-value
:param clean_description: Cleaned up description of the wall from the EPC data
:param age_band: age band of the property from the EPC data
:param is_granite_or_whinstone: Boolean indicating if the wall is made of granite or whinstone
:param is_sandstone_or_limestone: Boolean indicating if the wall is made of sandstone or limestone
:return:
"""
if not uvalue_estimates:
raise ValueError("No U-value estimate found for the given property - investigate")
mapped_description = epc_wall_description_map[clean_description]
# We try and filter on total_floor_area_group_decile
floor_area_filter = [
x for x in uvalue_estimates if
x["total-floor-area_group"] == total_floor_area_group_decile
]
mapped_value = wall_uvalues_df[wall_uvalues_df["Wall_type"] == mapped_description][age_band].values[0]
if not floor_area_filter:
# Take a mean of all the u-value estimates
return mean(
[x["median_thermal_transmittance"] for x in uvalue_estimates if x["median_thermal_transmittance"]]
if pd.isnull(mapped_value) and "Park home" in mapped_description:
# We don't know enough in this case so we default to 0
return 0
if mapped_value == "a":
# The rdSap documentation indicateswe should use a formula to calculate the u-value
return float(
apply_formula_s_5_1_1(
is_granite_or_whinstone=is_granite_or_whinstone,
is_sandstone_or_limestone=is_sandstone_or_limestone,
age_band=age_band
)
)
# Because of how spuriously populated the data is for number-habitable-rooms and number-heated-rooms,
# we will try and filter on these to see if we get a result
if "b" in mapped_value:
potential_uvalue = float(mapped_value.replace("b", ""))
formula_uvalue = float(apply_formula_s_5_1_1(
is_granite_or_whinstone=is_granite_or_whinstone,
is_sandstone_or_limestone=is_sandstone_or_limestone,
age_band=age_band
))
return min(potential_uvalue, formula_uvalue)
habitable_rooms_filer = [
x for x in floor_area_filter if
x["number-habitable-rooms"] == property.data["number-habitable-rooms"]
]
if mapped_value == "s1.1.2":
# We don't know enough in this case so we default to 0
return 0
if not habitable_rooms_filer:
# Take a mean of all the u-value estimates
return mean(
[x["median_thermal_transmittance"] for x in floor_area_filter if x["median_thermal_transmittance"]]
)
return float(mapped_value)
# Try perform a filter on heated rooms
heated_rooms_filter = [
x for x in habitable_rooms_filer if
x["number-heated-rooms"] == property.data["number-heated-rooms"]
]
if not heated_rooms_filter:
# Take a mean of all the u-value estimates
return mean(
[x["median_thermal_transmittance"] for x in habitable_rooms_filer if x["median_thermal_transmittance"]]
)
def get_u_value_from_s9(thickness, s9, is_loft, is_roof_room, is_thatched):
"""Get the U-value from table S9 based on the insulation thickness."""
if thickness in ["below average", "average", "above average", "none", None] or (
not is_loft and not is_roof_room
):
return None
elif thickness.endswith("+"):
thickness = int(thickness[:-1])
else:
try:
thickness = int(thickness)
except ValueError:
# If thickness is not a valid number (could be a string or None), return None
return None
return mean(
[x["median_thermal_transmittance"] for x in heated_rooms_filter if x["median_thermal_transmittance"]]
# Determine the column to refer based on the roof type
column = 'Thatched_roof_U_value_W_m2K' if is_thatched else 'Slates_or_tiles_U_value_W_m2K'
# Get the correct U-value based on the insulation thickness
return s9[s9['Insulation_thickness_mm'] >= thickness][column].iloc[0]
def get_roof_u_value(
insulation_thickness,
has_dwelling_above,
is_loft,
is_roof_room,
is_thatched,
age_band,
is_flat,
is_pitched,
is_at_rafters,
**kwargs
):
"""
Determine the U-value for a roof based on the description dictionary and age band.
We use table s9 is the insulation thickness was measured, otherwise we use table s10.
The methodology for this process can be found in page 23 of the BRE rdsap 2012 document found here:
https://bregroup.com/wp-content/uploads/2019/09/RdSAP_2012_9.94-20-09-2019.pdf
Parameters:
insulation_thickness (str): contains description of the insulation thickness - may be missing
has_dwelling_above (bool): Indicates if there is a property above
is_loft (bool): Indicates if ther oof has a loft
is_roof_room (bool): Indicates if there is a room in roof
is_thatched (bool): Indicates if the roof is thatched
is_flat (bool): Indicates if the roof is flat
is_pitched (bool): Indicates if the roof is pitched
is_at_rafters (bool): Indicates if there is insulation at the rafters of the roof
age_band (str): The age band of the property.
s9 (pd.DataFrame): The DataFrame representing table S9.
s10 (pd.DataFrame): The DataFrame representing table S10.
Returns:
float: The determined U-value.
"""
# If there is a dwelling above, the U-value is 0
if has_dwelling_above:
return 0.0
# Step 1: Try to get the U-value from table S9 based on the insulation thickness
# The conditions for using table S9 are:
# - The insulation thickness is known
# - The roof is either a loft or a roof room
# The criteria for using this table is predominately defined by insulation around joists which is predominately
# a feature of lofts and roof rooms
u_value = get_u_value_from_s9(
thickness=insulation_thickness,
s9=s9,
is_loft=is_loft,
is_roof_room=is_roof_room,
is_thatched=is_thatched,
)
if u_value is not None:
return u_value
# Step 2: If the U-value could not be determined from table S9, use table S10
# Define the columns to be used based on the description details
if is_flat:
column = 'Flat_roof'
elif is_thatched:
if is_roof_room:
column = 'Thatched_roof_room_in_roof'
else:
column = 'Thatched_roof'
elif is_roof_room:
column = 'Room_in_roof_slates_or_tiles'
elif is_pitched:
if is_at_rafters:
column = 'Pitched_slates_or_tiles_insulation_at_rafters'
else:
column = 'Pitched_slates_or_tiles_insulation_between_joists_or_unknown'
else:
# Default to pitched roof with insulation between joists or unknown
column = 'Pitched_slates_or_tiles_insulation_between_joists_or_unknown'
# Get the U-value from table S10 based on the age band and the determined column
u_value = s10.loc[s10['Age_band'].str.contains(age_band), column].values[0]
return u_value
def estimate_perimeter(floor_area, num_rooms):
"""
Uses a basic methodology to attempt to estimate perimeter. Works better for
:param floor_area: floor area of the home
:param num_rooms: number of rooms in the home
:return: estimated perimeter
"""
if floor_area < 0:
raise ValueError("Floor area cannot be negative.")
if num_rooms <= 0:
raise ValueError("Number of rooms must be greater than zero.")
# Compute average room size based on total floor area and number of rooms
avg_room_size = floor_area / num_rooms
# Estimate the side length of a square room with the average room size
avg_room_side_length = math.sqrt(avg_room_size)
# Estimate total side length assuming rooms are lined up in a row
total_side_length = avg_room_side_length * num_rooms
# Estimate the length and width of the property assuming it is rectangular
length = total_side_length / 2
width = floor_area / length
# Compute the perimeter of the property
perimeter = 2 * (length + width)
return perimeter
def get_floor_u_value(floor_type, area, perimeter, age_band, wall_type, insulation_thickness=None):
"""
Estimate the u-value of a suspended floor, based on RdSap methodology
Default U-value for UNINSULATED suspended floor, based on RdSAP methodology
https://files.bregroup.com/bre-co-uk-file-library-copy/filelibrary/SAP/2012/RdSAP-9.93/RdSAP_2012_9.93.pdf
w = wall thickness, where these estimates are based on the RD SAP methodology, as in table S3
A = floor area
Exposed perimeter = P
soil type clas thermal conductivity lambda_g = 1.5 W/mK
Rsi = 0.17m^2K/W
Rse = 0.04m^2K/W
Rf = 0.001 * d_ins / 0.035 where d_ins is the insulation thickness in mm
height above external ground h = 0.3m
average wind speed at 10m height v=5m/s
wind sheilding factor fw = 0.05
vantilation factor E = 0.003 m^2/m
U-value of walls to underfloor space Uw = 1.5 W/m^2K
# Calulations for suspended ground floors, example for 5 bedroom house with permiter estimated at
44.36214602563767
1) dg = w + lambda_g x (Rsi + Rse) = 0.5 + 1.5 * (0.17 + 0.04) = 0.615
2) B = 2 * A/P = 2 * 123.0 / 44.36214602563767 = 5.545268253204708
3) Ug = 2 * lambda_g * log(pi * B/dg + 1)/(pi * B + dg) =
2 * 1.5 * log(3.141592653589793 * 5.545268253204708/0.615 + 1) / (3.141592653589793 * 5.545268253204708
+ 0.615) = 0.5619604457160708
4) Ux = (2 * h * Uw /B) + (1450 * E * v * fw/B) = (2 * 0.3 * 1.5 / 5.545268253204708) + (1450 * 0.003 * 5 *
0.05/5.545268253204708) = 0.35841367978030436
5) U = 1/ (2 * Rsi + Rf + 1/(Ug + Ux)) = 1 / (2 * 0.17 + 0 + 1/(0.5619604457160708 + 0.35841367978030436)) =
0.701
"""
# Cleans our regularly inputted insulation thickness for usage in this function
insulation_thickness = extract_insulation_thickness(insulation_thickness)
# Define constants
lambda_g = 1.5 # thermal conductivity of soil in W/m·K
Rsi = 0.17 # in m²K/W
Rse = 0.04 # in m²K/W
lambda_ins = 0.035 # thermal conductivity of floor insulation in W/m·K
wall_thickness = [x[age_band] for x in default_wall_thickness if x["type"] == wall_type][0]
if wall_thickness is None and wall_type == "park home":
# We don't know enough and likely won't make recommendations
return 0
wall_thickness = wall_thickness / 1000
if insulation_thickness is None:
insulation_lookup = s11[s11["Age_band"].str.contains(age_band) & s11["Floor_construction"] == floor_type]
if insulation_lookup.empty:
insulation_thickness = 0
else:
insulation_thickness = insulation_lookup["England_Wales"].values[0]
# Calculate Rf for insulated floors
Rf = 0.001 * insulation_thickness / lambda_ins
# Calculate B
B = 2 * area / perimeter
if floor_type == 'solid':
# Calculate dt
dt = wall_thickness + lambda_g * (Rsi + Rf + Rse)
# Calculate U value based on dt and B
if dt < B:
U = 2 * lambda_g * math.log(math.pi * B / dt + 1) / (math.pi * B + dt)
else:
U = lambda_g / (0.457 * B + dt)
elif floor_type == 'suspended':
# Define additional constants for suspended floors
h = 0.3 # height above external ground level in meters
v = 5 # average wind speed at 10 m height in m/s
fw = 0.05 # wind shielding factor
epsilon = 0.003 # ventilation openings per m exposed perimeter in m²/m
Uw = 1.5 # U-value of walls to underfloor space in W/m²K
# Calculate dg
dg = wall_thickness + lambda_g * (Rsi + Rse)
# Calculate Ug and Ux
Ug = 2 * lambda_g * math.log(math.pi * B / dg + 1) / (math.pi * B + dg)
Ux = (2 * h * Uw / B) + (1450 * epsilon * v * fw / B)
# Calculate final U value for suspended floors
if insulation_thickness > 0:
Rf += 0.2 # adding thermal resistance of floor deck
else:
Rf = 0.2 # thermal resistance of uninsulated floor deck
U = 1 / (2 * Rsi + Rf + 1 / (Ug + Ux))
else:
raise ValueError("Invalid floor type. Acceptable values are 'solid' or 'suspended'.")
return round(U, 2) # rounding U value to two decimal places
def extract_insulation_thickness(insulation_thickness_str):
"""
Converts insulation thickness to a float
:param insulation_thickness_str:
:return:
"""
if insulation_thickness_str in ["none", "average", "below average", "above average", None]:
return None
return int(insulation_thickness_str.replace("mm", ""))
def get_wall_type(
is_cavity_wall,
is_solid_brick,
is_granite_or_whinstone,
is_sandstone_or_limestone,
is_timber_frame,
is_cob,
is_system_built,
is_park_home,
**kwargs
):
"""
Converts booleans to a string wall type, for querying the wall thickness table
:return:
"""
if is_cavity_wall:
return "cavity"
if is_solid_brick:
return "solid brick"
if is_granite_or_whinstone or is_sandstone_or_limestone:
return "stone"
if is_timber_frame:
return "timber frame"
if is_cob:
return "cob"
if is_system_built:
return "system build"
if is_park_home:
return "park home"
return None

View file

@ -0,0 +1,32 @@
floor_uvalue_test_cases = [
# Test with solid floor, no insulation
{
"floor_type": "solid",
"area": 100,
"perimeter": 40,
"age_band": "A",
"wall_type": "cavity",
"insulation_thickness": None,
"expected": 0.62,
},
# Test with suspended floor, with insulation
{
"floor_type": "suspended",
"area": 120,
"perimeter": 44,
"age_band": "B",
"wall_type": "solid brick",
"insulation_thickness": "50mm",
"expected": 0.33,
},
# Test with invalid floor type
{
"floor_type": "invalid",
"area": 100,
"perimeter": 40,
"age_band": "A",
"wall_type": "cavity",
"insulation_thickness": None,
"expected": ValueError,
},
]

View file

@ -0,0 +1,80 @@
wall_uvalue_test_cases = [
{
"clean_description": "Cavity wall, as built, partial insulation",
"age_band": "A",
"is_granite_or_whinstone": False,
"is_sandstone_or_limestone": False,
"uvalue": 0.7
},
{
"clean_description": "Cavity wall, as built, partial insulation",
"age_band": "F",
"is_granite_or_whinstone": False,
"is_sandstone_or_limestone": False,
"uvalue": 0.4
},
{
"clean_description": "Cavity wall, as built, partial insulation",
"age_band": "F",
"is_granite_or_whinstone": False,
"is_sandstone_or_limestone": False,
"uvalue": 0.4
},
{
"clean_description": "Solid brick, with internal insulation",
"age_band": "C",
"is_granite_or_whinstone": False,
"is_sandstone_or_limestone": False,
"uvalue": 0.32
},
{
"clean_description": "Solid brick, as built, no insulation",
"age_band": "C",
"is_granite_or_whinstone": False,
"is_sandstone_or_limestone": False,
"uvalue": 1.7
},
{
"clean_description": "Timber frame, as built, no insulation",
"age_band": "E",
"is_granite_or_whinstone": False,
"is_sandstone_or_limestone": False,
"uvalue": 0.8
},
{
"clean_description": "Sandstone or limestone, with external insulation",
"age_band": "E",
"is_granite_or_whinstone": False,
"is_sandstone_or_limestone": False,
"uvalue": 0.32
},
{
"clean_description": "Granite or whinstone, as built, partial insulation",
"age_band": "E",
"is_granite_or_whinstone": False,
"is_sandstone_or_limestone": False,
"uvalue": 0.55
},
{
"clean_description": "System built, as built, no insulation",
"age_band": "E",
"is_granite_or_whinstone": False,
"is_sandstone_or_limestone": False,
"uvalue": 1.7
},
{
"clean_description": "Cob, with internal insulation",
"age_band": "E",
"is_granite_or_whinstone": False,
"is_sandstone_or_limestone": False,
"uvalue": 0.26
},
{
"clean_description": "Park home wall, with internal insulation",
"age_band": "E",
"is_granite_or_whinstone": False,
"is_sandstone_or_limestone": False,
"uvalue": 0
}
]

View file

@ -8,12 +8,6 @@ from recommendations.FloorRecommendations import FloorRecommendations
# os.path.abspath(os.path.dirname(__file__)) + "/recommendations/tests/test_data/input_properties.pkl", "rb"
# ) as f:
# input_properties = pickle.load(f)
#
# with open(
# os.path.abspath(os.path.dirname(__file__)) + "/recommendations/tests/test_data/uvalue_estimates.pkl", "rb"
# ) as f:
# uvalue_estimates = pickle.load(f)
suspended_floor_insulation_parts = [
{
@ -85,13 +79,6 @@ class TestWallRecommendations:
) as f:
return pickle.load(f)
@pytest.fixture
def uvalue_estimates(self):
with open(
os.path.abspath(os.path.dirname(__file__)) + "/test_data/uvalue_estimates.pkl", "rb"
) as f:
return pickle.load(f)
@pytest.fixture
def mock_floor_rec_instance(self):
# Creating a mock instance of WallRecommendations with the necessary attributes
@ -99,27 +86,22 @@ class TestWallRecommendations:
property_mock.full_sap_epc = {"lodgement-date": "2000-01-01"} # or any date you want
property_mock.data = {"construction-age-band": "1950"} # or any other data that fits your tests
uvalue_estimates_mock = Mock()
mock_wall_rec_instance = FloorRecommendations(property_mock, uvalue_estimates_mock, "Decile 1")
mock_wall_rec_instance = FloorRecommendations(property_mock, "Decile 1", parts)
return mock_wall_rec_instance
def test_init(self, input_properties, uvalue_estimates):
def test_init(self, input_properties):
obj = FloorRecommendations(
property_instance=input_properties[0],
uvalue_estimates=uvalue_estimates,
total_floor_area_group_decile="Decile 1",
materials=parts
)
assert obj
assert obj.property
assert obj.uvalue_estimates
assert obj.total_floor_area_group_decile == "Decile 1"
def test_other_premises_below(self, input_properties, uvalue_estimates):
def test_other_premises_below(self, input_properties):
recommender = FloorRecommendations(
property_instance=input_properties[0],
uvalue_estimates=uvalue_estimates,
total_floor_area_group_decile="Decile 1",
materials=parts
)
@ -128,31 +110,32 @@ class TestWallRecommendations:
assert not recommender.recommendations
def test_suspended_no_insulation(self, input_properties, uvalue_estimates):
def test_suspended_no_insulation(self, input_properties):
"""
For a suspended floor without insulation, we use the rdsap methogology to estimate a U-value for the floor
:return:
"""
input_properties[2].floor_area = 50
input_properties[2].walls["is_park_home"] = False
input_properties[2].age_band = "A"
recommender = FloorRecommendations(
property_instance=input_properties[2],
uvalue_estimates=uvalue_estimates,
total_floor_area_group_decile="Decile 1",
materials=parts
)
assert recommender.estimated_u_value is None
recommender.recommend()
assert recommender.property.floor["is_suspended"]
assert recommender.estimated_u_value == 0.8766389420265843
assert recommender.estimated_u_value == 0.52
assert recommender.recommendations
types = {part["type"] for x in recommender.recommendations for part in x["parts"]}
assert types == {"suspended_floor_insulation"}
def test_uvalue_0_12(self, input_properties, uvalue_estimates):
def test_uvalue_0_12(self, input_properties):
"""
This is a home that doesn't have a property below but it's highly performant already and therefore
does not need floor insulation
@ -160,7 +143,6 @@ class TestWallRecommendations:
"""
recommender = FloorRecommendations(
property_instance=input_properties[3],
uvalue_estimates=uvalue_estimates,
total_floor_area_group_decile="Decile 1",
materials=parts
)
@ -171,16 +153,17 @@ class TestWallRecommendations:
assert recommender.estimated_u_value is None
assert not recommender.recommendations
def test_solid_no_insulation(self, input_properties, uvalue_estimates):
def test_solid_no_insulation(self, input_properties):
"""
:return:
"""
input_properties[4].floor_area = 100
input_properties[4].walls["is_park_home"] = False
input_properties[4].age_band = "B"
recommender = FloorRecommendations(
property_instance=input_properties[4],
uvalue_estimates=uvalue_estimates,
total_floor_area_group_decile="Decile 1",
materials=parts
)
@ -188,21 +171,20 @@ class TestWallRecommendations:
recommender.recommend()
assert not recommender.property.floor["is_suspended"]
assert recommender.property.floor["is_solid"]
assert recommender.estimated_u_value == 0.7528014214215474
assert recommender.estimated_u_value == 0.63
assert recommender.recommendations
types = {part["type"] for x in recommender.recommendations for part in x["parts"]}
assert types == {"solid_floor_insulation"}
def test_another_dwelling_below(self, input_properties, uvalue_estimates):
def test_another_dwelling_below(self, input_properties):
"""
This is another description we see when there is a property below
"""
recommender = FloorRecommendations(
property_instance=input_properties[6],
uvalue_estimates=uvalue_estimates,
total_floor_area_group_decile="Decile 1",
materials=parts
)

View file

@ -1,7 +1,10 @@
import pytest
import math
from unittest.mock import MagicMock
from recommendations import recommendation_utils
from datatypes.enums import QuantityUnits
from recommendations.tests.test_data.wall_uvalue_test_cases import wall_uvalue_test_cases
from recommendations.tests.test_data.floor_uvalue_test_cases import floor_uvalue_test_cases
class TestRecommendationUtils:
@ -43,35 +46,261 @@ class TestRecommendationUtils:
part=part, selected_depth=1, selected_total_cost=50, quantity=99, quantity_unit="m2"
) == {'depths': [1], 'estimated_cost': 50, 'quantity': 99, 'quantity_unit': QuantityUnits.m2.value}
def test_get_uvalue_estimate(self, property_mock):
uvalue_estimates = [
{
'total-floor-area_group': 'Decile 1',
'number-habitable-rooms': 3,
'number-heated-rooms': 2,
'median_thermal_transmittance': 1
},
{
'total-floor-area_group': 'Decile 1',
'number-habitable-rooms': 3,
'number-heated-rooms': 2,
'median_thermal_transmittance': 2
}
]
def test_get_roof_u_value(self):
# Test case 1: Insulation thickness is known and is_loft is True
inputs = {
'insulation_thickness': '50',
'is_loft': True,
'is_roof_room': False,
'is_thatched': False,
'has_dwelling_above': False,
'is_flat': False,
'is_pitched': True,
'is_at_rafters': False,
}
for age_band in ["A", "B", "C", "D"]:
assert recommendation_utils.get_roof_u_value(**{**inputs, "age_band": age_band}) == 0.68
assert recommendation_utils.get_uvalue_estimate(uvalue_estimates, property_mock, "Decile 1") == 1.5
def test_get_roof_u_value_case_2(self):
inputs = {
'original_description': 'Pitched, 400+ mm insulation at joists',
'clean_description': 'Pitched, 400+ mm insulation at joists',
'thermal_transmittance': None,
'thermal_transmittance_unit': None,
'is_pitched': True,
'is_roof_room': False,
'is_loft': False,
'is_flat': False,
'is_thatched': False,
'is_at_rafters': False,
'is_assumed': False,
'has_dwelling_above': False,
'is_valid': True,
'insulation_thickness': '400+',
'age_band': "J"
}
with pytest.raises(ValueError):
recommendation_utils.get_uvalue_estimate([], property_mock, "Decile 1")
u_value = recommendation_utils.get_roof_u_value(**inputs)
assert u_value == 0.16, f"Expected 0.16, but got {u_value}"
# Test with missing 'median_thermal_transmittance' key
uvalue_estimates_missing_key = [
{
'total-floor-area_group': 'Decile 1',
'number-habitable-rooms': 3,
'number-heated-rooms': 2
}
]
def test_get_roof_u_value_case_3(self):
inputs = {
'original_description': 'Room-in-roof, 200 mm insulation at rafters',
'clean_description': 'Room-in-roof, 200 mm insulation at rafters',
'thermal_transmittance': None,
'thermal_transmittance_unit': None,
'is_pitched': False,
'is_roof_room': True,
'is_loft': False,
'is_flat': False,
'is_thatched': False,
'is_at_rafters': True,
'is_assumed': False,
'has_dwelling_above': False,
'is_valid': True,
'insulation_thickness': '200',
'age_band': "J"
}
with pytest.raises(KeyError):
recommendation_utils.get_uvalue_estimate(uvalue_estimates_missing_key, property_mock, "Decile 1")
u_value = recommendation_utils.get_roof_u_value(**inputs)
assert u_value == 0.21, f"Expected 0.21, but got {u_value}"
def test_get_roof_u_value_case_4(self):
inputs = {
'original_description': 'Pitched, below average insulation',
'clean_description': 'Pitched, below average insulation',
'thermal_transmittance': None,
'thermal_transmittance_unit': None,
'is_pitched': True,
'is_roof_room': False,
'is_loft': False,
'is_flat': False,
'is_thatched': False,
'is_at_rafters': False,
'is_assumed': False,
'has_dwelling_above': False,
'is_valid': True,
'insulation_thickness': 'below average',
'age_band': "E"
}
u_value = recommendation_utils.get_roof_u_value(**inputs)
assert u_value == 1.5, f"Expected 1.5, but got {u_value}"
def test_get_roof_u_value_case_5(self):
# Test case where insulation thickness is exactly specified
inputs = {
'original_description': 'Pitched, 100mm insulation',
'clean_description': 'Pitched, 100mm insulation',
'thermal_transmittance': None,
'thermal_transmittance_unit': None,
'is_pitched': True,
'is_roof_room': False,
'is_loft': False,
'is_flat': False,
'is_thatched': False,
'is_at_rafters': False,
'is_assumed': False,
'has_dwelling_above': False,
'is_valid': True,
'insulation_thickness': '100',
'age_band': "G"
}
u_value = recommendation_utils.get_roof_u_value(**inputs)
assert u_value == 0.40, f"Expected 0.40, but got {u_value}"
def test_get_roof_u_value_case_6(self):
# Test case for a thatched roof
inputs = {
'original_description': 'Thatched, 75mm insulation',
'clean_description': 'Thatched, 75mm insulation',
'thermal_transmittance': None,
'thermal_transmittance_unit': None,
'is_pitched': False,
'is_roof_room': False,
'is_loft': False,
'is_flat': False,
'is_thatched': True,
'is_at_rafters': False,
'is_assumed': False,
'has_dwelling_above': False,
'is_valid': True,
'insulation_thickness': '75',
'age_band': "H"
}
u_value = recommendation_utils.get_roof_u_value(**inputs)
assert u_value == 0.35, f"Expected 0.35, but got {u_value}"
def test_get_roof_u_value_case_7(self):
# Test case where the roof has a room in it
inputs = {
'original_description': 'Pitched, room-in-roof, 100mm insulation',
'clean_description': 'Pitched, room-in-roof, 100mm insulation',
'thermal_transmittance': None,
'thermal_transmittance_unit': None,
'is_pitched': True,
'is_roof_room': True,
'is_loft': False,
'is_flat': False,
'is_thatched': False,
'is_at_rafters': False,
'is_assumed': False,
'has_dwelling_above': False,
'is_valid': True,
'insulation_thickness': '100',
'age_band': "J"
}
u_value = recommendation_utils.get_roof_u_value(**inputs)
assert u_value == 0.40, f"Expected 0.40, but got {u_value}"
def test_get_roof_u_value_case_8(self):
# Test case where there is a dwelling above the roof, U-value should be 0
inputs = {
'original_description': 'Pitched, 100mm insulation',
'clean_description': 'Pitched, 100mm insulation',
'thermal_transmittance': None,
'thermal_transmittance_unit': None,
'is_pitched': True,
'is_roof_room': False,
'is_loft': False,
'is_flat': False,
'is_thatched': False,
'is_at_rafters': False,
'is_assumed': False,
'has_dwelling_above': True,
'is_valid': True,
'insulation_thickness': '100',
'age_band': "J"
}
u_value = recommendation_utils.get_roof_u_value(**inputs)
assert u_value == 0.0, f"Expected 0.0, but got {u_value}"
@pytest.mark.parametrize(
"test_case",
wall_uvalue_test_cases
)
def test_get_wall_uvalue(self, test_case):
expected_uvalue = test_case["uvalue"]
inputs = test_case.copy()
del inputs["uvalue"]
uvalue = recommendation_utils.get_wall_u_value(**inputs)
assert expected_uvalue == uvalue, f"Expected u value {expected_uvalue}, recieved {uvalue}"
@pytest.mark.parametrize("test_input", floor_uvalue_test_cases)
def test_get_floor_u_value(self, test_input):
if not isinstance(test_input["expected"], float):
with pytest.raises(test_input["expected"]):
recommendation_utils.get_floor_u_value(
test_input["floor_type"],
test_input["area"],
test_input["perimeter"],
test_input["age_band"],
test_input["wall_type"],
test_input["insulation_thickness"],
)
else:
result = recommendation_utils.get_floor_u_value(
floor_type=test_input["floor_type"],
area=test_input["area"],
perimeter=test_input["perimeter"],
age_band=test_input["age_band"],
wall_type=test_input["wall_type"],
insulation_thickness=test_input["insulation_thickness"],
)
assert result == pytest.approx(test_input["expected"], abs=1e-2)
# Test with wall_type not in default_wall_thickness
def test_wall_type_not_in_default_wall_thickness(self):
with pytest.raises(IndexError):
recommendation_utils.get_floor_u_value(
floor_type="solid",
area=100,
perimeter=40,
age_band="A",
wall_type="InvalidWallType",
insulation_thickness=None,
)
# Test with age_band not in s11
def test_age_band_not_in_s11(self):
with pytest.raises(IndexError):
recommendation_utils.get_floor_u_value(
floor_type="solid",
area=100,
perimeter=40,
age_band="Z",
wall_type="Cavity",
insulation_thickness=None,
)
def test_estimate_perimeter_regular_inputs():
assert math.isclose(
recommendation_utils.estimate_perimeter(100, 5), 40.24922359499622,
rel_tol=1e-2
)
assert math.isclose(
recommendation_utils.estimate_perimeter(123, 5), 44.63854836349408,
rel_tol=1e-2
)
def test_estimate_perimeter_zero_floor_area():
with pytest.raises(ZeroDivisionError):
recommendation_utils.estimate_perimeter(0, 5)
with pytest.raises(ValueError):
assert recommendation_utils.estimate_perimeter(0, 0) == 0
def test_estimate_perimeter_invalid_inputs():
with pytest.raises(ValueError):
recommendation_utils.estimate_perimeter(100, 0)
with pytest.raises(ValueError):
recommendation_utils.estimate_perimeter(-100, 5)
with pytest.raises(ValueError):
recommendation_utils.estimate_perimeter(100, -5)

View file

@ -1,12 +1,8 @@
import os
import pandas as pd
import pytest
import pickle
import numpy as np
from unittest.mock import Mock, MagicMock
from recommendations.WallRecommendations import WallRecommendations
from model_data.analysis.UvalueEstimations import UvalueEstimations
from backend.Property import Property
from recommendations.recommendation_utils import is_diminishing_returns
@ -206,13 +202,6 @@ class TestWallRecommendations:
) as f:
return pickle.load(f)
@pytest.fixture
def uvalue_estimates(self):
with open(
os.path.abspath(os.path.dirname(__file__)) + "/test_data/uvalue_estimates.pkl", "rb"
) as f:
return pickle.load(f)
@pytest.fixture
def mock_wall_rec_instance(self):
# Creating a mock instance of WallRecommendations with the necessary attributes
@ -220,26 +209,22 @@ class TestWallRecommendations:
property_mock.full_sap_epc = {"lodgement-date": "2000-01-01"} # or any date you want
property_mock.data = {"construction-age-band": "1950"} # or any other data that fits your tests
uvalue_estimates_mock = Mock()
mock_wall_rec_instance = WallRecommendations(
property_mock, uvalue_estimates_mock, "Decile 1", materials=wall_parts
property_mock, "Decile 1", materials=wall_parts
)
return mock_wall_rec_instance
def test_init(self, input_properties, uvalue_estimates):
def test_init(self, input_properties):
obj = WallRecommendations(
property_instance=input_properties[0],
uvalue_estimates=uvalue_estimates,
total_floor_area_group_decile="Decile 1",
materials=wall_parts
)
assert obj
assert obj.property
assert obj.uvalue_estimates
assert obj.total_floor_area_group_decile == "Decile 1"
def test_uvalue_0_16(self, input_properties, uvalue_estimates):
def test_uvalue_0_16(self, input_properties):
"""
This tests the wall description Average thermal transmittance 0.16 W/m-¦K
The important data for this recommendation is:
@ -251,7 +236,6 @@ class TestWallRecommendations:
input_properties[0].year_built = 2014
recommender = WallRecommendations(
property_instance=input_properties[0],
uvalue_estimates=uvalue_estimates,
total_floor_area_group_decile="Decile 1",
materials=wall_parts
)
@ -260,7 +244,7 @@ class TestWallRecommendations:
# This should be empty
assert recommender.recommendations == []
def test_solid_brick_no_insulation(self, input_properties, uvalue_estimates):
def test_solid_brick_no_insulation(self, input_properties):
"""
This tests a property with a wall description of Solid brick, as built, no insulation (assumed)
The property was built in 1930, right on the threshold for when cavity walls were introduced
@ -271,10 +255,12 @@ class TestWallRecommendations:
"""
input_properties[1].year_built = 1930
input_properties[1].insulation_wall_area = 100
input_properties[1].walls["clean_description"] = "Solid brick, as built, no insulation"
input_properties[1].walls["is_sandstone_or_limestone"] = False
input_properties[1].age_band = "A"
recommender = WallRecommendations(
property_instance=input_properties[1],
uvalue_estimates=uvalue_estimates,
total_floor_area_group_decile="Decile 1",
materials=wall_parts
)
@ -296,7 +282,7 @@ class TestWallRecommendations:
recommender.recommendations
)
def test_solid_brick_insulation(self, input_properties, uvalue_estimates):
def test_solid_brick_insulation(self, input_properties):
"""
This tests a property with a wall description of Solid brick, as built, insulation (assumed)
The property was built in 1991, after cavity walls were introduced
@ -311,7 +297,6 @@ class TestWallRecommendations:
input_properties[6].year_built = 1991
recommender = WallRecommendations(
property_instance=input_properties[6],
uvalue_estimates=uvalue_estimates.walls.to_dict("records"),
total_floor_area_group_decile="Decile 1",
materials=wall_parts
)
@ -390,39 +375,10 @@ class TestWallRecommendationsBase:
return property_mock
@pytest.fixture
def uvalue_estimations_mock(self):
uvalue_estimations_mock = MagicMock(spec=UvalueEstimations)
uvalue_estimations_mock.walls = pd.DataFrame([
{
'local-authority': 'E09000012',
'property-type': 'Bungalow',
'walls-energy-eff': 'Very Good',
'walls-env-eff': 'Very Good',
'built-form': 'End-Terrace',
'number-habitable-rooms': '', 'number-heated-rooms': '', 'total-floor-area_group': 'Decile 1',
'median_thermal_transmittance': 0.15, 'n_samples': 1
}
])
uvalue_estimations_mock.walls_decile_data = {
'decile_labels': ['Decile 1', 'Decile 2', 'Decile 3', 'Decile 4', 'Decile 5', 'Decile 6', 'Decile 7',
'Decile 8', 'Decile 9', 'Decile 10'],
'decile_boundaries': np.array([11., 49., 52., 56., 63., 70., 74., 79.,
90., 103.8, 1936.])}
uvalue_estimations_mock.classify_decile_newvalues.return_value = ["Decile 1"]
return uvalue_estimations_mock
@pytest.fixture
def wall_recommendations_instance(self, property_mock, uvalue_estimations_mock):
def wall_recommendations_instance(self, property_mock):
wall_recommendations_instance = WallRecommendations(
property_mock, uvalue_estimations_mock, "Decile 1", materials=wall_parts
property_mock, "Decile 1", materials=wall_parts
)
wall_recommendations_instance.uvalue_estimates.walls_decile_data = {
"decile_labels": MagicMock(),
"decile_boundaries": MagicMock()
}
return wall_recommendations_instance
def test_ewi_valid_in_conservation_area(self, wall_recommendations_instance):
@ -443,7 +399,11 @@ class TestWallRecommendationsBase:
"thermal_transmittance": None,
"is_solid_brick": False,
"is_cavity_wall": False,
"insulation_thickness": "none"
"insulation_thickness": "none",
"clean_description": "Solid brick, as built, no insulation",
"is_granite_or_whinstone": False,
"is_sandstone_or_limestone": False,
}
wall_recommendations_instance.property.age_band = "A"
with pytest.raises(NotImplementedError):
wall_recommendations_instance.recommend()

View file

@ -1,6 +1,7 @@
import boto3
from io import BytesIO
from botocore.exceptions import NoCredentialsError, PartialCredentialsError
import pandas as pd
def read_from_s3(bucket_name, s3_file_name):
@ -63,3 +64,25 @@ def save_dataframe_to_s3_parquet(df, bucket_name, file_key):
# Upload the Parquet file to S3
client.put_object(Bucket=bucket_name, Key=file_key, Body=parquet_buffer.getvalue())
def read_dataframe_from_s3_parquet(bucket_name, file_key):
"""
Read a pandas DataFrame from a Parquet file stored in S3.
:param bucket_name: Name of the S3 bucket.
:param file_key: Key of the file (including directory path within the bucket).
:return: A pandas DataFrame.
"""
# Create the boto3 client
client = boto3.client('s3')
# Get the Parquet file from S3
response = client.get_object(Bucket=bucket_name, Key=file_key)
# Read the file into a pandas DataFrame
parquet_buffer = BytesIO(response['Body'].read())
df = pd.read_parquet(parquet_buffer)
return df