Skip to content
wannacode

Groups: groupby

In short: groupby in pandas splits the rows by a key, computes in each group (sum, mean, agg) and combines the result into one table.

Problem

“How big is the average order?”

The manager asks how much a customer spends on one order on average. You have the orders of the year and their lines: every line is one product in one order, with the quantity and the price. Cancelled orders are not revenue, so you remove them with a mask, as in the lesson on filters:

Pythondata: the shop tables

There are 25,069 lines and 10,459 orders. The mean of the revenue column is €4.44, but that is the average line, not the average order. An order has several lines, and for an order you first have to add up its lines.

You need a total for every order: 10,459 numbers from 25,069 lines. A for loop with a dictionary can do it, but it is slow and long. pandas has one operation for it.

The idea

Split, apply, combine

groupby('order_id') splits the rows into groups: one group for each different value of order_id. Then you pick a column and a calculation: ['revenue'].sum() adds up the revenue in each group. At the end pandas puts the results into one Series with order_id as its index.

This is called split-apply-combine. Here it is on the first 7 lines:

Pythondata: the shop tables

Order 1 has one line (€31.60), and orders 2, 3 and 4 have two each. Order 3 is 37.5 + 8.4 = €45.90. Out of 7 lines come 4 numbers, one per group. The key order_id is the index of the result, which is why it sits under the name of the column.

Instead of sum you can use any summary from NumPy: mean, min, max, count.

See it

How groupby goes through the rows

The same 7 lines, slowly: every line goes into the group of its order_id, the group adds up to one number, and the numbers line up in the result.

import pandas as pd
items = pd.read_csv('order_items.csv').head(7)
items['revenue'] = items['quantity'] * items['unit_price']
print(items[['order_id', 'revenue']])
items.groupby('order_id')['revenue'].sum()
Step by step, in words
  1. 7 rows from order_items with the revenue of each line: orders 1, 2, 3 and 4.
  2. Split: groupby('order_id') puts every row into the group of its order_id.
  3. 4 different order_id values, 4 groups. Order 1 has one row, the others 2 each.
  4. Apply: ['revenue'].sum() in every group. Order 3: 37.5 + 8.4 = 45.9.
  5. Combine: the 4 sums become one Series with order_id as its index.
  6. 7 rows went in, 4 numbers came out: one for each group.

Try it

Which product is ordered most

In the 60 orders of order_items.csv: how many units of each product were ordered? Put them in units, and the three products with the most units in top3.

Pythondata: the shop tables

The idea

Several calculations at once: agg

You often need several numbers for each group. agg takes a list of calculations and gives a column for each:

Pythondata: the shop tables

The columns are named after the calculations. For a report it is better to name them yourself. These are named aggregations: new_name=('column', 'calculation'), and each can use a different column:

Pythondata: the shop tables

Order 3 has 2 lines, 7 units and €45.90 of revenue. The table has a row per order and a column for every question you asked.

The idea

The count: size or count

The two counters look alike, but they count different things. size() counts the rows in each group. count() counts the filled-in values in each column: NaN is not counted. Here are the customers by the year they signed up:

Pythondata: the shop tables

16 customers signed up in 2025, but the city column has 15 values: one customer's city is missing. If you want “how many customers”, size() is the right answer. count() on a column answers “for how many do we know the city”.

1. There are 16 customers from 2025, and for one of them the city is missing. What does customers.groupby('year')['city'].count() show for 2025?

The idea

Several keys

The key can be a list of columns. Then a group is every pair of values. Here are the units of the year by product and by price:

Pythondata: the shop tables

There are 25 products and 26 groups: one product was sold at two prices. That is coffee (product 17): 245 packs at €13.90 and 444 packs at €14.90, after the price went up on 1 March. The result has an index with two levels, and units.loc[17] takes the groups of coffee.

More precisely, if you are curious

Groups with NaN in the key drop out: the gift card has no category, and products.groupby('category_id') quietly leaves it out. groupby('category_id', dropna=False) keeps it as a NaN group. Check with len how many rows go into the groups and how many come out.

The idea

The group's number on every row: transform

Sometimes you do not want to shrink the group, but to put its number next to each of its rows. For example: how expensive a product is compared with the average price of its category. transform('mean') works out the mean of the group and repeats it on every row, keeping the order and the index of the table:

Pythondata: the shop tables

transform gives 25 numbers, one per product, and mean() gives 6, one per category. That is why the cat_avg column fits straight into the table. Coffee costs €14.90, and the average in drinks is €5.35: 2.79 times as much. Laundry powder is 2.19 times the average for cleaning.

More precisely, if you are curious

The result of mean() has category_id as its index, and the products table has the index 0, 1, 2… If you assign it as a column, pandas lines it up by the index, not by the category, and almost everything becomes NaN. transform keeps the index of the table. The gift card has no category, so its cat_avg is NaN.

Try it

The line's share of the order

For every line of order_items.csv: what part of its order's revenue does it bring in? Add to items a column order_total with the revenue of the whole order and a column share with the line's share.

Pythondata: the shop tables

The idea

The key as a column: as_index=False

The result of groupby has the key in the index. To filter it, join it with another table or put it on a chart, you need an ordinary column. There are two ways, with the same result:

Pythondata: the shop tables

as_index=False says it at grouping time, and reset_index() does it afterwards: the index becomes a column, and the rows are numbered 0, 1, 2… The columns are order_id and revenue.

Exercise

The average order

Answer the manager from the first card. Put the revenue of every order of the year without the cancelled ones in order_totals, and the average order, rounded to 2 places, in avg_order.

Pythondata: the shop tables

How many customers ordered at least once during the year, and how many of them only once? Put the number of orders of every customer in per_customer, the number of customers in n_customers, and how many have exactly one order in once.

Pythondata: the shop tables

Test

Check yourself

1. order_items.csv has 152 lines from 60 orders. How many rows does items.groupby('order_id')['revenue'].sum() have?
2. The average order line brings in €4.44, and the average order €10.63. Why are they different?
3. Next to every product you want the average price of its category, in a new column. What do you write?
4. What does as_index=False do in groupby?

Made by Ivo Gergov as part of Evo Tech's campaign for accessible programming education ·