Forecasting Weekly Book Sales: the Price of a Missed Peak

Time-series forecasting of two backlist titles on Nielsen BookScan data — classical, ML, DL & hybrid models on a 32-week holdout. By Pierre Sutherland.

127.8 / 345.5SARIMA holdout MAE (Alchemist / Caterpillar)
32 weeksforecast horizon, held out & scored
5 model familiesSARIMA · XGBoost · LSTM · hybrids · Lag-Llama
Data note. Nielsen BookScan is a licensed dataset and is not included here — the repo tracks it only as a DVC pointer (data/raw.dvc). To reproduce, supply your own copy under data/raw/ (course Drive link in the README), then run the pipeline. Everything else — code, parameters, the 12-stage DAG — is public.

Pipeline parameters — params.yaml

# PACE Course 3 (C301) — Nielsen BookScan time-series forecasting.
# Knobs are read by every pipelines/forecast/*.py via yaml.safe_load.

seed: 42
horizon: 32                  # 32-week test horizon (rubric)
seasonality: 52              # weekly data, annual cycle
focus_start: "2012-01-01"    # restrict to post-2012 backlist regime
active_cutoff: "2024-07-01"  # ISBNs with any non-zero sale after this = active

alchemist: "9780722532935"
caterpillar_candidates:      # script picks the max-volume of these
  - "9780241003008"          # board book ISBN
  - "9780140500875"          # paperback ISBN

arima:
  m: 52
  max_p: 2
  max_q: 2
  max_d: 1
  max_P: 1
  max_Q: 1
  max_D: 1
  information_criterion: "aicc"
  stepwise: true

Pipeline stages — dvc.yaml

stages:
  ingest:
    cmd: .venv/bin/python pipelines/forecast/ingest.py
    deps:
      - pipelines/forecast/ingest.py
      - data/raw
    outs:
      - data/processed/raw_long.csv

  features:
    cmd: .venv/bin/python pipelines/forecast/features.py
    deps:
      - pipelines/forecast/features.py
      - data/processed/raw_long.csv
    params:
      - focus_start
      - active_cutoff
      - horizon
      - alchemist
      - caterpillar_candidates
    outs:
      - data/processed/train_alchemist.csv
      - data/processed/holdout_alchemist.csv
      - data/processed/train_caterpillar.csv
      - data/processed/holdout_caterpillar.csv
      - data/processed/active_isbns.csv
      - data/processed/caterpillar_isbn.txt

  train_arima:
    cmd: .venv/bin/python pipelines/forecast/train_arima.py
    deps:
      - pipelines/forecast/train_arima.py
      - data/processed/train_alchemist.csv
      - data/processed/train_caterpillar.csv
    params:
      - horizon
      - arima
    outs:
      - data/models/arima_alchemist.pkl
      - data/models/arima_caterpillar.pkl
      - data/models/forecast_alchemist.csv
      - data/models/forecast_caterpillar.csv
      - data/models/arima_orders.json

  train_imputation:
    cmd: .venv/bin/python pipelines/forecast/train_imputation.py
    deps:
      - pipelines/forecast/train_imputation.py
      - data/processed/train_alchemist.csv
      - data/processed/train_caterpillar.csv
      - data/processed/holdout_alchemist.csv
      - data/processed/holdout_caterpillar.csv
    params:
      - horizon
      - arima
    outs:
      - data/models/imputation_sweep.json

  train_xgb_lag:
    cmd: .venv/bin/python pipelines/forecast/train_xgb_lag.py
    deps:
      - pipelines/forecast/train_xgb_lag.py
      - data/processed/train_alchemist.csv
      - data/processed/train_caterpillar.csv
      - data/processed/holdout_alchemist.csv
      - data/processed/holdout_caterpillar.csv
    params:
      - horizon
      - seed
    outs:
      - data/models/forecast_xgb_lag_alchemist.csv
      - data/models/forecast_xgb_lag_caterpillar.csv
      - data/models/xgb_lag_orders.json

  train_xgb_calendar:
    cmd: .venv/bin/python pipelines/forecast/train_xgb_calendar.py
    deps:
      - pipelines/forecast/train_xgb_calendar.py
      - data/processed/train_alchemist.csv
      - data/processed/train_caterpillar.csv
      - data/processed/holdout_alchemist.csv
      - data/processed/holdout_caterpillar.csv
    params:
      - seed
      - horizon
    outs:
      - data/models/forecast_xgb_calendar_alchemist.csv
      - data/models/forecast_xgb_calendar_caterpillar.csv
      - data/models/xgb_calendar_orders.json

  train_lstm:
    cmd: .venv/bin/python pipelines/forecast/train_lstm.py
    deps:
      - pipelines/forecast/train_lstm.py
      - data/processed/train_alchemist.csv
      - data/processed/train_caterpillar.csv
    outs:
      - data/models/lstm_status.json

  train_hybrid_seq:
    cmd: .venv/bin/python pipelines/forecast/train_hybrid_seq.py
    deps:
      - pipelines/forecast/train_hybrid_seq.py
      - data/models/arima_alchemist.pkl
      - data/models/arima_caterpillar.pkl
      - data/models/forecast_alchemist.csv
      - data/models/forecast_caterpillar.csv
      - data/processed/train_alchemist.csv
      - data/processed/train_caterpillar.csv
      - data/processed/holdout_alchemist.csv
      - data/processed/holdout_caterpillar.csv
    outs:
      - data/models/forecast_hybrid_seq_alchemist.csv
      - data/models/forecast_hybrid_seq_caterpillar.csv
      - data/models/hybrid_seq_status.json

  train_lag_llama:
    cmd: .venv-lagllama/bin/python pipelines/forecast/train_lag_llama.py
    deps:
      - pipelines/forecast/train_lag_llama.py
      - data/processed/train_alchemist.csv
      - data/processed/holdout_alchemist.csv
      - data/processed/train_caterpillar.csv
      - data/processed/holdout_caterpillar.csv
    outs:
      - data/models/forecast_lag_llama_alchemist.csv
      - data/models/forecast_lag_llama_caterpillar.csv
      - data/models/lag_llama_status.json

  train_hybrid_par:
    cmd: .venv/bin/python pipelines/forecast/train_hybrid_par.py
    deps:
      - pipelines/forecast/train_hybrid_par.py
      - data/models/forecast_alchemist.csv
      - data/models/forecast_caterpillar.csv
      - data/processed/holdout_alchemist.csv
      - data/processed/holdout_caterpillar.csv
    outs:
      - data/models/forecast_hybrid_par_alchemist.csv
      - data/models/forecast_hybrid_par_caterpillar.csv
      - data/models/parallel_sweep_alchemist.csv
      - data/models/parallel_sweep_caterpillar.csv
      - data/models/hybrid_par_status.json

  charts:
    cmd: .venv/bin/python pipelines/forecast/charts.py
    deps:
      - pipelines/forecast/charts.py
      - data/processed/train_alchemist.csv
      - data/processed/holdout_alchemist.csv
      - data/processed/train_caterpillar.csv
      - data/processed/holdout_caterpillar.csv
      - data/models/forecast_alchemist.csv
      - data/models/forecast_caterpillar.csv
      - data/models/forecast_xgb_calendar_alchemist.csv
      - data/models/forecast_xgb_calendar_caterpillar.csv
      - data/models/forecast_hybrid_par_alchemist.csv
      - data/models/forecast_hybrid_par_caterpillar.csv
      - data/models/parallel_sweep_alchemist.csv
      - data/models/parallel_sweep_caterpillar.csv
    outs:
      - charts/forecast_alchemist.png
      - charts/forecast_caterpillar.png
      - charts/parallel_sweep.png

  evaluate:
    cmd: .venv/bin/python pipelines/forecast/evaluate.py
    deps:
      - pipelines/forecast/evaluate.py
      - data/processed/holdout_alchemist.csv
      - data/processed/holdout_caterpillar.csv
      - data/models/forecast_alchemist.csv
      - data/models/forecast_caterpillar.csv
      - data/models/arima_orders.json
      - data/models/forecast_xgb_lag_alchemist.csv
      - data/models/forecast_xgb_lag_caterpillar.csv
      - data/models/xgb_lag_orders.json
      - data/models/forecast_xgb_calendar_alchemist.csv
      - data/models/forecast_xgb_calendar_caterpillar.csv
      - data/models/xgb_calendar_orders.json
      - data/models/lstm_status.json
      - data/models/forecast_hybrid_seq_alchemist.csv
      - data/models/forecast_hybrid_seq_caterpillar.csv
      - data/models/hybrid_seq_status.json
      - data/models/forecast_hybrid_par_alchemist.csv
      - data/models/forecast_hybrid_par_caterpillar.csv
      - data/models/parallel_sweep_alchemist.csv
      - data/models/parallel_sweep_caterpillar.csv
      - data/models/hybrid_par_status.json
      - data/models/forecast_lag_llama_alchemist.csv
      - data/models/forecast_lag_llama_caterpillar.csv
      - data/models/lag_llama_status.json
      - data/models/imputation_sweep.json
    metrics:
      - metrics.json:
          cache: false
