hist() and the bin count that changes what the data appears to say.
Overview
What it returns
A histogram shows the distribution of a single numeric variable. It groups the values into bins and draws a bar for each bin whose height represents the count of values in that range. The shape of the distribution -- symmetric, skewed, bimodal, uniform -- is visible at a glance.
Histograms answer the question "how is this variable spread?" They show where the data clusters, where it thins out, and whether there are gaps or outliers. A summary statistic like the mean hides all of that. A histogram shows it.
The bin count controls the level of detail. Too few bins and the shape is lost. Too many bins and the noise dominates. There is no single correct number of bins. The right count depends on the sample size and the question you are asking. matplotlib provides several automatic rules, and this page covers when each one is appropriate.
Histograms are not bar charts. A bar chart compares categories. A histogram summarises a continuous variable. The bins are intervals on a numeric axis, not discrete labels. The bars touch because the underlying variable is continuous. Adding white edge colour between bars helps the eye separate them without implying gaps in the data.
This page covers the return values, bin choice, density normalisation, comparing groups, cumulative views, related displays like KDE curves, and the common formatting settings that make a histogram clear. It also covers the case where you choose your own bin edges, weight the values, and stack multiple distributions.
When comparing distributions across groups, overlapping histograms with reduced alpha work for two or three groups. Beyond that, the overlap becomes unreadable. Small multiples or violin plots are better for many groups. The page covers both approaches.
counts has one entry per bin. edges has one more. N bins are defined by n+1 boundaries. That off-by-one is the usual confusion when using the return values. It's the same rule np.histogram follows.
patches is the set of rectangles, kept if you want to recolour given bars. Highlighting a threshold, say.
edgecolor="white" is worth adding almost always: without it, adjacent bars merge into a single shape and the bin structure disappears.
Worth knowing
hist returns counts, edges, patches; there is always one more edge than count.
The bin count changes the apparent shape — too few hides structure, too many turns it into noise. Look at two or three.
bins="auto" applies a rule rather than a guess; the rules disagree, which is an honest summary of the problem.
density=True makes the area sum to 1, which is what lets samples of different sizes be compared.
A list of arrays draws groups side by side; separate calls with alpha overlays them.
A cumulative step plot reads off quantiles directly and barely depends on the bin count.
Histograms and Distributions
hist() and the bin count that changes what the data appears to say.
hist counts values into bins
And returns the counts and edges, which are often worth keeping.
example_01.pymatplotlib
import matplotlib.pyplot as plt
import numpy as np
rng = np.random.default_rng(0)
data = rng.normal(loc=50, scale=12, size=800)
fig, ax = plt.subplots(figsize=(6, 3))
counts, edges, patches = ax.hist(data, bins=20, edgecolor="white")
print("bins requested :", 20)
print("counts :", len(counts), "values")
print("edges :", len(edges), "<- one more than the counts")
print()
print("first three bins: %s to %s -> %d" % (round(edges[0], 1), round(edges[1], 1), counts[0]))
print(" %s to %s -> %d" % (round(edges[1], 1), round(edges[2], 1), counts[1]))
print()
print("Edges outnumber counts by one, because n bins have n+1 boundaries.")
Output
The bin count changes the story
Too few hides structure; too many turns it into noise.
example_02.pymatplotlib
import matplotlib.pyplot as plt
import numpy as np
rng = np.random.default_rng(1)
data = np.concatenate([rng.normal(30, 5, 400), rng.normal(60, 5, 400)])
fig, axes = plt.subplots(1, 4, figsize=(10, 2.6), sharey=True)
for ax, b in zip(axes, [3, 12, 50, 300]):
ax.hist(data, bins=b, edgecolor="white", linewidth=0.3)
ax.set_title("bins=%d" % b)
print("The data is genuinely two peaks at 30 and 60.")
print(" 3 bins : one lump - the structure is gone")
print(" 12 bins : both peaks visible")
print(" 50 bins : still clear, more ragged")
print(" 300 bins : noise; most bins hold 0-5 points")
print()
print("There is no correct answer, which is why looking at two or three")
print("is more honest than trusting one.")
Output
Letting numpy choose
bins="auto" applies a rule of thumb rather than a guess.
example_03.pymatplotlib
import matplotlib.pyplot as plt
import numpy as np
rng = np.random.default_rng(2)
data = rng.normal(size=2000)
fig, ax = plt.subplots(figsize=(6, 3))
counts, edges, _ = ax.hist(data, bins="auto", edgecolor="white")
for rule in ["auto", "sturges", "fd", "scott", "sqrt"]:
n = len(np.histogram_bin_edges(data, bins=rule)) - 1
print("%-8s -> %3d bins" % (rule, n))
print()
print("'auto' takes the larger of the Freedman-Diaconis and Sturges")
print("rules. They disagree, which is the honest summary of the problem.")
Output
density instead of counts
Needed to compare groups of different sizes, or to overlay a curve.
example_04.pymatplotlib
import matplotlib.pyplot as plt
import numpy as np
rng = np.random.default_rng(3)
small = rng.normal(0, 1, 200)
large = rng.normal(0, 1, 5000)
fig, (a, b) = plt.subplots(1, 2, figsize=(8, 3))
a.hist(small, bins=25, alpha=0.6, label="n=200")
a.hist(large, bins=25, alpha=0.6, label="n=5000")
a.set_title("counts - incomparable")
a.legend()
b.hist(small, bins=25, density=True, alpha=0.6, label="n=200")
b.hist(large, bins=25, density=True, alpha=0.6, label="n=5000")
b.set_title("density - comparable")
b.legend()
print("Same distribution, different sample sizes.")
print("Counts say the big sample is 25x more likely everywhere.")
print("density=True makes the total area 1, so the shapes overlay.")
Output
Comparing groups
Overlaid with transparency, or side by side - both have costs.
example_05.pymatplotlib
import matplotlib.pyplot as plt
import numpy as np
rng = np.random.default_rng(4)
a_data = rng.normal(45, 8, 600)
b_data = rng.normal(55, 8, 600)
fig, (p, q) = plt.subplots(1, 2, figsize=(9, 3))
p.hist([a_data, b_data], bins=20, label=["a", "b"])
p.set_title("side by side")
p.legend()
q.hist(a_data, bins=20, alpha=0.55, label="a")
q.hist(b_data, bins=20, alpha=0.55, label="b")
q.set_title("overlaid, alpha")
q.legend()
print("A list of arrays draws them side by side within each bin.")
print("Separate calls with alpha overlays them.")
print()
print("Side by side keeps both readable and halves the bin width.")
print("Overlaid keeps the bins but muddles the overlap colour.")
print("For more than three groups, neither works - use small multiples.")
Output
ax.hist([a, b], bins=20) — a list of arrays — draws them side by side within each bin. Both stay readable, at the cost of halving the good bin width.
split hist calls with alpha around 0.5 overlay them. The bins keep their width. The share region turns a blended colour that belongs to neither series.
histtype="step" draws outlines only. This Overlays cleanly for three or four groups where filled bars wouldn't.
Beyond three groups, none of these work well. The answer is small multiples. One histogram per group, sharing axes, side by side.
The cumulative view answers different questions
'How many are below x' is easier to read from a step than from bars.
example_06.pymatplotlib
import matplotlib.pyplot as plt
import numpy as np
rng = np.random.default_rng(5)
data = rng.normal(100, 15, 2000)
fig, (a, b) = plt.subplots(1, 2, figsize=(8, 3))
a.hist(data, bins=30, edgecolor="white")
a.set_title("distribution")
b.hist(data, bins=200, density=True, cumulative=True,
histtype="step", linewidth=2)
b.set_title("cumulative")
b.axhline(0.5, color="crimson", linestyle="--", linewidth=1)
print("median of the data: %.1f" % np.median(data))
print()
print("The cumulative plot reads off quantiles directly: where the")
print("curve crosses 0.5 is the median, 0.9 the 90th percentile.")
print("It also does not depend much on the bin count, which is why")
print("many bins is fine here and misleading on the left.")
Output
Bins decide the story
This is the thing to understand about histograms, and it isn't a detail.
The same data with three bins and with three hundred looks like two other datasets. Too few bins smooths away real structure. A bimodal spread turns one lump. Too many turns sampling noise into apparent spikes.
There's no correct number. It depends on the sample size, the underlying shape. What you're trying to see.
The honest practice is to look at more than one. If a feature lives at a few bin counts, it's probably real. If it appears only at one, it's probably not.
The automatic rules
bins="auto" uses the larger of two rules:
Sturges assumes roughly normal data and scales with log2(n).
Freedman–Diaconis uses the interquartile range and scales with n^(1/3). This Handles skew and outliers better.
"scott", "sqrt" and "rice" are also on hand. They disagree with each other, at times by a factor of a few. This Is a fair summary of how well-defined the problem is.
"auto" is a fair default and a starting point rather than an answer.
You can also pass clear edges: bins=np.arange(0, 101, 5) gives fixed 5-unit bins. This Is what you want when the boundaries have meaning or when two charts must be close.
density
density=True scales the bars so the total area is 1 rather than showing raw counts.
Two situations need it.
Checking samples of other sizes. With counts, a sample ten times larger has bars ten times taller all over. The shapes can't be checked. With density they overlay.
Overlaying a theoretical curve. A normal PDF is a density. It only lines up with a histogram that is also a density.
Note the y axis then reads as density, not proportion. The values can exceed 1 when the bins are narrow. The area is 1. The height isn't a chance.
The cumulative view
cumulative=True with histtype="step" draws an empirical cumulative spread.
It answers "what fraction is below x" right. This Is often the actual question. And quantiles read straight off it. Here They must be guessed by eye from a histogram.
It has a further hands-on edge: it barely depends on the bin count. Using two hundred bins gives a smooth curve rather than noise. Each bin adds to a running total rather than standing alone. That makes it the more honest display when the bin choice is doing too much work.
Related displays
ax.boxplot summarises a spread as five numbers. This Checks many groups compactly and hides the shape fully. A bimodal spread and a uniform one can give the same box.
ax.violinplot shows an guessed density. This Restores the shape at the cost of a smoothing param that has the same arbitrariness as the bin count.
ax.hexbin and ax.hist2d are the two-dimensional versions, for when the question is about the joint spread rather than a single var.
Bin edges you choose
Passing a count lets matplotlib pick the edges. This Land on unmemorable numbers like 3.17 to 7.42.
Passing an array puts them where you want:
ax.hist(ages, bins=np.arange(0, 101, 10))
Ten-year bands, starting at zero, with boundaries a reader can name. That is almost always better for exchange. It has a second gain. Two histograms drawn with the same clear edges are right close. Here Two drawn with bins=20 aren't.
range=(lo, hi) limits the extent without fixing the count. This Is how you exclude a long tail without dropping the data.
Values just on a boundary go into the right-hand bin, except at the last edge where the final bin includes both ends.
Weights
weights= gives each finding a multiplier. This Covers two common needs.
rate data — when the input is already counts per value rather than raw findings.
Percentages — weights=np.ones(n) / n * 100 makes the y axis read as a percentage of the sample. This Is often more handy than either counts or density and is easier to explain than density.
Density has the awkward property that its values depend on the bin width and can exceed 1. A percentage doesn't.
Two-dimensional histograms
ax.hist2d(x, y, bins=50) bins in both directions and colours the cells by count. ax.hexbin does the same with hexagonal bins. This Tile more evenly and avoid the visual artefacts of a square grid.
Both replace an overplotted scatter with a thing that measures density instead of implying it. Both need a colorbar to be readable.
bins="log" on hexbin, or norm=LogNorm(), handles the usual situation where a few cells hold most of the points and all else is faint.
Step histograms for comparison
histtype="step" draws the outline only. histtype="stepfilled" fills it with transparency.
For checking three or four spreads, outlines overlay far more cleanly than filled bars. Nothing is hidden. The colours don't blend into a fourth colour that belongs to no series.
Mixed with density=True it's the standard way to compare spreads of other sizes on one axes.
Beyond four, small multiples remain the answer.
What a histogram cannot show
A histogram shows a marginal spread and nothing else.
It can't show a link between two vars, a change over time, or an sorting within the data. Two datasets with same histograms can be fully other, in the same way that two datasets with same means can be.
The exact thing it hides is sequence: a series that drifts upward over time and one that is stationary give the same histogram. If the data has an order, a line chart of the values and a histogram of them answer other questions. The histogram alone can conceal a trend fully.
Choosing the bins in practice
A workable method, rather than a rule.
Start with bins="auto" and look at it. Then try roughly half and roughly double that number and look at those.
If the three agree about the shape, the shape is real and any of them will do. Pick round-numbered edges for the final version.
If they disagree, the disagreement is the finding. There's structure at one scale and not another. And the chart should show the bin count that matches to the question being asked.
For a chart that will be checked with another, fix the edges by name with an array. Two histograms with on its own chosen bins aren't close even when they look it. The bin widths differ.
And for anything above a few thousand points, a cumulative plot or a density guess sidesteps the choice fully. This Is often the honest move.
Related displays, and when to switch
A histogram is one of a family, and it isn't always the strongest member.
Cumulative — when the question is about quantiles or thresholds. To escape the bin-count problem.
Box plots — when checking more than three groups.
Violin or KDE — when the shape matters and the sample is large.
Strip plot — when the sample is small enough that the findings are the honest display.
Rug plot — tick marks along the axis, added under any of the above. This Shows just where the data is without a binning choice.
ax.plot(x, np.zeros_like(x), "|", markersize=12, alpha=0.3) draws a rug in one line. Mixing it with a histogram gives both the shape and the raw spots.
Histograms and Distributions, in summary
The bin count is the whole story: it decides what shape the data appears to have, there's no correct value. Looking at two or three is the only honest approach.
bins="auto" is a fair start and an arg between two rules rather than an answer.
clear edges make two histograms close. This On its own chosen bins never are.
density=True is what lets samples of other sizes be checked. Its y axis is a density rather than a proportion.
For a few groups, side-by-side or step outlines up to three or four, and small multiples beyond that.
And the cumulative view answers quantile questions right while barely based on the binning. Often the more honest display when the bin choice is doing too much work.
What to check on a histogram
Four things, none of which the chart tells you.
How many findings? A histogram of thirty looks much like one of thirty thousand. Means far less. Putting n in the title or a corner is a one-line fix.
How many bins, and does the shape live changing it? The single most key check.
Is anything outside the range? If range= was set, or the axis limits were, the excluded values are simply gone.
Are the bins equal width? With clear edges they may not be. Unequal bins with a count axis are misleading. The area should represent the count. This Is what density=True handles right and a raw count doesn't.
The last is a genuine trap: a histogram with wide bins at the tail and narrow ones in the middle, plotted as counts, exaggerates the tail. If the bins are unequal, density is the only honest y axis.
One more thing
np.histogram finds the counts without drawing anything. This Is handy when the numbers are wanted alongside the chart or when the binning needs checking before it's plotted.
It takes the same bins arg and returns counts and edges. The values behind a histogram can be printed, tested, or written to a file without a second pass over the data.
Histograms and Distributions: the short version
The bin count is a param that changes the answer. It's chosen by default when no one chooses it on purpose.
Looking at more than one binning. Stating which one the chart uses, is the whole of good practice here. Along with knowing that a cumulative view sidesteps the problem when the question is about quantiles.
Questions people ask about Histograms and Distributions
How many bins should I use? There is no single answer. Start with bins='auto', then try half and double that number. If the shape changes a lot, report that.
What does density=True do? It scales the bars so the total area is 1. Use it when comparing groups of different sizes.
What is the difference between a histogram and a bar chart? A histogram bins a continuous variable, so bars touch and order is fixed. A bar chart compares separate categories.
How do I compare two groups? Use the same bin edges for both, then use histtype='step', transparency, or side-by-side panels with a shared x axis.
How do I get the counts without drawing? Call np.histogram for counts and edges. ax.hist also returns them, as its first two return values.
Why does my histogram look different with the same data? Different bin edges move points between bars. A different bin count or a shifted start can change the apparent shape.
Reading the code back
A histogram is one call whose most key arg is the one people leave out. Passing clear edges rather than a count makes the chart close with another, gives boundaries the reader can name. Forces the binning to be a choice rather than a default. Adding the sample size to the title and an edge colour to the bars costs two more args and answers most of what a reader would ask.
Check yourself
0 of 4
Answer without scrolling back up.
Why does `hist` return one more edge than count?
The same convention np.histogram follows, and the usual off-by-one when using the return values.
What is the main risk of choosing a bin count?
There is no correct number. Looking at two or three tells you which features are real - a feature that survives several bin counts probably is.
Why is `density=True` needed to compare two samples of different sizes?
Density makes the total area 1 so the shapes overlay. The y axis then reads as density, and can exceed 1.
Why does a cumulative histogram tolerate many more bins?
It also reads off quantiles directly - where the curve crosses 0.5 is the median.
Cheat sheet
Histograms and Distributions
Plot a histogram in matplotlib and change the bin count. Watch the same data appear to tell a different story depending on the bins chosen.
Ashish Jangra builds and maintains VizLearn. Every module here is written and the visualisation behind it hand-built, so the numbers in a readout come from the same code that draws the picture. Corrections are genuinely welcome and get priority over everything else — if a page states something wrong, or an animation misrepresents what the algorithm does, get in touch.