Matplotlib vs Seaborn: Complete Data Visualization Guide

Master Python data visualization with Matplotlib and Seaborn. Compare features, learn when to use each library, create publication-quality charts, and build custom visualizations with practical examples.

📅 Published: October 5, 2025 ✏️ Updated: October 18, 2025 By Ojaswi Athghara
#matplotlib #seaborn #dataviz #python #charts #plotting

Matplotlib vs Seaborn: Complete Data Visualization Guide

The Visualization Library That Changed Everything

I spent hours tweaking Matplotlib code to create a simple bar chart with nice colors. Lines and lines of customization. Then someone showed me Seaborn: sns.barplot(data=df, x='category', y='value'). One line. Beautiful output.

But here's what they didn't tell me: Sometimes you need Matplotlib's fine-grained control. The best data scientists know both libraries and when to use each.

This comprehensive guide compares Matplotlib and Seaborn, teaching you when to use which, how to combine them, and how to create publication-quality visualizations.

Quick Comparison

FeatureMatplotlibSeaborn
Ease of UseModerateEasy
CustomizationMaximumGood
Statistical PlotsManualBuilt-in
AestheticsBasicBeautiful
Learning CurveSteepGentle
Use CaseFull control neededQuick analysis

Understanding Matplotlib

Matplotlib is the foundation of Python visualization. It's verbose but powerful.

Installation and Setup

# pip install matplotlib

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd

# Set style (optional)
plt.style.use('seaborn-v0_8')  # Modern style

# For Jupyter notebooks
%matplotlib inline

Basic Matplotlib Plots

# Line plot
x = np.linspace(0, 10, 100)
y = np.sin(x)

plt.figure(figsize=(10, 6))
plt.plot(x, y, label='sin(x)', linewidth=2, color='blue')
plt.plot(x, np.cos(x), label='cos(x)', linewidth=2, color='red', linestyle='--')
plt.xlabel('X axis', fontsize=12)
plt.ylabel('Y axis', fontsize=12)
plt.title('Sine and Cosine Functions', fontsize=14, fontweight='bold')
plt.legend(fontsize=10)
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

Scatter Plot

# Generate data
np.random.seed(42)
x = np.random.randn(100)
y = 2 * x + np.random.randn(100)

plt.figure(figsize=(8, 6))
plt.scatter(x, y, alpha=0.6, s=50, c=y, cmap='viridis', edgecolors='black')
plt.colorbar(label='Y value')
plt.xlabel('X')
plt.ylabel('Y')
plt.title('Scatter Plot with Color Mapping')
plt.show()

Bar Plot

categories = ['A', 'B', 'C', 'D', 'E']
values = [23, 45, 56, 78, 32]

plt.figure(figsize=(10, 6))
bars = plt.bar(categories, values, color='skyblue', edgecolor='black')

# Add value labels on bars
for bar in bars:
    height = bar.get_height()
    plt.text(bar.get_x() + bar.get_width()/2., height,
             f'{int(height)}',
             ha='center', va='bottom')

plt.xlabel('Categories')
plt.ylabel('Values')
plt.title('Bar Chart Example')
plt.show()

Subplots

fig, axes = plt.subplots(2, 2, figsize=(12, 10))

# Plot 1: Line
axes[0, 0].plot(x, np.sin(x))
axes[0, 0].set_title('Sine Wave')

# Plot 2: Scatter
axes[0, 1].scatter(x, y, alpha=0.5)
axes[0, 1].set_title('Scatter Plot')

# Plot 3: Histogram
axes[1, 0].hist(y, bins=20, color='green', alpha=0.7)
axes[1, 0].set_title('Histogram')

# Plot 4: Box plot
axes[1, 1].boxplot([x, y], labels=['X', 'Y'])
axes[1, 1].set_title('Box Plot')

plt.tight_layout()
plt.show()

Understanding Seaborn

Seaborn is built on Matplotlib, offering high-level interface and better defaults.

Installation and Setup

# pip install seaborn

import seaborn as sns
import matplotlib.pyplot as plt

