Plotting from pandas

df.plot as a shortcut, and when to drop back to matplotlib.

Overview

What it does for you

pandas has a built-in plotting interface. df.plot() draws a chart directly from a DataFrame or Series. It calls matplotlib under the hood, returns a matplotlib Axes, and accepts most of the same keyword arguments.

The value of the pandas plotting interface is speed of exploration. One call draws a chart with labelled axes, a legend from column names, and date formatting from a DatetimeIndex. Doing the same in raw matplotlib takes five or six lines. For a quick look at data during analysis, that difference matters.

The interface covers line charts, bar charts, scatter plots, histograms, box plots, area charts, pie charts, hexbin plots and kernel density estimates. Each is a kind= argument or a method: df.plot.bar(), df.plot.scatter(x=..., y=...), df.plot.hist(). The defaults are tuned for DataFrames: column names become legend entries, the index becomes the x axis, and grouped operations map naturally to grouped charts.

The trade-off is control. pandas plotting handles the common cases quickly but makes unusual layouts harder. Once you need a second y axis, custom tick formatting, insets or precise spacing, you are better off using matplotlib directly. Knowing when to drop back is part of the skill.

This page covers the common chart types through pandas, the relationship between the pandas wrapper and the matplotlib objects underneath, and the patterns for combining pandas convenience with matplotlib control. It also covers the DataFrame shapes that map cleanly to charts and the reshaping operations that produce them.

The most common mistake is treating df.plot as a separate library. It is not. It returns a matplotlib Axes. You can call ax.set_title, ax.set_ylabel, ax.legend and any other matplotlib method on the result. The two layers are designed to work together.

A second common mistake is plotting a wide DataFrame when a long one is needed. pandas plots each column as a series. If your groups are in rows rather than columns, the chart draws the wrong thing. df.melt() or df.pivot() reshapes the frame. The page covers both.

df.plot() draws each column as a line against the index, adds a legend labelled with the column names. Formats the x axis rightly for the index type.

For a time-indexed frame that is a dated axis, sensible tick spacing, one line per column and a legend. From one call. Doing the same in raw matplotlib is five or six lines.

That is the case for using it: for exploration. For the common chart types, it's simply faster.

Worth knowing

df.plot() draws every column against the index and supplies labels and a legend from the column names.
It returns an Axes and accepts ax=, which is how pandas plots fit into a matplotlib layout.
kind= covers line, bar, barh, hist, box, kde, area, pie, scatter and hexbin; df.plot.bar() is the same with better syntax.
Each column is a series and each row a group — so the frame's shape decides the chart, and df.T swaps them.
subplots=True gives one panel per column and returns an array of axes; the y axes are independent unless you pass sharey.
pandas is a shortcut for the common cases; annotations, thresholds and shading are easier in matplotlib, and the two mix freely.

Plotting from pandas

df.plot as a shortcut, and when to drop back to matplotlib.

df.plot draws every column against the index

One call, and pandas supplies the labels and the legend.

example_01.pymatplotlib
Output

It returns an Axes, and takes one

Which is how pandas plots fit into a matplotlib layout.

example_02.pymatplotlib
Output

The kinds

One argument covers most of the chart types.

example_03.pymatplotlib
Output

Grouped and stacked, from a frame

A wide frame is already a grouped bar chart.

example_04.pymatplotlib
Output

subplots=True for small multiples

One panel per column, sharing the x axis.

example_05.pymatplotlib
Output

When to drop back to matplotlib

pandas is a shortcut; anything unusual is easier underneath.

example_06.pymatplotlib
Output

It is a thin wrapper

The key thing to know is that df.plot returns a matplotlib Axes, and accepts one through ax=.

That means it isn't a split system. all in this track applies to the result:

fig, ax = plt.subplots()
df.plot(ax=ax)
ax.set_title(...)
ax.axhline(...)

ax= also matters for a other reason: without it, pandas creates its own figure. A df.plot call inside a loop over subplots draws each chart in a new figure rather than the panel you intended. This Is a common and confusing result.

kinds

kind= selects the chart: "line" (the default), "bar", "barh", "hist", "box", "kde", "area", "pie", "scatter", "hexbin".

df.plot.bar() is equal and reads better, with the edge that an editor can autocomplete the method names and their args.

scatter and hexbin need x= and y= naming columns. They take two vars rather than plotting all against the index.

Most keyword args are passed through to matplotlib. alpha, color, linewidth and the rest work as expected.

The frame's shape is the chart

This is the part that finds whether the output is what you wanted.

Each column turns a series. Each row turns a spot on the x axis.

So a frame with quarters as rows and products as columns gives grouped bars with one cluster per quarter. If you wanted one cluster per product, the frame needs transposing. df.T.plot(kind="bar"). Not a other plotting arg.

