Statsmodels 0.15.0 (27 Aug 2026) adds Diebold–Mariano, Pesaran–Timmermann, the Hamilton filter, NamedTuple results, rng= (SPEC-007), and optional Polars input converted to pandas at entry. After-work upgrade checklist plus a mini forecast-eval notebook. Not investment advice.
Statsmodels 0.15.0 shipped on 27 August 2026. The official 0.15.0 release notes cover everything merged into main from the v0.15.0.dev0 tag (5 May 2023) through that date: 358 issues closed, 655 pull requests, 171 named contributors. PyPI lists statsmodels 0.15.0 with Python ≥ 3.10, NumPy ≥ 1.23.5, SciPy ≥ 1.8, pandas ≥ 1.4, and a new required runtime dependency formulaic>=1.1.0. Optional extra: polars>=1.0.0.
This is an after-work upgrade checklist plus a mini forecast-evaluation notebook — not a research monograph, not a trading system, and not investment advice. The only course CTA is SoloQuant Academy’s Introduction to Python Programming.
If you already keep a small .py file or notebook after work, you have enough. Related Hub notes: Marimo vs Jupyter for the evening notebook, and LightGBM 4.7 Polars/Arrow if you already pass Polars tables around (statsmodels still converts Polars to pandas at the door — see below).
From the release notes, five items change how you write a short evaluation script:
statsmodels.tsa.stattools.diebold_mariano_test, with an optional Harvey, Leybourne and Newbold (1997) small-sample correction (PR #10066).statsmodels.stats.diagnostic.pesaran_timmermann (PR #10055).statsmodels.tsa.filters.hamilton_filter.hamilton_filter — Hamilton’s regression-based alternative to the Hodrick–Prescott (HP) filter (PR #9957, plot docs PR #9991). hpfilter is still there; 0.15 gives you a documented sibling with NamedTuple results.result_object=True to opt in now, or result_object=False to keep the legacy tuple and silence the FutureWarning. The default becomes the NamedTuple in 0.16. Functions whose shape never varied (including hpfilter and hamilton_filter) converted outright.rng= (SPEC-007). Older random_state and seed keywords still work, remap to rng, and raise FutureWarning. Switch evening code to rng= (integer seed, integer array, NumPy Generator, or RandomState).Polars input (not a pandas-vs-Polars bake-off): models and the formula API accept Polars DataFrame/Series. Input is converted to pandas at the data-entry point (handle_data and the formula layer). Internal computation stays pandas/NumPy. Column names, index information, and predictions with Polars exog are preserved. Polars is optional; paths that never see a Polars object are unchanged.
Also real, but not tonight’s homework unless you hit them: formula engine can use patsy (default if installed) or formulaic via SM_FORMULA_ENGINE; meson-python for source builds (wheels on PyPI are unaffected); stricter string options for alternative= (use documented "two-sided" / "larger" / "smaller", not "2s").
Confirm the wheel:
python -c "import statsmodels; print(statsmodels.__version__)"
# pip install -U "statsmodels==0.15.0"
Use a toy two-forecast comparison: a naive lag-1 predictor versus a rolling mean. Any public series you already have is enough (macro index, energy load, or a synthetic AR). Do not treat p-values as a trading signal.
import numpy as np
import pandas as pd
import statsmodels.api as sm
from statsmodels.tsa.stattools import diebold_mariano_test
from statsmodels.stats.diagnostic import pesaran_timmermann
from statsmodels.tsa.filters.hp_filter import hpfilter
from statsmodels.tsa.filters.hamilton_filter import hamilton_filter
rng = np.random.default_rng(42)
n = 240
y = pd.Series(np.cumsum(rng.normal(0, 1, n)), name="y")
# Two deliberately simple forecasts (illustrative only)
f_naive = y.shift(1)
f_roll = y.rolling(4, min_periods=4).mean().shift(1)
mask = f_naive.notna() & f_roll.notna()
actual, e1, e2 = y[mask], (y - f_naive)[mask], (y - f_roll)[mask]
# Diebold–Mariano: equal accuracy of two forecast error series
# Check 0.15 docs for the exact argument names in your build.
dm = diebold_mariano_test(e1.to_numpy(), e2.to_numpy())
print(type(dm), dm)
# Pesaran–Timmermann: directional accuracy (signs), not magnitude
# Align a forecast of direction with realized changes.
dy = y.diff()[mask]
sign_forecast = np.sign((f_naive - y.shift(1))[mask].to_numpy()) # toy
# Prefer the documented call: realized direction vs predicted direction
pt = pesaran_timmermann(dy.to_numpy(), np.sign(f_roll.diff()[mask].to_numpy()))
print(pt)
# Hamilton vs HP on the same series (cycle/trend sketch — not a trading rule)
hp_cycle, hp_trend = hpfilter(y)
ham = hamilton_filter(y)
print(getattr(ham, "_fields", type(ham)), type(hp_cycle))
If a function still returns a legacy tuple, unpack by name after result_object=True, or print the NamedTuple fields (statistic, pvalue, … — read the docstring; field names are documented per class).
SPEC-007 reminder anywhere you simulate:
# before: model.simulate(..., random_state=0) # FutureWarning in 0.15
# after:
res = sm.tsa.arima.ARIMA(y, order=(1, 0, 0)).fit()
sim = res.simulate(nsimulations=20, rng=0)
Polars at the door (optional; conversion is explicit in the notes):
# import polars as pl
# df = pl.DataFrame({"y": y.to_numpy(), "x": rng.normal(size=n)})
# OLS still runs after pandas conversion inside handle_data
# sm.OLS.from_formula("y ~ x", data=df).fit()
That last block is “does the formula API accept my Polars frame,” not a speed contest.
__version__.seed= / random_state= in your study repos; rename to rng=.python -W default and look for NamedTuple / result_object warnings. Prefer res.pvalue over res[1] going into 0.16.GitHub’s statsmodels releases mirror the same 0.15.0 tag if you prefer changelog browsing there.
If environments, syntax, and first libraries still feel sticky, start with Introduction to Python Programming after work, then come back to 0.15 with a tiny series and the checklist above.
rng, Polars converted to pandas at entry (PR #9804), formulaic, meson-python, Python ≥ 3.10.polars extra.