确定元素是否存在于向量

时间:2015-10-31 15:15:00

标签: r performance optimization vector

我有几种算法取决于确定元素是否存在于向量中的效率。在我看来,%in%(相当于is.element())应该是最有效的,因为它只返回一个布尔值。在测试了几种方法之后,令我惊讶的是,这些方法到目前为止效率最低。以下是我的分析(随着矢量大小的增加,结果变得更糟):

EfficiencyTest <- function(n, Lim) {

    samp1 <- sample(Lim, n)
    set1 <- sample(Lim, Lim)

    print(system.time(for(i in 1:n) {which(set1==samp1[i])}))
    print(system.time(for(i in 1:n) {samp1[i] %in% set1}))
    print(system.time(for(i in 1:n) {is.element(samp1[i], set1)}))
    print(system.time(for(i in 1:n) {match(samp1[i], set1)}))
    a <- system.time(set1 <- sort(set1))
    b <- system.time(for (i in 1:n) {BinVecCheck(samp1[i], set1)})
    print(a+b)
}

> EfficiencyTest(10^3, 10^5)
user  system elapsed 
0.29    0.11    0.40 
user  system elapsed 
19.79    0.39   20.21 
user  system elapsed 
19.89    0.53   20.44 
user  system elapsed 
20.04    0.28   20.33 
user  system elapsed 
0.02    0.00    0.03 

BinVecCheck是我写的二进制搜索算法,返回TRUE / FALSE。请注意,我包含使用最终方法对向量进行排序所需的时间。这是二进制搜索的代码:

BinVecCheck <- function(tar, vec) {      
    if (tar==vec[1] || tar==vec[length(vec)]) {return(TRUE)}        
    size <- length(vec)
    size2 <- trunc(size/2)
    dist <- (tar - vec[size2])       
    if (dist > 0) {
        lower <- size2 - 1L
        upper <- size
    } else {
        lower <- 1L
        upper <- size2 + 1L
    }        
    while (size2 > 1 && !(dist==0)) {
        size2 <- trunc((upper-lower)/2)
        temp <- lower+size2
        dist <- (tar - vec[temp])
        if (dist > 0) {
            lower <- temp-1L
        } else {
            upper <- temp+1L
        }
    }       
    if (dist==0) {return(TRUE)} else {return(FALSE)}
}

平台信息:

> sessionInfo()
R version 3.2.1 (2015-06-18)
Platform: x86_64-w64-mingw32/x64 (64-bit)
Running under: Windows 7 x64 (build 7601) Service Pack 1

问题

是否有更有效的方法来确定元素是否存在于R中的向量中?例如,是否有与Python set函数等效的R函数,这大大改进了这种方法?此外,为什么%in%等等,即使与提供更多信息的which函数相比也是如此低效(不仅它确定了存在,而且它还给出了所有真实账户的索引) ?

4 个答案:

答案 0 :(得分:8)

