Simul RAPM & The Breakdowns

A possession-level impact model that starts each player from a priced-out account of his skills, then lets the lineups move him — and three exact decompositions of the result.

Part 1 · The Fit

From Stints to a Regression

Raw plus-minus is misleading because a player’s number is tangled up with the four teammates and five opponents on the floor with him. RAPM untangles it by solving for everyone at once.

The unit is a stint — a stretch where the same ten players are on the floor. Each stint becomes two rows, one per direction of play:

5 offensive players  →  +1 in their O columns
5 defenders  →  −1 in their D columns
home  →  1 if the offence is home   ·   intercept  →  1

y = 100 × points / possessions   ·   weight = possessionsDefenders entering at −1 is what makes a positive D mean “points prevented” and lets total = O + D simply add up. Get that sign backwards and everything still runs while the totals scramble — correlation with truth drops to about 0.01.

So every ten-man combination that ever shared the floor becomes evidence, and the solve asks: what offensive and defensive coefficient for each player best explains the points scored across all of them?

Ridge, and Two Penalties

With thousands of players in tangled combinations, an unpenalised fit overfits wildly — a player with a handful of good minutes lands at +40. Ridge adds a penalty on large coefficients, and the thinner the sample behind an estimate, the harder it gets pulled in.

unpenalised fitafter ridgethe thinner the sample behind an estimate, the harder it is pulled in
Each line is one player: unpenalised on the left, regularised on the right.

Two things are deliberately left unpenalised: the intercept and home court. Shrinking the intercept would bias every rating, and home court is a real effect rather than noise. Defence carries the heavier penalty of the two player blocks — fewer attributable events per possession means lower signal-to-noise.

Lambda is fixed at 5000 offence / 6000 defence rather than cross-validated per window. That is a choice, and the reason is comparability: measured across 30 seasons, per-window CV picked anywhere from 1500 to 3500 with an RMSE curve flat to 0.003% across that whole range. It was choosing between indistinguishable options while breaking cross-season comparability — a +5 in a season that picked 1500 is shrunk less than a +5 in a season that picked 3500, which quietly distorts every all-time list.

The defensive lambda was picked for differentiation, not reliability. Pushing it to 25,000 raises split-half correlation from 0.32 to 0.51 — but collapses the spread from 0.93 to 0.21. That is shrinkage buying a reliability number while destroying the information the number was supposed to describe.

Windows

Fits run on backward-looking windows — one year and three years — labelled by the season they end on. A three-year window ending 2025-26 is built from 2023-24 through 2025-26. Windows ending in a completed season are final and never rewritten, so an in-season refresh touches only the current window.

Part 2 · The Prior

Starting From Talent Instead of Zero

A plain ridge model shrinks every player toward zero — it assumes you are league-average until the possessions prove otherwise. Simul RAPM shrinks toward a priced-out account of your skillsinstead. That is the “Simul” part.

minimise   ||√w (y − Xb)||²   +   λ ||b − b0||²Substituting g = b − b₀ turns this back into ordinary ridge on the residual from the prior, so the solver itself never changes: fit g on y − Xb₀, then report b₀ + g. b₀ is zero on the intercept and home court — folding a baseline into unpenalised terms would assert something about pace and home advantage that was never measured.

b₀ is built in three stages, and it is in points per 100 possessions, matching the units of the coefficients it sits next to. A z-score in that slot would look fine, run fine, and shrink every player toward a number that means nothing.

Stage 1 — Grade Each Skill

Four sources (role stats, career box score, tracking, shot zones) collapse to one row per player-season with roughly fifty talent columns, split into offensive and defensive groups: catch-and-shoot three production and accuracy, pull-up three production and accuracy, points per rim attempt, rim protection, turnover avoidance, and so on.

The zone data gives a decomposition worth naming, because it separates two things that are usually smeared together:

selection = Σz   FREQz × LEAGUE_AVG_FG%z × ptsz
making    = Σz   FREQz × (FG%z − LEAGUE_AVG_FG%z) × ptszSelection is what a league-average shooter would score on your shot diet — pure profile, zero accuracy. Making is points added over average holding location fixed — pure accuracy, zero profile. Orthogonal by construction, so both can sit in the matrix without fighting over the same variance.

Stage 2 — Shrink Each Column on Its Own Terms

Skills stabilise at wildly different rates, so every column is shrunk separately against its own sample size:

shrunk = ( n × observed  +  k × target ) / ( n + k )n is that column’s own denominator — three-point attempts for 3P%, rebound chances for OREB%, possessions for rate stats. k is asserted from known stabilisation points rather than fitted, and each one carries a one-line justification in the source. Those k values are the load-bearing assumption of the whole prior.

The target is not the league mean. Each stat is regressed on the role scores and the fitted value becomes the target, so a Rollman’s three-point prior regresses toward what Rollmen shoot, not toward what the league shoots. This is where the role system feeds the RAPM model.

Stage 3 — Let the League Price the Skills

Standardised talent then has to become points. The skills are priced by fitting directly on stint rows:

