Contested vs Open Shots

Beginner 10 min read 0 views Nov 27, 2025

Contested vs Open Shots: Shot Defense and Contest Metrics

Shot contest analysis represents one of the most significant advances in basketball analytics, providing objective measures of defensive effectiveness. Understanding how defender proximity affects shooting efficiency is crucial for evaluating defensive performance, optimizing shot selection, and developing defensive strategies.

This guide explores NBA tracking technology's measurement of shot contests, analyzes the impact of defender distance on shooting percentages, and provides practical tools for analyzing contest data.

1. Defender Distance Categories

NBA Official Classifications

The NBA's player tracking system categorizes shots based on the distance of the nearest defender at the moment of release:

Tight Contest (0-2 feet)

  • Definition: Defender within 2 feet of shooter at release
  • Characteristics: Hand in face, potential to block, maximum defensive pressure
  • Frequency: Approximately 30-35% of all NBA shots
  • League Average FG%: ~38-40% (varies by shot type)

Open Shot (2-4 feet)

  • Definition: Defender 2-4 feet away at release
  • Characteristics: Moderate pressure, defender can close out but limited contest
  • Frequency: Approximately 35-40% of all NBA shots
  • League Average FG%: ~42-44%

Wide Open Shot (4-6 feet)

  • Definition: Defender 4-6 feet away at release
  • Characteristics: Minimal pressure, shooter has time to set and shoot
  • Frequency: Approximately 20-25% of all NBA shots
  • League Average FG%: ~45-47%

Very Wide Open (6+ feet)

  • Definition: Nearest defender more than 6 feet away
  • Characteristics: Essentially uncontested, practice shot conditions
  • Frequency: Approximately 10-15% of all NBA shots
  • League Average FG%: ~48-50%

Key Insight: Contest Effect Size

The difference between a very tight contest (0-2 feet) and a wide open shot (6+ feet) typically results in a 10-12 percentage point increase in field goal percentage. For three-point shots, this gap can be even larger (12-15 percentage points), highlighting the critical importance of perimeter defense.

2. How NBA Tracking Measures Shot Contest

Second Spectrum Tracking Technology

The NBA uses Second Spectrum's optical tracking system installed in all arenas since the 2013-14 season:

Hardware Components

  • Cameras: Six cameras mounted in arena catwalks
  • Frame Rate: 25 frames per second
  • Coverage: Complete court coverage with redundancy
  • Resolution: Tracks player and ball positions with ~6-inch accuracy

Contest Measurement Process

  1. Shot Detection: System identifies shot attempt via ball trajectory analysis
  2. Release Point Identification: Determines exact moment ball leaves shooter's hands
  3. Defender Proximity Calculation: Measures distance from shooter to nearest defender at release
  4. Contest Classification: Assigns category based on defender distance
  5. Additional Metrics: Tracks defender height, hand position, and closing speed

Advanced Contest Metrics

  • Contests Per Game: Number of shots contested by a defender
  • Contest Rate: Percentage of opponent shots contested within 4 feet
  • Contest Quality: Average distance to shooter on contested attempts
  • Contest Differential: Opponent FG% on contested vs uncontested shots
  • Closeout Efficiency: Success rate when closing out to shooters

Important Considerations

  • Measurement Timing: Distance measured at release, not at jump or landing
  • Nearest Defender Only: Only the closest defender is considered
  • 2D Distance: Horizontal distance only (vertical difference not factored)
  • Help Defense: Late rotations may not register if they arrive after release

3. Python Analysis with nba_api

Retrieving Shot Contest Data

The NBA API provides detailed shot defense statistics through various endpoints:

Python Implementation


from nba_api.stats.endpoints import (
    leaguedashptdefend,
    playerdashptshots,
    teamdashptshots
)
from nba_api.stats.static import players, teams
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from datetime import datetime

# Set display options
pd.set_option('display.max_columns', None)
pd.set_option('display.width', None)

