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

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        550

2️⃣ 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     2

3️⃣ 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    40

Mini-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 here

Common Errors (and the fix)

Pro Tips

📋 Quick Reference — group_by & summarise

TaskR Syntax
Group rowsgroup_by(region)
Summarise a groupsummarise(total = sum(x))
Count rowssummarise(n = n())
Quick tallycount(region)
Multiple groupsgroup_by(a, b)
Clear groupingungroup()

🎉 Lesson Complete!

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

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.