Apply a function over all combinations of arguments
Asked Answered
R

2

18

I would like to be able to apply a function to all combinations of a set of input arguments. I have a working solution (below) but would be surprised if there's not a better / more generic way to do this using, e.g. plyr, but so far have not found anything. Is there a better solution?

# Apply function FUN to all combinations of arguments and append results to
# data frame of arguments
cmapply <- function(FUN, ..., MoreArgs = NULL, SIMPLIFY = TRUE, 
    USE.NAMES = TRUE)
{
    l <- expand.grid(..., stringsAsFactors=FALSE)
    r <- do.call(mapply, c(
        list(FUN=FUN, MoreArgs = MoreArgs, SIMPLIFY = SIMPLIFY, USE.NAMES = USE.NAMES), 
        l
    ))
    if (is.matrix(r)) r <- t(r) 
    cbind(l, r)
}

examples:

# calculate sum of combinations of 1:3, 1:3 and 1:2
cmapply(arg1=1:3, arg2=1:3, 1:2, FUN=sum)

# paste input arguments
cmapply(arg1=1:3, arg2=c("a", "b"), c("x", "y", "z"), FUN=paste)

# function returns a vector
cmapply(a=1:3, b=2, FUN=function(a, b) c("x"=b-a, "y"=a+b))
Rain answered 18/9, 2014 at 11:27 Comment(9)
What do you mean by "better"? What you have seems to be excellent.Prevaricator
I just hoped there might already be an existing function somewhere out thereRain
Not that I know of. You can pass a functions to combn or outer, but that's not quite what you want.Prevaricator
Do not hard code the optional arguments to mapply (for instance SIMPLIFY=SIMPLIFY etc). Furthermore, I don't think there is any need to do.call. You can directly call mapply.Bandsman
@Bandsman Not sure what you mean there: he's only hardcoding the inner assignments. the value of SIMPLIFY is set by the user when calling cmapply .Nigger
@CarlWitthoft See the edit history. He had SIMPLIFY=TRUE in his previous version, so calling cmapply with SIMPLIFY=FALSE wouldn't have any effect.Bandsman
@nicloa/@Carl - between these comments I made an edit which may explain the confusion. Not sure how to call mapply directly however.Rain
You are right, you need do.call.Bandsman
Ah., got it. fooled me againNigger
C
1

This function isn't necessarily any better, just slightly different:

rcapply <- function(FUN, ...) {

  ## Cross-join all vectors
  DT <- CJ(...)

  ## Get the original names
  nl <- names(list(...))

  ## Make names, if all are missing
  if(length(nl)==0L) nl <- make.names(1:length(list(...)))

  ## Fill in any missing names
  nl[!nzchar(nl)] <- paste0("arg", 1:length(nl))[!nzchar(nl)]
  setnames(DT, nl)

  ## Call the function using all columns of every row
  DT2 <- DT[,
            as.data.table(as.list(do.call(FUN, .SD))), ## Use all columns...
            by=.(rn=1:nrow(DT))][ ## ...by every row
              , rn:=NULL] ## Remove the temp row number

  ## Add res to names of unnamed result columns
  setnames(DT2, gsub("(V)([0-9]+)", "res\\2", names(DT2)))

  return(data.table(DT, DT2))
}

head(rcapply(arg1=1:3, arg2=1:3, 1:2, FUN=sum))
##    arg1 arg2 arg3 res1
## 1:    1    1    1    3
## 2:    1    1    2    4
## 3:    1    2    1    4
## 4:    1    2    2    5
## 5:    1    3    1    5
## 6:    1    3    2    6

head(rcapply(arg1=1:3, arg2=c("a", "b"), c("x", "y", "z"), FUN=paste))
##    arg1 arg2 arg3  res1
## 1:    1    a    x 1 a x
## 2:    1    a    y 1 a y
## 3:    1    a    z 1 a z
## 4:    1    b    x 1 b x
## 5:    1    b    y 1 b y
## 6:    1    b    z 1 b z

head(rcapply(a=1:3, b=2, FUN=function(a, b) c("x"=b-a, "y"=a+b)))
##    a b  x y
## 1: 1 2  1 3
## 2: 2 2  0 4
## 3: 3 2 -1 5
Chirr answered 15/8, 2016 at 7:36 Comment(0)
P
0

A slight simplification of your original code:

cmapply <- function(FUN, ..., MoreArgs = NULL)
{
    l <- expand.grid(..., stringsAsFactors=FALSE)
    r <- .mapply(FUN=FUN, dots=l, MoreArgs = MoreArgs)
    r <- simplify2array(r, higher = FALSE)
    
    if (is.matrix(r)) r <- t(r)
    return(cbind(l, r))
}

This does not require a do.call.

It does miss the SIMPLIFY and USE.NAMES arguments, but the way you are using it seems to make the arguments not usable anyway: if SIMPLIFY = FALSE, the rbind() will fail, and USE.NAMES = TRUE does not do anything because the names get lost after the rbind() anyway.

Pricecutting answered 20/4, 2022 at 18:5 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.