Learning ObjectivesBy the end of this chapter, you will be able to:
- Evaluate pass rush effectiveness beyond sacks
- Understand pressure rate and win rate metrics
- Analyze blitz effectiveness
- Study pass rush timing and disruption
- Combine pass rush with coverage analysis
Introduction
The pass rush is one of the most critical components of modern defensive football. A dominant pass rush can disrupt timing, force turnovers, and mask coverage deficiencies. Yet for decades, pass rush performance has been evaluated primarily through sacks—a metric that captures only a small fraction of a pass rusher's impact.
Consider this: In the 2023 NFL season, teams attempted approximately 18,000 passes but recorded only about 1,200 sacks—roughly 6.7% of all passing plays. This means that evaluating pass rushers solely on sacks ignores 93% of their snaps. A pass rusher who generates consistent pressure, forces quarterbacks into bad throws, and disrupts timing may have far more impact than one who records more sacks but disappears on most plays.
This chapter explores the advanced analytics that have transformed how we evaluate pass rush performance. We'll examine pressure rates, win rates, time to throw, blitz effectiveness, and how pass rush success correlates with coverage performance.
Why Pass Rush Analytics Matter
The difference between a league-average pass rush and an elite one can be worth 10+ points per game. Teams that generate pressure without blitzing force opponents into difficult situations while maintaining coverage advantages. Understanding which players and strategies generate consistent pressure—not just occasional sacks—is essential for building effective defenses.The Limitations of Sack Statistics
Traditional Pass Rush Metrics
Historically, defensive line and edge rusher performance has been evaluated using simple counting statistics:
Sacks: Number of times the quarterback is tackled behind the line of scrimmage
Quarterback Hits: Number of times the pass rusher makes contact with the quarterback
Tackles for Loss: Tackles made behind the line of scrimmage
While these metrics are easily understood and have their place in evaluation, they suffer from critical limitations that make them insufficient for comprehensive pass rush analysis.
Why Sacks Are Misleading
1. Coverage Sacks: Many sacks result not from dominant pass rush but from excellent coverage that forces quarterbacks to hold the ball. A "coverage sack" credits the pass rusher for the secondary's work.
2. Cleanup Sacks: When one rusher draws a double team and creates space for a teammate to reach the quarterback, the "cleanup" sack doesn't reflect the true source of pressure.
3. Situational Bias: Sack opportunities vary dramatically by down, distance, and game script. A pass rusher facing more third-and-long situations will have more sack opportunities.
4. Opportunity Variation: Pass rushers who play more snaps naturally accumulate more sacks, regardless of per-snap efficiency.
5. Variance and Luck: Sacks involve considerable randomness. A quarterback stepping up to avoid pressure may run into a defender who barely engaged. Conversely, a dominant rush may force a quick throw that results in an incompletion.
The Sack Stability Problem
Research has shown that sack totals are highly unstable year-over-year compared to other metrics. A player's sack total in one season predicts only about 30-40% of the variance in their next season's sack total, suggesting that sacks capture substantial randomness rather than pure skill.Let's examine the disconnect between sacks and overall pass rush impact:
#| label: sack-limitations-r
#| message: false
#| warning: false
#| cache: true
library(tidyverse)
library(nflfastR)
library(nflplotR)
library(gt)
# Load 2023 season data
pbp_2023 <- load_pbp(2023)
# Calculate pass rush outcomes
pass_rush_outcomes <- pbp_2023 %>%
filter(
season_type == "REG",
play_type == "pass",
!is.na(qb_hit)
) %>%
summarise(
total_dropbacks = n(),
sacks = sum(sack == 1, na.rm = TRUE),
qb_hits = sum(qb_hit == 1, na.rm = TRUE),
hurries = sum(qb_hit == 0 & qb_scramble == 0 &
(time_to_throw < 2.5 | incomplete_pass == 1), na.rm = TRUE),
sack_rate = mean(sack == 1, na.rm = TRUE),
pressure_events = sacks + qb_hits
)
# Display results
pass_rush_outcomes %>%
gt() %>%
cols_label(
total_dropbacks = "Total Dropbacks",
sacks = "Sacks",
qb_hits = "QB Hits",
hurries = "Hurries",
sack_rate = "Sack Rate",
pressure_events = "Pressure Events"
) %>%
fmt_number(
columns = c(total_dropbacks, sacks, qb_hits, hurries, pressure_events),
decimals = 0,
use_seps = TRUE
) %>%
fmt_percent(columns = sack_rate, decimals = 2) %>%
tab_header(
title = "Pass Rush Outcomes - 2023 NFL Season",
subtitle = "Sacks represent only a small fraction of pass rush impact"
)
#| label: sack-limitations-py
#| message: false
#| warning: false
#| cache: true
import pandas as pd
import numpy as np
import nfl_data_py as nfl
# Load 2023 season data
pbp_2023 = nfl.import_pbp_data([2023])
# Calculate pass rush outcomes
pass_rush_data = pbp_2023.query(
"season_type == 'REG' & play_type == 'pass' & qb_hit.notna()"
)
outcomes = {
'total_dropbacks': len(pass_rush_data),
'sacks': pass_rush_data['sack'].sum(),
'qb_hits': pass_rush_data['qb_hit'].sum(),
'sack_rate': pass_rush_data['sack'].mean(),
'hit_rate': pass_rush_data['qb_hit'].mean()
}
results = pd.DataFrame([outcomes])
print("\nPass Rush Outcomes - 2023 NFL Season")
print("=" * 60)
print(f"Total Dropbacks: {outcomes['total_dropbacks']:,}")
print(f"Sacks: {outcomes['sacks']:,} ({outcomes['sack_rate']:.2%})")
print(f"QB Hits: {outcomes['qb_hits']:,} ({outcomes['hit_rate']:.2%})")
print(f"\nSacks represent only {outcomes['sack_rate']:.2%} of all dropbacks")
Pressure Rate Metrics
Defining Pressure
Pressure is a more comprehensive metric than sacks, capturing all instances where the pass rush disrupts the quarterback:
- Sacks: QB tackled behind the line of scrimmage
- Hits: Defender makes contact with QB during or just after the throw
- Hurries: QB forced to throw earlier than intended or move off his spot
The combined pressure rate provides a much better picture of pass rush effectiveness:
$$ \text{Pressure Rate} = \frac{\text{Sacks + Hits + Hurries}}{\text{Total Pass Rush Snaps}} $$
Individual Pressure Rate
Let's calculate pressure rates for individual pass rushers. While play-by-play data doesn't include individual player pressure data, we can analyze team-level pressure and use this framework for understanding how pressure metrics work:
#| label: team-pressure-rate-r
#| message: false
#| warning: false
#| cache: true
# Calculate team defensive pressure metrics
team_pressure <- pbp_2023 %>%
filter(
season_type == "REG",
play_type == "pass",
!is.na(defteam)
) %>%
group_by(team = defteam) %>%
summarise(
dropbacks = n(),
sacks = sum(sack == 1, na.rm = TRUE),
qb_hits = sum(qb_hit == 1, na.rm = TRUE),
pressures = sacks + qb_hits,
sack_rate = mean(sack == 1, na.rm = TRUE),
pressure_rate = pressures / dropbacks,
epa_allowed = mean(epa, na.rm = TRUE),
success_rate_allowed = mean(success == 1, na.rm = TRUE),
.groups = "drop"
) %>%
arrange(desc(pressure_rate))
# Display top 10 teams
team_pressure %>%
head(10) %>%
left_join(
load_teams() %>% select(team_abbr, team_name),
by = c("team" = "team_abbr")
) %>%
select(team, team_name, dropbacks, sacks, qb_hits,
sack_rate, pressure_rate, epa_allowed) %>%
gt() %>%
cols_label(
team = "Team",
team_name = "Team Name",
dropbacks = "Dropbacks",
sacks = "Sacks",
qb_hits = "QB Hits",
sack_rate = "Sack Rate",
pressure_rate = "Pressure Rate",
epa_allowed = "EPA/Play"
) %>%
fmt_number(columns = c(dropbacks, sacks, qb_hits), decimals = 0) %>%
fmt_percent(columns = c(sack_rate, pressure_rate), decimals = 1) %>%
fmt_number(columns = epa_allowed, decimals = 3) %>%
data_color(
columns = pressure_rate,
colors = scales::col_numeric(
palette = c("#fee090", "#1a9850"),
domain = c(0.10, 0.20)
)
) %>%
tab_header(
title = "Team Pressure Rates - 2023 NFL Season",
subtitle = "Ranked by total pressure rate (sacks + hits)"
)
#| label: team-pressure-rate-py
#| message: false
#| warning: false
#| cache: true
# Calculate team defensive pressure metrics
team_pressure = (pbp_2023
.query("season_type == 'REG' & play_type == 'pass' & defteam.notna()")
.groupby('defteam')
.agg(
dropbacks=('defteam', 'count'),
sacks=('sack', 'sum'),
qb_hits=('qb_hit', 'sum'),
epa_allowed=('epa', 'mean'),
success_rate_allowed=('success', 'mean')
)
.reset_index()
)
team_pressure['pressures'] = team_pressure['sacks'] + team_pressure['qb_hits']
team_pressure['sack_rate'] = team_pressure['sacks'] / team_pressure['dropbacks']
team_pressure['pressure_rate'] = team_pressure['pressures'] / team_pressure['dropbacks']
# Sort and display
team_pressure_sorted = (team_pressure
.sort_values('pressure_rate', ascending=False)
.head(10)
)
print("\nTop 10 Teams by Pressure Rate - 2023 NFL Season")
print("=" * 80)
print(team_pressure_sorted[[
'defteam', 'dropbacks', 'sacks', 'qb_hits',
'sack_rate', 'pressure_rate', 'epa_allowed'
]].to_string(index=False))
Pressure Rate vs Sack Rate
A critical insight from pass rush analytics is that pressure rate is more predictive of future performance and defensive success than sack rate. Let's visualize this relationship:
#| label: fig-pressure-vs-sacks-r
#| fig-cap: "Relationship between pressure rate and sack rate"
#| fig-width: 10
#| fig-height: 7
#| message: false
#| warning: false
# Create scatter plot
team_pressure %>%
ggplot(aes(x = pressure_rate, y = sack_rate)) +
geom_point(aes(size = dropbacks), alpha = 0.6, color = "#0072B2") +
geom_smooth(method = "lm", se = TRUE, color = "#D55E00", linetype = "dashed") +
geom_text(
data = team_pressure %>% filter(pressure_rate > 0.16 | sack_rate > 0.085),
aes(label = team),
size = 3,
vjust = -1
) +
scale_x_continuous(
labels = scales::percent_format(accuracy = 1),
limits = c(0.08, 0.22)
) +
scale_y_continuous(
labels = scales::percent_format(accuracy = 1),
limits = c(0.04, 0.10)
) +
labs(
title = "Team Pressure Rate vs Sack Rate",
subtitle = "2023 NFL Regular Season | Size = Total Dropbacks Faced",
x = "Pressure Rate (Sacks + Hits) / Dropbacks",
y = "Sack Rate",
size = "Dropbacks",
caption = "Data: nflfastR"
) +
theme_minimal() +
theme(
plot.title = element_text(face = "bold", size = 14),
plot.subtitle = element_text(size = 10),
legend.position = "bottom"
)
#| label: fig-pressure-vs-sacks-py
#| fig-cap: "Relationship between pressure rate and sack rate - Python"
#| fig-width: 10
#| fig-height: 7
#| message: false
#| warning: false
import matplotlib.pyplot as plt
import seaborn as sns
from scipy import stats
# Create scatter plot
fig, ax = plt.subplots(figsize=(10, 7))
# Scatter points
scatter = ax.scatter(
team_pressure['pressure_rate'],
team_pressure['sack_rate'],
s=team_pressure['dropbacks'] / 10,
alpha=0.6,
color='#0072B2'
)
# Add regression line
slope, intercept, r_value, p_value, std_err = stats.linregress(
team_pressure['pressure_rate'],
team_pressure['sack_rate']
)
line = slope * team_pressure['pressure_rate'] + intercept
ax.plot(team_pressure['pressure_rate'], line,
color='#D55E00', linestyle='--', label=f'R² = {r_value**2:.3f}')
# Label top teams
top_teams = team_pressure.nlargest(5, 'pressure_rate')
for _, row in top_teams.iterrows():
ax.annotate(
row['defteam'],
(row['pressure_rate'], row['sack_rate']),
xytext=(5, 5),
textcoords='offset points',
fontsize=8
)
ax.set_xlabel('Pressure Rate (Sacks + Hits) / Dropbacks', fontsize=12)
ax.set_ylabel('Sack Rate', fontsize=12)
ax.set_title('Team Pressure Rate vs Sack Rate\n2023 NFL Regular Season',
fontsize=14, fontweight='bold')
ax.xaxis.set_major_formatter(plt.FuncFormatter(lambda y, _: '{:.0%}'.format(y)))
ax.yaxis.set_major_formatter(plt.FuncFormatter(lambda y, _: '{:.0%}'.format(y)))
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
📊 Visualization Output
The code above generates a visualization. To see the output, run this code in your R or Python environment. The resulting plot will help illustrate the concepts discussed in this section.
Win Rate Metrics
Pass Rush Win Rate
Win Rate is a binary metric that credits pass rushers for "winning" against their blocker within a specified time frame (typically 2.5 seconds):
$$ \text{Win Rate} = \frac{\text{Rush Wins}}{\text{Total Rush Attempts}} $$
A "win" is typically defined as:
- Beating the blocker and getting within 2 yards of the quarterback
- Achieving this within 2.5 seconds of the snap
- Regardless of whether a sack, hit, or hurry occurs
Win rate metrics are particularly valuable because they:
1. Isolate individual matchup performance
2. Remove the coverage-sack confound
3. Measure consistency rather than outcome-based events
4. Account for getting double-teamed or chipped
Why 2.5 Seconds?
The 2.5-second threshold represents the average time NFL quarterbacks need to make decisions and release the ball. Pass rushers who consistently win within this window disrupt timing and force quarterbacks into difficult decisions, even if they don't record traditional stats.True Pass Rush Rate vs. ESPN Pass Rush Win Rate
ESPN's proprietary Pass Rush Win Rate (PRWR) metric, derived from Next Gen Stats player tracking data, has become the gold standard for individual pass rusher evaluation. While we can't replicate the exact calculation without player tracking data, we can understand the framework and analyze similar concepts using play-by-play data.
Time to Throw and Pocket Disruption
Time to Throw Impact
Time to Throw (TTT) measures how long the quarterback holds the ball from snap to release. It's influenced by:
- Pass rush effectiveness
- Coverage quality
- Play design
- Quarterback decision-making
Analyzing time to throw helps us understand the relationship between pass rush pressure and quarterback performance:
#| label: time-to-throw-r
#| message: false
#| warning: false
#| cache: true
# Analyze QB performance by time to throw
# Note: nflfastR doesn't include TTT, but we can approximate with play metrics
qb_under_pressure <- pbp_2023 %>%
filter(
season_type == "REG",
play_type == "pass",
!is.na(qb_hit),
!is.na(epa)
) %>%
mutate(
pressure = if_else(qb_hit == 1 | sack == 1, 1, 0)
) %>%
group_by(pressure) %>%
summarise(
plays = n(),
completion_pct = mean(complete_pass, na.rm = TRUE),
yards_per_att = mean(yards_gained, na.rm = TRUE),
epa_per_play = mean(epa, na.rm = TRUE),
success_rate = mean(success == 1, na.rm = TRUE),
int_rate = mean(interception == 1, na.rm = TRUE),
td_rate = mean(touchdown == 1, na.rm = TRUE),
.groups = "drop"
) %>%
mutate(
pressure_status = if_else(pressure == 1, "Under Pressure", "Clean Pocket")
)
# Display comparison
qb_under_pressure %>%
select(pressure_status, plays, completion_pct, yards_per_att,
epa_per_play, success_rate, int_rate) %>%
gt() %>%
cols_label(
pressure_status = "Situation",
plays = "Plays",
completion_pct = "Comp%",
yards_per_att = "Yards/Att",
epa_per_play = "EPA/Play",
success_rate = "Success%",
int_rate = "INT%"
) %>%
fmt_number(columns = plays, decimals = 0, use_seps = TRUE) %>%
fmt_percent(columns = c(completion_pct, success_rate, int_rate), decimals = 1) %>%
fmt_number(columns = c(yards_per_att, epa_per_play), decimals = 2) %>%
data_color(
columns = epa_per_play,
colors = scales::col_numeric(
palette = c("#d73027", "#f7f7f7", "#1a9850"),
domain = c(-0.5, 0.5)
)
) %>%
tab_header(
title = "QB Performance: Clean Pocket vs Under Pressure",
subtitle = "2023 NFL Regular Season"
)
#| label: time-to-throw-py
#| message: false
#| warning: false
#| cache: true
# Analyze QB performance under pressure vs clean pocket
pressure_data = (pbp_2023
.query("season_type == 'REG' & play_type == 'pass' & qb_hit.notna() & epa.notna()")
.assign(pressure=lambda x: ((x['qb_hit'] == 1) | (x['sack'] == 1)).astype(int))
)
qb_pressure_stats = (pressure_data
.groupby('pressure')
.agg(
plays=('epa', 'count'),
completion_pct=('complete_pass', 'mean'),
yards_per_att=('yards_gained', 'mean'),
epa_per_play=('epa', 'mean'),
success_rate=('success', 'mean'),
int_rate=('interception', 'mean'),
td_rate=('touchdown', 'mean')
)
.reset_index()
)
qb_pressure_stats['pressure_status'] = qb_pressure_stats['pressure'].map({
0: 'Clean Pocket',
1: 'Under Pressure'
})
print("\nQB Performance: Clean Pocket vs Under Pressure")
print("=" * 80)
print(qb_pressure_stats[[
'pressure_status', 'plays', 'completion_pct',
'yards_per_att', 'epa_per_play', 'success_rate', 'int_rate'
]].to_string(index=False))
# Calculate impact
clean = qb_pressure_stats[qb_pressure_stats['pressure'] == 0].iloc[0]
pressure = qb_pressure_stats[qb_pressure_stats['pressure'] == 1].iloc[0]
epa_diff = clean['epa_per_play'] - pressure['epa_per_play']
print(f"\nPressure Impact: {epa_diff:.3f} EPA/play reduction")
Visualizing Pressure Impact
#| label: fig-pressure-impact-r
#| fig-cap: "Impact of pressure on QB performance metrics"
#| fig-width: 10
#| fig-height: 6
#| message: false
#| warning: false
# Reshape data for visualization
pressure_comparison <- qb_under_pressure %>%
select(pressure_status, completion_pct, epa_per_play, success_rate, int_rate) %>%
pivot_longer(
cols = -pressure_status,
names_to = "metric",
values_to = "value"
) %>%
mutate(
metric_label = case_when(
metric == "completion_pct" ~ "Completion %",
metric == "epa_per_play" ~ "EPA per Play",
metric == "success_rate" ~ "Success Rate",
metric == "int_rate" ~ "Interception %"
)
)
# Create grouped bar chart
pressure_comparison %>%
ggplot(aes(x = metric_label, y = value, fill = pressure_status)) +
geom_col(position = "dodge", width = 0.7) +
scale_fill_manual(
values = c("Clean Pocket" = "#1a9850", "Under Pressure" = "#d73027")
) +
scale_y_continuous(labels = scales::percent_format(accuracy = 0.1)) +
labs(
title = "Impact of Pass Rush Pressure on QB Performance",
subtitle = "2023 NFL Regular Season",
x = NULL,
y = "Percentage / Rate",
fill = "Pocket Status",
caption = "Data: nflfastR"
) +
theme_minimal() +
theme(
plot.title = element_text(face = "bold", size = 14),
plot.subtitle = element_text(size = 11),
legend.position = "top",
axis.text.x = element_text(angle = 0, hjust = 0.5)
)
📊 Visualization Output
The code above generates a visualization. To see the output, run this code in your R or Python environment. The resulting plot will help illustrate the concepts discussed in this section.
#| label: fig-pressure-impact-py
#| fig-cap: "Impact of pressure on QB performance metrics - Python"
#| fig-width: 10
#| fig-height: 6
#| message: false
#| warning: false
# Prepare data for visualization
metrics_to_plot = ['completion_pct', 'epa_per_play', 'success_rate', 'int_rate']
metric_labels = {
'completion_pct': 'Completion %',
'epa_per_play': 'EPA per Play',
'success_rate': 'Success Rate',
'int_rate': 'Interception %'
}
# Create grouped bar chart
fig, ax = plt.subplots(figsize=(10, 6))
x = np.arange(len(metrics_to_plot))
width = 0.35
clean_values = [qb_pressure_stats[qb_pressure_stats['pressure']==0][m].values[0]
for m in metrics_to_plot]
pressure_values = [qb_pressure_stats[qb_pressure_stats['pressure']==1][m].values[0]
for m in metrics_to_plot]
ax.bar(x - width/2, clean_values, width, label='Clean Pocket', color='#1a9850', alpha=0.8)
ax.bar(x + width/2, pressure_values, width, label='Under Pressure', color='#d73027', alpha=0.8)
ax.set_xlabel('Metric', fontsize=12)
ax.set_ylabel('Percentage / Rate', fontsize=12)
ax.set_title('Impact of Pass Rush Pressure on QB Performance\n2023 NFL Regular Season',
fontsize=14, fontweight='bold')
ax.set_xticks(x)
ax.set_xticklabels([metric_labels[m] for m in metrics_to_plot])
ax.legend(loc='upper right', title='Pocket Status')
ax.yaxis.set_major_formatter(plt.FuncFormatter(lambda y, _: '{:.0%}'.format(y)))
ax.grid(True, alpha=0.3, axis='y')
plt.tight_layout()
plt.show()
Blitz Effectiveness Analysis
Understanding Blitz Strategy
A blitz occurs when the defense sends five or more pass rushers (or, by some definitions, any defender beyond the standard four-man rush). Blitzing represents a high-risk, high-reward strategy:
Benefits:
- Increased pressure rate
- Less time for receivers to get open
- Forces quick decisions
Risks:
- Leaves defenders in man coverage
- Creates one-on-one matchups
- Vulnerable to quick passes and screens
Measuring Blitz Success
Let's analyze blitz effectiveness using EPA and success rate:
#| label: blitz-analysis-r
#| message: false
#| warning: false
#| cache: true
# Analyze blitz effectiveness
# Note: nflfastR includes blitz indicator in some data
blitz_analysis <- pbp_2023 %>%
filter(
season_type == "REG",
play_type == "pass",
!is.na(epa)
) %>%
# Approximate blitz with number of pass rushers if available
# For this example, we'll use defensive personnel as a proxy
mutate(
# Blitz proxy: short passes and quick pressure often indicate blitz
likely_blitz = if_else(
(qb_hit == 1 & time_to_throw < 2.5) |
(sack == 1 & air_yards < 5),
1, 0
)
) %>%
group_by(likely_blitz) %>%
summarise(
plays = n(),
sack_rate = mean(sack == 1, na.rm = TRUE),
pressure_rate = mean(qb_hit == 1 | sack == 1, na.rm = TRUE),
epa_per_play = mean(epa, na.rm = TRUE),
success_rate = mean(success == 1, na.rm = TRUE),
completion_pct = mean(complete_pass, na.rm = TRUE),
yards_per_att = mean(yards_gained, na.rm = TRUE),
big_play_rate = mean(yards_gained >= 20, na.rm = TRUE),
.groups = "drop"
) %>%
mutate(
rush_type = if_else(likely_blitz == 1, "Likely Blitz", "Standard Rush")
)
# Display comparison
blitz_analysis %>%
select(rush_type, plays, sack_rate, pressure_rate, epa_per_play,
success_rate, completion_pct, big_play_rate) %>%
gt() %>%
cols_label(
rush_type = "Rush Type",
plays = "Plays",
sack_rate = "Sack%",
pressure_rate = "Pressure%",
epa_per_play = "EPA/Play",
success_rate = "Success%",
completion_pct = "Comp%",
big_play_rate = "Big Play%"
) %>%
fmt_number(columns = plays, decimals = 0, use_seps = TRUE) %>%
fmt_percent(columns = c(sack_rate, pressure_rate, success_rate,
completion_pct, big_play_rate), decimals = 1) %>%
fmt_number(columns = epa_per_play, decimals = 3) %>%
data_color(
columns = epa_per_play,
colors = scales::col_numeric(
palette = c("#1a9850", "#f7f7f7", "#d73027"),
domain = c(-0.3, 0.3)
)
) %>%
tab_header(
title = "Defensive Performance: Blitz vs Standard Rush",
subtitle = "2023 NFL Regular Season (Approximate Blitz Detection)"
)
#| label: blitz-analysis-py
#| message: false
#| warning: false
#| cache: true
# Analyze blitz effectiveness
# Approximate blitz detection using pressure indicators
blitz_data = (pbp_2023
.query("season_type == 'REG' & play_type == 'pass' & epa.notna()")
.assign(
likely_blitz=lambda x: (
((x['qb_hit'] == 1) | (x['sack'] == 1))
).astype(int)
)
)
blitz_stats = (blitz_data
.groupby('likely_blitz')
.agg(
plays=('epa', 'count'),
sack_rate=('sack', 'mean'),
pressure_rate=('qb_hit', lambda x: ((x == 1) |
(blitz_data.loc[x.index, 'sack'] == 1)).mean()),
epa_per_play=('epa', 'mean'),
success_rate=('success', 'mean'),
completion_pct=('complete_pass', 'mean'),
yards_per_att=('yards_gained', 'mean'),
big_play_rate=('yards_gained', lambda x: (x >= 20).mean())
)
.reset_index()
)
blitz_stats['rush_type'] = blitz_stats['likely_blitz'].map({
0: 'Standard Rush',
1: 'Pressure/Blitz'
})
print("\nDefensive Performance: Standard Rush vs Pressure/Blitz")
print("=" * 90)
print(blitz_stats[[
'rush_type', 'plays', 'sack_rate', 'pressure_rate',
'epa_per_play', 'success_rate', 'completion_pct', 'big_play_rate'
]].to_string(index=False))
Blitz Rate by Down and Distance
Blitz usage varies significantly by down and distance. Let's examine when defenses choose to blitz:
#| label: blitz-by-down-r
#| message: false
#| warning: false
#| cache: true
# Analyze blitz rate by down and distance category
blitz_by_situation <- pbp_2023 %>%
filter(
season_type == "REG",
play_type == "pass",
!is.na(down),
!is.na(epa)
) %>%
mutate(
distance_category = case_when(
ydstogo <= 3 ~ "Short (1-3)",
ydstogo <= 7 ~ "Medium (4-7)",
ydstogo <= 10 ~ "Medium-Long (8-10)",
TRUE ~ "Long (11+)"
),
likely_blitz = if_else(qb_hit == 1 | sack == 1, 1, 0)
) %>%
group_by(down, distance_category) %>%
summarise(
plays = n(),
pressure_rate = mean(likely_blitz),
epa_defense = -mean(epa, na.rm = TRUE),
.groups = "drop"
) %>%
arrange(down, distance_category)
# Visualize blitz rate by situation
blitz_by_situation %>%
mutate(
down = factor(down, levels = 1:4, labels = c("1st", "2nd", "3rd", "4th"))
) %>%
ggplot(aes(x = distance_category, y = pressure_rate, fill = down)) +
geom_col(position = "dodge") +
scale_fill_manual(
values = c("1st" = "#440154", "2nd" = "#31688e",
"3rd" = "#35b779", "4th" = "#fde724")
) +
scale_y_continuous(labels = scales::percent_format()) +
labs(
title = "Pressure Rate by Down and Distance",
subtitle = "2023 NFL Regular Season",
x = "Distance to Go",
y = "Pressure Rate",
fill = "Down",
caption = "Data: nflfastR"
) +
theme_minimal() +
theme(
plot.title = element_text(face = "bold", size = 14),
legend.position = "top",
axis.text.x = element_text(angle = 45, hjust = 1)
)
#| label: blitz-by-down-py
#| message: false
#| warning: false
#| cache: true
# Analyze pressure rate by down and distance
def categorize_distance(ydstogo):
if ydstogo <= 3:
return "Short (1-3)"
elif ydstogo <= 7:
return "Medium (4-7)"
elif ydstogo <= 10:
return "Medium-Long (8-10)"
else:
return "Long (11+)"
situation_data = (pbp_2023
.query("season_type == 'REG' & play_type == 'pass' & down.notna() & epa.notna()")
.assign(
distance_category=lambda x: x['ydstogo'].apply(categorize_distance),
likely_blitz=lambda x: ((x['qb_hit'] == 1) | (x['sack'] == 1)).astype(int)
)
)
blitz_situation = (situation_data
.groupby(['down', 'distance_category'])
.agg(
plays=('epa', 'count'),
pressure_rate=('likely_blitz', 'mean'),
epa_defense=('epa', lambda x: -x.mean())
)
.reset_index()
)
print("\nPressure Rate by Down and Distance")
print("=" * 70)
print(blitz_situation.to_string(index=False))
Four-Man Rush Efficiency
The Gold Standard: Winning with Four
One of the most important concepts in pass rush analytics is four-man rush efficiency—the ability to generate pressure while rushing only four defenders and keeping seven in coverage.
Teams that can consistently pressure with four rushers have a massive advantage:
- Seven defenders in coverage vs five receivers
- More flexibility in coverage schemes
- Better ability to defend multiple routes
- Less vulnerability to screens and draw plays
The Four-Man Rush Premium
Research has shown that teams generating pressure on 35%+ of dropbacks with a four-man rush win significantly more games than teams that rely on blitzing. The correlation between four-man rush efficiency and defensive EPA is approximately -0.65, making it one of the strongest predictors of defensive success.Analyzing Four-Man Rush Success
#| label: four-man-rush-r
#| message: false
#| warning: false
#| cache: true
# Analyze effectiveness with different rush strategies
# Approximate 4-man vs 5+ rushers using defensive personnel
rush_efficiency <- pbp_2023 %>%
filter(
season_type == "REG",
play_type == "pass",
!is.na(defteam),
!is.na(epa)
) %>%
mutate(
# Estimate rush scheme based on defensive personnel
# This is approximate - actual data would come from charting
pressure = if_else(qb_hit == 1 | sack == 1, 1, 0),
rush_type = case_when(
pressure == 1 & yards_gained < 5 ~ "Four-Man Rush",
pressure == 1 ~ "Likely Blitz",
TRUE ~ "Four-Man Rush"
)
) %>%
group_by(defteam, rush_type) %>%
summarise(
plays = n(),
pressure_rate = mean(pressure),
epa_per_play = mean(epa, na.rm = TRUE),
success_allowed = mean(success == 1, na.rm = TRUE),
.groups = "drop"
)
# Calculate team four-man rush efficiency
four_man_efficiency <- rush_efficiency %>%
filter(rush_type == "Four-Man Rush") %>%
arrange(epa_per_play) %>%
head(15)
four_man_efficiency %>%
gt() %>%
cols_label(
defteam = "Team",
plays = "Plays",
pressure_rate = "Pressure%",
epa_per_play = "EPA/Play",
success_allowed = "Success%"
) %>%
fmt_number(columns = plays, decimals = 0, use_seps = TRUE) %>%
fmt_percent(columns = c(pressure_rate, success_allowed), decimals = 1) %>%
fmt_number(columns = epa_per_play, decimals = 3) %>%
data_color(
columns = c(pressure_rate, epa_per_play),
colors = scales::col_numeric(
palette = c("#d73027", "#fee090", "#1a9850"),
domain = NULL
)
) %>%
tab_header(
title = "Four-Man Rush Efficiency - Top 15 Defenses",
subtitle = "2023 NFL Regular Season (Lower EPA = Better Defense)"
)
#| label: four-man-rush-py
#| message: false
#| warning: false
#| cache: true
# Analyze four-man rush efficiency
rush_data = (pbp_2023
.query("season_type == 'REG' & play_type == 'pass' & defteam.notna() & epa.notna()")
.assign(
pressure=lambda x: ((x['qb_hit'] == 1) | (x['sack'] == 1)).astype(int)
)
)
team_rush_efficiency = (rush_data
.groupby('defteam')
.agg(
plays=('epa', 'count'),
pressure_rate=('pressure', 'mean'),
epa_per_play=('epa', 'mean'),
success_allowed=('success', 'mean'),
sack_rate=('sack', 'mean')
)
.reset_index()
.sort_values('epa_per_play')
)
print("\nFour-Man Rush Efficiency - Top 15 Defenses")
print("=" * 80)
print(team_rush_efficiency.head(15)[[
'defteam', 'plays', 'pressure_rate', 'epa_per_play',
'success_allowed', 'sack_rate'
]].to_string(index=False))
Pass Rush Moves and Techniques
Types of Pass Rush Moves
Elite pass rushers employ a diverse arsenal of moves and techniques:
Power Moves:
- Bull rush
- Long arm
- Rip move
Speed Moves:
- Speed rush (around the edge)
- Dip and rip
- Spin move
Finesse Moves:
- Swim move
- Arm over
- Hump move
Combo Moves:
- Speed-to-power
- Inside-outside
- Stutter-and-go
While play-by-play data doesn't capture specific moves, understanding move effectiveness is crucial for advanced pass rush evaluation. Organizations with access to charted data or video analysis can track:
- Move success rate by type
- Effectiveness against different blocker types
- Setup sequences that lead to wins
- Counter-move timing
Edge vs Interior Pressure
Pass rush effectiveness varies significantly by position:
Edge Rushers (DE/OLB):
- Higher sack totals
- More speed-based wins
- Longer paths to quarterback
- Often face chip blocks or slides
Interior Rushers (DT/NT):
- Lower sack totals but critical pressure
- More power-based wins
- Shorter paths to quarterback
- Disrupt passing lanes and QB stepping
Interior Pressure Value
Research shows that interior pressure is more valuable than edge pressure for several reasons: it forces QBs to step up into the pocket (where edge rushers are waiting), disrupts throwing lanes, and typically happens faster due to shorter distance to the QB.Double Teams and Chip Rates
Understanding Blocking Attention
One of the most important context metrics for pass rushers is how much blocking attention they command:
Double Team Rate: Percentage of snaps where two blockers engage the rusher
Chip Rate: Percentage of snaps where a running back or tight end provides extra help
Slide Protection: Percentage of times the offensive line slides protection toward the rusher
Elite pass rushers often face significantly more blocking attention, which:
1. Reduces their individual statistics
2. Creates opportunities for teammates
3. Indicates how much offenses respect their ability
The Double Team Discount
A pass rusher who faces double teams on 40% of snaps but still generates pressure on 15% of snaps is likely more valuable than one who faces single blocks on 90% of snaps with a 20% pressure rate. The former is creating opportunities for teammates while still winning at an elite rate given the circumstances.Measuring Teammate Impact
When evaluating pass rushers, we should also consider how they affect teammate performance:
#| label: team-pressure-impact-r
#| message: false
#| warning: false
#| cache: true
# Analyze how team pressure affects individual opportunities
# This shows the concept even though we can't track individual player attention
team_pressure_distribution <- pbp_2023 %>%
filter(
season_type == "REG",
play_type == "pass",
!is.na(defteam)
) %>%
group_by(defteam) %>%
summarise(
total_dropbacks = n(),
total_sacks = sum(sack == 1, na.rm = TRUE),
total_hits = sum(qb_hit == 1, na.rm = TRUE),
total_pressures = total_sacks + total_hits,
sack_rate = mean(sack == 1, na.rm = TRUE),
# Variance in sack distribution (lower = more distributed)
games = n_distinct(game_id),
.groups = "drop"
) %>%
arrange(desc(total_pressures))
# Show top teams
team_pressure_distribution %>%
head(12) %>%
gt() %>%
cols_label(
defteam = "Team",
total_dropbacks = "Dropbacks",
total_sacks = "Sacks",
total_hits = "Hits",
total_pressures = "Pressures",
sack_rate = "Sack Rate",
games = "Games"
) %>%
fmt_number(
columns = c(total_dropbacks, total_sacks, total_hits, total_pressures),
decimals = 0
) %>%
fmt_percent(columns = sack_rate, decimals = 1) %>%
tab_header(
title = "Team Pass Rush Production - 2023",
subtitle = "Total pressure generation by defense"
)
#| label: team-pressure-impact-py
#| message: false
#| warning: false
#| cache: true
# Analyze team pressure distribution
team_pressure_dist = (pbp_2023
.query("season_type == 'REG' & play_type == 'pass' & defteam.notna()")
.groupby('defteam')
.agg(
total_dropbacks=('defteam', 'count'),
total_sacks=('sack', 'sum'),
total_hits=('qb_hit', 'sum'),
games=('game_id', 'nunique')
)
.reset_index()
)
team_pressure_dist['total_pressures'] = (
team_pressure_dist['total_sacks'] + team_pressure_dist['total_hits']
)
team_pressure_dist['sack_rate'] = (
team_pressure_dist['total_sacks'] / team_pressure_dist['total_dropbacks']
)
team_pressure_sorted = team_pressure_dist.sort_values('total_pressures', ascending=False)
print("\nTeam Pass Rush Production - 2023")
print("=" * 80)
print(team_pressure_sorted.head(12)[[
'defteam', 'total_dropbacks', 'total_sacks', 'total_hits',
'total_pressures', 'sack_rate', 'games'
]].to_string(index=False))
Coverage-Pressure Synergy
The Coverage-Pressure Balance
Effective defenses don't just generate pressure—they coordinate pressure with coverage to maximize disruption. This synergy operates on several levels:
Coverage Creates Sacks:
- Tight coverage forces QBs to hold the ball longer
- More time for pass rush to reach the quarterback
- "Coverage sacks" when no one is open
Pressure Helps Coverage:
- Less time for routes to develop
- Forces quick, inaccurate throws
- Reduces yards after catch opportunities
Complementary Design:
- Man coverage enables more aggressive rushing
- Zone coverage requires faster pressure
- Pattern-match concepts bridge the gap
Measuring Synergy Effects
Let's analyze how pressure and coverage success correlate:
#| label: coverage-pressure-synergy-r
#| message: false
#| warning: false
#| cache: true
# Analyze EPA allowed by pressure and completion status
synergy_analysis <- pbp_2023 %>%
filter(
season_type == "REG",
play_type == "pass",
!is.na(epa),
!is.na(complete_pass)
) %>%
mutate(
pressure = if_else(qb_hit == 1 | sack == 1, 1, 0),
completion_status = case_when(
sack == 1 ~ "Sack",
complete_pass == 1 ~ "Complete",
TRUE ~ "Incomplete"
)
) %>%
group_by(pressure, completion_status) %>%
summarise(
plays = n(),
epa_per_play = mean(epa, na.rm = TRUE),
yards_per_play = mean(yards_gained, na.rm = TRUE),
.groups = "drop"
) %>%
mutate(
pressure_label = if_else(pressure == 1, "Pressure", "No Pressure")
)
# Display the synergy matrix
synergy_analysis %>%
select(pressure_label, completion_status, plays, epa_per_play, yards_per_play) %>%
arrange(pressure_label, completion_status) %>%
gt() %>%
cols_label(
pressure_label = "Pressure",
completion_status = "Outcome",
plays = "Plays",
epa_per_play = "EPA/Play",
yards_per_play = "Yards/Play"
) %>%
fmt_number(columns = plays, decimals = 0, use_seps = TRUE) %>%
fmt_number(columns = c(epa_per_play, yards_per_play), decimals = 2) %>%
data_color(
columns = epa_per_play,
colors = scales::col_numeric(
palette = c("#1a9850", "#f7f7f7", "#d73027"),
domain = c(-3, 2)
)
) %>%
tab_header(
title = "Coverage-Pressure Synergy Matrix",
subtitle = "EPA per play by pressure and completion status - 2023"
)
#| label: coverage-pressure-synergy-py
#| message: false
#| warning: false
#| cache: true
# Analyze synergy between pressure and coverage
def get_outcome(row):
if row['sack'] == 1:
return 'Sack'
elif row['complete_pass'] == 1:
return 'Complete'
else:
return 'Incomplete'
synergy_data = (pbp_2023
.query("season_type == 'REG' & play_type == 'pass' & epa.notna() & complete_pass.notna()")
.assign(
pressure=lambda x: ((x['qb_hit'] == 1) | (x['sack'] == 1)).astype(int),
outcome=lambda x: x.apply(get_outcome, axis=1)
)
)
synergy_matrix = (synergy_data
.groupby(['pressure', 'outcome'])
.agg(
plays=('epa', 'count'),
epa_per_play=('epa', 'mean'),
yards_per_play=('yards_gained', 'mean')
)
.reset_index()
)
synergy_matrix['pressure_label'] = synergy_matrix['pressure'].map({
0: 'No Pressure',
1: 'Pressure'
})
print("\nCoverage-Pressure Synergy Matrix")
print("=" * 70)
print(synergy_matrix[[
'pressure_label', 'outcome', 'plays', 'epa_per_play', 'yards_per_play'
]].sort_values(['pressure_label', 'outcome']).to_string(index=False))
Visualizing the Synergy
#| label: fig-synergy-visualization-r
#| fig-cap: "EPA allowed by pressure and completion status"
#| fig-width: 10
#| fig-height: 6
#| message: false
#| warning: false
synergy_analysis %>%
mutate(
completion_status = factor(
completion_status,
levels = c("Sack", "Incomplete", "Complete")
)
) %>%
ggplot(aes(x = completion_status, y = epa_per_play, fill = pressure_label)) +
geom_col(position = "dodge", width = 0.7) +
geom_hline(yintercept = 0, linetype = "dashed", color = "black") +
scale_fill_manual(
values = c("No Pressure" = "#d73027", "Pressure" = "#1a9850")
) +
labs(
title = "Coverage-Pressure Synergy: EPA per Play",
subtitle = "2023 NFL Regular Season | Lower EPA = Better Defense",
x = "Play Outcome",
y = "EPA per Play (Offense Perspective)",
fill = "Pressure Status",
caption = "Data: nflfastR"
) +
theme_minimal() +
theme(
plot.title = element_text(face = "bold", size = 14),
legend.position = "top"
)
#| label: fig-synergy-visualization-py
#| fig-cap: "EPA allowed by pressure and completion status - Python"
#| fig-width: 10
#| fig-height: 6
#| message: false
#| warning: false
# Prepare data for visualization
synergy_pivot = synergy_matrix.pivot(
index='outcome',
columns='pressure_label',
values='epa_per_play'
)
# Order outcomes
outcome_order = ['Sack', 'Incomplete', 'Complete']
synergy_pivot = synergy_pivot.reindex(outcome_order)
# Create grouped bar chart
fig, ax = plt.subplots(figsize=(10, 6))
x = np.arange(len(outcome_order))
width = 0.35
no_pressure = [synergy_pivot.loc[o, 'No Pressure'] for o in outcome_order]
pressure = [synergy_pivot.loc[o, 'Pressure'] for o in outcome_order]
ax.bar(x - width/2, no_pressure, width, label='No Pressure', color='#d73027', alpha=0.8)
ax.bar(x + width/2, pressure, width, label='Pressure', color='#1a9850', alpha=0.8)
ax.axhline(y=0, color='black', linestyle='--', alpha=0.7)
ax.set_xlabel('Play Outcome', fontsize=12)
ax.set_ylabel('EPA per Play (Offense Perspective)', fontsize=12)
ax.set_title('Coverage-Pressure Synergy: EPA per Play\n2023 NFL Regular Season | Lower EPA = Better Defense',
fontsize=14, fontweight='bold')
ax.set_xticks(x)
ax.set_xticklabels(outcome_order)
ax.legend(title='Pressure Status', loc='upper left')
ax.grid(True, alpha=0.3, axis='y')
plt.tight_layout()
plt.show()
Advanced Pass Rush Evaluation
Building a Comprehensive Pass Rusher Profile
To properly evaluate pass rushers, we need to combine multiple metrics:
- Volume Metrics: Total snaps, total pressures
- Efficiency Metrics: Pressure rate, win rate, sack rate
- Context Metrics: Double team rate, chip rate, opponent quality
- Impact Metrics: EPA impact, teammate performance
- Consistency Metrics: Week-to-week variance, situational performance
Pass Rush EPA Impact
Let's calculate the EPA impact of different pass rush outcomes:
#| label: pass-rush-epa-impact-r
#| message: false
#| warning: false
#| cache: true
# Calculate EPA impact of different pressure outcomes
epa_impact <- pbp_2023 %>%
filter(
season_type == "REG",
play_type == "pass",
!is.na(epa)
) %>%
mutate(
outcome_type = case_when(
sack == 1 ~ "Sack",
qb_hit == 1 & incomplete_pass == 1 ~ "Hit + Incompletion",
qb_hit == 1 & complete_pass == 1 ~ "Hit + Completion",
incomplete_pass == 1 ~ "Clean Incompletion",
complete_pass == 1 ~ "Clean Completion",
TRUE ~ "Other"
)
) %>%
group_by(outcome_type) %>%
summarise(
plays = n(),
avg_epa = mean(epa, na.rm = TRUE),
median_epa = median(epa, na.rm = TRUE),
sd_epa = sd(epa, na.rm = TRUE),
success_rate = mean(success == 1, na.rm = TRUE),
.groups = "drop"
) %>%
arrange(avg_epa)
epa_impact %>%
gt() %>%
cols_label(
outcome_type = "Outcome Type",
plays = "Plays",
avg_epa = "Mean EPA",
median_epa = "Median EPA",
sd_epa = "SD EPA",
success_rate = "Success%"
) %>%
fmt_number(columns = plays, decimals = 0, use_seps = TRUE) %>%
fmt_number(columns = c(avg_epa, median_epa, sd_epa), decimals = 3) %>%
fmt_percent(columns = success_rate, decimals = 1) %>%
data_color(
columns = avg_epa,
colors = scales::col_numeric(
palette = c("#1a9850", "#fee090", "#d73027"),
domain = c(-3, 2)
)
) %>%
tab_header(
title = "EPA Impact of Pass Rush Outcomes",
subtitle = "2023 NFL Regular Season | Defense Perspective (Lower = Better)"
)
#| label: pass-rush-epa-impact-py
#| message: false
#| warning: false
#| cache: true
# Calculate EPA impact by outcome type
def categorize_outcome(row):
if row['sack'] == 1:
return 'Sack'
elif row['qb_hit'] == 1 and row['incomplete_pass'] == 1:
return 'Hit + Incompletion'
elif row['qb_hit'] == 1 and row['complete_pass'] == 1:
return 'Hit + Completion'
elif row['incomplete_pass'] == 1:
return 'Clean Incompletion'
elif row['complete_pass'] == 1:
return 'Clean Completion'
else:
return 'Other'
epa_impact_data = (pbp_2023
.query("season_type == 'REG' & play_type == 'pass' & epa.notna()")
.assign(outcome_type=lambda x: x.apply(categorize_outcome, axis=1))
)
epa_impact_summary = (epa_impact_data
.groupby('outcome_type')
.agg(
plays=('epa', 'count'),
avg_epa=('epa', 'mean'),
median_epa=('epa', 'median'),
sd_epa=('epa', 'std'),
success_rate=('success', 'mean')
)
.reset_index()
.sort_values('avg_epa')
)
print("\nEPA Impact of Pass Rush Outcomes")
print("=" * 80)
print(epa_impact_summary.to_string(index=False))
Case Study: Evaluating Elite Pass Rushers
Comparative Analysis Framework
When evaluating pass rushers, consider:
Tier 1 - Elite (Top 5%):
- Pressure rate > 15% on four-man rushes
- Consistent production across situations
- Commands double teams but still wins
- Elevates teammate performance
Tier 2 - Above Average (Top 25%):
- Pressure rate 10-15% on four-man rushes
- Good situational performance
- Occasional double teams
- Solid contributor to team success
Tier 3 - Average (25-75%):
- Pressure rate 6-10% on four-man rushes
- Limited impact beyond base situations
- Rarely draws extra attention
- Neutral impact on team performance
Tier 4 - Below Average (Bottom 25%):
- Pressure rate < 6% on four-man rushes
- Inconsistent production
- Liability in key situations
- Negative impact on defensive efficiency
Summary
Pass rush analytics has evolved far beyond counting sacks. Modern evaluation requires understanding:
- Pressure rate as the primary efficiency metric
- Win rate for measuring individual matchup performance
- Context including double teams, chips, and opponent quality
- Four-man rush efficiency as a cornerstone of defensive success
- Blitz effectiveness through EPA and success rate frameworks
- Coverage-pressure synergy and complementary effects
- Situational performance across downs, distances, and game states
The most effective defenses generate consistent pressure with four rushers, allowing them to maintain coverage advantages while disrupting the passing game. Elite pass rushers create opportunities for teammates while still producing individually despite extra blocking attention.
Exercises
Conceptual Questions
-
Sack Limitations: Explain why a pass rusher with 8 sacks might be less valuable than one with 6 sacks. What additional metrics would you examine?
-
Four-Man Rush Value: Why is generating pressure with four rushers more valuable than generating the same pressure rate with five or more rushers?
-
Coverage Sacks: Describe how tight coverage contributes to sacks. How would you quantify this effect?
-
Interior vs Edge: Discuss the different values provided by interior and edge pass rushers. Which position's pressure is typically more disruptive and why?
Coding Exercises
Exercise 1: Team Pass Rush Efficiency
Load play-by-play data for 2022 and 2023 seasons and: a) Calculate each team's pressure rate for both seasons b) Examine year-over-year stability (correlation) c) Compare pressure rate stability to sack rate stability d) Create a scatter plot showing 2022 vs 2023 pressure rates **Hint**: Use correlation coefficients to measure stability.Exercise 2: Blitz Effectiveness Analysis
Analyze blitz effectiveness by game situation: a) Calculate blitz rate by down and distance categories b) Compare EPA per play for blitzes vs four-man rushes c) Identify which downs/distances favor blitzing d) Create visualizations showing optimal blitz situations **Challenge**: Account for score differential and time remaining.Exercise 3: Pressure-Coverage Interaction
Examine how pressure and coverage interact: a) Calculate completion percentage with/without pressure b) Measure yards per completion in each scenario c) Analyze EPA per play for all combinations of pressure and completion d) Visualize the interaction effects **Advanced**: Include pass depth categories in your analysis.Exercise 4: Building a Pass Rush Metric
Create your own composite pass rush metric: a) Combine pressure rate, sack rate, and EPA impact b) Weight each component based on importance c) Calculate team rankings using your metric d) Validate against win totals or other success metrics **Hint**: Consider standardizing metrics before combining.Further Reading
- PFF Pass Rush Win Rate: Understanding the methodology and applications
- Next Gen Stats Pressure Metrics: How player tracking data enhances pass rush evaluation
- Baldwin, B.: "The Case Against Using Sacks to Evaluate Pass Rushers"
- Pro Football Focus: Annual pass rush analysis and player grades
- Sports Info Solutions: Pass rush charting methodology
References
:::