Machine learning sports betting means training a model on past games to estimate the probability of an outcome, then comparing that probability with the price on offer. The method is ordinary supervised learning. What makes it hard is the data: every feature has to be something you knew before the game, validation has to run in time order, and the target to beat is the no-vig market price, which is usually the single strongest predictor you have.
If your model can't beat the market price on games it hasn't seen, it's not ready to bet with, however good its accuracy looks. The rest of this guide is about building it so that test is honest.
Framing the problem
Pick a target that has one clear answer per game:
- Classification. Home team wins, 1 or 0. Over goes over, 1 or 0. The model outputs a probability.
- Regression. Final margin, or total points. You then turn the predicted distribution into a probability for a given spread or total.
Classification is simpler to start with and easier to score. Regression carries more information per game, but you need a sensible error distribution around the prediction to price a line, and a wrong assumption there will mislead you.
Each row is one game, as of one decision time. That decision time is the most important column in your dataset, even though it never goes into the model.
Features, and the one that matters most
| Feature group | Examples | Leakage risk |
|---|---|---|
| Ratings | Elo or similar, updated only on earlier games | Low, if updated in order |
| Recent form | Rolling efficiency over the last N games | Medium: windows must end before the game |
| Schedule | Rest days, travel, back-to-backs | Low |
| Personnel | Starting lineup, confirmed injuries | High: news times vary |
| Market | No-vig opening or decision-time price | Low, if timestamped |
The market feature is the one most people leave out, and it's the strongest. A sharp book's no-vig price at your decision time already includes most of what the other features know. Feed it in as the log-odds of the no-vig probability, so a linear model can use it directly.
Then measure how much your other features add on top. If the answer is "almost nothing", that's useful to know, and common.
Labels and what counts as a result
Write down the settlement rule before you build labels. Does a moneyline include overtime? What happens to a postponed game? Does a pushed spread count as a loss, a void or a half? Your labels must settle the way the market you'll bet on settles, or the model learns a different question from the one you're pricing.
Validation by time, never at random
A random train and test split lets the model learn from games played after the ones it's tested on. In sports, that leaks the future: team strength changes over a season, and the model will look far better than it is.
Use walk-forward validation instead. Train on everything before a date, test on the block after it, then move the date forward and repeat.
import numpy as np
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import log_loss
def walk_forward(df, features, cutoffs):
"""df sorted by decision_time, with a 'y' column (0 or 1)
and a 'market_p' column (no-vig probability at decision time)."""
rows = []
for start, end in zip(cutoffs[:-1], cutoffs[1:]):
train = df[df.decision_time < start]
test = df[(df.decision_time >= start) & (df.decision_time < end)]
if len(train) == 0 or len(test) == 0:
continue
model = LogisticRegression(C=1.0, max_iter=1000)
model.fit(train[features], train.y)
p = model.predict_proba(test[features])[:, 1]
rows.append({
"block": str(start),
"model": log_loss(test.y, p, labels=[0, 1]),
"market": log_loss(test.y, test.market_p, labels=[0, 1]),
})
return rows
df["market_logit"] = np.log(df.market_p / (1 - df.market_p))
features = ["market_logit", "elo_diff", "rest_diff"]
Report the model's log loss next to the market's for every block. A model that wins one block and loses the next three hasn't found anything. Tune hyperparameters inside each training window only. If you tune on the test blocks, the test is no longer out of sample.
Calibration comes before accuracy
Accuracy, the share of games where the favored side won, is the wrong score. A model that picks every favorite can be accurate and still useless for betting. What you need is calibrated probabilities: when the model says 58%, it should happen about 58% of the time.
Check it by bucketing predictions and comparing predicted with actual rates, and score it with log loss or the Brier score. On one game the Brier score is the squared gap between the probability and the result: a 60% prediction on a team that wins scores (1 - 0.6)², which is 0.16. Lower is better, and like log loss, it only means something next to the market's score on the same games.
Gradient-boosted trees and neural networks often need recalibrating after training, with isotonic or Platt scaling fitted on a separate block. Logistic regression is usually close to calibrated out of the box, which is one reason it's a good first model.
Machine learning for sports betting fails in familiar ways
- Leaked features. A "starting lineup" column filled in after kickoff, or season averages that include the game itself.
- Too many features for the data. A few thousand games and a few hundred features is a recipe for fitting noise.
- Prices you couldn't bet. Testing profit against closing lines you'd never have been able to take.
- Accuracy as the goal. Picking winners is not the job. Pricing them better than the market is.
Even a model that passes all of this will lose many of its bets. Edges in betting are small, and results swing widely around them, so treat any stake as money you can afford to lose.
Where this fits
This guide covers training. Our sports betting model guide covers the broader question of how to judge any model, sports prediction models compares machine learning with ratings, simulation and market-derived models, and AI sports betting separates real modelling from the "AI picks" sold online.
For the market feature, you need prices with honest timestamps. ZippyOddsAPI keeps the time each price was observed and reads each operator's own public data. Its /v1/history endpoint returns a full-game moneyline, spread or total market's stored points for 30 days, which suits building the decision-time price going forward. ZippyOddsAPI also scores current prices against a no-vig consensus of sharp references, which you can log as a baseline next to your own model's numbers. The ZippyOddsAPI docs explain each endpoint and how to ask the operator for a key.