Ridge Regression to Adjust Opponents' Statistics
Raw EPA per play mixes two things: how good a team is, and who it played. This post separates them with ridge regression, picks the ridge penalty by forecasting the next week's games, and compares how much the adjustment matters in the NFL versus college football.
The problem with raw efficiency
Every post on this site so far has used raw EPA per play: add up a team's expected points added and divide by plays. That number ignores the opponent. An offense that averaged +0.10 EPA per play against bad defenses and one that did it against good defenses get the same score. In the NFL, where schedules are fairly balanced, the error is small. In college football, where Ohio State and Akron play completely different schedules, it can be large.
The standard fix is to model every play's EPA as the sum of an offense effect, a defense effect, and home field, and then estimate all of those effects at once:
EPA(play) = μ + offense[team with the ball] + defense[team on defense] + home + noise
Fitting this with ordinary least squares gives a pure opponent adjustment: each team's offense rating is what's left of its EPA after accounting for the defenses it faced, and vice versa. The catch is noise. Early in a season a team has played three or four opponents, so a single blowout can swing both its rating and its opponents' ratings. Ridge regression adds a penalty, λ times the sum of the squared coefficients, which pulls every team toward average unless the data clearly says otherwise. It does two jobs at once: opponent adjustment and shrinkage.
Setting it up
Each play becomes one row of a sparse matrix: a 1 in the offense's column, a 1 in the defense's column, and a 1 in the home column if the offense is at home. With 32 teams that's 65 columns and about 30,000 rows per NFL season. I drop garbage time (plays where either team's win probability is below 5%) and keep only runs and dropbacks.
import scipy.sparse as sp
from sklearn.linear_model import Ridge
def design(plays, teams):
idx = {t: i for i, t in enumerate(teams)}
n, t = len(plays), len(teams)
rows = np.arange(n)
offense = sp.csr_matrix((np.ones(n), (rows, plays.posteam.map(idx))), shape=(n, t))
defense = sp.csr_matrix((np.ones(n), (rows, plays.defteam.map(idx))), shape=(n, t))
home = sp.csr_matrix(plays.posteam.eq(plays.home_team).astype(float).values.reshape(-1, 1))
return sp.hstack([offense, defense, home]).tocsr()
teams = sorted(plays.posteam.unique())
model = Ridge(alpha=1000).fit(design(plays, teams), plays.epa)
offense_rating = pd.Series(model.coef_[:32], teams) # EPA/play added vs. an average offense
defense_rating = pd.Series(model.coef_[32:64], teams) # EPA/play allowed vs. an average defense
net_rating = offense_rating - defense_rating
What λ does
Figure 1 follows four 2024 NFL offenses as λ grows. At the far left (λ = 1) the penalty is negligible and the model is a pure opponent adjustment. The Lions' offense moves up from +0.138 raw to +0.165 because it faced tougher defenses than average, while the Eagles' moves down from +0.090 to +0.057 because theirs were easier. The Panthers barely move. As λ grows, every rating is pulled toward zero. By λ = 1000 the Lions are at +0.071 and the Browns, the worst offense in the league, go from −0.227 to −0.109.
Shrinking toward zero isn't a bug. Over a short sample, a team's true quality is closer to average than its results, and a good estimate should say so. The question is how much to shrink, and the answer has to come from data.
Choosing λ by forecasting the next week
The test I used is the one a rating system should pass: does it predict games it hasn't seen? For every week from 4 to 18 of the 2021–2024 NFL seasons, I fit the ratings on that season's earlier weeks only, then compared each game's rating gap (home net rating minus away net rating) with the actual final margin. That's 895 games, scored with each λ and with raw EPA.
for season in range(2021, 2025):
for week in range(4, 19):
train = plays[(plays.season == season) & (plays.week < week)]
test = games[(games.season == season) & (games.week == week)]
ratings = {lam: fit_net_rating(train, lam) for lam in LAMBDAS}
ratings["raw"] = train.groupby("posteam").epa.mean() - train.groupby("defteam").epa.mean()
for g in test.itertuples():
rows.append({"margin": g.result,
**{k: r[g.home_team] - r[g.away_team] for k, r in ratings.items()}})
results = pd.DataFrame(rows)
{k: results[k].corr(results.margin) for k in ["raw", *LAMBDAS]}
Three things stand out:
- Pure opponent adjustment is worse than raw EPA. With almost no penalty, the correlation drops to 0.284 from raw EPA's 0.330. With a few weeks of data, the adjustment chases noise.
- Enough shrinkage beats raw, but only a little. The best penalty, λ = 1000, reaches 0.348. A paired bootstrap over games puts the gain at −0.004 to +0.040, so in the NFL it's not clearly better. Ridge picks the winner in 61.5% of games and raw EPA in 61.2%.
- Vegas is far ahead. The closing spread correlates with final margins at 0.466. It includes injuries, weather, quarterback changes, and prior-season information, none of which an in-season EPA model sees.
College football, where schedules really differ
I ran the same model on every play between two FBS teams in the 2024 regular season: 134 teams and about 73,000 plays. The difference from the NFL is immediate. Raw and adjusted net ratings correlate at only 0.82, against 0.98 for the 2024 NFL, and some teams move as many as 71 places in the rankings.
The pattern is almost entirely about conference. Power Four teams rise an average of 19 spots after adjustment, and Group of Five teams fall by about the same amount. The biggest risers were Power Four teams that played strong conference schedules: Mississippi State (+65 places), Kentucky (+55), Florida (+49), Oklahoma (+45), and Oklahoma State (+44). The biggest fallers padded their numbers against weak opponents: Liberty (−71), Jacksonville State (−63), James Madison (−54), South Alabama (−53), and Sam Houston (−51).
The college model also finds real home-field advantage: +0.032 EPA per play for the home offense. The 2024 NFL had essentially none (−0.023, slightly in favor of road offenses).
As an outside check, these ridge ratings correlate at 0.950 with the opponent-adjusted ratings published in the sportsdataverse dataset. Raw EPA correlates with them at only 0.754. Two independent adjustment methods landing in the same place is a good sign that both are measuring the same thing.
Forecasting college games
The same week-by-week backtest on the 2023 and 2024 college seasons (1,166 FBS-vs-FBS games, neutral sites excluded) shows the payoff clearly:
Ridge with λ = 100 reaches a correlation of 0.535, against 0.502 for raw EPA. The bootstrap interval for that gain is +0.013 to +0.052, so unlike in the NFL, this improvement is real. The best penalty is also ten times smaller than in the NFL. College teams differ from each other far more than NFL teams do, so the data overwhelms the pull toward average sooner, and less shrinkage is needed. One limit: picking winners barely changes (67.1% for raw, 67.4% for ridge), because most college games aren't close enough for the adjustment to flip the favorite.
Clemson in 2024
Clemson's 2024 team, which won the ACC and made the College Football Playoff, looks better after adjustment: 10th by raw net EPA and 7th adjusted. Its schedule was close to average (58th hardest of 134), but both units faced better-than-average opponents. The offense goes from +0.102 EPA per play raw to +0.173 adjusted, and the defense from −0.138 allowed to −0.181. On both sides of the ball, that team was better than its raw numbers suggested.
Caveats
- Every play gets equal weight. A version that down-weights older games, or adds last season's rating as a prior, would likely forecast better early in the season.
- The model is additive. It can't capture matchups, such as a strong passing offense against a weak pass defense, unless separate pass and run effects are added.
- One λ is used for every team. A team with fewer plays gets shrunk relatively more, which is usually what you want, but the penalty isn't tuned by position or unit.
- College ratings use FBS-vs-FBS plays only, so games against FCS opponents don't count toward anyone's rating.
Data and code
NFL play-by-play and schedules from nflverse (2021–2024 regular seasons). College
play-by-play, schedules, team info, and ratings from the sportsdataverse
releases (2023–2024). Every number and chart comes from
football-analysis/ridge_adjustment.py.