R Academy · Lesson

Nesting and Unnesting Data Frames

Use nest() and unnest() to work with list-columns in tidy workflows.

Lesson 4 of 413 steps

Nesting and Unnesting Data Frames is a free R Academy lesson on CoddyKit. This is lesson 4 of 4. Any 3 lessons of this learning path are free to read in full — after that, CoddyKit PRO unlocks every lesson, plus hands-on practice with a built-in code editor and a 24/7 AI tutor. It is part of the R Academy learning path, and your progress syncs across the web and the CoddyKit app. The R Academy course includes 4 lessons in total.

List-Columns and Nested Data

A list-column is a column in a data frame where each cell contains an R object (a vector, data frame, model, or list). This makes it possible to store structured sub-data alongside other columns, enabling powerful group-wise workflows.

library(tidyr)
library(dplyr)

# A simple data frame with a list-column
df <- data.frame(
  group = c('A','B','C'),
  n = c(3, 2, 4)
)
df$values <- list(c(1,2,3), c(4,5), c(6,7,8,9))

print(df)
print(df$values[[1]])  # Access the first list element

nest() — Creating Nested Data Frames

nest(df, data = c(col1, col2)) groups rows by the non-nested columns and packs the specified columns into a list-column of data frames. One row per group, each containing a mini data frame.

library(tidyr)
library(dplyr)

df <- data.frame(
  region = c('East','East','East','West','West'),
  month = c(1,2,3,1,2),
  sales = c(100,120,110,200,190)
)

nested <- df %>%
  nest(data = c(month, sales))

print(nested)
print(nested$data[[1]])  # East region's data

nest() with group_by()

The most common pattern: use group_by() then nest() to create one row per group, with all remaining columns packed into a data list-column. Equivalent to nest(.by = group_col) in newer tidyr versions.

library(tidyr)
library(dplyr)

df <- data.frame(
  category = c('A','A','A','B','B','C','C','C','C'),
  x = c(1,2,3,4,5,6,7,8,9),
  y = c(2,4,3,8,7,5,6,9,8)
)

nested <- df %>%
  group_by(category) %>%
  nest()

print(nested)
cat('Rows per category B:', nrow(nested$data[[2]]))

map() with Nested Data Frames

Once data is nested, use purrr::map() to apply a function to each mini data frame. The function receives one data frame at a time, and the results become a new list-column — often a fitted model.

library(tidyr)
library(dplyr)
library(purrr)

df <- data.frame(
  group = c('A','A','A','B','B','B'),
  x = c(1,2,3,1,2,3),
  y = c(2.1,4.0,5.9,3.2,5.8,9.1)
)

models <- df %>%
  group_by(group) %>%
  nest() %>%
  mutate(model = map(data, ~lm(y ~ x, data = .x)))

print(models)

Extracting Model Results with map()

After fitting models per group, use map() with functions like broom::tidy() or coef() to extract results from each model. The output is another list-column that can later be unnested.

library(tidyr)
library(dplyr)
library(purrr)

df <- data.frame(
  group = c('A','A','A','B','B','B'),
  x = c(1,2,3,1,2,3),
  y = c(2.1,4.0,5.9,3.2,5.8,9.1)
)

df %>%
  group_by(group) %>%
  nest() %>%
  mutate(
    model = map(data, ~lm(y ~ x, data = .x)),
    r_squared = map_dbl(model, ~summary(.x)$r.squared),
    intercept = map_dbl(model, ~coef(.x)[1]),
    slope = map_dbl(model, ~coef(.x)[2])
  ) %>%
  select(group, r_squared, intercept, slope)

unnest() — Expanding List-Columns

unnest(df, cols = data) is the inverse of nest(). It expands the list-column back into regular columns, repeating the id columns as needed. Use cols to specify which list-column to unnest.

library(tidyr)
library(dplyr)

nested <- data.frame(
  region = c('East','West')
)
nested$data <- list(
  data.frame(month=1:3, sales=c(100,120,110)),
  data.frame(month=1:2, sales=c(200,190))
)

unnested <- unnest(nested, cols = data)
print(unnested)

unnest_longer() — Expand a List to Rows

unnest_longer(df, col) expands a list-column so each element of the list becomes a row. Unlike unnest() which expects data frames, unnest_longer() works with lists of vectors or scalars.

library(tidyr)

df <- data.frame(
  person = c('Alice','Bob','Carol')
)
df$skills <- list(
  c('R','Python','SQL'),
  c('Excel','Tableau'),
  c('R','Julia','Stan','BUGS')
)

unnest_longer(df, skills)

unnest_wider() — Expand a List to Columns

unnest_wider(df, col) expands each list element into a set of new columns. The list must have named elements — the names become column names. Useful for nested JSON-like structures.

