We use cookies to enhance your experience on the site
CodeWorlds

Visualization - seeing what a table hides

You have six months of counts from the reserve in front of you, @name: 8, 12, 15, 10, 18, 22. Every number is correct, every one of them is labelled, and they all sit in a single DataFrame column. And what follows from that? Is the lion population growing? Is May an exception or the start of a trend? With six values you can still work it out in your head. With three thousand rows you can work out nothing at all - and three thousand rows is exactly what one season brings back to camp.

My own first season in the reserve ended with a notebook full of entries that added up to nothing. Only when I pushed pins into a map and joined them with string did I finally see what four months of notes had never shown me: the herd was following the rain. A chart is that same map of pins - the same data, arranged so that your eye catches the pattern before your mind has finished counting.

This lesson hands you two tools. Matplotlib is the workshop where you build a chart piece by piece: the line, the axis labels, the title, the legend. Seaborn is a layer built on top of Matplotlib that already knows the standard statistical charts and draws each of them with a single command. We start in the workshop, because Seaborn leans on it with its full weight anyway.

Where
plt
comes from

Matplotlib is a package, that is, a fairly large collection of modules. Drawing is handled inside it by exactly one of them:

pyplot
. It is not a separate library - it lives inside the
matplotlib
package, so you have to reach it through a dotted path:
matplotlib.pyplot
. Typing that on every line would be torture, so the community settled on giving it the short alias
plt
with the keyword
as
. Nothing in the language forces this, it is only a convention - but a convention so strong that every example in the documentation and every answer you will find online assumes
plt
.

Three other spellings circle around this one line, none of them work, and it is worth knowing them so that you do not lose an evening to a typo.

import pyplot as plt
goes looking for a separately installed package called
pyplot
- there is no such package, so Python stops with
ModuleNotFoundError: No module named 'pyplot'
.
from matplotlib import plot
reaches for the name
plot
, but the module is called
pyplot
and not
plot
, so you get
ImportError: cannot import name 'plot' from 'matplotlib'
. The sneakiest one is
import matplotlib as plt
: that line runs without a murmur, because the
matplotlib
package really does exist. Only the first drawing call falls over, with
AttributeError: module 'matplotlib' has no attribute 'plot'
, because the drawing functions sit inside the
pyplot
module and not at the root of the package.

1import matplotlib.pyplot as plt

After that line no window opens and no file appears anywhere. The import only pulls the module into memory and hangs the label

plt
on it. You start drawing with the next command, so this single line can never break anything - and it is the only correct version out of the four.

The first track: a line of sightings

Let us start with the simplest question anyone asks of reserve data: how does the number of sightings change over time? Changes over time call for a line chart, because the line joining the points shows a direction and not just the values. It is drawn by the function

plot
, which takes two lists: the first goes on the horizontal axis, the second on the vertical one. At the end we call
show
, which is the command for "now let me see what you have drawn".

1months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun"]
2lion_sightings = [8, 12, 15, 10, 18, 22]
3
4plt.plot(months, lion_sightings)
5plt.show()

Six numbers from a notebook have turned into a line that climbs clearly upwards, with a single dip in April. Remember the division of labour:

plot
draws into memory,
show
displays. Those are two separate acts, and leaving out
show
in an ordinary script means the program runs, finishes without an error and shows you nothing.
show
itself computes nothing and changes no data - the list
lion_sightings
is exactly the same after the call as it was before. It is worth memorising character by character, because you will type it hundreds of times:
plt
,
.
,
show
,
(
,
)
.

A chart with no labels is pins with no map

The previous chart has one flaw: nobody except you knows what it shows. The axes have no names, there is no title, and the moment you add a second species there is no telling the lines apart. Four functions with self-explanatory names fix that:

title
sets the title,
xlabel
describes the horizontal axis,
ylabel
the vertical one, and
legend
draws a legend from the labels you passed to
plot
as
label
. On top of those comes
figure
with the
figsize
parameter, which gives the size of the sheet in inches as a pair of numbers, and
grid
, which lays a helper grid underneath.

1elephant_sightings = [5, 7, 6, 9, 11, 8]
2
3plt.figure(figsize=(10, 6))
4plt.plot(months, lion_sightings, marker="o", label="Lions", color="gold")
5plt.plot(months, elephant_sightings, marker="s", label="Elephants", color="gray")
6
7plt.title("Sightings in the reserve - first half of the year")
8plt.xlabel("Month")
9plt.ylabel("Number of sightings")
10plt.legend()
11plt.grid(True, alpha=0.3)
12plt.show()

Two

plot
calls one after the other do not create two charts - they add two lines to the same figure. A fresh sheet appears only at
plt.figure(...)
, and that is why I recommend starting every chart with exactly that line: without it the second chart in your script gets drawn on top of the first one and you are looking at a mess. The
marker
parameter puts a circle or a square on each individual measurement,
color
sets the colour, and
alpha
is transparency on a scale from 0 to 1. Every one of these functions changes the appearance of the picture and nothing else. The numbers in the lists stay untouched, and the lion line runs exactly where it ran before there was any title or legend.

Saving to a file:
savefig
before
show

A chart you look at on screen dies together with the window. For an expedition report you need a file, and that is what

savefig
is for: it takes a file name and writes the picture to disk. The extension in the name decides the format, and the
dpi
parameter decides the resolution - the higher it is, the sharper the image and the heavier the file.

1plt.figure(figsize=(10, 6))
2plt.plot(months, lion_sightings, marker="o", label="Lions")
3plt.legend()
4
5plt.savefig("sightings.png", dpi=150)
6plt.show()

The order of those last two lines is not an accident, and it is the most common trap in this lesson:

savefig
must come before
show
. Once the preview window is closed some backends clear the figure away, so a
savefig
called afterwards writes an empty white image - no error, no warning, simply empty. Saving to a file does not close the window either, and it does not excuse you from calling
show
if you still want to look at the chart.

Bars:
plt.bar
, not
plt.bars

A line is fine for time, but not for comparisons. When you have four species and their head counts, you want to see which bar is taller - not to tie a lion to a giraffe with a piece of string, because there is no "in between" for those two. Comparing categories is the job of a bar chart, and it is drawn by a function with a short, unambiguous name:

bar
. The first argument is the list of category labels, the second the list of values; the optional
color
takes either one colour or a list of colours, one per bar.

The name has to be memorised exactly, because Matplotlib does not reward creativity. The function is called

bar
, in the singular, not
bars
. There is also no
column
function - in Matplotlib columns are called bars and that is the end of it. And there is no
histogram
function either - a distribution chart is drawn by
hist
, which you will meet in a moment, while NumPy's
np.histogram
is something else entirely, because it returns plain numbers instead of a picture. All three of those slips end with the same message:
AttributeError: module 'matplotlib.pyplot' has no attribute ...
.

1species = ["Lion", "Elephant", "Cheetah", "Giraffe"]
2populations = [120, 450, 85, 200]
3
4plt.figure(figsize=(10, 6))
5plt.bar(species, populations, color=["gold", "gray", "orange", "brown"])
6plt.title("Species populations in the reserve")
7plt.xlabel("Species")
8plt.ylabel("Population")
9plt.show()

Four bars and one glance are enough to tell you there are more than five times as many elephants as cheetahs. The order of the bars comes straight from the order of the

species
list -
bar
sorts nothing and totals nothing on your behalf, it simply stands as many rectangles side by side as you gave it values. If you want horizontal bars there is a separate function,
barh
, but with species names the vertical ones read better and those are the ones I recommend.

A histogram is not the same thing as bars

Suppose that over the season you weighed twenty-four lions. A bar chart would give you twenty-four bars, one per animal - completely unreadable and good for nothing. The question here is a different one after all: which weights are typical and which are rare. That is what a histogram is for: it cuts the range of values into intervals, called bins, and shows how many measurements fell into each of them. It is drawn by the function

hist
, where the
bins
parameter sets the number of bins and
edgecolor
draws an outline so that neighbouring bars do not merge into one block.