# Set Seaborn style
sns.set_theme(style="darkgrid")  # or "whitegrid", "dark", "white", "ticks"
sns.set_palette("husl")  # Color palette

Basic Seaborn Plots

# Create sample data
tips = sns.load_dataset('tips')

# Scatter plot with regression line
plt.figure(figsize=(10, 6))
sns.scatterplot(data=tips, x='total_bill', y='tip', hue='time', size='size')
plt.title('Tips vs Total Bill')
plt.show()

# With regression line
plt.figure(figsize=(10, 6))
sns.regplot(data=tips, x='total_bill', y='tip')
plt.title('Tips vs Total Bill (with regression)')
plt.show()

Bar Plot

plt.figure(figsize=(10, 6))
sns.barplot(data=tips, x='day', y='total_bill', hue='sex', ci='sd')
plt.title('Average Bill by Day and Gender')
plt.show()

Box Plot and Violin Plot

fig, axes = plt.subplots(1, 2, figsize=(14, 6))

# Box plot
sns.boxplot(data=tips, x='day', y='total_bill', hue='sex', ax=axes[0])
axes[0].set_title('Box Plot')

# Violin plot (shows distribution)
sns.violinplot(data=tips, x='day', y='total_bill', hue='sex', split=True, ax=axes[1])
axes[1].set_title('Violin Plot')

plt.tight_layout()
plt.show()

Head-to-Head Comparison

Creating the Same Plot: Both Libraries

# Sample data
df = pd.DataFrame({
    'category': ['A', 'B', 'C', 'D', 'E'],
    'values': [23, 45, 56, 78, 32]
})

fig, axes = plt.subplots(1, 2, figsize=(14, 6))

# Matplotlib version
axes[0].bar(df['category'], df['values'], color='steelblue', edgecolor='black')
axes[0].set_xlabel('Category', fontsize=12)
axes[0].set_ylabel('Values', fontsize=12)
axes[0].set_title('Matplotlib Bar Chart', fontsize=14)
axes[0].grid(axis='y', alpha=0.3)

# Seaborn version
sns.barplot(data=df, x='category', y='values', ax=axes[1], color='steelblue')
axes[1].set_title('Seaborn Bar Chart', fontsize=14)

plt.tight_layout()
plt.show()

Distribution Plots Comparison

# Generate data
data = np.random.normal(100, 15, 1000)

fig, axes = plt.subplots(2, 2, figsize=(14, 10))

# Matplotlib histogram
axes[0, 0].hist(data, bins=30, color='skyblue', edgecolor='black', alpha=0.7)
axes[0, 0].set_title('Matplotlib Histogram')
axes[0, 0].set_xlabel('Value')
axes[0, 0].set_ylabel('Frequency')

# Seaborn histogram
sns.histplot(data, bins=30, kde=True, ax=axes[0, 1])
axes[0, 1].set_title('Seaborn Histogram with KDE')

# Matplotlib KDE (manual)
from scipy import stats
density = stats.gaussian_kde(data)
x_range = np.linspace(data.min(), data.max(), 100)
axes[1, 0].plot(x_range, density(x_range), linewidth=2)
axes[1, 0].fill_between(x_range, density(x_range), alpha=0.3)
axes[1, 0].set_title('Matplotlib KDE (Manual)')

# Seaborn KDE
sns.kdeplot(data, ax=axes[1, 1], fill=True)
axes[1, 1].set_title('Seaborn KDE (Built-in)')

plt.tight_layout()
plt.show()

Advanced Seaborn Features

Pair Plot (Correlation Matrix)

# Load iris dataset
iris = sns.load_dataset('iris')

# Create pair plot
sns.pairplot(iris, hue='species', diag_kind='kde', height=2.5)
plt.suptitle('Iris Dataset Pair Plot', y=1.02)
plt.show()

Heatmap

# Correlation heatmap
plt.figure(figsize=(10, 8))
correlation = iris.select_dtypes(include=[np.number]).corr()

sns.heatmap(correlation, annot=True, cmap='coolwarm', center=0,
            square=True, linewidths=1, cbar_kws={"shrink": 0.8})
plt.title('Correlation Heatmap', fontsize=14)
plt.show()

