Skip to content

Seaborn for Analytics

Seaborn is a statistical visualization library built on matplotlib. It produces clean, information-dense charts with significantly less code and handles DataFrames natively. Its chart types are designed for statistical analysis — KDE plots, violin plots, regression plots, pair plots, heatmaps — charts that matplotlib can produce but requires a lot of manual work to build.

The value of seaborn for analysts: you get statistically correct charts (confidence intervals, density estimates) without implementing the statistics yourself.

Learning Objectives

  • Set seaborn themes and color palettes
  • Create distribution plots: histplot, kdeplot, boxplot, violinplot
  • Create relationship plots: scatterplot, regplot, lineplot
  • Create categorical plots: barplot, countplot, stripplot
  • Create correlation heatmaps and pivot table heatmaps
  • Build pair plots for multivariate exploratory analysis

Setup and Themes

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

# Theme sets the background style
sns.set_theme(style="whitegrid")
# style options: white, whitegrid, dark, darkgrid, ticks

# Context scales font sizes and line weights for the output destination
sns.set_context("notebook")
# context options: paper (small), notebook (default), talk (slides), poster (large format)

# Palette — applies a color scheme to grouped charts
sns.set_palette("muted")
# Built-in palettes: deep, muted, bright, pastel, dark, colorblind

# Load sample data
df = pd.read_csv("orders_clean.csv", parse_dates=["order_date"])
df["total"] = df["quantity"] * df["unit_price"]
completed = df[df["status"] == "completed"].copy()

print(df.shape)      # e.g., (2500, 8)
print(df.dtypes)

For mid-level analysts

`sns.set_context("talk")` before generating charts for a slide deck. It increases font sizes and line widths so text is readable when projected. Switch back to `"notebook"` for exploratory work.

Distribution Plots

Distribution plots answer: "What does the spread of this variable look like?"

fig, axes = plt.subplots(1, 3, figsize=(16, 5))
fig.suptitle("Distribution of Order Totals", fontsize=14, fontweight="bold")

# histplot — histogram with optional KDE overlay
sns.histplot(
    completed["total"], bins=35, kde=True,
    color="#0D9488", ax=axes[0]
)
axes[0].set_title("Histogram + KDE")
axes[0].set_xlabel("Order Total (£)")
axes[0].set_ylabel("Count")

# kdeplot — smooth probability density estimate only
sns.kdeplot(
    completed["total"], fill=True, alpha=0.4,
    color="#0D9488", ax=axes[1]
)
axes[1].set_title("Kernel Density Estimate")
axes[1].set_xlabel("Order Total (£)")

# boxplot — summary statistics + outliers, grouped by category
sns.boxplot(
    data=completed, y="total", x="category",
    palette="muted", ax=axes[2]
)
axes[2].set_title("Order Total by Category")
axes[2].tick_params(axis="x", rotation=40)

plt.tight_layout()
plt.show()

Reading a box plot

The box spans the interquartile range (IQR): Q1 to Q3 (middle 50% of data). The line inside the box is the median. Whiskers extend to 1.5×IQR. Points beyond the whiskers are flagged as outliers. A box that is high on the chart = higher typical values. A wide box = more spread.

Violin Plot — Full Distribution Shape

A violin plot combines a box plot with a KDE. It shows where data is dense and sparse — something a box plot cannot show.

fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 6))

# Box plot for comparison
sns.boxplot(
    data=completed, x="category", y="total",
    palette="muted", ax=ax1
)
ax1.set_title("Box Plot — Summary Only")
ax1.tick_params(axis="x", rotation=40)

# Violin plot — full distribution shape per group
sns.violinplot(
    data=completed, x="category", y="total",
    palette="muted", inner="quartile", ax=ax2
)
ax2.set_title("Violin Plot — Full Distribution Shape")
ax2.tick_params(axis="x", rotation=40)

fig.suptitle("Box vs Violin: Same Data, Different Information",
             fontsize=13, fontweight="bold")
plt.tight_layout()
plt.show()

For senior analysts

Use violin plots when you suspect multimodal distributions — two peaks in the data. For example, an order value distribution might peak at £30 (accessories) and again at £300 (main products). A box plot shows the median; a violin plot shows both peaks.

Relationship Plots