library(tidyr)

df <- data.frame(id = 1:3)
df$info <- list(
  list(name='Alice', age=30, city='NYC'),
  list(name='Bob', age=25, city='LA'),
  list(name='Carol', age=35, city='Chicago')
)

unnest_wider(df, info)

Full Nested Workflow Pattern

The complete nested data frame workflow: group_by() + nest() → apply functions with map() → extract results → unnest() to get back a flat data frame. This pattern enables running any analysis per group.

library(tidyr)
library(dplyr)
library(purrr)

df <- data.frame(
  region = c('East','East','East','West','West','West'),
  month = c(1,2,3,1,2,3),
  revenue = c(100,120,115,200,195,210)
)

df %>%
  group_by(region) %>%
  nest() %>%
  mutate(
    mean_rev = map_dbl(data, ~mean(.x$revenue)),
    trend = map_dbl(data, ~coef(lm(revenue~month, data=.x))[2])
  ) %>%
  select(region, mean_rev, trend)

Nesting Multiple Columns

You can be selective about which columns go into the nested data frame. Specify them explicitly in nest(data = c(...)). Columns not specified stay as regular columns in the outer data frame, alongside the list-column.

library(tidyr)
library(dplyr)

df <- data.frame(
  region = c('East','East','West','West'),
  year = c(2023,2024,2023,2024),
  q1 = c(100,110,200,210),
  q2 = c(105,115,195,215),
  target = c(105,112,198,212)
)

# Only nest quarterly data, keep target as outer column
nested <- df %>%
  group_by(region, year) %>%
  nest(quarters = c(q1, q2))

print(nested)

When to Use Nested Data Frames

Nested data frames excel when you need to: fit the same model across groups, run per-group simulations, apply complex transformations to subsets, or work with JSON-like hierarchical data. They keep group-specific data organized alongside group-level metadata.

library(tidyr)
library(dplyr)
library(purrr)

# Bootstrap confidence interval per group
df <- data.frame(
  group = c('A','A','A','A','B','B','B','B'),
  value = c(10,12,11,13,20,22,19,21)
)

set.seed(42)
df %>%
  group_by(group) %>%
  nest() %>%
  mutate(
    boot_means = map(data, function(d) {
      replicate(100, mean(sample(d$value, replace=TRUE)))
    }),
    ci_low = map_dbl(boot_means, ~quantile(.x, 0.025)),
    ci_high = map_dbl(boot_means, ~quantile(.x, 0.975))
  ) %>%
  select(group, ci_low, ci_high)

Quick Check

What is the correct way to expand a list-column of data frames back into a flat data frame?

Recap: Nesting and Unnesting

Key takeaways for nested data frame workflows:

  • nest(data = c(cols)) — packs columns into a list-column of data frames, one per group
  • group_by() + nest() — the standard pattern to nest by group
  • map(nested$data, fn) — apply any function to each nested data frame
  • unnest(cols = data) — expand list-column of data frames back to flat format
  • unnest_longer(col) — expand a list of vectors/scalars to rows
  • unnest_wider(col) — expand a named list to columns
  • Ideal for per-group modeling, bootstrapping, and JSON data processing
library(tidyr)
library(dplyr)
library(purrr)

data.frame(
  team = c('A','A','B','B'),
  x = c(1,2,1,2),
  y = c(2,4,3,6)
) %>%
  group_by(team) %>%
  nest() %>%
  mutate(
    model = map(data, ~lm(y ~ x, data = .x)),
    slope = map_dbl(model, ~coef(.x)['x'])
  ) %>%
  select(team, slope)
Free to start

Learn R with an AI tutor — free

Write and run real code in your browser, get instant help from a 24/7 AI tutor, and pick up where you left off on the web or in the app.

Courses
43
Lessons
159

Frequently Asked Questions

Is the “Nesting and Unnesting Data Frames” lesson free?

Yes — any 3 lessons of the R Academy learning path, including “Nesting and Unnesting Data Frames”, are free to read in full here on the web. After that, CoddyKit PRO unlocks every lesson, plus interactive practice with a built-in code editor and a 24/7 AI tutor. The R Academy course includes 4 lessons in total.

What will I learn in “Nesting and Unnesting Data Frames”?

Use nest() and unnest() to work with list-columns in tidy workflows. You practise R Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start R Academy?

No prior experience is required. R Academy on CoddyKit is structured for beginners through advanced learners, so you can start here or from the beginning and move at your own pace. This is lesson 4 of 4.

How long does the “Nesting and Unnesting Data Frames” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this R Academy lesson?

Yes. Every R Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. Wide to Long with pivot_longer()
  2. Long to Wide with pivot_wider()
  3. separate() and unite() for String Columns
  4. Nesting and Unnesting Data Frames
← Back to R Academy