使用 data.table 快速解除嵌套

rob*_*tdj 5 r unnest data.table

我目前正在使用该tidyr包来解除列表列的嵌套。然而,我正在寻找一种更快的方法并转向data.table(我是一个菜鸟)。考虑以下示例:

dt1 <- data.table::data.table(
    a = c("a1", "a2"),
    df1 = list(data.frame(
        b = c("b1", "b2")
    ))
)

tidyr::unnest(dt1, df1)
#> # A tibble: 4 x 2
#>   a     b    
#>   <chr> <chr>
#> 1 a1    b1   
#> 2 a1    b2   
#> 3 a2    b1   
#> 4 a2    b2

dt1[, data.table::rbindlist(df1), by = .(a)]
#>     a  b
#> 1: a1 b1
#> 2: a1 b2
#> 3: a2 b1
#> 4: a2 b2
Created on 2021-06-22 by the reprex package (v1.0.0)
Run Code Online (Sandbox Code Playgroud)

我得到了相同的结果,但如果我有一个大data.table且更多的列,by这种方法的性能会. 这种情况可以缓解吗?data.tabletidyr

一个后续问题是如何取消多列的嵌套data.table。考虑这个例子:

dt2 <- data.table::data.table(
    a = c("a1", "a2"),
    df1 = list(data.frame(
        b = c("b1", "b2")
    )),
    df2 = list(data.frame(
        c = c("c1", "c2")
    ))
)

tidyr::unnest(dt2, c(df1, df2))
#> # A tibble: 4 x 3
#>   a     b     c    
#>   <chr> <chr> <chr>
#> 1 a1    b1    c1   
#> 2 a1    b2    c2   
#> 3 a2    b1    c1   
#> 4 a2    b2    c2
Created on 2021-06-22 by the reprex package (v1.0.0)
Run Code Online (Sandbox Code Playgroud)

使用多个参数似乎data.table::rbindlist不起作用。

更新:在制作了一个大型(r)示例来证明我关于执行时间的主张之后,事实证明这tidyr对列表列是否包含data.frames 或data.tables 非常敏感:

dt1 <- data.table::data.table(
    a = c("a1", "a2"),
    df1 = list(data.frame(
        b = c("b1", "b2")
    ))
)

tidyr::unnest(dt1, df1)
#> # A tibble: 4 x 2
#>   a     b    
#>   <chr> <chr>
#> 1 a1    b1   
#> 2 a1    b2   
#> 3 a2    b1   
#> 4 a2    b2

dt1[, data.table::rbindlist(df1), by = .(a)]
#>     a  b
#> 1: a1 b1
#> 2: a1 b2
#> 3: a2 b1
#> 4: a2 b2
Created on 2021-06-22 by the reprex package (v1.0.0)
Run Code Online (Sandbox Code Playgroud)

由reprex 包(v1.0.0)于 2021-06-22 创建

在我的实际用例中,我嵌套了data.frames,因为它来自用 解析的 JSON RcppSimdJson,这里tidyr更快。

GKi*_*GKi 5

只是制定一个基准,显示与解决方案的差异data.table,并tidyr给出有问题的另一种方式data.tablebase解决方案。

DT <- data.table::data.table(
    a = c("a1", "a2"),
    df1 = list(data.frame(
        b = c("b1", "b2")
    ))
)
n <- 1e5
set.seed(42)
dt1 <- DT[sample(seq_len(nrow(DT)), n, TRUE),]

bench::mark(check = FALSE
          , tidyr = tidyr::unnest(dt1, df1)
          , dt = dt1[, data.table::rbindlist(df1), by = .(a)]
          , dt2 = dt1[, unlist(df1, TRUE, FALSE), .(a)]
          , base = data.frame(a=rep(dt1$a, lapply(dt1$df1, nrow)), do.call(rbind, dt1$df1))
          , base2 = data.frame(a=rep(dt1$a, lapply(dt1$df1, nrow)), b=unlist(dt1$df1, TRUE, FALSE))
      )
#  expression      min   median `itr/sec` mem_alloc `gc/sec` n_itr  n_gc
#  <bch:expr> <bch:tm> <bch:tm>     <dbl> <bch:byt>    <dbl> <int> <dbl>
#1 tidyr         1.03s    1.03s     0.971   22.59MB     7.76     1     8
#2 dt           46.9ms  50.15ms    17.1     15.01MB     9.47     9     5
#3 dt2         11.66ms  13.66ms    70.8     14.03MB    35.4     36    18
#4 base          3.47s    3.47s     0.288   43.23MB    12.1      1    42
#5 base2       353.9ms 363.41ms     2.75     4.58MB    11.0      2     8
Run Code Online (Sandbox Code Playgroud)

