Box Plots (Box-and-Whisker Plots)
A box plot, invented by John Tukey, summarises a numeric distribution using the five-number summary and explicitly marks outliers. It is the most efficient chart for comparing distributions across groups.
Anatomy of a Box Plot
outlier
○
┌────────┴────────┐ <- upper whisker (max within fence)
│
┌─────┴─────┐ Q3 (75th percentile)
│ │
├───────────┤ Median (Q2, 50th percentile)
│ │
└─────┬─────┘ Q1 (25th percentile)
│
└────────┬────────┘ <- lower whisker (min within fence)
Box height = IQR = Q3 − Q1 (the middle 50% of the data)
Whiskers extend to the most extreme point within 1.5 × IQR of the box
Points beyond the whiskers are plotted individually as OUTLIERS
The Five-Number Summary
| Statistic | Meaning |
|---|---|
| Minimum | Smallest value within the lower fence |
| Q1 | 25% of data lies below this |
| Median (Q2) | 50% of data lies below this |
| Q3 | 75% of data lies below this |
| Maximum | Largest value within the upper fence |
Outlier Fences
IQR = Q3 − Q1
Lower fence = Q1 − 1.5 × IQR
Upper fence = Q3 + 1.5 × IQR
Extreme (far) outliers use 3.0 × IQR instead of 1.5
Worked Example — By Hand
Data: 12, 15, 18, 22, 24, 25, 28, 30, 35, 42, 78 (n = 11, already sorted)
Median (Q2) = 6th value = 25
Lower half: 12, 15, 18, 22, 24 -> Q1 = 18
Upper half: 28, 30, 35, 42, 78 -> Q3 = 35
IQR = 35 − 18 = 17
Lower fence = 18 − 1.5(17) = 18 − 25.5 = −7.5
Upper fence = 35 + 1.5(17) = 35 + 25.5 = 60.5
Outliers: 78 (it exceeds 60.5)
Whiskers: lower = 12 (smallest value ≥ −7.5)
upper = 42 (largest value ≤ 60.5)
Five-number summary: 12, 18, 25, 35, 42 with 78 plotted as an outlier
Reading Shape from a Box Plot
Box Plot vs Histogram
| Basis | Box Plot | Histogram |
|---|---|---|
| Shows | Five-number summary + outliers | Full frequency distribution |
| Detects outliers | Explicitly marked | Only implied by isolated bars |
| Reveals modality | No — a bimodal distribution looks identical to a unimodal one | Yes — peaks are visible |
| Comparing groups | Excellent — many boxes side by side | Poor — overlapping histograms get cluttered |
| Space required | Very compact | More space per variable |
| Sensitive to bin choice | No | Yes |
Key limitation: a box plot cannot show multi-modality. Two datasets — one unimodal, one strongly bimodal — can produce identical box plots. Pair box plots with histograms or use a violin plot (a box plot with a mirrored KDE) when shape matters.
Python
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
data = pd.Series([12, 15, 18, 22, 24, 25, 28, 30, 35, 42, 78])
Q1, Q2, Q3 = data.quantile([0.25, 0.5, 0.75])
IQR = Q3 - Q1
lower, upper = Q1 - 1.5 * IQR, Q3 + 1.5 * IQR
print(f"Q1 = {Q1}, Median = {Q2}, Q3 = {Q3}, IQR = {IQR}")
print(f"Fences: [{lower}, {upper}]")
print("Outliers:", data[(data < lower) | (data > upper)].tolist())
# Q1 = 20.0, Median = 25.0, Q3 = 32.5, IQR = 12.5
# Fences: [1.25, 51.25]
# Outliers: [78]
print("Five-number summary:\n", data.describe()[["min", "25%", "50%", "75%", "max"]])
Note on quartile methods: pandas uses linear interpolation by default, which gives Q1 = 20.0 here, while the "median of the lower half" method taught in most textbooks gives Q1 = 18. Both are accepted; state which method you used.
# Single box plot
fig, ax = plt.subplots(figsize=(7, 5))
bp = ax.boxplot(data, vert=True, patch_artist=True, widths=0.4,
boxprops=dict(facecolor="#168B99", alpha=0.7),
medianprops=dict(color="#ef4444", linewidth=2),
flierprops=dict(marker="o", markerfacecolor="#ef4444", markersize=8))
ax.set_title("Box Plot with One Outlier", fontweight="bold")
ax.set_ylabel("Value")
plt.show()
# COMPARING GROUPS — where box plots genuinely shine
tips = sns.load_dataset("tips")
fig, axes = plt.subplots(1, 3, figsize=(16, 4.5))
sns.boxplot(data=tips, x="day", y="total_bill", ax=axes[0], palette="Set2")
axes[0].set_title("Total Bill by Day")
sns.boxplot(data=tips, x="day", y="total_bill", hue="sex", ax=axes[1], palette="Set1")
axes[1].set_title("Total Bill by Day and Sex")
# Violin plot — box plot + distribution shape
sns.violinplot(data=tips, x="day", y="total_bill", ax=axes[2], palette="Set3", inner="box")
axes[2].set_title("Violin Plot — Shape Revealed")
plt.tight_layout()
plt.show()
# Group-wise five-number summary in numbers
summary = tips.groupby("day")["total_bill"].describe()[["min", "25%", "50%", "75%", "max"]].round(2)
summary["IQR"] = (summary["75%"] - summary["25%"]).round(2)
print(summary)
# min 25% 50% 75% max IQR
# day
# Thur 7.51 12.44 16.20 20.16 43.11 7.72
# Fri 5.75 12.09 15.38 21.75 40.17 9.66
# Sat 3.07 13.91 18.24 24.74 50.81 10.83
# Sun 7.25 14.99 19.63 25.60 48.17 10.61
# INSIGHT: weekend bills are both higher (median) and more variable (IQR)
# Automated outlier report across every numeric column
def outlier_report(df):
rows = []
for col in df.select_dtypes(include=np.number).columns:
Q1, Q3 = df[col].quantile([0.25, 0.75])
IQR = Q3 - Q1
lo, hi = Q1 - 1.5 * IQR, Q3 + 1.5 * IQR
out = df[(df[col] < lo) | (df[col] > hi)][col]
rows.append({"column": col, "Q1": round(Q1, 2), "Q3": round(Q3, 2),
"IQR": round(IQR, 2), "lower_fence": round(lo, 2),
"upper_fence": round(hi, 2), "n_outliers": len(out),
"pct": round(len(out) / len(df) * 100, 2)})
return pd.DataFrame(rows)
print(outlier_report(tips))
When to Use a Box Plot
| Use it when | Prefer something else when |
|---|---|
| Comparing a numeric variable across 2–20 groups | You need to see the exact distribution shape → histogram/violin |
| Quickly detecting outliers | The sample is tiny (n < 10) → plot the raw points |
| Presenting a compact statistical summary | The audience is non-technical and unfamiliar with quartiles |
| Checking a modelling assumption about spread | You need frequencies/counts → histogram |
Histograms and box plots describe one variable. The next chart handles two.