Search code examples
rfilterdplyrrlangtidyeval

R How to use curly curly with filter or filter_?


I was answering this question where commenters suggested !!ensym, and I thought this might be a good place to use curly curly {{ but I couldn't get it to work (maybe not applicable?).

How might I do this filter operation, without using filter_, eval/parse, or quote-unquote? Would ~ help?

My solution (1g) uses filter_ and conditions built up with paste. 1a works (but could it use {{ }} somehow?)

And what if we wanted to filter by multiple variables? This is where you see 2g working below (while 2a does not work anymore).

library(tidyverse)
set.seed(1234)
A <- matrix(rnorm(30),nrow = 10, ncol = 3) %>% as_tibble() %>% set_names(paste("var", seq(1:3), sep = ""))
varnames_1 <- c("var2")

(expected_result_1 <- filter(A, var2 > 0))
#> # A tibble: 3 x 3
#>     var1   var2   var3
#>    <dbl>  <dbl>  <dbl>
#> 1 -2.35  0.0645  0.460
#> 2  0.429 0.959  -0.694
#> 3 -0.890 2.42   -0.936

(answer_1a <- filter(A,!!ensym(varnames_1) > 0)) # works (thanks joran and aosmith)
#> # A tibble: 3 x 3
#>     var1   var2   var3
#>    <dbl>  <dbl>  <dbl>
#> 1 -2.35  0.0645  0.460
#> 2  0.429 0.959  -0.694
#> 3 -0.890 2.42   -0.936

(answer_1b <- filter_(A, varnames_1 > 0)) # filter_ not doing what I thought it might
#> Warning: filter_() is deprecated. 
#> Please use filter() instead
#> 
#> The 'programming' vignette or the tidyeval book can help you
#> to program with filter() : https://tidyeval.tidyverse.org
#> This warning is displayed once per session.
#> # A tibble: 10 x 3
#>      var1    var2    var3
#>     <dbl>   <dbl>   <dbl>
#>  1 -1.21  -0.477   0.134 
#>  2  0.277 -0.998  -0.491 
#>  3  1.08  -0.776  -0.441 
#>  4 -2.35   0.0645  0.460 
#>  5  0.429  0.959  -0.694 
#>  6  0.506 -0.110  -1.45  
#>  7 -0.575 -0.511   0.575 
#>  8 -0.547 -0.911  -1.02  
#>  9 -0.564 -0.837  -0.0151
#> 10 -0.890  2.42   -0.936

(answer_1c <- filter(A, {{varnames_1}} > 0)) # curly curly not doing what I thought it might
#> # A tibble: 10 x 3
#>      var1    var2    var3
#>     <dbl>   <dbl>   <dbl>
#>  1 -1.21  -0.477   0.134 
#>  2  0.277 -0.998  -0.491 
#>  3  1.08  -0.776  -0.441 
#>  4 -2.35   0.0645  0.460 
#>  5  0.429  0.959  -0.694 
#>  6  0.506 -0.110  -1.45  
#>  7 -0.575 -0.511   0.575 
#>  8 -0.547 -0.911  -1.02  
#>  9 -0.564 -0.837  -0.0151
#> 10 -0.890  2.42   -0.936
(answer_1d <- filter(A, {{varnames_1 > 0}})) # curly curly not doing what I thought it might
#> `arg` must be a symbol

conditions_1 <- paste(varnames_1, "> 0")
(answer_1e <- filter(A, conditions_1)) # does not work
#> Error: Argument 2 filter condition does not evaluate to a logical vector
(answer_1f <- filter(A, {{conditions_1}})) # curly curly not doing what I thought it might
#> Error: Argument 2 filter condition does not evaluate to a logical vector
(answer_1g <- filter_(A, conditions_1)) # works
#> # A tibble: 3 x 3
#>     var1   var2   var3
#>    <dbl>  <dbl>  <dbl>
#> 1 -2.35  0.0645  0.460
#> 2  0.429 0.959  -0.694
#> 3 -0.890 2.42   -0.936

# what if we wanted to filter multiple variables?

varnames_2 <- c("var2", "var3")
(expected_result_2 <- filter(A, var2 > 0 & var3 > 0))
#> # A tibble: 1 x 3
#>    var1   var2  var3
#>   <dbl>  <dbl> <dbl>
#> 1 -2.35 0.0645 0.460

(answer_2a <- filter(A,!!ensym(varnames_2) > 0)) # does not work
#> Only strings can be converted to symbols

conditions_2 <- paste(paste(varnames_2, "> 0"), collapse = " & ")
(answer_2f <- filter(A, {{conditions_2}})) # curly curly not doing what I thought it might
#> Error: Argument 2 filter condition does not evaluate to a logical vector
(answer_2g <- filter_(A, conditions_2)) # works
#> # A tibble: 1 x 3
#>    var1   var2  var3
#>   <dbl>  <dbl> <dbl>
#> 1 -2.35 0.0645 0.460

Created on 2019-08-28 by the reprex package (v0.3.0)


Solution

  • As Lionel points out, curly-curly works inside functions. To use it with filter, you thus have to wrap the call inside a function.

    f <- function(.df, v) { 
      filter(.df, {{ v }} > 0) 
    }
    
    # Curly-curly provides automatic NSE support
    f( A, var2 )
    # # A tibble: 3 x 3
    #     var1   var2   var3
    #    <dbl>  <dbl>  <dbl>
    # 1 -2.35  0.0645  0.460
    # 2  0.429 0.959  -0.694
    # 3 -0.890 2.42   -0.936
    
    # Strings have to be first converted to symbols
    f( A, !!sym("var3") )
    # # A tibble: 3 x 3
    #     var1    var2  var3
    #    <dbl>   <dbl> <dbl>
    # 1 -1.21  -0.477  0.134
    # 2 -2.35   0.0645 0.460
    # 3 -0.575 -0.511  0.575
    

    Curly-curly is meant to reference a single argument. You can extend it to work with multiple variables through sequential application with the help of purrr::reduce. (Don't forget to convert your strings into actual variable names first!):

    syms(varnames_2) %>% reduce(f, .init=A)
    # # A tibble: 1 x 3
    #    var1   var2  var3
    #   <dbl>  <dbl> <dbl>
    # 1 -2.35 0.0645 0.460