y = (X Zoff) goff  −  (X Zdef) gdef +  home  +  interceptZ is the skill matrix, g is one coefficient per skill — what that skill is worth in points per 100. There is no RAPM target anywhere in this fit, which is what dissolves the circularity: none of RAPM’s own noise propagates into the prices. The same garbage-time filter is applied, so talent is priced against exactly the possessions the ratings are fit on.
HIS SKILL GRADESLEAGUE PRICEPRIORFITTEDcatch-and-shoot 3s+1.4×0.42rim finishing+0.3×0.55rim protection+2.1×0.38turnover avoidance-0.6×0.21sum = +1.86+1.86+2.71possessionsthe gap between the two dots is delta — what the lineups saw that the skill accounting could not
A player’s skill grades times the league’s prices give his prior; the possessions move him from there.

Two details that took real work to get right. Signs are constrained.Columns are oriented so higher is better, so the sign is known before fitting. Left free, the regression produces things like blocks-over-expectation at −0.10 — which is not a finding about blocks, it is a residual correction in a collinear design being read as one. Constrained columns can go to zero, the honest “this adds nothing here”; they cannot go negative.

And lambda is chosen by coefficient stability, not prediction. Stint outcomes are so noisy that talent explains almost none of their variance, so CV error is flat across the whole grid and selecting on it leaves the fit essentially unpenalised — precisely where correlated columns split into large cancelling pairs that move every time you refit. Instead the games are cut into two disjoint halves, g is fit on each, and the two coefficient vectors are correlated. The lambda that makes the prices reproducible is the one that has stopped fitting noise.

Delta — The Interesting Column

Because the prior is stored alongside the result, every player carries the gap between them. Delta is what the lineups saw that the skill accounting could not — the closest thing this system has to a measurement of gravity, screening, off-ball defence and everything else that never enters a box score.

Part 3 · Cleaning

What Gets Removed Before the Fit

Free Throw Luck

Every free throw attempt has its actual outcome replaced by the shooter’s regressed season rate, (makes + k×league) / (attempts + k)with k = 50, so a hot or cold night at the line does not land in anyone’s rating. This was the single largest measured gain in the whole project: split-half reliability 0.408 → 0.451. Technicals are dropped, since the shooter is often a designated one rather than the player who was fouled.

Garbage Time

Blowout minutes are mostly bench players going through the motions. Possessions in the fourth quarter or later, inside 5:00, at a margin of 20 or more are dropped — whole stints, not truncated portions, because a stint straddling the 5:00 mark is usually entirely garbage or entirely not, so proration buys very little and adds an approximation.

4th quarter or later, margin 20 or more, under 5:00 — the whole stint goes200DROPPEDKEPT5:00 left0:00margin
Red is dropped, green is kept.

One implementation detail that matters more than it sounds: the margin is computed from cumulative stint points rather than from a score column, because pre-2017 files coerce missing scores to zero for 21–75% of rows. Gating on the raw column would misfire on every early season.

Honest caveat: this is only about 0.9% of possessions at the median. It is kept because it is defensible, not because it moves anything.

Recency Weighting — Available, and Off

A half-life decay exists as an option, weighting each stint by 0.5(age / half-life)and rescaling so the total weight is preserved. It defaults to off. Measured on three-year windows it produced a 0.021% improvement with individual windows disagreeing on the direction — which is noise, not an effect. It is documented here rather than quietly shipped.

Part 4 · The Breakdowns

Splitting a Rating Without Refitting It

A single number tells you a player helped; it does not tell you how. The breakdowns split O_RAPM and D_RAPM into components — and they do it exactly, which is unusual enough to be worth explaining.

β = (X′WX + λD)−1X′W y  =  M yRidge is linear in the target. M depends only on the design and the weights — never on y. So if y splits into pieces that add up, β splits the same way: y = y₁ + y₂ + … ⟹ β = β₁ + β₂ + … Points add up, so a split of points by how they were scored is a valid decomposition of the rating. No second-stage reconstruction, no R² to report, nothing fitted twice.

This is the property every failed attempt lacked, and it is the rule the breakdowns are held to: a component has to be a share of the same target, not a separate fit.

Breakdown 1 — By Scoring Channel

O_RAPM SPLIT BY WHERE THE TEAM SCOREDrim+2.1mid-0.93PT+1.6FT+0.4TOV+0.5other+0.2= +3.9exact by construction, not fitted — verified to 6.7e-13 on real data
Six components, summing back to the offensive total exactly.

Every target here is a team-level property of the stint— “points this lineup scored at the rim”. No player is ever named in a target, which is what keeps this out of the failure mode where a coefficient reads presence instead of effect. There are no tunable parameters: no charge rate, no assist share, no rebound price. The only inputs are the play-by-play and the league points-per-possession computed from it.

ChannelWhat it measures
RimEffect on how much the team scores at the rim while he is on the floor.
MidSame, midrange.
3PTSame, from three.
FTSame, at the line.
TOVTurnovers avoided — possessions that ended without a shot.
OtherPoints the parse could not tag. Runs 4.6–4.9%, mostly and-1s. A large value here is a parse gap, not a finding.

