在for循环中对data.table进行子集设置较慢且资源较多

Mar*_*ers 4 for-loop r subset cpu-usage data.table

使用data.tableR包,我注意到在运行一个简单的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)

Run Code Online (Sandbox Code Playgroud)

这是输出:

> #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
Run Code Online (Sandbox Code Playgroud)

现在,我知道可能有多种更好,更有效的方法来执行相同的任务,而且我可能不会这样做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   
Run Code Online (Sandbox Code Playgroud)

编辑:

感谢大家的评论.似乎减速问题与[.data.table@Hugh所详述的开销有关.这里提到了同样的问题有效的数据子集.如果@denis指出,使用大于,小于使用索引的数据.

@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 
> 
Run Code Online (Sandbox Code Playgroud)

只要你有lm()的瓶颈功能,你就可以使用基本的for循环,但是使用data.frames会更好(为了控制和可读性).

Fra*_*ank 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
Run Code Online (Sandbox Code Playgroud)

但是,我们可以说,由于某种原因,你有一个使用'data.frame'对象的脚本,你想快速重写使用data.table的东西.上面采用的方法似乎非常合理.

一旦习惯了data.table语法,这很快就可以编写.


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

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

对于此示例,行计数向量是有意义的,但如果您有一个更复杂的输出需要在a中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>
Run Code Online (Sandbox Code Playgroud)


Hug*_*ugh 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
Run Code Online (Sandbox Code Playgroud)

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

但同样重要的是你要拨打[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
Run Code Online (Sandbox Code Playgroud)

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