подмножество data.table на основе именованного списка - PullRequest
5 голосов
/ 17 апреля 2019

Я пытаюсь установить подмножество данных. Таблица

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 со связанными значениями? Спасибо!

EDIT

Я выполнил микробенчмарк с некоторыми ответами:

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

Ответы [ 4 ]

4 голосов
/ 17 апреля 2019

Вы можете использовать функцию CJ ( C ross J oin) из 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 голосов
/ 17 апреля 2019

Используя 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.


Благодаря @ Фрэнк , это можно улучшить до

DT[DT[, all(mapply(`%in%`, .SD, param)), by = 1:nrow(DT), .SDcols=names(param)]$V1]
3 голосов
/ 17 апреля 2019

Вы можете построить выражение с помощью 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 (см.пакет виньетки).Вы также можете включить многословный режим, чтобы получить примечание о неэффективности указания param$c в виде числа, когда DT$c равно int:

> 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).

Объединение, как и в ответе Натана, также использует индексы, но построение и объединение на декартовой таблице 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)

2 голосов
/ 17 апреля 2019

Мы можем выбрать столбцы в 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
...