Case Study: The $20M Decision
Scenario
The Miami Dolphins are facing a critical offseason decision. Their top wide receiver is entering free agency after a standout season:
Receiver A's Season Stats: - 142 targets (27% target share) - 102 receptions - 1,389 yards (3rd in NFL) - 9 touchdowns - 71.8% catch rate
His agent is seeking a 4-year, $80 million contract ($20M/year average).
Meanwhile, a receiver from another team has hit the market with different numbers:
Receiver B's Season Stats: - 87 targets (15% target share) - 65 receptions - 1,012 yards - 7 touchdowns - 74.7% catch rate
He's seeking $12M/year.
The front office has asked the analytics department: Which receiver represents the better value, and what should be our strategy?
Data Gathering
import nfl_data_py as nfl
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
# Load 2023 data
pbp = nfl.import_pbp_data([2023])
# Filter to pass attempts
passes = pbp[pbp['pass_attempt'] == 1].copy()
# Calculate comprehensive receiver stats
def analyze_receiver(passes: pd.DataFrame, receiver_name: str) -> dict:
"""Generate comprehensive analysis for a receiver."""
rec_passes = passes[passes['receiver_player_name'] == receiver_name]
if len(rec_passes) < 30:
return {"error": "Insufficient data"}
completions = rec_passes[rec_passes['complete_pass'] == 1]
stats = {
'targets': len(rec_passes),
'receptions': len(completions),
'yards': completions['yards_gained'].sum(),
'tds': rec_passes['pass_touchdown'].sum(),
'catch_rate': rec_passes['complete_pass'].mean(),
'epa_total': rec_passes['epa'].sum(),
'epa_per_target': rec_passes['epa'].mean(),
'success_rate': (rec_passes['epa'] > 0).mean(),
'adot': rec_passes['air_yards'].mean(),
'yac_per_rec': completions['yards_after_catch'].mean(),
'deep_targets': (rec_passes['air_yards'] >= 20).sum(),
'deep_catch_rate': rec_passes[rec_passes['air_yards'] >= 20]['complete_pass'].mean(),
'third_down_epa': rec_passes[rec_passes['down'] == 3]['epa'].mean(),
'rz_tds': rec_passes[rec_passes['yardline_100'] <= 20]['pass_touchdown'].sum()
}
return stats
Analysis 1: Efficiency Comparison
Raw Numbers vs. Efficiency
# Receiver A (high volume)
rec_a = {
'targets': 142,
'yards': 1389,
'epa_total': 58.3,
'epa_per_target': 0.41
}
# Receiver B (efficient)
rec_b = {
'targets': 87,
'yards': 1012,
'epa_total': 47.2,
'epa_per_target': 0.54
}
# Rankings among qualified receivers
print("EPA per Target Comparison:")
print(f" Receiver A: 0.41 (Rank 12 of 85)")
print(f" Receiver B: 0.54 (Rank 3 of 85)")
print("\nTotal Yards:")
print(f" Receiver A: 1,389 (Rank 3)")
print(f" Receiver B: 1,012 (Rank 15)")
Finding: Receiver A accumulates more total production, but Receiver B is significantly more efficient. On a per-target basis, B adds 32% more value.
Analysis 2: Target Quality Examination
# Analyze target quality for both receivers
def compare_target_quality(passes: pd.DataFrame, rec_a: str, rec_b: str) -> pd.DataFrame:
"""Compare target quality between receivers."""
rec_a_passes = passes[passes['receiver_player_name'] == rec_a]
rec_b_passes = passes[passes['receiver_player_name'] == rec_b]
comparison = pd.DataFrame({
'Receiver A': {
'ADOT': rec_a_passes['air_yards'].mean(),
'Deep Target %': (rec_a_passes['air_yards'] >= 20).mean(),
'Third Down %': (rec_a_passes['down'] == 3).mean(),
'Red Zone %': (rec_a_passes['yardline_100'] <= 20).mean()
},
'Receiver B': {
'ADOT': rec_b_passes['air_yards'].mean(),
'Deep Target %': (rec_b_passes['air_yards'] >= 20).mean(),
'Third Down %': (rec_b_passes['down'] == 3).mean(),
'Red Zone %': (rec_b_passes['yardline_100'] <= 20).mean()
}
})
return comparison
print("Target Quality Comparison:")
print("""
Receiver A Receiver B
ADOT 10.2 12.8
Deep Target % 15% 22%
Third Down % 24% 19%
Red Zone % 12% 10%
""")
Finding: Receiver B has a deeper target profile (12.8 vs 10.2 ADOT, 22% vs 15% deep targets). This makes his higher efficiency even more impressive—he's excelling on harder targets.
Analysis 3: Style Analysis (YAC vs. Air Yards)
# RACR and YAC analysis
def analyze_receiving_style(passes: pd.DataFrame, receiver: str) -> dict:
"""Analyze receiving style."""
rec_passes = passes[passes['receiver_player_name'] == receiver]
completions = rec_passes[rec_passes['complete_pass'] == 1]
total_air_yards = rec_passes['air_yards'].sum()
total_yards = completions['yards_gained'].sum()
total_yac = completions['yards_after_catch'].sum()
return {
'total_air_yards': total_air_yards,
'receiving_yards': total_yards,
'yac': total_yac,
'racr': total_yards / total_air_yards,
'yac_per_rec': completions['yards_after_catch'].mean(),
'pct_from_yac': total_yac / total_yards * 100
}
print("Receiving Style Comparison:")
print("""
Receiver A Receiver B
RACR 0.96 1.12
YAC/Reception 4.8 6.2
% from YAC 35% 42%
""")
Finding: - Receiver A (RACR = 0.96): Not fully converting air yards—fewer yards than targeted - Receiver B (RACR = 1.12): Gaining more than targeted—significant YAC contribution
Receiver B's higher YAC per reception (6.2 vs 4.8) suggests greater ability after the catch.
Analysis 4: QB Attribution
# Compare receivers to their QBs' overall performance
def qb_adjusted_analysis(passes: pd.DataFrame, receiver: str) -> dict:
"""Calculate QB-adjusted receiver metrics."""
rec_passes = passes[passes['receiver_player_name'] == receiver]
if len(rec_passes) == 0:
return {"error": "No data"}
# Get primary QB
primary_qb = rec_passes['passer_player_name'].mode().iloc[0]
# QB's baseline on all targets
qb_passes = passes[passes['passer_player_name'] == primary_qb]
qb_baseline = qb_passes['epa'].mean()
# Receiver's EPA vs QB baseline
rec_epa = rec_passes['epa'].mean()
return {
'qb': primary_qb,
'qb_overall_epa': qb_baseline,
'receiver_epa': rec_epa,
'epa_over_qb': rec_epa - qb_baseline
}
print("QB-Adjusted Analysis:")
print("""
Receiver A Receiver B
QB's Overall EPA 0.25 0.18
Receiver's EPA 0.41 0.54
EPA Over QB +0.16 +0.36
""")
Finding: Receiver B outperforms his QB by a larger margin (+0.36 vs +0.16). Despite having a less accurate quarterback, B produces elite efficiency. This suggests more individual skill.
Analysis 5: Volume Projection
If Receiver B got Receiver A's target volume, how would he perform?
def project_production(current_stats: dict, new_targets: int) -> dict:
"""Project production at different target volume."""
# Assume efficiency declines with volume (regression factor)
volume_increase = new_targets / current_stats['targets']
regression_factor = 1 - (0.10 * (volume_increase - 1)) # 10% decline per 100% increase
projected = {
'targets': new_targets,
'projected_receptions': int(new_targets * current_stats['catch_rate'] * regression_factor),
'projected_yards': int(new_targets * current_stats['yards_per_target'] * regression_factor),
'projected_epa': new_targets * current_stats['epa_per_target'] * regression_factor,
'regressed_epa_per_target': current_stats['epa_per_target'] * regression_factor
}
return projected
print("Volume Projection for Receiver B:")
print("""
If Receiver B had 142 targets (like A):
Projected Receptions: ~100 (with 5% regression)
Projected Yards: ~1,580
Projected EPA: ~68.5 (vs A's actual 58.3)
Regressed EPA/Target: 0.48 (still higher than A's 0.41)
""")
Finding: Even with conservative efficiency regression, Receiver B at high volume would likely outproduce Receiver A.
Analysis 6: Contract Valuation
def calculate_receiver_value(stats: dict, contract_aav: float) -> dict:
"""Calculate value of receiver contract."""
# Rough conversion: 10 EPA = 1 win, 1 win = ~$3M
epa_value = stats['epa_total'] / 10 * 3 # Millions
return {
'epa_total': stats['epa_total'],
'estimated_value': epa_value,
'contract_aav': contract_aav,
'surplus_value': epa_value - contract_aav,
'cost_per_epa': contract_aav / stats['epa_total']
}
print("Contract Value Analysis:")
print("""
Receiver A Receiver B
Total EPA 58.3 47.2
Estimated Value $17.5M $14.2M
Contract AAV $20.0M $12.0M
Surplus Value -$2.5M +$2.2M
Cost per EPA $0.34M $0.25M
""")
Finding: - Receiver A at $20M**: Overpay of ~$2.5M annually - Receiver B at $12M**: Underpay of ~$2.2M annually - Cost efficiency: B costs 26% less per EPA point
Visualization
fig, axes = plt.subplots(2, 2, figsize=(12, 10))
# Plot 1: EPA per Target vs Target Volume
ax1 = axes[0, 0]
ax1.scatter([142], [0.41], s=200, c='blue', label='Receiver A', zorder=5)
ax1.scatter([87], [0.54], s=200, c='red', label='Receiver B', zorder=5)
ax1.set_xlabel('Targets')
ax1.set_ylabel('EPA per Target')
ax1.set_title('Volume vs Efficiency')
ax1.legend()
# Plot 2: Yards Composition
ax2 = axes[0, 1]
categories = ['Air Yards', 'YAC']
a_data = [904, 485] # Example values
b_data = [588, 424]
x = np.arange(len(categories))
width = 0.35
ax2.bar(x - width/2, a_data, width, label='Receiver A')
ax2.bar(x + width/2, b_data, width, label='Receiver B')
ax2.set_xticks(x)
ax2.set_xticklabels(categories)
ax2.set_ylabel('Yards')
ax2.set_title('Yards Composition')
ax2.legend()
# Plot 3: Contract Value
ax3 = axes[1, 0]
receivers = ['Receiver A', 'Receiver B']
estimated = [17.5, 14.2]
contract = [20.0, 12.0]
x = np.arange(len(receivers))
ax3.bar(x - width/2, estimated, width, label='Estimated Value', color='green')
ax3.bar(x + width/2, contract, width, label='Contract AAV', color='orange')
ax3.set_xticks(x)
ax3.set_xticklabels(receivers)
ax3.set_ylabel('$ Millions')
ax3.set_title('Value vs Contract')
ax3.legend()
# Plot 4: Efficiency Radar (simplified as bar)
ax4 = axes[1, 1]
metrics = ['EPA/Tgt', 'RACR', 'Catch%', 'Deep%']
a_values = [0.41, 0.96, 0.72, 0.15]
b_values = [0.54, 1.12, 0.75, 0.22]
x = np.arange(len(metrics))
ax4.bar(x - width/2, a_values, width, label='Receiver A')
ax4.bar(x + width/2, b_values, width, label='Receiver B')
ax4.set_xticks(x)
ax4.set_xticklabels(metrics)
ax4.set_title('Efficiency Comparison')
ax4.legend()
plt.tight_layout()
plt.savefig('receiver_comparison.png', dpi=300, bbox_inches='tight')
plt.close()
Recommendation
Sign Receiver B
Evidence:
- Higher efficiency: 0.54 vs 0.41 EPA/target (32% better)
- Better value: $12M vs $20M for ~80% of production
- Skill-based production: Outperforms QB by larger margin
- Volume upside: Projects to higher production if given A's targets
- Style advantage: Higher YAC, better deep ball, more explosive
Contract Structure Recommendation
Receiver B: 4 years, $50M ($12.5M AAV) - Year 1: $10M (prove-it year) - Years 2-4: $13.3M average - Incentives for Pro Bowl, 100 receptions, 1,200 yards
Estimated surplus value: $2-3M/year
What About Receiver A?
If another team signs A at $20M+: - Let them overpay - A is a good player but not worth the premium - $8M/year saved can address other needs
Alternative Scenarios
If must choose Receiver A
Counter-offer at $16M/year max: - Still an overpay but closer to fair value - Don't exceed 4 years (age concerns) - Include performance de-escalators
If Receiver B is unavailable
Target similar profile receivers: - High EPA/target (>0.45) - Lower target share (12-18%) - RACR >1.0 - Seek value in $8-12M range
Lessons Learned
- Yards ≠ Value: Total yards don't capture efficiency
- Target share is opportunity, not skill: High volume may mask inefficiency
- EPA per target is king: Best predictor of receiving value
- QB adjustment matters: Isolate receiver from quarterback
- RACR reveals style: Conversion rate shows true production
- Price the surplus: Pay for efficiency, not volume
Discussion Questions
-
How would your analysis change if Receiver A were 3 years younger?
-
What if Receiver A's QB was significantly better—would that change attribution?
-
Could Receiver B's efficiency decline with more targets? How would you test this?
-
What non-statistical factors should influence this decision?
-
How does the team's offensive scheme affect which receiver fits better?