Key Takeaways: Defensive Analytics

One-page reference for Chapter 10 concepts


The Defensive Attribution Challenge

Challenge Impact
11-player coordination Credit/blame shared
Weak link determines outcome Best player may not matter
Dependent on offensive choices Coverage depends on targets
High variance events INTs, fumbles are noisy

Team-Level Defensive Metrics

# EPA Allowed (core metric)
defense_epa = (plays
    .groupby('defteam')
    .agg(
        epa_allowed=('epa', 'mean'),
        success_allowed=('epa', lambda x: (x > 0).mean())
    )
)
# Lower (more negative) is better

EPA Allowed Interpretation

EPA/Play Interpretation
< -0.10 Elite
-0.10 to -0.05 Above average
-0.05 to 0.00 Average
0.00 to 0.05 Below average
> 0.05 Poor

Pass Defense Metrics

# Pressure rate estimation
passes['pressured'] = (
    (passes['sack'] == 1) |
    (passes['qb_hit'] == 1) |
    (passes['qb_scramble'] == 1)
)
pressure_rate = passes['pressured'].mean()

# Coverage (team level)
comp_pct_allowed = non_sacks['complete_pass'].mean()
yards_per_target = non_sacks['yards_gained'].mean()

Run Defense Metrics

# Stuff rate (runs at/behind LOS)
stuff_rate = (rushes['yards_gained'] <= 0).mean()

# YPC allowed
ypc_allowed = rushes['yards_gained'].mean()

# Explosive prevention
explosive_10_rate = (rushes['yards_gained'] >= 10).mean()

Sack Rate vs Pressure Rate

Metric Measures Limitation
Sack Rate Actual takedowns High variance
Pressure Rate All disruptions Includes QB escapes
QB Hit Rate Physical contact Doesn't include hurries

Situational Defense

# Third down
third_conv_allowed = (third_downs
    .groupby('defteam')
    ['third_down_converted']
    .mean()
)

# Red zone
rz_td_rate = (red_zone
    .groupby('defteam')
    ['touchdown']
    .mean()
)

Turnover Analysis

Key insight: Turnovers are largely random

  • Year-to-year INT rate correlation: ~0.25
  • Fumble recovery is nearly random (~50% rate)
  • Don't overweight turnover metrics
turnover_rate = (
    plays['interception'] | plays['fumble_lost']
).mean()

Opponent Adjustment

# Simple adjustment
off_quality = plays.groupby('posteam')['epa'].mean()
expected_epa = plays.groupby('defteam').agg(
    expected=('opp_offense_quality', 'mean')
)
adjusted_epa = raw_epa - expected_epa

Data Source Limitations

Metric Available In
EPA allowed Standard PBP
Sacks, hits Standard PBP
Individual coverage PFF (subscription)
Pressures allowed Film charting
Coverage targets Tracking data
Win rate ESPN/NFL

Individual Evaluation Challenge

What we CAN'T measure from PBP: - Who was supposed to cover whom - Pass rush wins vs losses - Positioning and angles - Assignment correctness

Requires: PFF grades, SIS charting, or tracking data


Pass/Run Defense Split

pass_epa = passes.groupby('defteam')['epa'].mean()
run_epa = rushes.groupby('defteam')['epa'].mean()

# Positive = better pass defense
balance = run_epa - pass_epa

Common Pitfalls

Pitfall Better Approach
Over-weighting turnovers Focus on EPA/success rate
Individual stats from PBP Use film services
Ignoring game script Filter to neutral situations
Raw stats without context Opponent-adjust

Deep Pass Defense

deep_passes = passes[passes['air_yards'] >= 15]
deep_comp_pct = deep_passes.groupby('defteam')['complete_pass'].mean()
# High-value area to defend

YAC Allowed

# Reflects tackling/pursuit
yac_allowed = (completions
    .groupby('defteam')
    ['yards_after_catch']
    .mean()
)

Evaluation Framework

1. Overall
   - EPA per play allowed
   - Success rate allowed

2. Pass Defense
   - Pass EPA allowed
   - Sack/pressure rate
   - Comp % allowed

3. Run Defense
   - Run EPA allowed
   - Stuff rate
   - YPC allowed

4. Situational
   - 3rd down conversion
   - Red zone TD rate

5. Context
   - Opponent adjustment
   - Game script filter

Key Limitations

  • Individual attribution nearly impossible
  • Turnovers are high variance
  • Scheme effects not captured
  • QB/WR quality affects results
  • Sample sizes per player small

Preview: Chapter 11

Next: Special Teams Analytics - evaluating kicking, punting, and returns using EPA frameworks.