# 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.