Time series, section 2 of 4, 4h

Forecasting models, from simple to learned

By the end: You can choose between classical and learned forecasting and evaluate over a horizon.

  • Exponential smoothing and ARIMA
  • Tree models with lag features
  • Forecast horizons
  • Prediction intervals

New to time series? Time series data is measurements taken in order over time: daily sales, server latency every minute, a patient's readings across a year. The order is the information. Each value is related to the ones just before it, there are cycles that repeat by weekday and by season, and there is usually a slow trend underneath. Forecasting is predicting what the next values will be. Why it matters

You know how to validate a forecast honestly. This is what to put inside it. The classical methods, exponential smoothing and ARIMA, are fast, interpretable and genuinely hard to beat on many business series, so they are a baseline rather than a formality. The alternative is to reshape the problem into ordinary supervised learning with lag features and fit a tree, which handles many related series and external variables easily. The section also covers the horizon, since accuracy one day ahead says little about thirty, and giving an interval rather than a single number nobody should trust.

What you have                                  Start with
one series, a few hundred points               exponential smoothing or ARIMA
one series, external drivers you know ahead    gradient boosting on lag features
thousands of related series                    one global gradient boosting model
thousands of series, years of history, a team  a deep global model, and prove it
demand that is zero most days                  an intermittent-demand method
a series you cannot explain at all             the seasonal naive baseline, honestly
Worth memorising. The most common mistake is reaching three rows further down than the data justifies.

Classical models#

Exponential smoothing weights recent observations more heavily and handles trend and seasonality. ARIMA models a series through its own past values and past errors. Both are strong, fast and interpretable baselines, and on many business series they are hard to beat.

Simple exponential smoothing is one line, and writing it out shows exactly what the single parameter does. The level is a running blend of what just happened and what you already believed.

import numpy as np

def ses(series, alpha):
    """level_t = alpha * observation + (1 - alpha) * level_{t-1}"""
    level = series[0]
    out = [level]
    for x in series[1:]:
        level = alpha * x + (1 - alpha) * level
        out.append(level)
    return np.array(out)

s = np.array([10.0, 12, 11, 40, 13, 12, 11])     # a series with one spike
print(ses(s, 0.3).round(2))
print(ses(s, 0.8).round(2))

# [10.   10.6  10.72 19.5  17.55 15.89 14.42]   alpha 0.3: slow, ignores the spike
# [10.   11.6  11.12 34.22 17.24 13.05 11.41]   alpha 0.8: fast, chases the spike
Alpha is the whole model: near zero it is a long average that reacts to nothing, near one it is the naive forecast. Fitting it means choosing how much of a single odd day you believe.

The fuller versions add a trend term and a seasonal term with their own smoothing parameters, which is Holt-Winters. ARIMA arrives at similar forecasts from a different direction: differencing the series until it is stationary, then modelling what is left as a combination of its own past values and its own past errors. The practical point is not which is theoretically nicer but that both fit in milliseconds, both produce prediction intervals for free from their error model, and both are hard to beat on a single business series with a few hundred points.

Treat it as supervised learning#

Build lag and rolling features, add calendar features, and fit a gradient-boosted tree. This handles many related series at once and incorporates external variables easily, which is why it is common in practice.

date        sales     ->   lag_1  lag_7  roll_7  dow  holiday  promo   y
2026-03-08   120            118    115    117.4    6     0        0     120
2026-03-09   118            120    119    117.9    0     0        1     118
2026-03-10   131            118    122    118.6    1     0        1     131

One row per date, every feature known before that date. Now it is a table,
and everything you know about tabular modelling applies.
This reshape is the whole trick, and it is also where leakage enters: every column must be computable at forecast time. Write the feature builder once and use it for both training and serving.

Gradient boosting on that table is the honest default for applied work, for four reasons. It takes external variables — price, promotion, weather, a competitor's opening — which classical models handle awkwardly. It fits thousands of series in one model, so a product with six weeks of history borrows the shape of everything similar. It handles non-linear effects and interactions without being told they exist. And it is fast enough to backtest properly, which matters more than any single point of accuracy, because a model you can only evaluate once is a model you cannot trust.

The one thing a tree cannot do

A tree predicts the average of a training leaf, so its output can never leave the range of targets it was trained on. On a series with a trend, that is fatal: asked about next quarter, it returns the highest number it has ever seen, forever. This is the single most important limitation to know about, and it is invisible in any backtest whose test window sits inside the training range.

import numpy as np
from sklearn.ensemble import GradientBoostingRegressor
from sklearn.linear_model import LinearRegression

rng = np.random.default_rng(0)
n = 300
t = np.arange(n).astype(float)
y = 50 + 0.4 * t + 6 * np.sin(2 * np.pi * t / 7) + rng.normal(0, 3, n)

X = np.column_stack([t % 7, np.sin(2 * np.pi * t / 7), np.cos(2 * np.pi * t / 7)])
train, test = slice(0, 250), slice(250, 300)

def rmse(a, b):
    return round(float(np.sqrt(np.mean((a - b) ** 2))), 2)

gbm = GradientBoostingRegressor(random_state=0).fit(X[train], y[train])
print("GBM on the level    ", rmse(y[test], gbm.predict(X[test])))

