Catch-and-Shoot vs Pull-Up Shooting

Beginner 10 min read 1 views Nov 27, 2025

Catch-and-Shoot vs Pull-Up Shooting

Shot creation is one of the most critical aspects of offensive basketball, and understanding the distinction between catch-and-shoot and pull-up attempts provides fundamental insights into player roles, team strategy, and offensive efficiency. NBA tracking technology has revolutionized how we analyze these shot types, revealing patterns that inform everything from player development to roster construction.

Definitions and Mechanics

Catch-and-Shoot

A catch-and-shoot opportunity occurs when a player receives a pass and attempts a shot without taking a dribble or with minimal time between receiving the ball and releasing. According to NBA Advanced Stats tracking:

  • Technical Definition: A shot attempt taken within 2 seconds of touching the ball where the player took 0 dribbles
  • Skill Components: Off-ball movement, shot preparation, quick release mechanics, spatial awareness
  • Typical Scenarios: Spot-up threes, corner threes off kickouts, transition opportunities, pin-down actions
  • Defensive Challenge: Closeouts, help recovery, rotation speed

Pull-Up Shooting

A pull-up shot involves a player creating their own shot opportunity off the dribble, typically requiring deceleration, gathering, and shooting in one fluid motion:

  • Technical Definition: A shot attempt taken off the dribble (1+ dribbles) where the player creates separation through their own ball-handling
  • Skill Components: Ball-handling, deceleration mechanics, balance, shot creation, reading defensive positioning
  • Typical Scenarios: Pick-and-roll pull-ups, isolation mid-range, transition pull-ups, step-back threes
  • Defensive Challenge: On-ball defense, screen navigation, help positioning

NBA Tracking Methodology

The NBA's Second Spectrum tracking system (formerly SportVU) uses optical tracking cameras installed in every arena to capture shot type data:

Data Collection Process

  • Camera System: 6 cameras per arena capturing 25 frames per second
  • Ball Tracking: X, Y, Z coordinates tracked throughout possession
  • Player Tracking: Position, speed, and movement data for all 10 players
  • Touch Classification: Algorithm determines time of possession, dribbles taken, and shot preparation time

Classification Criteria

Shot Type Classification:
├── Catch and Shoot
│   └── Dribbles: 0
│   └── Touch Time: < 2 seconds
│
├── Pull-Up
│   └── Dribbles: 1+
│   └── Shot taken off dribble motion
│
└── Other (less than 5% of shots)
    └── Tip-ins, putbacks, standing shots > 2 seconds

Data Analysis with Python (nba_api)

The nba_api library provides access to NBA.com's official tracking data, including detailed shot type statistics.

Retrieving Shot Type Data

from nba_api.stats.endpoints import leaguedashplayerstats, playerdashboardbyshootingsplits
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)

# Get current season shot type data
def get_shot_type_data(season='2023-24', min_fga=100):
    """
    Retrieve catch-and-shoot and pull-up shooting data for all players

    Parameters:
    -----------
    season : str
        NBA season in format 'YYYY-YY'
    min_fga : int
        Minimum field goal attempts to include player

    Returns:
    --------
    DataFrame with shot type statistics
    """

    # Get shooting splits data
    shooting_data = leaguedashplayerstats.LeagueDashPlayerStats(
        season=season,
        season_type_all_star='Regular Season',
        per_mode_detailed='PerGame',
        measure_type_detailed_defense='Base'
    )

    df = shooting_data.get_data_frames()[0]

    # Filter by minimum attempts
    df = df[df['FGA'] >= min_fga].copy()

    # Get detailed shot type data for each player
    shot_type_stats = []

    for player_id in df['PLAYER_ID'].unique():
        try:
            # Get shot dashboard for player
            dashboard = playerdashboardbyshootingsplits.PlayerDashboardByShootingSplits(
                player_id=player_id,
                season=season,
                season_type_all_star='Regular Season'
            )

            # Extract shot type data
            shot_type_df = dashboard.get_data_frames()[0]

            # Process catch-and-shoot
            catch_shoot = shot_type_df[shot_type_df['SHOT_TYPE'] == 'Catch and Shoot']
            if not catch_shoot.empty:
                cs_fga = catch_shoot['FGA'].values[0]
                cs_fgm = catch_shoot['FGM'].values[0]
                cs_fg_pct = catch_shoot['FG_PCT'].values[0]
                cs_fg3a = catch_shoot['FG3A'].values[0]
                cs_fg3m = catch_shoot['FG3M'].values[0]
                cs_fg3_pct = catch_shoot['FG3_PCT'].values[0]
            else:
                cs_fga = cs_fgm = cs_fg_pct = cs_fg3a = cs_fg3m = cs_fg3_pct = 0

            # Process pull-ups
            pullup = shot_type_df[shot_type_df['SHOT_TYPE'] == 'Pull Up']
            if not pullup.empty:
                pu_fga = pullup['FGA'].values[0]
                pu_fgm = pullup['FGM'].values[0]
                pu_fg_pct = pullup['FG_PCT'].values[0]
                pu_fg3a = pullup['FG3A'].values[0]
                pu_fg3m = pullup['FG3M'].values[0]
                pu_fg3_pct = pullup['FG3_PCT'].values[0]
            else:
                pu_fga = pu_fgm = pu_fg_pct = pu_fg3a = pu_fg3m = pu_fg3_pct = 0

            shot_type_stats.append({
                'PLAYER_ID': player_id,
                'CS_FGA': cs_fga,
                'CS_FGM': cs_fgm,
                'CS_FG_PCT': cs_fg_pct,
                'CS_3PA': cs_fg3a,
                'CS_3PM': cs_fg3m,
                'CS_3P_PCT': cs_fg3_pct,
                'PU_FGA': pu_fga,
                'PU_FGM': pu_fgm,
                'PU_FG_PCT': pu_fg_pct,
                'PU_3PA': pu_fg3a,
                'PU_3PM': pu_fg3m,
                'PU_3P_PCT': pu_fg3_pct
            })

        except Exception as e:
            print(f"Error processing player {player_id}: {str(e)}")
            continue

    # Merge with player info
    shot_type_df = pd.DataFrame(shot_type_stats)
    result_df = df.merge(shot_type_df, on='PLAYER_ID', how='inner')

    # Calculate shot distribution
    result_df['CS_FREQ'] = result_df['CS_FGA'] / result_df['FGA']
    result_df['PU_FREQ'] = result_df['PU_FGA'] / result_df['FGA']

    # Calculate efficiency metrics
    result_df['CS_PPS'] = (result_df['CS_FGM'] * 2 + result_df['CS_3PM']) / result_df['CS_FGA']
    result_df['PU_PPS'] = (result_df['PU_FGM'] * 2 + result_df['PU_3PM']) / result_df['PU_FGA']

    return result_df