Cat Plot (Categorical Plots)

# Flexible categorical plots
sns.catplot(data=tips, x='day', y='total_bill', hue='sex',
            kind='box', height=6, aspect=1.5)
plt.title('Total Bill by Day and Gender')
plt.show()

# Different kinds: 'strip', 'swarm', 'box', 'violin', 'boxen', 'point', 'bar', 'count'

FacetGrid (Small Multiples)

# Create grid of plots
g = sns.FacetGrid(tips, col='time', row='sex', height=4)
g.map(sns.scatterplot, 'total_bill', 'tip')
g.add_legend()
plt.show()

Combining Matplotlib and Seaborn

The best approach: Use Seaborn for quick plots, Matplotlib for customization.

# Use Seaborn for plotting, Matplotlib for customization
fig, ax = plt.subplots(figsize=(12, 6))

# Seaborn plot
sns.violinplot(data=tips, x='day', y='total_bill', hue='sex', split=True, ax=ax)

# Matplotlib customization
ax.set_title('Tips Analysis', fontsize=16, fontweight='bold', pad=20)
ax.set_xlabel('Day of Week', fontsize=12, fontweight='bold')
ax.set_ylabel('Total Bill ($)', fontsize=12, fontweight='bold')
ax.grid(axis='y', alpha=0.3, linestyle='--')

# Add custom annotation
ax.annotate('Weekend peak', xy=(2, 40), xytext=(1, 45),
            arrowprops=dict(arrowstyle='->', color='red', lw=2),
            fontsize=12, color='red', fontweight='bold')

plt.tight_layout()
plt.show()

Styling and Themes

Matplotlib Styles

# Available styles
print(plt.style.available)

# Apply style
plt.style.use('ggplot')  # or 'seaborn-v0_8', 'fivethirtyeight', etc.

# Create plot with style
plt.figure(figsize=(10, 6))
plt.plot(x, y)
plt.title('Styled Plot')
plt.show()

# Reset to default
plt.style.use('default')

Seaborn Contexts

# Set context (affects scale of elements)
sns.set_context("paper")  # or "notebook", "talk", "poster"

# Set palette
sns.set_palette("Set2")  # or "husl", "Paired", "deep", etc.

# Create plot
plt.figure(figsize=(10, 6))
sns.boxplot(data=tips, x='day', y='total_bill')
plt.title('Styled with Seaborn Context')
plt.show()

Custom Color Palettes

# Custom Seaborn palette
custom_palette = ["#FF6B6B", "#4ECDC4", "#45B7D1", "#FFA07A"]
sns.set_palette(custom_palette)

# Matplotlib custom colors
colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#FFA07A']
plt.bar(range(4), [1, 2, 3, 4], color=colors)
plt.show()

Creating Publication-Quality Figures

def create_publication_plot(data, output_file='figure.png'):
    """Create publication-ready plot"""
    
    # Set style
    sns.set_style("whitegrid")
    sns.set_context("paper", font_scale=1.5)
    
    # Create figure
    fig, ax = plt.subplots(figsize=(8, 6), dpi=300)
    
    # Plot
    sns.scatterplot(data=data, x='x', y='y', hue='category', 
                    s=100, alpha=0.7, ax=ax)
    
    # Styling
    ax.set_xlabel('X Variable', fontsize=14, fontweight='bold')
    ax.set_ylabel('Y Variable', fontsize=14, fontweight='bold')
    ax.set_title('Publication Title', fontsize=16, fontweight='bold', pad=20)
    
    # Legend
    ax.legend(title='Category', title_fontsize=12, fontsize=11,
              frameon=True, shadow=True)
    
    # Grid
    ax.grid(True, alpha=0.3, linestyle='--')
    
    # Tight layout
    plt.tight_layout()
    
    # Save (high DPI for publication)
    plt.savefig(output_file, dpi=300, bbox_inches='tight', 
                facecolor='white', edgecolor='none')
    plt.show()
    
    print(f"Figure saved to {output_file}")

# Create sample data
data = pd.DataFrame({
    'x': np.random.randn(100),
    'y': np.random.randn(100),
    'category': np.random.choice(['A', 'B', 'C'], 100)
})

