Skip to content

statsmodels time-series models

The mapi-plugin-statsmodels package adds twelve classical time-series models from statsmodels (BSD-3-Clause) to the platform. These are the model groups:

  • The usual forecast models (ARIMA, exponential smoothing, theta).
  • Models with drivers (ARIMAX, ARDL, error correction).
  • Systems of series that have an effect on each other (VAR, VECM, VARMAX, dynamic factors).
  • Regime switching.

All of these models fit in seconds. They run in the time-series runner image (mapi/base-forecast). They pass the same contract kit as all other plugins.

Which model should I use?

Each model has an About card in the catalog. Help me choose in the web app, mapi choose and api.catalog.recommend() rank the models for your goal and data (see Choosing a model). This table gives a short summary:

Plugin In plain words Choose it when
ts.sarimax Forecast with past values and drivers You forecast one series a few periods ahead and know the future prices, promotions or holidays
ts.arima Forecast from past values You have only the series itself
ts.ets Exponential smoothing forecast You need a fast, robust baseline with trend and season
ts.theta Simple strong baseline You want a simple model that is difficult to beat, to compare other models with
ts.unobserved_components Trend, season and noise You want to see the underlying trend without the noise
ts.ardl Effects that increase over time The effect of a driver spreads over many periods
ts.ecm Long-run and short-run effects The result and its drivers have the same trend. You want the immediate effect and the lasting effect.
ts.var Series that have an effect on each other Many series have an effect on each other. You want shocks and Granger tests.
ts.vecm Series that stay tied together Series with a trend keep a stable long-run relation
ts.varmax Series that have an effect on each other, with drivers Like VAR, with outside drivers that you know ahead
ts.dynamic_factor Common movements across many series Many series follow a few hidden common forces
ts.markov_switching Changes between regimes The behaviour jumps between different modes (normal / with promotions)

Is the series ready? If you are not sure, use the time-series readiness workflow template. It does these steps:

  1. It runs the unit-root tests.
  2. It selects the differencing and the AR/MA orders.
  3. It does a backtest of ts.sarimax against a naive forecast.
  4. It keeps ts.sarimax only if it wins.

See Workflows.

Your data, in the model's names

The models use canonical names. Bind the names of your dataset to them in data.bind:

Canonical name What it is
t The time index: dates (default) or period numbers (settings.time_kind: integer). Policy extend: forecasts continue it.
y The target. Univariate models: y(t), or y(<series...>, t) for panels. Multivariate models: y(variable, t).
variable Multivariate models only: the series that the model uses together (realgdp, realcons, ...).
<series...> Panel dims that you name in settings.series_dims (geo, product). The plugin fits one model for each series.
<exog...> Drivers that you name in settings.exog, as (<series...>, t). Forecasts need their future values.

A table with one column for each series (realgdp, realcons, realinv) becomes y(variable, t) with the wide variables of the ingest mapping. See From a spreadsheet to a Guided draft.

Panels are a per-series loop

With series_dims: [geo], each geo gets its own model, with its own orders, parameters and residual checks. Geos are a fixed dim. The platform refuses a forecast for a geo that the model did not see, with UNSEEN_LEVELS. Multivariate models do the same loop. For example, there is one joint VAR for each geo.

An ARIMAX per geo

from mapi_plugin_statsmodels.datasets import sales_dataset, sales_schema
from mapi_sdk.harness import fit_spec, score_bundle
from mapi_spec import load_score_request

SPEC = """
apiVersion: mapi/v1
kind: ModelSpec
metadata: {name: weekly-sales-arimax, project: retail}
data:
  dataset: dataset://retail/weekly_sales@latest
  bind:
    dims: {t: date}            # the model's time dim is called t
    vars: {y: sales}           # ... and its target y
  split: {kind: time_holdout, dim: t, last: 8}
model:
  use: ts.sarimax@^1
  settings:
    order: [1, 0, 0]           # one past value (AR 1), no differencing, no MA term
    exog: [price, promo]       # drivers you know ahead
    series_dims: [geo]         # one model per geo
  fit: {mode: evaluate_then_refit, seed: 1}
evaluate: {metrics: [rmse, mape, coverage_90]}
"""

