Charts with Matplotlib
Line, bar, scatter, histogram and box plots, labeled and honest.
Why it matters
A chart is often the only part of an analysis anyone else sees. A good one makes a pattern obvious in a second; a careless one hides it, or invents a pattern that isn't there. In the AI phases you'll chart loss curves, compare model scores and look at data before you train on it, so being quick and careful with Matplotlib pays off in every phase after this one.
What you'll be able to do
- Make a figure and axes, and draw line, bar, scatter, histogram and box plots on them
- Label every chart so a reader knows what they're looking at
- Choose a log scale, a zero baseline and small multiples on purpose
- Spot the unfinished-period trap before it fools you or your reader
What it covers
- A figure and its axes
- Five charts for five questions
- The month that isn't over
- Scales and baselines
- Revenue by store
- Small multiples and a legend
- Box plots: comparing spreads
- Worked example: a chart ready to share
A figure and its axes
Matplotlib is Python's standard charting library. Two objects do the work:
- The figure is the whole picture, like a sheet of paper.
- An axes is one chart on that sheet: the plotting area, its two axis lines, its labels.
plt.subplots() makes both at once. You then draw on the axes and label it:
import matplotlib.pyplot as plt months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun"] orders = [405, 416, 579, 735, 846, 848] fig, ax = plt.subplots(figsize=(6, 3.5)) ax.plot(months, orders, marker="o") ax.set_title("Trail-shoe orders, first half of 2024") ax.set_ylabel("orders") ax.set_ylim(bottom=0)
In the academy, every open figure appears under the output when the code finishes. On your own computer you'd add plt.show() to open a window, or fig.savefig("orders.png") to save a file.
The habit to build from day one: every chart gets a title and labeled axes. A chart without them makes the reader guess what they're looking at.
Five charts for five questions
Pick the chart from the question you're asking:
| Question | Chart | Matplotlib |
|---|---|---|
| How does it change over time? | line | ax.plot(x, y) |
| How do categories compare? | bar | ax.bar(names, values) |
| Do two numbers move together? | scatter | ax.scatter(x, y) |
| What values are common? | histogram | ax.hist(values, bins=20) |
| How do groups' spreads compare? | box plot | ax.boxplot(groups) |
Here are three of them on the practice shop's catalog:
import matplotlib.pyplot as plt import pandas as pd catalog = pd.read_csv("https://api.gradient.test/v1/datasets/catalog.csv") fig, axes = plt.subplots(1, 3, figsize=(12, 3.5)) by_category = catalog.groupby("category")["units_q2"].sum().sort_values() axes[0].barh(by_category.index, by_category.values) axes[0].set_title("Units sold in Q2") axes[1].scatter(catalog["price"], catalog["units_q2"], alpha=0.7) axes[1].set_title("Price against units sold") axes[1].set_xlabel("price ($)") axes[1].set_ylabel("units in Q2") axes[2].hist(catalog["price"], bins=12) axes[2].set_title("How prices are spread") axes[2].set_xlabel("price ($)") fig.tight_layout()
plt.subplots(1, 3) gives one row of three axes, in an array you index like a list. barh draws the bars sideways, which leaves room for long category names. tight_layout() nudges the three charts apart so their labels don't overlap.
import matplotlib.pyplot as plt fig, axes = plt.subplots(2, 3) print(axes.shape, len(fig.axes))
Show the answer
(2, 3) 6
Two rows and three columns of charts: axes is a 2 × 3 array, and the figure holds all six.
The month that isn't over
Here's the trap that catches the most people. The shoe-orders table holds daily orders from 2019 up to the day it was exported: June 15, 2025. Add the days up by month and plot them:
import matplotlib.pyplot as plt import pandas as pd daily = pd.read_csv("https://api.gradient.test/v1/datasets/shoe-orders.csv") daily["month"] = daily["date"].str[:7] # "2025-06-15" -> "2025-06" monthly = daily.groupby("month")["orders"].agg(["sum", "count"]) print(monthly.tail(3)) recent = monthly.loc["2024-01":] fig, ax = plt.subplots(figsize=(8, 3.5)) ax.plot(recent.index, recent["sum"], marker="o") ax.set_title("Monthly trail-shoe orders") ax.set_ylabel("orders") ax.set_ylim(bottom=0) ax.tick_params(axis="x", labelrotation=90)
The last point falls off a cliff. A manager would ask what went wrong in June. Nothing did: the count column shows June has only 15 days of data, against 30 or 31 for the others.
agg(["sum", "count"]) works out two summaries at once, so each month's total sits next to the number of days behind it. That's the check that catches the trap. Then either drop the unfinished month, or show it differently and say so on the chart.
The same 18 months of trail-shoe orders. Switch to bars and turn off the zero baseline: small differences suddenly look huge. Then hide the last month and the false collapse disappears.
Scales and baselines
Two settings change what a reader believes, so set them on purpose:
- Where the axis starts. A bar's length is its value, so bar charts must start at zero. A line chart can zoom in to show small wiggles, as long as the axis labels make the scale clear.
- Linear or log. When values span from 5 to 50,000, a normal axis squashes the small ones flat.
ax.set_yscale("log")spaces 10, 100, 1,000 and 10,000 evenly, so every value has room. It also means equal steps up are equal percentage growth, which suits sales and populations.
import matplotlib.pyplot as plt sellers = ["A", "B", "C", "D", "E", "F"] units = [12, 48, 150, 900, 2400, 31000] fig, (left, right) = plt.subplots(1, 2, figsize=(10, 3.2)) left.bar(sellers, units) left.set_title("Linear: most bars vanish") right.bar(sellers, units) right.set_yscale("log") right.set_title("Log scale: every seller visible")
A log axis needs a label or a note, because many readers won't notice it. And it can't show zero or negative values: log(0) doesn't exist.
Revenue by store
From the orders table, work out each store's revenue (qty * unit_price, summed by store), then draw it as a bar chart on ax with the title Revenue by store and the y-axis label revenue ($). Sort the bars from largest to smallest.
import matplotlib.pyplot as plt import pandas as pd orders = pd.read_csv("https://api.gradient.test/v1/datasets/orders.csv") orders["revenue"] = orders["qty"] * orders["unit_price"] fig, ax = plt.subplots()
Small multiples and a legend
When you compare several groups, you can draw them all on one axes with a legend, or give each its own small chart. The second idea is called small multiples. One rule for choosing: up to three or four lines read fine on one chart. Beyond that, a tangle of lines is worse than a grid of small, simple charts.
import matplotlib.pyplot as plt import pandas as pd orders = pd.read_csv("https://api.gradient.test/v1/datasets/orders.csv") orders["month"] = orders["date"].str[:7] stores = ["portland", "seattle", "denver"] fig, ax = plt.subplots(figsize=(8, 3.2)) for store in stores: lines = orders[orders["store"] == store].groupby("month")["qty"].sum() ax.plot(lines.index, lines.values, label=store) ax.legend() ax.set_title("Units sold per month, by store") ax.tick_params(axis="x", labelrotation=90) fig2, axes = plt.subplots(1, 3, figsize=(11, 3), sharey=True) for ax2, store in zip(axes, stores): lines = orders[orders["store"] == store].groupby("month")["qty"].sum() ax2.plot(range(len(lines)), lines.values) ax2.set_title(store) ax2.set_xlabel("month number") axes[0].set_ylabel("units") fig2.tight_layout()
label= names each line, and ax.legend() draws the key. sharey=True gives the small multiples one y-axis scale. Without it, each chart picks its own scale, and a small store can look as busy as a big one.
Box plots: comparing spreads
An average hides how spread out the values are. A box plot shows the spread. The box runs from the 25th to the 75th percentile, so the middle half of the values sit inside it. The line across the box is the median. The whiskers reach out to the more typical extremes, and dots beyond them are unusual values.
import matplotlib.pyplot as plt import pandas as pd catalog = pd.read_csv("https://api.gradient.test/v1/datasets/catalog.csv") names = sorted(catalog["category"].unique()) groups = [catalog[catalog["category"] == c]["price"] for c in names] fig, ax = plt.subplots(figsize=(7, 3.5)) ax.boxplot(groups, tick_labels=names) ax.set_title("Price by category") ax.set_ylabel("price ($)")
Tents cost the most and vary the most. Camp-kitchen gear is cheap and tightly bunched. A bar chart of average prices would have shown the first fact but hidden the second. That's the reason to reach for a box plot whenever the spread matters as much as the middle.
Worked example: a chart ready to share
Turn a quick plot into one you could paste into a report: complete months only, a clear title that states the finding, labeled axes, and a note on the source.
import matplotlib.pyplot as plt import pandas as pd daily = pd.read_csv("https://api.gradient.test/v1/datasets/shoe-orders.csv") daily["month"] = daily["date"].str[:7] monthly = daily.groupby("month")["orders"].agg(["sum", "count"]) complete = monthly[monthly["count"] >= 28] recent = complete.loc["2024-01":] fig, ax = plt.subplots(figsize=(8, 3.5)) ax.plot(recent.index, recent["sum"], marker="o", label="orders") ax.set_ylim(bottom=0) ax.set_title("Trail-shoe orders peak every May to August") ax.set_ylabel("orders per month") ax.tick_params(axis="x", labelrotation=90) ax.spines[["top", "right"]].set_visible(False) fig.text(0.01, -0.02, "Source: practice shop online orders, complete months only.", fontsize=8) print(recent["sum"].idxmax(), recent["sum"].max())
The filter monthly["count"] >= 28 keeps months with at least 28 days of data, which drops the unfinished June. The title states the finding instead of just naming the data, so a busy reader gets the point without studying the chart. Removing the top and right borders (spines) takes away lines that carry no information. The small source note says where the numbers came from and what was left out. Run through the same checklist before you share any chart: right chart type, honest baseline, labeled axes, a title that says something, no unfinished periods.
Practice what you just read
- 7 code examples you can edit and run
- a practice exercise that checks your code
- a 5-question quiz
- 5 review cards for spaced repetition
Try a quiz question
You want to show whether cheaper products sell more units. Which chart fits best?