§13  Data Visualization
Article 13 Python Data Science Series

Data Visualization with Matplotlib and Seaborn: A Practical Guide

Numbers in a spreadsheet are like facts locked in a filing cabinet. They exist, but they don't tell you anything on their own. Data visualization is what turns those numbers into pictures, and pictures are where the patterns, outliers, and trends actually become visible. It's the tool a data scientist reaches for most, both to understand data and to explain it to someone else.

11 chart types, one at a time
Copy-paste ready code
When to use each
Styling & palettes
A real workflow, start to finish

This guide covers the charts you'll actually use day to day, not every visualization that exists, just the ones that answer most real business questions. For each one, you'll see what it looks like (as a text diagram, not code), what it's for, and when to use it or leave it alone. Two libraries do the work here. Matplotlib is the underlying engine. Seaborn sits on top of it and gives you good-looking, statistically aware charts with a lot less code.

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

The Relationship Between Matplotlib and Seaborn

Before the charts, here's the one thing worth understanding: these two libraries aren't alternatives. One sits on top of the other.

   ┌───────────────────────────────────────────────┐
   │                   SEABORN                     │
   │   "Give me a heatmap" → writes the Matplotlib │
   │    code for you, with better defaults          │
   ├───────────────────────────────────────────────┤
   │                 MATPLOTLIB                    │
   │   The engine: every line, label, colour,      │
   │   and tick mark ultimately lives here         │
   └───────────────────────────────────────────────┘

Matplotlib is baking from scratch: you control every ingredient. Seaborn is a baking mix: it handles the tedious parts (colour palettes, grid styles, error bars) and adds statistical features automatically, though the oven underneath is still Matplotlib. You can mix the two freely, and the best code usually does.


The Anatomy of a Matplotlib Figure

Every chart, regardless of type, is built from the same two core objects:

  • A Figure is the entire canvas, the outer box.
  • An Axes is the actual plot area inside that canvas, where the chart itself lives.
   FIGURE  (the whole canvas)
   ┌──────────────────────────────────────────────┐
   │  ┌────────────────────────────────────────┐  │
   │  │  AXES            Title          legend │  │
   │  │         │   ●          ●               │  │
   │  │ ylabel  │      ●   ●                   │  │
   │  │         │              ●               │  │
   │  │         └──────────────────── x-axis   │  │
   │  │                  xlabel                │  │
   │  └────────────────────────────────────────┘  │
   └──────────────────────────────────────────────┘

The cleanest way to create both, and the one that gives you full control:

python
fig, ax = plt.subplots(figsize=(10, 6))   # canvas 10 inches wide, 6 tall

Everything you call after this (ax.set_title(...), ax.set_xlabel(...), ax.plot(...)) draws onto that axes object. The title, axis labels, ticks and legend all belong to the Axes, which is why they're set through ax.


Chart 1: Line Plot, Tracking Change Over Time

What it looks like

   Revenue ($M)
    50 │                                ●
    40 │                           ●
    30 │                      ●
    20 │            ●    ●
    10 │  ●    ●
       └─────────────────────────────────────
         Jan  Feb  Mar  Apr  May  Jun  Jul

The Code

python
months  = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul"]
revenue = [10, 14, 23, 20, 30, 42, 50]

fig, ax = plt.subplots(figsize=(10, 5))
ax.plot(months, revenue, marker='o', linewidth=2, color='steelblue')
ax.set_title("Monthly Revenue | Netflix India")
ax.set_xlabel("Month")
ax.set_ylabel("Revenue ($M)")
plt.tight_layout()
plt.show()

When to Use It

A line plot assumes the x-axis is ordered and continuous, usually time. It's the chart for any "how did this change over time" question.

  • Netflix tracking daily active users over a quarter
  • Spotify charting weekly stream counts for a new release
  • Swiggy monitoring hourly order volumes during a festival
  • Tesla plotting quarterly production output over two years

When Not to Use It

Skip the line plot if your x-axis is categories with no natural order, like city names or product categories. Connecting them with a line implies a trend that isn't there. Use a bar chart instead.

Seaborn Version

Seaborn's lineplot adds confidence intervals automatically. It shows the average and the uncertainty around it, which is genuinely useful once you have repeated measurements:

python
sns.lineplot(data=df, x="month", y="revenue", hue="region")

The hue parameter splits one line into multiple lines, one per category, and generates a legend automatically.


Chart 2: Bar Chart, Comparing Categories Side by Side

What it looks like

   Orders
    500 │                          ████
    400 │        ████        ████  ████
    300 │  ████  ████  ████  ████  ████
    200 │  ████  ████  ████  ████  ████
    100 │  ████  ████  ████  ████  ████
        └──────────────────────────────
           Mon   Tue   Wed   Thu   Fri

The Code

python
days  = ["Mon", "Tue", "Wed", "Thu", "Fri"]
sales = [300, 450, 380, 420, 500]

fig, ax = plt.subplots(figsize=(8, 5))
ax.bar(days, sales, color='coral', edgecolor='black')
ax.set_title("Daily Orders | Swiggy")
ax.set_xlabel("Day")
ax.set_ylabel("Orders")
plt.tight_layout()
plt.show()

When to Use It

