Methods

Every number can be recomputed from source

The games use 14 era-normalised per-100 features for speed and transparency. The research model, MTNN, widens that to 130 features in 17 towers and compresses each season to a 64-d unit vector for neighbour search. The architecture is covered in depth in the model section below.

01

The vector space — two views, one frame

Games (fast): 12,966 player-seasons, 1996–97 to 2025–26, from stats.nba.com, per 100 possessions. Eligibility is schedule-aware — about 15% of team games plus a minutes bar (6% of schedule minutes, floored at 450) — not a flat cutoff. Each season’s 14 features are z-scored within that season. Player counts site-wide are distinct player IDs — 2,426 (distinct display names: 2,422; a few names belong to two different players).

FeatureMeasuresEra note
PTSScoring volume per 100Pace-adjusted
ASTPlaymakingRaw per 100, era-z
OREB / DREBReboundingPer 100
STL / BLKDefensive eventsSteals, rim
TOVTurnoversNegative
FG3A / FGA / FTAShot mix and pressureVolume, era-z
FG3% / FG% / FT%AccuracyEB-shrunk → percentile
PLUS_MINUSOn-court impactPer 100

Cleaning: dedupe; missing → season mean plus a mask bit m; attempt-weighted empirical-Bayes shrinkage on percentages; clip at ±4σ.

01·b

The richer research model

The same 12,966 player-seasons with far more context: volume, playmaking, rebounding, defense, efficiency, shot mix, bio, tracking (2013+, masked before), form, market, roster, career, competition, team, pedigree, playoffs and honors. Each family feeds its own tower.

130Features17Towers30Seasons8Types64-dL2
z-score
How far above the league average: 0 is average, +2 is elite.
era-z
Measured against that season only.
mask
“Never measured”, kept distinct from zero.
per 100
Per 100 team possessions, so pace doesn’t inflate a number.
02

MTNN — the model behind every guess

17 towers → residual towers → a 64-d fingerprint, L2-normalised so cosine is similarity → decoded to archetype (8), position (5) and skills. An 18th family, injury, feeds a durability read-out head rather than an input tower: as an input it measured −0.088 test recall, because durability is not a style axis.

Input 130 feats, mask m∈{0,1}
h = cat([x·m, m]) → 2·d_in per family
Tower 17×: 2d_in→160 LN GELU → 160→32 LN + skip ×2 → 32-d (544 total)
+ season_emb 12-d → 556 → 556→256 GELU → 64 L2
Heads: 8 arch / 5 pos / 14 next / 18 skills (64→16→1)
~224K params · ONNX 548 KB
03

The map

Two maps, both PCA(3), both min-max scaled. The league map (Atlas, Play) flattens the 14 era-normalised stat traits — its three axes keep about 62% of those traits’ variance. The explorer map above flattens the model’s 64-d embedding, whose first three axes keep only about 27% of its variance — so on either map, “nearby” is approximate, and the model’s true nearest seasons are computed in the full spaces, not read off the picture. Axes are named from their correlations.

The Atlas map’s axes:

AxisLow endHigh end
XPaint: boards, blocksPerimeter: shooters
YHigh-usage scorersLow-usage role players
ZOff-ball, low-eventHandlers (assists, steals, turnovers)
04

Archetypes — eight types

k-means with K = 8, in two spaces. The labels on players, dossiers and games come from the frozen game contract — k-means on the 14-d era-z serving vectors, seeded so it reproduces; the MTNN archetype head was trained to predict those labels (top-1 0.970 against the training snapshot). The serving vectors were rebuilt in October 2026 and their k-means re-drawn, so the head and the served labels now agree more loosely — cousins, not copies; where a surface shows a model archetype instead, it uses the head’s own assignment. The Trends stream's prevalence is computed separately, from a K = 8 clustering of the 64-d MTNN v5 embedding, with the same naming rule (names from distinctive skill z-scores).

  1. Rim protection + offensive glass
  2. Three-point volume (low finishing)
  3. Playmaking + steals
  4. Offensive glass + defensive glass
  5. Three-point volume + shot volume
  6. Three-point volume + three-point accuracy
  7. Scoring volume + shot volume
  8. Rim pressure (FTs) + defensive glass
