Repository: KIT-IAI/pyWATTS

Issue:
Calculate Metrics on cutouts
**What is your feature request related to?**
* Module request

**Please describe the module/problem**

Sometimes, it is useful to calculate a metric not only on the complete dataset but also on a cutout. This functionality is currently not available in the RMSE, MAE and so on.

**Describe the solution you'd like**
I would like to specify the additional cutouts for which the metric should be calculated by timestamps when initializing the summary


**What type of Feature Request**
* Module request

Discussion:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6d1db58..5cbbe89 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,8 @@
 ## Unreleased
 
 ### Added
+* Metric can be calculated on cutouts ([#149](https://github.com/KIT-IAI/pyWATTS/issues/149))
+
 
 ### Changed
 
diff --git a/pywatts/summaries/mae_summary.py b/pywatts/summaries/mae_summary.py
index 91f84e7..bd93760 100644
--- a/pywatts/summaries/mae_summary.py
+++ b/pywatts/summaries/mae_summary.py
@@ -20,4 +20,4 @@ class MAE(MetricBase):
 
 
     def _apply_metric(self, p, t):
-        return np.mean(np.abs((p[self.offset:] - t[self.offset:])))
+        return np.mean(np.abs((p - t)))
diff --git a/pywatts/summaries/mape_summary.py b/pywatts/summaries/mape_summary.py
index 7420099..bc2fdcf 100644
--- a/pywatts/summaries/mape_summary.py
+++ b/pywatts/summaries/mape_summary.py
@@ -20,5 +20,5 @@ class MAPE(MetricBase):
 
 
     def _apply_metric(self, p, t):
-        non_zero_indexes = t.nonzero()[0][t.nonzero()[0] >= self.offset]
+        non_zero_indexes = t.nonzero()[0]
         return 100 * np.mean(np.abs((p[non_zero_indexes] - t[non_zero_indexes]) / t[non_zero_indexes]))
diff --git a/pywatts/summaries/max_summary.py b/pywatts/summaries/max_summary.py
index 3f58bcd..bdc6372 100644
--- a/pywatts/summaries/max_summary.py
+++ b/pywatts/summaries/max_summary.py
@@ -20,5 +20,5 @@ class MaxErr(MetricBase):
 
 
     def _apply_metric(self, p, t):
-        return np.max(np.abs((p[self.offset:] - t[self.offset:])))
+        return np.max(np.abs((p- t)))
 
diff --git a/pywatts/summaries/metric_base.py b/pywatts/summaries/metric_base.py
index f8d4df6..d836ff6 100644
--- a/pywatts/summaries/metric_base.py
+++ b/pywatts/summaries/metric_base.py
@@ -1,15 +1,16 @@
 import logging
 from abc import ABC, abstractmethod
-from typing import Callable, Tuple, Dict, Optional
+from typing import Callable, Tuple, Dict, Optional, List
 
 import cloudpickle
 import numpy as np
+import pandas as pd
 import xarray as xr
 
 from pywatts.core.base_summary import BaseSummary
-from pywatts.core.summary_object import SummaryObject, SummaryObjectList
 from pywatts.core.exceptions import InputNotAvailable
 from pywatts.core.filemanager import FileManager
+from pywatts.core.summary_object import SummaryObjectList
 
 logger = logging.getLogger(__name__)
 
@@ -28,10 +29,15 @@ class MetricBase(BaseSummary, ABC):
     def __init__(self,
                  name: str = None,
                  filter_method: Callable[[np.ndarray, np.ndarray], Tuple[np.ndarray, np.ndarray]] = None,
-                 offset: int = 0):
+                 offset: int = 0,
+                 cuts: List[Tuple[pd.Timestamp, pd.Timestamp]] = None):
         super().__init__(name if name is not None else self.__class__.__name__)
         self.offset = offset
         self.filter_method = filter_method
+        if cuts is None:
+            self.cuts = []
+        else:
+            self.cuts = cuts
 
     def get_params(self) -> Dict[str, object]:
         """
@@ -40,17 +46,22 @@ class MetricBase(BaseSummary, ABC):
         :return: Parameters set for the Metric
         :rtype: Dict[str, object]
         """
-        return {"offset": self.offset}
+        return {"offset": self.offset,
+                "cuts": self.cuts}
 
-    def set_params(self, offset: Optional[int] = None):
+    def set_params(self, offset: Optional[int] = None, cuts=Optional[List[Tuple[pd.Timestamp, pd.Timestamp]]]):
         """
         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
+        :param cuts: The cutouts on which the metric should be additionally calculated.
+        :type cuts: List[Tuple[pd.Timestamp, pd.Timestamp]]
         """
         if offset:
             self.offset = offset
+        if cuts is not None:
+            self.cuts = cuts
 
     def transform(self, file_manager: FileManager, y: xr.DataArray, **kwargs: xr.DataArray) -> SummaryObjectList:
         """
@@ -74,6 +85,17 @@ class MetricBase(BaseSummary, ABC):
             logger.error(error_message)
             raise InputNotAvailable(error_message)
 
+        y_ = y[self.offset:]
+        kwargs_ = {key: value[self.offset:] for key, value in kwargs.items()}
+        self._transform({key: value for key, value in kwargs_.items()}, ": complete", summary, y_.values)
+        for start, end in self.cuts:
+            _kwargs = {key: value.loc[start:end] for key, value in kwargs_.items()}
+            if not len(y_.loc[start:end].values) == 0:
+                self._transform(_kwargs, f": Cut from {start} to {end}", summary, y_.loc[start:end].values)
+
+        return summary
+
+    def _transform(self, kwargs, suffix, summary, t):
         for key, y_hat in kwargs.items():
             p = y_hat.values
             if self.filter_method:
@@ -82,7 +104,7 @@ class MetricBase(BaseSummary, ABC):
 
             else:
                 mae = self._apply_metric(p, t)
-            summary.set_kv(key, mae)
+            summary.set_kv(key + suffix, mae)
         return summary
 
     def save(self, fm: FileManager) -> Dict:
diff --git a/pywatts/summaries/min_summary.py b/pywatts/summaries/min_summary.py
index 2ad1ea3..2eb3899 100644
--- a/pywatts/summaries/min_summary.py
+++ b/pywatts/summaries/min_summary.py
@@ -20,5 +20,5 @@ class MinErr(MetricBase):
 
 
     def _apply_metric(self, p, t):
-        return np.min(np.abs((p[self.offset:] - t[self.offset:])))
+        return np.min(np.abs((p - t)))
 
diff --git a/pywatts/summaries/rmse_summary.py b/pywatts/summaries/rmse_summary.py
index 47f1db7..79a5f9c 100644
--- a/pywatts/summaries/rmse_summary.py
+++ b/pywatts/summaries/rmse_summary.py
@@ -19,5 +19,5 @@ class RMSE(MetricBase):
     """
 
     def _apply_metric(self, p, t):
-        return np.sqrt(np.mean((p[self.offset:] - t[self.offset:]) ** 2))
+        return np.sqrt(np.mean((p - t) ** 2))
 


Test patch:
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/unit/core/__init__.py b/tests/unit/core/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/unit/modules/__init__.py b/tests/unit/modules/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/unit/summaries/__init__.py b/tests/unit/summaries/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/unit/summaries/metrics/__init__.py b/tests/unit/summaries/metrics/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/unit/summaries/metrics/base_test_metric_base.py b/tests/unit/summaries/metrics/base_test_metric_base.py
new file mode 100644
index 0000000..7bd9255
--- /dev/null
+++ b/tests/unit/summaries/metrics/base_test_metric_base.py
@@ -0,0 +1,60 @@
+from abc import ABC, abstractmethod
+from unittest.mock import MagicMock, patch
+
+
+class BaseTestMetricBase(ABC):
+    def setUp(self) -> None:
+        self.metric = self.get_metric()(name="NAME")
+
+
+    def tearDown(self) -> None:
+        self.metric = None
+
+    def test_get_params(self):
+        self.assertEqual(self.metric.get_params(),
+                         {'offset': 0, "cuts":[]})
+
+    def test_set_params(self):
+        self.metric.set_params(offset=24, cuts=[("Test", "test")])
+        self.assertEqual(self.metric.get_params(),
+                         {'offset': 24,
+                          "cuts": [("Test", "test")]})
+
+
+    @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()
+
+        rmse = self.get_metric()(name="NAME", filter_method=filter_mock)
+
+        json = rmse.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, "cuts" : []})
+
+
+    @patch("builtins.open")
+    @patch("pywatts.summaries.metric_base.cloudpickle")
+    def test_load(self, cloudpickle_mock, open_mock):
+        filter_mock = MagicMock()
+        cloudpickle_mock.load.return_value = filter_mock
+
+        metric = self.get_metric().load(self.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(metric.name, "NAME")
+        self.assertEqual(metric.filter_method, filter_mock)
+        self.assertEqual(metric.get_params(), {"offset": 24, "cuts": []})
+
+    @abstractmethod
+    def get_metric(self):
+        pass
\ No newline at end of file
diff --git a/tests/unit/summaries/test_max_error.py b/tests/unit/summaries/metrics/test_max_error.py
similarity index 56%
rename from tests/unit/summaries/test_max_error.py
rename to tests/unit/summaries/metrics/test_max_error.py
index 97b8fcf..a085524 100644
--- a/tests/unit/summaries/test_max_error.py
+++ b/tests/unit/summaries/metrics/test_max_error.py
@@ -8,24 +8,15 @@ import xarray as xr
 
 from pywatts.core.exceptions.input_not_available import InputNotAvailable
 from pywatts.summaries.max_summary import MaxErr
+from tests.unit.summaries.metrics.base_test_metric_base import BaseTestMetricBase
 
 
-class TestMaxErr(unittest.TestCase):
+class TestMaxErr(BaseTestMetricBase, unittest.TestCase):
+    load_information = {'params': {'offset': 24}, 'name': 'NAME', 'class': 'RMSE',
+                        'module': 'pywatts.summaries.mae_summary', 'filter': 'filter_path'}
 
-    def setUp(self) -> None:
-        self.max_err = MaxErr(name="NAME")
-
-    def tearDown(self) -> None:
-        self.max_err = None
-
-    def test_get_params(self):
-        self.assertEqual(self.max_err.get_params(),
-                         {'offset': 0})
-
-    def test_set_params(self):
-        self.max_err.set_params(offset=24)
-        self.assertEqual(self.max_err.get_params(),
-                         {'offset': 24})
+    def get_metric(self):
+        return MaxErr
 
     def test_transform_with_filter(self):
         filter_mock = MagicMock()
@@ -48,7 +39,7 @@ class TestMaxErr(unittest.TestCase):
         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": 4}
+        expected_result = {'pred1: complete': 4}
 
         self.assertEqual(test_result.k_v, expected_result)
 
@@ -61,15 +52,15 @@ class TestMaxErr(unittest.TestCase):
                                 "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.max_err.transform(file_manager=MagicMock(), y=test_data['testCol'], gt=test_data['testCol'],
+        test_result = self.metric.transform(file_manager=MagicMock(), y=test_data['testCol'], gt=test_data['testCol'],
                                              pred1=test_data['predictCol1'],
                                              pred2=test_data['predictCol2'])
 
-        expected_result = {"gt": 0, "pred1": 4, "pred2": 6}
+        expected_result = {'gt: complete': 0, 'pred1: complete': 4, 'pred2: complete': 6}
 
         self.assertEqual(test_result.k_v, expected_result)
 
-    def test_transform_without_predictions(self):
+    def test_transform_cutouts(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'])
@@ -78,46 +69,26 @@ class TestMaxErr(unittest.TestCase):
                                 "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.max_err.transform(file_manager=MagicMock(), y=test_data['testCol'])
-
-        self.assertEqual(e_info.value.message,
-                         "No predictions are provided as input for the MaxErr.  You should add the predictions by a "
-                         "seperate key word arguments if you add the MaxErr 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 = MaxErr(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})
+        self.metric.set_params(cuts=[(pd.Timestamp('2015-06-03 01:00:00'), pd.Timestamp('2015-06-03 03:00:00'))])
+        test_result = self.metric.transform(file_manager=MagicMock(), y=test_data['testCol'],
+                                          pred1=test_data['predictCol1'])
+        expected_result = {"pred1: complete": 4,
+                           "pred1: Cut from 2015-06-03 01:00:00 to 2015-06-03 03:00:00" : 3}
 
-    @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
+        self.assertEqual(test_result.k_v, expected_result)
 
-        mae = MaxErr.load(load_information)
+    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'])
 
-        open_mock.assert_called_once_with("filter_path", "rb")
-        cloudpickle_mock.load.assert_called_once_with(open_mock().__enter__.return_value)
+        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})
 
-        self.assertEqual(mae.name, "NAME")
-        self.assertEqual(mae.filter_method, filter_mock)
-        self.assertEqual(mae.get_params(), {"offset": 24})
+        with pytest.raises(InputNotAvailable) as e_info:
+            self.metric.transform(file_manager=MagicMock(), y=test_data['testCol'])
 
+        self.assertEqual(e_info.value.message,
+                         "No predictions are provided as input for the MaxErr.  You should add the predictions by a "
+                         "seperate key word arguments if you add the MaxErr to the pipeline.")
\ No newline at end of file
diff --git a/tests/unit/summaries/test_mean_abs_percentage_error.py b/tests/unit/summaries/metrics/test_mean_abs_percentage_error.py
similarity index 56%
rename from tests/unit/summaries/test_mean_abs_percentage_error.py
rename to tests/unit/summaries/metrics/test_mean_abs_percentage_error.py
index 1dc99c5..6584065 100644
--- a/tests/unit/summaries/test_mean_abs_percentage_error.py
+++ b/tests/unit/summaries/metrics/test_mean_abs_percentage_error.py
@@ -8,24 +8,12 @@ import xarray as xr
 
 from pywatts.core.exceptions.input_not_available import InputNotAvailable
 from pywatts.summaries.mape_summary import MAPE
+from tests.unit.summaries.metrics.base_test_metric_base import BaseTestMetricBase
 
 
-class TestMAE(unittest.TestCase):
-
-    def setUp(self) -> None:
-        self.mape = MAPE(name="NAME")
-
-    def tearDown(self) -> None:
-        self.mape = None
-
-    def test_get_params(self):
-        self.assertEqual(self.mape.get_params(),
-                         {'offset': 0})
-
-    def test_set_params(self):
-        self.mape.set_params(offset=24)
-        self.assertEqual(self.mape.get_params(),
-                         {'offset': 24})
+class TestMASE(BaseTestMetricBase, unittest.TestCase):
+    load_information = {'params': {'offset': 24, "cuts": []}, 'name': 'NAME', 'class': 'RMSE',
+                        'module': 'pywatts.summaries.mae_summary', 'filter': 'filter_path'}
 
     def test_transform_with_filter(self):
         filter_mock = MagicMock()
@@ -48,7 +36,7 @@ class TestMAE(unittest.TestCase):
         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"])
 
-        self.assertEqual(test_result.k_v, {"pred1": 3/2 * 100})
+        self.assertEqual(test_result.k_v, {"pred1: complete": 3/2 * 100})
 
     def test_transform(self):
         time = pd.to_datetime(['2015-06-03 00:00:00', '2015-06-03 01:00:00',
@@ -59,14 +47,14 @@ class TestMAE(unittest.TestCase):
                                 "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.mape.transform(file_manager=MagicMock(), y=test_data['testCol'], gt=test_data['testCol'],
+        test_result = self.metric.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, "pred1": 3/2 * 100, "pred2": 23/8 * 100}
+        expected_result = {"gt: complete": 0.0, "pred1: complete": 3/2 * 100, "pred2: complete": 23/8 * 100}
 
         self.assertEqual(test_result.k_v, expected_result)
 
-    def test_transform_without_predictions(self):
+    def test_transform_cutouts(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'])
@@ -75,45 +63,31 @@ class TestMAE(unittest.TestCase):
                                 "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.mape.transform(file_manager=MagicMock(), y=test_data['testCol'])
-
-        self.assertEqual(e_info.value.message,
-                         "No predictions are provided as input for the MAPE.  You should add the predictions by a "
-                         "seperate key word arguments if you add the MAPE 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 = MAPE(name="NAME", filter_method=filter_mock)
+        self.metric.set_params(cuts=[(pd.Timestamp('2015-06-03 01:00:00'), pd.Timestamp('2015-06-03 03:00:00'))])
+        test_result = self.metric.transform(file_manager=MagicMock(), y=test_data['testCol'],
+                                          pred1=test_data['predictCol1'])
+        expected_result = {"pred1: complete": 3/2 * 100,
+                           "pred1: Cut from 2015-06-03 01:00:00 to 2015-06-03 03:00:00" : (2/1 + 0/1)/2 * 100}
 
-        json = mae.save(fm_mock)
+        self.assertEqual(test_result.k_v, expected_result)
 
-        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})
+    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})
 
