Data Visualization in Python with Matplotlib and Seaborn
Matplotlib is Python's most widely used 2D rendering engine; Seaborn extends it with high-level statistical charts. Together they cover everything from quick exploratory plots to publication-ready figures.
Installation
pip install matplotlib seaborn
import matplotlib.pyplot as plt
import matplotlib as mpl
import seaborn as sns
import numpy as np
import pandas as pd
Matplotlib architecture: Figure and Axes
# Recommended: create Figure and Axes explicitly
fig, ax = plt.subplots(figsize=(8, 5))
x = np.linspace(0, 2 * np.pi, 300)
ax.plot(x, np.sin(x), label='sin(x)', color='steelblue', linewidth=2)
ax.plot(x, np.cos(x), label='cos(x)', color='tomato', linestyle='--')
ax.set_title('Trigonometric functions', fontsize=14)
ax.set_xlabel('x (radians)')
ax.set_ylabel('Amplitude')
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('trig.png', dpi=150)
plt.show()
Essential chart types
Line chart with shaded area
fig, ax = plt.subplots(figsize=(9, 4))
np.random.seed(42)
days = pd.date_range('2024-01-01', periods=90)
sales = 500 + np.cumsum(np.random.randn(90) * 20)
rolling = pd.Series(sales).rolling(7).mean()
ax.plot(days, sales, color='lightsteelblue', alpha=0.6, label='Daily sales')
ax.plot(days, rolling, color='steelblue', linewidth=2, label='7-day MA')
ax.fill_between(days, sales, rolling, alpha=0.15, color='steelblue')
ax.set_title('Q1 2024 Sales')
ax.legend()
ax.xaxis.set_major_locator(mpl.dates.WeekdayLocator(byweekday=0))
ax.xaxis.set_major_formatter(mpl.dates.DateFormatter('%b %d'))
fig.autofmt_xdate()
plt.tight_layout()
Grouped bar chart
categories = ['Engineering', 'Marketing', 'Sales', 'HR']
q1 = [420, 310, 580, 210]
q2 = [460, 285, 620, 230]
x = np.arange(len(categories))
width = 0.35
fig, ax = plt.subplots(figsize=(8, 5))
bars1 = ax.bar(x - width/2, q1, width, label='Q1', color='steelblue')
bars2 = ax.bar(x + width/2, q2, width, label='Q2', color='tomato')
ax.set_xticks(x)
ax.set_xticklabels(categories)
ax.set_ylabel('Thousands USD')
ax.set_title('Budget by department')
ax.legend()
ax.bar_label(bars1, fmt='%d', padding=3, fontsize=9)
ax.bar_label(bars2, fmt='%d', padding=3, fontsize=9)
plt.tight_layout()
Scatter plot with trend line
rng = np.random.default_rng(0)
n = 100
x = rng.uniform(0, 10, n)
y = 2.5 * x + rng.normal(0, 3, n)
fig, ax = plt.subplots(figsize=(7, 5))
ax.scatter(x, y, alpha=0.6, edgecolors='white', linewidths=0.5,
color='steelblue', s=60)
m, b = np.polyfit(x, y, 1)
xr = np.array([x.min(), x.max()])
ax.plot(xr, m*xr + b, 'r--', linewidth=1.5, label=f'y = {m:.2f}x + {b:.2f}')
ax.set_title('Price–Sales Correlation')
ax.legend()
plt.tight_layout()
Histogram and box plot
rng = np.random.default_rng(1)
group_a = rng.normal(70, 10, 500)
group_b = rng.normal(80, 15, 500)
fig, axes = plt.subplots(1, 2, figsize=(11, 4))
axes[0].hist(group_a, bins=30, alpha=0.6, color='steelblue', label='Group A', density=True)
axes[0].hist(group_b, bins=30, alpha=0.6, color='tomato', label='Group B', density=True)
axes[0].set_title('Score distribution')
axes[0].legend()
axes[1].boxplot([group_a, group_b], labels=['Group A', 'Group B'],
patch_artist=True,
boxprops=dict(facecolor='lightsteelblue'))
axes[1].set_title('Group comparison')
plt.tight_layout()
Advanced layouts with GridSpec
fig = plt.figure(figsize=(12, 8))
gs = fig.add_gridspec(2, 3, hspace=0.4, wspace=0.35)
ax_main = fig.add_subplot(gs[0, :2]) # spans 2 columns
ax_side = fig.add_subplot(gs[0, 2])
ax_bot1 = fig.add_subplot(gs[1, 0])
ax_bot2 = fig.add_subplot(gs[1, 1])
ax_bot3 = fig.add_subplot(gs[1, 2])
x = np.linspace(0, 4, 100)
ax_main.plot(x, np.sin(x) * np.exp(-x/3))
ax_main.set_title('Damped oscillation')
for ax in [ax_side, ax_bot1, ax_bot2, ax_bot3]:
ax.plot(np.random.randn(50).cumsum())
fig.suptitle('Analysis Dashboard', fontsize=15, y=1.01)
plt.tight_layout()
Seaborn: statistical visualization
tips = sns.load_dataset('tips') # built-in DataFrame
sns.set_theme(style='whitegrid', palette='muted', font_scale=1.1)
Distributions
fig, axes = plt.subplots(1, 3, figsize=(14, 4))
sns.histplot(tips['total_bill'], kde=True, ax=axes[0])
axes[0].set_title('Bill distribution')
sns.boxplot(data=tips, x='day', y='total_bill', hue='sex', ax=axes[1])
axes[1].set_title('Bills by day and sex')
sns.violinplot(data=tips, x='day', y='tip', inner='box', ax=axes[2])
axes[2].set_title('Tips by day')
plt.tight_layout()
Regression and scatter
fig, axes = plt.subplots(1, 2, figsize=(12, 5))
sns.regplot(data=tips, x='total_bill', y='tip', ax=axes[0],
scatter_kws={'alpha': 0.4}, line_kws={'color': 'red'})
axes[0].set_title('Tip vs Total bill')
sns.scatterplot(data=tips, x='total_bill', y='tip',
hue='smoker', size='size', ax=axes[1], alpha=0.7)
axes[1].set_title('Multivariate scatter')
plt.tight_layout()
Correlation heatmap
corr = tips[['total_bill', 'tip', 'size']].corr()
fig, ax = plt.subplots(figsize=(6, 5))
sns.heatmap(corr, annot=True, fmt='.2f', cmap='coolwarm',
vmin=-1, vmax=1, linewidths=0.5, ax=ax)
ax.set_title('Variable correlation matrix')
plt.tight_layout()
FacetGrid — automatic multi-panel plots
g = sns.FacetGrid(tips, col='time', row='sex', height=3.5, aspect=1.2)
g.map_dataframe(sns.scatterplot, x='total_bill', y='tip', alpha=0.5)
g.set_axis_labels('Total bill', 'Tip')
g.add_legend()
g.figure.suptitle('Tips by time and sex', y=1.02)
Color palettes and accessibility
# Categorical
sns.color_palette('tab10') # 10 distinct colors
sns.color_palette('Set2') # muted, good for print
sns.color_palette('colorblind') # accessible for color-blind viewers
# Sequential / diverging
sns.color_palette('viridis', as_cmap=True) # perceptually uniform
sns.color_palette('RdBu_r', as_cmap=True) # diverging
sns.palplot(sns.color_palette('colorblind'))
plt.show()
Exporting publication-quality figures
fig, ax = plt.subplots(figsize=(8, 5))
ax.plot(np.sin(np.linspace(0, 10, 300)))
# PNG for web (150-200 dpi)
fig.savefig('figure_web.png', dpi=150, bbox_inches='tight')
# Vector PDF for scientific papers
fig.savefig('figure_paper.pdf', bbox_inches='tight')
# SVG for Inkscape / Illustrator editing
fig.savefig('figure_edit.svg', bbox_inches='tight')
# Global rcParams for print documents
mpl.rcParams.update({
'font.family': 'serif',
'font.size': 11,
'axes.titlesize': 13,
'figure.dpi': 150,
})
Best practices
- Always use
fig, ax = plt.subplots()instead of the statefulplt.plot()API — easier to reuse, compose, and avoid side effects in notebooks. - Call
tight_layout()or passconstrained_layout=Truetoplt.subplots()to prevent label overlap automatically. - Use Seaborn for quick exploration; Matplotlib for fine control — generate the plot with Seaborn, then tweak axis labels, ticks, and annotations on the returned
ax. - Choose accessible color palettes: Seaborn's
'colorblind'or'viridis'/'plasma'(uniform in color and grayscale). - Export at high resolution (
dpi=300) or as vector (PDF/SVG) for print;dpi=96-150is enough for web.
Related conversions
Frequent conversions across the catalogue: