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.