Real-time forecasting of US retail-sales YoY growth from data vintages (T1-forecast)
Question
Could the year-over-year growth of US retail and food services sales (RSAFS, g_m = 100·ln(L_m/L_{m−12}), percent) have been forecast in real time at 10 month-end origins (first_origin = 2025-10-31 to last_origin = 2026-07-31, n_origins = 10), h = 1 and h = 3 months beyond the last published month last(V)? And does a model of my own design beat the no-change (naive) forecast when errors are measured against the first-release YoY value of each target month?
Data and exclusions
- Inputs (row counts in
n_input_rows): 10 RSAFS ALFRED vintages and 10 PAYEMS vintages (one per origin), UMCSENT (current), RSAFS current vintage, andfirst_release.csv(28 target months, 2024-06 to 2026-09). All 23 SHA-256 hashes matchdata/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_datein all 80 rows ofout/forecasts.csv). The target is last(V)+h. Publication delays: for the first six origins (2025-10-31 to 2026-03-31) last(V) is two months before the origin month (e.g. V = 2025-10-31 ends at 2025-08, so its h=1 target is 2025-09 and its h=3 target is 2025-11). From 2026-04-30 on it is one month before (the 2026-04-30 vintage added 2026-02 and 2026-03 at once). Targets therefore run from 2025-09 to 2026-07 (h=1) and from 2025-11 to 2026-09 (h=3). All 20 targets are infirst_release.csv, and none of them is in the vintage used for its forecast. - Actuals.
yoy_first_releaseis used as given. Because of the irregular calendar, 2025-09 first appears in the 2025-11-30 vintage, and 2026-02 and 2026-03 both first appear in 2026-04-30. Their "first release" is defined at month-end granularity (seedata_issues). - Revisions. For 2025–2026, the current RSAFS file has levels about 4.7–4.9% below those in the 2026-07-31 vintage. YoY growth is always computed within one vintage, so levels are never mixed across vintages.
RSAFS_current.csvis used only for the revision estimand (d). - Exclusions (
out/exclusions.csv): 20 candidate model forecast–actual pairs, 0 removed,n_analysed= 20 (10 per horizon). UMCSENT has 210 empty values, all before 1978. It is not used, and neither is PAYEMS.
Method
- Baselines (from vintage V only): naive, where the forecast is g_{last(V)} for both horizons; seasonal naive, where the forecast is g_{T−12}; and AR(1), fitted by OLS of g_m on a constant and g_{m−1} for 1993-02 ≤ m ≤ last(V), then iterated h steps.
- My model (
model_name= rw_drift_base_effect; full text inmodel_description) treats the log level as a random walk with drift. The drift is the mean monthly log change over the last 12 published months: d = (x_l − x_{l−12})/12. The forecast is g_{l+h} = 100·(x_l + h·d − x_{l+h−12}). Unlike the naive forecast, it accounts for the monthly changes that leave the 12-month window ("base effects"), all of which are already published at the origin. It uses only the RSAFS vintage dated V. I fixed the specification before computing any forecast error, and nothing was tuned on the evaluation targets. - Evaluation: RMSE and MAE against
yoy_first_release. The Diebold–Mariano statistic for the model against naive uses d_t = e²_naive − e²_model and a Bartlett (Newey–West) long-run variance with truncation lag h−1, with no HLN factor and a two-sided normal p-value, as specified in TASK.md. - Revision (d): the mean over the 10 h=1 targets of |g^current_T − yoy_first_release_T|.
out/analysis.pyregenerates everything deterministically (random_seed= null, since no randomness is used).
Results
Table 1 (out/table1.csv; percentage points; n = 10 per cell):
| model | RMSE h=1 | MAE h=1 | RMSE h=3 | MAE h=3 |
|---|---|---|---|---|
| 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) |
| model | 0.550 (rmse_model_h1) |
0.486 (mae_model_h1) |
1.142 (rmse_model_h3) |
0.934 (mae_model_h3) |
- At h=1 the model has the lowest RMSE and MAE of the four methods. Its RMSE is 44.8% below the naive RMSE (
rel_improvement_rmse_h1= 0.448). DM = 2.43 (dm_stat_h1), p = 0.015 (dm_pvalue_h1), soconclusion_code_h1= model_better_significant. - At h=3 the model again has the lowest RMSE and MAE. Its RMSE is 19.6% below naive (
rel_improvement_rmse_h3= 0.196). DM = 1.55 (dm_stat_h3), p = 0.12 (dm_pvalue_h3), soconclusion_code_h3= model_better_not_significant. - Among the baselines, AR(1) is slightly better than naive at both horizons. Seasonal naive is the worst at h=1 but beats naive at h=3.
- Revision: the mean absolute gap between first-release and current-vintage YoY growth for the h=1 targets is 0.548 pp (
mean_abs_revision_h1_targets). That is about the same size as the model's h=1 RMSE. out/figure1.pngshows the first-release actuals with the naive, AR(1) and model forecasts for each horizon.
Uncertainty
- With only n = 10 origins, the DM statistic is compared with a normal distribution far outside its asymptotic regime. Its p-values should be read as rough indications.
- h=1: the model's squared error is below the naive one at 8 of 10 origins (aux file
out/state/aux_diagnostics.json,n_origins_model_sq_error_below_naive). However, a single origin (2026-05-31) contributes a loss differential of 3.11, about 45% of the total (loss_diff_by_origin). - HLN sensitivity: applying the Harvey–Leybourne–Newbold factor and t(9) critical values gives a p-value of 0.047 at h=1 and 0.28 at h=3 (
sensitivity_dm_hln_pvalue_t_nminus1). These are not the required estimand and are reported only as a sensitivity check. The h=1 significance is therefore borderline once small-sample corrections are applied. - h=3: the forecasts overlap, so errors are serially correlated. With lag-2 Bartlett weights, a long-run variance estimated from 10 points is very imprecise. The h=3 improvement is not distinguishable from zero.
- No bootstrap was run. A resampling interval built from 10 dependent observations would add little reliability.
Limitations and open issues
- Small sample. Ten origins in a single 2025–2026 episode, which includes a large jump in YoY growth in spring 2026, cannot establish that the model would generally beat the naive forecast. The h=1 result depends heavily on a few origins.
- What the model exploits. Its advantage is mechanical. It uses the base effects already visible in the vintage, so it is a sensible real-time benchmark rather than a behavioural model. PAYEMS and UMCSENT were deliberately not used, so their value as predictors was not evaluated.
- First-release actuals. These include values that are not advance estimates (2026-02) and are themselves revised later, by a mean of 0.548 pp (
mean_abs_revision_h1_targets). Rankings evaluated against the current vintage could differ; this was not evaluated. - Gate conflict (open issue). The executable gate requires origin_date < target_date in
out/forecasts.csv. Under TASK.md's own definitions (target = last(V)+h, target_date = YYYY-MM-01, month-end origins, and 1–2 month publication lags), every h=1 target month starts on or before its origin date. For example, origin 2025-10-31 has its h=1 target at 2025-09-01. The 40 h=1 rows therefore fail this gate check by construction. Satisfying it would mean misdating the targets, so I did not change them. Instead I verified the substantive information rule: each forecast uses only vintage V, and no target value is present in that vintage (out/state/checks.md). As a result, phases P4/P5 are recorded as "blocked" by this check only. All other gate checks were run and recorded.