trend = LinearRegression().fit(t[train].reshape(-1, 1), y[train])
resid = y - trend.predict(t.reshape(-1, 1))
gbm2 = GradientBoostingRegressor(random_state=0).fit(X[train], resid[train])
pred = trend.predict(t[test].reshape(-1, 1)) + gbm2.predict(X[test])
print("GBM on the residual ", rmse(y[test], pred))
print("the trend alone     ", rmse(y[test], trend.predict(t[test].reshape(-1, 1))))

# GBM on the level     59.81
# GBM on the residual   3.11
# the trend alone       4.93
Scikit-learn, because the point is about how trees behave. Same model, same features, twenty times the error — the only difference is whether it was asked to produce a number outside the range it had seen. The fix is to remove the trend first, or to model the difference rather than the level, and then the tree does what trees are good at.

When a deep model earns its place#

Not on one series with three years of monthly data. The forecasting competitions that made global deep models credible were run on tens of thousands of related series with years of history each, and that is the setting where the extra capacity has something to learn from. Below roughly that scale, a deep model is a slower, less interpretable way of arriving at a worse number.

  • Many related series, so the model can learn shape from thousands of examples rather than one. This is the main condition, and it is the one people skip.
  • Inputs that do not fit in a table: raw text about a product, an image of a store shelf, a long history where the relevant pattern is a hundred steps back.
  • A need for the whole horizon at once. Sequence models output all thirty days in one pass, which avoids the compounding errors of feeding forecasts back in.
  • Enough traffic that a point of accuracy is worth a retraining pipeline, a GPU and someone who understands it at three in the morning.

The bar to clear is a tuned gradient boosting model on the same features, backtested over the same rolling origins, at the same horizons. That comparison is easy to arrange and frequently not arranged, which is why published deep forecasting results and applied experience disagree as often as they do.

Horizon matters#

Forecasting tomorrow is easier than forecasting next month, and errors grow with the horizon. Evaluate at every horizon you will actually use rather than reporting a single averaged number that hides how quickly accuracy decays.

There are two ways to get a multi-step forecast and the choice has consequences. The recursive way forecasts one step, appends the prediction, and forecasts again; it needs one model and it compounds its own errors, so by step thirty it may be forecasting from a history it invented. The direct way trains a separate model per horizon, each predicting h steps ahead from features available now; it costs h models and does not compound, and it is usually the better answer once the horizon is more than a few steps.

                 Recursive                   Direct
models            1                          one per horizon
error behaviour   compounds with each step   independent per step
features          can use lag_1              only lags at least h old
consistency       forecasts flow smoothly    steps can disagree with each other
use when          short horizons             the horizon is the thing you care about
Whichever you pick, report the error at each horizon separately. An average over one to thirty days is dominated by the easy end.

Intervals, not points#

A forecast without uncertainty is incomplete

A single number hides how confident the forecast is. A prediction interval lets people plan for the realistic range, and checking that the interval covers reality the stated share of the time is how you know it is honest.

Coverage is a measurement, not an assumption. Take every backtest fold, count how often the actual value fell inside the stated interval, and compare that with the number on the label. An eighty percent interval that contains sixty percent of outcomes is not a cautious forecast, it is a wrong one, and the planner who sized their safety stock from it will find out in a way you do not want to be present for.

import numpy as np

def coverage(actual, low, high):
    return float(np.mean((actual >= low) & (actual <= high)))

rng = np.random.default_rng(1)
actual = rng.normal(100, 10, 500)              # the truth: standard deviation 10

honest = coverage(actual, 100 - 1.2816 * 10, 100 + 1.2816 * 10)
optimistic = coverage(actual, 100 - 1.2816 * 6, 100 + 1.2816 * 6)
print(round(honest, 3), round(optimistic, 3))

# 0.852 0.616
# The second interval is labelled 80% and covers 62%. Nothing in the point
# forecast changed; only the claimed uncertainty was wrong.
1.2816 is the normal quantile for an 80 percent central interval. If your model gives quantiles directly, as quantile gradient boosting does, you can skip the normal assumption entirely and should.

Intervals from a model's own error assumptions are usually too narrow, because they account for the noise and not for the model being wrong about the level. Two fixes work in practice. Fit quantile models directly, one per quantile, so the interval is learned rather than derived. Or take the residuals from the backtest, use their empirical quantiles as the interval width, and widen with the horizon — the crude version of conformal prediction, and considerably better than a formula that assumes the model is correct.

Where people go wrong

Reporting a point forecast with no interval. It presents an uncertain estimate as if it were exact.

If this comes up in an interview

Would you use a neural network or a classical model for forecasting?

Start with classical baselines and a gradient-boosted model with lag features, which are fast, strong and interpretable on most business series. A neural network needs to beat those with evidence.

Why evaluate at multiple forecast horizons?

Error grows with the horizon, and an averaged single number hides how quickly accuracy decays. Evaluate at each horizon you will actually use.

Build this: Fit exponential smoothing and a gradient-boosted model with lag features to the same series. Compare them at horizons of 1, 7 and 30 days.

This is one section of Time series, 17 hours across 4 sections. Read it in full to get the whole topic in order.