Chapter 28 Intermediate ~55 min read

Shot Creation and Self-Creation Metrics

Measuring the ability to create scoring opportunities for oneself and teammates using tracking data.

The Value of Shot Creation

Shot creation represents perhaps the most valuable offensive skill in basketball—the ability to generate quality scoring opportunities against set defenses. Players who can create shots don't just score; they unlock offenses by forcing defensive reactions that open opportunities for teammates.

Defining Shot Creation

Self-creation refers to a player generating their own shot without direct assistance from teammates—beating a defender off the dribble, using size advantage in the post, or creating separation through moves like step-backs. Assist creation means generating shots for teammates through passing. Gravity creates opportunities indirectly through the attention defenders pay to a shooter.

def analyze_shot_creation(tracking_data, shot_data):
    """Measure shot creation from tracking data"""
    shot_with_context = shot_data.merge(tracking_data, on=['GAME_ID', 'EVENTNUM'])

    shot_with_context['SELF_CREATED'] = (
        (shot_with_context['DRIBBLES'] >= 3) |
        (shot_with_context['TOUCH_TIME'] >= 3.0)
    )

    player_creation = shot_with_context.groupby('PLAYER_NAME').agg({
        'SELF_CREATED': 'sum',
        'SHOT_MADE_FLAG': ['sum', 'count']
    })

    player_creation['Self_Creation_Rate'] = (
        player_creation[('SELF_CREATED', 'sum')] /
        player_creation[('SHOT_MADE_FLAG', 'count')]
    )

    return player_creation

Self-Created Shot Efficiency

Evaluating self-creation requires comparing efficiency on self-created shots versus assisted opportunities. Self-created shots are inherently more difficult. Elite self-creators like Kevin Durant maintain efficiency on self-created looks that would be excellent even for assisted shots.

Playmaking and Assist Creation

Potential assists—passes that lead to shot attempts regardless of outcome—better measure creation ability because they remove shooter variance. Shot quality of created opportunities provides another dimension. Secondary assists credit passes that start offensive sequences.

Gravity and Indirect Creation

The most challenging creation value to measure comes from players who command defensive attention without the ball. Stephen Curry's movement demands closeouts that open driving lanes for teammates. This "gravity" is real offensive value that doesn't appear in counting statistics.

Creation and Turnover Tradeoffs

Shot creation inevitably comes with turnover risk. Players who handle the ball more will turn it over more often. Elite creators often have better turnover rates relative to their creation volume. Full evaluation of shot creation requires weighing self-creation, playmaking, and turnovers together.

Implementation in R

# Working with Second Spectrum-style tracking data
library(tidyverse)

process_optical_tracking <- function(tracking_json) {
  # Parse advanced optical tracking features
  tracking_json %>%
    mutate(
      # Player pose detection
      body_orientation = atan2(shoulder_y - hip_y, shoulder_x - hip_x),

      # Ball screen detection
      screen_set = lead(player_id) != player_id &
                   lead(distance_to_ballhandler) < 3,

      # Off-ball movement classification
      movement_type = case_when(
        speed > 12 & direction_change > 90 ~ "cut",
        speed > 8 & abs(direction_change) < 30 ~ "sprint",
        speed < 3 & near_screen_location ~ "screen",
        TRUE ~ "general"
      )
    )
}

# Analyze off-ball movement quality
analyze_off_ball_movement <- function(tracking_data) {
  tracking_data %>%
    filter(has_ball == FALSE) %>%
    group_by(player_id, player_name) %>%
    summarise(
      total_frames = n(),
      cuts = sum(movement_type == "cut"),
      sprints = sum(movement_type == "sprint"),
      screens_set = sum(movement_type == "screen"),

      # Activity rate
      active_pct = round((cuts + sprints) / total_frames * 100, 1),

      # Value created
      gravity_possessions = sum(drew_help_defender),
      open_shots_created = sum(created_open_shot_for_teammate),

      .groups = "drop"
    )
}

optical_data <- read_csv("optical_tracking.csv")
off_ball <- analyze_off_ball_movement(optical_data)

