diff --git a/.idea/Model.iml b/.idea/Model.iml
index 80d3522c..ac61a988 100644
--- a/.idea/Model.iml
+++ b/.idea/Model.iml
@@ -6,7 +6,7 @@
-
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
index ca0e1cd9..242c02bb 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -1,6 +1,6 @@
-
+
diff --git a/backend/app/plan/router.py b/backend/app/plan/router.py
index 777e2df4..dbc01935 100644
--- a/backend/app/plan/router.py
+++ b/backend/app/plan/router.py
@@ -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,20 @@ async def trigger_plan(body: PlanTriggerRequest):
)
p.set_is_in_conservation_area(in_conservation_area)
+ logger.info("Getting components and properties recommendations")
+
+ for p in 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]
+ print("hey")
+ print(total_floor_area_group_decile)
+
for p in input_properties:
p.get_components(cleaned)
+ # floor_recommendations = FloorRecommendations(property_instance=p, uvalue_estimates=uvalue_estimates_floors)
return {"message": "Plan complete"}
diff --git a/model_data/app.py b/model_data/app.py
index de66428b..fd97ddb8 100644
--- a/model_data/app.py
+++ b/model_data/app.py
@@ -365,3 +365,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
diff --git a/model_data/tests/test_utils.py b/model_data/tests/test_utils.py
index 26a9bd7d..224471b8 100644
--- a/model_data/tests/test_utils.py
+++ b/model_data/tests/test_utils.py
@@ -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")
diff --git a/model_data/recommendations/FloorRecommendations.py b/recommendations/FloorRecommendations.py
similarity index 96%
rename from model_data/recommendations/FloorRecommendations.py
rename to recommendations/FloorRecommendations.py
index 1d15ad00..acbc0ddf 100644
--- a/model_data/recommendations/FloorRecommendations.py
+++ b/recommendations/FloorRecommendations.py
@@ -1,9 +1,8 @@
import math
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
)
@@ -92,7 +91,7 @@ class FloorRecommendations(BaseUtility):
"4th": 4
}
- def __init__(self, property_instance: Property, uvalue_estimates: UvalueEstimations):
+ def __init__(self, property_instance: Property, uvalue_estimates):
self.property = property_instance
self.uvalue_estimates = uvalue_estimates
# For audit purposes, when estimating u values we'll store it
@@ -253,7 +252,7 @@ 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):
+ def _get_floors_uvalue_estimate(self, total_floor_area_group_decile):
"""
Wrapper function which contains the methodology to extract a property's walls u-value estimate
@@ -261,11 +260,11 @@ class FloorRecommendations(BaseUtility):
: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]
+ # total_floor_area_group_decile = UvalueEstimations.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"]) &
diff --git a/model_data/recommendations/WallRecommendations.py b/recommendations/WallRecommendations.py
similarity index 99%
rename from model_data/recommendations/WallRecommendations.py
rename to recommendations/WallRecommendations.py
index aaeb777a..be618433 100644
--- a/model_data/recommendations/WallRecommendations.py
+++ b/recommendations/WallRecommendations.py
@@ -2,9 +2,8 @@ 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
)
@@ -217,7 +216,7 @@ class WallRecommendations(BaseUtility):
"solid_brick": 2,
}
- def __init__(self, property_instance: Property, uvalue_estimates: UvalueEstimations):
+ def __init__(self, property_instance: Property, uvalue_estimates):
self.property = property_instance
self.uvalue_estimates = uvalue_estimates
# For audit purposes, when estimating u values we'll store it
diff --git a/model_data/recommendations/recommendation_utils.py b/recommendations/recommendation_utils.py
similarity index 100%
rename from model_data/recommendations/recommendation_utils.py
rename to recommendations/recommendation_utils.py
diff --git a/model_data/tests/test_floor_recommendations.py b/recommendations/tests/test_floor_recommendations.py
similarity index 100%
rename from model_data/tests/test_floor_recommendations.py
rename to recommendations/tests/test_floor_recommendations.py
diff --git a/model_data/tests/test_wall_recommendations.py b/recommendations/tests/test_wall_recommendations.py
similarity index 100%
rename from model_data/tests/test_wall_recommendations.py
rename to recommendations/tests/test_wall_recommendations.py
diff --git a/utils/tests/test_logger.py b/utils/tests/test_logger.py
new file mode 100644
index 00000000..c7e64dc5
--- /dev/null
+++ b/utils/tests/test_logger.py
@@ -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)
diff --git a/utils/tests/test_uvalue_estimates.py b/utils/tests/test_uvalue_estimates.py
new file mode 100644
index 00000000..1b29994c
--- /dev/null
+++ b/utils/tests/test_uvalue_estimates.py
@@ -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']
diff --git a/utils/uvalue_estimates.py b/utils/uvalue_estimates.py
new file mode 100644
index 00000000..37dd2b38
--- /dev/null
+++ b/utils/uvalue_estimates.py
@@ -0,0 +1,27 @@
+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]:
+ """
+ This is a complementary function to UvalueEstimations.classify_decile_newvalues, which does not use pandas
+ so that we can use this function inside of our lamdbas, without having to import pandas.
+ :param decile_boundaries:
+ :param decile_labels:
+ :param new_values:
+ :return:
+ """
+
+ classifications = []
+ for value in new_values:
+ if value < decile_boundaries[0] or value > decile_boundaries[-1]:
+ classifications.append(None)
+ else:
+ i = bisect_left(decile_boundaries, value)
+ if i:
+ i -= 1
+ classifications.append(decile_labels[i])
+ return classifications