我的测试并未证实你的所有主张,但似乎(?)是由于跨平台的差异(这使得问题更加神秘,并且可能值得考虑{{1}虽然可能不是因为下面的r-devel@r-project.org解决方案无论如何都占主导地位......)

fastmatch

这表示 n <- 10^3; Lim <- 10^5 set.seed(101) samp1 <- sample(Lim,n) set1 <- sample(Lim,Lim) library("rbenchmark") library("fastmatch") `%fin%` <- function(x, table) { stopifnot(require(fastmatch)) fmatch(x, table, nomatch = 0L) > 0L } benchmark(which=sapply(samp1,function(x) which(set1==x)), infun=sapply(samp1,function(x) x %in% set1), fin= sapply(samp1,function(x) x %fin% set1), brc= sapply(samp1,BinVecCheck,vec=sort(set1)), replications=20, columns = c("test", "replications", "elapsed", "relative")) ## test replications elapsed relative ## 4 brc 20 0.871 2.329 ## 3 fin 20 0.374 1.000 ## 2 infun 20 6.480 17.326 ## 1 which 20 10.634 28.433 的速度约为%in%的两倍 - 您的which函数的效果提高了7倍,但来自hereBinVecCheck解决方案得到了fastmatch另一个因素是2.我不知道专门的Rcpp实现是否可以做得更好...... 事实上,即使运行代码,我也会得到不同的答案:

##    user  system elapsed   (which)
##   0.488   0.096   0.586 
##    user  system elapsed   (%in%) 
##   0.184   0.132   0.315 
##    user  system elapsed  (is.element)
##   0.188   0.124   0.313 
##    user  system elapsed  (match)
##   0.148   0.164   0.312 
##    user  system elapsed  (BinVecCheck)
##   0.048   0.008   0.055 

更新:on r-devel Peter Dalgaard通过指向R NEWS条目解释平台差异(这是R版本差异,而不是操作系统差异):< / p>

  感谢Haverty的PR#16491,当match(x, table)的长度为1且不可比的不变时,

x更快,有时甚至是一个数量级。

sessionInfo()
## R Under development (unstable) (2015-10-23 r69563)
## Platform: i686-pc-linux-gnu (32-bit)
## Running under: Ubuntu precise (12.04.5 LTS)

答案 1 :(得分:3)

%in%只是匹配的糖,定义为:

"%in%" <- function(x, table) match(x, table, nomatch = 0) > 0

matchwhich都是.Internal()调用的低级(编译C)函数。您可以使用pryr包实际查看源代码:

install.packages("pryr")
library(pryr)
pryr::show_c_source(.Internal(which(x)))
pryr::show_c_source(.Internal(match(x, table, nomatch, incomparables)))

您将被指向this page以及this page匹配。 which不会执行match执行的任何投射,检查等操作。这可能解释了它在测试中的更高性能(但我自己没有测试过你的结果)。

答案 2 :(得分:2)

经过多天研究这个主题,我发现确定存在的最快方法取决于被测元素的数量。根据@ ben-bolker给出的答案,%fin%看起来像是明确的赢家。当被测元素的数量(samp1中的所有元素)与向量(set1)的大小相比较时,情况似乎就是这种情况。在我们进一步讨论之前,让我们看看上面的二进制搜索算法。

首先,原始算法中的第一行评估为TRUE的可能性极低,为什么每次检查它?

if (tar==vec[1] || tar==vec[size]) {return(TRUE)}

相反,我将此语句放在最后的else语句中。

其次,每次确定向量的大小是多余的,特别是当我提前知道测试向量的长度(set1)时。所以,我添加size作为算法的参数,并简单地将其作为变量传递。以下是修改后的二进制搜索代码。

ModifiedBinVecCheck <- function(tar, vec, size) {
    size2 <- trunc(size/2)
    dist <- (tar - vec[size2])
    if (dist > 0) {
        lower <- size2 - 1L
        upper <- size
    } else {
        lower <- 1L
        upper <- size2 + 1L
    }
    while (size2 > 1 && !(dist==0)) {
        size2 <- trunc((upper-lower)/2)
        temp <- lower+size2
        dist <- (tar - vec[temp])
        if (dist > 0) {
            lower <- temp-1L
        } else {
            upper <- temp+1L
        }
    }
    if (dist==0) {
        return(TRUE)
    } else {
        if (tar==vec[1] || tar==vec[size]) {return(TRUE)} else {return(FALSE)}
    }
}

众所周知,为了使用二进制搜索,必须对向量进行排序,这需要花费时间。 sort的默认排序方法是shell,可用于所有数据类型,但缺点(一般来说)比quick方法慢quick只能用于双打或整数)。使用quick作为我的排序方法(因为我们处理数字)与修改后的二进制搜索相结合,我们得到了显着的性能提升(根据具体情况从旧的二进制搜索)。应该注意的是,fmatch仅在数据类型为整数,实数或字符时才会在match上进行改进。

