Exit Velocity and Launch Angle
Exit Velocity and Launch Angle: The Physics of Modern Baseball Hitting
Exit velocity and launch angle have revolutionized how we evaluate hitting performance in baseball. These two metrics, popularized by MLB's Statcast system, provide unprecedented insight into the quality of contact a hitter makes with the baseball.
Understanding Exit Velocity
Exit velocity (EV) measures the speed of the baseball as it comes off the bat, recorded in miles per hour (mph). Higher exit velocities correlate strongly with positive offensive outcomes—balls hit harder are more likely to become hits.
Exit Velocity Benchmarks
| Category | Exit Velocity | Outcome |
|---|---|---|
| Weak Contact | < 80 mph | Likely Out (95%+) |
| Average Contact | 80-90 mph | Mixed Results |
| Hard Contact | 90-95 mph | Positive Outcome |
| Very Hard | 95-100 mph | Strong Positive |
| Elite Contact | 100-105 mph | Excellent Outcome |
| Maximum | 110+ mph | Exceptional |
Understanding Launch Angle
Launch angle measures the vertical angle at which the ball leaves the bat in degrees. Negative angles indicate ground balls, zero is level, and positive angles indicate balls in the air.
Launch Angle Benchmarks
| Launch Angle | Type | Batting Avg |
|---|---|---|
| < -10° | Topped/Weak GB | .150-.220 |
| -10° to 10° | Ground Ball | .250-.290 |
| 10° to 25° | Line Drive | .650-.700 |
| 25° to 35° | Fly Ball | .350-.450 |
| > 40° | Pop Up | .050-.100 |
The Barrel Concept
A "barrel" represents the ideal combination of exit velocity and launch angle. Statcast defines barrels using a dynamic scale where minimum EV depends on launch angle. At 26-30° launch angle, 98+ mph is required. Barrels produce approximately .830 batting average and 2.650 slugging percentage.
Python Implementation
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from pybaseball import statcast_batter, playerid_lookup
# Get player Statcast data
def analyze_ev_la(first_name, last_name, start_date, end_date):
# Look up player ID
player = playerid_lookup(last_name, first_name)
player_id = player.iloc[0]['key_mlbam']
# Get Statcast data
data = statcast_batter(start_date, end_date, player_id)
# Filter for batted balls
batted_balls = data[data['type'] == 'X'].dropna(subset=['launch_speed', 'launch_angle'])
# Calculate metrics
avg_ev = batted_balls['launch_speed'].mean()
max_ev = batted_balls['launch_speed'].max()
avg_la = batted_balls['launch_angle'].mean()
hard_hit_rate = (batted_balls['launch_speed'] >= 95).sum() / len(batted_balls) * 100
barrel_rate = batted_balls['barrel'].sum() / len(batted_balls) * 100
print(f"\n{first_name} {last_name} - Exit Velocity & Launch Angle Analysis")
print("=" * 60)
print(f"Average Exit Velocity: {avg_ev:.1f} mph")
print(f"Max Exit Velocity: {max_ev:.1f} mph")
print(f"Average Launch Angle: {avg_la:.1f}°")
print(f"Hard Hit Rate (95+ mph): {hard_hit_rate:.1f}%")
print(f"Barrel Rate: {barrel_rate:.1f}%")
return batted_balls
# Create visualization
def create_ev_la_plot(data, player_name):
plt.figure(figsize=(12, 10))
# Color by outcome
colors = data['events'].map({
'single': 'green', 'double': 'blue', 'triple': 'purple',
'home_run': 'red', 'field_out': 'gray'
}).fillna('gray')
plt.scatter(data['launch_angle'], data['launch_speed'],
c=colors, alpha=0.6, s=50)
# Add barrel zone
plt.axhline(y=95, color='orange', linestyle='--', alpha=0.5, label='Hard Hit (95 mph)')
plt.axvline(x=10, color='green', linestyle=':', alpha=0.5)
plt.axvline(x=30, color='green', linestyle=':', alpha=0.5, label='Optimal LA Zone')
plt.xlabel('Launch Angle (degrees)', fontsize=12)
plt.ylabel('Exit Velocity (mph)', fontsize=12)
plt.title(f'{player_name} - Exit Velocity vs Launch Angle', fontsize=14)
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('ev_la_scatter.png', dpi=300)
plt.show()
# Example usage
data = analyze_ev_la("Aaron", "Judge", "2024-04-01", "2024-10-01")
create_ev_la_plot(data, "Aaron Judge")
# Sweet spot analysis
sweet_spot = data[(data['launch_angle'] >= 8) & (data['launch_angle'] <= 32)]
sweet_spot_pct = len(sweet_spot) / len(data) * 100
print(f"\nSweet Spot %: {sweet_spot_pct:.1f}%")
R Implementation
library(baseballr)
library(dplyr)
library(ggplot2)
# Get Statcast data
analyze_ev_la <- function(start_date, end_date) {
statcast_data <- scrape_statcast_savant(
start_date = start_date,
end_date = end_date,
player_type = "batter"
)
# Filter for batted balls
batted_balls <- statcast_data %>%
filter(type == "X", !is.na(launch_speed), !is.na(launch_angle))
# Calculate player-level stats
player_stats <- batted_balls %>%
group_by(player_name) %>%
summarise(
batted_balls = n(),
avg_ev = mean(launch_speed, na.rm = TRUE),
max_ev = max(launch_speed, na.rm = TRUE),
avg_la = mean(launch_angle, na.rm = TRUE),
hard_hit_rate = sum(launch_speed >= 95) / n() * 100,
barrel_rate = sum(barrel == 1, na.rm = TRUE) / n() * 100,
.groups = 'drop'
) %>%
filter(batted_balls >= 100) %>%
arrange(desc(avg_ev))
return(list(raw = batted_balls, summary = player_stats))
}
# Get data
results <- analyze_ev_la("2024-06-01", "2024-06-30")
# Top exit velocity leaders
cat("Top 10 Average Exit Velocity Leaders:\n")
print(head(results$summary, 10))
# Create EV/LA scatter plot
create_ev_la_plot <- function(data, player_name) {
player_data <- data %>% filter(player_name == !!player_name)
ggplot(player_data, aes(x = launch_angle, y = launch_speed)) +
geom_point(aes(color = events), alpha = 0.6, size = 3) +
geom_hline(yintercept = 95, linetype = "dashed", color = "orange") +
geom_vline(xintercept = c(10, 30), linetype = "dotted", color = "green") +
scale_color_manual(values = c(
"single" = "green", "double" = "blue", "triple" = "purple",
"home_run" = "red", "field_out" = "gray"
)) +
labs(
title = paste(player_name, "- Exit Velocity vs Launch Angle"),
x = "Launch Angle (degrees)",
y = "Exit Velocity (mph)",
color = "Outcome"
) +
theme_minimal() +
xlim(-90, 90) +
ylim(40, 120)
}
# Distribution plots
ggplot(results$raw, aes(x = launch_speed)) +
geom_histogram(binwidth = 2, fill = "steelblue", color = "black", alpha = 0.7) +
geom_vline(xintercept = 95, color = "red", linetype = "dashed") +
labs(title = "Exit Velocity Distribution", x = "Exit Velocity (mph)", y = "Count") +
theme_minimal()
ggplot(results$raw, aes(x = launch_angle)) +
geom_histogram(binwidth = 3, fill = "darkgreen", color = "black", alpha = 0.7) +
geom_vline(xintercept = c(10, 30), color = "orange", linetype = "dashed") +
labs(title = "Launch Angle Distribution", x = "Launch Angle (degrees)", y = "Count") +
theme_minimal()
The Launch Angle Revolution
The recognition of optimal launch angles triggered a revolution in hitting philosophy. Traditional coaching emphasized "staying on top of the ball," but data revealed that slightly elevated launch angles (15-30°) produced superior results. This led to the "fly ball revolution" beginning around 2015-2016, with league-wide home run rates exploding.
Expected Statistics
Exit velocity and launch angle combine to create expected statistics (xBA, xSLG, xwOBA) that predict what a hitter "should" have achieved based on contact quality. These help identify players who may have been lucky or unlucky.
Key Takeaways
- Exit velocity correlates with success: Harder hit balls are more likely to become hits
- Optimal launch angle is 10-30°: This range produces the best offensive outcomes
- Barrels are king: The combination of high EV and optimal LA produces elite results
- Hard hit rate matters: Consistently hitting 95+ mph indicates quality contact
- Expected stats reveal truth: xBA, xSLG strip away luck to show underlying performance