Capstone: a dashboard
Four questions, four panels, one figure that fits on a page
subplot_mosaicresamplegroupbybar_labelconstrainedsavefigWatch it happen
Play it through, or step back and forth yourself.
four questions, four panelsplt.subplot_mosaic([[…]]).resample("D").sum(), .groupby(…)plot / barh / hist / scattersort, label, annotatelayout="constrained", savefigOne figure that answers four questions about a month of trade: is business growing, what sells, how long people wait, and what the weather does to any of it. Every stage is something from an earlier lesson.
The idea
You have orders — 120 orders over March, with a timestamp, a temperature, a cup count, a wait time and an item. Build one figure that answers four questions about the month.
1. Decide the questions
Before any code. A dashboard assembled from whichever charts were easiest is a wall of charts; four questions with four answers is a page somebody reads.
- Is trade growing? → cups per day, as a line.
- What sells? → total cups per item, as a sorted
barh. - How long do people wait? → a histogram of
wait. - Does the weather matter? → temperature against cups, as a scatter.
Note that the chart type falls out of the question. Over time → line. Across categories → bars. One column's shape → histogram. Two columns together → scatter. That's lessons 5–8 in one sentence.
2. Lay it out
fig, axd = plt.subplot_mosaic(
[["trend", "trend"],
["items", "wait"],
["temp", "temp"]],
figsize=(10, 9),
layout="constrained",
)The trend gets the full width because it's the headline. Nested lists rather than a string, because the names are words (lesson 14).
3. Reshape, then draw
daily = orders.set_index("when")["cups"].resample("D").sum()
axd["trend"].plot(daily.index, daily.values)
by_item = orders.groupby("item")["cups"].sum().sort_values()
axd["items"].barh(by_item.index, by_item.values)
axd["wait"].hist(orders["wait"], bins=12)
axd["temp"].scatter(orders["temp"], orders["cups"], s=14, alpha=0.5)Each is one line of drawing after one line of reshaping. Note sort_values() ascending on the bars — barh counts upwards, so ascending puts the biggest on top (lesson 7).
4. Make each panel say something
A panel with a bare column name as its title makes the reader do the work. Put the finding in each title:
axd["trend"].set_title(f"{int(daily.sum()):,} cups over {len(daily)} days",
loc="left", fontsize=11)
axd["items"].set_title("Plain chai is nearly a third of sales", loc="left", fontsize=11)And add the things that turn a chart into a claim (lesson 12):
# the busiest day, found not typed
best = daily.idxmax()
axd["trend"].annotate(f"best day: {int(daily.max())}",
xy=(best, daily.max()),
xytext=(10, -18), textcoords="offset points",
arrowprops=dict(arrowstyle="->", color="#9a9a9a"))
# median rather than mean, because the wait times are skewed (lesson 8)
axd["wait"].axvline(orders["wait"].median(), linestyle="--", color="#74dfa2")
# the slope, in units a person can repeat
m, b = np.polyfit(orders["temp"], orders["cups"], 1)
xs = np.array([orders["temp"].min(), orders["temp"].max()])
axd["temp"].plot(xs, m * xs + b, linestyle="--", color="#66aaf9")textcoords="offset points" is worth knowing: it places the label a fixed distance from the target in points, so it stays put whatever the data range does.
5. Fit it, then save it
fig.suptitle("Chai stall — March", fontsize=15)
fig.savefig("dashboard.png", dpi=150, bbox_inches="tight")layout="constrained" was set at creation, so nothing collides (lesson 15), and bbox_inches="tight" keeps the suptitle from being cropped (lesson 4).
What to check before you send it
- Every axis labelled, with units.
- Every panel title states a finding, not a column name.
- Bars sorted; bars start at zero.
- The dense scatter has
alpha. - Nothing overlaps in the saved file, not just the preview.
- One colour doing the emphasis, not five competing.
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.
trymoving "temp" into the middle row and giving "wait" the full width instead.
Your turn
4 exercises. Write the code yourself, then press Check — a nudge and the answer are there if you want them.
Panel 1's data. Resample orders to daily total cups and return [len(daily), int(daily.max()), str(daily.idxmax().date())].
Panel 2's data. Total cups per item, sorted so that a barh puts the biggest bar on top. Return the result as a dict.
Panel 4's finding. Fit a straight line through temp against cups with np.polyfit(..., 1) and return the slope, rounded to 2 decimal places — the cups lost per degree.
Assemble the layout: a mosaic with trend across the top, items and wait beneath, and temp across the bottom, using constrained layout. Return [sorted(axd), len(fig.axes), fig.get_layout_engine().__class__.__name__].
