Skip to main content

NBA — additional Python functions — Analytics: nba_tracking–zone_value

nba_tracking_shot_diet_value​

nba_tracking_shot_diet_value(seasons: "'int | str | list'", *, league_id: 'str' = '00', per_mode: 'str' = 'Totals', by_position: 'bool' = True, positions: 'Optional[pl.DataFrame]' = None, return_as_pandas: 'bool' = False, _get_fn: 'Optional[Callable[..., dict]]' = None) -> "'Union[pl.DataFrame, pd.DataFrame]'"

Catch-&-shoot vs pull-up points-over-expected, per player-season.

Fetches CatchShoot and PullUpShot (two calls), scores each with the shared engine, joins on player_id (dtype-asserted Utf8 both sides first), and computes shot_diet_delta = (cs_pts_oe / cs_fga) - (pu_pts_oe / pu_fga) (null-safe on zero attempts) -- positive means the player's efficiency edge comes from catch-&-shoot, negative from off-the-dribble.

Parameters

ParameterTypeDefaultDescription
seasonsint | str | listA single season or list of seasons.
league_idstr'00'"00" NBA (default), "10" WNBA, "20" G-League.
per_modestr'Totals'per_mode_simple passed to each fetch (default "Totals").
by_positionboolTrueCompute each measure's baseline within role buckets (default); False forces one league-wide bucket.
positionsOptional[DataFrame]NoneOptional pre-fetched positions frame.
return_as_pandasboolFalseReturn a pandas.DataFrame instead of polars.
_get_fnOptional[Callable[..., dict]]NoneInjectable replacement for nba_stats_leaguedashptstats, dispatched by the pt_measure_type kwarg for each of the two calls.

Returns

One row per player-season: season:Int64, player_id:Utf8, player_name:Utf8, team_id:Utf8, position_bucket:Utf8, cs_fga:Float64, cs_pts:Float64, cs_pts_oe:Float64, pu_fga:Float64, pu_pts:Float64, pu_pts_oe:Float64, shot_diet_delta:Float64, league_id:Utf8. Empty/malformed input returns a zero-row frame with this schema.

No returns table is published for this function: no capture: it reads stats.nba.com, which answers HTTP 403 to the datacenter IP the docs are built on; the function works from a residential IP.

Example

from sportsdataverse.nba import nba_tracking_shot_diet_value
df = nba_tracking_shot_diet_value(2024)
print(df.sort("cs_pts_oe", descending=True).head())

nba_tracking_touch_value​

nba_tracking_touch_value(seasons: "'int | str | list'", *, league_id: 'str' = '00', per_mode: 'str' = 'Totals', by_position: 'bool' = True, positions: 'Optional[pl.DataFrame]' = None, return_as_pandas: 'bool' = False, _get_fn: 'Optional[Callable[..., dict]]' = None) -> "'Union[pl.DataFrame, pd.DataFrame]'"

Touch / possession-time value over expected, per player-season.

Fetches the Possessions leaguedashptstats measure and computes pts_per_touch_oe = pts - touches * bucket_pts_per_touch. time_of_poss_eff is the z-score of pts / time_of_poss within the player's role bucket -- scoring economy per second of possession, independent of touch volume.

Parameters

ParameterTypeDefaultDescription
seasonsint | str | listA single season or list of seasons.
league_idstr'00'"00" NBA (default), "10" WNBA, "20" G-League.
per_modestr'Totals'per_mode_simple passed to the fetch (default "Totals").
by_positionboolTrueCompute the baseline within role buckets (default); False forces one league-wide bucket.
positionsOptional[DataFrame]NoneOptional pre-fetched positions frame.
return_as_pandasboolFalseReturn a pandas.DataFrame instead of polars.
_get_fnOptional[Callable[..., dict]]NoneInjectable replacement for nba_stats_leaguedashptstats.

Returns

One row per player-season: season:Int64, player_id:Utf8, player_name:Utf8, team_id:Utf8, position_bucket:Utf8, gp:Int64, min:Float64, touches:Float64, pts:Float64, touch_baseline_rate:Float64, touch_expected:Float64, pts_per_touch_oe:Float64, time_of_poss:Float64, time_of_poss_eff:Float64, league_id:Utf8. Empty/malformed input returns a zero-row frame with this schema.

No returns table is published for this function: no capture: it reads stats.nba.com, which answers HTTP 403 to the datacenter IP the docs are built on; the function works from a residential IP.

Example

from sportsdataverse.nba import nba_tracking_touch_value
df = nba_tracking_touch_value(2024)
print(df.sort("pts_per_touch_oe", descending=True).head())

nbadraft_mock_draft​

nbadraft_mock_draft(year: 'Optional[int]' = None, *, proxy: 'Any' = None, return_as_pandas: 'bool' = False) -> 'Union[pl.DataFrame, pd.DataFrame]'

The current consensus mock draft from NBADraft.net.

One row per pick across both rounds. The page renders round 1 and round 2 as the first two pick tables and then repeats round 1 in a third table, so only the first two are taken -- concatenating all three double-counts round 1. The <noscript> fallback is another false-positive JS challenge; the pick tables are static. A traded pick's team cell carries *, which is stripped.

Parameters