# Load data
print("Fetching shot type data...")
shot_data = get_shot_type_data(season='2023-24', min_fga=200)

# Display top catch-and-shoot specialists
print("\nTop 10 Catch-and-Shoot Specialists (by frequency):")
top_cs = shot_data.nlargest(10, 'CS_FREQ')[
    ['PLAYER_NAME', 'CS_FGA', 'CS_FG_PCT', 'CS_3P_PCT', 'CS_FREQ', 'CS_PPS']
]
print(top_cs.to_string(index=False))

# Display top pull-up shooters
print("\nTop 10 Pull-Up Shooters (by frequency):")
top_pu = shot_data.nlargest(10, 'PU_FREQ')[
    ['PLAYER_NAME', 'PU_FGA', 'PU_FG_PCT', 'PU_3P_PCT', 'PU_FREQ', 'PU_PPS']
]
print(top_pu.to_string(index=False))

# Statistical comparison
print("\nLeague Average Efficiency:")
print(f"Catch-and-Shoot FG%: {shot_data['CS_FG_PCT'].mean():.3f}")
print(f"Catch-and-Shoot 3P%: {shot_data['CS_3P_PCT'].mean():.3f}")
print(f"Catch-and-Shoot PPS: {shot_data['CS_PPS'].mean():.3f}")
print(f"\nPull-Up FG%: {shot_data['PU_FG_PCT'].mean():.3f}")
print(f"Pull-Up 3P%: {shot_data['PU_3P_PCT'].mean():.3f}")
print(f"Pull-Up PPS: {shot_data['PU_PPS'].mean():.3f}")

# Identify versatile scorers (high frequency in both)
shot_data['VERSATILITY'] = shot_data['CS_FREQ'] * shot_data['PU_FREQ']
print("\nMost Versatile Shooters (balanced shot diet):")
versatile = shot_data.nlargest(10, 'VERSATILITY')[
    ['PLAYER_NAME', 'CS_FREQ', 'PU_FREQ', 'CS_FG_PCT', 'PU_FG_PCT']
]
print(versatile.to_string(index=False))

# Save to CSV
shot_data.to_csv('shot_type_analysis.csv', index=False)
print("\nData saved to shot_type_analysis.csv")

Advanced Shot Type Analysis

# Analyze shot type patterns by position
def analyze_by_position(df):
    """
    Analyze shot type tendencies by player position
    """
    position_analysis = df.groupby('POSITION').agg({
        'CS_FREQ': 'mean',
        'PU_FREQ': 'mean',
        'CS_FG_PCT': 'mean',
        'PU_FG_PCT': 'mean',
        'CS_3P_PCT': 'mean',
        'PU_3P_PCT': 'mean',
        'CS_PPS': 'mean',
        'PU_PPS': 'mean'
    }).round(3)

    return position_analysis

# Analyze shot selection vs efficiency
def efficiency_analysis(df):
    """
    Examine relationship between shot type frequency and efficiency
    """
    # Create efficiency tiers
    df['CS_EFFICIENCY_TIER'] = pd.qcut(df['CS_PPS'], q=4,
                                        labels=['Low', 'Medium', 'High', 'Elite'])
    df['PU_EFFICIENCY_TIER'] = pd.qcut(df['PU_PPS'], q=4,
                                        labels=['Low', 'Medium', 'High', 'Elite'])

    # Analyze volume vs efficiency
    print("\nCatch-and-Shoot: Volume vs Efficiency")
    cs_correlation = df[['CS_FGA', 'CS_PPS']].corr().iloc[0, 1]
    print(f"Correlation between attempts and efficiency: {cs_correlation:.3f}")

    print("\nPull-Up: Volume vs Efficiency")
    pu_correlation = df[['PU_FGA', 'PU_PPS']].corr().iloc[0, 1]
    print(f"Correlation between attempts and efficiency: {pu_correlation:.3f}")

    return df

# Identify role players vs shot creators
def classify_shooting_roles(df):
    """
    Classify players into shooting archetypes
    """
    conditions = [
        (df['CS_FREQ'] >= 0.5) & (df['PU_FREQ'] < 0.3),
        (df['PU_FREQ'] >= 0.5) & (df['CS_FREQ'] < 0.3),
        (df['CS_FREQ'] >= 0.35) & (df['PU_FREQ'] >= 0.35),
        (df['CS_FREQ'] < 0.35) & (df['PU_FREQ'] < 0.35)
    ]

    roles = [
        'Catch-and-Shoot Specialist',
        'Shot Creator',
        'Versatile Scorer',
        'Rim Runner/Post Player'
    ]

    df['SHOOTING_ROLE'] = np.select(conditions, roles, default='Balanced Scorer')

    role_counts = df['SHOOTING_ROLE'].value_counts()
    print("\nShooting Role Distribution:")
    print(role_counts)

    return df

