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.
plt comes fromMatplotlib 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 pltAfter 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.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, (, ).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.savefig before showA 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:
must come before savefig
. Once the preview window is closed some backends clear the figure away, so a show
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.plt.bar, not plt.barsA 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.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.plt.pieSometimes 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.plt.scatterSo 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.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.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 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.00Three 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 colourThree 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.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.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.plt.bar(). There is no plt.bars(), no plt.column() and no plt.histogram().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.plt.savefig("name.png"), and always before plt.show().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.sns, ., heatmap, (, corr_matrix, ).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.