如何避免为大型数据集编写嵌套的for循环?

时间:2020-06-05 22:49:58

标签: r nested-loops

对于两个变量的问题,outer最有可能是此问题的最佳解决方案,如果要循环的空间足够小,则可以让expand.grid正常工作。但是,如果我们有两个以上的变量并且有较大的循环空间,则将这些排除在外。 outer不能处理两个以上的变量,并且expand.grid占用的内存比我见过的机器能容纳的更多。

我最近发现自己正在编写如下代码:

n<-1000
for(c in 1:n){
    for(b in 1:c){
        for(a in 1:b){
            if(foo(a,b,c))
            {
                bar(a,b,c)
            }
        }
    }
}

在这些情况下,似乎嵌套循环是自然的解决方案(例如mapply不会这样做,并且tapply没有很好的使用条件),但是有更好的方法吗?看来这是通往不良代码的途径。

我怀疑combn能够以某种方式做到这一点,但是根据我的经验,很快它就会落入与expand.grid相同的内存陷阱中。如果有内存可用,我也知道它会采取不明智的措施,告诉我更改递归限制的全局设置。

4 个答案:

答案 0 :(得分:6)

这是重复的组合。 可能是您开箱即用的最佳选择,但是在n = 1000L上,要进行的组合超过5亿种,将占用约2GB的内存。

library(RcppAlgos)
n = 1000L
mat <- comboGeneral(n, 3L, repetition = TRUE)

现在有两条路线可以走。如果您有RAM并且可以对功能进行矢量化,则可以非常快速地完成上述操作。假设如果组合的总和大于1000,则需要组合的平均值,否则,则需要组合的总和。

res <- if (rowSums(mat) > 1000L) 
  rowMeans(mat)
else
  rowSums(mat)

## Error: cannot allocate vector of size 1.2 Gb

哦,不!我得到了可怕的分配向量错误。 允许您返回函数的结果。但是请注意,它返回一个列表并且速度较慢,因为它将不得不评估R函数,而不是停留在c ++中。正因为如此,我改了n = 100L,因为我没有整天的时间...

comboGeneral(100L, 3L, repetition = TRUE,
                        FUN = function(x) { 
                          if (sum(x) > 100L)
                            mean(x)
                          else
                            sum(x)
                        }
)

如果我有一个静态集合,而我总是从n中选择3种组合,那么根据Rcppfoo(a,b,c)是什么,我可能会直接使用bar(a,b,c)代码但首先,我想进一步了解这些功能。

答案 1 :(得分:5)

我以前的功能lazyExpandGrid并不是一个完美的选择,但我认为它可以解决您对内存耗尽的担忧。其他语言可能会出现延迟迭代器。 R放在iterators包中,并且由于我不精通,所以不久前我写了this gist来解决这个问题。

lazyExpandGrid的一个问题是它希望因素是预先定义的。这可以通过快速的条件来处理,因此尽管不会节省空间,但会节省内存。我认为这不是在该方法中实现条件的快速解决方案,因为它懒惰地处理扩展的机制是知道数学上哪个索引与哪个 combination 因素...条件将破坏这一点。

以下是该功能在此处的工作方式:

n <- 3
it <- lazyExpandGrid(aa = 1:n, bb = 1:n, cc = 1:n)
while (length(thistask <- it$nextItem())) {
  if (with(thistask, bb > aa || cc > bb)) next
  print(jsonlite::toJSON(thistask))
}
# [{"aa":1,"bb":1,"cc":1}] 
# [{"aa":2,"bb":1,"cc":1}] 
# [{"aa":3,"bb":1,"cc":1}] 
# [{"aa":2,"bb":2,"cc":1}] 
# [{"aa":3,"bb":2,"cc":1}] 
# [{"aa":3,"bb":3,"cc":1}] 
# [{"aa":2,"bb":2,"cc":2}] 
# [{"aa":3,"bb":2,"cc":2}] 
# [{"aa":3,"bb":3,"cc":2}] 
# [{"aa":3,"bb":3,"cc":3}] 

### to demonstrate what an exhausted lazy-expansion looks like
it$nextItem()
# NULL
it$nextItem()
# NULL

(请注意,带有next的条件语句如何跳过这些组合。)

这将转化为您的流程:

n <- 1000
it <- lazyExpandGrid(aa = 1:n, bb = 1:n, cc = 1:n)
it
# lazyExpandGrid: 4 factors, 1e+09 rows
#   $ index : 0

