cultivars.univariate.box_jenkins#

ARMA / ARIMA / SARIMA / SARIMAX – one engine behind four public fronts.

All four are the same estimator. The conditional mean is the multiplicative seasonal ARMA

\[\phi(L)\,\Phi(L^s)\,\bigl(w_t - x_t'\beta\bigr) = \theta(L)\,\Theta(L^s)\,\varepsilon_t, \qquad w_t = (1 - L)^d (1 - L^s)^D y_t, \qquad \varepsilon_t \sim N(0, \sigma^2),\]

with \(\theta(L) = 1 + \theta_1 L + \cdots\) (the plus sign convention), cast in the Harvey state-space form so the likelihood is exact and runs through the Kalman filter,

\[\log L = -\tfrac12 \sum_{t} \bigl(\log 2\pi + \log F_t + v_t^2 / F_t\bigr),\]

with \(v_t\) and \(F_t\) the filter’s innovations and their variances. Integration is applied by differencing before estimation; deterministic terms and exogenous regressors enter the observation intercept, which makes ARIMAX and SARIMAX nothing more than ARIMA and SARIMA with exog supplied. Stationarity and invertibility are enforced structurally by the partial-autocorrelation reparameterization, so the optimizer searches an unconstrained space and the reported diagnostics verify the transform rather than describe the data – a unit root fitted this way lands at a root of 0.999 with Stationary: True, and d is the right answer, not a different optimizer.

Two commitments shape the surface. First, SARIMAX is the real specification and the other four names are narrowing constructors over it, not separate models: a user who reaches for ARMA cannot pass a differencing order, one who reaches for SARIMA need not spell out (0, 0, 0, 0), and one who reaches for ARIMAX must supply regressors, so an import path reads as the specification it is and the four fits of one model return identical likelihoods. Second, both stability verdicts are assessed on the multiplied polynomials \(\phi(L)\Phi(L^s)\) and \(\theta(L)\Theta(L^s)\); checking the non-seasonal block alone would pass a specification with an explosive seasonal root, and the result exposes the expanded polynomials so the verdict can be audited.

Layout. SARIMAX validates order and seasonal_order through validate_order_tuple, the trend through validate_choice against Trend, and the regressors through validate_exog, all on the _BoxJenkinsModel base in _internals; fit builds a _BoxJenkinsObjective whose state_space maps a parameter vector to a _LinearGaussianStateSpace through _from_arma, and hands it to _maximize_likelihood. ARIMAX, SARIMA, ARIMA and ARMA fix or forbid arguments and delegate. The packed _BoxJenkinsFit is assembled into ARMAResult by _from_fit; expand_ar and expand_ma in _core multiply the polynomials for the stability and invertibility checks and for simulate, which runs _simulate_arma and undoes the differencing with _integrate; combined_difference applies it, and deterministic_columns and n_deterministic lay out the trend block. The pure autoregression with conditional least squares is autoregression; the long-memory generalization fractional_integration; the permanent-transitory reading of an ARIMA(p, 1, q) result BeveridgeNelsonDecomposition.

References

Box, G. E. P., Jenkins, G. M., Reinsel, G. C., & Ljung, G. M. (2015). Time Series Analysis: Forecasting and Control, 5th ed. Wiley.

Harvey, A. C. (1989). Forecasting, Structural Time Series Models and the Kalman Filter. Cambridge University Press.

Durbin, J., & Koopman, S. J. (2012). Time Series Analysis by State Space Methods, 2nd ed., chapter 3. Oxford University Press.

Monahan, J. F. (1984). A note on enforcing stationarity in ARMA models. Biometrika, 71(2), 403-404.

Example

One model, four fronts, one likelihood; then order selection on a common sample:

>>> import numpy as np
>>> rng = np.random.default_rng(0)
>>> e = rng.standard_normal(500)
>>> y = np.zeros(500)
>>> for t in range(1, 500):
...     y[t] = 0.6 * y[t - 1] + e[t] + 0.3 * e[t - 1]
>>> fits = (
...     ARMA(y, order=(1, 1)).fit(),
...     ARIMA(y, order=(1, 0, 1)).fit(),
...     SARIMA(y, order=(1, 0, 1), seasonal_order=(0, 0, 0, 0)).fit(),
...     SARIMAX(y, order=(1, 0, 1)).fit(),
... )
>>> len({round(fit.llf, 6) for fit in fits})
1
>>> candidates = [ARMA(y, order=(p, q)).fit() for p in (1, 2) for q in (0, 1)]
>>> table = candidates[0].compare(*candidates[1:], criterion="bic")
>>> table.rows[0][0] in {"ARIMA(1, 0, 1)", "ARIMA(2, 0, 0)"}
True

Classes#

ARIMA

ARIMA(p, d, q): non-seasonal differencing, no seasonal block, no regressors.

ARIMAX

ARIMA(p, d, q) with exogenous regressors, no seasonal block.

ARMA

Stationary ARMA(p, q): no differencing, no seasonal block, no regressors.

SARIMA

Multiplicative seasonal ARIMA, without exogenous regressors.

SARIMAX

Seasonal ARIMA with exogenous regressors -- the general specification.

ARMAResult

A fitted (seasonal) ARIMA, optionally with exogenous regressors.