【问题标题】:Tidyverse approach to binding unnamed list of unnamed vectors by row - do.call(rbind,x) equivalentTidyverse 按行绑定未命名向量列表的方法 - do.call(rbind,x) 等效
【发布时间】:2020-08-20 05:13:18
【问题描述】:

我经常发现人们以某种​​方式以未命名 未命名字符向量列表结束的问题,他们希望将它们逐行绑定到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

也许效率较低的方法是使用来自 base 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  

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

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

【问题讨论】:

  • 附带说明,Reduce(rbind, 不能比 do.call(rbind, 更有效,因为 do.call 构造分配内存并复制数据一次,而 Reduce 构造重复分配新内存并重新复制所有以前的“rbinded”元素。
  • 你说的很对。我没想到性能会如此糟糕,在 100,000 行上慢了 6,000 倍。我编辑了这个问题,称这是一种“效率较低的方法”。

标签: r dplyr data.table tidyverse purrr


【解决方案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  

【讨论】:

    【解决方案2】:

    不完全确定效率,但使用 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  
    

    【讨论】:

    • tibble 切换到 as_tibble,您将获得 2-3 倍的加速。除了细微差别,太棒了!不幸的是,与data.table 解决方案(和rbind)相比仍然非常慢(但比我的vctrs 解决方案快)。不幸的是,tidyverse 选项看起来相对较慢,但除了非常大的列表之外,这无关紧要。对于大多数典型用例来说非常干净。
    • @Adam 更新了帖子,谢谢 :) 我不记得 tidyverse 函数比 data.table 函数更快或一样快。
    【解决方案3】:

    编辑

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


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

    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
    

    【讨论】:

      【解决方案4】:

      这看起来相当紧凑。我相信这就是 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
      

      一些基准

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

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

      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() 解决方案中而不是其他解决方案中,并且仍然可以看到相当高的性能。

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

      为了它的价值。

      【讨论】:

      • 您可以通过将名称设置为不需要paste 的整数来进一步提高性能。
      • 可能类似于vctrs::vec_rbind(!!!lapply(data,function(x){attr(x,"names") &lt;- 1:5; x}))。但对于回答人们可以理解的日常问题,这并不理想。
      • 是的,这比我刚刚做的要快一些。但我同意。我很想在vctrs 中打开一个功能请求,看看他们是否可以提前解析这些名称。我没有时间玩这个。但这是一个有趣的问题。随意用基准编辑这篇文章,把它们移到另一篇文章中,或者任何你喜欢的东西。但我认为 setDF() 选项将是你的赢家。
      【解决方案5】:
      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
      

      【讨论】:

      • 我刚刚用其他一些方法进行了基准测试。这在速度方面碾压了其他一切,并且是第一个真正击败 base::rbind() 解决方案的解决方案。
      • @dww 是,但setDF()as.data.table() / as.data.frame() 不同。
      • @Adam,您认为您可以使用更新的解决方案更新您的基准测试吗?对于那些不知道setDF()/setDT() 是如何工作的人来说,这里是个好帖子:stackoverflow.com/a/44938350/4552295
      【解决方案6】:

      这里是 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
      

      【讨论】:

        【解决方案7】:

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

        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
        

        【讨论】:

          【解决方案8】:

          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    
          

          【讨论】:

            猜你喜欢
            • 2020-10-03
            • 1970-01-01
            • 2013-02-05
            • 1970-01-01
            • 2020-12-31
            • 1970-01-01
            • 2023-03-05
            • 2017-02-23
            相关资源
            最近更新 更多