class ShotContestAnalyzer:
    """Analyze shot contest data using NBA tracking information."""

    def __init__(self, season='2023-24'):
        self.season = season

    def get_player_defense_by_distance(self, player_id):
        """
        Get detailed defensive stats by defender distance for a player.

        Parameters:
        -----------
        player_id : int
            NBA player ID

        Returns:
        --------
        pd.DataFrame : Defensive statistics by distance range
        """
        defense_dashboard = leaguedashptdefend.LeagueDashPtDefend(
            league_id='00',
            per_mode_simple='PerGame',
            season=self.season,
            season_type_all_star='Regular Season',
            defense_category='Overall'
        )

        # Get data for all distance ranges
        distance_ranges = ['0-2 Feet - Very Tight', '2-4 Feet - Tight',
                          '4-6 Feet - Open', '6+ Feet - Wide Open']

        all_data = []
        for dist_range in distance_ranges:
            defense_dashboard = leaguedashptdefend.LeagueDashPtDefend(
                league_id='00',
                per_mode_simple='PerGame',
                season=self.season,
                season_type_all_star='Regular Season',
                defense_category=dist_range
            )
            df = defense_dashboard.get_data_frames()[0]
            df['DISTANCE_RANGE'] = dist_range
            all_data.append(df)

        combined_df = pd.concat(all_data, ignore_index=True)
        player_data = combined_df[combined_df['CLOSE_DEF_PERSON_ID'] == player_id]

        return player_data

    def get_team_shot_defense(self, team_id):
        """
        Get team-level shot defense statistics.

        Parameters:
        -----------
        team_id : int
            NBA team ID

        Returns:
        --------
        pd.DataFrame : Team defensive statistics by shot type and distance
        """
        team_defense = teamdashptshots.TeamDashPtShots(
            team_id=team_id,
            league_id='00',
            per_mode_simple='PerGame',
            season=self.season,
            season_type_all_star='Regular Season'
        )

        # Get general shooting defense
        general_df = team_defense.get_data_frames()[0]

        # Get shot clock defense
        shot_clock_df = team_defense.get_data_frames()[1]

        # Get dribble defense
        dribble_df = team_defense.get_data_frames()[2]

        # Get closest defender distance defense
        defender_distance_df = team_defense.get_data_frames()[3]

        return {
            'general': general_df,
            'shot_clock': shot_clock_df,
            'dribbles': dribble_df,
            'defender_distance': defender_distance_df
        }

    def get_player_shooting_by_contest(self, player_id):
        """
        Get player shooting stats broken down by contest level.

        Parameters:
        -----------
        player_id : int
            NBA player ID

        Returns:
        --------
        pd.DataFrame : Shooting stats by defender distance
        """
        player_shots = playerdashptshots.PlayerDashPtShots(
            player_id=player_id,
            league_id='00',
            per_mode_simple='PerGame',
            season=self.season,
            season_type_all_star='Regular Season'
        )

        # Get closest defender distance shooting
        defender_distance_df = player_shots.get_data_frames()[3]

        return defender_distance_df

    def calculate_contest_impact(self, player_id):
        """
        Calculate the impact of shot contests on a player's shooting.

        Parameters:
        -----------
        player_id : int
            NBA player ID

        Returns:
        --------
        dict : Contest impact metrics
        """
        shooting_data = self.get_player_shooting_by_contest(player_id)

        if shooting_data.empty:
            return None

        # Extract key metrics
        tight_data = shooting_data[shooting_data['SHOT_DIST_RANGE'].str.contains('2-4', na=False)]
        open_data = shooting_data[shooting_data['SHOT_DIST_RANGE'].str.contains('4-6', na=False)]
        wide_open_data = shooting_data[shooting_data['SHOT_DIST_RANGE'].str.contains('6\\+', na=False)]

        metrics = {
            'tight_fg_pct': tight_data['FG_PCT'].values[0] if not tight_data.empty else None,
            'open_fg_pct': open_data['FG_PCT'].values[0] if not open_data.empty else None,
            'wide_open_fg_pct': wide_open_data['FG_PCT'].values[0] if not wide_open_data.empty else None,
            'tight_fga': tight_data['FGA'].values[0] if not tight_data.empty else None,
            'open_fga': open_data['FGA'].values[0] if not open_data.empty else None,
            'wide_open_fga': wide_open_data['FGA'].values[0] if not wide_open_data.empty else None
        }

        # Calculate contest vulnerability
        if metrics['tight_fg_pct'] and metrics['wide_open_fg_pct']:
            metrics['contest_impact'] = metrics['wide_open_fg_pct'] - metrics['tight_fg_pct']
        else:
            metrics['contest_impact'] = None

        return metrics

    def compare_defenders(self, player_ids, defense_category='Overall'):
        """
        Compare multiple defenders' contest effectiveness.

        Parameters:
        -----------
        player_ids : list
            List of NBA player IDs to compare
        defense_category : str
            Type of defense to analyze

        Returns:
        --------
        pd.DataFrame : Comparison of defenders
        """
        defense_dashboard = leaguedashptdefend.LeagueDashPtDefend(
            league_id='00',
            per_mode_simple='PerGame',
            season=self.season,
            season_type_all_star='Regular Season',
            defense_category=defense_category
        )

        all_players_df = defense_dashboard.get_data_frames()[0]
        comparison_df = all_players_df[all_players_df['CLOSE_DEF_PERSON_ID'].isin(player_ids)]

        # Add calculated metrics
        comparison_df['CONTEST_EFFECTIVENESS'] = (
            comparison_df['NORMAL_FG_PCT'] - comparison_df['D_FG_PCT']
        )

        return comparison_df[['CLOSE_DEF_PERSON', 'D_FGA', 'D_FGM', 'D_FG_PCT',
                             'NORMAL_FG_PCT', 'CONTEST_EFFECTIVENESS']]

    def visualize_contest_impact(self, player_id, player_name):
        """
        Create visualization of contest impact on shooting.

        Parameters:
        -----------
        player_id : int
            NBA player ID
        player_name : str
            Player name for chart title
        """
        shooting_data = self.get_player_shooting_by_contest(player_id)

        if shooting_data.empty:
            print(f"No data available for {player_name}")
            return

        # Prepare data for visualization
        shooting_data = shooting_data.sort_values('SHOT_DIST_RANGE')

        fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 6))

        # Plot 1: FG% by contest level
        ax1.bar(range(len(shooting_data)), shooting_data['FG_PCT'],
               color=['#d62728', '#ff7f0e', '#2ca02c', '#1f77b4'])
        ax1.set_xticks(range(len(shooting_data)))
        ax1.set_xticklabels(shooting_data['SHOT_DIST_RANGE'], rotation=45, ha='right')
        ax1.set_ylabel('Field Goal %')
        ax1.set_title(f'{player_name} - FG% by Defender Distance')
        ax1.set_ylim(0, max(shooting_data['FG_PCT']) * 1.2)

        # Add percentage labels on bars
        for i, (idx, row) in enumerate(shooting_data.iterrows()):
            ax1.text(i, row['FG_PCT'] + 0.01, f"{row['FG_PCT']:.1%}",
                    ha='center', va='bottom', fontweight='bold')

        # Plot 2: Shot frequency by contest level
        ax2.bar(range(len(shooting_data)), shooting_data['FGA'],
               color=['#d62728', '#ff7f0e', '#2ca02c', '#1f77b4'])
        ax2.set_xticks(range(len(shooting_data)))
        ax2.set_xticklabels(shooting_data['SHOT_DIST_RANGE'], rotation=45, ha='right')
        ax2.set_ylabel('Field Goal Attempts')
        ax2.set_title(f'{player_name} - Shot Frequency by Defender Distance')

        # Add count labels on bars
        for i, (idx, row) in enumerate(shooting_data.iterrows()):
            ax2.text(i, row['FGA'] + 0.05, f"{row['FGA']:.1f}",
                    ha='center', va='bottom', fontweight='bold')

        plt.tight_layout()
        plt.savefig(f'{player_name.replace(" ", "_")}_contest_analysis.png', dpi=300, bbox_inches='tight')
        plt.show()

        print(f"Visualization saved as {player_name.replace(' ', '_')}_contest_analysis.png")


