A Complete Guide to Seaborn

0
5



Image by Editor

 

Introduction

 
Seaborn is a statistical visualization library for Python that sits on top of Matplotlib. It gives you clean defaults, tight integration with Pandas DataFrames, and high-level functions that reduce boilerplate. If you already know Matplotlib and want faster, more informative plots, this guide is for you.

The focus here is intermediate to advanced usage. You will work with relational, categorical, distribution, and regression plots, then move into grid layouts and matrix visuals that answer real analytical questions. Expect short code blocks, precise explanations, and practical parameter choices that affect readability and accuracy.

What this guide covers:

  • Set up themes and palettes you can reuse across projects
  • Plots that matter for analysis: scatterplot, lineplot, boxplot, violinplot, histplot, kdeplot, regplot, lmplot
  • High-dimensional layouts with FacetGrid, PairGrid, relplot, and catplot
  • Correlation and heatmaps with correct color scales, masking, and annotation
  • Precise control through Matplotlib hooks for titles, ticks, legends, and annotations
  • Performance tips for large datasets and fixes for common pitfalls

You will learn when to use confidence intervals, how to manage legends in crowded visuals, how to keep category colors consistent, and when to switch back to Matplotlib for fine control. The goal is clear, accurate plots that communicate findings without extra work.

 

Setup and Styling Baseline

 
This section sets a consistent visual baseline so every plot in the article looks professional and export-ready. We will install, import, set a global theme, choose practical palettes, and lock in figure sizing and DPI for clean outputs.

 

// Install and import

Use a clean environment and install Seaborn and Matplotlib.

pip install seaborn matplotlib

 

Standard imports:

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

 

Two quick checks that help avoid surprises:

 

// Project-wide theme in one line

Set a default style once, then focus on the analysis instead of constant styling tweaks.

sns.set_theme(
    context="talk",      # text size scaling: paper, notebook, talk, poster
    style="whitegrid",   # clean background with light grid
    palette="deep"       # readable, colorblind aware categorical palette
)

 

Why this matters:

  • context="talk" gives readable axis labels and titles for slides and reports
  • style="whitegrid" improves value reading for line and bar plots without heavy visual noise
  • palette="deep" provides distinct category colors that hold up when printed or projected

You can override any of these per plot, but setting them globally keeps the look uniform.

 

// Palettes you will actually use

Choose palettes that communicate the data type. Use discrete palettes for categories and colormaps for continuous values.

1. Viridis for continuous scales

# Discrete colors for categories
cats = sns.color_palette("viridis", n_colors=5)

# Continuous colormap for heatmaps and densities
viridis_cmap = sns.color_palette("viridis", as_cmap=True)

 

  • Viridis preserves detail across light and dark backgrounds and is perceptually uniform
  • Use n_colors= for discrete categories. Use as_cmap=True when mapping a numeric range

2. Cubehelix for ordered categories or low-ink plots

# Light-to-dark sequence that prints well
cube = sns.cubehelix_palette(
    start=0.5,    # hue start
    rot=-0.75,    # hue rotation
    gamma=1.0,    # intensity curve
    light=0.95,
    dark=0.15,
    n_colors=6
)

 

Cubehelix stays readable in grayscale and supports ordered categories where progression matters.

3. Blend a custom brand ramp

# Blend two brand colors into a smooth ramp
blend = sns.blend_palette(["#0F766E", "#60A5FA"], n_colors=7, as_cmap=False)

# If you need a continuous colormap instead
blend_cmap = sns.blend_palette(["#0F766E", "#60A5FA"], as_cmap=True)

 

Blending helps align visuals with a design system while keeping numerical gradients consistent.

Set a palette globally when you commit to a scheme for a whole figure or report

sns.set_palette(cats)     # or cube, or blend

 

Preview a palette quickly

sns.palplot(cats)
plt.show()

 

// Figure sizing and DPI for export

Control size and resolution from the start to avoid fuzzy labels or cramped axes.
Set a sensible default once

# Global defaults via Matplotlib rcParams
plt.rcParams["figure.figsize"] = (8, 5)    # width, height in inches
plt.rcParams["figure.dpi"] = 150           # on-screen clarity without huge files

 

You can still size individual figures explicitly when needed:

fig, ax = plt.subplots(figsize=(8, 5))

 

Save high-quality outputs

# Raster export for web or slide decks
plt.savefig("figure.png", dpi=300, bbox_inches="tight")

