TL;DR & Quick Summary
Croston's method, the SBA correction and TSB are the three standard methods for intermittent demand, meaning items that sell nothing in most periods. All three take only a few lines of Python. This guide gives working code, checks it against the statsforecast library, and backtests it on 200 items. It also shows a result most tutorials leave out.
Croston: smooths demand size and the interval between sales separately. Forecast = size ÷ interval.
SBA: Croston × (1 − α/2). It removes most of Croston's upward bias.
TSB: smooths the probability of a sale every period, so the forecast falls when an item stops selling.
Verified: the hand-written Croston below matches statsforecast's
CrostonClassicexactly.The honest result: in our backtest, a 13-week moving average beat all three on MASE. Croston-type methods still won on bias and on fading items.
Key Takeaway: Implementing the methods is easy. Classifying items correctly, testing against a simple benchmark, and judging by bias as well as error is where the value is.
Get Started: For the concepts behind this code, read intermittent demand forecasting: Croston, SBA and TSB explained. To have it applied to your catalogue, schedule a strategy call with Cogniq AI.
What Each Method Does
Croston's method forecasts intermittent demand by smoothing two things separately: the size of each non-zero demand (z) and the number of periods between non-zero demands (p). Both are updated only when a sale happens. The forecast per period is z ÷ p. The method comes from Croston (1972).
SBA (Syntetos–Boylan Approximation) multiplies the Croston forecast by (1 − α/2), where α is the smoothing constant. Syntetos and Boylan (2005) showed this removes most of Croston's tendency to forecast too high.
TSB (Teunter–Syntetos–Babai) smooths the probability that a sale occurs in each period instead of the interval, and updates it every period, including periods with no sales. Its forecast falls during long runs of zeros, which suits items that may be dying out. It comes from Teunter, Syntetos and Babai (2011).
Step 1: Classify Your Items
Only apply these methods to items that actually need them. The standard classification uses two numbers per item:
- ADI: average number of periods per non-zero demand
- CV²: squared coefficient of variation of the non-zero demand sizes
Items with ADI ≥ 1.32 are intermittent (low CV²) or lumpy (high CV²). The cut-offs come from Syntetos, Boylan and Croston (2005).
import numpy as np
def classify(y):
y = np.asarray(y, dtype=float)
nz = y[y > 0]
if len(nz) < 2:
return np.nan, np.nan, "insufficient"
adi = len(y) / len(nz)
cv2 = (nz.std(ddof=1) / nz.mean()) ** 2
if adi < 1.32:
cat = "smooth" if cv2 < 0.49 else "erratic"
else:
cat = "intermittent" if cv2 < 0.49 else "lumpy"
return adi, cv2, cat
Run it at the level you make stocking decisions, usually item by location. Remove stockout periods first: zero sales while out of stock is missing supply, not missing demand.
Step 2: Croston and SBA
def croston(y, alpha=0.1, variant="classic"):
"""Flat per-period forecast using Croston's method or the SBA variant."""
y = np.asarray(y, dtype=float)
nz = np.flatnonzero(y)
if len(nz) == 0:
return 0.0
z = y[nz[0]] # smoothed demand size
p = nz[0] + 1 # smoothed interval (periods since start)
last = nz[0]
for t in nz[1:]:
z = alpha * y[t] + (1 - alpha) * z
p = alpha * (t - last) + (1 - alpha) * p
last = t
f = z / p
if variant == "sba":
f *= 1 - alpha / 2
return f
The loop only visits periods with demand. That's the core idea: empty periods don't change the estimates.
Check it with the worked example from our intermittent demand guide. Demand of 5, then 3 four periods later, then 6 two periods later, with α = 0.2:
ex = [0, 0, 5, 0, 0, 0, 3, 0, 6]
croston(ex, 0.2) # 1.649
croston(ex, 0.2, "sba") # 1.484
Those match the hand calculation in that article: 1.65 and 1.48 units per period.
Step 3: TSB
def tsb(y, alpha_d=0.1, alpha_p=0.1):
"""Teunter-Syntetos-Babai: smooth demand probability every period."""
y = np.asarray(y, dtype=float)
nz = np.flatnonzero(y)
if len(nz) == 0:
return 0.0
prob = (y > 0).mean() # simple initialisation
size = y[nz].mean()
for v in y:
if v > 0:
prob = alpha_p + (1 - alpha_p) * prob
size = alpha_d * v + (1 - alpha_d) * size
else:
prob = (1 - alpha_p) * prob
return prob * size
The difference from Croston is in the else branch. Every empty period lowers the probability estimate, so the forecast decays when an item stops selling. Croston and SBA would hold their last value indefinitely.
The initialisation here is a simple choice. Libraries may start the estimates differently, which changes early forecasts slightly but matters less as history grows.
Step 4: Cross-Check Against statsforecast
Before trusting hand-written code, compare it with a maintained library. statsforecast from Nixtla includes CrostonClassic, CrostonSBA, CrostonOptimized and TSB:
import pandas as pd
from statsforecast import StatsForecast
from statsforecast.models import CrostonClassic, CrostonSBA, TSB
df = pd.DataFrame({
"unique_id": "sku",
"ds": pd.date_range("2024-01-01", periods=len(history), freq="W"),
"y": history,
})
sf = StatsForecast(
models=[CrostonClassic(), CrostonSBA(), TSB(alpha_d=0.1, alpha_p=0.1)],
freq="W",
)
print(sf.forecast(df=df, h=4))
Three things are worth knowing about the library (checked against statsforecast 2.1.1):
CrostonClassicuses a fixed α of 0.1. On the same item, ourcroston(history)returned 3.220, identical to the library's 3.220164.CrostonSBAapplies a fixed factor of 0.95, which is 1 − 0.1/2. Our SBA gave 3.059, matching the library.TSBhas no defaults: you must passalpha_dandalpha_p.CrostonOptimizedchooses the smoothing constant for you.
In production, use the library. It's tested, vectorised across thousands of series, and supports prediction intervals. Hand-written versions are for understanding the method and checking assumptions.
Step 5: Backtest Fairly
The forecast is a rate, so it will never match any single week. Judge it on a held-out period with metrics that work when demand is zero:
def mase(actual, forecast, history):
scale = np.mean(np.abs(np.diff(history))) # in-sample naive error
return np.mean(np.abs(actual - forecast)) / scale if scale > 0 else np.nan
bias = lambda actual, forecast: np.mean(forecast - actual)
MASE (Hyndman and Koehler, 2006) divides your error by the error of a naive forecast, so values below 1 beat that benchmark. Bias is the signed average error. For slow movers it often matters more than error size, because it turns directly into excess stock or stockouts.
What We Found on 200 Items
We generated 200 synthetic items over 104 weeks with a fixed random seed. Each item had its own sale probability (8–90% of weeks) and average order size, and one in ten items faded out during the second year. We held out the last 13 weeks, forecast with each method using α = 0.1, and also tested a plain 13-week moving average. 167 of the items classified as intermittent.
| Method | Mean MASE (intermittent items) | Mean bias, units/week (intermittent) | Mean bias, units/week (fading items) |
|---|---|---|---|
| Croston | 0.913 | +0.321 | +2.337 |
| SBA | 0.901 | +0.168 | +2.220 |
| TSB | 0.831 | +0.022 | +0.981 |
| 13-week moving average | 0.817 | +0.016 | +0.735 |
Three lessons:
- SBA roughly halved Croston's bias, as the theory predicts, at the cost of one multiplication.
- TSB was far less biased on fading items, because its forecast decays when sales stop, while Croston and SBA kept forecasting demand that no longer existed.
- The simple moving average did best on MASE. A recent-window average adapts naturally to items whose demand is changing. On this data, the more sophisticated methods didn't earn their complexity on accuracy alone.
This is synthetic data, and your results will differ. On catalogues with stable intermittent demand, Croston-type methods often perform better than they did here. The point is the process: always include a simple benchmark, and choose by accuracy and bias on your own history. If the simple method wins, use it.
Choosing the Smoothing Constant
The smoothing constant α controls how quickly the estimates react to new data. A low value, such as 0.05, produces stable forecasts that change slowly. A high value, such as 0.3, reacts quickly but also chases noise.
Practical guidance:
- Start at 0.1, the value statsforecast uses for
CrostonClassicand a common default in the literature - Use
CrostonOptimizedif you want the library to choose the value for each item from its history - For TSB, test
alpha_pseparately. It controls how quickly the forecast falls when an item stops selling. If it's too low, fading items keep receiving stock for months. - Pick by holdout performance, not by in-sample fit. Try three or four values and keep the one with the best combination of MASE and bias on held-out weeks.
Remember that SBA's correction depends on α. With α = 0.1 the factor is 0.95, and with α = 0.2 it's 0.9. If you change α, change the correction too. Our function does this automatically.
Common Pitfalls
- Forecasting at the wrong level. An item can be smooth across the whole business and intermittent at every branch. Forecast where stock decisions are made.
- Leaving stockouts in the history. Zero sales while out of stock make demand look rarer than it is, so every method will under-forecast.
- Judging the forecast against weekly actuals on a chart. A rate of 1.5 units a week will never match a week that sold 0 or 6. Compare cumulative totals over the holdout instead.
- Using MAPE. It's undefined whenever actual demand is zero. Use MASE and bias.
- Treating the forecast as the order quantity. The forecast is an input to a stocking decision that also needs lead time, service target and the demand distribution.
- Skipping the benchmark. As our backtest showed, the simplest method can win. Without a benchmark you'd never know.
Putting It Into Production
- Run per segment: classify items, then apply the method that wins for each segment in backtesting
- Use the forecast to set stock levels, not just to report. For intermittent items, a distribution-based method such as Poisson works better than mean plus normal safety stock. See safety stock calculation formulas
- Reclassify quarterly, as items move between categories
- Monitor bias after go-live, not only error
If your data is too short for a reliable backtest, see how much historical data demand forecasting needs.
Conclusion
Croston, SBA and TSB take about fifty lines of Python, and statsforecast gives you production-ready versions. The harder and more valuable part is classifying items, removing stockout periods, testing against a simple benchmark, and judging by bias as well as error. In our backtest, a moving average was the most accurate and TSB was the least biased on fading items. Your data decides which one belongs in production.
Schedule a strategy call with Cogniq AI to have your slow-moving catalogue backtested, or explore our predictive analytics services.



