Logistic regression converts team features into win probabilities: log(p/(1-p)) = β₀ + β₁X₁ + … + βₙXₙ. Use Ridge or Elastic Net when features are correlated. Compare model output against closing lines — if your model consistently beats the close, you have edge.
Why This Matters for Agents
Regression is the workhorse of Layer 4 — Intelligence. Every autonomous betting agent needs a function that takes observable features (team ratings, recent form, injuries, weather) and outputs a probability. Regression models are that function.
An agent’s decision pipeline works like this: pull odds from The Odds API and sportsbook feeds, compute features from historical data, run those features through a regression model to get a probability estimate, compare that estimate against the market’s implied probability, and — if edge exists — size the bet via Kelly criterion. The regression model is the core of the intelligence layer. Without it, the agent has no opinion on what anything is worth. Elo ratings and power rankings generate the features; regression turns those features into money.
The Math
Linear Regression for Point Spreads
Linear regression models a continuous outcome as a weighted sum of input features:
Y = β₀ + β₁X₁ + β₂X₂ + ... + βₙXₙ + ε
where Y is the outcome variable (e.g., point differential), X₁…Xₙ are predictor features, β₀ is the intercept, β₁…βₙ are coefficients, and ε is the error term (assumed normally distributed with mean 0 and variance σ²).
For NFL point spread modeling:
PointDiff = β₀ + β₁(HomeOffRating) + β₂(AwayDefRating) + β₃(HomeAdv) + β₄(RestDays) + ε
OLS (Ordinary Least Squares) estimates β by minimizing the sum of squared residuals:
β̂ = argmin_β Σᵢ (yᵢ - Xᵢβ)²
The closed-form solution:
β̂ = (XᵀX)⁻¹Xᵀy
R² (coefficient of determination) measures the fraction of variance explained:
R² = 1 - (SS_res / SS_tot)
= 1 - Σ(yᵢ - ŷᵢ)² / Σ(yᵢ - ȳ)²
In sports modeling, R² values of 0.15-0.30 are typical for game-level predictions. That sounds low, but sports outcomes have massive irreducible variance. An R² of 0.25 means you explain 25% of point differential variance — the other 75% is luck, injuries, and chaos. That 25% is where all the edge lives.
Multicollinearity: The Silent Killer
Sports features are correlated. Offensive yards correlate with points scored. Turnover differential correlates with win rate. When features are highly correlated, OLS coefficient estimates become unstable — small changes in training data cause large swings in β̂.
Detect multicollinearity with the Variance Inflation Factor (VIF):
VIF_j = 1 / (1 - R²_j)
where R²_j is the R² from regressing feature j on all other features. Rules of thumb: VIF > 5 is concerning, VIF > 10 is a serious problem. The fix is regularization (covered below) or feature elimination.
Logistic Regression for Win Probability
Binary outcomes (win/loss) violate linear regression assumptions — the output must be bounded between 0 and 1. Logistic regression solves this by modeling the log-odds (logit) as a linear function:
log(p / (1 - p)) = β₀ + β₁X₁ + β₂X₂ + ... + βₙXₙ
where p is the probability of the outcome (e.g., home team wins), and p/(1-p) is the odds ratio.
Solving for p:
p = 1 / (1 + exp(-(β₀ + β₁X₁ + ... + βₙXₙ)))
This is the sigmoid function. It maps any real-valued input to [0, 1].
Sigmoid Output vs Linear Predictor:
p
1.0 ┤ ╭────────
│ ╭──╯
0.8 ┤ ╭──╯
│ ╭──╯
0.6 ┤ ╭──╯
│ ╭──╯
0.4 ┤ ╭──╯
│ ╭──╯
0.2 ┤ ╭──╯
│ ╭──╯
0.0 ┤─────────╯
└──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──→ z = Xβ
-6 -5 -4 -3 -2 -1 0 1 2 3 4 5
Logistic regression estimates β via maximum likelihood estimation (MLE), not OLS. The log-likelihood function:
ℓ(β) = Σᵢ [yᵢ log(pᵢ) + (1 - yᵢ) log(1 - pᵢ)]
where yᵢ ∈ {0, 1} and pᵢ = sigmoid(Xᵢβ). No closed-form solution exists — optimization uses iteratively reweighted least squares (IRLS) or gradient descent.
Coefficient interpretation: In logistic regression, exp(βⱼ) is the multiplicative change in odds for a one-unit increase in Xⱼ. If β₁ = 0.35 for home advantage, then exp(0.35) = 1.42 — home teams have 1.42x the odds of winning, all else equal.
Regularized Regression: Ridge, Lasso, Elastic Net
When you have 50+ features with correlations among them, unregularized regression overfits. Regularization adds a penalty term to the loss function that shrinks coefficients toward zero.
Ridge Regression (L2 penalty):
Loss = Σ(yᵢ - Xᵢβ)² + λ Σⱼ βⱼ²
Ridge shrinks all coefficients proportionally. It never zeroes them out. Use Ridge when you believe all features have some predictive value but want to control variance.
Lasso Regression (L1 penalty):
Loss = Σ(yᵢ - Xᵢβ)² + λ Σⱼ |βⱼ|
Lasso drives some coefficients exactly to zero, performing automatic feature selection. Use Lasso when you suspect many features are irrelevant.
Elastic Net (combined L1 + L2):
Loss = Σ(yᵢ - Xᵢβ)² + λ [α Σⱼ |βⱼ| + (1 - α)/2 Σⱼ βⱼ²]
where α ∈ [0, 1] controls the L1/L2 mix. Elastic Net handles groups of correlated features better than Lasso alone. For sports models, Elastic Net with α = 0.5 is a strong default.
Choosing λ (regularization strength): Use k-fold cross-validation. Split training data into k folds (k = 5 is standard), train on k-1 folds, evaluate on the held-out fold, average the error across folds, and select λ that minimizes mean cross-validated error.
| Method | Penalty | Feature Selection | Best For |
|---|---|---|---|
| OLS | None | No | Small feature sets, no collinearity |
| Ridge | L2 (β²) | No (shrinks all) | Correlated features, all relevant |
| Lasso | L1 (|β|) | Yes (zeros some) | Many irrelevant features |
| Elastic Net | L1 + L2 | Yes (partial) | Correlated groups + irrelevant features |
Poisson Regression for Score Modeling
Goals in soccer, runs in baseball, and touchdowns in football are count data. Linear regression is wrong for counts — it can predict negative values. Poisson regression models count outcomes directly:
log(μ) = β₀ + β₁X₁ + β₂X₂ + ... + βₙXₙ
where μ = E[Y] is the expected count (e.g., expected goals). The Poisson distribution:
P(Y = k) = (μᵏ × e⁻ᵘ) / k!
Each coefficient βⱼ in Poisson regression has the interpretation: exp(βⱼ) is the multiplicative change in the expected count for a one-unit increase in Xⱼ.
When Poisson fails: The Poisson distribution assumes mean equals variance (E[Y] = Var[Y]). In real sports data, variance often exceeds the mean — this is overdispersion. Use the negative binomial model when the overdispersion test is significant. The Poisson Distribution guide covers the Poisson model in depth, including the Dixon-Coles adjustment for soccer.
Worked Examples
NFL Win Probability: Logistic Regression from Scratch
We build a logistic regression model for NFL game outcomes using team efficiency metrics. The features: home team offensive DVOA, away team defensive DVOA, home advantage indicator, and rest differential (days since last game, home minus away).
Suppose the 2025 NFL Week 12 matchup is Kansas City Chiefs at home vs. Buffalo Bills. From Football Outsiders:
Feature values (hypothetical but realistic):
KC Offensive DVOA: +18.2%
BUF Defensive DVOA: -12.4% (negative is good for defense)
Home advantage: 1
Rest differential: 0 (both on standard rest)
With fitted coefficients from a 5-season training set:
β₀ (intercept) = -0.12
β₁ (home_off_dvoa) = +0.043
β₂ (away_def_dvoa) = -0.038
β₃ (home_adv) = +0.35
β₄ (rest_diff) = +0.15
z = -0.12 + 0.043(18.2) + (-0.038)(-12.4) + 0.35(1) + 0.15(0)
z = -0.12 + 0.783 + 0.471 + 0.35 + 0
z = 1.484
p(KC wins) = 1 / (1 + exp(-1.484))
p(KC wins) = 1 / (1 + 0.227)
p(KC wins) = 0.815 = 81.5%
Now check the market. BetOnline has Chiefs -7.5 at -110, which implies roughly a 75% win probability (from the point spread, using the historical relationship between spread and moneyline). The model says 81.5% vs. the market’s 75%. That’s a 6.5-percentage-point edge — large enough to be actionable.
Expected CLV: if the line closes at -8.5 (corresponding to ~78% win probability), the model’s 81.5% estimate was closer to the closing line than the opening line. Consistent closing line value is the hallmark of a sharp model.
Point Spread Prediction: Ridge Regression
For a Bovada point spread on Lakers vs. Celtics (NBA):
Features (standardized, zero mean, unit variance):
Home offensive rating: +1.2σ
Away defensive rating: -0.8σ
Pace differential: +0.5σ
Back-to-back flag: 0 (neither team)
Rest advantage: +0.3σ
Ridge coefficients (λ = 1.0, fitted on 3 NBA seasons):
β₀ = +2.1 (home court baseline)
β₁ = +3.8 (off rating)
β₂ = -2.9 (def rating, negative means good defense hurts opponent)
β₃ = +1.2 (pace)
β₄ = -4.1 (back-to-back penalty)
β₅ = +1.5 (rest)
Predicted spread = 2.1 + 3.8(1.2) + (-2.9)(-0.8) + 1.2(0.5) + (-4.1)(0) + 1.5(0.3)
= 2.1 + 4.56 + 2.32 + 0.6 + 0 + 0.45
= 10.03
Model says: Home team (Lakers) by ~10 points.
Bovada line: Lakers -7.5 at -110
Edge: Model sees 2.5 points of value on Lakers -7.5.
The sharp betting approach is to bet when your model’s prediction diverges significantly from the market.
Implementation
"""
Regression models for sports betting agents.
Covers: OLS, Logistic, Ridge, Poisson regression with
full training, evaluation, and prediction pipelines.
"""
import numpy as np
import pandas as pd
from sklearn.linear_model import (
LinearRegression,
LogisticRegressionCV,
Ridge,
Lasso,
ElasticNet,
PoissonRegressor,
)
from sklearn.model_selection import cross_val_score, TimeSeriesSplit
from sklearn.metrics import (
brier_score_loss,
log_loss,
mean_squared_error,
r2_score,
)
from sklearn.preprocessing import StandardScaler
from dataclasses import dataclass
from typing import Optional
@dataclass
class RegressionPrediction:
"""Output from a sports regression model."""
predicted_value: float # point spread, win prob, or expected goals
model_type: str # "ols", "logistic", "ridge", "poisson"
confidence_interval: tuple[float, float]
features_used: list[str]
r2_or_accuracy: float
def compute_vif(X: pd.DataFrame) -> pd.DataFrame:
"""
Compute Variance Inflation Factor for each feature.
VIF > 5 indicates concerning multicollinearity.
VIF > 10 indicates severe multicollinearity.
"""
from sklearn.linear_model import LinearRegression
vif_data = []
for i, col in enumerate(X.columns):
y_vif = X[col].values
X_vif = X.drop(columns=[col]).values
reg = LinearRegression().fit(X_vif, y_vif)
r2 = reg.score(X_vif, y_vif)
vif = 1.0 / (1.0 - r2) if r2 < 1.0 else np.inf
vif_data.append({"feature": col, "VIF": round(vif, 2)})
return pd.DataFrame(vif_data).sort_values("VIF", ascending=False)
def build_nfl_logistic_model(
df: pd.DataFrame,
features: list[str],
target: str = "home_win",
regularization: str = "elasticnet",
n_splits: int = 5,
) -> dict:
"""
Train a logistic regression model for NFL win probability.
Args:
df: DataFrame with game-level features and outcomes.
Must have columns matching `features` plus `target`.
features: List of feature column names.
target: Binary target column (1 = home win, 0 = away win).
regularization: One of 'l1', 'l2', 'elasticnet'.
n_splits: Number of cross-validation folds.
Returns:
Dict with model, scaler, metrics, and coefficients.
"""
X = df[features].copy()
y = df[target].values
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
# TimeSeriesSplit respects temporal ordering — critical for sports
tscv = TimeSeriesSplit(n_splits=n_splits)
if regularization == "elasticnet":
solver = "saga"
penalty = "elasticnet"
l1_ratios = [0.1, 0.3, 0.5, 0.7, 0.9]
elif regularization == "l1":
solver = "saga"
penalty = "l1"
l1_ratios = None
else:
solver = "lbfgs"
penalty = "l2"
l1_ratios = None
model = LogisticRegressionCV(
penalty=penalty,
solver=solver,
cv=tscv,
Cs=20,
l1_ratios=l1_ratios,
max_iter=5000,
scoring="neg_log_loss",
random_state=42,
)
model.fit(X_scaled, y)
# Cross-validated metrics
cv_logloss = -cross_val_score(
model, X_scaled, y, cv=tscv, scoring="neg_log_loss"
)
cv_brier = -cross_val_score(
model, X_scaled, y, cv=tscv, scoring="neg_brier_score"
)
cv_accuracy = cross_val_score(
model, X_scaled, y, cv=tscv, scoring="accuracy"
)
# Coefficient table
coef_df = pd.DataFrame({
"feature": features,
"coefficient": model.coef_[0],
"odds_ratio": np.exp(model.coef_[0]),
}).sort_values("coefficient", ascending=False)
return {
"model": model,
"scaler": scaler,
"best_C": model.C_[0],
"coefficients": coef_df,
"cv_logloss_mean": cv_logloss.mean(),
"cv_logloss_std": cv_logloss.std(),
"cv_brier_mean": cv_brier.mean(),
"cv_accuracy_mean": cv_accuracy.mean(),
}
def predict_win_probability(
model_dict: dict,
game_features: dict[str, float],
) -> RegressionPrediction:
"""
Predict win probability for a single game.
Args:
model_dict: Output from build_nfl_logistic_model.
game_features: Dict of feature_name -> value for one game.
Returns:
RegressionPrediction with win probability and metadata.
"""
model = model_dict["model"]
scaler = model_dict["scaler"]
features = model_dict["coefficients"]["feature"].tolist()
X = np.array([[game_features[f] for f in features]])
X_scaled = scaler.transform(X)
prob = model.predict_proba(X_scaled)[0, 1]
# Bootstrap-style confidence interval approximation
# using coefficient standard errors
z = model.decision_function(X_scaled)[0]
se_approx = 0.15 # typical SE for NFL logistic models
ci_low = 1 / (1 + np.exp(-(z - 1.96 * se_approx)))
ci_high = 1 / (1 + np.exp(-(z + 1.96 * se_approx)))
return RegressionPrediction(
predicted_value=prob,
model_type="logistic",
confidence_interval=(round(ci_low, 4), round(ci_high, 4)),
features_used=features,
r2_or_accuracy=model_dict["cv_accuracy_mean"],
)
def build_ridge_spread_model(
df: pd.DataFrame,
features: list[str],
target: str = "point_diff",
alphas: Optional[list[float]] = None,
) -> dict:
"""
Train a Ridge regression model for point spread prediction.
Args:
df: DataFrame with game features and point differential.
features: Feature column names.
target: Continuous target (home points - away points).
alphas: List of regularization strengths to try.
Returns:
Dict with model, scaler, and evaluation metrics.
"""
from sklearn.linear_model import RidgeCV
if alphas is None:
alphas = np.logspace(-2, 4, 50)
X = df[features].copy()
y = df[target].values
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
tscv = TimeSeriesSplit(n_splits=5)
model = RidgeCV(alphas=alphas, cv=tscv, scoring="neg_mean_squared_error")
model.fit(X_scaled, y)
y_pred = model.predict(X_scaled)
r2 = r2_score(y, y_pred)
rmse = np.sqrt(mean_squared_error(y, y_pred))
cv_rmse = np.sqrt(-cross_val_score(
model, X_scaled, y, cv=tscv, scoring="neg_mean_squared_error"
))
coef_df = pd.DataFrame({
"feature": features,
"coefficient": model.coef_,
}).sort_values("coefficient", ascending=False)
return {
"model": model,
"scaler": scaler,
"best_alpha": model.alpha_,
"r2": r2,
"rmse": rmse,
"cv_rmse_mean": cv_rmse.mean(),
"cv_rmse_std": cv_rmse.std(),
"coefficients": coef_df,
}
def build_poisson_model(
df: pd.DataFrame,
features: list[str],
target: str = "goals_scored",
) -> dict:
"""
Train a Poisson regression model for count outcomes
(goals, runs, touchdowns).
Args:
df: DataFrame with features and count target.
features: Feature column names.
target: Count target column.
Returns:
Dict with model, scaler, and metrics.
"""
X = df[features].copy()
y = df[target].values
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
model = PoissonRegressor(alpha=1.0, max_iter=1000)
model.fit(X_scaled, y)
y_pred = model.predict(X_scaled)
# Overdispersion test: compare variance to mean
residuals = y - y_pred
pearson_chi2 = np.sum(residuals**2 / y_pred)
dispersion = pearson_chi2 / (len(y) - len(features) - 1)
coef_df = pd.DataFrame({
"feature": features,
"coefficient": model.coef_,
"rate_ratio": np.exp(model.coef_),
}).sort_values("coefficient", ascending=False)
return {
"model": model,
"scaler": scaler,
"coefficients": coef_df,
"dispersion": dispersion,
"overdispersed": dispersion > 1.5,
"mean_predicted": y_pred.mean(),
"mean_actual": y.mean(),
}
def calibration_table(
y_true: np.ndarray,
y_prob: np.ndarray,
n_bins: int = 10,
) -> pd.DataFrame:
"""
Build a reliability diagram table for model calibration assessment.
A well-calibrated model has observed frequency ≈ predicted probability
in each bin. Use this to diagnose systematic over/under-confidence.
Args:
y_true: Binary outcomes (0 or 1).
y_prob: Predicted probabilities.
n_bins: Number of bins.
Returns:
DataFrame with bin_center, mean_predicted, observed_frequency, count.
"""
bins = np.linspace(0, 1, n_bins + 1)
table = []
for i in range(n_bins):
mask = (y_prob >= bins[i]) & (y_prob < bins[i + 1])
if mask.sum() == 0:
continue
table.append({
"bin_center": (bins[i] + bins[i + 1]) / 2,
"mean_predicted": y_prob[mask].mean(),
"observed_frequency": y_true[mask].mean(),
"count": mask.sum(),
})
return pd.DataFrame(table)
def calculate_clv(
model_prob: float,
opening_implied: float,
closing_implied: float,
) -> dict:
"""
Calculate Closing Line Value.
CLV measures whether your model's edge was real by comparing
against the closing line (the sharpest estimate of true probability).
Args:
model_prob: Your model's predicted probability.
opening_implied: Sportsbook opening implied probability.
closing_implied: Sportsbook closing implied probability.
Returns:
Dict with CLV metrics.
"""
# Model edge vs opening
edge_vs_open = model_prob - opening_implied
# Model edge vs closing
edge_vs_close = model_prob - closing_implied
# Did model beat the closing line?
beat_close = model_prob > closing_implied
# CLV in percentage points
clv = closing_implied - opening_implied
return {
"model_prob": model_prob,
"opening_implied": opening_implied,
"closing_implied": closing_implied,
"edge_vs_open": round(edge_vs_open, 4),
"edge_vs_close": round(edge_vs_close, 4),
"clv_points": round(clv, 4),
"beat_closing_line": beat_close,
}
# ------------------------------------------------------------------
# Example: End-to-end NFL model pipeline
# ------------------------------------------------------------------
if __name__ == "__main__":
np.random.seed(42)
# Simulate 500 NFL games with realistic feature distributions
n_games = 500
df = pd.DataFrame({
"home_off_dvoa": np.random.normal(0, 15, n_games),
"away_off_dvoa": np.random.normal(0, 15, n_games),
"home_def_dvoa": np.random.normal(0, 12, n_games),
"away_def_dvoa": np.random.normal(0, 12, n_games),
"home_adv": np.ones(n_games),
"rest_diff": np.random.choice([-3, 0, 3, 7], n_games, p=[0.1, 0.6, 0.2, 0.1]),
})
# Generate outcomes from a known logistic model
true_z = (
0.35 * df["home_adv"]
+ 0.04 * df["home_off_dvoa"]
- 0.035 * df["away_off_dvoa"]
- 0.03 * df["home_def_dvoa"]
+ 0.03 * df["away_def_dvoa"]
+ 0.08 * df["rest_diff"]
)
true_prob = 1 / (1 + np.exp(-true_z))
df["home_win"] = (np.random.rand(n_games) < true_prob).astype(int)
# Point differential (correlated with win outcome)
df["point_diff"] = (
true_z * 7
+ np.random.normal(0, 10, n_games)
).round(0)
features = [
"home_off_dvoa", "away_off_dvoa",
"home_def_dvoa", "away_def_dvoa",
"home_adv", "rest_diff",
]
# 1. Check multicollinearity
print("=== VIF Analysis ===")
vif = compute_vif(df[features])
print(vif.to_string(index=False))
# 2. Train logistic model
print("\n=== Logistic Regression (Elastic Net) ===")
result = build_nfl_logistic_model(df, features, regularization="elasticnet")
print(f"Best C (inverse regularization): {result['best_C']:.4f}")
print(f"CV Log-Loss: {result['cv_logloss_mean']:.4f} +/- {result['cv_logloss_std']:.4f}")
print(f"CV Brier: {result['cv_brier_mean']:.4f}")
print(f"CV Accuracy: {result['cv_accuracy_mean']:.1%}")
print("\nCoefficients:")
print(result["coefficients"].to_string(index=False))
# 3. Predict a specific game
game = {
"home_off_dvoa": 18.2,
"away_off_dvoa": 12.5,
"home_def_dvoa": -8.3,
"away_def_dvoa": -12.4,
"home_adv": 1.0,
"rest_diff": 0.0,
}
pred = predict_win_probability(result, game)
print(f"\nPredicted win prob: {pred.predicted_value:.1%}")
print(f"95% CI: ({pred.confidence_interval[0]:.1%}, {pred.confidence_interval[1]:.1%})")
# 4. Calibration check
print("\n=== Calibration Table ===")
model = result["model"]
scaler = result["scaler"]
X_scaled = scaler.transform(df[features])
probs = model.predict_proba(X_scaled)[:, 1]
cal = calibration_table(df["home_win"].values, probs)
print(cal.to_string(index=False))
# 5. CLV calculation
print("\n=== CLV Example ===")
clv = calculate_clv(
model_prob=0.815,
opening_implied=0.75,
closing_implied=0.78,
)
for k, v in clv.items():
print(f" {k}: {v}")
# 6. Ridge spread model
print("\n=== Ridge Regression (Point Spread) ===")
spread_result = build_ridge_spread_model(df, features, target="point_diff")
print(f"Best alpha: {spread_result['best_alpha']:.2f}")
print(f"R²: {spread_result['r2']:.3f}")
print(f"RMSE: {spread_result['rmse']:.1f}")
print(f"CV RMSE: {spread_result['cv_rmse_mean']:.1f} +/- {spread_result['cv_rmse_std']:.1f}")
Limitations and Edge Cases
Small sample sizes. NFL seasons have 272 regular-season games. With 6 features, that’s roughly 45 observations per parameter — borderline for stable logistic regression. NBA (1,230 games) and MLB (2,430 games) provide more data, but even then, regime changes (rule changes, new shot clock rules) can invalidate historical coefficients. Regularization helps, but it does not fix fundamentally insufficient data.
Non-stationarity. Team quality changes throughout a season. A model trained on early-season data may not reflect late-season performance. Solutions: use rolling windows (train on the last N games only), exponentially weight recent observations, or build in-season Elo features that capture form changes. The Elo ratings guide covers dynamic rating systems that address this.
Feature engineering matters more than model complexity. A logistic regression with well-engineered features (opponent-adjusted efficiency, rest advantage, travel distance) beats a random forest with raw box score stats. The Feature Engineering guide covers this in detail.
Calibration drift. A model calibrated on 2020-2024 data may be miscalibrated on 2025 data if the game has changed (three-point revolution in the NBA, increased passing in the NFL). Recalibrate quarterly using Platt scaling or isotonic regression.
The closing line is the benchmark, not the outcome. A model that beats the closing line by 2% is profitable even if individual game predictions look wrong half the time. Evaluate against closing line value, not against outcomes. Outcomes have variance; CLV does not.
Poisson overdispersion. Real goal/run distributions often have more variance than Poisson predicts. If the dispersion statistic exceeds 1.5, switch to negative binomial. Ignoring overdispersion produces artificially narrow confidence intervals, which leads to oversized bets.
FAQ
What is the best regression model for predicting sports outcomes?
Logistic regression is the standard starting point for binary outcomes (win/loss). It outputs calibrated probabilities directly, handles mixed feature types, and resists overfitting with regularization. For point spread prediction, use OLS linear regression. For score/goal totals, use Poisson regression. Ridge or Elastic Net variants are preferred when you have many correlated features.
How do you use logistic regression for sports betting?
Fit a logistic regression model on historical game data with features like offensive rating, defensive rating, and home advantage. The model outputs win probabilities via the sigmoid function. Compare those probabilities against implied odds from sportsbooks — if your model says 62% and the market implies 55%, you have a 7-percentage-point edge. Size the bet using the Kelly Criterion.
Why is regularization important for sports prediction models?
Sports datasets have many correlated features (offensive yards correlate with points scored, for example). Without regularization, OLS overfits to noise in the training data and produces unstable coefficients. Ridge regression (L2 penalty) shrinks correlated coefficients toward zero, reducing variance. Lasso (L1) performs feature selection by zeroing out irrelevant predictors entirely.
How do you evaluate if a sports betting model is well-calibrated?
Use a reliability diagram: bin predictions into deciles (50-55%, 55-60%, etc.) and plot predicted probability vs. actual win rate. A calibrated model falls on the diagonal. Quantify miscalibration with the Hosmer-Lemeshow test or Brier score. Then validate against closing lines — a model that consistently beats the closing line has real predictive edge.
How does regression connect to Elo ratings and power rankings for agents?
Elo ratings and power rankings are features that feed into regression models. An agent computes Elo ratings for each team, then uses those ratings as input variables in a logistic regression to predict win probability. The Elo Ratings guide covers how to build the rating system; this guide covers how to turn those ratings into actionable probabilities and bet decisions.
What’s Next
This guide covers the regression toolkit for turning features into probabilities. The natural next steps:
- NFL-specific modeling: The NFL Mathematical Modeling guide applies these regression techniques to point spreads, totals, and player props with NFL-specific feature engineering.
- Feature engineering: Regression models are only as good as their inputs. The Feature Engineering guide covers how to build the features that make regression models profitable.
- Where to get the data: The Agent Betting Stack maps the full pipeline from data ingestion through model deployment.
- Evaluating your edge: Closing Line Value is how you know your regression model is working — compare model output against closing lines from BetOnline and Bookmaker.
- Sizing the bets: Once you have a probability from regression, feed it to the Kelly Criterion to determine optimal stake.
