Section 1 of 12

1 Before you start

Before you begin, take a few minutes to check what you already know and how confident you feel. You will see the same questions again at the end of the module — this helps both you and us measure what you have learned. Click an option for every question and confidence rating, then click Next to continue.

Pre-test

You draw a heatmap of a correlation matrix. Which set of arguments makes the colour scale comparable across different correlation matrices?

Pre-test

You have two categorical columns and want a heatmap of how many records fall into each combination, with whole-number labels in the cells. Which approach is correct?

Pre-test

You are drawing a heatmap of counts that range from 0 upward, with no negative values. Which colour map best matches the data?

Pre-test

Which seaborn function lets you split a scatter-plus-regression into a row of panels, one per level of a third column?

Pre-test

What is the difference between an axes-level and a figure-level seaborn function?

Pre-test

You just loaded a dataset with six numeric columns and want a quick first look at every pairwise relationship at once. Which function fits best?

Pre-test

You want every seaborn chart in your script to share a white-grid background with larger, slide-friendly fonts, set once at the top. Which call does this?

Pre-test

You want one colour-blind-safe scheme applied to every chart in the script, set a single time. Which call does this?

Pre-confidence

I can draw a multi-panel figure faceted by another column using catplot, relplot, or displot in a single line.

Not at all confident
Fully confident
Pre-confidence

I can produce a correlation heatmap from a DataFrame and pin the colour scale so it means the same thing every time.

Not at all confident
Fully confident
Pre-confidence

I can explain the difference between an axes-level and a figure-level seaborn function, and use pairplot or jointplot to explore several numeric columns at once.

Not at all confident
Fully confident
Section 2 of 12

2 Introduction

In Part I you drew the everyday single-question plots: counts and means by category, the shape of a distribution, and the relationship between two numeric columns. Real analysis rarely stops at a single chart, though.

This second part is about composition and presentation — taking those plots and combining them:

  • Heatmaps — coloured grids for correlation matrices and contingency tables, when you want every pairwise number on one page.
  • Combining and faceting — small multiples and multi-panel figures for splitting a plot across the levels of another category.
  • Themes and palettes — the one-line settings that turn an exploratory chart into a polished, presentation-ready one.

To follow along with this module, you will need the same two standard imports at the top of every script: seaborn (aliased as sns) and matplotlib.pyplot (aliased as plt). When you are ready to see your chart, calling plt.show() will pop the figure onto your screen.

Be sure to test out each snippet in the Python Scratchpad on the right as you go. By the time you finish this module, you will know how to take a raw CSV of patient measurements and turn it into a comprehensive, multi-panel figure that summarizes the entire cohort in one shot.

Section 3 of 12

3 Heatmaps for correlation and contingency tables

Sometimes you do not just want a single chart. You want a grid. Whether you are looking at how strongly every pair of variables correlates or how often different categories overlap, a raw table of numbers is tough to scan.

Section 3.1 of 12

3.1 Heatmap

A heatmap solves this by turning that table into a colored grid, where dark and light squares let you read the patterns in seconds.

The most common way to use sns.heatmap is for a correlation matrix. Pandas makes the math easy: calling df.corr() gives you a square DataFrame showing the correlation coefficient between every pair of numeric columns. You just hand that DataFrame to sns.heatmap, and you instantly have a publication-quality map.

Try it out

Try this snippet in the Python Scratchpad on the right.

Try this snippet
import seaborn as sns
import matplotlib.pyplot as plt
import pandas as pd

df = pd.DataFrame({
    "age": [54, 61, 47, 72, 38, 55, 49, 67, 41, 58],
    "bmi": [27.3, 28.4, 26.1, 30.2, 24.5, 27.0,
             25.8, 29.7, 24.0, 28.1],
    "glucose": [5.4, 6.1, 4.9, 6.8, 4.5, 5.5, 5.0, 6.4, 4.7, 5.7],
    "chol": [4.8, 5.5, 4.6, 6.2, 4.0, 5.0, 4.5, 5.9, 4.2, 5.1],
})
corr = df.corr()
sns.heatmap(corr, annot=True, cmap="coolwarm", vmin=-1, vmax=1, center=0)
plt.show()
sns.heatmap on a correlation matrix encodes every pairwise correlation as a colour, so the strength and direction of relationships across all numeric columns becomes visible at a glance.
sns.heatmap on a correlation matrix encodes every pairwise correlation as a colour, so the strength and direction of relationships across all numeric columns becomes visible at a glance.

