An end-to-end NFL game prediction system targeting point spread and totals betting. The pipeline ingests play-by-play and schedule data from nflverse, transforms it through a layered dbt project in Snowflake, trains a hybrid ensemble ML model, and generates weekly predictions β all orchestrated by Dagster.
Primary Goal: Achieve >52.4% ATS (against-the-spread) accuracy to overcome standard -110 juice.
| Layer | Technology |
|---|---|
| Data Warehouse | Snowflake |
| Data Transformations | dbt (dbt-snowflake) |
| Data Ingestion | Python Β· Polars Β· nflverse parquet releases |
| ML Models | XGBoost Β· PyMC (Bayesian) Β· PyTorch (Neural Net) Β· scikit-learn |
| Orchestration | Dagster (assets, jobs, schedules) |
| Code Quality | ruff (lint + format) |
| Logging | loguru |
| Auth | Snowflake key-pair authentication |
| Runtime | Python 3.11+ |
nfl_batchgineering/
βββ dagster_project/ # Dagster orchestration definitions
β βββ __init__.py # Definitions entry point (assets, jobs, schedules)
β βββ constants.py # Shared path constants
β βββ assets/
β β βββ ingestion.py # nflverse β Snowflake raw assets
β β βββ dbt_assets.py # dbt model assets (auto-generated from manifest)
β β βββ ml_training.py # XGBoost training asset
β β βββ predictions.py # Weekly predictions asset
β βββ jobs/
β β βββ weekly_pipeline.py # Full pipeline job definition
β βββ resources/
β β βββ dbt_resource.py # dbt CLI resource configuration
β βββ schedules/
β βββ weekly_schedule.py # Tuesday 8AM NFL-season schedule
β
βββ dbt_project/ # dbt transformations
β βββ dbt_project.yml # Project config (lookback: 10 seasons)
β βββ models/
β β βββ 1_staging/nflverse/ # Cleaned views over raw Snowflake tables
β β β βββ stgnv_play_by_play.sql
β β β βββ stgnv_player_summary_stats.sql
β β β βββ stgnv_team_summary_stats.sql
β β β βββ stgnv_rosters.sql
β β β βββ stgnv_schedules.sql
β β β βββ stgnv_injuries.sql
β β β βββ stgnv_play_by_play_participation.sql
β β βββ 2_intermediate/ # Feature engineering (views + materialized tables)
β β β βββ int_plays_cleaned.sql
β β β βββ int_team_offensive_metrics.sql # Rolling 4-week EPA/play
β β β βββ int_team_defensive_strength.sql # Rolling defensive EPA allowed
β β β βββ int_situational_efficiency.sql # Red zone, 3rd down, etc.
β β β βββ int_game_vegas_lines.sql # Opening lines, line movement
β β β βββ int_upcoming_games.sql # Future schedule with context
β β βββ 3_marts/ # Final ML-ready tables
β β βββ mart_predictive_features.sql # Per-team, per-week feature set
β β βββ mart_game_prediction_features.sql # Historical game rows for training
β β βββ mart_upcoming_game_predictions.sql # Upcoming games for inference
β β βββ mart_weather_impact_metrics.sql # Weather-adjusted efficiency
β β βββ mart_model_validation.sql # Walk-forward validation dataset
β βββ macros/
β βββ tests/
β
βββ src/
β βββ ingestion/
β β βββ training_data_loader.py # Bulk parquet β Snowflake loader (Polars)
β βββ ml/
β βββ base.py # Abstract BasePredictor interface
β βββ models/
β β βββ spread_predictor.py # XGBoost spread model
β β βββ elo_model.py # Elo rating baseline
β β βββ bayesian.py # Bayesian state-space model (PyMC)
β β βββ neural.py # Shallow neural network (PyTorch)
β β βββ ensemble.py # Ridge stacking meta-learner
β βββ utils/
β β βββ feature_engineering.py # Feature selection and preparation
β β βββ evaluation.py # ATS accuracy, Brier score, RMSE
β β βββ validation.py # Walk-forward CV helpers
β βββ train_spread_model.py # XGBoost training entry point
β βββ train_elo_model.py # Elo model training
β βββ train_ensemble.py # Full ensemble training
β βββ predict.py # Inference: load models β generate predictions
β
βββ models/ # Serialized model artifacts (.pkl, .json)
βββ data/ # Prediction CSV outputs
βββ notebooks/ # Exploratory analysis
βββ snowflake_sql/ # Ad-hoc SQL scripts
βββ logs/ # Rotating application logs
βββ pyproject.toml # Project metadata, uv/ruff config
βββ requirements.txt # Python dependencies
βββ TESTING_GUIDE.md # dbt layer validation walkthrough
βββ CLAUDE.md # AI assistant project instructions
nflverse GitHub Releases (parquet)
β
βΌ
TrainingDataLoader (Polars + Snowflake connector)
β bulk COPY INTO
βΌ
Snowflake RAW.NFLVERSE βββββββββββββββββββββββββββββββββββββββ
β β
βΌ dbt run β
1_staging (views) β
Β· stgnv_play_by_play β
Β· stgnv_schedules, stgnv_rosters β
Β· stgnv_player/team_summary_stats β
Β· stgnv_injuries, stgnv_pbp_participation β
β β
βΌ dbt run β
2_intermediate (views + tables) β
Β· int_plays_cleaned β
Β· int_team_offensive_metrics (rolling EPA/play) β
Β· int_team_defensive_strength (rolling EPA allowed) β
Β· int_situational_efficiency β
Β· int_game_vegas_lines β
Β· int_upcoming_games β
β β
βΌ dbt run β
3_marts (tables) β
Β· mart_game_prediction_features β training data β
Β· mart_predictive_features β per-team feature store β
Β· mart_upcoming_game_predictions β inference input β
Β· mart_model_validation β
Β· mart_weather_impact_metrics β
β β
βΌ Python β
ML Training (Snowflake β Polars β sklearn/XGBoost/PyMC) β
Β· SpreadPredictor (XGBoost) β
Β· EloModel (baseline) β
Β· BayesianStateSpace (PyMC AR(1)) β
Β· NeuralNetPredictor (PyTorch) β
Β· StackingEnsemble (Ridge meta-learner) β
β β
βΌ Python β
Weekly Predictions β Snowflake ML schema + CSV output βββββββββ
The system uses a stacking ensemble of four base models, combined by a Ridge regression meta-learner:
βββββββββββββββ βββββββββββββββ βββββββββββββββ βββββββββββββββ
β XGBoost β β Bayesian β β Neural β β Elo β
β Regressor β β State-Space β β Network β β Baseline β
β (max_depth4)β β (PyMC AR1) β β (PyTorch, β β (Glickman) β
β (lr=0.05) β β β β β€3 layers) β β β
ββββββββ¬βββββββ ββββββββ¬βββββββ ββββββββ¬βββββββ ββββββββ¬βββββββ
ββββββββββββββββββ΄βββββββββββββββββ΄βββββββββββββββββ
β
ββββββββββββ΄βββββββββββ
β Ridge Regression β
β Meta-Learner β
β (+ uncertainty) β
ββββββββββββ¬βββββββββββ
β
ββββββββββββ΄βββββββββββ
β Betting Decision β
β (edge β₯ 3 pts β β
β 0.25x Kelly bet) β
βββββββββββββββββββββββ
All models implement the BasePredictor abstract interface (src/ml/base.py) providing fit(), predict(), predict_proba(), save_model(), and load_model().
Walk-forward cross-validation is required β random splits are never used:
- Train on seasons
[t-n ... t-1], test on seasont - Repeat sliding the window forward for each available test season
- Target metrics: ATS Accuracy (>52.4%), Brier Score, RMSE vs Vegas line
The Dagster project (dagster_project/) defines the full pipeline as software-defined assets:
| Asset | Description |
|---|---|
raw_nflverse_data |
Loads current-season parquet datasets to Snowflake |
raw_schedules |
Loads full schedules file (all seasons, full replace) |
nfl_dbt_assets |
Runs dbt models (staging β intermediate β marts) |
trained_xgboost_model |
Trains XGBoost spread predictor, saves to models/ |
weekly_predictions |
Loads upcoming games mart, runs ensemble inference, writes to Snowflake + CSV |
Schedule: Every Tuesday at 8:00 AM during the NFL season (SeptemberβFebruary). The schedule auto-derives the current NFL week and season from the execution date.
To launch the Dagster UI locally:
dagster dev -m dagster_projectNFL_ANALYTICS.RAW.NFLVERSE -- Bulk-loaded parquet tables
NFL_ANALYTICS.STAGING -- dbt views (stgnv_*)
NFL_ANALYTICS.INTERMEDIATE -- dbt views/tables (int_*)
NFL_ANALYTICS.MARTS -- dbt materialized tables (mart_*)
NFL_ANALYTICS.ML -- Model artifacts and predictions- Python 3.11+
- Snowflake account with key-pair authentication configured
- dbt CLI (installed via
dbt-snowflake)
# Clone and create virtual environment
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txtCreate a .env file at the project root:
SNOWFLAKE_ACCOUNT=your_account_identifier
SNOWFLAKE_USER=your_username
SNOWFLAKE_WAREHOUSE=NFL_ANALYTICS_WH
SNOWFLAKE_DATABASE=NFL_ANALYTICS
SNOWFLAKE_SCHEMA=RAW
SNOWFLAKE_ROLE=your_role
# Key-pair authentication
SNOWFLAKE_KEYPAIR_PRIVATE_KEY="-----BEGIN ENCRYPTED PRIVATE KEY-----\n...\n-----END ENCRYPTED PRIVATE KEY-----"
SNOWFLAKE_KEYPAIR_PASSPHRASE=your_key_passphraseAdd the following to ~/.dbt/profiles.yml:
nfl_batchgineering:
target: dev
outputs:
dev:
type: snowflake
account: "{{ env_var('SNOWFLAKE_ACCOUNT') }}"
user: "{{ env_var('SNOWFLAKE_USER') }}"
private_key: "{{ env_var('SNOWFLAKE_KEYPAIR_PRIVATE_KEY') }}"
private_key_passphrase: "{{ env_var('SNOWFLAKE_KEYPAIR_PASSPHRASE') }}"
role: "{{ env_var('SNOWFLAKE_ROLE') }}"
database: NFL_ANALYTICS
warehouse: NFL_ANALYTICS_WH
schema: STAGING
threads: 4python src/ingestion/training_data_loader.pyLoads the following nflverse datasets for the current season (historical years are a one-time load):
play_by_playβ full play-by-play with EPArostersβ active and historical rostersplayer_summary_statsβ per-player season/weekly statsteam_summary_statsβ per-team summary statsplay_by_play_participationβ player participation per playinjuriesβ weekly injury reportsschedulesβ full schedule with Vegas lines (all seasons, single file)
cd dbt_project
dbt run # Build all models
dbt test # Run data quality testsOr layer by layer:
dbt run --select 1_staging
dbt run --select 2_intermediate
dbt run --select 3_martsSee TESTING_GUIDE.md for validation queries and expected row counts.
# XGBoost spread model (primary)
python src/ml/train_spread_model.py
# Elo baseline model
python src/ml/train_elo_model.py
# Full ensemble (requires base models trained first)
python src/ml/train_ensemble.pyTrained artifacts are serialized to the models/ directory.
python src/ml/predict.py --week 15 --season 2025 --output predictions_week15.csvOr run the full pipeline via Dagster:
dagster dev -m dagster_projectAll rolling features use point-in-time joins. Window functions use ROWS BETWEEN N PRECEDING AND 1 PRECEDING β never including the current game. This is enforced in dbt intermediate models and validated via mart_model_validation.
NFL seasons have ~270 games. With ~20 years of data that is ~5,000 samples. The architecture deliberately avoids deep learning (LSTM, transformers) in favor of shallow models with strong regularization.
Models are optimized for Brier Score. Well-calibrated probabilities produce higher ROI than raw directional accuracy (Walsh & Joshi, 2024).
All bet sizing uses 0.25x Kelly to reduce variance. A minimum edge threshold of 3β4 points against the Vegas line is required before any bet is recommended.
# Lint and format with ruff
ruff check .
ruff format .Configuration in pyproject.toml: line length 88, Python 3.11 target, isort integrated.
MIT