Histograms
A histogram shows the distribution of a single numeric variable: how many observations fall into each bin. It is the first chart you draw for any new numeric column - it reveals skew, multimodality, gaps, and outliers that mean and standard deviation hide. Matplotlib's hist takes the raw data and a bin specification, and supports density normalization, cumulative counts, and overlaid or stacked comparisons.
Basic Histogram
import numpy as np
import matplotlib.pyplot as plt
rng = np.random.default_rng(3)
data = rng.normal(loc=100, scale=15, size=5000)
fig, ax = plt.subplots()
ax.hist(data, bins=40, color="tab:blue", alpha=0.8, edgecolor="white")
ax.set_xlabel("value")
ax.set_ylabel("count")
plt.show()
The default bin count is 10, which is almost always too coarse for real data - 30 to 50 bins, or a rule-based count, is the practical range. The white edgecolor keeps adjacent bars legible at high density. Change bins and watch the story: a hidden second bump appears at fine resolution, and noise masquerades as structure at extremes.
Choosing Bins
- Sturges (the old default logic): log2-based, fine for small n, too few for thousands.
- Square-root rule: sqrt(n) bins - a decent middle ground.
- Freedman-Diaconis: width based on the IQR, robust to outliers -
np.histogram_bin_edges(data, bins="fd")if you want the edges explicitly. - Explicit edges:
bins=[0, 10, 25, 50, 100, 200]when the business has natural breakpoints (age brackets, price tiers).
import numpy as np
edges = np.histogram_bin_edges(data, bins="fd")
fig, ax = plt.subplots()
ax.hist(data, bins=edges)
print("fd bin width:", round(edges[1] - edges[0], 3))
Density and Cumulative
import numpy as np
import matplotlib.pyplot as plt
rng = np.random.default_rng(4)
a = rng.normal(0, 1, size=4000)
b = rng.normal(2, 1.5, size=4000)
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(9, 3.5))
# density: areas sum to 1, so different sizes are comparable
ax1.hist(a, bins=40, density=True, alpha=0.6, label="A")
ax1.hist(b, bins=40, density=True, alpha=0.6, label="B")
ax1.legend()
# cumulative: "what fraction is below x"
ax2.hist(a, bins=40, cumulative=True, density=True, label="A")
ax2.hist(b, bins=40, cumulative=True, density=True, label="B")
ax2.legend()
fig.tight_layout()
plt.show()
density=True scales the y-axis so the histogram integrates to 1 - the only correct way to overlay distributions of different sizes. cumulative=True turns it into an empirical CDF, which answers "what percentage of orders are under $50?" directly, and pairs well with a vertical line at the threshold of interest.
Reading a Histogram
- Shape: symmetric (normal-ish), right-skewed (income, durations - consider a log scale), or multi-modal (a mixture of populations - segment before analyzing).
- Gaps: unexpected empty ranges usually mean data collection boundaries, not nature.
- Tails: one bar far from the mass is an outlier to investigate, not a reason to stretch the axis.
Summary
Draw the first histogram with 30-50 bins (or Freedman-Diaconis), use density=True whenever comparing distributions of unequal size, and use cumulative=True for percentile questions. Read shape, gaps, and tails before any modeling - the histogram is the cheapest diagnostic you will run all day.