SPB Git

spb/wp29_uqo

Public

# Real-time forecasts of US retail-sales YoY growth: does a base-effect model beat no-change?

# 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 g = 100·ln(L_m/L_{m−12}) of US Retail Trade and Food Services sales (RSAFS) be forecast 1 and 3 months beyond the last published month, judged against the first-release value of each target month? Does a model of my own design beat the naive (no-change) forecast?

# Data and exclusions

  • Inputs (FRED/ALFRED, retrieved 2026-10-30; all SHA-256 hashes match data/README.md, see out/state/data_audit.md; row counts in n_input_rows): 10 RSAFS vintages and 10 PAYEMS vintages dated at the origins, UMCSENT (current), RSAFS current vintage, and first_release.csv (actuals).
  • Real-time design. The forecast at origin V uses only RSAFS_vintage_V.csv (column vintage_used in forecasts.csv equals origin_date on every row). Targets are last(V)+h, where last(V) is the last month in that vintage. Publication delays observed: for the six origins 2025-10-31 … 2026-03-31 the last published month is two calendar months before the origin (last = 2025-08, 2025-09, 2025-10, 2025-11, 2025-12, 2026-01) instead of the usual one; the 2026-04-30 vintage adds 2026-02 and 2026-03 at once, and the four origins 2026-04-30 … 2026-07-31 end one month before (last = 2026-03 … 2026-06). Consequently the h=1 targets are 2025-09 … 2026-02 and 2026-04 … 2026-07 (2026-03 is never an h=1 target), and the h=3 targets are 2025-11 … 2026-04 and 2026-06 … 2026-09 (13 distinct target months overall). Origin information is in results.json → supplementary.origins.
  • Actuals: yoy_first_release from first_release.csv; each target's first-release vintage is strictly after its origin (check 4 in out/state/checks.md).
  • Not used in forecasts: RSAFS_current.csv (used only for the revision estimand), PAYEMS and UMCSENT (read for auditing only; the chosen model does not use them).
  • Exclusions (out/exclusions.csv): no forecast–actual pair was dropped; 20 model pairs evaluated (n_analysed). Only the 15 rows of first_release.csv that are not targets of any origin are unused.
  • Data issues (full list in data_issues): irregular publication timing (above); an annual/benchmark revision of RSAFS between the 2026-08-31 and 2026-09-30 vintages lowered levels by ≈4% and current YoY growth sits below first release for every h=1 target; it changes the base of the first-release actuals for h=3 targets 2026-08 and 2026-09. No adjustment was made, since the task defines the actual as the first release.

# Method

  • Baselines (from vintage V only): naive g_{last(V)}; seasonal naive g_{T−12}; AR(1) estimated by OLS of g_m on a constant and g_{m−1} over 1993-02 … last(V), re-estimated at each origin, with iterated forecasts.
  • Own model, “BED (base-effect drift)” (model_name, model_description). YoY growth of month T is the sum of the 12 monthly log growths T−11 … T, and 12−h of them are already published at the origin. BED keeps those known terms and replaces only the h unknown months with the trailing 12-month average monthly growth: ĝ_T = 100·ln(L_last/L_{T−12}) + h·g_last/12, with both levels taken from vintage V. Nothing is estimated and there is nothing to tune. In comparison, the naive forecast implicitly fills the unknown months with the h oldest months of the last 12-month window, which brings in last year's month-specific noise (the "base effect"). I fixed this specification in out/state/log.md before computing any out-of-sample error and evaluated no other specification. I did not try PAYEMS or UMCSENT variants, because searching over models on 10 origins would mean selecting on the evaluation sample.
  • Evaluation: RMSE and MAE against the first release; relative improvement 1 − RMSE_model/RMSE_naive; Diebold–Mariano test on d_t = e²_naive − e²_model with a Bartlett long-run variance at truncation lag h−1 (no HLN factor) and a two-sided N(0,1) p-value. As a supplement, a moving-block bootstrap of RMSE_model − RMSE_naive (block length 2 at h=1, 3 at h=3, 9 999 draws, seed random_seed = 20261030).
  • Revision estimand: mean over the 10 h=1 targets of |g^current_T − yoy_first_release_T|.
  • Everything is produced by out/analysis.py.