-    @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
+        with pytest.raises(InputNotAvailable) as e_info:
+            self.metric.transform(file_manager=MagicMock(), y=test_data['testCol'])
 
-        mae = MAPE.load(load_information)
+        self.assertEqual(e_info.value.message,
+                         "No predictions are provided as input for the MAPE.  You should add the predictions by a "
+                         "seperate key word arguments if you add the MAPE to the pipeline.")
 
-        open_mock.assert_called_once_with("filter_path", "rb")
-        cloudpickle_mock.load.assert_called_once_with(open_mock().__enter__.return_value)
+    def get_metric(self):
+        return MAPE
 
-        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_mean_absolute_error.py b/tests/unit/summaries/metrics/test_mean_absolute_error.py
similarity index 57%
rename from tests/unit/summaries/test_mean_absolute_error.py
rename to tests/unit/summaries/metrics/test_mean_absolute_error.py
index fd5552a..53b6f72 100644
--- a/tests/unit/summaries/test_mean_absolute_error.py
+++ b/tests/unit/summaries/metrics/test_mean_absolute_error.py
@@ -8,24 +8,15 @@ import xarray as xr
 
 from pywatts.core.exceptions.input_not_available import InputNotAvailable
 from pywatts.summaries.mae_summary import MAE
+from tests.unit.summaries.metrics.base_test_metric_base import BaseTestMetricBase
 
 
-class TestMAE(unittest.TestCase):
+class TestMAE(BaseTestMetricBase, unittest.TestCase):
+    load_information = {'params': {'offset': 24}, 'name': 'NAME', 'class': 'RMSE',
+                        'module': 'pywatts.summaries.mae_summary', 'filter': 'filter_path'}
 
