Seaborn: Statistical Visualization, Heatmaps, Distributions

You have just spent twenty five lines of Matplotlib building one correlation heatmap, the everyday chore that Python Seaborn was built to shorten. Colorbar formatting, label rotation, value annotations, a title that keeps overlapping the cells. This seaborn tutorial exists so you can stop doing that. Seaborn sits on top of Matplotlib and turns those twenty five lines into one: distribution plots, heatmaps, pair plots, box plots, violin plots, and regression plots that look good before you touch a single setting.

“Seaborn makes Matplotlib beautiful by default.”

Michael Waskom, Seaborn creator

Last Updated: July 2026 | Tested on: Python 3.14.6, Seaborn 0.13.2 | Difficulty: Advanced | Reading Time: 14 minutes

Here is the simplest way to think about the two libraries. Matplotlib is the box of raw ingredients, and it gives you total control over every one of them. Seaborn is the meal kit: the ingredients are pre-measured, the recipe card is in the box, and you get a good dinner in ten minutes. When you need a custom plate, you go back to the raw ingredients. Most of the time during exploratory data analysis, the meal kit is exactly what you want. And if you want the full side-by-side of Matplotlib, Seaborn, and Plotly before committing, the visualization libraries comparison settles which tool fits which job.

The trade-off is that Python Seaborn is opinionated. It decides colors, spacing, and statistical overlays for you. When those decisions match what you want, which is most of the time while you are still poking at the data, Seaborn saves a huge amount of typing. When you need pixel-level control, you drop back to Matplotlib. The two work together, because every Seaborn plot is a Matplotlib object underneath.

Viraj, a 24 year old analyst on our team, was building a correlation heatmap for 15 features. In plain Matplotlib it took him 25 lines: colorbar formatting, label rotation, value annotations, the works. In Seaborn it was sns.heatmap(df.corr(), annot=True, cmap="coolwarm"). One line. Same chart. That is the whole pitch.

MatrixheatmappairplotclustermapCategoricalbarplotviolinplotboxplotstripplot /swarmplotDistributionhistplotecdfplotkdeplotrugplotRelationalscatterplotrelplotlineplot🎨 Seaborn Plot TypesPython Seaborn: Plot Types Grouped by Relational, Distribution, Categorical, Matrix

Tip: click the diagram to open it full screen, then use the zoom and pan controls for a closer look.

The diagram sorts Python Seaborn plot types into four families: relational (scatter, line), distribution (histogram, Kernel Density Estimate or KDE, box, violin), categorical (bar, count, strip, swarm), and matrix (heatmap, clustermap). Each family answers a different question. Relational plots show how two variables move together, distribution plots reveal the shape of one variable, categorical plots compare groups, and matrix plots show patterns across many variables at once. Picking the right family first narrows your choice from dozens of functions down to two or three.

Prerequisites

You should be comfortable with Pandas DataFrames and have worked through the Matplotlib subplots tutorial first, since Seaborn draws onto Matplotlib axes. Install Seaborn with pip install seaborn, which also pulls in Matplotlib, Pandas, and NumPy as dependencies. This guide was tested on Seaborn 0.13.2 (current stable at the time of writing) with Python 3.14.6.

Python Seaborn Quick Win: Better Defaults in One Line

The fastest way to feel what Python Seaborn does is to call sns.set_theme() once at the top of your script. That single line restyles every Matplotlib plot you draw afterward: nicer fonts, a soft grid, a sensible color palette. It is like swapping the default phone wallpaper for a designed one. You did not change what the phone does, but everything looks better instantly.

📄 seaborn_setup.py: one theme call, prettier everything

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

# Set the theme once. It affects every Matplotlib plot from here on.
sns.set_theme(style="darkgrid", palette="deep", font_scale=1.1)

# Sample data. Seed the generator so your numbers match the post.
rng = np.random.default_rng(42)
df = pd.DataFrame({
    "experience": rng.integers(1, 12, 100),
    "salary": rng.normal(75000, 15000, 100).astype(int),
    "department": rng.choice(["Eng", "DS", "Mkt"], 100),
    "satisfaction": rng.integers(1, 6, 100)
})

print(f"Seaborn version: {sns.__version__}")
print(f"Dataset shape: {df.shape}")
print(f"Sample:\n{df.head()}")

▶ Output

Seaborn version: 0.13.2
Dataset shape: (100, 4)
Sample:
   experience  salary department  satisfaction
0           1   85183         DS             5
1           9   76013        Mkt             4
2           8   79336        Eng             2
3           5   84469        Mkt             5
4           5   53142         DS             3

