mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
Merge pull request #261 from Hestia-Homes/birmingham-pilot
Birmingham pilot
This commit is contained in:
commit
9812bf0fb9
13 changed files with 414 additions and 56 deletions
2
.idea/Model.iml
generated
2
.idea/Model.iml
generated
|
|
@ -7,7 +7,7 @@
|
|||
<sourceFolder url="file://$MODULE_DIR$/open_uprn" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/recommendations" isTestSource="false" />
|
||||
</content>
|
||||
<orderEntry type="jdk" jdkName="Python 3.10 (model_data)" jdkType="Python SDK" />
|
||||
<orderEntry type="jdk" jdkName="Python 3.10 (backend)" jdkType="Python SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
<component name="PyNamespacePackagesService">
|
||||
|
|
|
|||
2
.idea/misc.xml
generated
2
.idea/misc.xml
generated
|
|
@ -3,7 +3,7 @@
|
|||
<component name="Black">
|
||||
<option name="sdkName" value="Python 3.10 (backend)" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.10 (model_data)" project-jdk-type="Python SDK" />
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.10 (backend)" project-jdk-type="Python SDK" />
|
||||
<component name="PythonCompatibilityInspectionAdvertiser">
|
||||
<option name="version" value="3" />
|
||||
</component>
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ from utils.logger import setup_logger
|
|||
from utils.s3 import read_dataframe_from_s3_parquet
|
||||
from epc_api.client import EpcClient
|
||||
from BaseUtility import Definitions
|
||||
from recommendations.rdsap_tables import england_wales_age_band_lookup
|
||||
from recommendations.rdsap_tables import england_wales_age_band_lookup, FLOOR_LEVEL_MAP
|
||||
from recommendations.recommendation_utils import (
|
||||
estimate_perimeter, get_wall_type, estimate_external_wall_area, esimtate_pitched_roof_area
|
||||
)
|
||||
|
|
@ -84,6 +84,7 @@ class Property(Definitions):
|
|||
self.pitched_roof_area = None
|
||||
self.insulation_floor_area = None
|
||||
self.number_lighting_outlets = None
|
||||
self.floor_level = None
|
||||
|
||||
self.current_adjusted_energy = None
|
||||
self.expected_adjusted_energy = None
|
||||
|
|
@ -324,6 +325,7 @@ class Property(Definitions):
|
|||
|
||||
self.set_wall_type()
|
||||
self.set_floor_type()
|
||||
self.set_floor_level()
|
||||
|
||||
def set_age_band(self):
|
||||
"""
|
||||
|
|
@ -369,7 +371,8 @@ class Property(Definitions):
|
|||
self.is_listed = spatial["is_listed_building"].values[0]
|
||||
self.is_heritage = spatial["is_heritage_building"].values[0]
|
||||
|
||||
if self.in_conservation_area is True | self.is_listed is True | self.is_heritage is True:
|
||||
# We do an equals True, in the case of one of these variables being True
|
||||
if (self.in_conservation_area == True) | (self.is_listed == True) | (self.is_heritage == True):
|
||||
self.restricted_measures = True
|
||||
|
||||
spatial_dict = spatial.to_dict("records")[0]
|
||||
|
|
@ -641,6 +644,38 @@ class Property(Definitions):
|
|||
floor_area=self.insulation_floor_area, floor_height=self.floor_height
|
||||
)
|
||||
|
||||
def set_floor_level(self):
|
||||
self.floor_level = (
|
||||
FLOOR_LEVEL_MAP[self.data["floor-level"]] if
|
||||
self.data["floor-level"] not in self.DATA_ANOMALY_MATCHES else None
|
||||
)
|
||||
|
||||
if self.floor_level is None:
|
||||
|
||||
if self.data["property-type"] != "Flat":
|
||||
return
|
||||
|
||||
if self.floor["another_property_below"]:
|
||||
self.floor_level = 1
|
||||
else:
|
||||
self.floor_level = 0
|
||||
return
|
||||
|
||||
# We perform some extra checks, if the property is not on the ground floor, as we have found cases
|
||||
# where a property is marked as being on the first floor
|
||||
if self.floor_level > 0:
|
||||
|
||||
# We check if there is another property below
|
||||
if not self.floor["another_property_below"]:
|
||||
self.floor_level = 0
|
||||
return
|
||||
|
||||
if self.floor_level == 0:
|
||||
# Check if another property below
|
||||
if self.floor["another_property_below"]:
|
||||
self.floor_level = 1
|
||||
return
|
||||
|
||||
def set_wall_type(self):
|
||||
"""
|
||||
This method sets the wall type of the property, using a simple approach based on the wall description
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ class MaterialType(enum.Enum):
|
|||
low_energy_lighting_installation = "low_energy_lighting_installation"
|
||||
flat_roof_preparation = "flat_roof_preparation"
|
||||
flat_roof_vapour_barrier = "flat_roof_vapour_barrier"
|
||||
flat_roof_waterpoofing = "flat_roof_waterpoofing"
|
||||
flat_roof_waterproofing = "flat_roof_waterproofing"
|
||||
|
||||
|
||||
class DepthUnit(enum.Enum):
|
||||
|
|
|
|||
|
|
@ -298,6 +298,17 @@ async def trigger_plan(body: PlanTriggerRequest):
|
|||
t for t in missing_types if t not in ["internal_wall_insulation", "external_wall_insulation"]
|
||||
]
|
||||
|
||||
# We check if NO wall insulation was selected but iwi and ewi are available
|
||||
# This condition will check
|
||||
# 1) iwi and ewi are both in missing_types
|
||||
# 2) iwi and ewi are not in default_types
|
||||
# If both of these are true, it means that no wall insulation was selected via the optimisation routine
|
||||
# but both are possible, so we need to select a default. We default to iwi because it's usually cheaper
|
||||
if (("internal_wall_insulation" in missing_types) and ("external_wall_insulation" in missing_types)) and (
|
||||
("internal_wall_insulation" not in default_types) and ("external_wall_insulation" not in default_types)
|
||||
):
|
||||
missing_types = [t for t in missing_types if t != "external_wall_insulation"]
|
||||
|
||||
if missing_types:
|
||||
for missed_type in missing_types:
|
||||
missed = [r for r in property_recommendations if r["type"] == missed_type]
|
||||
|
|
@ -404,12 +415,10 @@ async def trigger_plan(body: PlanTriggerRequest):
|
|||
|
||||
# We sum up the SAP points of the default recommendations and calculate a new EPC category. This
|
||||
# category is then used to produce adjusted energy figures
|
||||
total_sap_points = sum([x["sap_points"] for x in representative_recs[property_id]])
|
||||
expected_epc = sap_to_epc(float(property_instance.data["current-energy-efficiency"]) + total_sap_points)
|
||||
|
||||
expected_adjusted_energy = AnnualBillSavings.adjust_energy_to_metered(
|
||||
epc_energy_consumption=expected_heat_demand,
|
||||
current_epc_rating=expected_epc,
|
||||
current_epc_rating=property_instance.data["current-energy-rating"],
|
||||
)
|
||||
|
||||
heat_demand_change = (
|
||||
|
|
@ -531,8 +540,10 @@ async def trigger_plan(body: PlanTriggerRequest):
|
|||
new_sap_points = float(p.data["current-energy-efficiency"]) + total_sap_points
|
||||
new_epc = sap_to_epc(new_sap_points)
|
||||
|
||||
valuations = PropertyValuation.estimate(property_instance=p, target_epc=new_epc)
|
||||
|
||||
property_valuation_increases.append(
|
||||
PropertyValuation.estimate(property_instance=p, target_epc=new_epc)
|
||||
valuations["average_increased_value"] - valuations["current_value"]
|
||||
)
|
||||
|
||||
# Commit the session after each batch
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ class AnnualBillSavings:
|
|||
# This is a weighted mean of the price caps, using the consumption figures above as weights
|
||||
PRICE_FACTOR = 0.11183098591549295
|
||||
|
||||
EPC_BANDS = ["G", "F", "E", "D", "C", "B", "A"]
|
||||
|
||||
@classmethod
|
||||
def estimate(cls, kwh: float):
|
||||
"""
|
||||
|
|
@ -70,3 +72,22 @@ class AnnualBillSavings:
|
|||
adjusted_consumption = (epc_energy_consumption + consumption_difference)
|
||||
|
||||
return adjusted_consumption
|
||||
|
||||
@classmethod
|
||||
def adjust_expected_band(cls, expected_epc_rating, current_epc_rating):
|
||||
"""
|
||||
Because of the differing intercepts and intercepts when adjusting, it's possible for
|
||||
expected_adjusted_energy to be bigger than current_adjusted_energy. In this case, we'll
|
||||
adjust, against at most 1 EPC band above the curent. This function performs the EPC adjustment
|
||||
:param expected_epc_rating: The expected EPC rating
|
||||
:param current_epc_rating: The current EPC rating
|
||||
"""
|
||||
|
||||
# Find index of expected EPC rating
|
||||
expected_index = cls.EPC_BANDS.index(expected_epc_rating)
|
||||
current_index = cls.EPC_BANDS.index(current_epc_rating)
|
||||
|
||||
if expected_index - 1 < current_index:
|
||||
return current_epc_rating
|
||||
|
||||
return cls.EPC_BANDS[expected_index - 1]
|
||||
|
|
|
|||
|
|
@ -1,22 +1,121 @@
|
|||
import numpy as np
|
||||
|
||||
|
||||
class PropertyValuation:
|
||||
"""
|
||||
This is a placeholder class for the property valuation model
|
||||
"""
|
||||
|
||||
UPRN_VALUE_LOOKUP = {
|
||||
15038202: {"current_value": 202000, "increase_percentage": 0.05725},
|
||||
37024763: {"current_value": 213000, "increase_percentage": 0.025},
|
||||
15038202: 202000,
|
||||
37024763: 213000,
|
||||
100070478545: 212000,
|
||||
100070297696: 662000, # Based on Zoopla's estimation of nearby house, 8 bloomfield road
|
||||
100070476394: 222000, # Based on Zoopla's estimation of next door, 20 Parkside
|
||||
100071264896: 128000,
|
||||
# Based on next door neighbour: https://themovemarket.com/tools/propertyprices/flat-2-queens-wood-house-219
|
||||
# -brandwood-road-birmingham-b14-6pu
|
||||
100070533688: 218000, # Based on Zoopla's estimation of 95 Tenby Road, which is also end terrace
|
||||
100070505235: 344000, # Based on Zoopla's estimation of 131 School road, which is also semi-detached
|
||||
100070513306: 182000, # Based on Zoopla's estimation of 61 Simmons Drive
|
||||
100071306896: 77000, # Based on Flat 2 of 44 Wedgewood Road on Zoopla
|
||||
}
|
||||
|
||||
# We base our valuation uplifts on a number of sources
|
||||
# https://www.moneysupermarket.com/gas-and-electricity/value-of-efficiency/
|
||||
MSM_MAPPING = [
|
||||
{"start": "G", "end": "F", "increase_percentage": 0.06},
|
||||
{"start": "F", "end": "E", "increase_percentage": 0.01},
|
||||
{"start": "E", "end": "D", "increase_percentage": 0.01},
|
||||
{"start": "D", "end": "C", "increase_percentage": 0.02},
|
||||
{"start": "C", "end": "B", "increase_percentage": 0.04},
|
||||
{"start": "B", "end": "A", "increase_percentage": 0.0},
|
||||
]
|
||||
|
||||
# https://www.lloydsbankinggroup.com/media/press-releases/2021/halifax/homebuyers-pay-a-green-premium-of-40000
|
||||
# -for-the-most-energy-efficient-properties.html
|
||||
LLOYDS_MAPPING = [
|
||||
{"start": "G", "end": "F", "increase_percentage": 0.038},
|
||||
{"start": "F", "end": "E", "increase_percentage": 0.029},
|
||||
{"start": "E", "end": "D", "increase_percentage": 0.024},
|
||||
{"start": "D", "end": "C", "increase_percentage": 0.02},
|
||||
{"start": "C", "end": "B", "increase_percentage": 0.02},
|
||||
{"start": "B", "end": "A", "increase_percentage": 0.018},
|
||||
]
|
||||
|
||||
KNIGHT_FRANK_MAPPING = [
|
||||
{"start": "D", "end": "C", "increase_percentage": 0.03},
|
||||
{"start": "D", "end": "B", "increase_percentage": 0.088},
|
||||
]
|
||||
|
||||
NATIONWIDE_MAPPING = [
|
||||
{"start": "G", "end": "D", "increase_percentage": 0.035},
|
||||
{"start": "F", "end": "D", "increase_percentage": 0.035},
|
||||
{"start": "D", "end": "B", "increase_percentage": 0.017},
|
||||
{"start": "D", "end": "A", "increase_percentage": 0.017},
|
||||
]
|
||||
|
||||
EPC_BANDS = ["G", "F", "E", "D", "C", "B", "A"]
|
||||
|
||||
@classmethod
|
||||
def get_increase(cls, epc_band_range):
|
||||
|
||||
increases = []
|
||||
for i in range(len(epc_band_range)):
|
||||
|
||||
if i == len(epc_band_range) - 1:
|
||||
break
|
||||
|
||||
current = epc_band_range[i]
|
||||
next = epc_band_range[i + 1]
|
||||
|
||||
msm_increase = [x for x in cls.MSM_MAPPING if x["start"] == current and x["end"] == next][0]
|
||||
lloyds_increase = [x for x in cls.LLOYDS_MAPPING if x["start"] == current and x["end"] == next][0]
|
||||
|
||||
increases.append(
|
||||
{
|
||||
"start": current,
|
||||
"end": next,
|
||||
"msm_increase": msm_increase["increase_percentage"],
|
||||
"lloyds_increase": lloyds_increase["increase_percentage"],
|
||||
}
|
||||
)
|
||||
|
||||
# We now aggregate the increases. The should be compound increases so we multiply them together
|
||||
msm_increase = np.prod([1 + x["msm_increase"] for x in increases]) - 1
|
||||
lloyds_increase = np.prod([1 + x["lloyds_increase"] for x in increases]) - 1
|
||||
|
||||
return msm_increase, lloyds_increase
|
||||
|
||||
@classmethod
|
||||
def estimate(cls, property_instance, target_epc):
|
||||
data = cls.UPRN_VALUE_LOOKUP.get(property_instance.uprn)
|
||||
value = cls.UPRN_VALUE_LOOKUP.get(property_instance.uprn)
|
||||
|
||||
if not data:
|
||||
if not value:
|
||||
raise ValueError("Have not implemented valuation for this property")
|
||||
|
||||
new_valuation = (1 + data["increase_percentage"]) * data["current_value"]
|
||||
current_epc = property_instance.data["current-energy-rating"]
|
||||
# We get the spectrum of ratings between the current and target EPC
|
||||
epc_band_range = cls.EPC_BANDS[cls.EPC_BANDS.index(current_epc): cls.EPC_BANDS.index(target_epc) + 1]
|
||||
|
||||
increase = round(new_valuation - data["current_value"], 2)
|
||||
msm_increase, lloyds_increase = cls.get_increase(epc_band_range)
|
||||
|
||||
return increase
|
||||
# We now use the knight frank and nationwide data to get further valuation evidence, if we have it
|
||||
kf_increase = [x for x in cls.KNIGHT_FRANK_MAPPING if x["start"] == current_epc and x["end"] == target_epc]
|
||||
nw_increase = [x for x in cls.NATIONWIDE_MAPPING if x["start"] == current_epc and x["end"] == target_epc]
|
||||
|
||||
kf_increase = kf_increase[0]["increase_percentage"] if kf_increase else None
|
||||
nw_increase = nw_increase[0]["increase_percentage"] if nw_increase else None
|
||||
|
||||
all_increases = [x for x in [msm_increase, lloyds_increase, kf_increase, nw_increase] if x is not None]
|
||||
|
||||
max_increase = max(all_increases)
|
||||
min_increase = min(all_increases)
|
||||
avg_increase = np.mean(all_increases)
|
||||
|
||||
return {
|
||||
"current_value": value,
|
||||
"lower_bound_increased_value": value * (1 + min_increase),
|
||||
"upper_bound_increased_value": value * (1 + max_increase),
|
||||
"average_increased_value": value * (1 + avg_increase),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import pandas as pd
|
||||
import pytest
|
||||
from unittest.mock import Mock
|
||||
from epc_api.client import EpcClient
|
||||
|
|
@ -345,3 +346,95 @@ class TestProperty:
|
|||
# Verify that ValueError is raised when multiple attributes are found
|
||||
with pytest.raises(ValueError, match="Either No attributes or multiple found for roof-description"):
|
||||
property_instance.get_components(cleaned)
|
||||
|
||||
def test_set_spatial(self, mock_epc_client):
|
||||
prop = Property(1, "AB12CD", "Test Address", mock_epc_client)
|
||||
|
||||
spatial1 = pd.DataFrame([{
|
||||
'X_COORDINATE': 411143.0, 'Y_COORDINATE': 281701.0, 'LATITUDE': 52.4331896, 'LONGITUDE': -1.8375238,
|
||||
'conservation_status': True, 'is_listed_building': False, 'is_heritage_building': True
|
||||
}])
|
||||
|
||||
prop.set_spatial(spatial1)
|
||||
|
||||
assert prop.in_conservation_area
|
||||
assert not prop.is_listed
|
||||
assert prop.is_heritage
|
||||
assert prop.restricted_measures
|
||||
|
||||
prop2 = Property(1, "AB12CD", "Test Address", mock_epc_client)
|
||||
|
||||
spatial2 = pd.DataFrame([{
|
||||
'X_COORDINATE': 411143.0, 'Y_COORDINATE': 281701.0, 'LATITUDE': 52.4331896, 'LONGITUDE': -1.8375238,
|
||||
'conservation_status': None, 'is_listed_building': False, 'is_heritage_building': False
|
||||
}])
|
||||
|
||||
prop2.set_spatial(spatial2)
|
||||
|
||||
assert prop2.in_conservation_area is None
|
||||
assert not prop2.is_listed
|
||||
assert not prop2.is_heritage
|
||||
assert not prop2.restricted_measures
|
||||
|
||||
def test_set_floor_level(self, mock_epc_client):
|
||||
# In this case, we have a flat which looks looks it's on the first floor, but it's actually on the ground
|
||||
# floor, so we should set floor_level to 0
|
||||
prop = Property(1, "AB12CD", "Test Address", mock_epc_client)
|
||||
prop.data = {'floor-level': '01', 'property-type': 'Flat'}
|
||||
prop.floor = {
|
||||
'original_description': 'Solid, no insulation (assumed)', 'clean_description': 'Solid, no insulation',
|
||||
'thermal_transmittance': None, 'thermal_transmittance_unit': None, 'is_assumed': True,
|
||||
'is_to_unheated_space': False, 'is_to_external_air': False, 'is_suspended': False, 'is_solid': True,
|
||||
'another_property_below': False, 'insulation_thickness': 'none', 'floor_thermal_transmittance': None,
|
||||
'floor_insulation_thickness': 'none'
|
||||
}
|
||||
|
||||
prop.set_floor_level()
|
||||
|
||||
assert prop.floor_level == 0
|
||||
|
||||
# This property is labelled as being on the ground floor but actually has another property below
|
||||
# so we set floor level to 1
|
||||
prop2 = Property(1, "AB12CD", "Test Address", mock_epc_client)
|
||||
prop2.data = {'floor-level': 'Ground', 'property-type': 'Flat'}
|
||||
prop2.floor = {
|
||||
'original_description': '(Another dwelling below)', 'clean_description': 'Solid, no insulation',
|
||||
'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,
|
||||
'another_property_below': True, 'insulation_thickness': 'none', 'floor_thermal_transmittance': None,
|
||||
'floor_insulation_thickness': 'none'
|
||||
}
|
||||
|
||||
prop2.set_floor_level()
|
||||
|
||||
assert prop2.floor_level == 1
|
||||
|
||||
# this property is correctly labelled as being on the 2nd floor
|
||||
prop3 = Property(1, "AB12CD", "Test Address", mock_epc_client)
|
||||
prop3.data = {'floor-level': '02', 'property-type': 'Flat'}
|
||||
prop3.floor = {
|
||||
'original_description': '(Another dwelling below)', 'clean_description': 'Solid, no insulation',
|
||||
'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,
|
||||
'another_property_below': True, 'insulation_thickness': 'none', 'floor_thermal_transmittance': None,
|
||||
'floor_insulation_thickness': 'none'
|
||||
}
|
||||
|
||||
prop3.set_floor_level()
|
||||
|
||||
assert prop3.floor_level == 2
|
||||
|
||||
# Example of a house
|
||||
prop4 = Property(1, "AB12CD", "Test Address", mock_epc_client)
|
||||
prop4.data = {'floor-level': '', 'property-type': 'House'}
|
||||
prop4.floor = {
|
||||
'original_description': '(Another dwelling below)', 'clean_description': 'Solid, no insulation',
|
||||
'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,
|
||||
'another_property_below': False, 'insulation_thickness': 'none', 'floor_thermal_transmittance': None,
|
||||
'floor_insulation_thickness': 'none'
|
||||
}
|
||||
|
||||
prop4.set_floor_level()
|
||||
|
||||
assert prop4.floor_level is None
|
||||
|
|
|
|||
|
|
@ -22,49 +22,149 @@ def app():
|
|||
|
||||
# Birmingham has a Local Authority Code of E08000025
|
||||
|
||||
# ~~~~~~~~~~~~~~~~~~~~
|
||||
# First example
|
||||
# ~~~~~~~~~~~~~~~~~~~~
|
||||
# Let's take an EPC D property
|
||||
example_1_reponse = epc_client.domestic.search(
|
||||
params={
|
||||
"local-authority": "E08000025",
|
||||
"property-type": "house",
|
||||
}
|
||||
},
|
||||
size=1000
|
||||
)
|
||||
|
||||
g_data = epc_client.domestic.search(params={"energy-band": "g"}, size=n_g)
|
||||
f_data = epc_client.domestic.search(params={"energy-band": "f"}, size=n_f)
|
||||
e_data = epc_client.domestic.search(params={"energy-band": "e"}, size=n_e)
|
||||
d_data = epc_client.domestic.search(params={"energy-band": "d"}, size=n_d)
|
||||
c_data = epc_client.domestic.search(params={"energy-band": "c"}, size=n_c)
|
||||
b_data = epc_client.domestic.search(params={"energy-band": "b"}, size=n_b)
|
||||
a_data = epc_client.domestic.search(params={"energy-band": "a"}, size=n_a)
|
||||
|
||||
# Combine the final data
|
||||
final_data = (
|
||||
g_data["rows"] + f_data["rows"] + e_data["rows"] + d_data["rows"] + c_data["rows"] + b_data["rows"]
|
||||
+ a_data["rows"]
|
||||
)
|
||||
|
||||
# TODO: We also take homes with just a specific type of wall
|
||||
|
||||
final_data = [
|
||||
x for x in final_data if ("cavity wall" in x["walls-description"].lower()) or (
|
||||
"solid brick" in x["walls-description"].lower()
|
||||
) or ("average thermal transmittance" in x["walls-description"].lower())
|
||||
example_1_reponse = example_1_reponse["rows"]
|
||||
# Get a property with a cavity wall
|
||||
example_1_reponse_filtered = [
|
||||
x for x in example_1_reponse if
|
||||
"cavity wall, as built, no insulation (assumed)" in x["walls-description"].lower()
|
||||
]
|
||||
example_1_reponse_filtered = [
|
||||
x for x in example_1_reponse_filtered if "pitched, no insulation (assumed)" in x["roof-description"].lower()
|
||||
]
|
||||
# Get a social housing property
|
||||
example_1_reponse_filtered = [
|
||||
x for x in example_1_reponse_filtered if x["tenure"] == "Rented (social)"
|
||||
]
|
||||
|
||||
# TODO: For the moment, don't use park homes
|
||||
final_csv_data = pd.DataFrame(
|
||||
[{"address": x["address"], "postcode": x["postcode"], "Notes": None} for x
|
||||
in final_data if
|
||||
x["property-type"] not in ["Park home"]]
|
||||
)
|
||||
print(example_1_reponse_filtered[0]["postcode"])
|
||||
# B13 9LT
|
||||
print(example_1_reponse_filtered[0]["address1"])
|
||||
# 113 Tenby Road
|
||||
print(example_1_reponse_filtered[0]["built-form"])
|
||||
# Mid-Terrace
|
||||
print(example_1_reponse_filtered[0]["current-energy-rating"])
|
||||
# 'D'
|
||||
|
||||
final_csv_data = pd.concat([starting_csv, final_csv_data]).reset_index(drop=True)
|
||||
# ~~~~~~~~~~~~~~~~~~~~
|
||||
# Second example
|
||||
# ~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
# Let's take an EPC E property
|
||||
example_2_reponse = epc_client.domestic.search(
|
||||
params={
|
||||
"local-authority": "E08000025",
|
||||
"property-type": "house",
|
||||
"energy-band": "e"
|
||||
},
|
||||
size=1000
|
||||
)
|
||||
example_2_reponse = example_2_reponse["rows"]
|
||||
# Get a solid wall example
|
||||
example_2_reponse_filtered = [
|
||||
x for x in example_2_reponse if
|
||||
"solid brick, as built, no insulation (assumed)" in x["walls-description"].lower()
|
||||
]
|
||||
# With some existing loft insulation
|
||||
example_2_reponse_filtered = [
|
||||
x for x in example_2_reponse_filtered if "pitched, 100 mm loft insulation" in x["roof-description"].lower()
|
||||
]
|
||||
# Get a social housing property
|
||||
example_2_reponse_filtered = [
|
||||
x for x in example_2_reponse_filtered if x["tenure"] == "Rented (social)"
|
||||
]
|
||||
|
||||
print(example_2_reponse_filtered[0]["postcode"])
|
||||
# B28 8JF
|
||||
print(example_2_reponse_filtered[0]["address1"])
|
||||
# 139 School Road
|
||||
print(example_2_reponse_filtered[0]["built-form"])
|
||||
# Semi-Detached
|
||||
print(example_2_reponse_filtered[0]["current-energy-rating"])
|
||||
# E
|
||||
|
||||
# ~~~~~~~~~~~~~~~~~~~~
|
||||
# Third example
|
||||
# ~~~~~~~~~~~~~~~~~~~~
|
||||
example_3_reponse = epc_client.domestic.search(
|
||||
params={
|
||||
"local-authority": "E08000025",
|
||||
"property-type": "house",
|
||||
"energy-band": "f"
|
||||
},
|
||||
size=1000
|
||||
)
|
||||
example_3_reponse = example_3_reponse["rows"]
|
||||
# Get a social housing property]
|
||||
example_3_reponse_filtered = [
|
||||
x for x in example_3_reponse if x["tenure"] == "Rented (social)"
|
||||
]
|
||||
|
||||
print(example_3_reponse_filtered[4]["walls-description"])
|
||||
print(example_3_reponse_filtered[4]["floor-description"])
|
||||
print(example_3_reponse_filtered[4]["roof-description"])
|
||||
print(example_3_reponse_filtered[4]["postcode"])
|
||||
# B32 1SL
|
||||
print(example_3_reponse_filtered[4]["address1"])
|
||||
# 77 Simmons Drive
|
||||
print(example_3_reponse_filtered[4]["built-form"])
|
||||
# Semi-Detached
|
||||
|
||||
# ~~~~~~~~~~~~~~~~~~~~
|
||||
# Final example
|
||||
# ~~~~~~~~~~~~~~~~~~~~
|
||||
# Let's take a flat that is a D
|
||||
example_4_reponse = epc_client.domestic.search(
|
||||
params={
|
||||
"local-authority": "E08000025",
|
||||
"property-type": "flat",
|
||||
"energy-band": "d"
|
||||
},
|
||||
size=1000
|
||||
)
|
||||
example_4_reponse = example_4_reponse["rows"]
|
||||
|
||||
example_4_reponse_filtered = [
|
||||
x for x in example_4_reponse if
|
||||
"cavity wall, as built, no insulation (assumed)" in x["walls-description"].lower()
|
||||
]
|
||||
# Get a social housing property
|
||||
example_4_reponse_filtered = [
|
||||
x for x in example_4_reponse_filtered if x["tenure"] == "Rented (social)"
|
||||
]
|
||||
print(example_4_reponse_filtered[0]["postcode"])
|
||||
# B32 1LS
|
||||
print(example_4_reponse_filtered[0]["address1"])
|
||||
# Flat 2
|
||||
|
||||
print(example_4_reponse_filtered[0]["floor-description"])
|
||||
print(example_4_reponse_filtered[0]["property-type"])
|
||||
# Flat
|
||||
|
||||
test_file = pd.DataFrame(
|
||||
[
|
||||
# New properties
|
||||
{"address": "113 Tenby Road", "postcode": "B13 9LT", "Notes": None},
|
||||
{"address": "139 School Road", "postcode": "B28 8JF", "Notes": None},
|
||||
{"address": "77 Simmons Drive", "postcode": "B32 1SL", "Notes": None},
|
||||
{"address": "Flat 2, 54 Wedgewood Road", "postcode": "B32 1LS", "Notes": None},
|
||||
]
|
||||
)
|
||||
|
||||
# Store the data in s3
|
||||
filename = f"{USER_ID}/{PORTFOLIO_ID}/test_inputs.csv"
|
||||
save_csv_to_s3(
|
||||
dataframe=final_csv_data,
|
||||
dataframe=test_file,
|
||||
bucket_name="retrofit-plan-inputs-dev",
|
||||
file_name=filename
|
||||
)
|
||||
|
|
@ -73,7 +173,7 @@ def app():
|
|||
"portfolio_id": str(PORTFOLIO_ID),
|
||||
"housing_type": "Social",
|
||||
"goal": "Increase EPC",
|
||||
"goal_value": "B",
|
||||
"goal_value": "C",
|
||||
"trigger_file_path": filename
|
||||
}
|
||||
print(body)
|
||||
|
|
|
|||
|
|
@ -315,7 +315,9 @@ class Costs:
|
|||
|
||||
subtotal_before_profit = labour_costs + materials_costs
|
||||
|
||||
contingency_cost = subtotal_before_profit * self.CONTINGENCY
|
||||
# Because of the possiblity of damage to the existing floor, or difficulties associated to moving fittings,
|
||||
# we use a higher contingency rate
|
||||
contingency_cost = subtotal_before_profit * self.HIGH_RISK_CONTINGENCY
|
||||
preliminaries_cost = subtotal_before_profit * self.PRELIMINARIES
|
||||
profit_cost = subtotal_before_profit * self.PROFIT_MARGIN
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ 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_floor_u_value
|
||||
)
|
||||
from recommendations.rdsap_tables import FLOOR_LEVEL_MAP
|
||||
from recommendations.Costs import Costs
|
||||
|
||||
|
||||
|
|
@ -73,10 +72,6 @@ class FloorRecommendations(Definitions):
|
|||
def recommend(self):
|
||||
u_value = self.property.floor["thermal_transmittance"]
|
||||
|
||||
floor_level = (
|
||||
FLOOR_LEVEL_MAP[self.property.data["floor-level"]] if
|
||||
self.property.data["floor-level"] not in self.DATA_ANOMALY_MATCHES else None
|
||||
)
|
||||
property_type = self.property.data["property-type"]
|
||||
|
||||
floor_area = self.property.insulation_floor_area
|
||||
|
|
@ -90,7 +85,9 @@ class FloorRecommendations(Definitions):
|
|||
return
|
||||
|
||||
# If the property is a flat that isn't at ground level, it's likely impractical to recommend a floor upgrade
|
||||
if (floor_level != 0) and (property_type == "Flat"):
|
||||
if (self.property.floor_level != 0) and (property_type == "Flat") and (
|
||||
self.property.floor["another_property_below"]
|
||||
):
|
||||
return
|
||||
|
||||
if u_value:
|
||||
|
|
|
|||
|
|
@ -240,7 +240,7 @@ class TestCosts:
|
|||
)
|
||||
|
||||
assert sus_floor_results == {
|
||||
'total': 3114.6027360000003, 'subtotal': 2595.50228, 'vat': 519.100456, 'contingency': 185.39302,
|
||||
'total': 3337.07436, 'subtotal': 2780.8953, 'vat': 556.17906, 'contingency': 370.78604,
|
||||
'preliminaries': 185.39302, 'material': 483.405, 'profit': 370.78604, 'labour_hours': 54.940000000000005,
|
||||
'labour_days': 2.289166666666667, 'labour_cost': 1370.5252
|
||||
}
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ class TestFloorRecommendations:
|
|||
assert types == {"suspended_floor_insulation"}
|
||||
|
||||
assert len(recommender.recommendations) == 6
|
||||
assert recommender.recommendations[0]["total"] == 4596.858
|
||||
assert recommender.recommendations[0]["total"] == 4925.205
|
||||
assert recommender.recommendations[0]["new_u_value"] == 0.21
|
||||
|
||||
def test_uvalue_0_12(self, input_properties):
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue