Skip to content

Seaborn foundations

Chapter 14 8 code blocks 2862 words

Part I ended with a DataFrame. Part II turns that DataFrame into something a human can understand at a glance.

This chapter builds the mental model — what Seaborn is, how it relates to matplotlib, the one data shape it insists on, and the map of its whole function library. Get these four things right and every chapter after this one is just parameters.

What is Seaborn?

Seaborn is a Python library for statistical data visualisation, built on top of matplotlib and designed to work directly with pandas DataFrames.

The three things it actually gives you:

  1. It speaks DataFrame. You pass data=df and then refer to columns by name: x="total_bill". Seaborn reads the column, labels the axis with it, and picks a sensible scale. In matplotlib you extract arrays and label everything yourself.
  2. It does statistics for you. One call computes a mean and a confidence interval, or a kernel density estimate, or a linear fit, and draws the result. In matplotlib you compute those first, then draw them.
  3. It has taste. Colours, spacing, gridlines and fonts are chosen to be readable by default, not by accident.
🧩 Analogy

Matplotlib is a box of art supplies: pencils, rulers, paint. You can draw anything, but you draw every line yourself. Seaborn is a set of well-designed templates built from those same supplies — you say "box plot of tips by day" and it handles the rest. The supplies are still in the box, so anything the template does not cover, you can still draw by hand.

Seaborn vs matplotlib

Students often think they must choose. They must not — Seaborn is matplotlib. Every Seaborn function creates matplotlib objects, and you finish nearly every real plot with a few matplotlib calls.

The same chart, both ways:

python13 lines
# matplotlib: you do the grouping, the positioning and the labelling
import matplotlib.pyplot as plt

means = tips.groupby("day")["total_bill"].mean()
plt.bar(means.index, means.values)
plt.xlabel("day")
plt.ylabel("total_bill")
plt.title("Average bill by day")

# seaborn: one line, and the error bars come free
import seaborn as sns

sns.barplot(data=tips, x="day", y="total_bill")

matplotlibSeaborn

InputArrays / listsDataFrame + column names Axis labelsYou write themTaken from column names StatisticsYou compute them firstComputed inside the call Grouping by a categoryA loophue= Default appearancePlainConsidered ControlTotalHigh, with matplotlib underneath Best forAnything unusualStandard statistical charts

:::info 📌 The working relationship Seaborn draws the chart; matplotlib finishes it. A realistic plot is one Seaborn call followed by plt.title(), plt.xticks(rotation=45) and plt.savefig(). That is the normal pattern, not a failure of Seaborn. :::

Installing and importing

`pip install seaborn # brings matplotlib, pandas and numpy with it

conda install seaborn`

python6 lines
import seaborn as sns              # the universal alias
import matplotlib.pyplot as plt
import pandas as pd

sns.set_theme(style="whitegrid")   # apply Seaborn's styling (Chapter 21)
print(sns.__version__)             # check: this guide assumes 0.12+
Output
0.13.2

:::gotcha ⚠️ Version matters more than usual here Seaborn changed significantly at 0.12. Old tutorials still show sns.distplot() — deprecated since 0.11 and replaced by histplot/kdeplot/displot — and ci=95, which is deprecated in favour of errorbar=("ci", 95). If a student's code raises "unexpected keyword argument", the tutorial is older than their library. Always check sns.__version__ first. :::

💡 Why `sns`?

It is a joke that stuck: the library is named after Samuel Norman Seaborn, a character from The West Wing. Students always ask, so now you know.

Tidy data — the one rule that matters

Nearly every Seaborn problem a beginner hits is really a data shape problem. Seaborn expects long (tidy) format:

The tidy data ruleEach row = one observation Each column = one variable Each cell = one value

Compare the same information in both shapes:

Wide format — readable for humans, wrong for Seaborn:

student math physics english
Aziza 92 88 95
Bekzod 75 64 70

Long format — what Seaborn wants:

student subject score
Aziza math 92
Aziza physics 88
Aziza english 95
Bekzod math 75
Bekzod physics 64
Bekzod english 70

