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.

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.

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.

See it run

The lesson's code, ready to run and to fiddle with.

Putting the kettle on…

Starting up…

Worked example

not graded

Already written and ready to go — press Run to see what it does, then change a number, a column name, anything, and run it again.

trydeleting the fitted line and seeing how much harder the title is to believe.

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