Data Analysis with Python (pandas, polars, DuckDB) Module 5 – Summaries, group-by and pivot tables
Group-by in pandas: split, apply, combine
Summarise a pandas DataFrame by one or more keys with named aggregations, count rows correctly and keep rows with missing keys, as in polars and SQL.
What you will learn
- Aggregate by one or more keys with named aggregations
- Explain how group-by treats missing keys and unused categories in pandas 3
- Count correctly with size, count and nunique
- Rename and flatten aggregated results so they are ready for reporting
Before you start
What you’ll learn · Before you start
What you will learn
- Aggregate by one or more keys with named aggregations
- Explain how group-by treats missing keys and unused categories in pandas 3
- Count correctly with size, count and nunique
- Rename and flatten aggregated results so they are ready for reporting
Before you start
On this page
Sixteen authored canteen orders hold 1410 in recorded amounts, but a default outlet summary loses 125. A group-by answers “for each X, what is Y?”: revenue per outlet, orders per day, the average mark per class. pandas does it in three steps, often called split-apply-combine. It splits the rows into groups by
the values of one or more group key: The fields whose values place source rows into groups under an explicit policy for equality, missing keys and unused categories., applies a function such as a sum or a mean to each group, and combines
the results into a new table with one row per group. The clearest way to write it is a named aggregation: A grouped output definition that explicitly names its result column and identifies the input column and aggregation function.,
df.groupby("outlet").agg(revenue=("amount", "sum")): the name on the left becomes an output column, and the pair on
the right says which column to read and which function to apply. Three details decide whether the result is right:
rows whose key is missing, which pandas leaves out by default; the difference between counting rows, values and
distinct values; and the shape of the result.
One scalar aggregation reduces each observed group
The authored fixture uses 16 orders for a college canteen with three named outlets. One order was saved without its outlet and one without its amount, two distinct gaps whose treatment the report needs to state.
order_id,outlet,slot,customer,amount
1,Library,breakfast,C01,60
2,Library,lunch,C02,120
3,Hostel,lunch,C01,150
4,Main gate,snacks,C03,40
5,Library,lunch,C04,110
6,Hostel,breakfast,C05,70
7,Main gate,lunch,C02,130
8,Hostel,snacks,C01,45
9,Library,snacks,C06,35
10,Main gate,breakfast,C03,55
11,,lunch,C07,125
12,Hostel,lunch,C05,
13,Main gate,lunch,C08,140
14,Library,lunch,C02,115
15,Hostel,lunch,C09,165
16,Main gate,snacks,C03,50 Split, apply, combine on the 16 canteen orders
Text description of the diagram
The table holds 16 orders with the columns order_id, outlet, slot, customer and amount.
- Split: groupby("outlet") puts the rows of each outlet together. Hostel gets orders 3, 6, 8, 12 and 15, Library orders 1, 2, 5, 9 and 14, and Main gate orders 4, 7, 10, 13 and 16.
- Order 11 has no outlet. pandas leaves it out of every group by default; dropna=False gives it a group of its own.
- Apply: sum() adds the amounts of each group. Hostel's sum is 430, because order 12 has no amount and is skipped; Library's is 440 and Main gate's 415.
- Combine: the three sums become a new table with one row per outlet: Hostel 430, Library 440, Main gate 415.
The retained null-key group contains order 11 and 125. The complete four-row report reconciles to 1410, while the default three-row report totals 1285. The Hostel amount count is 4 rather than its row count 5. These are separate controls and separate denominators.
import pandas as pd
orders = pd.read_csv("orders.csv") # 16 canteen orders; one has no outlet, one has no amount
summary = orders.groupby("outlet", as_index=False).agg(
orders=("order_id", "size"), # rows in the group
revenue=("amount", "sum"), # missing amounts are skipped
avg_basket=("amount", "mean"),
)
print(summary.round(1))
print()
print("revenue in the summary:", summary["revenue"].sum())
print("revenue in the table: ", orders["amount"].sum()) Output
outlet orders revenue avg_basket 0 Hostel 5 430.0 107.5 1 Library 5 440.0 88.0 2 Main gate 5 415.0 83.0 revenue in the summary: 1285.0 revenue in the table: 1410.0
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 by_outlet.py
Each output column comes from one pair. orders=("order_id", "size") counts the rows of each group,
revenue=("amount", "sum") adds the amounts and avg_basket=("amount", "mean") averages them. as_index=False keeps
the outlet as an ordinary column, so the result can be merged, sorted or saved like any table; without it, the keys
become the index. Hostel’s average is 107.5, which is 430 divided by 4, not by 5: like most aggregations, sum and
mean skip missing values.
The last two lines are the check that matters. The summary’s revenue is 1,285, but the table holds 1,410, and the missing 125 belongs to order 11, the order without an outlet.
The drawing shows the whole scene; its 3 steps are listed below.
Every step as text
- Source rows. The source has 16 rows. One outlet is missing, and one amount is missing; the recorded non-missing amounts sum to 1410.
- Observed outlet groups. Default grouping uses the 15 rows with known outlets. Hostel has 5 rows but only 4 recorded amounts.
- Named output columns. Named scalar aggregations produce one row per observed group. Hostel's mean uses four amounts, so 430 divided by 4 gives 107.5.
Key idea
Checkpoint
Check: Named aggregation
An explicit missing-key policy restores the 125
By default, groupby drops every row whose key is missing (dropna=True), and nothing warns you. Two changes keep
the row:
import pandas as pd
orders = pd.read_csv("orders.csv")
print("order 11 has no outlet:", orders.loc[orders["order_id"] == 11, "outlet"].isna().item())
print()
print(orders.groupby("outlet")["amount"].sum()) # default: the order is left out
print()
print(orders.groupby("outlet", dropna=False)["amount"].sum()) # a group for the missing key
print()
labelled = orders.fillna({"outlet": "Unknown"}) # a name a report can show
print(labelled.groupby("outlet")["amount"].sum()) Output
order 11 has no outlet: True outlet Hostel 430.0 Library 440.0 Main gate 415.0 Name: amount, dtype: float64 outlet Hostel 430.0 Library 440.0 Main gate 415.0 NaN 125.0 Name: amount, dtype: float64 outlet Hostel 430.0 Library 440.0 Main gate 415.0 Unknown 125.0 Name: amount, dtype: float64
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 missing_outlet.py
dropna=False gives the missing key a group of its own, shown as NaN, and the totals reconcile again. Filling the key first with “Unknown” gives it a readable name in this fixture, which has no genuine outlet of that name. In a different source, a real “Unknown” label would collide with that sentinel; preserve a missing-key flag or keep a separate null group. polars and DuckDB’s
GROUP BY keep a null group by default, so a pandas summary written without dropna=False can quietly disagree with
the same query in SQL. Columns of the category type follow a related rule in pandas 3: only the categories that occur
get a row by default; observed=False can include declared unused categories. Sort explicitly before comparing either engine with pandas.
The drawing shows the whole scene; its 3 steps are listed below.
Every step as text
- Default key policy. dropna=True excludes order 11 because its outlet key is missing. Its 125 is absent from the output.
- Keep a null group. dropna=False keeps a group for the missing outlet, restoring all 16 rows and the recorded total 1410.
- Display name policy. This fixture has no real outlet called Unknown. In a different source, a sentinel can merge genuine and missing keys; preserve their distinction.
Key idea
Checkpoint
Check: Missing-key policy
Rows, recorded amounts and known IDs count different things
Counting is where most summaries go wrong, because three functions count three different things:
import pandas as pd
orders = pd.read_csv("orders.csv")
counts = orders.groupby("outlet").agg(
rows=("order_id", "size"), # every row of the group
priced=("amount", "count"), # rows whose amount is not missing
customers=("customer", "nunique"), # distinct recorded customer IDs
)
print(counts) Output
rows priced customers outlet Hostel 5 4 3 Library 5 5 4 Main gate 5 5 3
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 three_counts.py
For Hostel, size says 5 orders and count of the amount says 4, because order 12 has no amount. nunique of customer gives 3 distinct recorded IDs: C01 and C05 each occur twice. That is an ID count; the rows alone do not prove how many different people those IDs represent.
| Function | What it counts | Missing values |
|---|---|---|
size |
Rows in the group | Counted like any row |
count |
Values that are not missing | Skipped |
nunique |
Different values | Skipped by default |
Pick the one that answers the question: orders per outlet is size, priced orders is count, and distinct known customer IDs is nunique.
The drawing shows the whole scene; its 3 steps are listed below.
Every step as text
- Rows of the group. size counts all five Hostel rows, including the order whose amount is missing.
- Observed amounts. count on amount uses the four non-missing values. It does not count every order.
- Known identifier values. nunique counts three distinct recorded IDs. The fixture's repeated orders do not add an identifier value.
Key idea
Checkpoint
Check: Three counting questions
Output keys and names make a summary usable
What you get back depends on how you ask. orders.groupby("outlet")["amount"].sum() selects one column and returns a
Series, with the outlets as its index, as in the missing-key example. .agg(...) with named aggregations returns a
DataFrame with one column per name, and with as_index=False the keys come back as columns too, numbered rows and
all. A list of keys, groupby(["outlet", "slot"]), makes one group per combination of values that occurs, and its keys
become a two-level index unless as_index=False turns them into two columns. For anything that goes on to a merge, a
chart or a file, ask for the flat shape: a DataFrame with plain columns. A dictionary of lists, such as
{"amount": ["sum", "mean"]}, also works, but it returns two levels of column names that need flattening first.
The drawing shows the whole scene; its 3 steps are listed below.
Every step as text
- Select one value column. Selecting amount and calling sum produces a Series indexed by outlet.
- Name scalar aggregates. Named aggregations build a DataFrame whose output column names are chosen by the caller.
- Keep keys as columns. as_index=False keeps outlet in a regular column. It changes representation, not missing-key or sorting policy.
Key idea
Checkpoint
Check: Output grain and names
Practise the reading core
In missing_outlet.py, add a genuine outlet named Unknown; compare null-preserving groups with sentinel-filled groups.
Practice that adapts to you
Practise this lesson
8 questions about 9 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
The same summary in pandas, polars and DuckDB, compared row for row Pro: opens with a Pro, Premium or Ultimate pass
A summary that someone rebuilds in polars or SQL has to give the same rows and the same numbers. This part runs one summary in all three engines, proves that they agree, and reproduces the three places where their defaults part ways: missing keys, unused categories and distinct counts.
In this part:
- Explicit policies make three engine results comparable
- Zero-row categories and unknown amounts need separate policies
- A null marker is a value policy, not a new person
3 diagrams · 3 runnable examples (Python) · about 8 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Reporting-ready summaries: two keys, flat names and totals that reconcile Pro: opens with a Pro, Premium or Ultimate pass
A summary is ready for a report when its columns have plain names and its numbers add up to the source. This part groups by two keys, flattens the columns of multi-function aggregations, and builds checks that catch a lost group before a reader does.
In this part:
- Source controls detect this lost key but do not prove row identity
- Named columns avoid ambiguous flattened labels
- Practise a monthly summary by city
2 diagrams · 6 runnable examples (CSV, Python) · about 6 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Interview questions
-
Fresher · Warm-up
After a group-by, what is the difference between size, count and nunique, and which do you use for orders, priced orders and customers?
size counts every row of a group, including rows with missing values. count counts a selected column's observed values, so an unrecorded amount lowers amount count without removing its order. nunique counts distinct recorded values and skips missing markers by default. Repeated occurrences of the same recorded ID count once. Use size for orders, amount count for orders with a recorded amount, and customer-ID nunique for distinct known identifiers. A missing or shared ID does not establish how many different people placed the orders; report unidentified orders separately. State that identity assumption before calling an ID count a people count. These functions answer different questions despite returning plausible integers.
-
Fresher to mid
Your group-by summary of revenue by city adds up to 4 % less than the finance team's total for the same month. Which causes do you check, in what order, and how do you make the numbers reconcile?
-
Mid to senior
You port a pandas group-by report to polars. The polars output has one extra row, its rows come out in a different order from run to run, and two outlets show one customer more than in pandas. Explain each difference and how you make the outputs identical.
-
Fresher to mid
A weekly report groups sales by a categorical column with 12 product categories. After an upgrade to pandas 3 it shows 9 rows: three categories with no sales this week have vanished. What changed, and how do you bring them back with the right values?
Key takeaways
groupby(...).agg(name=(column, function))gives one row per group with columns you name;as_index=Falsekeeps the keys as ordinary columns.groupbyleaves out rows with a missing key unless you passdropna=Falseor fill the key first.sizecounts rows,countcounts values that are not missing, andnuniquecounts different values.- Check every summary against its source: the group totals must add up to the table’s.
Terms in this lesson
- group key
- The fields whose values place source rows into groups under an explicit policy for equality, missing keys and unused categories.
- named aggregation
- A grouped output definition that explicitly names its result column and identifies the input column and aggregation function.
Corrections
- Clarified statistical inference and source-row or reporting contracts; existing explanations remain in their original reading parts.
- Clarified distinct recorded identifiers; numeric sample comparisons now reject missing results.
References
- Group by: split-apply-combine (pandas user guide) (pandas development team (NumFOCUS))
- pandas.DataFrame.groupby (pandas development team (NumFOCUS))
- Comparison with SQL (pandas documentation) (pandas development team (NumFOCUS))
- Aggregation (Polars user guide) (Polars)
- polars.Expr.n_unique (Polars)
- GROUP BY clause (DuckDB documentation) (DuckDB Foundation)
- SQL on pandas (DuckDB documentation) (DuckDB Foundation)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress