对于两个逻辑向量,x
和y
,长度为> 1E8,计算2x2交叉表的最快方法是什么?
我怀疑答案是用C / C ++编写它,但是我想知道R中是否有一些关于这个问题已经非常聪明的事情,因为它并不罕见。
示例代码,对于300M条目(如果3E8太大,可以让N = 1E8;我选择的总大小不到2.5GB(2.4GB)。我的目标密度为0.02,只是为了让它更有趣(可以使用稀疏向量,如果这有帮助,但类型转换可能需要时间)。
set.seed(0)
N = 3E8
p = 0.02
x = sample(c(TRUE, FALSE), N, prob = c(p, 1-p), replace = TRUE)
y = sample(c(TRUE, FALSE), N, prob = c(p, 1-p), replace = TRUE)
一些明显的方法:
table
bigtabulate
sum(x & y)
)data.table
parallel
包中的multicore
(或新parallel
包)我已经尝试了前三个选项(请参阅我的回答),但我觉得必须有更好更好的东西。
我发现table
工作得很慢。 bigtabulate
对于一对逻辑向量来说似乎有些过分。最后,进行vanilla逻辑运算看起来像是一个kludge,并且它会查看每个向量太多次(3X?7X?),更不用说它在处理期间填充了大量额外的内存,这是一个巨大的时间浪费。 / p>
向量乘法通常是一个坏主意,但是当向量稀疏时,可能会因为存储它而获得优势,然后使用向量乘法。
如果这将显示制表功能的任何有趣行为,请随意更改N
和p
。 :)
更新1.我的第一个答案给出了三种天真方法的时间,这是相信table
缓慢的基础。然而,要意识到的关键是“逻辑”方法效率极低。看看它在做什么:
sum
)不仅如此,它甚至没有编译或并行化。然而,它仍然比table
更好。请注意,bigtabulate
,额外的类型转化(1 * cbind...
)仍然胜过table
。
更新2.以免任何人指出R中的逻辑向量支持NA
,并且这将成为系统中这些交叉表的一个棘手问题(在大多数情况下都是如此),我应该指出我的矢量来自is.na()
或is.finite()
。 :)我一直在调试NA
和其他非有限值 - they've been a headache for me recently。如果您不知道所有参赛作品是否都是NA
,您可以使用any(is.na(yourVector))
进行测试 - 在您采纳本Q& A中出现的一些想法之前,这是明智的。
更新3. Brandon Bertelsen在评论中提出了一个非常合理的问题:为什么在创建子样本(初始集合,毕竟是样本;-))时可能会使用如此多的数据交叉制表?不要过于偏向统计数据,但数据来自两个变量TRUE
观察非常罕见的情况。一个是数据异常的结果,另一个是由于代码中可能存在的错误(可能的错误,因为我们只看到计算结果 - 将变量x
视为“垃圾输入”,而y
为“Garbage Out”。结果,问题在于代码引起的输出问题是否仅仅是数据异常的情况,还是有其他一些好数据变坏的情况?(这就是我问的原因)关于stopping when a NaN
, NA
, or Inf
is encountered的问题。)
这也解释了为什么我的示例TRUE
值的概率很低;这些确实发生的时间远远少于0.1%。
这是否表明了不同的解决方案路径?是的:它表明我们可以使用两个指数(即每组中TRUE
的位置)和计数集交叉点。我避免设置交叉点,因为我被Matlab烧了一段时间(是的,这是R,但是请跟我一起),它首先会在一个交集之前对一个集合的元素进行排序。 (我含糊地回忆起复杂性更令人尴尬的是:比如O(n^2)
而不是O(n log n)
。)
答案 0 :(得分:11)
如果您正在对大型逻辑向量进行大量操作,请查看bit包。通过将布尔值存储为真正的1位布尔值,可以节省大量内存。
这对table
没有帮助;它实际上使它变得更糟,因为由于它的构造方式,位向量中有更多的唯一值。但它真的有助于逻辑比较。
# N <- 3e7
require(bit)
xb <- as.bit(x)
yb <- as.bit(y)
benchmark(replications = 1, order = "elapsed",
bit = {res <- func_logical(xb,yb)},
logical = {res <- func_logical(x,y)}
)
# test replications elapsed relative user.self sys.self user.child sys.child
# 1 bit 1 0.129 1.00000 0.132 0.000 0 0
# 2 logical 1 3.677 28.50388 2.684 0.928 0 0
答案 1 :(得分:10)
以下是逻辑方法table
和bigtabulate
的结果,对于N = 3E8:
test replications elapsed relative user.self sys.self
2 logical 1 23.861 1.000000 15.36 8.50
3 bigtabulate 1 36.477 1.528729 28.04 8.43
1 table 1 184.652 7.738653 150.61 33.99
在这种情况下,table
是一场灾难。
为了比较,这里是N = 3E6:
test replications elapsed relative user.self sys.self
2 logical 1 0.220 1.000000 0.14 0.08
3 bigtabulate 1 0.534 2.427273 0.45 0.08
1 table 1 1.956 8.890909 1.87 0.09
此时,似乎编写自己的逻辑函数是最好的,即使它滥用sum
,并且多次检查每个逻辑向量。我还没有尝试编译这些函数,但这应该会产生更好的结果。
更新1 如果我们提供已经整数的bigtabulate
值,即如果我们在bigtabulate之外进行类型转换1 * cbind(v1,v2)
,则N = 3E6倍数为1.80而不是2.4。相对于“逻辑”方法的N = 3E8倍数仅为1.21,而不是1.53。
更新2
正如约书亚·乌尔里希所指出的那样,转换为位向量是一项重大改进 - 我们正在分配和移动少量数据:R的逻辑向量每个条目消耗4个字节(“为什么?”,你可能会问...... 。Well, I don't know, but an answer may turn up here.),而一个位向量每个条目消耗一位,即1/32的数据。因此,x
消耗1.2e9字节,而xb
(下面代码中的位版本)仅消耗3.75e7字节。
我从更新的基准测试中删除了table
和bigtabulate
个变体(N = 3e8)。请注意,logicalB1
假设数据已经是位向量,而logicalB2
是相同的操作,并且对类型转换进行了惩罚。由于我的逻辑向量是对其他数据的操作结果,因此我没有从位向量开始的好处。尽管如此,要支付的罚款相对较小。 [“logical3”系列仅执行3次逻辑运算,然后进行减法运算。由于它是交叉制表,我们知道总数,正如DWin所说的那样。]
test replications elapsed relative user.self sys.self
4 logical3B1 1 1.276 1.000000 1.11 0.17
2 logicalB1 1 1.768 1.385580 1.56 0.21
5 logical3B2 1 2.297 1.800157 2.15 0.14
3 logicalB2 1 2.782 2.180251 2.53 0.26
1 logical 1 22.953 17.988245 15.14 7.82
我们现在加快了这个速度只需1.8-2.8秒,即使有很多严重的低效率。 毫无疑问在1秒内完成此操作应该是可行的,其中包括以下一项或多项更改:C代码,编译和多核处理。在所有3(或4)个不同的逻辑操作可以独立完成之后,即使这仍然是浪费计算周期。
最相似的最佳挑战者logical3B2
,比table
快80倍。它比天真的逻辑操作快约10倍。它仍然有很大的改进空间。
以下是产生上述代码的代码。 注意我建议您注释掉一些操作或向量,除非您有大量的RAM - 创建x
,x1
和xb
,使用相应的y
个对象,会占用相当多的内存。
另外,请注意:我应该使用1L
作为bigtabulate
的整数乘数,而不只是1
。在某些时候,我会重新运行此更改,并建议对使用bigtabulate
方法的任何人进行更改。
library(rbenchmark)
library(bigtabulate)
library(bit)
set.seed(0)
N <- 3E8
p <- 0.02
x <- sample(c(TRUE, FALSE), N, prob = c(p, 1-p), replace = TRUE)
y <- sample(c(TRUE, FALSE), N, prob = c(p, 1-p), replace = TRUE)
x1 <- 1*x
y1 <- 1*y
xb <- as.bit(x)
yb <- as.bit(y)
func_table <- function(v1,v2){
return(table(v1,v2))
}
func_logical <- function(v1,v2){
return(c(sum(v1 & v2), sum(v1 & !v2), sum(!v1 & v2), sum(!v1 & !v2)))
}
func_logicalB <- function(v1,v2){
v1B <- as.bit(v1)
v2B <- as.bit(v2)
return(c(sum(v1B & v2B), sum(v1B & !v2B), sum(!v1B & v2B), sum(!v1B & !v2B)))
}
func_bigtabulate <- function(v1,v2){
return(bigtabulate(1*cbind(v1,v2), ccols = c(1,2)))
}
func_bigtabulate2 <- function(v1,v2){
return(bigtabulate(cbind(v1,v2), ccols = c(1,2)))
}
func_logical3 <- function(v1,v2){
r1 <- sum(v1 & v2)
r2 <- sum(v1 & !v2)
r3 <- sum(!v1 & v2)
r4 <- length(v1) - sum(c(r1, r2, r3))
return(c(r1, r2, r3, r4))
}
func_logical3B <- function(v1,v2){
v1B <- as.bit(v1)
v2B <- as.bit(v2)
r1 <- sum(v1B & v2B)
r2 <- sum(v1B & !v2B)
r3 <- sum(!v1B & v2B)
r4 <- length(v1) - sum(c(r1, r2, r3))
return(c(r1, r2, r3, r4))
}
benchmark(replications = 1, order = "elapsed",
#table = {res <- func_table(x,y)},
logical = {res <- func_logical(x,y)},
logicalB1 = {res <- func_logical(xb,yb)},
logicalB2 = {res <- func_logicalB(x,y)},
logical3B1 = {res <- func_logical3(xb,yb)},
logical3B2 = {res <- func_logical3B(x,y)}
#bigtabulate = {res <- func_bigtabulate(x,y)},
#bigtabulate2 = {res <- func_bigtabulate2(x1,y1)}
)
答案 2 :(得分:4)
以下是使用Rcpp糖的答案。
N <- 1e8
x <- sample(c(T,F),N,replace=T)
y <- sample(c(T,F),N,replace=T)
func_logical <- function(v1,v2){
return(c(sum(v1 & v2), sum(v1 & !v2), sum(!v1 & v2), sum(!v1 & !v2)))
}
library(Rcpp)
library(inline)
doCrossTab1 <- cxxfunction(signature(x="integer", y = "integer"), body='
Rcpp::LogicalVector Vx(x);
Rcpp::LogicalVector Vy(y);
Rcpp::IntegerVector V(4);
V[0] = sum(Vx*Vy);
V[1] = sum(Vx*!Vy);
V[2] = sum(!Vx*Vy);
V[3] = sum(!Vx*!Vy);
return( wrap(V));
'
, plugin="Rcpp")
system.time(doCrossTab1(x,y))
require(bit)
system.time(
{
xb <- as.bit(x)
yb <- as.bit(y)
func_logical(xb,yb)
})
导致:
> system.time(doCrossTab1(x,y))
user system elapsed
1.067 0.002 1.069
> system.time(
+ {
+ xb <- as.bit(x)
+ yb <- as.bit(y)
+ func_logical(xb,yb)
+ })
user system elapsed
1.451 0.001 1.453
所以,我们可以通过比特包加快一点,但我对时代的竞争感到惊讶。
更新:为了纪念Iterator,这里有一个Rcpp迭代器解决方案:
doCrossTab2 <- cxxfunction(signature(x="integer", y = "integer"), body='
Rcpp::LogicalVector Vx(x);
Rcpp::LogicalVector Vy(y);
Rcpp::IntegerVector V(4);
V[0]=V[1]=V[2]=V[3]=0;
LogicalVector::iterator itx = Vx.begin();
LogicalVector::iterator ity = Vy.begin();
while(itx!=Vx.end()){
V[0] += (*itx)*(*ity);
V[1] += (*itx)*(!*ity);
V[2] += (!*itx)*(*ity);
V[3] += (!*itx)*(!*ity);
itx++;
ity++;
}
return( wrap(V));
'
, plugin="Rcpp")
system.time(doCrossTab2(x,y))
# user system elapsed
# 0.780 0.001 0.782
答案 3 :(得分:2)
另一种策略是考虑设置交叉点,使用TRUE
值的索引,利用样本非常偏向(即大多数FALSE
)。
为此,我介绍了func_find01
以及使用bit
包(func_find01B
)的翻译;以上答案中未出现的所有代码都粘贴在下面。
我重新运行了完整的N = 3e8评估,除了忘记使用func_find01B
;在第二轮中,我重新采用了更快的方法。
test replications elapsed relative user.self sys.self
6 logical3B1 1 1.298 1.000000 1.13 0.17
4 logicalB1 1 1.805 1.390601 1.57 0.23
7 logical3B2 1 2.317 1.785054 2.12 0.20
5 logicalB2 1 2.820 2.172573 2.53 0.29
2 find01 1 6.125 4.718798 4.24 1.88
9 bigtabulate2 1 22.823 17.583205 21.00 1.81
3 logical 1 23.800 18.335901 15.51 8.28
8 bigtabulate 1 27.674 21.320493 24.27 3.40
1 table 1 183.467 141.345917 149.01 34.41
只是“快速”方法:
test replications elapsed relative user.self sys.self
3 find02 1 1.078 1.000000 1.03 0.04
6 logical3B1 1 1.312 1.217069 1.18 0.13
4 logicalB1 1 1.797 1.666976 1.58 0.22
2 find01B 1 2.104 1.951763 2.03 0.08
7 logical3B2 1 2.319 2.151206 2.13 0.19
5 logicalB2 1 2.817 2.613173 2.50 0.31
1 find01 1 6.143 5.698516 4.21 1.93
因此,find01B
在不使用预转换位向量的方法中是最快的,以微小的余量(2.099秒对2.327秒)。 find02
来自哪里?我随后编写了一个使用预先计算的位向量的版本。现在这是最快的。
一般来说,“指数法”方法的运行时间可能会受到边际和“边际”的影响。联合概率。我怀疑当概率甚至更低时它会特别具有竞争力,但是必须知道先验,或通过子样本。
更新1.我还使用tabulate()
而不是table()
计算了Josh O'Brien的建议。结果在12秒后,约为find01
的2倍bigtabulate2
和约 user system elapsed
7.670 5.140 12.815
的一半。现在最好的方法接近1秒,这也相对缓慢:
func_find01 <- function(v1, v2){
ix1 <- which(v1 == TRUE)
ix2 <- which(v2 == TRUE)
len_ixJ <- sum(ix1 %in% ix2)
len1 <- length(ix1)
len2 <- length(ix2)
return(c(len_ixJ, len1 - len_ixJ, len2 - len_ixJ,
length(v1) - len1 - len2 + len_ixJ))
}
func_find01B <- function(v1, v2){
v1b = as.bit(v1)
v2b = as.bit(v2)
len_ixJ <- sum(v1b & v2b)
len1 <- sum(v1b)
len2 <- sum(v2b)
return(c(len_ixJ, len1 - len_ixJ, len2 - len_ixJ,
length(v1) - len1 - len2 + len_ixJ))
}
func_find02 <- function(v1b, v2b){
len_ixJ <- sum(v1b & v2b)
len1 <- sum(v1b)
len2 <- sum(v2b)
return(c(len_ixJ, len1 - len_ixJ, len2 - len_ixJ,
length(v1b) - len1 - len2 + len_ixJ))
}
func_bigtabulate2 <- function(v1,v2){
return(bigtabulate(cbind(v1,v2), ccols = c(1,2)))
}
func_tabulate01 <- function(v1,v2){
return(tabulate(1L + 1L*x + 2L*y))
}
benchmark(replications = 1, order = "elapsed",
table = {res <- func_table(x,y)},
find01 = {res <- func_find01(x,y)},
find01B = {res <- func_find01B(x,y)},
find02 = {res <- func_find01B(xb,yb)},
logical = {res <- func_logical(x,y)},
logicalB1 = {res <- func_logical(xb,yb)},
logicalB2 = {res <- func_logicalB(x,y)},
logical3B1 = {res <- func_logical3(xb,yb)},
logical3B2 = {res <- func_logical3B(x,y)},
tabulate = {res <- func_tabulate(x,y)},
bigtabulate = {res <- func_bigtabulate(x,y)},
bigtabulate2 = {res <- func_bigtabulate2(x1,y1)}
)
代码:
{{1}}
答案 4 :(得分:1)
以下是Rcpp
的答案,仅列出那些不同时为0
的条目。我怀疑必须有几种方法可以改善这一点,因为这种情况异常缓慢;这也是我对Rcpp
的第一次尝试,因此可能会出现一些与移动数据相关的效率低下的问题。我写了一个有目的的普通香草的例子,应该让其他人证明如何改进它。
library(Rcpp)
library(inline)
doCrossTab <- cxxfunction(signature(x="integer", y = "integer"), body='
Rcpp::IntegerVector Vx(x);
Rcpp::IntegerVector Vy(y);
Rcpp::IntegerVector V(3);
for(int i = 0; i < Vx.length(); i++) {
if( (Vx(i) == 1) & ( Vy(i) == 1) ){ V[0]++; }
else if( (Vx(i) == 1) & ( Vy(i) == 0) ){ V[1]++; }
else if( (Vx(i) == 0) & ( Vy(i) == 1) ){ V[2]++; }
}
return( wrap(V));
', plugin="Rcpp")
N = 3E8
的时间效果:
user system elapsed
10.930 1.620 12.586
在我的第二个答案中,这个时间比func_find01B
多6倍。