1lion_weights = [168, 175, 190, 182, 177, 195, 160, 188,
2                172, 181, 199, 165, 178, 186, 173, 191,
3                169, 184, 176, 193, 180, 187, 171, 179]
4
5plt.figure(figsize=(10, 6))
6plt.hist(lion_weights, bins=6, edgecolor="black", alpha=0.7)
7plt.title("Distribution of lion weights")
8plt.xlabel("Weight [kg]")
9plt.ylabel("Number of animals")
10plt.show()

The difference from

bar
is fundamental, even though both charts are made of bars. To
bar
you hand two lists: the categories and the finished values. To
hist
you hand one list of raw measurements, and the counting is done for you by the function. The vertical axis of a histogram always shows a count, even if you never write that down. Changing
bins
changes only the way the range is sliced - the data stays exactly the same, and with too few bins the distribution looks smooth, while with too many it falls apart into single measurements.

Shares of a whole:
plt.pie

Sometimes the absolute number of animals is not what interests you, only what portion of the reserve each species makes up. The question "what percentage" is answered by a pie chart, drawn by the function

pie
. You give it a list of values, a list of labels in the
labels
parameter, and a percentage formatting pattern in
autopct
- the string
"%1.1f%%"
means "one digit after the decimal point, and a percent sign at the end".

1plt.figure(figsize=(8, 8))
2plt.pie(populations, labels=species, autopct="%1.1f%%",
3        colors=["gold", "gray", "orange", "brown"])
4plt.title("Species share of the reserve population")
5plt.show()

The percentages on the slices were worked out by Matplotlib itself, by dividing each value by the sum of them all - the

populations
list still holds raw head counts, not percentages. That is also why a pie chart only makes sense when your values genuinely add up to one whole. For four species from a single reserve that is true; for four unrelated measurements it is not, and once you get to a dozen categories the slices become so thin that you are better off going back to
bar
.

Two numbers at once:
plt.scatter

So far every chart had a category and a number. But what if you want to check whether two numbers move together - for example, whether more lions are seen in the months with heavier rainfall? That is the job of a scatter plot, drawn by the function

scatter
, which puts one dot down for every pair of values. The
s
parameter sets the size of the dots, and the
alpha
you already know sets their transparency, which lets you see where points overlap.

1rainfall_mm = [12, 18, 45, 80, 130, 165]
2
3plt.figure(figsize=(10, 6))
4plt.scatter(rainfall_mm, lion_sightings, s=100, alpha=0.7, color="gold")
5plt.title("Rainfall vs number of sightings")
6plt.xlabel("Rainfall [mm]")
7plt.ylabel("Number of lion sightings")
8plt.show()

The six dots line up roughly along a rising line: the more rain, the more sightings. That is a first lead, not proof -

scatter
computes nothing and fits no line, it simply puts points wherever you tell it to. The order of the arguments matters: the first list goes on the horizontal axis and the second on the vertical one, and swapping them gives you a perfectly valid chart that says something completely different.

Seaborn: a layer on top, not a replacement

Matplotlib draws everything, but every statistical chart is something you have to assemble in it yourself. Seaborn goes one step further: it knows the charts typically used in data analysis, it accepts a Pandas DataFrame directly, and it picks the colours and the axis labels from your column names on its own. It does not replace Matplotlib, it wraps it, which is why

plt.title
and
plt.show
work on Seaborn charts exactly as they do on your own. The library is imported under the alias
sns
, and the function
set_style
sets a shared style for every picture that follows.

To show the weight distribution split by species we need the data in long format: one column with the species name and one with the measurement, one row per animal.

1import seaborn as sns
2import pandas as pd
3
4sns.set_style("whitegrid")
5
6observations = pd.DataFrame({
7    "species": ["Lion"] * 6 + ["Cheetah"] * 6 + ["Giraffe"] * 6,
8    "weight_kg": [168, 175, 190, 182, 177, 195,
9                  52, 58, 47, 61, 55, 49,
10                  810, 950, 880, 1020, 905, 870]
11})
12
13print(observations.shape)  # (18, 2)

Eighteen rows and two columns, exactly like the DataFrame from the previous lesson - Seaborn does not introduce a data structure of its own, it reads the one you already know. The

set_style
call drew nothing at all; it only changed the default look of the charts that are yet to be created. The pictures you drew earlier stay exactly as they were.

Boxplot: the box, the whiskers and the outliers

The histogram showed the distribution of one group. But how do you compare the distributions of three species side by side without drawing three separate histograms? That is what a box plot is for, drawn by the function

boxplot
. It condenses a distribution down to five numbers: the lower and upper edge of the box, which are the quartiles splitting the data into four equal parts, the median line inside the box, and the whiskers reaching out to the typical values. Points lying beyond the whiskers are drawn separately as outliers.

1plt.figure(figsize=(10, 6))
2sns.boxplot(data=observations, x="species", y="weight_kg")
3plt.title("Weight distribution by species")
4plt.show()

Three boxes on one picture, and you can see immediately that cheetahs fit into a narrow range while giraffes blow the scale apart. For the lions the lower quartile lands on 175.5 kg, the median on 179.5 kg and the upper quartile on 188 kg - those three numbers are precisely the edges and the line of the box. The

data
argument takes the whole DataFrame, while
x
and
y
are column names, not the data itself; Seaborn picks the axis labels up from those names automatically.

Three misunderstandings are worth clearing away right now, because the word "box" is misleading. A boxplot is not for drawing boxes around text - putting a frame around a caption is what the

bbox
parameter of the text functions does. Nor is it a tool for drawing rectangles as such; yes, it draws a rectangle, but that rectangle is a chart of five computed statistics, not a shape whose dimensions you supply. And it does not combine charts into frames - arranging several charts on one sheet is the job of
plt.subplots
, which you will meet at the end of this lesson. A boxplot has exactly one job: to show the distribution of the data with its quartiles and outliers. Close relatives are
sns.violinplot
, which draws the shape of the distribution instead of a box, and
sns.countplot
, which counts the rows in each category and turns that into bars by itself.

The correlation matrix: numbers before pictures

The scatter plot showed the link between rainfall and lion sightings, but with three or thirty columns you are not going to draw every pair separately. Pandas can compute all the links at once with the

corr
method, which returns a correlation matrix: a table where each cell holds the strength of the link between two columns, on a scale from minus one to one. One means the values rise perfectly together, zero means they have nothing to do with each other, and minus one means one rises as the other falls. We will assign the result to a variable named
corr_matrix
.

1monthly = pd.DataFrame({
2    "rainfall_mm": rainfall_mm,
3    "lion_sightings": lion_sightings,
4    "elephant_sightings": elephant_sightings
5})
6
7corr_matrix = monthly.corr()
8print(corr_matrix.round(2))
9#                     rainfall_mm  lion_sightings  elephant_sightings
10# rainfall_mm                1.00            0.85                0.71
11# lion_sightings             0.85            1.00                0.47
12# elephant_sightings         0.71            0.47                1.00

Three numeric columns gave a three by three table. The diagonal is all ones, because every column agrees with itself one hundred percent, and the whole table is symmetric about that diagonal. Note one condition that is easy to forget:

corr
works on numbers only. If even a single text column had been left in
monthly
, the call would have broken off with
ValueError: could not convert string to float
, and the numeric columns would have to be sifted out first with the
numeric_only=True
parameter or with the
select_dtypes
method you know from the previous lesson. A second trap lurks right there: if the sifting leaves you with one numeric column, you get a one by one matrix holding a single one - formally correct and informationally worthless. A meaningful correlation matrix needs at least two numeric columns.

sns.heatmap
, or the matrix in colour

Three by three you can still read with your eyes, but twenty by twenty is a wall of digits. A heatmap turns every number into a colour, which makes the strong links jump out of the table on their own. It is drawn by the

heatmap
function from Seaborn. The
annot
parameter writes the numbers onto the tiles,
cmap
picks the palette -
"coolwarm"
paints negative values blue and positive ones red - and
center
says which value should land in the middle of the colour scale, which here is zero.

In its simplest form this call is made of six parts, and they are worth knowing by heart:

sns
,
.
,
heatmap
,
(
,
corr_matrix
,
)
. First the library alias, then the dot, the function name, the opening bracket, the matrix being passed in and the closing bracket. Any extra parameters are added after the name of the matrix, before the closing bracket.

1plt.figure(figsize=(8, 6))
2sns.heatmap(corr_matrix, annot=True, cmap="coolwarm", center=0)
3plt.title("Correlations in the reserve data")
4plt.show()

The hottest tile off the diagonal joins rainfall to lion sightings: 0.85, which is a very strong link. Elephants react to rain more weakly, 0.71, and between lions and elephants there is barely 0.47 left. The heatmap computed nothing - all of those numbers were already sitting in

corr_matrix
, it only painted them so that you could compare them at a glance. And remember the classic trap: a strong correlation says that two things move together, not that one causes the other. Rain does not create lions, it pulls the game down to the waterhole where they are easier to spot.

Four charts on one sheet

An expedition report rarely consists of a single picture. Instead of saving four separate files, you can split the sheet into a grid with the

subplots
function, giving it a number of rows and a number of columns. It returns two things at once: the object representing the whole sheet, traditionally named
fig
, and the array of drawing areas, named
axes
. You reach a particular area the way you reach a cell of a NumPy matrix, that is
axes[row, column]
, counting from zero. Watch out for the method names: an area has no
title
, it has
set_title
, because it is an object and not the
plt
module. The same goes for its axis labels, which are
set_xlabel
and
set_ylabel
. Called with no arguments at all,
plt.subplots()
hands you a single area, and the working order is always the same: first create the figure and the area, then draw on it, then describe the axes, and only at the very end call
show
.

1fig, axes = plt.subplots(2, 2, figsize=(12, 10))
2
3axes[0, 0].bar(species, populations)
4axes[0, 0].set_title("Populations")
5
6axes[0, 1].pie(populations, labels=species, autopct="%1.1f%%")
7axes[0, 1].set_title("Share")
8
9axes[1, 0].hist(lion_weights, bins=6)
10axes[1, 0].set_title("Weight distribution")
11
12axes[1, 1].scatter(rainfall_mm, lion_sightings)
13axes[1, 1].set_title("Rainfall vs sightings")
14
15plt.tight_layout()
16plt.show()

Four charts from across the whole lesson have landed on one sheet in a two by two layout. The names of the drawing functions did not change by a single letter - they are still

bar
,
pie
,
hist
and
scatter
, only called on an area instead of on
plt
. The
tight_layout
call pushes the charts apart so that the titles and the axis labels do not overlap, and that is the only thing it does. At the end you still call a single
plt.show()
, because the entire grid is one figure.

Summary

  • You always import the drawing module like this:
    import matplotlib.pyplot as plt
    . The spellings
    import pyplot as plt
    ,
    from matplotlib import plot
    and
    import matplotlib as plt
    all lead to an error, the last one only at the first drawing call.
  • A bar chart is drawn by
    plt.bar()
    . There is no
    plt.bars()
    , no
    plt.column()
    and no
    plt.histogram()
    .
  • You display a picture with
    plt.show()
    , character by character:
    plt
    ,
    .
    ,
    show
    ,
    (
    ,
    )
    .
  • plt.plot
    is for change over time,
    plt.bar
    for comparing categories,
    plt.hist
    for the distribution of one variable,
    plt.pie
    for shares of a whole,
    plt.scatter
    for the link between two numbers.
  • You save to a file with
    plt.savefig("name.png")
    , and always before
    plt.show()
    .
  • Seaborn is imported as
    sns
    , it accepts a DataFrame and adds statistical charts on top.
  • sns.boxplot
    shows the distribution of the data with its quartiles and outliers - it does not draw boxes around text, it is not for drawing rectangles and it does not combine charts into frames.
  • You build a correlation heatmap out of six parts:
    sns
    ,
    .
    ,
    heatmap
    ,
    (
    ,
    corr_matrix
    ,
    )
    .
  • Several charts on one sheet are arranged by
    plt.subplots(rows, columns)
    , and the areas are described with
    set_title
    ,
    set_xlabel
    and
    set_ylabel
    .

In the next lesson we will put these charts to work in the reconnaissance known as EDA, where the histogram and the boxplot will help you track down gaps and outliers in a fresh dataset. For now remember one thing, @name: a chart adds not one single piece of information to your data, it only arranges it so that your eye spots the track that a column of numbers would never show you.

Go to CodeWorlds