在for循环中子集化data.table速度较慢,而且资源匮乏

时间:2019-01-29 13:50:23

标签: r for-loop data.table subset cpu-usage

使用data.table R包,我注意到运行一个简单的for循环时会使用非常高的处理器,该循环将使用来自另一个data.table的值来子集数据集。当我说高使用率时,我的意思是循环运行的整个过程中所有可用线程都是100%。

有趣的是,在同一过程中使用data.frame对象对于相同的输出所花的时间要少10倍。而且只有一个核心达到100%。

这是我希望重现的示例:

chr = c(rep(1, 1000), rep(2, 1000), rep(3, 1000), rep(3,1000))
start = rep(seq(from =1, to = 100000, by=100), 4)
end = start + 100

df1 <- data.frame(chr=chr, start=start, end=end)
df2 <- rbind(df1,df1,df1,df1,df1)
dt1 <- data.table::data.table(df1)
dt2 <- data.table::data.table(df2)

test1 <- list()
test2 <- list()

#loop subsetting a data.frame
system.time(
for (i in 1:nrow(df2)) {
  no.dim <- dim(df1[df1$chr == df2[i, 'chr'] & df1$start >= df2[i, 'start'] & df1$end <= df2[i, 'end'], ])[1]
  test1[i] <- no.dim
})

# loop subsetting a data.table using data.table syntax
system.time(
for (i in 1:nrow(dt2)) {
  no.dim <- dim(dt1[chr == dt2[i, chr] & start >= dt2[i, start] & end <= dt2[i, end], ])[1]
  test2[i] <- no.dim
})

# is the output the same
identical(test1, test2)

这是输出:

> #loop subsetting a data.frame
> system.time(
+ for (i in 1:nrow(df2)) {
+   no.dim <- dim(df1[df1$chr == df2[i, 'chr'] & df1$start >= df2[i, 'start'] & df1$end <= df2[i, 'end'], ])[1]
+   test1[i] <- no.dim
+ })
   user  system elapsed 
  2.607   0.004   2.612 
> 
> # loop subsetting a data.table using data.table syntax
> system.time(
+ for (i in 1:nrow(dt2)) {
+   no.dim <- dim(dt1[chr == dt2[i, chr] & start >= dt2[i, start] & end <= dt2[i, end], ])[1]
+   test2[i] <- no.dim
+ })
   user  system elapsed 
192.632   0.152  24.398 
> 
> # is the output the same
> identical(test1, test2)
[1] TRUE

现在,我知道执行同一任务可能有多种更好和更有效的方法,而且我可能不是data.table方法。但是可以说,由于某种原因,您有一个使用“ data.frame”对象的脚本,并且您想快速重写该内容以使用data.table来代替。上面采取的方法似乎很合理。

任何人都可以在速度下降和处理器使用率方面重现同样的情况吗?是否可以通过保持或多或少相同的子集过程来解决问题,还是必须将其完全重写才能在data.table上有效使用?

PS:刚刚在Windows计算机上对其进行了测试,线程使用率正常(一个线程以100%的速度运行),但速度仍然较慢。在与我的类似的系统上进行测试,结果与上面相同。

R version 3.5.1 (2018-07-02)
Platform: x86_64-pc-linux-gnu (64-bit)
Running under: Ubuntu 18.10

Matrix products: default
BLAS: /usr/lib/x86_64-linux-gnu/blas/libblas.so.3.8.0
LAPACK: /usr/lib/x86_64-linux-gnu/lapack/liblapack.so.3.8.0

locale:
 [1] LC_CTYPE=C           LC_NUMERIC=C         LC_TIME=C            LC_COLLATE=C        
 [5] LC_MONETARY=C        LC_MESSAGES=C        LC_PAPER=et_EE.UTF-8 LC_NAME=C           
 [9] LC_ADDRESS=C         LC_TELEPHONE=C       LC_MEASUREMENT=C     LC_IDENTIFICATION=C 

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods   base     

other attached packages:
[1] data.table_1.12.0

loaded via a namespace (and not attached):
 [1] compiler_3.5.1   assertthat_0.2.0 cli_1.0.1        tools_3.5.1      pillar_1.3.1    
 [6] rstudioapi_0.9.0 tibble_2.0.0     crayon_1.3.4     utf8_1.1.4       fansi_0.4.0     
[11] pkgconfig_2.0.2  rlang_0.3.1   

编辑:

感谢大家的评论。减速问题似乎与[.data.table的开销有关,如@Hugh所详述。 @denis指出这里efficient subsetting of data.table with greater-than, less-than using indices提到了相同的问题。

@Frank提出的修复程序虽然确实有效并且可以产生相似的输出,但是通过完全删除循环并在原始数据集中添加了可能不需要的列来更改过程的行为。

EDIT.1:

在我的第一次编辑之后,@ Frank添加了另一种方法,该方法包括使用data.table语法计算列表列。尽管它非常简洁,但我必须承认我需要一段时间才能弄清楚发生了什么。我发现它只是在子集data.table的开始和结束列上计算lm(),所以我尝试使用for循环和data.frames重现结果。时间:

> system.time({res <- dt1[dt2, on=.(chr, start >= start, end <= end), .(n = .N, my_lm = list(lm(x.start ~ x.end))), by=.EACHI][, .(n, my_lm)]; res <- as.list(res$my_lm)})
   user  system elapsed 
 11.538   0.003  11.336 
> 
> test_new <- list()
> system.time(
+   for (i in 1:20000) {
+     df_new <- df1[df1$chr == df2$chr[i] & df1$start >= df2$start[i] & df1$end <= df2$end[i],]
+     test_new[[i]] <- lm(df_new$start ~ df_new$end)
+   })
   user  system elapsed 
 12.377   0.048  12.425 
> 

只要您具有lm()的瓶颈功能,就最好使用基本的for循环(出于控制和可读性的考虑),但要使用data.frames。

2 个答案:

答案 0 :(得分:7)

  

任何人都可以在速度下降和处理器使用率方面重现同样的情况吗?是否可以通过保持大致相同的子集过程以某种方式修复,还是必须对其进行完全重写才能在data.table上有效使用?

对于OP的两种方法(分别是DF和DT),我得到的时间分别为5秒和44秒,但是...

system.time(
  dt2[, v := dt1[.SD, on=.(chr, start >= start, end <= end), .N, by=.EACHI]$N]
)
#    user  system elapsed 
#    0.03    0.01    0.03 
identical(dt2$v, unlist(test1))
# TRUE
  

但是可以说,由于某种原因,您有一个使用'data.frame'对象的脚本,并且您想快速重写该内容以使用data.table。上面采取的方法似乎很合理。

一旦您习惯了data.table语法,这将很快写出来。


如果您不想修改dt2,只需直接获取矢量...

res <- dt1[dt2, on=.(chr, start >= start, end <= end), .N, by=.EACHI]$N

在此示例中,行计数向量是有意义的,但是如果您需要在list中添加更复杂的输出,则可以使用list列...

res <- dt1[dt2, on=.(chr, start >= start, end <= end), .(
  n = .N, 
  my_lm = list(lm(x.start ~ x.end))
), by=.EACHI][, .(n, my_lm)]

       n my_lm
    1: 1  <lm>
    2: 1  <lm>
    3: 1  <lm>
    4: 1  <lm>
    5: 1  <lm>
   ---        
19996: 2  <lm>
19997: 2  <lm>
19998: 2  <lm>
19999: 2  <lm>
20000: 2  <lm>

答案 1 :(得分:5)

用户时间与经过时间之间的差是一个线索,表明在后台进行了一些并行化:

library(data.table)
chr = c(rep(1, 1000), rep(2, 1000), rep(3, 1000), rep(3,1000))
start = rep(seq(from =1, to = 100000, by=100), 4)
end = start + 100

df1 <- data.frame(chr=chr, start=start, end=end)
df2 <- rbind(df1,df1,df1,df1,df1)
dt1 <- data.table::data.table(df1)
dt2 <- data.table::data.table(df2)

print(dim(dt1))
#> [1] 4000    3
print(dim(dt2))
#> [1] 20000     3


test1 <- list()
test2 <- list()

bench::system_time({
  for (i in 1:nrow(df2)) {
    no.dim <- dim(df1[df1$chr == df2[i, 'chr'] &
                        df1$start >= df2[i, 'start'] &
                        df1$end <= df2[i, 'end'], ])[1]
    test1[i] <- no.dim
  }
})
#> process    real 
#>  3.547s  3.549s

print(getDTthreads())
#> [1] 12

bench::system_time({
  for (i in 1:nrow(dt2)) {
    no.dim <- dim(dt1[chr == dt2[i, chr] & start >= dt2[i, start] & end <= dt2[i, end], ])[1]
    test2[i] <- no.dim
  }
})
#> process    real 
#> 83.984s 52.266s

setDTthreads(1L)
bench::system_time({
  for (i in 1:nrow(dt2)) {
    no.dim <- dim(dt1[chr == dt2[i, chr] & start >= dt2[i, start] & end <= dt2[i, end], ])[1]
    test2[i] <- no.dim
  }
})
#> process    real 
#> 30.922s 30.920s

reprex package(v0.2.1)于2019-01-30创建

但是您打电话给[ 20,000次同样重要。考虑这种最小的用法,以证明单行表的[.data.table开销占运行时间的主要部分:

library(data.table)
chr = c(rep(1, 1000), rep(2, 1000), rep(3, 1000), rep(3,1000))
start = rep(seq(from =1, to = 100000, by=100), 4)
end = start + 100

df1 <- data.frame(chr=chr, start=start, end=end)
df2 <- rbind(df1,df1,df1,df1,df1)
dt1 <- data.table::data.table(df1)
dt2 <- data.table::data.table(df2)

bench::system_time({
  o <- integer(nrow(df2))
  for (i in 1:nrow(df2)) {
    o[i] <- df2[i, ][[2]]
  }
})
#>   process      real 
#> 875.000ms 879.398ms

bench::system_time({
  o <- integer(nrow(dt2))
  for (i in 1:nrow(dt2)) {
    o[i] <- dt2[i, ][[2]]
  }
})
#> process    real 
#> 26.219s 13.525s

reprex package(v0.2.1)于2019-01-30创建