Skip to content

Plotting

70 min Foundations Needs: Pandas

A chart is how you find the problems that info() and describe() cannot show you. This page is about choosing the right one and reading it honestly, and then about the two libraries you will draw it with.

Why this matters

Summary statistics hide shape. Two columns can share a mean, a standard deviation and a correlation and still look nothing alike when you plot them. This is the point of Anscombe's quartet: four datasets with nearly identical statistics, one a clean line, one a curve, one a line dragged by a single outlier. Nothing in the numbers tells you which you have.

There is a second reason, which arrives later and matters more. Somebody will eventually decide something because of a chart you made. Making that chart accurate is a technical skill. Making it honest is a separate one, and this page covers both.

How people read a chart

Not all visual channels are read equally well. Human beings judge position almost perfectly, length nearly as well, and everything after that badly.

How accurately people read each visual encoding The same pair of values shown as position, length, angle, area and colour. Position and length are read most accurately. Angle, area and colour are read least accurately. position length angle area colour read most accurately read least accurately

This one ranking explains a surprising number of chart rules. It is why a bar chart beats a pie chart, because comparing bar heights is comparing lengths while comparing pie slices is comparing angles. It is why bubble sizes mislead, because area is read poorly and doubling a radius quadruples the area. And it is why colour belongs to categories or to a rough sense of high and low, never to a value somebody needs to read off precisely.

The rule that follows: put what matters most on a position or length channel. Push the rest to colour and shape.

Choosing a chart

Start from what you have and what you are asking, never from which chart looks impressive.

Choosing a chart from what you have One numeric column gives a histogram or box plot. One categorical column gives a bar chart. Two numeric columns give a scatter plot. A numeric column split by a category gives a box plot or grouped bars. Many numeric columns give a correlation heatmap. what do you have? one numeric column Histogram or box plot one category column Bar chart counts per group two numeric columns Scatter line if ordered in time numeric split by category Box plot or grouped bars many numeric columns Heatmap of correlations Start from the data you hold and the question you are asking. Never from the chart that looks impressive.

The six charts you actually need

Almost all exploratory work uses these. Learn them properly and the exotic ones can wait until you need them.

Six chart types and the question each answers Histogram for the shape of one numeric variable. Bar chart for comparing categories. Scatter for the relationship between two numeric variables. Line for change over time. Box plot for comparing distributions across groups. Heatmap for many pairs at once. Histogram what shape is this one numeric column Bar chart which category is bigger counts or totals Scatter do these two move together two numeric columns Line how did it change over time a value in order Box plot how do groups compare numeric split by category Heatmap which pairs relate many numeric columns

A few notes that save time later.

Histogram answers "what shape is this column". Change the number of bins and look again. Too few bins hide structure, too many turn the chart into noise, and the default is a guess rather than an answer.

Bar chart compares categories. Start the axis at zero, always, for the reason in the next section. Sort the bars by value unless the categories have a natural order, because a sorted bar chart answers "which is biggest" instantly.

Scatter is the workhorse for two numeric columns, and the first thing to reach for when you want to know whether a feature has any relationship with your target.

Line is for a value in order, usually time. If your x axis is a category rather than a sequence, you want bars. A line between unordered categories implies a trend that does not exist.

Box plot compares distributions across groups, and shows the median, quartiles and outliers at once. It is the fastest way to see whether a category actually separates your data.

Heatmap shows many pairs at once, most often correlations between every pair of numeric columns. Use a diverging colour scale centred at zero, so that positive and negative are visually opposite.

And the one to avoid: pie charts. They ask you to compare angles, which is near the bottom of the ladder. Two or three slices is survivable. More than that and a sorted bar chart is better in every way.

Charts that lie

Most misleading charts are not fraud. They are somebody reaching for a default without thinking about it.

The same numbers on two different axes Four values, 98, 99, 100 and 102, drawn with the vertical axis starting at zero and again with it starting at 95. The second chart makes a two percent difference look enormous. axis starts at 0 0 110 98 99 100 102 axis starts at 95 95 103 98 99 100 102 Same four numbers. The chart on the right is not a chart, it is an argument.

The truncated axis. Bar charts encode value as length, so cutting the axis breaks the encoding: a bar twice as tall no longer means twice as much. Start bar charts at zero. Line charts are different, because they encode change rather than magnitude, so a truncated axis there is often the honest choice. Label it clearly either way.

Dual axes. Two lines with two different y scales can be made to cross, diverge or agree simply by choosing the scales. If you need two units on one chart, stack two charts instead.

