Pages (Desktop)

Pages (Mobile)

Loops in R: Nested loops

Programming loops are a way to repeat blocks of code to perform the same task over and over again. R offers the same functionality to perform repetitive tasks, allowing to write less and more efficient code.

This guide follows on from my introduction to loops guide, and shows you have to use nested loops in R. A nested loop is a loop within a loop.

Guide Information

Title Loops in R: Nested loops
Author Benjamin Bell
Published 05 Dec 2022
Last updated
R version 4.2.1
Packages base

This is a 2 part guide:

Part 1: Introduction to loops in R This guide explains how to write loops in R to automate and repeat code, and gives an overview of vectorized functions.
Part 2: Nested loops This guide explains how to write loops within a loop - a nested loop.

What is a nested loop?

A nested loop is a loop within a loop. Inside the main loop, or outer loop, sits another loop, or inner loop, which will run through all of its iterations on each iteration of the outer loop.

For example, in a nested for loop, if the outer loop has 10 iterations for(i in 1:10) and the inner loop has 5 iterations for(j in 1:5), on the first iteration of the outer loop (i = 1), the inner loop will run through each of its iterations to completion (j = 1, j = 2, j = 3 etc.) before returning to the outer loop for the next iteration (i = 2). On the second iteration of the outer loop, the inner loop will again run through its iterations to completion (j = 1, j = 2, j = 3 etc.), and this cycle will continue until the outer loop has completed all of its iterations.

Nested loops are particularly useful when working with more complex data structures such as lists of lists, or multiple objects, or where you want the result of the outer loop to be modified by the inner loop, perhaps based on a condition.

You can nest any number of loops inside the outer loop, and you can use for and while loops. (This guide will focus on for loops).

Are nested loops efficient? Probably not, but sometimes they are necessary to easily achieve your end goal.

An alternative approach is to use apply() and lapply(), since you can also nest these functions inside of each other (like with all R functions). For example, if you had a list of matrices and you wanted to sum each column of the matrix for each list item, you could use something like this: lapply(list, function(x) apply(x, 2, sum)). A future guide will explore the apply() family of functions, but for now, back to nested loops!

Nested for loops

The diagram below helps to illustrate a nested for loop:

START END EXECUTE CODE YES outer for loop NO ALL ITERATIONS COMPLETE? START END EXECUTE CODE YES inner for loop NO ALL ITERATIONS COMPLETE?
© Benjamin Bell. All Rights Reserved. https://www.benjaminbell.co.uk

Combining two vectors

Here's a simple example of a nested loop in action. Here, we will combine each element of two different sized vectors and print them to the terminal.

# Vectors to be printed
# Shades
a <- c("dark", "light")
# Colours
b <- c("red", "blue", "green", "yellow")

# Nested loop
for(i in 1:4) {
    for(j in 1:2) {
        print(paste(a[j], b[i]))
    }
}

This will result in the following output:

[1] "dark red"
[1] "light red"
[1] "dark blue"
[1] "light blue"
[1] "dark green"
[1] "light green"
[1] "dark yellow"
[1] "light yellow"

So how does this work? The outer loop has four iterations to match the size of the colours vector for(i in 1:4), while the inner loop has two iterations to match the size of the shades vector for(j in 1:2). On the first iteration of the outer loop, and on the first iteration of the inner loop, it will print() the first element of the shades vector a[j] = a[1] combined with the first element of the colours vector b[i] = b[1].

While still on the first iteration of the outer loop, but now on the second iteration of the inner loop, it will print() the second element of the shades vector a[j] = a[2] combined with the first element of the colours vector b[i] = b[1].

Now that both iterations of the inner loop are complete, the outer loop will move to the second iteration, and the above will repeat, except now b[i] = b[2].

This might be easier to understand by viewing the equivalent code when written out line by line:

# 1st iteration of outer loop
    # 1st iteration of inner loop
    print(paste(a[1], b[1]))
    # 2nd iteration of inner loop
    print(paste(a[2], b[1]))
