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:

  1. Higher efficiency: 0.54 vs 0.41 EPA/target (32% better)
  2. Better value: $12M vs $20M for ~80% of production
  3. Skill-based production: Outperforms QB by larger margin
  4. Volume upside: Projects to higher production if given A's targets
  5. 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

  1. Yards ≠ Value: Total yards don't capture efficiency
  2. Target share is opportunity, not skill: High volume may mask inefficiency
  3. EPA per target is king: Best predictor of receiving value
  4. QB adjustment matters: Isolate receiver from quarterback
  5. RACR reveals style: Conversion rate shows true production
  6. Price the surplus: Pay for efficiency, not volume

Discussion Questions

  1. How would your analysis change if Receiver A were 3 years younger?

  2. What if Receiver A's QB was significantly better—would that change attribution?

  3. Could Receiver B's efficiency decline with more targets? How would you test this?

  4. What non-statistical factors should influence this decision?

  5. How does the team's offensive scheme affect which receiver fits better?