Contested vs Open Shots
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
- Shot Detection: System identifies shot attempt via ball trajectory analysis
- Release Point Identification: Determines exact moment ball leaves shooter's hands
- Defender Proximity Calculation: Measures distance from shooter to nearest defender at release
- Contest Classification: Assigns category based on defender distance
- 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)
- Opponent Analysis: Review opponent's shooting by contest level
- Identify Targets: Find contest-vulnerable shooters to prioritize defending
- Exploit Opportunities: Locate defenders with poor contest rates to attack
- Set Game Plan: Establish contest rate targets and shot quality goals
In-Game Adjustments
- Live Tracking: Monitor real-time contest rates during game
- Rotation Decisions: Sub in/out defenders based on contest effectiveness
- Offensive Adjustments: Attack defenders showing poor contest discipline
- Timeout Strategy: Address contest breakdowns immediately
Post-Game Review
- Contest Metric Review: Analyze contest rates and effectiveness for each player
- Shot Quality Assessment: Evaluate offensive shot quality generated
- Identify Patterns: Find systematic contest issues in defensive scheme
- Player Feedback: Provide individual contest performance data
- Trend Tracking: Monitor contest metrics over rolling windows
Season-Long Development
- Baseline Establishment: Record initial contest vulnerability/effectiveness
- Progress Tracking: Monitor improvement in contest metrics
- Training Emphasis: Adjust practice based on contest data insights
- 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