purrr + dplyr Проблемы NSE внутри функции, написанной пользователем - PullRequest
4 голосов
/ 29 мая 2020

После множества проб и ошибок и консультации с предыдущими ответами, такими как Как определить, является ли голая переменная или строка Я думаю, что я сделал большую часть того, что мне нужно, сам. Но я очень хочу понять, делаю ли я какие-то неверные предположения или глупо подхожу к проблеме, прежде чем я перенесу свое «решение» в производство.

Рассмотрим следующие данные:

library(dplyr)
library(purrr)
library(tidyselect)

set.seed(1111)
dat1 <- data.frame(Region = rep(c("r1","r2"), each = 100),
                   State = rep(c("NY","MA","FL","GA"), each = 10),
                   Loc = rep(c("a","b","c","d","e","f","g","h"),each = 5),
                   ID = rep(c(1:10), each = 2),
                   var1 = rnorm(200),
                   var2 = rnorm(200),
                   var3 = rnorm(200),
                   var4 = rnorm(200),
                   var5 = rnorm(200))

Я хочу написать функцию, которая делает довольно много вещей, но я начну с минимально воспроизводимого примера. Я хочу получить tidied aov результатов либо для единственного случая var1 ~ State, либо для пары совпадающих списков, используя map2 с одним списком, содержащим "результаты" другой " предикторы ". Они никогда не идентичны в зависимости от использования, и переменные, в отличие от моего примера, редко поддаются легким решениям, например starts_with.

Две конкретные c проблемы и общий c вопрос.

