整洁数据处理方法中如何按行绑定未命名向量的未命名列表——使用do.call(rbind,x)等效方法

33

我经常发现人们在某些情况下得到了一个没有名称的列表,其中包含没有名称的字符向量,他们想要将它们按行绑定到一个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.table 以有效地将一个未命名的字符向量列表逐行绑定为一个 data.frame


2
顺便提一下,Reduce(rbind, 不可能比 do.call(rbind, 更高效,因为 do.call 结构只分配内存并复制数据一次,而 Reduce 结构会重复分配新的内存并重新复制所有以前 "rbinded" 的元素。 - alexis_laz
你说得很对。我没有预料到性能会如此糟糕,100,000行数据的处理速度慢了6,000倍。我编辑了问题,称其为“不太高效的方法”。 - Ian Campbell
8个回答

15

并不完全确定效率,但使用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
@Adam更新了帖子,谢谢 :) 我无法回忆起一个与data.table函数相同的tidyverse函数,速度更快或者和它一样快。 - tmfmnk

11

10
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
我刚刚和一些其他方法进行了基准测试。就速度而言,这个解决方案击败了其他所有东西,它是第一个真正打败 base::rbind() 解决方案的方法。 - user10917479
3
是的,但是 setDF()as.data.table() / as.data.frame() 是不同的。 - s_baldur
1
@Adam,你认为你能否使用更新的解决方案更新你的基准测试?对于那些不了解setDF()/setDT()工作原理的人,这是一个很好的帖子:https://dev59.com/J1gQ5IYBdhLWcg3w-48d#44938350 - s_baldur

9
这似乎非常紧凑。我相信这就是dplyrbind_rows()所使用的方法, 因此也适用于purrr中的map_df(),所以应该相当高效。
library(vctrs)

vec_rbind(!!!data)

这将得到一个数据框。

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

一些基准测试

看起来在 tidyverse 方法中的 .name_repair 是一个严重的瓶颈。我采用了一些相当简单的选项,这些选项似乎也是从其他帖子中运行最快的(感谢 H 1 和 sindri_baldur)。

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)))

基准测试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)))

实际上,在vec_rbind()解决方案中包含对向量命名的时间而不是其他解决方案,仍然可以看到相当高的性能。

benchmark 2

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)))

最终基准测试结果

就其价值而言。


1
您可以通过将名称设置为仅需要整数而不需要“paste”来进一步提高性能。 - Ian Campbell
1
也许像 vctrs::vec_rbind(!!!lapply(data,function(x){attr(x,"names") <- 1:5; x})) 这样的代码对于回答人们可以理解的日常问题来说并不是最理想的。 - Ian Campbell
1
是的,这比我刚才做的要快一些。但我同意。我想在vctrs中打开一个功能请求,看看他们能否提前解决名称。我现在没有时间了。但这是一个有趣的问题。请随意编辑此帖子并进行基准测试,将它们移动到另一个帖子中或任何您喜欢的内容。但我认为setDF()选项将是您的赢家。 - user10917479

6

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

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

如果您希望将数据类型从字符向量调整为相应的类型,则 lapply 也可以在此方面提供帮助。 首先,对于每一行调用 lapply ,其次,对于每一列调用 lapply
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

5

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    

3

这是对tmfmnk提出的建议进行微小变化的方法,使用as_tibble_row()将向量转换为单行tibbles。还需要使用.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

1

我认为这可以添加到已经完整的一组非常好的回答中:

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  

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