A live, CSV-backed system that produces:
- The current official AP Top 25.
- A machine-learning forecast of the next AP Top 25.
- An opponent-adjusted independent ranking of FBS teams.
- Predicted margins and scores for upcoming games, compared with current consensus spreads.
- Locally cached team logos and official team colors for the included HTML frontend.
- Full-season team schedules, selectable spread and moneyline props, model EV, and a local bet-slip calculator.
The sportsbook spread is never an input to the independent model. It is joined only after prediction so the model-versus-market comparison remains honest.
The backend serves the HTML and JSON API from the same process. Start it with:
cfb-rankings serveThen open http://127.0.0.1:8000. The frontend automatically loads the latest backend output;
CSV importing remains available only as a fallback. Available endpoints are:
GET /api/healthGET /api/rankingsGET /api/gamesGET /api/scheduleGET /api/teams/{team}GET /team_logos/{filename}
CSV responses are cached in memory until the source file's modification time or size changes. Because prediction files are written atomically, a live refresh cannot expose a partial CSV. For deployment, run this service behind the site's HTTPS reverse proxy rather than exposing the Uvicorn port directly.
The training unit is one team in one historical AP poll week. Every row contains only information available before that poll. The pipeline performs rolling-season validation of:
- XGBoost LambdaMART (
rank:ndcg) - LightGBM LambdaRank
- XGBoost AP-points regression
- Histogram gradient-boosted AP-points regression
- The prior poll as a persistence baseline
The champion is selected from out-of-time evidence using NDCG@25, top-25 membership F1,
rank error, and rank correlation. Results are written to models/ap_model_evidence.csv; the
dashboard exposes them rather than silently hard-coding a favored algorithm.
Bayesian Ridge and XGBoost are trained to predict the home team's final scoring margin from the same opponent-adjusted inputs. Complete future seasons select the winner; Bayesian Ridge is preferred only when its validation MAE is within 0.05 points of the best result.
For every completed game, separate partially pooled Bayesian offense and defense ratings create pregame expectations for scoring, rushing, and passing. Observations update both the team's offense and its opponent's defense according to their current uncertainty. The model retains both the latent ratings and actual-minus-expected residuals. Positive residuals mean the team performed better than the matchup model expected. Inputs also include turnover margin, possession time, Elo strength of schedule, home field, and optionally team Elo. The sportsbook line is never an input. The separate betting-edge model remains XGBoost and is not shown as validated unless its held-out evidence clears the safety thresholds.
To rank teams, the trained model predicts every FBS-versus-FBS matchup on a neutral field. A team's independent rating is its average predicted margin across those opponents. This makes the ranking a direct model output, not a manually weighted polynomial score.
Early-season rate statistics use a four-game neutral prior before entering the model. The raw record and displayed statistics remain unchanged, but one blowout cannot masquerade as a stable full-season average. Elo persists only for teams that were FBS in the prior season; new FBS members do not inherit a lower-division rating. Independent-ranking exports include feature-contribution groups so each team's detail page shows what raised or lowered its rank.
The project deliberately prevents these common backtesting errors:
- A game uses team state from before kickoff, never end-of-season averages.
- A poll forecast uses games through the preceding football week.
- Ranked wins use the poll available when the game was played.
- The betting spread is excluded from independent-model features.
- Model selection validates on later, completely unseen seasons.
- Betting lines are stored with retrieval timestamps rather than overwritten.
The Windows portable bundle includes all Python 3.14 dependency wheels. It does not require administrator rights, a compiler, editable installation, or internet access. From PowerShell:
.\setup-offline.ps1
.\cfb.ps1 bootstrap --start-year 2014
.\cfb.ps1 audit
.\cfb.ps1 build-features
.\cfb.ps1 train
.\cfb.ps1 predict
.\cfb.ps1 dashboardDependencies are placed under %LOCALAPPDATA%\CFBRankingRuntime\py314, outside OneDrive and
outside the system Python installation. You do not activate a virtual environment.
From PowerShell in this folder:
py -3.12 -m venv .venv
.\.venv\Scripts\Activate.ps1
python -m pip install --upgrade pip
pip install -e ".[dev]"
Copy-Item .env.example .env
notepad .envPut the CollegeFootballData key in .env:
CFBD_API_KEY=your_real_key_here
CFB_CURRENT_SEASON=2026
CFB_TIMEZONE=America/ChicagoThe .env file is ignored by Git and must not be emailed or committed.
cfb-rankings bootstrap --start-year 2014
cfb-rankings audit
cfb-rankings build-features
cfb-rankings train
cfb-rankings predict
cfb-rankings dashboardThe historical bootstrap may take several minutes. Optional advanced-stat or line endpoints are allowed to fail cleanly if the API key's plan does not include them; the core games, rankings, and team data are required.
After the initial model training:
.\scripts\update_current.ps1This refreshes the current season, audits the CSVs, rebuilds pregame features, and regenerates rankings and upcoming-game predictions. It does not retrain on every refresh. To deliberately reselect and retrain both models:
.\scripts\retrain.ps1Run the refresh after the final relevant game and before the AP poll is released to create a true next-poll prediction. Running it after release creates a nowcast that can be compared with the newly observed poll.
All tables are ordinary CSV files:
data/
├── raw/
│ ├── games.csv
│ ├── rankings.csv
│ ├── teams.csv
│ ├── team_game_stats.csv
│ ├── advanced_game_stats.csv
│ └── betting_lines.csv
├── processed/
│ ├── data_audit.csv
│ ├── game_training_data.csv
│ ├── team_week_features.csv
│ └── ap_training_data.csv
└── predictions/
├── actual_ap_poll.csv
├── predicted_ap_poll.csv
├── independent_rankings.csv
├── upcoming_game_predictions.csv
├── season_schedule.csv
├── current_rankings.csv
├── ap_prediction_history.csv
└── game_prediction_history.csv
frontend/
├── index.html
└── team_logos/
└── <team_id>.png
The two history files are append-only prediction snapshots. Each AP snapshot is marked
pre_release_forecast or nowcast_after_release, preventing a post-release refresh from being
mistaken for a genuine forecast.
Trained model binaries and validation evidence are placed in models/.
The CFBD teams response already supplies each team's logo URLs, primary color, and alternate
color. During predict, the backend downloads the current-season logos once into
frontend/team_logos/ and reuses the cached files on later refreshes. Logo download failures
are logged but never block rankings or game predictions.
The ranking CSVs include team_id, logo_url, logo_path, team_color, and alt_color.
Upcoming-game predictions include the same fields with home_ and away_ prefixes. Open
frontend/index.html directly; it uses the local image first, the remote URL as a fallback,
and team initials if neither image is available.
If team_game_stats.csv is empty, fetch just the missing box-score feed without
redownloading rankings, lines, or the schedule:
cfb box-scores --start-year 2014 --end-year 2026
cfb build-featuresThe command preserves successful weeks when CFBD rejects an unavailable week.
build-features will stop instead of silently training unless rushing yards,
passing yards, and possession time are present. Its output reports the usable
row count for each required category.
For each game, the backend keeps every provider's most recent quote, uses the median spread, and selects the best available home and away moneyline. Expected value per $100 is calculated for all four selectable outcomes: both teams' spreads and both teams' moneylines. The Highest EV view places those prices directly beside each team and sorts the upcoming slate by the strongest available model EV.
CFBD supplies spread points but usually does not supply spread juice. When exact spread prices
are absent, the backend uses a clearly marked standard -110 assumption. Exact provider spread
prices are retained and used automatically if homeSpreadOdds and awaySpreadOdds are present.
The frontend bet slip can mix spread or moneyline parlay legs and single bets. It calculates combined American odds, total wager, return if the picks hit, and model-estimated EV. Slip data stays in browser local storage and never places a wager. Parlay probability multiplies the leg probabilities and is explicitly labeled as an independence approximation because correlated outcomes can invalidate it.
The model stores model_home_margin as:
predicted home score - predicted away score
Positive means the home team is favored. CFBD's numeric sportsbook spread is a home-team
handicap, so a spread of -3.5 becomes a market-implied home margin of +3.5.
model_edge_home = model_home_margin - market_home_margin
Positive edge favors the home team against the line; negative edge favors the away team. Line timestamps and contributing providers appear beside every comparison. These estimates are uncertain and are intended for model evaluation, not as guaranteed betting outcomes.
pytest
ruff check src tests