Lookup function for mutate in data

Viewed 319

I'd like to store functions, or at least their names, in a column of a data.frame for use in a call to mutate. A simplified broken example:

library(dplyr)
expand.grid(mu = 1:5, sd = c(1, 10), stat = c('mean', 'sd')) %>%
  group_by(mu, sd, stat) %>%
  mutate(sample = get(stat)(rnorm(100, mu, sd))) %>%
  ungroup()

If this worked how I thought it would, the value of sample would be generated by the function in .GlobalEnv corresponding to either 'mean' or 'sd', depending on the row.

The error I get is:

 Error in mutate_impl(.data, dots) : 
    Evaluation error: invalid first argument. 

Surely this has to do with non-standard evaluation ... grrr.

2 Answers

A few issues here. First expand.grid will convert character values to factors. And get() doesn't like working with factors (ie get(factor("mean")) will give an error). The tidyverse-friendly version is tidyr::crossing(). (You could also pass stringsAsFactors=FALSE to expand.grid.)

Secondly, mutate() assumes that all functions you call are vectorized, but functions like get() are not vectorized, they need to be called one-at-a-time. A safer way rather than doing the group_by here to guarantee one-at-a-time evaluation is to use rowwise().

And finally, your real problem is that you are trying to call get("sd") but when you do, sd also happens to be a column in your data.frame that is part of the mutate. Thus get() will find this sd first, and this sd is just a number, not a function. You'll need to tell get() to pull from the global environment explicitly. Try

tidyr::crossing(mu = 1:5, sd = c(1, 10), stat = c('mean', 'sd')) %>%
  rowwise() %>% 
  mutate(sample = get(stat, envir = globalenv())(rnorm(100, mu, sd)))

Three problems (that I see): (1) expand.grid is giving you factors; (2) get finds variables, so using "sd" as a stat is being confused with the column names "sd" (that was hard to find!); and (3) this really is a row-wise operation, grouping isn't helping enough. The first is easily fixed with an option, the second can be fixed by using match.fun instead of get, and the third can be mitigated with dplyr::rowwise, purrr::pmap, or base R's mapply.

This helper function was useful during debugging and can be used to "clean up" the code within mutate, but it isn't required (for other than this demonstration). Inline "anonymous" functions will work as well.

func <- function(f,m,s) get(f)(rnorm(100,mean=m,sd=s))

Several implementation methods:

set.seed(0)
expand.grid(mu = 1:5, sd = c(1, 10), stat = c('mean', 'sd'),
            stringsAsFactors=FALSE)  %>%
  group_by(mu, sd, stat) %>%              # can also be `rowwise() %>%`
  mutate(
    sample0 = match.fun(stat)(rnorm(100, mu, sd)),
    sample1 = purrr::pmap_dbl(list(stat, mu, sd), ~ match.fun(..1)(rnorm(100, ..2, ..3))),
    sample2 = purrr::pmap_dbl(list(stat, mu, sd), func),
    sample3 = mapply(function(f,m,s) match.fun(f)(rnorm(100,m,s)), stat, mu, sd),
    sample4 = mapply(func, stat, mu, sd)
  ) %>%
  ungroup()
# # A tibble: 20 x 8
#       mu    sd stat  sample0 sample1 sample2 sample3 sample4
#    <int> <dbl> <chr>   <dbl>   <dbl>   <dbl>   <dbl>   <dbl>
#  1     1     1 mean    1.02    1.03    0.896   1.08    0.855
#  2     2     1 mean    1.95    2.07    2.05    1.90    1.92 
#  3     3     1 mean    2.93    3.07    3.03    2.89    3.01 
#  4     4     1 mean    4.01    3.94    4.23    4.05    3.96 
#  5     5     1 mean    5.04    5.11    5.05    5.17    5.19 
#  6     1    10 mean    1.67    1.21    1.30    2.08   -0.641
#  7     2    10 mean    1.82    2.82    2.35    3.65    1.78 
#  8     3    10 mean    1.45    3.10    3.15    4.28    2.58 
#  9     4    10 mean    3.49    6.33    5.11    2.84    3.41 
# 10     5    10 mean    5.33    4.85    4.07    5.58    6.66 
# 11     1     1 sd      0.965   1.04    0.993   0.942   1.08 
# 12     2     1 sd      0.974   0.967   0.981   0.984   1.15 
# 13     3     1 sd      1.12    0.902   1.06    0.977   1.02 
# 14     4     1 sd      0.946   0.928   0.960   1.01    0.992
# 15     5     1 sd      1.06    1.01    0.911   1.11    1.00 
# 16     1    10 sd      9.46    8.95   10.0     8.91    9.60 
# 17     2    10 sd      9.51    9.11   11.5     9.85   10.6  
# 18     3    10 sd      9.77    9.96   11.0     9.09   10.7  
# 19     4    10 sd     10.5     9.84   10.1    10.6     8.89 
# 20     5    10 sd     11.2     8.82   10.4     9.06    9.64 

sample0 happens to work because you have grouped it to be row-wise. If at some point any one grouping has two or more values, this will fail.

For sample1 through sample4, you can remove the group_by and it works equally well (though sample0 demonstrates its failing, so remove it too). You won't get identical results as above with grouping removed, because the entropy is being consumed differently.

Related