Nesting and Unnesting Data Frames
Use nest() and unnest() to work with list-columns in tidy workflows.
Nesting and Unnesting Data Frames is a free R Academy lesson on CoddyKit — lesson 4 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the R Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
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 elementnest() — 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 datanest() 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 groupgroup_by() + nest()— the standard pattern to nest by groupmap(nested$data, fn)— apply any function to each nested data frameunnest(cols = data)— expand list-column of data frames back to flat formatunnest_longer(col)— expand a list of vectors/scalars to rowsunnest_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)Frequently asked questions
Is the “Nesting and Unnesting Data Frames” lesson free?
Yes — the full text of “Nesting and Unnesting Data Frames” is free to read here on the web, and the R Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the R Academy course, upgrade to CoddyKit PRO.
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; this is — lesson 4 of 4, so you can start here or from the beginning and move at your own pace.
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
- Wide to Long with pivot_longer()
- Long to Wide with pivot_wider()
- separate() and unite() for String Columns
- Nesting and Unnesting Data Frames