The 2017 Philadelphia Eagles won Super Bowl LII with Carson Wentz going down to injury mid-season and Nick Foles stepping in. The 2022 Cincinnati Bengals reached the Super Bowl just two years after finishing 2-14. Meanwhile, the Dallas Cowboys have...
In This Chapter
Chapter 17: Team Building and Roster Construction
Introduction
The 2017 Philadelphia Eagles won Super Bowl LII with Carson Wentz going down to injury mid-season and Nick Foles stepping in. The 2022 Cincinnati Bengals reached the Super Bowl just two years after finishing 2-14. Meanwhile, the Dallas Cowboys have had only three playoff wins in 28 years despite consistently high payrolls. What separates successful roster construction from the rest?
Building a winning NFL team involves a complex interplay of: - The salary cap - $224.8M in 2023, creating hard constraints - The draft - Primary source of cost-controlled talent - Free agency - Expensive but immediate talent acquisition - Contract structure - Guaranteed money, cap manipulation, and timing - Position value - Not all positions contribute equally to winning
This chapter explores the analytics of roster construction—how successful teams allocate resources across positions, manage the salary cap, and build sustainable competitive windows.
The Salary Cap Economy
Understanding the Cap
The NFL salary cap is a hard limit on total player salaries, designed to create competitive balance:
import pandas as pd
import numpy as np
from dataclasses import dataclass
from typing import Dict, List, Optional, Tuple
@dataclass
class SalaryCapInfo:
"""NFL salary cap context."""
year: int
cap_amount: float
top_qb_pct: float # Top QB % of cap
avg_starter_pct: float
roster_size: int = 53
# Historical cap growth
CAP_HISTORY = {
2015: 143.28,
2016: 155.27,
2017: 167.00,
2018: 177.20,
2019: 188.20,
2020: 198.20,
2021: 182.50, # COVID reduction
2022: 208.20,
2023: 224.80,
2024: 255.40 # Projected
}
def analyze_cap_growth():
"""Analyze salary cap trends."""
years = list(CAP_HISTORY.keys())
caps = list(CAP_HISTORY.values())
# Average annual growth (excluding COVID year)
growth_rates = []
for i in range(1, len(caps)):
if years[i] != 2021: # Skip COVID anomaly
growth_rates.append((caps[i] - caps[i-1]) / caps[i-1])
avg_growth = np.mean(growth_rates)
return {
'avg_annual_growth': avg_growth,
'recent_cap': caps[-1],
'five_year_growth': (caps[-1] - caps[-6]) / caps[-6] if len(caps) > 5 else 0
}
Position Spending Benchmarks
Successful teams allocate cap space strategically:
@dataclass
class PositionBenchmark:
"""Cap allocation benchmarks by position."""
position: str
avg_pct_of_cap: float
elite_pct_of_cap: float
starter_count: int
positional_value: float # WAR-like metric
POSITION_BENCHMARKS = {
'QB': PositionBenchmark('QB', 12.0, 18.0, 1, 10.0),
'EDGE': PositionBenchmark('EDGE', 8.0, 12.0, 2, 7.5),
'WR': PositionBenchmark('WR', 10.0, 14.0, 3, 6.0),
'OT': PositionBenchmark('OT', 9.0, 12.0, 2, 7.0),
'CB': PositionBenchmark('CB', 8.0, 11.0, 2, 6.5),
'DT': PositionBenchmark('DT', 5.0, 8.0, 2, 5.0),
'LB': PositionBenchmark('LB', 6.0, 9.0, 3, 5.5),
'S': PositionBenchmark('S', 5.0, 7.0, 2, 4.5),
'TE': PositionBenchmark('TE', 4.0, 6.0, 1, 4.0),
'IOL': PositionBenchmark('IOL', 6.0, 8.0, 3, 5.5),
'RB': PositionBenchmark('RB', 3.0, 5.0, 1, 3.0),
}
def calculate_cap_allocation(contracts: pd.DataFrame,
cap: float) -> pd.DataFrame:
"""
Calculate cap allocation by position group.
Args:
contracts: DataFrame with player, position, cap_hit
cap: Total salary cap
Returns:
Position-level spending analysis
"""
position_spending = contracts.groupby('position').agg(
total_cap_hit=('cap_hit', 'sum'),
player_count=('player', 'count'),
max_contract=('cap_hit', 'max')
).reset_index()
position_spending['pct_of_cap'] = (
position_spending['total_cap_hit'] / cap * 100
)
# Compare to benchmarks
position_spending['benchmark_pct'] = position_spending['position'].map(
lambda x: POSITION_BENCHMARKS.get(x, PositionBenchmark(x, 5.0, 8.0, 1, 5.0)).avg_pct_of_cap
)
position_spending['vs_benchmark'] = (
position_spending['pct_of_cap'] - position_spending['benchmark_pct']
)
return position_spending.sort_values('pct_of_cap', ascending=False)
The Quarterback Dilemma
Elite QBs command 15-20% of the salary cap, creating roster-building tensions:
def analyze_qb_cap_impact(qb_cap_pct: float, cap: float) -> Dict:
"""
Analyze how QB salary affects roster construction.
Args:
qb_cap_pct: QB's percentage of salary cap
cap: Total salary cap
"""
qb_cap_hit = cap * (qb_cap_pct / 100)
remaining_cap = cap - qb_cap_hit
remaining_roster = 52 # Excluding QB
avg_per_player = remaining_cap / remaining_roster
# Benchmarks
elite_qb_pct = 18.0
rookie_qb_pct = 2.0
elite_remaining = cap * (1 - elite_qb_pct / 100) / remaining_roster
rookie_remaining = cap * (1 - rookie_qb_pct / 100) / remaining_roster
return {
'qb_cap_hit': qb_cap_hit,
'remaining_cap': remaining_cap,
'avg_per_other_player': avg_per_player,
'elite_qb_avg_per_player': elite_remaining,
'rookie_qb_avg_per_player': rookie_remaining,
'roster_advantage_vs_elite': (avg_per_player - elite_remaining) * remaining_roster
}
# Example: $45M QB on $225M cap
result = analyze_qb_cap_impact(20.0, 225.0)
# Remaining $180M for 52 players = $3.46M average
# vs rookie QB: $220.5M for 52 = $4.24M average
# Difference: ~$40M more to spend on supporting cast
Draft Value Analysis
The Draft Pick Value Chart
Draft picks have quantifiable expected value:
# Jimmy Johnson draft value chart (traditional)
JIMMY_JOHNSON_VALUES = {
1: 3000, 2: 2600, 3: 2200, 4: 1800, 5: 1700,
6: 1600, 7: 1500, 8: 1400, 9: 1350, 10: 1300,
11: 1250, 12: 1200, 13: 1150, 14: 1100, 15: 1050,
16: 1000, 17: 950, 18: 900, 19: 875, 20: 850,
32: 590, 33: 580, 64: 270, 65: 265,
96: 116, 97: 114, 128: 54, 129: 53,
160: 27, 192: 15, 224: 5, 256: 1
}
def get_jimmy_johnson_value(pick: int) -> float:
"""Get traditional draft value for a pick."""
if pick in JIMMY_JOHNSON_VALUES:
return JIMMY_JOHNSON_VALUES[pick]
# Interpolate
lower = max(k for k in JIMMY_JOHNSON_VALUES if k <= pick)
upper = min(k for k in JIMMY_JOHNSON_VALUES if k >= pick)
if lower == upper:
return JIMMY_JOHNSON_VALUES[lower]
lower_val = JIMMY_JOHNSON_VALUES[lower]
upper_val = JIMMY_JOHNSON_VALUES[upper]
# Linear interpolation
ratio = (pick - lower) / (upper - lower)
return lower_val - ratio * (lower_val - upper_val)
# Modern surplus value approach (AV-based)
def calculate_expected_surplus_value(pick: int, position: str) -> Dict:
"""
Calculate expected surplus value of a draft pick.
Surplus value = Expected performance value - Contract cost
"""
# Expected career AV by draft position (approximation)
# Based on historical data analysis
expected_av = max(0, 40 - pick * 0.35 + np.random.normal(0, 5))
# Rookie contract cost (4 years, scaled by pick)
if pick <= 32:
total_contract = 15 + (32 - pick) * 0.8 # First round premium
elif pick <= 64:
total_contract = 5 + (64 - pick) * 0.1
else:
total_contract = 3 + max(0, (100 - pick) * 0.02)
# Market value for equivalent AV
av_per_million = 0.8 # Rough conversion
market_value = expected_av / av_per_million
surplus_value = market_value - total_contract
return {
'pick': pick,
'position': position,
'expected_av': round(expected_av, 1),
'contract_cost': round(total_contract, 1),
'market_value': round(market_value, 1),
'surplus_value': round(surplus_value, 1)
}
Position Value in the Draft
Not all positions provide equal draft value:
# Position-specific hit rates and value
DRAFT_POSITION_VALUE = {
# (hit_rate_rd1, avg_years_as_starter, positional_scarcity)
'QB': (0.45, 8.0, 1.0),
'EDGE': (0.55, 6.0, 0.8),
'OT': (0.50, 7.0, 0.85),
'CB': (0.45, 5.5, 0.75),
'WR': (0.50, 5.0, 0.70),
'DT': (0.45, 6.0, 0.65),
'LB': (0.40, 5.0, 0.60),
'S': (0.40, 5.0, 0.55),
'IOL': (0.45, 6.0, 0.60),
'TE': (0.35, 5.0, 0.50),
'RB': (0.50, 3.5, 0.40),
}
def evaluate_draft_pick_value(pick: int, position: str,
player_grade: float) -> Dict:
"""
Evaluate expected value of a draft pick.
Args:
pick: Draft pick number
position: Position abbreviation
player_grade: Pre-draft grade (0-100)
"""
pos_data = DRAFT_POSITION_VALUE.get(position, (0.40, 5.0, 0.50))
hit_rate, avg_years, scarcity = pos_data
# Adjust hit rate for draft position
position_bonus = max(0, (32 - pick) / 32) * 0.15
adjusted_hit_rate = hit_rate + position_bonus
# Grade adjustment
grade_factor = (player_grade - 70) / 30 # Center around 70
adjusted_hit_rate = min(0.8, adjusted_hit_rate + grade_factor * 0.1)
# Expected value
expected_starter_years = avg_years * adjusted_hit_rate
expected_value = expected_starter_years * scarcity * 10
# Surplus value (rookie contract advantage)
rookie_contract_years = 4 if pick <= 32 else 4
fifth_year_option = pick <= 32
surplus_years = min(rookie_contract_years, expected_starter_years)
surplus_value = surplus_years * scarcity * 5
return {
'pick': pick,
'position': position,
'player_grade': player_grade,
'hit_probability': round(adjusted_hit_rate, 2),
'expected_starter_years': round(expected_starter_years, 1),
'expected_value': round(expected_value, 1),
'surplus_value': round(surplus_value, 1),
'fifth_year_option': fifth_year_option
}
Draft Capital Management
def calculate_draft_capital(picks: List[Tuple[int, int]]) -> Dict:
"""
Calculate total draft capital.
Args:
picks: List of (round, pick_in_round) tuples
"""
total_value = 0
round_values = {}
for round_num, pick_in_round in picks:
# Convert to overall pick
overall = (round_num - 1) * 32 + pick_in_round
value = get_jimmy_johnson_value(overall)
total_value += value
if round_num not in round_values:
round_values[round_num] = 0
round_values[round_num] += value
return {
'total_picks': len(picks),
'total_value': total_value,
'by_round': round_values,
'equivalent_pick': find_equivalent_single_pick(total_value)
}
def find_equivalent_single_pick(value: float) -> int:
"""Find single pick equivalent to given value."""
for pick in range(1, 260):
if get_jimmy_johnson_value(pick) <= value:
return pick
return 256
Free Agency Economics
Market Efficiency
Free agency is generally an inefficient market for buyers:
def analyze_free_agent_value(player_age: int, position: str,
aav: float, guaranteed: float,
performance_metric: float) -> Dict:
"""
Analyze free agent contract value.
Args:
player_age: Age at signing
position: Position
aav: Average annual value ($M)
guaranteed: Total guaranteed money ($M)
performance_metric: Prior year performance (EPA, PFF grade, etc.)
"""
pos_data = DRAFT_POSITION_VALUE.get(position, (0.40, 5.0, 0.50))
_, avg_career_years, _ = pos_data
# Expected remaining prime years
prime_end_by_position = {
'QB': 38, 'RB': 28, 'WR': 32, 'TE': 32,
'OT': 34, 'IOL': 33, 'EDGE': 31, 'DT': 31,
'LB': 30, 'CB': 30, 'S': 31
}
prime_end = prime_end_by_position.get(position, 30)
remaining_prime = max(0, prime_end - player_age)
# Age decline factor
if player_age <= 27:
age_factor = 1.0
elif player_age <= 30:
age_factor = 1.0 - (player_age - 27) * 0.05
else:
age_factor = 0.85 - (player_age - 30) * 0.1
# Expected performance value
expected_performance = performance_metric * age_factor
# Contract risk assessment
guaranteed_pct = guaranteed / (aav * 4) if aav > 0 else 0 # Assume 4-year deal
injury_risk = 1 - (player_age - 25) * 0.02 if player_age > 25 else 1
# Value assessment
cost_per_expected_year = aav / remaining_prime if remaining_prime > 0 else float('inf')
return {
'player_age': player_age,
'position': position,
'aav': aav,
'remaining_prime_years': remaining_prime,
'age_factor': round(age_factor, 2),
'expected_performance': round(expected_performance, 2),
'guaranteed_pct': round(guaranteed_pct, 2),
'cost_per_prime_year': round(cost_per_expected_year, 2),
'value_rating': 'good' if cost_per_expected_year < 12 else
'fair' if cost_per_expected_year < 18 else 'poor'
}
When Free Agency Makes Sense
def evaluate_fa_vs_draft(position: str, fa_aav: float,
fa_performance: float,
available_pick: int) -> Dict:
"""
Compare free agent acquisition to draft alternative.
Args:
position: Target position
fa_aav: Free agent AAV
fa_performance: Free agent expected performance (0-100)
available_pick: Best available draft pick
"""
# Draft alternative
draft_value = evaluate_draft_pick_value(available_pick, position, 75)
# Free agent over 4 years
fa_total_cost = fa_aav * 4
fa_expected_value = fa_performance * 4 # Immediate impact
# Draft over 4 years (including development time)
draft_expected_value = draft_value['expected_value'] * 0.7 # Risk discount
draft_cost = calculate_rookie_contract_cost(available_pick)
# Surplus comparison
fa_surplus = fa_expected_value - fa_total_cost / 3 # Performance - cost
draft_surplus = draft_expected_value - draft_cost / 3
return {
'position': position,
'fa_option': {
'cost_4yr': fa_total_cost,
'expected_value': fa_expected_value,
'surplus': round(fa_surplus, 1)
},
'draft_option': {
'pick': available_pick,
'cost_4yr': draft_cost,
'expected_value': round(draft_expected_value, 1),
'surplus': round(draft_surplus, 1)
},
'recommendation': 'free_agent' if fa_surplus > draft_surplus else 'draft'
}
def calculate_rookie_contract_cost(pick: int) -> float:
"""Estimate 4-year rookie contract cost."""
if pick <= 10:
return 35 + (10 - pick) * 2
elif pick <= 32:
return 15 + (32 - pick) * 0.6
elif pick <= 64:
return 5 + (64 - pick) * 0.1
else:
return 3.5
Roster Construction Strategies
The Competitive Window
Teams typically have 3-5 year windows to compete:
@dataclass
class CompetitiveWindow:
"""Analysis of team's competitive window."""
team: str
window_start: int
window_end: int
core_players: List[str]
cap_flexibility: float
draft_capital_rating: float
championship_probability: float
def analyze_competitive_window(roster: pd.DataFrame,
contracts: pd.DataFrame,
draft_picks: List,
current_year: int) -> CompetitiveWindow:
"""
Analyze team's competitive window.
Args:
roster: Current roster with age, position, performance
contracts: Contract details with years remaining
draft_picks: Future draft capital
current_year: Current season
"""
# Identify core players (top performers with years remaining)
core = roster.merge(contracts, on='player')
core = core[
(core['performance_rating'] >= 80) &
(core['years_remaining'] >= 2)
]
core_players = core['player'].tolist()
# Calculate window based on core player contracts
if len(core) > 0:
window_end = current_year + int(core['years_remaining'].median())
else:
window_end = current_year + 1
# Cap flexibility
total_committed = contracts[contracts['year'] == current_year + 1]['cap_hit'].sum()
projected_cap = 235.0 # Next year projection
cap_flexibility = (projected_cap - total_committed) / projected_cap
# Draft capital
draft_value = sum(get_jimmy_johnson_value(p[0] * 32 + p[1])
for p in draft_picks)
draft_capital_rating = min(100, draft_value / 50)
# Championship probability (simplified)
roster_strength = roster['performance_rating'].mean() / 100
qb_factor = roster[roster['position'] == 'QB']['performance_rating'].max() / 100
championship_prob = roster_strength * 0.4 + qb_factor * 0.4 + cap_flexibility * 0.2
return CompetitiveWindow(
team=roster['team'].iloc[0] if 'team' in roster.columns else 'Unknown',
window_start=current_year,
window_end=window_end,
core_players=core_players[:5], # Top 5
cap_flexibility=round(cap_flexibility, 2),
draft_capital_rating=round(draft_capital_rating, 1),
championship_probability=round(championship_prob, 2)
)
Build vs Buy Decision Framework
def build_vs_buy_analysis(team_state: str, position_need: str,
cap_space: float, draft_capital: float) -> Dict:
"""
Recommend build (draft) vs buy (FA) strategy.
Args:
team_state: 'rebuilding', 'competitive', 'contending'
position_need: Position to address
cap_space: Available cap space ($M)
draft_capital: Total draft value available
"""
recommendations = {
'rebuilding': {
'primary': 'draft',
'rationale': 'Accumulate cheap talent, extend window',
'fa_approach': 'short_term_prove_it_deals',
'trade_approach': 'sell_veterans_for_picks'
},
'competitive': {
'primary': 'balanced',
'rationale': 'Mix of draft and targeted FA',
'fa_approach': 'value_signings_under_30',
'trade_approach': 'opportunistic_both_directions'
},
'contending': {
'primary': 'buy',
'rationale': 'Maximize current window',
'fa_approach': 'premium_signings_for_key_positions',
'trade_approach': 'trade_picks_for_players'
}
}
base_rec = recommendations.get(team_state, recommendations['competitive'])
# Adjust for cap space
if cap_space < 20:
base_rec['cap_constraint'] = 'Must rely on draft/trades'
elif cap_space > 50:
base_rec['cap_opportunity'] = 'Can pursue multiple FA targets'
# Position-specific guidance
if position_need == 'QB' and team_state != 'rebuilding':
base_rec['position_note'] = 'QB via draft is preferred for cap efficiency'
elif position_need in ['EDGE', 'OT'] and team_state == 'contending':
base_rec['position_note'] = f'{position_need} worth premium FA investment'
elif position_need == 'RB':
base_rec['position_note'] = 'RB rarely worth significant FA investment'
return base_rec
Position Value Rankings
Wins Above Replacement by Position
def calculate_positional_war(pbp: pd.DataFrame,
roster: pd.DataFrame) -> pd.DataFrame:
"""
Calculate approximate WAR by position.
Based on EPA contribution and replacement level.
"""
# Position replacement levels (EPA/play)
REPLACEMENT_LEVELS = {
'QB': -0.10,
'RB': -0.05,
'WR': -0.02,
'TE': -0.03,
'OL': -0.04, # Combined
'EDGE': -0.03,
'DT': -0.02,
'LB': -0.02,
'CB': -0.04,
'S': -0.02
}
# Snap-weighted EPA contribution
# This is a simplified model
position_war = []
for position in REPLACEMENT_LEVELS.keys():
pos_players = roster[roster['position_group'] == position]
avg_epa = pos_players['epa_per_play'].mean() if len(pos_players) > 0 else 0
replacement = REPLACEMENT_LEVELS[position]
epa_above_replacement = avg_epa - replacement
# Snap weight (more snaps = more value)
snap_weights = {
'QB': 1.0, 'OL': 0.9, 'WR': 0.7, 'RB': 0.5,
'TE': 0.5, 'EDGE': 0.6, 'DT': 0.5, 'LB': 0.6,
'CB': 0.7, 'S': 0.6
}
war = epa_above_replacement * snap_weights.get(position, 0.5) * 100
position_war.append({
'position': position,
'avg_epa': round(avg_epa, 3),
'replacement_level': replacement,
'epa_above_replacement': round(epa_above_replacement, 3),
'war': round(war, 1)
})
return pd.DataFrame(position_war).sort_values('war', ascending=False)
def calculate_cost_per_war(contracts: pd.DataFrame,
war_values: pd.DataFrame) -> pd.DataFrame:
"""
Calculate cost per WAR by position.
Lower is better (more efficient spending).
"""
merged = contracts.merge(war_values, on='position')
merged['cost_per_war'] = merged['aav'] / merged['war'].replace(0, 0.1)
efficiency = merged.groupby('position').agg(
total_spend=('aav', 'sum'),
total_war=('war', 'sum'),
avg_cost_per_war=('cost_per_war', 'mean')
).reset_index()
efficiency['efficiency_rank'] = efficiency['avg_cost_per_war'].rank()
return efficiency.sort_values('efficiency_rank')
The Running Back Devaluation
def analyze_rb_value():
"""
Demonstrate why RBs are devalued in modern NFL.
Key factors:
1. Short career spans
2. High replacement level
3. Committee approaches work
4. Receiving backs vs pure rushers
"""
analysis = {
'avg_rb_career': 2.57, # Years as primary back
'avg_edge_career': 5.2,
'avg_wr_career': 4.8,
'replacement_gap': {
'RB': 0.02, # EPA difference starter vs backup
'WR': 0.08,
'EDGE': 0.12,
'QB': 0.25
},
'draft_capital_roi': {
'RB_round_1': 0.65, # Value vs cost
'RB_round_3': 0.85,
'WR_round_1': 0.80,
'EDGE_round_1': 0.90
},
'recommendation': 'Draft RBs in rounds 3-5, avoid premium FA deals',
'exceptions': 'Elite receiving backs (Austin Ekeler type) have more value'
}
return analysis
Practical Implementation
Complete Roster Evaluator
from dataclasses import dataclass
from typing import Dict, List, Optional
import pandas as pd
import numpy as np
@dataclass
class RosterReport:
"""Comprehensive roster evaluation report."""
team: str
season: int
# Overall
total_cap_used: float
cap_space: float
dead_money: float
# Position spending
qb_pct: float
offense_pct: float
defense_pct: float
# Roster composition
avg_age: float
players_under_26: int
players_over_30: int
# Contract health
players_in_final_year: int
total_guaranteed_remaining: float
# Performance
roster_war: float
cost_efficiency: float
# Window analysis
competitive_window_years: int
window_phase: str
class RosterAnalyzer:
"""
Comprehensive roster construction analyzer.
Example usage:
roster = pd.DataFrame(...) # Player data
contracts = pd.DataFrame(...) # Contract data
analyzer = RosterAnalyzer(roster, contracts, 2023)
report = analyzer.generate_report('KC')
recommendations = analyzer.get_recommendations('KC')
"""
def __init__(self, roster: pd.DataFrame, contracts: pd.DataFrame,
season: int, cap: float = 224.8):
"""
Initialize analyzer.
Args:
roster: Player roster with position, age, performance metrics
contracts: Contract details with cap_hit, years_remaining, guaranteed
season: Current season
cap: Salary cap amount
"""
self.roster = roster
self.contracts = contracts
self.season = season
self.cap = cap
# Merge data
self.full_data = roster.merge(contracts, on='player', how='left')
def calculate_cap_allocation(self, team: str) -> Dict:
"""Calculate cap spending breakdown."""
team_data = self.full_data[self.full_data['team'] == team]
total_cap_hit = team_data['cap_hit'].sum()
cap_space = self.cap - total_cap_hit
dead_money = team_data[team_data['on_roster'] == False]['cap_hit'].sum() if 'on_roster' in team_data.columns else 0
# Position groups
offense_positions = ['QB', 'RB', 'WR', 'TE', 'OT', 'IOL', 'C']
defense_positions = ['EDGE', 'DT', 'LB', 'CB', 'S']
offense_cap = team_data[
team_data['position'].isin(offense_positions)
]['cap_hit'].sum()
defense_cap = team_data[
team_data['position'].isin(defense_positions)
]['cap_hit'].sum()
qb_cap = team_data[team_data['position'] == 'QB']['cap_hit'].sum()
return {
'total_cap_hit': total_cap_hit,
'cap_space': cap_space,
'dead_money': dead_money,
'qb_pct': qb_cap / self.cap * 100,
'offense_pct': offense_cap / self.cap * 100,
'defense_pct': defense_cap / self.cap * 100
}
def analyze_roster_age(self, team: str) -> Dict:
"""Analyze roster age distribution."""
team_data = self.full_data[self.full_data['team'] == team]
return {
'avg_age': team_data['age'].mean(),
'median_age': team_data['age'].median(),
'under_26': len(team_data[team_data['age'] < 26]),
'over_30': len(team_data[team_data['age'] > 30]),
'age_distribution': team_data['age'].describe().to_dict()
}
def analyze_contract_health(self, team: str) -> Dict:
"""Analyze contract situation."""
team_data = self.full_data[self.full_data['team'] == team]
return {
'players_final_year': len(team_data[team_data['years_remaining'] == 1]),
'total_guaranteed': team_data['guaranteed_remaining'].sum(),
'avg_years_remaining': team_data['years_remaining'].mean(),
'upcoming_fa': team_data[
team_data['years_remaining'] == 1
][['player', 'position', 'cap_hit']].to_dict('records')
}
def calculate_roster_war(self, team: str) -> float:
"""Calculate total roster WAR."""
team_data = self.full_data[self.full_data['team'] == team]
# Simplified WAR calculation
if 'war' in team_data.columns:
return team_data['war'].sum()
elif 'performance_rating' in team_data.columns:
# Estimate from performance rating
return (team_data['performance_rating'].sum() - 50 * len(team_data)) / 10
else:
return 0
def estimate_competitive_window(self, team: str) -> Tuple[int, str]:
"""Estimate competitive window."""
team_data = self.full_data[self.full_data['team'] == team]
# Core player contracts
core_players = team_data[
team_data['performance_rating'] >= 80
] if 'performance_rating' in team_data.columns else team_data.head(10)
if len(core_players) == 0:
return 1, 'rebuilding'
avg_years_remaining = core_players['years_remaining'].mean()
# QB situation
qb_data = team_data[team_data['position'] == 'QB']
if len(qb_data) > 0:
qb_age = qb_data['age'].iloc[0]
qb_quality = qb_data['performance_rating'].iloc[0] if 'performance_rating' in qb_data.columns else 70
else:
qb_age = 30
qb_quality = 50
# Calculate window
if qb_quality < 60 or qb_age > 37:
window = min(2, int(avg_years_remaining))
phase = 'rebuilding' if qb_quality < 50 else 'retooling'
elif qb_quality >= 85 and avg_years_remaining >= 3:
window = int(min(5, avg_years_remaining + 1))
phase = 'contending'
else:
window = int(avg_years_remaining)
phase = 'competitive'
return window, phase
def generate_report(self, team: str) -> RosterReport:
"""Generate comprehensive roster report."""
cap = self.calculate_cap_allocation(team)
age = self.analyze_roster_age(team)
contracts = self.analyze_contract_health(team)
war = self.calculate_roster_war(team)
window, phase = self.estimate_competitive_window(team)
# Cost efficiency
if war > 0:
efficiency = cap['total_cap_hit'] / war
else:
efficiency = float('inf')
return RosterReport(
team=team,
season=self.season,
total_cap_used=round(cap['total_cap_hit'], 1),
cap_space=round(cap['cap_space'], 1),
dead_money=round(cap['dead_money'], 1),
qb_pct=round(cap['qb_pct'], 1),
offense_pct=round(cap['offense_pct'], 1),
defense_pct=round(cap['defense_pct'], 1),
avg_age=round(age['avg_age'], 1),
players_under_26=age['under_26'],
players_over_30=age['over_30'],
players_in_final_year=contracts['players_final_year'],
total_guaranteed_remaining=round(contracts['total_guaranteed'], 1),
roster_war=round(war, 1),
cost_efficiency=round(efficiency, 1),
competitive_window_years=window,
window_phase=phase
)
def get_recommendations(self, team: str) -> List[str]:
"""Generate roster construction recommendations."""
report = self.generate_report(team)
recommendations = []
# Cap recommendations
if report.cap_space < 10:
recommendations.append("Limited cap space - prioritize extensions over FA")
elif report.cap_space > 50:
recommendations.append("Significant cap space - consider strategic FA additions")
# Age recommendations
if report.avg_age > 27:
recommendations.append("Roster skews old - focus on youth infusion via draft")
if report.players_under_26 < 15:
recommendations.append("Need more young talent - prioritize draft capital")
# QB recommendations
if report.qb_pct > 18:
recommendations.append("High QB cap hit - must excel at value positions elsewhere")
elif report.qb_pct < 5:
recommendations.append("Rookie QB window - maximize spending elsewhere now")
# Window recommendations
if report.window_phase == 'contending':
recommendations.append("Contending window - trade future picks for immediate help")
elif report.window_phase == 'rebuilding':
recommendations.append("Rebuilding - accumulate draft picks, avoid long-term FA deals")
# Contract recommendations
if report.players_in_final_year > 15:
recommendations.append("Many pending FAs - prioritize extension decisions")
return recommendations
def print_roster_report(report: RosterReport) -> None:
"""Pretty print roster report."""
print(f"\n{'='*55}")
print(f"Roster Report: {report.team}")
print(f"Season: {report.season}")
print(f"{'='*55}")
print(f"\n--- Cap Situation ---")
print(f"Total Cap Used: ${report.total_cap_used}M")
print(f"Cap Space: ${report.cap_space}M")
print(f"Dead Money: ${report.dead_money}M")
print(f"\n--- Cap Allocation ---")
print(f"QB: {report.qb_pct:.1f}%")
print(f"Offense: {report.offense_pct:.1f}%")
print(f"Defense: {report.defense_pct:.1f}%")
print(f"\n--- Roster Composition ---")
print(f"Average Age: {report.avg_age}")
print(f"Players Under 26: {report.players_under_26}")
print(f"Players Over 30: {report.players_over_30}")
print(f"\n--- Contract Health ---")
print(f"Pending FAs: {report.players_in_final_year}")
print(f"Guaranteed $: ${report.total_guaranteed_remaining}M")
print(f"\n--- Performance ---")
print(f"Roster WAR: {report.roster_war}")
print(f"Cost Efficiency: ${report.cost_efficiency}M per WAR")
print(f"\n--- Competitive Window ---")
print(f"Window: {report.competitive_window_years} years")
print(f"Phase: {report.window_phase}")
print(f"\n{'='*55}\n")
Summary
Key Takeaways:
- The salary cap is a hard constraint - Elite roster construction works within ~$225M limit
- Draft picks have calculable surplus value - Rookie contracts provide cost-controlled talent
- Position value varies dramatically - QB and EDGE worth more than RB and S
- Free agency is generally inefficient - Buyers typically overpay for immediate help
- Competitive windows are real - Teams have 3-5 years to maximize chances
Roster Construction Principles:
- Build through the draft for cost-controlled talent
- Pay premium only for premium positions (QB, EDGE, OT, CB)
- Avoid long-term RB contracts
- Manage the cap to maintain flexibility
- Understand your competitive window and act accordingly
Preview: Part 4
With Part 3 complete, we've covered team-level analytics including efficiency metrics, play calling, situational football, home field advantage, strength of schedule, and roster construction.
Part 4: Predictive Modeling will build on these foundations to create actual prediction models: - Chapter 18: Introduction to Prediction Models - Chapter 19: Elo and Power Ratings - Chapter 20: Machine Learning for NFL Prediction - Chapter 21: Game Simulation - Chapter 22: Betting Market Analysis
References
- Over The Cap. "NFL Salary Cap Data"
- Spotrac. "NFL Team Contracts"
- Football Outsiders. "Draft Value Analysis"
- The Athletic. "NFL Roster Construction Deep Dives"
- PFF. "WAR and Position Value Research"