基于命名列表对data.table进行子集化

问题描述 投票:5回答:4

我正在尝试将给定的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
r list data.table subset
4个回答
2
投票

我们可以使用DT中的namesparam中选择列,将%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

4
投票

您可以使用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中的任何组合都不会返回一行。


4
投票

使用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]

3
投票

你可以使用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

© www.soinside.com 2019 - 2024. All rights reserved.