# Run analyses
shot_data = efficiency_analysis(shot_data)
shot_data = classify_shooting_roles(shot_data)

if 'POSITION' in shot_data.columns:
    position_stats = analyze_by_position(shot_data)
    print("\nShot Type Tendencies by Position:")
    print(position_stats)

Statistical Analysis with R (hoopR)

R's hoopR package combined with tidyverse provides powerful tools for analyzing shooting efficiency patterns and creating publication-quality visualizations.

Loading and Processing Shot Type Data

library(hoopR)
library(tidyverse)
library(ggplot2)
library(scales)
library(plotly)
library(ggrepel)

# Load NBA shot tracking data
get_shot_type_stats <- function(season = 2024) {

  # Get player stats from NBA API via hoopR
  player_stats <- nba_leaguedashplayerstats(
    season = season,
    season_type = "Regular Season"
  )

  # Process and clean data
  stats_df <- player_stats$LeagueDashPlayerStats %>%
    filter(FGA >= 200) %>%
    select(PLAYER_ID, PLAYER_NAME, TEAM_ABBREVIATION,
           GP, MIN, FGA, FGM, FG_PCT, FG3A, FG3M, FG3_PCT, PTS)

  return(stats_df)
}

# Calculate advanced shooting metrics
calculate_shot_metrics <- function(df) {

  df <- df %>%
    mutate(
      # Shooting efficiency
      TS_PCT = PTS / (2 * (FGA + 0.44 * FTA)),
      EFG_PCT = (FGM + 0.5 * FG3M) / FGA,

      # Volume metrics
      FGA_PER_GAME = FGA / GP,
      FG3A_RATE = FG3A / FGA,

      # Scoring metrics
      PPG = PTS / GP,
      PTS_PER_FGA = PTS / FGA
    )

  return(df)
}

# Load sample shot type data (simulated for demonstration)
set.seed(42)
shot_type_data <- tibble(
  PLAYER_NAME = c("Stephen Curry", "Damian Lillard", "Klay Thompson",
                  "Duncan Robinson", "Joe Harris", "Buddy Hield",
                  "Luka Doncic", "Trae Young", "James Harden",
                  "Kevin Durant", "Devin Booker", "Donovan Mitchell",
                  "Jayson Tatum", "LeBron James", "Giannis Antetokounmpo",
                  "Nikola Jokic", "Joel Embiid", "Anthony Davis"),

  # Catch-and-Shoot metrics
  CS_FGA = c(6.2, 4.1, 7.8, 8.5, 6.9, 7.2,
             2.3, 2.8, 3.1, 4.5, 3.2, 3.9,
             4.1, 3.5, 1.2, 2.1, 2.8, 2.5),

  CS_FG_PCT = c(0.458, 0.445, 0.462, 0.448, 0.456, 0.441,
                0.412, 0.398, 0.425, 0.475, 0.438, 0.429,
                0.442, 0.451, 0.389, 0.425, 0.418, 0.445),

  CS_3PA = c(5.8, 3.9, 7.5, 8.2, 6.7, 7.0,
             2.1, 2.5, 2.9, 3.8, 2.8, 3.5,
             3.7, 2.9, 0.8, 1.2, 1.5, 1.8),

  CS_3P_PCT = c(0.442, 0.425, 0.445, 0.435, 0.441, 0.428,
                0.395, 0.378, 0.405, 0.458, 0.421, 0.412,
                0.425, 0.438, 0.312, 0.385, 0.375, 0.418),

  # Pull-up metrics
  PU_FGA = c(8.5, 10.2, 3.1, 2.5, 2.8, 3.2,
             11.5, 10.8, 9.2, 9.5, 10.1, 9.8,
             8.9, 7.2, 8.5, 6.8, 7.5, 6.9),

  PU_FG_PCT = c(0.425, 0.418, 0.398, 0.365, 0.378, 0.385,
                0.445, 0.432, 0.421, 0.485, 0.458, 0.442,
                0.438, 0.465, 0.512, 0.498, 0.475, 0.488),

  PU_3PA = c(7.2, 8.5, 2.5, 1.8, 2.1, 2.5,
             6.8, 7.2, 6.5, 4.2, 5.8, 6.1,
             5.5, 3.1, 1.5, 0.8, 1.2, 0.9),

  PU_3P_PCT = c(0.398, 0.385, 0.368, 0.345, 0.358, 0.362,
                0.368, 0.358, 0.365, 0.425, 0.388, 0.378,
                0.385, 0.375, 0.285, 0.312, 0.325, 0.298),

  # Player context
  POSITION = c("PG", "PG", "SG", "SF", "SF", "SG",
               "PG", "PG", "SG", "PF", "SG", "SG",
               "SF", "SF", "PF", "C", "C", "PF"),

  ROLE = c("Star", "Star", "Starter", "Starter", "Starter", "Starter",
           "Star", "Star", "Star", "Star", "Star", "Star",
           "Star", "Star", "Star", "Star", "Star", "Star")
)

