如何用向量化代替for循环,每个数据帧行执行数千次



对于R和(更重要的是)矢量化仍然很陌生,我不知道如何加快下面的代码。

for-loop通过对每个种子应用随机概率,计算在具有不同种子产生植物密度的几个路段上落在道路上的种子数量。由于我的真实数据帧有~200k行,种子数高达300k/段,在我当前的机器上使用下面的示例需要几个小时。

#Example data.frame
df <- data.frame(Density=c(0,0,0,3,0,120,300,120,0,0))
#Example SeedRain vector
SeedRainDists <- c(7.72,-43.11,16.80,-9.04,1.22,0.70,16.48,75.06,42.64,-5.50)
#Calculating the number of seeds from plant densities
df$Seeds <- df$Density * 500
#Applying a probability of reaching the road for every seed
df$SeedsOnRoad <- apply(as.matrix(df$Seeds),1,function(x){
    SeedsOut <- 0
    if(x>0){
        #Summing up the number of seeds reaching a certain distance
        for(i in 1:x){
            SeedsOut <- SeedsOut +
                ifelse(sample(SeedRainDists,1,replace=T)>40,1,0)
        }
    }
    return(SeedsOut)
})

如果有人能给我一个提示,关于如何用向量化代替循环,或者如何在第一时间更好地组织数据以提高性能,我将非常感激!

编辑: Roland的回答表明我可能过分简化了这个问题。在for循环中,我从另一个作者记录的距离分布中提取一个随机值(这就是为什么我不能在这里提供数据)。添加了一个示例向量,其中包含SeedRain距离的可能值。

这应该做同样的模拟:

df$SeedsOnRoad2 <- sapply(df$Seeds,function(x){
  rbinom(1,x,0.6)
})

#   Density  Seeds SeedsOnRoad SeedsOnRoad2
#1        0      0           0            0
#2        0      0           0            0
#3        0      0           0            0
#4        3   1500         892          877
#5        0      0           0            0
#6      120  60000       36048        36158
#7      300 150000       90031        89875
#8      120  60000       35985        35773
#9        0      0           0            0
#10       0      0           0            0

一种选择是为每一行df生成所有Seedssample()

在基于循环的代码之前使用set.seed(1),我得到:

> df
   Density  Seeds SeedsOnRoad
1        0      0           0
2        0      0           0
3        0      0           0
4        3   1500         289
5        0      0           0
6      120  60000       12044
7      300 150000       29984
8      120  60000       12079
9        0      0           0
10       0      0           0

如果我这样做,我会在很短的时间内得到相同的答案:

set.seed(1)
tmp <- sapply(df$Seeds, 
              function(x) sum(sample(SeedRainDists, x, replace = TRUE) > 40)))
> tmp
 [1]     0     0     0   289     0 12044 29984 12079     0     0
比较:

df <- transform(df, GavSeedsOnRoad = tmp)
df
> df
   Density  Seeds SeedsOnRoad GavSeedsOnRoad
1        0      0           0              0
2        0      0           0              0
3        0      0           0              0
4        3   1500         289            289
5        0      0           0              0
6      120  60000       12044          12044
7      300 150000       29984          29984
8      120  60000       12079          12079
9        0      0           0              0
10       0      0           0              0

这里需要注意的是:

  1. 尽量避免在循环中重复调用函数,如果函数是矢量化的,或者可以通过一次调用生成整个最终结果。在这里,您为df的每一行调用sample() Seeds次,每次调用返回来自SeedRainDists的单个样本。在这里,我做一个单独的sample()调用,要求Seeds的样本大小,对于df的每一行-因此我调用sample 10次,你的代码调用它271500次。
  2. 即使你必须在循环中反复调用一个函数,也要从循环中删除任何可以在循环完成后对整个结果进行矢量化的内容。这里的一个例子是SeedsOut的累积,它大量调用+()

    最好是将每个SeedsOut收集到一个向量中,然后在循环之外收集该向量sum()。例如

    SeedsOut <- numeric(length = x)
    for(i in seq_len(x)) {
      SeedsOut[i] <- ifelse(sample(SeedRainDists,1,replace=TRUE)>40,1,0)
    }
    sum(SeedOut)
    
  3. 请注意,R将逻辑视为数字0 s或1 s,用于任何数学函数。因此

    sum(ifelse(sample(SeedRainDists, 100, replace=TRUE)>40,1,0))
    

    sum(sample(SeedRainDists, 100, replace=TRUE)>40)
    

    如果在相同的set.seed()下运行,结果将相同。

可能有一种更花哨的采样方法,需要更少地调用sample()(并且有sample(SeedRainDists, sum(Seeds), replace = TRUE) > 40,但随后您需要注意为df的每一行选择该向量的正确元素-并不困难,只是一个轻微的麻烦),但是我所展示的可能足够有效?

最新更新