PythonXGBoostTwo-sided Grabit (Tobit GBT)SHAPin progress

NBA Free Agent Valuation Model

An open, reproducible model of what an NBA free agent is worth, as a share of the salary cap, built only from public data.

It runs in three stages because the market and the Collective Bargaining Agreement answer different questions: what a player is worth, what the rules let him be paid, and what the route he actually signed through allows.

Open the valuation board →GitHub ↗Personal research project · Jun 2026 – Present
Cross-validated R²
0.872
2024–26, repeated grouped CV, v6.3.1
Forward R²
0.859
rolling origin, the actual inference metric
CV MAE
$2.50M
pooled selection CV
Training contracts
885
negotiated first years, grouped by player
inference pathRead only while fitting. Never available for an unsigned player.Two-pass inference: a base fit prices the seasons between contracts, the market filter runs on them, and the final model trains on the result.

00The problem

A max contract hides what a player is worth

The CBA bounds every salary from both sides. A max contract is capped at 25, 30 or 35% of the cap depending on service and awards, and nothing can be signed below the veteran minimum. A player worth 40% of the cap is paid 35%, so the recorded salary is a bound, not a price. Train an ordinary regression on those rows and it learns to predict the bound.

The chart holds every negotiated first-year contract the model trains on. Height is what the player was paid; position across is the Stage 1 latent value, what the model thinks an open market with no CBA would pay. In the middle the two agree and the points follow the diagonal. At the top, max deals stack on their tier ceilings while the latent keeps going. At the bottom, minimum deals stack on the floor, and their latents split: about half sit below it, held up by the rule, and the rest spread to the right, players the market would have paid more who took the minimum anyway. That second group is not censoring, and it comes back under Limits.

Everyone else
0%10%20%30%40%0%10%20%30%40%Stage 1 latent value, share of cap →↑ actually paid, year 125% max tier30% max tier35% max tierpaid = latentpaid the max, worth moreminimum deals: latent on both sides of the floor
52 of the 68 max-zone rows carry a latent above their own ceiling: the bound, not the market, set their pay. 135 of the 265 floor-zone rows carry one below what they were paid. Latent values for 2019–25 rows are the export’s training fit; 2026 rows were never seen by it. Hover or tap a dot for the player.
The 68 max-zone rows as a table
PlayerSeasonPaidLatent
Kawhi Leonard2018–1930.0%46.8%
Kyrie Irving2018–1929.1%42.5%
Giannis Antetokounmpo2020–2135.0%39.8%
Kemba Walker2018–1930.0%39.8%
Joel Embiid2022–2335.0%39.4%
Nikola Jokić2022–2335.0%38.6%
Jimmy Butler2018–1930.0%37.6%
Klay Thompson2018–1930.0%37.0%
Anthony Davis2019–2030.0%36.6%
James Harden2018–1935.0%36.2%
Kevin Durant2018–1934.1%36.1%
Devin Booker2023–2435.0%35.7%
Jaylen Brown2023–2435.0%35.7%
Karl-Anthony Towns2018–1925.0%35.1%
Damian Lillard2020–2135.0%34.2%
Jayson Tatum2024–2535.0%34.1%
Evan Mobley2024–2530.0%34.1%
Tobias Harris2018–1930.0%33.8%
Anthony Davis2024–2535.0%33.8%
Tyrese Haliburton2023–2430.0%33.5%
Luka Dončić2021–2230.0%33.2%
Jayson Tatum2020–2125.0%33.0%
Trae Young2021–2230.0%33.0%
Bradley Beal2021–2235.0%32.9%
Cade Cunningham2024–2530.0%32.8%
Lauri Markkanen2023–2430.0%32.7%
Jimmy Butler2022–2333.2%32.6%
Zach LaVine2021–2230.0%32.5%
Anthony Edwards2023–2430.0%32.5%
Karl-Anthony Towns2023–2435.0%31.7%
Zion Williamson2022–2325.0%31.6%
Devin Booker2018–1925.0%31.2%
Bam Adebayo2025–2630.0%31.2%
Bradley Beal2020–2130.0%30.9%
Domantas Sabonis2023–2427.9%30.8%
Pascal Siakam2019–2028.0%30.6%
Donovan Mitchell2024–2530.0%30.5%
Jamal Murray2024–2530.0%30.0%
Khris Middleton2018–1928.0%30.0%
Franz Wagner2024–2525.0%30.0%
Fred VanVleet2022–2330.0%29.9%
Ja Morant2022–2325.0%29.1%
Ben Simmons2019–2028.0%29.0%
D'Angelo Russell2018–1925.0%28.4%
Jamal Murray2019–2025.0%27.6%
Tyrese Maxey2023–2425.0%27.6%
LaMelo Ball2023–2425.0%27.3%
Brandon Ingram2019–2025.0%27.3%
Shai Gilgeous-Alexander2021–2225.0%27.1%
Bam Adebayo2020–2125.0%27.0%
Desmond Bane2023–2424.2%27.0%
Donovan Mitchell2020–2125.0%27.0%
Austin Reaves2025–2625.0%26.7%
Darius Garland2022–2325.0%26.2%
De'Aaron Fox2025–2630.0%26.2%
Chet Holmgren2025–2625.0%26.2%
Deandre Ayton2021–2225.0%26.1%
Jalen Suggs2024–2522.6%25.8%
Scottie Barnes2024–2525.0%25.4%
De'Aaron Fox2020–2125.0%25.3%
Jaren Jackson Jr.2021–2223.4%25.1%
Paolo Banchero2025–2625.0%24.8%
Immanuel Quickley2023–2423.1%24.7%
Jalen Williams2025–2625.0%23.5%
Jaren Jackson Jr.2025–2629.7%22.9%
Myles Turner2021–2228.0%22.3%
Michael Porter Jr.2021–2225.0%22.1%
Trae Young2025–2630.0%16.4%

