Targets
Three weekly targets are forecast independently:
target_count— Poisson / negative-binomial regression of weekly incident counts. Range 0–3 in TEST.target_any(binary, ≥1 incident in week) — logistic regression. Base rate in the TEST window: 22% (23 of 104 weeks).target_high(binary, ≥2 incidents in week) — logistic regression. Base rate in the TEST window: 1% (1 of 104 weeks).
All targets share the same weekly panel and the same feature set.
Features
Built weekly from signals observable before the outcome week. No-leak
invariant: each feature f for week t uses only data from weeks ≤ t−1.
- Autoregressive (
incidents_prev_{1,2,4,8,13,26,52}wk): rolling counts of incidents in the prior 1, 2, 4, 8, 13, 26, 52 weeks. incidents_prev_year_matching_woy: lag-52w count for the same week-of-year.- Public-coverage proxy (
incidents_count_lag{k}for k ∈ {1,2,4,8,13,26,52}): monthly mass-shooting incident count fromgtrends_mass_shooting.csv, linearly interpolated to week-grain and lagged. Monthly values only become available after month end, so a future monthly total never leaks into the same month. - NICS firearm background checks (
nics_{handgun,long_gun,multiple,other,total}_lag{1,4,13,26,52}): monthly national sums, interpolated to weekly via a piecewise-linear triangular window whose anchors are the month start, midpoint, and end (weights sum to the monthly total; never interpolate to an adjacent month's value). Lags of 1, 4, 13, 26, and 52 weeks. NICS data ends Sep 2023; the last observed weekly level is carried forward through Dec 2023 rather than fabricated. - Calendar (sin/cos pairs): month, day of year, ISO week of year.
- Holiday & school-session flags: ISO week containing Jan 1 / Jul 4 / Thanksgiving / Christmas-New Year. School-session indicator is the fraction of Monday-Friday dates in a week that fall between Aug 15 and May 31.
- Population (slow-moving scale control).
Models
Each model file exposes fit_predict(train, test, target_col, feature_cols):
| # | Model | Family | Notes |
|---|---|---|---|
| 1 | naive_seasonal | baseline | y_t = mean of historical same-week-of-year over prior 5y (Poisson quantiles) |
| 2 | nb_lag | stat | Negative-binomial GLM, lags-only features |
| 3 | nb_full | stat | Negative-binomial GLM, full feature set, light ridge regularization |
| 4 | logistic_any | stat | Logit on the ≥1 target, slopes penalized (intercept unpenalized) |
| 5 | logistic_high | stat | Logit on the ≥2 target |
| 6 | rf_any (conditional) | ml | sklearn RandomForestClassifier on ≥1 |
Random forest was intentionally dropped. The build rule allowed the RF
commit only if import sklearn completed in under 1 second; the measured
cold-import was 1.54 seconds, past the cutoff. The omission was made
before results were examined and is recorded in results/model_metadata.json
under rf_skip_reason. The two required logistic models are the binding
deliverable.
Metrics & validation
- Train — 2014-W01 → 2021-W52 (418 weeks).
- Val (last chronological 10% of train) — 2021-W01 → 2021-W52.
- TEST — 2022-W01 → 2023-W52 (104 weeks, never fit).
Backtest via rolling-origin inner cross-validation on train (3 folds in 2019,
2020, 2021) plus fixed held-out test. The metrics that ship in
results/metrics_*.csv are TEST-only; results/cv_metrics.csv carries
the inner-CV scores.
| Task | Count metrics | Binary metrics |
|---|---|---|
| count | RMSE, MAE, stabilized MAPE, pinball @ {0.5, 0.8, 0.9} | — |
| any | — | AUC-PR, AUC-ROC, Brier |
| high | — | AUC-PR, AUC-ROC, Brier |
Every model is reported alongside the seasonal baseline for the same task.
Stabilized MAPE uses max(|y|, 1) as denominator (ordinary MAPE is undefined
when most true counts are zero). Labeled explicitly in code.
Pipeline
make all
data → Load + validate CSVs
features → Build 522-week panel, compute 40 features
train → Fit 5 models on train (with rolling-origin inner-CV)
eval → Evaluate on held-out test (104 weeks, 2022-W01 → 2023-W52)
figures → Observed-vs-predicted, feature importance, residuals, ROC, calibration
paper → (rebuilt in pipeline repo at paper/manuscript.md)
test → pytest -q (loaders, features, metrics)
Each target is make-stage independent — make eval regenerates metrics
without re-training. Total wall-clock on a single CPU: ~2 minutes.