tidyr nesting() in R: Preserve Column Pairs in Combinations

The nesting() function in tidyr preserves existing column pairings inside expand() or complete(), instead of generating their full cross product.

⚡ Quick Answer
df |> expand(nesting(year, quarter), product)
df |> complete(nesting(user, plan), month)
df |> expand(year, quarter)            # without nesting: full cross product
expand(year = 2020:2024, quarter = 1:4) # all 5 * 4 = 20 combos

Need explanation? Read on for examples and pitfalls.

📊 When to use nesting()?
STARTpreserve existing (year, quarter) pairs inside cross-productnesting(year, quarter)full cross-product including nonexistent pairsdon't use nestinghierarchical data with parent-childnesting(parent, child)

What nesting() does in one sentence

nesting(...) is a tidyselect helper used inside expand() or complete() that keeps the named columns' EXISTING pairings together (treats them as one composite column) instead of cross-producting them.

Syntax

nesting(...). Used inside expand() or complete(). Not called standalone.

Run live
Run live, no install needed. Every R block on this page runs in your browser. Click Run, edit the code, re-run instantly. No setup.
RPreserve year-quarter pairs
library(tidyr) library(dplyr) sales <- tibble( year = c(2024, 2024, 2025), quarter = c(1, 2, 1), product = c("X","Y","X") ) # Without nesting (cross product): sales |> expand(year, quarter, product) #> 2 years * 2 quarters * 2 products = 8 rows #> But (2025, q2) doesn't exist in data - it's still generated! # With nesting (preserve year-quarter pairs): sales |> expand(nesting(year, quarter), product) #> 3 (year, quarter) pairs * 2 products = 6 rows #> (2025, q2) is NOT generated because it didn't exist

  
Tip
Use nesting() when some column combinations should not be cross-producted. E.g., year-quarter where (2025, q2) might not yet exist; you want to preserve actual time periods only.

Five common patterns

1. Year-quarter as a unit

RDon't cross years with all quarters
sales |> expand(nesting(year, quarter), product)

  

2. Hierarchical: parent-child

REach customer has a tier; cross with month
df |> expand(nesting(customer_id, tier), month)

  

3. complete() with nesting

RComplete missing month entries within existing pairs
df |> complete(nesting(user_id, plan), month = 1:12)

  

4. Multi-level nesting

RCountry-state preserved
df |> expand(nesting(country, state), year)

  

5. Without nesting comparison

RCross product can produce nonsense rows
df |> expand(country, state, year) #> Cross-products country with EVERY state, even unrelated ones (e.g., US x Bavaria)

  
Key Insight
nesting() is the cure for "I don't want a Cartesian product of columns A and B". When A and B have a natural pairing (like year-quarter, country-state), nesting keeps that intact while cross-producting them with OTHER columns.

nesting() vs cross-product (default)

Behavior Without nesting With nesting
Year x Quarter All combos Only existing pairs
Output rows n_year * n_quarter n_unique_pairs
Generates impossible rows Yes No

When to use nesting:

  • Hierarchical structures (country-state, year-quarter, customer-plan).
  • Time-stamped pairs that mustn't be artificially split.
  • When you only want combinations that EXIST in the data.

A practical workflow

Use nesting in time-series with multiple subjects to avoid generating cross-subject combinations.

RInteractive R
patients_visits |> complete( nesting(patient_id, treatment), visit = 1:10, fill = list(measurement = NA) )

  

Each (patient, treatment) pair gets visits 1-10; doesn't cross-product patient with all treatments.

Common pitfalls

Pitfall 1: confusing nesting with cross-product. Default behavior in expand/complete is FULL cross-product. nesting prevents that for the named columns.

Pitfall 2: trying to use nesting outside expand/complete. It is a tidyselect helper, not a standalone function.

Warning
nesting() only preserves pairs that EXIST in the data. It doesn't generate any new pairs, only cross-products with the OTHER expand/complete arguments.

Try it yourself

Try it: Generate all (cyl, gear) x am combinations using nesting to preserve cyl-gear pairs from mtcars. Save to ex_nested.

RYour turn: nest cyl-gear, cross with am
ex_nested <- mtcars |> # your code here # With nesting: only existing (cyl, gear) pairs * 2 am values nrow(ex_nested) #> Expected: rows = unique(cyl, gear) pairs * 2

  
Click to reveal solution
RSolution
ex_nested <- mtcars |> expand(nesting(cyl, gear), am) # 8 unique (cyl, gear) pairs * 2 am = 16 rows nrow(ex_nested) #> [1] 16

  

Explanation: Without nesting, you'd get 332 = 18 rows (including nonexistent (4, 5) etc). With nesting, only the actual cyl-gear pairs.

After mastering nesting, look at:

  • expand(): combinations from existing data
  • complete(): expand + merge with original
  • expand_grid() / crossing(): from vectors
  • tidyr::nest() / unnest(): list-column workflows
  • dplyr::group_by(): alternative for some uses

FAQ

What does nesting do in tidyr?

nesting(...) is a helper used inside expand/complete that preserves existing column pairings. Instead of cross-producting the named columns, it keeps them as observed pairs.

When should I use nesting?

When two or more columns have a natural pairing (year-quarter, country-state) and cross-producting them would create impossible rows.

Does nesting work outside expand/complete?

No. It is a tidyselect helper specifically for those functions.

What is the difference between nesting and group_by?

group_by changes how subsequent verbs operate (per-group). nesting controls combination generation in expand/complete only. Different scopes.

Can I use nesting with three or more columns?

Yes. nesting(country, state, city) preserves all three together. Use any number of columns.

Prefer to learn by doing? Every track on the learning roadmap opens with a free section of interactive lessons, where you write the R yourself and the page tells you when it is wrong.