Seaborn foundations
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:
- It speaks DataFrame. You pass
data=dfand 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. - 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.
- It has taste. Colours, spacing, gridlines and fonts are chosen to be readable by default, not by accident.
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:
# 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`
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+
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.
:::
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:
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.
:::
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.
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
['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:
# 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:
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:
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.
| 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 |
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() |
# 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.
:::
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 |
# 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. :::
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.
heatmapis 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) acceptsax=and composes with matplotlib. Figure-level (relplot,catplot) owns its figure, supportscol=/row=, and needsg.figure.suptitle()andheight=/aspect=. hue,size,style,col,rowmap 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+, wheredistplotis gone andci=becameerrorbar=.
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.