4 min read

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...

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:

  1. Sample Size Limitations - Prospects have 30-50 college games - Limited snaps at NFL-quality competition - Injury history often incomplete

  2. Developmental Uncertainty - College-to-NFL transition varies by player - Coaching and scheme fit impact outcomes - Character and work ethic difficult to quantify

  3. System Dependencies - College production heavily scheme-dependent - Competition level varies dramatically - Role in college may differ from NFL role

  4. 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:

  1. Production Analysis - Normalize college stats for comparison
  2. Physical Testing - Combine metrics predict athleticism, not success
  3. Profile Metrics - Breakout age and dominator rating indicate alpha potential
  4. Position Models - Each position requires unique evaluation criteria
  5. Draft Value - Pick values decrease non-linearly; understand trade math
  6. 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)