How to Create a Heatmap in Matplotlib

Heatmaps transform 2D data into colored grids where color intensity represents magnitude. They excel at revealing patterns in correlation matrices, time-series data across categories, and geographic...

Key Insights

  • imshow() works for simple heatmaps, but pcolormesh() offers better control over cell boundaries and irregular grids—use it when you need precise axis alignment or non-uniform data spacing
  • Always use perceptually uniform colormaps like ‘viridis’ or diverging colormaps like ‘RdBu’ for correlation matrices—avoid ‘jet’ which creates false visual boundaries
  • Seaborn’s heatmap() function handles annotations, formatting, and clustering automatically, saving 20+ lines of Matplotlib code for production-ready visualizations

Introduction to Heatmaps

Heatmaps transform 2D data into colored grids where color intensity represents magnitude. They excel at revealing patterns in correlation matrices, time-series data across categories, and geographic distributions. Instead of parsing hundreds of numbers, viewers instantly identify clusters, outliers, and trends through color gradients.

Use heatmaps when you have data organized in a matrix format—rows and columns with numeric values at each intersection. Common applications include feature correlations in machine learning, website click patterns across time periods, and gene expression levels across samples in bioinformatics.

Matplotlib provides multiple approaches for creating heatmaps, each with distinct advantages. This article covers the spectrum from basic implementations to production-ready visualizations.

Basic Heatmap with imshow()

The imshow() function treats 2D arrays as images, making it the fastest way to create a heatmap. It’s ideal for uniformly spaced data where you don’t need precise control over cell boundaries.

import numpy as np
import matplotlib.pyplot as plt

# Generate sample data
data = np.random.rand(10, 12)

# Create heatmap
fig, ax = plt.subplots(figsize=(10, 8))
im = ax.imshow(data, cmap='viridis', aspect='auto')

# Add colorbar
cbar = plt.colorbar(im, ax=ax)
cbar.set_label('Value', rotation=270, labelpad=20)

# Configure axes
ax.set_xlabel('Column Index')
ax.set_ylabel('Row Index')
ax.set_title('Basic Heatmap with imshow()')

plt.tight_layout()
plt.show()

The aspect='auto' parameter prevents distortion when your data isn’t square. The ‘viridis’ colormap is perceptually uniform—equal data differences appear as equal color differences, avoiding the misleading gradients of older colormaps like ‘jet’.

Enhanced Heatmap with pcolormesh()

When you need labeled axes or irregular grid spacing, pcolormesh() provides superior control. It explicitly defines cell boundaries, ensuring your axis labels align correctly with data cells.

import numpy as np
import matplotlib.pyplot as plt

# Sales data: products (rows) by months (columns)
products = ['Widget A', 'Widget B', 'Widget C', 'Widget D', 'Widget E']
months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun']
sales = np.array([
    [120, 135, 145, 160, 155, 170],
    [90, 85, 95, 100, 110, 105],
    [200, 210, 205, 220, 230, 240],
    [150, 145, 160, 165, 170, 180],
    [75, 80, 85, 90, 95, 100]
])

fig, ax = plt.subplots(figsize=(10, 6))

# Create mesh coordinates
x = np.arange(len(months) + 1)
y = np.arange(len(products) + 1)

# Plot heatmap
mesh = ax.pcolormesh(x, y, sales, cmap='YlOrRd', edgecolors='white', linewidth=1)

# Set tick positions and labels
ax.set_xticks(np.arange(len(months)) + 0.5)
ax.set_yticks(np.arange(len(products)) + 0.5)
ax.set_xticklabels(months)
ax.set_yticklabels(products)

# Add colorbar
cbar = plt.colorbar(mesh, ax=ax)
cbar.set_label('Sales Units', rotation=270, labelpad=20)

ax.set_title('Product Sales by Month')
plt.tight_layout()
plt.show()

Notice the + 0.5 offset for tick positions—this centers labels within cells. The edgecolors='white' parameter adds gridlines, improving readability for dense data.

Correlation Matrix Heatmap

Correlation matrices are heatmap staples in data science. They reveal relationships between features, guiding feature selection and identifying multicollinearity.

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.datasets import load_iris

# Load sample data
iris = load_iris()
df = pd.DataFrame(iris.data, columns=iris.feature_names)

# Compute correlation matrix
corr_matrix = df.corr()

fig, ax = plt.subplots(figsize=(10, 8))

# Create heatmap with diverging colormap
im = ax.imshow(corr_matrix, cmap='RdBu', vmin=-1, vmax=1, aspect='auto')

# Set ticks and labels
ax.set_xticks(np.arange(len(corr_matrix.columns)))
ax.set_yticks(np.arange(len(corr_matrix.columns)))
ax.set_xticklabels(corr_matrix.columns, rotation=45, ha='right')
ax.set_yticklabels(corr_matrix.columns)

# Add correlation values as text annotations
for i in range(len(corr_matrix)):
    for j in range(len(corr_matrix)):
        text = ax.text(j, i, f'{corr_matrix.iloc[i, j]:.2f}',
                      ha='center', va='center', color='black', fontsize=10)