What happened here: We seeded NumPy with default_rng(42) so the random numbers come out the same every run, which is why your df.head() will match the table above exactly. We built a small DataFrame of fake employee data and confirmed the Seaborn version. Nothing has been plotted yet, but the theme is already set, so the moment we call a plotting function the chart will use Seaborn’s styling. One thing worth saying out loud: Seaborn understands column names. From here on we pass strings like x="salary" instead of raw arrays, and Seaborn pulls the right column from the DataFrame for us.

Distribution Plots

A distribution plot answers one question: what does the spread of this column look like? Is it a tidy bell curve, is it lopsided, are there two humps? Think of it like pouring a bag of coins onto a table and seeing where they pile up. Seaborn gives you three close cousins for this: histplot for bars, kdeplot for a smooth curve, and boxplot for a five number summary you can line up side by side across groups.

📄 distributions.py: histogram, KDE, and box plot side by side

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

rng = np.random.default_rng(42)
df = pd.DataFrame({
    "salary": rng.normal(75000, 15000, 500).astype(int),
    "dept": rng.choice(["Eng", "DS", "Mkt"], 500)
})

fig, axes = plt.subplots(1, 3, figsize=(16, 5))

# 1. Histogram with a smooth KDE curve overlaid
sns.histplot(data=df, x="salary", kde=True, ax=axes[0], color="#8be9fd")
axes[0].set_title("Histogram + KDE")

# 2. KDE curve, one line per department
sns.kdeplot(data=df, x="salary", hue="dept", ax=axes[1], fill=True, alpha=0.3)
axes[1].set_title("KDE by Department")

# 3. Box plot. Assign hue to color by group (the modern Seaborn 0.13 way).
sns.boxplot(data=df, x="dept", y="salary", hue="dept", palette="Set2",
            legend=False, ax=axes[2])
axes[2].set_title("Salary by Department")

plt.tight_layout()
plt.savefig("seaborn_distributions.png", dpi=150)
plt.show()

What happened here: Three charts in roughly a dozen lines. The histogram bins the salaries into bars and kde=True lays a smooth density curve on top so you can see the bell shape. The KDE chart draws one filled curve per department because we set hue="dept", which makes it easy to compare where each team’s pay clusters. The box plot shows the median, the middle 50 percent, and any outliers for each department in one glance.

One detail that trips people up on Seaborn 0.13: to color a box plot by group you now assign the grouping column to hue and add legend=False, rather than passing palette on its own. Passing a palette without a hue is deprecated and will be removed in Seaborn 0.14, so the version above is the future-proof way to write it.

Heatmaps: Correlation at a Glance

A heatmap turns a table of numbers into a grid of colors, so your eye spots the strong relationships before your brain reads a single digit. It is the same idea as a weather map: you do not measure the temperature of every city, you just look for the red blobs and the blue blobs. Feed sns.heatmap a correlation matrix and the hot colors jump out as the variables that move together.

📄 heatmap.py: visualize a correlation matrix

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

rng = np.random.default_rng(42)
n = 200
df = pd.DataFrame({
    "hours_studied": rng.uniform(1, 10, n),
    "sleep_hours": rng.uniform(4, 9, n),
    "exam_score": rng.normal(75, 12, n),
    "attendance": rng.uniform(50, 100, n),
    "projects": rng.integers(0, 5, n)
})
# Bake a real relationship into exam_score so the heatmap has something to show
df["exam_score"] = 40 + 3 * df["hours_studied"] + 2 * df["attendance"] / 10 + rng.normal(0, 8, n)

corr = df.corr()
print(corr.round(2))

fig, ax = plt.subplots(figsize=(8, 6))
sns.heatmap(corr, annot=True, cmap="coolwarm", center=0,
            fmt=".2f", linewidths=0.5, ax=ax,
            vmin=-1, vmax=1)
ax.set_title("Correlation Heatmap", fontsize=14, pad=15)
plt.tight_layout()
plt.savefig("correlation_heatmap.png", dpi=150)
plt.show()

▶ Output

               hours_studied  sleep_hours  exam_score  attendance  projects
hours_studied           1.00        -0.03        0.70        0.04     -0.07
sleep_hours            -0.03         1.00        0.00       -0.05     -0.13
exam_score              0.70         0.00        1.00        0.20     -0.08
attendance              0.04        -0.05        0.20        1.00      0.03
projects               -0.07        -0.13       -0.08        0.03      1.00

What happened here: We printed the correlation matrix so you can see the exact numbers the heatmap colors. The values run from -1 (perfect opposite) through 0 (no link) to 1 (perfect match), and the diagonal is always 1.00 because every column correlates perfectly with itself. The one we engineered stands out: hours_studied versus exam_score sits at 0.70, a strong positive link, because we deliberately built study hours into the score.

