← Back to portfolio
Python · XGBoost · Two-sided Grabit (Tobit GBT) · SHAP · GroupKFold CV · Web Scrapingin progress

NBA Free Agent Valuation Model

Personal research project · Jun 2026 – Present

0.852Cross-validated R²2024–26, n = 324
0.836Forward R²rolling origin — what inference achieves
$2.92MMean absolute errorcalibration slope 0.973
944Training contractsselection CV, grouped by player

NBA teams commit over $4 billion a year to player salaries, and the most cited valuation framework — Hollinger’s BORD$ — is proprietary. This is an open, reproducible alternative that predicts what a player is worth as a percentage of the salary cap, built only from public data.

The interesting problem is not regression, it is censoring. The Collective Bargaining Agreement bounds a salary from both directions: a max contract is capped at 25–35% of the cap depending on experience, and no contract may fall below the veteran minimum. A player worth 40% is paid 35%, so his recorded salary is not his value — it is a ceiling. Train an ordinary model on those rows and it learns to predict the bound. I adapted the Grabit framework (Sigrist & Hirnschall, 2019) from default prediction: bounded rows get inverse-Mills-ratio gradients that push the latent prediction past the observed bound, unbounded rows keep ordinary squared error.

The subtlety is deciding what counts as censored, and the two sides answer it differently. On the ceiling, not every max-salary player is underpaid, so v8 gates the additional max push on P(max) ≥ 0.52. On the floor the economics invert: a good player choosing a minimum is not censored, while a latent value below the legal minimum is genuinely held at a bound. A final told-route stage handles extensions separately, because their raise limit is contract-specific rather than an ex-ante market ceiling. The stages stay explicit so latent market value, general CBA bounds, and the realised signing route never get conflated.

Censoring, on both sides

A max contract is a right-censored observation and a veteran minimum is a left-censored one: in each case the recorded salary is a bound, not a price. Grabit replaces squared error on those rows with a censored-normal likelihood. Stage 1 recovers latent worth; the v8 max push then lifts high-confidence max players toward their ceiling when P(max) ≥ 0.52. Stage 2 restores the general CBA floor and ceiling. Stage 3 acts only when the told signing route is an extension, applying its contract-specific raise cap without contaminating the ex-ante market value.

grad = −σ · φ(z) / Φ(z)      where  z = (pred − observed) / σ

latent → max push (τ = 0.52) → CBA clip → extension raise cap

The latent remains the headline market value. The general bounds vary by player and season; the Stage-3 extension limit is route-specific and is reported separately.

Results

Selection uses pooled 5-fold GroupKFold over 10 seeds, grouped so a player never appears in both train and test. Forecasting is measured separately by rolling origin — train on every season before T, score T — because that is what the inference pipeline actually does, and it is the number that would be wrong to quote as if it were the same thing.

Baseline XGBoostGrabit v8.0x
Selection CV R² (n = 944)0.78250.7989
Selection CV MAE$3.28M$2.92M
CV R² (2024–26, n = 324)0.82950.8515
Forward R² (rolling origin)0.83010.8356
Calibration slope1.0210.973

Forward R² carries a 95% interval of [0.75, 0.89]. A baseline ladder keeps the absolute number honest: minutes per game alone reaches 0.575 against the full model’s 0.799, so the other thirteen features buy the last 0.224. A 15%-of-players confirmation split, held out of every selection decision, scores 0.831 against 0.793 on the selection pool.

Judging a targeted intervention where it acts

A pooled score averages each targeted intervention over rows it never touches, so v8 keeps a per-zone scorecard beside the headline metric. In the max-censored zone, 68 of 70 rows improve and MAE falls by $2.91M. In the floor zone, MAE falls by $0.43M across 240 rows. Stage 3 moves only 13 extension rows, improves all 13, and lowers extension-zone MAE by $0.64M. The full champion still clears the pooled selection check: +0.0086 R² over the clip-only arm, while Stage 3 alone contributes +0.0128. The point is not to excuse a weak aggregate; it is to measure each narrow intervention where its arithmetic can actually change a prediction.

A large residual pattern that cannot be fixed

Grouping errors by the CBA mechanism a contract was signed under exposes structure the performance features cannot see. Teams holding Bird Rights may exceed the cap to keep their own player, and pay a retention premium with no feature behind it. Minimum and mid-level deals run the other way.

The tempting inference — model the mechanism, recover the error — does not work, and establishing that is one of the more useful results here. Mechanism is itself a function of the same player features, so a fold-honest P(mechanism | x) *lowers* CV R² by 0.0073. Even a leaky oracle handing the model the realised mechanism gains only +0.0137. The residual gap is the spread of a genuinely bimodal conditional: given the same inputs a player either lands a market deal or takes a minimum, and the conditional mean sits correctly between the two modes while being wrong for both. Any real gain has to come from information that is not a function of the current features.

Signing mechanismnModel bias
Bird Rights274−$2.33M
Early Bird46−$1.73M
Cap Space76−$0.24M
MLE123+$0.58M
Minimum242+$1.99M
Non-Bird20+$2.07M

Negative bias means the model underprices. Mechanism is held out of the feature set on purpose — that is what keeps these residuals usable as a diagnostic rather than something the model has already absorbed.

A correction worth stating plainly

An earlier version of this work reported that the model underpaid stars by up to $6M, read off residuals binned by actual salary. That gradient is an artifact: binning by a noisy target produces it even for a perfectly calibrated model, because the bin edges select on the noise. Binned by predicted value instead, the v8 champion’s calibration slope is 0.973; the remaining high-end miss is visible rather than hidden, with +$1.42M bias in the 25%+ band. Fold-honest recalibration still fails to improve the model.

The same trap has a second form, met while comparing two models: cutting segments on either model’s own predictions lets band composition shift between them and manufactures a difference that was never there. Model comparisons here assign rows to segments using something neither model produced.

Highlights

  • Adapted Grabit (gradient tree-boosted Tobit) from credit-risk modelling to NBA salaries, then composed its two-sided censoring with a probability-gated max push 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 14 features from 20+ candidates by ablation, and a four-layer evaluation protocol that separates structural estimation from forecasting and selection from confirmation
  • Established a characterised negative result — the large residual pattern by signing mechanism is real but unrecoverable, with the ceiling on any fix measured rather than assumed