# Example usage and analysis
if __name__ == "__main__":
    # Initialize analyzer
    analyzer = ShotContestAnalyzer(season='2023-24')

    # Get player ID (example: Stephen Curry)
    curry_id = 201939

    print("="*80)
    print("SHOT CONTEST ANALYSIS")
    print("="*80)

    # Analyze shooting by contest level
    print("\n1. Player Shooting Performance by Contest Level")
    print("-" * 80)
    shooting_stats = analyzer.get_player_shooting_by_contest(curry_id)
    print(shooting_stats[['SHOT_DIST_RANGE', 'FGA', 'FGM', 'FG_PCT', 'EFG_PCT']])

    # Calculate contest impact
    print("\n2. Contest Impact Metrics")
    print("-" * 80)
    impact = analyzer.calculate_contest_impact(curry_id)
    if impact:
        print(f"Tight Contest FG%: {impact['tight_fg_pct']:.1%}")
        print(f"Wide Open FG%: {impact['wide_open_fg_pct']:.1%}")
        print(f"Contest Impact: {impact['contest_impact']:.1%} percentage points")
        print(f"Shot Distribution: {impact['tight_fga']:.1f} tight, {impact['open_fga']:.1f} open, "
              f"{impact['wide_open_fga']:.1f} wide open")

    # Compare defenders (example: top defenders)
    print("\n3. Defender Comparison")
    print("-" * 80)
    defender_ids = [203507, 1629029, 203954]  # Giannis, Adebayo, Gobert
    comparison = analyzer.compare_defenders(defender_ids, '3 Pointers')
    print(comparison.to_string())

    # Create visualization
    print("\n4. Generating Visualization...")
    print("-" * 80)
    analyzer.visualize_contest_impact(curry_id, "Stephen Curry")

    print("\nAnalysis complete!")

Key Features of the Python Implementation

  • Multi-Distance Analysis: Retrieves data across all contest distance ranges
  • Contest Impact Calculation: Quantifies the effect of defense on shooting
  • Defender Comparison: Evaluates multiple defenders' contest effectiveness
  • Visualization: Creates clear charts showing contest effects
  • Team Analysis: Examines team-level defensive patterns

4. R Analysis with hoopR

Advanced Statistical Modeling of Contest Effects

R provides powerful tools for statistical analysis and modeling of shot contest data:

R Implementation


# Load required libraries
library(hoopR)
library(tidyverse)
library(ggplot2)
library(scales)
library(gganimate)
library(lme4)
library(broom)

# Shot Contest Analysis with hoopR
# Comprehensive analysis of defender distance impact on shooting

