2.7 判断和循环(流程控制)

2.7.1 给有编程基础者的快速指南

如果没编程基础,没接触过判断和循环,请看第2.7.2小节。

如果学过其他编程语言,知道判断和循环的作用,只是需要知道在R中的表达,那么请看以下两个例子快速入门,然后跳至第2.8节:

m <- 1:100 # 产生一个[1,2,3,...,99,100]的整数向量。上面讲过。
n <- vector("numeric")
for (i in n) {
  if (i %% 2 == 0) {
    n <- append(n, i^2)
  } else if (i == 51) {
    break
  }
}
n
#> numeric(0)
logi = TRUE
num <- 1
while (num <= 100) {
  if (logi) {
    num = num + 10 # R 不支持 num += 5的简写
    print(num)
    logi = FALSE
  } else {
    num = num + 20
    print(num)
    logi = TRUE
  }
}
#> [1] 11
#> [1] 31
#> [1] 41
#> [1] 61
#> [1] 71
#> [1] 91
#> [1] 101

2.7.2 无编程基础者的快速指南

我认为,举例子比讲述概念更容易理解。

2.7.2.1 if, else if, else语句(“如果……”,“或者,如果……”,“否则……”)

# 以下代码翻译成英语就是:If 1 + 1 = 2, print "hi". Else, print "bye".
# 或中文:如果一加一等于二,那么印出“hi”,否则印出“bye”.
if (1 + 1 == 2) { # 1 + 1 == 2 的运算结果是TRUE,因此“如果”成真
  print("hi") # 所以会执行`print("hi")`
} else {
  print("bye")
}
#> [1] "hi"
# 代码第一行中的FALSE可以替换成任何计算结果为FALSE的运算,
# 比如1 + 1 == 3;小括号内的计算过程不重要,
# 但运算结果必须为TRUE或FALSE(不可以是NA)
if (FALSE) { 
  print("hi")
} else { # 因为是FALSE,所以`else`里的语句被执行
  print("bye")
}
#> [1] "bye"
if (FALSE) { # 第一个`if`为FALSE
  print("hi")
} else if (FALSE) { # 检查下一个`else if`,也是FALSE
  print("yoo")
} else if (TRUE) { # 再检查下一个`else if`,这次是TRUE
  print("hey") # 所以执行`print("hey")`
} else {
  print("bye") # 而轮不到else
}
#> [1] "hey"

2.7.2.2 for循环

# 以下代码翻译成英文就是: for every element i in c(2, 4, 6, 8): 
# assign i^2 to n, then print n
# 中文:对c(2, 4, 6, 8)`中的每一个元素i:
# 创建一个n使得n等于i的平方,然后印出n
for (i in c(2, 4, 6, 8)) { # i可以是任何你想要的名字,比如num
  n <- i^2 # 如果上一行是 for (num in ..., 这一行就要写成 n <- num^2
  print(n)
}
#> [1] 4
#> [1] 16
#> [1] 36
#> [1] 64
x <- vector(mode = "numeric") # 创建一个空的numeric vector
for (m in 1:10) {
  if (m %% 2 == 0) {
    x <- append(x, m)
  }
}

x
#> [1]  2  4  6  8 10
M <- c(1, 2, 3 ,4 ,5)
N <- c(10, 100, 1000)

x <- vector("numeric")
for (m in M) {
  for (n in N) { # 在一个for循环中嵌入另一个for循环
    x <- append(x, m*n)
  }
}

x
#>  [1]   10  100 1000   20  200 2000   30  300 3000   40  400 4000   50  500
#> [15] 5000

实际操作中,要想尽办法避免for循环,尤其是以上这种双层(多层)嵌套的for循环!原因和方法请看第2.7.4节。

2.7.2.3 while循环

x <- 1
while (x < 10) { # 当x<10的时候,执行大括号内的语句
  print(x)
  x <- x + 3 # 一定要让x的值增加,否则会进入无限循环
}
#> [1] 1
#> [1] 4
#> [1] 7

2.7.2.4 breaknext

for (i in 1:10) {
  if (i == 3) {
    next # 当i == 3时,跳过它,继续(最近的)for循环的下一个回合
  } else if (i == 6) {
    break # 当i == 6时,结束(最近的)for循环
  } 
  print(i) # 只有当if和else if里的检验都为FALSE时,`print(i)`才会执行。
}
#> [1] 1
#> [1] 2
#> [1] 4
#> [1] 5
M <- c(1, 2, 3, 4, 5)

