Histograms
A histogram displays the frequency distribution of a single continuous numeric variable by dividing its range into consecutive, non-overlapping intervals (bins) and drawing a bar whose height is the count of values falling in each bin.
Marks: 45, 52, 55, 58, 61, 63, 65, 67, 68, 70, 72, 75, 78, 82, 88
Bin Frequency Bar
40-50 1 █
50-60 3 ███
60-70 6 ██████
70-80 3 ███
80-90 2 ██
Histogram vs Bar Chart — The Classic Exam Question
| Basis | Histogram | Bar Chart |
|---|---|---|
| Data type | Continuous / numeric | Categorical / discrete |
| X-axis | Numeric intervals (bins) | Distinct categories |
| Bars | Touch each other (no gaps) | Separated by gaps |
| Bar order | Cannot be reordered — the scale is continuous | Can be reordered (e.g. sorted by height) |
| Bar width | Represents the bin interval; meaningful | Arbitrary; no meaning |
| Shows | Distribution/shape of one variable | Comparison across categories |
| Example | Distribution of student marks | Number of students per department |
What a Histogram Reveals
- Central tendency — where the bulk of the data sits
- Spread — how wide the distribution is
- Shape/skewness — symmetric, left- or right-skewed
- Modality — one peak (unimodal), two (bimodal), many
- Outliers — isolated bars far from the main body
- Gaps — ranges with no data at all
A bimodal histogram is a signal, not a nuisance — it usually means two distinct populations have been mixed together (e.g. exam marks of a coached batch and an uncoached batch). Split and analyse them separately.
Choosing the Number of Bins
The bin count changes what you see, so it matters:
Sturges' Rule: k = 1 + 3.322 × log₁₀(n)
Square-root rule: k = √n
Rice rule: k = 2 × n^(1/3)
Freedman-Diaconis (bin WIDTH, robust to outliers):
2 × IQR
width = ───────────
n^(1/3)
Worked example. For n = 100 observations:
Sturges: k = 1 + 3.322 × log₁₀(100) = 1 + 3.322(2) = 7.64 ≈ 8 bins
Square-root: k = √100 = 10 bins
Rice: k = 2 × 100^(1/3) = 2 × 4.64 = 9.28 ≈ 9 bins
| Too few bins | Too many bins |
|---|---|
| Over-smoothed; hides multi-modality and gaps | Noisy, spiky; every random fluctuation looks like a feature |
Python
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
np.random.seed(42)
marks = np.random.normal(loc=65, scale=12, size=300).clip(0, 100)
fig, axes = plt.subplots(1, 3, figsize=(16, 4))
# Effect of bin count
for ax, bins in zip(axes, [5, 20, 60]):
ax.hist(marks, bins=bins, color="#168B99", edgecolor="white")
ax.set_title(f"{bins} bins")
ax.set_xlabel("Marks")
axes[0].set_ylabel("Frequency")
plt.tight_layout()
plt.show()
# Full-featured histogram with statistical reference lines
fig, ax = plt.subplots(figsize=(9, 5))
n, bins, patches = ax.hist(marks, bins=20, color="#168B99",
edgecolor="white", alpha=0.85)
ax.axvline(marks.mean(), color="#ef4444", linestyle="--", linewidth=2,
label=f"Mean = {marks.mean():.1f}")
ax.axvline(np.median(marks), color="#10b981", linestyle="-", linewidth=2,
label=f"Median = {np.median(marks):.1f}")
ax.set_title("Distribution of Student Marks (n = 300)", fontsize=13, fontweight="bold")
ax.set_xlabel("Marks")
ax.set_ylabel("Number of Students")
ax.legend()
ax.spines[["top", "right"]].set_visible(False)
plt.tight_layout()
plt.show()
print(f"Mean {marks.mean():.2f} | Median {np.median(marks):.2f} | Skew {pd.Series(marks).skew():.3f}")
# Mean ≈ Median and skew ≈ 0 -> symmetric distribution
# Histogram + KDE (smoothed density curve) with Seaborn
plt.figure(figsize=(9, 5))
sns.histplot(marks, bins=20, kde=True, color="#168B99", edgecolor="white")
plt.title("Marks Distribution with KDE Overlay")
plt.xlabel("Marks"); plt.ylabel("Count")
plt.show()
# Comparing distributions across a group
tips = sns.load_dataset("tips")
plt.figure(figsize=(9, 5))
sns.histplot(data=tips, x="total_bill", hue="time", bins=25,
kde=True, alpha=0.6, element="step")
plt.title("Total Bill Distribution — Lunch vs Dinner")
plt.show()
# Skewed data — histogram before and after a log transform
income = np.random.lognormal(mean=10.5, sigma=0.8, size=1000)
fig, axes = plt.subplots(1, 2, figsize=(13, 4))
axes[0].hist(income, bins=40, color="#f59e0b", edgecolor="white")
axes[0].set_title(f"Raw Income — skew = {pd.Series(income).skew():.2f}")
axes[1].hist(np.log(income), bins=40, color="#10b981", edgecolor="white")
axes[1].set_title(f"Log(Income) — skew = {pd.Series(np.log(income)).skew():.2f}")
plt.tight_layout(); plt.show()
# The log transform turns a heavily right-skewed distribution into a near-normal one
Frequency Table — The Histogram in Numbers
freq_table = pd.cut(pd.Series(marks), bins=[0, 40, 50, 60, 70, 80, 90, 100]).value_counts().sort_index()
freq_df = pd.DataFrame({
"Frequency": freq_table,
"Relative Freq %": (freq_table / len(marks) * 100).round(2),
"Cumulative Freq": freq_table.cumsum(),
})
print(freq_df)
# Frequency Relative Freq % Cumulative Freq
# (0, 40] 10 3.33 10
# (40, 50] 28 9.33 38
# (50, 60] 64 21.33 102
# (60, 70] 98 32.67 200
# (70, 80] 70 23.33 270
# (80, 90] 27 9.00 297
# (90, 100] 3 1.00 300
Related Chart Types
| Chart | Difference from a histogram |
|---|---|
| Frequency polygon | Line joining the mid-points of the bar tops; good for comparing several distributions |
| Ogive (cumulative frequency curve) | Plots cumulative frequency; used to read medians and quartiles graphically |
| KDE / density plot | Smooth continuous curve instead of discrete bars; no bin-width artefacts |
| Stem-and-leaf plot | Text-based; preserves the actual data values |
| Violin plot | Mirrored KDE, usually compared across groups |
The histogram shows the full shape of a distribution. The next chart — the box plot — sacrifices shape detail in exchange for a compact five-number summary that makes group comparison and outlier detection trivial.