Возвращаем тиббл: как векторизовать с помощью case_when?

У меня есть функция, которая возвращает тиббл. Он работает нормально, но я хочу его векторизовать.

library(tidyverse)

tibTest <- tibble(argX = 1:4, argY = 7:4)

square_it <- function(xx, yy) {
  if(xx >= 4){
    tibble(x = NA, y = NA)
  } else if(xx == 3){
    tibble(x = as.integer(), y = as.integer())
  } else if (xx == 2){
    tibble(x = xx^2 - 1, y = yy^2 -1)
  } else {
    tibble(x = xx^2, y = yy^2)
  }
}

Он работает нормально в mutate, когда я вызываю его с помощью map2, давая мне желаемый результат:

tibTest %>%
  mutate(sq = map2(argX, argY, square_it)) %>%
  unnest()
## A tibble: 3 x 4
#     argX  argY     x     y
#    <int> <int> <dbl> <dbl>
# 1     1     7     1    49
# 2     2     6     3    35
# 3     4     4    NA    NA

Моя первая попытка векторизовать его не удалась, и я понимаю, почему - я не могу вернуть вектор тибблей.

square_it2 <- function(xx, yy){
  case_when(
    x >= 4 ~ tibble(x = NA, y = NA),
    x == 3 ~ tibble(x = as.integer(), y = as.integer()),
    x == 2 ~ tibble(x = xx^2 - 1, y = yy^2 -1),
    TRUE   ~ tibble(x = xx^2,     y = yy^2)
  )
}
# square_it2(4, 2)  # FAILS

Моя следующая попытка работает нормально на простом вводе. Я могу вернуть список блюд, и это то, что мне нужно для unnest

square_it3 <- function(xx, yy){
  case_when(
    xx >= 4 ~ list(tibble(x = NA, y = NA)),
    xx == 3 ~ list(tibble(x = as.integer(), y = as.integer())),
    xx == 2 ~ list(tibble(x = xx^2 - 1, y = yy^2 -1)),
    TRUE   ~ list(tibble(x = xx^2,     y = yy^2))
  )
}
square_it3(4, 2)
# [[1]]
# # A tibble: 1 x 2
# x     y    
# <lgl> <lgl>
#   1 NA    NA   

Но когда я вызываю его в mutate, он не дает мне того результата, который был у меня с square_it. Я вроде как вижу, что не так. В предложении xx == 2 xx действует как атомарное значение 2. Но при построении тиббла xx является вектором длины 4.

tibTest %>%
  mutate(sq =  square_it3(argX, argY)) %>%
  unnest()
# # A tibble: 9 x 4
#    argX  argY     x     y
#    <int> <int> <dbl> <dbl>
# 1     1     7     1    49
# 2     1     7     4    36
# 3     1     7     9    25
# 4     1     7    16    16
# 5     2     6     0    48
# 6     2     6     3    35
# 7     2     6     8    24
# 8     2     6    15    15
# 9     4     4    NA    NA

Как мне получить тот же результат, что и с square_it, но из векторизованной функции с использованием case_when?


person David T    schedule 16.05.2020    source источник
comment
Используйте rowwise() на тибле.   -  person 27 ϕ 9    schedule 16.05.2020
comment
Спасибо; tibTest %>% rowwise() %>% mutate(sq = square_it3(argX, argY)) %>% unnest() сделал свое дело!   -  person David T    schedule 16.05.2020


Ответы (2)


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

Это работает независимо от того, есть ли у вас rowwise группа или нет.

Вы можете сделать это с switch, завернутым в map2:

Вот представитель:

library(tidyverse)

tibTest <- tibble(argX = 1:4, argY = 7:4)

square_it <- function(xx, yy) {
  map2(xx, yy, function(x, y){
    switch(which(c(x >= 4, 
                   x == 3, 
                   x == 2, 
                   x < 4 & x != 3 & x != 2)),
           tibble(x = NA, y = NA),
           tibble(x = as.integer(), y = as.integer()),
           tibble(x = x^2 - 1, y = y^2 -1),
           tibble(x = x^2, y = y^2))})
}

tibTest %>% mutate(sq =  square_it(argX, argY)) %>% unnest(cols = sq)
#> # A tibble: 3 x 4
#>    argX  argY     x     y
#>   <int> <int> <dbl> <dbl>
#> 1     1     7     1    49
#> 2     2     6     3    35
#> 3     4     4    NA    NA

Создано 16 мая 2020 г. пакетом REPEX (v0.3.0)

