按行绑定未命名向量的未命名向量列表的 Tidyverse 方法 - do.call(rbind,x) 等效项

Tidyverse approach to binding unnamed list of unnamed vectors by row - do.call(rbind,x) equivalent

提问人:Ian Campbell 提问时间:5/5/2020 最后编辑:user438383Ian Campbell 更新时间:9/28/2022 访问量:1947

问:

我经常发现一些问题,人们以某种方式最终得到了一个未命名的字符向量列表,他们想将它们逐行绑定到 .下面是一个示例:data.frame

library(magrittr)
data <- cbind(LETTERS[1:3],1:3,4:6,7:9,c(12,15,18)) %>%
  split(1:3) %>% unname
data
#[[1]]
#[1] "A"  "1"  "4"  "7"  "12"
#
#[[2]]
#[1] "B"  "2"  "5"  "8"  "15"
#
#[[3]]
#[1] "C"  "3"  "6"  "9"  "18"

一种典型的方法是使用基数 R。do.call

do.call(rbind, data) %>% as.data.frame
#  V1 V2 V3 V4 V5
#1  A  1  4  7 12
#2  B  2  5  8 15
#3  C  3  6  9 18

也许一种效率较低的方法是使用基础 R。Reduce

Reduce(rbind,data, init = NULL) %>% as.data.frame
#  V1 V2 V3 V4 V5
#1  A  1  4  7 12
#2  B  2  5  8 15
#3  C  3  6  9 18

但是,当我们考虑更现代的包(例如 或)时,可能会立即想到的一些方法不起作用,因为向量未命名或不是列表。dplyrdata.table

library(dplyr)
bind_rows(data)
#Error: Argument 1 must have names
library(data.table)
rbindlist(data)
#Error in rbindlist(data) : 
#  Item 1 of input is not a data.frame, data.table or list

一种方法可能是在向量上。set_names

library(purrr)
map_df(data, ~set_names(.x, seq_along(.x)))
# A tibble: 3 x 5
#  `1`   `2`   `3`   `4`   `5`  
#  <chr> <chr> <chr> <chr> <chr>
#1 A     1     4     7     12   
#2 B     2     5     8     15   
#3 C     3     6     9     18  

但是,这似乎比需要的步骤更多。

因此,我的问题是,将未命名字符向量的未命名列表绑定到逐行的有效方法是什么?tidyversedata.tabledata.frame

r dplyr data.table 咕噜

评论

