Forecasting looks like regression but breaks most of regression's assumptions. Observations are ordered and correlated, the test set must come after the training set, and the thing you are predicting is often several steps ahead rather than one.
The field has recently acquired a genuinely new option. Pretrained foundation models for time series produce useful forecasts on series they have never seen, with no training at all — the same zero-shot idea that arrived in NLP a few years earlier. They do not always beat a well-tuned classical model, but they remove the cold-start problem entirely.
Model families 4
Sub-categories within this task. The distinction that
matters is not which is best, but which fits your constraints.
Classical statistical
Negligible.
Use when
Your baseline, and frequently hard to beat. Strong seasonal structure, limited history, and a need to explain the forecast's components.
Avoid when
Many related series with shared behaviour, or many external drivers.
Examples
ARIMA / SARIMAETSThetaProphet
Gradient boosting on lag features
Megabytes.
Use when
You have covariates — promotions, rates, calendar effects — and want the flexibility of a general regressor. Often the pragmatic winner.
Avoid when
You need proper probabilistic intervals without extra work, or you must extrapolate a trend beyond the training range. Trees cannot.
Examples
LightGBM / XGBoost with lag and rolling features
Deep learning forecasters
1M–50M.
Use when
Hundreds or thousands of related series where a single model can learn shared patterns and pool information across them.
Avoid when
A handful of series. There is not enough data to justify it.
Examples
DeepARN-BEATSTemporal Fusion TransformerPatchTST
Time series foundation models
10M–700M.
Use when
Zero-shot forecasting with no training, or as an immediate strong baseline. Particularly useful for new series with little history.
Avoid when
You have abundant history and a well-tuned specialised model already performing well.
Examples
Chronos / Chronos-BoltTimesFMMoirai
In a bank 4
The third line in each is the part that makes it
harder than the textbook version.
Liquidity and cash flow forecasting
Project net cash positions across currencies and entities to manage buffers.
The wrinkleRegulatory liquidity requirements make the tails matter more than the central forecast. You need calibrated quantiles, not a point estimate — and the model must be defensible to treasury and to supervisors.
ATM and branch cash demand
Forecast withdrawals per location to schedule replenishment.
The wrinkleThousands of related series with strong local calendar effects — paydays, holidays, local events. A classic case for a single global model across all locations rather than one model per ATM.
Deposit balance and attrition modelling
Project balances under rate scenarios for asset-liability management.
The wrinkleThis is scenario forecasting, not point forecasting. The model must respond sensibly to rate paths it has never seen, which is exactly where purely learned models are weakest and structural models earn their place.
Contact centre volume for staffing
Forecast call and chat volume by interval to schedule staff.
The wrinkleIntraday seasonality on top of weekly and annual patterns, plus shocks from outages and product events. Hierarchical reconciliation across intervals, days and sites matters as much as the base forecast.
Code 3
Named functions and the arguments that actually
change the outcome. Comments flag the traps.
Backtest properly — this is the whole game
An expanding-window backtest is the only honest evaluation. A single train/test split tells you almost nothing.
import numpy as np
import pandas as pd
def backtest(series, model_fn, horizon=28, folds=6, step=28):
"""Expanding window: train on everything up to a cutoff, forecast
the next `horizon`, walk forward, repeat."""
results = []
for k in range(folds):
cut = len(series) - (folds - k) * step
train, actual = series[:cut], series[cut:cut + horizon]
if len(actual) < horizon:
continue
pred = model_fn(train, horizon)
# MASE: scaled against a seasonal naive baseline, so a value
# above 1 means you are worse than "same as last week".
naive = np.mean(np.abs(np.diff(train, 7)))
results.append({
"fold": k,
"MAE": np.mean(np.abs(actual - pred)),
"MASE": np.mean(np.abs(actual - pred)) / naive,
})
return pd.DataFrame(results)
# ALWAYS produce the naive baseline first. Plenty of deep learning
# forecasts lose to "same value as last week" - better to know early.
def seasonal_naive(train, h, period=7):
return np.array([train[-period + (i % period)] for i in range(h)])
Zero-shot forecasting with Chronos
No training. Point the model at a series and it forecasts — with quantiles, which is what you need for liquidity work.
import torch
import numpy as np
from chronos import ChronosPipeline
pipe = ChronosPipeline.from_pretrained(
"amazon/chronos-bolt-base",
device_map="cuda", torch_dtype=torch.bfloat16,
)
context = torch.tensor(history_values, dtype=torch.float32)
# Returns SAMPLES, not a point forecast. The distribution is the point.
forecast = pipe.predict(context, prediction_length=28, num_samples=100)
q = np.quantile(forecast[0].numpy(), [0.05, 0.5, 0.95], axis=0)
low, median, high = q
# For treasury work the 5th percentile is often the number that matters,
# not the median - plan against the bad case.
print("median day 1:", median[0], " 5th pct:", low[0])
Many related series with AutoGluon
Fits classical, boosted and deep models plus foundation models, then ensembles by backtest performance. A strong baseline in one call.
from autogluon.timeseries import TimeSeriesDataFrame, TimeSeriesPredictor
# Long format: one row per (series, timestamp)
data = TimeSeriesDataFrame.from_data_frame(
df, id_column="atm_id", timestamp_column="date"
)
predictor = TimeSeriesPredictor(
prediction_length=28,
target="withdrawals",
eval_metric="MASE",
known_covariates_names=["is_holiday", "is_payday"], # known in advance
quantile_levels=[0.05, 0.1, 0.5, 0.9, 0.95],
).fit(data, presets="high_quality", time_limit=1800)
print(predictor.leaderboard(data)) # see what actually won
preds = predictor.predict(data, known_covariates=future_calendar)
What catches people out 5
Never shuffle, never random split
Any cross-validation that mixes time order leaks the future. Use expanding or rolling windows exclusively. This is the single most common error in forecasting projects.
Beat the naive baseline before anything else
Seasonal naive — 'the same as this day last week' — is surprisingly strong. Report MASE so the comparison is explicit. A sophisticated model that loses to it is a finding, not a failure to hide.
Know which covariates are known in the future
Holidays and paydays are known in advance; competitor pricing is not. Using a covariate you will not have at prediction time is leakage, and it is easy to do accidentally.
Forecast the distribution, not the number
For anything feeding a buffer or a limit, the quantiles are the deliverable. A point forecast with no interval cannot support a decision about how much cash to hold.
Hierarchies must reconcile
Branch forecasts should sum to regional, and regional to national. Independent models will not, and stakeholders notice immediately. Reconciliation is a separate step you must plan for.