Chapter 26 Intermediate ~45 min read

Mid-Range Efficiency

Examining the declining but still relevant mid-range game and when two-point jumpers make analytical sense.

The Analytical Case Against Mid-Range

Basic expected value arithmetic makes the case against mid-range shots compelling. A league-average mid-range shot (roughly 40% from 10-22 feet) produces 0.80 expected points per attempt. A league-average three-pointer (roughly 36%) produces 1.08 expected points. A league-average rim attempt (roughly 62%) produces 1.24 expected points.

The shift has been dramatic. In 2000, mid-range shots constituted roughly 40% of all field goal attempts. By 2020, that figure had fallen below 20%.

When Mid-Range Makes Sense

Shot clock desperation represents the most obvious case. With the shot clock expiring and no rim or three-point opportunity available, a makeable mid-range shot beats a turnover.

Elite mid-range shooters change the expected value calculation. While league-average mid-range efficiency is poor, shooters like Kevin Durant or Chris Paul consistently exceed 50% from mid-range. At 50%, a two-point jumper produces 1.00 expected points—competitive with average three-point shooting.

Playoff basketball often compresses offensive options as defenses in elimination games pack the paint and contest threes more aggressively.

def analyze_mid_range_value(shot_data):
    """Analyze when mid-range shots provide value"""
    mid_range = shot_data[
        (shot_data['SHOT_DISTANCE'] >= 10) &
        (shot_data['SHOT_DISTANCE'] < 24) &
        (shot_data['SHOT_TYPE'] != '3PT Field Goal')
    ].copy()

    player_midrange = mid_range.groupby('PLAYER_NAME').agg({
        'SHOT_MADE_FLAG': ['sum', 'count', 'mean']
    })
    player_midrange.columns = ['Makes', 'Attempts', 'MR_PCT']
    player_midrange['Expected_PTS'] = player_midrange['MR_PCT'] * 2

    # Identify efficient mid-range shooters
    efficient_shooters = player_midrange[player_midrange['Expected_PTS'] >= 1.0]

    return efficient_shooters

The Elite Mid-Range Artists

Kevin Durant's combination of height, shooting stroke, and shot creation makes contested mid-range jumpers high-percentage for him. DeMar DeRozan has built an entire offensive identity around mid-range mastery. Chris Paul's mid-range game reflects shot selection, timing, and creating quality looks.

Team Strategy Implications

For most players, reducing mid-range attempts remains good strategy. However, maintaining a credible mid-range threat preserves offensive versatility. Playoff roster construction may benefit from mid-range capability when standard approaches are shut down.

Implementation in R

# Calculate defensive impact from tracking
library(tidyverse)

calculate_defensive_metrics <- function(defensive_data) {
  defensive_data %>%
    group_by(defender_id, defender_name) %>%
    summarise(
      # Shots defended
      shots_defended = n(),

      # Opponent shooting
      opp_fg_pct = mean(shot_made),
      opp_xfg_pct = mean(expected_fg_pct),
      dfg_diff = opp_fg_pct - opp_xfg_pct,

      # By zone
      rim_shots_defended = sum(shot_zone == "rim"),
      rim_dfg_pct = mean(shot_made[shot_zone == "rim"]),
      three_defended = sum(shot_zone == "three"),
      three_dfg_pct = mean(shot_made[shot_zone == "three"]),

      # Contest quality
      avg_contest_dist = mean(contest_distance),
      tight_contest_rate = mean(contest_distance < 2),

      .groups = "drop"
    )
}

defensive_data <- read_csv("defensive_tracking.csv")
defensive_metrics <- calculate_defensive_metrics(defensive_data)

# Best rim protectors
rim_protectors <- defensive_metrics %>%
  filter(rim_shots_defended >= 100) %>%
  arrange(rim_dfg_pct) %>%
  select(defender_name, rim_shots_defended, rim_dfg_pct, avg_contest_dist) %>%
  head(15)

print(rim_protectors)
# Matchup tracking analysis
library(tidyverse)

analyze_matchups <- function(matchup_data) {
  matchup_data %>%
    group_by(defender_name, offensive_player_name) %>%
    summarise(
      matchup_poss = n(),
      pts_allowed = sum(points),
      pts_per_poss = round(pts_allowed / matchup_poss, 2),
      fg_pct_allowed = round(mean(shot_made[shot_attempt == 1]), 3),
      .groups = "drop"
    ) %>%
    filter(matchup_poss >= 20)
}

matchups <- read_csv("matchup_tracking.csv")
matchup_analysis <- analyze_matchups(matchups)

# Find specific defender-offensive player matchups
best_defenders <- matchup_analysis %>%
  group_by(defender_name) %>%
  summarise(
    matchups = n(),
    avg_pts_per_poss = mean(pts_per_poss),
    .groups = "drop"
  ) %>%
  filter(matchups >= 10) %>%
  arrange(avg_pts_per_poss)

print(head(best_defenders, 15))

Implementation in R

# Calculate defensive impact from tracking
library(tidyverse)

calculate_defensive_metrics <- function(defensive_data) {
  defensive_data %>%
    group_by(defender_id, defender_name) %>%
    summarise(
      # Shots defended
      shots_defended = n(),

      # Opponent shooting
      opp_fg_pct = mean(shot_made),
      opp_xfg_pct = mean(expected_fg_pct),
      dfg_diff = opp_fg_pct - opp_xfg_pct,

      # By zone
      rim_shots_defended = sum(shot_zone == "rim"),
      rim_dfg_pct = mean(shot_made[shot_zone == "rim"]),
      three_defended = sum(shot_zone == "three"),
      three_dfg_pct = mean(shot_made[shot_zone == "three"]),

      # Contest quality
      avg_contest_dist = mean(contest_distance),
      tight_contest_rate = mean(contest_distance < 2),

      .groups = "drop"
    )
}

defensive_data <- read_csv("defensive_tracking.csv")
defensive_metrics <- calculate_defensive_metrics(defensive_data)

# Best rim protectors
rim_protectors <- defensive_metrics %>%
  filter(rim_shots_defended >= 100) %>%
  arrange(rim_dfg_pct) %>%
  select(defender_name, rim_shots_defended, rim_dfg_pct, avg_contest_dist) %>%
  head(15)

print(rim_protectors)
# Matchup tracking analysis
library(tidyverse)

analyze_matchups <- function(matchup_data) {
  matchup_data %>%
    group_by(defender_name, offensive_player_name) %>%
    summarise(
      matchup_poss = n(),
      pts_allowed = sum(points),
      pts_per_poss = round(pts_allowed / matchup_poss, 2),
      fg_pct_allowed = round(mean(shot_made[shot_attempt == 1]), 3),
      .groups = "drop"
    ) %>%
    filter(matchup_poss >= 20)
}

matchups <- read_csv("matchup_tracking.csv")
matchup_analysis <- analyze_matchups(matchups)

# Find specific defender-offensive player matchups
best_defenders <- matchup_analysis %>%
  group_by(defender_name) %>%
  summarise(
    matchups = n(),
    avg_pts_per_poss = mean(pts_per_poss),
    .groups = "drop"
  ) %>%
  filter(matchups >= 10) %>%
  arrange(avg_pts_per_poss)

print(head(best_defenders, 15))
Chapter Summary

You've completed Chapter 26: Mid-Range Efficiency.