-    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 get_metric(self):
+        return MAE
 
     def test_transform_with_filter(self):
         filter_mock = MagicMock()
@@ -48,7 +39,7 @@ class TestMAE(unittest.TestCase):
         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}
+        expected_result = {"pred1: complete": 2.6}
 
         self.assertEqual(test_result.k_v, expected_result)
 
@@ -61,11 +52,28 @@ class TestMAE(unittest.TestCase):
                                 "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'],
+        test_result = self.metric.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, "pred1": 2.6, "pred2": 3.6}
+        expected_result = {"gt: complete": 0.0, "pred1: complete": 2.6, "pred2: complete": 3.6}
+
+        self.assertEqual(test_result.k_v, expected_result)
+
+    def test_transform_cutouts(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})
+
+        self.metric.set_params(cuts=[(pd.Timestamp('2015-06-03 01:00:00'), pd.Timestamp('2015-06-03 03:00:00'))])
+        test_result = self.metric.transform(file_manager=MagicMock(), y=test_data['testCol'],
+                                             pred1=test_data['predictCol1'])
+        expected_result = {"pred1: complete": 2.6,
+                           "pred1: Cut from 2015-06-03 01:00:00 to 2015-06-03 03:00:00": 5 / 3}
 
         self.assertEqual(test_result.k_v, expected_result)
 
