Matplotlib·Capstone·22 min·0/4 exercises

Capstone: a dashboard

Four questions, four panels, one figure that fits on a page

subplot_mosaicresamplegroupbybar_labelconstrainedsavefig

Watch it happen

Play it through, or step back and forth yourself.

1
Askfour questions, four panels
decide before you draw
2
Lay outplt.subplot_mosaic([[…]])
lesson 14
3
Reshape.resample("D").sum(), .groupby(…)
lesson 17
4
Drawplot / barh / hist / scatter
lessons 5–8
5
Make readablesort, label, annotate
lessons 9–12
6
Fit and savelayout="constrained", savefig
lessons 15 and 4

One 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.

  1. Is trade growing? → cups per day, as a line.
  2. What sells? → total cups per item, as a sorted barh.
  3. How long do people wait? → a histogram of wait.
  4. 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 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.

trymoving "temp" into the middle row and giving "wait" the full width instead.

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.

Panel 1's data. Resample orders to daily total cups and return [len(daily), int(daily.max()), str(daily.idxmax().date())].

your answer

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.

your answer

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.

your answer

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__].

your answer