Distributions
In short: A distribution shows how all the values are spread at once: a histogram and a box plot show it for one variable, and a scatter plot shows the link between two.
Problem
“How much is an order worth?”
The manager wants to know what one order is worth. The mean has been worked out: €10.63. The advertising will be planned for “the typical customer with an order of about €10”.
There are 10,459 sold orders (without the cancelled ones), and 6,736 of them are below the mean. Almost two in every three. How can the “average” be above most of them? You see the answer when you look at all the orders at once instead of one number.
The idea
Histogram
How the values spread out, which are common and which are rare, is called a distribution. Its picture is the histogram: the x axis is split into equal intervals, and each bar is how many orders fall into its interval.
ax.hist(values, bins=30) makes 30 intervals from the smallest to the largest value and counts them. It returns three things: the count in each interval, the edges and the bars themselves.
The first bar runs from €0.80 to €3.40 and holds 1,915 orders. The second, from €3.40 to €5.99, is the highest: 2,122. After that the bars fall, and on the right a thin strip of rare orders remains, all the way to €78.70. That strip is called the tail. A distribution with a long tail to the right is normal for money: many small purchases and a few large ones.
The idea
The number of bins changes the picture
You choose bins, and a different number tells a different story. Here are the same orders with 4 and with 80 bins:
With 4 bins you only see that 9,091 orders are below €20.28, 1,182 are up to €39.75, and after that there are hardly any. With 80 bins you see that most orders are between €2 and €8, and the count falls off smoothly towards the tail. The spikes (one bar high, its neighbour low) come from the prices: some totals add up more often than others.
There is no single right number. Try a few and pick the one where the shape shows and the noise does not hide it. For a few thousand values, 30–60 bins is usually a good start.
More precisely, if you are curious
bins can also be a list of edges: bins=[0, 5, 10, 20, 40, 80]. Then the bins are not equally wide, and the height misleads: a wide bin collects more orders only because it is wide. With density=True the height is divided by the width of the bin, and the wide bins no longer mislead.
Try it
December up close
What do the December orders look like? Store the sold orders from December in december and draw a histogram of their value with 40 bins. Label the x axis.
Solution
import pandas as pd
import matplotlib.pyplot as plt
orders = pd.read_csv('year_orders.csv', parse_dates=['ordered_at'])
items = pd.read_csv('year_items.csv')
items['revenue'] = items['quantity'] * items['unit_price']
per_order = items.groupby('order_id', as_index=False).agg(
revenue=('revenue', 'sum'), lines=('product_id', 'size'), pieces=('quantity', 'sum'))
orders = orders.merge(per_order, left_on='id', right_on='order_id', how='left', validate='one_to_one')
# Revenue leaves out the cancelled orders.
sold = orders[orders['status'] != 'cancelled']
values = sold['revenue']
december = sold[sold['ordered_at'].dt.month == 12]
fig, ax = plt.subplots()
ax.hist(december['revenue'], bins=40)
ax.set_title('Orders in December')
ax.set_xlabel('Order value, €')
ax.set_ylabel('Number of orders')sold[sold["ordered_at"].dt.month == 12] gives 1,211 orders, and ax.hist(december["revenue"], bins=40) spreads them from €0.80 to €78.00. Besides the peak on the left there is a second bump around €20–30: many of these orders hold a €20 gift card, which sells mostly in December. Without bins matplotlib uses 10 bins and the shape is lost. If you take the orders from orders, the histogram also holds 74 cancelled ones: 1,285 instead of 1,211.
The idea
Mean and median
Sort all the orders from the smallest to the largest. The value right in the middle is the median: half the orders are below it, half above. values.median() gives it.
Draw the mean and the median on the histogram as vertical lines. ax.axvline(x) draws a line through the whole height of the chart at the point x:
The mean is €10.63, and the median is €7.70. The median sits where the bulk of the bars is. The mean is to its right: the tail of large orders pulls it up, because every euro of them goes into the sum. The median does not care whether the largest order is €78 or €7,800.
So with money, times and sizes, where there is a tail, look at both. If they are far apart, the median is closer to “the typical customer”.
The idea
Box plot
A histogram is detailed, but two or five histograms side by side are hard to compare. A box plot shrinks a distribution to five things. December is the strongest month: are its orders larger, or are there only more of them?
How to read it:
- the line in the box is the median: €8.60 in December and €7.60 in the other months;
- the box is the middle half of the orders: a quarter are below it, a quarter above;
- the whiskers reach the last usual values;
- the circles are unusually large orders.
The December box is taller and reaches higher: there are more large orders there (gifts, pans, knives). So the mean changes more than the median: €13.14 against €10.30, while the median is €8.60 against €7.60. December has more orders and larger ones, but the difference is mostly in the tail.
More precisely, if you are curious
A whisker reaches the farthest order that is no more than 1.5 box heights from the box. Anything farther is a circle. You will compute this rule and the box height (IQR) with numbers in the lesson on describing data.
In older versions of matplotlib (before 3.9) the labels are given with labels= instead of tick_labels=.
The idea
Scatter: two numbers per order
Does the money grow with the number of lines in the order? Here you have two numbers per order, and the question is about how they relate. The picture is a scatter plot: each order is a point, x is one number, y is the other. ax.scatter(x, y) draws it.
On the left everything is in solid columns. Thousands of points lie on top of each other, and you cannot see where there are many and where there are few. This is called overplotting. On the right alpha=0.03 makes each point almost transparent: where there are many, the colour builds up and it is dark.
Now you see that with each line the order grows by about €4–5 on average: from €4.27 with one line (2,517 orders) to €27.48 with six (145 orders). But the points of each line spread upwards: one expensive product costs as much as ten cheap ones.
sharey=True gives the two charts the same y axis, so that they compare fairly.
Try it
Pieces and value
The pieces column is the number of pieces in the order (the sum of quantity). Draw a scatter plot: the pieces on x, the order value on y, with transparency so you can see where many orders are. Label both axes.
Solution
import pandas as pd
import matplotlib.pyplot as plt
orders = pd.read_csv('year_orders.csv', parse_dates=['ordered_at'])
items = pd.read_csv('year_items.csv')
items['revenue'] = items['quantity'] * items['unit_price']
per_order = items.groupby('order_id', as_index=False).agg(
revenue=('revenue', 'sum'), lines=('product_id', 'size'), pieces=('quantity', 'sum'))
orders = orders.merge(per_order, left_on='id', right_on='order_id', how='left', validate='one_to_one')
# Revenue leaves out the cancelled orders.
sold = orders[orders['status'] != 'cancelled']
values = sold['revenue']
fig, ax = plt.subplots()
ax.scatter(sold['pieces'], sold['revenue'], alpha=0.05)
ax.set_title('Pieces and order value')
ax.set_xlabel('Pieces in the order')
ax.set_ylabel('Order value, €')ax.scatter(sold["pieces"], sold["revenue"], alpha=0.05) draws 10,459 points. The pieces go from 1 to 14, and the points are densest between 1 and 5 pieces and below €20. Without alpha the columns are solid, and an order with 14 pieces looks as important as the thousands with 2.
Exercise
The answer for the manager
The manager wants one chart: what the orders are worth and where the mean and the median are. Store the mean of the sold orders' value in mean_value and the median in median_value. Draw a histogram with 50 bins and the two values as vertical lines, with a legend.
Solution
import pandas as pd
import matplotlib.pyplot as plt
orders = pd.read_csv('year_orders.csv', parse_dates=['ordered_at'])
items = pd.read_csv('year_items.csv')
items['revenue'] = items['quantity'] * items['unit_price']
per_order = items.groupby('order_id', as_index=False).agg(
revenue=('revenue', 'sum'), lines=('product_id', 'size'), pieces=('quantity', 'sum'))
orders = orders.merge(per_order, left_on='id', right_on='order_id', how='left', validate='one_to_one')
# Revenue leaves out the cancelled orders.
sold = orders[orders['status'] != 'cancelled']
values = sold['revenue']
mean_value = values.mean()
median_value = values.median()
fig, ax = plt.subplots()
ax.hist(values, bins=50, color='lightgray')
ax.axvline(mean_value, color='black', label='mean')
ax.axvline(median_value, color='black', linestyle='--', label='median')
ax.set_title('What one order is worth')
ax.set_xlabel('Order value, €')
ax.legend()
print(round(mean_value, 2), round(median_value, 2))The mean is €10.63, and the median is €7.70. Half the orders are below €7.70, and 6,736 of 10,459 are below the mean. The typical order is closer to €7.70, and the mean is higher because of the tail of large orders up to €78.70. If you compute from orders, the cancelled ones get into the numbers too: the mean comes out at €10.61.
Test
Check yourself
Related lessons
Section titled "Related lessons"- Builds on: Groups: groupby · Joining: merge · The first chart · Which chart for which question
- Continues in: Describing the data · Sample and population · Confidence intervals · Correlation and cause
- Tasks on this topic: Charts