Technical Lab · Synthetic demonstration

Multi-step forecasting

One origin can support a one-step forecast or a path of later horizons. The evaluation has to respect that difference.

All technical examples

Synthetic demonstration · Independent educational implementation.

Produce a forecast for H1–H6 from one origin, rather than only the next period.

A replenishment decision covers a lead time, not a single week. Error is not the same at every step ahead.

Compare the path

  • Observed
  • Held-out actual
  • Forecast path
  • Illustrative band
One origin, six horizons. Each step is checked against a week the model did not see. Training weeks 49–66, then the held-out weeks 67–72. The ring marks H3. Point at or tap a horizon to mark it.

Recursive H3 is 98.9 units. The held-out actual in week 69 is 100. Across four chronological origins, mean absolute error at H3 is Seasonal naive 5.5, Recursive 1.9, Direct 2.1. The shaded band widens with the square root of the horizon, a teaching approximation for errors that compound. This is not a calibrated prediction interval, and it is not a business result.

Error by horizon

Mean absolute error across the four origins that fit in this series. Training for each origin stops before that origin. Changing the controls highlights a line and a horizon; it does not recompute a different experiment.

On this series the seasonal naive has the highest error at every horizon. Recursive and direct agree at H1 and separate later. The table gives the same values.
Mean absolute error by horizon and method
Horizon Seasonal naiveRecursiveDirect
H1 2.8 1.6 1.6
H2 4.3 2.2 2.5
H3 5.5 1.9 2.1
H4 3.5 2.5 2.5
H5 3.8 2.8 2.8
H6 5.8 2.3 3.0

Implementation

Multi-output models, which predict every horizon together, are the third standard strategy. This page implements the other two, plus the baseline.

src/lib/lab/forecasting.ts View source on GitHub: src/lib/lab/forecasting.ts

Seasonal naive: repeat the last complete season.src/lib/lab/forecasting.ts
export function seasonalNaive(history: number[], horizon: number, season: number): number[] {
  if (history.length < season || horizon < 1) throw new Error('Seasonal naive needs a full season of history.');
  const extended = history.slice();
  const forecast: number[] = [];
  for (let step = 0; step < horizon; step += 1) {
    const value = extended[extended.length - season];
    forecast.push(value);
    extended.push(value);
  }
  return forecast;
}
Show the recursive rollout
Recursive: one model, then feed each prediction into the next step.src/lib/lab/forecasting.ts
export function recursiveForecast(history: number[], horizon: number, season: number): number[] {
  const { coefficients } = oneStepFit(history, season);
  const extended = history.slice();
  const forecast: number[] = [];
  for (let step = 0; step < horizon; step += 1) {
    const target = extended.length;
    const features = directFeatures(extended, target, 1, season);
    if (!features) throw new Error('Recursive forecast ran out of lags.');
    const value = predict(coefficients, features);
    forecast.push(value);
    extended.push(value);
  }
  return forecast;
}
Show the direct-model features
Direct: each horizon uses lags that are already known, including the seasonal lag inside one season.src/lib/lab/forecasting.ts
export function directFeatures(series: number[], target: number, step: number, season: number): number[] | null {
  const latest = target - step;
  const seasonal = target - season;
  if (latest < 0 || seasonal < 0 || seasonal > latest) return null;
  return [1, series[latest], series[seasonal]];
}