-
Notifications
You must be signed in to change notification settings - Fork 8
AID
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()/ Pythonaid_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.
-
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 confidencelevelare 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. -
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.
| 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 |
| 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(). |
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. |
# 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# 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])# 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| 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). |
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 |
# 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 0Both 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- Top-level imports:
from greybox import aid, aid_cat. -
matplotlib>=3.5is a hard dependency, sores.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-3because Python usesnloptwhile R usesnloptr; the two optimisers converge to nearly-but-not-quite-identical optima. - Stockout positions in
stockouts.start/stockouts.endare reported as 1-based indices to match R; thedummy,new, andobsoletearrays are positional (binary) vectors of lengthnand behave identically in both languages. - The internal smoothing steps use LOWESS and SuperSmoother, which match R to machine precision.
- Svetunkov, I. and Sroginis, A. (2025). "Why do zeroes happen? A model-based approach for demand classification". arXiv:2504.05894. DOI: 10.48550/arXiv.2504.05894
- Svetunkov, I. (2023). Statistics for Business Analytics. https://openforecast.org/sba/