The Three-Point Revolution

Beginner 10 min read 1 views Nov 27, 2025
# The Three-Point Revolution ## Introduction The three-point revolution represents one of the most dramatic strategic shifts in basketball history. Over the past two decades, the NBA has transformed from a game dominated by mid-range jumpers and post play to one where three-point shooting is the cornerstone of offensive strategy. This revolution, driven by analytics and pioneered by teams like the Houston Rockets and Golden State Warriors, has fundamentally changed how basketball is played at every level. ## History of the Three-Point Shot ### Origins and Adoption The three-point line has a fascinating history that predates the NBA: **1961**: The American Basketball League (ABL) becomes the first professional league to introduce the three-point shot - Line set at 25 feet from the basket - League folded after just one and a half seasons **1967**: The American Basketball Association (ABA) adopts the three-point line - Set at 25 feet from the top of the key - Became a signature feature differentiating ABA from NBA - Players like Louie Dampier became prolific three-point shooters **1979**: The NBA finally adopts the three-point line - Initially met with skepticism from traditionalists - Set at 23 feet, 9 inches from the basket (22 feet in corners) - First three-pointer made by Chris Ford (Boston Celtics) on October 12, 1979 ### Early NBA Era (1979-1994) In the first 15 years, the three-pointer was underutilized: - **Low Volume**: Teams averaged only 2-3 three-point attempts per game - **Specialist Role**: Only designated shooters attempted threes - **Strategic Limitation**: Used primarily as a bailout option or desperation shot - **Cultural Resistance**: Coaches valued "fundamental" mid-range and post play **Notable Early Adopters**: - Larry Bird: One of the first superstars to embrace the three - Dale Ellis: Led league with 162 three-pointers in 1988-89 - Reggie Miller: Pioneered off-ball movement for three-point opportunities ### The Short Line Era (1994-1997) **1994**: NBA experiments with shortened three-point line - Moved to uniform 22 feet from basket - Led to spike in three-point attempts and percentages - Three-point attempts jumped from 9.9 to 15.3 per game **1997**: Line restored to original distance - 23 feet, 9 inches (22 feet in corners) - Three-point volume initially declined but soon resumed upward trajectory ### The Modern Era (2000-Present) **Early 2000s**: Analytics movement begins - Dean Oliver's "Basketball on Paper" (2004) emphasizes efficiency - Introduction of effective field goal percentage (eFG%) - Growing recognition of three-point value **2007-2012**: The Seven Seconds or Less Suns - Mike D'Antoni's Phoenix Suns prioritize pace and threes - Steve Nash orchestrates high-volume three-point offense - Demonstrated viability of three-point-heavy strategy **2012-Present**: Full Revolution - Houston Rockets under Daryl Morey embrace "Moreyball" - Golden State Warriors dynasty built on three-point shooting - League-wide adoption of three-point-centric offenses ## The Analytics Behind the Revolution ### Expected Points Per Shot The mathematical foundation of the three-point revolution is simple: **expected value**. **Basic Math**: ``` Expected Points = Field Goal % × Points per Make 2-Point Shot: 0.50 FG% × 2 points = 1.00 expected points 3-Point Shot: 0.35 FG% × 3 points = 1.05 expected points ``` **Key Insight**: A 35% three-point shooter produces more expected value than a 50% two-point shooter. ### Effective Field Goal Percentage (eFG%) eFG% adjusts field goal percentage to account for the extra value of three-pointers: ``` eFG% = (FGM + 0.5 × 3PM) / FGA ``` **Example**: - Player A: 10/20 on 2-pointers = 50% FG, 50% eFG% - Player B: 7/20 on 3-pointers = 35% FG, 52.5% eFG% Despite lower FG%, Player B is more efficient. ### True Shooting Percentage (TS%) TS% goes further by including free throws: ``` TS% = Points / (2 × (FGA + 0.44 × FTA)) ``` This provides the most complete picture of scoring efficiency. ### Break-Even Analysis **What three-point percentage equals a two-point percentage?** For a two-point shot at X% accuracy: ``` Required 3P% = (X% × 2) / 3 ``` Examples: - 45% on 2-pointers = 30% on 3-pointers (equal value) - 50% on 2-pointers = 33.3% on 3-pointers - 60% on 2-pointers = 40% on 3-pointers **League Context (2023-24)**: - League average 2P%: 54.9% - League average 3P%: 36.6% - Break-even 3P% for league average 2P%: 36.6% - Actual 3P% equals break-even, but this doesn't account for shot location ### Shot Quality and Location Not all two-pointers are equal: **At-Rim Shots**: ~65% FG (1.30 expected points) **Mid-Range Shots**: ~40% FG (0.80 expected points) **Three-Pointers**: ~36% FG (1.08 expected points) The key insight: Three-pointers are far more valuable than mid-range shots. ## Expected Points Per Shot by Location ### Shot Distribution Analysis Modern analytics divides the court into efficiency zones: **1. Restricted Area (0-3 feet)** - FG%: ~65% - Expected Points: 1.30 - Verdict: **Excellent shot** **2. Paint (Non-Restricted, 3-8 feet)** - FG%: ~45% - Expected Points: 0.90 - Verdict: **Acceptable but declining** **3. Short Mid-Range (8-16 feet)** - FG%: ~40% - Expected Points: 0.80 - Verdict: **Inefficient** **4. Long Mid-Range (16-23 feet)** - FG%: ~38% - Expected Points: 0.76 - Verdict: **Highly inefficient** **5. Corner Three (22 feet)** - FG%: ~38% - Expected Points: 1.14 - Verdict: **Excellent shot** **6. Above-the-Break Three (23.75 feet)** - FG%: ~35% - Expected Points: 1.05 - Verdict: **Good shot** ### The Efficiency Hierarchy ``` 1. Restricted Area (1.30 PPP) 2. Corner Three (1.14 PPP) 3. Above-Break Three (1.05 PPP) 4. Paint Non-Restricted (0.90 PPP) 5. Short Mid-Range (0.80 PPP) 6. Long Mid-Range (0.76 PPP) ``` **Strategic Implication**: Maximize attempts from zones 1-3, minimize from zones 5-6. ### Shot Distribution Evolution **2000-01 Season**: - At-Rim: 26.4% of FGA - Mid-Range: 41.2% of FGA - Three-Point: 14.7% of FGA **2023-24 Season**: - At-Rim: 32.8% of FGA - Mid-Range: 14.1% of FGA - Three-Point: 39.2% of FGA **Change**: Mid-range attempts cut by nearly two-thirds, replaced by threes. ## The Death of the Mid-Range ### Why the Mid-Range Died The mid-range shot has been systematically eliminated from modern NBA offenses: **1. Mathematical Inefficiency** - Lowest expected points per shot of any zone - No compensation for difficulty (unlike three-pointer) - Risk-reward ratio heavily unfavorable **2. Defensive Adaptation** - Defenses concede mid-range to protect rim and three-point line - "Give them the mid-range" became defensive philosophy - Less contested mid-range shots, but still inefficient **3. Skill Development** - Young players trained to shoot threes, not mid-range - AAU and college basketball follow NBA trends - Mid-range skills atrophying across player pool **4. Pace and Space** - Mid-range shots clog driving lanes - Three-point spacing creates more driving opportunities - Spacing benefits entire offense, not just shooters ### Mid-Range Holdouts Despite the trend, some players remain elite mid-range shooters: **DeMar DeRozan**: - 9.1 mid-range attempts per game (2023-24) - 47.5% from mid-range - Expected Points: 0.95 (still below corner three) **Kevin Durant**: - Elite efficiency across all zones - 48.5% from mid-range - Expected Points: 0.97 **Chris Paul**: - Mastery of mid-range pull-up - Uses mid-range to set up other actions - 46.8% from mid-range **The Exception**: Even these elite mid-range shooters would theoretically gain efficiency by shooting threes instead, but their mid-range mastery provides: - Late-clock bailout options - Mismatch exploitation - Defensive attention that opens teammates ### The Post-Up Decline Post-ups have suffered a similar fate: **Reasons**: - Lower efficiency than threes (~0.90 PPP) - Requires spacing-killing positioning - Limited to specific personnel - Slow pace, reduces possessions **Modern Post Play**: Reserved for mismatches and specific situations, no longer foundational offense. ## Moreyball: The Analytics Philosophy ### Daryl Morey and the Houston Rockets **Daryl Morey** (Rockets GM 2007-2020) pioneered the analytics-driven approach dubbed "Moreyball": **Core Principles**: 1. **Shot Selection Discipline** - Maximize threes and layups/dunks - Eliminate mid-range shots - "Shots at the rim or from three" 2. **Volume Over Precision** - More total three-point attempts matters more than slight efficiency gains - Law of large numbers favors high-volume three-point shooting 3. **Personnel Optimization** - Acquire shooters and playmakers, not mid-range specialists - Position-less basketball with interchangeable shooters - "Switching everything" defensively 4. **Pace Maximization** - More possessions = more opportunities for efficient shots - Fast breaks prioritize threes or layups ### The James Harden Era (2012-2020) Moreyball reached its apex with James Harden: **2018-19 Season** (Peak Moreyball): - Rockets attempted 45.3 three-pointers per game (NBA record at time) - 13.4% of shots from mid-range (NBA lowest) - 42.0% from three-point range - Led NBA in offensive rating (114.6) **2019-20 "Small Ball" Experiment**: - Traded center Clint Capela mid-season - Played 6'5" P.J. Tucker at center - Attempted 45.7 threes per game - Surrounded Harden and Westbrook with pure shooters **Results**: - Regular season success (44-28) - Playoff limitations exposed (lost to Lakers in second round) - Showed limits of extreme Moreyball without elite rim protection ### Criticisms of Moreyball **1. Playoff Variance** - High three-point volume creates variance - One cold shooting night ends playoff series - 2018 WCF Game 7: Rockets missed 27 consecutive threes vs Warriors **2. Aesthetic Concerns** - Repetitive offense (dribble-drive, kick-out three) - Reduced shot variety and creativity - Fan experience diminished **3. Personnel Limitations** - Requires specific player types - Difficult to build via draft - Expensive free agent market for shooters **4. Defensive Adaptation** - Advanced defenses adapted to limit corner threes - Switch-heavy schemes neutralize isolation - Playoff-level defense exposes predictability ### Moreyball's Legacy Despite criticisms, Moreyball fundamentally changed basketball: - Every team adopted elements (higher three-point volume, mid-range reduction) - Demonstrated mathematical optimization could compete with traditional approaches - Influenced player development, coaching philosophy, and front office construction - Forced defensive innovation to counter three-point volume ## League-Wide Trends Over Time ### Three-Point Attempt Volume **Historical Progression** (Team Average Per Game): | Season | 3PA/G | % of FGA | 3P% | |-----------|-------|----------|------| | 1979-80 | 2.8 | 3.1% | 28.0%| | 1989-90 | 6.6 | 7.4% | 33.1%| | 1999-00 | 13.7 | 16.0% | 35.4%| | 2009-10 | 18.1 | 22.2% | 35.6%| | 2014-15 | 22.4 | 26.7% | 35.0%| | 2019-20 | 34.1 | 38.5% | 35.8%| | 2023-24 | 35.2 | 39.2% | 36.6%| **Key Milestones**: - **2013-14**: First season where teams averaged 20+ 3PA/G - **2016-17**: Three-point attempts surpass mid-range for first time - **2018-19**: Teams average 30+ 3PA/G - **2023-24**: Nearly 40% of all shots are threes ### Team-Level Extremes **Highest Three-Point Volume** (Single Season): 1. **2018-19 Houston Rockets**: 45.3 3PA/G 2. **2019-20 Houston Rockets**: 45.7 3PA/G 3. **2020-21 Utah Jazz**: 42.5 3PA/G 4. **2023-24 Boston Celtics**: 42.5 3PA/G 5. **2021-22 Charlotte Hornets**: 41.5 3PA/G **Lowest Three-Point Volume** (Recent Seasons, 2015-present): 1. **2015-16 San Antonio Spurs**: 20.3 3PA/G (still won 67 games) 2. **2016-17 Memphis Grizzlies**: 20.5 3PA/G 3. **2017-18 Memphis Grizzlies**: 22.1 3PA/G ### Positional Evolution **Point Guards**: - 2000: 1.2 3PA/G, 32.1% 3P% - 2024: 5.8 3PA/G, 35.9% 3P% **Shooting Guards**: - 2000: 2.1 3PA/G, 35.2% 3P% - 2024: 6.4 3PA/G, 36.8% 3P% **Small Forwards**: - 2000: 1.6 3PA/G, 33.8% 3P% - 2024: 4.9 3PA/G, 35.7% 3P% **Power Forwards**: - 2000: 0.5 3PA/G, 30.1% 3P% - 2024: 3.7 3PA/G, 35.2% 3P% **Centers**: - 2000: 0.1 3PA/G, 21.4% 3P% - 2024: 2.1 3PA/G, 34.8% 3P% **Biggest Change**: Centers transformed from non-shooters to capable three-point threats. ### The Pace Factor Three-point revolution coincided with increased pace: | Season | Pace (Possessions/48 min) | |-----------|---------------------------| | 1999-00 | 90.8 | | 2009-10 | 92.7 | | 2019-20 | 100.3 | | 2023-24 | 99.1 | **Correlation**: Faster pace enables more three-point attempts and better transition threes. ## Visualizing the Three-Point Evolution ### Python Code for Analysis ```python import pandas as pd import numpy as np import matplotlib.pyplot as plt import seaborn as sns from nba_api.stats.endpoints import leaguedashteamstats, leaguegamelog from nba_api.stats.static import teams # Set style plt.style.use('seaborn-v0_8-darkgrid') sns.set_palette("husl") # 1. Historical Three-Point Trends def fetch_league_shooting_trends(start_year=2000, end_year=2024): """ Fetch league-wide shooting trends over time. Parameters: ----------- start_year : int Starting season (e.g., 2000 for 1999-00 season) end_year : int Ending season Returns: -------- DataFrame with yearly shooting statistics """ seasons_data = [] for year in range(start_year, end_year + 1): season_str = f"{year-1}-{str(year)[2:]}" try: # Fetch team stats for the season team_stats = leaguedashteamstats.LeagueDashTeamStats( season=season_str, season_type_all_star='Regular Season', per_mode_detailed='PerGame' ) df = team_stats.get_data_frames()[0] # Calculate league averages season_avg = { 'Season': season_str, 'Year': year, 'FGA': df['FGA'].mean(), 'FGM': df['FGM'].mean(), 'FG_PCT': df['FG_PCT'].mean(), 'FG3A': df['FG3A'].mean(), 'FG3M': df['FG3M'].mean(), 'FG3_PCT': df['FG3_PCT'].mean(), 'FG2A': df['FGA'].mean() - df['FG3A'].mean(), 'FG2M': df['FGM'].mean() - df['FG3M'].mean(), 'PTS': df['PTS'].mean() } # Calculate derived metrics season_avg['FG2_PCT'] = (season_avg['FG2M'] / season_avg['FG2A'] if season_avg['FG2A'] > 0 else 0) season_avg['3PA_Percentage'] = (season_avg['FG3A'] / season_avg['FGA'] * 100) season_avg['EFG_PCT'] = ((season_avg['FGM'] + 0.5 * season_avg['FG3M']) / season_avg['FGA']) seasons_data.append(season_avg) except Exception as e: print(f"Error fetching {season_str}: {e}") continue return pd.DataFrame(seasons_data) # 2. Visualize Three-Point Volume Growth def plot_three_point_volume_trend(df, save_path=None): """ Create line plot showing three-point attempt growth over time. """ fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(14, 10)) # Plot 1: Absolute attempts ax1.plot(df['Year'], df['FG3A'], marker='o', linewidth=2.5, markersize=8, color='#1f77b4', label='3-Point Attempts') ax1.plot(df['Year'], df['FG2A'], marker='s', linewidth=2.5, markersize=8, color='#ff7f0e', label='2-Point Attempts') ax1.set_xlabel('Season', fontsize=12, fontweight='bold') ax1.set_ylabel('Attempts Per Game', fontsize=12, fontweight='bold') ax1.set_title('NBA Shot Attempt Evolution (Per Game Average)', fontsize=16, fontweight='bold', pad=20) ax1.legend(fontsize=11, loc='best') ax1.grid(True, alpha=0.3) ax1.set_xlim(df['Year'].min() - 1, df['Year'].max() + 1) # Annotations for key moments ax1.annotate('Seven Seconds\nor Less Suns', xy=(2007, df[df['Year']==2007]['FG3A'].values[0]), xytext=(2007, df['FG3A'].max() * 0.3), arrowprops=dict(arrowstyle='->', color='red', lw=1.5), fontsize=9, ha='center', color='red', fontweight='bold') ax1.annotate('Moreyball\nEra Begins', xy=(2013, df[df['Year']==2013]['FG3A'].values[0]), xytext=(2013, df['FG3A'].max() * 0.5), arrowprops=dict(arrowstyle='->', color='red', lw=1.5), fontsize=9, ha='center', color='red', fontweight='bold') ax1.annotate('Warriors\nDynasty', xy=(2015, df[df['Year']==2015]['FG3A'].values[0]), xytext=(2015, df['FG3A'].max() * 0.7), arrowprops=dict(arrowstyle='->', color='red', lw=1.5), fontsize=9, ha='center', color='red', fontweight='bold') # Plot 2: Percentage of total attempts ax2.plot(df['Year'], df['3PA_Percentage'], marker='o', linewidth=2.5, markersize=8, color='#2ca02c') ax2.fill_between(df['Year'], df['3PA_Percentage'], alpha=0.3, color='#2ca02c') ax2.set_xlabel('Season', fontsize=12, fontweight='bold') ax2.set_ylabel('% of Total Field Goal Attempts', fontsize=12, fontweight='bold') ax2.set_title('Three-Point Attempts as Percentage of Total Shots', fontsize=16, fontweight='bold', pad=20) ax2.grid(True, alpha=0.3) ax2.set_xlim(df['Year'].min() - 1, df['Year'].max() + 1) ax2.set_ylim(0, max(df['3PA_Percentage']) * 1.1) # Add horizontal reference lines ax2.axhline(y=33.33, color='red', linestyle='--', linewidth=1, label='33% (1 in 3 shots)', alpha=0.7) ax2.legend(fontsize=10, loc='upper left') plt.tight_layout() if save_path: plt.savefig(save_path, dpi=300, bbox_inches='tight') return fig # 3. Efficiency Comparison Over Time def plot_efficiency_evolution(df, save_path=None): """ Compare 2-point vs 3-point efficiency evolution. """ fig, ax = plt.subplots(figsize=(14, 8)) # Calculate expected points df['2PT_Expected'] = df['FG2_PCT'] * 2 df['3PT_Expected'] = df['FG3_PCT'] * 3 # Plot lines ax.plot(df['Year'], df['2PT_Expected'], marker='o', linewidth=2.5, markersize=8, label='2-Point Expected Points', color='#ff7f0e') ax.plot(df['Year'], df['3PT_Expected'], marker='s', linewidth=2.5, markersize=8, label='3-Point Expected Points', color='#1f77b4') ax.plot(df['Year'], df['EFG_PCT'] * 2, marker='^', linewidth=2.5, markersize=8, label='Overall eFG% × 2', color='#2ca02c') ax.set_xlabel('Season', fontsize=12, fontweight='bold') ax.set_ylabel('Expected Points Per Shot', fontsize=12, fontweight='bold') ax.set_title('Shot Efficiency Evolution: Expected Points Per Attempt', fontsize=16, fontweight='bold', pad=20) ax.legend(fontsize=11, loc='best') ax.grid(True, alpha=0.3) ax.set_xlim(df['Year'].min() - 1, df['Year'].max() + 1) # Add break-even line ax.axhline(y=1.0, color='red', linestyle='--', linewidth=1.5, label='Break-Even (1.0 PPP)', alpha=0.7) plt.tight_layout() if save_path: plt.savefig(save_path, dpi=300, bbox_inches='tight') return fig # 4. Shot Distribution Pie Charts (Then vs Now) def plot_shot_distribution_comparison(df, year_early, year_recent, save_path=None): """ Compare shot distribution between two eras using pie charts. """ fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(16, 7)) # Early era early_data = df[df['Year'] == year_early].iloc[0] early_values = [early_data['FG2A'], early_data['FG3A']] early_labels = [f"2-Point\n{early_data['FG2A']:.1f} attempts\n" f"({100 - early_data['3PA_Percentage']:.1f}%)", f"3-Point\n{early_data['FG3A']:.1f} attempts\n" f"({early_data['3PA_Percentage']:.1f}%)"] ax1.pie(early_values, labels=early_labels, autopct='', startangle=90, colors=['#ff7f0e', '#1f77b4'], textprops={'fontsize': 12, 'fontweight': 'bold'}, explode=(0, 0.1)) ax1.set_title(f'{year_early-1}-{str(year_early)[2:]} Season\n' f'Avg: {early_data["FG3A"]:.1f} 3PA/game', fontsize=14, fontweight='bold', pad=20) # Recent era recent_data = df[df['Year'] == year_recent].iloc[0] recent_values = [recent_data['FG2A'], recent_data['FG3A']] recent_labels = [f"2-Point\n{recent_data['FG2A']:.1f} attempts\n" f"({100 - recent_data['3PA_Percentage']:.1f}%)", f"3-Point\n{recent_data['FG3A']:.1f} attempts\n" f"({recent_data['3PA_Percentage']:.1f}%)"] ax2.pie(recent_values, labels=recent_labels, autopct='', startangle=90, colors=['#ff7f0e', '#1f77b4'], textprops={'fontsize': 12, 'fontweight': 'bold'}, explode=(0, 0.1)) ax2.set_title(f'{year_recent-1}-{str(year_recent)[2:]} Season\n' f'Avg: {recent_data["FG3A"]:.1f} 3PA/game', fontsize=14, fontweight='bold', pad=20) plt.suptitle('Shot Distribution Evolution: The Three-Point Revolution', fontsize=18, fontweight='bold', y=1.02) plt.tight_layout() if save_path: plt.savefig(save_path, dpi=300, bbox_inches='tight') return fig # 5. Team-Level Three-Point Volume Distribution def plot_team_3pa_distribution(season='2023-24', save_path=None): """ Show distribution of three-point attempts across teams for a given season. """ # Fetch team stats team_stats = leaguedashteamstats.LeagueDashTeamStats( season=season, season_type_all_star='Regular Season', per_mode_detailed='PerGame' ) df = team_stats.get_data_frames()[0] df = df.sort_values('FG3A', ascending=True) fig, ax = plt.subplots(figsize=(12, 10)) # Create horizontal bar chart bars = ax.barh(df['TEAM_NAME'], df['FG3A'], color='#1f77b4', edgecolor='black') # Color top 5 teams differently for i in range(len(bars) - 5, len(bars)): bars[i].set_color('#d62728') # Add value labels for i, (team, value) in enumerate(zip(df['TEAM_NAME'], df['FG3A'])): ax.text(value + 0.3, i, f'{value:.1f}', va='center', fontsize=9, fontweight='bold') # Add league average line league_avg = df['FG3A'].mean() ax.axvline(x=league_avg, color='green', linestyle='--', linewidth=2, label=f'League Average: {league_avg:.1f}') ax.set_xlabel('Three-Point Attempts Per Game', fontsize=12, fontweight='bold') ax.set_ylabel('Team', fontsize=12, fontweight='bold') ax.set_title(f'{season} Three-Point Attempts Per Game by Team', fontsize=16, fontweight='bold', pad=20) ax.legend(fontsize=11, loc='lower right') ax.grid(True, axis='x', alpha=0.3) plt.tight_layout() if save_path: plt.savefig(save_path, dpi=300, bbox_inches='tight') return fig # 6. Expected Points by Shot Location (Heat Map Concept) def create_expected_points_table(): """ Create a detailed table showing expected points by shot location. """ zones = { 'Shot Zone': [ 'Restricted Area (0-3 ft)', 'Paint (4-8 ft)', 'Short Mid-Range (8-16 ft)', 'Long Mid-Range (16-23 ft)', 'Corner Three (22 ft)', 'Above-Break Three (23.75 ft)', 'Deep Three (28+ ft)' ], 'FG%': [65.0, 45.0, 40.0, 38.0, 38.0, 35.0, 30.0], 'Points per Make': [2, 2, 2, 2, 3, 3, 3] } df = pd.DataFrame(zones) df['Expected Points'] = (df['FG%'] / 100) * df['Points per Make'] df['Attempts/Game (League)'] = [28.5, 8.2, 6.5, 7.6, 8.4, 26.8, 2.1] df['Total Expected Points'] = df['Expected Points'] * df['Attempts/Game (League)'] df['Efficiency Rank'] = df['Expected Points'].rank(ascending=False).astype(int) # Create visualization fig, ax = plt.subplots(figsize=(14, 6)) ax.axis('tight') ax.axis('off') # Create table table_data = df[['Shot Zone', 'FG%', 'Expected Points', 'Attempts/Game (League)', 'Efficiency Rank']].values table = ax.table(cellText=table_data, colLabels=['Shot Zone', 'FG%', 'Expected\nPoints', 'Attempts/Game', 'Efficiency\nRank'], cellLoc='center', loc='center', colWidths=[0.35, 0.12, 0.15, 0.18, 0.15]) table.auto_set_font_size(False) table.set_fontsize(10) table.scale(1, 2.5) # Style header for i in range(5): table[(0, i)].set_facecolor('#4472C4') table[(0, i)].set_text_props(weight='bold', color='white', fontsize=11) # Color code efficiency ranks colors = ['#70AD47', '#70AD47', '#FFC000', '#FFC000', '#C00000', '#C00000', '#C00000'] for i in range(1, 8): table[(i, 4)].set_facecolor(colors[i-1]) table[(i, 4)].set_text_props(weight='bold', color='white') plt.title('Expected Points Per Shot by Court Location', fontsize=16, fontweight='bold', pad=20) return fig, df # 7. Growth Rate Analysis def plot_three_point_growth_rate(df, save_path=None): """ Show year-over-year growth rate in three-point attempts. """ df = df.sort_values('Year') df['3PA_Growth_Rate'] = df['FG3A'].pct_change() * 100 fig, ax = plt.subplots(figsize=(14, 7)) colors = ['green' if x > 0 else 'red' for x in df['3PA_Growth_Rate']] bars = ax.bar(df['Year'], df['3PA_Growth_Rate'], color=colors, edgecolor='black', alpha=0.7) ax.axhline(y=0, color='black', linewidth=1.5) ax.set_xlabel('Season', fontsize=12, fontweight='bold') ax.set_ylabel('Year-over-Year Growth Rate (%)', fontsize=12, fontweight='bold') ax.set_title('Three-Point Attempt Growth Rate by Season', fontsize=16, fontweight='bold', pad=20) ax.grid(True, axis='y', alpha=0.3) plt.tight_layout() if save_path: plt.savefig(save_path, dpi=300, bbox_inches='tight') return fig # Example usage and workflow if __name__ == '__main__': print("Fetching NBA three-point revolution data...") # Fetch historical data trends_df = fetch_league_shooting_trends(start_year=2000, end_year=2024) # Create visualizations print("Creating visualizations...") # 1. Volume trends fig1 = plot_three_point_volume_trend(trends_df, 'three_point_volume_trend.png') print("✓ Created volume trend plot") # 2. Efficiency evolution fig2 = plot_efficiency_evolution(trends_df, 'efficiency_evolution.png') print("✓ Created efficiency evolution plot") # 3. Shot distribution comparison fig3 = plot_shot_distribution_comparison(trends_df, 2000, 2024, 'shot_distribution_comparison.png') print("✓ Created shot distribution comparison") # 4. Current season team distribution fig4 = plot_team_3pa_distribution('2023-24', 'team_3pa_distribution.png') print("✓ Created team distribution plot") # 5. Expected points table fig5, exp_points_df = create_expected_points_table() plt.savefig('expected_points_table.png', dpi=300, bbox_inches='tight') print("✓ Created expected points table") # 6. Growth rate fig6 = plot_three_point_growth_rate(trends_df, 'three_point_growth_rate.png') print("✓ Created growth rate plot") # Summary statistics print("\n" + "="*60) print("THREE-POINT REVOLUTION SUMMARY") print("="*60) early_2000s = trends_df[trends_df['Year'] == 2000].iloc[0] recent = trends_df[trends_df['Year'] == 2024].iloc[0] print(f"\n2000 Season:") print(f" 3PA/Game: {early_2000s['FG3A']:.1f}") print(f" % of Total Shots: {early_2000s['3PA_Percentage']:.1f}%") print(f" 3P%: {early_2000s['FG3_PCT']:.1%}") print(f"\n2024 Season:") print(f" 3PA/Game: {recent['FG3A']:.1f}") print(f" % of Total Shots: {recent['3PA_Percentage']:.1f}%") print(f" 3P%: {recent['FG3_PCT']:.1%}") print(f"\nChange:") print(f" 3PA/Game: +{recent['FG3A'] - early_2000s['FG3A']:.1f} " f"({(recent['FG3A'] / early_2000s['FG3A'] - 1) * 100:.1f}% increase)") print(f" % of Total Shots: +{recent['3PA_Percentage'] - early_2000s['3PA_Percentage']:.1f}pp") print("\n" + "="*60) plt.show() ``` ### R Code for Analysis ```r # Load required libraries library(tidyverse) library(ggplot2) library(hoopR) library(scales) library(patchwork) library(ggthemes) # 1. Fetch Historical League Trends fetch_league_trends <- function(start_year = 2000, end_year = 2024) { seasons_data <- list() for (year in start_year:end_year) { season_str <- paste0(year - 1, "-", substr(year, 3, 4)) tryCatch({ # Fetch team stats (using hoopR or custom data) team_stats <- nba_leaguedashteamstats( season = season_str, per_mode = "PerGame" ) # Calculate league averages season_avg <- team_stats %>% summarise( Season = season_str, Year = year, FGA = mean(fga, na.rm = TRUE), FGM = mean(fgm, na.rm = TRUE), FG_PCT = mean(fg_pct, na.rm = TRUE), FG3A = mean(fg3a, na.rm = TRUE), FG3M = mean(fg3m, na.rm = TRUE), FG3_PCT = mean(fg3_pct, na.rm = TRUE), FG2A = FGA - FG3A, FG2M = FGM - FG3M, PTS = mean(pts, na.rm = TRUE) ) %>% mutate( FG2_PCT = FG2M / FG2A, Three_PA_Percentage = (FG3A / FGA) * 100, EFG_PCT = (FGM + 0.5 * FG3M) / FGA ) seasons_data[[length(seasons_data) + 1]] <- season_avg }, error = function(e) { message(sprintf("Error fetching %s: %s", season_str, e$message)) }) } bind_rows(seasons_data) } # 2. Plot Three-Point Volume Trend plot_three_point_volume <- function(df) { # Plot 1: Absolute attempts p1 <- ggplot(df, aes(x = Year)) + geom_line(aes(y = FG3A, color = "3-Point"), linewidth = 1.5) + geom_point(aes(y = FG3A, color = "3-Point"), size = 3) + geom_line(aes(y = FG2A, color = "2-Point"), linewidth = 1.5) + geom_point(aes(y = FG2A, color = "2-Point"), size = 3) + scale_color_manual(values = c("3-Point" = "#1f77b4", "2-Point" = "#ff7f0e")) + labs( title = "NBA Shot Attempt Evolution (Per Game Average)", x = "Season", y = "Attempts Per Game", color = "Shot Type" ) + theme_minimal() + theme( plot.title = element_text(size = 16, face = "bold", hjust = 0.5), axis.title = element_text(size = 12, face = "bold"), legend.title = element_text(size = 11, face = "bold"), legend.position = "top" ) + annotate("text", x = 2007, y = max(df$FG3A) * 0.3, label = "Seven Seconds\nor Less Suns", color = "red", fontface = "bold", size = 3) + annotate("text", x = 2013, y = max(df$FG3A) * 0.5, label = "Moreyball\nEra Begins", color = "red", fontface = "bold", size = 3) + annotate("text", x = 2015, y = max(df$FG3A) * 0.7, label = "Warriors\nDynasty", color = "red", fontface = "bold", size = 3) # Plot 2: Percentage of total attempts p2 <- ggplot(df, aes(x = Year, y = Three_PA_Percentage)) + geom_line(color = "#2ca02c", linewidth = 1.5) + geom_point(color = "#2ca02c", size = 3) + geom_area(fill = "#2ca02c", alpha = 0.3) + geom_hline(yintercept = 33.33, linetype = "dashed", color = "red", linewidth = 1, alpha = 0.7) + annotate("text", x = min(df$Year), y = 33.33 + 2, label = "33% (1 in 3 shots)", hjust = 0, color = "red", size = 3) + labs( title = "Three-Point Attempts as Percentage of Total Shots", x = "Season", y = "% of Total Field Goal Attempts" ) + theme_minimal() + theme( plot.title = element_text(size = 16, face = "bold", hjust = 0.5), axis.title = element_text(size = 12, face = "bold") ) # Combine plots p1 / p2 } # 3. Efficiency Evolution Plot plot_efficiency_evolution <- function(df) { df_plot <- df %>% mutate( Two_PT_Expected = FG2_PCT * 2, Three_PT_Expected = FG3_PCT * 3, Overall_EFG = EFG_PCT * 2 ) ggplot(df_plot, aes(x = Year)) + geom_line(aes(y = Two_PT_Expected, color = "2-Point Expected"), linewidth = 1.5) + geom_point(aes(y = Two_PT_Expected, color = "2-Point Expected"), size = 3) + geom_line(aes(y = Three_PT_Expected, color = "3-Point Expected"), linewidth = 1.5) + geom_point(aes(y = Three_PT_Expected, color = "3-Point Expected"), size = 3) + geom_line(aes(y = Overall_EFG, color = "Overall eFG% × 2"), linewidth = 1.5) + geom_point(aes(y = Overall_EFG, color = "Overall eFG% × 2"), size = 3) + geom_hline(yintercept = 1.0, linetype = "dashed", color = "red", linewidth = 1.5, alpha = 0.7) + scale_color_manual(values = c( "2-Point Expected" = "#ff7f0e", "3-Point Expected" = "#1f77b4", "Overall eFG% × 2" = "#2ca02c" )) + labs( title = "Shot Efficiency Evolution: Expected Points Per Attempt", x = "Season", y = "Expected Points Per Shot", color = "Metric" ) + theme_minimal() + theme( plot.title = element_text(size = 16, face = "bold", hjust = 0.5), axis.title = element_text(size = 12, face = "bold"), legend.title = element_text(size = 11, face = "bold"), legend.position = "top" ) } # 4. Shot Distribution Comparison (Pie Charts) plot_shot_distribution_comparison <- function(df, year_early, year_recent) { early_data <- df %>% filter(Year == year_early) recent_data <- df %>% filter(Year == year_recent) # Prepare data for early era early_pie <- data.frame( Shot_Type = c("2-Point", "3-Point"), Attempts = c(early_data$FG2A, early_data$FG3A) ) %>% mutate( Percentage = Attempts / sum(Attempts) * 100, Label = paste0(Shot_Type, "\n", round(Attempts, 1), " att\n", "(", round(Percentage, 1), "%)") ) # Prepare data for recent era recent_pie <- data.frame( Shot_Type = c("2-Point", "3-Point"), Attempts = c(recent_data$FG2A, recent_data$FG3A) ) %>% mutate( Percentage = Attempts / sum(Attempts) * 100, Label = paste0(Shot_Type, "\n", round(Attempts, 1), " att\n", "(", round(Percentage, 1), "%)") ) # Early era pie p1 <- ggplot(early_pie, aes(x = "", y = Attempts, fill = Shot_Type)) + geom_bar(stat = "identity", width = 1, color = "white", linewidth = 2) + coord_polar("y", start = 0) + geom_text(aes(label = Label), position = position_stack(vjust = 0.5), size = 4, fontface = "bold") + scale_fill_manual(values = c("2-Point" = "#ff7f0e", "3-Point" = "#1f77b4")) + labs(title = paste0(year_early - 1, "-", substr(year_early, 3, 4), " Season\nAvg: ", round(early_data$FG3A, 1), " 3PA/game")) + theme_void() + theme( plot.title = element_text(size = 14, face = "bold", hjust = 0.5), legend.position = "none" ) # Recent era pie p2 <- ggplot(recent_pie, aes(x = "", y = Attempts, fill = Shot_Type)) + geom_bar(stat = "identity", width = 1, color = "white", linewidth = 2) + coord_polar("y", start = 0) + geom_text(aes(label = Label), position = position_stack(vjust = 0.5), size = 4, fontface = "bold") + scale_fill_manual(values = c("2-Point" = "#ff7f0e", "3-Point" = "#1f77b4")) + labs(title = paste0(year_recent - 1, "-", substr(year_recent, 3, 4), " Season\nAvg: ", round(recent_data$FG3A, 1), " 3PA/game")) + theme_void() + theme( plot.title = element_text(size = 14, face = "bold", hjust = 0.5), legend.position = "none" ) # Combine p1 + p2 + plot_annotation( title = "Shot Distribution Evolution: The Three-Point Revolution", theme = theme(plot.title = element_text(size = 18, face = "bold", hjust = 0.5)) ) } # 5. Expected Points Table create_expected_points_table <- function() { zones_df <- tibble( Shot_Zone = c( "Restricted Area (0-3 ft)", "Paint (4-8 ft)", "Short Mid-Range (8-16 ft)", "Long Mid-Range (16-23 ft)", "Corner Three (22 ft)", "Above-Break Three (23.75 ft)", "Deep Three (28+ ft)" ), FG_PCT = c(65.0, 45.0, 40.0, 38.0, 38.0, 35.0, 30.0), Points_per_Make = c(2, 2, 2, 2, 3, 3, 3), Attempts_Per_Game = c(28.5, 8.2, 6.5, 7.6, 8.4, 26.8, 2.1) ) %>% mutate( Expected_Points = (FG_PCT / 100) * Points_per_Make, Total_Expected = Expected_Points * Attempts_Per_Game, Efficiency_Rank = rank(-Expected_Points) ) # Create visualization ggplot(zones_df, aes(x = reorder(Shot_Zone, Expected_Points), y = Expected_Points)) + geom_col(aes(fill = Expected_Points), color = "black", linewidth = 0.5) + geom_text(aes(label = sprintf("%.2f PPP", Expected_Points)), hjust = -0.1, fontface = "bold", size = 3.5) + scale_fill_gradient2( low = "#C00000", mid = "#FFC000", high = "#70AD47", midpoint = 1.0, name = "Expected\nPoints" ) + coord_flip() + labs( title = "Expected Points Per Shot by Court Location", x = "Shot Zone", y = "Expected Points Per Attempt" ) + theme_minimal() + theme( plot.title = element_text(size = 16, face = "bold", hjust = 0.5), axis.title = element_text(size = 12, face = "bold"), axis.text = element_text(size = 10), legend.position = "right" ) + ylim(0, max(zones_df$Expected_Points) * 1.15) } # 6. Year-over-Year Growth Rate plot_growth_rate <- function(df) { df_growth <- df %>% arrange(Year) %>% mutate(Growth_Rate = (FG3A - lag(FG3A)) / lag(FG3A) * 100) %>% filter(!is.na(Growth_Rate)) ggplot(df_growth, aes(x = Year, y = Growth_Rate)) + geom_col(aes(fill = Growth_Rate > 0), color = "black", linewidth = 0.3) + geom_hline(yintercept = 0, linewidth = 1) + scale_fill_manual(values = c("TRUE" = "#70AD47", "FALSE" = "#C00000"), guide = "none") + labs( title = "Three-Point Attempt Growth Rate by Season", x = "Season", y = "Year-over-Year Growth Rate (%)" ) + theme_minimal() + theme( plot.title = element_text(size = 16, face = "bold", hjust = 0.5), axis.title = element_text(size = 12, face = "bold") ) } # Example workflow if (interactive()) { # Fetch data trends_df <- fetch_league_trends(2000, 2024) # Create plots p_volume <- plot_three_point_volume(trends_df) p_efficiency <- plot_efficiency_evolution(trends_df) p_distribution <- plot_shot_distribution_comparison(trends_df, 2000, 2024) p_expected <- create_expected_points_table() p_growth <- plot_growth_rate(trends_df) # Display print(p_volume) print(p_efficiency) print(p_distribution) print(p_expected) print(p_growth) # Save ggsave("three_point_volume_r.png", p_volume, width = 14, height = 10, dpi = 300) ggsave("efficiency_evolution_r.png", p_efficiency, width = 14, height = 8, dpi = 300) ggsave("shot_distribution_r.png", p_distribution, width = 16, height = 7, dpi = 300) ggsave("expected_points_r.png", p_expected, width = 12, height = 8, dpi = 300) ggsave("growth_rate_r.png", p_growth, width = 14, height = 7, dpi = 300) } ``` ## The Future of the Three-Point Revolution ### Current Trends **2024 and Beyond**: - Three-point volume approaching 40% of all shots - Continued decline in mid-range attempts - Big men increasingly required to shoot threes - Defensive innovation focusing on three-point prevention ### Potential Limitations **1. Diminishing Returns** - Elite three-point shooters increasingly scarce and expensive - Variance in playoff settings remains a concern - Defensive schemes evolving to counter three-point volume **2. Rule Changes** - NBA could modify three-point line distance - Potential "four-point line" discussions - Defensive rule changes to balance offense **3. Strategic Counter-Trends** - Some teams experimenting with size and rim protection - Playoff success still requiring diverse offensive tools - Mid-range specialists finding niche value in specific matchups ### Impact on Basketball Culture The three-point revolution has transformed basketball at all levels: **Youth Development**: - Kids prioritize three-point shooting over mid-range - Big men develop perimeter skills earlier - Spacing concepts taught from young ages **International Basketball**: - FIBA game also experiencing three-point increase - European leagues adopting analytics-driven approaches - Global convergence toward three-point-heavy styles **College Basketball**: - NCAA three-point attempts increasing yearly - Recruiting prioritizes shooting and spacing - Coaching philosophies mirror NBA trends ## Conclusion The three-point revolution represents the most significant strategic transformation in basketball history. Driven by mathematical analysis and pioneered by analytics-minded organizations, the shift from mid-range-heavy to three-point-centric basketball has fundamentally altered how the game is played, coached, and evaluated. From the introduction of the three-point line in 1979 to the current era where nearly 40% of all shots are threes, the evolution has been dramatic. Teams like the Seven Seconds or Less Suns, Moreyball Rockets, and Warriors dynasty demonstrated the strategic superiority of three-point volume, forcing league-wide adoption. The math is simple but powerful: a 35% three-point shooter is more valuable than a 50% mid-range shooter. This insight, combined with the spacing benefits of perimeter shooting, has made the three-pointer the cornerstone of modern offense. The mid-range shot, once the staple of basketball offense, has been relegated to bailout situations and specialist weapons. As basketball continues to evolve, the three-point revolution shows no signs of slowing. Future innovations may include even greater volume, rule changes to rebalance the game, or defensive schemes that successfully counter three-point dominance. Regardless, the analytics revolution that drove the three-point transformation will continue to shape basketball strategy for generations to come.

Discussion

Have questions or feedback? Join our community discussion on Discord or GitHub Discussions.
Table of Contents
Quick Actions
Glossary