R Programming Quiz: Group By And Summarize
10 questions · exam conditions
0:00
Group By And SummarizeQuestion 1 of 10

Department totals are HR 20, IT 30, and Operations 50. The goal is to create one row per department with proportions 0.20, 0.30, and 0.50 of the grand total.

Which pipeline correctly calculates the desired department proportions?

work %>% group_by(dept) %>% summarize(total = sum(hours), pct = total / sum(total), .groups = "drop")
work %>% group_by(dept) %>% summarize(total = sum(hours), pct = sum(hours) / sum(sum(hours)))
work %>% group_by(dept) %>% summarize(total = sum(hours)) %>% mutate(pct = total / sum(total))
work %>% summarize(total = sum(hours)) %>% mutate(pct = total / sum(total))
← Back to quizzes

R Programming Quiz

R Programming Quiz: Group By And Summarize

Practice Group By And Summarize in R Programming with focused quiz questions that help you check what you know, review explanations, and build confidence with test-style prompts.

What this quiz covers

This quiz focuses on Group By And Summarize, giving you a quick way to practice the rules, question types, and explanations that matter most for R Programming.

How to use this quiz

Try each quiz question before looking at the correct answer. Use the explanations to review missed ideas, then come back to similar questions until the pattern feels familiar.

All questions

Question 1

Department totals are HR 20, IT 30, and Operations 50. The goal is to create one row per department with proportions 0.20, 0.30, and 0.50 of the grand total.

Which pipeline correctly calculates the desired department proportions?

  1. work %>% group_by(dept) %>% summarize(total = sum(hours), pct = total / sum(total), .groups = "drop")
  2. work %>% group_by(dept) %>% summarize(total = sum(hours), pct = sum(hours) / sum(sum(hours)))
  3. work %>% group_by(dept) %>% summarize(total = sum(hours)) %>% mutate(pct = total / sum(total)) (correct answer)
  4. work %>% summarize(total = sum(hours)) %>% mutate(pct = total / sum(total))
Explanation: When working with grouped data in R, the key question is always: at what scope does each calculation run? Inside summarize(), any function like sum() operates within each group. After summarize(), the grouping is dropped, so a subsequent mutate() sees the entire summarized data frame at once. Option C is correct because it separates the two concerns cleanly. First, group_by(dept) %>% summarize(total = sum(hours)) collapses the data into three rows — one per department — with totals 20, 30, and 50. Then mutate(pct = total / sum(total)) runs on that ungrouped three-row frame, where sum(total) correctly equals 100, yielding proportions 0.20, 0.30, and 0.50. Option A looks plausible but fails because pct = total / sum(total) is computed inside summarize(), still within each group. At that point, sum(total) for the HR group is just 20, so every department gets pct = 1.0 — not a proportion of the grand total. Option B doubles down on the same scoping mistake and adds unnecessary complexity with sum(sum(hours)). Nested aggregations inside a grouped summarize() don't escape the group boundary; the result is still wrong. Option D skips group_by() entirely, so summarize() collapses all rows into a single value — you lose the per-department breakdown completely. Study tip: Think of it as a two-stage recipe — first collapse (grouped summarize), then compare (ungrouped mutate). Whenever you need a proportion relative to a grand total, the division belongs in mutate(), not inside summarize().

Question 2

A tasks tibble has category X durations 2, NA, and 5, and category Y durations 4 and 6.

What is returned by tasks %>% group_by(category) %>% summarize(total = sum(duration), task_count = n())?

  1. X has total = 7, task_count = 2; Y has total = 10, task_count = 2.
  2. X has total = NA, task_count = 3; Y has total = 10, task_count = 2. (correct answer)
  3. X has total = NA, task_count = 2; Y has total = 10, task_count = 2.
  4. X has total = 7, task_count = 3; Y has total = 10, task_count = 2.