# Calculate derived metrics
shot_type_data <- shot_type_data %>%
  mutate(
    # Frequency
    TOTAL_FGA = CS_FGA + PU_FGA,
    CS_FREQ = CS_FGA / TOTAL_FGA,
    PU_FREQ = PU_FGA / TOTAL_FGA,

    # Points per shot
    CS_PPS = (CS_FGA * CS_FG_PCT * 2 + CS_3PA * CS_3P_PCT) / CS_FGA,
    PU_PPS = (PU_FGA * PU_FG_PCT * 2 + PU_3PA * PU_3P_PCT) / PU_FGA,

    # 3-point rate
    CS_3PA_RATE = CS_3PA / CS_FGA,
    PU_3PA_RATE = PU_3PA / PU_FGA,

    # Efficiency advantage
    EFFICIENCY_ADVANTAGE = CS_PPS - PU_PPS,

    # Volume score (attempts × efficiency)
    CS_VOLUME_SCORE = CS_FGA * CS_PPS,
    PU_VOLUME_SCORE = PU_FGA * PU_PPS,

    # Shooting archetype
    ARCHETYPE = case_when(
      CS_FREQ >= 0.55 ~ "Catch-and-Shoot Specialist",
      PU_FREQ >= 0.55 ~ "Shot Creator",
      CS_FREQ >= 0.40 & PU_FREQ >= 0.40 ~ "Versatile Scorer",
      TRUE ~ "Balanced"
    )
  )

# Summary statistics
cat("\n=== LEAGUE AVERAGES ===\n")
summary_stats <- shot_type_data %>%
  summarise(
    Avg_CS_FGA = mean(CS_FGA),
    Avg_CS_FG_PCT = mean(CS_FG_PCT),
    Avg_CS_3P_PCT = mean(CS_3P_PCT),
    Avg_CS_PPS = mean(CS_PPS),
    Avg_PU_FGA = mean(PU_FGA),
    Avg_PU_FG_PCT = mean(PU_FG_PCT),
    Avg_PU_3P_PCT = mean(PU_3P_PCT),
    Avg_PU_PPS = mean(PU_PPS)
  )

print(summary_stats)

# Archetype distribution
cat("\n=== SHOOTING ARCHETYPE DISTRIBUTION ===\n")
archetype_counts <- shot_type_data %>%
  count(ARCHETYPE, sort = TRUE)
print(archetype_counts)

Comparative Efficiency Analysis

# Statistical tests
efficiency_comparison <- function(data) {

  # Paired t-test: CS vs PU efficiency
  cat("\n=== EFFICIENCY COMPARISON ===\n")

  t_test_result <- t.test(data$CS_PPS, data$PU_PPS, paired = TRUE)

  cat(sprintf("Mean CS PPS: %.3f\n", mean(data$CS_PPS)))
  cat(sprintf("Mean PU PPS: %.3f\n", mean(data$PU_PPS)))
  cat(sprintf("Difference: %.3f\n", mean(data$CS_PPS) - mean(data$PU_PPS)))
  cat(sprintf("t-statistic: %.3f\n", t_test_result$statistic))
  cat(sprintf("p-value: %.4f\n", t_test_result$p.value))

  if (t_test_result$p.value < 0.05) {
    cat("Result: Statistically significant difference in efficiency\n")
  } else {
    cat("Result: No significant difference in efficiency\n")
  }

  # Correlation analysis
  cat("\n=== CORRELATION ANALYSIS ===\n")

  cor_cs_volume_eff <- cor(data$CS_FGA, data$CS_PPS)
  cor_pu_volume_eff <- cor(data$PU_FGA, data$PU_PPS)

  cat(sprintf("CS Volume-Efficiency Correlation: %.3f\n", cor_cs_volume_eff))
  cat(sprintf("PU Volume-Efficiency Correlation: %.3f\n", cor_pu_volume_eff))

  # Position-based analysis
  cat("\n=== EFFICIENCY BY POSITION ===\n")

  position_stats <- data %>%
    group_by(POSITION) %>%
    summarise(
      Count = n(),
      Avg_CS_FGA = mean(CS_FGA),
      Avg_CS_PPS = mean(CS_PPS),
      Avg_PU_FGA = mean(PU_FGA),
      Avg_PU_PPS = mean(PU_PPS),
      CS_Advantage = mean(CS_PPS) - mean(PU_PPS)
    ) %>%
    arrange(desc(CS_Advantage))

  print(position_stats, n = Inf)
}

# Run efficiency analysis
efficiency_comparison(shot_type_data)

# Advanced metrics by archetype
cat("\n=== METRICS BY ARCHETYPE ===\n")

archetype_analysis <- shot_type_data %>%
  group_by(ARCHETYPE) %>%
  summarise(
    Count = n(),
    Avg_CS_FREQ = mean(CS_FREQ),
    Avg_PU_FREQ = mean(PU_FREQ),
    Avg_CS_PPS = mean(CS_PPS),
    Avg_PU_PPS = mean(PU_PPS),
    Avg_Total_FGA = mean(TOTAL_FGA)
  ) %>%
  arrange(desc(Avg_CS_FREQ))

print(archetype_analysis, n = Inf)

Visualizations

Shot Type Distribution and Efficiency

# Visualization 1: Scatter plot - Frequency vs Efficiency
plot_freq_vs_efficiency <- function(data) {

  p <- ggplot(data, aes(x = CS_FREQ, y = PU_FREQ)) +
    geom_point(aes(size = TOTAL_FGA, color = ARCHETYPE), alpha = 0.7) +
    geom_text_repel(aes(label = PLAYER_NAME), size = 3, max.overlaps = 15) +
    geom_vline(xintercept = 0.5, linetype = "dashed", alpha = 0.5) +
    geom_hline(yintercept = 0.5, linetype = "dashed", alpha = 0.5) +
    scale_x_continuous(labels = percent_format(), limits = c(0, 1)) +
    scale_y_continuous(labels = percent_format(), limits = c(0, 1)) +
    scale_size_continuous(range = c(3, 10)) +
    scale_color_brewer(palette = "Set2") +
    labs(
      title = "Shot Type Distribution: Catch-and-Shoot vs Pull-Up",
      subtitle = "Size represents total FGA per game",
      x = "Catch-and-Shoot Frequency",
      y = "Pull-Up Frequency",
      color = "Shooting Archetype",
      size = "Total FGA"
    ) +
    theme_minimal() +
    theme(
      plot.title = element_text(face = "bold", size = 14),
      legend.position = "right"
    )

  return(p)
}