history = sales_dataset(periods=104, geos=["north", "south"], seed=0)
bundle = fit_spec(SPEC, history, raw_schema=sales_schema(geo=True))
print(bundle.metrics.values)  # holdout metrics of the evaluation fit
params = bundle.model.groups["params"]
print(params["estimate"].sel(param=["price", "promo"]).round(2).values)
# [[-7.96 11.62] [-7.32 12.18]] - per geo, close to the true -8 and +12

The params group of the bundle is the parameter table. For each parameter of each series, it gives the estimate, standard error, z/t statistic, p-value and 95% interval.

Forecasts and the drivers' future

plan = sales_dataset(periods=112, geos=["north", "south"], seed=0).isel(date=slice(104, None))
request = load_score_request("""
apiVersion: mapi/v1
kind: ScoreRequest
model: endpoint://retail/weekly-sales-arimax
mode: forecast
horizon: {dim: t, periods: 8}
output: {summary: [median, q5, q95]}
""")
forecast = score_bundle(bundle, request, plan.drop_vars("sales"))
print(forecast["summary"].to_dataset()["y_hat"].sel(geo="north").round(1).values)
# rows median, q5, q95 - the 90% prediction interval around the median

For all models, a score gives these results:

  • In the fitted range: the one-step-ahead predictions of the model from the fitted history. The model does not use the targets and drivers of the payload there. The only exception is a forecast with actuals (see below).
  • After the fitted range: a multi-step forecast from the end of the fit, with the drivers in the payload. If a driver or a value of a driver is missing, the score fails with MISSING_FUTURE_COVARIATE (422). With future_covariates: carry_last, seasonal_naive or zero, the platform extends the drivers that you do not give (see Forecasting).

A what-if on the future is another payload. Change the planned drivers, for example a promotion each week or a 5% price increase. Then forecast again.

The outputs are quantiles (y_hat(quantile, ..., t), with the representation quantiles). The default quantiles are 2.5%, 5%, 10%, 25%, 50%, 75%, 90%, 95% and 97.5%. Thus, you get the median and the 50/80/90/95% bands. settings.quantiles changes them, but the outputs always include 5%, 50% and 95%.

The quantiles are prediction intervals for the two output kinds. They include the noise of the next value, because statsmodels has no separate interval for the expected value. Each quantile is for one cell. Thus, the platform refuses totals across geos or series (CAPABILITY_UNSUPPORTED). To get a total, add the series in the pipeline and model the total.

The plugin does not give the score modes counterfactual, simulate and filter (MODE_NOT_SUPPORTED). To update a forecast with new data, use a forecast with actuals.

Forecasting from new actuals

All statsmodels models can move their forecast origin forward without a new fit (capabilities.conditions_on_actuals). The rules are on Forecasts with actuals. Send the periods that came after the fit, with their target values and drivers. Then send the periods to forecast, with an empty target:

import json

# Weeks 105-108 arrived for north, 105-106 for south: send them with the weeks to forecast
# (sales left empty) and the planned drivers. Nothing is refitted.
latest = sales_dataset(periods=112, geos=["north", "south"], seed=0).isel(date=slice(104, None))
weeks = latest["date"].values
latest["sales"].loc[{"geo": "north", "date": weeks[4:]}] = float("nan")
latest["sales"].loc[{"geo": "south", "date": weeks[2:]}] = float("nan")
updated = score_bundle(bundle, request, latest)
print(json.loads(updated.attrs["conditioning"])["message"])
# Forecast updated with 6 actuals (2024-12-29 to 2025-01-19) across 2 of 2 series after the
# fitted history, which ends 2024-12-22; the model's parameters were not re-estimated. ...
north = updated["summary"].to_dataset()["y_hat"].sel(geo="north")
print(north.sel(date=weeks[4:]).round(1).values)  # the forecast from north's last actual

