16 程序控制结构
16.1 表达式
R是一个表达式语言, 其任何一个语句都可以看成是一个表达式。 表达式之间以分号分隔或用换行分隔。 表达式可以续行, 只要前一行不是完整表达式(比如末尾是加减乘除等运算符, 或有未配对的括号)则下一行为上一行的继续。 若干个表达式可以放在一起组成一个复合表达式, 作为一个表达式使用,复合表达式的值为最后一个表达式的值, 组合用大括号表示, 如:
16.2 分支结构
分支结构包括if结构:
if (条件) 表达式1
或
if (条件) 表达式1 else 表达式2
其中的“条件”为一个标量的真或假值, 不允许取缺失值, 表达式可以是用大括号包围的复合表达式。 如
又如
多个分支,可以在中间增加else if
,如:
x <- c(0.05, 0.6, 0.3, 0.9)
for(i in seq(along=x)){
if(x[i] <= 0.2){
cat("Small\n")
} else if(x[i] <= 0.8){
cat("Medium\n")
} else {
cat("Large\n")
}
}
请注意这里的多重if-else结构的程序缩进和排列方法, 这是一种易读且不容易引起误解的写法。
在多重if-else结构中,
后面的判断,
一定是在前面的判断为假的前提下进行判断的,
所以上例中间的"Medium"
本应写条件为x[i]>0.2 & x[i]<=0.8
,
但因为是第二个分支,
所以前提就是第一个分支的x[i]<=0.2
条件已经被否定。
16.2.1 用逻辑下标代替分支结构
R是向量化语言,尽可能少用标量运算。 比如,x为一个向量,要定义y与x等长, 且y的每一个元素当且仅当x的对应元素为正数时等于1, 否则等于零。
这样是错误的:
正解为:
16.2.2 ifelse
函数
函数ifelse()
可以根据一个逻辑向量中的多个条件,
分别选择不同结果。如
## [1] 0 1 1
函数ifelse(test, yes, no)
中的test
是逻辑向量,
yes
和no
是向量,
test
、yes
和no
的配合符合向量化原则,
如果有长度为1的或者长度较短但其倍数等于最长一个的长度的,
短的一个自动从头循环使用。如:
## [1] -1 -2 1 2 1 2
当然,最常见的还是yes
、no
为标量的情形。
不同于if
语句,
ifelse
的test
中运行有缺失值,
对应结果也是缺失值。
dplyr包的if_else
函数与基本R的ifelse
功能基本相同,
但允许指定条件为缺失值时的不同处理方法。
16.2.3 switch
函数
函数switch()
可以建立多分枝结构,
可以根据一个整数表达式或者一个字符串表达式的值选择返还相对应的值。
例如,某校将百分制转换到等级制的规则如下:
- 98-100: A+
- 95-97: A
- 90-94: A-
- 85-89: B+
- 80-84: B
- 77-79: B-
- 73-76: C+
- 70-72: C
- 67-69: C-
- 63-66: D+
- 60-62: D
- 0-59: F
从百分制转换为等级制可以用if-else if-else
完成。
从等级制转换回百分制,用区间平均值,可以写成switch()
的写法:
gr_v <- c("A+", "A", "A-", "B+", "B", "B-",
"C+", "C", "C-", "D+", "F", "EX")
for(gr in gr_v){
switch(gr,
"A+" = 99,
"A" = 96,
"A-" = 92,
"B+" = 87,
"B" = 82,
"B-" = 78,
"C+" = 75,
"C" = 71,
"C-" = 68,
"D+" = 65,
"F" = 50,
NA)
}
switch()
中的最后一个选择通常用于所有前面选择都不匹配时的一个例外情况处理,
这里的例子对这种情况返回缺失值。
上例中用了for
循环,见下一节的说明。
switch()
中允许对多个值采取同一行动或者返回同一结果,
如:
for(ch in c("a", "i", "c")){
cat(ch, ":", switch(ch,
"a"=, "e"=, "i"=, "o"=, "u" = "Vower",
"Consonant"), "\n")
}
## a : Vower
## i : Vower
## c : Consonant
在上例中,a, e, i, o, u返回相同的值"Vower"
,
而其它值则返回"Consonant"
。
如果要建立\(1:n\)到\(x_1 \sim x_n\)的映射,
可以用整数下标的方法;
如果要建立\(s_1 \sim s_n\)(字符串向量)到\(x_1 \sim x_n\)的映射,
可以用有名向量的方法。
switch()
则可以处理有例外项的问题。
更一般的分支,
尤其是不同分支执行不同命令的情况,
应该使用if-else if-else
结构。
dplyr包的case_when
函数可以switch()
的向量化推广,
看成是ifelse
的多分支推广,
或看成if-else if-else语句的向量化。 输入条件是向量化的, 根据输入条件返回条件对应的结果。 所有条件都不满足的元素的输出结果为
NA`。
如:
v <- c("A+", "A", "A-", "B+", "B", "B-",
"C+", "C", "C-", "D+", "F", "EX")
dplyr::case_when(
v == "A+" ~ 99,
v == "A" ~ 96,
v == "A-" ~ 92,
v == "B+" ~ 87,
v == "B" ~ 82,
v == "B-" ~ 78,
v == "C+" ~ 75,
v == "C" ~ 71,
v == "C-" ~ 68,
v == "D+" ~ 65,
v == "F" ~ 50)
## [1] 99 96 92 87 82 78 75 71 68 65 50 NA
x <- c("a", "i", "c")
dplyr::case_when(
x %in% c("a", "e", "i", "o", "u")
~ "Vowel",
TRUE
~ "Consonant"
)
## [1] "Vowel" "Vowel" "Consonant"
16.3 循环结构
16.3.1 计数循环
为了对向量每个元素、矩阵每行、矩阵每列循环处理,语法为
for(循环变量 in 序列) 语句
其中的语句一般是复合语句。 如:
set.seed(101); x <- rnorm(5)
y <- numeric(length(x))
for(i in 1:5){
if(x[i]>=0) y[i] <- 1 else y[i] <- 0
}
print(y)
## [1] 0 1 0 1 1
其中rnorm(5)
会生成5个标准正态分布随机数。
numeric(n)
生成有n个0的数值型向量(基础类型为double)。
如果需要对某个向量x
按照下标循环,
获得所有下标序列的标准写法是seq_along(x)
,
而不用1:n
的写法,
因为在特殊情况下n
可能等于零,这会导致错误下标,
而seq_along(x)
在x
长度为零时返回零长度的下标。
例如,设序列\(x_n\)满足\(x_0=0\), \(x_n = 2 x_{n-1} + 1\), 求\(S_n = \sum_{i=1}^n x_n\):
## [1] 57
在R中应尽量避免for循环: 其速度比向量化版本慢一个数量级以上, 而且写出的程序不够典雅。 比如,前面那个示性函数例子实际上可以简单地写成
## [1] 0 1 0 1 1
许多计数循环都可以用lapply
、vapply
、sapply
、apply
、map
、Map
等函数替代,
详见18.3。
用break
语句退出所在的循环。
用next
语句进入所在循环的下一轮。
使用for循环的注意事项:
- 如果对向量每个元素遍历并保存结果,
应在循环之前先将结果变量产生等长的存储,
在循环内为已经分配好存储空间的输出向量的元素赋值。
为了产生长度为
n
的数值型向量,用numeric(n)
; 为了产生长度为n
的列表,用vector("list", n)
。 - 对一个向量元素遍历时如果用下标访问,
需要用
seq_along(x)
的做法而不是1:length(x)
的做法。 - 如果直接对向量元素遍历, 这有可能会丢失向量的属性(如日期型), 用下标访问则不存在此问题。
如:
## [1] 360086400
## [1] 1582300800
## [1] "1981-05-31 CST"
## [1] "2020-02-22 CST"
16.3.2 while
循环和repeat
循环
用
while(循环继续条件) 语句
进行当型循环。 其中的语句一般是复合语句。 仅当条件成立时才继续循环, 而且如果第一次条件就已经不成立就一次也不执行循环内的语句。
用
repeat 语句
进行无条件循环(一般在循环体内用if与break退出)。 其中的语句一般是复合语句。 如下的写法可以制作一个直到型循环:
直到型循环至少执行一次,
每次先执行...
代表的循环体语句,
然后判断是否满足循环退出条件,
满足条件就退出循环。
例如,
常量\(e\)的值可以用泰勒展开式表示为
\[
e = 1 + \sum_{k=1}^\infty \frac{1}{k!}
\]
R函数exp(1)
可以计算e
的为了计算\(e\)的值,
下面用泰勒展开逼近计算e
的值:
e0 <- exp(1.0)
s <- 1.0
x <- 1
k <- 0
repeat{
k <- k+1
x <- x/k
s <- s + x
if(x < .Machine$double.eps) break
}
err <- s - e0
cat("k=", k, " s=", s, " e=", e0, " 误差=", err, "\n")
## k= 18 s= 2.718282 e= 2.718282 误差= 4.440892e-16
其中.Machine$double.eps
称为机器\(\varepsilon\),
是最小的加1之后可以使得结果大于1的正双精度数,
小于此数的正双精度数加1结果还等于1。
用泰勒展开公式计算的结果与exp(1)
得到的结果误差在\(10^{-16}\)左右。
16.5 管道控制
数据处理中经常会对同一个变量(特别是数据框)进行多个步骤的操作,
比如,先筛选部分有用的变量,再定义若干新变量,再排序。
R从4.1.0版本开始提供了一个|>
运算符实现这样的操作流程,
R的magrittr包提供了一个%>%
运算符执行类似功能。
比如,变量x
先用函数f(x)
进行变换,再用函数g(x)
进行变换,
一般应该写成g(f(x))
,用|>
运算符,可以表示成 x |> f() |> g()
。
更多的处理,如h(g(f(x)))
可以写成 x |> f() |> g() |> h()
。
这样的表达更符合处理发生的次序,而且插入一个处理步骤也很容易。
处理用的函数也可以带有其它自变量,在管道控制中不要写第一个自变量。
例如:
## [1] 4.11325
结果为\(e^{\sqrt{2}}\)。
又如:
## [1] 0.5236
这等价于round(asin(0.5), digits=4)
。
在|>
表示的管道中,
上一步的输出通过管道作为下一步的第一个自变量。
如果需要当作下一步的其它自变量,
可以用自变量名 = _
的特殊格式,
这里_
代表管道中上一步的结果。如:
## [1] 3.141593
这等价于round(pi, digits=6)
。
tibble类型的数据框尤其适用于如此的管道操作, 在23中有大量使用管道进行多步骤操作的例子。
magrittr包定义了%T%
运算符,
x %T% f()
返回x
本身而不是用f()
修改后的返回值f(x)
,
这在中间步骤需要显示或者绘图但是需要进一步对输入数据进行处理时有用。
magrittr包定义了%$%
运算符,
此运算符的作用是将左运算元的各个变量(这时左运算元是数据框或列表)暴露出来,
可以直接在右边调用其中的变量,类似于with()
函数的作用。
magrittr包定义了%<>%
运算符,
用在管道链的第一个连接,
可以将处理结果存入最开始的变量中,
类似于C语言的+=
运算符。
如果一个操作是给变量加b
,可以写成add(b)
,
给变量乘b
,可以写成multiply_by(b)
。