01U1 · Data

Most salary rows are not market prices

  • Basketball Referencesalaries, height, awards, playoff minutes
  • Spotraccap hits, signing mechanism, dated contract spans, waivers
  • nbarapm.comDARKO, LEBRON, LAKER, usage and box-score rates
  • The CBAcap by season, max tiers, minimum scale, raise limits

Salaries come from Basketball Reference and Spotrac cap hits, the impact metrics DARKO, LEBRON and LAKER from nbarapm.com, and the bounds from the CBA itself. Stats lag salary by one season on purpose: a contract signed in July is priced on the season that just ended.

Years two and later of a contract are CBA escalators, rookie deals are slotted by draft position, and partial-season pay depends on the signing date. None of those is a price the market set, so each filter below removes one of them. Thresholds are shares of the cap, never dollar figures, because the minimum and the max move with the cap.

  1. Salaries × impact metricsjoined on player and season
    3,113
  2. First contract year onlyyears 2+ are 5–8% CBA escalators
    1,808 −1,305
  3. Rookie scale removedslotted by draft position, not priced
    1,556 −252
  4. Prorated pay removedunder 1.2% of the cap: 10-day and mid-season deals
    1,297 −259
  5. Provable mislabels removedpaid above the tier max, so a later year of an older deal
    1,291 −6
  6. Continuations removedspan, escalator step and FA list all agree
    1,172 −119
  7. Dated contract spansa dated deal that started earlier covers the row
    949 −223
Counts as each filter landed, v3.0.x to v3.1.x (legacy v7.x). Later source repairs moved the frame to the size in the scoreboard above. Removing prorated pay was a calibration fix, not an accuracy gain: those rows dragged every prediction down by about half a million dollars.

02U2 · Features

21 features, kept by ablation from more than 40

A feature must exist before the contract is signed and must not be derived from the salary. Each one entered through the same gate: rerun cross-validation on identical folds with and without it, and require the fold-by-fold improvement to clear t > 2.

The bars show how far each feature moves a prediction on average, as mean absolute SHAP in dollars over the training contracts. Minutes per game dominates on its own; the market trajectory and the Kalman quality estimate are the two engineered features that come next.

Role & workload

  • Minutes per game$3.11M
  • Availability, 3 years$0.66M
  • Usage rate$0.55M
  • Playoff minutes swing$0.41M
  • Assist rate$0.10M

Market history

  • Market trajectory$2.77M
  • Recently waived$0.18M
  • Minutes × waived$0.08M

Stage 1 linear terms

  • P(max) term$0.54M
  • Buyout term$0.15M

Performance

  • Kalman quality$0.99M
  • DARKO DPM$0.66M
  • DARKO offence − defence$0.36M
  • LAKER$0.22M
  • LAKER offence − defence$0.16M
  • LEBRON$0.14M
  • LEBRON offence − defence$0.12M

Age & body

  • Age$0.71M
  • Age²$0.25M
  • Height$0.22M

Pedigree & era

  • Award score$0.18M
  • Draft pick$0.06M
  • CBA era$0.02M
Mean absolute SHAP per feature over the training contracts, in dollars of first-year salary. SHAP decomposes the Stage 1 latent, so these are contributions to market value before any CBA bound.

Tried and rejected

Redundantalready carried by a kept feature

  • BPM, TS+, 3-point rates< +0.001
  • Assist-to-turnover+0.0001
  • All-NBA count+0.0001
  • Lottery / top-5 pick< +0.001
  • Injury reports+0.0002
  • Experience years−0.0004

Team proxypermuting within a team keeps the effect

  • Win %, made playoffs+0.0004
  • Playoff games playedteam signal
  • Team cap space+0.0008
  • Franchise value+0.0001

Reads the targetderived from the salary it predicts

  • Vet-minimum / MLE-range flagsleak
  • P(Minimum | x), P(Bird | x)−0.0073
  • Agent portfolio−0.002 to −0.005

No signalfailed the paired gate

  • Contract year+0.0009
  • Stayed with team+0.0006
  • Height × age+0.0004
  • Minutes × impact−0.0018
  • Spotrac injury burden−0.0024
Change in pooled CV R² when the candidate was added. Each row is from its own frame and version, so compare a row only with zero.

02bU2 · Two Kalman filters

One filter for how good he is, one for what the market last said

Quality. DARKO, LEBRON and LAKER are three noisy gauges of one hidden quantity. A Kalman filter fuses them per player, season by season, weighting each gauge by its measurement noise and adding an age drift: improvement through the early twenties, decline from 25 that steepens after 28. Every parameter comes from the metrics, never from salary. A moving average over the same gauges recovers only half the gain, so the weighting and the drift are doing the work.

Market. The second filter anchors at the last price the market actually set for a player, an in-season signing included, and walks forward through the seasons in between. A first-pass model prices those seasons, clipped into the legal range. At inference that takes two passes: the base model prices the intermediate seasons, the filter runs, then the final model trains on its output.

A second-round pick who became a star. The quality estimate rises a step behind the gauges, and the market trajectory re-anchors at both of his negotiated deals.

Quality, z-score within season−0.50+0.5+1+1.5+2+2.5Pay and market trajectory, share of cap0%10%20%30%2018–192019–202020–212021–222022–232023–242024–252025–26

Weight per update ∝ 1 / noise

  • DARKO50%R 0.138
  • LEBRON34%R 0.203
  • LAKER16%R 0.447
Real export rows. Each row’s stats are from the season before, so a 2025–26 row reads 2024–25 metrics. Seasons without a contract are left out.

03U3 · Stage 1 · Grabit

A loss that knows which salaries are bounds

Stage 1 is XGBoost, 500 trees of depth 4, with the loss replaced on censored rows. Grabit (Sigrist & Hirnschall, 2019) comes from credit-risk modelling and uses a censored-normal likelihood. For a max contract the observed salary is only a lower bound on the latent value, so the gradient never drags the prediction down to it. For a minimum deal the observation is an upper bound, and the gradient is mirrored.

Move the prediction. Squared error always pulls it back to the observed salary. Grabit pushes while the prediction sits at or below the bound and lets go above it; σ = 2% of the cap sets how wide that hand-off is.

The trees are not the whole of Stage 1. Two linear terms enter as the base margin, so the trees fit what is left around them: one for players bought out of money they were still owed, one for players likely to sign a max. Each coefficient is a censored-likelihood fit on out-of-fold predictions inside every training slice.

−8−40+4+825%30%35%prediction, share of cap →↑ pushes up↓ pulls downpaid the 30% maxGrabit lets go
Squared error
+3.0 pts
back toward the observed salary
Grabit
+3.9 pts
the bound is a floor on his worth
  • Not every row at a bound is bound by it, so censoring is gated on a baseline model agreeing.
  • Max side: the baseline must reach 55% of the player’s ceiling. That keeps declining stars on old max deals, which the market would not repeat, from being treated as underpaid.
  • Floor side: the baseline must be at most twice the observed pay. A good veteran on a minimum made a choice; only a player priced below the minimum hit a constraint.
max side:    pull = +σ·φ(z)/Φ(z)   z = (pred − obs)/σ
floor side:  pull = −σ·φ(z)/Φ(z)   z = (obs − pred)/σ
otherwise:   pull = obs − pred

In the export’s fit, 57 max rows and 137 floor rows are censored. Every other row keeps plain squared error.

Two linear terms in the base margin

trees(x)P(max) term +6.2 ptsbuyout term 0.0 ptslatent = trees(x) + 6.2 pts

Coefficients from the export’s fit. Both terms add to the trees’ output, not to kf; the picture measures each against the quantity it scales with.
latent = trees(x)
       + β_w · owed · kf
       + β_max · P(max) · max(ceiling − kf, 0)
  • Buyout term: a player bought out of money he was owed gives up a share of his market trajectory kf, because the contract that anchors kf overstates his price. A split on a waiver flag applied a near-uniform discount, too weak for bought-out stars. Plain waivers stay with the trees.
  • P(max) term: lifts a likely-max player part of the way from his market trajectory to his ceiling. A training row’s P(max) comes from a classifier that never saw that player, because route labels come from the target. Waived rows get P(max) = 0.
  • The classifier reads 36 columns stated explicitly and frozen. When it inherited the regression features, a column with no route signal flipped borderline rows from seed to seed.

04U4 · Stage 2 · CBA bounds

Stage 2 is a clipper circuit

Stage 2 maps the latent value to a legal price: nothing above the player’s tier ceiling, nothing below the league floor. Both bounds are knowable before the market opens, the ceiling from service years and awards, the floor from season and experience. The clip never reads the observed salary.

Until v6.3.0 Stage 2 also pushed likely-max players toward the ceiling after the fit, behind a P(max) ≥ 0.52 gate, because a clip can only cap from above. The P(max) term in Stage 1 replaced it. The lift is now fitted, the censored loss and the trees see it, and a knife-edge gate no longer decides who gets it.

0%0%10%10%20%20%30%30%40%40%in: Stage 1 latent →↑ out: legal priceceiling 30%floor

latent 24.0%inside the bounds24.0%

A clipper passes the middle of the range unchanged and flattens both ends. The dashed curve is what the removed push did to a row with P(max) 0.8: everything from the floor up was dragged toward the ceiling after the fit.
price = clip(latent, floor, ceiling)
  • Ceiling: 25% of the cap with 0–6 years of service, 30% with 7–9, 35% with 10 or more. An All-NBA, MVP or DPOY award lifts a young player to 30% (Rose rule) or a 7–9 year veteran to 35% (supermax).
  • A veteran’s max is also at least 105% of his previous salary, so the ceiling carries a no-decrease floor. An audit of every row paid above its ceiling brought the count from 13 to zero.
  • Floor: the veteran minimum for the season and service bucket, recovered from the data’s own mass points rather than maintained CBA tables.

What replacing the push changed

MAE by zone, $M

  • Max zone n 572.12 → 2.02
  • Floor zone n 2241.84 → 1.77
  • Everyone else n 4763.02 → 2.97

Named rows, error in $M

  • James Harden 2022+$11.6M → +$3.9M
  • Devin Booker 2019−$6.8M → −$2.6M
  • D’Angelo Russell 2019−$2.8M → −$1.1M
  • DeMar DeRozan 2024+$14.8M → +$16.3M
MAE and named-row error in $M, prediction minus actual: the v6.2.0 composition with the push against the v6.3.0 P(max) term, one run, the same folds and seeds. The gain is real but narrow: 30% of affected players improve, the pooled paired delta is +0.0096 at t = 1.59, and it was accepted as a structural refactor rather than on a t > 2 gate. DeRozan 2024 got worse.

05U5 · Stage 3 · Told route

What the signing route adds, once the deal is done

Some limits exist only once you know how a deal was signed. An extension’s first year is capped at 140% of prior salary (120% before the 2023 CBA) or the same multiple of the league’s average salary, whichever is greater. Early Bird and Non-Bird deals have their own ceilings. A team holding a player’s Bird rights pays a retention premium that no performance feature can see.

Stage 3 applies these in a fixed order: a per-route offset, the mechanism cap, the extension cap, then the [floor, ceiling] clip again. Every cap can only lower a prediction, and the final clip means no correction can price a contract the CBA would not allow.

Own team re-signs him over the cap. Gets the largest offset: a retention premium the features cannot see.

$0M$10M$20M$30M$40M$50M$60M$24.0MStage 2 price$26.2MRoute offset—Mechanism cap—Extension capcap $46.4M$26.2MRe-clip
A seven-year veteran signing for 2025–26: 30% tier, cap $154.6M. Offsets are the deployed constants in the export; the caps follow the CBA formulas the model applies, with an average salary of $13.87M. Rising steps are teal, falling steps gold, and a cap line turns solid when it binds.

What is left after Stage 3

Eligibility routes · offset applied

  • Bird Rights n 307−$1.89M → −$0.34M
  • Cap Space n 77−$1.22M → −$0.38M
  • Early Bird n 53−$1.67M → −$0.54M
  • Non-Bird n 14+$0.67M → +$0.49M

Exception routes · left alone, the label reads the price

  • MLE n 139+$0.39M
  • Minimum n 265+$1.63M
Mean error by signing mechanism, prediction minus actual, on v6.3.1 out-of-fold rows. The outline marks the bias before the route offset; negative means the model underprices. Only the four eligibility routes are corrected: they are settled by the previous contract and the team’s books before the price is. Sign & Trade counts as Bird Rights, since the originating team must hold his Bird or Early Bird rights. Each offset is shrunk toward zero by n / (n + 20) and estimated out of fold, so it never sees the row it corrects.

06Evaluation

Selection and forecasting are scored separately

Decisions use repeated grouped CV: five folds of players, never rows, so one player’s contracts never sit on both sides of a split. Each of ten seeds deals the players by a fixed hash of its own partition, so adding a row never moves anyone else. Deltas are paired by fold, because fold-to-fold spread dwarfs seed-to-seed spread.

Forecasting is measured on its own by rolling origin, because that is what inference does: train on every season before T, predict T. A 15% confirmation split of players stays out of every selection decision.

A · Selection: repeated grouped CV

Players, not rows, are dealt into five folds by a hash, so all of one player’s contracts land in the same fold. Each of the ten seeds deals a different partition.

B · Forecasting: rolling origin

Train on every season before T, predict T. This is exactly what pricing next summer’s free agents looks like.

What each part of the pipeline buys

  1. Stage 1 + clipv6.0.0 arm0.8303
  2. + push, route not toldv6.0.0 arm0.8280 −0.0023
  3. + extension capv6.0.0 arm0.8357 +0.0077
  4. + route offsets, mechanism capv6.0.00.8509 +0.0152
  5. + buyout termv6.2.00.8612 +0.0103
  6. P(max) term replaces the pushv6.3.10.8679 +0.0067
Forward R², rolling origin
0.859
CV MAE
$2.50M
Calibration slope
0.978
Every row is a published number on the same protocol and frame. The last two rows also carry small data corrections from v6.0.4 and v6.0.5, and v6.3.0 was measured on another machine, so read the steps, not the decimals. The axis starts at 0.82. The 2026 forward origin on its own reads 0.808.

A correction worth stating plainly

An earlier version of this work said the model underpaid stars by up to $6M, read from residuals binned by actual salary. That gradient appears even for a perfectly calibrated model: binning on a noisy target selects on the noise, so the top bin is full of players whose salary came in high. Binned by prediction, it disappears.

The same trap has a second form when comparing two models. Cutting segments on either model’s predictions lets the band composition shift between them, so segments are assigned by something neither model produced.

  • under 2% of cap+$1.76M
  • 8–15% of cap−$2.60M
  • 25% and above−$5.71M

Looks like the model underpays stars by almost $6M.

Mean residual of the same predictions, binned two ways. Negative means the model underprices.

07Limits

What the model cannot see

The remaining error is mostly not a modelling problem. It is information the public data does not hold.

Signing mechanism cannot be a feature

A fold-honest P(mechanism | x) lowers CV R² by 0.0073, and even a leaky oracle given the realized mechanism gains only +0.0137. The trees already extract what the features say about it. A route can correct an output after the deal; it cannot enter as an input before it.

The target is bimodal

With the same inputs, a mid-market player either signs a market deal or takes a minimum. The conditional mean sits correctly between the two modes and is wrong for both. A distributional output, P(minimum) beside a market value, would answer the real question better, and R² could not measure it.

Information outside the box score

Team cap room that summer, positional supply, how the last contract ended, and tracking data are all invisible to it. Rookie max extensions are priced on potential the stats do not show yet. Five route-mixture designs tried to recover this error from the existing features. Only the extension survived, and only because it applies a legal ceiling.

takes the minimumsigns a market dealconditional mean
Illustrative shape, not fitted data: two outcomes for one set of inputs, and the mean between them.

In short

  • Adapted Grabit (gradient tree-boosted Tobit) from credit-risk modeling to NBA salaries, then composed its two-sided censoring with fitted P(max) and buyout terms and a told-route extension cap so each stage answers one distinct CBA question
  • Built the full pipeline end to end, scraping Basketball Reference, Spotrac and nbarapm, engineering 21 features from 40+ candidates by ablation, and a four-layer evaluation protocol that separates structural estimation from forecasting and selection from confirmation
  • Established a characterized negative result: the residual pattern by signing mechanism is real but unrecoverable, with the ceiling on any fix measured empirically

Every prediction on the board breaks down into per-feature dollar attributions, and the what-if tab reruns the trees in your browser.

Open the valuation board →

Diagnostics from the export

Export v6.3.1, generated 2026-10-01. Interactive figures read the same files as the board.