create_publication_plot(data)

When to Use Which Library

Use Matplotlib When:

  1. Fine-grained control needed
    • Custom annotations
    • Complex layouts
    • Specific styling requirements
  2. Creating custom visualizations
    • New plot types
    • Scientific visualizations
    • Interactive plots
  3. 3D plots
    from mpl_toolkits.mplot3d import Axes3D
    
    fig = plt.figure()
    ax = fig.add_subplot(111, projection='3d')
    ax.scatter(x, y, z)
    

Use Seaborn When:

  1. Statistical visualization
    • Distribution plots
    • Regression plots
    • Categorical comparisons
  2. Quick exploratory analysis
    • Pair plots
    • Heatmaps
    • FacetGrids
  3. Beautiful defaults needed
    • Presentation plots
    • Dashboard visualizations
    • Reports

Complete Real-World Example

# Sales dashboard with both libraries

# Sample data
np.random.seed(42)
months = pd.date_range('2024-01', periods=12, freq='M')
sales_data = pd.DataFrame({
    'month': months,
    'product_A': np.random.randint(100, 200, 12),
    'product_B': np.random.randint(80, 150, 12),
    'product_C': np.random.randint(120, 180, 12),
    'region': np.random.choice(['North', 'South'], 12)
})

# Create dashboard
fig = plt.figure(figsize=(16, 10))
gs = fig.add_gridspec(3, 2, hspace=0.3, wspace=0.3)

# 1. Time series (Matplotlib)
ax1 = fig.add_subplot(gs[0, :])
ax1.plot(sales_data['month'], sales_data['product_A'], marker='o', label='Product A')
ax1.plot(sales_data['month'], sales_data['product_B'], marker='s', label='Product B')
ax1.plot(sales_data['month'], sales_data['product_C'], marker='^', label='Product C')
ax1.set_title('Monthly Sales Trend', fontsize=14, fontweight='bold')
ax1.set_xlabel('Month')
ax1.set_ylabel('Sales')
ax1.legend()
ax1.grid(True, alpha=0.3)

# 2. Bar chart (Seaborn)
ax2 = fig.add_subplot(gs[1, 0])
total_sales = sales_data[['product_A', 'product_B', 'product_C']].sum()
sns.barplot(x=total_sales.index, y=total_sales.values, ax=ax2, palette='Set2')
ax2.set_title('Total Sales by Product', fontweight='bold')
ax2.set_ylabel('Total Sales')

# 3. Distribution (Seaborn)
ax3 = fig.add_subplot(gs[1, 1])
sales_melted = sales_data.melt(id_vars=['month', 'region'], 
                                value_vars=['product_A', 'product_B', 'product_C'],
                                var_name='product', value_name='sales')
sns.boxplot(data=sales_melted, x='product', y='sales', ax=ax3, palette='Set3')
ax3.set_title('Sales Distribution', fontweight='bold')
ax3.set_xticklabels(['A', 'B', 'C'])

# 4. Heatmap (Seaborn)
ax4 = fig.add_subplot(gs[2, :])
correlation = sales_data[['product_A', 'product_B', 'product_C']].corr()
sns.heatmap(correlation, annot=True, cmap='coolwarm', center=0, ax=ax4, 
            square=True, linewidths=1)
ax4.set_title('Product Sales Correlation', fontweight='bold')

plt.suptitle('Sales Dashboard', fontsize=18, fontweight='bold', y=0.995)
plt.savefig('sales_dashboard.png', dpi=300, bbox_inches='tight')
plt.show()

Performance Considerations

When working with large datasets, performance matters.

Matplotlib Performance

# Rasterize complex plots for faster rendering
plt.plot(large_data_x, large_data_y, rasterized=True)
plt.savefig('plot.pdf', dpi=300)  # Vector with rasterized data

# Use Agg backend for server-side rendering
import matplotlib
matplotlib.use('Agg')  # Before importing pyplot
import matplotlib.pyplot as plt

Seaborn Performance

# Downsample for scatter plots
import numpy as np

