duckARIMA

July 17, 2026 · View on GitHub

Seasonal ARIMA with exogenous regressors (SARIMAX) — estimation, inference, and forecasting — as pure DuckDB SQL table macros. No extensions, no UDFs, no driver required: everything runs inside a stock DuckDB ≥ 1.5.4 session, from the CLI or any client.

Validated against statsmodels' SARIMAX on committed golden fixtures at honest tolerances: the Kalman-filter loglikelihood reproduces statsmodels to near machine precision at fixed parameters (abs ≤ 1e-8 / rel ≤ 1e-10, with per-timestep innovations at rel ≤ 1e-9); fitted parameters and forecasts agree to optimizer tolerance (parameters abs ≤ 1e-6, forecasts rel ≤ 1e-6, forecast standard errors rel ≤ 1e-5, parameter standard errors rel ≤ 1e-3). The argmax of a likelihood is only defined to optimizer tolerance — in statsmodels as much as here — so no blanket "machine precision" claim is made for the fit itself.

Setup

.read sarimax_macros.sql

That's it — one file of CREATE OR REPLACE MACRO statements, loaded once per session.

Quick taste

-- monthly sales with two regressors; SARIMA(0,1,1)(0,1,1)_12 + exog
CREATE TABLE m AS SELECT * FROM sarimax_fit('sales', 'units', 0, 1, 1,
                                            sd := 1, sq := 1, s := 12,
                                            exog_cols := ['promo', 'price']);

SELECT * FROM sarimax_summary('m', 'sales', 'units');          -- coefficients, SEs, z, p, CIs
SELECT * FROM sarimax_forecast('m', 'sales', 'units', 12,
                               newdata := 'future_regressors',
                               exog_cols := ['promo', 'price']); -- h, yhat, se, lo, hi
SELECT * FROM sarimax_evaluate('m', 'sales', 'units');          -- loglik, AIC, BIC, sigma2

Beyond the basics, sarimax_fit supports trend terms (trend := 'c' | 't' | 'ct', statsmodels' intercept/drift parameters), a concentrated scale (concentrate := true: sigma2 solved analytically, one fewer optimizer dimension), missing values in the target (NULL y is filtered over, not rejected), and exact full-sample estimation without up-front differencing (simple_differencing := false, statsmodels' default formulation — no observations lost to differencing):

CREATE TABLE m2 AS SELECT * FROM sarimax_fit('sales', 'units', 1, 1, 1,
                                             trend := 'ct', concentrate := true,
                                             simple_differencing := false);

Larger-than-memory fitting

DuckDB can spill relational sorts, joins, windows, and hash aggregates to a temporary directory. The regular fit path intentionally packs the timeline into ordered LIST values for speed and bitwise determinism; DuckDB cannot spill those aggregate states. Select the relational likelihood explicitly when the series may not fit in memory:

SET memory_limit = '8GB';
SET temp_directory = '/fast/local/duckdb-spill';
SET max_temp_directory_size = '100GB';
SET threads = 4;
SET preserve_insertion_order = false;

CREATE TABLE m_large AS
SELECT * FROM sarimax_fit(
    'large_sales', 'units', 1, 1, 1,
    t_col := 'timestamp',             -- required and must be unique
    out_of_core := true,
    compute_bse := false);            -- recommended for very large fits

out_of_core := true keeps observations relational through every likelihood pass and retains only the current Kalman state per parameter probe. Resource settings remain session-level and caller-controlled. compute_bse := false skips the numerical Hessian's O(parameter_count²) full-data passes and emits NULL standard errors; it does not change fitted coefficients or forecasts.

This is a correctness-first escape hatch, not a promise that very long fits will be fast: BFGS still needs many sequential O(n·state_dimension³) filter passes. The residual, evaluation, and Ljung–Box helpers retain full-trace or whole-series intermediates and are not yet covered by the out-of-core contract. See GUIDE.md for tuning, determinism, and verification details.

Note: the seasonal orders are sp (seasonal AR), sd (seasonal differencing), sq (seasonal MA) — DuckDB macro parameters are case-insensitive, so the textbook P/D/Q names would collide with p/d/q. Positionally everything matches SARIMAX(order=(p,d,q), seasonal_order=(sp,sd,sq,s)).

Files

FileWhat
sarimax_macros.sqlthe entire library, generated from sql/00..06 by tools/build_macros.py
CHEATSHEET.mdone-page signature reference for every public macro
GUIDE.mduser's guide: examples, statistical conventions, contracts, edge cases
sql/layered development sources (linalg → prep → state space → filter → estimation → forecast → harness)
tests/pytest validation path (statsmodels golden fixtures) + tests/smoke.sql pure-SQL path

Validation

Two independent paths (see tests/README.md): pytest tests/ -q runs the tiered statsmodels comparison on 11 committed v1 fixtures (ARMA/ARIMA/SARIMA/ ARIMAX/SARIMAX, including near-boundary cases and the airline benchmark) plus 6 v2 fixtures (trend, concentrated scale, missing values, no simple differencing — including a kitchen-sink model combining all four), and duckdb < tests/smoke.sql fits and forecasts with no Python at all.

MIT licensed.