Technical Lab · Synthetic demonstration
Intermittent demand
Intermittent-demand methods are useful when zeros are the normal week. They are not a reason to abandon a mean once demand becomes regular.
Synthetic demonstration · Independent educational implementation.
Forecast demand when many weeks are zero and the non-zero weeks come in irregular sizes.
A method that treats every week alike can keep ordering through a long drought, or it can forget the size of a demand when one finally occurs.
Watch a zero run
- Demand; open dots are zero weeks
- Historical mean
- Classic Croston
- TSB
A 20% chance of demand produces an intermittent series: ADI 7.2, CV² 0.1, and 86% zeros. One-step mean absolute error on the scored weeks is mean 1.9, classic Croston 1.9, TSB 1.8. It ends with 5 zero weeks. Classic Croston’s next rate stays at 1.2 because zeros do not update it. TSB’s next rate is 1.0 because the occurrence probability falls during that run. These errors are for this synthetic series only.
The updates
Classic Croston (uncorrected) forecast = smoothed size / smoothed interval. Zeros leave both smoothers unchanged. This is not the Syntetos–Boylan bias correction. TSB forecast = probability × size. Probability moves every week. Size moves only when demand occurs.
Implementation
src/lib/lab/intermittent.ts View source on GitHub: src/lib/lab/intermittent.ts
export function croston(demand: number[], alpha: number): RateForecast {
assertUnitInterval(alpha, 'Alpha');
const first = assertDemand(demand);
let size = demand[first];
let interval = first + 1;
let since = 0;
const fitted = Array(demand.length).fill(0);
for (let time = first + 1; time < demand.length; time += 1) {
fitted[time] = size / interval;
since += 1;
if (demand[time] > 0) {
size += alpha * (demand[time] - size);
interval += alpha * (since - interval);
since = 0;
}
}
return { fitted, next: size / interval, size, intervalOrProbability: interval };
}Show the TSB update
export function tsb(demand: number[], alpha: number, beta = alpha): RateForecast {
assertUnitInterval(alpha, 'Alpha');
assertUnitInterval(beta, 'Beta');
const first = assertDemand(demand);
let size = demand[first];
let probability = 1 / (first + 1);
const fitted = Array(demand.length).fill(0);
for (let time = first + 1; time < demand.length; time += 1) {
fitted[time] = probability * size;
const occurred = demand[time] > 0 ? 1 : 0;
probability += beta * (occurred - probability);
if (occurred) size += alpha * (demand[time] - size);
}
return { fitted, next: probability * size, size, intervalOrProbability: probability };
}