Repository: sktime/sktime

Issue:
MAPE loss function implementation
**Is your feature request related to a problem? Please describe.**
A need for MAPE loss function to be able to compare forecasting results.

**Describe the solution you'd like**
MAPE loss function compatible with sktime

**Describe alternatives you've considered**
None

**Additional context**
NA


Discussion:

Resolving patch:
diff --git a/sktime/performance_metrics/forecasting/__init__.py b/sktime/performance_metrics/forecasting/__init__.py
--- a/sktime/performance_metrics/forecasting/__init__.py
+++ b/sktime/performance_metrics/forecasting/__init__.py
@@ -1,19 +1,20 @@
 #!/usr/bin/env python3 -u
-# coding: utf-8
+# -*- coding: utf-8 -*-
 # copyright: sktime developers, BSD-3-Clause License (see LICENSE file)
 
-__author__ = ["Markus Löning"]
+__author__ = ["Markus Löning", "Tomasz Chodakowski"]
 __all__ = [
     "MASE",
     "sMAPE",
     "mase_loss",
     "smape_loss",
-    "make_forecasting_scorer"
+    "mape_loss",
+    "make_forecasting_scorer",
 ]
 
 from sktime.performance_metrics.forecasting._classes import MASE
-from sktime.performance_metrics.forecasting._classes import \
-    make_forecasting_scorer
+from sktime.performance_metrics.forecasting._classes import make_forecasting_scorer
 from sktime.performance_metrics.forecasting._classes import sMAPE
 from sktime.performance_metrics.forecasting._functions import mase_loss
 from sktime.performance_metrics.forecasting._functions import smape_loss
+from sktime.performance_metrics.forecasting._functions import mape_loss
diff --git a/sktime/performance_metrics/forecasting/_classes.py b/sktime/performance_metrics/forecasting/_classes.py
--- a/sktime/performance_metrics/forecasting/_classes.py
+++ b/sktime/performance_metrics/forecasting/_classes.py
@@ -1,12 +1,12 @@
+# -*- coding: utf-8 -*-
 from sktime.performance_metrics.forecasting._functions import mase_loss
-from sktime.performance_metrics.forecasting._functions import smape_loss
+from sktime.performance_metrics.forecasting._functions import smape_loss, mape_loss
 
-__author__ = ['Markus Löning']
-__all__ = ["MetricFunctionWrapper", "make_forecasting_scorer", "MASE", "sMAPE"]
+__author__ = ["Markus Löning", "Tomasz Chodakowski"]
+__all__ = ["MetricFunctionWrapper", "make_forecasting_scorer", "MASE", "sMAPE", "MAPE"]
 
 
 class MetricFunctionWrapper:
-
     def __init__(self, fn, name=None, greater_is_better=False):
         self.fn = fn
         self.name = name if name is not None else fn.__name__
@@ -18,25 +18,34 @@ def __call__(self, y_test, y_pred, *args, **kwargs):
 
 def make_forecasting_scorer(fn, name=None, greater_is_better=False):
     """Factory method for creating metric classes from metric functions"""
-    return MetricFunctionWrapper(fn, name=name,
-                                 greater_is_better=greater_is_better)
+    return MetricFunctionWrapper(fn, name=name, greater_is_better=greater_is_better)
 
 
 class MASE(MetricFunctionWrapper):
-
     def __init__(self):
         name = "MASE"
         fn = mase_loss
         greater_is_better = False
-        super(MASE, self).__init__(fn=fn, name=name,
-                                   greater_is_better=greater_is_better)
+        super(MASE, self).__init__(
+            fn=fn, name=name, greater_is_better=greater_is_better
+        )
 
 
 class sMAPE(MetricFunctionWrapper):
-
     def __init__(self):
         name = "sMAPE"
         fn = smape_loss
         greater_is_better = False
-        super(sMAPE, self).__init__(fn=fn, name=name,
-                                    greater_is_better=greater_is_better)
+        super(sMAPE, self).__init__(
+            fn=fn, name=name, greater_is_better=greater_is_better
+        )
+
+
+class MAPE(MetricFunctionWrapper):
+    def __init__(self):
+        name = "MAPE"
+        fn = mape_loss
+        greater_is_better = False
+        super(sMAPE, self).__init__(
+            fn=fn, name=name, greater_is_better=greater_is_better
+        )
diff --git a/sktime/performance_metrics/forecasting/_functions.py b/sktime/performance_metrics/forecasting/_functions.py
--- a/sktime/performance_metrics/forecasting/_functions.py
+++ b/sktime/performance_metrics/forecasting/_functions.py
@@ -8,8 +8,8 @@
 from sktime.utils.validation.series import check_time_index
 from sktime.utils.validation.forecasting import check_y
 
-__author__ = ["Markus Löning"]
-__all__ = ["mase_loss", "smape_loss"]
+__author__ = ["Markus Löning", "Tomasz Chodakowski"]
+__all__ = ["mase_loss", "smape_loss", "mape_loss"]
 
 
 def mase_loss(y_test, y_pred, y_train, sp=1):
@@ -97,3 +97,41 @@ def smape_loss(y_test, y_pred):
     nominator = np.abs(y_test - y_pred)
     denominator = np.abs(y_test) + np.abs(y_pred)
     return np.mean(2.0 * nominator / denominator)
