How much did each product bring in over the year? The question is not about any row; it is about the rows that belong together. summarize collapses rows into answers: each new column is an aggregation, and by names the columns that say which rows go together.
run(' sales then summarize [revenue] as total([revenue]), [orders] as row_count() by [product]')
product
revenue
orders
Doohickey
400
3
Gadget
720
4
Sprocket
240
2
Widget
700
6
sales |>summarize(revenue =total(revenue), orders =row_count(), by = product)
product
revenue
orders
Doohickey
400
3
Gadget
720
4
Sprocket
240
2
Widget
700
6
(sales>> summarize(revenue = total(col.revenue), orders = row_count(), by = col.product))
product
revenue
orders
Doohickey
400
3
Gadget
720
4
Sprocket
240
2
Widget
700
6
sales then summarize [revenue] as total([revenue]), [orders] as row_count() by [product]
Fifteen rows in, four out, one per product. This is the verb that turns a record of what happened into a statement about it, and it is the single most common reason anyone opens a table.
7.1 Several groups, or none
Grouping by several columns is a list, and every combination that appears in the data gets a row:
run(' sales then summarize [biggest] as largest([revenue]), [average] as average([cost])')
biggest
average
300
80.13333
sales |>summarize(biggest =largest(revenue), average =average(cost))
biggest
average
300
80.13333
(sales>> summarize(biggest = largest(col.revenue), average = average(col.cost)))
biggest
average
300
80.13333
7.2 Grouping is an argument, not a state
In this grammar, grouping is a word inside the step it affects. It is not a mode you switch the table into. Nothing needs ungrouping afterwards, no verb behaves differently because of what an earlier verb declared, and a pipeline read a year from now says everything it does, where it does it. dplyr readers will recognize the failure this avoids: a group_by() three steps up silently changing what summarise() and mutate() mean, and the ritual ungroup() that follows every recipe. Here there is nothing to forget, because there is nothing being remembered.
7.3 One spelling where you have learned five
This one operation is where the world’s tools disagree the loudest. Group and total, five ways:
dplyr df |> group_by(g) |> summarise(total = sum(x))
pandas df.groupby('g')['x'].sum().reset_index()
polars df.group_by('g').agg(pl.col('x').sum().alias('total'))
data.table df[, .(total = sum(x)), by = g]
SQL SELECT g, SUM(x) AS total FROM df GROUP BY g
Every one of these describes the same act: split the rows by a key, apply a collapse, combine the answers. The act is aggregation over a partition, older than the relational model itself (Codd, 1970), and the R world later named the pattern split-apply-combine (Wickham, 2011). The ideas have been stable for fifty years. The spelling is what churns, and the spelling is what you forget. god’s claim is that summarize ... by ..., read aloud, is the spelling you will still have when the others have faded, because it is the one that says what the operation does in the words you would use to describe it.
7.4 The eleven ways to collapse a group
total and average are the two you will use most. There are eleven aggregations altogether, and the list is closed on purpose: a small shelf you can hold whole beats a long one you have to search.
total, average and median combine numbers into one, and standard_deviation says how spread out they are. It is the sample deviation, which is what R, pandas, polars and both SQL engines all mean by their own bare word. The answer does not change with the engine. smallest and largest take the ends. first and last take a value by position, which is why they want the rows in a known order. row_count counts the rows and takes no column at all. unique_count counts how many different values a column holds. join_rows joins a group’s text values into one cell, with a separator you choose; tidying text teaches it.
run(' sales then summarize [cheapest] as smallest([cost]), [dearest] as largest([cost]), [middle] as median([revenue]), [spread] as standard_deviation([revenue]), [kinds] as unique_count([product]) by [region]')
sales >> sort(descending(col.revenue)) >> summarize( best = first(col.product), worst = last(col.product), by = col.region)
region
best
worst
East
Widget
Gadget
North
Gadget
Widget
West
Gadget
Widget
7.5 The same sentence at scale
Everything above ran on fifteen rows you can check by hand, which is how a verb should be met. Here is the same verb meeting a table nobody checks by hand: gapminder, the cast’s large table, holding a half century of countries and years.
run(' gapminder then keep where ([year] is 2007) then summarize [life] as average([life]), [countries] as row_count() by [continent] then sort [life] descending')
continent
life
countries
Oceania
80.71950
2
Europe
77.64860
30
Americas
73.60812
25
Asia
70.72848
33
Africa
54.80604
52
gapminder |>keep(year ==2007) |>summarize(life =average(life), countries =row_count(),by = continent) |>sort(descending(life))
continent
life
countries
Oceania
80.71950
2
Europe
77.64860
30
Americas
73.60812
25
Asia
70.72848
33
Africa
54.80604
52
(gapminder>> keep(col.year ==2007)>> summarize(life = average(col.life), countries = row_count(), by = col.continent)>> sort(descending(col.life)))
continent
life
countries
Oceania
80.7195
2
Europe
77.6486
30
Americas
73.60812
25
Asia
70.72848
33
Africa
54.80604
52
gapminder then keep where [year] is 2007 then summarize [life] as average([life]), [countries] as row_count() by [continent] then sort [life] descending
Gapminder’s 1,704 rows went in; five came out, one per continent, with the count beside each average, so you can see how many countries went into each number. The sentence is the same length it was on fifteen rows, and that is the point of the demonstration: nothing about the grammar knows or cares how big the table is. Somewhere else shows the same indifference reaching a warehouse.
7.6 Where the other columns went
summarize keeps the columns you grouped by and the ones you named, and takes away everything else. That is its whole behavior, and it is the thing readers are most often surprised by. You can look at it before you run anything.
sales |>summarize(total =total(revenue), by = region) |>show_steps()
show_steps(sales >> summarize(total = total(col.revenue), by = col.region))
One line makes total, and the same line takes date, product, quantity, revenue and cost away. When a later step asks for a column that is not there, this is usually where it went. What it does to the table reads the drawing in full.
7.7 What travels with it
by is the same word, with the same meaning, that grouped take in the last chapter and will group windows in giving each row a place: these rows belong together.
as names each answer, name first, exactly as it does in add.
The eleven aggregations reappear inside add ... by, where they broadcast a group’s answer back onto its rows instead of collapsing them; the chapter on the small words shows the share-of-group idiom this buys.
7.8 What it refuses
Every value in a summarize must span its group. A bare column does not, and the grammar will not guess which of a group’s values you meant:
sales |>summarize(revenue = revenue, by = region) |>collect()
Error:
!
illegal: `summarize` returns one row for each group, so `[revenue]` has to be a value that spans the group. Wrap it: `total(...)`, `average(...)`, `first(...)`, or count the rows with `row_count()`
|
2 | then summarize [revenue] as [revenue] by [region]
| ^^^^^^^
try: collect(sales >> summarize(revenue = col.revenue, by = col.region))except GodError as refusal:print(refusal)
illegal: `summarize` returns one row for each group, so `[revenue]` has to be a value that spans the group. Wrap it: `total(...)`, `average(...)`, `first(...)`, or count the rows with `row_count()`
|
2 | then summarize [revenue] as [revenue] by [region]
| ^^^^^^^
SQL answers this same mistake with an error about grouping clauses that has confused beginners for decades; some tools answer it by silently taking the first value, which is worse. This message names the actual problem, one row per group against many values per group, and lists the words that resolve it. Refusing with a direction is the grammar’s answer to a whole class of half-remembered rules.
Codd, E. F. (1970). A relational model of data for large shared data banks. Communications of the ACM, 13(6), 377–387. https://doi.org/10.1145/362384.362685
Wickham, H. (2011). The split-apply-combine strategy for data analysis. Journal of Statistical Software, 40(1), 1–29. https://doi.org/10.18637/jss.v040.i01