← Back to blog

Win Probability Models for Sports Betting: The Math, The Code, and The Mistakes

updated 2026-10-08 machine-learning tutorial python reference

By ZenHodl. Dataset documentation, research and model evaluations are linked in the article. A separate filtered ledger of bot-attributed trades, including losses and its admission rules, is public at /results.

Win probability (WP) models are the foundation of sports prediction. ESPN has one. FiveThirtyEight had one. Every sportsbook uses one internally. And if you're building a prediction market bot, you need one too.

This guide explains probability-model inputs, time-based validation, calibration metrics, and execution assumptions. The examples describe specific modeling choices, not a verified inventory of every current ZenHodl model.

What a WP Model Does

A WP model answers one question: given the current game state (score, time remaining, period, team strength), what's the probability the home team wins?

The input is a feature vector: - Score differential: home_score - away_score - Time remaining: seconds left in regulation - Period/quarter/inning: where in the game are we - Team strength: Elo ratings for both teams - Sport-specific features: possession, down/distance (football), shots on goal (hockey), pitcher stats (baseball)

The output is a probability between 0 and 1. A well-calibrated model that outputs 0.72 means the home team wins 72% of the time in similar game states.

Step 1: Elo Ratings

Every WP model needs a measure of team strength. Elo ratings, invented by Arpad Elo for chess, are the simplest approach that works.

The expected win probability for team A against team B:

E(A) = 1 / (1 + 10^((R_B - R_A) / 400))

After a game, ratings update:

R_A_new = R_A + K × (actual - expected)

Where K is the learning rate (how fast ratings respond to new results). We tune K per sport:

Sport K Home Advantage Why
NBA 20 100 points High-scoring, frequent games
NCAAMB 20 100 Same as NBA, wider skill gap
NFL 15 55 Few games, each matters more
NHL 8 40 Low-scoring, more randomness
MLB 8 24 162 games/season, high variance

Higher K means faster adaptation but more noise. Lower K means smoother ratings but slower to detect roster changes.

The margin-of-victory multiplier: For basketball and football, we scale K by the margin of victory. A 20-point blowout moves ratings more than a 1-point squeaker. This is disabled for hockey and baseball (low-scoring sports where margins are noise).

Season decay: Between seasons, we regress all ratings 50% toward the mean (1500). This prevents a team that was great last year from carrying an inflated rating into a rebuilding year.

Full implementation: Build an Elo Rating System from Scratch in Python

Step 2: Feature Engineering

Raw Elo difference plus score and time gets you 80% of the way. The remaining 20% comes from sport-specific features:

Basketball (NBA, NCAAMB): - score_diff: home - away score - time_fraction: fraction of game remaining (0 = game over, 1 = tip-off) - elo_diff: home Elo - away Elo - total_score: combined score (proxy for game pace) - score_diff_x_elo: interaction term (a 10-point lead by a strong team is more secure) - score_diff_x_tf: interaction (a 10-point lead with 2 minutes left vs 20 minutes) - pace_diff, ortg_diff, drtg_diff: team efficiency metrics

Football (NFL, CFB): - All of the above plus: - down, distance, yard_line: current play state - possession_home: 1 if home team has the ball

Hockey (NHL): - shots_on_goal_diff: SOG differential - power_play_state: 5v5, 5v4, 4v5, etc. - empty_net: whether the trailing team has pulled their goalie

Baseball (MLB): - is_home_batting: which team is batting (alternates by half-inning) - sp_era_diff, sp_whip_diff, sp_k9_diff: starting pitcher stats

Feature claims need an evaluated cohort. This article previously attributed a blanket rejection of rest days, travel, referees, weather and player statistics to ZenHodl testing without linking an ablation artifact. That claim is withdrawn. Compare feature sets on the same held-out rows and prediction times; an observed score difference does not establish its cause or profitability.

Step 3: Model Architecture

We use different model architectures per sport:

Logistic Regression + Spline Calibration: Best for high-scoring sports (basketball). Outputs well-calibrated probabilities by default. Spline post-processing smooths the calibration curve.

XGBoost + Isotonic Calibration: Best for low-scoring sports (hockey, baseball). Gradient boosting captures non-linear interactions (a 1-0 lead in the 3rd period of hockey is very different from a 1-0 lead in the 1st). Isotonic regression fixes the raw probability outputs.

Split-Phase Model: Separate models for early game (high uncertainty) and late game (low uncertainty), blended by time fraction. Works well for basketball where the dynamics change dramatically between the 1st quarter and final 2 minutes.

