Chapter 33 Intermediate ~45 min read

Perimeter Defense Metrics

Measuring the ability to defend on the perimeter using tracking data on matchups and opponent efficiency.

Perimeter Defense Challenges

Perimeter defense presents distinct measurement challenges from rim protection. Guards and wings primarily contest jump shots rather than layups, involve more one-on-one coverage, and must navigate screens that rim protectors rarely face. The outcomes—made or missed jump shots—are more variable than rim attempts, requiring larger samples for reliable evaluation.

Matchup Data Analysis

Tracking data records who guards whom on each possession, enabling direct measurement of perimeter defense. We can calculate opponent points per possession when guarded by specific defenders, opponent shooting percentages by shot type, and the frequency and quality of shots allowed.

def analyze_perimeter_defense(matchup_data, player_id):
    """Analyze perimeter defense from matchup tracking data"""

    # Possessions where player was primary defender
    defensive_poss = matchup_data[matchup_data['DEF_PLAYER_ID'] == player_id]

    # Points allowed
    pts_allowed = defensive_poss['PTS'].sum()
    total_poss = len(defensive_poss)

    if total_poss == 0:
        return None

    # Defensive PPP
    def_ppp = pts_allowed / total_poss

    # Shot outcomes when defending
    shots_defended = defensive_poss[defensive_poss['SHOT_ATTEMPT'] == 1]
    dfg_pct = shots_defended['SHOT_MADE'].mean()

    # Three-point defense
    threes_defended = shots_defended[shots_defended['SHOT_TYPE'] == '3PT']
    three_dfg = threes_defended['SHOT_MADE'].mean() if len(threes_defended) > 0 else 0

    return {
        'poss_defended': total_poss,
        'def_ppp': round(def_ppp, 2),
        'dfg_pct': round(dfg_pct * 100, 1),
        'three_dfg_pct': round(three_dfg * 100, 1)
    }

Matchup Difficulty Adjustment

Raw defensive numbers must account for matchup difficulty. A player assigned to guard elite scorers faces harder tasks than one hiding on weak offensive players. Comparing defensive statistics without adjustment unfairly penalizes players who draw tough assignments.

Difficulty adjustment compares opponent efficiency when guarded to their typical efficiency against other defenders. A defender holding opponents to 95% of their normal production is performing better than one allowing 105%, regardless of raw percentages. This relative approach provides fairer cross-player comparison.

Shot Quality Allowed

Beyond conversion rates, perimeter defense affects shot quality. Elite perimeter defenders force difficult, contested shots; weaker defenders allow open looks. Tracking shot quality metrics—expected points based on defender distance, shot type, and context—reveals whether defenders are actually causing misses or benefiting from poor opponent shooting.

A defender might show good results due to opponent bad luck on quality shots; another might show poor results despite consistently contesting. Shot quality allowed provides a more process-based evaluation that predicts future performance better than outcome-based metrics alone.

The Noise Problem

Perimeter defense statistics stabilize slowly because jump shooting is inherently variable. Even over a full season, opponent shooting percentages when guarded by a specific defender contain substantial noise. Analysts should weight perimeter defense metrics carefully, applying appropriate regression toward mean performance and acknowledging uncertainty.

Implementation in R

# Transition defense analysis
library(tidyverse)

analyze_transition_defense <- function(transition_data) {
  transition_data %>%
    group_by(defender_id, defender_name) %>%
    summarise(
      transition_poss = n(),

      # Getting back
      avg_sprint_back_speed = mean(sprint_back_speed),
      got_back_in_time_pct = mean(beat_ball_to_paint) * 100,

      # Defensive outcomes
      pts_allowed = sum(points),
      transition_ppp_allowed = round(pts_allowed / transition_poss, 2),
      turnover_created_rate = mean(turnover_created) * 100,

      # Comparison to half-court
      halfcourt_ppp_allowed = mean(halfcourt_ppp_allowed),
      transition_vs_halfcourt = transition_ppp_allowed - halfcourt_ppp_allowed,

      .groups = "drop"
    )
}