When a chart comes out grouped the wrong way, the fix is nearly always a pivot, a groupby().unstack(), or a transpose. That is the reshaping module doing its job: getting the frame into the right shape is the plotting work. The plot call is then trivial.

stacked=True stacks bars or areas.

subplots=True

df.plot(subplots=True) gives one panel per column and returns an array of axes rather than one.

It's the fastest route to small multiples. It has a default worth knowing. The y axes are split. The panels aren't close. sharey=True fixes it, and without it a panel showing a range of 0–2 sits beside one showing 0–2000 at the same visual height.

layout=(2, 3) arranges them in a grid rather than a column.

When to drop back

pandas covers the common cases and exposes nothing beyond them.

Reach for matplotlib when you need:

annotations, arrows or text;

link lines and shaded regions;

a second y axis, or a secondary unit;

fine control of ticks, formatters or scales;

anything drawn conditionally — highlighting one series, greying the rest.

The good news is that this isn't a switch. df.plot(ax=ax) for the data and ax. methods for all else is the normal way to work. It's why the wrapper is worth having.

And when to leave both

For statistical charts — faceted grids, regression plots with trust bands, cat scatter with built-in jitter — seaborn sits on top of matplotlib and makes them in one call, returning matplotlib objects you can adjust later.

For interactive charts in a browser, Plotly or Altair. matplotlib's interactivity is limited and not its purpose.

Knowing where matplotlib stops is part of using it well. It's a drawing library that happens to have statistical conveniences, not a statistical graphics system.

What pandas adds

Beyond convenience, three things are truly easier through pandas.

The index turns the x axis, with date formatting handled. That alone drops a few lines for any time series.

Column names become labels, so the legend is built from the data rather than from a list you maintain apart.

Groupby output plots directly: df.groupby("k")["v"].sum().plot(kind="barh") goes from raw rows to a chart in one line, because the aggregation produces exactly the index-and-values shape the plot wants.

That last point is the real workflow: reshape until the frame *is* the chart, then plot it.

Common frame shapes

A time-indexed frame, one column per series → df.plot() gives a multi-line chart.

A frame indexed by group, one column per group → df.plot(kind="bar") gives grouped bars.

A long frame — one row per finding with a group column — needs pivoting first: df.pivot(index=..., columns=..., values=...).

A group-by result with two keys → .unstack() puts one key into columns, giving the wide shape the plot needs.

Recognising which of these you've, and which the chart needs, is most of the work. The plot call is one line either way.

Where pandas gets in the way

df.plot has its own views that at times conflict with matplotlib's.

It sets its own tick locators for date axes. This Are often good and are awkward to replace later. Setting a matplotlib locator on an axes pandas has already formatted at times has no effect. Plotting with ax.plot(df.index, df[col]) instead gives back full control.

It creates a figure if not given one. This Is the ax= issue.

And secondary_y=True exists and makes a twin axis with all the problems from that module, plus a legend that is harder to assemble.

For anything beyond the standard cases, dropping to ax.plot(df.index, df["col"]) costs one line and drops the ambiguity about who owns the axes.

Categorical bar charts

df.plot(kind="bar") uses the index as the groups and draws them at int spots, with the labels rotated 90 degrees by default.

rot=0 stops the rotation. kind="barh" is often better still, for the reasons in the bar module.

Sorting is a pandas step, not a plotting one: df.sort_values("v").plot(kind="barh"). That is a small example of the general pattern. The data handling belongs in pandas. The plot call should be trivial.

Beyond pandas

df.plot covers the standard charts. seaborn covers the statistical ones. Faceted grids, regression with bands, cat scatter with jitter built in. And returns matplotlib objects. The same adjustment methods apply later.

The three layers work together: pandas for the data, seaborn for the statistical display, matplotlib for the final control. Knowing which layer a problem belongs to is most of using them well. The answer for "make this exact thing look right" is almost always the bottom one.

A complete example

The common shape of real plotting code. Here The data work dominates:

summary = (df
           .query("year == 2024")
           .groupby(["region", "month"], as_index=False)["sales"].sum()
           .pivot(index="month", columns="region", values="sales"))

fig, ax = plt.subplots(figsize=(7.5, 3.5))
summary.plot(ax=ax, linewidth=2)
ax.set_title("Sales by region, 2024", loc="left", fontweight="bold")
ax.set_ylabel("Sales (thousands)")
for side in ("top", "right"):
    ax.spines[side].set_visible(False)

Four lines of pandas to get the frame into the shape of the chart, one to draw it, and four to finish it.

That ratio is normal, and it's why reshaping is a plotting skill.

Pitfalls

**Losing ax=**, so the plot lands in its own figure.

The wrong frame shape, giving bars grouped by the wrong var.

Rotated labels by default on kind="bar", fixed with rot=0 or by using barh.

