ошибка при использовании NSE (в dplyr): объект «значение» не найден

Я пытаюсь ознакомиться с использованием NSE в моем коде, где это оправдано. Допустим, у меня есть пары столбцов, и я хочу создать новую строковую переменную для каждой пары, указывающую, совпадают ли значения в этой паре.

library(tidyverse)
library(magrittr)

df <- tibble(one.x = c(1,2,3,4),
             one.y = c(2,2,4,3),
             two.x = c(5,6,7,8),
             two.y = c(6,7,7,9),
             # not used but also in df
             extra = c(5,5,5,5))

Я пытаюсь написать код, который будет выполнять то же самое, что и следующий код:

df.mod <- df %>%
  # is one.x the same as one.y?
  mutate(one.x_suffix = case_when( 
    one.x == one.y ~ "same",
    TRUE ~ "different")) %>%
  # is two.x the same as two.y?
  mutate(two.x_suffix = case_when(
    two.x == two.y ~ "same",
    TRUE ~ "different"))

df.mod
#> # A tibble: 4 x 6
#>   one.x one.y two.x two.y one.x_suffix two.x_suffix
#>   <dbl> <dbl> <dbl> <dbl> <chr>        <chr>       
#> 1    1.    2.    5.    6. different    different   
#> 2    2.    2.    6.    7. same         different   
#> 3    3.    4.    7.    7. different    same        
#> 4    4.    3.    8.    9. different    different

В моих реальных данных у меня есть произвольное количество таких пар (например, three.x и three.y,...), поэтому я хочу написать более обобщенную процедуру, используя mutate_at.

Моя стратегия состоит в том, чтобы передать переменные «.x» как .vars, а затем gsub «x» вместо «y» на одной стороне проверки на равенство внутри case_when, например:

df.mod <- df %>%
  mutate_at(vars(one.x, two.x),
            funs(suffix = case_when(
              . == !!sym(gsub("x", "y", deparse(substitute(.)))) ~ "same",
              TRUE ~ "different")))
#> Error in mutate_impl(.data, dots): Evaluation error: object 'value' not found.

Это когда я получаю исключение. Похоже, часть gsub работает нормально:

df.debug <- df %>%
  mutate_at(vars(one.x, two.x),
            funs(suffix = gsub("x", "y", deparse(substitute(.)))))
df.debug
#> # A tibble: 4 x 6
#>   one.x one.y two.x two.y one.x_suffix two.x_suffix
#>   <dbl> <dbl> <dbl> <dbl> <chr>        <chr>       
#> 1    1.    2.    5.    6. one.y        two.y       
#> 2    2.    2.    6.    7. one.y        two.y       
#> 3    3.    4.    7.    7. one.y        two.y       
#> 4    4.    3.    8.    9. one.y        two.y

Здесь исключение вызывает операция !!sym(). Что я сделал не так?

Создана 07 ноября 2018 г. с помощью пакета reprex (v0.2.1)


person lost    schedule 08.11.2018    source источник


Ответы (2)


Проблема не в !!sym, как видно из следующего примера:

df %>% mutate_at( vars(one.x, two.x),
                  funs(suffix = case_when(
                    . == !!sym("one.y") ~ "same",
                    TRUE ~ "different")))
# # A tibble: 4 x 6
#   one.x one.y two.x two.y one.x_suffix two.x_suffix
#   <dbl> <dbl> <dbl> <dbl> <chr>        <chr>       
# 1     1     2     5     6 different    different   
# 2     2     2     6     7 same         different   
# 3     3     4     7     7 different    different   
# 4     4     3     8     9 different    different   

Проблема заключается в попытке раскавычить substitute(.) внутри case_when:

df %>% mutate_at( vars(one.x, two.x),
                  funs(suffix = case_when(
                    . == !!substitute(.) ~ "same",
                    TRUE ~ "different")))
# Error in mutate_impl(.data, dots) : 
#   Evaluation error: object 'value' not found.

Причиной этого является приоритет оператора. Со страницы справки для !!:

!! оператор раскапывает свой аргумент. Он немедленно оценивается в окружающем контексте.

В приведенном выше примере контекстом для !!substitute(.) является формула, которая сама находится внутри case_when. Это приводит к немедленной замене выражения на value, которое определено внутри case_when и не имеет значения внутри вашего фрейма данных.

Вы хотите, чтобы выражения находились рядом с их окружением, что и является quosures предназначены для. Заменив substitute на rlang::enquo, вы фиксируете выражение, которое породило ., вместе с его определяющей средой (вашим фреймом данных). Чтобы все было аккуратно, давайте переместим вашу манипуляцию gsub в отдельную функцию:

x2y <- function(.x)
{
  ## Capture the expression and its environment
  qq <- enquo(.x)

  ## Retrieve the expression and deparse it
  txt <- rlang::get_expr(qq) %>% rlang::expr_deparse()

  ## Replace x with y, as before
  txty <- gsub("x", "y", txt)

  ## Put the new expression back into the quosure
  rlang::set_expr( qq, sym(txty) )
}

Теперь вы можете использовать новую функцию x2y непосредственно в своем коде. При использовании квазур не требуется раскатываться, потому что выражения уже несут с собой свое окружение; вы можете просто оценить их, используя rlang::eval_tidy:

df %>% mutate_at(vars(one.x, two.x),
                 funs(suffix = case_when(
                   . == rlang::eval_tidy(x2y(.)) ~ "same",
                   TRUE ~ "different" )))
# # A tibble: 4 x 6
#   one.x one.y two.x two.y one.x_suffix two.x_suffix
#   <dbl> <dbl> <dbl> <dbl> <chr>        <chr>       
# 1     1     2     5     6 different    different   
# 2     2     2     6     7 same         different   
# 3     3     4     7     7 different    same        
# 4     4     3     8     9 different    different   

ИЗМЕНИТЬ, чтобы ответить на вопрос в вашем комментарии: объединение всего вашего кода в одну строку почти всегда является Плохой Идеей ™, и я настоятельно не рекомендую этого делать. Однако, поскольку этот вопрос касается NSE, я думаю, важно понять, почему простое взятие содержимого x2y и вставка его внутрь case_when приводит к проблемам.

enquo(), как и substitute(), посмотрите в среде вызова функции и замените аргумент выражением, предоставленным этой функции. substitute() поднимается только на одну среду вверх (находит value внутри case_when, когда вы не заключаете его в кавычки), в то время как enquo() продолжает двигаться вверх до тех пор, пока функции в стеке вызовов правильно обрабатывают квазицитата. (И большинство функций dplyr/tidyverse делают это.) Таким образом, когда вы вызываете enquo(.x) внутри x2y, он перемещает выражения, предоставляемые каждой функции в стеке вызовов, чтобы в конечном итоге найти one.x.

Когда вы вызываете enquo() внутри mutate_at, он теперь находится на том же уровне, что и one.x, поэтому он тоже заменяет аргумент (в данном случае one.x) выражением, которое его определило (в данном случае вектором c(1,2,3,4)). Это не то, что вы хотите. Вместо того, чтобы двигаться вверх по уровням, теперь вы хотите оставаться на том же уровне, что и one.x. Для этого используйте rlang::quo() вместо rlang::enquo():

library( rlang )   ## To maintain at least a little bit of sanity

df %>% 
 mutate_at(vars(one.x, two.x),
   funs(suffix = case_when(
    . == eval_tidy(set_expr(quo(.), 
                            sym(gsub("x","y", expr_deparse(get_expr(quo(.)))))
                       )
            ) ~ "same",
    TRUE ~ "different" )))
# Now works as expected
person Artem Sokolov    schedule 08.11.2018
comment
Спасибо! Объяснение очень полезно. У меня здесь много работы. Я упаковал функцию x2y в одну строку, чтобы лучше видеть структуру. set_expr(enquo(.x), sym(gsub("x", "y", expr_deparse(get_expr(enquo(.x)))))) отлично работает как уродливая версия функции x2y, но если я возьму ту же строку и заменю .x на . и положу это внутрь eval_tidy() (т. е. не создавая отдельную функцию), я получу исключение (‹dbl: 1, 2, 3, 4» не найдено). Требуется ли здесь создание именованной функции? - person lost; 08.11.2018

Вот вариант с map. Мы split разбиваем набор данных на пары столбцов «x», «y» с подстрокой имен столбцов, затем перебираем list наборов данных с map, transmute, чтобы создать новый столбец «суффикс», сравнивая строки каждого набора данных, связываем list наборов данных в один набор данных и привязать к исходному набору данных (bind_cols)

library(tidyverse)
df %>% 
    select(matches("\\.x|\\.y")) %>%
    split.default(str_remove(names(.), "\\..*")) %>%
    map( ~ .x %>%
                 transmute(!! paste0(names(.)[1], "_suffix") := 
                      reduce(., ~ c("different", "same")[(.x == .y) + 1]))) %>%
    bind_cols %>%
    bind_cols(df, .)
# A tibble: 4 x 7
#  one.x one.y two.x two.y extra one.x_suffix two.x_suffix
#   <dbl> <dbl> <dbl> <dbl> <dbl> <chr>        <chr>       
#1     1     2     5     6     5 different    different   
#2     2     2     6     7     5 same         different   
#3     3     4     7     7     5 different    same        
#4     4     3     8     9     5 different    different   

Или другой вариант - создать выражение, а затем разобрать его

library(rlang)
expr1 <- paste(grep("\\.x", names(df), value = TRUE), 
      grep("\\.y", names(df), value = TRUE), sep="==", collapse=";")
df %>% 
    mutate(!!!rlang::parse_exprs(expr1)) %>%
    rename_at(vars(matches("==")), ~ paste0(str_remove(.x, "\\s.*"), "_suffix"))
# A tibble: 4 x 7
#  one.x one.y two.x two.y extra one.x_suffix two.x_suffix
#  <dbl> <dbl> <dbl> <dbl> <dbl> <lgl>        <lgl>       
#1     1     2     5     6     5 FALSE        FALSE       
#2     2     2     6     7     5 TRUE         FALSE       
#3     3     4     7     7     5 FALSE        TRUE        
#4     4     3     8     9     5 FALSE        FALSE     

ПРИМЕЧАНИЕ. Его можно преобразовать в «тот же/другой», что и в первом решении. Но, может быть, лучше оставить его в виде логических столбцов.

person akrun    schedule 08.11.2018
comment
Спасибо. Мой фактический df содержит много дополнительных столбцов (я добавил это в пример), поэтому сначала нужно будет отложить остальные. Может с nest? - person lost; 08.11.2018