+
+
+def mape_loss(y_test, y_pred):
+    """Mean absolute percentage error (MAPE)
+        MAPE output is non-negative floating point where the best value is 0.0.
+        There is no limit on how large the error can be, particulalrly when `y_test`
+        values are close to zero. In such cases the function returns a large value
+        instead of `inf`.
+
+    Parameters
+    ----------
+    y_test : pandas Series of shape = (fh,) where fh is the forecasting horizon
+        Ground truth (correct) target values.
+    y_pred : pandas Series of shape = (fh,)
+        Estimated target values.
+
+    Returns
+    -------
+    loss : float
+        MAPE loss expressed as a fractional number rather than percentage point.
+
+
+    Examples
+    --------
+    >>> from sklearn.metrics import mean_absolute_error
+    >>> y_test = pd.Series([1, -1, 2])
+    >>> y_pred = pd.Series([2, -2, 4])
+    >>> mape_loss(y_test, y_pred)
+    1.0
+    """
+
+    y_test = check_y(y_test)
+    y_pred = check_y(y_pred)
+    check_equal_time_index(y_test, y_pred)
+
+    eps = np.finfo(np.float64).eps
+
+    return np.mean(np.abs(y_test - y_pred) / np.maximum(np.abs(y_test), eps))


Test patch:
diff --git a/sktime/performance_metrics/tests/__init__.py b/sktime/performance_metrics/tests/__init__.py
new file mode 100644
diff --git a/sktime/performance_metrics/tests/_config.py b/sktime/performance_metrics/tests/_config.py
new file mode 100644
--- /dev/null
+++ b/sktime/performance_metrics/tests/_config.py
@@ -0,0 +1,22 @@
+#!/usr/bin/env python3 -u
+# -*- coding: utf-8 -*-
+
+__author__ = ["Tomasz Chodakowski"]
+__all__ = [
+    "TEST_YS",
+    "TEST_YS_ZERO",
+]
+
+import pandas as pd
+
+from sktime.utils._testing.forecasting import _make_series
+
+RANDOM_SEED = 42
+TEST_YS = [
+    _make_series(n_timepoints=50, random_state=RANDOM_SEED),
+    _make_series(n_timepoints=1, random_state=RANDOM_SEED),
+    pd.Series([0.0000001, 0.0000002, 0.0000003]),
+]
+
+
+TEST_YS_ZERO = [pd.Series([0.0, 0.0, 0.0])]
diff --git a/sktime/performance_metrics/tests/test_performance_metrics_forecasting.py b/sktime/performance_metrics/tests/test_performance_metrics_forecasting.py
new file mode 100644
--- /dev/null
+++ b/sktime/performance_metrics/tests/test_performance_metrics_forecasting.py
@@ -0,0 +1,65 @@
+#!/usr/bin/env python3 -u
+# -*- coding: utf-8 -*-
+# copyright: sktime developers, BSD-3-Clause License (see LICENSE file)
+
+__author__ = ["Tomasz Chodakowski"]
+
+import pytest
+
+from sktime.performance_metrics.forecasting import mape_loss
+from sktime.performance_metrics.forecasting import smape_loss
+
+from sktime.performance_metrics.tests._config import TEST_YS, TEST_YS_ZERO
+
+
+@pytest.mark.parametrize("test_y", TEST_YS_ZERO)
+def test_mape_loss_near_zero(test_y):
+    # mape should be large when predicitons are close to zero
+    y_test = test_y
+    y_pred = y_test + 0.001
+    assert mape_loss(y_test, y_pred) > 1e6
+
+
+@pytest.mark.parametrize("test_y", TEST_YS)
+def test_mape_loss(test_y):
+    y_test = test_y
+    y_pred = y_test
+    assert mape_loss(y_test, y_pred) == pytest.approx(0.0)
+
+    y_pred = y_test * 1.1
+    assert mape_loss(y_test, y_pred) == pytest.approx(0.1)
+
+    y_pred = y_test * 0.9
+    assert mape_loss(y_test, y_pred) == pytest.approx(0.1)
+
+    y_pred = y_test * 1.000001
+    assert mape_loss(y_test, y_pred) == pytest.approx(0.000001)
+
+    y_pred = y_test * 2
+    assert mape_loss(y_test, y_pred) == pytest.approx(1.0)
+
+    y_pred = y_test * 100
+    assert mape_loss(y_test, y_pred) == pytest.approx(99.0)
+
+
+@pytest.mark.parametrize("test_y", TEST_YS)
+def test_smape_loss(test_y):
+    y_test = test_y
+    y_pred = y_test
+    assert smape_loss(y_test, y_pred) == pytest.approx(0.0)
+    assert smape_loss(y_pred, y_test) == pytest.approx(0.0)
+
+    y_pred = y_test * 1.1
+    assert smape_loss(y_test, y_pred) == pytest.approx(0.095238095238)
+    assert smape_loss(y_pred, y_test) == pytest.approx(0.095238095238)
+
+    y_pred = y_test * 1.000001
+    assert smape_loss(y_test, y_pred) == pytest.approx(0.000001)
+    assert smape_loss(y_pred, y_test) == pytest.approx(0.000001)
+
+    y_pred = y_test * 2.0
+    assert smape_loss(y_test, y_pred) == pytest.approx(0.6666666)
+    assert smape_loss(y_pred, y_test) == pytest.approx(0.6666666)
+
+    y_pred = y_test * 100
+    assert smape_loss(y_test, y_pred) == smape_loss(y_pred, y_test)