Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Beginner
  • 30 minutes
  • Examples run with Python 3.14.8, pandas 3.0.6, polars 1.44.2 and duckdb 1.5.6
  • By MySmartCoPilot
Free core, Pro depth: 12 of 30 minutes free
Free: One scalar aggregation reduces each observed group · An explicit missing-key policy restores the 125 · Rows, recorded amounts and known IDs count different things · 2 more sections. With a pass: 2 in-depth parts and 3 interview answers, about 18 minutes.

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

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.

orders.csv: the 16 orders CSV · orders.csv
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
groupby("outlet") splits 16 orders into three outlet groups, sums each group, and combines the sums into one row per outlet.Split, apply, combine16 source rowsrecorded sum 1410Order 11outlet missingHostel: 5 rows3,6,8,12,15Library: 5 rows1,2,5,9,14Main gate: 54,7,10,13,16Keep null key1 row: 125430 / 440 / 415sum observedHostel amount4 values; 1 gap3 named rowssum 12854 rows with nullsum 1410

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.

  1. 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.
  2. Order 11 has no outlet. pandas leaves it out of every group by default; dropna=False gives it a group of its own.
  3. 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.
  4. 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.

Orders, revenue and average basket per outlet Python · by_outlet.py
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.

Interactive · stage by stage Sixteen orders reduced to three named outlet rows Follow by_outlet.py and compare the recorded amount count with the row count.
Sixteen orders reduced to three named outlet rows, step 3 of 3. Showing Source rows, Observed outlet groups, Named output columns. In focus: Named output columns.Source rows16 rows; recorded sum: 1410Observed outlet groupsHostel:5; Library:5; Gate:5Named output columnsHostel sum 430; mean 107.5

The drawing shows the whole scene; its 3 steps are listed below.

Every step as text
  1. Source rows. The source has 16 rows. One outlet is missing, and one amount is missing; the recorded non-missing amounts sum to 1410.
  2. Observed outlet groups. Default grouping uses the 15 rows with known outlets. Hostel has 5 rows but only 4 recorded amounts.
  3. 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

Define each output’s denominator and grain before treating it as an outlet metric.

Checkpoint

Check: Named aggregation

  1. One question Hostel's recorded amount total is 430 and 4 amounts are present among 5 rows. What is its mean amount?

    Type a number.

    Show the answer: One question

    Answer: 107.5 amount units

    The mean uses 430/4, not 430/5.

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:

A missing outlet: left out, kept, or named Python · missing_outlet.py
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.

Interactive · stage by stage Order eleven under two key policies Use missing_outlet.py and compare the source total with both summaries.
Order eleven under two key policies, step 3 of 3. Showing Default key policy, Keep a null group, Display name policy. In focus: Display name policy.Default key policy15 rows; summary total: 1285Keep a null group16 rows; total: 1410Display name policyUnknown:125; check label

The drawing shows the whole scene; its 3 steps are listed below.

Every step as text
  1. Default key policy. dropna=True excludes order 11 because its outlet key is missing. Its 125 is absent from the output.
  2. Keep a null group. dropna=False keeps a group for the missing outlet, restoring all 16 rows and the recorded total 1410.
  3. 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

A missing key and a missing amount are different source conditions and need explicit handling.

Checkpoint

Check: Missing-key policy

  1. One question The source records 1410 and the missing-outlet order contributes 125. What total remains with the default key policy?

    Type a number.

    Show the answer: One question

    Answer: 1285 amount units

    1410−125=1285; the missing-key group is excluded.

Rows, recorded amounts and known IDs count different things

Counting is where most summaries go wrong, because three functions count three different things:

Rows, priced orders and customers per outlet Python · three_counts.py
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.

Interactive · stage by stage Hostel's five rows have four prices and three known IDs Read three_counts.py and identify the column each count measures.
Hostel's five rows have four prices and three known IDs, step 3 of 3. Showing Rows of the group, Observed amounts, Known identifier values. In focus: Known identifier values.Rows of the groupsize: 5Observed amountsamount count: 4Known identifier valuescustomer nunique: 3

The drawing shows the whole scene; its 3 steps are listed below.

Every step as text
  1. Rows of the group. size counts all five Hostel rows, including the order whose amount is missing.
  2. Observed amounts. count on amount uses the four non-missing values. It does not count every order.
  3. Known identifier values. nunique counts three distinct recorded IDs. The fixture's repeated orders do not add an identifier value.

