Chapter 32 Intermediate ~50 min read

Rim Protection Analytics

Measuring the ability to protect the rim through tracking data analysis of contests, blocks, and deterrence.

The Value of Rim Protection

Rim protection represents the most impactful individual defensive skill in basketball. Shots at the rim are the highest-efficiency attempts; a player who can consistently reduce opponent conversion at the basket provides enormous defensive value. Tracking data has transformed our ability to measure rim protection, moving beyond block totals to comprehensive assessment of rim defense.

Beyond Block Totals

Blocks capture only part of rim protection value. A shot blocked is a make prevented, but so is a shot altered into a miss without contact. And the most valuable rim protection comes through deterrence—preventing rim attempts altogether by forcing opponents to reconsider drives toward a protected basket.

Tracking data enables measurement of these components. We can count shot contests at the rim (challenges within a specified distance regardless of outcome), measure conversion rates on those contests, and compare overall rim attempt rates when specific defenders are protecting the basket.

Measuring Rim Protection Effectiveness

def analyze_rim_protection(tracking_data, player_id):
    """Analyze rim protection effectiveness from tracking data"""

    # Shots at rim when player is rim protector
    rim_shots_protected = tracking_data[
        (tracking_data['RIM_PROTECTOR_ID'] == player_id) &
        (tracking_data['SHOT_ZONE'] == 'Restricted Area')
    ]

    # Basic rim protection stats
    rim_contests = len(rim_shots_protected)
    rim_makes = rim_shots_protected['SHOT_MADE_FLAG'].sum()
    rim_dfg_pct = rim_makes / rim_contests if rim_contests > 0 else 0

    # Compare to league average at rim (~63%)
    league_avg_rim = 0.63
    rim_diff = rim_dfg_pct - league_avg_rim

    # Deterrence: rim attempts per minute when protecting
    minutes_protecting = tracking_data[
        tracking_data['RIM_PROTECTOR_ID'] == player_id
    ]['MIN'].sum()
    rim_attempts_per_min = rim_contests / minutes_protecting if minutes_protecting > 0 else 0

    return {
        'contests': rim_contests,
        'dfg_pct': round(rim_dfg_pct * 100, 1),
        'vs_league_avg': round(rim_diff * 100, 1),
        'rim_attempts_per_min': round(rim_attempts_per_min, 2)
    }

Contest Quality Versus Quantity

Not all rim contests are equal. A contest from behind after the shooter has already gathered provides less value than a contest that forces a difficult adjustment. Some rim protectors excel at quantity (contesting many shots), others at quality (significantly reducing conversion on contested shots), and the elite provide both.

Deterrence Effects

The most valuable rim protectors reduce opponent rim attempts, not just conversion rates on attempts that happen. Teams with elite rim protectors often see opponents settle for more mid-range shots and floaters, attempting fewer dunks and layups. This deterrence effect extends rim protection value beyond the shots actually contested.

Measuring deterrence requires comparing team rim attempt rates allowed when the rim protector is on versus off court. The difference estimates how many rim attempts the player's presence prevents, which can be converted to points saved by valuing each prevented attempt at its expected value.

Elite Rim Protectors

Historical data identifies the characteristics of elite rim protectors: height and wingspan provide physical tools, but positioning, timing, and anticipation often matter more. Players like Rudy Gobert combine physical advantages with elite instincts to hold opponents well below 50% at the rim while also deterring attempts that would otherwise occur.

Implementation in R

# Pick and roll defensive analysis
library(tidyverse)

analyze_pnr_defense <- function(pnr_data) {
  pnr_data %>%
    group_by(ball_defender_id, ball_defender_name) %>%
    summarise(
      pnr_possessions = n(),

      # Coverage types
      switch_rate = mean(coverage_type == "switch") * 100,
      drop_rate = mean(coverage_type == "drop") * 100,
      hedge_rate = mean(coverage_type == "hedge") * 100,
      blitz_rate = mean(coverage_type == "blitz") * 100,

      # Outcomes
      pts_allowed = sum(points),
      ppp_allowed = round(pts_allowed / pnr_possessions, 2),
      turnover_rate = mean(turnover) * 100,

      .groups = "drop"
    )
}

