diff --git a/.idea/Model.iml b/.idea/Model.iml index ac61a988..10f9791d 100644 --- a/.idea/Model.iml +++ b/.idea/Model.iml @@ -5,6 +5,7 @@ + diff --git a/backend/app/plan/router.py b/backend/app/plan/router.py index 4a68e7a7..73993e78 100644 --- a/backend/app/plan/router.py +++ b/backend/app/plan/router.py @@ -130,18 +130,21 @@ async def trigger_plan(body: PlanTriggerRequest): new_values=[float(p.data["total-floor-area"])], )[0] - # This + # 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"]) & - (x['walls-env-eff'] == p.data["walls-env-eff"]) & - (x['total-floor-area_group'] == total_floor_area_group_decile) + (x['walls-env-eff'] == p.data["walls-env-eff"]) ] - wall_recomendations = WallRecommendations(property_instance=p, uvalue_estimates=walls_u_value_estimate) + 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() wall_recomendations = wall_recomendations.recommendations # insert property id diff --git a/model_data/pytest.ini b/model_data/pytest.ini deleted file mode 100644 index 6ab74d5e..00000000 --- a/model_data/pytest.ini +++ /dev/null @@ -1,3 +0,0 @@ -[pytest] -addopts = --cov-report term-missing --cov=model_data -testpaths = model_data/tests diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 00000000..1019b4a6 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +addopts = --cov-report term-missing --cov=model_data --cov=recommendations +testpaths = model_data/tests recommendations/tests diff --git a/recommendations/WallRecommendations.py b/recommendations/WallRecommendations.py index 572bdf92..2e14aef5 100644 --- a/recommendations/WallRecommendations.py +++ b/recommendations/WallRecommendations.py @@ -1,6 +1,5 @@ import itertools import math -from statistics import mean from model_data.Property import Property from model_data.BaseUtility import BaseUtility @@ -217,9 +216,10 @@ class WallRecommendations(BaseUtility): "solid_brick": 2, } - def __init__(self, property_instance: Property, uvalue_estimates): + 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 @@ -400,47 +400,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: - """ - - if not self.uvalue_estimates: - 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_filer = [ - x for x in self.uvalue_estimates if - x["number-habitable-rooms"] == self.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 self.uvalue_estimates 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"] == self.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"]] - ) - @staticmethod def rvalue_per_mm(total_r_value: float, thickness_mm: float) -> float: """Return R-value per mm. diff --git a/recommendations/__init__.py b/recommendations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/recommendations/recommendation_utils.py b/recommendations/recommendation_utils.py index 1e6f307a..bb6ed9b9 100644 --- a/recommendations/recommendation_utils.py +++ b/recommendations/recommendation_utils.py @@ -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): + """ + 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"] == property.data["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"]] + ) diff --git a/recommendations/tests/test_recommendation_utils.py b/recommendations/tests/test_recommendation_utils.py new file mode 100644 index 00000000..6e07f1db --- /dev/null +++ b/recommendations/tests/test_recommendation_utils.py @@ -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) == 1.5 + + with pytest.raises(ValueError): + recommendation_utils.get_uvalue_estimate([], property_mock) + + # 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)