Defensive Scheme Analytics
Beginner
10 min read
1 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.
Table of Contents
Related Topics
Quick Actions