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 break
和 next
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
}
小括号里的内容必须是一个计算结果为TRUE
或FALSE
的表达式(和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 break
和next
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被跳过了。
对于多层嵌套的循环,break
和next
仅作用于当前所在的循环,比如:
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