Vectorize R命令(第2部分)

时间:2016-05-20 10:44:28

标签: r vectorization which

昨天I asked一个非常简单的矢量化问题并得到了一些很好的答案。今天问题有点复杂,我想知道R是否有通过向量化来加速这个循环的运行时的函数。

循环

for(j in 1:N) {
    A[j,1] = B[max(which(C[j]>=D))];
}

我试过

A[,1] = B[max(which(C>=D))];

这大大降低了运行时间...但答案是错误的。有没有正确的"在R?

中这样做的方法

EDIT1:

感谢有关数据的问题。我将在这里给出数组的大小:

我们正在循环1:N

 A is N x 1
 B is length M
 C is length N
 D is length M

如果速度方面很重要,在本例中为N = 844M = 2500

EDIT2:

以下是较小的模拟数据集的一些值:

B <- c(1.0000000, 1.0000000, 1.0000000, 0.9565217, 0.9565217, 0.9565217, 0.9565217,
0.9565217, 0.9565217, 0.9565217, 0.8967391, 0.8369565, 0.7771739, 0.7173913,
0.7173913, 0.7173913, 0.7173913, 0.7173913, 0.6277174, 0.6277174, 0.5230978,
0.5230978, 0.3923234, 0.3923234, 0.3923234)
C <- c(0.10607, 0.14705, 0.43607, 0.56587, 0.76203, 0.95657, 1.03524, 1.22956, 1.39074, 2.36452)
D <- c(0.10607, 0.13980, 0.14571, 0.14705, 0.29412, 0.33693, 0.43607, 0.53968, 0.56587,
0.58848, 0.64189, 0.65475, 0.75518, 0.76203, 0.95657, 1.03524, 1.05454, 1.18164,
1.22956, 1.23760, 1.39074, 1.87604, 2.36452, 2.89497, 4.42393)

结果应为:

 > A
           [,1]
 [1,] 1.0000000
 [2,] 0.9565217
 [3,] 0.9565217
 [4,] 0.9565217
 [5,] 0.7173913
 [6,] 0.7173913
 [7,] 0.7173913
 [8,] 0.6277174
 [9,] 0.5230978
[10,] 0.3923234

3 个答案:

答案 0 :(得分:1)

您可以使用outer

您的代码:

A1 <- matrix(NA_real_, ncol = 1, nrow = length(C))
for(j in seq_along(C)) {
  A1[j,1] = B[max(which(C[j]>=D))];
}

测试C的元素是否大于/等于D outer的元素:

test <- outer(C, D, FUN = ">=")
#      [,1]  [,2]  [,3]  [,4]  [,5]  [,6]  [,7]  [,8]  [,9] [,10] [,11] [,12] [,13] [,14] [,15] [,16] [,17] [,18] [,19] [,20] [,21] [,22] [,23] [,24] [,25]
# [1,] TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
# [2,] TRUE  TRUE  TRUE  TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
# [3,] TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
# [4,] TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
# [5,] TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
# [6,] TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
# [7,] TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
# [8,] TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE FALSE FALSE FALSE FALSE FALSE FALSE
# [9,] TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE FALSE FALSE FALSE FALSE
#[10,] TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE FALSE FALSE

请注意,这可能会为大型矢量使用大量内存。

然后找到每行中的最后一个TRUE值:

ind <- max.col(test, ties.method = "last") * (rowSums(test) > 0)

rowSums(test) > 0测试是否存在任何TRUE值,否则会使ind 0的对应元素生效。在这种情况下你不想要发生什么。 (在子集化过程中会忽略0索引。可能在您的最终结果中需要NA?)

现在子集:

A2 <- as.matrix(B[ind], ncol = 1)
#           [,1]
# [1,] 1.0000000
# [2,] 0.9565217
# [3,] 0.9565217
# [4,] 0.9565217
# [5,] 0.7173913
# [6,] 0.7173913
# [7,] 0.7173913
# [8,] 0.6277174
# [9,] 0.5230978
#[10,] 0.3923234

结果是否相同?

identical(A2, A1)
#[1] TRUE

数据(请在下次使用dput提供示例数据):

