Prospect Projection Systems
Advanced
10 min read
1 views
Nov 26, 2025
# Prospect Projection Systems
## Introduction
Prospect projection represents one of baseball analytics' most challenging frontiers. Unlike major league projection systems that can leverage stable MLB statistics, prospect projection must translate minor league performance across different competitive levels, account for age-relative-to-level dynamics, balance scouting tools with statistical output, and project development trajectories for players who may be years away from their peak performance.
The fundamental challenge is that minor league statistics are noisy, context-dependent, and often misleading. A player hitting .300 with 20 home runs in High-A ball may be a future star or a 24-year-old organizational filler. Prospect projection systems must disentangle signal from noise while accounting for the massive uncertainty inherent in forecasting young player development.
## Why Prospect Projection Is Unique
### The Multi-Level Challenge
Unlike MLB projections that deal with a single competitive level, prospect projections must translate performance across vastly different difficulty tiers. The gap between Low-A and MLB is enormous - roughly equivalent to the difference between high school baseball and Low-A. Each level represents a significant filtering mechanism, and success rates drop dramatically as players advance.
The competitive disparity between levels creates translation challenges. A prospect might dominate Double-A with a .350 OBP but struggle mightily when promoted to Triple-AAA. The projection system must understand not just current performance but readiness for the next level and ultimate MLB ceiling.
### Development Uncertainty
Young players are still developing physically, mechanically, and mentally. A 19-year-old's swing mechanics may look completely different at age 22. Pitchers add new pitches or improve existing ones. Players adjust to advanced scouting reports and coaching. This development trajectory is partially predictable but contains substantial randomness.
The uncertainty compounds over time. Projecting a Low-A player to MLB requires forecasting 3-5 years of development, during which countless variables can change. Injuries, mechanical adjustments, mental approach, and even off-field factors all influence outcomes.
### Sample Size Limitations
Minor league seasons provide limited data - perhaps 400-500 plate appearances or 100-150 innings pitched in a full season. For players moving between levels, the sample sizes at each level become even smaller. Statistical noise dominates, making it difficult to distinguish genuine skill from random variation.
This sample size challenge forces projection systems to rely heavily on aging curves, historical comparables, and scouting information to supplement the limited statistical evidence.
### Scouting vs Statistics Integration
Prospect projection uniquely requires blending traditional scouting evaluations (tools grades, body type, mechanics) with statistical performance. A player may have elite exit velocity and swing mechanics (scouting tools) but poor current statistics due to swing-and-miss issues. The projection must weigh future potential against current results.
Different systems weight scouting vs statistics differently. Some rely primarily on statistical translation with minor adjustments for tools. Others use tools grades as the foundation and adjust based on performance deviation from expectation.
## Minor League Statistics Translation
### The Equivalency Challenge
Minor league statistics must be translated to an MLB-equivalent baseline for meaningful projection. A .280 batting average in the Eastern League (Double-A) translates very differently than .280 in the Pacific Coast League (Triple-AAA) or the Florida State League (High-A).
Translation systems account for:
- **League offensive environment**: Some leagues are hitter-friendly, others pitcher-friendly
- **Park factors**: Minor league parks vary dramatically in dimensions and conditions
- **Competitive level**: The overall talent quality at each level
- **Era adjustments**: League-wide performance changes over time
### Statistical Categories to Translate
Different statistics translate with varying reliability:
**Highly Translatable:**
- Walk rate (BB%)
- Strikeout rate (K%)
- Isolated power (ISO)
- Ground ball rate (GB%)
**Moderately Translatable:**
- Batting average on balls in play (BABIP)
- Line drive rate (LD%)
- Home run rate (HR%)
**Poorly Translatable:**
- Raw batting average
- Raw ERA for pitchers
- Stolen base success rate
The more skill-based and less luck-dependent a metric, the better it translates across levels.
### Translation Methodology
The standard approach uses historical player transitions to establish equivalency factors. By tracking how players perform before and after level changes, we can calculate average performance decline/improvement and derive translation factors.
For example, if players who hit .280 in Double-AA average .250 in their first Triple-AAA stint, the equivalency factor suggests Double-AA performance should be translated down by approximately 30 points of batting average (though modern systems use more sophisticated rate-based approaches).
## Level Adjustments
### Understanding the Level Hierarchy
The minor league system creates a clear difficulty hierarchy:
1. **Rookie Ball**: Primarily recent draft picks, very young players
2. **Low-A**: First full-season assignment for most prospects
3. **High-A**: Intermediate step, significant jump from Low-A
4. **Double-AA**: Considered the most important proving ground
5. **Triple-AAA**: Closest to MLB, but still meaningful gap
6. **MLB**: The ultimate competitive level
### Quantifying Level Difficulty
Research suggests approximate run-scoring environment differences between levels:
- Low-A to High-A: ~0.5 runs per game easier in Low-A
- High-A to Double-AA: ~0.7 runs per game easier in High-A
- Double-AA to Triple-AAA: ~0.3 runs per game easier in Double-AA
- Triple-AAA to MLB: ~0.6 runs per game easier in Triple-AAA
These translate to wOBA differences of approximately:
- Low-A to MLB: +80-100 points of wOBA
- High-A to MLB: +60-70 points of wOBA
- Double-AA to MLB: +35-45 points of wOBA
- Triple-AAA to MLB: +15-25 points of wOBA
### Double-AA as the Critical Filter
Double-AA holds special significance because it represents the first level where players face primarily legitimate MLB prospects and future major leaguers. The pitching quality jumps substantially, with more advanced secondary pitches and better command.
Success at Double-AA correlates more strongly with MLB success than any other minor league level. A prospect who dominates Double-AA has demonstrated genuine MLB-caliber skills. Conversely, struggling at Double-AA often indicates a player will be marginal at best in the majors.
## Age Relative to Level Importance
### Why Age Matters
A 19-year-old hitting .260 in High-A demonstrates vastly more potential than a 24-year-old with identical statistics. Age relative to competition provides critical context for evaluating performance.
The typical age for each level:
- Low-A: 20-21
- High-A: 21-22
- Double-AA: 22-23
- Triple-AAA: 24-25
Players significantly younger than level-average who maintain league-average production show exceptional promise. Players older than average who excel may simply be physically mature players beating up on younger competition.
### Age Adjustment Methodology
Projection systems typically apply age-based adjustments to performance metrics. A simplified approach might add/subtract production based on age deviation from level average:
For each year younger than typical level age, add approximately:
- 20-30 points of wOBA equivalent
- 5-10% improvement in projection confidence
For each year older than typical level age, subtract approximately:
- 15-25 points of wOBA equivalent
- Increase skepticism of performance sustainability
### The Age-Level Matrix
Elite prospects move through the system quickly and are often significantly younger than their competition. A 20-year-old in Double-AA or a 19-year-old in High-A demonstrates organizational confidence and exceptional ability.
Conversely, old-for-level players face increased skepticism. A 25-year-old dominating Double-AA may never succeed in MLB because the statistical advantage comes from physical maturity rather than genuine MLB-caliber skill.
## Tools vs Performance Balance
### Understanding Scouting Tools
Traditional scouting evaluates five tools for position players on the 20-80 scale:
1. **Hit Tool**: Ability to make contact and hit for average
2. **Power**: Raw strength and game power production
3. **Speed**: Raw running speed
4. **Arm**: Throwing strength and accuracy
5. **Fielding**: Defensive ability and range
For pitchers, tools include:
- **Fastball velocity and movement**
- **Secondary pitches** (curveball, slider, changeup, etc.)
- **Command and control**
- **Athleticism and delivery**
Grades of 50 represent MLB average, 60 is above-average, 70 is plus-plus, and 80 is elite/generational. Below 50 indicates below-average tools.
### Performance vs Tools Divergence
Performance and tools sometimes diverge significantly:
**High tools, low performance**: A player with 60-grade raw power but a 35% strikeout rate shows the tools but not the refinement to access them in games. Projection must estimate likelihood of unlocking latent ability.
**High performance, average tools**: A player hitting .320 with modest exit velocity and tools grades may be maximizing limited ability. Projection must assess performance sustainability against better competition.
### Integrating Tools into Projections
Modern projection systems use scouting tools as priors or constraints on statistical projections:
- Tools grades establish ceiling estimates (what a player could become if everything develops)
- Statistical performance provides floor estimates (current demonstrated ability)
- Age and level context inform probability of reaching closer to ceiling vs floor
A 20-year-old with 70-grade raw power but modest current production might be projected for significant power growth. A 24-year-old with similar tools but similar production receives a more conservative projection.
## Hit Tool and Power Projection
### Hit Tool Components
The hit tool represents the most complex projection challenge because it encompasses multiple skills:
- **Contact ability**: Raw bat-to-ball skills
- **Bat speed**: Ability to catch up to velocity
- **Plate discipline**: Strike zone recognition and pitch selection
- **Approach**: Game planning and situational hitting
- **Hand-eye coordination**: Barrel accuracy on various pitch types
These components develop at different rates and have varying predictability.
### Contact Rate Projection
Strikeout rate shows strong persistence from minors to majors and across levels. Players with elite contact skills (sub-15% K rates) in the minors typically maintain strong contact in MLB, though rates worsen somewhat.
Conversely, players with extreme swing-and-miss issues (30%+ K rates) rarely solve these problems completely. Projection systems discount raw production for high-strikeout hitters because the underlying skill deficit suggests performance won't translate.
### Power Development
Power is perhaps the most developable tool. Players frequently add significant raw power as they physically mature from ages 20-25. Additionally, players learn to optimize launch angles and pull strategies to access raw power in games.
Indicators of future power development:
- High exit velocity on contact
- Strong raw power grades from scouts
- Increasing fly ball rates
- Young age with physical projection remaining
Red flags for power projection:
- Old for level with modest power output
- Low exit velocity despite strength
- Extreme ground ball tendencies
- Struggling against velocity (late bat)
### BABIP Considerations
Batting average on balls in play (BABIP) is heavily luck-influenced but not entirely random. Certain player types sustain above-average BABIP:
- High line drive rates
- Elite speed (infield hits, pressure on defense)
- Opposite field hitting approach
- Quality of contact (hard-hit rate)
Projection systems regress minor league BABIP toward expected levels based on batted ball profile, speed, and historical averages for similar player types.
## Pitch Mix and Development
### Pitcher Development Complexity
Pitching prospects develop along multiple dimensions simultaneously:
1. **Velocity gains**: Pitchers often add 2-5 mph as they mature and refine mechanics
2. **Secondary pitch development**: Learning new pitches or improving existing ones
3. **Command refinement**: Improving location and control
4. **Pitch sequencing**: Understanding how to set up hitters
5. **Durability**: Building innings capacity and injury resistance
Each dimension contains substantial uncertainty, making pitcher projection especially difficult.
### The Breaking Ball Threshold
Double-plus breaking balls (70-grade sliders, curveballs) dramatically improve pitcher projections. Elite breaking balls allow pitchers to neutralize same-handed hitters and provide a genuine out pitch.
A pitcher with a 60-grade fastball and a 70-grade slider has a much higher ceiling than a pitcher with a 70-grade fastball and a 50-grade slider. The ability to miss bats matters more than pure velocity.
### Changeup Development for Starters
For starting pitchers, changeup development often determines success probability. A third plus-pitch (typically the changeup) allows pitchers to handle opposite-handed hitters effectively and to survive multiple times through the batting order.
Projection systems boost ceiling estimates for pitchers showing promising changeup development. Conversely, starters without a functional changeup face increased reliever risk.
### Command vs Stuff
The command-versus-stuff debate influences projection approaches:
**Stuff-heavy pitchers**: High velocity, plus breaking balls, but below-average control. Higher variance outcomes - could be dominant or walk too many.
**Command-heavy pitchers**: Average velocity, average secondaries, but exceptional control. Lower ceiling but higher floor - more likely to be useful MLB pitchers but rarely stars.
Historical evidence suggests elite stuff has higher upside, but command pitchers have better success rates reaching the majors in some capacity.
## K/BB Ratio as Strongest Predictor
### Why K/BB Matters
The strikeout-to-walk ratio represents the single most predictive minor league statistic for future MLB success. This holds for both pitchers and hitters.
For pitchers:
- K/BB ratio correlates strongly with MLB ERA and FIP
- Pitchers with K/BB above 3.0 in Double-AA have much higher MLB success rates
- Walk rate particularly matters - control issues rarely fully resolve
For hitters:
- BB/K ratio predicts MLB OBP and overall offensive value
- Plate discipline shows strong persistence across levels
- Hitters with elite BB/K ratios (>0.75) in minors typically succeed in MLB
### The Discipline Threshold
Research consistently shows plate discipline thresholds that separate prospects:
**For hitters:**
- Elite: BB/K above 0.75
- Good: BB/K between 0.50-0.75
- Average: BB/K between 0.30-0.50
- Concerning: BB/K below 0.30
**For pitchers:**
- Elite: K/BB above 4.0
- Good: K/BB between 3.0-4.0
- Average: K/BB between 2.0-3.0
- Concerning: K/BB below 2.0
These ratios become even more predictive when age-adjusted. A young player with strong K/BB ratios demonstrates advanced feel for the strike zone.
### Translation Stability
K/BB ratios translate more cleanly across levels than most other statistics. A pitcher with a 4.0 K/BB in Double-AA might see strikeout rates drop and walk rates increase in MLB, but the ratio typically compresses rather than collapsing entirely.
This stability makes K/BB particularly valuable for projection systems dealing with noisy minor league data.
## Historical Prospect Success Rates
### The Brutal Reality
Prospect development is a high-failure enterprise. Even highly-regarded prospects fail frequently:
**Top 100 prospects (consensus industry rankings):**
- Approximately 60-70% reach MLB
- Approximately 40-50% become regular MLB players (2+ WAR seasons)
- Approximately 20-30% become above-average regulars (3+ WAR)
- Approximately 10-15% become stars (5+ WAR)
**Double-AA players with 100+ PA/IP:**
- Approximately 40-50% reach MLB in any capacity
- Approximately 15-25% become regular MLB players
- Approximately 5-10% become above-average regulars
### Position-Specific Success Rates
Different positions show varying success rates:
**Highest MLB success rates:**
- Catchers (if they reach Double-AA, strong bet to reach MLB)
- Corner outfielders with power
- Middle infielders with hit tool
**Lowest MLB success rates:**
- First base-only prospects (high performance threshold)
- Relief pitchers (high variance, volatile role)
- Defense-first middle infielders
**Highest star outcome rates:**
- Shortstops and center fielders (positional value + tools)
- Power-hitting corner outfielders
- #1-2 starter pitching prospects
### Age-Based Success Modifiers
Success rates vary dramatically by age at level:
**Young for level (2+ years):**
- 2-3x higher MLB success rate
- 4-5x higher star outcome rate
**Old for level (2+ years):**
- 50% reduction in MLB success rate
- 80% reduction in star outcome rate
These multipliers explain why age-relative-to-level dominates prospect evaluation.
## Ceiling vs Floor Projections
### Defining Ceiling and Floor
**Ceiling**: The 90th percentile outcome if development goes very well
**Floor**: The 10th percentile outcome if development disappoints
**Median/Expected**: The 50th percentile most-likely outcome
Prospects vary enormously in their ceiling/floor distributions:
**High ceiling, low floor**: Young player with elite tools but major questions (strikeout issues, injury history, limited track record). Could be a star or never reach MLB.
**High floor, modest ceiling**: Polished college player with advanced approach but limited tools. Very likely to reach MLB but ceiling is role player.
**Balanced profile**: Solid tools, solid performance, reasonable age. Moderate probability of various outcomes.
### The Ceiling-Floor Tradeoff
Organizations face strategic choices about prospect profiles:
**Ceiling-focused approach**: Target high-upside prospects even with significant risk. Accept higher failure rates in pursuit of star outcomes. Typical for teams in rebuilding phases.
**Floor-focused approach**: Target safe prospects likely to reach MLB and contribute. Accept lower star probability. Typical for contending teams seeking reliable depth.
Most organizations balance both approaches across their system.
### Projection Distribution Width
Some prospects have tight projection distributions (limited uncertainty), others have wide distributions (high uncertainty):
**Narrow distribution examples:**
- Polished college hitters with advanced approach
- Command-oriented pitchers with proven track record
- Players with extensive high-level minor league performance
**Wide distribution examples:**
- Very young international signees
- Pitchers recovering from injury
- Players with extreme tools but poor performance
Projection systems should communicate uncertainty, not just point estimates.
## KATOH Projection System
### Overview
KATOH (Kaleigh's Adaptive Tool Outcome Hypothesis) represents one of the most sophisticated publicly-available prospect projection systems, developed by Eno Sarris and Kaleigh O'Halloran.
KATOH uniquely integrates:
- FanGraphs scouting grades (tools)
- Minor league statistical performance
- Age relative to level
- Historical comparable player outcomes
### Methodology
KATOH uses machine learning to identify historical comparables based on tools, performance, and context. The system then projects future outcomes based on how similar prospects developed.
Key inputs:
- Current FanGraphs (FV) future value grade (20-80 scale)
- Detailed tools grades (hit, power, speed, field, arm)
- Recent minor league performance metrics
- Age and level information
- Position
The system outputs MLB projection percentiles (90th, 75th, 50th, 25th percentile outcomes) for various timeframes.
### Strengths and Limitations
**Strengths:**
- Integrates scouting and statistics elegantly
- Provides probabilistic outcomes, not false precision
- Updates frequently with new performance data
- Transparent methodology
**Limitations:**
- Dependent on FanGraphs scouting grades (subjective input)
- Historical comparables approach may miss unique players
- Limited granularity for pitchers' pitch mix
- Cannot account for injury risk or off-field factors
## ZIPS Minor League System
### Overview
ZIPS Minor League projections, developed by Dan Szymborski, extend the widely-respected ZIPS MLB projection system to prospects.
### Methodology
ZIPS Minor League emphasizes statistical translation over scouting tools:
1. Translates minor league statistics to MLB equivalents using historical conversion factors
2. Applies age-based development curves
3. Regresses toward league-average based on sample size and uncertainty
4. Produces MLB-equivalent projections (slash lines, counting stats)
The system treats minor league statistics as noisy signals of underlying MLB talent, applying Bayesian updating as more information accumulates.
### Translation Approach
ZIPS uses empirical translation factors derived from thousands of historical player transitions. The system accounts for:
- League and level effects
- Park factors
- Era adjustments
- Position-specific aging curves
- Sample size reliability
### Strengths and Limitations
**Strengths:**
- Purely objective, no scouting input needed
- Consistent with proven ZIPS MLB methodology
- Handles limited sample sizes well through regression
- Transparent statistical framework
**Limitations:**
- Cannot account for tools not yet showing in statistics
- May undervalue very young, raw prospects
- Less effective for players with minimal professional data
- Struggles with pitchers changing roles or pitch mix
## Steamer Minor League System
### Overview
Steamer Minor League projections complement the widely-used Steamer MLB system, providing statistically-driven prospect projections.
### Methodology
Steamer Minor League uses a weighted combination of:
1. Translated minor league statistics (recent and career)
2. Age-based development adjustments
3. Regression to positional/organizational average
4. Similar player comparables (statistical similarity, not scouting)
The system emphasizes recent performance while incorporating broader career context.
### Weighting Scheme
Steamer typically weights:
- Recent performance: 40-50%
- Career performance: 20-30%
- Age/level context: 15-25%
- Comparables: 10-15%
Weights adjust based on sample size and reliability.
### Strengths and Limitations
**Strengths:**
- Battle-tested MLB projection methodology
- Handles partial-season data effectively
- Quick to update with new performance
- Free of scouting subjectivity
**Limitations:**
- Conservative on extreme outcomes (regression dampens ceiling)
- Limited incorporation of tools/scouting
- May lag on players with changing approach or skills
- Relatively simple compared to machine learning alternatives
## Comparing Projection Systems
### Complementary Approaches
The three major public systems offer complementary perspectives:
**KATOH**: Best for integrating scouting tools with performance, ideal for evaluating ceiling outcomes
**ZIPS**: Best for pure statistical translation, ideal for playing-time projection and floor outcomes
**Steamer**: Best for recent-performance-driven projections, ideal for in-season updates
### Ensemble Approach
Sophisticated analysts often combine multiple projection systems:
- Average projections across systems for median estimates
- Use KATOH for ceiling estimates (tools-influenced)
- Use ZIPS/Steamer for floor estimates (performance-driven)
- Monitor agreement/disagreement for confidence intervals
### System Selection by Context
Different contexts favor different systems:
**Dynasty fantasy baseball**: KATOH (ceiling matters most)
**Redraft fantasy baseball**: Steamer (near-term MLB playing time)
**MLB front offices**: Proprietary systems combining all public approaches plus internal scouting
**Betting markets**: Ensemble of multiple systems for robust estimates
## Python Implementation Examples
### Minor League Stat Adjustment
```python
import pandas as pd
import numpy as np
class MinorLeagueTranslator:
"""Translate minor league statistics to MLB equivalents"""
# Translation factors by level (multiplicative for rate stats)
LEVEL_FACTORS = {
'MLB': 1.000,
'AAA': 0.950,
'AA': 0.850,
'A+': 0.750,
'A': 0.650,
'A-': 0.550
}
# wOBA adjustments (subtractive)
WOBA_ADJUSTMENTS = {
'MLB': 0.000,
'AAA': 0.020,
'AA': 0.040,
'A+': 0.065,
'A': 0.090,
'A-': 0.110
}
def __init__(self):
self.league_factors = {}
def translate_woba(self, woba, level, age, league_avg_age=23.0):
"""
Translate wOBA from minor league level to MLB equivalent
Parameters:
-----------
woba : float
Raw wOBA at minor league level
level : str
Minor league level (AAA, AA, A+, A, A-)
age : float
Player age
league_avg_age : float
Average age at that level
Returns:
--------
float : MLB-equivalent wOBA
"""
# Apply level adjustment
mlb_eq_woba = woba - self.WOBA_ADJUSTMENTS.get(level, 0)
# Age adjustment (younger = bonus, older = penalty)
age_diff = league_avg_age - age
age_adjustment = age_diff * 0.008 # ~8 points wOBA per year
mlb_eq_woba += age_adjustment
return mlb_eq_woba
def translate_statline(self, stats_dict, level, age):
"""
Translate full statistical line to MLB equivalent
Parameters:
-----------
stats_dict : dict
Dictionary with keys: PA, H, 2B, 3B, HR, BB, HBP, K
level : str
Minor league level
age : float
Player age
Returns:
--------
dict : MLB-equivalent statistics
"""
factor = self.LEVEL_FACTORS.get(level, 0.65)
# Calculate raw rates
pa = stats_dict['PA']
bb_rate = stats_dict['BB'] / pa
k_rate = stats_dict['K'] / pa
iso = (stats_dict['2B'] + 2*stats_dict['3B'] + 3*stats_dict['HR']) / (pa - stats_dict['BB'] - stats_dict['HBP'] - stats_dict['K'])
# Translate rates (K and BB translate well, ISO needs more adjustment)
mlb_bb_rate = bb_rate * 0.85 # BB rate drops slightly
mlb_k_rate = k_rate * 1.15 # K rate increases
mlb_iso = iso * factor
# Age adjustments
league_avg_ages = {'AAA': 25, 'AA': 23, 'A+': 22, 'A': 21, 'A-': 20}
age_diff = league_avg_ages.get(level, 23) - age
mlb_bb_rate += age_diff * 0.003
mlb_k_rate -= age_diff * 0.005
mlb_iso += age_diff * 0.008
return {
'BB%': mlb_bb_rate,
'K%': mlb_k_rate,
'ISO': mlb_iso,
'level': level,
'age': age
}
# Example usage
translator = MinorLeagueTranslator()
# Player A: 21-year-old in Double-A
stats_a = {
'PA': 450,
'H': 125,
'2B': 28,
'3B': 4,
'HR': 15,
'BB': 52,
'HBP': 8,
'K': 95
}
mlb_eq_a = translator.translate_statline(stats_a, 'AA', 21)
print("21-year-old Double-A Player MLB Equivalent:")
print(f"BB%: {mlb_eq_a['BB%']:.1%}")
print(f"K%: {mlb_eq_a['K%']:.1%}")
print(f"ISO: {mlb_eq_a['ISO']:.3f}")
# Player B: 25-year-old in Double-A (old for level)
mlb_eq_b = translator.translate_statline(stats_a, 'AA', 25)
print("\n25-year-old Double-A Player MLB Equivalent:")
print(f"BB%: {mlb_eq_b['BB%']:.1%}")
print(f"K%: {mlb_eq_b['K%']:.1%}")
print(f"ISO: {mlb_eq_b['ISO']:.3f}")
```
### Age-Level Analysis
```python
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
class ProspectAgeAnalyzer:
"""Analyze prospect performance relative to age and level"""
def __init__(self, prospect_data):
"""
Initialize with prospect database
Parameters:
-----------
prospect_data : pd.DataFrame
Must include columns: player_id, age, level, wOBA, future_MLB_WAR
"""
self.data = prospect_data
def calculate_age_advantage(self, age, level):
"""Calculate how many years younger/older than typical for level"""
typical_ages = {
'AAA': 25.0,
'AA': 23.0,
'A+': 22.0,
'A': 21.0,
'A-': 20.0
}
typical = typical_ages.get(level, 23.0)
return typical - age
def success_rate_by_age_group(self, level, min_war=2.0):
"""
Calculate MLB success rates by age relative to level
Parameters:
-----------
level : str
Minor league level to analyze
min_war : float
Minimum career WAR to count as success
Returns:
--------
pd.DataFrame : Success rates by age group
"""
level_data = self.data[self.data['level'] == level].copy()
level_data['age_advantage'] = level_data.apply(
lambda x: self.calculate_age_advantage(x['age'], x['level']),
axis=1
)
# Bin by age advantage
level_data['age_group'] = pd.cut(
level_data['age_advantage'],
bins=[-10, -2, -1, 0, 1, 2, 10],
labels=['Very Old', 'Old', 'Slightly Old', 'Average', 'Young', 'Very Young']
)
# Calculate success rate
success_rates = level_data.groupby('age_group').agg({
'future_MLB_WAR': [
('count', 'count'),
('success_rate', lambda x: (x >= min_war).mean()),
('avg_war', 'mean'),
('star_rate', lambda x: (x >= 5.0).mean())
]
})
return success_rates
def plot_age_performance_relationship(self, level):
"""Visualize relationship between age and performance at a level"""
level_data = self.data[self.data['level'] == level]
fig, axes = plt.subplots(1, 2, figsize=(14, 6))
# Plot 1: Age vs current wOBA
axes[0].scatter(level_data['age'], level_data['wOBA'], alpha=0.5)
axes[0].set_xlabel('Age')
axes[0].set_ylabel('wOBA')
axes[0].set_title(f'{level}: Age vs Current Performance')
# Add trend line
z = np.polyfit(level_data['age'], level_data['wOBA'], 1)
p = np.poly1d(z)
axes[0].plot(level_data['age'], p(level_data['age']), "r--", alpha=0.8)
# Plot 2: Age vs future MLB WAR
axes[1].scatter(level_data['age'], level_data['future_MLB_WAR'], alpha=0.5)
axes[1].set_xlabel('Age')
axes[1].set_ylabel('Future MLB WAR')
axes[1].set_title(f'{level}: Age vs Future MLB Success')
z2 = np.polyfit(level_data['age'], level_data['future_MLB_WAR'], 1)
p2 = np.poly1d(z2)
axes[1].plot(level_data['age'], p2(level_data['age']), "r--", alpha=0.8)
plt.tight_layout()
return fig
def create_age_adjusted_rankings(self, level, min_pa=200):
"""
Create prospect rankings adjusted for age
Parameters:
-----------
level : str
Level to rank
min_pa : int
Minimum PA to qualify
Returns:
--------
pd.DataFrame : Ranked prospects with age adjustments
"""
level_data = self.data[
(self.data['level'] == level) &
(self.data['PA'] >= min_pa)
].copy()
level_data['age_advantage'] = level_data.apply(
lambda x: self.calculate_age_advantage(x['age'], x['level']),
axis=1
)
# Create age-adjusted wOBA (add ~10 points per year younger)
level_data['age_adj_wOBA'] = (
level_data['wOBA'] +
level_data['age_advantage'] * 0.010
)
# Rank by age-adjusted performance
level_data['rank'] = level_data['age_adj_wOBA'].rank(ascending=False)
return level_data.sort_values('rank')[
['player_id', 'age', 'wOBA', 'age_advantage', 'age_adj_wOBA', 'rank']
]
# Example usage with synthetic data
np.random.seed(42)
n_prospects = 500
synthetic_prospects = pd.DataFrame({
'player_id': range(n_prospects),
'age': np.random.normal(23, 2, n_prospects),
'level': np.random.choice(['AA', 'AAA', 'A+'], n_prospects),
'wOBA': np.random.normal(0.340, 0.045, n_prospects),
'PA': np.random.randint(200, 500, n_prospects),
'future_MLB_WAR': np.random.exponential(1.5, n_prospects)
})
# Make younger players slightly better (simulate real relationship)
synthetic_prospects['wOBA'] += (24 - synthetic_prospects['age']) * 0.005
synthetic_prospects['future_MLB_WAR'] += (24 - synthetic_prospects['age']) * 0.15
analyzer = ProspectAgeAnalyzer(synthetic_prospects)
# Analyze Double-A
aa_success = analyzer.success_rate_by_age_group('AA', min_war=2.0)
print("Double-A Success Rates by Age Group:")
print(aa_success)
# Age-adjusted rankings
aa_rankings = analyzer.create_age_adjusted_rankings('AA', min_pa=200)
print("\nTop 10 Double-A Prospects (Age-Adjusted):")
print(aa_rankings.head(10))
```
### Prospect Success Probability Model
```python
import pandas as pd
import numpy as np
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report, roc_auc_score
import matplotlib.pyplot as plt
class ProspectSuccessModel:
"""Build predictive model for prospect MLB success"""
def __init__(self):
self.model = None
self.feature_names = None
def engineer_features(self, df):
"""
Create features from raw prospect data
Parameters:
-----------
df : pd.DataFrame
Must include: age, level, PA, BB, K, ISO, wOBA, tools_grade
Returns:
--------
pd.DataFrame : Feature matrix
"""
features = pd.DataFrame()
# Rate stats
features['BB_rate'] = df['BB'] / df['PA']
features['K_rate'] = df['K'] / df['PA']
features['ISO'] = df['ISO']
features['wOBA'] = df['wOBA']
# Discipline metrics
features['BB_K_ratio'] = df['BB'] / df['K'].replace(0, 1)
# Age relative to level
level_avg_ages = {'AAA': 25, 'AA': 23, 'A+': 22, 'A': 21}
features['age_advantage'] = df.apply(
lambda x: level_avg_ages.get(x['level'], 23) - x['age'],
axis=1
)
# Level encoding (higher = more advanced)
level_encoding = {'A': 1, 'A+': 2, 'AA': 3, 'AAA': 4}
features['level_num'] = df['level'].map(level_encoding)
# Interaction terms
features['wOBA_age_interaction'] = features['wOBA'] * features['age_advantage']
features['ISO_age_interaction'] = features['ISO'] * features['age_advantage']
features['discipline_age_interaction'] = features['BB_K_ratio'] * features['age_advantage']
# Tools grade if available
if 'tools_grade' in df.columns:
features['tools_grade'] = df['tools_grade']
features['tools_performance_gap'] = df['tools_grade'] - (df['wOBA'] * 100)
# Sample size indicator
features['PA'] = df['PA']
return features
def train(self, X, y, test_size=0.25):
"""
Train random forest model to predict MLB success
Parameters:
-----------
X : pd.DataFrame
Feature matrix
y : pd.Series
Target (1 = MLB success, 0 = failure)
test_size : float
Proportion for test set
Returns:
--------
dict : Training results and metrics
"""
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=test_size, random_state=42, stratify=y
)
self.feature_names = X.columns.tolist()
# Train Random Forest
self.model = RandomForestClassifier(
n_estimators=200,
max_depth=10,
min_samples_split=20,
min_samples_leaf=10,
random_state=42,
class_weight='balanced'
)
self.model.fit(X_train, y_train)
# Predictions
y_pred = self.model.predict(X_test)
y_pred_proba = self.model.predict_proba(X_test)[:, 1]
# Metrics
auc = roc_auc_score(y_test, y_pred_proba)
return {
'model': self.model,
'auc': auc,
'X_test': X_test,
'y_test': y_test,
'y_pred': y_pred,
'y_pred_proba': y_pred_proba,
'classification_report': classification_report(y_test, y_pred)
}
def predict_success_probability(self, prospect_features):
"""
Predict probability of MLB success for new prospect
Parameters:
-----------
prospect_features : pd.DataFrame
Engineered features for prospect(s)
Returns:
--------
np.array : Probability of success
"""
if self.model is None:
raise ValueError("Model not trained yet")
return self.model.predict_proba(prospect_features)[:, 1]
def feature_importance_plot(self):
"""Plot feature importance from trained model"""
if self.model is None:
raise ValueError("Model not trained yet")
importances = self.model.feature_importances_
indices = np.argsort(importances)[::-1]
plt.figure(figsize=(10, 8))
plt.title("Feature Importance for Prospect Success Prediction")
plt.barh(range(len(indices)), importances[indices])
plt.yticks(range(len(indices)), [self.feature_names[i] for i in indices])
plt.xlabel("Relative Importance")
plt.tight_layout()
return plt.gcf()
# Example usage with synthetic data
np.random.seed(42)
n_samples = 1000
# Create synthetic historical prospect data
historical_prospects = pd.DataFrame({
'age': np.random.normal(22, 2, n_samples),
'level': np.random.choice(['AA', 'AAA', 'A+'], n_samples),
'PA': np.random.randint(200, 500, n_samples),
'BB': np.random.randint(20, 80, n_samples),
'K': np.random.randint(50, 150, n_samples),
'ISO': np.random.normal(0.150, 0.050, n_samples),
'wOBA': np.random.normal(0.340, 0.045, n_samples),
'tools_grade': np.random.normal(50, 10, n_samples)
})
# Create target: success = 2+ WAR in MLB (simulate based on features)
success_probability = (
(historical_prospects['wOBA'] - 0.300) * 5 +
(historical_prospects['BB'] / historical_prospects['K']) * 0.3 +
(23 - historical_prospects['age']) * 0.05 +
np.random.normal(0, 0.1, n_samples)
)
historical_prospects['MLB_success'] = (success_probability > 0.5).astype(int)
# Train model
model = ProspectSuccessModel()
X = model.engineer_features(historical_prospects)
y = historical_prospects['MLB_success']
results = model.train(X, y)
print(f"Model AUC: {results['auc']:.3f}")
print("\nClassification Report:")
print(results['classification_report'])
# Predict for new prospect
new_prospect = pd.DataFrame({
'age': [21],
'level': ['AA'],
'PA': [400],
'BB': [55],
'K': [85],
'ISO': [0.180],
'wOBA': [0.360],
'tools_grade': [55]
})
new_prospect_features = model.engineer_features(new_prospect)
success_prob = model.predict_success_probability(new_prospect_features)
print(f"\nNew prospect MLB success probability: {success_prob[0]:.1%}")
# Feature importance
importance_fig = model.feature_importance_plot()
```
## R Implementation Examples
### Minor League Translation in R
```r
library(tidyverse)
library(broom)
# Minor league translation class
MinorLeagueTranslator <- R6::R6Class(
"MinorLeagueTranslator",
public = list(
level_factors = list(
MLB = 1.000,
AAA = 0.950,
AA = 0.850,
A_plus = 0.750,
A = 0.650,
A_minus = 0.550
),
woba_adjustments = list(
MLB = 0.000,
AAA = 0.020,
AA = 0.040,
A_plus = 0.065,
A = 0.090,
A_minus = 0.110
),
translate_woba = function(woba, level, age, league_avg_age = 23.0) {
# Apply level adjustment
mlb_eq_woba <- woba - self$woba_adjustments[[level]]
# Age adjustment
age_diff <- league_avg_age - age
age_adjustment <- age_diff * 0.008
mlb_eq_woba <- mlb_eq_woba + age_adjustment
return(mlb_eq_woba)
},
translate_statline = function(stats_df) {
stats_df %>%
mutate(
BB_rate = BB / PA,
K_rate = K / PA,
ISO = (X2B + 2*X3B + 3*HR) / (PA - BB - HBP - K),
# Apply MLB translation
mlb_BB_rate = BB_rate * 0.85,
mlb_K_rate = K_rate * 1.15,
mlb_ISO = ISO * self$level_factors[[level]],
# Age adjustments
age_diff = case_when(
level == "AAA" ~ 25 - age,
level == "AA" ~ 23 - age,
level == "A_plus" ~ 22 - age,
level == "A" ~ 21 - age,
TRUE ~ 23 - age
),
mlb_BB_rate = mlb_BB_rate + age_diff * 0.003,
mlb_K_rate = mlb_K_rate - age_diff * 0.005,
mlb_ISO = mlb_ISO + age_diff * 0.008
)
}
)
)
# Example usage
translator <- MinorLeagueTranslator$new()
prospect_stats <- tibble(
player_id = c("A", "B"),
age = c(21, 25),
level = c("AA", "AA"),
PA = c(450, 450),
H = c(125, 125),
X2B = c(28, 28),
X3B = c(4, 4),
HR = c(15, 15),
BB = c(52, 52),
HBP = c(8, 8),
K = c(95, 95)
)
mlb_equivalent <- translator$translate_statline(prospect_stats)
print(mlb_equivalent %>% select(player_id, age, mlb_BB_rate, mlb_K_rate, mlb_ISO))
```
### Comparing Prospect Rankings to MLB Outcomes
```r
library(tidyverse)
library(ggplot2)
library(viridis)
# Analysis of prospect ranking accuracy
ProspectRankingAnalysis <- R6::R6Class(
"ProspectRankingAnalysis",
public = list(
# Calculate ranking accuracy metrics
evaluate_ranking_accuracy = function(rankings_df) {
rankings_df %>%
mutate(
# Success tiers
success_tier = case_when(
actual_WAR >= 5 ~ "Star",
actual_WAR >= 2 ~ "Regular",
actual_WAR >= 0 ~ "Marginal",
TRUE ~ "Bust"
),
# Ranking tiers
rank_tier = case_when(
prospect_rank <= 20 ~ "Top 20",
prospect_rank <= 50 ~ "Top 50",
prospect_rank <= 100 ~ "Top 100",
TRUE ~ "Unranked"
)
) %>%
group_by(rank_tier) %>%
summarize(
n = n(),
star_rate = mean(success_tier == "Star"),
regular_rate = mean(success_tier %in% c("Star", "Regular")),
mlb_rate = mean(actual_WAR >= 0),
avg_war = mean(actual_WAR),
median_war = median(actual_WAR)
)
},
# Compare projection systems
compare_systems = function(projections_df) {
projections_df %>%
pivot_longer(
cols = c(KATOH_proj, ZIPS_proj, Steamer_proj),
names_to = "system",
values_to = "projection"
) %>%
group_by(system) %>%
summarize(
mae = mean(abs(projection - actual_WAR)),
rmse = sqrt(mean((projection - actual_WAR)^2)),
correlation = cor(projection, actual_WAR),
# Calibration: do 2 WAR projections average 2 WAR actual?
calibration_1WAR = mean(actual_WAR[projection >= 0.5 & projection < 1.5]),
calibration_2WAR = mean(actual_WAR[projection >= 1.5 & projection < 2.5]),
calibration_3WAR = mean(actual_WAR[projection >= 2.5 & projection < 3.5])
)
},
# Visualize ranking vs outcome
plot_ranking_outcomes = function(rankings_df) {
rankings_df %>%
ggplot(aes(x = prospect_rank, y = actual_WAR)) +
geom_point(alpha = 0.4, color = "steelblue") +
geom_smooth(method = "loess", color = "darkred", se = TRUE) +
scale_y_continuous(limits = c(-5, 15)) +
labs(
title = "Prospect Ranking vs Actual MLB Performance",
x = "Prospect Ranking (lower = better)",
y = "Actual MLB WAR (first 6 seasons)",
caption = "Smoothed trend line shows average outcomes"
) +
theme_minimal()
},
# Age-adjusted success rates
age_success_analysis = function(prospects_df) {
prospects_df %>%
mutate(
age_group = cut(
age,
breaks = c(0, 20, 21, 22, 23, 24, 30),
labels = c("< 20", "20", "21", "22", "23", "24+")
)
) %>%
group_by(age_group, level) %>%
summarize(
n = n(),
star_rate = mean(actual_WAR >= 5, na.rm = TRUE),
regular_rate = mean(actual_WAR >= 2, na.rm = TRUE),
avg_war = mean(actual_WAR, na.rm = TRUE),
.groups = "drop"
) %>%
ggplot(aes(x = age_group, y = regular_rate, fill = level)) +
geom_col(position = "dodge") +
scale_y_continuous(labels = scales::percent) +
scale_fill_viridis_d() +
labs(
title = "MLB Success Rate by Age and Level",
x = "Age",
y = "% Becoming Regular MLB Player (2+ WAR)",
fill = "Level"
) +
theme_minimal()
}
)
)
# Example usage with synthetic data
set.seed(42)
n <- 500
synthetic_rankings <- tibble(
player_id = 1:n,
prospect_rank = sample(1:200, n, replace = TRUE),
age = rnorm(n, 22, 2),
level = sample(c("AA", "AAA", "A+"), n, replace = TRUE),
KATOH_proj = rexp(n, 1/2),
ZIPS_proj = rexp(n, 1/1.8),
Steamer_proj = rexp(n, 1/1.9),
# Actual outcomes (correlated with projections but noisy)
actual_WAR = pmax(0, KATOH_proj * 0.6 + rnorm(n, 0, 2))
)
analyzer <- ProspectRankingAnalysis$new()
# Evaluate ranking accuracy
accuracy <- analyzer$evaluate_ranking_accuracy(synthetic_rankings)
print("Ranking Tier Success Rates:")
print(accuracy)
# Compare projection systems
system_comparison <- analyzer$compare_systems(synthetic_rankings)
print("\nProjection System Comparison:")
print(system_comparison)
# Visualizations
ranking_plot <- analyzer$plot_ranking_outcomes(synthetic_rankings)
age_plot <- analyzer$age_success_analysis(synthetic_rankings)
print(ranking_plot)
print(age_plot)
```
## Conclusion
Prospect projection represents baseball analytics at its most uncertain and most rewarding. Successfully identifying future MLB talent requires blending statistical analysis, scouting evaluation, developmental understanding, and probabilistic thinking.
The best prospect evaluators combine multiple approaches:
- Statistical translation systems (ZIPS, Steamer) for objective baseline projections
- Tools-integrated systems (KATOH) for ceiling estimation
- Age-relative-to-level analysis for context
- Historical comparable analysis for developmental trajectory
- Domain expertise for qualitative factors (makeup, injury risk, organizational development)
No projection system achieves high accuracy for individual prospects - the inherent uncertainty is simply too large. However, well-calibrated systems provide valuable probabilistic estimates that, aggregated across many prospects, generate significant analytical advantage.
The key is communicating uncertainty honestly, updating beliefs as new information arrives, and recognizing that prospect projection is a probability game, not a prediction certainty.
Discussion
Have questions or feedback? Join our community discussion on
Discord or
GitHub Discussions.
Table of Contents
Related Topics
Quick Actions