Repository: KIT-IAI/pyWATTS

Issue:
Sliding window RMSE
**What is your feature request related to?**
* Module request

**Please describe the module/problem**

Sometimes one is not interested in the overall accuracy or in the overall accuracy until now. Instead one is interested in the accuracy of the past x days or weeks. 

**Describe the solution you'd like**
Extend the RMSE module or write a new one, which enable to calculate the RMSE on a sliding window.

**Describe alternatives you've considered**
Return the prediction and calculate the windowed rmse after the execution of the pipeline

**What type of Feature Request**
* Module request

Discussion:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a9a2940..c4e4caa 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,7 @@
 
 ### Added
 * Logger for modules is defined in Base  ([#77](https://github.com/KIT-IAI/pyWATTS/issues/77))
+* Add parameters to RMSECalculator so that it can caluclate a sliding rmse too. ([#23](https://github.com/KIT-IAI/pyWATTS/issues/28))
 
 ### Changed
 
diff --git a/pywatts/modules/root_mean_squared_error.py b/pywatts/modules/root_mean_squared_error.py
index 12105d2..5aa5f9c 100644
--- a/pywatts/modules/root_mean_squared_error.py
+++ b/pywatts/modules/root_mean_squared_error.py
@@ -1,5 +1,5 @@
 import logging
-from typing import Dict
+from typing import Dict, Callable, Optional
 
 import numpy as np
 import xarray as xr
@@ -7,6 +7,7 @@ from pywatts.core.exceptions.input_not_available import InputNotAvailable
 
 from pywatts.core.base import BaseTransformer
 from pywatts.utils._xarray_time_series_utils import _get_time_indeces
+import pandas as pd
 
 logger = logging.getLogger(__name__)
 
@@ -16,12 +17,21 @@ class RmseCalculator(BaseTransformer):
     Module to calculate the Root Mean Squared Error (RMSE)
 
     :param offset: Offset, which determines the number of ignored values in the beginning for calculating the RMSE.
+                   Default 0
     :type offset: int
+    :param rolling: Flag that determines if a rolling rmse should be used. Default False
+    :type rolling: bool
+    :param window: Determine the window size if a rolling rmse should be calculated. Ignored if rolling is set to
+                   False. Default 24
+    :type window: int
+
     """
 
-    def __init__(self, name: str = "RmseCalculator", offset=0):
+    def __init__(self, name: str = "RmseCalculator", offset: int = 0, rolling: bool = False, window: int = 24):
         super().__init__(name)
         self.offset = offset
+        self.rolling = rolling
+        self.window = window
 
     def get_params(self) -> Dict[str, object]:
         """
@@ -30,7 +40,9 @@ class RmseCalculator(BaseTransformer):
         :return: Parameters set for the RMSE calculator
         :rtype: Dict[str, object]
         """
-        return {"offset": self.offset}
+        return {"offset": self.offset,
+                "rolling": self.rolling,
+                "window": self.window}
 
     def transform(self, y: xr.DataArray, **kwargs: xr.DataArray) -> xr.DataArray:
         """
@@ -56,18 +68,34 @@ class RmseCalculator(BaseTransformer):
         for key, y_hat in kwargs.items():
             p = y_hat.values
             predictions.append(key)
-            rmse.append(np.sqrt(np.mean((p[self.offset:] - t[self.offset:]) ** 2)))
-
-        time = y.indexes[_get_time_indeces(y)[0]][-1]
-        return xr.DataArray(np.array([rmse]), coords={"time": [time], "predictions": predictions},
+            if self.rolling:
+                time = y[_get_time_indeces(y)[0]][self.offset:]
+                p_, t_ = p.reshape((len(p), -1)), t.reshape((len(t), -1))
+                _rmse = pd.DataFrame((p_[self.offset:] - t_[self.offset:]) ** 2).rolling(
+                    self.window).apply(lambda x: np.sqrt(np.mean(x))).values
+            else:
+                time = [y.indexes[_get_time_indeces(y)[0]][-1]]
+                _rmse = [np.sqrt(np.mean((p[self.offset:] - t[self.offset:]) ** 2))]
+            rmse.append(_rmse)
+        return xr.DataArray(np.stack(rmse).swapaxes(0, 1).reshape((-1, len(predictions))),
+                            coords={"time": time, "predictions": predictions},
                             dims=["time", "predictions"])
 
-    def set_params(self, offset=None):
+    def set_params(self, offset: Optional[int] = None, rolling: Optional[bool] = None, window: Optional[int] = None):
         """
         Set parameters of the RMSECalculator.
 
         :param offset: Offset, which determines the number of ignored values in the beginning for calculating the RMSE.
         :type offset: int
+        :param rolling: Flag that determines if a rolling rmse should be used.
+        :type rolling: bool
+        :param window: Determine the window size if a rolling rmse should be calculated. Ignored if rolling is set to
+                       False.
+        :type window: int
         """
         if offset:
             self.offset = offset
+        if rolling:
+            self.rolling = rolling
+        if window:
+            self.window = window


Test patch:
diff --git a/tests/unit/modules/test_root_mean_squared_error.py b/tests/unit/modules/test_root_mean_squared_error.py
index 1c36d0e..cfa2021 100644
--- a/tests/unit/modules/test_root_mean_squared_error.py
+++ b/tests/unit/modules/test_root_mean_squared_error.py
@@ -19,13 +19,35 @@ class TestRMSECalculator(unittest.TestCase):
 
     def test_get_params(self):
         self.assertEqual(self.rmse_calculator.get_params(),
-                         {"offset" : 0})
-
+                         {'offset': 0, 'rolling': False, 'window': 24})
 
     def test_set_params(self):
-        self.rmse_calculator.set_params(offset=24)
+        self.rmse_calculator.set_params(offset=24, rolling=True, window=2)
         self.assertEqual(self.rmse_calculator.get_params(),
-                     {"offset": 24})
+                         {'offset': 24, 'rolling': True, 'window': 2})
+
+    def test_transform_rolling(self):
+        self.rmse_calculator.set_params(rolling=True, window=2)
+        time = pd.to_datetime(['2015-06-03 00:00:00', '2015-06-03 01:00:00',
+                               '2015-06-03 02:00:00', '2015-06-03 03:00:00',
+                               '2015-06-03 04:00:00'])
+
+        test_data = xr.Dataset({"testCol": ("time", xr.DataArray([-2, -1, 0, 1, 2])),
+                                "predictCol1": ("time", xr.DataArray([2, -3, 3, 1, -2])),
+                                "predictCol2": ("time", xr.DataArray([4, 4, 3, -2, 1])), "time": time})
+
+        test_result = self.rmse_calculator.transform(y=test_data['testCol'], gt=test_data['testCol'],
+                                                     pred1=test_data['predictCol1'],
+                                                     pred2=test_data['predictCol2'])
+        expected_result = xr.DataArray(np.array([[np.nan, np.nan, np.nan],
+                                                 [0.0, np.sqrt(10), np.sqrt(30.5)],
+                                                 [0.0, np.sqrt(6.5), np.sqrt(17)],
+                                                 [0.0, np.sqrt(4.5), 3],
+                                                 [0.0, np.sqrt(8), np.sqrt(5)], ]),
+                                       coords={"time": time, "predictions": ["gt", "pred1", "pred2"]},
+                                       dims=["time", "predictions"])
+
+        xr.testing.assert_allclose(test_result, expected_result)
 
     def test_transform(self):
         self.rmse_calculator.set_params()