Read the labels literally.“Rim” sounds like rim finishing and is not — it is the team’s rim scoring shift. A high-gravity player raises team three-point scoring on shots he never touches, and a great finisher whose gravity pulls the offence to the arc can come out slightly negative at the rim despite finishing at 80%.

On offence a row reads as a profile more than a skill list. Giannis at rim +6.9 / mid −3.2 is one fact — he moves attempts to the rim — not two findings.

The defensive split is the stronger half, and it is the reason this shipped. Rim protectors sort themselves out with no supervision at all: Holmgren, Wembanyama, Hartenstein and Queta at the top of D_rim, perimeter guards at the bottom. Defence has been the weak half of this project throughout — split-half 0.38, prior R² 0.23 — and a rim-versus-perimeter split falls out of it for free.

Breakdown 2 — By Who Else Was on the Floor

The same exactness argument, applied to a different split of the possessions. Two passes: an ordinary fit used only to rank lineups, then a decomposition of that same fit by the rank. Four two-way splits, each partitioning the possessions, each summing back to its parent:

SplitCut by
O vs good / bad DOpposing five’s mean defensive rating, at the possession-weighted median.
D vs good / bad OOpposing five’s mean offensive rating.
O with strong / weak castSecond-best offensive rating among his own five.
D with strong / weak castSecond-best defensive rating among his own five.

Why second-best and not the mean of his teammates. The tag has to be a property of the lineup, never of the player being evaluated. “Mean of his other four” is a different number depending on whose coefficient you are reading — the per-player-target trap that broke every earlier attempt at this. Second-best is one number per lineup, and it happens to mean something: for a star, normally the best rating on the floor, it is the quality of his best help.

Two derived columns come out of this. O Gap is offence against good defences minus offence against bad ones — how much of the offence holds up when the defence is real. Self-Made is offence with a weak cast minus offence with a strong one — how much of it survives without help.

Two Honest Caveats on This One

The first-pass rating of a player feeds the tag on his own rows through the supporting-cast split. Second-best removes him whenever he is the best rating in the lineup, which for stars is most of the time — but not always. The opponent splits have no such issue, since that is a different team entirely.

And compare within similar roles. A bench player’s “strong cast” rows are ones where a star was on the floor; a star’s are ones where his second option was. Same column name, different question.

What the Breakdowns Cannot Do

Isolating playmaking and creation was attempted twelve different ways and failed every time, for one underlying reason worth stating: a target that is nonzero only when a player is on the floor encodes his presence, not his effect.“Points he assisted” comes back as assist volume — Giannis at +10.8 on assisted threes is how many he assists, not what it is worth. Those columns exist in the research code and are deliberately not published.

Turnovers and offensive rebounds are also not fully separable in the channel split. The target is points per possession; turnovers change the denominator and offensive rebounds add attempts inside a possession whose points are already counted elsewhere. Isolating them properly needs a model that does not sum exactly — which is a trade this deliberately does not make.

Part 5 · How Good Is It

The Measurements, Including the Bad Ones

Reliability

Fit the odd-numbered games and the even-numbered games separately, then correlate the two sets of ratings. Same players, same season, different possessions — a direct read on measurement precision.

VariantCorrOffenceDefenceSpread
Baseline, λ=20000.4080.4350.3411.83
+ FT adjustment, λ=30000.4510.4680.3801.41
λ=60000.5380.5340.4540.90
λ=120000.6070.5910.5170.55

The last two rows are the shrinkage trap, not improvement. Reliability is trivially gamed: push the penalty high enough and both halves collapse toward zero and correlate at 0.99 while carrying no information. Spread falls from 1.41 to 0.55 across those rows. Always read the correlation next to the spread, and only compare variants at the same lambda.

Two structural facts fall out. Defence sits about 0.09 below offence and no lambda fixes it — there are simply fewer attributable events per possession. And reliability has declined over time, from 0.45–0.53 in the late 90s to 0.30–0.41 recently, because players per team went from 335 to 430 and each one gets fewer possessions.

Does the Prior Earn Its Place

Prior targetOffence R²Defence R²
Same-season 3yr RAPM0.530.23
Luck-adjusted on/off0.400.10
Next-season 3yr RAPM0.630.26

Read these against shrinking to zero, which is R² = 0 by construction. Even the weakest of them beats assuming everyone is average.

Predicting the Following Season

Ratings from the window ending in one season are used to predict lineup and team results in the next. Correlation with team net rating rises from 0.778 without the prior to 0.790 with it — but the slope falls from 0.78 to 0.68, which means the ratings are over-dispersed. That is a scale problem with a scale fix, not an information problem. Worth knowing when reading an extreme value: the ordering is better than the magnitude.

Sanity Checks That Pass

Reading the Numbers

As with everything here, this is a model and not the truth. RAPM cannot see why a player is good, only that lineups featuring him tend to win the possession battle. The breakdowns narrow that gap — they say which channel the value arrived through, and against whom — but they still describe an effect, not a cause.

See the RAPM Leaderboard
Offence, defence and total for every player, with the zone, opponent-quality and supporting-cast breakdowns — sortable and filterable.
→