Repository: KIT-IAI/pyWATTS

Issue:
Implement MASE metric
**What is your feature request related to?**
* Module request,


**Please describe the module/problem**

MASE metric for time series. See [wiki](https://en.wikipedia.org/wiki/Mean_absolute_scaled_error)

**Describe the solution you'd like**
A clear and concise description of what you want to happen.

**What type of Feature Request**
* Module request


Discussion:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5cbbe89..a77e40f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,10 +4,14 @@
 
 ### Added
 * Metric can be calculated on cutouts ([#149](https://github.com/KIT-IAI/pyWATTS/issues/149))
-
+* Add a MASE Summary ([#148](https://github.com/KIT-IAI/pyWATTS/issues/148))
 
 ### Changed
 
+### Fixed
+* Try to handle non matching shapes in metrics. If not possible raise an exception.  ([#154](https://github.com/KIT-IAI/pyWATTS/issues/154))
+
+
 ### Deprecated
 
 ### Fixed
diff --git a/docs/source/api/pywatts.summaries.rst b/docs/source/api/pywatts.summaries.rst
index d5c6065..2bf9c7e 100644
--- a/docs/source/api/pywatts.summaries.rst
+++ b/docs/source/api/pywatts.summaries.rst
@@ -20,6 +20,14 @@ pywatts.summaries.mape\_summary module
    :undoc-members:
    :show-inheritance:
 
+pywatts.summaries.mase\_summary module
+--------------------------------------
+
+.. automodule:: pywatts.summaries.mase_summary
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
 pywatts.summaries.max\_summary module
 -------------------------------------
 
diff --git a/pywatts/core/exceptions/invalid_input_exception.py b/pywatts/core/exceptions/invalid_input_exception.py
new file mode 100644
index 0000000..7490eb8
--- /dev/null
+++ b/pywatts/core/exceptions/invalid_input_exception.py
@@ -0,0 +1,10 @@
+
+class InvalidInputException(Exception):
+    """
+    Exception which indicates that the requested input for a module is not available
+    Attributes:
+        message -- explanation of the exception
+    """
+
+    def __init__(self, message):
+        self.message = message
diff --git a/pywatts/summaries/mase_summary.py b/pywatts/summaries/mase_summary.py
new file mode 100644
index 0000000..232d990
--- /dev/null
+++ b/pywatts/summaries/mase_summary.py
@@ -0,0 +1,31 @@
+import logging
+from typing import Optional, List, Tuple
+
+import numpy as np
+import pandas as pd
+
+from pywatts.summaries.metric_base import MetricBase
+
+logger = logging.getLogger(__name__)
+
+
+class MASE(MetricBase):
+    """
+    Module to calculate the Mean Absolute Scaled Error (MAPE)
+
+    :param offset: Offset, which determines the number of ignored values in the beginning for calculating the MAPE.
+                   Default 0
+    :type offset: int
+    :param filter_method: Filter which should performed on the data before calculating the MAPE.
+    :type filter_method: Callable[[np.ndarray, np.ndarray], Tuple[np.ndarray, np.ndarray]]
+    :param lag: The lag determines the persistence forecast that is used for scaling the error.
+    :type lag: int
+    """
+
+    def __init__(self, name: str = "MASE", filter_method=None, offset: int = 0,
+                 cuts: Optional[List[Tuple[pd.Timestamp, pd.Timestamp]]] = None, lag: int = 1):
+        super().__init__(name=name, filter_method=filter_method, offset=offset, cuts=cuts)
+        self.lag = lag
+
+    def _apply_metric(self, p, t):
+        return np.mean(np.abs(p[self.lag:] - t[self.lag:])) / np.mean(np.abs(t[:-self.lag] - t[self.lag:]))
diff --git a/pywatts/summaries/metric_base.py b/pywatts/summaries/metric_base.py
index d836ff6..add7e07 100644
--- a/pywatts/summaries/metric_base.py
+++ b/pywatts/summaries/metric_base.py
@@ -9,6 +9,7 @@ import xarray as xr
 
 from pywatts.core.base_summary import BaseSummary
 from pywatts.core.exceptions import InputNotAvailable
+from pywatts.core.exceptions.invalid_input_exception import InvalidInputException
 from pywatts.core.filemanager import FileManager
 from pywatts.core.summary_object import SummaryObjectList
 
@@ -98,6 +99,14 @@ class MetricBase(BaseSummary, ABC):
     def _transform(self, kwargs, suffix, summary, t):
         for key, y_hat in kwargs.items():
             p = y_hat.values
+            if p.shape != t.shape:
+                try:
+                    p = p.reshape(t.shape)
+                except ValueError:
+                    raise InvalidInputException(
+                        f"The prediction {key} does not match to the shape of the ground truth y in the instance "
+                        f"{self.name} of class {self.__class__.__name__}.")
+                self.logger.info(f"Reshaped prediction {key} in {self.name}")
             if self.filter_method:
                 p_, t_ = self.filter_method(p, t)
                 mae = self._apply_metric(p_, t_)


Test patch:
diff --git a/tests/unit/summaries/metrics/test_mase.py b/tests/unit/summaries/metrics/test_mase.py
new file mode 100644
index 0000000..5401a2c
--- /dev/null
+++ b/tests/unit/summaries/metrics/test_mase.py
@@ -0,0 +1,94 @@
+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.mase_summary import MASE
+from tests.unit.summaries.metrics.base_test_metric_base import BaseTestMetricBase
+
+
+class TestMase(BaseTestMetricBase, unittest.TestCase):
+    load_information = {'params': {'offset': 24}, 'name': 'NAME', 'class': 'RMSE',
+                        'module': 'pywatts.summaries.mae_summary', 'filter': 'filter_path'}
+
+    def get_metric(self):
+        return MASE
+
+    def test_transform_with_filter(self):
+        filter_mock = MagicMock()
+        mase = MASE(filter_method=filter_mock, lag=1)
+
+        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 = mase.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: complete': (9 / 4) / (4/4)}
+
+        self.assertEqual(test_result.k_v, 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.metric.transform(file_manager=MagicMock(), y=test_data['testCol'], gt=test_data['testCol'],
+                                             pred1=test_data['predictCol1'],
+                                             pred2=test_data['predictCol2'])
+
+        expected_result = {'gt: complete': 0, 'pred1: complete': (9 / 4) / (4/4), 'pred2: complete': (12 / 4) / (4/4)}
+
+        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": (9 / 4) / (4/4),
+                           "pred1: Cut from 2015-06-03 01:00:00 to 2015-06-03 03:00:00" : (3 / 2) / (2/2)}
+
+        self.assertEqual(test_result.k_v, 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.metric.transform(file_manager=MagicMock(), y=test_data['testCol'])
+
+        self.assertEqual(e_info.value.message,
+                         "No predictions are provided as input for the MASE.  You should add the predictions by a "
+                         "seperate key word arguments if you add the MASE to the pipeline.")
\ No newline at end of file
diff --git a/tests/unit/summaries/metrics/test_mean_absolute_error.py b/tests/unit/summaries/metrics/test_mean_absolute_error.py
index 53b6f72..d9aa4e6 100644
--- a/tests/unit/summaries/metrics/test_mean_absolute_error.py
+++ b/tests/unit/summaries/metrics/test_mean_absolute_error.py
@@ -7,6 +7,7 @@ import pytest
 import xarray as xr
 
 from pywatts.core.exceptions.input_not_available import InputNotAvailable
+from pywatts.core.exceptions.invalid_input_exception import InvalidInputException
 from pywatts.summaries.mae_summary import MAE
 from tests.unit.summaries.metrics.base_test_metric_base import BaseTestMetricBase
 
@@ -43,6 +44,41 @@ class TestMAE(BaseTestMetricBase, unittest.TestCase):
 
         self.assertEqual(test_result.k_v, expected_result)
 
+    def test_transform_with_wrong_shape_solvable_by_reshaping(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", "dim1"), xr.DataArray([[2], [-3], [3], [1], [-2]]).data),
+                                "predictCol2": (("time", "dim1"), xr.DataArray([[4], [4], [3], [-2], [1]]).data), "time": time})
+
+        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: complete": 0.0, "pred1: complete": 2.6, "pred2: complete": 3.6}
+
+        self.assertEqual(test_result.k_v, expected_result)
+
+    def test_transform_with_invalid_shape(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", "dim12"), xr.DataArray([[2, 2], [-3, 2], [3, 2], [1, 2], [-2,2 ]]).data),
+                                "predictCol2": (("time", "dim1"), xr.DataArray([[4], [4], [3], [-2], [1]]).data), "time": time})
+
+        with self.assertRaises(InvalidInputException) as cm:
+            test_result = self.metric.transform(file_manager=MagicMock(), y=test_data['testCol'], gt=test_data['testCol'],
+                                         pred1=test_data['predictCol1'],
+                                         pred2=test_data['predictCol2'])
+
+        self.assertEqual(cm.exception.message,
+                         "The prediction pred1 does not match to the shape of the ground truth y in the instance NAME of class MAE.")
+
+
     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',