Key idea

Name the counted object: rows, observed values or distinct known identifiers.

Checkpoint

Check: Three counting questions

  1. One question Which count includes a row even when its amount is missing?

    Choose one answer.

    Show the answer: One question

    Answer: size

    Choose a count by its measured object.

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.

Interactive · stage by stage A grouped result keeps keys as columns or as its index Compare by_outlet.py with the Series result in missing_outlet.py.
A grouped result keeps keys as columns or as its index, step 3 of 3. Showing Select one value column, Name scalar aggregates, Keep keys as columns. In focus: Keep keys as columns.Select one value columnSeries: key index and sumsName scalar aggregatesDataFrame: one named columnKeep keys as columnsas_index=False: plain keys

The drawing shows the whole scene; its 3 steps are listed below.

Every step as text
  1. Select one value column. Selecting amount and calling sum produces a Series indexed by outlet.
  2. Name scalar aggregates. Named aggregations build a DataFrame whose output column names are chosen by the caller.
  3. Keep keys as columns. as_index=False keeps outlet in a regular column. It changes representation, not missing-key or sorting policy.

Key idea

Output shape describes what one row and one column mean; it does not repair an excluded source row.

Checkpoint

Check: Output grain and names

  1. One question A merge expects an outlet column. Which aggregation representation provides it directly?

    Choose one answer.

    Show the answer: One question

    Answer: Named aggregation with as_index=False

    Representation and row inclusion are separate policies.

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.

  1. Question 1 of 8 One of the orders has no outlet. What happens to its amount in orders.groupby("outlet")["amount"].sum()?

    Choose one answer.

    Show the answer: Question 1 of 8

    Answer: It is left out of the result, without a warning

    groupby drops rows whose key is missing by default (dropna=True). Pass dropna=False to give the missing key a group of its own, or fill the key first, for example with "Unknown", so the totals still add up to the table's.

  2. Question 2 of 8 An outlet has 5 orders from 3 different customers, and one of the orders has no amount. Which named aggregation returns 4 for it?

    Choose one answer.

    Show the answer: Question 2 of 8

    Answer: priced=("amount", "count")

    count uses the four recorded amounts and skips the missing one. size counts all five source rows, while nunique on the customer column counts three distinct known IDs. A people count needs the stated identity assumptions.

  3. Question 3 of 8 What does as_index=False change in orders.groupby("outlet", as_index=False).agg(revenue=("amount", "sum"))?

    Choose one answer.

    Show the answer: Question 3 of 8

    Answer: The outlet stays an ordinary column, and the rows are numbered 0, 1, 2 …

    With the default as_index=True the group keys become the index of the result. as_index=False keeps them as columns, which is what merge, to_csv and most reports expect; reset_index() after the aggregation does the same.

  4. Question 4 of 8 What does one row of the shown scalar named outlet aggregation describe?

    Choose one answer.

    Show the answer: Question 4 of 8

    Answer: One observed outlet group

    State the key and scalar functions to determine the output grain.

  5. Question 5 of 8 Library's five recorded amounts total 440. What is their average?

    Type a number.

    Show the answer: Question 5 of 8

    Answer: 88 amount units

    The five recorded amounts total 440, so their mean is 440 divided by 5, or 88.

  6. Question 6 of 8 A default summary has 1285 while the recorded source total is 1410. What amount is omitted?

    Type a number.

    Show the answer: Question 6 of 8

    Answer: 125 amount units

    1410−1285=125; the fixture traces it to order 11's missing outlet.

  7. Question 7 of 8 Which column label correctly describes nunique on customer with its default missing-ID policy?

    Choose one answer.

    Show the answer: Question 7 of 8

    Answer: Distinct known customer IDs

    Name the observed object rather than inferring unseen people.

  8. Question 8 of 8 as_index=False is set, but order 11's outlet is missing. Which additional policy keeps its group?

    Choose one answer.

    Show the answer: Question 8 of 8

    Answer: dropna=False

    The key's column location and its missing-value inclusion need separate choices.

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.

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.

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

  1. 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.

  2. 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?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  3. 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.

    Model answer, Pro. First step and follow-ups

  4. 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?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • groupby(...).agg(name=(column, function)) gives one row per group with columns you name; as_index=False keeps the keys as ordinary columns.
  • groupby leaves out rows with a missing key unless you pass dropna=False or fill the key first.
  • size counts rows, count counts values that are not missing, and nunique counts 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

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.