Chapter 17: The storytelling capstone

Open In Colab Download Notebook

The department head has ten minutes before her next meeting. You have three charts and twelve numbers, and a strong opinion about which courses need help. She asks one question: “So what do I do?” Your charts are correct. None of them answer that.

Chapters 14 through 16 taught you how to build a chart and how to make it honest. This chapter is about the step before any of that: knowing who the chart is for and what they need to decide, diagnosing why a draft chart is not landing, and recognising the moment you stop exploring and start explaining. It closes the Data Visualization part by putting every judgment call from the last three chapters, and both libraries, to work on one real case, start to finish. Chapter 18 picks up the engineering practices, uv, ruff, git, testing, that make analysis like this reproducible.

Callout markers: book cover page.

By the end of Chapter 17 you will be able to:

# Skill Covered in
1 Name a chart’s audience, decision, and medium, and let medium decide the library Sec. 1
2 Diagnose a failing chart with the Trifecta Checkup: content, story, or visual form Sec. 2
3 Decide explore vs explain and change how you build a chart accordingly Sec. 3
4 Rebuild one explanatory chart in matplotlib and lets-plot for two different mediums Sec. 4
5 Produce one explanatory chart, backed by a one-paragraph brief, from a real dataset Capstone

1. Audience, decision, medium

Every chart decision so far assumed the audience was you: fast defaults, all the data, nothing hidden. The moment someone else needs to act on what you found, three questions come before any chart type: who is this for, what do they need to decide, and how will they see it? Skip these and even a perfectly honest, perfectly decluttered chart can still miss.

import pandas as pd

df = pd.read_csv("data/university_analytics.csv")

# Same three-course, three-Fall-cohort slice as Chapters 14 to 16: the
# department head's question is about these students specifically.
courses = ["Machine Learning", "Data Structures", "Python Programming"]
semesters = ["Fall 2022", "Fall 2023", "Fall 2024"]
results = df[df["course"].isin(courses) & df["semester"].isin(semesters)].copy()
results.head()
student_id cohort program gender region guardian has_internet course_id course semester enrollment_date study_hours attendance_pct midterm_score final_score project_score final_grade passed
0 S0001 2023 Information Technology F South Father True C01 Python Programming Fall 2023 2023-09-04 10.8 88.6 49.9 58.2 61.6 C True
2 S0001 2023 Information Technology F South Father True C03 Data Structures Fall 2023 2023-09-04 23.6 71.5 54.1 60.9 79.8 C True
4 S0001 2023 Information Technology F South Father True C05 Machine Learning Fall 2023 2023-09-04 16.2 63.2 48.4 54.2 51.8 D True
6 S0002 2023 Data Science M West Mother True C01 Python Programming Fall 2023 2023-09-04 17.4 67.0 93.6 60.5 67.0 B True
8 S0002 2023 Data Science M West Mother True C03 Data Structures Fall 2023 2023-09-04 30.4 84.9 67.4 93.5 70.6 B True

Key Concept: Audience, decision, medium

State three things before opening a plotting library: who will read this chart, what decision they need to make after seeing it, and how they will see it (a live presentation, a slide they read alone, a printed appendix). The same numbers, shown to a department head deciding whether to fund a study-hall program, need a different chart than the same numbers shown to a student checking their own grade.

Medium has a second consequence beyond how the chart is framed: it decides which library you reach for. matplotlib (Chapter 14) renders to a static image, the right choice for a printed report or a slide someone reads without you in the room. lets-plot (Chapter 15) renders to interactive HTML by default, the right choice for a live meeting or a notebook the reader scrolls through and hovers over themselves. Neither library is more correct: the medium decides, the same way it decided in Chapter 16, Section 5.

Activity 1 - Name the audience before you plot

The department head is deciding whether to fund a mandatory study-hall program next term.

1. Fill in a dict: brief = {“audience”: …, “decision”: …, “medium”: …}.
2. audience: who reads this chart.
3. decision: the one thing they do differently depending on what the chart shows.
4. medium: how they will see it (live meeting, slide, printed report).

Expected: three short, specific answers, not “everyone” or “management”.

# TODO: fill in the brief
brief = {"audience": ..., "decision": ..., "medium": ...}
brief
{'audience': Ellipsis, 'decision': Ellipsis, 'medium': Ellipsis}

2. The Trifecta Checkup

Here is a first attempt at answering the department head’s question. Something about it is not working, but “make it prettier” is rarely the actual fix:

import matplotlib.pyplot as plt

