anofox-forecast-backtest
Backtesting, cross-validation, evaluation metrics, and conformal prediction intervals for the anofox_forecast DuckDB extension. Use when evaluating forecast accuracy, comparing models with time-series-aware CV, computing metrics (MAE / RMSE / MAPE / MASE / coverage), or attaching
Install
npx skills add https://github.com/DataZooDE/anofox-forecast/tree/main/.claude/skills/anofox-forecast-backtest
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 — Backtesting, CV, Metrics & Conformal Cheat Sheet
Extension: anofox_forecast v0.15.3 | DuckDB: v1.4.5 LTS / v1.5.4+ | Dual naming: ts_* and anofox_fcst_ts_*
Time-series-aware cross-validation, error metrics, and distribution-free intervals.
Critical gotchas
ts_backtest_auto_bywas REMOVED. Use the two-step CV workflow (ts_cv_folds_by→ts_cv_forecast_by) instead. Older docs and tests may still reference the retired one-liner.- Metric
_bytable macros (ts_mae_by,ts_rmse_by, …) are deprecated. They're ~2400× slower than the scalar +GROUP BYpattern and don't parallelise. Use scalars. - Always
ORDER BYinsideLIST()for temporal correctness:LIST(y ORDER BY ds), notLIST(y). ts_cv_forecast_byoutput renames the target column toy(canonical). Don't try to access the original name.- Folds must be pre-computed before forecasting. Passing raw data to
ts_cv_forecast_bythrows a clear error.
The CV two-step workflow (standard)
-- Step 1: Create fold table (train/test rows with actual dates)
CREATE OR REPLACE TABLE cv_folds AS
SELECT * FROM ts_cv_folds_by('data', unique_id, ds, y,
3, -- n_folds
12, -- horizon per fold
MAP{}); -- optional params
-- Step 2: Forecast per fold's train set, predict its test set
CREATE OR REPLACE TABLE cv_forecasts AS
SELECT * FROM ts_cv_forecast_by('cv_folds', unique_id, ds, y,
'AutoETS',
MAP{'seasonal_period': '12'});
-- Step 3: Compute per-series / per-fold metrics
SELECT unique_id, fold_id,
ts_rmse(LIST(y ORDER BY ds), LIST(yhat ORDER BY ds)) AS rmse,
ts_mae(LIST(y ORDER BY ds), LIST(yhat ORDER BY ds)) AS mae
FROM cv_forecasts
GROUP BY unique_id, fold_id;
ts_cv_folds_by
ts_cv_folds_by(source VARCHAR, group_col COLUMN, date_col COLUMN, target_col COLUMN,
n_folds BIGINT, horizon BIGINT, params MAP/STRUCT) → TABLE
Input must be pre-cleaned (no gaps, consistent frequency). Uses position-based indexing so works with all frequencies (including calendar-based monthly / quarterly / yearly).
Params:
| Key | Default | Description |
|---|---|---|
gap |
0 | Periods between train end and test start |
embargo |
0 | Periods excluded from training after previous test |
window_type |
'expanding' |
'expanding', 'fixed', or 'sliding' |
min_train_size |
1 | Min training size (fixed / sliding only) |
initial_train_size |
auto | Periods before first fold |
skip_length |
horizon | Periods between folds (1 = dense overlap) |
clip_horizon |
false | Allow partial test windows near series end |
Output: 5 columns — <group_col>, <date_col>, <target_col>, fold_id BIGINT, split VARCHAR ('train' / 'test'). Features NOT passed through — use ts_cv_hydrate_by to join extra columns.
ts_cv_split_by / ts_cv_split_folds_by / ts_cv_split_index_by
Alternate fold-creation entry points for custom cutoffs, pre-computed fold boundaries, or memory-efficient index-only splits. See docs/api/08-cross-validation.md for their param signatures.
ts_cv_forecast_by
ts_cv_forecast_by(cv_folds_table VARCHAR, group_col COLUMN, date_col COLUMN, target_col COLUMN,
method VARCHAR, params MAP/STRUCT) → TABLE
Fits method on each fold's train partition, forecasts the test partition.
Output columns: fold_id, <group_col>, <date_col>, y (renamed from target_col), split, yhat, yhat_lower, yhat_upper, model_name.
Params: same shape as ts_forecast_by. All model strings work ('AutoETS', 'Laplace', 'HoltWinters', etc.). See anofox-forecast-models for the full catalogue.
ts_cv_hydrate_by — attach features to fold rows
If your model needs exogenous columns (features, calendar flags, promotions), the folds table lost them. Join them back:
CREATE OR REPLACE TABLE cv_folds_with_features AS
SELECT * FROM ts_cv_hydrate_by('cv_folds', unique_id, ds, y, 'features', ['promo', 'holiday']);
ts_check_leakage
Sanity-check that no test-window rows leaked into training:
SELECT * FROM ts_check_leakage('cv_folds', unique_id, ds, fold_id, split);
Metrics — use scalar functions
Pattern: SELECT group, ts_<metric>(LIST(actual ORDER BY ds), LIST(pred ORDER BY ds)) FROM ... GROUP BY group;
| Function | Signature | Description |
|---|---|---|
ts_mae |
(DOUBLE[], DOUBLE[]) → DOUBLE |
Mean Absolute Error |
ts_mse |
(DOUBLE[], DOUBLE[]) → DOUBLE |
Mean Squared Error |
ts_rmse |
(DOUBLE[], DOUBLE[]) → DOUBLE |
Root MSE |
ts_mape |
(DOUBLE[], DOUBLE[]) → DOUBLE |
Mean Absolute % Error |
ts_smape |
(DOUBLE[], DOUBLE[]) → DOUBLE |
Symmetric MAPE |
ts_r2 |
(DOUBLE[], DOUBLE[]) → DOUBLE |
R² |
ts_bias |
(DOUBLE[], DOUBLE[]) → DOUBLE |
Bias (mean error) |
ts_mase |
(actual, forecast, baseline) → DOUBLE |
Scaled by seasonal naive |
ts_rmae |
(actual, pred1, pred2) → DOUBLE |
Relative MAE. < 1 = pred1 better |
ts_coverage |
(actual, lower, upper) → DOUBLE |
Interval coverage rate |
ts_quantile_loss |
(actual, forecast, q) → DOUBLE |
Quantile loss at level q |
ts_mqloss |
(actual, quantiles[], levels[]) → DOUBLE |
Multi-quantile CRPS-adjacent |
Model comparison across CV folds
-- Fit two models
CREATE OR REPLACE TABLE cv_naive AS
SELECT * FROM ts_cv_forecast_by('cv_folds', id, ds, y, 'Naive', MAP{});
CREATE OR REPLACE TABLE cv_autoets AS
SELECT * FROM ts_cv_forecast_by('cv_folds', id, ds, y, 'AutoETS', MAP{'seasonal_period': '7'});
-- Compare
SELECT 'Naive' AS model,
ts_mae(LIST(y ORDER BY ds), LIST(yhat ORDER BY ds)) AS mae
FROM cv_naive
UNION ALL
SELECT 'AutoETS',
ts_mae(LIST(y ORDER BY ds), LIST(yhat ORDER BY ds))
FROM cv_autoets
ORDER BY mae;
Conformal prediction — distribution-free intervals
Attach a coverage-guaranteed prediction interval to forecasts, calibrated from backtest residuals.
Three flavours
| Approach | Functions | Use when |
|---|---|---|
| One-step | ts_conformal_by |
You already have backtest results, want intervals now |
| Modular | ts_conformal_calibrate + ts_conformal_apply_by |
Reuse calibration across multiple forecast rounds |
| Array-based | ts_conformal_predict, ts_conformal_quantile, etc. |
Custom pipelines over LIST(...) arrays |
ts_conformal_by (one-step)
ts_conformal_by(backtest_results VARCHAR, group_col COLUMN, actual_col COLUMN,
forecast_col COLUMN, point_forecast_col COLUMN, params STRUCT) → TABLE
Params:
| Key | Default | Description |
|---|---|---|
alpha |
0.1 | Miscoverage rate (0.1 = 90 %, 0.05 = 95 %) |
method |
'split' |
'split' (symmetric) or 'asymmetric' (skewed residuals) |
CREATE OR REPLACE TABLE conformal_bounds AS
SELECT * FROM ts_conformal_by('backtest', unique_id, y, yhat, yhat, {alpha: 0.1});
Modular (ts_conformal_calibrate → ts_conformal_apply_by)
-- Compute calibration once
CREATE OR REPLACE TABLE calib AS
SELECT * FROM ts_conformal_calibrate('backtest', y, yhat, {alpha: 0.1});
-- Apply to any future forecast
SELECT * FROM ts_conformal_apply_by('new_forecasts', unique_id, yhat,
(SELECT conformity_score FROM calib));
Array-based helpers
ts_conformal_predict,ts_conformal_predict_asymmetric,ts_conformal_predict_per_stepts_conformal_quantile,ts_conformal_intervalsts_conformal_learn,ts_conformal_applyts_conformal_coverage,ts_conformal_evaluate
Use these when composing custom pipelines over LIST(residual) arrays.
ts_estimate_backtest_memory
Pre-flight a CV run's memory footprint:
SELECT * FROM ts_estimate_backtest_memory(n_series, avg_length, n_folds, horizon);
Full backtest → conformal pipeline
-- 1. CV folds
CREATE OR REPLACE TABLE folds AS
SELECT * FROM ts_cv_folds_by('clean', product_id, ds, y, 5, 14, MAP{});
-- 2. Model forecast per fold
CREATE OR REPLACE TABLE bt AS
SELECT * FROM ts_cv_forecast_by('folds', product_id, ds, y, 'AutoETS',
MAP{'seasonal_period': '7'});
-- 3. Per-series metrics
SELECT product_id,
ts_mae(LIST(y ORDER BY ds), LIST(yhat ORDER BY ds)) AS mae,
ts_rmse(LIST(y ORDER BY ds), LIST(yhat ORDER BY ds)) AS rmse,
ts_coverage(LIST(y ORDER BY ds), LIST(yhat_lower ORDER BY ds), LIST(yhat_upper ORDER BY ds)) AS cov
FROM bt GROUP BY product_id;
-- 4. Conformalise → guaranteed 90 % coverage
CREATE OR REPLACE TABLE calib AS
SELECT * FROM ts_conformal_calibrate('bt', y, yhat, {alpha: 0.1});
-- 5. Apply calibration to new forecasts
CREATE OR REPLACE TABLE new_fcst AS
SELECT * FROM ts_forecast_by('clean', product_id, ds, y, 'AutoETS', 14, '1d',
MAP{'seasonal_period': '7'});
SELECT * FROM ts_conformal_apply_by('new_fcst', product_id, yhat,
(SELECT conformity_score FROM calib));
See also: anofox-forecast-data-prep (clean input required by ts_cv_folds_by), anofox-forecast-detection (detect seasonal_period before backtesting), anofox-forecast-models (any model string usable in ts_cv_forecast_by).
Reference docs:
docs/api/08-cross-validation.mddocs/api/09-evaluation-metrics.mddocs/api/11-conformal-prediction.md
Files (anofox-forecast)
-
SKILL.md 9.6 KB
--- name: anofox-forecast-backtest description: > Backtesting, cross-validation, evaluation metrics, and conformal prediction intervals for the anofox_forecast DuckDB extension. Use when evaluating forecast accuracy, comparing models with time-series-aware CV, computing metrics (MAE / RMSE / MAPE / MASE / coverage), or attaching distribution-free prediction intervals to forecasts. version: 0.15.3 user-invocable: false --- # Anofox Forecast — Backtesting, CV, Metrics & Conformal Cheat Sheet **Extension:** `anofox_forecast` v0.15.3 | **DuckDB:** v1.4.5 LTS / v1.5.4+ | **Dual naming:** `ts_*` and `anofox_fcst_ts_*` Time-series-aware cross-validation, error metrics, and distribution-free intervals. ## Critical gotchas - **`ts_backtest_auto_by` was REMOVED.** Use the two-step CV workflow (`ts_cv_folds_by` → `ts_cv_forecast_by`) instead. Older docs and tests may still reference the retired one-liner. - **Metric `_by` table macros (`ts_mae_by`, `ts_rmse_by`, …) are deprecated.** They're ~2400× slower than the scalar + `GROUP BY` pattern and don't parallelise. Use scalars. - **Always `ORDER BY` inside `LIST()`** for temporal correctness: `LIST(y ORDER BY ds)`, not `LIST(y)`. - **`ts_cv_forecast_by` output renames the target column to `y`** (canonical). Don't try to access the original name. - **Folds must be pre-computed before forecasting.** Passing raw data to `ts_cv_forecast_by` throws a clear error. ## The CV two-step workflow (standard) ```sql -- Step 1: Create fold table (train/test rows with actual dates) CREATE OR REPLACE TABLE cv_folds AS SELECT * FROM ts_cv_folds_by('data', unique_id, ds, y, 3, -- n_folds 12, -- horizon per fold MAP{}); -- optional params -- Step 2: Forecast per fold's train set, predict its test set CREATE OR REPLACE TABLE cv_forecasts AS SELECT * FROM ts_cv_forecast_by('cv_folds', unique_id, ds, y, 'AutoETS', MAP{'seasonal_period': '12'}); -- Step 3: Compute per-series / per-fold metrics SELECT unique_id, fold_id, ts_rmse(LIST(y ORDER BY ds), LIST(yhat ORDER BY ds)) AS rmse, ts_mae(LIST(y ORDER BY ds), LIST(yhat ORDER BY ds)) AS mae FROM cv_forecasts GROUP BY unique_id, fold_id; ``` ## `ts_cv_folds_by` ```sql ts_cv_folds_by(source VARCHAR, group_col COLUMN, date_col COLUMN, target_col COLUMN, n_folds BIGINT, horizon BIGINT, params MAP/STRUCT) → TABLE ``` Input must be pre-cleaned (no gaps, consistent frequency). Uses position-based indexing so works with all frequencies (including calendar-based monthly / quarterly / yearly). Params: | Key | Default | Description | |---|---|---| | `gap` | 0 | Periods between train end and test start | | `embargo` | 0 | Periods excluded from training after previous test | | `window_type` | `'expanding'` | `'expanding'`, `'fixed'`, or `'sliding'` | | `min_train_size` | 1 | Min training size (fixed / sliding only) | | `initial_train_size` | auto | Periods before first fold | | `skip_length` | horizon | Periods between folds (1 = dense overlap) | | `clip_horizon` | false | Allow partial test windows near series end | Output: 5 columns — `<group_col>`, `<date_col>`, `<target_col>`, `fold_id BIGINT`, `split VARCHAR` (`'train'` / `'test'`). Features NOT passed through — use `ts_cv_hydrate_by` to join extra columns. ## `ts_cv_split_by` / `ts_cv_split_folds_by` / `ts_cv_split_index_by` Alternate fold-creation entry points for custom cutoffs, pre-computed fold boundaries, or memory-efficient index-only splits. See `docs/api/08-cross-validation.md` for their param signatures. ## `ts_cv_forecast_by` ```sql ts_cv_forecast_by(cv_folds_table VARCHAR, group_col COLUMN, date_col COLUMN, target_col COLUMN, method VARCHAR, params MAP/STRUCT) → TABLE ``` Fits `method` on each fold's train partition, forecasts the test partition. Output columns: `fold_id`, `<group_col>`, `<date_col>`, `y` (renamed from target_col), `split`, `yhat`, `yhat_lower`, `yhat_upper`, `model_name`. Params: same shape as `ts_forecast_by`. All model strings work (`'AutoETS'`, `'Laplace'`, `'HoltWinters'`, etc.). See `anofox-forecast-models` for the full catalogue. ## `ts_cv_hydrate_by` — attach features to fold rows If your model needs exogenous columns (features, calendar flags, promotions), the folds table lost them. Join them back: ```sql CREATE OR REPLACE TABLE cv_folds_with_features AS SELECT * FROM ts_cv_hydrate_by('cv_folds', unique_id, ds, y, 'features', ['promo', 'holiday']); ``` ## `ts_check_leakage` Sanity-check that no test-window rows leaked into training: ```sql SELECT * FROM ts_check_leakage('cv_folds', unique_id, ds, fold_id, split); ``` ## Metrics — use scalar functions Pattern: `SELECT group, ts_<metric>(LIST(actual ORDER BY ds), LIST(pred ORDER BY ds)) FROM ... GROUP BY group;` | Function | Signature | Description | |---|---|---| | `ts_mae` | `(DOUBLE[], DOUBLE[]) → DOUBLE` | Mean Absolute Error | | `ts_mse` | `(DOUBLE[], DOUBLE[]) → DOUBLE` | Mean Squared Error | | `ts_rmse` | `(DOUBLE[], DOUBLE[]) → DOUBLE` | Root MSE | | `ts_mape` | `(DOUBLE[], DOUBLE[]) → DOUBLE` | Mean Absolute % Error | | `ts_smape` | `(DOUBLE[], DOUBLE[]) → DOUBLE` | Symmetric MAPE | | `ts_r2` | `(DOUBLE[], DOUBLE[]) → DOUBLE` | R² | | `ts_bias` | `(DOUBLE[], DOUBLE[]) → DOUBLE` | Bias (mean error) | | `ts_mase` | `(actual, forecast, baseline) → DOUBLE` | Scaled by seasonal naive | | `ts_rmae` | `(actual, pred1, pred2) → DOUBLE` | Relative MAE. < 1 = pred1 better | | `ts_coverage` | `(actual, lower, upper) → DOUBLE` | Interval coverage rate | | `ts_quantile_loss` | `(actual, forecast, q) → DOUBLE` | Quantile loss at level q | | `ts_mqloss` | `(actual, quantiles[], levels[]) → DOUBLE` | Multi-quantile CRPS-adjacent | ### Model comparison across CV folds ```sql -- Fit two models CREATE OR REPLACE TABLE cv_naive AS SELECT * FROM ts_cv_forecast_by('cv_folds', id, ds, y, 'Naive', MAP{}); CREATE OR REPLACE TABLE cv_autoets AS SELECT * FROM ts_cv_forecast_by('cv_folds', id, ds, y, 'AutoETS', MAP{'seasonal_period': '7'}); -- Compare SELECT 'Naive' AS model, ts_mae(LIST(y ORDER BY ds), LIST(yhat ORDER BY ds)) AS mae FROM cv_naive UNION ALL SELECT 'AutoETS', ts_mae(LIST(y ORDER BY ds), LIST(yhat ORDER BY ds)) FROM cv_autoets ORDER BY mae; ``` ## Conformal prediction — distribution-free intervals Attach a coverage-guaranteed prediction interval to forecasts, calibrated from backtest residuals. ### Three flavours | Approach | Functions | Use when | |---|---|---| | **One-step** | `ts_conformal_by` | You already have backtest results, want intervals now | | **Modular** | `ts_conformal_calibrate` + `ts_conformal_apply_by` | Reuse calibration across multiple forecast rounds | | **Array-based** | `ts_conformal_predict`, `ts_conformal_quantile`, etc. | Custom pipelines over `LIST(...)` arrays | ### `ts_conformal_by` (one-step) ```sql ts_conformal_by(backtest_results VARCHAR, group_col COLUMN, actual_col COLUMN, forecast_col COLUMN, point_forecast_col COLUMN, params STRUCT) → TABLE ``` Params: | Key | Default | Description | |---|---|---| | `alpha` | 0.1 | Miscoverage rate (0.1 = 90 %, 0.05 = 95 %) | | `method` | `'split'` | `'split'` (symmetric) or `'asymmetric'` (skewed residuals) | ```sql CREATE OR REPLACE TABLE conformal_bounds AS SELECT * FROM ts_conformal_by('backtest', unique_id, y, yhat, yhat, {alpha: 0.1}); ``` ### Modular (`ts_conformal_calibrate` → `ts_conformal_apply_by`) ```sql -- Compute calibration once CREATE OR REPLACE TABLE calib AS SELECT * FROM ts_conformal_calibrate('backtest', y, yhat, {alpha: 0.1}); -- Apply to any future forecast SELECT * FROM ts_conformal_apply_by('new_forecasts', unique_id, yhat, (SELECT conformity_score FROM calib)); ``` ### Array-based helpers - `ts_conformal_predict`, `ts_conformal_predict_asymmetric`, `ts_conformal_predict_per_step` - `ts_conformal_quantile`, `ts_conformal_intervals` - `ts_conformal_learn`, `ts_conformal_apply` - `ts_conformal_coverage`, `ts_conformal_evaluate` Use these when composing custom pipelines over `LIST(residual)` arrays. ## `ts_estimate_backtest_memory` Pre-flight a CV run's memory footprint: ```sql SELECT * FROM ts_estimate_backtest_memory(n_series, avg_length, n_folds, horizon); ``` ## Full backtest → conformal pipeline ```sql -- 1. CV folds CREATE OR REPLACE TABLE folds AS SELECT * FROM ts_cv_folds_by('clean', product_id, ds, y, 5, 14, MAP{}); -- 2. Model forecast per fold CREATE OR REPLACE TABLE bt AS SELECT * FROM ts_cv_forecast_by('folds', product_id, ds, y, 'AutoETS', MAP{'seasonal_period': '7'}); -- 3. Per-series metrics SELECT product_id, ts_mae(LIST(y ORDER BY ds), LIST(yhat ORDER BY ds)) AS mae, ts_rmse(LIST(y ORDER BY ds), LIST(yhat ORDER BY ds)) AS rmse, ts_coverage(LIST(y ORDER BY ds), LIST(yhat_lower ORDER BY ds), LIST(yhat_upper ORDER BY ds)) AS cov FROM bt GROUP BY product_id; -- 4. Conformalise → guaranteed 90 % coverage CREATE OR REPLACE TABLE calib AS SELECT * FROM ts_conformal_calibrate('bt', y, yhat, {alpha: 0.1}); -- 5. Apply calibration to new forecasts CREATE OR REPLACE TABLE new_fcst AS SELECT * FROM ts_forecast_by('clean', product_id, ds, y, 'AutoETS', 14, '1d', MAP{'seasonal_period': '7'}); SELECT * FROM ts_conformal_apply_by('new_fcst', product_id, yhat, (SELECT conformity_score FROM calib)); ``` See also: `anofox-forecast-data-prep` (clean input required by `ts_cv_folds_by`), `anofox-forecast-detection` (detect `seasonal_period` before backtesting), `anofox-forecast-models` (any model string usable in `ts_cv_forecast_by`). Reference docs: - `docs/api/08-cross-validation.md` - `docs/api/09-evaluation-metrics.md` - `docs/api/11-conformal-prediction.md`
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.