# Vector export for print or journals
plt.savefig("figure.svg", bbox_inches="tight")
plt.savefig("figure.pdf", bbox_inches="tight")

 

  • dpi=300 is a good target for crisp web images and presentations
  • bbox_inches="tight" trims empty margins, which keeps multi-panel layouts compact
  • Prefer SVG or PDF when editors will resize figures or when you need sharp text at any scale

 

Plots That Matter for Real Work

 
In this section, we will focus on plot types that answer analysis questions quickly. Each subsection explains when to use the plot, the key parameters that control meaning, and a short code sample you can adapt. The examples assume you already set the theme and baseline from the previous section.

 

// Relational plots: scatterplot, relplot(kind="line")

Use relational plots to show relationships between numeric variables and to compare groups with color, marker, and size encodings. Add clarity by mapping a categorical variable to hue and style, and a numeric variable to size.

import seaborn as sns
import matplotlib.pyplot as plt

penguins = sns.load_dataset("penguins").dropna(
    subset=["bill_length_mm", "bill_depth_mm", "body_mass_g", "species", "sex"]
)

# Scatter with multiple encodings
ax = sns.scatterplot(
    data=penguins,
    x="bill_length_mm",
    y="bill_depth_mm",
    hue="species",
    style="sex",
    size="body_mass_g",
    sizes=(30, 160),
    alpha=0.8,
    edgecolor="w",
    linewidth=0.5
)
ax.set_title("Bill length vs depth with species, sex, and mass encodings")
ax.legend(title="Species")
plt.tight_layout()
plt.show()

 
bill-length-vs-depth
 

For lines, prefer the figure-level API when you need markers per group and easy faceting.

flights = sns.load_dataset("flights")  # year, month, passengers

g = sns.relplot(
    data=flights,
    kind="line",
    x="year",
    y="passengers",
    hue="month",
    markers=True,      # marker on each point
    dashes=False,      # solid lines for all groups
    height=4, aspect=1.6
)
g.set_axis_labels("Year", "Passengers")
g.figure.suptitle("Monthly passenger trend by year", y=1.02)
plt.show()

 
monthly-passenger-trend
 

Notes:

  • Use style for a second categorical channel when hue is not enough
  • Keep alpha slightly below 1.0 on dense scatters to reveal overlap
  • Use sizes=(min, max) to constrain point sizes so the legend remains readable

 

// Categorical plots: boxplot, violinplot, barplot

Categorical plots show distributions and group differences. Choose box or violin when you care about spread and outliers. Choose bar when you want aggregated values with intervals.

import numpy as np
tips = sns.load_dataset("tips")

# Boxplot: robust summary of spread
ax = sns.boxplot(
    data=tips,
    x="day",
    y="total_bill",
    hue="sex",
    order=["Thur", "Fri", "Sat", "Sun"],
    dodge=True,
    showfliers=False
)
ax.set_title("Total bill by day and sex (boxplot, fliers hidden)")
plt.tight_layout()
plt.show()

# Violin: shape of the distribution with quartiles
ax = sns.violinplot(
    data=tips,
    x="day",
    y="total_bill",
    hue="sex",
    order=["Thur", "Fri", "Sat", "Sun"],
    dodge=True,
    inner="quartile",
    cut=0,
    scale="width"
)
ax.set_title("Total bill by day and sex (violin with quartiles)")
plt.tight_layout()
plt.show()

# Bar: mean tip with percentile intervals
ax = sns.barplot(
    data=tips,
    x="day",
    y="tip",
    hue="sex",
    order=["Thur", "Fri", "Sat", "Sun"],
    estimator=np.mean,
    errorbar=("pi", 95),   # percentile interval for skewed data
    dodge=True
)
ax.set_title("Mean tip by day and sex with 95% PI")
plt.tight_layout()
plt.show()

 
Categorical-plots
 

Notes:

  • order fixes category sorting for consistent comparisons
  • For large samples where intervals add noise, set errorbar=None (or ci=None on older Seaborn)
  • Hide fliers on boxplots when extreme points distract from the group comparison

 

// Distribution plots:histplot

Distribution plots reveal shape, multimodality, and group differences. Use stacking when you want totals, and fill when you want composition.

# Single distribution with a smooth density overlay
ax = sns.histplot(
    data=penguins,
    x="body_mass_g",
    bins=30,
    kde=True,
    element="step"
)
ax.set_title("Body mass distribution with KDE")
plt.tight_layout()
plt.show()

