Function calling variable names for group_by in dplyr - how do I vectorise this variable in the function?

Viewed 2998

I have created a function in R that takes a fixed data-frame and uses dplyr to give me summary statistics (e.g., the mean of a particular variable) grouped by a chosen argument variable. Here is some code showing a toy data-frame and my function:

#Create data frame for analysis
DF <- data.frame(Type1  = c(0,0,1,1,0,1,1,0,1,0,1,1,1,0),
                 Type2  = c(1,1,1,1,1,1,2,2,2,2,3,3,3,3),
                 Output = c(4,2,7,5,1,1,7,8,3,2,5,4,3,6));

#Inspect the data-frame
DF;

   Type1 Type2 Output
1      0     1      4
2      0     1      2
3      1     1      7
4      1     1      5
5      0     1      1
6      1     1      1
7      1     2      7
8      0     2      8
9      1     2      3
10     0     2      2
11     1     3      5
12     1     3      4
13     1     3      3
14     0     3      6

#Create a function that summarises the mean output grouped by input variable
MEAN_OUT <- function(VAR) { DF %>% group_by(!! sym(VAR)) %>% 
                                   summarise(Mean = mean(Output)) %>% 
                                   as.data.frame(); }

#Call the function grouping by variable 'Type1'
MEAN_OUT('Type1')

  Type1     Mean
1     0 3.714286
2     1 4.444444

At the moment I can call MEAN_OUT('Type1') or MEAN_OUT('Type2') and these give me the correct summaries grouped by either of these argument variables. However, I would like to also be able to call MEAN_OUT(c('Type1','Type2')) to get a summary grouped over both variables. You can do this in the dplyr::group_by function, but I cannot figure out how to do it when this material is wrapped in my function. If I use my present function (shown above) to try to group by both variables I get the following error:

MEAN_OUT(c('Type1','Type2'))
Error: Only strings can be converted to symbols
2 Answers

It would be better to use syms if the intention is to pass more than one grouping variable as a vector

library(dplyr)
library(rlang)
MEAN_OUT <- function(VARS) { 
                 DF %>% 
                    group_by(!!! syms(VARS)) %>% 
                    summarise(Mean = mean(Output)) %>% 
                    as.data.frame() 
         }

However, we can make use of the group_by_at that can take string as input avoiding the syms and evaluation (!!!)

MEAN_OUT2 <- function(VARS) {
                DF %>% 
                     group_by_at(VARS) %>% 
                     summarise(Mean = mean(Output)) %>% 
                     as.data.frame()
    }

-testing

identical(MEAN_OUT('Type1'), MEAN_OUT2('Type1'))
#[1] TRUE

identical(MEAN_OUT(c('Type1', 'Type2')), MEAN_OUT2(c('Type1', 'Type2')))
#[1] TRUE

Instead of passing as quoted string, there are other option to pass as quosure

MEAN_OUT3 <- function(VARS) {
                    DF %>% 
                        group_by(!!! VARS) %>% 
                               summarise(Mean = mean(Output)) %>% 
                               as.data.frame() 
                                  }

identical(MEAN_OUT('Type1'), MEAN_OUT3(quos(Type1)))
#[1] TRUE
identical(MEAN_OUT(c('Type1', 'Type2')), MEAN_OUT3(quos(Type1, Type2)))
#[1] TRUE

Or call the quos inside the function by passing the arguments as ...

MEAN_OUT4 <- function(...) {

                    DF %>% 
                        group_by(!!! quos(...)) %>% 
                               summarise(Mean = mean(Output)) %>% 
                               as.data.frame() 
                                  }

identical(MEAN_OUT('Type1'), MEAN_OUT4(Type1))
#[1] TRUE

identical(MEAN_OUT(c('Type1', 'Type2')), MEAN_OUT4(Type1, Type2))
#[1] TRUE

@akrun's answer offers a working solution, but I think this is an ideal situation to wrap function parameters in vars(), passing the variables you want to group by as a quasi-quotation that dplyr can interpret without any explicit tidyeval code in the body of the function.

library(tidyverse)
#> -- Attaching packages ------------------------------------ tidyverse 1.2.1 --
#> v ggplot2 3.0.0     v purrr   0.2.5
#> v tibble  1.4.2     v dplyr   0.7.6
#> v tidyr   0.8.0     v stringr 1.3.1
#> v readr   1.1.1     v forcats 0.3.0
#> -- Conflicts --------------------------------------- tidyverse_conflicts() --
#> x dplyr::filter() masks stats::filter()
#> x dplyr::lag()    masks stats::lag()
# Create data frame for analysis
dat <- data.frame(
  Type1  = c(0, 0, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 1, 0),
  Type2  = c(1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3),
  Output = c(4, 2, 7, 5, 1, 1, 7, 8, 3, 2, 5, 4, 3, 6)
)
# using the dplyr::vars() quoting function has 3 main advantages: 
# 1. It makes functions neater
mean_out <- function(.vars) {

  dat %>% 

    # group_by will continue to work for basic selections
    # group_by_at allows for full tidyselect functionality 
    group_by_at(.vars) %>% 
    summarise(mean = mean(Output)) 
}
# 2. It lets us harness the power of tidyselect
mean_out(vars(Type1))
#> # A tibble: 2 x 2
#>   Type1  mean
#>   <dbl> <dbl>
#> 1     0  3.83
#> 2     1  4.38
mean_out(vars(Type1, Type2))
#> # A tibble: 6 x 3
#> # Groups:   Type1 [?]
#>   Type1 Type2  mean
#>   <dbl> <dbl> <dbl>
#> 1     0     1  2.33
#> 2     0     2  5   
#> 3     0     3  6   
#> 4     1     1  4.33
#> 5     1     2  5   
#> 6     1     3  4
mean_out(vars(-Output))
#> # A tibble: 6 x 3
#> # Groups:   Type1 [?]
#>   Type1 Type2  mean
#>   <dbl> <dbl> <dbl>
#> 1     0     1  2.33
#> 2     0     2  5   
#> 3     0     3  6   
#> 4     1     1  4.33
#> 5     1     2  5   
#> 6     1     3  4
mean_out(vars(matches("Type")))
#> # A tibble: 6 x 3
#> # Groups:   Type1 [?]
#>   Type1 Type2  mean
#>   <dbl> <dbl> <dbl>
#> 1     0     1  2.33
#> 2     0     2  5   
#> 3     0     3  6   
#> 4     1     1  4.33
#> 5     1     2  5   
#> 6     1     3  4
# 3. It doesn't demand that we load rlang, since it's built into dplyr
Related