x <- vector("numeric")
for (m in M) {
  while (TRUE) { # 原本while(TRUE){}将会是一个无限循环(判定条件永远TRUE)
    x <- append(x, 2*m)
    break # break打破了最近的这个while循环,而不影响for循环。
  }
}

x
#> [1]  2  4  6  8 10

2.7.3 严谨版

如果看懂了上一节中的例子,并且作为新手不太想深究,可以暂时跳过这一节,前往第2.8节。

这里很多内容还没完成,请前往第2.8节。

2.7.3.1 if, else, else if 语句

if else语句长这样:

if (something is true) {
  do something
} else {
  do some other things 
}

其中小括号内为测试的条件,其运算结果需为TRUE或FALSE(不能是NA!)。如果你还不熟悉关于逻辑值的计算,请看第2.6节。

  • 若运算结果为TRUE:大括号内的语句将会被执行。(如果语句只有一行,大括号可以省略)

  • 如运算结果为FALSE:
    • 如果后面没有else语句:什么都不会发生。
    • 如果后面有else语句:else后(大括号里)的语句将会被执行。

R中没有专门的elseif语句,但用else加上if能实现同样的效果。else if可以添加在if语句之后,顾名思义(“或者如果”),它的作用是,如果前一个if测试的条件为FALSE,那么再新加一个测试条件。一整个if/else/else if代码块里可以包含多个else if.

注意,不能直接用x == NA来判断x是否是NA,而要用is.na(x). 否则会得到NA的结果。

2.7.3.2 ifelse()函数

ifelse()if/else语句的向量化版本。假设我有一组长度:

l <- c(1.21, 1.34, -1.45, 1.56, 1.22, 1.10, 1.78, -1.33, 1.71)

我们发现有两个值是负数。长度不可能是负数,因此这些测量结果是错误的,我们需要把它们替换成NA. 这时可以用ifelse()函数:

l_1 <- ifelse(l < 0, NA, l)
l_1
#> [1] 1.21 1.34   NA 1.56 1.22 1.10 1.78   NA 1.71

2.7.3.3 for循环

以下是R中for循环的伪代码:

for(i in <vector/list>) {
  <do something> on every i
}

<vector/list>是一个向量时,这个for循环会对那个向量里所有的元素依次执行大括号里的命令(即<do something>),比如

x <- c(1, 4, 9)
y <- c(1, 10, 100)

for(i in x){
  print(i * y)
}
#> [1]   1  10 100
#> [1]   4  40 400
#> [1]   9  90 900

for(i in x)i的意思是x中的元素。x中有三个元素,每个元素都是一个i. 因此大括号里写的print(i * y)便是各个元素* y的意思。可以看到,这个for循环对于x里的三个元素,1, 4, 和9分别执行了三次“乘以y”的计算,分别得到1 10 100, 4 40 400, 9 90 900的结果,与

1 * y; 4 * y; 9 * y
#> [1]   1  10 100
#> [1]   4  40 400
#> [1]   9  90 900

是等效的。

这个i可以替换成其他的名字(大括号内相应的名字也要变),比如:

for(num in x){
  print(num * y)
}

注意到一个for循环实际上返回了多个结果(这里是三个)。这在实际操作中并不是很有用。更多的实际应用没必要在这里赘述,在以后的使用中会有很多例子,现在需要做的只是能看懂它的逻辑。

如果是对一个列表(list)使用for循环,每个i是一个分量。关于列表的内容在第2.4节,为进阶内容,可酌情阅读。

2.7.3.4 while循环

以下是R中while循环的伪代码:

while(<some condition is TRUE>) {
 repeat doing something
}

小括号里的内容必须是一个计算结果为TRUEFALSE的表达式(和if/else语句类似)。当这个条件为TRUE时,大括号内的语句将会被执行,直到小括号里的判别结果为FALSE. 需要注意的是,不要让小括号里的运算结果一直为TRUE, 否则会造成无限循环。一个错误的例子是:

i <- 1
while (i < 5) {
  print(i * 10)
}

i永远小于5,所以是一个无限循环。我们只需每次执行大括号里的计算时给i增加一定的值,即可解决这个问题:

i <- 1
while (i < 5) {
  print(i * 10)
  i <- i + 1
}
#> [1] 10
#> [1] 20
#> [1] 30
#> [1] 40

i被加到5时候,i不再小于5,因此大括号内的语句不再执行。

2.7.3.5 breaknext

break可用来跳出当前所在的for或while循环。

for(i in 1:10){
  if(i <= 3){
    print(i)
  } else break
}
#> [1] 1
#> [1] 2
#> [1] 3

