当最终数据帧的大小未知时,如何提高行绑定的速度

How to improve the speed of rowbinds when the size of the final dataframe is unknown

提问人:degeso 提问时间:9/21/2023 更新时间:9/22/2023 访问量:58

问:

我想对多个数据帧进行行绑定,每个数据帧具有不同数量的行数。我知道使用在每次迭代中覆盖类似 a 的内容的 for 循环非常慢,因为 R 必须保留每次更改的副本。通常,可以通过为数据帧预先分配正确数量的行和列,然后在循环的每次迭代中对其进行修改来解决。但是,在我的情况下,这有点棘手,因为与前一个数据帧相比,每个单独的数据帧的行数可能不同。(在我的实际代码中,我正在处理一长串 XML 文件,我从中提取某些信息。根据文件的不同,我最终可能会得到更多或更少的行。final_df

到目前为止,我的尝试是使用 or ,它们似乎表现相似,并且都比同类产品高出很多。但是,我注意到,即使使用这些方法,如果我增加数据帧的数量,计算速度仍然会非线性增加。dplyr::bind_rows()data.table::rbindlist()do.call("rbind")

对于如何进一步提高代码速度,您有什么建议吗?提前非常感谢!

create_df <- function() {
  nrow <- sample(12:15, 1)
  ncol <- 10
  randomdf <- matrix(rnorm(nrow*ncol), nrow=nrow, ncol=ncol) |> data.frame()
  return(randomdf)
}

approach1 <- function(n) {
  final_df <<- matrix(ncol=ncol, nrow=0)
  for(i in 1:n) {
    current_df <- create_df()
    final_df <<- rbind(final_df, current_df)
  }
}

approach2 <- function(n) {
  df_list <<- vector("list", n)
  for(i in 1:n) {
    df_list[[i]] <<- create_df()
  }
  final_df <<- do.call("rbind", df_list)
}

approach3 <- function(n) {
  df_list <<- vector("list", n)
  for(i in 1:n) {
    df_list[[i]] <<- create_df()
  }
  final_df <<- dplyr::bind_rows(df_list)
}

approach4 <- function(n) {
  df_list <<- vector("list", n)
  for(i in 1:n) {
    df_list[[i]] <<- create_df()
  }
  final_df <<- data.table::rbindlist(df_list)
}


microbenchmark::microbenchmark(
  approach1(5),
  approach2(5),
  approach3(5),
  approach4(5),
  approach1(50),
  approach2(50),
  approach3(50),
  approach4(50),
  approach1(500),
  approach2(500),
  approach3(500),
  approach4(500),
  times = 10
  )
Unit: microseconds
           expr      min       lq      mean    median       uq      max neval
   approach1(5)   1173.5   1201.1   1317.12   1285.30   1402.2   1557.0    10
   approach2(5)    771.6    781.8   1121.18    829.15    944.6   3573.1    10
   approach3(5)    543.7    613.4    966.10    672.15    952.4   3131.8    10
   approach4(5)    520.8    586.5    641.18    621.65    663.8    818.8    10
  approach1(50)  12186.9  12381.4  13932.40  12760.10  14518.8  18537.4    10
  approach2(50)   6497.6   6766.0   7160.26   6967.55   7230.3   8390.6    10
  approach3(50)   3681.3   4143.1   4258.44   4233.10   4347.8   5022.8    10
  approach4(50)   3806.7   3821.8   4166.71   3962.95   4190.6   5900.4    10
 approach1(500) 275530.0 285779.1 326732.16 294302.30 304461.0 622130.3    10
 approach2(500)  65243.8  67456.7  72789.76  74422.30  77063.0  79485.0    10
 approach3(500)  38600.0  39328.4  41372.67  41215.80  42345.2  47488.8    10
 approach4(500)  32496.5  36788.1  41160.35  39940.10  46043.2  49752.9    10
r dplyr data.table rbind

评论

1赞 Gregor Thomas 9/21/2023
使用数据帧,我认为这和你得到的一样好。如果你能用矩阵来代替,那可能会更快。我还建议从基准测试代码中删除“创建数据帧”步骤,因为这是一个独立的过程。是的,它对它们的影响是平等的,但它会阻止你隔离你所关注的步骤——绑定。
1赞 Gregor Thomas 9/21/2023
我会说,如果我跑然后,我认为不能与 - 我看到比 快 4 倍。df_list = replicate(create_df(), n = 1000, simplify = FALSE)bench::mark( data.table::rbindlist(df_list), dplyr::bind_rows(df_list), check = FALSE )bind_rowsrbindlistrbindlistbind_rows
0赞 Gregor Thomas 9/21/2023
(bench::mark可能比这种情况更好,因为它还显示了内存分配,并且它使用自适应停止规则而不是固定次数的迭代,这意味着它通常更快)microbenchmark
0赞 Gregor Thomas 9/21/2023
当我将数据帧创建与绑定隔离时,我看到 1,000 个数据帧需要 1.31 毫秒,10,000 个数据帧需要 31 毫秒,100,000 个数据帧需要 494 毫秒。因此,增长是非线性的,但对于100,000个数据帧来说,不到0.5秒似乎确实很快。在这个规模下,速度较慢,但在 753 毫秒时速度不是很慢 2 倍,因此也非常有用。rbindlistbind_rows

答:

3赞 jblood94 9/21/2023 #1

approach3并且大部分时间都花在 上,所以你对绑定操作的速度没有很好的了解。最好只对绑定进行计时:approach4create_df

library(dplyr)
library(data.table)

create_df <- function(n) {
  nrow <- sample(12:15, 1)
  ncol <- 10
  randomdf <- matrix(rnorm(nrow*ncol), nrow=nrow, ncol=ncol) |> data.frame()
  return(randomdf)
}

df_list <- lapply(c(5, 50, 500), \(n) lapply(1:n, create_df))

approach2 <- function(i) do.call("rbind", df_list[[i]])
approach3 <- function(i) bind_rows(df_list[[i]])
approach4 <- function(i) rbindlist(df_list[[i]])
approach5 <- function(i) rbindlist(df_list[[i]], FALSE)

microbenchmark::microbenchmark(
  approach2(1),
  approach3(1),
  approach4(1),
  approach5(1),
  approach2(2),
  approach3(2),
  approach4(2),
  approach5(2),
  approach2(3),
  approach3(3),
  approach4(3),
  approach5(3)
)
#> Unit: microseconds
#>          expr     min       lq      mean   median       uq     max neval
#>  approach2(1)   321.1   360.40   389.968   377.25   406.65   601.5   100
#>  approach3(1)    89.9   118.85   157.806   135.80   191.45   690.2   100
#>  approach4(1)    77.2    89.05   176.894   103.05   161.15  4250.6   100
#>  approach5(1)    61.8    70.10   100.532    94.15   120.60   223.7   100
#>  approach2(2)  3070.4  3228.40  3735.250  3352.30  3574.90  8796.5   100
#>  approach3(2)   348.3   408.35   470.308   440.50   514.70   931.6   100
#>  approach4(2)   136.7   169.65   204.703   189.25   222.40   362.6   100
#>  approach5(2)   111.5   133.85   194.793   150.10   199.50  2957.8   100
#>  approach2(3) 31565.1 34130.30 36182.204 35523.60 36503.40 89033.4   100
#>  approach3(3)  3008.7  3268.30  3785.467  3440.65  3714.85  7923.1   100
#>  approach4(3)   794.4   913.45  1009.823   966.20  1054.20  1692.0   100
#>  approach5(3)   655.8   767.35   870.240   822.45   894.95  2124.1   100

现在很明显,对于较大的表列表来说,这是最快的。如果你的过程需要很长时间,那么绑定操作可能不是我首先要看的地方。rbindlist

如果您知道表列全部对齐,则可以通过将参数设置为 来提高性能。rbindlistuse.namesFALSE

1赞 sconfluentus 9/21/2023 #2

如果您有很多很多数据帧,超过 100-150 个,这个函数是一个不错的选择:从 ecospace 包中,考虑到规模,它非常有效。rbind_listdf()

如果你正在使用循环,那么我想说,如果你能考虑在for循环中管理内存,那会有所帮助。

在每次迭代的最后一步使用将清除任何大型内存占用,然后再返回下一次迭代。gc()

如果最终得到一个中间数据帧,并且可以为其分配一个变量名称,则可以使用 followed followed 来清除所有不必要的内容,而不是创建绑定后在内存中徘徊的临时数据帧。rm(temp_df)gc()

困难的是,当累积数据帧的大小变得足够大,以至于占用了大部分操作 RAM 时,就没有多少内存来执行绑定了。

在这种情况下,需要考虑的是创建一个 .csv 文件,并将其写入磁盘(保存),然后使用循环遍历数据帧并将行写入该 csv,这是一个附加操作,而不是打开、读取和保存。在下一次迭代之前,我仍然会使用 & 来清理内存。这可能更快,也可能不更快,因为写入比绑定慢,但这确实意味着如果您经常进行垃圾回收,则不会在内存中携带大量主数据帧。rm()gc()

创建完 csv 后,可以清除整个工作区,然后读入 csv 或数据库以处理数据。