Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Use dplyr's if_else function with functional true/false values

Tags:

r

dplyr

Question

How can dplyr's if_else() function be used such that its output is a function? With ifelse() from base this is trivial but with if_else() something goes wrong. Is that intended behavior or am I just not able to do that properly?

Reprex

x <- 2
ifelse(x == 2, min, max)
#> function (..., na.rm = FALSE)  .Primitive("min")
dplyr::if_else(x == 2, min, max)
#> Error in true[rep(NA_integer_, length(condition))]: object of type 'builtin' is not subsettable

Created on 2021-12-04 by the reprex package (v2.0.0)

like image 297
AlbertRapp Avatar asked Jul 16 '26 13:07

AlbertRapp


2 Answers

You shouldn't use ifelse for this. ifelse and if_else are for vectors--specifically for when your input is a vector and your output is a vector of the same length. You can't make vectors of functions, so ifelse is a poor choice for returning a function. Your code will only work in the special case when the input has length 1---which is what the control/flow function if() is for.

Your ifelse code will fail with a strange error if x has length > 1, try setting x = 2:3 and running your code.

> x = 2:3
> ifelse(x == 2, min, max)
Error in rep(yes, length.out = len) : 
  attempt to replicate an object of type 'builtin'

if(), on the other hand, will still check the first element and return the correct result for the first element while giving you a warning about the input length:

> if(x == 2) min else max
function (..., na.rm = FALSE)  .Primitive("min")
Warning message:
In if (x == 2) min else max :
  the condition has length > 1 and only the first element will be used

When you're checking a condition that should always have length 1 you should use if(){}else{}, and the result can be literally any expression, it can be assigned, or it can be arbitrary code that is run. Save ifelse (and if_else) for vectors as intended.

like image 149
Gregor Thomas Avatar answered Jul 19 '26 04:07

Gregor Thomas


If x is always a scalar then using if as described in @Gregor Thomas answer is the correct approach.

Another option would be to use switch, which makes sense, if you have more than two options and want to minimize the control flow:

x <- 2
min_max <- switch(as.character(x), `2` = min, max)

If, however, you are actually dealing with a vector for example in a {dplyr} pipeline, then using character vectors instead of functions in combination with do.call is one approach.

Here is a short example where we want to use a different function (min or max) depending on the am group in mtcars:

library(dplyr)

mtcars %>% 
  nest_by(am) %>% 
  mutate(min_max = if_else(am == 1, "min", "max"),
         mpg = summarise(data, x = do.call(min_max, list(mpg)))$x)

#> # A tibble: 2 x 4
#> # Rowwise:  am
#>      am                data min_max   mpg
#>   <dbl> <list<tibble[,10]>> <chr>   <dbl>
#> 1     0           [19 × 10] max      24.4
#> 2     1           [13 × 10] min      15

Another approach is to wrap each min, max in list(). In this case, we do not need do.call:

library(dplyr)

mtcars %>% 
  nest_by(am) %>% 
  mutate(min_max = if_else(am == 1, list(min), list(max)),
         mpg = summarise(data, x = min_max(mpg))$x)

#> # A tibble: 2 x 4
#> # Rowwise:  am
#>      am                data min_max   mpg
#>   <dbl> <list<tibble[,10]>> <list>  <dbl>
#> 1     0           [19 × 10] <fn>     24.4
#> 2     1           [13 × 10] <fn>     15

Created on 2021-12-04 by the reprex package (v0.3.0)

like image 41
TimTeaFan Avatar answered Jul 19 '26 02:07

TimTeaFan