Plotting directly from pandas and NumPy

What df.plot actually does under the hood, how to combine it with manual axes work, and where pandas stops being enough.

df.plot is matplotlib

pandas charting is a thin convenience layer over matplotlib: it builds a Figure and Axes, plots, and hands them back. Once you know that, mixing the two styles stops feeling like a hack.

import pandas as pd
import matplotlib.pyplot as plt

df = pd.DataFrame({
    "date": pd.date_range("2026-01-01", periods=6, freq="MS"),
    "revenue": [12, 18, 15, 24, 27, 31],
    "cost": [9, 11, 12, 14, 15, 17],
}).set_index("date")

ax = df.plot(kind="line", figsize=(8, 4), ylabel="GBP millions")
ax.set_title("Revenue and cost")

# add a manual element on the pandas axes
ax.axhline(df["revenue"].mean(), color="grey", linestyle="--", linewidth=1)
ax.annotate("average", xy=(df.index[3], df["revenue"].mean()), xytext=(3.2, 15))

# stacked or subplotted variants
df.plot(kind="area", stacked=True)
df.plot(subplots=True, layout=(1, 2), sharey=False)
pandas kindmatplotlib equivalent
lineax.plot
bar / barhax.bar / ax.barh
histax.hist
boxax.boxplot
scatterax.scatter
areaax.fill_between or stackplot

Combining pandas output with manual axes

ax = df.plot(kind="bar", y="revenue")      # returns the Axes

# one panel, several sources: pass the same ax everywhere
fig, ax = plt.subplots(figsize=(8, 4))
df["revenue"].plot(ax=ax, label="revenue")
df["cost"].plot(ax=ax, style="--", label="cost")
ax.legend()

# a numpy overlay on top of a pandas chart
import numpy as np
trend = np.poly1d(np.polyfit(np.arange(len(df)), df["revenue"], 1))
ax.plot(np.arange(len(df)), trend(np.arange(len(df))), color="black", lw=1)

# subplots=True returns an array of Axes, not one Axes
axes = df.plot(subplots=True)
axes[0].set_ylabel("GBP millions")
💡
Pass ax= whenever you combine charts. Without it, pandas creates a new figure each call — which is how a notebook ends up with four half-empty plots.

Feeding raw arrays and datetimes

rng = np.random.default_rng(0)
t = np.linspace(0, 2 * np.pi, 300)

ax.plot(t, np.sin(t), label="sin")
ax.fill_between(t, np.sin(t) - 0.1, np.sin(t) + 0.1, alpha=0.2)

# datetimes: numpy datetime64 arrays work directly
stamps = np.arange("2026-01-01", "2026-04-01", dtype="datetime64[D]")
values = np.cumsum(rng.normal(size=stamps.size))
ax.plot(stamps, values)

# pandas index vs numpy array: shapes must match after alignment
assert len(df) == values.size
  • Plain NumPy arrays need an explicit x axis; pandas supplies the index for you.
  • When passing a Series and a NumPy array together, check the lengths — pandas aligns on index labels while matplotlib aligns on position.
  • Pandas renders NaN as a gap in a line, which is usually what you want; numpy arrays with NaN behave the same way.
  • For text or categorical axes, pass strings to set_xticks rather than plotting by index and hoping.

FAQ

Why did df.plot create two figures?
It creates a new figure when no ax is supplied. Pass ax=ax to draw into an existing panel, or use subplots=True once and index the returned array.
Do I still need matplotlib if I use pandas?
Yes — for annotations, log scales, custom ticks, colorbars and export settings. Treat pandas as a shortcut for the plotting call and matplotlib for everything around it.

Chart types: bar, scatter, histogram, pie and box Debugging plots: empty axes, missing data and overlap

Last refreshed 2026-09-18.