# Sample large dataset
if len(data) > 10000:
    sample_size = 10000
    data_sample = data.sample(n=sample_size, random_state=42)
    sns.scatterplot(data=data_sample, x='x', y='y')
else:
    sns.scatterplot(data=data, x='x', y='y')

Common Mistakes and Solutions

Mistake 1: Not Closing Figures

# WRONG: Memory leak with many plots
for i in range(100):
    plt.figure()
    plt.plot(data[i])
    plt.savefig(f'plot_{i}.png')

# RIGHT: Close figures
for i in range(100):
    fig, ax = plt.subplots()
    ax.plot(data[i])
    plt.savefig(f'plot_{i}.png')
    plt.close(fig)  # Free memory

Mistake 2: Modifying Seaborn Plots Incorrectly

# WRONG: Direct modification
sns.lineplot(data=df, x='x', y='y')
plt.title('My Plot')  # Works but not recommended

# RIGHT: Get axes object
ax = sns.lineplot(data=df, x='x', y='y')
ax.set_title('My Plot')
ax.set_xlabel('X Label')

Mistake 3: Inconsistent Styling

# WRONG: Mixing styles
sns.set_style('darkgrid')
plt.plot(x, y)
plt.style.use('seaborn-v0_8-bright')  # Conflict!

# RIGHT: Choose one approach
# Option 1: Seaborn style
sns.set_theme(style='darkgrid', palette='muted')
sns.lineplot(x=x, y=y)

# Option 2: Matplotlib style
with plt.style.context('seaborn-v0_8-darkgrid'):
    plt.plot(x, y)

Pro Tips for Better Visualizations

  1. Save Configuration: Create a custom style file for consistent branding
  2. Use Context Managers: Keep style changes local with with statements
  3. Label Everything: Always add titles, axis labels, and legends
  4. Choose Appropriate DPI: 72 for screen, 300+ for print
  5. Test Colorblind-Friendly Palettes: Use sns.color_palette("colorblind")

Your Visualization Toolkit

You now understand:

  1. Matplotlib - Maximum control, verbose
  2. Seaborn - Beautiful defaults, statistical focus
  3. When to use which - Context-dependent
  4. Combining both - Best of both worlds
  5. Styling - Professional appearance
  6. Publication quality - High-DPI outputs

Master both libraries, and you'll handle any visualization need!

When I Use Each Library

My Daily Matplotlib Uses:

  • Quick exploratory plots during data analysis sessions
  • Custom publication figures with specific size and DPI requirements
  • Dashboards with multiple subplot layouts and precise positioning
  • Annotations and arrows for explanatory diagrams

My Daily Seaborn Uses:

  • Statistical distributions and relationships (violinplots, boxplots, pairplots)
  • Categorical comparisons with automatic confidence intervals
  • Heatmaps for correlation matrices and confusion matrices
  • Quick beautiful plots for presentations and reports

When I Combine Both:

  • Start with Seaborn for statistical plots and beautiful defaults
  • Add Matplotlib customizations for annotations, layout adjustments, and fine-tuning
  • Use Matplotlib's figure-level control with Seaborn's axes-level plotting
  • Leverage Seaborn themes but customize with Matplotlib's detailed control

The key insight: they're not competing tools, they're complementary. Seaborn builds on Matplotlib, so learning both gives you the best of both worlds—statistical power with full customization control.

Next Steps

Remember: The best visualization is the one that clearly communicates your insight!


Found this comparison helpful? Share with your data viz community! Connect with me on Twitter or LinkedIn for more visualization tips.

Support My Work

If this guide helped you choose between Matplotlib and Seaborn, create beautiful visualizations, or understand when to use each library, I'd greatly appreciate your support, I'd really appreciate your support! Creating comprehensive, free content like this takes significant time and effort. Your support helps me continue sharing knowledge and creating more helpful resources for developers.

☕ Buy me a coffee - Every contribution, big or small, means the world to me and keeps me motivated to create more content!


Cover image by Jakub Żerdzicki on Unsplash

Related Blogs

Ojaswi Athghara

SDE, 4+ Years

© ojaswiat.com 2025-2027