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.

nfl_prediction_model.ipynb
# 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:

Terminal
pip install moneyline-sports-api pandas matplotlib jupyter
export MONEYLINE_API_KEY=your-key
jupyter notebook nfl_prediction_model.ipynb

More examples