rguides

tidyr::unnest()

Overview

unnest() expands a list-column containing data frames into regular rows and columns. It is the inverse of nest(). Where nest() groups columns into data frames stored in a single cell, unnest() pulls those data frames back out so each row in the nested column becomes its own row in the output.

unnest() is most useful after you have applied a transformation to nested data, like fitting a model per group with mutate() and map(), and now want to work with the results in a flat format again.

Signature

unnest(data, cols, ..., keep_empty = FALSE, ptype = NULL,
       names_sep = NULL, names_repair = "check_unique")

Parameters

ParameterTypeDefaultDescription
datatibble / data frame,Input data.
colsRequiredList-column(s) to unnest. Use bare column names or tidyselect helpers.
keep_emptylogicalFALSEIf FALSE, rows with empty list elements (NULL, empty tibble) are dropped. If TRUE, they produce a row of NA values.
ptypelistNULLOptional column prototype(s) to coerce the unested columns to.
names_sepcharacterNULLIf non-NULL, outer and inner names are pasted together with this separator.
names_repair"check_unique"How to repair output column names.

Basic usage

Unnesting a single list-column

library(tidyr)

df <- tibble(
  x = 1:3,
  y = list(
    NULL,
    tibble(a = 1, b = 2),
    tibble(a = 1:3, b = 3:1, c = 4)
  )
)

df %>% unnest(y)
# # A tibble: 4 x 4
#       x     a     b     c
#   <int> <dbl> <dbl> <dbl>
# 1     2     1     2    NA
# 2     3     1     3     4
# 3     3     2     2     4
# 4     3     3     1     4

Row 1 (x = 1, y = NULL) is dropped because keep_empty = FALSE by default. Row 2 expands to one row. Row 3 expands to three rows.

Unnesting expands each list-column element back into regular rows, which restores the full row count and lets you apply standard dplyr verbs like filter() and mutate() to the previously nested data. This additional context makes the transformation pattern clearer and easier to adapt to your own data analysis needs.

Preserving empty elements

df %>%
  unnest(y, keep_empty = TRUE)
# # A tibble: 5 x 4
#       x     a     b     c
#   <int> <dbl> <dbl> <dbl>
# 1     1    NA    NA    NA
# 2     2     1     2    NA
# 3     3     1     3     4
# 4     3     2     2     4
# 5     3     3     1     4

After unnesting, the grouping structure is preserved in the expanded rows, so each row retains the identifier columns from the outer data frame alongside the expanded contents of the list-column. Use this approach when you need to prepare data for further analysis in a tidy workflow.

Unnesting multiple columns

When you unnest two columns at once, values are recycled to match:

df2 <- tibble(
  id = c(1, 2),
  x = list(tibble(a = 1:2), tibble(a = 3)),
  y = list(tibble(p = 10, q = 20), tibble(p = 30))
)

df2 %>% unnest(c(x, y))
# # A tibble: 3 x 3
#      id     a     p     q
#   <dbl> <int> <dbl> <dbl>
# 1     1     1    10    20
# 2     1     2    10    20
# 3     2     3    30    NA

Unnesting expands each list-column element back into regular rows, which restores the full row count and lets you apply standard dplyr verbs like filter() and mutate() to the previously nested data. This pattern is common in real-world data analysis pipelines.

Working with nested models

A common pattern: nest, fit models, extract results, then unnest to analyse coefficients:

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

df <- tibble(
  group = c("A", "A", "B", "B", "B"),
  x     = c(1, 2, 3, 4, 5),
  y     = c(10, 20, 30, 40, 50)
)

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

nested <- nested %>%
  mutate(coef = map(model, coef))

nested %>%
  select(group, coef) %>%
  unnest(coef)
# # A tibble: 4 x 3
#   group name           estimate
#   <chr> <chr>             <dbl>
# 1 A     (Intercept)           0
# 2 A     y                     0.1
# 3 B     (Intercept)            0
# 4 B     y                     0.1

After unnesting, the grouping structure is preserved in the expanded rows, so each row retains the identifier columns from the outer data frame alongside the expanded contents of the list-column. Applying this technique correctly saves time in data preparation.

Using names_sep

When nested column names might conflict with existing columns, names_sep prefixes the inner names:

df3 <- tibble(
  id = 1,
  data = list(tibble(x = 1:2, y = 3:4))
)

df3 %>% unnest(data, names_sep = "_")
# Columns become data_x, data_y instead of x, y

Common use cases

Unnesting expands each list-column element back into regular rows, which restores the full row count and lets you apply standard dplyr verbs like filter() and mutate() to the previously nested data. This additional context makes the transformation pattern clearer and easier to adapt to your own data analysis needs.

Expanding time series per entity

library(lubridate)

measurements <- tibble(
  station = c("A", "A", "B"),
  reading = list(
    tibble(date = as.Date("2021-01-01"), temp = 5.1),
    tibble(date = as.Date("2021-01-02"), temp = 5.8),
    tibble(date = as.Date("2021-01-01"), temp = 4.2)
  )
)

measurements %>% unnest(reading)
# # A tibble: 3 x 3
#   station date       temp
#   <chr>   <date>    <dbl>
# 1 A       2021-01-01  5.1
# 2 A       2021-01-02  5.8
# 3 B       2021-01-01  4.2

After unnesting, the grouping structure is preserved in the expanded rows, so each row retains the identifier columns from the outer data frame alongside the expanded contents of the list-column. Knowing when to use each variant improves your data cleaning efficiency.

Flattening survey responses

survey <- tibble(
  respondent = 1:2,
  responses = list(
    tibble(q1 = "agree", q2 = "disagree"),
    tibble(q1 = "neutral", q2 = "agree")
  )
)

survey %>% unnest(responses)
# # A tibble: 2 x 3
#   respondent q1     q2
#        <int> <chr>  <chr>
# 1          1 agree  disagree
# 2          2 neutral agree

Unnesting expands each list-column element back into regular rows, which restores the full row count and lets you apply standard dplyr verbs like filter() and mutate() to the previously nested data. Use this approach when you need to prepare data for further analysis in a tidy workflow.

Gotchas

Row ordering after unnesting. The order of rows in the output depends on the order of elements in the list-column, not the original row order. Use arrange() to restore a specific order: This additional context makes the transformation pattern clearer and easier to adapt to your own data analysis needs.

nested %>%
  unnest(data) %>%
  arrange(group, x)

Missing elements drop rows by default. If a row has an empty list element (NULL, empty tibble), that row disappears from the output unless you pass keep_empty = TRUE.

Deprecated arguments. The old unnest(x, y, z) syntax is deprecated. Use unnest(c(x, y, z)). Similarly, .id, .sep, and .drop are deprecated, use mutate() before unnest().

Multiple list-columns with mismatched lengths. Shorter vectors are recycled. If lengths are not divisible, you get a warning and some cells become NA.

unnest() is the inverse of nest(). It expands a list-column where each element is a data frame, appending those rows to the parent table. When list-column elements have different numbers of rows, unnest() repeats the parent row’s values to match. Use unnest_wider() for list-columns where each element is a named list of scalars.

See also