Skip to content

Instantly share code, notes, and snippets.

@bshishov
Last active October 15, 2024 23:41
Show Gist options
  • Save bshishov/5dc237f59f019b26145648e2124ca1c9 to your computer and use it in GitHub Desktop.
Save bshishov/5dc237f59f019b26145648e2124ca1c9 to your computer and use it in GitHub Desktop.
Python Numpy functions for most common forecasting metrics
import numpy as np
EPSILON = 1e-10
def _error(actual: np.ndarray, predicted: np.ndarray):
""" Simple error """
return actual - predicted
def _percentage_error(actual: np.ndarray, predicted: np.ndarray):
"""
Percentage error
Note: result is NOT multiplied by 100
"""
return _error(actual, predicted) / (actual + EPSILON)
def _naive_forecasting(actual: np.ndarray, seasonality: int = 1):
""" Naive forecasting method which just repeats previous samples """
return actual[:-seasonality]
def _relative_error(actual: np.ndarray, predicted: np.ndarray, benchmark: np.ndarray = None):
""" Relative Error """
if benchmark is None or isinstance(benchmark, int):
# If no benchmark prediction provided - use naive forecasting
if not isinstance(benchmark, int):
seasonality = 1
else:
seasonality = benchmark
return _error(actual[seasonality:], predicted[seasonality:]) /\
(_error(actual[seasonality:], _naive_forecasting(actual, seasonality)) + EPSILON)
return _error(actual, predicted) / (_error(actual, benchmark) + EPSILON)
def _bounded_relative_error(actual: np.ndarray, predicted: np.ndarray, benchmark: np.ndarray = None):
""" Bounded Relative Error """
if benchmark is None or isinstance(benchmark, int):
# If no benchmark prediction provided - use naive forecasting
if not isinstance(benchmark, int):
seasonality = 1
else:
seasonality = benchmark
abs_err = np.abs(_error(actual[seasonality:], predicted[seasonality:]))
abs_err_bench = np.abs(_error(actual[seasonality:], _naive_forecasting(actual, seasonality)))
else:
abs_err = np.abs(_error(actual, predicted))
abs_err_bench = np.abs(_error(actual, benchmark))
return abs_err / (abs_err + abs_err_bench + EPSILON)
def _geometric_mean(a, axis=0, dtype=None):
""" Geometric mean """
if not isinstance(a, np.ndarray): # if not an ndarray object attempt to convert it
log_a = np.log(np.array(a, dtype=dtype))
elif dtype: # Must change the default dtype allowing array type
if isinstance(a, np.ma.MaskedArray):
log_a = np.log(np.ma.asarray(a, dtype=dtype))
else:
log_a = np.log(np.asarray(a, dtype=dtype))
else:
log_a = np.log(a)
return np.exp(log_a.mean(axis=axis))
def mse(actual: np.ndarray, predicted: np.ndarray):
""" Mean Squared Error """
return np.mean(np.square(_error(actual, predicted)))
def rmse(actual: np.ndarray, predicted: np.ndarray):
""" Root Mean Squared Error """
return np.sqrt(mse(actual, predicted))
def nrmse(actual: np.ndarray, predicted: np.ndarray):
""" Normalized Root Mean Squared Error """
return rmse(actual, predicted) / (actual.max() - actual.min())
def me(actual: np.ndarray, predicted: np.ndarray):
""" Mean Error """
return np.mean(_error(actual, predicted))
def mae(actual: np.ndarray, predicted: np.ndarray):
""" Mean Absolute Error """
return np.mean(np.abs(_error(actual, predicted)))
def mad(actual: np.ndarray, predicted: np.ndarray):
""" Mean Absolute Deviation """
error = _error(actual, predicted)
return np.mean(np.abs(error - np.mean(error)))
def gmae(actual: np.ndarray, predicted: np.ndarray):
""" Geometric Mean Absolute Error """
return _geometric_mean(np.abs(_error(actual, predicted)))
def mdae(actual: np.ndarray, predicted: np.ndarray):
""" Median Absolute Error """
return np.median(np.abs(_error(actual, predicted)))
def mpe(actual: np.ndarray, predicted: np.ndarray):
""" Mean Percentage Error """
return np.mean(_percentage_error(actual, predicted))
def mape(actual: np.ndarray, predicted: np.ndarray):
"""
Mean Absolute Percentage Error
Properties:
+ Easy to interpret
+ Scale independent
- Biased, not symmetric
- Undefined when actual[t] == 0
Note: result is NOT multiplied by 100
"""
return np.mean(np.abs(_percentage_error(actual, predicted)))
def mdape(actual: np.ndarray, predicted: np.ndarray):
"""
Median Absolute Percentage Error
Note: result is NOT multiplied by 100
"""
return np.median(np.abs(_percentage_error(actual, predicted)))
def smape(actual: np.ndarray, predicted: np.ndarray):
"""
Symmetric Mean Absolute Percentage Error
Note: result is NOT multiplied by 100
"""
return np.mean(2.0 * np.abs(actual - predicted) / ((np.abs(actual) + np.abs(predicted)) + EPSILON))
def smdape(actual: np.ndarray, predicted: np.ndarray):
"""
Symmetric Median Absolute Percentage Error
Note: result is NOT multiplied by 100
"""
return np.median(2.0 * np.abs(actual - predicted) / ((np.abs(actual) + np.abs(predicted)) + EPSILON))
def maape(actual: np.ndarray, predicted: np.ndarray):
"""
Mean Arctangent Absolute Percentage Error
Note: result is NOT multiplied by 100
"""
return np.mean(np.arctan(np.abs((actual - predicted) / (actual + EPSILON))))
def mase(actual: np.ndarray, predicted: np.ndarray, seasonality: int = 1):
"""
Mean Absolute Scaled Error
Baseline (benchmark) is computed with naive forecasting (shifted by @seasonality)
"""
return mae(actual, predicted) / mae(actual[seasonality:], _naive_forecasting(actual, seasonality))
def std_ae(actual: np.ndarray, predicted: np.ndarray):
""" Normalized Absolute Error """
__mae = mae(actual, predicted)
return np.sqrt(np.sum(np.square(_error(actual, predicted) - __mae))/(len(actual) - 1))
def std_ape(actual: np.ndarray, predicted: np.ndarray):
""" Normalized Absolute Percentage Error """
__mape = mape(actual, predicted)
return np.sqrt(np.sum(np.square(_percentage_error(actual, predicted) - __mape))/(len(actual) - 1))
def rmspe(actual: np.ndarray, predicted: np.ndarray):
"""
Root Mean Squared Percentage Error
Note: result is NOT multiplied by 100
"""
return np.sqrt(np.mean(np.square(_percentage_error(actual, predicted))))
def rmdspe(actual: np.ndarray, predicted: np.ndarray):
"""
Root Median Squared Percentage Error
Note: result is NOT multiplied by 100
"""
return np.sqrt(np.median(np.square(_percentage_error(actual, predicted))))
def rmsse(actual: np.ndarray, predicted: np.ndarray, seasonality: int = 1):
""" Root Mean Squared Scaled Error """
q = np.abs(_error(actual, predicted)) / mae(actual[seasonality:], _naive_forecasting(actual, seasonality))
return np.sqrt(np.mean(np.square(q)))
def inrse(actual: np.ndarray, predicted: np.ndarray):
""" Integral Normalized Root Squared Error """
return np.sqrt(np.sum(np.square(_error(actual, predicted))) / np.sum(np.square(actual - np.mean(actual))))
def rrse(actual: np.ndarray, predicted: np.ndarray):
""" Root Relative Squared Error """
return np.sqrt(np.sum(np.square(actual - predicted)) / np.sum(np.square(actual - np.mean(actual))))
def mre(actual: np.ndarray, predicted: np.ndarray, benchmark: np.ndarray = None):
""" Mean Relative Error """
return np.mean(_relative_error(actual, predicted, benchmark))
def rae(actual: np.ndarray, predicted: np.ndarray):
""" Relative Absolute Error (aka Approximation Error) """
return np.sum(np.abs(actual - predicted)) / (np.sum(np.abs(actual - np.mean(actual))) + EPSILON)
def mrae(actual: np.ndarray, predicted: np.ndarray, benchmark: np.ndarray = None):
""" Mean Relative Absolute Error """
return np.mean(np.abs(_relative_error(actual, predicted, benchmark)))
def mdrae(actual: np.ndarray, predicted: np.ndarray, benchmark: np.ndarray = None):
""" Median Relative Absolute Error """
return np.median(np.abs(_relative_error(actual, predicted, benchmark)))
def gmrae(actual: np.ndarray, predicted: np.ndarray, benchmark: np.ndarray = None):
""" Geometric Mean Relative Absolute Error """
return _geometric_mean(np.abs(_relative_error(actual, predicted, benchmark)))
def mbrae(actual: np.ndarray, predicted: np.ndarray, benchmark: np.ndarray = None):
""" Mean Bounded Relative Absolute Error """
return np.mean(_bounded_relative_error(actual, predicted, benchmark))
def umbrae(actual: np.ndarray, predicted: np.ndarray, benchmark: np.ndarray = None):
""" Unscaled Mean Bounded Relative Absolute Error """
__mbrae = mbrae(actual, predicted, benchmark)
return __mbrae / (1 - __mbrae)
def mda(actual: np.ndarray, predicted: np.ndarray):
""" Mean Directional Accuracy """
return np.mean((np.sign(actual[1:] - actual[:-1]) == np.sign(predicted[1:] - actual[:-1])).astype(int))
METRICS = {
'mse': mse,
'rmse': rmse,
'nrmse': nrmse,
'me': me,
'mae': mae,
'mad': mad,
'gmae': gmae,
'mdae': mdae,
'mpe': mpe,
'mape': mape,
'mdape': mdape,
'smape': smape,
'smdape': smdape,
'maape': maape,
'mase': mase,
'std_ae': std_ae,
'std_ape': std_ape,
'rmspe': rmspe,
'rmdspe': rmdspe,
'rmsse': rmsse,
'inrse': inrse,
'rrse': rrse,
'mre': mre,
'rae': rae,
'mrae': mrae,
'mdrae': mdrae,
'gmrae': gmrae,
'mbrae': mbrae,
'umbrae': umbrae,
'mda': mda,
}
def evaluate(actual: np.ndarray, predicted: np.ndarray, metrics=('mae', 'mse', 'smape', 'umbrae')):
results = {}
for name in metrics:
try:
results[name] = METRICS[name](actual, predicted)
except Exception as err:
results[name] = np.nan
print('Unable to compute metric {0}: {1}'.format(name, err))
return results
def evaluate_all(actual: np.ndarray, predicted: np.ndarray):
return evaluate(actual, predicted, metrics=set(METRICS.keys()))
@SJ-CAI
Copy link

