Methodology
Possession-based ratings, calibrated game distributions, and a market-anchored in-game win probability model
This model rates every Division 1 college football team on a single scale: expected scoring margin against an average FBS opponent on a neutral field. From those ratings it projects a spread and a total for every game, with a full probability distribution around each number, and an in-game layer turns any game state into a home win probability on every play.
One thing should be said up front. The pregame model does not beat the closing spread. Across 3,853 backtested games from 2021 through 2025, the model's average margin error is 13.40 points and the closing line's is 12.14. The market wins every season. Those are backtested numbers on seasons that already happened, provisional in the way all sports backtests are; the backtest section says more. The result is not buried in a footnote because it drives the most consequential design decision in the system: once a game kicks off, the in-game model anchors on the closing spread rather than the model's own number, and gets measurably better for it.
What the model is for, then, is coverage and calibration. It rates all 266 D1 teams including FCS programs the market barely prices, every projection ships with a distribution whose 50/80/95% intervals have been verified against three untouched holdout seasons, rating uncertainty widens explicitly when inputs are missing, and the entire prediction history is frozen and graded in public on the Performance page.
Prediction target
Home margin and game total, joint distribution
Rating unit
Points per possession, offense and defense per team
Rating engine
Bayesian linear model, posterior in closed form
Training data
2019–2025 play-by-play; holdout 2023–2025
Home field
Fitted each season, currently 3.22 points
In-game model
3-parameter Gaussian on the final margin
Kickoff anchor
Market closing spread, sd 15.45 points
Market output
+EV flags for benchmarking; no picks, no sizing
The system splits into a rating engine that learns team strength from possession-level data, a projection layer that turns ratings into calibrated game distributions, and an in-game layer that consumes a pregame anchor plus a play feed and nothing else. Each layer only sees what it would have known at the time: the walk-forward backtest, the preseason snapshot, and the in-game serving path all enforce that boundary mechanically.
Inputs
ESPN play-by-play
SportsDataverse releases, 2019+
CFBD API
Games, lines, talent, portal, returning
The Odds API
Priced spreads and totals
Previous season fit
Seeds the preseason prior
Rating Engine
Possession construction
Scrimmage runs, garbage-time aware
Ridge over off/def PPP
Two numbers per team + fitted HFA
EPA process blend
Scoreboard and process, GLS-combined
Preseason prior
Talent, portal, returning production
Projections
Margin and total
Bivariate Student-t per game
Calibrated intervals
50/80/95% checked on holdout
Market comparison
Cover probability and EV per offer
Supabase publish
The only interface to this site
In-Game
Play-boundary states
Strictly pre-snap information
3-parameter WP model
Gaussian on the final margin
Market anchor
Closing spread as the kickoff prior
Rebuild-per-play serving
~9 ms median per event
Everything runs as batch CLI commands over parquet files. There is no daemon and no scheduler; the only network polling in the repo is an append-only live odds capture that writes immutable, content-hashed snapshots. Live in-game serving against a paid low-latency feed is designed for but deliberately not wired up yet.
Ingest
Schedules, lines, talent, portal, play-by-play into raw parquet
Possessions
Classify plays, build possessions, aggregate team-game features
Fit
Ridge over offense/defense PPP plus home field, or the preseason prior
Project
Margin and total distributions for every upcoming game
Anchor
Outcome-free pregame anchors for in-game serving
Publish
Serving tables written to the Supabase cfb schema
Data sources
The engine works at the possession level. Raw ESPN play-by-play is classified play by play (scrimmage, special teams, penalty, clock management, administrative), and possessions are rebuilt as maximal chronological runs of scrimmage plays by one offense. Garbage time is excluded with period-specific thresholds: a play no longer counts as competitive once the margin exceeds 43 points in the 1st quarter, 38 in the 2nd, 28 in the 3rd, or 22 late. Each game then contributes an offense and a defense row per team, with points per possession as the target.
The model itself is small on purpose: one offense number and one defense number per team, plus a single shared home-field term. For 266 teams that is roughly 530 parameters, solved as a ridge regression in closed form. Ridge is the computational name; statistically this is a Bayesian update. The priors are genuine Gaussian priors, conjugate with the Gaussian likelihood, so the posterior mean and covariance drop straight out of the normal equations, and that covariance is what feeds projection uncertainty later.
E[ppp_away] = base + off_away − def_home − 0.5 · hfa
// hfa applies only off neutral sites; prior 2.5 ± 1.5 points, currently fit at 3.22
Scoreboard points are a noisy signal of team quality, so the fit runs in two stages. The first stage regresses a 50/50 blend of actual points and an EPA-derived process score. The second stage measures the residual covariance between the two signals and re-solves against their precision-weighted combination, so whichever signal has been more reliable that season gets more say. Expected possessions come from a separate small ridge on game pace.
FCS teams are handled inside the same system rather than dropped: their prior is recentered to the observed FCS-to-FBS gap before the solve, and ratings are centered so that zero always means an average FBS team.
The hyperparameters (prior strength 0.45 ppp, covariance shrinkage 0.8, Student-t degrees of freedom 500, covariance scale 1.125) were selected by grid search on the 2019–2022 development seasons only. One search result worth calling out: the grid included recency half-lives from 3 weeks up, and it preferred no time decay at all. College football seasons are short. Throwing away September to sharpen November costs more than it buys.
The Ratings page also publishes rush offense, pass offense, rush defense, pass defense, pass blocking, and run blocking. Each number is fit with an opponent-adjusted ridge over completed games strictly before the forecast week, then centered so zero is an average FBS unit. Positive is better in every column. The six values are descriptive companions only: they never feed the joint scoring engine and do not add up to the headline offense or defense rating.
Rush and pass units use competitive-play PPA. Pass blocking measures sack PPA relative to the expected sack cost for the team's number of dropbacks. Run blocking uses adjusted line yards, converted to a PPA-equivalent scale from the same admitted training window. Both line ratings capture shared outcomes involving the line, skill players, quarterback, and scheme, so they are not film grades of the offensive line in isolation.
A preseason forecast carries the latest final unit ratings from the prior season. Teams without that history receive a neutral zero and a visible missing-history flag. Special teams is omitted because the current CFBD play contract does not provide reliable PPA for routine punts, kickoffs, and field-goal attempts; inventing a complete rating from sparse scoring-event rows would be misleading.
College football resets hard every offseason: rosters turn over through the draft and the transfer portal, and week 1 arrives with no current-season games to learn from. The preseason rating starts from last season's final fit and layers standardized offseason signals on top. Each input is converted to a z-score across all of D1, then multiplied by a weight denominated directly in points of rating:
| Input | Points per SD | Note |
|---|---|---|
| Previous season rating | 1.00 | Carried in points, not standardized |
| Talent composite (prior season) | 1.50 | CFBD roster talent |
| Talent composite (current season) | 1.50 | Zero until CFBD publishes it |
| Returning production | 1.20 | Percent of team PPA returning |
| Transfer portal quality balance | 1.00 | Rated arrivals minus departures |
| QB continuity | 1.00 | Percent of passing PPA returning |
| Recruiting class points | 0.80 | CFBD team recruiting |
| Transfer portal count balance | 0.35 | Headcount in minus out |
| Coach continuity | ±0.35 | Same head coach as last season, or not |
Transfer portal quality is measured as the summed rating of rated arrivals minus rated departures, with a separate QB-only version feeding the scoring environment (the offense/defense split). A parallel equation projects each team's scoring environment from QB continuity, QB transfers, and returning receiving production, which is what lets two teams with the same power rating project different totals.
Uncertainty is an explicit budget. Every rating starts at 6.05 points of standard deviation and grows in quadrature for each missing input: 3.0 more if the team has no previous rating, 3.0 for FCS teams, 1.5 for missing returning production, 1.25 for unknown QB continuity, and so on down to 0.75 for recruiting. Injury availability has no data source at all, so its term is always on. The SD column on the Ratings page is this number, and it flows straight into wider spread distributions for affected games. The preseason prior covers week 1 only; from week 2 on, the in-season engine takes over.
Because power rating is offense plus defense and scoring environment is offense minus defense, the projection algebra collapses to something you can do in your head:
model_total = league_base + environment_home + environment_away
// league_base = league scoring rate × the teams' blended pace
A team rated +10 is a 10-point favorite over an average FBS team on a neutral field. Home field adds 3.22 points this season. The published spread is the negated margin, following the sportsbook sign convention.
Around that point estimate sits a bivariate Student-t distribution over margin and total, built from the fit's residual covariance plus full parameter uncertainty, including both teams' rating SDs. A typical in-season margin SD is about 17 points, and preseason projections with more missing inputs run closer to 18. Seventeen points sounds enormous until you grade forecasts against final scores for seven seasons; college football is just that noisy, and pretending otherwise produces intervals that fail their coverage checks.
For market comparisons, the distribution prices every offer directly: the probability the home side covers is the t-CDF of the edge over the scale, and expected value follows from the American price. Plays where the model sees an edge of at least 4 points with positive EV get flagged. Those flags are the model's benchmark against the market, and each one is also a prompt to review the model's inputs first, since the biggest edges in practice tend to involve FCS opponents with degraded data. What never happens is the step after: no sizing, no picks, and every row ships with recommendation_status set to not_recommended.
Every backtested prediction was made walking forward through each season using only completed games from strictly earlier weeks, then frozen. The comparison is mean absolute error of the projected home margin against the actual margin, model versus closing spread, on the same games:
| Season | Model MAE | Market MAE |
|---|---|---|
| 2021 | 13.60 | 12.49 |
| 2022 | 13.86 | 12.28 |
| 2023 | 13.26 | 12.00 |
| 2024 | 13.46 | 12.02 |
| 2025 | 12.84 | 11.93 |
| Pooled | 13.40 | 12.14 |
The market wins every season and every week bucket. That is the expected outcome: the closing line aggregates injury news, weather, and the sharpest private models in the world, and beating it consistently is rare enough that claiming to should be treated as a red flag. What the model can claim is calibration. On the untouched 2023–2025 holdout (2,258 games), the 50/80/95% margin intervals covered 51.6%, 80.1%, and 94.2% of outcomes, and the total projection came out unbiased to a hundredth of a point.
One caveat applies to everything in this table, and it applies to sports modeling generally. A backtest reports how this procedure would have done in seasons that already happened, and college football does not hold still: the portal, NIL, realignment, and playoff expansion keep rewriting the sport underneath the model, and the market adapts too. The absolute error levels above are era-specific and should be expected to drift. They stand in because the season has not started yet; once it does, live graded games on the Performance page become the record that matters, and they supersede this table as they accumulate. What the backtest is trusted for is its structural findings, the ones that repeated in every single season: the market beats the model pregame, momentum fails out of sample, the stated intervals cover. Those drove the design decisions, and they transfer far better than any error number.
The known weak segment is early season: games where either team has fewer than two prior games grade at 16.5 points of margin MAE with intervals that run too narrow. That is precisely the hole the preseason prior exists to shrink. Full season-by-season detail is on the Performance page.
The in-game model answers one question at every play boundary: given the score, the clock, who has the ball and where, and what was believed before kickoff, what is the probability the home team wins? The state for play N is built strictly from plays 1 through N−1 plus the pre-snap situation, and a prefix-stability check proves it: rebuilding any game from only its first N plays must reproduce every earlier state bit for bit, or the pipeline refuses to continue.
The functional form is a Gaussian over the final margin whose mean decays from the pregame expectation toward the live score as the clock runs out:
μ = home_margin + f · pregame_margin + possession_value
σ² = f · pregame_margin_sd² + 3.355²
P(home win) = Φ(μ / σ)
// f = fraction of regulation remaining; 0 in overtime
Three parameters, all fit by direct search on play-level log loss over the 2019–2022 development seasons: possession is worth 0.306 points at a team's own 25, field position adds 0.0565 points per yard (so first and goal at the 5 is worth about +4.3 points, and being backed up at your own 1 costs about a point), and a 3.35-point floor keeps the distribution from collapsing as time expires.
On the 2023–2025 holdout (400,878 play states across 2,258 games) the model scores 0.4073 log loss and 0.1328 Brier, and passes calibration checks in every quarter and every score-margin bucket. Calibration tolerances are computed on game counts rather than play counts, since every state within a game shares one outcome. There are deliberately no momentum or streak features in this baseline; it exists to be the reference any such idea has to beat.
If the closing line is the better pregame forecast, the in-game model should start from it. The anchor construction is deliberately plain: one closing spread per game, taken as the median across every priced provider, enters the win probability equation as pregame_margin = −closing_spread. The market prices a spread but not an uncertainty, so margin_sd is a single frozen constant, 15.445 points, the standard deviation of actual margins around the closing spread over 3,794 development-season games. Holdout sensitivity was flat near that value, so nothing fancier than a constant is warranted yet.
The frozen baseline was then rescored on identical play boundaries with only the anchor swapped. On the 2023–2025 holdout:
| Metric | Model anchor | Market anchor | Δ |
|---|---|---|---|
| Log loss | 0.40730 | 0.38954 | −0.01775 |
| Brier score | 0.13280 | 0.12576 | −0.00704 |
The improvement lives exactly where theory says it should. The anchor's weight decays with the clock, so the gain is largest early and vanishes late:
1st quarter
−0.0406
2nd quarter
−0.0231
3rd quarter
−0.0083
4th quarter
+0.0002
Overtime
0.0000
The verdict that came out of this experiment now steers the roadmap: the pregame anchor is the binding constraint on in-game accuracy, so effort belongs on the anchor, not on further in-game adjustments. At kickoff the served win probability is essentially the market's own line converted to a probability; by the fourth quarter it is almost entirely the scoreboard. When no market anchor exists for a game, the model's own projection fills in.
The serving path is built so that a live deployment cannot accidentally cheat. A served game reads exactly three things: a four-column anchor contract (game_id, model_week, home_margin, margin_sd), the three frozen model parameters, and the play feed. Reads are column-restricted so no outcome field can leak in, and verification against stored results is a separate step that only runs after serving is done.
Rather than maintain incremental state, the server rebuilds the entire game state from the play prefix after every single play. That sounds wasteful until you measure it: across all 759 games of the 2025 season replayed as a simulated live feed, 135,662 served events matched the stored batch predictions exactly, at a median of 8.9 ms per event with a p99 of 11.6 ms, against a 1-second live budget. Exact-match replay means the live path and the evaluated path are provably the same code producing the same numbers.
The project is named after momentum, so this section is a little painful to write. The hypothesis was that chronological process evidence carries signal the scoreboard has not absorbed yet: sustained stops, drive efficiency, turnovers, field position, fourth-down outcomes, missed kicks, success rate, tempo. Eight evidence families, tracked as home-minus-away totals, shrunk toward zero by a prior play count, shifting the expected final margin on top of the frozen baseline.
Version one used cumulative totals and improved the development seasons by 0.0030 of log loss. On holdout it was worse by 0.0004. Version two weighted recent plays more heavily with a 120-play half-life, and its shrinkage search ran to the top of the grid, which is an optimizer's polite way of saying it would rather not use the features at all. Holdout: worse by 0.0006.
The honest read is that whatever is real in a hot streak shows up on the scoreboard quickly, and the scoreboard is already in the model. Momentum work is paused until there is a structurally different formulation worth testing, and the baseline above stands as the number any future attempt has to beat on holdout, not on the development years it was tuned on.
- No injury or availability modeling. There is no reliable public availability feed, and guessing from play-by-play text is a good way to be confidently wrong. The missing input permanently widens every team's rating uncertainty instead.
- No picks, no sizing, no profitability claims. The model does flag +EV plays; pricing the market and surfacing disagreements is how it gets benchmarked against the strongest available forecast. It stops there. Nothing sizes a wager or sells a pick, the flags are a measurement tool rather than betting advice, and every row ships with recommendation_status set to not_recommended.
- No live production feed yet. Current data sources are batch. The serving path is proven fast enough for live use, but real-time projections wait on a paid low-latency tier.
- Early-season fragility. Ratings for teams with fewer than two graded games are the model's weakest output, and FCS teams with sparse data feeds carry the widest uncertainty and produce the largest model-market gaps.
- Postseason in-game serving is unmapped. Regular-season weeks map cleanly onto serving anchors; bowl season needs its own mapping and does not have one yet.
Modeling
Data
Database
Frontend
Orchestration