Explanation: When working with group_by() and summarize() in R, you need to carefully track two things independently: how sum() handles NA values, and how n() counts rows. n() counts every row in the group, including rows where values are NA. Category X has three rows — durations 2, NA, and 5 — so task_count = 3. Category Y has two rows (4 and 6), so task_count = 2. This makes B and D the only candidates worth considering. Now for sum(): by default, if any value in the input is NA, sum() returns NA (unless you specify na.rm = TRUE). Category X contains an NA, so sum(duration) returns NA for X — not 7. Category Y has no missing values, so sum(4, 6) = 10. This confirms answer B is correct: X gets total = NA, task_count = 3; Y gets total = 10, task_count = 2. Answer A is wrong on two counts — it ignores the NA propagation in sum() and undercounts X's rows. Answer C correctly identifies total = NA for X but miscounts task_count as 2, confusing n() with sum()'s NA-removal behavior. Answer D correctly counts task_count = 3 for X but wrongly assumes the NA is silently dropped in sum(), producing 7. A reliable rule of thumb: in R, n() never skips NA rows, but sum() fails loudly with NA unless you add na.rm = TRUE. Keep those two behaviors separate in your memory.

Question 3

A readings tibble has site A values 3, NA, and 9, and site B values 4, 8, and 100.

What does readings %>% filter(value < 50) %>% group_by(site) %>% summarize(avg = mean(value, na.rm = TRUE)) return?

  1. Site A has avg = 6, and site B has avg = 6. (correct answer)
  2. Site A has avg = NA, and site B has avg = 6.
  3. Site A has avg = 6, and site B has avg = 37.3333.
  4. Site A has avg = 4, and site B has avg = 56.
Explanation: When working through a multi-step dplyr pipeline, always simulate the data transformation step by step before evaluating the final result. Start with the raw data: Site A has values 3, NA, 9 and Site B has values 4, 8, 100. The first operation is filter(value < 50). Critically, NA < 50 evaluates to NA — not TRUE — so R drops the NA row from Site A entirely. After filtering, Site A retains 3 and 9, while Site B retains 4 and 8 (the value 100 is removed because 100 ≥ 50). Now group_by(site) splits these clean, filtered rows by site, and summarize(avg = mean(value, na.rm = TRUE)) computes the average. For Site A: 3+92=6\frac{3 + 9}{2} = 6. For Site B: 4+82=6\frac{4 + 8}{2} = 6. Both return 6, confirming A is correct. Choice B suggests Site A returns NA, which would only happen if the NA survived the filter step — but it doesn't, because NA comparisons always return NA (which filter treats as FALSE). Choice C correctly computes Site A's mean as 6, but mistakenly includes 100 in Site B's average (4+8+100337.33\frac{4 + 8 + 100}{3} \approx 37.33), forgetting that filter(value < 50) already removed it. Choice D appears to use entirely incorrect logic, possibly averaging values across sites or misreading which rows belong where. Your key takeaway: NA comparisons produce NA, not TRUE or FALSE — so filter() silently drops NA rows. Always trace which rows survive each pipeline step before computing summaries.

Question 4

A scores tibble contains scores 60, 70, 80, and NA. It is processed with scores %>% group_by(high = score >= 70) %>% summarize(count = n(), avg = mean(score, na.rm = TRUE)).

Which set of summarized groups is produced?

  1. FALSE: count 2, avg 60; TRUE: count 2, avg 75; no missing logical group.
  2. FALSE: count 1, avg 60; TRUE: count 2, avg 75; no group for the missing score.
  3. FALSE: count 1, avg 60; TRUE: count 2, avg 75; NA: count 1, avg NaN. (correct answer)
  4. FALSE: count 1, avg 60; TRUE: count 3, avg 75; no separate missing group.