pnr_defense <- read_csv("pnr_defense_tracking.csv")
pnr_analysis <- analyze_pnr_defense(pnr_defense)

# Best PnR ball defenders
best_pnr_defender <- pnr_analysis %>%
  filter(pnr_possessions >= 100) %>%
  arrange(ppp_allowed) %>%
  select(ball_defender_name, pnr_possessions, ppp_allowed,
         switch_rate, turnover_rate) %>%
  head(15)

print(best_pnr_defender)
# Screen defense analysis
library(tidyverse)

analyze_screen_defense <- function(screen_data) {
  screen_data %>%
    group_by(screen_defender_id, screen_defender_name) %>%
    summarise(
      screens_defended = n(),

      # Navigation success
      over_screen_pct = mean(navigated == "over") * 100,
      under_screen_pct = mean(navigated == "under") * 100,
      caught_pct = mean(navigated == "caught") * 100,

      # Recovery time
      avg_recovery_time = mean(recovery_time, na.rm = TRUE),

      # Outcome after screen
      pts_after_screen = sum(points_scored),
      ppp_after_screen = round(pts_after_screen / screens_defended, 2),

      .groups = "drop"
    )
}

screen_defense <- read_csv("screen_defense_tracking.csv")
screen_analysis <- analyze_screen_defense(screen_defense)

# Best screen navigators
best_navigators <- screen_analysis %>%
  filter(screens_defended >= 100) %>%
  arrange(desc(over_screen_pct)) %>%
  select(screen_defender_name, screens_defended, over_screen_pct,
         caught_pct, ppp_after_screen) %>%
  head(15)

print(best_navigators)

Implementation in R

# Pick and roll defensive analysis
library(tidyverse)

analyze_pnr_defense <- function(pnr_data) {
  pnr_data %>%
    group_by(ball_defender_id, ball_defender_name) %>%
    summarise(
      pnr_possessions = n(),

      # Coverage types
      switch_rate = mean(coverage_type == "switch") * 100,
      drop_rate = mean(coverage_type == "drop") * 100,
      hedge_rate = mean(coverage_type == "hedge") * 100,
      blitz_rate = mean(coverage_type == "blitz") * 100,

      # Outcomes
      pts_allowed = sum(points),
      ppp_allowed = round(pts_allowed / pnr_possessions, 2),
      turnover_rate = mean(turnover) * 100,

      .groups = "drop"
    )
}

pnr_defense <- read_csv("pnr_defense_tracking.csv")
pnr_analysis <- analyze_pnr_defense(pnr_defense)

# Best PnR ball defenders
best_pnr_defender <- pnr_analysis %>%
  filter(pnr_possessions >= 100) %>%
  arrange(ppp_allowed) %>%
  select(ball_defender_name, pnr_possessions, ppp_allowed,
         switch_rate, turnover_rate) %>%
  head(15)

print(best_pnr_defender)
# Screen defense analysis
library(tidyverse)

analyze_screen_defense <- function(screen_data) {
  screen_data %>%
    group_by(screen_defender_id, screen_defender_name) %>%
    summarise(
      screens_defended = n(),

      # Navigation success
      over_screen_pct = mean(navigated == "over") * 100,
      under_screen_pct = mean(navigated == "under") * 100,
      caught_pct = mean(navigated == "caught") * 100,

      # Recovery time
      avg_recovery_time = mean(recovery_time, na.rm = TRUE),

      # Outcome after screen
      pts_after_screen = sum(points_scored),
      ppp_after_screen = round(pts_after_screen / screens_defended, 2),

      .groups = "drop"
    )
}

screen_defense <- read_csv("screen_defense_tracking.csv")
screen_analysis <- analyze_screen_defense(screen_defense)

# Best screen navigators
best_navigators <- screen_analysis %>%
  filter(screens_defended >= 100) %>%
  arrange(desc(over_screen_pct)) %>%
  select(screen_defender_name, screens_defended, over_screen_pct,
         caught_pct, ppp_after_screen) %>%
  head(15)

print(best_navigators)
Chapter Summary

You've completed Chapter 32: Rim Protection Analytics.