Methodology

Every number on this page is read live from the same exported JSON each feature's own view is built from — nothing here is hand-typed separately from the pipeline that produced it. Full narrative decision log: `docs/decisions.md` in the repo.

Data through 2026-04-12 (2015-16 through 2025-26 regular seasons).

Architecture

All heavy compute — fetching, feature engineering, model training — runs offline on the author's own machine. The deployed site is precomputed static files plus one thin Cloudflare Worker that only handles /api/ask. Data never goes into git; `make deploy` copies the latest exports into the static assets folder and runs `wrangler deploy` directly from the author's machine.

Home machineCloudflare free planBrowsernba_api fetch(throttled, cached)Raw cache(data/raw)Canonical tables(Parquet)Feature pipelines+ model trainingExports(JSON, grids, Parquet)make deploy(wrangler)Worker static assets(Next.js export + data)Worker /api/askWorkers AICache API + KV counterVisitor browser(canvas, kNN, DuckDB-WASM)

Data sources and season coverage

Every table is fetched from stats.nba.com via the `nba_api` Python library, throttled to about one request per second and fully cached so a run can be interrupted and resumed. Box scores, season totals, and shot charts cover 2015-16 through 2025-26. Play-by-play (used for game flow) covers the three most recent completed seasons: 2023-24 through 2025-26.

Features, models, and evals

One section per view: what it models, which approach shipped and why, and the real evaluation numbers behind it.

Player style map

Every player-season (2015-16 onward) placed on a 2D map by playing style — shot diet, role, rebounding and defense, self-creation, ball handling, shooting mode, touch location, and play types — via PCA for the underlying vector space, a Gaussian mixture for archetypes, and UMAP purely for 2D layout. Nearest-neighbor comps run as a brute-force cosine search entirely in the browser.

Loading eval data…

Limitation: the 30% hand-checked comp sanity rate reads low in isolation — it's measuring exact agreement with one author's subjective sense of a “good comp,” a much stricter bar than the map being generally sensible (most near-misses, like Curry↔Lillard or Giannis↔LeBron, are defensible archetype neighbors, just not the single comp the author had in mind).

Ask the box score

Plain-English questions are translated to a single validated SQL query by Cloudflare Workers AI (llama-3.3-70b-instruct, with 8B/3B fallbacks if the primary model is ever retired), then executed against real Parquet tables entirely in the browser via DuckDB-WASM. The same SQL guard (allowlisted tables, single SELECT/WITH statement, no file or network access, forced row limit) runs on both the Worker before caching a query and the browser before executing it.

Modelllama-3.3-70b8B model reproducibly refused recent-season questions
Measured cost34.1 neurons/question~290 questions/day of headroom on the free allocation
Daily budget250 questions1/minute per visitor

Not yet measured: golden-set execution accuracy (`evals/ask/golden.jsonl`, ~60 questions across easy/medium/hard) and Worker CPU time per request. Both are flagged open in `docs/decisions.md` rather than reported with a made-up number — the model choice above is based on manual spot checks, not the full eval, and may change once that eval exists.

Shot quality court

An expected field-goal probability (xFG) model — what an average player would shoot from a given location, situation, and shot type — trained on every field goal attempt since 2015-16. A LightGBM classifier was compared against a logistic-regression baseline with distance/angle splines; the model isn't shipped directly, only a precomputed lookup grid the browser reads at 1-foot resolution per action family.

Limitation: no defender-distance data exists in public shot logs, so the model can't separate a contested layup from a wide-open one — layupis the one action family with a materially wider calibration gap than the rest.

Game flow

Win probability through every game since 2023-24, from play-by-play events plus a FiveThirtyEight-style Elo rating (K=20, +75 home-court points, 30% regression toward the mean each season) computed from the full 2015-16 onward history. No betting-line data anywhere in the pipeline.

Limitation: Elo is built from final scores only — no injuries, rest, or in-season roster changes. Possession is derived from event sequences, not exposed directly by the API, so it can occasionally misfire on unusual sequences (technical fouls, replay reviews).

How this stays free

The whole site runs on Cloudflare's free plan. These are the budgets that shaped nearly every low-compute design choice above — precomputed grids instead of shipping models, caching before calling an LLM, static assets over server rendering.

ItemBudget
Landing page JavaScriptUnder 150 KB gzipped
Style map data fileUnder 2 MB gzipped
Shot court: one player shardUnder 300 KB gzipped; xFG grid under 100 KB
Game flow: one monthly bundleUnder 1 MB gzipped
Worker CPU per requestUnder 5 ms
Workers AIUnder 10,000 neurons/day, including evals
KV writesUnder 500/day
Nightly refreshUnder 15 minutes

Versions

Legal and brand hygiene

Noncommercial portfolio project, not affiliated with or endorsed by the NBA. Data sourced from stats.nba.com. No team or league logos are used.