B <- c(1.0000000, 1.0000000, 1.0000000, 0.9565217, 0.9565217, 0.9565217, 0.9565217,
0.9565217, 0.9565217, 0.9565217, 0.8967391, 0.8369565, 0.7771739, 0.7173913,
0.7173913, 0.7173913, 0.7173913, 0.7173913, 0.6277174, 0.6277174, 0.5230978,
0.5230978, 0.3923234, 0.3923234, 0.3923234)
C <- c(0.10607, 0.14705, 0.43607, 0.56587, 0.76203, 0.95657, 1.03524, 1.22956, 1.39074,
2.36452)
D <- c(0.10607, 0.13980, 0.14571, 0.14705, 0.29412, 0.33693, 0.43607, 0.53968, 0.56587,
0.58848, 0.64189, 0.65475, 0.75518, 0.76203, 0.95657, 1.03524, 1.05454, 1.18164,
1.22956, 1.23760, 1.39074, 1.87604, 2.36452, 2.89497, 4.42393)

答案 1 :(得分:1)

在看到@Roland的回答后,我想我更清楚你在问什么。要仔细检查:您想要将C(单独)的每个值与D的所有值进行比较,并获得D的最大索引(让我们称之为k)其值小于C[j]。然后,您可以使用它将B的相应值分配给A,从而A[j]=B[k]。这是对的吗?

我没有关于如何矢量化你想做什么的答案,但确实有一些关于如何加快它的建议。在此之前,让我问一下它是否真的值得付出努力。对于你提到的更大的例子(N~1000,M~2500),你的循环仍然可以在我的笔记本电脑上运行一秒钟。除非这个计算在另一个循环中多次完成,否则看似不必要的优化......

另外,就像@Roland所指出的那样,如果C中的值小于D中的所有值,则不清楚会发生什么。这些功能(包括你的原始循环)如果发生这种情况将不起作用,并且需要稍微调整一下。

无论如何,这些是我的建议:

首先,让我将循环包装到函数中以方便使用。

f_loop <- function(B, C, D){
  N <- length(C)
  A <- matrix(0, ncol=1, nrow=N)
  for(j in 1:N) {
    A[j,1] = B[max(which(C[j]>=D))]
  }
  return(A)
}

如果您希望它看起来更像“R-like”,您可以使用*apply函数之一替换循环。在这种情况下,它还比循环运行略快

vapply(C, function(x) B[max(which(x>=D))], 0)

## Wrapped into a function for easier reference
f_vapply <- function(B, C, D){
  vapply(C, function(x) B[max(which(x>=D))], 0)
}

我的另一个建议是丑陋的(而不是真正的“R-like”),但可以帮助加快速度(如果这是最终目标)。我使用inline包来创建循环的编译版本(请注意,根据您的操作系统和R设置,您可能需要下载其他工具或包以便能够编译代码)。

## Translate loop into Fortran
loopcode <-
"  integer i, j, k
   do i = 1, n
      k = 0
      do j = 1, m
         if (C(i) >= D(j)) k = j
      end do
      A(i) = B(k)
   end do
"
## Compile into function
library(inline)
loopfun <- cfunction(sig = signature(A="numeric", B="numeric", C="numeric", D="numeric", n="integer", m="integer"), dim=c("(n)", "(m)", "(n)", "(m)", "", ""), loopcode, language="F95")

## Wrap into function for easier reference
f_compiled <- function(B, C, D){
  A <- C
  n <- length(A)
  m <- length(B)
  out <- loopfun(A, B, C, D, n, m)
  return(as.matrix(out$A, ncol=1))
}

让我们检查结果是否匹配:

cbind(A, f_loop(B, C, D), f_vapply(B, C, D), f_compiled(B, C, D))
##            [,1]      [,2]      [,3]      [,4]
##  [1,] 1.0000000 1.0000000 1.0000000 1.0000000
##  [2,] 0.9565217 0.9565217 0.9565217 0.9565217
##  [3,] 0.9565217 0.9565217 0.9565217 0.9565217
##  [4,] 0.9565217 0.9565217 0.9565217 0.9565217
##  [5,] 0.7173913 0.7173913 0.7173913 0.7173913
##  [6,] 0.7173913 0.7173913 0.7173913 0.7173913
##  [7,] 0.7173913 0.7173913 0.7173913 0.7173913
##  [8,] 0.6277174 0.6277174 0.6277174 0.6277174
##  [9,] 0.5230978 0.5230978 0.5230978 0.5230978
## [10,] 0.3923234 0.3923234 0.3923234 0.3923234

