Fit a Ridge-Regression RAPM Model from Possession Data
Fit a Ridge-Regression RAPM Model from Possession Data
Arguments
- possessions
A possession-level stint matrix as produced by
nba_possession_lineups(), with columnsoff_player_1throughoff_player_5,def_player_1throughdef_player_5(integer NBA Stats person IDs), andpoints(numeric, points scored on that possession).- ...
Reserved for future keyword arguments (currently ignored).
Value
Returns a data.frame with one row per player:
| col_name | types | description |
| player_id | integer | NBA Stats person ID. Rows are sorted ascending by player_id. |
| o_rapm | numeric | Offensive RAPM (per-100-possession points added on offense). Positive = better offensive player. |
| d_rapm | numeric | Defensive RAPM (per-100-possession points saved on defense). Positive = better defensive player (sign is flipped so good defense is positive). |
| rapm | numeric | Total RAPM = o_rapm + d_rapm. Positive = net positive impact. |
| off_poss | integer | Number of possessions the player appeared on offense. |
| def_poss | integer | Number of possessions the player appeared on defense. |
Returns a 0-row frame with the same schema when input is empty or all possessions have NA lineup cells (never-raise).
Note: RAPM is expressed in per-100-possession units. A full season of possessions (~5,000-8,000) is needed for statistically meaningful estimates. Results from a single game (~150-250 possessions) are highly unstable and are provided here for pipeline illustration only.
Values are relative to average: an unpenalized intercept absorbs the mean
points per possession, and the ridge penalty applies equally to the raw
player indicators (no standardization), the same model as
sportsdataverse-py. glmnet and sklearn scale and choose the penalty
differently, so the two agree closely but not exactly. The data do not fix
the overall level of the offensive or defensive block (nor, therefore, of
rapm); the equal penalty sets it so that the plain average of
o_rapm over players, and of d_rapm, is 0. Possession-weighted
means are not fixed by the model: if playing time is correlated with
player quality they can be well away from 0. On the 2025-26 NBA regular
season they were +0.30 (O-RAPM) and +0.07 (D-RAPM).
Results are deterministic: the cross-validation uses fixed, construction-based folds (not random), so repeated calls on the same possessions return identical output with no need to set a seed.
Details
poss <- nba_possession_lineups(game_id = "0022200001")
nba_rapm(poss)See also
Other NBA Lineup Functions:
nba_boxscoretraditionalv3(),
nba_fantasywidget(),
nba_possession_lineups(),
nba_shot_zones(),
nba_shotchartdetail(),
nba_teamdashboardbyclutch()