A univariate model updates each series with its own actuals. Thus, the series of a panel can end at different dates. The answer includes all the periods that you sent. It gives one-step-ahead predictions on the actuals, and the forecast after the last actual of each series. Actuals for periods in the fitted range replace the fitted values (revised data).

The fitted parameters never change. The fitted noise variance, and thus the width of the bands, also never changes. This table shows how each model uses the new data:

Model Updated with the actuals by
ts.sarimax, ts.arima, ts.unobserved_components, ts.varmax, ts.dynamic_factor the Kalman filter with the fitted parameters over the history and the actuals. This is statsmodels' results.append(new, refit=False). Differencing and seasonal differencing are part of the model. Thus, actuals and forecasts are in the units of the data. The filter uses empty periods in the actuals as missing values (the univariate state-space models accept missing targets).
ts.ets the exponential-smoothing recursions with the fitted smoothing weights and initial states
ts.theta the fitted SES weight (or IMA(1,1) filter with use_mle), drift and seasonal indices
ts.ardl, ts.ecm the fitted equation on the lags of the result and the drivers that you supply (apply(refit=False); the ECM uses its levels ARDL)
ts.var, ts.vecm the fitted equations on the lags that you supply (levels for the VECM). The deterministic trend continues.
ts.markov_switching the Hamilton filter with the fitted parameters. The regime probabilities after the last actual start the forecast.

Joint models of series (ts.var, ts.vecm, ts.varmax, ts.dynamic_factor) need a value of each series for each period with actuals after the fit. If not, the score fails with ACTUALS_INVALID. The platform applies the preprocessing steps of the spec (for example a core.log1p or a standardisation) to the actuals before the update. It reverses them on the forecast, as for all payloads.

A transform.log, transform.box_cox or transform.difference of a workflow is not such a step. A model that you fit on its output refuses actuals (see Forecasts with actuals). Use the differencing of the model (order d, seasonal D) instead. Fit the model again from time to time, so that the parameters learn from the new data.

The models

Each section starts with the About card of the model. The web app, mapi about and api.catalog.about() show this card. A docs test makes sure that the quotes are the same as the catalog. Then each section gives a minimal spec. For models with drivers, it also gives a forecast from planned drivers.

All the specs use this small wrapper. The wrapper has the data binding and a holdout of the last 8 periods. The plugin scores the holdout before it fits the model again on all the data. The specs use synthetic or public data from mapi_plugin_statsmodels.datasets. The documentation tests run each spec (docs/snippets/statsmodels_catalog.py):

from mapi_plugin_statsmodels.datasets import (
    ECM_SCHEMA,
    GROWTH_SCHEMA,
    MACRO_SCHEMA,
    REGIME_SCHEMA,
    ecm_dataset,
    macro_dataset,
    macro_growth,
    regime_dataset,
    sales_dataset,
    sales_schema,
)
from mapi_sdk.harness import fit_spec, score_bundle
from mapi_spec import load_score_request


def spec(model: str, bind: str = "{dims: {t: date}, vars: {y: sales}}") -> str:
    """A complete ModelSpec around a ``model:`` block: the data binding and a holdout of the
    last 8 periods, scored before the model is refitted on everything."""
    return f"""
apiVersion: mapi/v1
kind: ModelSpec
metadata: {{name: example, project: growth}}
data:
  dataset: dataset://growth/example@latest
  bind: {bind}
  split: {{kind: time_holdout, dim: t, last: 8}}
model:
{model}
  fit: {{mode: evaluate_then_refit, seed: 1}}
evaluate: {{metrics: [rmse, mape, coverage_90]}}
"""


def forecast(bundle, future, periods: int = 8):  # type: ignore[no-untyped-def]
    """Forecast ``periods`` ahead from planned drivers (the future rows of ``future``)."""
    request = load_score_request(f"""
apiVersion: mapi/v1
kind: ScoreRequest
model: endpoint://growth/example
mode: forecast
horizon: {{dim: t, periods: {periods}}}
output: {{summary: [median, q5, q95]}}
""")
    return score_bundle(bundle, request, future)["summary"].to_dataset()


