The NFL Draft represents the primary mechanism for talent acquisition, making draft evaluation one of the highest-stakes applications of football analytics. Teams invest millions in scouting departments, analytics staff, and prospect evaluation—yet...
In This Chapter
- Introduction
- Section 1: The Draft Evaluation Framework
- Section 2: College Production Analysis
- Section 3: Physical Testing and Combine Analysis
- Section 4: Positional Projection Models
- Section 5: Draft Value Analysis
- Section 6: Building a Draft Board
- Section 7: Case Study: Evaluating a QB Class
- Chapter Summary
- Conclusion: Part 6 Complete
- Key Equations Reference
Chapter 28: Draft Analysis
Introduction
The NFL Draft represents the primary mechanism for talent acquisition, making draft evaluation one of the highest-stakes applications of football analytics. Teams invest millions in scouting departments, analytics staff, and prospect evaluation—yet draft success remains notoriously difficult to predict. This chapter explores the analytical frameworks used to evaluate NFL Draft prospects, from college production models to physical testing data.
Draft analytics operates under unique constraints: limited sample sizes, developmental uncertainty, and the transition gap between college and professional football. Understanding these limitations while extracting maximum signal from available data defines the modern approach to prospect evaluation.
Section 1: The Draft Evaluation Framework
1.1 What Makes Draft Prediction Difficult
Fundamental Challenges:
-
Sample Size Limitations - Prospects have 30-50 college games - Limited snaps at NFL-quality competition - Injury history often incomplete
-
Developmental Uncertainty - College-to-NFL transition varies by player - Coaching and scheme fit impact outcomes - Character and work ethic difficult to quantify
-
System Dependencies - College production heavily scheme-dependent - Competition level varies dramatically - Role in college may differ from NFL role
-
Selection Bias - Only see outcomes for drafted players - Undrafted successes complicate models - Draft position affects opportunity
1.2 Data Sources for Draft Evaluation
Primary Data Categories:
| Category | Sources | Signal Strength |
|---|---|---|
| College Production | Stats, PFF grades | Moderate-High |
| Physical Testing | Combine, Pro Days | Moderate |
| Biographical | Age, experience | Moderate |
| Contextual | Competition, scheme | High |
| Subjective | Interviews, film | Variable |
@dataclass
class ProspectProfile:
"""Complete prospect evaluation profile."""
name: str
position: str
school: str
conference: str
# Production
college_stats: Dict[str, float]
production_grades: Dict[str, float]
# Physical
combine_metrics: Dict[str, float]
size_metrics: Dict[str, float]
# Context
age_at_draft: float
years_started: int
competition_level: str
# Subjective
character_grade: Optional[float] = None
medical_grade: Optional[float] = None
Section 2: College Production Analysis
2.1 Production Metrics by Position
Different positions require different evaluation metrics:
Quarterback Metrics: | Metric | NFL Correlation | Weight | |--------|-----------------|--------| | Adjusted Completion % | 0.35 | High | | TD:INT Ratio | 0.30 | High | | Yards per Attempt | 0.32 | High | | Pressure-to-Sack Rate | 0.28 | Moderate | | Deep Ball Accuracy | 0.25 | Moderate |
Running Back Metrics: | Metric | NFL Correlation | Weight | |--------|-----------------|--------| | Yards After Contact/Att | 0.38 | High | | Breakaway Run Rate | 0.32 | High | | Receiving Proficiency | 0.35 | High | | Fumble Rate | 0.25 | Moderate | | Usage Rate | 0.20 | Low |
Wide Receiver Metrics: | Metric | NFL Correlation | Weight | |--------|-----------------|--------| | Yards per Route Run | 0.42 | High | | Contested Catch Rate | 0.35 | High | | Separation Metrics | 0.38 | High | | Drop Rate | 0.30 | Moderate | | Target Share | 0.28 | Moderate |
2.2 Production Normalization
Raw college stats require adjustment for context:
class ProductionNormalizer:
"""Normalize college production for comparison."""
# Conference strength multipliers
CONFERENCE_FACTORS = {
'SEC': 1.15,
'Big Ten': 1.10,
'ACC': 1.05,
'Big 12': 1.00,
'Pac-12': 1.00,
'AAC': 0.90,
'Mountain West': 0.85,
'Sun Belt': 0.80,
'MAC': 0.80,
'Other FBS': 0.75,
'FCS': 0.65
}
def normalize_production(self, raw_stat: float, conference: str,
position: str, era: int) -> float:
"""
Normalize college production stat.
Args:
raw_stat: Raw production number
conference: Conference played in
position: Player position
era: Season year (for era adjustment)
Returns:
Normalized production value
"""
# Conference adjustment
conf_factor = self.CONFERENCE_FACTORS.get(conference, 0.85)
# Era adjustment (college football evolution)
if era >= 2020:
era_factor = 1.0
elif era >= 2015:
era_factor = 0.95
else:
era_factor = 0.90
# Position-specific adjustments
pos_factor = self._get_position_factor(position, conference)
return raw_stat * conf_factor * era_factor * pos_factor
def _get_position_factor(self, position: str, conference: str) -> float:
"""Get position-specific normalization factor."""
# Air raid QBs inflate stats; run-heavy offenses suppress
if position == 'QB':
pass_heavy_conf = ['Big 12', 'Pac-12', 'AAC']
if conference in pass_heavy_conf:
return 0.92 # Deflate air raid QBs
return 1.0
return 1.0
2.3 Breakout Age Analysis
When production occurs matters as much as volume:
def calculate_breakout_age(seasons: List[Dict], threshold_percentile: float = 80) -> float:
"""
Calculate age at production breakout.
Args:
seasons: List of season stat dicts with age
threshold_percentile: Production percentile to define breakout
Returns:
Age at breakout season (or None if no breakout)
"""
for season in sorted(seasons, key=lambda x: x['age']):
if season.get('production_percentile', 0) >= threshold_percentile:
return season['age']
return None # No breakout
def evaluate_breakout_age(breakout_age: float, position: str) -> str:
"""
Evaluate breakout age quality.
Early breakouts correlate with NFL success.
"""
thresholds = {
'WR': {'elite': 19.5, 'good': 20.5, 'concern': 21.5},
'RB': {'elite': 19.0, 'good': 20.0, 'concern': 21.0},
'QB': {'elite': 20.0, 'good': 21.0, 'concern': 22.0},
'TE': {'elite': 20.5, 'good': 21.5, 'concern': 22.5}
}
pos_thresh = thresholds.get(position, thresholds['WR'])
if breakout_age <= pos_thresh['elite']:
return "ELITE - Early breakout indicates high NFL upside"
elif breakout_age <= pos_thresh['good']:
return "GOOD - Age-appropriate production timeline"
elif breakout_age <= pos_thresh['concern']:
return "CONCERN - Late breakout reduces confidence"
else:
return "RED FLAG - Very late production, high bust risk"
2.4 Dominator Rating
Market share of team production indicates alpha potential:
def calculate_dominator_rating(player_stats: Dict,
team_stats: Dict,
position: str) -> float:
"""
Calculate Dominator Rating (share of team production).
Args:
player_stats: Player's season statistics
team_stats: Team's total statistics
position: Player position
Returns:
Dominator Rating (0-100 scale)
"""
if position == 'WR' or position == 'TE':
# Receiving dominator
rec_yards_share = player_stats['rec_yards'] / team_stats['total_rec_yards']
rec_td_share = player_stats['rec_tds'] / team_stats['total_rec_tds']
dominator = (rec_yards_share + rec_td_share) / 2 * 100
elif position == 'RB':
# Rushing dominator
rush_yards_share = player_stats['rush_yards'] / team_stats['total_rush_yards']
rush_td_share = player_stats['rush_tds'] / team_stats['total_rush_tds']
dominator = (rush_yards_share + rush_td_share) / 2 * 100
else:
dominator = 0 # Other positions
return dominator
Dominator Rating Thresholds:
| Rating | WR Interpretation | RB Interpretation |
|---|---|---|
| 35%+ | Elite alpha | Bell cow potential |
| 25-35% | Quality starter | Lead back traits |
| 15-25% | Role player | Committee back |
| <15% | Limited upside | Depth piece |
Section 3: Physical Testing and Combine Analysis
3.1 Combine Metrics
The NFL Combine provides standardized athletic testing:
Key Tests by Position:
| Position | Primary Tests | Secondary Tests |
|---|---|---|
| QB | 40-yard dash, Arm strength | Short shuttle |
| RB | 40-yard dash, 3-cone | Vertical, Bench |
| WR | 40-yard dash, Vertical | 3-cone, Broad jump |
| TE | 40-yard dash, Bench | Vertical, 3-cone |
| OL | 40-yard dash, Bench | Short shuttle |
| DL | 40-yard dash, Bench | 3-cone |
| LB | 40-yard dash, 3-cone | Vertical |
| CB | 40-yard dash, Vertical | Short shuttle |
| S | 40-yard dash, Vertical | 3-cone |
3.2 Size-Speed Scoring
class SizeSpeedScorer:
"""Calculate composite size-speed scores."""
def calculate_speed_score(self, weight: float, forty_time: float) -> float:
"""
Calculate Bill Barnwell's Speed Score.
Speed Score = (Weight × 200) / (40 time)^4
Args:
weight: Player weight in pounds
forty_time: 40-yard dash time
Returns:
Speed Score
"""
return (weight * 200) / (forty_time ** 4)
def calculate_height_adjusted_speed(self, height_inches: float,
forty_time: float) -> float:
"""
Calculate Height-Adjusted Speed Score (HASS).
Taller players naturally run slower; adjust for height.
Args:
height_inches: Height in inches
forty_time: 40-yard dash time
Returns:
HASS score
"""
# Expected 40 time increases ~0.03 sec per inch above 72"
expected_penalty = max(0, (height_inches - 72) * 0.03)
adjusted_time = forty_time - expected_penalty
return 100 - (adjusted_time * 20) # Scale to 0-100
def calculate_agility_score(self, three_cone: float, shuttle: float,
position: str) -> float:
"""
Calculate composite agility score.
Args:
three_cone: 3-cone drill time
shuttle: Short shuttle time
position: Player position
Returns:
Agility composite (0-100)
"""
# Position-specific baselines
baselines = {
'WR': {'three_cone': 6.85, 'shuttle': 4.15},
'RB': {'three_cone': 6.95, 'shuttle': 4.20},
'CB': {'three_cone': 6.80, 'shuttle': 4.10},
'LB': {'three_cone': 7.05, 'shuttle': 4.25}
}
baseline = baselines.get(position, {'three_cone': 7.00, 'shuttle': 4.20})
# Calculate percentile vs baseline
three_cone_score = (baseline['three_cone'] - three_cone) / 0.15 * 10 + 50
shuttle_score = (baseline['shuttle'] - shuttle) / 0.10 * 10 + 50
return (three_cone_score + shuttle_score) / 2
3.3 Athletic Profiles
Different positions require different athletic profiles:
class AthleticProfiler:
"""Create athletic profiles from combine data."""
# Position-specific percentile thresholds
ELITE_THRESHOLDS = {
'WR': {
'forty': 4.45,
'vertical': 38,
'broad': 126,
'three_cone': 6.75
},
'RB': {
'forty': 4.48,
'vertical': 36,
'broad': 122,
'bench': 22
},
'TE': {
'forty': 4.60,
'vertical': 34,
'broad': 120,
'bench': 20
},
'CB': {
'forty': 4.42,
'vertical': 38,
'three_cone': 6.70,
'shuttle': 4.05
}
}
def create_profile(self, combine_data: Dict, position: str) -> Dict:
"""
Create athletic profile with percentiles.
Args:
combine_data: Raw combine measurements
position: Player position
Returns:
Profile with percentile rankings
"""
profile = {}
thresholds = self.ELITE_THRESHOLDS.get(position, {})
for metric, value in combine_data.items():
if metric in thresholds:
elite_mark = thresholds[metric]
if metric in ['forty', 'three_cone', 'shuttle']:
# Lower is better
percentile = 50 + (elite_mark - value) / 0.05 * 10
else:
# Higher is better
percentile = 50 + (value - elite_mark) / 2 * 10
profile[metric] = {
'value': value,
'percentile': min(99, max(1, percentile)),
'elite': self._is_elite(value, elite_mark, metric)
}
return profile
def _is_elite(self, value: float, threshold: float, metric: str) -> bool:
"""Check if value is elite for metric."""
if metric in ['forty', 'three_cone', 'shuttle']:
return value <= threshold
return value >= threshold
3.4 RAS (Relative Athletic Score)
Kent Lee Platte's RAS provides a composite athletic metric:
def calculate_ras(combine_data: Dict, position: str,
historical_data: pd.DataFrame) -> float:
"""
Calculate Relative Athletic Score (RAS).
RAS compares a player's combine metrics to historical
data at their position, producing a 0-10 scale.
Args:
combine_data: Player's combine results
position: Player position
historical_data: Historical combine data for position
Returns:
RAS score (0-10 scale)
"""
metrics = ['forty', 'vertical', 'broad', 'bench', 'three_cone', 'shuttle']
percentiles = []
for metric in metrics:
if metric in combine_data and metric in historical_data.columns:
value = combine_data[metric]
pos_data = historical_data[historical_data['position'] == position][metric]
if metric in ['forty', 'three_cone', 'shuttle']:
# Lower is better
percentile = (pos_data > value).mean() * 100
else:
# Higher is better
percentile = (pos_data < value).mean() * 100
percentiles.append(percentile)
if not percentiles:
return 5.0 # Default neutral score
# Average percentile scaled to 0-10
return np.mean(percentiles) / 10
Section 4: Positional Projection Models
4.1 Quarterback Evaluation Model
class QBProjectionModel:
"""Project quarterback NFL success probability."""
def __init__(self):
self.feature_weights = {
'adj_completion_pct': 0.20,
'td_int_ratio': 0.15,
'ypa': 0.15,
'pressure_performance': 0.12,
'deep_accuracy': 0.08,
'mobility': 0.10,
'conference_strength': 0.10,
'age': 0.05,
'experience': 0.05
}
def evaluate(self, prospect: Dict) -> Dict:
"""
Evaluate QB prospect.
Args:
prospect: QB evaluation data
Returns:
Evaluation results
"""
scores = {}
# Production metrics
scores['production'] = self._score_production(prospect)
# Physical traits
scores['athleticism'] = self._score_athleticism(prospect)
# Contextual factors
scores['context'] = self._score_context(prospect)
# Calculate weighted total
total = sum(
scores[cat] * self.feature_weights.get(cat, 0.1)
for cat in scores
) / sum(self.feature_weights.values())
return {
'scores': scores,
'composite': total,
'projection': self._get_projection_tier(total),
'comparable': self._find_comparable(prospect, total)
}
def _score_production(self, prospect: Dict) -> float:
"""Score production metrics."""
adj_comp = prospect.get('adj_completion_pct', 60)
td_int = prospect.get('td_int_ratio', 2.0)
ypa = prospect.get('ypa', 7.5)
# Normalize to 0-100 scale
comp_score = (adj_comp - 55) / 0.15
td_int_score = (td_int - 1.5) / 0.03
ypa_score = (ypa - 6.5) / 0.02
return (comp_score + td_int_score + ypa_score) / 3
def _score_athleticism(self, prospect: Dict) -> float:
"""Score physical traits."""
forty = prospect.get('forty', 4.85)
arm_strength = prospect.get('arm_strength', 55) # MPH
# Mobile QB boost
mobility_score = (5.0 - forty) / 0.05 * 10 + 50
# Arm strength
arm_score = (arm_strength - 50) / 0.5 * 10 + 50
return (mobility_score + arm_score) / 2
def _score_context(self, prospect: Dict) -> float:
"""Score contextual factors."""
conf_factor = ProductionNormalizer.CONFERENCE_FACTORS.get(
prospect.get('conference', 'Other FBS'), 0.85
)
age_penalty = max(0, (prospect.get('age', 22) - 22) * 5)
return conf_factor * 100 - age_penalty
def _get_projection_tier(self, score: float) -> str:
"""Convert score to projection tier."""
if score >= 80:
return "FRANCHISE QB - High probability of long-term starter"
elif score >= 65:
return "QUALITY STARTER - Should be multi-year starter"
elif score >= 50:
return "DEVELOPMENTAL - Could develop into starter"
elif score >= 35:
return "BACKUP - Projects as quality backup"
else:
return "UNLIKELY - Low probability of NFL success"
def _find_comparable(self, prospect: Dict, score: float) -> str:
"""Find historical comparable player."""
# Simplified comparable logic
if score >= 80 and prospect.get('mobility', 4.8) < 4.65:
return "Lamar Jackson / Kyler Murray"
elif score >= 75:
return "Justin Herbert / Joe Burrow"
elif score >= 65:
return "Mac Jones / Jared Goff"
else:
return "Mid-round developmental prospect"
4.2 Wide Receiver Projection Model
class WRProjectionModel:
"""Project wide receiver NFL success probability."""
def __init__(self):
self.key_metrics = [
'yards_per_route_run',
'contested_catch_rate',
'breakout_age',
'dominator_rating',
'forty_time',
'vertical'
]
def evaluate(self, prospect: Dict) -> Dict:
"""
Evaluate WR prospect.
Args:
prospect: WR evaluation data
Returns:
Comprehensive evaluation
"""
results = {}
# Production score
results['production_score'] = self._production_score(prospect)
# Athletic score
results['athletic_score'] = self._athletic_score(prospect)
# Profile score (breakout age, dominator)
results['profile_score'] = self._profile_score(prospect)
# Composite
composite = (
results['production_score'] * 0.40 +
results['athletic_score'] * 0.30 +
results['profile_score'] * 0.30
)
results['composite'] = composite
results['archetype'] = self._determine_archetype(prospect)
results['projection'] = self._project_outcome(composite, prospect)
return results
def _production_score(self, prospect: Dict) -> float:
"""Score college production."""
yprr = prospect.get('yards_per_route_run', 2.0)
contested = prospect.get('contested_catch_rate', 50)
drop_rate = prospect.get('drop_rate', 8)
# Normalize each metric
yprr_score = min(100, (yprr - 1.5) / 0.01)
contested_score = min(100, (contested - 40) / 0.3)
drop_score = max(0, 100 - (drop_rate * 5))
return (yprr_score + contested_score + drop_score) / 3
def _athletic_score(self, prospect: Dict) -> float:
"""Score athleticism."""
forty = prospect.get('forty', 4.55)
vertical = prospect.get('vertical', 35)
three_cone = prospect.get('three_cone', 7.0)
# Calculate composite
speed_score = (4.60 - forty) / 0.03 * 10 + 50
explosion_score = (vertical - 30) / 0.5 * 10 + 50
agility_score = (7.10 - three_cone) / 0.05 * 10 + 50
return (speed_score + explosion_score + agility_score) / 3
def _profile_score(self, prospect: Dict) -> float:
"""Score prospect profile metrics."""
breakout = prospect.get('breakout_age', 21)
dominator = prospect.get('dominator_rating', 20)
experience = prospect.get('years_started', 2)
# Breakout age (lower is better)
if breakout <= 19.5:
breakout_score = 100
elif breakout <= 20.5:
breakout_score = 75
elif breakout <= 21.5:
breakout_score = 50
else:
breakout_score = 25
# Dominator rating
dominator_score = min(100, dominator * 3)
# Experience
exp_score = min(100, experience * 30)
return (breakout_score + dominator_score + exp_score) / 3
def _determine_archetype(self, prospect: Dict) -> str:
"""Determine WR archetype."""
height = prospect.get('height', 72)
weight = prospect.get('weight', 200)
forty = prospect.get('forty', 4.50)
yprr = prospect.get('yards_per_route_run', 2.0)
if forty < 4.40 and yprr > 2.5:
return "BURNER - Elite speed, vertical threat"
elif height >= 74 and weight >= 215 and prospect.get('contested_catch_rate', 50) > 55:
return "X-RECEIVER - Alpha, contested-catch specialist"
elif forty < 4.50 and prospect.get('three_cone', 7.0) < 6.85:
return "SLOT - Quick, agile separator"
elif height >= 73 and forty < 4.50:
return "ALL-AROUND - Complete receiver, versatile"
else:
return "ROLE PLAYER - Situational contributor"
def _project_outcome(self, composite: float, prospect: Dict) -> str:
"""Project NFL outcome."""
archetype = self._determine_archetype(prospect)
if composite >= 80:
return f"PRO BOWL POTENTIAL - {archetype}"
elif composite >= 65:
return f"QUALITY STARTER - {archetype}"
elif composite >= 50:
return f"DEPTH/STARTER - {archetype}"
elif composite >= 35:
return f"ROSTER PLAYER - {archetype}"
else:
return "UNLIKELY NFL CONTRIBUTOR"
4.3 Running Back Projection Model
class RBProjectionModel:
"""Project running back NFL success."""
def evaluate(self, prospect: Dict) -> Dict:
"""Evaluate RB prospect."""
# RBs have highest bust rate; weight athleticism highly
production = self._score_production(prospect)
athleticism = self._score_athleticism(prospect)
receiving = self._score_receiving(prospect)
# RB-specific weighting
composite = (
production * 0.30 +
athleticism * 0.35 + # Higher weight for RBs
receiving * 0.35 # PPR value critical
)
return {
'production': production,
'athleticism': athleticism,
'receiving': receiving,
'composite': composite,
'role_projection': self._project_role(composite, prospect),
'bust_risk': self._calculate_bust_risk(prospect)
}
def _score_production(self, prospect: Dict) -> float:
"""Score rushing production."""
yac = prospect.get('yards_after_contact', 3.0)
breakaway = prospect.get('breakaway_rate', 20)
dominator = prospect.get('dominator_rating', 25)
yac_score = (yac - 2.5) / 0.02 * 10 + 50
breakaway_score = (breakaway - 15) / 0.2
dominator_score = (dominator - 20) / 0.1
return (yac_score + breakaway_score + dominator_score) / 3
def _score_athleticism(self, prospect: Dict) -> float:
"""Score physical traits."""
speed_score_val = prospect.get('speed_score', 100)
agility = prospect.get('agility_score', 50)
return (speed_score_val / 100 * 50) + (agility / 2)
def _score_receiving(self, prospect: Dict) -> float:
"""Score pass-catching ability."""
targets = prospect.get('college_targets', 30)
catch_rate = prospect.get('catch_rate', 70)
ypr = prospect.get('yards_per_reception', 8)
volume_score = min(100, targets * 1.5)
efficiency_score = catch_rate
ypr_score = (ypr - 6) / 0.1 * 5 + 50
return (volume_score + efficiency_score + ypr_score) / 3
def _project_role(self, composite: float, prospect: Dict) -> str:
"""Project NFL role."""
receiving = self._score_receiving(prospect)
if composite >= 75 and receiving >= 60:
return "THREE-DOWN BACK - Bell cow with receiving chops"
elif composite >= 70:
return "LEAD BACK - 60-70% snap share ceiling"
elif composite >= 55 and receiving >= 70:
return "PASS-CATCHING SPECIALIST - PPR value"
elif composite >= 55:
return "EARLY-DOWN BACK - Rushing specialist"
else:
return "COMMITTEE/DEPTH - Rotational role"
def _calculate_bust_risk(self, prospect: Dict) -> float:
"""Calculate bust probability."""
# RB bust predictors
age = prospect.get('age', 21)
speed_score = prospect.get('speed_score', 100)
receiving = self._score_receiving(prospect)
base_risk = 0.35 # RBs have highest bust rate
# Age penalty
if age >= 22:
base_risk += 0.10
# Speed floor
if speed_score < 95:
base_risk += 0.05
# One-dimensional risk
if receiving < 40:
base_risk += 0.10
return min(0.80, base_risk)
Section 5: Draft Value Analysis
5.1 Draft Pick Value Curves
Not all draft picks are equal. Value decreases non-linearly:
class DraftValueCalculator:
"""Calculate and compare draft pick values."""
# Approximate value per pick (based on contract value)
PICK_VALUE_CURVE = {
1: 3000, 2: 2800, 3: 2600, 4: 2400, 5: 2200,
10: 1500, 15: 1100, 20: 800, 25: 600, 32: 500,
33: 480, 40: 400, 50: 300, 64: 240,
65: 220, 80: 150, 100: 100,
# Later rounds
150: 40, 200: 20, 250: 10
}
def get_pick_value(self, pick: int) -> float:
"""
Get approximate value for a draft pick.
Args:
pick: Overall draft pick number
Returns:
Approximate value (arbitrary units)
"""
# Interpolate between known values
picks = sorted(self.PICK_VALUE_CURVE.keys())
for i, p in enumerate(picks):
if pick <= p:
if i == 0:
return self.PICK_VALUE_CURVE[p]
prev_p = picks[i-1]
prev_v = self.PICK_VALUE_CURVE[prev_p]
curr_v = self.PICK_VALUE_CURVE[p]
# Linear interpolation
ratio = (pick - prev_p) / (p - prev_p)
return prev_v - (prev_v - curr_v) * ratio
return self.PICK_VALUE_CURVE[max(picks)]
def evaluate_trade(self, giving: List[int], receiving: List[int]) -> Dict:
"""
Evaluate draft pick trade.
Args:
giving: Picks being traded away
receiving: Picks being received
Returns:
Trade evaluation
"""
giving_value = sum(self.get_pick_value(p) for p in giving)
receiving_value = sum(self.get_pick_value(p) for p in receiving)
return {
'giving_value': giving_value,
'receiving_value': receiving_value,
'net_value': receiving_value - giving_value,
'fair_trade': abs(receiving_value - giving_value) < giving_value * 0.1
}
def find_equivalent_picks(self, pick: int, round_target: int) -> List[int]:
"""
Find picks in target round with equivalent value.
Args:
pick: Current pick
round_target: Round to find equivalents
Returns:
List of equivalent picks in target round
"""
pick_value = self.get_pick_value(pick)
# Round boundaries (approximate)
round_starts = {1: 1, 2: 33, 3: 65, 4: 101, 5: 137, 6: 173, 7: 219}
start = round_starts.get(round_target, 1)
end = round_starts.get(round_target + 1, 256) - 1
equivalents = []
for p in range(start, end + 1):
if abs(self.get_pick_value(p) - pick_value) < pick_value * 0.15:
equivalents.append(p)
return equivalents
5.2 Expected Value by Pick
def calculate_expected_value(pick: int, position: str,
historical_data: pd.DataFrame) -> Dict:
"""
Calculate expected value of drafting position at pick.
Args:
pick: Draft pick number
position: Position to draft
historical_data: Historical draft outcomes
Returns:
Expected value metrics
"""
# Filter to similar picks
pick_range = (max(1, pick - 5), pick + 5)
similar = historical_data[
(historical_data['pick'] >= pick_range[0]) &
(historical_data['pick'] <= pick_range[1]) &
(historical_data['position'] == position)
]
if similar.empty:
return None
# Success metrics
starter_rate = (similar['career_starts'] >= 32).mean()
pro_bowl_rate = (similar['pro_bowls'] >= 1).mean()
bust_rate = (similar['career_av'] < 10).mean()
avg_av = similar['career_av'].mean()
avg_years = similar['years_in_league'].mean()
return {
'starter_probability': starter_rate,
'pro_bowl_probability': pro_bowl_rate,
'bust_probability': bust_rate,
'expected_av': avg_av,
'expected_years': avg_years,
'sample_size': len(similar)
}
Section 6: Building a Draft Board
6.1 Composite Ranking System
class DraftBoardBuilder:
"""Build comprehensive draft board."""
def __init__(self):
self.qb_model = QBProjectionModel()
self.wr_model = WRProjectionModel()
self.rb_model = RBProjectionModel()
self.value_calc = DraftValueCalculator()
def evaluate_prospect(self, prospect: Dict) -> Dict:
"""
Create comprehensive prospect evaluation.
Args:
prospect: Prospect data
Returns:
Complete evaluation
"""
position = prospect['position']
# Position-specific evaluation
if position == 'QB':
pos_eval = self.qb_model.evaluate(prospect)
elif position == 'WR':
pos_eval = self.wr_model.evaluate(prospect)
elif position == 'RB':
pos_eval = self.rb_model.evaluate(prospect)
else:
pos_eval = {'composite': 50} # Default for other positions
# Add draft range estimate
composite = pos_eval.get('composite', 50)
draft_range = self._estimate_draft_range(composite, position)
return {
'name': prospect['name'],
'position': position,
'school': prospect.get('school', 'Unknown'),
'evaluation': pos_eval,
'composite_grade': composite,
'draft_range': draft_range,
'value_pick': self._calculate_value_pick(composite, draft_range)
}
def _estimate_draft_range(self, composite: float, position: str) -> Tuple[int, int]:
"""Estimate appropriate draft range."""
# Position premium adjustments
position_premiums = {
'QB': 1.3, 'OT': 1.2, 'EDGE': 1.2, 'CB': 1.1,
'WR': 1.0, 'DT': 1.0, 'LB': 0.95, 'S': 0.95,
'RB': 0.85, 'TE': 0.90, 'IOL': 0.90
}
premium = position_premiums.get(position, 1.0)
adjusted_composite = composite * premium
if adjusted_composite >= 85:
return (1, 10)
elif adjusted_composite >= 75:
return (5, 25)
elif adjusted_composite >= 65:
return (20, 50)
elif adjusted_composite >= 55:
return (40, 90)
elif adjusted_composite >= 45:
return (80, 150)
else:
return (130, 256)
def _calculate_value_pick(self, composite: float,
draft_range: Tuple[int, int]) -> int:
"""Calculate optimal value pick for prospect."""
midpoint = (draft_range[0] + draft_range[1]) // 2
return midpoint
def build_board(self, prospects: List[Dict]) -> pd.DataFrame:
"""
Build complete draft board.
Args:
prospects: List of prospect data
Returns:
Draft board DataFrame
"""
evaluations = []
for prospect in prospects:
eval_result = self.evaluate_prospect(prospect)
evaluations.append({
'name': eval_result['name'],
'position': eval_result['position'],
'school': eval_result['school'],
'grade': eval_result['composite_grade'],
'projection': eval_result['evaluation'].get('projection', ''),
'draft_range_low': eval_result['draft_range'][0],
'draft_range_high': eval_result['draft_range'][1],
'value_pick': eval_result['value_pick']
})
board = pd.DataFrame(evaluations)
board = board.sort_values('grade', ascending=False)
board['rank'] = range(1, len(board) + 1)
return board
Section 7: Case Study: Evaluating a QB Class
7.1 Example Evaluation
def evaluate_qb_class_example():
"""Demonstrate QB class evaluation."""
prospects = [
{
'name': 'QB Prospect A',
'position': 'QB',
'school': 'Alabama',
'conference': 'SEC',
'adj_completion_pct': 68.5,
'td_int_ratio': 3.8,
'ypa': 9.2,
'forty': 4.65,
'arm_strength': 58,
'age': 21.5,
'pressure_performance': 75
},
{
'name': 'QB Prospect B',
'position': 'QB',
'school': 'Ohio State',
'conference': 'Big Ten',
'adj_completion_pct': 65.2,
'td_int_ratio': 4.2,
'ypa': 8.5,
'forty': 4.48,
'arm_strength': 55,
'age': 22.0,
'pressure_performance': 70
}
]
model = QBProjectionModel()
print("QB Class Evaluation")
print("=" * 60)
for prospect in prospects:
result = model.evaluate(prospect)
print(f"\n{prospect['name']} ({prospect['school']})")
print(f" Composite Grade: {result['composite']:.1f}")
print(f" Projection: {result['projection']}")
print(f" Comparable: {result['comparable']}")
Chapter Summary
Draft analysis combines statistical modeling with contextual evaluation to project NFL success. The key frameworks covered:
- Production Analysis - Normalize college stats for comparison
- Physical Testing - Combine metrics predict athleticism, not success
- Profile Metrics - Breakout age and dominator rating indicate alpha potential
- Position Models - Each position requires unique evaluation criteria
- Draft Value - Pick values decrease non-linearly; understand trade math
- Integration - Combine production, athleticism, and context for projections
Draft prediction remains imperfect, but analytical approaches improve expected outcomes over pure scouting.
Conclusion: Part 6 Complete
This concludes Part 6: Applications. We've explored how NFL analytics applies to: - Fantasy Football (Chapter 27) - VORP, projections, DFS optimization - Draft Analysis (Chapter 28) - Prospect evaluation frameworks
Part 7 provides capstone projects to integrate all concepts from the textbook.
Key Equations Reference
Speed Score:
Speed Score = (Weight × 200) / (40 time)^4
Dominator Rating:
Dominator = (Yards Share + TD Share) / 2 × 100
Production Normalization:
Normalized = Raw × Conference_Factor × Era_Factor
Draft Pick Value:
Approximate: Value ≈ 3000 × e^(-0.05 × pick)