Проблема №1 - я отказался от того, чтобы разрешать конечным пользователям (включая меня) передавать голые имена переменных, позже всегда вызывает у меня проблемы. В соответствии с приведенной выше ссылкой это что-то вроде моего кода - самый быстрый и надежный способ их поймать и сообщить пользователю? (Я помещаю комментарий в код, чтобы указать, о чем я говорю.

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

Generi c вопрос: Я добавил рекомендуемый tidyselect::all_of, потому что это могут быть неоднозначные списки, почему мне все еще нужно защищаться от появления .x и .y как вызовы, а не просто маркеры для итерации?

MyFunction <- function(data,
                 groupvar,
                 var) {
  # Issue #1 is this best way to warn/stop user?
  lst <- as.list(match.call())

  if (is.symbol(lst$groupvar) || is.symbol(lst$var)) {
    stop("Please quote all variables")
  }

  # Issue #2 I want the group label but if I don't include
  # this if logic it errors with " Error: Can't convert a call to a string"
  # when I run it with purrr::map2
  if (!is.call(groupvar)) {
     grouplabel <- rlang::as_name(rlang::enquo(groupvar))
  }

  data <-
    dplyr::select(
      .data = data,
      var = {{ var }},
      groupvar = {{ groupvar }}
    )

  aov_object <- aov(var ~ groupvar, data = data)
  aov_results <- broom::tidy(aov_object) %>%
    mutate(term = if_else(term != "Residuals", grouplabel, term))
  return(aov_results)
}

# Expected output

MyFunction(data = dat1, groupvar = "State", var = "var1") # works
#> # A tibble: 2 x 6
#>   term         df  sumsq meansq statistic p.value
#>   <chr>     <dbl>  <dbl>  <dbl>     <dbl>   <dbl>
#> 1 State         3   1.75  0.582     0.485   0.693
#> 2 Residuals   196 235.    1.20     NA      NA

MyFunction(data = dat1, groupvar = State, var = var1) # warns appropriately
#> Error in MyFunction(data = dat1, groupvar = State, var = var1): Please quote all variables

# Quick test of `map2`
grouping_vars <- names(dat1[,1:3])
names(grouping_vars) <- names(dat1[,1:3])

outcome_vars <- names(dat1[,5:7])
names(outcome_vars) <- names(dat1[,5:7])

names(outcome_vars) <- paste(outcome_vars, "~", grouping_vars)

# get multiple results this is where issue #2 comes in but this is what I want it to look like.

map2(.x = outcome_vars,
     .y = grouping_vars,
     .f = ~ MyFunction(dat = dat1,
                 var = tidyselect::all_of(.x),
                 groupvar = tidyselect::all_of(.y)))
#> $`var1 ~ Region`
#> # A tibble: 2 x 6
#>   term         df    sumsq meansq statistic p.value
#>   <chr>     <dbl>    <dbl>  <dbl>     <dbl>   <dbl>
#> 1 Region        1   0.0512 0.0512    0.0427   0.836
#> 2 Residuals   198 237.     1.20     NA       NA    
#> 
#> $`var2 ~ State`
#> # A tibble: 2 x 6
#>   term         df  sumsq meansq statistic p.value
#>   <chr>     <dbl>  <dbl>  <dbl>     <dbl>   <dbl>
#> 1 State         3   5.05  1.68       2.07   0.106
#> 2 Residuals   196 159.    0.814     NA     NA    
#> 
#> $`var3 ~ Loc`
#> # A tibble: 2 x 6
#>   term         df  sumsq meansq statistic p.value
#>   <chr>     <dbl>  <dbl>  <dbl>     <dbl>   <dbl>
#> 1 Loc           7   5.09  0.727     0.772   0.612
#> 2 Residuals   192 181.    0.943    NA      NA

Ответы [ 2 ]

4 голосов
/ 02 июня 2020

Мне кажется, что, поскольку вы настаиваете на передаче строк в качестве имен переменных, было бы проще и эффективнее изменить формулу для соответствия переменным, используя as.formula, а не изменять данные. Это также избавляет вас от необходимости отдельно называть группирующую переменную внутри функции.

Следующая функция короче и примерно в два раза быстрее в тестировании, чем исходная, но поведение остается неизменным:

MyFunctionNew <- function(data, groupvar, var) 
{  
  lst <- as.list(match.call())
  if (is.symbol(lst$groupvar) || is.symbol(lst$var)) 
    stop("Please quote all variables")

  broom::tidy(aov(as.formula(paste(var, "~", groupvar)), data = data)) %>%
    mutate(term = if_else(term != "Residuals", groupvar, term))
}

Вы можете видеть, что он по-прежнему работает внутри map2:

map2(.x = outcome_vars,
     .y = grouping_vars,
     .f = ~ MyFunctionNew(dat = dat1,
                       var = tidyselect::all_of(.x),
                       groupvar = tidyselect::all_of(.y)))
#> $`var1 ~ Region`
#> # A tibble: 2 x 6
#>   term         df    sumsq meansq statistic p.value
#>   <chr>     <dbl>    <dbl>  <dbl>     <dbl>   <dbl>
#> 1 Region        1   0.0512 0.0512    0.0427   0.836
#> 2 Residuals   198 237.     1.20     NA       NA    
#> 
#> $`var2 ~ State`
#> # A tibble: 2 x 6
#>   term         df  sumsq meansq statistic p.value
#>   <chr>     <dbl>  <dbl>  <dbl>     <dbl>   <dbl>
#> 1 State         3   5.05  1.68       2.07   0.106
#> 2 Residuals   196 159.    0.814     NA     NA    
#> 
#> $`var3 ~ Loc`
#> # A tibble: 2 x 6
#>   term         df  sumsq meansq statistic p.value
#>   <chr>     <dbl>  <dbl>  <dbl>     <dbl>   <dbl>
#> 1 Loc           7   5.09  0.727     0.772   0.612
#> 2 Residuals   192 181.    0.943    NA      NA    

Что касается проверочных переменных, чтобы убедиться, что они являются строками символов, я не думаю, что это идиоматия c использование R, и может вызвать путаницу у случайных пользователей вашей функции. Другими словами, это нарушает принцип наименьшего удивления .

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

MyVar <- "State"
MyFunction(data = dat1, groupvar = MyVar, var = "var1")

Однако я получаю сообщение об ошибке, говорящее мне, что все переменные должны цитироваться.

Это также означает, что ваша функция не будет работать в базовых циклах R и *apply функциях:

lapply(c("State", "Region", "ID"), function(x) MyFunction(dat1, x, "var1"))
#> Error in MyFunction(dat1, x, "var1") : Please quote all variables 

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

MyFunction <- function(data, groupvar, var) 
{  
  broom::tidy(aov(as.formula(paste(var, "~", groupvar)), data = data)) %>%
    mutate(term = if_else(term != "Residuals", groupvar, term))
}

Что работает следующим образом:

MyFunction(data = dat1, groupvar = "State", var = "var1") 
#> # A tibble: 2 x 6
#>   term         df  sumsq meansq statistic p.value
#>   <chr>     <dbl>  <dbl>  <dbl>     <dbl>   <dbl>
#> 1 State         3   1.75  0.582     0.485   0.693
#> 2 Residuals   196 235.    1.20     NA      NA    

MyFunction(data = dat1, groupvar = MyVar, var = "var1")
#> # A tibble: 2 x 6
#>   term         df  sumsq meansq statistic p.value
#>   <chr>     <dbl>  <dbl>  <dbl>     <dbl>   <dbl>
#> 1 State         3   1.75  0.582     0.485   0.693
#> 2 Residuals   196 235.    1.20     NA      NA    

MyFunction(data = dat1, groupvar = State, var = var1) 
#>  Error in paste(var, "~", groupvar) : object 'State' not found 

Я думаю, что большинство пользователей R поймут, почему они получают эту последнюю ошибку, так как это довольно ясно. Это также ошибка, которую обычные пользователи R видели много раз. Если вы меньше доверяете своим пользователям, возможно, вы могли бы попробовать обернуть тело функции в tryCatch, которое преобразует ошибку «символ не найден» в ошибку «пожалуйста, используйте кавычки».

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

1 голос
/ 02 июня 2020

Я решил проблему №1. Ваша функция работает независимо от того, указаны ли имена переменных в кавычках или нет.

MyFunction <- function(data,
                       groupvar,
                       var) {
  # Issue #1 is this best way to warn/stop user?
  lst <- as.list(match.call())

  if (is.symbol(lst$groupvar)) {
    q <- paste0("groupvar")
    varname <- expr('$'(lst,!!q))
    gval <- eval_tidy(varname)
    groupvarc <- as.character(gval)
  }else{groupvarc <- eval_tidy(lst$groupvar)}

  if (is.symbol(lst$var)) {
    v <- paste0("var")
    varnam <- expr('$'(lst,!!v))
    vval <- eval_tidy(varnam)
    varc <- as.character(vval)
  }else{varc <- eval_tidy(lst$var)}

  grouplabel <- groupvarc[1] 

  data <- dplyr::select(.data = data,
                        var = varc[[1]],
                        groupvar = groupvarc[[1]] )

  aov_object <- aov(var ~ groupvar, data = data)
  aov_results <- broom::tidy(aov_object)  %>%
     mutate(term = if_else(term != "Residuals", grouplabel, term))
  return(aov_results)
}

MyFunction(data = dat1, groupvar = "State", var = "var1") # works

MyFunction(data = dat1, groupvar = State, var = var1) # Also works

Для нескольких переменных вам нужно будет сделать их функцией и циклически перебирать lapply. Кроме того, это упорядочит мое повторение одного и того же кода два раза для проблемы №1. Надеюсь, это поможет вам двигаться вперед.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...