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.
Table of Contents
Related Topics
Quick Actions