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)