person Allan Cameron    schedule 16.05.2020
comment
Спасибо, но я специально пытался использовать case_when. - person David T; 16.05.2020
comment
Думаю, в этом проблема @DavidT. case_when предназначен для применения к векторам, но вы хотите использовать его специально для векторов длины 1. Передача построчных групп - это один из способов сделать это, но с точки зрения разработки программного обеспечения вы должны стремиться к тому, чтобы функция работала сама по себе внутри вызова mutate. Если вы предпочитаете синтаксис case_when, я изменю ответ. - person Allan Cameron; 16.05.2020
comment
Да, пожалуйста. - person David T; 16.05.2020
comment
case_when, если я правильно понимаю, принимает свои векторные входные данные и применяет свою условную логику к каждому элементу вектора. В моем коде внутри case_when строка xx == 2 ~ list(tibble(x = xx^2 - 1, y = yy^2 -1)) рассматривает xx слева от ~ как один векторный элемент, а xx справа от значения как весь вектор. - person David T; 16.05.2020
comment
@DavidT, это верно - поэтому, если ваш ввод xx = c(2, 2, 2), тогда для каждого элемента в xx вы создадите тиббл с тремя строками. Таким образом, при unnest() у вас будет 9 строк, а это не то, что вы хотели. Это работает rowwise, потому что xx будет иметь только один член. Но тогда зачем использовать case_when? - person Allan Cameron; 16.05.2020
comment
Я пытаюсь векторизовать все функции, которые пишу, когда это возможно. Мне нравится case_when. В большинстве случаев, когда я использую его, я могу сэкономить maping, и я предпочитаю его синтаксис switch. Поэтому я пытался понять, насколько далеко я могу зайти - каковы пределы. Думаю, я только что узнал. :-) Спасибо за вашу помощь. - person David T; 16.05.2020
comment
Это на самом деле не векторизовано, верно? Я имею в виду, что это то же самое, что и square_it. Единственная разница в том, что map2, который был вне функции, теперь находится внутри. @DavidT - person Ronak Shah; 17.05.2020
comment
@ ronak-shah Верно. Я имею в виду, что технически он векторизован - он будет принимать векторы в качестве входных данных. Но именно map разбивает вещи на отдельные случаи, а не case_when, который я искал. - person David T; 17.05.2020
comment
@RonakShah, ты прав, это не совсем векторизация. Смысл этого ответа состоял в том, чтобы показать, почему в третьем случае создается слишком много строк и что case_when, вероятно, не является правильным подходом к получению желаемого результата. Конечно, есть способы добиться желаемого результата лучше, чем раскладывание тибетских таблиц. Трудность, с которой я столкнулся с case_when, заключалась в том, чтобы создать однострочный тиббл для каждого значения xx и yy без предварительного разделения столбцов по строкам (либо на rowwise, либо на map2). Я также хотел указать, что это нужно делать внутри функции. - person Allan Cameron; 17.05.2020
comment
@RonakShah, если вы можете продемонстрировать способ получения одной строки таблицы для каждого элемента из двух столбцов с помощью case_when без предварительного создания групп по строкам или использования карты, я был бы очень благодарен за изучение, и это был бы лучший ответ на вопрос ОП. - person Allan Cameron; 17.05.2020

Мы определяем row_case_when, который имеет аналогичный интерфейс формулы, что и case_when, за исключением того, что он имеет первый аргумент .data, действует построчно и ожидает, что значение каждого отрезка будет фреймом данных. Он возвращает data.frame / tibble. Обтекание списком, rowwise и unnest не нужны.

case_when2 <- function (.data, ...) {
    fs <- dplyr:::compact_null(rlang:::list2(...))
    n <- length(fs)
    if (n == 0) {
        abort("No cases provided")
    }
    query <- vector("list", n)
    value <- vector("list", n)
    default_env <- rlang:::caller_env()
    quos_pairs <- purrr::map2(fs, seq_along(fs), dplyr:::validate_formula,
        rlang:::default_env, rlang:::current_env())
    for (i in seq_len(n)) {
        pair <- quos_pairs[[i]]
        query[[i]] <- rlang::eval_tidy(pair$lhs, data = .data, env = default_env)
        value[[i]] <- rlang::eval_tidy(pair$rhs, data = .data, env = default_env)
        if (!is.logical(query[[i]])) {
            abort_case_when_logical(pair$lhs, i, query[[i]])
        }
        if (query[[i]]) return(value[[i]])
    }
}

row_case_when <- function(.data, ...) {
  .data %>% 
    group_by(.group = 1:n(), !!!.data) %>%
    do(case_when2(., ...)) %>%
    mutate %>%
    ungroup %>%
    select(-.group)
}

Тестовый забег

Он используется так:

library(dplyr)

tibTest <- tibble(argX = 1:4, argY = 7:4) # test data from question

