18 min read

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...

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:

  1. Field Goal Attempts (FGA): Every shot attempt ends a possession, whether made or missed.

  2. 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.

  1. Offensive Rebounds (ORB): An offensive rebound extends a possession rather than starting a new one, so we subtract ORB to avoid double-counting.

  2. 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.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:

  1. Physical defensive play was at its peak before hand-checking rules were relaxed
  2. The three-point shot was still underutilized
  3. Post-up and isolation plays dominated offensive schemes
  4. 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:

  1. Emphasis on transition offense and early shot clock attempts
  2. High three-point attempt rates creating more rebounds and fast break opportunities
  3. Improved conditioning and sports science
  4. Rule changes penalizing defensive disruption
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:

  1. Pace: 130 possessions per game in 1962 vs. 100 in 2023
  2. Three-point shooting: Non-existent before 1979
  3. Defensive rules: Hand-checking, zone defenses, etc.
  4. Competition level: League size, international talent, etc.
  5. 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

  1. Oliver, D. (2004). Basketball on Paper: Rules and Tools for Performance Analysis. Potomac Books.

  2. 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).

  3. Hollinger, J. (2005). Pro Basketball Forecast. Potomac Books.

  4. Rosenbaum, D. T. (2004). Measuring how NBA players help their teams win. 82games.com.

  5. Basketball-Reference.com. Glossary and methodology documentation.

  6. Engelmann, J. (2017). Possession-based player performance analysis in basketball. MIT Sloan Sports Analytics Conference.