将列表列拆分为多个列

28

我有一个数据框,其中最后一列是一个列表列。以下是它的样子:

Col1 | Col2 | ListCol
--------------------------
 na  |  na  | [obj1, obj2]
 na  |  na  | [obj1, obj2]
 na  |  na  | [obj1, obj2]

我想要的是

Col1 | Col2 | Col3  | Col4
--------------------------
 na  |  na  | obj1  | obj2
 na  |  na  | obj1  | obj2
 na  |  na  | obj1  | obj2

我知道所有的列表都有相同数量的元素。

编辑:

ListCol中的每个元素都是一个包含两个元素的列表。


1
这很大程度上取决于ListCol的结构。如果它包含每行的数据框或命名列表,只需使用tidyr::unnest即可。如果它是其他结构,则可能需要先重新排列。为了获得更好的答案,请编辑并调用dput在您的示例数据上的结果,以便我们可以复制粘贴完全相同的结构。 - alistaire
2
你好。我尝试过使用unnest,但它所做的是将对象分离到不同的行而不是列中。ListCol的每一行都是一个列表。 - Santi
将其向侧面展开的最简单方法是使每个列表元素成为一个1行数据框,例如使用 df$ListCol <- lapply(df$ListCol, function(x) as.data.frame(t(x)))(如果您喜欢,则使用dplyr和purrr),然后调用 unnest - alistaire
这是alisatire针对类似问题提出的解决方案:https://dev59.com/l6rka4cB1Zd3GeqPhrO1,使用了invoke_map和tibble。以下是其他几个解决方案:https://dev59.com/V1UL5IYBdhLWcg3w27Nq。 - Arthur Yip
5个回答

24

目前,tidyverse的答案将是:

library(dplyr)
library(tidyr)
data %>% unnest_wider(ListCol)

2
如果您需要保留嵌套列的名称,例如 data %>% unnest_wider(ListCol, names_sep="_") 将会得到 ListCol_Col3,这时您可以添加 names_sep 参数,这在同时展开多个列时非常方便。 - Mario Reutter

11

这里是一种方法,使用unnesttidyr::spread...

library(dplyr)
library(tidyr)

#example df
df <- tibble(a=c(1, 2, 3), b=list(c(2, 3), c(4, 5), c(6, 7)))

df %>% unnest(b) %>% 
       group_by(a) %>% 
       mutate(col=seq_along(a)) %>% #add a column indicator
       spread(key=col, value=b)

      a   `1`   `2`
  <dbl> <dbl> <dbl>
1    1.    2.    3.
2    2.    4.    5.
3    3.    6.    7.

6
在你的例子中,你只是在执行cbind(df[1],do.call(rbind,df$b)),甚至可以使用cbind(df[1],t(data.frame(df$b))) - Onyambu
@Onyambu,你不想写一个完整的回答来回应这篇帖子吗?它对我很有帮助,也可能会对其他人有所帮助,但我第一次错过了它。 - JMarcelino
Spread的帮助文档说它已经被pivot_wider所代替,后者同样有效。 - ARobertson

5

两种优秀答案的比较

在本帖中,有两个很好的一行代码建议:

(1) cbind(df[1], t(data.frame(df$b)))

这是@Onyambu使用base R提出的解决方法。要得到这个答案,需要知道一个dataframe是一个列表,并需要一些创造性思维。

(2) df %>% unnest_wider(b)

这是@iago使用tidyverse提出的解决方法。您需要额外的软件包并且需要了解所有的nest动词,但可以认为它更易读。

现在让我们比较性能

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

N <- 100
df <- tibble(a = 1:N, b = map2(1:N, 1:N, c))

tidy_foo <- function() suppressMessages(df %>% unnest_wider(b))
base_foo <- function() cbind(df[1],t(data.frame(df$b))) %>% as_tibble # To be fair
  
microbenchmark(tidy_foo(), base_foo())

Unit: milliseconds
       expr      min        lq      mean    median       uq      max neval
 tidy_foo() 102.4388 108.27655 111.99571 109.39410 113.1377 194.2122   100
 base_foo()   4.5048   4.71365   5.41841   4.92275   5.2519  13.1042   100

哎呀!

base R解决方案快了20倍。


3
使用我的真实数据和问题,我看到的差异较小。我的数据框有10万行,65列,我正在展开一对变量。tidyr解决方案需要12.5秒,基本R解决方案需要11秒,因此基本R解决方案快1.14倍。用户可能希望在自己的数据上进行测试。 - Sam Firke

1

这里有一个使用 data.tablebase::unlist 的选项。

library(data.table)

DT <- data.table(a = list(1, 2, 3),
                                 b = list(list(1, 2),
                                              list(2, 1),
                                              list(1, 1)))

for (i in 1:nrow(DT)) {
  set(
    DT,
    i = i,
    j = c('b1', 'b2'),
    value = unlist(DT[i][['b']], recursive = FALSE)
  )
}
DT

这需要在每一行上使用for循环... 不是理想的方式,也非常反对data.table。我想知道是否有一种方法可以避免在第一次创建列表列...

1

@Alec data.table 提供了 tstrsplit 函数,用于将一列分割成多列。

DT = data.table(x=c("A/B", "A", "B"), y=1:3)
DT[]

#     x y
#1: A/B 1
#2:   A 2
#3:   B 3

DT[, c("c1") := tstrsplit(x, "/", fixed=TRUE, keep=1L)][] # keep only first

#     x y c1
#1: A/B 1  A
#2:   A 2  A
#3:   B 3  B

DT[, c("c1", "c2") := tstrsplit(x, "/", fixed=TRUE)][]

#     x y c1   c2
#1: A/B 1  A    B
#2:   A 2  A <NA>
#3:   B 3  B <NA>

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接