Explanation: When you use group_by() with a logical expression like score >= 70, R evaluates that condition for every row and assigns each observation to a group: TRUE, FALSE, or — critically — NA if the value itself is NA. This is the key concept being tested: NA comparisons produce NA, not FALSE. Walking through the data: 60 >= 70 is FALSE, 70 >= 70 and 80 >= 70 are both TRUE, and NA >= 70 evaluates to NA. So dplyr creates three groups. The FALSE group contains only 60 (count = 1, avg = 60), the TRUE group contains 70 and 80 (count = 2, avg = 75), and the NA group contains the missing score (count = 1). Because mean(score, na.rm = TRUE) on a group whose only value is NA removes that value and averages nothing, the result is NaN — not NA. This confirms C as correct. A is wrong because it assigns a count of 2 to the FALSE group, which would require two scores below 70 — there is only one (60). It also incorrectly omits the NA group entirely. B gets the FALSE and TRUE groups right but wrongly assumes the NA observation simply disappears instead of forming its own group. D incorrectly lumps the NA observation into the TRUE group, inflating its count to 3. As a study tip: whenever you group by a derived logical condition, always ask yourself what happens to NA inputs — they propagate into a separate NA group rather than silently dropping or defaulting to FALSE.

Question 5

An orders tibble contains these rows, written as (region, product, units): (East, A, 4), (East, A, 6), (East, B, 5), (West, A, 8), (West, B, 3), and (West, B, 9).

What rows are returned by orders %>% group_by(region, product) %>% summarize(total = sum(units), .groups = "drop") %>% filter(total >= 10)?

  1. Two rows: (East, A, 10) and (West, B, 12). (correct answer)
  2. Two rows: (East, B, 15) and (West, A, 20).
  3. Three rows: (East, A, 10), (East, B, 5), and (West, B, 12).
  4. Four rows, one for each observed region-product combination.
Explanation: When you see a dplyr pipeline like this, trace each operation step by step before evaluating any answer choice. First, group_by(region, product) creates four groups from the data: (East, A), (East, B), (West, A), and (West, B). Then summarize(total = sum(units)) collapses each group into a single row by summing its units values: East/A gets 4 + 6 = 10, East/B gets 5, West/A gets 8, and West/B gets 3 + 9 = 12. The .groups = "drop" argument simply removes the grouping structure afterward. Finally, filter(total >= 10) keeps only rows where the total meets the threshold — that's East/A (10 ✓) and West/B (12 ✓), producing exactly two rows. That confirms A is correct. Choice B describes rows (East, B, 15) and (West, A, 20) — those totals are completely fabricated and don't match any correct calculation. Choice C includes East/B with a total of 5, which fails the total >= 10 filter and would be dropped; including it means whoever chose C forgot to apply the final filter() step. Choice D says all four region-product combinations survive, but again ignores that the filter removes East/B (total = 5) and West/A (total = 8), neither of which meets the threshold. A reliable strategy here: when a pipeline has multiple steps, mentally materialize the intermediate table after each verb before moving to the next. Students most often go wrong by skipping the filter() or misreading which groups pass it.

Question 6

Store totals are East-A 10, East-B 20, West-A 5, and West-B 25. Consider starting with sales %>% group_by(region, store) %>% summarize(store_total = sum(amount)).

Which continuation produces exactly one row containing the largest store total across all regions, 25?

  1. Append %>% summarize(best = max(store_total)) without changing the remaining groups.
  2. Append %>% ungroup() %>% summarize(best = max(store_total)) after the first summary. (correct answer)
  3. Append %>% group_by(region) %>% summarize(best = max(store_total)) after the first summary.
  4. Append %>% group_by(store) %>% summarize(best = max(store_total)) after the first summary.
Explanation: Whenever you see a chained summarize() in dplyr, the critical question is: what grouping structure remains after each step? After group_by(region, store) %>% summarize(store_total = sum(amount)), dplyr automatically peels off the innermost grouping level (store), leaving the result still grouped by region. This is dplyr's default behavior — and it's the source of every trap in this question. Option B is correct because ungroup() completely removes the residual region grouping before the second summarize(). With no groups left, max(store_total) scans all four store totals (10, 20, 5, 25) globally and returns a single row with best = 25 — exactly what the question asks for. Option A fails because the result is still grouped by region after the first summarize. Calling summarize(best = max(store_total)) without ungrouping computes a maximum within each region — producing two rows: East max = 20, West max = 25 — not one global result. Option C explicitly re-groups by region, which has the same effect as A: you get one max per region (two rows), not one global winner. Option D re-groups by store (A and B), then summarizes — producing two rows, one max per store letter, not one global maximum across all four stores. Study tip: Always ask yourself "what grouping survives into the next step?" after a summarize(). When you need a truly global aggregation after a grouped summary, ungroup() is your required bridge — without it, phantom groups silently partition your results.

