Я пытаюсь подмножить данную таблицу данных.
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
Используя 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]
Мы можем выбрать столбцы в 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: Да, CJ
— это почти data.table
версия expand.grid
. Два преимущества CJ
: он не преобразует строки в множители и последовательно обрабатывает ввод списка. Посмотрите разницу между expand.grid(b = list(1, 2, 3))
, expand.grid(a = 1, b = list(1, 2, 3))
и тем, как CJ
обрабатывает эти входные данные.
Вы можете построить выражение с помощью 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
, версию которого я реализовал независимо . Хотя первый заметно быстрее для небольших наборов данных, последний гораздо лучше масштабируется:
Мне кажется, что точка, в которой относительная скорость переключается примерно на 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'
)
Спасибо, это работает, но возвращает много предупреждений: ...
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
...