# 1. Load and prepare shot data
load_shot_data <- function(seasons = c(2024)) {
  cat("Loading NBA shot data...\n")

  # Load play-by-play data
  pbp_data <- load_nba_pbp(seasons = seasons)

  # Filter for shot attempts
  shots <- pbp_data %>%
    filter(!is.na(shooting_play)) %>%
    select(
      game_id, game_date, season,
      shooting_team = team_id,
      shooter = athlete_id_1,
      shooter_name = athlete_display_name_1,
      shot_result = type_text,
      shot_value = score_value,
      coordinate_x, coordinate_y,
      home_score, away_score,
      quarter = period_number,
      time_remaining = clock_display_value
    )

  return(shots)
}

# 2. Simulate defender distance data (in real analysis, this comes from tracking data)
simulate_defender_distance <- function(shots_df) {
  set.seed(42)

  shots_df %>%
    mutate(
      # Simulate defender distance based on shot result
      # Made shots tend to have larger defender distances
      defender_distance = case_when(
        str_detect(shot_result, "Made") ~ rnorm(n(), mean = 4.5, sd = 2.0),
        TRUE ~ rnorm(n(), mean = 3.0, sd = 1.8)
      ),
      defender_distance = pmax(0, defender_distance),  # No negative distances

      # Classify contest level
      contest_category = case_when(
        defender_distance < 2 ~ "Very Tight (0-2 ft)",
        defender_distance < 4 ~ "Tight (2-4 ft)",
        defender_distance < 6 ~ "Open (4-6 ft)",
        TRUE ~ "Wide Open (6+ ft)"
      ),
      contest_category = factor(
        contest_category,
        levels = c("Very Tight (0-2 ft)", "Tight (2-4 ft)",
                   "Open (4-6 ft)", "Wide Open (6+ ft)")
      ),

      # Shot type
      shot_type = case_when(
        shot_value == 3 ~ "Three-Pointer",
        TRUE ~ "Two-Pointer"
      ),

      # Shot made indicator
      shot_made = as.integer(str_detect(shot_result, "Made"))
    )
}

# 3. Calculate contest impact metrics
calculate_contest_metrics <- function(shots_df) {

  # Overall metrics by contest level
  overall_metrics <- shots_df %>%
    group_by(contest_category) %>%
    summarise(
      attempts = n(),
      makes = sum(shot_made),
      fg_pct = mean(shot_made),
      points_per_shot = mean(shot_value * shot_made),
      .groups = 'drop'
    )

  # By shot type
  shot_type_metrics <- shots_df %>%
    group_by(contest_category, shot_type) %>%
    summarise(
      attempts = n(),
      makes = sum(shot_made),
      fg_pct = mean(shot_made),
      .groups = 'drop'
    )

  # By player
  player_metrics <- shots_df %>%
    group_by(shooter_name, contest_category) %>%
    summarise(
      attempts = n(),
      makes = sum(shot_made),
      fg_pct = mean(shot_made),
      .groups = 'drop'
    ) %>%
    filter(attempts >= 20)  # Minimum attempts threshold

  return(list(
    overall = overall_metrics,
    by_shot_type = shot_type_metrics,
    by_player = player_metrics
  ))
}

# 4. Statistical modeling of contest effect
model_contest_effect <- function(shots_df) {

  # Prepare data for modeling
  model_data <- shots_df %>%
    filter(!is.na(defender_distance), !is.na(shooter_name)) %>%
    mutate(
      defender_distance_sq = defender_distance^2,
      is_three = as.integer(shot_type == "Three-Pointer")
    )

  # Logistic regression model
  # Predicting shot success based on defender distance
  base_model <- glm(
    shot_made ~ defender_distance + is_three,
    data = model_data,
    family = binomial(link = "logit")
  )

  # Model with quadratic term
  quad_model <- glm(
    shot_made ~ defender_distance + defender_distance_sq + is_three +
                defender_distance:is_three,
    data = model_data,
    family = binomial(link = "logit")
  )

  # Mixed effects model accounting for player skill
  player_counts <- model_data %>%
    count(shooter_name) %>%
    filter(n >= 50)

  mixed_model_data <- model_data %>%
    filter(shooter_name %in% player_counts$shooter_name)

  mixed_model <- glmer(
    shot_made ~ defender_distance + is_three + (1 | shooter_name),
    data = mixed_model_data,
    family = binomial(link = "logit"),
    control = glmerControl(optimizer = "bobyqa")
  )

  # Model summaries
  cat("\n=== BASE MODEL (Defender Distance + Shot Type) ===\n")
  print(summary(base_model))

  cat("\n=== QUADRATIC MODEL (Non-linear Effects) ===\n")
  print(summary(quad_model))

  cat("\n=== MIXED EFFECTS MODEL (Accounting for Player Variation) ===\n")
  print(summary(mixed_model))

  # Calculate marginal effects
  dist_range <- seq(0, 8, by = 0.1)

  predictions <- data.frame(
    defender_distance = dist_range,
    defender_distance_sq = dist_range^2,
    is_three = 0
  )

  predictions$prob_two <- predict(quad_model, newdata = predictions, type = "response")

  predictions$is_three <- 1
  predictions$prob_three <- predict(quad_model, newdata = predictions, type = "response")

  return(list(
    base_model = base_model,
    quad_model = quad_model,
    mixed_model = mixed_model,
    predictions = predictions
  ))
}

