Preamble

In the previous lab, we created functions that package operations so that we can easily use them again. Today, we’re going to look at another way of avoiding repetitive code.

Iteration is a term that means repeatedly performing an operation. In this lab, we will primarily use for loops, which allow us to repeat some code for a collection of values that we already know.

Suppose we have the following vector:

values = c(2, 5, 8, 11)

If we wanted to square every value, we could write:

values[1]^2
## [1] 4
values[2]^2
## [1] 25
values[3]^2
## [1] 64
values[4]^2
## [1] 121

There is a lot of repetition here. Instead, we can tell R to perform the same operation for every value in values:

for (x in values){
  print(x^2)
}
## [1] 4
## [1] 25
## [1] 64
## [1] 121

Let’s think through what R is doing. It starts by taking the first value from values and temporarily assigning it to x. Since x = 2, R calculates 2^2. It then moves to the next value, so x = 5, and calculates 5^2. This continues until R has gone through every value in values.

The name x itself is not important. We could instead write:

for (number in values){
  print(number^2)
}
## [1] 4
## [1] 25
## [1] 64
## [1] 121

and get exactly the same result.

One important thing to keep in mind is what our looping variable contains during each repetition. Squaring each individual value makes sense because squaring is an operation we can perform on one number at a time. Consider what happens if we instead use max():

for (x in values){
  print(max(x))
}
## [1] 2
## [1] 5
## [1] 8
## [1] 11

This just prints each value again. During each repetition, x contains only one number, and the maximum of a single number is just that number.

Compare this to:

max(values)
## [1] 11

which gives max() the entire vector at once.

As we work through the lab, we’ll see several different ways of choosing what we loop over, including individual values, numbered positions, and variables in a data set. Choosing what to loop over is often just as important as deciding what operation to perform.

Goals

Throughout this lab, we will explore:

  • How a for loop repeatedly performs an operation and what changes during each repetition.
  • Different things we can loop over, including values, numbered positions, and variables in a data set.
  • How to store the results of a loop rather than simply printing them.
  • How functions and logical conditions can be used as part of iteration.
  • Why we often use R’s existing vector operations or other tools instead of manually writing every loop.
  • A few other ways R can handle iteration, including apply(), lapply(), and while loops.


Lab

Basic For Loops

For reference, the basic structure of a for loop is:

for (x in values){
  # Code to repeat
}

We will refer to the pieces of the loop as:

  • Values to loop through: values (I may sometimes call this the loop’s index, similar to the index underneath a summation symbol in mathematics)
  • Looping variable: x
  • Body: everything inside the { }

R takes the first value from values, stores it in the looping variable x, and runs the body. It then replaces x with the next value and runs the body again, continuing until it reaches the end of values.

For example:

temperatures = c(40, 55, 70)

for (temp in temperatures){
  print(temp + 10)
}
## [1] 50
## [1] 65
## [1] 80

Here, temperatures contains the values we are looping through, temp is our looping variable, and print(temp + 10) is the body. The loop therefore prints 50, 65, and 80.

Question #1

Before running the following loop, write down what you expect it to print. Then run the loop and verify your answer.

for (x in c(3, 7, 10)){
  print(2*x + 1)
}

Question #2

Create a vector containing the values 4, 9, 16, 25. Write a for loop that prints the square root of each value, then subtracts 1.


Looping Over Positions and Storing Results

Sometimes we care about the position of each value rather than just the value itself.

Consider:

values = c(4, 7, 2, 9)

for (i in 1:length(values)){
  print(values[i])
}
## [1] 4
## [1] 7
## [1] 2
## [1] 9

Instead of taking the values 4, 7, 2, and 9, our looping variable i takes the values 1, 2, 3, and 4. We can then use values[i] to access the value in the corresponding position.

R also has a function specifically for generating the positions of an object:

seq_along(values)
## [1] 1 2 3 4

So we could write the same loop as:

for (i in seq_along(values)){
  print(values[i])
}
## [1] 4
## [1] 7
## [1] 2
## [1] 9

Looping over positions becomes especially useful when we want to store the results of a loop in a new vector:

values = c(4, 7, 2, 9)
output = numeric(length(values))

for (i in seq_along(values)){
  output[i] = values[i]^2
}

output
## [1] 16 49  4 81

We first create a numeric vector with enough positions to hold our results. During each repetition, i tells us which input value to use and which position of output to fill. Using length(values) means our code will still create the appropriate amount of storage if the length of values changes.