Cherry-picked ranges. The same series over five years and over five months can tell opposite stories. Show the range that answers the question, and say what range you chose.

Colour that carries meaning it should not. Red for one group and green for another implies bad and good. Around one man in twelve cannot reliably tell those two apart anyway. Use a colourblind-safe palette and keep semantic colours for things that really are semantic.

ML connection

You will present model results to people who cannot read your code and will trust the picture. An accuracy of 94 percent on a chart with no baseline looks excellent until somebody notices that always guessing the majority class gives 93. Whether that baseline appears on the chart is a choice you make, and it is the same choice as where the axis starts.

When there is too much data

Overplotting and the fix The same scatter plot drawn twice. With large opaque points the data is a solid blob. With small semi transparent points the shape of the relationship and the density become visible. large opaque points small, semi transparent same data, same chart type. Only the point size and alpha changed.

With enough points a scatter plot becomes a solid shape and stops carrying information. Three fixes, in order of how often they work:

  • Make points smaller and semi transparent, so density shows as darkness.
  • Sample a few thousand rows. You are looking for shape, not for every record.
  • Switch to a chart that bins for you, such as a hexbin or a 2D histogram.

Part two: drawing it

Two libraries, and they are not rivals. Matplotlib is the engine and gives you control over every element. Seaborn sits on top of it, produces statistical charts in one line and has much better defaults.

The practical division: explore with seaborn, finish with matplotlib. Seaborn returns matplotlib objects, so you can always drop down a level.

Figure and axes

This is the concept that confuses every matplotlib beginner, and it takes one picture.

A matplotlib figure containing two axes The figure is the whole canvas. Each axes is one plot inside it, with its own x and y axis, title and labels. fig, the whole canvas ax[0] ax[1] fig, ax = plt.subplots(1, 2) gives you one figure and two axes. You draw on the axes, you save the figure.

import matplotlib.pyplot as plt

fig, ax = plt.subplots(figsize=(8, 5))
ax.plot(df["month"], df["sales"])
ax.set_title("Monthly sales")
ax.set_xlabel("Month")
ax.set_ylabel("Sales (EUR)")
fig.savefig("sales.png", dpi=150, bbox_inches="tight")

You will also see the shorter style, plt.plot(...) followed by plt.title(...), which draws on whichever figure happens to be current. It is fine for a quick look and it falls apart the moment you have two charts. Learn the version above and you never have to unlearn anything.

Several charts side by side:

fig, axes = plt.subplots(1, 2, figsize=(12, 5))
axes[0].hist(df["salary"], bins=30)
axes[0].set_title("Salary distribution")
axes[1].scatter(df["age"], df["salary"], s=8, alpha=0.3)
axes[1].set_title("Age against salary")
fig.tight_layout()

tight_layout stops the labels of one chart overlapping the next one. Call it before saving, every time.

Matplotlib goes much deeper than this page

Everything above is enough for the whole of trimester one. When you need custom axes, annotations or precise styling, the official quick start guide and the anatomy of a figure page are the two references worth bookmarking. Do not read them now. Read them the first time a chart will not do what you want.

Seaborn, one line each

import seaborn as sns

sns.histplot(data=df, x="salary", bins=30)
sns.countplot(data=df, x="city")
sns.scatterplot(data=df, x="age", y="salary", hue="city", alpha=0.4)
sns.lineplot(data=df, x="month", y="sales")
sns.boxplot(data=df, x="city", y="salary")
sns.heatmap(df.corr(numeric_only=True), annot=True, cmap="coolwarm", center=0)

Three arguments do most of the work. data takes the DataFrame, x and y take column names as strings, and hue splits by a category and adds the legend for you. That pattern is the same across every seaborn function, which is most of why it is worth learning.

Set sensible defaults once, at the top of a notebook:

sns.set_theme(style="whitegrid", palette="colorblind")

Finishing a chart

A chart is finished when somebody who did not make it can read it without asking you anything.

fig, ax = plt.subplots(figsize=(8, 5))
sns.boxplot(data=df, x="city", y="salary", ax=ax)
ax.set_title("Salary by city, 2026 survey")
ax.set_xlabel("City")
ax.set_ylabel("Monthly salary (EUR)")
fig.savefig("salary_by_city.png", dpi=150, bbox_inches="tight")

Note ax=ax, which tells seaborn to draw on the axes you created rather than making its own. That is how the two libraries combine.

