Skip to content

Fetch Historical NBA Stats Responses

The historical Stats flow preserves archived game responses directly from stats.nba.com. It is the primary acquisition path for historical games. Previously retained liveData files are used only as a compatibility fallback when a matching V3 document is absent.

Endpoints

The default run fetches two endpoints independently:

Endpoint Purpose Raw path
playbyplayv3 Historical event stream data/raw/stats/playbyplayv3/{game_id}.json
boxscoretraditionalv3 Players, minutes, and traditional game totals data/raw/stats/boxscoretraditionalv3/{game_id}.json

gamerotation is supported as an opt-in auxiliary endpoint. It can provide interval-level substitution evidence, but coverage and latency differ from the two cloud-backed V3 feeds. A missing rotation response never invalidates a retained play-by-play or box score.

Current status: Game Rotation acquisition is suspended. The endpoint's coverage varies sharply by season and produces frequent HTTP 500 responses. Retained responses remain available for local recovery experiments, but no historical backfill is part of the active data pipeline.

Canonical processing selects each endpoint independently. It prefers a valid Stats V3 artifact and otherwise uses the corresponding retained liveData artifact. The build ledger and quality report record the selected source and SHA-256 digest of the exact raw response.

Archive From 1996-97

playbyplayv3 has been verified as populated from 1996-97 onward. Begin a regular-season archive with play-by-play, then retain box scores in a separate resumable pass. Explicit --season values keep the source range visible in the run provenance:

uv run nba-fetch-stats-history \
  --season 1996-97 \
  --season 1997-98 \
  --season 1998-99 \
  --season 1999-00 \
  --endpoint playbyplayv3 \
  --season-type regular \
  --max-workers 2 \
  --min-request-interval 1.0 \
  --request-interval-jitter 0.25 \
  --run-id stats-v3-pbp-1996-1999

Repeat the same command for subsequent season blocks. The raw cache validates and skips completed responses, so an interrupted archive is resumed by rerunning the identical selection. Retain boxscoretraditionalv3 in a second pass after the play-by-play archive is complete.

Progress Logs

Each newly archived game endpoint emits a Prefect task log with the game ID, local game date, matchup, and final score from the cached V3 play-by-play event. For example:

Archived 0029600001 | 1996-11-01 | CHI @ BOS | final CHI 107-98 BOS

Validated cache hits do not produce a second progress line. Prefect still tracks them as completed tasks, while the raw cache remains the authoritative resume boundary.

One-game smoke test

Start with the historical game already used to validate the endpoint contract:

uv run nba-fetch-stats-history \
  --season 2019-20 \
  --game-id 0021900194 \
  --max-workers 1 \
  --min-request-interval 2.0

The flow creates one Prefect task per game and endpoint. A second invocation validates and skips both cache artifacts without making a request.

Legacy Cache Reconciliation

--cdn-missing-only is retained only to reconcile a legacy cache. It is not the recommended historical acquisition path:

uv run nba-fetch-stats-history \
  --season 2019-20 \
  --cdn-missing-only \
  --max-workers 2 \
  --min-request-interval 1.0 \
  --request-interval-jitter 0.25 \
  --run-id stats-gaps-2019-20

For a new historical pull, omit --cdn-missing-only and archive the V3 pair directly.

Preserve all historical V3 feeds

Without --season, the command selects final regular-season games from 2019-20 through 2024-25:

uv run nba-fetch-stats-history \
  --max-workers 2 \
  --min-request-interval 1.0 \
  --request-interval-jitter 0.25 \
  --run-id stats-v3-2019-2024

For an acquisition-first pass, request only play-by-play:

uv run nba-fetch-stats-history \
  --endpoint playbyplayv3 \
  --run-id stats-pbp-2019-2024

Then run boxscoretraditionalv3 separately. Endpoint-specific passes reduce the amount of work exposed to any single source interruption.

Suspended Game Rotation experiment

Request rotation data explicitly:

uv run nba-fetch-stats-history \
  --season 2024-25 \
  --endpoint gamerotation \
  --limit 10 \
  --max-workers 1

Treat this as a coverage probe only. The endpoint can return a server error for games that are otherwise available through the V3 feeds. Historical acquisition is currently suspended because observed coverage was materially nonuniform.

To probe only games whose latest regular-season processing attempt failed, use the durable build ledger. The command samples a deterministic number of failed games from each available season, archives only gamerotation, validates the two team interval tables, and writes a Parquet report under artifacts/reports/:

PREFECT_API_URL=http://127.0.0.1:4200/api \
  uv run nba-probe-game-rotation --per-season 3

This is a coverage and structural-availability test, not a claim that Game Rotation resolves the associated lineup ambiguity. Fetch it for the complete failure manifest only after the probe demonstrates useful coverage. The probe uses no per-game retry because an HTTP 500 is itself coverage evidence for this auxiliary endpoint; archival endpoint runs retain the default retries.

When a cached response is structurally valid, use the documented nba-recover-game-rotation command to measure whether the existing processing quality contract accepts the repair.

Fetch remaining lineup failures

After the coverage probe and recovery cohort establish that the source can fix period-start ambiguity, archive it only for current failures whose terminal message is Period lineup remains ambiguous or No legal period lineup can be inferred. This avoids spending endpoint capacity on unrelated score and possession defects.

PREFECT_API_URL=http://127.0.0.1:4200/api \
  uv run nba-fetch-game-rotation-failures \
  --max-workers 1 \
  --min-request-interval 1.0 \
  --request-interval-jitter 0.25 \
  --run-id game-rotation-lineup-failures-r1

The command writes its selected games to artifacts/reports/game_rotation_fetch/{run_id}/selection.parquet before any network activity and requests no retries: an HTTP 500 is recorded as coverage evidence. Rerunning the same command skips retained valid responses and tries only still-unavailable games.

The command also supports a season-by-season reverse backfill for a future re-evaluation. It is intentionally not part of the active pipeline:

PREFECT_API_URL=http://127.0.0.1:4200/api \
  uv run nba-fetch-game-rotation-failures \
  --max-season 2018-19 \
  --reverse-seasons \
  --max-workers 4 \
  --min-request-interval 1.0 \
  --request-interval-jitter 0.25 \
  --run-id game-rotation-backfill-through-2018-r1

The pacing gate remains process-wide: four workers improve throughput around slow responses but do not multiply the one-request-per-second source rate.

Resume, provenance, and failure policy

Every successful response is written byte-for-byte before the task reports success. Its .meta.json sidecar stores the requested URL, fetch time, SHA-256 digest, and selected source response headers. Cache reads validate the digest, endpoint, game ID, and minimum endpoint schema.

Terminal endpoint outcomes are appended to:

data/manifests/stats_fetches.parquet

The manifest is written after all selected tasks finish, but the raw cache is the resume boundary. If a process stops before the manifest write, rerun the same selection; valid endpoint files are skipped.

Network errors, HTTP 408, 425, and 5xx responses receive bounded retries. HTTP 403 or 429 opens a process-wide 15-minute circuit. The default global pacing is one request per second plus up to 0.25 seconds of jitter, including across concurrent Prefect workers.

Tests

The endpoint contract tests are in tests/test_stats_history_fetch.py. They cover:

  • exact query parameters and browser headers for all three endpoints;
  • byte preservation and SHA-256 sidecars;
  • source game-ID and minimum-schema validation;
  • cache resume behavior and typed Parquet manifest round trips;
  • transient server-error classification.
  • final-score extraction used by play-by-play progress logs.

Run the focused suite with:

uv run pytest -q tests/test_stats_history_fetch.py