SPB Git

spb/wp29_uqo

Public

# 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), how accurately could the year-over-year growth of US retail and food services sales (RSAFS), g_m = 100·ln(L_m/L_{m−12}), have been forecast 1 and 3 months past the last published month? And does a simple model of my own beat the naive (no-change) forecast when errors are measured against the first-release value of each target month?

# Data and exclusions

  • Real-time design. The forecast at origin V uses only RSAFS_vintage_V.csv (column vintage_used in forecasts.csv always equals the origin). The target is T = last(V)+h, where last(V) is the last month in that vintage. The actual is yoy_first_release from first_release.csv. My model uses no PAYEMS or UMCSENT data. Those files were read only for the data audit, so they cannot leak information. RSAFS_current.csv is used only for the revision estimand (d).
  • Publication delays. Normally a month-end vintage ends one calendar month before V. For the six origins from 2025-10-31 to 2026-03-31, the vintage ends two months before V: last(V) = 2025-08, 2025-09, 2025-10, 2025-11, 2025-12 and 2026-01. This matches the disrupted publication calendar of late 2025. The 2026-04-30 vintage added 2026-02 and 2026-03 at once, and the timing was normal from then on (last(V) = 2026-03, 04, 05, 06). The README mentions only 2025-10-31 and 2026-03-31 as two months behind. My audit of the files shows that all six origins are. As a result, at the delayed origins the h=1 target is a month that would normally already have been published. Target 2026-03 is never an h=1 target, and 2026-05 is never an h=3 target. Two target months (2026-02 and 2026-03) share the same first-release vintage (2026-04-30).
  • Revisions. Between the 2026-08-31 and 2026-09-30 vintages there was an annual/benchmark revision that lowered RSAFS levels by about 4% (e.g. 2025-08: 731,700 in the 2026-07-31 vintage vs 700,407 in the current file). Every growth rate is computed within a single vintage, so this is mostly a level rebasing, but it does show up in estimand (d). The PAYEMS 2026-02-28 vintage contains the BLS benchmark revision (about −1 million jobs); PAYEMS is unused.
  • Missing values. No vintage file has missing or duplicated months. UMCSENT has 210 empty values, all before 1978 (unused).
  • Nothing was excluded. All 10 origins × 2 horizons × 4 methods = 80 forecasts were evaluated.

# Method

  • Baselines (from the vintage dated V):
    • naive: g_last;
    • 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.
  • My model: random walk with drift in log levels, plus exact base effects ("RW-drift36"). The identity g_T = 100·ln(L_last/L_{T−12}) + Σ_{k=1..h} 100·Δln L_{last+k} holds exactly. Its first term is known at the origin because L_{T−12} is in the vintage, so it captures exactly the "base effect" of months leaving the 12-month window. That base effect is what the naive forecast ignores. Each unknown future monthly log change is forecast by the mean of the last 36 monthly log changes in the same vintage. The 36-month window was fixed before looking at any evaluation errors. The model is re-estimated at every origin.
  • Evaluation. RMSE and MAE against first release, with n = 10 per horizon.
  • Diebold–Mariano test. Model vs naive, with d_t = e²_naive − e²_model. The long-run variance uses a Bartlett kernel with truncation lag h−1, so 0 lags at h=1 and lags 1 and 2 with weights 2/3 and 1/3 at h=3. There is no HLN correction, and p-values are two-sided normal.
  • Bootstrap. A circular moving-block bootstrap of RMSE_naive − RMSE_model (block length max(h,2), 9,999 draws, seed 20261030).
  • Estimand (d). Mean of |g^current_T − yoy_first_release_T| over the 10 h=1 target months.

# Results

Table 1 (table1.csv; percentage points of YoY growth, n = 10 each):

