Any difference between dplyr::across and purrr::modify_if/at?

Viewed 57

If one wanted to apply a function to one or more columns, optionally based on some criteria, it seems that there are two identical ways to do this with Tidyverse packages. I'm wondering if there's any reason to prefer one over the other?

For example, if one wanted to convert all numeric columns to character, you could do either of the following:

library(dplyr, warn.conflicts = FALSE)
library(purrr)
library(ggplot2)

mpg |> 
  mutate(across(where(is.numeric), as.character)) |> 
  slice_head(n = 5)
#> # A tibble: 5 × 11
#>   manufacturer model displ year  cyl   trans      drv   cty   hwy   fl    class 
#>   <chr>        <chr> <chr> <chr> <chr> <chr>      <chr> <chr> <chr> <chr> <chr> 
#> 1 audi         a4    1.8   1999  4     auto(l5)   f     18    29    p     compa…
#> 2 audi         a4    1.8   1999  4     manual(m5) f     21    29    p     compa…
#> 3 audi         a4    2     2008  4     manual(m6) f     20    31    p     compa…
#> 4 audi         a4    2     2008  4     auto(av)   f     21    30    p     compa…
#> 5 audi         a4    2.8   1999  6     auto(l5)   f     16    26    p     compa…

mpg |> 
  modify_if(is.numeric, as.character) |> 
  slice_head(n = 5)
#> # A tibble: 5 × 11
#>   manufacturer model displ year  cyl   trans      drv   cty   hwy   fl    class 
#>   <chr>        <chr> <chr> <chr> <chr> <chr>      <chr> <chr> <chr> <chr> <chr> 
#> 1 audi         a4    1.8   1999  4     auto(l5)   f     18    29    p     compa…
#> 2 audi         a4    1.8   1999  4     manual(m5) f     21    29    p     compa…
#> 3 audi         a4    2     2008  4     manual(m6) f     20    31    p     compa…
#> 4 audi         a4    2     2008  4     auto(av)   f     21    30    p     compa…
#> 5 audi         a4    2.8   1999  6     auto(l5)   f     16    26    p     compa…

Or, if you just wanted to change the year column, either of the following would work:

library(dplyr, warn.conflicts = FALSE)
library(purrr)
library(ggplot2)

mpg |> 
  mutate(year = as.character(year)) |> 
  slice_head(n = 5)
#> # A tibble: 5 × 11
#>   manufacturer model displ year    cyl trans      drv     cty   hwy fl    class 
#>   <chr>        <chr> <dbl> <chr> <int> <chr>      <chr> <int> <int> <chr> <chr> 
#> 1 audi         a4      1.8 1999      4 auto(l5)   f        18    29 p     compa…
#> 2 audi         a4      1.8 1999      4 manual(m5) f        21    29 p     compa…
#> 3 audi         a4      2   2008      4 manual(m6) f        20    31 p     compa…
#> 4 audi         a4      2   2008      4 auto(av)   f        21    30 p     compa…
#> 5 audi         a4      2.8 1999      6 auto(l5)   f        16    26 p     compa…

mpg |> 
  modify_at('year', as.character) |> 
  slice_head(n = 5)
#> # A tibble: 5 × 11
#>   manufacturer model displ year    cyl trans      drv     cty   hwy fl    class 
#>   <chr>        <chr> <dbl> <chr> <int> <chr>      <chr> <int> <int> <chr> <chr> 
#> 1 audi         a4      1.8 1999      4 auto(l5)   f        18    29 p     compa…
#> 2 audi         a4      1.8 1999      4 manual(m5) f        21    29 p     compa…
#> 3 audi         a4      2   2008      4 manual(m6) f        20    31 p     compa…
#> 4 audi         a4      2   2008      4 auto(av)   f        21    30 p     compa…
#> 5 audi         a4      2.8 1999      6 auto(l5)   f        16    26 p     compa…

Is there any reason to prefer one over the other, other than possibly not having to have the extra import of purrr? Or are these identical?

1 Answers

I'm not certain this is the only difference, but mutate only operates on data.frame, whereas modify_if can operate on lists as well.

library(dplyr)
library(purrr)

list(a=1, b="b") %>% modify_if(is.numeric, `+`, 1)
# $a
# [1] 2
# $b
# [1] "b"

list(a=1, b="b") %>% mutate(across(where(is.numeric), ~ . + 1))
# Error in UseMethod("mutate") : 
#   no applicable method for 'mutate' applied to an object of class "list"

While you have shown that both, given input that inherits data.frame, should produce the same output.

identical(
  iris %>% modify_if(is.numeric, `+`, 1),
  iris %>% mutate(across(where(is.numeric), ~ . + 1))
)
# [1] TRUE

Interestingly, though, modify_if is much faster:

bench::mark(
  purrr = iris %>% modify_if(is.numeric, `+`, 1),
  dplyr = iris %>% mutate(across(where(is.numeric), ~ . + 1))
)
# # A tibble: 2 x 13
#   expression      min   median `itr/sec` mem_alloc `gc/sec` n_itr  n_gc total_time result         memory              time   gc    
#   <bch:expr> <bch:tm> <bch:tm>     <dbl> <bch:byt>    <dbl> <int> <dbl>   <bch:tm> <list>         <list>              <list> <list>
# 1 purrr         220us  254.5us     3816.    4.88KB     2.65  1440     1      377ms <df [150 x 5]> <Rprofmem [4 x 3]>  <benc~ <tibb~
# 2 dplyr        1.58ms   1.91ms      515.   11.34KB     2.50   206     1      400ms <df [150 x 5]> <Rprofmem [19 x 3]> <benc~ <tibb~

bench::mark autoplot

Related