Skip to content

pandas hist

The shape of one column, in one call — and one argument that decides what you see.

Where do the values pile up, and where are they thin? No summary statistic answers that. describe gives you eight numbers, and eight numbers cannot tell a single hill from two. A histogram chops the range of a numeric column into bins, counts how many values land in each, and draws the counts as bars.

Two notes before the code. Bamboo Weekly draws its charts with Plotly now, so px.histogram is where a new reader should start; Pandas plotting is the legacy tool, which I still teach and which about a hundred archived solutions use. And the accessor mechanics are on the plot page. This page is about bins.

Official documentation: Series.plot.hist, DataFrame.plot.hist and DataFrame.hist.

The arguments that earn their keep

s.plot.hist()                          # exactly 10 bins, every time
s.plot.hist(bins=70)                   # a count you chose
s.plot.hist(bins=[0, 20, 70, 700])     # explicit edges, unequal widths
s.plot.hist(range=(0, 100))            # ignore the tail, keep the resolution
s.plot.hist(title='...', figsize=(8, 4), alpha=0.5)

df.hist(figsize=(10, 6))               # one panel per numeric column
s.hist(by=grouping_column)             # one panel per group

bins is the argument. Everything else is presentation.

A worked example, on real data

Every earthquake of magnitude 4 and above recorded worldwide in 2025 — 18,301 of them when I ran this, from a USGS catalog that is revised as events are reanalyzed:

import pandas as pd

url = ('https://earthquake.usgs.gov/fdsnws/event/1/query.csv'
       '?starttime=2025-01-01&endtime=2026-01-01&minmagnitude=4')

df = pd.read_csv(url, usecols=['time', 'mag', 'place', 'depth'])

df['depth'].plot.hist(title='Earthquake depth, km', figsize=(8, 4))

Depths run from the surface to 669.6 km, so the default ten bins are 67 km wide. The first one holds 13,518 of the 18,301 quakes, the next 2,239, and the rest trail off into a thin tail with a slight rise around 500 to 600 km. The chart says: earthquakes are shallow. True, and about as much as ten bars can carry.

Ask for seven times as many:

df['depth'].plot.hist(bins=70, title='Earthquake depth, km', figsize=(8, 4))

Now the bins are 9.6 km wide, and the picture is completely different. The leftmost bar — everything from the surface down to 9.6 km — holds 257 events. The one next to it holds 8,783. There is a wall at 10 km with almost nothing in front of it, which is not how the crust works:

(df['depth'] == 10).sum()
8258

Forty-five percent of the catalog is the literal number 10.0. When a seismic network cannot resolve the depth of an event, USGS fixes it at 10 km and moves on. That sentinel sat inside the first bar of the ten-bin chart, indistinguishable from real measurements. The 500-to-600 km rise resolves too, into a distinct second population — deep-focus quakes along subduction zones, a separate mechanism the coarse chart smeared into a bump.

The default did not lie. It could not resolve. Which is why the answer is never one histogram.

Three mistakes people make

Believing a single bin count. Too few bins hide structure, as above. Too many invent it: ask for 60 bins on mag in this same file and 19 of them come back empty, because 99.7 percent of magnitudes are reported to one decimal place and a bin 0.08 wide can fall entirely between two reportable values. The comb of gaps is an artifact of the bin width and says nothing about seismology. Draw at least two bin counts before you believe either.

Reading heights as density when the bins are unequal. Pass explicit edges — bins=[0, 20, 70, 300, 700] — and matplotlib plots raw counts, so a bar 400 km wide and a bar 20 km wide are drawn on the same scale and read as comparable. When the buckets are meant to be unequal, label them with cut and plot the counts as a bar chart, where nobody expects the widths to mean anything.

Handing it something that is not numeric. A text column gets you TypeError: no numeric data to plot, which is the kind outcome. Missing values are dropped without a word instead, so the bars add up to the non-null count that info reports, not to len(df).

Where it shows up in Bamboo Weekly

Bamboo Weekly #73: Avocado hand is the histogram as a data-quality tool, and free to read. One call — df['Age'].plot.hist() — on emergency-room records produces a chart with a bar out past age 200. The rest of the exercise is cleaning that up and drawing it again.

Bamboo Weekly #7: Bank failures histograms a column of years: df.loc[...,'FAILDATE'].dt.year.plot.hist() for Texas banks. The value_counts right above it lists the same numbers, and the chart is what makes the savings-and-loan crisis obvious. Also free.

Bamboo Weekly #86: FEMA is bins used deliberately: .dt.month produces twelve integers, so .plot.hist(bins=12) gives one bar per month rather than ten bars spanning uneven groups of months. Disaster declarations peak in late summer and early fall.

Practice it

Work through a hist exercise, with instant feedback and no signup required: practice.lernerpython.com/bamboo-weekly/hist/

Go deeper

px.histogram is the version to reach for today, with nbins and a color= split; plot covers the rest of the Pandas chart family. cut is a histogram you can name and group by, describe is the numeric summary a histogram should sit beside, and value_counts is the right tool when the column is categories rather than a range.

More Pandas videos on Python and Pandas with Reuven Lerner.

Bamboo Weekly is the practice. If you want the structured version — full courses with downloadable Jupyter notebooks, plus live Pandas office hours when you get stuck — that is LernerPython+Data. A paid Bamboo Weekly subscription is included with it.

See it on real data