Automatic Identification of Demand
The greybox.aid module classifies a time series into one of six
demand types and flags stockouts, new products, and obsolete products.
It is a one-to-one Python port of R’s greybox::aid() and
greybox::aidCat().
aid()
- greybox.aid.aid(y, ic: str = 'AICc', level: float = 0.99, loss: str = 'likelihood', **alm_kwargs: Any) AidResult[source]
Automatic identification of demand type.
Classifies a time series into one of six demand types and flags stockouts, new products, and obsolete products. This is a one-to-one Python port of R’s
greybox::aid().The algorithm runs in two stages:
Stockout detection. Inter-demand intervals are smoothed with
lowess()and fitted with anALMGeometric model. Cumulative probabilities abovelevelare flagged as potential stockouts, with leading/trailing zero runs interpreted as new or obsolete products.Demand type classification. Smoothed regressors built with
supsmu()are used to fit up to four candidate models (rectified normal, normal + Bernoulli occurrence, negative binomial, negative binomial + Bernoulli occurrence). The model with the lowest information criterion names the demand type.
- Parameters:
y (array_like) – The time series.
NaNvalues are replaced by zero.ic ({"AICc", "AIC", "BICc", "BIC"}, default="AICc") – Information criterion used to choose between candidate models.
level (float, default=0.99) – Confidence level for stockout identification.
loss (str, default="likelihood") – Loss function passed to
ALM.**alm_kwargs – Extra keyword arguments forwarded to
ALM. Useful for tuning the optimiser, e.g.nlopt_kargs={"maxeval": 1000}.
- Returns:
Object containing the identified demand type, fitted models, stockout indices, and new/obsolete flags.
- Return type:
Notes
The six possible demand types are:
regular countregular fractionalsmooth intermittent countsmooth intermittent fractionallumpy intermittent countlumpy intermittent fractional
Examples
Classify an intermittent count series:
>>> 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) >>> res.name in { ... "smooth intermittent count", "lumpy intermittent count" ... } True
Inspect detected stockouts:
>>> y2 = rng.poisson(3, 100).astype(float) >>> y2[40:50] = 0 # injected stockout >>> res2 = aid(y2) >>> res2.stockouts["start"] # 1-based, matches R array([41])
Example:
import numpy as np
from greybox import aid
rng = np.random.default_rng(42)
# Intermittent count demand: Poisson(0.7) gives many zeros
y = rng.poisson(0.7, 120).astype(float)
result = aid(y)
print(result)
# The provided time series is smooth intermittent count
# Detect an injected stockout
y2 = rng.poisson(3, 100).astype(float)
y2[40:50] = 0
result2 = aid(y2)
print(result2.stockouts.start, result2.stockouts.end)
# [41] [50] (1-based positions, matches R)
Accessing the result
The type and stockouts fields are typed dataclasses
(AidType / Stockouts) that support both R-style
attribute access and dict-style subscript access:
result2.stockouts.start # numpy.ndarray of 1-based positions
result2.stockouts["start"] # same data, dict-style fallback
result2.type.type1 # "count" or "fractional"
Plotting
AidResult.plot() mirrors R’s plot.aid() — line plot of the
series with each stockout shaded in light grey and bracketed by a
solid red (start) / dashed green (end) vertical line. Leading-zero
runs flagged as a new product get a light-blue shading, trailing-zero
runs flagged as obsolete get a light-orange shading:
import matplotlib.pyplot as plt
ax = result2.plot()
plt.show()
aid_cat()
- greybox.aid.aid_cat(data, **aid_kwargs: Any) AidCatResult[source]
Apply
aid()to multiple series and summarise the result.One-to-one port of R’s
greybox::aidCat(). Each column ofdatais treated as a separate series and passed throughaid(); the per-series demand types are then tallied into a 2x3 demand-category table plus a count of anomalies (new, stockouts, obsolete) across the collection.- Parameters:
data (dict, pandas.DataFrame, or 2-D numpy.ndarray) – The series to categorise. Each column / dict entry is one series.
**aid_kwargs – Keyword arguments forwarded to
aid()(for exampleic="BIC"orlevel=0.95).
- Returns:
Object with the per-series categories, the 2x3 type matrix, the anomaly counts, and the individual
AidResultobjects.- Return type:
Examples
Apply to a small dictionary of series:
>>> import numpy as np >>> from greybox import aid_cat >>> rng = np.random.default_rng(1) >>> series = { ... "a": rng.poisson(1, 80).astype(float), ... "b": rng.poisson(5, 80).astype(float), ... "c": rng.normal(10, 2, 80), ... } >>> result = aid_cat(series) >>> result.types.shape (2, 3)
See also
aidApply the classification to a single series.
Example:
import numpy as np
from greybox import aid_cat
rng = np.random.default_rng(1)
series = {
"a": rng.poisson(1, 80).astype(float),
"b": rng.poisson(5, 80).astype(float),
"c": rng.normal(10, 2, 80),
}
result = aid_cat(series)
print(result.types) # 2x3 demand-type counts
print(result.anomalies) # New / Stockouts / Old counts
print(result.categories) # categorical vector per series
ax = result.plot() # 2x3 demand-category panel
Result classes
- class greybox.aid.AidResult(y: ndarray, models: dict, name: str, type: AidType | dict, stockouts: Stockouts | dict, new: bool, obsolete: bool, ic: str = 'AICc')[source]
Bases:
objectResult of
aid(). Mirrors R’saidS3 class.The nested
typeandstockoutsfields are typed dataclasses (AidTypeandStockouts) that support both attribute access and dict-style access. The two patterns below are equivalent:result.stockouts.start # Python idiom result.stockouts["start"] # R-list / dict idiom
- y
The original time series with NaN values replaced by 0.
- Type:
numpy.ndarray
- models
All fitted candidate models, keyed by demand-type name. If stockout detection ran, the stockout model is exposed under the key
"stockout".- Type:
dict[str, ALM]
- name
Name of the selected demand type (e.g.
"smooth intermittent count").- Type:
str
- stockouts
Stockout / new / obsolete metadata.
startandendare 1-based positions matching R;dummy/new/obsoleteare binary numpy arrays of lengthlen(y).- Type:
- new
Trueif the series starts with anomalous zeros (a new product).- Type:
bool
- obsolete
Trueif the series ends with anomalous zeros (a discontinued product).- Type:
bool
- ic
Information criterion used to select between candidates.
- Type:
str
- plot(ax=None, **kwargs)[source]
Plot the series with stockout / new / obsolete overlays.
Mirrors R’s
plot.aid():Line plot of
yagainst 1-based positions.Solid red vertical line at each stockout start, dashed green vertical line at each stockout end.
Light-grey shaded rectangle covering each stockout span.
Light-blue shaded rectangle covering leading zeros when the series is flagged as a new product.
Light-orange shaded rectangle covering trailing zeros when the series is flagged as obsolete.
- Parameters:
ax (matplotlib.axes.Axes, optional) – Axes to draw on. A new figure is created if
None.**kwargs – Forwarded to
matplotlib.axes.Axes.plot()for the main series line.
- Returns:
The axes containing the plot.
- Return type:
matplotlib.axes.Axes
- class greybox.aid.AidCatResult(categories: Categorical, types: DataFrame, anomalies: Series, results: list[AidResult] | None = None)[source]
Bases:
objectResult of
aid_cat(). Mirrors R’saidCatS3 class.- categories
Categorical vector of demand types, one entry per input series, with the six possible levels:
regular count,smooth intermittent count,lumpy intermittent count,regular fractional,smooth intermittent fractional,lumpy intermittent fractional.- Type:
pandas.Categorical
- types
2x3 frequency table with rows
["Count", "Fractional"]and columns["Regular", "Smooth Intermittent", "Lumpy Intermittent"].- Type:
pandas.DataFrame
- anomalies
Counts of
"New","Stockouts", and"Old"(obsolete) across the input series.- Type:
pandas.Series
- results
The individual
AidResultobjects, in the same order as the input columns.- Type:
list[AidResult]
- plot(ax=None, **kwargs)[source]
Plot the 2x3 demand-category panel.
Mirrors R’s
plot.aidCat()layout: six cells, one per demand category, each annotated with the count of series falling in it. Row marginals (Count / Fractional totals) are drawn along the right edge, column marginals (Regular / Smooth Intermittent / Lumpy Intermittent totals) along the bottom edge, and the grand total at the bottom-right.- Parameters:
ax (matplotlib.axes.Axes, optional) – Axes to draw on. A new figure is created if
None.**kwargs – Forwarded to
matplotlib.pyplot.subplots()whenaxisNone.
- Returns:
The axes containing the plot.
- Return type:
matplotlib.axes.Axes
Nested dataclasses
- class greybox.aid.AidType(type1: str | None, type2: str | None, type2a: str | None)[source]
Bases:
objectSub-categorisation of an AID classification (mirrors R’s
$type).Both attribute access (
aid_type.type1) and dict-style access (aid_type["type1"]) work; the latter exists for backward compatibility with code written against the earlier dict-based API.- type1
"count"or"fractional".- Type:
str or None
- type2
"regular"or"intermittent".- Type:
str or None
- type2a
"smooth"or"lumpy"when intermittent,Noneotherwise.- Type:
str or None
- class greybox.aid.Stockouts(start: ndarray | None, end: ndarray | None, dummy: ndarray, new: ndarray, obsolete: ndarray)[source]
Bases:
objectStockout / new / obsolete metadata (mirrors R’s
$stockouts).Both attribute access (
stockouts.start) and dict-style access (stockouts["start"]) work; the latter exists for backward compatibility with code written against the earlier dict-based API.- start
1-based positions where each stockout begins.
Noneif the original series had no zeros at all; an empty array if zeros were present but none were flagged as stockouts.- Type:
numpy.ndarray or None
- end
1-based positions where each stockout ends. Same length as
start.- Type:
numpy.ndarray or None
- dummy
Length-
nbinary indicator:1on observations inside a stockout window,0elsewhere.- Type:
numpy.ndarray
- new
Length-
nbinary indicator:1over the leading-zeros block when the series is flagged as a new product.- Type:
numpy.ndarray
- obsolete
Length-
nbinary indicator:1over the trailing-zeros block when the series is flagged as obsolete.- Type:
numpy.ndarray