NFL Draft Projection Models

Beginner 10 min read 1 views Nov 27, 2025
# NFL Draft Projection Models ## Overview Projecting college players to the NFL is one of the most challenging analytics problems in football. Success requires combining college performance metrics, physical measurements, game film analysis, and historical translation patterns. ## Key Evaluation Components ### College Production Metrics - **Yards per attempt/carry**: Efficiency measures - **Touchdown rate**: Red zone effectiveness - **Turnover rate**: Ball security for QBs/RBs - **Pressure rate**: QB performance under duress - **Target share**: WR usage and opportunity ### Physical Measurements - **Height/Weight/Speed**: Position-specific thresholds - **Arm length**: Critical for OL, DL, CB - **Explosion metrics**: Vertical jump, broad jump - **Agility scores**: 3-cone drill, shuttle - **RAS (Relative Athletic Score)**: Composite athletic rating ### Context Factors - **Competition level**: P5 vs G5 vs FCS - **Conference strength**: SEC vs MAC adjustment - **Age**: Younger players have higher upside - **Experience**: Years as starter - **Team quality**: Supporting cast considerations ## R Analysis with cfbfastR ```r library(cfbfastR) library(dplyr) library(ggplot2) library(randomForest) library(caret) # Load college player statistics player_stats_2023 <- cfbd_stats_season_player( year = 2023, season_type = "regular" ) # Focus on skill positions for draft projection qb_stats <- player_stats_2023 %>% filter(category == "passing") %>% group_by(player, team, conference) %>% summarise( completions = sum(stat[statType == "completions"]), attempts = sum(stat[statType == "attempts"]), yards = sum(stat[statType == "passingYards"]), tds = sum(stat[statType == "passingTDs"]), ints = sum(stat[statType == "interceptions"]), .groups = "drop" ) %>% mutate( comp_pct = completions / attempts * 100, yards_per_att = yards / attempts, td_rate = tds / attempts * 100, int_rate = ints / attempts * 100, td_int_ratio = tds / pmax(ints, 1) ) # Calculate QB draft grade (simplified) qb_draft_grade <- qb_stats %>% filter(attempts >= 200) %>% # Minimum attempts threshold mutate( # Normalize metrics to 0-100 scale yards_score = scale(yards_per_att) * 10 + 50, td_score = scale(td_rate) * 10 + 50, int_score = scale(-int_rate) * 10 + 50, # Negative because lower is better comp_score = scale(comp_pct) * 10 + 50, # Composite draft grade draft_grade = ( yards_score * 0.3 + td_score * 0.3 + int_score * 0.2 + comp_score * 0.2 ), # Draft round projection (simplified) projected_round = case_when( draft_grade >= 70 ~ "Round 1-2", draft_grade >= 60 ~ "Round 3-4", draft_grade >= 50 ~ "Round 5-7", TRUE ~ "Undrafted" ) ) %>% arrange(desc(draft_grade)) print("Top 25 QB Draft Prospects by Statistical Grade:") print(qb_draft_grade %>% select(player, team, yards_per_att, td_rate, int_rate, draft_grade, projected_round) %>% head(25)) # Visualize QB draft prospects top_qbs <- qb_draft_grade %>% head(30) ggplot(top_qbs, aes(x = yards_per_att, y = td_rate)) + geom_point(aes(size = draft_grade, color = projected_round), alpha = 0.7) + scale_color_manual(values = c("Round 1-2" = "darkgreen", "Round 3-4" = "blue", "Round 5-7" = "orange", "Undrafted" = "red")) + geom_text(aes(label = player), size = 2.5, vjust = -1, hjust = 0.5) + labs( title = "QB Draft Prospects - Statistical Profile", x = "Yards per Attempt", y = "TD Rate (%)", size = "Draft Grade", color = "Projected Round" ) + theme_minimal() # Historical draft analysis: college production vs NFL success # This would require NFL performance data - using synthetic example historical_qbs <- data.frame( player = c("QB1", "QB2", "QB3", "QB4", "QB5"), college_ypa = c(9.2, 8.5, 8.8, 7.9, 9.0), college_td_rate = c(6.5, 5.8, 6.2, 5.0, 6.8), college_comp_pct = c(68, 64, 66, 61, 70), draft_position = c(1, 15, 8, 42, 3), nfl_success = c(85, 60, 70, 45, 80) # Composite NFL rating ) # Correlation analysis cor_ypa <- cor(historical_qbs$college_ypa, historical_qbs$nfl_success) cor_td <- cor(historical_qbs$college_td_rate, historical_qbs$nfl_success) cor_comp <- cor(historical_qbs$college_comp_pct, historical_qbs$nfl_success) print("\nCorrelation with NFL Success:") print(paste("Yards per Attempt:", round(cor_ypa, 3))) print(paste("TD Rate:", round(cor_td, 3))) print(paste("Completion %:", round(cor_comp, 3))) # Running backs: yards after contact, elusiveness rb_stats <- player_stats_2023 %>% filter(category == "rushing") %>% group_by(player, team) %>% summarise( carries = sum(stat[statType == "rushingAttempts"]), yards = sum(stat[statType == "rushingYards"]), tds = sum(stat[statType == "rushingTDs"]), .groups = "drop" ) %>% mutate( yards_per_carry = yards / carries, td_rate = tds / carries * 100 ) %>% filter(carries >= 100) # Draft projection for RBs rb_draft_grade <- rb_stats %>% mutate( ypc_score = scale(yards_per_carry) * 10 + 50, volume_score = scale(carries) * 10 + 50, td_score = scale(td_rate) * 10 + 50, draft_grade = ( ypc_score * 0.4 + volume_score * 0.3 + td_score * 0.3 ), projected_round = case_when( draft_grade >= 65 ~ "Round 1-3", draft_grade >= 55 ~ "Round 4-5", draft_grade >= 45 ~ "Round 6-7", TRUE ~ "Priority FA" ) ) %>% arrange(desc(draft_grade)) print("\nTop 20 RB Draft Prospects:") print(rb_draft_grade %>% select(player, team, yards_per_carry, carries, tds, draft_grade, projected_round) %>% head(20)) # Visualize RB prospects top_rbs <- rb_draft_grade %>% head(25) ggplot(top_rbs, aes(x = yards_per_carry, y = carries)) + geom_point(aes(size = draft_grade, color = projected_round), alpha = 0.7) + scale_color_manual(values = c("Round 1-3" = "darkgreen", "Round 4-5" = "blue", "Round 6-7" = "orange", "Priority FA" = "red")) + labs( title = "RB Draft Prospects - Production Profile", x = "Yards per Carry", y = "Total Carries", size = "Draft Grade", color = "Projected Round" ) + theme_minimal() ``` ## Python Implementation ```python import pandas as pd import numpy as np import requests import matplotlib.pyplot as plt import seaborn as sns from sklearn.ensemble import RandomForestRegressor from sklearn.preprocessing import StandardScaler def get_player_stats(year, category='passing'): """Fetch player statistics from CFB Data API""" url = "https://api.collegefootballdata.com/stats/player/season" params = { 'year': year, 'seasonType': 'regular', 'category': category } response = requests.get(url, params=params) return pd.DataFrame(response.json()) # Load QB statistics qb_stats = get_player_stats(2023, 'passing') # Clean and prepare QB data qb_df = qb_stats[qb_stats['stat'] >= 200].copy() # Min 200 attempts # Calculate key metrics per QB qb_metrics = qb_df.groupby(['player', 'team', 'conference']).first() # Add calculated metrics (simplified - real data would have completions, TDs, etc.) # For demonstration, creating synthetic derived metrics np.random.seed(42) qb_metrics['comp_pct'] = np.random.uniform(55, 72, len(qb_metrics)) qb_metrics['yards_per_att'] = np.random.uniform(6.5, 10.5, len(qb_metrics)) qb_metrics['td_rate'] = np.random.uniform(3.5, 8.0, len(qb_metrics)) qb_metrics['int_rate'] = np.random.uniform(1.0, 4.5, len(qb_metrics)) # Calculate composite draft grade def calculate_qb_draft_grade(row): """Calculate QB draft grade based on statistics""" # Normalize each metric scaler = StandardScaler() # Weights for each metric grade = ( row['comp_pct'] * 0.20 + row['yards_per_att'] * 10 * 0.35 + row['td_rate'] * 10 * 0.30 + (10 - row['int_rate']) * 3 * 0.15 ) return grade qb_metrics['draft_grade'] = qb_metrics.apply(calculate_qb_draft_grade, axis=1) # Project draft round def project_draft_round(grade): if grade >= 70: return "Round 1" elif grade >= 60: return "Round 2-3" elif grade >= 50: return "Round 4-5" elif grade >= 40: return "Round 6-7" else: return "Undrafted" qb_metrics['projected_round'] = qb_metrics['draft_grade'].apply(project_draft_round) # Sort by draft grade qb_prospects = qb_metrics.sort_values('draft_grade', ascending=False).reset_index() print("Top 25 QB Draft Prospects:") print(qb_prospects[['player', 'team', 'comp_pct', 'yards_per_att', 'td_rate', 'int_rate', 'draft_grade', 'projected_round']].head(25)) # Load RB statistics rb_stats = get_player_stats(2023, 'rushing') rb_df = rb_stats[rb_stats['stat'] >= 100].copy() # Min 100 carries # Calculate RB metrics rb_df['yards_per_carry'] = np.random.uniform(4.0, 7.5, len(rb_df)) rb_df['receptions'] = np.random.randint(10, 60, len(rb_df)) rb_df['receiving_yards'] = rb_df['receptions'] * np.random.uniform(7, 12, len(rb_df)) rb_df['total_tds'] = np.random.randint(3, 18, len(rb_df)) # RB draft grade def calculate_rb_draft_grade(row): """Calculate RB draft grade""" grade = ( row['yards_per_carry'] * 10 * 0.40 + (row['stat'] / 250) * 50 * 0.25 + # Volume adjusted row['total_tds'] * 3 * 0.25 + row['receptions'] * 0.5 * 0.10 ) return grade rb_df['draft_grade'] = rb_df.apply(calculate_rb_draft_grade, axis=1) rb_df['projected_round'] = rb_df['draft_grade'].apply( lambda x: "Round 1-2" if x >= 75 else "Round 3-4" if x >= 60 else "Round 5-7" if x >= 45 else "Priority FA" ) rb_prospects = rb_df.sort_values('draft_grade', ascending=False) print("\nTop 20 RB Draft Prospects:") print(rb_prospects[['player', 'team', 'yards_per_carry', 'stat', 'total_tds', 'receptions', 'draft_grade', 'projected_round']].head(20)) # Visualizations fig, axes = plt.subplots(2, 2, figsize=(16, 12)) # 1. QB Draft Board top_qbs = qb_prospects.head(20) colors = top_qbs['projected_round'].map({ 'Round 1': 'darkgreen', 'Round 2-3': 'green', 'Round 4-5': 'orange', 'Round 6-7': 'red', 'Undrafted': 'gray' }) axes[0, 0].barh(range(len(top_qbs)), top_qbs['draft_grade'], color=colors) axes[0, 0].set_yticks(range(len(top_qbs))) axes[0, 0].set_yticklabels(top_qbs['player'], fontsize=8) axes[0, 0].set_xlabel('Draft Grade') axes[0, 0].set_title('Top 20 QB Draft Prospects') axes[0, 0].invert_yaxis() # 2. QB Statistical Profile axes[0, 1].scatter(top_qbs['yards_per_att'], top_qbs['td_rate'], s=top_qbs['draft_grade']*5, c=top_qbs['int_rate'], cmap='RdYlGn_r', alpha=0.6) axes[0, 1].set_xlabel('Yards per Attempt') axes[0, 1].set_ylabel('TD Rate (%)') axes[0, 1].set_title('QB Efficiency Profile (size=grade, color=INT rate)') axes[0, 1].grid(alpha=0.3) # 3. RB Draft Board top_rbs = rb_prospects.head(20) rb_colors = top_rbs['projected_round'].map({ 'Round 1-2': 'darkgreen', 'Round 3-4': 'green', 'Round 5-7': 'orange', 'Priority FA': 'red' }) axes[1, 0].barh(range(len(top_rbs)), top_rbs['draft_grade'], color=rb_colors) axes[1, 0].set_yticks(range(len(top_rbs))) axes[1, 0].set_yticklabels(top_rbs['player'], fontsize=8) axes[1, 0].set_xlabel('Draft Grade') axes[1, 0].set_title('Top 20 RB Draft Prospects') axes[1, 0].invert_yaxis() # 4. RB Production vs Efficiency axes[1, 1].scatter(top_rbs['stat'], top_rbs['yards_per_carry'], s=top_rbs['total_tds']*10, c=top_rbs['draft_grade'], cmap='viridis', alpha=0.6) axes[1, 1].set_xlabel('Total Carries') axes[1, 1].set_ylabel('Yards per Carry') axes[1, 1].set_title('RB Volume vs Efficiency (size=TDs, color=grade)') axes[1, 1].grid(alpha=0.3) plt.tight_layout() plt.show() ``` ## Position-Specific Translation Factors ### Quarterback - **Completion %**: 68%+ translates better - **Yards per Attempt**: 8.0+ indicates NFL arm talent - **Under Pressure**: Performance vs blitz critical - **Downfield Passing**: 20+ yard attempts success rate ### Running Back - **Yards After Contact**: NFL durability predictor - **Receiving Ability**: Pass-catching RBs more valuable - **Pass Protection**: Often overlooked, highly important - **Size/Speed Combo**: 215+ lbs with 4.5- 40 time ideal ### Wide Receiver - **Target Share**: 25%+ shows alpha WR traits - **Yards per Route Run**: Better than yards per catch - **Contested Catch Rate**: 1-on-1 winning ability - **Route Running**: Separation vs press/zone coverage ### Offensive Line - **Pass Block Win Rate**: Advanced metric via PFF - **Run Block Win Rate**: Creating movement - **Length**: Arm length > 33" preferred for tackles - **Anchor Strength**: Bench press, play strength ### Defensive Line - **Pressure Rate**: QB disruption frequency - **Run Stop %**: TFL + stops / total snaps - **Explosion**: 10-yard split, broad jump - **Length**: Arm length for edge rushers ### Linebacker - **Tackle Rate**: Solo tackles per snap - **Coverage Grade**: Slot/TE coverage ability - **Blitz Success**: Pressure rate on blitzes - **Athleticism**: 4.6- 40 time, 7.0- 3-cone ### Defensive Back - **Yards per Cover Snap**: Yards allowed/snap in coverage - **Ball Production**: INTs + PBUs per target - **Speed**: 4.4- 40 time for CBs - **Length**: Arm length + hand size for CBs ## Machine Learning Approach ### Feature Engineering 1. **Production Metrics**: Stats adjusted for competition level 2. **Physical Traits**: Combine measurables (RAS scores) 3. **Context Variables**: Team quality, conference, age 4. **Advanced Metrics**: PFF grades, EPA contributions ### Model Types - **Regression**: Predict draft position (1-260) - **Classification**: Draft round or NFL success tier - **Survival Analysis**: Career length prediction - **Ensemble**: Combine multiple models for robustness ### Validation - Historical accuracy: Past draft projections vs actuals - Cross-validation: Different years/conferences - Positional differences: Separate models per position ## Key Insights ### High-Value College Traits - **Efficiency over volume**: Yards per attempt > total yards - **Age-adjusted production**: Younger players with higher upside - **Competition level**: P5 production more predictive - **Versatility**: Multi-position ability increases value ### Common Projection Errors - Over-valuing measurables vs production - Ignoring competition level adjustments - Recency bias (final season weighed too heavily) - Position scarcity leading to reaches ### Draft Value Analysis - Rounds 1-2: Hit rate ~60%, expected value high - Round 3: Sweet spot for value vs risk - Rounds 4-5: Volume approach, developmental prospects - Rounds 6-7: Dart throws, special teams contributors ## Resources - [MockDraftable](https://www.mockdraftable.com/) - Combine data visualization - [Pro Football Focus Draft Guide](https://www.pff.com/draft) - [NFL Draft Scout](https://www.nfldraftscout.com/) - [The Athletic NFL Draft Coverage](https://theathletic.com/nfl/draft/)

Discussion

Have questions or feedback? Join our community discussion on Discord or GitHub Discussions.