Real-time forecasting of US retail-sales YoY growth from ALFRED vintages (T1-forecast)
Question
Using only the data published at each of 10 month-end forecast origins (2025-10-31 … 2026-07-31; first_origin, last_origin, n_origins), how well can the year-over-year log growth of US retail and food services sales (RSAFS), g_m = 100·ln(L_m/L_{m−12}), be forecast 1 and 3 months beyond the last published month, and does a model of my own beat the naive no-change forecast when errors are measured against the first-release YoY of each target month?
Data and exclusions
- Inputs: 10 RSAFS and 10 PAYEMS ALFRED vintages (one per origin), UMCSENT (current vintage), RSAFS_current, first_release.csv. Row counts are in
n_input_rows; every SHA-256 matcheddata/README.md(out/state/data_audit.md). - Real-time design. The forecast at origin V uses only the RSAFS vintage dated V (
vintage_used=origin_datefor all 80 rows of out/forecasts.csv). Targets are last(V)+h, where last(V) is the last month in that vintage. Publication delays seen in the vintages: at the six origins 2025-10-31 … 2026-03-31 the last published month is two months before the origin month (last(V) = 2025-08 … 2026-01). The 2026-04-30 vintage adds two months at once (2026-02, 2026-03), and the last four origins have the usual one-month delay (last(V) = 2026-03 … 2026-06). Per-origin last(V) and delay are stored inreal_time_design. Because of this, month 2026-03 is never an h=1 target, and for the first six origins the h=1 "forecast" is really a backcast of a month that had already ended but was not yet published. - Actuals:
yoy_first_releasefrom first_release.csv. Where the first-release vintage is one of the shipped vintages, its values match that vintage exactly (check C4). - RSAFS_current is used only for the revision estimand. PAYEMS and UMCSENT are read (for row counts) but are not used by any forecast.
- Exclusions (out/exclusions.csv): none. All 80 forecast–actual pairs (10 origins × 2 horizons × 4 models) have an actual.
n_analysed= 20 model pairs (10 per horizon). - Data issues found, with how each was handled, are listed in
data_issues: irregular publication timing; PAYEMS gaps and a benchmark revision; pre-1978 UMCSENT gaps; a benchmark revision in RSAFS_current that also affects the first-release levels of 2026-08 and 2026-09; and routine monthly revisions.
Method
- Baselines, computed from vintage V as TASK.md specifies: naive = g_{last(V)}; seasonal naive = g_{T−12}; AR(1) = OLS of g_m on a constant and g_{m−1} for 1993-02 ≤ m ≤ last(V), iterated h steps.
- Own model,
model_name= rw_drift_yoy (seemodel_description). This is a base-effect-aware random walk with drift on log levels. The drift μ is the mean monthly log change over the 36 months ending at last(V) in vintage V. The forecast is ĝ_T = 100·(ln L_last + h·μ − ln L_{T−12}), where the base-month level L_{T−12} is already published in vintage V. The point of the model is that YoY growth is a 12-month sum: 12−h of its monthly components are already known at the origin, and only h months need forecasting. The naive forecast instead implicitly assumes that the month dropping out of the 12-month window equals the month coming in. I fixed the specification and the 36-month window before computing any forecast error (out/state/log.md). Nothing was tuned on the evaluation sample, and no alternatives were tried. - Evaluation: RMSE and MAE over the 10 origins per horizon. Diebold–Mariano test of model vs naive: d_t = e²_naive − e²_model, with Bartlett long-run variance at truncation lag h−1 (h=3 weights 2/3, 1/3), no HLN factor, two-sided normal p-value. As an additional distribution-free measure, I report an exact two-sided sign test on the sign of d_t (
dm_details). - Revision estimand: the mean over the 10 h=1 target months of |g^current_T − yoy_first_release_T|.
- No randomness is used (
random_seed= null). out/analysis.py regenerates every output deterministically.
Results
RMSE / MAE in percentage points of YoY growth (out/table1.csv):
| model | h=1 RMSE | h=1 MAE | h=3 RMSE | h=3 MAE |
|---|---|---|---|---|
| naive | 0.997 (rmse_naive_h1) |
0.849 (mae_naive_h1) |
1.420 (rmse_naive_h3) |
1.200 (mae_naive_h3) |
| seasonal naive | 1.675 (rmse_snaive_h1) |
1.307 (mae_snaive_h1) |
1.316 (rmse_snaive_h3) |
1.066 (mae_snaive_h3) |
| AR(1) | 0.941 (rmse_ar1_h1) |
0.798 (mae_ar1_h1) |
1.187 (rmse_ar1_h3) |
0.953 (mae_ar1_h3) |
| rw_drift_yoy | 0.552 (rmse_model_h1) |
0.497 (mae_model_h1) |
1.054 (rmse_model_h3) |
0.747 (mae_model_h3) |
- The model has the lowest RMSE and MAE of the four methods at both horizons.
- Its RMSE is 44.6% lower than the naive RMSE at h=1 (
rel_improvement_rmse_h1= 0.446) and 25.8% lower at h=3 (rel_improvement_rmse_h3= 0.258). - AR(1) beats naive by about 6% (h=1) and 16% (h=3) in RMSE (ratios of the
rmse_ar1_h*andrmse_naive_h*keys). Seasonal naive is the worst method at h=1, but it beats naive at h=3. - Diebold–Mariano, model vs naive:
- h=1: DM = 2.30, p = 0.021 (
dm_stat_h1,dm_pvalue_h1), givingconclusion_code_h1= model_better_significant. - h=3: DM = 1.86, p = 0.062 (
dm_stat_h3,dm_pvalue_h3), givingconclusion_code_h3= model_better_not_significant.
- h=1: DM = 2.30, p = 0.021 (
- Revisions: the first-release YoY of the 10 h=1 target months differs from today's published YoY by 0.548 pp on average (
mean_abs_revision_h1_targets; per-month values inabs_revision_h1_by_target, range 0.17–0.98). This is the same order of magnitude as the model's h=1 RMSE, so a substantial part of what a "forecast error" against first release measures is measurement noise in the first release relative to today's data. - Figure: out/figure1.png shows first-release actuals and the naive, AR(1) and model forecasts by target month, one panel per horizon.
Uncertainty
- There are only n = 10 origins per horizon, and at h=3 the errors from neighbouring origins overlap. The DM statistic relies on asymptotic normality and on a long-run variance estimated from 10 points. With samples this small, DM tests without the HLN correction are known to over-reject, so the h=1 p-value of 0.021 (
dm_pvalue_h1) should be read as indicative, not conclusive. - A distribution-free check is weaker. The model had a lower squared error than naive at 8 of 10 origins at each horizon, and the exact two-sided sign-test p-value is 0.109 for both (
dm_details). This is not significant at 5%. - At h=3 the DM p-value of 0.062 (
dm_pvalue_h3) is above 5%. - The evaluation window covers one short period (target months 2025-09 to 2026-09). It includes an acceleration of YoY growth to 6.5–6.7% in 2026-05 and 2026-06 (first release), and it contains publication disruptions. The RMSE ranking could differ in other periods.
- I computed no bootstrap interval, since 10 dependent observations would make one unreliable.
Limitations and open issues
- Gate conflict (structural, reported, not fixed). The output-contract gate's
forecasts_chronologyrule (origin_date < target_date) fails for all 40 h=1 rows. TASK.md defines the target as last(V)+h and formatstarget_dateas YYYY-MM-01. With month-end origins, an h=1 target month always starts before the origin date, and for the six two-month-delay origins it has ended entirely before it. No date format that respects TASK.md can satisfy this rule, so I kept the TASK.md definition (TASK.md is authoritative). Information validity is unaffected: no target month is present in the vintage used, and vintage_used ≤ origin_date for every row (out/state/checks.md, C3, C10, C13). The latest gate receipt therefore reports passed = false on this single check only. - With n = 10 origins, all significance statements are fragile (see Uncertainty).
- The model is univariate. PAYEMS and UMCSENT were not used, so I cannot say whether they would help. I deliberately ran no specification search on 10 origins.
- The actuals mix pre- and post-benchmark vintages (2026-08 and 2026-09 come from post-benchmark vintages). YoY ratios within a vintage largely cancel level shifts, but this is a possible source of extra error at the last h=3 targets.
- The two-month publication delays at six origins mean the h=1 results partly measure backcasting, not forecasting, ability.
- Workflow incident: a shell-directory slip briefly created a folder
data/out/(containing only my own state notes). I removed it at once. No input file was modified, and all input SHA-256 values were re-verified afterwards (out/state/log.md).