Question #3

A: Without running the following code for loop, investigate what object the output = numeric(length(x)) line is creating, then determine what will be stored in output when the loop finishes. Then run the code and check your answer.

x = c(3, 6, 9)
output = numeric(length(x))

for (i in seq_along(x)){
  output[i] = x[i] + 2
}

output
## [1]  5  8 11

B: Use the following vector:

temperatures_f = c(32, 50, 68, 77, 95)

Create a numeric vector of the appropriate length to store the results. Then use a for loop to convert each temperature to Celsius and store the result in the corresponding position.

Recall:

\[ C = (F-32)\frac{5}{9} \]

Print your completed vector when you are finished.


Logical Conditions Inside Loops

We can also decide whether something should happen during each repetition of a loop. An if statement runs its code only when a logical condition is TRUE.

For example:

values = c(2, 5, 8, 11, 14)

for (x in values){
  if (x > 5){
    print(x)
  }
}
## [1] 8
## [1] 11
## [1] 14

During each repetition, R asks whether the current value of x is greater than 5. The print() statement only runs when this is TRUE.

We can use the same logical operators that we have already been working with:

for (x in values){
  if (x > 5 & x < 12){
    print(x)
  }
}
## [1] 8
## [1] 11

Here, both conditions must be TRUE. We could instead use |:

for (x in values){
  if (x < 5 | x > 12){
    print(x)
  }
}
## [1] 2
## [1] 14

We can also use %in%:

animals = c("cat", "bird", "dog", "fish", "rabbit")
pets = c("cat", "dog", "rabbit")

for (animal in animals){
  if (animal %in% pets){
    print(animal)
  }
}
## [1] "cat"
## [1] "dog"
## [1] "rabbit"

This is the same logical reasoning we used when subsetting data. The main difference is that the condition is being checked separately during each repetition of the loop.

Question #4

Consider:

scores = c(62, 88, 91, 73, 55, 84, 97)

Write a for loop that prints only scores that are at least 70 and less than 90.

Before running your loop, determine which values you expect to be printed.

Question #5

Consider the following vector of temperatures in degrees Fahrenheit:

temperatures_f = c(45, 62, 71, 83, 96, 58, 89)

In the previous lab, you created a function that converts a temperature from Fahrenheit to Celsius.

Write a for loop that goes through temperatures_f. If a temperature is below 60 or above 85, use your conversion function to convert the temperature and print the result.

Before running your loop, determine which temperatures you expect to meet the condition.


Looping Through Data

The things that we loop over do not have to be individual observations in a vector. We can also loop over parts of a data set.

We will use the built-in airquality data set:

air_data = airquality
head(air_data)
##   Ozone Solar.R Wind Temp Month Day
## 1    41     190  7.4   67     5   1
## 2    36     118  8.0   72     5   2
## 3    12     149 12.6   74     5   3
## 4    18     313 11.5   62     5   4
## 5    NA      NA 14.3   56     5   5
## 6    28      NA 14.9   66     5   6

Suppose we want the mean of the first three variables. We could refer to the columns by their numbered positions:

for (i in 1:3){
  print(mean(air_data[,i], na.rm = TRUE))
}
## [1] 42.12931
## [1] 185.9315
## [1] 9.957516

When i = 1, R calculates the mean of air_data[,1]. It then moves to the second column, followed by the third.

Just like before, we can store our results instead of only printing them:

column_means = numeric(3)

for (i in 1:3){
  column_means[i] = mean(air_data[,i], na.rm = TRUE)
}

column_means
## [1]  42.129310 185.931507   9.957516

We can also loop over column names instead of column numbers:

columns = c("Ozone", "Solar.R", "Wind")

for (col in columns){
  print(mean(air_data[[col]], na.rm = TRUE))
}
## [1] 42.12931
## [1] 185.9315
## [1] 9.957516

During the first repetition, col = "Ozone", so air_data[[col]] refers to the Ozone variable. During the next repetition, col = "Solar.R", and so on.

We can combine column names with stored output:

columns = c("Ozone", "Solar.R", "Wind")
column_means = numeric(length(columns))
names(column_means) = columns

for (i in seq_along(columns)){
  column_means[i] = mean(air_data[[columns[i]]], na.rm = TRUE)
}

column_means
##      Ozone    Solar.R       Wind 
##  42.129310 185.931507   9.957516