Question 7

A purchases tibble contains three purchases for customer A with amounts 10, 20, and 30, and one purchase for customer B with amount 100.

What result is produced by purchases %>% group_by(customer) %>% summarize(customer_avg = mean(amount)) %>% summarize(overall = mean(customer_avg))?

  1. One row with overall = 40, the mean of all four purchases.
  2. One row with overall = 60, the mean of the two customer means. (correct answer)
  3. Two rows with customer averages 20 for A and 100 for B.
  4. One row with overall = 50, the mean of the two customer totals.
Explanation: When you chain multiple summarize() calls in R's dplyr, each one collapses the data further — and critically, the second summarize() operates on the output of the first, not the original data. That distinction is exactly what this question tests. After group_by(customer) %>% summarize(customer_avg = mean(amount)), you get a two-row tibble: customer A has a mean of (10+20+30)/3=20(10 + 20 + 30)/3 = 20 and customer B has a mean of 100/1=100100/1 = 100. The second summarize(overall = mean(customer_avg)) then takes the mean of those two valuesmean(c(20, 100)) — giving 20+1002=60\frac{20 + 100}{2} = 60. So B is correct: one row with overall = 60. A is wrong because it computes the unweighted mean of all four raw purchases — (10+20+30+100)/4=40(10 + 20 + 30 + 100)/4 = 40 — but the second summarize() never sees the original rows; it only sees the two customer averages. C is wrong because it describes the intermediate output after the first summarize(), ignoring that the second summarize() collapses those two rows into one. D invents a calculation based on totals (30 + 100 = 130, half of which is 65, not even 50), which doesn't correspond to any step in the actual pipeline. A handy rule: treat each summarize() as a fresh dataset with only the columns it produced. If you see two consecutive summarize() calls, trace through them step by step rather than jumping straight to the original data.

Question 8

A tibble df has a group column with values (G, G, G, H, H, H) and an id column with values (1, 1, NA, 2, NA, NA), giving six rows total — three for group G and three for group H.

What does df %>% group_by(group) %>% summarize(rows = n(), unique_ids = n_distinct(id, na.rm = TRUE)) report?

  1. G has rows = 2, unique_ids = 2; H has rows = 1, unique_ids = 2.
  2. G has rows = 2, unique_ids = 1; H has rows = 1, unique_ids = 1.
  3. G has rows = 3, unique_ids = 2; H has rows = 3, unique_ids = 2.
  4. G has rows = 3, unique_ids = 1; H has rows = 3, unique_ids = 1. (correct answer)
Explanation: When working with dplyr's summarize(), it helps to think carefully about what each function actually counts — and whether NA values are included or excluded. n() counts all rows in each group, regardless of NA values. Since both group G and group H each have exactly three rows, both return rows = 3. This immediately rules out A and B, which incorrectly report rows = 2 for G and rows = 1 for H — those numbers have no basis in the data. For n_distinct(id, na.rm = TRUE), the na.rm = TRUE argument tells R to ignore NA values before counting unique entries. In group G, the id values are (1, 1, NA) — stripping the NA leaves (1, 1), which has only 1 distinct value. In group H, the values are (2, NA, NA) — stripping the NAs leaves just (2), again 1 distinct value. So both groups report unique_ids = 1, confirming D as the correct answer. Choice C is the most tempting trap: it gets rows = 3 right but incorrectly reports unique_ids = 2 for both groups, which is what you'd get if you forgot na.rm = TRUE and mistakenly counted NA as its own distinct value (since by default, n_distinct() does include NA as a unique entry). Choice A and B compound two errors at once — wrong row counts and wrong distinct counts. The key study tip: always distinguish between n() (counts rows, never skips NA) and n_distinct(..., na.rm = TRUE) (counts unique non-NA values). These two functions handle missing data very differently.