# Grouped comparison: composition across species
ax = sns.histplot(
    data=penguins,
    x="body_mass_g",
    hue="species",
    bins=25,
    multiple="fill",    # fraction per bin (composition)
    element="step",
    stat="proportion",
    common_norm=False
)
ax.set_title("Body mass composition by species")
plt.tight_layout()
plt.show()

# Grouped comparison: total counts by stacking
ax = sns.histplot(
    data=penguins,
    x="body_mass_g",
    hue="species",
    bins=25,
    multiple="stack",
    element="step",
    stat="count"
)
ax.set_title("Body mass counts by species (stacked)")
plt.tight_layout()
plt.show()

 
Distribution-plots
 

Notes:

  • Use multiple="fill" to compare relative composition across bins
  • Use common_norm=False when groups differ in size and you want within-group densities
  • Choose element="step" for clean edges and easy overlaying

 

// Regression plots: regplot, lmplot

Regression plots add fitted relationships and intervals. Use regplot for a single axes. Use lmplot when you need hue, row, or col faceting without manual grid work.

Let’s take a look at an lmplot. Just ensure the dataset has no missing values in the mapped columns.

penguins = sns.load_dataset("penguins").dropna(
    subset=["bill_length_mm", "bill_depth_mm", "species", "sex"]
)

g = sns.lmplot(
    data=penguins,
    x="bill_length_mm",
    y="bill_depth_mm",
    hue="species",
    col="sex",
    height=4,
    aspect=1,
    scatter_kws=dict(s=25, alpha=0.7),
    line_kws=dict(linewidth=2)
)
g.set_titles(col_template="{col_name}")
g.figure.suptitle("Bill dimensions by species and sex", y=1.02)
plt.show()

 
bill-dimension-lmplot
 

Notes:

  • On newer Seaborn versions, prefer errorbar=("ci", 95) on functions that support it. If ci is still accepted in your version, you can keep using it for now.
  • If you see similar errors, check for other exclusive pairs like lowess=True, logistic=True, or logx=True used together.

 

// Interval choices on large data

On big samples, interval bands can obscure the signal. Two options improve clarity:

  • Use percentile intervals for skewed distributions:
  • sns.barplot(data=tips, x="day", y="tip", errorbar=("pi", 95))

     

  • Remove intervals entirely when variation is already obvious:
  • sns.lineplot(data=flights, x="year", y="passengers", errorbar=None)
    # or on older versions:
    sns.lineplot(data=flights, x="year", y="passengers", ci=None)

     

Guideline:

  • Prefer errorbar=("pi", 95) for skewed or heavy-tailed data
  • Prefer errorbar=None (or ci=None) when the audience cares more about trend shape than precise uncertainty on a very large N

 

High-Dimensional Views with Grids

 
Grids help you compare patterns across groups without manual subplot juggling. You define rows, columns, and color once, then apply a plotting function to each subset. This keeps structure consistent and makes differences obvious.

 

// FacetGrid and catplot/ relplot

Use a FacetGrid when you want full control over what gets mapped to each facet. Use catplot and relplot when you want a quick, figure-level API that builds the grid for you. The core idea is the same: split data by row, col, and color with hue.
Before the code: keep facet counts realistic. Four to six small multiples are easy to scan. Beyond that, wrap columns or filter categories. Control sharing with sharex and sharey so comparisons remain valid.

import seaborn as sns
import matplotlib.pyplot as plt

tips = sns.load_dataset("tips").dropna()

# 1) Full control with FacetGrid + regplot
g = sns.FacetGrid(
    data=tips,
    row="time",                # Lunch vs Dinner
    col="day",                 # Thur, Fri, Sat, Sun
    hue="sex",                 # Male vs Female
    margin_titles=True,
    sharex=True,
    sharey=True,
    height=3,
    aspect=1
)
g.map_dataframe(
    sns.regplot,
    x="total_bill",
    y="tip",
    scatter_kws=dict(s=18, alpha=0.6),
    line_kws=dict(linewidth=2),
    ci=None
)
g.add_legend(title="Sex")
g.set_axis_labels("Total bill", "Tip")
g.fig.suptitle("Tipping patterns by day and time", y=1.02)
plt.show()

# 2) Quick grids with catplot (built on FacetGrid)
sns.catplot(
    data=tips,
    kind="box",
    x="day", y="total_bill",
    row="time", hue="sex",
    order=["Thur", "Fri", "Sat", "Sun"],
    height=3, aspect=1.1, dodge=True
).set_axis_labels("Day", "Total bill")
plt.show()

