1

I have a map of functions I want to apply to their respective columns.
Is there something liked a mapped mutate_at?

my_map <- 
  data_frame(col = names(iris)[-5],
             calc = rep(c("floor", "ceiling"), 2))
my_map 
# A tibble: 4 x 2
col          calc   
<chr>        <chr>  
Sepal.Length floor  
Sepal.Width  ceiling
Petal.Length floor  
Petal.Width  ceiling

Failed attempt:

tbl_df(iris) %>% mutate_at(vars(col_calcs$col), funs_(col_calcs$calc))

Sepal.Length Sepal.Width Petal.Length Petal.Width Species Sepal.Length_floor Sepal.Width_floor Petal.Length_floor Petal.Width_floor Sepal.Length_ceiling
      <dbl>       <dbl>        <dbl>       <dbl> <fct>                <dbl>             <dbl>              <dbl>             <dbl>                <dbl>
       5.1         3.5          1.4         0.2 setosa                   5                 3                  1                 0                    6
       4.9         3            1.4         0.2 setosa                   4                 3                  1                 0                    5

Desired output:

Sepal.Length Sepal.Width Petal.Length Petal.Width Species
        <dbl>       <dbl>        <dbl>       <dbl> <fct>  
         5.0         4.0          1.0         1.0 setosa 
         4.0         3.0          1.0         1.0 setosa

Last thing, my_map$calc may have unknown functions that may be applied.
Ex) Someone can change the last "floor" to "round".

pogibas
  • 27,303
  • 19
  • 84
  • 117
  • It seems like you changed your question based on the best answer you think, but it is a little bit misleading. If this is the original title, others may not submit their answers in the first place. – www Jul 17 '18 at 13:52
  • 1
    good point. I added back the mutate_at() in the title question. Thanks – user3550273 Jul 17 '18 at 15:30

5 Answers5

6

I don't think there's a straight forward way to do this with dplyr::mutate_* function; One work around is to use the reduce (or reduce2) function and mutate column with the corresponding transform function one by one:

library(tidyverse)

reduce2(.x = my_map$col, 
        .y = my_map$calc, 
        .f = function(df, col, f) mutate_at(df, vars(col), f), 
        .init = iris) %>% head(2)

#   Sepal.Length Sepal.Width Petal.Length Petal.Width Species
# 1            5           4            1           1  setosa
# 2            4           3            1           1  setosa
Psidom
  • 209,562
  • 33
  • 339
  • 356
1

Here is a way to use map2 to replace each column.

library(tidyverse)

iris2 <- iris

iris2[, -5] <- map2(my_map$calc, my_map$col, function(x, y){
  x2 <- eval(parse(text = x))
  y2 <- iris2[[y]]
  result <- x2(y2)
  return(result)
})

head(iris2)
#   Sepal.Length Sepal.Width Petal.Length Petal.Width Species
# 1            5           4            1           1  setosa
# 2            4           3            1           1  setosa
# 3            4           4            1           1  setosa
# 4            4           4            1           1  setosa
# 5            5           4            1           1  setosa
# 6            5           4            1           1  setosa
www
  • 38,575
  • 12
  • 48
  • 84
  • We can simplify this a little, first you can use `get(x)` instead of `eval(parse(text=x))`, then we can notice that we can use `mget`, which is a vectorized version of `get` with different defaults, and we can use it directly in the `map2` call, finally we can use the short formula notation to be a little more compact, and we obtain: `iris2[-5] <- map2(mget(my_map$calc,inherits =TRUE), my_map$col, ~ .x(iris[[.y]]))`. This can easily transform into a neat base solution : `iris2[-5] <- Map(function(x, y) x(iris[[y]]), mget(my_map$calc,inherits =TRUE), my_map$col)` – moodymudskipper Jul 17 '18 at 10:07
  • @Moody_Mudskipper Thanks for sharing your nice solution. – www Jul 17 '18 at 13:44
1

We could start from my_map :

library(tidyverse)
map2(my_map$col,my_map$calc,~transmute_at(iris,.x,.y)) %>%
  bind_cols(iris[!names(iris) %in% my_map$col]) %>% # or less general: iris[-5]  
  head

#   Sepal.Length Sepal.Width Petal.Length Petal.Width Species
# 1            5           4            1           1  setosa
# 2            4           3            1           1  setosa
# 3            4           4            1           1  setosa
# 4            4           4            1           1  setosa
# 5            5           4            1           1  setosa
# 6            5           4            1           1  setosa
moodymudskipper
  • 46,417
  • 11
  • 121
  • 167
0

If we assume that all variables, which you want to take the floor function, contain the same character, i.e. Length, and all variables, which you want to take the ceiling function, contain the same character, i.e. Width, then we can apply the following code:

library(tidyverse)
iris %>% 
  mutate_at(vars(ends_with("Length")), funs(floor)) %>% 
  mutate_at(vars(ends_with("Width")), funs(ceiling))

#   Sepal.Length Sepal.Width Petal.Length Petal.Width Species
# 1            5           4            1           1  setosa
# 2            4           3            1           1  setosa
HNSKD
  • 1,614
  • 2
  • 14
  • 25
0

Although verbose, I find the following very readable and a simple implementation of map:

iris2 <- iris %>% 
    mutate(id = 1:n()) %>%
    gather(key = col, value, my_map$col ) %>%
    full_join(my_map, by = "col") %>%
    mutate(value = invoke_map(.f = calc, .x = value)) %>%
    unnest() %>%
    select(-calc) %>
    spread(col, value) %>%
    select(-id)

head(iris2)
#    Species Petal.Length Petal.Width Sepal.Length Sepal.Width
# 1  setosa            1           1            5           4
# 2  setosa            1           1            4           3
# 3  setosa            1           1            4           4
# 4  setosa            1           1            4           4
# 5  setosa            1           1            5           4
AndS.
  • 7,748
  • 2
  • 12
  • 17