Capstone: a figure fit to publish
One claim, one chart, and everything that does not serve it removed
np.polyfitax.annotateax.spinesax.fill_betweensavefigfigsizeWatch it happen
Play it through, or step back and forth yourself.
"Every degree costs 2.3 cups"polyfit, corrcoef, group meansscatter + fitted linespines off, ticks thinnedtitle, annotation, direct labelsfigsize, dpi, bbox_inchesOne 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.1Slope, 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") # webAnd 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
- Can a reader state the claim after five seconds?
- Does the title say the finding, not the columns?
- Is the sample size visible somewhere?
- Both axes labelled, with units?
- Does every remaining element earn its place?
- Bars from zero; dense scatters with alpha; log axes labelled as log?
- Does it survive greyscale?
- 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 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.
trydeleting the fitted line and seeing how much harder the title is to believe.
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.
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.
Strip the furniture: hide the top and right spines and return the names of the ones still visible, sorted.
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()].
