mutating variables (symbol or string) with tidy evaluation

Viewed 564

I want to use tidy evaluation in my function while mutating the entered variable, but can't seem to get the function to work properly.

I have read both tidy eval with e.g. mtcars %>% mutate(target := log(target)) and Tidy Evaluation not working with mutate and stringr, and they seem to suggest that there needs to be a different solution based on whether a symbol or a string is passed as an argument. But I'd prefer a general solution to this problem that works with both of these inputs.

Is there a way to do this?

function

library(magrittr)

foo <- function(data, x) {
  # I know this is one solution to solve my problem
  # But I want to avoid it because it creates problem if the function is to be used within a loop
  # x <- rlang::ensym(x)
  
  df <- tibble::as_tibble(dplyr::select(data, {{ x }}))
  
  df %>% dplyr::mutate(.data = ., {{ x }} := droplevels(as.factor({{ x }})))
}

works as expected

foo(mtcars, am)
#> # A tibble: 32 x 1
#>    am   
#>    <fct>
#>  1 1    
#>  2 1    
#>  3 1    
#>  4 0    
#>  5 0    
#>  6 0    
#>  7 0    
#>  8 0    
#>  9 0    
#> 10 0    
#> # ... with 22 more rows

doesn't work as expected

foo(mtcars, "am")
#> # A tibble: 32 x 1
#>    am   
#>    <fct>
#>  1 am   
#>  2 am   
#>  3 am   
#>  4 am   
#>  5 am   
#>  6 am   
#>  7 am   
#>  8 am   
#>  9 am   
#> 10 am   
#> # ... with 22 more rows

P.S. In case you are curious about the issue caused in loops by the possible solution, here is a reprex:

library(magrittr)
col.name <- colnames(mtcars)
foo <- function(data, x) {
  x <- rlang::ensym(x)
  df <- tibble::as_tibble(dplyr::select(data, {{ x }}))
  df %>% dplyr::mutate(.data = ., {{ x }} := droplevels(as.factor({{ x }})))
}

for (i in 3:length(mtcars)) {
  foo(mtcars, col.name[i])
}
#> Error: Only strings can be converted to symbols
2 Answers

It is generally not recommended to try and make your function compatible with both masked columns and column names, because this is not how tidyverse functions are designed to work.

First choose whether you want to take actions or selections. If you take the latter, then you can use tidyselect features like all_of() for compatibility with character vectors. An easy way to take selections in all dplyr verbs is to use across().

In this case you also use select() to begin with, so we can just use across(everything()). In general, you could do across({{ x }}).

foo <- function(data, x) {
  data %>%
    as_tibble() %>%
    select({{ x }}) %>%
    # Could also remove the `select()` step and pass `{{ x }}` to across()
    mutate(across(everything(), ~ droplevels(as.factor(.))))
}

foo(mtcars, am)

Since you've forwarded x to a selection context, your function now supports all tidyselect features. If you need to pass a character vector or a string, just use all_of(). Adapting your example:

nms <- names(mtcars)

for (i in 3:length(mtcars)) {
  foo(mtcars, all_of(nms[i]))
}

Maybe try with !!:

#Function
col.name <- colnames(mtcars)
foo <- function(data, x) {
  x <- rlang::ensym(x)
  df <- tibble::as_tibble(dplyr::select(data, {{ x }}))
  df %>% dplyr::mutate(.data = ., {{ x }} := droplevels(as.factor({{ x }})))
}
#Loop
for (i in 3:length(mtcars)) {
  print(foo(mtcars, !!col.name[i]))
}

The function will also work for foo(mtcars, !!'am') and foo(mtcars, am) if you want to use with the loop.

Related