Challenges · C10 · ★★★★★

Who draws the whistle? An item-response model of NBA foul calls

Difficulty
★★★★★
Time
5-6 hours
Data
NBA Last Two Minute reports 2015-2021: 46,861 reviewed plays with the player who (may have) committed a foul, the player who was disadvantaged, and the league's verdict
Skills
Item-response (Rasch) models · crossed random effects with ~1,600 parameters · identifiability and location invariance · centred vs non-centred geometry · coords/dims with large index arrays · nutpie · group-level predictors · stratified posterior predictive checks · shrinkage · decisions about ranks

The brief

Close NBA games are decided at the free-throw line. A team's analytics group is preparing for the trade deadline and asks two questions:

  1. Which players are best at drawing fouls late in close games, and which defenders are best at not being whistled?
  2. Both answers must be separated from the matchup: a guard who is always defended by foul-prone opponents looks good for free, and a centre who only ever guards stars looks bad for free.

Their intern ranked players by raw foul rate. The top of the list is a set of players nobody has heard of, each at 100%. They would like something better - and they want to know how far to trust any ranking at all.

Since 2015 the league has reviewed every material play in the last two minutes of close games and published its verdict. Each row of the data is one reviewed play.

How this notebook works

  • Each task states what to deliver, not how. Write your code in the YOUR CODE HERE cells.
  • Stuck? h.hint("task2") reveals hints one level at a time: nudge → approach → code skeleton. Try to get by on nudges.
  • h.check("task2", sigma_theta=...) compares your numbers with the reference solution.
  • A full worked solution lives in notebooks/solutions/. Open it only when you are done (or truly stuck).

The models here have about 1,600 parameters and 47,000 observations. pm.sample(nuts_sampler="nutpie") fits each in roughly a minute; budget accordingly.

import time

import arviz as az
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import pymc as pm
from scipy import stats
from scipy.special import expit

from pymc_challenges import Hints, data

RANDOM_SEED = 2021
rng = np.random.default_rng(RANDOM_SEED)
az.style.use("arviz-variat")

h = Hints("C10")
h.tasks()

C10 - Who draws the whistle? An item-response model of NBA foul calls

  • task0 - Define the outcome, look at raw rates (3 hints, 2 check(s))
  • task1 - A colleague's Rasch model and what is wrong with it (3 hints, 1 check(s))
  • task2 - Identification and centred vs non-centred (3 hints, 3 check(s))
  • task3 - Position as a group-level predictor (3 hints, 2 check(s))
  • task4 - Posterior predictive check by position pair (3 hints)
  • task5 - Shrinkage - raw ranking vs posterior ranking (3 hints, 1 check(s))
  • task6 - Rank uncertainty and a specific matchup (3 hints, 2 check(s))

The data

column meaning
committing the player who committed the foul - or would have, had one been called
disadvantaged the player on the receiving end
decision the league's review: CC correct call, IC incorrect call, CNC correct no-call, INC incorrect no-call
committing_position, disadvantaged_position G(uard), F(orward), C(entre) and hybrids such as G-F
data.describe("nba_fouls")
fouls = data.load("nba_fouls")
fouls.head()
nba_fouls: Foul calls / non-calls in close games, with committing and disadvantaged player.
  source: NBA Last Two Minute reports 2015-2021, via pymc-examples.
  url:    https://raw.githubusercontent.com/pymc-devs/pymc-examples/main/examples/data/item_response_nba.csv
committing disadvantaged decision committing_position disadvantaged_position
play_id
1 Avery Bradley Stephen Curry CNC G G
3 Isaiah Thomas Andre Iguodala CNC G G-F
4 Jae Crowder Harrison Barnes INC F F
6 Draymond Green Isaiah Thomas CC F G
7 Avery Bradley Stephen Curry CC G G
fouls.decision.value_counts()
decision
CNC    33401
CC     10498
INC     2540
IC       422
Name: count, dtype: int64

Task 0 · Define the outcome, then look at raw rates

Deliver

  1. A binary outcome built from decision, with a justification: the four codes allow more than one defensible definition - which one answers the team's question?
  2. The intern's table: the ten players with the highest raw rate of drawing fouls, with the number of plays each rate is based on.
  3. A plot of raw foul-drawing rate against number of plays for every player, and one sentence on why ranking by raw rate is a bad idea.
Your turn Work this task in the notebook.

Task 1 · A colleague's Rasch model

Item-response theory was built for exams: the probability that student $i$ answers question $j$ correctly depends on the student's ability minus the question's difficulty. Here the "student" is the disadvantaged player and the "question" is the committing player:

$$\text{logit}\,P(\text{foul called}) = \mu + \theta_{\text{disadvantaged}} - b_{\text{committing}}$$

A colleague proposes the natural hierarchical version, "with weakly-informative priors":

$$\theta_i \sim \text{Normal}(\mu_\theta, \sigma_\theta), \quad b_j \sim \text{Normal}(\mu_b, \sigma_b), \quad \mu \sim \text{Normal}(0, 1.5), \quad \mu_\theta, \mu_b \sim \text{Normal}(0, 5), \quad \sigma_\theta, \sigma_b \sim \text{HalfNormal}(1)$$

Deliver

  1. This model, fitted to all plays, with named dimensions for the two sets of players.
  2. Diagnostics for the five top-level parameters. Something is badly wrong. Show with a plot which direction in parameter space the sampler cannot resolve.
  3. Find a function of $\mu, \mu_\theta, \mu_b$ that is well determined - report its posterior mean and r_hat - and explain in two sentences what the data can and cannot tell you in this model.
Your turn Work this task in the notebook.

Task 2 · Identify it, then worry about geometry

Deliver

  1. An identified version of the model, with a justification of the constraint you chose (there is more than one), and a prior predictive check on the probability scale.
  2. Fits of both a centred and a non-centred parameterisation of the player effects. Compare them on divergences and on the effective sample size of $\mu$, $\sigma_\theta$ and $\sigma_b$, and choose one.
  3. An explanation of the difference in terms of the data: which players favour which parameterisation, and how many of each are there? Show the relevant posterior geometry for one player of each kind.
Your turn Work this task in the notebook.

Task 3 · Does position explain it?

Guards handle the ball and attack the rim; centres set screens and protect it. Give both skills a group-level predictor: a player's effect is drawn around the mean of his position.

Deliver

  1. The model with position means for both $\theta$ and $b$, identified, with clean diagnostics.
  2. The posterior of the difference between guards (G) and centres (C) in foul-drawing skill, and the same for $b$. Do guards draw more fouls than centres?
  3. What happens to $\sigma_\theta$ and $\sigma_b$ compared with Task 2, and what does that tell you? Be careful about what a play is in this dataset before you interpret the position effects on $b$.
Your turn Work this task in the notebook.

Task 4 · Check it where it could fail

The model is additive on the logit scale: a guard's skill is the same whether a guard or a centre is defending him. That is a strong claim with 49 position pairings to test it on.

Deliver a posterior predictive check of the foul-call rate for every pair of (disadvantaged position, committing position), for the models of Task 2 and Task 3, with a verdict: is either model adequate for the team's purpose? Where, if anywhere, does additivity fail?

Your turn Work this task in the notebook.

Task 5 · What shrinkage buys you

Deliver

  1. For every player, the model's estimate of his foul-drawing rate against an average defender, plotted against his raw rate, with the number of plays visible in the plot.
  2. The top ten by raw rate next to the top ten by posterior mean. How many plays does the least-observed player in each list have?
  3. Two sentences for the intern on what the model did to the 100% players, and why that is not "throwing data away".
Your turn Work this task in the notebook.

Task 6 · Rankings the front office can use

A posterior mean is still a point estimate, and the front office will read a ranked list as if rank 3 were better than rank 7.

Deliver

  1. For the leading candidates: the posterior probability of being among the ten best foul-drawers in the league, and a 94% interval for each player's rank. Is anybody in the top ten with probability above one half?
  2. The same treatment, briefly, for defenders who avoid the whistle.
  3. A specific matchup: James Harden has the ball and Rudy Gobert is defending. The posterior of P(foul called) with an interval - and the same with an average centre in place of Gobert, to show what the defender is worth.
  4. Three sentences for the front office on how to use (and not use) the list.
Your turn Work this task in the notebook.

Going further

  • One player, two skills. Most players appear in both roles. Give each player a bivariate effect $(\theta_i, b_i)$ with an LKJCholeskyCov prior. Are good foul-drawers also whistle-prone defenders?
  • Matchup interactions. Task 4 tests additivity only at the position level. Add a position-pair interaction (ZeroSumNormal over two dims) and compare with LOO. Is it worth 49 more parameters?
  • Referee error instead of whistles. Refit with the outcome "the call was wrong" (IC or INC). Do some players get the benefit of the doubt?
  • Does skill drift? The file has no dates, but the reports do. Get the season for each play from the original L2M data and let $\theta$ follow a random walk across seasons.
h.progress()

C10: 0/21 hints used, 11/11 checks passed.

Reference solution

A complete, executed solution with commentary on every task — including the ways the obvious model fails. Try the hint ladder first.

Open the worked solution →