【问题标题】:Fast unnest with data.table使用 data.table 快速解除嵌套
【发布时间】:2021-09-05 20:08:35
【问题描述】:

我目前正在使用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)

我得到了相同的结果,但是如果我有一个大的 data.table 并且在 by 中有更多的列,那么这种方法在 data.table 中的性能比在 tidyr 中的性能更差。可以缓解吗?

一个后续问题是如何使用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)

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

更新:在做了一个大的(r)示例来证明我对执行时间的主张后,tidyr 对列表列是否包含 data.frames 或 @987654334 非常敏感@s:

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

bench::mark(check = FALSE,
    tidyr = tidyr::unnest(dt, d),
    datatable = dt[, data.table::rbindlist(d), by = .(a, b, c)]
)
#> # A tibble: 2 x 6
#>   expression      min   median `itr/sec` mem_alloc `gc/sec`
#>   <bch:expr> <bch:tm> <bch:tm>     <dbl> <bch:byt>    <dbl>
#> 1 tidyr          14ms   18.7ms      53.2      18MB     26.6
#> 2 datatable    56.2ms   56.2ms      17.8    25.5MB    178.

inner_dt <- data.table::as.data.table(inner_df)
dt$d <- rep(list(inner_dt), n_outer)

bench::mark(check = FALSE,
    tidyr = tidyr::unnest(dt, d),
    datatable = dt[, data.table::rbindlist(d), by = .(a, b, c)]
)
#> Warning: Some expressions had a GC in every iteration; so filtering is disabled.
#> # A tibble: 2 x 6
#>   expression      min   median `itr/sec` mem_alloc `gc/sec`
#>   <bch:expr> <bch:tm> <bch:tm>     <dbl> <bch:byt>    <dbl>
#> 1 tidyr       202.2ms  209.3ms      4.40    28.4MB     19.1
#> 2 datatable    43.5ms   49.9ms     18.3     25.4MB     22.0

reprex package (v1.0.0) 于 2021 年 6 月 22 日创建

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

【问题讨论】:

  • 您能否举一个示例数据集,其中可以观察到 data.table 的这种不良性能?

标签: r data.table unnest


【解决方案1】:

只是做一个基准,显示与data.tabletidyr 的解决方案的差异,给出了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

所以data.table 是两种方式最快的,然后是base 解决方案,然后是tidyr,然后是另一个base 解决方案。

【讨论】:

    【解决方案2】:

    也许您可以使用basedata.table 的组合,因为使用data.table::rbindlist 似乎比使用do.callrbind 更快。也可以看看: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)
    

    根据问题中给出的示例进行基准测试:

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

    目前看来,如果必须与 data.frame's 或 data.table's 一起使用,则使用 basedata.table 的组合最快。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2023-03-20
      • 2020-02-22
      • 2019-03-22
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多