# 5. Visualization: Contest impact on shooting
visualize_contest_impact <- function(metrics, model_results) {

  # Plot 1: FG% by contest category
  p1 <- ggplot(metrics$overall, aes(x = contest_category, y = fg_pct)) +
    geom_col(aes(fill = contest_category), show.legend = FALSE) +
    geom_text(aes(label = percent(fg_pct, accuracy = 0.1)),
              vjust = -0.5, fontface = "bold") +
    scale_y_continuous(labels = percent_format(), limits = c(0, 0.7)) +
    scale_fill_manual(values = c("#d62728", "#ff7f0e", "#2ca02c", "#1f77b4")) +
    labs(
      title = "Field Goal Percentage by Contest Level",
      subtitle = "NBA Shooting Efficiency vs Defender Distance",
      x = "Contest Category",
      y = "Field Goal Percentage"
    ) +
    theme_minimal() +
    theme(
      plot.title = element_text(face = "bold", size = 14),
      axis.text.x = element_text(angle = 45, hjust = 1)
    )

  # Plot 2: By shot type
  p2 <- ggplot(metrics$by_shot_type, aes(x = contest_category, y = fg_pct, fill = shot_type)) +
    geom_col(position = "dodge") +
    geom_text(aes(label = percent(fg_pct, accuracy = 0.1)),
              position = position_dodge(width = 0.9), vjust = -0.5, size = 3) +
    scale_y_continuous(labels = percent_format(), limits = c(0, 0.7)) +
    scale_fill_manual(values = c("#e74c3c", "#3498db")) +
    labs(
      title = "Field Goal Percentage by Contest Level and Shot Type",
      subtitle = "Comparing Two-Pointers vs Three-Pointers",
      x = "Contest Category",
      y = "Field Goal Percentage",
      fill = "Shot Type"
    ) +
    theme_minimal() +
    theme(
      plot.title = element_text(face = "bold", size = 14),
      axis.text.x = element_text(angle = 45, hjust = 1),
      legend.position = "top"
    )

  # Plot 3: Continuous relationship (model predictions)
  p3 <- ggplot(model_results$predictions) +
    geom_line(aes(x = defender_distance, y = prob_two, color = "Two-Pointer"),
              size = 1.2) +
    geom_line(aes(x = defender_distance, y = prob_three, color = "Three-Pointer"),
              size = 1.2) +
    geom_vline(xintercept = c(2, 4, 6), linetype = "dashed", alpha = 0.5) +
    annotate("text", x = 1, y = 0.65, label = "Very Tight", size = 3, angle = 90) +
    annotate("text", x = 3, y = 0.65, label = "Tight", size = 3, angle = 90) +
    annotate("text", x = 5, y = 0.65, label = "Open", size = 3, angle = 90) +
    annotate("text", x = 7, y = 0.65, label = "Wide Open", size = 3, angle = 90) +
    scale_y_continuous(labels = percent_format(), limits = c(0, 0.7)) +
    scale_color_manual(values = c("Two-Pointer" = "#e74c3c", "Three-Pointer" = "#3498db")) +
    labs(
      title = "Predicted Shot Success vs Defender Distance",
      subtitle = "Model-based estimates showing continuous relationship",
      x = "Defender Distance (feet)",
      y = "Probability of Made Shot",
      color = "Shot Type"
    ) +
    theme_minimal() +
    theme(
      plot.title = element_text(face = "bold", size = 14),
      legend.position = "top"
    )

  # Plot 4: Shot volume distribution
  p4 <- ggplot(metrics$overall, aes(x = contest_category, y = attempts)) +
    geom_col(aes(fill = contest_category), show.legend = FALSE) +
    geom_text(aes(label = scales::comma(attempts)),
              vjust = -0.5, fontface = "bold") +
    scale_y_continuous(labels = comma_format()) +
    scale_fill_manual(values = c("#d62728", "#ff7f0e", "#2ca02c", "#1f77b4")) +
    labs(
      title = "Shot Frequency by Contest Level",
      subtitle = "Distribution of shot attempts across contest categories",
      x = "Contest Category",
      y = "Number of Attempts"
    ) +
    theme_minimal() +
    theme(
      plot.title = element_text(face = "bold", size = 14),
      axis.text.x = element_text(angle = 45, hjust = 1)
    )

  return(list(p1 = p1, p2 = p2, p3 = p3, p4 = p4))
}

