Contents
Visualizing Data with Python
🍎 Low Hanging Fruit
Numbers in a spreadsheet hide their patterns. A well-chosen chart reveals them instantly. Visualization is not decoration — it is the fastest way to find outliers, understand distributions, and communicate findings. This article covers the three Python libraries that cover most visualization needs, and when to use each.
The Three Libraries
| Library | Best for | Output |
|---|---|---|
| matplotlib | Full control, publication figures, subplots | Static image |
| seaborn | Statistical plots, clean defaults | Static image |
| plotly | Interactive charts, dashboards, web embedding | Interactive HTML |
Install them:
pip install matplotlib seaborn plotly pandas
A practical default: start with seaborn. It has sensible defaults and the statistical plot types you’ll reach for first. Switch to plotly when the output needs to be interactive or live in a browser. Reach for matplotlib directly only when you need control that seaborn can’t give you — which is less often than you’d think. Most beginners reach for matplotlib first out of familiarity and spend twice as long fighting defaults that seaborn would have handled for free.
matplotlib: The Foundation
Everything in Python visualization ultimately runs on matplotlib. Knowing the basics prevents confusion when seaborn or pandas charts behave unexpectedly.
The Figure/Axes Model
import matplotlib.pyplot as plt
import pandas as pd
df = pd.read_csv("data/cleaned/prices.csv", parse_dates=["date"])
fig, ax = plt.subplots(figsize=(12, 5))
ax.plot(df["date"], df["close"], linewidth=1.5, color="#2563eb", label="Close")
ax.set_title("AAPL Daily Close Price", fontsize=14)
ax.set_xlabel("Date")
ax.set_ylabel("Price (USD)")
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig("charts/aapl_close.png", dpi=150)
plt.show()
fig is the whole figure window. ax is the plot area inside it. Most customization happens on the ax object.
Multiple Subplots
fig, axes = plt.subplots(2, 2, figsize=(14, 8))
axes = axes.flatten()
tickers = ["AAPL", "MSFT", "GOOG", "AMZN"]
for i, ticker in enumerate(tickers):
subset = df[df["ticker"] == ticker]
axes[i].plot(subset["date"], subset["close"])
axes[i].set_title(ticker)
axes[i].grid(True, alpha=0.3)
plt.suptitle("Close Prices", fontsize=16)
plt.tight_layout()
plt.savefig("charts/grid.png", dpi=150)
seaborn: Statistical Visualization
seaborn sits on top of matplotlib and adds sensible defaults and statistical plot types. It is the fastest way to go from a DataFrame to a meaningful chart. The thing that trips up newcomers: when something doesn’t look right and seaborn has no fix, you’ll end up calling plt.gca() to get the underlying matplotlib Axes object and adjusting it directly. This is expected behavior, not a bug — seaborn delegates rendering to matplotlib, so they’re always compatible. Knowing a bit of matplotlib is the unlock that makes seaborn fully usable.
Distribution Plots
import seaborn as sns
import matplotlib.pyplot as plt
# Set a clean theme
sns.set_theme(style="whitegrid")
fig, axes = plt.subplots(1, 2, figsize=(12, 5))
# Histogram with density curve
sns.histplot(df["daily_return"], bins=60, kde=True, ax=axes[0])
axes[0].set_title("Distribution of Daily Returns")
# Box plots by group
sns.boxplot(data=df, x="ticker", y="daily_return", ax=axes[1])
axes[1].set_title("Return Distribution by Ticker")
plt.tight_layout()
plt.savefig("charts/distributions.png", dpi=150)
Correlation Heatmap
import seaborn as sns
import matplotlib.pyplot as plt
# Pivot to wide format: one column per ticker
pivot = df.pivot(index="date", columns="ticker", values="close")
corr = pivot.pct_change().corr()
fig, ax = plt.subplots(figsize=(8, 6))
sns.heatmap(
corr,
annot=True,
fmt=".2f",
cmap="coolwarm",
vmin=-1,
vmax=1,
ax=ax,
)
ax.set_title("Return Correlation Matrix")
plt.tight_layout()
plt.savefig("charts/correlation.png", dpi=150)
Scatter Plot with Regression
sns.lmplot(
data=df,
x="volume_log",
y="abs_return",
hue="ticker",
height=5,
aspect=1.4,
scatter_kws={"alpha": 0.3},
)
plt.title("Volume vs. Absolute Return")
plt.savefig("charts/scatter.png", dpi=150)
plotly: Interactive Charts
Static PNGs are fine for reports and papers. For dashboards, notebooks, or any output that will be viewed in a browser, plotly’s interactivity (hover tooltips, zoom, pan, click-to-filter) makes a significant difference. One caveat: if the output is going into a PDF, slide deck, or email, most recipients won’t open an .html file. Interactive charts are for audiences already in a browser — for everything else, export a static image.
pip install plotly
Line Chart
import plotly.express as px
fig = px.line(
df[df["ticker"] == "AAPL"],
x="date",
y="close",
title="AAPL Daily Close",
labels={"close": "Close Price (USD)", "date": "Date"},
template="plotly_white",
)
# Save as interactive HTML
fig.write_html("charts/aapl_close.html")
# Display in a Jupyter notebook
fig.show()
Multi-ticker Comparison
fig = px.line(
df,
x="date",
y="close_normalized", # normalized to 100 at start
color="ticker",
title="Normalized Price Performance",
template="plotly_white",
)
fig.update_layout(legend_title="Ticker")
fig.write_html("charts/comparison.html")
Candlestick Chart
import plotly.graph_objects as go
aapl = df[df["ticker"] == "AAPL"].copy()
fig = go.Figure(data=[go.Candlestick(
x=aapl["date"],
open=aapl["open"],
high=aapl["high"],
low=aapl["low"],
close=aapl["close"],
)])
fig.update_layout(
title="AAPL — OHLC Candlestick",
xaxis_title="Date",
yaxis_title="Price (USD)",
xaxis_rangeslider_visible=False,
template="plotly_white",
)
fig.write_html("charts/aapl_candlestick.html")
Volume Bar Chart Overlay
from plotly.subplots import make_subplots
import plotly.graph_objects as go
fig = make_subplots(
rows=2, cols=1,
shared_xaxes=True,
row_heights=[0.7, 0.3],
vertical_spacing=0.04,
)
aapl = df[df["ticker"] == "AAPL"]
fig.add_trace(go.Scatter(x=aapl["date"], y=aapl["close"], name="Close"), row=1, col=1)
fig.add_trace(go.Bar(x=aapl["date"], y=aapl["volume"], name="Volume", marker_color="rgba(37,99,235,0.4)"), row=2, col=1)
fig.update_layout(title="AAPL Price and Volume", template="plotly_white")
fig.write_html("charts/aapl_price_volume.html")
Saving Chart Output Consistently
Organize charts by date and type to keep the output directory manageable.
from pathlib import Path
from datetime import date
CHART_DIR = Path("charts") / str(date.today())
CHART_DIR.mkdir(parents=True, exist_ok=True)
def save_fig(fig, name: str, formats=("png", "html")):
for fmt in formats:
path = CHART_DIR / f"{name}.{fmt}"
if fmt == "png":
fig.write_image(str(path), scale=2) # requires kaleido: pip install kaleido
elif fmt == "html":
fig.write_html(str(path))
print(f"Saved {path}")
Static vs. Interactive: Choosing a Format
The right output format depends on where the chart will be seen and what the reader needs to do with it.
| Situation | Use |
|---|---|
| Report, paper, or slide deck | Static PNG/SVG (matplotlib or seaborn) |
| Jupyter notebook for personal exploration | Either — plotly renders inline |
| Web page or dashboard | Interactive HTML (plotly) |
| High row count (100K+ points) | Static — interactive charts slow down at scale |
| Reader needs to filter, zoom, or hover | Interactive (plotly) |
| You need precise pixel-level control | matplotlib |
A practical default: use seaborn for exploration and static export, switch to plotly when you’re sharing something that benefits from tooltips or filtering.
Chart Choice Guide
| Question to answer | Recommended chart |
|---|---|
| How does a value change over time? | Line chart |
| How is a numeric value distributed? | Histogram, box plot |
| Are two variables correlated? | Scatter plot |
| How do categories compare? | Bar chart |
| What fraction does each part represent? | Stacked bar (not pie) |
| How do many variables correlate? | Heatmap |
| What is the price range over time? | Candlestick |
Avoid pie charts. The only defensible case is two categories where the split is dramatic enough to be obvious at a glance. Anything else, use a bar chart — it communicates the same information with less visual distortion and makes comparisons across groups far easier. The pie chart is the chart type that looks like a choice and is usually a mistake.
Colorblind-Friendly Palettes
Roughly 8% of men and 0.5% of women have some form of color vision deficiency. The default matplotlib and seaborn color cycles are not designed with this in mind. Use a colorblind-safe palette whenever a chart will be shared.
import seaborn as sns
import matplotlib.pyplot as plt
# seaborn's colorblind palette — safe for the most common deficiencies
sns.set_palette("colorblind")
# Or set it per plot
palette = sns.color_palette("colorblind")
sns.lineplot(data=df, x="date", y="close", hue="ticker", palette=palette)
For matplotlib directly:
# Okabe-Ito palette — widely recommended for accessibility
OKABE_ITO = [
"#E69F00", "#56B4E9", "#009E73",
"#F0E442", "#0072B2", "#D55E00", "#CC79A7",
]
fig, ax = plt.subplots()
for i, ticker in enumerate(tickers):
subset = df[df["ticker"] == ticker]
ax.plot(subset["date"], subset["close"],
color=OKABE_ITO[i % len(OKABE_ITO)],
label=ticker)
ax.legend()
Beyond color: use different line styles (linestyle="--", ":") or markers alongside color when printing in black and white is a possibility.
Next Steps
- Python Pandas Data Wrangling — Preparing the DataFrame before you plot it.
- Low-Hanging Data Sources for Stock Market Prediction — Finding more signals worth visualizing.
- DuckDB for Financial Data Analysis — Querying larger datasets efficiently before visualizing them.