The checklist: a title that states the finding rather than the variables, axis labels with units, a legend when there is more than one series, and a source or date if the chart leaves your machine.

The charts you will make in every project

Five of them, from here to the end of the programme.

Chart What it tells you When
Distribution of the target Is it skewed, is it balanced Before any modelling
Correlation heatmap Which features move together Feature selection
Missing value map Whether gaps cluster in one group Cleaning
Learning curve Whether more data would help Trimester two
Confusion matrix Which classes get confused with which Evaluation

The first three you can draw today:

sns.histplot(data=df, x="target", bins=30)
sns.heatmap(df.corr(numeric_only=True), cmap="coolwarm", center=0)
sns.heatmap(df.isna(), cbar=False)

The missing value map is the one people skip. Gaps arranged in horizontal bands mean whole rows failed, while a solid block in one column means a field stopped being collected on a certain date. Both change what you should do about them, and isna().sum() cannot tell them apart.

When it goes wrong

Nothing appears in my notebook

In a script you need plt.show() at the end. In Jupyter, put the plotting call last in the cell, or add plt.show(), otherwise you may see only the object's text representation.

My labels are cut off when I save the figure

Use fig.savefig("name.png", bbox_inches="tight"), and call fig.tight_layout() before saving when you have several subplots.

Every chart appears on top of the previous one

You are using the plt. style and drawing into whichever figure is current. Create a new one explicitly with fig, ax = plt.subplots() for each chart.

The x axis labels overlap and are unreadable

Either rotate them with ax.tick_params(axis="x", rotation=45) or swap the axes so the categories run down the side, which is usually the better fix when the names are long.

My scatter plot is a solid blob

Overplotting. Reduce s, set alpha=0.2, or sample the rows. See the section above.

The correlation heatmap is all one colour

You are using a sequential colour map. Use a diverging one centred at zero, such as cmap="coolwarm", center=0, so that positive and negative read as opposites.

Check yourself

Use any dataset you have to hand.

  1. Plot one numeric column as a histogram with three different bin counts. Say what each one hides and what it reveals.
  2. Draw the same comparison as a bar chart and as a pie chart. Which one lets you rank the categories faster, and why does the encoding ladder predict that?
  3. Take a bar chart and redraw it with a truncated axis. Write one sentence describing the story each version tells.
  4. Make a scatter plot of two columns with at least a few thousand rows. Fix the overplotting and describe what became visible.
  5. Build a two panel figure with plt.subplots, label both panels fully, and save it at 150 dpi with nothing cut off.
  6. Draw the missing value heatmap for a real file. Say whether the gaps look random, and what you would do next.

Quick reference

Task Code
New figure fig, ax = plt.subplots(figsize=(8, 5))
Several panels fig, axes = plt.subplots(1, 2, figsize=(12, 5))
Histogram sns.histplot(data=df, x="col", bins=30)
Counts per category sns.countplot(data=df, x="col")
Bar of a value sns.barplot(data=df, x="cat", y="val")
Scatter sns.scatterplot(data=df, x="a", y="b", hue="c", alpha=0.3)
Line sns.lineplot(data=df, x="t", y="v")
Box by group sns.boxplot(data=df, x="cat", y="val")
Correlation heatmap sns.heatmap(df.corr(numeric_only=True), cmap="coolwarm", center=0)
Missing values sns.heatmap(df.isna(), cbar=False)
Draw into your axes sns.boxplot(..., ax=ax)
Labels ax.set_title(...), ax.set_xlabel(...), ax.set_ylabel(...)
Rotate ticks ax.tick_params(axis="x", rotation=45)
Defaults sns.set_theme(style="whitegrid", palette="colorblind")
Save fig.savefig("f.png", dpi=150, bbox_inches="tight")

Questions to sit with

  1. The encoding ladder says position and length are read most accurately. Name a chart you have seen recently that put the most important number on a weak channel. What was gained by doing that, and by whom?
  2. A truncated axis on a bar chart is misleading, but on a line chart it is often correct. What is the difference, in terms of what each chart encodes?
  3. You draw a chart, dislike the story it tells, and redraw it differently. When is that clarification and when is it manipulation? What would you record so that somebody else could tell?
  4. Anscombe's quartet exists because summary statistics hide shape. What is the equivalent risk when you report a single accuracy number for a model?

Next

You can see your data. Now the language for describing what you are seeing, and for saying how confident you should be about it.

Probability

Worth your time: the seaborn tutorial is short and well written, and the Data Visualisation Catalogue is a good browse when you are unsure which chart fits.