TL;DR
对于大小相等但形状不同的整数矩阵(5e+06-by-2、5e+05-by-20、5000-by-2000),包含从 1 到 10 的整数,最快 base 答案测试是grouping/match,由@alexis_laz 在comment 中建议。最快的非base 答案是data.table::frank/match,尽管grouping/match 在所有情况下都相当,甚至在5000-by-2000 情况下的表现优于data.table 答案。
请注意,对于具有更大范围的双精度矩阵或整数矩阵,结果可能会有所不同,具体取决于data.table 可用的线程数。 [待办事项?]
背景
@MikaelJagan 的 asplit/match(<list>, <list>) answer 似乎是“使用基础 R 的优雅解决方案”。但是,?match 警告:
列表匹配可能非常缓慢,最好避免,除非在简单的情况下。
鉴于 OP 具有“具有许多行和列的矩阵”,我们想将 asplit/match(<list>, <list>) 答案的性能与其他 base 答案的性能进行比较:
- @Onyambu 的
paste/match(<chr>, <chr>)answer;
- @ThomasIsCoding 的
interaction/match(<int>, <int>)answer;
- @alexis_laz 的
grouping/match(<int>, <int>)answer。
我们将这些与一些非base 答案一起作为基准,我们将其用作参考点(认识到 OP 仅要求base):
- @MikaelJagan 的
Rcpp answer;
- @Henrik 的
data.table 回答:
- 将
which = TRUE 和mult = "first" 传递给[.data.table 的自联接;
- 两种基于行排名的方法,根据处理平局的方式不同:
-
frank(ties.method = "average")/match(<dbl>, <dbl>),
-
frank(ties.method = "dense")/match(<int>, <int>).
设置
library(microbenchmark)
library(data.table)
getDTthreads() # 4
f_asplit <- function(x) {
l <- asplit(x, 1L)
match(l, l) }
f_paste <- function(x) {
s <- do.call(paste, as.data.frame(x))
match(s, s) }
f_interaction <- function(x) {
z <- as.integer(interaction(as.data.frame(x)))
match(z, z) }
f_grouping <- function(x) {
g <- do.call(grouping, as.data.frame(x))
o <- order(g, method = "radix")
e <- attr(g, "ends")
z <- rep.int(seq_along(e), c(e[1L], e[-1L] - e[-length(e)]))[o]
match(z, z) }
f_join <- function(x) {
d <- as.data.table(x)
d[d, on = names(d), mult = "first", which = TRUE] }
f_frank_average <- function(x) {
d <- as.data.table(x)
r <- frank(d, ties.method = "average")
match(r, r) }
f_frank_dense <- function(x) {
d <- as.data.table(x)
r <- frank(d, ties.method = "dense")
match(r, r) }
Rcpp::sourceCpp('<copy source code from @MikaelJagan\'s answer here>')
基准测试
多行,少列
我们首先使用5e+06-by-2 整数矩阵评估性能:
set.seed(1L)
x <- matrix(sample(10L, size = 1e+07L, replace = TRUE), ncol = 2L)
microbenchmark(
f_asplit(x),
f_paste(x),
f_interaction(x),
f_grouping(x),
f_join(x),
f_frank_average(x),
f_frank_dense(x),
f_rcpp(x),
times = 10L,
check = "identical",
setup = gc(FALSE)
)
Unit: milliseconds
expr min lq mean median uq max neval
f_asplit(x) 17369.93905 18861.91195 19070.21298 19013.0180 19207.29194 22420.71085 10
f_paste(x) 502.63884 507.35077 509.01823 509.2443 511.72301 515.10083 10
f_interaction(x) 234.19311 236.52494 241.80098 238.7392 242.32923 259.75644 10
f_grouping(x) 182.25226 182.89358 187.09642 184.6124 187.10444 208.15532 10
f_join(x) 119.43460 120.86829 123.16607 122.9332 125.07169 128.44722 10
f_frank_average(x) 104.40150 107.53607 111.00268 108.5597 116.80375 121.83675 10
f_frank_dense(x) 86.60926 88.29555 91.42976 90.4716 92.32413 99.30659 10
f_rcpp(x) 459.02304 464.79855 472.43669 468.2492 470.25508 523.06734 10
f_asplit 比 base 替代品慢两个数量级。 f_grouping 是最快的 base 答案,但 f_frank_dense 快了大约 2 倍(也是最快的整体)。
更少的行,更多的列
上述结果并未推广到所有整数矩阵输入。例如,f_interaction 与 ncol(x) 的伸缩性很差:如果 x 的每一列都有 u 唯一元素,则可能的交互次数为 u^ncol(x)。
出于这个原因,我们执行了第二个基准测试,这次考虑的是一个行数较少 (5e+05) 和列数较多 (20) 的矩阵。
set.seed(1L)
x <- matrix(sample(10L, size = 1e+07L, replace = TRUE), ncol = 20L)
f_interaction 的初始测试导致内存分配错误,因此被排除在基准测试之外。
system.time(f_interaction(x))
Error: cannot allocate vector of size 7.5 Gb
Timing stopped at: 173.2 6.05 200.4
microbenchmark(
f_asplit(x),
f_paste(x),
## f_interaction(x),
f_grouping(x),
f_join(x),
f_frank_average(x),
f_frank_dense(x),
f_rcpp(x),
times = 10L,
check = "identical",
setup = gc(FALSE)
)
Unit: milliseconds
expr min lq mean median uq max neval
f_asplit(x) 5416.08762 5681.23523 5731.89246 5732.31779 5905.44517 5913.77141 10
f_paste(x) 592.92990 604.15083 629.31101 623.78679 637.81814 724.83871 10
f_grouping(x) 63.89522 64.14134 65.42723 65.11530 66.00557 68.06045 10
f_join(x) 340.73722 342.18096 353.35774 352.08861 359.88480 382.13480 10
f_frank_average(x) 69.90496 70.81840 72.29819 72.04409 73.11977 77.44347 10
f_frank_dense(x) 52.58033 53.33760 54.42029 54.01672 55.63532 56.99664 10
f_rcpp(x) 184096.21999 184816.36584 185774.76817 186218.58335 186696.31674 186781.24972 10
f_grouping 仍然是最快的base 答案。值得注意的是,它现在比f_paste 快了一个数量级,仅比f_frank_dense 慢一点。
行更少,列更多
我们执行了最终基准测试,排除了上一轮中最慢的答案(f_asplit 和 f_rcpp),现在考虑一个 5000×2000 整数矩阵:
set.seed(1L)
x <- matrix(sample(10L, size = 1e+07L, replace = TRUE), ncol = 2000L)
microbenchmark(
## f_asplit(x),
f_paste(x),
## f_interaction(x),
f_grouping(x),
f_join(x),
f_frank_average(x),
f_frank_dense(x),
## f_rcpp(x),
times = 10L,
check = "identical",
setup = gc(FALSE)
)
Unit: milliseconds
expr min lq mean median uq max neval
f_paste(x) 1067.47994 1075.45148 1083.17391 1080.72997 1089.74027 1102.45249 10
f_grouping(x) 19.24007 19.50026 19.86404 19.79002 20.25302 20.60127 10
f_join(x) 616.66706 621.29854 630.61460 628.16315 636.39097 650.16180 10
f_frank_average(x) 59.82007 61.41706 62.68610 62.99318 64.56520 64.88463 10
f_frank_dense(x) 58.03648 60.59857 63.50526 61.99278 66.03694 71.30638 10
现在 f_grouping 总体上最快,比 f_frank_dense 快大约 3 倍。