@@ -79,44 +87,9 @@ class TestMAE(unittest.TestCase):
                                 "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.metric.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_min_error.py b/tests/unit/summaries/metrics/test_min_error.py
similarity index 57%
rename from tests/unit/summaries/test_min_error.py
rename to tests/unit/summaries/metrics/test_min_error.py
index 0f043a5..0bb3c89 100644
--- a/tests/unit/summaries/test_min_error.py
+++ b/tests/unit/summaries/metrics/test_min_error.py
@@ -8,24 +8,15 @@ import xarray as xr
 
 from pywatts.core.exceptions.input_not_available import InputNotAvailable
 from pywatts.summaries.min_summary import MinErr
+from tests.unit.summaries.metrics.base_test_metric_base import BaseTestMetricBase
 
 
-class TestMinErr(unittest.TestCase):
+class TestMinErr(BaseTestMetricBase, unittest.TestCase):
+    load_information = {'params': {'offset': 24}, 'name': 'NAME', 'class': 'RMSE',
+                        'module': 'pywatts.summaries.mae_summary', 'filter': 'filter_path'}
 
-    def setUp(self) -> None:
-        self.min_err = MinErr()
-
-    def tearDown(self) -> None:
-        self.min_err = None
-
-    def test_get_params(self):
-        self.assertEqual(self.min_err.get_params(),
-                         {'offset': 0})
-
-    def test_set_params(self):
-        self.min_err.set_params(offset=24)
-        self.assertEqual(self.min_err.get_params(),
-                         {'offset': 24})
+    def get_metric(self):
+        return MinErr
 
     def test_transform_with_filter(self):
         filter_mock = MagicMock()
