Subplot grids
Small multiples, and the array shape that breaks your loop
plt.subplotsaxes.flatshareysqueezefig.delaxessmall multiplesWatch it happen
Play it through, or step back and forth yourself.
fig, axes = plt.subplots(
2, 3, # rows, then columns
figsize=(9, 5),
)
axes.shape # (2, 3)
axes[0, 2] # top right
len(fig.axes) # 6plt.subplots(2, 3) — rows first, then columns. One Figure, six Axes, and axes comes back as a numpy array you index like any other: axes[0, 2].
The idea
plt.subplots(2, 3) — rows first, then columns. One Figure, six Axes, and axes comes back as a numpy array you index normally:
fig, axes = plt.subplots(2, 3, figsize=(9, 5))
axes.shape # (2, 3)
axes[0, 2] # top right
len(fig.axes) # 6The shape trap
The shape of axes depends on how you called it, which is a real source of breakage:
plt.subplots() # a bare Axes — not an array at all
plt.subplots(1, 3) # shape (3,) -> axes[1]
plt.subplots(3, 1) # shape (3,) -> axes[1]
plt.subplots(2, 3) # shape (2, 3) -> axes[0, 1]So a loop written for a 1×3 row raises IndexError the day someone makes it 2×3 — or worse, axes[1] quietly becomes an entire row of Axes rather than one of them. Two ways out:
for ax, name in zip(axes.flat, names): # works for any shape
ax.plot(days, week[name])
fig, axes = plt.subplots(1, 3, squeeze=False) # always 2-Daxes.flat is the one to build the habit around. It iterates every Axes in reading order regardless of the grid, and it's shorter than what it replaces.
Small multiples
This is what grids are for. The same chart repeated once per category, same scale, same style, so the only thing varying between panels is the data:
fig, axes = plt.subplots(2, 3, figsize=(10, 5), sharey=True, layout="constrained")
for ax, item in zip(axes.flat, items):
sub = orders[orders["item"] == item]
ax.hist(sub["wait"], bins=12)
ax.set_title(item, fontsize=10)Five overlapping histograms on one Axes is a mess; five small ones side by side is readable at a glance. The general rule: when you're tempted to put a fifth series on one chart, make small multiples instead.
Sharing is what makes it work
sharey=True is not a nicety here — it's the thing that makes the comparison valid. Without it every panel autoscales to its own data, so a panel of tiny numbers looks identical to a panel of huge ones and the grid actively misleads.
fig, axes = plt.subplots(2, 3, sharey=True) # one y scale for all
fig, axes = plt.subplots(3, 1, sharex=True) # stacked time seriesSharing also hides the interior tick labels, which is why a shared grid looks tidier than one you spaced by hand — no repeated axis running down the middle. And it links the views, so zooming one panel interactively moves the rest.
Leftover panels
Five categories in a 2×3 grid leaves an empty box, and an empty box reads as missing data rather than spare capacity:
for extra in axes.flat[len(items):]:
fig.delaxes(extra)Better still, pick a grid that fits — 1×5, or 2×3 with something useful in the sixth cell.
Sizing
figsize is the whole figure, so a 2×3 grid at the default 6.4×4.8 gives each panel about 2×1.6 inches, which is too small for axis labels. Scale it with the grid — roughly 3×2.2 inches per panel is a reasonable starting point.
See it run
The lesson's code, ready to run and to fiddle with.
Putting the kettle on…
Starting up…
Worked example
not gradedAlready written and ready to go — press Run to see what it does, then change a number, a column name, anything, and run it again.
tryremoving sharey=True and watching the panels stop being comparable.
Your turn
3 exercises. Write the code yourself, then press Check — a nudge and the answer are there if you want them.
Make a 2 by 3 grid of Axes and return [list(axes.shape), len(fig.axes)].
Make a 2×3 grid and give every panel a title — "0" through "5" in reading order — using axes.flat. Return the six titles as a list.
