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 kind | matplotlib equivalent |
|---|---|
line | ax.plot |
bar / barh | ax.bar / ax.barh |
hist | ax.hist |
box | ax.boxplot |
scatter | ax.scatter |
area | ax.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_xticksrather 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.
Related
Chart types: bar, scatter, histogram, pie and box Debugging plots: empty axes, missing data and overlap
Last refreshed 2026-09-18.