@@ -48,7 +39,7 @@ class TestMinErr(unittest.TestCase):
         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": 0}
+        expected_result = {"pred1: complete": 0}
 
         self.assertEqual(test_result.k_v, expected_result)
 
@@ -61,15 +52,15 @@ class TestMinErr(unittest.TestCase):
                                 "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.min_err.transform(file_manager=MagicMock(), y=test_data['testCol'], gt=test_data['testCol'],
+        test_result = self.metric.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, "pred1": 0, "pred2": 1}
+        expected_result = {"gt: complete": 0.0, "pred1: complete": 0, "pred2: complete": 1}
 
         self.assertEqual(test_result.k_v, expected_result)
 
-    def test_transform_without_predictions(self):
+    def test_transform_cutouts(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'])
@@ -78,44 +69,28 @@ class TestMinErr(unittest.TestCase):
                                 "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.min_err.transform(file_manager=MagicMock(), y=test_data['testCol'])
-
-        self.assertEqual(e_info.value.message,
-                         "No predictions are provided as input for the MinErr.  You should add the predictions by a "
-                         "seperate key word arguments if you add the MinErr 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 = MinErr(name="NAME", filter_method=filter_mock)
+        self.metric.set_params(cuts=[(pd.Timestamp('2015-06-03 01:00:00'), pd.Timestamp('2015-06-03 03:00:00'))])
+        test_result = self.metric.transform(file_manager=MagicMock(), y=test_data['testCol'],
+                                          pred1=test_data['predictCol1'])
+        expected_result = {"pred1: complete": 0,
+                           "pred1: Cut from 2015-06-03 01:00:00 to 2015-06-03 03:00:00" : 0}
 
-        json = mae.save(fm_mock)
+        self.assertEqual(test_result.k_v, expected_result)
 
-        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})
+    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'])
 
-    @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
+        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})
 
-        mae = MinErr.load(load_information)
+        with pytest.raises(InputNotAvailable) as e_info:
+            self.metric.transform(file_manager=MagicMock(), y=test_data['testCol'])
 
-        open_mock.assert_called_once_with("filter_path", "rb")
-        cloudpickle_mock.load.assert_called_once_with(open_mock().__enter__.return_value)
+        self.assertEqual(e_info.value.message,
+                         "No predictions are provided as input for the MinErr.  You should add the predictions by a "
+                         "seperate key word arguments if you add the MinErr to the pipeline.")
 
-        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/metrics/test_root_mean_squared_error.py
similarity index 58%
rename from tests/unit/summaries/test_root_mean_squared_error.py
rename to tests/unit/summaries/metrics/test_root_mean_squared_error.py
index df6475b..9ee8755 100644
--- a/tests/unit/summaries/test_root_mean_squared_error.py
+++ b/tests/unit/summaries/metrics/test_root_mean_squared_error.py
@@ -7,24 +7,16 @@ import xarray as xr
 
 from pywatts.core.exceptions.input_not_available import InputNotAvailable
 from pywatts.summaries import RMSE
+from tests.unit.summaries.metrics.base_test_metric_base import BaseTestMetricBase
 
 
-class TestRMSE(unittest.TestCase):
+class TestRMSE(BaseTestMetricBase, unittest.TestCase):
 
