Choosing a plotting library shapes your entire analysis workflow. A wrong pick means rewriting code when you move from exploration to a production dashboard. This guide benchmarks Matplotlib, Seaborn, Plotly, Altair, Bokeh, and plotnine on API design, interactivity, scalability, and ecosystem fit, with runnable code for each.
Quick Takeaways
- Matplotlib is the foundation. It offers maximum control, and most other libraries build on it or imitate it.
- Seaborn produces statistical plots in one line of code. Use it for exploratory data analysis (EDA) on DataFrames.
- Plotly and Bokeh deliver browser-based interactivity. Pick them for dashboards and shareable reports.
- Altair uses a declarative grammar of graphics. It excels at linked views and compact code, but struggles with large datasets.
| Library | Paradigm | Interactivity | Best For | Learning Curve |
|---|---|---|---|---|
| Matplotlib | Imperative | Limited (static default) | Publication figures, custom plots | Steep |
| Seaborn | Declarative-ish, high-level | Static | Statistical EDA | Gentle |
| Plotly | Object/Express API | Rich, built-in | Dashboards, web apps | Moderate |
| Altair | Declarative (Vega-Lite) | Strong selections | Linked, exploratory views | Moderate |
| Bokeh | Imperative + server | Rich, streaming | Real-time apps | Moderate to steep |
| plotnine | Grammar of graphics (ggplot2) | Static | R users migrating to Python | Moderate |
How to Evaluate a Visualization Library
Compare libraries on five axes before you commit:
- Expressiveness: Can you build the chart type you need without hacks?
- Interactivity: Do you need hover, zoom, and cross-filtering, or only static images?
- Scalability: How many rows can it render before the browser or kernel stalls?
- Output targets: PNG/PDF for papers, HTML for web, or embedding in apps like Dash and Streamlit?
- Integration: Does it accept pandas, Polars, and NumPy inputs natively?
Setup and Sample Dataset
Install everything in one environment:
pip install matplotlib seaborn plotly altair bokeh plotnine pandas numpy
Every example below uses the same synthetic dataset, so output differences come from the library, not the data.
import numpy as np
import pandas as pd
rng = np.random.default_rng(42) # reproducible random generator
n = 500
df = pd.DataFrame({
"tenure_months": rng.integers(1, 72, n),
"monthly_spend": rng.normal(65, 20, n).clip(10),
"segment": rng.choice(["Basic", "Plus", "Premium"], n, p=[0.5, 0.3, 0.2]),
})
# Churn probability falls with tenure, rises with spend
logit = -0.04 * df["tenure_months"] + 0.02 * df["monthly_spend"]
df["churned"] = (rng.random(n) < 1 / (1 + np.exp(-logit))).astype(int)
Matplotlib: The Foundation
Matplotlib gives you full control over every artist on the canvas: axes, ticks, spines, and annotations. Its object-oriented interface (fig, ax = plt.subplots()) is the production standard. Avoid the stateful plt.plot() style in anything beyond quick scripts.
Strengths
- Pixel-level control for publication-quality figures
- Exports to PNG, SVG, PDF, and PGF (LaTeX)
- Largest ecosystem and documentation base
Weaknesses
- Verbose code for common statistical plots
- Default styling needs tuning
- Interactivity requires extra backends
import matplotlib.pyplot as plt
fig, ax = plt.subplots(figsize=(8, 5), dpi=120) # explicit figure and axes
for seg, grp in df.groupby("segment"):
ax.scatter(grp["tenure_months"], grp["monthly_spend"],
label=seg, alpha=0.6, s=25) # alpha handles overplotting
ax.set_xlabel("Tenure (months)")
ax.set_ylabel("Monthly spend ($)")
ax.set_title("Spend vs. Tenure by Segment")
ax.legend(title="Segment")
ax.spines[["top", "right"]].set_visible(False) # cleaner look
fig.tight_layout()
fig.savefig("scatter_matplotlib.png")
Seaborn: Statistical Plots in One Line
Seaborn sits on top of Matplotlib and adds statistical awareness. It aggregates, bins, and estimates confidence intervals for you. Its newer seaborn.objects interface adds a composable grammar-of-graphics style.
Strengths
- Native DataFrame support with
data=,x=,y=,hue=semantics - Built-in KDE, regression fits, and categorical estimators
- Attractive default themes
Weaknesses
- Limited interactivity
- Fine-tuning still drops into Matplotlib
- Slower on very large datasets because it computes statistics client-side
import seaborn as sns
sns.set_theme(style="whitegrid", context="talk")
# One call: scatter + marginal distributions + hue grouping
g = sns.jointplot(
data=df, x="tenure_months", y="monthly_spend",
hue="segment", kind="scatter", height=6,
)
g.figure.suptitle("Spend vs. Tenure", y=1.02)
# Statistical summary: mean churn by segment with 95% CI
fig, ax = plt.subplots(figsize=(6, 4))
sns.barplot(data=df, x="segment", y="churned",
errorbar=("ci", 95), ax=ax) # bootstrapped confidence interval
ax.set_ylabel("Churn rate")
Plotly: Interactive Charts for the Web
Plotly renders with JavaScript (plotly.js), so every chart ships with hover tooltips, zoom, pan, and export buttons. Plotly Express (px) provides a concise API. plotly.graph_objects offers full control.
Strengths
- Interactivity with zero extra code
- Native integration with Dash for production dashboards
- 3D, geo, and financial chart types
Weaknesses
- Large figures serialize to heavy JSON and slow the browser
- Static export needs the kaleido package
- Fine styling control is more verbose than Matplotlib
import plotly.express as px
fig = px.scatter(
df, x="tenure_months", y="monthly_spend",
color="segment", size="monthly_spend",
hover_data=["churned"],
title="Spend vs. Tenure by Segment",
template="plotly_white",
)
fig.update_traces(marker=dict(opacity=0.65))
fig.write_html("scatter_plotly.html") # shareable standalone file
fig.show()
For datasets above roughly 50,000 points, switch to px.scatter_gl (WebGL) or pre-aggregate.
Altair: Declarative Grammar of Graphics
Altair generates Vega-Lite specifications. You declare what to show (encodings), not how to draw it. This produces short, readable code and powerful interactive selections.
Strengths
- Concise syntax: mark + encoding + transform
- Linked brushing and cross-filtering in a few lines
- Output is a portable JSON spec
Weaknesses
- Default 5,000-row embedding limit (raise with
alt.data_transformers) - Less control over low-level styling
- Limited chart types versus Plotly
import altair as alt
alt.data_transformers.disable_max_rows() # use with care on large data
brush = alt.selection_interval() # drag to select a region
scatter = (
alt.Chart(df)
.mark_circle(size=40, opacity=0.6)
.encode(
x="tenure_months:Q",
y="monthly_spend:Q",
color=alt.condition(brush, "segment:N", alt.value("lightgray")),
tooltip=["segment", "churned"],
)
.add_params(brush)
)
bars = (
alt.Chart(df)
.mark_bar()
.encode(x="segment:N", y="mean(churned):Q")
.transform_filter(brush) # bars update when you brush the scatter
)
(scatter | bars).save("linked_altair.html")
Bokeh: Streaming and Server-Backed Apps
Bokeh targets browser interactivity with a Python server for callbacks. It handles streaming data and custom widgets better than most alternatives.
from bokeh.plotting import figure, show
from bokeh.models import ColumnDataSource, HoverTool
from bokeh.transform import factor_cmap
source = ColumnDataSource(df) # shared data source enables linked plots
palette = ["#1f77b4", "#ff7f0e", "#2ca02c"]
p = figure(width=700, height=450, title="Spend vs. Tenure",
tools="pan,wheel_zoom,box_select,reset")
p.scatter("tenure_months", "monthly_spend", source=source, size=7,
color=factor_cmap("segment", palette, ["Basic", "Plus", "Premium"]),
alpha=0.6, legend_field="segment")
p.add_tools(HoverTool(tooltips=[("Segment", "@segment"), ("Churned", "@churned")]))
show(p)
plotnine: ggplot2 for Python
plotnine implements the R ggplot2 grammar and renders through Matplotlib. It suits analysts who already think in layers (geom_*, facet_*, scale_*).
from plotnine import ggplot, aes, geom_point, facet_wrap, theme_minimal
(
ggplot(df, aes("tenure_months", "monthly_spend", color="segment"))
+ geom_point(alpha=0.6)
+ facet_wrap("~segment")
+ theme_minimal()
)
Head-to-Head Comparison
| Criterion | Matplotlib | Seaborn | Plotly | Altair | Bokeh |
|---|---|---|---|---|---|
| Lines of code (scatter + color) | ~8 | ~2 | ~2 | ~4 | ~7 |
| Interactivity | Low | None | High | High | High |
| Large data (100k+ rows) | Good | Moderate | Needs WebGL | Poor | Good (Datashader) |
| Statistical built-ins | Manual | Excellent | Moderate | Good (transforms) | Manual |
| Customization depth | Excellent | Good (via Matplotlib) | Good | Moderate | Good |
| Dashboard integration | Streamlit (static) | Streamlit (static) | Dash, Streamlit | Streamlit, Panel | Native server |
| Static export | Native | Native | Needs kaleido | Needs vl-convert | Needs selenium or export tools |
| Publication quality | Best | Very good | Good | Good | Moderate |
Scalability and Performance
Rendering cost depends on where drawing happens.
| Rows | Best Choices | Notes |
|---|---|---|
| < 5,000 | Any library | Pick by workflow preference |
| 5,000 to 100,000 | Matplotlib, Seaborn, Plotly (WebGL), Bokeh | Altair needs pre-aggregation |
| 100,000 to 10M+ | Datashader with Bokeh/HoloViews, Matplotlib hexbin | Rasterize server-side |
For massive datasets, aggregate before plotting:
# Hexbin compresses millions of points into density cells
fig, ax = plt.subplots(figsize=(7, 5))
hb = ax.hexbin(df["tenure_months"], df["monthly_spend"],
gridsize=30, cmap="viridis", mincnt=1)
fig.colorbar(hb, label="Customer count")
Real-World Use Cases
Exploratory Analysis of Customer Churn
Start with Seaborn pairplot and histplot to inspect distributions and correlations. Move to Plotly only when stakeholders need to filter by segment.
sns.pairplot(df, vars=["tenure_months", "monthly_spend"],
hue="churned", corner=True, plot_kws={"alpha": 0.5})
Model Evaluation Reports
Use Matplotlib for ROC curves, calibration plots, and confusion matrices in papers. Scikit-Learn’s RocCurveDisplay and ConfusionMatrixDisplay return Matplotlib axes, so styling stays consistent.
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import RocCurveDisplay
X = df[["tenure_months", "monthly_spend"]]
y = df["churned"]
X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.25, random_state=42)
model = LogisticRegression().fit(X_tr, y_tr)
RocCurveDisplay.from_estimator(model, X_te, y_te) # reports **AUC-ROC** on the plot
Live Sensor Monitoring
Use Bokeh server with ColumnDataSource.stream() to append readings in real time without redrawing the whole figure.
Executive Dashboards
Build with Plotly + Dash or Streamlit. Both turn a figure into a deployed app with callbacks and filters.
Decision Matrix: Which Library Should You Choose?
| If You Need… | Choose |
|---|---|
| Full control and print-ready figures | Matplotlib |
| Fast statistical EDA on DataFrames | Seaborn |
| Interactive dashboards and web embedding | Plotly |
| Linked views with minimal code | Altair |
| Real-time streaming and server callbacks | Bokeh |
| ggplot2 syntax in Python | plotnine |
| Millions of points | Datashader |
A practical stack for most teams: Seaborn for EDA, Matplotlib for final figures, Plotly for stakeholder-facing interactivity.
Best Practices Across All Libraries
- Set a reproducible theme once. Define colors, fonts, and sizes in a shared module.
- Use colorblind-safe palettes such as
viridis,cividis, or Seaborn’scolorblind. - Label units on every axis and title charts with the finding, not the variable names.
- Aggregate before plotting when row counts exceed what the renderer can handle.
- Pin library versions in
requirements.txt. API defaults change between releases, especially in Seaborn and Altair.
Frequently Asked Questions
Which Python visualization library is best for beginners?
Seaborn is the easiest starting point. It accepts a pandas DataFrame directly and produces readable statistical plots with one function call. Learn basic Matplotlib next, since Seaborn plots return Matplotlib objects you will need to customize.
Is Plotly better than Matplotlib?
Neither wins universally. Plotly is better for interactive, web-based output. Matplotlib is better for static, publication-quality figures and fine-grained control. Many data scientists use both.
Can Seaborn and Matplotlib be used together?
Yes. Seaborn is built on Matplotlib. Pass an ax= argument to any Seaborn function to draw on a Matplotlib axes, then customize titles, ticks, and annotations with Matplotlib methods.
Which library handles large datasets best?
Datashader (usually paired with Bokeh or HoloViews) handles tens of millions of points by rasterizing server-side. For moderate sizes, Matplotlib and Plotly with WebGL perform well. Altair is the weakest on large raw data.




