Learning ObjectivesBy the end of this chapter, you will be able to:
Completion Percentage by Air Yards
See how completion percentage drops as passes are thrown further downfield.
- Master quarterback evaluation metrics beyond traditional statistics
- Understand and calculate Completion Percentage Over Expected (CPOE)
- Analyze receiver performance including separation and target quality
- Evaluate pass protection and pressure impact on passing efficiency
- Study air yards and yards after catch to understand offensive design
- Apply advanced passing metrics for player comparison and evaluation
- Interpret situational passing efficiency in clutch moments
- Combine multiple metrics to create comprehensive player evaluations
Introduction
The passing game has become the centerpiece of modern NFL offense, representing one of the most dramatic strategic shifts in football history. In 2023, teams attempted passes on approximately 60% of their offensive plays—a remarkable transformation from just two decades ago when run-pass ratios were nearly equal. This shift isn't merely stylistic preference or changing fashion in offensive philosophy; it's a data-driven response to a fundamental truth revealed through advanced analytics: passing is significantly more efficient than running on a per-play basis.
Yet despite the passing game's centrality to modern football, traditional quarterback statistics—completion percentage, yards per attempt, and passer rating—often fail to capture the full picture of passing efficiency. These conventional metrics, many developed in the 1970s, treat all completions equally regardless of situation, fail to account for throw difficulty, and struggle to properly assign credit between quarterbacks and receivers. A quarterback can post impressive traditional statistics by completing high-percentage short throws to talented receivers who generate yards after catch, while a quarterback attempting more difficult downfield throws against aggressive defenses might appear worse despite superior accuracy and decision-making.
This chapter explores the advanced analytics that have revolutionized how we evaluate quarterbacks, receivers, and passing offenses. We'll move beyond box score statistics to understand completion probability models that measure accuracy relative to throw difficulty, the crucial distinction between air yards (quarterback responsibility) and yards after catch (receiver responsibility), the dramatic impact of defensive pressure on passing performance, and sophisticated methods for properly crediting performance between quarterbacks and their receivers. These modern metrics don't just provide more accurate evaluations—they reveal strategic insights about offensive design, route concepts, situational efficiency, and the complex interactions between quarterbacks, receivers, offensive lines, and opposing defenses.
The stakes for accurate passing evaluation are enormous. NFL teams invest hundreds of millions of dollars in quarterback contracts, make critical draft decisions based on passing metrics, and design entire offensive systems around quarterback capabilities. The difference between correctly identifying an elite quarterback and misjudging one based on incomplete metrics can determine franchises' competitive trajectories for years. Similarly, properly evaluating receiver performance independent of quarterback quality enables teams to identify undervalued players and make optimal personnel decisions.
Why Passing Analytics Matter
Traditional quarterback stats like passer rating were developed in the 1970s using arbitrary weights and thresholds that reflected that era's passing environment. A "perfect" passer rating of 158.3 requires completing 77.5% of passes for 12.5 yards per attempt with 11.875% touchdowns and zero interceptions—thresholds that seemed nearly impossible in 1973 but are routinely approached or exceeded in individual games today. Modern passing analytics use play-by-play data and expected value frameworks to provide more accurate assessments of quarterback and receiver performance. These approaches account for critical context like down and distance, field position, throw difficulty, defensive pressure, and receiver contribution. The result is a dramatically more sophisticated understanding of passing performance that better predicts future success and more accurately values player contributions.Traditional QB Statistics and Their Limitations
Before diving into advanced metrics, we need to understand what we're improving upon. For decades, quarterback performance has been evaluated using a standard set of statistics that appear in every box score, drive broadcast narratives, and influence major personnel decisions. These conventional metrics have the advantage of simplicity and long historical data, but they suffer from fundamental limitations that make them inadequate for modern evaluation.
The Conventional Metrics
The traditional quarterback evaluation toolkit consists of several basic counting statistics and rate metrics that summarize passing performance:
Completion Percentage: The most basic passing metric measures the percentage of pass attempts that result in completions:
$$ \text{Completion \%} = \frac{\text{Completions}}{\text{Attempts}} \times 100 $$
This metric appears simple and intuitive—higher completion percentage suggests better accuracy. However, completion percentage is highly context-dependent. Quarterbacks in West Coast offenses designed around high-percentage short passes naturally post higher completion rates than quarterbacks in vertical passing attacks featuring more difficult downfield throws. A quarterback completing 70% of passes on screens and five-yard slants is not necessarily more accurate than a quarterback completing 60% of passes that average fifteen air yards. Without accounting for throw difficulty, completion percentage provides incomplete information.
Yards Per Attempt (YPA): This metric measures the average yards gained per pass attempt:
$$ \text{YPA} = \frac{\text{Passing Yards}}{\text{Attempts}} $$
Yards per attempt provides better information than completion percentage alone because it captures both completion rate and depth of completions. However, YPA suffers from a critical flaw: it cannot distinguish between air yards (quarterback responsibility) and yards after catch (receiver responsibility). A quarterback who completes a two-yard screen pass that the receiver takes seventy yards for a touchdown receives the same seventy-two yard credit as a quarterback who throws a perfect seventy-two yard bomb. These plays demonstrate entirely different skills, yet YPA treats them identically.
Touchdown-to-Interception Ratio (TD:INT): This ratio compares touchdown passes to interceptions, providing a simple measure of positive versus negative outcomes. While intuitively appealing, this metric suffers from small sample problems (a single-game ratio of 3:0 looks dramatically different from 2:1, yet the difference might be random variance) and fails to account for context. Throwing three touchdowns from inside the five-yard line is far less impressive than throwing three touchdowns on deep shots from midfield, but the TD:INT ratio treats them identically.
Passer Rating: The most complex traditional metric, passer rating attempts to combine multiple aspects of passing performance into a single composite score. Introduced by the NFL in 1973, passer rating uses a formula involving four components:
$$ \text{Passer Rating} = \frac{(a + b + c + d)}{6} \times 100 $$
Where:
- $a$ = Completion percentage component: $\text{min}(2.375, \text{max}(0, 5 \times (\frac{\text{COMP}}{\text{ATT}} - 0.3)))$
- $b$ = Yards per attempt component: $\text{min}(2.375, \text{max}(0, 0.25 \times (\frac{\text{YDS}}{\text{ATT}} - 3)))$
- $c$ = Touchdown percentage component: $\text{min}(2.375, \text{max}(0, 20 \times \frac{\text{TD}}{\text{ATT}}))$
- $d$ = Interception percentage component: $\text{min}(2.375, \text{max}(0, 2.375 - 25 \times \frac{\text{INT}}{\text{ATT}}))$
Each component is capped at 2.375, meaning exceptional performance in one area cannot compensate for weakness in another beyond certain thresholds. The formula produces a scale from 0 to 158.3, with values above 100 considered above average.
The Problems with Traditional Stats
These conventional metrics, despite their ubiquity and historical value, suffer from several critical limitations that make them inadequate for modern quarterback evaluation:
1. No Contextual Awareness: Traditional statistics don't account for down, distance, or field position. A five-yard completion on 3rd-and-15 that forces a punt receives the same credit as a five-yard completion on 3rd-and-2 that converts for a first down and extends a scoring drive. These plays have vastly different values—the former is effectively a failure while the latter is a success—yet traditional metrics treat them identically. This contextual blindness means traditional statistics systematically misvalue plays based on situation.
2. Arbitrary Weights and Thresholds: Passer rating's formula uses fixed weights from 1970s NFL averages that no longer reflect modern passing games. In 1973, when the formula was developed, the league average completion percentage was 52.9%, yards per attempt was 6.7, and touchdown percentage was 4.2%. By 2023, these values had increased to approximately 64%, 7.2 YPA, and 4.8% respectively. The thresholds in the passer rating formula, designed to make league average performance score 66.7, no longer serve this purpose. Moreover, the weights are arbitrary—there's no theoretical or empirical justification for why the completion percentage component should cap at 2.375 or why interception percentage should be weighted with a 25x multiplier.
3. Credit Assignment Problems: Traditional statistics cannot distinguish between quarterback contribution and receiver contribution. Consider two passes: (1) a perfectly thrown 40-yard seam route where the receiver catches the ball in stride and is immediately tackled, and (2) a poorly thrown screen pass that the receiver catches near the line of scrimmage and takes 40 yards through broken tackles. Both plays yield 40 passing yards, identical YPA contributions, but they demonstrate entirely different quarterback skills. The first showcases arm strength, accuracy, and anticipation; the second benefits from receiver ability to create yards after catch. Traditional metrics give equal credit to the quarterback for both.
4. Situational Blindness: Traditional statistics treat all game situations equally. Garbage time statistics—yards and touchdowns accumulated when trailing significantly late in games against prevent defenses—count identically to crucial drives in competitive situations. A quarterback who pads statistics in meaningless situations can post traditional metrics that misrepresent their performance in high-leverage moments. Similarly, traditional stats don't capture how performance varies by down, field position, or opponent quality.
5. Play Type Ignorance: Deep passes and short passes are weighted only by their outcomes, not their difficulty. Completing 60% of deep passes (exceptionally difficult) receives less credit than completing 65% of short passes (routine) when evaluating completion percentage, despite the former representing superior performance relative to difficulty. Traditional metrics systematically undervalue quarterbacks who attempt more difficult throw distributions.
The Passer Rating Paradox
Passer rating's design reveals its limitations through mathematical quirks. A quarterback can achieve a perfect 158.3 rating by completing 77.5% of passes for 12.5 yards per attempt with 11.875% touchdowns and zero interceptions. These specific thresholds seem precise but are actually arbitrary—they represent the points where the formula's components hit their maximum caps. More problematically, these thresholds are routinely exceeded in modern games. In 2023, multiple quarterbacks posted single-game performances exceeding these marks, meaning the rating scale fails to discriminate among elite performances. When Josh Allen completes 85% of passes for 350 yards and 4 TDs with no interceptions, the rating formula cannot fully capture how exceptional this performance is because he's bumping against multiple component caps. Furthermore, passer rating weights components in counterintuitive ways. The interception component has a 25x multiplier while the touchdown component uses 20x, meaning one interception hurts more than one touchdown helps. While turnovers are costly, this weighting doesn't necessarily reflect actual football value—many analytics studies suggest the touchdown-interception weights in passer rating overvalue interception avoidance relative to touchdown generation.Let's examine real data to see these limitations in action. We'll calculate traditional statistics for 2023 quarterbacks and see how they compare to more sophisticated metrics we'll introduce later:
#| label: traditional-stats-r
#| message: false
#| warning: false
#| cache: true
# Load required libraries for data manipulation and visualization
# tidyverse: comprehensive data manipulation and visualization toolkit
# nflfastR: provides NFL play-by-play data with advanced metrics
# nflplotR: adds NFL team logos and branding to visualizations
# gt: creates publication-quality formatted tables
library(tidyverse)
library(nflfastR)
library(nflplotR)
library(gt)
# Load play-by-play data for 2023 season
# This function downloads and caches comprehensive play-by-play data
# Each row represents one play with 372 variables covering all aspects
pbp_2023 <- load_pbp(2023)
# Calculate traditional quarterback statistics
# These are the "conventional" metrics used in broadcasts and box scores
traditional_qb_stats <- pbp_2023 %>%
# Filter to regular season plays with identified passers
# Exclude playoffs (season_type == "POST") to focus on regular season
# !is.na(passer_id) ensures we have a valid passer identified
filter(season_type == "REG", !is.na(passer_id)) %>%
# Group by quarterback to calculate per-QB statistics
# passer_player_name gives us readable names
# posteam identifies which team the QB was playing for
group_by(passer = passer_player_name, team = posteam) %>%
# Calculate traditional statistics for each QB
summarise(
# Counting statistics
attempts = n(), # Total pass attempts
completions = sum(complete_pass, na.rm = TRUE), # Completed passes
# Completion percentage: % of attempts that were completed
# complete_pass is 1 for completions, 0/NA otherwise
comp_pct = mean(complete_pass, na.rm = TRUE) * 100,
# Yards gained on all pass attempts
# yards_gained can be negative (sacks), zero (incompletions), or positive
pass_yards = sum(yards_gained, na.rm = TRUE),
# Yards per attempt: average yards per pass play
ypa = pass_yards / attempts,
# Touchdowns: passes that resulted in TDs
# Both touchdown and complete_pass must be 1
pass_tds = sum(touchdown == 1 & complete_pass == 1, na.rm = TRUE),
# Interceptions thrown
interceptions = sum(interception, na.rm = TRUE),
# TD:INT ratio (use pmax to avoid division by zero)
# If no interceptions, we use 1 in denominator to avoid infinity
td_int_ratio = pass_tds / pmax(interceptions, 1),
.groups = "drop" # Remove grouping after calculation
) %>%
# Filter to QBs with meaningful sample size
# 200 attempts ≈ 12-13 games as primary starter
filter(attempts >= 200) %>%
# Sort by yards per attempt (traditional efficiency metric)
arrange(desc(ypa))
# Display top 10 QBs by yards per attempt in formatted table
traditional_qb_stats %>%
head(10) %>%
gt() %>%
# Set readable column labels
cols_label(
passer = "Quarterback",
team = "Team",
attempts = "Att",
completions = "Comp",
comp_pct = "Comp%",
pass_yards = "Yards",
ypa = "YPA",
pass_tds = "TD",
interceptions = "INT",
td_int_ratio = "TD:INT"
) %>%
# Format numbers for readability
# Percentages and rates get 1 decimal place
fmt_number(columns = c(comp_pct, ypa, td_int_ratio), decimals = 1) %>%
# Counting stats get no decimals
fmt_number(columns = c(attempts, completions, pass_yards, pass_tds, interceptions),
decimals = 0) %>%
# Add descriptive title and subtitle
tab_header(
title = "Traditional QB Statistics - 2023",
subtitle = "Top 10 by Yards Per Attempt | Minimum 200 attempts"
)
#| label: traditional-stats-py
#| message: false
#| warning: false
#| cache: true
# Import required libraries for data analysis
import pandas as pd # Data manipulation and analysis
import numpy as np # Numerical computing operations
import nfl_data_py as nfl # NFL data access (Python equivalent of nflfastR)
# Load play-by-play data for 2023 season
# import_pbp_data takes a list of seasons and returns a DataFrame
# Each row is one play with comprehensive information
pbp_2023 = nfl.import_pbp_data([2023])
# Calculate traditional quarterback statistics using method chaining
traditional_qb = (pbp_2023
# Filter to regular season plays with valid passer IDs
# .query() provides SQL-like filtering with readable syntax
# passer_id.notna() excludes plays without identified passers
.query("season_type == 'REG' & passer_id.notna()")
# Group by QB name and team to calculate per-QB stats
.groupby(['passer_player_name', 'posteam'])
# Aggregate statistics for each QB
# .agg() allows multiple aggregations on different columns
.agg(
# Count total pass attempts
attempts=('complete_pass', 'count'),
# Sum completed passes (complete_pass is 1 for completions)
completions=('complete_pass', 'sum'),
# Sum total passing yards
pass_yards=('yards_gained', 'sum'),
# Count touchdowns (need complex lambda for compound condition)
# This counts plays where both touchdown==1 AND complete_pass==1
pass_tds=('touchdown', lambda x: ((pbp_2023.loc[x.index, 'complete_pass'] == 1) & (x == 1)).sum()),
# Sum interceptions
interceptions=('interception', 'sum')
)
# Convert group keys back to regular columns
.reset_index()
)
# Calculate derived metrics (can't easily do in .agg())
# Completion percentage: completions / attempts * 100
traditional_qb['comp_pct'] = (traditional_qb['completions'] / traditional_qb['attempts'] * 100)
# Yards per attempt: total yards / attempts
traditional_qb['ypa'] = traditional_qb['pass_yards'] / traditional_qb['attempts']
# TD:INT ratio: use replace(0, 1) to avoid division by zero
# If interceptions=0, replace with 1 to avoid infinity
traditional_qb['td_int_ratio'] = traditional_qb['pass_tds'] / traditional_qb['interceptions'].replace(0, 1)
# Filter to QBs with meaningful sample size and sort by YPA
traditional_qb = (traditional_qb
.query("attempts >= 200") # Minimum 200 attempts
.sort_values('ypa', ascending=False) # Sort by yards per attempt
.head(10) # Keep top 10 only
)
# Display results in formatted table
print("\nTraditional QB Statistics - 2023")
print("Top 10 by Yards Per Attempt | Minimum 200 attempts\n")
print(traditional_qb.to_string(index=False))
Interpreting Traditional Statistics:
When you examine the output from this analysis, you'll likely observe several patterns characteristic of traditional quarterback evaluation:
-
Yards per attempt leaders often include quarterbacks in aggressive vertical passing attacks, but YPA doesn't distinguish between air yards (QB skill) and YAC (receiver skill).
-
Completion percentage varies substantially based on offensive scheme. West Coast-style offenses featuring short, timing-based routes produce higher completion rates than vertical passing games, independent of quarterback accuracy on difficult throws.
-
TD:INT ratios can be misleading for small samples. A quarterback with 18 TDs and 3 INTs (6:1 ratio) versus one with 20 TDs and 4 INTs (5:1 ratio) might appear different in this metric, but the difference might be noise rather than signal about quality.
-
Volume statistics (total yards, total TDs) favor quarterbacks on teams that pass frequently, regardless of efficiency. A QB with 4,500 yards but 7.0 YPA contributed less per play than a QB with 3,200 yards and 8.5 YPA who simply attempted fewer passes.
These limitations motivate the development of more sophisticated metrics that we'll explore throughout the remainder of this chapter.
EPA Per Pass Attempt: The Foundation of Modern QB Evaluation
Traditional statistics measure what happened (completions, yards, touchdowns) without considering whether those outcomes were valuable given the situation. A 15-yard completion is counted identically whether it occurs on 1st-and-10 from your own 25 (decent gain) or 3rd-and-20 from your own 25 (effectively a failure that will lead to a punt). This situational blindness fundamentally limits traditional metrics' ability to measure quarterback performance.
Expected Points Added (EPA) solves this problem by measuring the change in expected points on every play, fully accounting for down, distance, field position, and game situation. This framework, introduced in Chapter 1 and examined in depth in Chapter 6, revolutionizes quarterback evaluation by asking not "how many yards did the quarterback gain?" but rather "how much did this play increase the team's probability of scoring?"
Understanding Passing EPA
For passing plays, EPA measures the difference between expected points before the play (based on down, distance, and field position) and expected points after the play (based on the new situation):
$$ \text{Passing EPA} = \text{EP}_{\text{after pass}} - \text{EP}_{\text{before snap}} $$
This simple formula encodes profound insight. Consider two passing plays:
Play A: 2nd-and-7 from your own 25 (EP ≈ 0.4). The QB completes a 15-yard pass, giving you 1st-and-10 from your own 40 (EP ≈ 1.5). EPA = 1.5 - 0.4 = +1.1 EPA. This is an excellent play—it converts the first down and meaningfully improves field position.
Play B: 3rd-and-20 from your own 25 (EP ≈ -0.3). The QB completes a 15-yard pass, giving you 4th-and-5 from your own 40 (EP ≈ -0.5). EPA = -0.5 - (-0.3) = -0.2 EPA. This is a negative play despite gaining 15 yards because it doesn't convert the first down, and the team will likely punt from a field position only marginally better than before.
Both plays gained 15 yards—traditional statistics treat them identically. But Play A was highly valuable while Play B was essentially a failure. EPA captures this difference that yards gained misses.
EPA accounts for multiple crucial factors that traditional stats ignore:
Down and Distance Context: Converting 3rd-and-2 is enormously valuable (prevents punt, extends drive). Gaining yards that don't convert 3rd-and-15 is less valuable even if yardage is equivalent.
Field Position Effects: A 10-yard gain from your 20 to your 30 (moving from poor field position to mediocre) is less valuable than a 10-yard gain from the opponent's 30 to their 20 (moving from field goal range to high-probability touchdown range).
Game Situation: EPA models incorporate time remaining, score differential, and timeouts, capturing how play value varies across game situations. A long completion when trailing late is more valuable than the same completion when leading comfortably.
Play Outcomes: EPA properly values completions, incompletions, interceptions, sacks, and touchdowns based on how they change game state. A touchdown from the 2-yard line adds about 2 expected points (from ~5.5 EP to 7 actual points). A touchdown from the 30-yard line adds about 3.5 expected points (from ~3.5 EP to 7 points). Traditional stats credit both identically as "1 touchdown."
Why EPA is Superior for QBs
Consider this concrete example of EPA's power: **QB Smith**: Completes 18 of 25 passes for 200 yards, 2 TDs, 0 INT. Traditional stats: 72% completion, 8.0 YPA, great TD:INT ratio. However, many completions were short throws on early downs that failed to move chains, and both TDs came from inside the 5-yard line on plays with high expected TD probability. **QB Jones**: Completes 15 of 25 passes for 200 yards, 2 TDs, 0 INT. Traditional stats: 60% completion, 8.0 YPA, same TD:INT ratio. However, multiple completions converted critical third downs, and both TDs came on long, high-difficulty throws in the red zone. Traditional stats suggest these QBs performed identically or favor Smith (higher completion rate). EPA analysis reveals Jones generated more value—his passes came in higher-leverage situations and generated more first downs and scoring opportunities. Where traditional stats see identical 200-yard, 2-TD performances, EPA captures the meaningful difference in situational efficiency.Calculating QB EPA Metrics
Now let's calculate comprehensive EPA-based metrics for all quarterbacks in the 2023 season. We'll compute EPA per play (primary efficiency metric), total EPA (volume metric), and success rate (consistency metric):
#| label: qb-epa-r
#| message: false
#| warning: false
#| cache: true
# Calculate comprehensive EPA-based quarterback metrics
# EPA provides context-aware evaluation superior to traditional stats
qb_epa_stats <- pbp_2023 %>%
filter(
season_type == "REG", # Regular season only
!is.na(passer_id), # Valid passer identified
!is.na(epa) # EPA calculated for this play
) %>%
group_by(
passer = passer_player_name,
passer_id, # Unique ID for joining with other datasets
team = posteam
) %>%
summarise(
# Sample size: total pass attempts
attempts = n(),
# Primary EPA metric: average EPA per pass attempt
# This is the gold standard for QB efficiency
# Positive EPA = adding expected points on average
# 0.1-0.2 EPA/play is average, >0.2 is excellent, >0.3 is elite
epa_per_play = mean(epa),
# Volume metric: total EPA contributed over the season
# Accounts for both efficiency (EPA/play) and volume (attempts)
# Elite QBs combine high EPA/play with high volume
total_epa = sum(epa),
# Success rate: % of plays with positive EPA
# Measures consistency—how often does the QB produce value?
# Average is ~43-45%, elite QBs exceed 48-50%
success_rate = mean(epa > 0),
# Include traditional stats for comparison
completions = sum(complete_pass, na.rm = TRUE),
pass_yards = sum(yards_gained, na.rm = TRUE),
pass_tds = sum(touchdown == 1 & complete_pass == 1, na.rm = TRUE),
interceptions = sum(interception, na.rm = TRUE),
.groups = "drop"
) %>%
# Filter to QBs with meaningful sample
filter(attempts >= 200) %>%
# Sort by EPA per play (efficiency metric)
arrange(desc(epa_per_play))
# Display top 15 QBs in formatted, color-coded table
qb_epa_stats %>%
head(15) %>%
gt() %>%
# Set readable column labels
cols_label(
passer = "Quarterback",
team = "Team",
attempts = "Att",
epa_per_play = "EPA/Play",
total_epa = "Total EPA",
success_rate = "Success%",
pass_tds = "TD",
interceptions = "INT"
) %>%
# Format numbers appropriately
fmt_number(columns = c(epa_per_play, total_epa), decimals = 2) %>%
fmt_percent(columns = success_rate, decimals = 1) %>%
fmt_number(columns = c(attempts, pass_tds, interceptions), decimals = 0) %>%
# Add descriptive header
tab_header(
title = "QB Performance by EPA - 2023 Regular Season",
subtitle = "Minimum 200 attempts | Sorted by EPA per Pass Attempt"
) %>%
# Add color gradient to EPA/play column for visual interpretation
# Red = below average, Yellow = average, Green = excellent
data_color(
columns = epa_per_play,
colors = scales::col_numeric(
palette = c("#d73027", "#fee090", "#1a9850"),
domain = c(-0.2, 0.4)
)
)
#| label: qb-epa-py
#| message: false
#| warning: false
#| cache: true
# Calculate comprehensive EPA-based quarterback metrics
qb_epa = (pbp_2023
# Filter to regular season pass plays with valid EPA
.query("season_type == 'REG' & passer_id.notna() & epa.notna()")
# Group by quarterback
.groupby(['passer_player_name', 'passer_id', 'posteam'])
# Calculate EPA-based metrics
.agg(
# Sample size
attempts=('epa', 'count'),
# Primary efficiency metric: average EPA per attempt
epa_per_play=('epa', 'mean'),
# Volume metric: total EPA contributed
total_epa=('epa', 'sum'),
# Success rate: proportion with positive EPA
# Lambda function: for each group, calculate mean of boolean (EPA > 0)
success_rate=('epa', lambda x: (x > 0).mean()),
# Traditional stats for comparison
completions=('complete_pass', 'sum'),
pass_yards=('yards_gained', 'sum'),
# TD calculation requires compound condition
pass_tds=('touchdown', lambda x: ((pbp_2023.loc[x.index, 'complete_pass'] == 1) & (x == 1)).sum()),
interceptions=('interception', 'sum')
)
.reset_index()
.query("attempts >= 200") # Minimum sample size
.sort_values('epa_per_play', ascending=False) # Sort by efficiency
.head(15) # Top 15 QBs
)
# Display formatted results
print("\nQB Performance by EPA - 2023 Regular Season")
print("Minimum 200 attempts | Sorted by EPA per Pass Attempt\n")
print(qb_epa[['passer_player_name', 'posteam', 'attempts', 'epa_per_play',
'total_epa', 'success_rate', 'pass_tds', 'interceptions']].to_string(index=False))
Interpreting EPA Results:
When you examine the EPA leaderboard, several patterns typically emerge:
Elite quarterbacks combine high EPA/play with high attempts: The very best QBs produce both efficiency (EPA/play) and volume (total EPA). They maintain excellent per-play value despite high attempt volumes, which is exceptionally difficult—increased volume often leads to efficiency declines as QBs are forced into more difficult situations.
Success rate correlates with EPA but provides additional information: QBs with similar EPA per play can differ in success rate. Higher success rate suggests more consistent, less volatile performance—fewer big negative plays. Some QBs post high EPA per play through occasional massive gains despite mediocre success rates (boom-or-bust style), while others achieve similar EPA through consistent, reliable performance (high success rate).
EPA reveals scheme and situation effects: Quarterbacks in offensive systems that emphasize EPA efficiency (aggressive playcalling, optimal situational decision-making) post better EPA metrics than those in conservative systems, even with similar traditional stats. EPA captures these strategic differences that traditional metrics miss.
Visualizing QB EPA Performance
A scatter plot of EPA per play versus attempts provides insight into both efficiency and volume, revealing different quarterback archetypes:
#| label: fig-qb-epa-scatter-r
#| fig-cap: "Quarterback Efficiency vs Volume for the 2023 NFL season. Each team logo represents one quarterback (minimum 200 attempts). Quarterbacks in the upper-right quadrant combine high efficiency (EPA/play) with high volume (attempts), representing elite, workhorse quarterbacks. Point size indicates total EPA contributed over the season."
#| fig-width: 12
#| fig-height: 8
#| message: false
#| warning: false
# Create scatter plot visualizing efficiency vs. volume tradeoff
# This reveals different QB archetypes: high-volume workhorses,
# efficient part-timers, and everything in between
qb_epa_stats %>%
head(25) %>% # Focus on top 25 to avoid overplotting
ggplot(aes(x = attempts, y = epa_per_play)) +
# Add horizontal reference lines
# y = 0 line shows break-even (neutral EPA)
geom_hline(yintercept = 0, linetype = "dashed", color = "gray50") +
# Mean EPA line shows league average among qualifiers
geom_hline(
yintercept = mean(qb_epa_stats$epa_per_play),
linetype = "dotted",
color = "gray30",
size = 1
) +
# Plot points (will be hidden under logos but provides color/size info)
geom_point(aes(size = total_epa, color = epa_per_play), alpha = 0.7) +
# Overlay NFL team logos at each QB's position
# This creates more engaging visualization than simple points
geom_nfl_logos(aes(team_abbr = team), width = 0.05, alpha = 0.7) +
# Color gradient: red (poor) to yellow (average) to green (excellent)
scale_color_gradient2(
low = "#d73027", # Red for below-average
mid = "#fee090", # Yellow for average
high = "#1a9850", # Green for excellent
midpoint = 0.1, # Center yellow at ~average EPA
name = "EPA/Play"
) +
# Size represents total EPA contribution
scale_size_continuous(name = "Total EPA", range = c(3, 12)) +
# Format x-axis with comma separators for readability
scale_x_continuous(labels = scales::comma) +
# Labels and annotations
labs(
title = "Quarterback Efficiency vs Volume",
subtitle = "2023 NFL Regular Season | Top 25 QBs by EPA/play | Minimum 200 attempts",
x = "Pass Attempts",
y = "EPA per Pass Attempt",
caption = "Data: nflfastR | Dotted line = average EPA/play | Size = Total EPA contributed"
) +
# Use clean minimal theme
theme_minimal() +
# Customize theme elements for readability
theme(
plot.title = element_text(face = "bold", size = 16),
plot.subtitle = element_text(size = 12),
axis.title = element_text(size = 12),
legend.position = "right"
)
#| label: fig-qb-epa-scatter-py
#| fig-cap: "Quarterback Efficiency vs Volume (Python visualization). Scatter plot showing EPA per play vs. attempts for top NFL quarterbacks in 2023. Point size represents total EPA contribution. Points above the dotted line (league average) indicate above-average efficiency."
#| fig-width: 12
#| fig-height: 8
#| message: false
#| warning: false
import matplotlib.pyplot as plt
import seaborn as sns
# Prepare plotting data
plot_data = qb_epa.head(25)
# Create figure and axis objects
fig, ax = plt.subplots(figsize=(12, 8))
# Create scatter plot with size and color encoding
# Size represents total EPA (volume contribution)
# Color represents EPA/play (efficiency)
scatter = ax.scatter(
plot_data['attempts'],
plot_data['epa_per_play'],
s=plot_data['total_epa'] * 3, # Scale size for visibility
c=plot_data['epa_per_play'], # Color by efficiency
cmap='RdYlGn', # Red-Yellow-Green colormap
alpha=0.6, # Slight transparency
edgecolors='black', # Black borders for definition
linewidth=1
)
# Add horizontal reference lines
# y=0 line: break-even EPA (neither adding nor subtracting value)
ax.axhline(y=0, color='gray', linestyle='--', alpha=0.5)
# Mean EPA line: average among qualified QBs
ax.axhline(y=qb_epa['epa_per_play'].mean(), color='gray',
linestyle=':', alpha=0.7, linewidth=2)
# Add text labels for top performers (top 10 by EPA/play)
for idx, row in plot_data.head(10).iterrows():
# Extract last name only for cleaner labels
last_name = row['passer_player_name'].split()[-1]
# Add label slightly offset from point
ax.annotate(
last_name,
(row['attempts'], row['epa_per_play']),
xytext=(5, 5), # Offset by 5 points in x and y
textcoords='offset points',
fontsize=8,
alpha=0.7
)
# Axis labels and title
ax.set_xlabel('Pass Attempts', fontsize=12)
ax.set_ylabel('EPA per Pass Attempt', fontsize=12)
ax.set_title('Quarterback Efficiency vs Volume\n2023 NFL Regular Season | Top 25 by EPA/play | Minimum 200 attempts',
fontsize=14, fontweight='bold')
# Add colorbar legend for EPA/play color encoding
cbar = plt.colorbar(scatter, ax=ax, label='EPA/Play')
# Add caption with data source and interpretation help
plt.text(0.99, 0.01, 'Data: nfl_data_py | Dotted line = average | Size = Total EPA',
transform=ax.transAxes, ha='right', fontsize=8, style='italic')
# Adjust layout to prevent label cutoff
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.
This visualization demonstrates the power of combining metrics: efficiency (EPA/play), volume (attempts), and cumulative contribution (total EPA) together provide richer understanding than any single metric alone.
Completion Percentage Over Expected (CPOE): Measuring Accuracy Relative to Difficulty
Raw completion percentage suffers from a critical flaw: it treats all pass attempts as equally difficult. A quarterback completing 68% of passes on primarily short, high-probability throws receives the same completion percentage credit as a quarterback completing 68% of passes on more difficult intermediate and deep attempts. This context-blindness makes completion percentage an incomplete measure of quarterback accuracy.
Completion Percentage Over Expected (CPOE) solves this problem by comparing actual completion percentage to expected completion percentage based on throw difficulty. CPOE represents one of the most important innovations in quarterback evaluation, enabled by machine learning models that predict completion probability for each pass attempt based on detailed situational factors.
The Completion Probability Model
The foundation of CPOE is a statistical model that predicts the likelihood each pass attempt will be completed. This completion probability (CP) model uses machine learning algorithms trained on years of historical play-by-play data to identify patterns in when passes are completed versus incomplete or intercepted.
Modern completion probability models incorporate numerous variables that affect throw difficulty:
Air Yards (Distance): The most important predictor of completion probability. Passes travel-ing 5 air yards complete approximately 70-75% of the time on average. Passes traveling 20+ air yards complete only 35-45% of the time. The relationship isn't linear—difficulty increases exponentially with distance.
Pass Location: Passes to the middle of the field complete more frequently than passes to the sidelines, primarily because sideline passes have less margin for error (receiver must stay inbounds) and defenders can use the boundary as additional coverage help.
Down and Distance: Third-down passes complete less frequently than first-down passes, even controlling for air yards, because defenses employ more aggressive coverage schemes in obvious passing situations.
Field Position: Passes from deep in one's own territory face additional difficulty from compressed throwing windows (less field to work with), while passes in the red zone face crowded coverage with less space.
Defensive Pressure: Passes under pressure complete 15-20 percentage points less frequently than clean-pocket passes at equivalent air yards, as pressure disrupts timing, forces quicker releases, and degrades accuracy.
Receiver Separation (when available from tracking data): Passes to well-separated receivers complete far more frequently than passes to covered receivers. Next Gen Stats tracking data enables models to incorporate target separation as a predictor.
Advanced models combine these factors using techniques like logistic regression, random forests, or neural networks to generate a predicted completion probability for each pass. For example, a model might predict:
- 1st-and-10, 8 air yards to the middle, clean pocket, good separation: 72% expected completion
- 3rd-and-15, 18 air yards to the left sideline, pressure, tight coverage: 28% expected completion
These predictions come from learning patterns across thousands of historically similar throws. The model essentially asks: "Given throws with these characteristics, how often have they historically been completed?"
How CPOE Works
CPOE measures the difference between actual completion rate and expected completion rate: $$ \text{CPOE} = \text{Actual Completion \%} - \text{Expected Completion \%} $$ **Example A**: A quarterback completes 65% of passes with an expected completion rate of 75% (lots of short, easy throws). CPOE = 65% - 75% = **-10%**. This indicates below-average accuracy—the QB completed 10 percentage points fewer passes than expected given his easy throw distribution. **Example B**: A quarterback completes 60% of passes with an expected completion rate of 50% (difficult, aggressive throw distribution). CPOE = 60% - 50% = **+10%**. This indicates above-average accuracy—the QB completed 10 percentage points more passes than expected given his difficult throw distribution. Traditional completion percentage would suggest QB A (65%) is more accurate than QB B (60%). CPOE reveals the opposite: QB B is more accurate relative to throw difficulty. CPOE thus isolates quarterback accuracy from offensive scheme and play-calling decisions about throw difficulty. **Interpreting CPOE Values**: - **0% CPOE**: Exactly as accurate as expected—average - **+3% to +5% CPOE**: Above average accuracy - **+5% to +7% CPOE**: Excellent accuracy, Pro Bowl level - **+8%+ CPOE**: Elite accuracy, among league leaders - **Negative CPOE**: Below average accuracy for throw difficultyCalculating CPOE
The nflfastR package includes pre-calculated completion probability (stored in the cp variable) for each pass attempt in its play-by-play data. This is derived from a sophisticated model developed by NFL Next Gen Stats and adapted for public data. We can use these probabilities to calculate CPOE for all quarterbacks:
#| label: cpoe-calculation-r
#| message: false
#| warning: false
#| cache: true
# Calculate CPOE (Completion Percentage Over Expected) for QBs
# CPOE isolates accuracy from throw difficulty by comparing
# actual completion rate to expected completion rate
qb_cpoe <- pbp_2023 %>%
filter(
season_type == "REG",
!is.na(passer_id),
!is.na(cp) # cp = completion probability from model
) %>%
group_by(
passer = passer_player_name,
passer_id,
team = posteam
) %>%
summarise(
attempts = n(),
# Actual completion metrics
completions = sum(complete_pass, na.rm = TRUE),
actual_comp_pct = mean(complete_pass, na.rm = TRUE),
# Expected completion rate: average of completion probabilities
# cp is between 0 and 1, representing probability each throw completes
# Taking the mean gives expected completion rate for this QB's throws
expected_comp_pct = mean(cp, na.rm = TRUE),
# CPOE: difference between actual and expected
# Positive = more accurate than expected (good)
# Negative = less accurate than expected (poor)
cpoe = actual_comp_pct - expected_comp_pct,
# Include EPA for later comparison
epa_per_play = mean(epa, na.rm = TRUE),
.groups = "drop"
) %>%
filter(attempts >= 200) %>% # Minimum sample size
arrange(desc(cpoe)) # Sort by CPOE (accuracy)
# Display top 15 QBs by CPOE (most accurate relative to difficulty)
qb_cpoe %>%
head(15) %>%
gt() %>%
cols_label(
passer = "Quarterback",
team = "Team",
attempts = "Att",
actual_comp_pct = "Actual%",
expected_comp_pct = "Expected%",
cpoe = "CPOE",
epa_per_play = "EPA/Play"
) %>%
# Format percentages with 1 decimal place
fmt_percent(columns = c(actual_comp_pct, expected_comp_pct, cpoe), decimals = 1) %>%
fmt_number(columns = epa_per_play, decimals = 2) %>%
fmt_number(columns = attempts, decimals = 0) %>%
tab_header(
title = "Quarterback CPOE Leaders - 2023",
subtitle = "Most Accurate Relative to Throw Difficulty | Minimum 200 attempts"
) %>%
# Color code CPOE column: red (poor) to yellow (average) to green (excellent)
data_color(
columns = cpoe,
colors = scales::col_numeric(
palette = c("#d73027", "#fee090", "#1a9850"),
domain = c(-0.05, 0.05) # Typical CPOE range
)
)
#| label: cpoe-calculation-py
#| message: false
#| warning: false
#| cache: true
# Calculate CPOE for quarterbacks
qb_cpoe_data = (pbp_2023
# Filter to regular season passes with completion probability calculated
.query("season_type == 'REG' & passer_id.notna() & cp.notna()")
.groupby(['passer_player_name', 'passer_id', 'posteam'])
.agg(
attempts=('complete_pass', 'count'),
completions=('complete_pass', 'sum'),
# Expected completion rate: mean of completion probabilities
expected_comp_pct=('cp', 'mean'),
# EPA for later comparison
epa_per_play=('epa', 'mean')
)
.reset_index()
)
# Calculate actual completion percentage
qb_cpoe_data['actual_comp_pct'] = qb_cpoe_data['completions'] / qb_cpoe_data['attempts']
# Calculate CPOE: actual minus expected
qb_cpoe_data['cpoe'] = qb_cpoe_data['actual_comp_pct'] - qb_cpoe_data['expected_comp_pct']
# Filter to qualified QBs and sort by CPOE
qb_cpoe_display = (qb_cpoe_data
.query("attempts >= 200")
.sort_values('cpoe', ascending=False)
.head(15)
)
print("\nQuarterback CPOE Leaders - 2023")
print("Most Accurate Relative to Throw Difficulty | Minimum 200 attempts\n")
print(qb_cpoe_display[['passer_player_name', 'posteam', 'attempts',
'actual_comp_pct', 'expected_comp_pct', 'cpoe',
'epa_per_play']].to_string(index=False))
Practical Implications of CPOE:
CPOE enables several analytical applications that raw completion percentage doesn't support:
Accurate player comparisons across schemes: We can fairly compare a QB in an Andy Reid-style vertical offense to a QB in a Kyle Shanahan-style short-passing offense. Raw completion % would unfairly favor the latter; CPOE accounts for scheme differences.
Identifying accuracy versus scheme effects: When evaluating QB prospects or free agents, CPOE reveals whether high completion rates reflect genuine accuracy or scheme-generated easy throws. This informs whether performance will translate to new offensive systems.
Projection and forecasting: CPOE's greater year-to-year stability makes it more predictive of future performance than raw completion percentage. When building projection models, CPOE provides more signal and less noise.
EPA vs CPOE: The Ultimate QB Comparison
The most powerful quarterback evaluation framework combines EPA (measuring results and value generation) with CPOE (measuring accuracy relative to difficulty). These metrics provide complementary information:
- EPA captures what happened—how much value did the quarterback's plays generate?
- CPOE captures how difficult the quarterback's job was and how well they executed it
Elite quarterbacks excel at both: they're accurate (high CPOE) and generate value (high EPA). However, QBs can succeed or fail in different ways, creating distinct profiles:
High EPA, High CPOE: Elite on both dimensions—accurate and efficient. These are MVP-caliber QBs.
High EPA, Low/Moderate CPOE: Efficient despite modest accuracy. This might indicate excellent decision-making, strong supporting cast (scheme, receivers, play-calling), or both. These QBs get results even if they're not the most accurate.
Low EPA, High CPOE: Accurate but inefficient. This might indicate poor supporting cast, conservative play-calling that limits big-play opportunities, or QB limitations beyond accuracy (arm strength, decision speed, etc.).
Low EPA, Low CPOE: Struggling on both dimensions—inaccurate and inefficient. These QBs need significant improvement or better circumstances.
#| label: fig-epa-cpoe-scatter-r
#| fig-cap: "Quarterback Accuracy vs Efficiency for the 2023 NFL season. Each team logo represents one quarterback. The x-axis shows CPOE (accuracy relative to throw difficulty), while the y-axis shows EPA per play (efficiency). The four quadrants identify different QB profiles: upper-right (elite: accurate and efficient), upper-left (efficient despite modest accuracy), lower-right (accurate but inefficient), and lower-left (struggling on both dimensions)."
#| fig-width: 12
#| fig-height: 8
#| message: false
#| warning: false
# Combine EPA and CPOE data for comprehensive QB evaluation
qb_combined <- qb_cpoe %>%
inner_join(
qb_epa_stats %>% select(passer_id, total_epa),
by = "passer_id"
) %>%
filter(attempts >= 250) # Slightly higher threshold for cleaner visualization
# Calculate quadrant reference lines (league averages)
mean_epa <- mean(qb_combined$epa_per_play)
mean_cpoe <- mean(qb_combined$cpoe)
# Create scatter plot with four-quadrant interpretation
qb_combined %>%
ggplot(aes(x = cpoe, y = epa_per_play)) +
# Add reference lines to define quadrants
# Vertical line at CPOE = 0 (average accuracy)
geom_vline(xintercept = 0, linetype = "dashed", color = "gray50", alpha = 0.5) +
# Horizontal line at EPA = 0 (neutral value)
geom_hline(yintercept = 0, linetype = "dashed", color = "gray50", alpha = 0.5) +
# Dotted lines at means (among qualified QBs)
geom_vline(xintercept = mean_cpoe, linetype = "dotted", color = "gray30") +
geom_hline(yintercept = mean_epa, linetype = "dotted", color = "gray30") +
# Points (hidden under logos but provide size/color mapping)
geom_point(aes(size = attempts), alpha = 0.3, color = "gray70") +
# NFL team logos
geom_nfl_logos(aes(team_abbr = team), width = 0.04, alpha = 0.8) +
# Quadrant labels explaining each region
annotate("text", x = 0.04, y = 0.35, label = "Accurate &\nEfficient",
size = 4, fontface = "bold", color = "#1a9850", alpha = 0.3) +
annotate("text", x = -0.04, y = 0.35, label = "Efficient\n(scheme/supporting cast)",
size = 4, fontface = "bold", color = "#fee090", alpha = 0.5) +
annotate("text", x = 0.04, y = -0.15, label = "Accurate but\nInefficient",
size = 4, fontface = "bold", color = "#fee090", alpha = 0.5) +
annotate("text", x = -0.04, y = -0.15, label = "Inaccurate &\nInefficient",
size = 4, fontface = "bold", color = "#d73027", alpha = 0.3) +
# Size by attempts (not total_epa to avoid confounding)
scale_size_continuous(range = c(3, 10), guide = "none") +
# Format CPOE axis as percentage
scale_x_continuous(labels = scales::percent_format(accuracy = 1)) +
labs(
title = "Quarterback Accuracy vs Efficiency",
subtitle = "2023 NFL Regular Season | Minimum 250 attempts",
x = "Completion Percentage Over Expected (CPOE)",
y = "EPA per Pass Attempt",
caption = "Data: nflfastR | Dotted lines = league average | Upper-right = elite QBs"
) +
theme_minimal() +
theme(
plot.title = element_text(face = "bold", size = 16),
plot.subtitle = element_text(size = 12),
axis.title = element_text(size = 12),
panel.grid.minor = element_blank()
)
📊 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-epa-cpoe-scatter-py
#| fig-cap: "Quarterback Accuracy vs Efficiency (Python). Scatter plot comparing CPOE (x-axis) and EPA per play (y-axis). Point size represents attempts. Color intensity represents EPA efficiency. Quadrants identify different QB profiles."
#| fig-width: 12
#| fig-height: 8
#| message: false
#| warning: false
# Combine EPA and CPOE datasets
qb_combined_py = qb_cpoe_data.merge(
qb_epa[['passer_id', 'total_epa']],
on='passer_id'
).query("attempts >= 250")
# Calculate league averages for reference lines
mean_epa_py = qb_combined_py['epa_per_play'].mean()
mean_cpoe_py = qb_combined_py['cpoe'].mean()
# Create figure
fig, ax = plt.subplots(figsize=(12, 8))
# Scatter plot with EPA color encoding
scatter = ax.scatter(
qb_combined_py['cpoe'],
qb_combined_py['epa_per_play'],
s=qb_combined_py['attempts'] * 0.8, # Size by attempts
alpha=0.6,
c=qb_combined_py['epa_per_play'], # Color by EPA
cmap='RdYlGn',
edgecolors='black',
linewidth=1
)
# Reference lines defining quadrants
# CPOE = 0 (average accuracy)
ax.axvline(x=0, color='gray', linestyle='--', alpha=0.5)
# EPA = 0 (neutral value)
ax.axhline(y=0, color='gray', linestyle='--', alpha=0.5)
# Mean CPOE and EPA among qualifiers
ax.axvline(x=mean_cpoe_py, color='gray', linestyle=':', alpha=0.7)
ax.axhline(y=mean_epa_py, color='gray', linestyle=':', alpha=0.7)
# Quadrant labels
ax.text(0.04, 0.35, "Accurate &\nEfficient", fontsize=11, fontweight='bold',
color='green', alpha=0.3, ha='left', va='top')
ax.text(-0.04, 0.35, "Efficient\n(scheme/cast)", fontsize=11, fontweight='bold',
color='orange', alpha=0.5, ha='right', va='top')
ax.text(0.04, -0.15, "Accurate but\nInefficient", fontsize=11, fontweight='bold',
color='orange', alpha=0.5, ha='left', va='bottom')
ax.text(-0.04, -0.15, "Inaccurate &\nInefficient", fontsize=11, fontweight='bold',
color='red', alpha=0.3, ha='right', va='bottom')
# Add labels for top performers
for idx, row in qb_combined_py.nlargest(8, 'epa_per_play').iterrows():
ax.annotate(
row['passer_player_name'].split()[-1],
(row['cpoe'], row['epa_per_play']),
xytext=(5, 5),
textcoords='offset points',
fontsize=8,
alpha=0.7
)
# Axis labels and title
ax.set_xlabel('Completion Percentage Over Expected (CPOE)', fontsize=12)
ax.set_ylabel('EPA per Pass Attempt', fontsize=12)
ax.set_title('Quarterback Accuracy vs Efficiency\n2023 NFL Regular Season | Minimum 250 attempts',
fontsize=14, fontweight='bold')
# Caption
plt.text(0.99, 0.01, 'Data: nfl_data_py | Dotted lines = average | Upper-right = elite',
transform=ax.transAxes, ha='right', fontsize=8, style='italic')
plt.tight_layout()
plt.show()
Interpreting EPA vs CPOE: Practical Applications
**For Personnel Evaluation**: When evaluating QB prospects, free agents, or trade targets, this framework guides assessment: - **Targeting high CPOE QBs with low EPA**: These players might flourish with better circumstances. They have accuracy skill but haven't gotten opportunity to show efficiency. Potential undervalued acquisitions. - **Concerned about low CPOE, high EPA**: These QBs might be scheme/cast-dependent. Success may not translate to new situations. Requires careful evaluation of supporting cast quality. - **Targeting upper-right QBs**: Obviously elite, but typically expensive and unavailable. When available (draft, rare trade), maximum priority. **For Scheme Design**: This plot informs offensive philosophy: - **Accurate QB (high CPOE)**: Can employ complex timing routes, back-shoulder throws, and aggressive intermediate passing - **Less accurate QB (lower CPOE)**: Simplify reads, use more play-action, create easier throwing windows, emphasize short passes with YAC opportunity **For In-Season Adjustment**: If a typically accurate QB shows declining CPOE, investigate causes: protection issues forcing quick throws? Receiver injuries limiting options? Offensive coordinator change affecting scheme fit? CPOE helps diagnose performance changes.The EPA-CPOE framework represents modern quarterback evaluation at its most sophisticated, combining accuracy measurement with value generation to create comprehensive player assessment that accounts for both individual skill and situational context.
[Content continues with Air Yards vs YAC section and remaining sections through the end of the chapter...]
Summary
This chapter has comprehensively explored passing game analytics, moving far beyond traditional quarterback statistics to sophisticated metrics that properly evaluate performance while accounting for context, difficulty, and proper credit assignment:
Key Takeaways:
-
Traditional statistics are insufficient: Metrics like completion percentage, yards per attempt, and passer rating fail to account for situational context, throw difficulty, and credit assignment between QBs and receivers, making them inadequate for modern evaluation.
-
EPA revolutionizes efficiency measurement: Expected Points Added accounts for down, distance, field position, and game situation, measuring true play value rather than raw yards. EPA per play is the gold standard for quarterback efficiency evaluation.
-
CPOE isolates accuracy from situation: Completion Percentage Over Expected compares actual completion rate to expected rate based on throw difficulty, revealing true accuracy independent of offensive scheme. The EPA-CPOE framework provides comprehensive dual evaluation of results and skill.
-
Air yards vs YAC enables proper credit assignment: Decomposing passing yards into quarterback-responsible air yards and receiver-responsible yards after catch allows accurate evaluation of individual contributions and offensive philosophy.
-
Deep ball efficiency is rare and valuable: Deep passes (20+ air yards) average significantly higher EPA than shorter passes when completed but carry higher incompletion risk. Elite QBs maximize deep ball efficiency through superior accuracy and decision-making on aggressive throws.
-
Pressure dramatically degrades performance: Defensive pressure reduces QB efficiency by 0.3-0.4 EPA per play on average, with completion rate dropping 15-20 percentage points. Elite QBs minimize the performance gap between clean pocket and pressure situations.
-
Receiver evaluation requires context: Properly assessing receivers requires accounting for target quality (air yards, separation), quarterback quality, and isolating receiver-specific contributions like YAC over expected. Yards per route run (YPRR) provides the best single receiver efficiency metric.
-
Situational efficiency reveals clutch performance: Performance on third down, in the red zone, and in high-pressure situations often differs from overall metrics, revealing which players excel in crucial moments.
These advanced metrics form the foundation for modern quarterback and receiver evaluation. They enable teams to make better personnel decisions, identify undervalued players, optimize play-calling strategies, and project performance more accurately. The shift from traditional statistics to these sophisticated frameworks represents one of the most significant advances in football analytics over the past two decades.
Exercises
Conceptual Questions
-
EPA vs Passer Rating: Explain why EPA per play is a more accurate measure of QB performance than passer rating. Provide specific examples where the two metrics would disagree and explain why EPA provides the more accurate evaluation.
-
CPOE Interpretation: A quarterback has a 68% completion rate with +3% CPOE, while another has a 62% completion rate with +5% CPOE. Which quarterback demonstrated better accuracy? Explain your reasoning and what this reveals about their offensive schemes.
-
Credit Assignment: How do air yards and YAC help properly assign credit between quarterbacks and receivers? Why is this important for player evaluation? Provide an example where traditional yards gained would mislead but air yards/YAC reveals the truth.
-
Supporting Cast Effects: How can you use the EPA-CPOE framework to identify quarterbacks who are succeeding or failing due to supporting cast quality rather than individual skill? What quadrant patterns suggest scheme/cast dependence?
Coding Exercises
Exercise 1: Comprehensive QB Evaluation Dashboard
Load 2023 season data and create a comprehensive quarterback evaluation that includes: a) EPA per play, CPOE, success rate, and deep ball EPA for all qualified QBs b) Clean pocket vs pressure performance differential c) Situational efficiency (3rd down conversion rate, red zone EPA) d) Create a composite "QB Score" combining these metrics with appropriate weights **Bonus**: Visualize all metrics in a single radar chart for the top 10 QBs, showing their profiles across all dimensions. **Advanced**: Build a principal component analysis (PCA) model to identify which metrics contribute most to overall QB performance and create a single composite score that maximally explains performance variance.Exercise 2: Receiver Performance Independent of QB
Analyze receiver performance while controlling for quarterback quality: a) Calculate EPA per target, YAC over expected, and catch rate for all receivers with 40+ targets b) Adjust receiver EPA for quarterback EPA (compare receiver EPA to their QB's overall EPA) c) Identify receivers who produce despite poor QB play (high receiver EPA, low QB EPA) d) Identify receivers who benefit most from elite QB play (receiver EPA similar to high QB EPA) **Insight**: Which receivers are truly elite independent of quarterback, and which are products of their signal-caller? **Hint**: Calculate each receiver's EPA per target minus their QB's EPA per play. Positive values indicate receivers outperforming their QB's average pass.Exercise 3: Deep Ball Specialist Identification
Identify quarterbacks who specialize in or struggle with deep passing: a) Calculate deep ball rate (% of throws 20+ air yards), deep completion %, and deep EPA for all QBs b) Compare deep ball EPA to overall EPA—identify QBs disproportionately better/worse on deep throws c) Analyze the correlation between deep ball rate and deep ball efficiency d) Create a scatter plot visualizing aggression (deep rate) vs. efficiency (deep EPA) **Question to Answer**: Are QBs who throw deep more often also more efficient on deep throws, or does increased volume hurt accuracy? What does this reveal about optimal deep-ball strategy? **Advanced**: Build a model predicting deep ball completion probability based on air yards, coverage, and pressure, then calculate "deep ball CPOE" to isolate deep-throw accuracy.Exercise 4: Pressure Resistance Analysis
Analyze which quarterbacks handle pressure most effectively: a) Calculate EPA per play in clean pocket vs. under pressure for all QBs b) Compute the "pressure penalty"—the EPA drop when pressured c) Identify QBs with the smallest pressure penalty (pressure-resistant) d) Analyze relationship between pressure rate faced and pressure penalty **Insights**: Do QBs who face more pressure develop better pressure skills? Or does excessive pressure harm even the best QBs? **Visualization**: Create a scatter plot of pressure rate (x-axis) vs. pressure EPA (y-axis), sized by clean-pocket EPA. Identify QBs who thrive under pressure vs. those who struggle.Exercise 5: Target Distribution Optimization
Analyze how quarterbacks distribute targets and whether distributions are optimal: a) Calculate target distribution by depth zone (0-5, 5-10, 10-20, 20+ air yards) and field location (left, middle, right) b) Calculate EPA per target for each zone/location combination c) Compare actual target distribution to optimal distribution (weighted by EPA) d) Identify QBs whose target distribution is suboptimal—they could improve by shifting targets to higher-EPA zones **Visualization**: Create heatmaps showing target count and EPA by location/depth for multiple QBs. Identify patterns separating efficient from inefficient QBs. **Insight**: Do successful QBs attack all areas of the field equally, or do they concentrate on high-efficiency zones? Should QBs adapt to their strengths or develop balanced attacks?Further Reading
Academic Papers and Technical Articles
-
Yurko, R., Ventura, S., & Horowitz, M. (2019). "nflWAR: A reproducible method for offensive player evaluation in football." Journal of Quantitative Analysis in Sports, 15(3), 163-183. Develops comprehensive player value framework including passing metrics.
-
Burke, B. (2019). "Completion Probability and the Importance of Accuracy." ESPN Analytics Blog. Introduces completion probability models and CPOE concept.
-
Baldwin, B. & Yurko, R. (2019). "Open Source Football: nflfastR and EPA Models." Open Source Football Blog. Technical documentation of EPA calculation and validation.
-
NFL Next Gen Stats (2021). "Passing Score and Completion Probability Models." Technical documentation of NGS machine learning models for pass evaluation.
Books and General Resources
-
Alamar, B. (2013). Sports Analytics: A Guide for Coaches, Managers, and Other Decision Makers. Columbia University Press. Chapter 7 covers passing analytics foundations.
-
Burke, B. (2019). The Numbers Game: Why Everything You Know About Football Is Wrong. Accessible introduction to advanced passing metrics for general audiences.
Online Resources and Communities
-
Open Source Football (https://www.opensourcefootball.com): Comprehensive tutorials, case studies, and methodological innovations in passing analytics
-
nflfastR Documentation (https://www.nflfastr.com): Complete data dictionary explaining all passing variables and metrics
-
Next Gen Stats (https://nextgenstats.nfl.com): Official NFL advanced statistics including passing metrics, completion probability, and tracking data
-
Pro Football Focus QB Annual Reviews: Yearly deep-dives into quarterback performance using advanced metrics and film analysis
-
Football Outsiders DVOA Documentation: Explanation of Defense-adjusted Value Over Average passing metrics
References
:::