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

OM - Occurrence Model

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.

Function signatures

R

om(data, model = "ZXZ", lags = c(frequency(data)),
   orders = list(ar = c(0), i = c(0), ma = c(0), select = FALSE),
   constant = FALSE, formula = NULL,
   regressors = c("use", "select", "adapt"),
   occurrence = c("auto", "fixed", "odds-ratio", "inverse-odds-ratio",
                  "direct", "general"),
   loss = c("likelihood", "MSE", "MAE", "HAM", "LASSO", "RIDGE"),
   h = 0, holdout = FALSE,
   persistence = NULL, phi = NULL,
   initial = c("backcasting", "optimal", "two-stage", "complete"),
   arma = NULL,
   ic = c("AICc", "AIC", "BIC", "BICc"),
   bounds = c("usual", "admissible", "none"),
   ets = c("conventional", "adam"),
   silent = TRUE, ...)

omg() (general two-sub-model OM) and auto.om() (automatic occurrence-type selection) share the same parameter style; see OMG and AutoOM sections below.

Python

class OM(ADAM):
    def __init__(
        self,
        model: str | list[str] = "ZXZ",
        lags: list[int] | None = None,
        ar_order: int | list[int] = 0,
        i_order: int | list[int] = 0,
        ma_order: int | list[int] = 0,
        orders: dict | None = None,
        constant: bool = False,
        formula: str | None = None,
        regressors: Literal["use", "select", "adapt"] = "use",
        occurrence: str = "odds-ratio",
        loss: str | Callable = "likelihood",
        reg_lambda: float | None = None,
        h: int = 0,
        holdout: bool = False,
        persistence: dict | float | None = None,
        phi: float | None = None,
        initial: dict | str = "backcasting",
        n_iterations: int | None = None,
        arma: dict | None = None,
        ic: Literal["AIC", "AICc", "BIC", "BICc"] = "AICc",
        bounds: Literal["usual", "admissible", "none"] = "usual",
        verbose: int = 0,
        nlopt_kargs: dict | None = None,
        ets: Literal["conventional", "adam"] = "conventional",
        **kwargs,
    ) -> None: ...

Passing occurrence="auto" or occurrence="general" to OM(...) returns an OMG instance via __new__ redirect; otherwise it returns an OM. AutoOM(...).fit(...) returns the best OM or OMG per the R auto.om() convention.

Mathematical Framework

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.

Model Variants

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.

Link Functions (Occurrence Types)

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.

OM_F (Fixed)

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.

OM_O (Odds-Ratio)

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.

OM_I (Inverse Odds-Ratio)

Sets μ_a = 1:

p_t = 1 / (1 + μ_{b,t})

Related to Croston's method when 1 + b_t represents demand inter-arrival intervals.

OM_D (Direct)

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.

OM_G (General)

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 Full Model Notation

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_t component)
  • 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 for a_t and b_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).

Class Routing (Python)

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.

ETS Model Specification

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.

Usage

om() / OM — Basic Models

R

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")

Python

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)

om() / OM — ARIMA Components

R

# 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")

Python

# 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)

om() / OM — External Regressors

R

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")

Python

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)

omg() / OMG — General Model

R

# 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))

Python

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)

actuals() on omg

actuals(omg_obj) (R) and OMG.actuals (Python) return the binary occurrence indicator (0/1) built from the original input series, with the same class (ts / zoo / numeric) as actuals(om(y)) would on the same series. Previously the omg-level actuals lost the ts/zoo metadata; the top-level omg object now stores the input series under $data so the class is preserved.

auto.om() / AutoOM — Automatic Selection

R

# 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")

Python

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)

Inference and Output

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() and summary() are not yet available for Python occurrence models. OM inherits them from ADAM, but they currently raise ValueError: Unknown distribution: plogis because the cumulative-logistic occurrence distribution is not handled in the Fisher Information path; OMG does not define them at all. For Gaussian-family models use ADAM — see Coefficients-and-Parameters and Visualisation-and-Output.

R

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)

Python

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

Parameters

OM Core Parameters

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 / function str / Callable "likelihood" Optimisation objective. Single-step strings: "likelihood" (Bernoulli on the predicted probability), "MSE", "MAE", "HAM" (on the probability-scale residual ot - p_t), "LASSO" / "RIDGE" (regularised — pair with lambda / reg_lambda). A callable (actual, fitted, B) → scalar is also accepted for a custom loss. See Loss-Functions for the full menu and formulas.
lambda (R) / reg_lambda (Python) numeric float / None 0 Penalty weight for "LASSO" / "RIDGE"0 is unregularised, 1 is pure penalty. Acts on the parameter vector B (for OM) or the joint concat(B_A, B_B) (for OM).
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.

omg() / OMG — Additional Parameters

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

auto.om() / AutoOM — Additional Parameters

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"

Fitted Attributes

OM Fitted Attributes

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 — Additional Fitted Attributes

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 — Fitted Attributes

auto.om() / AutoOM.fit() returns the best-fitting om / OM or omg / OMG object directly. All standard OM or OMG 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)

Accessing Elements

R

# 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)

Python

# 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)

Note on Interval Forecasts

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)

References

See Also

Clone this wiki locally