SPB Git

spb/wp29_uqo

Public

# 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, and first_release.csv (28 target months, 2024-06 to 2026-09). All 23 SHA-256 hashes match data/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_date in all 80 rows of out/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 in first_release.csv, and none of them is in the vintage used for its forecast.
  • Actuals. yoy_first_release is 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 (see data_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.csv is 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 in model_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.py regenerates 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), so conclusion_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), so conclusion_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.png shows 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.
Files · main
Menu

Theme