04·b

Career trails — a player, season by season

Every player with two or more seasons gets a trail: their season points joined in order and drawn over the same map projection you see. The scrubber walks rookie → twilight, and the “what changed” strip names the two style traits that moved most each season, in plain words. Dotted stretches mean a missed season (injury, retirement, lockout).

Trails show movement in style, not improvement — a trail drifting across the map means the player’s statistical shape changed, not that they got better or worse. Nearby points are similarity, not rankings.

04·c

Twin explainer — why they’re twins

Every twin reveal now says why, in three short chips: the three style traits the two seasons share most closely, plus the single biggest difference between them. Matches below 0.60 similarity are flagged as looser matches.

The twin itself is found in the full 64-dimensional embedding; the explanation is a simpler breakdown using the 14 serving traits behind the map. It describes the match rather than re-deriving it.

04·d

Insights — shareable findings

The Insights page curates the most striking things the embedding model has measured: the strangest and most average seasons, the biggest career reinventions, era twins decades apart, dying archetypes, the three-point takeover, and the turnover decline. The model cards there are generated from the same served vectors — nothing hand-written, nothing invented. Alongside them sits a second, labeled series, From the ledger: salary, tax and ticket findings assembled by hand from named public sources (Spotrac, Hoops Rumors, Basketball-Reference and the others printed on each card), each with its source, sample and window on the card face. The two series are never blended, and a ledger card never borrows the model's authority.