SJ-CAI commented Mar 1, 2023

thanks, this helps a lot!

@marvinfriede
Copy link

Hi! I was wondering about the equality of MAE and MAD.
The MAE is defined as

$$\text{MAE}=\dfrac{1}{n} \sum_{i=1}^{n} |\text{actual}_i - \text{predict}_i| = \dfrac{1}{n} \sum_{i=1}^{n} |\text{error}_i|$$

The MAD, however, is defined as the mean absolute difference around a central/reference point $m(\text{E})$

$$\text{MAD}=\dfrac{1}{n} \sum_{i=1}^{n} |\text{error}_i - m(\text{E})|$$

This reference point is the mean (or the median) of the error. Only if we explicitly set the reference point to zero, MAE = MAD (as also explained in this paper). Hence, I think the general implementation of the MAD should look like:

def mad(actual: np.ndarray, predicted: np.ndarray):
    """ Mean Absolute Deviation """
    error = _error(actual, predicted)
    return np.mean(np.abs(error - np.mean(error)))

pandas has a mad() function, which also follows this definition (see here).

@tejaskhare99
Copy link

tejaskhare99 commented Aug 29, 2023

Hello, thanks for this. This saved a great amount of time.
However, I believe the mda (Mean Directional Accuracy) Function is wrong.

