Подмножество 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
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
7
0
1 065
5
Перейти к ответу Данный вопрос помечен как решенный

Ответы 5

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

Спасибо, это работает, но возвращает много предупреждений: ... 34: In all(Map(``%in%``, .SD, param)) : coercing argument of type 'list' to logical 35: In mapply(FUN = f, ..., SIMPLIFY = FALSE) : longer argument not a multiple of length of shorter ...

jaydee 17.04.2019 15:30
Ответ принят как подходящий

Мы можем выбрать столбцы в 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 (Сross Дж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, не вернет строку.

Очень хорошо. Но нельзя ли вместо этого использовать DT[expand.grid(param), on = names(param), nomatch = NULL]?

jaydee 17.04.2019 18:33

@jaydee: Да, CJ — это почти data.table версия expand.grid. Два преимущества CJ: он не преобразует строки в множители и последовательно обрабатывает ввод списка. Посмотрите разницу между expand.grid(b = list(1, 2, 3)), expand.grid(a = 1, b = list(1, 2, 3)) и тем, как CJ обрабатывает эти входные данные.

Nathan Werth 17.04.2019 19:19

Вы можете построить выражение с помощью 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)

Я добавляю еще один ответ, потому что в решениях, представленных OP, отсутствует важная деталь: как каждое из них масштабируется с большими наборами данных. Я часто работаю с наборами данных с более чем 1 миллионом записей, поэтому для своей выгоды я провел эксперимент по микробенчмаркингу, который представляет OP, используя наборы данных разных размеров для решения pmin + %in% + Map и решения CJ, версию которого я реализовал независимо . Хотя первый заметно быстрее для небольших наборов данных, последний гораздо лучше масштабируется:

Execution Time by Fields to Subset on

Мне кажется, что точка, в которой относительная скорость переключается примерно на 200 тыс. записей, независимо от количества полей для подмножества, поэтому я упаковал обе функции в одну для будущего использования:

subsel <- function(x, sub, sel = NULL,
                   nomatch = getOption('datatable.nomatch')){
  #' function to subset data.table (x) using a named list (sub). sel
  #' can be used to return only the specified columns. algorithms
  #' copied from https://stackoverflow.com/questions/55728200/subsetting-a-data-table-based-on-a-named-list
  #' and cutoff decided on some ad hoc testing.
  if (is.null(sel)) sel <- names(x)
  if (x[, .N] < 200000L){
    return(
      x[
        do.call(
          pmin,
          Map(`%in%`, x[, .SD, .SDcols = names(sub)], sub)
        ) == 1L,
        .SD,
        .SDcols = sel,
        nomatch = nomatch
      ]
    )
  } else {
    return(
      x[
        do.call(CJ, sub),
        .SD,
        .SDcols = sel,
        on = names(sub),
        nomatch = nomatch
      ]
    )
  }
}

Вот код, используемый для генерации графика, если кому-то интересно:

require(data.table)
require(ggplot)
require(microbenchmark)
require(scales)

subsel <- function(x, sub, nomatch = NULL, sel = list()){
  if (length(sel) == 0) sel <- names(x)
  return(
    x[
      do.call(CJ, sub),
      .SD,
      .SDcols = sel,
      on = names(sub),
      nomatch = nomatch
    ]
  )
}

subsel2 <- function(x, sub, nomatch = NULL, sel = list()){
  if (length(sel) == 0) sel <- names(x)
  return(
    x[
      do.call(
        pmin,
        Map(`%in%`, x[, .SD, .SDcols = names(sub)], sub)
      ) == 1L,
      .SD,
      .SDcols = sel,
      nomatch = nomatch
    ]
  )
}

ll <- list(
  a = letters[1:10],
  b = 1:10,
  c = letters[1:10],
  d = 1:10
)

times <- rbindlist(
  lapply(
    seq(from = 100000, to = 1000000, by = 25000),
    function(y){
      dat <- data.table(
        a = sample(letters, y, replace = T),
        b = sample.int(100, y, replace = T),
        c = sample(letters, y, replace = T),
        d = sample.int(100, y, replace = T)
      )
      return(
        rbindlist(
          lapply(
            2:4,
            function(x){
              return(
                setDT(
                  microbenchmark(
                    subsel(dat, sub = head(ll, x), sel = letters[2:4]),
                    subsel2(dat, sub = head(ll, x), sel = letters[2:4])
                  )
                )[, fields := x]
              )
            }
          )
        )[, size := y]
      )
    }
  )
)

times[
  ,
  expr2 := unlist(
    lapply(
      as.character(expr),
      function(x) unlist(strsplit(x, '(', fixed = T))[1]
    )
  )
]
times[
  ,
  expr2 := factor(
    expr2,
    levels = c('subsel', 'subsel2'),
    labels = c('CJ', 'pmin + Map + %in%')
  )
]

ggplot(times, aes(size, time, group = expr2, color = expr2)) +
  geom_smooth() +
  facet_grid(factor(fields) ~ .) +
  scale_y_continuous(labels = number_format(scale = 1e-6)) +
  labs(
    title = 'Execution Time by Fields to Subset on',
    x = 'Dataset Size',
    y = 'Time (Milliseconds)',
    color = 'Function'
  )

Другие вопросы по теме