检查速度:

microbenchmark(f_loop(B, C, D), f_vapply(B, C, D), f_compiled(B, C, D))
## Unit: microseconds
##                 expr    min      lq     mean  median      uq    max neval cld
##      f_loop(B, C, D) 52.804 54.8075 57.34588 56.5420 58.4615 83.843   100   c
##    f_vapply(B, C, D) 38.677 41.5055 43.21231 42.8825 44.1525 65.355   100  b 
##  f_compiled(B, C, D) 17.095 18.2775 20.55372 20.1770 21.4710 66.407   100 a  

我们也可以尝试使用与您提到的较大尺寸相似的矢量(注意结果的单位变化):

## Make the vector larger for benchmark
B <- rep(B, 100)  # M = 2500
C <- rep(C, 100)  # N = 1000
D <- rep(D, 100)  # M = 2500

microbenchmark(f_loop(B, C, D), f_vapply(B, C, D), f_compiled(B, C, D))
## Unit: milliseconds
##                 expr       min       lq     mean    median        uq      max neval cld
##      f_loop(B, C, D) 24.380069 24.85061 25.99855 25.839282 25.952433 62.75721   100   b
##    f_vapply(B, C, D) 23.543749 24.18427 25.34881 25.015859 25.179924 62.60746   100   b
##  f_compiled(B, C, D)  1.976611  2.01403  2.06750  2.032864  2.057594  3.13658   100  a

<强> 编辑:

我意识到如果你总是想要D所拥有的C[j]>=D的最大索引,当然从数组的末尾开始循环D会更有意义。 ,并在找到第一个实例后立即退出(而不是遍历整个数组)。

这是对我上面编写的Fortran代码的一个小调整,利用了这一点。

loopcode <-
"  integer i, j, k
   do j = 1, n
      k = 0
      do i = m, 1, -1
         if (C(j) >= D(i)) then
            k = i
            exit
         end if
      end do
      A(j) = B(k)
   end do
"

我不会将它包含在基准测试中,因为它将更加依赖于实际的数据点。但很明显,最坏情况的行为与前一个循环相同(例如,如果感兴趣的索引发生在开头,D完全循环)并且最佳情况行为几乎完全消除了循环{{ 1}}(例如,如果条件保持在数组的末尾)。

答案 2 :(得分:1)

如果您渴望立即得到答案,请跳至结论。我为您提供单行R代码,效率最高。有关详细信息/想法,请阅读以下内容。

代码重塑和问题重新定义

当OP询问以下循环的矢量化时:

for(j in 1:N) A[j, 1] <- B[max(which(C[j] >= D))]

我要做的第一件事就是把它变成一个很好的版本:

## stage 1: index computation (need vectorization)
id <- integer(N); for(j in 1:N) id[j] <- max(which(D <= C[j]))
## stage 2: shuffling (readily vectorized)
A[, 1] <- B[id]

现在我们看到只有阶段1需要进行矢量化。这个阶段基本上做了以下几点:

        D[1]  D[2]  D[3]  ...  D[M]
C[1]
C[2]
C[3]
 .
 .
C[N]

对于每一行j,找到k(j)中的截止位置DD[k(j) + 1], D[k(j) + 2], ..., D[M] > C[j]

基于排序的高效算法

