Not all plays matter equally. A first down run in the second quarter of a blowout has far less impact than a third down pass in the final two minutes of a tie game. Understanding how teams perform in high-leverage situations reveals clutch...
In This Chapter
Chapter 14: Situational Football
Analyzing performance in critical game situations
Introduction
Not all plays matter equally. A first down run in the second quarter of a blowout has far less impact than a third down pass in the final two minutes of a tie game. Understanding how teams perform in high-leverage situations reveals clutch performance, coaching acumen, and true competitive quality.
This chapter explores: - Red zone efficiency - scoring inside the opponent's 20 - Third down conversions - the critical "money down" - Two-minute offense - hurry-up execution - Goal-to-go situations - short-field scoring - Late and close games - performance under pressure - Clutch vs choke patterns - does "clutch" exist?
These situational metrics help identify teams that consistently execute when it matters most.
Red Zone Efficiency
Defining the Red Zone
The "red zone" traditionally refers to the area inside the opponent's 20-yard line. Once an offense reaches this territory, the field compresses, defensive coverage tightens, and scoring becomes the primary objective.
import pandas as pd
import numpy as np
import nfl_data_py as nfl
# Load data
pbp = nfl.import_pbp_data([2023])
# Identify red zone plays
red_zone_plays = pbp[
(pbp['yardline_100'] <= 20) &
(pbp['play_type'].isin(['pass', 'run'])) &
(pbp['epa'].notna())
].copy()
print(f"Red zone plays: {len(red_zone_plays):,}")
print(f"Percentage of all plays: {len(red_zone_plays)/len(pbp)*100:.1f}%")
Red Zone Scoring Rate
The primary red zone metric is touchdown conversion rate:
def calculate_red_zone_efficiency(pbp: pd.DataFrame, team: str) -> dict:
"""Calculate comprehensive red zone metrics for a team."""
# Get all drives that reached the red zone
drives = pbp[
(pbp['posteam'] == team) &
(pbp['yardline_100'] <= 20)
].groupby(['game_id', 'drive']).agg(
reached_rz=('yardline_100', 'min'),
scored_td=('touchdown', 'max'),
scored_fg=('field_goal_result', lambda x: (x == 'made').any()),
turnover=('interception', lambda x: (x == 1).any() | (pbp.loc[x.index, 'fumble_lost'] == 1).any())
).reset_index()
# Filter to drives that actually reached RZ
rz_drives = drives[drives['reached_rz'] <= 20]
total_trips = len(rz_drives)
touchdowns = rz_drives['scored_td'].sum()
field_goals = rz_drives['scored_fg'].sum()
turnovers = rz_drives['turnover'].sum()
# Points per trip
points = touchdowns * 7 + field_goals * 3
points_per_trip = points / total_trips if total_trips > 0 else 0
return {
'red_zone_trips': total_trips,
'touchdowns': touchdowns,
'td_rate': touchdowns / total_trips if total_trips > 0 else 0,
'field_goals': field_goals,
'fg_rate': field_goals / total_trips if total_trips > 0 else 0,
'points_per_trip': points_per_trip,
'turnovers': turnovers,
'turnover_rate': turnovers / total_trips if total_trips > 0 else 0
}
# Example
kc_rz = calculate_red_zone_efficiency(pbp, 'KC')
print("KC Red Zone Efficiency:")
for key, value in kc_rz.items():
if 'rate' in key:
print(f" {key}: {value:.1%}")
else:
print(f" {key}: {value:.2f}")
Red Zone Benchmarks
| Metric | League Average | Good | Elite |
|---|---|---|---|
| TD Rate | 55-58% | 60-65% | 65%+ |
| Points/Trip | 4.0-4.3 | 4.5-5.0 | 5.0+ |
| Turnover Rate | 5-7% | 3-5% | < 3% |
Red Zone EPA
Beyond scoring rates, EPA provides efficiency context:
def calculate_red_zone_epa(pbp: pd.DataFrame, team: str) -> dict:
"""Calculate red zone EPA metrics."""
rz_plays = pbp[
(pbp['posteam'] == team) &
(pbp['yardline_100'] <= 20) &
(pbp['play_type'].isin(['pass', 'run'])) &
(pbp['epa'].notna())
]
overall_epa = rz_plays['epa'].mean()
pass_plays = rz_plays[rz_plays['play_type'] == 'pass']
rush_plays = rz_plays[rz_plays['play_type'] == 'run']
pass_epa = pass_plays['epa'].mean() if len(pass_plays) > 0 else 0
rush_epa = rush_plays['epa'].mean() if len(rush_plays) > 0 else 0
success_rate = (rz_plays['epa'] > 0).mean()
return {
'rz_epa': overall_epa,
'rz_pass_epa': pass_epa,
'rz_rush_epa': rush_epa,
'rz_success_rate': success_rate,
'rz_pass_rate': len(pass_plays) / len(rz_plays) if len(rz_plays) > 0 else 0
}
Key Insight: Red zone EPA tends to be lower than overall EPA because: 1. Field compression limits big plays 2. Defenses tighten coverage 3. Each yard is more valuable (steeper EP curve)
Third Down Analysis
The Money Down
Third down is often called the "money down" because it determines whether drives continue or stall. Third down conversion rate is one of the most discussed metrics in football.
def calculate_third_down_efficiency(pbp: pd.DataFrame, team: str) -> dict:
"""Calculate third down conversion metrics."""
third_downs = pbp[
(pbp['posteam'] == team) &
(pbp['down'] == 3) &
(pbp['play_type'].isin(['pass', 'run']))
]
# Overall conversion rate
conversions = (
(third_downs['first_down'] == 1) |
(third_downs['touchdown'] == 1)
).sum()
conversion_rate = conversions / len(third_downs) if len(third_downs) > 0 else 0
# By distance
short = third_downs[third_downs['ydstogo'] <= 3]
medium = third_downs[(third_downs['ydstogo'] > 3) & (third_downs['ydstogo'] <= 6)]
long = third_downs[third_downs['ydstogo'] > 6]
def conv_rate(df):
if len(df) == 0:
return 0
return ((df['first_down'] == 1) | (df['touchdown'] == 1)).sum() / len(df)
return {
'total_third_downs': len(third_downs),
'conversions': conversions,
'conversion_rate': conversion_rate,
'short_rate': conv_rate(short), # 1-3 yards
'medium_rate': conv_rate(medium), # 4-6 yards
'long_rate': conv_rate(long), # 7+ yards
'third_down_epa': third_downs['epa'].mean()
}
kc_3rd = calculate_third_down_efficiency(pbp, 'KC')
print("\nKC Third Down Efficiency:")
print(f" Overall: {kc_3rd['conversion_rate']:.1%}")
print(f" 3rd & Short (1-3): {kc_3rd['short_rate']:.1%}")
print(f" 3rd & Medium (4-6): {kc_3rd['medium_rate']:.1%}")
print(f" 3rd & Long (7+): {kc_3rd['long_rate']:.1%}")
Third Down Benchmarks
| Distance | League Average | Good | Elite |
|---|---|---|---|
| Overall | 38-42% | 43-47% | 48%+ |
| Short (1-3) | 62-68% | 70-75% | 76%+ |
| Medium (4-6) | 40-45% | 48-52% | 53%+ |
| Long (7+) | 25-30% | 32-37% | 38%+ |
Third Down EPA Context
def analyze_third_down_context(pbp: pd.DataFrame, team: str) -> pd.DataFrame:
"""Detailed third down analysis by situation."""
third_downs = pbp[
(pbp['posteam'] == team) &
(pbp['down'] == 3) &
(pbp['play_type'].isin(['pass', 'run'])) &
(pbp['epa'].notna())
]
# Create distance buckets
def distance_bucket(ytg):
if ytg <= 2:
return "1-2"
elif ytg <= 4:
return "3-4"
elif ytg <= 6:
return "5-6"
elif ytg <= 9:
return "7-9"
else:
return "10+"
third_downs['distance_bucket'] = third_downs['ydstogo'].apply(distance_bucket)
result = third_downs.groupby('distance_bucket').agg(
attempts=('epa', 'count'),
conversion_rate=('first_down', lambda x: ((x == 1) | (third_downs.loc[x.index, 'touchdown'] == 1)).mean()),
epa=('epa', 'mean'),
pass_rate=('play_type', lambda x: (x == 'pass').mean())
).reset_index()
return result
third_analysis = analyze_third_down_context(pbp, 'KC')
print("\nKC Third Down by Distance:")
print(third_analysis.to_string(index=False))
Two-Minute Offense
Definition and Importance
The two-minute offense refers to drives at the end of halves (or games) where teams operate with urgency. These situations test: - Clock management skills - No-huddle execution - Decision-making under pressure - Sideline communication
def identify_two_minute_drives(pbp: pd.DataFrame) -> pd.DataFrame:
"""Identify two-minute drill situations."""
two_min_plays = pbp[
# End of half/game
((pbp['qtr'] == 2) & (pbp['game_seconds_remaining'] <= 120 + 1800)) | # End of 1st half
((pbp['qtr'] == 4) & (pbp['game_seconds_remaining'] <= 120)) # End of game
].copy()
# Filter to meaningful situations (not garbage time)
two_min_plays = two_min_plays[
(two_min_plays['score_differential'].abs() <= 16) &
(two_min_plays['play_type'].isin(['pass', 'run', 'field_goal']))
]
return two_min_plays
two_min = identify_two_minute_drives(pbp)
print(f"Two-minute plays identified: {len(two_min):,}")
Two-Minute Efficiency Metrics
def calculate_two_minute_efficiency(pbp: pd.DataFrame, team: str) -> dict:
"""Calculate two-minute drill effectiveness."""
two_min = identify_two_minute_drives(pbp)
team_two_min = two_min[two_min['posteam'] == team]
if len(team_two_min) == 0:
return {'no_data': True}
# Plays
plays = team_two_min[team_two_min['play_type'].isin(['pass', 'run'])]
# Scoring
drives = team_two_min.groupby(['game_id', 'drive']).agg(
scored_td=('touchdown', 'max'),
scored_fg=('field_goal_result', lambda x: (x == 'made').any()),
plays=('play_type', 'count')
).reset_index()
scoring_drives = ((drives['scored_td'] == 1) | (drives['scored_fg'] == True)).sum()
total_drives = len(drives)
return {
'two_min_drives': total_drives,
'scoring_drives': scoring_drives,
'scoring_rate': scoring_drives / total_drives if total_drives > 0 else 0,
'epa_per_play': plays['epa'].mean() if len(plays) > 0 else 0,
'pass_rate': (plays['play_type'] == 'pass').mean() if len(plays) > 0 else 0,
'success_rate': (plays['epa'] > 0).mean() if len(plays) > 0 else 0
}
kc_2min = calculate_two_minute_efficiency(pbp, 'KC')
print("\nKC Two-Minute Drill Efficiency:")
for key, value in kc_2min.items():
if isinstance(value, float):
print(f" {key}: {value:.3f}")
else:
print(f" {key}: {value}")
Two-Minute Benchmarks
| Metric | League Average | Good | Elite |
|---|---|---|---|
| Scoring Rate | 35-40% | 45-50% | 55%+ |
| EPA/Play | 0.05-0.10 | 0.12-0.18 | 0.20+ |
| Success Rate | 45-48% | 50-55% | 56%+ |
Goal-to-Go Situations
Inside the 10
Goal-to-go situations (less than 10 yards from the end zone) represent the highest-value plays:
def calculate_goal_to_go_efficiency(pbp: pd.DataFrame, team: str) -> dict:
"""Calculate goal-to-go efficiency."""
goal_to_go = pbp[
(pbp['posteam'] == team) &
(pbp['yardline_100'] <= 10) &
(pbp['goal_to_go'] == 1) &
(pbp['play_type'].isin(['pass', 'run']))
]
if len(goal_to_go) == 0:
return {'no_data': True}
td_rate = (goal_to_go['touchdown'] == 1).mean()
epa = goal_to_go['epa'].mean()
pass_rate = (goal_to_go['play_type'] == 'pass').mean()
# By distance
inside_5 = goal_to_go[goal_to_go['yardline_100'] <= 5]
inside_5_td = (inside_5['touchdown'] == 1).mean() if len(inside_5) > 0 else 0
return {
'goal_to_go_plays': len(goal_to_go),
'td_rate': td_rate,
'epa': epa,
'pass_rate': pass_rate,
'inside_5_td_rate': inside_5_td
}
Goal Line (1-2 Yards)
The true goal line is the ultimate short-yardage test:
def calculate_goal_line_efficiency(pbp: pd.DataFrame, team: str) -> dict:
"""Calculate efficiency from the 1-2 yard line."""
goal_line = pbp[
(pbp['posteam'] == team) &
(pbp['yardline_100'] <= 2) &
(pbp['play_type'].isin(['pass', 'run']))
]
if len(goal_line) == 0:
return {'no_data': True}
td_rate = (goal_line['touchdown'] == 1).mean()
rush_plays = goal_line[goal_line['play_type'] == 'run']
pass_plays = goal_line[goal_line['play_type'] == 'pass']
rush_td = (rush_plays['touchdown'] == 1).mean() if len(rush_plays) > 0 else 0
pass_td = (pass_plays['touchdown'] == 1).mean() if len(pass_plays) > 0 else 0
return {
'goal_line_plays': len(goal_line),
'overall_td_rate': td_rate,
'rush_td_rate': rush_td,
'pass_td_rate': pass_td,
'pass_rate': len(pass_plays) / len(goal_line) if len(goal_line) > 0 else 0
}
Late and Close Games
Defining High-Leverage Situations
Games are most influenced by plays in close, late situations:
def identify_late_close_plays(pbp: pd.DataFrame) -> pd.DataFrame:
"""
Identify plays in late and close game situations.
Definition: 4th quarter (or OT), score within 8 points.
"""
late_close = pbp[
(pbp['qtr'] >= 4) &
(pbp['score_differential'].abs() <= 8) &
(pbp['play_type'].isin(['pass', 'run'])) &
(pbp['epa'].notna())
].copy()
return late_close
late_close = identify_late_close_plays(pbp)
print(f"Late & close plays: {len(late_close):,}")
print(f"Percentage of all plays: {len(late_close)/len(pbp)*100:.1f}%")
Late and Close Efficiency
def calculate_late_close_efficiency(pbp: pd.DataFrame, team: str) -> dict:
"""Calculate efficiency in late and close situations."""
late_close = identify_late_close_plays(pbp)
team_plays = late_close[late_close['posteam'] == team]
if len(team_plays) == 0:
return {'no_data': True}
# Compare to overall performance
all_plays = pbp[
(pbp['posteam'] == team) &
(pbp['play_type'].isin(['pass', 'run'])) &
(pbp['epa'].notna())
]
overall_epa = all_plays['epa'].mean()
late_close_epa = team_plays['epa'].mean()
overall_success = (all_plays['epa'] > 0).mean()
late_close_success = (team_plays['epa'] > 0).mean()
return {
'late_close_plays': len(team_plays),
'late_close_epa': late_close_epa,
'overall_epa': overall_epa,
'epa_difference': late_close_epa - overall_epa,
'late_close_success': late_close_success,
'overall_success': overall_success,
'success_difference': late_close_success - overall_success
}
kc_late = calculate_late_close_efficiency(pbp, 'KC')
print("\nKC Late & Close Performance:")
print(f" Late/Close EPA: {kc_late['late_close_epa']:.3f}")
print(f" Overall EPA: {kc_late['overall_epa']:.3f}")
print(f" Difference: {kc_late['epa_difference']:+.3f}")
Win Probability-Weighted Performance
For a more sophisticated view, weight plays by leverage:
def calculate_leverage_weighted_epa(pbp: pd.DataFrame, team: str) -> dict:
"""Calculate leverage-weighted EPA."""
team_plays = pbp[
(pbp['posteam'] == team) &
(pbp['play_type'].isin(['pass', 'run'])) &
(pbp['epa'].notna()) &
(pbp['wp'].notna())
].copy()
# Leverage = how much WP can change
# Highest when WP is near 50%
team_plays['leverage'] = 4 * team_plays['wp'] * (1 - team_plays['wp'])
# Weighted EPA
weighted_epa = (team_plays['epa'] * team_plays['leverage']).sum() / team_plays['leverage'].sum()
unweighted_epa = team_plays['epa'].mean()
return {
'unweighted_epa': unweighted_epa,
'leverage_weighted_epa': weighted_epa,
'difference': weighted_epa - unweighted_epa
}
Defensive Situational Performance
Red Zone Defense
def calculate_red_zone_defense(pbp: pd.DataFrame, team: str) -> dict:
"""Calculate defensive red zone efficiency."""
rz_defense = pbp[
(pbp['defteam'] == team) &
(pbp['yardline_100'] <= 20) &
(pbp['play_type'].isin(['pass', 'run']))
]
# Drives defended
drives = pbp[
(pbp['defteam'] == team) &
(pbp['yardline_100'] <= 20)
].groupby(['game_id', 'drive']).agg(
allowed_td=('touchdown', 'max')
).reset_index()
td_allowed = drives['allowed_td'].sum()
total_trips = len(drives)
return {
'rz_trips_allowed': total_trips,
'tds_allowed': td_allowed,
'td_rate_allowed': td_allowed / total_trips if total_trips > 0 else 0,
'rz_def_epa': rz_defense['epa'].mean() if len(rz_defense) > 0 else 0
}
Third Down Defense
def calculate_third_down_defense(pbp: pd.DataFrame, team: str) -> dict:
"""Calculate third down defensive efficiency."""
third_defense = pbp[
(pbp['defteam'] == team) &
(pbp['down'] == 3) &
(pbp['play_type'].isin(['pass', 'run']))
]
conversions_allowed = (
(third_defense['first_down'] == 1) |
(third_defense['touchdown'] == 1)
).sum()
conversion_rate_allowed = conversions_allowed / len(third_defense) if len(third_defense) > 0 else 0
return {
'third_downs_faced': len(third_defense),
'conversions_allowed': conversions_allowed,
'conversion_rate_allowed': conversion_rate_allowed,
'third_down_def_epa': third_defense['epa'].mean() if len(third_defense) > 0 else 0
}
Does "Clutch" Exist?
The Statistical Debate
A persistent question in sports analytics: Is "clutch" performance a repeatable skill?
def analyze_clutch_consistency(pbp_multiple_years: dict, team: str) -> dict:
"""
Analyze whether clutch performance is consistent across seasons.
Args:
pbp_multiple_years: Dict of {year: pbp_dataframe}
team: Team to analyze
Returns:
Year-over-year clutch performance correlation
"""
yearly_clutch = []
for year, pbp in pbp_multiple_years.items():
late_close = identify_late_close_plays(pbp)
team_plays = late_close[late_close['posteam'] == team]
if len(team_plays) >= 50: # Minimum sample
clutch_epa = team_plays['epa'].mean()
all_plays = pbp[
(pbp['posteam'] == team) &
(pbp['play_type'].isin(['pass', 'run'])) &
(pbp['epa'].notna())
]
overall_epa = all_plays['epa'].mean()
yearly_clutch.append({
'year': year,
'clutch_epa': clutch_epa,
'overall_epa': overall_epa,
'clutch_diff': clutch_epa - overall_epa
})
if len(yearly_clutch) < 3:
return {'insufficient_data': True}
df = pd.DataFrame(yearly_clutch)
# Year-to-year correlation of clutch differential
df['prev_clutch_diff'] = df['clutch_diff'].shift(1)
correlation = df['clutch_diff'].corr(df['prev_clutch_diff'])
return {
'yearly_data': df.to_dict('records'),
'yoy_correlation': correlation,
'interpretation': 'Clutch is repeatable' if correlation > 0.3 else 'Clutch appears random'
}
Research Finding: Year-to-year correlation of "clutch" performance is typically very low (r ≈ 0.05-0.15), suggesting clutch performance is largely random variance rather than a stable skill.
Building a Situational Analyzer
from dataclasses import dataclass
@dataclass
class SituationalReport:
"""Comprehensive situational performance report."""
team: str
season: int
# Red Zone
rz_trips: int
rz_td_rate: float
rz_points_per_trip: float
rz_epa: float
# Third Down
third_down_rate: float
third_short_rate: float
third_long_rate: float
third_down_epa: float
# Two Minute
two_min_scoring_rate: float
two_min_epa: float
# Late & Close
late_close_epa: float
late_close_vs_overall: float
# Defense
rz_def_td_rate: float
third_down_def_rate: float
class SituationalAnalyzer:
"""Comprehensive situational football analyzer."""
def __init__(self, pbp: pd.DataFrame, season: int = 2023):
self.pbp = pbp
self.season = season
self.plays = pbp[
(pbp['play_type'].isin(['pass', 'run'])) &
(pbp['epa'].notna())
].copy()
def analyze_team(self, team: str) -> SituationalReport:
"""Generate complete situational report for a team."""
# Red zone offense
rz = calculate_red_zone_efficiency(self.pbp, team)
rz_epa = calculate_red_zone_epa(self.pbp, team)
# Third down
third = calculate_third_down_efficiency(self.pbp, team)
# Two minute
two_min = calculate_two_minute_efficiency(self.pbp, team)
# Late and close
late_close = calculate_late_close_efficiency(self.pbp, team)
# Defense
rz_def = calculate_red_zone_defense(self.pbp, team)
third_def = calculate_third_down_defense(self.pbp, team)
return SituationalReport(
team=team,
season=self.season,
rz_trips=rz.get('red_zone_trips', 0),
rz_td_rate=rz.get('td_rate', 0),
rz_points_per_trip=rz.get('points_per_trip', 0),
rz_epa=rz_epa.get('rz_epa', 0),
third_down_rate=third.get('conversion_rate', 0),
third_short_rate=third.get('short_rate', 0),
third_long_rate=third.get('long_rate', 0),
third_down_epa=third.get('third_down_epa', 0),
two_min_scoring_rate=two_min.get('scoring_rate', 0) if not two_min.get('no_data') else 0,
two_min_epa=two_min.get('epa_per_play', 0) if not two_min.get('no_data') else 0,
late_close_epa=late_close.get('late_close_epa', 0) if not late_close.get('no_data') else 0,
late_close_vs_overall=late_close.get('epa_difference', 0) if not late_close.get('no_data') else 0,
rz_def_td_rate=rz_def.get('td_rate_allowed', 0),
third_down_def_rate=third_def.get('conversion_rate_allowed', 0)
)
def rank_all_teams(self, metric: str = 'rz_td_rate') -> pd.DataFrame:
"""Rank teams by a situational metric."""
results = []
for team in self.plays['posteam'].unique():
report = self.analyze_team(team)
results.append({
'team': team,
'rz_td_rate': report.rz_td_rate,
'third_down_rate': report.third_down_rate,
'two_min_scoring_rate': report.two_min_scoring_rate,
'late_close_epa': report.late_close_epa
})
df = pd.DataFrame(results)
return df.sort_values(metric, ascending=False)
Key Takeaways
Situational Performance Matters
- Red zone efficiency - converting opportunities to touchdowns
- Third down conversion - sustaining drives
- Two-minute execution - capitalizing on late-half opportunities
- Late & close performance - winning when it matters most
Benchmarks to Remember
- Elite RZ TD rate: 65%+
- Elite 3rd down rate: 48%+
- Elite two-minute scoring: 55%+
Analytical Caveats
- Sample sizes - situational stats have fewer plays
- Context - opponent quality matters
- Clutch - largely not a repeatable skill
- Regression - extreme situational performance often regresses
Practice Exercises
- Calculate red zone efficiency for all teams and identify the top 5
- Analyze third down conversion rates by pass vs rush
- Identify teams that perform significantly better (or worse) in late/close situations
- Compare offensive vs defensive situational performance
- Test whether "clutch" performance persists year-to-year
Summary
Situational football analysis reveals how teams perform when stakes are highest. Key metrics include:
- Red zone TD rate for scoring efficiency
- Third down conversion for drive sustainability
- Two-minute performance for late-half execution
- Late and close EPA for clutch performance
While these metrics are valuable, analysts should account for sample size limitations and the largely random nature of "clutch" performance. Situational excellence often reflects overall team quality more than unique situational skill.
Preview: Chapter 15
Next, we'll explore Home Field Advantage - quantifying the value of playing at home and what factors drive it.