course_scores = results.groupby("course")["final_score"].mean()

fig, ax = plt.subplots(figsize=(5, 3))
ax.bar(course_scores.index, course_scores.values, color="#4477AA")
ax.set_ylabel("Mean final score")
ax.set_title("Final score by course")
fig.tight_layout()

Key Concept: The Trifecta Checkup

Berinato’s Trifecta Checkup diagnoses a failing chart with three questions, asked in order: (1) Content: is the underlying analysis right, is this even the correct number to show? (2) Story: is there one clear message, could a reader state it in one sentence? (3) Visual form: is the chart type and design right for that message? Fixing (3) before checking (1) is polishing the wrong number. The chart above actually has real content: Machine Learning students do score noticeably lower than Python Programming students. It still fails the content check, for a subtler reason: “course” is not something the department head can act on. She is deciding whether to fund a study-hall program, an intervention aimed at how students study, not at which courses exist. Correct content and useful content are two different questions.

Fixed for content: not because the course numbers were wrong, but because they were not actionable. Group students by how much they studied instead of by which course they took, since study hours is the variable a study-hall program can actually change:

results["study_bucket"] = pd.qcut(results["study_hours"], 3, labels=["Low", "Mid", "High"])
score_by_bucket = results.groupby("study_bucket", observed=True)["final_score"].mean()

fig, ax = plt.subplots(figsize=(5, 3))
ax.bar(score_by_bucket.index, score_by_bucket.values, color="#4477AA")
ax.set_ylabel("Mean final score")
ax.set_ylim(0, 100)
ax.set_title("Final score by study-hours group")
fig.tight_layout()

Content is fixed. Visual form has its own, independent failure mode: the same correct numbers, drawn dishonestly. Here is the same chart with nothing changed except the y-axis limits:

fig, ax = plt.subplots(figsize=(5, 3))
ax.bar(score_by_bucket.index, score_by_bucket.values, color="#4477AA")
ax.set_ylabel("Mean final score")
ax.set_ylim(50, 68)
ax.set_title("Final score by study-hours group")
fig.tight_layout()

Common Mistake: A visual-form problem can hide behind correct content

Both charts above plot the exact same three numbers. The second one starts its y-axis at 50, the same truncation lie from Chapter 16, and it makes the Low-to-High gap look roughly three times larger than it is. Content and visual form are independent checks: this chart passes (1) and still fails (3). Run all three questions every time, even when an earlier one already passed.

Content and visual form both check out on the honest, zero-baseline version. Now the story check: does this chart state one message, or does the reader have to find it? Right now, nothing points at the answer: the department head still has to read three bar heights and work out the gap herself.

Activity 2 - Run the Trifecta Checkup

Diagnose the truncated-y-axis chart from Chapter 16’s capstone using the three questions.

1. Content: is the number plotted the right one for the question being asked?
2. Story: could you state the message in one sentence?
3. Visual form: is the chart type and axis honest?

Write one short sentence answering each question.

Expected: that chart’s problem is visual form (the y-axis), not content or story: the checkup should say so explicitly, in order.

# TODO: one sentence per question
content_check = "..."
story_check = "..."
visual_form_check = "..."

3. Explore vs explain

Every chart in Chapters 14 to 16 so far has been exploratory: fast, all the data, default styling, built to help you understand. The department head does not need to explore. She needs the one message, explained.

Key Concept: Explore, then explain

An exploratory chart is for you: fast, shows everything, default styling, built to find a pattern. An explanatory chart is for someone else: filtered to the one message, branded, annotated, built to make a decision easy. The transition point is the moment you can write the “so what” in one sentence. Until you can write that sentence, you are still exploring, no matter how polished the chart looks.

The study-hours chart passed content and visual form. What is missing is the explanatory layer: the branding from Chapter 16 and an annotation that states the message, not just shows it. The department head asked for a printed handout, so matplotlib is the right tool here:

from ark.plot.matplot_theme import configure_matplotlib_style

configure_matplotlib_style(font_size=10, fig_width=5)

gap = score_by_bucket["High"] - score_by_bucket["Low"]

fig, ax = plt.subplots(figsize=(6, 3.5))
colors = ["#DC2626" if b == "High" else "#ADB5BD" for b in score_by_bucket.index]
ax.bar(score_by_bucket.index, score_by_bucket.values, color=colors)
ax.set_ylabel("Mean final score")
ax.set_title(f"Students who study most score {gap:.0f} points higher")
for i, v in enumerate(score_by_bucket.values):
    ax.text(i, v + 1, f"{v:.0f}", ha="center")
