PrizePicks training data for player prop models.

For developers training a prop model on graded PrizePicks lines since 28 October 2026 and adding longer box-score history as separate features.

Build a player prop model from graded PrizePicks lines, recorded line changes and player game logs. Get a joined training dataset through one API, then test it on later games with a clear decision-time cutoff. Start with the Free sample; page the full available record on Developer.

Not for

A multi-year line backtest or a claim of long-run profitability
Recorded lines are too new. Box scores are not lines. Read more
Finished picks or betting advice
This is data for your own model.
Pixel-art archive drawers beside a stepped line-history chart.

Base rates to beat, NBA graded lines

Graded lines45,126
Standard over rate45.6%
Goblin over rate64.4%
Demon over rate20.3%
MarketStandard over rateStandard rowsAll tier rows
Points43.9%7265,417
Pts+Rebs46.0%6955,042
Pts+Rebs+Asts44.4%7234,633
Rebounds47.9%4224,037
Pts+Asts46.2%5634,011
Rebs+Asts49.7%4593,643
Assists44.8%2813,026
3-PT Made53.9%1652,753

Games from 2026-10-28 to 2026-11-02. Pushes excluded. A short sample, so expect these to move. Every market and player

Get a player prop training dataset, not just today’s board

GET /api/v1/training-set returns one flat row per graded projection, with opening and closing lines, the tier, matchup fields and the later outcome. It supports JSON and CSV, league and stat filters, date windows, and cursor pagination. The player and projection identifiers remove the need to match outcome labels to live offers by a display name alone.

Your modelling goalStart withWhat the data can establish
Predict whether a recorded line goes over/api/v1/training-setFeatures and over/under/push outcomes for graded lines; the line archive begins 28 Oct 2026.
Forecast a player’s next stat value/api/v1/game-logsPast box scores in the loaded leagues and seasons. Use them for lagged form features, not as proof of older offered lines.
Backtest a decision before game start/api/v1/projections/{id}/history or /api/v1/board?as_of=…Which recorded offer state existed by the chosen cutoff. A closing-time dataset cannot stand in for an earlier decision.
Score the current PrizePicks board/api/v1/projectionsThe current lines, tiers and observation times. Future results are unavailable.

What one training row contains

Use one league, market, period and tier while building the first model. Mixing NBA Points with Rebounds or full-game lines with first-half lines changes what the target means. The target for a binary classifier is result == "over" after excluding pushes and voids; actual_value is the target for a stat-regression model. Those are different problems.

FieldRole in a modelTime check
closing_line, closing_observed_atThreshold for a close-time classifierThe observation must be at or before the simulated decision time. Check close_confidence and close_gap_seconds.
opening_lineFirst line recorded for the projectionFor an earlier-cutoff strategy, verify the first history observation was available by that cutoff.
tier, stat, periodMarket definitionKeep these consistent with the offer state at prediction time.
home_away, opponent, rest_daysMatchup contextSome fields are reconstructed from completed records. Audit availability before using them in a point-in-time simulation. Null means unproven, not zero.
trending_count_at_start, trending_observed_atOptional popularity featureOften null. At-start counts may leak into a strategy that predicts earlier.
minutes, actual_value, resultSame-game labels or later analysisNever use them as features for that same game. Prior-game values need a strict earlier-game join.
revision, updated_atReproducibility and label checksSave the export. The current revision may include a correction published after an earlier evaluation cutoff.

Run a chronological Python probability baseline

Save the program below as prop_model.py and run python3 prop_model.py with PRIZEPICKS_API_KEY set in your environment. It uses only the Python standard library. It pages JSON, saves training-snapshot.json without the key, and stops if the API identifies a Free sample. HTTP errors remain explicit. A full export can still be too small for useful evaluation.

This educational baseline learns a smoothed over rate for five-point closing-line buckets. It uses confirmed full-game NBA Points closes, not player names or post-game features. It splits whole game dates into earlier training, later validation and newest test windows. The validation window chooses smoothing; the test window is reported once. There is no claim that this simple baseline beats the constant training over-rate.

"""A small close-time probability baseline. Python 3; standard library only."""
import json
import math
import os
import time
from collections import defaultdict
from datetime import datetime, timezone
from urllib.parse import urlencode
from urllib.request import Request, urlopen

BASE = "https://prizepicksapi.com/api/v1"
FILTERS = {"league": "NBA", "stat": "Points", "tier": "standard", "limit": 1000}


def stamp(value):
    return datetime.fromisoformat(value.replace("Z", "+00:00"))