现在,让我们看看一些不同大小 n 的测试用例。

案例1 n = 10^3&amp; Lim = 10^6,所以n to Lim ratio is 1:1000):

n <- 10^3; Lim <- 10^6
set.seed(101)
samp1 <- sample(Lim,n)
set1 <- sample(Lim,Lim)
benchmark(fin= sapply(samp1,function(x) x %fin% set1),
            brc= sapply(samp1,ModifiedBinVecCheck,vec=sort(set1, method = "quick"),size=Lim),
            oldbrc= sapply(samp1,BinVecCheck,vec=sort(set1)),
            replications=10,
            columns = c("test", "replications", "elapsed", "relative"))
test replications elapsed relative
2    brc           10    0.97    4.217
1    fin           10    0.23    1.000
3 oldbrc           10    1.45    6.304

案例2 n = 10^4&amp; Lim = 10^6,所以n to Lim ratio is 1:100):

n <- 10^4; Lim <- 10^6
set.seed(101)
samp1 <- sample(Lim,n)
set1 <- sample(Lim,Lim)
benchmark(fin= sapply(samp1,function(x) x %fin% set1),
            brc= sapply(samp1,ModifiedBinVecCheck,vec=sort(set1, method = "quick"),size=Lim),
            oldbrc= sapply(samp1,BinVecCheck,vec=sort(set1)),
            replications=10,
            columns = c("test", "replications", "elapsed", "relative"))
test replications elapsed relative
2    brc           10    2.08    1.000
1    fin           10    2.16    1.038
3 oldbrc           10    2.57    1.236

案例3:n = 10^5&amp; Lim = 10^6,所以n to Lim ratio is 1:10):

n <- 10^5; Lim <- 10^6
set.seed(101)
samp1 <- sample(Lim,n)
set1 <- sample(Lim,Lim)
benchmark(fin= sapply(samp1,function(x) x %fin% set1),
            brc= sapply(samp1,ModifiedBinVecCheck,vec=sort(set1, method = "quick"),size=Lim),
            oldbrc= sapply(samp1,BinVecCheck,vec=sort(set1)),
            replications=10,
            columns = c("test", "replications", "elapsed", "relative"))
    test replications elapsed relative
2    brc           10   13.13    1.000
1    fin           10   21.23    1.617
3 oldbrc           10   13.93    1.061

案例4:n = 10^6&amp; Lim = 10^6,所以n to Lim ratio is 1:1):

n <- 10^6; Lim <- 10^6
set.seed(101)
samp1 <- sample(Lim,n)
set1 <- sample(Lim,Lim)
benchmark(fin= sapply(samp1,function(x) x %fin% set1),
            brc= sapply(samp1,ModifiedBinVecCheck,vec=sort(set1, method = "quick"),size=Lim),
            oldbrc= sapply(samp1,BinVecCheck,vec=sort(set1)),
            replications=10,
            columns = c("test", "replications", "elapsed", "relative"))
   test replications elapsed relative
2    brc           10  124.61    1.000
1    fin           10  214.20    1.719
3 oldbrc           10  127.39    1.022


正如您所看到的那样, n 相对于 Lim 变得更大,二元搜索的效率(两者都是他们)开始占主导地位。在案例1中,%fin%比修改后的二进制搜索快4倍,在Case2中几乎没有差异,在案例3中我们真正开始看到二进制搜索优势,而在案例4中,修改后的二进制搜索是几乎是%fin%的两倍。

因此,要回答“哪种方法更快?”的问题,这取决于。 %fin%对于测试向量的少量元素检查更快,ModifiedBinVecCheck对于测试向量的大量元素检查更快。

答案 3 :(得分:1)

如果您确定any( x == "foo" )没有NA,那么

x应该足够快。如果您可能有NAs,则R 3.3的“%in%”速度会有所帮助。

对于二进制搜索,请在滚动自己之前查看findInterval。除非x是常量和排序的,否则这听起来不像二进制搜索的工作。