Key Takeaways: Advanced Passing Metrics

Quick Reference Card

Core Advanced Metrics

Metric Formula Interpretation
EPA EP After - EP Before Value added per play
CPOE Actual Comp% - Expected Comp% Accuracy above/below expectation
aDOT Total Air Yards / Attempts Average throw depth
IAY/PA Intended Air Yards / Attempts Aggressiveness measure
CAY/PA Completed Air Yards / Attempts Successful downfield passing

Essential Formulas

Expected Points Added (EPA)

def calculate_epa(ep_before: float, ep_after: float) -> float:
    """Calculate Expected Points Added for a play."""
    return ep_after - ep_before

# Special cases:
# Touchdown: EP After = 7.0
# Interception: EP After = -(opponent's EP at their starting position)
# Incomplete: EP After = EP at same yard line, next down

Completion Probability (Simplified)

def completion_probability(air_yards: float,
                           pressure: bool = False,
                           third_down: bool = False) -> float:
    """
    Estimate completion probability.
    Production models include more features.
    """
    # Logistic regression coefficients (simplified)
    log_odds = 2.0  # Intercept
    log_odds -= 0.08 * air_yards
    log_odds -= 0.6 * int(pressure)
    log_odds -= 0.2 * int(third_down)

    # Sigmoid function
    probability = 1 / (1 + np.exp(-log_odds))
    return probability

CPOE Calculation

def calculate_cpoe(passes: List[Dict]) -> Dict:
    """Calculate Completion Percentage Over Expected."""
    expected = sum(p['completion_prob'] for p in passes)
    actual = sum(1 for p in passes if p['completed'])

    expected_pct = expected / len(passes) * 100
    actual_pct = actual / len(passes) * 100

    cpoe = actual_pct - expected_pct

    return {
        'expected_comp_pct': expected_pct,
        'actual_comp_pct': actual_pct,
        'cpoe': cpoe
    }

Air Yards Metrics

def calculate_air_yards_metrics(passes: List[Dict]) -> Dict:
    """Calculate comprehensive air yards metrics."""
    total_air_yards = sum(p['air_yards'] for p in passes)
    completed_air_yards = sum(p['air_yards'] for p in passes if p['completed'])
    completed = [p for p in passes if p['completed']]

    total_yac = sum(p['yards'] - p['air_yards'] for p in completed
                    if p['yards'] > p['air_yards'])
    total_yards = sum(p['yards'] for p in completed)

    return {
        'adot': total_air_yards / len(passes),
        'iay_pa': total_air_yards / len(passes),  # Same as aDOT
        'cay_pa': completed_air_yards / len(passes),
        'avg_yac': total_yac / len(completed) if completed else 0,
        'air_yards_share': completed_air_yards / total_yards * 100 if total_yards else 0
    }

Metric Benchmarks (FBS Quarterbacks)

EPA Benchmarks

Rating EPA per Dropback
Elite > +0.25
Good +0.10 to +0.25
Average -0.05 to +0.10
Below Average -0.15 to -0.05
Poor < -0.15

CPOE Benchmarks

Rating CPOE
Elite > +6%
Good +3% to +6%
Average -2% to +3%
Below Average -5% to -2%
Poor < -5%

Air Yards Benchmarks

Style aDOT Interpretation
Aggressive > 10.0 Deep passing attack
Balanced 8.0 - 10.0 Standard NFL-style
Conservative 6.0 - 8.0 Short/intermediate focus
Check-down heavy < 6.0 Very conservative

Key Concepts to Remember

1. Why Traditional Stats Fall Short

  • Completion % doesn't account for throw difficulty
  • Yards don't consider game situation
  • TD/INT ratio ignores context (garbage time, desperation throws)
  • All traditional stats miss play-by-play value

2. EPA Captures Context

EPA values the same play differently based on: - Down and distance - Field position - Score differential - Time remaining

3. CPOE Reveals True Accuracy

High Comp% + Low CPOE = Easy throws, average accuracy
Low Comp% + High CPOE = Difficult throws, elite accuracy

4. Air Yards Tell the Story

  • High aDOT: Aggressive, takes chances
  • High YAC Share: Short passes, relies on playmakers
  • Low air yards share: Ball placement creates YAC

5. Pressure Changes Everything

Typical performance drops under pressure: - Completion %: -15% to -20% - EPA: Positive → Negative - Must separate QB skill from line quality


Quick Analysis Workflow

Step 1: Calculate Traditional Baseline

traditional = {
    'comp_pct': completions / attempts * 100,
    'ypa': yards / attempts,
    'td_pct': touchdowns / attempts * 100,
    'int_pct': interceptions / attempts * 100
}

Step 2: Add Advanced Context

advanced = {
    'cpoe': actual_comp_pct - expected_comp_pct,
    'epa_per_dropback': total_epa / dropbacks,
    'adot': total_air_yards / attempts,
    'success_rate': positive_epa_plays / total_plays * 100
}

Step 3: Analyze Splits

splits = {
    'clean_pocket': filter_clean_pocket(passes),
    'under_pressure': filter_pressured(passes),
    'early_down': filter_downs(passes, [1, 2]),
    'late_down': filter_downs(passes, [3, 4]),
    'first_half': filter_half(passes, 1),
    'second_half': filter_half(passes, 2)
}

Step 4: Draw Conclusions

  • Compare CPOE to raw completion %
  • Check EPA consistency across situations
  • Identify pressure impact
  • Evaluate depth of target trends

Common Mistakes to Avoid

  1. Using EPA without sample size consideration - Small samples are unreliable - Minimum ~100 dropbacks for stable metrics

  2. Ignoring expected completion probability - A 5-yard checkdown ≠ 5-yard comeback route - Always compare to expectation

  3. Crediting/blaming QB for all passing results - Receivers affect YAC - O-line affects pressure - Play-calling affects opportunity

  4. Comparing across eras without adjustment - Modern passing games produce higher EPA - aDOT varies by scheme

  5. Overweighting single-game EPA - High variance game-to-game - Use rolling averages or season totals


Pandas Quick Reference

import pandas as pd
import numpy as np

# Calculate CPOE for multiple QBs
df['cpoe'] = df['actual_comp_pct'] - df['expected_comp_pct']

# EPA success rate
df['success'] = df['epa'] > 0
success_rate = df.groupby('qb')['success'].mean() * 100

# Split by pressure
clean = df[~df['under_pressure']]
pressured = df[df['under_pressure']]

# Depth zone analysis
df['depth_zone'] = pd.cut(df['air_yards'],
                          bins=[-10, 0, 10, 20, 100],
                          labels=['Behind', 'Short', 'Medium', 'Deep'])
zone_stats = df.groupby('depth_zone').agg({
    'completed': 'mean',
    'yards': 'mean',
    'epa': 'mean'
})

# Rolling EPA (3-game average)
df['rolling_epa'] = df.groupby('qb')['epa_per_db'].rolling(3).mean().values

# Composite ranking
df['cpoe_rank'] = df['cpoe'].rank(ascending=False)
df['epa_rank'] = df['epa_per_db'].rank(ascending=False)
df['composite_rank'] = (df['cpoe_rank'] + df['epa_rank']) / 2

Chapter Summary

Advanced passing metrics provide deeper insight than traditional statistics:

Traditional Advanced Replacement Improvement
Completion % CPOE Accounts for difficulty
Yards EPA Values situation context
TD/INT EPA (captures both) Single value metric
Passer Rating Composite (EPA + CPOE) More predictive

Key Insight: The best evaluation combines multiple advanced metrics with situational splits (pressure, depth, game situation) for complete quarterback assessment.