group_by & summarise
Reviewed & published by Brayan K
group_by() and summarise() are dplyr's tools for grouped statistics — the "split-apply-combine" pattern that answers questions like average sales per region or counts per category in a couple of readable lines.
Part of the free R course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
By the end of this lesson you'll group data by one or more columns, compute several summaries at once, count rows with n(), and sort the results into a tidy report.
What You'll Learn in This Lesson
- The split-apply-combine idea behind grouping
- How group_by() tags rows by category
- How summarise() collapses each group to one row
- How to compute several summaries at once
- How to count rows with n()
- How to group by multiple columns
1️⃣ group_by() + summarise()
group_by() tags rows by a column; the following summarise() computes its result once per group. Together they turn raw rows into a per-category summary.
library(dplyr)
sales <- data.frame(
region = c("N", "S", "N", "S", "N"),
amount = c(100, 250, 80, 300, 120)
)
# group_by() tags rows by a category; summarise() collapses each group.
sales |>
group_by(region) |>
summarise(total = sum(amount))
# region total
# 1 N 300
# 2 S 5502️⃣ Multiple Summaries and n()
Inside one summarise() you can compute many statistics — sum, mean, min, max — and use n() to count the rows in each group.
library(dplyr)
sales <- data.frame(
region = c("N", "S", "N", "S", "N"),
amount = c(100, 250, 80, 300, 120)
)
# Compute several summaries at once. n() counts rows per group.
sales |>
group_by(region) |>
summarise(
total = sum(amount),
avg = mean(amount),
n = n()
)
# region total avg n
# 1 N 300 100 3
# 2 S 550 275 23️⃣ Grouping by Several Columns
Group by two or more columns for a cross-tab style breakdown — for instance sales by region and channel. Use .groups = "drop" to clear grouping afterwards.
library(dplyr)
orders <- data.frame(
region = c("N", "N", "S", "S", "N"),
channel = c("web", "shop", "web", "shop", "web"),
amount = c(100, 50, 200, 90, 120)
)
# Group by TWO columns for a cross-tab style summary.
orders |>
group_by(region, channel) |>
summarise(total = sum(amount), .groups = "drop") |>
arrange(desc(total))
# region channel total
# 1 S web 200
# 2 N web 220 <- N web = 100 + 120
# ... (sorted by total)Your turn. Fill in the ___ blank, run it, and compare with the expected output.
library(dplyr)
# 🎯 YOUR TURN — replace each ___ then run it.
scores <- data.frame(
team = c("A", "B", "A", "B", "A"),
points = c(10, 20, 30, 40, 50)
)
result <- scores |>
group_by(team) |>
summarise(
total = sum(points),
best = ___ # 👉 replace ___ with max(points)
)
print(result)
# ✅ Expected output:
# # A tibble: 2 × 3
# team total best
# <chr> <dbl> <dbl>
# 1 A 90 50
# 2 B 60 40Mini-Challenge: Category Sales Report
Write the full pipeline from the outline, run it, and check it against the example output. mutate then group then summarise then arrange is the classic reporting flow.
# 🎯 MINI-CHALLENGE: sales report by category.
#
# Start from:
# sales <- data.frame(
# cat = c("food","tech","food","tech","food"),
# units = c(5, 2, 8, 4, 3),
# price = c(2, 100, 2, 100, 2)
# )
#
# In one pipeline:
# 1. mutate() a revenue column = units * price
# 2. group_by(cat)
# 3. summarise(): total_rev = sum(revenue), avg_units = mean(units), n = n()
# 4. arrange() by total_rev, highest first
#
# ✅ Example output (a tibble): one row per cat with the three summaries.
# your code hereCommon Errors (and the fix)
- Forgetting group_by() — summarise() alone collapses the WHOLE table to one row. Group first to get per-category results.
- summarise returns NA — a group has missing values. Add na.rm = TRUE inside sum()/mean().
- Still grouped later — operations downstream act per group unexpectedly. End with ungroup() or .groups = "drop".
- Using n() outside dplyr — n() only works inside dplyr verbs like summarise() or mutate().
Pro Tips
- 💡 count() shortcut: df |> count(region) is a quick group-and-tally in one verb.
- 💡 Summaries feed plots: a grouped summary is exactly the tidy input ggplot2 wants for a bar chart.
- 💡 across() for many columns: summarise(across(where(is.numeric), mean)) averages every numeric column at once.
📋 Quick Reference — group_by & summarise
| Task | R Syntax |
|---|---|
| Group rows | group_by(region) |
| Summarise a group | summarise(total = sum(x)) |
| Count rows | summarise(n = n()) |
| Quick tally | count(region) |
| Multiple groups | group_by(a, b) |
| Clear grouping | ungroup() |
🎉 Lesson Complete!
- ✅ group_by() tags rows for per-group operations
- ✅ summarise() collapses each group to one row
- ✅ Compute many summaries in one summarise()
- ✅ n() counts rows per group
- ✅ Group by several columns for cross-tabs
Practice quiz
What does group_by() do on its own?
- Collapses the table to one row
- Sorts the data
- Tags rows so the NEXT operation runs per group
- Deletes duplicate rows
Answer: Tags rows so the NEXT operation runs per group. group_by() tags rows by category; the following verb then runs within each group.
What does summarise() do to each group?
- Adds a column to every row
- Collapses each group to a single summary row
- Keeps all rows unchanged
- Removes the grouping column
Answer: Collapses each group to a single summary row. summarise() reduces each group to one row of summary values.
Which function counts the number of rows in the current group?
- n()
- count()
- nrow()
- length()
Answer: n(). n() returns the number of rows in the current group inside dplyr verbs.
Which pattern does group_by() + summarise() implement?
- Map-reduce-only
- Filter-sort-join
- Read-eval-print
- Split-apply-combine
Answer: Split-apply-combine. Grouping then summarising is the classic split-apply-combine workflow.
How do summarise() and mutate() differ?
- summarise() collapses to one row per group; mutate() keeps every row
- They are identical
- mutate() collapses rows; summarise() keeps them
- Both delete columns
Answer: summarise() collapses to one row per group; mutate() keeps every row. mutate() keeps the table height; summarise() aggregates each group to one row.
What does summarise(.groups = "drop") do?
- Drops the data frame
- Removes all grouping after summarising
- Deletes the summary columns
- Drops missing values
Answer: Removes all grouping after summarising. .groups = "drop" clears the grouping so later steps act on the whole result.
To get sales per region AND channel, you should:
- Use filter() twice
- group_by(region, channel)
- Only group_by(region)
- Skip group_by entirely
Answer: group_by(region, channel). Group by several columns for a cross-tab style summary.
Why might summarise(total = sum(x)) return NA for a group?
- x is a character
- The group is too large
- The group contains missing values (NA)
- summarise never returns NA
Answer: The group contains missing values (NA). Missing values propagate; add na.rm = TRUE inside sum()/mean() to ignore them.
Which is a quick group-and-tally shortcut in dplyr?
- tally_all(region)
- group(region)
- rollup(region)
- count(region)
Answer: count(region). count(region) groups and counts in a single verb.
Where does n() work?
- Inside dplyr verbs like summarise() or mutate()
- Anywhere in base R
- Only inside print()
- Only on vectors
Answer: Inside dplyr verbs like summarise() or mutate(). n() only works inside dplyr verbs, not in plain base R calls.
Continue this course
- Previous: ggplot2 Fundamentals
- Next: Joining Data — Combine multiple tables with inner, left, and other joins
- Quick reference: R cheat sheet › dplyr
Frequently asked questions
What does group_by() actually do?
On its own, group_by() doesn't change the data's appearance — it tags the rows with a grouping so that the NEXT operation runs separately within each group. It's the 'split' step of split-apply-combine. When you follow it with summarise(), R computes the summary once per group rather than once for the whole table. You can group by one column or several, and you can remove the grouping later with ungroup().
How is summarise() different from mutate()?
mutate() keeps every row and adds or changes columns, so the table stays the same height. summarise() collapses each group down to a single row of summary values, so the result has one row per group (or one row total if there's no grouping). Use mutate() for row-level calculations and summarise() for aggregations like sums, means, and counts. They're often used together: mutate a helper column, then group and summarise it.
What is the n() function for?
n() returns the number of rows in the current group — it's how you count records per category inside summarise(). For example summarise(count = n()) gives the size of each group. There's also n_distinct(column) to count unique values. Counting is one of the most common summaries, so n() shows up constantly: 'how many orders per region', 'how many users per day', and so on.
Why do I get a message about '.groups' when grouping by multiple columns?
When you group by several columns and summarise, dplyr drops the last grouping level and tells you so, in case it affects later steps. To control this explicitly and silence the message, pass .groups = "drop" to remove all grouping after summarising (usually what you want), or .groups = "keep" to retain it. It's informational, not an error — but setting .groups makes your intent clear and your output predictable.