可以看到,本来应对1至10逐个执行print,但当i等于4时,i不再小于等于3,触发else后的break, 结束for循环。While循环也是同样的道理:

i <- 1
while(i < 10){
  if(i <= 3){
    print(i)
    i <- i + 1
  } else break
}
#> [1] 1
#> [1] 2
#> [1] 3

对于for循环,我们可以用next跳过一个元素/分量,比如:

for(i in 1:5){
  if(i == 3 | i == 4) next
  print(i)
}
#> [1] 1
#> [1] 2
#> [1] 5

可以看到3和4被跳过了。

对于多层嵌套的循环,breaknext仅作用于当前所在的循环,比如:

for(i in 1:3){
  for(j in c(1, 10, 100)){
    if(i == 2) break
    print(i * j)
  }
}
#> [1] 1
#> [1] 10
#> [1] 100
#> [1] 3
#> [1] 30
#> [1] 300

像这样的结构,可以理解为,对于i等于1,2和3,分别执行3次(独立的)里面的for循环。

i等于2时,break被触发,但只是退出了里面那个for循环,而外面的for循环继续,使i等于3,然后重新执行另一次里面的for循环(因i不等于2,这一次不会被打断)。

2.7.4 如何避免for循环——apply()家族函数

R中的循环效率是很低的,尤其是有多层嵌套。通过system.time()函数,看看你的电脑执行以下运算需要花多少秒:(system.time()函数在第2.8.6小节有介绍)。如果你还不熟悉R中的函数,不妨先看完第2.8节。

x <- vector("numeric")
system.time(
  for (l in 1:40) {
    for (m in 1:50) {
      for (n in 1:60) {
        x <- c(x, l*m*n)
      }
    }
  }
)

我的i5处理器(i5-8259U CPU @ 2.30GHz)花了39秒左右才能算出来,然而看起来计算量并不大: \[x = \left(1\times1\times1, 1\times1\times2\ldots, 40\times50\times59, 40\times50\times60\right)\] 一共有\(40\times50\times60 = 120000\)次计算. 一个原因是,无论你的CPU有多少核心,R默认只会使用其中的一个进行计算。在第2.7.5.1节中介绍了开挂使用多核的方法。但是它治标不治本,解决for循环缓慢的终极方案是避免使用for循环,而使用向量化的方法进行计算 (vectorized computation)。在第2.1.5我介绍了简单的(二元)向量化计算。除了二元运算以外,很多时候,复杂的for循环也能用向量化计算实现。我们需要用到apply()家族的一系列函数:apply(), sapply(), lapply(), mapply(), tapply(), vapply(), rapply(), eapply();此外,像Map(), rep(), seq()等函数也会执行向量化的计算。

在学习它们的用法之前,先来看一个直观的数据:

方法 \((L,M,N)=(1:40,1:50,1:60)\) \((L,M,N)=(1:500,1:600,1:700)\)
普通(单核)for循环 39秒 等了一小时,无果,遂弃
开挂(四核)for循环 12.304秒;CPU巨热 怕CPU炸,不敢试
sapply() 0.001秒 2.719秒
rep() 0.002秒 2.825秒
rapply() 0.003秒 2.094秒

同样是运算上面那个for循环花了39秒的例子,使用sapply()函数和rep()函数几乎是瞬间完成;而把\((l,m,n)\)增至\((1:500,1:600,1:700)\)时(计算量为1750倍),它们仍只需不到3秒,而for循环则是不可行的。

至于如何用这些函数算出来,就作为本章的练习(见第2.9.2节)。

2.7.4.1 lapply()

lapply() (list apply)至少需要两个参数,第一个是对象(可以是vector或者list),第二个是函数。它的作用是把函数作用于对象中的每一个元素,并返回一个list. 无论对象是vector还是list, 返回的都是一个list.

有两类使用lapply()的方法。第一种是使用匿名函数,这个很直观:

lapply(c(1, 2, 3), function(i) i^2*10)
#> [[1]]
#> [1] 10
#> 
#> [[2]]
#> [1] 40
#> 
#> [[3]]
#> [1] 90

另一种是使用有命名的函数。此时,第二个参数是函数名;随后,如果有需要,还可以加上这个函数需要的其它参数:

lapply(list(5, 6, 7), rnorm, 3, .1)
#> [[1]]
#> [1] 3.16 3.05 3.08 3.06 3.05
#> 
#> [[2]]
#> [1] 3.07 2.97 2.98 3.06 3.02 2.92
#> 
#> [[3]]
#> [1] 2.98 2.95 2.92 3.05 3.01 3.00 2.93

