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)