2
votes

I have a function that applies specific functions to multiple columns in a data frame. Each of these functions are unique and can only be applied to that column.

convert_columns <- function(df) {
    df %>% mutate(
        a = convert_a(a),
        b = convert_b(b),
        c = convert_c(c),
        d = convert_d(d),
        e = convert_e(e)
        )
}

However, it is possible that users may input a data frame that only have a subset of those columns (for example, only a, b, and c. I would like the function to mutate column a, b, and c if those columns exist in the inputted data frame and ignore columns d and e.

I have tried

convert_columns <- function(df) {
    df %>% mutate(across(any of(),
        a = convert_a(a),
        b = convert_b(b),
        c = convert_c(c),
        d = convert_d(d),
        e = convert_e(e)
        ))
}

and

convert_columns <- function(df) {
    df %>% mutate(across(any of(
        a = convert_a(a),
        b = convert_b(b),
        c = convert_c(c),
        d = convert_d(d),
        e = convert_e(e)
        )))
}

These do not work. Is there a simple way in the tidyverse syntax to accomplish what I am trying to do? In my actual use case, I have ~150 columns I will be mutating.

4
Are you really applying a different function to each column? - csgroen
Yes, they all have to be processed individually in different ways. - Dylan Russell

4 Answers

1
votes

Since functions are unique to each variable and you want to return remaining values if one of the columns fail can't really come up with better solution than to use tryCatch on individual columns.

library(dplyr)

convert_columns <- function(df) {
  df %>% 
    mutate(
    a = tryCatch(convert_a(a),error = function(z) return(NA)),
    b = tryCatch(convert_b(b),error = function(z) return(NA)),
    c = tryCatch(convert_c(c),error = function(z) return(NA)),
    #...
    #...
    )
}

This can be tested using the following mtcars example :

This works -

mtcars %>%
  mutate(a = n_distinct(cyl), 
         b = mean(mpg), 
         c = sd(am))

Now if we remove one of the column, the above fails :

mtcars %>%
  select(-am) %>%
  mutate(a = n_distinct(cyl), 
         b = mean(mpg), 
         c = sd(am))

Error: Problem with mutate() input c. x cannot coerce type 'closure' to vector of type 'double' ℹ Input c is sd(am).

Now using tryCatch

mtcars %>%
  select(-am) %>%
  mutate(a = tryCatch(n_distinct(cyl), error = function(e) return(NA)), 
         b = tryCatch(mean(mpg), error = function(e) return(NA)), 
         c = tryCatch(sd(am), error = function(e) return(NA)))

#   mpg cyl disp  hp drat  wt qsec vs gear carb a  b  c
#1   21   6  160 110  3.9 2.6   16  0    4    4 3 20 NA
#2   21   6  160 110  3.9 2.9   17  0    4    4 3 20 NA
#3   23   4  108  93  3.9 2.3   19  1    4    1 3 20 NA
#4   21   6  258 110  3.1 3.2   19  1    3    1 3 20 NA
#....
1
votes

You can use switch() to get a specific function based on column name. For instance, here, columns a, b, and c are either added, subtracted, or multiplied together, based on column name. We have to use dplyr::cur_column() to get the column name within across (deparse(substitute()) just returns "col").

Thus, with the below method, you can supply just a single function to across() but apply specific function to each column, while getting benefits of any_of()

library(dplyr)

ex <- function(x) {
  arg <- cur_column()
  fn <- switch(arg,
               a = `+`,
               b = `-`,
               c = `*`)
  fn(x, x)
}

df <- data.frame(a = c(1,2),
                 b = c(3,4))

mutate(df, across(any_of(c("a", "b", "c")), ex))
#>   a b
#> 1 2 0
#> 2 4 0
0
votes

Using data.table:

existing_cols <- c("a", "b", "c", "d") %>% intersect(names(df))
setDT(df)
if(length(existing_cols) > 0)
  df[, 
    (existing_cols) := map2(.SD, str_c("convert_", existing_cols), ~do.call(.y, list(.x))), 
    .SDcols = existing_cols
  ]
0
votes

This is straight forward in base R. There must be some way to associate the functions with column names so let us assume we have a named vector of functions or function names, funs. Then loop through the data frame columns looking up the column name in funs applying the corresponding function to each column.

The first argument of convert_coiumns is the data frame, the second argument is the named vector of functions (or function names) and the third argument is a character vector of columns to convert. The last argument defaults to all columns for which there is a function in funs. The default for the last argument could be simplified to names(data) if it is always the case that every column must have a corresponding function.

Internally match.fun takes a function or function name, i.e. character string, and returns the function in each case allowing funs to contain functions, function names or a mix.

convert_columns <- function(data, funs, 
     nms = intersect(names(data), names(funs))) {
  for(nm in nms) data[[nm]] <- match.fun(funs[[nm]])(data[[nm]])
  data
}

# example 1 - uses built in BOD data frame
funs <- c(Time = sqrt, demand = mean)
convert_columns(BOD, funs)

# example 2 - same but use function names rather than functions themselves
funs2 <- c(Time = "sqrt", demand = "mean")
convert_columns(BOD, funs2)

# example 3 - DF does not have column b
funs3 <- c(a = sqrt, b = sum, c = mean)
DF <- data.frame(a = 1:3, c = 3:1)
convert_columns(DF, funs3)

# example 4 - grab functions from global environment - same DF
convert_a <- sum; convert_b <- prod; convert_c <- sqrt
funs4 <- mget(ls(pattern = "^convert_"))
names(funs4) <- sub("convert_", "", names(funs4)) # remove convert_ from names
convert_columns(DF, funs4)

# example 5 - similar to 4
funs5 <- setNames(paste("convert", names(DF), sep = "_"), names(DF))
convert_columns(DF, funs5)