mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
Merge pull request #97 from Hestia-Homes/main
Restructuring entire repo to get the trigger api to run actual recommendations
This commit is contained in:
commit
68b8439d13
35 changed files with 7122 additions and 5379 deletions
|
|
@ -1,8 +1,10 @@
|
|||
[run]
|
||||
omit =
|
||||
model_data/tests/*
|
||||
*__init__*
|
||||
*/tests/*
|
||||
model_data/temp_inputs.py
|
||||
model_data/config.py
|
||||
model_data/__init__.py
|
||||
model_data/app.py
|
||||
model_data/plotting/*
|
||||
model_data/plotting/*
|
||||
model_data/rdsap_tables.py
|
||||
3
.idea/Model.iml
generated
3
.idea/Model.iml
generated
|
|
@ -5,8 +5,9 @@
|
|||
<sourceFolder url="file://$MODULE_DIR$/backend" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/model_data" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/open_uprn" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/recommendations" isTestSource="false" />
|
||||
</content>
|
||||
<orderEntry type="jdk" jdkName="Python 3.10 (model_data)" jdkType="Python SDK" />
|
||||
<orderEntry type="jdk" jdkName="Python 3.10 (hestia-data)" jdkType="Python SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
2
.idea/misc.xml
generated
2
.idea/misc.xml
generated
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.10 (model_data)" project-jdk-type="Python SDK" />
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.10 (hestia-data)" project-jdk-type="Python SDK" />
|
||||
<component name="PythonCompatibilityInspectionAdvertiser">
|
||||
<option name="version" value="3" />
|
||||
</component>
|
||||
|
|
|
|||
|
|
@ -6,6 +6,10 @@ from backend.app.config import get_settings
|
|||
from model_data.Property import Property
|
||||
from epc_api.client import EpcClient
|
||||
from utils.logger import setup_logger
|
||||
from recommendations.FloorRecommendations import FloorRecommendations
|
||||
from recommendations.WallRecommendations import WallRecommendations
|
||||
from utils.uvalue_estimates import classify_decile_newvalues
|
||||
|
||||
# TODO: This is placeholder until data is stored in DB
|
||||
from backend.app.plan.temp_cleaned_data import cleaned
|
||||
from backend.app.plan.uvalue_estimates_walls import uvalue_estimates_walls
|
||||
|
|
@ -51,6 +55,17 @@ in_conservation_area_data = [
|
|||
{'uprn': 200003489276, 'is_in_conservation_area': 'in_conservation_area'}
|
||||
]
|
||||
|
||||
# TODO: db
|
||||
floors_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': [6., 50., 56., 69., 77.6, 87., 98., 112.,
|
||||
127., 150., 2279.]}
|
||||
|
||||
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': [6., 49., 51., 55., 64., 71., 76., 83., 96.,
|
||||
120., 2279.]}
|
||||
|
||||
|
||||
@router.post("/trigger")
|
||||
async def trigger_plan(body: PlanTriggerRequest):
|
||||
|
|
@ -85,7 +100,71 @@ async def trigger_plan(body: PlanTriggerRequest):
|
|||
)
|
||||
p.set_is_in_conservation_area(in_conservation_area)
|
||||
|
||||
for p in input_properties:
|
||||
logger.info("Getting components and properties recommendations")
|
||||
recommendations = []
|
||||
for property_id, p in enumerate(input_properties):
|
||||
# For each property, classiy floor area decide
|
||||
total_floor_area_group_decile = classify_decile_newvalues(
|
||||
decile_boundaries=floors_decile_data["decile_boundaries"],
|
||||
decile_labels=floors_decile_data["decile_labels"],
|
||||
new_values=[float(p.data["total-floor-area"])],
|
||||
)[0]
|
||||
|
||||
# Property recommendations
|
||||
p.get_components(cleaned)
|
||||
|
||||
return {"message": "Plan complete"}
|
||||
# This is placeholder, until the full dataset is loaded into the database and we just make a read to the
|
||||
# database
|
||||
floors_u_value_estimate = [
|
||||
x for x in uvalue_estimates_floors
|
||||
if (x['local-authority'] == p.data["local-authority"]) &
|
||||
(x['property-type'] == p.data["property-type"]) &
|
||||
(x['built-form'] == p.data["built-form"]) &
|
||||
(x['floor-energy-eff'] == p.data["floor-energy-eff"] if p.data["floor-energy-eff"] != 'N/A' else True) &
|
||||
(x['floor-env-eff'] == p.data["floor-env-eff"] if p.data["floor-env-eff"] != 'N/A' else True)
|
||||
]
|
||||
|
||||
# Floor recommendations
|
||||
floor_recommender = FloorRecommendations(
|
||||
property_instance=p, uvalue_estimates=floors_u_value_estimate,
|
||||
total_floor_area_group_decile=total_floor_area_group_decile
|
||||
)
|
||||
floor_recommender.recommend()
|
||||
# insert property id
|
||||
for rec in floor_recommender.recommendations:
|
||||
rec["property_id"] = property_id
|
||||
|
||||
recommendations.extend(floor_recommender.recommendations)
|
||||
|
||||
# Wall recommendations
|
||||
# We would make this u-value query directly to the database
|
||||
total_floor_area_group_decile = classify_decile_newvalues(
|
||||
decile_boundaries=walls_decile_data["decile_boundaries"],
|
||||
decile_labels=walls_decile_data["decile_labels"],
|
||||
new_values=[float(p.data["total-floor-area"])],
|
||||
)[0]
|
||||
|
||||
# This is placeholder, until the full dataset is loaded into the database and we just make a read to the
|
||||
# database
|
||||
walls_u_value_estimate = [
|
||||
x for x in uvalue_estimates_walls
|
||||
if (x['local-authority'] == p.data["local-authority"]) &
|
||||
(x['property-type'] == p.data["property-type"]) &
|
||||
(x['built-form'] == p.data["built-form"]) &
|
||||
(x['walls-energy-eff'] == p.data["walls-energy-eff"] if p.data["walls-energy-eff"] != 'N/A' else True) &
|
||||
(x['walls-env-eff'] == p.data["walls-env-eff"] if p.data["walls-env-eff"] != 'N/A' else True)
|
||||
]
|
||||
|
||||
wall_recomendations = WallRecommendations(
|
||||
property_instance=p,
|
||||
uvalue_estimates=walls_u_value_estimate,
|
||||
total_floor_area_group_decile=total_floor_area_group_decile
|
||||
)
|
||||
wall_recomendations.recommend()
|
||||
# insert property id
|
||||
for rec in wall_recomendations.recommendations:
|
||||
rec["property_id"] = property_id
|
||||
|
||||
recommendations.extend(wall_recomendations.recommendations)
|
||||
|
||||
return {"recommendations": recommendations}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -72,6 +72,7 @@ def handler():
|
|||
# TODO: Do this at a constituency level
|
||||
constituencies = {p.data["constituency"] for p in input_properties}
|
||||
property_types = ["bungalow", "flat", "house", "maisonette", "park home"]
|
||||
floor_areas = ["unknown", "s", "m", "l", "xl", "xxl", "xxxl"]
|
||||
|
||||
# We pull properties from local authorities, by property type. This will allow us to build
|
||||
# a dataset of up to 10k properties per local authority/property type combination
|
||||
|
|
@ -82,24 +83,28 @@ def handler():
|
|||
data = []
|
||||
for c in tqdm(constituencies):
|
||||
for pt in property_types:
|
||||
data.extend(
|
||||
pagenated_epc_download(
|
||||
client=epc_client,
|
||||
params={
|
||||
"constituency": c,
|
||||
"property-type": pt,
|
||||
"from-month": 8,
|
||||
"from-year": 2014,
|
||||
},
|
||||
page_size=5000,
|
||||
n_pages=10,
|
||||
for fa in floor_areas:
|
||||
data.extend(
|
||||
pagenated_epc_download(
|
||||
client=epc_client,
|
||||
params={
|
||||
"constituency": c,
|
||||
"property-type": pt,
|
||||
"from-month": 8,
|
||||
"from-year": 2014,
|
||||
"floor-area": fa,
|
||||
},
|
||||
page_size=5000,
|
||||
n_pages=10,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
# Incorporate input data into cleaning
|
||||
cleaner = EpcClean(data + [p.data for p in input_properties])
|
||||
cleaner.clean()
|
||||
|
||||
z = [x for x in data if x["floor-description"] == "(anheddiad arall islaw)"]
|
||||
|
||||
address_meta = [
|
||||
{
|
||||
"postcode": x["postcode"].upper(),
|
||||
|
|
@ -137,6 +142,60 @@ def handler():
|
|||
uvalue_estimates = UvalueEstimations(data=data)
|
||||
uvalue_estimates.get_estimates(cleaner=cleaner)
|
||||
|
||||
x = {'low-energy-fixed-light-count': '', 'address': 'Flat 28, 22, Adelina Grove', 'uprn-source': 'Address Matched',
|
||||
'floor-height': '', 'heating-cost-potential': '668', 'unheated-corridor-length': '7.73',
|
||||
'hot-water-cost-potential': '190', 'construction-age-band': 'England and Wales: 1991-1995',
|
||||
'potential-energy-rating': 'D', 'mainheat-energy-eff': 'Very Poor', 'windows-env-eff': 'Average',
|
||||
'lighting-energy-eff': 'Average', 'environment-impact-potential': '46',
|
||||
'glazed-type': 'double glazing, unknown install date', 'heating-cost-current': '1081', 'address3': '',
|
||||
'mainheatcont-description': 'No time or thermostatic control of room temperature',
|
||||
'sheating-energy-eff': 'N/A', 'property-type': 'Flat', 'local-authority-label': 'Tower Hamlets',
|
||||
'fixed-lighting-outlets-count': '', 'energy-tariff': 'dual', 'mechanical-ventilation': 'natural',
|
||||
'hot-water-cost-current': '190', 'county': 'Greater London Authority', 'postcode': 'E1 3BX',
|
||||
'solar-water-heating-flag': 'N', 'constituency': 'E14000555', 'co2-emissions-potential': '5.2',
|
||||
'number-heated-rooms': '2', 'floor-description': '(another dwelling below)',
|
||||
'energy-consumption-potential': '301', 'local-authority': 'E09000030', 'built-form': 'Semi-Detached',
|
||||
'number-open-fireplaces': '0', 'windows-description': 'Fully double glazed', 'glazed-area': 'Normal',
|
||||
'inspection-date': '2018-09-05', 'mains-gas-flag': 'N', 'co2-emiss-curr-per-floor-area': '53',
|
||||
'address1': 'Flat 28', 'heat-loss-corridor': 'unheated corridor', 'flat-storey-count': '',
|
||||
'constituency-label': 'Bethnal Green and Bow', 'roof-energy-eff': 'Average', 'total-floor-area': '103.0',
|
||||
'building-reference-number': '4441803568', 'environment-impact-current': '44', 'co2-emissions-current': '5.5',
|
||||
'roof-description': 'Pitched, insulated (assumed)', 'floor-energy-eff': 'NO DATA!',
|
||||
'number-habitable-rooms': '2', 'address2': '22, Adelina Grove', 'hot-water-env-eff': 'Poor',
|
||||
'posttown': 'LONDON', 'mainheatc-energy-eff': 'Very Poor', 'main-fuel': 'electricity (not community)',
|
||||
'lighting-env-eff': 'Average', 'windows-energy-eff': 'Average', 'floor-env-eff': 'N/A',
|
||||
'sheating-env-eff': 'N/A', 'lighting-description': 'Low energy lighting in 25% of fixed outlets',
|
||||
'roof-env-eff': 'Average', 'walls-energy-eff': 'Good', 'photo-supply': '', 'lighting-cost-potential': '84',
|
||||
'mainheat-env-eff': 'Very Poor', 'multi-glaze-proportion': '100', 'main-heating-controls': '2701',
|
||||
'lodgement-datetime': '2018-09-06 17:25:59', 'flat-top-storey': 'Y', 'current-energy-rating': 'E',
|
||||
'secondheat-description': 'None', 'walls-env-eff': 'Good', 'transaction-type': 'rental (private)',
|
||||
'uprn': '6032920', 'current-energy-efficiency': '48', 'energy-consumption-current': '316',
|
||||
'mainheat-description': 'Electric ceiling heating', 'lighting-cost-current': '147',
|
||||
'lodgement-date': '2018-09-06', 'extension-count': '1', 'mainheatc-env-eff': 'Very Poor',
|
||||
'lmk-key': '175926409402018090617255958380158', 'wind-turbine-count': '0', 'tenure': 'rental (private)',
|
||||
'floor-level': '4th', 'potential-energy-efficiency': '67', 'hot-water-energy-eff': 'Average',
|
||||
'low-energy-lighting': '25', 'walls-description': 'Solid brick, as built, insulated (assumed)',
|
||||
'hotwater-description': 'Electric immersion, off-peak'}
|
||||
from utils.uvalue_estimates import classify_decile_newvalues
|
||||
total_floor_area_group_decile = UvalueEstimations.classify_decile_newvalues(
|
||||
decile_boundaries=uvalue_estimates.walls_decile_data["decile_boundaries"],
|
||||
decile_labels=uvalue_estimates.walls_decile_data["decile_labels"],
|
||||
new_values=[float(x["total-floor-area"])],
|
||||
)[0]
|
||||
|
||||
u_value_estimate = uvalue_estimates.walls[
|
||||
(uvalue_estimates.walls["local-authority"] == x["local-authority"]) &
|
||||
(uvalue_estimates.walls["property-type"] == x["property-type"]) &
|
||||
(uvalue_estimates.walls["built-form"] == x["built-form"]) &
|
||||
(uvalue_estimates.walls["walls-energy-eff"] == x["walls-energy-eff"]) &
|
||||
(uvalue_estimates.walls["walls-env-eff"] == x["walls-env-eff"]) &
|
||||
(uvalue_estimates.walls["total-floor-area_group"] == total_floor_area_group_decile)
|
||||
]
|
||||
|
||||
uvalue_estimates.walls[
|
||||
uvalue_estimates.walls
|
||||
]
|
||||
|
||||
# all_data = {
|
||||
# "input_properties": input_properties,
|
||||
# "cleaner": cleaner,
|
||||
|
|
@ -341,20 +400,20 @@ def app():
|
|||
)
|
||||
|
||||
# Production of sample data for land registry
|
||||
address_meta = [
|
||||
{
|
||||
"postcode": x["postcode"].upper(),
|
||||
"address1": x["address1"].upper(),
|
||||
"address2": x["address2"].upper(),
|
||||
"address3": x["address3"].upper(),
|
||||
"address": x["address"],
|
||||
"uprn": x["uprn"]
|
||||
} for x in data
|
||||
]
|
||||
|
||||
import pickle
|
||||
with open("sample_addresses.pkl", "wb") as f:
|
||||
pickle.dump(address_meta, f)
|
||||
# address_meta = [
|
||||
# {
|
||||
# "postcode": x["postcode"].upper(),
|
||||
# "address1": x["address1"].upper(),
|
||||
# "address2": x["address2"].upper(),
|
||||
# "address3": x["address3"].upper(),
|
||||
# "address": x["address"],
|
||||
# "uprn": x["uprn"]
|
||||
# } for x in data
|
||||
# ]
|
||||
#
|
||||
# import pickle
|
||||
# with open("sample_addresses.pkl", "wb") as f:
|
||||
# pickle.dump(address_meta, f)
|
||||
|
||||
# Incorporate input data into cleaning
|
||||
cleaner = EpcClean(data)
|
||||
|
|
@ -365,3 +424,5 @@ def app():
|
|||
uvalue_estimates = UvalueEstimations(data=data)
|
||||
uvalue_estimates.get_estimates(cleaner=cleaner)
|
||||
# TODO: Store these to a db
|
||||
|
||||
uvalue_estimates.floors_decile_data
|
||||
|
|
|
|||
|
|
@ -12,12 +12,23 @@ class FloorAttributes(BaseUtility):
|
|||
|
||||
OBSERVED_ERRORS = ["Conservatory"]
|
||||
|
||||
WELSH_TEXT = {
|
||||
"(anheddiad arall islaw)": "(another dwelling below)",
|
||||
}
|
||||
|
||||
def __init__(self, description: str):
|
||||
self.description: str = description.lower()
|
||||
|
||||
self.nodata = (not description) or (description in self.DATA_ANOMALY_MATCHES) or (
|
||||
description in self.OBSERVED_ERRORS)
|
||||
|
||||
# Try and perform a translation, incase it's in welsh
|
||||
translation = self.WELSH_TEXT.get(self.description)
|
||||
|
||||
if translation:
|
||||
self.nodata = False
|
||||
self.description = translation
|
||||
|
||||
if not self.nodata and not any(
|
||||
rt in self.description for rt in
|
||||
self.FLOOR_TYPES + self.DWELLING_BELOW + ["average thermal transmittance"]
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ class HotWaterAttributes(BaseUtility):
|
|||
# A device that transfers heat from a source (like the ground or air) into a building to provide hot water
|
||||
'electric immersion', # An electric device that heats water using electric resistance
|
||||
'gas boiler', # A boiler that uses gas as fuel to heat water
|
||||
'oil boiler', # A boiler that uses oil as fuel to heat water
|
||||
'electric instantaneous', # Similar to gas instantaneous, but uses electricity as its energy source
|
||||
'gas multipoint', # A gas water heater that can supply hot water to multiple points of use at once
|
||||
'heat pump' # A general category for heat pumps, regardless of the energy source
|
||||
|
|
@ -89,11 +90,21 @@ class HotWaterAttributes(BaseUtility):
|
|||
'from second main heating system': 'from secondary system',
|
||||
}
|
||||
|
||||
WELSH_TEXT = {
|
||||
"ogçör brif system": "from main system",
|
||||
}
|
||||
|
||||
def __init__(self, description: str):
|
||||
self.description: str = clean_description(description.lower())
|
||||
|
||||
self.nodata = not description or description in self.DATA_ANOMALY_MATCHES
|
||||
|
||||
translation = self.WELSH_TEXT.get(self.description)
|
||||
|
||||
if translation:
|
||||
self.nodata = False
|
||||
self.description = translation
|
||||
|
||||
if not any(
|
||||
self._keyword_in_description(keywords)
|
||||
for keywords in [
|
||||
|
|
|
|||
|
|
@ -20,11 +20,20 @@ class MainHeatAttributes(BaseUtility):
|
|||
"pipes in insulated timber floor", "pipes in concrete slab"]
|
||||
OTHERS = ["assumed", "electricaire", "assumed for most rooms"]
|
||||
|
||||
WELSH_TEXT = {
|
||||
"bwyler a rheiddiaduron, nwy prif gyflenwad": "boiler and radiators, mains gas",
|
||||
}
|
||||
|
||||
def __init__(self, description: str):
|
||||
self.description: str = clean_description(description.lower())
|
||||
# Remove special characters
|
||||
self.nodata = not description or description in self.DATA_ANOMALY_MATCHES
|
||||
|
||||
translation = self.WELSH_TEXT.get(self.description)
|
||||
if translation:
|
||||
self.nodata = False
|
||||
self.description = translation
|
||||
|
||||
if not description or not any(
|
||||
rt in self.description for rt in
|
||||
self.HEAT_SYSTEMS + self.FUEL_TYPES + self.DISTRIBUTION_SYSTEMS + self.OTHERS
|
||||
|
|
|
|||
|
|
@ -8,6 +8,10 @@ class RoofAttributes(BaseUtility):
|
|||
ROOF_TYPES = ['pitched', 'roof room', 'loft', 'flat', 'thatched', 'at rafters', 'assumed']
|
||||
DWELLING_ABOVE = ["another dwelling above", "other premises above"]
|
||||
|
||||
WELSH_TEXT = {
|
||||
"ar oleddf, dim inswleiddio": "pitched, no insulation",
|
||||
}
|
||||
|
||||
def __init__(self, description: str):
|
||||
"""
|
||||
:param description: Description of the roof.
|
||||
|
|
@ -15,7 +19,12 @@ class RoofAttributes(BaseUtility):
|
|||
|
||||
self.description: str = description.lower()
|
||||
self.nodata = not description or description in self.DATA_ANOMALY_MATCHES
|
||||
|
||||
|
||||
translation = self.WELSH_TEXT.get(self.description)
|
||||
if translation:
|
||||
self.nodata = False
|
||||
self.description = translation
|
||||
|
||||
if not self.nodata and not any(
|
||||
rt in self.description for rt in self.ROOF_TYPES + self.DWELLING_ABOVE + ["average thermal transmittance"]
|
||||
):
|
||||
|
|
|
|||
|
|
@ -7,12 +7,22 @@ class WallAttributes(BaseUtility):
|
|||
WALL_TYPES = ['cavity wall', 'filled cavity', 'solid brick', 'system built', 'timber frame', 'granite or whinstone',
|
||||
'as built', 'cob', 'assumed', 'sandstone or limestone']
|
||||
|
||||
WELSH_TEXT = {
|
||||
"Briciau solet, fel yGÇÖu hadeiladwyd, dim inswleiddio (rhagdybiaeth)":
|
||||
"Solid brick, as built, no insulation (assumed)",
|
||||
}
|
||||
|
||||
def __init__(self, description: str):
|
||||
"""
|
||||
:param description: Description of the walls.
|
||||
"""
|
||||
self.description: str = description
|
||||
|
||||
translation = self.WELSH_TEXT.get(self.description)
|
||||
if translation:
|
||||
self.nodata = False
|
||||
self.description = translation
|
||||
|
||||
self.nodata = not description or description in self.DATA_ANOMALY_MATCHES
|
||||
|
||||
def process(self) -> Dict[str, Union[float, str, bool, None]]:
|
||||
|
|
|
|||
|
|
@ -17,6 +17,10 @@ class WindowAttributes(BaseUtility):
|
|||
"throughout": "full"
|
||||
}
|
||||
|
||||
WELSH_TEXT = {
|
||||
"gwydrau dwbl llawn": "full double glazing",
|
||||
}
|
||||
|
||||
def __init__(self, description: str):
|
||||
self.description: str = clean_description(description.lower())
|
||||
|
||||
|
|
@ -24,6 +28,11 @@ class WindowAttributes(BaseUtility):
|
|||
# and indicate there was no data
|
||||
self.nodata = not description or description in self.DATA_ANOMALY_MATCHES
|
||||
|
||||
translation = self.WELSH_TEXT.get(self.description)
|
||||
if translation:
|
||||
self.nodata = False
|
||||
self.description = translation
|
||||
|
||||
if not self.nodata:
|
||||
if not any(
|
||||
rt in self.description for rt in
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
[pytest]
|
||||
addopts = --cov-report term-missing --cov=model_data
|
||||
testpaths = model_data/tests
|
||||
|
|
@ -3,6 +3,10 @@ clean_floor_cases = [
|
|||
'thermal_transmittance_unit': None, 'is_assumed': False, 'is_to_unheated_space': False,
|
||||
'is_to_external_air': False, 'is_suspended': False, 'is_solid': False, 'insulation_thickness': None,
|
||||
"another_property_below": True},
|
||||
{'original_description': '(anheddiad arall islaw)', 'thermal_transmittance': None,
|
||||
'thermal_transmittance_unit': None, 'is_assumed': False, 'is_to_unheated_space': False,
|
||||
'is_to_external_air': False, 'is_suspended': False, 'is_solid': False, 'insulation_thickness': None,
|
||||
"another_property_below": True},
|
||||
{'original_description': '(other premises below)', 'thermal_transmittance': None,
|
||||
'thermal_transmittance_unit': None,
|
||||
'is_assumed': False, 'is_to_unheated_space': False, 'is_to_external_air': False, 'is_suspended': False,
|
||||
|
|
|
|||
|
|
@ -125,5 +125,13 @@ hotwater_cases = [
|
|||
'system_type': 'from secondary system',
|
||||
'thermostat_characteristics': None, 'heating_scope': None, 'energy_recovery': None, 'tariff_type': None,
|
||||
'extra_features': None, 'chp_systems': None, 'distribution_system': None, 'no_system_present': None,
|
||||
'assumed': False, "appliance": None},
|
||||
{'original_description': 'OGÇÖr brif system', 'heater_type': None, 'system_type': 'from main system',
|
||||
'thermostat_characteristics': None, 'heating_scope': None, 'energy_recovery': None, 'tariff_type': None,
|
||||
'extra_features': None, 'chp_systems': None, 'distribution_system': None, 'no_system_present': None,
|
||||
'assumed': False, "appliance": None},
|
||||
{'original_description': 'Oil boiler/circulator', 'heater_type': None, 'system_type': 'oil boiler',
|
||||
'thermostat_characteristics': None, 'heating_scope': None, 'energy_recovery': None, 'tariff_type': None,
|
||||
'extra_features': None, 'chp_systems': None, 'distribution_system': 'circulator', 'no_system_present': None,
|
||||
'assumed': False, "appliance": None}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -29,5 +29,6 @@ test_cases = [
|
|||
{'original_description': 'Below average lighting efficiency', 'low_energy_proportion': 0.25},
|
||||
{'original_description': 'Excellent lighting efficiency', 'low_energy_proportion': 1.0},
|
||||
{'original_description': 'Low energy lighting in 2% of fixed outlets', 'low_energy_proportion': 0.02},
|
||||
{'original_description': 'No Low energy lighting', 'low_energy_proportion': 0}
|
||||
{'original_description': 'No Low energy lighting', 'low_energy_proportion': 0},
|
||||
{'original_description': 'Goleuadau ynni-isel mewn 60% oGÇÖr mannau gosod', 'low_energy_proportion': 0.6}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1090,5 +1090,18 @@ mainheat_cases = [
|
|||
'has_warm_air': False,
|
||||
'has_water_source_heat_pump': False,
|
||||
'has_wood_logs': False,
|
||||
'has_wood_pellets': False}
|
||||
'has_wood_pellets': False},
|
||||
{'original_description': 'Bwyler a rheiddiaduron, nwy prif gyflenwad', '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_LPG': 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}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -344,5 +344,10 @@ clean_roof_test_cases = [
|
|||
{'original_description': 'Thatched, with additional 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': 'above average'}
|
||||
'insulation_thickness': 'above average'},
|
||||
{'original_description': 'Ar oleddf, dim inswleiddio', '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': 'none'}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -684,5 +684,11 @@ wall_cases = [
|
|||
'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': False,
|
||||
'is_cob': False, 'is_assumed': False, 'is_sandstone_or_limestone': False, 'insulation_thickness': 'above average',
|
||||
'external_insulation': False, 'internal_insulation': False},
|
||||
{'original_description': 'Briciau solet, 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': 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}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -35,5 +35,7 @@ windows_cases = [
|
|||
{'original_description': 'Some secondary glazing', 'has_glazing': True, 'glazing_coverage': 'partial',
|
||||
'glazing_type': 'secondary', 'no_data': False},
|
||||
{'original_description': 'Some triple glazing', 'has_glazing': True, 'glazing_coverage': 'partial',
|
||||
'glazing_type': 'triple', 'no_data': False}
|
||||
'glazing_type': 'triple', 'no_data': False},
|
||||
{'original_description': 'Gwydrau dwbl llawn', 'has_glazing': True, 'glazing_coverage': 'full',
|
||||
'glazing_type': 'double', 'no_data': False}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,53 +1,7 @@
|
|||
import logging
|
||||
from io import StringIO
|
||||
from unittest.mock import patch
|
||||
from model_data.utils import is_percentage_or_number, correct_spelling
|
||||
from utils.logger import setup_logger
|
||||
|
||||
|
||||
class TestLogger:
|
||||
def test_setup_logger_default(self):
|
||||
log_stream = StringIO()
|
||||
handler = logging.StreamHandler(log_stream)
|
||||
logger = setup_logger()
|
||||
logger.addHandler(handler)
|
||||
|
||||
# log something
|
||||
logger.info("Hello World!")
|
||||
|
||||
log_stream.seek(0)
|
||||
# assert that log was written
|
||||
assert log_stream.read() == "Hello World!\n"
|
||||
# remove the handler after use
|
||||
logger.removeHandler(handler)
|
||||
|
||||
@patch('logging.FileHandler')
|
||||
def test_setup_logger_file(self, mock_file_handler):
|
||||
# setup the logger
|
||||
logger = setup_logger(log_file='test.log', overwrite_handler=True)
|
||||
|
||||
# assert FileHandler was called correctly
|
||||
mock_file_handler.assert_called_once_with('test.log')
|
||||
|
||||
# clean up after use
|
||||
for handler in logger.handlers[:]:
|
||||
handler.close()
|
||||
logger.removeHandler(handler)
|
||||
|
||||
def test_setup_logger_loglevel(self):
|
||||
log_stream = StringIO()
|
||||
handler = logging.StreamHandler(log_stream)
|
||||
logger = setup_logger(level=logging.DEBUG)
|
||||
logger.addHandler(handler)
|
||||
|
||||
# log something
|
||||
logger.debug("Hello World!")
|
||||
|
||||
log_stream.seek(0)
|
||||
# assert that log was written
|
||||
assert log_stream.read() == "Hello World!\n"
|
||||
# remove the handler after use
|
||||
logger.removeHandler(handler)
|
||||
class TestUtils:
|
||||
|
||||
def test_is_percentage_or_number(self):
|
||||
assert is_percentage_or_number("88")
|
||||
|
|
|
|||
3
pytest.ini
Normal file
3
pytest.ini
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
[pytest]
|
||||
addopts = --cov-report term-missing --cov=model_data --cov=recommendations
|
||||
testpaths = model_data/tests recommendations/tests
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
import math
|
||||
from typing import List
|
||||
from model_data.BaseUtility import BaseUtility
|
||||
from model_data.Property import Property
|
||||
from model_data.analysis.UvalueEstimations import UvalueEstimations
|
||||
from model_data.rdsap_tables import default_wall_thickness, age_band_data
|
||||
from model_data.recommendations.recommendation_utils import (
|
||||
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_recommended_part, get_uvalue_estimate
|
||||
)
|
||||
|
||||
suspended_floor_insulation_parts = [
|
||||
|
|
@ -89,12 +89,16 @@ class FloorRecommendations(BaseUtility):
|
|||
"Ground": 0,
|
||||
# We don't know what floor level, we just make sure it's not 0
|
||||
"mid floor": 1,
|
||||
"4th": 4
|
||||
"4th": 4,
|
||||
# We set
|
||||
"00": 0,
|
||||
"3rd": 3
|
||||
}
|
||||
|
||||
def __init__(self, property_instance: Property, uvalue_estimates: UvalueEstimations):
|
||||
def __init__(self, property_instance: Property, uvalue_estimates: List, total_floor_area_group_decile: str):
|
||||
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
|
||||
|
||||
|
|
@ -241,7 +245,11 @@ class FloorRecommendations(BaseUtility):
|
|||
age_band=age_band,
|
||||
)
|
||||
else:
|
||||
u_value = self._get_floors_uvalue_estimate()
|
||||
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
|
||||
|
||||
|
|
@ -253,54 +261,6 @@ class FloorRecommendations(BaseUtility):
|
|||
# Given the U-value, we recommend solid floor insulation options which are usually solid foam
|
||||
self.recommend_floor_insulation(u_value=u_value, parts=solid_floor_insulation_parts)
|
||||
|
||||
def _get_floors_uvalue_estimate(self):
|
||||
|
||||
"""
|
||||
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
|
||||
:return:
|
||||
"""
|
||||
|
||||
total_floor_area_group_decile = self.uvalue_estimates.classify_decile_newvalues(
|
||||
decile_boundaries=self.uvalue_estimates.floors_decile_data["decile_boundaries"],
|
||||
decile_labels=self.uvalue_estimates.floors_decile_data["decile_labels"],
|
||||
new_values=[float(self.property.data["total-floor-area"])],
|
||||
)[0]
|
||||
|
||||
u_value_estimate = self.uvalue_estimates.floors[
|
||||
(self.uvalue_estimates.floors["local-authority"] == self.property.data["local-authority"]) &
|
||||
(self.uvalue_estimates.floors["property-type"] == self.property.data["property-type"]) &
|
||||
(self.uvalue_estimates.floors["built-form"] == self.property.data["built-form"]) &
|
||||
(self.uvalue_estimates.floors["floor-energy-eff"] == self.property.data["floor-energy-eff"]) &
|
||||
(self.uvalue_estimates.floors["floor-env-eff"] == self.property.data["floor-env-eff"]) &
|
||||
(self.uvalue_estimates.floors["total-floor-area_group"] == total_floor_area_group_decile)
|
||||
]
|
||||
|
||||
if u_value_estimate.empty:
|
||||
raise ValueError("No U-value estimate found for the given property")
|
||||
|
||||
# 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
|
||||
|
||||
habitable_rooms_filter = (
|
||||
self.uvalue_estimates.walls["number-habitable-rooms"] == self.property.data["number-habitable-rooms"]
|
||||
)
|
||||
|
||||
if any(habitable_rooms_filter):
|
||||
u_value_estimate = u_value_estimate[habitable_rooms_filter]
|
||||
|
||||
heated_rooms_filter = (
|
||||
self.uvalue_estimates.walls["number-heated-rooms"] == self.property.data["number-heated-rooms"]
|
||||
)
|
||||
|
||||
if any(heated_rooms_filter):
|
||||
u_value_estimate = u_value_estimate[heated_rooms_filter]
|
||||
|
||||
# It's possible for us to have multiple rows if we didn't do a habitable/heated rooms filter so we
|
||||
# average
|
||||
|
||||
return u_value_estimate["median_thermal_transmittance"].mean()
|
||||
|
||||
def recommend_floor_insulation(self, u_value, parts):
|
||||
"""
|
||||
This method is tasked with estimating the impact of performing suspended floor insulation
|
||||
|
|
@ -2,11 +2,10 @@ import itertools
|
|||
import math
|
||||
|
||||
from model_data.Property import Property
|
||||
from model_data.analysis.UvalueEstimations import UvalueEstimations
|
||||
from model_data.BaseUtility import BaseUtility
|
||||
from model_data.recommendations.recommendation_utils import (
|
||||
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_recommended_part, get_uvalue_estimate
|
||||
)
|
||||
|
||||
external_wall_insulation_parts = [
|
||||
|
|
@ -217,9 +216,10 @@ class WallRecommendations(BaseUtility):
|
|||
"solid_brick": 2,
|
||||
}
|
||||
|
||||
def __init__(self, property_instance: Property, uvalue_estimates: UvalueEstimations):
|
||||
def __init__(self, property_instance: Property, uvalue_estimates, total_floor_area_group_decile):
|
||||
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
|
||||
|
||||
|
|
@ -284,7 +284,11 @@ class WallRecommendations(BaseUtility):
|
|||
# This is an estimated figure based on industry standards
|
||||
u_value = self.DEFAULT_U_VALUES["solid_brick"]
|
||||
else:
|
||||
u_value = self._get_walls_uvalue_estimate()
|
||||
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 u_value >= self.BUILDING_REGULATIONS_PART_L_MAX_U_VALUE:
|
||||
|
|
@ -322,10 +326,12 @@ class WallRecommendations(BaseUtility):
|
|||
if new_u_value <= self.BUILDING_REGULATIONS_PART_L_MAX_U_VALUE:
|
||||
lowest_selected_u_value = update_lowest_selected_u_value(lowest_selected_u_value, new_u_value)
|
||||
|
||||
recommendations.append({
|
||||
"parts": [get_recommended_part(part, depth)],
|
||||
"new_u_value": new_u_value,
|
||||
})
|
||||
recommendations.append(
|
||||
{
|
||||
"parts": [get_recommended_part(part, depth)],
|
||||
"new_u_value": new_u_value,
|
||||
}
|
||||
)
|
||||
|
||||
return recommendations
|
||||
|
||||
|
|
@ -398,54 +404,6 @@ class WallRecommendations(BaseUtility):
|
|||
rec for rec in self.recommendations if rec["new_u_value"] >= self.DIMINISHING_RETURNS_U_VALUE
|
||||
]
|
||||
|
||||
def _get_walls_uvalue_estimate(self):
|
||||
|
||||
"""
|
||||
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
|
||||
:return:
|
||||
"""
|
||||
|
||||
total_floor_area_group_decile = self.uvalue_estimates.classify_decile_newvalues(
|
||||
decile_boundaries=self.uvalue_estimates.walls_decile_data["decile_boundaries"],
|
||||
decile_labels=self.uvalue_estimates.walls_decile_data["decile_labels"],
|
||||
new_values=[float(self.property.data["total-floor-area"])],
|
||||
)[0]
|
||||
|
||||
u_value_estimate = self.uvalue_estimates.walls[
|
||||
(self.uvalue_estimates.walls["local-authority"] == self.property.data["local-authority"]) &
|
||||
(self.uvalue_estimates.walls["property-type"] == self.property.data["property-type"]) &
|
||||
(self.uvalue_estimates.walls["built-form"] == self.property.data["built-form"]) &
|
||||
(self.uvalue_estimates.walls["walls-energy-eff"] == self.property.data["walls-energy-eff"]) &
|
||||
(self.uvalue_estimates.walls["walls-env-eff"] == self.property.data["walls-env-eff"]) &
|
||||
(self.uvalue_estimates.walls["total-floor-area_group"] == total_floor_area_group_decile)
|
||||
]
|
||||
|
||||
if u_value_estimate.empty:
|
||||
raise ValueError("No U-value estimate found for the given property")
|
||||
|
||||
# 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
|
||||
|
||||
habitable_rooms_filter = (
|
||||
self.uvalue_estimates.walls["number-habitable-rooms"] == self.property.data["number-habitable-rooms"]
|
||||
)
|
||||
|
||||
if any(habitable_rooms_filter):
|
||||
u_value_estimate = u_value_estimate[habitable_rooms_filter]
|
||||
|
||||
heated_rooms_filter = (
|
||||
self.uvalue_estimates.walls["number-heated-rooms"] == self.property.data["number-heated-rooms"]
|
||||
)
|
||||
|
||||
if any(heated_rooms_filter):
|
||||
u_value_estimate = u_value_estimate[heated_rooms_filter]
|
||||
|
||||
# It's possible for us to have multiple rows if we didn't do a habitable/heated rooms filter so we
|
||||
# average
|
||||
|
||||
return u_value_estimate["median_thermal_transmittance"].mean()
|
||||
|
||||
@staticmethod
|
||||
def rvalue_per_mm(total_r_value: float, thickness_mm: float) -> float:
|
||||
"""Return R-value per mm.
|
||||
0
recommendations/__init__.py
Normal file
0
recommendations/__init__.py
Normal file
|
|
@ -1,4 +1,6 @@
|
|||
from copy import deepcopy
|
||||
from model_data.Property import Property
|
||||
from statistics import mean
|
||||
|
||||
|
||||
def r_value_per_mm_to_u_value(depth_mm: int, r_value_per_mm: float):
|
||||
|
|
@ -110,3 +112,56 @@ def get_recommended_part(part, selected_depth):
|
|||
recommended_part["depths"] = [selected_depth]
|
||||
|
||||
return recommended_part
|
||||
|
||||
|
||||
def get_uvalue_estimate(uvalue_estimates, property: Property, total_floor_area_group_decile):
|
||||
"""
|
||||
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
|
||||
:return:
|
||||
"""
|
||||
|
||||
if not uvalue_estimates:
|
||||
raise ValueError("No U-value estimate found for the given property - investigate")
|
||||
|
||||
# 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
|
||||
]
|
||||
|
||||
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"]]
|
||||
)
|
||||
|
||||
# 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
|
||||
|
||||
habitable_rooms_filer = [
|
||||
x for x in floor_area_filter if
|
||||
x["number-habitable-rooms"] == property.data["number-habitable-rooms"]
|
||||
]
|
||||
|
||||
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"]]
|
||||
)
|
||||
|
||||
# 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"]]
|
||||
)
|
||||
|
||||
return mean(
|
||||
[x["median_thermal_transmittance"] for x in heated_rooms_filter if x["median_thermal_transmittance"]]
|
||||
)
|
||||
|
|
@ -2,7 +2,7 @@ import pickle
|
|||
import pytest
|
||||
import os
|
||||
from unittest.mock import Mock
|
||||
from model_data.recommendations.FloorRecommendations import FloorRecommendations
|
||||
from recommendations.FloorRecommendations import FloorRecommendations
|
||||
|
||||
|
||||
class TestWallRecommendations:
|
||||
|
|
@ -30,17 +30,26 @@ class TestWallRecommendations:
|
|||
|
||||
uvalue_estimates_mock = Mock()
|
||||
|
||||
mock_wall_rec_instance = FloorRecommendations(property_mock, uvalue_estimates_mock)
|
||||
mock_wall_rec_instance = FloorRecommendations(property_mock, uvalue_estimates_mock, "Decile 1")
|
||||
return mock_wall_rec_instance
|
||||
|
||||
def test_init(self, input_properties, uvalue_estimates):
|
||||
obj = FloorRecommendations(property_instance=input_properties[0], uvalue_estimates=uvalue_estimates)
|
||||
obj = FloorRecommendations(
|
||||
property_instance=input_properties[0],
|
||||
uvalue_estimates=uvalue_estimates,
|
||||
total_floor_area_group_decile="Decile 1"
|
||||
)
|
||||
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):
|
||||
recommender = FloorRecommendations(property_instance=input_properties[0], uvalue_estimates=uvalue_estimates)
|
||||
recommender = FloorRecommendations(
|
||||
property_instance=input_properties[0],
|
||||
uvalue_estimates=uvalue_estimates,
|
||||
total_floor_area_group_decile="Decile 1"
|
||||
)
|
||||
recommender.recommend()
|
||||
assert recommender.property.floor["another_property_below"]
|
||||
|
||||
|
|
@ -51,7 +60,11 @@ class TestWallRecommendations:
|
|||
For a suspended floor without insulation, we use the rdsap methogology to estimate a U-value for the floor
|
||||
:return:
|
||||
"""
|
||||
recommender = FloorRecommendations(property_instance=input_properties[2], uvalue_estimates=uvalue_estimates)
|
||||
recommender = FloorRecommendations(
|
||||
property_instance=input_properties[2],
|
||||
uvalue_estimates=uvalue_estimates,
|
||||
total_floor_area_group_decile="Decile 1"
|
||||
)
|
||||
assert recommender.estimated_u_value is None
|
||||
recommender.recommend()
|
||||
assert recommender.property.floor["is_suspended"]
|
||||
|
|
@ -68,7 +81,11 @@ class TestWallRecommendations:
|
|||
does not need floor insulation
|
||||
:return:
|
||||
"""
|
||||
recommender = FloorRecommendations(property_instance=input_properties[3], uvalue_estimates=uvalue_estimates)
|
||||
recommender = FloorRecommendations(
|
||||
property_instance=input_properties[3],
|
||||
uvalue_estimates=uvalue_estimates,
|
||||
total_floor_area_group_decile="Decile 1"
|
||||
)
|
||||
assert recommender.estimated_u_value is None
|
||||
recommender.recommend()
|
||||
assert not recommender.property.floor["is_suspended"]
|
||||
|
|
@ -80,7 +97,11 @@ class TestWallRecommendations:
|
|||
"""
|
||||
:return:
|
||||
"""
|
||||
recommender = FloorRecommendations(property_instance=input_properties[4], uvalue_estimates=uvalue_estimates)
|
||||
recommender = FloorRecommendations(
|
||||
property_instance=input_properties[4],
|
||||
uvalue_estimates=uvalue_estimates,
|
||||
total_floor_area_group_decile="Decile 1"
|
||||
)
|
||||
assert recommender.estimated_u_value is None
|
||||
recommender.recommend()
|
||||
assert not recommender.property.floor["is_suspended"]
|
||||
|
|
@ -96,8 +117,12 @@ class TestWallRecommendations:
|
|||
"""
|
||||
This is another description we see when there is a property below
|
||||
"""
|
||||
input_properties[6].floor
|
||||
recommender = FloorRecommendations(property_instance=input_properties[6], uvalue_estimates=uvalue_estimates)
|
||||
|
||||
recommender = FloorRecommendations(
|
||||
property_instance=input_properties[6],
|
||||
uvalue_estimates=uvalue_estimates,
|
||||
total_floor_area_group_decile="Decile 1"
|
||||
)
|
||||
assert recommender.estimated_u_value is None
|
||||
recommender.recommend()
|
||||
assert not recommender.property.floor["is_suspended"]
|
||||
74
recommendations/tests/test_recommendation_utils.py
Normal file
74
recommendations/tests/test_recommendation_utils.py
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
import pytest
|
||||
from unittest.mock import MagicMock
|
||||
from recommendations import recommendation_utils
|
||||
|
||||
|
||||
class TestRecommendationUtils:
|
||||
@pytest.fixture
|
||||
def property_mock(self):
|
||||
PropertyMock = MagicMock()
|
||||
PropertyMock.data = {
|
||||
'total_floor_area_group_decile': 'Decile 1',
|
||||
'number-habitable-rooms': 3,
|
||||
'number-heated-rooms': 2
|
||||
}
|
||||
return PropertyMock
|
||||
|
||||
def test_r_value_per_mm_to_u_value(self):
|
||||
assert recommendation_utils.r_value_per_mm_to_u_value(1, 2) == 0.5
|
||||
with pytest.raises(ZeroDivisionError):
|
||||
recommendation_utils.r_value_per_mm_to_u_value(0, 2)
|
||||
|
||||
def test_calculate_u_value_uplift(self):
|
||||
assert recommendation_utils.calculate_u_value_uplift(1, 2) == (0.33333333333333337, 0.6666666666666666)
|
||||
with pytest.raises(ZeroDivisionError):
|
||||
recommendation_utils.calculate_u_value_uplift(0, 2)
|
||||
with pytest.raises(ZeroDivisionError):
|
||||
recommendation_utils.calculate_u_value_uplift(1, 0)
|
||||
|
||||
def test_is_diminishing_returns(self):
|
||||
assert not recommendation_utils.is_diminishing_returns([1, 2, 3], 1, 1, 1)
|
||||
assert recommendation_utils.is_diminishing_returns([1, 2, 3], 0.5, 1, 1)
|
||||
assert not recommendation_utils.is_diminishing_returns([], 1, None, 1)
|
||||
|
||||
def test_update_lowest_selected_u_value(self):
|
||||
assert recommendation_utils.update_lowest_selected_u_value(1, 2) == 1
|
||||
assert recommendation_utils.update_lowest_selected_u_value(None, 2) == 2
|
||||
assert recommendation_utils.update_lowest_selected_u_value(1, 0.5) == 0.5
|
||||
|
||||
def test_get_recommended_part(self):
|
||||
part = {'depths': [1, 2, 3]}
|
||||
assert recommendation_utils.get_recommended_part(part, 1) == {'depths': [1]}
|
||||
|
||||
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
|
||||
}
|
||||
]
|
||||
|
||||
assert recommendation_utils.get_uvalue_estimate(uvalue_estimates, property_mock, "Decile 1") == 1.5
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
recommendation_utils.get_uvalue_estimate([], property_mock, "Decile 1")
|
||||
|
||||
# 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
|
||||
}
|
||||
]
|
||||
|
||||
with pytest.raises(KeyError):
|
||||
recommendation_utils.get_uvalue_estimate(uvalue_estimates_missing_key, property_mock, "Decile 1")
|
||||
|
|
@ -5,10 +5,10 @@ import pytest
|
|||
import pickle
|
||||
import numpy as np
|
||||
from unittest.mock import Mock, MagicMock
|
||||
from model_data.recommendations.WallRecommendations import WallRecommendations
|
||||
from recommendations.WallRecommendations import WallRecommendations
|
||||
from model_data.analysis.UvalueEstimations import UvalueEstimations
|
||||
from model_data.Property import Property
|
||||
from model_data.recommendations.recommendation_utils import is_diminishing_returns
|
||||
from recommendations.recommendation_utils import is_diminishing_returns
|
||||
|
||||
|
||||
class TestWallRecommendations:
|
||||
|
|
@ -36,14 +36,19 @@ class TestWallRecommendations:
|
|||
|
||||
uvalue_estimates_mock = Mock()
|
||||
|
||||
mock_wall_rec_instance = WallRecommendations(property_mock, uvalue_estimates_mock)
|
||||
mock_wall_rec_instance = WallRecommendations(property_mock, uvalue_estimates_mock, "Decile 1")
|
||||
return mock_wall_rec_instance
|
||||
|
||||
def test_init(self, input_properties, uvalue_estimates):
|
||||
obj = WallRecommendations(property_instance=input_properties[0], uvalue_estimates=uvalue_estimates)
|
||||
obj = WallRecommendations(
|
||||
property_instance=input_properties[0],
|
||||
uvalue_estimates=uvalue_estimates,
|
||||
total_floor_area_group_decile="Decile 1"
|
||||
)
|
||||
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):
|
||||
"""
|
||||
|
|
@ -55,7 +60,11 @@ class TestWallRecommendations:
|
|||
already has really good insulation, we do NOT recommend any measures for this property
|
||||
"""
|
||||
input_properties[0].year_built = 2014
|
||||
recommender = WallRecommendations(property_instance=input_properties[0], uvalue_estimates=uvalue_estimates)
|
||||
recommender = WallRecommendations(
|
||||
property_instance=input_properties[0],
|
||||
uvalue_estimates=uvalue_estimates,
|
||||
total_floor_area_group_decile="Decile 1"
|
||||
)
|
||||
assert recommender.property.walls["original_description"] == "Average thermal transmittance 0.16 W/m-¦K"
|
||||
recommender.recommend()
|
||||
# This should be empty
|
||||
|
|
@ -71,7 +80,11 @@ class TestWallRecommendations:
|
|||
This property is not in a conservation area, however it's a flat so we don't recommend external wall insulation
|
||||
"""
|
||||
input_properties[1].year_built = 1930
|
||||
recommender = WallRecommendations(property_instance=input_properties[1], uvalue_estimates=uvalue_estimates)
|
||||
recommender = WallRecommendations(
|
||||
property_instance=input_properties[1],
|
||||
uvalue_estimates=uvalue_estimates,
|
||||
total_floor_area_group_decile="Decile 1"
|
||||
)
|
||||
assert recommender.property.walls["original_description"] == "Solid brick, as built, no insulation (assumed)"
|
||||
assert not recommender.ewi_valid
|
||||
assert recommender.property.in_conservation_area == "not_in_conservation_area"
|
||||
|
|
@ -106,7 +119,11 @@ class TestWallRecommendations:
|
|||
"""
|
||||
|
||||
input_properties[6].year_built = 1991
|
||||
recommender = WallRecommendations(property_instance=input_properties[6], uvalue_estimates=uvalue_estimates)
|
||||
recommender = WallRecommendations(
|
||||
property_instance=input_properties[6],
|
||||
uvalue_estimates=uvalue_estimates.walls.to_dict("records"),
|
||||
total_floor_area_group_decile="Decile 1"
|
||||
)
|
||||
|
||||
assert recommender.property.walls["original_description"] == "Solid brick, as built, insulated (assumed)"
|
||||
assert not recommender.ewi_valid
|
||||
|
|
@ -118,7 +135,7 @@ class TestWallRecommendations:
|
|||
|
||||
# We should result in some recommendations, all of which should be internal wall insulation
|
||||
assert recommender.recommendations
|
||||
assert recommender.estimated_u_value == 0.45
|
||||
assert recommender.estimated_u_value == 0.4115686274509804
|
||||
|
||||
rec_types = {part["type"] for rec in recommender.recommendations for part in rec["parts"]}
|
||||
assert rec_types == {"internal_wall_insulation"}
|
||||
|
|
@ -221,7 +238,7 @@ class TestWallRecommendationsBase:
|
|||
|
||||
@pytest.fixture
|
||||
def wall_recommendations_instance(self, property_mock, uvalue_estimations_mock):
|
||||
wall_recommendations_instance = WallRecommendations(property_mock, uvalue_estimations_mock)
|
||||
wall_recommendations_instance = WallRecommendations(property_mock, uvalue_estimations_mock, "Decile 1")
|
||||
wall_recommendations_instance.uvalue_estimates.walls_decile_data = {
|
||||
"decile_labels": MagicMock(),
|
||||
"decile_boundaries": MagicMock()
|
||||
|
|
@ -241,21 +258,6 @@ class TestWallRecommendationsBase:
|
|||
wall_recommendations_instance.property.data = {"property-type": "house"}
|
||||
assert wall_recommendations_instance.ewi_valid is True
|
||||
|
||||
def test_get_walls_uvalue_estimate(self, wall_recommendations_instance, uvalue_estimations_mock):
|
||||
wall_recommendations_instance.uvalue_estimates = uvalue_estimations_mock
|
||||
wall_recommendations_instance.property.data = {
|
||||
"local-authority": "E09000012",
|
||||
"property-type": "Bungalow",
|
||||
"built-form": "End-Terrace",
|
||||
"walls-energy-eff": "Very Good",
|
||||
"walls-env-eff": "Very Good",
|
||||
"total-floor-area": 10,
|
||||
"number-habitable-rooms": "",
|
||||
"number-heated-rooms": ""
|
||||
}
|
||||
|
||||
assert wall_recommendations_instance._get_walls_uvalue_estimate() == 0.15
|
||||
|
||||
def test_recommend_without_u_value(self, wall_recommendations_instance):
|
||||
wall_recommendations_instance.property.walls = {
|
||||
"thermal_transmittance": None,
|
||||
49
utils/tests/test_logger.py
Normal file
49
utils/tests/test_logger.py
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import logging
|
||||
from io import StringIO
|
||||
from unittest.mock import patch
|
||||
from utils.logger import setup_logger
|
||||
|
||||
|
||||
class TestLogger:
|
||||
def test_setup_logger_default(self):
|
||||
log_stream = StringIO()
|
||||
handler = logging.StreamHandler(log_stream)
|
||||
logger = setup_logger()
|
||||
logger.addHandler(handler)
|
||||
|
||||
# log something
|
||||
logger.info("Hello World!")
|
||||
|
||||
log_stream.seek(0)
|
||||
# assert that log was written
|
||||
assert log_stream.read() == "Hello World!\n"
|
||||
# remove the handler after use
|
||||
logger.removeHandler(handler)
|
||||
|
||||
@patch('logging.FileHandler')
|
||||
def test_setup_logger_file(self, mock_file_handler):
|
||||
# setup the logger
|
||||
logger = setup_logger(log_file='test.log', overwrite_handler=True)
|
||||
|
||||
# assert FileHandler was called correctly
|
||||
mock_file_handler.assert_called_once_with('test.log')
|
||||
|
||||
# clean up after use
|
||||
for handler in logger.handlers[:]:
|
||||
handler.close()
|
||||
logger.removeHandler(handler)
|
||||
|
||||
def test_setup_logger_loglevel(self):
|
||||
log_stream = StringIO()
|
||||
handler = logging.StreamHandler(log_stream)
|
||||
logger = setup_logger(level=logging.DEBUG)
|
||||
logger.addHandler(handler)
|
||||
|
||||
# log something
|
||||
logger.debug("Hello World!")
|
||||
|
||||
log_stream.seek(0)
|
||||
# assert that log was written
|
||||
assert log_stream.read() == "Hello World!\n"
|
||||
# remove the handler after use
|
||||
logger.removeHandler(handler)
|
||||
80
utils/tests/test_uvalue_estimates.py
Normal file
80
utils/tests/test_uvalue_estimates.py
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
from utils.uvalue_estimates import classify_decile_newvalues
|
||||
|
||||
|
||||
def test_classify_decile_newvalues_edge_cases():
|
||||
decile_labels = [f"Decile {i + 1}" for i in range(10)]
|
||||
decile_boundaries = list(range(11))
|
||||
|
||||
# Test with values at the exact boundaries
|
||||
assert classify_decile_newvalues(decile_boundaries, decile_labels, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) == ['Decile 1',
|
||||
'Decile 2',
|
||||
'Decile 3',
|
||||
'Decile 4',
|
||||
'Decile 5',
|
||||
'Decile 6',
|
||||
'Decile 7',
|
||||
'Decile 8',
|
||||
'Decile 9',
|
||||
'Decile 10']
|
||||
|
||||
# Test with values at the exact boundaries, but in reverse order
|
||||
assert classify_decile_newvalues(decile_boundaries, decile_labels, [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]) == ['Decile 10',
|
||||
'Decile 9',
|
||||
'Decile 8',
|
||||
'Decile 7',
|
||||
'Decile 6',
|
||||
'Decile 5',
|
||||
'Decile 4',
|
||||
'Decile 3',
|
||||
'Decile 2',
|
||||
'Decile 1']
|
||||
|
||||
# Test with values just below the boundaries
|
||||
assert classify_decile_newvalues(decile_boundaries, decile_labels, [x - 0.5 for x in range(2, 12)]) == ['Decile 1',
|
||||
'Decile 2',
|
||||
'Decile 3',
|
||||
'Decile 4',
|
||||
'Decile 5',
|
||||
'Decile 6',
|
||||
'Decile 7',
|
||||
'Decile 8',
|
||||
'Decile 9',
|
||||
'Decile 10']
|
||||
|
||||
# Test with values just above the boundaries
|
||||
assert classify_decile_newvalues(decile_boundaries, decile_labels, [x + 0.5 for x in range(1, 11)]) == ['Decile 2',
|
||||
'Decile 3',
|
||||
'Decile 4',
|
||||
'Decile 5',
|
||||
'Decile 6',
|
||||
'Decile 7',
|
||||
'Decile 8',
|
||||
'Decile 9',
|
||||
'Decile 10',
|
||||
None]
|
||||
|
||||
# Test with empty list
|
||||
assert classify_decile_newvalues(decile_boundaries, decile_labels, []) == []
|
||||
|
||||
# Test with a single value
|
||||
assert classify_decile_newvalues(decile_boundaries, decile_labels, [5.5]) == ['Decile 6']
|
||||
|
||||
# Test with all values the same
|
||||
assert classify_decile_newvalues(decile_boundaries, decile_labels, [5, 5, 5, 5, 5]) == ['Decile 5', 'Decile 5',
|
||||
'Decile 5', 'Decile 5',
|
||||
'Decile 5']
|
||||
|
||||
# Test with values out of order
|
||||
assert classify_decile_newvalues(decile_boundaries, decile_labels, [10, 5, 1, 7, 3]) == ['Decile 10', 'Decile 5',
|
||||
'Decile 1', 'Decile 7',
|
||||
'Decile 3']
|
||||
|
||||
# Test with negative decile boundaries
|
||||
decile_boundaries = list(range(-10, 1))
|
||||
assert classify_decile_newvalues(decile_boundaries, decile_labels, [-9, -5, -1]) == ['Decile 2', 'Decile 6',
|
||||
'Decile 10']
|
||||
|
||||
# Test with floating point decile boundaries
|
||||
decile_boundaries = [x / 10 for x in range(11)]
|
||||
assert classify_decile_newvalues(decile_boundaries, decile_labels, [0.35, 0.55, 0.75]) == ['Decile 4', 'Decile 6',
|
||||
'Decile 8']
|
||||
39
utils/uvalue_estimates.py
Normal file
39
utils/uvalue_estimates.py
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
from typing import List
|
||||
|
||||
from bisect import bisect_left
|
||||
|
||||
|
||||
def classify_decile_newvalues(
|
||||
decile_boundaries: List[float], decile_labels: List[str], new_values: List[float]
|
||||
) -> List[str]:
|
||||
"""
|
||||
Classify a list of new values into pre-established deciles.
|
||||
|
||||
This function is an alternative to UvalueEstimations.classify_decile_newvalues that does not depend on pandas,
|
||||
making it suitable for use in environments where pandas may not be available (such as AWS Lambda).
|
||||
|
||||
:param decile_boundaries: A list of decile boundaries. These define the ranges of the deciles.
|
||||
:param decile_labels: A list of labels for the deciles. These are the classifications to be assigned to the values.
|
||||
:param new_values: A list of new values to be classified into the deciles.
|
||||
|
||||
:return: A list of classifications for the new values. Each classification corresponds to the decile in which
|
||||
the respective new value falls. If a value falls outside the range of the deciles, its classification is
|
||||
None.
|
||||
"""
|
||||
classifications = []
|
||||
|
||||
# For each new value...
|
||||
for value in new_values:
|
||||
# If the value is outside the range of the deciles, classify it as None
|
||||
if value < decile_boundaries[0] or value > decile_boundaries[-1]:
|
||||
classifications.append(None)
|
||||
else:
|
||||
# Use bisect_left to find the decile in which the value falls
|
||||
i = bisect_left(decile_boundaries, value)
|
||||
# If the value falls exactly on a decile boundary, classify it in the lower decile
|
||||
if i:
|
||||
i -= 1
|
||||
# Append the classification to the list of classifications
|
||||
classifications.append(decile_labels[i])
|
||||
return classifications
|
||||
|
||||
Loading…
Add table
Reference in a new issue