def download():
    headers = {
        "Authorization": "Bearer " + os.environ["PRIZEPICKS_API_KEY"],
        "X-User-Request": "Train and evaluate an NBA Points close-time probability baseline",
    }
    rows, pages, seen = [], [], set()
    cursor = None
    while True:
        params = dict(FILTERS)
        if cursor:
            params["cursor"] = cursor
        request = Request(BASE + "/training-set?" + urlencode(params), headers=headers)
        with urlopen(request, timeout=30) as response:
            page = json.load(response)
        rows.extend(page["data"])
        pages.append(page["meta"])
        if page["meta"].get("plan_limit"):
            # A Free sample is useful for inspection, not a complete training archive.
            break
        cursor = page["meta"].get("next_cursor")
        if not cursor:
            break
        if cursor in seen:
            raise RuntimeError("Repeated pagination cursor")
        seen.add(cursor)
        time.sleep(1.1)
    snapshot = {
        "captured_at": datetime.now(timezone.utc).isoformat(),
        "filters": FILTERS, "pages": pages, "data": rows,
    }
    with open("training-snapshot.json", "w") as file:
        json.dump(snapshot, file, indent=2)
    if any(meta.get("plan_limit") for meta in pages):
        raise RuntimeError("Saved Free sample. Full archive evaluation needs an unrestricted export.")
    return rows


def eligible(rows):
    # One league, market, period and tier; no mixing incomparable targets.
    kept = {}
    for row in rows:
        if (row.get("league") != "NBA" or row.get("stat") != "Points"
                or row.get("tier") != "standard" or row.get("period") != "Full game"
                or row.get("close_confidence") != "confirmed"
                or row.get("status") != "graded" or row.get("result") not in ("over", "under")):
            continue
        try:
            line = float(row["closing_line"])
            start, observed = stamp(row["starts_at"]), stamp(row["closing_observed_at"])
            stamp(row["updated_at"])
            if not math.isfinite(line) or observed > start:
                continue
        except (ValueError, TypeError, KeyError):
            continue
        old = kept.get(row["projection_id"])
        if old is None or stamp(row["updated_at"]) > stamp(old["updated_at"]):
            kept[row["projection_id"]] = row
    return sorted(kept.values(), key=lambda row: (row["game_date"], row["starts_at"], row["projection_id"]))


def chronological_split(rows):
    dates = sorted({row["game_date"] for row in rows})
    if len(dates) < 5:
        raise ValueError("Need at least five game dates for this educational split; more history is preferable.")
    valid_date, test_date = dates[int(len(dates) * 0.6)], dates[int(len(dates) * 0.8)]
    valid_cutoff = min(stamp(row["starts_at"]) for row in rows if row["game_date"] >= valid_date)
    test_cutoff = min(stamp(row["starts_at"]) for row in rows if row["game_date"] >= test_date)
    train, valid, test = [], [], []
    for row in rows:
        if row["game_date"] < valid_date:
            # Conservative: do not train on a result revision published after validation began.
            if stamp(row["updated_at"]) < valid_cutoff:
                train.append(row)
        elif row["game_date"] < test_date:
            if stamp(row["updated_at"]) < test_cutoff:
                valid.append(row)
        else:
            test.append(row)
    if not all((train, valid, test)):
        raise ValueError("An evaluation window is empty after timestamp checks; do not invent a score.")
    return train, valid, test


def label(row):
    return int(row["result"] == "over")


def fit(train, strength):
    # Learn a smoothed over rate by five-point closing-line bucket.
    prior = (sum(map(label, train)) + 1) / (len(train) + 2)
    groups = defaultdict(list)
    for row in train:
        groups[math.floor(float(row["closing_line"]) / 5)].append(label(row))
    rates = {bucket: (sum(values) + strength * prior) / (len(values) + strength)
             for bucket, values in groups.items()}
    return lambda row: rates.get(math.floor(float(row["closing_line"]) / 5), prior)


def brier(rows, predict):
    return sum((predict(row) - label(row)) ** 2 for row in rows) / len(rows)


def evaluate(rows):
    train, valid, test = chronological_split(eligible(rows))
    # Choose smoothing on validation, then keep test untouched until this final report.
    models = [(strength, fit(train, strength)) for strength in (5, 20, 100)]
    strength, model = min(models, key=lambda pair: brier(valid, pair[1]))
    constant = (sum(map(label, train)) + 1) / (len(train) + 2)
    report = {
        "rows": {"train": len(train), "validation": len(valid), "test": len(test)},
        "smoothing": strength,
        "validation_brier": brier(valid, model),
        "test_brier": brier(test, model),
        "test_constant_baseline_brier": brier(test, lambda row: constant),
        "test_dates": [test[0]["game_date"], test[-1]["game_date"]],
    }
    print(json.dumps(report, indent=2))
    return report