The rest hover near zero, which is what you expect from random data. On the chart, cmap="coolwarm" paints high values warm red and low values cool blue, center=0 keeps the neutral midpoint white, and annot=True writes each number into its cell so you get the picture and the precise figure together.

Pair Plot: All Relationships at Once

A pair plot is the power tool of early data exploration. In one call it draws a scatter plot for every pair of numeric columns and a distribution down the diagonal, so you see all the relationships in a single grid. It is like spreading every photo from a trip across the table at once instead of flipping through them one by one. The catch, which we get to in Common Mistakes, is that the grid grows fast.

📄 pairplot.py: the exploratory analysis power tool

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

rng = np.random.default_rng(42)
df = pd.DataFrame({
    "feature_1": rng.normal(0, 1, 150),
    "feature_2": rng.normal(0, 1, 150),
    "feature_3": rng.normal(0, 1, 150),
    "target": rng.choice(["Class A", "Class B", "Class C"], 150)
})

# One call draws every scatter pair plus a distribution on the diagonal
g = sns.pairplot(df, hue="target", diag_kind="kde",
                 plot_kws={"alpha": 0.6}, palette="Set2")
g.fig.suptitle("Pair Plot: Feature Relationships by Class", y=1.02)
plt.savefig("pairplot.png", dpi=150, bbox_inches="tight")
plt.show()
print("Pair plot shows every feature combination at once!")

▶ Output

Pair plot shows every feature combination at once!

What happened here: With three numeric features, sns.pairplot built a 3 by 3 grid. The off-diagonal cells are scatter plots of each feature pair, and the diagonal shows a KDE curve of each single feature because we set diag_kind="kde". The hue="target" argument colors every point by its class, so if any class separated cleanly in feature space you would see it here at a glance. The print line is the only text output, since the real result is the saved image. This is usually the first plot you reach for on a fresh dataset, because it surfaces structure you did not know to look for.

Common Mistakes

Mistake 1: Running pairplot on high-dimensional data

The pair plot grid is N by N, so the cost explodes. Three features means 9 cells, but 50 features means 2,500 cells. That is a chart nobody can read and a script that may run out of memory. The fix is to pick a handful of important columns first, then call pairplot on just those.

📄 pairplot_subset.py: select first, then plot

# BAD: 50 features means 2,500 subplots, which is a crash or an unreadable wall
# sns.pairplot(df)   # with 50 columns, do not do this

# GOOD: keep the columns that matter, then pairplot
df_subset = df[["top_feature_1", "top_feature_2", "target"]]
sns.pairplot(df_subset, hue="target")

# Rule of thumb: pairplot is comfortable at 3 to 6 features.
# Beyond that, reach for a heatmap of the correlation matrix instead.

Why this matters: A heatmap stays one fixed grid no matter how many features you have, so it scales to 50 columns where a pair plot cannot. Use the pair plot to study a small, interesting subset, and use the heatmap when you need the wide overview. The two are partners, not rivals.

Mistake 2: Coloring a box plot with palette but no hue

On older Seaborn you could write sns.boxplot(data=df, x="dept", y="salary", palette="Set2") and each box came out a different color. On Seaborn 0.13 that throws a deprecation warning, and it stops working entirely in 0.14. Assign the grouping column to hue and silence the duplicate legend instead.

🚫 Deprecated (warns on 0.13, breaks on 0.14)

sns.boxplot(data=df, x="dept", y="salary", palette="Set2")

✅ Correct on Seaborn 0.13.2

sns.boxplot(data=df, x="dept", y="salary", hue="dept",
            palette="Set2", legend=False)

Why this matters: Seaborn now ties color to a real variable through hue rather than letting you paint categories arbitrarily. Since hue="dept" repeats the information already on the x axis, you add legend=False to skip the redundant legend. Write it this way today and your charts keep working when 0.14 lands.

Conclusion

You have gone from one line of styling to a full Python Seaborn exploratory toolkit. sns.set_theme() upgrades every Matplotlib chart for free, distribution plots reveal the shape of a single column, heatmaps turn a correlation matrix into a grid of colors you can read at a glance, and the pair plot lays out every relationship at once. You also picked up the Seaborn 0.13 hue rule for coloring box plots, which keeps your code running when 0.14 arrives. The habit that ties it together: choose the plot family that matches your question first, and the exact function almost picks itself.

Next up is Plotly, where these same charts become interactive: hover to read values, zoom into a region, and share a live dashboard in the browser. For the full path from Python basics to AI and ML, head back to the Python + AI/ML tutorial series home.

Frequently Asked Questions

Can I customize Seaborn plots with Matplotlib?

Yes. Every Python Seaborn plot is a Matplotlib object underneath. After you create a Seaborn plot you can tweak it with normal Matplotlib calls like ax.set_title(), ax.set_xlim(), and ax.set_xlabel(). The usual workflow is simple: Seaborn draws the plot, Matplotlib does the fine-tuning.

