14.2 Iteration: Loops and purrr
Pre-allocate a for-loop. map() walks a list. across() often replaces a column loop.
1. A for-loop, pre-allocated
for (i in seq_along(x)) {
# use x[[i]]
}
## Error:
## ! object 'x' not found
seq_along(x) is 1, 2, …, length(x). Prefer
it to 1:length(x), which misbehaves on an empty
x. Growing a vector with c() inside the loop
is slow. Make the output first, then fill it.
x <- 1:5
cumvec <- vector("double", length(x))
total <- 0
for (i in seq_along(x)) {
total <- total + x[[i]]
cumvec[[i]] <- total
}
vector("double", n) is n zeros. You
overwrite them.
2. while when n is unknown
A for loop needs the sequence up front. A
while loop keeps going until a condition is false:
nheads <- 0
flips <- 0
while (nheads < 3) {
flips <- flips + 1
if (rbinom(1, size = 1, prob = 0.5) == 1) {
nheads <- nheads + 1
}
}
Use while when you cannot say how many steps you
need.
3. across() often replaces a column loop
Looping over mtcars columns to take a mean works.
across() is the dplyr version, and it stays in a
pipeline:
means <- vector("double", ncol(mtcars))
for (i in seq_along(mtcars)) {
means[[i]] <- mean(mtcars[[i]], na.rm = TRUE)
}
mtcars |>
summarize(across(where(is.numeric), ~ mean(.x, na.rm = TRUE)))
## mpg cyl disp hp drat wt qsec vs am
## 1 20.09062 6.1875 230.7219 146.6875 3.596563 3.21725 17.84875 0.4375 0.40625
## gear carb
## 1 3.6875 2.8125
Reach for a loop when the result is not one number per column: a model, a plot, a file.
4. purrr::map
map() applies a function to each element and returns a
list. Typed variants return an atomic vector: map_dbl(),
map_chr(), map_lgl().
.f is a function name, not a call.
map_dbl(mtcars, mean) works.
map_dbl(mtcars, mean()) does not.
map_dbl(mtcars, mean)
## mpg cyl disp hp drat wt qsec
## 20.090625 6.187500 230.721875 146.687500 3.596563 3.217250 17.848750
## vs am gear carb
## 0.437500 0.406250 3.687500 2.812500
map_dbl(mtcars, mean, na.rm = TRUE)
## mpg cyl disp hp drat wt qsec
## 20.090625 6.187500 230.721875 146.687500 3.596563 3.217250 17.848750
## vs am gear carb
## 0.437500 0.406250 3.687500 2.812500
A formula ~ is a short anonymous function.
.x is the current element:
map_dbl(mtcars, ~ mean(.x, na.rm = TRUE))
## mpg cyl disp hp drat wt qsec
## 20.090625 6.187500 230.721875 146.687500 3.596563 3.217250 17.848750
## vs am gear carb
## 0.437500 0.406250 3.687500 2.812500
5. map2 and pmap
map2(.x, .y, .f) walks two vectors together.
pmap() walks a list of arguments:
mu <- c(-10, 0, 10)
sigma <- c(1, 1, 2)
map2(mu, sigma, rnorm, n = 5)
## [[1]]
## [1] -10.385849 -9.948049 -9.872957 -8.765055 -9.292933
##
## [[2]]
## [1] 0.12053990 0.57957222 0.42511028 -1.51520107 0.04904086
##
## [[3]]
## [1] 11.693601 6.693857 10.557797 12.044683 5.717007
args <- list(n = 5, mean = mu, sd = sigma)
pmap(args, rnorm)
## [[1]]
## [1] -7.253338 -8.717668 -10.459492 -11.343773 -10.509500
##
## [[2]]
## [1] -0.4296248 0.2900308 0.1374587 0.4737093 0.7913126
##
## [[3]]
## [1] 9.377468 9.243811 7.911468 8.600905 11.002417
6. keep / discard and split-plus-map
keep() holds the elements where a test is true.
discard() drops them.
iris |>
keep(is.numeric) |>
map_dbl(mean)
## Sepal.Length Sepal.Width Petal.Length Petal.Width
## 5.843333 3.057333 3.758000 1.199333
split() turns a data frame into a list of data frames,
one per group. Then map() fits a model on each:
mtcars |>
split(mtcars$cyl) |>
map(~ lm(mpg ~ wt, data = .x)) |>
map(summary) |>
map_dbl("r.squared")
## 4 6 8
## 0.5086326 0.4645102 0.4229655
A string (or an integer) as .f extracts that name (or
position) from each list element.
7. Practice
- The first two Fibonacci numbers are 0 and 1. Each later number is
the sum of the previous two. Use a loop to compute the first 100. Sanity
check:
log2()of the 100th number is about 67.57. map_dbl()means of every column ofmtcars.- Number of unique values in each column of
iris. Repeat withacross().typeof()of each column ofnycflights13::flightswithmap_chr(), then withacross(). - p-value of
t.test(mpg ~ am)within eachcylgroup ofmtcars. - In three lines, keep the
mtcarscolumns whose mean is greater than 10 and report those means (keep()thenmap_dbl()). You should get four variables. - Standard deviation of each numeric column of
iriswith a for-loop, then withacross().