使用purrr实现迭代
15.1 简介
函数是减少重复代码的一种工具,其减少重复代码的方法是,先识别出代码中的重复模式,然后将其提取出来,成为更容易修改和重用的独立部分。减少重复代码的另一种工具是迭代,它的作用在于可以对多个输入执行同一种处理,比如对多个列或多个数据集进行同样的操作。
library(tidyverse)
15.2 for循环
举个例子,计算下面数据框的每列中位数:
df <- tibble( a = rnorm(10), b = rnorm(10), c = rnorm(10), d = rnorm(10) ) > df # A tibble: 10 x 4 a b c d <dbl> <dbl> <dbl> <dbl> 1 -0.0803 0.0689 0.548 1.37 2 -0.998 2.14 0.222 0.244 3 -0.882 -0.229 -0.776 -1.24 4 -0.293 0.781 0.482 1.69 5 -0.875 0.0440 0.964 -0.196 6 -0.461 -1.29 -2.17 -1.62 7 -0.417 0.206 0.230 -0.564 8 -2.21 -4.66 0.957 1.30 9 -1.41 -2.05 0.859 -1.03 10 -0.820 1.19 0.101 -0.0891
可以一列一列重复的算:
> median(df$a) [1] -0.8474354 > median(df$b) [1] 0.05642144 > median(df$c) [1] 0.3562925 > median(df$d) [1] -0.1424249
也可以使用for循环:
output <- vector("double", ncol(df)) # 1. 输出 for (i in seq_along(df)) { # 2. 序列 output[[i]] <- median(df[[i]]) # 3. 循环体 } output > output [1] -0.84743543 0.05642144 0.35629250 [4] -0.14242489
每个 for 循环都包括 3 个部分。
输出:output <- vector("double", length(x))
在开始循环前,必须为输出结果分配足够的空间。这对循环效率非常重要,如果在每次迭代中都使用 c() 来保存循环的结果,那么 for 循环的速度就会特别慢。创建给定长度的空向量的一般方法是使用 vector() 函数,该函数有两个参数:向量类型("logical"、"integer"、"double"、"character" 等)和向量的长度。
序列:i in seq_along(df)
这部分确定了使用哪些值来进行循环:每一轮 for 循环都会赋予 i 一个来自于 seq_ along(df) 的不同的值。我们可以将 i 看作一个代词,和 it 类似。 seq_along() 函数的作用与 1:length(l) 的作用基本相同,但最重要的区别是更加安全。如果我们有一个长度为 0 的向量,那么 seq_along() 会进行正确的处理,而1:length(l)则会出错:
> y <- vector("double", 0) > seq_along(y) integer(0) > 1:length(y) [1] 1 0
循环体:output[[i]] <- median(df[[i]])
这部分就是执行具体操作的代码。它们会重复运行,每次运行都使用一个不同的 i 值。第一次迭代运行的是 output[[1]] <- median(df[[1]]),第二次迭代运行的是 output[[2]] <- median[df[[2]]],以此类推。
15.3 for循环的变体
15.3.1 修改现有对象
比如《R数据科学》学习笔记|Note13:函数中讲过的标准化例子:
df <- tibble( a = rnorm(10), b = rnorm(10), c = rnorm(10), d = rnorm(10) ) rescale01 <- function(x) { rng <- range(x, na.rm = TRUE) (x - rng[1]) / (rng[2] - rng[1]) } df$a <- rescale01(df$a) df$b <- rescale01(df$b) df$c <- rescale01(df$c) df$d <- rescale01(df$d)
使用 for 循环解决最后的重复问题:
for (i in seq_along(df)) { df[[i]] <- rescale01(df[[i]]) }
值得注意的是,在所有 for 循环中使用的都是 [[。因为它可以明确表示我们要处理的是单个元素。
15.3.2 循环模式
除了通过 for (i in seq_along(xs)) 使用数值索引进行循环,并使用 x[[i]] 提取出相应的值这种最常用的循环方式外,还有另外两种循环方式:
使用元素进行循环:for (x in xs)。如果只关心副作用,比如绘图或保存文件,那么这种方式是最适合的,因为有效率地保存输出结果是非常困难的。
使用名称进行循环:for (nm in names(xs))。这种方式会给出一个名称,你可以使用这个名称和 x[[nm]] 来访问元素的值。如果想要在图表标题或文件名中使用元素名称,那么你就应该使用这种方式。
创建命名的输出向量:
results <- vector("list", length(x)) names(results) <- names(x)
使用数值索引进行循环是最常用的方式,因为给定位置后,就可以提取出元素的名称和值:
for (i in seq_along(x)) { name <- names(x)[[i]] value <- x[[i]] }
15.3.3 未知的输出长度
有时你可能不知道输出的长度。例如,假设你想模拟长度随机的一些随机向量。
means <- c(0, 1, 2) output <- double() for (i in seq_along(means)) { n <- sample(100, 1) output <- c(output, rnorm(n, means[[i]])) } str(output) > str(output) num [1:172] -0.976 0.235 0.626 0.291 -0.552 ...
但这并不是一种非常高效的方式,因为 R 要在每次迭代中复制上一次迭代中的所有数据。 从技术角度来看,你执行了一种“平方”操作,这意味着,如果元素数量增加到原来的 3 倍,那么循环时间就要增加到原来的 9 倍。
更好的解决方式是将结果保存在一个列表中,循环结束后再组合成一个向量:
out <- vector("list", length(means)) for (i in seq_along(means)) { n <- sample(100, 1) out[[i]] <- rnorm(n, means[[i]]) } str(out) str(unlist(out)) > str(out) List of 3 $ : num [1:94] -0.2441 -1.9689 -0.8901 0.1324 -0.0745 ... $ : num [1:22] 0.6286 0.0227 0.9343 1.5355 1.013 ... $ : num [1:37] 3.116 1.539 -0.472 2.6 3.13 ... > str(unlist(out)) num [1:153] -0.2441 -1.9689 -0.8901 0.1324 -0.0745 ...
这里我们使用了 unlist() 函数将一个向量列表转换为单个向量。
如果生成一个的是很长的字符串,不要使用 paste() 函数将每次迭代的结果与上一 次连接起来,而应该将每次迭代结果保存在字符向量中,然后再使用 paste(output, collapse = "") 将这个字符向量组合成一个字符串。
如果生成一个的是很大的数据框,不要在每次迭代中依次使用 rbind() 函数,而应该将每次迭代结果保存在列表中,再使用 dplyr::bind_rows(output) 将结果组合成数据框。
15.3.4 未知的序列长度
有时你甚至不知道输入序列的长度。例如,在掷硬币时,你想要循环到连续 3 次掷出正面向上。这种迭代不能使用 for 循环来实现,而应该使用 while 循环。while 循环比 for 循环更简单,因为前者只需要 2 个部分:条件和循环体。
while (condition) { # 循环体 } 1 2 3 for (i in seq_along(x)) { # 循环体 } # 等价于 i <- 1 while (i <= length(x)) { # 循环体 i <- i + 1 }
使用 while 循环找出了连续 3 次掷出正面向上的硬币所需的投掷次数:
flip <- function() sample(c("T", "H"), 1) # sample(x, size, replace = FALSE) # x 整体数据,以向量形式给出 # size 抽取样本的数目 # replace 如果为F(默认),则是不重复抽样,此时size不能大于x的长度; # sample(c("T", "H"), 1)相当于随机扔硬币 flips <- 0 #总次数 nheads <- 0 #连续投掷正面次数 while (nheads < 3) { #连续投掷正面次数 <3 if (flip() == "H") { #如果投掷到正面 nheads <- nheads + 1 #连续投掷正面次数 +1 } else { nheads <- 0 #否则连续投掷正面次数归零 } flips <- flips + 1 #总次数+1 } #循环至nheads = 3,即投掷正面次数连续三次 flips #输出投掷总次数
15.4 for循环与函数式编程
for 循环在 R 中不像在其他语言中那么重要,因为 R 是一门函数式编程语言。这意味着可以先将 for 循环包装在函数中,然后再调用这个函数,而不是直接使用 for 循环。
如计算每列均值:
df <- tibble( a = rnorm(10), b = rnorm(10), c = rnorm(10), d = rnorm(10) ) output <- vector("double", length(df)) for (i in seq_along(df)) { output[[i]] <- mean(df[[i]]) } output
可以将这段代码提取出来,转换成一个函数:
col_mean <- function(df) { output <- vector("double", length(df)) for (i in seq_along(df)) { output[i] <- mean(df[[i]]) } output }
也可以计算出每列的中位数和标准差:
col_median <- function(df) { output <- vector("double", length(df)) for (i in seq_along(df)) { output[i] <- median(df[[i]]) } output } col_sd <- function(df) { output <- vector("double", length(df)) for (i in seq_along(df)) { output[i] <- sd(df[[i]]) } output }
通过添加支持函数应用到每列的一个参数,我们可以使用同一个函数完成与 col_mean()、 col_median() 和 col_sd() 函数相同的操作:
col_summary <- function(df, fun) { out <- vector("double", length(df)) for (i in seq_along(df)) { out[i] <- fun(df[[i]]) } out } > col_summary(df, median) [1] -0.84743543 0.05642144 0.35629250 [4] -0.14242489 > col_summary(df, mean) [1] -0.84475995 -0.38086077 0.14231605 [4] -0.01268487
将函数作为参数传入另一个函数的这种做法是一种非常强大的功能,它是促使 R 成为函数式编程语言的因素之一。