Relationship plots answer: "Do these two variables move together?"

fig, axes = plt.subplots(1, 2, figsize=(15, 6))

# regplot — scatter + regression line with confidence interval shading
sns.regplot(
    data=completed, x="unit_price", y="total",
    scatter_kws={"alpha": 0.3, "color": "#CBD5E1"},
    line_kws={"color": "#EF4444", "linewidth": 2},
    ax=axes[0]
)
axes[0].set_title("Unit Price vs Order Total\n(with Regression Line + 95% CI)")
axes[0].set_xlabel("Unit Price (£)")
axes[0].set_ylabel("Order Total (£)")

# scatterplot — colored by a third variable (hue)
sns.scatterplot(
    data=completed, x="unit_price", y="total",
    hue="category", alpha=0.5, s=40, ax=axes[1]
)
axes[1].set_title("Unit Price vs Total\n(colored by Category)")
axes[1].legend(bbox_to_anchor=(1.05, 1), loc="upper left", framealpha=0.9)

plt.tight_layout()
plt.show()

Line Plot — Time Series with Confidence Interval

Seaborn's lineplot automatically aggregates repeated x-values and shows a confidence interval band. This is useful when each x-value has multiple y-values (e.g., daily revenue across multiple years of data).

# Build monthly revenue by category
completed["month"] = completed["order_date"].dt.to_period("M").astype(str)

monthly_cat = (
    completed
    .groupby(["month", "category"])["total"]
    .sum()
    .reset_index()
)

plt.figure(figsize=(13, 5))
sns.lineplot(
    data=monthly_cat, x="month", y="total",
    hue="category", marker="o"
)
plt.title("Monthly Revenue by Category", fontweight="bold")
plt.xlabel("Month")
plt.ylabel("Revenue (£)")
plt.xticks(rotation=40)
plt.legend(bbox_to_anchor=(1.05, 1), loc="upper left")
plt.tight_layout()
plt.show()

Categorical Plots

Categorical plots answer: "How does a numeric variable differ across categories?"

fig, axes = plt.subplots(1, 3, figsize=(18, 6))

# barplot — shows mean per group with 95% CI error bars
sns.barplot(
    data=completed, x="category", y="total",
    palette="muted", capsize=0.1, ax=axes[0]
)
axes[0].set_title("Avg Order Value by Category\n(bars = mean, lines = 95% CI)")
axes[0].tick_params(axis="x", rotation=40)
axes[0].set_ylabel("Mean Order Total (£)")

# countplot — frequency of each category
sns.countplot(
    data=df, x="status",
    palette="muted", order=df["status"].value_counts().index,
    ax=axes[1]
)
axes[1].set_title("Order Count by Status")
axes[1].set_ylabel("Count")

# stripplot — raw data points per group (shows all individual values)
sns.stripplot(
    data=completed, x="category", y="total",
    palette="muted", alpha=0.35, jitter=True,
    size=4, ax=axes[2]
)
axes[2].set_title("Individual Order Values by Category")
axes[2].tick_params(axis="x", rotation=40)

plt.tight_layout()
plt.show()

seaborn barplot vs countplot

barplot shows the mean (or another aggregation) of a numeric variable, grouped by a category. countplot shows how many rows belong to each category — no numeric variable needed. Use barplot for "what is the average order value per category?" and countplot for "how many orders are in each status?"


Heatmap — Correlation and Pivot Tables

Heatmaps encode numeric values with color. Two main uses: correlation matrices and pivot tables.

# --- Correlation heatmap ---
numeric_cols = ["quantity", "unit_price", "total"]
corr_matrix  = completed[numeric_cols].corr()

plt.figure(figsize=(6, 5))
sns.heatmap(
    corr_matrix,
    annot=True,       # print the correlation values inside each cell
    fmt=".2f",        # format to 2 decimal places
    cmap="RdYlGn",    # red = negative, yellow = neutral, green = positive
    center=0,         # 0 is neutral (white/yellow)
    vmin=-1, vmax=1,
    square=True,
    linewidths=0.5,
    linecolor="#FFFFFF"
)
plt.title("Correlation Matrix — Numeric Columns", fontweight="bold")
plt.tight_layout()
plt.show()