There are a few keyword arguments that will do the heavy lifting for you here:

  • annot=True: This writes the actual number inside each cell. It is almost always worth turning on, since colors alone can be hard to read precisely.
  • cmap: This chooses your color palette. Use "viridis" for values going in one direction (like zero to large positive numbers), or "coolwarm" for diverging values that range from negative to positive (like correlations).
  • vmin, vmax, and center: These pin your color scale to fixed limits. For correlations, setting vmin=-1, vmax=1, and center=0 ensures your colors mean the exact same thing every time, which is critical if you are comparing multiple heatmaps side-by-side.
A heatmap reads correctly when annot=True shows the cell values, cmap matches the data shape (viridis for sequential, coolwarm for diverging), and vmin/vmax/center pins the scale so colors mean the same thing across plots.
A heatmap reads correctly when annot=True shows the cell values, cmap matches the data shape (viridis for sequential, coolwarm for diverging), and vmin/vmax/center pins the scale so colors mean the same thing across plots.
Section 3.2 of 12

3.2 Contingency table

The other common use case is a contingency table, which simply shows how many records fall into specific combinations of two categories. To get the numbers, use pd.crosstab on two columns. Hand that resulting table to sns.heatmap, and you get a clear visual of your joint distribution.

Try it out

Try this snippet in the Python Scratchpad on the right.

Try this snippet
import seaborn as sns
import matplotlib.pyplot as plt
import pandas as pd

df = pd.DataFrame({
    "clinic": ["A", "A", "A", "B", "B", "B", "C", "C"],
    "sex": ["F", "F", "M", "M", "F", "M", "F", "M"],
})
table = pd.crosstab(df["clinic"], df["sex"])
sns.heatmap(table, annot=True, cmap="viridis", fmt="d")
plt.show()
A heatmap of a contingency table places every record into its (row, column) cell, then uses colour intensity to make the joint distribution readable at a glance.
A heatmap of a contingency table places every record into its (row, column) cell, then uses colour intensity to make the joint distribution readable at a glance.

Notice the fmt="d" argument in that snippet?

That forces the cell labels to display as clean integers. Without it, annot=True defaults to writing them as floats with a bunch of messy decimal places. You can also use fmt=".2f" for two decimal places, or fmt=".0%" for percentages.

One final, important note: sns.heatmap does not actually compute anything. It strictly draws whatever 2D table you hand it. All the mathematical thinking happens in pandas before you call Seaborn.

Section 4 of 12

4 Combining and faceting plots

Real data analysis usually requires you to break down distributions by categories. Instead of writing messy for loops to generate multiple separate charts, Seaborn uses faceting.

Faceting takes one plot and repeats it across different categories, lining up the panels side-by-side so your eye can compare them instantly.

Faceting turns one chart into a grid of aligned panels, one per category, so distributions can be compared at a glance across one or two grouping variables.
Faceting turns one chart into a grid of aligned panels, one per category, so distributions can be compared at a glance across one or two grouping variables.
Section 4.1 of 12

4.1 Core rules in faceting

To use faceting, you must understand Seaborn's two distinct types of functions. This is the most common stumbling block for beginners:

  • Axes-level functions (scatterplot, boxplot, histplot): draw onto a single, specific chart.
  • Figure-level functions (relplot, catplot, displot): control the entire figure canvas. Can split the figure into a grid using the col= (columns) or row= (rows) arguments.
Figure-level functions like displot own the whole canvas and can subdivide it with col= for faceting, while axes-level functions like histplot only draw a single chart and reject col= entirely.
Figure-level functions like displot own the whole canvas and can subdivide it with col= for faceting, while axes-level functions like histplot only draw a single chart and reject col= entirely.

Think of figure-level functions as "families." To facet your data, you call the family function, then tell it exactly which chart to draw using the kind= argument:

  • Categorical plots (boxplot, barplot) become catplot (e.g., kind="box")
  • Relational plots (scatterplot, lineplot) become relplot (e.g., kind="scatter")
  • Distributions (histplot, kdeplot) become displot (e.g., kind="hist")
A figure-level function is one family that draws many chart types, and the kind= argument is the switch that picks which one.
A figure-level function is one family that draws many chart types, and the kind= argument is the switch that picks which one.
Try it out

Try this snippet in the Python Scratchpad on the right.

Try this snippet
import seaborn as sns
import matplotlib.pyplot as plt
import pandas as pd

df = pd.DataFrame({
    "clinic": ["A"]*8 + ["B"]*8,
    "sex": (["F"]*4 + ["M"]*4) * 2,
    "bmi": [22.5, 23.0, 24.0, 25.5, 28.0, 29.0, 30.5, 31.0,
             24.0, 25.0, 26.5, 27.0, 28.5, 29.5, 30.0, 31.5],
})
sns.catplot(data=df, x="clinic", y="bmi", col="sex", kind="box")
plt.show()

That single line above draws two side-by-side boxplots—one for female patients and one for male—each comparing clinics A and B.

If you pass both row= and col=, you get a full grid of panels. You can control the size of these panels using the height and aspect arguments (for example, height=4, aspect=1.2 is a standard starting point for professional reports).

Try it out

Try this snippet in the Python Scratchpad on the right.

Try this snippet
import seaborn as sns
import matplotlib.pyplot as plt
import pandas as pd

df = pd.DataFrame({
    "age": [54, 61, 47, 72, 38, 55, 49, 67, 41, 58, 63, 45],
    "bmi": [27.3, 28.4, 26.1, 30.2, 24.5, 27.0,
             25.8, 29.7, 24.0, 28.1, 29.5, 25.5],
    "sex": ["F", "M"] * 6,
    "clinic": ["A", "A", "B", "B"] * 3,
})
sns.relplot(data=df, x="age", y="bmi", col="sex", row="clinic", kind="scatter")
plt.show()
Section 4.2 of 12

4.2 Two essential explorers

There are two dedicated figure-level functions built specifically for data exploration.

sns.pairplot is the chart you should draw on day one with any new multi-column numeric dataset. It automatically draws scatterplots for every possible pair of numeric columns, plus histograms down the diagonal.

Try it out

Try this snippet in the Python Scratchpad on the right.

Try this snippet
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
from pyodide.http import open_url

url = "https://raw.githubusercontent.com/mwaskom/seaborn-data/master/iris.csv"
iris = pd.read_csv(open_url(url))

sns.pairplot(iris, hue="species")
plt.show()
sns.pairplot lays every pair of numeric columns into an N×N grid where the off-diagonals are scatterplots, the diagonal is each column's histogram, and hue colors the points by a categorical column so cluster separation is visible at a glance.
sns.pairplot lays every pair of numeric columns into an N×N grid where the off-diagonals are scatterplots, the diagonal is each column's histogram, and hue colors the points by a categorical column so cluster separation is visible at a glance.

sns.jointplot is a highly focused tool for deep-diving into a single pair of variables. It places a scatterplot in the center and attaches the individual distributions along the top and right edges.

You can pass kind="reg" to add a regression line, or kind="hex" if you have so many data points that a standard scatterplot turns into an unreadable black blob.

Try it out

Try this snippet in the Python Scratchpad on the right.

Try this snippet
import seaborn as sns
import matplotlib.pyplot as plt
import pandas as pd

df = pd.DataFrame({
 "bmi": [25.0, 27.2, 24.1, 28.5, 29.0, 26.3],
 "glucose": [5.4, 6.1, 4.9, 5.8, 6.7, 5.1],
})
sns.jointplot(data=df, x="bmi", y="glucose", kind="reg")
plt.show()
sns.jointplot shows one pair of variables as a central scatter with marginal histograms on the top and right, where kind="reg" adds a regression line and kind="hex" replaces dots with density-coloured hex bins when the data is too crowded for individual points to read.
sns.jointplot shows one pair of variables as a central scatter with marginal histograms on the top and right, where kind="reg" adds a regression line and kind="hex" replaces dots with density-coloured hex bins when the data is too crowded for individual points to read.

When do you use the different plots?

  • catplot, relplot, displot: Use these whenever you need to facet data into rows or columns.
  • pairplot: Use this for first-day exploration on a new dataset.
  • jointplot: Use this to deep-dive into two specific variables and their marginals.
  • Axes-level functions (scatterplot, boxplot, etc.): Keep these for situations where you are manually managing a single set of axes yourself, usually as part of a highly custom matplotlib figure.
Different seaborn functions answer different questions, so the choice between catplot/relplot/displot (faceting), pairplot (broad first look), jointplot (two-variable deep-dive) and the axes-level functions (custom matplotlib work) is driven by what you are trying to see, not by personal preference.
Different seaborn functions answer different questions, so the choice between catplot/relplot/displot (faceting), pairplot (broad first look), jointplot (two-variable deep-dive) and the axes-level functions (custom matplotlib work) is driven by what you are trying to see, not by personal preference.
Section 5 of 12

5 Themes and palettes

Seaborn's default theme is perfectly fine when you are just exploring your data, but it can look a bit too plain for a slide deck or a final report.

To fix this, you can use sns.set_theme(). Calling this once at the top of your script instantly updates the look of every plot that follows. It uses two incredibly helpful arguments:

  • The style argument changes the background layout. Your options are "whitegrid", "darkgrid", "white", or "ticks".
  • The context argument automatically scales your font sizes and line widths so they are easily readable in their final destination. Your options are "paper", "notebook", "talk", or "poster".
sns.set_theme() takes two arguments — style swaps the background, grid, and axes, while context proportionally scales fonts and line widths inside a fixed figure, with poster so large it crowds out the plot area itself.
sns.set_theme() takes two arguments — style swaps the background, grid, and axes, while context proportionally scales fonts and line widths inside a fixed figure, with poster so large it crowds out the plot area itself.

You also have full control over the colors. You can change the global color scheme using sns.set_palette(), or you can just add the palette= argument to an individual plot. Two excellent, highly accessible defaults to keep in your back pocket are "viridis" and "colorblind".

Colours in seaborn can be set globally with sns.set_palette() or for one plot only via the palette= argument, and "viridis" and "colorblind" are two accessible defaults that work identically with either method.
Colours in seaborn can be set globally with sns.set_palette() or for one plot only via the palette= argument, and "viridis" and "colorblind" are two accessible defaults that work identically with either method.

Here is how all of that comes together to create a presentation-ready chart:

Try it out

Try this snippet in the Python Scratchpad on the right.

Try this snippet
import seaborn as sns
import matplotlib.pyplot as plt
import pandas as pd

sns.set_theme(style="whitegrid", context="talk")

df = pd.DataFrame({
    "clinic": ["A", "A", "B", "B", "C", "C"],
    "sex": ["F", "M", "F", "M", "F", "M"],
    "bmi": [27.4, 29.0, 31.1, 30.5, 24.8, 26.1],
})
sns.barplot(data=df, x="clinic", y="bmi", hue="sex", palette="colorblind")
plt.show()
Section 6 of 12

6 Putting it together

The plot families across both parts of this Seaborn module are usually used together. A typical first look at a new clinical dataset is a four-panel summary: a barplot showing the mean of an outcome per clinic, a boxplot of a key measurement per group, a scatter or regplot of two related measurements, and a heatmap of all the pairwise correlations. The worked example below builds something close to that on a small inline DataFrame.

Worked example · Read a patient CSV and produce a multi-panel summary figure

Work through this example in three stages. You unlock each stage only after the tutor confirms the previous one. Each stage removes more of the scaffolding — by the end you are writing it yourself.

Problem: You have a CSV called patients.csv with columns patient_id, clinic, sex, age, bmi, glucose, chol. Read it, then produce: (1) a faceted boxplot of bmi by clinic, faceted by sex; (2) a regression plot of age vs bmi, with the points coloured by sex; (3) a heatmap of correlations among age, bmi, glucose, and chol. Save each as a PNG.

Stage 1 · Study the solved example
Fully solved solution
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt

df = pd.read_csv("patients.csv")

g = sns.catplot(data=df, x="clinic", y="bmi", col="sex", kind="box")
g.savefig("bmi_by_clinic_by_sex.png")

sns.lmplot(data=df, x="age", y="bmi", hue="sex")
plt.savefig("age_vs_bmi_by_sex.png")

corr = df[["age", "bmi", "glucose", "chol"]].corr()
sns.heatmap(corr, annot=True, cmap="coolwarm", vmin=-1, vmax=1, center=0)
plt.savefig("correlation_heatmap.png")
Walk-through
  1. Read the CSV with pd.read_csv into a DataFrame called df.
  2. Use sns.catplot with kind="box" and col="sex" to draw two side-by-side boxplots — one per sex — each comparing bmi across clinics. catplot is figure-level, so it returns a FacetGrid object with its own savefig method.
  3. Use sns.lmplot with hue="sex" to draw a single scatter of age vs bmi with points coloured by sex and one regression line per sex. lmplot is also figure-level.
  4. Pull out the four numeric columns with df[[...]], call .corr() to get the correlation matrix, and pass it to sns.heatmap with annot=True for the numbers and the diverging coolwarm palette pinned to (-1, 1) so colours mean the same thing every time.
  5. plt.savefig writes whatever the most recent axes-level call drew. FacetGrid objects (returned by catplot and lmplot) have their own savefig method, which is the safer choice for figure-level plots.
Parsons problem · Read a CSV and produce a faceted distribution figure

All the lines you need are in the Line bank on the left — some may be distractors you should leave behind. Drag the lines you need into the Your solution column on the right, in the correct order, then click Check.

Task: Assemble a script that reads patients.csv, computes a new bmi_band column with three levels ("under_25", "25_to_30", "over_30") based on the bmi column, and draws a faceted countplot of patients per bmi_band, split into one panel per clinic.

Line bank
  • sns.catplot(data=df, x="clinic", y="bmi_band", kind="box")
  • df = pd.read_csv("patients.csv")
  • import seaborn as sns
  • df["bmi_band"] = pd.cut(df["bmi"], bins=[0, 25, 30, 100], labels=["under_25", "25_to_30", "over_30"])
  • import matplotlib.pyplot as plt
  • df = pd.read_excel("patients.csv")
  • df["bmi_band"] = df["bmi"].apply(lambda x: "high" if x > 25 else "low")
  • sns.countplot(data=df, x="bmi_band", col="clinic")
  • plt.show()
  • import pandas as pd
  • sns.catplot(data=df, x="bmi_band", col="clinic", kind="count")
Your solution
  • Drop lines here, in order.
Reflect

Generating a reflection question for you…

Section 7 of 12

7 Check your understanding

You have reached the end of the module. Try the same questions again — your answers here, paired with your pre-test answers, are how we measure what the module taught you. Answer every question and confidence rating, then click Submit and see results to view your score.

Post-test

You draw a heatmap of a correlation matrix. Which set of arguments makes the colour scale comparable across different correlation matrices?

Post-test

You have two categorical columns and want a heatmap of how many records fall into each combination, with whole-number labels in the cells. Which approach is correct?

Post-test

You are drawing a heatmap of counts that range from 0 upward, with no negative values. Which colour map best matches the data?

Post-test

Which seaborn function lets you split a scatter-plus-regression into a row of panels, one per level of a third column?

Post-test

What is the difference between an axes-level and a figure-level seaborn function?

Post-test

You just loaded a dataset with six numeric columns and want a quick first look at every pairwise relationship at once. Which function fits best?

Post-test

You want every seaborn chart in your script to share a white-grid background with larger, slide-friendly fonts, set once at the top. Which call does this?

Post-test

You want one colour-blind-safe scheme applied to every chart in the script, set a single time. Which call does this?

Post-confidence

I can draw a multi-panel figure faceted by another column using catplot, relplot, or displot in a single line.

Not at all confident
Fully confident
Post-confidence

I can produce a correlation heatmap from a DataFrame and pin the colour scale so it means the same thing every time.

Not at all confident
Fully confident
Post-confidence

I can explain the difference between an axes-level and a figure-level seaborn function, and use pairplot or jointplot to explore several numeric columns at once.

Not at all confident
Fully confident
Section 8 of 12

8 Your results

Here is how your post-test answers compare with your pre-test answers. The pre/post pairing is the most reliable way to see what this module actually taught you.

Your score

Submit the post-test to see your results.

Muddiest point

What is the one thing from this module that is still unclear to you?

Rate this module

Overall, how would you rate this module?

How likely are you to recommend this module to a peer? (0 = not at all, 10 = extremely likely)