16 程序控制结构

16.1 表达式

R是一个表达式语言, 其任何一个语句都可以看成是一个表达式。 表达式之间以分号分隔或用换行分隔。 表达式可以续行, 只要前一行不是完整表达式(比如末尾是加减乘除等运算符, 或有未配对的括号)则下一行为上一行的继续。 若干个表达式可以放在一起组成一个复合表达式, 作为一个表达式使用,复合表达式的值为最后一个表达式的值, 组合用大括号表示, 如:

{
  x <- 15
  x
}

16.2 分支结构

分支结构包括if结构:

if (条件) 表达式1

if (条件) 表达式1 else 表达式2

其中的“条件”为一个标量的真或假值, 不允许取缺失值, 表达式可以是用大括号包围的复合表达式。 如

if(is.na(lambda)) lambda <- 0.5

又如

if(x>1) {
  y <- 2.5
} else {
  y <- -y
}

多个分支,可以在中间增加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, 否则等于零。

这样是错误的:

if(x>0) y <- 1 else y <- 0

正解为:

y <- numeric(length(x))
y[x>0] <- 1
y

16.2.2 ifelse函数

函数ifelse()可以根据一个逻辑向量中的多个条件, 分别选择不同结果。如

x <- c(-2, 0, 1)
y <- ifelse(x >=0, 1, 0); print(y)
## [1] 0 1 1

函数ifelse(test, yes, no)中的test是逻辑向量, yesno是向量, testyesno的配合符合向量化原则, 如果有长度为1的或者长度较短但其倍数等于最长一个的长度的, 短的一个自动从头循环使用。如:

ifelse((1:6) >= 3, 1:2, c(-1,-2))
## [1] -1 -2  1  2  1  2

当然,最常见的还是yesno为标量的情形。

不同于if语句, ifelsetest中运行有缺失值, 对应结果也是缺失值。

dplyr包的case_when函数可以看成是ifelse的多分支推广, 或看成`if-else if-else语句的向量化。 可以设定多个向量化的分支, 每个分支有对应的输出值。

16.2.3 switch函数

函数switch()可以建立多分枝结构。 不如if-else if-else结构容易理解。

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\):

x <- 0.0; s <- 0; n <- 5
for(i in 1:n){
  x <- 2*x + 1
  s <- s + x
}
print(s)
## [1] 57

在R中应尽量避免for循环: 其速度比向量化版本慢一个数量级以上, 而且写出的程序不够典雅。 比如,前面那个示性函数例子实际上可以简单地写成

set.seed(101); x <- rnorm(5)
y <- ifelse(x >= 0, 1, 0)
print(y)
## [1] 0 1 0 1 1

许多计数循环都可以用lapplyvapplysapplyapplymapMap等函数替代, 详见18.3

break语句退出所在的循环。 用next语句进入所在循环的下一轮。

使用for循环的注意事项:

  • 如果对向量每个元素遍历并保存结果, 应在循环之前先将结果变量产生等长的存储, 在循环内为已经分配好存储空间的输出向量的元素赋值。 为了产生长度为n的数值型向量,用numeric(n); 为了产生长度为n的列表,用vector("list", n)
  • 对一个向量元素遍历时如果用下标访问, 需要用seq_along(x)的做法而不是1:length(x)的做法。
  • 如果直接对向量元素遍历, 这有可能会丢失向量的属性(如日期型), 用下标访问则不存在此问题。

如:

x <- as.POSIXct(c("1981-05-31", "2020-02-22"))
for(xi in x){print(xi)}
## [1] 360086400
## [1] 1582300800
for(i in seq_along(x)){print(x[i])}
## [1] "1981-05-31 CST"
## [1] "2020-02-22 CST"

16.3.2 while循环和repeat循环

while(循环继续条件) 语句

进行当型循环。 其中的语句一般是复合语句。 仅当条件成立时才继续循环, 而且如果第一次条件就已经不成立就一次也不执行循环内的语句。

repeat 语句

进行无条件循环(一般在循环体内用if与break退出)。 其中的语句一般是复合语句。 如下的写法可以制作一个直到型循环:

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.4 R中判断条件

if语句和while语句中用到条件。 条件必须是标量值, 而且必须为TRUE或FALSE, 不能为NA或零长度。 这是R编程时比较容易出错的地方。

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()。 这样的表达更符合处理发生的次序,而且插入一个处理步骤也很容易。

处理用的函数也可以带有其它自变量,在管道控制中不要写第一个自变量。

例如:

2 |> sqrt() |> exp()
## [1] 4.11325

结果为\(e^{\sqrt{2}}\)

tibble类型的数据框尤其适用于如此的管道操作, 在26中有大量使用管道进行多步骤操作的例子。

magrittr包定义了%T%运算符, x %T% f()返回x本身而不是用f()修改后的返回值f(x), 这在中间步骤需要显示或者绘图但是需要进一步对输入数据进行处理时有用。

magrittr包定义了%$%运算符, 此运算符的作用是将左运算元的各个变量(这时左运算元是数据框或列表)暴露出来, 可以直接在右边调用其中的变量,类似于with()函数的作用。

magrittr包定义了%<>%运算符, 用在管道链的第一个连接, 可以将处理结果存入最开始的变量中, 类似于C语言的+=运算符。

如果一个操作是给变量加b,可以写成add(b), 给变量乘b,可以写成multiply_by(b)