这绝对是一个扩展性较差的问题。在对(比如)as.matrix、apply、asplit、data.table::transpose 等进行了一些基准测试之后,我还没有找到一个可以合理扩展超过 50K 行的。
最直接的(对我来说,是可口的,性能方面的)路径是最直接的:
a1[, result := +(z1 == z2 | z1 == x2 | x1 == z2 | x1 == x2)]
但是,NA 值会失败,因此我们需要更加小心。玩了一会儿,我觉得这个辅助函数是最直接的,因为它完全符合我们需要的逻辑,并且是完全向量化的:
`%=%` <- function(a, b) !is.na(a) & !is.na(b) & a == b
a1[, +(z1 %=% z2 | x1 %=% z2 | z1 %=% x2 | x1 %=% x2)]
# [1] 1 0 0 0 1
(我故意避免使用`%==%`,因为我在其他包中看到它允许NA %==% NA 为真。如果您更喜欢使用`%==%`,请随意,或使用其他一些中缀运算符由您选择。它甚至不需要是中缀,这主要是为了美观。)
问题是当我们在每个组中有更多变量时如何自动执行此操作(由变量名称中的尾随数字定义)。为此,我建议我们手动创建表达式,然后对其进行评估/解析。
g1 = grep("1", names(a1), value = TRUE)
g2 = grep("2", names(a1), value = TRUE)
expr <- paste0(
"+(",
paste(outer(g1, g2, function(a, b) sprintf("%s %%=%% %s", a, b)), collapse = " | "),
")")
expr
# [1] "+(z1 %=% z2 | x1 %=% z2 | z1 %=% x2 | x1 %=% x2)"
这会产生预期的结果:
a1[, result2 := eval(parse(text = expr))]
# z1 x1 z2 x2 result result2
# <num> <char> <num> <num> <num> <int>
# 1: 1 3 2 3 1 1
# 2: NA 4 NA 5 0 0
# 3: 3 5 4 4 0 0
# 4: 4 6 5 7 0 0
# 5: 5 7 6 5 1 1
这可以很好地垂直缩放。如果 a1 是 5 行,那么复制它 1e4 次会产生 50K 行,等等。
a1e4 <- rbindlist(replicate(1e4, a1, simplify=FALSE)) # 50K rows
system.time(a1e4[, result2 := eval(parse(text = expr))])
# user system elapsed
# 0.06 0.00 0.06
a1e5 <- rbindlist(replicate(1e5, a1, simplify=FALSE)) # 500K
system.time(a1e5[, result2 := eval(parse(text = expr))])
# user system elapsed
# 0.7 0.0 0.7
a1e6 <- rbindlist(replicate(1e6, a1, simplify=FALSE)) # 5M
system.time(a1e6[, result2 := eval(parse(text = expr))])
# user system elapsed
# 7.16 0.06 7.22
它似乎是线性缩放的,这意味着另外 4 倍的行应该在大约 30 秒内解决。
如果每个组有更多变量呢? (即,水平缩放)
set.seed(42)
b1 <- copy(a1[,1:4])[, c("s1","t1","u1","v1","w1","y1", "s2","t2","u2","v2","w2","y2") :=
replicate(12, sample(9, .N, replace = TRUE), simplify = FALSE)]
b1
# z1 x1 z2 x2 s1 t1 u1 v1 w1 y1 s2 t2 u2 v2 w2 y2
# <num> <char> <num> <num> <int> <int> <int> <int> <int> <int> <int> <int> <int> <int> <int> <int>
# 1: 1 3 2 3 1 2 9 9 4 8 6 8 1 2 2 1
# 2: NA 4 NA 5 5 1 5 9 2 6 2 2 5 4 7 1
# 3: 3 5 4 4 1 8 4 4 8 8 5 3 2 3 6 7
# 4: 4 6 5 7 9 7 2 5 3 4 4 8 6 6 8 4
# 5: 5 7 6 5 4 4 3 5 1 4 2 7 6 5 5 9
bg1 = grep("1", names(b1), value = TRUE)
bg2 = grep("2", names(b1), value = TRUE)
bexpr <- paste0(
"+(",
paste(outer(bg1, bg2, function(a, b) sprintf("%s %%=%% %s", a, b)), collapse = " | "),
")")
bexpr
# [1] "+(z1 %=% z2 | x1 %=% z2 | s1 %=% z2 | t1 %=% z2 | u1 %=% z2 | v1 %=% z2 | w1 %=% z2 | y1 %=% z2 | z1 %=% x2 | x1 %=% x2 | s1 %=% x2 | t1 %=% x2 | u1 %=% x2 | v1 %=% x2 | w1 %=% x2 | y1 %=% x2 | z1 %=% s2 | x1 %=% s2 | s1 %=% s2 | t1 %=% s2 | u1 %=% s2 | v1 %=% s2 | w1 %=% s2 | y1 %=% s2 | z1 %=% t2 | x1 %=% t2 | s1 %=% t2 | t1 %=% t2 | u1 %=% t2 | v1 %=% t2 | w1 %=% t2 | y1 %=% t2 | z1 %=% u2 | x1 %=% u2 | s1 %=% u2 | t1 %=% u2 | u1 %=% u2 | v1 %=% u2 | w1 %=% u2 | y1 %=% u2 | z1 %=% v2 | x1 %=% v2 | s1 %=% v2 | t1 %=% v2 | u1 %=% v2 | v1 %=% v2 | w1 %=% v2 | y1 %=% v2 | z1 %=% w2 | x1 %=% w2 | s1 %=% w2 | t1 %=% w2 | u1 %=% w2 | v1 %=% w2 | w1 %=% w2 | y1 %=% w2 | z1 %=% y2 | x1 %=% y2 | s1 %=% y2 | t1 %=% y2 | u1 %=% y2 | v1 %=% y2 | w1 %=% y2 | y1 %=% y2)"
呃,这看起来很糟糕,但性能非常好,每组 8 个变量:
b1e4 <- rbindlist(replicate(1e4, b1, simplify=FALSE))
system.time(b1e4[, result2 := eval(parse(text = bexpr))])
# user system elapsed
# 0.11 0.00 0.10
b1e5 <- rbindlist(replicate(1e5, b1, simplify=FALSE))
system.time(b1e5[, result2 := eval(parse(text = bexpr))])
# user system elapsed
# 1.03 0.00 1.03
b1e6 <- rbindlist(replicate(1e6, b1, simplify=FALSE))
system.time(b1e6[, result2 := eval(parse(text = bexpr))])
# user system elapsed
# 11.72 0.51 12.25