tibTest %>%
  row_case_when(argX >= 4 ~ tibble(x = NA, y = NA),
    argX == 3 ~ tibble(x = as.integer(), y = as.integer()),
    argX == 2 ~ tibble(x = argX^2 - 1, y = argY^2 -1),
    TRUE   ~ tibble(x = argX^2,     y = argY^2)
  )

давая:

# A tibble: 3 x 4
   argX  argY     x     y
  <int> <int> <dbl> <dbl>
1     1     7     1    49
2     2     6     3    35
3     4     4    NA    NA

mutate_cond и mutate_when

Это не совсем то же самое, что row_case_when, поскольку они не проходят через условия, принимающие первое истинное, но, используя взаимоисключающие условия, их можно использовать для определенных аспектов этой проблемы. Они не обрабатывают изменение количества строк в результате, но мы можем использовать dplyr::filter для удаления строк для определенного условия.

mutate_cond определено в dplyr изменяет / заменяет несколько столбцы в подмножестве строк похожи на mutate, за исключением того, что второй аргумент является условием, а последующие аргументы применяются только к строкам, для которых это условие ИСТИНА.

mutate_when, определенный в dplyr для изменения / замены нескольких столбцы в подмножестве строк аналогичен case_when, за исключением того, что он применяется к строкам, значения замены предоставляются в виде списка, а альтернативными аргументами являются условия и списки. Кроме того, все участки всегда выполняются с применением значений замены к строкам, удовлетворяющим условиям (в отличие от выполнения замены только на первом истинном участке для каждой строки). Чтобы получить эффект, аналогичный row_case_, убедитесь, что условия исключают друг друга.

# mutate_cond example
tibTest %>%
  filter(argX != 3) %>%
  mutate(x = NA_integer_, y = NA_integer_) %>%
  mutate_cond(argX == 2, x = argX^2 - 1L, y = argY^2 - 1L) %>%
  mutate_cond(argX < 2, x = argX^2, y = argY^2)

# mutate_when example
tibTest %>%
  filter(argX != 3) %>%
  mutate_when(TRUE, list(x = NA_integer_, y = NA_integer_),
              argX == 2, list(x = argX^2 - 1L, y = argY^2 - 1L), 
              argX < 2, list(x = argX^2, y = argY^2))
person G. Grothendieck    schedule 17.05.2020
comment
Это дает приятный интерфейс с отличным интуитивно понятным синтаксисом. Если я правильно понимаю, он все еще не векторизован, потому что row_case_when должен вызвать rowwise, что означает, что case_when2 зацикливается внутри функции карты (или ее эквивалента)? Или я неправильно понял? - person Allan Cameron; 17.05.2020
comment
Конечно, векторизация означает, что он берет фрейм данных и действует построчно, а вы сами этого не делаете. в отличие от того, как это работает внутри. В конце концов, где-то всегда есть цикл, даже если он находится глубоко в коде C. Обратите внимание, что из-за того, как case_when действует в этом случае, это сложнее, чем просто перемещаться внутрь по строкам, если мы не хотим оборачивать каждую ногу в списке. - person G. Grothendieck; 17.05.2020
comment
Я думаю, что это очень хорошее определение векторизации, но кажется, что многие пользователи R возражают против того, что называется векторизованным, если оно создается циклом внутри R, в отличие от цикла в базовом коде C, поскольку он имеет гораздо большие накладные расходы. Я думаю, это то, что имел в виду @RonakShah, когда сказал, что на самом деле это не векторизация. В некотором смысле, это полезное различие, которое нужно сохранить, потому что разница в производительности настолько заметна, что пользователи R будут пытаться избегать циклов, применять функции и карты, если существует собственный способ C векторизации. Возможно, я ошибаюсь насчет того, как в сообществе используется термин - person Allan Cameron; 17.05.2020
comment
Скорее всего, вы не получите значительного ускорения от написания его на C, потому что время будет зависеть от кода R в ногах, который необходимо повторять. - person G. Grothendieck; 17.05.2020
comment
Я избегаю loops, apply функций и maps и использую векторизованные функции. Не только из соображений эффективности, но и потому, что они экономят мне шаги (где я также могу ввести ошибки). И они удаляют беспорядок из моего кода, не делая его загадочным. ИМХО, использование case-when дает мне гораздо более разборчивый код, чем любые другие условные выражения (if/else, ifelse, if_else, _8 _...) Но у меня были проблемы, когда я пытался заставить case_when возвращать тиблицы. - person David T; 17.05.2020
comment
Вот почему я приветствую row_case_when. Я уже скопировал его в свой личный служебный пакет. Я буду использовать его на этой неделе при разработке кода. - person David T; 17.05.2020
comment
Добавили обсуждение mutate_cond и mutate_when, которые были определены в цитируемых сообщениях SO. - person G. Grothendieck; 18.05.2020