A bar chart is what you reach for when comparing a measurement across discrete, unordered categories. The height of each bar is the one thing your eye picks up immediately.

  • Amazon comparing total revenue by product category (Electronics, Clothing, Books)
  • YouTube showing watch time by content genre
  • Uber displaying average ride duration by city
  • Instagram comparing engagement rates across different content types

Horizontal Bar Chart: When Labels Are Long

If your category names run long (city names, full product titles, campaign names), rotate the chart horizontally instead. It keeps the labels from getting cramped or angled:

python
ax.barh(days, sales, color='teal')   # categories run down the y-axis; bar length = value

The bar lengths scale with the data (sales = [300, 450, 380, 420, 500]): Mon is shortest and Fri is longest. Matplotlib draws the first category at the bottom, so Mon sits at the bottom and Fri at the top.

   Fri   ████████████████████████████████████████ (500)
   Thu   ██████████████████████████████████       (420)
   Wed   ██████████████████████████████           (380)
   Tue   ████████████████████████████████████     (450)
   Mon   ████████████████████████                 (300)
         0       100     200     300     400     500

When Not to Use It

Avoid bar charts for continuous data with many categories, say 100 countries. The bars collapse into an unreadable wall. Use a line plot if there's order to the data, or aggregate and show only the top N.

Seaborn: countplot and barplot

Seaborn has two bar chart variants, and it's worth knowing the difference:

  • sns.countplot takes a categorical column on the x-axis and counts the occurrences for you automatically. Good for "how many of each?"
  • sns.barplot takes both an x (category) and a y (value) and draws bars with a 95% confidence interval by default. Good for "what's the average of Y, per category?"
python
sns.countplot(data=df, x="product_category", order=df["product_category"].value_counts().index)
sns.barplot(data=df, x="city", y="fare", errorbar=('ci', 95),
            hue="city", palette="viridis", legend=False)

Version note: older tutorials often show ci=95 as the parameter here. That was deprecated in Seaborn 0.12 and now only triggers a deprecation warning. errorbar is the current parameter, and it defaults to a 95% confidence interval anyway, so in most cases you can just leave it out.


Chart 3: Scatter Plot, Finding Relationships Between Two Variables

What it looks like

   Price ($)
    300 │               ●
    250 │          ●         ●
    200 │     ●         ●
    150 │  ●       ●
    100 │●    ●
        └────────────────────────────
         0    20   40   60   80  100
                  Rating (out of 100)

The Code

python
fig, ax = plt.subplots(figsize=(8, 6))
ax.scatter(x=df["rating"], y=df["price"],
           alpha=0.6, s=50, color='steelblue', edgecolors='white')
ax.set_title("Price vs Rating | Airbnb Listings, Mumbai")
ax.set_xlabel("Rating Score")
ax.set_ylabel("Price per Night ($)")
plt.tight_layout()
plt.show()

When to Use It

A scatter plot shows the relationship, the correlation, between two continuous variables. Each point is one observation, one row in your data.

  • Airbnb exploring whether higher-rated listings charge more
  • Uber studying whether longer trips earn more (distance vs fare)
  • Amazon investigating if more reviews lead to higher prices
  • A streaming platform checking whether longer movies have lower completion rates

The patterns you look for:

   Positive            Negative           No Correlation    Curved
   Correlation         Correlation

   ●                   ●                  ●  ●   ●           ●
     ●                 ●●                ●    ● ●          ●     ●
       ●●           ●●               ●  ● ●  ●          ●         ●
         ●●       ●●                   ●  ●           ●             ●
           ●    ●●                   ●  ●             ●             ●

   "as X ↑, Y ↑"  "as X ↑, Y ↓"  "no pattern"     "U-shaped"

When Not to Use It

Scatter plots break down with very large datasets (millions of points), because the dots overlap and pile up into what's called overplotting. Fix it by making the dots transparent (alpha=0.1), switching to a hexbin plot, or adding jitter.

Seaborn: scatterplot and regplot

sns.regplot goes a step further: it draws the scatter plot and fits a regression line through the data, with a shaded confidence band, all in one call.

python
sns.scatterplot(data=df, x="distance", y="fare", hue="city", style="vehicle_type")
sns.regplot(data=df, x="rating", y="price", scatter_kws={"alpha": 0.4})

The hue and style parameters add two more dimensions of information without changing the chart type: different colours per city, different marker shapes per vehicle type.


Chart 4: Histogram, Seeing the Shape of Your Data

What it looks like

   Frequency
    80 │       ████
    60 │  ████ ████ ████
    40 │  ████ ████ ████ ████
    20 │  ████ ████ ████ ████ ████
     0 │  ████ ████ ████ ████ ████ ████
        └────────────────────────────────
           10   20   30   40   50   60
                    Age (years)

The Code

python
fig, ax = plt.subplots(figsize=(9, 5))
ax.hist(df["age"], bins=20, color='mediumseagreen', edgecolor='white')
ax.set_title("Age Distribution of Uber Riders")
ax.set_xlabel("Age")
ax.set_ylabel("Number of Riders")
plt.tight_layout()
plt.show()

What bins Means

A histogram groups continuous numbers into buckets (called bins) and counts how many values fall into each bucket. The bins parameter controls how many buckets to use:

   bins=5 (wide buckets)          bins=20 (narrow buckets)

   ████████████                     ████
   ████████████  ████               ████ ████
   ████████████  ████  ████         ████ ████ ████
   ─────────────────────            ────────────────────
   "rough shape"                    "fine detail"

