Download icon
Fail icon
Queries icon
Loop icon
Script icon
If icon
Log icon
Schedule icon

Forecast a FRED Series and Publish a Challenger Only If It Beats Seasonal-Naive

Backtest ETS and Theta against seasonal-naive on free FRED data with rolling-origin MASE, and publish a challenger only if it beats the baseline.

Categories
BusinessData

A forecast is only worth publishing if it beats the simplest reasonable alternative. For seasonal data that alternative is seasonal-naive: repeat last season. This blueprint backtests two challengers, ETS and Theta, against that baseline on any series from FRED (Federal Reserve Economic Data), and publishes a challenger's forecast only if it beats the baseline by a margin you choose. Otherwise it publishes the seasonal-naive forecast and says why. Every decision, every backtest fold and the published forecast are stored in a DuckDB file, so a downstream consumer can tell a real champion from a fallback without reading logs.

It runs with no API keys or accounts. With the defaults (US electric and gas utilities output, monthly), ETS wins with a mean MASE of 1.105 against 1.550 for seasonal-naive, about 29% better, so its forecast is published. With series_id = DGS10, season_length = 5 and horizon = 20, no challenger clears the 10% bar and the flow falls back.

How it works

  1. download (io.kestra.plugin.core.http.Download) fetches the full history of series_id as CSV from FRED's public endpoint. unknown_series (io.kestra.plugin.core.execution.Fail) stops the run with a readable message if FRED returns 404.
  2. clean (io.kestra.plugin.jdbc.duckdb.Queries) turns empty and . values into missing values, writes the remaining observations to clean.csv, and infers the frequency from the median spacing between dates. Two checks then stop the run early with a clear message:
    • irregular_dates: a weekly, monthly, quarterly or yearly series with a hole in it (the message names the first missing date), or a series with no regular spacing. Daily series are not checked for holes, because weekends and holidays are expected gaps.
    • too_short: fewer than n_folds x horizon + season_length + 1 usable observations.
  3. backtest (io.kestra.plugin.core.flow.Loop) runs one iteration per model, all at the same time. Each iteration (io.kestra.plugin.scripts.python.Script) runs a rolling-origin backtest: for each of n_folds cutoffs it trains on everything before the cutoff, forecasts horizon steps and scores them with MASE (out-of-sample MAE divided by the in-sample MAE of a seasonal-naive forecast). It then forecasts the horizon steps after the last observation, so the chosen forecast is never refitted.
    • SeasonalNaive: repeats the last season.
    • ETS: statsmodels ETSModel, choosing by AIC from no, additive or damped trend combined with additive or multiplicative seasonality. Multiplicative options are skipped if any value is zero or negative, which is common for rates, spreads and growth rates.
    • Theta: statsmodels ThetaModel. It tests the lag-season_length autocorrelation at the 10% level and removes seasonality with a classical decomposition only if that test is significant. The configuration each model used in each fold is logged and stored in the config column.
  4. decide (io.kestra.plugin.jdbc.duckdb.Queries) averages MASE per model and picks the champion. A challenger is published only if its MASE is more than min_improvement below the baseline's. If the champion is seasonal-naive itself, including an exact tie, or the margin is too small, the flow falls back to seasonal-naive.
  5. gate (io.kestra.plugin.core.flow.If) logs the decision: INFO when the challenger is published, WARN with the reason when it falls back.

What you get

  • {{ outputs.decide.databaseUri }}, also exposed as the flow output results: a DuckDB file with four tables.
    • decision: series_id, run_at, champion, champion_mase, baseline_mase, min_improvement, model_used, is_fallback and reason.
    • forecast: the published forecast (step, ds, yhat) with the model that produced it.
    • backtest_scores: MASE, MAE and model configuration for every model and fold.
    • candidate_forecasts: the forecasts of all three models.
  • Flow outputs model_used, is_fallback and reason, for a parent flow (Subflow task) or an API client to act on.

Who it's for

Analysts and data scientists who publish forecasts of economic or business time series and want an objective, repeatable check that a model earns its place before its numbers reach a report.

Why orchestrate this with Kestra

  • The champion/challenger rule runs on every refresh, not when someone remembers to check, and each decision is kept with the exact data and folds that produced it.
  • The three backtests run in parallel from one task definition, and bad input stops the run early with a message a non-specialist can act on.
  • Logging fallbacks at WARN makes them easy to filter for, and flow outputs let other flows consume the result.

Prerequisites

  • Docker available to the Kestra worker. The model tasks run in python:3.13.16-slim-trixie and install a fully pinned set of 13 packages (statsmodels 0.15.0 and its dependencies) with pip. A run takes about 1.5 to 3 minutes, almost all of it installing packages: each of the three parallel model tasks downloads about 78 MB, which took from 15 seconds to about 3 minutes in testing depending on the connection. Model fitting takes seconds. Each model task has a 15-minute timeout, and pip retries interrupted downloads.
  • Outbound HTTPS to fred.stlouisfed.org and to PyPI. No secrets are needed.

Inputs

Input Type Default Purpose
series_id STRING IPG2211A2N FRED series ID. Letters, digits, _, . and - only.
season_length INT 12 Observations per seasonal cycle: 12 for monthly, 4 for quarterly, 5 for business-day weekly patterns.
horizon INT 12 Steps to forecast, and the length of each backtest fold.
n_folds INT 5 Rolling-origin folds, 1 to 20.
min_improvement FLOAT 0.10 Required relative MASE improvement over seasonal-naive, between 0 and 1.

Common pitfalls

  • The schedule. The monthly trigger runs at 09:00 New York time on the 20th, after the Federal Reserve's industrial production release. Change cron to match your series' release calendar.
  • FRED and the User-Agent header. FRED drops requests from Java's default User-Agent and from bare names, and the download then hangs until it times out. Keep the User-Agent header on download.
  • season_length must match the data. A monthly series with season_length 5 still runs, but every model will be scored against the wrong seasonal pattern.
  • Long daily series. DGS10 has more than 16,000 observations. All three models handle it, but ETS fits six configurations per fold, so expect longer compute than for monthly data.

Links

See How

New to Kestra?

Use blueprints to kickstart your first workflows.