-
Notifications
You must be signed in to change notification settings - Fork 22
OM models the probability of demand occurrence for intermittent time series. It implements the occurrence part of the iETS framework as a state-space model with a Bernoulli likelihood and one of several link functions. Available in both R (om(), omg(), auto.om()) and Python (OM, OMG, AutoOM).
Note: iETS refers to the full model for demand sizes and demand occurrence. OM (oETS) refers to the occurrence part only. See ADAM for the full iETS model.
The iETS framework decomposes intermittent demand as:
y_t = o_t × z_t
Where o_t ∈ {0, 1} is the binary occurrence indicator and z_t is the demand size (modelled separately - see ADAM). OM focuses on the occurrence part:
o_t ~ Bernoulli(p_t)
p_t = link(μ_{a,t}, μ_{b,t})
Where p_t ∈ (0, 1) is the probability of non-zero demand, and μ_{a,t}, μ_{b,t} are conditional expectations of unobservable latent variables a_t and b_t. Each follows its own ADAM model - which may be pure ETS, pure ARIMA, or their combination, optionally extended with external regressors. For example, for OM with ETS(M,N,N) sub-model:
a_t = l_{a,t-1}(1 + ε_{a,t})
l_{a,t} = l_{a,t-1}(1 + α_a ε_{a,t})
μ_{a,t} = l_{a,t-1}
And similarly for b_t. The link function and the restrictions on μ_{a,t} and μ_{b,t} determine the occurrence subtype.
Because each sub-model can be ETS, ARIMA, or their ETSX/ARIMAX variants (with regressors), the full class is called OM regardless of internal model type. The notation of specific variants follows the underlying sub-model:
| Underlying sub-model | Name | Example model name |
|---|---|---|
| ETS only | oETS | oETS(MNN)[O] |
| ARIMA only | oARIMA | oARIMA(1,0,0)[O] |
| ETS + regressors | oETSX | oETSX(MNN)[O] |
| ARIMA + regressors | oARIMAX | oARIMAX(1,0,0)[O] |
| ETS + ARIMA | oETS+ARIMA | oETS(MNN)[O]+ARIMA(1,0,0) |
| ETS + ARIMA + regressors | oETS+ARIMAX | oETSX(MNN)[O]+ARIMA(1,0,0) |
All variants are fitted through the same OM / om() interface by combining the model, orders, and formula / X parameters.
| Type | Code | Link formula | Description |
|---|---|---|---|
| OM_F | "fixed" |
p = T₁/T | Constant probability; no optimisation |
| OM_O | "odds-ratio" |
p = μₐ/(μₐ+1) | Logistic-style; μ_b fixed at 1 |
| OM_I | "inverse-odds-ratio" |
p = 1/(1+μ_b) | Inverse logistic; Croston-related; μ_a fixed at 1 |
| OM_D | "direct" |
p = μₐ | Direct probability; TSB-like; μₐ ∈ [0,1] |
| OM_G | "general" |
p = μₐ/(μₐ+μ_b) | Both sub-models evolve - calls OMG
|
| OM_A | "auto" |
- | Automatic selection between the five above via AutoOM
|
Where T₁ is the count of non-zero observations and T is the total number of observations.
Constant probability - no optimisation, no state equation:
o_t ~ Bernoulli(p)
p̂ = T₁/T
For an ETS-based OM_F the sub-model is forced to "ANN" with α = 0.
Sets μ_b = 1:
p_t = μ_{a,t} / (μ_{a,t} + 1)
Equivalent to a logistic transform of the latent state a_t. The a_t sub-model can be ETS, ARIMA, or ETSX/ARIMAX.
Sets μ_a = 1:
p_t = 1 / (1 + μ_{b,t})
Related to Croston's method when 1 + b_t represents demand inter-arrival intervals.
Imposes μ_a + μ_b = 1 with μ_a ∈ [0, 1]:
p_t = μ_{a,t} = min(l_{a,t-1}, 1)
Analogous to the TSB (Teunter–Syntetos–Babai) method, modelling the probability directly as a bounded state.
No restrictions - both a_t and b_t evolve with their own independent ADAM models:
p_t = μ_{a,t} / (μ_{a,t} + μ_{b,t})
Most flexible but requires the most parameters. Implemented via OMG / omg().
iADAM is the model that joins the demand occurrence with the demand sizes parts. Here is an example with the iETS, i.e. model that has ETS components only:
iETS(E,T,S)_X(E,T,S)(E,T,S)
- First brackets: ETS model for demand sizes (
z_tcomponent) - Subscript letter: Occurrence subtype (F, O, I, D, G)
- Second brackets: ETS model for
a_t - Third brackets: ETS model for
b_t(only for OM_G)
Examples:
-
iETS(M,N,N)_F: Fixed probability, MNN for sizes -
iETS(M,M,N)_O(M,N,N): Odds-ratio, MMN for sizes, MNN for occurrence -
iETS(M,N,N)_G(M,N,N)(A,A,N): General, different sub-models fora_tandb_t
When discussing the occurrence part alone, the bracket shows the sub-model type, e.g. oETS(MNN)[O], oARIMA(1,0,0)[O], oETS(MNN)[O]+ARIMA(1,0,0).
OM is the single entry point. It transparently dispatches to specialised classes:
| Call | Returns |
|---|---|
OM(occurrence="fixed") |
OM instance |
OM(occurrence="odds-ratio") |
OM instance |
OM(occurrence="inverse-odds-ratio") |
OM instance |
OM(occurrence="direct") |
OM instance |
OM(occurrence="general") |
OMG instance |
OM(occurrence="auto") |
AutoOM instance (before .fit()); calling .fit() returns the best OM or OMG
|
Users only need to import OM; the other classes (OMG, AutoOM) are available for explicit use.
OM uses the same three-letter ETS codes as ADAM (Error–Trend–Seasonal). The same wildcards apply:
-
"Z"- auto-select from {A, M} -
"X"- auto-select from {N, A, Ad, M, Md} -
"N"- force None
Default: model="ZXZ". Check ADAM for mode details.
For occurrence="fixed" the model is forced to "ANN" (constant probability, α = 0).
OM also supports ARIMA components (via orders, ar_order, i_order, ma_order) and external regressors (via formula / regressors). The oes() R function is an ETS-only wrapper of om() that disables ARIMA and formula support - see OES.
library(smooth)
y <- rbinom(120, 1, 0.6)
# Fixed probability (no smoothing)
m_fixed <- om(y, occurrence="fixed")
# Odds-ratio (logistic link)
m <- om(y, model="MNN", occurrence="odds-ratio")
forecast(m, h=12)
# Inverse odds-ratio (Croston-related)
m <- om(y, model="MNN", occurrence="inverse-odds-ratio")
# Direct (TSB-like)
m <- om(y, model="MNN", occurrence="direct")
# With holdout validation
m <- om(y, model="MNN", occurrence="odds-ratio", h=12, holdout=TRUE)
# With seasonal lags
m <- om(y, model="MNA", lags=c(1, 7), occurrence="odds-ratio")from smooth import OM
import numpy as np
y = np.array([0,1,0,0,1,1,0,1,0,0,1]*10, dtype=float)
# Fixed probability (no smoothing)
m_fixed = OM(occurrence="fixed")
m_fixed.fit(y)
# Odds-ratio (logistic link)
m = OM(model="MNN", occurrence="odds-ratio")
m.fit(y)
fc = m.predict(h=12)
fc.mean # pd.Series of probability forecasts ∈ [0,1]
# Inverse odds-ratio (Croston-related)
m = OM(model="MNN", occurrence="inverse-odds-ratio")
m.fit(y)
# Direct (TSB-like)
m = OM(model="MNN", occurrence="direct")
m.fit(y)
# With holdout validation
m = OM(model="MNN", occurrence="odds-ratio", h=12, holdout=True)
m.fit(y)
# With seasonal lags
m = OM(model="MNA", lags=[1, 7], occurrence="odds-ratio")
m.fit(y)# Pure ARIMA(1,0,0) — oARIMA(1,0,0)[O]
m <- om(y, model="NNN", orders=list(ar=1, i=0, ma=0), occurrence="odds-ratio")
# ETS + ARIMA — oETS(MNN)[O]+ARIMA(1,0,0)
m <- om(y, model="MNN", orders=list(ar=1, i=0, ma=0), occurrence="odds-ratio")# Pure ARIMA(1,0,0) — oARIMA(1,0,0)[O]
m = OM(model="NNN", ar_order=1, occurrence="odds-ratio")
m.fit(y)
# ETS + ARIMA — oETS(MNN)[O]+ARIMA(1,0,0)
m = OM(model="MNN", ar_order=1, occurrence="odds-ratio")
m.fit(y)X <- matrix(runif(120), ncol=1)
colnames(X) <- "x1"
# oETSX — ETS with fixed regressor
m <- om(y, model="MNN", occurrence="odds-ratio", formula=~x1, xreg=X)
# Automatic regressor selection
m <- om(y, model="MNN", occurrence="odds-ratio", formula=~x1, xreg=X,
regressors="select")X = np.random.rand(len(y), 1)
# oETSX — ETS with fixed regressor
m = OM(model="MNN", occurrence="odds-ratio", regressors="use")
m.fit(y, X=X)
# Automatic regressor selection
m = OM(model="MNN", occurrence="odds-ratio", regressors="select")
m.fit(y, X=X)# Same ETS model for both sub-models
m <- omg(y, modelA="MNN", modelB="MNN", h=12)
# Different ETS models for a_t and b_t
m <- omg(y, modelA="MNN", modelB="AAN", h=12)
# Access individual sub-model states
m$modelA$states # sub-model A states
m$modelB$states # sub-model B states
# Seasonal data
m <- omg(y, modelA="MNA", modelB="MNA", lags=c(1, 7))from smooth import OMG
# Same model for both sub-models
m = OMG(model_a="MNN", model_b="MNN")
m.fit(y)
print(m.model_name) # "oETS[G](MNN)(MNN)"
# Different models for a_t and b_t
m = OMG(model_a="MNN", model_b="AAN")
m.fit(y)
# Access individual sub-model fitted values
m.model_a.fitted # sub-model A probabilities (odds-ratio side)
m.model_b.fitted # sub-model B probabilities (inverse-odds-ratio side)
# Seasonal data
m = OMG(model_a="MNA", model_b="MNA", lags=[1, 7])
m.fit(y)# Try all types, select best by AICc
m <- auto.om(y, model="ZXZ", ic="AICc")
m$occurrence # winning type, e.g. "odds-ratio"
forecast(m, h=12)
# Restrict candidates to a subset
m <- auto.om(y, model="MNN",
occurrence=c("odds-ratio", "inverse-odds-ratio", "general"),
ic="AICc")from smooth import AutoOM
# Try all types, select best by AICc — returns OM or OMG directly
m = AutoOM(model="ZXZ", ic="AICc").fit(y)
print(m.model_name) # e.g. "oETS(MNN)[O]"
print(m.time_elapsed_) # total selection time in seconds
fc = m.predict(h=12)
# Restrict candidates to a subset
m = AutoOM(model="MNN",
occurrence=["odds-ratio", "inverse-odds-ratio", "general"]).fit(y)Support for the inference/output methods differs between the R and Python implementations:
| Method | R om
|
R omg
|
Python OM
|
Python OMG
|
|---|---|---|---|---|
print() / str()
|
✓ | ✓ | ✓ | ✓ |
coef() / .coef (+ .coef_names) |
✓ | ✓ | ✓ | ✓ |
vcov() |
✓ (vcov.om) |
✓ (vcov.omg) |
✗ | ✗ |
confint() |
✓ (via confint.adam) |
✓ (confint.omg) |
✗ | ✗ |
summary() |
✓ (summary.om) |
✓ (summary.omg) |
✗ | ✗ |
Python limitation:
vcov(),confint()andsummary()are not yet available for Python occurrence models.OMinherits them fromADAM, but they currently raiseValueError: Unknown distribution: plogisbecause the cumulative-logistic occurrence distribution is not handled in the Fisher Information path;OMGdoes not define them at all. For Gaussian-family models useADAM— see Coefficients-and-Parameters and Visualisation-and-Output.
y <- rbinom(120, 1, 0.6)
# Single occurrence model
m <- om(y, model="MNN", occurrence="odds-ratio")
summary(m) # estimates, standard errors, confidence intervals
vcov(m) # covariance matrix (inverted Fisher Information)
confint(m) # 95% confidence intervals
# General model: joint covariance / intervals over both sub-models
g <- omg(y, modelA="MNN", modelB="MNN")
summary(g) # A: / B: prefixed parameter blocks
confint(g, level=0.9)from smooth import OM, OMG
m = OM(model="MNN", occurrence="odds-ratio")
m.fit(y)
print(m) # concise model report
m.coef # estimated parameter vector (NDArray)
m.coef_names # matching parameter labels
g = OMG(model_a="MNN", model_b="MNN")
g.fit(y)
print(g.model_name) # "oETS[G](MNN)(MNN)"
g.model_a.fitted # sub-model A probabilities
g.model_b.fitted # sub-model B probabilities| Parameter | Type (R) | Type (Python) | Default (R / Python) | Description |
|---|---|---|---|---|
data / y
|
vector/ts | NDArray | - | Binary or non-binary series; binarised automatically (non-zero → 1) |
model |
character | str | "ZXZ" |
ETS specification or wildcard |
lags |
vector | int/List[int]/None |
frequency(y) / [1]
|
Seasonal period(s) |
orders |
list | Dict/None | zeros | ARIMA orders as list(ar=, i=, ma=) / {"ar":, "i":, "ma":}
|
ar_order |
- | int/List[int] | 0 | AR order(s) (Python alternative to orders) |
i_order |
- | int/List[int] | 0 | Integration order(s) |
ma_order |
- | int/List[int] | 0 | MA order(s) |
occurrence |
character | str |
"auto" / "odds-ratio"
|
Link function type; see link table above |
constant |
logical | bool | FALSE / False | Include constant/drift term |
formula |
formula | str/None | NULL / None | External regressors formula |
regressors |
character | str | "use" |
Regressor handling: "use", "select", or "adapt"
|
persistence |
vector | Dict[str,float]/None | NULL / None | Fixed smoothing parameters; keys "alpha", "beta", "gamma"
|
phi |
numeric | float/None | NULL / None | Damping parameter |
initial |
character | str/Dict | "backcasting" |
Initialisation: "backcasting", "optimal", "two-stage", "complete"
|
arma |
list | Dict/None | NULL / None | Fixed ARMA coefficients |
loss |
character | str | "likelihood" |
"likelihood" (Bernoulli) or "MSE"
|
ic |
character | str | "AICc" |
Information criterion: "AIC", "AICc", "BIC", "BICc"
|
bounds |
character | str | "usual" |
Parameter bounds: "usual", "admissible", "none"
|
h |
integer | int | 0 | Forecast horizon |
holdout |
logical | bool | FALSE / False | Hold out last h observations for validation |
verbose |
- | int | 0 | Verbosity level (Python only; R uses silent) |
nlopt_kargs |
... |
Dict/None | NULL / None | Advanced NLopt options (Python) / extra ... args (R) |
ets |
character | str | "conventional" |
ETS formulation: "conventional" (Hyndman et al. 2008) or "adam" (ADAM reformulation). Experimental. |
All om() / OM parameters apply to each sub-model. The following are unique to omg() / OMG, exposing per-sub-model control via _a / _b suffixes:
| Parameter (R) | Parameter (Python) | Default (R / Python) | Description |
|---|---|---|---|
modelA |
model_a |
"MNN" |
ETS spec for sub-model A (odds-ratio side) |
modelB |
model_b |
same as modelA / model_a
|
ETS spec for sub-model B (inverse-odds-ratio side) |
ordersA |
orders_a |
zeros | ARIMA orders for sub-model A |
ordersB |
orders_b |
same as ordersA / orders_a
|
ARIMA orders for sub-model B |
constantA |
constant_a |
FALSE / False | Constant/drift for sub-model A |
constantB |
constant_b |
FALSE / False | Constant/drift for sub-model B |
persistenceA |
persistence_a |
NULL / None | Fixed smoothing for sub-model A |
persistenceB |
persistence_b |
NULL / None | Fixed smoothing for sub-model B |
phiA |
phi_a |
NULL / None | Damping for sub-model A |
phiB |
phi_b |
NULL / None | Damping for sub-model B |
armaA |
arma_a |
NULL / None | Fixed ARMA coefficients for sub-model A |
armaB |
arma_b |
NULL / None | Fixed ARMA coefficients for sub-model B |
formulaA |
formula_a |
NULL / None | Regressor formula for sub-model A |
formulaB |
formula_b |
NULL / None | Regressor formula for sub-model B |
regressorsA |
regressors_a |
"use" |
Regressor handling for sub-model A |
regressorsB |
regressors_b |
"use" |
Regressor handling for sub-model B |
All om() / OM parameters apply and are forwarded to every candidate model (including both sub-models of the general/OMG candidate). The following are unique to auto.om() / AutoOM:
| Parameter (R) | Parameter (Python) | Type (R) | Type (Python) | Default (R / Python) | Description |
|---|---|---|---|---|---|
occurrence |
occurrence |
character vector | List[str] | all 5 types | Occurrence types to compare; subset to restrict candidates |
parallel |
— | logical/integer | — | FALSE | Fit candidates in parallel using foreach (R only) |
ets |
ets |
character | str | "conventional" |
ETS formulation forwarded to every candidate: "conventional" or "adam"
|
| Attribute (R) | Attribute (Python) | Type | Description |
|---|---|---|---|
modelName(m) |
m.model_name |
str | Full name, e.g. "oETS(MNN)[O]"
|
fitted(m) |
m.fitted |
NDArray | In-sample probability estimates ∈ (0,1) |
residuals(m) |
m.residuals |
NDArray | o_t − p̂_t |
m$states |
m.states |
NDArray | State matrix (components × T+lags) |
m$persistence |
m.persistence_vector |
Dict | Smoothing parameters {"alpha": ..., ...}
|
m$phi |
m.phi_ |
float/None | Damping parameter |
m$transition |
m.transition |
NDArray | Transition matrix F |
m$measurement |
m.measurement |
NDArray | Measurement matrix W |
m$initial |
m.initial_value |
Dict | Initial states |
coef(m) |
m.coef |
NDArray | Estimated parameter vector B |
logLik(m) |
m.loglik |
float | Log-likelihood (Bernoulli) |
AIC(m) |
m.aic |
float | AIC |
AICc(m) |
m.aicc |
float | Corrected AIC |
BIC(m) |
m.bic |
float | BIC |
BICc(m) |
m.bicc |
float | Corrected BIC |
m$lossValue |
m.loss_value |
float | Value of loss function |
m$distribution |
m.distribution_ |
str | Always "plogis"
|
m$scale |
m.scale / m.sigma
|
float |
nan (no scale for Bernoulli) |
m$occurrence |
m._om_occurrence |
str | Occurrence type used |
nobs(m) |
m.nobs |
int | Number of in-sample observations |
m$accuracy |
m.accuracy |
Dict/None | Holdout accuracy (when holdout=True) |
OM inherits all ADAM attributes; see ADAM for the full list.
omg() / OMG returns an object with all standard #OM-Fitted-Attributes plus:
| Attribute (R) | Attribute (Python) | Description |
|---|---|---|
m$modelA |
m.model_a |
Fitted om / OM sub-model A (odds-ratio side) |
m$modelB |
m.model_b |
Fitted om / OM sub-model B (inverse-odds-ratio side) |
modelName(m) |
m.model_name |
"oETS[G](MNN)(MNN)" |
fitted(m) |
m.fitted |
Combined probability a_t/(a_t + b_t)
|
coef(m) |
m.coef |
Joint parameter vector [B_A, B_B]
|
auto.om() / AutoOM.fit() returns the best-fitting om / OM or omg / OMG object directly. All standard #OM-Fitted-Attributes or #omg--OMG--Additional-Fitted-Attributes fitted attributes are available on the returned model. One additional attribute is set:
| Attribute (R) | Attribute (Python) | Description |
|---|---|---|
m$timeElapsed |
m.time_elapsed_ |
Total selection time (seconds) |
# om()
m <- om(y, model="MNN", occurrence="odds-ratio")
modelName(m) # "oETS(MNN)[O]"
fitted(m) # in-sample probabilities
residuals(m) # o_t - p̂_t
m$states # state matrix
m$persistence # smoothing parameters
logLik(m)
AIC(m); AICc(m); BIC(m)
coef(m)
forecast(m, h=12)
# omg()
g <- omg(y, modelA="MNN", modelB="MNN")
g$modelA # fitted sub-model A
g$modelB # fitted sub-model B
fitted(g) # combined probability p_t = a/(a+b)
logLik(g); AIC(g)
# auto.om() — returns the best om or omg directly
a <- auto.om(y, model="ZXZ")
a$occurrence # selected type, e.g. "odds-ratio"
a$timeElapsed # selection time in seconds
forecast(a, h=12)# OM
m = OM(model="MNN", occurrence="odds-ratio")
m.fit(y)
m.model_name # "oETS(MNN)[O]"
m.fitted # in-sample probability estimates
m.residuals # o_t - p̂_t
m.states # state matrix
m.persistence_vector # {"alpha": ..., ...}
m.aic; m.aicc; m.bic; m.bicc
fc = m.predict(h=12)
fc.mean # pd.Series of probability forecasts ∈ [0,1]
# OMG
g = OMG(model_a="MNN", model_b="MNN")
g.fit(y)
g.model_a.fitted # sub-model A probabilities
g.model_b.fitted # sub-model B probabilities
g.fitted # combined probability p_t = a/(a+b)
g.loglik; g.aic
# AutoOM — fit() returns the best OM or OMG directly
a = AutoOM(model="ZXZ").fit(y)
a.model_name # e.g. "oETS(MNN)[O]"
a.time_elapsed_ # total selection time in seconds
fc = a.predict(h=12)OM.predict() returns probability forecasts (mean ∈ [0,1]). The interval and level parameters are accepted for API compatibility but interval estimation for binary occurrence is not currently supported.
To forecast full intermittent demand (occurrence × demand sizes), pass the fitted OM as the occurrence argument of ADAM.fit():
occ_model = OM(model="MNN", occurrence="odds-ratio")
occ_model.fit(y)
from smooth import ADAM
demand_model = ADAM(model="MNN")
demand_model.fit(y, occurrence=occ_model)
fc = demand_model.predict(h=12)In R:
occ_model <- om(y, model="MNN", occurrence="odds-ratio")
demand_model <- adam(y, "MNN", occurrence=occ_model, h=12)- Svetunkov, I. (2023). Forecasting and Analytics with ADAM. Chapter 13: https://openforecast.org/adam/ADAMIntermittent.html
- Svetunkov, I. & Boylan, J.E. (2023). iETS: State space model for intermittent demand forecasting. International Journal of Production Economics, 265, 109013. DOI: 10.1016/j.ijpe.2023.109013
- ADAM - full iETS model (occurrence + demand sizes in one fit)
-
OES - R-only
oes()/oesg()ETS-only wrapper interface -
Fitted-Values-and-Forecasts -
predict()output format -
Residuals-and-Errors -
residuals(),rmultistep(),outlierdummy()