我正在尝试将给定的data.table子集化
DT <- data.table(
a = c(1:20),
b = (3:4),
c = (5:14),
d = c(1:4)
)
在一个函数内的参数是一个命名列表
param <- list(a = 1:10,
b = 2:3,
c = c(5, 7, 10))
我可能有点卡在这里,但我当然不希望实现像这样丑陋的东西。特别是因为它不是很有活力。
DT[(if (!is.null(param$a))
a %in% param$a
else
TRUE)
&
(if (!is.null(param$b))
b %in% param$b
else
TRUE)
&
(if (!is.null(param$c))
c %in% param$c
else
TRUE)
&
(if (!is.null(param$d))
d %in% param$d
else
TRUE)]
a b c d
1: 1 3 5 1
2: 3 3 7 3
任何想法如何在data.table或base R中以优雅的方式使用命名列表的名称来实现这一点,以使用关联值对data.table中的相应列进行子集化?谢谢!
编辑
我用一些答案执行了一个微基准测试:
func_4 <- function(myp, DT) {
myp = Filter(Negate(is.null), param)
exs = Map(function(var, val)
call("%in%", var, val),
var = sapply(names(myp), as.name),
val = myp)
exi = Reduce(function(x, y)
call("&", x, y), exs)
ex = call("[", x = as.name("DT"), i = exi)
# eval(as.call(c(as.list(ex))))
eval(ex)
}
microbenchmark(
(DT[do.call(pmin, Map(`%in%`, DT[, names(param), with = FALSE], param)) == 1L]),
(DT[rowSums(mapply(`%in%`, DT[, names(param), with = FALSE], param)) == length(param)]),
(DT[do.call(CJ, param), on = names(param), nomatch = NULL]),
(DT[expand.grid(param), on = names(param), nomatch = NULL]),
(DT[DT[, all(mapply(`%in%`, .SD, param)), by = 1:nrow(DT), .SDcols = names(param)]$V1]),
(func_4(myp = param, DT = DT)),
times = 200)
min lq mean median uq max neval
446.656 488.5365 565.5597 511.403 533.7785 7167.847 200
454.120 516.3000 566.8617 538.146 561.8965 1840.982 200
2433.450 2538.6075 2732.4749 2606.986 2704.5285 10302.085 200
2478.595 2588.7240 2939.8625 2642.311 2743.9375 10722.578 200
2648.707 2761.2475 3040.4926 2814.177 2903.8845 10334.822 200
3243.040 3384.6220 3764.5087 3484.423 3596.9140 14873.898 200
我们可以使用DT
中的names
在param
中选择列,将%in%
应用于包含列的每个列表元素,并仅选择所有值为TRUE
的行。
DT[which(rowSums(mapply(`%in%`, DT[, names(param), with = FALSE],
param)) == length(param)), ]
# a b c d
#1: 1 3 5 1
#2: 3 3 7 3
您可以使用CJ
中的data.table
(交叉连接)函数从列表中创建过滤表。
lookup <- do.call(CJ, param)
head(lookup)
# a b c
# 1: 1 2 5
# 2: 1 2 7
# 3: 1 2 10
# 4: 1 3 5
# 5: 1 3 7
# 6: 1 3 10
DT[
lookup,
on = names(lookup),
nomatch = NULL
]
# a b c d
# 1: 1 3 5 1
# 2: 3 3 7 3
请注意,nomatch = 0
表示在lookup
中不存在的DT
中的任何组合都不会返回一行。
使用Map
我们可以做到
DT[DT[, all(Map(`%in%`, .SD, param)), by = 1:nrow(DT)]$V1]
# a b c d
#1: 1 3 5 1
#2: 3 3 7 3
对于每一行,我们检查DT
中是否存在param
中的所有元素。
感谢@Frank,这可以改进
DT[DT[, all(mapply(`%in%`, .SD, param)), by = 1:nrow(DT), .SDcols=names(param)]$V1]
你可以使用call(fun, ...)
和as.name
构建表达式:
myp = Filter(Negate(is.null), param)
exs = Map(function(var, val) call("%in%", var, val), var = sapply(names(myp), as.name), val = myp)
exi = Reduce(function(x,y) call("&", x, y), exs)
ex = call("[", x = as.name("DT"), i = exi)
# DT[i = a %in% 1:10 & b %in% 2:3 & c %in% c(5, 7, 10)]
eval(ex)
# a b c d
# 1: 1 3 5 1
# 2: 3 3 7 3
通过正确编写调用,您可以利用data.table中“indices”的高效算法(请参阅包装晕影)。您还可以打开详细信息以获取有关在param$c
为int时将DT$c
指定为数字的效率低的注释:
> z <- as.call(c(as.list(ex), verbose=TRUE))
> eval(z)
Optimized subsetting with index 'c__b__a'
on= matches existing index, using index
Coercing double column i.'c' to integer to match type of x.'c'. Please avoid coercion for efficiency.
Starting bmerge ...done in 0.020sec
a b c d
1: 1 3 5 1
2: 3 3 7 3
也就是说,你应该使用c(5L, 7L, 10L)
。
如Nathan的回答一样,连接也使用索引,但如果param
很大,那么在prod(lengths(param))
的笛卡尔表上构建和连接将是昂贵的。
由于行操作,@ markus方法可能会很慢,所以这里有一个变体:
DT[do.call(pmin, Map(`%in%`, DT[, names(param), with=FALSE], param)) == 1L]
# a b c d
# 1: 1 3 5 1
# 2: 3 3 7 3
诀窍是all
的元素版本是pmin(...) == 1L
。同样,any
对应pmax(...) == 1L
。 (这就是为什么pany
/ pall
不包含在r-devel的对话中:http://r.789695.n4.nabble.com/There-is-pmin-and-pmax-each-taking-na-rm-how-about-psum-td4647841.html)