Exit Velocity and Launch Angle

Intermediate 10 min read 1 views Nov 26, 2025

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

CategoryExit VelocityOutcome
Weak Contact< 80 mphLikely Out (95%+)
Average Contact80-90 mphMixed Results
Hard Contact90-95 mphPositive Outcome
Very Hard95-100 mphStrong Positive
Elite Contact100-105 mphExcellent Outcome
Maximum110+ mphExceptional

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 AngleTypeBatting 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

Discussion

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