Question #6

Use the following variables from air_data:

columns = c("Ozone", "Wind", "Temp")

Create a vector that stores the standard deviation of each variable using sd() and a for loop.

Give the resulting vector names corresponding to the column names.


Functions and Iteration

The previous examples used built-in functions such as mean() and sd() inside our loops. We can do exactly the same thing with functions that we create ourselves.

For example, suppose we wanted to calculate a mean without using R’s mean() function:

x = c(3, 7, 8, 12)
total = 0

for (value in x){
  total = total + value
}

total / length(x)
## [1] 7.5

total begins at zero. Each time the loop runs, we replace total with its previous value plus the current value from x.

We can package this inside a function:

my_mean = function(x){
  total = 0

  for (value in x){
    total = total + value
  }

  return(total / length(x))
}

Let’s compare it to R’s built-in function:

x = c(3, 7, 8, 12)

my_mean(x)
## [1] 7.5
mean(x)
## [1] 7.5

This is useful for understanding how repeated operations can be combined inside a function, but we normally would not calculate a mean this way in our own data analysis. R already has a mean() function that does the work for us.

The same is true for many mathematical operations. We could square every value using a loop:

x_squared = numeric(length(x))

for (i in seq_along(x)){
  x_squared[i] = x[i]^2
}

x_squared
## [1]   9  49  64 144

but R allows us to simply write:

x^2
## [1]   9  49  64 144

R is designed to perform many operations directly on entire vectors. When a simple vector operation or built-in function already does what we need, that will generally be easier to write and easier to read than creating a loop ourselves.

Question #7

Write a function named my_sum() that takes a numeric vector x and calculates its sum using a for loop.

Do not use sum() inside your function.

Test your function using:

test_values = c(4, 7, 12, 3, 9)

my_sum(test_values)
sum(test_values)

Verify that your function produces the same result as sum().

Question #8

Write a function named my_max() that takes a numeric vector x and finds its maximum value using a for loop.

Begin by treating the first value of x as the largest value you have encountered. Then, as your loop moves through x, update this value whenever you encounter something larger.

Do not use max() inside your function.

Test your function using:

test_values = c(-4, 12, 7, 21, 3, 16)

my_max(test_values)
max(test_values)

Verify that your function produces the same result as max().


Repeating Functions Across Data

Suppose we want to calculate the mean of several variables in our data. We already know how to do this with a loop:

columns = c("Wind", "Temp")

for (col in columns){
  print(mean(air_data[[col]], na.rm = TRUE))
}
## [1] 9.957516
## [1] 77.88235

We could also use the my_mean() function that we created:

column_means = numeric(length(columns))

for (i in seq_along(columns)){
  column_means[i] = my_mean(air_data[[columns[i]]])
}

column_means
## [1]  9.957516 77.882353

The function tells R how to calculate the mean of one vector. The loop repeatedly supplies different vectors to that function.

In principle, we could write additional loops to perform the calculations within each variable ourselves. We generally do not need to do this. Functions allow us to package the calculation for one input, and iteration allows us to repeatedly use that function on different inputs.


Other Ways to Iterate: Using lapply() and apply()

Repeatedly applying the same function to several variables is common enough that R has functions that can handle some of this iteration for us.

For example, we just used a loop to apply my_mean() to several columns. We can accomplish the same general task with lapply():

columns = c("Wind", "Temp")
lapply(air_data[columns], my_mean)
## $Wind
## [1] 9.957516
## 
## $Temp
## [1] 77.88235

lapply() applies the specified function separately to each element of the supplied object. In this case, those elements are the columns Wind and Temp.

Another option you may encounter is apply(), which can perform a function across rows or columns:

apply(air_data[columns], MARGIN = 2, FUN = mean)
##      Wind      Temp 
##  9.957516 77.882353

For apply(), MARGIN = 1 works across rows and MARGIN = 2 works across columns.

For now, you do not need to know every difference between these approaches. The important idea is that functions such as lapply() and apply() give us ways to perform some common types of iteration without explicitly writing the for loop ourselves.

Question #9

Use lapply() to calculate the standard deviation of the following variables:

columns = c("Wind", "Temp")

Compare your results to the values you would get from using sd() on each variable separately.


While Loops

One other type of loop you may encounter is a while loop. Rather than moving through a collection of known values, a while loop continues running while (as long as) a specified logical condition remains TRUE.

For example:

x = 1

while (x < 20){
  print(x)
  x = x * 2
}
## [1] 1
## [1] 2
## [1] 4
## [1] 8
## [1] 16

R checks whether x < 20 before each repetition. Eventually x becomes large enough that this condition becomes FALSE, and the loop stops.

This means that when writing a while loop, we need to ask:

What changes during the loop that will eventually make the condition FALSE?

Consider:

# **WARNING: Do not run this code.**
x = 1

while (x < 20){
  print(x)
}

x never changes, so x < 20 will remain TRUE indefinitely.

We can also change x in the wrong direction:

# **WARNING: Do not run this code.**
x = 1

while (x < 20){
  print(x)
  x = x - 1
}

Although x changes, it will never become large enough to make x < 20 false.

We will primarily use for loops and other iteration tools in our data analysis. For now, the important thing is to recognize how a while loop works and why its condition needs to eventually become FALSE.

Question #10

The following loop was intended to count down from 10 to 1:

# **Do not run the loop as written.**
count = 10

while (count > 0){
  print(count)
  count = count + 1
}

Explain why the loop does not terminate. Then modify the code so that it counts down from 10 to 1 and stops.



Required Practice

The following questions give you additional practice with the ideas from this lab. Some of these combine several ideas that we worked with separately above. If you do not finish these during class, complete them before our next class.

Question #11: Building Standard Deviation

Earlier we created my_mean() using a for loop. We can use a similar approach to create our own version of sd().

Recall that the sample standard deviation is:

\[ s = \sqrt{\frac{\sum (x_i-\bar{x})^2}{n-1}} \]

Create a function named my_sd() that calculates the sample standard deviation of a numeric vector.

Your function should:

  1. Use my_mean() to calculate the mean.
  2. Start an object at 0 to keep track of the sum of the squared differences.
  3. Use a for loop to add each squared difference to this total.
  4. Calculate and return the sample standard deviation.

Do not use sum(), mean(), or sd() inside my_sd(), although you should use my_mean() to calculate the mean.

Test your function using:

test_values = c(4, 8, 10, 12, 16)

my_sd(test_values)
sd(test_values)

The results should match.


Question #12: Conditions and Stored Output

Consider:

student_scores = data.frame(
  student = c("A", "B", "C", "D", "E", "F"),
  exam1 = c(82, 67, 91, 74, 88, 59),
  exam2 = c(79, 72, 94, 68, 85, 63)
)

student_scores
##   student exam1 exam2
## 1       A    82    79
## 2       B    67    72
## 3       C    91    94
## 4       D    74    68
## 5       E    88    85
## 6       F    59    63

Create a logical vector named both_above_70 with the same number of positions as there are rows in student_scores.

Use a for loop to go through the rows of student_scores. For each student, store either TRUE or FALSE in the corresponding position of both_above_70 depending on whether their exam1 and exam2 scores are both at least 70.

Print both_above_70 when you are finished.

Then use this logical vector to subset student_scores and return only the students who scored at least 70 on both exams.


Question #13: Looping Through Variables

Use the built-in mtcars data set:

car_data = mtcars

We are interested in:

columns = c("mpg", "hp", "wt", "qsec")

A: Use a for loop to calculate and store the mean of each variable.

B: Repeat the calculation using lapply().

Verify that both approaches give you the same values.

In a sentence or two, describe what repetitive work lapply() is handling for us that we explicitly wrote ourselves in Part A.


Question #14: Combining Functions and Iteration

Create a function named range_size() that takes a numeric vector and returns the difference between its maximum and minimum values.

Then use the following variables:

columns = c("mpg", "disp", "hp", "wt")

A: Use a for loop and your range_size() function to calculate and store the range size of each of these variables in car_data.

B: Give the resulting values names corresponding to the variables.

C: Use lapply() to perform the same calculations.

Verify that the two approaches produce the same values.


Question #15: While Loop Practice

Without running the loops first, determine whether each loop will eventually stop. If it will stop, predict the values it will print. If it will not stop, explain why.

x = 2

while (x < 15){
  print(x)
  x = x + 3
}
x = 10

while (x > 0){
  print(x)
  x = x + 1
}
x = 100

while (x > 5){
  print(x)
  x = x / 2
}

After making your predictions, run only the loops that you believe will terminate and check your answers. If you accidentally run a non-terminating loop, look for and click the red Stop Sign symbol at the top-right of your R console.