Объединение нескольких условий ИЛИ и И в R

Я хочу объединить несколько условий ИЛИ и И в 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))). Интересно написать это более компактно.

Спасибо @r2evans за ваш комментарий. Результатом x1 == 1 & y1 == 1 является TRUE FALSE FALSE, тогда как all(c(x1, y1) == 1) дает результат FALSE. Есть предположения?

MYaseen208 23.03.2024 02:31

Извините, это был мимолетный комментарий, бесполезный.

r2evans 23.03.2024 02:37

Я не могу придумать ничего, что было бы лучше или быстрее, чем эти примитивные операции.

r2evans 23.03.2024 02:38

ваши векторы являются целыми числами? то есть x1*y1 == 1

Onyambu 23.03.2024 02:40

Будьте осторожны, если NA встречается, c(1, NA, 4) == 1 | c(1, NA, 4) == 2 отличается от c(1, NA, 4) %in% c(1, 2). `%in%` больше связан с ?match, чем с `==`.

jay.sf 23.03.2024 05:30
Стоит ли изучать 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 называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
3
5
384
5
Перейти к ответу Данный вопрос помечен как решенный

Ответы 5

Создайте некоторые данные

Поскольку 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 (или какие бы то ни было входные данные), которые не копируются.

Некоторые ориентиры

Вот некоторые критерии для трех подходов:

  1. reduce: Подход Reduce() от Moodymudskipper.
  2. df: функция compare(), использующая фрейм данных.
  3. 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 26.03.2024 15:43

@nicola интересно, я этого не знал. Хотя я не думаю, что здесь проблема. В основном код выполняет rowSums(data.frame(l) == 1), а не rowSums(data.frame(l)) == 1. Таким образом, аргументом rowSums() является data.frame(l) == 1, который уже является матрицей, а не исходным фреймом данных, поэтому не следует создавать копию. Хотя я не знал, что это происходит под капотом rowSums() — я склонен предполагать, что подобные функции оптимизированы в C. Есть на что обратить внимание — спасибо.

SamR 26.03.2024 15:59

@SamR, поскольку все фреймы данных состоят из столбцов, можно предположить, что все, что работает со строками фрейма данных, преобразует их в матрицу. rowSums, rowMeans, apply, ... ну, я даже не могу вспомнить других.

Gregor Thomas 26.03.2024 17:59

@SamR Тем не менее, ваша точка зрения относительно преимуществ производительности при использовании data.frame здесь очень сомнительна. Не знаю, предлагаете ли вы использовать cbind(который создаст матрицу) вместо data.frame где-то делать копии. Я думаю, что матрица была бы более эффективной; посмотрите на исходный код, который вы опубликовали, сколько проверок, поднаборов и приведения требуется для использования логических операторов между data.frames. Эти операторы наверняка работают быстрее с числовыми векторами.

nicola 27.03.2024 16:46

@nicola Я добавил к своему ответу несколько тестов. Я надеялся, что они однозначно докажут мою точку зрения, но картина немного сложнее. Если векторы короткие, матрицы лучше. По мере того, как они становятся длиннее, фрейм данных становится лучше. Я уверен, что это связано с тем, что копии создаются при приведении к матрице - поскольку матрица является смежной в памяти, так и должно быть. Это не относится к фрейму данных. В любом случае это несколько спорно, поскольку подход Reduce() сбивает их обоих с ног.

SamR 27.03.2024 22:47

@SamR Возможно, моя точка зрения была неясна. Вы можете получить тот же результат, если в своей функции compare вы используете cbind(...) вместо data.frame(...). Другое решение, которое вы протестировали, во многом отличается от вашего. Я хочу сказать, что нет никакого преимущества в производительности от использования в какой-то момент data.frame просто ради него.

nicola 28.03.2024 07:59

Просто для ясности: то, что вы сравнивали, имеет вызовы apply и t, которые совершенно не нужны.

nicola 28.03.2024 08:01

Более конкретно: протестируйте 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 выигрывает по всем направлениям.

nicola 28.03.2024 08:23

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) лучше, поскольку они не будут копировать данные.

SamR 25.03.2024 09:16

В R умножение матриц выполняется на матрицах, а не на кадрах данных, поэтому, если вы хотите сохранить аналогию, входные данные должны быть матрицей.

G. Grothendieck 25.03.2024 14:05

Я упускаю из виду вашу точку зрения - возможно, потому, что я не знаю APL. Вы хотите сказать, что бывают случаи, когда cbind(x1, y1) %==.&% c(1, 1) будет давать разные результаты для data.frame(x1, y1) %==.&% c(1, 1)? Или вы говорите, что лучше использовать матрицу из-за концептуального сходства с умножением матриц? Или оба?

SamR 25.03.2024 14:35

Попробуйте BOD %*% 1:2, где BOD — это встроенный фрейм данных. Вы получаете ошибку, поскольку фрейм данных не может быть аргументом для умножения матрицы.

G. Grothendieck 25.03.2024 14:40
Ответ принят как подходящий

Я думаю, вам нужны 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))), но оно слишком длинное.

MYaseen208 26.03.2024 19:20

В данном случае, я думаю, это будет Reduce(`&`, lapply(list(x1, y1), `%in%`, c(1,2))

moodymudskipper 26.03.2024 20:52

Но как бы это ни было интересно с точки зрения программирования, кажется, что у вас есть данные, которые должны быть структурированы по-другому, и вы можете пересмотреть свой исходный код, возможно, у вас есть векторы, которые должны быть в фрейме данных, длинные, а не широкие. Если x1 и y1 — это одно и то же, с аккуратными данными они, вероятно, будут в одном столбце друг над другом.

moodymudskipper 26.03.2024 20:57

Я также предположил, что в реальном случае у вас больше векторов, чем 2, если нет, используйте читаемые простые варианты! :D

moodymudskipper 26.03.2024 20:59

К вашему сведению, я добавил в свой ответ несколько ориентиров. Это определенно победитель.

SamR 27.03.2024 22:44

Вот обобщение, основанное на ответах @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

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