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.
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
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.
| Horizon | Seasonal naive | Recursive | Direct |
|---|---|---|---|
| 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
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
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
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]];
}