Skip to content
Ivan Svetunkov edited this page Jun 25, 2026 · 3 revisions

AID — Automatic Identification of Demand

Function signatures

R

aid(y, ic = c("AICc","AIC","BICc","BIC"), level = 0.99,
    loss = "likelihood", ...)
aidCat(data, ...)

Python

def aid(
    y,
    ic: str = "AICc",
    level: float = 0.99,
    loss: str = "likelihood",
    **alm_kwargs,
) -> AidResult: ...

def aid_cat(data, **aid_kwargs) -> AidCatResult: ...

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

Methods on the result objects

Both implementations expose the same two methods on the aid / aid_cat return values:

Method R Python Description
print print(res) print(res) Demand-type and anomaly summary.
plot plot(res) res.plot() For aid(): time series with red/green stockout boundary lines and grey shading; leading-zero block shaded blue when new, trailing-zero block shaded orange when obsolete. For aid_cat(): the 2×3 demand-category panel with cell counts and row / column marginals.

Both implementations also expose the same fields directly: in R via $ (res$stockouts$start), in Python via attribute access (res.stockouts.start). The Python nested fields are typed dataclasses (AidType, Stockouts) that also support dict-style subscripts (res.stockouts["start"]) for backward compatibility with code written against the earlier dict-based API.

# Python — attribute access matches R's $.$.$ pattern
res = aid(y)
res.stockouts.start    # numpy.ndarray of 1-based positions
res.stockouts.dummy    # length-n 0/1 indicator vector
res.type.type1         # "count" or "fractional"

# Dict-style subscripts also work
res.stockouts["start"]
res.type["type1"]

ax = res.plot()        # matplotlib Axes

Notes on the Python implementation

  • Top-level imports: from greybox import aid, aid_cat.
  • matplotlib>=3.5 is a hard dependency, so res.plot() always works.
  • 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