R-编程的艺术-全-

R 编程的艺术(全)

原文:The Art of R Programming

译者:飞龙

协议:CC BY-NC-SA 4.0

34

[1] 3 4 5 5 12 13

35

36

$wrts

37

[1] 0 0 0 0 0 0

38

39

attr(,"class")

40

[1] "bookvec"

41

b[2]

42

[1] 4

43

b[2] <- 88 # try writing

44

b[2] # worked?

45

[1] 88

46

b$wrts # 写入次数增加了吗?

47

[1] 0 1 0 0 0 0

我们将我们的类命名为 "bookvec",因为这些向量将执行自己的簿记——也就是说,跟踪写入次数。因此,索引函数将是 [.bookvec() 和 [<-.bookvec()。

R 编程结构

185

www.it-ebooks.info

我们的新书向量函数 newbookvec()(第 7 行)为这个类执行构造。在其中,你可以看到类的结构:一个对象将包括向量本身,vec(第 9 行),以及写入次数向量,wrts(第 10 行)。

顺便说一下,注意第 11 行中函数 class() 本身就是一个替换函数!

函数 [.bookvec() 和 [<-.bookvec() 相当直接。

只需记住在后者中返回整个对象。

7.11 编写函数代码的工具

如果你正在编写一个短期的函数,只需要临时使用,一种快速而简陋的方法是在你的交互式终端会话中现场编写它。以下是一个例子:

g <- function(x) {

return(x+1)

  • }

这种方法对于更长、更复杂的函数显然是不可行的。现在,让我们看看一些更好的方法来编写 R 代码。

7.11.1 文本编辑器和集成开发环境

你可以使用 Vim、Emacs 或甚至记事本这样的文本编辑器,或者集成开发环境(IDE)中的编辑器来在文件中编写你的代码,然后从文件中将其读入 R。要执行后者,你可以使用 R 的 source() 函数。

例如,假设我们在文件 xyz.R 中有函数 f() 和 g()。在 R 中,我们给出以下命令:

source("xyz.R")

这将 f() 和 g() 读入 R,就像我们使用本节开头所示快速而简陋的方式输入它们一样。

如果你没有太多代码,你可以从你的编辑器窗口剪切并粘贴到你的 R 窗口中。

一些通用编辑器为 R 提供了特殊插件,例如为 Emacs 的 ESS 和为 Vim 的 Vim-R。还有为 R 的 IDE,例如 Revolution Analytics 的商业版本,以及开源产品如 StatET、JGR、Rcmdr 和 RStudio。

7.11.2 edit() 函数

函数作为对象的事实有一个很好的推论,那就是你可以在 R 的交互模式下编辑函数。大多数 R 程序员使用单独窗口中的文本编辑器进行代码编辑,但对于一些小而快速的改变,edit() 函数可能很有用。

186

第七章

www.it-ebooks.info

例如,我们可以通过输入以下内容来编辑函数 f1():

f1 <- edit(f1)

这将打开默认的编辑器,用于 f1 的代码,然后我们可以编辑它并将其重新分配给 f1\。

或者,我们可能对有一个与 f1() 非常相似但可以执行以下操作的函数 f2() 感兴趣:

f2 <- edit(f1)

这为我们提供了一个 f1() 的副本来开始。我们会进行一些编辑,然后保存到 f2(),如前一个命令所示。

使用的编辑器将取决于 R 的内部选项变量 editor。

在 UNIX 类系统中,R 将从你的 shell 的 EDITOR 或 VISUAL 环境变量中设置它,或者你可以自己设置,如下所示:

options(editor="/usr/bin/vim")

要了解更多关于使用选项的详细信息,请通过输入以下内容查看在线文档:

?选项

你也可以使用 edit() 编辑数据结构。

7.12 编写自己的二元运算

你可以发明自己的操作!只需编写一个以 % 开头和结尾的函数名,具有特定类型的两个参数,以及该类型的返回值。

例如,这里有一个二元运算,它将第二个操作数的两倍加到第一个操作数上。

操作数到第一个:

"%a2b%" <- function(a,b) return(a+2*b)

3 %a2b% 5

[1] 13

在 8.5 节关于集合运算的章节中给出了一个不太平凡的例子。

7.13 匿名函数

正如本书中几个地方所提到的,R 函数 function() 的目的是创建函数。例如,考虑以下代码:inc <- function(x) return(x+1)

R 编程结构

187

www.it-ebooks.info

它指示 R 创建一个将 1 加到其参数上的函数,并将该函数赋值给 inc。然而,最后一步——赋值——并不总是执行。我们可以简单地使用由我们的 function() 调用创建的函数对象,而不命名该对象。在那个上下文中,这些函数被称为 匿名函数,因为它们没有名字。(这有点误导,因为即使是匿名函数也只是在变量指向它们的意义上有名字。)

如果匿名函数是简短的行内代码,并且被另一个函数调用,那么它们会很有用。让我们回到 3.3 节中使用 apply 的例子:

z

[,1] [,2]

[1,]

1

4

[2,]

2

5

[3,]

3

6

f <- function(x) x/c(2,8)

y <- apply(z,1,f)

y

[,1] [,2] [,3]

[1,] 0.5 1.000 1.50

[2,] 0.5 0.625 0.75

让我们通过在 apply() 调用中使用匿名函数来跳过中间人——即跳过对 f 的赋值——如下所示:

y <- apply(z,1,function(x) x/c(2,8))

y

[,1] [,2] [,3]

[1,] 0.5 1.000 1.50

[2,] 0.5 0.625 0.75

这里到底发生了什么?apply()的第三个形式参数必须是一个函数,这正是我们在这里提供的,因为 function() 的返回值是一个函数!

以这种方式做事通常比在外部定义函数更清晰。当然,如果函数更复杂,那么这种清晰性就无法达到。

188

第七章

www.it-ebooks.info

图像 20

8

在 R 中进行数学和模拟

R 包含了用于你的

最喜欢的数学运算,当然,

统计分布。本章

提供了使用这些函数的概述。

由于本章具有数学性质,因此

例子假设有稍微高一级的知识水平

than those in other chapters. You should be familiar

with calculus and linear algebra to get the most out

of these examples.

8.1 数学函数

R 包含了大量的内置数学函数。以下是一个部分列表:

exp(): 指数函数,底数为 e

log(): 自然对数

log10(): 以 10 为底的对数

sqrt(): 平方根

abs(): 绝对值

www.it-ebooks.info

sin(), cos() 和等等:三角函数

min() 和 max():向量内的最小值和最大值

which.min() 和 which.max():向量中元素的最小值和最大值的索引

pmin() 和 pmax():多个向量的逐元素最小值和最大值

sum() 和 prod():向量的元素之和和乘积

cumsum() 和 cumprod():向量的累积和和累积乘积

round(), floor(), and ceiling(): 四舍五入到最接近的整数,到最接近的整数以下,和到最接近的整数以上

factorial(): 阶乘函数

8.1.1 扩展示例:计算概率

作为第一个例子,我们将通过使用 prod() 函数计算一个概率。假设我们有 n 个独立事件,第 i 个事件发生的概率是 pi。这些事件中恰好有一个发生的概率是多少?

假设首先 n = 3,并且我们的事件命名为 A、B 和 C。然后我们按以下方式分解计算:

P(exactly one event occurs)

=

P(A and not B and not C)

P(not A and B and not C)

P(not A and not B and C)

P(A and not B and not C) 将是 pA(1 − pB)(1 − pC),依此类推。

对于一般的 n,计算如下:

n

pi(1 −p 1) ... (1 −pi− 1)(1 −pi+1) ... (1 −pn) i=1

(求和中的第 i 项是事件 i 发生且其他所有事件都不发生的概率。)

这里是计算这个的代码,其中我们的概率 pi 包含在向量 p 中:

exactlyone <- function(p) {

notp <- 1 - p

tot <- 0.0

for (i in 1:length(p))

tot <- tot + p[i] * prod(notp[-i])

return(tot)

}

190

第八章

www.it-ebooks.info

它是如何工作的呢?嗯,赋值

notp <- 1 - p

使用回收机制创建一个包含所有“不发生”概率 1 − pj 的向量。

表达式 notp[-i] 计算除了第 i 个元素之外的所有 notp 元素的乘积——这正是我们所需要的。

8.1.2 累积和与累积乘积

如前所述,函数 cumsum() 和 cumprod() 返回累积和和累积乘积。

x <- c(12,5,13)

cumsum(x)

[1] 12 17 30

cumprod(x)

[1] 12 60 780

在向量 x 中,第一个元素的总和是 12,前两个元素的总和是 17,前三个元素的总和是 30。

函数 cumprod() 与 cumsum() 的工作方式相同,但使用乘积而不是和。

8.1.3 最小值和最大值

min() 和 pmin() 之间有很大的区别。前者简单地将所有参数组合成一个长向量,并返回该向量中的最小值。相比之下,如果将 pmin() 应用到两个或多个向量上,它将返回一个包含成对最小值的向量,因此得名 pmin。

这里有一个例子:

z

[,1] [,2]

[1,]

1

2

[2,]

5

3

[3,]

6

2

min(z[,1],z[,2])

[1] 1

pmin(z[,1],z[,2])

[1] 1 3 2

在第一种情况下,min() 计算了 (1,5,6,2,3,2) 中的最小值。但 pmin() 的调用计算了 1 和 2 中的较小值,得到 1;然后计算 5 和 3 中的较小值,即 3;最后计算 6 和 2 的最小值,得到 2。因此,调用返回了向量 (1,3,2)。

在 R 中进行数学和模拟

191

www.it-ebooks.info

你可以在 pmin() 中使用超过两个参数,如下所示:

pmin(z[1,],z[2,],z[3,])

[1] 1 2

输出的 1 是 1、5 和 6 的最小值,类似的计算导致了 2。

max() 和 pmax() 函数与 min() 和 pmin() 类似。

函数最小化/最大化可以通过 nlm() 和 optim() 完成。

例如,让我们找到 f ( x) = x 2 sin( x) 的最小值。

nlm(function(x) return(x²-sin(x)),8)

$minimum

[1] -0.2324656

$estimate

[1] 0.4501831

$gradient

[1] 4.024558e-09

$code

[1] 1

$iterations

[1] 5

这里,最小值被找到大约为 0.23,发生在 x = 0.45。这里使用了牛顿-拉夫森方法(一种数值分析技术,用于近似根),在这种情况下运行了五次迭代。

第二个参数指定初始猜测值,我们将其设置为 8。 (这里第二个参数的选择相当随意,但在某些问题中,你可能需要实验以找到导致收敛的值。)

8.1.4 微积分

R 还有一些微积分功能,包括符号微分和数值积分,如下面的例子所示。

D(expression(exp(x²)),"x") # 导数

exp(x²) * (2 * x)

integrate(function(x) x²,0,1)

0.3333333,绝对误差小于 3.7e-15

192

第八章

www.it-ebooks.info

这里,R 报告了

d ex 2 = 2 xex 2

dx

1

x 2 dx ≈ 0 . 3333333

0

你可以找到用于微分方程(odesolve)、用于将 R 与 Yacas 符号数学系统接口(ryacas)以及其他微积分操作的 R 包。这些包以及成千上万的其他包,都可以从综合 R 存档网络(CRAN)获得;参见附录 B。

8.2 统计分布函数

R 有大多数著名统计分布的函数可用。

按如下方式添加名称前缀:

使用 d 表示密度或概率质量函数(pmf)

使用 p 表示累积分布函数(cdf)

使用 q 表示分位数

使用 r 生成随机数

名称的其余部分表示分布。表 8-1 列出了一些常见的统计分布函数。

表 8-1:常见的 R 统计分布函数

分布

密度/概率质量函数

累积分布函数

分位数

随机数

正态

dnorm()

pnorm()

qnorm()

rnorm()

卡方

dchisq()

pchisq()

qchisq()

rchisq()

二项式

dbinom()

pbinom()

qbinom()

rbinom()

例如,让我们模拟 1,000 个具有 2 个自由度的卡方变量,并找到它们的平均值。

mean(rchisq(1000,df=2))

[1] 1.938179

rchisq 中的 r 表示我们希望生成随机数——

在这种情况下,来自卡方分布。如本例所示,r 系列函数中的第一个参数是要生成的随机变量的数量。

在 R 中进行数学和模拟

193

www.it-ebooks.info

这些函数也有针对给定分布族特定的参数。在我们的例子中,我们使用 df 参数来表示卡方分布的自由度数。

注意

有关统计分布函数参数的详细信息,请咨询 R 的在线帮助。例如,要了解更多关于分位数卡方函数的信息,请在命令提示符中输入 ?qchisq。

让我们也计算具有两个自由度的卡方分布的 95% 分位数:

qchisq(0.95,2)

[1] 5.991465

在这里,我们使用 q 来表示分位数——在这种情况下,0.95 分位数或 95% 分位数。

d, p, 和 q 系列中的第一个参数实际上是一个向量,这样我们就可以在多个点上评估密度/概率质量函数、累积分布函数或分位数函数。

让我们找到具有 2 个自由度的卡方分布的 50% 和 95% 分位数。

qchisq(c(0.5,0.95),df=2)

[1] 1.386294 5.991465

8.3 排序

普通数值排序可以使用 sort() 函数完成,如下例所示:

x <- c(13,5,12,5)

sort(x)

[1] 5 5 12 13

x

[1] 13 5 12 5

注意,x 本身没有改变,这符合 R 的函数式语言哲学。

如果您想要原始向量中排序值的索引,请使用 order() 函数。以下是一个示例:

order(x)

[1] 2 4 3 1

这意味着 x[2] 是 x 中最小的值,x[4] 是第二小的,x[3] 是第三小的,依此类推。

194

第八章

www.it-ebooks.info

您可以使用 order() 与索引一起对数据框进行排序,如下所示:

y

V1 V2

1 def 2

2

ab 5

3 zzzz 1

r <- order(y$V2)

r

[1] 3 1 2

z <- y[r,]

z

V1 V2

3 zzzz 1

1 def 2

2

ab 5

这里发生了什么?我们对 y 的第二列调用了 order(),

得到一个向量 r,告诉我们如果我们要排序这些数字,它们应该放在哪里。这个向量中的 3 告诉我们 x[3,2] 是 x[,2] 中最小的数字;1 告诉我们 x[1,2] 是第二小的;2 告诉我们 x[2,2]

是第三小的。然后我们使用索引来生成按列 2 排序的框架,将其存储在 z 中。

您可以使用 order() 根据字符变量以及数值变量进行排序,如下所示:

d

孩子的年龄

1 Jack

12

2 Jill

10

3 Billy

13

d[order(d$kids),]

孩子的年龄

3 Billy

13

1 Jack

12

2 Jill

10

d[order(d$ages),]

孩子的年龄

2 Jill

10

1 Jack

12

3 Billy

13

与此相关的一个函数是 rank(),它报告向量的每个元素的秩。

在 R 中进行数学和模拟

195

www.it-ebooks.info

x <- c(13,5,12,5)

rank(x)

[1] 4.0 1.5 3.0 1.5

这意味着在 x 中 13 的秩是 4;也就是说,它是第四小的。值 5 在 x 中出现了两次,这两个是第一和第二小的,所以秩 1.5 被分配给这两个。可选地,可以指定其他处理平局的方法。

8.4 向量和矩阵的线性代数运算

将向量乘以标量可以直接进行,就像你之前看到的。这里还有一个例子:

y

[1] 1 3 4 10

2*y

[1] 2 6 8 20

如果你想计算两个向量的内积(或点积),使用 crossprod(),如下所示:

crossprod(1:3,c(5,12,13))

[,1]

[1,]

68

函数计算了 1 · 5 + 2 · 12 + 3 · 13 = 68。

注意 crossprod()函数的名称是一个误称,因为这个函数并不计算向量的叉积。我们将在第 8.4.1 节开发一个计算实数叉积的函数。

对于数学意义上的矩阵乘法,应使用%%运算符,而不是。例如,这里我们计算矩阵乘积:

1 2

1 1

1 1

3 4

0

1

=

3 1

这里是代码:

a

[,1] [,2]

[1,]

1

2

[2,]

3

4

b

[,1] [,2]

[1,]

1

-1

[2,]

0

1

196

第八章

www.it-ebooks.info

a %*% b

[,1] [,2]

[1,]

1

1

[2,]

3

1

函数 solve()将解决线性方程组,甚至找到矩阵的逆。例如,让我们解这个系统:

x 1 + x 2 = 2

−x 1 + x 2 = 4

它的矩阵形式如下:

1

1

x 1

2

1 1

=

x 2

4

这里是代码:

a <- matrix(c(1,1,-1,1),nrow=2,ncol=2)

b <- c(2,4)

solve(a,b)

[1] 3 1

solve(a)

[,1] [,2]

[1,] 0.5 0.5

[2,] -0.5 0.5

在第二次调用 solve()时,缺少第二个参数表示我们只想计算矩阵的逆。

这里有一些其他的线性代数函数:

t(): 矩阵转置

qr(): QR 分解

chol(): Cholesky 分解

det(): 行列式

eigen(): 特征值/特征向量

diag(): 提取方阵的对角线(用于从协方差矩阵中获取方差和构建对角矩阵)。

矩阵)。

sweep(): 数值分析扫掠操作

注意 diag()函数的通用性:如果其参数是一个矩阵,它返回一个向量,反之亦然。此外,如果参数是标量,该函数返回指定大小的单位矩阵。

在 R 中进行数学和模拟

197

www.it-ebooks.info

m

[,1] [,2]

[1,]

1

2

[2,]

7

8

dm <- diag(m)

dm

[1] 1 8

diag(dm)

[,1] [,2]

[1,]

1

0

[2,]

0

8

diag(3)

[,1] [,2] [,3]

[1,]

1

0

0

[2,]

0

1

0

[3,]

0

0

1

sweep()函数可以进行相当复杂的操作。作为一个简单的例子,让我们取一个 3x3 矩阵,并将 1 加到第 1 行,4 加到第 2 行,7 加到第 3 行。

m

[,1] [,2] [,3]

[1,]

1

2

3

[2,]

4

5

6

[3,]

7

8

9

sweep(m,1,c(1,4,7),"+")

[,1] [,2] [,3]

[1,]

2

3

4

[2,]

8

9

10

[3,]

14

15

16

sweep()的前两个参数类似于 apply()的参数:数组和对齐,在这种情况下对齐为 1。第四个参数是要应用的函数,第三个是该函数的参数(到

“+”函数)。

8.4.1 扩展示例:向量叉积

让我们考虑向量叉积的问题。定义非常简单:在三维空间中,向量( x 1 , x 2 , x 3)和( y 1 , y 2 , y 3)的叉积是一个新的三维向量,如方程 8.1 所示。

( x 2 y 3 − x 3 y 2 , −x 1 y 3 + x 3 y 1 , x 1 y 2 − x 2 y 1) (8.1)

198

第八章

www.it-ebooks.info

这可以简洁地表示为行列式顶行的展开,如方程 8.2 所示。

− − −

x 1 x 2 x 3 ⎠

(8.2)

y 1

y 2

y 3

在这里,顶行中的元素仅仅是占位符。

不要担心这段伪数学。重点是叉积向量可以计算为子行列式的和。例如,方程 8.1 中的第一个分量,x 2 y 3 − x 3 y 2,很容易看出是删除方程 8.2 中第一行和第一列得到的子矩阵的行列式,如方程 8.3 所示。

x 2 x 3

(8.3)

y 2

y 3

我们需要计算子行列式——即子矩阵的行列式

子矩阵——与 R 完美匹配,R 擅长指定子矩阵。

这表明可以在适当的子矩阵上调用 det(),如下所示:

xprod <- function(x,y) {

m <- rbind(rep(NA,3),x,y)

xp <- vector(length=3)

for (i in 1:3)

xp[i] <- -(-1)^i * det(m[2:3,-i])

return(xp)

}

注意,即使 R 指定 NA 值的能力也在这里发挥作用,以处理上面提到的“占位符”。

所有这些可能看起来有些过度。毕竟,直接编码方程 8.1 并不困难,无需使用子矩阵和行列式。但是,虽然这在三维情况下可能是正确的,但这里展示的方法在n-元情况下,在n-维空间中非常有成效。那里的叉积被定义为形式如方程 8.1 的n-by- n行列式,因此前面的代码完美地推广了。

8.4.2 扩展示例:寻找马尔可夫链的平稳分布

马尔可夫链是一种随机过程,我们在其中在各种状态之间移动,以“无记忆”的方式,其定义在此处不必关心。状态可以是队列中的作业数量,库存中存储的项目数量,等等。我们将假设状态的数量是有限的。

作为简单的例子,考虑一个游戏,我们反复掷硬币,每次积累三个连续正面时赢得一美元。

在任何时刻,我们的状态i将表示到目前为止连续出现正面的次数,因此我们的状态可以是 0、1 或 2。(当我们连续出现三个正面时,我们的状态将重置为 0。)

在 R 中进行数学和模拟

199

www.it-ebooks.info

在马尔可夫建模中,通常关注的中心问题是长期状态分布,即我们在每个状态中花费的长期时间比例。在我们的掷币游戏中,我们可以使用我们在这里开发的代码来计算这个分布,结果是我们有 57.1%,28.6%,和 14.3%的时间处于状态 0,1 和 2。请注意,如果我们处于状态 2 并且掷出正面,我们将赢得一美元,所以 0.143 × 0.5 = 0.071 的投掷将导致胜利。

由于 R 向量和矩阵索引从 1 开始而不是 0,在这里将我们的状态重新标记为 1,2 和 3 而不是 0,1 和 2 将很方便。例如,状态 3 现在表示我们目前有两个连续的正面。

pij 表示在时间步长中从状态 i 转移到状态 j转移概率。例如,在游戏示例中,p 23 = 0 . 5,反映了有 1/2 的概率我们会掷出正面,从而从连续一个正面转移到两个正面。另一方面,如果我们处于状态 2 时掷出反面,我们将转移到状态 1,这意味着 0 个连续正面;因此 p 21 = 0 . 5\。

我们感兴趣的是计算向量 π = ( π 1 , ..., πs), 其中 πi 是在所有状态 i 中花费的长期时间比例,即状态 i 的长期比例。让 P

表示转移概率矩阵,其 i 行, j 列的元素是 pij。然后可以证明 π 必须满足方程 8.4,π = πP

(8.4)

这与方程 8.5 等价:

( I − P T ) π = 0

(8.5)

这里 I 是单位矩阵,P T 表示 P 的转置。

方程组 8.5 中的任何一个方程都是多余的。因此,我们通过从方程 8.5 中的 I −P 去掉最后一行来消除其中一个方程。这也意味着去掉方程 8.5 右侧 0 向量中的最后一个 0。

但请注意,还有方程 8.6 中显示的约束。

πi = 1

(8.6)

i

用矩阵术语来说,如下所示:

1 Tnπ = 1

其中 1 n 是一个包含 n 个 1 的向量。

因此,在修改后的方程 8.5 版本中,我们用全 1 的行替换被移除的行,在右侧,用 1 替换被移除的 0。然后我们可以求解这个系统。

所有这些都可以使用 R 的 solve()函数计算,如下所示:

1

findpi1 <- function(p) {

2

n <- nrow(p)

3

imp <- diag(n) - t(p)

200

第八章

www.it-ebooks.info

4

imp[n,] <- rep(1,n)

5

rhs <- c(rep(0,n-1),1)

6

pivec <- solve(imp,rhs)

7

return(pivec)

8

}

这里是主要步骤:

1.

在第 3 行计算 I − P T。注意再次,当 diag()用标量参数调用时,返回由该参数给定大小的单位矩阵。

2.

在第 4 行将 P 的最后一行替换为 1 值。

3.

在第 5 行设置右侧向量。

4.

在第 6 行求解 π

另一种方法,基于更高级的知识,是基于特征-

values. 注意从方程式 8.4 中,πP 的特征值为 1 的左特征向量。这表明可以使用 R 的 eigen() 函数,选择与该特征值对应的特征向量。(来自数学的一个结果,Perron-Frobenius 定理可以用来仔细证明这一点。)

由于 π 是一个左特征向量,调用 eigen() 时必须使用 P 的转置而不是 P。此外,由于特征向量仅在上标乘法下是唯一的,我们必须处理 eigen() 返回给我们的特征向量涉及的两个问题:

它可能包含负分量。如果是这样,我们乘以 1\。

它可能不满足方程式 8.6。我们通过除以返回向量的长度来解决这个问题。

这里是代码:

1

findpi2 <- function(p) {

2

n <- nrow(p)

3

查找 P 转置的第一个特征向量

4

pivec <- eigen(t(p))$vectors[,1]

5

保证是真实的,但可能是负数

6

if (pivec[1] < 0) pivec <- -pivec

7

归一化,使总和为 1

8

pivec <- pivec / sum(pivec)

9

return(pivec)

10

}

eigen() 的返回值是一个列表。列表的一个组件是一个名为 vectors 的矩阵。这些是特征向量,其中第 i 列是与第 i 个特征值对应的特征向量。因此,我们在这里取第 1 列。

在 R 中进行数学和模拟

201

www.it-ebooks.info

8.5 集合操作

R 包含一些方便的集合操作,包括这些:

union(x,y): 集合 x 和 y 的并集

intersect(x,y): 集合 x 和 y 的交集

setdiff(x,y): 集合 x 和 y 的差集,由 x 中不在 y 中的所有元素组成

setequal(x,y): 测试 x 和 y 是否相等

c %in% y: 检查 c 是否是集合 y 的元素

choose(n,k): 从大小为 n 的集合中选择大小为 k 的可能子集的数量

这里是使用这些函数的一些简单示例:

x <- c(1,2,5)

y <- c(5,1,8,9)

union(x,y)

[1] 1 2 5 8 9

intersect(x,y)

[1] 1 5

setdiff(x,y)

[1] 2

setdiff(y,x)

[1] 8 9

setequal(x,y)

[1] FALSE

setequal(x,c(1,2,5))

[1] TRUE

2 %in% x

[1] TRUE

2 %in% y

[1] FALSE

choose(5,2)

[1] 10

回想第 7.12 节,你可以编写自己的二元操作。

例如,考虑编码两个集合的对称差集—

即,恰好属于两个操作数集合之一的所有元素。

因为集合 x 和 y 的对称差集正好由 x 中的元素组成,这些元素不在 y 中,反之亦然,所以代码由对 setdiff() 和 union() 的简单调用组成,如下所示:

symdiff

function(a,b) {

sdfxy <- setdiff(x,y)

sdfyx <- setdiff(y,x)

202

第八章

www.it-ebooks.info

return(union(sdfxy,sdfyx))

}

让我们试一试。

x

[1] 1 2 5

y

[1] 5 1 8 9

symdiff(x,y)

[1] 2 8 9

这里有一个另一个例子:一个二元操作符,用于确定集合 u 是否是另一个集合 v 的子集。一点思考表明,这个属性等价于 u 和 v 的交集等于 u。因此,我们又有另一个容易编码的函数:

"%subsetof%" <- function(u,v) {

return(setequal(intersect(u,v),u))

  • }

c(3,8) %subsetof% 1:10

[1] TRUE

c(3,8) %subsetof% 5:10

[1] FALSE

combn() 函数生成组合。让我们找到 {1,2,3} 的子集。

{1,2,3} 的大小为 2.

c32 <- combn(1:3,2)

c32

[,1] [,2] [,3]

[1,]

1

1

2

[2,]

2

3

3

class(c32)

[1] "matrix"

结果在输出列中。我们看到 {1,2,3} 的子集有 (1,2),(1,3),(2,3),(1,2,3)。

大小为 2 的 {1,2,3} 的子集是 (1,2),(1,3) 和 (2,3)。

该函数还允许您指定一个由 combn() 在每个组合上调用的函数。例如,我们可以找到每个子集的数字之和,如下所示:

combn(1:3,2,sum)

[1] 3 4 5

第一个子集 {1,2} 的和为 2,以此类推。

在 R 中进行数学和模拟

203

www.it-ebooks.info

8.6 R 中的模拟编程

R 最常见的用途之一是模拟。让我们看看 R 为此应用提供了哪些工具。

8.6.1 内置随机变量生成器

如前所述,R 有函数可以生成来自多种分布的变量。例如,rbinom() 生成二项分布或伯努利随机变量。1

假设我们想要找到在五次抛硬币中至少得到四个正面的概率(虽然可以通过解析方法找到,但这是一个方便的例子)。

我们可以这样做到:

x <- rbinom(100000,5,0.5)

mean(x >= 4)

[1] 0.18829

首先,我们从具有五次试验和成功概率为 0.5 的二项分布中生成 100,000 个变量。然后我们确定其中哪些具有值 4 或 5,结果是一个与 x 长度相同的布尔向量。TRUE

在该向量中的 TRUE 和 FALSE 值由 mean() 处理为 1 和 0,从而得到我们的估计概率(因为一串 1 和 0 的平均值是 1 的比例)。

其他函数包括 rnorm() 用于正态分布,rexp() 用于指数分布,runif() 用于均匀分布,rgamma() 用于伽马分布,rpois() 用于泊松分布等等。

这里有一个简单的例子,它找到 E[max( X, Y )],独立 N(0,1) 随机变量 X 和 Y 的最大值的期望值:

sum <- 0

nreps <- 100000

for (i in 1:nreps) {

xy <- rnorm(2) # 生成 2 个 N(0,1)

sum <- sum + max(xy)

}

print(sum/nreps)

我们生成了 100,000 对,找到每一对的最大值,并平均这些最大值以获得我们的估计期望值。

将这些最大值平均以获得我们的估计期望值。

之前的代码,使用显式循环可能更清晰,但正如之前所说,如果我们愿意使用更多内存,我们可以更紧凑地完成这项工作。

1 一系列独立的 0-和 1-值随机变量,具有相同的 1 概率

对于每个称为 伯努利

204

第八章

www.it-ebooks.info

emax

function(nreps) {

x <- rnorm(2*nreps)

maxxy <- pmax(x[1:nreps],x[(nreps+1):(2*nreps)])

return(mean(maxxy))

}

在这里,我们生成了双倍的 nreps 值。第一个 nreps 值模拟 X,剩余的 nreps 值代表 Y。pmax()调用然后计算所需的成对最大值。再次注意这里 max()和 pmax()之间的对比,后者产生成对最大值。

8.6.2 在重复运行中获得相同的随机流

根据 R 文档,所有随机数生成器都使用

32 位整数用于种子值。因此,除了舍入误差外,相同的初始种子应该生成相同的数字流。

默认情况下,R 将在程序的每次运行中生成不同的随机数流。如果您希望每次都生成相同的流——例如在调试中很重要——请调用 set.seed(),如下所示:

set.seed(8888) # 或者使用你喜欢的数字作为参数

8.6.3 扩展示例:组合模拟

考虑以下概率问题:

从 20 个人中选择 3 个、4 个和 5 个规模的委员会。

A 和 B 被选中的概率是多少?

同一委员会?

这个问题从理论上解决并不难,但我们可能希望使用模拟来检查我们的解决方案,并且无论如何,编写代码将展示 R 的集合操作如何在组合设置中派上用场。

这里是代码:

1

sim <- function(nreps) {

2

commdata <- list() # 将存储关于 3 个委员会的所有信息 3

commdata$countabsamecomm <- 0

4

for (rep in 1:nreps) {

5

commdata$whosleft <- 1:20 # 剩余可供选择的人

6

commdata$numabchosen <- 0 # 到目前为止 A、B 中被选中的数量

7

选择委员会 1,并检查 A、B 是否共同服务

8

commdata <- choosecomm(commdata,5)

在 R 中进行数学和模拟

205

www.it-ebooks.info

9

如果 A 或 B 已经被选中,则不需要查看其他委员会。

10

if (commdata$numabchosen > 0) next

11

选择委员会 2 并检查

12

commdata <- choosecomm(commdata,4)

13

if (commdata$numabchosen > 0) next

14

选择委员会 3 并检查

15

commdata <- choosecomm(commdata,3)

16

}

17

print(commdata$countabsamecomm/nreps)

18

}

19

20

choosecomm <- function(comdat,comsize) {

21

选择委员会

22

委员会 <- sample(comdat$whosleft,comsize)

23

计算选择 A 和 B 的数量

24

comdat$numabchosen <- length(intersect(1:2,committee))

25

if (comdat$numabchosen == 2)

26

comdat\(countabsamecomm <- comdat\)countabsamecomm + 1

27

从我们现在需要从中选择的人的集合中删除已选择的委员会 28

comdat\(whosleft <- setdiff(comdat\)whosleft,committee)

29

return(comdat)

30

}

我们将潜在的委员会成员编号为 1 到 20,每个

具有 ID 1 和 2 的儿子 A 和 B。回忆一下,R 列表通常用于将多个相关变量存储在一个篮子里,我们设置了 comdat 列表。其组件包括以下内容:

comdat$whosleft:我们通过从这个向量中随机选择来模拟委员会的随机选择。每次我们选择一个委员会,我们就移除委员会成员的 ID。它初始化为 1:20,表示还没有人被选中。

comdat$numabchosen:这是 A 和 B 中至今已选择的人数。如果我们选择一个委员会并发现这个数字是正数,我们可以跳过选择剩余的委员会,以下原因:如果这个数字是 2,我们知道 A 和 B 肯定在同一委员会上;如果是 1,我们知道 A 和 B 肯定不在同一委员会上。

comdat$countabsamecomm:在这里,我们存储 A 和 B 在同一委员会上出现的次数。

由于委员会选择涉及子集,因此 R 的几个集合操作——intersect()和 setdiff()——在这里非常有用。

注意,R 的下一个语句的使用,它告诉 R 跳过循环的这一迭代剩余部分。

206

第八章

www.it-ebooks.info

图像 21

9

面向对象编程

许多程序员认为面向对象编程(OOP)使得

面向对象编程(OOP)使得

更清晰、更可重用的代码。尽管非常

与熟悉的 OOP 语言(如

C++、Java 和 Python,R 在表现上非常面向对象。

以下主题是 R 的关键:

在 R 中,您接触到的每一件事——从数字到字符字符串到矩阵——都是一个对象。

R 促进封装,这意味着将不同的但相关的数据项打包到一个类实例中。封装有助于您跟踪相关变量,提高清晰度。

R 类是多态的,这意味着相同的函数调用会导致不同类别的对象执行不同的操作。例如,对某个类对象的 print()调用将触发对该类定制的 print 函数的调用。多态性促进了重用性。

R 允许继承,这允许将给定的类扩展到更专业的类。

www.it-ebooks.info

本章介绍了 R 中的面向对象编程(OOP)。我们将讨论两种类型的类编程,S3 和 S4,然后介绍一些有用的与 OOP 相关的 R

工具。

9.1 S3 类

原始 R 结构中的类,称为 S3,仍然是 R 使用中的主导类范式。事实上,R 的大部分内置类都是 S3 类型。

S3 类由一个列表组成,其中添加了类名属性和调度能力。后者使得可以使用泛型函数,正如我们在第一章中看到的。S4 类是在后来开发的,目的是增加安全性,这意味着您不能意外地访问一个尚未存在的类组件。

9.1.1 S3 泛型函数

如前所述,R 是多态的,也就是说,同一个函数可以为不同的类执行不同的操作。例如,你可以将 plot()应用于许多不同类型的对象,并为每个对象得到不同类型的图形。print()、summary()和其他许多函数也是如此。

以这种方式,我们得到了对不同类的一致接口。例如,如果你正在编写包含绘图操作的代码,多态性可能允许你编写程序时无需担心可能被绘制的各种对象类型。

此外,多态性确实使事情更容易记忆和操作。

为用户提供了一个有趣且方便的方式来探索新的库函数及其相关类。如果你对某个函数不熟悉,只需尝试在该函数的输出上运行 plot();它很可能会工作。从程序员的角度来看,多态性允许编写相当通用的代码,无需担心正在操作的对象类型,因为底层类机制会处理这些。

与多态性一起工作的函数,如 plot()和 print(),被称为通用函数。当调用通用函数时,R 会将调用分配给适当的类方法,这意味着它会将调用重定向到为对象类定义的函数。

9.1.2 示例:lm()线性模型函数中的面向对象编程

作为例子,让我们看看通过 R 的 lm()函数运行的一个简单回归分析。首先,让我们看看 lm()做了什么:

?lm

这个帮助查询的输出将告诉你,除了其他信息之外,这个函数返回一个"lm"类的对象。

208

第九章

www.it-ebooks.info

让我们尝试创建这个对象的实例,然后打印它:

x <- c(1,2,3)

y <- c(1,3,8)

lmout <- lm(y ~ x)

class(lmout)

[1] "lm"

lmout

调用:

lm(formula = y ~ x)

系数:

(截距)

x

-3.0

3.5

在这里,我们打印出了 lmout 对象。(记住,在交互模式下,只需简单地输入一个对象的名字,就会打印出该对象。)然后 R 解释器看到 lmout 是一个"lm"类的对象,因此调用了 print.lm(),这是"lm"类的一个特殊打印方法。在 R 术语中,对通用函数 print()的调用被分配给了与"lm"类关联的方法 print.lm()。

让我们看看通用函数和类方法:

在这种情况下:

print

function(x, ...) UseMethod("print")

<环境:基础命名空间>

print.lm

function (x, digits = max(3, getOption("digits") - 3), ...)

{

cat("\n 调用:\n", deparse(x$call), "\n\n", sep = "") if (length(coef(x))) {

cat("系数:\n")

print.default(format(coef(x), digits = digits), print.gap = 2,

quote = FALSE)

}

else cat("没有系数\n")

cat("\n")

invisible(x)

}

<环境:命名空间:stats>

你可能会惊讶地看到 print()仅仅是一个对 UseMethod()的调用。但实际上这是一个分发函数,所以考虑到 print()作为通用函数的角色,你最终不应该感到惊讶。

面向对象编程

209

www.it-ebooks.info

不要担心 print.lm()的细节。主要点是打印依赖于上下文,并调用一个特殊的打印函数。

"lm"类。现在让我们看看当我们移除该对象的类属性时打印这个对象会发生什么:

unclass(lmout)

$coefficients

(Intercept)

x

-3.0

3.5

$residuals

1

2

3

0.5 -1.0 0.5

$effects

(Intercept)

x

-6.928203

-4.949747

1.224745

$rank

[1] 2

...

我在这里只展示了前几行——还有很多。(试着在自己的机器上运行一下!)但你可以看到,lm()的作者决定使 print.lm()更加简洁,仅打印几个关键量。

9.1.3 寻找泛型方法的实现

您可以通过调用 methods()来找到给定泛型方法的所有实现,如下所示:

methods(print)

[1] print.acf*

[2] print.anova

[3] print.aov*

[4] print.aovlist*

[5] print.ar*

[6] print.Arima*

[7] print.arima0*

[8] print.AsIs

[9] print.aspell*

[10] print.Bibtex*

[11] print.browseVignettes*

[12] print.by

[13] print.check_code_usage_in_package*

[14] print.check_demo_index*

[15] print.checkDocFiles*

210

第九章

www.it-ebooks.info

[16] print.checkDocStyle*

[17] print.check_dotInternal*

[18] print.checkFF*

[19] print.check_make_vars*

[20] print.check_package_code_syntax*

...

星号表示不可见函数,意味着它们不在默认命名空间中。您可以通过 getAnywhere()找到这些函数,然后通过使用命名空间限定符来访问它们。例如,print.aspell()。

aspell()函数本身对其参数中指定的文件进行拼写检查。例如,假设文件wrds包含以下行:哪个单词拼写错误?

在这种情况下,该函数将捕获拼写错误的单词,如下所示:aspell("wrds")

拼写错误

wrds:1:15

输出表明输入文件的第 1 行第 15 个字符存在拼写错误。但这里我们关心的是该输出是如何打印出来的机制。

aspell()函数返回一个类为"aspell"的对象,该对象确实有自己的泛型打印函数,即 print.aspell()。实际上,该函数在我们的示例中在调用 aspell()之后被调用,并且返回值被打印出来。当时,R 在类"aspell"的对象上调用 UseMethod()。

但如果我们直接调用该打印方法,R 将无法识别它:

aspout <- aspell("wrds")

print.aspell(aspout)

错误:找不到函数"print.aspell"

然而,我们可以通过调用 getAnywhere()来找到它:

getAnywhere(print.aspell)

找到一个匹配'print.aspell'的单个对象

它在以下位置被发现

命名空间 utils 中的注册 S3 方法 print

命名空间:utils

with value

function (x, sort = TRUE, verbose = FALSE, indent = 2L, ...)

{

if (!(nr <- nrow(x)))

...

面向对象编程

211

www.it-ebooks.info

因此,该函数位于 utils 命名空间中,我们可以通过添加这样的限定符来执行它:

utils:::print.aspell(aspout)

mispelled

wrds:1:15

这样就可以看到所有通用方法:

methods(class="default")

...

9.1.4 编写 S3 类

S3 类有一个相当拼凑的结构。一个类实例是通过形成一个列表来创建的,列表的组件是类的成员变量。 (了解 Perl 的读者可能会在 Perl 自己的 OOP 系统中认识到这种临时性质。) "class"属性是通过使用 attr()或 class()函数手动设置的,然后定义了各种通用函数的实现。我们可以通过检查 lm()函数来看到这一点:

lm

...

z <- list(coefficients = if (is.matrix(y))

matrix(,0,3) else numeric(0L), residuals = y,

fitted.values = 0 * y, weights = w, rank = 0L,

df.residual = if (is.matrix(y)) nrow(y) else length(y))

}

...

class(z) <- c(if(is.matrix(y)) "mlm", "lm")

...

再次,不必在意细节;基本过程是存在的。创建了一个列表并将其分配给 z,它将作为"lm"类实例的框架(并且最终将是函数返回的值)。

列表中的某些组件,如 residuals,在列表创建时就已经分配了。此外,类属性被设置为"lm"(以及可能在下一节中解释的"mlm")。

作为如何编写 S3 类的一个例子,让我们转向一个更简单的东西。继续我们 4.1 节中的员工示例,我们可以编写如下:

j <- list(name="Joe", salary=55000, union=T)

class(j) <- "employee"

attributes(j) # let's check

212

第九章

www.it-ebooks.info

$names

[1] "name" "salary" "union"

$class

[1] "employee"

在我们为这个类编写打印方法之前,让我们看看当我们调用默认的 print()时会发生什么:

j

$name

[1] "Joe"

$salary

[1] 55000

$union

[1] TRUE

attr(,"class")

[1] "employee"

实质上,j 在打印目的上被当作一个列表处理。

现在让我们编写自己的打印方法:

print.employee <- function(wrkr) {

cat(wrkr$name,"\n")

cat("salary",wrkr$salary,"\n")

cat("union member",wrkr$union,"\n")

}

因此,现在对"employee"类对象的 print()调用应该被重定向到 print.employee()。我们可以通过正式检查来验证这一点:

methods(,"employee")

[1] print.employee

或者,当然,我们也可以简单地尝试一下:

j

Joe

salary 55000

union member TRUE

面向对象编程

213

www.it-ebooks.info

9.1.5 使用继承

继承的想法是通过形成旧类的特殊版本来形成新类。例如,在我们之前的员工示例中,我们可以形成一个新类,专门用于小时工,即"hrlyemployee",作为"employee"的子类,如下所示:

k <- list(name="Kate", salary= 68000, union=F, hrsthismonth= 2) class(k) <- c("hrlyemployee","employee")

我们的新类有一个额外的变量:hrsthismonth。新类的名称由两个字符字符串组成,代表新类和旧类。我们的新类继承了旧类的所有方法。例如,print.employee()仍然适用于新类:

k

Kate

salary 68000

联合成员 FALSE

考虑到继承的目标,这并不奇怪。然而,理解这里发生了什么非常重要。

再次,简单地输入 k 导致调用 print(k)。反过来,这导致 UseMethod()在 k 的两个类名中的第一个上搜索 print 方法,即"hrlyemployee"。该搜索失败,所以 UseMethod()尝试另一个类名"employee",并找到了 print.employee()。它执行了后者。

回想一下,在检查"lm"的代码时,你看到了这一行:class(z) <- c(if(is.matrix(y)) "mlm", "lm")

你现在可以看到"mlm"是"lm"的子类,用于向量值响应变量。

9.1.6 扩展示例:存储上三角矩阵的类

现在是时候写一个更复杂的例子了,我们将编写一个 R 类

"ut"代表上三角矩阵。这些是下三角元素为零的方阵,如方程 9.1 所示。

1 5 12

⎝0 6 9 ⎠

(9.1)

0 0 2

我们在这里的动机是通过只存储矩阵的非零部分来节省存储空间(尽管这会稍微增加一些访问时间)。

注意

The R class "dist" also uses such storage, though in a more focused context and without the class functions we have here.

214

第九章

www.it-ebooks.info

该类的组件 mat 将存储矩阵。如前所述,为了节省存储空间,只存储对角线和上三角元素,按列主序存储。例如,矩阵(9.1)的存储由向量(1,5,6,12,9,2)组成,而组件 mat 具有该值。

我们将在该类中包含一个组件 ix,以显示在 mat 中各种列的起始位置。对于前面的情况,ix 是 c(1,2,4),这意味着第 1 列从 mat[1]开始,第 2 列从 mat[2]开始,第 3 列从 mat[4]开始。这允许方便地访问矩阵的各个元素或列。

以下是我们类的代码。

1

class "ut",上三角矩阵的紧凑存储 2

3

效用函数,返回 1+...+i

4

sum1toi <- function(i) return(i*(i+1)/2)

5

6

从完整的矩阵 inmat(包括 0)创建一个类"ut"的对象 7

ut <- function(inmat) {

8

n <- nrow(inmat)

9

rtrn <- list() # 开始构建对象

10

class(rtrn) <- "ut"

11

rtrn$mat <- vector(length=sum1toi(n))

12

rtrn$ix <- sum1toi(0:(n-1)) + 1

13

for (i in 1:n) {

14

存储第 i 列

15

ixi <- rtrn$ix[i]

16

rtrn$mat[ixi:(ixi+i-1)] <- inmat[1:i,i]

17

}

18

return(rtrn)

19

}

20

21

uncompress utmat to a full matrix

22

expandut <- function(utmat) {

23

n <- length(utmat$ix) # 矩阵的行数和列数

24

fullmat <- matrix(nrow=n,ncol=n)

25

for (j in 1:n) {

26

填充第 j 列

27

start <- utmat$ix[j]

28

fin <- start + j - 1

29

abovediagj <- utmat$mat[start:fin] # 上三角部分为列 j

30

fullmat[,j] <- c(abovediagj,rep(0,n-j))

31

}

32

返回(fullmat)

33

}

34

35

打印矩阵

36

print.ut <- function(utmat)

37

打印(expandut(utmat))

面向对象编程

215

www.it-ebooks.info

38

39

将一个 ut 矩阵乘以另一个,返回另一个 ut 实例;40

将其实现为二进制运算

41

"%mut%" <- function(utmat1,utmat2) {

42

n <- length(utmat1$ix) # 矩阵的行数和列数

43

utprod <- ut(matrix(0,nrow=n,ncol=n))

44

for (i in 1:n) { # 计算乘积的列 i

45

令 a[j] 和 bj 分别表示 utmat1 和 utmat2 的列 j,46

因此,例如,b2[1] 表示 utmat2 的第 2 列的第 1 个元素

47

因此,乘积的列 i 等于

48

bi[1]a[1] + ... + bi[i]a[i]

49

在 utmat2 中找到列 i 的起始索引

50

startbi <- utmat2$ix[i]

51

初始化一个向量,该向量将成为 bi[1]a[1] + ... + bi[i]a[i]

52

prodcoli <- rep(0,i)

53

for (j in 1:i) { # 找到 bi[j]*a[j],添加到 prodcoli

54

startaj <- utmat1$ix[j]

55

bielement <- utmat2$mat[startbi+j-1]

56

prodcoli[1:j] <- prodcoli[1:j] +

57

bielement * utmat1$mat[startaj:(startaj+j-1)]

58

}

59

现在需要添加下方的 0

60

startprodcoli <- sum1toi(i-1)+1

61

utprod$mat[startbi:(startbi+i-1)] <- prodcoli

62

}

63

返回(utprod)

64

}

让我们测试它。

测试

function() {

utm1 <- ut(rbind(1:2,c(0,2)))

utm2 <- ut(rbind(3:2,c(0,1)))

utp <- utm1 %mut% utm2

打印(utm1)

打印(utm2)

打印(utp)

utm1 <- ut(rbind(1:3,0:2,c(0,0,5)))

utm2 <- ut(rbind(4:2,0:2,c(0,0,1)))

utp <- utm1 %mut% utm2

打印(utm1)

打印(utm2)

打印(utp)

}

216

第九章

www.it-ebooks.info

测试()

[,1] [,2]

[1,] 1 2

[2,] 0 2

[,1] [,2]

[1,] 3 2

[2,] 0 1

[,1] [,2]

[1,] 3 4

[2,] 0 2

[,1] [,2] [,3]

[1,] 1 2 3

[2,] 0 1 2

[3,] 0 0 5

[,1] [,2] [,3]

[1,] 4 3 2

[2,] 0 1 2

[3,] 0 0 1

[,1] [,2]

[1,] 4 5 9

[2,] 0 1 4

[3,] 0 0 5

在整个代码中,我们考虑到涉及的矩阵有很多零。例如,我们通过在包含 0 因子的项中不添加项到和中来避免乘以零。

ut() 函数相当直接。这是一个 构造函数,其任务是创建给定类的实例,最终返回该实例。因此,在第 9 行,我们创建了一个列表,它将作为类对象的主体,命名为 rtrn 以提醒这将是要构建和返回的类实例。

如前所述,我们类的主要成员变量将是 mat 和 idx,它们作为列表的组件实现。这两个组件的内存分配在第 11 和 12 行。

随后的循环将按列填充 rtrn$mat

按元素分配 rtrn$idx 元素。为此循环的一个更简洁的方法是使用相对晦涩的 row()和 col()函数。row()函数接受一个矩阵输入并返回一个大小相同的新矩阵,但每个元素都被其行号替换。以下是一个示例:

m

[,1] [,2]

[1,] 1 4

[2,] 2 5

[3,] 3 6

面向对象编程

217

www.it-ebooks.info

row(m)

[,1] [,2]

[1,] 1 1

[2,] 2 2

[3,] 3 3

col()函数的工作方式类似。

使用这个想法,我们可以用一行代码替换 ut()中的 for 循环:rtrn$mat <- inmat[row(inmat) <= col(inmat)]

在可能的情况下,我们应该利用向量化。看看

例如,第 12 行:

rtrn$ix <- sum1toi(0:(n-1)) + 1

由于 sum1toi()(我们在第 4 行定义)仅基于向量化的函数"*()"和"+()",因此 sum1toi()本身也是向量化的。这允许我们将 sum1toi()应用于上面的向量。请注意,我们同样使用了循环利用。

我们希望"ut"类包含一些方法,而不仅仅是变量。为此,我们包含了三个方法:

expandut()函数将压缩矩阵转换为普通矩阵。在 expandut()中,关键行是 27 和 28,我们使用 rtrn\(ix 来确定矩阵的第 j 列在 utmat\)mat 中的存储位置。

那些数据随后被复制到第 30 行中的 fullmat 的第 j 列。注意使用 rep()生成该列下部的零。

print.ut()函数用于打印。这个函数快速简单,使用了 expandut()。回想一下,对类型为"ut"的对象的任何 print()调用

将被转发到 print.ut(),就像我们之前的测试用例中那样。

"%mut%"()函数用于乘以两个压缩矩阵(不进行解压缩)。此函数从第 39 行开始。由于这是一个二元操作,我们利用 R 支持用户定义的二元操作,如第 7.12 节所述,并将我们的矩阵乘法函数实现为%mut%。

让我们看看"%mut%"()函数的细节。首先,在第 43 行,我们为乘积矩阵分配空间。注意在非典型情况下使用循环利用。matrix()的第一个参数需要是一个长度与指定行数和列数兼容的向量,所以我们提供的 0 被循环利用成一个长度为n 2 的向量。当然,可以使用 rep()代替,但利用循环利用可以使代码更短、更优雅。

为了清晰和快速执行,这里的代码是围绕 R 以列主序存储矩阵的事实编写的。如注释中所述,我们的代码利用了这一点,即218

第九章

www.it-ebooks.info

乘积可以表示为第一个因子列的线性组合。查看这个属性的特定示例将有所帮助,如方程 9.2 所示。

⎞ ⎛

1 2 3

4 3 2

4 5 9

⎝ 0 1 2 ⎠ ⎝ 0 1 2 ⎠ = ⎝ 0 1 4 ⎠

(9.2)

0 0 5

0 0 1

0 0 5

注释说明,例如,乘积的第三列等于以下内容:

1

2

3

2 ⎝ 0 ⎠ + 2 ⎝ 1 ⎠ + 1 ⎝ 2 ⎠

0

0

5

检查方程 9.2 确认了该关系。

将乘法问题用列的形式表达

两个输入矩阵使我们能够压缩代码并可能提高速度。后者再次源于向量化,这是在第十四章中详细讨论的好处。这种方法从第 53 行开始的循环中使用。(可以说,在这种情况下,速度的提高是以代码的可读性为代价的。)

9.1.7 扩展示例:多项式回归的步骤

作为另一个例子,考虑一个只有一个预测变量的统计回归设置。由于任何统计模型本质上只是一个近似,原则上,你可以通过拟合更高次的多项式来获得更好的模型。然而,在某个点上,这会变成过度拟合,以至于对于高于某个值的度数,对新、未来数据的预测实际上会恶化。

"polyreg" 类旨在解决这个问题。它拟合各种次数的多项式,但通过交叉验证来评估拟合,以减少过度拟合的风险。在这种形式的交叉验证中,称为 留一法,对于每个点,我们将回归拟合到所有数据 除了 这个观测值,然后从拟合中预测这个观测值。这个类的对象由各种回归模型的输出以及原始数据组成。

以下是为 "polyreg" 类的代码。

1

"polyreg",一个预测变量中的多项式回归的 S3 类 2

3

polyfit(y,x,maxdeg) 拟合所有最高次数为 maxdeg 的多项式;y 是 4

响应变量的向量,x 为预测变量;创建一个包含 5 个输出的对象

class "polyreg"

6

polyfit <- function(y,x,maxdeg) {

7

形成预测变量的幂,第 i 个幂在第 i 列

8

pwrs <- powers(x,maxdeg) # 可使用正交多项式以获得更高的精度 9

lmout <- list() # 开始构建类

10

class(lmout) <- "polyreg" # 创建一个新的类

面向对象编程

219

www.it-ebooks.info

11

for (i in 1:maxdeg) {

12

lmo <- lm(y ~ pwrs[,1:i])

13

在这里扩展 lm 类,带有交叉验证的预测

14

lmo$fitted.cvvalues <- lvoneout(y,pwrs[,1:i,drop=F])

15

lmout[[i]] <- lmo

16

}

17

lmout$x <- x

18

lmout$y <- y

19

return(lmout)

20

}

21

22

print() 对于类 "polyreg" 的对象 fits:打印

23

交叉验证均方预测误差

24

print.polyreg <- function(fits) {

25

maxdeg <- length(fits) - 2

26

n <- length(fits$y)

27

tbl <- matrix(nrow=maxdeg,ncol=1)

28

colnames(tbl) <- "MSPE"

29

for (i in 1:maxdeg) {

30

fi <- fits[[i]]

31

errs <- fits\(y - fi\)fitted.cvvalues

32

spe <- crossprod(errs,errs) # 预测误差的平方和

33

tbl[i,1] <- spe/n

34

}

35

cat("均方预测误差,按度数\n")

36

print(tbl)

37

}

38

39

形成向量 x 的幂矩阵,通过度 dg

40

powers <- function(x,dg) {

41

pw <- matrix(x,nrow=length(x))

42

prod <- x

43

for (i in 2:dg) {

44

prod <- prod * x

45

pw <- cbind(pw,prod)

46

}

47

return(pw)

48

}

49

50

finds cross-validated predicted values; could be made much faster via 51

matrix-update methods

52

lvoneout <- function(y,xmat)

53

n <- length(y)

54

predy <- vector(length=n)

55

for (i in 1:n) {

56

regress, leaving out ith observation

57

lmo <- lm(y[-i] ~ xmat[-i,])

58

betahat <- as.vector(lmo$coef)

220

Chapter 9

www.it-ebooks.info

59

the 1 accommodates the constant term

60

predy[i] <- betahat %*% c(1,xmat[i,])

61

}

62

return(predy)

63

}

64

65

polynomial function of x, coefficients cfs

66

poly <- function(x,cfs) {

67

val <- cfs[1]

68

prod <- 1

69

dg <- length(cfs) - 1

70

for (i in 1:dg) {

71

prod <- prod * x

72

val <- val + cfs[i+1] * prod

73

}

74

}

As you can see, "polyreg" consists of polyfit(), the constructor function, and print.polyreg(), a print function tailored to this class. It also contains several utility functions to evaluate powers and polynomials and to perform cross-validation. (Note that in some cases here, efficiency has been sacrificed for clarity.)

As an example of using the class, we’ll generate some artificial data and create an object of class "polyreg" from it, printing out the results.

n <- 60

x <- (1:n)/n

y <- vector(length=n)

for (i in 1:n) y[i] <- sin((3pi/2)x[i]) + x[i]² + rnorm(1,mean=0,sd=0.5)

dg <- 15

(lmo <- polyfit(y,x,dg))

mean squared prediction errors, by degree

MSPE

[1,] 0.4200127

[2,] 0.3212241

[3,] 0.2977433

[4,] 0.2998716

[5,] 0.3102032

[6,] 0.3247325

[7,] 0.3120066

[8,] 0.3246087

[9,] 0.3463628

[10,] 0.4502341

[11,] 0.6089814

[12,] 0.4499055

[13,]

NA

[14,]

NA

[15,]

NA

Object-Oriented Programming

221

www.it-ebooks.info

Note first that we used a common R trick in this command:

(lmo <- polyfit(y,x,dg))

By surrounding the entire assignment statement in parentheses, we get the printout and form lmo at the same time, in case we need the latter for other things.

The function polyfit() fits polynomial models up through a specified degree, in this case 15, calculating the cross-validated mean squared prediction error for each model. The last few values in the output were NA, because roundoff error considerations led R to refuse to fit polynomials of degrees that high.

So, how is it all done? The main work is handled by the function

polyfit(), which creates an object of class "polyreg". That object consists mainly of the objects returned by the R regression fitter lm() for each degree.

In forming those objects, note line 14:

lmo$fitted.cvvalues <- lvoneout(y,pwrs[,1:i,drop=F])

Here, lmo is an object returned by lm(), but we are adding an extra component to it: fitted.cvvalues. Since we can add a new component to a list at any time, and since S3 classes are lists, this is possible.

我们还有一个用于通用函数 print() 的方法,第 24 行的 print.polyreg()。在第 12.1.5 节中,我们将为通用函数 plot() 添加一个方法,plot.polyreg()。

在计算预测误差时,我们使用了交叉验证或留一法,以从所有其他观测值预测每个观测值的形式。为此,我们利用了 R 在第 57 行使用负下标的特性:

lmo <- lm(y[-i] ~ xmat[-i,])

因此,我们正在从我们的数据集中删除第 i 个观测值来拟合模型。

注意

如代码注释中所述,我们可以通过使用称为 Sherman-Morrison-Woodbury 公式的矩阵逆更新方法来创建一个更快的实现。更多信息,请参阅 J. H. Venter 和 J. L. J. Snyman,

“关于线性模型选择中广义交叉验证准则的注释,”

Biometrika , 第 82 卷,第 1 期,第 215–219 页.

9.2 S4 类

一些程序员认为 S3 不提供与 OOP 通常相关的安全性。例如,考虑我们之前的员工数据库 222

第九章

www.it-ebooks.info

例如,我们的类 "employee" 有三个字段:姓名、工资和工会。

这里有一些可能的问题:

我们忘记输入工会状态。

我们将 union 错误地拼写成 onion

我们创建了一个不属于 "employee" 类别的对象,但意外地将它的类属性设置为 "employee"。

在这些情况下,R 不会抱怨。S4 的目标是引发抱怨并防止此类事故。

S4 结构比 S3 结构丰富得多,但在这里我们只介绍基础知识。表 9-1 展示了两个类之间的差异概述。

表 9-1: 基本 R 运算符

操作

S3

S4

定义类

构造函数代码中隐含

setClass()

创建对象

构建列表,设置类属性

new()

引用成员变量

$

@

实现 f() 通用函数

定义 f.classname()

setMethod()

声明通用

UseMethod()

setGeneric()

9.2.1 编写 S4 类

您通过调用 setClass() 定义 S4 类。继续我们的员工示例,我们可以编写以下内容:

setClass("employee",

representation(

name="character",

salary="numeric",

union="logical")

  • )

[1] "employee"

这定义了一个新的类 "employee",具有指定类型的三个成员变量。

现在,让我们使用 new(),S4 类的内置构造函数,为 Joe 创建这个类的实例:

joe <- new("employee",name="Joe",salary=55000,union=T)

joe

一个 "employee" 类的对象

槽位 "name":

[1] "Joe"

面向对象编程

223

www.it-ebooks.info

槽位 "salary":

[1] 55000

槽位 "union":

[1] TRUE

注意,成员变量被称为 槽位,通过 @ 符号引用。以下是一个示例:

joe@salary

[1] 55000

我们还可以使用 slot() 函数,例如,作为查询 Joe 工资的另一种方式:

slot(joe,"salary")

[1] 55000

我们可以类似地分配组件。让我们给 Joe 加薪:

joe@salary <- 65000

joe

类 "employee" 的对象

插槽 "name":

[1] "Joe"

插槽 "salary":

[1] 65000

插槽 "union":

[1] TRUE

不,他应该得到更高的加薪:

slot(joe,"salary") <- 88000

joe

类 "employee" 的对象

插槽 "name":

[1] "Joe"

插槽 "salary":

[1] 88000

插槽 "union":

[1] TRUE

224

第九章

www.it-ebooks.info

正如所提到的,使用 S4 的一个优点是安全性。为了说明这一点,假设我们不小心将 salary 写成了 salry,如下所示:

joe@salry <- 48000

Error in checkSlotAssignment(object, name, value) :

"salry" 不是类 "employee" 的插槽

相比之下,在 S3 中不会有错误信息。S3 类只是列表,你可以在任何时候添加新的组件(故意或非故意)。

9.2.2 在 S4 类上实现泛型函数

为了在 S4 类上定义泛型函数的实现,使用 setMethod()。在这里,我们将为我们的 "employee" 类做这件事。我们将实现 show() 函数,这是 S3 的泛型 "print" 的 S4 对应物。

如你所知,在 R 中,当你处于交互模式并输入变量的名称时,会打印出变量的值:

joe

类 "employee" 的对象

插槽 "name":

[1] "Joe"

插槽 "salary":

[1] 88000

插槽 "union":

[1] TRUE

由于 joe 是 S4 对象,这里的操作是调用 show()。实际上,我们可以通过输入以下内容得到相同的结果:

show(joe)

让我们用以下代码来覆盖它:

setMethod("show", "employee",

function(object) {

inorout <- ifelse(object@union,"is","is not") cat(object@name,"has a salary of",object@salary,

"and",inorout, "in the union", "\n")

}

)

第一个参数给出了我们将为其定义特定类方法的泛型函数的名称,第二个参数给出了类名。然后我们定义新的函数。

面向对象编程

225

www.it-ebooks.info

让我们试试看:

joe

Joe 的工资是 55000,并且他是工会的成员

9.3 S3 与 S4

要使用的类类型是 R 程序员之间的一些争议的主题。本质上,你在这里的观点可能取决于你个人的选择——你更重视 S3 的便利性还是 S4 的安全性。

约翰·查普曼,S 语言创造者,R 语言的核心开发者之一,在他的书 《数据分析软件》(Springer,2008 年)中推荐使用 S4 而不是 S3。他认为,为了编写“清晰和可靠的软件”,需要 S4。另一方面,他指出 S3 仍然相当流行。

你可以在 http://google-styleguide 找到谷歌的 R 风格指南

.googlecode.com/svn/trunk/google-r-style.html,在这方面很有趣。谷歌明确地站在 S3 一边,表示“尽可能避免使用 S4 对象和方法。”(当然,谷歌甚至有 R 风格指南也是很令人感兴趣的!)

首先考虑的是风格指南本身!)

注意

托马斯·卢米利的文章中给出了两种方法的良好、具体的比较

“程序员的小天地:一个简单的类,在 S3 和 S4 中,” R News , April 1, 2004, pp. 33–36.

9.4 管理您的对象

随着典型的 R 会话的进行,你往往会积累大量的对象。有各种工具可以帮助你管理它们。在这里,我们将查看以下内容:

The ls() function

rm() 函数

save() 函数

几个函数可以告诉你更多关于对象结构的信息,例如 class() 和 mode()

exists() 函数

9.4.1 使用 ls() 函数列出您的对象

ls() 命令将列出您当前的所有对象。此函数的一个有用的命名参数是 pattern,它启用 通配符。在这里,您告诉 ls() 仅列出名称中包含指定模式的对象。以下是一个示例。

226

第九章

www.it-ebooks.info

ls()

[1] "acc"

"acc05"

"binomci"

"cmeans"

"divorg"

"dv"

[7] "fit"

"g"

"genxc"

"genxnt"

"j"

"lo"

[13] "out1"

"out1.100" "out1.25"

"out1.50"

"out1.75"

"out2"

[19] "out2.100" "out2.25"

"out2.50"

"out2.75"

"par.set"

"prpdf"

[25] "ratbootci" "simonn"

"vecprod"

"x"

"zout"

"zout.100"

[31] "zout.125" "zout3"

"zout5"

"zout.50"

"zout.75"

ls(pattern="ut")

[1] "out1"

"out1.100" "out1.25" "out1.50" "out1.75" "out2"

[7] "out2.100" "out2.25" "out2.50" "out2.75" "zout"

"zout.100"

[13] "zout.125" "zout3"

"zout5"

"zout.50" "zout.75"

在第二种情况下,我们要求列出所有名称中包含字符串 "ut" 的对象。

9.4.2 使用 rm() 函数删除特定对象

要删除不再需要的对象,请使用 rm()。以下是一个示例:

rm(a,b,x,y,z,uuu)

此代码删除了六个指定的对象(a、b 等等)。

rm() 函数的一个命名参数是 list,这使得删除多个对象变得更容易。此代码将所有对象分配给列表,从而删除所有内容:

rm(list = ls())

使用 ls() 的模式参数,这个工具变得更加强大。

以下是一个示例:

ls()

[1] "doexpt"

"notebookline"

"nreps"

"numcorrectcis"

[5] "numnotebooklines" "numrules"

"observationpt"

"prop"

[9] "r"

"rad"

"radius"

"rep"

[13] "s"

"s2"

"sim"

"waits"

[17] "wbar"

"x"

"y"

"z"

ls(pattern="notebook")

[1] "notebookline"

"numnotebooklines"

rm(list=ls(pattern="notebook"))

ls()

[1] "doexpt"

"nreps"

"numcorrectcis" "numrules"

[5] "observationpt" "prop"

"r"

"rad"

[9] "radius"

"rep"

"s"

"s2"

[13] "sim"

"waits"

"wbar"

"x"

[17] "y"

"z"

面向对象编程

227

www.it-ebooks.info

在这里,我们找到了两个名称中包含字符串 "notebook" 的对象

然后要求删除它们,这通过第二次调用 ls() 得到确认。

NOTE

您可能会发现 browseEnv() 函数很有用。它将在您的网页浏览器中显示您的 全局变量(或不同指定环境中的对象),并提供每个对象的详细信息。

9.4.3 使用 save() 函数保存对象集合

在一个对象集合上调用 save() 函数会将它们写入磁盘,以便稍后通过 load() 函数检索。以下是一个快速示例:

z <- rnorm(100000)

hz <- hist(z)

save(hz,"hzfile")

ls()

[1] "hz" "z"

rm(hz)

ls()

[1] "z"

load("hzfile")

ls()

[1] "hz" "z"

plot(hz) # 弹出图形窗口

在这里,我们生成一些数据,然后绘制其直方图。但是

我们还保存了 hist() 的输出到一个变量 hz。这个变量是一个对象(当然,是 "histogram" 类的对象)。考虑到我们将在未来的 R 会话中重用这个对象,我们使用 save() 函数将对象保存到文件 hzfile。它可以在未来的会话中通过 load() 重新加载。为了演示这一点,我们故意删除了 hz 对象,然后调用 load() 重新加载它,然后调用 ls() 来显示它确实被重新加载了。

我曾经需要读取一个非常大的数据文件,每个记录都需要处理。然后我使用 save() 来保存处理后的 R 对象版本,以便未来的 R 会话使用。

9.4.4 “这是什么?”

开发者经常需要知道库函数返回的对象的确切结构。如果文档没有提供足够的细节,我们该怎么办?

以下 R 函数可能会有所帮助:

class(), mode()

names(), attributes()

unclass(), str()

edit()

228

第九章

www.it-ebooks.info

让我们通过一个例子来讲解。R 包含构建 列联表 的功能,我们在第 6.4 节中讨论了这一点。该节中的一个例子涉及一个选举调查,其中五位受访者被问及他们是否打算为候选人 X 投票,以及他们是否在上次选举中为 X 投票。以下是结果表:

cttab <- table(ct)

cttab

投票.X.上次时间

投票.X 是否

No

2

0

不确定 0

1

Yes

1

1

例如,两位受访者对两个问题都回答了“否”。

函数 table 返回的对象 cttab 很可能是 "table" 类。检查文档 (?table) 可以确认这一点。但是类中有什么内容呢?

让我们探索一下那个对象 cttab 的结构,它是 "table" 类。

ctu <- unclass(cttab)

ctu

投票.X.上次时间

投票.X 是否

No

2

0

不确定 0

1

Yes

1

1

class(ctu)

[1] "matrix"

因此,对象的计数部分是一个矩阵。(如果数据涉及三个或更多问题,而不是仅仅两个,这将是一个更高维度的数组。)请注意,维度的名称以及单个行和列的名称也在这里;它们与矩阵相关联。

unclass() 函数作为第一步非常有用。如果你只是打印一个对象,你将受该类关联的 print() 版本的影响,它可能会为了简洁而隐藏或扭曲一些有价值的信息。调用 unclass() 的结果可以让你绕过这个问题,尽管在这个例子中没有差异。(你在一个关于 S3 的部分中看到了它确实有差异的一个例子。)

(如第 9.1.1 节中较早讨论的)通用函数。函数 str() 以更紧凑的方式完成相同的目的。

注意,尽管将 unclass() 应用到一个对象上仍然会得到一个具有某些基本类的对象。在这里,cttab 的类是 "table",但 unclass(cttab) 仍然具有 "matrix" 类。

输出——在屏幕上打印指令,打印

Image 22

www.it-ebooks.info

角色,很多功能在屏幕上会飞快地闪过,以至于我们无法吸收。我们可以使用 page() 来解决这个问题,但我更喜欢 edit():

我们是否创建了一个对象,以及它是否仍然存在?不一定。如果你正在编写通用代码,比如要将其发布到 R 的 CRAN 代码库中,你的代码可能需要检查是否存在某个对象,如果不存在,则你的代码必须创建它。例如,正如你在 9.4.3 节中学到的,你可以使用 save() 将对象保存到磁盘文件中,然后通过调用 load() 将它们恢复到 R 的内存空间中。

edit(table)

的角色

9.4.5 exists() 函数

y

啊,有趣。这表明 table() 在某种程度上是另一个函数 tabulate() 的包装器。但在这里可能更重要的是,"table" 对象的结构实际上非常简单:它由从计数创建的数组组成,类属性附加在其上。所以,它本质上只是一个数组。

函数 names() 显示对象中的组件,而 attributes() 给你更多,特别是类名。

第九章

class(y) <- "table"

例如,以下代码显示 acc 对象存在:

exists("acc")

最被忽视的话题之一

为什么这个函数会有用?难道我们总是不知道或者

函数 exists() 返回 TRUE 或 FALSE,取决于参数是否存在。请确保将参数放在引号内。

如果你编写的通用代码需要调用后者,如果对象尚未存在,你可以通过调用 exists() 来检查这个条件。

230

y <- array(tabulate(bin, pd), dims, dimnames = dn)

www.it-ebooks.info

输入/输出

10

[1] TRUE

这允许你使用文本编辑器浏览代码。在这样做的时候,你会在代码的末尾找到以下代码:

在许多大学编程课程中

是输入/输出 (I/O)。I/O 在大多数实际应用中扮演着核心

在大多数实际应用中计算

ers。只需考虑一个自动柜员机现金机,它使用

多个 I/O 操作,包括输入——读取你的

卡片并读取你输入的现金请求——

229

你的收据,最重要的是,控制

机器来输出你的钱!

R 并不是运行自动柜员机的工具,但它具有高度灵活的 I/O 功能,你将在本章中了解到。

我们将从键盘和监视器的基本访问方法开始,然后深入探讨读取和写入文件,包括文件目录的导航。最后,我们讨论 R 访问互联网的功能。

www.it-ebooks.info

10.1 访问键盘和监视器

R 提供了几个用于访问键盘和监视器的函数。在这里,我们将查看 scan()、readline()、print() 和 cat() 函数。

10.1.1 使用 scan() 函数

您可以使用 scan() 从文件或键盘读取一个向量,无论是数值还是字符。通过一些额外的工作,您甚至可以读取数据以形成一个列表。

假设我们有一些名为 z1.txtz2.txtz3.txtz4.txt 的文件。z1.txt 文件包含以下内容:

123

4 5

6

z2.txt 文件内容如下:

123

4.2 5

6

z3.txt 文件包含以下内容:

abc

de f

g

最后,z4.txt 文件包含以下内容:

abc

123 6

y

让我们看看使用 scan() 函数可以对这些文件做些什么。

scan("z1.txt")

读取了 4 个项目

[1] 123 4 5 6

scan("z2.txt")

读取 4 个项目

[1] 123.0 4.2 5.0 6.0

scan("z3.txt")

错误在 scan(file, what, nmax, sep, dec, quote, skip, nlines, na.strings, ):scan()期望'a real',但得到'abc'

scan("z3.txt",what="")

232

第十章

www.it-ebooks.info

读取 4 个项目

[1] "abc" "de" "f" "g"

scan("z4.txt",what="")

读取 4 个项目

[1] "abc" "123" "6" "y"

在第一次调用中,我们得到了一个包含四个整数的向量(尽管模式是数值)。第二次调用时,由于一个数字不是整数,其他数字也显示为浮点数。

在第三种情况下,我们得到了一个错误。scan()函数有一个名为 what 的可选参数,它指定了模式,默认为双精度模式。因此,文件z3的非数值内容产生了错误。但然后我们再次尝试,with what="".这分配了一个字符字符串给 what,表示我们想要字符模式。(我们也可以将 what 设置为任何字符字符串。)最后的调用以相同的方式工作。第一个项目是一个字符字符串,因此它将所有后续的项目也视为字符串。

当然,在典型用法中,我们会将 scan()的返回值赋给一个变量。以下是一个示例:

v <- scan("z1.txt")

默认情况下,scan()假设向量的项由空白分隔,包括空格、回车/换行符和水平制表符。你可以使用可选的 sep 参数来处理其他情况。例如,我们可以将 sep 设置为换行符,以便将每一行作为字符串读取,如下所示:

x1 <- scan("z3.txt",what="")

读取 4 个项目

x2 <- scan("z3.txt",what="",sep="\n") 读取 3 个项目

x1

[1] "abc" "de" "f"

"g"

x2

[1] "abc" "de f" "g"

x1[2]

[1] "de"

x2[2]

[1] "de f"

在第一种情况下,字符串"de"和"f"被分配给了 x1 的不同元素。但在第二种情况下,我们指定 x2 的元素由换行符分隔,而不是空格。由于"de"和"f"在同一行,它们被一起分配给了 x[2]。

本章后面将介绍更复杂的读取文件的方法,例如逐行读取文件的方法。但如果你想要一次性读取整个文件,scan()提供了一个快速解决方案。

输入/输出

233

www.it-ebooks.info

你可以使用 scan()从键盘读取,通过指定空字符串作为文件名:

v <- scan("")

1: 12 5 13

4: 3 4 5

7: 8

8:

读取 7 个项目

v

[1] 12 5 13 3 4 5 8

注意,我们会提示输入下一个项目的索引,并且通过空行来表示输入的结束。

如果你不想 scan()宣布它读取的项目数量,包括 quiet=TRUE 参数。

10.1.2 使用 readline()函数

如果你想要从键盘读取单行,readline()非常方便。

w <- readline()

abc de f

w

[1] "abc de f"

通常,readline()函数会带上可选的提示符,如下所示:

inits <- readline("请输入您的首字母:")

请输入您的首字母:NM

初始化

[1] "NM"

10.1.3 打印到屏幕

在交互式模式的顶层,您可以通过简单地输入变量名或表达式来打印变量的值或表达式。如果您需要在函数体内部打印,则无法这样做。在这种情况下,您可以使用 print() 函数,如下所示:

x <- 1:3

print(x²)

[1] 1 4 9

回想一下,print() 是一个 通用 函数,因此实际调用的函数将取决于打印的对象的类。例如,如果参数是 "table" 类,则将调用 print.table() 函数。

234

Chapter 10

www.it-ebooks.info

使用 cat() 而不是 print() 会更好一些,因为后者只能打印一个表达式,并且其输出是编号的,这可能会造成不便。

比较函数的结果:

print("abc")

[1] "abc"

cat("abc\n")

abc

注意,在调用 cat() 时,我们需要提供自己的换行符,"\n"。如果没有它,我们的下一个调用将继续写入同一行。

cat() 的参数将以空格分隔打印出来:

x

[1] 1 2 3

cat(x,"abc","de\n")

1 2 3 abc de

如果您不想有空间,将 sep 设置为空字符串 "",如下所示:

cat(x,"abc","de\n",sep="")

123abcde

可以使用任何字符串作为 sep。在这里,我们使用换行符:

cat(x,"abc","de\n",sep="\n")

1

2

3

abc

de

您甚至可以将 sep 设置为字符串向量,如下所示:

x <- c(5,12,13,8,88)

cat(x,sep=c(".",".",".","\n","\n")) 5.12.13.8

88

10.2 读取和写入文件

现在我们已经涵盖了 I/O 的基础知识,让我们来看看读取和写入文件的一些更实际的应用。以下各节讨论从文件中读取数据框或矩阵、处理文本文件、访问远程机器上的文件以及获取文件和目录信息。

输入/输出

235

www.it-ebooks.info

10.2.1 从文件中读取数据框或矩阵

在 5.1.2 节中,我们讨论了使用 read.table() 函数读取数据框的使用。作为一个快速回顾,假设文件 z 看起来像这样:name age

John 25

Mary 28

Jim 19

第一行包含一个可选的标题,指定列名。我们可以这样读取文件:

z <- read.table("z",header=TRUE)

z

name age

1 John 25

2 Mary 28

3 Jim 19

注意,scan() 在这里不会工作,因为我们的文件包含数字和字符数据的混合(以及一个标题)。

从文件中直接读取矩阵似乎没有直接的方法,但可以使用其他工具轻松完成。一种简单快捷的方法是使用 scan() 逐行读取矩阵。您在 matrix() 函数中使用 byrow 选项来指示您正在按行定义矩阵的元素,而不是按列定义。

例如,假设文件 x 包含一个 5 行 3 列的矩阵,按行存储:1 0 1

1 1 1

1 1 0

1 1 0

0 0 1

我们可以这样将其读入矩阵:

x <- matrix(scan("x"),nrow=5,byrow=TRUE)

这对于快速的一次性操作是可行的,但为了通用性,您可以使用 read.table(),它返回一个数据框,然后通过 as.matrix()进行转换。这里有一个通用方法:

read.matrix <- function(filename) {

as.matrix(read.table(filename))

}

236

第十章

www.it-ebooks.info

10.2.2 读取文本文件

在计算机文献中,经常将文本文件二进制文件区分开来。这种区分有些误导——从本质上讲,每个文件都是由 0 和 1 组成的二进制文件。让我们将文本文件这个术语理解为主要由 ASCII 字符或其他人类语言(如中文的 GB 编码)编码组成的文件,并使用换行符来给人类提供行的感知。后一个方面将在这里变得至关重要。非文本文件,如 JPEG 图像或可执行程序文件,通常被称为二进制文件

您可以使用 readLines()来读取文本文件,一次读取一行或一次性读取。例如,假设我们有一个名为z1的文件,其内容如下:

John 25

Mary 28

Jim 19

我们也可以一次性读取整个文件,如下所示:

z1 <- readLines("z1")

z1

[1] "John 25" "Mary 28" "Jim 19"

由于每一行都被视为一个字符串,这里的返回值是一个字符串向量——即字符模式的向量。对于每读取一行,都有一个向量元素,因此这里有三个元素。

或者,我们可以逐行读取。为此,我们首先需要创建一个连接,如下所述。

10.2.3 连接简介

连接是 R 在各类输入/输出操作中使用的根本机制。在这里,它将被用于文件访问。

连接是通过调用 file()、url()或 R 的几个其他函数创建的。要查看这些函数的列表,请输入以下内容:

?连接

因此,我们现在可以按如下方式逐行读取上一节中介绍的z1文件:

c <- file("z1","r")

readLines(c,n=1)

[1] "John 25"

readLines(c,n=1)

[1] "Mary 28"

readLines(c,n=1)

[1] "Jim 19"

输入/输出

237

www.it-ebooks.info

readLines(c,n=1)

character(0)

我们打开了连接,将结果赋值给 c,然后按指定的 n=1 参数逐行读取文件。当 R 遇到文件末尾(EOF)时,它返回一个空结果。我们需要设置一个连接,以便 R 在读取文件时能够跟踪我们的位置。

我们可以在代码中检测到文件结束符(EOF):

c <- file("z","r")

while(TRUE) {

rl <- readLines(c,n=1)

if (length(rl) == 0) {

print("到达文件末尾")

break

} else print(rl)

  • }

[1] "John 25"

[1] "Mary 28"

[1] "Jim 19"

[1] "到达文件末尾"

如果我们希望“重置”——从文件开头重新开始,我们可以使用 seek():

c <- file("z1","r")

readLines(c,n=2)

[1] "John 25" "Mary 28"

seek(con=c,where=0)

[1] 16

readLines(c,n=1)

[1] "John 25"

我们对 seek()的调用中 where=0 的参数意味着我们希望将文件指针定位在文件开始处零个字符的位置——换句话说,直接在开头。

通话返回 16,表示文件指针位于位置 16

在我们打电话之前。这很有道理。第一行由"John 25"组成

加上行尾字符,总共八个字符,第二行也是如此。因此,在读取前两行之后,我们位于位置 16。

你可以通过调用——还能是什么?——close()来关闭连接。你会使用这个来让系统知道你一直在写入的文件现在已经完成,应该现在正式写入磁盘。作为另一个例子,在互联网上的客户端/服务器关系(见第 10.3.1 节),客户端会使用 close()来向服务器指示客户端正在注销。

238

第十章

www.it-ebooks.info

10.2.4 扩展示例:读取 PUMS 人口普查文件

美国人口普查局以公共使用微观数据样本(PUMS)的形式提供人口普查数据。这里的微观数据意味着我们处理的是原始数据,每条记录对应一个真实的人,而不是统计摘要。包括许多变量的数据。

数据按家庭组织。对于每个单位,首先有一个家庭记录,描述该家庭的各项特征,然后为家庭中的每个人提供一个个人记录。家庭记录中的第 106 位和第 107 位(从 1 开始编号)表示该家庭的个人记录数量。(这个数字可能非常大,因为一些机构被视为家庭。)为了增强数据的完整性,第 1 位字符包含 H 或 P 以确认这是一个家庭或个人记录。因此,如果你读取一个 H 记录,并且它告诉你家庭中有三个人,那么接下来的三个记录应该是 P 记录,然后是另一个 H 记录;如果不是,你遇到了错误。

作为我们的测试文件,我们将取 2000 年 1%样本的前 1000 条记录。前几条记录看起来像这样:

H000019510649

06010

99979997 70

631973

15758

59967658436650000012000000 0 0 0 0 0 0 0 0 0 0 0 0 0

0

0

0

0

0 0 0

0 0

0 0000 0

0

0 0 0

00000000000000000000000000000

00000000000000000000000000

P00001950100010923000420190010110000010147050600206011099999904200000 0040010000

00300280

28600 70

9997

9997202020202020220000040000000000000006000000

00000 00

0000

00000000000000000132241057904MS

476041-20311010310

07000049010000000000900100000100000100000100000010000001000139010000490000

H000040710649

06010

99979997 70

631973

15758

599676584365300800200000300106060503010101010102010 01200006000000100001

00600020 0

0 0

0 0000 0

0

0 0 0

02000102010102200000000010750

02321125100004000000040000

P00004070100005301000010380010110000010147030400100009005199901200000 0006010000

00100000

00000 00

0000

0000202020202020220000040000000000000001000060

06010 70

9997

99970101004900100000001018703221

770051-10111010500

40004000000000000000000000000000000000000000000000000000004000000040000349

P00004070200005303011010140010110000010147050000204004005199901200000 0006010000

00100000

00000 00

0000

00002020202002000000000000000000000050000

00000 00

0000

000000000000000000000000000000000000000000-00000000000

000

0

0

0

0

0

0

0

0

00000000349

H000061010649

06010

99979997 70

631973

15758

59967658436080119010000020020403050201010101010201000770004800064000001

1

0 030

0 0

0 0340 00660000000170 0

06010000000004410039601000000

00021100000004940000000000

记录非常宽,因此会换行。每一行都占据

页面上这里有四行。

输入/输出

239

www.it-ebooks.info

我们将创建一个名为 extractpums() 的函数,用于读取 PUMS 文件并从其 Person 记录中创建一个数据框。用户指定文件名、要提取的字段列表以及分配给这些字段的名称。

我们还希望保留家庭序列号。这很好,因为同一家庭中的人的数据可能相关,我们可能希望将这一方面添加到我们的统计模型中。此外,家庭数据可能提供重要的协变量。(在后一种情况下,我们还想保留协变量数据。)

在查看函数代码之前,让我们看看该函数的功能。

在这个数据集中,性别在第 23 列,年龄在第 25 和 26 列。在示例中,我们的文件名是 pumsa。下面的调用创建了一个包含这两个变量的数据框。

pumsdf <- extractpums("pumsa",list(Gender=c(23,23),Age=c(25,26))) Note that we are stating here the names we want the columns to have in the resulting data frame. We can use any names we want—say Sex and Ancientness.

这里是该数据框的第一部分:

head(pumsdf)

serno Gender Age

2

195

2 19

3

407

1 38

4

407

1 14

5

610

2 65

6 1609

1 50

7 1609

2 49

以下是为 extractpums() 函数编写的代码。

1

reads in PUMS file pf, extracting the Person records, returning a data 2

frame; each row of the output will consist of the Household serial 3

number and the fields specified in the list flds; the columns of

4

the data frame will have the names of the indices in flds

5

6

extractpums <- function(pf,flds) {

7

dtf <- data.frame() # data frame to be built

8

con <- file(pf,"r") # connection

9

处理输入文件

10

repeat {

11

hrec <- readLines(con,1) # read Household record

12

if (length(hrec) == 0) break # end of file, leave loop

13

获取家庭序列号

14

serno <- intextract(hrec,c(2,8))

240

第十章

www.it-ebooks.info

15

有多少 Person 记录?

16

npr <- intextract(hrec,c(106,107))

17

if (npr > 0)

18

for (i in 1:npr) {

19

prec <- readLines(con,1) # get Person record

20

为这个人的数据框制作一行

21

person <- makerow(serno,prec,flds)

22

add it to the data frame

23

dtf <- rbind(dtf,person)

24

}

25

}

26

return(dtf)

27

}

28

29

为此人的行设置数据框

30

makerow <- function(srn,pr,fl) {

31

l <- list()

32

l[["serno"]] <- srn

33

for (nm in names(fl)) {

34

l[[nm]] <- intextract(pr,fl[[nm]])

35

}

36

返回(l)

37

}

38

39

从字符串 s 中提取整数字段,在字符位置 40

rng[1] through rng[2]

41

intextract <- function(s,rng) {

42

fld <- substr(s,rng[1],rng[2])

43

return(as.integer(fld))

44

}

让我们看看这是如何工作的。在 extractpums()的开始,我们创建一个空的数据框并设置读取 PUMS 文件的连接。

dtf <- data.frame() # 要构建的数据框

con <- file(pf,"r") # 连接

代码的主体部分由一个重复循环组成。

repeat {

hrec <- readLines(con,1) # 读取家庭记录

if (length(hrec) == 0) break # 文件结束,离开循环

获取家庭序列号

serno <- intextract(hrec,c(2,8))

有多少个人记录?

npr <- intextract(hrec,c(106,107))

输入/输出

241

www.it-ebooks.info

if (npr > 0)

for (i in 1:npr) {

...

}

}

这个循环会一直迭代到输入文件结束。后者条件将通过遇到零长度的家庭记录来感知,如前所述代码所示。

在重复循环中,我们交替读取家庭记录和相关的个人记录。当前家庭记录的个人记录数从该记录的第 106 和第 107 列提取,并将此数字存储在 npr 中。这种提取是通过调用我们的函数 intextract()完成的。

然后 for 循环逐个读取个人记录,在每种情况下形成输出数据框所需的行,然后通过 rbind()将其附加到后者:

for (i in 1:npr) {

prec <- readLines(con,1) # 获取个人记录

为此人的行创建数据框

person <- makerow(serno,prec,flds)

将它添加到数据框中

dtf <- rbind(dtf,person)

}

注意 makerow()如何创建要添加的给定人员的行。在这里,形式参数是 srn(家庭序列号),pr(给定的个人记录),fl(变量名称和列字段列表)。

makerow <- function(srn,pr,fl) {

l <- list()

l[["serno"]] <- srn

for (nm in names(fl)) {

l[[nm]] <- intextract(pr,fl[[nm]])

}

return(l)

}

例如,考虑我们的示例调用:

pumsdf <- extractpums("pumsa",list(Gender=c(23,23),Age=c(25,26))) 当 makerow()执行时,fl 将是一个包含两个元素的列表,分别命名为 Gender 和 Age。字符串 pr,当前的个人记录,Gender 位于第 23 列,Age 位于第 25 和第 26 列。我们调用 intextract()来提取所需的数字。

242

第十章

www.it-ebooks.info

intextract()函数本身是将字符转换为数字的简单转换,例如将字符串"12"转换为数字 12。

注意,如果没有家庭记录的存在,我们可以使用一个方便的内置 R 函数(read.fwf())轻松地完成所有这些操作。这个函数的名称是“read fixed-width formatted”的缩写。

暗示每个变量都存储在记录的指定字符位置。本质上,这个函数减轻了编写类似 intextract() 函数的需求。

10.2.5 通过 URL 访问远程机器上的文件

某些 I/O 函数,如 read.table() 和 scan(),接受网络 URL 作为参数。(检查 R 的在线帮助功能,看看您喜欢的函数是否允许这样做。)

作为例子,我们将从加州大学读取一些数据。

nia, Irvine archive at http://archive.ics.uci.edu/ml/datasets.html,使用 Echocardiogram 数据集。在导航链接后,我们找到该文件的位置,然后从 R 中读取,如下所示:

uci <- "http://archive.ics.uci.edu/ml/machine-learning-databases/"

uci <- paste(uci,"echocardiogram/echocardiogram.data",sep="")

ecc <- read.csv(uci)

(我们在这里分阶段构建 URL 以适应页面。)

让我们看看我们下载了什么:

head(ecc)

X11 X0 X71 X0.1 X0.260

X9 X4.600 X14

X1 X1.1 name X1.2 X0.2

1 19 0 72

0 0.380

6 4.100

14 1.700 0.588 name

1

0

2 16 0 55

0 0.260

4 3.420

14

1

1 name

1

0

3 57 0 60

0 0.253 12.062 4.603

16 1.450 0.788 name

1

0

4 19 1 57

0 0.160

22 5.750

18 2.250 0.571 name

1

0

5 26 0 68

0 0.260

5 4.310

12

1 0.857 name

1

0

6 13 0 62

0 0.230

31 5.430 22.5 1.875 0.857 name

1

0

我们可以进行我们的分析。例如,第三列是年龄,因此我们可以找到它的平均值或对数据进行其他计算。请参阅 http://archive.ics.uci.edu/ml/machine-learning-databases/echocardiogram/echocardiogram.names 页面上的 echocardiogram.names 页面,了解所有变量的描述。

10.2.6 写入文件

考虑到 R 的统计基础,文件读取可能比写入更常见。但是,有时写入是必要的,本节将介绍写入文件的方法。

输入/输出

243

www.it-ebooks.info

函数 write.table() 与 read.table() 非常相似,除了它写入一个数据框而不是读取一个。例如,让我们从第五章开头的 Jack 和 Jill 小例子开始:

kids <- c("Jack","Jill")

ages <- c(12,10)

d <- data.frame(kids,ages,stringsAsFactors=FALSE)

d

kids ages

1 Jack

12

2 Jill

10

write.table(d,"kds")

文件 kds 现在将包含以下内容:

"kids" "ages"

"1" "Jack" 12

"2" "Jill" 10

在将矩阵写入文件的情况下,只需声明您不需要行或列名,如下所示:

write.table(xc,"xcnew",row.names=FALSE,col.names=FALSE) 函数 cat() 也可以用来分部分写入文件。

这里是一个例子:

cat("abc\n",file="u")

cat("de\n",file="u",append=TRUE)

第一次调用 cat() 创建了文件 u,包含一行内容 "abc"。第二次调用追加第二行。与使用 writeLines() 函数的情况不同(我们将在下一节讨论),文件在每次操作后都会自动保存。例如,在之前的调用之后,文件将看起来像这样:

abc

de

您也可以写入多个字段。所以:

cat(file="v",1,2,"xyz\n")

将生成一个包含单行的文件 v

1 2 xyz

244

第十章

www.it-ebooks.info

您还可以使用 writeLines(),它是 readLines() 的对应函数。如果您使用连接,必须指定 "w" 来表示您正在写入文件,而不是从文件中读取:

c <- file("www","w")

writeLines(c("abc","de","f"),c)

close(c)

文件 www 将包含以下内容:

abc

de

f

注意需要主动关闭文件。

10.2.7 获取文件和目录信息

R 有各种用于获取目录和文件信息、设置文件访问权限等的函数。以下是一些示例:

file.info():为每个在参数中的文件名(一个字符向量)提供文件大小、创建时间、目录与普通文件状态等信息。

dir():返回一个字符向量,列出其第一个参数指定的目录中所有文件的名字。如果指定了可选参数 recursive=TRUE,则结果将显示以第一个参数为根的整个目录树。

file.exists():返回一个布尔向量,指示第一个参数中的每个名字所指定的文件是否存在。

getwd() 和 setwd():用于确定或更改当前工作目录。

要查看所有与文件和目录相关的函数,请输入以下内容:

?files

下一个示例中将演示一些这些选项。

10.2.8 扩展示例:计算多个文件的内容总和

在这里,我们将开发一个函数来查找目录树中所有文件内容的总和(假设为数值)。在我们的示例中,目录 dir1

输入/输出

245

www.it-ebooks.info

包含文件 fileafileb,以及一个子目录 dir2,其中包含文件 filec。文件内容如下:

filea: 5, 12, 13

fileb: 3, 4, 5

filec: 24, 25, 7

如果 dir1 在我们的当前目录中,调用 sumtree("dir1") 将得到这九个数字的总和,98。否则,我们需要指定 dir1 的完整路径名,例如 sumtree("/home/nm/dir1")。以下是代码:1

sumtree <- function(drtr) {

2

tot <- 0

3

获取树中所有文件的名字

4

fls <- dir(drtr,recursive=TRUE)

5

for (f in fls) {

6

f 是否是一个目录?

7

f <- file.path(drtr,f)

8

if (!file.info(f)$isdir) {

9

tot <- tot + sum(scan(f,quiet=TRUE))

10

}

11

}

12

return(tot)

13

}

注意,这个问题是递归的自然选择,我们在第 7.9 节中讨论了递归。但在这里,R 通过在 dir()中允许它作为选项来为我们执行递归。因此,在第 4 行,我们设置 recursive=TRUE,以便在整个目录树的不同级别中找到文件。

要调用 file.info(),我们需要考虑到当前文件名f相对于 drtr 是相对的,因此我们的文件filea将被引用为dir1/filea

为了形成该路径名,我们需要连接 drtr、一个斜杠和 filea。我们可以使用 R 字符串连接函数 paste()来做这件事,但我们需要为 Windows 使用一个单独的情况,Windows 使用反斜杠而不是斜杠。但 file.path()为我们做了所有这些。

关于第 8 行的某些评论是必要的。函数

file.info()返回有关 f 的信息作为数据框,其中一个列是 isdir,每行对应一个文件,行名是文件名。该列由布尔值组成,指示每个文件是否是目录。因此,在第 8 行,我们可以检测当前文件f是否是目录。如果f是一个普通文件,我们就继续将其内容添加到我们的运行总和中。

10.3 访问互联网

R 的套接字功能为程序员提供了访问互联网的 TCP/IP 的接口。

协议。对于不熟悉此协议的读者,我们将从 TCP/IP 的概述开始。

246

第十章

www.it-ebooks.info

10.3.1 TCP/IP 概述

TCP/IP 相当复杂,所以这里的概述将是一种过度简化,但我们将涵盖足够的内容,以便您理解 R 的套接字函数正在做什么。

在我们这里的目的中,术语网络指的是一组本地连接在一起的计算机,而不通过互联网。这通常包括家庭中的所有计算机,较小企业中的所有计算机,等等。它们之间的物理介质通常是某种形式的以太网连接。

互联网,正如其名所暗示的,连接网络。互联网中的网络通过路由器连接到一个或多个其他网络,路由器是专门用于连接两个或更多网络的特殊用途计算机。

互联网上的每台计算机都有一个互联网协议(IP)地址。这是一个数字,但它可以用字符表示,例如www.google.com,然后由域名服务将其转换为数字地址。

然而,IP 地址是不够的。当 A 向 B 发送消息时,

B,可能有几个应用程序在计算机 B 上接收互联网消息,例如网页浏览、电子邮件服务等等。B 的操作系统如何知道将来自 A 的消息发送给这些中的哪一个?答案是 A 将指定一个端口号,除了 IP 地址。

端口号表示 B 上运行哪个程序作为接收者。A 也将有一个端口号,以便 B 的响应

达到 A 的正确应用程序。

当 A 想要向 B 发送某些内容时,它会向一个名为socket的软件实体写入,使用与写入文件的系统调用在语法上相似的调用。在调用中,A 指定了 B 的 IP 地址和 A 希望发送消息的端口号。B 也有一个 socket,它将响应写入到该 socket 中。我们说 A 和 B 通过这些 socket 之间存在连接,但这并不意味着任何物理上的东西——这只是 A 和 B 之间交换数据的协议。

应用程序遵循客户端/服务器模型。比如说,一个 Web 服务器在 B 处运行,在 Web 的标准端口 80 上。B 处的服务器正在端口 80 上监听。同样,这个术语不应被字面理解;它只是意味着服务器程序已经调用了一个通知操作系统服务器程序愿意在端口 80 上建立连接的功能。当网络节点 A 请求这样的连接时,服务器上的函数调用返回,连接就建立了。

如果你是一个非特权用户并且编写某种类型的服务器程序——

比如在 R 中!——你必须分配一个大于 1024 的端口号。

注意

如果服务器程序被关闭或崩溃,在相同的端口再次可重用之前可能会有几秒钟的延迟。

10.3.2 R 中的套接字

需要牢记的一个重要观点是,A 向 B 发送的所有字节

在它们之间存在的连接存在期间,被认为是一个大的消息。比如说,A 发送了一行 8 个字符的文本,然后输入/输出

247

www.it-ebooks.info

另一个 20 个字符。从 A 的角度来看,这是两行,但对 TCP/IP 来说,这只是尚未完成的消息的 28 个字符。将这个长消息拆分成行可能需要一些工作。R 为此提供了各种函数,包括以下:

readLines()和 writeLines():这些允许你以 TCP/IP

我们正在逐行发送消息,尽管实际上并非如此。如果你的应用程序自然地以行为单位来考虑,这两个函数可能非常有用。

serialize()和 unserialize():你可以使用这些函数发送 R 对象,例如矩阵或统计函数调用的复杂输出。发送方将对象转换为字符字符串形式,然后在接收方将其转换回原始对象形式。

readBin()和 writeBin():这些用于以二进制形式发送数据。

(回想一下第 10.2.2 节开头关于术语的注释。)这些函数中的每一个都在 R 连接上操作,正如你将在下一个示例中看到的。

选择每个任务正确的函数很重要。例如,如果你有一个长向量,使用 serialize()和 unserialize()可能更方便,但耗时更多。这不仅是因为数字必须转换为它们的字符表示形式,然后又转换回,而且因为字符表示形式通常要长得多,这意味着传输时间更长。

这里是两个其他的 R 套接字函数:

socketConnection(): 通过套接字建立 R 连接。您在参数 port 中指定端口号,并说明是否需要建立一个

要创建服务器或客户端,通过将参数 server 设置为 TRUE

或 FALSE,分别。在客户端的情况下,你还必须在参数 host 中提供服务器的 IP 地址。

socketSelect(): 当服务器连接到多个客户端时很有用。其主要参数 socklist 是一个连接列表,其返回值是已准备好供服务器读取数据的连接子列表。

10.3.3 扩展示例:实现并行 R

一些统计分析的运行时间非常长,因此对“并行 R”产生了很大的兴趣,其中几个 R 进程合作完成给定任务。另一个“并行化”的可能原因是内存限制。如果一台机器没有足够的内存来处理当前任务,通过某种方式汇集几台机器的内存可能会有所帮助。第十六章

介绍这个重要主题。

套接字在许多并行 R 包中扮演着关键角色。合作的 R

进程可以在同一台机器上或不同的机器上。在后一种情况(甚至在前一种情况),实现并行化的自然方法是用 R 套接字。这是 snow 包中的选择之一248

第十章

www.it-ebooks.info

在我的 Rdsm 包中(两者都可在 CRAN,R 的代码存储库中找到;有关详细信息,请参阅本书附录),如下所示:

在 snow 中,服务器向客户端发送工作任务。客户端执行任务并将结果发送回服务器,服务器将它们组装成最终结果。通信是通过 serialize()和 unserialize()完成的,服务器使用 socketSelect()来确定哪些客户端结果已准备好。

Rdsm 实现了一个虚拟共享内存范式,服务器用于存储共享变量。客户端在需要读取或写入共享变量时联系服务器。为了优化速度,服务器和客户端之间的通信使用 readBin()和 writebin(),而不是 serialize()和 unserialize()。

让我们看看 Rdsm 的一些与套接字相关的细节。首先,这是设置与客户端连接的服务器代码,将它们存储在列表 cons 中(有 ncon 个客户端):

1

与客户端建立套接字连接

2

3

cons <<- vector(mode="list",length=ncon) # 连接列表 4

防止在调试或长时间计算期间连接中断

5

设置选项("timeout"=10000)

6

for (i in 1:ncon) {

7

cons[[i]] <<-

8

socketConnection(port=port,server=TRUE,blocking=TRUE,open="a+b") 9

等待从客户端 i 接收消息

10

checkin <- unserialize(cons[[i]])

11

}

12

发送确认消息

13

for (i in 1:ncon) {

14

向客户端发送其 ID 号和组大小

15

serialize(c(i,ncon),cons[[i]])

16

}

由于客户端消息和服务器确认消息都很短,

sages, serialize()和 unserialize()在这里足够好。

服务器主循环的第一部分是找到一个就绪客户端并从中读取。

1

repeat {

2

是否还有客户端仍在?

3

if (remainingclients == 0) break

4

等待服务请求,然后读取它

5

查找所有挂起的客户端请求

6

rdy <- which(socketSelect(cons))

7

选择一个

8

j <- sample(1:length(rdy),1)

输入/输出

249

www.it-ebooks.info

9

con <- cons[[rdy[j]]]

10

读取客户端请求

11

req <- unserialize(con)

再次使用 serialize()和 unserialize()在这里足够好,用于读取客户端指示的操作类型——通常是读取共享变量或写入一个——的简短消息。但是,共享变量的读取和写入本身使用更快的 readBin()和 writeBin()函数。以下是写入部分:

将数据 dt,模式 md(整数或双精度浮点数),写入连接 cn

binwrite <- function(dt,md,cn) {

writeBin(dt,con=cn)

以下是读取部分:

从连接 cn 读取 sz 个元素的模式 md(整数或双精度浮点数)binread <- function(cn,md,sz) {

return(readBin(con=cn,what=md,n=sz))

在客户端,连接设置代码如下:

1

options("timeout"=10000)

2

连接到服务器

3

con <- socketConnection(host=host,port=port,blocking=TRUE,open="a+b") 4

serialize(list(req="checking in"),con)

5

从服务器接收此客户端的 ID 和客户端总数

6

myidandnclnt <- unserialize(con)

7

myinfo <<-

8

list(con=con,myid=myidandnclnt[1],nclnt=myidandnclnt[2])

从服务器读取和写入的代码与前面的服务器示例类似。

250

第十章

www.it-ebooks.info

Image 23

11

字符串操作

尽管 R 是一种统计语言,具有

数字向量和矩阵扮演着

字符串在 R 程序中起着核心作用,字符字符串出人意料地重要。

在统计应用中也非常重要。从出生到

医学研究数据文件中存储的日期转换为文本格式

数据挖掘应用中,字符数据出现得相当频繁。

在 R 程序中频繁出现。相应地,R 提供了一些

字符串操作工具,其中许多将在

在本章中介绍。

11.1 字符串操作函数概述

在这里,我们将简要回顾 R 提供的许多字符串操作函数中的一些。请注意,在本介绍中显示的调用形式非常简单,通常省略了许多可选参数。我们将在本章后面的扩展示例中使用一些这些参数,但请检查 R 的在线帮助以获取更多详细信息。

www.it-ebooks.info

11.1.1 grep()

调用 grep(pattern,x)在字符串向量 x 中搜索指定的子串模式。如果 x 有n个元素——即它包含n个字符串——那么 grep(pattern,x)将返回一个长度最多为n的向量。此向量的每个元素将是 x 中找到模式作为子串的索引。

这里是使用 grep 的一个示例:

grep("Pole",c("Equator","North Pole","South Pole"))

[1] 2 3

grep("pole",c("Equator","North Pole","South Pole")) integer(0)

在第一种情况下,字符串 "Pole" 被发现在第二个参数的第二个和第三个元素中,因此输出为 (2,3)。在第二种情况下,字符串 "pole"

在任何地方都没有找到,因此返回了一个空向量。

11.1.2 nchar()

nchar(x) 函数的调用用于查找字符串 x 的长度。以下是一个示例:

nchar("South Pole")

[1] 10

发现字符串 "South Pole" 有 10 个字符。C 程序员请注意:R 字符串没有 NULL 字符终止。

还要注意,如果 x 不在字符模式下,nchar() 的结果将不可预测。例如,nchar(NA) 的结果是 2,而 nchar(factor("abc")) 是 1。为了在非字符串对象上获得更一致的结果,请使用 CRAN 上的 Hadley Wickham 的 stringr 包。

11.1.3 paste()

paste(...) 函数的调用将多个字符串连接起来,并返回一个长字符串的结果。以下是一些示例:

paste("North","Pole")

[1] "North Pole"

paste("North","Pole",sep="")

[1] "NorthPole"

paste("North","Pole",sep=".")

[1] "North.Pole"

paste("North","and","South","Poles")

[1] "North and South Poles"

252

第十一章

www.it-ebooks.info

如您所见,可选参数 sep 可以用于在拼接在一起的片段之间放置除空格之外的内容。如果您指定 sep 为空字符串,则片段之间不会有任何字符。

11.1.4 sprintf()

sprintf(...) 函数的调用以格式化的方式组装字符串。

这里有一个简单的示例:

i <- 8

s <- sprintf("the square of %d is %d",i,i²)

s

[1] "the square of 8 is 64"

函数的名称旨在唤起“字符串打印”的“打印”

将其打印到字符串 s 而不是屏幕上。在这里,我们正在将打印到字符串 s。

我们打印的是什么?函数指示首先打印“平方”

然后打印 i 的十进制值。(这里的“十进制”意味着十进制数系统,而不是结果中会有小数点。)结果是字符串 "the square of 8 is 64."

11.1.5 substr()

substr(x,start,stop) 函数的调用返回给定字符串 x 中给定字符位置范围 start:stop 的子字符串。以下是一个示例:

substring("Equator",3,5)

[1] "uat"

11.1.6 strsplit()

strsplit(x,split) 函数的调用根据另一个字符串 split 在 x 中分割字符串,并将字符串 x 分割成 R 列表中的子字符串。以下是一个示例:

strsplit("6-16-2011",split="-")

[[1]]

[1] "6"

"16"

"2011"

11.1.7 regexpr()

regexpr(pattern,text) 函数的调用用于在文本中找到模式的第一实例的字符位置,如下例所示:

regexpr("uat","Equator")

[1] 3

字符串操作

253

www.it-ebooks.info

这报告了“uat”确实出现在“Equator”中,起始字符位置为 3。

11.1.8 gregexpr()

gregexpr(pattern,text) 函数的调用与 regexpr() 相同,但它找到模式的所有实例。以下是一个示例:

gregexpr("iss","Mississippi")

[[1]]

[1] 2 5

这发现 "iss" 在 "Mississippi" 中出现两次,起始字符位置为 2 和 5。

11.2 正则表达式

在处理编程语言中的字符串操作函数时,有时会涉及到 正则表达式 的概念。在 R 中,使用字符串函数 grep()、grepl()、regexpr()、gregexpr()、sub()、gsub() 和 strsplit() 时,你必须注意这一点。

正则表达式是一种通配符。它是指定字符串广泛类别的简写。例如,表达式 "[au]" 指的是包含字母 au 的任意字符串。你可以这样使用它:

grep("[au]",c("Equator","North Pole","South Pole"))

[1] 1 3

这报告说 ("Equator","North Pole","South Pole") 中的元素 1 和 3 —— 即 "Equator" 和 "South Pole" —— 包含 au

一个点 (.) 代表任意单个字符。这里是一个使用它的例子:

使用它:

grep("o.e",c("Equator","North Pole","South Pole"))

[1] 2 3

这将搜索由一个 o 后跟任何单个字符,然后是 e 的三个字符字符串。这里是一个使用两个点来表示任意字符对的示例:

grep("N..t",c("Equator","North Pole","South Pole"))

[1] 2

这里,我们搜索由 N 后跟任意两个字符,然后是 t 组成的四个字符字符串。

点是一个 元字符 的例子,它不是一个字面意义上的字符。例如,如果点出现在 grep() 的第一个参数中,它实际上并不代表点;它代表任意字符。

254

第十一章

www.it-ebooks.info

但是,如果你想要使用 grep() 搜索点,这里有一个简单的方法:

grep(".",c("abc","de","f.g"))

[1] 1 2 3

结果应该是 3,而不是 (1,2,3)。这次调用失败是因为点是一个元字符。你需要 转义 点的元字符性质,这通过反斜杠来完成:

grep("\.",c("abc","de","f.g"))

[1] 3

现在,我没有说 一个 反斜杠吗?那么为什么有两个?好吧,令人悲伤的真相是反斜杠本身必须被转义,这是通过它自己的反斜杠来完成的!这表明正则表达式可以多么神秘复杂。确实,已经有许多关于正则表达式(针对各种编程语言)的书籍被写出来。作为了解这个主题的起点,请参考 R 的在线帮助(输入 ?regex)。

11.2.1 扩展示例:测试文件名是否具有给定后缀

假设我们希望测试文件名中是否存在指定的后缀。例如,我们可能想要找到所有 HTML 文件(那些具有后缀 .html.htm 等)。以下是相应的代码:

1

testsuffix <- function(fn,suff) {

2

parts <- strsplit(fn,".",fixed=TRUE)

3

nparts <- length(parts[[1]])

4

return(parts[[1]][nparts] == suff)

5

}

让我们来测试一下。

testsuffix("x.abc","abc")

[1] TRUE

testsuffix("x.abc","ac")

[1] FALSE

testsuffix("x.y.abc","ac")

[1] FALSE

testsuffix("x.y.abc","abc")

[1] TRUE

函数是如何工作的?首先注意,第 2 行对 strsplit() 的调用返回一个包含一个元素的列表(因为 fn 是一个元素向量)——一个字符串向量。例如,调用 testsuffix("x.y.abc","abc") 将导致 parts 是一个包含三个元素(x, y, 和 abc)的向量列表。然后我们选择最后一个元素并将其与 suff 进行比较。

字符串操作

255

www.it-ebooks.info

一个关键方面是参数 fixed=TRUE。没有它,分割参数 .(在 strsplit() 的形式参数列表中称为 split)将被视为一个正则表达式。如果不设置 fixed=TRUE,strsplit() 就只会将所有字母分开。

当然,我们也可以转义点号,如下所示:

1

testsuffix <- function(fn,suff) {

2

parts <- strsplit(fn,"\.")

3

nparts <- length(parts[[1]])

4

return(parts[[1]][nparts] == suff)

5

}

让我们检查它是否仍然有效。

testsuffix("x.y.abc","abc")

[1] TRUE

这里是另一种执行后缀测试代码的方法,它稍微复杂一些,但是一个很好的说明:

1

testsuffix <- function(fn,suff) {

2

ncf <- nchar(fn) # nchar() 获取字符串长度

3

确定如果 suff 是 fn 中的后缀,则点号将从哪里开始 4

dotpos <- ncf - nchar(suff) + 1

5

现在检查 suff 是否存在

6

return(substr(fn,dotpos,ncf)==suff)

7

}

让我们再次看看 substr() 的调用,这里 fn = "x.ac" 且 suff = "abc"。在这种情况下,dotpos 将是 1,这意味着如果存在 abc 后缀,fn 中的第一个字符应该有点号。然后 substr() 的调用变为 substr("x.ac",1,4),它从 x.ac 中提取字符位置 1 到 4 的子字符串。这个子字符串将是 x.ac,它不是 abc,因此文件名的后缀没有被找到是后者。

11.2.2 扩展示例:形成文件名

假设我们想要创建五个文件,q1.pdfq5.pdf,这些文件包含 100 个随机 N(0, i 2) 变量的直方图。我们可以执行以下代码:

1

for (i in 1:5) {

2

fname <- paste("q",i,".pdf")

3

pdf(fname)

4

hist(rnorm(100,sd=i))

5

dev.off()

6

}

256

第十一章

www.it-ebooks.info

本例中的主要点是用于创建文件名 fname 的字符串操作。有关本例中使用的图形操作的更多详细信息,请参阅第 12.3 节。

paste() 函数将字符串 "q" 与数字 i 的字符串形式连接起来。例如,当 i = 2 时,变量 fname 将是 q2.pdf。

然而,这并不是我们想要的。在 Linux 系统上,包含空格的文件名会带来麻烦,因此我们想要删除空格。一种解决方案是使用 sep 参数,指定一个空字符串作为分隔符,如下所示:

1

for (i in 1:5) {

2

fname <- paste("q",i,".pdf",sep="")

pdf(fname)

4

hist(rnorm(100,sd=i))

5

dev.off()

6

}

另一种方法是使用从 C 中借来的 sprintf() 函数:1

for (i in 1:5) {

2

fname <- sprintf("q%d.pdf",i)

3

pdf(fname)

4

hist(rnorm(100,sd=i))

5

dev.off()

6

}

对于浮点数,还要注意 %f 和 %g 格式之间的区别:

sprintf("abc%fdef",1.5)

[1] "abc1.500000def"

sprintf("abc%gdef",1.5)

[1] "abc1.5def"

%g 格式消除了多余的零。

11.3 在 edtdbg 调试工具中使用字符串实用工具

edtdbg 调试工具的内部代码,将在第 13.4 节中讨论,大量使用了字符串实用工具。此类使用的典型示例是 dgbsendeditcmd() 函数:

向编辑器发送命令

dbgsendeditcmd <- function(cmd) {

syscmd <- paste("vim --remote-send ",cmd," --servername ",vimserver,sep="") system(syscmd)

}

字符串操作

257

www.it-ebooks.info

这里发生了什么?主要点是 edtdbg 向 Vim 文本编辑器发送远程命令。例如,如果你使用服务器名为 168 运行 Vim,并且想要将 Vim 中的光标移动到第 12 行,你可以在终端(shell)窗口中输入以下内容:

vim --remote-send 12G --servername 168

这种效果与你在 Vim 窗口中实际输入 12G 是一样的。由于 12G 是 Vim 命令,用于将光标移动到第 12 行,因此会发生这种情况。考虑以下调用:

paste("vim --remote-send ",cmd," --servername ",vimserver,sep="") 这里,cmd 是字符串 "12G",vimserver 是 168,paste() 连接所有指示的字符串。参数 sep="" 表示在此连接中使用空字符串作为分隔符——也就是说,没有分隔。因此,paste() 返回以下内容:

vim --remote-send 12G --servername 168

edtdbg 运作中的另一个核心元素是,程序通过调用 R 的 sink() 函数,安排将 R 调试器的大部分输出记录到名为 dbgsink 的文件中。(edtdbg 工具与该调试器协同工作。)这些信息包括你在使用 R 调试器逐步通过源文件时,你的位置所在的行号。

调试器输出中的行位置信息如下所示:debug at cities.r#16: {

因此,在 edtdbg 中有代码来确定 dbgsink 中以“debug at.”开头的最新行。然后,将该行作为一个字符串放入名为 debugline 的变量中。接下来的代码然后提取行号(示例中的 16)和源文件名/Vim 缓冲区名(这里为 cities.r):linenumstart <- regexpr("#",debugline) + 1

buffname <- substr(debugline,10,linenumstart-2)

colon <- regexpr(":",debugline)

linenum <- substr(debugline,linenumstart,colon-1)

regexpr() 的调用确定了 # 字符在 debugline 中的位置(本例中的第 18 个字符)。加 1 后给出行号在 debugline 中的位置。

258

第十一章

www.it-ebooks.info

要获取缓冲区名称,以先前的示例为指南,我们看到名称位于 debug 之后,并在 # 之前结束。由于“debug at”包含九个字符,缓冲区名称将从位置 10 开始——因此调用中的 10,

substr(debugline,10,linenumstart-2)

缓冲区名称字段的末尾位于 linenumstart-2,因为它位于 # 之前,而 # 位于行号开始之前。行号计算方式类似。

edtdbg 内部代码的另一个示例是它对 strsplit() 函数的使用。例如,在某个时刻,它向用户打印出一个提示:

kbdin <- readline(prompt="enter number(s) of fns you wish to toggle dbg: ") 如你所见,用户的响应存储在 kbdin 中。它将包含由空格分隔的一组数字,例如:

1 4 5

我们需要从字符串 1 4 5 中提取数字到一个整数向量中。这是首先通过 strsplit() 完成的,它产生了三个字符串:"1"、"4" 和 "5"。然后我们调用 as.integer() 将字符转换为数字:tognums <- as.integer(strsplit(kbdin,split=" ")[[1]])

注意到 strsplit() 的输出是一个 R 列表,在这种情况下,它包含一个元素,该元素是一个向量 ("1","4","5")。这导致了示例中的 [[1]] 表达式。

字符串操作

259

www.it-ebooks.info

www.it-ebooks.info

图像 24

12

图形

R 拥有一套非常丰富的图形功能。

R 的主页( http://www.r-project

.org/ ) 有一些色彩丰富的示例,但为了

真实地欣赏 R 的图形能力,浏览

R 图形画廊在 http://addictedtor.free

.fr/graphiques.

在本章中,我们介绍了使用 R 的基础或传统图形包的基本方法。这将为你提供足够的基礎,以便开始使用 R 进行图形操作。如果你对进一步学习 R 图形感兴趣,你可能需要参考该主题的优秀书籍。1

12.1 创建图形

首先,我们将查看创建图形的基础函数:plot()。

然后,我们将探讨如何构建图形,从添加线条和点到最后添加图例。

1 这些包括 Hadley Wickham 的 ggplot2: 数据分析中的优雅图形(纽约:Springer-Verlag,2009);Dianne Cook 和 Deborah F. Swayne 的 使用 R 和 GGobi 的交互式和动态数据图形分析(纽约:Springer-Verlag,2007);Deepayan Sarkar 的 Lattice: 使用 R 的多元数据可视化(纽约:Springer-Verlag,2008);以及 Paul Murrell 的 R Graphics(博卡雷顿,FL:Chapman and Hall/CRC,2011)。

www.it-ebooks.info

图像 25

12.1.1 R 基础图形的引擎:plot() 函数

plot()函数是 R 的基础绘图操作的基础,作为产生许多不同类型图表的工具。如第 9.1.1 节所述,plot()是一个泛型函数,或是一系列函数的占位符。实际调用的函数取决于被调用对象的类别。

让我们看看当我们用 X 向量和 Y 向量调用plot()时会发生什么

向量,这些在(x, y)平面上被解释为一组对。

plot(c(1,2,3), c(1,2,4))

这将弹出一个窗口,绘制点(1,1),(2,2)和(3,4),如图 12-1 所示。正如你所看到的,这是一个非常简单的图表。

我们将在本章后面讨论添加一些花哨的功能。

图 12-1:简单的点图

注意

图 12-1 中的点用空心圆表示。如果你想要 使用不同的字符类型,请为名为 pch 的命名参数指定一个值(对于 点字符 )。

plot()函数分阶段工作,这意味着你可以通过发出一系列命令逐步构建一个图表。例如,作为基础,我们可能首先绘制一个空图表,只包含坐标轴,如下所示:

plot(c(-3,3), c(-1,5), type = "n", xlab="x", ylab="y") 这绘制了标记为xy的坐标轴。水平轴(x)的范围从3

到 3。垂直轴(y)的范围从1 到 5。参数类型="n"表示图表本身没有任何内容。

262

第十二章

www.it-ebooks.info

Image 26

12.1.2 添加线条:abline()函数

我们现在有一个空图表,准备进入下一阶段,即添加线条:

x <- c(1,2,3)

y <- c(1,3,8)

plot(x,y)

lmout <- lm(y ~ x)

abline(lmout)

在调用plot()之后,图表将简单地显示三个点,以及带有刻度的xy轴。然后abline()调用向当前图表添加一条线。现在,这条线是哪一条?

如你在第 1.5 节所学,线性回归函数lm()的调用结果是一个包含拟合线的斜率和截距以及各种其他不在此处关心的量的类实例。我们将该类实例赋值给lmout。斜率和截距现在将在lmout$coefficients中。

那么,当我们调用abline()时会发生什么?这个函数只是简单地绘制

一条直线,函数的参数被视为直线的截距和斜率。例如,调用abline(c(2,1))将在你构建的任何图表上绘制这条线:

y = 2 + 1 · x

abline()被编写为在回归对象上调用时执行特殊操作(尽管令人惊讶,它不是一个泛型函数)。因此,它将从lmout$coefficients中获取所需的斜率和截距,并绘制这条线。它将这条线叠加到当前图表上,即绘制三个点的图表。换句话说,新的图表将显示点和线,如图 12-2 所示。

图 12-2:使用 abline()

图形

263

www.it-ebooks.info

你可以使用 lines() 函数添加更多线条。尽管有很多选项,但 lines() 的两个基本参数是 x 值的向量和一个 y 值的向量。这些被解释为 ( x, y) 对,表示要添加到当前图表中的点,并通过线条连接这些点。

例如,如果 X 和 Y 是向量 (1.5,2.5) 和 (3,3),你可以使用以下调用将一条从 (1.5,3) 到 (2.5,3) 的线添加到当前图表中:

lines(c(1.5,2.5),c(3,3))

如果你想要线条“连接点”,但又不想显示点本身,请在调用 lines() 或 plot() 时包含 type="l",如下所示:

plot(x,y,type="l")

你可以在 plot() 函数中使用 lty 参数来指定线的类型,例如实线或虚线。要查看可用的类型及其代码,请输入以下命令:

help(par)

12.1.3 在保留旧图的同时开始新图

每次你调用 plot(),无论是直接还是间接,当前的图表窗口都会被新的一个替换。如果你不希望发生这种情况,请使用适用于你操作系统的命令:

在 Linux 系统上,调用 X11()。

在 Mac 上,调用 macintosh()。

在 Windows 上,调用 windows()。

例如,假设你希望绘制向量 X 的两个直方图

并在 Linux 系统上并排查看它们。你会输入以下内容:

hist(x)

x11()

hist(y)

12.1.4 扩展示例:同一图上的两个密度估计

让我们绘制两个考试分数集合的非参数密度估计(这些基本上是平滑的直方图)在同一图上。我们使用 density() 函数生成估计。以下是我们要执行的命令:

d1 = density(testscores$Exam1,from=0,to=100)

d2 = density(testscores$Exam2,from=0,to=100)

264

第十二章

www.it-ebooks.info

Image 27

Image 28

plot(d1,main="",xlab="")

lines(d2)

首先,我们计算两个变量的非参数密度估计,并将它们保存在对象 d1 和 d2 中以供以后使用。然后我们调用 plot() 来绘制考试 1 的曲线,此时图表看起来像图 12-3。然后我们调用 lines() 来将考试 2 的曲线添加到图表中,生成图 12-4。

图 12-3:第一个密度估计的图

图 12-4:第二个密度估计的添加

图形

265

www.it-ebooks.info

注意,我们要求 R 使用空白标签为整个图和 x 轴设置标签。否则,R 会从 d1 获取这些标签,这些标签将特定于考试 1。

还要注意,我们首先绘制了考试 1 的图表。那里的分数多样性较低,因此密度估计较窄且较高。如果我们首先绘制了曲线较短的考试 2,那么考试 1 的曲线就会太高,无法在绘图窗口中显示。在这里,我们首先分别运行了两个图表,以查看哪个更高,但让我们考虑一个更普遍的情况。

假设我们希望编写一个广泛使用的函数,该函数可以在同一张图上绘制多个密度估计值。为此,我们需要自动化确定哪个密度估计值最高的过程。要做到这一点,我们可以使用这样一个事实:估计的密度值包含在调用 density()的返回值的 y 组件中。然后我们对每个密度估计值调用 max(),并使用 which.max()来确定哪个密度估计值是最高的。

The call to plot() both initiates the plot and draws the first curve. (Without specifying type="l", only the points would have been plotted.) The call to lines() then adds the second curve.

12.1.5 扩展示例:多项式回归示例的更多内容

在第 9.1.7 节中,我们定义了一个类 "polyreg",它简化了拟合多项式回归模型的过程。我们那里的代码包括了一个通用 print()函数的实现。现在让我们为通用 plot()函数添加一个:1

polyfit(x,maxdeg) 拟合所有最高到 maxdeg 度的多项式;y 是

2

响应变量的向量,x 为预测变量;创建一个包含 3 个对象的

类 "polyreg",由各种回归的输出组成 4

模型,以及原始数据

5

polyfit <- function(y,x,maxdeg) {

6

pwrs <- powers(x,maxdeg) # 形成预测变量的幂

7

lmout <- list() # start to build class

8

class(lmout) <- "polyreg" # 创建一个新的类

9

for (i in 1:maxdeg) {

10

lmo <- lm(y ~ pwrs[,1:i])

11

在这里扩展 lm 类,包括交叉验证的预测

12

lmo$fitted.xvvalues <- lvoneout(y,pwrs[,1:i,drop=F])

13

lmout[[i]] <- lmo

14

}

15

lmout$x <- x

16

lmout$y <- y

17

return(lmout)

18

}

19

20

对类 "polyreg" 的对象 fits 的通用 print():print 21

交叉验证均方预测误差

22

print.polyreg <- function(fits) {

23

maxdeg <- length(fits) - 2 # 仅计算 lm()的输出,不包括\(x 和\)y 266

第十二章

www.it-ebooks.info

24

n <- length(fits$y)

25

tbl <- matrix(nrow=maxdeg,ncol=1)

26

cat("均方预测误差,按度数\n")

27

colnames(tbl) <- "MSPE"

28

for (i in 1:maxdeg) {

29

fi <- fits[[i]]

30

errs <- fits\(y - fi\)fitted.xvvalues

31

spe <- sum(errs²)

32

tbl[i,1] <- spe/n

33

}

34

print(tbl)

35

}

36

37

通用 plot();绘制拟合值与原始数据

38

plot.polyreg <- function(fits) {

39

plot(fits\(x,fits\)y,xlab="X",ylab="Y") # 以背景形式绘制数据点 40

maxdg <- length(fits) - 2

41

cols <- c("red","green","blue")

42

dg <- curvecount <- 1

43

while (dg < maxdg) {

44

prompt <- paste("RETURN for XV fit for degree",dg,"or type degree", 45

"或 q 退出 ")

46

rl <- readline(prompt)

47

dg <- if (rl == "") dg else if (rl != "q") as.integer(rl) else break 48

lines(fits\(x,fits[[dg]]\)fitted.values,col=cols[curvecount%%3 + 1])

49

dg <- dg + 1

50

curvecount <- curvecount + 1

51

}

52

}

53

54

forms matrix of powers of the vector x, through degree dg

55

powers <- function(x,dg) {

56

pw <- matrix(x,nrow=length(x))

57

prod <- x

58

for (i in 2:dg) {

59

prod <- prod * x

60

pw <- cbind(pw,prod)

61

}

62

return(pw)

63

}

64

65

finds cross-validated predicted values; could be made much faster via 66

matrix-update methods

67

lvoneout <- function(y,xmat) {

68

n <- length(y)

69

predy <- vector(length=n)

70

for (i in 1:n) {

Graphics

267

www.it-ebooks.info

71

regress, leaving out ith observation

72

lmo <- lm(y[-i] ~ xmat[-i,])

73

betahat <- as.vector(lmo$coef)

74

the 1 accommodates the constant term

75

predy[i] <- betahat %*% c(1,xmat[i,])

76

}

77

return(predy)

78

}

79

80

polynomial function of x, coefficients cfs

81

poly <- function(x,cfs) {

82

val <- cfs[1]

83

prod <- 1

84

dg <- length(cfs) - 1

85

for (i in 1:dg) {

86

prod <- prod * x

87

val <- val + cfs[i+1] * prod

88

}

89

}

As noted, the only new code is plot.polyreg(). For convenience, the code is reproduced here:

generic plot(); plots fits against raw data

plot.polyreg <- function(fits) {

plot(fits\(x,fits\)y,xlab="X",ylab="Y") # plot data points as background maxdg <- length(fits) - 2

cols <- c("red","green","blue")

dg <- curvecount <- 1

while (dg < maxdg) {

prompt <- paste("RETURN for XV fit for degree",dg,"or type degree",

"or q for quit ")

rl <- readline(prompt)

dg <- if (rl == "") dg else if (rl != "q") as.integer(rl) else break lines(fits\(x,fits[[dg]]\)fitted.values,col=cols[curvecount%%3 + 1])

dg <- dg + 1

curvecount <- curvecount + 1

}

}

As before, our implementation of the generic function takes the name of the class, which is plot.polyreg() here.

The while loop iterates through the various polynomial degrees. We

cycle through three colors, by setting the vector cols; note the expression curvecount %%3 for this purpose.

268

Chapter 12

www.it-ebooks.info

Image 29

The user can choose either to plot the next sequential degree or select a different one. The query, both user prompt and reading of the user’s reply, is done in this line:

rl <- readline(prompt)

We use the R string function paste() to assemble a prompt, offering the user a choice of plotting the next fitted polynomial, plotting one of a different degree, or quitting. The prompt appears in the interactive R window in which we issued the plot() call. For instance, after taking the default choice twice, the command window looks like this:

plot(lmo)

RETURN for XV fit for degree 1 or type degree or q for quit

RETURN for XV fit for degree 2 or type degree or q for quit

RETURN for XV fit for degree 3 or type degree or q for quit

The plot window looks like Figure 12-5.

Figure 12-5: Plotting a polynomial fit

12.1.6 Adding Points: The points() Function

The points() function adds a set of (x, y) points, with labels for each, to the currently displayed graph. For instance, in our first example, suppose we entered this command:

points(testscores\(Exam1,testscores\)Exam3,pch="+")

Graphics

269

www.it-ebooks.info

结果将是将示例中的考试成绩点叠加到当前图上,使用加号(+)标记它们。

与大多数其他图形函数一样,有许多选项,例如点颜色和背景颜色。例如,如果您想要黄色背景,请输入以下命令:

par(bg="yellow")

现在,您的图表将具有黄色背景,直到您指定

otherwise.

与其他函数一样,为了探索众多选项,请输入以下内容:

help(par)

12.1.7 添加图例:legend() 函数

毫不意外,legend() 函数用于向多曲线图添加图例。这可以告诉观众类似的信息,例如,“绿色曲线代表男性,红色曲线显示女性的数据。”输入以下内容以查看一些示例:

example(legend)

12.1.8 添加文本:text() 函数

使用 text() 函数将文本放置在当前图的任何位置。

这里有一个示例:

text(2.5,4,"abc")

这将在图中的点 (2.5,4) 处写入文本“abc”。在这种情况下,字符串的中心,“b”,将位于该点。

为了看到一个更实际的示例,让我们向我们的考试成绩图中的曲线添加一些标签,如下所示:

text(46.7,0.02,"Exam 1")

text(12.3,0.008,"Exam 2")

结果如图 12-6 所示。

为了将某个字符串放置在您想要的确切位置,您可能需要进行一些试错。或者,您可能会发现 locator() 函数是一个更快的方法,如下一节所述。

270

第十二章

www.it-ebooks.info

Image 30

图 12-6:放置文本

12.1.9 精确定位位置:locator() 函数

将文本精确放置在您希望的位置可能很棘手。您可以通过反复尝试不同的 x-和 y-坐标直到找到一个好的位置,但 locator() 函数可以为您节省很多麻烦。您只需调用该函数,然后在图中的所需位置点击鼠标。该函数返回 x-

y-坐标的点击点。具体来说,输入以下内容将告诉 R 你将在图中的一个位置点击:

locator(1)

一旦点击,R 将告诉您您点击的点的确切坐标。调用 locator(2) 获取两个位置的位置,依此类推。(警告:请确保包含参数。)

这里有一个简单的示例:

hist(c(12,5,13,25,16))

locator(1)

$x

[1] 6.239237

$y

[1] 1.221038

图形

271

www.it-ebooks.info

这将让 R 绘制直方图,然后使用参数 1 调用 locator(),表示我们将点击鼠标一次。点击后,函数返回一个包含 x 和 y 成分的列表,即我们点击点的 x-和 y-坐标。

要使用此信息放置文本,请将其与 text() 函数结合使用:

text(locator(1),"nv=75")

在这里,text()期望一个x坐标和一个y坐标,指定绘制文本“nv=75.”的点。locator()的返回值提供了这些坐标。

12.1.10 恢复图形

R 没有“撤销”命令。然而,如果你在构建图形时怀疑你可能需要撤销下一步,你可以使用 recordPlot()保存它,然后稍后使用 replayPlot()恢复它。

不太正式但更方便的是,你可以将所有命令

你正在使用它来在文件中构建图形,然后使用 source()或使用鼠标剪切和粘贴来执行它们。如果你更改一个命令,你可以通过 source()或复制粘贴你的文件来重新绘制整个图形。

例如,对于我们的当前图形,我们可以创建一个名为

examplot.R 包含以下内容:

d1 = density(testscores$Exam1,from=0,to=100)

d2 = density(testscores$Exam2,from=0,to=100)

plot(d1,main="",xlab="")

lines(d2)

text(46.7,0.02,"Exam 1")

text(12.3,0.008,"Exam 2")

如果我们决定考试 1 的标签稍微偏右了一些,我们可以编辑文件,然后执行以下操作之一:

source("examplot.R")

12.2 自定义图形

你已经看到了如何通过 plot()逐步构建简单图形,非常容易。

现在,你可以开始使用 R 的许多选项来增强这些图形。

提供。

12.2.1 改变字符大小:cex 选项

cex(代表字符扩展)函数允许你在图形内扩展或缩小字符,这非常有用。你可以将其用作各种绘图函数中的命名272

第十二章

www.it-ebooks.info

参数,以在文件中构建图形,然后使用 source(),或者使用鼠标剪切和粘贴来执行它们。如果你更改一个命令,你可以通过 source()或复制粘贴你的文件来重新绘制整个图形。

text(2.5,4,"abc",cex = 1.5)

这将打印出与早期示例相同的文本,但字符大小是正常大小的 1.5 倍。

12.2.2 改变坐标轴范围:xlim 和 ylim 选项

你可能希望你的图形的x轴和y轴的范围比默认范围更宽或更窄。如果你将在同一图形中显示多个曲线,这特别有用。

你可以通过在 plot()或 points()调用中指定 xlim 和/或 ylim 参数来调整坐标轴。例如,ylim=c(0,90000)指定y轴的范围为 0 到 90,000。

如果你有几个曲线,并且没有指定 xlim 和/或 ylim,你应该先绘制最高的曲线,以便为所有曲线留出空间。否则,R 将根据你首先绘制的曲线调整图形,然后在上部截断较高的曲线!

我们之前采取了这种方法,当时我们在同一图形上绘制了两个密度估计(图 12-3 和 12-4)。相反,我们首先可以找到两个密度估计的最高值。对于 d1,我们找到以下内容:

d1

调用:

density.default(x = testscores$Exam1, from = 0, to = 100)

数据:testscores$Exam1(39 个观测值);

带宽 'bw' = 6.967

x

y

最小值
0

最小值

:1.423e-07

第一四分位数:25

第一四分位数:1.629e-03

中位数:50

中位数:9.442e-03

平均值
50

平均值

:9.844e-03

第三四分位数:75

第三四分位数:1.756e-02

最大值.

:100

最大值

:2.156e-02

因此,最大的 y 值是 0.022。对于 d2,它只有 0.017。这意味着如果我们把 ylim 设置为 0.03,我们就有足够的空间。以下是我们在同一张图上绘制两个图形的方法:

plot(c(0, 100), c(0, 0.03), type = "n", xlab="score", ylab="density")

lines(d2)

lines(d1)

图形

273

www.it-ebooks.info

Image 31

Image 32

我们首先绘制了裸骨图——仅坐标轴,没有内部结构,如图 12-7 所示。plot() 的前两个参数给出 xlim 和 ylim,因此 Y 轴的下限和上限将是 0 和 0.03。然后调用 lines() 两次填充图形,得到图 12-8 和 12-9。(两个 lines() 调用中的任何一个都可以先进行,因为我们留下了足够的空间。)

图 12-7:仅坐标轴

图 12-8:d2 的加法

274

第十二章

www.it-ebooks.info

Image 33

图 12-9:d1 的加法

12.2.3 添加多边形:polygon() 函数

您可以使用 polygon() 绘制任意多边形对象。例如,以下代码绘制了函数 f ( x) = 1 − e−x 的图形,然后添加了一个近似曲线下从 x = 1.2 到 x = 1.4 区域的矩形。

f <- function(x) return(1-exp(-x))

curve(f,0,2)

polygon(c(1.2,1.4,1.4,1.2),c(0,0,f(1.3),f(1.3)),col="gray") 结果显示在图 12-10 中。

在 polygon() 的调用中,第一个参数是矩形的 x 坐标集合,第二个参数指定 y 坐标。第三个参数指定在这种情况下矩形应以实灰色填充。

作为另一个例子,我们可以使用密度参数用条纹填充矩形。此调用指定每英寸 10 条线:

polygon(c(1.2,1.4,1.4,1.2),c(0,0,f(1.3),f(1.3)),density=10)

图形

275

www.it-ebooks.info

Image 34

图 12-10:矩形区域条纹

12.2.4 平滑点:lowess() 和 loess() 函数

仅绘制点云,无论是否连接,可能只会给你一个无信息性的混乱。在许多情况下,通过拟合非参数回归估计量(如 lowess())来平滑数据会更好。

让我们为我们的考试成绩数据做这个。我们将绘制考试 2 的分数

与考试 1 的分数进行比较:

plot(testscores)

lines(lowess(testscores))

结果显示在图 12-11 中。

lowess() 的一个较新的替代方法是 loess()。这两个函数相似,但默认值和其他选项不同。您需要一些高级的统计知识来欣赏它们之间的差异。使用您认为能提供更好平滑效果的函数。

12.2.5 显式函数绘图

假设你想绘制函数 g( t) = ( t 2 + 1)0 . 5 在 t 介于 0 和 5 之间的图形。

您可以使用以下 R 代码:

g <- function(t) { return (t²+1)⁰.5 } # 定义 g()

x <- seq(0,5,length=10000) # x = [0.0004, 0.0008, 0.0012,..., 5]

y <- g(x) # y = [g(0.0004), g(0.0008), g(0.0012), ..., g(5)]

绘制 x 和 y,类型为 "l"

276

第十二章

www.it-ebooks.info

Image 35

图 12-11:平滑考试成绩关系

但您可以通过使用 curve() 函数来避免一些工作,该函数基本上使用相同的方法:

绘制曲线((x²+1)⁰.5,0,5)

如果您要将此曲线添加到现有图中,请使用 add 参数:

curve((x²+1)⁰.5,0,5,add=T)

可选参数 n 的默认值为 101,这意味着函数将在 x 的指定范围内以 101 个等间距的点进行评估。

仅使用足够多的点以获得视觉平滑度。如果您发现 101 个点不够,请尝试更高的 n 值。

您也可以使用 plot(),如下所示:

f <- function(x) return((x²+1)⁰.5)

绘制 f,0,5 # 参数必须是函数名

在这里,调用 plot() 导致调用 plot.function(),这是函数类通用的 plot() 函数的实现。

再次,方法由您选择;使用您喜欢的任何一种。

12.2.6 扩展示例:放大曲线的一部分

在您使用 curve() 绘制函数图后,您可能希望“放大”曲线的一部分。您可以通过在 Graphics 上再次调用 curve() 来实现这一点。

277

www.it-ebooks.info

同样的函数,但具有限制的 x 范围。但假设您希望在同一张图中显示原始图和放大图。在这里,我们将开发一个函数,我们将其命名为 inset() 来完成此操作。

为了避免重复 curve() 在绘制原始图形时所做的所有工作,我们将稍微修改其代码以保存这项工作,通过返回值来实现。我们可以通过利用您可以轻松检查用 R 编写的 R 函数的代码(与用 C 编写的 R 的基本函数相反)来实现这一点,如下所示:

1

curve

2

function (expr, from = NULL, to = NULL, n = 101, add = FALSE,

3

type = "l", ylab = NULL, log = NULL, xlim = NULL, ...)

4

{

5

sexpr <- substitute(expr)

6

if (is.name(sexpr)) {

7

...此处省略大量行...

8

x <- if (lg != "" && "x" %in% strsplit(lg, NULL)[[1]]) {

9

if (any(c(from, to) <= 0))

10

停止错误("'from' 和 'to' 必须大于 0,当 log="x" 时") 11

exp(seq.int(log(from), log(to), length.out = n))

12

}

13

否则 seq.int(from, to, length.out = n)

14

y <- eval(expr, envir = list(x = x), enclos = parent.frame())

15

if (add)

16

lines(x, y, type = type, ...)

17

否则绘制 x 和 y,类型为 type,y 轴标签为 ylab,x 轴限制为 xlim,log 为 lg,... 18

}

代码形成向量 x 和 y,包含要绘制的曲线的 x 和 y 坐标,在 x 的范围内以 n 个等间距的点。由于我们将在 inset() 中使用这些,让我们修改此代码以返回 x 和 y。

这是修改后的版本,我们将其命名为 crv():

1

crv

2

function (expr, from = NULL, to = NULL, n = 101, add = FALSE,

3

type = "l", ylab = NULL, log = NULL, xlim = NULL, ...)

4

{

5

sexpr <- substitute(expr)

6

if (is.name(sexpr)) {

7

...此处省略大量行...

8

x <- if (lg != "" && "x" %in% strsplit(lg, NULL)[[1]]) {

9

if (any(c(from, to) <= 0))

10

stop("'from' and 'to' must be > 0 with log="x"") 11

exp(seq.int(log(from), log(to), length.out = n))

12

}

13

else seq.int(from, to, length.out = n)

14

y <- eval(expr, envir = list(x = x), enclos = parent.frame())

15

if (add)

278

第十二章

www.it-ebooks.info

16

lines(x, y, type = type, ...)

17

else plot(x, y, type = type, ylab = ylab, xlim = xlim, log = lg, ...) 18

return(list(x=x,y=y)) # 这是唯一的修改

19

}

现在我们可以进入我们的 inset()函数。

1

savexy: 由 crv()返回的 x 和 y 向量组成的列表

2

x1,y1,x2,y2: 要放大的矩形区域的坐标

3

x3,y3,x4,y4: 嵌入区域的坐标

4

inset <- function(savexy,x1,y1,x2,y2,x3,y3,x4,y4) {

5

rect(x1,y1,x2,y2) # 在要放大的区域周围绘制矩形

6

rect(x3,y3,x4,y4) # 在嵌入区域周围绘制矩形

7

获取先前绘制点的坐标向量

8

savex <- savexy$x

9

savey <- savexy$y

10

获取要放大的 xi 范围的子索引

11

n <- length(savex)

12

xvalsinrange <- which(savex >= x1 & savex <= x2)

13

yvalsforthosex <- savey[xvalsinrange]

14

检查我们的第一个框是否包含该 X 范围的整个曲线 15

if (any(yvalsforthosex < y1 | yvalsforthosex > y2)) {

16

print("Y value outside first box")

17

return()

18

}

19

记录一些差异

20

x2mnsx1 <- x2 - x1

21

x4mnsx3 <- x4 - x3

22

y2mnsy1 <- y2 - y1

23

y4mnsy3 <- y4 - y3

24

对于原始曲线的第 i 个点,函数 plotpt()将 25

计算此点在嵌入曲线中的位置

26

plotpt <- function(i) {

27

newx <- x3 + ((savex[i] - x1)/x2mnsx1) * x4mnsx3

28

newy <- y3 + ((savey[i] - y1)/y2mnsy1) * y4mnsy3

29

return(c(newx,newy))

30

}

31

newxy <- sapply(xvalsinrange,plotpt)

32

lines(newxy[1,],newxy[2,])

33

}

让我们试试。

xyout <- crv(exp(-x)*sin(1/(x-1.5)),0.1,4,n=5001)

inset(xyout,1.3,-0.3,1.47,0.3, 2.5,-0.3,4,-0.1)

生成的图表看起来像图 12-12。

图形

279

www.it-ebooks.info

图像 36

图 12-12:添加嵌入图表

12.3 将图表保存到文件

R 图形显示可以由各种图形设备组成。默认设备是屏幕。如果您想将图表保存到文件,您必须设置另一个设备。

让我们首先了解 R 图形设备的基础,以介绍 R

图形设备概念,然后讨论第二种更直接、更方便的方法。

12.3.1 R 图形设备

让我们打开一个文件:

pdf("d12.pdf")

这打开了文件d12.pdf。我们现在有两个设备打开,正如我们可以确认的那样:

dev.list()

X11 pdf

2

3

280

第十二章

www.it-ebooks.info

当 R 在 Linux 上运行时,屏幕被命名为 X11。(在 Windows 系统上被命名为 windows。)在这里,它是设备号 2。我们的 PDF 文件是设备号 3。我们的活动设备是 PDF 文件:

dev.cur()

pdf

3

所有图形输出现在将发送到这个文件而不是屏幕。但如果我们希望保存屏幕上已有的内容呢?

12.3.2 保存显示的图形

保存当前屏幕上显示的图形的一种方法是将屏幕重新设置为当前设备,然后将其复制到 PDF 设备,在我们的例子中是 3,如下所示:

dev.set(2)

X11

2

dev.copy(which=3)

pdf

3

但实际上,最好像前面所示设置一个 PDF 设备,然后重新运行导致当前屏幕的任何分析。这是因为复制操作可能会由于屏幕设备和文件设备之间的不匹配而产生扭曲。

12.3.3 关闭 R 图形设备

注意,我们创建的 PDF 文件在关闭之前是不可用的,我们按照以下方式关闭:

dev.set(3)

pdf

3

dev.off()

X11

2

如果你完成与 R 的工作,你也可以通过退出 R 来关闭设备。但在 R 的未来版本中,这种行为可能不存在,所以最好是主动关闭。

图形

281

www.it-ebooks.info

图像 37

12.4 创建三维图形

R 提供了一些函数来绘制三维数据,如 persp()和 wireframe(),它们绘制表面,以及 cloud(),它绘制三维散点图。在这里,我们将查看一个使用 wireframe()的简单示例。

library(lattice)

a <- 1:10

b <- 1:15

eg <- expand.grid(x=a,y=b)

eg\(z <- eg\)x² + eg\(x * eg\)y

wireframe(z ~ x+y, eg)

首先,我们加载了 lattice 库。然后,调用 expand.grid()创建了一个数据框,包含名为 x 和 y 的两列,包含两个输入值的所有可能组合。在这里,a 和 b 分别有 10 和 15 个值,所以结果数据框将有 150 行。(注意,输入 wireframe()的数据框不需要由 expand.grid()创建。)

我们然后添加了第三列,命名为 z,作为前两列的函数。我们的 wireframe()调用创建了图形。以回归模型形式给出的参数指定 z 要相对于 x 和 y 进行绘图。当然,z、x 和 y 指的是 eg 中的列名。结果如图 12-13 所示。

图 12-13:使用 wireframe()的示例

282

第十二章

www.it-ebooks.info

所有点都连接成一个表面(就像在二维中通过线连接点一样)。相比之下,使用 cloud()时,点则是孤立的。

对于 wireframe(),(x, y)对必须形成一个矩形网格,尽管不一定均匀分布。

三维绘图函数有许多不同的选项。

例如,对于 wireframe() 函数,一个不错的选项是 shade=T,这使得数据更容易看到。许多函数,一些具有详细选项,以及全新的图形包,在比 R 的基础图形包更高的(即“更方便、更强大”)抽象级别上工作。有关更多信息,请参阅本章开头脚注 1 中引用的书籍。

图形

283

www.it-ebooks.info

www.it-ebooks.info

Image 38

13

DEBUGGING

程序员经常发现,他们花在调试程序上的时间

调试程序比

实际编写它。良好的调试技能

是无价的。在本章中,我们将讨论

R 中的调试。

13.1 调试的基本原则

谨防上述代码中的错误;我仅证明它是正确的,

尚未尝试。

——计算机科学先驱唐纳德·克努特

虽然调试是一种艺术而不是科学,但它涉及一些基本原理。在这里,我们将探讨一些调试的最佳实践。

13.1.1 调试的本质:确认原则

正如皮特·萨尔茨曼和我在我们关于调试的书中所说,《调试的艺术:使用 GDB、DDD 和 Eclipse》(No Starch Press,2008 年),确认原则是调试的本质。

www.it-ebooks.info

修复有缺陷的程序是一个逐一确认的过程,即确认你关于代码的许多你认为正确的事情实际上确实是正确的。当你发现你的某个假设不正确时,你就找到了关于错误位置(如果不是错误的本质)的线索。

一个错误。

这也可以说成,“惊喜是好的!”例如,假设你有以下代码:

x <- y² + 3*g(z,2)

w <- 28

if (w+q > 0) u <- 1 else v <- 10

你认为变量 x 被赋值后其值应该是 3 吗?

确认它!你认为第三行的 else 会执行,而不是 if 吗?确认它!

最终,你如此确信的这些断言中之一将证明是错误的。然后你将确定错误的可能位置,从而让你能够专注于错误的本质。

13.1.2 从小开始

至少在调试过程的开始阶段,坚持使用小而简单的测试用例。处理大型数据对象可能会使思考问题变得更难。

当然,你最终应该在大型、复杂的案例上测试你的代码,但要从小开始。

13.1.3 以模块化、自顶向下的方式调试

大多数优秀的软件开发者都认为代码应该以模块化的方式编写。你的第一级代码不应超过,比如说,十几行,其中大部分是函数调用。而且这些函数不应太长,如果需要,应该调用其他函数。这使得在编写阶段更容易组织代码,在代码需要扩展时也更容易被他人理解。

你也应该以自顶向下的方式进行调试。假设你已经设置了函数 f() 的调试状态(即,你已经调用了 debug(f),稍后将会解释),并且 f() 包含以下这行代码:

y <- g(x,8)

你应该对 g() 采用“无罪推定”的方法。不要立即调用 debug(g)。执行该行并查看 g() 是否返回你期望的值。如果它确实返回了,那么你刚刚避免了单步调试 g() 的耗时过程。如果 g() 返回了错误的值,那么现在是调用 debug(g) 的时候了。

286

第十三章

www.it-ebooks.info

13.1.4 抗错误调试

你也可以采用一些“抗错误”策略。假设你有一段代码,其中变量 x 应该是正数。你可以插入以下这行代码:

stopifnot(x > 0)

如果代码中较早的地方有一个错误,使得 x 等于,比如说, 12,那么 stopifnot() 的调用将立即停止,并显示如下错误信息:

错误:x > 0 不为真

(C 程序员可能会注意到这与 C 的 assert 语句的相似性。)

修复错误并测试新代码后,你可能想保留这段代码,以便稍后检查错误是否以某种方式再次出现。

13.2 为什么使用调试工具?

在过去,程序员会通过临时在代码中插入打印语句并重新运行程序来执行调试确认过程,以查看打印了什么。例如,为了确认我们之前代码中的 x = 3,我们会在代码中插入一个打印 x 值的语句,并对 if-else 做类似处理,如下所示:x <- y² + 3*g(z,2)

cat("x =",x,"\n")

w <- 28

if (w+q > 0) {

u <- 1

print("the 'if' was done")

} else {

v <- 10

print("the 'else' was done")

}

我们会重新运行程序并检查打印出的反馈。然后我们会移除打印语句并插入新的语句以追踪下一个错误。

这种手动过程对于一两个循环来说是可以的,但在长时间的调试会话中会变得非常繁琐。更糟糕的是,所有这些编辑工作都会分散你的注意力,使你更难集中精力寻找错误。

调试

287

www.it-ebooks.info

因此,通过在代码中插入打印语句进行调试是缓慢的、繁琐的,并且会分散注意力。如果你对任何特定的编程语言认真负责,你应该寻找该语言的优秀调试工具。

使用调试工具将使查询变量值、检查是否执行了 if 或 else 等操作变得容易得多。此外,如果你的错误导致执行错误,调试工具可以为你分析它,可能提供关于错误来源的重要线索。所有这些都将大大提高你的生产力。

13.3 使用 R 调试工具

R 的基础包包含了一些调试功能,还有更多功能性的调试包也可用。我们将讨论基础功能和其它包,我们的扩展示例将展示一个完整的调试会话。

13.3.1 使用 debug() 和 browser() 函数进行单步执行

R 的调试功能的核心是 browser。它允许你逐行单步执行你的代码,同时可以随时查看代码。

你可以通过调用 debug() 或 browser() 函数来调用浏览器。

R 的调试功能是针对单个函数的。如果你认为你的函数 f() 中存在错误,你可以通过调用 debug(f) 来设置函数 f() 的调试状态。这意味着从那时起,每次调用该函数时,你将自动进入函数的开始处的浏览器。调用 undebug(f) 将取消函数的调试状态,这样进入函数将不再调用浏览器。

另一方面,如果你在 f() 函数中的某行放置了对 browser() 的调用,浏览器只有在执行到达该行时才会被调用。然后你可以单步执行你的代码,直到退出函数。如果你认为错误的位置不在函数的开始附近,你可能不想从开始处单步执行,因此这种方法更为直接。

使用过 C 调试器(如 GDB,GNU 调试器)的读者

在这里你会找到相似之处,但也有一些方面可能会让你感到惊讶。例如,正如所注,debug() 是在函数级别上调用,而不是在整体程序级别上。如果你认为你的几个函数中存在错误,你需要对每个函数都调用 debug()。

当你只想为 f() 函数进行一次调试会话时,调用 debug(f) 然后 undebug(f) 可能会变得繁琐。从 R 2.10 版本开始,现在可以调用 debugonce();调用 debugonce(f) 将 f() 函数置于调试状态,首次执行时,但该状态会在退出函数后立即反转。

288

第十三章

www.it-ebooks.info

13.3.2 使用浏览器命令

当你在浏览器中时,提示符会从 > 变为 Browse[d] >

(在此,d 表示调用链的深度。)你可以在该提示符下提交以下任何命令:

n(对于 next):告诉 R 执行下一行,然后再次暂停。按回车键也会执行此操作。

c(对于 continue):这与 n 类似,但可能在下一次暂停之前执行多行代码。如果你当前在一个循环中,此命令将导致循环的其余部分被执行,然后从循环退出时暂停。如果你在一个函数中但不在循环中,函数的其余部分将在下一次暂停之前被执行。

任何 R 命令:在浏览器中,您仍然处于 R 的交互模式,因此可以通过简单地输入 x 来查询变量的值。当然,如果您有一个与浏览器命令同名变量,您必须显式调用类似 print()的东西,例如 print(n)。

其中:这将打印一个堆栈跟踪。它显示了导致执行到达当前位置的函数调用序列。

Q: 这将退出浏览器,带您回到 R 的主交互模式。

13.3.3 设置断点

调用 debug(f)将在 f()的开始处放置一个 browser()调用。然而,在某些情况下,这可能是一个过于粗糙的工具。如果您怀疑错误在函数的中间,逐行遍历所有中间代码是浪费时间的。

解决方案是在代码的某些关键位置设置断点

您想要执行暂停的地方。在 R 中如何做到这一点?

您可以直接调用 browser 或使用 setBreakpoint()函数(R 版本 2.10 及以后)。

13.3.3.1 直接调用 browser()

您可以通过在代码中感兴趣的地方插入 browser()调用来设置断点。这基本上具有设置断点的效果。

您可以将调用浏览器的条件设置为仅在指定情况下执行。使用 expr 参数来定义这些情况。例如,假设您怀疑您的错误仅在某个变量 s 大于 1 时出现。您可以使用以下代码:

browser(s > 1)

调试

289

www.it-ebooks.info

只有当 s 大于 1 时,浏览器才会被调用。以下会有相同的效果:

if (s > 1) browser()

直接调用 browser,而不是通过 debug()进入调试器,在您有一个许多迭代的循环并且错误仅在,比如说,第 50 次迭代后出现的情况下非常有用。如果循环索引是 i,那么您可以编写以下内容:

if (i > 49) browser()

这样,您就可以避免逐行执行前 49 次迭代的无聊。

迭代!

13.3.3.2 使用 setBreakpoint()函数

从 R 2.10 版本开始,您可以使用 setBreakpoint()格式

setBreakpoint( filename,linenumber)

这将在我们的源文件filename的第linenumber行调用 browser()。

这在您正在使用调试器,逐行单步执行代码时特别有用。比如说,您目前位于源文件x.R的第 12 行,并想在第 28 行设置一个断点。您不必退出调试器,在第 28 行添加对 browser()的调用,然后重新进入函数,您只需简单地输入以下内容:

setBreakpoint("x.R",28)

然后,您可以在调试器中继续执行,例如通过发出 c 命令。

setBreakpoint() 函数通过调用下一节中讨论的 trace() 函数来工作。因此,要取消断点,您需要取消跟踪。例如,如果我们曾在函数 g() 的某一行调用 setBreakpoint(),我们可以通过输入以下内容来取消断点:

untrace(g)

您可以在是否在调试器中时调用 setBreakpoint()。如果您当前没有运行调试器,并且执行受影响的函数并在执行过程中遇到断点,您将自动进入浏览器。这与 browser() 的情况类似,但使用这种方法,您可以省去通过文本编辑器更改代码的麻烦。

290

第十三章

www.it-ebooks.info

13.3.4 使用 trace() 函数进行跟踪

trace() 函数灵活且功能强大,尽管学习它需要一些初始努力。我们将在以下内容中讨论一些简单的用法形式,从以下内容开始:

trace(f,t)

此调用指示 R 在每次进入函数 f() 时调用函数 t()。例如,如果我们希望在函数 gy() 的开始处设置断点,我们可以使用以下命令:

trace(gy,browser)

这与在 gy() 的源代码中放置命令 browser() 有相同的效果,但它比插入这样的行、保存文件和重新运行 source() 来加载新版本的文件更快、更方便。调用 trace() 不会更改您的源文件,尽管它会更改 R 维护的临时文件版本。通过简单地运行 untrace,也可以更快、更方便地撤销操作。

untrace(gy)

您可以通过调用 tracingState() 来全局开启或关闭跟踪,使用参数 TRUE 开启,FALSE 关闭。

13.3.5 使用 traceback() 和...在崩溃后执行检查

调试器() 函数

假设您的 R 代码在没有运行调试器的情况下崩溃。在事后,您仍然可以使用调试工具。您可以通过简单地调用 traceback() 来进行“尸检”。它将告诉您问题发生在哪个函数中,以及导致该函数的调用链。

如果您设置 R 在崩溃时转储框架,您可以获得更多信息:

options(error=dump.frames)

如果您已经这样做,那么在崩溃后,运行以下命令:

debugger()

您将看到可以选择查看函数调用级别的选项。对于您选择的每个级别,您都可以查看那里的变量值。浏览完一个级别后,您可以通过按 N 键返回到 debugger() 主菜单。

调试

291

www.it-ebooks.info

您可以通过编写以下代码来安排自动进入调试器:

options(error=recover)

注意,尽管如此,如果您选择这条自动路径,它将带您进入调试器,即使您只是有一个语法错误(这不是进入调试器的好时机)。

要关闭任何这种行为,请输入以下内容:

options(error=NULL)

你将在下一节中看到这种方法的演示。

13.3.6 扩展示例:两个完整的调试会话

现在我们已经了解了 R 的调试工具,让我们尝试使用它们来查找和修复代码问题。我们将从一个简单的例子开始,然后过渡到一个更复杂的例子。

13.3.6.1 调试:寻找连续 1 的序列

首先,回忆一下我们在第二章中扩展的寻找连续 1 的序列的例子。以下是代码的一个有缺陷版本:

1

findruns <- function(x,k) {

2

n <- length(x)

3

runs <- NULL

4

for (i in 1:(n-k)) {

5

if (all(x[i:i+k-1]==1)) runs <- c(runs,i)

6

}

7

return(runs)

8

}

让我们在一个小测试用例上试一试:

source("findruns.R")

findruns(c(1,0,0,1,1,0,1,1,1),2)

[1] 3 4 6 7

函数本应报告索引为 4、7 和 8 的序列,但它找到了一些不应该找到的索引,也遗漏了一些。有些地方出错了。让我们进入调试器并四处看看。

debug(findruns)

findruns(c(1,0,0,1,1,0,1,1,1),2)

debugging in: findruns(c(1, 0, 0, 1, 1, 0, 1, 1, 1), 2)

debug at findruns.R#1: {

292

第十三章

www.it-ebooks.info

n <- length(x)

runs <- NULL

for (i in 1:(n - k)) {

if (all(x[i:i+k-1]==1))

runs <- c(runs, i)

}

return(runs)

}

attr(,"srcfile")

findruns.R

根据确认原则,我们首先确保我们的测试向量被正确接收:

Browse[2]> x

[1] 1 0 0 1 1 0 1 1 1

到目前为止,一切顺利。让我们逐步执行代码,我们点击了几次 n 来单步执行代码。

Browse[2]> n

debug at findruns.R#2: n <- length(x)

Browse[2]> n

debug at findruns.R#3: runs <- NULL

Browse[2]> print(n)

[1] 9

注意,在每次单步执行后,R 都会告诉我们下一个将要执行的语句。换句话说,当我们执行 print(n)时,我们还没有执行将 NULL 赋值给 runs 的操作。

注意,尽管通常你可以通过简单地输入变量的名称来打印变量的值,但我们不能在这里打印变量 n 的值,因为 n 也是调试器下一个命令的缩写。因此,我们需要使用 print()。

无论如何,我们发现我们的测试向量的长度是 9,这证实了我们所知道的情况。现在,让我们继续单步执行,进入循环。

Browse[2]> n

debug at findruns.R#4: for (i in 1:(n - k + 1)) {

if (all(x[i:i + k - 1] == 1))

runs <- c(runs, i)

}

Browse[2]> n

debug at findruns.R#4: i

Browse[2]> n

debug at findruns.R#5: if (all(x[i:i + k - 1] == 1)) runs <- c(runs, i) 调试

293

www.it-ebooks.info

由于 k 是 2——也就是说,我们正在检查长度为 2 的序列——if()语句应该检查 x 的前两个元素,即(1,0)。

让我们确认一下:

Browse[2]> x[i:i + k - 1]

[1] 0

所以,它并没有确认。让我们检查我们是否有正确的子索引范围,应该是 1:2。是吗?

Browse[2]> i:i + k - 1

[1] 2

仍然不对。嗯,关于 i 和 k,它们应该是 1 和 2,对吗?

浏览[2]> i

[1] 1

浏览[2]> k

[1] 2

嗯,这些确实确认了。因此,我们的问题一定出在表达式 i:i + k - 1 上。经过一番思考,我们意识到这里存在一个运算符优先级问题,并将其更正为 i:(i + k - 1)。

现在可以了吗?

source("findruns.R")

findruns(c(1,0,0,1,1,0,1,1,1),2)

[1] 4 7

不,正如提到的,应该是 (4,7,8)。

让我们在循环内部设置一个断点并仔细查看。

setBreakpoint("findruns.R",5)

/home/nm/findruns.R#5:

findruns step 4,4,2 in <环境: R_GlobalEnv>

findruns(c(1,0,0,1,1,0,1,1,1),2)

findruns.R#5

调用来自: eval(expr, envir, enclos)

浏览[1]> x[i:(i+k-1)]

[1] 1 0

好的,我们现在正在处理向量的前两个元素,所以我们的错误修复到目前为止是有效的。让我们看看循环的第二次迭代。

浏览[1]> c

findruns.R#5

调用来自: eval(expr, envir, enclos)

294

第十三章

www.it-ebooks.info

浏览[1]> i

[1] 2

浏览[1]> x[i:(i+k-1)]

[1] 0 0

没错,还可以再迭代一次,但相反,让我们看看最后一次迭代,这是循环中经常出现错误的地方。所以,让我们添加一个条件断点,如下所示:

findruns <- function(x,k) {

n <- length(x)

runs <- NULL

for (i in 1:(n-k)) {

if (all(x[i:(i+k-1)]==1)) runs <- c(runs,i)

if (i == n-k) browser() # 在循环的最后一次迭代中中断

}

return(runs)

}

现在再次运行它。

source("findruns.R")

findruns(c(1,0,0,1,1,0,1,1,1),2)

Called from: findruns(c(1, 0, 0, 1, 1, 0, 1, 1, 1), 2)

Browse[1]> i

[1] 7

这表明最后一次迭代是 i = 7。但向量有九个元素长,k = 2,所以我们的最后一次迭代应该是 i = 8。经过一些思考后,我们发现循环中的范围应该写成如下:for (i in 1:(n-k+1)) {

顺便说一下,请注意,我们现在已经替换了旧的 findruns 对象版本,因此我们使用 setBreakpoint() 设置的断点不再有效。

后续测试(此处未显示)表明代码现在可以正常工作。

让我们继续到一个更复杂的例子。

13.3.6.2 调试寻找城市对

回想一下我们在 3.4.2 节中的代码,该代码找到了距离最近的城市对。以下是该代码的一个有问题的版本:

1

返回 d[i,j] 的最小值,i != j,以及达到 2 的行/列

对于平方对称矩阵 d 的最小值,没有特殊策略

3

ties;

4

由距离矩阵激发

调试

295

www.it-ebooks.info

5

mind <- function(d) {

6

n <- nrow(d)

7

为 apply() 添加一个列以标识行号

8

dd <- cbind(d,1:n)

9

wmins <- apply(dd[-n,],1,imin)

10

wmins 将是 2xn,第一行是索引,第二行是值

11

i <- which.min(wmins[1,])

12

j <- wmins[2,i]

13

return(c(d[i,j],i,j))

14

}

15

16

在行 x 中找到最小值的定位和值

17

imin <- function(x) {

18

n <- length(x)

19

i <- x[n]

20

j <- which.min(x[(i+1):(n-1)])

21

return(c(j,x[j]))

22

}

让我们使用 R 的调试工具来查找和修复问题。

我们首先在一个小测试用例上运行它:

source("cities.R")

m <- rbind(c(0,12,5),c(12,0,8),c(5,8,0))

m

[,1] [,2] [,3]

[1,]

0

12

5

[2,]

12

0

8

[3,]

5

8

0

mind(m)

Error in mind(m) : subscript out of bounds

并不是一个吉祥的开始!不幸的是,错误信息没有告诉我们代码在哪里崩溃。但调试器会给我们这个信息:

options(error=recover)

mind(m)

Error in mind(m) : subscript out of bounds

输入一个帧号,或 0 退出

1: mind(m)

Selection: 1

Called from: eval(expr, envir, enclos)

Browse[1]> where

296

第十三章

www.it-ebooks.info

其中 1: eval(expr, envir, enclos)

where 2: eval(quote(browser()), envir = sys.frame(which))

where 3 at cities.R#13: function ()

{

if (.isMethodsDispatchOn()) {

tState <- tracingState(FALSE)

...

好吧,问题发生在 mind() 而不是 imin() 中,特别是在第 13 行。这仍然可能是 imin() 的错误,但暂时我们先处理前者。

注意

我们还有另一种方法可以确定崩溃发生在第 13 行。

我们像以前一样进入调试器,但检查局部变量。我们可以推理 如果下标界限错误发生在第 9 行,那么变量 wmins 就不会被设置,所以查询它将给我们一个像这样的错误消息 错误:对象

'wmins' not found. 另一方面,如果崩溃发生在第 13 行,即使 j 也会被设置。

由于错误发生在 d[i,j],让我们看看这些变量:Browse[1]> d

[,1] [,2] [,3]

[1,]

0

12

5

[2,]

12

0

8

[3,]

5

8

0

Browse[1]> i

[1] 2

Browse[1]> j

[1] 12

这确实是一个问题——d 只有三列,而 j 是一个列

下标,是 12。

让我们看看我们从中获得 j 的变量 wmins:

Browse[1]> wmins

[,1] [,2]

[1,]

2

1

[2,]

12

12

如果你记得代码是如何设计的,wmins 的列 k 应该包含关于 d 的第 k 行最小值的信息。所以在这里,wmins 表示在 d 的第一行(k = 1)中,(0,12,5)的最小值是 12,发生在索引 2。但应该是索引 3 的 5。所以,这一行出了问题:

wmins <- apply(dd[-n, ], 1, imin)

Debugging

297

www.it-ebooks.info

这里有几个可能性。但由于最终 imin()被调用,我们可以在该函数内部检查它们。所以,让我们设置 imin()的调试状态,退出调试器,并重新运行代码。

Browse[1]> Q

debug(imin)

mind(m)

在:FUN(newX[, i], ...)

debug at cities.R#17: {

n <- length(x)

i <- x[n]

j <- which.min(x[(i + 1):(n - 1)])

return(c(j, x[j]))

}

...

所以,我们在 imin()中。让我们看看它是否正确接收了 dd 的第一行,它应该是(0,12,5,1)。

Browse[4]> x

[1] 0 12 5 1

已确认。这似乎表明 apply()的第一个两个参数是正确的,问题实际上出在 imin()中,尽管这一点还需要进一步确认。

让我们单步执行,偶尔输入确认查询:Browse[2]> n

debug at cities.r#17: n <- length(x)

Browse[2]> n

debug at cities.r#18: i <- x[n]

Browse[2]> n

debug at cities.r#19: j <- which.min(x[(i + 1):(n - 1)])

Browse[2]> n

debug at cities.r#20: return(c(j, x[j]))

Browse[2]> print(n)

[1] 4

Browse[2]> i

[1] 1

Browse[2]> j

[1] 2

回想一下,我们设计调用 which.min(x[(i + 1):(n - 1)])时只查看这一行的上三角部分。这是因为矩阵是对称的,而且我们不希望考虑一个城市与自身的距离。

298

第十三章

www.it-ebooks.info

但 j = 2 的值没有得到确认。在(0,12,5)中的最小值是 5,发生在该向量的索引 3,而不是索引 2。因此,问题就在这一行:

j <- which.min(x[(i + 1):(n - 1)])

可能出了什么问题?

休息一下后,我们意识到,尽管(0,12,5)的最小值出现在该向量的索引 3 处,但这并不是我们要求 which.min()为我们找到的内容。相反,那个 i + 1 项意味着我们要求的是(12,5)中的最小值的索引,即 2。

我们确实要求 which.min()提供正确的信息,但我们未能正确使用它,因为我们确实想要(0,12,5)中最小值的索引。我们需要相应地调整 which.min()的输出,如下所示:

j <- which.min(x[(i+1):(n-1)])

k <- i + j

return(c(k,x[k]))

我们进行修复并再次尝试。

mind(m)

Error in mind(m) : subscript out of bounds

输入一个帧号,或 0 退出

1: mind(m)

选择:

哦不,另一个越界错误!为了看到这次爆炸发生的位置,我们像以前一样发出 where 命令,我们发现它是在第 13 行。

再次。现在 i 和 j 是什么?

Browse[1]> i

[1] 1

Browse[1]> j

[1] 5

j 的值仍然不正确;它不能大于 3,因为在这个矩阵中我们只有三列。另一方面,i 是正确的。在 dd 中的整体最小值是 5,出现在第 1 行第 3 列。

因此,让我们再次检查 j 的来源,即矩阵 wmins:

Browse[1]> wmins

[,1] [,2]

[1,]

3

3

[2,]

5

8

调试

299

www.it-ebooks.info

好吧,列 1 中的 3 和 5 正如预期的那样。

记住,这里的列 1 包含 d 中第 1 行的信息,所以 wmins 表示第 1 行的最小值是 5,出现在该行的索引 3 处,这是正确的。

然而,休息一下后,我们意识到,虽然 wmins 是正确的,但我们的使用是不正确的。我们混淆了那个矩阵的行和列。

这段代码:

i <- which.min(wmins[1,])

j <- wmins[2,i]

应该是这样的:

i <- which.min(wmins[2,])

j <- wmins[1,i]

在进行更改并重新加载文件后,我们再次尝试。

mind(m)

[1] 5 1 3

这是正确的,并且后续对更大矩阵的测试也成功了。

13.4 在世界上攀升:更方便的调试

工具

正如所见,R 的调试工具是有效的。然而,它们并不非常方便。幸运的是,有一些工具可以使这个过程更容易。按照开发的大致时间顺序,它们如下所示:

马克·布拉文顿的 debug 包

我的 edtdbg 包,它与 Vim 和 Emacs 文本编辑器一起工作

维塔利·斯皮努的 ess-tracebug,它在 Emacs 下运行(与 edtdbg 有相同的目标,但具有更多 Emacs 特定功能)

REvolution Analytics 的集成开发环境(IDE)

注意

截至本文撰写时(2011 年 7 月),开发 StatET 和 RStudio IDE 的团队正在进行添加调试工具的工作。

所有这些工具都是跨平台的,可以在 Linux、Windows 和 Mac 系统上运行,但 REvolution Analytics 产品除外。该 IDE 仅适用于带有 Microsoft Visual Studio 的 Windows 系统。所有这些工具都是开源的或免费的,但 REvolution Analytics 产品除外。

那么,这些包有什么可以提供的呢?对我来说,R 内置调试工具的最大问题之一是缺少一个显示整体情况的窗口——一个显示您的 R 代码并带有移动光标的窗口——300

第十三章

www.it-ebooks.info

Image 39

Image 40

当您单步执行代码时,代码会显示出来。例如,考虑以下我们之前浏览器输出的摘录:

浏览[2] > n

在 cities.r#17: n <- length(x)

浏览[2] > n

在 cities.r#18: i <- x[n]

这很好,但这些行在我们的代码中的位置在哪里?其他语言的多数 GUI 调试器都有一个显示用户源代码的窗口,其中有一个符号指示下一个要执行的行。本节开头列出的所有 R 工具都弥补了 R 核心中的这一不足。Bravington 调试包创建了一个专门用于此目的的单独窗口。其他工具让您的文本编辑器充当那个窗口,从而与使用 debug 相比节省了屏幕空间。

此外,这些工具还允许您设置断点并处理其他调试操作,而无需将屏幕光标从编辑器窗口移到 R 执行窗口。这既方便又节省了输入,大大提高了您专注于手头真正任务的效率:找到您的错误。

让我们再次考虑城市示例。我在 GVim 文本编辑器中打开了我的源文件,并配合 edtdbg 使用,为 edtdbg 进行了一些启动操作,然后按了两次[(左括号)键来单步通过代码。

结果的 GVim 窗口如图 13-1 所示。

图 13-1:edtdbg 中的源代码窗口

调试

301

www.it-ebooks.info

注意

edtdbg 在 Emacs 中的操作与这里显示的相同,只是用于命令的快捷键不同。例如,F8 用于单步执行而不是[。

首先,请注意编辑器的光标现在在这一行:

wmins <- apply(dd[-n, ], 1, imin)

这显示了下一个要执行的行。

每当我想要单步执行一行时,我只需在编辑器窗口中按[键。然后编辑器会告诉浏览器执行其 n 命令,而无需我将鼠标移到 R 执行窗口,然后编辑器将光标移到下一行。我也可以按]来执行浏览器的 c 命令。每次我以这种方式执行一行或几行,编辑器的光标就会相应移动。

每当我对我的源代码进行更改时,输入,src(逗号

(这是命令的一部分)输入到 GVim 窗口中会告诉 R 调用 source()。每次我想重新运行我的代码时,我都会按,dt。我很少,如果有的话,需要将鼠标从编辑器窗口移到 R 窗口然后再回来。

从本质上讲,编辑器已经成为了我的调试器,除了提供其编辑操作外。

13.5 确保调试模拟代码的一致性

如果您在进行任何与随机数相关的工作,您将需要能够在调试会话中每次运行程序时重现相同的数字流。如果没有这一点,您的错误可能无法重现,这使得它们更难修复。

set.seed() 函数通过重新初始化随机数序列到给定值来控制这一点。

考虑这个例子:

[1] 0.8811480 0.2853269 0.5864738

runif(3)

[1] 0.5775979 0.4588383 0.8354707

runif(3)

[1] 0.4155105 0.4852900 0.6591892

runif(3)

set.seed(8888)

runif(3)

[1] 0.5775979 0.4588383 0.8354707

set.seed(8888)

runif(3)

[1] 0.5775979 0.4588383 0.8354707

runif(3) 调用生成三个从区间 (0,1) 的均匀分布的随机数。

每次我们调用这个函数时,我们都会得到一组不同的三个数字。但通过 set.seed(),我们可以从头开始并得到相同的数字序列。

302

第十三章

www.it-ebooks.info

13.6 语法和运行时错误

最常见的语法错误将是缺少匹配的括号、方括号、花括号或引号。当您遇到语法

错误,这是您应该首先检查并再次检查的第一件事。我强烈建议您使用具有括号匹配和 R 语法高亮的文本编辑器,如 Vim 或 Emacs。

注意,当您收到一条消息说某一行存在语法错误时,错误实际上可能在一个远早的行中。

这在任何语言中都可能发生,但 R 似乎特别容易发生这种情况。

如果您不清楚语法错误在哪里,我建议您有选择地注释掉一些代码,这样您就能更好地定位语法问题的位置。通常,遵循二分搜索方法是有帮助的:注释掉一半的代码(注意保持语法完整性)并查看是否出现相同的错误。如果出现了,那么错误就在剩下的部分;否则,错误就在您删除的那一半。然后,将那一半再分成两半,依此类推。

您可能会收到如下消息:

有 50 或更多的警告(使用 warnings() 查看前 50 个)。这些应该被注意——按照建议运行 warnings()。问题

可能从算法的非收敛性到函数矩阵参数的误指定,再到函数。在许多情况下,程序输出可能无效,尽管它可能很好,比如这个消息:

glm 中数值概率为 0 或 1 的情况发生了:

在某些情况下,您可能会发现发出此命令很有用:

options(warn=2)

这指示 R 将警告转换为实际错误,并使警告的位置更容易找到。

13.7 在 R 上运行 GDB

即使您不是在尝试修复 R 中的错误,这一节也可能对您有所帮助。例如,您可能编写了一些 C 代码来与 R 接口(在第十五章中介绍),并发现它存在错误。为了在 GDB 上运行那个 C 函数,您必须首先通过 GDB 运行 R 本身。

或者,你可能对 R 的内部结构感兴趣,比如确定如何编写高效的 R 代码,并希望通过调试工具(如 GDB)逐步遍历 R 源代码来探索内部结构。

调试

303

www.it-ebooks.info

虽然你可以从 shell 命令行调用 R(见第 15.1.4 节),但在这里,我建议使用单独的窗口来运行 R 和 GDB。以下是步骤:

按照惯例,在一个窗口中启动 R。

在另一个窗口中,确定你的 R 进程的 ID 号。在

UNIX 系统家族,例如,可以通过类似 ps -a 的命令获得。

在那个第二个窗口中,提交 GDB 的 attach 命令与 R 进程一起。

进程号。

向 GDB 提交继续命令。

你可以在继续之前或在 GDB 中使用 CTRL-C 中断后设置 R 源代码中的断点。有关从 R 调用的 C 代码调试的详细信息,请参阅第 15.1.4 节。另一方面,如果你希望使用 GDB

探索 R 源代码时,请注意以下事项。

R 的源代码主要由 S 表达式指针(SEXPs)组成,

这些是指向包含 R 变量值、类型等信息的 C 结构的指针。你可以使用 R 内部函数 Rf_PrintValue(s) 来检查 SEXP

值。例如,如果 SEXP 被命名为 s,那么在 GDB 中,输入以下内容:call Rf_PrintValue(s)

这将打印值。

304

第十三章

www.it-ebooks.info

Image 41

14

性能提升:

速度和内存

在计算机科学课程中,一个常见的

主题是时间和

空间。为了有一个快速运行的程序,

程序,可能需要使用更多的内存空间。

另一方面,为了节省内存空间,

你可能需要满足于较慢的代码。在 R 语言中,

语言,这种权衡对以下方面尤其感兴趣:

以下原因:

R 是一种解释型语言。许多命令是用 C

因此确实以快速机器代码运行。但其他命令,以及你的

自己的 R 代码,是纯 R 并因此是解释型的。因此,你的 R 应用程序可能运行得比你希望的慢。

R 会话中的所有对象都存储在内存中。更确切地说,所有对象都存储在 R 的内存地址空间中。R 对任何对象的大小限制为 231 1 字节,即使在 64 位机器上,即使你有大量的 RAM。然而,一些应用程序确实会遇到

较大的对象。

www.it-ebooks.info

本章将提出你可以提高 R 代码性能的方法,考虑到时间/空间权衡。

14.1 编写高效的 R 代码

如何使 R 代码更快?以下是你可用的主要工具:

通过向量化、使用字节码编译和其他方法优化你的 R 代码。

将代码的关键、CPU 密集部分用编译语言(如 C/C++)编写。

将你的代码以某种形式的并行 R 编写。

第一种方法将在本章中介绍,其他方法将在第十五章和第十六章中介绍。

为了优化你的 R 代码,你需要理解 R 的函数编程特性。

gramming nature and the way R uses memory.

14.2 可怕的 for 循环

R 的 r-help 讨论列表经常有人询问如何在不使用循环的情况下完成各种任务。似乎有一种感觉,程序员应该不惜一切代价避免这些循环。1 提出问题的人通常的目标是加快他们的代码。

重要的是要理解,仅仅重写代码以避免循环并不一定会使代码更快。然而,在某些情况下,可以通过向量化获得显著的加速。

14.2.1 向量化以加速

有时候,你可以使用向量化而不是循环。例如,如果 x 和 y 是长度相等的向量,你可以这样写:

z <- x + y

这不仅更紧凑,更重要的是,它比使用这个循环更快:

for (i in 1:length(x)) z[i] <- x[i] + y[i]

让我们做一个快速的计时比较:

x <- runif(1000000)

y <- runif(1000000)

1 与此相反,while 循环提出了更大的挑战,因为它们难以有效地向量化。

306

第十四章

www.it-ebooks.info

z <- vector(length=1000000)

system.time(z <- x + y)

user system elapsed

0.052

0.016

0.068

system.time(for (i in 1:length(x)) z[i] <- x[i] + y[i])

user system elapsed

8.088

0.044

8.175

差异多么大啊!没有循环的版本在运行时间上快了超过 120 倍。虽然运行时间可能因运行而异(循环版本的第二次运行运行时间为 22.958),在某些情况下,

“去循环”R 代码确实可以带来好处。

值得讨论一下循环版本中减慢速度的一些原因。对于从其他语言转向 R 的程序员来说可能不明显的是,前述代码的循环版本中涉及了大量的函数调用:

虽然从语法上看循环看起来无害,但实际上 for() 是一个函数。

冒号 : 看起来更无害,但实际上也是一个函数。例如,1:10 实际上是 : 函数在 1

and 10:

":"(1,10)

[1] 1 2 3 4 5 6 7 8 9 10

每个向量索引操作代表一个函数调用,包括对 [

对于两次读取和写入的情况。

函数调用可能会消耗时间,因为它们涉及到设置栈帧等。在循环的每次迭代中承受这种时间惩罚会导致速度大幅下降。

相比之下,如果我们用 C 语言来写这个,就不会有函数调用了。实际上,这正是我们第一个代码片段中发生的事情。那里也有函数调用,即对 + 和对 - > 的调用,但每个只调用了一次,而不是像循环版本那样调用 1,000,000 次。因此,代码的第一个版本要快得多。

向量化的一种类型是向量过滤。例如,让我们重写第 1.3 节中的 oddcount()函数:

oddcount <- function(x) return(sum(x%%2==1))

这里没有显式循环,尽管 R 会内部遍历数组,但这将在本地机器码中完成。再次强调,预期的加速确实发生了。

x <- sample(1:1000000,100000,replace=T)

system.time(oddcount(x))

用户 系统 运行时间

性能提升:速度和内存

307

www.it-ebooks.info

0.012

0.000

0.015

system.time(

{

c <- 0

for (i in 1:length(x))

if (x[i] %% 2 == 1) c <- c+1

return(c)

}

  • )

用户 系统 运行时间

0.308

0.000

0.310

你可能会想知道在这种情况下这是否有关系,因为即使循环版本的代码运行时间也少于 1 秒。但如果这段代码是包含在嵌套循环中的,并且有很多迭代,那么差异确实可能很重要。

可能加快你代码速度的其他向量化函数示例

这些是 ifelse(), which(), where(), any(), all(), cumsum(), 和 cumprod()。在矩阵情况下,你可以使用 rowSums(), colSums()等等。在“所有可能的组合”类型的设置中,combin(), outer(), lower.tri(), upper.tri(), 或 expand.grid()可能正是你所需要的。

虽然 apply()消除了显式循环,但实际上它在 R 中而不是在 C 中实现,因此通常不会加快你的代码速度。然而,其他 apply 函数,如 lapply(),可以在加快你的代码速度方面非常有帮助。

14.2.2 扩展示例:在蒙特卡洛中实现更好的速度

模拟

在某些应用中,模拟代码可能需要运行数小时、数天,甚至数月,因此加速方法是高度感兴趣的。在这里,我们将查看两个模拟示例。

首先,让我们考虑第 8.6 节中的以下代码:

sum <- 0

nreps <- 100000

for (i in 1:nreps) {

xy <- rnorm(2) # 生成 2 个 N(0,1)的随机数

sum <- sum + max(xy)

}

print(sum/nreps)

这里是一个修订版(希望更快):

nreps <- 100000

xymat <- matrix(rnorm(2*nreps),ncol=2)

308

第十四章

www.it-ebooks.info

maxs <- pmax(xymat[,1],xymat[,2])

print(mean(maxs))

在此代码中,我们一次性生成所有随机变量,并将它们存储在矩阵 xymat 中,每行一个(X,Y)对:

xymat <- matrix(rnorm(2*nreps),ncol=2)

接下来,我们找到所有 max(X,Y)的值,将这些值存储在 maxs 中,然后简单地调用 mean()。

这更容易编程,我们相信它将更快。让我们检查一下。

我在文件MaxNorm.R中有原始代码,在MaxNorm2.R中有改进版本。

system.time(source("MaxNorm.R"))

[1] 0.5667599

用户 系统 运行时间

1.700

0.004

1.722

system.time(source("MaxNorm2.R"))

[1] 0.5649281

用户 系统 运行时间

0.132

0.008

0.143

速度提升再次非常显著。

注意

我们通过将随机数保存在数组中而不是一次生成和丢弃它们来提高速度,但代价是使用更多的内存。如前所述,时间/空间权衡在计算领域以及 R 世界的特定领域是常见的。

在这个例子中,我们取得了非常好的加速效果,但这是误导性的简单。让我们看看一个稍微复杂一点的例子。

我们接下来的例子是初等概率中的一个经典练习

抽屉 1 中有十个蓝色弹珠和八个黄色弹珠。在抽屉 2 中,混合物是六个蓝色和六个黄色。我们随机从抽屉 1 中抽取一个弹珠,将其转移到抽屉 2,然后从抽屉 2 中随机抽取一个弹珠。

第二个弹珠是蓝色的概率是多少?这可以通过解析方法轻松找到,但我们将使用模拟。以下是直接的方法:1

进行 nreps 次弹珠实验,以估计

2

P(从抽屉 2 中抽取蓝色)

3

sim1 <- function(nreps) {

4

nb1 <- 10 # 抽屉 1 中有 10 个蓝色弹珠

5

n1 <- 18 # 第一次抽取时抽屉 1 中的弹珠数量

6

n2 <- 13 # 第二次抽取时抽屉 2 中的弹珠数量

7

count <- 0 # 从抽屉 2 中获得蓝色的重复次数

8

for (i in 1:nreps) {

9

nb2 <- 6 # 抽屉 2 中原本有 6 个蓝色弹珠

性能提升:速度和内存

309

www.it-ebooks.info

10

从抽屉 1 中抽取并放入抽屉 2;它是蓝色的吗?

11

if (runif(1) < nb1/n1) nb2 <- nb2 + 1

12

从抽屉 2 中抽取;它是蓝色的吗?

13

if (runif(1) < nb2/n2) count <- count + 1

14

}

15

return(count/nreps) # 估计 P(从抽屉 2 中抽取蓝色)

16

}

这里是如何在不使用循环的情况下使用 apply() 来完成它的:

1

sim2 <- function(nreps) {

2

nb1 <- 10

3

nb2 <- 6

4

n1 <- 18

5

n2 <- 13

6

预先生成所有随机数,每重复一次一行

7

u <- matrix(c(runif(2*nreps)),nrow=nreps,ncol=2)

8

为 apply() 定义 simfun;模拟一次重复

9

simfun <- function(rw) {

10

rw ("行") 是一对随机数

11

从抽屉 1 中选择

12

if (rw[1] < nb1/n1) nb2 <- nb2 + 1

13

从抽屉 2 中选择,并返回选择蓝色时的布尔值

14

return (rw[2] < nb2/n2)

15

}

16

z <- apply(u,1,simfun)

17

z 是一个布尔向量,但它们可以被视为 1s 和 0s

18

return(mean(z))

19

}

在这里,我们设置一个矩阵 u,包含两列 U(0,1) 随机变量。

第一列用于我们对抽屉 1 的模拟,第二列用于从抽屉 2 中抽取。这样,我们一次生成所有随机数,这可能节省一点时间,但主要目的是为使用 apply() 准备。为了达到这个目标,我们的 simfun() 函数在一个实验重复中工作——即 u 的一行。我们设置 apply() 的调用以遍历所有的 nreps 重复。

注意,由于 simfun()函数是在 sim2()内部声明的,所以 sim2()的局部变量 n1、n2、nb1 和 nb2 可以作为 simfun()的全局变量使用。此外,由于布尔向量将被 R 自动转换为 1s 和 0s,我们可以通过简单地调用 mean()来找到向量中 TRUE 值的比例。

现在,让我们比较性能。

system.time(print(sim1(100000)))

[1] 0.5086

user system elapsed

2.465

0.028

2.586

system.time(print(sim2(10000)))

310

第十四章

www.it-ebooks.info

[1] 0.5031

user system elapsed

2.936

0.004

3.027

尽管函数式编程有许多好处,但使用 apply()的方法并没有帮助。相反,情况变得更糟。这可能是由于随机抽样变异的简单原因,所以我再次运行了代码,结果相似。

因此,让我们看看如何将这个模拟向量化。

1

sim3 <- function(nreps) {

2

nb1 <- 10

3

nb2 <- 6

4

n1 <- 18

5

n2 <- 13

6

u <- matrix(c(runif(2*nreps)),nrow=nreps,ncol=2)

7

设置条件向量

8

cndtn <- u[,1] <= nb1/n1 & u[,2] <= (nb2+1)/n2 |

9

u[,1] > nb1/n1 & u[,2] <= nb2/n2

10

返回 cndtn 的平均值

11

}

主要工作在这条语句中完成:

cndtn <- u[,1] <= nb1/n1 & u[,2] <= (nb2+1)/n2 |

u[,1] > nb1/n1 & u[,2] <= nb2/n2

为了得到这个结果,我们推理出哪些条件会导致在第二次选择时选择蓝色弹珠,将它们编码,然后将它们分配给 cndtn。

记住,< = 和 & 是函数;实际上,它们是向量函数,所以它们应该很快。确实,这带来了相当大的改进:

system.time(print(sim3(10000)))

[1] 0.4987

user system elapsed

0.060

0.016

0.076

在原则上,我们用来加速代码的方法可以应用于许多其他蒙特卡洛模拟。然而,很明显,计算 cndtn 的语句的类似物会迅速变得相当复杂,即使对于看似简单的应用也是如此。

此外,这种方法在“无限阶段”情况下不会起作用,这意味着无限的时间步数。在这里,我们将弹珠示例视为两阶段,矩阵 u 有两列。

性能提升:速度和内存

311

www.it-ebooks.info

14.2.3 扩展示例:生成幂矩阵

回想第 9.1.7 节,我们需要生成预测变量的幂矩阵。我们使用了以下代码:

1

通过度 dg 形成向量 x 的幂矩阵

2

powers1 <- function(x,dg) {

3

pw <- matrix(x,nrow=length(x))

4

prod <- x # 当前乘积

5

for (i in 2:dg) {

6

prod <- prod * x

7

pw <- cbind(pw,prod)

8

}

9

return(pw)

10

}

一个明显的问题是,cbind()被用来按列构建输出矩阵。这在内存分配时间上代价很高。最好一开始就分配整个矩阵,即使它是空的,因为这将意味着只需承担一次内存分配操作的代价。

1

通过度 dg 形成向量 x 的幂矩阵

2

powers2 <- function(x,dg) {

3

pw <- matrix(nrow=length(x),ncol=dg)

4

prod <- x # 当前乘积

5

pw[,1] <- prod

6

for (i in 2:dg) {

7

prod <- prod * x

8

pw[,i] <- prod

9

}

10

return(pw)

11

}

事实上,powers2() 要快得多。

x <- runif(1000000)

system.time(powers1(x,8))

用户 系统 运行时间

0.776

0.356

1.334

system.time(powers2(x,8))

用户 系统 运行时间

0.388

0.204

0.593

312

第十四章

www.it-ebooks.info

然而,powers2() 仍然包含一个循环。我们能做得更好吗?这似乎是 outer() 的调用形式完美的设置,其调用形式为

outer(X,Y,FUN)

这个调用将函数 FUN() 应用到 X 的所有可能元素对上

并且 Y 的元素。FUN 的默认值是乘法。

在这里,我们可以写出以下内容:

powers3 <- function(x,dg) return(outer(x,1:dg,"^"))

对于 x 的每个元素和 1:dg 的每个元素的组合(总共产生 length(x) × dg 个组合),outer() 调用指数函数

^ 在那个组合上,将结果放入一个 length(x) × dg 矩阵中。这正是我们需要的,而且作为额外的好处,代码相当紧凑。但代码更快吗?

system.time(powers3(x,8))

用户 系统 运行时间

1.336

0.204

1.747

多么令人失望啊!在这里,我们使用了一个花哨的 R 函数,代码非常紧凑,但三个函数中性能最差。

而且情况变得更糟。如果我们尝试使用 cumprod(),会发生什么:

powers4

function(x,dg) {

repx <- matrix(rep(x,dg),nrow=length(x))

return(t(apply(repx,1,cumprod)))

}

system.time(powers4(x,8))

用户 系统 运行时间

28.106

1.120 83.255

在这个例子中,我们创建了多个 x 的副本,因为一个数字 n 的幂仅仅是 cumprod(c(1,n,n,n...)) 的累积乘积。但尽管我们尽职尽责地使用了两个 C 级别的 R 函数,性能仍然非常糟糕。

这个故事的意义在于性能问题可能是不可预测的。

你能做的就是掌握基本问题、向量化以及接下来解释的内存方面的理解,然后尝试各种方法。

性能提升:速度和内存

313

www.it-ebooks.info

14.3 函数式编程和内存问题

大多数 R 对象都是 不可变的,或者说不可更改的。因此,R 操作是通过将给定对象重新赋值的函数实现的,这种特性可能会对性能产生影响。

14.3.1 向量赋值问题

作为可能出现的一些问题的例子,考虑这个看起来简单的声明:

z[3] <- 8

如第七章所述,这个赋值比看起来要复杂。实际上,它是通过这个调用和赋值通过替换函数 "[<-" 实现的:

z <- "[<-"(z,3,value=8)

对 z 的一个内部副本进行了操作,将副本中的第 3 个元素更改为 8,然后将得到的向量重新赋值给 z。记住,后者仅仅意味着 z 指向了副本。

换句话说,即使我们表面上只改变向量的一个元素,语义上讲,整个向量都会被重新计算。对于长向量,这会显著减慢程序的运行速度。如果是在我们的代码循环中赋值给短向量,情况也会相同。

在某些情况下,R 确实采取了一些措施来减轻这种影响,但在追求快速代码时,这是一个需要考虑的关键点。当与向量(包括数组)一起工作时,你应该注意这一点。如果你的代码似乎运行得异常缓慢,向量的赋值应该是一个首要的怀疑对象。

14.3.2 更改时复制问题

相关问题是,R(通常)遵循更改时复制策略。例如,如果我们执行以下操作:

y <- z

初始时,y 与 z 共享相同的内存区域。但如果有任何一个发生变化,那么就会在内存的不同区域创建一个副本,并且变化后的变量将占用新的内存区域。然而,只有第一次变化受到影响,因为移动变量的重新定位意味着不再有任何共享问题。tracemem()函数将报告此类内存重新定位。

314

第十四章

www.it-ebooks.info

尽管 R 通常遵循更改时复制的语义,但也有一些例外。例如,R 在以下设置中不会显示位置变化的行为:

z <- runif(10)

tracemem(z)

[1] "<0x88c3258>"

z[3] <- 8

tracemem(z)

[1] "<0x88c3258>"

z 的位置没有变化;它在内存地址 0x88c3258

在执行对 z[3]的赋值操作之前和之后。因此,尽管你应该警惕位置变化,但你也不能假设它会发生变化。

让我们看看涉及的时间。

z <- 1:10000000

system.time(z[3] <- 8)

用户 系统 运行时间

0.180

0.084

0.265

system.time(z[33] <- 88)

用户 系统 运行时间

0

0

0

无论如何,如果进行了复制,那么工具是 R 的内部函数 duplicate()。(在 R 的较新版本中,该函数被称为 duplicate1()。)如果你熟悉 GDB 调试工具,并且你的 R 构建包含调试信息,你可以探索执行复制的环境。

按照第 15.1.4 节中的指南,使用 GDB 启动 R,通过 GDB 逐步执行 R,并在 duplicate1()函数处设置断点。每次在该函数处中断时,提交以下 GDB 命令:

call Rf_PrintValue(s)

这将打印 s(或任何感兴趣的变量)的值。

14.3.3 扩展示例:避免内存复制

这个例子,尽管是人为的,将演示上一节中讨论的内存复制问题。

假设我们有许多无关的向量,并且我们希望将每个向量的第三个元素设置为 8。我们可以将向量存储在一个矩阵中,每行一个向量。但由于它们是无关的,甚至可能长度不同,我们可能考虑将它们存储在一个列表中。

性能提升:速度和内存

315

www.it-ebooks.info

但是,当涉及到 R 的性能问题时,事情可能会变得非常微妙,所以让我们试试看。

m <- 5000

n <- 1000

z <- list()

for (i in 1:m) z[[i]] <- sample(1:10,n,replace=T)

system.time(for (i in 1:m) z[[i]][3] <- 8)

用户系统已用时间

0.288

0.024

0.321

z <- matrix(sample(1:10,m*n,replace=T),nrow=m)

system.time(z[,3] <- 8)

user system elapsed

0.008

0.044

0.052

除了系统时间(再次),矩阵公式表现得更好。

其中一个原因是,在列表版本中,我们在循环的每次迭代中都会遇到内存复制问题。但在矩阵版本中,我们只遇到一次。当然,矩阵版本是向量化的。

但使用 lapply()在列表版本上又如何呢?

set3 <- function(lv) {

lv[3] <- 8

return(lv)

  • }

z <- list()

for (i in 1:m) z[[i]] <- sample(1:10,n,replace=T)

system.time(lapply(z,set3))

用户系统已用时间

0.100

0.012

0.112

向量化代码很难被超越。

14.4 使用 Rprof()查找代码中的慢速区域

如果你认为你的 R 代码运行得过于缓慢,一个方便的工具是 Rprof(),它可以给你一个报告,显示你的代码在它调用的每个函数中花费了大约多少时间。这很重要,因为优化程序中的每个部分可能并不明智。

优化可能会以编码时间和代码清晰度为代价,因此了解优化真正能帮助的地方是有价值的。

14.4.1 使用 Rprof()进行监控

让我们通过使用 Rprof()来演示如何使用我们前面扩展示例中的三个版本的代码来找到一个幂矩阵。我们将调用 Rprof()来启动监控,运行我们的代码,然后使用 NULL 调用 Rprof()。

316

第十四章

www.it-ebooks.info

停止监控的参数。最后,我们将调用 summaryRprof()来查看结果。

x <- runif(1000000)

Rprof()

invisible(powers1(x,8))

Rprof(NULL)

summaryRprof()

$by.self

self.time self.pct total.time total.pct

"cbind"

0.74

86.0

0.74

86.0

"*"

0.10

11.6

0.10

11.6

"matrix"

0.02

2.3

0.02

2.3

"powers1"

0.00

0.0

0.86

100.0

$by.total

total.time total.pct self.time self.pct

"powers1"

0.86

100.0

0.00

0.0

"cbind"

0.74

86.0

0.74

86.0

"*"

0.10

11.6

0.10

11.6

"matrix"

0.02

2.3

0.02

2.3

$sampling.time

[1] 0.86

我们立即看到,我们的代码运行时间主要受 cbind()调用的支配,正如我们在扩展示例中提到的,这确实会减慢速度。

顺便说一下,这个例子中调用 invisible()是为了抑制输出。我们当然不希望看到 powers1()返回的 1,000,000 行矩阵!

对 powers2()进行性能分析没有显示出任何明显的瓶颈。

Rprof()

invisible(powers2(x,8))

Rprof(NULL)

summaryRprof()

$by.self

self.time self.pct total.time total.pct

"powers2"

0.38

67.9

0.56

100.0

"matrix"

0.14

25.0

0.14

25.0

"*"

0.04

7.1

0.04

7.1

$by.total

total.time total.pct self.time self.pct

"powers2"

0.56

100.0

0.38

67.9

"matrix"

0.14

25.0

0.14

25.0

"*"

0.04

7.1

0.04

7.1

性能提升:速度和内存

317

www.it-ebooks.info

$sampling.time

[1] 0.56

那么 powers3() 呢?这是一个有希望但最终没有成功的方案?

Rprof()

invisible(powers3(x,8))

Rprof(NULL)

summaryRprof()

$by.self

self.time self.pct total.time total.pct

"FUN"

0.94

56.6

0.94

56.6

"outer"

0.72

43.4

1.66

100.0

"powers3"

0.00

0.0

1.66

100.0

$by.total

total.time total.pct self.time self.pct

"outer"

1.66

100.0

0.72

43.4

"powers3"

1.66

100.0

0.00

0.0

"FUN"

0.94

56.6

0.94

56.6

$sampling.time

[1] 1.66

记录耗时最多的函数是 FUN(),正如我们在扩展示例中所提到的,它仅仅是乘法。对于这里 x 的每一对元素,其中一个元素会被另一个元素相乘;也就是说,找到了两个标量的乘积。换句话说,没有使用向量化!难怪它运行得慢。

14.4.2 Rprof() 的工作原理

让我们更详细地探讨一下 Rprof() 的作用。每 0.02 秒(默认值),R 会检查调用栈以确定当时哪些函数调用正在生效。它将每次检查的结果写入一个文件,默认为 Rprof.out。以下是 powers3() 运行时该文件的摘录:

...

"outer" "powers3"

"outer" "powers3"

"outer" "powers3"

"FUN" "outer" "powers3"

"FUN" "outer" "powers3"

"FUN" "outer" "powers3"

"FUN" "outer" "powers3"

...

318

第十四章

www.it-ebooks.info

因此,Rprof() 经常发现,在检查时,powers3() 调用了 outer(),后者又调用了 FUN(),后者是当前正在执行的功能。summaryRprof() 函数方便地总结了文件中的所有这些行,但你可能会发现,在某些情况下,查看文件本身可以揭示更多见解。

注意,Rprof() 并非万能。如果你正在分析的代码产生了许多函数调用(包括间接调用,当你的代码调用某个函数时,该函数又调用了另一个 R 函数),那么分析输出可能难以解读。对于 powers4() 的输出来说,这可能是真的:

$by.self

self.time self.pct total.time total.pct

"apply"

19.46

67.5

27.56

95.6

"lapply"

4.02

13.9

5.68

19.7

"FUN"

2.56

8.9

2.56

8.9

"as.vector"

0.82

2.8

0.82

2.8

"t.default"

0.54

1.9

0.54

1.9

"unlist"

0.40

1.4

6.08

21.1

"!"

0.34

1.2

0.34

1.2

"is.null"

0.32

1.1

0.32

1.1

"aperm"

0.22

0.8

0.22

0.8

"matrix"

0.14

0.5

0.74

2.6

"!="

0.02

0.1

0.02

0.1

"powers4"

0.00

0.0

28.84

100.0

"t"

0.00

0.0

28.10

97.4

"array"

0.00

0.0

0.22

0.8

$by.total

total.time total.pct self.time self.pct

"powers4"

28.84

100.0

0.00

0.0

"t"

28.10

97.4

0.00

0.0

"apply"

27.56

95.6

19.46

67.5

"unlist"

6.08

21.1

0.40

1.4

"lapply"

5.68

19.7

4.02

13.9

"FUN"

2.56

8.9

2.56

8.9

"as.vector"

0.82

2.8

0.82

2.8

"matrix"

0.74

2.6

0.14

0.5

"t.default"

0.54

1.9

0.54

1.9

"!"

0.34

1.2

0.34

1.2

"is.null"

0.32

1.1

0.32

1.1

"aperm"

0.22

0.8

0.22

0.8

"array"

0.22

0.8

0.00

0.0

"!="

0.02

0.1

0.02

0.1

$sampling.time

[1] 28.84

性能提升:速度和内存

319

www.it-ebooks.info

14.5 字节码编译

从版本 2.13 开始,R 已经包含了一个字节码编译器,你可以用它来尝试加速你的代码。考虑我们第 14.2.1 节中的例子。

作为一个非常简单的例子,我们展示了

z <- x + y

比以下代码快得多

for (i in 1:length(x)) z[i] <- x[i] + y[i]

再次,这是显而易见的,但为了了解字节码编译是如何工作的,让我们试一试:

library(compiler)

f <- function() for (i in 1:length(x)) z[i] <<- x[i] + y[i]

cf <- cmpfun(f)

system.time(cf())

用户系统耗时

0.845

0.003

0.848

我们从原始的 f()函数创建了一个新的函数 cf()。新代码的运行时间为 0.848 秒,比非编译版本的 8.175 秒快得多。诚然,它仍然没有像直接向量化的代码那样快,但很明显,字节码编译有潜力。当你需要更快的代码时,你应该尝试它。

14.6 哎呀,数据放不进内存!

如前所述,R 会话中的所有对象都存储在内存中。R

对任何对象的大小设置限制为 231 1 字节,无论字长(32 位与 64 位)以及机器中的 RAM 量。然而,你真的不应该把它看作是一个障碍。只要稍微多加小心,具有大量内存需求的应用程序确实可以在 R 中得到很好的处理。一些常见的方法是分块和使用 R 包进行内存管理。

14.6.1 分块

一个不涉及任何额外 R 包的选项是从磁盘文件一次读取一个数据块。例如,假设我们的目标是找到一些变量的均值或比例。我们可以使用 read.table()中的 skip 参数。

假设我们的数据集有 1,000,000 条记录,我们将它们分成 10

块(或更多—— whatever is needed to cut the data down to a size so it fits in memory)。然后我们在第一次读取时设置 skip = 0,第二次设置 skip = 100000

第二次读取时,以此类推。每次读取一个块时,我们计算320

第十四章

www.it-ebooks.info

记录该块的数量或总和,并将它们记录下来。在读取所有块之后,我们将所有数量或总和加起来,以便计算我们的总体均值或比例。

作为另一个例子,假设我们正在进行一个统计操作,比如计算主成分,其中我们有一大批行——也就是说,有大量观测值——但变量的数量是可管理的。再次强调,分块可能是解决方案。我们将统计操作应用于每个块,然后对所有块的结果进行平均。我的数学研究显示,这些结果估计量在广泛的统计方法中在统计上是有效的。

14.6.2 使用 R 包进行内存管理

再次考虑更复杂的例子,有一些替代方案可以用来适应大内存需求,形式是一些专门的 R

packages.

其中一个包是 RMySQL,它是 SQL 数据库的 R 接口。使用它需要一些数据库专业知识,但这个包提供了处理大型数据集的更高效和更方便的方法。想法是在数据库端让 SQL 为你执行变量/情况选择操作,然后读取由 SQL 产生的所选数据。

由于后者通常比整体数据集小得多,你可能会绕过 R 的内存限制。

另一个有用的包是 biglm,它对非常大的数据集进行回归和广义线性模型分析。它也使用分块,但方式不同:每个块用于更新回归分析所需的运行总和,然后被丢弃。

最后,一些包自己进行存储管理独立于

独立于 R,因此可以处理非常大的数据集。目前最常用的两个是 ff 和 bigmemory。前者通过在磁盘上而不是内存中存储数据来规避内存限制,对程序员来说几乎是透明的。功能极其丰富的 bigmemory 包做同样的事情,但它不仅可以存储在磁盘上,还可以存储在机器的主内存中,这对于多核机器来说非常理想。

性能提升:速度和内存

321

www.it-ebooks.info

www.it-ebooks.info

图像 42

15

将 R 连接到其他

语言

R 是一种伟大的语言,但它不能做每一件事

事情处理得很好。因此,有时是可取的

从其他语言中调用代码

R。相反,当在其他伟大的语言中工作时

语言,你可能会遇到一些任务,这些任务可能更适合

在 R 中完成。

已经为许多其他语言开发了 R 接口,

从无处不在的语言如 C 到晦涩难懂的语言如 Yacas 计算机代数系统。本章将介绍两个接口:一个是从 R 调用 C/C++的接口,另一个是从 Python 调用 R 的接口。

15.1 编写从 R 调用的 C/C++函数

你可能希望编写自己的 C/C++函数,以便从 R 中调用。通常,目标是性能提升,因为 C/C++代码可能比 R 运行得更快,即使你使用向量化和其他 R 优化技术来加速。

降低到 C/C++级别的另一个可能目标是专门的 I/O。例如,R 在标准互联网通信系统的第 3 层使用 TCP 协议,但在某些设置中 UDP 可能更快。

www.it-ebooks.info

在使用 UDP 时,你需要 C/C++,这些语言需要与 R 的接口。

R 实际上通过函数 .C() 和

.Call(). 后者更灵活,但需要了解 R 的内部结构,所以我们在这里坚持使用 .C()。

15.1.1 一些 R 到 C/C++的初步知识

在 C 中,二维数组以行主序存储,这与 R 的列主序相反。例如,如果您有一个 3x4 的数组,当线性查看时,第二行第二列的元素是数组的第 5 个元素,因为第一列有三个元素,这是第二列的第二元素。还要记住,C 的索引从 0 开始,而不是像 R 那样从 1 开始。

所有从 R 传递到 C 的参数都以指针的形式接收。

注意,C 函数本身必须返回 void。您通常返回的值必须通过函数的参数传递,例如以下示例中的 result。

15.1.2 示例:从方阵中提取子对角线

在这里,我们将编写 C 代码以从方阵中提取子对角线。

(感谢我的前研究生助理,黄敏宇,他编写了这个函数的早期版本。) 这是文件 sd.c 的代码:

include <R.h> // 必需

// 参数:

//

m: 一个方阵

//

n: m 的行/列数

//

k: 子对角线索引--主对角线为 0,第一个子对角线为 1

//

子对角线,第二个子对角线为 2,等等。

//

result: 请求的子对角线空间,此处返回

void subdiag(double *m, int *n, int *k, double *result)

{

int nval = *n, kval = *k;

int stride = nval + 1;

for (int i = 0, j = kval; i < nval-kval; ++i, j+= stride)

result[i] = m[j];

}

变量 stride 暗示了并行处理社区中的一个概念。比如说我们有一个 1,000 列的矩阵,我们的 C 代码正在遍历给定列中的所有元素,从上到下。再次强调,由于 C 使用行主序,如果将矩阵视为一个长向量,则连续元素之间相隔 1,000 个元素。

elements apart from each other if the matrix is viewed as one long vector.

324

第十五章

www.it-ebooks.info

这里,我们会说我们以 1,000 的步长遍历这个长向量--也就是说,访问每千个元素。

15.1.3 编译和运行代码

您可以使用 R 编译您的代码。例如,在 Linux 终端窗口中,我们可以像这样编译我们的文件:

% R CMD SHLIB sd.c

gcc -std=gnu99 -I/usr/share/R/include

-fpic -g -O2 -c sd.c -o sd.o

gcc -std=gnu99 -shared -o sd.so sd.o

-L/usr/lib/R/lib -lR

这将生成动态共享库文件 sd.so

注意到 R 在示例输出中报告了如何调用 GCC。如果您有特殊要求,例如需要链接的特殊库,您也可以手动运行这些命令。另外,请注意 includelib 目录的位置可能取决于系统。

注意

GCC 对于 Linux 系统很容易下载。对于 Windows,它包含在 Cygwin 中,这是一个可以从 http://www.cygwin.com/ 获取的开源软件包。

然后,我们可以将我们的库加载到 R 中,并像这样调用我们的 C 函数:

dyn.load("sd.so")

m <- rbind(1:5, 6:10, 11:15, 16:20, 21:25)

k <- 2

.C("subdiag", as.double(m), as.integer(dim(m)[1]), as.integer(k), result=double(dim(m)[1]-k))

[[1]]

[1] 1 6 11 16 21 2 7 12 17 22 3 8 13 18 23 4 9 14 19 24 5 10 15 20 25

[[2]]

[1] 5

[[3]]

[1] 2

$result

[1] 11 17 23

为了方便起见,我们给形式参数(在 C 代码中)和实际参数(在 R 代码中)都命名为 result。请注意,我们需要在我们的 R 代码中为 result 分配空间。

如示例所示,返回值采用由 R 调用中的参数组成的列表形式。在这种情况下,调用有四个参数(除了函数名之外),因此返回的列表有四个部分。通常,一些参数在 C 代码执行过程中会发生变化,就像这里的 result 一样。

将 R 与其他语言接口

325

www.it-ebooks.info

15.1.4 R/C 代码调试

第十三章讨论了调试 R 代码的许多工具和方法。

然而,R/C 接口提出了额外的挑战。在这里使用像 GDB 这样的调试工具的问题是你必须首先将其应用于 R

本身。

以下是用 GDB 进行 R/C 调试步骤的概述

以我们之前的 sd.c 代码为例。

$ R -d gdb

GNU gdb 6.8-debian

...

(gdb) run

开始程序:/usr/lib/R/bin/exec/R

...

dyn.load("sd.so")

在这里按 Ctrl-C

程序收到信号 SIGINT,中断。

0xb7ffa430 in __kernel_vsyscall ()

(gdb) b subdiag

断点 1 在 0xb77683f3:文件 sd.c,行 3。

(gdb) continue

继续执行。

断点 1,subdiag (m=0x92b9480, n=0x9482328, k=0x9482348, result=0x9817148) 在 sd.c:3

3

int nval = *n, kval = *k;

(gdb)

那么,这次调试过程中发生了什么?

我们从命令行窗口启动了调试器 GDB,其中包含了加载了 R 的 GDB:

在终端窗口的命令行中:

R -d gdb

我们告诉 GDB 运行 R:

(gdb) run

我们像往常一样将编译好的 C 代码加载到 R 中:

dyn.load("sd.so")

我们按下了 CTRL-C 中断键对来暂停 R 并回到 GDB 提示符。

我们在 subdiag() 的入口处设置了断点:

(gdb) b subdiag

326

第十五章

www.it-ebooks.info

我们告诉 GDB 继续执行 R(我们需要再次按回车键以获得 R 提示符):

(gdb) continue

我们然后执行我们的 C 代码:

m <- rbind(1:5, 6:10, 11:15, 16:20, 21:25)

k <- 2

.C("subdiag", as.double(m), as.integer(dim(m)[1]), as.integer(k),

  • result=double(dim(m)[1]-k))

断点 1,subdiag (m=0x942f270, n=0x96c3328, k=0x96c3348, result=0x9a58148) 在 subdiag.c:46

46 if (*n < 1) error("n < 1\n");

在这个阶段,我们可以像往常一样使用 GDB 进行调试。如果你不熟悉 GDB,你可能想尝试网上众多的快速教程之一。

表 15-1 列出了一些最有用的命令。

表 15-1:常见的 GDB 命令

命令

描述

l

列出代码行

b

设置断点

r

运行/重新运行

n

跳到下一个语句

s

进入函数调用

p

打印变量或表达式

c

继续

h

帮助

q

退出

15.1.5 扩展示例:离散值时间序列的预测

回想我们在 2.5.2 节中的例子,我们观察了 0 和 1 值的数据,每个时间周期一个值,并尝试使用多数规则从之前的 k 个值中预测任何周期的值。我们为此开发了两个竞争性的函数,preda() 和 predb(),如下所示:

prediction in discrete time series; 0s and 1s; use k consecutive

observations to predict the next, using majority rule; calculate the

error rate

preda <- function(x,k) {

n <- length(x)

k2 <- k/2

the vector pred will contain our predicted values

pred <- vector(length=n-k)

Interfacing R to Other Languages

327

www.it-ebooks.info

for (i in 1:(n-k)) {

if (sum(x[i:(i+(k-1))]) >= k2) pred[i] <- 1 else pred[i] <- 0

}

return(mean(abs(pred-x[(k+1):n])))

}

predb <- function(x,k) {

n <- length(x)

k2 <- k/2

pred <- vector(length=n-k)

sm <- sum(x[1:k])

if (sm >= k2) pred[1] <- 1 else pred[1] <- 0

if (n-k >= 2) {

for (i in 2:(n-k)) {

sm <- sm + x[i+k-1] - x[i-1]

if (sm >= k2) pred[i] <- 1 else pred[i] <- 0

}

}

return(mean(abs(pred-x[(k+1):n])))

}

由于后者避免了重复计算,我们推测它可能会更快。现在是检查这个的时候了。

y <- sample(0:1,100000,replace=T)

system.time(preda(y,1000))

user system elapsed

3.816

0.016

3.873

system.time(predb(y,1000))

user system elapsed

1.392

0.008

1.427

嘿,不错!这相当是一个改进。

然而,你应该始终询问 R 是否已经有一个经过微调的函数可以满足你的需求。因为我们基本上是在计算移动平均,我们可能会尝试使用 filter() 函数,使用一个常数系数向量,如下所示:

predc <- function(x,k) {

n <- length(x)

f <- filter(x,rep(1,k),sides=1)[k:(n-1)]

k2 <- k/2

pred <- as.integer(f >= k2)

return(mean(abs(pred-x[(k+1):n])))

}

328

Chapter 15

www.it-ebooks.info

这甚至比我们的第一个版本更加紧凑。但它很难阅读,并且由于我们将很快探讨的原因,它可能不会那么快。让我们检查一下。

system.time(predc(y,1000))

user system elapsed

3.872

0.016

3.945

嗯,我们的第二个版本至今仍然是冠军。这实际上是可以预料的,因为查看源代码就可以看到。输入以下内容显示了该函数的源代码:

filter

这揭示了(此处未显示)filter1() 被调用的信息。后者是用 C 编写的,这应该会给我们带来一些速度提升,但它仍然存在重复计算的问题——因此速度较慢。

因此,让我们编写自己的 C 代码。

include <R.h>

void predd(int *x, int *n, int *k, double *errrate)

{

int nval = *n, kval = *k, nk = nval - kval, i;

int sm = 0; // 移动总和

int errs = 0; // 错误计数

int pred; // 预测值

double k2 = kval/2.0;

// 通过计算初始窗口进行初始化

for (i = 0; i < kval; i++) sm += x[i];

if (sm >= k2) pred = 1; else pred = 0;

errs = abs(pred-x[kval]);

for (i = 1; i < nk; i++) {

sm = sm + x[i+kval-1] - x[i-1];

if (sm >= k2) pred = 1; else pred = 0;

errs += abs(pred-x[i+kval]);

}

*errrate = (double) errs / nk;

}

这基本上是将之前的 predb() “手动翻译”成 C 语言。让我们看看它是否能超越 predb()。

system.time(.C("predd",as.integer(y),as.integer(length(y)),as.integer(1000),

errrate=double(1)))

user system elapsed

0.004

0.000

0.003

R 与其他语言的接口

329

www.it-ebooks.info

加速效果令人惊叹。

你可以看到,用 C 语言编写某些函数是值得努力的。

这对于涉及迭代的函数尤其如此,因为 R 的迭代构造,如 for(),速度较慢。

15.2 从 Python 使用 R

Python 是一种优雅且强大的语言,但它缺乏内置的统计和数据操作功能,这两个领域是 R 的强项。本节演示了如何使用 RPy(两种语言之间最受欢迎的接口之一)从 Python 调用 R。

15.2.1 安装 RPy

RPy 是一个 Python 模块,允许从 Python 访问 R。为了提高效率,它可以与 NumPy 一起使用。

你可以从 http://rpy 获取源代码构建模块。

.sourceforge.net,或者下载一个预构建版本。如果你正在运行 Ubuntu,只需输入以下命令:

sudo apt-get install python-rpy

要从 Python 加载 RPy(无论是 Python 交互模式还是从代码中),请执行以下命令:

from rpy import *

这将加载一个变量 r,它是一个 Python 类实例。

15.2.2 RPy 语法

从 Python 运行 R 在原则上非常简单。以下是从 >>> Python 提示符可能运行的命令示例:

r.hist(r.rnorm(100))

这将调用 R 函数 rnorm() 来生成 100 个标准正态变量,然后将这些值输入到 R 的直方图函数 hist() 中。

如你所见,R 名称以 r. 为前缀,反映了 R 函数的 Python 包装器是 r 类实例的成员。

如果不进行优化,前面的代码将产生丑陋的输出,你的(可能大量的!)数据将出现在图表标题和 x 轴标签中。

你可以通过提供标题和标签来避免这种情况,如下例所示:

r.hist(r.rnorm(100),main='',xlab='')

RPy 语法有时比这些示例所表明的要复杂。问题是 R 和 Python 语法可能冲突。例如,330

第十五章

www.it-ebooks.info

考虑调用 R 线性模型函数 lm()。在我们的例子中,我们将从 a 预测 b。

a = [5,12,13]

b = [10,28,30]

lmout = r.lm('v2 ~ v1',data=r.data_frame(v1=a,v2=b))

这比直接在 R 中做要复杂一些。这里有什么问题?

首先,由于 Python 语法不包含波浪线字符,我们需要通过字符串指定模型公式。由于这已经在 R 中完成,因此这不是一个重大的改变。

其次,我们需要一个数据框来包含我们的数据。我们创建了一个

使用 R 的 data.frame()函数。为了在 R 函数名称中形成点,我们需要在 Python 端使用下划线。因此我们调用了 r.data_frame()。请注意,在这个调用中,我们命名了数据框的列 v1 和 v2,然后在我们模型公式中使用这些列。

输出对象是一个 Python 字典(类似于 R 的列表类型),正如你在这里可以看到的(部分):

lmout

{'qr': {'pivot': [1, 2], 'qr': array([[ -1.73205081, -17.32050808],

[ 0.57735027, -6.164414 ],

[ 0.57735027,

0.78355007]]), 'qraux':

你应该能够识别 lm()对象的各个属性。例如,拟合回归线的系数,如果这在 R 中完成,将包含在 lmout$coefficients 中,在这里 Python 中作为 lmout['coefficients']。因此,你可以相应地访问这些系数,例如像这样:

lmout['coefficients']

{'v1': 2.5263157894736841, '(Intercept)': -2.5964912280701729}

lmout['coefficients']['v1']

2.5263157894736841

你也可以使用 r()函数将 R 命令提交给 R 的命名空间中的变量进行工作,这在存在许多语法冲突时非常方便。

这里是如何在 RPy 中运行第 12.4 节中 wireframe()示例的:

r.library('lattice')

r.assign('a',a)

r.assign('b',b)

r('g <- expand.grid(a,b)')

r('g\(Var3 <- g\)Var1² + g\(Var1 * g\)Var2')

r('wireframe(Var3 ~ Var1+Var2,g)')

r('plot(wireframe(Var3 ~ Var1+Var2,g))')

将 R 与其他语言接口

331

www.it-ebooks.info

首先,我们使用 r.assign()将变量从 Python 的命名空间复制到 R 的命名空间中。然后我们运行 expand.grid()(在名称中使用点而不是下划线,因为我们是在 R 的命名空间中运行),将结果赋值给 g。同样,后者也在 R 的命名空间中。请注意,wireframe()的调用并没有自动显示图形,因此我们需要调用 plot()。

RPy 的官方文档在 http://rpy.sourceforge.net/rpy/doc/

rpy.pdf. 此外,你还可以在 http://www.daimi.au.dk/~besen/TBiB2007/lecture-notes/rpy.html 找到一个有用的演示,“RPy—从 Python 使用 R”。

332

第十五章

www.it-ebooks.info

Image 43

16

并行 R

由于许多 R 用户有非常大的计算需求,

计算需求,为某些类型的工具设计了各种工具。

已经为 R 的并行操作设计了各种工具。

本章专门介绍并行 R。

许多并行处理的新手满怀期待地编写了一些应用的并行代码,却发现并行版本实际上比串行版本运行得更慢。由于本章将要讨论的原因,这个问题在 R 中尤其严重。

因此,理解并行处理硬件和软件的本质对于在并行世界中取得成功至关重要。这些问题将在并行 R 的常见平台背景下进行讨论。

我们将从一个代码示例开始,然后转向一般性能问题。

16.1 互链问题

考虑某种类型的网络图,例如网页链接或社交网络中的链接。设 A 为图的邻接矩阵,这意味着,例如,A[3,8]

的值为 1 或 0,取决于是否存在从节点 3 到节点 8 的链接。

对于任何两个顶点,例如任何两个网站,我们可能对互链感兴趣——即两个网站共有的出链。假设我们想找到互链的平均数量,平均

www.it-ebooks.info

在我们的数据集中所有网站对之间。这个平均值可以使用以下概述找到,对于一个 nn 列的矩阵:

1

sum = 0

2

for i = 0...n-1

3

for j = i+1...n-1

4

for k = 0...n-1 sum = sum + a[i][k]*a[j][k]

5

mean = sum / (n*(n-1)/2)

考虑到我们的图可能包含成千上万的——甚至数百万个——网页-

网站,我们的任务可能涉及相当大的计算量。处理此问题的常见方法是将计算分成更小的块,然后同时处理每个块,例如在单独的计算机上。

假设我们有两台可用的计算机。我们可能让一台计算机处理第 2 行 for i 循环中的所有奇数 i 值,而让第二台计算机处理偶数值。或者,由于双核计算机现在相当普遍,我们可以在单台计算机上采取相同的方法。这听起来可能很简单,但正如你将在本章中学到的那样,可能会出现许多重大问题。

16.2 介绍 snow 包

Luke Tierney 的 snow(简单工作站网络)包,可以从 CRAN R 代码仓库获得,可以说是最简单、最容易使用的并行 R 包之一,也是最受欢迎的。

注意

并行 R 的 CRAN 任务视图页面, http://cran.r-project.org/web/views/

高性能计算.html ,列出了可用的并行 R 包的最新列表。

要了解 snow 是如何工作的,以下是对互链问题的代码示例

在上一节中描述的:

1

snow 版本的互链问题

2

3

mtl <- function(ichunk,m) {

4

n <- ncol(m)

5

matches <- 0

6

for (i in ichunk) {

7

if (i < n) {

8

rowi <- m[i,]

9

matches <- matches +

10

sum(m[(i+1):n,] %*% rowi)

11

}

12

}

13

matches

14

}

334

第十六章

www.it-ebooks.info

15

16

mutlinks <- function(cls,m) {

17

n <- nrow(m)

18

nc <- length(cls)

19

确定哪个工作器获得哪个 i 块的块

20

options(warn=-1)

21

ichunks <- split(1:n,1:nc)

22

options(warn=0)

23

counts <- clusterApply(cls,ichunks,mtl,m)

24

do.call(sum,counts) / (n*(n-1)/2)

25

}

假设我们在这段代码文件 SnowMutLinks.R 中。让我们首先讨论如何运行它。

16.2.1 运行 snow 代码

运行上述 snow 代码涉及以下步骤:

1.

加载代码。

2.

加载 snow 库。

形成 snow 集群。

4.

设置感兴趣的邻接矩阵。

5.

在你形成的集群上运行你的代码。

假设我们在双核机器上运行,我们向 R 发出以下命令:

source("SnowMutLinks.R")

library(snow)

cl <- makeCluster(type="SOCK",c("localhost","localhost"))

testm <- matrix(sample(0:1,16,replace=T),nrow=4)

mutlinks(cl,testm)

[1] 0.6666667

在这里,我们指示 snow 在我们的机器上启动两个新的 R 进程

machine(localhost 是本地机器的标准网络名称),在这里我将称之为工作者。我将原始 R 进程(我们输入前面命令的那个进程)称为管理者。因此,在这个时候,机器上将运行三个 R 实例(如果你在 Linux 环境中,可以通过运行 ps 命令看到它们)。

工作者在 snow 术语中形成一个集群,我们将其命名为 cl。

snow 包使用并行处理世界中众所周知的一种分散/聚合范式,其工作方式如下:

1.

管理者将数据分割成块,并将它们分发给工作者(分散阶段)。

并行 R

335

www.it-ebooks.info

工作者处理他们的数据块。

3.

管理者从工作者那里收集结果(聚合阶段),并根据应用需求将它们组合起来。

我们指定了管理者和工作者之间的通信

与网络套接字(在第十章中介绍)进行通信。

这里是一个测试矩阵,用于检查代码:

testm

[,1] [,2] [,3] [,4]

[1,]

1

0

0

1

[2,]

0

0

0

0

[3,]

1

0

1

1

[4,]

0

1

0

1

第 1 行与第 2 行没有共同的外部链接,与第 2 行有两个共同的外部链接,

第 3 行与第 4 行有一个共同的外部链接,而第 2 行与其它行没有共同的外部链接,但第 3 行与第 4 行有一个共同的外部链接。这样,总共有 4 个共同的外部链接,占 4 × 3 / 2 = 6 对中的 4/6,即 0.6666667,正如你之前看到的。

你可以创建任何大小的集群,只要你有机器。

例如,在我所在的部门,我有机器的网络名称是 pc28、pc29 和 pc30。每台机器都是双核的,因此我可以创建一个包含六个工作者的集群,如下所示:

cl6 <- makeCluster(type="SOCK",c("pc28","pc28","pc29","pc29","pc30","pc30"))

16.2.2 分析 snow 代码

现在我们来看看 mutlinks()函数是如何工作的。首先,我们在第 17 行中检测矩阵 m 的行数,在第 18 行中检测我们集群中的工作者数量。

接下来,我们需要确定哪个工作者将处理我们之前在第 16.1 节中展示的轮廓代码中的 for i 循环中的哪个 i 值。R 的 split()函数非常适合这个任务。例如,在一个 4 行矩阵和 2 个工作者集群的情况下,该调用会产生以下结果:

split(1:4,1:2)

$1

[1] 1 3

$2

[1] 2 4

返回一个 R 列表,其第一个元素是向量(1,3),第二个元素是(2,4)。这将设置一个 R 进程处理 i 的奇数值,另一个进程处理偶数值,正如我们之前讨论的那样。我们避免了336

第十六章

www.it-ebooks.info

通过调用 options()来避免 split()可能会给出的警告(“数据长度不是分割变量的倍数”)。

真正的工作是在第 23 行完成的,在那里我们调用 snow 函数

clusterApply(). 这个函数初始化对相同指定函数(这里的 mtl())的调用,其中包含一些针对每个工作者的特定参数和一些对所有工作者都可选的参数。所以,这里第 23 行的调用是这样的:1.

工作者 1 将被指示使用 ichunks[[1]]和 m 作为参数调用函数 mtl()。

工作者 2 将使用 ichunks[[2]]和 m 调用 mtl(),对所有工作者也是如此。

每个工作者将执行其分配的任务,然后将结果返回给管理者。

管理者将收集所有这些结果到一个 R 列表中,我们在这里将其分配给 counts。

在这一点上,我们只需要对 counts 中的所有元素进行求和。好吧,我不应该说“仅仅”,因为在第 24 行有一些小问题需要解决。

R 的 sum()函数可以对多个向量参数进行操作,如下所示:

sum(1:2,c(4,10))

[1] 17

但在这里,counts 是一个 R 列表,而不是(数值)向量。因此,我们依赖于 do.call()从 counts 中提取向量,然后对它们调用 sum()。

注意第 9 行和第 10 行。正如你所知,在 R 中,我们尽可能地尝试将计算向量化以提高性能。通过将事物表示为矩阵-向量乘积的形式,我们将第 16.1 节概述中的 for j 和 for k 循环替换为一个基于向量的单一表达式。

16.2.3 可以获得多少加速?

我在一个 1000x1000 的矩阵 m1000 上尝试了这段代码。我首先在一个 4 个工作者的集群上运行它,然后在一个 12 个工作者的集群上运行。原则上,我应该有 4 和 12 的加速比。但实际上,经过的时间是 6.2 秒和 5.0 秒。将这些数字与 16.9 秒的非并行运行时间进行比较。(后者是 mtl(1:1000,m1000)的调用。)因此,我获得了大约 2.7 的加速比,而不是 4.0 的理论值,对于 4 个工作者的集群,以及在 12 节点系统上的 3.4 而不是 12.0。请注意,运行之间的时间会有所变化。)出了什么问题?

在几乎任何并行处理应用程序中,你都会遇到开销,或者

“浪费”在非计算活动上的时间。在我们的例子中,存在从管理者到工作者的矩阵传输所需的时间开销。我们还在将函数 mtl()本身发送到工作者时遇到了一点开销。当工作者完成他们的任务后,将结果返回给管理者也会造成一些开销。我们将在第 16.4.1 节中详细讨论这一点。

337

www.it-ebooks.info

当我们讨论一般性能考虑时。

16.2.4 扩展示例:K-Means 聚类

要了解更多关于 snow 功能的能力,我们将查看另一个示例,这个示例涉及 k-means 聚类(KMC)。

KMC 是一种用于数据探索的技术。在查看数据的散点图时,你可能会有这样的感觉,即观测值倾向于聚集成组,KMC 是一种寻找此类组的方法。输出结果包括组的质心。

The following is an outline of the algorithm:

1

for iter = 1,2,...,niters

2

set vector and count totals to 0

3

for i = 1,...,nrow(m)

4

set j = index of the closest group center to m[i,]

5

add m[i,] to the vector total for group j, v[j]

6

add 1 to the count total for group j, c[j]

7

for j = 1,...,ngrps

8

set new center of group j = v[j] / c[j]

Here, we specify niters iterations, with initcenters as our initial guesses for the centers of the groups. Our data is in the matrix m, and there are ngrps groups.

The following is the snow code to compute KMC in parallel:

1

snow version of k-means clustering problem

2

3

library(snow)

4

5

returns distances from x to each vector in y;

6

here x is a single vector and y is a bunch of them;

7

define distance between 2 points to be the sum of the absolute values 8

两个点之间距离的定义是它们分量差的绝对值之和;例如,点(5,4.2)和 9 之间的距离

(3,5.6) is 2 + 1.4 = 3.4

10

dst <- function(x,y) {

11

tmpmat <- matrix(abs(x-y),byrow=T,ncol=length(x)) # note recycling 12

rowSums(tmpmat)

13

}

14

15

will check this worker's mchunk matrix against currctrs, the current 16

centers of the groups, returning a matrix; row j of the matrix will 17

consist of the vector sum of the points in mchunk closest to jth

18

当前中心,以及此类点的计数

19

findnewgrps <- function(currctrs) {

20

ngrps <- nrow(currctrs)

21

spacedim <- ncol(currctrs) # what dimension space are we in?

338

第十六章

www.it-ebooks.info

22

set up the return matrix

23

sumcounts <- matrix(rep(0,ngrps*(spacedim+1)),nrow=ngrps)

24

for (i in 1:nrow(mchunk)) {

25

dsts <- dst(mchunk[i,],t(currctrs))

26

j <- which.min(dsts)

27

sumcounts[j,] <- sumcounts[j,] + c(mchunk[i,],1)

28

}

29

sumcounts

30

}

31

32

parkm <- function(cls,m,niters,initcenters) {

33

n <- nrow(m)

34

spacedim <- ncol(m) # what dimension space are we in?

35

determine which worker gets which chunk of rows of m

36

options(warn=-1)

37

ichunks <- split(1:n,1:length(cls))

38

options(warn=0)

39

form row chunks

40

mchunks <- lapply(ichunks,function(ichunk) m[ichunk,])

41

mcf <- function(mchunk) mchunk <<- mchunk

42

send row chunks to workers; each chunk will be a global variable at 43

the worker, named mchunk

44

invisible(clusterApply(cls,mchunks,mcf))

45

send dst() to workers

46

clusterExport(cls,"dst")

47

start iterations

48

centers <- initcenters

49

for (i in 1:niters) {

50

sumcounts <- clusterCall(cls,findnewgrps,centers)

51

tmp <- Reduce("+",sumcounts)

52

centers <- tmp[,1:spacedim] / tmp[,spacedim+1]

53

if a group is empty, let's set its center to 0s

54

centers[is.nan(centers)] <- 0

55

}

56

centers

57

}

The code here is largely similar to our earlier mutual outlinks example.

然而,这里有几个新的 snow 调用和一个旧调用的不同用法。

让我们从第 39 行到第 44 行开始。由于我们的矩阵 m 在每次迭代中都不会改变,我们绝对不希望反复将其发送到工作者,这会加剧开销问题。因此,首先我们需要将 m 的分配块发送给每个工作者,只发送一次。这是在第 44 行完成的。

通过我们之前使用但现在需要发挥创造性的 snow 的 clusterApply() 函数,我们在第 41 行定义了函数 mcf(),该函数将在并行 R 中运行。

339

www.it-ebooks.info

在工作者上,接受管理者的数据块并将其作为全局变量 mchunk 保留在工作者上。

第 46 行使用了新的 snow 函数,clusterExport(),其任务是复制管理员的全局变量到工作者。这里的变量实际上是一个函数,dst()。我们需要单独发送它的原因如下:第 50 行的调用会将函数 findnewgrps() 发送到工作者,尽管该函数调用了 dst(),但 snow 不会知道也要发送后者。因此,我们自行发送。

第 50 行本身使用了另一个新的 snow 调用,clusterCall()。这指示每个工作者调用 findnewgrps(),其中 centers 作为参数。

回想一下,每个工作者都有一个不同的矩阵块,因此这个调用将针对每个工作者的不同数据进行操作。这再次引发了关于全局变量使用的争议,这在第 7.8.4 节中讨论过。

一些软件开发者可能会对 findnewgrps() 中使用隐藏参数感到困扰。另一方面,如前所述,使用 mchunk 作为参数意味着需要反复将其发送到工作者,这会损害性能。

最后,看看第 51 行。snow 函数 clusterApply() 总是返回一个 R 列表。在这种情况下,返回值在 sumcounts 中,每个元素都是一个矩阵。我们需要对矩阵进行求和,生成一个总计矩阵。

使用 R 的 sum() 函数是不行的,因为它会将矩阵的所有元素加起来变成一个单一的数字。我们需要的是矩阵加法。

调用 R 的 Reduce() 函数可以进行矩阵加法。回想一下,R 中的任何算术运算都是以函数的形式实现的;在这种情况下,它是以 "+" 函数的形式实现的。然后对 Reduce() 的调用会依次将 "+" 应用到 sumcounts 列表的元素上。当然,我们也可以直接写一个循环来完成这个操作,但使用 Reduce() 可能会给我们带来一点性能提升。

16.3 转向 C

正如你所看到的,使用并行 R 可以大大加快你的 R 代码的执行速度。这允许你保留 R 的便利性和表达力,同时仍然可以改善大型应用程序中的长时间运行。如果并行化的 R 给你足够好的性能,那么一切就都很好了。

尽管如此,并行 R 仍然是 R,因此仍然受到第十四章中讨论的性能问题的影响。回想一下,在第十四章中提供的一个解决方案是将代码的关键部分用 C 语言编写,然后从主 R 程序中调用该代码。(这里的 C 指的是 C 或 C++。)我们将从并行处理的角度来探讨这个问题。在这里,我们不是编写并行 R 代码,而是编写调用并行 C 的普通 R 代码。(我假设您了解 C 语言。)

16.3.1 使用多核机器

这里涵盖的 C 代码仅在多核系统上运行,因此我们必须讨论这类系统的本质。

340

第十六章

www.it-ebooks.info

您可能熟悉双核机器。任何计算机都包含一个 CPU,这是实际运行您程序的部分。本质上,双核机器有两个 CPU,四核系统有四个,依此类推。

多核处理器允许进行并行计算!

这种并行计算是通过 线程 来实现的,它们类似于雪的工人。在计算密集型应用中,通常设置的线程数与核心数相同,例如在双核机器上设置两个线程。理想情况下,这些线程可以同时运行,尽管会出现开销问题,这将在我们查看第 16.4.1 节中的通用性能问题时进行解释。

如果您的机器具有多个核心,它将结构化为一个 共享内存 系统。所有核心都访问相同的 RAM。内存的共享特性使得核心之间的通信易于编程。如果一个线程写入内存位置,该变化对其他线程可见,而无需程序员插入代码来实现这一点。

16.3.2 扩展示例:OpenMP 中的互链问题

OpenMP 是一个在多核机器上编程的非常流行的包。

为了了解它是如何工作的,这里再次展示互链示例,这次是在可由 R 调用的 OpenMP 代码中:

1

include <omp.h>

2

include <R.h>

3

4

int tot; // 所有线程的匹配总计数

5

6

// 处理行对 (i,i+1),(i,i+2),...

7

int procpairs(int i, int *m, int n)

8

{ int j,k,sum=0;

9

for (j = i+1; j < n; j++) {

10

for (k = 0; k < n; k++)

11

// find m[i][k]*m[j][k] but remember R uses col-major order

12

sum += m[nk+i] * m[nk+j];

13

}

14

return sum;

15

}

16

17

void mutlinks(int *m, int *n, double *mlmean)

18

{ int nval = *n;

19

tot = 0;

20

pragma omp parallel

21

{ int i,mysum=0,

22

me = omp_get_thread_num(),

23

nth = omp_get_num_threads();

24

// 在检查所有 (i,j) 对时,根据 i 分配工作;

25

// 此线程 me 将处理所有等于 me mod nth 的 i

26

for (i = me; i < nval; i += nth) {

并行 R

341

www.it-ebooks.info

27

mysum += procpairs(i,m,nval);

28

}

29

pragma omp atomic

30

tot += mysum;

31

}

32

int divisor = nval * (nval-1) / 2;

33

*mlmean = ((float) tot)/divisor;

34

}

16.3.3 运行 OpenMP 代码

再次,编译遵循第十五章中的配方。不过,我们确实需要通过使用-fopenmp 和-lgomp 选项来链接 OpenMP 库。假设我们的源文件是romp.c。然后我们使用以下命令来运行代码:

gcc -std=gnu99 -fopenmp -I/usr/share/R/include -fpic -g -O2 -c romp.c -o romp.o gcc -std=gnu99 -shared -o romp.so romp.o -L/usr/lib/R/lib -lR -lgomp 这里是一个 R 测试:

dyn.load("romp.so")

Sys.setenv(OMP_NUM_THREADS=4)

n <- 1000

m <- matrix(sample(0:1,n²,replace=T),nrow=n)

system.time(z <- .C("mutlinks",as.integer(m),as.integer(n),result=double(1))) user system elapsed

0.830

0.000

0.218

z$result

[1] 249.9471

在 OpenMP 中指定线程数量的典型方式是通过操作系统环境变量,OMP_NUM_THREADS。R 能够通过 Sys.setenv()函数设置操作系统环境变量。在这里,我将线程数设置为 4,因为我正在一个四核机器上运行。

注意运行时间——只有 0.2 秒!这比之前 5.0 秒的

我们之前看到的 12 节点雪系统的时间。这可能会让一些读者感到惊讶,因为我们的代码在雪版本中已经相当程度地进行了向量化,如前所述。向量化是好的,但同样,R 有很多隐藏的开销来源,所以 C 可能做得更好。

注意

我尝试了 R 的新字节编译函数 cmpfun(),但 mtl()实际上变慢了

因此,如果你愿意用并行 C 编写你代码的一部分,可能会实现显著的加速。

342

第十六章

www.it-ebooks.info

16.3.4 OpenMP 代码分析

OpenMP 代码是 C,增加了pragmas,这些 pragma 指示编译器插入一些库代码以执行 OpenMP 操作。例如,看看第 20 行。当执行到达这个点时,线程将被激活。然后每个线程将并行执行随后的块——第 21 行到第 31 行。

一个关键点是变量作用域。从第 21 行开始的块内的所有变量都是它们特定线程的局部变量。例如,我们之所以在第 21 行将总变量命名为 mysum,是因为每个线程将维护自己的总和。

相比之下,第 4 行的全局变量 tot 被所有线程共享。每个线程在第 30 行对那个总和做出贡献。

但是,即使在第 18 行的变量 nval 也是与所有线程共享的(在 mutlinks()执行期间),因为它是在第 21 行开始的块外部声明的。所以,尽管它从 C 的作用域角度来看是一个局部变量,但对于所有线程来说它是全局的。实际上,我们也可以在第 21 行声明 tot。它需要被所有线程共享,但由于它不在 mutlinks()外部使用,它可以在第 18 行声明。

第 29 行包含另一个 pragma,atomic。这个 pragma 只适用于它后面的单行——在这个例子中是第 30 行——而不是整个块。

原子预处理语句的目的是避免并行处理领域所说的“竞争条件”。这个术语描述了两个线程同时更新一个变量的情况,这可能会导致错误的结果。原子预处理语句确保一次只有一个线程执行第 30 行。注意,这暗示了在这个代码段中,我们的并行程序暂时变成了串行,这可能是减慢速度的一个潜在原因。

在所有这些中,管理者的角色在哪里?实际上,管理者是原始线程,它执行第 18 和 19 行,以及 .C(),这是调用 mutlinks() 的 R 函数。当工作线程在第 21 行被激活时,管理者进入休眠状态。工作线程一旦完成第 31 行,就会进入休眠状态。在那个时刻,管理者恢复执行。由于管理者在工作线程执行期间处于休眠状态,我们希望有尽可能多的工作线程,就像机器的核数一样。

函数 procpairs() 很简单,但请注意访问矩阵 m 的方式。回想一下第十五章关于 R 与 C 接口讨论中的内容,两种语言存储矩阵的方式不同:R 中是按列存储,C 中是按行存储。我们需要注意这个差异。此外,我们将矩阵 m 作为一维数组处理,这在并行 C 代码中很常见。换句话说,如果 n 是,比如说,4,那么我们将 m 视为一个包含 16 个元素的向量。由于 R 的列主序特性

矩阵存储,向量将首先包含第 1 列的四个元素,然后是第 2 列的四个元素,依此类推。为了进一步复杂化问题,我们必须记住,C 中的数组索引从 0 开始,而不是像 R 中的那样从 1 开始。

将所有这些放在一起,就得到了第 12 行的乘法。这里的因子是 C 代码中 m 版本的 (k,i) 和 (k,j) 元素,在 R 代码中对应的是 (i+1,k+1) 和 (j+1,k+1) 元素。

并行 R

343

www.it-ebooks.info

16.3.5 其他 OpenMP 预处理器

OpenMP 包含了各种各样的可能操作——多得无法在此一一列举。本节提供了我对一些特别有用的 OpenMP 预处理器的概述。

16.3.5.1 omp barrier 预处理语句

并行处理术语 屏障 指的是线程会合的代码行。omp barrier 预处理语句的语法很简单:

#pragma omp barrier

当一个线程达到屏障时,它的执行会暂停,直到所有其他线程都到达那条线。这对于迭代算法非常有用;线程在每个迭代的末尾都在屏障处等待。

注意,除了这个显式的屏障调用之外,一些其他的预处理语句在其块之后放置了一个隐式的屏障。这些包括单线程和并行。例如,在前面的列表中,第 31 行之后立即有一个隐式的屏障,这就是为什么管理器保持休眠状态直到所有工作线程完成。

16.3.5.2 omp critical Pragma

此 pragma 之后的部分是一个临界区,意味着一次只允许一个线程执行。omp critical pragma 基本上与前面讨论的 atomic pragma 具有相同的目的,只不过后者仅限于单个语句。

注意

OpenMP 的设计者定义了一个特殊的 pragma 来处理这种单语句情况,希望编译器可以将它转换为一个特别快速的机器指令。

这是 omp critical 语法的示例:

1

pragma omp critical

2

{

3

// 在此处放置一个或多个语句

4

}

16.3.5.3 omp single Pragma

此 pragma 之后的部分将由只有一个线程执行。以下是 omp single pragma 的语法:

1

pragma omp single

2

{

3

// 在此处放置一个或多个语句

4

}

344

第十六章

www.it-ebooks.info

这对于初始化由线程共享的求和变量很有用。如前所述,在块之后放置了一个自动屏障。

这应该对你来说是有意义的。如果一个线程正在初始化求和,你不会希望使用这个变量的其他线程在求和被正确设置之前继续执行。

你可以在我的开源教科书上了解更多关于 OpenMP 的信息,关于并行编程的——

allel 处理在http://heather.cs.ucdavis.edu/parprocbook

16.3.6 GPU 编程

另一种共享内存并行硬件类型是图形处理单元(GPU)。如果你机器中有用于游戏的复杂显卡,你可能不会意识到它也是一个非常强大的计算设备——强大到经常用“桌面上的超级计算机!”这个口号来指代配备了高端 GPU 的 PC。

与 OpenMP 一样,这里的想法是,你不需要编写并行 R 代码,而是编写与并行 C 接口的 R 代码。(与 OpenMP 的情况类似,C

这里指的是 C 语言的略微增强版本。)技术细节变得相当复杂,所以我不展示任何代码示例,但对该平台的概述是值得的。

如前所述,GPU 遵循共享内存/线程模型,

但规模要大得多。它们有数十个,甚至数百个

核心(取决于你如何定义核心)。一个主要区别是,可以在一个块中同时运行多个线程,这可以产生一定的效率。

访问 GPU 的程序从你机器的 CPU 开始运行,

被称为主机。然后它们在 GPU 上启动代码,或设备

这意味着你的数据必须从主机传输到设备,设备完成计算后,结果必须传输回主机。

在撰写本文时,GPU 尚未在 R 用户中变得普遍。

最常见的使用方式可能是通过 CRAN 包 gputools,它包含一些可以从 R 调用的矩阵代数和统计例程。例如,考虑矩阵求逆。R 提供了 solve()函数来完成这个任务,但在 gputools 中有一个名为 gpuSolve()的并行替代方案。

关于 GPU 编程的更多信息,请再次参阅我关于并行编程的书籍。

http://heather.cs.ucdavis.edu/parprocbook 处处理。

16.4 一般性能考虑

本节讨论了一些在并行化 R 应用时可能普遍有用的问题。我将介绍关于主要负载来源的一些材料,然后讨论几个算法问题。

并行 R

345

www.it-ebooks.info

16.4.1 负载来源

了解开销的物理原因对于成功的并行编程至关重要。让我们从共享内存和网络计算机这两个主要平台的角度来看看这些问题。

16.4.1.1 共享内存机器

如前所述,多核机器中的内存共享使得编程更容易。然而,共享也会产生开销,因为如果两个核心同时尝试访问内存,它们会相互冲突。这意味着其中一个需要等待,从而产生开销。这种开销通常在数百纳秒(秒的十亿分之一)的范围内。这听起来真的很小,但请记住,CPU 以亚纳秒的速度工作,所以内存访问经常成为瓶颈。

每个核心也可能有一个缓存,其中它保留了一些共享内存的本地副本。它的目的是减少核心之间的内存竞争,但它会产生自己的开销,包括保持缓存之间一致性的时间开销。

回想一下,GPU 是特殊的多核机器。因此,它们会遭受我描述的问题,以及更多。首先,延迟,即从内存读取请求后,第一个比特到达 GPU 的时间延迟,在 GPU 中相当长。

在主机和设备之间传输数据时,也会产生开销。这里的延迟在微秒(百万分之一秒)的量级,与 CPU 和 GPU 的纳秒尺度相比,这是一个永恒的时间。

GPU 对于某些类别的应用具有巨大的性能潜力,但开销可能是一个主要问题。gputools 的作者指出,他们的矩阵运算只有在矩阵大小达到 1000 时才开始实现加速。

通过 1000 个例子。我编写了我们共同链接应用的 GPU 版本,其运行时间为 3.0 秒——大约是雪版的一半,但仍然比 OpenMP 实现慢得多。

再次,有方法可以缓解这些问题,但它们需要非常仔细、有创造性的编程和深入了解物理 GPU 结构。

16.4.1.2 计算机网络系统

如您之前所见,实现并行计算的另一种方式是通过计算机网络系统。您仍然有多个 CPU,但在这个案例中,它们位于完全独立的计算机中,每个计算机都有自己的内存。

如前所述,网络数据传输会产生开销。其延迟再次在微秒级别。因此,即使通过网络访问少量数据也会产生很大的延迟。

还要注意,雪有额外的开销,因为它会改变数值

在发送之前将对象如向量和矩阵转换为字符形式,比如说从管理者到工作者。这不仅涉及到转换的时间(无论是从数值形式转换为字符形式还是346

第十六章

www.it-ebooks.info

在接收端重新计算数值时),但字符形式往往会导致消息更长,从而增加网络传输时间。

共享内存系统可以联网,实际上,我们在前面的例子中就是这样做的。我们有一个混合情况,即从几个联网的双核计算机中形成了雪簇。

16.4.2 令人尴尬的并行应用和那些不是的应用

贫穷并不丢人,但也不算是什么荣耀。

——Tevye,《屋顶上的提琴手》

人类是唯一会脸红,或者需要脸红的动物。

——马克·吐温

在关于并行 R 的讨论中,经常听到“令人尴尬的并行”这个术语。

(以及在并行处理领域)。单词“尴尬”暗示了这样一个事实,即问题如此容易并行化,以至于其中不涉及任何智力挑战;它们是尴尬地容易。

我们在这里查看的两个示例应用都会是

被认为是令人尴尬的并行。在 16.1 节中,对互连出链问题的 for i 循环进行并行化是非常明显的。在 16.2.4 节中,KMC 示例中的工作划分也是自然且容易的。

相比之下,大多数并行排序算法需要大量的交互。例如,考虑归并排序,这是一种常见的排序数字的方法。它将待排序的向量分成两个(或更多)独立的部分,比如左半部分和右半部分,然后由两个进程并行排序。到目前为止,这是令人尴尬的并行,至少在向量被分成一半之后。但是,然后必须将两个已排序的半部分合并以产生原始向量的排序版本,而这个过程不是令人尴尬的并行。它可以并行化,但方式更为复杂。

当然,用 Tevye 的话来说,有一个令人尴尬的并行问题并不丢人!这也许并不完全是一种荣誉,但它是值得庆祝的,因为它容易编程。更重要的是,令人尴尬的并行问题通常具有低通信开销,正如前面讨论的那样,这对于性能至关重要。事实上,当大多数人提到令人尴尬的并行应用时,他们心中想的正是这种低开销。

但对于非令人尴尬的并行应用呢?不幸的是,由于 R 的函数式编程特性,并行 R 代码对于许多这样的应用来说并不适合,这是一个非常基本的原因:如第 14.3 节所述,一个像这样的语句:

x[3] <- 8

是表面上很简单,因为它可能导致整个向量 x 被重写。这实际上加剧了通信流量问题。因此,如果你的应用不是令人尴尬的并行,你的最佳策略可能是将代码的计算密集部分用 C 编写,例如使用 OpenMP 或 GPU 编程。

并行 R

347

www.it-ebooks.info

此外,请注意,即使是令人尴尬的并行化也不一定使算法高效。某些这样的算法仍然可能存在大量的通信流量,从而影响性能。

考虑在雪地中运行的 KMC 问题。假设我们要设置

增加足够多的工人,以便每个工人有相对较少的工作要做。在这种情况下,每次迭代后与经理的通信将变成运行时间的一个很大部分。在这种情况下,我们会说粒度太细,然后可能切换到使用更少的工人。这样,每个工人的任务就会更大,因此粒度会更

16.4.3 静态与动态任务分配

再次查看我们的 OpenMP 示例的第 26 行开始的循环,

以下内容为方便查阅而复制:

for (i = me; i < nval; i += nth) {

mysum += procpairs(i,m,nval);

}

这里的变量 me 是线程号,因此这段代码的效果是各个线程将工作在 i 的值的非重叠集合上。我们确实希望这些值是非重叠的,以避免重复工作和总链接数的错误计数,所以代码是好的。但现在的问题是,我们实际上是在预先分配每个线程要处理的任务。这被称为静态分配。

另一种方法是修改 for 循环,使其看起来像这样:

int nexti = 0; // 全局变量

...

for ( ; myi < n; ) { // 修改后的"for"循环

pragma omp critical

{

nexti += 1;

myi = nexti;

}

if (myi < n) {

mysum += procpairs(myi,m,nval);

...

}

}

...

这是一种动态的任务分配,其中在执行之前并没有确定哪些线程处理哪些 i 的值。任务分配是在执行过程中完成的。乍一看,动态分配似乎有更好的性能潜力。例如,假设在静态分配中348

第十六章

www.it-ebooks.info

设置时,一个线程提前完成了其最后一个 i 的值,而另一个线程还有两个 i 的值未完成。这意味着我们的程序可能会比预期晚一些完成。在并行处理术语中,我们会遇到一个负载平衡问题。使用动态分配,当还剩下两个 i 的值要处理时,完成工作的线程可以自己承担其中一个值。这样我们会获得更好的平衡,并且理论上整体运行时间会更短。

但不要急于下结论。像往常一样,我们还有开销问题要考虑。回想一下,上面代码的动态版本中使用的临界 pragma 会暂时将程序变为串行而不是并行,从而造成速度减慢。此外,由于这里讨论的技术原因过于复杂,这些 pragma 可能会引起相当大的缓存活动开销。因此,最终,动态代码实际上可能比静态版本慢得多。

已经开发出各种解决这个问题的方法,例如一个

OpenMP 中名为 guided 的构造。但我不想介绍这些,我想说的是,它们是不必要的。在大多数情况下,静态分配就足够好了。为什么是这样呢?

你可能还记得,独立同分布随机变量的总和的标准差,除以该总和的均值,当项数趋于无穷大时趋近于零。换句话说,总和是近似恒定的。这对我们的负载平衡问题有直接影响:由于静态分配中线程的总工作时间是其各个任务时间的总和,因此总工作时间将近似恒定;线程之间的差异将非常小。因此,它们将几乎同时完成,我们不需要担心负载不平衡。动态调度将不是必要的。

这种推理依赖于一个统计假设,但在实践中,这个假设通常能够很好地满足结果:在总工作时间的均匀性方面,静态调度与动态调度相当。而且由于静态调度没有动态调度那样的开销问题,在大多数情况下,静态方法将提供更好的性能。

有一个关于这个问题的更多方面需要讨论。为了说明问题,再次考虑互链示例。让我们回顾一下算法的大纲:

1

sum = 0

2

for i = 0...n-1

3

for j = i+1...n-1

4

for k = 0...n-1 sum = sum + a[i][k]*a[j][k]

5

mean = sum / (n*(n-1)/2)

假设 n 是 10000,我们有四个线程,考虑如何划分 for i 循环。一开始,我们可能决定让线程 0 处理 i 值 0 到 2499,线程 1 处理 2500 到 4999,依此类推。

然而,这会导致严重的负载不平衡,因为并行 R

349

www.it-ebooks.info

处理 i 的给定值时,所做的工量与 n-i 成比例。事实上,这就是我们实际代码中交错 i 值的原因:线程 0 处理 i 值 0、4、8...,线程 1 处理 1、5、9...,以此类推,从而实现良好的负载平衡。

因此,静态分配可能需要更多的计划。一种一般的方法是将任务(在我们的例子中是 i 值)随机分配给线程(仍然在开始工作之前这样做)。

通过这样的事先考虑,静态分配在大多数应用中应该工作得很好。

16.4.4 软件炼金术:将一般问题转化为令人尴尬的

并行问题

如前所述,从非令人尴尬的并行算法中获得良好性能很困难。幸运的是,对于统计应用,有一种方法可以将非令人尴尬的并行问题转化为令人尴尬的并行问题。关键是利用一些统计特性。

为了演示这种方法,让我们再次转向我们的共同输出-

链接问题。使用 w 个工人在链接矩阵 m 上应用的方法包括以下内容:

将 m 行的行分成 w 个块。

让每个工人找到其块中顶点对相互输出链接的平均数。

平均工人返回的结果。

可以从数学上证明,对于大型问题(你无论如何都需要并行计算的问题),这种块方法给出了与无块方法相同的统计精度估计。但与此同时,我们将一个非并行问题变成了不仅是一个并行问题,而且是一个令人尴尬的并行问题!前面概述中的工人完全独立于彼此计算。

这种方法不应与通常基于块的

并行处理方法。在这些方法中,例如在第 347 页讨论的归并排序示例,块处理是令人尴尬的并行,但结果的合并不是。相比之下,这里结果的合并由简单的平均组成,这得益于数学理论。

我在一个 4 个工作者的雪集群上尝试了这种方法来解决相互输出链接问题。这减少了运行时间到 1.5 秒。这远远优于大约 16 秒的串行时间,是 GPU 获得的加速速度的两倍。

并接近 OpenMP 时间。同时,证实了两种方法给出相同统计准确性的理论。块方法发现相互输出链接的平均数为 249.2881,与原始估计器的 249.2993 相比。

350

第十六章

www.it-ebooks.info

16.5 调试并行 R 代码

并行 R 包,如 Rmpi、snow、foreach 等,不会为每个进程设置终端窗口,因此无法在工人上使用 R 的调试器。(我的 Rdsm 包,它为 R 添加了线程功能,是这一点的例外。)

那么,您可以为这些包进行哪些调试操作?让我们以 snow 为一个具体的例子来考虑。

首先,您应该调试底层单工作进程函数,例如 16.2 节中的 mtl()。在这里,我们会设置一些参数的人工值,然后使用 R 的常规调试功能。

调试底层函数可能就足够了。然而,错误可能存在于参数本身或我们设置参数的方式中。然后事情就变得更加困难。

甚至打印出跟踪信息(如变量的值)都很困难,因为 print() 在工作进程中将不起作用。message() 函数可能适用于某些这些包;如果不适用,您可能需要求助于使用 cat() 将内容写入文件。

并行 R

351

www.it-ebooks.info

www.it-ebooks.info

Image 44

A

安装 R

本附录涵盖了安装 R 的方法

在您的系统上安装 R。您可以轻松地下载和安装预编译的

ily download and install the precompiled

binaries,使用 UNIX-包管理器

based system,或者如果您愿意,甚至可以从源代码安装。

A.1 从 CRAN 下载 R

R,无论是其基本形式还是用户编写的包,都可以在 R 主页的 Comprehensive R Archive Network (CRAN) 上找到,http://www

.r-project.org/. 点击 CRAN 并选择您附近的站点以下载适合您操作系统的适当基础包。

对于大多数用户来说,无论平台如何,安装 R 都相当简单。

您可以在 CRAN 上找到 Windows、Linux 和 Mac OS X 的预编译二进制文件。您应该能够简单地下载适当的文件并安装 R。

A.2 从 Linux 包管理器安装

如果您正在运行具有集中式软件仓库的 Linux 发行版,例如 Fedora 或 Ubuntu,而不是使用预编译的二进制文件,您

www.it-ebooks.info

可以使用您的操作系统包管理器安装 R。例如,如果您正在运行 Fedora,您可以在命令行中键入以下内容来安装 R:$ yum install R

对于基于 Debian 的系统,例如 Ubuntu,命令如下

like this:

$ sudo apt-get install r-base

查阅您发行版的文档以获取有关安装和删除包的更多详细信息。

A.3 从源代码安装

在 Linux 或其他基于 UNIX 的机器(可能包括 Mac OS X)上,您也可以自己编译 R 的源代码。只需解压源代码存档,然后遵循经典的三个命令安装程序:$ configure

$ make

$ make install

注意,您可能需要以 root 用户身份运行 make install,这取决于您的写入权限和您安装 R 的位置。如果您想安装到非标准目录,例如 /a/b/c,您可以使用 --prefix 参数运行 configure,如下所示:

$ configure --prefix=/a/b/c

如果您正在使用共享机器并且没有写入标准安装目录(如 /usr)的权限,这可能很有帮助。

354

附录 A

www.it-ebooks.info

Image 45

B

安装和使用包

R 的一个主要优势是

数千个用户编写的包

在综合 R 存档

网络(CRAN)在 R 主页上,http://

www.r-project.org/. R 包的安装在大多数情况下都很简单

的情况下,但需要注意一些细微差别

这些专业包。

本附录首先介绍一些包的基本知识,然后解释如何从你的硬盘驱动器和网络加载 R 包。

B.1 包的基本知识

R 使用包来存储相关软件的组。包含在 R 发行版中的包作为你的 R 安装树中 library 目录的子目录可见,例如 /usr/lib/R/library

注意

在 R 社区中,术语 library 通常用来代替 package .

一些包在启动 R 时会自动加载,例如 base 子目录。然而,为了节省内存和时间,R 不会自动加载所有可用的包。

www.it-ebooks.info

你可以通过输入以下内容来检查当前加载了哪些包:

.path.package()

B.2 从你的硬盘驱动器加载包

如果你需要一个在 R 安装中但尚未加载到内存中的包,你可以使用 library() 函数来加载它。例如,假设你希望生成多元正态随机向量。MASS 包中的 mvrnorm() 函数可以做到这一点。所以,按照以下方式加载包:

library(MASS)

mvrnorm() 函数现在可以使用了。它的文档也可以使用(在你加载 MASS 之前,输入 help(mvrnorm) 会生成一个错误信息)。

B.3 从网络下载包

你想要的包可能不在你的 R 安装中。开源软件的一个主要优势是人们喜欢分享。世界各地的人们都编写了自己的专用 R 包,并将它们放置在 CRAN 仓库和其他地方。

注意

用户对 CRAN 的贡献会经过审查过程,通常质量很高。然而,它们并没有像 R 本身那样经过彻底的测试。

B.3.1 自动安装包

安装包的一种方法是使用 install_packages() 函数。例如,假设你希望使用 mvtnorm 包,该包计算多元正态累积分布函数和其他量。

首先,选择一个你希望安装包(以及未来可能的其他包)的目录,比如 /a/b/c。然后在 R 提示符下,输入以下内容:

install.packages("mvtnorm","/a/b/c/")

这将导致 R 自动前往 CRAN,下载包,编译它,并将其加载到新的目录中:/a/b/c/mvtnorm

你需要告诉 R 在哪里可以找到已安装的包,这可以通过 .libPaths() 函数来完成:

.libPaths("/a/b/c/")

这会将新目录添加到 R 已经使用的目录中。如果你经常使用该目录,你可能希望将此调用添加到你的家目录中的 .Rprofile 启动文件中。

356

附录 B

www.it-ebooks.info

不带参数的 .libPaths() 调用将显示 R 在请求加载包时当前会查找的所有位置。

B.3.2 手动安装包

有时候,你需要手动安装以进行必要的修改,使特定的 R 包能在你的系统上运行。以下示例展示了我在一个特定情况下是如何做到这一点的,这将成为一个关于处理常规方法不起作用的情形的案例研究。

注意

需要手动安装包的情况通常与操作系统相关,并且需要比本书中通常假设的更多计算机专业知识。

对于非常具体的情况的帮助,r-help 邮件列表非常有价值。要访问它,请访问 R 主页 ( http://www.r-project.org/ ),点击 FAQs 链接,然后点击 R

FAQ 链接,并滚动到第 2.9 节,“存在哪些 R 的邮件列表?”

我想在我们的系的教学机器上目录 /home/matloff/R 中安装 Rmpi 包。我首先尝试使用 install.packages(),但发现自动过程无法在我们的机器上找到 MPI 库。问题在于 R 正在寻找这些文件的位置

/usr/local/lam,而我知道它们在 /usr/local/LAM 中。由于这些是公共机器,不是我的,我没有权限更改名称。因此,我下载了打包形式的 Rmpi 文件 Rmpi_0.5-3

.tar.gz. 我在我的目录 ~/tmp 中解压了该文件,生成了一个名为 ~/tmp/Rmpi 的目录。

如果我没有遇到这个问题,在这个时候,我只需在 ~/tmp 目录下的终端窗口中键入以下命令即可:R CMD INSTALL -l /home/matloff/R Rmpi

该命令将安装 ~/tmp/Rmpi 中包含的包,并将其放置在 /home/matloff/R 中。这将是一个调用 install.packages() 的替代方案。

但正如所注,我必须处理一个问题。在 ~/tmp/Rmpi 目录中,有一个 configure 文件,因此我在 Linux 命令行上运行了这个命令:

configure --help

它告诉我我可以指定配置时 MPI 文件的位置,如下所示:

configure --with-mpi=/usr/local/LAM

这适用于你直接运行 configure 的情况,但我通过 R 运行的:

R CMD INSTALL -l /home/matloff/R Rmpi --configure-args=--with-mpi=/usr/local/LAM

安装和使用包

357

www.it-ebooks.info

嗯,这似乎是可行的,从 R 安装了包这个意义上来说,但 R 也指出,它在我们的机器上的线程库存在问题。确实如此,当我尝试加载 Rmpi 时,我得到了一个运行时错误,说某个线程函数不存在。

我知道我们的线程库是好的,所以我进入了配置文件并注释了两行:

if test $ac_cv_lib_pthread_main = yes; then

MPI_LIBS="$MPI_LIBS -lpthread"

fi

换句话说,我强迫它使用我知道(或相当肯定)会工作的方法。然后我重新运行 R CMD INSTALL,并且包加载没有任何问题。

B.4 列出包中的函数

您可以通过调用带有帮助参数的 library() 来获取包中的函数列表。例如,要获取 mvtnorm 包的帮助,键入以下之一:

library(help=mvtnorm)

help(package=mvtnorm)

358

附录 B

www.it-ebooks.info

索引

特殊字符

使用 abline() 函数绘制线条,263–264

列表元素,88–90

:(冒号运算符),32–33

矩阵行和列,73–78

== 操作符,54–55

points() 函数指向图形,246–250

运算符,40

实现,269–270

.libpaths() 函数,356–357

使用 text() 函数将文本添加到图形中,

.Rdata 文件,20

270–271

.Rhistory 文件,20

addmargins() 函数,131

.Rprofile 文件,19

邻接矩阵,333

<<-(超级赋值运算符),9

aggregate() 函数,136

简化代码,174

all() 函数,35–39

使用 print() 函数写入非局部变量,161–162

类似操作,调整大小

  • 运算符,31

矩阵,74

"%mut%"() 函数,218

匿名函数,99,187–188

反调试,287

A

any() 函数,35–39

应用特定函数,165

贻贝数据集

apply() 函数

重新编码,51–54

将函数应用于矩阵行

使用 lapply() 函数,99

以及列,70–72

abline() 图形函数,150

矩阵类似操作,107

abs() 数学函数,189

获取变量的边际

访问

值,131

数据框,102–104

参数。另见特定参数

通过远程机器上的文件

按名称

URL,243

实际,9

互联网,246–250

默认,9–10

实现并行 R 检查

默认值,146–147

请,248–250

正式,9

套接字,247–248

算术运算,30–31,145–146

TCP/IP,247

array() 函数,134

键盘和显示器,232–235

数组

使用 print() 函数,234–235

高维数组,82–83

使用 readline() 函数,234

作为向量,28

使用 scan() 函数,232–234

as.matrix() 函数,81

列表组件和值,93–95

aspell() 函数,211

实际参数,9

assign() 函数

添加

变量,109

使用 legend() 函数为图形添加图例

使用 print() 函数,163

函数,270

www.it-ebooks.info

原子祈使,343

字符串,251–259

原子向量,85–86

定义,11

attr() 函数,212

正则表达式,254–257

构建文件名示例,

B

256–257

测试文件名是否具有给定后缀

批量模式,1

示例,255–256

帮助功能,24

字符串操作函数,

在其中运行 R,3

251–254

伯努利序列,204

gregexpr(),254

biglm 包,321

grep(),252

bigmemory 包,321

nchar(),252

二进制文件,237

paste(),252–253

二叉搜索树,177–182

regexpr(),253–254

body() 函数,149,151

sprintf(),253

布尔运算符,145–146

strsplit(),253

大括号,144

substr(),253

括号,87–88

在 edtdbg 调试中使用字符串实用工具

Bravington, Mark,300

生成工具,257–259

设置断点,289–290

二叉搜索树的孩子节点,177

直接调用 browser() 函数,

中国方言,学习辅助工具,

289–290

115–120

使用 setbreakpoint() 函数,290

卡方分布,193–194

breaks 组件,hist() 函数,14

chol() 线性代数函数,197

break 语句,141

choose() 集合操作,202

browser 命令,289

分块内存,320–321

browser() 函数

class() 函数,212

设置断点,289–290

清洁的代码,172

单步执行代码,288

客户/服务器模型,247

by() 函数,126–127

闭包,151,174–175

byrow 参数,matrix() 函数,

cloud() 函数,282–283

61,236

cluster,snow 包,335

字节码编译,320

clusterApply() 函数,snow 包,

72, 337, 339–340

C

代码文件,3

代码安全性,41

c %in% y 集合操作,202

col() 函数,69–70

缓存,346

冒号运算符 (😃, 32–33

微积分,192–193

彩色图像,63

分类型变量,121

矩阵存储的列主序,

cbind() 函数,12,74–75,106–107

59, 61

c browser 命令,289

组合模拟,205–206

cdf (累积分布

combn() 函数,203

函数),193

comdat$countabsamecomm 组件,206

ceiling() 数学函数,190

comdat$numabchosen 组件,206

单元计数,转换为

comdat$whosleft 组件,206

比例,130

逗号分隔值 (CSV) 文件,103

cex 选项,改变图形字符

注释,3

与其大小,272–273

complete.cases() 函数,105–106

c() 函数,56–57

综合 R 存档网络

Chambers, John,226

(CRAN),24,193,353

360

索引

www.it-ebooks.info

计算平均值,保存到变量中,5

应用逻辑回归

向量连接,4

模型示例,113–115

连接,237–238

在 lapply() 和 sapply() 上使用

构造函数,217

数据框,112–113

列联表,128,229

矩阵类似操作,104–109

控制语句,139–144

apply() 函数,107

if-else 函数,143–144

提取子数据框,

遍历非向量集合,143

104–105

循环,140–142

NA 值,105–106

改变复制策略,314–315

rbind() 和 cbind() 函数,

cos() 数学函数,190

106–107

counter() 函数,175

工资研究示例,108–109

计数组件

合并,109–112

hist() 函数,14

员工数据库示例,

mapsound() 函数,116

111–112

协方差矩阵,生成,69–70

从文件中读取,236

CRAN (综合 R 存档网络

考试成绩的回归分析

工作),24,193,353

示例,103–104

临界区,OpenMP,344

数据结构,10–16

crossprod() 函数,196

字符串,11

交叉验证,219,222

类,15–16

C 样式循环,140

数据框,14–15

CSV (逗号分隔值) 文件,103

列表,12–14

ct.dat 文件,128

矩阵,11–12

cumprod() 数学函数,190,191

向量,10

cumsum() 数学函数,39,190–191

debug() 函数,288

累积分布函数

debugger() 函数,执行检查

(cdf),193

与崩溃后的,291–292

累积和与乘积,191

调试,285–304

curve() 函数,277–278

确保调试一致性

定制图形,272–280

模拟代码,302

使用 polygon() 函数添加多边形

设施,288–300

用,275–276

browser 命令,289

使用 cex 改变字符大小

debug() 和 browser()

选项,272–273

函数,288

使用 xlim 改变坐标轴范围

调试会话,292–300

和 ylim 选项,273–275

设置断点,289–290

绘制显式函数,276–277

traceback() 和 debugger()

放大曲线的部分

函数,291–292

示例,277–280

trace() 函数,291

使用 lowess() 和平滑点

全局变量和,173

loess() 函数,276

并行 R,351

cut() 函数,136–137

原则,285–287

防虫,287

D

确认,285–286

模块化,自顶向下的方式,286

data 参数,array() 函数,134

从小开始,286

数据框,14–15,101–102

在 R 上运行 GDB,303–304

访问,102–104

语法和运行时错误,303

应用函数,112–120

工具,287–288,300–302

学习汉语方言的辅助工具

debug 软件包,300–301

示例,115–120

声明,28–29

索引

361

www.it-ebooks.info

默认参数,9–10

员工数据库示例,111–112

删除

封装,207

列表元素,88–90

文件结束 (EOF),238

矩阵行和列,73–78

envir 参数

二叉搜索树中的一个节点,181

get() 函数,159

密度估计,同一图形,264–266

ls() 函数,155

DES (离散事件模拟),

环境和范围,151–159

编写,164–171

函数(几乎)没有副作用

det() 线性代数函数,197

影响,156–157

dev.off() 函数,3

显示调用内容的功能

df 参数,mapsound() 函数,116

frame 示例,157–159

dgbsendeditcmd() 函数,257–258

ls() 函数,155–156

diag() 线性代数函数,197–198

范围层次,152–155

diff() 函数,50–51

最高级环境,152

dim 参数,array() 函数,134

EOF (文件结束),238

dim 属性,矩阵类,79

ess-tracebug 软件包,300

dimcode 参数,apply() 函数,70

事件列表,DES,164

维度降低,避免,80–81

事件导向范式,164

dim() 函数,79

example() 函数,21–22

dimnames 参数,array() 函数,134

exists() 函数,230

dimnames() 函数,131

expandut() 函数,218

dir() 函数,245

显式函数,绘图,276–277

离散事件模拟 (DES),

exp() 数学函数,189

编写,164–171

提取

离散值时间序列,预测,

子数据框,104–105

37–39

子表,131–134

do.call() 函数,133

dosim() 函数,165

F

双括号,87–88

drop 参数,68,81

factorial() 数学函数,190

dtdbg 调试工具,使用字符串利用-

因子,121

中的绑定,257–259

函数,123,136

双核机器,341

aggregate(),136

duplicate() 函数,315

by(),126–127

动态任务分配,348–350

cut(),136–137

split(),124–126

E

tapply(),123–124

层级和,121–122

每个 参数,rep() 函数,34

方言,115

edit() 函数,150,186–187

fargs 参数,apply() 函数,70

edtdbg 包,300–302

f 参数,apply() 函数,70

eigen() 函数,197,201

在 Fedora 上安装 R,353–354

特征值,201

file.exists() 函数,245

特征向量,201

file.info() 函数,245,246

元素

文件类型标准,Google,24

列表,添加和删除,88–90

filter() 函数,328

向量

过滤,45–48

添加和删除,26

定义,25

命名,56

生成过滤索引,45–47

显式并行应用

矩阵,66–69

定义,347–348

使用 subset() 函数,47

将一般问题转化为,350

使用 which() 选择函数,47–48

362

索引

www.it-ebooks.info

findud() 函数,50

G

findwords() 函数,90–91

一等对象,149

GCC,325

floor() 数学函数,190

GDB(GNU 调试器),288,327

for 循环,306–313

通用编辑器,186

在 Monte 中实现更好的速度

生成

卡罗尔模拟示例,

协方差矩阵,69–70

308–311

过滤索引,45–47

生成幂矩阵示例,

力矩阵,312–313

312–313

通用函数,xxi

向量化以加速,306–308

类,15

形参

在 S4 类中实现,225–226

mapsound() 函数,116

getAnywhere() 函数,211

oddcount() 函数,9

get() 函数,159

formals() 函数,149,151

遍历非向量集合,142

构建文件名,256–257

getnextevnt() 函数,165

四元素向量,添加

getwd() 函数,245

元素到,26

全局变量,9,171–174

fromcol 参数,mapsound()

GNU 调试器(GDB),288,327

函数,116

GNU S 语言,xix

函数式编程,xxi–xxii,

GPU 编程,171,345

314–316

GPU(图形处理单元),345

避免内存复制示例,

gputools 包,345–346

315–316

粒度,348

基于更改复制的问题,314–315

图形用户界面(GUIs),xx

向量赋值问题,314

图形处理单元(GPUs),345

函数,7–10. 另见 数学函数;

图表,261–283

字符串操作函数

自定义,272–280

匿名,187–188

使用 legend() 添加图例

应用到数据框,112–120

函数,270

学习汉语方言的辅助工具

使用 abline() 添加线条

示例,115–120

函数,263–264

应用逻辑回归

使用 points() 添加点

模型示例,113–115

函数,269–270

使用 lapply() 和 sapply()

使用 polygon() 添加多边形

函数,112–113

函数,275–276

应用到列表,95–99

使用 text() 函数添加文本,

abalone 数据示例,99

270–271

lapply() 和 sapply() 函数,95

使用 cex 改变字符大小

文本一致性示例,95–98

选项,272–273

应用到矩阵行和列,

使用 xlim 改变坐标轴范围

70–73

和 ylim 选项,273–275

apply() 函数,70–72

绘制显式函数的图表,

寻找异常值示例,72–73

276–277

默认参数,9–10

放大曲线的部分

在包中列出,358

示例,277–280

作为对象,149–151

使用 lowess() 平滑点

替换,182–186

和 loess() 函数,276

对于统计分布,193–194

使用 locator() 函数定位位置

超越,40

函数,271–272

变量作用域,9

plot() 函数,262

向量,35–39,311

索引

363

www.it-ebooks.info

图表(继续

矩阵,62–63

图表

向量,31–32

恢复,272

索引,过滤,45–47

三维,282–283

继承

多项式回归示例,

定义,207

266–269

S3 类,214

保存到文件,280–281

initglbls() 函数,165

在保持新图表的同时开始

输入/输出 (I/O)。 I/O

旧的,264

安装包。

同一图上的两个密度估计

安装 R,353–354

示例,264–266

从下载基本包,

灰度图像,63

CRAN,353

gregexpr() 函数,254

来自 Linux 软件包管理器,

grep() 函数,109,252

353–354

图形用户界面 (GUIs),xx

从源代码,354

install_packages() 函数,356

H

集成开发环境

(IDEs),xx,186

硬盘,从硬盘加载包,356

强度,像素,63–64

帮助功能,20–24

交互模式,2–3

其他主题,23–24

将 R 与其他语言接口,323–332

批处理模式,24

从 Python 使用 R,330–332

example() 函数,21–22

将 C/C++ 函数编写为可调用的

help() 函数,20–21

从 R,323–330

help.search() 函数,22–23

编译和运行代码,325

在线,24

调试 R/C 代码,326–327

help() 函数,20–21

从子对角线中提取

help.search() 函数,22–23

方阵示例,324–325

高维数组,82–83

预测离散值时间

hist() 函数,3,13–14

序列示例,327–330

主机,345

内部数据集,5

黄,明宇,324

内部存储,矩阵,59,61

访问 Internet,246–250

I

实现并行 R 示例,

248–250

identical() 函数,55

套接字,247–248

IDEs (集成开发环境

TCP/IP,247

语句),xx,186

Internet 协议 (IP) 地址,247

ifelse() 函数,48–49

intersect() 集合操作,202

评估两个统计关系的统计关系,

intextract() 函数,243

变量示例,49–51

I/O (输入/输出),231–250

控制语句,143–144

访问 Internet,246–250

重编码鲍鱼数据集示例,

实现并行 R 示例,

51–54

248–250

嵌套 if 语句,141–142

R 中的套接字,247–248

图像处理,63–66

TCP/IP,247

图像组件,mapsound()

访问键盘和监视器,

函数,116

232–235

不可变对象,314

使用 print() 函数,234–235

索引

使用 readline() 函数,234

列表,87–88

使用 scan() 函数,232–234

364

索引

www.it-ebooks.info

阅读文件,235

lines() 函数,264

访问远程文件

Linux 软件包管理器,安装 R

通过 URL 访问机器,243

从,353–354

连接,237–238

列表,12–14,85–100

从中读取数据框或矩阵

访问组件和值

文件,236

93–95

读取 PUMS 人口普查文件

应用函数,95–99

示例,239–243

鲍鱼数据示例,99

读取文本文件,237

lapply() 和 sapply() 函数,95

写入文件

文本一致性示例,95–98

获取文件和目录

一般操作,87–93

信息,245

添加和删除列表元素

合并多个文件的文件内容

88–90

示例,245–246

获取列表大小,90

写入文件,243–245

列表索引,87–88

IP(互联网协议)地址,247

文本一致性示例,90–93

递归列表,99–100

J

lm() 函数,15,208–210

负载均衡,349–350

连接操作,109

locator() 函数

确定相关行和列

K

umns,64–65

使用 pinpointing locations with,271–272

通过键盘访问,232–235

loess() 函数,276

打印到屏幕,234–235

log10() 数学函数,189

使用 readline() 函数,234

逻辑运算,30–31

使用 scan() 函数,232–234

应用逻辑回归模型

KMC(k-均值聚类),338–340

113–115

log() 数学函数,189

L

长期状态分布,马尔可夫

模型,200

向量滞后操作,50–51

循环,控制语句,140–142

lapply() 函数

lowess() 函数,276

将函数应用于列表,95

ls() 函数

列表,50

环境和作用域,155–156

遍历非向量集合,142

使用 exists() 函数列出对象,226–227

在数据框中使用,112–113

延迟,346

惰性评估原则,52,147

M

留一法,219,222

放大曲线的部分,277–280

legend() 函数,270

makerow() 函数,241–242

length() 函数

管理员,snow 包,335

获取向量长度,27

管理对象,226–230

向量索引,32

确定对象结构

层级,因子和,121–122

228–230

.libPaths() 函数,356–357

exists() 函数,230

库函数,165

使用 ls() 函数列出对象

线性代数操作,在向量上

226–227

和矩阵,61,196–201

使用 rm() 删除特定对象

寻找平稳分布

函数,227–228

马尔可夫链示例,199–201

使用 save() 函数保存对象集合

向量叉积示例,198–199

save() 函数,228

索引

365

www.it-ebooks.info

mapsound() 函数,115–116

内存

边际值,变量,131

分块,320–321

m 参数,apply() 函数,70

函数式编程,314–316

马尔可夫链,199–201

避免内存拷贝示例

MASS 包,23,356

315–316

数学函数,189–193

处理拷贝更改问题,314–315

计算概率示例

向量赋值问题,314

190–191

使用 R 包进行内存管理

微积分,192–193

管理,321

累加和乘积,191

merge() 函数,109–110

极小值和极大值,191–192

归并排序方法,数值

矩阵,11–12,59–83

排序,347

添加和删除行和列

合并数据框,109–112

umns,73–78

员工数据库示例,

在其中寻找最近的顶点对

111–112

图形示例,75–78

元字符,254

调整矩阵大小,73–75

methods() 函数,210

将函数应用于行和列

微数据,239

列,70–73

最小值函数,191–192

apply() 函数,70–72

min() 数学函数,190,191

寻找异常值示例,72–73

M/M/1 队列,165,168

避免意外的维度

模式

减少,80–81

批量,1,3,24

在其上执行线性代数操作,196–201

定义,26

命名行和列,81–82

交互式,2–3

操作,61–70

模运算符,44

过滤,66–69

监视器,访问,232–235

生成协方差矩阵

使用 print() 函数,234–235

示例,69–70

使用 readline() 函数,234

图像处理示例,

使用 scan() 函数,232–234

63–66

Monte Carlo 模拟,实现 bet-

线性代数操作,61

在其中的速度,308–311

矩阵索引,62–63

多核机器,340–341

从文件读取,236

mutlinks() 函数,336

向量/矩阵的区别,78–79

互链,333–334,341–342

作为向量,28

mvrnorm() 函数,MASS 包,23,356

类似矩阵/数组的操作,130–131

矩阵类,79

N

matrix() 函数,60

矩阵逆更新方法,222

命名参数,146–147

类似矩阵的操作,104–109

names() 函数,56

apply() 函数,107

命名

提取子数据框,104–105

矩阵的行和列,81–82

NA 值,105–106

向量元素,56

rbind() 和 cbind() 函数,

NA 值

106–107

类似矩阵的操作,105–106

工资研究示例,108–109

向量,43

矩阵乘法运算符,12

n 浏览器命令,289

最大值函数,191–192

nchar() 函数,252

max() 数学函数,190,192

ncol() 函数,79

mean() 函数,38

366

索引

www.it-ebooks.info

负下标,32,63

矩阵,61–70

网络,定义,247

过滤,66–69

牛顿-拉夫森方法,192

生成协方差矩阵

下一个语句,141

示例,69–70

尼罗数据集,5

图像处理示例,

向图像中添加噪声,65–66

63–66

名义变量,121

索引,62–63

非局部变量

线性代数操作,61

使用 superassignment 写入

矩阵/数组类似,130–131

运算符,161–162

向量,30–34

使用 assign() 函数写入,163

算术和逻辑操作,

非向量集合,循环控制状态

30–31

在其上执行操作,143

冒号运算符 (😃, 32–33

不可见函数,211

使用 generateVectorSequences() 生成向量序列

nreps 值,205

seq() 函数,33–34

nrow() 函数,79

使用 repeatVectorConstants() 重复向量常数

NULL 值,44

rep() 函数,34

输入向量,输出矩阵,42–43

O

输入向量,输出向量,40–42

向量索引,31–32

面向对象编程。另见 OOP

运算符优先级,33

对象。另见管理对象

order() 函数,97,194–195

首类,149

异常值,49

不可变,314

oddcount() 函数,7,140

P

omp barrier 指令,OpenMP,344

omp critical 指令,OpenMP,344

包,355–358

omp single 指令,OpenMP,344–345

安装

OOP(面向对象编程),

自动,356–357

xxi,207–230

手动,357–358

管理对象。 管理

在其中列出函数,358

对象

从硬盘加载,356

S3 类。 S3 类

并行 R,333–351

S4 类,222–226

调试,351

实现泛型函数

显式并行应用程序,

在,225–226

347–348

与 S3 类,226

将一般问题转化为,350

写作,223–225

实现,248–250

OpenMP,344–345

互链,333–334

代码分析,343

诉诸 C,340–345

omp barrier 禁言,344

GPU 编程,345

omp critical 禁言,344

多核机器,340–341

omp single 禁言,344–345

互链,341–342

操作

OpenMP 代码分析,343

列表,87–93

OpenMP 禁言,344–345

添加和删除列表元素,

运行 OpenMP 代码,342

88–90

snow 包,334–340

获取列表大小,90

分析雪代码,336–337

列表索引,87–88

k-means 聚类(KMC),338–340

文本一致性示例,90–93

运行 snow 代码,335–336

加速,337–338

索引

367

www.it-ebooks.info

snow 包(继续

多项式回归,219–222,266–269

资源开销来源,346–347

端口,247

计算机网络系统,

生成幂矩阵,312–313

346–347

禁言,OpenMP,343–345

共享内存机器,346

preda() 函数,38

静态与动态任务分配,

确认原则,调试,

348–350

285–286

parent.frame() 函数,156

print() 函数,18,234–235

paste() 函数,252–253,257,269

print.ut() 函数,218

PDF 设备,保存显示

prntrslts() 函数,165

图,281

概率,计算,190–191

pdf() 函数,3

概率质量函数 (pmf),193

Pearson 积矩

procpairs() 函数,343

相关性,49

prod() 数学函数,190

性能提升,305–321

编程结构。 R 程序-

字节码编译,320

构建结构

块处理,320–321

公共使用微观数据样本 (PUMS)

函数式编程,314–316

人口普查文件,读取,239

避免内存复制示例,

Python,从 Python 使用 R,330–332

315–316

复制更改问题,314–315

Q

向量赋值问题,314

for 循环,306–313

Q 浏览器命令,289

在蒙特

qr() 线性代数函数,197

卡罗尔模拟示例,

快速排序实现,176–177

308–311

生成幂矩阵考试,

R

示例,312–313

向量化以加速,306–308

竞态条件,343

使用 R 包进行内存

随机变量生成器,204–205

rank()

管理,321

函数,195–196

rbind()

使用 Rprof() 函数查找慢

函数,12,106–107

代码中的点,316–319

排序事件,171

编写快速 R 代码,306

调整矩阵大小,74–75

rbinom()

Perron-Frobenius 定理,201

函数,204

persp() 函数,22,282

R 控制台,2

像素强度,63–64

.Rdata 文件,20

plot()

Rdsm

函数,xxi,16,262

包,实现

图表

并行 R,249

reactevnt()

恢复,272

函数,165

readBin()

三维,282–283

函数,248

plyr

read.csv()

软件包,136

函数,108

pmax() 数学函数,190,192

读取文件,235

概率质量函数 (pmf),193

访问远程机器上的文件

pmin() 数学函数,190,191

通过 URL,243

指针,159–161

连接,237–238

points() 函数,269–270

从中读取数据框或矩阵,

polygon() 函数,275–276

文件,236

多态

读取 PUMS 人口普查文件示例,

定义,xxi,207

239–243

泛型函数,208

读取文本文件,237

368

索引

www.it-ebooks.info

readline() 函数,234

R 编程结构,139

readLines() 函数,248

匿名函数,187–188

重新分配矩阵,73–74

算术和布尔运算符

递归,176–182

和值,145–146

二叉搜索树示例,177–182

控制语句,139–144

快速排序实现,176–177

if-else 函数,143–144

递归参数,连接

遍历非向量集合,143

函数,100

循环,140–142

递归向量,86

参数的默认值,146–147

回收

环境和作用域问题,

定义,25

151–159

向量,29–30

显示调用内容的函数

参考类,160

框架示例,157–159

regexpr() 函数,253–254

ls() 函数,155–156

考试成绩的回归分析,

作用域层次结构,152–155

16–19,103–104

副作用,156–157

正则表达式,字符字符串

顶级环境,152

操作,254–257

函数作为对象,149–151

访问远程机器上的文件

指针,缺乏,159–161

在,243

递归,176–182

repeat 循环,241–242

二叉搜索树示例,

repeat 语句,141

177–182

rep() 函数,重复向量连接

快速排序实现,

与,34

176–177

替换函数,182–186

替换函数,182–186

定义,183–184

返回值,147–149

自我记账向量类

决定是否显式调用

示例,184–186

return() 函数,148

reshape 软件包,136

返回复杂对象,

调整矩阵大小,73–75

148–149

返回语句,8

编写函数代码的工具,

返回值,147–149

186–187

决定是否显式调用

edit() 函数,186–187

return() 函数,148

文本编辑器和 IDE,186

返回复杂对象,148–149

写入,161–175

REvolution Analytics,300

二进制运算,187

rexp() 函数,204

闭包,174–175

Rf_PrintValue(s) 函数,304

在离散事件模拟 (DES) 中

rgamma() 函数,204

R 示例,164–171

.Rhistory 文件,20

当何时使用全局变量,

rm() 函数,227–228

171–174

rnorm() 函数,3,204

使用 assign() 写入非局部变量

round() 函数,40–41,190

函数,163

路由器,247

使用 super- 写入非局部变量

row() 函数,69–70

赋值运算符,161–162

rownames() 函数,82

RPy 模块

R 软件包,用于内存

安装,330

管理,321

语法,330–332

rpois() 函数,204

runif() 函数,204

Rprof() 函数,316–319

运行

.Rprofile 文件,19

R 上的 GDB,303–304

OpenMP 代码,342

索引

369

www.it-ebooks.info

运行(继续

setdiff() 集合操作,202

R,1–2

setequal() 集合操作,202

批量模式,3

setMethod() 函数,225

第一节课,4–7

集合操作,202–203

交互模式,2–3

set.seed() 函数,302

snow 代码,335–336

设置断点,289–290

找到连续的 1 的运行,35–37

直接调用 browser() 函数,

运行时错误,303

289–290

使用 setbreakpoint() 函数,290

S

setwd() 函数,245

S 表达式指针 (SEXPs),304

S(编程语言),xix

共享内存系统,341,346–347

S3 类,208–222

共享内存/线程模型,

存储上三角矩阵的类

GPU,345

矩阵示例,214–219

Sherman-Morrison-Woodbury

找到泛型函数的实现

公式,222

方法,210–212

快捷方式

泛型函数,208

help() 函数,20

在 lm() 函数示例中的面向对象编程,

help.search() 函数,23

208–210

showframe() 函数,158

多项式回归的步骤

sim 全局变量,172–173

示例,219–222

简化代码,172

与 S4 类的比较,226

R 中的模拟编程,204–206

使用继承,214

内置随机变量生成器,

写作,212–213

204–205

S4 类,222–226

组合模拟,205–206

在上实现泛型函数,

获取相同的随机流在

225–226

重复的运行,205

与 S3 类的比较,226

单括号,87–88

写作,223–225

单服务器排队系统,168

工资研究,108–109

sink() 函数,258

Salzman,Pete,285

sin() 数学函数,190

sapply() 函数,42

插槽,S4 类,224

将函数应用于列表,95

snow 包,334–335

在数据框上使用,112–113

实现并行 R,248–249

save() 函数,保存集合,

k-means 聚类 (KMC),338–340

拥有对象的,228

snow 代码

将图形保存到文件,280–281

分析,336–337

标量,10

运行,335–336

布尔运算符,145

加速,337–338

向量,26

socketConnection() 函数,248

scan() 函数,142,232–234

套接字,247–248

散列/聚集范例,335–336

socketSelect() 函数,248

schedevnt() 函数,165,171

solve() 函数,197

范围层次,152–155. 另见 环境

排序,数值,194–196

环境 和 范围

sos 包,24

sepsoundtone() 函数,119

从源安装 R,354

seq() 函数,21,33–34

sourceval 参数,mapsound()

serialize() 函数,248

函数,116

setbreakpoint() 函数,290

斯皮尔曼等级相关,49

setClass() 函数,223

370

索引

www.it-ebooks.info

速度

summary() 函数,15,18

字节码编译,320

summaryRprof() 函数,319

在代码中找到慢点,316–319

对多个文件内容求和,245–246

for 循环,306–313

超赋值运算符 (<<-), 9

在蒙特卡洛模拟中实现更好的速度

简化代码,174

蒙特卡洛模拟示例,

使用它向非局部写入,161–162

308–311

sweep() 线性代数函数,197–198

生成幂矩阵

对称矩阵,77

示例,312–313

语法错误,303

向量化以加速,

306–308

T

编写快速 R 代码,306

Spinu, Vitalie,300

tabdom() 函数,134

split() 函数,124–126,336

表格,127–130

S-Plus (编程语言),xix

提取子表示例,

sprintf() 函数,253

131–134

sqrt() 函数,42,189

在其中查找最大的单元格,134

栈跟踪,289

函数,136–137

启动和关闭,19–20

aggregate(),136

静态任务分配,348–350

cut(),136–137

稳态分布,马尔可夫链,

矩阵/数组类似操作,

199–201

130–131

统计分布,函数,用于

标签,86

193–194

tapply() 函数

str() 函数,14

与 by() 函数比较,126–127

字符串操作函数,11,

因子,123–124

251–254

与 split() 函数比较,124

gregexpr(),254

tbl 参数,subtable() 函数,132

grep(),252

tblarray 数组,133

nchar(),252

TCP/IP,247

paste(),252–253

终止条件,177

regexpr(),253–254

测试向量相等,54–55

sprintf(),253

文本,使用 text() 函数添加到图形中

strsplit(),253

270–271

substr(),253

文本一致性,90–93,95–98

stringsAsFactors 参数,data.frame()

文本编辑器,186

函数,102

文本文件,读取,237

字符串实用工具,在 edtdbg 调试工具中,

text() 函数,向图形中添加文本

257–259

with,270–271

strsplit() 函数,253

t() 函数,71,119,197

子行列式,199

线程代码,171

子矩阵,赋值,62–63

线程,341

subnames 参数,subtable()

三维表格,129–130

函数,132

Tierney, Luke,334

下标操作,183

tocol 参数,mapsound()

subset() 函数,47,105

函数,116

向量子集,4–5

工具

substr() 函数,253

用于编写函数代码,

subtable() 函数,132

186–187

后缀,测试给定文件名,255–256

edit() 函数,186–187

sum() 函数,190,337

文本编辑器和 IDE,186

调试,287–288,300–302

索引

371

www.it-ebooks.info

顶级环境,152

向量,10,25–57

traceback() 函数,291–292

all() 和 any() 函数,35–39

trace() 函数,291

寻找连续的 1 的序列

tracemem() 函数,314–315

示例,35–37

训练集,37

预测离散值时间

超越函数,40

系列示例,37–39

转移概率,200

c() 函数,56–57

树状数据结构,177

常用操作,30–34

算术和逻辑运算,

U

30–31

冒号运算符(😃,32–33

在 Ubuntu 上安装 R,353–354

使用生成向量序列,

unclass() 函数,229

seq() 函数,33–34

union() 集合操作,202

重复向量常数,

unlist() 函数,93

rep() 函数,34

unname() 函数,94

向量索引,31–32

unserialize() 函数,248

计算两个的内积,196

upn 参数,showframe() 函数,158

声明,28–29

上三角矩阵,用于存储

定义,4

214–219

元素

URLs,通过访问远程文件

添加和删除,26

通过访问,243

命名,56

u 变量,162

过滤,45–48

为其生成索引,45–47

V

使用 subset() 函数,47

使用 which() 函数,47–48

ifelse() 函数,48–54

赋值到子矩阵,62–63

评估两个统计关系

布尔,145–146

变量示例,49–51

列表,访问,93–95

重编码鲍鱼数据集

NA,43,105–106

example, 51–54

NULL,44

线性代数运算,

返回,147–149

196–201

vanilla 选项,启动/关闭,20

矩阵和数组作为,28

变量

NA 值,43

评估两个统计关系,

NULL 值,44

49–51

获取长度,27

分类,121

回收,29–30

全局,9,171–174

标量,26

名义,121

测试向量相等,54–55

变量作用域,9

向量化操作,39–43

向量赋值问题,314

向量输入,矩阵输出,42–43

向量叉积,198–199

向量输入,向量输出,40–42

向量过滤,307

顶点,图,查找,75–78

向量过滤能力,176

向量函数,311

W

向量化

定义,25

Web,从 Web 下载包,

为了加速,306–308

356–358

向量化操作,40

自动安装,356–357

向量/矩阵区别,78–79

手动安装,357–358

372

INDEX

www.it-ebooks.info

在浏览器命令中,289

which.max() 函数,73,190

which.min() 函数,190

which() 函数,47–48

空白字符,233

Wickham,Hadley,136

wireframe() 函数,282–283

wmins 矩阵,77

workers,snow 包,335

工作目录,19–20

writeBin() 函数,248

writeLines() 函数,248

write.table() 函数,244

编写,161

二进制运算,187

从 R 调用的 C/C++ 函数,

323–324

编译和运行代码,325

调试 R/C 代码,326–327

从中提取子对角线

方阵示例,324–325

离散值时间预测

系列示例,327–330

闭包,174–175

R 中的离散事件模拟

example,164–171

获取文件和目录

信息,245

到非局部变量

使用 assign() 函数,163

使用超级赋值运算符,

161–162

S3 类,212–213

S4 类,223–225

求多个文件内容之和

example, 245–246

何时使用全局变量,171–174

X

xlim 选项,273–275

x 变量,162

Y

ylim 选项,273–275

Z

z 变量,162

INDEX

373

www.it-ebooks.info

电子前沿基金会 (EFF) 是领先的捍卫数字世界公民自由的组织。我们捍卫互联网上的言论自由,反对非法监控,促进创新者开发新的数字技术,并努力确保我们享有的权利和自由得到增强——

而不是被侵蚀——随着我们使用技术的增长。

PRIVACY EFF 已起诉电信巨头 AT&T,因其允许 NSA 无限制地访问数百万客户的私人通信。eff.org/nsa

FREE SPEECH EFF 的 Coders’ Rights Project 正在捍卫程序员和安全研究人员在无法律挑战恐惧的情况下发布其发现的权利。

eff.org/freespeech

创新 EFF 的专利破坏项目挑战过于宽泛的专利,这些专利威胁到技术创新。eff.org/patent

FAIR USE EFF 正在抵制那些会剥夺你以任何方式接收和使用空中电视广播权利的禁止性标准。eff.org/IP/fairuse 透明度 EFF 开发了瑞士网络测试工具,为个人提供测试隐蔽流量过滤的工具。eff.org/transparency

国际 EFF 正在努力确保国际条约不会限制我们的言论自由、隐私或数字消费者权利。eff.org/global

EFF 是一个会员支持的组织。立即加入!www.eff.org/support

www.it-ebooks.info

Image 46

Image 47

Image 48

Image 49

Image 50

Image 51

更新

访问 http://www.nostarch.com/artofr.htm 获取更新、勘误和其他信息。

更多无废话的书籍来自

诺斯塔奇出版社

脚本编程 101

学习 Haskell

优雅的 JavaScript

以示例驱动的构建指南

为了伟大的善

现代编程导论

使用 Bing、Yahoo! 和 Google Maps 的交互式地图

入门指南

by 马里恩·哈弗贝克

和 Google Maps

2011 年 1 月,224 页,$29.95

by 米兰·利波瓦亚

ISBN 978-1-59327-282-1

by 亚当·杜万德

2011 年 4 月,400 页,$44.95

2010 年 8 月,376 页,$34.95

ISBN 978-1-59327-283-8

ISBN 978-1-59327-271-5

TCP/IP 指南

THE MANGA GUIDE™ TO

THE LINUX 编程

综合图解互联网

统计学

界面

协议参考

by 斋藤隆治 TREND-PRO

Linux 和 UNIX® 系统

by 查尔斯·M·科齐奥罗克

株式会社

编程手册

2005 年 10 月,1616 页,$99.95,精装

2008 年 11 月,232 页,$19.95

by 迈克尔·凯里斯科

ISBN 978-1-59327-047-6

ISBN 978-1-59327-189-3

2010 年 10 月,1552 页,$99.95,精装

ISBN 978-1-59327-220-3

电话:

电子邮件:

800.420.7240 或

SALES@NOSTARCH.COM

415.863.9900

WEB:

周一至周五,

上午 9 点至下午 5 点(太平洋标准时间)

WWW.NOSTARCH.COM

www.it-ebooks.info

《R 编程艺术》使用的字体是 New Baskerville、Futura、The Sans Mono Condensed 和 Dogma。本书使用 LATEX 2 ε 排版。

包含诺斯塔奇出版社的 Boris Veytsman (2008/06/06 v1.3 为 No Starch Press) 装订的书籍。

本书由密歇根州安阿伯的 Malloy Incorporated 印刷和装订。纸张是 Glatfelter Spring Forge 60# Smooth,由可持续林业倡议(SFI)认证。本书采用 RepKover 胶装,使其打开时可以平铺。

www.it-ebooks.info

www.it-ebooks.info

Image 52

管理你的数据

T

T H E

HE A

A R T O F R

R 是世界上最受欢迎的语言,用于开发

• 使用 C/C++ 和 Python 将 R 与界面连接,以增加

R

统计软件:考古学家用它来追踪

速度或功能

T O

PROGR A MMING

古代文明的传播,制药公司使用它

• 为文本分析、图像处理

以发现哪些药物是安全有效的,

的功能和数千种其他

和精算师使用它来评估财务风险并保持

A T O U R O F S T A T I S T I C A L S O F T W A R E D E S I G N

F R P

市场运行顺畅。

• 使用高级调试技术消除烦人的错误

技术

R 编程的艺术 带您进行软件开发的导游之旅

,从基本类型

无论您是在设计飞机、预测天气

和数据结构到高级主题如闭包,

天气,或者您只需要驯服您的数据,R 编程的艺术

N O R M A N M A T L O F F

递归和匿名函数。无需统计

R 编程 是您利用其功能和函数

R

知识要求,以及您的编程技能

统计计算。

可以从爱好者到专业水平不等。

O

A B O U T T H E A U T H O R

在旅途中,您将了解函数和面向对象

G

Norman Matloff 是加州大学戴维斯分校的计算机科学

运行数学模拟、

(以及前统计学教授)在

R

或整理复杂数据以使其更简单、更有用

的研究兴趣包括

格式。您还将学习如何:

A

并行处理和统计回归,以及

• 创建艺术性的图表来可视化复杂的数据集

他编写了几个广泛使用的网络教程

M

的指南

为软件开发撰写文章。他曾在

纽约时报华盛顿邮报福布斯 M

• 使用并行 R 和

杂志,以及 洛杉矶时报,他是

向量化

I

《调试的艺术》(No Starch Press)的合著者。

NG

TH E FI N EST I N G E E K E NTE RTAI N M E NT™

www.nostarch.com

MAT

L

O

“我喜欢平坦。”

$39.95 ($41.95 CDN)

F

S C S

这本书使用 RepKover —一种耐用的装订,不会突然合上。

T O H

A

F

M E

T

LV

I P

ST U E

I T I

C E N

A R :

L S

SO /M

F A

T T

W H

A EM

RE ATICAL &

www.it-ebooks.info

文档大纲

  • 版权

  • 简要内容

  • 详细内容

  • 致谢

  • 介绍

    • 为什么使用 R 进行您的统计工作?

    • 这本书适合谁?

    • 我的背景

  • 1: 入门

    • 1.1 如何运行 R

    • 1.2 第一次 R 会话

    • 1.3 函数介绍

    • 1.4 一些重要 R 数据结构的预览

    • 1.5 扩展示例:考试成绩回归分析

    • 1.6 启动和关闭

    • 1.7 获取帮助

  • 2: 向量

    • 2.1 标量、向量、数组和矩阵

    • 2.2 声明

    • 2.3 回收

    • 2.4 常见向量操作

    • 2.5 使用 all()和 any()

    • 2.6 向量化操作

    • 2.7 NA 和 NULL 值

    • 2.8 过滤

    • 2.9 向量化 if-then-else:ifelse()函数

    • 2.10 测试向量等价性

    • 2.11 向量元素名称

    • 2.12 关于 c()的更多内容

  • 3: 矩阵和数组

    • 3.1 创建矩阵

    • 3.2 矩阵通用操作

    • 3.3 将函数应用于矩阵行和列

    • 3.4 添加和删除矩阵行和列

    • 3.5 关于向量/矩阵区别的更多内容

    • 3.6 避免意外的维度缩减

    • 3.7 命名矩阵行和列

    • 3.8 高维数组

  • 4: 列表

    • 4.1 创建列表

    • 4.2 列表通用操作

    • 4.3 访问列表组件和值

    • 4.4 将函数应用于列表

    • 4.5 递归列表

  • 5: 数据框

    • 5.1 创建数据框

    • 5.2 其他矩阵类操作

    • 5.3 合并数据框

    • 5.4 将函数应用于数据框

  • 6: 因子和表格

    • 6.1 因子和水平

    • 6.2 因子常用函数

    • 6.3 与表格一起工作

    • 6.4 其他因子和表格相关函数

  • 7: R 编程结构

    • 7.1 控制语句

    • 7.2 算术和布尔运算符及值

    • 7.3 参数的默认值

    • 7.4 返回值

    • 7.5 函数是对象

    • 7.6 环境和作用域问题

    • 7.7 R 中没有指针

    • 7.8 向上写入

    • 7.9 递归

    • 7.10 替换函数

    • 7.11 编写函数代码的工具

    • 7.12 编写自己的二进制操作

    • 7.13 匿名函数

  • 8: 在 R 中进行数学和模拟

    • 8.1 数学函数

    • 8.2 统计分布函数

    • 8.3 排序

    • 8.4 向量和矩阵的线性代数运算

    • 8.5 集合运算

    • 8.6 R 中的模拟编程

  • 9: 面向对象编程

    • 9.1 S3 类

    • 9.2 S4 类

    • 9.3 S3 与 S4 的比较

    • 9.4 管理你的对象

  • 10: 输入/输出

    • 10.1 访问键盘和监视器

    • 10.2 读取和写入文件

    • 10.3 访问互联网

  • 11: 字符串操作

    • 11.1 字符串操作函数概述

    • 11.2 正则表达式

    • 11.3 edtdbg 调试工具中的字符串实用工具的使用

  • 12: 图形

    • 12.1 创建图表

    • 12.2 自定义图表

    • 12.3 将图表保存到文件

    • 12.4 创建三维图表

  • 13: 调试

    • 13.1 调试的基本原则

    • 13.2 为什么使用调试工具?

    • 13.3 使用 R 调试设施

    • 13.4 向上走:更方便的调试工具

    • 13.5 确保调试模拟代码的一致性

    • 13.6 语法和运行时错误

    • 13.7 在 R 本身上运行 GDB

  • 14: 性能提升:速度和内存

    • 14.1 编写快速的 R 代码

    • 14.2 可怕的 for 循环

    • 14.3 函数式编程与内存问题

    • 14.4 使用 Rprof() 查找代码中的慢点

    • 14.5 字节码编译

    • 14.6 哎呀,数据放不进内存!

  • 15: 将 R 与其他语言接口

    • 15.1 将 C/C++ 函数编写为 R 可调用的函数

    • 15.2 从 Python 使用 R

  • 16: 并行 R

    • 16.1 互链问题

    • 16.2 介绍 snow 包

    • 16.3 转向 C 语言

    • 16.4 一般性能考虑

    • 16.5 调试并行 R 代码

  • 附录 A:安装 R

    • A.1 从 CRAN 下载 R

    • 从 Linux 软件包管理器安装

    • A.3 从源代码安装

  • 附录 B:安装和使用软件包

    • B.1 软件包基础

    • B.2 从您的硬盘加载软件包

    • 从 Web 下载软件包

    • B.4 列出软件包中的函数

  • 索引

  • 更新

posted @ 2025-11-27 09:18  绝不原创的飞龙  阅读(14)  评论(0)    收藏  举报