# 6. Player-level contest analysis
analyze_player_contest_vulnerability <- function(shots_df) {

  # Calculate each player's performance by contest level
  player_contest <- shots_df %>%
    group_by(shooter_name) %>%
    mutate(
      total_attempts = n(),
      overall_fg_pct = mean(shot_made)
    ) %>%
    filter(total_attempts >= 100) %>%
    group_by(shooter_name, contest_category, overall_fg_pct) %>%
    summarise(
      attempts = n(),
      fg_pct = mean(shot_made),
      .groups = 'drop'
    ) %>%
    pivot_wider(
      names_from = contest_category,
      values_from = c(attempts, fg_pct),
      names_sep = "_"
    )

  # Calculate contest vulnerability metric
  player_contest <- player_contest %>%
    mutate(
      # Difference between wide open and tight shooting
      contest_vulnerability = `fg_pct_Wide Open (6+ ft)` - `fg_pct_Tight (2-4 ft)`,

      # Percentage of shots that are contested
      pct_contested = (`attempts_Very Tight (0-2 ft)` + `attempts_Tight (2-4 ft)`) /
                      (`attempts_Very Tight (0-2 ft)` + `attempts_Tight (2-4 ft)` +
                       `attempts_Open (4-6 ft)` + `attempts_Wide Open (6+ ft)`)
    ) %>%
    arrange(desc(contest_vulnerability))

  return(player_contest)
}

# Main execution
main <- function() {
  cat("Starting Shot Contest Analysis\n")
  cat("=====================================\n\n")

  # Load data
  shots <- load_shot_data(seasons = c(2024))

  # Simulate defender distance (replace with actual tracking data in production)
  shots_with_distance <- simulate_defender_distance(shots)

  # Calculate metrics
  cat("\nCalculating contest metrics...\n")
  metrics <- calculate_contest_metrics(shots_with_distance)

  cat("\nOverall Contest Impact:\n")
  print(metrics$overall)

  cat("\nBy Shot Type:\n")
  print(metrics$by_shot_type)

  # Statistical modeling
  cat("\n\nFitting statistical models...\n")
  model_results <- model_contest_effect(shots_with_distance)

  # Create visualizations
  cat("\nGenerating visualizations...\n")
  plots <- visualize_contest_impact(metrics, model_results)

  # Save plots
  ggsave("contest_fg_pct.png", plots$p1, width = 10, height = 6, dpi = 300)
  ggsave("contest_by_shot_type.png", plots$p2, width = 12, height = 6, dpi = 300)
  ggsave("contest_continuous.png", plots$p3, width = 10, height = 6, dpi = 300)
  ggsave("contest_frequency.png", plots$p4, width = 10, height = 6, dpi = 300)

  cat("\nVisualizations saved!\n")

  # Player analysis
  cat("\nAnalyzing player contest vulnerability...\n")
  player_analysis <- analyze_player_contest_vulnerability(shots_with_distance)

  cat("\nTop 10 Most Contest-Vulnerable Players:\n")
  print(head(player_analysis %>% select(shooter_name, overall_fg_pct,
                                         contest_vulnerability, pct_contested), 10))

  cat("\n\nAnalysis complete!\n")

  return(list(
    shots = shots_with_distance,
    metrics = metrics,
    models = model_results,
    plots = plots,
    player_analysis = player_analysis
  ))
}

# Run the analysis
results <- main()

Statistical Modeling Insights

  • Non-linear Effects: Quadratic models capture diminishing returns of defender distance
  • Mixed Effects: Account for player-specific shooting ability and contest vulnerability
  • Marginal Effects: Quantify the precise impact of each additional foot of space
  • Player Profiling: Identify which players are most/least affected by contests

5. Impact on Shooting Percentages

Quantified Contest Effects

League-Wide Impact Data (2023-24 Season)

Contest Level 2PT FG% 3PT FG% Overall FG% eFG% Frequency
Very Tight (0-2 ft) 42.1% 28.9% 38.3% 42.7% 32.4%
Tight (2-4 ft) 48.6% 33.2% 43.1% 47.8% 37.2%
Open (4-6 ft) 52.3% 36.7% 46.4% 51.5% 21.8%
Wide Open (6+ ft) 54.8% 39.4% 49.2% 54.9% 8.6%

Key Findings

  • Total Contest Impact (2PT): 12.7 percentage point difference between very tight and wide open
  • Total Contest Impact (3PT): 10.5 percentage point difference between very tight and wide open
  • Largest Single Jump: 6.5 percentage points from very tight to tight (2-4 feet)
  • Diminishing Returns: Marginal benefit decreases with each additional category

Impact by Shot Type

Three-Point Shots
  • Most Sensitive to Contest: Corner threes show 13.2% differential
  • Above-the-Break Threes: 9.8% differential
  • Contest Threshold: Maximum impact occurs in 0-4 foot range
  • Elite Shooters: Show 5-7% smaller contest effect than league average
Mid-Range Shots
  • Moderate Contest Sensitivity: 8.9% differential overall
  • Pull-Up vs Catch-and-Shoot: Pull-ups show larger contest effect (10.3% vs 7.1%)
  • Distance Factor: Longer mid-range shots more affected by contest
At-Rim Shots
  • Contest Still Matters: 14.7% differential at rim
  • Vertical Contest: Rim protection more about vertical than horizontal distance
  • Shot Type Variation: Dunks/layups react differently to contest

