- Decompose a series into trend, seasonality and remainder, and diagnose stationarity and autocorrelation from plots and the ACF.
- Implement naive, seasonal naive and simple exponential smoothing forecasts and explain ARIMA(p,d,q)(P,D,Q)m notation.
- Build a gradient-boosting forecaster with lag, rolling and calendar features without leaking future information.
- Evaluate models with rolling-origin backtesting using MAE, MASE and sMAPE.
- Construct and check prediction intervals, and choose between classical, machine-learning and pretrained forecasting models.
Before this: Linear and Logistic Regression, Decision Trees and Ensembles, Model Evaluation and Metrics, Feature Engineering and Preprocessing Pipelines
Forecasting is the job of saying what a quantity will be in the future and how sure you are. Retailers forecast demand per product and store to set inventory, grid operators forecast load every few minutes, cloud teams forecast capacity, and finance teams forecast cash. Forecasting is different from ordinary supervised learning in one way that matters a lot: the rows are ordered in time, so the past can be used to predict the future but never the other way round. Most forecasting failures in industry come from ignoring that fact.
Intuition: weather, habits and luck
Think of a coffee shop's daily sales. There is a slow drift up as the neighbourhood grows (trend). There is a weekly rhythm, busy on weekdays and quiet on Sunday (seasonality). And there is day-to-day noise that nobody could have predicted: a rainstorm, a bus strike (noise or the irregular component). A good forecaster separates these three. The trend and seasonality can be projected forward; the noise cannot, but its size tells you how wide your uncertainty band should be.
Components and decomposition
A time series is a sequence observed at regular intervals. A classical additive decomposition writes
where is the trend-cycle, the seasonal component with period (for example for monthly data with a yearly pattern, for daily data with a weekly pattern), and the remainder. When seasonal swings grow with the level of the series, a multiplicative decomposition fits better; taking logs turns it into an additive one. Robust tools such as STL (Seasonal and Trend decomposition using Loess) estimate these components and are the standard first look at a new series.
Stationarity and autocorrelation
A series is (weakly) stationary if its mean, variance and autocovariance do not change over time. Many classical models assume stationarity, and trends and seasonality break it. The usual fix is differencing: the first difference removes a linear trend, and the seasonal difference removes a stable seasonal pattern. Unit-root tests (ADF, KPSS) help decide how much differencing is needed, but plots usually tell you first.
Autocorrelation measures how a series relates to its own past. The sample autocorrelation at lag is
where is the sample mean. Plotting against gives the ACF. A slowly decaying ACF signals a trend; spikes at signal seasonality. The partial autocorrelation function (PACF) measures the correlation at lag after removing the effect of shorter lags, and together the ACF and PACF guide the choice of ARIMA orders. For a white-noise series, about 95% of values should fall inside .
Baselines first
Every forecasting project starts with baselines, because they are surprisingly hard to beat and they give your metrics a meaning.
| Method | Forecast for horizon | When it is strong |
|---|---|---|
| Mean | Stationary noise around a level | |
| Naive (random walk) | Financial prices, slowly drifting series | |
| Seasonal naive | , with | Strong, stable seasonality |
| Drift | Steady linear trend |
Seasonal naive simply repeats the last observed season: next Monday equals last Monday. If a sophisticated model cannot beat it in a fair backtest, ship the baseline.
Exponential smoothing
Simple exponential smoothing (SES) forecasts with a weighted average of past observations whose weights decay geometrically. With smoothing parameter and level :
A large reacts quickly to new data; a small smooths heavily. Holt's method adds a trend term , and Holt–Winters adds a seasonal term . The modern formulation wraps these in state-space ETS models (Error, Trend, Seasonal), which give likelihoods, automatic model selection by AIC, and proper prediction intervals.
Worked example: SES by hand
Take , , and initialise .
So the forecast for every future step is . SES produces flat forecasts; that is why Holt and Holt–Winters exist.
ARIMA in one page of notation
ARIMA combines three ideas. An autoregressive term of order regresses on its own past values; a moving-average term of order regresses on past forecast errors ; and integration of order means the model is fitted to the -times differenced series. Using the backshift operator , an ARIMA(, , ) model is
where are AR coefficients, are MA coefficients, is a constant and is white noise. Seasonal ARIMA, written ARIMA, adds the same three pieces at seasonal lags . Two famous special cases: ARIMA(0,1,0) is the random walk (the naive forecast), and ARIMA(0,1,1) is equivalent to simple exponential smoothing. In practice, automatic order search (for example auto.arima in R or AutoARIMA in the Python statsforecast package) picks orders by information criteria. ARIMA shines on single, well-behaved series with a few hundred points; it scales poorly to millions of series and ignores external drivers unless you add them as regressors (ARIMAX).
Machine learning: gradient boosting with lag features
When you have many related series (thousands of products) and external drivers (price, promotions, holidays, weather), global machine-learning models trained across all series usually win. You turn forecasting into tabular regression by building features from the past:
- Lags: .
- Rolling statistics: mean and standard deviation over the last 7 or 28 steps, computed only from data before .
- Calendar features: day of week, month, holiday flags.
- Known-future covariates: planned price or promotion.
- Series identifiers or static attributes (store, category).
Gradient-boosted trees (LightGBM, XGBoost, scikit-learn's HistGradientBoostingRegressor) dominated the top of the M5 retail forecasting competition this way. Two design decisions matter. First, recursive forecasting predicts one step, feeds the prediction back as a lag, and repeats, which compounds errors; direct forecasting trains a separate model (or a horizon feature) for each step ahead. Second, trees cannot extrapolate beyond the target values seen in training, so a trending series should be modelled as differences or ratios to a recent level.
import numpy as np
from sklearn.ensemble import HistGradientBoostingRegressor
rng = np.random.default_rng(0)
n, m = 156, 12
t = np.arange(n)
y = 100 + 0.5 * t + 10 * np.sin(2 * np.pi * t / m) + rng.normal(0, 3, n)
LAGS = [1, 2, 3, 12]
def make_xy(series, start):
X, Y = [], []
for i in range(start, len(series)):
X.append([series[i - L] for L in LAGS] + [i % m]) # lags + month-of-year
Y.append(series[i])
return np.array(X), np.array(Y)
d = np.diff(y) # model first differences: trees cannot extrapolate a trend
split = n - 12 # hold out the last 12 points of y
X, Y = make_xy(d[:split - 1], max(LAGS))
model = HistGradientBoostingRegressor(max_iter=300, learning_rate=0.05).fit(X, Y)
hist, level, preds = list(d[:split - 1]), y[split - 1], []
for k in range(12): # recursive forecast: feed predictions back as lags
i = len(hist)
step = model.predict([[hist[i - L] for L in LAGS] + [i % m]])[0]
hist.append(step); level += step; preds.append(level)
print("GBM MAE:", np.mean(np.abs(y[split:] - np.array(preds))).round(2))
print("Seasonal naive MAE:", np.mean(np.abs(y[split:] - y[split - 12:split])).round(2))
On this single synthetic series the boosted model (MAE about 3.6) does not beat seasonal naive (about 3.5). That is a realistic result, not a bug: a pure sine wave plus noise is exactly what seasonal naive is built for, and one series of 156 points is too little data for trees to shine. Gradient boosting earns its keep across many series with covariates.
Deep learning and pretrained time-series models
Neural forecasters learn across many series at once. Notable designs include DeepAR (Salinas et al., Amazon), an autoregressive recurrent network that outputs a probability distribution at each step; N-BEATS, a deep stack of fully connected blocks; the Temporal Fusion Transformer, which handles static, known-future and observed covariates with attention; and PatchTST, which splits a series into patches that act like tokens in a Transformer.
Since 2023 a new family has appeared: pretrained time-series foundation models, trained on very large collections of real and synthetic series and used zero-shot on new series, much as a language model answers questions it was not fine-tuned for. Examples with published papers include Amazon's Chronos (Ansari et al., 2024), which scales and quantises values into a vocabulary of tokens and trains a T5-style language model on them; Google's TimesFM (Das et al., ICML 2024), a decoder-only Transformer over patches; and Salesforce's Moirai (Woo et al., 2024). These models give strong baselines with no training, which is valuable for cold-start series and quick prototypes. They do not make backtesting optional: on a series with strong domain-specific drivers (promotions, outages), a tuned global gradient-boosting model with covariates is often still better. Treat a foundation model as one more candidate in your backtest. Model families and versions change fast; check the current model cards before you adopt one.
Backtesting with rolling-origin evaluation
A random train/test split leaks the future into training. The correct procedure is rolling-origin evaluation (also called time-series cross-validation or backtesting): choose several forecast origins ; for each origin, train only on data up to , forecast steps ahead, and score against the actuals. The training window can expand (keep all history) or slide (fixed length, useful when old data is no longer relevant). scikit-learn's TimeSeriesSplit implements the expanding version. Use the same horizon your business needs: if purchasing decisions lock in two weeks ahead, the one-day-ahead error is irrelevant. Leave a gap between training data and the forecast window when features are not available in real time.
Metrics: MAE, MASE, sMAPE
Let be actuals and forecasts over a test window of points.
- MAE , in the units of the data. Optimising MAE targets the median.
- RMSE , which targets the mean and punishes large errors.
- sMAPE , a percentage error that is bounded but unstable near zero and asymmetric despite its name.
- MASE (Hyndman & Koehler, 2006) scales the MAE by the in-sample MAE of the seasonal naive method on the training data of length :
MASE below 1 means you beat the in-sample seasonal naive forecast. It is scale-free, so you can average it across thousands of series, and it is well defined when values are zero.
Worked example: computing the metrics
Non-seasonal training data (): $20, 22, 21, 25, 2426, 2725, 25$.
- Absolute errors: , , so MAE .
- MASE scale: one-step naive in-sample errors are , , , ; mean . MASE .
- sMAPE: and ; mean , so sMAPE .
Prediction intervals
A point forecast without uncertainty is hard to act on: a warehouse needs the 95th percentile of demand, not the mean. For the naive method with one-step residual standard deviation , the -step forecast error variance grows as , so an approximate 95% interval is
With , the half-width is at and at . Intervals widen with horizon; a model whose intervals do not is suspicious. Other routes to intervals: ETS and ARIMA give analytic intervals under Gaussian errors; quantile regression trains models on the pinball loss for chosen quantiles (gradient boosting supports loss="quantile"); probabilistic deep models output distributions; and conformal prediction calibrates intervals from backtest residuals with coverage guarantees under exchangeability (which time series only approximately satisfy). Always check coverage in the backtest: a nominal 90% interval should contain about 90% of actuals.
import numpy as np
def mae(a, f): return np.mean(np.abs(a - f))
def smape(a, f): return 100 * np.mean(2 * np.abs(a - f) / (np.abs(a) + np.abs(f)))
def mase(a, f, train, m):
return mae(a, f) / np.mean(np.abs(train[m:] - train[:-m]))
def seasonal_naive(train, h, m):
return np.array([train[-m:][i % m] for i in range(h)])
rng = np.random.default_rng(0)
n, m, h = 156, 12, 12
t = np.arange(n)
y = 100 + 0.5 * t + 10 * np.sin(2 * np.pi * t / m) + rng.normal(0, 3, n)
scores = []
for origin in range(n - 4 * h, n, h): # 4 rolling origins, expanding window
train, test = y[:origin], y[origin:origin + h]
f = seasonal_naive(train, h, m)
scores.append((mae(test, f), smape(test, f), mase(test, f, train, m)))
print("MAE=%.2f sMAPE=%.2f%% MASE=%.2f" % tuple(np.mean(scores, axis=0)))
This backtest prints roughly MAE 6.0, sMAPE 3.7% and MASE 0.95 for seasonal naive, and the plain naive method scores MASE about 1.16 on the same origins. Any new model has to beat those numbers on the same origins to be worth deploying.
Common pitfalls
- Leakage through features. A rolling mean that includes the current value, or a feature computed on the whole dataset (scaling, target encoding) before splitting, quietly uses the future.
- Random cross-validation. Shuffled K-fold on time series gives optimistic scores. Use rolling origins.
- No baseline. Without seasonal naive in the table, a MAPE of 12% means nothing.
- MAPE on intermittent demand. Percentage errors explode or are undefined when actuals are zero; use MASE or a scaled error.
- Ignoring the horizon. Reporting one-step-ahead accuracy for a model used four weeks ahead.
- Trees and trend. Raw-level gradient boosting flattens out at the highest value seen in training.
- Overconfident intervals. Residual-based intervals ignore parameter and model uncertainty; check empirical coverage.
- Structural breaks. COVID-era data, a price change or a new competitor can make history misleading; flag or down-weight those periods.
In practice
Start with plots, a decomposition and the ACF. Build seasonal naive, ETS and (for few series) ARIMA baselines with a library such as statsforecast or R's fable. For many series with covariates, build a global gradient-boosting model with lag, rolling and calendar features using a framework that handles the bookkeeping safely (for example mlforecast, skforecast or darts). Add a pretrained foundation model as a zero-shot candidate. Compare everything in one rolling-origin backtest at the business horizon, with MASE for accuracy and coverage for intervals, and pick per segment if needed. In production, forecast quantiles rather than points, retrain on a schedule, monitor error drift against the baseline, and keep a human override for known events the model cannot see. Simple ensembles (the average of a few good models) are a reliable last step.
Key takeaways
- Trend, seasonality and noise are the three things to separate; only the first two can be projected forward, and the third sets your uncertainty.
- Seasonal naive is the baseline every model must beat in a fair backtest; it often wins on clean single series.
- Never shuffle time series: evaluate with rolling-origin backtests at the horizon the business actually uses.
- MASE is scale-free and defined at zero, so it is the safest metric for comparing across many series.
- Global gradient-boosting models with lag and calendar features dominate many-series problems with covariates; pretrained time-series models are strong zero-shot candidates but still need backtesting.
- Forecast distributions or quantiles, not just points, and verify interval coverage empirically.
Terms in this lesson
- Seasonality
- A repeating pattern with a fixed, known period m, such as weekly or yearly cycles.
- Stationarity
- The property that a series' mean, variance and autocovariance do not change over time.
- Autocorrelation function (ACF)
- The correlation between a series and its own values k steps earlier, plotted against lag k.
- Seasonal naive forecast
- A forecast that repeats the value from the same point in the last observed season.
- Exponential smoothing
- A family of forecasting methods that weight past observations with geometrically decaying weights, optionally with trend and seasonal states.
- ARIMA
- A model combining autoregression on past values, differencing to remove non-stationarity and a moving average of past errors.
- Rolling-origin evaluation
- Backtesting by repeatedly training on data up to an origin, forecasting the next h steps and scoring, for several origins.
- MASE
- Mean absolute scaled error: test MAE divided by the in-sample MAE of the seasonal naive method.
- Prediction interval
- A range expected to contain a future observation with a stated probability.
- Time-series foundation model
- A model pretrained on large collections of series that can forecast new series zero-shot.
Check your understanding
- 1.
Why is shuffled K-fold cross-validation inappropriate for evaluating a forecasting model?
- 2.
A model achieves MASE = 1.3 on a seasonal series with m = 12. What does this tell you?
- 3.
Using simple exponential smoothing with alpha = 0.5 and current level 20, a new observation of 30 arrives. What is the next forecast?
- 4.
You must forecast weekly demand for 40,000 product-store pairs with price and promotion plans known in advance. Which approach is the best starting candidate?
- 5.
A naive forecast has one-step residual standard deviation 4. What is the approximate half-width of the 95% prediction interval at horizon h = 9?
Exercises
Work each problem before opening the solution.
- Exercise 1Core
Quarterly training data with m = 4 (yearly seasonality):
10, 20, 30, 40, 12, 22, 33, 41. Test actuals for the next four quarters:14, 23, 35, 44.- Give the seasonal naive forecast for the test period.
- Compute MAE.
- Compute MASE using the in-sample seasonal naive scale.
Show solution
- Seasonal naive repeats the last season: 12, 22, 33, 41.
- Errors: |14-12| = 2, |23-22| = 1, |35-33| = 2, |44-41| = 3. MAE = 8/4 = 2.0.
- In-sample seasonal differences (t = 5..8): |12-10| = 2, |22-20| = 2, |33-30| = 3, |41-40| = 1. Mean = 8/4 = 2.0. MASE = 2.0/2.0 = 1.0: the forecast is exactly as good on the test set as seasonal naive was in-sample.
- Exercise 2Core
A colleague builds a feature
rolling_mean_7 = df['sales'].rolling(7).mean()and uses it to predictsaleson the same row with gradient boosting. Their backtest MAE is excellent. What is wrong, and how do you fix it in pandas?Show solution
rolling(7).mean()at row t includessales[t]itself, so the model sees the target. That is leakage, and the excellent backtest will not hold in production. Shift before rolling so that only past values are used:df['rolling_mean_7'] = df['sales'].shift(1).rolling(7).mean()For a multi-step horizon h with direct forecasting, shift by h (or by the feature's real availability delay) so the feature is truly known when the forecast is made. With many series, do the shift within each group:
df.groupby('id')['sales'].transform(lambda s: s.shift(1).rolling(7).mean()). - Exercise 3Core
Write the ARIMA(1,1,0) model in backshift notation and expand it into an equation for in terms of past values. Then forecast given , , , .
Show solution
Backshift form: .
Let . Then , so
Forecast (set ): . Equivalently, the last difference was 5, the predicted next difference is , so .
- Exercise 4Challenge
Implement a rolling-origin backtest that compares seasonal naive with simple exponential smoothing (choose alpha from {0.1, 0.3, 0.5, 0.8} by minimising one-step in-sample squared error on each training window) for the synthetic series used in the lesson. Report mean MASE for each method over 4 origins with horizon 12. Explain the result.
Show solution
import numpy as np rng = np.random.default_rng(0) n, m, h = 156, 12, 12 t = np.arange(n) y = 100 + 0.5 * t + 10 * np.sin(2 * np.pi * t / m) + rng.normal(0, 3, n) def mase(a, f, train): return np.mean(np.abs(a - f)) / np.mean(np.abs(train[m:] - train[:-m])) def ses_fit(train): best = None for a in (0.1, 0.3, 0.5, 0.8): level, sse = train[0], 0.0 for v in train[1:]: sse += (v - level) ** 2 # one-step forecast error level = a * v + (1 - a) * level if best is None or sse < best[0]: best = (sse, a, level) return best[2] # final level = flat forecast res = {'snaive': [], 'ses': []} for origin in range(n - 4 * h, n, h): train, test = y[:origin], y[origin:origin + h] res['snaive'].append(mase(test, np.array([train[-m:][i % m] for i in range(h)]), train)) res['ses'].append(mase(test, np.repeat(ses_fit(train), h), train)) for k, v in res.items(): print(k, round(float(np.mean(v)), 2))Seasonal naive scores MASE of about 0.95. SES scores well above 1, because it produces a flat line and cannot represent the strong 12-month cycle or the trend. The amplitude-10 seasonal swing dominates its errors. The lesson: pick a model whose structure matches the components you saw in the decomposition. Holt-Winters (additive trend and season) would be the appropriate exponential-smoothing model here.
- Exercise 5Challenge
Your 90% prediction intervals from a gradient-boosting quantile model cover only 71% of actuals across a 26-origin backtest. List three plausible causes and a concrete remedy for each.
Show solution
- Quantile models fit on in-sample residuals are overconfident. Trees fit training data closely, so the 5% and 95% quantile models are too narrow. Remedy: calibrate with conformal prediction. Compute absolute errors or quantile-score residuals on held-out backtest windows and widen the intervals by the empirical 90th percentile of those residuals for each horizon.
- Uncertainty does not grow with horizon. With recursive forecasting the quantile models were trained for one step, but errors compound. Remedy: train direct models per horizon (or include horizon as a feature) and evaluate coverage per horizon.
- Distribution shift or structural breaks in the backtest period (promotions, a new competitor) that training data did not contain. Remedy: add the missing covariates, use a sliding window that down-weights stale history, and monitor coverage over time with an alert when rolling coverage drops below target. Also check that the quantile loss was set correctly (for example
loss='quantile', quantile=0.05and0.95for a central 90% interval, not 0.10 and 0.90).
Further reading
- bookRob J. Hyndman and George Athanasopoulos, Forecasting: Principles and Practice, 3rd ed. (2021)
- paperRob J. Hyndman and Anne B. Koehler, Another look at measures of forecast accuracy (2006)
- paperDavid Salinas, Valentin Flunkert, Jan Gasthaus, Tim Januschowski, DeepAR: Probabilistic Forecasting with Autoregressive Recurrent Networks (2017)
- paperAbdul Fatir Ansari et al., Chronos: Learning the Language of Time Series (2024)
- paperAbhimanyu Das, Weihao Kong, Rajat Sen, Yichen Zhou, A decoder-only foundation model for time-series forecasting (2023)
- docsscikit-learn documentation, TimeSeriesSplit
Go further
Sources
Fact-checked and last reviewed 2026-11-05