A 12-team, full-PPR mock draft simulator with ML-driven upside projections. Trains quantile regression models on historical NFL data to give every draftable player a floor / median / ceiling PPR projection, then runs a snake draft against 11 AI-managed bots so you can see the best-upside pick at every slot.
src/build_dataset.py— pulls 2010–2024 weekly + roster data from nflverse vianfl_data_py(free, public, no scraping/ToS issues) and builds a per-player-season feature table (usage, age, prior-season production) with PPR fantasy points as the label. Also callssrc/team_context.py(below) to attach depth-chart features.src/team_context.py— pulls nflverse depth charts (archived week-1 charts for historical seasons back to 2009, nflverse's continuously-refreshed live scrape for the current draft season) and derives, per player-season,depth_rank(1 = current starter at that position/team) andteammate_competition_ppr(how much fantasy production the other players at that position/team brought last season). This is what actually catches "a teammate this player used to split touches with is no longer on the roster" — a real signal from real roster data, fully automated, no per-player manual input. It turned out to be the single most important RB feature (37% of the median model's weight) once added.src/train_models.py— trains a LightGBM quantile regression model per position (QB/RB/WR/TE) at the 10th/50th/90th percentiles, using only information that would have been known before the season started. Median predictions beat a "just repeat last year's total" baseline at every position on held-out data (seemodels/metrics.json).src/fetch_live_adp.py— pulls live, real ADP from the free, public Fantasy Football Calculator API (aggregated from real drafts happening right now, 12-team full-PPR).src/projections.py— applies the trained models to the most recent completed season to project the upcoming (config.DRAFT_SEASON) season for every draftable player, overlays the live depth chart on top of each player's stale prior-season depth signal, merges in live ADP, estimates floor/median/ceiling for ADP-listed players with no modeled NFL history (see gap note below), and computes value-based-drafting metrics (VBD, upside score = ceiling above replacement).src/draft_engine.py— the draft simulator: snake order, roster slots, bot AI (drafts off live ADP, not our own ceiling model, so recommendations are a genuine edge rather than the model grading its own homework), and pick recommendations.app.py— Streamlit UI on top of the engine.
- A 2-season data gap, currently: nflverse's free player-stats release
tops out at the 2024 season (verified directly against the GitHub
release, not just a wrapper quirk), but live ADP tracks the real upcoming
draft (
config.DRAFT_SEASON). UpdateLAST_COMPLETED_SEASONinsrc/config.pyonce nflverse publishes a newer season. - Players with no 2024 stats (a real rookie/debut class) aren't
ML-projected — they get an ADP-tier estimate instead: an isotonic
curve fit from how similarly-drafted modeled players at the same
position actually project. Flagged
is_estimated=True/ shown with a ⚡ in the app. This is a reasonable stand-in, not a substitute for real per-player scouting. - DST and K are flat placeholders, not ML-projected — nflverse's free player-level weekly data doesn't include team defense stats, and kicker tagging is too sparse to train on. Ordered by live ADP so draft timing is still realistic.
- Resolved: median was underrating players who lost a competing teammate.
Jahmyr Gibbs initially projected to a 166 median off a 355-point 2024,
which looked wrong once you knew David Montgomery split his 2024
backfield. Investigation found two real things and fixed one bug:
- Real signal (kept as-is): Gibbs' 2024 TD rate (6.3%, 97th percentile of all 150+ touch RB seasons since 2010) was historically extreme, and the 42 RB-seasons in our data that followed a 300+ PPR prior season averaged a 35% drop the following year. Some regression is legitimate.
- Real blind spot (fixed): the original feature set was pure
prior-season box-score totals with zero depth-chart/personnel context,
so it had no way to know a competing teammate left. Added
depth_rank/teammate_competition_ppr(seeteam_context.pyabove) — fully automated from real nflverse depth charts, no manual per-player input. Backtested MAE improved at QB (-10%), RB (-9%), and WR (-9%);depth_rankalone became the single most important RB feature. Gibbs' median moved from 166 to 213 once his live depth chart (clear RB1, minimal competition) was factored in. - Bug (fixed): the p10/p50/p90 quantile models are trained independently and can disagree ("quantile crossing") for ~1-2% of players — an earlier version clamped Bijan Robinson's ceiling to exactly his median because the raw p90 prediction came in below the raw p50. Fixed by sorting the three raw predictions instead of clamping, preserving the real spread.
- Also fixed along the way:
build_dataset.pywas silently duplicating ~300 player-season rows for anyone traded mid-season (two+ roster rows in one season merged as a cartesian product). Deduped before the merge.
- Models predict season totals; no in-season injury updates (the depth
chart used for projection is whatever nflverse has scraped most
recently, refreshed continuously, but re-run
fetch_live_adp.py+projections.pyto pick up a newer snapshot). - Resolved: bot draft order had way more variance than real drafts.
A tight-consensus player like Bijan Robinson (real ADP stdev=0.8 --
drafted picks 1-4 almost every time) was falling to pick #9, or past #9
entirely, in some mocks. Root cause: bot picks used an arbitrary
"top-8 window, weighted by rank" heuristic instead of the REAL per-player
variance (
stdev) that the live ADP source already provides (and that this app was fetching but silently discarding). Fixed by simulating each available player's "effective pick this mock" as a draw fromNormal(adp_rank_overall, stdev)and taking the lowest — reproduces realistic draft-order noise instead of a made-up window size. Verified over 30 simulated drafts: Bijan now lands picks 1-3 every time, matching his real-world consensus range.
python -m venv .venv
./.venv/Scripts/pip install -r requirements.txt
# one-time (or re-run each offseason to refresh with a new completed season)
./.venv/Scripts/python src/build_dataset.py
./.venv/Scripts/python src/train_models.py
# re-run any time to refresh ADP/pool (ADP shifts daily during draft season)
./.venv/Scripts/python src/fetch_live_adp.py
./.venv/Scripts/python src/projections.py
# launch the app
./.venv/Scripts/streamlit run app.pyThen open http://localhost:8501 (or whatever port Streamlit reports).
src/config.py league settings, scoring, model knobs
src/build_dataset.py nflverse pull -> data/season_features.parquet
src/team_context.py nflverse depth charts -> depth_rank / teammate_competition_ppr
src/train_models.py trains models -> models/*.txt, models/metrics.json
src/fetch_live_adp.py Fantasy Football Calculator pull -> data/adp_live.parquet
src/projections.py models + live ADP + live depth chart -> data/draft_pool.parquet
src/draft_engine.py draft simulation engine (no UI)
app.py Streamlit UI
- Rookie projections seeded from real draft capital / college production instead of the ADP-tier curve fit.
- Further context features: offensive line continuity/scheme, target share trend within a season (not just the season total), injury report status.
- Multiplayer draft (multiple humans, one board).
- Trade/keeper support.
- Perk-tree-style "what should I have done" post-draft grading vs. season-end actuals once a season completes.