Skip to main content
MahiDev — Learn. Build. Become AI-ready.

Lesson 17

Choosing Histogram Bins

Set the number of bins or their exact edges, work out the bin width, and see how the choice changes the shape a histogram shows.

Intermediate25 min
Choosing Histogram Bins hero Module 25 Lesson 17Choosing Histogram Bins hero Module 25 Lesson 17

The bins are the one real decision a histogram asks you to make, and they change what the chart says. Too few and every interesting feature is flattened into one block. Too many and the shape disappears into noise. You can give bins a count and let Matplotlib divide the range, or hand it the exact edges.

A histogram of heights split into five equal binsA histogram of heights split into five equal bins

A count, and the width it implies

Give bins a whole number and Matplotlib splits the range from the smallest value to the largest into that many equal intervals. The width follows from arithmetic you can do yourself: the range divided by the number of bins.

python
import matplotlib.pyplot as plt

heights = [
    189, 185, 195, 149, 189, 147, 154, 174, 169, 195, 159, 192,
    155, 191, 153, 157, 140, 144, 172, 157, 181, 182, 166, 167,
]

counts, edges, _ = plt.hist(heights, bins=5, edgecolor="#0F172A")
plt.title("Five equal bins")
plt.xlabel("centimetres")
plt.ylabel("people")
plt.show()

print("smallest:", min(heights), "largest:", max(heights))
print("width by hand:", (max(heights) - min(heights)) / 5)
print("width Matplotlib used:", round(edges[1] - edges[0], 1))

Output:

text
smallest: 140 largest: 195
width by hand: 11.0
width Matplotlib used: 11.0

The same data, three bin counts

Nothing about the data changes here, only the number of bins, and the story changes with it. This is worth doing every time: try a few counts, and only believe a feature that survives all of them.

python
import matplotlib.pyplot as plt

values = [
    87, 53, 66, 61, 67, 68, 62, 110, 104, 61, 111, 123, 117, 119,
    116, 104, 92, 111, 90, 103, 81, 80, 101, 51, 79, 107, 110, 129,
    145, 139, 110,
]

plt.figure(figsize=(11, 4))
for index, count in enumerate([3, 7, 20], start=1):
    plt.subplot(1, 3, index)
    plt.hist(values, bins=count, edgecolor="#0F172A", color="#2DD4BF")
    plt.title(f"bins={count}")

plt.tight_layout()
plt.show()

print("range:", max(values) - min(values))
for count in [3, 7, 20]:
    print(f"bins={count:<3} width {(max(values) - min(values)) / count:.1f}")

Output:

text
range: 94
bins=3   width 31.3
bins=7   width 13.4
bins=20  width 4.7

Three bins hide the two clusters. Twenty turns them into spikes that mostly reflect which single values happened to appear. Seven shows the shape that is really there.

Your own edges

Pass a list and its numbers become the boundaries. Five edges make four bins. Every bin is half-open, including its left edge and excluding its right, so a value of exactly 150 lands in the bin starting at 150. The last bin is the exception: it includes both ends, or the largest value would be counted nowhere.

python
import matplotlib.pyplot as plt

marks = [30, 34, 40, 44, 50, 55, 60, 60, 70, 84, 90, 100]
grades = [0, 35, 50, 70, 100]

counts, edges, _ = plt.hist(marks, bins=grades, edgecolor="#0F172A", color="#7C3AED")
plt.xticks(grades)
plt.title("Bins chosen to match the grade boundaries")
plt.xlabel("mark")
plt.ylabel("learners")
plt.show()

for start, end, count in zip(edges, edges[1:], counts):
    print(f"[{start:g}, {end:g}) -> {int(count)}")
print("the mark of exactly 100 is counted in the last bin")

Output:

text
[0, 35) -> 2
[35, 50) -> 2
[50, 70) -> 4
[70, 100) -> 4
the mark of exactly 100 is counted in the last bin

Unequal widths, read with care

Your edges do not have to be evenly spaced, which is useful when the meaningful groups are uneven. Be careful reading the result: a wide bin catches more values simply for being wide, so its taller bar is not necessarily a denser range.

python
import matplotlib.pyplot as plt

heights = [
    189, 185, 195, 149, 189, 147, 154, 174, 169, 195, 159, 192,
    155, 191, 153, 157, 140, 144, 172, 157, 181, 182, 166, 167,
]
edges = [140, 150, 160, 175, 185, 200]

counts, used, _ = plt.hist(heights, bins=edges, edgecolor="#0F172A", color="#94A3B8")
plt.xticks(edges)
plt.title("Bins of different widths")
plt.xlabel("centimetres")
plt.ylabel("people")
plt.show()

for start, end, count in zip(used, used[1:], counts):
    width = end - start
    print(f"[{start:g}, {end:g}) width {width:g} count {int(count)} "
          f"per cm {count / width:.2f}")

Output:

text
[140, 150) width 10 count 4 per cm 0.40
[150, 160) width 10 count 6 per cm 0.60
[160, 175) width 15 count 5 per cm 0.33
[175, 185) width 10 count 2 per cm 0.20
[185, 200) width 15 count 7 per cm 0.47

The last column is the honest comparison. Dividing each count by its bin width shows which range is genuinely crowded rather than simply wide.

Key takeaways

  • bins as a number splits the range into that many equal intervals

  • Bin width is the range divided by the number of bins

  • bins as a list gives the exact edges, and n edges make n minus 1 bins

  • Bins include their left edge and exclude their right, except the last

  • Next: Histogram Styles

  • Previous: Histograms

Loading playground…

Check your understanding

Choosing Histogram Bins Quiz

What does bins=6 do?

1 / 10