Player Archetype Contest Vulnerabilities

1. Elite Shot Creators (Low Contest Vulnerability)
  • Examples: Curry, Durant, Lillard, Doncic
  • Contest Differential: 6-8 percentage points
  • Key Skill: Creating space, shooting over contests
  • Implication: Can take tightly contested shots without major efficiency loss
2. Catch-and-Shoot Specialists (High Contest Vulnerability)
  • Examples: Duncan Robinson, Joe Harris, Luke Kennard
  • Contest Differential: 12-15 percentage points
  • Key Skill: Elite when open, significant drop when contested
  • Implication: Require offensive system creating clean looks
3. Athletic Finishers (Moderate Contest Vulnerability)
  • Examples: Giannis, Zion, Edwards
  • Contest Differential: 8-10 percentage points
  • Key Skill: Can finish through contact and contests
  • Implication: Contest matters but athleticism compensates
4. Role Players (Very High Contest Vulnerability)
  • Contest Differential: 15-18 percentage points
  • Key Pattern: Rely heavily on system-generated open shots
  • Implication: Efficiency highly dependent on offensive scheme

6. Visualizations and Advanced Metrics

Key Visualization Types

1. Contest Curve Charts

Purpose: Show the continuous relationship between defender distance and shooting efficiency

  • X-axis: Defender distance (0-10 feet)
  • Y-axis: Field goal percentage
  • Multiple lines for different shot types or players
  • Highlights: Steepest decline zones, plateau regions

Insight: Reveals non-linear effects—most impact in 0-4 foot range, diminishing returns beyond 6 feet

2. Contest Heat Maps

Purpose: Spatial representation of where contests most impact shooting

  • Court overlay showing FG% differential by location and contest level
  • Color intensity indicates contest sensitivity
  • Separate maps for different defenders or offensive players

Insight: Corner threes and restricted area show highest contest sensitivity

3. Player Contest Profiles

Purpose: Compare individual player vulnerability to contests

  • Scatter plot: X = % of shots contested, Y = FG% differential (open vs contested)
  • Quadrant analysis identifying player types
  • Bubble size = total shot volume

Insight: Identifies elite shot creators vs system-dependent shooters

4. Defender Contest Effectiveness

Purpose: Evaluate defensive players' contest quality and impact

  • Contests per game vs opponent FG% when contested
  • Contest rate (% of defensive possessions with contest)
  • Average contest distance (closer = better)

Insight: Separates active defenders from effective ones

5. Shot Quality Expected vs Actual

Purpose: Measure shooting performance relative to contest difficulty

  • Expected FG% based on contest level vs actual FG%
  • Identifies over/underperformers given contest distribution
  • Useful for evaluating "degree of difficulty" scoring

Advanced Contest Metrics

Offensive Metrics
  • Contest Resilience Score: (Contested FG% / Expected FG%) × 100
  • Open Shot Creation Rate: % of shots taken with 4+ feet of space
  • Contested Shot Volume: FGA per game with defender within 2 feet
  • Contest Differential Impact: Total points lost due to contests vs league average
Defensive Metrics
  • Contest Quality Index: Weighted score combining contest rate and average distance
  • Contest Suppression Rate: Reduction in opponent FG% when contesting
  • Late Close-out Rate: % of contests where defender arrives in final 0.5 seconds
  • Effective Contest Rate: Contests resulting in miss or difficult shot / Total defensive plays
Team Metrics
  • Team Contest Defense Rating: Opponent eFG% on contested shots vs league average
  • Open Shot Surrender Rate: % of opponent shots taken with 6+ feet of space
  • Contest Consistency Score: Standard deviation of individual defender contest rates

7. Practical Applications for Coaching

Offensive Applications

1. Shot Selection Optimization

  • Shot Quality Analysis: Track each player's FG% by contest level to establish shot selection guidelines
  • Green Light Thresholds: Identify which players can shoot contested shots efficiently
  • Role Definition: Distinguish shot creators (can shoot contested) from spot-up shooters (need space)
  • Action Item: Create player-specific shot charts showing acceptable vs unacceptable contest levels

2. Offensive System Design

  • Space Creation Plays: Design sets specifically to generate open shots for contest-vulnerable players
  • Mismatch Hunting: Exploit defenders with poor contest rates or slow close-outs
  • Ball Movement Metrics: Track correlation between passes and open shot generation
  • Action Item: Measure "open shot generation rate" for different offensive actions

3. Player Development Focus

  • Contest Shooting Drills: Practice contested shooting for players showing high vulnerability
  • Shot Creation Training: Develop space creation skills to reduce contest frequency
  • Quick Release Work: Improve release speed to shoot before contests arrive
  • Action Item: Set individual improvement targets for contest differential reduction

4. Scouting Opponent Defenders

  • Contest Rate Mapping: Identify which defenders contest aggressively vs give space
  • Close-out Speed Analysis: Find slow close-out defenders to attack
  • Help Tendency Patterns: Exploit defenders who leave shooters to help
  • Action Item: Create pre-game defender profiles with contest tendencies

