Examples · Python notebook
Build an NFL Game Prediction Model With Odds Data (Python Notebook)
This notebook builds a simple NFL prediction model from three seasons of results, checks how well it predicts games it hasn't seen, then compares its picks with what sportsbooks are pricing this week.
On the 2025 season it picked 64.6% of winners, with a Brier score of 0.222 where a coin flip scores 0.250.
Cost: About 40 credits per run. The free plan includes 1,000 credits a month. Full project on GitHub.
1. Load every final score
The notebook pages through /v1/events for final NFL games since 2023, 100 at a time, and keeps the date, teams and score of each.
2. Build Elo ratings
Every team starts at 1500. After each game the winner takes points from the loser, more for an upset and for a bigger margin. Home teams get a 48-point boost, and ratings drift a third of the way back to 1500 between seasons.
3. Measure it, then compare with the market
The notebook scores the last full season with accuracy, the Brier score and a calibration chart. It then pulls this week's moneylines, removes each book's margin to get a fair market probability, and lists the games where the model and the market disagree most.
The biggest gaps usually mean the market knows something Elo can't, like an injury. Treat them as questions to research, not bets.
The code
game-prediction-notebook/nfl_prediction_model.ipynb, straight from the repository.
# pip install moneyline-sports-api pandas matplotlib
from datetime import date
import math
import matplotlib.pyplot as plt
import pandas as pd
from moneyline_sports_api import MoneyLine
ml = MoneyLine() # reads MONEYLINE_API_KEY
def final_games(league, since):
rows, page = [], 1
while True:
res = ml.request("GET", "/v1/events", {"league": league, "status": "final", "from": since,
"to": date.today().isoformat(), "limit": 100, "page": page})
rows += res["data"]
if page >= res["meta"]["pages"]:
return rows
page += 1
games = pd.DataFrame([
{"start": g["startTime"], "home": g["homeTeamName"], "away": g["awayTeamName"],
"home_score": g["scores"]["home"], "away_score": g["scores"]["away"]}
for g in final_games("nfl", "2023-08-01")
if g["homeTeamName"] != "TBD" and g["scores"]
])
games["start"] = pd.to_datetime(games["start"])
games = games.sort_values("start").reset_index(drop=True)
games["season"] = games["start"].apply(lambda t: t.year if t.month >= 8 else t.year - 1)
print(len(games), "games")
games.tail()
K, HOME_EDGE, CARRYOVER = 20, 48, 2 / 3
def win_prob(home_elo, away_elo):
return 1 / (1 + 10 ** ((away_elo - home_elo - HOME_EDGE) / 400))
ratings, season, preds = {}, None, []
for g in games.itertuples():
if g.season != season: # regress toward the mean each new season
ratings = {t: 1500 + (r - 1500) * CARRYOVER for t, r in ratings.items()}
season = g.season
home, away = ratings.get(g.home, 1500), ratings.get(g.away, 1500)
p = win_prob(home, away)
home_won = 1.0 if g.home_score > g.away_score else 0.5 if g.home_score == g.away_score else 0.0
preds.append({"season": g.season, "p_home": p, "home_won": home_won})
margin_mult = math.log(abs(g.home_score - g.away_score) + 1) # bigger wins move ratings more
shift = K * margin_mult * (home_won - p)
ratings[g.home], ratings[g.away] = home + shift, away - shift
preds = pd.DataFrame(preds)
test = preds[preds.season == preds.season.max() - (1 if date.today().month >= 9 else 0)]
accuracy = ((test.p_home > 0.5) == (test.home_won == 1)).mean()
brier = ((test.p_home - test.home_won) ** 2).mean()
print(f"{len(test)} games accuracy {accuracy:.1%} Brier {brier:.3f} (coin flip: 0.250)")
bins = pd.cut(test.p_home, [0, .3, .4, .5, .6, .7, 1])
calib = test.groupby(bins, observed=True).agg(predicted=("p_home", "mean"), actual=("home_won", "mean"))
ax = calib.plot(x="predicted", y="actual", marker="o", legend=False, figsize=(5, 5))
ax.plot([0, 1], [0, 1], linestyle="--", color="gray")
ax.set(xlabel="Predicted home win chance", ylabel="How often the home team won", title="Calibration")
plt.show()
def market_home_prob(game):
fair = []
for book in game["bookmakers"]:
if book["sourceType"] != "sportsbook":
continue
prices = {o["name"]: o["impliedProbability"] for m in book["markets"] for o in m["outcomes"]}
home, away = prices.get(game["homeTeamName"]), prices.get(game["awayTeamName"])
if home and away:
fair.append(home / (home + away)) # remove the vig
return pd.Series(fair).median() if fair else None
rows = []
for g in ml.odds(league="nfl", market="moneyline"):
market = market_home_prob(g)
if market is None:
continue
model = win_prob(ratings.get(g["homeTeamName"], 1500), ratings.get(g["awayTeamName"], 1500))
rows.append({"game": f"{g['awayTeamName']} at {g['homeTeamName']}", "start": g["startTime"][:10],
"model_home": round(model, 3), "market_home": round(market, 3),
"gap": round(model - market, 3)})
board = pd.DataFrame(rows).sort_values("gap", key=abs, ascending=False)
board.head(10)
Run it
Get a free API key, clone the example, then run:
pip install moneyline-sports-api pandas matplotlib jupyter
export MONEYLINE_API_KEY=your-key
jupyter notebook nfl_prediction_model.ipynb