In the long version, "subject" is a real column — so you can write hue="subject". In the wide version there is no such column, only three column names, and there is nothing to hand to hue=. That is the whole difficulty.

`# wide → long long_df = wide_df.melt(id_vars="student", var_name="subject", value_name="score")

long → wide (for a heatmap, Chapter 18)

wide_df = long_df.pivot(index="student", columns="subject", values="score"):::info 📌 Why Part I was not wasted A normalized database joined together produces long format **automatically** — one row per enrollment, with student, course and grade as columns. That is exactly the shape Seaborn wants. Your SQL query is already doing the reshaping work;melt` is only needed when data arrives from a spreadsheet.

And if your data is wide in the database, Chapter 9's UNION ALL unpivot does the same job before it ever reaches Python. :::

⚠️ Heatmap is the exception

sns.heatmap() is the one function that wants wide data — a matrix. Everything else wants long. When students say "seaborn is inconsistent", this is usually what they mean; it is worth flagging before Chapter 18 rather than after.

The built-in datasets

Seaborn ships example datasets so you can teach without hunting for data.

python7 lines
tips     = sns.load_dataset("tips")        # 244 restaurant bills
penguins = sns.load_dataset("penguins")    # 344 penguins, 3 species
titanic  = sns.load_dataset("titanic")     # 891 passengers
flights  = sns.load_dataset("flights")     # monthly passengers 1949-1960
iris     = sns.load_dataset("iris")        # the classic 150 flowers

print(sns.get_dataset_names())             # the full list
Output
['anagrams', 'anscombe', 'attention', 'brain_networks', 'car_crashes', 'diamonds', 'dots', 'dowjones', 'exercise', 'flights', 'fmri', 'geyser', 'glue', 'healthexp', 'iris', 'mpg', 'penguins', 'planets', 'seaice', 'taxis', 'tips', 'titanic']

DatasetRowsGood for teaching

tips244Categories + numbers: bill, tip, day, sex, smoker, party size penguins344Distributions and clusters; contains real NaNs titanic891Binary outcomes, missing data, logistic regression flights144Time series, seasonality, and heatmaps after pivoting iris150pairplot and separable groups

:::gotcha ⚠️ load_dataset() downloads from the internet It fetches from GitHub every time and is not bundled with the library. In a classroom with no reliable connection, every example fails at line one.

Prepare offline copies once, at home:

python8 lines
# run once where you DO have internet
import seaborn as sns
for name in ["tips", "penguins", "titanic", "flights", "iris"]:
    sns.load_dataset(name).to_csv(f"data/{name}.csv", index=False)

# then in class, no internet needed
import pandas as pd
tips = pd.read_csv("data/tips.csv")

Everything else in Part II works identically — the DataFrame is the same either way. :::

Your first plots

The minimum viable Seaborn call is one line:

python6 lines
import seaborn as sns
import matplotlib.pyplot as plt

tips = sns.load_dataset("tips")
sns.scatterplot(data=tips, x="total_bill", y="tip")
plt.show()

One line of code: sns.scatterplot(data=tips, x="total_bill", y="tip", hue="time"). The legend is automatic.

Read the arguments as a sentence: "scatter plot, using the tips data, total_bill across the bottom, tip up the side." Every Seaborn function follows this pattern, which is why learning the first one teaches you most of the rest.

Adding a third variable takes one more word:

sns.scatterplot(data=tips, x="total_bill", y="tip", hue="time") Now lunch and dinner are drawn in different colours, and a legend appears automatically. You wrote no loop, no colour list and no legend code.

A realistic, presentation-ready plot — the pattern used throughout Part II:

python12 lines
sns.set_theme(style="whitegrid")          # 1. styling
plt.figure(figsize=(9, 5))                # 2. size (matplotlib)

sns.scatterplot(data=tips, x="total_bill", y="tip",
                hue="time", size="size", alpha=0.7)   # 3. the plot

plt.title("Tip vs total bill")            # 4. finishing (matplotlib)
plt.xlabel("Total bill (USD)")
plt.ylabel("Tip (USD)")
plt.tight_layout()
plt.savefig("tips.png", dpi=300, bbox_inches="tight")   # 5. save BEFORE show
plt.show()

:::gotcha ⚠️ Save before you show plt.show() clears the current figure. Calling plt.savefig() afterwards writes a blank image — with no error message. Always save first, then show. This catches everyone once. :::

The map of every Seaborn function

Seaborn's ~25 plotting functions look like a lot until you see that they fall into four families, each with one figure-level function on top.

The whole library in one diagramRELATIONAL → relplot() → scatterplot, lineplot DISTRIBUTION → displot() → histplot, kdeplot, ecdfplot, rugplot CATEGORICAL → catplot() → barplot, countplot, boxplot, violinplot, boxenplot, stripplot, swarmplot, pointplot REGRESSION → lmplot() → regplot, residplot MATRIX → heatmap, clustermap (wide data) GRIDS → pairplot, jointplot, FacetGrid, PairGrid, JointGrid
Your question Family Chapter
How do two numbers relate? Relational 15
How is one number spread out? Distribution 16
How does a number differ between categories? Categorical 17
How do many numbers relate to each other at once? Matrix 18
Is there a trend, and how strong? Regression 19
Does the pattern hold in every subgroup? Grids 20
💡 Teach the question, not the function

Students who memorise function names freeze in front of a real dataset. Students who ask "what question am I answering?" find the function from the table above in seconds. Put that table on the wall.

Axes-level vs figure-level

This distinction explains more confusing Seaborn behaviour than anything else, so meet it now rather than in Chapter 20.

Axes-level Figure-level
Examples scatterplot, histplot, boxplot, regplot relplot, displot, catplot, lmplot
Draws into One existing Axes Its own new figure
Returns A matplotlib Axes A Seaborn Grid
Accepts ax= Yes No
Can facet with col=/row= No Yes
Size control plt.figure(figsize=...) height=, aspect=
Title plt.title() g.figure.suptitle()
python8 lines
# axes-level: fits into a matplotlib layout you control
fig, axes = plt.subplots(1, 2, figsize=(12, 4))
sns.histplot(data=tips, x="total_bill", ax=axes[0])
sns.boxplot(data=tips, x="day", y="total_bill", ax=axes[1])

# figure-level: owns the whole figure, but can split by a column
g = sns.relplot(data=tips, x="total_bill", y="tip", col="time", height=4)
g.figure.suptitle("Tips by service", y=1.03)

:::gotcha ⚠️ The two symptoms students report - "My title only appeared on the last panel." You used plt.title() on a figure-level plot. It titles the current Axes, which is the last subplot. Use g.figure.suptitle(). - "figsize did nothing." Figure-level functions build their own figure and ignore plt.figure(figsize=...). Use height= and aspect= instead — width is height × aspect. :::

💡 Which to reach for

Use the axes-level function by default — it composes with matplotlib and fits into dashboards. Switch to the figure-level one the moment you want col= or row= faceting, which is the one thing axes-level functions cannot do.

hue, size, style — semantic mapping

Seaborn's most powerful idea has a formal name: semantic mapping — attaching a data column to a visual property.

Parameter Visual property Best for
hue= Colour Categories, or a continuous value
size= Marker size / line width A continuous quantity
style= Marker shape / line dashes A few categories (2–4)
col=, row= Separate panels Categories, when overlap is confusing
python6 lines
# four variables in one chart — and one more via faceting makes five
sns.relplot(data=tips,
            x="total_bill", y="tip",     # 1 and 2: position
            hue="day",                   # 3: colour
            size="size",                 # 4: point size
            col="time")                  # 5: panel

:::gotcha ⚠️ Just because you can does not mean you should A chart encoding five variables is usually unreadable. Position (x and y) is by far the most accurate channel a reader has; colour is next; size is genuinely poor — humans judge area badly. Two or three variables per chart is the practical limit. Beyond that, use faceting (Chapter 20), which lets the reader compare positions across panels instead of decoding a crowded legend. :::

⚠️ `hue` on a continuous column

Give hue= a numeric column and Seaborn switches to a continuous colourbar instead of a categorical legend. That is correct behaviour, but if the column is really a category stored as numbers (a rating of 1–5, a course_id), convert it first — df["rating"].astype(str) or astype("category") — or you get a meaningless gradient.

Common mistakes

Mistake What happens The fix
Passing wide data to a long-format function Wrong chart, or an error df.melt(...) first
Passing long data to heatmap Error or nonsense pivot it into a matrix
plt.savefig() after plt.show() Blank image, no error Save first, then show
plt.title() on a figure-level plot Only the last panel is titled g.figure.suptitle()
plt.figure(figsize=) with relplot Ignored height=, aspect=
Using sns.distplot() Deprecated; warns and will be removed histplot / kdeplot / displot
Using ci=95 Deprecated warning errorbar=("ci", 95)
Passing numpy arrays instead of a DataFrame Axes lose their labels Pass data=df and column names
No internet in class load_dataset() fails Save CSVs beforehand
Encoding five variables at once Unreadable Facet instead

Interview questions

Q: What is Seaborn and how does it relate to matplotlib?

A statistical visualisation library built on top of matplotlib. It adds DataFrame-aware plotting, built-in statistical estimation and better defaults, but every object it produces is a matplotlib object, so the two are used together rather than as alternatives.

Q: What data format does Seaborn expect?

Long (tidy) format: one row per observation, one column per variable. The exception is heatmap, which needs a wide matrix. Convert with melt() and pivot().

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

Axes-level functions draw into a single existing Axes, accept ax= and return an Axes. Figure-level functions create their own figure, return a Grid object, support col=/row= faceting, and take height=/aspect= instead of figsize=.

Q: Why did plt.title() not work on your catplot?

Because catplot is figure-level. plt.title() sets the title of the current Axes — the last subplot. Use g.figure.suptitle().

Q: What do hue, size and style do?

They map a data column to colour, marker size and marker/line style respectively. This is called semantic mapping and it is how you add a third or fourth variable to a chart.

Q: How many variables should one chart encode?

Realistically two or three. Position is the most accurately perceived channel, colour is next, and size is poor. Beyond three, facet into small multiples.

Q: What replaced distplot?

histplot, kdeplot and ecdfplot at the axes level, with displot as the figure-level entry point. distplot has been deprecated since 0.11 — it still runs in 0.13 but emits a warning, and it will be removed.

Q: Your saved PNG is blank. Why?

plt.savefig() was called after plt.show(), which clears the figure. Save before showing.

Summary

  • Seaborn is matplotlib with statistics and taste. It reads DataFrames by column name, computes estimates for you, and returns matplotlib objects you can finish by hand.
  • Long (tidy) format is the rule: one row per observation, one column per variable. heatmap is the one exception and wants a wide matrix.
  • A JOIN of a normalized database already produces long format — Part I did the reshaping work for you.
  • load_dataset() needs internet. Save CSVs in advance for classroom use.
  • Four families — relational, distribution, categorical, regression — plus matrix and grids. Choose by the question, not the function name.
  • Axes-level (scatterplot, boxplot) accepts ax= and composes with matplotlib. Figure-level (relplot, catplot) owns its figure, supports col=/row=, and needs g.figure.suptitle() and height=/aspect=.
  • hue, size, style, col, row map columns to visual properties — but two or three variables per chart is the readable limit.
  • Save before you show, or you get a blank file.
  • Check sns.__version__: this guide assumes 0.12+, where distplot is gone and ci= became errorbar=.

Next: Chapter 15 starts with relational plots — scatterplot, lineplot and relplot.

Questions about this chapter?

The course, homework review and the other five books are in Uzbek. Questions in English are welcome on Telegram.

Write on Telegram Contents