model RMSE h=1 MAE h=1 RMSE h=3 MAE h=3
naive 0.997 0.849 1.420 1.200
seasonal naive 1.675 1.307 1.316 1.066
AR(1) 0.941 0.798 1.187 0.953
RW-drift36 (model) 0.552 0.497 1.054 0.747
  • Relative improvement over naive (1 − RMSE_model/RMSE_naive): 0.446 at h=1 and 0.258 at h=3. The model has the lowest RMSE and MAE of all four methods at both horizons.
  • Diebold–Mariano, model vs naive:
    • h=1: DM = 2.30, p = 0.021 → model_better_significant.
    • h=3: DM = 1.86, p = 0.062 → model_better_not_significant.
  • Among the baselines, AR(1) beats naive slightly at both horizons. Seasonal naive is the worst at h=1 but beats naive at h=3.
  • figure1.png shows that most of the model's gain comes from anticipating base effects. Examples: the drop in YoY growth in late 2025 (targets 2025-09 to 2025-12) and the jump in 2026-04/05, which the naive forecast follows only with a lag. The model's largest misses are at h=3 for targets 2026-04 (forecast 2.24 vs actual 4.76) and 2026-06 (4.96 vs 6.50), when spring 2026 sales rose unexpectedly fast.
  • Revisions (estimand d). The mean absolute revision between first-release and currently published YoY growth over the 10 h=1 target months is 0.548 pp. All 10 revisions are negative: current growth is lower than first release, ranging from −0.17 to −0.98 pp. That revision is about the same size as the model's h=1 RMSE (0.552). In other words, much of the remaining h=1 forecast error is the same order as measurement noise in the "actual" itself.

# Uncertainty

  • Small sample. With only 10 origins per horizon, the DM statistic rests on 10 loss differentials, and its asymptotic N(0,1) reference distribution is not reliable. Specific concerns:
    • At h=3 the Bartlett long-run variance is estimated from 10 overlapping, serially correlated errors and is itself very noisy.
    • Without the Harvey–Leybourne–Newbold correction and Student-t critical values, the test tends to over-reject in samples this small.
    • The h=1 result (p = 0.021) should therefore be read as suggestive, not decisive. With the HLN factor and t(9) critical values it would probably sit near the 5% boundary. I did not compute this; it is a qualitative statement.
  • The h=3 result does not reach the 5% level.
  • Bootstrap. The block bootstrap agrees with this picture:
    • h=1: 95% interval for RMSE_naive − RMSE_model is [0.26, 0.65], and 100% of draws are positive.
    • h=3: interval [−0.03, 0.91], with 96% of draws positive.
    • With n = 10 and blocks of length 2–3, these intervals are also only rough.
  • Sensitivity to the drift window (post hoc). Computed after the main result and not used to choose the model, so it is reported in supplement.json for transparency only. RMSE at h=1/h=3 by window: 12 months 0.550/1.142; 24 months 0.531/0.989; 60 months 0.595/1.076; 120 months 0.552/0.977. Every window beats naive at both horizons. The conclusion therefore does not depend on the a-priori choice of 36 months.
  • Regime. The evaluation covers one 10-month period with an unusual publication calendar and a spring-2026 surge in sales. It should not be generalised to other periods without more origins.

# Limitations and open issues

  • Ten origins is very few. The h=3 forecast errors overlap, so the effective sample is even smaller.
  • "First release" is defined at month-end granularity (from first_release.csv). For months whose publication was delayed, this is the first value published by the first month-end at which the month existed.
  • The current-vintage comparison (d) mixes ordinary monthly revisions with an annual revision that rebased levels. I did not separate the two.
  • PAYEMS and UMCSENT were not used. Given how little evaluation data there is, I preferred a parameter-light model that cannot overfit, and there was no out-of-sample room to test extra predictors credibly. Whether they add information remains an open question.
  • The model treats future monthly changes as equal to a historical mean. It cannot anticipate turning points such as the 2026 spring acceleration.
  • Supplementary outputs (publication audit, bootstrap, per-month revisions, window sensitivity) are in supplement.json, which analysis.py regenerates.
Files · main
Menu

Theme