# Results

Table 1 (out/table1.csv), percentage points of YoY growth, 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)
BED model 0.550 (rmse_model_h1) 0.486 (mae_model_h1) 1.142 (rmse_model_h3) 0.934 (mae_model_h3)
  • At h=1, BED reduces RMSE relative to naive by 44.8% (rel_improvement_rmse_h1 = 0.448). DM = 2.43, p = 0.015 (dm_stat_h1, dm_pvalue_h1), so conclusion_code_h1 = model_better_significant. BED has a smaller squared error than naive at 8 of 10 origins (supplementary.bootstrap_rmse_diff.h1.n_origins_model_smaller_sq_error).
  • At h=3, BED reduces RMSE relative to naive by 19.6% (rel_improvement_rmse_h3 = 0.196). DM = 1.55, p = 0.122 (dm_stat_h3, dm_pvalue_h3), so conclusion_code_h3 = model_better_not_significant. BED wins at 6 of 10 origins. At this horizon its RMSE is only slightly below AR(1)'s (1.142 vs 1.187), and I did not test that difference.
  • At h=1, AR(1) is only marginally better than naive, and seasonal naive is the worst baseline. At h=3, all three alternatives (seasonal naive, AR(1), BED) have lower RMSE than naive.
  • Figure 1 (out/figure1.png) shows the misses common to all methods: the jump to ≈6.6% YoY in 2026-05 and the drop to 4.9% in 2026-07.
  • Revisions: the mean absolute revision between first-release and current YoY growth of the h=1 targets is 0.548 pp (mean_abs_revision_h1_targets). Every revision is negative (−0.17 to −0.98 pp; supplementary.revisions_h1_targets), which reflects the benchmark revision. The revision is about as large as BED's h=1 RMSE, so the ranking could differ if the forecasts were scored against current data.

# Uncertainty

  • There are only 10 origins per horizon. The DM statistic relies on asymptotic normality, which is a rough approximation at n = 10, and without the Harvey–Leybourne–Newbold correction the test tends to over-reject in small samples. The h=1 p-value of 0.015 (dm_pvalue_h1) should therefore be read as suggestive evidence rather than a precise significance level. A Student-t reference distribution or the HLN factor would give a larger p-value; I did not compute this.
  • At h=1 the evidence holds up under a second check: the moving-block bootstrap 95% interval for RMSE_model − RMSE_naive is [−0.70, −0.26] pp and lies entirely below zero (supplementary.bootstrap_rmse_diff.h1). This bootstrap is itself crude with n = 10.
  • At h=3 the long-run variance uses only two lags on overlapping 3-step errors. The DM test cannot reject equal accuracy (dm_pvalue_h3 = 0.12), and the bootstrap interval [−0.64, +0.14] pp contains zero (supplementary.bootstrap_rmse_diff.h3).
  • The 10 origins span a single 9-month period with unusual publication delays and a sharp acceleration in mid-2026, so the results may not carry over to other periods.

# Limitations and open issues

  • The sample is small and covers a single episode. The h=1 result is significant at 5% under the prescribed DM test but has not been confirmed on other periods.
  • The actual is the first release, which here sometimes comes from a vintage that already includes a large level rebasing (2026-08 and 2026-09 targets). With a different definition of "truth" (current vintage) the errors would change by about 0.5 pp on average (mean_abs_revision_h1_targets).
  • PAYEMS and UMCSENT were deliberately left out (see Method), so the task's question about the value of these predictors is not addressed (claim C5: not evaluated).
  • n_input_rows lists the 23 CSV files read by the script. data/README.md was read for provenance only and has no data rows.
  • Feasibility: complete (feasibility). No required value is null.
Files · main
Menu

Theme