Pick and Roll Analytics

Beginner 10 min read 0 views Nov 27, 2025
# Pick and Roll Analytics ## Introduction The pick-and-roll is basketball's most fundamental and prevalent offensive action, used in nearly every NBA game and accounting for roughly 20-25% of all possessions. Modern basketball analytics has revolutionized how we understand, evaluate, and defend this timeless play through detailed tracking data, synergy classifications, and efficiency metrics. This guide covers the analytical framework for evaluating pick-and-roll effectiveness from both offensive and defensive perspectives, with practical code examples for data analysis. ## Pick and Roll Fundamentals ### What is a Pick and Roll? A pick-and-roll occurs when: 1. **Screener sets a pick**: One offensive player (typically a big) sets a screen on the ball handler's defender 2. **Ball handler uses the screen**: The player with the ball dribbles off the screen to create separation 3. **Screener rolls/pops**: The screener moves toward the basket (roll) or away (pop) looking for a pass 4. **Decision making**: The ball handler reads the defense and makes the optimal play ### Types of Pick and Roll Actions **1. Ball Screen on Top** - Set above the 3-point line - Most common (60%+ of all ball screens) - Creates driving angles and forces help rotations - Used in delay and side pick-and-roll sets **2. Side Pick and Roll** - Set on wings or baseline - Limits defensive help options - Often results in corner kick-outs - Favored by guards with mid-range games **3. Drag Screen** - Set in transition before defense is set - Quick hitting, creates numbers advantages - Growing in popularity (15-20% increase since 2015) **4. Spain Pick and Roll** - Back screen set on ball handler's screener's defender - Creates confusion in defensive rotations - Advanced action requiring coordination **5. Ram Screen** - Screen-the-screener action - Ball handler receives screen before using main pick - Popular in European basketball ### Play Type Outcomes Pick-and-roll possessions can result in: - **Ball handler shot**: Handler keeps ball and shoots - **Roll man finish**: Pass to screener for score - **Kick-out**: Pass to perimeter shooter - **Pocket pass**: Quick pass to rolling big before help arrives - **Short roll**: Screener stops in mid-range for pass - **Turnover**: Bad pass, offensive foul, or steal - **Reset**: Action doesn't create advantage, possession continues ## Ball Handler Metrics ### Core Efficiency Metrics **1. Points Per Possession (PPP)** ``` PPP = Total Points Scored / Total Possessions ``` - **Elite**: > 1.00 PPP - **Above Average**: 0.95-1.00 PPP - **Average**: 0.90-0.95 PPP - **Below Average**: < 0.90 PPP **League Context**: Pick-and-roll ball handler possessions typically average 0.91-0.94 PPP, slightly below overall offensive efficiency due to defensive attention. **2. Effective Field Goal Percentage (eFG%)** ``` eFG% = (FGM + 0.5 × 3PM) / FGA ``` Accounts for the higher value of 3-point shots made off pick-and-roll actions. **3. Frequency (Possessions Per Game)** - High volume: > 10 possessions/game - Medium volume: 5-10 possessions/game - Low volume: < 5 possessions/game **4. Scoring Frequency** ``` Scoring Frequency = Possessions Ending in Handler Score / Total Possessions ``` Measures how often the ball handler keeps the ball and scores versus passing. **5. Assist Rate** ``` Assist Rate = Assists Generated / Total Possessions ``` Measures playmaking ability out of pick-and-roll. ### Advanced Ball Handler Metrics **1. Decision Quality Index (DQI)** ``` DQI = (Shots at Rim + Assists + Fouls Drawn) / (Total Possessions) ``` Measures frequency of creating high-value outcomes. **2. Pressure Score** ``` Pressure Score = (Pull-up 3s + Floaters + Free Throws) / Shot Attempts ``` Indicates ability to score under defensive pressure without getting to rim. **3. Turnover Rate** ``` TOV% = Turnovers / Total Possessions ``` - **Elite**: < 10% - **Good**: 10-15% - **Concerning**: > 15% **4. Usage in Pick and Roll** ``` PnR Usage = PnR Possessions / Player's Total Possessions ``` Shows reliance on pick-and-roll for offensive production. ### Shot Selection Analysis Key shot types from pick-and-roll ball handler: - **At Rim**: 0-3 feet (Expected FG%: 60-65%) - **Floater Range**: 4-10 feet (Expected FG%: 40-45%) - **Mid-range**: 10-16 feet (Expected FG%: 38-42%) - **Pull-up 3**: Beyond arc (Expected FG%: 33-37%) **Shot Quality Score**: ```python def calculate_shot_quality(shots_df): """ Assign quality scores based on shot location """ quality_scores = [] for _, shot in shots_df.iterrows(): distance = shot['SHOT_DISTANCE'] if distance <= 3: quality_scores.append(1.0) # At rim elif distance <= 10: quality_scores.append(0.7) # Floater elif distance <= 16: quality_scores.append(0.5) # Mid-range else: quality_scores.append(0.8) # Three-point return quality_scores ``` ## Roll Man Metrics ### Core Roll Man Metrics **1. Roll Man Efficiency (PPP)** - **Elite**: > 1.20 PPP (better than ball handler due to shot quality) - **Above Average**: 1.10-1.20 PPP - **Average**: 1.00-1.10 PPP - **Below Average**: < 1.00 PPP **2. Field Goal Percentage** - Roll men should shoot 65%+ on rim attempts - 40%+ on short roll/pop attempts - Lower percentages indicate poor screening or finishing **3. Screen Assists** ``` Screen Assists = Assists directly resulting from screen ``` Tracks how often the screen creates scoring opportunities. **4. Rim Gravity** ``` Rim Gravity = % of possessions where roll man receives pass ``` Measures defensive attention paid to the roller. **5. Finishing Efficiency by Zone** - **Dunk Zone** (0-3 feet): Should be 75%+ FG% - **Short Roll** (4-10 feet): Should be 50%+ FG% - **Pop** (16+ feet): Should be 38%+ FG% ### Advanced Roll Man Metrics **1. Vertical Spacing Score** ``` Vertical Spacing = Average depth of roll (feet from 3-point line) ``` - Deep rollers (20+ feet): Create more driving space - Short rollers (10-15 feet): Better for pocket passes **2. Roll Speed** Measured via tracking data - how quickly screener gets to rim after setting screen. - **Fast rollers** (> 15 ft/sec): Create timing issues for defense - **Patient rollers** (< 12 ft/sec): Better for reading and short roll **3. Hands Readiness** Advanced tracking metric measuring how quickly roll man is ready to catch pass. **4. Catch Radius** Area (square feet) where roll man can successfully catch passes. - Elite finishers: Large catch radius, catch difficult passes - Limited finishers: Small catch radius, need perfect passes **5. Screening Effectiveness** ``` Screen Effectiveness = Ball Handler's PPP when using this screener / Ball Handler's Average PPP ``` Ratio > 1.0 indicates screener improves ball handler's efficiency. ## Points Per Possession by Play Type ### Synergy Play Type Classifications **1. Ball Handler Shoots** - **Pull-up Jumper**: 0.85-0.90 PPP - **Drive to Rim**: 1.05-1.10 PPP - **Floater**: 0.80-0.85 PPP **2. Pass to Roll Man** - **Roll to Rim**: 1.20-1.25 PPP (most efficient) - **Short Roll**: 1.00-1.05 PPP - **Pop to Perimeter**: 0.95-1.00 PPP **3. Kick-out to Shooter** - **Corner 3**: 1.05-1.10 PPP - **Wing 3**: 1.00-1.05 PPP - **Above Break 3**: 0.95-1.00 PPP **4. Reset/No Shot** - 0.00 PPP (possession continues) - Can indicate good defense or poor execution **5. Turnover** - -1.0 to 0.0 PPP (depending on whether points scored in transition) ### Outcome Distribution Analysis Typical NBA team pick-and-roll outcome distribution: ``` Ball Handler Shot: 42-48% Pass to Roll Man: 15-20% Kick-out: 18-23% Turnover: 8-12% Reset: 12-18% ``` **Efficiency by Outcome Example**: ```python import pandas as pd import numpy as np # Example pick-and-roll outcomes data outcomes_data = { 'outcome_type': [ 'Ball Handler - Rim', 'Ball Handler - Mid', 'Ball Handler - Three', 'Roll Man - Dunk', 'Roll Man - Short Roll', 'Roll Man - Pop', 'Kick-out - Corner 3', 'Kick-out - Wing 3', 'Turnover' ], 'frequency': [0.18, 0.12, 0.15, 0.08, 0.05, 0.04, 0.12, 0.08, 0.10], 'ppp': [1.08, 0.84, 1.02, 1.35, 1.05, 0.90, 1.08, 0.99, 0.00] } df = pd.DataFrame(outcomes_data) # Calculate weighted PPP df['weighted_ppp'] = df['frequency'] * df['ppp'] total_ppp = df['weighted_ppp'].sum() print(f"Overall Pick and Roll PPP: {total_ppp:.3f}") # Identify most efficient outcomes df['efficiency_rank'] = df['ppp'].rank(ascending=False) print("\nOutcomes ranked by efficiency:") print(df.sort_values('ppp', ascending=False)[['outcome_type', 'ppp', 'frequency']]) ``` ### Expected PPP Models **Baseline Expected PPP**: ```python def calculate_expected_ppp(ball_handler_skill, roll_man_skill, defense_quality, spacing): """ Calculate expected PPP based on key factors Parameters: - ball_handler_skill: 0-1 scale (shooting, passing, driving) - roll_man_skill: 0-1 scale (finishing, screening) - defense_quality: 0-1 scale (switching ability, help defense) - spacing: Number of capable shooters on floor (0-4) """ base_ppp = 0.90 # Ball handler contribution (max +0.15) bh_factor = ball_handler_skill * 0.15 # Roll man contribution (max +0.10) rm_factor = roll_man_skill * 0.10 # Defense penalty (max -0.20) def_factor = -defense_quality * 0.20 # Spacing bonus (max +0.15) spacing_factor = (spacing / 4) * 0.15 expected_ppp = base_ppp + bh_factor + rm_factor + def_factor + spacing_factor return expected_ppp # Example usage ppp = calculate_expected_ppp( ball_handler_skill=0.85, # Elite roll_man_skill=0.70, # Above average defense_quality=0.60, # Moderate spacing=3 # 3 shooters ) print(f"Expected PPP: {ppp:.3f}") # ~1.02 ``` ## Defensive Coverage Types ### Primary Coverage Schemes **1. Drop Coverage** - **Description**: Big defender stays in paint, forces ball handler into mid-range - **Usage**: ~35-40% of NBA possessions - **Strengths**: Protects rim, prevents easy rolls - **Weaknesses**: Vulnerable to pull-up shooting - **PPP Against**: 0.92-0.96 (effective against non-shooters) **2. Switch** - **Description**: All defenders switch, no help needed - **Usage**: ~25-30% of NBA possessions (increasing) - **Strengths**: No gaps, maintains matchups - **Weaknesses**: Creates mismatches (guard on big, big on guard) - **PPP Against**: 0.95-1.00 (depends on switching personnel) **3. Hedge (Hard Show)** - **Description**: Big steps up aggressively to trap/slow ball handler - **Usage**: ~15-20% of NBA possessions - **Strengths**: Disrupts rhythm, forces tough passes - **Weaknesses**: Leaves roll man open momentarily - **PPP Against**: 0.88-0.93 (effective when executed well) **4. Soft Show (Flat)** - **Description**: Big shows briefly then retreats - **Usage**: ~10-15% of NBA possessions - **Strengths**: Slows ball handler, recovers to roll man - **Weaknesses**: Doesn't fully commit to either threat - **PPP Against**: 0.93-0.97 **5. Ice/Blue (Push Baseline)** - **Description**: Force ball handler toward baseline, away from screen - **Usage**: ~5-10% of NBA possessions - **Strengths**: Limits angles, easier to help from baseline - **Weaknesses**: Vulnerable to rejections and slips - **PPP Against**: 0.90-0.95 **6. Trap/Blitz** - **Description**: Both defenders aggressively trap ball handler - **Usage**: ~3-5% of NBA possessions (situational) - **Strengths**: Forces turnover or difficult pass - **Weaknesses**: Leaves shooters open, requires perfect rotation - **PPP Against**: 0.85-0.95 (high variance) ### Coverage Selection Matrix ```python import pandas as pd # Decision matrix for defensive coverage def recommend_coverage(ball_handler_threat, roll_man_threat, shooter_threat, defensive_personnel): """ Recommend optimal pick-and-roll coverage Parameters: - ball_handler_threat: 'elite_shooter', 'driver', 'balanced', 'limited' - roll_man_threat: 'elite', 'good', 'limited' - shooter_threat: 'high' (3+ shooters), 'medium', 'low' - defensive_personnel: 'switch', 'drop', 'mobile', 'traditional' """ recommendations = { ('elite_shooter', 'elite', 'high', 'switch'): 'Switch with Size', ('elite_shooter', 'elite', 'high', 'mobile'): 'Hedge & Recover', ('elite_shooter', 'good', 'medium', 'switch'): 'Switch', ('elite_shooter', 'limited', 'low', 'drop'): 'Drop & Close', ('driver', 'elite', 'high', 'mobile'): 'Ice/Blue', ('driver', 'good', 'medium', 'drop'): 'Drop Coverage', ('driver', 'limited', 'low', 'drop'): 'Drop Deep', ('balanced', 'elite', 'high', 'switch'): 'Switch', ('balanced', 'good', 'medium', 'mobile'): 'Soft Show', ('limited', 'elite', 'low', 'drop'): 'Drop & Protect Rim', ('limited', 'limited', 'low', 'any'): 'Under Screen' } key = (ball_handler_threat, roll_man_threat, shooter_threat, defensive_personnel) return recommendations.get(key, 'Switch (Default)') # Example coverage = recommend_coverage( ball_handler_threat='elite_shooter', roll_man_threat='elite', shooter_threat='high', defensive_personnel='switch' ) print(f"Recommended Coverage: {coverage}") ``` ### Defensive Metrics by Coverage **Coverage Success Metrics**: ```python def evaluate_coverage_effectiveness(coverage_data): """ Evaluate defensive coverage effectiveness """ metrics = { 'ppp_allowed': coverage_data['points'].sum() / len(coverage_data), 'fg_pct_allowed': coverage_data['makes'].sum() / coverage_data['attempts'].sum(), 'turnover_rate': coverage_data['turnovers'].sum() / len(coverage_data), 'rim_protection': 1 - (coverage_data['rim_attempts'].sum() / len(coverage_data)), 'rotation_success': coverage_data['clean_rotations'].sum() / len(coverage_data) } return metrics ``` ## Synergy Play Type Data ### Understanding Synergy Classifications Synergy Sports Technology tracks pick-and-roll possessions with detailed classifications: **Play Type Categories**: 1. **Pick-and-Roll Ball Handler** (PRBH) 2. **Pick-and-Roll Roll Man** (PRRM) 3. **Off Screen** (when ball handler passes immediately) 4. **Transition** (when PnR occurs in transition) 5. **Isolation** (when action starts but becomes 1-on-1) ### Synergy Metrics Hierarchy **Tier 1: Volume Metrics** - Possessions per game - Frequency (% of team's offensive possessions) - Touches per game **Tier 2: Efficiency Metrics** - Points per possession (PPP) - Effective field goal percentage (eFG%) - Turnover percentage - Free throw attempt rate **Tier 3: Shot Quality Metrics** - Field goal attempts per possession - % at rim - % three-point attempts - Shot type distribution **Tier 4: Playmaking Metrics** - Assists generated - Assist rate - Potential assists - Secondary assists ### Accessing and Analyzing Synergy-Style Data ```python import pandas as pd import numpy as np import matplotlib.pyplot as plt import seaborn as sns from nba_api.stats.endpoints import playerdashptpass, playerdashptshotdefend from nba_api.stats.endpoints import synergyleader # Note: Synergy data requires subscription, but similar data available via NBA API def get_pick_and_roll_stats(season='2023-24'): """ Fetch pick-and-roll statistics from NBA API """ # Get ball handler stats ball_handler_stats = synergyleader.SynergyLeader( season=season, season_type_all_star='Regular Season', play_type='PRBH', type_grouping='offensive', per_mode='PerGame' ).get_data_frames()[0] # Get roll man stats roll_man_stats = synergyleader.SynergyLeader( season=season, season_type_all_star='Regular Season', play_type='PRRM', type_grouping='offensive', per_mode='PerGame' ).get_data_frames()[0] return ball_handler_stats, roll_man_stats def analyze_pick_and_roll_tendencies(player_name, stats_df): """ Comprehensive pick-and-roll tendency analysis """ player_data = stats_df[stats_df['PLAYER_NAME'] == player_name].iloc[0] analysis = { 'player': player_name, 'possessions_per_game': player_data['POSS_PER_GAME'], 'ppp': player_data['PPP'], 'percentile_rank': player_data['PERCENTILE'], 'fg_pct': player_data['FG_PCT'], 'efg_pct': player_data['EFG_PCT'], 'to_pct': player_data['TOV_PCT'], 'ft_rate': player_data['FT_RATE'], 'score_pct': player_data['SCORE_PCT'], 'team_name': player_data['TEAM_NAME'] } return analysis # Example usage (simulated data structure) # In practice, requires NBA.com authentication ``` ### Building a Pick-and-Roll Database ```python import sqlite3 import pandas as pd from datetime import datetime def create_pick_and_roll_database(): """ Create database schema for pick-and-roll analytics """ conn = sqlite3.connect('pick_and_roll_analytics.db') cursor = conn.cursor() # Ball handler table cursor.execute(''' CREATE TABLE IF NOT EXISTS ball_handler_stats ( id INTEGER PRIMARY KEY AUTOINCREMENT, player_id INTEGER, player_name TEXT, season TEXT, team TEXT, possessions INTEGER, possessions_per_game REAL, ppp REAL, fg_pct REAL, efg_pct REAL, three_pt_pct REAL, ft_rate REAL, to_pct REAL, assists INTEGER, ast_pct REAL, score_pct REAL, shots_at_rim INTEGER, rim_fg_pct REAL, pull_up_threes INTEGER, pull_up_three_pct REAL, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ''') # Roll man table cursor.execute(''' CREATE TABLE IF NOT EXISTS roll_man_stats ( id INTEGER PRIMARY KEY AUTOINCREMENT, player_id INTEGER, player_name TEXT, season TEXT, team TEXT, possessions INTEGER, possessions_per_game REAL, ppp REAL, fg_pct REAL, dunks INTEGER, dunk_pct REAL, short_roll_fg_pct REAL, pop_attempts INTEGER, pop_fg_pct REAL, screen_assists INTEGER, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ''') # Coverage effectiveness table cursor.execute(''' CREATE TABLE IF NOT EXISTS coverage_effectiveness ( id INTEGER PRIMARY KEY AUTOINCREMENT, team TEXT, season TEXT, coverage_type TEXT, possessions INTEGER, ppp_allowed REAL, fg_pct_allowed REAL, three_pt_pct_allowed REAL, to_rate REAL, frequency REAL, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ''') # Matchup table cursor.execute(''' CREATE TABLE IF NOT EXISTS pick_and_roll_matchups ( id INTEGER PRIMARY KEY AUTOINCREMENT, game_id TEXT, ball_handler_id INTEGER, roll_man_id INTEGER, defender_on_ball_id INTEGER, defender_on_screener_id INTEGER, coverage_type TEXT, outcome TEXT, ppp REAL, shot_made INTEGER, assist INTEGER, turnover INTEGER, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ''') conn.commit() conn.close() print("Pick-and-roll database created successfully!") create_pick_and_roll_database() ``` ### Comprehensive Analysis Framework ```python import pandas as pd import numpy as np import matplotlib.pyplot as plt import seaborn as sns from scipy import stats class PickAndRollAnalyzer: """ Comprehensive pick-and-roll analysis framework """ def __init__(self, season='2023-24'): self.season = season self.ball_handler_data = None self.roll_man_data = None self.league_averages = { 'ball_handler_ppp': 0.92, 'roll_man_ppp': 1.15, 'overall_ppp': 0.93 } def calculate_synergy_index(self, ball_handler, roll_man): """ Calculate how well two players work together in pick-and-roll """ # Get individual stats bh_ppp = ball_handler['ppp'] rm_ppp = roll_man['ppp'] # Calculate expected combined PPP expected_ppp = (bh_ppp * 0.65 + rm_ppp * 0.35) # Get actual combined PPP (would need matchup data) # For demonstration, simulating actual_ppp = expected_ppp * np.random.uniform(0.95, 1.08) synergy_index = actual_ppp / expected_ppp return { 'ball_handler': ball_handler['name'], 'roll_man': roll_man['name'], 'expected_ppp': round(expected_ppp, 3), 'actual_ppp': round(actual_ppp, 3), 'synergy_index': round(synergy_index, 3), 'rating': self._get_synergy_rating(synergy_index) } def _get_synergy_rating(self, index): if index >= 1.08: return 'Elite' elif index >= 1.04: return 'Great' elif index >= 1.00: return 'Good' elif index >= 0.96: return 'Average' else: return 'Poor' def compare_to_league(self, player_ppp, role='ball_handler'): """ Compare player efficiency to league average """ league_avg = self.league_averages[f'{role}_ppp'] difference = player_ppp - league_avg percentile = stats.percentileofscore( np.random.normal(league_avg, 0.08, 1000), player_ppp ) return { 'player_ppp': round(player_ppp, 3), 'league_average': round(league_avg, 3), 'difference': round(difference, 3), 'percentile': round(percentile, 1), 'rating': self._get_efficiency_rating(percentile) } def _get_efficiency_rating(self, percentile): if percentile >= 90: return 'Elite' elif percentile >= 75: return 'Above Average' elif percentile >= 50: return 'Average' elif percentile >= 25: return 'Below Average' else: return 'Poor' def analyze_shot_distribution(self, shots_df): """ Analyze shot distribution from pick-and-roll """ shots_df['zone'] = shots_df['distance'].apply(self._categorize_shot) distribution = shots_df.groupby('zone').agg({ 'shot_made': ['count', 'sum', 'mean'] }).round(3) distribution.columns = ['Attempts', 'Makes', 'FG%'] distribution['PPP'] = distribution.apply( lambda row: row['FG%'] * (3 if 'Three' in row.name else 2), axis=1 ).round(3) distribution['Frequency'] = ( distribution['Attempts'] / distribution['Attempts'].sum() * 100 ).round(1) return distribution def _categorize_shot(self, distance): if distance <= 3: return 'At Rim' elif distance <= 10: return 'Floater Range' elif distance <= 16: return 'Mid-Range' else: return 'Three-Point' def identify_defensive_vulnerabilities(self, coverage_stats): """ Identify which defensive coverages a player exploits """ vulnerabilities = coverage_stats.sort_values('ppp', ascending=False) analysis = [] for _, row in vulnerabilities.iterrows(): if row['ppp'] > self.league_averages['overall_ppp']: analysis.append({ 'coverage': row['coverage_type'], 'ppp': round(row['ppp'], 3), 'frequency': round(row['frequency'], 1), 'advantage': round(row['ppp'] - self.league_averages['overall_ppp'], 3) }) return pd.DataFrame(analysis) # Example usage analyzer = PickAndRollAnalyzer(season='2023-24') # Analyze ball handler vs league ball_handler_comparison = analyzer.compare_to_league( player_ppp=0.98, role='ball_handler' ) print("Ball Handler Comparison:") print(ball_handler_comparison) # Analyze roll man vs league roll_man_comparison = analyzer.compare_to_league( player_ppp=1.22, role='roll_man' ) print("\nRoll Man Comparison:") print(roll_man_comparison) # Calculate synergy between two players ball_handler = {'name': 'Player A', 'ppp': 0.98} roll_man = {'name': 'Player B', 'ppp': 1.22} synergy = analyzer.calculate_synergy_index(ball_handler, roll_man) print("\nPick-and-Roll Synergy:") print(synergy) ``` ## R Implementation ### Pick-and-Roll Analysis in R ```r library(tidyverse) library(hoopR) library(ggplot2) library(scales) # Function to calculate pick-and-roll metrics calculate_pnr_metrics <- function(pnr_data) { metrics <- pnr_data %>% group_by(player_name, role) %>% summarise( possessions = n(), possessions_per_game = n() / n_distinct(game_id), points = sum(points, na.rm = TRUE), ppp = points / possessions, fg_attempts = sum(fga, na.rm = TRUE), fg_makes = sum(fgm, na.rm = TRUE), fg_pct = fg_makes / fg_attempts, three_attempts = sum(fga_3, na.rm = TRUE), three_makes = sum(fgm_3, na.rm = TRUE), efg_pct = (fg_makes + 0.5 * three_makes) / fg_attempts, turnovers = sum(turnover, na.rm = TRUE), to_pct = turnovers / possessions, assists = sum(assist, na.rm = TRUE), ast_pct = assists / possessions, .groups = 'drop' ) %>% mutate( ppp_percentile = percent_rank(ppp) * 100, efficiency_rating = case_when( ppp_percentile >= 90 ~ 'Elite', ppp_percentile >= 75 ~ 'Above Average', ppp_percentile >= 50 ~ 'Average', ppp_percentile >= 25 ~ 'Below Average', TRUE ~ 'Poor' ) ) return(metrics) } # Analyze ball handler decision making analyze_ball_handler_decisions <- function(pnr_data) { decisions <- pnr_data %>% filter(role == 'ball_handler') %>% group_by(player_name) %>% summarise( total_possessions = n(), kept_ball = sum(outcome_type == 'shot'), passed_roll_man = sum(outcome_type == 'pass_roller'), kicked_out = sum(outcome_type == 'kick_out'), turnover = sum(outcome_type == 'turnover'), .groups = 'drop' ) %>% mutate( kept_ball_pct = kept_ball / total_possessions * 100, passed_roll_man_pct = passed_roll_man / total_possessions * 100, kicked_out_pct = kicked_out / total_possessions * 100, turnover_pct = turnover / total_possessions * 100 ) return(decisions) } # Analyze roll man effectiveness analyze_roll_man <- function(pnr_data) { roll_stats <- pnr_data %>% filter(role == 'roll_man') %>% group_by(player_name) %>% summarise( possessions = n(), ppp = sum(points) / possessions, fg_pct = sum(fgm) / sum(fga), dunks = sum(shot_type == 'dunk'), dunk_pct = dunks / possessions * 100, at_rim_attempts = sum(shot_distance <= 3), at_rim_pct = sum(fgm[shot_distance <= 3]) / at_rim_attempts * 100, short_roll_attempts = sum(shot_distance > 3 & shot_distance <= 10), short_roll_pct = sum(fgm[shot_distance > 3 & shot_distance <= 10]) / short_roll_attempts * 100, pop_attempts = sum(shot_distance > 16), pop_pct = if_else(pop_attempts > 0, sum(fgm[shot_distance > 16]) / pop_attempts * 100, 0), .groups = 'drop' ) return(roll_stats) } # Evaluate defensive coverage effectiveness evaluate_coverage <- function(defensive_data) { coverage_stats <- defensive_data %>% group_by(coverage_type) %>% summarise( possessions = n(), ppp_allowed = sum(points) / possessions, fg_pct_allowed = sum(fgm) / sum(fga) * 100, three_pt_pct_allowed = sum(fgm_3) / sum(fga_3) * 100, to_rate = sum(turnover) / possessions * 100, frequency = n() / sum(n()) * 100, .groups = 'drop' ) %>% arrange(ppp_allowed) return(coverage_stats) } # Visualize pick-and-roll efficiency by play type plot_pnr_efficiency <- function(pnr_data) { outcome_summary <- pnr_data %>% group_by(outcome_type) %>% summarise( possessions = n(), ppp = sum(points) / possessions, frequency = n() / nrow(pnr_data) * 100, .groups = 'drop' ) %>% arrange(desc(ppp)) p <- ggplot(outcome_summary, aes(x = reorder(outcome_type, ppp), y = ppp)) + geom_bar(stat = 'identity', aes(fill = ppp), color = 'black') + geom_text(aes(label = sprintf('%.2f\n(%.1f%%)', ppp, frequency)), hjust = -0.1, size = 3.5, fontface = 'bold') + scale_fill_gradient2( low = '#d7191c', mid = '#ffffbf', high = '#1a9641', midpoint = 0.95, name = 'PPP' ) + coord_flip() + labs( title = 'Pick-and-Roll Efficiency by Outcome Type', subtitle = 'Points Per Possession and Frequency', x = 'Outcome Type', y = 'Points Per Possession (PPP)' ) + theme_minimal() + theme( plot.title = element_text(size = 16, face = 'bold', hjust = 0.5), plot.subtitle = element_text(size = 12, hjust = 0.5), axis.title = element_text(size = 12, face = 'bold'), axis.text = element_text(size = 10), legend.position = 'right' ) return(p) } # Compare ball handlers compare_ball_handlers <- function(pnr_data, players) { comparison <- pnr_data %>% filter(player_name %in% players, role == 'ball_handler') %>% group_by(player_name) %>% summarise( possessions_pg = n() / n_distinct(game_id), ppp = sum(points) / n(), fg_pct = sum(fgm) / sum(fga) * 100, efg_pct = (sum(fgm) + 0.5 * sum(fgm_3)) / sum(fga) * 100, ast_rate = sum(assist) / n() * 100, to_pct = sum(turnover) / n() * 100, .groups = 'drop' ) # Create radar chart data comparison_scaled <- comparison %>% mutate(across(where(is.numeric), ~scale(.)[,1])) %>% pivot_longer(cols = -player_name, names_to = 'metric', values_to = 'value') p <- ggplot(comparison_scaled, aes(x = metric, y = value, group = player_name, color = player_name)) + geom_polygon(alpha = 0.2, linewidth = 1.2) + geom_point(size = 3) + coord_polar() + labs( title = 'Pick-and-Roll Ball Handler Comparison', subtitle = 'Scaled Metrics (Z-scores)', color = 'Player' ) + theme_minimal() + theme( plot.title = element_text(size = 16, face = 'bold', hjust = 0.5), plot.subtitle = element_text(size = 12, hjust = 0.5), axis.text.x = element_text(size = 10, face = 'bold'), legend.position = 'bottom' ) return(list(data = comparison, plot = p)) } # Example usage with simulated data set.seed(123) pnr_simulated <- tibble( player_name = sample(c('Player A', 'Player B', 'Player C'), 500, replace = TRUE), game_id = sample(1:20, 500, replace = TRUE), role = sample(c('ball_handler', 'roll_man'), 500, replace = TRUE), outcome_type = sample(c('shot', 'pass_roller', 'kick_out', 'turnover'), 500, replace = TRUE, prob = c(0.45, 0.20, 0.25, 0.10)), points = sample(0:3, 500, replace = TRUE, prob = c(0.45, 0, 0.35, 0.20)), fga = rbinom(500, 1, 0.6), fgm = rbinom(500, 1, 0.45), fga_3 = rbinom(500, 1, 0.3), fgm_3 = rbinom(500, 1, 0.35), assist = rbinom(500, 1, 0.25), turnover = rbinom(500, 1, 0.10), shot_type = sample(c('dunk', 'layup', 'jumper'), 500, replace = TRUE), shot_distance = abs(rnorm(500, 8, 6)) ) # Calculate metrics pnr_metrics <- calculate_pnr_metrics(pnr_simulated) print(pnr_metrics) # Analyze decisions decisions <- analyze_ball_handler_decisions(pnr_simulated) print(decisions) # Visualize efficiency efficiency_plot <- plot_pnr_efficiency(pnr_simulated) print(efficiency_plot) # Compare players comparison_results <- compare_ball_handlers(pnr_simulated, c('Player A', 'Player B')) print(comparison_results$data) print(comparison_results$plot) ``` ## Advanced Applications ### Machine Learning for Coverage Prediction ```python from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import train_test_split from sklearn.metrics import classification_report, confusion_matrix import pandas as pd import numpy as np def build_coverage_predictor(historical_data): """ Build ML model to predict defensive coverage """ # Feature engineering features = pd.DataFrame({ 'ball_handler_three_pt_pct': historical_data['bh_3p_pct'], 'ball_handler_rim_freq': historical_data['bh_rim_freq'], 'roll_man_ppp': historical_data['rm_ppp'], 'roll_man_dunk_rate': historical_data['rm_dunk_rate'], 'perimeter_shooters': historical_data['shooters_on_floor'], 'game_situation': historical_data['score_differential'], 'defender_switching_ability': historical_data['def_switch_rating'], 'defender_rim_protection': historical_data['def_rim_rating'] }) target = historical_data['coverage_used'] # Split data X_train, X_test, y_train, y_test = train_test_split( features, target, test_size=0.2, random_state=42 ) # Train model model = RandomForestClassifier(n_estimators=100, random_state=42) model.fit(X_train, y_train) # Evaluate predictions = model.predict(X_test) print("Coverage Prediction Accuracy:") print(classification_report(y_test, predictions)) # Feature importance importance_df = pd.DataFrame({ 'feature': features.columns, 'importance': model.feature_importances_ }).sort_values('importance', ascending=False) print("\nFeature Importance:") print(importance_df) return model, importance_df ``` ### Expected Points Added (EPA) for Pick-and-Roll ```python def calculate_pick_and_roll_epa(player_data, league_avg_ppp=0.93): """ Calculate Expected Points Added for pick-and-roll actions """ player_data['expected_points'] = player_data['possessions'] * league_avg_ppp player_data['actual_points'] = player_data['points'] player_data['epa'] = player_data['actual_points'] - player_data['expected_points'] player_data['epa_per_100'] = (player_data['epa'] / player_data['possessions']) * 100 return player_data[['player_name', 'possessions', 'ppp', 'epa', 'epa_per_100']] ``` ## Best Practices ### For Analysts 1. **Context is crucial**: Always consider game situation, opponent quality, and personnel 2. **Sample size matters**: Minimum 50 possessions for reliable individual metrics 3. **Combine metrics**: Don't rely on PPP alone - use efficiency, frequency, and decision-making 4. **Video validation**: Numbers tell part of story; watch film to understand why 5. **Synergy evaluation**: Assess how well players work together, not just individual stats ### For Coaches 1. **Know your personnel**: Match play calls to player strengths 2. **Scout coverage tendencies**: Know what defense will play before running action 3. **Spacing matters**: Always have shooters ready for kick-outs 4. **Practice reads**: Train ball handlers to make correct read vs. each coverage 5. **Versatility wins**: Have multiple roll/pop options with your screener ### For Players **Ball Handlers**: - Master pull-up shooting to counter drop coverage - Develop pocket pass for rolling bigs - Read defender's positioning early **Roll Men**: - Screen angle creates space - Roll hard and with purpose - Develop short roll game as counter ## Conclusion Pick-and-roll analytics provide deep insights into basketball's most fundamental action. By tracking ball handler efficiency, roll man effectiveness, outcome distributions, and defensive coverages, teams can optimize their offensive attack and defensive schemes. Modern tracking data and Synergy classifications have revolutionized our understanding of pick-and-roll basketball, allowing for precise measurement of decision-making quality, synergy between players, and coverage effectiveness. As analytics continue to evolve, pick-and-roll analysis will remain central to team strategy and player evaluation. Success in pick-and-roll basketball requires understanding not just individual metrics, but how players, coverages, and decisions interact to create advantages. The frameworks and code examples provided here offer a foundation for comprehensive pick-and-roll analysis.

Discussion

Have questions or feedback? Join our community discussion on Discord or GitHub Discussions.