# 3) Quick relational grids with relplot
penguins = sns.load_dataset("penguins").dropna()
sns.relplot(
    data=penguins,
    kind="scatter",
    x="bill_length_mm", y="bill_depth_mm",
    row="sex", col="island", hue="species",
    height=3.2, aspect=1
)
plt.show()

 

Key points:

  • Use order to fix category sorting
  • Use col_wrap when you have one facet dimension with many levels
  • Add a suptitle to summarize the comparison; keep axis labels consistent across facets

 

// PairGrid and pairplot

Pairwise plots reveal relationships across many numeric variables. pairplot is the fast path. PairGrid gives you per-region control. For dense datasets, limit variables and consider corner=True to drop redundant upper panels.

Before the code: choose variables that are informative together. Mix scales only when you have a reason, then standardize or log-transform first.

# Quick pairwise view
num_cols = ["bill_length_mm", "bill_depth_mm", "flipper_length_mm", "body_mass_g"]
sns.pairplot(
    data=penguins[num_cols + ["species"]].dropna(),
    vars=num_cols,
    hue="species",
    corner=True,           # only lower triangle + diagonal
    diag_kind="hist",      # or "kde"
    plot_kws=dict(s=18, alpha=0.6),
    diag_kws=dict(bins=20, element="step")
)
plt.show()

 

Tips:

  • corner=True reduces clutter and speeds up rendering
  • Keep marker size modest so overlaps remain readable
  • For very different scales, apply np.log10 to skewed measures before plotting

 

// Mixed layers on a PairGrid

A mixed mapping helps you compare scatter patterns and density structure in one view. Use scatter on the upper triangle, bivariate KDE on the lower triangle, and histograms on the diagonal. This combination is compact and informative.
Before the code: density layers can get heavy. Reduce levels and avoid too many bins. Add a legend once and keep it outside the grid if space is tight.

from seaborn import PairGrid

g = PairGrid(
    data=penguins[num_cols + ["species"]].dropna(),
    vars=num_cols,
    hue="species",
    height=2.6, aspect=1
)

# Upper triangle: scatter
g.map_upper(
    sns.scatterplot,
    s=16, alpha=0.65, linewidth=0.3, edgecolor="w"
)

# Lower triangle: bivariate KDE
g.map_lower(
    sns.kdeplot,
    fill=True, thresh=0.05, levels=5
)

# Diagonal: histograms
g.map_diag(
    sns.histplot,
    bins=18, element="step"
)

g.add_legend(title="Species")
for ax in g.axes.flat:
    if ax is not None:
        ax.tick_params(axis="x", labelrotation=30)

g.fig.suptitle("Pairwise structure of penguin measurements", y=1.02)
plt.show()

 
pairwise-structure-of-penguin-measurements
 

Guidelines:

  • Start with 4 numeric variables. Add more only if each adds a distinct signal
  • For uneven group sizes, focus on proportions rather than raw counts when you compare distributions
  • If rendering slows down, sample rows before plotting or drop fill from the KDE layer

 

Correlation, Heatmaps, and Matrices

 
Correlation heatmaps are a compact way to scan relationships across many numeric variables. The goal is a readable matrix that highlights real signal, keeps noise out of the way, and exports cleanly.

 

// Build a correlation matrix and mask redundant cells

Start by selecting numeric columns and choosing a correlation method. Pearson is standard for linear relationships. Spearman is better for ranked or monotonic patterns. A triangular mask removes duplication so the eye focuses on unique pairs.

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

# Data
penguins = sns.load_dataset("penguins").dropna()

# Choose numeric columns and compute correlation
num_cols = penguins.select_dtypes(include="number").columns
corr = penguins[num_cols].corr(method="pearson")

# Mask the upper triangle (keep lower + diagonal)
mask = np.triu(np.ones_like(corr, dtype=bool))

# Heatmap with diverging palette centered at zero
ax = sns.heatmap(
    corr,
    mask=mask,
    annot=True,
    fmt=".2f",
    cmap="vlag",
    center=0,
    vmin=-1, vmax=1,
    square=True,
    cbar_kws={"shrink": 0.8, "label": "Pearson r"},
    linewidths=0.5, linecolor="white"
)

ax.set_title("Correlation matrix of penguin measurements")
plt.tight_layout()
plt.show()

 
Correlation matrix of penguin measurements
 

Notes:

  • Use method="spearman" when variables are not on comparable scales or contain outliers that affect Pearson
  • Keep vmin and vmax symmetric so the color scale treats negative and positive values equally

 

// Control visibility with scale and colorbar options

