Run NBA GESTALT Locally
NBA GESTALT is a local interactive Matchup Lab built around the completed 2025-26 NAIL-RAPM v1.2 state. This release carries a returner's last observed profile through an injury or other non-playing gap instead of replacing it with a generic cold-start profile. The browser app and API are separate processes during development.
Install dependencies
From the repository root, install the Python environment:
uv sync --group dev
Install the frontend dependencies once:
cd apps/lineup-explorer
npm install
Start the API
In a terminal at the repository root:
uv run nba-gestalt-api --port 8001
The API listens at http://127.0.0.1:8001. It loads the latest published
forward_nail_rapm_v1212_residualized_lambda/2025-26
artifact on its first request. It retains that completed NAIL-RAPM v1.2.1.3 state
in memory for the lifetime of the process. It does not read raw possession
data or retrain a model for each browser session.
Materialize descriptive O/D ratings
The Rankings page, player biographies, and Matchup Lab can additionally show
the descriptive Constrained Split NAIL offense/defense allocation for every
completed player-season. It is a sidecar to the active scalar NAIL release:
the API rejects it unless its production run ID matches and every player obeys
offense + defense = NAIL.
uv run nba-build-constrained-split-cache
This does not retrain NAIL or change the GESTALT score. The 2026-27 preseason preview intentionally remains scalar-only until a matching completed O/D state is available.
Restart Both Services
To stop listeners on the local API and frontend ports, then start both services in the background with readiness checks:
./scripts/restart_gestalt_local.sh
The script writes logs to artifacts/logs/gestalt-api-local.log and
artifacts/logs/gestalt-web-local.log.
Materialize observed lineup rankings
The Lineups page reads precomputed, retrospective regular-season five-man tables. Build the current season with:
uv run nba-build-gestalt-lineup-rankings --season 2025-26
Build the historical archive, from 1997-98 through 2025-26, with:
uv run nba-build-gestalt-lineup-rankings --all-seasons
Each table uses that season's completed-fit player ratings and contextual model, while player and context edges are weighted by the opponents a unit actually faced. The initial 1996-97 fit is excluded from the Lineups page because it initializes the forward context state and has no completed contextual model. Player Rankings nevertheless include 1996-97 as the initial, zero-prior RAPM fit; its additive-profile adjustment is zero.
Start the Lineup Lab
In a second terminal:
cd apps/lineup-explorer
npm run dev
Open http://127.0.0.1:5174. Vite proxies /api
requests to the local API on port 8001, while the Zensical documentation site
continues to use port 8000.
Both lineup columns start empty. Each has a dice control that fills that side's empty slots with a random sample from the upper possession quartile, preserving already selected players and excluding the other side's selected players. The dice control disables once the side is full. Its trash control clears that side only.
Historical and mixed-era matchups
Each lineup column has an independent Season selector. A selected unit is always drawn from exactly one completed-fit season, but the two units may come from different seasons. The Evaluation era control chooses which season's contextual functions score their profiles:
- Your unit applies your unit's season-specific composition and matchup functions.
- Opponent unit applies the opponent's season-specific functions.
- Neutral averages those two directional estimates. It is symmetric, but is not yet a separately trained era-neutral model.
Player ratings remain tied to each unit's own completed season. The Lab uses the selected season's realized, statistically padded player profiles for its context calculation, while the environment changes only the contextual response surface. This makes a historical matchup internally retrospective rather than combining completed player ratings with prior-season profile inputs.
Materialize the compact cache of realized profiles for every completed season:
uv run nba-build-gestalt-realized-profiles
This cache is display-only. The frozen prior-season profiles used by leaderboard and preseason-prediction workflows remain a separate artifact contract.
Warm all completed historical response surfaces once with:
uv run nba-build-gestalt-response-cache --all-seasons
Materialize player-team splits
Player profile histories show the regular-season team split for players who appeared for more than one club. The primary label remains the team with the most reconstructed on-court possessions, but the table lists every team with its on-court possessions and games. Build the cached profile metadata with:
uv run nba-build-gestalt-player-team-splits
This is a display artifact only: a player's NAIL-RAPM remains one completed season-wide estimate across all of that player's team stints.
Current model contract
The initial Lineup Lab is intentionally defined as a retrospective 2025-26 explorer. A neutral-court matchup estimate is:
\hat{r}_iis the completed 2025-26 bounded portable-model player coefficient for playeri.h(A)-h(B)is the portable composition advantage against the completed 2025-26 possession-weighted reference unit field.q(A,B)is the opponent-specific matchup adjustment after portable composition has been removed.- No home-court term is applied in the Lineup Lab.
This is not a 2026-27 forecast and is not an observed lineup net rating. It is
a portable, retrospective model estimate for a user-selected matchup. The
result panel displays additive player value, portable composition, and the
specific matchup adjustment separately. Its context signals show all material
feature contributions to each term: lineup composition factors sum to
h(A)-h(B), while matchup factors sum to q(A,B). They are exact
orientation-symmetrized spline-Ridge contributions over the original context
features, with each h contribution averaged over the frozen reference-unit
field. Composition sparklines show the portable per-feature response against
the reference field, with orange for the selected unit and a dark outlined
marker for the opponent. Matchup sparklines hold the opponent's feature value
fixed and vary the selected unit. The shaded band is the fitted
possession-weighted 5th-to-95th side-feature support.
API contract
| Route | Purpose |
|---|---|
GET /api/health |
Loaded artifact identity and player count. |
GET /api/players?q=<query>&season=<season> |
Accent-insensitive player search over one completed-fit season pool. |
GET /api/players/{player_id} |
One display profile. |
GET /api/rankings |
Completed-fit player rankings. |
GET /api/lineups |
Materialized observed five-man rankings; accepts season, minimum_possessions, and repeated player_id parameters. |
GET /api/default-opponent?season=<season> |
Random five-player sample from that season's upper possession quartile. |
POST /api/matchups |
Score two distinct five-player units. |
POST /api/matchups accepts:
{
"unit_player_ids": [203999, 201939, 202691, 203110, 202710],
"opponent_player_ids": [2544, 1629029, 1630162, 203507, 203954],
"unit_season": "2017-18",
"opponent_season": "2025-26",
"environment": "neutral"
}
The response includes the additive margin, portable composition margin, specific matchup adjustment, total contextual adjustment, final net-rating estimate, player profiles, and separate feature-level contributions for the composition edge and matchup bonus. Each contribution list reconstructs its corresponding displayed context term.
Verify changes
Run the backend contract tests and build the frontend before committing:
uv run ruff check src/nba_lineup_model/web_api tests/test_web_api.py
uv run pytest tests/test_web_api.py
cd apps/lineup-explorer && npm run build