Merge pull request #261 from Hestia-Homes/birmingham-pilot

Birmingham pilot
This commit is contained in:
KhalimCK 2023-12-06 10:23:57 +00:00 committed by GitHub
commit 9812bf0fb9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 414 additions and 56 deletions

2
.idea/Model.iml generated
View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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