Matplotlib and Seaborn
🎯 Goal
After reading this chapter:
- With Matplotlib you’ll be able to control your charts at the
figure,axeslevel - With Seaborn you’ll create statistical charts in 1-2 lines
- You know all the basic chart types needed in ML projects
- You can prepare beautiful visualizations for EDA (Exploratory Data Analysis) reports
What to learn
Matplotlib
FigureandAxesarchitecturepyplotinterface (simple) vs Object-oriented API (control)- Main chart types:
plot,scatter,bar,hist,boxplot - Subplots:
subplots(),GridSpec - Customization: title, labels, legend, ticks, colors
- Saving:
savefig(PNG, SVG, PDF)
Seaborn
- Themes and styling (
set_theme,set_palette) - Categorical plots:
countplot,barplot,boxplot,violinplot - Distribution plots:
histplot,kdeplot,displot - Relationship plots:
scatterplot,lineplot,regplot - Matrix plots:
heatmap,clustermap - Multi-plot grids:
FacetGrid,PairGrid,pairplot
Libraries
pip install matplotlib seaborn
Plotly alternative (for interactive charts):
pip install plotly
Important topics
Matplotlib architecture
Each plot in Matplotlib consists of 3 layers:
- Figure — the entire “canvas” (image file)
- Axes — a single chart area (subplot)
- Plot elements — line, point, bar, label, etc.
import matplotlib.pyplot as plt
# Two interfaces exist:
# 1. Pyplot API (simple, but global state)
plt.plot([1, 2, 3], [4, 5, 6])
plt.title("Quick")
plt.show()
# 2. Object-oriented API (RECOMMENDED — for larger projects)
fig, ax = plt.subplots(figsize=(8, 5))
ax.plot([1, 2, 3], [4, 5, 6])
ax.set_title("Better")
ax.set_xlabel("X")
ax.set_ylabel("Y")
fig.savefig("plot.png", dpi=150, bbox_inches="tight")
When Matplotlib, when Seaborn?
- Matplotlib — when full control is needed, custom layout
- Seaborn — statistical charts, direct DataFrame work, “good-looking defaults”
In real work, usually both together:
fig, ax = plt.subplots(figsize=(10, 6))
sns.heatmap(corr_matrix, annot=True, ax=ax)
ax.set_title("My Correlation Matrix")
Code examples
Main chart types
import numpy as np
import matplotlib.pyplot as plt
x = np.linspace(0, 10, 100)
y1 = np.sin(x)
y2 = np.cos(x)
fig, ax = plt.subplots(figsize=(10, 5))
ax.plot(x, y1, label="sin(x)", color="blue", linewidth=2)
ax.plot(x, y2, label="cos(x)", color="red", linestyle="--")
ax.set_title("Trigonometric Functions")
ax.set_xlabel("x")
ax.set_ylabel("y")
ax.legend()
ax.grid(True, alpha=0.3)
plt.show()
Subplots
fig, axes = plt.subplots(2, 2, figsize=(12, 8))
# Histogram
data = np.random.normal(0, 1, 1000)
axes[0, 0].hist(data, bins=30, color="steelblue", edgecolor="black")
axes[0, 0].set_title("Histogram")
# Scatter
x = np.random.rand(100)
y = x + np.random.normal(0, 0.1, 100)
axes[0, 1].scatter(x, y, alpha=0.6)
axes[0, 1].set_title("Scatter")
# Bar
categories = ["A", "B", "C", "D"]
values = [23, 45, 56, 78]
axes[1, 0].bar(categories, values, color=["red", "green", "blue", "orange"])
axes[1, 0].set_title("Bar")
# Box plot
data_groups = [np.random.normal(i, 1, 100) for i in range(3)]
axes[1, 1].boxplot(data_groups, labels=["Group 1", "Group 2", "Group 3"])
axes[1, 1].set_title("Box Plot")
plt.tight_layout()
plt.show()
Statistical charts in Seaborn
import seaborn as sns
import pandas as pd
# Load Titanic dataset
df = sns.load_dataset("titanic")
# Set theme
sns.set_theme(style="whitegrid", palette="muted")
# Categorical plot
fig, ax = plt.subplots(figsize=(8, 5))
sns.countplot(data=df, x="class", hue="survived", ax=ax)
ax.set_title("Survival by Class")
plt.show()
# Distribution
sns.histplot(data=df, x="age", hue="survived", multiple="stack", bins=30)
plt.title("Age distribution by survival")
plt.show()
# Pairplot — relationships among all features
sns.pairplot(df[["age", "fare", "pclass", "survived"]].dropna(), hue="survived")
plt.show()
# Heatmap — correlation matrix
numeric_df = df.select_dtypes(include="number")
corr = numeric_df.corr()
fig, ax = plt.subplots(figsize=(10, 8))
sns.heatmap(corr, annot=True, cmap="coolwarm", center=0, fmt=".2f", ax=ax)
ax.set_title("Correlation Matrix")
plt.show()
Production-ready style
# Custom theme
plt.style.use("seaborn-v0_8-darkgrid") # or "ggplot", "fivethirtyeight"
# Or fully custom
plt.rcParams.update({
"font.size": 11,
"axes.titlesize": 14,
"axes.titleweight": "bold",
"figure.dpi": 100,
"savefig.dpi": 200,
"savefig.bbox": "tight",
})
Backend integration
1. Chart endpoint in FastAPI (return PNG)
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
import matplotlib
matplotlib.use("Agg") # IMPORTANT: no GUI for backend
import matplotlib.pyplot as plt
import io
app = FastAPI()
@app.get("/chart/sales.png")
async def sales_chart():
df = pd.read_sql("SELECT date, sales FROM daily_sales ORDER BY date", engine)
fig, ax = plt.subplots(figsize=(12, 5))
ax.plot(df["date"], df["sales"], color="navy", linewidth=2)
ax.fill_between(df["date"], df["sales"], alpha=0.3, color="navy")
ax.set_title("Daily Sales")
ax.set_xlabel("Date")
ax.set_ylabel("Sales (USD)")
buf = io.BytesIO()
fig.savefig(buf, format="png", dpi=150, bbox_inches="tight")
plt.close(fig) # IMPORTANT: prevent memory leak
buf.seek(0)
return StreamingResponse(buf, media_type="image/png")
2. Background report generation
@celery_app.task
def generate_monthly_report(month: str):
df = load_data(month)
fig, axes = plt.subplots(2, 2, figsize=(15, 10))
# Revenue trend
df.set_index("date")["revenue"].plot(ax=axes[0, 0], title="Revenue")
# Top categories
df.groupby("category")["revenue"].sum().nlargest(10).plot.barh(ax=axes[0, 1])
# User growth
df.groupby("date")["new_users"].sum().plot(ax=axes[1, 0])
# Correlation
sns.heatmap(df.corr(), ax=axes[1, 1], annot=True, fmt=".2f")
plt.tight_layout()
fig.savefig(f"/reports/{month}.pdf", format="pdf")
plt.close(fig)
send_email_with_attachment(f"/reports/{month}.pdf")
Important note for server-side rendering
When using matplotlib in backend:
- Do
matplotlib.use("Agg")— to avoid loading GUI backend - **Call
plt.close(fig)**— to prevent memory leak - Thread safety — matplotlib is not thread-safe. You can use Gunicorn workers, but in async context render in a separate thread (
asyncio.to_thread)
Resources
- Matplotlib official tutorials — matplotlib.org/stable/tutorials/
- Seaborn gallery — seaborn.pydata.org/examples/
- “Python Data Science Handbook” — Jake VanderPlas (free online: jakevdp.github.io)
- “Storytelling with Data” — Cole Nussbaumer Knaflic (chart design)
- Plotly Express — for interactive charts: plotly.com/python/plotly-express/
🏋️ Exercises
🟢 Easy
- Create 1000 random numbers with NumPy and plot their histogram (matplotlib).
- In Seaborn, load
irisdataset and dopairplot. - Create a 2x2 subplot with a different chart type in each.
🟡 Medium
- Plot Titanic dataset’s correlation matrix as a heatmap with
annot=Trueand custom colormap. - Create a custom theme: fonts, colors, grid style — save it in a
mlflow_style.pymodule and import into other projects. - Create a chart with 2 y-axes in one
Figure(twinx) — e.g., daily users and daily revenue on the same x-axis.
🔴 Hard
- FastAPI Dashboard: create the endpoint
/api/charts/{chart_type}.png. User sends query parameterschart_type=line|bar|hist|scatter,data_source=...,title=...and receives a nice PNG. Add caching (with Redis). - PDF report: create a 10-page multi-page PDF report (using matplotlib
PdfPages): cover page, analytics per section, final summary.
Capstone
notebooks/month-01/03_visualization.ipynb:
- Load COVID-19 or any public time-series dataset
- Create an EDA report with 6 different chart types (line, bar, hist, box, scatter, heatmap)
- All in one
Figure, layout usingGridSpec - Save in PDF format
✅ Checklist
- I know the difference between
pyplotAPI and OO API - I understand the relationship of
FigureandAxes - I can create subplots and manage layout
- I know how to use heatmap, pairplot, distplot in Seaborn
- I can create a custom style/theme
- I know I use
Aggandplt.closewhen using matplotlib in backend - I can save a chart in PNG, SVG, PDF formats
EDA Capstone project — now on to the real work.