# 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 origins (2025-10-31 … 2026-07-31; `first_origin`, `last_origin`, `n_origins`), how accurately can the year-over-year growth g_m = 100·ln(L_m/L_{m−12}) of RSAFS (US retail and food services sales, SA) be forecast h = 1 and h = 3 months beyond the last published month last(V)? And does a model of my own design beat the naive no-change forecast when errors are measured against the first-release value of each target month? ## Data and exclusions - Inputs (row counts in `n_input_rows`): 10 RSAFS and 10 PAYEMS ALFRED vintages, UMCSENT (current vintage), RSAFS current vintage, and `first_release.csv` (28 target months). All SHA-256 hashes match the README (see `out/state/data_audit.md`). - **Real-time design.** The forecast made at origin V uses only `RSAFS_vintage_V.csv` (`vintage_used` = origin date for all 80 rows of `out/forecasts.csv`). The last published month last(V) was 2025-08, 2025-09, 2025-10, 2025-11, 2025-12, 2026-01, 2026-03, 2026-04, 2026-05, 2026-06 for the 10 origins in order (`supplementary.real_time_design`). Publication delays: the first six origins (2025-10-31 … 2026-03-31) end two calendar months before the origin month; the 2026-04-30 vintage adds two months at once (2026-02 and 2026-03), after which the lag is one month. 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-02, 2026-03, 2026-04, 2026-06 … 2026-09. - Actuals are `yoy_first_release` from `first_release.csv`; all 20 targets are present, so the exclusion ledger (`out/exclusions.csv`) removes nothing: 20 pairs analysed for the model (`n_analysed`), 80 forecast rows overall. - `RSAFS_current.csv` is used only for the revision estimand (d), after all forecasts are computed. PAYEMS and UMCSENT were read and audited but are not used by the chosen model. - Data issues and their handling are listed in `data_issues` (irregular publication timing, simultaneous first release of 2026-02/2026-03, PAYEMS benchmark revision in the 2026-02-28 vintage, a large RSAFS benchmark level revision after the last origin, pre-1978 gaps in UMCSENT, COVID observations in the AR(1) sample). ## Method - Baselines, all from vintage V: **naive** g_{last(V)} for both horizons; **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 (`model_name` = RW-drift-base-effect; full text in `model_description`).** A random walk with drift in log levels that is aware of base effects: ln L̂_T = ln L_{last} + h·δ with δ = average monthly log change over the 12 months ending at last(V); the YoY forecast is 100·ln(L̂_T / L_{T−12}), where the base level L_{T−12} is already published in the vintage. The idea: the change in YoY growth over the next h months equals the new monthly changes minus the monthly changes of the corresponding months a year earlier that drop out of the 12-month window. The second part is already known at the origin, and the naive forecast ignores it. The model has no estimated parameters. Its specification was fixed and logged (`out/state/log.md`) before any evaluation error was computed. Because nothing was selected on the evaluation sample, there is no look-ahead in model choice. - Evaluation: RMSE and MAE over the 10 origins per horizon against first-release actuals. The Diebold–Mariano test uses d_t = e²_naive − e²_model and a Bartlett (Newey–West) long-run variance with truncation lag h − 1, without the HLN correction, with a two-sided N(0,1) p-value. Revision (d) is the mean over the 10 h = 1 targets of |g^current − g^first|. The script is deterministic (`random_seed` = null; no resampling is used). ## Results RMSE / MAE in percentage points (`out/table1.csv`; keys `rmse__h`, `mae__h`): | model | h=1 RMSE | h=1 MAE | h=3 RMSE | h=3 MAE | |---|---|---|---|---| | 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-drift-base-effect | 0.550 | 0.486 | 1.142 | 0.934 | - At h = 1 the model's RMSE is 45% below the naive forecast's (`rel_improvement_rmse_h1` = 0.448). Its squared error is lower at 8 of the 10 origins (`supplementary.n_origins_model_sqerr_lower_h1`). DM = 2.43, p = 0.015 (`dm_stat_h1`, `dm_pvalue_h1`), so `conclusion_code_h1` = model_better_significant. - At h = 3 the improvement is 20% (`rel_improvement_rmse_h3` = 0.196), with lower squared error at 6 of 10 origins (`supplementary.n_origins_model_sqerr_lower_h3`). DM = 1.55, p = 0.122 (`dm_stat_h3`, `dm_pvalue_h3`), so `conclusion_code_h3` = model_better_not_significant. - The model also has the lowest RMSE and MAE of all four methods at both horizons. The AR(1) beats the naive forecast only slightly (`rmse_ar1_h1`, `rmse_ar1_h3`). The seasonal naive forecast is the worst at h = 1 and second-best among the baselines at h = 3 (`rmse_snaive_h1`, `rmse_snaive_h3`). - Revision: the first-release YoY growth of the 10 h = 1 target months differs from today's published growth by 0.548 pp on average in absolute value (`mean_abs_revision_h1_targets`). All of these revisions are downward: the signed mean is −0.548 (`supplementary.mean_revision_h1_targets_signed`). This revision is about the size of the model's h = 1 MAE (`mae_model_h1`), so evaluating against current data would give materially different error levels. - Figure: `out/figure1.png` shows the first-release actuals and the naive and model forecasts by target month. ## Uncertainty - Only 10 origins are available per horizon, so the DM statistic's normal approximation is weak. The 10 origins also cover a single episode (a dip in growth around 2025-12 followed by a rise to about 6.5% in mid-2026). - At h = 1 the evidence is moderately strong. The p-value is 0.015 (`dm_pvalue_h1`), and with the HLN small-sample correction and Student-t(9) reference it is 0.047 (`supplementary.dm_pvalue_hln_t_h1`, sensitivity only), just under 5%. So the h = 1 significance survives the small-sample correction, but only barely. - At h = 3 the overlapping 3-step errors are serially correlated, and the long-run variance rests on only two autocovariance lags estimated from 10 points. The test does not reject (`dm_pvalue_h3` = 0.122; the HLN version gives 0.277, `supplementary.dm_pvalue_hln_t_h3`). The 20% RMSE gain at h = 3 is therefore not statistically distinguishable from zero. - No bootstrap interval was computed: with 10 serially dependent pairs, a block bootstrap would add little information. ## Limitations and open issues - **Gate conflict (open issue).** The provided gate checks `origin_date < target_date` for every row of `out/forecasts.csv`. The 40 rows with h = 1 fail this check. This is not information leakage. Under the task's own definition, the target is last(V)+1 and `target_date` is the first day of that month. Because RSAFS is published with a 1–2 month lag, that date always lies before the month-end origin date. The real-time condition that matters, target month > last(V) (the target is not yet published in vintage V), holds for all 80 rows (`out/state/checks.md`). I kept the column exactly as TASK.md specifies rather than relabel dates. As a result, the latest gate receipt reports `passed: false` for this item only. - Small evaluation sample (10 origins, one macro episode). The ranking could change over a different period. - The model uses no external predictors (PAYEMS, UMCSENT). I did not test whether they would add information, and I make no claim about it. - Actuals use the month-end "first release" definition of `first_release.csv`. The 2026-08 and 2026-09 targets (h = 3, last two origins) are first released after a large benchmark level revision. Growth rates are computed within a single vintage, which limits the effect, but those two actuals are on a revised basis. - The AR(1) baseline is estimated on the full sample including the COVID swings, as prescribed. This probably biases its slope, but that is part of the baseline's definition.