weekly = sales_dataset(periods=112, seed=0)  # sales(date) with price and promo
history, plan = weekly.isel(date=slice(0, 104)), weekly.isel(date=slice(104, None))
panel = sales_dataset(periods=104, geos=["north", "south"], seed=1)  # sales(geo, date)

ts.sarimax - forecast with past values and drivers

Forecasts a series from its recent past, its seasonal pattern and outside drivers that you know in advance, such as planned prices or holidays.

Use it when you forecast one regular series a few periods ahead, and known future drivers (prices, promotions, holidays) are important. Use a different model when you have less than about 50 periods, or when many series have an effect on each other (use ts.var).

SARIMAX = """
  use: ts.sarimax@^1
  settings:
    order: [1, 0, 0]          # AR(1) errors, no differencing, no MA term
    exog: [price, promo]      # drivers you know ahead
"""
sarimax = fit_spec(spec(SARIMAX), history, raw_schema=sales_schema())
fan = forecast(sarimax, plan.drop_vars("sales"))  # next 8 weeks from the planned price/promo
print(fan["y_hat"].sel(stat="median").round(1).values)

This is seasonal ARIMA with drivers, that is, a regression with ARIMA errors. The Kalman filter estimates it by exact maximum likelihood. Missing target values are permitted (the filter skips them). The forecast needs the planned future values of the drivers (here plan without sales). If one is missing, the forecast fails with MISSING_FUTURE_COVARIATE. The exception is when the request tells how to extend the driver.

Setting Default Meaning
order [1, 0, 0] (p, d, q): past values, times differenced, past errors
seasonal_order [0, 0, 0, 0] (P, D, Q, s) for a pattern that repeats each s periods
trend auto n, c, t, ct. auto is a constant, unless the series is differenced.
exog [] drivers
series_dims [] panel dims: one model for each series

Are you not sure about order? The readiness workflow selects it from unit-root tests and a search on an information criterion. It keeps the model only if it is better than a naive forecast.

ts.arima - forecast from past values

Forecasts a series from its recent past and trend, without outside drivers.

Use it when you have only the series itself and want a solid, well-known method. Use a different model when outside drivers explain most of the movement (use ts.sarimax).

ARIMA = """
  use: ts.arima@^1
  settings: {order: [1, 1, 1], series_dims: [geo]}   # one ARIMA(1,1,1) per geo
"""
arima = fit_spec(spec(ARIMA), panel, raw_schema=sales_schema(geo=True))

This is the same estimator as ts.sarimax, without drivers. Here, there is one ARIMA(1,1,1) for each geo of a panel. ts.arima refuses a constant together with differencing (use trend: t for a drift).

ts.ets - exponential smoothing

Forecasts a series and gives recent values more weight than older values. It has an optional trend and seasonality, and it is quick and robust.

Use it when you need a fast, reliable baseline, and the level, trend and season change slowly. Use a different model when outside drivers explain most of the movement.

ETS = """
  use: ts.ets@^1
  settings: {trend: add, damped_trend: true}
"""
ets = fit_spec(spec(ETS), history, raw_schema=sales_schema())  # drivers are not used

This is the error-trend-season model (ETSModel): error, trend (add/mul), damped_trend, and seasonal with seasonal_periods. Additive models have closed-form intervals. The plugin simulates the intervals of multiplicative models (simulate_repetitions paths). The seed of the request sets the paths. Thus, the same request gives the same quantiles.

ts.theta - a simple, strong baseline

Forecasts a series with a combination of a straight-line trend and smoothed recent values. This simple method is hard to beat.

Use it when you want a strong, simple baseline to compare other models against. Use a different model when outside drivers are very important, or when you need detailed explanations.

THETA = """
  use: ts.theta@^1
  settings: {series_dims: [geo]}
"""
theta = fit_spec(spec(THETA), panel, raw_schema=sales_schema(geo=True))

This is the theta method with optional seasonal adjustment (period, deseasonalize, method, theta). It is the benchmark to beat in a forecast bake-off.

ts.unobserved_components - trend, season and noise

Divides a series into a level and a trend that change slowly, a seasonal pattern and noise. It forecasts the series from these parts.