ParameterTypeDefaultDescription
yearOptional[int]NoneDraft year (e.g. 2025). None (default) reads the site's current mock; a year uses the /nba-mock-drafts/{year}/ path where NBADraft.net has one.
proxyAnyNoneProxy configuration forwarded to ~sportsdataverse.dl_utils.download (requests proxies= shape).
return_as_pandasboolFalseReturn a pandas DataFrame instead of polars.

Returns

One row per pick with round (1 or 2), pick, team, player, height, weight, position, school and class. An unreachable or table-less page yields a zero-row frame with that schema.

col_nametypedescription
roundInt64Draft round of the pick, 1 or 2 (NBADraft.net's first two pick tables).
pickInt64Pick number within the round.
teamStringTeam-side label or team identifier.
playerStringPlayer name.
heightStringPlayer height (string e.g. '6-2' or inches).
weightInt64Player weight in pounds.
positionStringListed roster position (G, F, C, etc.).
schoolStringPlayer school / pre-draft team.
classStringCollege class / draft eligibility note.

Raises

ExceptionDescription
UserWarningEmitted (not raised) when the page cannot be fetched or carries no pick table; the call still returns an empty frame.

Example

import polars as pl
from sportsdataverse.nba import nbadraft_mock_draft

mock = nbadraft_mock_draft()
print(mock.shape)

# A specific draft year, as pandas

mock_pd = nbadraft_mock_draft(year=2025, return_as_pandas=True)

# Pipeline next step (lottery only)

mock.filter((pl.col("round") == 1) & (pl.col("pick") <= 14))

player_play_context​

player_play_context(possessions: 'pl.DataFrame', *, league_non_transition_ppp: 'Optional[float]' = None, apply_ctg_filters: 'bool' = True, return_as_pandas: 'bool' = False) -> 'Union[pl.DataFrame, pd.DataFrame]'

Per-player offensive On/Off Play-Context table (CTG's On/Off page, offense half).

For each player: their team's offensive play-context with them on the floor (on_*), without them (off_*), and the on-minus-off difference (diff_*) — which is the number CTG actually displays.

The OFF side is derived by subtraction (team total minus on-court), not by a second scan. That is deliberate: it makes the partition exact by construction — on_poss + off_poss == team_poss and the same for points — so a leak (a double-counted possession, a dropped lineup slot) is impossible to hide. The test suite asserts that identity directly.

Like CTG's on/off, this is a raw split: no luck adjustment, no opponent adjustment, no minutes threshold. It is a descriptive difference, not a causal estimate — for that, use the RAPM surface (~sportsdataverse.nba.nba_rapm.nba_rapm).

Requires off_player_1..5 from ~sportsdataverse.nba.nba_possessions.attach_possession_lineups.

Parameters

ParameterTypeDefaultDescription
possessionsDataFrameFrame from add_play_context with lineups attached.
league_non_transition_pppOptional[float]NonePts+/Poss baseline; see team_play_context. One baseline is shared across the on and off sides so the diffs are comparable.
apply_ctg_filtersboolTrueDrop garbage-time / heave / non-counting possessions first.
return_as_pandasboolFalseReturn a pandas DataFrame instead of polars.

Returns

One row per (player, team) with PLAYER_PLAY_CONTEXT_SCHEMA. Empty input returns a zero-row frame with that schema.

col_nametypedescription
player_idInt64Unique player identifier.
offense_team_idInt64Unique identifier for offense team.
on_possInt64
off_possInt64
on_pointsInt64
off_pointsInt64
on_pts_per_100Float64
off_pts_per_100Float64
diff_pts_per_100Float64
on_transition_freqFloat64
off_transition_freqFloat64
diff_transition_freqFloat64
on_transition_pts_per_100Float64
off_transition_pts_per_100Float64
diff_transition_pts_per_100Float64
on_halfcourt_pts_per_100Float64
off_halfcourt_pts_per_100Float64
diff_halfcourt_pts_per_100Float64
on_transition_pts_added_per_100Float64
off_transition_pts_added_per_100Float64

Raises

ExceptionDescription
ValueErrorWhen the off_player_* columns are missing.

Example

poss = attach_possession_lineups(add_play_context(enh), oncourt, enh, home_team_id=home)
onoff = player_play_context(poss)
print(onoff.sort("diff_pts_per_100", descending=True).head())

# Who makes their team run?

print(onoff.sort("diff_transition_freq", descending=True).head())

player_rates​

player_rates(box_logs: 'pl.DataFrame') -> 'pl.DataFrame'

Per-player per-minute rate stats from box logs.

Rows with null, zero or negative minutes (DNPs) are dropped before anything is counted, so games counts only games the player logged minutes in. Rate = total stat / total minutes across those games; minutes_pg is the mean minutes. A traded player gets one row: the rates pool every team's games, and team_id is the team on the player's last kept input row (sort box_logs by date to make that the latest team).

Parameters

ParameterTypeDefaultDescription
box_logsDataFramePer-player-per-game frame with player_id, team_id, minutes, pts, reb, ast, fg3m.

Returns

One row per player: player_id, team_id, games, minutes_pg, pts_per_min, reb_per_min, ast_per_min, fg3m_per_min; games is Int64. player_id and team_id keep the input's dtype (String from the nba_player_props loader; integer ids come back as integers); empty input returns the documented schema, with String ids, and zero rows.

col_nametypedescription
player_idStringUnique player identifier.
team_idStringUnique team identifier.
gamesInt64Games played.
minutes_pgFloat64
pts_per_minFloat64
reb_per_minFloat64
ast_per_minFloat64
fg3m_per_minFloat64

Example

from sportsdataverse.nba.nba_player_props import player_rates
rates = player_rates(box_logs)

players_on_court_from_pbp​

players_on_court_from_pbp(enhanced_pbp: 'pl.DataFrame', raw_box: 'dict', *, home_team_id: 'int', away_team_id: 'int') -> 'pl.DataFrame'

Reconstruct the 5-on-5 on-court lineup from pbp subs + boxscore starters.

Pure function (no network). A gamerotation-free alternative to players_on_court_from_rotation returning the identical LINEUPS_SCHEMA frame (one row per action, slots sorted ascending or None). See the module design for the algorithm.

Parameters

ParameterTypeDefaultDescription
enhanced_pbpDataFrameOutput of enhanced_pbp_from_payload. Must carry game_id, action_number, order_index, period, team_id, person_id, description, is_substitution.
raw_boxdictRaw boxscoretraditionalv3 dict (starters + name map).
home_team_idintHome team id (from boxscore_home_away).
away_team_idintAway team id (from boxscore_home_away).

Returns

polars.DataFrame conforming to LINEUPS_SCHEMA. Empty input returns a zero-row frame (never raises).

col_nametypedescription
game_idStringUnique game identifier.
action_numberInt64Sequential action number within a game (V3 PBP).
periodInt64Period of the game (1-4 quarters; 5+ for OT).
home_player_1Int64
home_player_2Int64
home_player_3Int64
home_player_4Int64
home_player_5Int64
away_player_1Int64
away_player_2Int64
away_player_3Int64
away_player_4Int64
away_player_5Int64

Example

import json, pathlib
from sportsdataverse.nba.nba_enhanced_pbp import enhanced_pbp_from_payload
from sportsdataverse.nba.nba_lineups import (
boxscore_home_away, players_on_court_from_pbp,
)
box = json.loads(pathlib.Path("boxscoretraditionalv3.json").read_text())
pbp = json.loads(pathlib.Path("playbyplayv3.json").read_text())
enh = enhanced_pbp_from_payload(pbp)
home, away = boxscore_home_away(box)
oc = players_on_court_from_pbp(enh, box, home_team_id=home, away_team_id=away)
print(oc.shape)

players_on_court_from_quarter_boxscores​

players_on_court_from_quarter_boxscores(enhanced_pbp: 'pl.DataFrame', period_boxscores: 'Dict[int, dict]', raw_box: 'Optional[dict]' = None, *, home_team_id: 'int', away_team_id: 'int') -> 'pl.DataFrame'

Reconstruct the 5-on-5 on-court lineup, seeding each period exactly where possible.

A structural sibling of players_on_court_from_pbp — same LINEUPS_SCHEMA output, same sub-batching walk / ffill-bfill / ascending sort tail — whose only difference is how each period is seeded: when that period's range-boxscore (_period_box_oncourt) narrows to exactly 5 on-court candidates for a team, the period is seeded EXACTLY from it; otherwise it falls back to the same gamerotation-free first-appearance inference players_on_court_from_pbp uses (_period_starters, carrying the prior period's ending lineup as the silent-starter fallback). See _period_box_oncourt for the narrowing recipe (empirically re-derived against pbpstats' StartOfPeriod._get_starters_from_boxscore_request — see that function's docstring for the concrete evidence behind its zero-sentinel polarity).

Substitution name resolution merges up to three sources via _merge_name_maps: _name_map_from_period_boxes (the union of every period's range-box roster), _name_map_from_pbp_actors (every row's own actor identity — covers bench players who never touch a period boundary but do record at least one action), and — when the caller supplies it — _boxscore_name_map over the full-game raw_box payload, the SAME full-roster source players_on_court_from_pbp uses. That third source is what fixes the one residual name-resolution gap the first two cannot cover: a player who is subbed in and then records zero further pbp actions for the rest of the game (so never appears in _name_map_from_pbp_actors) and never happens to be on court at an exact period-opening tick (so never appears in _name_map_from_period_boxes) is still present in the full-game boxscore roster — which lists every player on both teams regardless of playing time — and therefore still resolvable. Passing raw_box is optional (None preserves the pre-existing two-source behavior) but strongly recommended: without it this producer's per-game agreement with the gamerotation oracle can regress well below players_on_court_from_pbp's own floor on a fixture with a late, stat-less bench appearance (see tests/nba/test_nba_lineups.py::test_quarter_box_agreement_floors).

Parameters

ParameterTypeDefaultDescription
enhanced_pbpDataFrameOutput of ~sportsdataverse.nba.nba_enhanced_pbp.enhanced_pbp_from_payload. Must carry game_id, action_number, order_index, period, team_id, person_id, player_name, player_name_i, description, is_substitution.
period_boxscoresDict[int, dict]{period: raw_boxscoretraditionalv3_range_payload} — one entry per period, captured at that period's _period_start_range window. A missing period key falls back to pbp seeding for that period only (never raises).
raw_boxOptional[dict]NoneOptional raw full-game boxscoretraditionalv3 payload (the same one players_on_court_from_pbp and boxscore_home_away consume) — supplies the full-roster name map described above. None (default) falls back to resolving names from period_boxscores + pbp actors only.
home_team_idintHome team id (from boxscore_home_away).
away_team_idintAway team id (from boxscore_home_away).

Returns

polars.DataFrame conforming to LINEUPS_SCHEMA. Empty enhanced_pbp returns a zero-row frame (never raises).

col_nametypedescription
game_idStringUnique game identifier.
action_numberInt64Sequential action number within a game (V3 PBP).
periodInt64Period of the game (1-4 quarters; 5+ for OT).
home_player_1Int64
home_player_2Int64
home_player_3Int64
home_player_4Int64
home_player_5Int64
away_player_1Int64
away_player_2Int64
away_player_3Int64
away_player_4Int64
away_player_5Int64

Example

import json, pathlib
from sportsdataverse.nba.nba_enhanced_pbp import enhanced_pbp_from_payload
from sportsdataverse.nba.nba_lineups import (
boxscore_home_away, players_on_court_from_quarter_boxscores,
)
box = json.loads(pathlib.Path("boxscoretraditionalv3.json").read_text())
pbp = json.loads(pathlib.Path("playbyplayv3.json").read_text())
periods = json.loads(pathlib.Path("boxv3_periods.json").read_text())
period_boxscores = {int(k): v for k, v in periods.items()}
enh = enhanced_pbp_from_payload(pbp)
home, away = boxscore_home_away(box)
oc = players_on_court_from_quarter_boxscores(
enh, period_boxscores, box, home_team_id=home, away_team_id=away
)
print(oc.shape)

players_on_court_from_rotation​

players_on_court_from_rotation(enhanced_pbp: 'pl.DataFrame', rotation: 'dict[str, list[dict]]', *, home_team_id: 'int', away_team_id: 'int', league_id: 'Optional[str]' = None) -> 'pl.DataFrame'

Reconstruct the 5-on-5 on-court lineup via the rotation (gamerotation) algorithm.

Pure function — no network calls. Port of hoopR's .players_on_court_v3() (R/nba_stats_pbp.R lines 857-1041).

The rotation dict may use either "HomeTeam"/"AwayTeam" or "homeTeam"/"awayTeam" as keys — both are accepted.

Parameters

ParameterTypeDefaultDescription
enhanced_pbpDataFrameOutput of ~sportsdataverse.nba.nba_enhanced_pbp.enhanced_pbp_from_payload. Must contain game_id, action_number, period, seconds_remaining, is_substitution, and team_id.
rotationdict[str, list[dict]]Parsed rotation dict, typically from parse_rotation_resultsets. Each team's list contains stint dicts with numeric PERSON_ID, IN_TIME_REAL, OUT_TIME_REAL.
home_team_idintInteger team ID of the home team.
away_team_idintInteger team ID of the away team.
league_idOptional[str]None"00" NBA, "10" WNBA, "15" Summer League, "20" G League. Sets the period lengths the rotation clock runs on; when None the league is read off the game id.

Returns

polars.DataFrame conforming to LINEUPS_SCHEMA with one row per action in enhanced_pbp (same row count, same ordering). Never raises — an empty or malformed rotation (stints that do not sum to five players' game time, or carry negative / inverted clocks) logs a warning and returns a zero-row frame, so callers fall back to another lineup source.

col_nametypedescription
game_idStringUnique game identifier.
action_numberInt64Sequential action number within a game (V3 PBP).
periodInt64Period of the game (1-4 quarters; 5+ for OT).
home_player_1Int64
home_player_2Int64
home_player_3Int64
home_player_4Int64
home_player_5Int64
away_player_1Int64
away_player_2Int64
away_player_3Int64
away_player_4Int64
away_player_5Int64

Example

import json, pathlib
import polars as pl
from sportsdataverse.nba.nba_enhanced_pbp import enhanced_pbp_from_payload
from sportsdataverse.nba.nba_lineups import (
boxscore_home_away, parse_rotation_resultsets,
players_on_court_from_rotation,
)
box = json.loads(pathlib.Path("boxscoretraditionalv3.json").read_text())
pbp = json.loads(pathlib.Path("playbyplayv3.json").read_text())
rot = json.loads(pathlib.Path("gamerotation.json").read_text())
enh = enhanced_pbp_from_payload(pbp)
home, away = boxscore_home_away(box)
rotation = parse_rotation_resultsets(rot)
df = players_on_court_from_rotation(
enh, rotation, home_team_id=home, away_team_id=away
)
print(df.shape)

prob_over​

prob_over(exp_value: 'float', line: 'float', stat: 'str', *, league_id: 'str' = '00') -> 'float'

Probability a stat finishes strictly above line.

Parameters

ParameterTypeDefaultDescription
exp_valuefloatProjected mean of the stat.
linefloatThe prop line.
statstrOne of "pts", "reb", "ast", "fg3m".
league_idstr'00'Accepted for parity.

Returns

P(stat > line) in [0, 1].

Example

from sportsdataverse.nba.nba_player_props import prob_over
prob_over(24.0, 22.5, "pts")

project_player_line​

project_player_line(rate_row: 'dict[str, Any]', exp_minutes: 'float', pace_factor: 'float' = 1.0) -> 'dict[str, float]'

Project a player's expected counting line from per-minute rates.

exp_stat = rate_per_min * exp_minutes * pace_factor -- counting stats scale with both projected minutes and pace.

Parameters

ParameterTypeDefaultDescription
rate_rowdict[str, Any]One row of player_rates (as a dict).
exp_minutesfloatProjected minutes for the game.
pace_factorfloat1.0Pace multiplier (exp_poss / avg_pace); 1.0 for a league-average-pace matchup.

Returns

{"exp_pts", "exp_reb", "exp_ast", "exp_fg3m"}.

Example

from sportsdataverse.nba.nba_player_props import player_rates, project_player_line
r = player_rates(box_logs).row(0, named=True)
line = project_player_line(r, exp_minutes=32.0, pace_factor=1.02)

prop_distribution​

prop_distribution(exp_value: 'float', stat: 'str', *, league_id: 'str' = '00') -> 'tuple[str, dict[str, float]]'

Distribution family + parameters for a projected stat mean.

Points -> Normal (mu, sd) with sd = a + b*sqrt(mu); counts (reb/ast/fg3m) -> Negative-Binomial (r, p) matching mean mu and variance dispersion*mu (Poisson if dispersion <= 1).

Parameters

ParameterTypeDefaultDescription
exp_valuefloatProjected mean of the stat.
statstrOne of "pts", "reb", "ast", "fg3m".
league_idstr'00'Accepted for parity (dispersion is currently league-shared).

Returns

(family, params) where family is "normal", "nbinom" or "poisson".

Example

from sportsdataverse.nba.nba_player_props import prop_distribution
fam, par = prop_distribution(24.0, "pts")

ratings_as_of​

ratings_as_of(model: 'AnyModel', possessions: 'pl.DataFrame', asof: 'datetime.date') -> 'RatingsFit'

Fit model on every possession dated on or before asof and return ratings.

This is the through-date primitive: possessions with game_date > asof are excluded from the fit entirely (never merely down-weighted), which is what makes the panel built from repeated calls to this function leakage-free by construction — see tests/nba/test_nba_ratings_panel.py::test_ratings_as_of_is_leakage_free_append_invariant. NOTE: the leakage property is proven by the append-invariance test TOGETHER with the panel's per-date-parity test — neither alone covers cross-checkpoint-window leaks; do not prune one without the other.

Parameters

ParameterTypeDefaultDescription
modelAnyModelA harness model conforming to nba_model_validation.AnyModel (a RapmModel, RatingsModel, or PriorModel).
possessionsDataFrameA possession+lineup frame that MUST carry a game_date (pl.Date) column (as emitted by compile_nba_season).
asofdateThe through-date checkpoint (inclusive: game_date <= asof). A pregame consumer of a game on day D passes D - 1 day; the pregame surfaces (nba_player_props, nba_team_ratings) take a strict as_of_date instead.

Returns

RatingsFit with per-player offense/defense ratings (per-100-possession scale, same sign convention as nba_rapm: positive d_ratings means good defense). Empty dicts when no possessions fall on or before asof or when possessions is empty.

Raises

ExceptionDescription
ValueErrorIf possessions (non-empty) does not carry a game_date column — this is a caller-contract violation, not a normal empty case, so it raises rather than silently returning empty ratings.

Example

import datetime
from sportsdataverse.nba.nba_model_validation import RidgeRapmModel
from sportsdataverse.nba.nba_ratings_panel import ratings_as_of

rf = ratings_as_of(RidgeRapmModel(), season_poss, datetime.date(2023, 12, 1))
print(rf.o_ratings[201939]) # per-100 offensive rating through Dec 1

rotowire_injuries​

rotowire_injuries(*, proxy: 'Any' = None, return_as_pandas: 'bool' = False) -> 'Union[pl.DataFrame, pd.DataFrame]'

The current NBA injury report from RotoWire.

One row per injured player: team, position, the injury, the designation (Out / Doubtful / Questionable / GTD / Day-To-Day) and a link to the player's RotoWire page. This is the live replacement for the defunct RotoWorld feed.

The rendered grid at /basketball/news.php?view=injuries builds itself client-side, so this reads the JSON table endpoint the grid calls (/basketball/tables/injury-report.php?team=ALL&pos=ALL) rather than scraping the page. The projected return date is subscriber-gated and comes back as "Subscribers Only"; it is returned as null for non-subscribers.

Parameters

ParameterTypeDefaultDescription
proxyAnyNoneProxy configuration forwarded to ~sportsdataverse.dl_utils.download (requests proxies= shape).
return_as_pandasboolFalseReturn a pandas DataFrame instead of polars.

Returns

One row per injured player with player_id, player, first_name, last_name, team, position, injury, status, return_date and url. An unreachable endpoint or a non-list body yields a zero-row frame with that schema.

col_nametypedescription
player_idStringUnique player identifier.
playerStringPlayer name.
first_nameStringPlayer's first name.
last_nameStringPlayer's last name.
teamStringTeam-side label or team identifier.
positionStringListed roster position (G, F, C, etc.).
injuryStringInjury (body part / description).
statusStringStatus label.
return_dateStringProjected return date as RotoWire shows it; null for non-subscribers (the endpoint sends "Subscribers Only") and when the cell is blank.
urlStringRotoWire player page URL.

Raises

ExceptionDescription
UserWarningEmitted (not raised) when the endpoint cannot be fetched or does not answer with a JSON array; the call still returns an empty frame.

Example

import polars as pl
from sportsdataverse.nba import rotowire_injuries

injuries = rotowire_injuries()
print(injuries.shape)

# As pandas

injuries_pd = rotowire_injuries(return_as_pandas=True)

# Pipeline next step (who is ruled out)

injuries.filter(pl.col("status") == "Out").select("player", "team", "injury")

score_shot_xpoints​

score_shot_xpoints(shots: 'pl.DataFrame', league_avgs: 'pl.DataFrame', *, return_as_pandas: 'bool' = False) -> "'Union[pl.DataFrame, pd.DataFrame]'"

Score each shot with expected points from the league-average baseline.

Joins the per-shot frame to the zone baseline (falling back to the within-shot_zone_range mean when a zone triple is unmatched) and adds shot_value (3 for a 3PT shot else 2), xpoints = base_fg_pct * shot_value, and actual_points = shot_made_flag * shot_value.

Parameters

ParameterTypeDefaultDescription
shotsDataFramePer-shot Shot_Chart_Detail frame (needs shot_type + the three zone keys + shot_made_flag).
league_avgsDataFrameThe LeagueAverages frame (see xpoints_baseline).
return_as_pandasboolFalseReturn a pandas DataFrame instead of polars.

Returns

The input shots plus shot_value:Int64, base_fg_pct:Float64, xpoints:Float64, actual_points:Float64. Empty input returns the augmented schema with zero rows.

No returns table is published for this function: no capture: its input is stats.nba.com shot-chart detail (shot zones and types), which answers HTTP 403 to the datacenter IP the docs are built on and is not in the raw store.

Raises

ExceptionDescription
ValueErrorA zone join key has a different dtype on the two frames.

Example

import polars as pl
from sportsdataverse.nba.nba_shot_value import score_shot_xpoints
scored = score_shot_xpoints(shots, league_avgs)

# Pipeline next step (one line)

scored.group_by("player_id").agg(pl.col("xpoints").sum())

shooter_talent​

shooter_talent(scored_shots: 'pl.DataFrame', *, league_id: 'str' = '00', min_attempts: 'int' = 50, return_as_pandas: 'bool' = False) -> "'Union[pl.DataFrame, pd.DataFrame]'"

Regressed shooter true-talent: make%-above-expected, shrunk to the mean.

Aggregates score_shot_xpoints output per shooter and regresses the raw over-expected rate toward zero by n/(n+k) (k = get_shrinkage_k(league_id), fitted split-half). As-of leakage boundary: to score a shooter's talent for shots after date D, pass only that shooter's shots before D -- this function does not enforce the cut itself.

Parameters

ParameterTypeDefaultDescription
scored_shotsDataFramescore_shot_xpoints output (needs player_id, shot_made_flag, base_fg_pct, xpoints, actual_points).
league_idstr'00'"00" NBA, "10" WNBA, "20" G-League.
min_attemptsint50Drop shooters with fewer attempts (unstable estimate).
return_as_pandasboolFalseReturn a pandas DataFrame instead of polars.

Returns

One row per player_id: player_id:Int64, n_att:Int64, actual_makes:Int64, exp_makes:Float64, points_above_expected:Float64, raw_above_pct:Float64, talent_pct:Float64. Empty input returns the zero-row schema.

No returns table is published for this function: no capture: its input is stats.nba.com shot-chart detail (shot zones and types), which answers HTTP 403 to the datacenter IP the docs are built on and is not in the raw store.

Example

from sportsdataverse.nba.nba_shot_value import score_shot_xpoints, shooter_talent
talent = shooter_talent(score_shot_xpoints(shots, league_avgs))

# Pipeline next step (one line)

talent.sort("talent_pct", descending=True).head(15)

shot_selection_quality​

shot_selection_quality(scored_shots: 'pl.DataFrame', *, min_attempts: 'int' = 50, return_as_pandas: 'bool' = False) -> "'Union[pl.DataFrame, pd.DataFrame]'"

Player shot-selection quality: mean expected value vs the league mean.

xev_per_shot is a player's mean xpoints (the value of the LOOKS they take, independent of makes); selection_quality is that minus the league-wide mean xpoints over the same frame -- a rim-and-three diet scores positive, a mid-range diet negative.

Parameters

ParameterTypeDefaultDescription
scored_shotsDataFramescore_shot_xpoints output (needs player_id, xpoints).
min_attemptsint50Drop players with fewer attempts.
return_as_pandasboolFalseReturn a pandas DataFrame instead of polars.

Returns

One row per player_id: player_id:Int64, n_att:Int64, xev_per_shot:Float64, league_xev_per_shot:Float64, selection_quality:Float64. Empty input returns the zero-row schema.

No returns table is published for this function: no capture: its input is stats.nba.com shot-chart detail (shot zones and types), which answers HTTP 403 to the datacenter IP the docs are built on and is not in the raw store.

Example

from sportsdataverse.nba.nba_shot_value import score_shot_xpoints, shot_selection_quality
sel = shot_selection_quality(score_shot_xpoints(shots, league_avgs))

# Pipeline next step (one line)

sel.sort("selection_quality", descending=True).head(15)

shrink_clutch​

shrink_clutch(delta: 'pl.DataFrame', *, league_id: 'str' = '00') -> 'pl.DataFrame'

Empirical-Bayes / James-Stein shrinkage of clutch_delta toward zero.

Per-team sampling variance is σ²_i = scale / clutch_poss (small samples shrink harder); the between-team signal variance τ² is the observed variance of clutch_delta net of mean sampling variance; the shrink factor k_i = τ² / (τ² + σ²_i) and clutch_skill_shrunk = k_i · delta_i.

Parameters

ParameterTypeDefaultDescription
deltaDataFrameOutput of clutch_delta (needs clutch_delta + clutch_poss).
league_idstr'00'"00"/"10"/"20" (accepted for parity; the scale is currently league-shared).

Returns

delta with an added clutch_skill_shrunk column. Empty input returns the input schema plus that column.

No returns table is published for this function: no capture: its clutch frame is built from stats.nba.com leaguedashteamclutch, which answers HTTP 403 to the datacenter IP the docs are built on.

Example

from sportsdataverse.nba.nba_clutch import clutch_delta, shrink_clutch
skill = shrink_clutch(clutch_delta(clutch_frame, baseline_frame))

spotrac_team_cap​

spotrac_team_cap(season: 'Optional[int]' = None, *, proxy: 'Any' = None, return_as_pandas: 'bool' = False) -> 'Union[pl.DataFrame, pd.DataFrame]'

Team salary-cap allocations from Spotrac.

One row per team: cap allocations, cap space, active-player count and average roster age for a season. No API key required.

The page carries a <noscript> fallback that looks like a JS challenge but is not -- the cap table is in the static HTML. The team cell duplicates the abbreviation ("ORL ORL"), so only the first token is kept, and every $-formatted column is parsed to Float64.

Parameters

ParameterTypeDefaultDescription
seasonOptional[int]NoneSeason in 4-digit ENDING-year form (2024 = the 2023-24 season). Defaults to ~sportsdataverse.nba.nba_schedule.most_recent_nba_season.
proxyAnyNoneProxy configuration forwarded to ~sportsdataverse.dl_utils.download (requests proxies= shape).
return_as_pandasboolFalseReturn a pandas DataFrame instead of polars.

Returns

One row per team. Columns follow Spotrac's table -- rank, team, record, players_active, avg_age_team, total_cap_allocations, cap_space_all -- plus season. An unreachable or table-less page yields a zero-row frame with that schema.

col_nametypedescription
rankInt64Rank.
teamStringTeam-side label or team identifier.
recordStringOverall win-loss record.
players_activeInt64Number of active players.
avg_age_teamFloat64Average roster age.
total_cap_allocationsFloat64Total cap allocations (USD).
cap_space_allFloat64Cap space / over-the-cap amount (USD).
seasonInt64Season year.

Raises

ExceptionDescription
UserWarningEmitted (not raised) when the page cannot be fetched or carries no table; the call still returns an empty frame.

Example

from sportsdataverse.nba import spotrac_team_cap

cap = spotrac_team_cap(season=2024)
print(cap.shape)

# As pandas

cap_pd = spotrac_team_cap(season=2024, return_as_pandas=True)

# Pipeline next step (most cap space)

cap.sort("cap_space_all", descending=True).head()

starters_on_court_counts​

starters_on_court_counts(possessions: 'pl.DataFrame', starters: 'dict[int, list[int]]') -> 'dict[int, int]'

Count, per possession, how many starters are on the floor across BOTH teams.

This supplies the second half of CTG's garbage-time rule — "there have to be two or fewer starters on the floor combined between the two teams" — which flag_garbage_time cannot evaluate on its own (the possession frame does not carry who is on the floor).

Parameters

ParameterTypeDefaultDescription
possessionsDataFramePossession frame with the ten on-court columns off_player_1..5 and def_player_1..5 (from ~sportsdataverse.nba.nba_possessions.attach_possession_lineups).
startersdict[int, list[int]]{team_id: [player_id, ...]} — e.g. from ~sportsdataverse.nba.nba_lineups._starters_from_boxscore_v3. Player ids are matched across both teams' starting fives, so the offense/defense split of the lineup columns does not matter.

Returns

{possession_number: starters_on_floor}, each value in 0..10. An empty starters map yields all-zero counts, which would make CTG's <= 2 clause vacuously true and flag every margin-qualifying possession. The counts are reported honestly rather than guessed — do not pass an empty map and then read the result as CTG-exact.

Raises

ExceptionDescription
ValueErrorWhen the off_player_* / def_player_* columns are absent.

Example

counts = starters_on_court_counts(poss, _starters_from_boxscore_v3(box))
print(max(counts.values())) # 10 at the opening tip

team_pace_projection​

team_pace_projection(home_team_id: 'str', away_team_id: 'str', ratings: 'pl.DataFrame', *, league_id: 'str' = '00') -> 'float'

Expected possessions for a matchup (Phase-3 expected_possessions).

Parameters

ParameterTypeDefaultDescription
home_team_idstrHome team id (matched against ratings['team_id']).
away_team_idstrAway team id.
ratingsDataFrameOne row per team with team_id, adj_pace.
league_idstr'00'"00"/"10"/"20".

Returns

Expected possessions for the game.

Example

from sportsdataverse.nba.nba_player_props import team_pace_projection
poss = team_pace_projection("1", "2", ratings)

team_play_context​

team_play_context(possessions: 'pl.DataFrame', *, league_non_transition_ppp: 'Optional[float]' = None, apply_ctg_filters: 'bool' = True, return_as_pandas: 'bool' = False) -> 'Union[pl.DataFrame, pd.DataFrame]'

Roll possessions up into CTG's team Play-Context table.

Reproduces the offensive half of CTG's /stats/league/context page.

Columns: poss, points, pts_per_100, transition_poss, transition_points, transition_freq, transition_pts_per_100 (CTG's "Eff"), non_transition_pts_per_100, transition_pts_added_per_100 (CTG's "Pts+/Poss"), plus halfcourt_* twins and per-source transition frequencies (freq_off_steal / freq_off_live_rebound).

Pts+/Poss is the subtle one. CTG: "CTG takes a team's points per possession that starts with transition, and subtracts out what an average team does in a possession that did not start with transition. ... We take the

Parameters

ParameterTypeDefaultDescription
possessionsDataFrameFrame from add_play_context.
league_non_transition_pppOptional[float]NoneLeague-average points per 100 possessions on non-transition-start possessions. Computed from the frame when omitted.
apply_ctg_filtersboolTrueDrop garbage-time, heave and non-counting possessions first (CTG's default view). Set False for the unfiltered totals.
return_as_pandasboolFalseReturn a pandas DataFrame instead of polars.

Returns

One row per offense_team_id. Empty input returns a zero-row frame.

col_nametypedescription
offense_team_idInt64Unique identifier for offense team.
possInt64Poss.
pointsInt64Points scored.
transition_possInt64
transition_pointsInt64
halfcourt_possInt64
pts_per_100Float64
transition_freqFloat64
transition_pts_per_100Float64
non_transition_pts_per_100Float64
freq_off_stealFloat64
freq_off_live_reboundFloat64
transition_pts_added_per_100Float64
halfcourt_pts_per_100Float64

Example

ctx = team_play_context(add_play_context(pbp))
print(ctx.select("offense_team_id", "transition_freq", "transition_pts_added_per_100"))

# Season-comparable Pts+/Poss

ctx = team_play_context(season_poss, league_non_transition_ppp=104.8)

xpoints_baseline​

xpoints_baseline(league_avgs: 'pl.DataFrame') -> 'pl.DataFrame'

League-average FG% baseline table keyed by the three shot-zone columns.

Parameters

ParameterTypeDefaultDescription
league_avgsDataFrameThe LeagueAverages result set from nba_stats_shotchartdetail (shot_zone_basic / shot_zone_area / shot_zone_range / fga / fgm / fg_pct).

Returns

One row per (shot_zone_basic, shot_zone_area, shot_zone_range): ... base_fg_pct:Float64, is_three:Boolean (is_three = the basic zone names a three). Empty input returns the zero-row schema.

col_nametypedescription
shot_zone_basicStringShot zone (e.g. 'Restricted Area', 'Mid-Range', 'Above the Break 3').
shot_zone_areaStringShot zone area ('Left Side', 'Right Side', 'Center').
shot_zone_rangeStringShot zone range ('Less Than 8 ft.', '8-16 ft.', '16-24 ft.', etc.).
base_fg_pctFloat64
is_threeBoolean

Example

from sportsdataverse.nba import nba_stats
from sportsdataverse.nba.nba_shot_value import xpoints_baseline
base = xpoints_baseline(league_avgs)

zone_value_map​

zone_value_map(scored_shots: 'pl.DataFrame', *, return_as_pandas: 'bool' = False) -> "'Union[pl.DataFrame, pd.DataFrame]'"

Per-player per-zone value map: points and expected points per shot.

Collapses shot_zone_basic to a canonical zone via ZONE_COLLAPSE (the two corner-3 zones merge) and aggregates realized vs expected points per shot in each zone.

Parameters

ParameterTypeDefaultDescription
scored_shotsDataFramescore_shot_xpoints output (needs player_id, shot_zone_basic, shot_made_flag, actual_points, xpoints).
return_as_pandasboolFalseReturn a pandas DataFrame instead of polars.

Returns

One row per (player_id, zone): player_id:Int64, zone:Utf8, att:Int64, makes:Int64, pts:Float64, pps:Float64, xpps:Float64, pps_above_expected:Float64 (pps = points per shot, xpps = expected). Empty input returns the zero-row schema.

No returns table is published for this function: no capture: its input is stats.nba.com shot-chart detail (shot zones and types), which answers HTTP 403 to the datacenter IP the docs are built on and is not in the raw store.

Example

import polars as pl
from sportsdataverse.nba.nba_shot_value import score_shot_xpoints, zone_value_map
zmap = zone_value_map(score_shot_xpoints(shots, league_avgs))

# Pipeline next step (one line)

zmap.filter(pl.col("zone") == "corner_3").sort("pps_above_expected", descending=True)