Basketball statistics, at their most fundamental level, are counting stats: points scored, rebounds grabbed, assists distributed, shots attempted. However, these raw numbers can be deeply misleading when comparing players across different contexts...
In This Chapter
- Introduction
- 7.1 The Problem with Counting Statistics
- 7.2 Per-Minute Statistics
- 7.3 Per-Possession Statistics
- 7.4 Pace: Definition and Calculation
- 7.5 League Pace Trends Over Time
- 7.6 Pace-Adjusted Player Comparisons
- 7.7 Era Adjustments for Historical Comparisons
- 7.8 The Possessions Formula: Deep Dive
- 7.9 When to Use Rate Stats vs. Counting Stats
- 7.10 Advanced Rate Concepts
- 7.11 Practical Implementation: A Complete Example
- 7.12 Common Pitfalls and Best Practices
- 7.13 Summary
- References
Chapter 7: Rate Statistics and Pace Adjustment
Introduction
Basketball statistics, at their most fundamental level, are counting stats: points scored, rebounds grabbed, assists distributed, shots attempted. However, these raw numbers can be deeply misleading when comparing players across different contexts. A player averaging 20 points per game on a team that plays at a frenetic pace with 105 possessions per game is operating in a fundamentally different environment than a player scoring 20 points on a team that methodically works through 90 possessions per contest.
Rate statistics solve this problem by normalizing counting stats to a common baseline, whether that baseline is minutes played, possessions used, or some other denominator. This chapter explores the mathematics, methodology, and practical applications of rate statistics in professional basketball analytics.
The concept is straightforward: instead of asking "how many points did this player score?", we ask "how many points did this player score per unit of opportunity?" This simple reframing transforms our ability to make meaningful comparisons across players, teams, eras, and playing styles.
7.1 The Problem with Counting Statistics
7.1.1 Playing Time Disparities
Consider two hypothetical players from the same season:
| Player | Games | Minutes | Points | PPG |
|---|---|---|---|---|
| Player A | 82 | 2,870 | 1,640 | 20.0 |
| Player B | 82 | 2,050 | 1,230 | 15.0 |
At first glance, Player A appears to be the superior scorer with 5 more points per game. But let's calculate their scoring rates:
- Player A: 1,640 points / 2,870 minutes = 0.571 points per minute
- Player B: 1,230 points / 2,050 minutes = 0.600 points per minute
Player B actually scores at a higher rate when given the opportunity. The difference in their per-game averages is entirely attributable to playing time, not scoring ability.
7.1.2 Pace Disparities
The 2020-21 Sacramento Kings played at a pace of approximately 101.4 possessions per 48 minutes, while the Miami Heat played at roughly 96.6 possessions per 48 minutes. This 5% difference in pace means that a Kings player, all else being equal, would have approximately 5% more opportunities to accumulate statistics simply by virtue of team context.
7.1.3 Era Disparities
The pace problem becomes even more pronounced when comparing across eras. In the 1961-62 season, the average NBA team used approximately 130 possessions per game. By the 1998-99 season, that number had dropped to around 88 possessions. A direct comparison of counting stats between Wilt Chamberlain's 50.4 PPG in 1961-62 and Michael Jordan's 32.6 PPG in 1997-98 is fundamentally flawed without accounting for the 48% difference in league pace.
7.2 Per-Minute Statistics
7.2.1 The Per 36 Minutes Convention
The most common per-minute rate statistic in basketball is "per 36 minutes" (sometimes written as P36 or /36). The choice of 36 minutes as the baseline is not arbitrary—it represents a typical starter's workload in a 48-minute game, accounting for normal rest and substitution patterns.
The formula is straightforward:
$$\text{Stat per 36} = \frac{\text{Raw Stat}}{\text{Minutes Played}} \times 36$$
For example, if a player scores 450 points in 900 minutes:
$$\text{Points per 36} = \frac{450}{900} \times 36 = 18.0$$
7.2.2 Per 40 Minutes: The College Standard
College basketball games are 40 minutes long, so the per-40 minute rate is often used when analyzing college players or comparing college production to NBA production:
$$\text{Stat per 40} = \frac{\text{Raw Stat}}{\text{Minutes Played}} \times 40$$
7.2.3 Per 48 Minutes: The Full Game Projection
Per 48 minute statistics project what a player would produce if they played an entire regulation NBA game:
$$\text{Stat per 48} = \frac{\text{Raw Stat}}{\text{Minutes Played}} \times 48$$
This rate is particularly useful for understanding a player's maximum theoretical impact in a regulation game.
7.2.4 Implementation in Python
def calculate_per_minute_stats(stats_dict, minutes_played, baseline=36):
"""
Convert raw counting stats to per-minute rate stats.
Parameters:
-----------
stats_dict : dict
Dictionary of raw counting statistics
minutes_played : float
Total minutes played
baseline : int
Minutes baseline for rate calculation (36, 40, or 48)
Returns:
--------
dict : Per-minute statistics
"""
if minutes_played <= 0:
raise ValueError("Minutes played must be positive")
rate_stats = {}
for stat, value in stats_dict.items():
rate_stats[f"{stat}_per_{baseline}"] = (value / minutes_played) * baseline
return rate_stats
# Example usage
raw_stats = {
'points': 1640,
'rebounds': 520,
'assists': 410,
'steals': 95,
'blocks': 45
}
per_36 = calculate_per_minute_stats(raw_stats, minutes_played=2870, baseline=36)
print(per_36)
# Output: {'points_per_36': 20.56, 'rebounds_per_36': 6.52, ...}
7.2.5 Limitations of Per-Minute Statistics
Per-minute statistics have significant limitations that analysts must understand:
1. Small Sample Size Inflation
A player who plays 50 minutes across a season and scores 15 points would have a per-36 of 10.8 PPG—but this tells us almost nothing meaningful. Per-minute stats become unreliable with small samples.
A common threshold is requiring a minimum of 500-1000 minutes before treating per-minute stats as reliable. Some analysts use games played thresholds (e.g., 40+ games) in combination with minutes thresholds.
2. Non-Linear Scaling
Player performance does not scale linearly with playing time. A player who is highly efficient in 15 minutes per game would likely see efficiency decline if given 35 minutes due to fatigue, defensive attention, and role expansion.
3. Role Context
A bench player's per-36 statistics are often inflated because they face weaker competition (opposing bench units) and play in specific favorable situations (garbage time, mismatches).
4. No Pace Adjustment
Per-minute stats still do not account for pace differences between teams or eras.
7.3 Per-Possession Statistics
7.3.1 The Logic of Possession-Based Rates
A possession is a single opportunity for a team to score. By measuring statistics per possession rather than per minute, we control for the fundamental unit of basketball opportunity.
The most common convention is per 100 possessions, which provides numbers that are roughly comparable to traditional per-game statistics:
$$\text{Stat per 100 Poss} = \frac{\text{Raw Stat}}{\text{Possessions}} \times 100$$
7.3.2 Team Offensive and Defensive Rating
The most widely used per-possession statistics are Offensive Rating (ORtg) and Defensive Rating (DRtg):
$$\text{Offensive Rating} = \frac{\text{Points Scored}}{\text{Possessions}} \times 100$$
$$\text{Defensive Rating} = \frac{\text{Points Allowed}}{\text{Possessions}} \times 100$$
These metrics tell us how many points a team scores or allows per 100 possessions, making comparison across different pace environments possible.
For the 2022-23 season, league average was approximately 114.8 points per 100 possessions. Teams above this threshold had above-average offenses; teams below had below-average offenses.
7.3.3 Individual Per-Possession Statistics
Individual players can also be evaluated on a per-possession basis. For counting stats, we typically use team possessions while the player is on the floor:
$$\text{Player Points per 100 Poss} = \frac{\text{Player Points}}{\text{Team Possessions with Player On}} \times 100$$
This requires play-by-play data to calculate precisely, as we need to track possessions during the specific minutes each player is on the court.
7.3.4 Usage-Adjusted Per-Possession Statistics
Dean Oliver's individual offensive rating takes this further by calculating how many points a player produces per 100 possessions that they personally use:
$$\text{Individual ORtg} = \frac{\text{Points Produced}}{\text{Individual Possessions Used}} \times 100$$
We will explore this metric in detail in Chapter 10 on individual offensive rating.
7.4 Pace: Definition and Calculation
7.4.1 What is Pace?
Pace is the number of possessions a team uses per some unit of time, typically 48 minutes (a full NBA game) or 40 minutes (a full college game). It measures how fast a team plays—how quickly they move through offensive and defensive possessions.
$$\text{Pace} = \frac{\text{Possessions}}{\text{Minutes Played}} \times 48$$
High-pace teams push the ball up the court quickly, take early shots, and generate many possessions. Low-pace teams slow the game down, work through their half-court offense, and generate fewer possessions.
7.4.2 The Possessions Formula
Since possessions are not officially tracked in the NBA box score, we must estimate them. The standard formula, developed by Dean Oliver and refined by others, is:
$$\text{Possessions} = \text{FGA} + 0.44 \times \text{FTA} - \text{ORB} + \text{TOV}$$
Where: - FGA = Field Goal Attempts - FTA = Free Throw Attempts - ORB = Offensive Rebounds - TOV = Turnovers
Understanding Each Component:
-
Field Goal Attempts (FGA): Every shot attempt ends a possession, whether made or missed.
-
Free Throw Attempts (0.44 x FTA): Free throws are complex because: - Two-shot fouls: 2 FTA = 1 possession (effectively) - Three-shot fouls: 3 FTA = 1 possession - And-one fouls: 1 FTA = 0 additional possessions - Technical free throws: 1 FTA = 0 possessions (no change of possession)
The 0.44 multiplier is an empirically-derived estimate that accounts for this mix. Some analysts use 0.40 or 0.44 depending on era and league.
-
Offensive Rebounds (ORB): An offensive rebound extends a possession rather than starting a new one, so we subtract ORB to avoid double-counting.
-
Turnovers (TOV): Every turnover ends a possession without a shot attempt.
7.4.3 Refined Possessions Formulas
Several refinements to the basic formula exist:
Basketball-Reference Formula: $$\text{Poss} = 0.5 \times \left[(\text{FGA} + 0.4 \times \text{FTA} - 1.07 \times \frac{\text{ORB}}{\text{ORB} + \text{Opp DRB}} \times (\text{FGA} - \text{FG}) + \text{TOV}) + (\text{Opp FGA} + 0.4 \times \text{Opp FTA} - 1.07 \times \frac{\text{Opp ORB}}{\text{Opp ORB} + \text{DRB}} \times (\text{Opp FGA} - \text{Opp FG}) + \text{Opp TOV})\right]$$
This formula averages the possessions calculated from both teams' perspectives to reduce estimation error, and uses a more sophisticated offensive rebound adjustment.
Simplified Team Possessions: $$\text{Poss} = \text{FGA} - \text{ORB} + \text{TOV} + 0.475 \times \text{FTA}$$
The 0.475 coefficient has been found to be more accurate for modern NBA play.
7.4.4 Python Implementation
def estimate_possessions(fga, fta, orb, tov, ft_coefficient=0.44):
"""
Estimate team possessions using the standard formula.
Parameters:
-----------
fga : int
Field goal attempts
fta : int
Free throw attempts
orb : int
Offensive rebounds
tov : int
Turnovers
ft_coefficient : float
Free throw coefficient (0.40-0.475 depending on era)
Returns:
--------
float : Estimated possessions
"""
return fga + (ft_coefficient * fta) - orb + tov
def estimate_possessions_refined(team_stats, opp_stats, ft_coefficient=0.4):
"""
Estimate possessions using the refined Basketball-Reference formula.
Parameters:
-----------
team_stats : dict
Dictionary with keys: fga, fg, fta, orb, tov
opp_stats : dict
Dictionary with keys: fga, fg, fta, orb, drb, tov
ft_coefficient : float
Free throw coefficient
Returns:
--------
float : Estimated possessions
"""
# Team perspective
team_orb_pct = team_stats['orb'] / (team_stats['orb'] + opp_stats['drb'])
team_poss = (team_stats['fga'] + ft_coefficient * team_stats['fta']
- 1.07 * team_orb_pct * (team_stats['fga'] - team_stats['fg'])
+ team_stats['tov'])
# Opponent perspective
opp_orb_pct = opp_stats['orb'] / (opp_stats['orb'] + team_stats.get('drb', 0))
opp_poss = (opp_stats['fga'] + ft_coefficient * opp_stats['fta']
- 1.07 * opp_orb_pct * (opp_stats['fga'] - opp_stats['fg'])
+ opp_stats['tov'])
# Average both perspectives
return 0.5 * (team_poss + opp_poss)
def calculate_pace(possessions, minutes_played, baseline=48):
"""
Calculate pace (possessions per 48 minutes or other baseline).
Parameters:
-----------
possessions : float
Total possessions
minutes_played : float
Total minutes played
baseline : int
Minutes baseline (48 for NBA, 40 for college)
Returns:
--------
float : Pace
"""
return (possessions / minutes_played) * baseline
7.5 League Pace Trends Over Time
7.5.1 Historical Pace Data
NBA pace has varied dramatically throughout the league's history:
| Era | Approximate Pace | Context |
|---|---|---|
| 1960-61 to 1967-68 | 115-130 | Pre-modern era, fast-paced game |
| 1968-69 to 1977-78 | 105-115 | Gradual slowdown begins |
| 1978-79 to 1987-88 | 100-105 | Introduction of 3-point line (1979) |
| 1988-89 to 1998-99 | 90-97 | "Slowdown era," defensive emphasis |
| 1999-00 to 2003-04 | 90-93 | Post-Jordan malaise, isolation heavy |
| 2004-05 to 2014-15 | 92-96 | Rule changes increase scoring |
| 2015-16 to present | 97-102 | Modern pace-and-space era |
7.5.2 The Slowest Season in NBA History
The 1998-99 season, shortened to 50 games due to a lockout, featured the slowest pace in NBA history at approximately 88.9 possessions per 48 minutes. Several factors contributed:
- Physical defensive play was at its peak before hand-checking rules were relaxed
- The three-point shot was still underutilized
- Post-up and isolation plays dominated offensive schemes
- The season's abbreviated nature may have affected team preparation
7.5.3 The Fastest Modern Seasons
The 2020-21 season saw pace reach 99.2 possessions per 48 minutes, the fastest since the early 1990s. Factors include:
- Emphasis on transition offense and early shot clock attempts
- High three-point attempt rates creating more rebounds and fast break opportunities
- Improved conditioning and sports science
- Rule changes penalizing defensive disruption
7.5.4 Visualizing Pace Trends
import matplotlib.pyplot as plt
import numpy as np
def plot_pace_trends():
"""
Plot historical NBA pace trends.
"""
# Historical pace data (approximate league averages)
seasons = list(range(1974, 2024))
pace_data = [
# 1974-1983
107.8, 104.5, 105.5, 106.5, 106.7, 105.8, 103.1, 101.8, 100.9, 101.4,
# 1984-1993
101.4, 102.1, 100.8, 99.6, 100.6, 100.3, 98.3, 97.8, 96.6, 95.8,
# 1994-2003
95.1, 93.9, 91.8, 90.9, 90.3, 88.9, 93.1, 91.3, 90.7, 91.0,
# 2004-2013
90.5, 90.9, 91.9, 92.4, 92.7, 93.4, 92.7, 92.1, 91.3, 92.0,
# 2014-2023
93.9, 95.8, 96.4, 97.3, 97.3, 100.0, 99.2, 98.2, 99.2, 99.5
]
plt.figure(figsize=(14, 6))
plt.plot(seasons, pace_data, 'b-', linewidth=2)
plt.fill_between(seasons, pace_data, alpha=0.3)
# Add era annotations
plt.axvspan(1974, 1984, alpha=0.1, color='green', label='High Pace Era')
plt.axvspan(1995, 2004, alpha=0.1, color='red', label='Slowdown Era')
plt.axvspan(2015, 2024, alpha=0.1, color='blue', label='Modern Pace-and-Space')
plt.xlabel('Season', fontsize=12)
plt.ylabel('Pace (Possessions per 48 min)', fontsize=12)
plt.title('NBA League Average Pace: 1974-2023', fontsize=14)
plt.grid(True, alpha=0.3)
plt.legend()
plt.tight_layout()
return plt.gcf()
7.6 Pace-Adjusted Player Comparisons
7.6.1 The Need for Pace Adjustment
When comparing players on different teams or from different eras, pace adjustment is essential. The formula to convert a counting stat to a pace-adjusted rate:
$$\text{Pace-Adjusted Stat} = \frac{\text{Raw Stat}}{\text{Team Possessions}} \times \text{League Average Pace}$$
Or equivalently:
$$\text{Pace-Adjusted Stat} = \text{Raw Stat} \times \frac{\text{League Average Pace}}{\text{Team Pace}}$$
7.6.2 Worked Example: Comparing Scorers
Consider two players from the 2022-23 season:
| Player | Team | PPG | Team Pace |
|---|---|---|---|
| Player X | Team A | 28.5 | 102.3 |
| Player Y | Team B | 27.2 | 95.8 |
League average pace: 99.5
Pace-Adjusted PPG:
$$\text{Player X Adjusted} = 28.5 \times \frac{99.5}{102.3} = 27.72$$
$$\text{Player Y Adjusted} = 27.2 \times \frac{99.5}{95.8} = 28.25$$
After pace adjustment, Player Y actually has the higher scoring rate. The 1.3 PPG gap in raw numbers was entirely attributable to team pace—and in fact reverses direction after adjustment.
7.6.3 Python Implementation
def pace_adjust_stat(raw_stat, team_pace, league_pace):
"""
Adjust a counting stat for pace differences.
Parameters:
-----------
raw_stat : float
The raw counting statistic
team_pace : float
The team's pace
league_pace : float
The league average pace
Returns:
--------
float : Pace-adjusted statistic
"""
return raw_stat * (league_pace / team_pace)
def pace_adjust_player_stats(player_stats, team_pace, league_pace):
"""
Adjust all counting stats for a player.
Parameters:
-----------
player_stats : dict
Dictionary of raw counting statistics
team_pace : float
The team's pace
league_pace : float
The league average pace
Returns:
--------
dict : Pace-adjusted statistics
"""
adjusted = {}
pace_factor = league_pace / team_pace
for stat, value in player_stats.items():
adjusted[f"{stat}_adj"] = value * pace_factor
return adjusted
def compare_players_pace_adjusted(player1, player2, league_pace):
"""
Compare two players with pace adjustment.
Parameters:
-----------
player1 : dict
Must contain 'stats' (dict) and 'team_pace' (float)
player2 : dict
Must contain 'stats' (dict) and 'team_pace' (float)
league_pace : float
League average pace for normalization
Returns:
--------
dict : Comparison results
"""
p1_adjusted = pace_adjust_player_stats(
player1['stats'], player1['team_pace'], league_pace
)
p2_adjusted = pace_adjust_player_stats(
player2['stats'], player2['team_pace'], league_pace
)
comparison = {
'player1_adjusted': p1_adjusted,
'player2_adjusted': p2_adjusted,
'differences': {}
}
for stat in player1['stats'].keys():
adj_stat = f"{stat}_adj"
if adj_stat in p1_adjusted and adj_stat in p2_adjusted:
comparison['differences'][stat] = (
p1_adjusted[adj_stat] - p2_adjusted[adj_stat]
)
return comparison
7.6.4 Per 100 Possessions as an Alternative
Instead of adjusting stats to league average pace, many analysts prefer expressing stats per 100 team possessions:
$$\text{Stat per 100 Poss} = \frac{\text{Raw Stat}}{\text{Team Possessions}} \times 100$$
This approach is simpler and doesn't require choosing a league average as a baseline. It's particularly useful when comparing players from different eras, as each player's production is expressed relative to their own team's opportunities.
7.7 Era Adjustments for Historical Comparisons
7.7.1 The Challenge of Cross-Era Comparison
Comparing Wilt Chamberlain to LeBron James, or Bob Cousy to Stephen Curry, requires accounting for dramatic differences in:
- Pace: 130 possessions per game in 1962 vs. 100 in 2023
- Three-point shooting: Non-existent before 1979
- Defensive rules: Hand-checking, zone defenses, etc.
- Competition level: League size, international talent, etc.
- Training and conditioning: Modern sports science advantages
7.7.2 The Relative-to-Era Approach
One approach is to express each player's statistics relative to their era's average:
$$\text{Relative Stat} = \frac{\text{Player Stat}}{\text{League Average Stat}}$$
A relative stat of 1.5 means the player was 50% above league average, regardless of era.
7.7.3 Standard Deviation Approach (Z-Scores)
A more statistically rigorous approach uses z-scores:
$$z = \frac{\text{Player Stat} - \text{League Mean}}{\text{League Standard Deviation}}$$
This accounts not just for the mean but for the spread of the distribution. A z-score of +2.0 means the player was 2 standard deviations above average—an elite performance in any era.
7.7.4 Pace-and-Era Adjusted Statistics
The most comprehensive approach combines pace adjustment with era normalization:
$$\text{Adjusted Stat} = \frac{\text{Player Stat per 100 Poss}}{\text{League Avg Stat per 100 Poss}} \times \text{Modern League Avg}$$
This expresses a historical player's production in terms of what their rate production would translate to in today's game.
7.7.5 Python Implementation
import numpy as np
from scipy import stats
def calculate_z_score(player_stat, league_mean, league_std):
"""
Calculate z-score for a player's statistic.
Parameters:
-----------
player_stat : float
The player's statistic
league_mean : float
League average for that statistic
league_std : float
League standard deviation
Returns:
--------
float : Z-score
"""
if league_std <= 0:
raise ValueError("Standard deviation must be positive")
return (player_stat - league_mean) / league_std
def era_adjust_stat(player_stat, player_era_league_avg, target_era_league_avg):
"""
Adjust a statistic from one era to another.
Parameters:
-----------
player_stat : float
The player's raw statistic
player_era_league_avg : float
League average in the player's era
target_era_league_avg : float
League average in the target era
Returns:
--------
float : Era-adjusted statistic
"""
relative_performance = player_stat / player_era_league_avg
return relative_performance * target_era_league_avg
def comprehensive_era_adjustment(player_stat, player_team_pace,
player_era_league_pace, player_era_league_avg,
target_era_league_pace, target_era_league_avg):
"""
Perform comprehensive era and pace adjustment.
Parameters:
-----------
player_stat : float
Raw counting statistic
player_team_pace : float
Player's team pace
player_era_league_pace : float
League average pace in player's era
player_era_league_avg : float
League average of the stat in player's era
target_era_league_pace : float
League average pace in target era
target_era_league_avg : float
League average of the stat in target era
Returns:
--------
float : Fully adjusted statistic
"""
# Step 1: Pace-adjust within era
pace_adjusted = player_stat * (player_era_league_pace / player_team_pace)
# Step 2: Calculate relative performance
relative_performance = pace_adjusted / player_era_league_avg
# Step 3: Apply to target era
return relative_performance * target_era_league_avg
class EraAdjuster:
"""
Class for performing era adjustments on player statistics.
"""
def __init__(self, era_data):
"""
Initialize with historical era data.
Parameters:
-----------
era_data : dict
Dictionary with years as keys and dicts containing
'pace', 'ppg_avg', 'rpg_avg', etc. as values
"""
self.era_data = era_data
def adjust_to_era(self, player_stats, from_year, to_year):
"""
Adjust player stats from one year to another.
Parameters:
-----------
player_stats : dict
Player's raw statistics
from_year : int
Year of the player's statistics
to_year : int
Target year for adjustment
Returns:
--------
dict : Adjusted statistics
"""
if from_year not in self.era_data or to_year not in self.era_data:
raise ValueError("Year not found in era data")
from_era = self.era_data[from_year]
to_era = self.era_data[to_year]
adjusted = {}
for stat, value in player_stats.items():
if f"{stat}_avg" in from_era and f"{stat}_avg" in to_era:
adjusted[stat] = era_adjust_stat(
value,
from_era[f"{stat}_avg"],
to_era[f"{stat}_avg"]
)
return adjusted
7.7.6 Case Study: Wilt Chamberlain's 50.4 PPG Season
In 1961-62, Wilt Chamberlain averaged 50.4 points per game while playing 48.5 minutes per game on a team with approximately 130 possessions per 48 minutes.
League average scoring that season: approximately 118.8 points per team per game Modern league average (2022-23): approximately 114.7 points per team per game
Step 1: Calculate Wilt's per-100-possessions scoring
Assuming Wilt's team (Philadelphia Warriors) used 130 possessions per 48 minutes:
$$\text{Wilt PPG per 100 Poss} = \frac{50.4}{130} \times 100 = 38.77$$
Step 2: Compare to modern context
In 2022-23, with pace at ~100 possessions per 48 minutes:
$$\text{Modern-Pace Equivalent} = 38.77 \times \frac{100}{100} = 38.77 \text{ PPG}$$
Even after pace adjustment, Wilt's scoring rate remains historically unprecedented. No modern player has approached 39 points per 100 possessions over a full season.
7.8 The Possessions Formula: Deep Dive
7.8.1 Why 0.44 for Free Throws?
The free throw coefficient requires careful consideration. Let's examine the math:
| FT Situation | FTA | Possessions Used |
|---|---|---|
| Two-shot foul | 2 | ~0.5 each = 1.0 total |
| Three-shot foul | 3 | ~0.33 each = 1.0 total |
| And-one | 1 | 0 (possession already counted in FGA) |
| Technical FT | 1 | 0 (no possession change) |
| Flagrant FT | 2 | 0 (no possession change) |
The weighted average across all these situations, based on historical distributions, yields approximately 0.44. However, this varies by:
- Era: More three-point shooting means more three-shot fouls
- Player: High-usage scorers may have different foul-drawn profiles
- Team style: Teams that attack the rim more draw different foul types
7.8.2 The Offensive Rebound Complication
Offensive rebounds present a philosophical question: should an offensive rebound be considered a "new" possession or an extension of the existing one?
The standard formula treats ORBs as possession extensions (hence the subtraction). However, some analysts argue for treating them as partial new possessions, since the defense had an opportunity to secure the ball.
Advanced formulas use offensive rebound percentage to weight the ORB adjustment:
$$\text{ORB Adjustment} = 1.07 \times \text{ORB\%} \times (\text{FGA} - \text{FG})$$
The 1.07 coefficient accounts for the fact that not all missed shots result in rebound opportunities (e.g., shots that go out of bounds).
7.8.3 Team vs. Individual Possessions
Team possessions differ from individual possessions used. Dean Oliver defined individual possessions as:
$$\text{Individual Poss} = \text{FGA} + 0.44 \times \text{FTA} + \text{TOV} - \text{ORB} \times \text{Team Play\%}$$
Where Team Play% accounts for possessions where multiple players were involved before the final outcome.
This concept underlies Usage Rate:
$$\text{Usage\%} = \frac{\text{Individual Possessions}}{\text{Team Possessions while on court}} \times 100$$
We will explore Usage Rate in detail in Chapter 9.
7.9 When to Use Rate Stats vs. Counting Stats
7.9.1 Advantages of Rate Statistics
1. Fair Comparison Across Contexts Rate stats enable meaningful comparisons between players with different playing times, on different teams, and from different eras.
2. Efficiency Measurement Rate stats tell us how effectively a player uses their opportunities, not just how many opportunities they receive.
3. Predictive Power Rate statistics often have better predictive validity for future performance than counting stats, because they're less dependent on role and opportunity.
4. Roster Construction For evaluating potential acquisitions, rate stats help project how a player might perform in a different context with different playing time.
7.9.2 Advantages of Counting Statistics
1. Capturing Total Contribution A player who scores 2,000 points in a season has a greater total impact than one who scores 1,000 points at a higher rate. Counting stats capture this.
2. Durability and Availability Counting stats naturally reward players who play many minutes and many games—durability that has real value.
3. Simplicity and Clarity Everyone understands "30 points per game." Per-100-possessions stats require explanation and context.
4. Historical Significance Records, milestones, and career totals are expressed in counting terms. These have cultural and historical meaning.
7.9.3 A Framework for Choosing
| Analysis Goal | Preferred Metric Type |
|---|---|
| Comparing playing efficiency | Rate stats |
| Evaluating total team impact | Counting stats |
| Projecting role change performance | Rate stats |
| MVP/award voting | Both, context-dependent |
| Historical comparisons | Era-adjusted rate stats |
| Career achievements | Counting stats |
| Identifying undervalued players | Rate stats |
| Evaluating starter vs. bench | Rate stats with caveats |
7.9.4 The Best Practice: Report Both
The most informative approach reports both counting and rate statistics:
"Player X averaged 22.4 points per game (26.8 per 36 minutes, 31.2 per 100 possessions) on a team that played at the league's slowest pace."
This gives the full picture: total production, rate efficiency, and context.
7.10 Advanced Rate Concepts
7.10.1 True Shooting Rate
Beyond basic per-possession scoring, True Shooting Attempts (TSA) provide a better denominator for scoring efficiency:
$$\text{TSA} = \text{FGA} + 0.44 \times \text{FTA}$$
$$\text{Points per TSA} = \frac{\text{Points}}{\text{TSA}}$$
This is closely related to True Shooting Percentage (TS%), which we covered in Chapter 4.
7.10.2 Rebound Rate vs. Rebounds Per Game
Rebounds per game is heavily influenced by team pace and teammate rebound competition. Rebound Rate controls for available rebounds:
$$\text{ORB\%} = \frac{\text{ORB}}{\text{Available ORB opportunities while on court}} \times 100$$
$$\text{DRB\%} = \frac{\text{DRB}}{\text{Available DRB opportunities while on court}} \times 100$$
We will explore these metrics in Chapter 8.
7.10.3 Assist Rate
Similarly, assists per game depends on teammate shot-making and team pace. Assist Rate measures assists per teammate field goal:
$$\text{AST\%} = \frac{\text{AST}}{\text{Teammate FGM while on court}} \times 100$$
7.10.4 Turnover Rate
Turnovers per game can be misleading for high-usage players. Turnover Rate measures turnovers per possession used:
$$\text{TOV\%} = \frac{\text{TOV}}{\text{FGA} + 0.44 \times \text{FTA} + \text{TOV}} \times 100$$
7.11 Practical Implementation: A Complete Example
7.11.1 Building a Pace-Adjustment System
import pandas as pd
import numpy as np
class PaceAdjustmentSystem:
"""
Complete system for pace adjustment and rate statistic calculation.
"""
def __init__(self, league_pace=100.0, ft_coefficient=0.44):
"""
Initialize the pace adjustment system.
Parameters:
-----------
league_pace : float
League average pace for normalization
ft_coefficient : float
Coefficient for free throw possession estimation
"""
self.league_pace = league_pace
self.ft_coefficient = ft_coefficient
def estimate_team_possessions(self, team_stats):
"""
Estimate team possessions from box score stats.
Parameters:
-----------
team_stats : dict
Must contain: fga, fta, orb, tov
Returns:
--------
float : Estimated possessions
"""
return (team_stats['fga']
+ self.ft_coefficient * team_stats['fta']
- team_stats['orb']
+ team_stats['tov'])
def calculate_team_pace(self, team_stats, minutes=48):
"""
Calculate team pace.
Parameters:
-----------
team_stats : dict
Must contain: fga, fta, orb, tov, minutes_played
minutes : int
Baseline minutes (48 for NBA)
Returns:
--------
float : Team pace
"""
possessions = self.estimate_team_possessions(team_stats)
return (possessions / team_stats['minutes_played']) * minutes
def per_minute_stats(self, player_stats, minutes, baseline=36):
"""
Calculate per-minute statistics.
Parameters:
-----------
player_stats : dict
Raw counting statistics
minutes : float
Minutes played
baseline : int
Minutes baseline (36, 40, or 48)
Returns:
--------
dict : Per-minute statistics
"""
return {
f"{stat}_per_{baseline}": (value / minutes) * baseline
for stat, value in player_stats.items()
}
def per_100_possessions(self, player_stats, team_possessions_on_court):
"""
Calculate per-100-possessions statistics.
Parameters:
-----------
player_stats : dict
Raw counting statistics
team_possessions_on_court : float
Team possessions while player was on court
Returns:
--------
dict : Per-100-possessions statistics
"""
return {
f"{stat}_per_100": (value / team_possessions_on_court) * 100
for stat, value in player_stats.items()
}
def pace_adjust(self, stat, team_pace):
"""
Pace-adjust a single statistic.
Parameters:
-----------
stat : float
Raw statistic
team_pace : float
Team's pace
Returns:
--------
float : Pace-adjusted statistic
"""
return stat * (self.league_pace / team_pace)
def full_adjustment(self, player_stats, team_pace, minutes_played,
team_possessions_on_court):
"""
Perform full rate conversion and pace adjustment.
Parameters:
-----------
player_stats : dict
Raw counting statistics
team_pace : float
Team's pace
minutes_played : float
Player's minutes played
team_possessions_on_court : float
Team possessions while player was on court
Returns:
--------
dict : Comprehensive rate statistics
"""
result = {
'raw': player_stats.copy(),
'per_36': self.per_minute_stats(player_stats, minutes_played, 36),
'per_48': self.per_minute_stats(player_stats, minutes_played, 48),
'per_100_poss': self.per_100_possessions(
player_stats, team_possessions_on_court
),
'pace_adjusted': {
f"{stat}_pace_adj": self.pace_adjust(value, team_pace)
for stat, value in player_stats.items()
},
'context': {
'minutes_played': minutes_played,
'team_pace': team_pace,
'league_pace': self.league_pace,
'possessions_on_court': team_possessions_on_court
}
}
return result
def example_usage():
"""
Demonstrate the PaceAdjustmentSystem.
"""
# Initialize system with 2022-23 league pace
system = PaceAdjustmentSystem(league_pace=99.5)
# Example player stats (season totals)
player_stats = {
'points': 2085,
'rebounds': 562,
'assists': 512,
'steals': 89,
'blocks': 41,
'turnovers': 258
}
# Context
minutes_played = 2682
team_pace = 102.3
# Estimate possessions on court based on minutes percentage
games = 82
team_total_poss = team_pace * games # Approximate
minutes_pct = minutes_played / (48 * games)
poss_on_court = team_total_poss * minutes_pct
# Calculate full adjustment
result = system.full_adjustment(
player_stats=player_stats,
team_pace=team_pace,
minutes_played=minutes_played,
team_possessions_on_court=poss_on_court
)
# Display results
print("Raw Stats:", result['raw'])
print("\nPer 36 Minutes:", result['per_36'])
print("\nPer 100 Possessions:", result['per_100_poss'])
print("\nPace-Adjusted:", result['pace_adjusted'])
return result
if __name__ == "__main__":
example_usage()
7.12 Common Pitfalls and Best Practices
7.12.1 Pitfalls to Avoid
1. Small Sample Extrapolation Never extrapolate per-minute stats from tiny samples. A player with 2 points in 4 minutes has a "per-36" of 18 points, which is meaningless.
Minimum thresholds: - Per-36 stats: 500+ minutes - Per-100 possession stats: 500+ possessions - Pace calculations: Full season preferred
2. Ignoring Context Changes A bench player's per-36 stats often don't translate to starter minutes because: - They face weaker competition - They play in favorable lineup combinations - Fatigue affects extended minutes
3. Double-Adjusting Don't pace-adjust statistics that are already rate-based (like TS% or ORtg). These are inherently pace-neutral.
4. Mismatched Baselines When comparing players, ensure all stats use the same baseline (all per-36, or all per-100 possessions). Mixing baselines invalidates comparisons.
7.12.2 Best Practices
1. Report Confidence Intervals For smaller samples, report uncertainty:
def rate_stat_with_confidence(successes, opportunities, confidence=0.95):
"""
Calculate rate statistic with confidence interval.
"""
from scipy import stats
rate = successes / opportunities
# Wilson score interval for proportions
z = stats.norm.ppf(1 - (1 - confidence) / 2)
denominator = 1 + z**2 / opportunities
center = (rate + z**2 / (2 * opportunities)) / denominator
spread = z * np.sqrt(rate * (1 - rate) / opportunities +
z**2 / (4 * opportunities**2)) / denominator
return {
'rate': rate,
'lower': center - spread,
'upper': center + spread,
'confidence': confidence
}
2. Use Multiple Rate Metrics Don't rely on a single rate statistic. Triangulate with per-minute, per-possession, and pace-adjusted metrics.
3. Consider Role and Competition Supplement rate stats with qualitative context about player role, lineup combinations, and competition faced.
4. Document Your Methodology When reporting pace-adjusted stats, specify: - Which possession formula was used - What FT coefficient was applied - What league average pace was used for normalization - What minimum thresholds were applied
7.13 Summary
Rate statistics transform our ability to make meaningful basketball comparisons. By normalizing counting stats to common denominators—whether minutes, possessions, or era-adjusted baselines—we can see through the noise of context to evaluate true performance.
Key formulas from this chapter:
Per-Minute Rate: $$\text{Stat per X minutes} = \frac{\text{Raw Stat}}{\text{Minutes Played}} \times X$$
Possessions Estimate: $$\text{Possessions} = \text{FGA} + 0.44 \times \text{FTA} - \text{ORB} + \text{TOV}$$
Pace: $$\text{Pace} = \frac{\text{Possessions}}{\text{Minutes}} \times 48$$
Per-100-Possessions Rate: $$\text{Stat per 100 Poss} = \frac{\text{Raw Stat}}{\text{Possessions}} \times 100$$
Pace Adjustment: $$\text{Adjusted Stat} = \text{Raw Stat} \times \frac{\text{League Pace}}{\text{Team Pace}}$$
Era-Adjusted Rate: $$\text{Era-Adjusted} = \frac{\text{Player Stat}}{\text{Era Average}} \times \text{Target Era Average}$$
The choice between counting and rate statistics depends on the analytical question. For comparing efficiency, use rate stats. For measuring total contribution, use counting stats. For historical comparisons, use era-adjusted rate stats. The most informative analyses report both, with full context about the metrics and methodology used.
In the next chapter, we will explore rebounding statistics and how rate-based approaches (rebound percentage) revolutionized our understanding of this fundamental basketball skill.
References
-
Oliver, D. (2004). Basketball on Paper: Rules and Tools for Performance Analysis. Potomac Books.
-
Kubatko, J., Oliver, D., Pelton, K., & Rosenbaum, D. T. (2007). A starting point for analyzing basketball statistics. Journal of Quantitative Analysis in Sports, 3(3).
-
Hollinger, J. (2005). Pro Basketball Forecast. Potomac Books.
-
Rosenbaum, D. T. (2004). Measuring how NBA players help their teams win. 82games.com.
-
Basketball-Reference.com. Glossary and methodology documentation.
-
Engelmann, J. (2017). Possession-based player performance analysis in basketball. MIT Sloan Sports Analytics Conference.