# 2nd iteration of outer loop
    # 1st iteration of inner loop
    print(paste(a[1], b[2]))
    # 2nd iteration of inner loop
    print(paste(a[2], b[2]))
# 3rd iteration of outer loop
    # 1st iteration of inner loop
    print(paste(a[1], b[3]))
    # 2nd iteration of inner loop
    print(paste(a[2], b[3]))
# 4th iteration of outer loop
    # 1st iteration of inner loop
    print(paste(a[1], b[4]))
    # 2nd iteration of inner loop
    print(paste(a[2], b[4]))

Why doesn't this work in a single loop? Go ahead and try it!

# Try to run as single loop
for(i in 1:4) {
    print(paste(a[i], b[i]))
}
[1] "dark red"
[1] "light blue"
[1] "NA green"
[1] "NA yellow"

In order to run this as a single loop, the two vectors would need need to be the same size, and they would need to repeat each element the appropriate number of times. You would also need to change the number of iterations:

# To get this to work in a single loop
c <- rep(c("dark", "light"), times=4)
d <- rep(c("red", "blue", "green", "yellow"), each=2)

# Single loop
for(i in 1:8) {
    print(paste(c[i], d[i]))
}

But doing that would be crazy right?!

What happens if you swap the i and j iterators? (nothing good!)

# Swap i and j
for(i in 1:4) {
    for(j in 1:2) {
        print(paste(a[i], b[j]))
    }
}
[1] "dark red"
[1] "dark blue"
[1] "light red"
[1] "light blue"
[1] "NA red"
[1] "NA blue"
[1] "NA red"
[1] "NA blue"

Now the loop is trying to iterate through the shades vector four times, but this vector only has a length of two (hence the NA values in the output). It is also ignoring the third and forth elements of the colours vector, since the j iterator only has a length of two.

Okay, so what if you also swap the sizes of the iterators:

# Swap i and j and swap the iterators
for(i in 1:2) {
    for(j in 1:4) {
        print(paste(a[i], b[j]))
    }
}

You will end up with this output, which is still not correct:

[1] "dark red"
[1] "dark blue"
[1] "dark green"
[1] "dark yellow"
[1] "light red"
[1] "light blue"
[1] "light green"
[1] "light yellow"

The point of these examples, is that if you are writing a nested loop and not getting the output you were expecting, play around with the iterators, it may simply be that you have them the wrong way round, or the wrong length.

Lets consider another example where we will use the results of a calculation, to perform a second calculation. To understand this better, lets run the first part of the calculation as a single loop and view the results.

# Numbers
num1 <- 1:5
# Container
mat <- matrix(0, nrow=5, ncol=5)
# Set value of "k"
k <- 0

# Loop
for(i in 1:5) {
    k <- k + i
    mat[i,] <- k * num1
}

And the output will look like this:

> num1
[1] 1 2 3 4 5
>
> mat
     [,1] [,2] [,3] [,4] [,5]
[1,]    1    2    3    4    5
[2,]    3    6    9   12   15
[3,]    6   12   18   24   30
[4,]   10   20   30   40   50
[5,]   15   30   45   60   75

In this example, we produce a matrix where we multiply the num1 object by k. The value of k changes on each iteration of the loop, and each row of the matrix shows the results.

For the first row results (first loop iteration), k = 1 because the starting value was k <- 0, while the first iterator value was i = 1, thus 0 + 1 = 1. This is then multiplied by the num1 object to give the results.

For the second row results (second loop iteration), k = 3 because the iterator value is i = 2, while the starting value is now 1, since the value was modified by the first iteration of the loop. For the third row results (third loop iteration), the value is now k = 6 and so on.

Okay, so now that we understand how the first part of the loop is working, lets modify the results of each loop iteration using a nested loop.

# Numbers
num1 <- 1:5
# Container
mat <- matrix(0, nrow=5, ncol=5)
# Set value of "k"
k <- 0

# Outer loop
for(i in 1:5) {
    # Change value of k on each iteration (of the outer loop)
    k <- k + i
    mat[i,] <- k * num1
    # Inner loop
    for(j in 1:5) {
       # Multiply each element of the results by corresponding value of num1
       mat[i,][j] <- mat[i,][j] * num1[j]
    }
}

Which will result in the following output:

> mat
     [,1] [,2] [,3] [,4] [,5]
[1,]    1    4    9   16   25
[2,]    3   12   27   48   75
[3,]    6   24   54   96  150
[4,]   10   40   90  160  250
[5,]   15   60  135  240  375

So what is happening here? The outer loop is doing the same as in the previous example, but now, the inner loop takes those results and does a further calculation.

For the first row results, on the first iteration of the inner loop j = 1 and i = 1, so the first element of the results is multiplied by the first element of the num1 object: num1[1] = 1, resulting in 1 1 * 1 * 1 = 1.

While still on the first row of results (first outer loop iteration), but now on the second iteration of the inner loop j = 2 and i = 1. Now, the second element of the results is multiplied by the second element of the num1 object: num1[2] = 2, resulting in 3 2 * 1 * 2 = 4.

Continuing on the first row of results (first outer loop iteration), but now on the third iteration of the inner loop j = 3 and i = 1. Now, the third element of the results is multiplied by the third element of the num1 object: num1[3] = 3, resulting in 9 3 * 1 * 3 = 9.

And this continues for each element of each row of results.

© Benjamin Bell. All Rights Reserved. http://www.benjaminbell.co.uk

Complex data structures (lists of lists)

Nested loops can be particularly useful when dealing with more complex data structures. A list is typically used to group multiple similar objects, for example, a list of matrices. But, lists objects can also contain other lists.

For this example, we'll create some random data to make a list of lists (using a nested loop!).

# Random number containers
rn1 <- vector("numeric", 11)
rn2 <- vector("numeric", 11)

# List container
list <- vector("list", 4)

# Generate random data
for(i in seq_along(list)) {
    # Random numbers (re-run on each iteration of loop)
    rn1 <- round(runif(11, 5, 10), 0)
    rn2 <- round(runif(11, 1, 10), 0)
    # Create list of lists
    list[[i]] <- vector("list", rnorm(1, 5))
    # Populate each list with random data
    for(j in seq_along(list[[i]])) {
        list[[i]][[j]] <- matrix(data=runif((rn1[j] * rn2[j]), 1, 100), ncol=rn1[j], nrow=rn2[j])
    }
}

If all went to plan, you should now have a list of lists containing random data. Let's take a look at the structure to confirm. Your results will vary!

> str(list)
List of 4
 $ sample1:List of 4
  ..$ : num [1:10, 1:8] 31.45 98.38 7.07 66.84 38.24 ...
  ..$ : num [1, 1:8] 69.09 56.47 70.71 8.92 7.31 ...
  ..$ : num [1, 1:9] 42.2 33.62 26.49 55.8 7.93 ...
  ..$ : num [1, 1:5] 72.1 61.8 24.4 67.9 60.7
 $ sample2:List of 5
  ..$ : num [1:7, 1:5] 19.5 52.2 47.3 15.4 37 ...
  ..$ : num [1, 1:7] 63.18 47.61 97.29 7.16 23.69 ...
  ..$ : num [1:6, 1:10] 71.21 63.83 6.64 75.99 29.78 ...
  ..$ : num [1:7, 1:7] 81.9 18 94.7 53.7 90.3 ...
  ..$ : num [1:8, 1:8] 47.9 14 92.2 31 23.6 ...
 $ sample3:List of 6
  ..$ : num [1:4, 1:7] 80.3 83.5 45.7 23.6 84.6 ...
  ..$ : num [1:2, 1:6] 90.13 97.79 24.11 96.54 3.41 ...
  ..$ : num [1:2, 1:7] 72.1 30.5 94.8 80.8 28 ...
  ..$ : num [1:9, 1:7] 40 94.4 34.6 21.4 54.5 ...
  ..$ : num [1:4, 1:10] 92.63 19.77 52.35 8.64 51.46 ...
  ..$ : num [1:5, 1:6] 38.5 70.5 52 14.6 78.2 ...
 $ sample4:List of 3
  ..$ : num [1:8, 1:8] 30.9 45.8 19.3 89.1 20.8 ...
  ..$ : num [1:3, 1:5] 36.9 19.4 95.5 36.7 83.3 ...
  ..$ : num [1:2, 1:7] 31.31 39.73 2.21 85.21 1.09 ...

You can also look at each list using list[[1]], or look at each list item using list[[1]][1] - just replace the numbers with the list or item you want to look at.

Next, we're going to give our main list some names. Lets imagine that each list item is a sample for some scientific analysis that we ran, and each sub-list is a sub-sample, while the matrix contains the analysis data, but each column of data is a replicate of the analysis. I will give these samples some imaginative names (you can be more creative!):

names(list) <- c("sample1", "sample2", "sample3", "sample4")

Okay, now we have our list of lists data structure. For the example exercise, lets also give names to all of the items in the list We'll use following format: "sample1_sub1", and we'll use a nested loop to do it.

# Outer loop
for(i in seq_along(list)) {
    # Inner loop
    # Give names to sub-lists (based on main list)
    for(j in seq_along(list[[i]])) {
        names(list[[i]])[[j]] <- paste0(names(list)[i], "_sub", j)
    }
}

If all went well, your list should now look similar to this (your results will vary!):

> str(list)
List of 4
 $ sample1:List of 4
  ..$ sample1_sub1: num [1:10, 1:8] 31.45 98.38 7.07 66.84 38.24 ...
  ..$ sample1_sub2: num [1, 1:8] 69.09 56.47 70.71 8.92 7.31 ...
  ..$ sample1_sub3: num [1, 1:9] 42.2 33.62 26.49 55.8 7.93 ...
  ..$ sample1_sub4: num [1, 1:5] 72.1 61.8 24.4 67.9 60.7
 $ sample2:List of 5
  ..$ sample2_sub1: num [1:7, 1:5] 19.5 52.2 47.3 15.4 37 ...
  ..$ sample2_sub2: num [1, 1:7] 63.18 47.61 97.29 7.16 23.69 ...
  ..$ sample2_sub3: num [1:6, 1:10] 71.21 63.83 6.64 75.99 29.78 ...
  ..$ sample2_sub4: num [1:7, 1:7] 81.9 18 94.7 53.7 90.3 ...
  ..$ sample2_sub5: num [1:8, 1:8] 47.9 14 92.2 31 23.6 ...
 $ sample3:List of 6
  ..$ sample3_sub1: num [1:4, 1:7] 80.3 83.5 45.7 23.6 84.6 ...
  ..$ sample3_sub2: num [1:2, 1:6] 90.13 97.79 24.11 96.54 3.41 ...
  ..$ sample3_sub3: num [1:2, 1:7] 72.1 30.5 94.8 80.8 28 ...
  ..$ sample3_sub4: num [1:9, 1:7] 40 94.4 34.6 21.4 54.5 ...
  ..$ sample3_sub5: num [1:4, 1:10] 92.63 19.77 52.35 8.64 51.46 ...
  ..$ sample3_sub6: num [1:5, 1:6] 38.5 70.5 52 14.6 78.2 ...
 $ sample4:List of 3
  ..$ sample4_sub1: num [1:8, 1:8] 30.9 45.8 19.3 89.1 20.8 ...
  ..$ sample4_sub2: num [1:3, 1:5] 36.9 19.4 95.5 36.7 83.3 ...
  ..$ sample4_sub3: num [1:2, 1:7] 31.31 39.73 2.21 85.21 1.09 ...

Hopefully, if you have followed the rest of this guide, the code should now be fairly self-explanatory. If not, remember that on each iteration of the outer and inner loop, R is simply replacing the iterator variables with a value. These are then used to generate the name through sub-setting.

On the first iteration of the outer loop and the first iteration of the inner loop: i = 1 and j = 1.

On the first iteration of the outer loop and the second iteration of the inner loop: i = 1 and j = 2.

