DataLens

Bar charts and histograms

Compare categories with sorted, labelled bars, choose grouped or stacked bars, and show distributions with histograms.

Level: Foundations · 8 min read

Practise this module

The questions for this lesson run in your browser and check your answer straight away.

The idea

Bar charts and histograms look alike, rectangles side by side, but they answer different questions:

This module uses Toko Kita's order lines, sales.csv:

import pandas as pd
import plotly.express as px

sales = pd.read_csv("sales.csv")
completed = sales[sales["status"] == "completed"]
print(completed[["product", "category", "quantity", "revenue", "channel"]].head(3))
             product    category  quantity  revenue channel
0     Desk Organizer  Stationery         1    89000     app
1  Arabica Gayo 250g      Coffee         3   285000     web
2   Pour-over Kettle     Kitchen         3   960000     app

A bar chart from aggregated data

The manager's first question is usually "which category earns the most?". That's a comparison between categories, so a bar chart fits. One thing to know: px.bar draws one bar per row of the DataFrame you give it. The sales data has one row per order line, so you add up revenue per category first, then chart the result:

by_category = (completed.groupby("category", as_index=False)["revenue"].sum()
               .sort_values("revenue", ascending=False))
fig = px.bar(by_category, x="category", y="revenue", title="Completed revenue by category")
fig.show()

Two habits are already in this code:

Tip

If you pass unaggregated rows, px.bar stacks one thin segment per row. It looks almost right, but hovering shows dozens of slivers. Aggregate first.

Horizontal bars for long labels

Product names are long. Rotated labels are hard to read; horizontal bars aren't:

by_product = (completed.groupby("product", as_index=False)["revenue"].sum()
              .sort_values("revenue"))
fig = px.bar(by_product, x="revenue", y="product", orientation="h",
             title="Completed revenue by product")
fig.show()

Swap x and y, and set orientation="h". Note the sort is ascending here: Plotly draws the first row at the bottom, so ascending order puts the biggest bar on top.

Labels on the bars

text_auto=True writes each bar's value on it. A format string controls how:

fig = px.bar(by_category, x="category", y="revenue", text_auto=".3s",
             title="Completed revenue by category")
fig.show()
print(fig.data[0].texttemplate)
%{y:.3s}

.3s means "3 significant digits with an SI suffix", so 5113000 shows as 5.11M. With values on the bars, readers don't have to trace gridlines, and you can often drop the y-axis entirely.

Two categories: grouped or stacked

The next question is often "and how does that split between the website and the app?". You can show a second category inside each bar by colouring it, with color=:

by_cat_channel = completed.groupby(["category", "channel"], as_index=False)["revenue"].sum()
fig = px.bar(by_cat_channel, x="category", y="revenue", color="channel",
             title="Revenue by category and channel")
fig.show()
print(fig.layout.barmode)
relative

Express stacks coloured bars by default (barmode="relative"). Stacked bars show the total and each part's contribution. To compare the parts with each other, put them side by side:

fig = px.bar(by_cat_channel, x="category", y="revenue", color="channel",
             barmode="group", title="Revenue by category and channel")
fig.show()
UseWhen the question is
stacked (barmode="relative")"What's the total, and what's it made of?"
grouped (barmode="group")"Is web bigger than app in each category?"

Histograms: the shape of one number

A different kind of question: are most orders small, big, or somewhere in between? You're not comparing categories now, you're looking at the spread of one number across all the orders. That's what a histogram shows:

fig = px.histogram(completed, x="revenue", nbins=10, title="Order line values")
fig.show()

px.histogram takes raw rows (no aggregating first): it sorts the values into bins (ranges) and counts the rows in each. nbins suggests how many bins to use. The shape tells a story: most lines are small, with a long tail of a few big ones. That "skewed right" shape is typical of money data, and it's why the median often describes a "typical" order better than the mean.

A histogram can also sum instead of count, which turns it into a quick bar chart of raw rows:

fig = px.histogram(completed, x="category", y="revenue", histfunc="sum",
                   title="Revenue by category (from raw rows)")
fig.show()

That's handy for exploration. For final charts, aggregate explicitly in pandas, so the numbers are easy to check.

Bars must start at zero

A bar's length encodes its value. If the axis starts at 1,000,000 instead of 0, a bar for 2,000,000 looks twice as long as one for 1,500,000, though it's only a third bigger. Plotly starts bar axes at zero by default; don't change that for bars. (Line charts are different: they encode change, so a zoomed-in axis can be fine.)

Why this matters for clean code

The cleanest chart code has three visible steps: filter (completed), aggregate (groupby + sum, sorted), draw (one px.bar call). Each step can be checked on its own, and the chart can't silently double-count, because you looked at the aggregated table before drawing it.

Recap

Practise this module

The questions for this lesson run in your browser and check your answer straight away.

Next: Axis titles, labels, and hover text →