Method & data sources
Doctrine: every number a player sees must be recomputable from source; every mechanic states its rules; nothing is vibes. Games use 14 era-z per-100 features for speed + honesty. The research model MTNN v4 expands to 120 feats / 17 families → 48-d L2 for neighbor search, archetype scoring, and daily puzzles. Both live in the same era-honest space. Glass-box: Inside the Network → 17×160→32 towers
Solo personal project, no connection to employer, built with public/free-tier only • Okabe-Ito AAA palette • 18px/1.65 body • mobile-first 56px tabs safe-area • best-app-ever polish • ONNX WASM 2MB distilled, localStorage only.
The vector space — two views, one honest frame
Games (fast, transparent): 12,392 player-seasons 1996-97 → 2025-26 from stats.nba.com per-100-poss splits, MIN ≥800. Each season 14 features, z-scored within its own season (era-honest). This is what you play with.
| Feature | What it measures | Era note |
|---|---|---|
PTS | scoring volume per 100 | pace-adjusted at door |
AST | playmaking | raw per 100, era-z |
OREB / DREB | off / def glass | boards per 100 |
STL / BLK | def events | steals, rim protection |
TOV | turnovers | negative signal |
FG3A / FGA / FTA | shot mix + rim pressure | volume, era-z |
FG3_PCT / FG_PCT / FT_PCT | accuracy | empirical-Bayes shrunk by attempts → shown as percentile, not fake % |
PLUS_MINUS | on-court impact | per 100, not RAPM |
Cleaning: dedupe (player,season), missing→season mean + mask m∈{0,1} so we track cat([x·m,m]), attempt-weighted EB on %s, clip ±4σ.
MTNN v4 expanded — 120 feats / 17 families
Same 12,392 seasons but richer: volume, playmaking, rebounding, defense (STL/BLK/DEF_RTG), efficiency (EFG%/TS%/OFF_RTG/NET_RTG), shotmix, bio (AGE/HEIGHT/WEIGHT/DRAFT), tracking (2013+ masked), form (FORM_DD), market, roster, career (YEAR_IN_LEAGUE/LAG1_COSINE/DELTA_NORM), competition (SOS_NET_RTG/B2B_RATE), team, pedigree, playoffs, honors. Each family = own residual tower.
Reconciled: 14 = game distillation for speed + transparency. 120 = research MTNN that powers daily puzzles. Cosine in 48-d decides neighbors in both, root-frame Procrustes aligned.
Three words this page keeps using
- z-score
- How far above/below average in “typical gaps.” 0=average, +1 solid notch, +2 elite. Two-thirds sit –1…+1. Cap ±4.
- era-z
- Compared only to players he faced that season. A 1997 center and 2026 guard share one honest space because each is scored vs own league first.
- mask
- “Never measured” vs “measured zero.” Tracking pre-2013 = masked, not guessed.
- per 100
- What he does with ball he gets — not per-game inflated by pace. 30 pts ≈37 per 100.
MTNN v4 — the glass-box behind every guess
17 stat families each run through own residual tower → fuse into 48-number fingerprint (L2-normalized, cosine = similarity) → decode into archetype 8 / position 5 / skill grades 18×(48→16→1) + per-100 forecast. See it live: Inside the Network → click any node to probe family share and signed grad×input pushes.
Training: era-honest σ per season, Procrustes RᵀR=I chained to 1996 root frame drift ↓18%, RealMLP robust scaling, leak-free split via pipeline/leakfree.py so recall isn't inflated. Asset mtnn_embeddings.f32 48-d per season powers Chimera/Era Twin/Team labs.
The map
3D league map in-game projects 14-d (or 48-d MTNN) down to 3 axes via PCA, minmax [0,1] for rendering. Client projection = exact affine recovery of build-time PCA + minmax, not re-fit. Axes named from correlations:
- PC1 — Paint vs perimeter: bigs (boards, blocks) ↔ shooters (3PA, 3P%)
- PC2 — Scoring load: high-usage scorers (PTS,FGA,FTA) ↔ low-usage role players
- PC3 — Ball in hand: off-ball low-event ↔ handlers (AST,STL,TOV)
Okabe-Ito AAA + shape + icon + text triple-encoding: color never alone. In /model the 3D map uses auto-rotate slow, drag, scroll zoom, hover name+archetype, orange selected.
Archetypes — 8 global player types
8 k-means clusters at build time on same 14-d era-normalized vectors, seeded reproducible. Names = strongest centroid traits, not scouting:
- Three-Point Accuracy (Low Turnovers)
- Scoring Volume + Shot Volume
- Defensive Glass + Rim Pressure (FTs)
- Three-Point Volume + Three-Point Accuracy
- Offensive Glass (Low On-Court Impact)
- Rim Protection + Offensive Glass
- Offensive Glass + Defensive Glass
- Playmaking + Steals
Client never re-derives membership — uses pipeline's label, verified all 12,392 via harness V2. Trend Research adds layer 2: K=8 re-fit within five era windows, named top-2 sigma, lineage = nearest predecessor-era centroid cosine in Procrustes root frame.
layer 1: per-season share 8 global k-means (labels from vectors.json, no re-fit); layer 2: k-means K=8 re-fit within five era windows (seeded), named from top-2 centroid sigmas; lineage = nearest predecessor-era centroid cosine in Procrustes root frame; shares of charted MIN≥800 players — stated scope
The Deadline — midseason team change
Midseason move = in-season TEAM_ID change with ≥15 games and ≥12 MPG both sides, 2015-16 → 2025-26 game logs (nba_api PlayerGameLogs). Deltas: per-36 points, context-adjusted plus-minus, points-per-shot-attempt proxy before vs after.
333 movers analyzed → daily-set pool 25 thrived / 25 cratered. Composite score = stated blend, not truth claim. “Midseason move” ≠ officially reported trade; project has only team-change dates.
Fader or Finisher
Split each player-season at own game-sequence midpoint (not calendar ASB) with ≥25 games and ≥12 MPG required both halves, per-36 rates, 2015-16 → 2025-26. Quiz pool 600 rounds limited to unambiguous deltas 1.5–6.0 per-36 swing, so ties/noise never appear.
The Skills Lens — 0–99 grades + badges
Every charted season graded 0–99 on twelve skills. Each skill = fixed, published linear composite of same era-z per-100 features (e.g. Perimeter Shooting = 0.55·FG3A + 0.45·FG3%), percentile within own season pool — so every era same distribution, 90 in 1997 = 90 in 2026. Badges 90+, gold 97+.
Fused chimera on reveal graded same composites vs pooled all-era quantile table (assets/skill_probe.json) — blend never existed as real season, so reference = all 12,392.
MTNN two scores — plain English
- recall@10 — “can it find same guy next year?”
- Hide next season, ask 10 nearest to this. Recall = how often own next season in top10. Higher better, 1.0 = always.
- purity@20 — “do neighbors look like him?”
- 20 nearest from other eras, fraction sharing archetype. Higher = groups roles not decades. ~0.12 = random (8 types).
- held-out — leak fix
- Earlier training included pairs used to score recall → inflated to ~1.0. Fixed via
pipeline/leakfree.py: trains only on players not graded later. Honest numbers lower and real.
Next-season profile: MTNN next_profile head reads embedding and predicts next season's 14-d era-z vector. Prior seasons show predicted vs actual MAE. Latest 2025-26 shows prediction only — labeled as such. Asset: assets/next_profile_eval.json.
Wide-matrix (masked 2015-16+): Post Play, Transition, Motor, Shooting/Rim/Disruption Gravity from tracking+hustle — stated proxies, not Second Spectrum. Pre-2013 shows “not tracked this era.”
Data sources
- stats.nba.com via
nba_api—leaguedashplayerstats(Base/Advanced/Scoring),leaguedashplayerbiostats,leaguedashptstats(player tracking 2013-14+ masked, never imputed),leaguegamelog+PlayerGameLogs2015-16→2025-26 for Deadline/Fader. - Basketball-Reference — per-season positions 99.7% coverage; contracts where salary history unavailable.
- Every endpoint cached locally; re-runs resume from cache. Throttles respected, builds incremental. Pipeline: Gather → Clean (era-honest σ + Procrustes + RealMLP) → Train (160→32 towers 556→128→48) → Eval (harness) → Deploy (ONNX WASM,
mtnn.js).
The League Drift — Procrustes
Trend Research asks not “how good” but “how much did 14-d vector space itself rotate, season to season.” Quoted verbatim from assets/drift.json:
orthogonal Procrustes on consecutive-season shared players (>=30); rotation = mean principal angle of Q vs identity; residual = normalized Frobenius after alignment; no scaling (z-spaces pre-normalized); chained transforms map any season into 1996-97 root frame; axisDrift = 1-|Q_ii|, stated proxy
Chained root-frame powers Trend Research drift charts; Era Twin uses promoted MTNN embedding for cross-decade matching. See interactive: What changed this season? uses gauge + compass + ranked shift bars.
Career Shapes — how roles move over a career
Trend Research Career Shapes asks: not “what archetype was this season” but “how did own archetype label move across whole career.” Careers ≥4 charted seasons; each keeps per-season global label (never career-static):
- stable — one archetype ≥75% seasons
- reinvention — one sustained switch: modal ≥75% before, different modal ≥75% after
- late-bloom — reinvention with switch ≥60% career index
- migrator — 3+ distinct archetypes, none ≥60%
- drifter — else: 2+ archetypes, no sustained switch
careers >=4 charted seasons; per-season global archetype labels (never career-static); taxonomy rule-based as documented (stable >=75% modal; reinvention = one sustained >=75%/>=75% switch; late-bloom = switch at >=60% career index; migrator = 3+ archetypes none >=60%); era comparison by career-midpoint decade; correlates observed with selection effects — trajectory class is outcome, not assignment
The accuracy harness — blocks deploy on fail
pipeline/verify_accuracy.py gates every deploy — non-zero exit blocks shipping:
- V1 —
vectors.jsonintegrity: dims, ranges, no duplicate (name,season), valid map coords. - V2 — cluster labels: recomputes nearest-centroid from member means, confirms client centroid method reproduces pipeline labels for all 12,392.
- V3 —
deadline.json: recomputes every mover's deltas directly from raw game logs, exact match 0.01 tolerance — 100% sample, not spot-check. - V4 — Chimera determinism: seeded daily target checked across 30 sampled dates for stability and low-similarity (MTNN cosine <0.3) between its two donors.
MTNN extras: recall@10 leak-free, purity@20, next_profile MAE. Bundle ~105KB gz <300KB target, ONNX 549KB. Visual harness: AAA Okabe-Ito, 18px/1.65, 56px tabs safe-area.
Attribution, license & limitations — stated plainly
Attribution: stats from stats.nba.com and Basketball-Reference; derived, era-normalized aggregates, not verbatim scrapes.
License: code all-rights-reserved pending license file. Source data subject to stats.nba.com / BR terms.
What this site is not:
- No lineup/on-off data — nothing claims to measure who shared floor with.
- “Midseason move” = TEAM_ID-change detection, not verified trade metadata.
- Tracking features only 2013-14 onward; earlier = explicit missing mask, not imputed.
- Position coverage 99.7%, not 100%.
- Archetype names = centroid stats, not scouting judgment.
- Skills “Two-Way Impact” = per-100 plus-minus with small scoring stabilizer — not BPM/RAPM, small-minute spike possible.
- Chimera fused vector never existed — its skill pool = all-era.
- MTNN v4 glass-box explorer at /model shows true weights — no hidden layers beyond documented LN/GELU/skip.
Solo personal project, no connection to employer, built with public/free-tier only • Videos relevant: MTNNFlow.mp4 + ChimeraEquation.mp4 only • Manim source: workspace/manim_dumbmodels/ • Free-tier Vercel cleanUrls true.