21  Iteration

Iteration is about tools repeatedly performing the same action on different objects. Iteration in R generally tends to look rather different from other programming languages because so much of it is implicit and we get it for free. For example, if you want to double a numeric vector x in R, you can just write 2 * x. In most other languages, you’d need to explicitly double each element of x using some sort of for loop.

21.1 Loops

R is set up so that such programming tasks can be accomplished using vector operations rather than looping. Using vector operations is more efficient computationally, as well as more concise literally.

Example:

# Find the sum of the first n squares using a loop
n <- 100
S <- 0
for (i in 1:n) {
  S <- S + i^2
}
S
[1] 338350
# Alternatively, use vector operations
sum((1:n)^2)
[1] 338350

21.1.1 for Loops

The for command has the following form, where x is a simple variable and vector is a vector.

for (x in vector) {
  expression_1
  ...
}

Example:

x <- c("a", "b", "c", "d")
for(i in 1:4) / for(i in seq_along(x)) / for(letter in x) { ## set an iterator variable and assign it successive values over the elements of an object (list, vector, etc.) 
  print(x[i])
  print(letter)
}

When executed, the for command executes the group of expressions within the braces { } once for each element of vector. The grouped expressions can use x, which takes on each of the values of the elements of vector as the loop is repeated.

21.1.2 nested for loops

Example:

x <- matrix(1:6, 2, 3) 
for(i in seq_len(nrow(x))) { 
  for(j in seq_len(ncol(x))) { 
    print(x[i,j]) 
  }
}

21.1.3 while Loops

Often we do not know beforehand how many times we need to go around a loop. That is, each time we go around the loop, we check some condition to see if we are done yet. In this situation we use a while loop, which has the form:

while (logical_expression) {
  expression_1
  ...
}

When a while command is executed, logical_expression is evaluated first. If it is TRUE then the group of expressions in braces { } is executed. Control is then passed back to the start of the command: if logical_expression is still TRUE then the grouped expressions are executed again, and so on. Clearly, for the loop to stop eventually, logical_expression must eventually be FALSE. To achieve this logical_expression usually depends on a variable that is altered within the grouped expressions.

Example:

time <- 0
debt <- debt_initial
while (debt > 0) {
  time <- time + period
  debt <- debt*(1 + r*period) - repayments
}

21.1.4 repeat Loops

repeat initiates an infinite loop right from the start. The only way to exit a repeat loop is to call break:

repeat { 
  if() { 
    break
  }
}

21.1.5 next, break

next is used to skip an iteration of a loop:

for(i in 1:100) { 
  if(i <= 20) { 
    next ## Skip the first 20 iterations 
  } 
  ## Do something here
}

break is used to exit a loop immediately, regardless of what iteration the loop may be on.

for(i in 1:100) { 
  print(i) 
  if(i > 20) { 
    break ## Stop loop after 20 iterations 
  }
}

21.2 Loop Functions

Many R functions are vectorised, meaning that given vector input the function acts on each element separately, and a vector output is returned. This is a very powerful aspect of R that allows for compact, efficient, and readable code. Moreover, for many R functions, applying the function to a vector is much faster than if we were to write a loop to apply it to each element one at a time.

Besides, writing for and while loops is useful when programming but not particularly easy when working interactively on the command line.

R has some functions which implement looping in a compact form to make your life easier:

lapply() ## Apply the function FUN to every element of vector X
sapply() ## Same as lapply but try to simplify the result 
apply() ## Apply a function that takes a vector argument to each of the rows (or columns) of a matrix, 
tapply() ## Apply a function over subsets of a vector
mapply() ## Multivariate version of lapply (to vectorise over more than one argument)

X can be a list or an atomic vector, which is a vector that comprises atomic objects (logical, integer, numeric, complex, character and raw). That is, sapply(X, FUN) returns a vector whose i -th element is the value of the expression FUN(X[i]). Note that R performs a loop over the elements of X, so execution of this code is not faster than execution of an equivalent loop.

If FUN has arguments other than X[i], then they can be included using the dots protocol as shown above. That is, sapply(X, FUN, ...) returns FUN(X[i], ...) as the i -th element.

21.2.1 lapply

The lapply() function does the following simple series of operations:

  1. It loops over a list, iterating over each element in that list
  2. It applies a function to each element of the list (a function that you specify)
  3. Returns a list (the “l” is for “list”).

lapply() always returns a list, regardless of the class of the input.

