SPB Git

spb/wp29_uqo

Public

# Real-time forecasting of US retail-sales YoY growth (T1-forecast)

# Question

Using only the RSAFS data published at each of 10 month-end origins (2025-10-31 … 2026-07-31; first_origin, last_origin, n_origins), how accurately can YoY growth g_m = 100·ln(L_m/L_{m−12}) of US retail & food services sales be forecast h = 1 and h = 3 months beyond the last published month last(V), measured 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: 10 RSAFS and 10 PAYEMS ALFRED vintages, UMCSENT (current), RSAFS current, first_release.csv (row counts in n_input_rows). SHA-256 hashes all match data/README.md (see out/state/data_audit.md). No gaps, duplicate dates or missing values in the RSAFS/PAYEMS vintages; UMCSENT has 210 empty pre-1978 fields (data_issues).
  • Real-time design: the forecast at origin V uses only the RSAFS vintage dated V (vintage_used = origin_date in every row of out/forecasts.csv). The target is last(V)+h; the actual is yoy_first_release of that month.
  • Publication delays observed (publication_delays): in the six vintages 2025-10-31 … 2026-03-31 the last published month is two months before the origin month (2025-08 … 2026-01); the 2026-04-30 vintage adds two months at once (2026-02, 2026-03), and from then on the lag is one month (2026-03 … 2026-06). As a result the h=1 targets are 2025-09 … 2026-02 and 2026-04 … 2026-07 (2026-03 never an h=1 target) and the h=3 targets are 2025-11 … 2026-04 and 2026-06 … 2026-09.
  • A large downward level revision (about 4%) appears between the first releases of 2026-07 and 2026-08 (a benchmark/annual revision); because YoY growth is computed within one vintage, it largely cancels in the growth rates (data_issues).
  • RSAFS_current.csv is used only for estimand (d). PAYEMS and UMCSENT were admissible but are not used by my model.
  • Exclusions (out/exclusions.csv): 20 (origin, horizon) pairs, 0 removed; n_analysed = 20 pairs for the model (10 per horizon).

# Method

  • Baselines (per TASK.md): naive = g_{last(V)}; seasonal naive = g_{T−12} from the vintage; AR(1) = OLS of g_m on a constant and g_{m−1}, 1993-02 ≤ m ≤ last(V), iterated h steps.
  • My model (model_name = rw_drift_loglevel_12m; model_description): random walk with drift in the log level, ln L̂_{last+h} = ln L_last + h·μ, where μ is the mean monthly log growth over the last 12 months of vintage V. The YoY forecast is 100·(ln L̂_T − ln L_{T−12}), with the year-ago level L_{T−12} read from vintage V. Unlike the naive forecast, this uses the known year-ago months, i.e. it accounts for base effects. It has one data-determined parameter, was specified before looking at any evaluation errors, and was not tuned.
  • Evaluation: RMSE and MAE against first release over the 10 origins per horizon; relative improvement 1 − RMSE_model/RMSE_naive; Diebold–Mariano on d_t = e²_naive − e²_model with a Bartlett long-run variance with truncation lag h−1, no HLN correction, two-sided normal p-value. Revision estimand (d): mean over the 10 h=1 targets of |g^current − g^first|. Deterministic, no random numbers (random_seed = null).

# Results

RMSE / MAE in percentage points (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)
model 0.550 (rmse_model_h1) 0.486 (mae_model_h1) 1.142 (rmse_model_h3) 0.934 (mae_model_h3)
  • h = 1: the model cuts the naive RMSE by 44.8% (rel_improvement_rmse_h1 = 0.448). DM = 2.43, p = 0.015 (dm_stat_h1, dm_pvalue_h1) → conclusion_code_h1 = model_better_significant. The model has the smaller squared error at 8 of 10 origins (n_model_better_sq_h1).
  • h = 3: RMSE improvement of 19.6% (rel_improvement_rmse_h3 = 0.196), DM = 1.55, p = 0.122 (dm_stat_h3, dm_pvalue_h3) → conclusion_code_h3 = model_better_not_significant; the model wins at 6 of 10 origins (n_model_better_sq_h3). At h=3 the AR(1) and seasonal-naive baselines also beat the naive forecast, and the model is only marginally better than AR(1).
  • Revisions (d): the first-release YoY growth of the h=1 targets differs from today's published growth by 0.548 pp on average (mean_abs_revision_h1_targets); every one of the 10 revisions is negative (revision_h1_targets), so growth is lower in current data. That revision is about as large as the model's h=1 RMSE.
  • Figure: out/figure1.png (first-release actuals, naive and model forecasts by horizon).

# Uncertainty

With only n = 10 origins per horizon, the DM statistic's normal reference distribution is a rough approximation at best. At h = 1 (lag 0, dm_lrv_h1) the rejection at 5% (p = 0.015) is suggestive but fragile: two or three origins can change the conclusion, and a small-sample (HLN/Student-t) correction, which the task excluded, would make the p-value larger (I did not compute it). At h = 3 the errors from overlapping 3-month windows are serially correlated. A Bartlett LRV with only 2 lags estimated from 10 points is very noisy (dm_lrv_h3), and the p = 0.12 result means the data cannot tell the model and the naive forecast apart at that horizon. The evaluation window (late 2025 – 2026) includes a disrupted publication calendar and a sharp rise in growth in spring 2026, so the results may not carry over to other periods.

# Limitations and open issues

  • Gate conflict (unresolved): the gate checks origin_date < target_date for every row of forecasts.csv. TASK.md sets the target to last(V)+h and requires target_date in YYYY-MM-01 format. Because last(V) is one or two months behind the month-end origin, every h=1 target month starts before its origin date (it is a nowcast of a month not yet published). So 40 of the 80 rows fail that gate check no matter how they are written. I did not shift dates to satisfy the gate, because that would misrepresent the targets. Every target month is absent from the vintage used, so the information rule holds (out/state/checks.md). For this reason P4 is marked "blocked" in the phase ledger, and the latest gate receipt reports failure on that check only.
  • The model uses no PAYEMS or UMCSENT information; I did not test whether they would help (no specification search on the 10 evaluation points, to avoid overfitting).
  • "First release" here means the first month-end vintage. For 2025-09 and 2026-02/03 this is a delayed release.
  • Small sample: 10 forecast–actual pairs per horizon.
Files · main
Menu

Theme