Histograms and Distributions

hist() and the bin count that changes what the data appears to say.

Overview

What it returns

ax.hist(data, bins=20) returns counts, edges, patches.

counts has one entry per bin. edges has one more, because n bins are defined by n+1 boundaries. That off-by-one is the usual confusion when using the return values, and it is the same convention np.histogram follows.

patches is the collection of rectangles, kept if you want to recolour particular bars — highlighting a threshold, for instance.

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
Output

The bin count changes the story

Too few hides structure; too many turns it into noise.

example_02.pymatplotlib
Output

Letting numpy choose

bins="auto" applies a rule of thumb rather than a guess.

example_03.pymatplotlib
Output

density instead of counts

Needed to compare groups of different sizes, or to overlay a curve.

example_04.pymatplotlib
Output

Comparing groups

Overlaid with transparency, or side by side - both have costs.

example_05.pymatplotlib
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 effective bin width.

Separate hist calls with alpha around 0.5 overlay them. The bins keep their width, and the overlap region becomes a blended colour that belongs to neither series.

histtype="step" draws outlines only, which overlays cleanly for three or four groups where filled bars would not.

Beyond three groups, none of these work well, and 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
Output

Bins decide the story

This is the thing to understand about histograms, and it is not a detail.

The same data with three bins and with three hundred looks like two different datasets. Too few bins smooths away real structure — a bimodal distribution becomes one lump. Too many turns sampling noise into apparent spikes.

There is no correct number. It depends on the sample size, the underlying shape, and what you are trying to see.

The honest practice is to look at more than one. If a feature survives at several bin counts, it is probably real; if it appears only at one, it is 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), which handles skew and outliers better.

"scott", "sqrt" and "rice" are also available. They disagree with each other, sometimes by a factor of several, which is a fair summary of how well-defined the problem is.

"auto" is a reasonable default and a starting point rather than an answer.

You can also pass explicit edges: bins=np.arange(0, 101, 5) gives fixed 5-unit bins, which is what you want when the boundaries have meaning or when two charts must be comparable.

density

density=True scales the bars so the total area is 1 rather than showing raw counts.

Two situations need it.

Comparing samples of different sizes. With counts, a sample ten times larger has bars ten times taller everywhere, and the shapes cannot be compared. With density they overlay.

Overlaying a theoretical curve. A normal PDF is a density, and it only lines up with a histogram that is also a density.

Note the y axis then reads as density, not proportion, and the values can exceed 1 when the bins are narrow. The area is 1; the height is not a probability.

The cumulative view

cumulative=True with histtype="step" draws an empirical cumulative distribution.

It answers "what fraction is below x" directly, which is often the actual question — and quantiles read straight off it, where they must be estimated by eye from a histogram.

It has a further practical advantage: it barely depends on the bin count. Using two hundred bins gives a smooth curve rather than noise, because 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.

ax.boxplot summarises a distribution as five numbers, which compares many groups compactly and hides the shape entirely — a bimodal distribution and a uniform one can produce the same box.

ax.violinplot shows an estimated density, which restores the shape at the cost of a smoothing parameter 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 distribution rather than a single variable.

Bin edges you choose

Passing a count lets matplotlib pick the edges, which 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 communication, and it has a second benefit: two histograms drawn with the same explicit edges are directly comparable, where two drawn with bins=20 are not.

range=(lo, hi) limits the extent without fixing the count, which is how you exclude a long tail without dropping the data.

Values exactly 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 observation a multiplier, which covers two common needs.

Frequency data — when the input is already counts per value rather than raw observations.

Percentagesweights=np.ones(n) / n * 100 makes the y axis read as a percentage of the sample, which is often more useful 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 does not.

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, which tile more evenly and avoid the visual artefacts of a square grid.

Both replace an overplotted scatter with something that measures density instead of implying it, and 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 everything else is faint.

Step histograms for comparison

histtype="step" draws the outline only. histtype="stepfilled" fills it with transparency.

For comparing three or four distributions, outlines overlay far more cleanly than filled bars: nothing is hidden, and the colours do not blend into a fourth colour that belongs to no series.

Combined with density=True it is the standard way to compare distributions of different sizes on one axes.

Beyond four, small multiples remain the answer.

What a histogram cannot show

A histogram shows a marginal distribution and nothing else.

It cannot show a relationship between two variables, a change over time, or an ordering within the data. Two datasets with identical histograms can be completely different, in the same way that two datasets with identical means can be.

The specific thing it hides is sequence: a series that drifts upward over time and one that is stationary produce the same histogram. If the data has an order, a line chart of the values and a histogram of them answer different questions, and the histogram alone can conceal a trend entirely.

Choosing the bins in practice

A workable procedure, 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 is structure at one scale and not another — and the chart should show the bin count that corresponds to the question being asked.

For a chart that will be compared with another, fix the edges explicitly with an array. Two histograms with automatically chosen bins are not comparable even when they look it, because the bin widths differ.

And for anything above a few thousand points, a cumulative plot or a density estimate sidesteps the choice entirely, which is often the honest move.

A histogram is one of a family, and it is not always the strongest member.

Cumulative — when the question is about quantiles or thresholds, and to escape the bin-count problem.

Box plots — when comparing 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 observations are the honest display.

Rug plot — tick marks along the axis, added under any of the above, which shows exactly 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, and combining it with a histogram gives both the shape and the raw positions.

In summary

The bin count is the whole story: it decides what shape the data appears to have, there is no correct value, and looking at two or three is the only honest approach.

bins="auto" is a reasonable start and an argument between two rules rather than an answer.

Explicit edges make two histograms comparable, which automatically chosen bins never are.

density=True is what lets samples of different sizes be compared, and its y axis is a density rather than a proportion.

For several groups, side-by-side or step outlines up to three or four, and small multiples beyond that.

And the cumulative view answers quantile questions directly while barely depending 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 observations? A histogram of thirty looks much like one of thirty thousand, and means far less. Putting n in the title or a corner is a one-line fix.

How many bins, and does the shape survive changing it? The single most important 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 explicit edges they may not be, and unequal bins with a count axis are misleading — the area should represent the count, which is what density=True handles correctly and a raw count does not.

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 computes the counts without drawing anything, which is useful when the numbers are wanted alongside the chart or when the binning needs checking before it is plotted.

It takes the same bins argument and returns counts and edges, so the values behind a histogram can be printed, tested, or written to a file without a second pass over the data.

The short version

The bin count is a parameter that changes the answer, and it is chosen by default when nobody chooses it deliberately.

Looking at more than one binning, and 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.

Reading the code back

A histogram is one call whose most important argument is the one people leave out. Passing explicit edges rather than a count makes the chart comparable with another, gives boundaries the reader can name, and forces the binning to be a decision rather than a default. Adding the sample size to the title and an edge colour to the bars costs two more arguments and answers most of what a reader would ask.

Check yourself

0 of 4

Answer without scrolling back up.

  1. Why does `hist` return one more edge than count?

  2. What is the main risk of choosing a bin count?

  3. Why is `density=True` needed to compare two samples of different sizes?

  4. Why does a cumulative histogram tolerate many more bins?

Cheat sheet

Histograms and Distributions

counts has one entry per bin. edges has one more, because n bins are defined by n+1 boundaries. That off-by-one is the usual confusion when using the return values, and it is the same convention np.histogram follows.

MATPLOTLIB · vizlearn.in/matplotlib/histograms.html

About the author

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.