The submission notebook, rendered with its outputs.open full ↗
The 5-page report (Quarto → PDF/HTML).open full ↗
Interactive: the active backlist as a market; hover any title. Figures are rounded and the licensed dataset is not republished.open full ↗

Every model on the same 32-week holdout (MAE; lower is better). The submission stands behind SARIMA; the rest is documented here as research, not as the headline.

ModelAlch. MAECat. MAENotes
SARIMA (auto-ARIMA)127.8345.5submission baseline; near-best, cheapest
Hybrid (sequential)127.4339.9LSTM on SARIMA residuals — marginal gain
XGBoost (lag-window)136.8368.1in the report; trails on both
XGBoost + calendar features113.6304.5lowest point MAE — but not significant (DM p=0.18 / 0.13)
Hybrid (parallel)125.5351.2best blend weight collapses to SARIMA-only
LSTM (KerasTuner)176.0883.3weakest, badly so on the board book
Lag-Llama (zero-shot)162.4634.9time-series foundation model; lost to SARIMA
The honest cross-check. A calendar-feature XGBoost posts the lowest point MAE on both books — but a Diebold-Mariano test (n=32) puts the gap at p=0.18 / 0.13, so it is not statistically significant. On ~650 weekly points with one dominant annual cycle, SARIMA's simplicity and near-best accuracy make it the right production default. The point ranking is reported; the significance claim is not.

Also explored (beyond the rubric): SARIMAX with explicit Christmas event-regressors; a Box-Cox / log transform to model the multiplicative shape directly (ruled out — no holdout gain); a monthly cross-check (aggregate to months, 8-month horizon — did not beat the weekly models); and the arithmetic cost / stocking model in the next tab.