# Visualization 2: Efficiency comparison
plot_efficiency_comparison <- function(data) {

  # Reshape data for plotting
  efficiency_long <- data %>%
    select(PLAYER_NAME, CS_PPS, PU_PPS, ARCHETYPE) %>%
    pivot_longer(cols = c(CS_PPS, PU_PPS),
                 names_to = "Shot_Type",
                 values_to = "PPS") %>%
    mutate(Shot_Type = recode(Shot_Type,
                              CS_PPS = "Catch-and-Shoot",
                              PU_PPS = "Pull-Up"))

  p <- ggplot(efficiency_long, aes(x = reorder(PLAYER_NAME, PPS),
                                   y = PPS, fill = Shot_Type)) +
    geom_col(position = "dodge", alpha = 0.8) +
    coord_flip() +
    scale_fill_manual(values = c("Catch-and-Shoot" = "#1f77b4",
                                  "Pull-Up" = "#ff7f0e")) +
    labs(
      title = "Points Per Shot: Catch-and-Shoot vs Pull-Up",
      subtitle = "Comparing efficiency across shot types",
      x = NULL,
      y = "Points Per Shot (PPS)",
      fill = "Shot Type"
    ) +
    theme_minimal() +
    theme(
      plot.title = element_text(face = "bold", size = 14),
      legend.position = "top",
      axis.text.y = element_text(size = 9)
    )

  return(p)
}

# Visualization 3: Volume vs Efficiency quadrant
plot_volume_efficiency_quadrant <- function(data) {

  # Calculate medians for quadrant lines
  median_cs_fga <- median(data$CS_FGA)
  median_cs_pps <- median(data$CS_PPS)

  p <- ggplot(data, aes(x = CS_FGA, y = CS_PPS)) +
    geom_vline(xintercept = median_cs_fga, linetype = "dashed",
               color = "gray50", alpha = 0.7) +
    geom_hline(yintercept = median_cs_pps, linetype = "dashed",
               color = "gray50", alpha = 0.7) +
    geom_point(aes(color = ARCHETYPE, size = CS_3P_PCT), alpha = 0.7) +
    geom_text_repel(aes(label = PLAYER_NAME), size = 2.5, max.overlaps = 12) +
    scale_color_brewer(palette = "Set2") +
    scale_size_continuous(range = c(3, 8), labels = percent_format()) +
    labs(
      title = "Catch-and-Shoot: Volume vs Efficiency",
      subtitle = "Quadrants based on median values",
      x = "Catch-and-Shoot FGA per game",
      y = "Catch-and-Shoot PPS",
      color = "Archetype",
      size = "3P%"
    ) +
    annotate("text", x = Inf, y = Inf, label = "High Volume\nHigh Efficiency",
             hjust = 1.1, vjust = 1.5, color = "gray40", size = 3) +
    annotate("text", x = -Inf, y = Inf, label = "Low Volume\nHigh Efficiency",
             hjust = -0.1, vjust = 1.5, color = "gray40", size = 3) +
    annotate("text", x = Inf, y = -Inf, label = "High Volume\nLow Efficiency",
             hjust = 1.1, vjust = -0.5, color = "gray40", size = 3) +
    annotate("text", x = -Inf, y = -Inf, label = "Low Volume\nLow Efficiency",
             hjust = -0.1, vjust = -0.5, color = "gray40", size = 3) +
    theme_minimal() +
    theme(plot.title = element_text(face = "bold", size = 14))

  return(p)
}

# Visualization 4: 3-Point rate comparison
plot_three_point_rate <- function(data) {

  p <- ggplot(data, aes(x = CS_3PA_RATE, y = PU_3PA_RATE)) +
    geom_point(aes(color = POSITION, size = TOTAL_FGA), alpha = 0.7) +
    geom_text_repel(aes(label = PLAYER_NAME), size = 2.5, max.overlaps = 10) +
    geom_abline(slope = 1, intercept = 0, linetype = "dashed",
                color = "red", alpha = 0.5) +
    scale_x_continuous(labels = percent_format(), limits = c(0.5, 1)) +
    scale_y_continuous(labels = percent_format(), limits = c(0.5, 1)) +
    scale_color_brewer(palette = "Dark2") +
    labs(
      title = "Three-Point Rate: Catch-and-Shoot vs Pull-Up",
      subtitle = "Proportion of attempts from beyond the arc",
      x = "CS 3PA / CS FGA",
      y = "PU 3PA / PU FGA",
      color = "Position",
      size = "Total FGA"
    ) +
    theme_minimal() +
    theme(plot.title = element_text(face = "bold", size = 14))

  return(p)
}

# Generate all plots
p1 <- plot_freq_vs_efficiency(shot_type_data)
p2 <- plot_efficiency_comparison(shot_type_data)
p3 <- plot_volume_efficiency_quadrant(shot_type_data)
p4 <- plot_three_point_rate(shot_type_data)

# Display plots
print(p1)
print(p2)
print(p3)
print(p4)

# Save plots
ggsave("shot_type_distribution.png", p1, width = 12, height = 8, dpi = 300)
ggsave("efficiency_comparison.png", p2, width = 10, height = 12, dpi = 300)
ggsave("volume_efficiency_quadrant.png", p3, width = 12, height = 8, dpi = 300)
ggsave("three_point_rate.png", p4, width = 12, height = 8, dpi = 300)