Uniqueness is Euclidean distance from the centroid of all 12,966 player-seasons in the 14-d style space (minimum 1,500 minutes). Career transformation is first-to-last season distance (minimum 4 seasons at 800+ minutes). Era-twin gaps use the full 64-d cosine similarity, similarity ≥ 0.60, one appearance per player. Every card deep-links (/insights.html#<slug>), exports a 1080×1350 PNG rendered in the browser, and has a crawler page at /insights/<slug> with a pre-rendered unfurl image. The daily spotlight never repeats a card shown in the grid below it.

04·e

Player dossier — every player, from the model

Open any player in the directory and their dossier is built from the same model behind the map — no hand-written bios, nothing to go stale. Each dossier carries a minutes-weighted 14-trait style fingerprint, the player’s archetype and how it moved across their career, their era twin (with the plain-words why), how their game changed from first to last charted season, the five nearest player-seasons in style space, and a plain-words read of where they live on the map.

All dossier numbers come from the 14-d serving projection (vectors.json — the transparent era-z style space, not an MTNN output) and the derived research assets (era twins, career trails, archetype assignments, honors). Similarity is cosine in the 14-d style space; era twins are matched in the 64-d MTNN v5 embedding. “How his game changed” compares first vs last charted season only; traits that moved less than half a standard deviation are left out.

05

League drift — Procrustes

Orthogonal Procrustes on consecutive-season shared players (≥ 30). Rotation = mean principal angle of Q against the identity; residual = Frobenius norm after alignment; chained to the 1996 root frame; RᵀR = I.

The biggest rotation: the summer entering 2019–20 (2018–19 → 2019–20) at 12.49°

Where careers end up, from the same trajectories: 51% of players stay stable specialists, 20% migrate to a neighbouring role, 13% drift, 10% reinvent themselves once, and 5% bloom late. Reinventors last longest — 9.3 seasons against 7.9 for everyone else. The steepest single fall is Interior Finishing + Defensive Glass: 16.5% of minutes early in the window to 8.0% late (−8.6 points). This section is what the Trends page showed until October 2026 — its charts were these numbers, drawn from the same drift series. After 2019–20, the next-largest rotation is 2020–21 → 2021–22 at 11.35°.

06

Skills lens — 0 to 99

Twelve skills, each a fixed linear composite of era-z per-100 stats, expressed as a percentile within the season, so a 90 in 1997 equals a 90 in 2026. Badges start at 90; gold at 97.

recall@10
Can the model find the same player the following season?
purity@20
Do a season’s neighbours share its archetype?
07 · Experimental

Forecast — where a player is headed

Three-season context → next-season embedding (first 8 of 64 dimensions). Source: the served 64-d MTNN v5 embedding, 12,966 × 64. A deterministic numpy forecaster (patch length 3, cross-variate least squares, seed 7), 7,096 windows, chronological split — train ≤2021, validation 2022–23, test ≥2024; the metrics stored in the file are train-split, research-only. No quantile calibration yet. Not TimesFM-3 (non-commercial) or a validated TimesFM-2.5.

One contract everywhere: assets/timesfm_forecasts.json, 7,096 forecasts, with an honest failure message if the file is missing.

Future work: chronological, player-aware splits with no leakage; saved seeds and weights; naive-last and drift baselines; MAE / RMSE plus angular error; quantile coverage.

08

Accuracy harness — blocks a deploy on failure

V1vectors.json dimensions, ranges, no duplicates
V2Cluster labels recomputed by nearest centroid; all 12,966 match
V3deadline.json deltas recomputed from raw logs, 0.01 tolerance
V4Chimera determinism across 30 dates; cosine < 0.3 between donors

MTNN extras: leak-free recall@10 via leakfree.py, purity@20, next-profile MAE. ONNX 548 KB.

08+

The model, graded — the Lab, on this page

The Lab and Methods are one page now: the scorecard, the architecture and the live exhibits sit under the same recompute promise as everything above.

Promote-gate baseline, re-anchored 2026-07-24. The old baseline was measured on the pre-protocol loop that trained on held-out positives — its recall@10 of 1.0 was memorization, so the gate demanded ≥0.98 and no honest model could ever pass it. The current baseline is leak-free and averaged across seeds; it is computed by the gate at promotion time (pipeline/composite_score.py), not republished here as decimals.

leak-freeCQS baseline protocol
multi-seedmean baseline
player-splitno train/test leakage
2× SEclearance over seed noise

Promoted 2026-07-25: mtnn_v5_concat_b2_h160_t32_d64_mlp128_fus256, replacing the 2026-07-14 build. On the identical held-out protocol (assets/eval_scoreboard.json) it nearly quadruples the committed transparent baseline on never-trained 2024+ pairs — top-5 0.196 → 0.757, top-1 0.079 → 0.438 (baseline: the 14-d era-z profile in vectors.json). The previous build predated the position-label, career-feature and durability-leak fixes.

Served-model scorecard — MTNN v5, built 2026-07-25 (assets/mtnn_meta.json): test recall@10 0.846 (held-out 2024+ targets) · cross-era purity@20 0.783 · archetype top-1 0.970 (training snapshot). Held-out retrieval detail:

…held-out top-5 retrieval
…held-out top-1
…14-d baseline top-5
…eligible pairs

Loading assets/eval_scoreboard.json…

Training Cockpit • MTNN v5 — served · v6 — research

Training cockpit — what ships now and what trains next

Current deployed — mtnn_v5_concat_b2_h160_t32_d64_mlp128_fus256

Loading mtnn_arch.json…

CQS weights from pipeline/composite_score.py: recall 0.18, purity 0.16, margin_14d 0.08, archetype 0.08, position 0.05, skills_r2 0.14, skill_nn 0.05, next_r2 0.12, next_mae 0.06, aux_r2 0.08. Gate re-anchored to a leak-free baseline (multi-seed mean, 130-feature matrix). Thresholds are now 2× the standard error of the seed mean rather than a flat 0.02 — seed-to-seed spread on test recall is wide, so the old slack was adjudicating sampling noise. A candidate must also hold same-player continuity spread under its bar, which is what catches a model that memorizes the training window instead of generalizing.

Hill-climb 01→05 + loss weights

01Gatherstats.nba 12,966
→
02Vectorsper-season z ±4 + mask
→
03Towers17×160→32 ×2 blocks
→
04Fusion556→256→64 L2
→
05DeployONNX + WASM + Procrustes

The served champion: 17 towers × 2 residual blocks, 160 hidden → 32 out (LN + GELU + skip) = 544, + 12-d season embedding = 556 concat → 256 fusion → 64-d L2 unit sphere. One embedding feeds every head — 8 archetype + 5 position + 14 next-profile + 18 skills. It holds 12,966 player-seasons, and Procrustes-chained drift maps any season into the 1996-97 root frame.

Research candidate — not served: an MTNN v6 transformer-fusion build (d-model 128, 64-d out) is specified in assets/eval_scoreboard_v6.json — its own status there is candidate, not trained to completion, with projected rather than measured metrics. Production serves ONE model only — MTNN v5 above. Nothing becomes champion without a leak-free held-out win over v5.

Exact v6 candidate training command (research — not the served model)

python pipeline/train_mtnn.py --epochs 150 --dim 64 --tower-width 40 --tower-hidden 192 --tower-blocks 3 --mlp-heads --d-head-hidden 128 --fusion transformer --d-model 128 --n-fusion-layers 4 --n-attn-heads 4 --fusion-hidden 512 --lr 0.001 --lr-schedule onecycle --warmup-pct 0.1 --anneal-strategy linear --weight-decay 0.0002 --nce-loss hybrid --nce-player-weight 0.65 --nce-arch-weight 0.35 --hard-neg-boost 0.4 --drop-p 0.15 --checkpoint-metric cqs --val-every 5 --era-align procrustes --robust-scaling --phase auto

Preprocessing: Procrustes chains drift.json back to 1996-97 root RᵀR=I, robust median/IQR clip[-3,3]. Loss: NCE hybrid player 0.65 arch 0.35 + archetype/position/profile/next/skills/salary/team_fit/roster_lift/form_recon/career_slope/competition/pedigree/playoff/honors. Split: player leakfree. Optim: AdamW no decay bias/LN. Scheduler: OneCycle warmup 10% linear.

Forecast — same family, one season forward Where you're headed

A deterministic TimesFM-lite forecaster (numpy, seed 7) projects player embeddings one season forward from 3-season windows — 7,096 forecasts served from assets/timesfm_forecasts.json (built 2026-09-02), The metrics in that file are train-split, so this stays research-only: it does not drive the map or any prediction product. Stone dashed = uncertainty, terracotta dashed = headed direction. ONE champion served — MTNN v5.

8-d forecast head7,096 forecastsresearch-onlyPast → Modern → Headed
Loading forecast…
Research — in progress

The Frontier Program

Four bets, each with a held-out eval gate. Nothing ships without beating the baseline that matters.

01 · Mixed-curvature embeddings

Player similarity isn't one geometry. Archetype hierarchies are tree-like, era drift is flat, matchup cycles are spherical — so we're learning a product space (hyperbolic × spherical × Euclidean) with curvatures the data chooses, not ones we guess. Challenger to our v5 embeddings, gated on a δ-hyperbolicity diagnostic and twin-retrieval evals.

02 · DARKO-style game forecasting

Per-player Bayesian state estimation — exponential decay plus Kalman filtering over every box score — with who-guards-whom matchup tensors and a minute-projection engine conditioned on blowout probability. The literature's verdict is blunt: minutes move the needle more than model cleverness. Evaluated against the betting line, never a naive baseline.

03 · Live in-game updating

Bayesian second-half projections from live play-by-play: current stat plus remaining minutes at current usage, pace, and foul state, shrunk toward the pre-game prior — because a hot first half predicts a cooler second half, not a continuation. Published baselines for this are few and far between — we're building a public one.

04 · Hyperbolic representation learning

The GGBall paper's hyperbolic VQ-VAE, repurposed as an encoder for the archetype taxonomy — the one job hyperbolic geometry was born for. Pursued only if the δ diagnostic says the data wants curvature. (We checked the hype first: the paper's "18%" claim appears in neither its abstract nor its full text.)

