
CSCI 1109 — Practical Data Science
By the end of this module, you should be able to:
🙇 Exploratory Data Analysis (EDA)
🗣️ Narrative / Explanatory visualization
Key idea:
EDA is like a lab notebook; narrative viz is like a short article.
| Aspect | EDA scratchpad | Narrative figure |
|---|---|---|
| Audience | You (analyst) | Others (non-expert or expert) |
| Number of plots | Many | Few (often 1–3) |
| Titles | “tmp”, “check this”, or missing | Clear, descriptive, question/answer |
| Axes & scales | Change often, experimental | Consistent, justified |
| Colour & style | Default / ad hoc | Intentional, consistent legend |
| Annotations | Rare | Strategic: callouts, highlights |
| Story structure | None | Beginning → middle → end |
Think of building a mini storyboard (see also Healy (2019)):

When you see a plot in this course (or anywhere else), run this quick SEE checklist:
You don’t have to write these down every time, but the goal is that this three-beat rhythm (“Scan → Examine → Explain”) becomes automatic.
Imagine a dashboard slide:
import pandas as pd
import matplotlib.pyplot as plt
# Tiny real difference, but we'll exaggerate it visually
data = pd.DataFrame({
"period": ["Before new menu", "After new menu"],
"avg_tip_pct": [18.2, 19.1] # less than 1 percentage point difference
})
fig, ax = plt.subplots(figsize=(6, 4))
# Bar chart
bars = ax.bar(data["period"], data["avg_tip_pct"])
# Misleading y-axis: start high so the difference looks huge
ax.set_ylim(17, 20) # <- truncation that exaggerates the difference
# Over-confident title
ax.set_title("Our new menu dramatically increased tips", fontsize=14)
ax.set_ylabel("Average tip (%)")
# No mention of sample size, no error bars, no context
# (We could add those in a 'fixed' version, but we deliberately don't.)
# Add value labels on top of bars (helps sell the story)
for bar, val in zip(bars, data["avg_tip_pct"]):
ax.text(
bar.get_x() + bar.get_width() / 2,
val + 0.05,
f"{val:.1f}%",
ha="center",
va="bottom",
fontsize=10
)
# Remove grid to keep the focus on the bars
ax.grid(False)
plt.tight_layout()
plt.show()
Problems:
Anti-example lesson
A clean-looking chart is not automatically honest or informative (Tufte 2001).
| total_bill | tip | tip_pct | day | time | size | |
|---|---|---|---|---|---|---|
| 0 | 16.99 | 1.01 | 5.944673 | Sun | Dinner | 2 |
| 1 | 10.34 | 1.66 | 16.054159 | Sun | Dinner | 3 |
| 2 | 21.01 | 3.50 | 16.658734 | Sun | Dinner | 3 |
| 3 | 23.68 | 3.31 | 13.978041 | Sun | Dinner | 2 |
| 4 | 24.59 | 3.61 | 14.680765 | Sun | Dinner | 4 |
Question (vague):
“How does tip % behave as bills get larger?”
Initial EDA pass:
EDA observations
Before going narrative, check some simple summaries:
| time | count | mean | median | |
|---|---|---|---|---|
| 0 | Lunch | 68 | 16.4 | 15.4 |
| 1 | Dinner | 176 | 16.0 | 15.5 |
✏️
Now create a single narrative plot for the restaurant stakeholders.
Design choices
fig, ax = plt.subplots(figsize=(7, 4))
sns.scatterplot(
data=tips,
x="total_bill",
y="tip_pct",
hue="time",
alpha=0.5,
ax=ax,
legend=True,
)
# Focus on the main range where most customers live
ax.set_xlim(0, 60)
ax.set_ylim(0, 40)
ax.set_title("Most customers tip around 15–20% on typical dinner bills")
ax.set_xlabel("Total bill ($)")
ax.set_ylabel("Tip (% of bill)")
# Add a horizontal band for 15–20% as a “normal tipping” region
ax.axhspan(15, 20, color="grey", alpha=0.1)
ax.text(
5, 21.5,
"Common tipping range\n(15–20%)",
fontsize=9,
va="bottom"
)
# Optional annotation: call out high tips on mid-sized dinner bills
ax.annotate(
"Some dinner parties tip 25%+ on $30–$50 bills",
xy=(40, 26),
xytext=(48, 32),
arrowprops=dict(arrowstyle="->"),
fontsize=9,
)
ax.legend(title="Time of day", loc="lower right")
plt.tight_layout()New question:
“When do we see the highest average tip %: weekday vs weekend, lunch vs dinner?”
First, build a small summary table:
| day | time | count | mean_pct | |
|---|---|---|---|---|
| 0 | Thur | Lunch | 61 | 16.1 |
| 1 | Thur | Dinner | 1 | 16.0 |
| 2 | Fri | Lunch | 7 | 18.9 |
| 3 | Fri | Dinner | 12 | 15.9 |
| 4 | Sat | Lunch | 0 | NaN |
| 5 | Sat | Dinner | 87 | 15.3 |
| 6 | Sun | Lunch | 0 | NaN |
| 7 | Sun | Dinner | 76 | 16.7 |
fig, ax = plt.subplots(figsize=(7, 4))
sns.barplot(
data=summary_day_time,
x="day",
y="mean_pct",
hue="time",
ax=ax,
)
ax.set_ylabel("Average tip (%)")
ax.set_xlabel("Day of week")
ax.set_title("Weekend dinners bring the highest tip percentages")
# Add labels on bars for quick reading
for container in ax.containers:
ax.bar_label(container, fmt="%.1f", padding=2, fontsize=8)
ax.legend(title="Time of day", loc="lower right")
plt.tight_layout()Design choices
Before trusting your visual story, ask:
Narrative visualizations can be powerful — and misleading:
Simple mitigations:
Where does this show up in real life?
Connection to later work:
MCQ: Which statement best describes a narrative visualization?
Short answer: Name two differences between EDA plots and narrative plots.
MCQ: You create a bar chart of average tip % by day, but one day has only 3 observations.
What’s the best next step?
Short answer: Suppose your scatterplot suggests a weak trend, but your title says
“Massive increase in tips after new menu”.
What’s one change you could make to make this more honest?
