R: Avoid using a for-loop to sequentially select values in one column and apply a function using the vector of values in another column

Viewed 229

The problem

I am trying to revise code that is currently in the form of a nested for loop such that it achieves the same outcome without relying on loops, if possible, to improve readability and facilitate troubleshooting. I will present a really simplified version of what I am actually trying to do. Any advice to help avoid loops would be greatly appreciated.

I have a dataframe (df) with 2 variables (x, y). For a value of x1, I want to subtract every value of y. Then I want to repeat this for x2 through xi.

Here is an image to help illustrate:

enter image description here

What I have tried

A simple reproducible example:

The data:

x <- c(221.7, 285.9, 22.3, 67.2, 133.2)
y <- c(121.5, 193.8, 226.2, 313.6, 17.9)
df <- as.data.frame(cbind(x,y))

The loop:

outcome <- c()
for (i in seq(1,length(df$x),1)){
  eachX <- df[i,1]
  outcome[[i]] <- df$y-eachX
}
 outcome

Where I need help

This method relies on a for-loop and I am under the impression that for-loops can generally be avoided.

Thanks in advance!

3 Answers

I think you need outer:

outer(df$y, df$x, `-`)
#        [,1]   [,2]  [,3]  [,4]   [,5]
# [1,] -100.2 -164.4  99.2  54.3  -11.7
# [2,]  -27.9  -92.1 171.5 126.6   60.6
# [3,]    4.5  -59.7 203.9 159.0   93.0
# [4,]   91.9   27.7 291.3 246.4  180.4
# [5,] -203.8 -268.0  -4.4 -49.3 -115.3

From your code:

do.call(cbind, outcome)
#        [,1]   [,2]  [,3]  [,4]   [,5]
# [1,] -100.2 -164.4  99.2  54.3  -11.7
# [2,]  -27.9  -92.1 171.5 126.6   60.6
# [3,]    4.5  -59.7 203.9 159.0   93.0
# [4,]   91.9   27.7 291.3 246.4  180.4
# [5,] -203.8 -268.0  -4.4 -49.3 -115.3

Note that the function (`-` in this example) is called once, with long vectors that cover all pairs of each vector. For example:

minus <- function(...) { `-`(...); }
outer(df$y, df$x, minus)
#        [,1]   [,2]  [,3]  [,4]   [,5]
# [1,] -100.2 -164.4  99.2  54.3  -11.7
# [2,]  -27.9  -92.1 171.5 126.6   60.6
# [3,]    4.5  -59.7 203.9 159.0   93.0
# [4,]   91.9   27.7 291.3 246.4  180.4
# [5,] -203.8 -268.0  -4.4 -49.3 -115.3
### same results

debug(minus)
outer(df$y, df$x, minus)
# debugging in: FUN(X, Y, ...)
# debug at #1: {
#     -...
# }
list(...) # <--- view what arguments were passed to our 'minus'
# [[1]]
#  [1] 121.5 193.8 226.2 313.6  17.9 121.5 193.8 226.2 313.6  17.9 121.5 193.8 226.2 313.6  17.9
# [16] 121.5 193.8 226.2 313.6  17.9 121.5 193.8 226.2 313.6  17.9
# [[2]]
#  [1] 221.7 221.7 221.7 221.7 221.7 285.9 285.9 285.9 285.9 285.9  22.3  22.3  22.3  22.3  22.3
# [16]  67.2  67.2  67.2  67.2  67.2 133.2 133.2 133.2 133.2 133.2

So if your real-world problem has a more complex function (perhaps preferring working a single pair of data at a time) then you might need to think about how to vectorize it properly, such as with mapply.


Somebody asked about performance.

microbenchmark::microbenchmark(
  r2_vec = outer(y, x, `-`),
  r2_df = outer(df$y, df$x, `-`),
  akrun1_vec = crossing(y, x) %>% transmute(col = y - x),
  akrun2_vec = sapply(y, `-`, x),
  akrun2_df = sapply(df$y, `-`, df$x),
  hammoire_vec = map(x, function(x0){ y - x0 }),
  hammoire_df = map(df$x, function(x){ df$y - x })
)
# Unit: microseconds
#          expr    min      lq     mean  median      uq    max neval
#        r2_vec    5.1    6.45    9.967    8.50   12.45   44.7   100
#         r2_df   16.0   19.30   25.565   21.90   32.40   80.1   100
#    akrun1_vec 3433.6 3702.50 3912.103 3827.45 4041.50 5545.7   100
#    akrun2_vec   19.5   23.05   30.787   26.85   38.30   59.5   100
#     akrun2_df   31.9   36.85   47.008   41.50   56.25  111.3   100
#  hammoire_vec   19.8   25.10   28.527   28.35   31.30   45.4   100
#   hammoire_df   53.6   59.65   65.561   62.95   71.95   90.2   100

I should note that only hammoire's is exactly like your outcome: mine is a matrix; akrun1 is a single column; akrun2 is like mine but one is a tranposition of the other. Only hammoire's is a list of vectors, not sure if that's ultimately what you need.

You could use a functional programming option from the purrr package in the tidyverse. It's a small for loop so there is no particular reason to avoid them, but I think the map/apply functions are a little more intuitive. Having said that it depends what your motivation for avoiding a for loop is.

library(purrr)
outcome1 <- map(df$x, function(x){
  df$y - x
  })

outcome1

identical(outcome, outcome1)

Just as an FYI you could also make the for loop a little more concise.

outcome2 <-  c()
for (i in seq_along(df$x)){
  x <- df$x[[i]]
  outcome2[[i]] <- df$y-x
}

outcome2

identical(outcome, outcome2)

An option with crossing

library(tidyr)
library(dplyr)
crossing(y, x) %>% 
      transmute(col = y - x)

Or with base R using sapply

sapply(y, `-`, x)

by @Gwang-JinKim:

require(tidyr)
require(dplyr)
require(microbenchmark)

x <- c(221.7, 285.9, 22.3, 67.2, 133.2)
y <- c(121.5, 193.8, 226.2, 313.6, 17.9)

microbenchmark(sapply = sapply(x, `-`, y),
               dplyr  = crossing(x, y) %>% transmute(col=x - y))

Unit: microseconds
   expr      min        lq        mean     median        uq       max neval cld
 sapply    50.59    55.652    94.60729   100.3705   109.212   376.754   100  a 
  dplyr 12009.29 13008.126 14580.49195 13526.5130 14922.512 51349.338   100   b

The sapply-method is by 2~3 magnitudes of order faster.

Related