Once the matrix is in place, tune what the reader sees. Symmetric limits, a centered palette, and a labeled colorbar prevent misreads. You can also hide weak correlations or the diagonal to reduce clutter.

# Optional: hide weak correlations below a threshold
threshold = 0.2
weak = corr.abs() < threshold
mask2 = np.triu(np.ones_like(corr, dtype=bool)) | weak  # combine masks

ax = sns.heatmap(
    corr,
    mask=mask2,
    annot=False,                 # let strong colors carry the message
    cmap="vlag",
    center=0,
    vmin=-1, vmax=1,
    square=True,
    cbar_kws={"shrink": 0.8, "ticks": [-1, -0.5, 0, 0.5, 1], "label": "Correlation"},
    linewidths=0.4, linecolor="#F5F5F5"
)

ax.set_title("Strong correlations only (|r| ≥ 0.20)")
plt.tight_layout()
plt.show()

 

Tips:

  • cbar_kws controls readability of the legend. Set ticks that match your audience
  • Turn annot=True back on when you need exact values for a report. Keep it off for dashboards where shape and color are enough

 

// Large matrices: keep labels and edges readable

Big matrices need discipline. Thin or rotate tick labels, add grid lines between cells, and consider reordering variables to group related blocks. If the matrix is very wide, show every nth tick to avoid label collisions.

# Synthetic wide example: 20 numeric columns
rng = np.random.default_rng(0)
wide = pd.DataFrame(rng.normal(size=(600, 20)),
                    columns=[f"f{i:02d}" for i in range(1, 21)])

corr_wide = wide.corr()

fig, ax = plt.subplots(figsize=(10, 8), dpi=150)

hm = sns.heatmap(
    corr_wide,
    cmap="vlag",
    center=0,
    vmin=-1, vmax=1,
    square=True,
    cbar_kws={"shrink": 0.7, "label": "Correlation"},
    linewidths=0.3, linecolor="white"
)

# Rotate x labels and thin ticks
ax.set_xticklabels(ax.get_xticklabels(), rotation=40, ha="right")
ax.set_yticklabels(ax.get_yticklabels(), rotation=0)
ax.tick_params(axis="both", labelsize=8)

# Show every 2nd tick on both axes
xt = ax.get_xticks()
yt = ax.get_yticks()
ax.set_xticks(xt[::2])
ax.set_yticks(yt[::2])

ax.set_title("Correlation matrix with tick thinning and grid lines")
plt.tight_layout()
plt.show()

 

When structure matters more than exact order, try a clustered view that groups similar variables:

# Clustered matrix for pattern discovery
g = sns.clustermap(
    corr_wide,
    cmap="vlag",
    center=0,
    vmin=-1, vmax=1,
    linewidths=0.2, linecolor="white",
    figsize=(10, 10),
    cbar_kws={"shrink": 0.6, "label": "Correlation"},
    method="average",  # linkage
    metric="euclidean" # distance on correlations
)
g.fig.suptitle("Clustered correlation matrix", y=1.02)
plt.show()

 

Guidelines:

  • Increase figure size rather than shrinking font until it becomes unreadable
  • Add linewidths and linecolor to define cell boundaries on dense matrices
  • Use clustering when you want to surface block structure. Keep a plain ordered matrix when you need stable positions across reports

 

Precision Control with Matplotlib Hooks

 
Seaborn handles the heavy lifting, but final polish comes from Matplotlib. These hooks let you set clear titles, control axes precisely, manage legends in tight spaces, and annotate important points without clutter.

 

// Titles, labels, legends

Good plots read themselves. Set titles that state the question, label axes with units, and keep the legend compact and informative. Place the legend where it helps the eye, not where it hides data.

Before the code: prefer axis-level methods over plt.* so settings stay attached to the right subplot. Use a legend title and consider moving the legend outside the axes when you have many groups.

import seaborn as sns
import matplotlib.pyplot as plt

penguins = sns.load_dataset("penguins").dropna()

ax = sns.scatterplot(
    data=penguins,
    x="bill_length_mm",
    y="bill_depth_mm",
    hue="species",
    style="sex",
    s=60,
    alpha=0.8,
    edgecolor="w",
    linewidth=0.5
)

# Titles and labels
ax.set_title("Bill length vs depth by species")
ax.set_xlabel("Bill length (mm)")
ax.set_ylabel("Bill depth (mm)")

# Legend with title, placed outside to the right
leg = ax.legend(
    title="Species",
    loc="center left",
    bbox_to_anchor=(1.02, 0.5),   # outside the axes
    frameon=True,
    borderaxespad=0.5
)

