Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions DashAI/back/initial_components.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,8 @@
from DashAI.back.metrics.classification.precision import Precision
from DashAI.back.metrics.classification.recall import Recall
from DashAI.back.metrics.classification.roc_auc import ROCAUC
from DashAI.back.metrics.forecasting.mape import MAPE
from DashAI.back.metrics.forecasting.smape import SMAPE
from DashAI.back.metrics.regression.explained_variance import ExplainedVariance
from DashAI.back.metrics.regression.mae import MAE
from DashAI.back.metrics.regression.median_absolute_error import MedianAbsoluteError
Expand All @@ -182,6 +184,14 @@
from DashAI.back.models.efficientnet_b0_image_classifier import (
EfficientNetB0ImageClassifier,
)
from DashAI.back.models.forecasting.arima import ARIMA
from DashAI.back.models.forecasting.exponential_smoothing import (
ExponentialSmoothing,
)
from DashAI.back.models.forecasting.naive import NaiveForecaster
from DashAI.back.models.forecasting.seasonal_naive import (
SeasonalNaiveForecaster,
)

# Models
from DashAI.back.models.hugging_face.albert_transformer import AlbertTransformer
Expand Down Expand Up @@ -477,6 +487,10 @@ def get_initial_components():
TranslationTask,
RegressionTask,
ForecastingTask,
NaiveForecaster,
SeasonalNaiveForecaster,
ARIMA,
ExponentialSmoothing,
TextToImageGenerationTask,
TextToTextGenerationTask,
ControlNetTask,
Expand Down Expand Up @@ -601,6 +615,8 @@ def get_initial_components():
Chrf,
MSE,
RMSE,
MAPE,
SMAPE,
MAE,
R2,
MedianAbsoluteError,
Expand Down
Empty file.
102 changes: 102 additions & 0 deletions DashAI/back/metrics/forecasting/mape.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
"""DashAI MAPE forecasting metric implementation."""

from typing import TYPE_CHECKING

from DashAI.back.core.utils import MultilingualString
from DashAI.back.metrics.regression_metric import RegressionMetric, prepare_to_metric

if TYPE_CHECKING:
import numpy as np

from DashAI.back.dataloaders.classes.dashai_dataset import DashAIDataset


class MAPE(RegressionMetric):
"""Average error as a percentage of the true value.

Mean Absolute Percentage Error expresses each error relative to the value
it missed, so a forecast can be judged without knowing the scale of the
series. That is what makes it the usual way to compare a forecast across
products, regions or periods whose magnitudes differ by orders of
magnitude, where an absolute error in units says nothing on its own.

::

MAPE(y, y') = 100 / N · sum |yi - y'i| / |yi|

Range: [0, +inf), lower is better.

The formula divides by the true value, so it is undefined wherever that
value is zero. Those rows are left out of the average rather than being
allowed to produce an infinity that would swallow the whole score, and a
series that is zero throughout has no defined MAPE at all, which is
reported as ``nan``. Prefer :class:`SMAPE` on series that reach zero.

MAPE is also asymmetric: it penalises a forecast that is too high more
heavily than one that is too low by the same amount, since the denominator
stays fixed while the error does not.

References
----------
- [1] https://otexts.com/fpp3/accuracy.html
"""

DESCRIPTION = MultilingualString(
en=(
"Average error as a percentage of the true value, so forecasts can "
"be compared across series of different sizes. Undefined where the "
"true value is zero, and those rows are skipped."
),
es=(
"Error promedio como porcentaje del valor real, lo que permite "
"comparar pronosticos entre series de distinta magnitud. No esta "
"definido cuando el valor real es cero, y esas filas se omiten."
),
pt=(
"Erro medio como porcentagem do valor real, permitindo comparar "
"previsoes entre series de magnitudes diferentes. Indefinido "
"quando o valor real e zero, e essas linhas sao ignoradas."
),
de=(
"Durchschnittlicher Fehler als Prozentsatz des wahren Wertes, "
"sodass Prognosen ueber unterschiedlich grosse Reihen hinweg "
"vergleichbar sind. Undefiniert, wo der wahre Wert null ist; "
"solche Zeilen werden uebersprungen."
),
zh=(
"以真实值的百分比表示的平均误差,便于比较不同量级序列的预测效果。"
"当真实值为零时无定义,这些行会被跳过。"
),
)