Defensive Applications

1. Individual Defensive Accountability

  • Contest Rate Targets: Set minimum contest rates for each position
  • Close-out Discipline: Teach controlled close-outs that maximize contest quality
  • Contest Distance Tracking: Monitor and reward defenders getting within 2 feet consistently
  • Action Item: Post-game review showing each defender's contest metrics

2. Defensive Scheme Optimization

  • Help Timing: Balance rim protection with perimeter contest rates
  • Rotation Priorities: Prioritize contesting high-value shots (corners, elite shooters)
  • Switch vs Drop Coverage: Analyze which scheme produces better contest rates
  • Action Item: Track open shot surrender rate by defensive coverage type

3. Matchup Strategies

  • Elite Shooter Attention: Assign best contest defenders to opponent's best shooters
  • Contest-Resilient Scorers: Identify opponents who shoot well contested, adjust strategy
  • Forcing Contest-Vulnerable Shots: Defensive strategy to make certain players shoot contested
  • Action Item: Create defensive game plans based on opponent contest vulnerability profiles

4. Defensive Communication Systems

  • Contest Calls: Verbal/non-verbal signals to ensure shooters are contested
  • Rotation Timing: Communicate to avoid late contests that don't affect shot
  • Priority Shooter Identification: Call out which shooters require tightest contests
  • Action Item: Implement color-coded system for contest urgency levels

Analytics Integration Workflow

Game Preparation (Pre-Game)

  1. Opponent Analysis: Review opponent's shooting by contest level
  2. Identify Targets: Find contest-vulnerable shooters to prioritize defending
  3. Exploit Opportunities: Locate defenders with poor contest rates to attack
  4. Set Game Plan: Establish contest rate targets and shot quality goals

In-Game Adjustments

  1. Live Tracking: Monitor real-time contest rates during game
  2. Rotation Decisions: Sub in/out defenders based on contest effectiveness
  3. Offensive Adjustments: Attack defenders showing poor contest discipline
  4. Timeout Strategy: Address contest breakdowns immediately

Post-Game Review

  1. Contest Metric Review: Analyze contest rates and effectiveness for each player
  2. Shot Quality Assessment: Evaluate offensive shot quality generated
  3. Identify Patterns: Find systematic contest issues in defensive scheme
  4. Player Feedback: Provide individual contest performance data
  5. Trend Tracking: Monitor contest metrics over rolling windows

Season-Long Development

  1. Baseline Establishment: Record initial contest vulnerability/effectiveness
  2. Progress Tracking: Monitor improvement in contest metrics
  3. Training Emphasis: Adjust practice based on contest data insights
  4. Role Optimization: Place players in roles matching their contest profiles

Best Practices and Considerations

Data Quality Considerations

  • Sample Size: Require minimum 50-100 attempts per category for reliable conclusions
  • Context Matters: Shot clock, game situation, and score affect shot quality
  • Defensive Scheme Impact: Team defensive system affects individual contest metrics
  • Measurement Limitations: 2D tracking misses vertical contest dimension

Interpretation Guidelines

  • League Average Comparison: Always compare to position and role-based averages
  • Shot Type Separation: Analyze different shot types (3PT, mid-range, rim) separately
  • Temporal Trends: Look for improvement/decline over time, not just snapshots
  • Correlation vs Causation: Good shooters may create space, causing good contest numbers

Common Pitfalls to Avoid

  • Over-Contesting: Aggressive contests can lead to fouls and poor positioning
  • Ignoring Help Defense: Rim protection sometimes more valuable than perimeter contest
  • Misidentifying Skill: Good shooters maintain efficiency despite contests (not because of openness)
  • Cherry-Picking Metrics: Use comprehensive contest profile, not single statistics

Conclusion

Shot contest analysis has revolutionized how basketball organizations evaluate defensive performance and offensive shot quality. By quantifying the precise impact of defender distance on shooting efficiency, teams can make data-driven decisions about shot selection, defensive strategy, player development, and roster construction.

The integration of tracking technology and advanced analytics provides coaches and analysts with objective measures previously unavailable. Understanding that a few feet of defensive space can mean 10+ percentage points in shooting efficiency fundamentally changes strategic thinking about offense and defense.

As tracking technology continues to advance, expect even more sophisticated contest metrics that account for defender height, hand position, closing speed, and other factors that influence shot difficulty. The teams that best integrate these insights into their decision-making processes will gain significant competitive advantages in an increasingly analytics-driven sport.

Further Resources

  • NBA Stats: Official Shot Defense Dashboard
  • Second Spectrum: NBA's official tracking technology provider
  • Cleaning the Glass: Advanced shot quality and contest metrics
  • Basketball Reference: Historical shooting data with basic contest information
  • NBA API Documentation: Technical documentation for accessing tracking data

Discussion

Have questions or feedback? Join our community discussion on Discord or GitHub Discussions.