提问人:degeso 提问时间:9/21/2023 更新时间:9/22/2023 访问量:58
当最终数据帧的大小未知时,如何提高行绑定的速度
How to improve the speed of rowbinds when the size of the final dataframe is unknown
问:
我想对多个数据帧进行行绑定,每个数据帧具有不同数量的行数。我知道使用在每次迭代中覆盖类似 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
答:
approach3
并且大部分时间都花在 上,所以你对绑定操作的速度没有很好的了解。最好只对绑定进行计时:approach4
create_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
如果您知道表列全部对齐,则可以通过将参数设置为 来提高性能。rbindlist
use.names
FALSE
如果您有很多很多数据帧,超过 100-150 个,这个函数是一个不错的选择:从 ecospace 包中,考虑到规模,它非常有效。rbind_listdf()
如果你正在使用循环,那么我想说,如果你能考虑在for循环中管理内存,那会有所帮助。
在每次迭代的最后一步使用将清除任何大型内存占用,然后再返回下一次迭代。gc()
如果最终得到一个中间数据帧,并且可以为其分配一个变量名称,则可以使用 followed followed 来清除所有不必要的内容,而不是创建绑定后在内存中徘徊的临时数据帧。rm(temp_df)
gc()
困难的是,当累积数据帧的大小变得足够大,以至于占用了大部分操作 RAM 时,就没有多少内存来执行绑定了。
在这种情况下,需要考虑的是创建一个 .csv 文件,并将其写入磁盘(保存),然后使用循环遍历数据帧并将行写入该 csv,这是一个附加操作,而不是打开、读取和保存。在下一次迭代之前,我仍然会使用 & 来清理内存。这可能更快,也可能不更快,因为写入比绑定慢,但这确实意味着如果您经常进行垃圾回收,则不会在内存中携带大量主数据帧。rm()
gc()
创建完 csv 后,可以清除整个工作区,然后读入 csv 或数据库以处理数据。
评论
df_list = replicate(create_df(), n = 1000, simplify = FALSE)
bench::mark( data.table::rbindlist(df_list), dplyr::bind_rows(df_list), check = FALSE )
bind_rows
rbindlist
rbindlist
bind_rows
bench::mark
可能比这种情况更好,因为它还显示了内存分配,并且它使用自适应停止规则而不是固定次数的迭代,这意味着它通常更快)microbenchmark
rbindlist
bind_rows