Forecasting in Conditions of Uncertainty

May 2026

Problem

Use Croston’s method when a time series has many zero periods interspersed with sporadic non-zero demand. Standard exponential smoothing collapses because zeros dilute estimates and ARIMA struggles with the structural zeros.

Classic symptoms:

  • ADI (Average Demand Interval) > 1.32
  • CV² (squared coefficient of variation of non-zero demand) < 0.49

Assumptions & Conditions

  • Demand arrivals follow a Bernoulli process (independent across periods).
  • Non-zero demand sizes are i.i.d. (no trend, no seasonality within bursts).
  • Demand interval and demand size are independent of each other.

Breaks down when: demand has a trend, sizes cluster seasonally, or there’s a long-run structural shift.


Approach

Croston splits the series into two separate ES processes:

z^t=αzt+(1α)z^t1\hat{z}_t = \alpha \cdot z_t + (1 - \alpha) \cdot \hat{z}_{t-1}

p^t=αpt+(1α)p^t1\hat{p}_t = \alpha \cdot p_t + (1 - \alpha) \cdot \hat{p}_{t-1}

where ztz_t is the non-zero demand size at occurrence tt, ptp_t is the interval between occurrences, and α(0,1)\alpha \in (0, 1) is the smoothing parameter.

Forecast per period:

d^=z^p^\hat{d} = \frac{\hat{z}}{\hat{p}}

Bias correction (Syntetos-Boylan, SBA): Croston’s estimator is positively biased. SBA fixes this:

d^SBA=(1α2)z^p^\hat{d}_{SBA} = \left(1 - \frac{\alpha}{2}\right) \cdot \frac{\hat{z}}{\hat{p}}


Implementation

import numpy as np
from dataclasses import dataclass

@dataclass
class CrostonResult:
    forecast: float
    z_hat: float  # smoothed demand size
    p_hat: float  # smoothed interval


def croston(demand: np.ndarray, alpha: float = 0.1, variant: str = "sba") -> CrostonResult:
    """
    Croston's method for intermittent demand forecasting.

    Parameters
    ----------
    demand : 1-D array of demand values (zeros allowed)
    alpha  : smoothing parameter in (0, 1)
    variant: "classic" or "sba" (Syntetos-Boylan bias correction)

    Returns
    -------
    CrostonResult with per-period rate forecast
    """
    nonzero_idx = np.where(demand > 0)[0]
    if len(nonzero_idx) == 0:
        return CrostonResult(forecast=0.0, z_hat=0.0, p_hat=1.0)

    # Bootstrap from first occurrence
    z_hat = demand[nonzero_idx[0]]
    p_hat = float(nonzero_idx[0] + 1)  # periods until first demand

    for i in range(1, len(nonzero_idx)):
        interval = nonzero_idx[i] - nonzero_idx[i - 1]
        z_hat = alpha * demand[nonzero_idx[i]] + (1 - alpha) * z_hat
        p_hat = alpha * interval + (1 - alpha) * p_hat

    rate = z_hat / p_hat
    if variant == "sba":
        rate *= 1 - alpha / 2

    return CrostonResult(forecast=rate, z_hat=z_hat, p_hat=p_hat)

Results & Tradeoffs

PropertyCrostonSBA
BiasPositive bias ~5–30%Near-unbiased
VarianceLowerSlightly higher
Best forNever outperforms SBAADI > 1.32, CV² < 0.49

When to prefer alternatives:

  • TSB (Teunter-Syntetos-Babai): Demand can become extinct (e.g., end-of-life parts). TSB updates probability of non-zero demand each period.
  • ADIDA aggregation: Aggregate to non-zero-free bucket, forecast normally, then disaggregate. Simple and often competitive.
  • ML on features: If you have rich features (promotions, seasonality), a gradient-boosted model on binned demand usually beats smoothing-based methods.

Typical error metrics: CSL (Cycle Service Level), Fill Rate, MAE on non-zero periods. Avoid MAPE — division by zero on empty periods.


References

  • Croston, J.D. (1972). Forecasting and Stock Control for Intermittent Demands. Operational Research Quarterly.
  • Syntetos, A.A. & Boylan, J.E. (2005). The Accuracy of Intermittent Demand Estimates. International Journal of Forecasting.
  • Teunter, R., Syntetos, A.A. & Babai, M.Z. (2011). Intermittent demand: Linking forecasting to inventory obsolescence. EJOR.