# Colorbar
cbar = plt.colorbar(im, ax=ax)
cbar.set_label('Correlation Coefficient', rotation=270, labelpad=20)

ax.set_title('Feature Correlation Matrix - Iris Dataset')
plt.tight_layout()
plt.show()

The ‘RdBu’ (red-blue) diverging colormap makes positive and negative correlations immediately distinguishable. Setting vmin=-1 and vmax=1 ensures the colormap spans the full correlation range, with neutral white at zero.

Customization Options

Professional heatmaps require careful styling. Font sizes, color schemes, and annotations transform raw data into communicative visualizations.

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.colors import TwoSlopeNorm

# Sample data with positive and negative values
data = np.random.randn(8, 10) * 50 + 100

fig, ax = plt.subplots(figsize=(12, 8))

# Use TwoSlopeNorm to center colormap at specific value
norm = TwoSlopeNorm(vmin=data.min(), vcenter=100, vmax=data.max())
im = ax.imshow(data, cmap='RdYlGn', norm=norm, aspect='auto')

# Add value annotations with conditional text color
for i in range(data.shape[0]):
    for j in range(data.shape[1]):
        value = data[i, j]
        # Use white text on dark backgrounds, black on light
        text_color = 'white' if value < 80 or value > 120 else 'black'
        ax.text(j, i, f'{value:.0f}', ha='center', va='center', 
                color=text_color, fontsize=9, weight='bold')

# Customize axes
ax.set_xlabel('Time Period', fontsize=12, weight='bold')
ax.set_ylabel('Metric Category', fontsize=12, weight='bold')
ax.set_title('Performance Metrics Dashboard', fontsize=14, weight='bold', pad=20)

# Rotate x-axis labels
plt.setp(ax.get_xticklabels(), rotation=45, ha='right', rotation_mode='anchor')

# Colorbar with custom label
cbar = plt.colorbar(im, ax=ax, fraction=0.046, pad=0.04)
cbar.set_label('Value (centered at 100)', rotation=270, labelpad=25, fontsize=11)

plt.tight_layout()
plt.show()

The TwoSlopeNorm centers the colormap at a meaningful value (100 in this case), making deviations visually symmetric. Conditional text coloring ensures readability across the entire value range.

Advanced: Seaborn Integration

Seaborn builds on Matplotlib with opinionated defaults and simplified APIs. For complex heatmaps, it eliminates boilerplate code.

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.datasets import load_iris

# Load and prepare data
iris = load_iris()
df = pd.DataFrame(iris.data, columns=iris.feature_names)
corr_matrix = df.corr()

# Create side-by-side comparison
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(16, 6))

# Matplotlib approach (simplified from earlier)
im1 = ax1.imshow(corr_matrix, cmap='coolwarm', vmin=-1, vmax=1)
ax1.set_title('Matplotlib Heatmap', fontsize=14, weight='bold')
plt.colorbar(im1, ax=ax1)

# Seaborn approach with clustering
sns.heatmap(corr_matrix, annot=True, fmt='.2f', cmap='coolwarm', 
            center=0, square=True, linewidths=1, cbar_kws={'label': 'Correlation'},
            ax=ax2)
ax2.set_title('Seaborn Heatmap', fontsize=14, weight='bold')

plt.tight_layout()
plt.show()

Seaborn’s heatmap() automatically handles annotations (annot=True), formats numbers (fmt='.2f'), centers the colormap (center=0), and adds gridlines (linewidths=1). The square=True parameter ensures cells are square, improving visual comparison.

For hierarchical clustering:

# Cluster both rows and columns
sns.clustermap(corr_matrix, annot=True, fmt='.2f', cmap='coolwarm',
               center=0, linewidths=1, figsize=(10, 10),
               cbar_kws={'label': 'Correlation Coefficient'})
plt.show()

The clustermap() function adds dendrograms and reorders rows/columns to group similar features—invaluable for exploratory data analysis.

Best Practices and Common Pitfalls

Choose appropriate colormaps: Use sequential colormaps (‘viridis’, ‘plasma’) for data with a natural ordering. Use diverging colormaps (‘RdBu’, ‘coolwarm’) for data centered around a meaningful value. Never use ‘jet’—it creates artificial boundaries and misleads viewers.

Handle missing data explicitly: Mask missing values or use a distinct color. Don’t let NaN values appear as zeros or interpolate across gaps.

import numpy as np
import matplotlib.pyplot as plt

data = np.random.rand(10, 10)
data[2:4, 5:7] = np.nan  # Introduce missing data

masked_data = np.ma.masked_invalid(data)
plt.imshow(masked_data, cmap='viridis')
plt.colorbar()
plt.show()

Optimize for readability: Limit heatmap dimensions to 20x20 for annotated versions. Larger matrices lose detail—consider clustering or filtering to the most important features.

Match colormap to data distribution: For skewed data, consider log scaling or quantile normalization before visualization. Otherwise, outliers dominate the color range and compress the remaining data into indistinguishable shades.

Label meaningfully: Replace generic “Row 0, Row 1” with actual category names. Your audience shouldn’t need a codebook to interpret the visualization.

Heatmaps are powerful when used correctly. Master these techniques, choose appropriate colormaps, and your data patterns will speak for themselves.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.