Advanced Visualizations

# Heatmap: Shot type preferences by position
plot_position_heatmap <- function(data) {

  heatmap_data <- data %>%
    group_by(POSITION) %>%
    summarise(
      CS_FGA = mean(CS_FGA),
      CS_PPS = mean(CS_PPS),
      PU_FGA = mean(PU_FGA),
      PU_PPS = mean(PU_PPS)
    ) %>%
    pivot_longer(cols = -POSITION, names_to = "Metric", values_to = "Value")

  p <- ggplot(heatmap_data, aes(x = Metric, y = POSITION, fill = Value)) +
    geom_tile(color = "white") +
    geom_text(aes(label = round(Value, 2)), color = "white", fontface = "bold") +
    scale_fill_gradient2(low = "#3498db", mid = "#f39c12", high = "#e74c3c",
                         midpoint = median(heatmap_data$Value)) +
    labs(
      title = "Shot Type Metrics by Position",
      x = NULL,
      y = NULL,
      fill = "Value"
    ) +
    theme_minimal() +
    theme(
      plot.title = element_text(face = "bold", size = 14),
      axis.text.x = element_text(angle = 45, hjust = 1)
    )

  return(p)
}

# Density plot: Distribution of efficiency advantage
plot_efficiency_advantage_dist <- function(data) {

  p <- ggplot(data, aes(x = EFFICIENCY_ADVANTAGE)) +
    geom_density(fill = "#3498db", alpha = 0.6) +
    geom_vline(xintercept = 0, linetype = "dashed", color = "red", size = 1) +
    geom_vline(xintercept = mean(data$EFFICIENCY_ADVANTAGE),
               linetype = "solid", color = "darkblue", size = 1) +
    labs(
      title = "Distribution of Efficiency Advantage",
      subtitle = "CS PPS - PU PPS (positive = CS more efficient)",
      x = "Efficiency Advantage (PPS)",
      y = "Density"
    ) +
    annotate("text", x = 0, y = Inf,
             label = "Break-even", vjust = 2, hjust = -0.1, color = "red") +
    annotate("text", x = mean(data$EFFICIENCY_ADVANTAGE), y = Inf,
             label = paste0("Mean: ", round(mean(data$EFFICIENCY_ADVANTAGE), 3)),
             vjust = 2, hjust = 1.1, color = "darkblue") +
    theme_minimal() +
    theme(plot.title = element_text(face = "bold", size = 14))

  return(p)
}

# Interactive plotly visualization
create_interactive_plot <- function(data) {

  p <- plot_ly(data,
               x = ~CS_FGA,
               y = ~PU_FGA,
               z = ~TOTAL_FGA,
               color = ~ARCHETYPE,
               text = ~PLAYER_NAME,
               type = "scatter3d",
               mode = "markers",
               marker = list(size = ~TOTAL_FGA * 2, opacity = 0.7)) %>%
    layout(
      title = "3D Shot Type Analysis",
      scene = list(
        xaxis = list(title = "CS FGA"),
        yaxis = list(title = "PU FGA"),
        zaxis = list(title = "Total FGA")
      )
    )

  return(p)
}

# Generate additional plots
p5 <- plot_position_heatmap(shot_type_data)
p6 <- plot_efficiency_advantage_dist(shot_type_data)
p7 <- create_interactive_plot(shot_type_data)

print(p5)
print(p6)
print(p7)

ggsave("position_heatmap.png", p5, width = 10, height = 6, dpi = 300)
ggsave("efficiency_advantage_dist.png", p6, width = 10, height = 6, dpi = 300)

Player Profiles and Archetypes

Catch-and-Shoot Specialists

Players who derive the majority of their scoring opportunities from off-ball movement and spot-up situations.

Duncan Robinson (Miami Heat)

  • CS Frequency: 68% of all field goal attempts
  • CS 3P%: 43.5% on 8.2 attempts per game
  • CS PPS: 1.31 (Elite efficiency)
  • Role Impact: Gravity creator, spacing provider, DHO weapon
  • Shot Distribution: 96% of CS attempts are threes

Analysis: Robinson epitomizes the modern 3-and-D archetype, with elite off-ball movement generating high-value catch-and-shoot opportunities. His shooting gravity forces defensive rotations, creating advantages for teammates.

Klay Thompson (Golden State Warriors)

  • CS Frequency: 71% of all field goal attempts
  • CS 3P%: 44.5% on 7.5 attempts per game
  • CS PPS: 1.35 (Elite efficiency)
  • Role Impact: Motion offense fulcrum, transition threat, quick release specialist
  • Shot Distribution: 95% of CS attempts are threes

Analysis: Thompson's combination of elite shooting and tireless off-ball movement makes him the perfect complementary scorer. His ability to score in bunches on catch-and-shoot attempts creates unpredictable scoring bursts.

Shot Creators

Players who create their own shooting opportunities through ball-handling and isolation play.

Luka Doncic (Dallas Mavericks)

  • PU Frequency: 63% of all field goal attempts
  • PU FG%: 44.5% overall, 36.8% on threes
  • PU PPS: 1.15 (Above average for pull-ups)
  • Role Impact: Primary ball-handler, pick-and-roll maestro, step-back specialist
  • Shot Distribution: 59% of PU attempts are threes

Analysis: Doncic's game is predicated on creating advantages through his handle and passing threat. His pull-up shooting forces defenses to extend beyond the three-point line, opening driving lanes and pick-and-roll opportunities.

Damian Lillard (Milwaukee Bucks)

  • PU Frequency: 66% of all field goal attempts
  • PU FG%: 41.8% overall, 38.5% on threes
  • PU PPS: 1.18 (Elite for pull-ups)
  • Role Impact: Logo range threat, pick-and-roll scorer, late-clock specialist
  • Shot Distribution: 83% of PU attempts are threes

