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.
Throughout this lab, we will explore:
for loop repeatedly performs an operation and
what changes during each repetition.apply(), lapply(), and while
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 (I may
sometimes call this the loop’s index, similar to the index underneath a
summation symbol in mathematics)x{ }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.
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)
}
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.
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.
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.
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.
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.
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.
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
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.
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.
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().
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().
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.
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.
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.
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.
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.
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.
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:
my_mean() to calculate the mean.0 to keep track of the sum of the
squared differences.for loop to add each squared difference to this
total.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.
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.
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.
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.
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.