-    def setUp(self) -> None:
-        self.rmse = RMSE(name="NAME")
-
-    def tearDown(self) -> None:
-        self.rmse = None
-
-    def test_get_params(self):
-        self.assertEqual(self.rmse.get_params(),
-                         {'offset': 0})
+    load_information = {'params': {'offset': 24}, 'name': 'NAME', 'class': 'RMSE',
+                            'module': 'pywatts.summaries.rmse_summary', 'filter': 'filter_path'}
 
-    def test_set_params(self):
-        self.rmse.set_params(offset=24)
-        self.assertEqual(self.rmse.get_params(),
-                         {'offset': 24})
+    def get_metric(self):
+        return RMSE
 
     def test_transform_with_filter(self):
         filter_mock = MagicMock()
@@ -47,7 +39,7 @@ class TestRMSE(unittest.TestCase):
         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": 3}
+        expected_result = {"pred1: complete": 3}
 
         self.assertEqual(test_result.k_v, expected_result)
 
@@ -60,15 +52,15 @@ class TestRMSE(unittest.TestCase):
                                 "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'],
+        test_result = self.metric.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, "pred1": 3.0, "pred2": 4.0}
+        expected_result = {"gt: complete": 0.0, "pred1: complete": 3.0, "pred2: complete": 4.0}
 
         self.assertEqual(test_result.k_v, expected_result)
 
-    def test_transform_without_predictions(self):
+    def test_transform_cutouts(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'])
@@ -77,45 +69,29 @@ class TestRMSE(unittest.TestCase):
                                 "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'])
-
-        self.assertEqual(e_info.value.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.")
-
-    @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()
-
-        rmse = RMSE(name="NAME", filter_method=filter_mock)
+        self.metric.set_params(cuts=[(pd.Timestamp('2015-06-03 01:00:00'), pd.Timestamp('2015-06-03 03:00:00'))])
+        test_result = self.metric.transform(file_manager=MagicMock(), y=test_data['testCol'],
+                                          pred1=test_data['predictCol1'])
+        expected_result = {"pred1: complete": 3,
+                           "pred1: Cut from 2015-06-03 01:00:00 to 2015-06-03 03:00:00" : np.sqrt(13/3)}
 
-        json = rmse.save(fm_mock)
+        self.assertEqual(test_result.k_v, expected_result)
 
-        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})
+    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})
 
-    @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.rmse_summary', 'filter': 'filter_path'}
-        filter_mock = MagicMock()
-        cloudpickle_mock.load.return_value = filter_mock
+        with pytest.raises(InputNotAvailable) as e_info:
+            self.metric.transform(file_manager=MagicMock(), y=test_data['testCol'])
 
-        rmse = RMSE.load(load_information)
+        self.assertEqual(e_info.value.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.")
 
-        open_mock.assert_called_once_with("filter_path", "rb")
-        cloudpickle_mock.load.assert_called_once_with(open_mock().__enter__.return_value)
 
-        self.assertEqual(rmse.name, "NAME")
-        self.assertEqual(rmse.filter_method, filter_mock)
-        self.assertEqual(rmse.get_params(), {"offset": 24})