Chapter 27 Intermediate ~45 min read

Free Throw Analytics

Analyzing free throw shooting, getting to the line, and the role of free throws in offensive efficiency.

The Value of Free Throws

Free throws represent the most efficient shot in basketball on a per-attempt basis, with league-average conversion around 78%. Yet free throw analysis extends far beyond conversion rate—understanding who gets to the line, how often, and in what contexts reveals insights about offensive value and foul drawing skill.

Free Throw Rate and Drawing Fouls

Free throw rate (FTR)—free throw attempts per field goal attempt—measures how often players get to the line relative to their shooting volume. The league average FTR hovers around 0.25. Getting to the line provides multiple benefits beyond the free points themselves: foul trouble for opponents, bonus situations, and-one opportunities.

Players who draw fouls at elite rates provide substantial offensive value. James Harden at his peak generated over 10 free throw attempts per game while maintaining elite FTR.

def analyze_free_throw_value(player_data):
    """Comprehensive free throw analysis"""
    player_data['FTR'] = player_data['FTA'] / player_data['FGA']
    player_data['TSA'] = player_data['FGA'] + 0.44 * player_data['FTA']
    player_data['TS_FROM_FT'] = player_data['FTM'] / (2 * player_data['TSA'])

    player_ft_analysis = player_data.groupby('PLAYER_NAME').agg({
        'FTA': 'sum',
        'FTM': 'sum',
        'FGA': 'sum',
        'FTR': 'mean'
    })

    player_ft_analysis['FT_PCT'] = player_ft_analysis['FTM'] / player_ft_analysis['FTA']

    return player_ft_analysis.sort_values('FTR', ascending=False)

Free Throw Conversion Analysis

While 78% represents league average, the distribution of free throw percentages is notably narrow compared to field goal shooting. Most players fall between 70% and 85%. Free throw percentage is one of the most stable statistics, requiring fewer attempts to reliably estimate true talent. Free throw shooting is also highly predictive of three-point shooting ability.

Strategic Fouling Decisions

Intentional fouling represents one of basketball's most analytically tractable strategic decisions. The Hack-a-Shaq decision has a clear framework: if a player's expected points from free throws (FT% Ă— 2) is less than the opponent's expected points from a normal possession, fouling is beneficial.

Free Throw Value in Player Evaluation

True Shooting percentage's inclusion of free throws represents the standard approach to crediting free throw value. For player evaluation, free throw analysis provides multiple insights: shooting ability (from FT%), aggression and craftiness (from FTR), and reliability in pressure situations.

Implementation in R

# Advanced rebounding analytics with tracking
library(tidyverse)

calculate_rebounding_opportunities <- function(rebound_data) {
  rebound_data %>%
    group_by(player_id, player_name) %>%
    summarise(
      # Opportunity-based metrics
      rebound_chances = n(),
      rebounds_secured = sum(secured_rebound),
      contested_rebounds = sum(contested & secured_rebound),

      # Rates
      rebound_chance_pct = round(rebounds_secured / rebound_chances * 100, 1),
      contested_win_pct = round(contested_rebounds / sum(contested) * 100, 1),

      # Positioning metrics
      avg_distance_to_ball = mean(distance_to_ball_at_miss),
      avg_box_out_time = mean(box_out_time, na.rm = TRUE),

      .groups = "drop"
    )
}

rebound_tracking <- read_csv("rebound_opportunities.csv")
rebound_metrics <- calculate_rebounding_opportunities(rebound_tracking)

# Best contested rebounders
contested_rebounders <- rebound_metrics %>%
  filter(rebound_chances >= 200) %>%
  arrange(desc(contested_win_pct)) %>%
  select(player_name, rebound_chances, contested_win_pct,
         avg_distance_to_ball) %>%
  head(15)

print(contested_rebounders)
# Box out effectiveness analysis
library(tidyverse)

analyze_box_out_effectiveness <- function(box_out_data) {
  box_out_data %>%
    group_by(player_id, player_name) %>%
    summarise(
      total_box_outs = n(),
      successful_box_outs = sum(opponent_missed_rebound),
      box_out_success_rate = round(successful_box_outs / total_box_outs * 100, 1),

      # Team benefit
      teammate_rebounds_after_boxout = sum(teammate_secured),
      team_rebound_rate = round(teammate_rebounds_after_boxout /
                                 total_box_outs * 100, 1),

      .groups = "drop"
    )
}

box_outs <- read_csv("box_out_tracking.csv")
box_out_analysis <- analyze_box_out_effectiveness(box_outs)

# Top box out players
top_box_out <- box_out_analysis %>%
  filter(total_box_outs >= 100) %>%
  arrange(desc(box_out_success_rate)) %>%
  head(15)

print(top_box_out)

Implementation in R

# Advanced rebounding analytics with tracking
library(tidyverse)

calculate_rebounding_opportunities <- function(rebound_data) {
  rebound_data %>%
    group_by(player_id, player_name) %>%
    summarise(
      # Opportunity-based metrics
      rebound_chances = n(),
      rebounds_secured = sum(secured_rebound),
      contested_rebounds = sum(contested & secured_rebound),

      # Rates
      rebound_chance_pct = round(rebounds_secured / rebound_chances * 100, 1),
      contested_win_pct = round(contested_rebounds / sum(contested) * 100, 1),

      # Positioning metrics
      avg_distance_to_ball = mean(distance_to_ball_at_miss),
      avg_box_out_time = mean(box_out_time, na.rm = TRUE),

      .groups = "drop"
    )
}

rebound_tracking <- read_csv("rebound_opportunities.csv")
rebound_metrics <- calculate_rebounding_opportunities(rebound_tracking)

# Best contested rebounders
contested_rebounders <- rebound_metrics %>%
  filter(rebound_chances >= 200) %>%
  arrange(desc(contested_win_pct)) %>%
  select(player_name, rebound_chances, contested_win_pct,
         avg_distance_to_ball) %>%
  head(15)

print(contested_rebounders)
# Box out effectiveness analysis
library(tidyverse)

analyze_box_out_effectiveness <- function(box_out_data) {
  box_out_data %>%
    group_by(player_id, player_name) %>%
    summarise(
      total_box_outs = n(),
      successful_box_outs = sum(opponent_missed_rebound),
      box_out_success_rate = round(successful_box_outs / total_box_outs * 100, 1),

      # Team benefit
      teammate_rebounds_after_boxout = sum(teammate_secured),
      team_rebound_rate = round(teammate_rebounds_after_boxout /
                                 total_box_outs * 100, 1),

      .groups = "drop"
    )
}

box_outs <- read_csv("box_out_tracking.csv")
box_out_analysis <- analyze_box_out_effectiveness(box_outs)

# Top box out players
top_box_out <- box_out_analysis %>%
  filter(total_box_outs >= 100) %>%
  arrange(desc(box_out_success_rate)) %>%
  head(15)

print(top_box_out)
Chapter Summary

You've completed Chapter 27: Free Throw Analytics.