实际上有一种有效的算法可以做到这一点:

  1. 按升序排序CCC(记录排序索引iC,以便C[iC] == CC
  2. 按升序排序DDD(记录排序索引iD,以便D[iD] == DD
  3. 通过排序,我们大大降低了工作的复杂性。

    如果数据未排序,那么我们必须显式扫描所有元素:D[1], D[2], ..., D[M]以便决定k(j)。因此,每行的费用为O(M),因此费用总计为O(MN)

    但是,如果数据已排序,那么我们只需要执行以下操作:

    j = 1: search `D[1], D[2], ..., D[k(1)]`, till `D[k(1) + 1] > C[1]`;
    j = 2: search `D[k(1) + 1], D[k(1)+2], ..., D[k(2)]`, till `D[k(2) + 1] > C[2]`;
    ...
    

    对于每一行,仅应用部分搜索,整体复杂度仅为O(M),即D向量仅触摸一次,而不是{{1在简单的实现中。因此,排序后,算法的速度提高了N倍!对于较大的NM,这是一个巨大的差异!正如您在其他评论中所说的那样,此代码将被调用数百万次,然后我们肯定需要N算法而不是O(M)算法。

    另请注意,此方法的内存成本为O(MN),即我们只将两个向量连接在一起,而不是将其扩展为O(M + N)矩阵。因此,这种存储节省也很明显。

    事实上,我们可以通过将此比较问题转换为匹配问题来进一步采取,这更容易在R中进行矢量化。

    M-by-N

    要了解这项工作的原因,请考虑另一种表述方式:

    ## version 1:
    CCDD <- c(CC, DD) ## combine CC and DD
    CCDD <- sort(CCDD, decreasing = TRUE)  ## sort into descending order
    id0 <- M + N - match(CC, CCDD) + 1
    id <- id0 - 1:N
    

    现在,下图说明了## version 2: CCDD <- c(CC, DD) ## combine CC and DD CCDD <- sort(CCDD) ## sort into ascending order id0 <- match(CC, CCDD) id <- id0 - 1:N 向量的外观:

    CCDD

    因此,CCDD: D[1] D[2] C[1] D[3] C[2] C[3] D[4] D[5] D[6] C[4] ..... id0: 3 5 6 10 ..... id : 2 3 3 6 ..... 给出:CCDD[id],恰好是不超过D[2], D[3], D[3], D[6], ....的最后一个元素,因此,C[1], C[2]. C[3], C[4], ....只是我们想要的索引!

    然后人们可能想知道为什么我建议做&#34;版本1&#34;而不是&#34;版本2&#34;。因为当id,&#34;版本2&#34;将给出错误的结果,因为CCDD将采用匹配的第一个元素,忽略以后的匹配。因此,不是从左到右匹配(在升序索引中),我们必须从右到左匹配(在降序索引中)。

    使用OP的数据

    考虑到这一点,我开始查看OP的数据。 现在令人惊讶的是,OP的数据已经排序

    match()

    此外,OP已合并C <- c(0.10607, 0.14705, 0.43607, 0.56587, 0.76203, 0.95657, 1.03524, 1.22956, 1.39074, 2.36452) D <- c(0.10607, 0.13980, 0.14571, 0.14705, 0.29412, 0.33693, 0.43607, 0.53968, 0.56587, 0.58848, 0.64189, 0.65475, 0.75518, 0.76203, 0.95657, 1.03524, 1.05454, 1.18164, 1.22956, 1.23760, 1.39074, 1.87604, 2.36452, 2.89497, 4.42393) M <- length(D); N <- length(C) is.unsorted(C) # FALSE is.unsorted(D) #FALSE C

    D

    似乎OP和我在效率方面有相同的想法。据推测OP曾经有一个较短的all(C %in% D) # TRUE 向量,而他提供的D向量确实是我上面提到的D向量!

    现在,在这种情况下,事情很简单:我们只做一个单行

    CCDD

    注意我放id <- M - match(C, rev(D)) + 1 因为OP按升序排序rev()所以我需要反转它。这一行可能看起来与版本1&#34;非常不同。代码,但没有错。请注意,此处使用的D实际上是&{34;版本1&#34;中的D。代码,CCDD这里真的是M。此外,由于我们对M + N的定义不同,因此无需从1:N中减去id

    检查结果

    现在,简单的R-loop给出:

    D

    好吧,我们的单行矢量化代码给出了:

    id <- integer(N); for(j in 1:N) id[j] <- max(which(D <= C[j]))
    id
    # [1]  1  4  7  9 14 15 16 19 21 23
    

    完美匹配,因此我们正在做正确的事。

    结论

    所以,Laurbert,这就是你想要的答案:

    id <- M - match(C, rev(D)) + 1
    id
    # [1]  1  4  7  9 14 15 16 19 21 23