Too few bins hide structure; too many create noise. A good starting point is 15–30 bins and adjusting from there.

The Shapes You'll See

Understanding histogram shapes is a core data analysis skill:

   Normal / Bell           Right-Skewed           Left-Skewed          Bimodal
   "most people are        "a few very high        "a few very low      "two clusters"
    average"                values pull the tail"   values pull the tail"

      ████                        ████               ████████
     ██████                      ██████              ████████████
    ████████                    █████████              ████████████
   ██████████                  ████████████               ██████████

   adult heights           app revenue             exam scores         orders by hour
   (one population)        (a few whales)          (easy exam)         (lunch + dinner)

Here's a quick way to check the logic: on an easy exam, most students score high, near the top, with only a small tail of low scores from the ones who struggled. That tail sits on the left, at low values, which is what "left-skewed" means. A hard exam is the opposite: scores bunch low, with a small tail of high scorers stretching right. That's the right-skewed case, the same shape as the app revenue example above it.

When to Use It

Use a histogram when you want to understand the distribution of a single continuous variable: how spread out it is, where most values cluster, whether it's symmetric, whether there are outliers.

  • PhonePe checking the distribution of transaction amounts (are most small, with rare large ones?)
  • Spotify analyzing song length distribution across a genre
  • Swiggy understanding delivery time distribution (where's the peak? how wide is the spread?)
  • Netflix examining hours watched per user per month

Seaborn: histplot and kdeplot

Seaborn's histplot can overlay a smooth density curve, the KDE (Kernel Density Estimate), that shows the shape without the jaggedness of bins:

python
sns.histplot(data=df, x="fare", kde=True, bins=25, color="steelblue")
sns.kdeplot(data=df, x="fare", hue="city", fill=True, alpha=0.3)

The KDE (kde=True) is that smooth curve drawn over the bars. Think of it as a smoothed-out histogram, and it makes comparing multiple distributions on the same chart a lot cleaner.


Chart 5: Box Plot for Distribution, Spread, and Outliers

What it looks like

                       ○     ← outlier (beyond 1.5 × IQR from the box)
                       │
                       ┴     ← upper whisker (max value that ISN'T an outlier)
                  ┌────────┐
                  │        │  ← Q3, 75th percentile, top of the box
                  │────────│  ← Median, 50th percentile, line inside the box
                  │        │  ← Q1, 25th percentile, bottom of the box
                  └────────┘
                       ┬     ← lower whisker (min value that ISN'T an outlier)
                       │

            IQR (Interquartile Range) = height of the box = Q3 − Q1

More concretely, with real numbers:

   Ride Fare ($)

    50 │              ○                 ← outlier (unusually expensive ride)
    45 │              │
    40 │              ┴                 ← upper whisker (highest normal fare)
       │         ┌─────────┐
    30 │         │    ┼    │            ← median fare
       │         └─────────┘
    20 │              ┬                 ← lower whisker (lowest normal fare)
       │              │
    10 │
       └──────────────────────
              All Rides

The Code

python
fig, ax = plt.subplots(figsize=(10, 6))
ax.boxplot(df["fare"], patch_artist=True, boxprops=dict(facecolor='lightblue'))
ax.set_title("Distribution of Ride Fares | Uber")
ax.set_ylabel("Fare ($)")
plt.tight_layout()
plt.show()

Decoding the Box Plot

The box plot packs five statistics into one picture:

Feature What it shows
Bottom of box (Q1) The 25th percentile, 25% of values are below this
Middle line The median, 50% of values are below this
Top of box (Q3) The 75th percentile, 75% of values are below this
Height of box (IQR) Interquartile range = Q3 − Q1. The middle 50% of the data. Wide means spread out, narrow means clustered.
Whiskers Lines extending from the box to the furthest value that is not an outlier (at most 1.5 × IQR beyond the box, by default)
Dots beyond whiskers Outliers, unusually extreme values worth investigating

When to Use It

Box plots are for comparing distributions across categories. You get the full spread and any outliers, not just the average, across multiple groups at a glance.

   City:       Mumbai      Delhi       Bangalore
               ┌──┐         ┌──┐         ┌──┐
               │  │      ○  │  │         │  │
               ├──┤         ├──┤         │  │
               │  │         │  │         ├──┤
               └──┘         └──┘         └──┘
               Tight        Outliers     Wide spread
               distribution  present     (varied fares)
  • Uber comparing fare distributions across cities (which city has more variability?)
  • Amazon comparing delivery times across warehouses
  • Spotify analyzing song popularity scores by genre (is Hip-Hop more consistently popular than Classical?)
  • A pharma company comparing test results across treatment groups

Seaborn: boxplot and violinplot

Seaborn makes comparing multiple groups easy with the x and hue parameters:

python
sns.boxplot(data=df, x="city", y="fare", hue="city", palette="Set2", legend=False)

Passing the same column to hue is how current Seaborn applies a palette per category; legend=False hides the legend, which would only repeat the x-axis labels.

The violin plot is a box plot's more expressive sibling. It shows the full distribution shape (like a KDE turned sideways) rather than just the five-number summary.

python
sns.violinplot(data=df, x="city", y="fare", hue="city", palette="muted",
               legend=False, inner="quartile")
   Box Plot            Violin Plot

     ┌──┐              ┃░░░░░░░░┃
     │  │             ┃░░░░░░░░░░░┃
     ├──┤             ┃░░░░░░░░░░░░░░┃    ← widest = most common values
     │  │             ┃░░░░░░░░░░░┃
     └──┘              ┃░░░░░░░░┃

   shows 5 numbers    shows the full shape

Use a violin plot when the shape of the distribution matters: whether it's bimodal (two humps), skewed, or has a heavy tail. Use a box plot when the outliers or the IQR itself is what you want to communicate.


Chart 6: Heatmap, Visualizing a Matrix of Numbers

What it looks like

   Correlation Matrix
           Price  Rating  Distance  Duration
   Price   [1.00]  [0.72]   [0.85]    [0.78]     ← darker = stronger relationship
   Rating  [0.72]  [1.00]   [0.40]    [0.33]
   Dist    [0.85]  [0.40]   [1.00]    [0.91]
   Dur     [0.78]  [0.33]   [0.91]    [1.00]

   ░ = weak     ▒ = moderate     ▓ = strong

The Code

python
corr = df[["price", "rating", "distance", "duration"]].corr()   # compute correlations

fig, ax = plt.subplots(figsize=(8, 6))
sns.heatmap(corr, annot=True, fmt=".2f", cmap="coolwarm", center=0,
            square=True, linewidths=0.5, ax=ax)
ax.set_title("Feature Correlation Matrix")
plt.tight_layout()
plt.show()

Heatmap Parameters That Matter

Parameter What it does
annot=True Write the number inside each cell
fmt=".2f" Format numbers to 2 decimal places
cmap="coolwarm" Blue for negative, red for positive. "viridis" for sequential data.
center=0 Anchors the colour scale at zero, so a diverging cmap like coolwarm stays symmetric around "no correlation"
square=True Forces the cells to be square
vmin=-1, vmax=1 Fix the colour scale (useful for correlations)

What Correlation Values Mean

   -1.0           -0.5          0.0          +0.5          +1.0
    │──────────────│─────────────│─────────────│──────────────│
    Perfect        Moderate      No            Moderate       Perfect
    negative       negative      relationship  positive       positive
    (as X↑, Y↓)                              (as X↑, Y↑)

A value near ±1 means the two variables move closely together in a straight-line way. Near 0 means there's no linear relationship, though the variables can still be related in a curved way (a scatter plot will show it). In a correlation heatmap, the dark red and dark blue cells are the strongest relationships, and the ones worth investigating further.

When to Use It

Heatmaps work for any data that lives in a grid, where both rows and columns are meaningful categories. The correlation matrix is the most common use case, but they're also good for:

  • User engagement by hour and day (rows are hours, columns are days, values are average orders), which is how Swiggy's operations team spots which hour of which day is the peak
  • Product cross-sell patterns, which items get bought together, the kind of thing Amazon's recommendation team looks at
  • Sensor data, like temperatures across factory zones over time

When Not to Use It

Heatmaps lose their value once one dimension has too many categories, say 60 cities or 200 features. The cells get too small to read. Reduce the dimensions first: pick the top 10 most important features, or cluster before plotting.


Chart 7: Pairplot, Exploring All Relationships at Once

What it looks like

         price        distance        rating
          │                │                │
  price ──│── kde          │   scatter      │   scatter
          │                │                │
          │                │                │
  dist ───│── scatter   ───│── kde          │   scatter
          │                │                │
          │                │                │
  rating──│── scatter   ───│── scatter   ───│── kde
          │                │                │

The Code

python
sns.pairplot(df[["price", "distance", "duration", "rating", "city"]],
             hue="city", diag_kind="kde", corner=True)
plt.suptitle("Pairwise Relationships | Ride Data", y=1.02)
plt.show()

Note: if you're subsetting columns before plotting, make sure the column you pass to hue is included in that subset. It's a common mistake: slice a DataFrame down to just the numeric columns, then reference a category column in hue that got left behind, and you'll get a KeyError.

When to Use It

A pairplot is a quick exploration tool. It draws every scatter plot of every variable pair in one grid, with distribution plots on the diagonal, and answers "which variables are related?" without you creating a dozen scatter plots by hand.

  • Early in any analysis, before you know which variables matter
  • When you have 4–8 features and want a bird's-eye view of relationships
  • When colouring by category (hue) might reveal patterns, like whether city clusters separate cleanly

When Not to Use It

A pairplot with more than 8–10 variables becomes a wall of tiny unreadable charts. For high-dimensional data, use a correlation heatmap instead.


Chart 8: Subplots, Multiple Charts in One Figure

Often the best answer to "how do I show X and Y together?" is to put them side by side in one figure rather than combining them into one chart. Subplots do exactly that.

The Code

python
# months, revenue, days and sales come from the earlier examples;
# ratings stands in for whatever column you're working with
fig, axes = plt.subplots(nrows=1, ncols=3, figsize=(15, 5))

# Chart 1: Line plot on the first axes
axes[0].plot(months, revenue, marker='o', color='steelblue')
axes[0].set_title("Revenue Trend")

# Chart 2: Bar chart on the second axes
axes[1].bar(days, sales, color='coral')
axes[1].set_title("Orders by Day")

# Chart 3: Histogram on the third axes
axes[2].hist(ratings, bins=20, color='mediumseagreen')
axes[2].set_title("Rating Distribution")

plt.suptitle("Q3 Dashboard | Uber India", fontsize=14)
plt.tight_layout()
plt.show()

Subplot Grid Layouts

   1 row, 3 columns          2 rows, 2 columns
   plt.subplots(1, 3)        plt.subplots(2, 2)

   ┌──────┬──────┬──────┐    ┌──────┬──────┐
   │  [0] │  [1] │  [2] │    │ [0,0]│ [0,1]│
   └──────┴──────┴──────┘    ├──────┼──────┤
                              │ [1,0]│ [1,1]│
                              └──────┴──────┘
   axes[0], axes[1], ...     axes[0,0], axes[0,1], ...

When to Use It

Subplots are good for dashboards and reports, where you want several related charts together so they can be compared at a glance. Instead of making a viewer click through four separate charts, you put them on one page: trend over time on the left, breakdown by category in the middle, distribution on the right.


Chart 9: Strip Plot and Swarm Plot, Showing Every Individual Data Point

The Problem with Box Plots and Bar Charts

Bar charts and box plots summarise data: they show you an average, a median, or a spread. But summary statistics can hide things. Two datasets can share nearly identical summary numbers and still look completely different once you plot every point.

The classic demonstration of this is Anscombe's Quartet: four small datasets that share nearly identical means, variances, and correlations, yet look completely different once you plot them point by point.

   Dataset A               Dataset B               Dataset C
   ●  ●  ●  ●            ●●●●                    ●
      ●  ●  ●  ●            ●●●●●                  ●●
         ●  ●                 ●●●●                   ●●●●
                                 ●                    ●●●
                                                        ●
   Mean ≈ 7.5            Mean ≈ 7.5              Mean ≈ 7.5
   Looks like a spread   Looks like a curve      An outlier changes everything

Going only by the summary numbers, the same mean, the same rough spread, you'd assume these were three similar datasets. Strip and swarm plots apply that same lesson to categorical comparisons. They show every individual data point instead of a summary, so nothing is hidden behind an average.

Strip Plot

   Fare ($)
    50 │   ●
       │  ●●
    40 │  ●●●
       │ ●●●●●
    30 │●●●●●●
       │ ●●●●
    20 │  ●●
       │   ●
        ─────────────
           UberX
python
fig, ax = plt.subplots(figsize=(10, 6))
sns.stripplot(data=df, x="vehicle_type", y="fare", hue="vehicle_type",
              jitter=True, alpha=0.6, palette="Set2", legend=False, ax=ax)
ax.set_title("Fare Distribution by Vehicle Type | Uber")
ax.set_xlabel("Vehicle Type")
ax.set_ylabel("Fare ($)")
plt.tight_layout()
plt.show()

The jitter=True parameter nudges each point slightly left or right, so they don't all stack on the same vertical line and disappear into each other. Without jitter, every point at the same value sits exactly on top of the last one and becomes invisible.

Swarm Plot: No Overlap, Ever

A swarm plot takes this further. Instead of random jitter, it arranges points so they never overlap, spreading them into a clean "bee swarm" shape. The width of the swarm at any height tells you how many data points sit at that value.

   Fare ($)
    50 │      ●
    40 │    ●●●●
    30 │  ●●●●●●●●
    20 │    ●●●●●
    10 │      ●●
        ─────────
          UberX
   Width of swarm ≈ how many rides had that fare
python
sns.swarmplot(data=df, x="vehicle_type", y="fare", hue="vehicle_type",
              palette="muted", legend=False)

The Power Move: Overlay on a Box Plot

The most information-rich combination is a box plot (showing the statistics) with a strip plot on top of it (showing the raw data). You get both the summary and the individual points in one view:

python
fig, ax = plt.subplots(figsize=(10, 6))
sns.boxplot(data=df, x="city", y="fare", hue="city", palette="pastel",
            legend=False, width=0.4, ax=ax)
sns.stripplot(data=df, x="city", y="fare", color="black", alpha=0.4,
              jitter=True, size=3, ax=ax)
ax.set_title("Fare Distribution by City | Box + Raw Points")
plt.tight_layout()
plt.show()
   Box plot + Strip plot overlay

   ●●    ┌──┐  ●●
    ●    │  │  ●
   ● ●   ├──┤  ●●●
   ●     │  │  ●
    ●    └──┘  ●
   ────────────────
     Mumbai   Delhi
   Box = summary    Dots = individual rides

When to Use It

Use strip or swarm plots when your sample size is small to moderate, up to a few hundred points, and you want the audience to see the actual data rather than a summary that could be misleading.

  • A clinical trial comparing treatment outcomes across three groups, where seeing every patient matters
  • Comparing employee salaries across departments, where individual points reveal clusters an average would hide
  • Airbnb showing price spread across property types: the box plot might look the same, but the strip plot reveals that "Entire Apartment" is actually bimodal, a budget cluster and a luxury cluster

When Not to Use It

With very large datasets, 10,000+ rows, every point overlaps and the swarm turns into a solid mass. At that scale, use a violin plot or a KDE instead.


Chart 10: Joint Plot, Scatter and Distribution in One

The joint plot is one of Seaborn's more elegant charts. It combines:

  • a scatter plot in the centre: the relationship between two variables
  • a histogram or KDE along the top: the distribution of the x variable
  • a histogram or KDE along the right side: the distribution of the y variable

You get the relationship and the shape of each variable in one glance.

What it looks like

                ── Distribution of Duration ──
                      ▄▄  ██  ▄▄
                   ▄▄ ██  ██  ██ ▄▄
              ┌──────────────────────────┐ ┌─────┐
            F │   ●        ●●            │ │██   │
            a │      ●●●  ●●●            │ │████ │  Distribution
            r │   ●     ●●● ●●           │ │███  │  of Fare
            e │     ●●●●  ●  ●  ●        │ │█    │
              └──────────────────────────┘ └─────┘
                        Duration
                 ← scatter in the middle →
python
g = sns.jointplot(data=df, x="duration", y="fare",
                  kind="scatter", height=7,
                  marginal_kws=dict(bins=20, fill=True))
g.set_axis_labels("Duration (min)", "Fare ($)")
g.figure.suptitle("Duration vs. Fare with Marginal Distributions", y=1.02)
plt.show()

The kind Parameter: Four Views of the Same Relationship

The kind parameter changes what's drawn in the centre while keeping the marginals:

kind= Centre shows Best for
"scatter" Individual dots Small to medium datasets
"kde" Density contours Large datasets, seeing clusters
"hex" Hexagonal bins (colour = count) Very large datasets
"reg" Scatter + regression line Measuring the trend
python
sns.jointplot(data=df, x="duration", y="fare", kind="kde")    # smooth density
sns.jointplot(data=df, x="duration", y="fare", kind="hex")    # hexagonal bins
sns.jointplot(data=df, x="duration", y="fare", kind="reg")    # with trend line

When to Use It

A joint plot works well when you want to report on one specific relationship and also want the reader to see both variables' distributions, without needing two separate charts. It's a self-contained exploratory report in a single figure.

  • Uber asking: "Does longer duration always mean higher fare, and what are the typical duration and fare distributions?"
  • Spotify: "Is there a relationship between audio energy and danceability, and are either of these normally distributed?"
  • An e-commerce team: "Do products with higher ratings actually sell more units?"

Chart 11: FacetGrid, the Same Chart for Every Category

A FacetGrid takes one chart and repeats it for every category in your data, arranged in a grid. Instead of cramming five city comparisons onto one scatter plot with five colours, you get five separate scatter plots side by side, each one clean and readable on its own.

What it looks like

   ┌─────────────┬─────────────┬─────────────┐
   │   Mumbai    │    Delhi    │  Bangalore  │
   │     ●  ●   │   ●         │    ●●       │
   │   ●●  ●    │    ●●       │  ●  ●●      │
   │  ●   ●     │  ●●●●      │   ●   ●     │
   └─────────────┴─────────────┴─────────────┘
   Same chart type, same axes, different city
python
g = sns.FacetGrid(df, col="city", col_wrap=3, height=4, sharey=True)
g.map(sns.scatterplot, "distance", "fare", alpha=0.5)
g.set_axis_labels("Distance (miles)", "Fare ($)")
g.set_titles(col_template="{col_name}")
g.figure.suptitle("Distance vs. Fare by City", y=1.02)
plt.show()

Key FacetGrid Parameters

Parameter What it controls
col="city" Which column creates the panels (one per unique value)
row="time_of_day" Adds a second dimension of rows
col_wrap=3 Wrap into a new row after 3 columns (prevents one giant row)
sharey=True All panels share the same y-axis scale (enables fair comparison)
height=4 How tall each individual panel is

col vs row vs Both

   col="city" only              col="city", row="time"

   ┌──┬──┬──┐                  ┌──┬──┬──┐   ← Morning
   │M │D │B │                  ├──┼──┼──┤
   └──┴──┴──┘                  │M │D │B │   ← Evening
                                └──┴──┴──┘
   3 panels in 1 row            6 panels in a 2×3 grid

When to Use It

A FacetGrid is worth reaching for whenever you want to compare the same pattern across groups without the visual noise of overlapping, colour-coded series.

  • Netflix: "How does watch-time-vs-episode-count look differently across genres?"
  • Amazon: "Is the rating-vs-price relationship the same for Electronics, Books, and Clothing?"
  • Swiggy: "Does order volume peak at the same hour in Mumbai, Delhi, and Bangalore?"

The advantage over cramming everything onto one chart is that side-by-side panels on shared axes let the eye compare instantly, with no legend to decode.


Choosing the Right Chart: A Decision Guide

The most common mistake beginners make is picking a chart type they like the look of, rather than one that fits the data. Here's the decision framework:

   What do you want to show?
   │
   ├── CHANGE OVER TIME?
   │       └── LINE PLOT
   │
   ├── COMPARISON ACROSS CATEGORIES?
   │       ├── A few categories (≤ 10)  →  BAR CHART
   │       └── Long category names      →  HORIZONTAL BAR CHART
   │
   ├── RELATIONSHIP BETWEEN TWO VARIABLES?
   │       ├── Both continuous          →  SCATTER PLOT
   │       ├── With a regression line   →  sns.regplot
   │       └── Plus both distributions  →  JOINT PLOT
   │
   ├── DISTRIBUTION OF ONE VARIABLE?
   │       ├── Overall shape            →  HISTOGRAM
   │       ├── Compare groups/shape     →  BOX PLOT or VIOLIN PLOT
   │       └── Every individual point   →  STRIP PLOT or SWARM PLOT
   │
   ├── RELATIONSHIPS IN A GRID?
   │       ├── All variable pairs       →  PAIRPLOT
   │       └── Correlation matrix       →  HEATMAP
   │
   ├── SAME QUESTION, SPLIT BY CATEGORY?
   │       └── FACETGRID
   │
   └── MULTIPLE DIFFERENT QUESTIONS AT ONCE?
           └── SUBPLOTS

The Chart No One Talks You Out Of: Pie Charts

Pie charts are the most widely used chart type outside of data science, and the most widely criticised inside it. The human eye is good at comparing bar heights side by side, but bad at comparing slice angles and areas. Can you tell, just by looking, whether a 24% slice is bigger than a 26% slice? Not reliably. A bar chart makes that comparison instant.

Use a pie chart only if you have 3-5 categories and the proportions are clearly different, something like 60/25/15. In almost any other case, a bar chart says the same thing more clearly. And skip 3D pie charts entirely: the perspective distortion makes slices look larger or smaller than they really are.

python
fig, ax = plt.subplots(figsize=(6, 6))
ax.pie(sizes, labels=labels, autopct='%1.0f%%', colors=sns.color_palette("Set2"))
ax.set_title("Market Share | Top 3 Ride-Sharing Apps")

Styling Your Charts: Making Them Look Professional

A technically correct chart that looks ugly loses its audience. Both Matplotlib and Seaborn offer styling tools that require almost no extra code.

Seaborn Themes

sns.set_theme() applies a clean style globally, and every chart after it will use it:

python
sns.set_theme(style="whitegrid", palette="muted", font_scale=1.2)

The four most useful styles:

   "whitegrid"    "darkgrid"     "white"        "ticks"
   ┌────────┐     ┌════════┐     ┌────────┐     ┌────────┐
   │░░│░░│░░│     │▒▒│▒▒│▒▒│     │        │     │        │
   │──┼──┼──│     │▒▒│▒▒│▒▒│     │        │     │  ticks │
   │░░│░░│░░│     │▒▒│▒▒│▒▒│     │        │     │  only  │
   └────────┘     └════════┘     └────────┘     └────────┘
   White with     Dark with      Clean white    Minimal
   grid lines     grid (default) no grid        ticks
Style Best for
"whitegrid" Most general use, clean and readable
"white" Presentations, posters, minimalist
"darkgrid" Dense charts where gridlines help navigation
"ticks" Academic papers, very clean

Colour Palettes

The palette parameter controls the colours used for different categories. Seaborn has many built-in palettes grouped by purpose:

   QUALITATIVE (for categories: each colour = different group)
   "Set1"      ● ● ● ● ● ● ● ● ●    (bold, distinct)
   "Set2"      ● ● ● ● ● ● ● ● ●    (softer, muted)
   "tab10"     ● ● ● ● ● ● ● ● ● ●  (Matplotlib default)
   "muted"     ● ● ● ● ● ● ● ● ●    (professional, subdued)

   SEQUENTIAL (for continuous data: light to dark)
   "Blues"     ░ ▒ ▓ ███            (white → dark blue)
   "viridis"   ░ ▒ ▓ ███            (perceptually uniform)
   "YlOrRd"    ░ ▒ ▓ ███            (yellow → red)

   DIVERGING (for data that has a meaningful centre, like correlations)
   "coolwarm"  ── ░ │ ▓ ──          (blue → white → red)
   "RdBu"      ── ░ │ ▓ ──          (red → white → blue)

The rule of thumb: use a qualitative palette for categorical data, a sequential palette for ordered or continuous data (like heatmaps with positive values only), and a diverging palette when your data has a meaningful midpoint (like correlations from -1 to +1).

Essential Chart Labels: Never Skip These

A chart without proper labels forces the reader to guess what they're looking at. These four elements are non-negotiable:

python
ax.set_title("Clear, descriptive title that states the finding",
             fontsize=14, fontweight='bold')
ax.set_xlabel("What the x-axis represents (unit)", fontsize=12)
ax.set_ylabel("What the y-axis represents (unit)", fontsize=12)
ax.legend(title="What the colours/markers mean", fontsize=10)

A good title states the finding, not just the topic. "Revenue by Month" is a topic. "Revenue grew 40% in Q3, driven by festive sales" is a finding. A chart that gives you the conclusion in its own title saves the reader some thinking.


Putting It All Together: A Real Data Science Workflow

Here's how a data scientist at a company like Swiggy would move through a full visualization workflow for a single business question: "Why are delivery times increasing in Mumbai on weekends?"

Step 1: First look at the distributions (Histogram)

python
mumbai = df[df["city"] == "Mumbai"]

fig, axes = plt.subplots(1, 2, figsize=(14, 5), sharey=True)
sns.histplot(mumbai[mumbai["day_type"]=="Weekday"]["delivery_mins"], bins=25,
             kde=True, color="steelblue", ax=axes[0])
sns.histplot(mumbai[mumbai["day_type"]=="Weekend"]["delivery_mins"], bins=25,
             kde=True, color="coral", ax=axes[1])
axes[0].set_title("Weekday Delivery Times")
axes[1].set_title("Weekend Delivery Times")
for ax in axes:
    ax.set_xlabel("Delivery Time (minutes)")
plt.suptitle("Mumbai: Weekend vs Weekday Delivery Distribution")
plt.tight_layout()

Finding: the weekend distribution is right-skewed. Most deliveries are fine, but a long tail of very slow ones is pulling the average up.

Step 2: Compare the spread and outliers by hour (Box Plot)

python
fig, ax = plt.subplots(figsize=(14, 6))
sns.boxplot(data=mumbai, x="hour", y="delivery_mins",
            hue="day_type", palette="Set2", ax=ax)
ax.set_title("Delivery Time by Hour | Mumbai")
ax.set_xlabel("Hour of Day")
ax.set_ylabel("Delivery Time (minutes)")

Finding: the 7pm–10pm window on weekends has far more outliers (dots above the whisker). That points to a surge in demand the fleet can't handle.

Step 3: See where delays correlate with order volume (Scatter + Trend)

python
# weekend_df: Mumbai weekend data summarised to one row per zone and hour,
# with orders_per_hour and avg_delivery_mins columns
fig, ax = plt.subplots(figsize=(10, 6))
sns.regplot(data=weekend_df, x="orders_per_hour", y="avg_delivery_mins",
            scatter_kws={"alpha": 0.5}, line_kws={"color": "red"}, ax=ax)
ax.set_title("More Orders → Slower Delivery (Weekend, Mumbai)")
ax.set_xlabel("Orders per Hour")
ax.set_ylabel("Avg Delivery Time (minutes)")

Finding: there's a strong positive correlation. As orders increase, so do delivery times, and the regression line puts a number on it.

Step 4: Check if the pattern is the same across zones (FacetGrid)

python
g = sns.FacetGrid(weekend_df, col="zone", col_wrap=3, height=4)
g.map(sns.lineplot, "hour", "avg_delivery_mins")
g.set_axis_labels("Hour", "Avg Delivery (min)")
g.set_titles("{col_name}")
g.figure.suptitle("Weekend Delivery by Hour | Mumbai Zones", y=1.02)

Finding: South Mumbai and Bandra spike hardest at 8pm; Thane and Andheri stay much flatter. The problem is concentrated in specific zones, not citywide.

Conclusion delivered to operations team:

"Weekend delivery slowdowns are concentrated in South Mumbai and Bandra between 7pm–10pm, driven by order volumes that exceed fleet capacity in those zones. Redistributing 20% of rider allocation from Andheri to those zones during peak weekend hours should reduce P90 delivery time by an estimated 8 minutes."

This is the actual point of visualization: turning raw data into a precise, actionable statement for someone who has to act on it.


Quick Reference Summary

Chart Matplotlib Code Seaborn Code Best For Avoid When
Line Plot ax.plot(x, y) sns.lineplot(data, x, y, hue) Trends over time No natural order on x-axis
Bar Chart ax.bar(x, y) sns.barplot(data, x, y) Comparing category averages Too many categories (>15)
H. Bar Chart ax.barh(x, y) sns.barplot(orient='h') Long category labels N/A
Count Plot (manual) sns.countplot(data, x) Counting occurrences Comparing averages
Scatter Plot ax.scatter(x, y) sns.scatterplot(data, x, y, hue) Relationship between 2 continuous vars Millions of points
Regression Plot (manual) sns.regplot(data, x, y) Scatter + trend line Categorical x-axis
Histogram ax.hist(x, bins) sns.histplot(data, x, kde=True) Distribution of 1 variable Comparing many groups
KDE Plot (manual) sns.kdeplot(data, x, hue, fill) Smooth shape of distribution Small datasets
Box Plot ax.boxplot(x) sns.boxplot(data, x, y) Spread, outliers, group comparison Shape of distribution matters
Violin Plot (complex) sns.violinplot(data, x, y) Distribution shape across groups Unfamiliar audiences
Strip Plot (manual) sns.stripplot(data, x, y, jitter) All individual points per category Very large datasets
Swarm Plot (manual) sns.swarmplot(data, x, y) Points with no overlap Datasets > ~500 rows
Joint Plot (manual) sns.jointplot(data, x, y, kind) Scatter + both marginal distributions Multiple comparisons
Heatmap (manual) sns.heatmap(matrix, annot, cmap) Correlation matrix, grid data Too many categories
Pairplot (manual) sns.pairplot(df, hue, diag_kind) Quick EDA across all variable pairs More than 8–10 variables
FacetGrid (manual) sns.FacetGrid(df, col, row) Same chart replicated per category Few categories (use hue instead)
Pie Chart ax.pie(sizes, labels) (rarely used) 3–5 categories, big differences More than 5 categories
Subplots plt.subplots(r, c) Combined with any Seaborn chart Dashboards, multiple questions Single focused story
Styling plt.style.use(...) sns.set_theme(style, palette) Professional appearance N/A, always use it

Data visualization is part technical, part editorial. The technical part, knowing which function produces which chart, is the easy half. The editorial part is harder: deciding what to show, to whom, and in what form, and that's what separates a chart that confuses from one that convinces. Every element on the chart should earn its place; if a colour, label, or line isn't helping the viewer understand something, cut it. The best charts feel effortless to read because someone put in the work to make them that way.