6  Control Structures

Control structures in R allow you to control the flow of execution of a series of R expressions. Basically, control structures allow you to put some ‘logic’ into your R code, rather than just always executing the same R code every time.

6.1 Loops

Sometimes it is necessary to repeat a calculation multiple times, e.g. calculate the sum of each row of a matrix. You can use for loops to do this.

Lets assume a simple matrix with 5 rows and 3 columns. How would we generate the sum of each row?
We could do so, by calculating the sum step-by-step:

#build a dataframe
m <- matrix(1:15, 5)
m
     [,1] [,2] [,3]
[1,]    1    6   11
[2,]    2    7   12
[3,]    3    8   13
[4,]    4    9   14
[5,]    5   10   15
#sum rows without a loop --> sum row 1
sum(m[1, ])
[1] 18
#sum rows without a loop --> sum row 2, ...
sum(m[2, ])
[1] 21

However, you will quickly see that this becomes quite tedious. So lets do this via a loop, in which we:

  • define that we want to loop through rows 1 to 5 (1:5)
  • for (i in 1:5) –> we say that we want to run a for loop, in which we name our variable i
  • {} –> defines the operation we want to loop through
  • in our case we want to sum a specific row (rows 1 through 5)
  • so in i we store the numbers 1,2,3,4 and 5 and then run through the sum functions 5x
#run a loop to get the sum for all five rows
for (i in 1:5) {
print(sum(m[i, ]))
}
[1] 18
[1] 21
[1] 24
[1] 27
[1] 30

How would we store our results in a new dataframe and do not just print them to the screen? To do this, we:

  • define a variable (named results) in which we store an empty vector with c(). We need this empty vector to have something to store our results in while running the loop.
  • start the for loop and define i as before
  • run the sum as before but now store the results for each iteration in results
#store the results, Note that here, the variable results had to be created before as an empty vector.
results <- c()
for (i in 1:5) {
results[i] <- sum(m[i, ])
}

results
[1] 18 21 24 27 30

Very often, there are some R packages or functions that are faster than loops.
The apply() function is such an example. apply() applies a function, i.e. sum, across a matrix.

However, since loops are also a very useful feature in bash or python it is useful to understand their general concept.

#way to do the same using the apply function (faster than loops)
results2 <- apply(m, 1, sum)
results2
[1] 18 21 24 27 30

Apply takes the following arguments:

  1. Our input, the matrix,
  2. The dimension of the matrix on which the function should be applied ((1 means rows and 2 means columns))
    • apply(data, 1, mean) = apply mean on each row
    • apply(data, 2, mean) = apply mean on each column
  3. The function we want to use. Here, the function sum is applied to each row of m.

Actually, there is a family of apply functions, depending on the object you pass as an input, and/or the object you want as output. You can read a brief tutorial under this link.

Two other examples are sapply and lapply, which work on lists.

6.2 if-else

One can also create a decision making structure in R. A decision making structure has at least one condition to be evaluated together with a statement or statements to be evaluated if the condition is TRUE, and optionally, other statements to be executed if the condition is FALSE, as we can see in the figure.

The test_expression has to be logical, i.e., it has to be a expression that, when evaluated, is either TRUE or FALSE. The logical operators listed above can be used to construct them. For example, we can use an if-else statement to check if a number is positive or negative,

#store a random number
x <- -5

#write a loop and ask if our number is positive or negative
if (x > 0) {
print("Positive number")
} else if (x == 0) {
print("Number is zero")
} else {
print("Negative number")
}
[1] "Negative number"

Note that in the example there was an else if. In that way we can check more than 2 conditions.

This of course is a very simplistic example. But if-else statements can be useful if we want to deal with larger dataframes, i.e. our growth data.

Specifically, we want to:

  • Create a new column, with the name category
  • If a value in our Root length column is equal to or smaller than 75 mm (about the median), we want to say this category is small.
  • If that is not the case, we want to say it is large
#apply ifelse
growth_data$category <- ifelse(growth_data$Rootlength <= 75, "small", "large")

#check the first rows of our data
head(growth_data)

So the function works like this:

ifelse(our_test, value_to_return_if_test_is_true, value_to_return_if_test_is_false)

If the root length is missing (NA), the test can’t be answered, and ifelse() returns NA as well.

We can also combine different statements with the & (AND) or | (OR) symbol. I.e. we only send things in the large category if the roots are longer than 75 mm AND the shoot weight is larger than 15 mg

#apply ifelse
growth_data$category <- ifelse(growth_data$Rootlength > 75 & growth_data$FW_shoot_mg > 15, "large", "small")

#check the first rows of our data
head(growth_data)

What happens with the plants where the shoot weight is missing? Let’s look at them:

#show the rows where the shoot weight is missing
growth_data[is.na(growth_data$FW_shoot_mg), ]

Some of them are small and some are NA. Why? For the plants with roots shorter than 75 mm, the first test is already FALSE. With AND, both tests must be TRUE, so the result is FALSE, whatever the shoot weight is. R only returns NA if the answer really depends on the missing value: when the roots are longer than 75 mm, the result depends on the shoot weight, and that is unknown.