Joe
Joe

Reputation: 913

Assign array values in R using function of indices

In Mathematica, I can make arrays using:

Table[f(x1, x2, ..., xn), {x1, v1}, {x2, v2}, ..., {xn, vn}]

Where f is any function, the xi are variables, and the corresponding vi are the vectors of values the variables take on.

If the vi have lengths Li, then the array is L1xL2x...xLn.

Is this functionality available in R with some built in function, or would I have to write my own program to do this?

Here's a function that I wrote which accomplishes what I want, but is there a built-in function that does this?

# Function definition
xapply <- function(fun,vecs){
 n <- length(vecs)
 dims <- sapply(vecs,length)
 prods <- c(1,cumprod(dims))
 ivecs <- vector(mode="list",length=n)
 for(i in seq(n)){
   ivecs[[i]] <- rep(rep(vecs[[i]],each=prods[i]),prods[n+1]/prods[i+1])
 }
 my.args <- c(fun,ivecs)
 return(array(do.call(mapply,my.args),dims))
}

# Example
v1=c(1,2,3,4)
v2=c(10,20,30,40,50)
v3=c(100,200,300)
v123=list(v1,v2,v3)
my.array1 <- xapply(function(x,y,z) x+y+z,v123)
my.array1

Upvotes: 1

Views: 2891

Answers (4)

IRTFM
IRTFM

Reputation: 263301

I would have used rowSums (which takes hte place of your function) and used it on the result of expand.grid which tos the all-combinations expansions:

> array( rowSums( expand.grid(v123) ) , c(4,5,3) )
, , 1

     [,1] [,2] [,3] [,4] [,5]
[1,]  111  121  131  141  151
[2,]  112  122  132  142  152
[3,]  113  123  133  143  153
[4,]  114  124  134  144  154

, , 2

     [,1] [,2] [,3] [,4] [,5]
[1,]  211  221  231  241  251
[2,]  212  222  232  242  252
[3,]  213  223  233  243  253
[4,]  214  224  234  244  254

, , 3

     [,1] [,2] [,3] [,4] [,5]
[1,]  311  321  331  341  351
[2,]  312  322  332  342  352
[3,]  313  323  333  343  353
[4,]  314  324  334  344  354

You could use a less vectorised solution if the function didn't operate on a row by row basis. The mapply or Reduce functions would let you get the same vector:

> array( Reduce("+", expand.grid(v123) ) , c(4,5,3) )
, , 1

     [,1] [,2] [,3] [,4] [,5]
[1,]  111  121  131  141  151
[2,]  112  122  132  142  152
[3,]  113  123  133  143  153
[4,]  114  124  134  144  154

, , 2

     [,1] [,2] [,3] [,4] [,5]
[1,]  211  221  231  241  251
[2,]  212  222  232  242  252
[3,]  213  223  233  243  253
[4,]  214  224  234  244  254

, , 3

     [,1] [,2] [,3] [,4] [,5]
[1,]  311  321  331  341  351
[2,]  312  322  332  342  352
[3,]  313  323  333  343  353
[4,]  314  324  334  344  354

Upvotes: 1

Joe
Joe

Reputation: 913

Here's another way that I figured out how to make my arrays using expand.grid(), but it's formatted slightly differently from my original question in that: (1) I'm entering the vectors individually, instead of in a list, (2) I'm defining the function using the variables x[1], x[2], etc., and (3) I'm assuming that I know the lengths of the vectors. I still wish that outer() would just take an arbitrary number of arrays as arguments, because that function does exactly what I want, but only for two arrays.

array(apply(expand.grid(1:4,4:8,3:5), 1, function(x) 100*x[1]+10*x[2]+x[3]),c(4,5,3))

Upvotes: 0

bgoldst
bgoldst

Reputation: 35314

Here's how I would do this, demonstrating with your lambda and vector list v123 as input:

array(do.call(function(x,y,z) x+y+z,unname(do.call(expand.grid,v123))),lapply(v123,length));
## , , 1
##
##      [,1] [,2] [,3] [,4] [,5]
## [1,]  111  121  131  141  151
## [2,]  112  122  132  142  152
## [3,]  113  123  133  143  153
## [4,]  114  124  134  144  154
##
## , , 2
##
##      [,1] [,2] [,3] [,4] [,5]
## [1,]  211  221  231  241  251
## [2,]  212  222  232  242  252
## [3,]  213  223  233  243  253
## [4,]  214  224  234  244  254
##
## , , 3
##
##      [,1] [,2] [,3] [,4] [,5]
## [1,]  311  321  331  341  351
## [2,]  312  322  332  342  352
## [3,]  313  323  333  343  353
## [4,]  314  324  334  344  354
##
identical(.Last.value,my.array1);
## [1] TRUE

The advantage of this line over an apply()- or Map()-based solution is that it is vectorized. Only one call to the function is made.

First, expand.grid() is called via do.call() with the vector list as the argument list. This produces a data.frame containing all possible combinations of the input vectors, one per row. We must then remove the automatically-generated column names from the data.frame by calling unname().

We can then pass the resulting unnamed data.frame as the argument list to the lambda, again invoked via do.call(). This works because data.frames are lists internally, with one list component per column, hence a data.frame is suitable for use as the list of arguments expected by do.call(). This also clarifies why the unname() call was needed; otherwise the lambda would be invoked with named arguments (the automatically-generated names being Var1, Var2, Var3) which would fail to bind to the parameters (x, y, z).

The result of the vectorized lambda call is an atomic vector. Since you want an array, we must wrap it in a call to array(), and pass the appropriate dimensionality from the lengths of the input vectors via lapply().


Also you may want to look into the outer() function, which internally performs this kind of cartesian expansion, vectorized call, and dimensionality wrapping, but only for two input objects at a time.

Upvotes: 2

lmo
lmo

Reputation: 38500

If your goal is to apply a function to every combination of v1 ... vn, you could use expand.grid and apply:

x1 <- 1:5
x2 <- 1:10
x3 <- 8:10

# get the average
apply(expand.grid(x1,x2,x7), 1, FUN=mean)
# get the sum of squares
apply(expand.grid(x1,x2,x7), 1, FUN=function(i) sum(i^2))

If you instead have v1, ..., vn of equal length and you want to apply some function to corresponding entries, you can use Map (or mapply)

x1 <- 1:5
x2 <- 2:6
x3 <- 6:10
# get the sum of squares
Map(f=function(x,y,z) sum(x^2 + y^2 + z^2), x1, x2, x3)

Upvotes: 1

Related Questions