# Best off-ball movers
best_movers <- off_ball %>%
  filter(total_frames >= 10000) %>%
  arrange(desc(active_pct)) %>%
  select(player_name, active_pct, cuts, screens_set, open_shots_created) %>%
  head(15)

print(best_movers)
# Analyze spacing and gravity effects
library(tidyverse)

calculate_gravity_metrics <- function(possession_data) {
  possession_data %>%
    group_by(player_id, player_name) %>%
    summarise(
      possessions = n(),

      # Spacing value
      avg_defender_pulled_distance = mean(defender_distance_from_rim),
      space_created_for_teammates = mean(teammate_closest_defender_dist),

      # Attention drawn
      double_team_rate = mean(double_teamed) * 100,
      help_defender_rate = mean(drew_help) * 100,

      # Impact on teammates
      teammate_efg_when_on = mean(teammate_efg_on_court),
      teammate_rim_rate_when_on = mean(teammate_rim_attempt_rate),

      .groups = "drop"
    )
}

possession_tracking <- read_csv("possession_gravity.csv")
gravity <- calculate_gravity_metrics(possession_tracking)

# Highest gravity players
high_gravity <- gravity %>%
  filter(possessions >= 500) %>%
  arrange(desc(space_created_for_teammates)) %>%
  head(15)

print(high_gravity)

Implementation in R

# Working with Second Spectrum-style tracking data
library(tidyverse)

process_optical_tracking <- function(tracking_json) {
  # Parse advanced optical tracking features
  tracking_json %>%
    mutate(
      # Player pose detection
      body_orientation = atan2(shoulder_y - hip_y, shoulder_x - hip_x),

      # Ball screen detection
      screen_set = lead(player_id) != player_id &
                   lead(distance_to_ballhandler) < 3,

      # Off-ball movement classification
      movement_type = case_when(
        speed > 12 & direction_change > 90 ~ "cut",
        speed > 8 & abs(direction_change) < 30 ~ "sprint",
        speed < 3 & near_screen_location ~ "screen",
        TRUE ~ "general"
      )
    )
}

# Analyze off-ball movement quality
analyze_off_ball_movement <- function(tracking_data) {
  tracking_data %>%
    filter(has_ball == FALSE) %>%
    group_by(player_id, player_name) %>%
    summarise(
      total_frames = n(),
      cuts = sum(movement_type == "cut"),
      sprints = sum(movement_type == "sprint"),
      screens_set = sum(movement_type == "screen"),

      # Activity rate
      active_pct = round((cuts + sprints) / total_frames * 100, 1),

      # Value created
      gravity_possessions = sum(drew_help_defender),
      open_shots_created = sum(created_open_shot_for_teammate),

      .groups = "drop"
    )
}

optical_data <- read_csv("optical_tracking.csv")
off_ball <- analyze_off_ball_movement(optical_data)

# Best off-ball movers
best_movers <- off_ball %>%
  filter(total_frames >= 10000) %>%
  arrange(desc(active_pct)) %>%
  select(player_name, active_pct, cuts, screens_set, open_shots_created) %>%
  head(15)

print(best_movers)
# Analyze spacing and gravity effects
library(tidyverse)

calculate_gravity_metrics <- function(possession_data) {
  possession_data %>%
    group_by(player_id, player_name) %>%
    summarise(
      possessions = n(),

      # Spacing value
      avg_defender_pulled_distance = mean(defender_distance_from_rim),
      space_created_for_teammates = mean(teammate_closest_defender_dist),

      # Attention drawn
      double_team_rate = mean(double_teamed) * 100,
      help_defender_rate = mean(drew_help) * 100,

      # Impact on teammates
      teammate_efg_when_on = mean(teammate_efg_on_court),
      teammate_rim_rate_when_on = mean(teammate_rim_attempt_rate),

      .groups = "drop"
    )
}

possession_tracking <- read_csv("possession_gravity.csv")
gravity <- calculate_gravity_metrics(possession_tracking)

# Highest gravity players
high_gravity <- gravity %>%
  filter(possessions >= 500) %>%
  arrange(desc(space_created_for_teammates)) %>%
  head(15)

print(high_gravity)
Chapter Summary

You've completed Chapter 28: Shot Creation and Self-Creation Metrics.