# Reading the heatmap: total vs unit_price shows dark green (strong positive)
# because higher-priced items produce higher order totals.
# --- Revenue pivot heatmap (category × month) ---
month_order = ["January", "February", "March", "April", "May", "June",
               "July", "August", "September", "October", "November", "December"]

completed["month_name"] = completed["order_date"].dt.month_name()

pivot = (
    completed
    .pivot_table(
        values="total",
        index="category",
        columns="month_name",
        aggfunc="sum"
    )
    .reindex(columns=[m for m in month_order if m in completed["month_name"].unique()])
    .fillna(0)
)

plt.figure(figsize=(14, 5))
sns.heatmap(
    pivot / 1000,
    annot=True, fmt=".0f",
    cmap="YlGn",
    cbar_kws={"label": "Revenue (£k)"},
    linewidths=0.5, linecolor="#F1F5F9"
)
plt.title("Revenue by Category and Month (£k)", fontweight="bold")
plt.xlabel("")
plt.ylabel("")
plt.tight_layout()
plt.show()

# Output: table showing which category × month combinations drive the most revenue.
# Dark green cells = high revenue; light yellow = low revenue.

Pair Plot — Multivariate Overview

A pair plot shows every pairwise combination of numeric variables as scatter plots, with distribution plots on the diagonal. It is the fastest way to survey many variables at once during exploratory analysis.

numeric_df = completed[["quantity", "unit_price", "total", "category"]].dropna()

g = sns.pairplot(
    numeric_df,
    hue="category",
    vars=["quantity", "unit_price", "total"],
    plot_kws={"alpha": 0.35, "s": 25},
    diag_kind="kde"
)
g.fig.suptitle("Pairwise Relationships — Numeric Variables", y=1.02, fontweight="bold")
plt.show()

# What to look for in a pair plot:
# - Linear patterns in off-diagonal scatter plots → correlation
# - Clustered groups by color → category differences
# - Skewed KDE plots on diagonal → consider log transform
# - Outliers appearing in multiple panels → likely the same rows

For mid-level analysts

Pair plots become hard to read above 6 variables. For large feature sets, use a correlation heatmap first to identify the most correlated pairs, then build targeted scatter plots for those pairs only.

Combining Seaborn + Matplotlib

Because seaborn builds on matplotlib, you can use matplotlib commands to customize any seaborn chart.

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

# Seaborn creates the chart
sns.boxplot(data=completed, x="category", y="total", palette="muted", ax=ax)

# Matplotlib customizes it
ax.set_title("Electronics Has the Widest Order Value Range",
             fontsize=13, fontweight="bold")
ax.set_xlabel("")
ax.set_ylabel("Order Total (£)", fontsize=11)
ax.tick_params(axis="x", rotation=30, labelsize=10)
ax.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: f{x:,.0f}"))
ax.spines["top"].set_visible(False)
ax.spines["right"].set_visible(False)

# Add a reference line at the overall median
overall_median = completed["total"].median()
ax.axhline(overall_median, color="#F59E0B", linestyle="--", linewidth=1.5,
           label=f"Overall median: £{overall_median:.0f}")
ax.legend()

plt.tight_layout()
plt.show()

Seaborn Color Palettes

# Show available palettes
palettes = ["deep", "muted", "bright", "pastel", "dark",
            "colorblind", "Set1", "Set2", "tab10"]

fig, axes = plt.subplots(len(palettes), 1, figsize=(8, 12))
for ax, pal in zip(axes, palettes):
    sns.palplot(sns.color_palette(pal, 8), ax=ax)   # not available in all versions
    ax.set_ylabel(pal, rotation=0, labelpad=50, va="center")

plt.tight_layout()
plt.show()

# Use "colorblind" palette for accessibility
sns.set_palette("colorblind")

# Use a diverging palette for charts where the midpoint matters
# (e.g., change vs baseline, positive vs negative)
cmap = sns.diverging_palette(240, 10, as_cmap=True)  # blue → red

Avoid rainbow palettes

Rainbow palettes (jet, rainbow) have no perceptual ordering and are not colorblind-safe. Use sequential palettes (YlGn, Blues, Oranges) for ordered data and diverging palettes (RdYlGn, coolwarm) for data with a meaningful midpoint.


