The previous lesson ended with the answers in tables: Saturday 130 units, April 168, Faust with 11.9% conversion. Correct — but Ana doesn't think in tables, and her brain, like everyone's, processes one bar being taller than another in milliseconds and a column of numbers in... considerably longer. Matplotlib is the reference visualization library in Python: the oldest, the most flexible, and the foundation on which almost all the others are built. This lesson teaches you to pick the chart based on the question, to assemble it piece by piece and not to lie along the way — ending with a dashboard of the semester's numbers hanging in Papyrus's back room.
Contents
- Why visualize: the Anscombe lesson
- Anatomy of a figure
plt.plot: the monthly evolution (and Sant Jordi)barandbarh: comparing titleshist: the distribution of amountsscatter: price against unitspieand when not to use itsubplots: the store's 2×2 dashboard- Style and readability
- The
df.plotshortcut andsavefig - Which chart for which question
Why visualize: the Anscombe lesson
In 1973 the statistician Francis Anscombe built four datasets with the same statistics — same mean of x and y, same variance, same regression line — that, when drawn, turn out to be four completely different stories: a linear cloud, a curve, a perfect line with one rebel point, and a degenerate case where a single point manufactures all the correlation. The moral is twofold and governs this lesson:
- Numerical summaries hide the shape. 11-03's
describe()can't tell those four worlds apart; one glance can. - Always draw before concluding. Especially before fitting the 11-05 model: Anscombe's regression was "correct" in all four cases and only made sense in one.
Anatomy of a figure
Matplotlib distinguishes the canvas (Figure) from each drawing area (Axes — a set of axes with its data, title and legend; a Figure can contain several). Almost every "where do I put the title?" problem is solved by knowing which of the two each piece belongs to.
flowchart TB
F["Figure (the canvas)"] --> A["Axes (the drawing area)"]
A --> T["title — set_title"]
A --> X["X axis — set_xlabel, ticks"]
A --> Y["Y axis — set_ylabel"]
A --> D["the data — plot, bar, scatter..."]
A --> L["legend — legend (uses the label=)"]
The recommended way to work is the object-oriented interface: plt.subplots() hands you the fig, ax pair and from there everything is a method on ax — no hidden global state:
import matplotlib.pyplot as plt # the plt alias is convention, like np and pd
fig, ax = plt.subplots() # 1 Figure with 1 Axes
ax.set_title("My first chart")
plt.show() # in Jupyter it shows by itself; in a .py script, show() opens the windowplt.plot: the monthly evolution (and Sant Jordi)
Lines tell evolution over time. The data: the monthly units that groupby gave us in 11-03.
months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun"]
units = [62, 58, 71, 168, 84, 77]
fig, ax = plt.subplots(figsize=(8, 4))
ax.plot(months, units, marker="o", label="units/month")
ax.annotate("Sant Jordi (23 Apr)", xy=("Apr", 168), xytext=("Feb", 150),
arrowprops=dict(arrowstyle="->"))
ax.set_title("Papyrus — units sold per month (2026)")
ax.set_xlabel("Month")
ax.set_ylabel("Units")
ax.legend()
plt.show()The marker="o" flags the real data points (there are only 6; the line between them is a visual guide, not a measurement), and annotate pins the arrow on the peak: April, 168 units, more than double a normal month. It's the same number from 11-02's axis=0 and 11-03's groupby — but now you can see it, and Ana understands it without knowing Python.
bar and barh: comparing titles
Bars compare categories. Units per title (11-03):
titles = ["Don Quixote", "The Odyssey", "Hamlet", "Faust"]
title_units = [182, 154, 121, 63]
fig, ax = plt.subplots()
ax.bar(titles, title_units)
ax.set_ylabel("Units (Jan-Jun)")
ax.set_title("Sales per title")
plt.show()Two tips worth their weight in gold: sort the bars by value (they already are: that's 11-03's sort_values working for the chart) and, with long names or many categories, use barh (horizontal bars): the labels read straight through without tilting your head — ax.barh(titles[::-1], title_units[::-1]) (reversed so the biggest ends up on top).
And an honesty rule: a bar chart's axis starts at 0. A bar starting at 100 turns a 5% difference into a visual chasm. Matplotlib gets this right by default; don't "fix" it.
hist: the distribution of amounts
A histogram slices a range into intervals (bins) and counts how many values fall into each one: it's the shape of the data, the thing describe() summarizes blindly.
df = pd.read_csv("data/sales_2026.csv", parse_dates=["date"])
fig, ax = plt.subplots()
ax.hist(df["amount"], bins=20, edgecolor="white")
ax.set_xlabel("Sale amount (EUR)")
ax.set_ylabel("Number of sales")
ax.set_title("Distribution of amounts")
plt.show()The drawing confirms 11-01's suspicion: a mountain concentrated between 9 and 16 EUR (the single-copy sales) and a long tail to the right — the Sant Jordi bundles up to 63.60 EUR — which is what separates the mean (15.03) from the median (12.50). Try bins=5 and bins=50: too few bins hide the shape, too many turn it into noise. There is no magic number; you feel it out.
scatter: price against units
The scatter plot looks for a relationship between two numeric variables:
prices = [15.90, 12.50, 9.95, 21.00]
title_units = [182, 154, 121, 63]
fig, ax = plt.subplots()
ax.scatter(prices, title_units, s=80)
for p, u, t in zip(prices, title_units, ["Don Quixote", "The Odyssey", "Hamlet", "Faust"]):
ax.annotate(t, (p, u), xytext=(5, 5), textcoords="offset points")
ax.set_xlabel("Price (EUR)")
ax.set_ylabel("Units sold")
ax.set_title("Does what costs more sell less?")
plt.show()Four points and one story: there's no clean line (Don Quixote, at 15.90, is the bestseller), but Faust sits alone in its corner — the most expensive and the worst seller, consistent with its 11.9% conversion from 11-03. With only 4 points this is a clue, not a law: exactly the caution we'll need when fitting the 11-05 regression on this same pair of variables.
pie and when not to use it
fig, ax = plt.subplots()
ax.pie(title_units, labels=["Don Quixote", "The Odyssey", "Hamlet", "Faust"],
autopct="%1.0f%%")
ax.set_title("Share of units per title")
plt.show()The honest warning: the human eye compares lengths far better than angles. Is The Odyssey's slice (30%) bigger than Hamlet's (23%)? In the pie it's hard work; in a bar it's obvious. The pie only works with few categories (2-4), parts of a whole, and big differences — the store/web split (60/40) passes the cut; almost everything else is better as bars. If in doubt, bars.
subplots: the store's 2×2 dashboard
Everything together: the dashboard Ana wants to print and hang in the back room.
fig, axs = plt.subplots(2, 2, figsize=(11, 7)) # axs is a 2x2 array of Axes (11-02!)
axs[0, 0].plot(months, units, marker="o")
axs[0, 0].set_title("Units per month")
axs[0, 1].barh(titles[::-1], title_units[::-1])
axs[0, 1].set_title("Units per title")
axs[1, 0].hist(df["amount"], bins=20, edgecolor="white")
axs[1, 0].set_title("Distribution of amounts (EUR)")
days = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
axs[1, 1].bar(days, [48, 52, 55, 60, 78, 130, 97])
axs[1, 1].set_title("Units per day of the week")
fig.suptitle("Papyrus — first half of 2026 dashboard", fontsize=14)
fig.tight_layout()
plt.show()subplots(2, 2) returns the Figure and a 2×2 matrix of Axes indexed like the 11-02 arrays (axs[1, 1] = bottom right). suptitle titles the whole canvas and tight_layout() repositions everything so nothing overlaps — without it, the top titles trample the bottom axes.
Style and readability
- Labels, ALWAYS. A chart without
xlabel/ylabel/titleis a riddle. If the axis is euros, say so; "I know what it is" expires in a week. - Colors with intent. The default blue is fine; the rainbow is not. Use one neutral color for everything and a single accent for the data point that matters (Faust in the scatter, April in the line). Color should mean something or not be there. And remember that ~8% of men confuse red and green: don't hang the only distinction on that pair.
- Rotated dates. With many dates on the X axis they pile up:
ax.tick_params(axis="x", rotation=45)orfig.autofmt_xdate(). - Less is more. Every element (grid, border, single-series legend) must earn its ink.
ax.grid(alpha=0.3)at most.
The df.plot shortcut and savefig
pandas wraps Matplotlib: every Series or DataFrame has .plot, ideal for quick looks while exploring:
df.groupby(df["date"].dt.month)["units"].sum().plot(kind="bar", title="Units per month")
df["amount"].plot(kind="hist", bins=20)One line from the groupby to the drawing. It returns the usual ax, so you can keep tweaking (ax.set_ylabel(...)). To explore, df.plot; for the final, made-to-measure figure, the fig, ax interface.
And the last step — Ana doesn't run Python, Ana receives a PNG:
Into M6's reports/ directory, like the CSVs from 11-03. dpi=150 gives reasonable print quality and bbox_inches="tight" trims dead margins. Important: call savefig before show() — after showing, the figure may come out blank.
Which chart for which question
| Type of question | Chart | Papyrus example |
|---|---|---|
| How does it evolve over time? | plot (lines) |
Units per month, the Sant Jordi peak |
| How do categories compare? | bar / barh |
Sales per title, per weekday |
| What shape does my data have? | hist |
Distribution of amounts |
| Are two variables related? | scatter |
Price vs units |
| What share of the whole? (few parts) | pie (with caution) |
Store/web split |
| Several questions at once | subplots |
The 2×2 dashboard |
When this table falls short: seaborn (elegant statistical charts on top of Matplotlib, in one line what here takes ten) and plotly (interactive charts with zoom and tooltips, perfect for the M10 website) are the natural next stops. Everything learned here — figures, axes, chart types, honesty — transfers as is.
Common Mistakes and Tips
- Forgetting
plt.show()in scripts. In Jupyter the figure appears on its own; in a.pywithoutshow()the program ends showing nothing. (Andsavefiggoes beforeshow.) - Reusing the figure by accident. Drawing two charts in a row on the same
axoverlays them. Every new figure gets its ownfig, ax = plt.subplots(). - Truncating the Y axis on bars. The classic of lying charts: tiny differences look enormous. Bars start at 0; on lines, cropping is acceptable if it's flagged.
- Too many series on one chart. Five crossing lines are spaghetti. Better
subplotswith one per panel, or highlight one and dim the rest in grey. - Axes without units. "168 what? Euros, units, visits?" Every report that leaves
reports/must survive without you standing next to it explaining.
Exercises
- Draw the monthly evolution of revenue (sum of
amountper month) as a line with markers, annotating Sant Jordi, starting from the corresponding 11-03groupby. Label the axes with units (EUR) and save it toreports/monthly_revenue.png. - Make a grouped bar chart with units per title and channel (the 11-03
unstack()table: store 121/38/74/79, web 61/25/47/75). Hint:table.T.plot(kind="bar")does it in one line; interpret the story it tells. - The lesson's 2×2 dashboard uses the same color for everything. Modify it so that only the Saturday bar (in the days chart) and the April point (in the monthly line) get an accent color, and the rest stays grey. Hint:
baraccepts a list of colors, and you can overlay anax.plotof a single point.
Solutions
-
The curve traces the units one — peak in April — because the average basket barely changes between months. Confirming that two metrics tell the same story is a finding too.rev = df.groupby(df["date"].dt.month)["amount"].sum() fig, ax = plt.subplots(figsize=(8, 4)) ax.plot(["Jan", "Feb", "Mar", "Apr", "May", "Jun"], rev.values, marker="o") ax.annotate("Sant Jordi", xy=("Apr", rev[4]), xytext=("Feb", rev[4] * 0.9), arrowprops=dict(arrowstyle="->")) ax.set_title("Papyrus — monthly revenue 2026") ax.set_xlabel("Month"); ax.set_ylabel("Revenue (EUR)") fig.savefig("reports/monthly_revenue.png", dpi=150, bbox_inches="tight") plt.show() -
The story: on almost every title the store bar wins... except on The Odyssey, where the web nearly matches the store (75 vs 79) and clearly beats web Quixote. The 11-03 finding, now visible in one shot.table = df.groupby(["channel", "title"])["units"].sum().unstack() ax = table.T.plot(kind="bar", figsize=(8, 4)) # .T: titles on the X axis, one bar per channel ax.set_ylabel("Units"); ax.set_title("Sales per title and channel") ax.tick_params(axis="x", rotation=0) plt.show() -
Grey mutes the secondary and the accent steers the eye exactly to the two numbers holding up Ana's decision: Saturday and Sant Jordi. That "dim to highlight" is probably the communication trick with the highest return per line of code in the whole lesson.colors = ["#9e9e9e"] * 7 colors[5] = "#d62728" # Saturday, index 5 axs[1, 1].bar(days, day_units, color=colors) axs[0, 0].plot(months, units, marker="o", color="#9e9e9e") axs[0, 0].plot(["Apr"], [168], marker="o", color="#d62728", markersize=10)
Conclusion
You now know how to turn Papyrus's numbers into stories: lines for evolution (with the arrow pinned on Sant Jordi), bars for comparing titles and days, histograms to see the shape describe() hides, scatter to suspect relationships — with Faust alone in its expensive corner —, the pie chart out on bail, and the 2×2 dashboard saved as a PNG in reports/ for the back room. And the principle governing it all, Anscombe's legacy: draw before concluding, because summaries hide the shape. That advice is about to become immediately practical: the question still standing — how much stock to order for next Sant Jordi? — isn't answered by describing the past, but by projecting it. Learning patterns from the history to predict what's coming is machine learning, and the module's final lesson does it for real, with scikit-learn, two complete cases, and the honest warnings marketing usually skips.
Python Programming Course
Module 1: Introduction to Python
- Introduction to Python
- Setting Up the Development Environment
- Python Syntax and Basic Data Types
- Variables and Constants
- Basic Input and Output
- Virtual Environments and Package Management
Module 2: Control Structures
Module 3: Functions and Modules
- Defining Functions
- Function Arguments
- Lambda Functions
- Modules and Packages
- Standard Library Overview
Module 4: Data Structures
Module 5: Object-Oriented Programming
Module 6: File Handling
Module 7: Error and Exception Handling
- Introduction to Exceptions
- Handling Exceptions
- Raising Exceptions
- Custom Exceptions
- Best Practices and Error Logging
Module 8: Advanced Topics
- Type Hints
- Decorators
- Generators
- Context Managers
- Concurrency: Threads and Processes
- Asyncio for Asynchronous Programming
Module 9: Testing and Debugging
- Introduction to Testing
- Unit Testing with unittest
- Testing with pytest
- Test-Driven Development
- Debugging Techniques
- Using pdb for Debugging
Module 10: Web Development with Python
- Introduction to Web Development
- Flask Framework Fundamentals
- Building REST APIs with Flask
- Introduction to Django
- Building Web Applications with Django
Module 11: Data Science with Python
- Introduction to Data Science
- NumPy for Numerical Computing
- Pandas for Data Manipulation
- Matplotlib for Data Visualization
- Introduction to Machine Learning with scikit-learn
