← Matplotlib·Capstone·24 min·0/4 exercises

Capstone: a figure fit to publish

One claim, one chart, and everything that does not serve it removed

np.polyfitax.annotateax.spinesax.fill_betweensavefigfigsize

Watch it happen

Play it through, or step back and forth yourself.

1
Write the sentence"Every degree costs 2.3 cups"
the claim comes first
2
Check it survivespolyfit, corrcoef, group means
lessons 6 and 8
3
Choose the encodingscatter + fitted line
lesson 6
4
Strip the furniturespines off, ticks thinned
lessons 9 and 11
5
Say it on the charttitle, annotation, direct labels
lesson 12
6
Export for the mediumfigsize, dpi, bbox_inches
lessons 4 and 18

One chart, made properly. Not four panels — one, carrying a single claim that a reader takes away in five seconds. The order is the lesson: the sentence comes first, and the chart is built to support it.

The idea

The dashboard was four questions. This is one — and a chart that carries one claim, made properly, is worth more than a page of correct panels nobody reads.

1. Write the sentence first

Before you touch matplotlib, write the sentence you want the reader to leave with. Ours:

"Every degree hotter costs the stall about two and a half cups."

Everything after this is in service of that sentence. If a gridline doesn't help someone believe it, it goes.

Hand-drawn notes showing that the sentence a figure is meant to say should be written before the figure is drawn, and then chosen for.

2. Try to break it

The step people skip. Before you draw the sentence, check whether it survives contact with the data:

m, b = np.polyfit(orders["temp"], orders["cups"], 1)     # slope -2.34
np.corrcoef(orders["temp"], orders["cups"])[0, 1]        # -0.768

cold = orders[orders["temp"] < 25]["cups"]     # n = 19, mean 86.1
hot  = orders[orders["temp"] > 35]["cups"]     # n = 11, mean 53.1

Slope, correlation and a group comparison all say the same thing, and the group sizes are small but not trivial. It holds. Plenty of sentences you write won't — and finding that out now is cheaper than finding out in the meeting.

Notice that the number in the sentence is the slope, not the correlation. −0.768 is unitless and nobody can repeat it; "2.3 cups per degree" is something a person can carry around and check.

3. Choose the encoding

Two continuous columns, one relationship: a scatter with the fit drawn on it (lesson 6). The raw points stay — they show the spread, and a fitted line alone would hide how much variation there is around it.

fig, ax = plt.subplots(figsize=(7.5, 4.6), layout="constrained")

ax.scatter(orders["temp"], orders["cups"], s=22, alpha=0.55,
           color="#ff7d0c", edgecolors="none")

xs = np.linspace(orders["temp"].min(), orders["temp"].max(), 50)
ax.plot(xs, m * xs + b, color="#e8e8e8", linewidth=1.6)

The fit is quieter than the data, not louder. It's the summary; the points are the evidence.

4. Strip the furniture

for side in ("top", "right"):
    ax.spines[side].set_visible(False)

ax.grid(axis="y", alpha=0.18)
ax.tick_params(length=0)
ax.set_xlabel("temperature (°C)")
ax.set_ylabel("cups sold per order window")

Every element earns its place or leaves. The top and right spines enclose nothing. Tick marks duplicate what the labels already say. A faint y grid helps the eye carry a value across; a strong one competes with the data.

Hand-drawn notes showing a cluttered chart stripped of spines, heavy gridlines and a legend until only the data and its labels remain.

5. Say it on the chart

ax.set_title(f"Every degree costs about {abs(m):.1f} cups",
             loc="left", fontsize=14)
ax.text(0, 1.02, f"120 orders, March 2026  ·  r = {r:.2f}",
        transform=ax.transAxes, fontsize=9.5, color="#9a9a9a")

ax.annotate(f"below 25°C: {cold.mean():.0f} cups on average",
            xy=(22, cold.mean()), xytext=(-8, 28), textcoords="offset points",
            fontsize=9.5, color="#74dfa2",
            arrowprops=dict(arrowstyle="->", color="#74dfa2"))

The title is the claim, left-aligned so it reads as a headline. The subtitle carries the sample size and the strength of the relationship — in transAxes coordinates, so it stays pinned under the title whatever the data does (lesson 12). The annotation gives the reader one concrete number to hold onto.

A reader should be able to get the point from the title alone, then find it confirmed in the chart. That's the test.

6. Export for where it's going

fig.savefig("temp-vs-cups.svg", bbox_inches="tight")            # print, slides
fig.savefig("temp-vs-cups.png", dpi=200, bbox_inches="tight")   # web

And look at the file, at its final size, on the device it's going to. A figure that reads on a 27-inch monitor can be unreadable projected at the back of a room, and the only way to know is to check (lessons 4 and 18).

The checklist

  1. Can a reader state the claim after five seconds?
  2. Does the title say the finding, not the columns?
  3. Is the sample size visible somewhere?
  4. Both axes labelled, with units?
  5. Does every remaining element earn its place?
  6. Bars from zero; dense scatters with alpha; log axes labelled as log?
  7. Does it survive greyscale?
  8. Have you looked at the exported file, at its real size?

That's the track. The matplotlib is the easy half — Figure → Axes → Artists, and the question is always which object owns the thing you want to change. The hard half is deciding what the chart is for and then removing everything that isn't.

Practice

Write it yourself. The answer is there when you want it.

Putting the kettle on…

Starting up…

Write it yourself

not graded

Take one sentence — hotter days sell less chai — and check it before you draw it: print the slope and the correlation, then the mean cups below 25°C against above 35°C with the counts beside them. Then make the chart that says it. Scatter the points, lay the fitted line over them, take off the top and right spines, leave a faint y grid, label both axes. Put the finding in a left-aligned title, the sample and the correlation in small grey type above the plot, and annotate the cold end with its average. Finish with fig.

Write something and press Run — the output appears here.

Your turn

4 exercises. Write the code yourself, then press Check — a nudge and the answer are there if you want them.

The claim needs a number a person can repeat. Return the fitted slope and intercept of cups against temp, each rounded to 2 decimal places, as a list.

your answer

Try to break the claim a second way. Return [n_cold, mean_cold, n_hot, mean_hot] for days below 25°C and above 35°C, means rounded to 1 decimal place.

your answer

Strip the furniture: hide the top and right spines and return the names of the ones still visible, sorted.

your answer

Put the finding in the title — computed from the fit, not typed — and a subtitle in axes coordinates at (0, 1.02). Return [ax.get_title(), ax.texts[0].get_text()].

your answer