在处理嵌套的命名向量时,有比 tidyr::unnest_longer() 更快的替代方法吗?

Any speedier alternatives to tidyr::unnest_longer() when dealing with nested named vectors?

我有一个大数据集,它有一个列表列,其中包含嵌套的 named 向量。我想将这些向量取消嵌套到两个新列中:

现在,我发现的唯一直接的方法是 tidyr::unnest_longer()。虽然它非常适合小数据对象,但我发现它在处理非常大的数据时太慢了。因此,我正在寻找更快的替代方案。

我已经看到 让我接近,但不是我所需要的:unnest_wider() 的基础 R 替代品,但速度要快得多。但是,我正在寻找一种模仿 unnest_longer 的快速解决方案。

无论是基于base Rdata.tablerrapply, or collapse的解决方案——只要能缩短处理时间,都欢迎。

可重现的例子

数据

library(stringi)
library(tidyr)
library(magrittr, warn.conflicts = FALSE)

# simulate data
set.seed(123)
vec_n <- 1e6
vec_vals  <- 1:vec_n
vec_names <- stringi::stri_rand_strings(vec_n, 5)

my_named_vec <- setNames(vec_vals, vec_names)

split_func <- function(x, n) {
  unname(split(x, rep_len(1:n, length(x))))
} 

my_tbl <-
  tibble(col_1 = sample(split_func(my_named_vec, n = vec_n / 5)))

所以 my_tblgiven 我需要“取消嵌套”的数据对象。数据结构的简要预览显示列 col_1.

的每一行中都有一个嵌套的命名向量
# preview my_tbl
my_tbl
#> # A tibble: 200,000 x 1
#>    col_1    
#>    <list>   
#>  1 <int [5]>
#>  2 <int [5]>
#>  3 <int [5]>
#>  4 <int [5]>
#>  5 <int [5]>
#>  6 <int [5]>
#>  7 <int [5]>
#>  8 <int [5]>
#>  9 <int [5]>
#> 10 <int [5]>
#> # ... with 199,990 more rows

head(my_tbl$col_1)
#> [[1]]
#>  9YAGC  hTjlr  vgxjQ  y4qG2  R1fUE 
#>  56356 256356 456356 656356 856356 
#> 
#> [[2]]
#>  nz5rk  ZvEe6  ustHv  2TdA8  Rreqn 
#> 119257 319257 519257 719257 919257 
#> 
#> [[3]]
#>  ubbWp  aw6zR  ab0Ax  N747j  GY1xU 
#>   4663 204663 404663 604663 804663 
#> 
#> [[4]]
#>  JHo4w  otk4s  BTZ3h  zlAKU  svSgH 
#>  75297 275297 475297 675297 875297 
#> 
#> [[5]]
#>  A1pxZ  T7y0l  0ixE2  DRBxP  IBqxe 
#>  19495 219495 419495 619495 819495 
#> 
#> [[6]]
#>  fDkau  Z7tmy  TIzgx  nKANU  Bqwo1 
#> 184074 384074 584074 784074 984074

“取消嵌套”

如果我们只有 10 行

my_tbl[1:10, ] %>%
  tidyr::unnest_longer(col_1)
#> # A tibble: 50 x 2
#>     col_1 col_1_id
#>     <int> <chr>   
#>  1  56356 9YAGC   
#>  2 256356 hTjlr   
#>  3 456356 vgxjQ   
#>  4 656356 y4qG2   
#>  5 856356 R1fUE   
#>  6 119257 nz5rk   
#>  7 319257 ZvEe6   
#>  8 519257 ustHv   
#>  9 719257 2TdA8   
#> 10 919257 Rreqn   
#> # ... with 40 more rows

但是在 my_tbl 中有 200,000 行,所以这个 tidyr::unnest_longer() 需要很多时间:

my_benchmark <-
  bench::mark(tidyrunnestlonger = 
              my_tbl %>%
              tidyr::unnest_longer(col_1)
)
#> Warning: Some expressions had a GC in every iteration; so filtering is disabled.

my_benchmark
#> # A tibble: 1 x 6
#>   expression             min   median `itr/sec` mem_alloc `gc/sec`
#>   <bch:expr>        <bch:tm> <bch:tm>     <dbl> <bch:byt>    <dbl>
#> 1 tidyrunnestlonger    2.47m    2.47m   0.00674     309MB     1.50

由 reprex 包 (v2.0.0) 创建于 2021-08-23


虽然 my_tbl 有 200,000 行,但我的真实数据在这种格式下有超过 100 万行。所以我正在寻找最快的解决方案。

谢谢!

您可以对每个元素尝试 tibble::enframe,然后 unnest。我正在研究这个答案,寻找在展平之前将所有命名向量转换为数据帧的方法:Convert Named Character Vector to data.frame.

my_tbl %>%
  mutate(col_1 = map(col_1, enframe)) %>%
  unnest(col_1)

快速基准测试

library(tidyverse) # purrr, tibble, dplyr, tidyr

microbenchmark::microbenchmark(
  tidyrunnestlonger = 
    my_tbl %>%
    unnest_longer(col_1),
  enframe = 
    my_tbl %>%
    mutate(col_1 = map(col_1, enframe)) %>%
    unnest(col_1),
  times = 1
)

#-------------
Unit: seconds
              expr        min         lq       mean     median         uq        max neval
 tidyrunnestlonger 101.419181 101.419181 101.419181 101.419181 101.419181 101.419181     1
           enframe   6.140771   6.140771   6.140771   6.140771   6.140771   6.140771     1

您可以 unlist 您的专栏:

x<-unlist(my_tbl[[1]])
res<-tibble(col_1=unname(x),col_1_id=names(x))
res
## A tibble: 1,000,000 x 2
#    col_1 col_1_id
#    <int> <chr>   
# 1  56356 9YAGC   
# 2 256356 hTjlr   
# 3 456356 vgxjQ   
# 4 656356 y4qG2   
# 5 856356 R1fUE   
# 6 119257 nz5rk   
# 7 319257 ZvEe6   
# 8 519257 ustHv   
# 9 719257 2TdA8   
#10 919257 Rreqn   
## … with 999,990 more rows