Time Series Evaluation Metrics
One forecast needs several scores. MAE stays steady, RMSE punishes one big miss, and percentage errors let you compare across scales.
Why Does This Exist?
A forecast that misses by a little every day and one that nails every day but misses a holiday spike by a mile need different responses. The first wastes a little stock daily. The second empties the shelf on the biggest day of the year. A single average hides which failure you have.
Metrics exist to price errors differently. Some treat every unit the same, some charge extra for large misses, and some divide by the actual value so a pen forecast and a car forecast can share one scoreboard. The split you validate with belongs to cross-validation, which keeps every test block after its training rows. The forecaster you score here is often exponential smoothing.
Basics first: every metric below starts from the residual, meaning actual minus predicted at the same time. If residuals are new, think of them as the signed miss per row before any averaging happens.
Think of It Like This
Darts with two scoring rules
You throw four darts. Three land close to the bullseye and one lands far in the wall.
Mean absolute error measures each throw distance and averages them. Every inch counts the same, so three good throws mostly cover one bad throw.
Root mean squared error squares each distance first. The far throw contributes 9 times more than a throw 3 times farther, so one wild throw decides the score.
Percentage errors judge relative to the target size. Missing by one inch on a dinner plate and on a coin are different achievements, which is why business reports prefer percentages when series live on different scales.
Where it stops: dart throws stand alone, while forecast errors arrive in order and correlate. A metric can price the misses but cannot tell you whether the misses cluster after a break.
How It Actually Works
Call the actual at time by , meaning the observed value. Call the prediction , meaning the forecast for that same time. Call the residual , meaning the signed miss. Call the count of scored rows.
- Mean absolute error averages the absolute miss. It grows linearly, so it stays steady when one outlier appears.
- Root mean squared error averages the squared miss and takes the root, so the units stay original while large misses dominate.
- Mean absolute percentage error divides each absolute miss by its actual, then averages as a percent. It allows comparison across scales but breaks near zero.
- Symmetric MAPE divides by the average of actual and predicted magnitudes, which softens the asymmetry when actuals vary widely.
Worked example
Actual values , , and . Predicted values , , and . Residuals are , , and .
Absolute misses are , , and . Their sum is and .
Squared misses are , , and . Their sum is , their mean is , and . The miss of contributes of the squared total, which is why RMSE at sits above MAE at .
Percentage misses are , , and . Their mean is , so percent.
Symmetric denominators are , , and . The ratios are , , and . Their mean is , so percent. That is the diagram: four actual and forecast dots with error bars, and the bar of length highlighted as the RMSE driver.
Code
The point is the gap: same four forecasts, steady MAE beside outlier-driven RMSE, computed with the standard library only.
actual: list[float] = [10.0, 12.0, 11.0, 15.0]pred: list[float] = [9.0, 12.0, 13.0, 12.0]
abs_err: list[float] = [abs(a - p) for a, p in zip(actual, pred)]mae: float = sum(abs_err) / len(abs_err)print(f"{mae:.2f}") # -> 1.50
sq: list[float] = [(a - p) ** 2 for a, p in zip(actual, pred)]rmse: float = (sum(sq) / len(sq)) ** 0.5print(f"{rmse:.2f}") # -> 1.87
mape: float = sum(abs(a - p) / a for a, p in zip(actual, pred)) / len(actual) * 100.0print(f"{mape:.2f}") # -> 12.05
smape: float = sum(abs(a - p) / ((abs(a) + abs(p)) / 2.0) for a, p in zip(actual, pred)) / len(actual) * 100.0print(f"{smape:.2f}") # -> 12.35Four prints, four agreements with the hand steps. Change the last prediction from to and RMSE falls to meet MAE, which confirms the outlier was driving the gap.
Watch Out For
MAPE on rows that can be zero
Intermittent demand holds many true zeros. MAPE divides by , so one zero actual sends that term toward infinity and the average with it.
Symptom: a forecast that looks sane reports MAPE in the thousands while MAE stays small. Fix: never use MAPE where zeros occur. Report MAE alongside RMSE for scale, or use sMAPE with care, and slice errors by nonzero demand separately.
Reading RMSE alone after a spike
One missed promotion dominates the squared total, so RMSE describes that week rather than the typical week.
Symptom: RMSE far above MAE, here against , with most of the squared sum in one term. Fix: always report both. When they disagree, inspect the largest residuals first, and validate the split itself with cross-validation before tuning the model.
The Quick Version
- MAE averages absolute misses and treats every unit the same, here 1.50 on four forecasts.
- RMSE squares misses first, so one miss of 3 lifts it to 1.87 on the same rows.
- MAPE divides by actuals for scale-free comparison, here 12.05 percent, but breaks on zero actuals.
- sMAPE softens asymmetry with averaged denominators, here 12.35 percent.
- Report MAE with RMSE together. A gap between them points at a few large errors, not steady bias.
- Honest splits come first, so score these metrics only on test blocks after their training rows.