默认lapply()的对象的各元素作为函数的第一个参数。上面这个例子等同于:

list(rnorm(5, 3, .1), # 即 `rnorm(n = 5, mean = 3, sd = .1)`
     rnorm(6, 3, .1),
     rnorm(7, 3, .1))

当第一个参数在后面被指定时,lapply()的对象的各元素所代表的参数按照排序顺延,比如:

lapply(list(5, 6, 7), rnorm, n = 3, .1)
#> [[1]]
#> [1] 4.92 5.21 5.18
#> 
#> [[2]]
#> [1] 5.98 6.00 5.90
#> 
#> [[3]]
#> [1] 6.93 6.99 7.00

等同于:

list(rnorm(n = 3, 5, .1),
     rnorm(n = 3, 6, .1),
     rnorm(n = 3, 7, .1))

但是这么做会降低易读性。当对象不是被作为函数的第一个参数时,最好使用匿名函数,使之更易读:

lapply(list(5, 6, 7), function(x) rnorm(3, x, .1))

2.7.4.2 sapply()

sapply() (simplified list apply)的功能本质上和lapply()一样。sapply()额外的一个特点是尽可能地化简结果:

  • 当结果只有一个分量时,sapply()返回一个vector
  • 当结果有多个分量,但每个分量只包含一个vector且长度相等时,sapply()会返回一个matrix

试试以下计算:

lapply(c(1, 2, 3), function(i) i*10)
sapply(c(1, 2, 3), function(i) i*10)

lapply(list(c(1, 2), c(4, 6), c(7, 9)), function(i) i*10)
sapply(list(c(1, 2), c(4, 6), c(7, 9)), function(i) i*10)

lapply(list(1, 2, 3), function(i) i*c(1, 10, 100))
sapply(list(1, 2, 3), function(i) i*c(1, 10, 100))

lapply(list(c(1, 2), c(4, 6), c(7, 9)), function(i) i*10)
sapply(list(c(1, 2, 3), c(4, 6), c(7, 9)), function(i) i*10)

2.7.4.3 rapply()

lapply()无法使用宇含有子列表的列表。比如,你可以尝试:

lapply(list(c(1, 2, 3), list(c(4, 5, 6))), "*", 10)

rapply()lapply()的recursive版本,它可以使用于含有子列表的列表,并且有三种使用模式,其中两种比较常用。第一种是unlist, 它是默认的模式。它会在计算之后拆解列表至单个向量:

rapply(list(c(1, 2, 3), list(c(4, 5, 6), list(7, 8, 9))), function(x) x * 10, how = "unlist") # 默认的模式
#> [1] 10 20 30 40 50 60 70 80 90

这可能会造成数据类型的强制转换:

rapply(list(c(1, 2, 3), list(c(4, 5, 6), list(c("a", "b", "c")))), function(x) c(x, 1))
#>  [1] "1" "2" "3" "1" "4" "5" "6" "1" "a" "b" "c" "1"

第二种模式,list,则保留了原列表的结构:

rapply(list(c(1, 2, 3), list(c(4, 5, 6), list(c("a", "b", "c")))), function(x) c(x, 1), how = "list") 
#> [[1]]
#> [1] 1 2 3 1
#> 
#> [[2]]
#> [[2]][[1]]
#> [1] 4 5 6 1
#> 
#> [[2]][[2]]
#> [[2]][[2]][[1]]
#> [1] "a" "b" "c" "1"

2.7.4.4 mapply()Map()

lapply()和它的衍生产物sapply()rapply()本质上是把一个函数应用在一个向量/列表上,即这个向量/列表作为函数唯一的“自变量”。Map()则可以使用多组自变量。这意味着,lapply()能做到的,Map都能做到;Map能做到的,lapply()不一定做得到。

之前lapply()的例子lapply(c(5, 6, 7), rnorm, n = 3, .1)Map()版本是这样的:

Map(rnorm, c(5, 6, 7), 3, .1)
#> [[1]]
#> [1] 2.94 2.99 3.10 2.85 2.82
#> 
#> [[2]]
#> [1] 2.99 2.87 2.90 2.87 3.02 2.94
#> 
#> [[3]]
#> [1] 3.10 2.95 2.90 2.83 2.94 2.99 3.05

多个自变量的计算也很自然:

Map(rnorm, c(2, 3, 4), c(1, 10, 100), c(.1, .5, 1))
#> [[1]]
#> [1] 1.00 1.07
#> 
#> [[2]]
#> [1] 10.71 10.70  9.86
#> 
#> [[3]]
#> [1] 101 100 101 100

mapply()Map()的自动化简版本:

mapply(rnorm, 3, c(1, 10, 100), c(.1, .5, 1))
#>       [,1]  [,2]  [,3]
#> [1,] 0.994 10.63 101.2
#> [2,] 1.082  9.26  99.4
#> [3,] 1.054 10.06 102.2

想一想,Map(rep, list(c(1,2), list(2,3)), 3)的计算结果是什么?

2.7.5 foreach 包:for循环的进化版

foreach 包相对于base R中的for循环增加了一些特性,不过最实用的是支持多核并行运算:

2.7.5.1 使用多内核进行计算

首先需要安装和使用doParallel,然后才可以使用foreach中的%dopar进行多核并行运算。

查看和设置内核数量:

library(doParallel)
getDoParWorkers() # 查看R当前使用的内核数量;默认应为1
#> [1] 1
detectCores() # 查看可用内核总数
#> [1] 8
registerDoParallel(4) # 设置内核数量
getDoParWorkers() # 再次检查内核数量
#> [1] 4

设置完之后就可以使用%dopar进行多核并行运算了:

x <- foreach(l = 1:40, .combine = "c") %dopar% {
    foreach(m = 1:50, .combine = "c") %dopar% {
        foreach(n = 1:60, .combine = "c") %do% {
        l*m*n
        }
    }
}
x

相比单核for循环的39秒,开挂(四核)的速度是12秒(计算量越大,优势越明显)。

2.7.6 purrr 包中的apply家族函数替代品和进化产物

这一节需要使用purrr, 它是tidyverse的一部分。所以我们首先要加载它:

library(tidyverse) # 或library(purrr)

2.7.6.1 map(), map_dbl(), map_chr(), …

map()的使用方法和lapply()几乎一样。lapply(list(5, 6, 7), rnorm, 3, .1)map()转写就是map(list(5, 6, 7), rnorm, 3, .1)map()(和下面介绍的其他函数)有一个绝招就是简写匿名函数。在第2.7.4.1节讲过,lapply()的对象默认会被作为函数的第一个参数(map()也是如此)。当不想让它作为第一个参数的时候,要使用匿名函数以保证易读性:

lapply(list(5, 6, 7), function(x) rnorm(3, x, .1))

map()的简写版本则是:

map(list(5, 6, 7), ~ rnorm(3, ., .1))
#> [[1]]
#> [1] 5.10 5.05 5.05
#> 
#> [[2]]
#> [1] 5.86 5.77 5.85
#> 
#> [[3]]
#> [1] 7.18 6.92 6.98

map_dbl(), map_chr()函数可以把结果化简为一个向量,前提是每次的计算结果的长度都为1(即一个标量),比如这里,mean(x), mean(y), mean(z)的结果都是一个标量,所以map()的结果可以化简为一个浮点数向量。

x = c(1, 2, 3); y = c(10, 20, 30); z = c(5, 60, 115)
map_dbl(list(x, y, z), mean)
#> [1]  2 20 60

2.7.6.2 map2()和`pmap()系列

map2()使用两个因变量。

map2(.x = c(1, 100, 10000), .y = c(.1, 1, 10), ~ rnorm(5, .x, .y))
#> [[1]]
#> [1] 0.760 0.991 1.004 1.209 0.881
#> 
#> [[2]]
#> [1] 100.1  99.4  99.8 100.3 100.5
#> 
#> [[3]]
#> [1]  9999  9995 10000  9996 10023

pmap()使用多个因变量。与Base R的Map()不同,pmap()的第一个参数是对象,第二个才是函数。你可以使用命名列表来指定使用的函数的参数:

pmap(list(mean = c(1, 100, 10000), sd = c(.1, 1, 10)), rnorm, n = 3)
#> [[1]]
#> [1] 0.833 0.967 0.865
#> 
#> [[2]]
#> [1]  98.8 100.2 101.1
#> 
#> [[3]]
#> [1] 10008  9993 10027

下一章会讲到,因为dataframe/tibble的本质是list,上面的操作也可以适用于tibble:

args <- tibble(mean = c(1, 100, 10000),
               sd = c(.1, 1, 10))
pmap(args, rnorm, 3)
#> [[1]]
#> [1] 0.994 0.922 1.103
#> 
#> [[2]]
#> [1] 101 100 101
#> 
#> [[3]]
#> [1] 10004  9990 10000