Level 5 · In the Wild

Charts and Visualisation 📈

A chart is an argument. This lesson covers how to draw one in Python, and how to make sure the argument it makes is true.

The simplest chart

import matplotlib      # pip install matplotlib
matplotlib.use("Agg")           # render to a file, no window needed
import matplotlib.pyplot as plt

months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun"]
sales = [100, 120, 118, 135, 150, 162]

fig, ax = plt.subplots(figsize=(8, 4.5))
ax.plot(months, sales, marker="o")
ax.set_title("Monthly sales, 2026")
ax.set_xlabel("Month")
ax.set_ylabel("Sales (£000s)")
ax.grid(alpha=0.3)

fig.tight_layout()
fig.savefig("sales.png", dpi=150)
print("written to sales.png")

The pattern is always the same: make a figure and axes, draw on the axes, label everything, save or show. matplotlib.use("Agg") tells it to render to a file rather than open a window, which is what you want on a server or in a script.

Choosing the right chart

QuestionChartNotes
How has this changed over time?LineTime on the x-axis, always
How do these categories compare?BarHorizontal if the labels are long
What is the distribution?HistogramThe one people forget, and often the most informative
Are these two things related?ScatterCorrelation, not causation. Say so
What are the parts of a whole?Stacked barNot a pie chart. See below
Where are the outliers?Box plotShows median, quartiles and stragglers at once
🥧 On pie charts

Humans compare angles badly and lengths well. A pie chart with more than about four slices is harder to read than the bar chart of the same data, and two pie charts side by side are nearly impossible to compare. Use a bar chart. If someone insists on a pie, sort the slices and never explode them.

Several charts at once

import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np

rng = np.random.default_rng(seed=42)
data = rng.normal(loc=100, scale=15, size=500)

fig, axes = plt.subplots(1, 3, figsize=(14, 4))

axes[0].hist(data, bins=30, edgecolor="white")
axes[0].set_title("Distribution")

axes[1].boxplot(data, vert=False)
axes[1].set_title("Spread and outliers")

axes[2].scatter(data[:-1], data[1:], alpha=0.4, s=12)
axes[2].set_title("Each value against the next")

for ax in axes:
    ax.grid(alpha=0.3)

fig.suptitle("Three views of the same 500 numbers")
fig.tight_layout()
fig.savefig("three-views.png", dpi=150)
print("saved")

Note the seeded random generator: default_rng(seed=42) makes the figure reproducible, which matters as much for a chart in a report as it does for a test.

The chart nobody labels properly

import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt

regions = ["East", "North", "South", "West"]
totals = [4200, 2090, 1490, 980]

fig, ax = plt.subplots(figsize=(8, 4.5))
bars = ax.barh(regions, totals, color="#4584b6")

ax.set_title("Total sales by region, Q2 2026")
ax.set_xlabel("Sales (£)")
ax.bar_label(bars, fmt="£{:,.0f}", padding=4)
ax.set_xlim(0, max(totals) * 1.15)
ax.spines[["top", "right"]].set_visible(False)

fig.tight_layout()
fig.savefig("regions.png", dpi=150)
print("saved")

A chart is finished when it has:

How charts lie

import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt

months = ["Jan", "Feb", "Mar", "Apr"]
values = [100, 102, 101, 104]

fig, (left, right) = plt.subplots(1, 2, figsize=(11, 4))

left.bar(months, values, color="#c92a2a")
left.set_ylim(99, 105)
left.set_title("'Explosive growth!'")

right.bar(months, values, color="#4584b6")
right.set_ylim(0, 120)
right.set_title("The same data, honest axis")

fig.tight_layout()
fig.savefig("truncated.png", dpi=150)
print("saved")
RHETORIC[Formidable: Success]

The left chart is not false. Every number on it is correct. It is simply drawn so that a four percent change fills the frame and reads as a quadrupling.

Truncating a bar chart's y-axis is the most common deception in business presentations, and it is usually not malice: it is someone letting the plotting library pick the limits. Bar charts must start at zero, because the bar's length is the message. Line charts may not, because the slope is the message.

The other reliable ways to mislead, in case you meet them:

Other tools

ToolGood for
matplotlibEverything, eventually. Verbose but total control
seabornStatistical charts in one line, sensible defaults
plotlyInteractive charts for web pages
pandas .plot()Quick looks straight from a DataFrame
AltairDeclarative: you describe the mapping, it draws it
import pandas as pd

df = pd.DataFrame({"month": ["Jan", "Feb", "Mar"], "sales": [100, 120, 118]})

# the fastest possible look at some data
ax = df.plot(x="month", y="sales", kind="bar", title="Sales", figsize=(6, 3))
ax.figure.savefig("quick.png")
Exercise 1

Draw a distribution

Generate 1,000 dice-roll totals for two dice, plot them as a histogram, and label it properly. Seed the generator so it is reproducible.

Reveal solution
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import random

random.seed(42)
totals = [random.randint(1, 6) + random.randint(1, 6) for _ in range(1000)]

fig, ax = plt.subplots(figsize=(8, 4.5))
ax.hist(totals, bins=range(2, 14), align="left", rwidth=0.85, color="#4584b6")

ax.set_title("Two dice, 1,000 rolls: seven is the most likely total")
ax.set_xlabel("Total of both dice")
ax.set_ylabel("Number of rolls")
ax.set_xticks(range(2, 13))
ax.spines[["top", "right"]].set_visible(False)

fig.tight_layout()
fig.savefig("dice.png", dpi=150)
print("saved dice.png")

The title states the finding rather than naming the chart. That single habit improves reports more than any styling.

Exercise 2

Fix the misleading chart

A colleague sends a chart showing 'a 300% increase in engagement'. The y-axis runs from 4.0 to 4.3 and the x-axis covers eleven days. What do you say?

Reveal solution

Something like: the axis starts at 4.0, so a 0.3 change fills the whole frame. In absolute terms this is a 7% move, not 300%, and the 300% figure appears to be the change relative to the truncated baseline rather than to zero. Eleven days is also short enough that normal weekly variation could explain it. Could we see it from zero, over a quarter, with the weekly cycle visible?

Note the tone. The chart is nearly always an honest mistake by someone who let the library choose the limits. Asking to see it differently gets a better chart; accusing someone of lying gets a defensive colleague.

Exercise 3

Chart selection

Which chart for each?

  1. Website visitors per day for a year.
  2. Revenue from five product lines this quarter.
  3. How long users spend on a page.
  4. Whether taller people earn more.
  5. The share of traffic from four sources, compared across three months.
Reveal solution
  1. Line. Time series. Consider a seven-day rolling average to reveal the trend under the weekly cycle.
  2. Bar, horizontal if the product names are long, sorted by value.
  3. Histogram. The mean is nearly useless here: this distribution will be heavily skewed by a few very long sessions.
  4. Scatter, with a note that any correlation is not causation, and that confounders like age and occupation are doing much of the work.
  5. Stacked bar, three bars side by side. Three pie charts would make the comparison nearly impossible.
+100 XP