Skip to content

plotly pie, histogram, and box

Three Plotly Express charts for one family of questions: how are these values divided up, and how are they spread out?

Three questions, three charts

Which chart should you reach for when the question is about distribution rather than about a trend?

It depends on which distribution question you are asking, and there are three of them. How does one total divide into parts? That is px.pie. What does one column of numbers look like — where do the values pile up, and where are they thin? That is px.histogram. How do several groups compare, spread and all, not just at their averages? That is px.box.

Those questions are close enough that people mix up the charts, and far enough apart that the wrong one hides the finding. A pie of ages is nonsense. A box plot of one column gives you five numbers where a histogram would have shown you the whole shape.

Official documentation: px.pie, px.histogram, and px.box, plus the tutorial pages for pie charts, histograms, and box plots.

Write the chart as the last step of the chain

Every Plotly Express function takes a data frame as its first argument, so you can always write px.histogram(df, x='Capacity (MW)'). I almost never do. I write the chart as the last step of a method chain, handing the data frame over with pipe:

(
    df
    .loc[pd.col('Status') == 'operating']
    .pipe(px.histogram, x='Capacity (MW)')
)

pipe passes the data frame to the function as its first argument, and every keyword argument after that goes straight through. This is the preferred form: filtering, grouping, and plotting in one expression, in reading order, with no intermediate variable. pipe shows up more than two hundred times across the Bamboo Weekly solutions, so a chart that breaks the chain reads as foreign.

The data below is the Global Coal Plant Tracker: 13,906 generating units, each with a country, region, status, and capacity.

import pandas as pd
import plotly.express as px

url = ('https://www.bambooweekly.com/content/files/wp-content/uploads/2024/02/'
       'global-coal-plant-tracker-january-2024.xlsx')

df = pd.read_excel(url, sheet_name='Units',
                   usecols=['Country', 'Region', 'Status', 'Start year',
                            'Capacity (MW)'])

px.pie: parts of one whole

px.pie is the odd one out in Plotly Express. Most of these functions guess sensibly from x= and y=; pie makes you say names= for the labels and values= for the sizes, every time. So a grouped result usually needs reset_index() first, to make both pieces real columns:

(
    df
    .loc[pd.col('Status') == 'operating']
    .groupby('Region')['Capacity (MW)'].sum()
    .div(1000)
    .reset_index()
    .pipe(px.pie, names='Region', values='Capacity (MW)', hole=0.4,
          title='Operating coal capacity by region (GW)')
)
Region
Africa        51.1526
Americas     221.4440
Asia        1667.4654
Europe       167.1535
Oceania       22.9030
Name: Capacity (MW), dtype: float64

Five slices: Asia 78.3 percent, the Americas 10.4, Europe 7.85, Africa 2.4, Oceania 1.08. That is a legitimate pie, because the parts really do add up to the world's operating coal fleet and the finding is a share rather than a quantity. hole=0.4 turns it into a donut, which I like because the center gives the eye somewhere to rest. Plotly also sorts the slices largest-first for you, whatever order your rows were in.

px.histogram: the shape of one column

A histogram takes one numeric column, chops its range into bins, and counts. In Plotly Express you name the column with x=, and the counting happens in the browser:

(
    df
    .loc[pd.col('Status') == 'operating']
    .pipe(px.histogram, x='Capacity (MW)', nbins=200)
)

With 200 bins the chart stops being a smooth hill and turns into a picket fence of tall thin spikes at round numbers. Count the raw values and you see why: 497 units are exactly 300 MW, 436 are exactly 350, 414 are exactly 660, 379 are exactly 600, and 172 are exactly 1000. Coal units are not built at arbitrary sizes. They come in catalog models, and the histogram is the only chart here that shows you that.

Three more arguments earn their place. color= splits one histogram into one per group; barmode='overlay' with opacity= stacks those groups on top of each other rather than side by side; and histnorm='percent' rescales each group to its own share, which is what you want the moment the groups are different sizes:

(
    df
    .loc[pd.col('Status').isin(['operating', 'retired'])]
    .pipe(px.histogram, x='Capacity (MW)', color='Status', nbins=60,
          barmode='overlay', opacity=0.6, histnorm='percent')
)

There are 6,580 operating units and 2,890 retired ones, so on raw counts the retired distribution looks like a shrunken copy of the operating one. On percentages the real difference appears: 71.8 percent of retired units were under 200 MW, against 37.9 percent of operating ones. The units being shut down are the small ones.

px.box: comparing several distributions

A box plot compresses a distribution to five numbers — minimum, first quartile, median, third quartile, maximum — which is a thin way to look at one group and an excellent way to compare many. Put the grouping column on x= and the numbers on y=:

(
    df
    .loc[pd.col('Status') == 'operating']
    .pipe(px.box, x='Region', y='Capacity (MW)', points='outliers',
          title='Operating coal unit capacity by region')
)

Median unit size runs 376 MW in the Americas, 365 in Oceania, 330 in Africa, 300 in Asia, and 137 in Europe. Europe's fleet is not smaller in total than Africa's — it is three times bigger — but it is made of far smaller machines, which no bar chart of totals would have told you.

Three arguments matter here. points='outliers' is the default, drawing only the stragglers; points='all' draws every observation as a jittered dot beside the box, and points=False draws none. color= gives each group its own color, or splits each group in two when you color by a second column. And notched=True cuts a waist into each box around the median, roughly a confidence interval: if two notches do not overlap, the medians are probably genuinely different.

Four mistakes worth knowing about

Drawing a pie with more than about five slices. Here is the single most useful sentence on this page: almost every pie chart you are about to draw should be a sorted bar chart instead. Group this data by country rather than region and you get 75 slices, 63 of them under one percent. Plotly does not refuse — it shrinks the pie to a coin to make room for 75 leader lines, and the labels (0.118%, 0.135%, 0.138%) run right off the bottom of the figure. A sorted px.bar of the same numbers stays readable at 75 rows and lets a reader compare any two of them. Pie earns its place only when there are a handful of parts and the share is the story.

Putting numbers in a pie that do not add up to anything. px.pie will happily divide a circle by whatever you hand it. Take the mean capacity per region — 428 MW in the Americas, 417 in Oceania, 353 in Africa, 330 in Asia, 205 in Europe — and the chart reports that the Americas are 24.7 percent of the pie. Of what? Those five means sum to 1,733 MW, a number that describes nothing in the world. Sums and counts can be pie charts. Averages, rates, and medians cannot.

Letting the bin count tell the story. The same 6,580 capacities look like two different datasets depending on nbins. At nbins=10 you get seven fat bars and a smooth right-skewed hill: most units small, a few large, nothing else to see. At nbins=200 you get the picket fence of standard unit sizes. Both are honest renderings of the same column, so try at least two bin counts before you believe a histogram, including your own. Note too that nbins is a hint rather than an instruction — Plotly rounds to a nice bin width, so asking for 60 bins here gets you about 28 of them, 50 MW wide.

Drawing a box plot that hides how many observations it stands on. Filter this data to units under construction and box it by region: Asia's box rests on 368 units, Africa's and Europe's on 6 each, and the Americas' on exactly 1, drawn as a flat line at 120 MW the same width as everyone else's box. Nothing in the chart distinguishes a group of 3 from a group of 3,000. The cheap fix is points='all', which makes a six-dot group look like a six-dot group. The thorough fix is to put the count in the label, or to drop tiny groups before plotting.

Where it shows up in Bamboo Weekly

These three are a quiet corner of the Plotly toolbox: px.pie appears in five Bamboo Weekly solutions, px.histogram in two, px.box in two. Every one of them is written with pipe.

Bamboo Weekly #171: Hantavirus uses two of the three. The histogram of patient ages comes out as one blob at the default bin count, and .pipe(px.histogram, nbins=20) fixes it. Then a value_counts() of symptoms, pulled out of a comma-separated column with explode, goes to .pipe(px.pie, names='symptoms', values='count').

Bamboo Weekly #161: Missiles in Israel is the clearest nbins argument I know: air-raid alerts by hour of day, spread across 10 default bins for 24 hours. .pipe(px.histogram, x='hour', nbins=24) gives one bin per hour, and only then does the daily rhythm appear.

Bamboo Weekly #150: Kalshi is a pie chart that deserves to be one: trading volume by contract category, grouped, sorted, and piped into px.pie. More than 77 percent of the money on a prediction market is on sports — a share, of a real whole, in a handful of slices.

Bamboo Weekly #130: Jobs reporting uses px.box on the variance in US jobs numbers over the years, and Bamboo Weekly #145: Economic indicators uses one to make a point about scale: box several economic series together and the ones measured in millions squash the ones measured in percentages flat against the axis.

Practice it

Work through a px.pie(), px.histogram() and px.box() exercise, with instant feedback and no signup required: practice.lernerpython.com/bamboo-weekly/plotly-distributions/

Go deeper

The chart is the last step; the shape is the work. groupby gives a pie one row per slice, value_counts does the same for categories, and unstack turns a grouped result into the one-column-per-series shape a split histogram wants. describe prints the same five numbers a box plot draws, which is a good way to check that a chart says what you think it says. And when the pie chart turns out to be a bar chart, that page is px.bar.

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.

Part of the Pandas Methods Index. See also practice by skill.