Defensive Scheme Analytics

Beginner 10 min read 0 views Nov 27, 2025
# Defensive Scheme Analytics ## Introduction Defensive scheme analytics focuses on evaluating coverage concepts, pressure generation, and overall defensive system effectiveness. Modern analytics allows quantification of scheme success and identification of exploitable tendencies. ## Defensive Concepts ### Coverage Schemes - **Cover 1**: Man coverage, single-high safety - **Cover 2**: Two-deep zone, underneath zones - **Cover 3**: Three-deep zone, four underneath - **Cover 4 (Quarters)**: Four-deep zone, pattern matching - **Cover 6**: Split coverage (Cover 4 + Cover 2) ### Pressure Concepts - **Four-Man Rush**: Standard front, seven in coverage - **Nickel Blitz**: Five rushers, LB or DB - **Zero Blitz**: All-out pressure, no deep safety - **Simulated Pressure**: Show blitz, drop to coverage ### Key Metrics - **EPA Allowed per Play**: Defensive efficiency - **Success Rate Allowed**: Percentage of plays stopping offense - **Pressure Rate**: QB pressures per dropback - **Yards per Coverage**: Yards allowed per coverage snap - **Explosive Plays Allowed**: Plays of 15+ yards given up ## R Analysis with nflfastR ```r library(nflfastR) library(dplyr) library(ggplot2) library(tidyr) # Load play-by-play data pbp <- load_pbp(2023) # Filter to relevant plays plays <- pbp %>% filter( !is.na(defteam), !is.na(epa), play_type %in% c("run", "pass") ) # Defensive EPA analysis defense_epa <- plays %>% group_by(defteam, play_type) %>% summarise( plays = n(), epa_allowed = mean(epa, na.rm = TRUE), success_rate_allowed = mean(success, na.rm = TRUE), explosive_allowed = mean(yards_gained >= 15, na.rm = TRUE), yards_per_play_allowed = mean(yards_gained, na.rm = TRUE), .groups = "drop" ) # Top defenses overall top_defenses <- defense_epa %>% group_by(defteam) %>% summarise( total_plays = sum(plays), avg_epa_allowed = weighted.mean(epa_allowed, plays), avg_success_allowed = weighted.mean(success_rate_allowed, plays), avg_explosive_allowed = weighted.mean(explosive_allowed, plays) ) %>% arrange(avg_epa_allowed) %>% head(10) print("Top 10 Defenses by EPA Allowed:") print(top_defenses) # Pass defense by coverage type (if available in data) # Note: Coverage data often requires additional tracking sources pass_defense <- plays %>% filter(play_type == "pass") %>% group_by(defteam) %>% summarise( dropbacks = n(), epa_per_dropback = mean(epa, na.rm = TRUE), completion_rate = mean(complete_pass, na.rm = TRUE), yards_per_attempt = mean(yards_gained, na.rm = TRUE), sack_rate = mean(sack == 1, na.rm = TRUE), int_rate = mean(interception == 1, na.rm = TRUE), .groups = "drop" ) %>% arrange(epa_per_dropback) print("\nTop 10 Pass Defenses (EPA/dropback):") print(pass_defense %>% head(10)) # Visualize pass defense efficiency top_pass_d <- pass_defense %>% head(15) ggplot(top_pass_d, aes(x = reorder(defteam, epa_per_dropback), y = epa_per_dropback)) + geom_col(fill = "darkred", alpha = 0.7) + geom_hline(yintercept = 0, linetype = "dashed", color = "black") + coord_flip() + labs( title = "Top 15 Pass Defenses - EPA Allowed per Dropback (2023)", x = "Team", y = "EPA Allowed per Dropback", subtitle = "Lower (more negative) is better" ) + theme_minimal() # Run defense analysis run_defense <- plays %>% filter(play_type == "run") %>% group_by(defteam) %>% summarise( carries = n(), epa_per_carry = mean(epa, na.rm = TRUE), yards_per_carry = mean(yards_gained, na.rm = TRUE), stuff_rate = mean(yards_gained <= 0, na.rm = TRUE), explosive_rate = mean(yards_gained >= 10, na.rm = TRUE), .groups = "drop" ) %>% arrange(epa_per_carry) print("\nTop 10 Run Defenses (EPA/carry):") print(run_defense %>% head(10)) # Visualize run defense efficiency ggplot(run_defense, aes(x = yards_per_carry, y = stuff_rate)) + geom_point(aes(size = carries, color = epa_per_carry), alpha = 0.7) + scale_color_gradient2(low = "darkgreen", mid = "white", high = "red", midpoint = 0) + labs( title = "Run Defense Efficiency - Yards per Carry vs Stuff Rate", x = "Yards per Carry Allowed", y = "Stuff Rate (0 or negative yards)", size = "Run Plays", color = "EPA/Carry" ) + theme_minimal() # Third down defense third_down_d <- plays %>% filter(down == 3) %>% group_by(defteam) %>% summarise( attempts = n(), conversion_rate = mean(third_down_converted, na.rm = TRUE), epa_per_play = mean(epa, na.rm = TRUE), avg_distance = mean(ydstogo, na.rm = TRUE), .groups = "drop" ) %>% filter(attempts >= 100) %>% arrange(conversion_rate) print("\nTop 10 Third Down Defenses (Conversion Rate Allowed):") print(third_down_d %>% head(10)) # Pressure impact analysis # Filter to pass plays with pressure data pressure_analysis <- plays %>% filter(play_type == "pass", !is.na(qb_hit)) %>% group_by(defteam, qb_hit) %>% summarise( dropbacks = n(), epa_per_dropback = mean(epa, na.rm = TRUE), completion_rate = mean(complete_pass, na.rm = TRUE), yards_per_attempt = mean(yards_gained, na.rm = TRUE), .groups = "drop" ) %>% pivot_wider( names_from = qb_hit, values_from = c(dropbacks, epa_per_dropback, completion_rate, yards_per_attempt), names_prefix = "hit_" ) # Calculate pressure advantage pressure_advantage <- pressure_analysis %>% mutate( epa_advantage = `epa_per_dropback_hit_1` - `epa_per_dropback_hit_0`, pressure_rate = `dropbacks_hit_1` / (`dropbacks_hit_1` + `dropbacks_hit_0`) * 100 ) %>% arrange(epa_advantage) print("\nTop 10 Teams - Pass Rush Effectiveness (EPA advantage when pressuring):") print(pressure_advantage %>% select(defteam, pressure_rate, epa_advantage, `epa_per_dropback_hit_1`, `epa_per_dropback_hit_0`) %>% head(10)) # Visualize pressure effectiveness ggplot(pressure_advantage, aes(x = pressure_rate, y = epa_advantage)) + geom_point(aes(size = `dropbacks_hit_1`), alpha = 0.6, color = "darkblue") + geom_smooth(method = "lm", se = TRUE, color = "red", linetype = "dashed") + geom_hline(yintercept = 0, linetype = "solid", color = "gray") + labs( title = "Pass Rush Pressure Rate vs Effectiveness - 2023", x = "Pressure Rate (%)", y = "EPA Advantage When Pressuring (vs No Pressure)", size = "Pressures", subtitle = "More negative = better defense" ) + theme_minimal() # Red zone defense redzone_d <- plays %>% filter(yardline_100 <= 20) %>% group_by(defteam, play_type) %>% summarise( plays = n(), epa_per_play = mean(epa, na.rm = TRUE), td_rate = mean(touchdown == 1, na.rm = TRUE), .groups = "drop" ) redzone_summary <- redzone_d %>% group_by(defteam) %>% summarise( total_plays = sum(plays), avg_epa = weighted.mean(epa_per_play, plays), td_rate = weighted.mean(td_rate, plays) ) %>% arrange(avg_epa) print("\nTop 10 Red Zone Defenses (EPA allowed):") print(redzone_summary %>% head(10)) ``` ## Python Implementation ```python import nfl_data_py as nfl import pandas as pd import numpy as np import matplotlib.pyplot as plt import seaborn as sns # Load play-by-play data pbp = nfl.import_pbp_data([2023]) # Filter defensive plays plays = pbp[ (pbp['play_type'].isin(['run', 'pass'])) & (pbp['epa'].notna()) & (pbp['defteam'].notna()) ].copy() # Overall defensive efficiency defense_overall = plays.groupby('defteam').agg({ 'play_id': 'count', 'epa': 'mean', 'success': 'mean', 'yards_gained': 'mean' }).rename(columns={ 'play_id': 'plays', 'epa': 'epa_allowed', 'success': 'success_rate_allowed', 'yards_gained': 'yards_per_play_allowed' }) defense_overall = defense_overall.sort_values('epa_allowed') print("Top 10 Defenses - Overall EPA Allowed:") print(defense_overall.head(10)) # Pass vs run defense split defense_split = plays.groupby(['defteam', 'play_type']).agg({ 'play_id': 'count', 'epa': 'mean', 'yards_gained': 'mean', 'success': 'mean' }).reset_index() defense_split.columns = ['team', 'play_type', 'plays', 'epa_allowed', 'yards_allowed', 'success_allowed'] # Pivot for comparison defense_comparison = defense_split.pivot( index='team', columns='play_type', values=['plays', 'epa_allowed', 'yards_allowed'] ) defense_comparison.columns = ['_'.join(col) for col in defense_comparison.columns] defense_comparison = defense_comparison.reset_index() # Calculate defensive rankings defense_comparison['total_epa_allowed'] = ( defense_comparison['epa_allowed_pass'] * defense_comparison['plays_pass'] + defense_comparison['epa_allowed_run'] * defense_comparison['plays_run'] ) / (defense_comparison['plays_pass'] + defense_comparison['plays_run']) defense_comparison = defense_comparison.sort_values('total_epa_allowed') print("\nDefensive Efficiency - Pass vs Run:") print(defense_comparison[['team', 'epa_allowed_pass', 'epa_allowed_run', 'total_epa_allowed']].head(15)) # Situational defense analysis # First down defense first_down_d = plays[plays['down'] == 1].groupby('defteam').agg({ 'epa': 'mean', 'success': 'mean', 'play_id': 'count' }).rename(columns={ 'epa': 'epa_allowed', 'success': 'success_allowed', 'play_id': 'plays' }) first_down_d = first_down_d[first_down_d['plays'] >= 200].sort_values('epa_allowed') print("\nTop 10 First Down Defenses:") print(first_down_d.head(10)) # Third down defense third_down_d = plays[plays['down'] == 3].groupby('defteam').agg({ 'third_down_converted': 'mean', 'epa': 'mean', 'play_id': 'count' }).rename(columns={ 'third_down_converted': 'conversion_rate', 'epa': 'epa_allowed', 'play_id': 'attempts' }) third_down_d = third_down_d[third_down_d['attempts'] >= 100].sort_values('conversion_rate') print("\nTop 10 Third Down Defenses (Lowest Conversion Rate):") print(third_down_d.head(10)) # Red zone defense redzone_plays = plays[plays['yardline_100'] <= 20].copy() redzone_defense = redzone_plays.groupby('defteam').agg({ 'play_id': 'count', 'epa': 'mean', 'touchdown': 'mean' }).rename(columns={ 'play_id': 'plays', 'epa': 'epa_allowed', 'touchdown': 'td_rate_allowed' }) redzone_defense = redzone_defense[redzone_defense['plays'] >= 50].sort_values('epa_allowed') print("\nTop 10 Red Zone Defenses:") print(redzone_defense.head(10)) # Explosive plays allowed explosive_plays = plays[plays['yards_gained'] >= 15].copy() explosive_defense = explosive_plays.groupby('defteam').agg({ 'play_id': 'count' }).rename(columns={'play_id': 'explosive_plays_allowed'}) total_plays_by_team = plays.groupby('defteam').size() explosive_defense['explosive_rate'] = ( explosive_defense['explosive_plays_allowed'] / total_plays_by_team * 100 ) explosive_defense = explosive_defense.sort_values('explosive_rate') print("\nTop 10 Defenses - Fewest Explosive Plays Allowed:") print(explosive_defense.head(10)) # Visualizations fig, axes = plt.subplots(2, 2, figsize=(16, 12)) # 1. Pass defense vs run defense axes[0, 0].scatter(defense_comparison['epa_allowed_run'], defense_comparison['epa_allowed_pass'], s=100, alpha=0.6, c=defense_comparison['total_epa_allowed'], cmap='RdYlGn_r') axes[0, 0].axhline(0, color='black', linestyle='--', alpha=0.3) axes[0, 0].axvline(0, color='black', linestyle='--', alpha=0.3) axes[0, 0].set_xlabel('EPA per Run Play Allowed') axes[0, 0].set_ylabel('EPA per Pass Play Allowed') axes[0, 0].set_title('Pass vs Run Defense Efficiency') axes[0, 0].grid(alpha=0.3) # 2. Third down defense top_3rd = third_down_d.head(15) axes[0, 1].barh(range(len(top_3rd)), top_3rd['conversion_rate'] * 100, color='darkgreen', alpha=0.7) axes[0, 1].set_yticks(range(len(top_3rd))) axes[0, 1].set_yticklabels(top_3rd.index, fontsize=9) axes[0, 1].set_xlabel('3rd Down Conversion Rate Allowed (%)') axes[0, 1].set_title('Top 15 Third Down Defenses') axes[0, 1].axvline(40, color='red', linestyle='--', alpha=0.5) axes[0, 1].invert_yaxis() # 3. Red zone defense top_rz = redzone_defense.head(15) axes[1, 0].barh(range(len(top_rz)), top_rz['epa_allowed'], color='darkred', alpha=0.7) axes[1, 0].set_yticks(range(len(top_rz))) axes[1, 0].set_yticklabels(top_rz.index, fontsize=9) axes[1, 0].set_xlabel('EPA Allowed per Play') axes[1, 0].set_title('Top 15 Red Zone Defenses') axes[1, 0].axvline(0, color='black', linestyle='--', alpha=0.5) axes[1, 0].invert_yaxis() # 4. Explosive plays allowed top_explosive = explosive_defense.head(15) axes[1, 1].barh(range(len(top_explosive)), top_explosive['explosive_rate'], color='steelblue', alpha=0.7) axes[1, 1].set_yticks(range(len(top_explosive))) axes[1, 1].set_yticklabels(top_explosive.index, fontsize=9) axes[1, 1].set_xlabel('Explosive Play Rate Allowed (%)') axes[1, 1].set_title('Top 15 Defenses - Explosive Plays Allowed') axes[1, 1].invert_yaxis() plt.tight_layout() plt.show() ``` ## Key Insights ### Defensive Scheme Effectiveness - **Cover 3 Success**: Best against intermediate routes, vulnerable deep - **Man Coverage**: Higher variance, elite corners enable aggressive schemes - **Pressure Impact**: QB pressures reduce EPA by ~0.3 per play - **Red Zone**: Zone coverage more effective in compressed field ### Situational Defense - **First Down**: Setting up manageable second down crucial - **Third and Long**: Coverage success rate 70%+ - **Third and Short**: Run defense determines conversion rate - **Two-Minute**: Prevent defense allows highest yards per play ## Resources - [nflfastR documentation](https://www.nflfastr.com/) - [NFL Next Gen Stats](https://nextgenstats.nfl.com/) - [Pro Football Focus](https://www.pff.com/)

Discussion

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