Visualizing Data with Python
Contents
· 7 min read

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

LibraryBest forOutput
matplotlibFull control, publication figures, subplotsStatic image
seabornStatistical plots, clean defaultsStatic image
plotlyInteractive charts, dashboards, web embeddingInteractive 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.

SituationUse
Report, paper, or slide deckStatic PNG/SVG (matplotlib or seaborn)
Jupyter notebook for personal explorationEither — plotly renders inline
Web page or dashboardInteractive HTML (plotly)
High row count (100K+ points)Static — interactive charts slow down at scale
Reader needs to filter, zoom, or hoverInteractive (plotly)
You need precise pixel-level controlmatplotlib

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 answerRecommended 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