Use it when you want to see the underlying trend without the noise, and the trend and season change slowly. Use a different model when you only need a quick baseline.

UC = """
  use: ts.unobserved_components@^1
  settings: {level: local linear trend, exog: [price]}
"""
uc = fit_spec(spec(UC), history, raw_schema=sales_schema())
print(sorted(uc.model.groups["components"].data_vars))  # the smoothed level, trend, ...
uc_fan = forecast(uc, plan.drop_vars("sales"))

This is a structural model. It has a level (local level, local linear trend, smooth trend, ...), and optional stochastic seasonal, cycle, autoregressive noise and drivers. The components group of the bundle keeps the smoothed level, trend and season. The decomposition of the Model Explorer shows them. With drivers, forecasts need the planned values of the drivers.

ts.ardl - effects that build up over time

Explains a series from its past and from the current and past values of drivers. You can see how an effect increases over several periods.

Use it when the effect of a driver spreads over many periods. Use a different model when the effects are only immediate (a simple regression is sufficient).

ARDL = """
  use: ts.ardl@^1
  settings: {lags: 2, order: 2, exog: [price, promo]}   # or auto: true to choose them
"""
ardl = fit_spec(spec(ARDL), history, raw_schema=sales_schema())
ardl_fan = forecast(ardl, plan.drop_vars("sales"))

This model uses least squares on lags of the result and on order lags of each driver. With auto: true, the plugin selects them for each series by ic, up to those maximum values. The parameter table shows the effect of each lag. Thus, you can see how a price change has an effect over the subsequent weeks.

ts.ecm - long-run and short-run effects

Separates the short-term effect of a driver from its long-term effect on one series. It also tests if a long-run relationship exists.

Use it when one result and some drivers have the same trend, and you want the immediate effect and the lasting effect of a driver. Use a different model when the result and the drivers have no trend.

ECM = """
  use: ts.ecm@^1
  settings: {lags: 1, order: 1, exog: [income]}
"""
monthly = ecm_dataset(periods=126, seed=4)  # sales tied to income in the long run
ecm = fit_spec(
    spec(ECM, "{dims: {t: month}, vars: {y: sales}}"),
    monthly.isel(month=slice(0, 120)),
    raw_schema=ECM_SCHEMA,
)
group = ecm.model.groups["ecm"]  # speed of adjustment, half-life, the bounds test
print(round(float(group["half_life"]), 1), float(group["bounds_p_upper"]) < 0.05)
ecm_fan = forecast(ecm, monthly.isel(month=slice(120, None)).drop_vars("sales"), periods=6)

This is the same model in error-correction form (statsmodels UECM):

  • The coefficient on y.L1 is the speed of adjustment. Its half-life is in the ecm group.
  • The long_run.* rows of the parameter table are the long-run effects, with delta-method standard errors.
  • The Pesaran-Shin-Smith bounds test tells if a long-run relation exists. The trend sets the case (c is case 3). The result is levels_relationship, none or inconclusive. The scorecard also shows it as a diagnostic (ECM_BOUNDS_TEST).

ts.var - series that drive each other

Models two or more series that change each other over time, such as the prices and sales of two brands. It forecasts them together.

Use it when many series move together and have an effect on each other, and you want to see how a shock to one series spreads to the others. Use a different model when you have one series, or when the series stay tied together in the long run (use ts.vecm).

VAR = """
  use: ts.var@^1
  settings: {max_lags: 4, ic: aic}     # the lag order chosen by AIC
"""
MACRO_BIND = "{dims: {t: quarter, variable: series}, vars: {y: value}}"
var = fit_spec(spec(VAR, MACRO_BIND), macro_growth(quarters=120), raw_schema=GROWTH_SCHEMA)
print(sorted(var.model.groups))  # ... fevd, granger, irf, params, residuals ...

This model uses least squares. To select the order by ic up to max_lags, do not give a value for lags. The data has the series along one dimension (y(variable, t)). The bundle contains these items:

  • Orthogonalised impulse responses with asymptotic 95% bands (irf).
  • The forecast-error variance decomposition (fevd).
  • Granger causality tests for each pair of series (granger).

ts.vecm - series tied together for good

Models series that wander but stay tied together in the long run, such as the prices of close substitutes. It shows how fast they go back after a gap opens.

Use it when the series have a trend but keep a stable long-run relation. Use a different model when the series have no trend, or when a long-run relation is not plausible.

VECM = """
  use: ts.vecm@^1
  settings: {k_ar_diff: 2, deterministic: ci}   # the rank from the Johansen test
"""
vecm = fit_spec(spec(VECM, MACRO_BIND), macro_dataset(quarters=160), raw_schema=MACRO_SCHEMA)
print(int(vecm.model.groups["cointegration"]["rank"]))

This is a Johansen VECM. If you do not give a value for coint_rank, the Johansen rank_test (trace or max-eigenvalue) at rank_signif selects it. The plugin refuses some ranks with a SETTINGS_INVALID in plain language:

  • A rank of 0. There is no long-run relation. Model the changes with ts.var.
  • A rank that is equal to the number of series. The series are stationary. Use ts.var on the levels.

The cointegration group has the Johansen table, the rank, alpha (speed of adjustment) and beta (the long-run relations, normalised on the first series). The VAR/VECM recipe selects one of the two models for you.

ts.varmax - series that drive each other, with drivers

Models two or more series that change each other and also respond to outside drivers that you know in advance. It forecasts them together.

Use it when many series have an effect on each other, and outside drivers are also important. Use a different model when you have one series, or many series and a short history.

VARMAX = """
  use: ts.varmax@^1
  settings: {order: [1, 0]}            # VARX(1): q = 0
"""
varmax = fit_spec(spec(VARMAX, MACRO_BIND), macro_growth(quarters=90), raw_schema=GROWTH_SCHEMA)

This is VARMA(p, q) with drivers in state-space form (VARX when q = 0). As with ts.sarimax, the exog drivers need their planned values for a forecast.

ts.dynamic_factor - common movements across many series

Finds a few hidden forces that move many series together, such as the general economy behind the sales in many categories.

Use it when you have many related series and want their shared drivers. Use a different model when you have one or two series.

DFM = """
  use: ts.dynamic_factor@^1
  settings: {k_factors: 1}
"""
dfm = fit_spec(spec(DFM, MACRO_BIND), macro_growth(quarters=90), raw_schema=GROWTH_SCHEMA)
print(sorted(dfm.model.groups["factors"].data_vars))

In this state-space model, some hidden factors follow a VAR and drive many series. The factors group has the smoothed factors and loadings. Standardise the series first (core.standardize), because factor models assume comparable scales.

ts.markov_switching - switching between regimes

Finds if a series changes between regimes, such as periods of high growth and low growth. It estimates the behaviour in each regime.

Use it when the behaviour jumps between different modes, and you want the probability of each mode now. Use a different model when the changes are gradual, or when you have a short history.

MARKOV = """
  use: ts.markov_switching@^1
  settings: {k_regimes: 2, time_kind: integer}   # weeks numbered 0, 1, 2, ...
"""
regimes = regime_dataset(periods=150, seed=3).drop_vars("regime")
markov = fit_spec(
    spec(MARKOV, "{dims: {t: week}, vars: {y: demand}}"), regimes, raw_schema=REGIME_SCHEMA
)
print(markov.model.groups["regimes"]["mean_level"].round(1).values)  # about 10 and 16

This is the regime-switching regression of Hamilton (order: 0) or autoregression (order > 0). The level, the driver effects and the noise can change between k_regimes hidden regimes. The regimes group has these items:

  • The smoothed and filtered probability of each regime for each period.
  • The transition matrix.
  • The expected durations.
  • The mean level of each regime.

Forecasts are mixtures of regimes. They are analytic for regressions. For autoregressions, they are simulated (simulations seeded paths). The start values come from a fit on the standardised series. This prevents the failures of the default start values of statsmodels on series that are far from zero.

Forecasting through an endpoint, the client and the CLI

