14.2 Iteration: Loops and purrr

DATA 412/612 · Week 14 · PDF · Source (.qmd)

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

  1. 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.
  2. map_dbl() means of every column of mtcars.
  3. Number of unique values in each column of iris. Repeat with across(). typeof() of each column of nycflights13::flights with map_chr(), then with across().
  4. p-value of t.test(mpg ~ am) within each cyl group of mtcars.
  5. In three lines, keep the mtcars columns whose mean is greater than 10 and report those means (keep() then map_dbl()). You should get four variables.
  6. Standard deviation of each numeric column of iris with a for-loop, then with across().