Ensemble: Weighted combination of all three, with weights determined by which model trades best on held-out data. The ensemble provides the most robust predictions across all game states.

The model choice matters less than calibration. Calibration alone does not rank logistic regression against XGBoost; compare proper scores and calibration on the same held-out sample. See Calibration Beats Accuracy for the full explanation.

Step 4: Training Protocol

The most common mistake in sports ML is data leakage — using future information to predict past outcomes. We prevent this with strict time-split validation:

Training set:   2020-21 through 2023-24 seasons
Calibration set: First half of 2024-25
Test set:       Second half of 2024-25 through 2025-26

Never random-split sports data. Random splits mix games from the same season, allowing the model to "learn" team strength from future games. Time-split ensures the model only ever sees data from before the prediction date.

Calibration happens on a separate set. Fitting a calibrator on training predictions can bias its evaluation; it does not guarantee perfect training calibration. The calibration set must be data the model has never seen during training.

Test on the most recent data. Your test set should be the most recent season because that's the closest approximation to the distribution you'll trade on. A model that works on 2021 data but fails on 2025 data is useless.

Step 5: Calibration

The step most people skip — and the one that determines whether you make money. We've written a full guide to measuring and fixing sports betting model calibration, with working Python code; the short version follows.

Expected Calibration Error (ECE): Bin predictions into 10 buckets (0-10%, 10-20%, ..., 90-100%). For each bucket, compare average predicted probability to actual win rate. The weighted average of the absolute differences is your ECE.

ECE depends on the evaluation population and binning. Report bin counts, the weighted-average gap and uncertainty; a fixed ECE threshold is neither a profit forecast nor a universal quality grade.

Brier Score: The mean squared error of probability predictions. Lower is better. Measures both accuracy and calibration simultaneously.

The original article reported the following evaluation figures without identifying a matched current production cohort. They are retained as historical reported values, not verified October runtime metrics: - NCAAMB: Brier 0.145, ECE 0.022 - NBA: Brier 0.127, ECE 0.057 - NHL: Brier 0.157, ECE 0.034 - MLB: Brier 0.152, ECE 0.011

Correction (2026-09-25): good ECE means our stated probabilities are honest, not that they beat the market. Measured on live fills, our bought-side calibration (realized win rate vs. the price we actually paid) is currently negative in every sport we have enough fills to measure, roughly -7pp to -31pp depending on sport (internal fill study, September 2026). Our CLV retraction and honest aggregate are at /clv-evidence. A well-calibrated model can still match the market exactly and earn nothing above it.

The 7 Mistakes That Lose Money

Mistake 1: Confusing inputs with evaluation independence. A market-derived input can make a comparison partly circular. Record the source of every feature and evaluate with an appropriate held-out target. A model_pre_blend label describes the output before blending; it does not prove that all upstream features are market independent.

Mistake 2: Random train/test splits. See above. Always time-split.

Mistake 3: Ignoring calibration. A 65% accurate model can lose money if it's not calibrated. When it says 75%, the team needs to actually win 75% of the time — not 62%.

Mistake 4: Unverified feature-count rules. Neither 50 nor 6–14 features establishes overfitting or generalization. Compare feature sets on time-separated data; record the metric, uncertainty and cost assumptions.

Mistake 5: Not accounting for execution costs. A 5-cent edge looks profitable. After 2-cent fees and 3-cent average slippage, it's negative. Backtest with realistic costs from day one.

Mistake 6: Backtesting with market prices you can't actually trade at. The ask price in your historical data might have had 8 contracts of depth. These quotes alone cannot tell you the price of a 100-contract fill. Inspect the available ladder and timing, and distinguish a hypothetical depth calculation from an executed order.

Mistake 7: Not dampening extreme predictions. A raw model output of 95% probably isn't 95% in reality. Dampening (shrinking toward 50%) prevents overconfident bets. We use output = 50 + (raw - 50) × 0.80 for most sports.

From Model to Money

A WP model is necessary but not sufficient. You also need:

The model is the foundation. Execution is what makes it profitable.


This methodology is taught step-by-step across 6 modules in our course. Module 1 covers data collection, Module 2 covers Elo ratings, Module 3 covers WP model training and calibration, Module 4 covers backtesting, Module 5 covers live execution, and Module 6 covers deployment. Live results from these exact models at zenhodl.net/results.

Related reading

Get ZenHodl Weekly

Dataset releases, research notes, and public results, including corrections.

Research and dataset updates from ZenHodl.

Want to build this yourself?

Work through ESPN data collection, Elo, probability modeling, calibration and backtesting in six Jupyter notebooks. Inspect the free first module and course requirements before buying.