Question 9

A sales tibble has (2024, North, 10), (2024, South, 20), (2025, North, 30), and (2025, South, 40), where each tuple contains (year, region, sales).

What does sales %>% group_by(year) %>% group_by(region) %>% summarize(total = sum(sales)) return?

  1. One row with total = 100, because the second grouping removes all groups.
  2. Two rows: 2024 has total = 30, and 2025 has total = 70.
  3. Four rows, one for every observed combination of year and region.
  4. Two rows: North has total = 40, and South has total = 60. (correct answer)
Explanation: When you chain multiple group_by() calls in dplyr, the key behavior to understand is that each new group_by() completely replaces the previous grouping — it does not add to it. So the grouping active when summarize() runs is the one set by the last group_by() call, which here is group_by(region). With only region as the active grouping, summarize(total = sum(sales)) aggregates across all years within each region. North contributes 10 (2024) + 30 (2025) = 40, and South contributes 20 (2024) + 40 (2025) = 60. That gives you two rows: North = 40 and South = 60, confirming D is correct. A is wrong because the second group_by() doesn't remove all grouping — it simply replaces the first one with region. You still get grouped output, not a grand total. B describes what you'd get if summarize() were grouping by year, which was the first group_by() — a common trap if you assume groupings stack. C would be the result if you had used group_by(year, region) in a single call, which creates a four-combination grouping. D is correct precisely because only the final group_by(region) matters. A useful rule of thumb: in dplyr, group_by() is not additive by default — each call resets the grouping entirely. If you want to group by multiple variables, list them all inside a single group_by(year, region). Watch for chained group_by() calls on exam questions; they're a classic trap testing whether you know this replacement behavior.

Question 10

After grouping transaction data by team and quarter, the group totals are A-Q1 30, A-Q2 10, B-Q1 20, and B-Q2 20.

The data are processed with transactions %>% group_by(team, quarter) %>% summarize(q_total = sum(amount)) %>% mutate(share = q_total / sum(q_total)). Assuming the usual default grouping behavior of summarize(), which shares are produced?

  1. A-Q1 0.75, A-Q2 0.25, B-Q1 0.50, and B-Q2 0.50. (correct answer)
  2. A-Q1 0.375, A-Q2 0.125, B-Q1 0.25, and B-Q2 0.25.
  3. A-Q1 1, A-Q2 1, B-Q1 1, and B-Q2 1.
  4. A-Q1 0.60, A-Q2 0.20, B-Q1 0.40, and B-Q2 0.40.
Explanation: When you pipe a grouped data frame through summarize(), dplyr's default behavior is to drop the last grouping variable, leaving the result grouped by all remaining variables. Here, after grouping by team and quarter and summarizing, the result stays grouped by team alone. That means the subsequent mutate(share = q_total / sum(q_total)) computes sum(q_total) within each team, not across all rows. For team A, the total is 30+10=4030 + 10 = 40, so A-Q1 gets 30/40=0.7530/40 = 0.75 and A-Q2 gets 10/40=0.2510/40 = 0.25. For team B, the total is 20+20=4020 + 20 = 40, giving B-Q1 and B-Q2 each 20/40=0.5020/40 = 0.50. That confirms A is correct. Choice B divides each value by the grand total of all four groups (30+10+20+20=8030+10+20+20 = 80), which is what you'd get if no grouping remained after summarize() — a common misconception, but not the default behavior. Choice C would result if sum(q_total) operated row-by-row (i.e., each row divided by itself), which isn't how sum() works inside mutate(). Choice D uses 30+20=5030+20 = 50 as the denominator — essentially grouping by quarter instead of team — which reflects a misread of which grouping variable was retained. A useful rule of thumb: after summarize() with n grouping variables, you're left with n − 1 groups. Always ask yourself, "what is sum() summing over?" — the answer depends entirely on the active grouping at that step in the pipeline.