Repository: KIT-IAI/pyWATTS

Issue:
Change metrics to Summaries
Change the metrics max/min error and MAE to summaries.

Their rolling functionality should be implemented like rollingRMSE

Discussion:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 942a369..707f3cf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -20,9 +20,11 @@
 
 ### Changed
 * Remove parameter step.stop. Instead we call the method _should_stop on the previous steps. ([#25](https://github.com/KIT-IAI/pyWATTS/issues/25))
+* Change metrics to summaries ([#115](https://github.com/KIT-IAI/pyWATTS/issues/115))
 
 ### Deprecated
 * Usage of RMSECalculator. Will be removed in version 0.3. Calculation of metrics should be a summary and not a module. ([#34](https://github.com/KIT-IAI/pyWATTS/issues/34))
+* Usage of MAECalculator. Will be removed in version 0.3. Calculation of metrics should be a summary and not a module. ([#115](https://github.com/KIT-IAI/pyWATTS/issues/115))
 
 ### Fixed
 * Converts the input for the keras model in the kerasWrapper from xr.Dataarray to np.array ([#97](https://github.com/KIT-IAI/pyWATTS/issues/97))
diff --git a/pywatts/modules/mean_absolute_error.py b/pywatts/modules/mean_absolute_error.py
index 7169f59..70bc3ce 100644
--- a/pywatts/modules/mean_absolute_error.py
+++ b/pywatts/modules/mean_absolute_error.py
@@ -32,6 +32,11 @@ class MaeCalculator(BaseTransformer):
         self.offset = offset
         self.rolling = rolling
         self.window = window
+        if not rolling:
+            DeprecationWarning("If you do not need a rolling RMSE, you should use the RMSESummary module. Will be removed in 0.3")
+        else:
+            DeprecationWarning("If you need a rolling RMSE  you should use the RollingRMSE module. Will be removed in 0.3")
+
 
     def get_params(self) -> Dict[str, object]:
         """
diff --git a/pywatts/modules/rolling_mae.py b/pywatts/modules/rolling_mae.py
new file mode 100644
index 0000000..b623c6e
--- /dev/null
+++ b/pywatts/modules/rolling_mae.py
@@ -0,0 +1,21 @@
+import numpy as np
+import pandas as pd
+
+from pywatts.modules.rolling_metric_base import RollingMetricBase
+
+
+class RollingMAE(RollingMetricBase):
+    """
+    Module to calculate the Rolling Mean Absolute Error (MAE)
+    :param window_size: Determine the window size for the rolling mae. Default 24
+    :type window_size: int
+    :param window_size_unit: Determine the unit of the window size. Default Day (d)"
+    :type window_size_unit: str
+
+    """
+
+    def _apply_rolling_metric(self, p, t, index):
+        return pd.DataFrame(np.mean((p - t), axis=-1),
+                                    index=index).rolling(
+            f"{self.window_size}{self.window_size_unit}").apply(
+            lambda x: np.mean(np.abs(x))).values
diff --git a/pywatts/modules/rolling_metric_base.py b/pywatts/modules/rolling_metric_base.py
new file mode 100644
index 0000000..a842a30
--- /dev/null
+++ b/pywatts/modules/rolling_metric_base.py
@@ -0,0 +1,87 @@
+from abc import ABC, abstractmethod
+from typing import Dict
+
+import numpy as np
+import xarray as xr
+
+from pywatts.core.base import BaseTransformer
+from pywatts.core.exceptions import InputNotAvailable
+from pywatts.utils._xarray_time_series_utils import _get_time_indeces
+
+
+class RollingMetricBase(BaseTransformer, ABC):
+    """
+    Module to calculate a Rolling Metric
+    :param window_size: Determine the window size for the rolling metric. Default 24
+    :type window_size: int
+    :param window_size_unit: Determine the unit of the window size. Default Day (d)"
+    :type window_size_unit: str
+
+    """
+
+    def __init__(self, name: str = None, window_size=24, window_size_unit="d"):
+        super().__init__(name if name is not None else self.__class__.__name__)
+        self.window_size_unit = window_size_unit
+        self.window_size = window_size
+
+    def get_params(self) -> Dict[str, object]:
+        """
+        Returns a dict of parameters used in the RollingMetric.
+
+        :return: Parameters set for the RollingMetric
+        :rtype: Dict[str, object]
+        """
+        return {
+            "window_size_unit": self.window_size_unit,
+            "window_size": self.window_size
+        }
+
+    def set_params(self, window_size=None, window_size_unit=None):
+        """
+        Set the parameter for the RollingMetric.
+
+        :param window_size: Determine the window size if a rolling metric should be calculated.
+                            Ignored if rolling is set to False. Default 24
+        :type window_size: int
+        :param window_size_unit: Determine the unit of the window size. Default Day (d)"
+        :type window_size_unit: str
+
+        """
+        if window_size is not None:
+            self.window_size = window_size
+        if window_size_unit is not None:
+            self.window_size_unit = window_size_unit
+
+    def transform(self, y: xr.DataArray, **kwargs: xr.DataArray) -> xr.DataArray:
+        """
+        Calculates the MAE based on the predefined target and predictions variables.
+
+        :param x: the input dataset
+        :type x: Optional[xr.DataArray]
+
+        :return: The calculated MAE
+        :rtype: xr.DataArray
+        """
+
+        if kwargs == {}:
+            error_message = f"No predictions are provided as input for the {self.__class__.__name__}. You should add the predictions" \
+                            f" by a seperate key word arguments if you add the {self.__class__.__name__} to the pipeline."
+            self.logger.error(error_message)
+            raise InputNotAvailable(error_message)
+
+        t = y.values
+        results = {}
+        for key, y_hat in kwargs.items():
+            p = y_hat.values
+            p_, t_ = p.reshape((len(p), -1)), t.reshape((len(t), -1))
+            index = y.indexes[_get_time_indeces(kwargs)[0]]
+            results[key] = self._apply_rolling_metric(p_, t_, index)
+        time = y.indexes[_get_time_indeces(y)[0]]
+
+        return xr.DataArray(np.concatenate(list(results.values()), axis=1),
+                            coords={"time": time, "predictions": list(results.keys())},
+                            dims=["time", "predictions"])
+
+    @abstractmethod
+    def _apply_rolling_metric(self, p_, t_, index):
+        pass
diff --git a/pywatts/modules/rolling_rmse.py b/pywatts/modules/rolling_rmse.py
index 5f32957..5764f25 100644
--- a/pywatts/modules/rolling_rmse.py
+++ b/pywatts/modules/rolling_rmse.py
@@ -1,86 +1,21 @@
-from typing import Dict
-
 import numpy as np
 import pandas as pd
-import xarray as xr
 
-from pywatts.core.base import BaseTransformer
-from pywatts.core.exceptions.input_not_available import InputNotAvailable
-from pywatts.utils._xarray_time_series_utils import _get_time_indeces
+from pywatts.modules.rolling_metric_base import RollingMetricBase
 
 
-class RollingRMSE(BaseTransformer):
+class RollingRMSE(RollingMetricBase):
     """
     Module to calculate the Rolling Root Mean Squared Error (RMSE)
-    :param window_size: Determine the window size if a rolling rmse should be calculated. Ignored if rolling is set to
-                   False. Default 24
+    :param window_size: Determine the window size of the rolling rmse. Default 24
     :type window_size: int
     :param window_size_unit: Determine the unit of the window size. Default Day (d)"
     :type window_size_unit: str
 
     """
 
-    def __init__(self, name: str = "RollingRMSE", window_size=24, window_size_unit="d"):
-        super().__init__(name)
-        self.window_size_unit = window_size_unit
-        self.window_size = window_size
-
-    def get_params(self) -> Dict[str, object]:
-        """
-        Returns a dict of parameters used in the RollingRMSE.
-
-        :return: Parameters set for the RollingRMSE
-        :rtype: Dict[str, object]
-        """
-        return {
-            "window_size_unit": self.window_size_unit,
-            "window_size": self.window_size
-        }
-
-    def set_params(self, window_size=None, window_size_unit=None):
-        """
-        Set the parameter for the RollingRMSE.
-
-        :param window_size: Determine the window size if a rolling rmse should be calculated.
-                            Ignored if rolling is set to False. Default 24
-        :type window_size: int
-        :param window_size_unit: Determine the unit of the window size. Default Day (d)"
-        :type window_size_unit: str
-
-        """
-        if window_size is not None:
-            self.window_size = window_size
-        if window_size_unit is not None:
-            self.window_size_unit = window_size_unit
-
-    def transform(self, y: xr.DataArray, **kwargs: xr.DataArray) -> xr.DataArray:
-        """
-        Calculates the RMSE based on the predefined target and predictions variables.
-
-        :param x: the input dataset
-        :type x: Optional[xr.DataArray]
-
-        :return: The calculated RMSE
-        :rtype: xr.DataArray
-        """
-
-        if kwargs == {}:
-            error_message = "No predictions are provided as input for the RollingRMSE. You should add the predictions" \
-                            " by a seperate key word arguments if you add the RollingRMSE to the pipeline."
-            self.logger.error(error_message)
-            raise InputNotAvailable(error_message)
-
-        t = y.values
-        results = {}
-        for key, y_hat in kwargs.items():
-            p = y_hat.values
-            p_, t_ = p.reshape((len(p), -1)), t.reshape((len(t), -1))
-            results[key] = pd.DataFrame(np.mean((p_ - t_) ** 2, axis=-1),
-                                        index=y.indexes[_get_time_indeces(kwargs)[0]]).rolling(
-                f"{self.window_size}{self.window_size_unit}").apply(
-                lambda x: np.sqrt(np.mean(x))).values
-        time = y.indexes[_get_time_indeces(y)[0]]
-
-        return xr.DataArray(np.concatenate(list(results.values()), axis=1),
-                            coords={"time": time, "predictions": list(results.keys())},
-                            dims=["time", "predictions"])
+    def _apply_rolling_metric(self, p_, t_, index):
+        return pd.DataFrame(np.mean((p_ - t_) ** 2, axis=-1),
+                            index=index).rolling(
+            f"{self.window_size}{self.window_size_unit}").apply(
+            lambda x: np.sqrt(np.mean(x))).values
diff --git a/pywatts/modules/root_mean_squared_error.py b/pywatts/modules/root_mean_squared_error.py
index 8242963..8a8488b 100644
--- a/pywatts/modules/root_mean_squared_error.py
+++ b/pywatts/modules/root_mean_squared_error.py
@@ -34,9 +34,9 @@ class RmseCalculator(BaseTransformer):
         self.window = window
         self.filter = filter
         if not rolling:
-            DeprecationWarning("If you do not need a rolling RMSE, you should use the RMSESummary module.")
+            DeprecationWarning("If you do not need a rolling RMSE, you should use the RMSESummary module. Will be removed in 0.3")
         else:
-            DeprecationWarning("If you need a rolling RMSE  you should use the RollingRMSE module.")
+            DeprecationWarning("If you need a rolling RMSE  you should use the RollingRMSE module. Will be removed in 0.3")
 
     def get_params(self) -> Dict[str, object]:
         """
diff --git a/pywatts/summaries/mae_summary.py b/pywatts/summaries/mae_summary.py
new file mode 100644
index 0000000..8878b82
--- /dev/null
+++ b/pywatts/summaries/mae_summary.py
@@ -0,0 +1,24 @@
+import logging
+
+import numpy as np
+
+from pywatts.summaries.metric_base import MetricBase
+
+logger = logging.getLogger(__name__)
+
+
+class MAE(MetricBase):
+    """
+    Module to calculate the Mean Absolute Error (MAE)
+
+    :param offset: Offset, which determines the number of ignored values in the beginning for calculating the MAE.
+                   Default 0
+    :type offset: int
+    :param filter_method: Filter which should performed on the data before calculating the MAE.
+    :type filter_method: Callable[[np.ndarray, np.ndarray], Tuple[np.ndarray, np.ndarray]]
+    """
+
+
+    def _apply_metric(self, p, t):
+        return np.mean(np.abs((p[self.offset:] - t[self.offset:])))
+
diff --git a/pywatts/summaries/metric_base.py b/pywatts/summaries/metric_base.py
new file mode 100644
index 0000000..88781f5
--- /dev/null
+++ b/pywatts/summaries/metric_base.py
@@ -0,0 +1,108 @@
+import logging
+from abc import ABC, abstractmethod
+from typing import Callable, Tuple, Dict, Optional
+
+import cloudpickle
+import numpy as np
+import xarray as xr
+
+from pywatts.core.base_summary import BaseSummary
+from pywatts.core.exceptions import InputNotAvailable
+from pywatts.core.filemanager import FileManager
+
+logger = logging.getLogger(__name__)
+
+
+class MetricBase(BaseSummary, ABC):
+    """
+    Base Class for all Metrics
+
+    :param offset: Offset, which determines the number of ignored values in the beginning for calculating the Metric.
+                   Default 0
+    :type offset: int
+    :param filter_method: Filter which should performed on the data before calculating the Metric.
+    :type filter_method: Callable[[np.ndarray, np.ndarray], Tuple[np.ndarray, np.ndarray]]
+    """
+
+    def __init__(self,
+                 name: str = None,
+                 filter_method: Callable[[np.ndarray, np.ndarray], Tuple[np.ndarray, np.ndarray]] = None,
+                 offset: int = 0):
+        super().__init__(name if name is not None else self.__class__.__name__)
+        self.offset = offset
+        self.filter_method = filter_method
+
+    def get_params(self) -> Dict[str, object]:
+        """
+        Returns a dict of parameters used in the Metric.
+
+        :return: Parameters set for the Metric
+        :rtype: Dict[str, object]
+        """
+        return {"offset": self.offset}
+
+    def set_params(self, offset: Optional[int] = None):
+        """
+        Set parameters of the Metric.
+
+        :param offset: Offset, which determines the number of ignored values in the beginning for calculating the Metric.
+        :type offset: int
+        """
+        if offset:
+            self.offset = offset
+
+    def transform(self, file_manager: FileManager, y: xr.DataArray, **kwargs: xr.DataArray) -> str:
+        """
+        Calculates the MAE based on the predefined target and predictions variables.
+        :param file_manager: The filemanager, it can be used to store data that corresponds to the summary as a file.
+        :type: file_manager: FileManager
+        :param y: the input dataset
+        :type y: xr.DataArray
+        :param kwargs: the predictions
+        :type kwargs: xr.DataArray
+
+        :return: The calculated MAE
+        :rtype: xr.DataArray
+        """
+
+        t = y.values
+        summary = ""
+        if kwargs == {}:
+            error_message = f"No predictions are provided as input for the {self.__class__.__name__}.  You should add the predictions by a " \
+                            f"seperate key word arguments if you add the {self.__class__.__name__} to the pipeline."
+            logger.error(error_message)
+            raise InputNotAvailable(error_message)
+
+        for key, y_hat in kwargs.items():
+            p = y_hat.values
+            if self.filter_method:
+                p_, t_ = self.filter_method(p, t)
+                mae = self._apply_metric(p_, t_)
+
+            else:
+                mae = self._apply_metric(p, t)
+            summary += f"  * {key}: {mae}\n"
+        return summary
+
+    def save(self, fm: FileManager) -> Dict:
+        json = super().save(fm)
+        if self.filter_method is not None:
+            filter_path = fm.get_path(f"{self.name}_filter.pickle")
+            with open(filter_path, 'wb') as outfile:
+                cloudpickle.dump(self.filter_method, outfile)
+            json["filter"] = filter_path
+        return json
+
+    @classmethod
+    def load(cls, load_information: Dict):
+        params = load_information["params"]
+        name = load_information["name"]
+        filter_method = None
+        if "filter" in load_information:
+            with open(load_information["filter"], 'rb') as pickle_file:
+                filter_method = cloudpickle.load(pickle_file)
+        return cls(name=name, filter_method=filter_method, **params)
+
+    @abstractmethod
+    def _apply_metric(self, p, t):
+        pass
diff --git a/pywatts/summaries/rmse_summary.py b/pywatts/summaries/rmse_summary.py
index c5e08ee..47f1db7 100644
--- a/pywatts/summaries/rmse_summary.py
+++ b/pywatts/summaries/rmse_summary.py
@@ -1,18 +1,13 @@
 import logging
-from typing import Dict, Callable, Optional, Tuple
 
-import cloudpickle
 import numpy as np
-import xarray as xr
 
-from pywatts.core.base_summary import BaseSummary
-from pywatts.core.exceptions.input_not_available import InputNotAvailable
-from pywatts.core.filemanager import FileManager
+from pywatts.summaries.metric_base import MetricBase
 
 logger = logging.getLogger(__name__)
 
 
-class RMSE(BaseSummary):
+class RMSE(MetricBase):
     """
     Module to calculate the Root Mean Squared Error (RMSE)
 
@@ -23,81 +18,6 @@ class RMSE(BaseSummary):
     :type filter_method: Callable[[np.ndarray, np.ndarray], Tuple[np.ndarray, np.ndarray]]
     """
 
-    def __init__(self,
-                 name: str = "RMSE",
-                 filter_method: Callable[[np.ndarray, np.ndarray], Tuple[np.ndarray, np.ndarray]] = None,
-                 offset: int = 0):
-        super().__init__(name)
-        self.offset = offset
-        self.filter_method = filter_method
+    def _apply_metric(self, p, t):
+        return np.sqrt(np.mean((p[self.offset:] - t[self.offset:]) ** 2))
 
-    def get_params(self) -> Dict[str, object]:
-        """
-        Returns a dict of parameters used in the RMSE Calculator.
-
-        :return: Parameters set for the RMSE calculator
-        :rtype: Dict[str, object]
-        """
-        return {"offset": self.offset}
-
-    def transform(self, file_manager: FileManager, y: xr.DataArray, **kwargs: xr.DataArray) -> str:
-        """
-        Calculates the RMSE based on the predefined target and predictions variables.
-        :param file_manager: The filemanager, it can be used to store data that corresponds to the summary as a file.
-        :type: file_manager: FileManager
-        :param y: the input dataset
-        :type y: xr.DataArray
-        :param kwargs: the predictions
-        :type kwargs: xr.DataArray
-
-        :return: The calculated RMSE
-        :rtype: xr.DataArray
-        """
-
-        t = y.values
-        summary = ""
-        if kwargs == {}:
-            error_message = "No predictions are provided as input for the RMSE.  You should add the predictions by a " \
-                            "seperate key word arguments if you add the RMSE to the pipeline."
-            logger.error(error_message)
-            raise InputNotAvailable(error_message)
-
-        for key, y_hat in kwargs.items():
-            p = y_hat.values
-            if self.filter_method:
-                p_, t_ = self.filter_method(p, t)
-                rmse = np.sqrt(np.mean((p_[self.offset:] - t_[self.offset:]) ** 2))
-
-            else:
-                rmse = np.sqrt(np.mean((p[self.offset:] - t[self.offset:]) ** 2))
-            summary += f"  * {key}: {rmse}\n"
-        return summary
-
-    def set_params(self, offset: 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
-        """
-        if offset:
-            self.offset = offset
-
-    def save(self, fm: FileManager) -> Dict:
-        json = super().save(fm)
-        if self.filter_method is not None:
-            filter_path = fm.get_path(f"{self.name}_filter.pickle")
-            with open(filter_path, 'wb') as outfile:
-                cloudpickle.dump(self.filter_method, outfile)
-            json["filter"] = filter_path
-        return json
-
-    @classmethod
-    def load(cls, load_information: Dict):
-        params = load_information["params"]
-        name = load_information["name"]
-        filter_method = None
-        if "filter" in load_information:
-            with open(load_information["filter"], 'rb') as pickle_file:
-                filter_method = cloudpickle.load(pickle_file)
-        return cls(name=name, filter_method=filter_method, **params)
diff --git a/pywatts/wrapper/sklearn_wrapper.py b/pywatts/wrapper/sklearn_wrapper.py
index 069dba1..5408988 100644
--- a/pywatts/wrapper/sklearn_wrapper.py
+++ b/pywatts/wrapper/sklearn_wrapper.py
@@ -62,7 +62,7 @@ class SKLearnWrapper(BaseWrapper):
         target = self._dataset_to_sklearn_input(targets)
         self.targets = list(
             zip(targets.keys(), map(lambda x: x.shape[-1] if len(x.shape) > 1 else 1, list(targets.values()))))
-        self.module.fit(x, target)
+        self.fit_result = self.module.fit(x, target)
         self.is_fitted = True
 
     @staticmethod


Test patch:
diff --git a/tests/unit/modules/test_linear_interpolation.py b/tests/unit/modules/test_linear_interpolation.py
index 364b8e5..439e3f6 100644
--- a/tests/unit/modules/test_linear_interpolation.py
+++ b/tests/unit/modules/test_linear_interpolation.py
@@ -42,13 +42,13 @@ class TestLinearInterpolater(unittest.TestCase):
         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({"test": ("time", xr.DataArray([1, 2, np.nan, 4, 5])),
-                                "test2": ("time", xr.DataArray([np.nan, 2, 3, 4, 5])),
-                                "test3": ("time", xr.DataArray([1, 2, 3, 4, np.nan])),
-                                "test4": ("time", xr.DataArray([1, np.nan, np.nan, np.nan, 5])), "time": time})
+        test_data = xr.Dataset({"test": ("time", xr.DataArray([1, 2, np.nan, 4, 5]).data),
+                                "test2": ("time", xr.DataArray([np.nan, 2, 3, 4, 5]).data),
+                                "test3": ("time", xr.DataArray([1, 2, 3, 4, np.nan]).data),
+                                "test4": ("time", xr.DataArray([1, np.nan, np.nan, np.nan, 5]).data), "time": time})
         test_result = self.linear_interpolater.transform(test_data)
-        expected_result = xr.Dataset({"test": ("time", xr.DataArray([1., 2., 3., 4., 5.])),
-                                      "test2": ("time", xr.DataArray([1., 2., 3., 4., 5.])),
-                                      "test3": ("time", xr.DataArray([1., 2., 3., 4., 5.])),
-                                      "test4": ("time", xr.DataArray([1., 2., 3., 4., 5.])), "time": time})
+        expected_result = xr.Dataset({"test": ("time", xr.DataArray([1., 2., 3., 4., 5.]).data),
+                                      "test2": ("time", xr.DataArray([1., 2., 3., 4., 5.]).data),
+                                      "test3": ("time", xr.DataArray([1., 2., 3., 4., 5.]).data),
+                                      "test4": ("time", xr.DataArray([1., 2., 3., 4., 5.]).data), "time": time})
         xr.testing.assert_equal(test_result, expected_result)
diff --git a/tests/unit/modules/test_mean_absolute_error.py b/tests/unit/modules/test_mean_absolute_error.py
index 8f837b9..b568531 100644
--- a/tests/unit/modules/test_mean_absolute_error.py
+++ b/tests/unit/modules/test_mean_absolute_error.py
@@ -32,9 +32,9 @@ class TestMaeCalculator(unittest.TestCase):
                                '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_data = xr.Dataset({"testCol": ("time", xr.DataArray([-2, -1, 0, 1, 2]).data),
+                                "predictCol1": ("time", xr.DataArray([2, -3, 3, 1, -2]).data),
+                                "predictCol2": ("time", xr.DataArray([4, 4, 3, -2, 1]).data), "time": time})
 
         test_result = self.mae_calculator.transform(y=test_data['testCol'], gt=test_data['testCol'],
                                                     pred1=test_data['predictCol1'],
@@ -58,9 +58,9 @@ class TestMaeCalculator(unittest.TestCase):
 
         result_time = pd.to_datetime(['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_data = xr.Dataset({"testCol": ("time", xr.DataArray([-2, -1, 0, 1, 2]).data),
+                                "predictCol1": ("time", xr.DataArray([2, -3, 3, 1, -2]).data),
+                                "predictCol2": ("time", xr.DataArray([4, 4, 3, -2, 1]).data), "time": time})
 
         test_result = self.mae_calculator.transform(y=test_data['testCol'], gt=test_data['testCol'],
                                                     pred1=test_data['predictCol1'],
@@ -79,9 +79,9 @@ class TestMaeCalculator(unittest.TestCase):
                                '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_data = xr.Dataset({"testCol": ("time", xr.DataArray([-2, -1, 0, 1, 2]).data),
+                                "predictCol1": ("time", xr.DataArray([2, -3, 3, 1, -2]).data),
+                                "predictCol2": ("time", xr.DataArray([4, 4, 3, -2, 1]).data), "time": time})
 
         with pytest.raises(InputNotAvailable) as e_info:
             self.mae_calculator.transform(y=test_data['testCol'])
diff --git a/tests/unit/modules/test_missing_value_detection.py b/tests/unit/modules/test_missing_value_detection.py
index 1df1fb0..97d944e 100644
--- a/tests/unit/modules/test_missing_value_detection.py
+++ b/tests/unit/modules/test_missing_value_detection.py
@@ -26,15 +26,15 @@ class TestMissingValueDetector(unittest.TestCase):
         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({"test": ("time", xr.DataArray([1, 2, np.nan, 4, 5])),
-                                "test2": ("time", xr.DataArray([np.nan, 2, 3, 4, 5])),
-                                "test3": ("time", xr.DataArray([1, 2, 3, 4, np.nan])),
-                                "test4": ("time", xr.DataArray([1, np.nan, np.nan, np.nan, 5])), "time": time})
+        test_data = xr.Dataset({"test": ("time", xr.DataArray([1, 2, np.nan, 4, 5]).data),
+                                "test2": ("time", xr.DataArray([np.nan, 2, 3, 4, 5]).data),
+                                "test3": ("time", xr.DataArray([1, 2, 3, 4, np.nan]).data),
+                                "test4": ("time", xr.DataArray([1, np.nan, np.nan, np.nan, 5]).data), "time": time})
         test_result = self.missing_value_detector.transform(test_data)
 
-        expected_result = xr.Dataset({"test": ("time", xr.DataArray([False, False, True, False, False])),
-                                      "test2": ("time", xr.DataArray([True, False, False, False, False])),
-                                      "test3": ("time", xr.DataArray([False, False, False, False, True])),
-                                      "test4": ("time", xr.DataArray([False, True, True, True, False])), "time": time})
+        expected_result = xr.Dataset({"test": ("time", xr.DataArray([False, False, True, False, False]).data),
+                                      "test2": ("time", xr.DataArray([True, False, False, False, False]).data),
+                                      "test3": ("time", xr.DataArray([False, False, False, False, True]).data),
+                                      "test4": ("time", xr.DataArray([False, True, True, True, False]).data), "time": time})
         xr.testing.assert_equal(test_result,expected_result)
 
diff --git a/tests/unit/modules/test_rolling_mae.py b/tests/unit/modules/test_rolling_mae.py
new file mode 100644
index 0000000..32554bf
--- /dev/null
+++ b/tests/unit/modules/test_rolling_mae.py
@@ -0,0 +1,66 @@
+import unittest
+
+import numpy as np
+import pandas as pd
+import pytest
+import xarray as xr
+
+from pywatts.core.exceptions.input_not_available import InputNotAvailable
+from pywatts.modules.rolling_mae import RollingMAE
+
+
+class TestRollingMAE(unittest.TestCase):
+
+    def setUp(self) -> None:
+        self.rolling_mae = RollingMAE()
+
+    def tearDown(self) -> None:
+        self.rolling_mae = None
+
+    def test_get_params(self):
+        self.assertEqual(self.rolling_mae.get_params(), {'window_size': 24, 'window_size_unit': "d"})
+
+    def test_set_params(self):
+        self.rolling_mae.set_params(window_size=2, window_size_unit="m")
+        self.assertEqual(self.rolling_mae.get_params(),
+                         {"window_size": 2, "window_size_unit": "m"})
+
+    def test_transform(self):
+        self.rolling_mae.set_params(window_size=2, window_size_unit="h")
+        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]).data),
+                                "predictCol1": ("time", xr.DataArray([2, -3, 3, 1, -2]).data),
+                                "predictCol2": ("time", xr.DataArray([4, 4, 3, -2, 1]).data), "time": time})
+
+        test_result = self.rolling_mae.transform(y=test_data['testCol'], gt=test_data['testCol'],
+                                                 pred1=test_data['predictCol1'],
+                                                 pred2=test_data['predictCol2'])
+        expected_result = xr.DataArray(np.array([[0.0, 4, 6],
+                                                 [0.0, 3, 5.5],
+                                                 [0.0, 2.5, 4],
+                                                 [0.0, 1.5, 3],
+                                                 [0.0, 2, 2]]),
+                                       coords={"time": time, "predictions": ["gt", "pred1", "pred2"]},
+                                       dims=["time", "predictions"])
+
+
+        xr.testing.assert_allclose(test_result, expected_result)
+
+    def test_transform_without_predictions(self):
+        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]).data),
+                                "predictCol1": ("time", xr.DataArray([2, -3, 3, 1, -2]).data),
+                                "predictCol2": ("time", xr.DataArray([4, 4, 3, -2, 1]).data), "time": time})
+
+        with pytest.raises(InputNotAvailable) as e_info:
+            self.rolling_mae.transform(y=test_data['testCol'])
+
+        self.assertEqual(e_info.value.message,
+                         "No predictions are provided as input for the RollingMAE. You should add the predictions by a"
+                         " seperate key word arguments if you add the RollingMAE to the pipeline.")
diff --git a/tests/unit/modules/test_rolling_rmse.py b/tests/unit/modules/test_rolling_rmse.py
index ad84526..22aaa22 100644
--- a/tests/unit/modules/test_rolling_rmse.py
+++ b/tests/unit/modules/test_rolling_rmse.py
@@ -32,9 +32,9 @@ class TestRollingRMSE(unittest.TestCase):
                                '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_data = xr.Dataset({"testCol": ("time", xr.DataArray([-2, -1, 0, 1, 2]).data),
+                                "predictCol1": ("time", xr.DataArray([2, -3, 3, 1, -2]).data),
+                                "predictCol2": ("time", xr.DataArray([4, 4, 3, -2, 1]).data), "time": time})
 
         test_result = self.rolling_rmse.transform(y=test_data['testCol'], gt=test_data['testCol'],
                                                   pred1=test_data['predictCol1'],
@@ -54,9 +54,9 @@ class TestRollingRMSE(unittest.TestCase):
                                '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_data = xr.Dataset({"testCol": ("time", xr.DataArray([-2, -1, 0, 1, 2]).data),
+                                "predictCol1": ("time", xr.DataArray([2, -3, 3, 1, -2]).data),
+                                "predictCol2": ("time", xr.DataArray([4, 4, 3, -2, 1]).data), "time": time})
 
         with pytest.raises(InputNotAvailable) as e_info:
             self.rolling_rmse.transform(y=test_data['testCol'])
diff --git a/tests/unit/modules/test_root_mean_squared_error.py b/tests/unit/modules/test_root_mean_squared_error.py
index cfa2021..7ae96a2 100644
--- a/tests/unit/modules/test_root_mean_squared_error.py
+++ b/tests/unit/modules/test_root_mean_squared_error.py
@@ -32,9 +32,9 @@ class TestRMSECalculator(unittest.TestCase):
                                '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_data = xr.Dataset({"testCol": ("time", xr.DataArray([-2, -1, 0, 1, 2]).data),
+                                "predictCol1": ("time", xr.DataArray([2, -3, 3, 1, -2]).data),
+                                "predictCol2": ("time", xr.DataArray([4, 4, 3, -2, 1]).data), "time": time})
 
         test_result = self.rmse_calculator.transform(y=test_data['testCol'], gt=test_data['testCol'],
                                                      pred1=test_data['predictCol1'],
@@ -58,9 +58,9 @@ class TestRMSECalculator(unittest.TestCase):
 
         result_time = pd.to_datetime(['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_data = xr.Dataset({"testCol": ("time", xr.DataArray([-2, -1, 0, 1, 2]).data),
+                                "predictCol1": ("time", xr.DataArray([2, -3, 3, 1, -2]).data),
+                                "predictCol2": ("time", xr.DataArray([4, 4, 3, -2, 1]).data), "time": time})
 
         test_result = self.rmse_calculator.transform(y=test_data['testCol'], gt=test_data['testCol'],
                                                      pred1=test_data['predictCol1'],
@@ -79,9 +79,9 @@ class TestRMSECalculator(unittest.TestCase):
                                '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_data = xr.Dataset({"testCol": ("time", xr.DataArray([-2, -1, 0, 1, 2]).data),
+                                "predictCol1": ("time", xr.DataArray([2, -3, 3, 1, -2]).data),
+                                "predictCol2": ("time", xr.DataArray([4, 4, 3, -2, 1]).data), "time": time})
 
         with pytest.raises(InputNotAvailable) as e_info:
             self.rmse_calculator.transform(y=test_data['testCol'])
diff --git a/tests/unit/summaries/test_mean_absolute_error.py b/tests/unit/summaries/test_mean_absolute_error.py
new file mode 100644
index 0000000..cf16a21
--- /dev/null
+++ b/tests/unit/summaries/test_mean_absolute_error.py
@@ -0,0 +1,121 @@
+import unittest
+from unittest.mock import MagicMock, patch
+
+import numpy as np
+import pandas as pd
+import pytest
+import xarray as xr
+
+from pywatts.core.exceptions.input_not_available import InputNotAvailable
+from pywatts.summaries.mae_summary import MAE
+
+
+class TestMAE(unittest.TestCase):
+
+    def setUp(self) -> None:
+        self.mae = MAE(name="NAME")
+
+    def tearDown(self) -> None:
+        self.mae = None
+
+    def test_get_params(self):
+        self.assertEqual(self.mae.get_params(),
+                         {'offset': 0})
+
+    def test_set_params(self):
+        self.mae.set_params(offset=24)
+        self.assertEqual(self.mae.get_params(),
+                         {'offset': 24})
+
+    def test_transform_with_filter(self):
+        filter_mock = MagicMock()
+        mae = MAE(filter_method=filter_mock)
+
+        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]).data),
+                                "predictCol1": ("time", xr.DataArray([2, -3, 3, 1, -2]).data),  "time": time})
+
+        filter_mock.return_value = (test_data["predictCol1"].values, test_data["testCol"].values)
+
+        test_result = mae.transform(file_manager=MagicMock(), y=test_data['testCol'],
+                                          pred1=test_data['predictCol1'])
+
+        filter_mock.assert_called_once()
+
+        np.testing.assert_equal(filter_mock.call_args[0][0], test_data["predictCol1"])
+        np.testing.assert_equal(filter_mock.call_args[0][1], test_data["testCol"])
+
+        expected_result = '  * pred1: 2.6\n'
+
+        self.assertEqual(test_result, expected_result)
+    def test_transform(self):
+        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]).data),
+                                "predictCol1": ("time", xr.DataArray([2, -3, 3, 1, -2]).data),
+                                "predictCol2": ("time", xr.DataArray([4, 4, 3, -2, 1]).data), "time": time})
+
+        test_result = self.mae.transform(file_manager=MagicMock(), y=test_data['testCol'], gt=test_data['testCol'],
+                                         pred1=test_data['predictCol1'],
+                                         pred2=test_data['predictCol2'])
+
+        expected_result = '  * gt: 0.0\n  * pred1: 2.6\n  * pred2: 3.6\n'
+
+        self.assertEqual(test_result, expected_result)
+
+    def test_transform_without_predictions(self):
+        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]).data),
+                                "predictCol1": ("time", xr.DataArray([2, -3, 3, 1, -2]).data),
+                                "predictCol2": ("time", xr.DataArray([4, 4, 3, -2, 1]).data), "time": time})
+
+        with pytest.raises(InputNotAvailable) as e_info:
+            self.mae.transform(file_manager=MagicMock(), y=test_data['testCol'])
+
+        self.assertEqual(e_info.value.message,
+                         "No predictions are provided as input for the MAE.  You should add the predictions by a "
+                         "seperate key word arguments if you add the MAE to the pipeline.")
+
+    @patch("builtins.open")
+    @patch("pywatts.summaries.metric_base.cloudpickle")
+    def test_save(self, cloudpickle_mock, open_mock):
+        fm_mock = MagicMock()
+        fm_mock.get_path.return_value = "filter_path"
+        filter_mock = MagicMock()
+
+        mae = MAE(name="NAME", filter_method=filter_mock)
+
+        json = mae.save(fm_mock)
+
+        fm_mock.get_path.assert_called_once_with("NAME_filter.pickle")
+        open_mock.assert_called_once_with("filter_path", "wb")
+
+        cloudpickle_mock.dump.assert_called_once_with(filter_mock, open_mock().__enter__.return_value)
+        self.assertEqual(json["filter"], "filter_path")
+        self.assertEqual(json["params"], {"offset": 0})
+
+
+    @patch("builtins.open")
+    @patch("pywatts.summaries.metric_base.cloudpickle")
+    def test_load(self, cloudpickle_mock, open_mock):
+        load_information = {'params': {'offset': 24}, 'name': 'NAME', 'class': 'RMSE',
+                            'module': 'pywatts.summaries.mae_summary', 'filter': 'filter_path'}
+        filter_mock = MagicMock()
+        cloudpickle_mock.load.return_value = filter_mock
+
+        mae = MAE.load(load_information)
+
+        open_mock.assert_called_once_with("filter_path", "rb")
+        cloudpickle_mock.load.assert_called_once_with(open_mock().__enter__.return_value)
+
+        self.assertEqual(mae.name, "NAME")
+        self.assertEqual(mae.filter_method, filter_mock)
+        self.assertEqual(mae.get_params(), {"offset": 24})
diff --git a/tests/unit/summaries/test_root_mean_squared_error.py b/tests/unit/summaries/test_root_mean_squared_error.py
index 5aee45f..5fb3ae1 100644
--- a/tests/unit/summaries/test_root_mean_squared_error.py
+++ b/tests/unit/summaries/test_root_mean_squared_error.py
@@ -34,8 +34,8 @@ class TestRMSE(unittest.TestCase):
                                '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])),  "time": time})
+        test_data = xr.Dataset({"testCol": ("time", xr.DataArray([-2, -1, 0, 1, 2]).data),
+                                "predictCol1": ("time", xr.DataArray([2, -3, 3, 1, -2]).data),  "time": time})
 
         filter_mock.return_value = (test_data["predictCol1"].values, test_data["testCol"].values)
 
@@ -55,9 +55,9 @@ class TestRMSE(unittest.TestCase):
                                '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_data = xr.Dataset({"testCol": ("time", xr.DataArray([-2, -1, 0, 1, 2]).data),
+                                "predictCol1": ("time", xr.DataArray([2, -3, 3, 1, -2]).data),
+                                "predictCol2": ("time", xr.DataArray([4, 4, 3, -2, 1]).data), "time": time})
 
         test_result = self.rmse.transform(file_manager=MagicMock(), y=test_data['testCol'], gt=test_data['testCol'],
                                           pred1=test_data['predictCol1'],
@@ -72,9 +72,9 @@ class TestRMSE(unittest.TestCase):
                                '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_data = xr.Dataset({"testCol": ("time", xr.DataArray([-2, -1, 0, 1, 2]).data),
+                                "predictCol1": ("time", xr.DataArray([2, -3, 3, 1, -2]).data),
+                                "predictCol2": ("time", xr.DataArray([4, 4, 3, -2, 1]).data), "time": time})
 
         with pytest.raises(InputNotAvailable) as e_info:
             self.rmse.transform(file_manager=MagicMock(), y=test_data['testCol'])
@@ -84,7 +84,7 @@ class TestRMSE(unittest.TestCase):
                          "seperate key word arguments if you add the RMSE to the pipeline.")
 
     @patch("builtins.open")
-    @patch("pywatts.summaries.rmse_summary.cloudpickle")
+    @patch("pywatts.summaries.metric_base.cloudpickle")
     def test_save(self, cloudpickle_mock, open_mock):
         fm_mock = MagicMock()
         fm_mock.get_path.return_value = "filter_path"
@@ -103,7 +103,7 @@ class TestRMSE(unittest.TestCase):
 
 
     @patch("builtins.open")
-    @patch("pywatts.summaries.rmse_summary.cloudpickle")
+    @patch("pywatts.summaries.metric_base.cloudpickle")
     def test_load(self, cloudpickle_mock, open_mock):
         load_information = {'params': {'offset': 24}, 'name': 'NAME', 'class': 'RMSE',
                             'module': 'pywatts.summaries.rmse_summary', 'filter': 'filter_path'}