`weighted.mean` in a function with an optional argument?

Viewed 101

I need to include the weighted.mean function enclosed in another function as part of a project I'm working on. I'm having trouble getting the w argument to work correctly in the function I'm working on.

For my overall function to work properly I need to meet to make the weights argument needs to be optional. If there is no weights given I need to default to some kind of wt = 1. This is probably easy to do with a conditional statement where I make a dummy column filled with 1's but I am wondering if this is the best way.

library(tidyverse)

my_weighted_mean <- function(var, wt) {
  var = enquo(var)
  
  mtcars %>%
    summarise_at(vars(!!var), ~weighted.mean(., w = wt))
  
}

## wrong output
my_weighted_mean(cyl, wt = "hp")
#>        cyl
#> 1 6.599231

## expected output
weighted.mean(mtcars$cyl, mtcars$hp)
#> [1] 6.860673

Created on 2020-10-27 by the reprex package (v0.3.0)

2 Answers

Final update
Making wt an optional argument (wt = NULL) was more complex than I anticipated. Below is an approach using tryCatch. Once we know whether wt is NULL we can replace it with a vector of 1 with the length of your data.frame. Otherwise, we can just use it with the curly-curly operator. Following the OP's comments the new function takes only one Argument x to summarise one variable and multiple grouping variables can be put into the ellipsis ....

library(dplyr)
library(rlang)

my_weighted_mean <- function(.dat, x, ..., wt = NULL) {
  
  .pred <- tryCatch(
    is.null(wt),
    error = function(e) {
      is.null(rlang::eval_tidy(enquo(wt), data = mtcars))
    })
  
  .dat %>% 
    group_by(...) %>% 
    summarise(
      {{x}} := weighted.mean({{x}},
                             w = if (.pred) rep(1, length({{x}})) else {{wt}} ))
  
}

mtcars %>% 
  my_weighted_mean(mpg)
#> # A tibble: 1 x 1
#>     mpg
#>   <dbl>
#> 1  20.1

mtcars %>% 
  my_weighted_mean(mpg, cyl)
#> `summarise()` ungrouping output (override with `.groups` argument)
#> # A tibble: 3 x 2
#>     cyl   mpg
#>   <dbl> <dbl>
#> 1     4  26.7
#> 2     6  19.7
#> 3     8  15.1

mtcars %>% 
  my_weighted_mean(mpg, cyl, wt = disp)
#> `summarise()` ungrouping output (override with `.groups` argument)
#> # A tibble: 3 x 2
#>     cyl   mpg
#>   <dbl> <dbl>
#> 1     4  25.8
#> 2     6  19.8
#> 3     8  14.9

mtcars %>% 
  my_weighted_mean(mpg, cyl, gear, wt = disp)
#> `summarise()` regrouping output by 'cyl' (override with `.groups` argument)
#> # A tibble: 8 x 3
#> # Groups:   cyl [3]
#>     cyl  gear   mpg
#>   <dbl> <dbl> <dbl>
#> 1     4     3  21.5
#> 2     4     4  25.9
#> 3     4     5  27.9
#> 4     6     3  19.9
#> 5     6     4  19.7
#> 6     6     5  19.7
#> 7     8     3  14.8
#> 8     8     5  15.4

Created on 2020-10-28 by the reprex package (v0.3.0)

older answer
You need to enqou() wt as well or just use the curly-curly operator. If you want to insert more than one variable into var then you can use the ellipsis ... instead of the variable name wrapped into curly-curly.

library(tidyverse)

my_weighted_mean <- function(var, wt) {
  
  mtcars %>%
    summarise_at(vars({{var}}), ~weighted.mean(., w = {{wt}}))
  
}

my_weighted_mean(cyl, wt = hp)
#>        cyl
#> 1 6.860673


my_weighted_mean <- function(..., wt) {

  mtcars %>%
    summarise_at(vars(...), ~weighted.mean(., w = {{wt}}))
  
}

my_weighted_mean(cyl, disp, wt = hp)
#>        cyl     disp
#> 1 6.860673 275.1096

Created on 2020-10-27 by the reprex package (v0.3.0)

Fromer update of old answer (corrected) As @Konrad Rudolph correctly points out, summarise_at is superseded and you don't need it for a single variable - here summarise is enough. If you want to summarise many variables, the new official way would be to use across() as follows:

my_weighted_mean <- function(..., wt) {
  
  mtcars %>%
    summarise(across(c(...),
                     ~weighted.mean(., w = {{wt}})))
  
}

my_weighted_mean(cyl, disp, wt = hp)

As Tim mentions, you need to handle wt in the same way as var, i.e. by passing it as an unquoted expression, and enquoting it inside the function.

Furthermore, since we’re already using tidy evaluation, there’s no need to for summarize_at (which is, incidentally, superseded).

my_weighted_mean = function (.data, var, wt) {
    dplyr::summarize(.data, {{var}} := weighted.mean({{var}}, w = {{wt}}))
}
my_weighted_mean(mtcars, cyl, hp)
#        cyl
# 1 6.860673
Related