因此,data.table这两种方法都是最快的,然后是一种base解决方案,然后tidyr是另一种base解决方案。


GKi*_*GKi 3

base也许您可以使用和的组合data.table,因为看起来 usingdata.table::rbindlistdo.callwith更快rbind。另请参阅:How to speed up rbind?

对于更新中的给定数据,它看起来像:

data.frame(dt[rep(seq_len(nrow(dt)), vapply(dt$d, nrow, 0L)),1:3],
  data.table::rbindlist(dt$d)
Run Code Online (Sandbox Code Playgroud)

基于问题中给出的示例的基准:

f <- alist(tidyr = tidyr::unnest(dt, d)
 , datatable = dt[, data.table::rbindlist(d), by = .(a, b, c)]
 , base=do.call(rbind, lapply(seq_len(nrow(dt)), function(i) do.call(data.frame, dt[i,])))
 , base2=data.frame(dt[rep(seq_len(nrow(dt)), vapply(dt$d, nrow, 0L)),1:3], do.call(rbind, dt$d))
 , dtBase=data.frame(dt[rep(seq_len(nrow(dt)), vapply(dt$d, nrow, 0L)),1:3], data.table::rbindlist(dt$d)))

set.seed(42)
n_inner <- 300
inner_df <- data.frame(
    d1 = seq.POSIXt(as.POSIXct("2020-01-01"), as.POSIXct("2021-01-01"), length.out = n_inner),
    d2 = seq.POSIXt(as.POSIXct("2020-01-01"), as.POSIXct("2021-01-01"), length.out = n_inner),
    d3 = rnorm(n_inner)
)

n_outer <- 400

dt <- data.table::data.table(
    a = sample(10, n_outer, replace = TRUE),
    b = seq.POSIXt(as.POSIXct("2020-01-01"), as.POSIXct("2021-01-01"), length.out = n_outer),
    c = seq.POSIXt(as.POSIXct("2019-01-01"), as.POSIXct("2020-01-01"), length.out = n_outer),
    d = rep(list(inner_df), n_outer)
)
Run Code Online (Sandbox Code Playgroud)
inner_dt <- as.data.frame(inner_df) #Having data.frames in the dt
dt$d <- rep(list(inner_dt), n_outer)
do.call(bench::mark, c(f, check = FALSE))
#  expression      min   median `itr/sec` mem_alloc `gc/sec` n_itr  n_gc
#  <bch:expr> <bch:tm> <bch:tm>     <dbl> <bch:byt>    <dbl> <int> <dbl>
#1 tidyr        17.5ms  17.93ms     53.4    22.09MB     19.8    27    10
#2 datatable   45.52ms  50.54ms     17.2    25.59MB     27.5    10    16
#3 base       809.87ms 809.87ms      1.23    2.22GB    115.      1    93
#4 base2      290.01ms 294.97ms      3.39    1.12GB    173.      2   102
#5 dtBase       4.71ms   5.06ms    159.      10.6MB     69.4    80    35
Run Code Online (Sandbox Code Playgroud)
inner_dt <- data.table::as.data.table(inner_df) #Having data.tables in the dt
dt$d <- rep(list(inner_dt), n_outer)
do.call(bench::mark, c(f, check = FALSE))
#  expression      min   median `itr/sec` mem_alloc `gc/sec` n_itr  n_gc
#  <bch:expr> <bch:tm> <bch:tm>     <dbl> <bch:byt>    <dbl> <int> <dbl>
#1 tidyr      285.56ms 285.94ms      3.50   28.32MB     15.7     2     9
#2 datatable   45.73ms  48.67ms     16.7     25.3MB     18.5     9    10
#3 base       784.33ms 784.33ms      1.27    2.23GB    105.      1    82
#4 base2        4.61ms   4.83ms    166.     10.62MB     50.0    83    25
#5 dtBase       4.75ms   5.02ms    158.      10.6MB     49.9    79    25
Run Code Online (Sandbox Code Playgroud)

目前,如果必须与's 或's一起使用,则看起来使用base和的组合是最快的独立速度。data.tabledata.framedata.table