while (length(thistask <- it$nextItem())) {
  if (with(thistask, bb > aa || cc > bb)) next
  with(thistask, {
    if (foo(aa, bb, cc)) bar(aa, bb, cc)
  })
}

(或不使用with,使用thistask$aa等)

注意:我不会撒谎,尽管这简化了流程,但并没有使流程变快。在这种情况下,1e+09次操作将花费 的时间,除了并行操作和友好的R主机群集之外,我不知道有什么方法可以帮助完成此任务。 (如上所述,我开始运行一个空的no-op while循环,并花了268秒的时间才能解决其中的822K。我希望您有很多处理能力。)

答案 2 :(得分:4)

重要的是要指出为什么使用既容易又推荐这样做。

当我们提到时,在下编译的是一堆代码。到目前为止,R开发人员无需开发已编译的代码即可允许将任意函数foo()bar()与重复结合使用。因此,作为用户,我们可以使用循环来获得的灵活性,或者,当我们有很多迭代要遍历时,请考虑一些替代方法。

将R循环简化为Rcpp循环。我已经包含了任意函数​​,以便我们可以返回一些内容(如果OP帖子也包含要返回的内容,那将是很好的……):

#include <Rcpp.h>
using namespace Rcpp;

bool foo(int x, int y, int z) {
  return((x + y + z) > 50);
}

int bar(int x, int y, int z) {
  return(x - y + z);
}

// [[Rcpp::export]]
double manual_combos_w_reps(const int n) {
  double ans = 0;

  for (int i = 1; i <= n; i++) {
    for (int j = 1; j <= i; j++) {
      for (int k = 1; k <= j; k++) {
        if (foo(i, j, k)) {
          ans += bar(i, j, k);
        }
      }
    }
  }

  return(ans);
}

这是R中的对应部分,它只是添加了foo(...)bar(...)的代码。

r_foo = function(x, y, z) {
  return((x + y + z) > 50L)
}

r_bar = function (x, y, z) {
  return(x - y + z)
}

r_loop = function (n) {
  ans = 0;
  for (i in 1:n) {
    for (j in 1:i) {
      for (k in 1:j) {
        if (r_foo(i, j, k)) {
          ans = ans + r_bar(i, j, k)
        }
      }
    }
  }
  return(ans)
}

现在这是神奇的部分。 Rcpp经历了这些迭代。对于n = 1000L,R代码需要360秒才能运行。 Rcpp只需运行0.5秒。

n = 10L
bench::mark(manual_combos_w_reps(n)
            , r_loop(n)
            )

### A tibble: 2 x 13
##  expression                 min median `itr/sec` mem_alloc
##  <bch:expr>              <bch:> <bch:>     <dbl> <bch:byt>
##1 manual_combos_w_reps(n)  4.7us    5us   178048.    2.48KB
##2 r_loop(n)               1.63ms 1.68ms      505.        0B

n = 100L

### A tibble: 2 x 13
##  expression                min median `itr/sec` mem_alloc
##  <bch:expr>              <bch> <bch:>     <dbl> <bch:byt>
##1 manual_combos_w_reps(n) 467us  469us   2064.      2.48KB
##2 r_loop(n)               627ms  627ms      1.60        0B

n = 1000L

### A tibble: 2 x 13
##  expression                   min   median `itr/sec`
##  <bch:expr>              <bch:tm> <bch:tm>     <dbl>
##1 manual_combos_w_reps(n) 459.29ms 459.39ms   2.18   
##2 r_loop(n)                  6.04m    6.04m   0.00276

为此,您应该绝对考虑 -在基R中没有真正的规范答案可以为您正在执行的任务提供高性能。唯一需要考虑的是您实际的foo()bar()函数的功能,因为它们可能难以在中实现。

答案 3 :(得分:1)

使用purrr

.filter解决方案也可以:

library(purrr)

n <- 10L
levels <- 3L

# keep only elements below diagonal
isdesc<- function(...){all(diff(unlist(list(...)))<=0)}
# some extra filtering
foo <- function(...) { sum(unlist(list(...)))==27}

filter <- function(...) {!isdesc(...)|!foo(...)}

cross_list <- cross(rep(list(1L:n),levels),.filter = filter)

bar <- function(...) ( unlist(list(...))) 

cross_list %>% map(bar)

不幸的是,与grid.expand一样,它的缩放效果也不理想,因为cross在过滤之前先分配了完整的笛卡尔积。