Analysis: Lillard's deep shooting range revolutionizes floor spacing. His willingness and ability to shoot from 30+ feet forces unprecedented defensive coverage, creating driving lanes for himself and teammates.

Versatile Scorers

Elite players who excel in both catch-and-shoot and pull-up situations, providing maximum offensive flexibility.

Stephen Curry (Golden State Warriors)

  • CS Frequency: 42% | PU Frequency: 58%
  • CS 3P%: 44.2% | PU 3P%: 39.8%
  • CS PPS: 1.38 | PU PPS: 1.21
  • Role Impact: Dual-threat scorer, motion offense hub, gravity supernova

Analysis: Curry's unique ability to score efficiently in both modes makes him impossible to game-plan against. Defenders must respect his shooting whether he's spotting up or coming off the dribble, creating unprecedented offensive stress.

Kevin Durant (Phoenix Suns)

  • CS Frequency: 32% | PU Frequency: 68%
  • CS 3P%: 45.8% | PU 3P%: 42.5%
  • CS PPS: 1.42 | PU PPS: 1.28
  • Role Impact: Matchup nightmare, mid-range maestro, efficient shot creator

Analysis: Durant's 7-foot frame with guard skills allows him to shoot over any defender in either scenario. His efficiency on pull-ups approaches catch-and-shoot levels, a nearly unique combination in NBA history.

Devin Booker (Phoenix Suns)

  • CS Frequency: 24% | PU Frequency: 76%
  • CS 3P%: 42.1% | PU 3P%: 38.8%
  • CS PPS: 1.33 | PU PPS: 1.24
  • Role Impact: Mid-range specialist, pick-and-roll scorer, crunch-time closer

Analysis: Booker's scoring versatility makes him a complete offensive weapon. His willingness to take mid-range pull-ups keeps defenses honest and prevents extreme drop coverage in pick-and-roll situations.

League-Wide Efficiency Patterns

Overall Efficiency Comparison

Shot Type League Avg FG% League Avg 3P% League Avg PPS Efficiency Rank
Catch-and-Shoot 43.8% 38.2% 1.22 1st (Most efficient)
Pull-Up 39.5% 33.1% 1.05 3rd
At Rim (reference) 67.2% - 1.34 1st (within shot types)

Key Efficiency Insights

  • Efficiency Gap: Catch-and-shoot attempts are ~16% more efficient (0.17 PPS advantage)
  • Volume Consideration: Despite lower efficiency, pull-ups represent 42% of all perimeter attempts
  • Three-Point Impact: CS 3P% is 5.1 percentage points higher than PU 3P%
  • Necessity Factor: Pull-ups are essential for late-clock situations and against set defenses
  • Context Matters: Elite shot creators can achieve CS-level efficiency on pull-ups

Efficiency by Shot Distance

Distance CS FG% PU FG% Efficiency Gap
Above the Break 3 (25-29 ft) 38.5% 33.8% +4.7%
Corner 3 (22-23 ft) 41.2% 36.5% +4.7%
Long Mid-Range (16-23 ft) 43.8% 40.2% +3.6%
Short Mid-Range (8-16 ft) 46.5% 43.8% +2.7%

Volume Patterns by Player Type

Player Type CS Freq PU Freq Sample Description
CS Specialists 65-75% 15-25% Rotation players, 3-and-D wings
Primary Ball-Handlers 15-30% 55-70% Lead guards, primary initiators
Versatile Stars 35-45% 45-55% All-Star level multi-dimensional scorers
Rim Runners 10-20% 5-15% Traditional bigs, lob threats

Strategic Implications

Offensive Strategy

1. Shot Quality Optimization

Principle: Maximize catch-and-shoot opportunities while maintaining pull-up capability for shot clock situations.

  • Ball Movement: Extra passes correlate with higher CS frequency (r = 0.67)
  • Pace Impact: Fast-break opportunities yield 78% CS rate vs 45% in half-court
  • Spacing Requirements: 5-out lineups increase CS opportunities by 22%
  • Motion Principles: Screen-the-screener actions generate elite CS looks

2. Roster Construction

Balance Requirements: Successful offenses blend shot creators with CS specialists.

  • Top Offenses (2023-24): Average 1.8 elite shot creators + 3.2 CS specialists
  • Playoff Necessity: At least one player with 50%+ PU frequency and >1.10 PPS
  • Depth Consideration: Bench units require at least 2 competent CS shooters
  • Positionless Basketball: Multiple ball-handlers enable DHO-heavy CS generation

3. Play Design

CS Generation Tactics:

  • Pin-Downs: Force help decisions, create CS threes (1.28 PPS average)
  • DHO Actions: Quick hitting, hard to help (1.24 PPS average)
  • Flare Screens: Punish aggressive help defense (1.31 PPS average)
  • Transition Leaks: Highest efficiency CS opportunities (1.42 PPS average)

4. Late-Game Execution

Shot Type Preferences by Situation:

  • Last 5 Minutes, Tied: Pull-ups increase to 58% (necessity over efficiency)
  • Three-Point Deficit: CS attempts increase by 31% (hunting efficiency)
  • One-Possession Game: Elite creators take 73% pull-ups (talent advantage)
  • Timeout Plays: 68% designed for CS opportunities

Defensive Strategy

1. Limiting Catch-and-Shoot Opportunities

  • Closeout Discipline: Contest without fouling (CS FT rate only 8%)
  • Help Recovery: Quick rotations to prevent open CS looks
  • Deny Principles: Force dribbles on catch to convert CS to pull-ups
  • Screen Navigation: Go over screens on elite CS shooters

