1

I wonder if there is a more efficient way of applying a function over every two consecutive elements of a list. This question got me thinking, and the solution I posted for the user came from here.

I think it is decent enough to use Map/mapply with somelist[-length(somelist)] and somelist[-1] as the arguments to a function(x, y) call, but is there any direct approach, maybe implemented in one of the bigger/newer packages?

Consider this example (stolen from the aforementioned question):

I've got a list of three matrices:

set.seed(1)
matlist <- list(M1 = matrix(sample(1:10, 4, replace = T), nrow = 2, ncol = 2),
                M2 = matrix(sample(1:10, 4, replace = T), nrow = 2, ncol = 2),
                M3 = matrix(sample(1:10, 4, replace = T), nrow = 2, ncol = 2)
                )

and now I want to calculate M1+M2 and M2+M3. The mentioned approach would be

Map(`+`, matlist[-3], matlist[-1])

[[1]]
     [,1] [,2]
[1,]    5   11
[2,]   11   15

[[2]]
     [,1] [,2]
[1,]   12   13
[2,]    5   18

Is there any variant of the apply family to which I would just feed

xapply(matlist, `+`)

I know I could just write that myself as a little helper, like

xapply <- function(x, FUN){
  Map(FUN, x[-length(x)], x[-1])
}

but as far as I understood this classic, sapply/lapply and so on gain their performance advantage over for loops by utilizing C code, therefore the xapply function above would be convenience only, not boosting performance.

LAP
  • 6,605
  • 2
  • 15
  • 28
  • 1
    what is the problem with the function you created. There won't be much performance advantage with a properly written `for` and `sapply/lapply` – akrun Feb 05 '18 at 13:09
  • There's no problem with it per se, I was just wondering if this way of looping over `x` and `x+1` was already implemented in any kind of optimized fashion. – LAP Feb 05 '18 at 13:20
  • Note that `Map` belongs to the `apply` family already, just calling `mapply` – RolandASc Feb 05 '18 at 13:35
  • Yeah, I just used `Map` to avoid the default `SIMPLIFY = TRUE`. – LAP Feb 05 '18 at 13:36

2 Answers2

3

It is not necessarily true that the apply function are faster than looping. Sometimes they are not.

If the idea is not to use indexes but to use the whole object approach then here are some approaches although the Map approach in the question seems better.

1) matrix rollsum will perform a sum over windows. It does not work with lists but if you write conversion functions to and from matrix it would work:

library(magrittr)
library(zoo)

# convert between list and matrix where each row of matrix is one list component
list2mat <- function(x) t(sapply(x, c))
mat2list <- function(x, n) lapply(1:nrow(x), function(i) matrix(x[i, ], n))

nr <- nrow(matlist[[1]])
matlist %>% list2mat %>% rollsum(2) %>% mat2list(nr)

2) Reduce This is an attempt with Reduce:

ans <- list()
invisible(Reduce(function(x, y) { ans <<- c(ans, list(x + y)); y }, matlist))

3) array Another approach is to work with 3d arrays rather than lists. This gives rise to a compact one-liner using aaply and rollsum.

We first convert the list to a 3d array using simplify2array giving array a and now within that framework:

library(plyr)
library(zoo)

a <- simplify2array(matlist) # convert matlist to array

aa <- aaply(a, 1:2, rollsum, 2)

# check
bb <- simplify2array(Map("+", matlist[-1], matlist[-3]))
identical(unname(aa), unname(bb))
## [1] TRUE

aaply is basically an idempotent apply and we could do this with ordinary apply if we were willing to permute (i.e. generalized transpose) the 3d array. That is this is the same as the aaply line above:

library(magrittr)
aa <- a %<% apply(1:2, rollsum, 2) %>% aperm(c(2, 3, 1))

This would also work:

aa <- a %>% aperm(3:1) %>% apply(2:3, rollsum, 2) %>% aperm(3:1)
G. Grothendieck
  • 254,981
  • 17
  • 203
  • 341
0
  1. Here's one slightly different approach, although I don't like it much.

     purr::map(seq_along(matlist)[-length(matlist)], 
           ~ reduce(list(matlist[[.]], matlist[[.+1]]), `+`))
    
  2. Here's a variant with the pipe, which I think is better, just because I like the pipe.

    matlist %>% 
       list(a = .[-length(.)], b = .[-1]) %>%
       .[-1] %>% 
       pmap( ~ .x + .y)
    
  3. Unfortunately like the original Map answer, that gives a list with erroneous names. To get rid of the erroneous names you'd have to do:

    matlist %>% 
        list(a = .[-length(.)], b = .[-1]) %>% 
        .[-1] %>% 
        modify_depth(1, unname)  %>% 
        pmap( ~ .x + .y)
    

I think it's worth it to get rid of the names, since they're misleading.

Tom Greenwood
  • 1,502
  • 14
  • 17