Charts in Python Without the Cargo Cult: The Six Lines That Matter
Most matplotlib code is copied, and most of it is copied from a state-machine style that works for one chart and stops making sense with two. Six lines cover almost everything an analyst needs, and they are worth typing from memory.
What you do: make a figure and axes, plot onto the axes, label everything, save with a bounding box.
The short version. fig is the page, ax is the chart on it. Call methods on ax.
The six lines
import matplotlib.pyplot as plt
fig, ax = plt.subplots(figsize=(8, 4.5))
ax.bar(totals.index, totals.values, color='#0E7490')
ax.set_title('East sells most, West sells least')
ax.set_ylabel('Revenue')
ax.spines[['top', 'right']].set_visible(False)
fig.savefig('revenue.png', dpi=200, bbox_inches='tight')
That is a chart somebody can paste into a document without apologising for it. Line by line: a figure of a sensible aspect ratio, one plot call, a title that says the finding, a unit on the axis, two borders removed, and a save that does not clip the labels.
Why the object form
# state machine: acts on whichever axes is current
plt.plot(x, y)
plt.title('...')
# object oriented: says which axes it means
fig, ax = plt.subplots()
ax.plot(x, y)
ax.set_title('...')
With one chart the two are equivalent. With two, the state-machine version depends on which axes is current at that moment, which is decided by the order of the calls and by anything pandas did on the way. The object form has no ambiguity, and it is what every larger example eventually uses anyway.
Note the naming: the pyplot function is title, the axes method is set_title. That set_ prefix trips people up when converting old code.
Several charts
fig, axes = plt.subplots(1, 2, figsize=(11, 4), sharey=True)
axes[0].bar(jan.index, jan.values)
axes[0].set_title('January')
axes[1].bar(feb.index, feb.values)
axes[1].set_title('February')
fig.suptitle('January outsold February by more than two to one')
fig.tight_layout()
sharey=True is the important argument. Two charts side by side with different y scales invite exactly the comparison they cannot support, which is the small-multiples version of the problem in dual axis charts.
Plotting from pandas
totals = df.groupby('region')['amount'].sum().sort_values()
fig, ax = plt.subplots(figsize=(7, 4))
totals.plot(kind='barh', ax=ax, color='#0E7490')
ax.set_xlabel('Revenue')
ax.set_title('East is 3 times West')
Passing ax=ax is what keeps you in control: pandas draws onto your axes and you keep every labelling method. Sorting before plotting is the other half, since a chart in alphabetical order asks the reader to do the ranking themselves.
The defaults worth changing once
| Default | Change to | Why |
|---|---|---|
| Box on all four sides | Remove top and right | Less ink, same information |
| Small default font | 11 or 12 | Charts get pasted and shrunk |
| No thousands separator | A comma formatter | 1200000 is unreadable |
| Default figure size | 8 x 4.5 for a slide | Matches a widescreen layout |
| Ten-colour cycle | One colour, or two | Colour should mean something |
ax.yaxis.set_major_formatter(
plt.matplotlib.ticker.StrMethodFormatter('{x:,.0f}'))
plt.rcParams.update({'font.size': 11, 'axes.spines.top': False,
'axes.spines.right': False, 'figure.dpi': 110})
Put the rcParams block at the top of your notebook template and every chart in the file inherits it, which is how a set of charts starts looking like one set rather than nine.
Labelling the bars
bars = ax.bar(totals.index, totals.values, color='#0E7490')
ax.bar_label(bars, fmt='{:,.0f}', padding=3)
ax.get_yaxis().set_visible(False)
Once every bar carries its number, the axis is redundant ink and can go. That is often the cleanest chart on a slide: labelled bars, a title stating the finding, and nothing else.
Saving
fig.savefig('chart.png', dpi=200, bbox_inches='tight')
fig.savefig('chart.svg', bbox_inches='tight') # scales, for print
dpi=200 for anything that will be looked at, bbox_inches='tight' always. SVG when the chart will be resized, since it stays sharp. And close figures in a loop with plt.close(fig), or a long-running script accumulates them in memory.
How to apply this to your own work
- Convert one existing chart from
plt.calls to thefig, axform. It is a mechanical change and it makes the next one easier. - Put a
rcParamsblock at the top of your template today. - Give every chart a title that states the finding, not the contents.
- Sort before plotting, unless the categories have a natural order.
- Save with
bbox_inches="tight"and check the file rather than the notebook preview.
The one habit to keep
Write the title first. It settles what the chart is for, and it often reveals that the chart you were about to draw was not the one that shows it.
Would your last Python chart survive being pasted into a document with no explanation?
set_ prefixed axes methods, bar_label, StrMethodFormatter and bbox_inches are all current matplotlib.Charts and Visualization is the chart-choosing book: what each shape can carry, what it quietly distorts, and how to label it so the reader reaches your finding without being told.
Charts and Visualization, $19 →Choosing the right chart and chart titles apply whatever you draw with. The Python Kit covers the pandas side.
Read Choose the Right Chart →