Skip to content
Ivan Svetunkov edited this page May 15, 2026 · 3 revisions

AID — Automatic Identification of Demand

Overview

AID (Automatic Identification of Demand) classifies a univariate time series of demand observations into one of six demand categories and flags three kinds of anomalies (stockouts, new products, obsolete products). The procedure is model-based: it fits a small set of candidate ALM models to the cleaned series and picks the type whose model has the lowest information criterion.

Two entry points are provided in both implementations:

  • aid() — Run AID on a single time series.
  • R aidCat() / Python aid_cat() — Apply AID to a collection of series (matrix columns, data frame, or dict) and return a 2×3 category table plus anomaly counts.

The procedure is described in Svetunkov & Sroginis (2025); see References.

Algorithm

  1. Stockout detection — Build the vector of inter-demand intervals (gaps between non-zero observations). Smooth it with LOWESS. Fit a Geometric ALM (distribution="dgeom"). Compute the cumulative likelihood at each interval; values above the confidence level are flagged as stockouts. Leading and trailing zero runs are interpreted as a new product and an obsolete product respectively; the geometric model is refit on the trimmed data when either flag is set.

  2. Demand-type classification — Drop the stockout periods and the leading/trailing zero runs. Build a smoothed regressor via SuperSmoother. Fit up to four candidate ALMs:

    Slot Distribution Mixture Demand category
    1 drectnorm smooth intermittent fractional
    2 dnorm + Bernoulli occurrence lumpy intermittent fractional
    3 dnbinom smooth intermittent count
    4 dnbinom + Bernoulli occurrence lumpy intermittent count

    When the cleaned series contains no zeros, only two regular candidates (dnorm, dnbinom) are considered. Special branches handle binary data and very-low-volume data. The candidate with the smallest information criterion gives the final classification.

Demand categories

Type 1 Type 2 Type 2a Name
count regular regular count
fractional regular regular fractional
count intermittent smooth smooth intermittent count
fractional intermittent smooth smooth intermittent fractional
count intermittent lumpy lumpy intermittent count
fractional intermittent lumpy lumpy intermittent fractional

aid() — single series

Parameters

Parameter R Python Type Default Description
series y y numeric vector / array_like Time series (NA/NaN values are replaced by 0).
ic ic ic character / str "AICc" Information criterion: "AIC", "AICc", "BIC", "BICc".
level level level numeric / float 0.99 Confidence level for the stockout detection.
loss loss loss character / str "likelihood" Loss function passed to alm() / ALM().
extras ... **alm_kwargs Additional arguments forwarded to alm() / ALM().

Return Value

R returns an S3 object of class "aid"; Python returns an AidResult. Both expose the same fields:

Field Description
y Original series (NAs replaced by 0).
models Named list / dict of fitted candidate models, plus the stockout model when applicable.
name Selected demand type (one of the six category names).
type Sub-categorisation with elements type1, type2, type2a.
stockouts Sub-list / dict with start, end (1-based positions in the original series), dummy, new, obsolete (binary indicator vectors of length n).
new TRUE/True if leading zeros indicate a new product.
obsolete TRUE/True if trailing zeros indicate a discontinued product.

Examples

Intermittent count demand

# R
set.seed(42)
y <- rpois(120, 0.7)
res <- aid(y)
print(res)
# Python
import numpy as np
from greybox import aid

rng = np.random.default_rng(42)
y = rng.poisson(0.7, 120).astype(float)
res = aid(y)
print(res)
# The provided time series is smooth intermittent count

Injected stockout

# R
set.seed(0)
y <- rpois(100, 3); y[41:50] <- 0
res <- aid(y)
res$stockouts$start  # 41
res$stockouts$end    # 50
# Python
rng = np.random.default_rng(0)
y = rng.poisson(3, 100).astype(float)
y[40:50] = 0  # 0-based slice = 1-based 41..50
res = aid(y)
res.stockouts["start"]  # array([41])  -- 1-based, matches R
res.stockouts["end"]    # array([50])

New product (leading zeros)

# R
y <- c(rep(0, 20), rpois(80, 5))
res <- aid(y)
res$new       # TRUE
# Python
y = np.concatenate([np.zeros(20), rng.poisson(5, 80)])
res = aid(y)
res.new        # True

aidCat() / aid_cat() — multiple series

Parameters

Parameter R Python Type Default Description
data data data matrix / data.frame / array_like or DataFrame or dict Columns / entries are treated as individual series.
extras ... **aid_kwargs Forwarded to aid() (e.g. ic="BIC", level=0.95).

Return Value

R returns an object of class "aidCat"; Python returns an AidCatResult. Both contain:

Field R Python
categories factor vector of demand types pandas.Categorical
types 2×3 frequency matrix pandas.DataFrame indexed by ["Count","Fractional"] × ["Regular","Smooth Intermittent","Lumpy Intermittent"]
anomalies named vector New / Stockouts / Old pandas.Series with the same index
(extra) results — list of per-series AidResult objects

Example — multi-series tabulation

# R
set.seed(1)
xreg <- cbind(
  x1 = rpois(80, 1),
  x2 = rpois(80, 5),
  x3 = rnorm(80, 10, 2)
)
cat <- aidCat(xreg)
print(cat)
# Python
import numpy as np
from greybox import aid_cat

rng = np.random.default_rng(1)
series = {
    "x1": rng.poisson(1, 80).astype(float),
    "x2": rng.poisson(5, 80).astype(float),
    "x3": rng.normal(10, 2, 80),
}
cat = aid_cat(series)
print(cat)
# Demand categories:
#             Regular  Smooth Intermittent  Lumpy Intermittent
# Count             1                    1                   0
# Fractional        1                    0                   0
#
# Anomalies:
# New          0
# Stockouts    0
# Old          0

Notes on the Python implementation

  • Top-level imports: from greybox import aid, aid_cat.
  • Mirrors the R algorithm one-to-one; categorical decisions (name, type, new, obsolete, stockouts$start/$end) match R exactly on the same input.
  • Numerical IC values agree with R to about 1e-3 because Python uses nlopt while R uses nloptr; the two optimisers converge to nearly-but-not-quite-identical optima.
  • Stockout positions in stockouts["start"] / stockouts["end"] are reported as 1-based indices to match R; the dummy, new, and obsolete arrays are positional (binary) vectors of length n and behave identically in both languages.
  • The internal smoothing steps use LOWESS and SuperSmoother, which match R to machine precision.

References

Clone this wiki locally