anofox-forecast-models
Forecasting models and the `ts_forecast_by` / `ts_forecast_var_by` API surface of the anofox_forecast DuckDB extension. Covers 36 models (baseline, exponential smoothing, state-space ARIMA + Kalman, classical GARCH, Theta, multi-seasonal, intermittent-demand, distributional Lapla
Install
npx skills add https://github.com/DataZooDE/anofox-forecast/tree/main/.claude/skills/anofox-forecast-models
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install datazoode-anofox-forecast@llmmart
git clone https://github.com/DataZooDE/anofox-forecast.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole datazoode/anofox-forecast collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Anofox Forecast — Models & ts_forecast_by Cheat Sheet
Extension: anofox_forecast v0.15.3 (Rust crate anofox-forecast v0.15.3) | DuckDB: v1.4.5 LTS / v1.5.4+ | Dual naming: ts_* and anofox_fcst_ts_*
36 forecasting models exposed by SQL via three call surfaces (table macro, aggregate, scalar) + ts_forecast_var_by for multivariate VAR.
Critical gotchas
Seasonality is NOT auto-detected. All models — including the
Auto*family — treatseasonal_periodas user-supplied. Runts_detect_periods_byfirst and pass the result explicitly. Seeanofox-forecast-detection.Model names are case-sensitive.
'AutoETS'works,'autoets'errors.ts_forecast_byrequires frequency as the 7th positional param. No default:-- WRONG (missing frequency): ts_forecast_by('sales', id, ds, y, 'Naive', 12) -- CORRECT: ts_forecast_by('sales', id, ds, y, 'Naive', 12, '1d')STRUCT + MAP both work in params. STRUCT keeps numeric params typed (recommended):
-- STRUCT (recommended) ts_forecast_by(..., 'HoltWinters', 12, '1d', {seasonal_period: 7}) -- MAP (all strings, legacy) ts_forecast_by(..., 'HoltWinters', 12, '1d', MAP{'seasonal_period': '7'})The
_byoutput renames the target column toyand addsforecast_step,yhat,yhat_lower,yhat_upper,model_name.ts_forecast_aggtakes(date, value, model, horizon, params)directly — noLIST(...)wrapping.
ts_forecast_by (primary surface)
ts_forecast_by(
source VARCHAR, -- table name (quoted string, NOT a CTE)
group_col COLUMN, -- series identifier (unquoted)
date_col COLUMN, -- date / timestamp (unquoted)
target_col COLUMN, -- value to forecast (unquoted)
method VARCHAR, -- e.g. 'AutoETS', 'Laplace'
horizon INTEGER, -- number of periods ahead
frequency VARCHAR, -- '1d', '1mo', '1h', ...
params MAP or STRUCT -- model-specific config
) → TABLE(group_col, forecast_step INT, ds, yhat DOUBLE, yhat_lower, yhat_upper, model_name)
ts_forecast_panel_by (panel / global models)
Fit-once-emit-many panel API — pools parameter optimization across all series, then predicts per-series. Use when individual series are short but collectively form a large, homogeneous panel. Three panel methods:
ts_forecast_panel_by(
source VARCHAR, -- table name (quoted string)
group_col COLUMN, -- series identifier (unquoted)
date_col COLUMN, -- date / timestamp (unquoted)
target_col COLUMN, -- value to forecast (unquoted)
method VARCHAR, -- 'GlobalETS' | 'GlobalTheta' | 'GlobalCroston'
horizon INTEGER,
frequency VARCHAR, -- '1d', '1mo', ...
params MAP{} -- optional; see below
) → TABLE(group_col, forecast_step INT, date_col TIMESTAMP, yhat DOUBLE, model_name)
Panel methods
| Method | Best for | Key params |
|---|---|---|
'GlobalETS' |
Many related series, shared seasonal dynamics | seasonal_period (0=non-seasonal default), model_pool ('Reduced' default | 'Complete') |
'GlobalTheta' |
Trended panels, minimal config | none (seasonal_period ignored) |
'GlobalCroston' |
Intermittent/spare-parts panels (many zeros) | croston_variant ('Classic' default | 'SBA') |
Critical panel gotchas
- Series dropped below 10 obs: Short series after alignment emit
DROPPED: too_shortrows — not errors. Checkmodel_namein the result. - Minimum 3 series after drop: Fewer than 3 kept series →
InvalidInputException. Ensure your panel has enough history. - Point forecasts only (v1):
yhat_lower/yhat_upperare not populated. Usets_conformal_byseparately if intervals needed. - TABLE arg must be a subselect: The underlying
_ts_forecast_panel_nativeuses the subselect pattern internally. Pass only a table name (quoted string) tots_forecast_panel_by— do NOT pass a CTE or subquery assource. - GlobalCroston with all-zero panel fails: Ensure at least one series has ≥ 2 non-zero demand events in the aligned window.
- GlobalETS
seasonal_period=0→ non-seasonal (Reduced pool,ANN/AAdN/MNN/MAdNcandidates only). Period=1 has the same effect.
Panel quick examples
-- GlobalETS weekly seasonal panel
SELECT * FROM ts_forecast_panel_by('sales', product_id, ds, y, 'GlobalETS', 14, '1d',
MAP {'seasonal_period': '7'});
-- GlobalTheta trended panel (no config needed)
SELECT * FROM ts_forecast_panel_by('sales', product_id, ds, y, 'GlobalTheta', 14, '1d');
-- GlobalCroston SBA for spare-parts panel
SELECT * FROM ts_forecast_panel_by('spares', item_id, ds, qty, 'GlobalCroston', 6, '1d',
MAP {'croston_variant': 'SBA'});
ts_forecast_var_by (multivariate VAR)
Dedicated multivariate function — distinct from ts_forecast_by. Fits a VAR(p) model
across K variables simultaneously (cross-variable dynamics). Returns long format.
v1 constraints: Single-panel only (no group_col). Named param p for lag order (order is a SQL reserved word). Point forecasts only (no intervals).
ts_forecast_var_by(
source VARCHAR, -- source table name (quoted string)
date_col VARCHAR, -- date column name (quoted string)
value_cols VARCHAR[], -- array of value column names ['y1', 'y2', ...]
horizon INTEGER, -- periods to forecast
frequency VARCHAR, -- time step between observations
p INTEGER, -- lag order (named param, default: 1)
params MAP -- reserved for future use (default: MAP{})
) → TABLE(variable VARCHAR, forecast_step BIGINT, <date_col>, forecast_value DOUBLE)
Output: k_vars × horizon rows in long format. One row per (variable, forecast_step).
-- VAR(1) — 2-variable system, 14-step ahead
SELECT * REPLACE(ROUND(forecast_value, 6) AS forecast_value)
FROM ts_forecast_var_by('var_src', 'ds', ['y1', 'y2'], 14, '1d')
ORDER BY variable, forecast_step;
-- Returns 28 rows: y1 × 14 + y2 × 14
-- VAR(2) — higher lag order
SELECT * FROM ts_forecast_var_by('var_src', 'ds', ['y1', 'y2'], 14, '1d', p:=2);
Pitfalls:
- Use
p:=2NOTorder:=2(ORDER is a SQL reserved word). - All value columns must have the same valid observation count after null imputation.
- Minimum obs: n > k×p+1 (n=obs, k=variables, p=lag order).
- Non-stationary series → unstable coefficient matrix; difference first with
ts_diff_by. - Benchmark: VAR(1) on synthetic VAR(1) data — MAE ratio vs statsmodels = 1.000 (exact match, PASS).
Ensembles (v0.8.0)
Ensembles combine multiple member models per series rather than replacing them — they are a combination surface over the existing 36 models, not additional model count. Three entry points plus two introspection functions.
ts_forecast_by with method='AutoEnsemble'
Auto-fits AutoARIMA, AutoETS, and AutoTheta; ranks them by in-sample MSE; combines the top-K members.
ts_forecast_by(
source VARCHAR, group_col COLUMN, date_col COLUMN, target_col COLUMN,
'AutoEnsemble', horizon INTEGER, frequency VARCHAR,
params MAP or STRUCT -- top_k, combination_method, seasonal_period
) → TABLE(group_col, forecast_step INT, ds, yhat DOUBLE, yhat_lower, yhat_upper, model_name)
Params:
| Key | Default | Values |
|---|---|---|
top_k |
3 | Number of top-ranked members to combine |
combination_method |
'mean' |
'mean', 'median', 'weighted_mse', 'inverse_aic', 'stacking', 'horizon_adaptive' |
seasonal_period |
0 | Shared with member models; 0 = non-seasonal |
-- AutoEnsemble with default params (top_k=3, combination_method='mean')
-- (sourced from autoensemble.sql Section 2)
SELECT forecast_step, ds, ROUND(yhat, 4) AS yhat, yhat_lower, yhat_upper, model_name
FROM ts_forecast_by('ae_test', id, ds, y, 'AutoEnsemble', 5, '1d')
ORDER BY forecast_step;
-- AutoEnsemble with explicit params (sourced from autoensemble.sql Section 1)
SELECT forecast_step, yhat, model_name
FROM ts_forecast_by(
'ae_test', id, ds, y,
'AutoEnsemble', 5, '1d',
params := {top_k: 3, combination_method: 'mean', seasonal_period: 0}
);
Note: yhat_lower / yhat_upper are NULL from ts_forecast_by('AutoEnsemble', ...). Use the conformal pipeline (ts_cv_folds_by → _ts_forecast_scalar per fold → ts_conformal_calibrate) to attach distribution-free intervals — see anofox-forecast-backtest and ensemble_intervals.sql.
ts_forecast_ensemble_by — explicit member list
User-named member models as a VARCHAR[]. Any of the 26-member allowlist (all models except GARCH, AutoEnsemble) may appear.
ts_forecast_ensemble_by(
source VARCHAR,
group_col COLUMN,
date_col COLUMN,
target_col COLUMN,
members VARCHAR[], -- e.g. ['AutoARIMA', 'AutoETS', 'Theta']
horizon INTEGER,
frequency VARCHAR,
combination_method := 'mean', -- named param
seasonal_period := 0 -- named param
) → TABLE(group_col, forecast_step INT, ds, yhat DOUBLE, yhat_lower DOUBLE, yhat_upper DOUBLE, model_name)
-- Explicit three-member ensemble, mean combination
-- (sourced from ensemble_explicit.sql Section 1)
SELECT * FROM ts_forecast_ensemble_by(
'ae_test', id, ds, y,
['AutoARIMA', 'AutoETS', 'Theta'],
5, '1d',
combination_method := 'mean',
seasonal_period := 0
);
-- Seasonal tier-2 members (sourced from ensemble_explicit.sql Section 3 Sample D)
SELECT forecast_step, ROUND(yhat, 4) AS yhat FROM ts_forecast_ensemble_by(
'ae_test', id, ds, y,
['SeasonalNaive', 'HoltWinters'],
3, '1d',
combination_method := 'mean', seasonal_period := 12);
Errors: unknown member name, fewer than 2 members, or blocked model (GARCH) each raise a descriptive InvalidInput exception naming the offending member.
ts_ensemble_inspect_by — inspect explicit-member ensemble
Returns one row per group per member: member_name, weight, plus group column.
ts_ensemble_inspect_by(
source VARCHAR,
group_col COLUMN,
date_col COLUMN,
target_col COLUMN,
members VARCHAR[],
combination_method := 'mean',
seasonal_period := 0
) → TABLE(group_col, member_name VARCHAR, weight DOUBLE)
-- Mean combination: weight == 1/k for every member
-- (sourced from ensemble_inspect.sql Section 1)
SELECT * FROM ts_ensemble_inspect_by(
'insp_series', id, ds, y,
['AutoARIMA', 'AutoETS', 'Theta'],
combination_method := 'mean',
seasonal_period := 0
)
ORDER BY id, member_name;
-- WeightedMSE: inverse-MSE weights; non-negative, sum to 1 per series
-- (sourced from ensemble_inspect.sql Section 2)
SELECT * FROM ts_ensemble_inspect_by(
'insp_series', id, ds, y,
['AutoARIMA', 'AutoETS', 'Theta'],
combination_method := 'weighted_mse',
seasonal_period := 0
)
ORDER BY id, member_name;
ts_auto_ensemble_inspect_by — inspect AutoEnsemble members
Returns one row per group per selected member: member_name, weight, score (in-sample MSE), rank.
ts_auto_ensemble_inspect_by(
source VARCHAR,
group_col COLUMN,
date_col COLUMN,
target_col COLUMN,
top_k := 3,
combination_method := 'mean',
seasonal_period := 0
) → TABLE(group_col, member_name VARCHAR, weight DOUBLE, score DOUBLE, rank BIGINT)
-- AutoEnsemble Mean inspection: weight == 1/k, score > 0, rank in 1..k
-- (sourced from ensemble_inspect.sql Section 3)
SELECT * FROM ts_auto_ensemble_inspect_by(
'insp_series', id, ds, y,
top_k := 3,
combination_method := 'mean',
seasonal_period := 0
)
ORDER BY id, rank;
-- AutoEnsemble WeightedMSE: weight IS NULL (see gotcha b below), score > 0
-- (sourced from ensemble_inspect.sql Section 4)
SELECT * FROM ts_auto_ensemble_inspect_by(
'insp_series', id, ds, y,
top_k := 3,
combination_method := 'weighted_mse',
seasonal_period := 0
)
ORDER BY id, rank;
Ensemble gotchas
(a) ts_cv_forecast_by('AutoEnsemble') segfaults (crate 0.15.3). The CV native does not parse ensemble-specific params (ensemble_top_k / ensemble_method are zeroed), causing a runtime crash. Do NOT use ts_cv_forecast_by for ensemble conformal CV. Instead, run a manual per-fold _ts_forecast_scalar loop:
-- Safe conformal CV path for AutoEnsemble (sourced from ensemble_intervals.sql Section 1)
-- Step 1: generate folds
CREATE OR REPLACE TABLE ens_folds AS
SELECT * FROM ts_cv_folds_by('ens_series', id, ds, y, 3, 5, MAP{});
-- Step 2: forecast per fold with _ts_forecast_scalar (not ts_cv_forecast_by)
-- Step 3: ts_conformal_calibrate on residuals
-- Step 4: ts_forecast_by('AutoEnsemble') + apply calibrated quantile
-- See examples/forecasting/ensemble_intervals.sql for the full pipeline.
(b) ts_auto_ensemble_inspect_by returns NULL weight for non-Mean combination_method (crate 0.15.3). The AutoEnsemble struct's inner combination weights are stored in a private field not accessible via the public all_scores() / model_count() API. Mean combination is the only variant for which weights are returned. score (in-sample MSE) is always populated. This is a documented upstream-crate limitation.
ts_forecast_agg (aggregate)
For custom GROUP BY shapes.
ts_forecast_agg(date_col TIMESTAMP, value_col DOUBLE, method VARCHAR, horizon INTEGER, params MAP)
→ STRUCT(point_forecast DOUBLE[], lower_90 DOUBLE[], upper_90 DOUBLE[], model_name VARCHAR, insample_fitted DOUBLE[], ...)
SELECT product_id, ts_forecast_agg(ds, y, 'AutoETS', 12, MAP{}) AS fcst
FROM sales GROUP BY product_id;
Access fields: (fcst).point_forecast, (fcst).lower_90, etc.
Model catalogue (36)
Automatic selection (6)
| Model | Params | Best for |
|---|---|---|
AutoETS |
seasonal_period, model_pool |
Unknown patterns — default pick |
AutoARIMA |
seasonal_period |
Unknown patterns, ARIMA family |
AutoTheta |
seasonal_period |
Unknown patterns, Theta family (best RMSE on M5-monthly) |
AutoMFLES |
seasonal_periods[] |
Multiple seasonalities |
AutoMSTL |
seasonal_periods[] |
Multiple seasonalities |
AutoTBATS |
seasonal_periods[] |
Multiple seasonalities |
Baseline (6)
| Model | Required | Optional |
|---|---|---|
Naive |
— | — |
SMA |
— | window (default 5) |
SeasonalNaive |
seasonal_period |
— |
SES |
— | alpha (default 0.3) |
SESOptimized |
— | — |
RandomWalkDrift |
— | — |
Exponential smoothing (5)
| Model | Required | Optional |
|---|---|---|
Holt |
— | alpha, beta |
HoltWinters |
seasonal_period |
alpha, beta, gamma |
SeasonalES |
seasonal_period |
alpha, gamma |
SeasonalESOptimized |
seasonal_period |
— |
SeasonalWindowAverage |
seasonal_period |
— |
Theta (5)
| Model | Optional |
|---|---|
Theta |
seasonal_period, theta |
OptimizedTheta |
seasonal_period |
DynamicTheta |
seasonal_period, theta |
DynamicOptimizedTheta |
seasonal_period |
AutoTheta |
seasonal_period (listed above) |
Classical volatility (1)
| Model | Required | Optional | Important |
|---|---|---|---|
GARCH |
— | garch_p (default 1), garch_q (default 1) |
yhat is VOLATILITY (std-dev = sqrt(variance)), NOT variance. Use on returns (first differences), not raw price levels. Min obs: p+q+10. |
-- GARCH(1,1) — volatility forecast on returns (40 obs minimum > 12)
SELECT asset_id, forecast_step, ds, yhat AS conditional_volatility, model_name
FROM ts_forecast_by('returns', asset_id, ds, y, 'GARCH', 7, '1d');
-- GARCH(1,1) with explicit params
FROM ts_forecast_by('returns', asset_id, ds, y, 'GARCH', 7, '1d',
params := MAP{'garch_p':'1','garch_q':'1'})
Critical: yhat is σ (std-dev), not σ². Square for variance: yhat * yhat.
Critical: Use on returns (LN differences of prices), not raw levels — non-stationary levels cause α+β→1 divergence.
Benchmark: ratio vs arch package = 0.897 on M4 Daily (PASS).
State-space / ARIMA (3 — AutoETS/AutoARIMA counted above)
| Model | Required | Optional |
|---|---|---|
ETS |
— | seasonal_period, model ('AAA', 'AAN', …) |
ARIMA |
p, d, q |
P, D, Q, s |
Kalman |
— | kalman_model ('local_level' default | 'local_linear_trend') |
Kalman details:
local_level(default): random walk + noise; h-step forecast is flat at filtered level.local_linear_trend: level + trend; h-step forecast grows/shrinks linearly.- Uses fixed variance params (obs_var=1.0, level_var=0.1), NOT MLE-estimated.
- Benchmark: ratio vs statsmodels UnobservedComponents = 1.000 (local_level) / 0.992 (llt), both PASS.
-- Kalman local_level (default)
SELECT * FROM ts_forecast_by('sales', product_id, ds, y, 'Kalman', 14, '1d');
-- Kalman local_linear_trend
SELECT * FROM ts_forecast_by('sales', product_id, ds, y, 'Kalman', 14, '1d',
params := MAP{'kalman_model': 'local_linear_trend'});
Multi-seasonal (3 — Auto* counted above)
| Model | Required | Optional |
|---|---|---|
MFLES |
seasonal_periods[] |
iterations |
MSTL |
seasonal_periods[] |
stl_method |
TBATS |
seasonal_periods[] |
use_box_cox |
Intermittent demand (6)
| Model | Optional |
|---|---|
CrostonClassic |
— |
CrostonOptimized |
— |
CrostonSBA |
— |
ADIDA |
— |
IMAPA |
— |
TSB |
alpha_d, alpha_p |
Distributional (1) — Laplace
Streaming likelihood-weighted mixture of leaves (EMA / drift / AR(1) / damped-Holt + optional seasonal / distribution-family leaves). Three zero-config selectors via laplace_variant.
| Variant | Best for | Notes |
|---|---|---|
auto (default) |
Smooth continuous series | Balanced defaults |
auto_aid |
Retail SKU / intermittent counts | AID-based distribution-family leaf selection. Best negative-forecast rate on non-negative counts. |
skaters |
Fuller ensemble | Multi-h scoring, stacking, larger leaf set. Slower, more robust. |
Extra param laplace_seasonal_batch_init: 1 batch-initialises the seasonal-EMA leaf from the last training cycle. Safe on stationary / declining amplitude; UNSAFE on growing amplitude or phase-shifted seasonality (collapses to flat). Default off.
-- Retail-tuned selector on monthly counts
SELECT * FROM ts_forecast_by('sales', id, ds, y, 'Laplace', 12, '1mo',
{seasonal_period: 12, laplace_variant: 'auto_aid'});
model_name tags the state: Laplace(auto,seasonal=7), Laplace(auto_aid,seasonal=12), Laplace(skaters,seasonal=12,batch_init).
Performance context (M5-monthly, 24 k series × 12-month horizon on 12-core dev box):
- Laplace(auto): MAE 18.99, wall 0.5 s
- AutoTheta: MAE 19.33, wall 0.7 s
- AutoETS: MAE 19.91, wall 15 s (~30× slower)
- SeasonalES (baseline): MAE 26.55
Parameter surface — universal keys
Every model accepts these via params:
| Key | Type | Notes |
|---|---|---|
seasonal_period |
INTEGER | e.g. 7 (weekly), 12 (monthly), 24 (hourly-daily), 365 (yearly) |
seasonal_periods |
ARRAY (JSON string in MAP form) | Multi-seasonal: '[24, 168]' or [24, 168] in STRUCT |
confidence_level |
DOUBLE | Default 0.90; range (0, 1) |
window |
INTEGER | For SMA / SeasonalWindowAverage |
model_pool |
VARCHAR | AutoETS: 'complete' (default), 'reduced' |
laplace_variant |
VARCHAR | 'auto' / 'auto_aid' / 'skaters' — Laplace only |
laplace_seasonal_batch_init |
INTEGER (0/1) | Opt-in — Laplace only |
Frequency strings
| Format | Examples |
|---|---|
| Polars-style | '1d', '1h', '30m', '1w', '1mo', '1q', '1y' |
| DuckDB INTERVAL | '1 day', '1 hour' |
| Raw integer | '1', '7' (days) |
Model selection — one-liner rules
| Data characteristics | First try | Alternative |
|---|---|---|
| Don't know | AutoETS |
Laplace (auto) |
| Trend, no seasonality | Holt |
Theta |
| Weekly seasonal | HoltWinters |
Laplace (auto) |
| Monthly retail counts | Laplace (auto_aid) |
AutoTheta |
| Multiple seasonalities | MSTL |
MFLES |
| Intermittent (many zeros) | CrostonSBA |
Laplace (auto_aid) |
| Amplitude-declining seasonal | Laplace (auto, batch_init=1) |
AutoTheta |
| Speed on large panels | Laplace (auto) |
SeasonalES |
| Short series (< 20 obs) | Naive |
SES |
Canonical forecast pipeline
-- 0. Sessions using detection / conformal need json
SET autoinstall_known_extensions = 1;
SET autoload_known_extensions = 1;
-- 1. Detect seasonality
CREATE OR REPLACE TABLE periods AS
SELECT id, primary_period AS sp
FROM ts_detect_periods_by('clean', product_id, ds, y, MAP{});
-- 2. Forecast (panel-modal seasonal_period)
CREATE OR REPLACE TABLE forecasts AS
SELECT * FROM ts_forecast_by('clean', product_id, ds, y,
'Laplace', 14, '1d',
{seasonal_period: (SELECT mode() WITHIN GROUP (ORDER BY sp) FROM periods),
laplace_variant: 'auto'});
-- 3. Inspect
SELECT product_id, forecast_step, ds, yhat, yhat_lower, yhat_upper, model_name
FROM forecasts
ORDER BY product_id, forecast_step;
Explainability surfaces (Tier 6)
Two macros expose per-group fit-state and forecast decomposition, each
returning a nullable wide STRUCT so the shape is stable across model
families. Both require the json extension.
ts_forecast_inspect_by(source, group_col, date_col, target_col, method, params?)
Fit-state snapshot. Signature returns TABLE(group_col, inspection STRUCT(...)).
(inspection).model_family names which sub-fields are populated:
model_family |
Key fields |
|---|---|
Ets |
spec, alpha, beta, gamma, phi, trend_component, seasonal_component, residuals |
Arima |
order_p/d/q, seasonal_order_P/D/Q/s, coefficients, aic, bic |
Theta |
variant, theta, alpha |
Tbats |
seasonal_periods, box_cox_lambda, selected_config, aic |
Mfles |
max_rounds, multiplicative, penalty, trend_component, seasonal_component |
Mstl |
seasonal_periods, iterations, trend_component, seasonal_component |
Laplace |
leaf_names, leaf_weights, horizon_dists_json |
Fields not in the current family are NULL. raw_json carries the full serde payload.
Supported models: AutoETS, AutoARIMA, AutoTheta, AutoTBATS, MFLES, AutoMFLES, MSTL, AutoMSTL, Laplace. Others raise does not implement Inspectable.
-- Read AutoETS smoothing params per group
SELECT product_id, (inspection).spec, (inspection).alpha
FROM ts_forecast_inspect_by('sales', product_id, ds, y, 'AutoETS',
{seasonal_period: 12});
ts_forecast_explain_by(source, group_col, date_col, target_col, method, horizon, params?)
Per-horizon decomposition. Returns TABLE(group_col, decomposition STRUCT(horizon, level DOUBLE[], trend DOUBLE[], seasonal DOUBLE[], residual DOUBLE[], named_components_json, raw_json)). Each array has horizon elements; components not produced by a family are NULL.
Supported models: ETS (fixed spec), MSTL, AutoMSTL, Theta. Others raise does not implement Explainable.
-- Reconstruct: yhat = level + trend + seasonal + residual
SELECT product_id, (decomposition).level, (decomposition).trend, (decomposition).seasonal
FROM ts_forecast_explain_by('sales', product_id, ds, y, 'ETS', 12,
{seasonal_period: 12});
See also: anofox-forecast-data-prep (clean input required), anofox-forecast-detection (produce seasonal_period), anofox-forecast-backtest (any model string usable in ts_cv_forecast_by), anofox-forecast-eda (understand series before picking model).
Reference docs:
docs/api/07-forecasting.md(§ Explainability)docs/reference/models/distributional/laplace.md(Laplace deep-dive)docs/guides/02-model-selection.md
Files (anofox-forecast)
-
SKILL.md 24.5 KB
--- name: anofox-forecast-models description: > Forecasting models and the `ts_forecast_by` / `ts_forecast_var_by` API surface of the anofox_forecast DuckDB extension. Covers 36 models (baseline, exponential smoothing, state-space ARIMA + Kalman, classical GARCH, Theta, multi-seasonal, intermittent-demand, distributional Laplace with three variants, panel/global GlobalETS/GlobalTheta/GlobalCroston, and multivariate VAR via ts_forecast_var_by), plus model ensembling (AutoEnsemble via ts_forecast_by, explicit-member ts_forecast_ensemble_by, six combination methods, and member/weight introspection via ts_ensemble_inspect_by / ts_auto_ensemble_inspect_by), parameter surfaces (MAP + STRUCT), model selection guidance, and common workflow gotchas. Use when picking a model, building an ensemble, or writing `ts_forecast_by` / `ts_forecast_agg` / `ts_forecast_var_by` / `ts_forecast_ensemble_by` calls. version: 0.15.3 user-invocable: false --- # Anofox Forecast — Models & `ts_forecast_by` Cheat Sheet **Extension:** `anofox_forecast` v0.15.3 (Rust crate `anofox-forecast` v0.15.3) | **DuckDB:** v1.4.5 LTS / v1.5.4+ | **Dual naming:** `ts_*` and `anofox_fcst_ts_*` 36 forecasting models exposed by SQL via three call surfaces (table macro, aggregate, scalar) + `ts_forecast_var_by` for multivariate VAR. ## Critical gotchas 1. **Seasonality is NOT auto-detected.** All models — including the `Auto*` family — treat `seasonal_period` as user-supplied. Run `ts_detect_periods_by` first and pass the result explicitly. See `anofox-forecast-detection`. 2. **Model names are case-sensitive.** `'AutoETS'` works, `'autoets'` errors. 3. **`ts_forecast_by` requires frequency as the 7th positional param.** No default: ```sql -- WRONG (missing frequency): ts_forecast_by('sales', id, ds, y, 'Naive', 12) -- CORRECT: ts_forecast_by('sales', id, ds, y, 'Naive', 12, '1d') ``` 4. **STRUCT + MAP both work in params.** STRUCT keeps numeric params typed (recommended): ```sql -- STRUCT (recommended) ts_forecast_by(..., 'HoltWinters', 12, '1d', {seasonal_period: 7}) -- MAP (all strings, legacy) ts_forecast_by(..., 'HoltWinters', 12, '1d', MAP{'seasonal_period': '7'}) ``` 5. **The `_by` output renames the target column to `y`** and adds `forecast_step`, `yhat`, `yhat_lower`, `yhat_upper`, `model_name`. 6. **`ts_forecast_agg` takes `(date, value, model, horizon, params)` directly** — no `LIST(...)` wrapping. ## `ts_forecast_by` (primary surface) ```sql ts_forecast_by( source VARCHAR, -- table name (quoted string, NOT a CTE) group_col COLUMN, -- series identifier (unquoted) date_col COLUMN, -- date / timestamp (unquoted) target_col COLUMN, -- value to forecast (unquoted) method VARCHAR, -- e.g. 'AutoETS', 'Laplace' horizon INTEGER, -- number of periods ahead frequency VARCHAR, -- '1d', '1mo', '1h', ... params MAP or STRUCT -- model-specific config ) → TABLE(group_col, forecast_step INT, ds, yhat DOUBLE, yhat_lower, yhat_upper, model_name) ``` ## `ts_forecast_panel_by` (panel / global models) **Fit-once-emit-many** panel API — pools parameter optimization across all series, then predicts per-series. Use when individual series are short but collectively form a large, homogeneous panel. **Three panel methods:** ```sql ts_forecast_panel_by( source VARCHAR, -- table name (quoted string) group_col COLUMN, -- series identifier (unquoted) date_col COLUMN, -- date / timestamp (unquoted) target_col COLUMN, -- value to forecast (unquoted) method VARCHAR, -- 'GlobalETS' | 'GlobalTheta' | 'GlobalCroston' horizon INTEGER, frequency VARCHAR, -- '1d', '1mo', ... params MAP{} -- optional; see below ) → TABLE(group_col, forecast_step INT, date_col TIMESTAMP, yhat DOUBLE, model_name) ``` ### Panel methods | Method | Best for | Key params | |---|---|---| | `'GlobalETS'` | Many related series, shared seasonal dynamics | `seasonal_period` (0=non-seasonal default), `model_pool` ('Reduced' default \| 'Complete') | | `'GlobalTheta'` | Trended panels, minimal config | none (seasonal_period ignored) | | `'GlobalCroston'` | Intermittent/spare-parts panels (many zeros) | `croston_variant` ('Classic' default \| 'SBA') | ### Critical panel gotchas 1. **Series dropped below 10 obs:** Short series after alignment emit `DROPPED: too_short` rows — not errors. Check `model_name` in the result. 2. **Minimum 3 series after drop:** Fewer than 3 kept series → `InvalidInputException`. Ensure your panel has enough history. 3. **Point forecasts only (v1):** `yhat_lower`/`yhat_upper` are not populated. Use `ts_conformal_by` separately if intervals needed. 4. **TABLE arg must be a subselect:** The underlying `_ts_forecast_panel_native` uses the subselect pattern internally. Pass only a table name (quoted string) to `ts_forecast_panel_by` — do NOT pass a CTE or subquery as `source`. 5. **GlobalCroston with all-zero panel fails:** Ensure at least one series has ≥ 2 non-zero demand events in the aligned window. 6. **GlobalETS `seasonal_period=0`** → non-seasonal (Reduced pool, `ANN`/`AAdN`/`MNN`/`MAdN` candidates only). Period=1 has the same effect. ### Panel quick examples ```sql -- GlobalETS weekly seasonal panel SELECT * FROM ts_forecast_panel_by('sales', product_id, ds, y, 'GlobalETS', 14, '1d', MAP {'seasonal_period': '7'}); -- GlobalTheta trended panel (no config needed) SELECT * FROM ts_forecast_panel_by('sales', product_id, ds, y, 'GlobalTheta', 14, '1d'); -- GlobalCroston SBA for spare-parts panel SELECT * FROM ts_forecast_panel_by('spares', item_id, ds, qty, 'GlobalCroston', 6, '1d', MAP {'croston_variant': 'SBA'}); ``` --- ## `ts_forecast_var_by` (multivariate VAR) **Dedicated multivariate function** — distinct from `ts_forecast_by`. Fits a VAR(p) model across K variables simultaneously (cross-variable dynamics). Returns long format. **v1 constraints:** Single-panel only (no `group_col`). Named param `p` for lag order (`order` is a SQL reserved word). Point forecasts only (no intervals). ```sql ts_forecast_var_by( source VARCHAR, -- source table name (quoted string) date_col VARCHAR, -- date column name (quoted string) value_cols VARCHAR[], -- array of value column names ['y1', 'y2', ...] horizon INTEGER, -- periods to forecast frequency VARCHAR, -- time step between observations p INTEGER, -- lag order (named param, default: 1) params MAP -- reserved for future use (default: MAP{}) ) → TABLE(variable VARCHAR, forecast_step BIGINT, <date_col>, forecast_value DOUBLE) ``` **Output:** `k_vars × horizon` rows in long format. One row per (variable, forecast_step). ```sql -- VAR(1) — 2-variable system, 14-step ahead SELECT * REPLACE(ROUND(forecast_value, 6) AS forecast_value) FROM ts_forecast_var_by('var_src', 'ds', ['y1', 'y2'], 14, '1d') ORDER BY variable, forecast_step; -- Returns 28 rows: y1 × 14 + y2 × 14 -- VAR(2) — higher lag order SELECT * FROM ts_forecast_var_by('var_src', 'ds', ['y1', 'y2'], 14, '1d', p:=2); ``` **Pitfalls:** - Use `p:=2` NOT `order:=2` (ORDER is a SQL reserved word). - All value columns must have the same valid observation count after null imputation. - Minimum obs: n > k×p+1 (n=obs, k=variables, p=lag order). - Non-stationary series → unstable coefficient matrix; difference first with `ts_diff_by`. - **Benchmark:** VAR(1) on synthetic VAR(1) data — MAE ratio vs statsmodels = 1.000 (exact match, PASS). --- ## Ensembles (v0.8.0) Ensembles combine multiple member models per series rather than replacing them — they are a **combination surface over the existing 36 models**, not additional model count. Three entry points plus two introspection functions. ### `ts_forecast_by` with `method='AutoEnsemble'` Auto-fits `AutoARIMA`, `AutoETS`, and `AutoTheta`; ranks them by in-sample MSE; combines the top-K members. ```sql ts_forecast_by( source VARCHAR, group_col COLUMN, date_col COLUMN, target_col COLUMN, 'AutoEnsemble', horizon INTEGER, frequency VARCHAR, params MAP or STRUCT -- top_k, combination_method, seasonal_period ) → TABLE(group_col, forecast_step INT, ds, yhat DOUBLE, yhat_lower, yhat_upper, model_name) ``` Params: | Key | Default | Values | |---|---|---| | `top_k` | 3 | Number of top-ranked members to combine | | `combination_method` | `'mean'` | `'mean'`, `'median'`, `'weighted_mse'`, `'inverse_aic'`, `'stacking'`, `'horizon_adaptive'` | | `seasonal_period` | 0 | Shared with member models; 0 = non-seasonal | ```sql -- AutoEnsemble with default params (top_k=3, combination_method='mean') -- (sourced from autoensemble.sql Section 2) SELECT forecast_step, ds, ROUND(yhat, 4) AS yhat, yhat_lower, yhat_upper, model_name FROM ts_forecast_by('ae_test', id, ds, y, 'AutoEnsemble', 5, '1d') ORDER BY forecast_step; -- AutoEnsemble with explicit params (sourced from autoensemble.sql Section 1) SELECT forecast_step, yhat, model_name FROM ts_forecast_by( 'ae_test', id, ds, y, 'AutoEnsemble', 5, '1d', params := {top_k: 3, combination_method: 'mean', seasonal_period: 0} ); ``` **Note:** `yhat_lower` / `yhat_upper` are NULL from `ts_forecast_by('AutoEnsemble', ...)`. Use the conformal pipeline (`ts_cv_folds_by` → `_ts_forecast_scalar` per fold → `ts_conformal_calibrate`) to attach distribution-free intervals — see `anofox-forecast-backtest` and ensemble_intervals.sql. --- ### `ts_forecast_ensemble_by` — explicit member list User-named member models as a `VARCHAR[]`. Any of the 26-member allowlist (all models except `GARCH`, `AutoEnsemble`) may appear. ```sql ts_forecast_ensemble_by( source VARCHAR, group_col COLUMN, date_col COLUMN, target_col COLUMN, members VARCHAR[], -- e.g. ['AutoARIMA', 'AutoETS', 'Theta'] horizon INTEGER, frequency VARCHAR, combination_method := 'mean', -- named param seasonal_period := 0 -- named param ) → TABLE(group_col, forecast_step INT, ds, yhat DOUBLE, yhat_lower DOUBLE, yhat_upper DOUBLE, model_name) ``` ```sql -- Explicit three-member ensemble, mean combination -- (sourced from ensemble_explicit.sql Section 1) SELECT * FROM ts_forecast_ensemble_by( 'ae_test', id, ds, y, ['AutoARIMA', 'AutoETS', 'Theta'], 5, '1d', combination_method := 'mean', seasonal_period := 0 ); -- Seasonal tier-2 members (sourced from ensemble_explicit.sql Section 3 Sample D) SELECT forecast_step, ROUND(yhat, 4) AS yhat FROM ts_forecast_ensemble_by( 'ae_test', id, ds, y, ['SeasonalNaive', 'HoltWinters'], 3, '1d', combination_method := 'mean', seasonal_period := 12); ``` **Errors:** unknown member name, fewer than 2 members, or blocked model (`GARCH`) each raise a descriptive `InvalidInput` exception naming the offending member. --- ### `ts_ensemble_inspect_by` — inspect explicit-member ensemble Returns one row per group per member: `member_name`, `weight`, plus group column. ```sql ts_ensemble_inspect_by( source VARCHAR, group_col COLUMN, date_col COLUMN, target_col COLUMN, members VARCHAR[], combination_method := 'mean', seasonal_period := 0 ) → TABLE(group_col, member_name VARCHAR, weight DOUBLE) ``` ```sql -- Mean combination: weight == 1/k for every member -- (sourced from ensemble_inspect.sql Section 1) SELECT * FROM ts_ensemble_inspect_by( 'insp_series', id, ds, y, ['AutoARIMA', 'AutoETS', 'Theta'], combination_method := 'mean', seasonal_period := 0 ) ORDER BY id, member_name; -- WeightedMSE: inverse-MSE weights; non-negative, sum to 1 per series -- (sourced from ensemble_inspect.sql Section 2) SELECT * FROM ts_ensemble_inspect_by( 'insp_series', id, ds, y, ['AutoARIMA', 'AutoETS', 'Theta'], combination_method := 'weighted_mse', seasonal_period := 0 ) ORDER BY id, member_name; ``` --- ### `ts_auto_ensemble_inspect_by` — inspect AutoEnsemble members Returns one row per group per selected member: `member_name`, `weight`, `score` (in-sample MSE), `rank`. ```sql ts_auto_ensemble_inspect_by( source VARCHAR, group_col COLUMN, date_col COLUMN, target_col COLUMN, top_k := 3, combination_method := 'mean', seasonal_period := 0 ) → TABLE(group_col, member_name VARCHAR, weight DOUBLE, score DOUBLE, rank BIGINT) ``` ```sql -- AutoEnsemble Mean inspection: weight == 1/k, score > 0, rank in 1..k -- (sourced from ensemble_inspect.sql Section 3) SELECT * FROM ts_auto_ensemble_inspect_by( 'insp_series', id, ds, y, top_k := 3, combination_method := 'mean', seasonal_period := 0 ) ORDER BY id, rank; -- AutoEnsemble WeightedMSE: weight IS NULL (see gotcha b below), score > 0 -- (sourced from ensemble_inspect.sql Section 4) SELECT * FROM ts_auto_ensemble_inspect_by( 'insp_series', id, ds, y, top_k := 3, combination_method := 'weighted_mse', seasonal_period := 0 ) ORDER BY id, rank; ``` --- ### Ensemble gotchas **(a) `ts_cv_forecast_by('AutoEnsemble')` segfaults (crate 0.15.3).** The CV native does not parse ensemble-specific params (`ensemble_top_k` / `ensemble_method` are zeroed), causing a runtime crash. Do NOT use `ts_cv_forecast_by` for ensemble conformal CV. Instead, run a manual per-fold `_ts_forecast_scalar` loop: ```sql -- Safe conformal CV path for AutoEnsemble (sourced from ensemble_intervals.sql Section 1) -- Step 1: generate folds CREATE OR REPLACE TABLE ens_folds AS SELECT * FROM ts_cv_folds_by('ens_series', id, ds, y, 3, 5, MAP{}); -- Step 2: forecast per fold with _ts_forecast_scalar (not ts_cv_forecast_by) -- Step 3: ts_conformal_calibrate on residuals -- Step 4: ts_forecast_by('AutoEnsemble') + apply calibrated quantile -- See examples/forecasting/ensemble_intervals.sql for the full pipeline. ``` **(b) `ts_auto_ensemble_inspect_by` returns NULL `weight` for non-`Mean` `combination_method` (crate 0.15.3).** The `AutoEnsemble` struct's inner combination weights are stored in a private field not accessible via the public `all_scores()` / `model_count()` API. Mean combination is the only variant for which weights are returned. `score` (in-sample MSE) is always populated. This is a documented upstream-crate limitation. --- ## `ts_forecast_agg` (aggregate) For custom `GROUP BY` shapes. ```sql ts_forecast_agg(date_col TIMESTAMP, value_col DOUBLE, method VARCHAR, horizon INTEGER, params MAP) → STRUCT(point_forecast DOUBLE[], lower_90 DOUBLE[], upper_90 DOUBLE[], model_name VARCHAR, insample_fitted DOUBLE[], ...) ``` ```sql SELECT product_id, ts_forecast_agg(ds, y, 'AutoETS', 12, MAP{}) AS fcst FROM sales GROUP BY product_id; ``` Access fields: `(fcst).point_forecast`, `(fcst).lower_90`, etc. ## Model catalogue (36) ### Automatic selection (6) | Model | Params | Best for | |---|---|---| | `AutoETS` | `seasonal_period`, `model_pool` | Unknown patterns — default pick | | `AutoARIMA` | `seasonal_period` | Unknown patterns, ARIMA family | | `AutoTheta` | `seasonal_period` | Unknown patterns, Theta family (best RMSE on M5-monthly) | | `AutoMFLES` | `seasonal_periods[]` | Multiple seasonalities | | `AutoMSTL` | `seasonal_periods[]` | Multiple seasonalities | | `AutoTBATS` | `seasonal_periods[]` | Multiple seasonalities | ### Baseline (6) | Model | Required | Optional | |---|---|---| | `Naive` | — | — | | `SMA` | — | `window` (default 5) | | `SeasonalNaive` | `seasonal_period` | — | | `SES` | — | `alpha` (default 0.3) | | `SESOptimized` | — | — | | `RandomWalkDrift` | — | — | ### Exponential smoothing (5) | Model | Required | Optional | |---|---|---| | `Holt` | — | `alpha`, `beta` | | `HoltWinters` | `seasonal_period` | `alpha`, `beta`, `gamma` | | `SeasonalES` | `seasonal_period` | `alpha`, `gamma` | | `SeasonalESOptimized` | `seasonal_period` | — | | `SeasonalWindowAverage` | `seasonal_period` | — | ### Theta (5) | Model | Optional | |---|---| | `Theta` | `seasonal_period`, `theta` | | `OptimizedTheta` | `seasonal_period` | | `DynamicTheta` | `seasonal_period`, `theta` | | `DynamicOptimizedTheta` | `seasonal_period` | | `AutoTheta` | `seasonal_period` (listed above) | ### Classical volatility (1) | Model | Required | Optional | Important | |---|---|---|---| | `GARCH` | — | `garch_p` (default 1), `garch_q` (default 1) | **`yhat` is VOLATILITY (std-dev = sqrt(variance)), NOT variance.** Use on returns (first differences), not raw price levels. Min obs: p+q+10. | ```sql -- GARCH(1,1) — volatility forecast on returns (40 obs minimum > 12) SELECT asset_id, forecast_step, ds, yhat AS conditional_volatility, model_name FROM ts_forecast_by('returns', asset_id, ds, y, 'GARCH', 7, '1d'); -- GARCH(1,1) with explicit params FROM ts_forecast_by('returns', asset_id, ds, y, 'GARCH', 7, '1d', params := MAP{'garch_p':'1','garch_q':'1'}) ``` **Critical:** `yhat` is σ (std-dev), not σ². Square for variance: `yhat * yhat`. **Critical:** Use on returns (LN differences of prices), not raw levels — non-stationary levels cause α+β→1 divergence. **Benchmark:** ratio vs arch package = 0.897 on M4 Daily (PASS). ### State-space / ARIMA (3 — `AutoETS`/`AutoARIMA` counted above) | Model | Required | Optional | |---|---|---| | `ETS` | — | `seasonal_period`, `model` (`'AAA'`, `'AAN'`, …) | | `ARIMA` | `p`, `d`, `q` | `P`, `D`, `Q`, `s` | | `Kalman` | — | `kalman_model` (`'local_level'` default \| `'local_linear_trend'`) | **Kalman details:** - `local_level` (default): random walk + noise; h-step forecast is flat at filtered level. - `local_linear_trend`: level + trend; h-step forecast grows/shrinks linearly. - Uses fixed variance params (obs_var=1.0, level_var=0.1), NOT MLE-estimated. - Benchmark: ratio vs statsmodels UnobservedComponents = 1.000 (local_level) / 0.992 (llt), both PASS. ```sql -- Kalman local_level (default) SELECT * FROM ts_forecast_by('sales', product_id, ds, y, 'Kalman', 14, '1d'); -- Kalman local_linear_trend SELECT * FROM ts_forecast_by('sales', product_id, ds, y, 'Kalman', 14, '1d', params := MAP{'kalman_model': 'local_linear_trend'}); ``` ### Multi-seasonal (3 — `Auto*` counted above) | Model | Required | Optional | |---|---|---| | `MFLES` | `seasonal_periods[]` | `iterations` | | `MSTL` | `seasonal_periods[]` | `stl_method` | | `TBATS` | `seasonal_periods[]` | `use_box_cox` | ### Intermittent demand (6) | Model | Optional | |---|---| | `CrostonClassic` | — | | `CrostonOptimized` | — | | `CrostonSBA` | — | | `ADIDA` | — | | `IMAPA` | — | | `TSB` | `alpha_d`, `alpha_p` | ### Distributional (1) — Laplace Streaming likelihood-weighted mixture of leaves (EMA / drift / AR(1) / damped-Holt + optional seasonal / distribution-family leaves). Three zero-config selectors via `laplace_variant`. | Variant | Best for | Notes | |---|---|---| | `auto` (default) | Smooth continuous series | Balanced defaults | | `auto_aid` | Retail SKU / intermittent counts | AID-based distribution-family leaf selection. Best negative-forecast rate on non-negative counts. | | `skaters` | Fuller ensemble | Multi-h scoring, stacking, larger leaf set. Slower, more robust. | Extra param `laplace_seasonal_batch_init: 1` batch-initialises the seasonal-EMA leaf from the last training cycle. **Safe on stationary / declining amplitude; UNSAFE on growing amplitude or phase-shifted seasonality** (collapses to flat). Default off. ```sql -- Retail-tuned selector on monthly counts SELECT * FROM ts_forecast_by('sales', id, ds, y, 'Laplace', 12, '1mo', {seasonal_period: 12, laplace_variant: 'auto_aid'}); ``` `model_name` tags the state: `Laplace(auto,seasonal=7)`, `Laplace(auto_aid,seasonal=12)`, `Laplace(skaters,seasonal=12,batch_init)`. **Performance context** (M5-monthly, 24 k series × 12-month horizon on 12-core dev box): - Laplace(auto): MAE 18.99, wall 0.5 s - AutoTheta: MAE 19.33, wall 0.7 s - AutoETS: MAE 19.91, wall 15 s (~30× slower) - SeasonalES (baseline): MAE 26.55 ## Parameter surface — universal keys Every model accepts these via `params`: | Key | Type | Notes | |---|---|---| | `seasonal_period` | INTEGER | e.g. 7 (weekly), 12 (monthly), 24 (hourly-daily), 365 (yearly) | | `seasonal_periods` | ARRAY (JSON string in MAP form) | Multi-seasonal: `'[24, 168]'` or `[24, 168]` in STRUCT | | `confidence_level` | DOUBLE | Default 0.90; range (0, 1) | | `window` | INTEGER | For SMA / SeasonalWindowAverage | | `model_pool` | VARCHAR | AutoETS: `'complete'` (default), `'reduced'` | | `laplace_variant` | VARCHAR | `'auto'` / `'auto_aid'` / `'skaters'` — Laplace only | | `laplace_seasonal_batch_init` | INTEGER (0/1) | Opt-in — Laplace only | ## Frequency strings | Format | Examples | |---|---| | Polars-style | `'1d'`, `'1h'`, `'30m'`, `'1w'`, `'1mo'`, `'1q'`, `'1y'` | | DuckDB INTERVAL | `'1 day'`, `'1 hour'` | | Raw integer | `'1'`, `'7'` (days) | ## Model selection — one-liner rules | Data characteristics | First try | Alternative | |---|---|---| | Don't know | `AutoETS` | `Laplace` (auto) | | Trend, no seasonality | `Holt` | `Theta` | | Weekly seasonal | `HoltWinters` | `Laplace` (auto) | | Monthly retail counts | `Laplace` (auto_aid) | `AutoTheta` | | Multiple seasonalities | `MSTL` | `MFLES` | | Intermittent (many zeros) | `CrostonSBA` | `Laplace` (auto_aid) | | Amplitude-declining seasonal | `Laplace` (auto, `batch_init=1`) | `AutoTheta` | | Speed on large panels | `Laplace` (auto) | `SeasonalES` | | Short series (< 20 obs) | `Naive` | `SES` | ## Canonical forecast pipeline ```sql -- 0. Sessions using detection / conformal need json SET autoinstall_known_extensions = 1; SET autoload_known_extensions = 1; -- 1. Detect seasonality CREATE OR REPLACE TABLE periods AS SELECT id, primary_period AS sp FROM ts_detect_periods_by('clean', product_id, ds, y, MAP{}); -- 2. Forecast (panel-modal seasonal_period) CREATE OR REPLACE TABLE forecasts AS SELECT * FROM ts_forecast_by('clean', product_id, ds, y, 'Laplace', 14, '1d', {seasonal_period: (SELECT mode() WITHIN GROUP (ORDER BY sp) FROM periods), laplace_variant: 'auto'}); -- 3. Inspect SELECT product_id, forecast_step, ds, yhat, yhat_lower, yhat_upper, model_name FROM forecasts ORDER BY product_id, forecast_step; ``` ## Explainability surfaces (Tier 6) Two macros expose per-group fit-state and forecast decomposition, each returning a nullable **wide STRUCT** so the shape is stable across model families. Both require the `json` extension. ### `ts_forecast_inspect_by(source, group_col, date_col, target_col, method, params?)` Fit-state snapshot. Signature returns `TABLE(group_col, inspection STRUCT(...))`. `(inspection).model_family` names which sub-fields are populated: | `model_family` | Key fields | |---|---| | `Ets` | `spec`, `alpha`, `beta`, `gamma`, `phi`, `trend_component`, `seasonal_component`, `residuals` | | `Arima` | `order_p/d/q`, `seasonal_order_P/D/Q/s`, `coefficients`, `aic`, `bic` | | `Theta` | `variant`, `theta`, `alpha` | | `Tbats` | `seasonal_periods`, `box_cox_lambda`, `selected_config`, `aic` | | `Mfles` | `max_rounds`, `multiplicative`, `penalty`, `trend_component`, `seasonal_component` | | `Mstl` | `seasonal_periods`, `iterations`, `trend_component`, `seasonal_component` | | `Laplace` | `leaf_names`, `leaf_weights`, `horizon_dists_json` | Fields not in the current family are `NULL`. `raw_json` carries the full serde payload. Supported models: `AutoETS`, `AutoARIMA`, `AutoTheta`, `AutoTBATS`, `MFLES`, `AutoMFLES`, `MSTL`, `AutoMSTL`, `Laplace`. Others raise `does not implement Inspectable`. ```sql -- Read AutoETS smoothing params per group SELECT product_id, (inspection).spec, (inspection).alpha FROM ts_forecast_inspect_by('sales', product_id, ds, y, 'AutoETS', {seasonal_period: 12}); ``` ### `ts_forecast_explain_by(source, group_col, date_col, target_col, method, horizon, params?)` Per-horizon decomposition. Returns `TABLE(group_col, decomposition STRUCT(horizon, level DOUBLE[], trend DOUBLE[], seasonal DOUBLE[], residual DOUBLE[], named_components_json, raw_json))`. Each array has `horizon` elements; components not produced by a family are `NULL`. Supported models: `ETS` (fixed spec), `MSTL`, `AutoMSTL`, `Theta`. Others raise `does not implement Explainable`. ```sql -- Reconstruct: yhat = level + trend + seasonal + residual SELECT product_id, (decomposition).level, (decomposition).trend, (decomposition).seasonal FROM ts_forecast_explain_by('sales', product_id, ds, y, 'ETS', 12, {seasonal_period: 12}); ``` See also: `anofox-forecast-data-prep` (clean input required), `anofox-forecast-detection` (produce `seasonal_period`), `anofox-forecast-backtest` (any model string usable in `ts_cv_forecast_by`), `anofox-forecast-eda` (understand series before picking model). Reference docs: - `docs/api/07-forecasting.md` (§ Explainability) - `docs/reference/models/distributional/laplace.md` (Laplace deep-dive) - `docs/guides/02-model-selection.md`
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.