# Real-time forecasting of US retail-sales YoY growth from ALFRED vintages (T1-forecast) ## Question Could the year-over-year log growth of US retail and food services sales (RSAFS), g_m = 100·ln(L_m/L_{m−12}), have been forecast in real time, using only the data published at each of 10 month-end origins (2025-10-31 … 2026-07-31; `first_origin`, `last_origin`, `n_origins`), one and three months beyond the last published month? And does a model of my own design beat the no-change (naive) forecast, when errors are measured against the first-release value of each target month? ## Data and exclusions Inputs (hashes verified against `data/README.md`; row counts in `n_input_rows`; full audit in `out/state/data_audit.md`): 10 ALFRED vintages of RSAFS and of PAYEMS, current UMCSENT, current RSAFS, and `first_release.csv`. **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, where last(V) is the last month present in that vintage (`last_month_by_origin`): | origin V | last(V) | h=1 target | h=3 target | |---|---|---|---| | 2025-10-31 | 2025-08 | 2025-09 | 2025-11 | | 2025-11-30 | 2025-09 | 2025-10 | 2025-12 | | 2025-12-31 | 2025-10 | 2025-11 | 2026-01 | | 2026-01-31 | 2025-11 | 2025-12 | 2026-02 | | 2026-02-28 | 2025-12 | 2026-01 | 2026-03 | | 2026-03-31 | 2026-01 | 2026-02 | 2026-04 | | 2026-04-30 | 2026-03 | 2026-04 | 2026-06 | | 2026-05-31 | 2026-04 | 2026-05 | 2026-07 | | 2026-06-30 | 2026-05 | 2026-06 | 2026-08 | | 2026-07-31 | 2026-06 | 2026-07 | 2026-09 | **Publication delays observed.** The first six vintages (2025-10-31 to 2026-03-31) end two months before the vintage date rather than one; the 2026-04-30 vintage adds two months at once (2026-02 and 2026-03). So the target months are unevenly spaced (h=1 never targets 2026-03, h=3 never targets 2026-05). The actual for every target is `yoy_first_release` from `first_release.csv`. Its first-release vintage is always later than the origin. Other data issues are listed in `data_issues`: revisions within the vintage grid, a large (~3.5–4 %) downward level revision between the 2026-08-31 and 2026-09-30 vintages that also appears in the current file, PAYEMS delays, and pre-1978 gaps in UMCSENT. `RSAFS_current.csv` is used only for estimand (d). **Exclusions.** None: 80 forecast–actual pairs out of 80, of which 20 belong to my model (`n_analysed`). See `out/exclusions.csv`. ## Method All four forecasts are computed from the RSAFS vintage dated V only: - **naive**: g_{last(V)} for both horizons; - **seasonal naive (snaive)**: g_{T−12} from the vintage; - **AR(1)**: OLS of g_m on a constant and g_{m−1}, 1993-02 ≤ m ≤ last(V), with iterated forecasts (coefficients in `ar1_coef_by_origin`); - **my model, "base-effect drift" (BED)** (`model_name`, `model_description`): the log level is extrapolated as ln L̂_T = ln L_last + h·μ, with μ = (ln L_last − ln L_{last−12})/12 (the latest YoY growth spread evenly across months). The forecast is ĝ_T = 100·(ln L̂_T − ln L_{T−12}), where the base-period level L_{T−12} is already published in vintage V. In words, the naive forecast is corrected for the known *base effect*, i.e. the monthly changes from a year ago that drop out of the YoY window, and h months of drift are added. The model estimates no parameters. Its specification was fixed before any out-of-sample error was computed, and no alternative variant was tried on the evaluation sample. PAYEMS and UMCSENT are not used. Evaluation: RMSE and MAE over the 10 origins per horizon against first-release YoY. Relative improvement = 1 − RMSE_model/RMSE_naive. Diebold–Mariano test of model vs naive: d_t = e²_naive − e²_model, Bartlett (Newey–West) long-run variance with truncation lag h−1, no HLN correction, two-sided normal p-value. Supplementary measure: a circular moving-block bootstrap (block length 3, B = 9999, seed `random_seed` = 20261030) of the paired errors, giving a 95 % percentile interval for RMSE_model − RMSE_naive. Estimand (d) is the mean over the 10 h=1 targets of |g_current − yoy_first_release|. Everything is produced by `out/analysis.py`. ## Results RMSE / MAE in percentage points of YoY growth (`out/table1.csv`, n = 10 each): | 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`) | | BED model | 0.550 (`rmse_model_h1`) | 0.486 (`mae_model_h1`) | 1.142 (`rmse_model_h3`) | 0.934 (`mae_model_h3`) | - **h = 1:** the BED model has the lowest RMSE and MAE of the four methods. Its RMSE is 44.8 % below naive (`rel_improvement_rmse_h1` = 0.448), and it has the smaller squared error at 8 of 10 origins (`n_model_wins_h1`). DM = 2.43, p = 0.015 (`dm_stat_h1`, `dm_pvalue_h1`), so `conclusion_code_h1` = `model_better_significant`. - **h = 3:** the BED model again has the lowest RMSE and MAE, but its margin over AR(1) is small (1.142 vs 1.187). Its RMSE is 19.6 % below naive (`rel_improvement_rmse_h3` = 0.196), with 6 wins out of 10 (`n_model_wins_h3`). DM = 1.55, p = 0.122 (`dm_stat_h3`, `dm_pvalue_h3`), so `conclusion_code_h3` = `model_better_not_significant`. - Seasonal naive is the worst method at h = 1 but beats naive at h = 3. AR(1) beats naive at both horizons. - **Revisions (d):** the first-release YoY growth of the 10 h=1 target months differs from the currently published growth by 0.548 pp on average (`mean_abs_revision_h1_targets`). The revisions are all downward (`mean_signed_revision_h1_targets` = −0.548; maximum 0.976 pp, `max_abs_revision_h1_targets`). That average revision is the same size as the BED model's h=1 RMSE. - Figure: `out/figure1.png` shows the first-release actuals with the naive and BED forecasts, one panel per horizon. ## Uncertainty There are only 10 origins, the evaluation window is a single year, and the h = 3 errors overlap. The DM statistic is compared with the standard normal, which is an asymptotic approximation. With n = 10 the Newey–West variance estimate is itself very noisy, and the test is known to over-reject in small samples (this is why the Harvey–Leybourne–Newbold correction exists; as the task specifies, it is not applied here). So the h = 1 p-value of 0.015 (`dm_pvalue_h1`) should be read as suggestive evidence, not decisive. With the HLN factor and t(9) critical values it would be less extreme; that variant was not computed for the reported keys. Two things support the h = 1 result: the model wins at 8 of 10 origins, and every block-bootstrap resample gives the model the lower RMSE. The 95 % interval for RMSE_model − RMSE_naive is [−0.649, −0.279] pp (`boot_rmse_diff_lo_h1`, `boot_rmse_diff_hi_h1`, `boot_share_model_better_h1`). This bootstrap is crude at n = 10 and should not be over-interpreted. At h = 3 the DM test does not reject (p = 0.122, `dm_pvalue_h3`), and the bootstrap interval [−0.688, 0.082] pp includes zero (`boot_rmse_diff_lo_h3`, `boot_rmse_diff_hi_h3`). An improvement over naive at h = 3 is plausible but not established. ## Limitations and open issues - **Gate chronology check conflicts with the task definition.** The gate requires `origin_date < target_date` in `out/forecasts.csv`, which fails for all 40 h = 1 rows. TASK.md defines the target as last(V)+h, written as YYYY-MM-01. Since last(V) is always at least one month before the month of V, the h = 1 target month always begins on or before the month-end origin (e.g. origin 2026-04-30, target 2026-04-01). The forecast is still made before the target's value exists in any data: the target value is absent from vintage V, and its first-release vintage is later than the origin in all 80 rows (`out/state/checks.md`). I kept the target dates as TASK.md defines them rather than relabelling them to pass the gate. So the gate reports this one check as failed, and phases P4/P5 are marked "blocked" for this reason only. All other gate checks, including reproducibility, pass. - Small sample (10 origins, one year, a period with shutdown-related publication disruption). The ranking of methods could change in other periods. - The model was designed by one analyst, and its design reflects general knowledge of how YoY base effects work. I did not search over variants on these data. Still, I cannot fully exclude awareness of the general behaviour of 2025–26 data as a source of bias. - The h = 3 actuals for the last two origins (targets 2026-08 and 2026-09) come from vintages released after a large level revision. Within-vintage YoY growth is less affected than levels, but not entirely unaffected. - The large downward revision behind estimand (d) is mostly a single annual/benchmark revision. It is a statement about this particular window, not about typical revision sizes. - PAYEMS and UMCSENT were not used. Whether they would add information was not evaluated. - **Controller resume (gate re-run).** The controller re-ran the gate and reported the same single failure (`forecasts_chronology`); I re-ran it again and got the same result (receipt `20261030T060507.995429Z`). For 6 of the 10 h = 1 rows (origins 2025-10-31 to 2026-03-31) the whole target month lies before the origin date. For the other 4 (2026-04-30 to 2026-07-31) the target month is the origin's own month. So no date inside the target month satisfies `origin_date < target_date`. To pass the check I would have to write a date outside the target month, for example the first-release date, into a column that TASK.md defines as the target month. That would falsify the deliverable, so I did not do it. Resolving this requires changing either the gate rule or the task's target definition.