2. Forcing Pull-Ups

  • No-Middle Coverage: Force baseline pull-ups (3.2% less efficient)
  • Drop Coverage: Concede long pull-up twos (0.92 PPS average)
  • Early Clock Pressure: Force pull-ups before set offense generates CS looks
  • Switching Strategy: Eliminate screen advantage for CS generation

3. Personnel Matchups

  • CS Specialists: Can play more conservative, focus on closeouts
  • Shot Creators: Require primary defensive attention, limit touches
  • Versatile Scorers: Demand elite individual defenders, no schematic solution
  • Switching Flexibility: Allows aggressive CS denial without screen advantage

Player Development Implications

Skill Progression Path

  1. Foundation (Years 1-2): Develop elite CS shooting (38%+ from three)
  2. Creation (Years 2-4): Add pull-up capability, expand shot diet
  3. Mastery (Years 4+): Elite efficiency in both modes, complete offensive weapon

Training Emphasis by Player Type

  • Wings/Forwards: 70% CS work, 30% pull-up (role player trajectory)
  • Combo Guards: 50% CS, 50% pull-up (versatility requirement)
  • Lead Guards: 30% CS, 70% pull-up (creation responsibility)
  • Traditional Bigs: 40% CS (short range), 60% rim finishing

Advanced Metrics and Concepts

Shot Quality (SQ) Score

Composite metric evaluating the difficulty and value of a player's shot distribution:

SQ Score = (CS_FREQ × 1.0) + (PU_FREQ × 0.75) + (Rim_FREQ × 1.15)

High SQ = Better shot quality (more efficient shot types)
Interpretation:
  > 0.95: Elite shot quality (optimal mix)
  0.85-0.95: Good shot quality
  0.75-0.85: Average shot quality
  < 0.75: Poor shot quality (too many difficult shots)

Creation Burden Index (CBI)

Measures how much offensive responsibility a player carries:

CBI = (PU_FREQ × Usage_Rate) + (Assists_Per_100 × 0.5)

High CBI = High creation burden
Elite shot creators: CBI > 30
Role players: CBI < 15

Shot Type Versatility Score

Measures a player's ability to score efficiently in multiple modes:

Versatility = min(CS_PPS, PU_PPS) × (1 - |CS_FREQ - PU_FREQ|)

Rewards: Efficiency in both modes + balanced distribution
Elite versatile scorers: > 0.50
Specialists: < 0.30

Defensive Stress Score (DSS)

Quantifies the defensive challenge a player presents:

DSS = (CS_PPS × CS_FGA) + (PU_PPS × PU_FGA × 1.2) + Gravity_Factor

Accounts for:
- Total offensive production
- Creation difficulty (PU weighted higher)
- Off-ball impact (spacing, gravity)

Elite offensive players: DSS > 20

Historical Context and Evolution

League-Wide Trends (2015-2024)

  • CS Frequency: Increased from 38% to 45% (+7 percentage points)
  • CS 3PA Rate: Increased from 72% to 89% (three-point revolution)
  • Pull-Up Mid-Range: Decreased from 28% to 16% (efficiency optimization)
  • Overall Efficiency: CS PPS increased from 1.14 to 1.22; PU from 0.98 to 1.05

Strategic Evolution

The modern NBA has undergone a dramatic shift in shot selection philosophy:

  • Early 2010s: Mid-range pull-ups accepted as necessary (Kobe, Dirk era)
  • Mid 2010s: Warriors demonstrate CS-heavy offense can win championships
  • Late 2010s: Rockets take efficiency optimization to extreme (heliocentric offense)
  • Early 2020s: League-wide adoption of spacing and ball movement principles
  • Current Era: Balance between creation and efficiency, versatility valued

Future Projections

  • Continued CS Growth: Expect 50%+ CS frequency as league average by 2026
  • Pull-Up Evolution: More step-back threes, fewer long twos
  • Skill Convergence: Big men increasingly required to shoot CS threes
  • Defensive Adaptation: Switching and versatility to combat CS generation

Conclusion

The distinction between catch-and-shoot and pull-up shooting represents a fundamental dimension of offensive basketball. While catch-and-shoot attempts offer superior efficiency, pull-up shooting remains essential for creating offense against set defenses and in late-clock situations.

Elite teams optimize shot distribution by:

  • Maximizing catch-and-shoot opportunities through ball movement and spacing
  • Employing versatile scorers who can excel in both modes
  • Designing plays that generate high-value CS looks
  • Maintaining shot creation capability for critical situations

Modern basketball analytics has revealed that the most successful offensive players aren't necessarily those who take the most pull-ups, but rather those who efficiently blend both shot types while understanding situational requirements. The continued evolution of the game will likely see further optimization of this balance, with player development increasingly focused on creating versatile scorers capable of elite efficiency in multiple modes.

Key Takeaways

  1. Catch-and-shoot attempts are ~16% more efficient than pull-ups league-wide
  2. Elite offenses generate 45-50% of attempts as catch-and-shoot
  3. Pull-up shooting remains essential for creating against set defenses
  4. Versatile scorers who excel in both modes provide maximum offensive value
  5. Shot type optimization should consider situation, personnel, and defensive coverage
  6. Player development should prioritize CS proficiency before adding creation skills
  7. Defensive strategy must balance preventing efficient CS looks while not over-helping

References and Data Sources

  • NBA Advanced Stats - stats.nba.com
  • Second Spectrum Tracking Data
  • Basketball Reference - basketball-reference.com
  • Cleaning the Glass - cleaningtheglass.com
  • nba_api Python Package - github.com/swar/nba_api
  • hoopR R Package - hoopr.sportsdataverse.org

Discussion

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