Practice Exercises

Warm-up

  1. Create a histogram of total using sns.histplot with a KDE overlay. Add a vertical line at the median.
  2. Create a boxplot of total by status using seaborn. Label the axes clearly.
  3. Create a count plot of the category column ordered by frequency (most frequent first).

Main

  1. Create a violin plot of total by category. What additional information does it show compared to a box plot? Write one observation.
  2. Build a heatmap of the correlation matrix for all numeric columns. Annotate the cell with the strongest correlation and write an interpretation.
  3. Create a scatter plot of unit_price vs total, colored by category, with a regression line for each category.

Stretch

  1. Build a pair plot for numeric columns, colored by status. Write 3 observations about what you see.
  2. Build a 2×3 figure showing six different seaborn chart types for the same dataset. Add a fig.suptitle() as a dashboard title. Apply consistent formatting to all subplots.

Interview Questions

[Beginner] What is the difference between seaborn's barplot and countplot?

Show answer

barplot shows the mean (or other aggregation) of a numeric variable grouped by a categorical variable. The error bars show a 95% confidence interval. Use it for: "What is the average order value per product category?"

countplot shows how many rows belong to each category — no numeric variable needed. Use it for: "How many orders are in each status?" or "How many customers are in each segment?"

sns.barplot(data=df, x="category", y="total")   # mean total per category
sns.countplot(data=df, x="category")             # row count per category

[Mid-level] When would you use a violin plot instead of a boxplot?

Show answer

Use a violin plot when the distribution shape within groups matters — specifically when you want to see whether distributions are unimodal (one peak) or multimodal (multiple peaks). A box plot shows 5 summary statistics (min, Q1, median, Q3, max). A violin plot shows the full probability density, so you can see if orders cluster around two prices (e.g., budget items at £25 and premium items at £250), which a box plot would hide by summarizing both peaks into a single median.

Practical rule: use box plots in presentations to non-technical audiences (they're simpler to explain). Use violin plots in exploratory analysis with technical colleagues.

[Senior] How would you build a reusable Python function that generates a standard "company dashboard" figure for any date range input?

Show answer
def company_dashboard(df: pd.DataFrame, start_date: str, end_date: str,
                      title_prefix: str = "Sales Dashboard") -> plt.Figure:
    """
    Generate a standard 3-panel dashboard for the given date range.
    Returns the figure object so callers can save or display it.
    """
    mask = (df["order_date"] >= start_date) & (df["order_date"] <= end_date)
    data = df[mask & (df["status"] == "completed")].copy()

    if data.empty:
        raise ValueError(f"No completed orders between {start_date} and {end_date}")

    fig, axes = plt.subplots(1, 3, figsize=(18, 5))
    period_label = f"{start_date} to {end_date}"

    # Panel 1: monthly revenue
    monthly = data.groupby(data["order_date"].dt.to_period("M"))["total"].sum()
    axes[0].bar(monthly.index.astype(str), monthly.values / 1000, color="#0D9488")
    axes[0].set_title("Monthly Revenue (£k)")
    axes[0].tick_params(axis="x", rotation=45)

    # Panel 2: category breakdown
    cat_rev = data.groupby("category")["total"].sum().sort_values()
    axes[1].barh(cat_rev.index, cat_rev.values / 1000, color="#0D9488")
    axes[1].set_title("Revenue by Category (£k)")

    # Panel 3: order value distribution
    sns.histplot(data["total"], bins=30, kde=True, color="#0D9488", ax=axes[2])
    axes[2].set_title("Order Value Distribution")
    axes[2].axvline(data["total"].median(), color="#F59E0B", linestyle="--",
                    label=f"Median £{data['total'].median():.0f}")
    axes[2].legend()

    for ax in axes:
        ax.spines["top"].set_visible(False)
        ax.spines["right"].set_visible(False)

    fig.suptitle(f"{title_prefix}{period_label}",
                 fontsize=14, fontweight="bold", y=1.02)
    plt.tight_layout()
    return fig

# Usage
fig = company_dashboard(df, "2024-01-01", "2024-06-30")
fig.savefig("q1_dashboard.png", dpi=150, bbox_inches="tight")
plt.close(fig)

Previous: Matplotlib Basics | Next: Chart Selection