What is the difference between histplot and displot in this seaborn tutorial?

histplot draws a single histogram onto an existing Axes, which is what you want when you are arranging your own subplots. displot creates a brand new Figure and adds extra options like faceting into a grid of columns. Reach for histplot when combining with plt.subplots, and displot for a standalone distribution view.

When should I use a violin plot instead of a box plot?

A box plot shows quartiles and outliers, so it is great for comparing medians across groups. A violin plot shows the full shape of the distribution, which is better when the data is bimodal or skewed. Use a violin when the shape itself matters, and a box when a clean five number summary is enough.

Which Seaborn version does this guide use?

All code was run on Seaborn 0.13.2 (current stable at the time of writing) with Python 3.14.6 and Pandas 2.3.3. The box plot hue note matters most here: passing palette without hue is deprecated in 0.13 and removed in 0.14, so the examples use the hue plus legend=False form throughout.

Interview Questions on Seaborn

Try each one aloud first. The phrasing you produce under mild pressure is what interviews measure.

Q: What does Seaborn add on top of Matplotlib, and when would you drop back to plain Matplotlib?

Seaborn adds sensible statistical defaults, DataFrame-aware plotting (you pass column names as strings instead of raw arrays), and built-in aggregation like automatic error bars and KDE curves. It also ships attractive themes so charts look good before you touch a setting. You drop back to Matplotlib for pixel-level control, because every Seaborn plot is a Matplotlib Axes underneath and accepts the usual ax.set_title(), ax.set_xlim(), and similar calls.

Q: What is the difference between axes-level and figure-level functions in Seaborn?

Axes-level functions such as histplot, boxplot, and scatterplot draw onto a single existing Axes and accept an ax= argument, which is what you want when arranging your own plt.subplots grid. Figure-level functions such as displot, relplot, catplot, and pairplot create and manage their own Figure, support faceting through row and col, and return a FacetGrid rather than an Axes. Mixing a figure-level call into a manual subplot layout is a common source of confusion.

Q: You call sns.pairplot on a DataFrame with 60 numeric columns and your notebook freezes or runs out of memory. What do you check and fix first?

A pair plot builds an N by N grid, so 60 columns means 3,600 subplots, which is both unreadable and a memory risk. First confirm the column count, then narrow it: pass vars= with a handful of important features, or subset the DataFrame before plotting. When you truly need the wide overview, switch to a correlation heatmap, which stays one fixed grid no matter how many columns you have.

Q: A teammate’s box plot code from last year now prints a deprecation warning and the colors disappear after a Seaborn upgrade. What changed and how do you fix it?

Passing palette without a hue assignment is deprecated in Seaborn 0.13 and removed in 0.14, which is why the colors vanished. The fix is to assign the grouping column to hue and add legend=False to skip the now-redundant legend, for example sns.boxplot(data=df, x="dept", y="salary", hue="dept", palette="Set2", legend=False). This ties color to a real variable instead of painting categories arbitrarily.

Q: How does the hue parameter work, and why is it central to Seaborn?

The hue argument maps a categorical column to color, splitting one plot into several per-group series with a single argument. It works consistently across plot types, so the same hue="department" colors a scatter plot, a KDE, or a box plot the same way. That consistency is what lets you compare groups quickly without writing a loop or filtering the DataFrame yourself.

Q: Your correlation heatmap looks misleading because the neutral point is not at white and weak correlations look strong. Which parameters do you set to fix it?

Use a diverging colormap like cmap="coolwarm" and pin the scale with center=0 so the midpoint renders neutral, plus vmin=-1 and vmax=1 so the full correlation range maps to the full color range. Without vmin and vmax, Seaborn scales colors to the data’s own min and max, which can make a 0.2 correlation look as intense as a 0.9. Add annot=True and fmt=".2f" to print the exact numbers alongside the colors.

Reference: the complete, always-current details live in Seaborn documentation.

Previous: Matplotlib Advanced: Subplots, Annotations, Styles

Next: Plotly: Interactive Visualizations & Dashboards

Series Home: Python + AI/ML Tutorial Series

RahulAuthor posts

Avatar for Rahul

Rahul is a passionate IT professional who loves to sharing his knowledge with others and inspiring them to expand their technical knowledge. Rahul's current objective is to write informative and easy-to-understand articles to help people avoid day-to-day technical issues altogether. Follow Rahul's blog to stay informed on the latest trends in IT and gain insights into how to tackle complex technical issues. Whether you're a beginner or an expert in the field, Rahul's articles are sure to leave you feeling inspired and informed.

No comment

Leave a Reply

Your email address will not be published. Required fields are marked *