Instead of comparing the signs of the predicted values with the signs of the actual values, you are comparing the signs of the predicted values with the signs of the differences between the actual values.

Currently -

def mda(actual: np.ndarray, predicted: np.ndarray):
    """ Mean Directional Accuracy """
    return np.mean((np.sign(actual[1:] - actual[:-1]) == np.sign(predicted[1:] - actual[:-1])).astype(int))

It should be -

def mda(actual: np.ndarray, predicted: np.ndarray):
    """ Mean Directional Accuracy """
    return np.mean((np.sign(actual[1:] - actual[:-1]) == np.sign(predicted[1:] - predicted[:-1])).astype(int))

@bshishov
Copy link
Author

bshishov commented Sep 2, 2023

Hi! I was wondering about the equality of MAE and MAD. The MAE is defined as

The MAD, however, is defined as the mean absolute difference around a central/reference point m(E)

This reference point is the mean (or the median) of the error. Only if we explicitly set the reference point to zero, MAE = MAD (as also explained in this paper). Hence, I think the general implementation of the MAD should look like:

def mad(actual: np.ndarray, predicted: np.ndarray):
    """ Mean Absolute Deviation """
    error = _error(actual, predicted)
    return np.mean(np.abs(error - np.mean(error)))

pandas has a mad() function, which also follows this definition (see here).

