Strike Zone Heat Maps
Intermediate
10 min read
1 views
Nov 26, 2025
Strike Zone Heatmaps: Visual Pitch Analysis
Strike zone heatmaps transform raw pitch location data into intuitive, color-coded visualizations showing where pitches are thrown and how batters perform in different zones. Hot zones (warm colors) indicate high activity or performance, while cold zones (cool colors) represent low activity or performance.
Strike Zone Numbering System
| Zone | Location | Description |
|---|---|---|
| 1 | Top-Inside | Upper inside corner |
| 2 | Top-Middle | Upper middle of zone |
| 3 | Top-Outside | Upper outside corner |
| 4 | Middle-Inside | Middle height, inside |
| 5 | Middle-Middle | Heart of the plate |
| 6 | Middle-Outside | Middle height, outside |
| 7 | Bottom-Inside | Lower inside corner |
| 8 | Bottom-Middle | Lower middle of zone |
| 9 | Bottom-Outside | Lower outside corner |
| 11-14 | Off-Zone | Outside strike zone edges |
Python Implementation
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from pybaseball import statcast_batter, playerid_lookup
# Get player data
def get_player_pitch_data(first_name, last_name, start_date, end_date):
player = playerid_lookup(last_name, first_name)
player_id = player.iloc[0]['key_mlbam']
data = statcast_batter(start_date, end_date, player_id)
return data.dropna(subset=['plate_x', 'plate_z'])
# Create pitch location heatmap
def create_pitch_heatmap(data, player_name):
fig, ax = plt.subplots(figsize=(10, 10))
# Create 2D histogram
heatmap = ax.hexbin(data['plate_x'], data['plate_z'],
gridsize=15, cmap='YlOrRd', mincnt=1)
# Draw strike zone
strike_zone_x = [-0.83, 0.83, 0.83, -0.83, -0.83]
strike_zone_z = [1.5, 1.5, 3.5, 3.5, 1.5]
ax.plot(strike_zone_x, strike_zone_z, color='black', linewidth=2)
ax.set_xlabel('Horizontal Location (ft)', fontsize=12)
ax.set_ylabel('Vertical Location (ft)', fontsize=12)
ax.set_title(f'{player_name} - Pitch Location Heatmap', fontsize=14, fontweight='bold')
plt.colorbar(heatmap, ax=ax, label='Pitch Count')
ax.set_xlim(-2, 2)
ax.set_ylim(0, 5)
plt.tight_layout()
plt.savefig('pitch_heatmap.png', dpi=300)
plt.show()
# Create performance heatmap by zone
def create_performance_heatmap(data, player_name, metric='avg'):
# Create zone bins
x_bins = np.linspace(-1.5, 1.5, 10)
z_bins = np.linspace(1.0, 4.0, 10)
data['x_zone'] = pd.cut(data['plate_x'], bins=x_bins)
data['z_zone'] = pd.cut(data['plate_z'], bins=z_bins)
# Calculate batting average by zone
data['hit'] = data['events'].isin(['single', 'double', 'triple', 'home_run'])
data['ab'] = data['events'].isin(['single', 'double', 'triple', 'home_run',
'field_out', 'strikeout', 'force_out',
'double_play', 'fielders_choice_out'])
zone_stats = data[data['ab']].groupby(['x_zone', 'z_zone']).agg({
'hit': 'sum', 'ab': 'sum'
}).reset_index()
zone_stats['avg'] = zone_stats['hit'] / zone_stats['ab']
zone_stats = zone_stats[zone_stats['ab'] >= 5]
# Create pivot for heatmap
pivot = zone_stats.pivot_table(values='avg', index='z_zone', columns='x_zone')
# Plot
fig, ax = plt.subplots(figsize=(12, 10))
sns.heatmap(pivot, annot=True, fmt='.3f', cmap='RdYlGn',
center=pivot.mean().mean(), ax=ax, cbar_kws={'label': 'Batting Average'})
ax.set_title(f'{player_name} - Batting Average by Zone', fontsize=14, fontweight='bold')
ax.set_xlabel('Horizontal Location (Inside → Outside)', fontsize=12)
ax.set_ylabel('Vertical Location (Low → High)', fontsize=12)
plt.tight_layout()
plt.savefig('performance_heatmap.png', dpi=300)
plt.show()
# Swing rate heatmap
def create_swing_rate_heatmap(data, player_name):
swing_types = ['hit_into_play', 'swinging_strike', 'foul',
'swinging_strike_blocked', 'foul_tip']
data['swing'] = data['description'].isin(swing_types)
x_bins = np.linspace(-2, 2, 15)
z_bins = np.linspace(0.5, 4.5, 15)
data['x_bin'] = pd.cut(data['plate_x'], bins=x_bins)
data['z_bin'] = pd.cut(data['plate_z'], bins=z_bins)
swing_rate = data.groupby(['x_bin', 'z_bin']).agg({
'swing': ['sum', 'count']
}).reset_index()
swing_rate.columns = ['x_bin', 'z_bin', 'swings', 'total']
swing_rate['rate'] = swing_rate['swings'] / swing_rate['total']
swing_rate = swing_rate[swing_rate['total'] >= 5]
pivot = swing_rate.pivot_table(values='rate', index='z_bin', columns='x_bin')
fig, ax = plt.subplots(figsize=(14, 10))
sns.heatmap(pivot, annot=True, fmt='.2f', cmap='coolwarm', center=0.5,
ax=ax, vmin=0, vmax=1, cbar_kws={'label': 'Swing Rate'})
ax.set_title(f'{player_name} - Swing Rate by Zone', fontsize=14, fontweight='bold')
plt.tight_layout()
plt.savefig('swing_rate_heatmap.png', dpi=300)
plt.show()
# Example usage
data = get_player_pitch_data("Aaron", "Judge", "2024-04-01", "2024-09-30")
create_pitch_heatmap(data, "Aaron Judge")
create_performance_heatmap(data, "Aaron Judge")
R Implementation
library(baseballr)
library(ggplot2)
library(dplyr)
library(viridis)
# Get Statcast data
get_pitch_data <- function(player_id, start_date, end_date) {
data <- scrape_statcast_savant(
start_date = start_date,
end_date = end_date,
playerid = player_id,
player_type = "batter"
)
return(data %>% filter(!is.na(plate_x), !is.na(plate_z)))
}
# Create heatmap
create_pitch_heatmap <- function(data, player_name) {
# Strike zone boundaries
strike_zone <- data.frame(
x = c(-0.83, 0.83, 0.83, -0.83, -0.83),
z = c(1.5, 1.5, 3.5, 3.5, 1.5)
)
p <- ggplot(data, aes(x = plate_x, y = plate_z)) +
stat_density_2d(aes(fill = ..density..), geom = "raster", contour = FALSE) +
scale_fill_viridis(option = "plasma", name = "Density") +
geom_path(data = strike_zone, aes(x = x, y = z),
color = "white", linewidth = 1.5) +
coord_fixed(ratio = 1) +
xlim(-2, 2) +
ylim(0, 5) +
labs(
title = paste(player_name, "- Pitch Location Heatmap"),
x = "Horizontal Location (ft)",
y = "Vertical Location (ft)"
) +
theme_minimal() +
theme(
plot.title = element_text(size = 16, face = "bold", hjust = 0.5)
)
ggsave("pitch_heatmap.png", p, width = 10, height = 8, dpi = 300)
print(p)
}
# Performance heatmap
create_performance_heatmap <- function(data, player_name) {
data <- data %>%
mutate(
x_zone = cut(plate_x, breaks = seq(-1.5, 1.5, length.out = 8)),
z_zone = cut(plate_z, breaks = seq(1.0, 4.0, length.out = 8)),
hit = events %in% c("single", "double", "triple", "home_run"),
ab = events %in% c("single", "double", "triple", "home_run",
"field_out", "strikeout", "force_out")
)
zone_stats <- data %>%
filter(ab) %>%
group_by(x_zone, z_zone) %>%
summarise(
hits = sum(hit),
at_bats = n(),
avg = hits / at_bats,
.groups = "drop"
) %>%
filter(at_bats >= 5)
p <- ggplot(zone_stats, aes(x = x_zone, y = z_zone, fill = avg)) +
geom_tile(color = "white") +
geom_text(aes(label = sprintf("%.3f", avg)), color = "white", fontface = "bold") +
scale_fill_gradient2(low = "blue", mid = "yellow", high = "red",
midpoint = mean(zone_stats$avg), name = "AVG") +
labs(
title = paste(player_name, "- Batting Average by Zone"),
x = "Horizontal (Inside → Outside)",
y = "Vertical (Low → High)"
) +
theme_minimal() +
theme(
plot.title = element_text(face = "bold", hjust = 0.5),
axis.text = element_blank(),
panel.grid = element_blank()
)
ggsave("performance_heatmap.png", p, width = 12, height = 10, dpi = 300)
print(p)
}
# Example: Aaron Judge (592450)
# data <- get_pitch_data(592450, "2024-04-01", "2024-09-30")
# create_pitch_heatmap(data, "Aaron Judge")
Applications
Pitcher-Hitter Matchups
- Overlay pitcher heat zones with batter cold zones
- Identify optimal attack zones for specific matchups
- Develop count-specific strategies
Pitch Sequencing
| Count | Strategy | Zone Targeting |
|---|---|---|
| 0-0 | Establish tone | Pitcher's best command zones |
| 0-1, 0-2 | Expand zone | Chase areas outside zone |
| 2-2, 3-2 | Critical counts | Best putaway locations |
| 3-0, 3-1 | Batter's counts | Safe zones, avoid damage |
Key Takeaways
- Hot/cold zones reveal tendencies: Identify strengths and weaknesses
- Performance by zone: Shows where hitters succeed or struggle
- Swing rate analysis: Reveals discipline and chase tendencies
- Matchup planning: Target optimal zones for specific batters
- Count-specific: Different strategies for different situations
Discussion
Have questions or feedback? Join our community discussion on
Discord or
GitHub Discussions.
Table of Contents
Related Topics
Quick Actions