# Optional legend styling
for text in leg.get_texts():
    text.set_fontsize(9)
leg.get_title().set_fontsize(10)

plt.tight_layout()
plt.show()

 
bill-length-vs-depth-by-species
 

Notes

  • bbox_to_anchor gives you fine control over legend placement outside the axes
  • Keep legend fonts slightly smaller than axis tick labels to reduce visual weight
  • If you need a custom legend order, pass hue_order= in the plotting call

 

// Axis control

Axis limits, ticks, and rotation improve readability more than any color choice. Set only the ticks your audience needs. Use rotation when labels collide. Add small margins to stop markers from touching the frame.

Before the code: decide which ticks matter. For time or evenly spaced integers, show fewer ticks. For skewed data, consider log scales and custom formatters.

import numpy as np
flights = sns.load_dataset("flights")  # columns: year, month, passengers

ax = sns.lineplot(
    data=flights,
    x="year",
    y="passengers",
    estimator=None,
    errorbar=None,
    marker="o",
    dashes=False
)

ax.set_title("Airline passengers by year")
ax.set_xlabel("Year")
ax.set_ylabel("Passengers")

# Show a tick every 5 years
years = np.sort(flights["year"].unique())
ax.set_xticks(years[::5])

# Tidy the view
ax.margins(x=0.02, y=0.05)   # small padding inside axes
ax.set_ylim(0, None)         # start at zero for clearer trend

# Rotate if needed
plt.xticks(rotation=0)
plt.tight_layout()
plt.show()

 
Airline-passengers-by-year
 

Extras you can add when required

  • Symmetric limits: ax.set_xlim(left, right) and ax.set_ylim(bottom, top) for fair comparisons
  • Log scaling: ax.set_xscale("log") or ax.set_yscale("log") for long tails
  • Fewer ticks: ax.xaxis.set_major_locator(matplotlib.ticker.MaxNLocator(nbins=6))

 

// Annotations, lines, and spans

Annotations call out the reason the plot exists. Use a short label, a clear arrow, and consistent styling. Lines and spans mark thresholds or periods that matter.

Before the code: place annotations near the data they refer to, but avoid covering points. Consider using a semi-transparent span for ranges.

import matplotlib as mpl
tips = sns.load_dataset("tips")

ax = sns.regplot(
    data=tips,
    x="total_bill",
    y="tip",
    ci=None,
    scatter_kws=dict(s=28, alpha=0.6),
    line_kws=dict(linewidth=2)
)

ax.set_title("Tip vs total bill with callouts")
ax.set_xlabel("Total bill ($)")
ax.set_ylabel("Tip ($)")

# Threshold line for a tipping rule of thumb
ax.axhline(3, color="#444", linewidth=1, linestyle="--")
ax.text(ax.get_xlim()[0], 3.1, "Reference: $3 tip", fontsize=9, color="#444")

# Highlight a bill range with a span
ax.axvspan(20, 40, color="#fde68a", alpha=0.25, linewidth=0)  # soft highlight

# Annotate a representative point
pt = tips.loc[tips["total_bill"].between(20, 40)].iloc[0]
ax.annotate(
    "Example check",
    xy=(pt["total_bill"], pt["tip"]),
    xytext=(pt["total_bill"] + 10, pt["tip"] + 2),
    arrowprops=dict(
        arrowstyle="->",
        color="#111",
        shrinkA=0,
        shrinkB=0,
        linewidth=1.2
    ),
    fontsize=9,
    bbox=dict(boxstyle="round,pad=0.2", fc="white", ec="#ddd", alpha=0.9)
)

plt.tight_layout()
plt.show()

 
Tip-vs-total-bill-with-callouts
 

Guidelines:

  • Keep annotations short. The plot should still read without them
  • Use axvline, axhline, axvspan, and axhspan for thresholds and ranges
  • If labels overlap, adjust with small offsets or reduce font size, not by removing the annotation that carries meaning

 

Wrapping Up

 
You now have a complete baseline for fast, consistent Seaborn work: sample or aggregate when scale demands it, control legends and axes with Matplotlib hooks, keep colors stable across figures, and fix labels before export. Combine these with the grid patterns and statistical plots from earlier sections and you can cover most analysis needs without custom subplot code.

Where to learn more:

 
 

Shittu Olumide is a software engineer and technical writer passionate about leveraging cutting-edge technologies to craft compelling narratives, with a keen eye for detail and a knack for simplifying complex concepts. You can also find Shittu on Twitter.