Full provenance for everything in the Lab: the sections at the top of this page · The paper-trading experiment: Props Lab.

Architecture — 4 animations • code-generated

How MTNN actually works

MTNNFlow — glass box
17 towers, 130 feats cat([x·m,m]) → 17× 160→32 LN+GELU ×2 blocks → 544 + season emb 12 → 556→256 fusion → 64 L2 → heads 8/5/14/18. Every box is a real dimension. Renders the served MTNN v5 architecture. Injury is a durability read-out family, not an input tower.
ChimeraEquation — how Chimera scores
Donor A + Donor B → fuse in 64-d → nearest real season among 12,966 by cosine. The argmin behind daily puzzle. MTNN v5 · 64-d
InputFamilies — 130 feats → 17 families → cat([x·m,m])
volume·play·reb·def·eff·shotmix·bio·tracking*·form*·market·roster·career·comp·team·pedig·playoffs·honors =130. Missing tracking pre-2013 masked to zero.
EmbeddingL2 — 64-d → L2 → unit sphere
v̂ = v/||v||₂, ||v̂||=1, cos = v̂·ŵ. 12,966 points on sphere powers Chimera + Era Twin.

Data flow — truthful boxes measured

Input mask m∈{0,1} • cat([x·m,m]) → 17 towers b2 h160 → t32 ×2 residual LN GELU + skip → 544 + season 12 =556 concat → 256 fusion → L2 embed 64-d → MLP heads 8/5/14/18. Tap any node. MTNN v5 champion, 12,966×64.