After you promote a version, you can get the same forecast from the endpoint. Send the plan of the drivers inline (an xr.Dataset with the future rows):

from mapi_client import Client

api = Client()
fan = api.endpoint("weekly-sales").score(plan_ds, mode="forecast", horizon={"dim": "t", "periods": 8})
print(fan["y_hat"].sel(stat="median").values)
mapi endpoints score weekly-sales --data plan.json --periods 8 -o forecast.zarr.zip
mapi explore model model://growth/weekly-sales@v3 -s forecast -s residuals --horizon 8

Standard plots v0.10

Each model declares its standard plots. The Insert panel of the spec editor shows them first. A fit draws only the plots that the spec lists in outputs.plots.

Models Standard plots
All twelve models core.ppc_fan, core.fit_vs_actual, core.residuals_over_time
ts.unobserved_components, ts.ets Also core.decomposition. It stacks the level, the seasonal part and the other parts from the components group. It does not stack the slope of a trend, because a slope is a rate.
ts.markov_switching Also core.state_sequence. It reads smoothed_prob in the regimes group over the regime dim.

ts.ets keeps a components group only when its error, trend and season are additive. A multiplicative model has no decomposition. For the plots of a score run, see Plots of hidden states.

What is stored, and why there is no pickle

The plugin never keeps a statsmodels results object, because pickles cannot cross the sandbox boundary. A bundle keeps these items:

  • The training data at the model stage (observed_data, and the drivers in constant_data).
  • All estimated parameters (params), with the order that the model needs in learned.
  • Fit statistics (fit_stats: AIC, BIC, HQIC, log-likelihood, observations, noise variance, and if the optimiser converged), residuals and residual tests (Ljung-Box, Jarque-Bera, ARCH-LM).
  • The groups for each model that the sections above give.

When you score, the plugin makes the results again:

  • State-space models (SARIMAX, ARIMA, UnobservedComponents, VARMAX, DynamicFactor) and the ETS and Markov-switching models apply the stored parameters to the stored data. They use their own filter (model.filter(params), smooth(params)).
  • Least-squares and closed-form models (VAR, VECM, ARDL/ECM, Theta) estimate again on the stored data with the stored orders. The plugin compares the result with the stored parameters. If they are different, the score fails with INTEGRITY_FAILURE.

The Model Explorer

StatsmodelsModel.explorer_views(fitted) changes a bundle into sections of the Model Explorer (mapi_spec.views DTOs):

  • params: the table with 95% intervals and p-values.
  • diagnostics: fit statistics, and Granger, Johansen and bounds tests.
  • residuals: the residuals over time, their autocorrelation, Q-Q points and the residual tests for each series.
  • irf, fevd and decomposition.

Panels show as facets. views.forecast_view(predictions, observed=...) changes the quantiles of a forecast score run into the forecast section. There is one fan for each series (with the recent history), because you cannot add quantiles of different series into one fan.

How it is tested

  • The contract kit (check_model) passes for each plugin, with About cards necessary.
  • Parameters, in-sample predictions and forecast intervals are equal to direct statsmodels calls on the same data. This is true for SARIMAX, ARIMA, UnobservedComponents, ETS, Theta, ARDL, UECM, VAR, VECM, VARMAX, DynamicFactor and Markov switching. A test also does a check of the UECM on the Danish money-demand data of Johansen and Juselius. The VAR(2) on US macro growth gives the same coefficient table as the statsmodels documentation.
  • Forecasts with actuals are equal to statsmodels with the fitted parameters on the extended data. The tests use results.append(new, refit=False) for the state-space models and ARDL. They also use the VAR/VECM forecast on the supplied lags, the fixed-parameter ETS smoother, theta and the Hamilton filter. For each model, the last values of the fit as actuals give the plain forecast again. A jump in these values changes the forecast. A model from the ts_readiness_arimax workflow forecasts from new monthly actuals in the original units.
  • The conformance fixtures sm_arimax (an ARIMAX panel with drivers) and sm_vecm_macro (a VECM on the public-domain macrodata of statsmodels) run the full path: fit, score and evaluate. See Conformance corpus.