Transfer Portal Analytics

Beginner 10 min read 1 views Nov 27, 2025
# Transfer Portal Analytics ## Overview The transfer portal has revolutionized college football roster construction. Understanding transfer analytics is crucial for evaluating team-building strategies, player value, and predicting team performance. ## Transfer Portal Landscape ### Key Statistics - 2,000+ FBS players enter portal annually (15-20% of rosters) - Peak windows: December (post-season) and April-May (spring) - Approximately 60% of portal entrants find new FBS homes - Graduate transfers have higher placement rate than underclassmen ### Transfer Categories 1. **Seeking Playing Time**: Buried on depth chart 2. **Upgrading Competition**: Moving to better program 3. **Following Coach**: Staff changes trigger transfers 4. **Position Changes**: Seeking different role 5. **Personal Reasons**: NIL, location, academics ## R Analysis with cfbfastR ```r library(cfbfastR) library(dplyr) library(ggplot2) library(tidyr) # Load transfer portal data portal_data <- cfbd_recruiting_transfer_portal(year = 2023) # Analyze transfer patterns transfer_summary <- portal_data %>% group_by(origin, destination) %>% summarise( transfers = n(), avg_rating = mean(rating, na.rm = TRUE), .groups = "drop" ) %>% filter(!is.na(destination)) # Schools gaining most transfers top_destinations <- transfer_summary %>% group_by(destination) %>% summarise( incoming_transfers = sum(transfers), avg_incoming_rating = mean(avg_rating, na.rm = TRUE) ) %>% arrange(desc(incoming_transfers)) %>% head(25) print("Top Transfer Destinations - 2023:") print(top_destinations) # Schools losing most transfers top_origins <- transfer_summary %>% group_by(origin) %>% summarise( outgoing_transfers = sum(transfers), avg_outgoing_rating = mean(avg_rating, na.rm = TRUE) ) %>% arrange(desc(outgoing_transfers)) %>% head(25) print("\nSchools Losing Most Transfers - 2023:") print(top_origins) # Net transfer analysis (incoming - outgoing) net_transfers <- transfer_summary %>% group_by(destination) %>% summarise(incoming = sum(transfers)) %>% full_join( transfer_summary %>% group_by(origin) %>% summarise(outgoing = sum(transfers)), by = c("destination" = "origin") ) %>% mutate( incoming = replace_na(incoming, 0), outgoing = replace_na(outgoing, 0), net_transfers = incoming - outgoing ) %>% arrange(desc(net_transfers)) # Visualize net transfer activity top_net <- net_transfers %>% filter(abs(net_transfers) >= 5) %>% arrange(net_transfers) ggplot(top_net, aes(x = net_transfers, y = reorder(destination, net_transfers))) + geom_col(aes(fill = net_transfers > 0)) + scale_fill_manual(values = c("red", "green"), labels = c("Net Loss", "Net Gain")) + labs( title = "Net Transfer Activity by School - 2023", x = "Net Transfers (Incoming - Outgoing)", y = "School", fill = "Direction" ) + theme_minimal() + theme(legend.position = "bottom") # Analyze transfer performance impact # Get team stats for teams with high transfer activity team_stats <- cfbd_stats_season_team(year = 2023) transfer_impact <- net_transfers %>% inner_join(team_stats, by = c("destination" = "team")) %>% select(destination, net_transfers, games, wins, losses) %>% mutate(win_pct = wins / games) # Correlation between transfers and performance cor_result <- cor.test( transfer_impact$net_transfers, transfer_impact$win_pct ) print(paste("\nCorrelation between net transfers and win percentage:", round(cor_result$estimate, 3), "| p-value:", round(cor_result$p.value, 4))) # Scatter plot of transfers vs performance ggplot(transfer_impact, aes(x = net_transfers, y = win_pct)) + geom_point(alpha = 0.6, size = 3, color = "steelblue") + geom_smooth(method = "lm", se = TRUE, color = "red") + labs( title = "Transfer Portal Activity vs Team Performance (2023)", x = "Net Transfers (Incoming - Outgoing)", y = "Win Percentage" ) + 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 scipy.stats import pearsonr def get_transfer_data(year): """ Fetch transfer portal data from CFB Data API """ url = "https://api.collegefootballdata.com/player/portal" params = {'year': year} response = requests.get(url, params=params) return pd.DataFrame(response.json()) # Load transfer portal data transfers_2023 = get_transfer_data(2023) # Clean and prepare data transfers = transfers_2023.dropna(subset=['origin', 'destination']) # Calculate net transfer balance by school incoming = transfers.groupby('destination').size().rename('incoming') outgoing = transfers.groupby('origin').size().rename('outgoing') net_transfer_df = pd.DataFrame({ 'incoming': incoming, 'outgoing': outgoing }).fillna(0) net_transfer_df['net'] = net_transfer_df['incoming'] - net_transfer_df['outgoing'] net_transfer_df = net_transfer_df.sort_values('net', ascending=False) print("Top 15 Net Transfer Gainers:") print(net_transfer_df.head(15)) print("\nTop 15 Net Transfer Losers:") print(net_transfer_df.tail(15)) # Position analysis position_transfers = transfers.groupby('position').agg({ 'origin': 'count', 'rating': 'mean' }).rename(columns={'origin': 'count', 'rating': 'avg_rating'}) position_transfers = position_transfers.sort_values('count', ascending=False) print("\nTransfers by Position:") print(position_transfers.head(15)) # Visualizations fig, axes = plt.subplots(2, 2, figsize=(15, 12)) # 1. Net transfer distribution top_movers = net_transfer_df.nlargest(15, 'net') bottom_movers = net_transfer_df.nsmallest(15, 'net') movers = pd.concat([top_movers, bottom_movers]) axes[0, 0].barh(range(len(movers)), movers['net'], color=['green' if x > 0 else 'red' for x in movers['net']]) axes[0, 0].set_yticks(range(len(movers))) axes[0, 0].set_yticklabels(movers.index, fontsize=8) axes[0, 0].set_xlabel('Net Transfers') axes[0, 0].set_title('Top & Bottom 15 Net Transfer Activity - 2023') axes[0, 0].axvline(0, color='black', linewidth=0.8) # 2. Incoming vs Outgoing scatter axes[0, 1].scatter(net_transfer_df['outgoing'], net_transfer_df['incoming'], alpha=0.6, s=50) axes[0, 1].plot([0, 30], [0, 30], 'r--', label='Equal In/Out') axes[0, 1].set_xlabel('Outgoing Transfers') axes[0, 1].set_ylabel('Incoming Transfers') axes[0, 1].set_title('Transfer Flow Balance by School') axes[0, 1].legend() axes[0, 1].grid(alpha=0.3) # 3. Position distribution top_positions = position_transfers.head(12) axes[1, 0].bar(range(len(top_positions)), top_positions['count'], color='steelblue') axes[1, 0].set_xticks(range(len(top_positions))) axes[1, 0].set_xticklabels(top_positions.index, rotation=45, ha='right') axes[1, 0].set_ylabel('Number of Transfers') axes[1, 0].set_title('Transfers by Position - 2023') # 4. Transfer timing (if date available) if 'transferDate' in transfers.columns: transfers['month'] = pd.to_datetime( transfers['transferDate'], errors='coerce' ).dt.month monthly = transfers['month'].value_counts().sort_index() axes[1, 1].plot(monthly.index, monthly.values, marker='o', linewidth=2) axes[1, 1].set_xlabel('Month') axes[1, 1].set_ylabel('Number of Transfers') axes[1, 1].set_title('Transfer Portal Entry Timing') axes[1, 1].grid(alpha=0.3) else: axes[1, 1].text(0.5, 0.5, 'Date data not available', ha='center', va='center') axes[1, 1].axis('off') plt.tight_layout() plt.show() ``` ## Transfer Portal Strategy ### High-Impact Transfer Positions 1. **Quarterback**: Immediate starter potential, program-changing 2. **Edge Rusher**: Premium position with high demand 3. **Offensive Tackle**: Protects QB, enables offense 4. **Cornerback**: Critical for pass defense in modern game 5. **Running Back**: Immediate production, shorter shelf life ### Transfer vs High School Recruiting **Advantages of Transfers:** - Proven production at college level - Immediate eligibility and contribution - Known physical development and maturity - Fill specific roster holes quickly **Advantages of HS Recruiting:** - Longer development runway (3-5 years) - Program culture fit from day one - No adjustment period to new system - Lower risk of immediate departure ### Transfer Success Factors - Similar offensive/defensive scheme - Position of immediate need - Strong position coach relationship - Geographic/cultural fit - Playing time opportunity ## Key Insights ### Portal Trends - Elite programs using portal to fill specific needs, not build entire roster - Mid-tier programs becoming "development factories" for blue bloods - Graduate transfers have highest success rate (maturity + eligibility) - QB transfers get disproportionate attention but OL/DL more impactful ### Performance Impact - Net positive transfers correlate with improved win percentage (+0.15) - Quality over quantity: 3-5 high-impact transfers > 10+ depth pieces - First-year transfers underperform expectations (adjustment period) - Second-year transfers (grad year) maximize impact ### Roster Construction - Balanced approach: 60% HS recruits, 30% developed players, 10% transfers - Target portal for immediate needs, recruit HS for pipeline - Avoid over-reliance on portal (culture/continuity concerns) ## Resources - [247Sports Transfer Portal Tracker](https://247sports.com/Season/2024-Football/TransferPortal/) - [On3 Transfer Portal Rankings](https://www.on3.com/transfer-portal/rankings/) - [NCAA Transfer Portal Data](https://www.ncaa.org/sports/2021/4/27/transfer-portal.aspx)

Discussion

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