fig.tight_layout()

Activity 3 - Write the “so what”

Write the one-sentence takeaway the chart above is making explicit.

1. State the point gap between the High and Low study-hours groups.
2. Say what the department head should do next, in the same sentence.

Expected: one sentence a reader could act on without looking at the chart at all.

so_what = "..."
so_what
'...'

4. One message, two tools

The chart above is right for a printed handout. Now the department head says she would rather review it live, in the meeting, hovering over each bar to see the exact numbers. Same audience, same decision, same message, a different medium, so a different library: lets-plot instead of matplotlib.

from lets_plot import LetsPlot, aes, geom_bar, ggplot, ggsize, labs, scale_fill_manual

from ark.plot.theme import modern_theme

LetsPlot.setup_html()

score_by_bucket_df = score_by_bucket.reset_index()

(
    ggplot(score_by_bucket_df, aes(x="study_bucket", y="final_score", fill="study_bucket"))
    + geom_bar(stat="identity")
    + scale_fill_manual(values=["#ADB5BD", "#ADB5BD", "#DC2626"])
    + labs(
        title=f"Students who study most score {gap:.0f} points higher",
        x="",
        y="Mean final score",
    )
    + modern_theme()
    + ggsize(500, 320)
)

Key Concept: The judgment transfers, the tool does not have to match

Every decision that made the matplotlib chart explanatory, one message, one colour reserved for emphasis, a title that states the finding, is unchanged here. Only the + layers changed. This is the payoff of Chapter 15’s grammar: scale_fill_manual() and labs() work the same way regardless of which chart they are attached to, so switching libraries for a medium change costs a rewrite of the drawing code, not a rethink of the message.

Capstone: the department head’s chart

Everything from this chapter, applied once, on one real question: does study time predict performance strongly enough to justify funding a study-hall program, and does attendance tell the same story?

Capstone exercise - The full judgment loop

Produce one explanatory chart and a written brief, start to finish:
  1. Audience, decision, medium: reuse the brief dict from Activity 1. Decide which medium fits: a live meeting or a printed handout.
  2. Content: bucket attendance_pct into three groups the same way as study_bucket, and compute mean final_score per group. Is the gap as large as the study-hours gap?
  3. Story: write the one-sentence “so what” before touching a plotting library.
  4. Visual form: build one chart, branded, that makes the message in step 3 the only thing a reader can take away. Use configure_matplotlib_style() for a printed medium, or lets-plot with modern_theme() for a live one, matching the medium from step 1.
  5. Caption: print the brief and the “so what” sentence beneath the chart.

Hint: pd.qcut(results[“attendance_pct”], 3, labels=[“Low”, “Mid”, “High”]) mirrors the study_bucket code from Section 2.

# TODO: audience/decision/medium brief (reuse Activity 1)
brief = {"audience": ..., "decision": ..., "medium": ...}

# TODO: content check: mean final_score by attendance bucket
score_by_attendance = ...

# TODO: story: the "so what" sentence
so_what = "..."

# TODO: visual form: one branded, annotated chart (matplotlib or lets-plot, matching brief["medium"])
...

print(brief)
print(so_what)
{'audience': Ellipsis, 'decision': Ellipsis, 'medium': Ellipsis}
...

Further reading

Resource Why it matters
Knaflic, C.N. (2015). Storytelling with Data. Wiley. Chapter 1 makes the audience-first case Section 1 is built on
Berinato, S. (2016). Good Charts. Harvard Business Review Press. The Trifecta Checkup, in full, alongside the “visual periodic table”
Dykes, B. (2019). Effective Data Storytelling. Wiley. The explore/explain distinction and the Data + Narrative + Visuals framework Section 3 draws on
Wilkinson, L. (2005). The Grammar of Graphics, 2nd ed. Springer. The formal grammar that makes Section 4’s judgment-transfers-across-tools argument true

Summary

Concept Key rule
Audience, decision, medium State all three before choosing a chart type
Medium picks the library Static and printable: matplotlib. Interactive and live: lets-plot
The Trifecta Checkup Diagnose in order: content, then story, then visual form: each is an independent check
Explore vs explain Exploratory is for you; explanatory is for someone else, filtered to one message
The “so what” Write the one-sentence takeaway before building the explanatory chart
One message, two tools The judgment (message, emphasis, title) transfers across libraries; only the drawing code changes

Next: 18-vscode-setup.qmd, setting up the engineering environment: editor, interpreter, and linting, that the rest of the book runs inside.