Я хочу объединить несколько условий ИЛИ и И в R. Я думаю, x1 == 1 | x1 == 2
можно объединить как x1 %in% c(1, 2)
. Мне интересно, как x1 == 1 | y1 == 1
и x1 == 1 & y1 == 1
можно объединить в более компактный R
код.
x1 <- c(1, 2, 3)
y1 <- c(1, 2, 4)
x1 == 1 | x1 == 2
#> [1] TRUE TRUE FALSE
x1 %in% c(1, 2)
#> [1] TRUE TRUE FALSE
x1 == 1 | y1 == 1
#> [1] TRUE FALSE FALSE
intersect(x1, y1) == 1
#> [1] TRUE FALSE
intersect(x1, y1) == 2
#> [1] FALSE TRUE
intersect(x1, y1) %in% c(1, 2)
#> [1] TRUE TRUE
> x1 == 1 & y1 == 1
[1] TRUE FALSE FALSE
Отредактировано
Код (x1 == 1 | x1 == 2) & (y1 == 1 | y1 == 2)
равен Reduce(any, lapply(list(x1),
%in%, c(1, 2))) & Reduce(any, lapply(list(y1),
%in%, c(1, 2)))
. Интересно написать это более компактно.
Извините, это был мимолетный комментарий, бесполезный.
Я не могу придумать ничего, что было бы лучше или быстрее, чем эти примитивные операции.
ваши векторы являются целыми числами? то есть x1*y1 == 1
Будьте осторожны, если NA
встречается, c(1, NA, 4) == 1 | c(1, NA, 4) == 2
отличается от c(1, NA, 4) %in% c(1, 2)
. `%in%`
больше связан с ?match
, чем с `==`
.
Поскольку x1 == 1 | y1 == 1
не кажется слишком многословным, давайте создадим больше векторов:
set.seed(1)
lapply(1:10, \(.) sample(10, 10, replace = TRUE)) |>
setNames(paste0(c("x", "y"), c(1:5, 1:5))) |>
list2env(.GlobalEnv)
ls()
# [1] "x1" "x2" "x3" "x4" "x5" "y1" "y2" "y3" "y4" "y5"
Теперь становится довольно утомительно писать:
x1 == 1 | y2 == 1 | x3 == 1 | y4 == 1 | x5 == 1 | y1 == 1 | x2 == 1 | y3 == 1 | x4 == 1 | y5 == 1
# [1] FALSE FALSE TRUE TRUE TRUE TRUE FALSE FALSE TRUE TRUE
Мы используем поэлементные логические операторы, поэтому векторы гарантированно будут одинаковой длины. Давайте поместим их в фрейм данных для сравнения:
compare <- function(..., val = 1, op = c("= = ", "<", ">", "> = ", "< = ", "! = ")) {
fun <- match.fun(match.arg(op))
rowSums(fun(data.frame(...), val)) > 0
}
Затем мы можем сделать:
compare(x1, y2, x3, y4, x5, y1, x2, y3, x4, y5)
# [1] FALSE FALSE TRUE TRUE TRUE TRUE FALSE FALSE TRUE TRUE
Или, альтернативно, если это все еще слишком много ввода, мы можем поместить векторы в список (что в любом случае лучше) и передать его в функцию:
# Create a list of x1:x5 and y1:y5
l <- mget(ls(pattern = "[xy]\\d"))
compare(l)
# [1] FALSE FALSE TRUE TRUE TRUE TRUE FALSE FALSE TRUE TRUE
Это также работает с ==
и другими операторами.
identical(
compare(l, val = 1),
x1 == 1 | y2 == 1 | x3 == 1 | y4 == 1 | x5 == 1 | y1 == 1 | x2 == 1 | y3 == 1 | x4 == 1 | y5 == 1
)
# [1] TRUE
identical(
compare(l, op = ">", val = 9),
x1 > 9 | y2 > 9 | x3 > 9 | y4 > 9 | x5 > 9 | y1 > 9 | x2 > 9 | y3 > 9 | x4 > 9 | y5 > 9
)
# [1] TRUE
Преимущество использования фрейма данных (а не, скажем, матрицы) состоит в том, что это просто список указателей на каждый вектор. Это означает, что копии данных не создаются. Кроме того, кадры данных поддерживают операции поэлементного сравнения. Как сказала Дженни Брайан, конечно, кто-то должен писать циклы. Это не обязательно должны быть вы.
В комментариях было отмечено, что rowSums(dat)
, где dat
— это data.frame
, приведет dat
к матрице, т. е. сделает копию.
Это правда (и интересно!), но входные данные в этом случае не копируются. Приведенный выше код не вызывает rowSums(data.frame(...))
, который вызвал бы приведение к матрице. Он бежит rowSums(data.frame(...)==val)
. Мы можем увидеть эквивалентную разницу, используя браузер отладки:
debug(as.matrix)
rowSums(mtcars) # Triggers debug browser; debugging in: as.matrix(x)
rowSums(mtcars == 1) # Does not trigger browser
Это потому, что mtcars==1
уже является матрицей. Теперь, если вы покопаетесь в источнике Ops.data.frame(), который представляет собой функцию, вызываемую при использовании ==
во фрейме данных, вы увидите, что она вызывает matrix(value, nrow = nr, dimnames = list(rn,cn))
. Однако matrix()
вызывается для возвращаемого значения, чтобы гарантировать, что mtcars==1
возвращает матрицу, а не mtcars
(или какие бы то ни было входные данные), которые не копируются.
Вот некоторые критерии для трех подходов:
reduce
: Подход Reduce()
от Moodymudskipper.df
: функция compare()
, использующая фрейм данных.cbind_mat
; Функция %==.|% <- function(x, y) apply(t(x) == y, 2, any)
определена в ответе Г. Гротендика.Я варьировал как количество векторов, так и длину векторов от 10 до 10 тыс. элементов. Реальная картина о том, что лучше: фрейм данных или матрица, сложна. Для более коротких векторов использование матрицы происходит намного быстрее, чем использование фрейма данных. Для более длинных векторов фрейм данных обрабатывается значительно быстрее, чем матрица, и при этом выделяется гораздо меньше памяти.
Однозначно ясно, что Reduce()
— самый быстрый вариант:
Аналогичным образом, по мере увеличения объема данных подход с использованием фреймов данных использует намного меньше оперативной памяти, чем приведение векторов к матрицам. Однако подход Reduce()
во всех случаях использует меньше всего оперативной памяти.
results <- bench::press(
num_vectors = 10^(1:4),
vec_length = 10^(1:4),
{
l <- lapply(seq(num_vectors), \(.) sample(vec_length, vec_length, replace = TRUE))
bench::mark(
min_iterations = 10,
max_iterations = 1000,
relative = TRUE,
df = {
compare(l)
},
cbind_mat = {
do.call(cbind, l) %==.|% 1
},
reduce = {
Reduce(`|`, lapply(l, `==`, 1))
}
)
}
)
# Plots
library(ggplot2)
# beeswarm plot
autoplot(results) +
ggh4x::facet_grid2(
vars(num_vectors), vars(vec_length),
scales = "free_x", independent = "x",
labeller = label_both
) +
scale_y_continuous() +
theme_minimal(base_size = 13) +
theme(legend.position = "bottom")
# RAM usage plot
results |>
dplyr::mutate(
expr = attr(expression, "description"),
mem_alloc = unclass(mem_alloc),
size = as.factor(num_vectors)
) |>
ggplot() +
geom_col(aes(
x = reorder(expr, mem_alloc),
y = mem_alloc,
fill = expr
), color = "black") +
ggh4x::facet_grid2(
vars(num_vectors), vars(vec_length),
scales = "free", independent = "y"
) +
labs(
title = "Total RAM usage",
y = "Relative RAM usage",
x = "Expression"
) +
theme_minimal(base_size = 14) +
theme(legend.position = "bottom")
Остерегайтесь того, что вы сказали о преимуществе использования data.frame. В своем решении вы использовали rowSums
, который можно применить к data.frame, но внутри он приводит его к матрице. Итак, копия действительно сделана.
@nicola интересно, я этого не знал. Хотя я не думаю, что здесь проблема. В основном код выполняет rowSums(data.frame(l) == 1)
, а не rowSums(data.frame(l)) == 1
. Таким образом, аргументом rowSums()
является data.frame(l) == 1
, который уже является матрицей, а не исходным фреймом данных, поэтому не следует создавать копию. Хотя я не знал, что это происходит под капотом rowSums()
— я склонен предполагать, что подобные функции оптимизированы в C. Есть на что обратить внимание — спасибо.
@SamR, поскольку все фреймы данных состоят из столбцов, можно предположить, что все, что работает со строками фрейма данных, преобразует их в матрицу. rowSums
, rowMeans
, apply
, ... ну, я даже не могу вспомнить других.
@SamR Тем не менее, ваша точка зрения относительно преимуществ производительности при использовании data.frame здесь очень сомнительна. Не знаю, предлагаете ли вы использовать cbind
(который создаст матрицу) вместо data.frame
где-то делать копии. Я думаю, что матрица была бы более эффективной; посмотрите на исходный код, который вы опубликовали, сколько проверок, поднаборов и приведения требуется для использования логических операторов между data.frames. Эти операторы наверняка работают быстрее с числовыми векторами.
@nicola Я добавил к своему ответу несколько тестов. Я надеялся, что они однозначно докажут мою точку зрения, но картина немного сложнее. Если векторы короткие, матрицы лучше. По мере того, как они становятся длиннее, фрейм данных становится лучше. Я уверен, что это связано с тем, что копии создаются при приведении к матрице - поскольку матрица является смежной в памяти, так и должно быть. Это не относится к фрейму данных. В любом случае это несколько спорно, поскольку подход Reduce()
сбивает их обоих с ног.
@SamR Возможно, моя точка зрения была неясна. Вы можете получить тот же результат, если в своей функции compare
вы используете cbind(...)
вместо data.frame(...)
. Другое решение, которое вы протестировали, во многом отличается от вашего. Я хочу сказать, что нет никакого преимущества в производительности от использования в какой-то момент data.frame просто ради него.
Просто для ясности: то, что вы сравнивали, имеет вызовы apply
и t
, которые совершенно не нужны.
Более конкретно: протестируйте compare <- function(l, val = 1, op = c("= = ", "<", ">", "> = ", "< = ", "! = ")) { fun <- match.fun(match.arg(op)) rowSums(fun(data.frame(l), val)) > 0 }
против compare_1 <- function(l, val = 1, op = c("= = ", "<", ">", "> = ", "< = ", "! = ")) { fun <- match.fun(match.arg(op)) rowSums(fun(do.call(cbind, l), val)) > 0 }
, и вы увидите, что сравнение_1 выигрывает по всем направлениям.
1) Следующее проверяет, что x1[i] и y1[i] оба равны 1, т. е. И.
x1 <- c(1, 2, 3, 1)
y1 <- c(1, 2, 4, 3)
paste(x1, y1) == "1 1"
## [1] TRUE FALSE FALSE FALSE
2) Если бы мы знали, что векторы состоят из целых чисел >= 1, как в вопросе, то мы могли бы использовать pmin и pmax. Обратите внимание, что у них есть дополнительные аргументы na.rm=, которые можно настроить для обработки NA.
pmin(x1, y1) == 1 # OR
## [1] TRUE FALSE FALSE TRUE
pmax(x1, y1) == 1 # AND
## [1] TRUE FALSE FALSE FALSE
Все вышесказанное легко обобщается на более чем два вектора.
3) Выражение в вопросе, включающее только x1, можно немного сократить.
x1 %in% 1:2
## [1] TRUE TRUE FALSE TRUE
4) Случай с x1 и y1 можно рассматривать как обобщенное матричное умножение, где * заменяется на ==, а + заменяется либо на | или & . В языке APL это =.v и =.^ . (Другие реализации см. https://rpubs.com/deleeuw/158476 и Blockmodeling::genMatrixMult.)
`%==.|%` <- function(x, y) apply(t(x) == y, 2, any)
`%==.&%` <- function(x, y) apply(t(x) == y, 2, all)
cbind(x1, y1) %==.&% c(1, 1)
## [1] TRUE FALSE FALSE FALSE
cbind(x1, y1) %==.|% c(1, 1)
## [1] TRUE FALSE FALSE TRUE
У меня есть идея определить инфиксные операторы, но если данные имеют разумный размер, я думаю, что cbind.data.frame(x1, y1)
лучше, поскольку они не будут копировать данные.
В R умножение матриц выполняется на матрицах, а не на кадрах данных, поэтому, если вы хотите сохранить аналогию, входные данные должны быть матрицей.
Я упускаю из виду вашу точку зрения - возможно, потому, что я не знаю APL. Вы хотите сказать, что бывают случаи, когда cbind(x1, y1) %==.&% c(1, 1)
будет давать разные результаты для data.frame(x1, y1) %==.&% c(1, 1)
? Или вы говорите, что лучше использовать матрицу из-за концептуального сходства с умножением матриц? Или оба?
Попробуйте BOD %*% 1:2
, где BOD — это встроенный фрейм данных. Вы получаете ошибку, поскольку фрейм данных не может быть аргументом для умножения матрицы.
Я думаю, вам нужны lapply()
и Reduce()
для чистой «хакерской» абстракции:
x1 <- c(1, 2, 3)
y1 <- c(1, 2, 4)
y1 == 1 | x1 == 1
#> [1] TRUE FALSE FALSE
Reduce(`|`, lapply(list(x1, y1), `==`, 1))
#> [1] TRUE FALSE FALSE
Вы можете выиграть несколько персонажей с помощью apply(cbind(x1, y1) == 1, 1, all)
(используя матрицу в качестве промежуточной фигуры), но я не знаю, стоит ли оно того.
Created on 2024-03-26 with reprex v2.0.2
Спасибо за очень полезный ответ. Буду признателен, если подскажете, как написать это (x1 == 1 | x1 == 2) & (y1 == 1 | y1 == 2)
более компактно. Я попробовал это Reduce(
|, lapply(list(x1),
%in%, c(1, 2))) & Reduce(
|, lapply(list(y1),
%in%, c(1, 2)))
, но оно слишком длинное.
В данном случае, я думаю, это будет Reduce(`&`, lapply(list(x1, y1), `%in%`, c(1,2))
Но как бы это ни было интересно с точки зрения программирования, кажется, что у вас есть данные, которые должны быть структурированы по-другому, и вы можете пересмотреть свой исходный код, возможно, у вас есть векторы, которые должны быть в фрейме данных, длинные, а не широкие. Если x1
и y1
— это одно и то же, с аккуратными данными они, вероятно, будут в одном столбце друг над другом.
Я также предположил, что в реальном случае у вас больше векторов, чем 2, если нет, используйте читаемые простые варианты! :D
К вашему сведению, я добавил в свой ответ несколько ориентиров. Это определенно победитель.
Вот обобщение, основанное на ответах @moodymudskipper и @SamR
f <- function(
...,
val = 1,
op = c("= = ", "<", ">", "> = ", "< = ", "! = "),
how = c("|", "&")) {
Reduce(as.symbol(how), lapply(list(...), as.symbol(op), val))
}
Склейте части вместе и оцените их как выражение, например
x1 <- c(1, 2, 3)
y1 <- c(1, 2, 4)
string <- paste0(c('x','y'), 1, '==', c(1,2), collapse='&')
eval(parse(text=string))
# [1] FALSE FALSE FALSE
Спасибо @r2evans за ваш комментарий. Результатом
x1 == 1 & y1 == 1
являетсяTRUE FALSE FALSE
, тогда какall(c(x1, y1) == 1)
дает результатFALSE
. Есть предположения?