@staticmethod
def score(
true_values: "DashAIDataset",
pred_values: "np.ndarray",
) -> float:
"""Calculate the MAPE between true values and predicted values.

Parameters
----------
true_values : DashAIDataset
A DashAI dataset with true values.
pred_values : np.ndarray
A one-dimensional array with the predicted values for each
instance.

Returns
-------
float
MAPE as a percentage. ``nan`` when every true value is zero, since
no row of such a series has a defined percentage error.
"""
import numpy as np

true_values, pred_values = prepare_to_metric(true_values, pred_values)

defined = true_values != 0
if not defined.any():
return float("nan")

errors = np.abs(true_values[defined] - pred_values[defined])
return float(100 * np.mean(errors / np.abs(true_values[defined])))
101 changes: 101 additions & 0 deletions DashAI/back/metrics/forecasting/smape.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
"""DashAI sMAPE forecasting metric implementation."""

from typing import TYPE_CHECKING

from DashAI.back.core.utils import MultilingualString
from DashAI.back.metrics.regression_metric import RegressionMetric, prepare_to_metric

if TYPE_CHECKING:
import numpy as np

from DashAI.back.dataloaders.classes.dashai_dataset import DashAIDataset


class SMAPE(RegressionMetric):
"""Percentage error measured against the size of both values.

Symmetric Mean Absolute Percentage Error divides each error by the average
of the true and predicted values rather than by the true value alone. That
small change fixes the two things that make :class:`MAPE` awkward on real
series: it stays defined when the truth is zero, and it does not punish
over-forecasting more harshly than under-forecasting.

::

sMAPE(y, y') = 100 / N · sum 2 |yi - y'i| / (|yi| + |y'i|)

Range: [0, 200], lower is better. The ceiling is reached when a value and
its forecast have nothing in common, for example predicting a non-zero
number where the truth is zero, which is exactly the case MAPE cannot
score at all.

A row where the true value and the forecast are both zero is counted as no
error rather than as an undefined ratio, since a forecast of nothing that
turned out to be nothing is right.

References
----------
- [1] https://otexts.com/fpp3/accuracy.html
"""

DESCRIPTION = MultilingualString(
en=(
"Percentage error measured against the average of the true and "
"predicted values. Stays defined when the true value is zero and "
"treats over and under forecasting alike, unlike MAPE."
),
es=(
"Error porcentual medido respecto al promedio del valor real y el "
"predicho. Sigue definido cuando el valor real es cero y trata "
"igual la sobreestimacion y la subestimacion, a diferencia de MAPE."
),
pt=(
"Erro percentual medido em relacao a media do valor real e do "
"previsto. Permanece definido quando o valor real e zero e trata "
"igualmente super e subprevisao, ao contrario do MAPE."
),
de=(
"Prozentualer Fehler, gemessen am Mittel aus wahrem und "
"vorhergesagtem Wert. Bleibt definiert, wenn der wahre Wert null "
"ist, und behandelt Ueber- und Unterschaetzung gleich, anders als "
"MAPE."
),
zh=(
"以真实值与预测值的平均数为基准衡量的百分比误差。"
"与 MAPE 不同,它在真实值为零时仍有定义,且对高估和低估一视同仁。"
),
)

@staticmethod
def score(
true_values: "DashAIDataset",
pred_values: "np.ndarray",
) -> float:
"""Calculate the sMAPE between true values and predicted values.

Parameters
----------
true_values : DashAIDataset
A DashAI dataset with true values.
pred_values : np.ndarray
A one-dimensional array with the predicted values for each
instance.

Returns
-------
float
sMAPE as a percentage between 0 and 200.
"""
import numpy as np

true_values, pred_values = prepare_to_metric(true_values, pred_values)

scale = np.abs(true_values) + np.abs(pred_values)
errors = np.abs(true_values - pred_values)

# Both values zero means the forecast was right, so the ratio is 0
# rather than the 0/0 that dividing would produce.
ratios = np.divide(
2 * errors, scale, out=np.zeros_like(scale, dtype=float), where=scale != 0
)
return float(100 * np.mean(ratios))
5 changes: 4 additions & 1 deletion DashAI/back/metrics/regression_metric.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ class RegressionMetric(BaseMetric):
"""

MAXIMIZE: bool = False
COMPATIBLE_COMPONENTS = ["RegressionTask"]
# ForecastingTask measures the same thing: a continuous prediction against
# a continuous truth. Listing it here is what makes MAE, RMSE and the rest
# available to forecasting runs without reimplementing any of them.
COMPATIBLE_COMPONENTS = ["RegressionTask", "ForecastingTask"]


def prepare_to_metric(
Expand Down
Empty file.
Loading
Loading