str(lapply)
function (X, FUN, ...)  

Note that the function() definition is right in the call to lapply():

lapply(x, function(elt) { elt[,1] }

Example:

x <- list(a = 1:5, b = rnorm(10)) 
lapply(x, mean)
$a
[1] 3

$b
[1] 0.1657981

Note that when you pass a function to another function, you do not need to include the open and closed parentheses () like you do when you are calling a function.

Once the call to lapply() is finished, the function disappears and does not appear in the workspace.

21.2.2 sapply

The sapply() function behaves similarly to lapply(), the only real difference being that the return value is a vector or a matrix.

str(sapply)
function (X, FUN, ..., simplify = TRUE, USE.NAMES = TRUE)  

sapply() will try to simplify the result of lapply() if possible.

Example:

s <- split(airquality, airquality$Month)
mode(s)
[1] "list"
(q <- sapply(s, function(x) { 
              colMeans(x[, c("Ozone", "Solar.R", "Wind")])
          }
))
               5         6          7        8        9
Ozone         NA        NA         NA       NA       NA
Solar.R       NA 190.16667 216.483871       NA 167.4333
Wind    11.62258  10.26667   8.941935 8.793548  10.1800
mode(q)
[1] "numeric"

21.2.3 apply

The apply() function can take a list, matrix, or array. It is most often used to apply a function to the rows or columns of a matrix (which is just a 2-dimensional array).

str(apply)
function (X, MARGIN, FUN, ..., simplify = TRUE)  

Using apply() is not really faster than writing a loop, but it works in one line and is highly compact.

x <- matrix(rnorm(200), 20, 10)
# Take the mean of each column
(k <- apply(x, 2, mean))
 [1]  0.14727811  0.43325584 -0.26956194 -0.07370726 -0.07655632 -0.08571866
 [7]  0.29305626  0.18876939  0.51761735 -0.02386109
mode(k)
[1] "numeric"
## Take the mean of each row
apply(x, 1, sum) 
 [1] -1.5097227  2.9241030 -3.6808014  6.9609231 -2.5324817 -3.5862790
 [7]  4.1464080  2.6878499  0.2111298  0.7161461  0.7516207  8.1343997
[13] -3.3015266  3.1270903 -1.9789594  1.2783737  4.0279224 -1.2387867
[19]  2.8347659  1.0392588

For the special case of column/row sums and column/row means of matrices, there are some useful shortcuts:

rowSums = apply(x, 1, sum) 
rowMeans = apply(x, 1, mean)
colSums = apply(x, 2, sum) 
colMeans = apply(x, 2, mean)

21.2.4 tapply

tapply() is used to apply a function over subsets of a vector. It can be thought of as a combination of split() and sapply() for vectors only.

str(tapply)
function (X, INDEX, FUN = NULL, ..., default = NA, simplify = TRUE)  

Example:

(x <- c(rnorm(10), runif(10), rnorm(10, 1)))
 [1] -1.28447476 -0.87516348  0.38151289 -1.05675253 -0.49208629 -0.62921217
 [7] -0.79653296  1.02096588  0.70847441 -0.37224811  0.64748401  0.37824349
[13]  0.83174195  0.33945354  0.90395266  0.20921515  0.92049471  0.82909468
[19]  0.95179298  0.38200533  1.70245776  1.83811324  1.49084291 -0.57756468
[25]  0.81538190 -0.48060434  1.04121825  0.07955710  1.93430660 -0.08805508
# Define some groups with a factor variable
(f <- gl(3, 10))
 [1] 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3
Levels: 1 2 3
tapply(x, f, mean)
         1          2          3 
-0.3395517  0.6393478  0.7755654 
# We can reduce the noise as follows:
round(tapply(x, f, mean), digits=1)
   1    2    3 
-0.3  0.6  0.8 

21.2.5 mapply

The mapply() function is a multivariate apply of sorts which applies a function in parallel over a set of arguments. Recall that lapply() and friends only iterate over a single R object. What if you want to iterate over multiple R objects in parallel? This is what mapply() is for.

str(mapply) 
function (FUN, ..., MoreArgs = NULL, SIMPLIFY = TRUE, USE.NAMES = TRUE)  

The mapply() function has a different argument order from lapply() because the function to apply comes first rather than the object to iterate over. The R objects over which we apply the function are given in the ... argument because we can apply over an arbitrary number of R objects.

Example 1:

mapply(rep, 1:4, 4:1)
[[1]]
[1] 1 1 1 1

[[2]]
[1] 2 2 2

[[3]]
[1] 3 3

[[4]]
[1] 4

Example 2:

noise <- function(n, mean, sd) { 
            rnorm(n, mean, sd)
          }
mapply(noise, 1:5, 1:5, 2)
[[1]]
[1] -1.004983

[[2]]
[1] -0.2012958  5.8551029

[[3]]
[1] 4.475691 5.301174 4.193333

[[4]]
[1] 6.561300 8.247429 1.022848 6.973876

[[5]]
[1]  3.2210572  2.3405193 -0.3194301  7.4476711  3.8813917

The mapply() function can be used to automatically “vectorize” a function. What this means is that it can be used to take a function that typically only takes single arguments and create a new function that can take vector arguments. This is often needed when you want to plot functions.

Example:

## Generate some data
x <- rnorm(100)
## This is not what we want
sumsq(1:10, 1:10, x)  
## Note that the call to sumsq() only produced one value instead of 10 values.
mapply(sumsq, 1:10, 1:10, MoreArgs = list(x = x))

There’s even a function in R called vectorize() that automatically can create a vectorized version of your function. So, we could create a vsumsq() function that is fully vectorized as follows.

Example:

vsumsq <- vectorize(sumsq, c("mu", "sigma")) 
vsumsq(1:10, 1:10, x)

21.2.6 split

The split() function takes a vector or other objects and splits it into groups determined by a factor or list of factors.

The form of the split() function is this:

str(split) 
function (x, f, drop = FALSE, ...)  

The combination of split() and a function like lapply() or sapply() is a common paradigm in R.

Example 1:

x <- c(rnorm(10), runif(10), rnorm(10, 1))
f <- gl(3, 10) 
# split() returns a list
(j <- split(x, f))
$`1`
 [1] -0.2351062  1.0849005 -0.2073284 -0.6839409  0.5267443  0.5026479
 [7] -0.1038182 -0.4041406 -0.4897551 -1.1820040

$`2`
 [1] 0.9764283 0.9572011 0.4070798 0.6731056 0.9316433 0.7500879 0.5913454
 [8] 0.8603848 0.5486396 0.1240526

$`3`
 [1] 0.8848965 0.7053862 1.6342864 0.9173015 0.4236826 0.5112164 1.1382702
 [8] 1.0402037 0.9944404 0.3444231
mode(j)
[1] "list"

Example 2:

# Splitting a Dataframe
s <- split(airquality, airquality$Month)

21.3 Functional Programming Loops

Functional programming loops are built around functions that take other functions as inputs. In this chapter we’ll focus on three common tasks: modifying multiple columns, reading multiple files, and saving multiple objects. The tools that we need are provided by dplyr and purrr, both core members of the tidyverse.

library(tidyverse)

21.3.1 Modifying multiple columns

Let’s start creating a simple tibble and counting the number of observations and compute the median of every column.

Example:

df <- tibble(
  a = rnorm(10),
  b = rnorm(10),
  c = rnorm(10),
  d = rnorm(10)
)

You could do it with copy-and-paste:

df |> summarize(
  n = n(),
  a = median(a),
  b = median(b),
  c = median(c),
  d = median(d),
)
# A tibble: 1 × 5
      n     a      b       c      d
  <int> <dbl>  <dbl>   <dbl>  <dbl>
1    10 0.805 -0.796 -0.0315 0.0922

But you can imagine that this will get very tedious if you have tens or even hundreds of columns. Instead, you can use across():

Example:

df |> summarize(
  n = n(),
  across(a:d, median),
)
# A tibble: 1 × 5
      n     a      b       c      d
  <int> <dbl>  <dbl>   <dbl>  <dbl>
1    10 0.805 -0.796 -0.0315 0.0922

There are two additional selection techniques that are particularly useful for across():

  • everything(): selects every (non-grouping) column:
df |> 
  group_by(grp) |> 
  summarize(across(everything(), median))
  • where(): allows you to select columns based on their type:

    -   `where(is.numeric)` selects all numeric columns.
    -   `where(is.character)` selects all string columns.
    -   `where(is.Date)` selects all date columns.
    -   `where(is.POSIXct)` selects all date-time columns.
    -   `where(is.logical)` selects all logical columns.

    Just like other selectors, you can combine these with Boolean algebra. For example, !where(is.numeric) selects all non-numeric columns.