@marvinfriede, Thank you for the contribution. I've updated the gist.

@bshishov
Copy link
Author

bshishov commented Sep 2, 2023

Hello, thanks for this. This saved a great amount of time. However, I believe the mda (Mean Directional Accuracy) Function is wrong.

Instead of comparing the signs of the predicted values with the signs of the actual values, you are comparing the signs of the predicted values with the signs of the differences between the actual values.

Currently -

def mda(actual: np.ndarray, predicted: np.ndarray):
    """ Mean Directional Accuracy """
    return np.mean((np.sign(actual[1:] - actual[:-1]) == np.sign(predicted[1:] - actual[:-1])).astype(int))

It should be -

def mda(actual: np.ndarray, predicted: np.ndarray):
    """ Mean Directional Accuracy """
    return np.mean((np.sign(actual[1:] - actual[:-1]) == np.sign(predicted[1:] - predicted[:-1])).astype(int))

According to the references:

  • Blaskowitz and Herwartz (2009, 2011) On economic evaluation of directional forecasts see section 2
  • Bergmeir, C., Costantini, M., & Benítez, J. M. (2014). On the usefulness of cross-validation for directional forecast evaluation. Computational Statistics & Data Analysis, 76, 132-143. see p.7 here
  • MDA on wiki

implementation in the gist is correct. Am I missing something? Can you please provide a reference where to prove your point?
Note that there was a confusion previously due to an error in the wiki :) see my comment above

@MildredVL
Copy link

Thanks <3

@fkivela
Copy link

fkivela commented Apr 2, 2024

Thanks for the file, I'm finding it quite useful! I made a fork where I added a minimal pyproject.toml so that I could add this to my project's dependencies and have it be installed automatically with pip. Feel free to copy it to your original version if you find it useful. (I would make a pull request but looks like those aren't supported for gists.) In the meantime, here's my version for anyone else who might wish to install this with pip: https://gist.github.com/fkivela/53434b22458b350fc71b8fc32379d20f

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment