Skip to content
Laviyx Learn

Try

Python for Data course menu

seaborn Tutorial: Statistical Charts in a Few Lines of Python

Make statistical charts with seaborn from pandas DataFrames: bar plots with averages, box and violin plots, histograms with KDE, scatter plots with hue, regression lines, correlation heatmaps and faceted plots.

Intermediate12 min readUpdated
On this page

seaborn builds on matplotlib and works directly with pandas DataFrames: you name the columns, and it handles grouping, colours, legends and statistical summaries. Charts that need ten lines of matplotlib often take one line of seaborn.

All examples use this simulated restaurant dataset (in long format: one row per bill):

Python
import numpy as np
import pandas as pd

rng = np.random.default_rng(12)
n = 240
bills = pd.DataFrame({
    "day": rng.choice(["Thu", "Fri", "Sat", "Sun"], n, p=[0.2, 0.2, 0.3, 0.3]),
    "time": rng.choice(["Lunch", "Dinner"], n, p=[0.35, 0.65]),
    "size": rng.integers(1, 7, n),
})
bills["total"] = (180 * bills["size"] + rng.normal(0, 120, n)
                  + np.where(bills["time"] == "Dinner", 150, 0)).round(0).clip(lower=120)
bills["tip"] = (bills["total"] * rng.uniform(0.05, 0.2, n)).round(0)
print(bills.head())
print(bills.shape)
Output
   day    time  size  total    tip
0  Fri   Lunch     1  120.0   13.0
1  Sun   Lunch     2  224.0   26.0
2  Thu   Lunch     3  477.0   25.0
3  Thu  Dinner     3  672.0   42.0
4  Fri   Lunch     5  953.0  119.0
(240, 5)

Comparing groups: barplot and boxplot#

sns.barplot shows the mean of a column for each category, with an error bar for its uncertainty. sns.boxplot shows each group's whole distribution:

Python
import numpy as np
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt

rng = np.random.default_rng(12)
n = 240
bills = pd.DataFrame({
    "day": rng.choice(["Thu", "Fri", "Sat", "Sun"], n, p=[0.2, 0.2, 0.3, 0.3]),
    "time": rng.choice(["Lunch", "Dinner"], n, p=[0.35, 0.65]),
    "size": rng.integers(1, 7, n),
})
bills["total"] = (180 * bills["size"] + rng.normal(0, 120, n)
                  + np.where(bills["time"] == "Dinner", 150, 0)).round(0).clip(lower=120)

order = ["Thu", "Fri", "Sat", "Sun"]
fig, axes = plt.subplots(1, 2, figsize=(11, 4))
sns.barplot(data=bills, x="day", y="total", order=order, ax=axes[0])
axes[0].set_title("Average bill by day")
sns.boxplot(data=bills, x="time", y="total", hue="time", ax=axes[1])
axes[1].set_title("Bills at lunch and dinner")
fig.tight_layout()
plt.show()

Left: bars showing a similar average bill on each day, each with a black error bar. Right: box plots where dinner bills are generally higher than lunch bills.

Notice there is no grouping code: seaborn computes the means and quartiles itself. The error bars are 95% confidence intervals for the mean by default.

Distributions: histplot with KDE#

Python
import numpy as np
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt

rng = np.random.default_rng(12)
n = 240
bills = pd.DataFrame({
    "day": rng.choice(["Thu", "Fri", "Sat", "Sun"], n, p=[0.2, 0.2, 0.3, 0.3]),
    "time": rng.choice(["Lunch", "Dinner"], n, p=[0.35, 0.65]),
    "size": rng.integers(1, 7, n),
})
bills["total"] = (180 * bills["size"] + rng.normal(0, 120, n)
                  + np.where(bills["time"] == "Dinner", 150, 0)).round(0).clip(lower=120)

fig, ax = plt.subplots(figsize=(7, 4))
sns.histplot(data=bills, x="total", hue="time", kde=True, bins=20, ax=ax)
ax.set_title("Bill totals by meal")
plt.show()

Overlapping histograms of bill totals for lunch and dinner, each with a smooth density curve; dinner bills are more common and spread further to the right.

kde=True adds a smooth kernel density estimate curve over the histogram.

Relationships: scatterplot and regplot#

Python
import numpy as np
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt

rng = np.random.default_rng(12)
n = 240
bills = pd.DataFrame({
    "day": rng.choice(["Thu", "Fri", "Sat", "Sun"], n, p=[0.2, 0.2, 0.3, 0.3]),
    "time": rng.choice(["Lunch", "Dinner"], n, p=[0.35, 0.65]),
    "size": rng.integers(1, 7, n),
})
bills["total"] = (180 * bills["size"] + rng.normal(0, 120, n)
                  + np.where(bills["time"] == "Dinner", 150, 0)).round(0).clip(lower=120)
bills["tip"] = (bills["total"] * rng.uniform(0.05, 0.2, n)).round(0)

fig, axes = plt.subplots(1, 2, figsize=(11, 4))
sns.scatterplot(data=bills, x="total", y="tip", hue="time", ax=axes[0])
axes[0].set_title("Tip vs bill, coloured by meal")
sns.regplot(data=bills, x="size", y="total", ax=axes[1], x_jitter=0.15)
axes[1].set_title("Bill vs party size, with trend line")
fig.tight_layout()
plt.show()

Left: a scatter of tips against bill totals, with lunch and dinner in different colours; tips fan out as bills grow. Right: bills rising steadily with party size, with a fitted regression line and shaded confidence band.

hue colours points by a category; regplot adds a fitted straight line with a shaded confidence band. x_jitter spreads overlapping points that share the same party size.

Correlation heatmap#

A heatmap is the standard way to show a correlation matrix:

Python
import numpy as np
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt

rng = np.random.default_rng(12)
n = 240
bills = pd.DataFrame({"size": rng.integers(1, 7, n)})
bills["total"] = (180 * bills["size"] + rng.normal(0, 120, n)).round(0).clip(lower=120)
bills["tip"] = (bills["total"] * rng.uniform(0.05, 0.2, n)).round(0)
bills["wait_min"] = rng.normal(20, 6, n).round(0)

corr = bills.corr()
fig, ax = plt.subplots(figsize=(5, 4))
sns.heatmap(corr, annot=True, fmt=".2f", cmap="RdBu_r", vmin=-1, vmax=1, ax=ax)
ax.set_title("Correlation matrix")
plt.show()
print(corr.round(2))
Output
          size  total   tip  wait_min
size      1.00   0.93  0.71      0.02
total     0.93   1.00  0.76      0.01
tip       0.71   0.76  1.00      0.07
wait_min  0.02   0.01  0.07      1.00

A heatmap of correlations between size, total, tip and wait time: size, total and tip are strongly positively correlated (dark red), while wait time is near zero with everything (pale).

annot=True writes the values; fixing vmin=-1, vmax=1 with a diverging palette (RdBu_r) makes the colour meaning consistent across charts.

Facets: one chart per group#

Figure-level functions such as sns.relplot, sns.displot and sns.catplot can split the data into a grid of charts with col and row:

Python
import numpy as np
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt

rng = np.random.default_rng(12)
n = 240
bills = pd.DataFrame({
    "time": rng.choice(["Lunch", "Dinner"], n, p=[0.35, 0.65]),
    "size": rng.integers(1, 7, n),
})
bills["total"] = (180 * bills["size"] + rng.normal(0, 120, n)
                  + np.where(bills["time"] == "Dinner", 150, 0)).round(0).clip(lower=120)
bills["tip"] = (bills["total"] * rng.uniform(0.05, 0.2, n)).round(0)

grid = sns.relplot(data=bills, x="total", y="tip", col="time", height=3.5, aspect=1.2)
grid.set_titles("{col_name}")
plt.show()

Two side-by-side scatter plots of tip against bill total, one for Dinner and one for Lunch, sharing the same axes.

Common mistakes#

Practice#

Practice exercise

Using the bills data from the first example, draw a sns.countplot of how many bills there were on each day (in the order Thu, Fri, Sat, Sun), and print the counts per day.

Show hint

sns.countplot(data=bills, x="day", order=[...]) counts rows; bills["day"].value_counts() gives the same numbers.

Show solution
Python
import numpy as np
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt

rng = np.random.default_rng(12)
n = 240
bills = pd.DataFrame({
    "day": rng.choice(["Thu", "Fri", "Sat", "Sun"], n, p=[0.2, 0.2, 0.3, 0.3]),
    "time": rng.choice(["Lunch", "Dinner"], n, p=[0.35, 0.65]),
    "size": rng.integers(1, 7, n),
})

order = ["Thu", "Fri", "Sat", "Sun"]
fig, ax = plt.subplots(figsize=(6, 3.5))
sns.countplot(data=bills, x="day", order=order, ax=ax)
ax.set_title("Bills per day")
plt.show()
print(bills["day"].value_counts().reindex(order))
Output
day
Thu    56
Fri    38
Sat    68
Sun    78
Name: count, dtype: int64

A bar chart counting bills per day, with Saturday and Sunday having more bills than Thursday and Friday.

Key takeaway#

Next: Plotting Directly from pandas.

Finished this lesson?