While on the second iteration of the outer loop and the first iteration of the inner loop: i = 2 and j = 1, and so on.

Simple eh?

© Benjamin Bell. All Rights Reserved. https://www.benjaminbell.co.uk

Nested loops with conditional statements

You might want to add a condition statement to your nested loop. Going back to our colours and shades example (at the start of this guide), lets change the value "light" to "bright" within the nested loop.

# Vectors to be printed
# Shades
a <- c("dark", "light")
# Colours
b <- c("red", "blue", "green", "yellow")

# Nested loop with conditions
for(i in 1:4) {
    for(j in 1:2) {
        # Condition statement
        if(a[j]=="light") {
            # If TRUE, do this
            print(paste("bright", b[i]))
        } else
        # If FALSE, do this
        print(paste(a[j], b[i]))
    }
}

Now the output will look like this:

[1] "dark red"
[1] "bright red"
[1] "dark blue"
[1] "bright blue"
[1] "dark green"
[1] "bright green"
[1] "dark yellow"
[1] "bright yellow"

Obviously, it would have been easier to simply change the shades vector so that this condition statement wasn't necessary e.g. a <- c("dark", "bright"), so lets consider another example.

Lets say you only want to change the shade for the colour blue, you could set two conditions:
# Nested loop with conditions
for(i in 1:4) {
    for(j in 1:2) {
        # Condition statement
        if(a[j]=="light" && b[i]=="blue") {
            # If TRUE, do this
            print(paste("bright", b[i]))
        } else
        # If FALSE, do this
        print(paste(a[j], b[i]))
    }
}

Now the output will look like this:

[1] "dark red"
[1] "light red"
[1] "dark blue"
[1] "bright blue"
[1] "dark green"
[1] "light green"
[1] "dark yellow"
[1] "light yellow"

In both of those examples, the condition statement was inside the inner loop. Lets add a condition to the outer loop instead where we will skip the colour blue in our output:

# Nested loop with conditions
for(i in 1:4) {
    # Condition statement
    if(b[i]=="blue")
        next
    for(j in 1:2) {
        print(paste(a[j], b[i]))
    }
}
[1] "dark red"
[1] "light red"
[1] "dark green"
[1] "light green"
[1] "dark yellow"
[1] "light yellow"

Lets return to our numbers example, and lets add a condition to change the multiplication value (of the inner loop) based on the value of the results of the outer loop.

# Numbers
num1 <- 1:5
num2 <- 10:15
# Container
mat <- matrix(0, nrow=5, ncol=5)
# Set value of "k"
k <- 0

# Outer loop
for(i in 1:5) {
    # Change value of k on each iteration
    k <- k + i
    mat[i,] <- k * num1
    # Inner loop
    for(j in 1:5) {
        # Check whether the result is below 25
        if(mat[i,][j] < 25) {
            # If it is, do this:
            mat[i,][j] <- mat[i,][j] * num1[j]
        } else {
            # If it isn't, do this:
            mat[i,][j] <- mat[i,][j] * num2[j]
        }
    }
}
> mat
     [,1] [,2] [,3] [,4] [,5]
[1,]    1    4    9   16   25
[2,]    3   12   27   48   75
[3,]    6   24   54   96  420
[4,]   10   40  360  520  700
[5,]   15  330  540  780 1050

Similar to other examples, the condition simply checks the value of the result from the outer loop, then performs the appropriate action. Compare these results to the original outer loop results to see which ones changed based on the condition.

Thanks for reading, and please leave any comments or questions below.

This is a 2 part guide:

Part 1: Introduction to loops in R This guide explains how to write loops in R to automate and repeat code, and gives an overview of vectorized functions.
Part 2: Nested loops This guide explains how to write loops within a loop - a nested loop.
© Benjamin Bell. All Rights Reserved. http://www.benjaminbell.co.uk

Further reading

Getting started with R - An introduction to R, everything you need to know to get started with R.

No comments

Post a Comment

Comments are moderated. There may be a delay until your comment appears.