transition <- read_csv("transition_defense.csv")
transition_analysis <- analyze_transition_defense(transition)

# Best transition defenders
best_transition <- transition_analysis %>%
  filter(transition_poss >= 100) %>%
  arrange(transition_ppp_allowed) %>%
  select(defender_name, transition_poss, transition_ppp_allowed,
         got_back_in_time_pct, avg_sprint_back_speed) %>%
  head(15)

print(best_transition)
# Fast break prevention analysis
library(tidyverse)

analyze_fastbreak_prevention <- function(possession_data) {
  possession_data %>%
    group_by(team_id, team_name) %>%
    summarise(
      total_opponent_poss = n(),

      # Opponent transition rate
      opp_transition_rate = mean(opponent_in_transition) * 100,

      # Transition defense quality
      opp_transition_ppp = mean(points[opponent_in_transition == TRUE]),
      opp_halfcourt_ppp = mean(points[opponent_in_transition == FALSE]),

      # Contribution to transition
      turnover_induced_transition = mean(transition_off_turnover) * 100,
      miss_induced_transition = mean(transition_off_miss) * 100,

      .groups = "drop"
    )
}

team_possessions <- read_csv("team_defensive_possessions.csv")
fastbreak_analysis <- analyze_fastbreak_prevention(team_possessions)

# Best transition prevention teams
best_prevention <- fastbreak_analysis %>%
  arrange(opp_transition_rate) %>%
  select(team_name, opp_transition_rate, opp_transition_ppp,
         opp_halfcourt_ppp)

print(best_prevention)

Implementation in R

# Transition defense analysis
library(tidyverse)

analyze_transition_defense <- function(transition_data) {
  transition_data %>%
    group_by(defender_id, defender_name) %>%
    summarise(
      transition_poss = n(),

      # Getting back
      avg_sprint_back_speed = mean(sprint_back_speed),
      got_back_in_time_pct = mean(beat_ball_to_paint) * 100,

      # Defensive outcomes
      pts_allowed = sum(points),
      transition_ppp_allowed = round(pts_allowed / transition_poss, 2),
      turnover_created_rate = mean(turnover_created) * 100,

      # Comparison to half-court
      halfcourt_ppp_allowed = mean(halfcourt_ppp_allowed),
      transition_vs_halfcourt = transition_ppp_allowed - halfcourt_ppp_allowed,

      .groups = "drop"
    )
}

transition <- read_csv("transition_defense.csv")
transition_analysis <- analyze_transition_defense(transition)

# Best transition defenders
best_transition <- transition_analysis %>%
  filter(transition_poss >= 100) %>%
  arrange(transition_ppp_allowed) %>%
  select(defender_name, transition_poss, transition_ppp_allowed,
         got_back_in_time_pct, avg_sprint_back_speed) %>%
  head(15)

print(best_transition)
# Fast break prevention analysis
library(tidyverse)

analyze_fastbreak_prevention <- function(possession_data) {
  possession_data %>%
    group_by(team_id, team_name) %>%
    summarise(
      total_opponent_poss = n(),

      # Opponent transition rate
      opp_transition_rate = mean(opponent_in_transition) * 100,

      # Transition defense quality
      opp_transition_ppp = mean(points[opponent_in_transition == TRUE]),
      opp_halfcourt_ppp = mean(points[opponent_in_transition == FALSE]),

      # Contribution to transition
      turnover_induced_transition = mean(transition_off_turnover) * 100,
      miss_induced_transition = mean(transition_off_miss) * 100,

      .groups = "drop"
    )
}

team_possessions <- read_csv("team_defensive_possessions.csv")
fastbreak_analysis <- analyze_fastbreak_prevention(team_possessions)

# Best transition prevention teams
best_prevention <- fastbreak_analysis %>%
  arrange(opp_transition_rate) %>%
  select(team_name, opp_transition_rate, opp_transition_ppp,
         opp_halfcourt_ppp)

print(best_prevention)
Chapter Summary

You've completed Chapter 33: Perimeter Defense Metrics.