if __name__ == "__main__":
    evaluate(download())

Keep the decision time and label availability honest

The example is a retrospective close-time baseline: its threshold is the last recorded line at or before start. It is not a replay of what a live agent could have executed. Recorded observations do not guarantee availability, account eligibility or a fill. For a model that predicts 60 minutes before start, use history observed by starts_at minus 60 minutes and reconstruct both the threshold and features at that same instant.

  • Keep every offer from the same event and game date in one chronological partition. Randomly splitting rows can put near-duplicate markets from the same game in both training and test.
  • Train preprocessing, imputation, feature selection and calibration on training windows only. Choose settings on validation, then freeze them before looking at test results.
  • Exclude training labels whose current updated_at is later than the next evaluation cutoff. The example does the same for validation labels before test begins. This is conservative: updated_at is the latest revision time, not the first time a result became knowable.
  • A current export is not a complete historical revision feed. Archive successive exports and their timestamps if the research needs to replay exactly which label revision was available then.
  • Voids are omitted by default. include_void=true lets you inspect their reasons; do not encode a pulled offer or non-participation as an under or zero. Excluding voids also changes the population being studied.

Add past-game features without borrowing the future

The separate game-log endpoint can provide a longer performance history than the PrizePicks line archive. Check meta.stats_leagues and the loaded seasons rather than assuming every sport is present. Fetch player game logs by stable player identifier where available, keep the target game separate, and calculate rolling points, minutes and usage-like features from completed earlier games only.

A five-game mean must exclude the game it predicts. If two games share a date and precise completion times are unavailable, use a conservative earlier-date cutoff instead of assuming the earlier row was already complete. Shift rolling features before joining them to training rows. Fit missing-value handling on the training window; a missing stat key is not evidence of a zero.

Report probability quality before talking about returns

The code reports binary Brier loss: the average squared difference between predicted over probability and the 0/1 outcome. Lower is better for that score. Compare it with the constant over-rate learned from the same training window. Also inspect reliability by probability bucket, sample sizes by date and tier, and performance on later untouched windows; Brier loss alone is not a calibration diagnosis.

Irregular sports schedules make a fixed number of rows a poor proxy for equal time windows. The example uses whole dates; longer experiments should use explicit calendar cutoffs and walk-forward evaluation. Five dates are only a guard against an empty toy split, not a sufficient scientific sample. Re-running many models against the same test window turns it into another validation set.

Accuracy or a probability score is not return on investment. Payout structures, correlated selections, push and void rules, promotions and actual availability matter separately. This API does not establish personal promotional eligibility or promise profitable picks.

Free sample versus the full available training record

PlanTraining dataWhen it fits
FreeOne page, at most 100 training rows from the last 7 days; game logs limited to the last 10 games per player or team.Inspect fields, build a parser and check the first request.
Developer: $49/monthPage the full available training set; read all loaded game-log seasons and supported historical queries.Build and rerun chronological training and validation windows.

Developer removes the Free sample restrictions; it does not manufacture older PrizePicks lines or missing leagues. The recorded line archive begins 28 Oct 2026, while loaded game logs may extend further back. Get a Free API key to inspect the schema, then choose Developer when the sample or game-count limit prevents the requested run.

Try it

Endpoint docs:GET /api/v1/training-setGET /api/v1/resultsPlans and limits

Frequently asked questions

What data does a PrizePicks player prop model need to train on?

For an over/under classifier, use the line and tier known at the prediction cutoff plus the later graded outcome. GET /api/v1/training-set joins those fields by projection. For a player-stat forecasting model, older game logs provide prior performances; join only games completed before the target game.

Are older NBA game logs the same thing as historical PrizePicks lines?

No. Game logs are completed player performances and may cover loaded seasons before the line archive began. The observed PrizePicks line history starts on 28 Oct 2026. Older box scores cannot prove what PrizePicks offered on those dates.

Can I evaluate a prop model using the Free training-data sample?

Free is useful for inspecting the schema: at most 100 training rows from the last 7 days, one page. It is not the full record, and a model score on that small sample is not evidence of reliable performance. Developer can page the available archive and read full loaded seasons of game logs.

Is a closing line safe when my model makes predictions an hour before a game?

Only if that line was already observed by your prediction cutoff. A closing line recorded after the one-hour cutoff is future information for that strategy. Rebuild the offer from history observed at or before the cutoff, and apply the same rule to popularity counts and book prices.

Does the training dataset guarantee a profitable model?

No. It supplies recorded lines, timestamps and outcomes. You still need out-of-time evaluation, adequate samples, calibration checks and a realistic treatment of voids, payouts and unavailable offers. A better probability score alone does not establish positive returns.

Continue building

References