From 3b7fd55a91b213d396306fd16f1472653114a8ac Mon Sep 17 00:00:00 2001 From: Bart Pleiter Date: Mon, 30 Oct 2023 14:59:40 +0100 Subject: [PATCH 1/5] Added fix and unit test for detecting flatliners on prediction data with NaN load values in the future. Signed-off-by: Bart Pleiter --- openstef/validation/validation.py | 4 +++- ...alidation_detect_ongoing_zero_flatliner.py | 24 +++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/openstef/validation/validation.py b/openstef/validation/validation.py index b0c2c4fb6..423073554 100644 --- a/openstef/validation/validation.py +++ b/openstef/validation/validation.py @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: 2017-2023 Contributors to the OpenSTEF project # noqa E501> # # SPDX-License-Identifier: MPL-2.0 -from datetime import timedelta +from datetime import datetime, timedelta from typing import Union import math @@ -222,6 +222,8 @@ def detect_ongoing_zero_flatliner( bool: Indicating wether or not there is a zero flatliner ongoing for the given load. """ + # remove all timestamps in the future + load = load[load.index <= datetime.utcnow()] latest_measurement_time = load.index.max() latest_measurements = load[ latest_measurement_time - timedelta(minutes=duration_threshold_minutes) : diff --git a/test/unit/validation/test_validation_detect_ongoing_zero_flatliner.py b/test/unit/validation/test_validation_detect_ongoing_zero_flatliner.py index b4c44a7e4..fca633a17 100644 --- a/test/unit/validation/test_validation_detect_ongoing_zero_flatliner.py +++ b/test/unit/validation/test_validation_detect_ongoing_zero_flatliner.py @@ -14,6 +14,10 @@ start=now - timedelta(minutes=180), end=now, freq="0.25H" ) +four_hour_range_predict_setting = pd.date_range( + start=now - timedelta(minutes=180), end=now + timedelta(minutes=60), freq="0.25H" +) + class TestDetectOngoingZeroFlatliners(BaseTestCase): def test_all_zero(self): @@ -105,3 +109,23 @@ def test_all_missing_values(self): # Assert assert zero_flatliner_ongoing == False + + def test_zero_flatliner_predict_future(self): + # Scenario: A forecast is made on a zero flatliner, which contains timestamps in the + # future with NaN values that need to be predicted. + # In this case: Time in future > duration_threshold. + + # Arrange + load = pd.Series( + index=four_hour_range_predict_setting, + data=[1, 2, 3] + + [0, 0, 0, 0, 0, 0, 0, 0, 0] + + [np.nan, np.nan, np.nan, np.nan, np.nan], + ) + duration_threshold = 60 + + # Act + zero_flatliner_ongoing = detect_ongoing_zero_flatliner(load, duration_threshold) + + # Assert + assert zero_flatliner_ongoing == True From 5343489a145cf32b6ac9190730626bc5fdbdd3e7 Mon Sep 17 00:00:00 2001 From: Bart Pleiter Date: Mon, 30 Oct 2023 16:08:46 +0100 Subject: [PATCH 2/5] Added freeze_time for flatliner unit tests. Improved datetime comparison. Signed-off-by: Bart Pleiter --- openstef/validation/validation.py | 2 +- test-requirements.txt | 1 + ...alidation_detect_ongoing_zero_flatliner.py | 38 ++++++++++--------- 3 files changed, 22 insertions(+), 19 deletions(-) diff --git a/openstef/validation/validation.py b/openstef/validation/validation.py index 423073554..4207e4f69 100644 --- a/openstef/validation/validation.py +++ b/openstef/validation/validation.py @@ -223,7 +223,7 @@ def detect_ongoing_zero_flatliner( """ # remove all timestamps in the future - load = load[load.index <= datetime.utcnow()] + load = load[load.index.tz_localize(None) <= datetime.utcnow()] latest_measurement_time = load.index.max() latest_measurements = load[ latest_measurement_time - timedelta(minutes=duration_threshold_minutes) : diff --git a/test-requirements.txt b/test-requirements.txt index af4dcebb4..fc5a42e79 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -9,6 +9,7 @@ autoflake==1.7.5 bandit==1.7.4 black==23.9.1 docformatter==1.5.0 +freezegun==1.2.2 isort==5.10.1 pydocstyle==6.1.1 pylint==2.15.4 diff --git a/test/unit/validation/test_validation_detect_ongoing_zero_flatliner.py b/test/unit/validation/test_validation_detect_ongoing_zero_flatliner.py index fca633a17..ebf3b3942 100644 --- a/test/unit/validation/test_validation_detect_ongoing_zero_flatliner.py +++ b/test/unit/validation/test_validation_detect_ongoing_zero_flatliner.py @@ -3,26 +3,28 @@ # SPDX-License-Identifier: MPL-2.0 from datetime import datetime, timedelta +from freezegun import freeze_time from test.unit.utils.base import BaseTestCase import numpy as np import pandas as pd from openstef.validation.validation import detect_ongoing_zero_flatliner -now = datetime.utcnow() -three_hour_range = pd.date_range( - start=now - timedelta(minutes=180), end=now, freq="0.25H" -) - -four_hour_range_predict_setting = pd.date_range( - start=now - timedelta(minutes=180), end=now + timedelta(minutes=60), freq="0.25H" -) - - +@freeze_time("2023-10-30 12:01:02") class TestDetectOngoingZeroFlatliners(BaseTestCase): + def setUp(self) -> None: + super().setUp() + now = datetime.utcnow() + self.three_hour_range = pd.date_range( + start=now - timedelta(minutes=180), end=now, freq="0.25H" + ) + self.four_hour_range_predict_setting = pd.date_range( + start=now - timedelta(minutes=180), end=now + timedelta(minutes=60), freq="0.25H" + ) + def test_all_zero(self): # Arrange - load = pd.Series(index=three_hour_range, data=[0 for i in range(13)]) + load = pd.Series(index=self.three_hour_range, data=[0 for i in range(13)]) duration_threshold = 120 # Act @@ -33,7 +35,7 @@ def test_all_zero(self): def test_all_nonzero(self): # Arrange - load = pd.Series(index=three_hour_range, data=[i for i in range(1, 14)]) + load = pd.Series(index=self.three_hour_range, data=[i for i in range(1, 14)]) duration_threshold = 120 # Act @@ -47,7 +49,7 @@ def test_only_last_nonzero(self): # now the pattern has ended since the last measurement is not zero anymore. # Arrange - load = pd.Series(index=three_hour_range, data=[0 for i in range(1, 13)] + [1]) + load = pd.Series(index=self.three_hour_range, data=[0 for i in range(1, 13)] + [1]) duration_threshold = 120 # Act @@ -62,7 +64,7 @@ def test_zero_flatliner_pattern_below_threshold(self): # Arrange load = pd.Series( - index=three_hour_range, data=[i for i in range(1, 10)] + [0, 0, 0, 0] + index=self.three_hour_range, data=[i for i in range(1, 10)] + [0, 0, 0, 0] ) duration_threshold = 120 @@ -75,7 +77,7 @@ def test_zero_flatliner_pattern_below_threshold(self): def test_zero_flatliner_pattern_just_above_threshold(self): # Arrange load = pd.Series( - index=three_hour_range, data=[1, 2, 3, 4] + [0 for i in range(9)] + index=self.three_hour_range, data=[1, 2, 3, 4] + [0 for i in range(9)] ) duration_threshold = 120 @@ -88,7 +90,7 @@ def test_zero_flatliner_pattern_just_above_threshold(self): def test_zero_flatliner_and_missing_values(self): # Arrange load = pd.Series( - index=three_hour_range, + index=self.three_hour_range, data=[1, 2, 3, 4] + [0, 0, 0, 0, np.nan, np.nan, np.nan, np.nan, 0], ) duration_threshold = 120 @@ -101,7 +103,7 @@ def test_zero_flatliner_and_missing_values(self): def test_all_missing_values(self): # Arrange - load = pd.Series(index=three_hour_range, data=[np.nan for i in range(13)]) + load = pd.Series(index=self.three_hour_range, data=[np.nan for i in range(13)]) duration_threshold = 120 # Act @@ -117,7 +119,7 @@ def test_zero_flatliner_predict_future(self): # Arrange load = pd.Series( - index=four_hour_range_predict_setting, + index=self.four_hour_range_predict_setting, data=[1, 2, 3] + [0, 0, 0, 0, 0, 0, 0, 0, 0] + [np.nan, np.nan, np.nan, np.nan, np.nan], From 7d0cca3dca1b39ddb096ec7a6b4a3c4f7bd46669 Mon Sep 17 00:00:00 2001 From: black Date: Mon, 30 Oct 2023 15:09:25 +0000 Subject: [PATCH 3/5] Format Python code with Black Signed-off-by: black --- .../test_validation_detect_ongoing_zero_flatliner.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/test/unit/validation/test_validation_detect_ongoing_zero_flatliner.py b/test/unit/validation/test_validation_detect_ongoing_zero_flatliner.py index ebf3b3942..42f9a5dae 100644 --- a/test/unit/validation/test_validation_detect_ongoing_zero_flatliner.py +++ b/test/unit/validation/test_validation_detect_ongoing_zero_flatliner.py @@ -10,6 +10,7 @@ from openstef.validation.validation import detect_ongoing_zero_flatliner + @freeze_time("2023-10-30 12:01:02") class TestDetectOngoingZeroFlatliners(BaseTestCase): def setUp(self) -> None: @@ -19,9 +20,11 @@ def setUp(self) -> None: start=now - timedelta(minutes=180), end=now, freq="0.25H" ) self.four_hour_range_predict_setting = pd.date_range( - start=now - timedelta(minutes=180), end=now + timedelta(minutes=60), freq="0.25H" + start=now - timedelta(minutes=180), + end=now + timedelta(minutes=60), + freq="0.25H", ) - + def test_all_zero(self): # Arrange load = pd.Series(index=self.three_hour_range, data=[0 for i in range(13)]) @@ -49,7 +52,9 @@ def test_only_last_nonzero(self): # now the pattern has ended since the last measurement is not zero anymore. # Arrange - load = pd.Series(index=self.three_hour_range, data=[0 for i in range(1, 13)] + [1]) + load = pd.Series( + index=self.three_hour_range, data=[0 for i in range(1, 13)] + [1] + ) duration_threshold = 120 # Act From 7d849cf2fac2112e48e3210b36fb8c1f039f98e9 Mon Sep 17 00:00:00 2001 From: Bart Pleiter Date: Mon, 30 Oct 2023 16:23:10 +0100 Subject: [PATCH 4/5] Moved freezegun dependency to requirements.txt Signed-off-by: Bart Pleiter --- requirements.txt | 1 + test-requirements.txt | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 4c12b92e6..a8ae8c6c5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ # SPDX-FileCopyrightText: 2017-2023 Contributors to the OpenSTEF project # noqa E501> # # SPDX-License-Identifier: MPL-2.0 +freezegun~=1.2.2 holidays==0.21 lightgbm~=3.3 matplotlib~=3.7 diff --git a/test-requirements.txt b/test-requirements.txt index fc5a42e79..af4dcebb4 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -9,7 +9,6 @@ autoflake==1.7.5 bandit==1.7.4 black==23.9.1 docformatter==1.5.0 -freezegun==1.2.2 isort==5.10.1 pydocstyle==6.1.1 pylint==2.15.4 From 6860434d1050cc8b127fda0d41d17a885b2c02ba Mon Sep 17 00:00:00 2001 From: Bart Pleiter Date: Mon, 30 Oct 2023 16:48:51 +0100 Subject: [PATCH 5/5] Moved freezegun dependency back to test-requirements.txt, added test-requirements to github unittest action. Signed-off-by: Bart Pleiter --- .github/workflows/python-build.yaml | 2 +- requirements.txt | 1 - test-requirements.txt | 1 + 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/python-build.yaml b/.github/workflows/python-build.yaml index 805c73194..30642441f 100644 --- a/.github/workflows/python-build.yaml +++ b/.github/workflows/python-build.yaml @@ -56,7 +56,7 @@ jobs: # Test - name: Unit test with pytest run: | - pip install pytest pytest-cov + pip install -r test-requirements.txt pytest --cov-report=xml --cov=openstef/ test/ --junitxml=pytest-report.xml # Fix relative paths in coverage file # Known bug: https://community.sonarsource.com/t/sonar-on-github-actions-with-python-coverage-source-issue/36057 diff --git a/requirements.txt b/requirements.txt index a8ae8c6c5..4c12b92e6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,6 @@ # SPDX-FileCopyrightText: 2017-2023 Contributors to the OpenSTEF project # noqa E501> # # SPDX-License-Identifier: MPL-2.0 -freezegun~=1.2.2 holidays==0.21 lightgbm~=3.3 matplotlib~=3.7 diff --git a/test-requirements.txt b/test-requirements.txt index af4dcebb4..4a610226a 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -9,6 +9,7 @@ autoflake==1.7.5 bandit==1.7.4 black==23.9.1 docformatter==1.5.0 +freezegun~=1.2.2 isort==5.10.1 pydocstyle==6.1.1 pylint==2.15.4