2赞 alexis_laz 5/6/2020
顺便说一句,效率再高不过了,因为构造分配内存并复制一次数据,而构造重复分配新内存并重新复制所有以前“编辑”的元素。Reduce(rbind, do.call(rbind, do.callReducerbind
0赞 Ian Campbell 5/6/2020
你说得很对。我没想到性能会这么糟糕,在 100,000 行上慢了 6,000 倍。我编辑了这个问题,称其为“效率较低的方法”。

答:

15赞 tmfmnk 5/5/2020 #1

不完全确定效率,但使用和可能的紧凑选项是:purrrtibble

map_dfc(purrr::transpose(data), ~ unlist(tibble(.)))

  V1    V2    V3    V4    V5   
  <chr> <chr> <chr> <chr> <chr>
1 A     1     4     7     12   
2 B     2     5     8     15   
3 C     3     6     9     18  

评论

1赞 tmfmnk 5/9/2020
@Adam更新了帖子,谢谢:)不过,我不记得有哪个函数比同一事物的函数更快或一样快。tidyversedata.table
11赞 markus 5/6/2020 #2

编辑

使用 @sindri_baldur 的方法:https://stackoverflow.com/a/61660119/8583393


一种与 类似的方式,类似于@tmfmnk显示的data.table

library(data.table)
as.data.table(transpose(data))
#   V1 V2 V3 V4 V5
#1:  A  1  4  7 12
#2:  B  2  5  8 15
#3:  C  3  6  9 18
5赞 akrun 5/6/2020 #3

一个选项unnest_wider

library(tibble)
library(tidyr)
library(stringr)
tibble(col = data) %>%
    unnest_wider(c(col), names_repair = ~ str_c('value', seq_along(.)))
# A tibble: 3 x 5
#  value1 value2 value3 value4 value5
#  <chr>  <chr>  <chr>  <chr>  <chr> 
#1 A      1      4      7      12    
#2 B      2      5      8      15    
#3 C      3      6      9      18    
6赞 jangorecki 5/6/2020 #4

我的方法是将这些列表条目转换为预期类型

rbindlist(lapply(data, as.list))
#       V1     V2     V3     V4     V5
#   <char> <char> <char> <char> <char>
#1:      A      1      4      7     12
#2:      B      2      5      8     15
#3:      C      3      6      9     18

如果您希望将数据类型从字符向量调整为适当的类型,那么也可以在这里提供帮助。每行调用 First,每列调用 second。lapplylapplylapply

rbindlist(lapply(data, as.list))[, lapply(.SD, type.convert)]
       V1    V2    V3    V4    V5
   <fctr> <int> <int> <int> <int>
1:      A     1     4     7    12
2:      B     2     5     8    15
3:      C     3     6     9    18
3赞 lotus 5/6/2020 #5

以下是 tmfmnk 建议的方法的细微变化,该方法用于将向量转换为单行提示。还需要使用以下参数:as_tibble_row().name_repair

library(purrr)
library(tibble)

map_df(data, as_tibble_row, .name_repair = ~paste0("value", seq(.x)))

# A tibble: 3 x 5
  value1 value2 value3 value4 value5
  <chr>  <chr>  <chr>  <chr>  <chr> 
1 A      1      4      7      12    
2 B      2      5      8      15    
3 C      3      6      9      18
9赞 user10917479 5/7/2020 #6

这似乎相当紧凑。我相信这就是 和 因此在 中 的权力,所以应该相当有效。bind_rows()dplyrmap_df()purrr

library(vctrs)

vec_rbind(!!!data)

这给出了一个 data.frame。

  ...1 ...2 ...3 ...4 ...5
1    A    1    4    7   12
2    B    2    5    8   15
3    C    3    6    9   18

一些基准

似乎方法内部是一个严重的瓶颈。我采取了一些相当简单的选项,这些选项似乎也是其他帖子中运行最快的(感谢 H 1 和 sindri_baldur)。.name_repairtidyverse

microbenchmark(vctrs = vec_rbind(!!!data),
               dt = rbindlist(lapply(data, as.list)),
               map = map_df(data, as_tibble_row, .name_repair = "unique"),
               base = as.data.frame(do.call(rbind, data)))

benchmark 1

但是,如果你先命名向量(但不一定是列表元素),你会得到一个不同的故事。

data2 <- modify(data, ~set_names(.x, seq(.x)))

microbenchmark(vctrs = vec_rbind(!!!data2),
               dt = rbindlist(lapply(data2, as.list)),
               map = map_df(data2, as_tibble_row),
               base = as.data.frame(do.call(rbind, data2)))

benchmark 2

事实上,您可以将时间命名为向量到解决方案中,而不是其他向量,并且仍然可以看到相当高的性能。vec_rbind()

microbenchmark(vctrs = vec_rbind(!!!modify(data, ~set_names(.x, seq(.x)))),
               dt = setDF(transpose(data)),
               map = map_df(data2, as_tibble_row),
               base = as.data.frame(do.call(rbind, data)))

final benchmark

为了它的价值。

评论

1赞 Ian Campbell 5/7/2020
您可以通过将名称设置为不需要 .paste
1赞 Ian Campbell 5/7/2020
也许像.但对于回答人们可以理解的日常问题来说,这并不理想。vctrs::vec_rbind(!!!lapply(data,function(x){attr(x,"names") <- 1:5; x}))
1赞 5/7/2020
是的,这比我刚才做的要快一些。但我同意。我很想打开一个功能请求,看看他们是否可以提前解析名称。我没有上场时间了。但这是一个有趣的问题。随意编辑这篇文章与基准,将它们移动到另一个帖子中,或任何你喜欢的东西。但我认为 setDF() 选项将是你的赢家。vctrs
10赞 s_baldur 5/7/2020 #7
library(data.table)
setDF(transpose(data))

  V1 V2 V3 V4 V5
1  A  1  4  7 12
2  B  2  5  8 15
3  C  3  6  9 18

评论

4赞 5/7/2020
我只是用其他一些方法运行了一个基准测试。这在速度方面碾压了其他一切,并且是第一个真正击败解决方案的人。base::rbind()
3赞 s_baldur 5/8/2020
@dww 是的,但与 / 不同。setDF()as.data.table()as.data.frame()
1赞 s_baldur 5/10/2020
@Adam,您认为可以使用更新的解决方案更新基准测试吗?对于那些不知道如何/工作的人来说,这是个好帖子:stackoverflow.com/a/44938350/4552295setDF()setDT()
1赞 Anoushiravan R 6/16/2021 #8

我认为这可以添加到已经完整的这个问题的非常好的答案中:

library(rlang) # Or purrr

data %>%
  exec(rbind, !!!.) %>%
  as_tibble() %>%
  set_names(~ letters[seq_along(.)])

# A tibble: 3 x 5
  a     b     c     d     e    
  <chr> <chr> <chr> <chr> <chr>
1 A     1     4     7     12   
2 B     2     5     8     15   
3 C     3     6     9     18