Plotting & Visualization
Communicating data and results clearly with Matplotlib (and friends) — taught from zero, starting with what a plot actually is, and built up to the learning curves, histograms, and heatmaps you will make every day in ML.
Start here — what a plot actually is
A plot looks like a picture, but it is really a translation. You hand it numbers, and it hands you back positions, lengths, and colors — things your eye is extremely good at comparing, in a way it is hopeless at comparing a column of 500 numbers.
Ask someone whether 0.6931 is bigger than 0.6934 and they need a second. Draw the two as bars and they answer instantly, without thinking. A plot does not add information to your data — it moves that information into the channel your brain processes fastest. That is the entire reason plotting exists.
A map does not contain a single thing that is not already true of the land. But nobody navigates by reading a table of latitudes and longitudes. The map takes numbers you cannot hold in your head and turns them into a shape you can see at a glance — and, like a map, a plot can also lie if it is drawn badly. Learning to plot is learning to draw honest maps of your data.
This matters enormously in machine learning, because a training run is invisible. You cannot look at a million-parameter model and tell whether it is learning. The only way you find out that your loss exploded, your validation set is memorized, or your data has a bug is by plotting something. In practice, plotting is the debugger for ML.
It starts from absolute zero — no plotting experience assumed. Flip the Depth switch at the top for the formal notation behind scales, bins, and error bars, and for the mechanics of how numbers become pixels. Nothing is hidden for good.
is the library we will use. It was created by John D. Hunter in 2003 and published in 2007, and it remains the base layer of the scientific Python stack — pandas, seaborn, and scikit-learn all draw their plots by calling it.
Your first plot — three lines
Here is the smallest complete plot in Python. Run it, and read the code afterwards — it is short enough to guess most of it.
Three ideas are already doing all the work:
- Data in, drawing out.
plt.plot(x, y)takes two lists of numbers and pairs them up: the firstxwith the firsty, the second with the second, and so on. Each pair becomes a point on the page. - The axes carry the meaning. Without
xlabelandylabel, a plot is a pretty shape that says nothing. An unlabelled axis is a bug, not a style choice. - Nothing is magic. Every visual element — the line, the ticks, the title — is an ordinary Python object you can grab and change.
What does plt.plot(x, y) do with the two lists you give it?
Hint: Think about how the first number of each list relates to the other.
Anatomy of a figure — the four words worth learning
Almost all Matplotlib confusion comes from not knowing which object you are talking to. There are only four names that matter, and they nest inside each other like boxes.
- The is the whole page. When you save a PNG, you are saving a Figure.
- An is one plot box inside that page. A figure with four side-by-side charts has one Figure and four Axes.
- An is one ruler: the x-axis or the y-axis, with its ticks and numbers.
- An is anything drawn at all — a line, a bar, a label, the legend. The Figure and the Axes are themselves Artists.
The Figure is the frame — the physical thing you hang up and hand to someone. Each Axes is a photo mounted inside that frame; a frame can hold one big photo or a grid of four small ones. The Axis objects are the rulers printed along the edges of a photo. And every Artist is a mark of ink somewhere on it. When something looks wrong, your first question is always: which box am I talking to?
Here is the vocabulary drawn on an actual chart. Every label below names a real object you can reach in code:
A figure shows a 2 by 2 grid of charts. How many Figures and how many Axes is that?
Hint: One is the page, the other is a plotting box.
The two interfaces — the thing that confuses everyone
Matplotlib can be driven two ways, and mixing them up is the number-one source of "why did my label land on the wrong chart?"
The implicit style (plt.plot(...), plt.title(...)) is like shouting instructions into a room: Matplotlib guesses you mean the plot you touched most recently. That is fine when there is exactly one plot in the room. The explicit style (fig, ax = plt.subplots(), then ax.plot(...)) is like handing the instruction to a named person: you hold a variable for each Axes and say precisely which one you mean. The moment you have more than one chart, guessing goes wrong and naming does not.
The official docs call these the implicit pyplot interface and the explicit Axes (object-oriented) interface, and recommend the explicit one for anything beyond a quick look. Rougier makes the same point by quoting the Zen of Python at it: explicit is better than implicit.
Implicit (fine for one throwaway plot):
plt.plot(x, y)
plt.title("Loss")
plt.xlabel("step")
Each call says draw on whichever Axes is current. There is an invisible "current Axes" being tracked for you.
Explicit (what you should default to):
fig, ax = plt.subplots()
ax.plot(x, y)
ax.set_title("Loss")
ax.set_xlabel("step")
Now ax is a variable you hold. Three things to notice:
plt.subplots()returns two things: the Figure and the Axes. That is why the line has two names on the left.- The setter names gain a
set_prefix:plt.titlebecomesax.set_title. This trips up everyone once. - If you later make four Axes, nothing about these lines has to change — you just say
ax2.set_title(...)for the next one.
Start every plot with fig, ax = plt.subplots() and call methods on ax. It costs one extra line and saves you every multi-panel headache later. The only time the plt. shortcut is genuinely better is a single quick chart in a notebook — which is exactly what the runnable cells on this page do, to keep them short.
You write plt.plot(a, b), then create a second chart, then call plt.ylabel('loss'). Where does the label land?
Choosing a chart — pick by the shape of the question
Beginners ask "which chart looks best?" The useful question is "what shape is my question?" Answer that and the chart picks itself.
| Your question | The shape of it | Chart |
|---|---|---|
| How does this change over time or over an ordered input? | One value tracked along an ordered axis | Line (ax.plot) |
| Are these two measurements related? | Two numbers per item, no ordering | Scatter (ax.scatter) |
| Which category is biggest? | One number per named category | Bar (ax.bar) |
| How are these values spread out? | Many samples of one number | Histogram (ax.hist) |
| Which cells of this grid are hot? | A 2-D table of numbers | Heatmap (ax.imshow) |
| How big is my uncertainty? | A value plus a spread | Error bars (ax.errorbar) |
Lines imply that the space between two points is meaningful. So use a line for loss-versus-step (step 4.5 is a real, if unmeasured, moment), and bars for accuracy-per-model (there is nothing halfway between ResNet and ViT). Drawing a line across categories is the most common way a chart quietly tells a lie.
Here is the same underlying question — "which model is best?" — as a bar chart, because the x-axis is a set of names, not a number:
And here is the code for the four workhorse chart types, all in one figure. Run it and compare each panel to the table above:
Why is a line chart the wrong choice for accuracy across four different model names?
Hint: Ask what the space between two points on the line would mean.
How numbers become pixels
You now know the vocabulary. The remaining mystery is mechanical: how does the number 3.7 decide where on the screen to put a dot? The answer is one short piece of arithmetic, and knowing it explains a surprising number of plotting bugs.
Your data lives on one ruler — say, values from 0 to 10. The screen lives on another — say, pixels 80 to 560. Drawing a point means asking where it sits along the first ruler as a fraction, then walking that same fraction along the second. A value in the exact middle of the data range lands in the exact middle of the box. That is all a plot is: a fraction, copied from one ruler to another.
Your x values run from to , and the plotting box spans screen pixels (left edge) to (right edge). Where does the value go?
- How far along the data ruler is it? It is out of a total span of , so the fraction is — a bit over a third of the way across.
- How wide is the box? pixels.
- Walk that fraction along the box. pixels from the left edge.
- Add the left edge back, because the box does not start at pixel zero. , so the dot is drawn at pixel .
Change the axis limits to to and the same value now has fraction and lands at pixel — almost against the left wall. Nothing about the data changed; only the ruler did. This is exactly why a plot can be honest or misleading depending on its limits.
Scale — when to reach for a log axis
Some quantities do not vary by amounts; they vary by factors. Loss during training might go . On an ordinary axis, the last three of those are indistinguishable smudges pinned to the bottom.
On a normal (linear) axis, moving one centimetre always adds the same amount. On a , moving one centimetre always multiplies by the same factor. So the gap from 1 to 10 is drawn the same size as the gap from 10 to 100, and from 100 to 1000. Anything that grows or shrinks by a constant factor becomes a straight line — and straight lines are the one shape human eyes judge reliably.
A magnitude 7 quake is not one-seventh worse than a 49 — it releases about 32 times more energy than a magnitude 6. The scale is logarithmic because the raw numbers span such an enormous range that a linear scale would be useless. Training losses, model parameter counts, and learning rates all have exactly this problem.
Watch the same three curves on both scales. On the left the two smaller runs are flat lines glued to the bottom; on the right you can actually tell them apart:
Your loss values are [10, 1, 0.1, 0.01, 0.001]. Why does a log y-axis help?
Distributions — histograms and the bin trap
A answers "what does my data look like?" — the very first question to ask about any dataset, and the one that catches most data bugs.
Tip a jar of coins onto the table and sort them into the slots of a change tray: all the 5p in one slot, all the 10p in the next. Now the height of each pile tells you the shape of your jar's contents at a glance. A histogram is exactly that — the value range is chopped into slots, and each bar is how tall the pile got.
The catch is that you choose the slot width, and that choice changes the story. Too few bins and you flatten real structure into one lump; too many and you turn random noise into fake spikes.
Your histogram of image pixel values shows one tall spike at 0 and nothing else. What is that telling you?
Hint: A histogram is a picture of your data, so a strange picture usually means strange data.
Uncertainty — the error bar and why a bare number lies
You train a model three times with different random seeds and get accuracies of 91.2%, 88.7%, and 90.4%. Reporting "90.1%" as a single bar hides the most important fact: run-to-run wobble is nearly two points, so a rival model at 90.8% is not obviously better.
Every measurement you make is one draw from a noisy process. The bar height is your best guess at the truth. The whisker on top is an honest statement of how far that guess might be off. A chart with bars but no whiskers is quietly claiming perfect precision, which is almost never true in machine learning.
The three plots you will actually make every day
Everything above was groundwork. In practice, most of your ML plotting is these three charts.
1. The learning curve — is it training, and is it overfitting?
Plot training loss and validation loss on the same Axes, against step or epoch. This one chart diagnoses most training problems.
Read it like this:
- Both curves falling together — healthy. Keep training.
- Training falls, validation rises — . Stop at the validation minimum, or add regularization.
- Both curves flat and high — the model is not learning at all: check the learning rate, the labels, and whether gradients are reaching the weights.
- Loss spikes to NaN — the learning rate is too large, or something divided by zero.
Real loss curves are far noisier than the idealized one above, which is why they are almost always drawn with smoothing.
2. The confusion matrix — which mistakes is it making?
Accuracy is one number and tells you almost nothing about how a classifier fails. A shows every kind of mistake at once, and a heatmap is the natural way to draw a grid of numbers.
Never use jet or rainbow for a heatmap. They have bright bands in the middle that invent boundaries where the data is smooth, and they turn into unreadable mush when printed in grayscale or viewed by a colorblind reader. Use a perceptually uniform map — viridis, cividis, magma — where equal steps in the data look like equal steps in brightness. For data that is meaningfully signed, like weight changes, use a diverging map such as coolwarm centred on zero.
3. The image grid — look at your actual data
Before you trust any vision model, look at the pixels it is being fed, after your preprocessing pipeline. ax.imshow draws an array as an image, and a grid of them catches inverted channels, wrong normalization, and mislabelled examples in seconds.
Your classifier reports 94% accuracy, but the confusion matrix has one very bright off-diagonal cell. What have you learned that accuracy alone could not tell you?
Making a figure readable — the short version
Rougier, Droettboom and Bourne's ten rules for better figures are worth reading in full; these are the ones that fix ninety percent of student charts.
- Label both axes, with units.
lossis fine;xis not. This is not politeness — an unlabelled chart is unusable a week later, including by you. - Say what the reader should conclude. Put the finding in the title: "validation loss diverges after epoch 12" beats "loss curve".
- Only add a legend when there is more than one thing. A legend on a single line is noise.
- Do not truncate a bar chart's y-axis. Bars encode value by length, so starting at 0.85 makes a two-point gap look like a landslide. Truncating a line chart's axis is fine — lines encode change, not magnitude.
- Choose colours that survive a grayscale printer and a colorblind reader. Roughly 1 in 12 men has some colour vision deficiency; never let colour be the only thing distinguishing two lines. Vary the dash pattern or marker too.
- Save vector for print, raster for the web.
fig.savefig("f.pdf")stays sharp at any zoom;fig.savefig("f.png", dpi=200)is what you paste into Slack.
fig.tight_layout() — stops labels overlapping.
ax.legend() — only after you pass label= to your plot calls.
ax.grid(alpha=0.3) — a faint grid helps reading values; a dark one competes with your data.
ax.set_ylim(0, None) — pins bars to a zero baseline.
fig.savefig(path, dpi=200, bbox_inches="tight") — the save call that does not clip your labels.
- Nothing appears. In a plain script you need
plt.show(); in a notebook you usually do not. (In the cells on this page, figures are captured automatically.) - Every plot lands on top of the previous one. You reused the current Axes — call
plt.figure()orfig, ax = plt.subplots()to start a fresh one, andplt.close(fig)in loops so you do not leak memory. - Labels are cut off in the saved file. Add
bbox_inches="tight"tosavefig. x and y must have same first dimension. Your two arrays are different lengths — usually an off-by-one from a loop that appended to one list but not the other.
Why is truncating the y-axis dishonest on a bar chart but acceptable on a line chart?
Hint: Ask what visual property each chart uses to encode the value.
Explain to a friend who has never coded: what is the difference between a Figure and an Axes, and how would you decide between a line chart and a bar chart for some data they describe? Then explain what a rising validation-loss curve is telling you. If you stall on any of the three, that is the section to reread.
- A plot is a translation of numbers into positions and lengths — the channel your eye reads fastest. In ML it is the debugger, because training is otherwise invisible.
- Four words carry all of Matplotlib: the Figure (the whole canvas), an Axes (one plotting box), an Axis (one ruler), and Artists (everything drawn).
- Prefer the explicit style —
fig, ax = plt.subplots()thenax.plot(...)— over the implicitplt.calls, so a command can never land on the wrong chart. - Pick the chart by the shape of the question: line for ordered x, scatter for two related numbers, bar for categories, histogram for spread, heatmap for a grid.
- A log scale turns constant-factor change into a straight line — which is why nearly every loss curve uses one.
- Bins and error bars are choices that change the story: too many bins invent structure, and a missing whisker claims a precision you do not have.
- The three charts you will draw constantly: the learning curve (is it training? is it overfitting?), the confusion matrix (which mistakes?), and the image grid (is my data what I think it is?).
Practice — and how to make it stick
• Retrieval practice: before scrolling up, name the four figure parts and one chart type for each row of the shape table — pulling it from memory beats rereading it.
• Spaced repetition: mark this topic complete to add it to your Review queue, so it resurfaces right before you would forget it.
• Interleaving: mix these exercises with Python & NumPy array work and Probability & Statistics problems rather than doing them in one block — messier practice, sturdier memory.
- Read a chart, do not draw one: find any figure in a recent arXiv paper and write down its Figure/Axes count, why that chart type was chosen, and one thing you would change. Reading figures critically is what makes drawing them easy.
- Rebuild the diagnosis: simulate a training run that overfits, plot train and validation loss on one Axes with a log y-axis, and mark the best epoch with
ax.axvline. - Break it on purpose: take the bar chart with error bars and truncate the y-axis to
(0.85, 0.95). Screenshot both versions side by side — the lesson is much stronger when it is your own chart lying to you. - Make it publication-ready: take any chart above and add a descriptive title, labelled axes with units, a legend, and save it as both a 200-dpi PNG and a PDF.
Try it right here — edit and run the code, and if you get stuck or hit an error, ask Ada on the right: she can see your code and your terminal output.
Next: put these charts on real tabular data with Pandas & DataFrames, or go and plot the thing they were invented for in Optimization.