**secondary_y=True**, which is a twin axis with all its problems.

given shared axes with subplots=True. Here The default is split.

A datetime index that is really strings, giving a cat axis with no gaps.

The last is worth checking with df.index.dtype before wondering why the spacing looks wrong.

The three layers

Real plotting code sits across three libraries. Knowing which one owns a problem saves a lot of searching.

pandas owns the data: filtering, grouping, pivoting, resampling. If the chart is grouped by the wrong thing, or the bars are clustered incorrectly, or a date axis is behaving like a group, the fix is here. In the shape or the dtypes, not in the plot call.

seaborn owns statistical display: faceting, regression with bands, cat scatter with jitter, spread checks. If you're writing twenty lines to build a grid of conditioned plots, this layer already has it.

matplotlib owns the drawing: annotations, link lines, exact ticks, colour control, layout, saving. all the other two give is a matplotlib object. This layer is always on hand underneath.

The common mistake is trying to solve a data problem in the plotting call. Adding args to df.plot to fix a grouping that should have been a pivot.

Plotting from pandas, in summary

df.plot() draws each column against the index and builds the legend from the column names.

It returns an Axes and takes one. This Is what makes it a shortcut rather than a split system. And ax= is needed, or a plot inside a subplot loop creates its own figure.

The frame's shape is the chart: columns are series, rows are groups. A chart grouped the wrong way needs a transpose or a pivot.

subplots=True gives small multiples and split y axes unless you say else.

Reach for matplotlib for annotations, thresholds, shading and exact control. And mix freely. It's the same Axes object either way.

Reshaping is the plotting work

The recurring lesson of this module is worth stating on its own.

The plot call is often one line. The work is getting the frame into a shape where that one line is correct.

A wide frame — one column per series, index as x — is what a multi-line chart or a grouped bar chart wants.

A long frame — one row per finding — is what seaborn and most statistical tools want.

An aggregated frame — one row per group — is what a bar chart of totals wants.

Moving between them is pivot, melt, groupby().agg() and unstack, all covered in the pandas track.

When a chart comes out grouped by the wrong var, or with the series and groups swapped, or with one line where there should be five, the fix is almost never an arg to plot. It's the shape of the frame. Recognising that at once saves a lot of time reading plotting docs for a thing the data work should have solved.

A closing note on Plotting from pandas

df.plot is a shortcut, and its value is scaled to how standard the chart is.

For a quick line chart of a time-indexed frame it's unbeatable. One call, correct axis, legend included. For anything with an annotation, a threshold, a highlighted series or a exact tick format, it stops helping and matplotlib underneath does the work.

The two together are the normal way to write plotting code. The boundary isn't a choice you make once. df.plot(ax=ax) followed by half a dozen ax. calls is a just right plain chart.

What is worth internalising is that most plotting problems are data problems. When the chart is grouped wrongly, or has too many lines, or shows a group as a number, the fix is in the frame.

Questions people ask about Plotting from pandas

Does pandas use matplotlib? Yes. df.plot builds a matplotlib figure and returns an Axes, so every matplotlib method works on the result.

How do I put a pandas plot on my own axes? Pass ax=ax to df.plot. This is how you place pandas charts in a subplot grid.

Why is my index on the x axis? df.plot uses the index for x by default. Set the index first, or pass x='column'.

How do I plot one column against another? Use df.plot(x='a', y='b'), or pass the columns to ax.plot directly.

How do I get dates formatted well? Use a DatetimeIndex. pandas then picks sensible date ticks for you, and you can adjust them with matplotlib's date tools.

Should I aggregate before plotting? Yes. Use groupby or resample first, so the chart draws the summary rather than every raw row.

Plotting from pandas: the short version

df.plot is a shortcut that returns a matplotlib Axes. This Is what makes it a convenience rather than a split system.

Its limits arrive quickly, and the boundary isn't a choice made once: df.plot(ax=ax) followed by half a dozen ax. calls is plain code. And most plotting problems are data problems.

Check yourself

0 of 4

Answer without scrolling back up.

  1. What does `df.plot()` use for the x axis?

  2. Why pass `ax=` to `df.plot`?

  3. Your grouped bars are clustered by the wrong variable. What do you change?

  4. What is the default for the y axes with `subplots=True`?

Cheat sheet

Plotting from pandas

Create charts from a pandas DataFrame in one line with df.plot. See when the shortcut works and when to fall back to raw matplotlib calls.

MATPLOTLIB · vizlearn.in/matplotlib/plotting_from_pandas.html

About the author

Ashish Jangra builds and maintains VizLearn. Every module here is written and the visualisation behind it hand-built, so the numbers in a readout come from the same code that draws the picture. Corrections are genuinely welcome and get priority over everything else — if a page states something wrong, or an animation misrepresents what the algorithm does, get in touch.