Tap a node to see what fed it.

Loading tower activations, family share per head…

Tap an input, tower, or output to see exact numbers.

Guess & grade — heads decode the 64-d MTNN v5 embedding

Same MLP heads as rightmost column. Tap a row to lock trace back through network.

Archetype — 8

Position — 5

Skill grades — 18×(64→16→1)

Next-season forecast — per 100

Explorer • Glass box debugger

Probe any player-season

Loading index…
No compare

Loading architecture… MTNN v5: 17 towers 160→32, LN GELU ×2 blocks, 12-d season emb, 544+12=556 → 256 fusion → 64 L2. Era-honest sigma per-season, masked, Procrustes-chained drift root 1996-97 Q^T Q=I.

Loading story…

Compare off — toggle to diff two seasons.

Embedding map — 64-d MTNN v5 rendered in 3D

Slow auto-rotate • drag to orbit • scroll to zoom • orange=selected — 12,966 points L2-normalized.

    Loading nearby players…

    Lab pipeline 01→05

    From box score to fingerprint

    01 Gather → 02 Vectors

    Pull every per-100 split from stats.nba.com 1996-2026. 12,966 player-seasons after the schedule-aware eligibility filter (games + minutes bars, 450-minute floor). Per-season z-score so 1999 and 2026 are both 0 = average of their era. Mask m creates cat([x·m,m]) so model knows never-measured vs zero.

    • volume, playmaking, rebounding, defense, efficiency, shotmix
    • bio age/height/weight/draft, tracking 2013+ masked, form 2015+ masked
    • market salary, roster complement, career lag cosine, competition SOS, team, pedigree, playoffs, honors

    03 Train → 04 Export → 05 Deploy

    Residual towers 17×160→32 ×2 LN GELU + skip =544, + season emb 12 =556 concat, fusion 256 → 64 L2. 17 families, cat([x·m,m]) masked, Procrustes chained RᵀR=I root 1996-97 so Era Twin compares directly. Heads MLP decode 8/5/14/18. Forecasts research-only. ONNX 548 KB runs in the browser (WASM).

    Shipped spec reproducible

    Arch spec — mtnn_arch.json

    Loading mtnn_arch.json…
    09

    Attribution and limits

    • No lineup or on/off data — the model doesn’t know who shared the floor.
    • A midseason move is a TEAM_ID change, not verified trade metadata.
    • Tracking exists only from 2013; earlier seasons are masked, not imputed.
    • Position coverage is high but not total; unlabelled rows are masked, not imputed.
    • Archetype names describe centroid stats, not scouting reports.
    • A Chimera’s fused vector never existed as a real season.