R-之书-全-
R 之书(全)
原文:
zh.annas-archive.org/md5/0cd1b8aebb70757d20f63f9697955afc译者:飞龙
前言

R 在各种研究和数据分析项目中发挥着关键作用,因为它使许多现代统计方法,无论是简单还是高级,都变得易于使用和快速获得。然而,确实,对于 R 的初学者来说,往往也是编程的新手。作为初学者,你不仅需要学习如何使用 R 来完成特定的数据分析目标,还需要学习如何像程序员一样思考。这部分是为什么 R 有时被认为“难学”的原因——但请放心,事实并非如此。
R 语言简史
R 语言在很大程度上基于 S 语言,S 语言最早是在 1960 年代和 1970 年代由新泽西州贝尔实验室的研究人员开发的(欲了解概况,参见例如 Becker 等,1988)。为了拥抱开源软件,R 的开发者——来自新西兰奥克兰大学的 Ross Ihaka 和 Robert Gentleman——在 1990 年代初期以 GNU 公共许可证发布了它。(该软件的名字源自于 Ross 和 Robert 共同的首字母。)从那时起,R 的受欢迎程度迅速增长,因为它在数据分析中的无与伦比的灵活性和强大的图形工具,而这一切都以零成本提供。也许 R 最具吸引力的特点是任何研究人员都可以通过包(或库)的形式贡献代码,这样全世界就能快速获取统计学和数据科学的最新进展(参见附录 A.2)。
今天,R 的主要源代码库由一个专门的团队——R 核心团队——维护,R 是一个协作的成果。你可以在www.r-project.org/上找到最杰出贡献者的名字;这些人值得感谢,因为他们不断的努力使得 R 始终保持活力,并处于统计计算的前沿!
团队相对频繁地发布 R 的更新版本。尽管随着时间的推移软件有了显著的变化,但相邻版本通常是相似的。在本书中,我使用了版本 3.0.1 至 3.2.2。你可以通过点击相关下载页面上的 NEWS 链接来查看最新版本的更新内容(参见附录 A)。
关于本书
R 语言手册旨在帮助你熟悉 R,作为第一门编程语言,并掌握其背后的统计思维。目标是为理解现代数据科学的计算特性打下一个既入门又全面的基础。
这本书的结构旨在内容上自然进展,首先聚焦于将 R 作为计算和编程工具使用,然后转向讨论如何利用 R 进行概率、统计以及数据探索和建模。你将逐步建立知识,在每一章的末尾,你会找到一个总结重要代码的部分,作为快速参考。
第一部分:语言基础
第一部分介绍了 R 编程中使用的基本语法和对象类型,对于初学者来说至关重要。第二章至第五章介绍了简单算术、赋值以及向量、矩阵、列表和数据框等重要对象类型的基础知识。在第六章中,我将讨论 R 如何表示缺失数据值并区分不同的对象类型。第七章为你提供了绘图的入门,使用了内置功能和贡献的功能(通过ggplot2包——参见 Wickham, 2009);本章为后续讨论的图形设计奠定了基础。在第八章中,我将介绍从外部文件读取数据的基础知识,这对于分析你自己收集的数据非常重要。
第二部分:编程
第二部分专注于帮助你熟悉常见的 R 编程机制。首先,我将在第九章中讨论函数及其在 R 中的工作方式。接下来,在第十章中,我将介绍循环和条件语句,它们用于控制代码的流程、重复执行和执行顺序,之后我将在第十一章中教你如何编写自己的可执行 R 函数。这两章的示例主要帮助你理解这些机制的行为,而不是呈现真实世界的分析。我还将在第十二章中介绍一些额外的话题,如错误处理和衡量函数执行时间。
第三部分:统计与概率
当你对 R 语言有了扎实的掌握后,你将把注意力转向统计思维,在第三部分中学习。在第十三章中,你将了解描述变量的重要术语;基础的汇总统计量,如均值、方差、分位数和相关性;以及这些统计量在 R 中的实现。再次回到绘图,第十四章讲解如何通过使用和自定义常见的统计图形(如直方图和箱线图)来直观地探索数据(包括内置功能和ggplot2功能)。第十五章概述了概率和随机变量的概念,然后在第十六章中,你将了解一些常见概率分布在 R 中的实现和统计解释。
第四部分:统计检验与建模
在第四部分,你将接触到统计假设检验和线性回归模型。第十七章介绍了抽样分布和置信区间。第十八章详细讲解了假设检验和p-值,并通过 R 语言展示了实现与解释;然后,第十九章讨论了常见的方差分析(ANOVA)过程。在第二十章和第二十一章中,你将深入探索线性回归建模,包括模型拟合、处理不同类型的预测变量、推断与预测、以及变量转换和交互效应。第四部分的最后,第二十二章讨论了选择合适线性模型的方法,并通过各种诊断工具评估模型的有效性。
线性回归仅代表一类参数模型,是学习统计回归的自然起点。同样,用于拟合、总结、预测和诊断此类线性模型的 R 语法和输出与其他回归模型的语法和输出几乎相同——因此,一旦你掌握了这些章节,你将能够相对轻松地处理更高级文本中涉及的更复杂模型的 R 实现。
第三部分和第四部分包含了你在大学一年级和二年级统计学课程中所期待的内容。我的目标是将数学细节保持在最小范围内,重点放在实现和解释上。如果你有兴趣深入了解底层理论,我会在必要时提供其他资源的参考。
第五部分:高级图形
最后一部分讲解了一些更高级的绘图技巧。第二十三章展示了如何自定义传统的 R 图形,从处理图形设备本身到控制绘图外观的细节。在第二十四章中,你将进一步学习流行的ggplot2包,研究更高级的功能,比如添加平滑散点图趋势和通过分面制作多个图形。最后两章集中讨论 R 中的高维绘图。第二十五章讲解了颜色处理和三维表面准备,随后介绍了等高线图、透视图和像素图,并通过多个示例进行演示。第二十六章则专注于交互式图形,并包括一些绘制多元参数方程的简单说明。
虽然不严格要求,但在攻克第五部分之前,了解第四部分中讨论的线性回归方法会有所帮助,因为最后部分的一些例子使用了拟合的线性模型。
针对学生
像许多人一样,我开始熟练掌握 R 编程和相关的统计方法实现时,是在我开始研究生学习(在新西兰帕默斯顿北的梅西大学)时。依托于我在澳大利亚本科时接触过的几行代码,虽然“被抛入深水区”既有好处也有坏处。虽然这种沉浸式学习加速了我的进步,但当遇到问题时不知道该怎么办,当然是令人沮丧的。
R 编程之书因此代表了我希望在开始学习 R 时所能拥有的语言入门书籍,并结合了统计学这一学科的第一年基础知识,这些内容都已在 R 中实现。通过这本书,你将能够为使用 R 打下坚实的基础,既可以作为编程语言,也可以作为统计分析的工具。
本书的写作方式是从头到尾阅读,就像一篇故事(尽管没有情节曲折!)。书中的思想是逐步构建的,因此你可以选择从头开始,或是从你目前的知识水平开始。考虑到这一点,我给 R 语言的学生们提供以下建议:
• 尽量不要害怕 R。它会完全按照你所说的去做——不会多做,也不会少做。当事情没有按预期进行或出现错误时,这种字面上的行为反而对你有利。仔细查看每一行命令,并尝试缩小导致错误的指令。
• 尝试完成书中的练习,并通过提供的解决方案检查你的答案——这些答案作为 R 脚本文件都可以在本书网站上找到,* www.nostarch.com/bookofr/。下载.zip文件并提取.R*文件,每个文件对应书中的一个部分。将它们打开并在你的 R 会话中运行,就像运行任何 R 代码一样,可以查看输出。短小的练习题旨在作为练习,而不是困难或难以克服的挑战。完成这些练习所需的一切信息都会在该章节的前面部分提供。
• 尤其是在学习的早期阶段,当你远离这本书时,尽量用 R 来做所有事情,即使是一些你平时可能在其他地方做的简单任务或计算。这将迫使你的思维更频繁地切换到“R 模式”,并且让你迅速适应这个环境。
给教师的建议
本书是根据我目前所在机构——新西兰奥塔哥大学数学与统计系——举办的为期三天的工作坊《R 语言入门》设计的,作为我们为研究生和教职工举办的统计学工作坊(SWoPS)的一部分。此课程由我的两位同事继承并发展为 SWoPS 类别的《统计建模 1》,《R 语言入门》的目标正如其标题所示,旨在解决编程方面的问题。您的内容覆盖将自然取决于目标受众。
在此,我提供了一些使用 《R 语言宝典》 内容的建议,适用于类似于我们 SWoPS 系列的工作坊。根据您的目标工作坊时长和学生现有知识,可以选择性地添加或删除某些章节。
• 编程入门: 第一部分 和 第二部分。从 第五部分 中选取的内容,特别是 第二十三章(高级绘图自定义),也可能适合这样的课程范围。
• 统计学入门: 第三部分 和 第四部分。如果需要简短介绍 R 语言,可以考虑省略,例如 第三部分 中的 第十三章,以及 第四部分 中的 第十七章 到 第十九章,并通过 第一部分 的内容构建初步基础。
• 中级编程与统计: 第二部分 和 第四部分。如果受众希望提高绘图技能,可以考虑省略 第四部分 中的 第十七章 到 第十九章,并包含 第五部分。
• R 图形: 第一部分 和 第五部分。根据受众的知识水平,可能会省略 第一部分 的内容,以便包含 第二部分 中的 第十四章(基础数据可视化)。
如果您打算根据本书设计更长时间的课程,实践练习特别适合作为课后作业,帮助学生跟上 R 语言和统计学技能的发展。构成每章的主要内容相对容易转化为幻灯片,初步结构可以通过“详细目录”得到帮助。
第一部分
语言
第一章:1
开始使用

R 提供了一个非常灵活的编程环境,深受许多从事数据分析工作的研究人员喜爱。在本章中,我将为学习和使用 R 打下基础,并介绍安装 R 的基础知识以及一些在开始之前值得了解的内容。
1.1 从 CRAN 获取和安装 R
R 可用于 Windows、OS X 和 Linux/Unix 平台。你可以在综合 R 存档网络(CRAN)上找到 R 资源的主要集合。如果你访问 R 项目网站 www.r-project.org/,你可以导航到本地的 CRAN 镜像,并下载与你的操作系统相关的安装程序。第 A.1 节提供了安装 R 基础分发版的逐步说明。
1.2 初次打开 R
R 是一种解释型语言,严格区分大小写和字符,这意味着你需要将遵循特定语法规则的指令输入到控制台或命令行界面中。软件随后解释并执行你的代码,并返回任何结果。
注意
R 是一种所谓的 高级 编程语言。 级别 是指从计算机执行的基本细节中抽象出来的层次。也就是说,低级语言需要你像手动管理机器的内存分配一样做一些事情,但像 R 这样的高级语言,幸运的是,你不需要处理这些技术细节。
当你打开基础 R 应用程序时,会看到 R 控制台;图 1-1 显示了 Windows 中的实例,图 1-2 左图显示了 OS X 中的示例。这代表了 R 自然融入的 图形用户界面(GUI),这是使用基础 R 的典型方式。

图 1-1:Windows 中 R GUI 应用程序(默认配置)
解释器的功能性和“简洁”的外观,在我看来,常常让许多本科生感到畏惧,这也恰恰体现了该软件的本质——一个空白的统计画布,可以用于任何数量的任务。请注意,OS X 版本使用单独的窗口来显示控制台和编辑器,而 Windows 中的默认行为是将这些面板包含在一个整体的 R 窗口中(如果需要,你可以在 GUI 偏好设置中更改此设置;详见第 1.2.1 节)。

图 1-2:OS X 中的基础 R GUI 控制台面板(左)和新打开的内置编辑器实例(右)
注意
正如我刚才所做的那样,在本书的某些部分,我将特别提到 Windows 和 OS X 中的 R GUI 功能,因为这两种平台是初学者最常用的。除了 Linux/Unix 实现之外,R 也可以从终端或 shell 运行,甚至可以在批处理模式下运行。书中的绝大多数代码在所有设置下都是功能性的。
1.2.1 控制台和编辑器面板
用于编程 R 代码和查看输出的有两种主要窗口类型。你刚刚看到的控制台或命令行解释器是所有执行发生的地方,也是所有文本和数字输出的地方。你可以直接使用 R 控制台进行计算或绘图。通常,你只会直接使用控制台来执行短小的单行命令。
默认情况下,R 的 提示符 用 > 符号表示,表示 R 已准备好并等待输入命令,此时光标会出现在提示符后面。为了避免与数学符号“更大于”的符号 > 混淆,一些作者(包括我)更喜欢修改这个提示符。一个常见的选择是 R>,你可以按如下方式设置:
> options(prompt="R> ")
R>
将光标置于提示符后,你可以使用键盘上的向上箭头(↑)和向下箭头(↓)滚动浏览之前执行过的命令;这在对之前的命令进行小幅调整时非常有用。
对于较长的代码块和函数的编写,最好先在 编辑器 中编写命令,完成后再在控制台中执行。为此,R 提供了一个内置的代码编辑器。在代码编辑器中编写的 R 脚本 本质上只是带有 .R 扩展名的纯文本文件。
你可以通过 R GUI 菜单打开一个新的编辑器实例(例如,在 Windows 中选择 文件 → 新建脚本,或者在 OS X 中选择 文件 → 新建文档)。
内置编辑器提供了有用的快捷键(例如,在 Windows 中是 CTRL-R,或者在 OS X 中是
-RETURN),这些快捷键会自动将行发送到控制台。你可以发送光标所在的行、已选中的行、已选中的行的一部分,或者已选中的代码块。当你在处理多个 R 脚本文件时,通常会同时打开多个编辑器面板;快捷键提交代码时只会作用于当前选中的编辑器。
控制台和编辑器的美学效果,如着色和字符间距,可以根据操作系统的不同进行一定程度的定制;你只需访问相关的 GUI 偏好设置即可。图 1-3 显示了 Windows 中的 R GUI 偏好设置(编辑 → GUI 偏好设置...)和 OS X 中的设置(R → 偏好设置...)。特别是 OS X 版本的 R 提供了代码着色和括号匹配功能,这能提高大量代码的编写和可读性。

图 1-3:Windows(左)和 OS X(右)中的 R GUI 偏好设置
1.2.2 注释
在 R 中,您可以使用 注释 来注解您的代码。只需在行首加上井号符号(#),之后的内容将被解释器忽略。例如,在控制台执行以下内容不会做任何事情,只会将您返回到提示符:
R> # This is a comment in R...
注释也可以出现在有效命令之后。
R> 1+1 # This works out the result of one plus one!
[1] 2
如果您在编辑器中编写大量或复杂的代码,这种注释可以帮助其他人(以及您自己!)理解您的代码在做什么。
1.2.3 工作目录
活跃的 R 会话始终与一个 工作目录 相关联。除非您在保存或导入数据文件时明确指定文件路径,否则 R 会默认使用这个工作目录。要检查工作目录的位置,可以使用 getwd 函数。
R> getwd()
[1] "/Users/tdavies"
文件路径总是被双引号括起来,R 在指定文件夹位置时使用正斜杠,而不是反斜杠。
您可以使用 setwd 函数更改默认工作目录,如下所示:
R> setwd("/folder1/folder2/folder3/")
您可以相对于当前工作目录提供文件路径,或者提供完整路径(换句话说,从系统根驱动器开始)。无论哪种方式,都必须记住 R 对大小写敏感;您必须精确匹配文件夹名称的大小写和标点符号,否则会抛出错误。
也就是说,如果您愿意每次读取或写入文件时都指定完整且正确的文件路径(第八章中有更多细节),那么相关文件可以存放在您计算机的任何位置。
1.2.4 安装和加载 R 包
R 的基本安装已经准备好大量内建命令,用于数值计算、常见统计分析以及绘图和可视化。这些命令可以直接使用,无需任何加载或导入。我将在本文中称这些函数为 内建 或 现成可用 的函数。
更加专业的技术和数据集包含在 包(也称为 库)中。使用贡献的包是常见的做法,在本书中您将会经常这样做,因此熟悉安装和加载所需库非常重要。
第 A.2 节涵盖了从 CRAN 下载和安装软件包的相关细节,但在这里我将提供一个简要概述。
加载包
有少数推荐的包已经包含在 R 的基础分发版中(列在 第 A.2.2 节)。这些包无需单独安装,但要使用它们,您需要通过调用 library 来加载它们。在本书中,您将使用一个名为 MASS 的包(Venables 和 Ripley, 2002)。要加载它(或任何其他已安装的包)并访问其功能和数据集,只需在提示符下执行 library,如下所示:
R> library("MASS")
请注意,调用 library 仅为当前的 R 会话提供对包功能的访问。当你关闭 R 并重新打开一个新的实例时,你需要重新加载任何你想使用的包。
安装包
有成千上万的贡献包没有包含在典型的 R 安装中;为了让它们可以在 R 中加载,你必须首先从仓库(通常是 CRAN)下载并安装它们。最简单的方法是直接在 R 提示符下使用 install.packages 函数(为此,你需要互联网连接)。
例如,ks 包(Duong, 2007)就是一个这样的包,你将在第二十六章中使用它。执行以下命令将尝试连接到本地 CRAN 镜像,下载并安装 ks,以及它依赖的几个包(称为依赖项):
R> install.packages("ks")
控制台将在过程完成时显示运行输出。
你只需要安装包一次;之后它将对你的 R 安装可用。然后,你可以像加载 MASS 一样,通过调用 library 在任何新打开的 R 实例中加载已安装的包(如 ks)。
第 A.2.3 节提供了有关包安装的更多细节。
更新包
贡献包的维护者会定期提供版本更新,以修复错误和添加功能。你可能需要不时检查已安装包的更新情况。
从 R 提示符执行以下命令,将尝试连接到你的包仓库(默认为 CRAN),查找所有已安装包的更新版本。
R> update.packages()
第 A.3 节提供了更多关于更新包的详细信息,第 A.4 节讨论了备用 CRAN 镜像和仓库。
1.2.5 帮助文件和函数文档
R 配备了一套帮助文件,你可以利用它们来搜索特定功能,了解如何使用给定的函数并指定其参数(换句话说,执行函数时你提供的值或对象),澄清参数在操作中的角色,学习任何返回对象的形式,提供函数使用示例,并获取如何引用软件或数据集的详细信息。
要访问给定命令或其他对象的帮助文件,请在控制台提示符下使用 help 函数或方便的快捷方式 ?。例如,考虑现成的算术平均函数 mean。
R> ?mean
这会弹出图 1-4 中的文件。

图 1-4:R 中函数 mean 的帮助文件(上图)和在 OS X 中搜索字符串 "mean" 的帮助结果(下图)
如果你不确定所需函数的精确名称,你可以使用字符字符串(双引号中的语句)传递给help.search来搜索所有已安装包中的文档,或者你也可以使用??作为快捷方式:
R> ??"mean"
这个搜索会列出包含相关字符串的函数、它们所在的包和描述,如图 1-4 底部图像所示(高亮显示的条目是算术平均值的函数)。
所有帮助文件遵循图 1-4 顶部图像中展示的通用格式;文件的长度和详细程度通常反映了函数执行操作的复杂性。大多数帮助文件包括这里列出的前三项;其他项则是常见但可选的:
• 描述部分简要说明了执行的操作。
• 用法部分指定了函数的形式,包括如何将其传递给 R 控制台,参数的自然顺序以及任何默认值(这些是通过=设置的参数)。
• 在参数部分,提供了每个参数的详细说明,以及它们允许采用的可能值。
• 返回的对象的性质(如果有的话)在返回值下进行了说明。
• 参考文献部分提供了与命令或函数背后的方法论相关的引用。
• 相关函数的帮助文件可以通过另见链接找到。
• 示例提供了可以复制并粘贴到控制台中的可执行代码,演示函数的实际应用。
帮助文件中可能还有更多的字段——具有较长解释的函数通常在参数部分后面包含一个详细信息部分。调用函数时常见的陷阱或错误通常会放在警告部分,额外的信息可以放在注释部分。
虽然当你刚开始时它们看起来可能相当技术化,但我鼓励你继续查看帮助文件——即使你已经知道一个函数的工作原理,熟悉函数文档的布局和解释也是成为一名熟练 R 用户的重要步骤。
1.2.6 第三方编辑器
R 的流行促使了多个第三方代码编辑器的开发,或者为现有的代码编辑软件提供兼容插件,这些都能提升在 R 中编程的体验。
一个值得注意的贡献是 RStudio(RStudio 团队,2015)。这是一个集成开发环境(IDE),可以在 Windows、OS X 和 Linux/Unix 平台上免费使用,网址为www.rstudio.com/。
RStudio 包含一个直接提交的代码编辑器;独立的点选面板,用于文件、对象和项目管理等功能;以及创建包含 R 代码的标记文档。附录 B 更详细地讨论了 RStudio 及其功能。
使用任何第三方编辑器,包括 RStudio,基本上是个人选择。在本书中,我假设使用的是典型的基础 R 图形用户界面应用程序。
1.3 保存工作并退出 R
所以,你已经在 R 中编程几个小时了,是时候回家了吗?在保存 R 中的工作时,你需要注意两点:任何在当前会话中创建(并保存)的 R 对象,以及任何在编辑器中编写的 R 脚本文件。
1.3.1 工作区
你可以使用 GUI 菜单项(例如,在 Windows 中是文件下的选项,在 OS X 中是工作区下的选项)来保存和加载 工作区图像 文件。一个 R 工作区图像包含了在退出时 R 会话中所有的信息,并以 .RData 文件形式保存。这将包括你在会话中创建并保存(换句话说,分配)的所有对象(你将在第二章中看到如何做到这一点),包括那些可能从先前的工作区文件中加载的对象。
本质上,加载存储的 .RData 文件可以让你“从上次停下的地方继续”。在 R 会话的任何时刻,你都可以在提示符下执行 ls(),它会列出当前在活动工作区中所有的对象、变量和用户定义的函数。
或者,你可以在控制台使用 R 命令 save.image 和 load 来处理工作区 .RData 文件——这两个函数都包含一个 file 参数,你可以传递目标 .RData 文件的文件夹位置和名称(有关这些函数的更多信息,请参见相应的帮助文件 ?save.image 和 ?load)。
请注意,以这种方式保存工作区图像不会保留先前加载的任何贡献包的功能。如第 1.2.4 节所述,你需要使用 library 来加载每个新的 R 实例所需的任何包。
退出软件的最快方式是输入 q() 在提示符下:
R> q()
仅仅退出控制台会弹出一个对话框,询问是否希望保存工作区图像。在这种情况下,选择保存并不会打开文件浏览器来命名你的文件,而是会在工作目录中创建(或覆盖)一个“无名”文件,并为其命名为 .RData 扩展名的文件(参见第 1.2.3 节)。
如果在默认工作目录中存在一个未命名的 .RData 文件,当打开新的 R 实例时,程序会自动加载该默认工作区——如果发生了这种情况,你将在控制台的欢迎文本中收到通知。
注意
与 .RData 文件一起,R 会自动保存一个文件,包含所有在控制台中执行的命令的逐行历史记录,并将其保存在同一目录下的相关工作空间中。正是这个历史文件使你能够使用键盘方向键滚动查看之前执行的命令,正如前面所提到的。
1.3.2 脚本
对于需要超过少量命令的任务,通常你会希望在内置的代码编辑器中工作。因此,保存你的 R 脚本至少和保存工作空间一样重要,甚至可能更重要。
你将编辑器脚本保存为纯文本文件,文件扩展名为 .R(在第 1.2.1 节中提到);这会让你的操作系统默认将这些文件与 R 软件关联。要从内置编辑器保存脚本,请确保选择了编辑器,然后导航到 文件 → 保存(或在 Windows 中按 CTRL-S,或在 OS X 中按
-S)。要打开一个之前保存的脚本,请选择 文件 → 打开脚本...(在 Windows 中按 CTRL-O),或在 OS X 中选择 文件 → 打开文档... (
-O)。
通常,如果你的脚本文件已保存,你就不需要保存工作空间 .RData 文件。只要在新的 R 控制台中重新执行保存脚本中的任何必要命令,之前创建的对象(换句话说,即那些包含在保存的 .RData 文件中的对象)将被重新创建。这在你同时处理多个问题时非常有用,因为单靠独立的默认工作空间很容易不小心覆盖一个对象。因此,保持 R 脚本的分离是区分多个项目的简单方法,无需担心覆盖任何之前可能存储的重要内容。
R 还提供了多种方法将单独的对象(如数据集和绘图的图像文件)写入磁盘,你将在第八章中了解更多内容。
1.4 约定
在书中,我会遵循一些关于代码和数学表达式的呈现约定。
1.4.1 编程
如前所述,当你使用 R 编程时,通常是在控制台中执行代码,可能是在编辑器中先编写脚本。以下几点是需要特别注意的:
• 直接在控制台输入执行的 R 代码显示时,前面会有 R> 提示符,并且后面会跟上控制台中显示的任何输出。例如,来自第 2.1.1 节的这个简单的 14 除以 6 运算看起来是这样的:
R> 14/6
[1] 2.333333
如果你想直接从书中的文本中复制并粘贴在控制台执行的代码,你需要去掉 R> 提示符。
• 对于应该在编辑器中编写后再在控制台执行的代码,我会在文本中注明,并且代码将 不带 提示符。以下示例来自第 10.2.1 节:
for(myitem in 5:7){
cat("--大括区域开始--\n")
cat("当前项是", myitem, "\n")
cat("--大括区域结束--\n\n")
}
我在实际编排和缩进此类代码块时的编码风格,会随着你在 第 II 部分 的学习而变得更加清晰。
• 有时会出现较长的代码行(无论是在控制台直接执行还是在编辑器中编写),为了打印方便,这些代码行将在合适的位置分割并缩进,以适应页面。例如,看看 第 6.2.2 节中的这一行:
R> ordfac.vec <- factor(x=c("Small","Large","Large","Regular","Small"),
levels=c("Small","Regular","Large"),
ordered=TRUE)
尽管在使用 R 时可以将其写成一行,但你也可以在逗号处换行(在这种情况下,逗号分隔了 factor 函数的参数)。换行后的代码将会缩进至相关命令的左括号位置。无论是单行形式还是分行形式,执行时都能正常工作。
• 最后,在某些地方,当控制台输出较长且对于你理解当前内容并非必需时,为了打印方便,它将被抑制。我会在文本中说明这一点,你会看到受影响的代码块中出现 --snip-- 的标记。
1.4.2 数学和方程引用
本书中出现的数学和方程(主要在 第 III 部分 和 第 IV 部分 中)将尽量简化,但在某些章节中,有时需要涉及一些数学细节。
重要的方程将单独列在自己的行上,如下所示:

方程会用括号编号,文中的方程引用将使用这些带括号的数字,前面可能会加上 Equation。例如,你会看到以下两种方式引用方程:
• 根据 方程 (1.1),当 x = 2 时,y = 8。
• 对(1.1)进行反演得到 x = y/4。
当数字结果四舍五入到某一位时,会根据小数位数(decimal places,缩写为 d.p.)注明。例如:
• 著名的几何值π表示为 π = 3.1416(保留 4 位小数)。
• 在(1.1)中设置 x = 1.467 会得到 y = 5.87(保留 2 位小数)。
1.4.3 练习
章节中的练习题会出现在一个圆角框中:
练习 1.1
-
大声说出 cat 这个词。
-
仅用你的大脑,找出 1 + 1 的解答。
这些练习是可选的。如果你选择完成它们,它们旨在帮助你在出现在文本中的时候完成,以帮助你练习和理解紧接在它们前面的章节中的具体内容和代码。
本书中用于编码和绘图示例的所有数据集,都可以作为内建的 R 对象或作为你将安装的贡献包的一部分。这些包会在相关文字中注明(有关它们的简短列表,请参见第 A.2.3 节)。
为了方便起见,本书中的所有代码示例,以及对所有练习题的完整建议解决方案,都可以作为可运行的.R脚本文件免费下载,文件可在本书的网页上找到,网址是 www.nostarch.com/bookofr/。
你应该将这些解决方案(以及任何附带的评论)视为“建议”,因为在 R 中通常有多种方式来执行某项任务,这些方式未必比提供的方式更好或更差。
本章重要代码
| 函数/操作符 | 简要描述 | 首次出现 |
|---|---|---|
options |
设置各种 R 选项 | 第 1.2.1 节,第 5 页 |
# |
注释(解释器忽略) | 第 1.2.2 节,第 6 页 |
getwd |
打印当前工作目录 | 第 1.2.3 节,第 7 页 |
setwd |
设置当前工作目录 | 第 1.2.3 节,第 7 页 |
library |
加载已安装的包 | 第 1.2.4 节,第 7 页 |
install.packages |
下载并安装包 | 第 1.2.4 节,第 8 页 |
update.packages |
更新已安装的包 | 第 1.2.4 节,第 8 页 |
help 或 ? |
函数/对象帮助文件 | 第 1.2.5 节,第 9 页 |
help.search 或 ?? |
搜索帮助文件 | 第 1.2.5 节,第 10 页 |
q |
退出 R | 第 1.3.1 节,第 12 页 |
第二章:2
数值、算术、赋值和向量

在最简单的情况下,R 可以作为一个普通的桌面计算器。在本章中,我将讨论如何使用软件进行算术运算。我还会展示如何存储结果,以便你可以在其他计算中稍后使用它们。接下来,你将学习向量,它可以让你一次处理多个值。向量是 R 中的一个重要工具,R 的许多功能设计时都考虑到了向量操作。你将研究一些常见且有用的方式来操作向量,并利用面向向量的行为。
2.1 R 基础数学
所有常见的算术操作和数学功能都可以在控制台提示符下使用。你可以分别使用符号 +、-、* 和 / 执行加法、减法、乘法和除法。你可以使用 ^ 创建指数(也称为幂或指数),并通过括号 () 控制单个命令中的运算顺序。
2.1.1 算术运算
在 R 中,标准的数学规则始终适用,并遵循通常的从左到右的运算顺序:括号、指数、乘法、除法、加法、减法(PEMDAS)。这是一个在控制台中的示例:
R> 2+3
[1] 5
R> 14/6
[1] 2.333333
R> 14/6+5
[1] 7.333333
R> 14/(6+5)
[1] 1.272727
R> 3²
[1] 9
R> 2³
[1] 8
你可以使用 sqrt 函数来计算任何非负数的平方根。只需将期望的数字提供给 x,如下所示:
R> sqrt(x=9)
[1] 3
R> sqrt(x=5.311)
[1] 2.304561
在使用 R 时,你会发现需要将复杂的算术公式转化为代码进行计算(例如,当复制教科书或研究论文中的计算时)。以下示例提供了一个数学表达式的计算,然后是其在 R 中的执行:

请注意,有些 R 表达式需要额外的括号,而这些括号在数学表达式中并不存在。缺少或位置不当的括号是 R 中算术错误的常见原因,尤其是在处理指数时。如果指数本身是一个算术计算,它必须始终放在括号中。例如,在第三个表达式中,你需要在 2.25-1/4 周围加上括号。如果要对某个数进行幂运算,而这个数本身是一个计算结果,像第三个例子中的表达式 2^(2+1),你也需要使用括号。请注意,R 将负数视为一个计算,因为它会将 -2 解释为 -1*2。这就是为什么在这个表达式中你也需要将 -2 放入括号的原因。强调这些问题很重要,因为它们在大量代码中很容易被忽视。
2.1.2 对数与指数
你会经常看到或阅读到研究人员对某些数据进行对数变换。这意味着根据对数重新缩放数字。当给定一个数字x和一个被称为底数的值时,对数计算出必须将底数提升到哪个幂才能得到x。例如,x = 243 以 3 为底的对数(数学上写作 log[3] 243)为 5,因为 3⁵ = 243。在 R 中,对数变换通过log函数实现。你提供log函数需要变换的数字,赋值给x,以及底数,赋值给base,如下所示:
R> log(x=243,base=3)
[1] 5
以下是一些需要考虑的事项:
• x和底数都必须是正数。
• 当底数为x时,任意数x的对数结果为 1。
• 对于x = 1 的对数结果永远是 0,不论底数是什么。
在数学中有一种特别的对数变换,称为自然对数,它将底数固定为一个特殊的数学常数——欧拉数。这个数通常表示为e,约等于 2.718。
欧拉数引出了指数函数,其定义为e的x次方,其中x可以是任何数(负数、零或正数)。指数函数f(x) = e^x,通常写作 exp(x),表示自然对数的逆,使得 exp(log[e] x) = log[e] exp(x) = x。指数函数的 R 命令是exp:
R> exp(x=3)
[1] 20.08554
log的默认行为是默认使用自然对数:
R> log(x=20.08554)
[1] 3
如果你想使用除了e以外的其他底数,必须自己提供base的值。这里提到对数和指数函数,因为它们在本书后面非常重要——许多统计方法使用它们,因为它们具有多种有用的数学性质。
2.1.3 E-表示法
当 R 打印出超出某个有效数字阈值(默认设置为 7)的非常大或非常小的数字时,这些数字将以经典的科学计数法表示。E 表示法是大多数编程语言——甚至许多桌面计算器——所使用的,用以便于理解极端值。在 E 表示法中,任何数字x都可以表示为xey,它等于x × 10^(y)。考虑数字 2,342,151,012,900。例如,它可以表示为:
• 2.3421510129e12,相当于写作 2.3421510129 × 10¹²
• 234.21510129e10,相当于写作 234.21510129 × 10¹⁰
你可以为y的幂使用任何值,但标准的 E 表示法使用将小数点放在第一个有效数字后面的幂。简单来说,对于正的幂+y,E 表示法可以解释为“将小数点向右移动y位”。对于负的幂−y,解释为“将小数点向左移动y位”。这正是 R 如何呈现 E 表示法的方式:
R> 2342151012900
[1] 2.342151e+12
R> 0.0000002533
[1] 2.533e-07
在第一个示例中,R 只显示前七位有效数字,并隐藏其余部分。请注意,即使 R 隐藏了数字,计算过程中并不会丢失任何信息;e-表示法仅仅是为了方便用户阅读,额外的数字仍然被 R 存储,即使它们没有显示出来。
最后,注意 R 必须对数字的极限值施加约束,才能将其视为无穷大(对于大数字)或零(对于小数字)。这些约束取决于你的具体系统,我将在第 6.1.1 节中详细讨论技术细节。然而,任何现代桌面系统默认情况下都可以被认为足够精确,能够满足大多数 R 中的计算和统计需求。
练习 2.1
-
使用 R,验证以下内容
![image]()
当 a = 2.3 时。
-
以下哪个选项是将负 4 平方并加 2 到结果?
-
(-4)²+2 -
-4²+2 -
(-4)^(2+2) -
-4^(2+2)
-
-
使用 R,你如何计算数字 25.2、15、16.44、15.3 和 18.6 的平均值的一半的平方根?
-
计算 log[e] 0.3。
-
计算你在 (d) 中的答案的指数变换。
-
在控制台中打印时,识别 R 如何表示 −0.00000000423546322 这个数字。
2.2 赋值对象
到目前为止,R 仅通过将计算结果打印到控制台来展示示例计算结果。如果你想保存结果并进行进一步操作,你需要能够将给定计算的结果赋值给当前工作空间中的一个对象。简单来说,这意味着将某个项或结果存储在给定的名称下,以便稍后访问,而不必重新计算这个结果。在本书中,我将交替使用赋值和存储这两个术语。请注意,有些编程书籍将存储的对象称为变量,因为可以轻松地覆盖该对象并将其更改为其他内容,这意味着它所代表的内容可能在整个会话中发生变化。然而,我将在本书中使用对象这个术语,因为我们将在第三部分讨论变量作为一个截然不同的统计学概念。
你可以通过两种方式在 R 中指定赋值:使用箭头符号(<-)和使用单个等号符号(=)。这两种方法在这里都展示了:
R> x <- -5
R> x
[1] -5
R> x = x + 1 # this overwrites the previous value of x
R> x
[1] -4
R> mynumber = 45.2
R> y <- mynumber*x
R> y
[1] -180.8
R> ls()
[1] "mynumber" "x" "y"
正如你从这些示例中看到的,当你将对象的名称输入到控制台时,R 会显示分配给该对象的值。当你在后续操作中使用该对象时,R 会替代你分配给它的值。最后,如果你使用 ls 命令(你在第 1.3.1 节中看到过)检查当前工作空间的内容,它将按字母顺序显示对象的名称(以及其他任何先前创建的项目)。
虽然=和<-执行相同的操作,但为了代码的整洁(至少在这方面),保持一致性是明智的。许多用户选择坚持使用<-,因为使用=可能会引起混淆(例如,之前我显然不是想说x在数学上等于x + 1)。在本书中,我也会这样做,并将=保留用于设置函数参数,这将在第 2.3.2 节中开始讲解。到目前为止,你只使用了数字值,但请注意,赋值的过程对于所有类型和类的对象都是通用的,接下来的章节中你将进一步探讨这些内容。
对象的命名几乎没有限制,只要名称以字母开头(换句话说,不能以数字开头),避免使用符号(不过下划线和句点是可以的),并避免使用一些“保留”词汇,例如用于定义特殊值的词汇(见第 6.1 节)或用于控制代码流的词汇(见第十章)。你可以在第 9.1.2 节找到这些命名规则的有用总结。
练习 2.2
-
创建一个存储值为 3² × 4^(1/8)的对象。
-
用(a)中的对象除以 2.33 来覆盖该对象。将结果打印到控制台。
-
创建一个值为−8.2 × 10^(−13)的新对象。
-
直接在控制台打印将(b)乘以(c)的结果。
2.3 向量
通常你会希望对多个实体执行相同的计算或比较,例如当你需要重新缩放数据集中的测量值时。你可以逐个条目进行操作,尽管这样显然不理想,尤其是当你有大量条目时。R 提供了一个更高效的解决方案——向量。
目前为了简化问题,你将继续只处理数字条目,尽管这里讨论的许多实用函数也可以应用于包含非数字值的结构。你将在第四章开始查看这些其他类型的数据。
2.3.1 创建一个向量
向量是 R 中处理多个项目的基本构件。从数字的角度来看,你可以把向量看作是关于单一变量的观察值或测量值的集合,例如 50 个人的身高或你每天喝的咖啡数量。更复杂的数据结构可能由多个向量组成。创建向量的函数是单个字母c,所需条目放在括号内,条目之间用逗号分隔。
R> myvec <- c(1,3,1,42)
R> myvec
[1] 1 3 1 42
向量条目可以是计算结果或先前存储的项目(包括向量本身)。
R> foo <- 32.1
R> myvec2 <- c(3,-3,2,3.45,1e+03,64⁰.5,2+(3-1.1)/9.44,foo)
R> myvec2
[1] 3.000000 -3.000000 2.000000 3.450000 1000.000000 8.000000
[7] 2.201271 32.100000
这段代码创建了一个新向量并赋值给对象myvec2。其中一些条目被定义为算术表达式,存储在向量中的则是表达式的结果。最后一个元素foo是一个已定义的数字对象,其值为32.1。
我们来看一个新的例子。
R> myvec3 <- c(myvec,myvec2)
R> myvec3
[1] 1.000000 3.000000 1.000000 42.000000 3.000000 -3.000000
[7] 2.000000 3.450000 1000.000000 8.000000 2.201271 32.100000
这段代码创建并存储了另一个向量myvec3,它包含了myvec和myvec2的条目,按顺序连接在一起。
2.3.2 序列、重复、排序和长度
在这里,我将讨论一些与 R 向量相关的常见和有用的函数:seq、rep、sort和length。
让我们创建一个均匀间隔的递增或递减数值序列。这是您在编程循环时(见第十章)或绘制数据点时(见第七章)常常需要的功能。创建此类序列最简单的方法是使用冒号操作符,将数值按间隔 1 分隔。
R> 3:27
[1] 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
示例3:27应理解为“从 3 到 27(步长为 1)”。结果是一个数字向量,就像您手动列出每个数字并用c括起来一样。像往常一样,您也可以在使用冒号操作符时提供先前存储的值或(严格的)括号计算:
R> foo <- 5.3
R> bar <- foo:(-47+1.5)
R> bar
[1] 5.3 4.3 3.3 2.3 1.3 0.3 -0.7 -1.7 -2.7 -3.7 -4.7
[12] -5.7 -6.7 -7.7 -8.7 -9.7 -10.7 -11.7 -12.7 -13.7 -14.7 -15.7
[23] -16.7 -17.7 -18.7 -19.7 -20.7 -21.7 -22.7 -23.7 -24.7 -25.7 -26.7
[34] -27.7 -28.7 -29.7 -30.7 -31.7 -32.7 -33.7 -34.7 -35.7 -36.7 -37.7
[45] -38.7 -39.7 -40.7 -41.7 -42.7 -43.7 -44.7
使用 seq 生成序列
您还可以使用seq命令,它允许您更灵活地创建序列。这个现成的函数接收一个from值,一个to值,以及一个by值,然后返回相应的序列,作为一个数字向量。
R> seq(from=3,to=27,by=3)
[1] 3 6 9 12 15 18 21 24 27
这将为您提供一个间隔为 3 而不是 1 的序列。请注意,这类序列将始终从from数字开始,但是否包含to数字取决于您要求 R 按什么方式增加(或减少)它们。例如,如果您按偶数增加(或减少),且序列以奇数结尾,则最后一个数字将不会被包含。然而,您可以通过指定length.out值来生成一个具有该数量数字的向量,这些数字在from和to值之间均匀分布。
R> seq(from=3,to=27,length.out=40)
[1] 3.000000 3.615385 4.230769 4.846154 5.461538 6.076923 6.692308
[8] 7.307692 7.923077 8.538462 9.153846 9.769231 10.384615 11.000000
[15] 11.615385 12.230769 12.846154 13.461538 14.076923 14.692308 15.307692
[22] 15.923077 16.538462 17.153846 17.769231 18.384615 19.000000 19.615385
[29] 20.230769 20.846154 21.461538 22.076923 22.692308 23.307692 23.923077
[36] 24.538462 25.153846 25.769231 26.384615 27.000000
通过将length.out设置为40,您可以让程序从 3 到 27 打印出恰好 40 个均匀间隔的数字。
对于递减序列,by的值必须为负。这里是一个例子:
R> foo <- 5.3
R> myseq <- seq(from=foo,to=(-47+1.5),by=-2.4)
R> myseq
[1] 5.3 2.9 0.5 -1.9 -4.3 -6.7 -9.1 -11.5 -13.9 -16.3 -18.7 -21.1
[13] -23.5 -25.9 -28.3 -30.7 -33.1 -35.5 -37.9 -40.3 -42.7 -45.1
这段代码使用先前存储的对象foo作为from的值,并使用括号中的计算(-47+1.5)作为to的值。给定这些值(即foo大于(-47+1.5)),序列只能按负数步长递进;在上面,我们将by设置为-2.4。然而,使用length.out来创建递减序列的方式保持不变(指定“负长度”是没有意义的)。对于相同的from和to值,您可以轻松创建一个长度为 5 的递减序列,如下所示:
R> myseq2 <- seq(from=foo,to=(-47+1.5),length.out=5)
R> myseq2
[1] 5.3 -7.4 -20.1 -32.8 -45.5
有一些简化的调用方式,您将在第九章中学到这些,但在这些早期阶段,我将坚持使用显式的用法。
使用 rep 进行重复
序列非常有用,但有时您可能只想重复某个特定值。您可以使用rep来做到这一点。
R> rep(x=1,times=4)
[1] 1 1 1 1
R> rep(x=c(3,62,8.3),times=3)
[1] 3.0 62.0 8.3 3.0 62.0 8.3 3.0 62.0 8.3
R> rep(x=c(3,62,8.3),each=2)
[1] 3.0 3.0 62.0 62.0 8.3 8.3
R> rep(x=c(3,62,8.3),times=3,each=2)
[1] 3.0 3.0 62.0 62.0 8.3 8.3 3.0 3.0 62.0 62.0 8.3 8.3 3.0 3.0 62.0
[16] 62.0 8.3 8.3
rep函数接受一个单一值或一个值的向量作为参数x,并且需要提供times和each的值。times的值指定重复x的次数,而each指定重复x中每个元素的次数。在上面的第一行,你仅仅是将一个值重复四次。其他例子首先使用rep和times对一个向量进行重复整个向量的操作,然后使用each重复向量中每个元素,最后同时使用times和each一次性完成两者操作。
如果没有指定times或each,R 的默认行为是将times和each的值视为1,因此调用rep(x=c(3,62,8.3))时,返回的将是原始的x,没有任何变化。
与seq一样,你可以将rep的结果包含在相同数据类型的向量中,如下例所示:
R> foo <- 4
R> c(3,8.3,rep(x=32,times=foo),seq(from=-2,to=1,length.out=foo+1))
[1] 3.00 8.30 32.00 32.00 32.00 32.00 -2.00 -1.25 -0.50 0.25 1.00
在这里,我构建了一个向量,其中第三到第六个条目(包含在内)由rep命令的计算结果决定——单一值32被重复foo次(其中foo的值为 4)。最后五个条目是seq的计算结果,即从−2 到 1 的序列,长度为foo+1(5)。
使用 sort 进行排序
对一个向量进行升序或降序排序是日常任务中的另一个简单操作。便捷的sort函数正是用来做这件事的。
R> sort(x=c(2.5,-1,-10,3.44),decreasing=FALSE)
[1] -10.00 -1.00 2.50 3.44
R> sort(x=c(2.5,-1,-10,3.44),decreasing=TRUE)
[1] 3.44 2.50 -1.00 -10.00
R> foo <- seq(from=4.3,to=5.5,length.out=8)
R> foo
[1] 4.300000 4.471429 4.642857 4.814286 4.985714 5.157143 5.328571 5.500000
R> bar <- sort(x=foo,decreasing=TRUE)
R> bar
[1] 5.500000 5.328571 5.157143 4.985714 4.814286 4.642857 4.471429 4.300000
R> sort(x=c(foo,bar),decreasing=FALSE)
[1] 4.300000 4.300000 4.471429 4.471429 4.642857 4.642857 4.814286 4.814286
[9] 4.985714 4.985714 5.157143 5.157143 5.328571 5.328571 5.500000 5.500000
sort函数非常简单。你将一个向量作为参数x传入该函数,第二个参数decreasing表示你希望排序的顺序。这个参数使用了一种你之前没有遇到过的类型:即重要的逻辑值。逻辑值只能是两个特定的、区分大小写的值之一:TRUE或FALSE。一般来说,逻辑值用于指示某个条件是否满足,并且是所有编程语言的重要组成部分。你将在 R 语言第四章第一部分中详细学习逻辑值。现在,关于sort,你可以将decreasing=FALSE设为从小到大排序,而decreasing=TRUE则是从大到小排序。
使用 length 查找向量长度
我将通过length函数来总结这一部分,length函数用于确定作为参数x传入的向量中存在多少条目。
R> length(x=c(3,2,8,1))
[1] 4
R> length(x=5:13)
[1] 9
R> foo <- 4
R> bar <- c(3,8.3,rep(x=32,times=foo),seq(from=-2,to=1,length.out=foo+1))
R> length(x=bar)
[1] 11
请注意,如果你包含依赖于其他函数计算结果的条目(在此案例中,调用了rep和seq),length会告诉你在这些内置函数执行后的条目数。
练习 2.3
-
创建并存储一个从 5 到−11 的数值序列,步长为 0.3。
-
使用相反顺序的相同序列覆盖(a)中的对象。
-
将向量
c(-1,3,-5,7,-9)重复两次,每个元素重复 10 次,并存储结果。然后将结果按从大到小排序并显示。 -
创建并存储一个包含以下内容的向量,无论其配置如何:
-
从 6 到 12(包含 12)的整数序列
-
值 5.3 的三重重复
-
数字−3
-
从 102 开始,到在(c)中创建的向量的总长度为止,共九个值的序列
-
-
确认在(d)中创建的向量的长度为 20。
2.3.3 子集和元素提取
在你到目前为止在控制台屏幕上看到的所有结果中,你可能注意到一个奇怪的特征。在输出的左侧紧挨着有一个方括号中的[1]。当输出是一个较长的向量,跨越了控制台的宽度并换行时,新的方括号中的数字会出现在新的一行左侧。这些数字表示直接右侧条目的索引。简单来说,索引对应于一个值在向量中的位置,这就是为什么第一个值旁边总是有一个[1](即使它是唯一的值,不是更大向量的一部分)。
这些索引允许你从一个向量中提取特定的元素,这被称为子集提取。假设你在工作区有一个名为myvec的向量。那么myvec中将会有length(x=myvec)个条目,每个条目有一个特定的位置:1、2、3,一直到length(x=myvec)。你可以通过让 R 返回myvec在特定位置的值来访问单个元素,这通过输入向量的名称并在后面跟上方括号中的位置来完成。
R> myvec <- c(5,-2.3,4,4,4,6,8,10,40221,-8)
R> length(x=myvec)
[1] 10
R> myvec[1]
[1] 5
R> foo <- myvec[2]
R> foo
[1] -2.3
R> myvec[length(x=myvec)]
[1] -8
因为length(x=myvec)给出了向量的最终索引(在这个例子中是10),在方括号中输入这个短语会提取最后一个元素-8。同样,你可以通过从长度中减去 1 来提取倒数第二个元素;我们来尝试一下,并将结果赋值给一个新对象:
R> myvec.len <- length(x=myvec)
R> bar <- myvec[myvec.len-1]
R> bar
[1] 40221
正如这些例子所示,索引可能是其他数字或之前存储值的算术函数。你可以像平常一样使用<-符号将结果赋值给工作区中的新对象。利用你对序列的了解,你可以使用冒号符号和特定向量的长度来获得所有可能的索引,以提取向量中的特定元素:
R> 1:myvec.len
[1] 1 2 3 4 5 6 7 8 9 10
你还可以通过使用方括号中提供的索引的负数版本来删除单个元素。继续使用前面定义的对象myvec、foo、bar和myvec.len,请考虑以下操作:
R> myvec[-1]
[1] -2.3 4.0 4.0 4.0 6.0 8.0 10.0 40221.0 -8.0
这一行生成了不包含第一个元素的myvec内容。同样,以下代码将myvec中不包含第二个元素的内容赋值给对象baz:
R> baz <- myvec[-2]
R> baz
[1] 5 4 4 4 6 8 10 40221 -8
同样,方括号中的索引也可以是一个适当计算的结果,如下所示:
R> qux <- myvec[-(myvec.len-1)]
R> qux
[1] 5.0 -2.3 4.0 4.0 4.0 6.0 8.0 10.0 -8.0
使用方括号操作符来提取或删除向量中的值,并不会改变你正在子集化的原始向量,除非你显式地用子集化后的版本覆盖该向量。例如,在这个例子中,qux是一个新向量,定义为myvec去掉倒数第二个条目,但在你的工作空间中,myvec本身保持不变。换句话说,以这种方式对子集化向量仅仅返回请求的元素,如果你想的话可以将它们分配给一个新对象,但不会改变工作空间中的原始对象。
现在,假设你想通过qux和bar将myvec重新拼接在一起。你可以调用类似这样的代码:
R> c(qux[-length(x=qux)],bar,qux[length(x=qux)])
[1] 5.0 -2.3 4.0 4.0 4.0 6.0 8.0 10.0 40221.0
[10] -8.0
如你所见,这行代码使用c将向量分成三部分来重构:qux[-length(x=qux)],之前定义的bar对象,以及qux[length(x=qux)]。为了清晰起见,我们依次查看每一部分。
• qux[-length(x=qux)]
这段代码返回了qux的值,除了它的最后一个元素。
R> length(x=qux)
[1] 9
R> qux[-length(x=qux)]
[1] 5.0 -2.3 4.0 4.0 4.0 6.0 8.0 10.0
现在你有一个与myvec的前八个条目相同的向量。
• bar
之前,你已经将bar存储为以下内容:
R> bar <- myvec[myvec.len-1]
R> bar
[1] 40221
这正是myvec中qux缺少的倒数第二个元素。因此,你将把这个值插入到qux[-length(x=qux)]之后。
• qux[length(x=qux)]
最后,你只需要qux的最后一个元素,它与myvec的最后一个元素匹配。这是通过length从qux中提取的(不像之前那样删除)。
R> qux[length(x=qux)]
[1] -8
现在应该清楚了,如何通过按此顺序调用这三部分代码来重构myvec。
与 R 中的大多数操作一样,你不必逐一进行操作。你还可以使用索引向量来子集化对象,而不是单个索引。再次使用之前的myvec,你可以得到以下结果:
R> myvec[c(1,3,5)]
[1] 5 4 4
这一次返回了myvec的第一个、第三个和第五个元素。另一个常见且方便的子集化工具是冒号操作符(在第 2.3.2 节中讨论),它用于创建索引序列。这里是一个例子:
R> 1:4
[1] 1 2 3 4
R> foo <- myvec[1:4]
R> foo
[1] 5.0 -2.3 4.0 4.0
这提供了myvec的前四个元素(记住,冒号操作符返回一个数字向量,所以不需要显式使用c来包裹它)。
返回的元素的顺序完全取决于方括号中提供的索引向量。例如,再次使用foo,考虑索引的顺序和由此得到的提取结果,如下所示:
R> length(x=foo):2
[1] 4 3 2
R> foo[length(foo):2]
[1] 4.0 4.0 -2.3
这里你从向量的末尾提取了元素,向后工作。你还可以使用rep来重复一个索引,如下所示:
R> indexes <- c(4,rep(x=2,times=3),1,1,2,3:1)
R> indexes
[1] 4 2 2 2 1 1 2 3 2 1
R> foo[indexes]
[1] 4.0 -2.3 -2.3 -2.3 5.0 5.0 -2.3 4.0 -2.3 5.0
这现在已经是一个比严格的“子集”更通用的东西——通过使用索引向量,你可以创建一个全新的向量,长度可以为任意值,并包含原始向量中的一些或所有元素。如前所示,这个索引向量可以包含按任意顺序排列的目标元素位置,并且可以重复索引。
你还可以在删除多个元素后返回向量的元素。例如,要创建一个删除了foo中的第一个和第三个元素后的向量,可以执行以下操作:
R> foo[-c(1,3)]
[1] -2.3 4.0
请注意,无法在单个索引向量中混合使用正负索引。
有时候,你需要用新值覆盖现有向量中的某些元素。在这种情况下,你首先使用方括号指定要覆盖的元素,然后使用赋值运算符给这些元素赋上新值。以下是一个例子:
R> bar <- c(3,2,4,4,1,2,4,1,0,0,5)
R> bar
[1] 3 2 4 4 1 2 4 1 0 0 5
R> bar[1] <- 6
R> bar
[1] 6 2 4 4 1 2 4 1 0 0 5
这会将bar的第一个元素(原为3)覆盖为新值6。当选择多个元素时,你可以指定一个单一的值来替换它们,或者输入一个与所选替换元素数量相同的向量,从而一一替换它们。我们来尝试一下之前提到的同一个bar向量。
R> bar[c(2,4,6)] <- c(-2,-0.5,-1)
R> bar
[1] 6.0 -2.0 4.0 -0.5 1.0 -1.0 4.0 1.0 0.0 0.0 5.0
在这里,你将第 2、第 4 和第 6 个元素分别覆盖为-2、-0.5和-1,其余部分保持不变。相比之下,以下代码将元素7到10(包括第 10 个)覆盖为100:
R> bar[7:10] <- 100
R> bar
[1] 6.0 -2.0 4.0 -0.5 1.0 -1.0 100.0 100.0 100.0 100.0 5.0
最后,需要提到的是,本节仅讨论了 R 中两种主要方法之一,或者说是两种“风格”的向量元素提取方法。你将在第 4.1.5 节中查看另一种方法,使用逻辑标志。
练习 2.4
-
创建并存储一个包含以下元素的向量,顺序如下:
– 一个从 3 到 6(包括 6)的长度为 5 的序列
– 向量
c(2,-5.1,-33)的两倍重复– 值
![image]()
-
从向量(a)中提取第一个和最后一个元素,并将它们存储为一个新对象。
-
将通过省略向量(a)的第一个和最后一个值返回的值存储为第三个对象。
-
只使用(b)和(c)来重建(a)。
-
使用从最小到最大排序后的相同值覆盖(a)。
-
使用冒号运算符作为索引向量来反转(e)的顺序,并确认这与对(e)使用
sort函数并设置decreasing=TRUE是等价的。 -
从(c)中创建一个向量,其中包含(c)的第三个元素重复三次、第六个元素重复四次以及最后一个元素重复一次。
-
创建一个新向量,作为(e)的副本,将(e)原样赋值给一个新命名的对象。使用这个新副本的(e),依次将第一个元素、第 5 到第 7 个元素(包括第 7 个)以及最后一个元素覆盖为 99 到 95(包括 95)的值。
2.3.4 向量导向行为
向量之所以如此有用,是因为它们允许 R 以高速和高效的方式对多个元素同时进行操作。这个面向向量、向量化或按元素的行为是该语言的一个关键特性,你将在这里通过一些重新缩放测量的示例简要地了解它。
让我们从这个简单的例子开始:
R> foo <- 5.5:0.5
R> foo
[1] 5.5 4.5 3.5 2.5 1.5 0.5
R> foo-c(2,4,6,8,10,12)
[1] 3.5 0.5 -2.5 -5.5 -8.5 -11.5
这段代码创建了一个从 5.5 到 0.5 的六个值的序列,增量为 1。然后,从这个向量中减去另一个包含 2、4、6、8、10 和 12 的向量。这会怎么做呢?简单来说,R 会根据元素的位置匹配每个元素,并对每一对相应的元素进行操作。结果向量是通过从 c(2,4,6,8,10,12) 的第一个元素减去 foo 的第一个元素(5.5 − 2 = 3.5),然后从 c(2,4,6,8,10,12) 的第二个元素减去 foo 的第二个元素(4.5 − 4 = 0.5),依此类推。因此,与手动或显式使用循环逐个处理每个元素相比,R 允许使用面向向量的行为进行快速而高效的替代。图 2-1 展示了如何理解这种类型的计算,并突出了元素位置在最终结果中的重要性;不同位置的元素互不影响。
当使用不同长度的向量时,情况会变得更加复杂,这种情况可以通过两种不同的方式发生。第一种是较长的向量的长度可以被较短向量的长度整除。第二种是较长的向量的长度不能被较短向量的长度整除——这通常是用户无意的行为。在这两种情况下,R 本质上会尝试重复,或回收,较短的向量,直到它的长度与较长的向量匹配,然后完成指定的操作。举个例子,假设你想将之前显示的 foo 的条目交替设为负数和正数。你可以显式地将 foo 乘以 c(1,-1,1,-1,1,-1),但你不需要写出完整的后者向量。相反,你可以写以下内容:

图 2-1:在 R 中对两个相同长度的向量进行比较或操作时,按元素行为的概念图。请注意,操作是通过将元素位置一一匹配来进行的。
R> bar <- c(1,-1)
R> foo*bar
[1] 5.5 -4.5 3.5 -2.5 1.5 -0.5
这里 bar 已在 foo 的整个长度上重复应用,直到完成。图 2-2 的左侧图展示了这个特定示例。现在让我们看看当向量长度不能整除时会发生什么。
R> baz <- c(1,-1,0.5,-0.5)
R> foo*baz
[1] 5.50 -4.50 1.75 -1.25 1.50 -0.50
Warning message:
In foo * baz :
longer object length is not a multiple of shorter object length
在这里,你可以看到 R 已将 foo 的前四个元素与整个 baz 匹配,但它无法再次完全重复该向量。虽然 R 尝试重复该向量,将 baz 的前两个元素与较长的 foo 的后两个元素匹配,但这并不没有完全成功,因为 R 发出警告,提示长度无法均匀除尽(你将在第 12.1 节中更详细地查看警告)。右图中的图 2-2 展示了这一示例。

图 2-2:对两个不同长度的向量执行逐元素操作。左: foo 与 bar 相乘;长度可以均匀除尽。右: foo 与 baz 相乘;长度不能均匀除尽,并发出警告。
正如我在第 2.3.3 节中提到的,你可以将单个值视为长度为 1 的向量,因此你可以使用单个值对任何长度的向量执行操作。以下是一个例子,使用相同的向量 foo:
R> qux <- 3
R> foo+qux
[1] 8.5 7.5 6.5 5.5 4.5 3.5
这远比执行 foo+c(3,3,3,3,3,3) 或更一般的 foo+rep(x=3,times=length(x=foo)) 要容易。以这种方式操作向量,使用单个值是非常常见的,例如,如果你想通过某个常数对一组测量值进行重新缩放或平移。
面向向量的行为的另一个好处是,你可以使用矢量化函数完成潜在的繁重任务。例如,如果你想对一个数字向量中的所有元素求和或相乘,你只需要使用一个内置函数。
回想一下之前展示的 foo:
R> foo
[1] 5.5 4.5 3.5 2.5 1.5 0.5
你可以通过以下方式求得这六个元素的和:
R> sum(foo)
[1] 18
以及它们的乘积:
R> prod(foo)
[1] 162.4219
矢量化函数不仅方便,而且比显式编写的迭代方法(例如循环)更快、更高效。这些示例的主要启示是,R 的许多功能是专门为某些数据结构设计的,确保了代码的简洁性和性能的优化。
最后,如前所述,这种面向向量的行为同样适用于覆盖多个元素。再次使用 foo,请查看以下内容:
R> foo
[1] 5.5 4.5 3.5 2.5 1.5 0.5
R> foo[c(1,3,5,6)] <- c(-99,99)
R> foo
[1] -99.0 4.5 99.0 2.5 -99.0 99.0
你会看到四个特定元素被长度为 2 的向量覆盖,这些向量以你熟悉的方式重复。同样,替换向量的长度必须能均匀除尽要被覆盖的元素数量,否则当 R 无法完成一个完整长度的重复时,将发出类似之前的警告。
练习 2.5
-
将向量
c(2,0.5,1,2,0.5,1,2,0.5,1)转换为仅包含1的向量,使用长度为 3 的向量。 -
华氏度 F 到摄氏度 C 的转换是通过以下公式进行的:
![image]()
使用面向向量的行为,在 R 中将温度值 45、77、20、19、101、120 和 212 华氏度转换为摄氏度。
-
使用向量
c(2,4,6)和向量c(1,2),结合rep和*运算符,生成向量c(2,4,6,4,8,12)。 -
将结果向量(来自(c))中的中间四个元素,按顺序替换为两个回收的值
-0.1和-100。
本章的重要代码
| 函数/运算符 | 简要描述 | 首次出现 |
|---|---|---|
+, *, -, /, ^ |
算术运算 | 第 2.1 节, 第 17 页 |
sqrt |
平方根 | 第 2.1.1 节, 第 18 页 |
log |
对数 | 第 2.1.2 节, 第 19 页 |
exp |
指数 | 第 2.1.2 节, 第 19 页 |
<-,= |
对象赋值 | 第 2.2 节, 第 21 页 |
c |
向量创建 | 第 2.3.1 节, 第 23 页 |
:,seq |
序列创建 | 第 2.3.2 节, 第 24 页 |
rep |
值/向量重复 | 第 2.3.2 节, 第 25 页 |
sort |
向量排序 | 第 2.3.2 节, 第 26 页 |
length |
确定向量长度 | 第 2.3.2 节, 第 27 页 |
[ ] |
向量子集提取 | 第 2.3.3 节, 第 28 页 |
sum |
求和所有向量元素 | 第 2.3.4 节, 第 36 页 |
prod |
乘积所有向量元素 | 第 2.3.4 节, 第 36 页 |
第三章:3
矩阵与数组

到现在为止,你已经能够熟练使用 R 中的向量了。一个矩阵实际上是几个向量的集合。而向量的大小由其长度描述,矩阵的大小则通过行数和列数来指定。你还可以创建更高维度的结构,称为数组。在本章中,我们将首先了解如何处理矩阵,然后再增加维度形成数组。
3.1 定义矩阵
矩阵是一个重要的数学构造,并且是许多统计方法的基础。你通常将矩阵 A 描述为一个 m × n 矩阵;即 A 将有 m 行和 n 列。这意味着 A 将有总共 mn 个条目,每个条目 a[i,j] 都有一个独特的位置,由其特定的行(i = 1,2, ..., m)和列(j = 1, 2, ..., n)给出。
因此,你可以按以下方式表示矩阵:

要在 R 中创建一个矩阵,可以使用恰如其分的matrix命令,将矩阵的条目作为向量传递给data参数:
R> A <- matrix(data=c(-3,2,893,0.17),nrow=2,ncol=2)
R> A
[,1] [,2]
[1,] -3 893.00
[2,] 2 0.17
你必须确保这个向量的长度与所需的行数(nrow)和列数(ncol)完全匹配。你也可以选择在调用matrix时不提供nrow和ncol,在这种情况下,R 的默认行为是返回一个包含data条目的单列矩阵。例如,matrix(data=c(-3,2,893,0.17))与matrix(data=c(-3,2,893,0.17),nrow=4,ncol=1)是相同的。
3.1.1 填充方向
重要的是要了解 R 如何使用来自 data 的条目填充矩阵。通过查看前面的示例,你可以看到 2 × 2 矩阵 A 是按 列 填充的,即从左到右读取 data 条目。你可以使用 byrow 参数控制 R 填充数据的方式,如以下示例所示:
R> matrix(data=c(1,2,3,4,5,6),nrow=2,ncol=3,byrow=FALSE)
[,1] [,2] [,3]
[1,] 1 3 5
[2,] 2 4 6
在这里,我指示 R 提供一个包含数字 1 到 6 的 2 × 3 矩阵。通过使用可选参数byrow并将其设置为FALSE,你明确告诉 R 按列填充这个 2 × 3 结构,即在移动到下一列之前填充每一列,从左到右读取data参数向量。这是 R 对matrix函数的默认处理方式,因此如果没有提供byrow参数,软件将默认byrow=FALSE。图 3-1 展示了这一行为。

图 3-1:按列填充 2 × 3 矩阵,byrow=FALSE(R 默认)
现在,让我们重复相同的代码行,但将byrow=TRUE。
R> matrix(data=c(1,2,3,4,5,6),nrow=2,ncol=3,byrow=TRUE)
[,1] [,2] [,3]
[1,] 1 2 3
[2,] 4 5 6
结果得到的 2 × 3 结构现在已经按行填充,如图 3-2 所示。

图 3-2:按行填充 2 × 3 矩阵,byrow=TRUE
3.1.2 行和列绑定
如果你有多个相同长度的向量,可以通过使用内置的 R 函数rbind和cbind将这些向量快速地构建为一个矩阵。你可以将每个向量视为一行(使用命令rbind),或者将每个向量视为一列(使用命令cbind)。假设你有两个向量1:3和4:6。你可以通过以下方式使用rbind重建图 3-2 中的 2 × 3 矩阵:
R> rbind(1:3,4:6)
[,1] [,2] [,3]
[1,] 1 2 3
[2,] 4 5 6
在这里,rbind将向量作为矩阵的两行连接在一起,行的自上而下顺序与提供给rbind的向量顺序一致。可以使用cbind按以下方式构造相同的矩阵:
R> cbind(c(1,4),c(2,5),c(3,6))
[,1] [,2] [,3]
[1,] 1 2 3
[2,] 4 5 6
这里,你有三个长度为 2 的向量。你使用cbind按提供的顺序将这三个向量粘合在一起,每个向量成为结果矩阵的一列。
3.1.3 矩阵维度
另一个有用的函数dim可以提供存储在工作空间中的矩阵的维度。
R> mymat <- rbind(c(1,3,4),5:3,c(100,20,90),11:13)
R> mymat
[,1] [,2] [,3]
[1,] 1 3 4
[2,] 5 4 3
[3,] 100 20 90
[4,] 11 12 13
R> dim(mymat)
[1] 4 3
R> nrow(mymat)
[1] 4
R> ncol(mymat)
[1] 3
R> dim(mymat)[2]
[1] 3
定义了矩阵mymat之后,你可以使用dim确认其维度,dim会返回一个长度为 2 的向量;dim总是首先提供行数,然后是列数。你还可以使用两个相关的函数:nrow(只提供行数)和ncol(只提供列数)。在最后的命令中,你使用dim和向量子集的知识来提取与ncol相同的结果。
3.2 子集操作
从矩阵中提取和子集元素的过程与从向量中提取元素类似。唯一的复杂之处在于,现在你有了一个额外的维度。元素提取仍然使用方括号操作符,但现在必须同时提供行和列的位置,严格按照[row, column]的顺序。让我们从创建一个 3 × 3 的矩阵开始,我将使用它来展示本节中的示例。
R> A <- matrix(c(0.3,4.5,55.3,91,0.1,105.5,-4.2,8.2,27.9),nrow=3,ncol=3)
R> A
[,1] [,2] [,3]
[1,] 0.3 91.0 -4.2
[2,] 4.5 0.1 8.2
[3,] 55.3 105.5 27.9
要让 R“查看A的第三行并给我第二列的元素”,你可以执行以下操作:
R> A[3,2]
[1] 105.5
正如预期的那样,你得到了位于位置[3,2]的元素。
3.2.1 行、列和对角线提取
要从矩阵中提取整个行或列,你只需要指定所需的行或列编号,并将另一个值留空。需要注意的是,你仍然必须包括用来分隔行和列编号的逗号——这是 R 区分请求行和请求列的方式。以下代码返回矩阵A的第二列:
R> A[,2]
[1] 91.0 0.1 105.5
以下代码检查第一行:
R> A[1,]
[1] 0.3 91.0 -4.2
请注意,每当一个提取(或删除,稍后讨论)结果为单一值、单行或单列时,R 总是会返回一个独立的向量,其中包含所请求的值。你还可以执行更复杂的提取,例如请求整行或整列,或者多行或多列,结果将以新的矩阵形式返回,并具有适当的维度。考虑以下子集:
R> A[2:3,]
[,1] [,2] [,3]
[1,] 4.5 0.1 8.2
[2,] 55.3 105.5 27.9
R> A[,c(3,1)]
[,1] [,2]
[1,] -4.2 0.3
[2,] 8.2 4.5
[3,] 27.9 55.3
R> A[c(3,1),2:3]
[,1] [,2]
[1,] 105.5 27.9
[2,] 91.0 -4.2
第一条命令返回A的第二行和第三行,第二条命令返回A的第三列和第一列。最后一条命令访问A的第三行和第一行,并按顺序返回这两行的第二列和第三列元素。
你还可以使用diag命令来识别方阵(即行列数相等的矩阵)沿对角线的值。
R> diag(x=A)
[1] 0.3 0.1 27.9
这将返回一个向量,包含A沿对角线的元素,从A[1,1]开始。
3.2.2 省略与覆盖
要删除或省略矩阵中的元素,你再次使用方括号,但这次使用负索引。以下命令返回没有第二列的A:
R> A[,-2]
[,1] [,2]
[1,] 0.3 -4.2
[2,] 4.5 8.2
[3,] 55.3 27.9
以下命令从A中删除第一行,并按顺序获取剩余两行的第三列和第二列的值:
R> A[-1,3:2]
[,1] [,2]
[1,] 8.2 0.1
[2,] 27.9 105.5
以下命令返回没有第一行和第二列的A:
R> A[-1,-2]
[,1] [,2]
[1,] 4.5 8.2
[2,] 55.3 27.9
最后,这条命令删除第一行,然后删除结果中的第二列和第三列:
R> A[-1,-c(2,3)]
[1] 4.5 55.3
请注意,这个最终操作只留下了A第一列的最后两个元素,因此结果作为独立向量而不是矩阵返回。
要覆盖特定元素或整个行或列,你需要识别要替换的元素,然后分配新的值,正如你在第 2.3.3 节中对向量所做的那样。新的元素可以是一个单一值,一个长度与要替换元素数量相同的向量,或一个长度能够整除要替换元素数量的向量。为了说明这一点,让我们首先创建A的副本,并将其命名为B。
R> B <- A
R> B
[,1] [,2] [,3]
[1,] 0.3 91.0 -4.2
[2,] 4.5 0.1 8.2
[3,] 55.3 105.5 27.9
以下命令用序列1、2和3覆盖B的第二行:
R> B[2,] <- 1:3
R> B
[,1] [,2] [,3]
[1,] 0.3 91.0 -4.2
[2,] 1.0 2.0 3.0
[3,] 55.3 105.5 27.9
以下命令用900覆盖第一行和第三行的第二列元素:
R> B[c(1,3),2] <- 900
R> B
[,1] [,2] [,3]
[1,] 0.3 900 -4.2
[2,] 1.0 2 3.0
[3,] 55.3 900 27.9
接下来,你用B的第三行的值替换B的第三列。
R> B[,3] <- B[3,]
R> B
[,1] [,2] [,3]
[1,] 0.3 900 55.3
[2,] 1.0 2 900.0
[3,] 55.3 900 27.9
为了尝试 R 的向量回收,我们现在将第一行和第三行的第一列和第三列元素(共四个元素)用两个值-7和7覆盖。
R> B[c(1,3),c(1,3)] <- c(-7,7)
R> B
[,1] [,2] [,3]
[1,] -7 900 -7
[2,] 1 2 900
[3,] 7 900 7
长度为 2 的向量以按列的方式替换了这四个元素。替换向量c(-7,7)按顺序覆盖了位置(1,1)和(3,1)的元素,然后重复该过程,按顺序覆盖(1,3)和(3,3)的元素。
为了突出索引顺序对矩阵元素替换的影响,请考虑以下示例:
R> B[c(1,3),2:1] <- c(65,-65,88,-88)
R> B
[,1] [,2] [,3]
[1,] 88 65 -7
[2,] 1 2 900
[3,] -88 -65 7
替换向量中的四个值已按列的顺序覆盖了四个指定的元素。在这种情况下,由于我反向指定了第一列和第二列,因此覆盖操作依次进行,先填充第二列,再转到第一列。位置(1,2)对应65,然后(3,2)对应-65;接着(1,1)变成88,(3,1)变成-88。
如果你只想替换一个方阵的对角线元素,你可以避免显式索引,直接使用diag命令来覆盖值。
R> diag(x=B) <- rep(x=0,times=3)
R> B
[,1] [,2] [,3]
[1,] 0 65 -7
[2,] 1 0 900
[3,] -88 -65 0
练习 3.1
-
构造并存储一个 4 × 2 的矩阵,按行顺序填充值 4.3、3.1、8.2、8.2、3.2、0.9、1.6 和 6.5。
-
如果从(a)中删除任意一行,确认矩阵的维度是 3 × 2。
-
用按从小到大的顺序排序的相同列,覆盖矩阵(a)中的第二列。
-
如果从(c)中删除第四行和第一列,R 会返回什么?使用
matrix确保结果是一个单列矩阵,而不是一个向量。 -
将(c)中的底部四个元素存储为新的 2 × 2 矩阵。
-
按此顺序,覆盖(c)中位置(4,2)、(1,2)、(4,1)和(1,1)的元素,使用矩阵(e)对角线上的两个值的–
。
3.3 矩阵运算与代数
你可以从两个角度理解 R 中的矩阵。首先,你可以纯粹将这些结构作为编程中的计算工具,用来存储和操作结果,正如你迄今所见。其次,你可以利用矩阵在相关计算中的数学性质,例如使用矩阵乘法表示回归模型方程。这个区别很重要,因为矩阵的数学行为并不总是与更通用的数据处理行为相同。在这里,我将简要介绍一些特殊的矩阵,以及一些涉及矩阵的最常见数学运算和 R 中的相应功能。如果矩阵的数学行为对你不感兴趣,你可以暂时跳过这一部分,稍后根据需要再参考。
3.3.1 矩阵转置
对于任何m × n矩阵A,它的转置,A^⊤,是通过将其列写成行或将其行写成列得到的n × m矩阵。
这是一个例子:

在R中,矩阵的转置可以通过t函数找到。让我们创建一个新矩阵,然后对其进行转置。
R> A <- rbind(c(2,5,2),c(6,1,4))
R> A
[,1] [,2] [,3]
[1,] 2 5 2
[2,] 6 1 4
R> t(A)
[,1] [,2]
[1,] 2 6
[2,] 5 1
[3,] 2 4
如果你对* A *进行“转置再转置”,你将恢复原始矩阵。
R> t(t(A))
[,1] [,2] [,3]
[1,] 2 5 2
[2,] 6 1 4
3.3.2 单位矩阵
单位矩阵写作I[m],是数学中一种特殊类型的矩阵。它是一个m × m的方阵,对角线上的元素为 1,其他位置为 0。
这是一个例子:

你可以使用标准的 matrix 函数创建任意维度的单位矩阵,但有一种更快捷的方法可以使用 diag。之前,我在一个现有的矩阵上使用了 diag 来提取或覆盖它的对角元素。你也可以按以下方式使用它:
R> A <- diag(x=3)
R> A
[,1] [,2] [,3]
[1,] 1 0 0
[2,] 0 1 0
[3,] 0 0 1
在这里你可以看到 diag 可以用来轻松生成单位矩阵。为进一步说明,diag 的行为取决于你传递给它的参数 x。如果像之前那样,x 是一个矩阵,diag 会提取该矩阵的对角元素。如果 x 是一个正整数,就像这里的情况,diag 会生成对应维度的单位矩阵。你可以在 diag 的帮助页面找到更多的用法。
3.3.3 矩阵的标量倍数
标量值只是一个单一的、一维的值。任何矩阵 A 与标量值 a 相乘,都会得到一个新的矩阵,其中每个元素都被 a 乘以。
这里有一个例子:

R 会以逐元素的方式执行此乘法,正如你所期望的那样。矩阵的标量乘法是通过标准的算术 * 运算符来执行的。
R> A <- rbind(c(2,5,2),c(6,1,4))
R> a <- 2
R> a*A
[,1] [,2] [,3]
[1,] 4 10 4
[2,] 12 2 8
3.3.4 矩阵加法和减法
两个相同大小矩阵的加法或减法也是逐元素进行的。对应的元素会根据操作进行加法或减法。
这里有一个例子:

你可以使用标准的 + 和 - 符号来加减任何两个相同大小的矩阵。
R> A <- cbind(c(2,5,2),c(6,1,4))
R> A
[,1] [,2]
[1,] 2 6
[2,] 5 1
[3,] 2 4
R> B <- cbind(c(-2,3,6),c(8.1,8.2,-9.8))
R> B
[,1] [,2]
[1,] -2 8.1
[2,] 3 8.2
[3,] 6 -9.8
R> A-B
[,1] [,2]
[1,] 4 -2.1
[2,] 2 -7.2
[3,] -4 13.8
3.3.5 矩阵乘法
为了乘两个矩阵 A 和 B,它们的大小分别为 m × n 和 p × q,必须满足 n = p。结果矩阵 A · B 的大小将是 m × q。乘积的元素是按行乘列的方式计算的,其中位置 (AB)[i,j] 的值是通过将 A 的第 i 行的每个元素与 B 的第 j 列的每个元素进行逐元素相乘并求和得到的。
这里有一个例子:

请注意,通常情况下,适当大小的矩阵(例如,C 和 D)的乘法不是交换的;也就是说,CD ≠ DC。
与加法、减法和标量乘法不同,矩阵乘法不是简单的逐元素计算,标准的 * 运算符不能使用。相反,你必须使用 R 的矩阵乘积运算符,它是由百分号符号 %*% 表示的。在你尝试此运算符之前,让我们先存储这两个示例矩阵,并使用 dim 检查确保第一个矩阵的列数与第二个矩阵的行数匹配。
R> A <- rbind(c(2,5,2),c(6,1,4))
R> dim(A)
[1] 2 3
R> B <- cbind(c(3,-1,1),c(-3,1,5))
R> dim(B)
[1] 3 2
这证明了这两个矩阵可以进行相乘,因此你可以继续操作。
R> A%*%B
[,1] [,2]
[1,] 3 9
[2,] 21 3
你可以通过使用相同的两个矩阵来证明矩阵乘法是不可交换的。改变乘法的顺序会得到完全不同的结果。
R> B%*%A
[,1] [,2] [,3]
[1,] -12 12 -6
[2,] 4 -4 2
[3,] 32 10 22
3.3.6 矩阵求逆
一些方阵是可以逆转的。矩阵 A 的逆矩阵用 A^(–1) 表示。一个可逆矩阵满足以下方程:
AA^(–1) = I[m]
这是一个矩阵及其逆矩阵的示例:

不可逆的矩阵被称为奇异矩阵。当使用矩阵解方程时,求矩阵的逆是常常需要的,并且具有重要的实际意义。矩阵求逆有多种方法,随着矩阵大小的增大,这些计算会变得非常耗费计算资源。我们在这里不会深入讨论,但如果你感兴趣,可以参考 Golub 和 Van Loan(1989)的正式讨论。
现在,我将向你展示 R 语言中的 solve 函数作为求矩阵逆的一个选项。
R> A <- matrix(data=c(3,4,1,2),nrow=2,ncol=2)
R> A
[,1] [,2]
[1,] 3 1
[2,] 4 2
R> solve(A)
[,1] [,2]
[1,] 1 -0.5
[2,] -2 1.5
你也可以验证这两个矩阵的乘积(使用矩阵乘法规则)会得到一个 2 × 2 的单位矩阵。
R> A%*%solve(A)
[,1] [,2]
[1,] 1 0
[2,] 0 1
练习 3.2
-
计算如下内容:
![image]()
-
存储这两个矩阵:
![image]()
以下哪些乘法是可能的?对于那些可以的,计算结果。
-
A · B
-
A ^⊤ · B
-
B ^⊤ · (A · A ^⊤)
-
(A · A ^⊤) · B ^⊤
-
[(B · B ^⊤) + (A · A ^⊤) − 100I[3]]^(−1)
-
-
对于
![image]()
验证 A^(–1) · A − I[4] 结果是一个 4 × 4 的零矩阵。
3.4 多维数组
就像矩阵(一个“矩形”元素集合)是通过增加向量(一个“线”元素集合)的维度得到的结果一样,矩阵的维度也可以增加,从而得到更复杂的数据结构。在 R 中,向量和矩阵可以被认为是更一般的数组的特殊情况,当它们具有超过两个维度时,我将用这个术语来指代这些结构。
那么,矩阵的下一步是什么呢?就像矩阵被认为是相同长度的向量集合一样,三维数组可以被认为是等维度矩阵的集合,从而为你提供一个元素的长方体。你仍然有固定数量的行和列,以及一个新的第三维度,称为层。图 3-3 展示了一个三行四列两层(3 × 4 × 2)的数组。

图 3-3:一个 3 × 4 × 2 数组的概念图。每个元素的索引在相应位置给出。这些索引是严格按照 [行,列,层]的顺序提供的。*
3.4.1 定义
在 R 中创建这些数据结构时,使用 array 函数,并将 data 参数中的元素指定为一个向量。然后,在 dim 参数中指定大小,作为一个向量,其长度对应于维度的数量。注意,array 按列的顺序严格地填充每一层的条目,从第一层开始。考虑以下示例:
R> AR <- array(data=1:24,dim=c(3,4,2))
R> AR
, , 1
[,1] [,2] [,3] [,4]
[1,] 1 4 7 10
[2,] 2 5 8 11
[3,] 3 6 9 12
, , 2
[,1] [,2] [,3] [,4]
[1,] 13 16 19 22
[2,] 14 17 20 23
[3,] 15 18 21 24
这会给你一个和图 3-3 一样大小的数组——每个层构成一个 3 × 4 的矩阵。在这个例子中,注意提供给dim的维度顺序:c(行,列,层)。就像单个矩阵一样,数组的维度大小乘积将得出元素的总数。随着维度的增加,dim向量必须相应地扩展。例如,四维数组是下一个层次,可以看作是三维数组的块。假设你有一个四维数组,由三个AR(即刚才定义的三维数组)组成。这个新数组可以如下方式在 R 中存储(同样,数组按列填充):
R> BR <- array(data=rep(1:24,times=3),dim=c(3,4,2,3))
R> BR
, , 1, 1
[,1] [,2] [,3] [,4]
[1,] 1 4 7 10
[2,] 2 5 8 11
[3,] 3 6 9 12
, , 2, 1
[,1] [,2] [,3] [,4]
[1,] 13 16 19 22
[2,] 14 17 20 23
[3,] 15 18 21 24
, , 1, 2
[,1] [,2] [,3] [,4]
[1,] 1 4 7 10
[2,] 2 5 8 11
[3,] 3 6 9 12
, , 2, 2
[,1] [,2] [,3] [,4]
[1,] 13 16 19 22
[2,] 14 17 20 23
[3,] 15 18 21 24
, , 1, 3
[,1] [,2] [,3] [,4]
[1,] 1 4 7 10
[2,] 2 5 8 11
[3,] 3 6 9 12
, , 2, 3
[,1] [,2] [,3] [,4]
[1,] 13 16 19 22
[2,] 14 17 20 23
[3,] 15 18 21 24
使用BR,你现在拥有三个AR的副本。这些副本被拆分为两个层,以便 R 能够将对象打印到屏幕上。和之前一样,行由第一位数字索引,列由第二位数字索引,层由第三位数字索引。新的第四位数字索引块。
3.4.2 子集、提取和替换
尽管高维对象可能难以概念化,但 R 会始终如一地对它们进行索引。这使得提取这些结构中的元素变得直接简单,因为你已经学会了如何对子集矩阵进行操作——你只需继续在方括号中使用逗号作为维度之间的分隔符。接下来的例子会强调这一点。
假设你想要获取之前创建的数组AR的第二层的第二行。你只需在方括号中输入AR的这些精确维度位置。
R> AR[2,,2]
[1] 14 17 20 23
所需的元素已被提取为一个长度为 4 的向量。如果你想从这个向量中获取特定的元素,比如第三个和第一个(按这个顺序),你可以调用以下代码:
R> AR[2,c(3,1),2]
[1] 20 14
再次,这种字面量的子集方法使得处理 R 中的高维对象变得可控。
产生多个向量的提取会在返回的矩阵中以列的形式呈现。例如,要提取AR的两个层的第一行,你可以输入如下代码:
R> AR[1,,]
[,1] [,2]
[1,] 1 13
[2,] 4 16
[3,] 7 19
[4,] 10 22
返回的对象包含了每个矩阵层的第一行。然而,它返回的这些向量被作为单个返回矩阵的列。正如这个例子所示,当从数组中提取多个向量时,它们会默认作为列返回。这意味着提取的行不一定会作为行返回。
转到对象BR,下面的代码会给你三维数组中位于第三块的矩阵第一层的第二行第一列的单一元素。
R> BR[2,1,1,3]
[1] 2
同样,你只需查看方括号中索引的位置,就能知道你要求 R 从数组中返回哪些值。以下示例强调了这一点:
R> BR[1,,,1]
[,1] [,2]
[1,] 1 13
[2,] 4 16
[3,] 7 19
[4,] 10 22
这将返回第一个块中第一行的所有值。由于我在这个子集[1,,,1]中没有指定列和层的索引,这个命令返回了BR块中所有四列和两个层的值。
接下来,以下代码将返回BR数组第二层中的所有值,该层由三个矩阵组成:
R> BR[,,2,]
, , 1
[,1] [,2] [,3] [,4]
[1,] 13 16 19 22
[2,] 14 17 20 23
[3,] 15 18 21 24
, , 2
[,1] [,2] [,3] [,4]
[1,] 13 16 19 22
[2,] 14 17 20 23
[3,] 15 18 21 24
, , 3
[,1] [,2] [,3] [,4]
[1,] 13 16 19 22
[2,] 14 17 20 23
[3,] 15 18 21 24
最后的这个例子突出了前面提到的一个特性,其中从AR提取的多个向量作为矩阵返回。一般来说,如果你进行提取操作并得到多个d维数组,结果将会是下一个维度(d + 1)的数组。在最后的例子中,你提取了多个(二维)矩阵,它们作为三维数组返回。这个特性在接下来的例子中再次得到展示:
R> BR[3:2,4,,]
, , 1
[,1] [,2]
[1,] 12 24
[2,] 11 23
, , 2
[,1] [,2]
[1,] 12 24
[2,] 11 23
, , 3
[,1] [,2]
[1,] 12 24
[2,] 11 23
这将提取所有层和所有数组块中,第三行和第二行(按顺序),第四列的元素。考虑下面的最后一个例子:
R> BR[2,,1,]
[,1] [,2] [,3]
[1,] 2 2 2
[2,] 5 5 5
[3,] 8 8 8
[4,] 11 11 11
在这里,你要求 R 返回存储在BR中的所有数组第一层的完整第二行。
删除和覆盖高维数组中的元素遵循与独立向量和矩阵相同的规则。你通过负索引(用于删除)或使用赋值运算符(用于覆盖)来指定维度位置。
你可以使用array函数来创建一维数组(向量)和二维数组(矩阵),如果需要的话(通过设置dim参数的长度为 1 或 2)。不过需要注意的是,尤其是向量,在使用array而不是c创建时,某些函数可能会以不同的方式对待它们(有关技术细节,请参见帮助文件?array)。因此,为了使大段代码更具可读性,在 R 编程中通常更倾向于使用特定的向量和矩阵创建函数c和matrix。
练习 3.3
-
创建并存储一个三维数组,包含六层 4 × 2 矩阵,矩阵中的值是从 4.8 到 0.1 的递减序列,长度适当。
-
提取并存储第二列中所有层的第四行和第一行元素,顺序为:第四行,第一行,作为一个新对象。
-
使用矩阵(b)中第二行的四次重复,填充一个新的 2 × 2 × 2 × 3 维数组。
-
创建一个新数组,包含删除(a)第六层后的结果。
-
用−99 覆盖(d)中第 1 层、第 3 层和第 5 层的第二列的第二行和第四行元素。
本章中的重要代码
| 函数/运算符 | 简要描述 | 首次出现 |
|---|---|---|
matrix |
创建一个矩阵 | 第 3.1 节,第 40 页 |
rbind |
创建一个矩阵(按行绑定) | 第 3.1.2 节,第 41 页 |
cbind |
创建一个矩阵(按列绑定) | 第 3.1.2 节,第 42 页 |
dim |
获取矩阵维度 | 第 3.1.3 节, 第 42 页 |
nrow |
获取行数 | 第 3.1.3 节, 第 42 页 |
ncol |
获取列数 | 第 3.1.3 节, 第 42 页 |
[ , ] |
矩阵/数组子集提取 | 第 3.2 节, 第 43 页 |
diag |
对角线元素/单位矩阵 | 第 3.2.1 节, 第 44 页 |
t |
矩阵转置 | 第 3.3.1 节, 第 47 页 |
* |
标量矩阵乘法 | 第 3.3.3 节, 第 49 页 |
+, - |
矩阵加法/减法 | 第 3.3.4 节, 第 49 页 |
%*% |
矩阵乘法 | 第 3.3.5 节, 第 50 页 |
solve |
矩阵求逆 | 第 3.3.6 节, 第 51 页 |
array |
创建数组 | 第 3.4.1 节, 第 53 页 |
第四章:4
非数值类型

到目前为止,你几乎只在处理数值类型。但统计编程也需要非数值类型。在本章中,我们将考虑三种重要的非数值数据类型:逻辑值、字符和因子。这些数据类型在有效使用 R 时起着重要作用,特别是当我们进入第二部分的更复杂的 R 编程时。
4.1 逻辑值
逻辑值(也叫做逻辑型)基于一个简单的前提:逻辑值对象只能是TRUE或FALSE。这些可以解释为是/否、1/0、满足/不满足,等等。这是一个出现在所有编程语言中的概念,逻辑值有许多重要的用途。通常,它们用来表示某个条件是否已满足,或者某个参数是否应该开启或关闭。
你在使用第 2.3.2 节中的sort函数和第 3.1 节中的matrix函数时,简要地接触过逻辑值。在使用sort时,设置decreasing=TRUE返回一个从大到小排序的向量,decreasing=FALSE则将向量按相反顺序排序。类似地,在构造矩阵时,byrow=TRUE按行填充矩阵条目;否则,矩阵按列填充。现在,我们将更详细地探讨如何使用逻辑值。
4.1.1 TRUE 还是 FALSE?
R 中的逻辑值完全写作TRUE和FALSE,但通常缩写为T或F。缩写版对代码执行没有影响,因此,例如,使用decreasing=T与在sort函数中使用decreasing=TRUE等效。(但是,如果你想利用这种便捷性,千万不要创建名为T或F的对象——参见第 9.1.3 节)。
将逻辑值赋给一个对象与赋给数值相同。
R> foo <- TRUE
R> foo
[1] TRUE
R> bar <- F
R> bar
[1] FALSE
这将给你一个值为TRUE的对象和一个值为FALSE的对象。类似地,向量也可以用逻辑值填充。
R> baz <- c(T,F,F,F,T,F,T,T,T,F,T,F)
R> baz
[1] TRUE FALSE FALSE FALSE TRUE FALSE TRUE TRUE TRUE FALSE TRUE FALSE
R> length(x=baz)
[1] 12
矩阵(和其他高维数组)也可以用这些值创建。使用之前的foo和baz,你可以构造类似这样的内容:
R> qux <- matrix(data=baz,nrow=3,ncol=4,byrow=foo)
R> qux
[,1] [,2] [,3] [,4]
[1,] TRUE FALSE FALSE FALSE
[2,] TRUE FALSE TRUE TRUE
[3,] TRUE FALSE TRUE FALSE
4.1.2 逻辑结果:关系运算符
逻辑值通常用于检查值之间的关系。例如,你可能想知道某个数字a是否大于预定义的阈值b。为此,你可以使用表 4-1 中显示的标准关系运算符,它们的结果是逻辑值。
表 4-1: 关系运算符
| 运算符 | 解释 |
|---|---|
== |
等于 |
!= |
不等于 |
> |
大于 |
< |
小于 |
>= |
大于或等于 |
<= |
小于或等于 |
通常,这些操作符用于数值型数据(尽管你将在第 4.2.1 节中查看其他可能性)。这里是一个例子:
R> 1==2
[1] FALSE
R> 1>2
[1] FALSE
R> (2-1)<=2
[1] TRUE
R> 1!=(2+3)
[1] TRUE
结果应该不令人惊讶:1 等于 2 是 FALSE,而 1 大于 2 也是 FALSE,但 2-1 小于或等于 2 的结果是 TRUE,同样,1 不等于 5(2+3)也是 TRUE。这种类型的操作在处理某些方式可变的数字时更加有用,正如你稍后会看到的那样。
当你使用向量时,R 的按元素行为你已经很熟悉。当使用关系运算符时,规则相同。为了说明这一点,让我们先创建两个向量,并再次检查它们的长度是否相等。
R> foo <- c(3,2,1,4,1,2,1,-1,0,3)
R> bar <- c(4,1,2,1,1,0,0,3,0,4)
R> length(x=foo)==length(x=bar)
[1] TRUE
现在考虑以下四个评估:
R> foo==bar
[1] FALSE FALSE FALSE FALSE TRUE FALSE FALSE FALSE TRUE FALSE
R> foo<bar
[1] TRUE FALSE TRUE FALSE FALSE FALSE FALSE TRUE FALSE TRUE
R> foo<=bar
[1] TRUE FALSE TRUE FALSE TRUE FALSE FALSE TRUE TRUE TRUE
R> foo<=(bar+10)
[1] TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE
第一行检查 foo 中的条目是否等于 bar 中的对应条目,只有第 5 和第 9 个条目为 TRUE。返回的向量将包含每对元素的逻辑结果,因此它将与正在比较的向量长度相同。第二行以相同的方式比较 foo 和 bar,这次检查 foo 中的条目是否小于 bar 中的条目。与第三个比较结果对比,第三个比较检查条目是否小于或等于彼此。最后,第四行检查 foo 的成员是否小于或等于 bar,当 bar 的元素增加 10 时。自然地,结果都是 TRUE。
向量回收也适用于逻辑值。我们使用之前的 foo,以及一个较短的向量 baz。
R> baz <- foo[c(10,3)]
R> baz
[1] 3 1
这里你创建了 baz,它是一个长度为 2 的向量,包含了 foo 的第 10 和第 3 个元素。现在考虑以下内容:
R> foo>baz
[1] FALSE TRUE FALSE TRUE FALSE TRUE FALSE FALSE FALSE TRUE
在这里,baz 的两个元素会被回收,并与 foo 的 10 个元素进行比较。foo 的第 1 和第 2 个元素与 baz 的第 1 和第 2 个元素进行比较,foo 的第 3 和第 4 个元素与 baz 的第 1 和第 2 个元素进行比较,依此类推。你也可以将向量的所有值与单一值进行比较。这里是一个例子:
R> foo<3
[1] FALSE TRUE TRUE FALSE TRUE TRUE TRUE TRUE TRUE FALSE
这是在 R 中处理数据集时的典型操作。
现在让我们将 foo 和 bar 的内容重写为一个 5 × 2 的列填充矩阵。
R> foo.mat <- matrix(foo,nrow=5,ncol=2)
R> foo.mat
[,1] [,2]
[1,] 3 2
[2,] 2 1
[3,] 1 -1
[4,] 4 0
[5,] 1 3
R> bar.mat <- matrix(bar,nrow=5,ncol=2)
R> bar.mat
[,1] [,2]
[1,] 4 0
[2,] 1 0
[3,] 2 3
[4,] 1 0
[5,] 1 4
这里同样应用按元素行为;如果你比较矩阵,你将得到一个大小相同、填充了逻辑值的矩阵。
R> foo.mat<=bar.mat
[,1] [,2]
[1,] TRUE FALSE
[2,] FALSE FALSE
[3,] TRUE TRUE
[4,] FALSE TRUE
[5,] TRUE TRUE
R> foo.mat<3
[,1] [,2]
[1,] FALSE TRUE
[2,] TRUE TRUE
[3,] TRUE TRUE
[4,] FALSE TRUE
[5,] TRUE FALSE
这种评估也适用于多维数组。
你可以使用两个有用的函数来快速检查逻辑值的集合:any 和 all。在检查一个向量时,如果向量中的任何一个逻辑值为 TRUE,则 any 返回 TRUE,否则返回 FALSE。函数 all 只有在所有逻辑值都为 TRUE 时才返回 TRUE,否则返回 FALSE。作为一个快速示例,我们来处理在本节开头通过比较 foo 和 bar 形成的两个逻辑向量。
R> qux <- foo==bar
R> qux
[1] FALSE FALSE FALSE FALSE TRUE FALSE FALSE FALSE TRUE FALSE
R> any(qux)
[1] TRUE
R> all(qux)
[1] FALSE
在这里,qux 包含两个 TRUE,其余为 FALSE——因此,any 的结果自然是 TRUE,但 all 的结果是 FALSE。按照相同的规则,你会得到以下结果:
R> quux <- foo<=(bar+10)
R> quux
[1] TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE
R> any(quux)
[1] TRUE
R> all(quux)
[1] TRUE
any 和 all 函数对逻辑值的矩阵和数组执行相同的操作。
练习 4.1
-
将以下包含 15 个值的向量存储为工作空间中的一个对象:
c(6,9,7,3,6,7,9,6,3,6,6,7,1,9,1)。识别以下元素:-
这些等于 6 的元素
-
这些大于或等于 6 的元素
-
这些小于 6 + 2 的元素
-
这些不等于 6 的元素
-
-
从(a)中使用的向量创建一个新向量,通过删除其前 3 个元素。使用这个新向量,填充一个 2 × 2 × 3 的数组。检查数组中的以下条目:
-
这些小于或等于 6 除以 2 后加 4 的元素
-
这些小于或等于 6 除以 2 后加 4 的元素,在将数组中的每个元素增加 2 后
-
-
确认在 10 × 10 单位矩阵 I[10] 中等于 0 的元素的具体位置(参见 第 3.3 节)。
-
检查在(b)中创建的逻辑数组中的任何值是否为
TRUE。如果是,检查它们是否全部为TRUE。 -
通过提取在(c)中创建的逻辑矩阵的对角元素,使用
any来确认没有TRUE条目。
4.1.3 多重比较:逻辑运算符
逻辑值特别有用,当你想检查多个条件是否都满足时。通常,你希望只有在满足多个不同条件的情况下执行某些操作。
上一节介绍了关系运算符,用于比较存储的 R 对象的字面值(即数值或其他)。现在,你将学习 逻辑运算符,它们用于比较两个 TRUE 或 FALSE 对象。这些运算符基于 AND 和 OR 语句。表 4-2 总结了 R 语法和逻辑运算符的行为。AND 和 OR 运算符各自有“单一”和“逐元素”版本——稍后你会看到它们的不同。
表 4-2: 比较两个逻辑值的逻辑运算符
| 运算符 | 解释 | 结果 |
|---|
| & | 与(逐元素) | TRUE & TRUE 为 TRUE,TRUE & FALSE 为 FALSE |
FALSE & TRUE 为 FALSE
FALSE & FALSE 为 FALSE |
&& |
与(单一比较) | 与上面的 & 相同 |
|---|
| | | 或(逐元素) | TRUE|TRUE 为 TRUE,TRUE|FALSE 为 TRUE |
FALSE|TRUE 为 TRUE
FALSE|FALSE 为 FALSE |
|| |
或(单一比较) | 与上面的 | 相同 |
|---|---|---|
! |
非 | !TRUE 为 FALSE,!FALSE 为 TRUE |
使用任何逻辑运算符的结果都是一个逻辑值。只有当两个逻辑值都为 TRUE 时,AND 比较才为真。当 OR 比较时,只要至少有一个逻辑值为 TRUE,结果就为真。NOT 运算符(!)简单地返回它所作用的逻辑值的相反值。你可以将这些运算符组合起来,一次检查多个条件。
R> FALSE||((T&&TRUE)||FALSE)
[1] TRUE
R> !TRUE&&TRUE
[1] FALSE
R> (T&&(TRUE||F))&&FALSE
[1] FALSE
R> (6<4)||(3!=1)
[1] TRUE
与数字算术一样,R 中的逻辑运算也有优先级。AND 语句的优先级高于 OR 语句。将每对比较项放入括号中有助于保留正确的运算顺序,并使代码更具可读性。你可以在这段代码的第一行看到这一点,其中最内层的比较是首先执行的:T&&TRUE的结果是TRUE;然后它作为下一个括号比较中的逻辑值之一参与计算,其中TRUE||FALSE的结果是TRUE。最后的比较是FALSE||TRUE,结果为TRUE,并打印到控制台。第二行表示 NOT TRUE AND TRUE,结果当然是FALSE。在第三行,再次是最内层的配对首先被计算:TRUE||F为TRUE;T&&TRUE为TRUE;最后TRUE&&FALSE为FALSE。第四个也是最后一个例子比较了两个括号中的条件,然后使用逻辑运算符进行比较。由于6<4为FALSE,而3!=1为TRUE,所以得到逻辑比较FALSE||TRUE,最终结果是TRUE。
在表 4-2 中,有 AND 和 OR 运算符的简短版本(&,|)和长版本(&&,||)。简短版本用于逐元素比较,其中你有两个逻辑向量,并且希望得到多个逻辑值作为结果。长版本,正如你到目前为止所使用的,是用于比较两个单独的值并返回单一逻辑值的。当你在 R 中编写条件判断语句时(在第十章中会介绍),这是非常重要的。使用简短版本也可以比较一对逻辑值,尽管当只需要一个TRUE或FALSE结果时,通常推荐使用长版本。
让我们来看一些逐元素比较的例子。假设你有两个等长的向量,foo 和 bar:
R> foo <- c(T,F,F,F,T,F,T,T,T,F,T,F)
R> foo
[1] TRUE FALSE FALSE FALSE TRUE FALSE TRUE TRUE TRUE FALSE TRUE FALSE
和
R> bar <- c(F,T,F,T,F,F,F,F,T,T,T,T)
R> bar
[1] FALSE TRUE FALSE TRUE FALSE FALSE FALSE FALSE TRUE TRUE TRUE TRUE
逻辑运算符的简短版本通过位置匹配每对元素,并返回比较的结果。
R> foo&bar
[1] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE TRUE FALSE TRUE FALSE
R> foo|bar
[1] TRUE TRUE FALSE TRUE TRUE FALSE TRUE TRUE TRUE TRUE TRUE TRUE
使用运算符的长版本意味着 R 只对两个向量中的第一个逻辑对进行比较。
R> foo&&bar
[1] FALSE
R> foo||bar
[1] TRUE
注意,最后两个结果与使用逻辑运算符简短版本得到的向量的第一个条目相匹配。
练习 4.2
-
将向量
c(7,1,7,10,5,9,10,3,10,8)存储为foo。识别大于 5 或等于 2 的元素。 -
将向量
c(8,8,4,4,5,1,5,6,6,8)存储为bar。识别小于或等于 6 并且不等于 4 的元素。 -
在
foo中识别满足(a)并且在bar中满足(b)的元素。 -
存储一个名为
baz的第三个向量,该向量等于foo和bar的逐元素和。确定以下内容:-
baz中大于或等于 14 但不等于 15 的元素。 -
通过对
baz除以foo进行逐元素除法得到的向量,其中元素大于 4 或小于等于 2
-
-
确认在之前的所有练习中使用长版本时,只执行了第一个比较(即,结果与之前获得的向量的第一个条目相匹配)。
4.1.4 逻辑值就是数字!
由于逻辑值的二元性质,它们通常用TRUE表示为 1,FALSE表示为 0。实际上,在 R 中,如果对逻辑值进行基本的数值运算,TRUE会被当作1,而FALSE会被当作0。
R> TRUE+TRUE
[1] 2
R> FALSE-TRUE
[1] -1
R> T+T+F+T+F+F+T
[1] 4
这些操作的结果与直接使用数字 1 和 0 相同。在某些情况下,当你使用逻辑值时,可以用数字值来替代。
R> 1&&1
[1] TRUE
R> 1||0
[1] TRUE
R> 0&&1
[1] FALSE
能够将逻辑值解释为零和一,意味着你可以使用多种函数来总结逻辑向量,你将在第三部分中进一步探索这个问题。
4.1.5 逻辑子集化和提取
逻辑值也可以用于提取和子集化向量及其他对象中的元素,方式与之前使用索引向量相同。你可以用逻辑标志向量来替代显式索引,当标志向量中相应位置的值为TRUE时,该元素就会被提取。因此,逻辑标志向量应该与正在访问的向量具有相同的长度(不过,对于较短的标志向量,R 会进行回收,如后面的示例所示)。
在第 2.3.3 节的开头,你定义了一个长度为 10 的向量,如下所示:
R> myvec <- c(5,-2.3,4,4,4,6,8,10,40221,-8)
如果你想提取两个负数元素,可以输入myvec[c(2,10)],或者你也可以使用逻辑标志来做如下操作:
R> myvec[c(F,T,F,F,F,F,F,F,F,T)]
[1] -2.3 -8.0
这个特定的例子可能看起来过于繁琐,难以在实际中使用。然而,当你想要基于某个条件(或多个条件)提取元素时,它会变得非常有用。例如,你可以轻松地使用逻辑值通过应用条件<0来找到myvec中的负数元素。
R> myvec<0
[1] FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE TRUE
这是一个完全有效的标志向量,你可以使用它对子集化myvec,并得到与之前相同的结果。
R> myvec[myvec<0]
[1] -2.3 -8.0
如前所述,如果标志向量太短,R 会对其进行回收。要从myvec中提取每隔一个元素,从第一个元素开始,你可以输入以下内容:
R> myvec[c(T,F)]
[1] 5 4 4 8 40221
你可以使用关系运算符和逻辑运算符进行更复杂的提取,例如:
R> myvec[(myvec>0)&(myvec<1000)]
[1] 5 4 4 4 6 8 10
这会返回小于 1,000 的正数元素。你还可以像使用索引向量一样,使用逻辑标志向量来覆盖特定元素。
R> myvec[myvec<0] <- -200
R> myvec
[1] 5 -200 4 4 4 6 8 10 40221 -200
这会将所有现有的负值条目替换为−200。请注意,你不能直接使用负逻辑标志向量来删除特定的元素;只能使用数字索引向量来做到这一点。
如你所见,逻辑值在元素提取中非常有用。你不需要事先知道要返回的具体索引位置,因为条件检查可以为你找到它们。当你处理大型数据集时,特别是在需要检查记录或重新编码符合特定条件的条目时,这尤其有价值。
在某些情况下,你可能希望将逻辑标志向量转换为数值索引向量。当你需要明确标记为TRUE的元素的索引时,这非常有用。R 函数which接受一个逻辑向量作为参数x,并返回对应于所有TRUE条目的位置的索引。
R> which(x=c(T,F,F,T,T))
[1] 1 4 5
你可以用它来识别myvec中满足某些条件的索引位置;例如,包含负数的那些元素:
R> which(x=myvec<0)
[1] 2 10
对于你实验过的其他myvec选择项,也可以执行相同的操作。请注意,像myvec[which(x=myvec<0)]这样的代码行是多余的,因为提取可以仅使用条件本身进行,即通过myvec[myvec<0],无需使用which。另一方面,使用which可以让你基于逻辑标志向量删除元素。你可以简单地使用which来识别要删除的数值索引,并将它们设为负数。要省略myvec中的负数条目,你可以执行以下操作:
R> myvec[-which(x=myvec<0)]
[1] 5 4 4 4 6 8 10 40221
对矩阵和其他数组也可以做同样的操作。在第 3.2 节中,你按照以下方式存储了一个 3×3 的矩阵:
R> A <- matrix(c(0.3,4.5,55.3,91,0.1,105.5,-4.2,8.2,27.9),nrow=3,ncol=3)
R> A
[,1] [,2] [,3]
[1,] 0.3 91.0 -4.2
[2,] 4.5 0.1 8.2
[3,] 55.3 105.5 27.9
若要使用数值索引提取A的第一行的第二列和第三列元素,你可以执行A[1,2:3]。要使用逻辑标志来实现这一点,你可以输入以下内容:
R> A[c(T,F,F),c(F,T,T)]
[1] 91.0 -4.2
然而,通常你不会显式地指定逻辑向量。假设你想将A中所有小于 1 的元素替换为−7。使用数值索引来执行此操作相当繁琐。使用以下方式创建逻辑标志矩阵要容易得多:
R> A<1
[,1] [,2] [,3]
[1,] TRUE FALSE TRUE
[2,] FALSE TRUE FALSE
[3,] FALSE FALSE FALSE
你可以将这个逻辑矩阵提供给方括号操作符,替换过程如下:
R> A[A<1] <- -7
R> A
[,1] [,2] [,3]
[1,] -7.0 91.0 -7.0
[2,] 4.5 -7.0 8.2
[3,] 55.3 105.5 27.9
这是你第一次在不需要在方括号内列出行或列位置的情况下对子集化矩阵,使用逗号分隔维度(参见第 3.2 节)。这是因为标志矩阵的行列数与目标矩阵相同,从而提供了所有相关的结构信息。
如果你使用which基于逻辑标志结构来识别数值索引,那么在处理二维或更高维对象时你必须小心一些。假设你想要大于 25 的元素的索引位置。适当的逻辑矩阵如下所示。
R> A>25
[,1] [,2] [,3]
[1,] FALSE TRUE FALSE
[2,] FALSE FALSE FALSE
[3,] TRUE TRUE TRUE
现在,假设你询问 R 以下问题:
R> which(x=A>25)
[1] 3 4 6 9
这会返回四个满足关系检查的元素的索引,但它们作为标量值提供。这些如何对应于矩阵的行/列位置呢?
答案在 R 的which函数的默认行为中,该函数本质上将多维对象视为一个单一向量(按列堆叠),然后返回对应的索引向量。假设矩阵A通过首先将第一列到第三列按列堆叠,形成一个向量c(A[,1],A[,2],A[,3])。那么返回的索引就更有意义了。
R> which(x=c(A[,1],A[,2],A[,3])>25)
[1] 3 4 6 9
将列按顺序排列时,返回TRUE的元素是列表中的第三、第四、第六和第九个元素。然而,这种情况可能较难解释,尤其是在处理高维数组时。在这种情况下,你可以通过使用可选参数arr.ind(数组索引),使which返回特定维度的索引。默认情况下,这个参数设置为FALSE,这会导致返回一个转换后的向量索引。而将arr.ind设置为TRUE时,R 会将对象视为矩阵或数组,而不是向量,从而为你提供所请求元素的行和列位置。
R> which(x=A>25,arr.ind=T)
row col
[1,] 3 1
[2,] 1 2
[3,] 3 2
[4,] 3 3
返回的对象现在是一个矩阵,其中每一行表示一个满足逻辑比较的元素,每一列提供该元素的位置。将这里的输出与A进行比较,你会发现这些位置确实对应于A > 25的元素。
两种输出版本(arr.ind=T或arr.ind=F)都可能有用——正确的选择取决于具体应用。
练习 4.3
-
存储这个 10 个值的向量:
foo <- c(7,5,6,1,2,10,8,3,8,2)。然后,执行以下操作:-
提取大于或等于 5 的元素,并将结果存储为
bar。 -
显示包含那些从
foo中去除所有大于或等于 5 的元素后剩下的元素的向量。
-
-
使用(a)(i)中的
bar构造一个 2×3 的矩阵,称为baz,按行填充。然后,执行以下操作:-
将任何等于 8 的元素替换为
baz中第 1 行第 2 列元素的平方值。 -
确认
baz中的所有值现在都小于或等于 25 且大于 4。
-
-
使用以下 18 个值的向量
c(10,5,1,4,7,4,3,3,1,3,4,3,1,7,8,3,7,3)创建一个 3×2×3 的数组,称为qux。然后,执行以下操作:-
确定元素值为 3 或 4 的维度特定索引位置。
-
将
qux中所有小于 3 或大于等于 7 的元素替换为 100。
-
-
从(a)返回到
foo。使用向量c(F,T)从foo中提取每隔一个的值。在第 4.1.4 节中,你已经看到,在某些情况下,可以将TRUE和FALSE替换为0和1。你能使用向量c(0,1)从foo中执行相同的提取操作吗?为什么或为什么不?在这种情况下,R 会返回什么?
4.2 字符
字符串是另一种常见的数据类型,用于表示文本。在 R 中,字符串常用于指定文件夹位置或软件选项(如第 1.2 节简要所示);传递函数参数;以及注释存储的对象、提供文本输出或帮助澄清绘图和图形。简单来说,它们也可以用来定义构成分类变量的不同组,尽管正如你将在第 4.3 节中看到的,因子更适合用于此目的。
注意
R 环境中有三种不同的字符串格式。默认的字符串格式叫做扩展正则表达式;其他变种分别被称为Perl和字面正则表达式。这些变种的复杂性超出了本书的范围,因此从这里开始提到的字符字符串都指的是扩展正则表达式。有关其他字符串格式的更多技术细节,请在提示符下输入?regex。
4.2.1 创建字符串
字符串由双引号"表示。要创建一个字符串,只需在一对引号之间输入文本。
R> foo <- "This is a character string!"
R> foo
[1] "This is a character string!"
R> length(x=foo)
[1] 1
R 将字符串视为一个单独的实体。换句话说,foo是一个长度为 1 的向量,因为 R 只计算不同字符串的总数,而不是单个的单词或字符。要计算单个字符的数量,可以使用nchar函数。以下是一个使用foo的示例:
R> nchar(x=foo)
[1] 27
几乎任何字符组合,包括数字,都可以是有效的字符字符串。
R> bar <- "23.3"
R> bar
[1] "23.3"
请注意,在这种形式下,字符串没有数值含义,它不会像数字 23.3 一样被处理。例如,尝试将其乘以 2 会导致错误。
R> bar*2
Error in bar * 2 : non-numeric argument to binary operator
这个错误发生是因为*期望对两个数值进行操作(而不是一个数字和一个字符串,这没有意义)。
字符串可以通过多种方式进行比较,最常见的比较是检查是否相等。
R> "alpha"=="alpha"
[1] TRUE
R> "alpha"!="beta"
[1] TRUE
R> c("alpha","beta","gamma")=="beta"
[1] FALSE TRUE FALSE
其他关系运算符的行为如你所料。例如,R 认为字母表中后面的字母大于前面的字母,这意味着它可以根据字母表顺序判断一个字符串是否大于另一个字符串。
R> "alpha"<="beta"
[1] TRUE
R> "gamma">"Alpha"
[1] TRUE
此外,大写字母被视为大于小写字母。
R> "Alpha">"alpha"
[1] TRUE
R> "beta">="bEtA"
[1] FALSE
大多数符号也可以在字符串中使用。例如,以下字符串是有效的:
R> baz <- "&4 _ 3 **%.? $ymbolic non$en$e ,; "
R> baz
[1] "&4 _ 3 **%.? $ymbolic non$en$e ,; "
一个重要的例外是反斜杠\,也称为转义符。当反斜杠在字符串的引号内使用时,它启动了一些简单的控制,用于控制字符串本身的打印或显示。你将在第 4.2.3 节中看到这一点。首先,让我们看一下两个用于合并字符串的有用函数。
4.2.2 拼接
有两个主要函数用于连接(或将多个字符串粘合在一起):cat和paste。这两者之间的区别在于它们如何返回内容。第一个函数,cat,将其输出直接发送到控制台屏幕,并且不正式返回任何内容。paste函数将其内容连接起来,然后将最终的字符字符串作为可用的 R 对象返回。当字符串连接的结果需要传递给另一个函数或以某种二次方式使用时,这非常有用,而不仅仅是显示出来。考虑以下字符字符串的向量:
R> qux <- c("awesome","R","is")
R> length(x=qux)
[1] 3
R> qux
[1] "awesome" "R" "is"
与数字和逻辑值一样,您也可以将任意数量的字符串存储在矩阵或数组结构中,前提是您愿意这样做。
当调用cat或paste时,您按希望的顺序将参数传递给函数进行合并。以下几行展示了这两个函数的相同用法,但输出类型有所不同:
R> cat(qux[2],qux[3],"totally",qux[1],"!")
R is totally awesome !
R> paste(qux[2],qux[3],"totally",qux[1],"!")
[1] "R is totally awesome !"
在这里,您使用了qux的三个元素以及两个附加字符串"totally"和"!",以生成最终的连接字符串。在输出中,请注意,cat只是将文本连接并打印到屏幕上。这意味着您不能直接将结果赋值给一个新变量,并将其作为字符字符串使用。而对于paste,输出左侧的[1]以及"引号的存在表明返回的项是一个包含字符字符串的向量,且可以将其赋值给对象并在其他函数中使用。
注意
在使用 R GUI 时,OS X 和 Windows 在默认的字符串连接处理上有所不同。在 Windows 中调用cat后,新 R 提示符会出现在与打印字符串同一行,在这种情况下,您可以直接按ENTER键进入下一行,或者使用转义序列,您将在第 4.2.3 节中学习到。在 OS X 中,新提示符照常出现在下一行。
这两个函数都有一个可选参数sep,用于在字符串连接时作为分隔符。您将字符字符串传递给sep,它将在您传递给paste或cat的所有其他字符串之间插入该字符串。例如:
R> paste(qux[2],qux[3],"totally",qux[1],"!",sep="---")
[1] "R---is---totally---awesome---!"
R> paste(qux[2],qux[3],"totally",qux[1],"!",sep="")
[1] "Ristotallyawesome!"
对于cat,也会发生相同的行为。请注意,如果您不想要任何分隔,可以将sep=""(一个空字符串)设置为分隔符,如第二个示例所示。空字符串分隔符可以用于正确的句子间距;请注意,在您首次使用paste和cat时,awesome和感叹号之间的间隙。如果没有包含sep参数,R 默认会在字符串之间插入空格。
例如,您可以手动插入必要的空格,编写如下代码:
R> cat("Do you think ",qux[2]," ",qux[3]," ",qux[1],"?",sep="")
Do you think R is awesome?
当你想要简洁地总结某个函数或一组计算的结果时,连接操作非常有用。许多类型的 R 对象可以直接传递给paste或cat函数;软件会尝试自动强制转换这些项为字符字符串。这意味着 R 会将输入转换为字符串,以便将值包含在最终的连接字符串中。对于数值对象,这种方式尤其有效,以下示例可以证明这一点:
R> a <- 3
R> b <- 4.4
R> cat("The value stored as 'a' is ",a,".",sep="")
The value stored as 'a' is 3.
R> paste("The value stored as 'b' is ",b,".",sep="")
[1] "The value stored as 'b' is 4.4."
R> cat("The result of a+b is ",a,"+",b,"=",a+b,".",sep="")
The result of a+b is 3+4.4=7.4.
R> paste("Is ",a+b," less than 10? That's totally ",a+b<10,".",sep="")
[1] "Is 7.4 less than 10? That's totally TRUE."
在这里,非字符串对象的值被放置在你希望它们出现在最终字符串输出中的位置。计算结果也可以作为字段显示,如算术运算a+b和逻辑比较a+b<10所示。你将在第 6.2.4 节看到更多关于一种类型的值强制转换为另一种类型的细节。
4.2.3 转义序列
在第 4.2.1 节中,我提到过独立的反斜杠在字符串中不像普通字符那样起作用。\\用于调用转义序列。转义序列允许你输入控制字符串格式和间距的字符,而不是被解释为普通文本。表 4-3 描述了一些最常见的转义序列,完整列表可以通过在提示符下输入?Quotes来查找。
表 4-3: 常用的字符字符串转义序列
| 转义序列 | 结果 |
|---|---|
\n |
开始换行 |
\t |
水平制表符 |
\b |
调用退格 |
\\ |
用作单个反斜杠 |
\" |
包含双引号 |
转义序列为字符字符串的显示增加了灵活性,这在结果汇总和图表注释中非常有用。你可以精确地在想要的地方输入这些序列。我们来看一个例子。
R> cat("here is a string\nsplit\tto neww\b\n\n\tlines")
here is a string
split to new
lines
由于转义符号是\,而字符串的开始和结束符号是",如果你希望这些字符之一包含在字符串中,必须使用转义符号让它们被解释为普通字符。
R> cat("I really want a backslash: \\\nand a double quote: \"")
I really want a backslash: \
and a double quote: "
这些转义序列意味着你不能在 R 中使用独立的反斜杠来表示文件路径字符串。如第 1.2.3 节中所述(你曾使用getwd打印当前工作目录,使用setwd更改工作目录),文件夹分隔符必须使用正斜杠/,而不是反斜杠。
R> setwd("/folder1/folder2/folder3/")
文件路径的指定在读取和写入文件时经常出现,你将在第八章中深入探索。
4.2.4 子字符串和匹配
模式匹配让你检查给定的字符串,以识别其中的较小字符串。
函数substr接受一个字符串x并提取字符串中两个字符位置之间的部分(包括这两个位置),这两个位置由作为start和stop参数传递的数字指定。让我们试一下第 4.2.1 节中的对象foo。
R> foo <- "This is a character string!"
R> substr(x=foo,start=21,stop=27)
[1] "string!"
在这里,你提取了第 21 个字符到第 27 个字符之间的部分,得到"string!"。substr函数也可以与赋值运算符一起使用,直接替换为一组新的字符。在这种情况下,替换字符串应包含与选定区域相同数量的字符。
R> substr(x=foo,start=1,stop=4) <- "Here"
R> foo
[1] "Here is a character string!"
如果替换字符串的长度超过start和stop指示的字符数,则仍然会进行替换,从start位置开始,到stop位置结束。它会截断任何超过替换字符数的部分。如果字符串短于替换的字符数,则替换在字符串完全插入时结束,原始字符保持不变,直到stop位置。
使用sub和gsub函数进行替换更加灵活。sub函数会在给定的字符串x中搜索包含的小字符串pattern,然后用新的字符串(作为replacement参数)替换第一个匹配项。gsub函数做的是相同的事情,但它会替换每个匹配的pattern。以下是一个例子:
R> bar <- "How much wood could a woodchuck chuck"
R> sub(pattern="chuck",replacement="hurl",x=bar)
[1] "How much wood could a woodhurl chuck"
R> gsub(pattern="chuck",replacement="hurl",x=bar)
[1] "How much wood could a woodhurl hurl"
使用sub和gsub时,replacement值的字符数不需要与要替换的pattern相同。这些函数还具有像大小写敏感度这样的搜索选项。帮助文件?substr和?sub有更多详细信息,还列出了其他一些模式匹配函数和技术。你可能还想查看grep命令及其变体;请参阅相关的帮助文件?grep。
练习 4.4
-
完全重新创建以下输出:
"The quick brown fox jumped over the lazy dogs" -
假设你已经存储了值
num1 <- 4和num2 <- 0.75。写一行 R 代码,返回以下字符串:[1] "The result of multiplying 4 by 0.75 is 3"确保你的代码能够生成正确的乘法结果,适用于存储在
num1和num2中的任何两个数字。 -
在我的本地机器上,我在 R 中为这本书的工作指定的目录是
"/Users/tdavies/Documents/RBook/"。假设这是你的机器——写一行代码,将这个字符串中的tdavies替换为你的名字首字母和姓氏。 -
在第 4.2.4 节中,你存储了以下字符串:
R> bar <- "How much wood could a woodchuck chuck"-
通过将
"if a woodchuck could chuck wood"这句话粘贴到bar后,存储一个新字符串。 -
在(i)的结果中,将所有
wood替换为metal。
-
-
存储字符串
"Two 6-packs for $12.99"。然后执行以下操作:-
使用相等性检查,确认从字符 5 开始,到字符 10 结束的子字符串是
"6-pack"。 -
通过将价格更改为$10.99,使其成为更好的交易。
-
4.3 因子
在这一节中,你将学习一些与创建、处理和检查因子相关的简单函数。因子是 R 表示仅属于有限类别的离散数据点的最自然方式,而不是属于连续体的数据。像这样的分类数据在数据科学中扮演着重要角色,你将在第十三章中从统计学的角度再次详细学习因子。
4.3.1 确定类别
为了了解因子的工作原理,让我们从一个简单的数据集开始。假设你找到八个人,并记录下他们的名字、性别和出生月份,见表 4-4。
表 4-4: 八个个体的示例数据集
| 姓名 | 性别 | 出生月份 |
|---|---|---|
| Liz | Female | April |
| Jolene | Female | January |
| Susan | Female | December |
| Boris | Male | September |
| Rochelle | Female | November |
| Tim | Male | July |
| Simon | Male | July |
| Amy | Female | June |
事实上,表示每个人姓名的合理方式只有一种——作为字符字符串的向量。
R> firstname <- c("Liz","Jolene","Susan","Boris","Rochelle","Tim","Simon",
"Amy")
然而,在记录性别时,你有更多的灵活性。将女性编码为 0,男性编码为 1,数值选项如下:
R> sex.num <- c(0,0,0,1,0,1,1,0)
当然,也可以使用字符字符串,许多人更喜欢这种方式,因为你不需要记住每个组的数字代码。
R> sex.char <- c("female","female","female","male","female","male","male",
"female")
然而,存储个人姓名和性别数据时,有一个根本性的区别。个人姓名是一个独特的标识符,可以有无限多种可能性,而记录性别通常只有两种选择。这类数据,即所有可能的值都属于有限的类别时,最适合使用 R 中的因子来表示。
因子通常是从数值向量或字符向量中创建的(请注意,你不能使用因子值来填充矩阵或多维数组;因子只能以向量的形式存在)。要创建因子向量,可以使用factor函数,下面是一个使用sex.num和sex.char的示例:
R> sex.num.fac <- factor(x=sex.num)
R> sex.num.fac
[1] 0 0 0 1 0 1 1 0
Levels: 0 1
R> sex.char.fac <- factor(x=sex.char)
R> sex.char.fac
[1] female female female male female male male female
Levels: female male
在这里,你可以得到存储性别值的两个向量的因子版本。
乍一看,这些对象看起来与它们所创建的字符向量和数值向量没什么不同。事实上,因子对象的工作方式与向量非常相似,只不过附加了一些额外的信息(R 内部表示因子对象的方式也略有不同)。例如,像length和which这样的函数在因子对象上的工作方式与在向量上的使用方式是一样的。
因子对象包含的最重要的额外信息(或属性;见第 6.2.1 节)是其水平,即存储因子中可能值的部分。这些水平会打印在每个因子向量的底部。你可以使用levels函数提取这些水平作为字符字符串的向量。
R> levels(x=sex.num.fac)
[1] "0" "1"
R> levels(x=sex.char.fac)
[1] "female" "male"
你也可以使用levels重新标记因子。下面是一个示例:
R> levels(x=sex.num.fac) <- c("1","2")
R> sex.num.fac
[1] 1 1 1 2 1 2 2 1
Levels: 1 2
这将重新标记女性为1,男性为2。
因子值向量的子集提取与其他任何向量的子集提取方式相同。
R> sex.char.fac[2:5]
[1] female female male female
Levels: female male
R> sex.char.fac[c(1:3,5,8)]
[1] female female female female female
Levels: female male
请注意,在对子集化的因子对象进行子集提取后,即使某些层次不再出现在子集对象中,该对象仍然会继续存储所有已定义的层次。
如果你想通过逻辑标志向量从因子中提取子集,请记住,即使原始数据向量是数值型的,因子的层次是以字符字符串形式存储的,因此在请求或测试特定层次时,需要使用字符串。例如,要使用新标记的sex.num.fac来识别所有男性,可以使用以下代码:
R> sex.num.fac=="2"
[1] FALSE FALSE FALSE TRUE FALSE TRUE TRUE FALSE
由于firstname和sex中的元素在它们的因子向量中有相应的位置,你可以使用这个逻辑向量来获取所有男性的名字(这次使用的是"male"/"female"因子向量)。
R> firstname[sex.char.fac=="male"]
[1] "Boris" "Tim" "Simon"
当然,这种简单的子集提取方式也可以通过原始的数字向量sex.num或原始的字符向量sex.char以类似的方式实现。在下一节中,你将探索将分类数据表示为 R 中的因子所带来的一些更独特的优势。
4.3.2 定义和排序层次
上一节中的性别因子表示了最简单的因子变量——它只有两个可能的层次,并且没有顺序,因为一个层次并不直观地被认为是“高于”或“跟随”另一个层次。在这里,你将查看具有逻辑顺序的因子层次;例如,出生月份(MOB),其中有 12 个层次,它们有自然的顺序。让我们将之前观察到的 MOB 数据存储为字符向量。
R> mob <- c("Apr","Jan","Dec","Sep","Nov","Jul","Jul","Jun")
这个向量中的数据存在两个问题。首先,并非所有可能的类别都有表示,因为mob只包含七个独特的月份。其次,这个向量没有反映月份的自然顺序。如果你比较一月和十二月,看看哪个更大,你会得到:
R> mob[2]
[1] "Jan"
R> mob[3]
[1] "Dec"
R> mob[2]<mob[3]
[1] FALSE
按字母顺序排列,这个结果当然是正确的——J显然不会出现在D之前。但从日历月份的顺序来看,也就是我们感兴趣的部分,FALSE的结果就是不正确的。
如果你从这些值创建一个因子对象,你可以通过向factor函数提供额外的参数来解决这两个问题。你可以通过向levels参数提供一个包含所有可能值的字符向量来定义额外的层次,然后通过将ordered参数设置为TRUE,指示 R 按levels中出现的顺序精确排列这些值。
R> ms <- c("Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov",
"Dec")
R> mob.fac <- factor(x=mob,levels=ms,ordered=TRUE)
R> mob.fac
[1] Apr Jan Dec Sep Nov Jul Jul Jun
Levels: Jan < Feb < Mar < Apr < May < Jun < Jul < Aug < Sep < Oct < Nov < Dec
在这里,mob.fac向量包含了与之前的mob向量相同的单独条目,并且它们处于相同的索引位置。但请注意,这个变量有 12 个水平,尽管你并没有为"Feb"、"Mar"、"May"、"Aug"和"Oct"这些水平进行任何观测。(请注意,如果你的 R 控制台窗口太窄,无法显示所有的水平,你可能会看到...,表示有更多的输出被隐藏了。只需调整窗口宽度并重新打印对象,就可以看到隐藏的水平。)此外,这些水平的严格顺序通过对象输出中的<符号显示。使用这个新的因子对象,你可以执行之前的关系比较,并得到你预期的结果。
R> mob.fac[2]
[1] Jan
Levels: Jan < Feb < Mar < Apr < May < Jun < Jul < Aug < Sep < Oct < Nov < Dec
R> mob.fac[3]
[1] Dec
Levels: Jan < Feb < Mar < Apr < May < Jun < Jul < Aug < Sep < Oct < Nov < Dec
R> mob.fac[2]<mob.fac[3]
[1] TRUE
这些改进远不仅仅是外观上的。举个例子,具有某些类别零观测值的数据集,与最初定义时类别较少的同一数据集之间,存在很大区别。是否指示 R 正式对因子向量进行排序,也可能对各种统计方法的实现产生重要影响,例如回归和其他类型的建模。
4.3.3 合并与切割
如你所见,通常使用c函数将多个相同类型的向量(无论是数值型、逻辑型还是字符型)合并起来都很简单。这里有一个例子:
R> foo <- c(5.1,3.3,3.1,4)
R> bar <- c(4.5,1.2)
R> c(foo,bar)
[1] 5.1 3.3 3.1 4.0 4.5 1.2
这将把两个数值向量合并成一个。
然而,c函数在处理因子值向量时并不像处理其他向量那样工作。让我们看看当你在表 4-4 的数据和 MOB 因子向量mob.fac(来自 4.3.2 节)上使用它时会发生什么。假设现在你再观察了三个新的个体,其 MOB 值分别是"Oct"、"Feb"和"Feb",这些值作为因子对象存储,如下所示。
R> new.values <- factor(x=c("Oct","Feb","Feb"),levels=levels(mob.fac),
ordered=TRUE)
R> new.values
[1] Oct Feb Feb
Levels: Jan < Feb < Mar < Apr < May < Jun < Jul < Aug < Sep < Oct < Nov < Dec
现在你有了带有原始八个观测值的mob.fac和包含三个额外观测值的new.values。它们都是因子对象,定义了相同的、有序的水平。你可能会认为可以通过c函数将两者合并,如下所示。
R> c(mob.fac,new.values)
[1] 4 1 12 9 11 7 7 6 10 2 2
显然,这并没有达到你想要的效果。将这两个因子对象合并后得到了一个数值向量。这是因为c函数将因子解释为整数。通过与已定义的水平进行比较,你可以看到这些数字对应于有序水平中每个月的索引位置。
R> levels(mob.fac)
[1] "Jan" "Feb" "Mar" "Apr" "May" "Jun" "Jul" "Aug" "Sep" "Oct" "Nov" "Dec"
这意味着你可以使用这些整数与levels(mob.fac)结合,获取一个完整的字符向量,包含所有观测数据——原始的八个观测值加上额外的三个。
R> levels(mob.fac)[c(mob.fac,new.values)]
[1] "Apr" "Jan" "Dec" "Sep" "Nov" "Jul" "Jul" "Jun" "Oct" "Feb" "Feb"
现在你已经将所有的观测值存储在一个向量中,但它们目前是作为字符串存储的,而不是因子值。最后一步是将这个向量转换为因子对象。
R> mob.new <- levels(mob.fac)[c(mob.fac,new.values)]
R> mob.new.fac <- factor(x=mob.new,levels=levels(mob.fac),ordered=TRUE)
R> mob.new.fac
[1] Apr Jan Dec Sep Nov Jul Jul Jun Oct Feb Feb
Levels: Jan < Feb < Mar < Apr < May < Jun < Jul < Aug < Sep < Oct < Nov < Dec
正如这个例子所示,合并因素要求你实际上拆解这两个对象,获取每个条目相对于因素级别的数值索引位置,然后将它们重新组合在一起。这有助于确保级别的一致性,并确保最终产品中的观察值有效。
因素通常也是从原本在连续范围内测量的数据中创建的,例如一组成年人的体重或给患者服用的药物量。有时候,你需要将这些类型的观察值分组(或分箱)成不同的类别,比如小/中/大或低/高。在 R 中,你可以使用 cut 函数将这类数据转化为离散的因素类别。考虑以下长度为 10 的数值向量:
R> Y <- c(0.53,5.4,1.5,3.33,0.45,0.01,2,4.2,1.99,1.01)
假设你想按以下方式对数据进行分箱:小表示区间[0,2),中表示[2,4),大表示[4,6]。方括号表示包含其最接近的值,圆括号表示排除,所以如果 0 ≤ y < 2,观察值 y 将落入小区间;如果 2 ≤ y < 4,则落入中区间;如果 4 ≤ y ≤ 6,则落入大区间。为此,你可以使用 cut 并将所需的分界区间传递给 breaks 参数:
R> br <- c(0,2,4,6)
R> cut(x=Y,breaks=br)
[1] (0,2] (4,6] (0,2] (2,4] (0,2] (0,2] (0,2] (4,6] (0,2] (0,2]
Levels: (0,2] (2,4] (4,6]
这会给你一个因素,每个观察值现在被分配了一个区间。然而,注意到你的边界区间是颠倒的——你希望边界级别位于左侧,例如 [0,2),而不是默认情况下位于右侧的 (0,2]。你可以通过将逻辑参数 right 设置为 FALSE 来修正这个问题。
R> cut(x=Y,breaks=br,right=F)
[1] [0,2) [4,6) [0,2) [2,4) [0,2) [0,2) [2,4) [4,6) [0,2) [0,2)
Levels: [0,2) [2,4) [4,6)
现在你交换了哪些边界是包含的,哪些是排除的。这很重要,因为它会改变哪些类别值落入。注意第七个观察值的类别已经发生了变化。但是,仍然存在一个问题:当前的最后一个区间排除了 6,而你希望这个最大值能够包含在最高的级别中。你可以通过另一个逻辑参数来修正这个问题:include.lowest。尽管它被称为“include.lowest”,但如果 right 设置为 FALSE,这个参数也可以用来包含最高值,正如帮助文件 ?cut 中所指出的那样。
R> cut(x=Y,breaks=br,right=F,include.lowest=T)
[1] [0,2) [4,6] [0,2) [2,4) [0,2) [0,2) [2,4) [4,6] [0,2) [0,2)
Levels: [0,2) [2,4) [4,6]
现在,区间已经按你想要的方式定义好了。最后,你希望为类别添加更好的标签,而不是使用 R 默认应用的区间级别,可以通过将一个字符向量传递给 labels 参数来实现。标签的顺序必须与因素对象中的级别顺序匹配。
R> lab <- c("Small","Medium","Large")
R> cut(x=Y,breaks=br,right=F,include.lowest=T,labels=lab)
[1] Small Large Small Medium Small Small Medium Large Small Small
Levels: Small Medium Large
练习 4.5
新西兰政府由国家党、工党、绿党和毛利党组成,还有几个较小的党派统称为其他。假设你问了 20 位新西兰人他们最认同哪个党派,并获得了以下数据:
• 有 12 个男性和 8 个女性;编号为 1、5–7、12、14–16 的个体为女性。
• 编号为 1、4、12、15、16 和 19 的个体认同工党;没有人认同毛利党;编号为 6、9 和 11 的个体认同绿党;10 和 20 认同其他党派;其余认同国民党。
-
运用你对向量的知识(例如,子集和重写)创建两个字符向量:
sex,其中包含"M"(男性)和"F"(女性);party,其中包含"National"(国民党)、"Labour"(工党)、"Greens"(绿党)、"Maori"(毛利党)和"Other"(其他党派)。确保条目按照前面概述的正确位置放置。 -
创建两个不同的因子向量,分别基于
sex和party。在这两种情况下使用ordered=TRUE是否有意义?R 是如何排列这些水平的? -
使用因子子集来执行以下操作:
-
返回仅包含男性参与者所选政党的因子向量。
-
返回选择国民党参与者的性别因子向量。
-
-
又有六人加入了调查,偏好的政党结果是
c("National","Maori","Maori","Labour","Greens","Labour"),性别为c("M","M","F","F","F","M")。将这些结果与(b)中的原始因子结合起来。
假设你还要求所有参与者陈述他们对工党在下一次选举中获得比国民党更多议席的信心,并为这种信心附加一个主观百分比。以下是获得的 26 个结果:93,55,29,100,52,84,56,0,33,52,35,53,55,46,40,40,56,45,64,31,10,29,40,95,18,61。
-
创建一个表示信心水平的因子,信心水平如下:低为百分比在[0,30]之间;中等为百分比在(30,70]之间;高为百分比在(70,100]之间。
-
从(e)中提取出那些最初表示认同工党的个体所对应的水平。也为国民党提取相应的水平。你注意到什么了吗?
本章重要代码
| 函数/运算符 | 简要描述 | 首次出现 |
|---|---|---|
TRUE, FALSE |
保留的逻辑值 | 第 4.1.1 节,第 60 页 |
T, F |
上述的非保留版本 | 第 4.1.1 节,第 60 页 |
==, !=, >, <, >=, <= |
关系运算符 | 第 4.1.2 节,第 61 页 |
any |
检查是否有任何条目为TRUE |
第 4.1.2 节,第 63 页 |
all |
检查所有条目是否为TRUE |
第 4.1.2 节,第 63 页 |
&&, &, ||, |, ! |
逻辑运算符 | 第 4.1.3 节,第 65 页 |
which |
确定TRUE值的索引 |
第 4.1.5 节,第 69 页 |
" " |
创建一个字符字符串 | 第 4.2.1 节,第 73 页 |
nchar |
获取字符串中的字符数 | 第 4.2.1 节, 第 73 页 |
cat |
拼接字符串(无返回值) | 第 4.2.2 节, 第 74 页 |
paste |
拼接字符串(返回一个字符串) | 第 4.2.2 节, 第 74 页 |
\ |
字符串转义 | 第 4.2.3 节, 第 76 页 |
substr |
提取字符串子集 | 第 4.2.4 节, 第 77 页 |
sub, gsub |
字符串匹配与替换 | 第 4.2.4 节, 第 78 页 |
factor |
创建因子向量 | 第 4.3.1 节, 第 80 页 |
levels |
获取因子的水平 | 第 4.3.1 节, 第 81 页 |
cut |
从连续型数据创建因子 | 第 4.3.3 节, 第 85 页 |
第五章:5
列表和数据框

向量、矩阵和数组是 R 中高效且方便的数据存储结构,但它们有一个明显的限制:它们只能存储一种类型的数据。在本章中,你将探讨另外两种数据结构,列表和数据框,它们可以同时存储多种类型的值。
5.1 对象的列表
列表是一种非常有用的数据结构。它可以用来将任何类型的 R 结构和对象组合在一起。一个单一的列表可以包含一个数值矩阵、一个逻辑数组、一个单一的字符字符串和一个因子对象。你甚至可以将一个列表作为另一个列表的组件。在本节中,你将学习如何创建、修改和访问这些灵活结构的组件。
5.1.1 定义和组件访问
创建一个列表与创建一个向量非常类似。你将想要包含的元素提供给list函数,并用逗号分隔。
R> foo <- list(matrix(data=1:4,nrow=2,ncol=2),c(T,F,T,T),"hello")
R> foo
[[1]]
[,1] [,2]
[1,] 1 3
[2,] 2 4
[[2]]
[1] TRUE FALSE TRUE TRUE
[[3]]
[1] "hello"
在列表foo中,你存储了一个 2 × 2 的数值矩阵,一个逻辑向量和一个字符字符串。这些元素会按它们提供给list函数的顺序打印出来。与向量一样,你可以使用length函数检查列表中的组件数量。
R> length(x=foo)
[1] 3
你可以使用索引来从列表中获取组件,索引是通过双中括号输入的。
R> foo[[1]]
[,1] [,2]
[1,] 1 3
[2,] 2 4
R> foo[[3]]
[1] "hello"
这个操作被称为成员引用。当你通过这种方式获取组件时,可以像对待工作区中的独立对象一样对待它;不需要做任何特殊处理。
R> foo[[1]] + 5.5
[,1] [,2]
[1,] 6.5 8.5
[2,] 7.5 9.5
R> foo[[1]][1,2]
[1] 3
R> foo[[1]][2,]
[1] 2 4
R> cat(foo[[3]],"you!")
hello you!
要覆盖foo的某个成员,你可以使用赋值运算符。
R> foo[[3]]
[1] "hello"
R> foo[[3]] <- paste(foo[[3]],"you!")
R> foo
[[1]]
[,1] [,2]
[1,] 1 3
[2,] 2 4
[[2]]
[1] TRUE FALSE TRUE TRUE
[[3]]
[1] "hello you!"
假设现在你想访问foo的第二和第三个组件,并将它们存储为一个对象。你可能的第一个直觉是尝试如下操作:
R> foo[[c(2,3)]]
[1] TRUE
但是 R 没有按你想要的方式工作。相反,它返回了第二个组件的第三个元素。这是因为在列表上使用双中括号总是按单个成员来解释的。幸运的是,使用双中括号进行成员引用并不是访问列表组件的唯一方式。你也可以使用单中括号表示法,这被称为列表切片,它允许你一次选择多个列表项。
R> bar <- foo[c(2,3)]
R> bar
[[1]]
[1] TRUE FALSE TRUE TRUE
[[2]]
[1] "hello you!"
请注意,结果bar本身就是一个列表,其中包含按请求顺序存储的两个组件。
5.1.2 命名
你可以命名列表组件,以便让元素更易于识别和操作。就像你在第 4.3.1 节中看到的因子水平的信息一样,名称是 R 的属性。
让我们从之前的列表foo开始,给它添加名称。
R> names(foo) <- c("mymatrix","mylogicals","mystring")
R> foo
$mymatrix
[,1] [,2]
[1,] 1 3
[2,] 2 4
$mylogicals
[1] TRUE FALSE TRUE TRUE
$mystring
[1] "hello you!"
这改变了对象在控制台上的打印方式。之前它在每个组件前打印[[1]]、[[2]]和[[3]],现在它打印你指定的名称:$mymatrix、$mylogicals和$mystring。现在,你可以使用这些名称和美元符号运算符来进行成员引用,而不是使用双中括号。
R> foo$mymatrix
[,1] [,2]
[1,] 1 3
[2,] 2 4
这与调用foo[[1]]是一样的。实际上,即使一个对象已命名,你仍然可以使用数字索引来获取一个成员。
R> foo[[1]]
[,1] [,2]
[1,] 1 3
[2,] 2 4
子集化命名成员的工作方式也是相同的。
R> all(foo$mymatrix[,2]==foo[[1]][,2])
[1] TRUE
这确认了(使用你在第 4.1.2 节中看到的all函数)这两种提取foo中矩阵第二列的方法,提供了相同的结果。
要在创建列表时命名其组件,可以在list命令中为每个组件分配一个标签。使用foo的一些组件,创建一个新的命名列表。
R> baz <- list(tom=c(foo[[2]],T,T,T,F),dick="g'day mate",harry=foo$mymatrix*2)
R> baz
$tom
[1] TRUE FALSE TRUE TRUE TRUE TRUE TRUE FALSE
$dick
[1] "g'day mate"
$harry
[,1] [,2]
[1,] 2 6
[2,] 4 8
现在,baz对象包含了三个命名组件tom、dick和harry。
R> names(baz)
[1] "tom" "dick" "harry"
如果你想重命名这些成员,可以像之前为foo所做的那样,简单地将一个长度为 3 的字符向量赋值给names(baz)。
注意
当使用 names 函数时,组件名称总是以双引号中的字符字符串形式提供和返回。然而,如果在创建列表时指定名称(在 list 函数内部),或者使用名称通过美元操作符提取成员时,名称则不带引号(换句话说,它们不是以字符串形式给出)。
5.1.3 嵌套
如前所述,列表的一个成员本身可以是一个列表。当像这样嵌套列表时,重要的是要跟踪任何成员的深度,以便稍后进行子集提取或提取。
请注意,你可以通过使用美元操作符和一个新名称,向任何现有列表添加组件。这里是一个使用之前的foo和baz的示例:
R> baz$bobby <- foo
R> baz
$tom
[1] TRUE FALSE TRUE TRUE TRUE TRUE TRUE FALSE
$dick
[1] "g'day mate"
$harry
[,1] [,2]
[1,] 2 6
[2,] 4 8
$bobby
$bobby$mymatrix
[,1] [,2]
[1,] 1 3
[2,] 2 4
$bobby$mylogicals
[1] TRUE FALSE TRUE TRUE
$bobby$mystring
[1] "hello you!"
这里,你定义了一个名为bobby的第四个组件,属于列表baz。成员bobby被赋予整个列表foo。如你所见,通过打印新的baz,bobby现在有三个组件。名称和索引现在都是分层的,你可以使用任意一个(或结合使用)来提取内部列表的成员。
R> baz$bobby$mylogicals[1:3]
[1] TRUE FALSE TRUE
R> baz[[4]][[2]][1:3]
[1] TRUE FALSE TRUE
R> baz[[4]]$mylogicals[1:3]
[1] TRUE FALSE TRUE
这些指令告诉 R 返回存储为列表bobby的第二个组件([[2]],也名为mylogicals)中的逻辑向量的前三个元素,而bobby又是列表baz的第四个组件。只要你了解每一层子集返回的内容,就可以继续根据需要使用名称和数字索引进行子集化。考虑此示例中的第三行。子集的第一层是baz[[4]],它是一个包含三个组件的列表。第二层子集通过调用baz[[4]]$mylogicals从该列表中提取组件mylogicals。这个组件代表一个长度为 4 的向量,所以第三层子集通过baz[[4]]$mylogicals[1:3]提取该向量的前三个元素。
列表通常用于返回各种 R 函数的输出。但它们在系统资源上可能迅速变成相当大的对象。通常建议,当只有一种类型的数据时,应坚持使用基本的向量、矩阵或数组结构来记录和存储观察值。
练习 5.1
-
创建一个列表,其中包含以下内容:按顺序排列的 20 个均匀分布的数字,介于 −4 和 4 之间;按列填充的 3 × 3 逻辑向量矩阵
c(F,T,T,T,F,T,T,F,F);包含两个字符串"don"和"quixote"的字符向量;以及包含观察值c("LOW","MED","LOW","MED","MED","HIGH")的因子向量。然后执行以下操作:-
提取逻辑矩阵中第 2 行、第 1 行的第 2 列和第 3 列元素,按此顺序。
-
使用
sub将"quixote"替换为"Quixote",将"don"替换为"Don",并在列表中进行修改。然后,使用修改后的列表成员,精确地将以下语句连接到控制台屏幕上:"Windmills! ATTACK!" -\Don Quixote/- -
获取序列中介于 −4 和 4 之间且大于 1 的所有值。
-
使用
which确定因子向量中哪些索引被分配为"MED"级别。
-
-
创建一个新列表,其中包含从 (a) 中获取的因子向量作为名为
"facs"的组件;数值向量c(3,2.1,3.3,4,1.5,4.9)作为名为"nums"的组件;以及由 (a) 中列表的前三个成员组成的嵌套列表,命名为"oldlist"。然后执行以下操作:-
提取
"facs"中对应于"nums"中大于或等于 3 的元素的项。 -
向列表中添加一个新成员
"flags"。该成员应为长度为 6 的逻辑向量,获取方式是将"oldlist"组件中的逻辑矩阵的第三列重复两次。 -
使用
"flags"和逻辑非运算符!提取与FALSE对应的"num"项。 -
用单一字符字符串
"Don Quixote"替换"oldlist"中的字符字符串向量组件。
-
5.2 数据框
数据框 是 R 中呈现数据集的最自然方式,它包含一个或多个变量的记录观察集合。与列表一样,数据框对变量的数据类型没有限制;你可以存储数值数据、因子数据等等。R 数据框可以被视为具有一些额外规则的列表。最重要的区别在于,在数据框中(与列表不同),成员必须是相同长度的向量。
数据框是 R 中最重要且最常用的统计数据分析工具之一。在本节中,你将学习如何创建数据框并了解其一般特征。
5.2.1 构建
要从头创建数据框,使用 data.frame 函数。你提供按变量分组的数据,这些数据作为相同长度的向量——就像你构造命名列表一样。考虑以下示例数据集:
R> mydata <- data.frame(person=c("Peter","Lois","Meg","Chris","Stewie"),
age=c(42,40,17,14,1),
sex=factor(c("M","F","F","M","M")))
R> mydata
person age sex
1 Peter 42 M
2 Lois 40 F
3 Meg 17 F
4 Chris 14 M
5 Stewie 1 M
在这里,你已经构建了一个包含五个个体的名字、年龄(以年为单位)和性别的数据框。返回的对象应该清楚地说明为什么传递给 data.frame 的向量必须具有相同的长度:长度不同的向量在这个上下文中没有意义。如果你将长度不等的向量传递给 data.frame,那么 R 将尝试回收任何较短的向量,以匹配最长的向量,这会破坏你的数据,并可能将观察值分配到错误的变量中。请注意,数据框会以行和列的形式打印到控制台——它们看起来更像是矩阵而非命名列表。这种自然的电子表格样式使得读取和操作数据集变得更加容易。数据框中的每一行叫做 记录,每一列叫做 变量。
你可以通过指定行和列的索引位置来提取数据的部分内容(就像操作矩阵一样)。下面是一个示例:
R> mydata[2,2]
[1] 40
这会给你第二行第二列的元素——Lois 的年龄。现在提取第三列的第三、第四和第五个元素:
R> mydata[3:5,3]
[1] F M M
Levels: F M
这将返回一个因子向量,包含 Meg、Chris 和 Stewie 的性别。以下代码提取了第三列和第一列的整个数据(顺序为这样):
R> mydata[,c(3,1)]
sex person
1 M Peter
2 F Lois
3 F Meg
4 M Chris
5 M Stewie
这将生成另一个数据框,显示每个人的性别和姓名。
你还可以使用传递给 data.frame 的向量名称来访问变量,即使你不知道它们的列索引位置,这对于大数据集来说非常有用。你使用的是和引用命名列表成员时相同的美元符号操作符。
R> mydata$age
[1] 42 40 17 14 1
你也可以对这个返回的向量进行子集操作:
R> mydata$age[2]
[1] 40
这将返回与之前调用 mydata[2,2] 相同的结果。
你可以报告数据框的大小——记录数和变量数——就像你在矩阵的维度中看到的那样(首次展示于 第 3.1.3 节)。
R> nrow(mydata)
[1] 5
R> ncol(mydata)
[1] 3
R> dim(mydata)
[1] 5 3
nrow 函数获取行数(记录数),ncol 获取列数(变量数),dim 则返回两者。
R 在传递给 data.frame 的字符向量中的默认行为是将每个变量转换为因子对象。观察以下内容:
R> mydata$person
[1] Peter Lois Meg Chris Stewie
Levels: Chris Lois Meg Peter Stewie
注意,这个变量有层级,这表明它被视为一个因子。但是这不是你在之前定义 mydata 时的初衷——你明确地将 sex 定义为因子,但将 person 留作字符向量。为了防止在使用 data.frame 时字符字符串自动转换为因子,可以将可选参数 stringsAsFactors 设置为 FALSE(否则,它默认为 TRUE)。使用这种方式重新构建 mydata 如下所示:
R> mydata <- data.frame(person=c("Peter","Lois","Meg","Chris","Stewie"),
age=c(42,40,17,14,1),
sex=factor(c("M","F","F","M","M")),
stringsAsFactors=FALSE)
R> mydata
person age sex
1 Peter 42 M
2 Lois 40 F
3 Meg 17 F
4 Chris 14 M
5 Stewie 1 M
R> mydata$person
[1] "Peter" "Lois" "Meg" "Chris" "Stewie"
你现在已经得到了期望的、非因子的 person。
5.2.2 添加数据列和合并数据框
假设你想要向现有的数据框添加数据。这可以是新增变量的观察值(增加列数),或者是更多的记录(增加行数)。同样,你可以使用一些之前已经应用于矩阵的函数。
回顾一下第 3.1.2 节中的rbind和cbind函数,它们分别让你追加行和列。这些相同的函数可以直观地用于扩展数据框。例如,假设你有另一个记录需要包含在mydata中:另一个人的年龄和性别,Brian。第一步是创建一个包含 Brian 信息的新数据框。
R> newrecord <- data.frame(person="Brian",age=7,
sex=factor("M",levels=levels(mydata$sex)))
R> newrecord
person age sex
1 Brian 7 M
为了避免任何混淆,确保变量名和数据类型与你打算添加到的那个数据框匹配是非常重要的。请注意,对于因子,你可以使用levels提取现有因子变量的水平。
现在,你可以简单地调用以下内容:
R> mydata <- rbind(mydata,newrecord)
R> mydata
person age sex
1 Peter 42 M
2 Lois 40 F
3 Meg 17 F
4 Chris 14 M
5 Stewie 1 M
6 Brian 7 M
使用rbind,你将mydata与新记录合并,并用结果覆盖了mydata。
向数据框添加变量也非常简单。假设现在你获得了关于这六个人的幽默程度分类数据,定义为“幽默度”。幽默度可以有三个可能的值:Low(低),Med(中),和High(高)。假设 Peter、Lois 和 Stewie 的幽默度很高,Chris 和 Brian 的幽默度为中等,而 Meg 的幽默度较低。在 R 中,你会有一个这样的因子向量:
R> funny <- c("High","High","Low","Med","High","Med")
R> funny <- factor(x=funny,levels=c("Low","Med","High"))
R> funny
[1] High High Low Med High Med
Levels: Low Med High
第一行创建了基本的字符向量funny,第二行通过将其转换为因子来覆盖funny。这些元素的顺序必须与数据框中的记录相对应。现在,你可以简单地使用cbind将这个因子向量作为一列附加到现有的mydata中。
R> mydata <- cbind(mydata,funny)
R> mydata
person age sex funny
1 Peter 42 M High
2 Lois 40 F High
3 Meg 17 F Low
4 Chris 14 M Med
5 Stewie 1 M High
6 Brian 7 M Med
rbind和cbind函数并不是扩展数据框的唯一方式。添加变量的一个有用替代方法是使用美元符号运算符,类似于第 5.1.3 节中添加命名列表成员的方式。假设现在你想通过包含个体年龄(以月为单位,而不是年)来为mydata添加另一个变量,将此新变量命名为age.mon。
R> mydata$age.mon <- mydata$age*12
R> mydata
person age sex funny age.mon
1 Peter 42 M High 504
2 Lois 40 F High 480
3 Meg 17 F Low 204
4 Chris 14 M Med 168
5 Stewie 1 M High 12
6 Brian 7 M Med 84
这使用美元符号运算符创建了一个新的age.mon列,并同时将其赋值为年龄(已经以年为单位存储在age中)乘以 12 的向量。
5.2.3 逻辑记录子集
在第 4.1.5 节中,你学习了如何使用逻辑标志向量来对子集数据结构进行筛选。这在数据框中尤其有用,因为你通常会想查看满足特定条件的记录子集。例如,在处理临床药物试验数据时,研究人员可能想查看仅男性参与者的结果,并将其与女性的结果进行比较。或者,研究人员可能想查看对药物反应最积极的个体的特征。
让我们继续处理mydata。假设你想查看所有与男性相关的记录。从第 4.3.1 节中,你知道以下这一行可以识别sex因子向量中相关的位置:
R> mydata$sex=="M"
[1] TRUE FALSE FALSE TRUE TRUE TRUE
这标记了男性记录。你可以结合第 5.2.1 节中看到的类似矩阵的语法来获取仅限男性的子集。
R> mydata[mydata$sex=="M",]
person age sex funny age.mon
1 Peter 42 M High 504
4 Chris 14 M Med 168
5 Stewie 1 M High 12
6 Brian 7 M Med 84
这将返回所有变量的数据,但仅限于男性参与者。你可以使用相同的行为来选择哪些变量返回在子集中。例如,由于你知道你只选择男性,你可以使用负数的列索引来从结果中省略sex。
R> mydata[mydata$sex=="M",-3]
person age funny age.mon
1 Peter 42 High 504
4 Chris 14 Med 168
5 Stewie 1 High 12
6 Brian 7 Med 84
如果你没有列号,或者你想对返回的列有更多控制,可以改为使用一个包含变量名的字符向量。
R> mydata[mydata$sex=="M",c("person","age","funny","age.mon")]
person age funny age.mon
1 Peter 42 High 504
4 Chris 14 Med 168
5 Stewie 1 High 12
6 Brian 7 Med 84
你用于子集数据框的逻辑条件可以简单或复杂,取决于需要。你放入方括号中的逻辑标志向量必须与数据框中的记录数相匹配。让我们从mydata中提取所有年龄大于 10 岁或有很高幽默感的个体的完整记录。
R> mydata[mydata$age>10|mydata$funny=="High",]
person age sex funny age.mon
1 Peter 42 M High 504
2 Lois 40 F High 480
3 Meg 17 F Low 204
4 Chris 14 M Med 168
5 Stewie 1 M High 12
有时候,要求一个子集时可能不会返回任何记录。在这种情况下,R 会返回一个行数为零的数据框,如下所示:
R> mydata[mydata$age>45,]
[1] person age sex funny age.mon
<0 rows> (or 0-length row.names)
在这个例子中,由于没有个体年龄超过 45 岁,mydata没有返回任何记录。要检查子集是否包含任何记录,你可以对结果使用nrow,如果其结果为零,则表示没有记录满足指定的条件。
练习 5.2
-
在你的 R 工作空间中创建并存储这个数据框作为
dframe:personsexfunnyStanMHighFrancineFMedSteveMLowRogerMHighHayleyFMedKlausMMed变量
person、sex和funny应与第 5.2 节中研究的mydata对象的变量本质上相同。也就是说,person应是字符向量,sex应是一个具有F和M级别的因子,funny应是一个具有Low、Med和High级别的因子。 -
Stan 和 Francine 分别 41 岁,Steve15 岁,Hayley21 岁,Klaus60 岁,Roger 非常老—1600 岁。将这些数据作为新的数值列变量
age添加到dframe中。 -
利用你关于按列索引位置重新排序列变量的知识来覆盖
dframe,使其与mydata保持一致。也就是说,第一列应为person,第二列为age,第三列为sex,第四列为funny。 -
将注意力集中到在第 5.2.2 节中包含
age.mon变量后留下的mydata上。通过删除age.mon列,创建一个名为mydata2的新版本。 -
现在,将
mydata2与dframe合并,并将结果对象命名为mydataframe。 -
编写一行代码,从
mydataframe中提取仅限女性且幽默感水平为Med或High的记录的姓名和年龄。 -
使用你在 R 中处理字符字符串的知识,从
mydataframe中提取所有名字以S开头的人的记录。提示:回忆一下第 4.2.4 节中的substr(注意,substr可以应用于多个字符字符串的向量)。
本章重要代码
| 函数/操作符 | 简要描述 | 首次出现 |
|---|---|---|
list |
创建一个列表 | 第 5.1.1 节,第 89 页 |
[[ ]] |
无名成员引用 | 第 5.1.1 节,第 90 页 |
[ ] |
列表切片(多个成员) | 第 5.1.1 节,第 91 页 |
| ` | 函数/操作符 | 简要描述 |
| --- | --- | --- |
list |
创建一个列表 | 第 5.1.1 节,第 89 页 |
[[ ]] |
无名成员引用 | 第 5.1.1 节,第 90 页 |
[ ] |
列表切片(多个成员) | 第 5.1.1 节,第 91 页 |
| 获取命名成员/变量 | 第 5.1.2 节,第 92 页 | |
data.frame |
创建一个数据框 | 第 5.2.1 节,第 96 页 |
[ , ] |
提取数据框的行/列 | 第 5.2.1 节,第 96 页 |
第六章:6
特殊值、类和强制转换

你现在已经了解了数值型、逻辑型、字符型和因子型,以及它们的独特属性和应用。接下来,你将学习 R 中一些不太明确的特殊值。你将看到它们是如何产生的,以及如何处理和测试它们。然后,你将了解 R 中的不同数据类型以及一些通用的对象类概念。
6.1 一些特殊值
在 R 中,许多情况需要使用特殊值。例如,当数据集中存在缺失的观测值,或者计算出一个近似无限大的数时,软件会为这些情况保留一些特殊的术语。这些特殊值可以用于标记向量、数组或其他数据结构中的异常值或缺失值。
6.1.1 无限大
在第 2.1 节中,我提到过 R 对数字的极限有一定的限制,当数字过大,软件无法可靠地表示时,值会被认为是无限大。当然,数学中的无限大(∞)并不对应一个特定的数字——R 只需要定义一个极限值。这个精确的截止值会因系统而异,部分由 R 可以访问的内存量决定。这个值由特殊对象 Inf 表示,大小写敏感。因为它代表一个数值,Inf 只能与数值型向量关联。让我们创建一些对象来进行测试。
R> foo <- Inf
R> foo
[1] Inf
R> bar <- c(3401,Inf,3.1,-555,Inf,43)
R> bar
[1] 3401.0 Inf 3.1 -555.0 Inf 43.0
R> baz <- 90000¹⁰⁰
R> baz
[1] Inf
这里,你定义了一个对象 foo,它是一个无限大值的单一实例。你还定义了一个数值向量 bar,其中包含两个无限大元素,然后在 baz 中将 90,000 的 100 次方运算得到 R 认为是无限大的结果。
R 还可以表示负无穷,用 -Inf 表示。
R> qux <- c(-42,565,-Inf,-Inf,Inf,-45632.3)
R> qux
[1] -42.0 565.0 -Inf -Inf Inf -45632.3
这会创建一个包含两个负无穷值和一个正无穷值的向量。
尽管无限大并不代表任何特定的数值,但在一定程度上,你仍然可以在 R 中对无限大值进行数学运算。例如,将 Inf 乘以任何负数将得到 -Inf。
R> Inf*-9
[1] -Inf
如果你对无限大进行加法或乘法运算,结果仍然是无限大。
R> Inf+1
[1] Inf
R> 4*-Inf
[1] -Inf
R> -45.2-Inf
[1] -Inf
R> Inf-45.2
[1] Inf
R> Inf+Inf
[1] Inf
R> Inf/23
[1] Inf
零与无限大在除法运算中是密切相关的。任何(有限的)数值除以正或负的无限大都会得到零。
R> -59/Inf
[1] 0
R> -59/-Inf
[1] 0
尽管在数学上没有明确的定义,但请注意,在 R 中,任何非零值除以零都会得到无限大(根据分子符号的不同,可能是正无穷或负无穷)。
R> -59/0
[1] -Inf
R> 59/0
[1] Inf
R> Inf/0
[1] Inf
通常,你只是想检测数据结构中的无限大值。is.infinite 和 is.finite 函数接受一个值的集合,通常是一个向量,并为每个元素返回一个逻辑值,回答提出的问题。这里是一个使用之前的 qux 的例子:
R> qux
[1] -42.0 565.0 -Inf -Inf Inf -45632.3
R> is.infinite(x=qux)
[1] FALSE FALSE TRUE TRUE TRUE FALSE
R> is.finite(x=qux)
[1] TRUE TRUE FALSE FALSE FALSE TRUE
请注意,这些函数不会区分正无穷和负无穷,而is.finite的结果总是与is.infinite的结果相反(即否定)。
最后,关系运算符的功能如你所预期的那样。
R> -Inf<Inf
[1] TRUE
R> Inf>Inf
[1] FALSE
R> qux==Inf
[1] FALSE FALSE FALSE FALSE TRUE FALSE
R> qux==-Inf
[1] FALSE FALSE TRUE TRUE FALSE FALSE
这里,第一行确认-Inf确实被视为小于Inf,第二行显示Inf不是大于Inf。第三行和第四行再次使用qux,测试等式,这是区分正负无穷的一个有用方式,特别是如果你需要做这样的区分。
6.1.2 NaN
在某些情况下,无法用数字、Inf或-Inf表示计算结果。这些难以量化的特殊值在 R 中被标记为NaN,即不是一个数字。
和无穷大值一样,NaN值仅与数值型观察相关。虽然可以直接定义或包含NaN值,但这并不是它们常见的方式。
R> foo <- NaN
R> foo
[1] NaN
R> bar <- c(NaN,54.3,-2,NaN,90094.123,-Inf,55)
R> bar
[1] NaN 54.30 -2.00 NaN 90094.12 -Inf 55.00
通常,NaN是尝试进行无法执行的计算时的意外结果,计算中涉及的值不可用。
在第 6.1.1 节中,你看到将Inf或-Inf加减后仍然得到Inf或-Inf。然而,如果你试图以任何方式抵消无穷大的表示,结果将是NaN。
R> -Inf+Inf
[1] NaN
R> Inf/Inf
[1] NaN
在这里,第一行不会得到零,因为正负无穷大不能以数值的方式进行解释,因此结果是NaN。如果你尝试将Inf除以它本身,也会出现相同的情况。此外,虽然你之前看到过非零值除以零会得到正无穷或负无穷,但当零除以零时,结果是NaN。
R> 0/0
[1] NaN
请注意,任何涉及NaN的数学运算都会简单地得到NaN。
R> NaN+1
[1] NaN
R> 2+6*(4-4)/0
[1] NaN
R> 3.5^(-Inf/Inf)
[1] NaN
在第一行,将 1 加到“不是一个数字”上仍然是NaN。在第二行,你从(4-4)/0中得到NaN,这显然是0/0,因此结果也是NaN。在第三行,-Inf/Inf的结果是NaN,因此剩余的计算结果再次是NaN。这开始让你明白NaN或无穷大的值是如何无意中出现的。如果你有一个函数,其中多个值被传递给固定计算,并且没有防止出现例如0/0的情况,那么代码将返回NaN。
与Inf一样,使用特殊函数(is.nan)来检测NaN值的存在。然而,与无穷大值不同,关系运算符不能与NaN一起使用。这里是一个使用之前定义的bar的示例:
R> bar
[1] NaN 54.30 -2.00 NaN 90094.12 -Inf 55.00
R> is.nan(x=bar)
[1] TRUE FALSE FALSE TRUE FALSE FALSE FALSE
R> !is.nan(x=bar)
[1] FALSE TRUE TRUE FALSE TRUE TRUE TRUE
R> is.nan(x=bar)|is.infinite(x=bar)
[1] TRUE FALSE FALSE TRUE FALSE TRUE FALSE
R> bar[-(which(is.nan(x=bar)|is.infinite(x=bar)))]
[1] 54.30 -2.00 90094.12 55.00
使用is.nan函数对bar进行操作,会将两个NaN位置标记为TRUE。在第二个示例中,你使用否定操作符!来标记那些元素不是NaN的位置。然后,使用按元素的逻辑“或”运算符|(参见第 4.1.3 节),你可以识别那些既是NaN又是无限大的元素。最后,最后一行使用which将这些逻辑值转换为数字索引位置,这样你就可以通过方括号中的负索引将其移除(关于如何使用which的复习,请参见第 4.1.5 节)。
你可以通过在提示符下输入?Inf来获取更多关于NaN和Inf功能和行为的详细信息。
练习 6.1
-
存储以下向量:
foo <- c(13563,-14156,-14319,16981,12921,11979,9568,8833,-12968, 8133)然后,进行以下操作:
-
输出所有
foo中的元素,这些元素在升到 75 次方时不为无限大。 -
返回
foo的元素,排除那些在升到 75 次方时结果为负无穷大的元素。
-
-
将以下 3 × 4 矩阵存储为对象
bar:![image]()
现在,进行以下操作:
-
当将
bar的元素升到 65 次方并除以无穷大时,识别出NaN的坐标特定索引。 -
返回
bar中那些在bar升到 67 次方并加上无穷大后不为NaN的值。确认这与识别那些在升到 67 次方时不等于负无穷大的bar值是相同的。 -
在将
bar的元素升到 67 次方时,识别出那些负无穷大或有限的值。
-
6.1.3 NA
在统计分析中,数据集经常包含缺失值。例如,有人填写问卷时可能未回答某个项目,或者研究人员可能错误地记录了实验中的一些观察值。识别和处理缺失值很重要,这样你就可以继续使用其余数据。R 提供了一个标准的特殊术语来表示缺失值,NA,即不可用。
NA条目与NaN条目不同。NaN仅用于数字操作,而缺失值可以出现在任何类型的观察值中。因此,NA可以存在于数字和非数字设置中。以下是一个示例:
R> foo <- c("character","a",NA,"with","string",NA)
R> foo
[1] "character" "a" NA "with" "string" NA
R> bar <- factor(c("blue",NA,NA,"blue","green","blue",NA,"red","red",NA,
"green"))
R> bar
[1] blue <NA> <NA> blue green blue <NA> red red <NA> green
Levels: blue green red
R> baz <- matrix(c(1:3,NA,5,6,NA,8,NA),nrow=3,ncol=3)
R> baz
[,1] [,2] [,3]
[1,] 1 NA NA
[2,] 2 5 8
[3,] 3 6 NA
对象foo是一个字符向量,其中第 3 和第 6 项缺失;bar是一个长度为 11 的因子向量,其中第 2、3、7 和 10 项缺失;baz是一个数值矩阵,第 1 行、第 2 和 3 列、第 3 行第 3 列缺失元素。在因子向量中,请注意NA被打印为<NA>。这是为了区分因子的真实水平和缺失的观察值,防止将NA误解为某个因子水平。
与前面讨论的其他特殊值一样,你可以使用is.na函数来识别NA元素。这通常对删除或替换NA值很有用。考虑以下数字向量:
R> qux <- c(NA,5.89,Inf,NA,9.43,-2.35,NaN,2.10,-8.53,-7.58,NA,-4.58,2.01,NaN)
R> qux
[1] NA 5.89 Inf NA 9.43 -2.35 NaN 2.10 -8.53 -7.58 NA -4.58
[13] 2.01 NaN
该向量总共有 14 个条目,包括NA、NaN和Inf。
R> is.na(x=qux)
[1] TRUE FALSE FALSE TRUE FALSE FALSE TRUE FALSE FALSE FALSE TRUE FALSE
[13] FALSE TRUE
正如你所看到的,is.na将qux中相应的NA条目标记为TRUE。但这并非全部——请注意,它还标记了元素 7 和 14,它们是NaN,而不是NA。严格来说,NA和NaN是不同的实体,但在数值上它们几乎是相同的,因为你几乎无法对这两者做任何事情。使用is.na将它们都标记为TRUE,允许用户同时删除或重新编码这两者。
如果你想单独标识NA和NaN条目,可以将is.nan与逻辑运算符结合使用。以下是一个示例:
R> which(x=is.nan(x=qux))
[1] 7 14
这标识出具体为NaN的元素索引位置。如果你只想标识NA条目,可以尝试以下方法:
R> which(x=(is.na(x=qux)&!is.nan(x=qux)))
[1] 1 4 11
这仅标识NA条目的元素索引(通过检查is.na为TRUE并且is.nan不为TRUE的条目)。
在找到问题元素后,你可以使用方括号中的负索引将它们移除,尽管 R 提供了一个更直接的选项。函数na.omit将接收一个结构并删除其中所有的NA;如果元素是数字类型,na.omit也会应用于NaN。
R> quux <- na.omit(object=qux)
R> quux
[1] 5.89 Inf 9.43 -2.35 2.10 -8.53 -7.58 -4.58 2.01
attr(,"na.action")
[1] 1 4 7 11 14
attr(,"class")
[1] "omit"
请注意,传递给na.omit的结构作为参数object给出,并且打印返回对象时会显示一些额外的输出。这些额外的细节旨在告知用户原始向量中有一些元素被删除(在本例中,被删除的元素位置在属性na.action中提供)。属性将在第 6.2.1 节中进一步讨论。
类似于NaN,对NA进行算术计算也会得到NA。使用关系运算符与NaN或NA进行操作时,结果也会是NA。
R> 3+2.1*NA-4
[1] NA
R> 3*c(1,2,NA,NA,NaN,6)
[1] 3 6 NA NA NaN 18
R> NA>76
[1] NA
R> 76>NaN
[1] NA
你可以通过输入?NA来获取更多关于NA值的使用和技术细节。
6.1.4 NULL
最后,我们来看一下null值,即NULL。这个值通常用来显式定义一个“空”的实体,这与用NA表示的“缺失”实体有所不同。NA的实例清晰地表示一个可以访问和/或在必要时被覆盖的位置——而NULL则不是如此。如果你将NA的赋值与NULL的赋值进行比较,你会看到这点的体现。
R> foo <- NULL
R> foo
NULL
R> bar <- NA
R> bar
[1] NA
请注意,bar,即NA对象,以索引位置[1]打印。这表明你有一个包含单个元素的向量。相比之下,你显式地用NULL指示foo为空。打印该对象时不会显示位置索引,因为没有可访问的位置。
对NULL的这种解释同样适用于那些具有其他明确定义项目的向量。考虑以下两行代码:
R> c(2,4,NA,8)
[1] 2 4 NA 8
R> c(2,4,NULL,8)
[1] 2 4 8
第一行创建了一个长度为 4 的向量,第三个位置编码为NA。第二行创建了一个类似的向量,但使用NULL代替NA。结果是一个长度只有 3 的向量。这是因为NULL无法在向量中占据一个位置。因此,将NULL赋值给向量中的多个位置(或任何其他结构)是没有意义的。再举一个例子:
R> c(NA,NA,NA)
[1] NA NA NA
R> c(NULL,NULL,NULL)
NULL
第一行可以解释为“有三个可能的槽位,其中包含未记录的观测值。”第二行只是简单地提供了“三次空值”,它被解释为一个单一的、无法子集化的空对象。
此时,你可能会想,为什么甚至需要NULL。如果某个东西为空且不存在,为什么一开始要定义它?答案在于需要能够明确声明或检查某个对象是否已被定义。这在调用 R 中的函数时经常发生。例如,当一个函数包含可选参数时,函数内部必须检查这些参数中哪些已提供,哪些缺失或为空。NULL值是一个有用且灵活的工具,函数的作者可以利用它来方便地进行此类检查。你将在第十一章看到这些的示例。
is.null函数用于检查某个东西是否明确为NULL。假设你有一个包含名为opt.arg的可选参数的函数,如果提供了,opt.arg应该是一个长度为 3 的字符向量。假设用户以以下方式调用该函数。
R> opt.arg <- c("string1","string2","string3")
现在,如果你检查是否使用NA提供了参数,你可能会调用这个:
R> is.na(x=opt.arg)
[1] FALSE FALSE FALSE
NA的位置信息性质意味着此检查是逐元素进行的,并且会为opt.arg中的每个值返回一个答案。这是有问题的,因为你只想要一个答案——opt.arg是空的,还是已经提供?这时NULL就派上用场了。
R> is.null(x=opt.arg)
[1] FALSE
很明显,opt.arg不是空的,函数可以按需继续。如果参数为空,使用NULL而不是NA进行检查,出于这些目的,通常更为合适。
R> opt.arg <- c(NA,NA,NA)
R> is.na(x=opt.arg)
[1] TRUE TRUE TRUE
R> opt.arg <- c(NULL,NULL,NULL)
R> is.null(x=opt.arg)
[1] TRUE
如前所述,填充向量时使用NULL并不是一种常规做法;这里这样做只是为了说明。但NULL的使用远不止限于此特定示例。在 R 中,无论是现成的功能还是用户贡献的功能中,它都被广泛使用。
如果将空的NULL包含在算术或关系比较中,它有一个有趣的效果。
R> NULL+53
numeric(0)
R> 53<=NULL
logical(0)
结果并不是你可能预期的NULL,而是一个“空的”向量,其类型由所尝试的操作的性质决定。NULL通常主导任何算术运算,即使它包含其他特殊值。
R> NaN-NULL+NA/Inf
numeric(0)
NULL在检查列表和数据框时也自然出现。例如,定义一个新的列表foo。
R> foo <- list(member1=c(33,1,5.2,7),member2="NA or NULL?")
R> foo
$member1
[1] 33.0 1.0 5.2 7.0
$member2
[1] "NA or NULL?"
这个列表显然不包含名为member3的成员。看看当你尝试通过该名称访问foo中的成员时会发生什么:
R> foo$member3
NULL
NULL 的结果表示在 foo 中没有名为 member3 的成员,或者用 R 的术语来说,就是为空。因此,你可以用任何你想要的内容填充它。
R> foo$member3 <- NA
R> foo
$member1
[1] 33.0 1.0 5.2 7.0
$member2
[1] "NA or NULL?"
$member3
[1] NA
当使用美元符号操作符查询数据框中不存在的列或变量时,同样的原则适用(如 第 5.2.2 节所示)。
有关 NULL 和 is.null 在 R 中如何处理的更多技术细节,请参阅通过 ?NULL 访问的帮助文件。
练习 6.2
-
考虑以下代码行:
foo <- c(4.3,2.2,NULL,2.4,NaN,3.3,3.1,NULL,3.4,NA)自己判断以下哪些语句是真,哪些是假的,然后使用 R 来确认:
-
foo的长度是 8。 -
调用
which(x=is.na(x=foo))不会返回4和8。 -
检查
is.null(x=foo)将提供你当前存在的两个NULL值的位置。 -
执行
is.na(x=foo[8])+4/NULL不会导致NA。
-
-
创建并存储一个包含单个成员的列表:向量
c(7,7,NA,3,NA,1,1,5,NA)。然后,执行以下操作:-
将成员命名为
"alpha"。 -
使用适当的逻辑值函数确认列表中没有名为
"beta"的成员。 -
添加一个名为
beta的新成员,该成员是通过标识alpha中为NA的索引位置得到的向量。
-
6.2 理解类型、类和强制转换
到目前为止,你已经学习了 R 语言中用于表示、存储和处理数据的许多基本特性。在本节中,你将研究如何正式区分不同类型的值和结构,并查看一些从一种类型转换到另一种类型的简单示例。
6.2.1 属性
你创建的每个 R 对象都有关于该对象本身性质的附加信息。这些附加信息被称为对象的 属性。你已经见过一些属性。在 第 3.1.3 节中,你通过 dim 确定了矩阵的维度属性。在 第 4.3.1 节中,你使用 levels 获取了因子的层次属性。还在 第 5.1.2 节中提到过,names 可以获取列表的成员名,在 第 6.1.3 节中,属性注释了应用 na.omit 的结果。
一般来说,你可以将属性视为 显式 或 隐式 的。显式属性对用户是立即可见的,而 R 内部决定隐式属性。你可以使用 attributes 函数打印给定对象的显式属性,该函数接受任何对象并返回一个命名的列表。例如,考虑以下的 3 × 3 矩阵:
R> foo <- matrix(data=1:9,nrow=3,ncol=3)
R> foo
[,1] [,2] [,3]
[1,] 1 4 7
[2,] 2 5 8
[3,] 3 6 9
R> attributes(foo)
$dim
[1] 3 3
在这里,调用 attributes 返回一个包含一个成员的列表:dim。当然,你可以通过 attributes(foo)$dim 检索 dim 的内容,但如果你知道一个属性的名称,也可以使用 attr:
R> attr(x=foo,which="dim")
[1] 3 3
该函数将对象作为 x 输入,并将属性名称作为 which。回忆一下,在 R 中,名称是作为字符字符串指定的。为了更加方便,最常见的属性都有自己的函数(通常以属性名命名)来访问对应的值。对于矩阵的维度,你已经看到过函数 dim。
R> dim(foo)
[1] 3 3
这些特定属性的函数很有用,因为它们还允许访问隐式属性,这些属性虽然仍然可以由用户控制,但作为必要性,软件会自动设置它们。前面提到的 names 和 levels 函数也是特定属性的函数。
显式属性通常是可选的;如果没有指定,它们默认为 NULL。例如,在使用 matrix 函数构建矩阵时,你可以使用可选参数 dimnames 来为行和列添加名称。你将 dimnames 传递一个由两个成员组成的列表,每个成员都是一个适当长度的字符向量——第一个给出行名,第二个给出列名。我们可以如下定义矩阵 bar:
R> bar <- matrix(data=1:9,nrow=3,ncol=3,dimnames=list(c("A","B","C"),
c("D","E","F")))
R> bar
D E F
A 1 4 7
B 2 5 8
C 3 6 9
因为维度名称是属性,所以当你调用 attributes(bar) 时,dimnames 会出现。
R> attributes(bar)
$dim
[1] 3 3
$dimnames
$dimnames[[1]]
[1] "A" "B" "C"
$dimnames[[2]]
[1] "D" "E" "F"
请注意,dimnames 本身是一个列表,嵌套在更大的属性列表中。同样,为了提取这个属性的值,你可以使用列表成员引用,使用前面提到的 attr,或者使用特定属性的函数。
R> dimnames(bar)
[[1]]
[1] "A" "B" "C"
[[2]]
[1] "D" "E" "F"
有些属性可以在对象创建后进行修改(如你在第 5.1.2 节中看到的那样,在那里你重新命名了列表的成员)。在这里,为了使 foo 完全匹配 bar,你可以通过将 dimnames 分配给特定属性的函数来给 foo 添加一些 dimnames:
R> dimnames(foo) <- list(c("A","B","C"),c("D","E","F"))
R> foo
D E F
A 1 4 7
B 2 5 8
C 3 6 9
在这里的讨论中我使用了矩阵,但在 R 中,其他对象的可选属性也以相同的方式处理。属性不仅仅局限于内置的 R 对象。你自己构建的对象也可以定义自己的属性和特定属性的函数。只要记住,属性的作用通常是提供关于对象的描述性数据,否则你可能会不必要地使对象结构变得复杂。
6.2.2 对象类
一个对象的 class 是描述 R 中实体最有用的属性之一。你创建的每个对象都会被标识,隐式或显式地至少有一个类。R 是一种 面向对象 的编程语言,意味着实体作为对象存储,并且具有可操作它们的方法。在这种语言中,类的标识正式被称为 继承。
注意
本节将重点介绍 R 中最常用的分类结构——S3 结构。 S4 是另一种结构,基本上是对不同对象的识别和处理的更正式的规则集。对于大多数实际用途,尤其是对于初学者,理解和使用 S3 就足够了。你可以在 R 的在线文档中找到更多细节。
在一些情况下,对象的类别是显式的,比如在你有用户自定义的对象结构或因子向量或数据框等对象时,其他属性在处理对象本身时发挥着重要作用——例如,因子向量的级别标签或数据框中的变量名是可修改的属性,在访问每个对象的观测值时起着主要作用。另一方面,像向量、矩阵和数组这样的基础 R 对象是隐式分类的,这意味着类别不能通过attributes函数来识别。无论是隐式还是显式,给定对象的类别始终可以通过特定的属性函数class来获取。
独立向量
让我们创建一些简单的向量作为示例。
R> num.vec1 <- 1:4
R> num.vec1
[1] 1 2 3 4
R> num.vec2 <- seq(from=1,to=4,length=6)
R> num.vec2
[1] 1.0 1.6 2.2 2.8 3.4 4.0
R> char.vec <- c("a","few","strings","here")
R> char.vec
[1] "a" "few" "strings" "here"
R> logic.vec <- c(T,F,F,F,T,F,T,T)
R> logic.vec
[1] TRUE FALSE FALSE FALSE TRUE FALSE TRUE TRUE
R> fac.vec <- factor(c("Blue","Blue","Green","Red","Green","Yellow"))
R> fac.vec
[1] Blue Blue Green Red Green Yellow
Levels: Blue Green Red Yellow
你可以将任何对象传递给class函数,它会返回一个字符向量作为输出。以下是使用刚刚创建的向量的示例:
R> class(num.vec1)
[1] "integer"
R> class(num.vec2)
[1] "numeric"
R> class(char.vec)
[1] "character"
R> class(logic.vec)
[1] "logical"
R> class(fac.vec)
[1] "factor"
使用class函数在字符向量、逻辑向量和因子向量上的输出结果仅仅是数据存储的类型。然而,数值向量的输出稍微复杂一些。到目前为止,我将所有包含算术有效数字的对象称为"numeric"。如果向量中存储的所有数字都是整数,那么 R 会将该向量识别为"integer"。另一方面,带有小数点的数字(称为浮动点数字)会被识别为"numeric"。这种区分是必要的,因为某些任务严格要求使用整数,而非浮动点数字。在口语上,我将继续把这两种类型称为"numeric",事实上,is.numeric函数会对整数和浮动点结构都返回TRUE,正如你在第 6.2.3 节中所看到的。
其他数据结构
如前所述,R 的类别本质上是为了方便面向对象编程而设计的。因此,class通常报告的是数据结构的性质,而不是存储的数据类型——它仅在用于独立向量时才返回数据类型。让我们在一些矩阵上试试这个函数。
R> num.mat1 <- matrix(data=num.vec1,nrow=2,ncol=2)
R> num.mat1
[,1] [,2]
[1,] 1 3
[2,] 2 4
R> num.mat2 <- matrix(data=num.vec2,nrow=2,ncol=3)
R> num.mat2
[,1] [,2] [,3]
[1,] 1.0 2.2 3.4
[2,] 1.6 2.8 4.0
R> char.mat <- matrix(data=char.vec,nrow=2,ncol=2)
R> char.mat
[,1] [,2]
[1,] "a" "strings"
[2,] "few" "here"
R> logic.mat <- matrix(data=logic.vec,nrow=4,ncol=2)
R> logic.mat
[,1] [,2]
[1,] TRUE TRUE
[2,] FALSE FALSE
[3,] FALSE TRUE
[4,] FALSE TRUE
请注意,在第 4.3.1 节中提到,因子只能以向量形式使用,因此fac.vec不包括在这里。现在检查这些矩阵的class。
R> class(num.mat1)
[1] "matrix"
R> class(num.mat2)
[1] "matrix"
R> class(char.mat)
[1] "matrix"
R> class(logic.mat)
[1] "matrix"
你会发现,无论数据类型如何,class报告的是对象本身的结构——所有的矩阵都是如此。其他对象结构,如数组、列表和数据框,也一样。
多种类
某些对象会有多个类。一个对象的变种形式,如有序因子向量,会继承常规因子类,并且还会包含额外的 ordered 类。如果使用 class 函数,它们都会被返回。
R> ordfac.vec <- factor(x=c("Small","Large","Large","Regular","Small"),
levels=c("Small","Regular","Large"),
ordered=TRUE)
R> ordfac.vec
[1] Small Large Large Regular Small
Levels: Small < Regular < Large
R> class(ordfac.vec)
[1] "ordered" "factor"
之前,fac.vec 被标识为仅为 "factor",但 ordfac.vec 的类有两个组成部分。它仍然被标识为 "factor",但还包括 "ordered",这标识了该对象中存在的 "factor" 类的变体。这里,您可以将 "ordered" 看作是 "factor" 的 子类。换句话说,它是一个从 "factor" 继承并因此表现得像 "factor" 的特殊情况。有关 R 子类的更多技术细节,我推荐 Matloff 的 《R 编程艺术》(2011)第九章。
注意
我在这里重点讲解 class 函数,因为它与本书中采用的面向对象编程风格直接相关,特别是在第二部分中。还有其他一些函数展示了 R 类规则的复杂性。例如,函数 typeof 报告对象中包含的数据类型,不仅适用于向量,还适用于矩阵和数组。然而请注意, typeof 输出中的术语不一定与 class 的输出相匹配。有关它返回值的详细信息,请参阅帮助文件 ?typeof 。
总结一下,一个对象的类首先是数据结构的描述符,尽管对于简单的向量,class 函数报告的是存储的数据类型。如果向量中的条目完全是整数,那么 R 会将该向量归类为 "integer",而 "numeric" 则用来标记包含浮点数的向量。
6.2.3 Is-Dot 对象检查函数
确定对象的类对于操作存储对象的函数至关重要,尤其是那些根据对象类的不同表现不同的函数。要检查对象是否属于特定的类或数据类型,可以对该对象使用 is-dot 函数,返回 TRUE 或 FALSE 逻辑值。
对于几乎任何合理的检查,都会有对应的 is-dot 函数。例如,再次考虑 第 6.2.2 节中的 num.vec1 向量和以下六个检查:
R> num.vec1 <- 1:4
R> num.vec1
[1] 1 2 3 4
R> is.integer(num.vec1)
[1] TRUE
R> is.numeric(num.vec1)
[1] TRUE
R> is.matrix(num.vec1)
[1] FALSE
R> is.data.frame(num.vec1)
[1] FALSE
R> is.vector(num.vec1)
[1] TRUE
R> is.logical(num.vec1)
[1] FALSE
第一个、第二个和第六个 is-dot 函数检查对象中存储的数据类型,而其他函数检查对象本身的结构。结果是可以预期的:num.vec1 是 “整数”(并且 是 “数值型”),它 是 一个“向量”。它不是矩阵或数据框,也不是逻辑型。
简单来说,值得注意的是,这些检查使用的类别比通过class标识的正式类要更一般化。回想一下,在第 6.2.2 节中,num.vec1仅被标识为"integer",但在这里使用is.numeric仍然返回TRUE。在这个例子中,带有整数数据的num.vec1被概括为"numeric"。类似地,对于数据框,类为"data.frame"的对象会对is.data.frame 和 is.list返回TRUE,因为数据框本质上被概括为列表。
这里详细说明的对象 is-dot 函数与在第 6.1 节中讨论的诸如is.na等函数之间存在差异。用于检查特殊值(如NA)的函数应该被视为一个等式检查;它们存在的原因是写出像foo==NA这样的语法是非法的。因此,来自第 6.1 节的那些函数以元素逐一的方式在 R 中操作,而对象 is-dot 函数则检查对象 本身,并只返回一个逻辑值。
6.2.4 As-Dot 强制转换函数
你已经看到了在对象创建后修改对象的不同方式——例如,通过访问和覆盖元素。但对象本身的结构以及其中包含的数据类型呢?
从一个对象或数据类型转换到另一个对象或数据类型被称为强制转换。像你迄今遇到的其他 R 功能一样,强制转换可以是隐式的,也可以是显式的。隐式强制转换会在元素需要转换为另一种类型以完成操作时自动发生。事实上,你已经遇到过这种行为,例如在第 4.1.4 节中,当你使用数值代替逻辑值时。记住,逻辑值可以被视为整数——TRUE为 1,FALSE为 0。逻辑值隐式转换为其数值对等物的强制转换在像这样的代码行中发生:
R> 1:4+c(T,F,F,T)
[1] 2 2 3 5
在这个操作中,R 识别出你正在尝试进行一个算术计算(使用+),因此它期望数值型量。由于逻辑向量不是这种形式,软件会在内部将其强制转换为 1 和 0,然后再完成任务。
另一个常见的隐式强制转换例子是,当使用paste和cat将字符字符串拼接在一起时,如第 4.2.2 节中探讨的那样。非字符项在拼接之前会自动转换为字符串。这里是一个例子:
R> foo <- 34
R> bar <- T
R> paste("Definitely foo: ",foo,"; definitely bar: ",bar,".",sep="")
[1] "Definitely foo: 34; definitely bar: TRUE."
在这里,整数34和逻辑值T被隐式地转换为字符,因为 R 知道paste的输出必须是字符串。
在其他情况下,强制转换不会自动发生,必须由用户进行。这种显式强制转换可以通过as-dot函数来实现。像 is-dot 函数一样,as-dot 函数也适用于大多数典型的 R 数据类型和对象类。前面的两个例子可以显式地进行强制转换,如下所示。
R> as.numeric(c(T,F,F,T))
[1] 1 0 0 1
R> 1:4+as.numeric(c(T,F,F,T))
[1] 2 2 3 5
R> foo <- 34
R> foo.ch <- as.character(foo)
R> foo.ch
[1] "34"
R> bar <- T
R> bar.ch <- as.character(bar)
R> bar.ch
[1] "TRUE"
R> paste("Definitely foo: ",foo.ch,"; definitely bar: ",bar.ch,".",sep="")
[1] "Definitely foo: 34; definitely bar: TRUE."
在大多数情况下,强制转换是“合理”的。例如,很容易理解为什么 R 能够读取如下内容:
R> as.numeric("32.4")
[1] 32.4
然而,以下转换是没有意义的:
R> as.numeric("g'day mate")
[1] NA
Warning message:
NAs introduced by coercion
由于没有逻辑方式将“g’day mate”翻译成数字,因此该条目返回 NA(在这种情况下,R 还发出了警告信息)。这意味着在某些情况下,可能需要多次强制转换才能获得最终结果。例如,假设你有字符型向量 c("1","0","1","0","0"),并希望将其强制转换为逻辑值向量。直接的字符到逻辑的强制转换是不可行的,因为即使所有字符字符串都包含数字,也无法保证它们全都是 1 和 0。
R> as.logical(c("1","0","1","0","0"))
[1] NA NA NA NA NA
然而,您知道字符型数字可以转换为数字数据类型,且 1 和 0 很容易被强制转换为逻辑值。因此,您可以按照以下两步执行强制转换:
R> as.logical(as.numeric(c("1","0","1","0","0")))
[1] TRUE FALSE TRUE FALSE FALSE
不是所有的数据类型强制转换都完全简单明了。例如,因子更复杂,因为 R 将级别视为整数。换句话说,无论给定因子的级别如何标记,软件内部都会将其作为级别 1、级别 2 等处理。如果尝试将因子强制转换为数字数据类型,这一点会很明显。
R> baz <- factor(x=c("male","male","female","male"))
R> baz
[1] male male female male
Levels: female male
R> as.numeric(baz)
[1] 2 2 1 2
在这里,您可以看到 R 已根据因子标签的存储顺序(默认按字母顺序)分配了因子的数字表示。级别 1 代表 female,级别 2 代表 male。这个例子相对简单,但重要的是要意识到这种行为,因为从具有数字级别的因子进行强制转换可能会导致混淆。
R> qux <- factor(x=c(2,2,3,5))
R> qux
[1] 2 2 3 5
Levels: 2 3 5
R> as.numeric(qux)
[1] 1 1 2 3
因子的数字表示 qux 是 c(1,1,2,3)。这再次强调,qux 的级别简单地被视为级别 1(即使它的标签是 2)、级别 2(标签为 3)和级别 3(标签为 5)。
对象类和结构之间的强制转换也非常有用。例如,您可能需要将矩阵的内容存储为单一的向量。
R> foo <- matrix(data=1:4,nrow=2,ncol=2)
R> foo
[,1] [,2]
[1,] 1 3
[2,] 2 4
R> as.vector(foo)
[1] 1 2 3 4
请注意,as.vector 通过“堆叠”列将矩阵强制转换为单一向量。对于更高维度的数组,也会发生相同的按列解构,按层次或区块的顺序进行。
R> bar <- array(data=c(8,1,9,5,5,1,3,4,3,9,8,8),dim=c(2,3,2))
R> bar
, , 1
[,1] [,2] [,3]
[1,] 8 9 5
[2,] 1 5 1
, , 2
[,1] [,2] [,3]
[1,] 3 3 8
[2,] 4 9 8
R> as.matrix(bar)
[,1]
[1,] 8
[2,] 1
[3,] 9
[4,] 5
[5,] 5
[6,] 1
[7,] 3
[8,] 4
[9,] 3
[10,] 9
[11,] 8
[12,] 8
R> as.vector(bar)
[1] 8 1 9 5 5 1 3 4 3 9 8 8
您可以看到,as.matrix 将数组存储为 12 × 1 的矩阵,而 as.vector 将其存储为单一向量。类似的常识性数据类型规则也适用于在处理对象结构时的强制转换。例如,将以下列表 baz 强制转换为数据框会产生错误:
R> baz <- list(var1=foo,var2=c(T,F,T),var3=factor(x=c(2,3,4,4,2)))
R> baz
$var1
[,1] [,2]
[1,] 1 3
[2,] 2 4
$var2
[1] TRUE FALSE TRUE
$var3
[1] 2 3 4 4 2
Levels: 2 3 4
R> as.data.frame(baz)
Error in data.frame(var1 = 1:4, var2 = c(TRUE, FALSE, TRUE), var3 = c(1L, :
arguments imply differing number of rows: 2, 3, 5
错误发生是因为变量的长度不匹配。但对于这里所示的 qux 列表,它的成员长度相同,所以没有问题。
R> qux <- list(var1=c(3,4,5,1),var2=c(T,F,T,T),var3=factor(x=c(4,4,2,1)))
R> qux
$var1
[1] 3 4 5 1
$var2
[1] TRUE FALSE TRUE TRUE
$var3
[1] 4 4 2 1
Levels: 1 2 4
R> as.data.frame(qux)
var1 var2 var3
1 3 TRUE 4
2 4 FALSE 4
3 5 TRUE 2
4 1 TRUE 1
这将以列方式存储变量,按你的列表提供的顺序将其作为成员存入数据集。
关于对象类别、数据类型和强制转换的讨论并不全面,但它作为一个有用的介绍,帮助你了解 R 是如何处理与所创建对象的正式识别、描述和处理相关的问题的——这些问题在大多数高级语言中都是存在的。一旦你对 R 更加熟悉,可以通过帮助文件(例如,输入 ?as 以访问的帮助文件)获取更多有关对象处理的详细信息。
练习 6.3
-
确定以下对象的类别。对于每个对象,还需要说明该类别是显式定义的还是隐式定义的。
-
foo <- array(data=1:36,dim=c(3,3,4)) -
bar <- as.vector(foo) -
baz <- as.character(bar) -
qux <- as.factor(baz) -
quux <- bar+c(-0.1,0.1)
-
-
对于在(a)中定义的每个对象,分别调用
is.numeric和is.integer来计算结果的总和。例如,is.numeric(foo)+is.integer(foo)将计算(i)的总和。将这五个结果的集合转换为一个因子,具有0、1和2这三个级别,并由结果本身标识。将此因子向量与将其强制转换为数值向量的结果进行比较。 -
执行以下操作:
[,1] [,2] [,3] [,4] [1,] 2 5 8 11 [2,] 3 6 9 12 [3,] 4 7 10 13转换为以下内容:
[1] "2" "5" "8" "11" "3" "6" "9" "12" "4" "7" "10" "13" -
存储以下矩阵:
![image]()
然后,执行以下操作:
-
将矩阵强制转换为数据框。
-
作为数据框,将第二列强制转换为逻辑值。
-
作为数据框,将第三列强制转换为因子值。
-
本章重要代码
| 函数/运算符 | 简要描述 | 首次出现 |
|---|---|---|
Inf, -Inf |
±无穷大的值 | 第 6.1.1 节, 第 104 页 |
is.infinite |
元素级别检查 Inf |
第 6.1.1 节, 第 105 页 |
is.finite |
元素级别检查是否有限 | 第 6.1.1 节, 第 105 页 |
NaN |
无效数值的值 | 第 6.1.2 节, 第 106 页 |
is.nan |
元素级别检查 NaN |
第 6.1.2 节, 第 107 页 |
NA |
缺失观测的值 | 第 6.1.3 节, 第 108 页 |
is.na |
元素级别检查 NA 或 NaN |
第 6.1.3 节, 第 109 页 |
na.omit |
删除所有 NA 和 NaN |
第 6.1.3 节, 第 110 页 |
NULL |
“空”值 | 第 6.1.4 节, 第 110 页 |
is.null |
检查 NULL |
第 6.1.4 节, 第 111 页 |
attributes |
列出显式属性 | 第 6.2.1 节, 第 114 页 |
attr |
获取特定属性 | 第 6.2.1 节, 第 115 页 |
dimnames |
获取数组维度名称 | 第 6.2.1 节,第 116 页 |
class |
获取对象类别(S3) | 第 6.2.2 节,第 117 页 |
is._ |
对象检查函数 | 第 6.2.3 节,第 120 页 |
as._ |
对象强制转换函数 | 第 6.2.4 节,第 121 页 |
第七章:7
基础绘图

R 的一个特别受欢迎的功能是其极其灵活的数据和模型可视化绘图工具。这正是许多人最初选择 R 的原因。掌握 R 的图形功能确实需要一些练习,但基本概念是直接明了的。在本章中,我将概述plot函数以及一些控制最终图形外观的有用选项。然后,我将介绍使用ggplot2的基础知识,ggplot2是一个强大的 R 数据可视化库。本章将仅涵盖绘图的基础知识,之后你将在第十四章中学习更多关于创建不同类型的统计图形的内容,以及在第五部分中学习高级绘图技术。
7.1 使用坐标向量绘图
在 R 中生成绘图的最简单方法是把你的屏幕当作一个空白的二维画布。你可以使用x和y坐标来绘制点和线。在纸上,这些坐标通常表示为一对数值:(x值,y值)。而 R 的plot函数则接收两个向量——一个x位置的向量和一个y位置的向量——并打开一个图形设备来显示结果。如果图形设备已经打开,R 的默认行为是刷新设备,用新图形覆盖当前内容。
例如,假设你想绘制以下五个点:(1.1,2),(2,2.2),(3.5, − 1.3),(3.9,0),和(4.2,0.2)。在plot中,你必须首先提供x位置的向量,其次提供y位置的向量。我们将这两个向量分别定义为foo和bar:
R> foo <- c(1.1,2,3.5,3.9,4.2)
R> bar <- c(2,2.2,-1.3,0,0.2)
R> plot(foo,bar)
图 7-1 显示了绘制结果的图形设备(我将在本节中使用这个简单的数据集作为工作示例)。

图 7-1:使用 R 默认行为绘制的五个点
x和y位置不一定需要作为单独的向量来指定。你也可以以矩阵的形式提供坐标,其中x值在第一列,y值在第二列,或者以列表的形式提供。例如,设置五个点的矩阵,下面的代码正好重现图 7-1(注意,窗口窗格在不同的操作系统上可能看起来略有不同):
R> baz <- cbind(foo,bar)
R> baz
foo bar
[1,] 1.1 2.0
[2,] 2.0 2.2
[3,] 3.5 -1.3
[4,] 3.9 0.0
[5,] 4.2 0.2
R> plot(baz)
plot函数是 R 的多功能通用函数之一。它对不同的对象工作方式不同,并允许用户为处理对象(包括用户定义的对象类)定义自己的方法。从技术上讲,你刚刚使用的plot命令的版本在内部被标识为plot.default。帮助文件?plot.default提供了有关这种散点图数据可视化风格的更多细节。
7.2 图形参数
有许多图形参数可以作为参数传递给plot函数(或其他绘图函数,例如第 7.3 节中的函数)。这些参数用于简单的视觉增强,如给点上色和添加坐标轴标签,还可以控制图形设备的技术细节(第二十三章详细讲解了后者)。这里列出了一些常用的图形参数;我将在接下来的部分简要讨论每个参数:
type 告诉 R 如何绘制提供的坐标(例如,作为单独的点,或者通过线条连接,或同时显示点和线)。
main、xlab、ylab 分别用于包含绘图标题、水平轴标签和垂直轴标签的选项。
col 用于绘制点和线的颜色(或颜色)。
pch 代表点字符。此参数选择用于绘制单个点的字符。
cex 代表字符扩展。此参数控制绘制点字符的大小。
lty 代表线条类型。此参数指定用于连接点的线条类型(例如,实线、虚线或点线)。
lwd 代表线条宽度。此参数控制绘制线条的粗细。
xlim、ylim 分别提供绘图区域的水平范围和垂直范围的限制。
7.2.1 自动绘图类型
默认情况下,plot函数将绘制单个点,如图 7-1 所示。这是默认的绘图类型,但其他绘图类型将具有不同的外观。要控制绘图类型,您可以为type参数指定一个字符值选项。
例如,在许多情况下,连接每个坐标的线条是有意义的,例如在绘制时间序列数据时。为此,您可以指定绘图类型为"l"。使用第 7.1 节中的foo和bar,如下代码会在图 7-2 的左侧面板中生成图表:
R> plot(foo,bar,type="l")

图 7-2:使用五个相连的坐标生成的线图,设置为 type="l" (左侧) 或 type="b" (右侧)
type的默认值是"p",可以理解为“仅点”。由于您没有指定其他内容,因此在图 7-1 中的图表使用的是该类型。另一方面,在最后这个例子中,您设置了type="l"(意味着“仅线”)。其他选项包括"b",表示同时绘制点和线(如图 7-2 的右侧面板所示),以及"o",表示用线条覆盖点(这会消除type="b"中点和线之间的间隙)。type="n"选项则表示不绘制点或线,创建一个空白图,这对于需要逐步构建的复杂图形非常有用。
7.2.2 标题和坐标轴标签
默认情况下,基本图形没有主标题,坐标轴会用正在绘制的向量名称来标注。但主标题和更具描述性的坐标轴标签通常会使绘制的数据更易于理解。你可以通过将文本作为字符字符串提供给main来添加标题,xlab来添加x轴标签,ylab来添加y轴标签。请注意,这些字符串可能包含转义序列(在第 4.2.3 节中讨论)。以下代码生成了图 7-3 中的图表:
R> plot(foo,bar,type="b",main="My lovely plot",xlab="x axis label",
ylab="location y")
R> plot(foo,bar,type="b",main="My lovely plot\ntitle on two lines",xlab="",
ylab="")

图 7-3:带有坐标轴标签和标题的图表示例
在第二个图中,请注意新添加的线条转义序列将标题分割成两行。在该图中,xlab和ylab也被设置为空字符串"",以防止 R 自动用x和y向量的名称标记坐标轴。
7.2.3 颜色
给图形添加颜色远不只是美学考虑。颜色可以使数据更加清晰——例如,通过区分因子水平或强调重要的数值限制。你可以通过col参数以多种方式设置颜色。最简单的选项是使用整数选择器或字符字符串。R 语言识别多个颜色字符串值,你可以通过在提示符下输入colors()来查看这些颜色。默认颜色是整数1或字符字符串"black"。 图 7-4 的顶部行展示了通过以下代码创建的两种彩色图表示例:
R> plot(foo,bar,type="b",main="My lovely plot",xlab="",ylab="",col=2)
R> plot(foo,bar,type="b",main="My lovely plot",xlab="",ylab="",col="seagreen4")
有八种可能的整数值(见图 7-5 的最左侧图),并且大约有 650 种字符字符串可用于指定颜色。但你并不局限于这些选项,因为你还可以通过 RGB(红色、绿色和蓝色)水平以及创建你自己的调色板来指定颜色。我将在第二十五章中详细讨论后两种选项。

图 7-4:基本 R 绘图实验。顶部行:使用col=2(左)和col="seagreen4"(右)创建的两种彩色图表。中间行:进一步使用pch、lty、cex和lwd的两种示例。底部行:设置绘图区域限制xlim=c(-10,5)、ylim=c(-3,3)(左)和xlim=c(3,5)、ylim=c(-0.5,0.2)(右)。
7.2.4 线条和点的外观
若要改变绘制点的外观,可以使用pch参数,若要改变线条的外观,可以使用lty参数。pch参数控制绘制单个数据点时使用的字符。你可以指定一个字符来表示每个点,或者指定一个介于1到25(包含)之间的值。每个整数对应的符号显示在图 7-5 的中间图中。lty参数控制线条的类型,它的值可以是1到6。这些选项显示在图 7-5 的最右侧图中。

图 7-5:一些参考图形,显示了col(左)、pch(中)和lty(右)可能的整数选项结果
你还可以使用cex控制绘制点的大小,使用lwd控制线条的粗细。默认情况下,这两个的大小和粗细为1。例如,要请求半大小的点,可以指定cex=0.5;要指定双倍粗细的线条,则使用lwd=2。
以下两行代码生成了图 7-4 中间行的两个图形,展示了pch、lty、cex和lwd:
R> plot(foo,bar,type="b",main="My lovely plot",xlab="",ylab="",
col=4,pch=8,lty=2,cex=2.3,lwd=3.3)
R> plot(foo,bar,type="b",main="My lovely plot",xlab="",ylab="",
col=6,pch=15,lty=3,cex=0.7,lwd=2)
7.2.5 绘图区域的限制
正如你在foo和bar的图形中看到的,默认情况下,R 通过使用提供的X和Y值的范围(加上一小常数来为最外层的点周围留下一些区域)来设置每个轴的范围。但你可能需要更多空间,例如,为单独的点添加注释、添加图例或绘制超出原始范围的附加点(正如你将在第 7.3 节中看到的)。你可以使用xlim和ylim设置自定义的绘图区域限制。两个参数都需要一个长度为 2 的数值向量,表示为c(下限, 上限)。
考虑图 7-4 底部行中的图形,这些图形是通过以下两个命令创建的:
R> plot(foo,bar,type="b",main="My lovely plot",xlab="",ylab="",
col=4,pch=8,lty=2,cex=2.3,lwd=3.3,xlim=c(-10,5),ylim=c(-3,3))
R> plot(foo,bar,type="b",main="My lovely plot",xlab="",ylab="",
col=6,pch=15,lty=3,cex=0.7,lwd=2,xlim=c(3,5),ylim=c(-0.5,0.2))
这些图形与中间行的两个图形完全相同,唯一的区别在于,左下图中的X和Y轴被设置得比观察到的数据宽得多,而右侧的图形限制了绘图窗口,只显示数据的一部分。
7.3 向现有图形添加点、线和文本
一般来说,每次调用plot都会刷新活动图形设备以创建一个新的绘图区域。但这并不总是所需的——为了构建更复杂的图形,最容易的方法是从一个空的绘图区域开始,并逐步向该画布添加所需的点、线、文本和图例。以下是一些在 R 中可以用于向图形添加内容而不刷新或清除窗口的有用函数:
points 添加点
lines, abline, segments 添加线条
text 写入文本
arrows 添加箭头
legend 添加图例
这些函数的调用和设置参数的语法与plot相同。查看这些如何工作的最佳方式是通过一个扩展的示例,我将基于一些由 20 个(x,y)位置组成的假设数据。
R> x <- 1:20
R> y <- c(-1.49,3.37,2.59,-2.78,-3.94,-0.92,6.43,8.51,3.41,-8.23,
-12.01,-6.58,2.87,14.12,9.63,-4.58,-14.78,-11.67,1.17,15.62)
使用这些数据,你将构建出图 7-6 所示的图形(请注意,你可能需要手动放大图形设备并重新绘制,以确保图例不会与图像的其他部分重叠)。值得记住的是一个公认的绘图规则:“保持清晰简洁”。图 7-6 是一个例外,为了演示使用的 R 命令。

图 7-6:一些假设数据的复杂最终图形
在图 7-6 中,数据点将根据它们的 x 和 y 位置进行不同的绘制,具体取决于它们与图中指出的“甜点”位置的关系。y 值大于 5 的点用紫色 × 标记;y 值小于 −5 的点用绿色 + 标记。位于这两个 y 值之间但仍位于甜点之外的点用 ○ 标记。最后,位于甜点中的点(x 在 5 到 15 之间 且 y 在 −5 到 5 之间)用蓝色 • 标记。红色的水平和垂直线划定了甜点区域,并用箭头标出,同时也有一个图例。
用了十行代码来完整构建这个图形(再加上一行代码来添加图例)。该图形的每个步骤的样式可以参见图 7-7。接下来将详细介绍这些代码行。
-
第一步是创建一个空的绘图区域,你可以在其中添加点和绘制线条。这一行代码告诉 R 绘制
x和y的数据,尽管type选项被设置为"n"。如第 7.2 节所述,这将打开或刷新图形设备,并将坐标轴设置为适当的长度(带有标签和坐标轴),但不会绘制任何点或线。R> plot(x,y,type="n",main="") -
abline函数是一种简单的方式,用于在图形中添加跨越整个图形的直线。线条(或线条们)可以通过 斜率 和 截距 值来指定(详见后续在第二十章中的回归讨论)。你也可以简单地添加水平或垂直线。这行代码添加了两条单独的水平线,一条在 y = 5,另一条在 y = 5,使用h=c(-5,5)。这三个参数(在第 7.2 节中讲解过)使得这两条线为红色、虚线且具有双重厚度。对于垂直线,你也可以写v=c(-5,5),这将在 x = −5 和 x = 5 处绘制它们。![image]()
图 7-7:根据图 7-6 给出的最终绘图构建过程。图中的(1)到(10)对应文本中逐项列出的代码行。
R> abline(h=c(-5,5),col="red",lty=2,lwd=2) -
代码的第三行在步骤 2 绘制的水平线之间添加了较短的垂直线,以形成一个框。为此,使用
segments而不是abline,因为你不希望这些线跨越整个绘图区域。segments命令接受一个“起点”坐标(以x0和y0表示)和一个“终点”坐标(以x1和y1表示),并绘制相应的线。R 的矢量化行为将两组“起点”和“终点”坐标配对。两条线均为红色、虚线且具有双重厚度。(你也可以将长度为 2 的向量传递给这些参数,在这种情况下,第一个段落将使用第一个参数值,第二个段落将使用第二个值。)R> segments(x0=c(5,15),y0=c(-5,-5),x1=c(5,15),y1=c(5,5),col="red",lty=3, lwd=2) -
第四步,使用
points开始将特定坐标从x和y添加到图中。与plot一样,points需要两个具有相等长度的向量,分别包含x和y值。在这种情况下,你希望根据位置不同绘制不同的点,因此使用逻辑向量子集(参见第 4.1.5 节)来识别并提取那些y值大于或等于 5 的x和y元素。这些(且仅这些)点将作为紫色的×符号添加,并通过cex放大 2 倍。R> points(x[y>=5],y[y>=5],pch=4,col="darkmagenta",cex=2) -
第五行代码与第四行非常相似;这一次,它提取了那些y值小于或等于−5 的坐标。使用的是+点字符,并且颜色设置为深绿色。
R> points(x[y<=-5],y[y<=-5],pch=3,col="darkgreen",cex=2) -
第六步添加了蓝色的“甜蜜点”坐标,这些点的条件为
(x>=5&x<=15)&(y>-5&y<5)。这一稍微复杂的条件集提取了那些x值介于 5 到 15(包含 5 和 15)之间,且y值介于−5 到 5(不包括−5 和 5)之间的点。请注意,这一行使用了逻辑操作符&的“简短”形式,因为你希望进行逐元素比较(参见第 4.1.3 节)。R> points(x[(x>=5&x<=15)&(y>-5&y<5)],y[(x>=5&x<=15)&(y>-5&y<5)],pch=19, col="blue") -
接下来的命令识别数据集中其x值小于 5 或大于 15,且y值介于−5 和 5 之间的剩余点。没有指定图形参数,因此这些点会以默认的黑色○符号绘制。
R> points(x[(x<5|x>15)&(y>-5&y<5)],y[(x<5|x>15)&(y>-5&y<5)]) -
为了连接
x和y中的坐标,你使用lines。在这里,你还将lty设置为4,这会绘制一个点划线样式的线条。R> lines(x,y,lty=4) -
第九行代码添加指向最佳位置的箭头。
arrows函数的用法与segments相似,你需要提供“起始”坐标(x0,y0)和“终点”坐标(x1,y1)。默认情况下,箭头的箭头头部位于“终点”坐标处,尽管可以通过帮助文件?arrows中描述的可选参数修改这一点(以及箭头头部的角度和长度等其他选项)。R> arrows(x0=8,y0=14,x1=11,y1=2.5) -
第十行代码在箭头顶部的图表上打印标签。根据
text的默认行为,作为labels提供的字符串会在x和y提供的坐标上居中显示。R> text(x=8,y=15,labels="sweet spot")
最后,你可以使用legend函数添加图例,这会生成图 7-6 中所示的最终效果。
legend("bottomleft",
legend=c("overall process","sweet","standard",
"too big","too small","sweet y range","sweet x range"),
pch=c(NA,19,1,4,3,NA,NA),lty=c(4,NA,NA,NA,NA,2,3),
col=c("black","blue","black","darkmagenta","darkgreen","red","red"),
lwd=c(1,NA,NA,NA,NA,2,2),pt.cex=c(NA,1,1,2,2,NA,NA))
第一个参数设置图例的显示位置。有多种方法可以做到这一点(包括设置确切的x和y坐标),但通常只需要选择一个角落,使用以下四个字符串之一即可:"topleft"、"topright"、"bottomleft"或"bottomright"。接下来,你需要将标签作为字符向量传递给legend参数。然后,你需要为剩余的参数提供与标签长度相同的向量,这样正确的元素就能与每个标签匹配。
例如,对于第一个标签("overall process"),您希望使用默认粗细和颜色为 4 类型的一条线。因此,在剩余参数向量的第一个位置上,您设置pch=NA,lty=4,col="black",lwd=1和pt.cex=NA(所有这些都是默认值,除了lty)。这里,pt.cex 只是在调用 points 时使用 cex 参数(在 legend 中使用 cex 会扩展使用的文本,而不是点)。
请注意,当您不希望设置相应的图形参数时,必须在这些向量中的某些元素中填写 NA。这只是为了保持所提供的向量的等长,以便 R 可以跟踪每个特定参考的参数值。当您阅读本书时,您将看到更多使用 legend 的示例。
练习 7.1
-
尽可能地重新创建以下图形:
![图像]()
-
使用以下数据,在 x 轴上创建体重图,y 轴上创建身高图。使用不同的点字符或颜色来区分男性和女性,并提供匹配的图例。标记坐标轴并为图形添加标题。
体重(kg) 身高(cm) 性别 55 161 女性 85 185 男性 75 174 男性 42 154 女性 93 188 男性 63 178 男性 58 170 女性 75 167 男性 89 181 男性 67 178 女性
7.4 ggplot2 软件包
到目前为止,本章展示了 R 的内置图形工具(通常称为 基础 R 图形 或 传统 R 图形)。现在,让我们看看另一个重要的图形工具套件:ggplot2,这是由 Hadley Wickham(2009)开发的一个知名的贡献包。像任何其他贡献包一样,ggplot2 在 CRAN 上提供强大的替代标准绘图程序。gg 代表 图形语法 —— 图形生成的一种特定方法,由 Wilkinson(2005)描述。遵循这种方法,ggplot2 标准化了不同绘图类型的生成,简化了向现有图形添加一些棘手的方面(例如包含图例),并允许您通过定义和操作 层 来构建图形。暂时,让我们使用 第 7.1 节–第 7.3 节 中相同的简单示例来了解 ggplot2 的基本行为。您将熟悉基本的绘图函数 qplot 及其与之前使用的通用 plot 函数的区别。在 第十四章 中讨论统计图时,我将回到 ggplot2 的话题,并在 第二十四章 中探索更多高级功能。
7.4.1 使用 qplot 快速绘图
首先,你必须通过手动下载或直接在提示符下输入install.packages("ggplot2")来安装ggplot2包(参见第 A.2.3 节)。然后,使用以下命令加载该包:
R> library("ggplot2")
现在,让我们回到最初存储在第 7.1 节中的五个数据点foo和bar。
R> foo <- c(1.1,2,3.5,3.9,4.2)
R> bar <- c(2,2.2,-1.3,0,0.2)
你可以使用ggplot2的“快速绘图”函数qplot生成图 7-1 的版本。
R> qplot(foo,bar)
结果显示在图 7-8 的左图中。这个图像与使用plot生成的图像有一些明显的不同,但qplot的基本语法与之前相同。传递给qplot的前两个参数是长度相等的向量,foo中的x坐标首先给出,接着是bar中的y坐标。

图 7-8:使用ggplot2默认行为绘制的五个数据点qplot函数(左图)和添加标题及轴标签后的版本(右图)
添加标题和轴标签也使用了你在第 7.2 节中已经看到的相同参数。
R> qplot(foo,bar,main="My lovely qplot",xlab="x axis label",ylab="location y")
这将生成图 7-8 的右面板。
尽管在语法上有这种基本的相似性,但ggplot2和基础 R 图形创建图形的方式有着根本的区别。使用内置图形工具构建图形本质上是一个实时、逐步的过程。特别是在第 7.3 节中,这一点尤为明显,当时你将图形设备视为一个活跃的画布,逐个添加点、线及其他特征。相比之下,ggplot2图形是作为对象存储的,这意味着它们有一个潜在的、静态的表示,直到你更改该对象——实际上,你通过qplot可视化的是任何给定时刻的printed 对象。为强调这一点,输入以下代码:
R> baz <- plot(foo,bar)
R> baz
NULL
R> qux <- qplot(foo,bar)
R> qux
第一个赋值使用了内置的plot函数。当你运行那行代码时,图 7-1 中的图形会弹出。由于没有实际存储在工作空间中,打印假定的对象baz将返回空值NULL。另一方面,将qplot的内容存储起来(这里存储为对象qux)是有意义的。这次,进行赋值时不会显示图形。只有当你在提示符下输入qux时,才会显示图形,它调用该对象的print方法。这个看似微不足道的细节,实际上通过这种方式保存图形,可以在显示之前修改或增强图形(正如你稍后将看到的那样),并且它相较于基础 R 图形来说是一个明显的优势。
7.4.2 使用 Geoms 设置外观常量
要在ggplot2图形中添加和自定义点和线,你需要修改对象本身,而不是使用冗长的参数列表或单独执行的辅助函数(如points或lines)。你可以使用ggplot2方便的几何修改器,也称为geoms,来修改对象。假设你想像在第 7.1 节中那样连接foo和bar中的五个点,你可以首先创建一个空白的图形对象,然后像这样使用几何修改器:
R> qplot(foo,bar,geom="blank") + geom_point() + geom_line()
结果图像显示在图 7-9 的左侧。在第一次调用qplot时,你通过设置初始几何修改器为geom="blank"来创建一个空的图形对象(如果显示该图形,你将只看到灰色背景和坐标轴)。然后,你将另外两个几何图形geom_point()和geom_line()叠加上去。如括号所示,这些几何图形是函数,它们产生自己的专门对象。你可以通过+运算符将几何图形添加到qplot对象中。在这里,你没有为任何几何图形提供参数,这意味着它们将使用最初提供给qplot的数据(foo和bar),并且会遵循任何其他特性的默认设置,如颜色或点/线类型。你可以通过指定可选参数来控制这些特性,如下所示:
R> qplot(foo,bar,geom="blank") + geom_point(size=3,shape=6,color="blue") +
geom_line(color="red",linetype=2)

图 7-9:使用几何修改器来改变qplot对象外观的两个简单图形。左:使用默认设置添加点和线。右:使用几何图形影响点的字符、大小和颜色,以及线条类型和颜色。
请注意,这里使用的ggplot2的一些参数名称,如点的字符和大小(shape和size),与基础 R 图形中的参数名称(pch和cex)不同。但ggplot2实际上与 R 标准plot函数中使用的许多常见图形参数兼容,因此如果你更喜欢,也可以在此使用那些参数。例如,在此示例中,将cex=3和pch=6设置在geom_point中将会得到相同的图像。
ggplot2图形的面向对象特性意味着调整图形或尝试不同的视觉特性不再需要每次更改内容时重新运行所有绘图命令。这是通过几何图形(geoms)实现的。假设你喜欢图 7-9 右侧使用的线条类型,但希望使用不同的点字符。为了尝试,你可以首先存储之前创建的qplot对象,然后使用geom_point与该对象一起尝试不同的点样式。
R> myqplot <- qplot(foo,bar,geom="blank") + geom_line(color="red",linetype=2)
R> myqplot + geom_point(size=3,shape=3,color="blue")
R> myqplot + geom_point(size=3,shape=7,color="blue")
第一行将原始图形存储在myqplot中,接下来的几行调用myqplot并使用不同的点形状。第二行和第三行分别生成图 7-10 左侧和右侧的图形。

图 7-10:利用 ggplot2 图形的面向对象特性来尝试不同的点形状
在ggplot2中,有许多几何修饰符可以通过以geom_开头的函数名称调用。要获取列表,只需确保加载了该包,并在提示符下输入??"geom_"进行帮助搜索。
7.4.3 美学映射与 Geoms
Geoms 和ggplot2还提供了高效的自动化方法,将不同的样式应用于图表的不同子集。如果你使用因子对象将数据集拆分成类别,ggplot2可以自动将特定样式应用于不同的类别。在ggplot2的文档中,保存这些类别的因子被称为变量,ggplot2可以将其映射到美学值。这消除了许多使用基本 R 图形将数据子集隔离并单独绘制的工作(就像你在第 7.3 节中做的那样)。
所有这些最好通过一个例子来说明。让我们回到你手动绘制的 20 个观察值,逐步生成图 7-6 中的精美图表。
R> x <- 1:20
R> y <- c(-1.49,3.37,2.59,-2.78,-3.94,-0.92,6.43,8.51,3.41,-8.23,
-12.01,-6.58,2.87,14.12,9.63,-4.58,-14.78,-11.67,1.17,15.62)
在第 7.3 节中,你定义了几个类别,将每个观察值根据其x和y值分类为“标准”,“甜美”,“过大”或“过小”。使用相同的分类规则,让我们显式地定义一个因子来对应x和y。
R> ptype <- rep(NA,length(x=x))
R> ptype[y>=5] <- "too_big"
R> ptype[y<=-5] <- "too_small"
R> ptype[(x>=5&x<=15)&(y>-5&y<5)] <- "sweet"
R> ptype[(x<5|x>15)&(y>-5&y<5)] <- "standard"
R> ptype <- factor(x=ptype)
R> ptype
[1] standard standard standard standard sweet sweet too_big
[8] too_big sweet too_small too_small too_small sweet too_big
[15] too_big standard too_small too_small standard too_big
Levels: standard sweet too_big too_small
现在你有一个包含 20 个值的因子,并将这些值排序为四个层级。你将使用这个因子来告诉qplot如何映射你的美学属性。下面是一个简单的方法:
R> qplot(x,y,color=ptype,shape=ptype)
这一行代码生成了图 7-11 中的左侧图表,该图表通过颜色和点形状分隔了四个类别,并提供了图例。这一切都是通过在qplot调用中的美学映射完成的,你在其中将color和shape映射到ptype变量。

图 7-11:使用 qplot 和 geoms 演示美学映射在 ggplot2 中的应用。左:初始调用的 qplot,它使用 ptype映射点形状和颜色。右:通过各种 geoms 增强左侧图表,以覆盖默认映射。
现在,让我们使用相同的qplot对象和一系列的 geom 修饰来重新绘制这些数据,以获得更像图 7-6 的结果。执行以下代码会生成图 7-11 右侧的图表:
R> qplot(x,y,color=ptype,shape=ptype) + geom_point(size=4) +
geom_line(mapping=aes(group=1),color="black",lty=2) +
geom_hline(mapping=aes(yintercept=c(-5,5)),color="red") +
geom_segment(mapping=aes(x=5,y=-5,xend=5,yend=5),color="red",lty=3) +
geom_segment(mapping=aes(x=15,y=-5,xend=15,yend=5),color="red",lty=3)
在第一行中,你添加了geom_point(size=4)来增大图中所有点的大小。接下来的几行中,你添加了一条连接所有点的线,并加上了水平和垂直线来标出最佳点。在最后四行中,你必须使用aes来设置点类别的替代美学映射。让我们仔细看看这里发生了什么。
由于你在初始调用qplot时使用了ptype进行美学映射,默认情况下,所有其他几何体将按照相同方式映射到每个类别,除非你通过aes覆盖了该默认映射。例如,当你调用geom_line连接所有点时,如果你坚持使用默认的ptype映射,而没有包括mapping=aes(group=1),那么这个几何体将绘制连接每个类别内的点的线条。你将看到四条独立的虚线——一条连接所有“标准”点,另一条连接所有“甜美”点,以此类推。但这不是你在这里想要的;你希望绘制一条从左到右连接所有点的线。所以,你告诉geom_line通过输入aes(group=1)将所有观察值视为一个组。
此后,你使用geom_hline函数绘制了y = −5 和y = 5 的水平线,使用它的yintercept参数,参数再次传递给aes来重新定义该几何体的mapping。在这种情况下,你需要重新定义映射,使其作用于向量c(-5,5),而不是使用观察数据中的x和y。类似地,你最后使用geom_segment绘制了两条垂直的虚线段。geom_segment的操作方式与segments非常相似——你根据“起始”坐标(x和y参数)和“结束”坐标(这里的xend和yend参数)重新定义映射。由于第一个几何体geom_point(size=4)为每个绘制的点设置了固定的放大尺寸,因此不管几何体如何映射,都无关紧要,因为它只是对每个点进行了统一的大小调整。
在 R 中绘图,从基础图形到像ggplot2这样的贡献包,都忠实于语言的本质。逐元素匹配使得你能够通过少量直观而简单的函数创建复杂的图形。显示图形后,你可以通过选择图形设备并选择“文件”→“保存”来将其保存到硬盘。不过,你也可以直接将图形写入文件,正如你稍后将在第 8.3 节看到的那样。
本节探索的图形功能只是冰山一角,从现在开始,你将继续使用数据可视化。
练习 7.2
在练习 7.1(b)中,你使用了基础 R 图形绘制了一些体重和身高数据,使用不同的点或颜色区分了男性和女性。请使用ggplot2重复此任务。
本章重要代码
| 函数/操作符 | 简要描述 | 首次出现 |
|---|---|---|
plot |
创建/显示基础 R 图形 | 第 7.1 节,第 128 页 |
type |
设置图形类型 | 第 7.2.1 节,第 130 页 |
main, xlab, ylab |
设置坐标轴标签 | 第 7.2.2 节,第 130 页 |
col |
设置点/线颜色 | 第 7.2.3 节,第 131 页 |
pch, cex |
设置点的类型/大小 | 第 7.2.4 节, 第 133 页 |
lty, lwd |
设置线型/线宽 | 第 7.2.4 节, 第 133 页 |
xlim, ylim |
设置绘图区域的限制 | 第 7.2.5 节, 第 134 页 |
abline |
添加垂直/水平线 | 第 7.3 节, 第 137 页 |
segments |
添加特定的线段 | 第 7.3 节, 第 137 页 |
points |
添加点 | 第 7.3 节, 第 137 页 |
lines |
根据坐标添加线条 | 第 7.3 节, 第 138 页 |
arrows |
添加箭头 | 第 7.3 节, 第 138 页 |
text |
添加文本 | 第 7.3 节, 第 138 页 |
legend |
添加/控制图例 | 第 7.3 节, 第 138 页 |
qplot |
创建ggplot2的“快速绘图” |
第 7.4.1 节, 第 140 页 |
geom_point |
添加点的几何对象 | 第 7.4.2 节, 第 141 页 |
geom_line |
添加线的几何对象 | 第 7.4.2 节, 第 141 页 |
size, shape, color |
设置几何对象常量 | 第 7.4.2 节, 第 142 页 |
linetype |
设置几何对象的线型 | 第 7.4.2 节, 第 142 页 |
mapping, aes |
几何对象美学映射 | 第 7.4.3 节, 第 145 页 |
geom_hline |
添加水平线的几何对象 | 第 7.4.3 节, 第 145 页 |
geom_segment |
添加线段几何对象 | 第 7.4.3 节, 第 145 页 |
第八章:8
读取和写入文件

现在我将介绍使用 R 的另一个基本方面:通过读取和写入文件在活动工作区加载和保存数据。通常,要处理一个大数据集,你需要从外部文件读取数据,无论它是存储为纯文本、电子表格文件,还是托管在网站上。R 提供了命令行函数,可以用来导入这些数据集,通常是作为数据框对象。你还可以通过在计算机上写入新文件的方式,从 R 导出数据框,此外,你还可以将创建的任何图表保存为图像文件。在本章中,我将介绍一些有用的基于命令的读写操作,用于导入和导出数据。
8.1 R 准备好的数据集
首先,让我们简要看看一些内置于软件中的数据集或属于用户贡献包的数据集。这些数据集是用来练习和实验功能的有用示例。
在提示符下输入data(),会弹出一个窗口,列出这些准备好的数据集,并提供简短的描述。这些数据集按名称的字母顺序组织,并按包分组(显示的确切列表将取决于从 CRAN 安装的贡献包;请参阅第 A.2 节)。
8.1.1 内置数据集
内置的、自动加载的包datasets中包含了多个数据集。要查看该包中包含的数据集摘要,你可以使用library函数,如下所示:
R> library(help="datasets")
R 准备好的数据集有一个对应的帮助文件,你可以在其中找到关于数据及其组织方式的重要细节。例如,内置的数据集之一叫做ChickWeight。如果你在提示符下输入?ChickWeight,你会看到图 8-1 中的窗口。

图 8-1:ChickWeight数据集的帮助文件
如你所见,这个文件解释了变量及其值,并指出数据存储在一个具有 578 行和 4 列的数据框中。由于datasets中的对象是内置的,你只需要在提示符下输入ChickWeight的名称就可以访问它。让我们来看一下前 15 条记录。
R> ChickWeight[1:15,]
weight Time Chick Diet
1 42 0 1 1
2 51 2 1 1
3 59 4 1 1
4 64 6 1 1
5 76 8 1 1
6 93 10 1 1
7 106 12 1 1
8 125 14 1 1
9 149 16 1 1
10 171 18 1 1
11 199 20 1 1
12 205 21 1 1
13 40 0 2 1
14 49 2 2 1
15 58 4 2 1
你可以像处理 R 中创建的任何其他数据框一样处理这个数据集——注意使用[1:15,]来访问这个对象中所需的行,具体细节请参考第 5.2.1 节。
8.1.2 贡献的数据集
还有更多作为贡献包的一部分提供的 R 准备好的数据集。要访问它们,首先安装并加载相关的包。考虑一下数据集ice.river,它位于 Trapletti 和 Hornik(2013)的贡献包tseries中。首先,你需要安装该包,可以通过在提示符下运行install.packages("tseries")来实现。然后,使用library加载包,以访问包的组成部分:
R> library("tseries")
'tseries' version: 0.10-32
'tseries' is a package for time series analysis and computational finance.
See 'library(help="tseries")' for details.
现在,您可以输入library(help="tseries")来查看该包中数据集的列表,并且您可以输入?ice.river来查找更多关于您想要使用的数据集的详细信息。帮助文件将ice.river描述为一个“时间序列对象”,包含河流流量、降水量和温度的测量数据——这些数据最初由 Tong(1990)报道。要访问该对象本身,您必须显式加载它,使用data函数。然后,您可以像平常一样在工作空间中操作ice.river。以下是前五条记录:
R> data(ice.river)
R> ice.river[1:5,]
flow.vat flow.jok prec temp
[1,] 16.10 30.2 8.1 0.9
[2,] 19.20 29.0 4.4 1.6
[3,] 14.50 28.4 7.0 0.1
[4,] 11.00 27.8 0.0 0.6
[5,] 13.60 27.8 0.0 2.0
这些适用于 R 的数据集的可用性和便利性使得测试代码变得简单,我将在随后的章节中使用它们进行演示。然而,要分析自己的数据,您通常需要从某个外部文件导入它们。让我们来看一下如何操作。
8.2 读取外部数据文件
R 有多种函数可以从存储的文件中读取字符并理解它们。您将看到如何读取表格格式文件,它们是 R 最容易读取和导入的文件之一。
8.2.1 表格格式
表格格式的文件最好理解为纯文本文件,具有三个关键特征,这些特征完全定义了 R 如何读取数据。
标题 如果存在标题,它总是文件的第一行。这个可选特性用于为每一列数据提供名称。在将文件导入 R 时,您需要告诉软件是否存在标题,以便它知道是否将第一行视为变量名,或者作为观察到的数据值。
分隔符 极其重要的分隔符是用于分隔每行条目的字符。分隔符字符在文件中不能用于其他任何用途。这告诉 R 一个特定的条目何时开始和结束(换句话说,它在表格中的确切位置)。
缺失值 这是另一个独特的字符字符串,用来专门表示缺失值。在读取文件时,R 会将这些条目转换为它识别的形式:NA。
通常,这些文件的扩展名是.txt(强调纯文本样式)或.csv(用于逗号分隔值)。
让我们尝试一个例子,使用在第 5.2.2 节末尾定义的数据框mydata的变体。图 8-2 展示了一个名为mydatafile.txt的合适的表格格式文件,其中包含该数据框的数据,并且现在标记了一些缺失值。这个数据文件可以在本书网站上找到,网址是* www.nostarch.com/bookofr/*,或者您可以使用文本编辑器从图 8-2 自行创建它。

图 8-2:一个纯文本表格格式文件
请注意,第一行是标题,值之间用单个空格分隔,缺失的值用星号(*)表示。同时,请注意每个新记录必须从新的一行开始。假设你被交给了一个用于数据分析的纯文本文件,在 R 中可以使用现成的命令 read.table 导入表格格式的文件,生成一个数据框对象,如下所示:
R> mydatafile <- read.table(file="/Users/tdavies/mydatafile.txt",
header=TRUE,sep=" ",na.strings="*",
stringsAsFactors=FALSE)
R> mydatafile
person age sex funny age.mon
1 Peter NA M High 504
2 Lois 40 F <NA> 480
3 Meg 17 F Low 204
4 Chris 14 M Med 168
5 Stewie 1 M High NA
6 Brian NA M Med NA
在调用 read.table 时,file 参数接受一个包含文件名和文件夹位置的字符字符串(使用正斜杠),header 是一个逻辑值,告诉 R file 是否有标题(此处为 TRUE),sep 接受一个字符字符串,提供分隔符(此处为单个空格 " "),而 na.strings 用于指定缺失值所表示的字符(此处为 "*")。
如果你要读取多个文件,并且不想每次都输入完整的文件夹位置,可以先通过 setwd 设置工作目录(见第 1.2.3 节),然后只需使用文件名及其扩展名作为传递给 file 参数的字符字符串。然而,这两种方法都要求你在 R 提示符下工作时确切知道文件的位置。幸运的是,R 还有一些有用的附加工具,可以帮助你记住文件的具体位置。如果你忘记了文件的准确位置,可以通过 list.files 查看任何文件夹的文本输出。以下示例展示了我的本地用户目录的混乱状态。
R> list.files("/Users/tdavies")
[1] "bands-SCHIST1L200.txt" "Brass" "Desktop"
[4] "Documents" "DOS Games" "Downloads"
[7] "Dropbox" "Exercise2-20Data.txt" "Google Drive"
[10] "iCloud" "Library" "log.txt"
[13] "Movies" "Music" "mydatafile.txt"
[16] "OneDrive" "peritonitis.sav" "peritonitis.txt"
[19] "Personal9414" "Pictures" "Public"
[22] "Research" "Rintro.tex" "Rprofile.txt"
[25] "Rstartup.R" "spreadsheetfile.csv" "spreadsheetfile.xlsx"
[28] "TakeHome_template.tex" "WISE-P2L" "WISE-P2S.txt"
[31] "WISE-SCHIST1L200.txt"
这里需要注意的一个重要特点是,文件和文件夹之间可能很难区分。文件通常会有扩展名,而文件夹则没有;然而,WISE-P2L 是一个没有扩展名的文件,看起来与任何列出的文件夹没有区别。
你也可以通过 R 交互式地查找文件。file.choose 命令直接从 R 提示符打开你的文件系统浏览器——就像任何其他程序在你想打开某个文件时一样。然后,你可以浏览到感兴趣的文件夹,选择文件后(见图 8-3),只会返回一个字符字符串。
R> file.choose()
[1] "/Users/tdavies/mydatafile.txt"

图 8-3:在调用 file.choose 时打开的我的本地文件浏览器。当感兴趣的文件被打开时,R 命令返回该文件的完整文件路径作为字符字符串。
这个命令特别有用,因为它返回的目录字符字符串正是用于像 read.table 这样的命令所要求的格式。因此,调用以下命令并选择 mydatafile.txt(如图 8-3 所示),将产生与之前明确使用文件路径在 file 中的结果完全相同:
R> mydatafile <- read.table(file=file.choose(),header=TRUE,sep=" ",
na.strings="*",stringsAsFactors=FALSE)
如果文件已经成功加载,您应该会返回到 R 提示符,并且不会收到任何错误消息。您可以通过调用mydatafile来检查这一点,它应该返回数据框。当将数据导入数据框时,请记住字符型观察值与因子型观察值之间的区别。纯文本文件中不会存储因子属性信息,但read.table默认会将非数字值转换为因子。在这里,您希望将一些数据保存为字符串,因此设置stringsAsFactors=FALSE,以防止 R 将所有非数字元素视为因子。这样,person、sex和funny都将作为字符型字符串存储。
然后,如果您希望将sex和funny作为因子类型的数据,可以将它们覆盖为因子版本。
R> mydatafile$sex <- as.factor(mydatafile$sex)
R> mydatafile$funny <- factor(x=mydatafile$funny,levels=c("Low","Med","High"))
8.2.2 电子表格工作簿
接下来,让我们来看看一些常见的电子表格软件文件格式。Microsoft Office Excel 的标准文件格式是 .xls 或 .xlsx。通常,这些文件不能直接与 R 兼容。有一些贡献的包函数试图填补这个空白——例如,Warnes 等人(2014)的gdata或 Mirai Solutions GmbH(2014)的XLConnect——但通常最好先将电子表格文件导出为表格格式,如 CSV。考虑一下来自练习 7.1(b)中的假设数据,它已存储在名为spreadsheetfile.xlsx的 Excel 文件中,如图 8-4 所示。

图 8-4:来自 练习 7.1(b)数据的电子表格文件
要用 R 读取这个电子表格,首先需要将其转换为表格格式。在 Excel 中,点击 文件 → 另存为... 提供了许多选项。将电子表格保存为逗号分隔的文件,命名为spreadsheet.csv。R 有一个简化版本的read.table,即read.csv,用于处理这些文件。
R> spread <- read.csv(file="/Users/tdavies/spreadsheetfile.csv",
header=FALSE,stringsAsFactors=TRUE)
R> spread
V1 V2 V3
1 55 161 female
2 85 185 male
3 75 174 male
4 42 154 female
5 93 188 male
6 63 178 male
7 58 170 female
8 75 167 male
9 89 181 male
10 67 178 female
在这里,file参数再次指定所需的文件,该文件没有头部,因此header=FALSE。你设置stringsAsFactors=TRUE,因为你确实希望将sex变量(唯一的非数字变量)视为因子。文件中没有缺失值,因此不需要指定na.strings(尽管如果有缺失值,可以像之前一样使用该参数),并且.csv文件本身是以逗号分隔的,read.csv默认正确实现了这一点,因此不需要sep参数。最终的数据框spread可以在 R 控制台中打印出来。
如你所见,将表格数据读取到 R 中相当简单——你只需要注意数据文件的标题和分隔符,以及如何标识缺失值。简单的表格格式是数据集常见的存储方式,但如果你需要读取结构更复杂的文件,R 及其贡献的包提供了一些更复杂的函数。例如,可以查看 scan 和 readLines 函数的文档,这些函数提供了对文件解析的高级控制。你也可以通过在提示符下访问 ?read.table 来查阅 read.table 和 read.csv 的文档。
8.2.3 基于网络的文件
在有互联网连接的情况下,R 可以使用相同的read.table命令从网站读取文件。有关标题、分隔符和缺失值的所有相同规则依然适用;你只需指定文件的 URL 地址,而不是本地文件夹路径。
作为示例,你将使用美国统计协会通过《统计教育期刊(JSE)》提供的在线数据集库,地址为www.amstat.org/publications/jse/jse_data_archive.htm。
本页面顶部链接的第一个文件是表格格式的数据集4cdata.txt(www.amstat.org/publications/jse/v9n2/4cdata.txt),该文件包含基于新加坡报纸广告的 Chu(2001)对 308 顆钻石特征的分析数据。图 8-5 显示了这些数据。
你可以查看文档文件(4c.txt)和 JSE 网站上链接的相关文章,以了解此表中记录的具体内容。请注意,五列数据中,第一列和第五列为数值型,其他列则可以用因子来表示。分隔符是空白字符,没有标题,也没有缺失值(因此你不需要指定表示缺失值的标记)。

图 8-5:在线找到的表格格式数据文件
记住这一点后,你可以通过以下几行直接从 R 提示符创建数据框:
R> dia.url <- "http://www.amstat.org/publications/jse/v9n2/4cdata.txt"
R> diamonds <- read.table(dia.url)
请注意,你在调用 read.table 时没有提供额外的值,因为默认值已经足够好。由于表格中没有标题,因此你可以保持默认的 header 值为 FALSE。sep 的默认值是 "",表示空白字符(不要与 " " 混淆,后者表示明确的空格字符),这正是此表使用的分隔符。stringsAsFactors 的默认值为 TRUE,这正是你希望在字符列中使用的设置。导入后,你可以根据文档中的信息为每一列指定列名,如下所示:
R> names(diamonds) <- c("Carat","Color","Clarity","Cert","Price")
R> diamonds[1:5,]
Carat Color Clarity Cert Price
1 0.30 D VS2 GIA 1302
2 0.30 E VS1 GIA 1510
3 0.30 G VVS1 GIA 1510
4 0.30 G VS1 GIA 1260
5 0.31 D VS1 GIA 1641
查看前五条记录可以看到数据框按你预期的方式显示。
8.2.4 其他文件格式
除了 .txt 或 .csv 文件之外,还有其他文件格式可以导入 R,例如数据文件格式 .dat。这些文件也可以使用 read.table 导入,尽管它们可能包含一些顶部的额外信息,必须使用可选的 skip 参数跳过。skip 参数指定文件顶部应该忽略的行数,在 R 开始导入之前。
如第 8.2.2 节中所述,还有一些贡献包可以处理其他统计软件的文件;然而,如果文件中有多个工作表,这可能会使事情变得复杂。CRAN 上的 R 包 foreign(R Core Team, 2015)提供了对 Stata、SAS、Minitab 和 SPSS 等统计程序使用的数据文件的读取支持。
CRAN 上的其他贡献包可以帮助 R 处理来自各种数据库管理系统(DBMS)的文件。例如,RODBC 包(Ripley 和 Lapsley, 2013)让你查询 Microsoft Access 数据库,并将结果作为数据框对象返回。其他接口包括 RMySQL 包(James 和 DebRoy, 2012)和 RJDBC 包(Urbanek, 2013)。
8.3 写出数据文件和图表
从数据框对象中写出新文件和读取文件一样简单。R 的向量化行为是一种快速便捷的方式来重新编码数据集,因此它非常适合读取数据、重新结构化数据并将其写回文件。
8.3.1 数据集
用于将表格格式文件写入计算机的函数是 write.table。你提供一个数据框对象作为 x,该函数将其内容写入一个指定名称、分隔符和缺失值字符串的新文件。例如,以下代码将 第 8.2 节中的 mydatafile 对象写入一个文件:
R> write.table(x=mydatafile,file="/Users/tdavies/somenewfile.txt",
sep="@",na="??",quote=FALSE,row.names=FALSE)
你提供 file 参数,指定文件夹位置,并以你想要的文件名结束。这条命令将在指定的文件夹位置创建一个新的表格格式文件,名为 somenewfile.txt,以 @ 作为分隔符,缺失值用 ?? 表示(因为你实际上是在创建一个新文件,因此通常不会使用 file.choose 命令)。由于 mydatafile 包含变量名,这些变量名会自动作为文件头写入文件。可选的逻辑参数 quote 决定是否将每个非数字条目用双引号括起来(例如,如果你需要它们用于其他软件的格式要求);通过将该参数设置为 FALSE,可以请求不使用引号。另一个可选的逻辑参数 row.names 用于决定是否将 mydatafile 的行名包含在文件中(在本例中,即为 1 到 6 的数字),如果不需要行名,可以设置为 FALSE。生成的文件,如图 8-6 所示,可以在文本编辑器中打开。
类似于 read.csv,write.csv 是 write.table 函数的简化版本,专为 .csv 文件设计。

图 8-6: somenewfile.txt 的内容
8.3.2 绘图和图形文件
绘图也可以直接写入文件中。在第七章中,你在活动的图形设备中创建并展示了绘图。这个图形设备不一定是屏幕窗口;它可以是指定的文件。你可以让 R 按照以下步骤进行操作:打开一个“文件”图形设备,执行任何绘图命令以创建最终图形,然后关闭设备。R 支持使用相同名称的函数直接写入 .jpeg、.bmp、.png 和 .tiff 文件。例如,以下代码使用这三个步骤来创建一个 .jpeg 文件:
R> jpeg(filename="/Users/tdavies/myjpegplot.jpeg",width=600,height=600)
R> plot(1:5,6:10,ylab="a nice ylab",xlab="here's an xlab",
main="a saved .jpeg plot")
R> points(1:5,10:6,cex=2,pch=4,col=2)
R> dev.off()
null device
1
文件图形设备通过调用 jpeg 打开,在其中你提供文件的预期名称和其文件夹位置作为 filename。默认情况下,设备的尺寸设置为 480 × 480 像素,但在这里你将其更改为 600 × 600。你也可以通过为 width 和 height 提供其他单位(英寸、厘米或毫米),并通过可选的 units 参数指定单位来设置这些尺寸。一旦文件被打开,你就可以执行任何必要的 R 绘图命令来创建图像——这个例子绘制了一些点,然后通过第二个命令添加了一些额外的点。最终的图形结果会默默地写入指定的文件,就像它会显示在屏幕上一样。当你完成绘图后,必须明确地通过调用 dev.off() 关闭文件设备,这将打印出有关剩余活动设备的信息(这里,“null device”可以粗略解释为“没有打开的设备”)。如果没有调用 dev.off(),R 将继续将任何后续的绘图命令输出到文件中,可能会覆盖你在文件中已有的内容。图 8-7 的左图展示了在此示例中创建的文件。

图 8-7:直接写入磁盘的 R 绘图: .jpeg 版本(左)和 .pdf 版本(右)来自相同绘图命令
你还可以将 R 绘图存储为其他文件类型,如 PDF(使用 pdf 函数)和 EPS 文件(使用 postscript 函数)。虽然这些函数的一些参数名称和默认值不同,但它们遵循相同的基本原理。你需要指定文件夹位置、文件名以及宽度和高度的尺寸;输入你的绘图命令;然后使用 dev.off() 关闭设备。图 8-7 的右面板展示了使用以下代码创建的 .pdf 文件:
R> pdf(file="/Users/tdavies/mypdfplot.pdf",width=5,height=5)
R> plot(1:5,6:10,ylab="a nice ylab",xlab="here's an xlab",
main="a saved .pdf plot")
R> points(1:5,10:6,cex=2,pch=4,col=2)
R> dev.off()
null device
1
在这里,您使用了与之前相同的绘图命令,只是在代码中有一些小的差异。文件的参数是file(而不是filename),width和height的单位在pdf中默认为英寸。图 8-7 中两张图像的外观差异主要来自这些宽度和高度的差异。
这个过程同样适用于ggplot2图像。然而,忠于其风格,ggplot2提供了一个方便的替代方法。ggsave函数可以用来将最近绘制的ggplot2图形写入文件,并在一行代码中执行设备的打开/关闭操作。
例如,以下代码创建并显示了一个来自简单数据集的ggplot2对象。
R> foo <- c(1.1,2,3.5,3.9,4.2)
R> bar <- c(2,2.2,-1.3,0,0.2)
R> qplot(foo,bar,geom="blank")
+ geom_point(size=3,shape=8,color="darkgreen")
+ geom_line(color="orange",linetype=4)
现在,要将这个图保存到文件中,您只需要以下一行代码:
R> ggsave(filename="/Users/tdavies/mypngqplot.png")
Saving 7 x 7 in image
这将把图像写入指定filename目录下的.png文件中。(注意,如果您没有通过width和height明确设置它们,尺寸将会被报告;这些尺寸会根据您的图形设备大小而有所不同。)结果如图 8-8 所示。

图 8-8:使用 ggplot2 的 ggsave 命令创建的 .png 文件
除了简洁,ggsave在其他几个方面也很方便。首先,您可以使用相同的命令创建各种图像文件类型——类型仅由您在filename参数中提供的扩展名决定。此外,ggsave具有一系列可选参数,如果您想控制图像的大小、质量或图形的缩放,它们也可以提供帮助。
关于从基础 R 图形保存图像的更多细节,请参阅 ?jpeg、?pdf 和 ?postscript 帮助文件。如果要了解如何使用ggplot2保存图像,请参考?ggsave。
8.4 临时对象的读写操作
对于典型的 R 用户,最常见的输入/输出操作可能围绕数据集和图像文件展开。但是,如果您需要读取或写入其他类型的 R 对象,如列表或数组,您将需要dput和dget命令,它们可以以更临时的方式处理对象。
假设,例如,您在当前会话中创建了这个列表:
R> somelist <- list(foo=c(5,2,45),
bar=matrix(data=c(T,T,F,F,F,F,T,F,T),nrow=3,ncol=3),
baz=factor(c(1,2,2,3,1,1,3),levels=1:3,ordered=T))
R> somelist
$foo
[1] 5 2 45
$bar
[,1] [,2] [,3]
[1,] TRUE FALSE TRUE
[2,] TRUE FALSE FALSE
[3,] FALSE FALSE TRUE
$baz
[1] 1 2 2 3 1 1 3
Levels: 1 < 2 < 3
该对象本身可以写入文件,这在您希望将其传递给同事或在其他地方的新的 R 会话中打开时非常有用。使用dput,以下代码将对象存储为一个纯文本文件,R 可以解释该文件:
R> dput(x=somelist,file="/Users/tdavies/myRobject.txt")
从技术角度讲,这个命令创建了一个对象的美国标准信息交换码(ASCII)表示。通过调用dput,您要写入的对象被指定为x,新纯文本文件的位置和名称传递给file。图 8-9 展示了结果文件的内容。

图 8-9: myRobject.txt 是使用 dput 对 somelist 操作后创建的
注意,dput 存储了对象的所有成员以及其他相关信息,如属性。例如,somelist 的第三个元素是一个有序因子,因此仅将其作为独立向量表示在文本文件中是不够的。
现在,假设你想将这个列表导入到 R 工作空间。如果已经通过 dput 创建了一个文件,那么可以使用 dget 将其读取到任何其他工作空间。
R> newobject <- dget(file="/Users/tdavies/myRobject.txt")
R> newobject
$foo
[1] 5 2 45
$bar
[,1] [,2] [,3]
[1,] TRUE FALSE TRUE
[2,] TRUE FALSE FALSE
[3,] FALSE FALSE TRUE
$baz
[1] 1 2 2 3 1 1 3
Levels: 1 < 2 < 3
你通过 dget 从 myRobject.txt 文件中读取该对象,并将其赋值给 newobject。这个对象与原始的 R 对象 somelist 相同,所有结构和属性都完整保留。
使用这些命令有一些缺点。首先,dput 不是像 write.table 那样可靠的命令,因为 R 有时很难为一个对象创建必要的纯文本表示(基本对象类通常没有问题,但复杂的用户自定义类可能会有问题)。另外,由于它们需要存储结构信息,使用 dput 创建的文件在所需空间和执行读写操作所需时间上相对低效。对于包含大量数据的对象,这一点尤为明显。尽管如此,dput 和 dget 仍然是存储或传输特定对象的有用方式,而不必保存整个工作空间。
练习 8.1
-
在 R 的内置
datasets库中,有一个数据框quakes。确保你能够访问这个对象并查看相应的帮助文件,以了解这个数据的含义。然后,执行以下操作:-
仅选择那些
mag(震级)大于或等于5的记录,并将它们写入一个名为 q5.txt 的表格格式文件,该文件存放在你机器上的一个现有文件夹中。使用!作为分隔符,并且不要包含任何行名。 -
将文件重新读取到 R 工作空间中,命名为
q5.dframe。
-
-
在贡献包
car中,有一个数据框Duncan,提供了 1950 年对工作声望的历史数据。安装car包并访问Duncan数据集及其帮助文件。然后,执行以下操作:-
编写 R 代码,将
education显示在 x 轴上,income显示在 y 轴上,并将 x 轴和 y 轴的范围固定为 [0,100]。提供适当的轴标签。对于prestige值小于或等于80的职业,使用黑色 ○ 作为点符号。对于prestige大于 80 的职业,使用蓝色 •。 -
添加一个图例,解释两种点符号的区别,然后保存一个 500 × 500 像素的 .png 文件。
-
-
创建一个名为
exer的列表,其中包含三个数据集:quakes、q5.dframe和Duncan。然后,执行以下操作:-
将列表对象直接写入磁盘,命名为 Exercise8-1.txt。在文本编辑器中简要检查文件内容。
-
重新读取Exercise8-1.txt文件到工作空间中;将结果对象命名为
list.of.dataframes。检查list.of.dataframes确实包含这三个数据框对象。
-
-
在第 7.4.3 节中,你创建了一个
ggplot2图形,显示了 20 个观测值,作为图 7-11 中底部图像,位于第 144 页。使用ggsave将该图表保存为.tiff文件。
本章重要代码
| 函数/操作符 | 简要描述 | 首次出现 |
|---|---|---|
data |
加载贡献的数据集 | 第 8.1.2 节, 第 149 页 |
read.table |
导入表格格式数据文件 | 第 8.2.1 节, 第 151 页 |
list.files |
打印特定文件夹内容 | 第 8.2.1 节, 第 151 页 |
file.choose |
交互式文件选择 | 第 8.2.1 节, 第 152 页 |
read.csv |
导入逗号分隔的文件 | 第 8.2.2 节, 第 153 页 |
write.table |
将表格格式文件写入磁盘 | 第 8.3.1 节, 第 156 页 |
jpeg, bmp, png, tiff |
将图像/图表文件写入磁盘 | 第 8.3.2 节, 第 157 页 |
dev.off |
关闭文件图形设备 | 第 8.3.2 节, 第 157 页 |
pdf, postscript |
将图像/图表文件写入磁盘 | 第 8.3.2 节, 第 158 页 |
ggsave |
将ggplot2图表文件写入磁盘 |
第 8.3.2 节, 第 159 页 |
dput |
将 R 对象写入文件(ASCII 格式) | 第 8.4 节, 第 160 页 |
dget |
导入 ASCII 对象文件 | 第 8.4 节, 第 161 页 |
第二部分
编程
第九章:9
调用函数

在你开始编写自己的 R 函数之前,了解函数在 R 会话中的调用和解释方式非常有用。首先,你将查看 R 中变量名是如何被划分的。你将了解 R 对参数和对象命名的规则,以及在调用函数时,R 如何查找参数和其他变量。然后,你将了解调用函数时指定参数的几种替代方法。
9.1 作用域
首先,理解 R 的 作用域规则 很重要,它决定了语言如何划分对象并在给定的会话中检索它们。这个框架还定义了可以同时存在重复对象名称的情况。例如,在调用 matrix 时,你使用了 data 作为参数(第 3.1 节),但 data 也是一个现成的函数名称,用于加载来自贡献包的数据集(第 8.1.2 节)。在本节中,你将对 R 在这些情况下的内部行为有一个初步的了解,这将帮助你在以后编写和执行你自己以及其他包的函数时。
9.1.1 环境
R 通过虚拟 环境 强制执行作用域规则。你可以将环境视为存储数据结构和函数的独立隔间。它们允许 R 区分与不同作用域相关的相同名称,并因此存储在不同的环境中。环境是动态实体——可以创建新的环境,也可以操作或删除现有的环境。
注意
严格来说,环境并不包含项目。相反,它们有指向计算机内存中这些项目位置的 指针 。但是,使用“隔间”这一隐喻,并将对象“归属于”这些隔间,在你初步理解环境如何工作的过程中是很有用的。
有三种重要的环境:全局环境、包环境和命名空间,以及局部或词法环境。
全局环境
全局环境是为用户定义的对象保留的隔间。到目前为止,你创建或重写的每个对象都驻留在当前 R 会话的全局环境中。在第 1.3.1 节中,我提到过,调用 ls() 会列出活动工作空间中的所有对象、变量和用户定义的函数——更准确地说,ls() 会打印当前全局环境中的所有内容。
从一个新的 R 工作空间开始,以下代码创建了两个对象并确认它们在全局环境中的存在:
R> foo <- 4+5
R> bar <- "stringtastic"
R> ls()
[1] "bar" "foo"
那么,所有现成的对象和函数呢?为什么它们没有和foo、bar一起作为这个环境的成员打印出来呢?事实上,这些对象和函数属于特定的包环境,接下来会进行描述。
包环境与命名空间
为了简便起见,我会宽泛地使用包环境一词,指代 R 中每个包所提供的项目。实际上,R 包的作用域结构要复杂得多。每个包环境实际上代表了多个环境,这些环境控制着对给定对象搜索的不同方面。例如,包的命名空间基本上定义了其函数的可见性。(一个包可以有可见函数,用户可以使用这些函数,也可以有不可见函数,这些函数为可见函数提供内部支持。)包环境的另一个部分处理导入指定,涉及包需要从其他库导入的任何函数或对象,以实现其自身功能。
为了更清楚地说明这一点,你可以将本书中你正在使用的所有现成函数和对象视为属于特定的包环境。同样,任何你通过调用library显式加载的贡献包的函数和对象也是如此。你可以使用ls列出包环境中的项目,方法如下:
R> ls("package:graphics")
[1] "abline" "arrows" "assocplot" "axis"
[5] "Axis" "axis.Date" "axis.POSIXct" "axTicks"
[9] "barplot" "barplot.default" "box" "boxplot"
[13] "boxplot.default" "boxplot.matrix" "bxp" "cdplot"
[17] "clip" "close.screen" "co.intervals" "contour"
[21] "contour.default" "coplot" "curve" "dotchart"
[25] "erase.screen" "filled.contour" "fourfoldplot" "frame"
[29] "grconvertX" "grconvertY" "grid" "hist"
[33] "hist.default" "identify" "image" "image.default"
[37] "layout" "layout.show" "lcm" "legend"
[41] "lines" "lines.default" "locator" "matlines"
[45] "matplot" "matpoints" "mosaicplot" "mtext"
[49] "pairs" "pairs.default" "panel.smooth" "par"
[53] "persp" "pie" "plot" "plot.default"
[57] "plot.design" "plot.function" "plot.new" "plot.window"
[61] "plot.xy" "points" "points.default" "polygon"
[65] "polypath" "rasterImage" "rect" "rug"
[69] "screen" "segments" "smoothScatter" "spineplot"
[73] "split.screen" "stars" "stem" "strheight"
[77] "stripchart" "strwidth" "sunflowerplot" "symbols"
[81] "text" "text.default" "title" "xinch"
[85] "xspline" "xyinch" "yinch"
ls命令列出了graphics包环境中所有可见的对象。请注意,这个列表包括你在第七章中使用的部分函数,例如arrows、plot和segments。
局部环境
每当在 R 中调用一个函数时,都会创建一个新的环境,称为局部环境,有时也被称为词法环境。这个局部环境包含所有在函数内部创建的、且该函数可以访问的对象和变量,包括你在执行函数时传入的任何参数。正是这个特性使得在给定的工作空间中,函数参数名可以与其他可访问对象名相同。
例如,假设你调用matrix并传入参数data,如下所示:
R> youthspeak <- matrix(data=c("OMG","LOL","WTF","YOLO"),nrow=2,ncol=2)
R> youthspeak
[,1] [,2]
[1,] "OMG" "WTF"
[2,] "LOL" "YOLO"
调用此函数会创建一个包含data向量的局部环境。当你执行函数时,它首先会在该局部环境中查找data。这意味着 R 不会受到其他环境中名为data的对象或函数的干扰(例如,自动从utils包环境中加载的data函数)。如果在局部环境中找不到所需的项,R 才会开始扩大搜索范围(我将在第 9.1.2 节中进一步讨论这个特性)。一旦函数执行完成,该局部环境会自动被移除。对于nrow和ncol参数,也适用相同的说明。
9.1.2 搜索路径
要访问来自非直接全局环境的数据结构和函数,R 会遵循一个搜索路径。搜索路径列出了当前 R 会话可用的所有环境。
搜索路径基本上是 R 在请求对象时会搜索的环境列表。如果某个环境中没有找到对象,R 会继续搜索下一个环境。你可以随时使用search()查看 R 的搜索路径。
R> search()
[1] ".GlobalEnv" "tools:RGUI" "package:stats"
[4] "package:graphics" "package:grDevices" "package:utils"
[7] "package:datasets" "package:methods" "Autoloads"
[10] "package:base"
从命令提示符来看,这条路径始终从全局用户环境(.GlobalEnv)开始,并在base包环境(package:base)之后结束。你可以将这些环境视为属于一个层次结构,每对环境之间有一条从左到右的箭头指示。在我当前的会话中,如果我在 R 提示符下请求某个对象,程序将依次检查.GlobalEnv → tools:RGUI → package:stats → ... → package:base,当找到所需对象并检索后就停止搜索。请注意,根据你的操作系统以及是否使用内置 GUI,tools:RGUI可能不包括在你的搜索路径中。
如果 R 通过搜索路径中的各个环境没有找到所需的对象,便会到达空环境。空环境不会在search()的输出中显式列出,但它总是package:base之后的最终目的地。这个环境很特殊,因为它标志着搜索路径的结束。
例如,如果你调用以下内容,内部会发生一些事情:
R> baz <- seq(from=0,to=3,length.out=5)
R> baz
[1] 0.00 0.75 1.50 2.25 3.00
R 首先在全局环境中搜索名为seq的函数,当找不到时,它会继续在封闭环境中搜索,这是搜索路径中下一层的环境(如前所述,从左到右的箭头表示)。如果在那里也找不到,R 会继续沿路径搜索,查找已加载的包(无论是自动加载还是手动加载),直到找到所需的内容。在这个例子中,R 在内置的base包环境中找到了seq。然后,它执行seq函数(创建一个临时的局部环境),并将结果分配给一个新的对象baz,该对象位于全局环境中。在随后的print(baz)调用中,R 首先在全局环境中搜索,立即找到了请求的对象。
你可以使用environment查找任何函数的封闭环境,方法如下:
R> environment(seq)
<environment: namespace:base>
R> environment(arrows)
<environment: namespace:graphics>
在这里,我已将base包的命名空间标识为seq函数的拥有者,并将graphics包标识为arrows函数的拥有者。
每个环境都有一个父环境,用于指导搜索路径的顺序。通过检查之前search()调用的输出,您可以看到例如package:stats的父环境是package:graphics。这种父子结构是动态的,意味着当加载额外的库或数据框被attach时,搜索路径会发生变化。当您通过library调用加载一个贡献包时,它基本上会将所需的包插入到搜索路径中。例如,在练习 8.1 中,您安装了贡献包car。加载此包后,您的搜索路径将包括它的内容。
R> library("car")
R> search()
[1] ".GlobalEnv" "package:car" "tools:RGUI"
[4] "package:stats" "package:graphics" "package:grDevices"
[7] "package:utils" "package:datasets" "package:methods"
[10] "Autoloads" "package:base"
请注意car包环境在路径中的位置——它直接插入在全局环境之后。这是每个后续加载的包将被放置的位置(接着是它依赖的其他包)。
如前所述,一旦 R 搜索完所有路径并到达空环境,它将停止搜索。如果您请求一个未定义的函数或对象,或者是可能在一个您忘记加载的贡献包中(这是一个常见的小错误),那么会抛出错误。这些“找不到”的错误无论是对于函数还是其他对象都可以识别。
R> neither.here()
Error: could not find function "neither.here"
R> nor.there
Error: object 'nor.there' not found
环境有助于将 R 中的大量功能进行隔离。当搜索路径中不同包中有相同名称的函数时,这一点尤其重要。此时,掩蔽,如第 12.3 节所讨论的那样,便开始起作用。
随着您对 R 的熟悉,您可能希望更精确地控制其操作,因此值得全面调查 R 如何处理环境。关于这一点的更多技术细节,Gupta(2012)提供了一篇特别精彩的在线文章。
9.1.3 保留和受保护的名称
R 中有一些关键术语严格禁止用作对象名称。这些保留名称是为了保护语言中经常使用的基本操作和数据类型。
以下标识符是保留的:
• if 和 else
• for、while 和 in
• function
• repeat、break 和 next
• TRUE 和 FALSE
• Inf 和 -Inf
• NA、NaN 和 NULL
我还没有覆盖这个列表中的一些术语。这些条目代表了 R 语言编程的核心工具,您将在接下来的章节中开始探索它们。最后三个项目包括熟悉的逻辑值(第 4.1 节)和用于表示无限大和缺失条目的特殊术语(第 6.1 节)。
如果您尝试为这些保留术语分配新值,会发生错误。
R> NaN <- 5
Error in NaN <- 5 : invalid (do_set) left-hand side to assignment
由于 R 是区分大小写的,可能会给保留名称的任何大小写变体赋值,但这可能会导致混淆,通常不建议这样做。
R> False <- "confusing"
R> nan <- "this is"
R> cat(nan,False)
this is confusing
另外,要小心不要给T和F赋值,这两个是TRUE和FALSE的简写。完整的标识符TRUE和FALSE是保留的,但简写版本不是。
R> T <- 42
R> F <- TRUE
R> F&&TRUE
[1] TRUE
以这种方式给T和F赋值将影响任何后续的代码,这些代码意图使用T和F来表示TRUE和FALSE。第二个赋值(F <- TRUE)在 R 看来是完全合法的,但鉴于F通常作为简写,它会非常令人困惑:这一行F&&TRUE现在表示的是一个TRUE&&TRUE的比较!最好避免这种类型的赋值。
如果你一直在跟随 R 控制台中的示例,建议此时清空全局环境(从工作区删除对象False、nan、T和F)。可以使用rm函数,如下所示。使用ls()时,提供一个包含全局环境中所有对象的字符向量作为list参数。
R> ls()
[1] "bar" "baz" "F" "False" "foo" "nan"
[7] "T" "youthspeak"
R> rm(list=ls())
R> ls()
character(0)
现在,全局环境为空,调用ls()返回一个空的字符向量(character(0))。
练习 9.1
-
识别内置并自动加载的
methods包中的前 20 个项目。总共有多少个项目? -
确定以下每个函数所属的环境:
-
read.table -
data -
matrix -
jpeg
-
-
使用
ls和字符字符串相等性测试来确认smoothScatter函数属于graphics包。
9.2 参数匹配
另一组决定 R 如何解释函数调用的规则涉及参数匹配。参数匹配条件允许你通过简写的名称或完全不带名称的方式向函数提供参数。
9.2.1 精确匹配
到目前为止,你主要在使用精确匹配参数,其中每个参数标签都写出完整。这是调用函数时最详尽的方式。当你初次学习 R 或新函数时,写出完整的参数名是非常有帮助的。
精确匹配的其他好处包括:
• 与其他匹配方式相比,精确匹配更不容易出错。
• 参数的提供顺序无关紧要。
• 当一个函数有多个可能的参数,但你只想指定其中一部分时,精确匹配非常有用。
精确匹配的主要缺点很明显:
• 对于相对简单的操作来说,这可能显得笨重。
• 精确匹配要求用户记住或查找完整的、区分大小写的标签。
作为示例,在第 6.2.1 节中,你使用了精确匹配来执行以下操作:
R> bar <- matrix(data=1:9,nrow=3,ncol=3,dimnames=list(c("A","B","C"),
c("D","E","F")))
R> bar
D E F
A 1 4 7
B 2 5 8
C 3 6 9
这会创建一个 3 × 3 的矩阵对象 bar,并为行和列设置 dimnames 属性。由于参数标签已完全指定,参数的顺序并不重要。你可以交换参数,函数仍然能获得它所需的所有信息。
R> bar <- matrix(nrow=3,dimnames=list(c("A","B","C"),c("D","E","F")),ncol=3,
data=1:9)
R> bar
D E F
A 1 4 7
B 2 5 8
C 3 6 9
这与之前的函数调用行为相同。为了保持一致性,通常在每次调用函数时你不会交换参数,但这个例子展示了精确匹配的一个好处:你不需要担心任何可选参数的顺序或跳过它们。
9.2.2 部分匹配
部分匹配让你能够通过缩写标签来识别参数。这可以缩短代码,同时仍然允许你以任意顺序提供参数。
这里是另一种调用 matrix 的方式,它利用了部分匹配:
R> bar <- matrix(nr=3,di=list(c("A","B","C"),c("D","E","F")),nc=3,dat=1:9)
R> bar
D E F
A 1 4 7
B 2 5 8
C 3 6 9
请注意,我已将 nrow、dimnames 和 ncol 的参数标签缩短为前两个字母,并将 data 参数缩短为前三个字母。对于部分匹配,你不必提供固定数量的字母,只要每个参数对于被调用的函数仍然是唯一可识别的。部分匹配有以下优点:
• 它比精确匹配需要的代码更少。
• 参数标签仍然可见(这减少了误配的可能性)。
• 提供的参数顺序仍然不重要。
但部分匹配也有一些局限性。首先,如果有多个参数的标签以相同的字母开头,情况会变得更加复杂。这里有一个例子:
R> bar <- matrix(nr=3,di=list(c("A","B","C"),c("D","E","F")),nc=3,d=1:9)
Error in matrix(nr = 3, di = list(c("A", "B", "C"), c("D", "E", "F")), :
argument 4 matches multiple formal arguments
发生了错误。第四个参数标签仅指定为 d,代表 data。这是不合法的,因为另一个参数 dimnames 也以 d 开头。即使 dimnames 在同一行中稍早前已单独指定为 di,这个调用仍然无效。
部分匹配的缺点包括以下几点:
• 用户必须意识到其他可能被缩短标签匹配的参数(即使它们没有在调用中指定或已分配了默认值)。
• 每个标签必须有唯一的标识符,这可能很难记住。
9.2.3 位置匹配
R 中最紧凑的函数调用方式是位置匹配。这是指你在没有标签的情况下提供参数,R 会仅根据它们的顺序来解释它们。
位置匹配通常用于相对简单的函数,这些函数只有少量参数,或者是用户非常熟悉的函数。对于这种类型的匹配,你必须了解每个参数的精确位置。你可以在函数帮助文件的“Usage”部分找到该信息,或者可以通过 args 函数将其打印到控制台。这里有一个例子:
R> args(matrix)
function (data = NA, nrow = 1, ncol = 1, byrow = FALSE, dimnames = NULL)
NULL
这显示了 matrix 函数的参数定义顺序,以及每个参数的默认值。要使用位置匹配构造矩阵 bar,请执行以下操作:
R> bar <- matrix(1:9,3,3,F,list(c("A","B","C"),c("D","E","F")))
R> bar
D E F
A 1 4 7
B 2 5 8
C 3 6 9
位置匹配的好处如下:
• 更简短、更清晰的代码,特别是对于常规任务
• 不需要记住特定的参数标签。
注意,在使用精确匹配和部分匹配时,你不需要为byrow参数提供任何内容,因为默认情况下它被设置为FALSE。而在位置匹配时,你必须为byrow提供一个值(这里为F),作为第四个参数,因为 R 仅通过位置来解释函数调用。如果你省略了这个参数,就会出现如下错误:
R> bar <- matrix(1:9,3,3,list(c("A","B","C"),c("D","E","F")))
Error in matrix(1:9, 3, 3, list(c("A", "B", "C"), c("D", "E", "F"))) :
invalid 'byrow' argument
这里 R 试图将第四个参数(你原本打算用于dimnames的列表)赋值给逻辑值byrow的参数。这引出了位置匹配的缺点:
• 你必须查找并精确匹配已定义的参数顺序。
• 阅读别人写的代码可能更困难,尤其是当代码中包含不熟悉的函数时。
9.2.4 混合匹配
由于每种匹配方式都有优缺点,因此在一次函数调用中混合使用这三种匹配方式是非常常见且完全合法的。
例如,你可以像下面这样避免之前示例中的错误:
R> bar <- matrix(1:9,3,3,dim=list(c("A","B","C"),c("D","E","F")))
R> bar
D E F
A 1 4 7
B 2 5 8
C 3 6 9
在这里,我为前三个参数使用了位置匹配,这些参数你现在已经熟悉了。同时,我使用了部分匹配,明确告诉 R 该列表是作为dimnames值,而不是byrow。
9.2.5 点点点:省略号的使用
许多函数展示了可变参数行为。也就是说,它们可以接受任意数量的参数,由用户决定提供多少个参数。函数c、data.frame和list都是如此。当你调用像data.frame这样的函数时,你可以指定任意数量的成员作为参数。
这种灵活性是在 R 中通过特殊的点点点符号(...)实现的,也叫做省略号。这种构造允许用户提供任意数量的数据向量(这些向量将成为最终数据框中的列)。你可以通过查看函数的帮助页面或使用args来判断函数是否使用了省略号。在data.frame中,可以看到第一个参数位置是一个省略号:
R> args(data.frame)
function (..., row.names = NULL, check.rows = FALSE, check.names = TRUE,
stringsAsFactors = default.stringsAsFactors())
NULL
当你调用一个函数并提供一个无法与函数定义的参数标签匹配的参数时,通常这会导致一个错误。但是,如果函数是用省略号定义的,任何未与其他参数标签匹配的参数将会匹配到省略号。
使用省略号的函数通常分为两类。第一类包括像c、data.frame和list这样的函数,其中省略号始终表示函数调用中的“主要成分”。也就是说,函数的目的是将省略号中的内容用于结果对象或输出中。第二类函数则是将省略号作为补充或潜在的可选参数存储库。这种情况通常出现在某个感兴趣的函数调用了其他子函数,这些子函数根据最初提供的参数可能需要额外的参数。为了避免将子函数需要的所有参数显式复制到“父”函数的参数列表中,父函数可以通过定义一个省略号来实现,之后将其提供给子函数。
这是一个使用省略号传递补充参数的例子,出现在通用的plot函数中:
R> args(plot)
function (x, y, ...)
NULL
从检查参数来看,很明显如果提供了点大小(参数标签cex)或线型(参数标签lty)等可选参数,它们将与省略号匹配。这些可选参数随后会传递给函数,用于调整图形参数的各种方法。
省略号是编写可变参数函数或函数时一个方便的编程工具,其中可以传入未知数量的参数。当你开始在第十一章编写自己的函数时,这一点将变得更加清晰。然而,在编写这样的函数时,重要的是要正确记录...的预期用途,这样函数的潜在用户才能确切知道哪些参数可以传递给它,以及这些参数在执行过程中将如何使用。
练习 9.2
-
使用位置匹配和
seq创建一个从-4 到 4 的值序列,步长为 0.2。 -
在接下来的每一行代码中,识别使用的是哪种参数匹配方式:精确匹配、部分匹配、位置匹配或混合匹配。如果是混合匹配,请确定每种方式中指定了哪些参数。
-
array(8:1,dim=c(2,2,2)) -
rep(1:2,3) -
seq(from=10,to=8,length=5) -
sort(decreasing=T,x=c(2,1,1,2,0.3,3,1.3)) -
which(matrix(c(T,F,T,T),2,2)) -
which(matrix(c(T,F,T,T),2,2),a=T)
-
-
假设你显式运行了绘图函数
plot.default并为参数type、pch、xlab、ylab、lwd、lty和col提供了值。使用函数文档来确定这些参数中哪些属于省略号的范畴。
本章中的重要代码
| 函数/运算符 | 简要描述 | 首次出现 |
|---|---|---|
ls |
检查环境对象 | 第 9.1.1 节,第 167 页 |
search |
当前搜索路径 | 第 9.1.2 节,第 168 页 |
environment |
函数环境属性 | 第 9.1.2 节,第 169 页 |
rm |
删除工作区中的对象 | 第 9.1.3 节,第 171 页 |
args |
显示函数参数 | 第 9.2.3 节,第 174 页 |
第十章:10
条件与循环

要用 R 编写更复杂的程序,你需要控制代码的执行流程和顺序。实现这一点的一种基本方法是使某些代码段的执行依赖于一个条件。另一种基本控制机制是循环,它会将一段代码重复执行指定的次数。在本章中,我们将使用 if-else 语句、for 和 while 循环以及其他控制结构,来探讨这些核心编程技巧。
10.1 if 语句
if 语句是控制在特定代码块中到底执行哪些操作的关键。if 语句只有在某个条件为真时,才会执行代码块中的内容。这些构造使得程序可以根据条件是否为 TRUE 或 FALSE 做出不同的反应。
10.1.1 独立语句
让我们从独立的 if 语句开始,它看起来大概是这样的:
if(condition){
do any code here
}
condition 放在 if 关键字后面的括号内。这个条件必须是一个表达式,它返回一个单一的逻辑值(TRUE 或 FALSE)。如果条件为 TRUE,大括号 {} 中的代码将会被执行。如果条件不成立,大括号中的代码将被跳过,R 将什么都不做(或继续执行在闭合大括号后面的代码)。
这是一个简单的示例。在控制台中,存储以下内容:
R> a <- 3
R> mynumber <- 4
现在,在 R 编辑器中,写下以下代码块:
if(a<=mynumber){
a <- a²
}
当执行这段代码时,a 的值会是多少?这取决于定义 if 语句的条件,以及大括号内实际指定的内容。在这个例子中,当条件 a<=mynumber 被评估时,结果是 TRUE,因为 3 确实小于或等于 4。这意味着大括号内的代码会被执行,a 被设为 a²,即 9。
现在,高亮显示编辑器中的整个代码块,并将其发送到控制台进行评估。记住,你可以通过几种方式做到这一点:
• 直接将选中的文本从编辑器复制并粘贴到控制台中。
• 在菜单中,选择 编辑 → 运行行或选择(Windows)或选择 编辑 → 执行(OS X)。
• 使用快捷键,例如在 Windows 中按 CTRL-R,或在 Mac 上按 -RETURN。
一旦你在控制台中执行代码,你将看到类似下面的结果:
R> if(a<=mynumber){
+ a <- a²
+ }
然后,查看对象 a,如图所示:
R> a
[1] 9
接下来,假设你立刻再次执行相同的 if 语句。a 会再次被平方,变为 81 吗?不会!因为 a 现在是 9,而 mynumber 依然是 4,条件 a<=mynumber 将是 FALSE,因此大括号内的代码不会被执行;a 将保持为 9。
注意,在你将if语句发送到控制台后,每一行的前面都会有一个+。这些+符号并不表示任何形式的算术加法;相反,它们表示 R 在开始执行之前,期望更多的输入。例如,当左花括号被打开时,R 不会开始执行,直到该部分以右花括号关闭。为了避免重复,今后的示例中,我不会展示从编辑器发送到控制台的这部分代码的重复。
注意
你可以通过将不同的字符字符串分配给 R 的options命令中的continue组件来改变+符号,就像在第 1.2.1 节中重置提示符一样。
if语句提供了极大的灵活性——你可以在花括号区域内放置任何类型的代码,包括更多的if语句(参见即将讨论的嵌套部分,见第 10.1.4 节),这样可以使你的程序做出一系列的决策。
为了说明一个更复杂的if语句,考虑以下两个新对象:
R> myvec <- c(2.73,5.40,2.15,5.29,1.36,2.16,1.41,6.97,7.99,9.52)
R> myvec
[1] 2.73 5.40 2.15 5.29 1.36 2.16 1.41 6.97 7.99 9.52
R> mymat <- matrix(c(2,0,1,2,3,0,3,0,1,1),5,2)
R> mymat
[,1] [,2]
[1,] 2 0
[2,] 0 3
[3,] 1 0
[4,] 2 1
[5,] 3 1
在这里使用这两个对象的代码块:
if(any((myvec-1)>9)||matrix(myvec,2,5)[2,1]<=6){
cat("Condition satisfied --\n")
new.myvec <- myvec
new.myvec[seq(1,9,2)] <- NA
mylist <- list(aa=new.myvec,bb=mymat+0.5)
cat("-- a list with",length(mylist),"members now exists.")
}
将其发送到控制台,会产生以下输出:
Condition satisfied --
-- a list with 2 members now exists.
确实,已经创建了一个名为mylist的对象,你可以检查它。
R> mylist
$aa
[1] NA 5.40 NA 5.29 NA 2.16 NA 6.97 NA 9.52
$bb
[,1] [,2]
[1,] 2.5 0.5
[2,] 0.5 3.5
[3,] 1.5 0.5
[4,] 2.5 1.5
[5,] 3.5 1.5
在这个例子中,条件由两部分组成,通过使用||的 OR 语句连接,产生一个单一的逻辑结果。我们来逐步分析它。
• 条件的第一部分查看myvec,从每个元素中减去1,并检查结果是否有任何值大于 9。如果单独运行这一部分,结果是FALSE。
R> myvec-1
[1] 1.73 4.40 1.15 4.29 0.36 1.16 0.41 5.97 6.99 8.52
R> (myvec-1)>9
[1] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
R> any((myvec-1)>9)
[1] FALSE
• 条件的第二部分在调用matrix时使用位置匹配,构造了一个由原始myvec的条目填充的两行五列的矩阵。然后,检查该结果的第一列第二行的数字,看看它是否小于或等于 6,结果是符合的。
R> matrix(myvec,2,5)
[,1] [,2] [,3] [,4] [,5]
[1,] 2.73 2.15 1.36 1.41 7.99
[2,] 5.40 5.29 2.16 6.97 9.52
R> matrix(myvec,2,5)[2,1]
[1] 5.4
R> matrix(myvec,2,5)[2,1]<=6
[1] TRUE
这意味着if语句检查的整体条件将是FALSE||TRUE,其结果为TRUE。
R> any((myvec-1)>9)||matrix(myvec,2,5)[2,1]<=6
[1] TRUE
结果是,花括号内的代码被访问并执行。首先,它打印出"Condition satisfied"字符串,并将myvec复制到new.myvec。接着,使用seq访问new.myvec中的奇数索引,并将其值覆盖为NA。然后,它创建了mylist,在这个列表中,new.myvec被存储在一个名为aa的成员中,接着将原始的mymat的所有元素增加 0.5,并将结果存储在bb中。最后,打印出生成的列表的长度。
请注意,if语句不必完全按照我在这里使用的样式。有些程序员,例如,喜欢在条件后面将左大括号放在新的一行,或者有些人可能喜欢不同的缩进方式。
10.1.2 else 语句
if语句只有在定义的条件为TRUE时才会执行一段代码。如果你希望在条件为FALSE时发生不同的事情,你可以添加一个else声明。这里是一个伪代码示例:
if(condition){
do any code in here if condition is TRUE
} else {
do any code in here if condition is FALSE
}
你设置条件,然后在第一组大括号中放置当条件为TRUE时要执行的代码。在此之后,你声明else,后面跟着一个新的大括号,你可以在其中放置当条件为FALSE时要执行的代码。
让我们回到第 10.1.1 节中的第一个例子,再次将这些值存储在控制台提示符下。
R> a <- 3
R> mynumber <- 4
在编辑器中,创建一个新的版本的早期if语句。
if(a<=mynumber){
cat("Condition was",a<=mynumber)
a <- a²
} else {
cat("Condition was",a<=mynumber)
a <- a-3.5
}
a
在这里,如果条件a<=mynumber为TRUE,你再次将a平方;但如果为FALSE,则将a覆盖为自身减去 3.5 的结果。你还会打印文本到控制台,说明条件是否满足。在将a和mynumber重置为它们的原始值后,if循环的第一次运行将a计算为 9,就像之前一样,并输出以下内容:
Condition was TRUE
R> a
[1] 9
现在,立即高亮并再次执行整个语句。这一次,a<=mynumber将计算为FALSE并执行else之后的代码。
Condition was FALSE
R> a
[1] 5.5
10.1.3 使用 ifelse 进行逐元素检查
if语句只能检查单一的逻辑值。如果你传入一个逻辑向量作为条件,例如,if语句将只检查(并基于)第一个元素。它会发出警告,正如下面的虚拟示例所示:
R> if(c(FALSE,TRUE,FALSE,TRUE,TRUE)){}
Warning message:
In if (c(FALSE, TRUE, FALSE, TRUE, TRUE)) { :
the condition has length > 1 and only the first element will be used
然而,有一个可用的快捷函数ifelse,它可以在相对简单的情况下执行这种向量化检查。为了演示它是如何工作的,考虑以下定义的对象x和y:
R> x <- 5
R> y <- -5:5
R> y
[1] -5 -4 -3 -2 -1 0 1 2 3 4 5
现在,假设你想得到x/y的结果,但将任何Inf(即x除以零的任何实例)替换为NA。换句话说,对于y中的每个元素,你想检查y是否为零。如果是,那么你希望代码输出NA,如果不是,它应该输出x/y的结果。
正如你刚刚看到的,简单的if语句在这里不起作用。由于它只接受单一的逻辑值,它不能遍历y==0生成的整个逻辑向量。
R> y==0
[1] FALSE FALSE FALSE FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE
相反,你可以在这种情况下使用逐元素的ifelse函数。
R> result <- ifelse(test=y==0,yes=NA,no=x/y)
R> result
[1] -1.000000 -1.250000 -1.666667 -2.500000 -5.000000 NA 5.000000 2.500000
[9] 1.666667 1.250000 1.000000
使用精确匹配,这个命令在一行中创建了期望的 result 向量。必须指定三个参数:test 接受一个逻辑值数据结构,yes 提供满足条件时返回的元素,no 提供条件为 FALSE 时返回的元素。正如函数文档中所指出的(你可以通过 ?ifelse 访问它),返回的结构将具有与 test 相同的长度和属性。
练习 10.1
-
创建以下两个向量:
vec1 <- c(2,1,1,3,2,1,0) vec2 <- c(3,8,2,2,0,0,0)在不执行它们的情况下,确定以下哪个
if语句会导致字符串被打印到控制台。然后在 R 中确认你的答案。-
if((vec1[1]+vec2[2])==10){ cat("Print me!") } -
if(vec1[1]>=2&&vec2[1]>=2){ cat("Print me!") } -
if(all((vec2-vec1)[c(2,6)]<7)){ cat("Print me!") } -
if(!is.na(vec2[3])){ cat("Print me!") }
-
-
使用(a)中的
vec1和vec2,编写并执行一行代码,只有当它们的和大于 3 时,才将两个向量的对应元素相乘。否则,代码应简单地将两个元素相加。 -
在编辑器中编写 R 代码,该代码接受一个方形字符矩阵,并检查对角线上的任何字符字符串(从左上角到右下角)是否以字母 g(无论是小写还是大写)开头。如果满足条件,这些特定条目应该被字符串
"HERE"覆盖。否则,整个矩阵应该被同样维度的单位矩阵替换。然后,在以下矩阵上尝试你的代码,并每次检查结果:-
mymat <- matrix(as.character(1:16),4,4) -
mymat <- matrix(c("DANDELION","Hyacinthus","Gerbera", "MARIGOLD","geranium","ligularia", "Pachysandra","SNAPDRAGON","GLADIOLUS"),3,3) -
mymat <- matrix(c("GREAT","exercises","right","here"),2,2, byrow=T)
提示:这需要一些思考——你会发现 第 3.2.1 节 中的
diag函数和 第 4.2.4 节 中的substr函数会很有用。 -
10.1.4 嵌套和堆叠语句
一个 if 语句可以被放置在另一个 if 语句的结果中。通过 嵌套 或 堆叠 多个语句,你可以在执行过程中检查多个条件,从而编织出复杂的决策路径。
在编辑器中再次修改 mynumber 示例,如下所示:
if(a<=mynumber){
cat("First condition was TRUE\n")
a <- a²
if(mynumber>3){
cat("Second condition was TRUE")
b <- seq(1,a,length=mynumber)
} else {
cat("Second condition was FALSE")
b <- a*mynumber
}
} else {
cat("First condition was FALSE\n")
a <- a-3.5
if(mynumber>=4){
cat("Second condition was TRUE")
b <- a^(3-mynumber)
} else {
cat("Second condition was FALSE")
b <- rep(a+mynumber,times=3)
}
}
a
b
这里你会看到与之前相同的初始决策。如果 a 小于或等于 mynumber,则将其平方;否则,将其减去 3.5。但是现在每个大括号区域内有另一个 if 语句。如果第一个条件满足且 a 被平方,则继续检查 mynumber 是否大于 3。如果是 TRUE,则将 b 赋值为 seq(1,a,length=mynumber);如果是 FALSE,则将 b 赋值为 a*mynumber。
如果第一个条件失败并且你从 a 中减去 3.5,然后检查第二个条件,查看 mynumber 是否大于或等于 4。如果是,那么 b 变为 a^(3-mynumber)。如果不是,b 变为 rep(a+mynumber,times=3)。请注意,我已经缩进了每个大括号内的代码,以便更容易看到哪些行与每个可能的决策相关。
现在,在控制台中直接或通过编辑器重置 a <- 3 和 mynumber <- 4。当你运行 mynumber 示例代码时,你将得到以下输出:
First condition was TRUE
Second condition was TRUE
R> a
[1] 9
R> b
[1] 1.000000 3.666667 6.333333 9.000000
结果显示了究竟是哪个代码被调用——第一个条件和第二个条件都为TRUE。在再次运行相同代码之前,首先设置
R> a <- 6
R> mynumber <- 4
你将看到这个输出:
First condition was FALSE
Second condition was TRUE
R> a
[1] 2.5
R> b
[1] 0.4
这次第一个条件失败了,但在else语句内检查的第二个条件是TRUE。
另外,你也可以通过依次堆叠if语句并在每个条件中使用逻辑表达式的组合来实现相同的效果。在下面的示例中,你检查了相同的四种情况,但这次你通过将新的if声明直接跟在else声明后面来堆叠if语句:
if(a<=mynumber && mynumber>3){
cat("Same as 'first condition TRUE and second TRUE'")
a <- a²
b <- seq(1,a,length=mynumber)
} else if(a<=mynumber && mynumber<=3){
cat("Same as 'first condition TRUE and second FALSE'")
a <- a²
b <- a*mynumber
} else if(mynumber>=4){
cat("Same as 'first condition FALSE and second TRUE'")
a <- a-3.5
b <- a^(3-mynumber)
} else {
cat("Same as 'first condition FALSE and second FALSE'")
a <- a-3.5
b <- rep(a+mynumber,times=3)
}
a
b
就像之前一样,四个括起来的区域中只有一个最终会被执行。与嵌套版本相比,前两个括起来的区域对应于最初的第一个条件(a<=mynumber)被满足,但这次你使用&&同时检查两个表达式。如果这两个情况都不满足,那么第一个条件就是假,因此在第三个语句中,你只需要检查mynumber>=4。对于最终的else语句,你无需检查任何条件,因为该语句仅在所有之前的条件未满足时才会执行。
如果你再次将a和mynumber分别重置为 3 和 4,并执行之前展示的堆叠语句,你将得到以下结果:
Same as 'first condition TRUE and second TRUE'
R> a
[1] 9
R> b
[1] 1.000000 3.666667 6.333333 9.000000
这将产生与之前相同的a和b的值。如果你使用第二组初始值(a为 6,mynumber为 4)再次执行代码,你将得到以下结果:
Same as 'first condition FALSE and second TRUE'
R> a
[1] 2.5
R> b
[1] 0.4
这再次与使用嵌套版本代码的结果相匹配。
10.1.5 switch 函数
假设你需要根据一个对象的值来选择运行的代码(这是一个常见场景)。一种选择是使用一系列的if语句,通过将对象与各种可能的值进行比较,为每个条件生成一个逻辑值。下面是一个示例:
if(mystring=="Homer"){
foo <- 12
} else if(mystring=="Marge"){
foo <- 34
} else if(mystring=="Bart"){
foo <- 56
} else if(mystring=="Lisa"){
foo <- 78
} else if(mystring=="Maggie"){
foo <- 90
} else {
foo <- NA
}
这段代码的目标是简单地为对象foo赋一个数值,其中具体的数字取决于mystring的值。mystring对象可以有五种可能的值,或者如果mystring与这些值都不匹配,则foo被赋值为NA。
这段代码按原样运行得很好。例如,设置
R> mystring <- "Lisa"
并执行代码块,你会看到这个结果:
R> foo
[1] 78
设置以下
R> mystring <- "Peter"
并再次执行代码块,你会看到这个结果:
R> foo
[1] NA
然而,使用if-else语句来设置这种基础操作显得相当繁琐。R 可以通过switch函数以更紧凑的形式处理这种多选决策。例如,你可以将堆叠的if语句改写为一个更简洁的switch语句,如下所示:
R> mystring <- "Lisa"
R> foo <- switch(EXPR=mystring,Homer=12,Marge=34,Bart=56,Lisa=78,Maggie=90,NA)
R> foo
[1] 78
以及
R> mystring <- "Peter"
R> foo <- switch(EXPR=mystring,Homer=12,Marge=34,Bart=56,Lisa=78,Maggie=90,NA)
R> foo
[1] NA
第一个参数 EXPR 是感兴趣的对象,可以是数值型或字符型字符串。其余的参数提供基于 EXPR 值进行的值或操作。如果 EXPR 是字符串,这些参数标签必须完全匹配 EXPR 的可能结果。在这里,如果 mystring 是 "Homer",switch 语句返回 12;如果 mystring 是 "Marge",返回 34,以此类推。最后一个未标记的值 NA 表示如果 mystring 不匹配任何前面的项时的结果。
整数版的 switch 的工作方式稍有不同。它不是使用标签,而是通过位置匹配来确定结果。考虑以下示例:
R> mynum <- 3
R> foo <- switch(mynum,12,34,56,78,NA)
R> foo
[1] 56
在这里,你提供一个整数 mynum 作为第一个参数,并且它与 EXPR 按位置匹配。示例代码随后展示了五个未标记的参数:12 到 NA。switch 函数简单地返回由 mynum 请求的特定位置的值。由于 mynum 为 3,语句将 56 赋值给 foo。如果 mynum 是 1、2、4 或 5,foo 将分别被赋值为 12、34、78 或 NA。任何其他值的 mynum(小于 1 或大于 5)将返回 NULL。
R> mynum <- 0
R> foo <- switch(mynum,12,34,56,78,NA)
R> foo
NULL
在这些情况下,switch 函数的行为与一组堆叠的 if 语句相同,因此它可以作为一个方便的快捷方式。然而,如果你需要同时检查多个条件,或者需要根据该决策执行一组更复杂的操作,你将需要使用显式的 if 和 else 控制结构。
练习 10.2
-
编写一组显式堆叠的
if语句,执行与前面展示的整数版switch函数相同的操作。使用mynum <- 3和mynum <- 0进行测试,正如文中所示。 -
假设你负责计算某种药物在一系列假设的科学实验中的精确剂量。这些剂量依赖于一些预定的“剂量阈值”(
lowdose、meddose和highdose),以及一个名为doselevel的预定剂量水平因子向量。请查看以下项目(i–iv)以了解这些对象的预期形式。然后编写一组嵌套的if语句,按照以下规则生成一个新的数值向量dosage:– 首先,检查
doselevel中是否有任何"High"的实例,如果有,执行以下操作:检查
lowdose是否大于或等于 10。如果是,覆盖lowdose为 10;否则,将lowdose替换为它本身除以 2。检查
meddose是否大于或等于 26。如果是,将meddose覆盖为 26。检查
highdose是否小于 60。如果是,覆盖highdose为 60;否则,将highdose替换为它本身乘以 1.5。创建一个名为
dosage的向量,其值为lowdose重复(rep),以匹配doselevel的length。-
将
dosage中对应于doselevel中"Med"实例索引位置的元素覆盖为meddose。 -
将
dosage中对应于doselevel中"High"实例索引位置的元素覆盖为highdose。
– 否则(换句话说,如果
doselevel中没有"High"实例),执行以下操作:-
创建
doselevel的新版本,一个仅具有级别"Low"和"Med"的因子向量,并将这些级别分别标记为"Small"和"Large"(有关详细信息,请参见第 4.3 节,或查看?factor)。 -
检查
lowdose是否小于 15,并且meddose是否小于 35。如果是,单独将lowdose乘以 2,并将meddose覆盖为其本身加上highdose。 -
创建一个名为
dosage的向量,其值为lowdose重复(rep)至与doselevel的length匹配。 -
将
dosage中对应于doselevel中"Large"实例索引位置的元素覆盖为meddose。
现在,确认以下内容:
-
给定
lowdose <- 12.5 meddose <- 25.3 highdose <- 58.1 doselevel <- factor(c("Low","High","High","High","Low","Med", "Med"),levels=c("Low","Med","High"))运行嵌套
if语句后,dosage的结果如下:R> dosage [1] 10.0 60.0 60.0 60.0 10.0 25.3 25.3 -
使用与(i)中相同的
lowdose、meddose和highdose阈值,给定doselevel <- factor(c("Low","Low","Low","Med","Low","Med", "Med"),levels=c("Low","Med","High"))运行嵌套
if语句后,dosage的结果如下:R> dosage [1] 25.0 25.0 25.0 83.4 25.0 83.4 83.4此外,
doselevel已被如下覆盖:R> doselevel [1] Small Small Small Large Small Large Large Levels: Small Large -
给定
lowdose <- 9 meddose <- 49 highdose <- 61 doselevel <- factor(c("Low","Med","Med"), levels=c("Low","Med","High"))运行嵌套
if语句后,dosage的结果如下:R> dosage [1] 9 49 49此外,
doselevel已被如下覆盖:R> doselevel [1] Small Large Large Levels: Small Large -
使用与(iii)中相同的
lowdose、meddose和highdose阈值,以及与(i)中相同的doselevel,运行嵌套if语句后,dosage的结果如下:R> dosage [1] 4.5 91.5 91.5 91.5 4.5 26.0 26.0
-
-
假设对象
mynum始终是介于 0 和 9 之间的单个整数。使用ifelse和switch来生成一个命令,该命令接受mynum并返回与所有可能值 0, 1, ..., 9 对应的字符字符串。例如,传入3时应返回"three";传入0时应返回"zero"。
10.2 编码循环
另一种核心编程机制是循环,它会重复指定的代码段,通常是通过递增索引或计数器来实现。有两种循环方式:for循环会在向量中逐个元素地执行代码;while循环则会在某个特定条件评估为FALSE时停止。循环行为还可以通过 R 的apply函数系列来实现,相关内容讨论见第 10.2.3 节。
10.2.1 for 循环
R 的for循环始终采用以下通用形式:
for(loopindex in loopvector){
do any code in here
}
在这里,loopindex是一个占位符,代表loopvector中的一个元素——它从向量中的第一个元素开始,并在每次循环重复时移动到下一个元素。当for循环开始时,它运行大括号区域中的代码,将loopindex的任何出现替换为loopvector中的第一个元素。当循环达到闭合大括号时,loopindex会增加,取loopvector中的第二个元素,并重复大括号中的区域。这个过程一直持续到循环到达loopvector的最后一个元素,此时大括号代码被执行最后一次,循环退出。
这是一个在编辑器中编写的简单示例:
for(myitem in 5:7){
cat("--BRACED AREA BEGINS--\n")
cat("the current item is",myitem,"\n")
cat("--BRACED AREA ENDS--\n\n")
}
这个循环打印了loopindex(在这里我将其命名为myitem)的当前值,它从 5 递增到 7。以下是将结果输出到控制台后的输出:
--BRACED AREA BEGINS--
the current item is 5
--BRACED AREA ENDS--
--BRACED AREA BEGINS--
the current item is 6
--BRACED AREA ENDS--
--BRACED AREA BEGINS--
the current item is 7
--BRACED AREA ENDS--
你可以使用循环来操作循环外部存在的对象。考虑以下示例:
R> counter <- 0
R> for(myitem in 5:7){
+ counter <- counter+1
+ cat("The item in run",counter,"is",myitem,"\n")
+ }
The item in run 1 is 5
The item in run 2 is 6
The item in run 3 is 7
在这里,我首先定义了一个对象counter,并在工作空间中将其设置为零。然后,在循环内部,counter被其自身加 1 所覆盖。每次循环重复时,counter增加,并将当前值打印到控制台。
通过索引或值进行循环
请注意,使用loopindex直接表示loopvector中的元素与使用它表示向量的索引之间的区别。以下两个循环使用这两种不同的方法来print每个myvec中的数字的双倍:
R> myvec <- c(0.4,1.1,0.34,0.55)
R> for(i in myvec){
+ print(2*i)
+ }
[1] 0.8
[1] 2.2
[1] 0.68
[1] 1.1
R> for(i in 1:length(myvec)){
+ print(2*myvec[i])
+ }
[1] 0.8
[1] 2.2
[1] 0.68
[1] 1.1
第一个循环使用loopindex i 直接表示myvec中的元素,打印每个元素乘以 2 的值。另一方面,在第二个循环中,你使用i表示1:length(myvec)中的整数。这些整数构成了myvec所有可能的索引位置,你可以使用这些索引来提取myvec的元素(再次将每个元素乘以 2 并打印结果)。虽然这种方式稍显冗长,但使用向量索引位置在你如何使用loopindex时提供了更多灵活性。当你需要更复杂的for循环时,这一点会更加清晰,正如下一个例子所展示的。
假设你想编写一些代码,检查任何列表对象,并收集列表中作为成员存储的任何矩阵对象的信息。请考虑以下列表:
R> foo <- list(aa=c(3.4,1),bb=matrix(1:4,2,2),cc=matrix(c(T,T,F,T,F,F),3,2),
dd="string here",ee=matrix(c("red","green","blue","yellow")))
R> foo
$aa
[1] 3.4 1.0
$bb
[,1] [,2]
[1,] 1 3
[2,] 2 4
$cc
[,1] [,2]
[1,] TRUE TRUE
[2,] TRUE FALSE
[3,] FALSE FALSE
$dd
[1] "string here"
$ee
[,1]
[1,] "red"
[2,] "green"
[3,] "blue"
[4,] "yellow"
在这里,你创建了foo,它包含三个不同维度和数据类型的矩阵。你将编写一个for循环,遍历像这样的列表的每个成员,并检查该成员是否为矩阵。如果是,循环将获取矩阵的行数、列数以及数据类型。
在编写for循环之前,你应当创建一些向量来存储关于列表成员的信息:name用于存储列表成员的名称,is.mat用于指示每个成员是否是矩阵(值为"Yes"或"No"),nc和nr用于存储每个矩阵的行数和列数,data.type用于存储每个矩阵的数据类型。
R> name <- names(foo)
R> name
[1] "aa" "bb" "cc" "dd" "ee"
R> is.mat <- rep(NA,length(foo))
R> is.mat
[1] NA NA NA NA NA
R> nr <- is.mat
R> nc <- is.mat
R> data.type <- is.mat
在这里,你将foo的成员名称存储为name。同时,设置is.mat、nr、nc和data.type,这些都被分配为长度为length(foo)的向量,且初始值为NA。这些值将在你的for循环中根据需要进行更新,接下来你就可以编写循环了。请在编辑器中输入以下代码:
for(i in 1:length(foo)){
member <- foo[[i]]
if(is.matrix(member)){
is.mat[i] <- "Yes"
nr[i] <- nrow(member)
nc[i] <- ncol(member)
data.type[i] <- class(as.vector(member))
} else {
is.mat[i] <- "No"
}
}
bar <- data.frame(name,is.mat,nr,nc,data.type,stringsAsFactors=FALSE)
最初,设置loopindex变量i,使其能够通过foo的索引位置递增(即1:length(foo)的序列)。在大括号中的代码首先将foo中位置为i的成员写入一个对象member。接下来,你可以使用is.matrix检查该成员是否为矩阵(参见第 6.2.3 节)。如果为TRUE,执行以下操作:将is.mat向量的第i位置设置为"Yes";将nr和nc的第i元素分别设置为member的行数和列数;将data.type的第i元素设置为class(as.vector(member))的结果。该命令首先通过as.vector将矩阵强制转换为向量,然后使用class函数(详见第 6.2.2 节)来查找元素的数据类型。
如果member不是矩阵且if条件失败,则is.mat中相应的条目将被设置为"No",其他向量中的条目保持不变(因此它们仍然是NA)。
循环执行完毕后,从向量中创建数据框bar(注意使用stringsAsFactors=FALSE,以防止bar中的字符型向量被自动转换为因子;参见第 5.2.1 节)。执行代码后,bar的样式如下:
R> bar
name is.mat nr nc data.type
1 aa No NA NA <NA>
2 bb Yes 2 2 integer
3 cc Yes 3 2 logical
4 dd No NA NA <NA>
5 ee Yes 4 1 character
如你所见,这与列表foo中矩阵的性质相匹配。
嵌套 for 循环
你还可以像if语句一样嵌套for循环。当一个for循环嵌套在另一个for循环中时,内层循环会在外层循环的loopindex递增之前执行完整,接着内层循环会再次执行一遍。请在你的 R 控制台中创建以下对象:
R> loopvec1 <- 5:7
R> loopvec1
[1] 5 6 7
R> loopvec2 <- 9:6
R> loopvec2
[1] 9 8 7 6
R> foo <- matrix(NA,length(loopvec1),length(loopvec2))
R> foo
[,1] [,2] [,3] [,4]
[1,] NA NA NA NA
[2,] NA NA NA NA
[3,] NA NA NA NA
以下嵌套循环将foo填充为将loopvec1中的每个整数与loopvec2中的每个整数相乘的结果:
R> for(i in 1:length(loopvec1)){
+ for(j in 1:length(loopvec2)){
+ foo[i,j] <- loopvec1[i]*loopvec2[j]
+ }
+ }
R> foo
[,1] [,2] [,3] [,4]
[1,] 45 40 35 30
[2,] 54 48 42 36
[3,] 63 56 49 42
请注意,嵌套循环需要为每个for循环使用唯一的loopindex。在这种情况下,外部循环的loopindex是i,内部循环的loopindex是j。当代码执行时,i首先被赋值为1,然后开始内部循环,此时j也被赋值为1。内部循环中唯一的命令是将loopvec1的第i个元素与loopvec2的第j个元素相乘,并将结果赋值给foo矩阵的第i行、第j列。内部循环会重复执行,直到j达到length(loopvec2),填满foo的第一行;然后,i递增,重新启动内部循环。整个过程将在i达到length(loopvec1)并填满矩阵后完成。
内部的loopvector甚至可以被定义为与外部循环的当前loopindex值相匹配。以下是使用之前的loopvec1和loopvec2的一个示例:
R> foo <- matrix(NA,length(loopvec1),length(loopvec2))
R> foo
[,1] [,2] [,3] [,4]
[1,] NA NA NA NA
[2,] NA NA NA NA
[3,] NA NA NA NA
R> for(i in 1:length(loopvec1)){
+ for(j in 1:i){
+ foo[i,j] <- loopvec1[i]+loopvec2[j]
+ }
+ }
R> foo
[,1] [,2] [,3] [,4]
[1,] 14 NA NA NA
[2,] 15 14 NA NA
[3,] 16 15 14 NA
在这里,foo矩阵的第i行、第j列元素被填充为loopvec1[i]与loopvec2[j]的和。然而,内部循环的j值现在是根据i的值来决定的。例如,当i为1时,内部的loopvector是1:1,因此内部循环只执行一次,然后返回外部循环。当i为2时,内部的loopvector是1:2,依此类推。这使得foo的每一行仅部分被填充。以这种方式编写循环时需要格外小心。例如,在这里,j的值依赖于loopvec1的长度,因此如果length(loopvec1)大于length(loopvec2),就会发生错误。
任意数量的for循环可以嵌套使用,但如果嵌套循环使用不当,计算开销可能会成为问题。循环一般会增加一些计算成本,因此在 R 中编写更高效的代码时,你应该始终问自己:“我能否以面向向量的方式来做这件事?”只有当单独的操作不可能或无法轻松批量实现时,才应考虑探索迭代的、循环的方法。你可以在 Ligges 和 Fox 的《R Help Desk》文章中找到一些关于 R 循环和相关最佳实践编程的有价值评论(2008)。
习题 10.3
-
为了提高编码效率,请重新编写本节中的嵌套循环示例,该示例将矩阵
foo填充为loopvec1和loopvec2元素的倍数,改为仅使用一个for循环。 -
在第 10.1.5 节中,你使用了命令
switch(EXPR=mystring,Homer=12,Marge=34,Bart=56,Lisa=78,Maggie=90, NA)该命令根据提供的单字符字符串值返回一个数字。如果
mystring是字符向量,则此行代码将无法正常工作。编写一些代码,接受一个字符向量并返回一个适当的数字值向量。用以下向量进行测试:c("Peter","Homer","Lois","Stewie","Maggie","Bart") -
假设你有一个名为
mylist的列表,它可以包含其他列表作为成员,但假设这些“成员列表”本身不能包含列表。编写嵌套循环,能够搜索任何以这种方式定义的mylist,并统计其中有多少个矩阵。提示:只需在开始循环之前设置一个计数器,每次找到矩阵时递增,无论它是mylist的直接成员,还是mylist的成员列表中的成员。然后确认以下内容:
-
如果你有以下内容,答案是 4:
mylist <- list(aa=c(3.4,1),bb=matrix(1:4,2,2), cc=matrix(c(T,T,F,T,F,F),3,2),dd="string here", ee=list(c("hello","you"),matrix(c("hello", "there"))), ff=matrix(c("red","green","blue","yellow"))) -
如果你有以下内容,答案是 0:
mylist <- list("tricked you",as.vector(matrix(1:6,3,2))) -
如果你有以下内容,答案是 2:
mylist <- list(list(1,2,3),list(c(3,2),2), list(c(1,2),matrix(c(1,2))), rbind(1:10,100:91))
-
10.2.2 while 循环
要使用for循环,你必须知道或能够轻松计算循环应重复的次数。如果你不知道需要运行多少次所需的操作,可以使用while循环。while循环在指定的条件返回TRUE时运行并重复,并且具有以下通用形式:
while(loopcondition){
do any code in here
}
while循环使用单一的逻辑值loopcondition来控制循环重复的次数。在执行时,loopcondition会被评估。如果条件为TRUE,则代码块会按常规逐行执行,直到完成,此时会再次检查loopcondition。循环仅在条件评估为FALSE时终止,而且是立即终止——代码块不会再执行最后一次。
这意味着代码块中执行的操作必须以某种方式导致循环退出,要么通过以某种方式影响loopcondition,要么通过声明break,稍后你会看到。如果没有,循环将会永远重复下去,形成一个无限循环,这将冻结控制台(并且,根据代码块中的操作,R 可能会因为内存限制崩溃)。如果发生这种情况,你可以通过点击顶部菜单的停止按钮或按 ESC 键在 R 用户界面中终止循环。
作为一个简单的while循环示例,考虑以下代码:
myval <- 5
while(myval<10){
myval <- myval+1
cat("\n'myval' is now",myval,"\n")
cat("'mycondition' is now",myval<10,"\n")
}
在这里,你将一个新对象myval设置为5。然后你开始一个while循环,条件为myval<10。由于一开始条件为TRUE,你进入了代码块。在循环内部,你将myval加 1,打印当前值,并打印条件myval<5的逻辑值。循环会继续,直到下次评估时条件myval<10为FALSE。执行代码块,你会看到以下结果:
'myval' is now 6
'mycondition' is now TRUE
'myval' is now 7
'mycondition' is now TRUE
'myval' is now 8
'mycondition' is now TRUE
'myval' is now 9
'mycondition' is now TRUE
'myval' is now 10
'mycondition' is now FALSE
正如预期的那样,循环会重复直到myval被设置为10,此时myval<10返回FALSE,导致循环退出,因为初始条件不再是TRUE。
在更复杂的设置中,通常将 loopcondition 设置为一个独立的对象是非常有用的,这样你可以在大括号内根据需要修改它。在下一个示例中,你将使用 while 循环迭代一个整数向量,并创建一个单位矩阵(参见 第 3.3.2 节),其维度与当前整数匹配。这个循环应该在遇到向量中的一个大于 5 的数字时停止,或者当它到达整数向量的末尾时停止。
在编辑器中,定义一些初始对象,然后是循环本身。
mylist <- list()
counter <- 1
mynumbers <- c(4,5,1,2,6,2,4,6,6,2)
mycondition <- mynumbers[counter]<=5
while(mycondition){
mylist[[counter]] <- diag(mynumbers[counter])
counter <- counter+1
if(counter<=length(mynumbers)){
mycondition <- mynumbers[counter]<=5
} else {
mycondition <- FALSE
}
}
第一个对象 mylist 将存储循环创建的所有矩阵。你将使用向量 mynumbers 提供矩阵的大小,并使用 counter 和 mycondition 来控制循环。
loopcondition,即 mycondition,最初设置为 TRUE,因为 mynumbers 的第一个元素小于或等于 5。在从 while 开始的循环内,第一行使用双重方括号和 counter 的值动态创建 mylist 中该位置的新条目(你之前在 第 5.1.3 节 中使用命名列表做过类似的操作)。该条目被分配一个单位矩阵,其大小与 mynumbers 中相应元素的大小匹配。接着,counter 增加,你需要更新 mycondition。在这里,你要检查 mynumbers[counter] <= 5,但还需要检查是否已经到达整数向量的末尾(否则,试图访问 mynumbers 范围外的索引位置会导致错误)。因此,可以使用 if 语句首先检查条件 counter <= length(mynumbers)。如果条件为 TRUE,则将 mycondition 设置为 mynumbers[counter] <= 5 的结果。如果条件不成立,意味着你已到达 mynumbers 的末尾,因此需要通过设置 mycondition <- FALSE 来确保循环退出。
使用那些预定义的对象执行循环,它将生成如下所示的 mylist 对象:
R> mylist
[[1]]
[,1] [,2] [,3] [,4]
[1,] 1 0 0 0
[2,] 0 1 0 0
[3,] 0 0 1 0
[4,] 0 0 0 1
[[2]]
[,1] [,2] [,3] [,4] [,5]
[1,] 1 0 0 0 0
[2,] 0 1 0 0 0
[3,] 0 0 1 0 0
[4,] 0 0 0 1 0
[5,] 0 0 0 0 1
[[3]]
[,1]
[1,] 1
[[4]]
[,1] [,2]
[1,] 1 0
[2,] 0 1
正如预期的那样,你有一个包含四个元素的列表——大小分别为 4 × 4、5 × 5、1 × 1 和 2 × 2 的单位矩阵——与 mynumbers 的前四个元素相匹配。当循环执行到 mynumbers 的第五个元素(6)时停止,因为它大于 5。
练习 10.4
-
基于最近一个将单位矩阵存储在列表中的示例,确定在不执行任何操作的情况下,对于以下每个可能的
mynumbers向量,结果mylist会是什么样子:-
mynumbers <- c(2,2,2,2,5,2) -
mynumbers <- 2:20 -
mynumbers <- c(10,1,10,1,2)
然后,在 R 中确认你的答案(注意,每次你还需要像文本中所示的那样重置
mylist、counter和mycondition的初始值)。 -
-
对于这个问题,我将介绍 阶乘 运算符。一个非负整数 x 的阶乘,表示为 x!,是 x 乘以所有小于 x 的整数的积,一直到 1。形式上,它可以这样表示:
“x的阶乘” = x! = x × (x − 1) × (x − 2) × ... × 1
请注意,零的阶乘是一个特殊情况,总是等于 1。也就是说:
0! = 1
例如,要计算 3 的阶乘,你需要做如下计算:
3 × 2 × 1 = 6
要计算 7 的阶乘,你需要做如下计算:
7 × 6 × 5 × 4 × 3 × 2 × 1 = 5040
写一个
while循环,通过每次递减mynum的值来计算并存储任意非负整数mynum的阶乘,直到循环结束。使用你的循环,确认以下内容:
-
使用
mynum <- 5时结果为120 -
使用
mynum <- 12时结果为479001600 -
当
mynum <- 0时,正确返回1
-
-
考虑以下代码,其中
while循环内的操作部分已省略:mystring <- "R fever" index <- 1 ecount <- 0 result <- mystring while(ecount<2 && index<=nchar(mystring)){ # several omitted operations # } result你的任务是完成大括号中的代码,使其逐个检查
mystring中的字符,直到达到第二个字母e或字符串的末尾,以先到者为准。如果没有第二个e,则result对象应该是整个字符字符串;如果有第二个e,则result应该是从头到第二个e之前的所有字符。例如,mystring <- "R fever"应该返回result为"R fev"。这必须通过在大括号内执行以下操作来实现:-
使用
substr(第 4.2.4 节)提取mystring中index位置的单个字符。 -
使用等式检查,判断这个单字符字符串是否为
"e"或"E"。如果是,则将ecount加1。 -
接下来,进行单独检查,查看
ecount是否等于2。如果是,使用substr将result设置为从1到index-1(包括index-1)之间的字符。 -
将
index增加1。
测试你的代码—确保
mystring <- "R fever"的前一个result。此外,确认以下内容:– 使用
mystring <- "beautiful"会得到result为"beautiful"– 使用
mystring <- "ECCENTRIC"会得到result为"ECC"– 使用
mystring <- "ElAbOrAte"会得到result为"ElAbOrAt"– 使用
mystring <- "eeeeek!"会得到result为"e" -
10.2.3 使用 apply 进行隐式循环
在某些情况下,特别是对于相对常规的for循环(例如对列表中的每个成员执行某个函数),你可以通过使用apply函数避免一些与显式循环相关的细节。apply函数是最基本的隐式循环形式——它接受一个函数,并将其应用于数组的每个维度。
对于一个简单的示例,假设你有以下矩阵:
R> foo <- matrix(1:12,4,3)
R> foo
[,1] [,2] [,3]
[1,] 1 5 9
[2,] 2 6 10
[3,] 3 7 11
[4,] 4 8 12
假设你想计算每一行的和。如果你调用以下代码,你只会得到所有元素的总和,而这不是你想要的。
R> sum(foo)
[1] 78
你也可以像这样使用for循环:
R> row.totals <- rep(NA,times=nrow(foo))
R> for(i in 1:nrow(foo)){
+ row.totals[i] <- sum(foo[i,])
+ }
R> row.totals
[1] 15 18 21 24
这将循环遍历每一行,并将总和存储在row.totals中。但你可以使用apply以更简洁的形式得到相同的结果。调用apply时,你必须指定至少三个参数。第一个参数X是你要循环处理的对象。第二个参数MARGIN是一个整数,标记了要操作的X的哪一维(行、列等)。最后,FUN提供你希望在每个维度上执行的函数。通过以下调用,你将得到与之前for循环相同的结果。
R> row.totals2 <- apply(X=foo,MARGIN=1,FUN=sum)
R> row.totals2
[1] 15 18 21 24
MARGIN索引遵循矩阵和数组维度的位置顺序,如第三章所讨论的那样——1总是指行,2指列,3指层,4指块,依此类推。要指示 R 对foo的每一列求和,只需将MARGIN参数更改为2。
R> apply(X=foo,MARGIN=2,FUN=sum)
[1] 10 26 42
FUN所提供的操作应与所选择的MARGIN相适应。因此,如果你选择了MARGIN=1或MARGIN=2的行或列,请确保FUN函数适用于向量。或者,如果你有一个三维数组并使用apply函数设置MARGIN=3,请确保将FUN设置为适用于矩阵的函数。下面是你可以输入的示例:
R> bar <- array(1:18,dim=c(3,3,2))
R> bar
, , 1
[,1] [,2] [,3]
[1,] 1 4 7
[2,] 2 5 8
[3,] 3 6 9
, , 2
[,1] [,2] [,3]
[1,] 10 13 16
[2,] 11 14 17
[3,] 12 15 18
然后,进行以下调用:
R> apply(bar,3,FUN=diag)
[,1] [,2]
[1,] 1 10
[2,] 5 14
[3,] 9 18
这将提取bar的每个矩阵层的对角元素。每次对矩阵调用diag时都会返回一个向量,这些向量会作为新矩阵的列返回。FUN参数也可以是任何适当的用户定义函数,你将在第十一章中看到使用自己函数的apply示例。
其他 apply 函数
基本的apply函数有不同的变体。例如,tapply函数对目标对象的子集执行操作,这些子集是通过一个或多个因子向量定义的。作为示例,让我们回到第 8.2.3 节的代码,该代码读取一个关于钻石定价的基于 Web 的数据文件,设置数据框的适当变量名称,并显示前五条记录。
R> dia.url <- "http://www.amstat.org/publications/jse/v9n2/4cdata.txt"
R> diamonds <- read.table(dia.url)
R> names(diamonds) <- c("Carat","Color","Clarity","Cert","Price")
R> diamonds[1:5,]
Carat Color Clarity Cert Price
1 0.30 D VS2 GIA 1302
2 0.30 E VS1 GIA 1510
3 0.30 G VVS1 GIA 1510
4 0.30 G VS1 GIA 1260
5 0.31 D VS1 GIA 1641
若要计算按Color分组的钻石总值,你可以像这样使用tapply:
R> tapply(diamonds$Price,INDEX=diamonds$Color,FUN=sum)
D E F G H I
113598 242349 392485 287702 302866 207001
这将对目标向量diamonds$Price的相关元素求和。相应的因子向量diamonds$Color传递给INDEX,感兴趣的函数通过FUN=sum指定,正如之前所做的那样。
另一个特别有用的替代方案是lapply,它可以对列表中的每个成员逐个操作。在第 10.2.1 节中,回忆一下你写了一个for循环来检查以下列表中的矩阵:
R> baz <- list(aa=c(3.4,1),bb=matrix(1:4,2,2),cc=matrix(c(T,T,F,T,F,F),3,2),
dd="string here",ee=matrix(c("red","green","blue","yellow")))
使用lapply,你可以用一行简短的代码检查列表中的矩阵。
R> lapply(baz,FUN=is.matrix)
$aa
[1] FALSE
$bb
[1] TRUE
$cc
[1] TRUE
$dd
[1] FALSE
$ee
[1] TRUE
请注意,lapply 不需要任何边界或索引信息;R 知道将 FUN 应用到指定列表的每个成员。返回值本身是一个列表。另一种变体 sapply 返回与 lapply 相同的结果,但以数组的形式呈现。
R> sapply(baz,FUN=is.matrix)
aa bb cc dd ee
FALSE TRUE TRUE FALSE TRUE
这里,结果作为一个向量提供。在这个例子中,baz 有一个 names 属性,它会被复制到返回对象的相应条目中。
apply 的其他变体包括 vapply,它类似于 sapply,但有一些相对细微的区别;还有 mapply,它可以同时操作多个向量或列表。要了解更多关于 mapply 的内容,请参阅 ?mapply 帮助文件;vapply 和 sapply 都在 ?lapply 帮助文件中有介绍。
R 的所有 apply 函数都允许将额外的参数传递给 FUN;其中大多数通过省略号来实现这一点。例如,再次查看矩阵 foo:
R> apply(foo,1,sort,decreasing=TRUE)
[,1] [,2] [,3] [,4]
[1,] 9 10 11 12
[2,] 5 6 7 8
[3,] 1 2 3 4
这里你已对矩阵的每一行应用了 sort 函数,并提供了额外的参数 decreasing=TRUE,将行按从大到小排序。
一些程序员更倾向于在可能的情况下使用一系列 apply 函数,以提高代码的简洁性和整洁性。然而,请注意,这些函数在计算速度或效率上通常不会比显式循环有任何实质性的提升(尤其是在 R 的较新版本中)。此外,当你刚开始学习 R 语言时,显式循环通常更容易阅读和理解,因为操作是逐行清晰地呈现的。
练习 10.5
-
在文本中最近的例子基础上,编写一个隐式循环,计算通过调用
apply(foo,1,sort,decreasing=TRUE)返回的矩阵中所有列元素的乘积。 -
将以下
for循环转换为一个隐式循环,实现完全相同的功能:matlist <- list(matrix(c(T,F,T,T),2,2), matrix(c("a","c","b","z","p","q"),3,2), matrix(1:8,2,4)) matlist for(i in 1:length(matlist)){ matlist[[i]] <- t(matlist[[i]]) } matlist -
在 R 中,将以下 4 × 4 × 2 × 3 数组存储为对象
qux:R> qux <- array(96:1,dim=c(4,4,2,3))即,它是一个四维数组,由三个块组成,每个块是一个由两层 4 × 4 矩阵组成的数组。然后,执行以下操作:
-
编写一个隐式循环,获取所有第二层矩阵的对角元素,生成以下矩阵:
[,1] [,2] [,3] [1,] 80 48 16 [2,] 75 43 11 [3,] 70 38 6 [4,] 65 33 1 -
编写一个隐式循环,返回通过访问
qux中每个矩阵的第四列形成的三个矩阵的dim,无论层或块如何,再通过另一个隐式循环计算该返回结构的行和,最终得到以下向量:[1] 12 6
-
10.3 其他控制流机制
为了结束本章,你将学习另外三种控制流机制:break、next 和 repeat。这些机制通常与之前学过的循环和 if 语句一起使用。
10.3.1 声明 break 或 next
通常,for 循环只有在 loopindex 耗尽 loopvector 时才会退出,而 while 循环则只有在 loopcondition 评估为 FALSE 时才会退出。但你也可以通过声明 break 来预先终止循环。
例如,假设你有一个数字 foo,你想用它除以数值向量 bar 中的每个元素。
R> foo <- 5
R> bar <- c(2,3,1.1,4,0,4.1,3)
此外,假设你想逐个元素地将 foo 除以 bar,但如果某个结果评估为 Inf(例如除以零时),则希望停止执行。为此,你可以在每次迭代中使用 is.finite 函数(第 6.1.1 节),并在返回 FALSE 时发出 break 命令以终止循环。
R> loop1.result <- rep(NA,length(bar))
R> loop1.result
[1] NA NA NA NA NA NA NA
R> for(i in 1:length(bar)){
+ temp <- foo/bar[i]
+ if(is.finite(temp)){
+ loop1.result[i] <- temp
+ } else {
+ break
+ }
+ }
R> loop1.result
[1] 2.500000 1.666667 4.545455 1.250000 NA NA NA
在这里,循环通常进行除法运算,直到遇到 bar 的第五个元素并进行除以零的运算,结果为 Inf。经过条件检查后,循环立即结束,剩余的 loop1.result 条目保持原样——为 NA。
调用 break 是一种相对激烈的操作。通常,程序员只会在作为安全防护措施时使用它,用来突出或避免意外的计算。对于更常规的操作,最好使用其他方法。例如,示例循环完全可以通过 while 循环或面向向量的 ifelse 函数来复制,而不依赖 break。
你也可以使用 next 来代替 break,这样就可以简单地跳到下一次迭代并继续执行。考虑以下例子,其中使用 next 可以避免除以零的情况:
R> loop2.result <- rep(NA,length(bar))
R> loop2.result
[1] NA NA NA NA NA NA NA
R> for(i in 1:length(bar)){
+ if(bar[i]==0){
+ next
+ }
+ loop2.result[i] <- foo/bar[i]
+ }
R> loop2.result
[1] 2.500000 1.666667 4.545455 1.250000 NA 1.219512 1.666667
首先,循环检查 bar 的第 i 个元素是否为零。如果是,则声明 next,因此 R 会忽略循环中大括号部分的后续代码行,并自动跳到 loopindex 的下一个值。在当前的示例中,循环跳过了 bar 的第五个元素(保持该位置的原始 NA 值),并继续执行剩余的 bar。
请注意,如果你在嵌套循环中使用 break 或 next,该命令只会应用于最内层的循环。只有内层循环会退出或跳到下一次迭代,任何外层循环将正常继续。例如,让我们回到 第 10.2.1 节 中的嵌套 for 循环,这些循环用于填充一个矩阵,矩阵中是两个向量的倍数。这次你将在内层循环中使用 next 跳过某些值。
R> loopvec1 <- 5:7
R> loopvec1
[1] 5 6 7
R> loopvec2 <- 9:6
R> loopvec2
[1] 9 8 7 6
R> baz <- matrix(NA,length(loopvec1),length(loopvec2))
R> baz
[,1] [,2] [,3] [,4]
[1,] NA NA NA NA
[2,] NA NA NA NA
[3,] NA NA NA NA
R> for(i in 1:length(loopvec1)){
+ for(j in 1:length(loopvec2)){
+ temp <- loopvec1[i]*loopvec2[j]
+ if(temp>=54){
+ next
+ }
+ baz[i,j] <- temp
+ }
+ }
R> baz
[,1] [,2] [,3] [,4]
[1,] 45 40 35 30
[2,] NA 48 42 36
[3,] NA NA 49 42
如果当前元素的乘积大于或等于 54,则内层循环跳到 next 迭代。请注意,效果仅适用于 最内层的循环——即,只有 j loopindex 被预先递增,而 i 保持不变,外层循环正常继续。
我一直在使用 for 循环来说明 next 和 break,但它们在 while 循环中也表现得一样。
10.3.2 repeat 语句
另一种重复一组操作的选项是repeat语句。它的定义非常简单。
repeat{
do any code in here
}
请注意,repeat语句不包括任何类型的loopindex或loopcondition。为了停止大括号内代码的重复,你必须在大括号内使用break声明(通常在if语句中);如果没有它,大括号内的代码将无限重复,形成一个无限循环。为了避免这种情况,你必须确保操作在某个时刻会导致循环达到break。
为了展示repeat的实际应用,你将用它来计算著名的数学序列斐波那契数列。斐波那契数列是一个无限的整数序列,起始为 1,1,2,3,5,8,13,...其中每个项是前两个项的和。形式化地说,如果F[n]表示第n个斐波那契数,那么你会得到:
| F[n][+][1] = F[n] + F[n][−][1]; | n = 2,3,4,5,... |
|---|
其中
F[1] = F[2] = 1。
以下repeat语句计算并打印斐波那契数列,直到它达到大于 150 的项为止:
R> fib.a <- 1
R> fib.b <- 1
R> repeat{
+ temp <- fib.a+fib.b
+ fib.a <- fib.b
+ fib.b <- temp
+ cat(fib.b,", ",sep="")
+ if(fib.b>150){
+ cat("BREAK NOW...\n")
+ break
+ }
+ }
2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, BREAK NOW...
首先,序列通过存储前两个项(都为 1)初始化为fib.a和fib.b。然后进入repeat语句,它使用fib.a和fib.b计算序列中的下一个项,并将其存储为temp。接下来,fib.a被覆盖为fib.b,fib.b被覆盖为temp,使得这两个变量在序列中向前移动。也就是说,fib.b变成新计算的斐波那契数,fib.a变成到目前为止序列中的倒数第二个数字。然后使用cat将fib.b的新值打印到控制台。最后,会检查最新的项是否大于 150,如果是,则声明break。
当你运行代码时,大括号内的区域会反复执行,直到fib.b达到第一个大于 150 的数字,即 89 + 144 = 233。一旦发生这种情况,if语句的条件被评估为TRUE,然后 R 执行break,终止循环。
repeat语句不像标准的while或for循环那样常用,但如果你不想受限于正式指定for循环的loopindex和loopvector,或while循环的loopcondition,它是非常有用的。然而,使用repeat时,你必须更加小心,以防止出现无限循环。
习题 10.6
-
使用第 10.3.1 节中的相同对象,
foo <- 5 bar <- c(2,3,1.1,4,0,4.1,3)执行以下操作:
-
编写一个
while循环——不使用break或next——它将达到与第 10.3.1 节中的break示例完全相同的结果。也就是说,生成与文本中loop2.result相同的向量。 -
使用
ifelse函数而不是循环,获得与loop3.result相同的结果,loop3.result是关于next的示例。
-
-
为了展示第 10.2.2 节中的
while循环,你使用了向量mynumbers <- c(4,5,1,2,6,2,4,6,6,2)逐步填充
mylist,使其包含与mynumbers中的值匹配的单位矩阵。循环的指令是当它到达数值向量的末尾或遇到大于 5 的数字时停止。-
使用
break声明编写一个for循环,完成相同的操作。 -
编写一个
repeat语句,完成相同的操作。
-
-
假设你有两个列表,
matlist1和matlist2,它们的成员都是数值矩阵。假设所有成员都有有限的、非缺失的值,但不要假设矩阵的维度在整个过程中相同。编写一对嵌套的for循环,目的是根据以下指导方针创建一个结果列表reslist,该列表包含两个列表成员的所有可能的 矩阵乘积(参见 第 3.3.5 节):–
matlist1对象应该在外层循环中被索引/搜索,而matlist2对象应该在内层循环中被索引/搜索。– 你只对
matlist1中成员与matlist2中成员按顺序进行的可能矩阵乘积感兴趣。– 如果某个乘积不可行(即,如果
matlist1中某个成员的ncol不匹配matlist2中某个成员的nrow),则应跳过该乘法,在reslist中相关位置存储字符串"not possible",并直接进行下一个矩阵乘法。– 你可以定义一个
counter,在每次比较时递增(在内层循环中),以跟踪reslist的当前位置。因此,请注意,
reslist的length将等于length(matlist1)*length(matlist2)。现在,确认以下结果:-
如果你有
matlist1 <- list(matrix(1:4,2,2),matrix(1:4),matrix(1:8,4,2)) matlist2 <- matlist1那么除了成员
[[1]]和[[7]]外,reslist中的所有成员应为"not possible"。 -
如果你有
matlist1 <- list(matrix(1:4,2,2),matrix(2:5,2,2), matrix(1:16,4,2)) matlist2 <- list(matrix(1:8,2,4),matrix(10:7,2,2), matrix(9:2,4,2))那么
reslist中只有"not possible"的成员应为[[3]]、[[6]]和[[9]]。
-
本章的重要代码
| 函数/操作符 | 简要描述 | 首次出现 |
|---|---|---|
if( ){ } |
条件检查 | 第 10.1.1 节,第 180 页 |
if( ){ } else { } |
检查和替代 | 第 10.1.2 节,第 183 页 |
ifelse |
元素级 if-else 检查 |
第 10.1.3 节,第 185 页 |
switch |
多重 if 选择 |
第 10.1.5 节,第 190 页 |
for( ){ } |
迭代循环 | 第 10.2.1 节,第 194 页 |
while( ){ } |
条件循环 | 第 10.2.2 节,第 200 页 |
apply |
按边缘隐式循环 | 第 10.2.3 节,第 205 页 |
tapply |
按因子隐式循环 | 第 10.2.3 节,第 207 页 |
lapply |
按成员隐式循环 | 第 10.2.3 节, 第 207 页 |
sapply |
与lapply类似,返回数组 |
第 10.2.3 节, 第 207 页 |
break |
退出显式循环 | 第 10.3.1 节, 第 210 页 |
next |
跳过到下一个循环迭代 | 第 10.3.1 节, 第 210 页 |
repeat{ } |
重复执行代码直到遇到break |
第 10.3.2 节, 第 212 页 |
第十一章:11
编写函数

定义函数可以让你重复使用一段代码,而无需反复复制粘贴。它还允许其他用户使用你的函数对他们自己的数据或对象执行相同的计算。在本章中,你将学习如何编写自己的 R 函数。你将了解如何定义和使用参数,如何从函数中返回输出,以及如何以其他方式对函数进行专业化。
11.1 函数命令
要定义一个函数,使用function命令并将结果赋给一个对象名称。完成此操作后,你可以像使用其他内置或贡献函数一样,通过该对象名称调用该函数。本节将引导你了解函数创建的基础知识,并讨论一些相关问题,例如返回对象和指定参数。
11.1.1 函数创建
函数定义始终遵循以下标准格式:
functionname <- function(arg1,arg2,arg3,...){
do any code in here when called
return(returnobject)
}
functionname占位符可以是任何有效的 R 对象名称,最终你将使用它来调用该函数。将functionname赋值为function的调用,后跟括号,括号内是你希望函数拥有的任何参数。伪代码包括三个参数占位符和一个省略号。当然,参数的数量、标签以及是否包括省略号都取决于你定义的具体函数。如果函数不需要任何参数,只需包含空括号:()。如果你确实在这个定义中包含参数,请注意,它们不是工作区中的对象,也没有任何类型或class属性—它们仅仅是一个参数名称的声明,这些参数是functionname所需要的。
当函数被调用时,它会执行大括号区域中的代码(也称为函数体或函数代码)。它可以包括if语句、循环,甚至其他函数调用。在执行过程中遇到内部函数调用时,R 会遵循第九章中讨论的搜索规则。在大括号区域中,你可以使用arg1、arg2和arg3,它们会被当作函数词法环境中的对象来处理。
根据这些声明的参数在函数体中的使用方式,每个参数可能需要特定的数据类型和对象结构。如果你编写的是打算供他人使用的函数,确保有完善的文档说明函数的预期要求是非常重要的。
通常,函数主体会包含一个或多个return命令的调用。当 R 在执行过程中遇到return语句时,函数将退出,将控制权交还给用户的命令提示符。这个机制允许你将函数内部操作的结果返回给用户。这个输出在伪代码中由returnobject表示,通常是一个在函数主体中较早创建或计算的对象。如果没有return语句,函数将简单地返回最后执行表达式所创建的对象(我将在第 11.1.2 节中详细讨论这个特性)。
现在是时候来看一个例子了。我们从第 10.3.2 节中提取斐波那契数列生成器,并将其转化为编辑器中的一个函数。
myfib <- function(){
fib.a <- 1
fib.b <- 1
cat(fib.a,", ",fib.b,", ",sep="")
repeat{
temp <- fib.a+fib.b
fib.a <- fib.b
fib.b <- temp
cat(fib.b,", ",sep="")
if(fib.b>150){
cat("BREAK NOW...")
break
}
}
}
我将函数命名为myfib,并且它不使用或要求任何参数。代码主体与第 10.3.2 节中的示例完全相同,唯一不同的是我添加了第三行代码cat(fib.a,", ",fib.b,", ",sep=""),确保前两个数值 1 和 1 也会被打印到屏幕上。
在你能够从控制台调用myfib之前,你需要将函数定义传送到控制台。选中编辑器中的代码并按 CTRL-R 或
-RETURN。
R> myfib <- function(){
+ fib.a <- 1
+ fib.b <- 1
+ cat(fib.a,", ",fib.b,", ",sep="")
+ repeat{
+ temp <- fib.a+fib.b
+ fib.a <- fib.b
+ fib.b <- temp
+ cat(fib.b,", ",sep="")
+ if(fib.b>150){
+ cat("BREAK NOW...")
+ break
+ }
+ }
+ }
这会将函数导入工作空间(如果你在命令提示符下输入ls(),"myfib"现在会出现在当前对象列表中)。每次你创建或修改一个函数,并且想要在命令提示符下使用它时,都需要执行这一步骤。
现在,你可以从控制台调用这个函数了。
R> myfib()
1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, BREAK NOW...
它计算并打印出最大为 250 的斐波那契数列,正如要求的那样。
添加参数
与其打印一个固定的数列,让我们添加一个参数来控制打印多少个斐波那契数。考虑下面这个新的函数myfib2,它进行了这种修改:
myfib2 <- function(thresh){
fib.a <- 1
fib.b <- 1
cat(fib.a,", ",fib.b,", ",sep="")
repeat{
temp <- fib.a+fib.b
fib.a <- fib.b
fib.b <- temp
cat(fib.b,", ",sep="")
if(fib.b>thresh){
cat("BREAK NOW...")
break
}
}
}
这个版本现在接受一个单一的参数thresh。在代码主体中,thresh充当一个阈值,用于决定何时结束repeat过程、停止打印并完成函数——一旦计算出的fib.b值大于thresh,repeat语句将在遇到break调用时退出。因此,打印到控制台的输出将是包含第一个大于thresh的fib.b值的斐波那契数列。这意味着thresh必须作为单一的数值提供——例如,提供一个字符字符串是没有意义的。
将myfib2的定义导入控制台后,请注意,当你设置thresh=150时,得到的结果与原始myfib相同。
R> myfib2(thresh=150)
1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, BREAK NOW...
但是现在,你可以将数列打印到任何你想要的限制(这次使用位置匹配来指定参数):
R> myfib2(1000000)
1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584,
4181, 6765, 10946, 17711, 28657, 46368, 75025, 121393, 196418, 317811,
514229, 832040, 1346269, BREAK NOW...
返回结果
如果你想在未来的操作中使用函数的结果(而不是仅仅将输出打印到控制台),你需要将内容返回给用户。继续使用当前的例子,这是一个将序列存储在向量中并返回的斐波那契函数:
myfib3 <- function(thresh){
fibseq <- c(1,1)
counter <- 2
repeat{
fibseq <- c(fibseq,fibseq[counter-1]+fibseq[counter])
counter <- counter+1
if(fibseq[counter]>thresh){
break
}
}
return(fibseq)
}
首先,你创建了向量fibseq并将其赋值为序列的前两个项。最终,这个向量将成为returnobject。你还创建了一个counter,初始化为2,用于跟踪当前在fibseq中的位置。然后,函数进入一个repeat语句,它通过c(fibseq,fibseq[counter-1]+fibseq[counter])来覆盖fibseq。这个表达式通过将最近两个项的和添加到fibseq的当前内容中,构造出一个新的fibseq。例如,当counter从2开始时,第一次运行这一行会将fibseq[1]和fibseq[2]相加,并将结果作为第三项添加到原来的fibseq中。
接下来,counter被递增,并且检查条件。如果fibseq[counter]的最新值不大于thresh,循环将继续。如果大于,循环会中断,并且你会到达myfib3的最后一行。调用return结束函数,并返回指定的returnobject(在此情况下,是fibseq的最终内容)。
导入myfib3后,考虑以下代码:
R> myfib3(150)
[1] 1 1 2 3 5 8 13 21 34 55 89 144 233
R> foo <- myfib3(10000)
R> foo
[1] 1 1 2 3 5 8 13 21 34 55 89 144
[13] 233 377 610 987 1597 2584 4181 6765 10946
R> bar <- foo[1:5]
R> bar
[1] 1 1 2 3 5
这里,第一行调用了myfib3并将thresh赋值为150。输出仍然会打印到屏幕上,但这不是之前通过cat命令得到的结果,而是returnobject。你可以将这个returnobject赋值给一个变量,比如foo,此时foo就成了全局环境中的另一个 R 对象,可以进行操作。例如,你可以利用它创建一个简单的向量子集bar。这是在使用myfib或myfib2时无法做到的。
11.1.2 使用 return
如果函数内部没有return语句,函数会在执行完最后一行代码后结束,此时它会返回函数中最后创建或赋值的对象。如果没有创建任何对象,比如之前的myfib和myfib2,函数会返回NULL。为了说明这一点,请在编辑器中输入以下两个示例函数:
dummy1 <- function(){
aa <- 2.5
bb <- "string me along"
cc <- "string 'em up"
dd <- 4:8
}
dummy2 <- function(){
aa <- 2.5
bb <- "string me along"
cc <- "string 'em up"
dd <- 4:8
return(dd)
}
第一个函数dummy1简单地在其词法环境中(而不是全局环境中)赋值了四个不同的对象,并没有显式地返回任何内容。另一方面,dummy2创建了相同的四个对象,并显式地返回最后一个对象dd。如果你导入并运行这两个函数,它们都会返回相同的对象。
R> foo <- dummy1()
R> foo
[1] 4 5 6 7 8
R> bar <- dummy2()
R> bar
[1] 4 5 6 7 8
函数会在评估到return命令时立即结束,而不会执行函数体中剩余的任何代码。为了强调这一点,考虑一下另一个版本的示例函数:
dummy3 <- function(){
aa <- 2.5
bb <- "string me along"
return(aa)
cc <- "string 'em up"
dd <- 4:8
return(bb)
}
在这里,dummy3函数有两个return调用:一个在中间,另一个在末尾。但是,当你导入并执行该函数时,它只返回一个值。
R> baz <- dummy3()
R> baz
[1] 2.5
执行dummy3只会返回对象aa,因为只有第一个return语句被执行,函数会立即在这一点退出。在当前定义的dummy3中,最后三行(cc和dd的赋值以及bb的return)永远不会被执行。
使用return会向你的代码添加另一个函数调用,因此从技术上讲,它会引入一些额外的计算开销。因此,有人认为,除非绝对必要,否则应避免使用return语句。但调用return的额外计算成本对于大多数用途来说足够小,可以忽略不计。而且,return语句可以使代码更具可读性,更容易看到函数作者打算在哪一处结束函数,并明确希望返回什么作为输出。在剩下的内容中,我将始终使用return。
练习 11.1
-
编写另一个斐波那契数列函数,命名为
myfib4。该函数应提供一个选项,可以执行myfib2中的操作(即仅将序列打印到控制台),或者执行myfib3中的操作(即正式返回一个序列向量)。你的函数应接受两个参数:第一个,thresh,应定义序列的限制(就像myfib2或myfib3一样);第二个,printme,应为逻辑值。如果为TRUE,则myfib4应仅打印;如果为FALSE,则myfib4应返回一个向量。通过以下调用验证结果是否正确:–
myfib4(thresh=150,printme=TRUE)–
myfib4(1000000,T)–
myfib4(150,FALSE)–
myfib4(1000000,printme=F) -
在练习 10.4 中,第 203 页要求你编写一个
while循环来执行整数阶乘计算。-
使用你的阶乘
while循环(如果你之前没有写,可以编写一个),写一个 R 函数myfac,用来计算整数参数int的阶乘(你可以假设int总是作为非负整数传入)。通过计算 5 的阶乘(即 120)、12 的阶乘(即 479,001,600)和 0 的阶乘(即 1)来快速测试该函数。 -
编写另一个版本的阶乘函数,命名为
myfac2。这次,你仍然可以假设int将作为整数传入,但不能假设它一定是非负数。如果是负数,函数应返回NaN。在之前的三个测试值上测试myfac2,同时尝试使用int=-6。
-
11.2 参数
参数是大多数 R 函数中不可或缺的一部分。在这一部分,你将考虑 R 如何求值参数。你还将看到如何编写具有默认参数值的函数,如何使函数处理缺失的参数值,以及如何通过省略号将额外的参数传递到内部函数调用中。
11.2.1 延迟求值
处理许多高级编程语言中参数的一个重要概念是延迟求值。通常,这指的是只有在需要时才会对表达式进行求值。这也适用于参数,即它们仅在函数体中出现时才会被访问和使用。
让我们看看 R 函数在执行过程中如何识别和使用参数。作为本节的工作示例,您将编写一个函数,在指定的列表中搜索矩阵对象,并尝试将每个矩阵与作为第二个参数指定的另一个矩阵进行后乘(有关矩阵乘法的详细信息,请参考第 3.3.5 节)。该函数将存储并返回结果到一个新列表中。如果提供的列表中没有矩阵,或者没有适合的矩阵(根据乘法矩阵的维度),函数应该返回一个字符字符串,告知用户这些情况。您可以假设,如果指定的列表中有矩阵,它们将是数值型的。考虑以下函数,我称之为multiples1:
multiples1 <- function(x,mat,str1,str2){
matrix.flags <- sapply(x,FUN=is.matrix)
if(!any(matrix.flags)){
return(str1)
}
indexes <- which(matrix.flags)
counter <- 0
result <- list()
for(i in indexes){
temp <- x[[i]]
if(ncol(temp)==nrow(mat)){
counter <- counter+1
result[[counter]] <- temp%*%mat
}
}
if(counter==0){
return(str2)
} else {
return(result)
}
}
这个函数接受四个参数,没有默认值。要搜索的目标列表应该传递给x;进行后乘的矩阵传递给mat;另外两个参数str1和str2接收字符字符串,如果x中没有合适的成员时返回这些字符串。
在代码主体内部,创建了一个名为matrix.flags的向量,使用sapply隐式循环函数。该函数将is.matrix应用于列表参数x。结果是一个与x等长的逻辑向量,TRUE元素表示x中对应的成员确实是矩阵。如果x中没有矩阵,函数会触发return语句,退出函数并输出参数str1。
如果函数在那个点没有退出,意味着x中确实包含矩阵。接下来的步骤是通过将which应用于matrix.flags来检索矩阵成员的索引。初始化一个counter为0,用于跟踪成功执行了多少次矩阵乘法,并创建一个空列表(result)用于存储结果。
接下来,进入for循环。对于indexes中的每个成员,循环会将该位置的矩阵成员存储为temp,并检查是否可以将temp与参数mat进行后乘(为了执行操作,ncol(temp)必须等于nrow(mat))。如果矩阵兼容,counter增加,并且在result的对应位置填充相关计算结果。如果为FALSE,则不执行任何操作。索引器i然后取indexes中的下一个值,并继续重复直到完成。
multiples1中的最终程序检查for循环是否找到了任何兼容的矩阵乘积。如果没有兼容的矩阵,for循环内的花括号if语句代码将不会执行,且counter将保持为零。因此,如果counter在循环结束时仍然等于零,函数将简单地返回str2参数。否则,如果找到了兼容的矩阵,适当的结果将被计算,并且multiples1将返回result列表,列表中至少有一个成员。
现在是时候导入并测试这个函数了。你将使用以下三个列表对象:
R> foo <- list(matrix(1:4,2,2),"not a matrix",
"definitely not a matrix",matrix(1:8,2,4),matrix(1:8,4,2))
R> bar <- list(1:4,"not a matrix",c(F,T,T,T),"??")
R> baz <- list(1:4,"not a matrix",c(F,T,T,T),"??",matrix(1:8,2,4))
你将把参数mat设置为 2 × 2 的单位矩阵(将任何合适的矩阵与它后乘将只是返回原始矩阵),并为str1和str2传入适当的字符串消息。以下是该函数在foo上的执行方式:
R> multiples1(x=foo,mat=diag(2),str1="no matrices in 'x'",
str2="matrices in 'x' but none of appropriate dimensions given
'mat'")
[[1]]
[,1] [,2]
[1,] 1 3
[2,] 2 4
[[2]]
[,1] [,2]
[1,] 1 5
[2,] 2 6
[3,] 3 7
[4,] 4 8
函数已经返回了result,包含foo的两个兼容矩阵(成员[[1]]和[[5]])。现在,让我们使用相同的参数尝试它在bar上的表现。
R> multiples1(x=bar,mat=diag(2),str1="no matrices in 'x'",
str2="matrices in 'x' but none of appropriate dimensions given
'mat'")
[1] "no matrices in 'x'"
这次,返回了str1的值。初步检查确认在提供给x的列表中没有矩阵,因此函数在for循环之前已经退出。最后,让我们尝试baz。
R> multiples1(x=baz,mat=diag(2),str1="no matrices in 'x'",
str2="matrices in 'x' but none of appropriate dimensions given
'mat'")
[1] "matrices in 'x' but none of appropriate dimensions given 'mat'"
在这里返回了str2的值。尽管baz中有一个矩阵,且multiples1的for循环体内的代码已经执行,但该矩阵不适合用mat进行后乘。
请注意,字符串参数str1和str2仅在参数x不包含具有适当维度的矩阵时使用。例如,当你将multiples1应用于x=foo时,根本不需要使用str1或str2。R 会懒惰地评估已定义的表达式,这意味着只有在执行过程中实际需要这些参数时,才会查找其值。在此函数中,str1和str2仅在输入列表中没有合适的矩阵时才需要,因此,当x=foo时,你可以懒惰地忽略为这些参数提供值。
R> multiples1(x=foo,mat=diag(2))
[[1]]
[,1] [,2]
[1,] 1 3
[2,] 2 4
[[2]]
[,1] [,2]
[1,] 1 5
[2,] 2 6
[3,] 3 7
[4,] 4 8
这与之前的结果相同,完全没有问题。然而,尝试用bar来进行此操作则无法成功。
R> multiples1(x=bar,mat=diag(2))
Error in multiples1(x = bar, mat = diag(2)) :
argument "str1" is missing, with no default
在这里,R 正确地提醒我们需要str1的值。它告诉我们该值缺失且没有默认值。
11.2.2 设置默认值
上一个例子展示了在某些情况下设置默认值对于特定参数是有用的。在许多其他情况下,设置默认参数值也是合理的,例如当函数有大量参数时,或者当参数有自然的值并且这些值使用得更频繁时。让我们编写multiples1函数的一个新版本,multiples2,它现在包含str1和str2的默认值,参见第 11.2.1 节。
multiples2 <- function(x,mat,str1="no valid matrices",str2=str1){
matrix.flags <- sapply(x,FUN=is.matrix)
if(!any(matrix.flags)){
return(str1)
}
indexes <- which(matrix.flags)
counter <- 0
result <- list()
for(i in indexes){
temp <- x[[i]]
if(ncol(temp)==nrow(mat)){
counter <- counter+1
result[[counter]] <- temp%*%mat
}
}
if(counter==0){
return(str2)
} else {
return(result)
}
}
在这里,你为str1提供了一个默认值"no valid matrices",通过在参数的正式定义中为其赋值字符串。你还通过将str1赋值给它来为str2设置了默认值。如果你再次导入并执行此函数,针对三个列表,你不再需要显式地为这些参数提供值。
R> multiples2(foo,mat=diag(2))
[[1]]
[,1] [,2]
[1,] 1 3
[2,] 2 4
[[2]]
[,1] [,2]
[1,] 1 5
[2,] 2 6
[3,] 3 7
[4,] 4 8
R> multiples2(bar,mat=diag(2))
[1] "no valid matrices"
R> multiples2(baz,mat=diag(2))
[1] "no valid matrices"
现在,无论结果如何,你都可以调用该函数,而无需完全指定每个参数。如果你在某次调用中不想使用默认参数,你仍然可以为这些参数指定不同的值,而这些值将覆盖默认值。
11.2.3 检查缺失的参数
missing函数检查函数的参数,看看是否所有必需的参数都已提供。它接受一个参数标签,并返回一个逻辑值TRUE,如果指定的参数没有找到。你可以使用missing来避免在之前调用multiples1时看到的错误,当时str1是必需的但没有提供。
在某些情况下,missing函数在代码主体中尤其有用。考虑对示例函数的另一个修改:
multiples3 <- function(x,mat,str1,str2){
matrix.flags <- sapply(x,FUN=is.matrix)
if(!any(matrix.flags)){
if(missing(str1)){
return("'str1' was missing, so this is the message")
} else {
return(str1)
}
}
indexes <- which(matrix.flags)
counter <- 0
result <- list()
for(i in indexes){
temp <- x[[i]]
if(ncol(temp)==nrow(mat)){
counter <- counter+1
result[[counter]] <- temp%*%mat
}
}
if(counter==0){
if(missing(str2)){
return("'str2' was missing, so this is the message")
} else {
return(str2)
}
} else {
return(result)
}
}
这个版本和multiples1之间的唯一区别在于第一个和最后一个if语句。第一个if语句检查x中是否没有矩阵,在这种情况下返回一条字符串消息。在multiples1中,该消息始终是str1,但现在你使用另一个if语句和missing(str1)来检查str1参数是否有值。如果没有,函数将返回另一条字符字符串,说明str1是“缺失的”。对str2也定义了类似的替代方案。这里再次导入该函数并使用foo、bar和baz:
R> multiples3(foo,diag(2))
[[1]]
[,1] [,2]
[1,] 1 3
[2,] 2 4
[[2]]
[,1] [,2]
[1,] 1 5
[2,] 2 6
[3,] 3 7
[4,] 4 8
R> multiples3(bar,diag(2))
[1] "'str1' was missing, so this is the message"
R> multiples3(baz,diag(2))
[1] "'str2' was missing, so this is the message"
以这种方式使用missing可以允许在给定的函数调用中不提供某些参数。它主要用于当某个参数很难选择默认值时,但函数仍然需要处理没有提供该参数的情况。在当前示例中,为str1和str2定义默认值更加合理,正如你为multiples2所做的那样,而避免了实现missing所需的额外代码。
11.2.4 处理省略号
在第 9.2.5 节中,我介绍了省略号,也称为点点点符号。省略号允许你传入额外的参数,而不必先在参数列表中定义它们,然后这些参数可以传递给代码主体中的另一个函数调用。当省略号包含在函数定义中时,它通常(但不总是)放在最后一个位置,因为它表示参数的可变数量。
基于第 11.1.1 节中的myfib3函数,让我们使用省略号编写一个可以绘制指定斐波那契数列的函数。
myfibplot <- function(thresh,plotit=TRUE,...){
fibseq <- c(1,1)
counter <- 2
repeat{
fibseq <- c(fibseq,fibseq[counter-1]+fibseq[counter])
counter <- counter+1
if(fibseq[counter]>thresh){
break
}
}
if(plotit){
plot(1:length(fibseq),fibseq,...)
} else {
return(fibseq)
}
}
在这个函数中,一个if语句检查plotit参数是否为TRUE(这是默认值)。如果是这样,那么你调用plot,传入1:length(fibseq)作为* x 轴的坐标,斐波那契数列本身作为 y *轴的坐标。在这些坐标之后,你还将省略号直接传递给plot。在这种情况下,省略号表示用户可能传递给plot的任何其他参数,用以控制图形的执行。
导入myfibplot并执行以下代码后,图形设备中将弹出图形图 11-1。
R> myfibplot(150)
这里,你使用位置匹配将150分配给thresh,并为plotit参数保留默认值。此调用中的省略号为空。

图 11-1:通过调用 myfibplot 产生的默认图形,参数为 thresh=150
由于你没有另行指定,R 会按照plot的默认行为运行。你可以通过指定更多的绘图选项来美化图形。以下代码将生成图 11-2 中的图形:
R> myfibplot(150,type="b",pch=4,lty=2,main="Terms of the Fibonacci sequence",
ylab="Fibonacci number",xlab="Term (n)")

图 11-2:通过调用 myfibplot 并使用省略号传递图形参数生成的图形
这里,省略号允许你通过调用myfibplot将参数传递给plot,即使特定的图形参数并未显式定义为myfibplot的参数。
省略号可以很方便,但需要小心使用。模糊的...可以代表任何数量的神秘参数。良好的函数文档是指示适当用法的关键。
如果你想解包通过省略号传递的参数,可以使用list函数将这些参数转换为列表。以下是一个示例:
unpackme <- function(...){
x <- list(...)
cat("Here is ... in its entirety as a list:\n")
print(x)
cat("\nThe names of ... are:",names(x),"\n\n")
cat("\nThe classes of ... are:",sapply(x,class))
}
这个虚拟函数简单地接受一个省略号,并通过x <- list(...)将其转换为一个列表。这样,x对象就可以像其他列表一样进行处理。在这种情况下,你可以通过提供其names和class属性来总结该对象。以下是一个示例运行:
R> unpackme(aa=matrix(1:4,2,2),bb=TRUE,cc=c("two","strings"),
dd=factor(c(1,1,2,1)))
Here is ... in its entirety as a list:
$aa
[,1] [,2]
[1,] 1 3
[2,] 2 4
$bb
[1] TRUE
$cc
[1] "two" "strings"
$dd
[1] 1 1 2 1
Levels: 1 2
The names of ... are: aa bb cc dd
The classes of ... are: matrix logical character factor
四个带标签的参数,aa、bb、cc和dd,作为省略号的内容提供,并通过简单的list(...)操作在unpackme中显式识别。此结构可用于识别或提取通过...在给定调用中传递的特定参数。
练习 11.2
-
累积年复利是投资者常见的财务收益。给定一个本金投资额P,年利率i(以百分比表示),以及每年支付利息的频率t,经过y年后的最终金额F可以通过以下公式计算:
![image]()
编写一个函数,按照以下说明计算F:
– 必须为P、i、t和y提供参数。t的默认值应为 12。
– 另一个参数给出一个逻辑值,用于确定是否应包括在每个整数时间点
plot显示金额 F。例如,如果plotit=TRUE(默认值)且 y 为 5 年,则图表应显示 F 在 y = 1,2,3,4,5 时的金额。– 如果绘制此函数,图表应始终为步骤图,因此
plot应始终使用type="s"。– 如果
plotit=FALSE,则应返回最终金额 F,作为与前面显示的相同整数时间对应的数值向量。– 如果进行绘图,还应包括省略号以控制其他细节。
现在,使用你的函数,执行以下操作:
-
计算一个$5000 的本金,在年利率 4.4%、按月复利的条件下,10 年后的最终金额。
-
重新绘制以下步骤图,显示在每年 22.9%的利率下,按月复利投资$100,持续 20 年的结果:
![image]()
-
基于(ii)中的相同参数,进行另一项计算,但这次假设利息按年复利。返回并存储结果作为数值向量。然后,使用
lines将一个第二条步骤线(对应于按年复利计算的金额)添加到之前创建的图表中。使用不同的颜色或线型,并利用legend函数区分两条线。
-
-
x 的二次方程通常以以下形式表示:
k[1]x² + k[2]x + k[3] = 0
其中,k[1]、k[2] 和 k[3] 是常数。给定这些常数的值后,你可以尝试找到最多两个实根—即满足方程的x值。编写一个函数,接受 k[1]、k[2] 和 k[3] 作为参数,并在这种情况下找到并返回任何解(作为一个数值向量)。实现方法如下:
– 评估
。如果它为负,则没有解,应在控制台打印适当的消息。– 如果
为零,则有一个解,通过 −k[2]/2k[1] 计算得出。– 如果
为正,则有两个解,分别由
和
给出。– 不需要为这三个参数提供默认值,但函数应检查是否有任何参数缺失。如果有,则应返回一个适当的字符字符串消息,通知用户无法进行计算。
现在,测试你的函数。
-
确认以下内容:
-
2x² − x − 5 的根为 1.850781 和 −1.350781。
-
x² + x + 1 没有实根。
-
-
尝试解决以下二次方程的解:
-
1.3x² − 8x − 3.13
-
2.25x² − 3x + 1
-
1.4x² − 2.2x − 5.1
-
−5x² + 10.11x − 9.9
-
-
测试当函数的一个参数缺失时,你编写的响应。
-
11.3 专门函数
在本节中,你将了解三种特殊的用户自定义 R 函数。首先,你将了解辅助函数,它们设计为可以被另一个函数多次调用(甚至可以在父函数体内定义)。接下来,你将了解一次性函数,它们可以直接作为另一个函数调用的参数来定义。最后,你将了解递归函数,它们会调用自己。
11.3.1 辅助函数
R 函数在其函数体内调用其他函数是很常见的。辅助函数是一个通用术语,用来描述专门编写并用于便捷地执行另一个函数计算的函数。它们是提高复杂函数可读性的好方法。
辅助函数可以是内部定义的(在另一个函数定义内)或外部定义的(在全局环境中)。在本节中,你将看到每种情况的示例。
外部定义
基于第 11.2.2 节中的multiples2函数,下面是一个新版本,它将功能拆分成两个独立的函数,其中一个是外部定义的辅助函数:
multiples_helper_ext <- function(x,matrix.flags,mat){
indexes <- which(matrix.flags)
counter <- 0
result <- list()
for(i in indexes){
temp <- x[[i]]
if(ncol(temp)==nrow(mat)){
counter <- counter+1
result[[counter]] <- temp%*%mat
}
}
return(list(result,counter))
}
multiples4 <- function(x,mat,str1="no valid matrices",str2=str1){
matrix.flags <- sapply(x,FUN=is.matrix)
if(!any(matrix.flags)){
return(str1)
}
helper.call <- multiples_helper_ext(x,matrix.flags,mat)
result <- helper.call[[1]]
counter <- helper.call[[2]]
if(counter==0){
return(str2)
} else {
return(result)
}
}
如果你导入并执行这个代码,使用之前的示例列表,它的行为和前一个版本相同。你所做的只是将矩阵检查循环移到了一个外部函数中。multiples4函数现在调用一个名为multiples_helper_ext的辅助函数。一旦multiples4中的代码确认列表x中确实有需要检查的矩阵,它就会调用multiples_helper_ext来执行所需的循环。这个辅助函数是外部定义的,这意味着它存在于全局环境中,任何其他函数都可以调用它,这使得它更容易重用。
内部定义
如果辅助函数只打算用于一个特定的函数,那么将辅助函数定义在调用它的函数的词法环境内更有意义。矩阵乘法函数的第五个版本正是这样做的,它将定义移到了函数体内。
multiples5 <- function(x,mat,str1="no valid matrices",str2=str1){
matrix.flags <- sapply(x,FUN=is.matrix)
if(!any(matrix.flags)){
return(str1)
}
multiples_helper_int <- function(x,matrix.flags,mat){
indexes <- which(matrix.flags)
counter <- 0
result <- list()
for(i in indexes){
temp <- x[[i]]
if(ncol(temp)==nrow(mat)){
counter <- counter+1
result[[counter]] <- temp%*%mat
}
}
return(list(result,counter))
}
helper.call <- multiples_helper_int(x,matrix.flags,mat)
result <- helper.call[[1]]
counter <- helper.call[[2]]
if(counter==0){
return(str2)
} else {
return(result)
}
}
现在,辅助函数multiples_helper_int在multiples5内定义。这意味着它仅在词法环境内可见,而不像multiples_helper_ext那样存在于全局环境中。当(a)一个辅助函数仅由一个父函数使用,并且(b)它在父函数中被多次调用时,将它定义为内部函数是有意义的。(当然,multiples5只满足(a),它在这里仅用于说明。)
11.3.2 一次性函数
通常,你可能需要一个执行简单、一行任务的函数。例如,当你使用apply时,你通常只需要传入一个简短、简单的函数作为参数。这就是临时(或匿名)函数的用武之地——它们允许你定义一个仅用于单一实例的函数,而无需在全局环境中显式创建一个新对象。
假设你有一个数值矩阵,想要将其每列重复两次并进行排序。
R> foo <- matrix(c(2,3,3,4,2,4,7,3,3,6,7,2),3,4)
R> foo
[,1] [,2] [,3] [,4]
[1,] 2 4 7 6
[2,] 3 2 3 7
[3,] 3 4 3 2
这是apply的完美任务,apply可以将一个函数应用到矩阵的每一列。这个函数只需接受一个向量,重复它,然后对结果进行排序。你无需单独定义这个简短的函数,而是可以在apply的参数中使用function命令直接定义一个临时函数。
R> apply(foo,MARGIN=2,FUN=function(x){sort(rep(x,2))})
[,1] [,2] [,3] [,4]
[1,] 2 2 3 2
[2,] 2 2 3 2
[3,] 3 4 3 6
[4,] 3 4 3 6
[5,] 3 4 7 7
[6,] 3 4 7 7
该函数直接在调用apply时以标准格式定义。函数被定义、调用,并且在apply完成后立即被遗忘。它是临时的,因为它仅存在于实际使用的那个实例中。
以这种方式使用function命令更多的是一种快捷方式;此外,它还避免了在全局环境中不必要地创建和存储一个函数对象。
11.3.3 递归函数
递归是指一个函数调用它自己。这种技术在统计分析中不常用,但了解它仍然很有用。本节将简要说明一个函数如何调用它自己。
假设你想编写一个函数,接受一个单一的正整数参数n并返回相应的第 n 项斐波那契数列(其中n = 1 和n = 2 分别对应初始的两个项 1 和 1)。早些时候你通过循环构建了斐波那契数列的迭代形式。在递归函数中,函数不使用循环来重复操作,而是多次调用自身。请考虑以下内容:
myfibrec <- function(n){
if(n==1||n==2){
return(1)
} else {
return(myfibrec(n-1)+myfibrec(n-2))
}
}
递归函数myfibrec检查一个单一的if语句,用来定义停止条件。如果传递给函数的是1或2(请求第一个或第二个斐波那契数),那么myfibrec会直接返回1。否则,函数返回myfibrec(n-1)和myfibrec(n-2)的和。这意味着,如果你调用myfibrec并传入n大于2,函数将生成两个额外的myfibrec调用,分别使用n-1和n-2。递归会持续直到遇到请求第 1 项或第 2 项的调用,触发停止条件if(n==1||n==2),此时函数仅返回1。以下是一个示例调用,用于获取第五个斐波那契数:
R> myfibrec(5)
[1] 5
图 11-3 展示了这个递归调用的结构。
请注意,任何递归函数都需要一个可访问的停止规则。没有停止规则,递归将会无限进行。例如,当前的myfibrec函数定义只要用户提供一个正整数作为参数n,就能正常工作。但如果n是负数,停止规则条件将永远不会满足,函数会无限递归下去(尽管 R 语言有一些自动化的保护措施来帮助防止这种情况,并应当返回错误消息,而不是陷入无限循环)。
递归是一种强大的方法,特别是当你无法预知函数需要被调用多少次才能完成任务时。对于许多排序和搜索算法,递归提供了最快和最有效的解决方案。但在更简单的情况中,比如这里的斐波那契例子,递归方法往往比迭代循环方法需要更多的计算资源。对于初学者,我建议除非严格要求使用递归,否则最好使用显式的循环。

图 11-3:对myfibrec函数的递归调用可视化,n=5
练习 11.3
-
给定一个包含不同长度字符字符串向量的列表,使用一次性函数与
lapply将一个感叹号附加到每个元素的末尾,分隔符为空字符串(请注意,paste在应用于字符向量时的默认行为是对每个元素进行连接)。在以下列表上执行此代码:foo <- list("a",c("b","c","d","e"),"f",c("g","h","i")) -
编写一个递归版本的函数,实现非负整数阶乘操作符(有关阶乘操作符的详细信息,请参见练习 10.4,见第 203 页)。停止规则应当在提供的整数为
0时返回1。确认你的函数输出与之前的结果相同。-
5 的阶乘是 120。
-
120 的阶乘是 479,001,600。
-
0 的阶乘是 1。
-
-
对于这个问题,我将介绍几何平均数。几何平均数是一个特定的集中趋势度量,区别于更常见的算术平均数。给定n个观察值,分别记作x[1]、x[2]、...、x[n],它们的几何平均数
的计算公式如下:![image]()
例如,要找到数据 4.3、2.1、2.2、3.1 的几何平均数,可以按以下方式计算:
![image]()
(此值已四舍五入至小数点后一位。)
编写一个名为
geolist的函数,它能够遍历指定的列表,并根据以下指导原则计算每个成员的几何平均数:– 你的函数必须定义并使用一个内部辅助函数,该函数返回向量参数的几何平均数。
– 假设列表的成员只能是数字向量或数字矩阵。你的函数应包含一个适当的循环来依次检查每个成员。
– 如果成员是一个向量,计算该向量的几何平均值,用结果覆盖该成员,结果应该是一个单一的数字。
– 如果成员是一个矩阵,使用隐式循环计算矩阵每一行的几何平均值,并用结果覆盖该成员。
– 最终的列表应返回给用户。
现在,作为一个快速测试,检查你的函数是否与以下两个调用匹配:
-
R> foo <- list(1:3,matrix(c(3.3,3.2,2.8,2.1,4.6,4.5,3.1,9.4),4,2), matrix(c(3.3,3.2,2.8,2.1,4.6,4.5,3.1,9.4),2,4)) R> geolist(foo) [[1]] [1] 1.817121 [[2]] [1] 3.896152 3.794733 2.946184 4.442972 [[3]] [1] 3.388035 4.106080 -
R> bar <- list(1:9,matrix(1:9,1,9),matrix(1:9,9,1),matrix(1:9,3,3)) R> geolist(bar) [[1]] [1] 4.147166 [[2]] [1] 4.147166 [[3]] [1] 1 2 3 4 5 6 7 8 9 [[4]] [1] 3.036589 4.308869 5.451362
-
本章中的重要代码
| 函数/运算符 | 简短描述 | 首次出现 |
|---|---|---|
function |
函数创建 | 第 11.1.1 节,第 216 页 |
return |
函数返回对象 | 第 11.1.1 节,第 219 页 |
missing |
参数检查 | 第 11.2.3 节,第 227 页 |
... |
省略号(作为参数) | 第 11.2.4 节,第 228 页 |
第十二章:12
异常、计时和可见性

现在你已经看到如何在 R 中编写自己的函数,我们来看看一些常见的函数扩展和行为。在本章中,你将学习如何让你的函数在接收到意外输入时抛出错误或警告。你还将了解一些简单的方式来测量完成时间并检查计算密集型函数的进度。最后,你将看到 R 如何在两个同名但位于不同包中的函数之间进行遮蔽。
12.1 异常处理
当函数执行过程中遇到意外问题时,R 会通知你,可能是一个警告或错误。在本节中,我将演示如何在适当的情况下将这些构造体构建到你自己的函数中。我还将展示如何尝试一个计算,检查它是否可以在没有错误的情况下执行(即,看看它是否能正常工作)。
12.1.1 正式通知:错误和警告
在第十一章,当你的函数无法执行某些操作时,你让它们打印出一个字符串(例如,"无效矩阵")。警告和错误是更正式的机制,用于传达这些类型的信息并处理后续操作。错误会强制函数在发生错误的地方立即终止。警告则较为轻微,表示函数以不典型的方式运行,但会尝试绕过问题并继续执行。在 R 中,你可以使用warning命令发出警告,使用stop命令抛出错误。以下两个函数展示了各自的例子:
warn_test <- function(x){
if(x<=0){
warning("'x' is less than or equal to 0 but setting it to 1 and
continuing")
x <- 1
}
return(5/x)
}
error_test <- function(x){
if(x<=0){
stop("'x' is less than or equal to 0... TERMINATE")
}
return(5/x)
}
warn_test和error_test都将 5 除以参数x。它们也都期望x是正数。在warn_test中,如果x是非正数,函数会发出警告,并将x的值覆盖为1。而在error_test中,如果x是非正数,函数会抛出一个错误并立即终止。两个命令warning和stop都使用字符字符串参数,作为打印到控制台的消息。
你可以通过以下方式导入并调用这些函数来查看通知:
R> warn_test(0)
[1] 5
Warning message:
In warn_test(0) :
'x' is less than or equal to 0 but setting it to 1 and continuing
R> error_test(0)
Error in error_test(0) : 'x' is less than or equal to 0... TERMINATE
注意,warn_test已继续执行并返回了值5—这是将x设为1后,5/1的结果。error_test的调用没有返回任何内容,因为 R 在stop命令处退出了该函数。
警告在函数即使未得到预期的输入时,仍能以某种自然方式尝试自我修复时非常有用。例如,在第 10.1.3 节中,当你为 if 语句提供了一个逻辑向量时,R 会发出警告。请记住,if 语句期望一个单一的逻辑值,但如果提供了一个逻辑向量,它不会退出,而是继续执行,使用提供向量中的第一个条目。也就是说,有时实际上抛出错误并完全停止执行会更为合适。
让我们回到第 11.3.3 节中的 myfibrec 函数。这个函数期望一个正整数(它应该返回的斐波那契数的位置)。假设你认为如果用户提供了一个负整数,那么用户实际上是想要这个数值的正数版本。你可以添加一个警告来处理这种情况。同时,如果用户输入 0,这个数值在斐波那契数列中没有对应的位置,代码将抛出错误。考虑以下修改:
myfibrec2 <- function(n){
if(n<0){
warning("Assuming you meant 'n' to be positive -- doing that instead")
n <- n*-1
} else if(n==0){
stop("'n' is uninterpretable at 0")
}
if(n==1||n==2){
return(1)
} else {
return(myfibrec2(n-1)+myfibrec2(n-2))
}
}
在 myfibrec2 中,你现在检查 n 是否为负数或零。如果是负数,函数会发出警告并在交换参数符号后继续执行。如果 n 为零,错误会终止执行并显示相应的消息。你可以看到以下不同参数的响应:
R> myfibrec2(6)
[1] 8
R> myfibrec2(-3)
[1] 2
Warning message:
In myfibrec2(-3) :
Assuming you meant 'n' to be positive -- doing that instead
R> myfibrec2(0)
Error in myfibrec2(0) : 'n' is uninterpretable at 0
请注意,调用 myfibrec2(-3) 返回的是第三个斐波那契数。
广义来说,错误和警告都表明发生了问题。如果你正在使用某个函数或运行代码块,并遇到这些信息,你应该仔细查看已执行的操作以及可能导致这些问题的原因。
注意
识别和修复错误代码被称为 调试 ,对此有多种策略。最基本的策略之一是使用 print 或 cat 命令,在实时执行过程中检查计算的各种量。R 确实有一些更复杂的调试工具;如果你有兴趣,可以查看在《The Art of R Programming》一书中由 Matloff 提供的第十三章的精彩讨论(2011)。更多的一般性讨论可以在 Matloff 和 Salzman 合著的《The Art of Debugging》(2008)中找到。随着你在 R 中积累更多经验,理解错误信息或在问题出现之前定位潜在问题变得越来越容易,这是你部分得益于 R 的解释性风格。
12.1.2 使用 try 语句捕获错误
当一个函数因错误终止时,它也会终止任何父函数的执行。例如,如果函数 A 调用函数 B,而函数 B 因错误停止执行,这会导致函数 A 在同一位置停止执行。为了避免这种严重后果,你可以使用 try 语句来尝试函数调用并检查是否产生错误。你还可以使用 if 语句来指定替代操作,而不是让所有流程停止。
例如,如果你调用之前的 myfibrec2 函数并传入 0,函数会抛出错误并终止。但是,看看当你将该函数调用作为第一个参数传递给 try 时会发生什么:
R> attempt1 <- try(myfibrec2(0),silent=TRUE)
似乎什么都没有发生。错误去哪了?实际上,错误仍然发生了,但由于你将 silent 设置为 TRUE,try 抑制了错误信息的打印。错误信息现在被存储在对象 attempt1 中,该对象属于 "try-error" 类。要查看错误,只需将 attempt1 打印到控制台:
R> attempt1
[1] "Error in myfibrec2(0) : 'n' is uninterpretable at 0\n"
attr(,"class")
[1] "try-error"
attr(,"condition")
<simpleError in myfibrec2(0): 'n' is uninterpretable at 0>
如果你将 silent 设置为 FALSE,你会看到这个错误信息打印到控制台。以这种方式捕获错误非常有用,尤其是当一个函数在另一个函数的主体代码中产生错误时。使用 try,你可以在不终止父函数的情况下处理错误。
同时,如果你将一个函数传递给 try,而该函数没有抛出错误,那么 try 就不会产生任何影响,你会得到正常的返回值。
R> attempt2 <- try(myfibrec2(6),silent=TRUE)
R> attempt2
[1] 8
在这里,你用一个有效的参数 n=6 执行了 myfibrec2。由于此调用没有产生错误,传递给 attempt2 的结果是 myfibrec2 的正常返回值,在此情况下是 8。
在函数主体中使用 try
让我们看一个更完整的例子,展示如何在更大的函数中使用 try。以下 myfibvector 函数接受一个索引向量作为参数 nvec,并提供斐波那契数列中相应的项:
myfibvector <- function(nvec){
nterms <- length(nvec)
result <- rep(0,nterms)
for(i in 1:nterms){
result[i] <- myfibrec2(nvec[i])
}
return(result)
}
这个函数使用 for 循环逐个处理 nvec 中的元素,利用之前的函数 myfibrec2 计算相应的斐波那契数。只要 nvec 中的所有值非零,myfibvector 就能正常工作。例如,以下调用会得到第一个、第二个、第十个和第八个斐波那契数:
R> foo <- myfibvector(nvec=c(1,2,10,8))
R> foo
[1] 1 1 55 21
假设有一个错误,nvec 中的某个条目是零。
R> bar <- myfibvector(nvec=c(3,2,7,0,9,13))
Error in myfibrec2(nvec[i]) : 'n' is uninterpretable at 0
当在 n=0 时调用 myfibrec2 时,内部调用抛出了一个错误,这导致 myfibvector 执行终止。没有返回任何结果,整个调用失败。
你可以通过在 for 循环中使用 try 来防止这种失败,检查每次调用 myfibrec2 并捕获任何错误。以下函数 myfibvectorTRY 就实现了这一点。
myfibvectorTRY <- function(nvec){
nterms <- length(nvec)
result <- rep(0,nterms)
for(i in 1:nterms){
attempt <- try(myfibrec2(nvec[i]),silent=T)
if(class(attempt)=="try-error"){
result[i] <- NA
} else {
result[i] <- attempt
}
}
return(result)
}
在for循环中,你使用attempt存储每次调用myfibrec2的结果。然后,你检查attempt。如果该对象的类是"try-error",则表示myfibrec2产生了错误,你在result向量中相应的位置填充NA。否则,attempt将代表myfibrec2的有效返回值,因此你将其放入result向量的相应位置。现在,如果你导入并在相同的nvec上调用myfibvectorTRY,你会看到完整的结果集。
R> baz <- myfibvectorTRY(nvec=c(3,2,7,0,9,13))
R> baz
[1] 2 1 13 NA 34 233
本来会导致终止的错误被悄无声息地捕获,替代的响应是NA,它被插入到result向量中。
注意
try命令是 R 中更复杂的tryCatch函数的简化版本,后者超出了本书的讨论范围,但它提供了更精确的控制方式,用于测试和执行代码块。如果你有兴趣了解更多,输入?tryCatch以获得帮助。
抑制警告消息
在我展示的所有try调用中,我都将silent参数设置为TRUE,这样可以停止打印错误消息。如果将silent设置为FALSE(默认值),则错误消息会被打印出来,但错误仍然会被捕获而不会终止执行。
请注意,设置silent=TRUE仅会抑制错误消息,而不会抑制警告消息。请观察以下内容:
R> attempt3 <- try(myfibrec2(-3),silent=TRUE)
Warning message:
In myfibrec2(-3) :
Assuming you meant 'n' to be positive -- doing that instead
R> attempt3
[1] 2
尽管silent设置为TRUE,但仍然会发出警告(在这个例子中是针对n的负值)。警告在这种情况下与错误被分别处理,因为它们应该被分开处理——警告可以在代码执行过程中高亮显示其他未预见到的问题。如果你完全确定不希望看到任何警告,可以使用suppressWarnings。
R> attempt4 <- suppressWarnings(myfibrec2(-3))
R> attempt4
[1] 2
suppressWarnings函数应仅在你确定可以安全忽略某个调用中的每个警告,并且希望保持输出整洁时使用。
习题 12.1
-
在习题 11.3 (b)中,第 238 页的任务是编写一个递归的 R 函数来计算整数阶乘,给定某个非负整数
x。现在,修改你的函数,使其在x为负时抛出错误(并给出适当的消息)。通过以下方法测试你的新函数响应:-
x为5 -
x为8 -
x为-8
-
-
矩阵求逆的概念在第 3.3.6 节中简要讨论,仅对某些方阵(列数与行数相等的矩阵)有效。这些逆矩阵可以通过
solve函数来计算,例如:R> solve(matrix(1:4,2,2)) [,1] [,2] [1,] -2 1.5 [2,] 1 -0.5请注意,如果提供的矩阵无法求逆,
solve会抛出错误。考虑到这一点,编写一个 R 函数,尝试根据以下指南对列表中的每个矩阵进行求逆:– 该函数应接受四个参数。
-
要测试是否能进行矩阵求逆的列表
x -
一个值
noninv,如果x的给定矩阵成员无法求逆,则填充结果,默认值为NA。 -
一个字符字符串
nonmat,如果x的给定成员不是矩阵,则返回该结果,默认值为"not a matrix"。 -
一个逻辑值
silent,默认为TRUE,传递给try函数的主体代码。
– 函数应该首先检查
x是否为列表。如果不是,应该抛出一个带有适当信息的错误。– 然后,函数应确保
x至少包含一个成员。如果没有,应该抛出一个带有适当信息的错误。– 接下来,函数应检查
nonmat是否为字符字符串。如果不是,应该尝试使用适当的“as-dot”函数(见第 6.2.4 节)将其强制转换为字符字符串,并且应发出适当的警告。– 在这些检查之后,循环应该检查列表
x的每个成员i。-
如果成员
i是矩阵,尝试使用try对其进行求逆。如果可以无误地求逆,则用结果覆盖x中的成员i。如果捕获到错误,则应用noninv的值覆盖x中的成员i。 -
如果成员
i不是矩阵,则应用nonmat的值覆盖x中的成员i。
– 最后,修改后的列表
x应被返回。现在,使用以下参数值测试你的函数,以确保其按预期响应:
-
x为list(1:4,matrix(1:4,1,4),matrix(1:4,4,1),matrix(1:4,2,2))
以及所有其他参数均使用默认值。
-
x如(i)所示,noninv为Inf,nonmat为666,silent使用默认值。 -
重复(ii),但这次
silent=FALSE。 -
x为
list(diag(9),matrix(c(0.2,0.4,0.2,0.1,0.1,0.2),3,3), rbind(c(5,5,1,2),c(2,2,1,8),c(6,1,5,5),c(1,0,2,0)), matrix(1:6,2,3),cbind(c(3,5),c(6,5)),as.vector(diag(2)))以及
noninv为"unsuitable matrix";所有其他值使用默认值。最后,通过以下调用测试错误信息,确保你的函数能按预期响应:
-
x为"hello" -
x为list()
-
12.2 进度和计时
R 常用于长时间的数值计算,如模拟或随机变量生成。对于这些复杂、耗时的操作,通常很有用的是跟踪进度或查看某个任务完成所花的时间。例如,你可能想要比较两种不同编程方法在解决同一问题时的速度。在本节中,你将学习如何计时代码执行并显示其进度。
12.2.1 文本进度条:我们快到了吗?
一个进度条显示 R 在执行一组操作时的进展情况。为了演示这一点,你需要运行一些需要一定时间才能执行的代码,你可以通过让 R休眠来实现。Sys.sleep命令使 R 在继续执行之前暂停指定的秒数。
R> Sys.sleep(3)
如果你运行这段代码,R 将在继续使用控制台之前暂停三秒钟。休眠将被用作在这部分中替代由于计算量大而造成的延迟,这正是进度条最有用的地方。
要更常规地使用Sys.sleep,可以考虑以下方式:
sleep_test <- function(n){
result <- 0
for(i in 1:n){
result <- result + 1
Sys.sleep(0.5)
}
return(result)
}
sleep_test函数是基本的——它接受一个正整数n,并在n次迭代中,每次都将result值加1。在每次迭代中,你还会告诉循环休眠半秒。由于有这个休眠命令,执行以下代码大约需要四秒钟才能返回结果:
R> sleep_test(8)
[1] 8
现在,假设你想要跟踪这种类型的函数执行进度。你可以通过三步来实现文本进度条:使用txtProgressBar初始化进度条对象,使用setTxtProgressBar更新进度条,使用close终止进度条。下一个函数prog_test修改了sleep_test,加入了这三个命令。
prog_test <- function(n){
result <- 0
progbar <- txtProgressBar(min=0,max=n,style=1,char="=")
for(i in 1:n){
result <- result + 1
Sys.sleep(0.5)
setTxtProgressBar(progbar,value=i)
}
close(progbar)
return(result)
}
在for循环之前,你通过调用txtProgressBar并传入四个参数来创建一个名为progbar的对象。min和max参数是定义进度条范围的数值。在这种情况下,你设置max=n,它与即将执行的for循环的迭代次数相匹配。style参数(整数,可以是1、2或3)和char参数(字符字符串,通常是单个字符)决定了进度条的外观。设置style=1表示进度条将仅显示一行char;如果char="=",则会显示一系列等号。
一旦创建了这个对象,你需要通过调用setTxtProgressBar来指示进度条在执行过程中实际前进。你将进度条对象(progbar)和需要更新的value(在这种情况下是i)传递给它。完成后(退出循环之后),进度条必须通过调用close来终止,传入相关的进度条对象。导入并执行prog_test,你将看到等号"="在循环完成时逐步绘制出来。
R> prog_test(8)
================================================================
[1] 8
进度条的宽度默认由执行txtProgressBar命令时,R 控制台窗格的宽度决定。你可以通过改变style和char参数来稍微定制进度条。例如,选择style=3会显示进度条,并且还会有一个“完成百分比”计数器。一些包还提供了更复杂的选项,比如弹出小部件,但文本版本是最简单且在不同系统中兼容性最好的版本。
12.2.2 测量完成时间:需要多长时间?
如果你想知道一个计算任务需要多长时间才能完成,可以使用Sys.time命令。该命令输出一个对象,详细列出基于你系统的当前日期和时间信息。
R> Sys.time()
[1] "2016-03-06 16:39:27 NZDT"
你可以在某些代码执行前后存储这些对象,然后比较它们,以查看经过了多少时间。在编辑器中输入以下内容:
t1 <- Sys.time()
Sys.sleep(3)
t2 <- Sys.time()
t2-t1
现在高亮显示这四行并在控制台中执行它们。
R> t1 <- Sys.time()
R> Sys.sleep(3)
R> t2 <- Sys.time()
R> t2-t1
Time difference of 3.012889 secs
通过一起执行整个代码块,你可以轻松地衡量总完成时间,并将格式良好的字符串打印到控制台。请注意,解释和调用任何命令都需要一点时间,除了你告诉 R 休眠的三秒钟外,这个时间在不同计算机之间会有所不同。
如果你需要更详细的计时报告,有更复杂的工具可供使用。例如,你可以使用proc.time()来获得不仅是总的“墙钟”时间,还包括计算机相关的 CPU 时间(请参见帮助文件?proc.time中的定义)。要计时一个单独的表达式,你还可以使用system.time函数(它的输出细节与proc.time相同)。还有基准测试工具(对不同方法的正式或系统性比较)用于计时你的代码;例如,参见rbenchmark包(Kusnierczyk, 2012)。然而,对于日常使用,本文使用的时间对象差分方法易于理解,并提供了关于计算开销的良好指示。
习题 12.2
-
修改第 12.2.1 节中的
prog_test,使其参数列表中包含省略号,旨在接收txtProgressBar中的附加参数;将新函数命名为prog_test_fancy。计时prog_test_fancy执行所需的时间。设置50为n,通过省略号指示进度条使用style=3,并将进度条字符设置为"r"。 -
在第 12.1.2 节中,你定义了一个名为
myfibvectorTRY的函数(它本身调用了第 12.1.1 节中的myfibrec2),用于根据提供的“项向量”nvec返回斐波那契数列的多个项。编写一个新版本的myfibvectorTRY,其中包含一个style=3的进度条,以及你选择的字符,在每次通过内部for循环时递增。然后,执行以下操作:-
使用你的新函数重新生成文本中
nvec=c(3,2,7,0,9,13)的结果。 -
计时使用你的新函数返回斐波那契数列前 35 项所需的时间。你注意到了什么?这说明了你的递归斐波那契函数的什么问题?
-
-
继续使用斐波那契数列。编写一个独立的
for循环,用来计算并存储前 35 项(与(b)(ii)中的相同)。并进行计时。你更喜欢哪种方法?
12.3 屏蔽
由于 R 中有大量内建和贡献的数据和功能,几乎不可避免地,你会在某些时候遇到那些在不同加载的包中共享相同名称的对象,通常是函数。
那么,在那些情况下会发生什么呢?例如,假设你定义了一个与已加载的 R 包中的函数同名的函数。R 会通过屏蔽其中一个对象来响应——也就是说,一个对象或函数将优先于另一个,并假定该对象或函数的名称,而被屏蔽的函数必须通过额外的命令进行调用。这可以防止对象互相覆盖或阻塞。在本节中,你将了解 R 中最常见的两种屏蔽情况。
12.3.1 函数与对象的区别
当不同环境中的两个函数或对象具有相同的名称时,搜索路径中较早的对象会覆盖较晚的对象。也就是说,当搜索该对象时,R 会使用它首先找到的对象或函数,且你需要额外的代码才能访问另一个被覆盖的版本。记住,你可以通过执行 search() 来查看当前的搜索路径。
R> search()
[1] ".GlobalEnv" "tools:RGUI" "package:stats"
[4] "package:graphics" "package:grDevices" "package:utils"
[7] "package:datasets" "package:methods" "Autoloads"
[10] "package:base"
当 R 搜索时,搜索路径中最接近起始位置(全局环境)的函数或对象会首先被找到,并覆盖路径中稍后的同名函数或对象。为了展示遮蔽的简单例子,你将定义一个与基础包中的 sum 函数同名的函数:sum。以下是 sum 正常工作的方式,它会将向量 foo 中的所有元素加起来:
R> foo <- c(4,1.5,3)
R> sum(foo)
[1] 8.5
现在,假设你输入以下函数:
sum <- function(x){
result <- 0
for(i in 1:length(x)){
result <- result + x[i]²
}
return(result)
}
这个版本的 sum 接收一个向量 x,并使用 for 循环将每个元素平方后再求和并返回结果。这可以毫无问题地导入到 R 控制台,但显然,它提供的功能与内建的(原始)版本的 sum 不同。现在,在导入该函数后,如果你调用 sum,将使用你自己定义的版本。
R> sum(foo)
[1] 27.25
之所以发生这种情况,是因为用户自定义的函数存储在全局环境(.GlobalEnv)中,而全局环境总是位于搜索路径的最前面。R 的内建函数属于 base 包,它位于搜索路径的最后。此时,用户自定义的函数遮蔽了原函数。
现在,如果你希望 R 运行 base 版本的 sum,你必须在调用时包括它所属包的名称,并使用双冒号。
R> base::sum(foo)
[1] 8.5
这会告诉 R 使用 base 中的版本,即使全局环境中有另一个版本的函数。
为了避免任何混淆,让我们从全局环境中移除 sum 函数。
R> rm(sum)
当包对象发生冲突时
当你加载一个包时,R 会通知你包中的任何对象是否与当前会话中可以访问的其他对象发生冲突。为了说明这一点,我将使用两个贡献包:car 包(你在 练习 8.1 (b) 中见过,位于 第 162 页)和 spatstat 包(你将在 第 V 部分中使用)。确保这两个包已经安装后,当我按照以下顺序加载它们时,我会看到这个信息:
R> library("spatstat")
spatstat 1.40-0 (nickname: 'Do The Maths')
For an introduction to spatstat, type 'beginner'
R> library("car")
Attaching package: 'car'
The following object is masked from 'package:spatstat':
ellipse
这表明两个包中各自有一个同名对象——ellipse。R 自动通知你该对象正在被遮蔽。注意,car 和 spatstat 的功能仍然完全可用;只是如果需要使用 ellipse 对象,它们需要加以区分。使用提示符中的 ellipse 将访问 car 的对象,因为该包加载得更晚。若要使用 spatstat 的版本,必须输入 spatstat::ellipse。这些规则同样适用于访问各自的帮助文件。
当你加载一个包含被全局环境对象(全局环境对象总是优先于包对象)的对象时,会出现类似的通知。要查看一个例子,你可以加载MASS包(Venables 和 Ripley,2002),这个包是 R 自带的,但不会自动加载。继续在当前的 R 会话中,创建以下对象:
R> cats <- "meow"
现在,假设你需要加载MASS。
R> library("MASS")
Attaching package: 'MASS'
The following object is masked _by_ '.GlobalEnv':
cats
The following object is masked from 'package:spatstat':
area
加载包后,你会被通知到,你刚创建的cats对象正在遮蔽MASS中同名的对象。(如你在?MASS::cats中所见,这个对象是一个包含家猫体重测量的 数据框。)此外,MASS似乎也与spatstat共享一个对象名称——area。对于该特定项,显示了与之前相同的“包遮蔽”消息。
卸载包
你可以从搜索路径中卸载已加载的包。根据本讨论中加载的包,我当前的搜索路径如下:
R> search()
[1] ".GlobalEnv" "package:MASS" "package:car"
[4] "package:spatstat" "tools:RGUI" "package:stats"
[7] "package:graphics" "package:grDevices" "package:utils"
[10] "package:datasets" "package:methods" "Autoloads"
[13] "package:base"
现在,假设你不再需要car。你可以通过detach函数将其移除,方法如下。
R> detach("package:car",unload=TRUE)
R> search()
[1] ".GlobalEnv" "package:MASS" "package:spatstat"
[4] "tools:RGUI" "package:stats" "package:graphics"
[7] "package:grDevices" "package:utils" "package:datasets"
[10] "package:methods" "Autoloads" "package:base"
这将从路径中移除选定的包,卸载其命名空间。现在,car的功能不再立即可用,spatstat的ellipsis函数也不再被遮蔽。
注意
随着贡献包被维护者更新,它们可能会包含新的对象,导致新的遮蔽,或者移除或重命名以前引起遮蔽的对象(与其他贡献包相比)。这里所示的car、spatstat和MASS之间的具体遮蔽发生在写作时的版本,并可能在未来发生变化。
12.3.2 数据框变量区分
还有一种常见情况,你会明确收到遮蔽通知:当你将一个数据框添加到搜索路径时。让我们看看这如何工作。继续在当前工作区中,定义以下数据框:
R> foo <- data.frame(surname=c("a","b","c","d"),
sex=c(0,1,1,0),height=c(170,168,181,180),
stringsAsFactors=F)
R> foo
surname sex height
1 a 0 170
2 b 1 168
3 c 1 181
4 d 0 180
数据框foo有三个列变量:person、sex和height。要访问这些列中的一个,通常需要使用$运算符,输入类似foo$surname的内容。然而,你可以附加一个数据框到你的搜索路径,这样更容易访问一个变量。
R> attach(foo)
R> search()
[1] ".GlobalEnv" "foo" "package:MASS"
[4] "package:spatstat" "tools:RGUI" "package:stats"
[7] "package:graphics" "package:grDevices" "package:utils"
[10] "package:datasets" "package:methods" "Autoloads"
[13] "package:base"
现在surname变量可以直接访问了。
R> surname
[1] "a" "b" "c" "d"
这可以避免每次访问一个变量时都输入foo$,如果你的分析完全处理一个静态且不变的数据框,这可以是一个便捷的快捷方式。然而,如果你忘记了附加的对象,它们可能会在之后造成问题,特别是如果你在同一会话中继续将更多对象挂载到搜索路径中。例如,假设你输入了另一个数据框。
R> bar <- data.frame(surname=c("e","f","g","h"),
sex=c(1,0,1,0),weight=c(55,70,87,79),
stringsAsFactors=F)
R> bar
surname sex weight
1 e 1 55
2 f 0 70
3 g 1 87
4 h 0 79
然后也将其添加到搜索路径中。
R> attach(bar)
The following objects are masked from foo:
sex, surname
通知告诉你,bar对象现在在搜索路径中排在foo之前。
R> search()
[1] ".GlobalEnv" "bar" "foo"
[4] "package:MASS" "package:spatstat" "tools:RGUI"
[7] "package:stats" "package:graphics" "package:grDevices"
[10] "package:utils" "package:datasets" "package:methods"
[13] "Autoloads" "package:base"
结果是,任何直接使用sex或surname的操作现在将访问bar的内容,而不是foo的内容。同时,未遮蔽的变量height来自foo,仍然可以直接访问。
R> height
[1] 170 168 181 180
这是一个相当简单的例子,但它突出了在将数据框、列表或其他对象添加到搜索路径时可能出现的混淆。以这种方式挂载对象可能会迅速变得难以追踪,尤其是对于包含许多不同变量的大型数据集。因此,作为一般准则,最好避免以这种方式附加对象——除非如前所述,你仅仅在处理一个数据框。
请注意,detach可以用于从搜索路径中移除对象,方法与之前看到的移除包的方法类似。在这种情况下,你只需输入对象的名称即可。
R> detach(foo)
R> search()
[1] ".GlobalEnv" "bar" "package:MASS"
[4] "package:spatstat" "tools:RGUI" "package:stats"
[7] "package:graphics" "package:grDevices" "package:utils"
[10] "package:datasets" "package:methods" "Autoloads"
[13] "package:base"
本章重要代码
| 函数/操作符 | 简要描述 | 首次出现 |
|---|---|---|
warning |
发出警告 | 第 12.1.1 节, 第 242 页 |
stop |
抛出错误 | 第 12.1.1 节, 第 242 页 |
try |
尝试捕获错误 | 第 12.1.2 节, 第 244 页 |
Sys.sleep |
睡眠(暂停)执行 | 第 12.2.1 节, 第 249 页 |
txtProgressBar |
初始化进度条 | 第 12.2.1 节, 第 249 页 |
setTxtProgressBar |
增加进度条 | 第 12.2.1 节, 第 249 页 |
close |
关闭进度条 | 第 12.2.1 节, 第 249 页 |
Sys.time |
获取本地系统时间 | 第 12.2.2 节, 第 250 页 |
detach |
从路径中移除库/对象 | 第 12.3.1 节, 第 255 页 |
attach |
将对象附加到搜索路径 | 第 12.3.2 节, 第 256 页 |
第三部分
统计学与概率
第十三章:13
基础统计学

统计学是将数据转化为信息,以识别趋势并理解群体特征的实践。本章将涵盖一些基本定义,并使用 R 语言演示它们的应用。
13.1 描述原始数据
统计分析师常常首先面对的是原始数据——换句话说,就是构成样本的记录或观察值。根据预定分析的性质,这些数据可能存储在一个专门的 R 对象中,通常是一个数据框(第五章),也可能通过第八章中的技术从外部文件读取。在开始对数据进行汇总或建模之前,明确可用的变量是非常重要的。
变量是群体中个体的一个特征,其值在该群体中的不同实体之间可能有所不同。例如,在第 5.2 节中,你使用了一个示例数据框mydata进行实验。你记录了一个样本中人们的年龄、性别和幽默感水平。这些特征就是你的变量;所测量的值在个体之间会有所不同。
变量可以有多种形式,这取决于它们可能取值的性质。在开始使用 R 之前,我们将先看看一些描述变量的标准方式。
13.1.1 数值变量
数值变量是指其观察值自然记录为数字的变量。数值变量有两种类型:连续型和离散型。
连续变量可以记录为某个区间内的任意值,可以精确到任意小数位(技术上来说,这会导致无限多的可能值,即使连续区间的范围是有限的)。例如,如果你在观察降水量,15 毫米的值是合理的,同样,15.42135 毫米也是合理的。任何精度的测量都可以得出有效的观察值。
离散变量则不同,它只能取特定的数字值——如果其范围是有限的,那么可能的值的数量也是有限的。例如,如果你观察 20 次抛硬币的结果中正面朝上的次数,那么只能得到整数结果。观察 15.42135 次正面朝上是没有意义的;可能的结果仅限于 0 到 20(包括 20)之间的整数。
13.1.2 类别变量
尽管许多变量的观察值是数字型的,但考虑类别变量也同样重要。像一些离散变量一样,类别变量只能取有限数量的可能值。不过,与离散变量不同的是,类别观察值并不总是以数字形式记录。
分类变量有两种类型。不能逻辑排序的分类变量称为 名义型(nominal)。性别就是一个典型的名义型分类变量。在大多数数据集中,它有两个固定的值——男性和女性,而这些类别的顺序是无关紧要的。可以自然排序的分类变量称为 有序型(ordinal)。药物剂量就是一个典型的有序型分类变量,可能的值有低、中和高。这些值可以按增加或减少的顺序排列,而这种排序可能与研究相关。
注意
一些统计学文献模糊了离散变量和分类变量的定义,甚至将它们互换使用。虽然这种做法不一定错误,但为了清晰起见,我更倾向于将这些定义区分开来。也就是说,当我提到“离散”时,我指的是一种自然的数值变量,它不能用连续尺度表示(比如计数),而当我提到“分类”时,我指的是某个个体的可能结果不一定是数值的,并且可能值的数量始终是有限的。
一旦你知道该找什么,识别给定数据集中的变量类型就变得很简单。以数据框 chickwts 为例,它包含在自动加载的 datasets 包中。在提示符下,直接输入以下内容可以获取该数据集的前五条记录。
R> chickwts[1:5,]
weight feed
1 179 horsebean
2 160 horsebean
3 136 horsebean
4 227 horsebean
5 217 horsebean
R 的帮助文件(?chickwts)描述了这些数据,包含了 71 只小鸡(以克为单位)在六周后的体重数据,数据是根据提供的食物类型来分类的。现在让我们看看这两列数据,作为向量的整体:
R> chickwts$weight
[1] 179 160 136 227 217 168 108 124 143 140 309 229 181 141 260 203 148 169
[19] 213 257 244 271 243 230 248 327 329 250 193 271 316 267 199 171 158 248
[37] 423 340 392 339 341 226 320 295 334 322 297 318 325 257 303 315 380 153
[55] 263 242 206 344 258 368 390 379 260 404 318 352 359 216 222 283 332
R> chickwts$feed
[1] horsebean horsebean horsebean horsebean horsebean horsebean horsebean
[8] horsebean horsebean horsebean linseed linseed linseed linseed
[15] linseed linseed linseed linseed linseed linseed linseed
[22] linseed soybean soybean soybean soybean soybean soybean
[29] soybean soybean soybean soybean soybean soybean soybean
[36] soybean sunflower sunflower sunflower sunflower sunflower sunflower
[43] sunflower sunflower sunflower sunflower sunflower sunflower meatmeal
[50] meatmeal meatmeal meatmeal meatmeal meatmeal meatmeal meatmeal
[57] meatmeal meatmeal meatmeal casein casein casein casein
[64] casein casein casein casein casein casein casein
[71] casein
Levels: casein horsebean linseed meatmeal soybean sunflower
weight 是一个数值测量,可以落在一个连续的范围内,因此它是一个数值连续变量。尽管小鸡体重看起来已被四舍五入或记录为最接近的克数,但这并不影响该定义,因为实际上体重可以是任何合理的数字。feed 显然是一个分类变量,因为它只有六个可能的结果,而这些结果不是数值的。由于没有任何自然或容易识别的排序,可以得出结论,feed 是一个分类名义型变量。
13.1.3 单变量和多变量数据
当讨论或分析仅与一个维度相关的数据时,你正在处理 单变量 数据。例如,前面例子中的 weight 变量就是单变量的,因为每个测量值可以通过一个组成部分——一个数字来表达。
当有必要考虑具有多个维度的变量数据时(换句话说,即每个观察值关联的组件或测量不止一个),你的数据被视为 多变量 数据。当单独考虑每个组件(即作为单变量量)在任何给定的统计分析中不太有用时,多变量测量尤为相关。
一个理想的例子是空间坐标,它必须至少考虑两个分量——一个水平的x坐标和一个垂直的y坐标。仅仅考虑单变量数据——例如,仅考虑x轴值——并不特别有用。考虑quakes数据集(与chickwts类似,它可以通过datasets包自动获得),该数据集包含了在斐济海岸记录的 1,000 个地震事件的观测值。如果你查看前五条记录,并阅读帮助文件?quakes中的描述,你很快就能对所展示的内容有一个清晰的理解。
R> quakes[1:5,]
lat long depth mag stations
1 -20.42 181.62 562 4.8 41
2 -20.62 181.03 650 4.2 15
3 -26.00 184.10 42 5.4 43
4 -17.97 181.66 626 4.1 19
5 -20.42 181.96 649 4.0 11
lat和long列提供了事件的纬度和经度,depth提供了事件的深度(以公里为单位),mag提供了里氏震级,stations提供了检测到该事件的观测站数量。如果你对这些地震的空间分布感兴趣,仅仅查看纬度或经度并不能提供太多信息。每个事件的位置由两个分量描述:纬度和经度值。你可以轻松绘制这 1,000 个事件;图 13-1 展示了以下代码的结果:
R> plot(quakes$long,quakes$lat,xlab="Longitude",ylab="Latitude")

图 13-1:使用双变量(具有两个分量的多变量)变量绘制地震的空间位置
13.1.4 参数还是统计量?
如前所述,统计学作为一门学科,关注的是理解一个整体群体的特征,群体被定义为所有相关个体或实体的集合。该群体的特征称为参数。由于研究人员很少能够访问到每个群体成员的相关数据,因此他们通常会收集一个样本的实体,代表整个群体,并记录这些实体的相关数据。然后,他们可以使用样本数据来估计感兴趣的参数——这些估计值就是统计量。
例如,如果你对拥有猫的美国女性的平均年龄感兴趣,那么感兴趣的群体就是所有居住在美国并拥有至少一只猫的女性。感兴趣的参数是拥有至少一只猫的美国女性的真实平均年龄。当然,获得每一个拥有猫的美国女性的年龄将是一个困难的任务。一种更可行的方法是随机挑选少量拥有猫的美国女性,并从她们那里收集数据——这就是你的样本,而样本中女性的平均年龄就是你的统计量。
因此,统计量与参数之间的关键区别在于,特征是否指的是你所抽取数据的样本,还是更广泛的总体。图 13-2 用均值 μ 表示总体中个体的参数,而用均值 x̄ 表示从该总体抽取的个体样本的统计量。

图 13-2:一个统计实践的概念化,使用均值作为例子来说明 参数 和 统计量 的定义
练习 13.1
-
对于以下每一项,识别所描述的变量类型:数值-连续型,数值-离散型,分类-名义型,或分类-顺序型:
-
从生产线下来的汽车引擎盖上的瑕疵数量
-
一项调查问题,要求参与者从“强烈同意”、“同意”、“中立”、“不同意”和“强烈不同意”中选择
-
音乐会上的噪音水平(以分贝为单位)
-
三个可能选项中的噪音水平:高,中,低
-
选择一个主色
-
猫和老鼠之间的距离
-
-
对于以下每一项,识别所讨论的量是总体参数还是样本统计量。如果是后者,还要识别相应的总体参数是什么。
-
50 名新西兰人中拥有游戏主机的比例
-
“No Dodgy Carz”场地上三辆车引擎盖上的瑕疵数量的平均值
-
美国国内佩戴项圈的家猫比例
-
一年中自动售货机每天使用的平均次数
-
基于在该年内三个不同日期收集的数据,自动售货机每天使用的平均次数
-
13.2 概括性统计量
现在你已经学习了基本术语,准备使用 R 来计算一些统计量。在这一节中,你将了解用来概括我所讨论的不同类型变量的最常见统计量。
13.2.1 集中趋势:均值、中位数、众数
集中趋势度量 通常用于通过描述数值观察值的集中位置来解释大量数据。最常见的集中趋势度量之一当然是算术 均值。它被认为是观察值集合的中央“平衡点”。
对于一组 n 个数值型测量值标记为 x = {x[1], x[2], . . . , x[n]},你可以通过以下方式计算样本均值 x̄:

举个例子,如果你观察到的数据是 2, 4.4, 3, 3, 2, 2.2, 2, 4,均值的计算方式如下:

中位数是你观测值的“中间大小”,因此,如果你按从小到大的顺序排列观测值,可以通过取中间值(如果观测值个数是奇数)或取中间两个值的平均值(如果观测值个数是偶数)来找到中位数。使用标记为x的n个测量值,表示为x = {x[1]、x[2]、...、x[n]},你可以按如下方式找到样本中位数
:
• 将观测值从小到大排序,以获得“顺序统计量”
、
、...、
,其中
表示第t小的观测值,不论观测值编号i、j、k等。
• 然后,执行以下操作:

对于相同的数据,按从小到大的顺序排序,它们变为 2、2、2、2.2、3、3、4、4.4。对于n = 8 个观测值,n/2 = 4。因此,中位数为:

众数只是“最常见”的观测值。这个统计量通常用于数字离散数据,而非数字连续数据,尽管它也可以用于后者的区间(通常是在讨论概率密度函数时—见第十五章和第十六章)。一组n个数字测量值 x[1]、x[2]、...、x[n] 可能没有众数(如果每个观测值都是唯一的),也可能有多个众数(如果有多个特定值出现的次数最多)。要找到众数
,只需列出每个测量值的频率。
再次使用示例中的八个观测值,你可以在这里看到频率:
| 观测值 | 2 | 2.2 | 3 | 4 | 4.4 |
|---|---|---|---|---|---|
| 频率 | 3 | 1 | 2 | 1 | 1 |
值 2 出现三次,比其他任何值都更频繁,因此这些数据的唯一众数是值 2。
在 R 中,使用内置的相同名称函数可以轻松计算算术平均值和中位数。首先,将这八个观测值存储为数字向量xdata。
R> xdata <- c(2,4.4,3,3,2,2.2,2,4)
然后计算统计量。
R> x.bar <- mean(xdata)
R> x.bar
[1] 2.825
R> m.bar <- median(xdata)
R> m.bar
[1] 2.6
寻找众数最简单的方法或许是使用 R 的table函数,它可以给出所需的频率。
R> xtab <- table(xdata)
R> xtab
xdata
2 2.2 3 4 4.4
3 1 2 1 1
虽然这清楚地展示了一个小数据集的众数,但一个好的做法是编写可以自动识别任何表格中最频繁观测值的代码。min和max函数会报告最小值和最大值,而range返回这两个值,并以长度为 2 的向量形式输出。
R> min(xdata)
[1] 2
R> max(xdata)
[1] 4.4
R> range(xdata)
[1] 2.0 4.4
当应用于table时,这些命令会作用于报告的频率。
R> max(xtab)
[1] 3
最后,因此,你可以构建一个逻辑标志向量来从table中获取众数。
R> d.bar <- xtab[xtab==max(xtab)]
R> d.bar
2
3
在这里,2 是值,3 是该值的频率。
让我们回到 第 13.1.2 节中探索过的 chickwts 数据集。小鸡的均值和中位数体重如下:
R> mean(chickwts$weight)
[1] 261.3099
R> median(chickwts$weight)
[1] 258
你还可以查看 第 13.1.3 节中探索过的 quakes 数据集。数据集中最常见的地震震级如下所示,表明有 107 次震级为 4.5 的地震发生:
R> Qtab <- table(quakes$mag)
R> Qtab[Qtab==max(Qtab)]
4.5
107
注意
有几种方法可以计算中位数,尽管对于大多数实际目的,结果的影响通常可以忽略不计。在这里,我只是使用了 R 默认的“样本”版本。
R 用于从数值结构中计算统计量的许多函数,如果数据集中包含缺失或未定义值(NA 或 NaN),将无法运行。以下是一个示例:
R> mean(c(1,4,NA))
[1] NA
R> mean(c(1,4,NaN))
[1] NaN
为了防止在用户不知情的情况下忽略无意中的 NaN 或忘记的 NA,R 默认情况下在运行诸如 mean 之类的函数时不会忽略这些特殊值——因此不会返回预期的数值结果。不过,你可以将可选参数 na.rm 设置为 TRUE,这将强制函数仅在存在的数值上操作。
R> mean(c(1,4,NA),na.rm=TRUE)
[1] 2.5
R> mean(c(1,4,NaN),na.rm=TRUE)
[1] 2.5
只有在你知道可能存在缺失值,并且结果将仅基于那些已观测到的值时,才应使用此参数。我已经讨论过的函数,如 sum、prod、mean、median、max、min 和 range——基本上所有基于数值向量计算数值统计量的函数——都提供了 na.rm 参数。
最后,在计算简单的总结统计时,提醒自己使用 tapply 函数是很有用的(参见 第 10.2.3 节),它用于按特定类别变量计算分组统计量。例如,假设你想要计算按饲料类型分组的小鸡体重的均值。一个解决方案是对每个特定子集使用 mean 函数。
R> mean(chickwts$weight[chickwts$feed=="casein"])
[1] 323.5833
R> mean(chickwts$weight[chickwts$feed=="horsebean"])
[1] 160.2
R> mean(chickwts$weight[chickwts$feed=="linseed"])
[1] 218.75
R> mean(chickwts$weight[chickwts$feed=="meatmeal"])
[1] 276.9091
R> mean(chickwts$weight[chickwts$feed=="soybean"])
[1] 246.4286
R> mean(chickwts$weight[chickwts$feed=="sunflower"])
[1] 328.9167
这既繁琐又冗长。然而,使用 tapply,你可以仅通过一行代码按类别计算相同的值。
R> tapply(chickwts$weight,INDEX=chickwts$feed,FUN=mean)
casein horsebean linseed meatmeal soybean sunflower
323.5833 160.2000 218.7500 276.9091 246.4286 328.9167
在这里,第一个参数是要操作的数值向量,INDEX 参数指定分组变量,FUN 参数给出要在第一个参数的数据上执行的函数名称,按 INDEX 定义的子集进行操作。就像你见过的要求用户指定另一个函数来控制操作的其他函数一样,tapply 包含一个省略号(参见 第 9.2.5 节 和 第 11.2.4 节),允许用户在需要时将其他参数直接传递给 FUN。
13.2.2 计数、百分比和比例
在这一节中,你将查看不一定是数字的数据的总结。例如,要求 R 计算分类变量的均值毫无意义,但有时统计每个类别中观察值的数量是有用的——这些计数或频率代表了分类数据的最基本摘要统计量。
这使用了与第 13.2.1 节中的众数计算所必需的相同计数汇总,因此你仍然可以使用table命令来获得频率。记住,在chickwts数据框中,有六种饲料类型构成了小鸡的饮食。获取这些因子水平的计数就像这样简单:
R> table(chickwts$feed)
casein horsebean linseed meatmeal soybean sunflower
12 10 12 11 14 12
通过识别每个类别中观察值的比例,你可以从这些计数中获取更多信息。这将为你提供跨多个数据集的可比度量。比例表示每个类别中观察值的分数,通常以 0 到 1 之间的十进制(浮动点)数字表示(包括 0 和 1)。要计算比例,你只需要通过将计数(或频率)除以总样本大小来修改之前的计数函数(在此通过对适当的数据框对象使用nrow获得样本大小;见第 5.2 节)。
R> table(chickwts$feed)/nrow(chickwts)
casein horsebean linseed meatmeal soybean sunflower
0.1690141 0.1408451 0.1690141 0.1549296 0.1971831 0.1690141
当然,你不需要通过table处理所有与计数相关的操作。对适当的逻辑标志向量做一个简单的sum运算也同样有用——记住,在 R 中对逻辑结构的任何算术操作中,TRUE会自动被视为1,FALSE会被视为0(参见第 4.1.4 节)。这样的sum将为你提供所需的频率,但要得到比例,你仍然需要除以总样本大小。此外,这实际上等同于找到逻辑标志向量的mean。例如,要找到喂食大豆的小鸡的比例,请注意,以下两个计算给出的结果约为 0.197,它们是相同的:
R> sum(chickwts$feed=="soybean")/nrow(chickwts)
[1] 0.1971831
R> mean(chickwts$feed=="soybean")
[1] 0.1971831
你还可以使用这种方法来计算联合组中实体的比例,方法是通过逻辑运算符轻松实现的(见第 4.1.3 节)。喂食大豆或马豆的小鸡的比例如下:
R> mean(chickwts$feed=="soybean"|chickwts$feed=="horsebean")
[1] 0.3380282
再次,tapply函数可以证明是有用的。这一次,为了得到每种饮食的小鸡比例,你将把FUN参数定义为一个匿名函数(参见第 11.3.2 节),该函数执行所需的计算。
R> tapply(chickwts$weight,INDEX=chickwts$feed,
FUN=function(x) length(x)/nrow(chickwts))
casein horsebean linseed meatmeal soybean sunflower
0.1690141 0.1408451 0.1690141 0.1549296 0.1971831 0.1690141
这里的可丢弃函数定义了一个虚拟参数x,你用它来表示每个饲料组中FUN应用的权重向量。因此,找到所需的比例就是将x中的观察数除以总观察数。
最后要注意的函数是round函数,它将数值数据输出四舍五入到指定的小数位数。你只需将你的数值向量(或矩阵或任何其他适当的数据结构)和你希望四舍五入的小数位数(作为digits参数)提供给round函数即可。
R> round(table(chickwts$feed)/nrow(chickwts),digits=3)
casein horsebean linseed meatmeal soybean sunflower
0.169 0.141 0.169 0.155 0.197 0.169
这将提供更易于一目了然的输出。如果将digits=0(默认值),则输出会四舍五入到最接近的整数。
在进行下一个练习之前,值得简要说明比例与百分比之间的关系。二者表示的是相同的东西,唯一的区别是尺度;百分比只是将比例乘以 100。因此,食用大豆饮食的小鸡百分比大约为 19.7%。
R> round(mean(chickwts$feed=="soybean")*100,1)
[1] 19.7
由于比例总是在区间[0,1]内,百分比总是在区间[0,100]内。
大多数统计学家使用比例而非百分比,因为比例在直接表示概率(在第十五章中讨论)中发挥重要作用。然而,在某些情况下,百分比更为常用,例如基本数据摘要或百分位数的定义,这将在第 13.2.3 节中详细介绍。
练习 13.2
-
获取
quakes数据框中发生在 300 公里深度或更深的地震事件的比例,并四舍五入到小数点后两位。 -
使用
quakes数据集,计算发生在深度 300 公里或更深的事件的均值和中位数震级。 -
使用
chickwts数据集,编写一个for循环,计算每种饲料类型的小鸡平均体重——与第 13.2.1 节中tapply函数给出的结果相同。显示结果时,四舍五入到小数点后一位,并确保每个均值都标明相应的饲料类型。
另一个现成可用的数据集(在自动加载的datasets包中)是InsectSprays。它包含了在不同农业单元上发现的昆虫数量,以及在每个单元上使用的昆虫喷雾类型。确保你可以在提示符下访问数据框;然后研究帮助文件?InsectSprays,了解 R 如何表示这两个变量。
-
确定
InsectSprays中两个变量的类型(根据第 13.1.1 节和第 13.1.2 节中的定义)。 -
计算不考虑喷雾类型的昆虫计数分布的众数。
-
使用
tapply报告每种喷雾类型的昆虫总数。 -
使用与(c)中相同类型的
for循环,计算每个喷雾类型组中至少有五只昆虫的农业单元的百分比。打印到屏幕时,四舍五入到最接近的整数。 -
获取与(g)中相同的数值结果,并进行四舍五入,但使用
tapply和一次性函数。
13.2.3 分位数、百分位数和五数概括
让我们再一次回到思考原始数字观察值。理解观察值是如何分布的,是一个重要的统计概念,这将成为从第十五章开始讨论的一个关键特征。
通过检查分位数,你可以更深入地了解一组观察值的分布情况。分位数是从一组数字测量值中计算出的一个值,表示一个观察值在与其他观察值比较时的排名。例如,中位数(第 13.2.1 节)本身就是一个分位数——它给出一个值,表示半数测量值位于该值之下——它是 0.5 分位数。或者,分位数也可以表示为百分位数——这与分位数相同,但在 0 到 100 的“百分比尺度”上。换句话说,p分位数等同于 100 × p百分位数。因此,中位数就是第 50 百分位数。
有多种不同的算法可以用来计算分位数和百分位数。它们的工作原理是将观察值从最小到最大排序,并使用某种形式的加权平均来找到与p对应的数值,但在其他统计软件中,结果可能会略有不同。
在 R 中获取分位数和百分位数是通过quantile函数完成的。使用存储为向量xdata的八个观察值,确认 0.8 分位数(或 80 百分位数)为 3.6:
R> xdata <- c(2,4.4,3,3,2,2.2,2,4)
R> quantile(xdata,prob=0.8)
80%
3.6
如你所见,quantile将数据向量作为其第一个参数,后跟传递给prob的数值,用于表示感兴趣的分位数。事实上,prob可以接受一个数字向量作为分位数值。当需要多个分位数时,这非常方便。
R> quantile(xdata,prob=c(0,0.25,0.5,0.75,1))
0% 25% 50% 75% 100%
2.00 2.00 2.60 3.25 4.40
在这里,你使用了quantile来获取所谓的xdata的五数概括,包含 0 百分位数(最小值)、25 百分位数、50 百分位数、75 百分位数和 100 百分位数(最大值)。0.25 分位数被称为第一个或下四分位数,而 0.75 分位数被称为第三个或上四分位数。还要注意,xdata的 0.5 分位数等于中位数(2.6,在第 13.2.1 节中通过median计算)。中位数是第二四分位数,最大值是第四四分位数。
除了使用quantile外,还有其他方法可以获取五数概括;当应用于一个数字向量时,summary函数也会自动提供这些统计数据,以及均值。
R> summary(xdata)
Min. 1st Qu. Median Mean 3rd Qu. Max.
2.000 2.000 2.600 2.825 3.250 4.400
为了查看一些使用真实数据的示例,让我们计算chickwts中小鸡体重的下四分位数和上四分位数。
R> quantile(chickwts$weight,prob=c(0.25,0.75))
25% 75%
204.5 323.5
这表示 25%的权重位于或低于 204.5 克,且 75%的权重位于或低于 323.5 克。
让我们还来计算斐济海岸深度小于 400 公里的地震事件震级的五数概括(以及均值),使用quakes数据框。
R> summary(quakes$mag[quakes$depth<400])
Min. 1st Qu. Median Mean 3rd Qu. Max.
4.00 4.40 4.60 4.67 4.90 6.40
这开始突显了分位数在解释数值测量分布中的重要性。从这些结果中,你可以看到,大多数深度小于 400 公里的事件的震级集中在 4.6 附近,中位数,第一和第三四分位数分别为 4.4 和 4.9。但你也可以看到,最大值远远大于上四分位数,而最小值与下四分位数的差距较小,这表明了一个偏态分布,即从中心向正方向(换句话说,向右)伸展的程度比向负方向(换句话说,向左)要大。这一观点也得到了均值大于中位数这一事实的支持——均值被较大的值“拉高”了。
当你在第十四章中使用基本统计图表来分析数据集时,你将进一步探讨这一点,一些相关术语将在第十五章中正式定义。
13.2.4 离散度:方差、标准差和四分位距
在第 13.2.1 节中探讨的集中趋势度量提供了数值测量值集中位置的良好指示,但均值、中位数和众数并没有描述数据的分散性。为此,需要使用离散度的度量。
除了你已经给出的八个假设观测值外,
R> xdata <- c(2,4.4,3,3,2,2.2,2,4)
你还将查看以下存储的另外八个观测值:
R> ydata <- c(1,4.4,1,3,2,2.2,2,7)
尽管这两组数字不同,但请注意,它们具有相同的算术均值。
R> mean(xdata)
[1] 2.825
R> mean(ydata)
[1] 2.825
现在让我们并排绘制这两个数据向量,每个数据向量位于一条水平线上,通过执行以下操作:
R> plot(xdata,type="n",xlab="",ylab="data vector",yaxt="n",bty="n")
R> abline(h=c(3,3.5),lty=2,col="gray")
R> abline(v=2.825,lwd=2,lty=3)
R> text(c(0.8,0.8),c(3,3.5),labels=c("x","y"))
R> points(jitter(c(xdata,ydata)),c(rep(3,length(xdata)),
rep(3.5,length(ydata))))
你已经在第七章中了解了如何使用这些基础的 R 图形函数,尽管需要解释的是,由于xdata和ydata中的某些观测值出现了多次,你可以随机稍微修改它们以防止重叠,这有助于视觉解释。这一步被称为抖动,可以通过在绘图前将感兴趣的数值向量传递给jitter函数来实现。此外,请注意,在任何plot调用中使用yaxt="n"可以抑制y-轴的显示;同样,bty="n"可以去除绘图周围的典型框线(你将在第二十三章中更多地关注这种类型的图形定制)。
如图 13-3 所示,结果提供了有价值的信息。尽管 xdata 和 ydata 的均值相同,但你可以很容易看出,ydata 中的观察值围绕中心值的“扩展”比 xdata 中的观察值要更大。为了量化这种扩展,你可以使用方差、标准差和四分位差等值。

图 13-3:比较两个假设的数据向量,它们共享相同的算术均值(由垂直虚线标出),但具有不同的扩展幅度。相同的观察值稍微有些抖动。
样本方差衡量了数值观察值围绕算术均值的扩展程度。方差是每个观察值与均值的平均平方距离的特殊表示。对于一组标记为 x = {x[1], x[2], ... , x[n]} 的 n 个数值测量,样本方差
由下式给出,其中 x̄ 是在公式(13.1)中描述的样本均值:

例如,如果你取八个示例观察值 2、4.4、3、3、2、2.2、2、4,它们的样本方差在四舍五入到三位小数后如下(为了可读性,某些项被省略以“...”表示):

标准差 只是方差的平方根。由于方差是平均平方距离的表示,标准差提供了一个可以根据原始观察值的尺度来解释的值。使用相同的符号,对于 n 个观察值的样本,样本标准差 s 可以通过取公式(13.3)的平方根来计算。

例如,根据之前计算的样本方差,这八个假设观察值的标准差如下(三位小数):

因此,粗略地说,0.953 表示每个观察值与均值的平均距离。
与方差和标准差不同,四分位差(IQR) 不是相对于样本均值计算的。四分位差衡量数据“中间 50%”的宽度,也就是说,位于中位数两侧 25%四分位数范围内的值的范围。因此,IQR 是通过计算数据的上四分位数和下四分位数之差来得出的。正式地,当 Q[x] ( · ) 表示四分位函数(如 13.2.3 节中定义),IQR 的计算公式为:

用于计算这些扩展度量的直接 R 命令是 var(方差)、sd(标准差)和 IQR(四分位差)。
R> var(xdata)
[1] 0.9078571
R> sd(xdata)
[1] 0.9528154
R> IQR(xdata)
[1] 1.25
你可以使用平方根函数sqrt来验证样本方差与标准差之间的关系,基于var的结果,并可以通过计算第三四分位数和第一四分位数之间的差异来重现 IQR。
R> sqrt(var(xdata))
[1] 0.9528154
R> as.numeric(quantile(xdata,0.75)-quantile(xdata,0.25))
[1] 1.25
请注意,as.numeric(见第 6.2.4 节)会去掉quantile返回对象中的百分位标注(默认情况下为结果标注标签)。
现在,对具有与xdata相同算术平均值的ydata观察进行相同的操作。计算结果如下:
R> sd(ydata)
[1] 2.012639
R> IQR(ydata)
[1] 1.6
ydata与xdata在同一尺度上,因此结果验证了你在图 13-3 中看到的内容——即前者的观察数据比后者更加分散。
对于两个快速的最终示例,让我们再次回到chickwts和quakes数据集。在第 13.2.1 节中,你看到所有小鸡的平均体重为 261.3099 克。你现在可以找到体重的标准差如下:
R> sd(chickwts$weight)
[1] 78.0737
非正式地说,这意味着每只小鸡的平均体重大约偏离均值 78.1 克(但从技术上讲,记住这仅仅是平方距离函数的平方根——请参见以下注释)。
在第 13.2.3 节中,你使用summary来获取quakes数据集中某些地震的五数概括。查看这些结果中的第一和第三四分位数(分别为 4.4 和 4.9),你可以快速得出这个子集事件的 IQR 为 0.5。这可以通过使用IQR来确认。
R> IQR(quakes$mag[quakes$depth<400])
[1] 0.5
这给出了在里氏震级单位中,观察数据中间 50%范围的宽度。
注释
这里方差(从而标准差)的定义仅指“样本估计量”,这是 R 的默认设置,使用公式中的 n − 1 作为除数。这是当手头的观察代表假定的大型总体样本时所使用的公式。在这些情况下,使用除数 n − 1 更为准确,提供了所谓的无偏估计量。因此,你并不完全是在计算“平均平方距离”,尽管可以宽松地认为它是这样,并且随着样本量 n 的增加,确实会接近这种情况。
练习 13.3
-
使用
chickwts数据框,计算所有小鸡体重的第 10、第 30 和第 90 百分位数,然后使用tapply来确定与体重样本方差最大相关的饲料类型。 -
转到
quakes中的地震事件数据,并完成以下任务:-
计算记录深度的四分位距(IQR)。
-
查找所有发生在 400 km 或更深深度的地震事件震级的五数摘要。将其与第 13.2.3 节中发生在 400 km 以下深度的地震事件的五数摘要进行比较,并简要评论你所观察到的差异。
-
运用你对
cut(第 4.3.3 节)的理解,创建一个新的因子向量depthcat,它将quakes$depth分为四个均匀间隔的类别,确保当你使用levels(depthcat)时,得到如下结果:R> levels(depthcat) [1] "[40,200)" "[200,360)" "[360,520)" "[520,680]" -
查找与
depthcat每个深度类别相关的地震事件震级的样本均值和标准差。 -
使用
tapply来计算quakes中按depthcat分组的地震事件震级的 0.8 分位数。
-
13.2.5 协方差与相关性
在分析数据时,能够研究两个数值变量之间的关系是很有用的,以便评估趋势。例如,你可能预期身高与体重之间会有明显的正相关关系——身高较高的人往往体重大一些。相反,你可能认为手掌跨度和头发长度之间的关联较小。量化和比较这种关联的最简单和最常见的方式之一就是通过相关性的概念,而这需要协方差的计算。
协方差表示两个数值变量是如何“共同变化”的,以及这种关系的性质,无论是正相关还是负相关。假设你有n个个体,对于两个变量,分别标记为x = {x[1], x[2], . . . , x[n]} 和 y = {y[1], y[2], . . . , y[n]},其中x[i]与y[i]相对应,i = 1, . . . , n。样本协方差r[xy]的计算公式如下,其中 x̄和
分别表示两组观测值的样本均值:

当你得到正值的r[xy]时,表示存在正线性关系——即随着x的增加,y也增加。当你得到负值时,表示存在负线性关系——即随着x的增加,y减少,反之亦然。当r[xy] = 0 时,表示x和y之间没有线性关系。需要注意的是,公式中变量的顺序并不重要;换句话说,r[xy] ≡ r[yx]。
为了演示,我们使用原始的八个示例观测值,记作x = {2,4.4,3,3,2,2.2,2,4},以及另外八个观测值,记作y = {1,4.4,1,3,2,2.2,2,7}。请记住,x和y的样本均值均为 2.825。这两组观测值的样本协方差如下(保留三位小数):

该数字为正值,这表明基于x和y的观测结果,存在正相关关系。
相关性使你能够通过识别任何关联的方向和强度,进一步解释协方差。有不同类型的相关系数,但最常见的是皮尔逊积矩相关系数,这是 R 语言中默认实现的相关系数(这是我在本章中使用的估计器)。皮尔逊样本相关系数ρ[xy]是通过将样本协方差除以每个数据集的标准差乘积来计算的。形式上,其中r[xy]对应于方程式 (13.6),s[x]和s[y]对应于方程式 (13.4),

这确保了−1 ≤ ρ[xy] ≤ 1。
当ρ[xy] = −1 时,存在完全负线性关系。任何小于零的结果表示负相关关系,系数越接近零,关系越弱,直到ρ[xy] = 0,表示完全没有关系。当系数大于零时,表示正相关关系,直到ρ[xy] = 1,表示完全正线性关系。
如果你采用第 13.2.4 节中已经计算出的x和y的标准差(s[x] = 0.953,s[y] = 2.013,精确到小数点后三位),你会发现以下结果(精确到小数点后三位):

ρ[xy]是正数,就像r[xy]一样;0.771 的值表示x和y之间存在中到强的正相关关系。同样,ρ[xy] ≡ ρ[yx]。
R 命令cov和cor用于计算样本协方差和相关系数;你只需提供两个相应的数据向量。
R> xdata <- c(2,4.4,3,3,2,2.2,2,4)
R> ydata <- c(1,4.4,1,3,2,2.2,2,7)
R> cov(xdata,ydata)
[1] 1.479286
R> cov(xdata,ydata)/(sd(xdata)*sd(ydata))
[1] 0.7713962
R> cor(xdata,ydata)
[1] 0.7713962
你可以将这些二元观察值绘制为基于坐标的图(散点图——更多示例见第 14.4 节)。执行以下命令会得到图 13-4:
R> plot(xdata,ydata,pch=13,cex=1.5)

图 13-4:绘制 xdata 和 ydata 观察值作为二元数据点,以说明相关系数的解释
如前所述,相关系数估计两个观测数据集之间线性关系的性质,因此,如果你查看图 13-4 中点所形成的模式,并假设画出一条完全直的线来最好地表示所有点,你可以通过这些点与线之间的接近程度来确定线性关联的强度。与完美直线更接近的点,其ρ[xy]值会更接近-1 或 1。方向由线条的斜率决定——上升趋势,线条朝右上方倾斜,表示正相关;下降趋势则表现为线条朝右下方倾斜。考虑到这一点,你可以看到,根据之前的计算,图 13-4 中的数据所估计的相关系数是合理的。数据点确实看起来会随着xdata和ydata的值一起增加,形成一条粗略的直线,但这种线性关联并不完美。如何计算拟合此类数据的“理想”或“最佳”直线将在第二十章中讨论。
为了帮助你理解相关性的概念,图 13-5 展示了不同的散点图,每个图显示 100 个数据点。这些观测值是随机且人工生成的,遵循预设的“真实”ρ[xy]值,并在每个图上方标注。

图 13-5:人工生成的 x 和 y 观测值,用于说明给定的相关系数值
第一道散点图显示了负相关数据;第二道显示了正相关数据。这些与我们预期看到的结果一致——线条的方向显示了趋势的负相关或正相关,而系数的极值对应于“完美线”的接近程度。
第三行也是最后一行,显示了相关系数为零的数据集,意味着* x和 y*之间没有线性关系。中间和最右边的图特别重要,因为它们强调了皮尔逊相关系数仅识别“直线”关系的事实;这最后两幅图清楚地显示了某种趋势或模式,但该统计量不能用于检测这种趋势。
为了总结本节内容,再次查看quakes数据。两个变量是mag(每个事件的震级)和stations(报告事件检测的站点数量)。可以通过以下方式绘制stations在* y轴上的数据与mag在 x*轴上的数据:
R> plot(quakes$mag,quakes$stations,xlab="Magnitude",ylab="No. of stations")
图 13-6 展示了这一图像。

图 13-6:绘制报告事件的站点数量( y)和每个事件的震级( x)在quakes数据框中的分布图
通过垂直的图案你可以看到,幅度似乎已被记录到某个特定的精度水平(这与精确测量地震幅度的困难有关)。尽管如此,在散点图中,显然可以看到一个正相关(更多的站点倾向于检测到更大幅度的事件),这一特征通过正协方差得到了确认。
R> cov(quakes$mag,quakes$stations)
[1] 7.508181
正如你从模式中可以预料到的,皮尔逊相关系数确认了线性关联非常强。
R> cor(quakes$mag,quakes$stations)
[1] 0.8511824
注意
重要的是要记住,相关性并不意味着因果关系。当你检测到两个变量之间有很高的相关效应时,这并不意味着一个导致另一个。因果关系在即使是最受控的情况下也很难证明。相关性只是让你测量关联性*。
如前所述,还有其他相关性的表示方法可以使用;秩相关系数,例如斯皮尔曼和肯德尔的相关系数,与皮尔逊的估计不同,因为它们不要求关系是线性的。这些也可以通过cor函数访问,通过设置可选的method参数(详细信息请参见?cor)。然而,皮尔逊相关系数是最常用的,并且与线性回归方法相关,你将在第二十章开始研究这些方法。
13.2.6 异常值
异常值是一个观察值,它似乎与其余数据“不匹配”。与大多数数据相比,它是一个显著的极端值,换句话说,是一个异常。在某些情况下,你可能怀疑这样的极端观察值并不是来自与其他观察值相同的机制,但没有硬性数值规则来界定什么构成异常值。例如,考虑foo中的 10 个假设数据点。
R> foo <- c(0.6,-0.6,0.1,-0.2,-1.0,0.4,0.3,-1.8,1.1,6.0)
利用第七章的技巧(以及制作图 13-3 的技巧),你可以按照以下方式在一条线上绘制foo。
R> plot(foo,rep(0,10),yaxt="n",ylab="",bty="n",cex=2,cex.axis=1.5,cex.lab=1.5)
R> abline(h=0,col="gray",lty=2)
R> arrows(5,0.5,5.9,0.1,lwd=2)
R> text(5,0.7,labels="outlier?",cex=3)
结果位于图 13-7 的左侧。

图 13-7:展示单变量(左)和双变量(右)数据的异常值定义。你是否应该在统计分析中包括这些值?答案可能很难确定。
从这个图中,你可以看到大多数观察值集中在零附近,但有一个值远在 6 之外。为了给出一个双变量的例子,我将使用两个进一步的向量,bar和baz,如下所示:
R> bar <- c(0.1,0.3,1.3,0.6,0.2,-1.7,0.8,0.9,-0.8,-1.0)
R> baz <- c(-0.3,0.9,2.8,2.3,1.2,-4.1,-0.4,4.1,-2.3,-100.0)
我将使用以下代码绘制这些数据;结果位于图 13-7 的右侧。
R> plot(bar,baz,axes=T,cex=2,cex.axis=1.5,cex.lab=1.5)
R> arrows(-0.5,-80,-0.94,-97,lwd=2)
R> text(-0.45,-74,labels="outlier?",cex=3)
识别异常值非常重要,因为它们可能对任何统计计算或模型拟合产生影响。因此,许多研究人员在计算结果之前,会通过进行“探索性”分析,使用基本的汇总统计和数据可视化工具(比如你将在第十四章中学习到的那些)来尝试识别可能的异常值。
异常值可能是自然发生的,其中异常值是从总体中真实或准确记录的观察值,也可能是非自然发生的,其中某些因素“污染”了样本中的某一贡献,例如数据输入错误。因此,通常会在分析前排除通过非自然来源发生的异常值,但在实践中,这并不总是容易的,因为异常值的原因可能很难确定。在某些情况下,研究人员会同时进行两种分析——展示包括和排除任何被认为是异常值的结果。
考虑到这一点,如果你返回查看图 13-7 中左侧的示例,你会看到当你包含所有观察值时,得到如下结果:
R> mean(foo)
[1] 0.49
然而,当删除可能的异常值 6(第 10 个观察值)时,得到如下结果:
R> mean(foo[-10])
[1] -0.1222222
这突显了单个极端观察值可能产生的影响。如果没有关于样本的额外信息,就很难判断是否应该排除异常值 6。类似的效果也能在计算例如bar与baz的相关系数时观察到,正如图 13-7 右侧所示(再次强调,第 10 个观察值可能是异常值)。
R> cor(bar,baz)
[1] 0.4566361
R> cor(bar[-10],baz[-10])
[1] 0.8898639
你会发现去除异常值后,相关性变得更强了。同样,判断是否删除异常值在实际中可能很难正确评估。在这个阶段,重要的是要意识到异常值可能对分析的影响,并在进行更严格的统计分析之前,至少对原始数据进行初步检查。
注释
极端观察值对数据分析的影响程度不仅取决于其极端性,还取决于你打算计算的统计量。例如,样本均值对异常值非常敏感,包含或排除异常值会导致其差异很大,因此,任何依赖于均值的统计量,如方差或协方差,也会受到影响。分位数和相关统计量,如中位数或四分位距(IQR),相对不受异常值的影响。在统计学术语中,这种特性被称为稳健性*。
练习 13.4
-
在练习 7.1 的(b)部分中,第 139 页展示了身高与体重的关系图。请根据这两个变量的观测数据计算相关系数。
-
R 的另一个内置的、现成可用的数据集是
mtcars,其中包含了 32 辆汽车的多个性能方面的描述性数据。-
确保你能通过在提示符下输入
mtcars来访问这个数据框。然后检查它的帮助文件,以了解其中的数据类型。 -
两个变量描述了车辆的马力和完成四分之一英里距离的最短时间。使用基础 R 图形,绘制这两个数据向量,马力位于* x *轴上,并计算相关系数。
-
确定
mtcars中对应变速类型的变量。利用你对 R 中因子的知识,从该变量中创建一个名为tranfac的新因子,其中手动汽车应标记为"manual",自动汽车标记为"auto"。 -
现在,使用
ggplot2中的qplot与tranfac结合,生成与(ii)相同的散点图,以便你能够在视觉上区分手动和自动汽车。 -
最后,基于车辆的变速类型计算马力和四分之一英里时间的相关系数,并将这些估计值与(ii)中的整体值进行比较,简要评论你注意到的内容。
-
-
返回
chickwts以完成以下任务:-
根据仅食向日葵饮食的小鸡体重,绘制如图 13-7 左侧面板所示的图表。注意其中一只向日葵饲养的小鸡的体重比其他小鸡低得多。
-
计算向日葵饲养的小鸡体重的标准差和四分位距。
-
现在,假设有人告诉你,食向日葵饲养的小鸡的最低体重大概是由某种疾病引起的,而这与您的研究无关。删除此观察值,并重新计算剩余向日葵饲养小鸡的标准差和四分位距。简要评论计算值的差异。
-
本章重要代码
| Function/operator | 简要描述 | 首次出现 |
|---|---|---|
mean |
算术平均数 | 第 13.2.1 节, 第 268 页 |
median |
中位数 | 第 13.2.1 节, 第 268 页 |
table |
频率表 | 第 13.2.1 节, 第 268 页 |
min, max, range |
最小值和最大值 | 第 13.2.1 节, 第 268 页 |
round |
四舍五入数值 | 第 13.2.2 节, 第 272 页 |
quantile |
分位数/百分位数 | 第 13.2.3 节, 第 274 页 |
summary |
五数概括 | 第 13.2.3 节, 第 275 页 |
jitter |
绘图中的抖动点 | 第 13.2.4 节, 第 276 页 |
var, sd |
方差,标准差 | 第 13.2.4 节, 第 278 页 |
IQR |
四分位距 | 第 13.2.4 节, 第 278 页 |
cov, cor |
协方差,相关性 | 第 13.2.5 节,第 281 页 |
第十四章:14
基础数据可视化

数据可视化是统计分析中的一个重要部分。适合给定数据集的可视化工具取决于你所观察的变量类型(根据第 13.1.1 节和 13.1.2 节的定义)。在本章中,你将了解在统计分析中最常用的数据图,并通过基础 R 图形和ggplot2功能的示例来展示。
14.1 条形图和饼图
条形图和饼图常用于通过类别频率可视化定性数据。在这一节中,你将学习如何使用 R 生成这两种图表。
14.1.1 构建条形图
条形图绘制的是垂直或水平条形,通常通过空白区域分开,用于根据相关类别可视化频率。尽管原始频率通常会显示出来,条形图也可以用来可视化其他量,例如均值或比例,这些量直接依赖于这些频率。
作为一个示例,让我们使用来自练习 13.4(b)的mtcars数据集(位于第 287 页)。该数据集详细描述了 1970 年代中期 32 款经典性能车的各种特征,可以直接从命令行查看前五条记录。
R> mtcars[1:5,]
mpg cyl disp hp drat wt qsec vs am gear carb
Mazda RX4 21.0 6 160 110 3.90 2.620 16.46 0 1 4 4
Mazda RX4 Wag 21.0 6 160 110 3.90 2.875 17.02 0 1 4 4
Datsun 710 22.8 4 108 93 3.85 2.320 18.61 1 1 4 1
Hornet 4 Drive 21.4 6 258 110 3.08 3.215 19.44 1 0 3 1
Hornet Sportabout 18.7 8 360 175 3.15 3.440 17.02 0 0 3 2
?mtcars中的文档解释了已记录的变量。其中,cyl提供了每个发动机的气缸数——四缸、六缸或八缸。要找出观察到的每种气缸数量的汽车数量,可以使用table,如下面所示:
R> cyl.freq <- table(mtcars$cyl)
R> cyl.freq
4 6 8
11 7 14
结果可以轻松地显示为条形图,如下所示:
R> barplot(cyl.freq)
你可以在图 14-1 的左侧找到生成的条形图。

图 14-1:使用基础 R 图形从mtcars数据绘制的两个条形图示例。左侧:最简单的默认版本,使用一个分类变量。右侧:一个“并排”的条形图,展示了各种可视化选项,并使用了两个分类变量。
该图展示了数据集中四缸、六缸和八缸车的数量,但老实说,这个图表相当无趣,而且没有注释,无法清晰说明总结了什么内容。幸运的是,给这种图表添加注释是很容易的,而且还可以根据一个额外的分类变量进一步分割每个条形的频率。请看以下代码,这次你将根据变速器类型(am)来找出与cyl相关的计数:
R> table(mtcars$cyl[mtcars$am==0])
4 6 8
3 4 12
R> table(mtcars$cyl[mtcars$am==1])
4 6 8
8 3 2
如果你想生成一个堆叠的条形图(即条形垂直分割)或并排的条形图(即条形被分开并并排显示),barplot的第一个参数需要传入一个合适排列的矩阵。你可以使用matrix从之前的两个向量构建它,但继续使用table会更简单。
R> cyl.freq.matrix <- table(mtcars$am,mtcars$cyl)
R> cyl.freq.matrix
4 6 8
0 3 4 12
1 8 3 2
如你所见,通过向table传递两个相等长度的分类或离散向量,你可以进行交叉表格计数;第一个向量规定行计数,第二个定义列。结果是一个矩阵对象;在这里它是一个 2 × 3 结构,提供了第一行四缸、六缸和八缸自动挡车的数量,第二行则是手动挡车的数量。规则是,条形图的每一列将对应于提供的矩阵的每一列;这些列会根据提供的矩阵的每一行进一步拆分。图 14-1 右侧的图表就是以下代码的结果:
R> barplot(cyl.freq.matrix,beside=TRUE,horiz=TRUE,las=1,
main="Performance car counts\nby transmission and cylinders",
names.arg=c("V4","V6","V8"),legend.text=c("auto","manual"),
args.legend=list(x="bottomright"))
帮助文件?barplot详细解释了这里的选项。要根据最初传递给barplot的矩阵的列变量类别为条形图添加标签,可以使用适当长度的字符向量传递给names.arg。选项beside=TRUE和horiz=TRUE选择一个错开的水平条形图。如果这两个选项都为FALSE,则会选择一个堆叠的垂直条形图。参数las=1强制垂直轴上的标签水平显示,而不是与其平行。最后两个参数legend.text和args.legend用于图例——你本可以像在第 7.3 节中一样通过legend单独绘制图例,但这种方式会自动分配颜色,以确保参考键与条形本身的精确阴影匹配。
可以使用ggplot2生成类似的图表。如果加载已安装的包并输入以下内容,将生成最基础的条形图,如图 14-2 左侧所示:
R> qplot(factor(mtcars$cyl),geom="bar")
请注意,相关的几何对象是"bar"(或者如果单独使用geom_bar,如你稍后将看到的那样),并且在qplot中默认的映射变量必须作为因子提供(在mtcars中,向量mtcars$cyl仅为数字类型,这对于barplot来说是可以的,但ggplot2的功能要求更加严格)。

图 14-2:使用 ggplot2 功能展示的来自 mtcars 数据的两种条形图示例。左:最简单的 qplot 版本,使用一个分类变量。右:一个“错开”的条形图,类似于图 14-1,基于各种附加几何对象和缩放选项的供应。
同样,你可以根据需要显示的内容创建更复杂的图像。要生成与 14-1 中的“错开”条形图相同的ggplot2版本,可以调用以下代码:
R> qplot(factor(mtcars$cyl),geom="blank",fill=factor(mtcars$am),xlab="",
ylab="",main="Performance car counts\nby transmission and cylinders")
+ geom_bar(position="dodge")
+ scale_x_discrete(labels=c("V4","V6","V8"))
+ scale_y_continuous(breaks=seq(0,12,2))
+ theme_bw() + coord_flip()
+ scale_fill_grey(name="Trans.",labels=c("auto","manual"))
你可以在图 14-2 右侧找到结果。
请注意,基本的qplot设置中增加了许多新的内容。默认映射仍然是按cyl,和之前一样。你进一步指定,条形图的填充应根据使用变速器变量am创建的因子来填充;因此,每个cyl的条形图被指示根据该变量进行拆分。最初对qplot的调用是“空”的,意思是geom="blank",因此绘图从向ggplot2对象添加geom_bar开始。通过position="dodge",它变成了一个偏移条形图;与基本的 R 图形类似,默认行为是生成堆积图。scale_x_discrete修饰符指定了每个类别的标签,默认映射为cyl;scale_y_continuous修饰符则用于控制频率的轴标签。
此外,向对象添加theme_bw()会改变图像的视觉主题;在当前的示例中,我选择移除灰色背景,因为它的颜色与手动变速车的条形图颜色过于相似。向对象添加coord_flip会翻转坐标轴,提供水平条形图,而不是默认的垂直样式(注意,scale_函数的调用是针对未翻转的图像)。fill的默认行为是使用颜色,因此你使用scale_fill_grey修饰符强制将其设置为灰度,并同时更改自动生成的图例标签,使其匹配。
使用ggplot2相对于基本 R 图形的最大优势在于,你不需要手动列出计数或设计这些频率的特定矩阵结构——变量映射会自动完成这一工作。为了练习,我鼓励你尝试修改这个代码示例,省略或修改qplot对象中的某些内容,以评估对结果图像的影响。
14.1.2 快速饼图
古老的饼图是另一种可视化频率基础数量在类别变量水平之间的替代方式,通过适当大小的“切片”表示每个类别变量的相对计数。
R> pie(table(mtcars$cyl),labels=c("V4","V6","V8"),
col=c("white","gray","black"),main="Performance cars by cylinders")
你可以在图 14-3 中找到结果图。
虽然通过一定的努力可以实现,但ggplot2中没有直接的“饼图”几何对象。这至少部分是由于统计学家普遍偏好条形图而非饼图。这一点在帮助文件?pie中也有所总结!
饼图是展示信息的一个糟糕方式。人眼擅长判断线性度量,但不擅长判断相对面积。
此外,如果你希望按第二个类别变量拆分频率,或者因子水平是有序的,那么条形图比饼图更有价值。

图 14-3:来自mtcars数据框中总气缸频率的饼图
14.2 直方图
条形图在根据类别变量计数观测值时直观易懂,但如果你感兴趣的变量是数值型连续变量时,几乎没有用处。为了可视化连续测量的分布,可以使用直方图——这种工具有时会因外观相似而与条形图混淆。直方图同样衡量频率,但在针对数值型连续变量时,首先需要对观察数据进行“分箱”,即定义区间,然后统计落在每个区间内的连续观测值数量。这个区间的大小被称为分箱宽度。
作为直方图的一个简单例子,可以考虑mtcars数据集中 32 辆车的马力数据,它位于第四列,名为hp。
R> mtcars$hp
[1] 110 110 93 110 175 105 245 62 95 123 123 180 180 180 205 215 230 66
[19] 52 65 97 150 150 245 175 66 91 113 264 175 335 109
对于本节内容,定义那个时代所有高性能汽车的马力为你的总体,并假设这些观测值代表了从该总体中抽取的一个样本。使用基础 R 图形,hist命令接受一个数值型连续观测值的向量,并生成一个直方图,如图 14-4 左侧所示。
R> hist(mtcars$hp)

图 14-4:展示 hist 函数在 mtcars 马力数据上的默认行为(左);定制分箱宽度、颜色和标题选项,并添加集中性标记(右)
你可以立即看到,左侧的直方图使用了 50 个单位的分箱宽度,覆盖了数据的范围,从而为你提供了关于马力测量分布的快速而有用的第一印象。它似乎大致集中在 75 到 150 的范围内,右侧逐渐减少(这被称为右偏或正偏;更多术语将在第 15.2.4 节中介绍)。
直方图作为测量分布形状的表示的准确性完全取决于用于分箱的区间宽度。hist函数通过breaks参数控制分箱宽度。你可以通过提供一个向量,给出每个断点,来手动设置这些区间。在下面的代码中,通过将每个分箱的宽度从 50 减小到 25,并略微扩展整体范围,使用等距序列来完成这一操作。
R> hist(mtcars$hp,breaks=seq(0,400,25),col="gray",main="Horsepower",xlab="HP")
R> abline(v=c(mean(mtcars$hp),median(mtcars$hp)),lty=c(2,3),lwd=2)
R> legend("topright",legend=c("mean HP","median HP"),lty=c(2,3),lwd=2)
该图表展示在图 14-4 的右侧,显示了使用更窄分箱的结果,同时将条形颜色设为灰色并添加了更易读的标题。它还包括表示均值和中位数的垂直线,使用abline,并添加了图例(请参阅第 7.3 节)。
使用较小的区间宽度时,数据分布的细节更加清晰。然而,使用更窄的区间有可能突出显示“不重要的特征”(换句话说,直方图中代表由于有限样本大小所产生的自然变异的特征)。这些通常出现在数据稀缺的区间位置。例如,唯一的一辆 335 马力的汽车在右侧的区间产生了一个孤立的条形,但你可能合理地得出结论,认为这并不是该位置的“真实突起”,而是整体人群数据的一个不精确的反映。因此,选择区间宽度时需要注意,这是一种平衡行为。
你需要选择一个合适的宽度,这样可以让你清楚地了解数据分布,而不会因使用过小的区间宽度而强调不重要的细节。换句话说,你也需要避免因使用过大的区间宽度而隐藏重要的特征。为了解决这个问题,有一些数据驱动的算法,它们利用记录的观测数据的规模来尝试计算一个合适的平衡区间宽度。你可以为breaks提供一个字符字符串,给出你希望使用的算法名称。默认的breaks="Sturges"通常表现得很好,尽管在以这种方式探索数据时,尝试少量的替代区间宽度也是值得的。有关此内容和其他使用breaks的方法的更多详细信息,文档?hist提供了清晰简明的说明。
在ggplot2中,区间及其宽度的问题以另一种方式得到强调。默认情况下,当你向qplot函数提供一个单一的数值向量但没有为geom参数指定值时,它会生成一个直方图。
R> qplot(mtcars$hp)
`stat_bin()` using `bins = 30`. Pick better value with `binwidth`.
你可以在图 14-5 中找到左侧的结果。然而需要注意的是,qplot会向控制台打印关于区间宽度的通知。

图 14-5:展示默认的 qplot 行为,基于 mtcars 的马力数据(左);定制区间宽度、颜色和标题选项,以及添加中央性标记(右)
如果你没有明确指定区间,将使用恰好 30 个区间来覆盖数据的范围。通过查看相关的geom文档(通过调用?geom_histogram)可以得到以下信息:
默认情况下,stat_bin使用 30 个区间。这并不是一个理想的默认值,但它的目的是让你开始尝试不同的区间宽度。你可能需要查看几个不同的区间宽度,以揭示数据背后的完整故事。
因此,ggplot2鼓励用户意识到问题并主动设置自己的箱宽,而不是默认使用像hist这样的数据驱动算法。你可以看到,30 个箱子为此示例提供了不合适的狭窄区间——有很多间隙,那里没有观测值。你可以通过多种方式选择qplot中的直方图区间,其中一种是使用breaks,像之前一样,给它提供一个适当的数值区间端点向量。为了重新创建图 14-4 右侧的图,使用ggplot2功能,使用以下代码,这会生成图 14-5 中的右侧图:
R> qplot(mtcars$hp,geom="blank",main="Horsepower",xlab="HP")
+ geom_histogram(color="black",fill="white",breaks=seq(0,400,25),
closed="right")
+ geom_vline(mapping=aes(xintercept=c(mean(mtcars$hp),median(mtcars$hp)),
linetype=factor(c("mean","median"))),show.legend=TRUE)
+ scale_linetype_manual(values=c(2,3)) + labs(linetype="")
从一个“空白”几何对象开始,geom_histogram完成了大部分工作,color控制条形的轮廓颜色,fill控制条形的内部颜色。参数closed="right"决定每个区间在右侧是“封闭的”(也就是说,不包括在内),在左侧是“开放的”(也就是说,包括在内),这与?hist中注明的默认值相同。geom_vline函数用于添加垂直的均值和中位线;在这里,mapping必须使用aes进行指示,改变这些线的位置。为了确保为均值和中位数创建正确标注的图例,你还必须指示linetype在aes中映射到所需的值。在这种情况下,这只是一个由两个所需“级别”组成的因子。
由于你是在手动添加这些线条及其相关映射到ggplot2对象中,因此必须指示图例本身通过show.legend=TRUE来显示。默认情况下,两条线将被绘制为lty=1(实线)和lty=2(虚线),但是为了与之前的图匹配,你需要lty=2和lty=3(点线)。你可以添加scale_linetype_manual修饰符来进行此更改;所需的线条类型编号作为一个向量传递给values。最后,为了抑制自动为你手动添加的图例包含标题,labs(linetype="")的添加指示与变量映射到linetype的aes调用相关的尺度显示时不带标题。
使用ggplot2与基本 R 图形的选择通常取决于你的目标。对于自动化处理图形,尤其是在使用分类变量来分割数据集子集的情况下,ggplot2特别强大。另一方面,如果你需要手动控制图像的创建,传统的 R 图形可能更容易处理,而且你不需要跟踪多个美学变量映射。
14.3 箱型图
一个特别流行的直方图替代品是箱型图,或简称箱线图。这仅仅是第 13.2.3 节中讨论的五数概括的可视化表示。
14.3.1 独立箱型图
让我们回到内置的quakes数据框,该数据包含了 1000 个发生在斐济附近的地震事件。为了进行比较,你可以使用默认的 R 基础行为检查这些事件的震级的直方图和箱线图。以下代码生成了图 14-6 中给出的图像:
R> hist(quakes$mag)
R> boxplot(quakes$mag)

图 14-6:来自quakes的震中数据的默认直方图(左)和箱线图(右)。在箱线图上,外部叠加的注释指出了显示的关键信息。
类似于直方图,箱线图显示了分布的主要特征,如全局(换句话说,总体)集中性、分散度和偏态。然而,它并不能真正展示局部特征,比如分布中的多个显著峰值。如标签箭头所指,箱体中间的线表示中位数,箱体的上下边缘显示各自的四分位数,从箱体延伸出的垂直线(胡须)表示最小值和最大值,任何超出胡须的点被认为是极端值或异常值。默认情况下,boxplot将异常值定义为低于下四分位数或高于上四分位数 1.5 倍 IQR 的观测值。这样做是为了防止胡须延伸过远,从而过度强调任何偏态。因此,胡须标记的“最大值”和“最小值”并不总是数据集中的原始整体最大值或最小值,因为被视为“异常值”的数据点实际上可能代表最高或最低值。你可以通过boxplot中的range参数控制这种分类的性质,尽管默认值range=1.5对于基本的数据探索来说通常是合理的。
14.3.2 并排箱线图
这些图表的一个特别令人愉快的方面是,你可以通过并排箱线图轻松比较不同组别的五数总结分布。再次使用quakes数据,定义以下对应的因子,并检查前五个元素(如有需要,请参考第 4.3.3 节中cut命令的用法):
R> stations.fac <- cut(quakes$stations,breaks=c(0,50,100,150))
R> stations.fac[1:5]
[1] (0,50] (0,50] (0,50] (0,50] (0,50]
Levels: (0,50] (50,100] (100,150]
记得,stations变量记录了有多少监测站检测到每个事件。该代码生成了一个因子,将这些观测值分为三组——50 个或更少监测站检测到的事件,51 到 100 个监测站检测到的事件,以及 101 到 150 个监测站检测到的事件。因此,你可以根据这三组比较事件震级的分布。以下代码生成了图 14-7 中的左侧图像:
R> boxplot(quakes$mag~stations.fac,
xlab="# stations detected",ylab="Magnitude",col="gray")
在这行代码中,你应该注意到新的语法形式,波浪号~,在quakes$mag~stations.fac中显示。你可以将~理解为“按”、“由”或“根据”(你将在第二十章到第二十二章中频繁使用波浪号表示法)。在这里,你指示boxplot按照station.fac绘制quakes$mag,因此每个组都会生成一个单独的箱线图,组内的顺序自然与分组因子的顺序一致。还使用了可选参数来控制轴标签和箱体颜色。你对这个图的解释与在图 13-6 中看到的相同,即记录的震级越高,检测到给定地震事件的台站越多。

图 14-7:使用基础 R 图形(左)和ggplot2功能(右)绘制的quakes震级的并排箱线图,按station.fac标识的三组分割
转向ggplot2的功能,qplot可以轻松地生成相同类型的图,下面的代码生成了图 14-7 右侧的图像:
R> qplot(stations.fac,quakes$mag,geom="boxplot",
xlab="# stations detected",ylab="Magnitude")
默认的箱线图略有不同,但你仍然可以做出相同的解释。在这种qplot的使用中,你将箱线图的分组因子作为X轴变量(第一个参数),将需要箱线图的连续变量作为Y轴变量(第二个参数)。在这里,我显式设置了geom="boxplot"以确保显示箱线图,并且添加了轴标签。
14.4 散点图
散点图最常用于识别两个不同数值连续变量之间的关系,通常显示为x-y坐标图。基础 R 图形的坐标特性自然适合创建散点图,因此你已经在本书中看到了一些例子。然而,并非每一个基于x-y坐标的图都被称为散点图;散点图通常假设存在某种“感兴趣的关系”。例如,像图 13-1 这样的空间坐标图可能不被视为散点图,而像图 13-6 中震级与检测到地震事件的台站数量之间的关系图则是。
我将在本章结束时扩展说明如何使用散点图探索多个连续变量。为此,我们来访问另一个现成的 R 数据集,即著名的iris数据。该数据集收集于 1930 年代中期,共有 150 行和 5 列,包含了三种多年生鸢尾花的花瓣和萼片的测量数据——Iris setosa、Iris virginica和Iris versicolor(Anderson, 1935; Fisher, 1936)。你可以在这里查看前五条记录:
R> iris[1:5,]
Sepal.Length Sepal.Width Petal.Length Petal.Width Species
1 5.1 3.5 1.4 0.2 setosa
2 4.9 3.0 1.4 0.2 setosa
3 4.7 3.2 1.3 0.2 setosa
4 4.6 3.1 1.5 0.2 setosa
5 5.0 3.6 1.4 0.2 setosa
通过查看?iris,你可以看到每个物种的每个变量都有 50 个观测值,单位为厘米(cm)。
14.4.1 单个图
你可以修改简单的散点图,根据分类变量拆分绘制的点,揭示与连续变量相关的任何可见关系之间的潜在差异。例如,使用基本的 R 图形,你可以根据三种物种检查花瓣的测量值。采用第七章中首次介绍的“垫脚石”方法,你可以通过首先使用type="n"来生成一个空白的绘图区域,并根据每个物种的情况逐步添加对应的点,按需要调整点的特征和颜色,手动构建该图。
R> plot(iris[,4],iris[,3],type="n",xlab="Petal Width (cm)",
ylab="Petal Length (cm)")
R> points(iris[iris$Species=="setosa",4],
iris[iris$Species=="setosa",3],pch=19,col="black")
R> points(iris[iris$Species=="virginica",4],
iris[iris$Species=="virginica",3],pch=19,col="gray")
R> points(iris[iris$Species=="versicolor",4],
iris[iris$Species=="versicolor",3],pch=1,col="black")
R> legend("topleft",legend=c("setosa","virginica","versicolor"),
col=c("black","gray","black"),pch=c(19,19,1))
你可以在图 14-8 中找到该图。注意,Iris virginica物种的花瓣最大,其次是Iris versicolor,最小的花瓣属于Iris setosa。然而,尽管这段代码可以正常工作,但它相当繁琐。你可以通过首先设置指定每个观测值的点特征和颜色的向量,以更简单的方式生成相同的图像。

图 14-8:按物种拆分的花瓣测量值的散点图,来自内置的iris数据框
考虑这里创建的两个对象:
R> iris_pch <- rep(19,nrow(iris))
R> iris_pch[iris$Species=="versicolor"] <- 1
R> iris_col <- rep("black",nrow(iris))
R> iris_col[iris$Species=="virginica"] <- "gray"
第一行创建了一个与iris中的观测值数量相等的向量iris_pch,每个条目的值为19。然后,通过向量子集操作,覆盖了对应Iris versicolor的条目,并将点特征设置为1。按照相同的步骤创建iris_col;首先填充一个适当大小的向量,并将字符字符串"black"填入其中,接着覆盖对应Iris virginica的条目,将其设置为"gray"。这样,注意,接下来的单行代码,配合之前的legend调用,将生成一个相同的图:
R> plot(iris[,4],iris[,3],col=iris_col,pch=iris_pch,
xlab="Petal Width (cm)",ylab="Petal Length (cm)")
14.4.2 图矩阵
“单个”类型的平面散点图仅在比较两个数值连续变量时非常有用。当涉及更多连续变量时,无法在单个图中令人满意地显示这些信息。一种简单且常见的解决方案是为每一对变量生成一个二变量散点图,并以结构化的方式将它们展示在一起;这种方法被称为散点矩阵。利用之前创建的iris_pch和iris_col向量,你可以为iris中的所有四个连续变量生成散点矩阵,并保持物种之间的区分。使用基本的 R 图形,可以使用pairs函数。
R> pairs(iris[,1:4],pch=iris_pch,col=iris_col,cex=0.75)
你可以在图 14-9 中找到该行代码的结果。

图 14-9:显示数据框中所有四个连续测量值的散点矩阵
使用pairs最简单的方法是将原始观测值的矩阵或数据框作为第一个参数传递,这里通过选择iris的所有列(除了Species列,即iris[,1:4])来实现。图表的解释依赖于对角面板的标签,这些面板从左上到右下排列。它们会按照作为第一个参数传递的列的顺序出现。这些“标签面板”可以让你确定矩阵中的每个单独图表对应的是哪一对变量。例如,图 14-9 中的散点图矩阵的第一列对应* x 轴上的萼片长度;矩阵的第三行对应 y 轴上的花瓣长度,每一行和每一列显示的尺度在左右或上下移动时保持不变。这意味着对角线上的图表与对角线下的图表是镜像关系——位于第 4 行第 2 列的花瓣宽度(y)与萼片宽度(x)的图表显示的数据与位于第 2 行第 4 列的散点图相同,只是坐标轴翻转了。因此,pairs包括一个选项,可以仅显示对角线上的或*下方的散点图,通过设置lower.panel=NULL或upper.panel=NULL来抑制其中一个。
散点图矩阵因此可以更容易地比较由对多个连续变量进行观测所形成的所有成对关系。在这个矩阵中,注意到花瓣尺寸之间存在强烈的正线性关联,而萼片尺寸之间的关系则较弱。此外,尽管Iris setosa可以合理地被认为是花瓣最小的花,但就萼片而言,情况并非如此。
对于使用ggplot2的用户,你们知道根据分类变量分割点是很自然的,以下是一个例子。
R> qplot(iris[,4],iris[,3],xlab="Petal width",ylab="Petal length",
shape=iris$Species)
+ scale_shape_manual(values=4:6) + labs(shape="Species")
你可以在图 14-10 中找到结果。

图 14-10:使用 ggplot2 功能绘制三种 iris 物种的花瓣尺寸,点的 shape 作为美学修饰符
在这里,我使用映射到shape的Species变量(相当于 R 基础术语pch)将点进行分割,并通过scale_shape_manual修饰符修改了点的类型。我还通过labs简化了自动生成的图例标题,正如在第 14.2 节中所做的那样。然而,仅使用ggplot2无法轻松实现散点图矩阵。为了以ggplot2风格生成矩阵,建议下载GGally包(Schloerke 等,2014),以访问ggpairs函数。该包旨在作为ggplot2的扩展或附加功能。可以通过常规方式从 CRAN 安装——例如,运行install.packages("GGally")——并且在使用前必须通过library("GGally")加载。完成此步骤后,作为一个快速示例,以下代码会生成图 14-11 中的图:
R> ggpairs(iris,mapping=aes(col=Species),axisLabels="internal")
尽管你可能会看到与直方图箱宽相关的常见警告,但ggpairs为这样一行简短的代码提供了令人印象深刻的视觉效果。输出不仅提供了使用pairs生成的散点图矩阵的下半部分,还在底部提供了等效的直方图,在右侧提供了箱线图。它还显示了相关系数的估计值。如图所示,你可以将一个变量映射到一个美学修饰符上,根据因子水平分割绘制的观测值。在图 14-11 中,这是通过颜色实现的,并且你指示ggpairs操作Species变量。在?ggpairs中的文档提供了控制各个图形存在与外观的各种选项的简洁信息。

图 14-11:使用GGally包中的ggpairs生成散点图矩阵,使用颜色分隔物种。注意增加了估计的相关系数和分布图。
练习 14.1
回顾内置的InsectSprays数据框,包含在接受六种不同喷雾处理的各种农业单位上昆虫的计数。
-
使用 R 基础图形绘制昆虫计数的直方图。
-
获取每种喷雾处理下发现的昆虫总数(这在练习 13.2(f)中也有要求,见第 273 页)。然后,使用 R 基础图形生成这些总数的垂直条形图和饼图,并适当地标注每个图。
-
使用
ggplot2功能生成根据每种喷雾类型的昆虫计数的并排箱线图,并包括适当的轴标签和标题。
R 中另一个有用的现成数据集是USArrests,其中包含 1973 年美国 50 个州每 10 万人口中谋杀、强奸和攻击的逮捕人数数据(例如,参见 McNeil, 1977)。它还包括一个变量,给出每个州的城市人口比例。简要检查数据框对象和附带的文档?USArrests。然后完成以下操作:
-
使用
ggplot2功能生成一个关于各州城市人口比例的右侧排他直方图。将您的断点设置为每 10 个单位,在 0 和 100 之间。直方图显示第一四分位数、中位数和第三四分位数;然后提供一个匹配的图例。根据需要使用颜色,并包括适当的轴注释。 -
代码
t(as.matrix(USArrests[,-3]))创建了一个不包含城市人口列的USArrests数据矩阵,内置的 R 对象state.abb提供了按字母顺序排列的两字母州缩写作为字符向量。使用这两个结构和基本 R 图形,生成一个水平堆叠条形图,水平条形标记有州缩写,每个条形根据犯罪类型(谋杀、强奸和攻击)进行分割。包括一个图例。 -
定义一个新的因子向量
urbancat,如果相应的州的城市人口比例大于中位数百分比,则设置为1,否则设置为0。 -
在删除
UrbanPop列后,在您的工作空间中创建USArrests的新副本,只留下三个犯罪率变量。然后在此对象中插入一个新的第四列urbancat。 -
使用(g)中的数据框生成一个散点图矩阵和其他相关图,通过
GGally功能将三种犯罪率相互对比。使用颜色根据urbancat的两个级别分割犯罪率。
返回内置的quakes数据集。
-
创建一个与幅度对应的因子向量。每个条目应根据最小幅度、
分位数、
分位数和最大幅度标记的断点分为三个类别之一。 -
重新创建下一个显示的图,根据(i)中的因子向量,绘制低、中、高幅事件,其中
pch分别分配为1、2和3。![image]()
-
在(j)的图中添加一个图例,引用三个
pch值。
本章重要代码
| 函数/运算符 | 简要描述 | 首次出现 |
|---|---|---|
barplot |
创建条形图 | 第 14.1.1 节,p. 290 |
geom_bar |
条形图几何对象 | 第 14.1.1 节,p. 292 |
scale_x_discrete |
修改离散的* x *轴(ggplot2) |
第 14.1.1 节,第 292 页 |
scale_y_continuous |
修改连续的* y *轴 | 第 14.1.1 节,第 292 页 |
theme_bw |
黑白配色主题 | 第 14.1.1 节,第 292 页 |
coord_flip |
切换* x 轴和 y *轴 | 第 14.1.1 节,第 292 页 |
scale_fill_grey |
填充颜色为灰度色 | 第 14.1.1 节,第 292 页 |
pie |
创建饼图 | 第 14.1.2 节,第 293 页 |
hist |
创建直方图 | 第 14.2 节,第 294 页 |
geom_histogram |
直方图几何对象 | 第 14.2 节,第 297 页 |
geom_vline |
添加垂直线几何对象 | 第 14.2 节,第 297 页 |
scale_linetype_manual |
修改ggplot2线条类型 |
第 14.2 节,第 297 页 |
labs |
ggplot2 图例标签 |
第 14.2 节,第 297 页 |
boxplot |
创建箱线图 | 第 14.3.1 节,第 298 页 |
~ |
根据...绘制 | 第 14.3.2 节,第 299 页 |
pairs |
散点图矩阵 | 第 14.4.2 节,第 303 页 |
scale_shape_manual |
修改ggplot2点的形状 |
第 14.4.2 节,第 304 页 |
ggpairs |
散点图矩阵(GGally) |
第 14.4.2 节,第 304 页 |
第十五章:15
概率

概率的概念是统计推理的核心。即使是最复杂的统计技术和模型,通常也有一个最终目标,即对某一现象做出概率性的陈述。在本章中,我将通过简单的日常例子来阐述这一关键概念,为接下来的章节做准备。如果你已经熟悉概率和随机变量的基础知识以及相关术语,你可能想跳到第十六章,那里 R 的功能将更为突出。
15.1 什么是概率?
概率是一个数字,它描述了与做出特定观察或陈述相关的“机会大小”。它始终是一个介于 0 和 1 之间(包括 0 和 1)的数字,通常以分数形式表示。你如何计算概率取决于事件的定义。
15.1.1 事件与概率
在统计学中,事件通常指的是可能发生的某个特定结果。为了描述事件A实际发生的概率,你使用概率,记作 Pr(A)。在极端情况下,Pr(A) = 0 意味着A不可能发生,而 Pr(A) = 1 则意味着A会确定无疑地发生。
假设你掷一个六面公平骰子。让A表示“你掷出 5 或 6”这一事件。你可以假设,在标准骰子上,每个结果发生的概率是 1/6。基于这些条件,你可以得到:

这就是所谓的频率主义,或经典概率,它假设是事件在多个相同的客观试验中发生的相对频率。
另一个例子是,假设你结婚了,并且比平时晚得多回到家。让B表示“你的伴侣因你迟到而生气”这一事件。从数学角度观察A是一个相对直接的过程,但B就不那么容易客观观察,而且这个量也无法轻易计算。相反,你可能会根据自己过去的经验为 Pr(B)赋值。例如,如果你认为你的伴侣生气的概率是 50%,你可能会说“我认为 Pr(B) = 0.5”,但这只是基于你对这种情况的个人印象以及对伴侣脾气或情绪的了解,而不是基于一个可以轻松复制的公正实验。这就是所谓的贝叶斯概率,它利用先验知识或主观信念来指导计算。
由于其自然隐含的客观性,频率主义的解释通常被视为概率的默认定义;在本书中,你将专注于这种类型的概率。如果你有兴趣了解如何使用 R 进行贝叶斯分析,Kruschke(2010)是一本关于这一主题的广受好评的书籍。
注意
尽管用可能性(likelihood)来定义概率概念是很诱人的(在口语中,许多人确实这么做),但在统计理论中,可能性具有稍微不同的含义,因此我现在避免使用这个术语。
当考虑多个事件时,计算概率的方式由几个重要的规则决定。这些规则与比较逻辑值TRUE和FALSE时的与(AND)和或(OR)概念类似,后者在 R 中通过&&和||进行比较(参见第 4.1.3 节)。就像这些逻辑比较一样,基于多个已定义事件的概率计算通常可以分解为针对两个不同事件的特定计算。为了在接下来的几个部分中作为简单的示例,假设你掷一个标准骰子,事件A定义为“你掷出 4 或更大的点数”,事件B定义为“你掷出一个偶数”。因此,你可以得出结论,既
又
。
15.1.2 条件概率
条件概率是指在考虑另一个事件发生后,某个事件发生的概率。数量 Pr(A|B)表示“在B已发生的情况下,A发生的概率”,如果你写 Pr(B|A),则表示反过来的情况。
如果 Pr(A|B) = Pr(A),则两个事件是独立的;如果 Pr(A|B) ≠ Pr(A),则两个事件是相关的。通常情况下,你不能假设 Pr(A|B)等于 Pr(B|A)。
回到之前定义的事件A和B,假设你掷一个骰子。你已经知道
。现在考虑 Pr(A|B)。在已知掷出的数字是偶数的情况下,掷出 4 或更大数字的概率是多少?由于偶数有三个,2、4 和 6,因此,在偶数已出现的情况下,掷出 4 或更大数字的概率是
。因此,Pr(A|B) ≠ Pr(A),因此这两个事件是相关的。
15.1.3 交集
两个事件的交集写作 Pr(A ∩ B), 读作“A和B同时发生的概率”。通常用维恩图来表示这一点,如下所示:

这里,标有A的圆盘表示满足A的结果(或结果集合),而圆盘B表示满足B的结果(或结果集合)。阴影区域表示同时满足A和B的具体结果(或结果集合),而两个圆盘外的区域表示既不满足A也不满足B的结果(或结果集合)。理论上,你有如下情况:

如果 Pr(A ∩ B) = 0,则表示两个事件是互斥的。换句话说,它们不能同时发生。还需要注意的是,如果两个事件是独立的,那么公式(15.1)可以简化为 Pr(A ∩ B) = Pr(A) × Pr(B)。
回到骰子的例子,单次掷骰时,出现偶数并且它是 4 或以上的概率是多少?利用
和
的事实,计算
是很容易的,如果你愿意,还可以在 R 中验证这一点。
R> (2/3)*(1/2)
[1] 0.3333333
你可以看到这两个事件不是互斥的,因为 Pr(A ∩ B) ≠ 0。这是有道理的——在掷骰子时,观察到一个既是偶数又大于等于 4 的数字是完全可能的。
15.1.4 并集
两个事件的并集表示为 Pr(A ∪ B),并且可以读作“A或B发生的概率”。以下是并集的维恩图表示:

从理论上讲,你有这个:

你需要在这个图中减去交集的原因是,在单独求 Pr(A)和 Pr(B)时,你会错误地把 Pr(A ∩ B)计算两次。然而,如果两个事件是互斥的,那么公式(15.2)就简化为 Pr(A ∪ B) = Pr(A) + Pr(B)。
所以,在掷骰子时,观察到一个偶数或者一个至少为 4 的数字的概率是多少?利用(15.2)公式,很容易计算出
。以下是在 R 中验证这一结果:
R> (1/2)+(1/2)-(1/3)
[1] 0.6666667
15.1.5 补集
最后,事件的补集的概率表示为 Pr(Ā),并且可以读作“A不发生的概率”。
这是它的维恩图表示:

从这个图中,你可以看到以下内容:
Pr(Ā) = 1 − Pr(A)
总结这个运行例子,很容易找到你不会掷到 4 或更大的数字的概率:
。自然地,如果没有掷到 4、5 或 6,那么你一定掷到了 1、2 或 3,所以剩下的六个结果中有三个是可能的。
当然,掷骰子的例子可能并不是今天统计研究者面临的最紧迫的问题,但它提供了概率规则行为和术语的清晰示例。这些规则在各个领域都适用,并在统计建模中的更紧迫任务的解释中起着重要作用。
习题 15.1
你有一副标准的 52 张扑克牌。共有两种颜色(黑色和红色)和四种花色(黑桃是黑色,梅花是黑色,红心是红色,方块是红色)。每种花色有 13 张牌,其中包括一张 A,2 到 10 的数字牌,以及三张人头牌(杰克、皇后和国王)。
-
你随机抽取一张卡片,然后放回。抽到一张 A 的概率是多少?抽到黑桃 4 的概率是多少?
-
你随机抽一张牌,并在替换后再抽一张。设 A 为事件“抽到一张梅花牌”;设 B 为事件“抽到一张红牌”。求 Pr(A|B)。也就是说,第二张牌是梅花牌的概率是多大,给定 第一张牌是红牌?这两个事件独立吗?
-
重复(b)部分,这次假设当第一张(梅花)牌被抽出时,它不会被替换。这会改变你对(b)部分独立性的回答吗?
-
设 C 为事件“抽到一张面牌”,设 D 为事件“抽到一张黑色牌”。你抽出一张牌。求 Pr(C ∩ D)。这两个事件是互斥事件吗?
15.2 随机变量与概率分布
随机变量 是一种变量,其特定的结果被假定为通过偶然或某种随机的或 随机过程 机制产生。
你已经遇到过 变量 —— 描述个体实体的特征,基于你观察到的数据(第 13.1 节)。然而,当你考虑随机变量时,假设你还没有进行观察。观察到一个特定值,或位于特定区间的值,具有与之相关的概率。
因此,考虑随机变量与定义这些概率的函数相关联是有意义的,这个函数被称为 概率分布。在本节中,你将看到一些关于如何总结随机变量及其对应的概率分布如何在统计上处理的基本方法。
15.2.1 实现
因此,随机变量的概念围绕着以概率的方式考虑变量可能的结果。当你实际观察到一个随机变量时,这些结果被称为 实现。
考虑以下情况——假设你掷了你心爱的骰子。定义随机变量 Y 为结果。可能的结果是 Y = 1, Y = 2, Y = 3, Y = 4, Y = 5 和 Y = 6。
现在,假设你计划去野餐,并监测你选择的地点的每日最高气温。设随机变量 W 为你观察到的温度(华氏度)。严格来说,你可以说 W 的可能实现值位于区间 −∞ < W < ∞。
这些例子旨在说明两种类型的随机变量。Y 是一个 离散随机变量;W 是一个 连续随机变量。任何给定的随机变量是否是离散的还是连续的,都会影响你对与实现相关的概率的思考方式,并可能影响你如何利用这些概率。
15.2.2 离散随机变量
离散随机变量遵循与第十三章中所述变量相同的定义。其取值只能是某些精确的数值,且无法进一步精确度量或解释。例如,掷标准骰子时,结果只能是先前由 Y 描述的六种不同可能性之一,因此,观察到“5.91”是没有意义的。
从第 15.1.1 节中,你知道概率与已定义的结果(即事件)直接相关。在讨论离散随机变量时,事件因此是根据变量可以取的不同可能值来定义的,且当考虑与所有可能实现相关的所有概率范围时,相应的概率分布就形成了。
与离散随机变量相关的概率分布称为概率质量函数。由于这些函数定义了所有可能结果的概率,因此任何完整的概率质量函数中概率的总和必须始终等于 1。
例如,假设你进入一个赌场并玩一个简单的赌博游戏。在每一轮中,你可能会以 0.32 的概率输掉 4 美元,0.48 的概率不赢也不输(平局),0.15 的概率赢得 1 美元,或 0.05 的概率赢得 8 美元。由于这些是唯一的四种可能结果,因此概率的总和为 1。令离散随机变量 X 定义为每轮游戏中的“赚得金额”。这些概率的分布如表 15-1 所示;注意,4 美元的损失被表示为负收入,符合 X 的定义。
表 15-1: 假设赌博游戏中获得金额 X 的概率和累积概率
| x | –4 | 0 | 1 | 8 |
|---|---|---|---|---|
| Pr(X = x) | 0.32 | 0.48 | 0.15 | 0.05 |
| Pr(X ≤ x) | 0.32 | 0.80 | 0.95 | 1.00 |
离散随机变量的累积概率分布
累积概率也是概率分布的一部分。对于随机变量 X,累积概率是“观察到小于或等于 x 的概率”,并写作 Pr(X ≤ x)。在离散情况下,通过将概率质量函数中的各个概率相加,直到并包括某个给定的 x 值,来获得累积概率分布。这在表 15-1 的底部一行中有所展示。例如,尽管 Pr(X = 0) 为 0.48,Pr(X ≤ 0) = 0.32 + 0.48 = 0.80。
可视化概率分布总是很有用的,由于X是离散的,使用barplot函数非常方便。运用第 14.1 节中的技巧,以下代码首先存储可能的结果及其相应概率的向量(分别是X.outcomes和X.prob),然后生成图 15-1 的左图:
R> X.outcomes <- c(-4,0,1,8)
R> X.prob <- c(0.32,0.48,0.15,0.05)
R> barplot(X.prob,ylim=c(0,0.5),names.arg=X.outcomes,space=0,
xlab="x",ylab="Pr(X = x)")
可选参数space=0可以去除条形之间的间隙。
接下来,你可以使用内建的cumsum函数逐步累加X.prob中的条目,如下所示,从而获得累积概率:
R> X.cumul <- cumsum(X.prob)
R> X.cumul
[1] 0.32 0.80 0.95 1.00
最后,使用X.cumul,可以像之前一样绘制累积概率分布;以下代码生成了图 15-1 的右面板:
R> barplot(X.cumul,names.arg=X.outcomes,space=0,xlab="x",ylab="Pr(X <= x)")

图 15-1:可视化假设赌博游戏中与事件特定概率相关的概率分布(左)以及相应的累积概率分布(右)
通常,记住以下几点对于基于离散随机变量X的概率质量函数非常重要:
• 有k个不同的结果 x[1]、...、x[k]。
• 对于每个 x[i],其中 i = {1, ..., k},0 ≤ Pr(X = x[i]) ≤ 1。
•
。
离散随机变量的均值和方差
描述或总结感兴趣的随机变量的属性就像处理原始数据一样是很有用的。最有用的两个属性是均值和方差,它们都依赖于与该随机变量相关的概率分布。
对于某个离散随机变量X,均值 μ[X](也称为期望或期望值
[X])是你可以期望在多次实现中得到的“平均结果”。假设X有k个可能的结果,分别标记为x[1]、x[2]、...、x[k]。那么,公式如下:

要找到均值,只需将每个结果的数值与其相应的概率相乘,并将结果相加即可。
对于离散随机变量X,方差
,也写作 Var[X],量化了X可能实现的变异性。理论上,从期望的角度来看,可以证明
。如你所见,离散随机变量方差的计算依赖于其均值μ[X],并给出如下公式:

再次强调,计算过程非常简单——方差是通过对每个实现值与均值的差异进行平方,再乘以对应的发生概率,最后将这些乘积相加来计算的。
实际上,与每个结果相关的概率通常是未知的,需要通过观察数据进行估算。完成这一步后,你应用公式(15.3)和(15.4)来获得相应性质的估计值。另外,请注意,均值和方差的一般描述与第 13.2 节中相同——只不过现在你是针对随机现象来量化集中性和离散度。
让我们考虑一下赌博游戏,其中可能的收益实现X及其关联的概率如表 15-1 中所示。使用面向向量的行为(参见第 2.3.4 节),使用 R 计算X的均值和方差非常容易。通过先前的对象X.outcomes和X.prob,你可以通过元素间的乘法计算X的均值,方法如下:
R> mu.X <- sum(X.outcomes*X.prob)
R> mu.X
[1] -0.73
所以,μ[X] = −0.73。根据同样的逻辑,以下提供了X的方差:
R> var.X <- sum((X.outcomes-mu.X)²*X.prob)
R> var.X
[1] 7.9371
你也可以通过对方差取平方根来计算标准差(回顾第 13.2.4 节中的定义)。这可以通过内置的sqrt命令来实现。
R> sd.X <- sqrt(var.X)
R> sd.X
[1] 2.817286
基于这些结果,你可以对赌博游戏及其结果做出几条评论。期望结果−0.73 表明,平均而言,每回合你将损失 0.73 美元,标准差约为 2.82 美元。这些数量不是,也不需要是,具体定义的结果。它们描述了随机机制在长期运行中的行为。
15.2.3 连续随机变量
再次根据第十三章中变量的定义,连续随机变量的可能实现数量是没有限制的。对于离散随机变量,通常将一个特定的结果视为一个事件,并为其分配一个相应的概率。然而,当你处理连续随机变量时,情况就有所不同了。如果你以第 15.2.1 节中的野餐例子为例,你会发现,即使你限制假设W可能取值的温度范围,比如限制在 40 到 90 华氏度之间(或者,更正式地说,40 ≤ W ≤ 90),在这个范围内仍然有无数个不同的值。测量 59.1 度就像观察 59.16742 度一样合理。因此,不可能为具体的单一温度赋予概率;相反,你需要为区间的值分配概率。例如,基于W,问 Pr(W = 55.2)——“温度正好是 55.2 华氏度的概率是多少?”——这个问题是不合法的。然而,问 Pr(W ≤ 55.2)——“温度小于或等于 55.2 华氏度的概率是多少?”——则是可以回答的,因为它定义了一个区间。
如果你再次思考概率的分布方式,这会更容易理解。对于离散随机变量,你可以直接想象其质量函数为离散的,即像表 15-1 那样的内容,可以像图 15-1 那样绘制。然而,对于连续随机变量,描述概率分布的函数现在必须是连续的,覆盖所有可能的值范围。概率是作为该连续函数下的“面积”计算的,和离散随机变量一样,连续概率分布下的“总面积”必须恰好等于 1。与连续随机变量相关的概率分布被称为概率密度函数。
当考虑以下示例时,这些事实会变得更加清晰。假设你被告知与野餐温度随机变量 40 ≤ W ≤ 90 相关的概率遵循密度函数f(w),其中有以下事实成立:

在这个特定函数中,除以 625 是为了确保总概率为 1。将这个函数可视化时,这一点会更容易理解。为了绘制这个密度函数,首先考虑以下代码:
R> w <- seq(35,95,by=5)
R> w
[1] 35 40 45 50 55 60 65 70 75 80 85 90 95
R> lower.w <- w>=40 & w<=65
R> lower.w
[1] FALSE TRUE TRUE TRUE TRUE TRUE TRUE FALSE FALSE FALSE
[11] FALSE FALSE FALSE
R> upper.w <- w>65 & w<=90
R> upper.w
[1] FALSE FALSE FALSE FALSE FALSE FALSE FALSE TRUE TRUE TRUE
[11] TRUE TRUE FALSE
第一个赋值设置了一个偶数值序列,用来表示某些f(w)的实现,简单地称之为w;第二个赋值使用关系运算符和逐元素逻辑运算符&来创建一个逻辑标志向量,识别出w中那些构成f(w)的“下半部分”值的元素,这部分值由方程(15.5)定义;第三个赋值做了同样的事情,只不过是针对“上半部分”值。
接下来的几行利用lower.w和upper.w来评估f(w)在w中的条目的正确结果。
R> fw <- rep(0,length(w))
R> fw[lower.w] <- (w[lower.w]-40)/625
R> fw[upper.w] <- (90-w[upper.w])/625
R> fw
[1] 0.000 0.000 0.008 0.016 0.024 0.032 0.040 0.032 0.024 0.016
[11] 0.008 0.000 0.000
这并不意味着你刚刚写了一个 R 语言函数来返回任何w的f(w)。你只是创建了向量w并获取了作为向量fw的对应数学函数值。然而,这两个向量足以绘制图形。借助第七章中的技巧,你可以绘制一条表示 35 ≤ w ≤ 95 区间内连续密度函数f(w)的曲线。
R> plot(w,fw,type="l",ylab="f(w)")
R> abline(h=0,col="gray",lty=2)
绘图见图 15-2;注意通过abline添加了一个在f(w)= 0 的虚线水平线。

图 15-2:根据方程(15.5)定义的野餐温度随机变量 W 的概率密度函数的可视化(左)以及从文本中演示特定概率计算的插图(右)
你可以看到,方程式 (15.5) 定义的连续函数呈三角形形状,顶点位于 w = 65。由 w = 40 到 w = 65 的上升线代表了 (15.5) 中的第一个组成部分,下降线代表第二个组成部分,而对于所有 w < 40 和 w > 90 的情况,线坐落在零处,这是 (15.5) 的第三个也是最后一个组成部分。
通常,任何定义了随机变量 W 概率密度的函数 f (w) 都必须具备以下属性:
• f (w) ≥ 0 对于所有 −∞ < w < ∞;并且
•
(函数下方的总面积必须为 1)。
就温度示例而言,你可以从 (15.5) 看出,对于任何 w 值,f (w) ≥ 0。为了计算函数下方的总面积,你只需关注在 40 ≤ w ≤ 90 之间的函数值,因为在其他地方它的值为零。
你可以通过几何方法来计算,由函数与零的水平线围成的三角形的面积。对于这个三角形,你可以使用标准的“半底边乘高”规则。三角形的底边为 90 − 40 = 50,顶点的高度为 0.04。因此,在 R 中,半底边宽度乘以高度可以通过以下公式给出:
R> 0.5*50*0.04
[1] 1
这确认了它确实等于 1;你现在可以看到我在 (15.5) 中具体定义背后的原因。
让我们回到获取温度小于或等于 55.2 华氏度的概率的问题。为此,你必须找到在 f(w) 概率密度函数下的区域,该区域由零的水平线和 55.2 处的虚拟垂直线所限定。这个特定的区域形成了另一个三角形,因此再次适合使用“半底边乘高”的规则。在笛卡尔坐标系中,这是由 (40,0)、(55.2,0) 和 (55.2, f (55.2)) 这三点形成的三角形,如 图 15-2 右侧面板所示——稍后你会看到如何绘制这一图形。
因此,你应该首先计算 f (55.2) 的值。从 方程式 (15.5) 中,这可以通过创建以下对象来提供:
R> fw.specific <- (55.2-40)/625
R> fw.specific
[1] 0.02432
请注意,这不是一个概率;它不能被赋予特定的实现值。它只是连续密度函数中三角形的高度值,你将需要它来计算基于区间的概率 Pr(W ≤ 55.2)。
你可以轻松确定在这种情况下,感兴趣的三角形的底边是 55.2 − 40 = 15.2。然后,结合 fw.specific,注意到“半底边乘高”给出了以下结果:
R> fw.specific.area <- 0.5*15.2*fw.specific
R> fw.specific.area
[1] 0.184832
答案已经得出。你已经通过几何方法,使用 f(w) 证明了 Pr(W ≤ 55.2) = 0.185(四舍五入到三位小数)。换句话说,你可以说,野餐地点的最大温度小于或等于 55.2 华氏度的概率大约是 18.5%。
再次强调,所有这些内容在视觉上更易于理解。以下的 R 代码重新绘制了密度函数f(* w *),并标出并填充了感兴趣的区域:
R> fw.specific.vertices <- rbind(c(40,0),c(55.2,0),c(55.2,fw.specific))
R> fw.specific.vertices
[,1] [,2]
[1,] 40.0 0.00000
[2,] 55.2 0.00000
[3,] 55.2 0.02432
R> plot(w,fw,type="l",ylab="f(w)")
R> abline(h=0,col="gray",lty=2)
R> polygon(fw.specific.vertices,col="gray",border=NA)
R> abline(v=55.2,lty=3)
R> text(50,0.005,labels=fw.specific.area)
结果是图 15-2 的右侧面板。绘图命令应该与第七章中的相似,除了polygon。内置的polygon函数允许你提供自定义的顶点,以便在现有图形上绘制或填充一个多边形。这里,通过rbind定义了一个包含两列的矩阵,分别提供了需要填充的三角形三个顶点的* x 和 y 位置(第一列和第二列)。注意,fw.specific.vertices的创建使用了fw.specific,即 f ( w )在 w * = 55.2 时的值;这是被填充三角形的最上方顶点。传递给polygon的进一步参数控制着阴影的颜色(col="gray")和是否在定义的多边形周围绘制边框(border=NA表示不绘制边框)。
并非所有的密度函数都可以通过这种简单的几何方式来评估。正式来说,积分是用于求解连续函数下的面积的数学运算,通常用∫符号表示。也就是说,对于熟悉这一技术的数学爱好者来说,证明“从w = 40 到w = 55.2 的f(* w )下的面积,即 Pr(W* ≤ 55.2)”是通过以下公式得到的,应该是简单直观的:

在 R 中,这个计算的第三行看起来是这样的:
R> (55.2²-80*55.2-40²+80*40)/1250
[1] 0.184832
R 找到了正确的结果。我暂时不讨论数学细节,但确认更一般的积分确实与基于三角形面积的直观几何解相匹配,这是之前计算的fw.specific.area。
现在变得更加清晰,为什么将概率分配给与连续随机变量相关的单一特定实现没有意义。例如,在单一值处评估“函数f(* w )下的面积”就等同于找一个底边宽度为零的多边形的面积,因此,任何Pr(W* = w)的概率本质上是零。此外,在连续设置中,无论你使用<还是≤,或>还是≥,对你的计算结果没有影响。因此,尽管你之前找到了 Pr(W ≤ 55.2),如果你的任务是找 Pr(W < 55.2),你仍然会得到相同的答案 0.185。最开始这些概念可能看起来有点不自然,但归根结底,这是由于存在无限多个可能的实现,因此对于特定值的“相等”没有实际意义。
连续随机变量的累积分布函数
连续变量的累积分布与离散变量的累积分布解释方式相同。给定某个值 w,累积分布函数提供了观察到 w 或更小值的概率。这可能看起来很熟悉;你之前计算的概率 Pr(W ≤ 55.2),基于图 15-2 右侧阴影三角形或使用解析方法,实际上就是一个累积概率。更一般地,计算连续随机变量的累积概率时,需要计算从 −∞ 到 w 的密度函数下的面积。因此,这一过程通常需要对相关概率密度函数进行数学积分。看图 15-2,你应该想象一条垂直线从密度图的左侧向右移动,并在每个位置上评估该线左侧密度函数下的面积。
对于野餐温度的例子,可以证明累积分布函数 F 给出如下:

利用之前的序列 w 和逻辑标志向量 lower.w 与 upper.w,你可以使用相同的向量子集和覆盖方法绘制 F(w);以下代码创建了所需的向量 Fw 并生成了图 15-3:
R> Fw <- rep(0,length(w))
R> Fw[lower.w] <- (w[lower.w]²-80*w[lower.w]+1600)/1250
R> Fw[upper.w] <- (180*w[upper.w]-w[upper.w]²-6850)/1250
R> Fw[w>90] <- 1
R> plot(w,Fw,type="l",ylab="F(w)")
R> abline(h=c(0,1),col="gray",lty=2)

图 15-3:绘制野餐温度例子的累积分布函数,给出如方程 (15.6)所示。观察到温度小于(或等于)55.2 的累积概率已被标出。
在创建该图之后,附加这两行清楚地标明了在 w = 55.2 时,累积概率恰好位于 F 曲线之上:
R> abline(v=55.2,lty=3)
R> abline(h=fw.specific.area,lty=3)
连续随机变量的均值和方差
自然地,确定连续随机变量的均值和方差也是可能的,并且是有用的。
对于具有密度 f 的连续随机变量 W,其均值 μ[W](或称为期望或期望值
)可以解释为在多个实验中你可以期望的“平均结果”。其数学表达式如下:

该方程表示方程 (15.3)的连续类比,可以解读为“通过将密度 f(w) 与 w 自身的值相乘得到的函数下的总面积”。
对于 W,方差
,也写作 Var[W],量化了 W 实现的固有变异性。计算连续随机变量的方差依赖于其均值 μ[W],其计算公式如下:

再次说明,步骤是找到密度函数下的面积,并乘以某个量——在这个例子中,是W的值与总体期望值μ[W]的平方差。
对于野餐温度随机变量的均值和方差的评估必须分别遵循(15.7)和(15.8)公式。这些计算变得相当复杂,因此我不会在这里重复它们。不过,图 15-2 显示,W的均值必须是μ[W] = 65;它是对称密度函数f (w)的完美中心。
就所需的积分而言,你可以使用先前存储的w和fw对象,通过执行以下操作来查看这两个函数,w f (w)和(w − μ[W])² f (w),从而生成图 15-4 中的两个图像:
R> plot(w,w*fw,type="l",ylab="wf(w)")
R> plot(w,(w-65)²*fw,type="l",ylab="(w-65)² f(w)")

图 15-4:温度例子中概率密度函数的期望值(左)和方差(右)的积分函数
请放心,接下来可以通过公式(15.7)和(15.8)进行数学推导。

通过在图 15-4 中的每个图像下方近似计算面积,你会发现这些结果是一致的。正如之前所说,W的分布的标准差是通过方差的平方根给出的,以下内容可以轻松提供这个值:
R> sqrt(104.1667)
[1] 10.20621
15.2.4 形态、偏斜与模态
到目前为止,你已经熟悉了连续和离散随机变量及其与概率分布的自然配对,并且已经看过了概率质量和密度函数分布的可视化图像。在本节中,我将定义一些用于描述这些分布外观的术语——能够描述你的视觉印象和能够迅速计算它们同样重要。
你常常会听到或读到以下这些描述词:
对称性 如果一个分布是对称的,那么你可以在中心画一条垂直线,且它在该中心线两侧的反射是相等的,且 0.5 的概率分别落在这两侧(见图 15-2)。一个对称的概率分布意味着该分布的均值和中位数是相同的。
偏斜 如果一个分布是非对称的,你可以通过讨论偏斜来进一步描述。 当一个分布的“尾巴”(也就是说,从其集中趋势的度量偏移)在某一方向上比另一方向更长时,就说该分布在这个方向上存在偏斜。正偏或右偏表示尾巴在中心右侧延伸得更长;负偏或左偏则表示尾巴在中心左侧延伸得更长。你还可以进一步描述偏斜的强度或显著性。
峰态 概率分布不一定总是只有一个峰。峰态 描述了感兴趣的分布中易于识别的峰的数量。例如,单峰、双峰 和 三峰 是描述具有一个、两个和三个峰的分布的术语。
图 15-5 提供了对称性、不对称性、偏斜和峰态的视觉解释。(注意,尽管它们是用连续线绘制的,但你可以假设它们代表离散概率质量函数 或 连续密度函数的整体形状。)

图 15-5:描述概率分布的术语的一般示例。前三个图像是单峰的,突出了对称与不对称偏斜的概念;底部两个图像强调了峰态的参考。
你可以使用这些描述符来讨论赌博游戏和野餐温度的概率分布示例。左边在图 15-1 中的 X 的质量函数是单峰且不对称的——它似乎有轻微但明显的右偏。给定在图 15-2 中的 W 的密度函数也是单峰的,尽管如前所述,它是完全对称的。
习题 15.2
-
对于以下每个定义,识别它是最适合描述为随机变量还是随机变量的实现。此外,识别每个陈述是否描述的是连续量还是离散量。
-
2016 年 6 月 3 日你附近咖啡店制作的咖啡数量 x
-
你附近咖啡店在任何给定日子制作的咖啡数量 X
-
Y,明天是否会下雨
-
Z,明天降水的量
-
你桌面上现在有多少块面包屑 k?
-
任何指定时间你桌面上面包屑的总质量 W
-
-
假设你构建了下表,提供与随机变量 S 相关的概率,这个随机变量表示某位评论家在特定电影类型中给予的总星级:
s 1 2 3 4 5 Pr(S = s) 0.10 0.13 0.21 ??? 0.15 -
假设这个表格描述了完整的结果集,求出缺失的概率 Pr(S = 4)。
-
获取累积概率。
-
随机变量 S 的均值是多少?即这位评论家为该类型任何给定的电影预计给予多少颗星?
-
随机变量 S 的标准差是多少?
-
在这个类型中,任何给定的电影至少获得三颗星的概率是多少?
-
可视化并简要评论概率质量函数的外观。
-
-
返回到基于随机变量 W 的野餐温度例子,该随机变量在第 15.2.3 节中定义。
-
编写一个 R 函数,根据公式(15.5)返回任意数值向量w的f(w)。尽量避免使用循环,使用面向向量的操作。
-
编写一个 R 函数,根据公式(15.6)返回任意数值向量w的F(w)。同样,尽量避免使用显式或隐式的循环。
-
使用你在(i)和(ii)中编写的函数来验证文本中的结果,换句话说,验证f(55.2) = 0.02432,以及F(55.2) = 0.184832。
-
使用你为F(w)编写的函数来计算 Pr(W > 60)。提示:注意,由于f(w)下方的总面积为 1,因此 Pr(W > 60) = 1 − Pr(W ≤ 60)。
-
求解 Pr(60.3 < W < 76.89)。
-
-
假设以下每个标记为(i)–(iv)的图形展示了概率分布的大致形态。使用第 15.2.4 节中的术语来描述每个图形的形状。

本章重要代码
| 函数/操作符 | 简短描述 | 首次出现 |
|---|---|---|
polygon |
向图形中添加阴影多边形 | 第 15.2.3 节,第 322 页 |
第十六章:16
常见的概率分布

在本章中,你将了解一些用于处理统计建模中常见随机现象的标准概率分布。这些分布遵循与第十五章中所展示的例子相同的自然规则,它们非常有用,因为它们的性质已经得到了广泛的理解和记录。实际上,它们的普遍性如此之强,以至于大多数统计软件包都具有相应的内置功能来评估它们,R 语言也不例外。这些分布中的一些在传统的统计假设检验中扮演了重要角色,这些内容将在第十七章和第十八章中进行探讨。
就像它们所建模的随机变量一样,你在这里将要查看的常见分布大致可分为离散型和连续型。每个分布都有四个核心的 R 函数与之相关——d-函数,用于提供特定的质量或密度函数值;p-函数,用于提供累积分布概率;q-函数,用于提供分位数;以及 r-函数,用于提供随机变量生成。
16.1 常见的概率质量函数
你将从查看一些常见的离散随机变量的概率质量函数的定义和例子开始。连续分布将在第 16.2 节中进行探讨。
16.1.1 伯努利分布
伯努利 分布是一个离散随机变量的概率分布,它只有两个可能的结果,例如成功或失败。这种类型的变量可以称为 二元 或 二分。
假设你已经定义了一个二元随机变量 X,表示一个事件的成功或失败,其中 X = 0 为失败,X = 1 为成功,p 是已知的成功概率。表 16-1 显示了 X 的概率质量函数。
表 16-1: 伯努利概率质量函数
| x | 0 | 1 |
|---|---|---|
| Pr(X = x) | 1 − p | p |
从第 15.2.2 节你可以知道,所有可能结果的概率总和必须为 1。因此,如果二元随机变量的成功概率是 p,那么唯一的其他可能结果——失败,必须以概率 1 − p 发生。
用数学术语表示,对于离散随机变量 X = x,伯努利质量函数 f 为

其中 p 是分布的一个参数。符号表示
X ∼ BERN(p)
通常用来表示“X 遵循一个参数为 p 的伯努利分布。”
以下是需要记住的关键点:
• X 是二元的,只能取值 1(“成功”)或 0(“失败”)。
• p 应该被理解为“成功的概率”,因此 0 ≤ p ≤ 1。
均值和方差分别定义如下:

假设你使用掷骰子的常见例子,成功定义为掷出 4 点,并且你掷一次骰子。那么你就有了一个二元随机变量 X,它可以通过伯努利分布来建模,成功的概率是
。对于这个例子,
。你可以轻松地通过(16.1)得出:

同样地,
也有类似的定义。进一步地,你会得到
和
。
16.1.2 二项分布
二项分布是描述在 n 次试验中成功的分布,这些试验涉及二元离散随机变量。伯努利分布的作用通常是作为更复杂分布(如二项分布)的“构建模块”,这些复杂分布能提供更有趣的结果。
例如,假设你定义了一个随机变量
,其中 Y[1]、Y[2]、...、Y[n]* 都是每个对应于相同事件的伯努利随机变量,换句话说,就是掷骰子的结果,成功定义为掷出 4 点。新的随机变量 X 是伯努利随机变量的和,现在描述的是“在 n 次试验中成功的次数”。如果满足某些合理的假设,描述这个成功次数的概率分布就是二项分布。
在数学术语中,对于离散随机变量和一个具体实现 X = x,二项质量函数 f 为:

其中

被称为二项系数。(回想一下在习题 10.4 中首次讨论的整数阶乘运算符!,见第 203 页)这个系数,也叫做组合,表示你可能在 n 次试验中观察到 x 次成功的所有不同顺序。
二项分布的参数是 n 和 p,符号表示为
X ∼ BIN(n, p)
通常用来表示 X 服从二项分布,进行 n 次试验,参数为 p。
以下是需要记住的关键点:
• X 只能取值 0, 1, ..., n,表示成功的总次数。
• p 应该理解为“每次试验的成功概率”。因此,0 ≤ p ≤ 1,且 n > 0 是一个整数,表示“试验的次数”。
• 每次* n * 次试验中的每一项都是伯努利成功或失败事件,试验是独立的(换句话说,一个试验的结果不会影响其他试验的结果),并且p是常数。
均值和方差分别定义如下:

计数重复试验中二值测试的成功次数是本节开头提到的常见随机现象之一。考虑一种特定情况,其中只有一次“试验”,即 n = 1。检查方程 (16.2) 和 (16.3),应该很清楚 (16.2) 会简化为 (16.1)。换句话说,伯努利分布只是二项分布的一个特例。显然,从二项随机变量作为 n 个伯努利随机变量之和的定义来看,这一点是有意义的。R 提供了二项分布的相关功能,但并未明确支持伯努利分布。
为了说明这一点,我将回到投掷骰子的例子,其中成功定义为得到 4。如果你独立地掷骰子八次,观察到恰好五次成功(五个 4)的概率是多少?那么,你将得到
,这个概率可以通过 (16.2) 进行数学推导。

结果告诉你,在八次投掷中恰好观察到五个 4 的概率约为 0.4%。这个概率很小,且合乎常理——在八次投掷中,观察到零到两个 4 的概率远大于观察到五个 4 的概率。
幸运的是,R 函数可以处理这些情况下的运算。内置函数 dbinom、pbinom、qbinom 和 rbinom 都与二项分布和伯努利分布相关,并在一个帮助文件中根据这些函数名称进行总结。
• dbinom 直接提供任何有效 x 的质量函数概率 Pr(X = x)——即,0 ≤ x ≤ n。
• pbinom 提供累积概率分布——给定一个有效的 x,它会输出 Pr(X ≤ x)。
• qbinom 提供逆累积概率分布(也称为分布的分位数函数)——给定一个有效的概率 0 ≤ p ≤ 1,它会输出满足 Pr(X ≤ x) = p 的 x 值。
• rbinom 用于根据特定的二项分布生成任何数量的 X 实现。
dbinom 函数
通过这些知识,你可以使用 R 来确认刚才提到的骰子投掷例子的结果 Pr(X = 5)。
R> dbinom(x=5,size=8,prob=1/6)
[1] 0.004167619
对于 dbinom 函数,你需要提供感兴趣的具体值作为 x;试验的总次数 n 作为 size;每次试验成功的概率 p 作为 prob。符合 R 规范,你可以为 x 提供一个向量参数。如果你想得到这个例子的 X 的完整概率质量函数表,可以将向量 0:8 传递给 x。
R> X.prob <- dbinom(x=0:8,size=8,prob=1/6)
R> X.prob
[1] 2.325680e-01 3.721089e-01 2.604762e-01 1.041905e-01 2.604762e-02
[6] 4.167619e-03 4.167619e-04 2.381497e-05 5.953742e-07
这些值可以确认其和为 1。
R> sum(X.prob)
[1] 1
结果概率向量对应于特定的结果x = {0, 1, ..., 8},以科学计数法形式返回(参见第 2.1.3 节)。你可以通过使用第 13.2.2 节中介绍的round函数来整理结果,四舍五入到三位小数,使结果更易于阅读。
R> round(X.prob,3)
[1] 0.233 0.372 0.260 0.104 0.026 0.004 0.000 0.000 0.000
在八次试验中,成功一次的概率最高,约为 0.372。 此外,X的均值(期望值)和方差分别为
和
。
R> 8/6
[1] 1.333333
R> 8*(1/6)*(5/6)
[1] 1.111111
你可以像第 15.2.2 节中的例子那样绘制相应的概率质量函数;以下代码生成了图 16-1:
R> barplot(X.prob,names.arg=0:8,space=0,xlab="x",ylab="Pr(X = x)")

图 16-1:与掷骰子例子的二项分布相关的概率质量函数
pbinom 函数
其他与二项分布相关的 R 函数的工作方式大致相同。第一个参数始终是感兴趣的值(或值的集合);n被提供为size,p作为prob。例如,要找出你在八次投掷中观察到三次或更少的 4 的概率 Pr(X ≤ 3),你可以像之前那样从dbinom中累加相关的单个条目,或者使用pbinom。
R> sum(dbinom(x=0:3,size=8,prob=1/6))
[1] 0.9693436
R> pbinom(q=3,size=8,prob=1/6)
[1] 0.9693436
请注意,pbinom的关键参数是标记为q,而不是x;这是因为在累积的意义上,你是在根据分位数寻找概率。pbinom的累积分布结果可以以相同的方式用于寻找“上尾”概率(给定值右侧的概率),因为你知道总的概率质量始终为 1。要找出你在八次掷骰子中至少观察到三次 4 的概率 Pr(X ≥ 3)(这在此离散随机变量的上下文中等同于 Pr(X > 2)),请注意以下内容能找到正确的结果,因为它是 Pr(X ≤ 2)的补集。
R> 1-pbinom(q=2,size=8,prob=1/6)
[1] 0.1348469
qbinom 函数
较少使用的是qbinom函数,它是pbinom的逆函数。pbinom在给定分位值q时提供累积概率,而qbinom则在给定累积概率p时提供分位值。二项随机变量的离散性质意味着qbinom将返回p所在分位值以下的最近* x *值。例如,注意到
R> qbinom(p=0.95,size=8,prob=1/6)
[1] 3
提供了 3 作为分位值,尽管从之前的内容你已经知道,Pr(X ≤ 3)的确切概率是 0.9693436。 在处理连续概率分布时,你会更多地接触到p-和q-函数;见第 16.2 节。
rbinom 函数
最后,使用 rbinom 函数检索二项分布随机变量的实现。再次以
分布为例,请注意以下几点:
R> rbinom(n=1,size=8,prob=1/6)
[1] 0
R> rbinom(n=1,size=8,prob=1/6)
[1] 2
R> rbinom(n=1,size=8,prob=1/6)
[1] 2
R> rbinom(n=3,size=8,prob=1/6)
[1] 2 1 1
初始参数 n 并不代表试验的次数。试验次数仍然通过 size 提供,p 赋值给 prob。在这里,n 表示你想为随机变量
生成的实现次数。前三行每一行请求一个单一的实现——在前八次投掷中,你观察到零次成功(4 点),在第二和第三组八次投掷中,你分别观察到两个和两个 4 点。第四行突出了通过增加 n 可以轻松获得和存储多个 X 实现并将其作为一个向量的事实。由于这些是随机生成的实现,如果你现在运行这些代码,可能会观察到一些不同的值。
尽管在标准统计测试方法中不常使用,但概率分布的 r- 函数,无论是离散的还是连续的,在模拟和计算统计学中的各种高级数值算法中起着重要作用。
练习 16.1
一个森林自然保护区在一大片土地上分布着 13 个观鸟平台。自然学家声称,在任何时刻,每个平台看到鸟的概率为 75%。假设你在保护区内走动并参观每个平台。如果假设所有相关条件都得到满足,设 X 为一个二项分布的随机变量,表示你看到鸟的所有平台数量。
-
可视化感兴趣的二项分布的概率质量函数。
-
在所有平台上看到鸟的概率是多少?
-
在超过 9 个平台上看到鸟的概率是多少?
-
在 8 到 11 个平台之间(包括 8 和 11)看到鸟的概率是多少?使用
d-函数确认你的答案,然后再使用p-函数确认一次。 -
假设在你访问之前,你决定如果你在不到 9 个站点看到鸟,你就会制造一场骚动并要求退还入场费。你让自己尴尬的概率是多少?
-
模拟代表 10 次不同访问的 X 实现;将你得到的向量存储为一个对象。
-
计算感兴趣分布的均值和标准差。
16.1.3 泊松分布
在本节中,你将使用 泊松 分布来模拟一个稍微更一般但同样重要的离散随机变量——计数。例如,感兴趣的变量可能是某个站点在一年内检测到的地震震动次数,或者某个工厂生产线下来的每平方英尺金属板上的缺陷数量。
重要的是,被计数的事件或项目假设彼此独立地发生。从数学角度来看,对于离散随机变量和一个实现 X = x,泊松质量函数 f 如下所示,其中 λ[p] 是分布的一个参数(稍后会进一步解释):

记号
X ∼ POIS(λ[p])
通常用于表示“X 遵循具有参数 λ[p] 的泊松分布。”
以下是需要记住的关键点:
• 被计数的实体、特征或事件在一个明确的区间内以恒定的速率独立发生。
• X 只能取非负整数:0,1,…。
• λ[p] 应解释为“事件发生的均值数量”,因此必须是有限且严格正的;即 0 < λ[p] < ∞。
均值和方差如下:

像二项式随机变量一样,泊松随机变量的取值是离散的、非负整数。然而,与二项式不同,泊松计数通常没有上限。这意味着“无限计数”是允许发生的,但泊松分布的一个显著特点是,当 x 趋向于无穷大时,与某个值 x 相关的概率质量趋近于零。
如方程式 (16.4)所示,任何泊松分布都依赖于单个参数的指定,这里用 λ[p] 表示。这个参数描述了事件发生的均值数量,它影响了质量函数的整体形状,如图 16-2 所示。

图 16-2:泊松概率质量函数的三个示例,绘制了 0 ≤ x ≤ 30 的范围。 “期望计数”参数 λ[p] 从 3.00(左)到 6.89(中间)再到 17.20(右)。*
再次需要注意的是,无论 λ[p] 的值是多少,总概率质量在所有可能的结果上加和为 1,并且尽管可能的结果理论上可以从 0 到无限,但这一点始终成立。
根据定义,容易理解为什么 X 的均值 μ[X] 等于 λ[p];实际上,事实证明,泊松分布随机变量的方差也等于 λ[p]。
考虑本节开头提到的金属生产线上每平方英尺的缺陷示例。假设你被告知,发现的缺陷数量 X 预计遵循泊松分布,且 λ[p] = 3.22,即 X ∼ POIS(3.22)。换句话说,你期望在每个 1 平方英尺的金属片上看到平均 3.22 个缺陷。
dpois 和 ppois 函数
R 中的 dpois 函数提供了泊松分布的单个质量函数概率 Pr(X = x)。ppois 函数提供左侧累积分布概率,如 Pr(X ≤ x)。请参考以下代码行:
R> dpois(x=3,lambda=3.22)
[1] 0.2223249
R> dpois(x=0,lambda=3.22)
[1] 0.03995506
R> round(dpois(0:10,3.22),3)
[1] 0.040 0.129 0.207 0.222 0.179 0.115 0.062 0.028 0.011 0.004 0.001
第一次调用结果表明 Pr(X = 3) = 0.22(保留两位小数);换句话说,观察到在随机选择的金属片上有恰好三个缺陷的概率约为 0.22。第二次调用表示该金属片没有缺陷的概率小于 4%。第三行返回了相关质量函数的一个近似值,适用于 0 ≤ x ≤ 10。你可以手动确认第一个结果,方法如下:
R> (3.22³*exp(-3.22))/prod(3:1)
[1] 0.2223249
你通过以下方法创建质量函数的可视化:
R> barplot(dpois(x=0:10,lambda=3.22),ylim=c(0,0.25),space=0,
names.arg=0:10,ylab="Pr(X=x)",xlab="x")
这显示在图 16-3 的左侧。

图 16-3:泊松概率质量函数(左侧)和累积分布函数(右侧),对于 λ[p] = 3.22 在整数 0 ≤ x ≤ 10 范围内的绘图,参考金属片示例
要计算累积结果,你可以使用 ppois。
R> ppois(q=2,lambda=3.22)
[1] 0.3757454
R> 1-ppois(q=5,lambda=3.22)
[1] 0.1077005
这些行的结果显示,观察到至多两个缺陷的概率 Pr(X ≤ 2) 约为 0.38,观察到严格超过五个缺陷的概率 Pr(X ≥ 6) 约为 0.11。
累积质量函数的可视化展示在右侧的图 16-3,通过以下方法创建:
R> barplot(ppois(q=0:10,lambda=3.22),ylim=0:1,space=0,
names.arg=0:10,ylab="Pr(X<=x)",xlab="x")
qpois 函数
泊松分布的 q 函数 qpois 提供了 ppois 的逆函数,类似于第 16.1.2 节中的 qbinom 提供了 pbinom 的逆函数。
rpois 函数
要生成随机变量,你可以使用 rpois;你需要提供想要生成的变量数 n,并且提供重要的参数 lambda。你可以想象
R> rpois(n=15,lambda=3.22)
[1] 0 2 9 1 3 1 9 3 4 3 2 2 6 3 5
就像从生产线上随机选择 15 个 1 平方英尺的金属片,并统计每片上的缺陷数一样。再次强调,这属于随机生成;你的具体结果可能会有所不同。
练习 16.2
每个周六,在同一时间,一个人站在路边,记录 120 分钟内经过的车辆数。根据先前的知识,她认为在这段时间内经过的车辆平均数量正好是 107。让 X 代表每周六观察到的通过她位置的车辆数的泊松随机变量。
-
在任何一个周六,超过 100 辆车经过她的概率是多少?
-
计算没有车经过的概率。
-
绘制 60 ≤ x ≤ 150 范围内相关的泊松质量函数。
-
从该分布中模拟 260 个结果(大约五年的每周六监测数据)。使用
hist绘制模拟结果;使用xlim设置横坐标范围为 60 到 150。将你的直方图与(c)部分的质量函数形状进行比较。
16.1.4 其他质量函数
在 R 内置的统计计算套件中,还有许多其他明确定义的概率质量函数。它们在某些条件下以特定方式对离散随机变量建模,并且至少定义一个参数,且大多数都有自己的d-、p-、q-和r-函数。这里我总结了更多的函数:
• 几何分布统计在成功发生之前失败的次数,并依赖于一个“成功概率参数”prob。其函数为dgeom、pgeom、qgeom和rgeom。
• 负二项分布是几何分布的推广,依赖于参数size(试验次数)和prob。其函数为dnbinom、pnbinom、qnbinom和rnbinom。
• 超几何分布用于建模不放回抽样(换句话说,某次“成功”会改变之后成功的概率),依赖于描述样本项目性质的参数m、n和k。其函数为dhyper、phyper、qhyper和rhyper。
• 多项式分布是二项分布的推广,在每次试验中,成功可以出现在多个类别中的任何一个,其参数为size和prob(这时,prob必须是一个概率向量,表示多个类别的概率)。其内置函数仅限于dmultinom和rmultinom。
如前所述,一些常见的概率分布只是描述更一般类别分布函数的简化版或特例。
16.2 常见概率密度函数
在考虑连续随机变量时,您需要处理概率密度函数。有许多常见的连续概率分布,通常用于各种不同类型的问题。在这一节中,您将了解其中一些分布及其在 R 中的d-、p-、q-和r-函数。
16.2.1 均匀分布
均匀分布是一个简单的密度函数,用于描述一个连续的随机变量,其可能值的区间内,概率没有波动。稍后在绘制图 16-4 时,这一点会变得更加清晰。

图 16-4:为了便于比较,将两个均匀分布绘制在相同的尺度上。左: X ∼ UNIF(−0.4,1.1);右: X ∼ UNIF(0.223,0.410)。每个密度函数下方的总面积,和往常一样,都是 1。
对于连续随机变量a ≤ X ≤ b,均匀密度函数f为:

其中a和b是定义分布可能取值区间的参数。记法:
X ∼ UNIF(a, b)
通常用来表示“X服从一个均匀分布,区间为a和b”。
以下是需要记住的关键点:
• X可以取a和b之间区间中的任何值。
• a和b可以是任何值,只要a < b,它们分别表示可能值区间的下限和上限。
均值和方差如下:

对于本节中的更复杂的密度函数,特别是为了理解与连续随机变量相关的概率结构,形象地展示这些函数非常有用。对于均匀分布,考虑方程(16.5),你可以识别出图 16-4 中显示的两种不同的均匀分布。我会很快提供生成这些类型图形的代码。
对于图 16-4 中的左图,你可以手动确认X ∼ UNIF(−0.4,1.1)密度的确切高度:
。对于右图,基于X ∼ UNIF(0.223,0.410),你可以使用 R 来发现它的高度大约是 5.35。
R> 1/(0.41-0.223)
[1] 5.347594
dunif 函数
你可以使用内置的均匀分布d函数dunif来返回定义区间内任何值的高度。dunif命令对于区间外的值返回零。分布的参数,a和b,分别作为min和max的参数提供。例如,下面这一行
R> dunif(x=c(-2,-0.33,0,0.5,1.05,1.2),min=-0.4,max=1.1)
[1] 0.0000000 0.6666667 0.6666667 0.6666667 0.6666667 0.0000000
在传递给x的向量中评估X ∼ UNIF(−0.4,1.1)的均匀密度函数。你会注意到,第一个和最后一个值超出了min和max定义的范围,因此它们的值为零。其他所有值的高度为
,如之前计算的那样。
作为第二个例子,下面这一行
R> dunif(x=c(0.3,0,0.41),min=0.223,max=0.41)
[1] 5.347594 0.000000 5.347594
确认了X ∼ UNIF(0.223,0.410)分布的正确密度值,第二个值零,超出了定义的区间。
尤其是这个最新的例子应该提醒你,连续随机变量的概率密度函数与离散变量的质量函数不同,并不直接提供概率,正如在第 15.2.3 节中提到的。换句话说,dunif返回的结果仅代表各自的密度函数本身,而不是与它们评估的特定x值相关的概率。
要基于均匀密度函数计算一些概率,可以使用一个故障钻孔机的例子。在一个木工车间,假设有一台钻孔机在使用时无法保持恒定对准;相反,它会随机地在目标左偏最多 0.4 厘米或右偏最多 1.1 厘米的地方打击目标。让随机变量 X ∼ UNIF(−0.4,1.1) 表示钻孔机相对于目标的位置。 图 16-5 在更精细的尺度上重新绘制了图 16-4 中的左图。你有三种版本,每个版本都在密度函数下标出不同的区域:Pr(X ≤ −0.21)、Pr(−0.21 ≤ X ≤ 0.6) 和 Pr(X ≥ 0.6)。

图 16-5:钻孔机示例下 X ∼ UNIF(−0.4,1.1)密度函数下的三个区域。左:Pr(X ≤ −0.21);中:Pr(−0.21* ≤ X ≤ 0.6);右:Pr(X ≥ 0.6)。*
这些图是使用第七章中介绍的基于坐标的绘图技巧创建的。密度本身通过以下方式呈现:
R> a1 <- -4/10
R> b1 <- 11/10
R> unif1 <- 1/(b1-a1)
R> plot(c(a1,b1),rep(unif1,2),type="o",pch=19,xlim=c(a1-1/10,b1+1/10),
ylim=c(0,0.75),ylab="f(x)",xlab="x")
R> abline(h=0,lty=2)
R> segments(c(a1-2,b1+2,a1,b1),rep(0,4),rep(c(a1,b1),2),rep(c(0,unif1),each=2),
lty=rep(1:2,each=2))
R> points(c(a1,b1),c(0,0))
你可以使用相同的代码来生成图 16-4 中的图,只需修改xlim和ylim参数以调整坐标轴的刻度。
你在图 16-5 中添加了表示 f (−0.21) 和 f (0.6) 的垂直线,使用了另一次segments调用。
R> segments(c(-0.21,0.6),c(0,0),c(-0.21,0.6),rep(unif1,2),lty=3)
最后,你可以使用polygon函数来为区域上色,该函数最早在第 15.2.3 节中进行探讨。例如,在图 16-5 中的最左边的图中,使用前面的绘图代码,后跟以下内容:
R> polygon(rbind(c(a1,0),c(a1,unif1),c(-0.21,unif1),c(-0.21,0)),col="gray",
border=NA)
如前所述,图 16-5 中的三个阴影区域分别代表从左到右的 Pr(X < −0.21)、Pr(−0.21 < X < 0.6)和 Pr(X > 0.6)。就钻孔机的例子而言,你可以将它们解释为:钻孔机命中目标的概率在左偏 0.21 厘米或更多,钻孔机命中目标的概率在左偏 0.21 厘米到右偏 0.6 厘米之间,和钻孔机命中目标的概率在右偏 0.6 厘米或更多。(记住,在第 15.2.3 节中提到,对于连续随机变量,使用 ≤ 或 <(或 ≥ 或 >)对概率没有影响。)尽管你可以通过几何方法计算这些概率来应对如此简单的密度函数,但使用 R 来计算更快。
punif 函数
记住,连续随机变量相关的概率是通过函数下的面积来定义的,因此你的研究集中在 X 的适当区间,而不是任何特定的数值。密度的 p-函数,和离散随机变量的 p-函数一样,提供了累积概率分布 Pr(X ≤ x)。在均匀密度的上下文中,这意味着给定一个特定的 x 值(作为“分位数”参数 q 提供),punif 将提供从该特定值开始的函数下的左侧区域。
访问 punif,该行
R> punif(-0.21,min=a1,max=b1)
[1] 0.1266667
告诉你,图 16-5 中最左侧的区域代表大约 0.127 的概率。该线
R> 1-punif(q=0.6,min=a1,max=b1)
[1] 0.3333333
告诉你
。最终结果 Pr(−0.21 < X < 0.6),即 54% 的概率,通过以下公式得到:
R> punif(q=0.6,min=a1,max=b1) - punif(q=-0.21,min=a1,max=b1)
[1] 0.54
由于第一次调用提供的是从 0.6 开始向左的密度下的面积,而第二次调用提供的是从 −0.21 开始向左的面积。因此,这个差值即为定义中的中间面积。
在使用 R 处理概率分布时,能够操作累积概率结果是非常重要的,初学者可能会觉得在使用 p-函数之前,特别是在处理密度函数时,绘制出所需的区域会很有帮助。
qunif 函数
密度的 q-函数比质量函数使用得更多,因为变量的连续性意味着可以为任何有效的概率 p 找到唯一的分位数值。
qunif 函数是 punif 的逆函数:
R> qunif(p=0.1266667,min=a1,max=b1)
[1] -0.21
R> qunif(p=1-1/3,min=a1,max=b1)
[1] 0.6
这些行确认了之前用来获取下尾和上尾概率 Pr(X < −0.21) 和 Pr(X > 0.6) 的 X 值。任何 q-函数都期待一个累积(换句话说,左侧)概率作为它的第一个参数,这就是为什么你需要在第二行提供 1-1/3 来恢复 0.6 的原因。(总面积为 1。你知道你想要的是 0.6 右侧的区域!image;因此,左侧的区域必须是
。)
runif 函数
最后,为了生成特定均匀分布的随机实现,你可以使用 runif。假设木工使用故障的压机钻了 10 个独立的孔;你可以通过以下调用来模拟每个孔相对于其目标的位置的一个实例。
R> runif(n=10,min=a1,max=b1)
[1] -0.2429272 -0.1226586 0.9318365 0.4829028 0.5963365
[6] 0.2009347 0.3073956 -0.1416678 0.5551469 0.4033372
再次提醒,像 runif 这样的 r-函数的具体值每次运行时都会不同。
练习 16.3
你参观了一个国家公园,并被告知,森林中某种树木的高度在 3 到 70 英尺之间均匀分布。
-
遇到一棵比
英尺更矮的树的概率是多少? -
对于这个概率密度函数,标志着最高 15% 树木的分界点的高度是多少?
-
计算树高分布的均值和标准差。
-
使用(c),确认树高在均值的半个标准差范围内(即高于或低于)出现的概率大约为 28.9%。
-
密度函数本身的高度是多少?请在图中显示它。
-
模拟 10 个观察到的树高。根据这些数据,使用
quantile(参考第 13.2.3 节)估计你在(b)中得到的答案。重复你的模拟,这次生成 1,000 个随机变量,再次估计(b)。做几次这个实验,每次都记录下你的两个估计值(一个基于 10 个随机变量,另一个基于 1,000 个)。总的来说,你会注意到你两个估计值(一个基于 10 个随机变量,一个基于 1,000 个随机变量)与(b)中的“真实”值之间有什么不同吗?
16.2.2 正态分布
正态分布 是建模连续随机变量时最著名和最常用的概率分布之一。它的特点是具有独特的“钟形”曲线,也叫做 高斯 分布。
对于连续随机变量 −∞ < X < ∞,正态密度函数 f 为

其中 μ 和 σ 是分布的参数,π 是熟悉的几何常数 3.1415 ...,而 exp{·} 是指数函数(参考第 2.1.2 节)。该符号
X ∼ N(μ, σ)
常用来表示“X 服从均值为 μ 和标准差为 σ 的正态分布”。
以下是需要记住的关键点:
• 理论上,X 可以取从 −∞ 到 ∞ 的任何值。
• 如前所述,参数 μ 和 σ 直接描述了分布的均值和标准差,后者的平方,σ²,就是方差。
• 在实际应用中,均值参数是有限的 −∞ < μ < ∞,标准差参数严格为正且有限 0 < σ < ∞。
• 如果你有一个随机变量 X ∼ N(μ, σ),那么你可以创建一个新的随机变量 Z = (X − μ)/σ,这意味着 Z ∼ N(0,1)。这被称为 标准化 X。
之前提到的两个参数完全定义了一个特定的正态分布。这些分布总是完美对称的,单峰的,并且以均值 μ 为中心,具有通过标准差 σ 定义的“扩展”程度。
图 16-6 的顶部图像展示了四个特定正态分布的密度函数。你可以看到,改变均值会导致分布的平移,其中分布的中心只是简单地移到 μ 的特定值上。较小的标准差的效果是减少分布的扩展,导致密度曲线变得更高、更窄。增加 σ 会使密度曲线在均值周围变平。
底部的图像聚焦于 N(0,1) 分布,这时你有一个均值为 μ = 0,标准差为 σ = 1 的正态密度分布。这个分布被称为 标准正态分布,常作为标准参考,用来比较不同的正态分布随机变量。通常会将某个变量 X ∼ N(μ[X],σ[X]) 重新标定或 标准化 为一个新的变量 Z,使得 Z ∼ N(0,1)(你将在 第十八章 中看到这一应用)。图中的垂直线表示均值零上下 1、2、3 倍标准差的值。这有助于突出显示,对于 任何 正态分布,精确的 0.5 概率位于均值的上方或下方。此外,注意到,数据落在均值一个标准差以内的概率大约是 0.683,落在从 −2σ 到 +2σ 的曲线下的概率约为 0.954,落在 −3σ 到 +3σ 之间的概率约为 0.997。

图 16-6:正态分布示意图。上图:通过改变均值 μ 和标准差 σ 得到的四种不同的密度情况。下图:标准正态分布 N(0,1),标出均值 ±1σ, ±2σ, 和 ±3σ*。
注意
正态密度的数学定义意味着,当你离均值越来越远时,密度函数的值会趋近于零。实际上,任何正态密度函数都永远不会真正触及零的水平线;它只是随着向正无穷或负无穷移动而越来越接近零。这种行为正式被称为 渐近性;在这种情况下,你可以说正态分布 f (x) 在 f (x) = 0 处有一个水平渐近线。讨论概率作为曲线下的面积时,你可以提到“从负无穷到正无穷曲线下的总面积”是 1,换句话说,
。*
dnorm 函数
作为一个概率密度函数,dnorm 函数本身并不提供概率——它仅仅提供所需正态函数曲线 f (x) 在任意 x 点的值。因此,为了绘制正态密度图,你可以使用 seq(参见 第 2.3.2 节)创建一系列细致的 x 值,通过 dnorm 在这些值处评估密度,然后将结果绘制成一条曲线。例如,为了生成与 图 16-6 底部图像类似的标准正态分布曲线,以下代码将生成所需的 x 值,保存在 xvals 中。
R> xvals <- seq(-4,4,length=50)
R> fx <- dnorm(xvals,mean=0,sd=1)
R> fx
[1] 0.0001338302 0.0002537388 0.0004684284 0.0008420216 0.0014737603
[6] 0.0025116210 0.0041677820 0.0067340995 0.0105944324 0.0162292891
[11] 0.0242072211 0.0351571786 0.0497172078 0.0684578227 0.0917831740
[16] 0.1198192782 0.1523049307 0.1885058641 0.2271744074 0.2665738719
[21] 0.3045786052 0.3388479358 0.3670573564 0.3871565916 0.3976152387
[26] 0.3976152387 0.3871565916 0.3670573564 0.3388479358 0.3045786052
[31] 0.2665738719 0.2271744074 0.1885058641 0.1523049307 0.1198192782
[36] 0.0917831740 0.0684578227 0.0497172078 0.0351571786 0.0242072211
[41] 0.0162292891 0.0105944324 0.0067340995 0.0041677820 0.0025116210
[46] 0.0014737603 0.0008420216 0.0004684284 0.0002537388 0.0001338302
然后,dnorm,其中 μ 被指定为 mean,σ 被指定为 sigma,生成在这些 xvals 上的 f(x) 的精确值。最后,像 plot(xvals,fx,type="l") 这样的调用实现了一个简单的密度图,你可以通过添加标题并使用 abline 和 segments 等命令来标记位置(我稍后会再生成另一个图,因此这里没有显示这个基本的图)。
注意,如果你没有为 mean 和 sd 提供任何值,R 的默认行为是实现标准正态分布;前面显示的对象 fx 本可以通过更简短的调用,只用 dnorm(xvals) 创建。
pnorm 函数
pnorm 函数在指定的正态密度下获取左侧概率。与 dnorm 一样,如果没有提供参数值,R 会自动设置 mean=0 和 sd=1。像你在 第 16.2.1 节 中使用 punif 一样,你可以通过提供所需的 q 参数值,使用 pnorm 查找结果的差异,从而找到你想要的区域。
例如,之前提到过,大约 0.683 的概率位于均值的一个标准差范围内。你可以通过对标准正态分布使用 pnorm 来确认这一点。
R> pnorm(q=1)-pnorm(q=-1)
[1] 0.6826895
第一次调用 pnorm 评估了从正 1 左侧(换句话说,从 −∞ 到达)的曲线下的面积,然后计算该面积与从 −1 左侧的面积之间的差值。结果反映了 图 16-6 底部两条虚线之间的比例。这些类型的概率对 任何 正态分布都是相同的。考虑 μ = −3.42 和 σ = 0.2 的分布。那么,以下提供了相同的值:
R> mu <- -3.42
R> sigma <- 0.2
R> mu.minus.1sig <- mu-sigma
R> mu.minus.1sig
[1] -3.62
R> mu.plus.1sig <- mu+sigma
R> mu.plus.1sig
[1] -3.22
R> pnorm(q=mu.plus.1sig,mean=mu,sd=sigma) -
pnorm(q=mu.minus.1sig,mean=mu,sd=sigma)
[1] 0.6826895
如果要指定感兴趣的分布,需要多一些工作,因为它不是标准的,但原理是相同的:从均值加减一个标准差。
正态分布的对称性在计算概率时也非常有用。以 N(3.42,0.2) 分布为例,可以看到你得到一个大于 μ + σ = −3.42 + 0.2 = −3.22(一个 上尾 概率)的观察值的概率,与得到一个小于 μ − σ = −3.42 − 0.2 = −3.62(一个 下尾 概率)的观察值的概率是相同的。
R> 1-pnorm(mu.plus.1sig,mu,sigma)
[1] 0.1586553
R> pnorm(mu.minus.1sig,mu,sigma)
[1] 0.1586553
你也可以手动计算这些值,参考你之前计算的结果,即 Pr(μ − σ < X < μ + σ) = 0.6826895。剩余的概率 在 这个中间区域之外应该是:
R> 1-0.6826895
[1] 0.3173105
因此,在由 μ − σ 和 μ + σ 分别标记的下尾和上尾区域中,必须有如下的概率:
R> 0.3173105/2
[1] 0.1586552
这就是刚刚使用 pnorm 找到的结果(注意,这些计算中可能会有一些小的舍入误差)。你可以在 图 16-7 中看到这个,最初绘制的是以下内容:
R> xvals <- seq(-5,-2,length=300)
R> fx <- dnorm(xvals,mean=mu,sd=sigma)
R> plot(xvals,fx,type="l",xlim=c(-4.4,-2.5),main="N(-3.42,0.2) distribution",
xlab="x",ylab="f(x)")
R> abline(h=0,col="gray")
R> abline(v=c(mu.plus.1sig,mu.minus.1sig),lty=3:2)
R> legend("topleft",legend=c("-3.62\n(mean - 1 sd)","\n-3.22\n(mean + 1 sd)"),
lty=2:3,bty="n")

图 16-7:展示文本中的示例,利用正态分布的对称性来指出曲线下方概率的特征。请注意,密度下方的总面积为 1,结合对称性有助于进行计算。
要添加μ ± σ之间的阴影区域,可以使用polygon,需要输入感兴趣形状的顶点。为了得到平滑的曲线,可以使用代码中定义的精细序列xvals和相应的fx,并使用逻辑向量子集来限制关注范围至* x* 的位置,使得 −3.62 ≤ x ≤ −3.22。
R> xvals.sub <- xvals[xvals>=mu.minus.1sig & xvals<=mu.plus.1sig]
R> fx.sub <- fx[xvals>=mu.minus.1sig & xvals<=mu.plus.1sig]
然后,你可以通过使用polygon函数所期望的矩阵结构,将这些点夹在阴影多边形底部的两个角落之间。
R> polygon(rbind(c(mu.minus.1sig,0),cbind(xvals.sub,fx.sub),c(mu.plus.1sig,0)),
border=NA,col="gray")
最后,arrows和text表示文本中讨论的区域。
R> arrows(c(-4.2,-2.7,-2.9),c(0.5,0.5,1.2),c(-3.7,-3.15,-3.4),c(0.2,0.2,1))
R> text(c(-4.2,-2.7,-2.9),c(0.5,0.5,1.2)+0.05,
labels=c("0.159","0.159","0.682"))
qnorm 函数
我们来看看qnorm。要找到一个使得下尾概率为 0.159 的分位数值,可以使用以下方法:
R> qnorm(p=0.159,mean=mu,sd=sigma)
[1] -3.619715
基于之前的结果和你对q-函数的了解,应该能清楚为什么结果大约是−3.62。你可以使用以下方法找到上四分位数(即使得概率为 0.25 的值):
R> qnorm(p=1-0.25,mean=mu,sd=sigma)
[1] -3.285102
请记住,q-函数将根据(左侧)下尾概率进行操作,因此要基于上尾概率找到分位数,必须先从总概率 1 中减去它。
在频率学派统计学中,某些方法和模型假设观察到的数据是正态分布的。你可以通过使用对正态分布理论分位数的了解来验证这一假设,该分位数可以通过qnorm函数获得:计算观察数据的分位数范围,并将其与标准化的正态分布的相应分位数进行对比。这种可视化工具被称为正态分位数-分位数图或QQ图,当与直方图一起查看时特别有用。如果绘制的点没有落在一条直线上,那么数据的分位数与正态曲线的外观不匹配,假设数据符合正态分布可能不成立。
内置的qqnorm函数接收原始数据并生成相应的图表。再一次回到现成的chickwts数据集。假设你想要检验假设体重是否符合正态分布。为此,你可以使用
R> hist(chickwts$weight,main="",xlab="weight")
R> qqnorm(chickwts$weight,main="Normal QQ plot of weights")
R> qqline(chickwts$weight,col="gray")
生成图 16-8 中给出的 71 个体重的直方图和正态 QQ 图,见图 16-8。附加的qqline命令会添加一条“最优”线,如果数据完全符合正态分布,数据点将沿着这条线排列。

图 16-8:小鸡体重的直方图(左)和正态 QQ 图(右),来自chickwts数据集。数据符合正态分布吗?
如果你检查权重的直方图,你会发现数据大致符合正态分布的整体外观,呈现出大致对称的单峰特征。也就是说,它并没有完全达到平滑且自然衰减的高度,这种高度会产生我们熟悉的正态钟形曲线。这一点在右侧的 QQ 图中有所反映;中央的分位数值似乎相对较好地位于直线上,除了有一些相对较小的“波动”。在外尾部有一些明显的偏差,但需要注意的是,在任何 QQ 图中,观察到这些极端分位数的偏差是很典型的,因为在这些区域自然会出现较少的数据点。综合考虑所有这些因素,对于这个例子来说,正态性的假设并不完全不合理。
注意
在评估这类假设的有效性时,考虑样本大小非常重要;样本越大,直方图和 QQ 图中的随机变动越小,你可以更有信心地得出数据是否符合正态分布的结论。例如,这个例子中的正态性假设可能会因为样本量较小(只有 71 个样本)而变得复杂。
rnorm 函数
任意给定正态分布的随机变量是通过rnorm生成的;例如,以下代码行
R> rnorm(n=7,mu,sigma)
[1] -3.764532 -3.231154 -3.124965 -3.490482 -3.884633 -3.192205 -3.475835
会生成七个来自 N(−3.42,0.2)的正态分布值。与图 16-8 中小鸡体重的 QQ 图相比,你可以使用rnorm、qqnorm和qqline来检查那些假设上符合正态分布的观测数据集在 QQ 图中的变动程度。
以下代码生成 71 个标准正态值,并生成相应的正态 QQ 图,然后对另一个数据集(n = 710)进行相同的操作;这些结果显示在图 16-9 中。
R> fakedata1 <- rnorm(n=71)
R> fakedata2 <- rnorm(n=710)
R> qqnorm(fakedata1,main="Normal QQ plot of generated N(0,1) data; n=71")
R> qqline(fakedata1,col="gray")
R> qqnorm(fakedata2,main="Normal QQ plot of generated N(0,1) data; n=710")
R> qqline(fakedata2,col="gray")

图 16-9:从标准正态分布随机生成的 71 个(左)和 710 个(右)观测值的正态 QQ 图
你可以看到,对于模拟数据集(大小为n = 71)的 QQ 图,与小鸡体重数据集的偏差相似。将样本量增加十倍后,n = 710 的正态观察值的 QQ 图显示出更少的随机变动,尽管尾部的偏差仍然存在。一个很好的方法是多次运行这些代码(换句话说,每次生成新的数据集),并检查每个新的 QQ 图是如何变化的,从而习惯于评估这些影响。
正态函数的应用:一个快速示例
让我们通过一个工作问题结束这一部分。假设某种类型的零食的制造商知道其 80 克标称包装中的零食总净重X是正态分布的,平均值为 80.2 克,标准差为 1.1 克。制造商称重随机选取的单个包装内容。随机选取的包装小于 78 克的概率(即 Pr(X < 78))如下:
R> pnorm(78,80.2,1.1)
[1] 0.02275013
数据包的重量在 80.5 克和 81.5 克之间的概率如下:
R> pnorm(81.5,80.2,1.1)-pnorm(80.5,80.2,1.1)
[1] 0.2738925
小于下列重量的轻量 20%的数据包如下:
R> qnorm(0.2,80.2,1.1)
[1] 79.27422
随机选择的五个数据包的模拟结果如下:
R> round(rnorm(5,80.2,1.1),1)
[1] 78.6 77.9 78.6 80.2 80.8
练习 16.4
-
一名辅导员知道,某种统计学问题的完成时间,对于大一本科生来说,X是正态分布的,平均值为 17 分钟,标准差为 4.5 分钟。
-
随机选择的本科生完成该问题超过 20 分钟的概率是多少?
-
学生完成该问题所需时间在 5 到 10 分钟之间的概率是多少?
-
找出标志着最慢 10%的学生的时间。
-
绘制感兴趣的正态分布图,范围在±4σ之间,并标出概率区域(iii),即最慢的 10%学生。
-
基于 10 名学生完成问题的时间,生成一个时间实现。
-
-
一位细心的园丁对他草坪上的草叶长度很感兴趣。他认为草叶长度X遵循以 10 毫米为中心,方差为 2 毫米的正态分布。
-
找出草叶长度在 9.5 毫米和 11 毫米之间的概率。
-
在该分布的背景下,9.5 和 11 的标准化值分别是多少?利用这些标准化值,确认你能用标准正态密度计算出在(i)中找到的相同概率。
-
最短的 2.5%的草叶长度低于哪个值?
-
将你的答案从(iii)进行标准化。
-
16.2.3 学生 t 分布
学生 t 分布 是一种连续概率分布,通常用于处理从数据样本中估计的统计数据。它将在接下来的两章中变得尤为重要,因此我在这里先简要解释一下。
任何特定的t-分布看起来都像标准正态分布——它是钟形的,对称的,单峰的,并且以零为中心。不同之处在于,正常分布通常用于处理总体数据,而t-分布用于处理来自总体的样本。
对于* t 分布,你不需要定义任何参数,但你必须通过严格正整数 ν * > 0 来选择合适的* t 分布;这称为自由度*(df),之所以这样称呼,是因为它表示在计算给定统计量时,“可以自由变化”的个体组件的数量。在接下来的章节中,你会看到这个量通常与样本大小直接相关。
目前,你可以大致把* t 分布看作是一系列曲线的表示,并把自由度看作是你用来选择使用哪一特定版本密度的“选择器”。虽然在初学阶段, t 分布的精确密度方程并不是特别有用,但记住任何 t *曲线下的总概率自然是 1,这一点是有帮助的。
对于* t 分布,dt、pt、qt 和 rt 函数分别代表 R 中的密度函数、累积分布(左侧概率)、分位数和随机变量生成函数。第一个参数 x、q、p 和 n 分别提供了这些函数所需的相关值;所有这些函数的第二个参数是 df,你必须在其中指定自由度 ν *。
获取* t 分布家族印象的最佳方式是通过可视化。图 16-10 绘制了标准正态分布,以及自由度 ν * = 1,* ν * = 6 和 * ν * = 20 的* t *分布曲线。

图 16-10:比较标准正态分布与三种不同自由度的 t 分布。请注意,自由度越高, t 分布的逼近越接近正态分布。
从图 16-10 以及这一节中,最重要的要点是,随着自由度的增加,* t 密度函数如何相对于 N(0,1) 分布发生变化。对于接近 1 的小自由度 ν 值, t 分布在其众数上更短,更多的概率出现在明显更胖的尾部。事实证明, t 密度随着 ν * → ∞ 而逼近标准正态密度。作为一个例子,注意标准正态分布的上 5%尾部由以下值划定:
R> qnorm(1-0.05)
[1] 1.644854
相同的* t 分布的上尾部提供了 ν * = 1,* ν * = 6 和 * ν * = 20 的自由度值。
R> qt(1-0.05,df=1)
[1] 6.313752
R> qt(1-0.05,df=6)
[1] 1.94318
R> qt(1-0.05,df=20)
[1] 1.724718
与标准正态分布直接比较,* t 密度在尾部的较大权重自然导致在给定特定概率时,分位数值更为极端。然而,随着自由度的增加,这种极端性会减少——这与前面提到的事实一致,即 t *分布随着自由度的提高,逼近标准正态分布的效果会越来越好。
16.2.4 指数分布
当然,概率密度函数不一定像你之前遇到的那样对称,也不需要允许随机变量取值范围从负无穷到正无穷(如正态分布或t分布)。一个很好的例子就是指数分布,对于这种分布,随机变量X的取值仅在 0 ≤ X < ∞的范围内有效。
对于连续随机变量 0 ≤ X < ∞,指数密度函数f为:

其中λ[e]是分布的一个参数,exp{·}是指数函数。记号
X ∼ EXP(λ[e])
通常用来表示“X服从带有速率λ[e]的指数分布”。
以下是需要注意的关键点:
• 从理论上讲,X可以取值范围为 0 到∞,并且随着x的增加,f(x)会减小。
• “速率”参数必须严格为正;换句话说,λ[e] > 0。它定义了f(0)和该函数衰减到零水平渐近线的速率。
均值和方差分别如下所示:

dexp 函数
指数分布的密度函数是一条从f(0)= λ开始的稳定下降的曲线;这一衰减的速率确保曲线下方的总面积为 1。你可以通过下面的代码,使用相关的d函数创建图 16-11。
R> xvals <- seq(0,10,length=200)
R> plot(xvals,dexp(x=xvals,rate=1.65),xlim=c(0,8),ylim=c(0,1.65),type="l",
xlab="x",ylab="f(x)")
R> lines(xvals,dexp(x=xvals,rate=1),lty=2)
R> lines(xvals,dexp(x=xvals,rate=0.4),lty=3)
R> abline(v=0,col="gray")
R> abline(h=0,col="gray")
R> legend("topright",legend=c("EXP(1.65)","EXP(1)","EXP(0.4)"),lty=1:3)

图 16-11:三种不同的指数密度函数。减小λ[e]会降低众数并延长尾部。
参数λ[e]被提供给rate参数,在dexp函数中进行评估,并传递给第一个参数x(通过这个示例中的xvals对象)。你可以看到,指数密度函数的一个显著特征是前述的衰减到零,λ[e]的较大值意味着更高(但更陡峭和更快速)的下降。
这种自然衰减的行为有助于识别指数分布在应用中的作用——一种“直到事件发生的时间”性质的分布。实际上,指数分布和泊松分布之间有一个特殊的关系,后者在第 16.1.3 节中已介绍。当泊松分布用于建模某一事件在时间上的发生次数时,指数分布则用于建模这些事件之间的时间间隔。在这种情况下,指数分布的参数λ[e]定义了事件发生的平均速率。
pexp 函数
让我们回顾一下 练习 16.2 中的例子,其中提到在 120 分钟的时间窗口内通过某个人的平均汽车数量为 107。定义随机变量 X 为两辆车之间的等待时间,并且使用指数分布来表示 X,以分钟为时间尺度,设定 λ[e] = 107/120 ≈ 0.89(保留两位小数)。如果通常在两小时的时间窗口内观察到 107 辆车,那么你看到车的平均速率是每分钟 0.89 辆。
因此,你将 λ[e] 解释为泊松质量函数中 λ[p] 参数的“单位时间”度量。将均值解释为速率的倒数,μ[X] = 1/λ[e],也是直观的。例如,当以每分钟大约 0.89 的速率观察汽车时,注意到两辆车之间的平均等待时间大约是 1/0.89 ≈ 1.12 分钟。
所以,在当前的例子中,你想要检查密度
。
R> lambda.e <- 107/120
R> lambda.e
[1] 0.8916667
假设一辆车刚刚通过某个人的位置,你想找出他们必须等待超过两分半钟才能看到下一辆车的概率,换句话说,Pr(X > 2.5)。你可以使用 pexp 来做到这一点。
R> 1-pexp(q=2.5,rate=lambda.e)
[1] 0.1076181
这表明你大约有 10% 的机会观察到下辆车出现之前有至少 2 分钟 30 秒的间隔。记住,p 函数的默认行为是找到给定值的累计左侧概率,因此你需要从 1 中减去结果,以找到上尾概率。你可以通过以下方式找到等待少于 25 秒的概率,结果大约为 0.31:
R> pexp(25/60,lambda.e)
[1] 0.3103202
注意需要首先将关注值从秒转换为分钟,因为你通过 λ[e] ≈ 0.89 在后者的尺度上定义了 f (x)。
qexp 函数
使用适当的分位数函数 qexp 来找到例如最短 15% 等待时间的截止点。
R> qexp(p=0.15,lambda.e)
[1] 0.1822642
这表明你关注的值大约是 0.182 分钟,换句话说,大约是 0.182 × 60 = 10.9 秒。
和往常一样,你可以使用 rexp 来生成任意特定指数分布的随机变量。
注意
重要的是要区分“指数分布”、“指数族分布”和“指数函数”。前者指的是刚刚研究的密度函数,而后者指的是一个包含泊松分布、正态分布以及指数分布本身的一般概率分布类。第三个则只是标准的数学指数函数,指数族的成员依赖于该函数,并且可以通过 exp 在 R 中直接访问。
练习 16.5
-
位于新西兰中北岛的波胡图间歇泉被称为南半球最大的活跃间歇泉。假设它每年喷发约 3,500 次。
-
为了将随机变量 X 建模为连续喷发之间的时间,计算时间尺度为天数时的参数值 λ[e](假设每年 365.25 天以考虑闰年)。
-
绘制感兴趣的密度函数。两次喷发之间的平均等待时间是多少天?
-
等待下次喷发少于 30 分钟的概率是多少?
-
定义最长 10%的等待时间是多少?将答案转换为小时。
-
-
你也可以使用指数分布来建模某些产品的生存时间,或者“故障时间”类型的变量。假设某个空调设备制造商知道,产品的平均使用寿命为 11 年,之后需要维修。设随机变量 X 表示该设备需要维修的时间,并假设 X 服从参数 λ[e] = 1/11 的指数分布。
-
该公司为该设备提供五年的全面维修保修。随机选择的空调用户使用保修服务的概率是多少?
-
另一家竞争公司为其空调设备提供六年保修,但知道其设备的平均寿命仅为九年,之后需要维修。使用该保修的概率是多少?
-
确定(i)和(ii)中的单位持续超过 15 年的概率。
-
16.2.5 其他密度函数
有许多其他常见的概率密度函数广泛用于涉及连续随机变量的各种任务。以下是一些总结:
• 卡方分布 模拟平方的正态变量之和,因此通常与关于正态分布数据样本方差的操作相关。其函数有 dchisq、pchisq、qchisq 和 rchisq,与 t-分布类似(参见 16.2.3 节),它依赖于作为输入的自由度指定,输入参数为 df。
• F-分布 用于建模两个卡方随机变量的比率,通常用于回归问题中(参见 第二十章)。其函数包括 df、pf、qf 和 rf,由于涉及两个卡方值,因此依赖于一对自由度值的指定,这两个值通过 df1 和 df2 作为输入。
• 伽马分布 是指数分布和卡方分布的一个推广。其函数有 dgamma、pgamma、qgamma 和 rgamma,并且依赖于“形状”参数和“尺度”参数,这两个参数分别通过 shape 和 scale 作为输入。
• 贝塔分布 常用于贝叶斯建模,其已实现的函数有 dbeta、pbeta、qbeta 和 rbeta。它由两个“形状”参数 α 和 β 定义,分别通过 shape1 和 shape2 提供。
尤其在接下来的几章中,你将会遇到卡方分布和F分布。
注意
在你之前研究的所有常见概率分布中,我强调了执行“减一”操作来找到关于上尾或右尾区域的概率或分位数的必要性。这是因为p-和q-函数的累积性质——根据定义,处理的是下尾。然而,大多数 R 中的p-和q-函数包括一个可选的逻辑参数,lower.tail,其默认值为FALSE。因此,另一种方法是将lower.tail=TRUE设置为任何相关函数调用中的参数,这样 R 将特别期望或返回上尾区域。
本章重要代码
| 函数/运算符 | 简要描述 | 首次出现 |
|---|---|---|
dbinom |
二项质量函数 | 第 16.1.2 节,第 335 页 |
pbinom |
二项分布累积问题 | 第 16.1.2 节,第 336 页 |
qbinom |
二项分位数函数 | 第 16.1.2 节,第 337 页 |
rbinom |
二项随机实现 | 第 16.1.2 节,第 337 页 |
dpois |
泊松质量函数 | 第 16.1.3 节,第 340 页 |
ppois |
泊松分布累积问题 | 第 16.1.3 节,第 341 页 |
rpois |
泊松随机实现 | 第 16.1.3 节,第 341 页 |
dunif |
均匀密度函数 | 第 16.2.1 节,第 344 页 |
punif |
均匀分布累积问题 | 第 16.2.1 节,第 346 页 |
qunif |
均匀分位数 | 第 16.2.1 节,第 346 页 |
runif |
均匀随机实现 | 第 16.2.1 节,第 347 页 |
dnorm |
正态密度函数 | 第 16.2.2 节,第 350 页 |
pnorm |
正态分布累积问题 | 第 16.2.2 节,第 350 页 |
qnorm |
正态分位数 | 第 16.2.2 节,第 353 页 |
rnorm |
正态随机实现 | 第 16.2.2 节,第 355 页 |
qt |
学生* t *分位数 | 第 16.2.3 节,第 358 页 |
dexp |
指数密度函数 | 第 16.2.4 节,第 359 页 |
pexp |
指数分布累积问题 | 第 16.2.4 节,第 361 页 |
qexp |
指数分位数 | 第 16.2.4 节, 第 361 页 |
第四部分
统计检验与建模
第十七章:17
抽样分布与置信区间

在第十五章和第十六章中,你将概率分布的概念应用于定义为某些感兴趣的测量或观察的随机变量的示例。在本章中,你将把样本统计量本身作为随机变量来引入抽样分布的概念——这是一种概率分布,用于解释在使用样本统计量估计总体参数时自然存在的变异性。接着,我将引入置信区间的概念,它直接反映了抽样分布中的变异性,并以这种方式使用,能够提供总体参数的区间估计。这将为第十八章中的正式假设检验奠定基础。
17.1 抽样分布
抽样分布就像任何其他概率分布,但它特别与作为样本统计量的随机变量相关。在第十五章和第十六章中,我们假设知道相关示例分布的参数(例如,正态分布的均值和标准差,或二项分布中的成功概率),但在实践中,这些数量通常是未知的。在这种情况下,你通常会从样本中估计这些数量(有关这一点的可视化说明,参见图 13-2,位于第 266 页)。任何从样本中估计的统计量都可以视为一个随机变量,其估计值本身就是该随机变量的实现。因此,完全有可能从同一总体中抽取的不同样本会为相同的统计量提供不同的值——随机变量的实现自然会受到变异性的影响。能够理解并建模这种估计样本统计量固有的自然变异性(使用相关的抽样分布)是许多统计分析中的关键部分。
与其他任何概率分布一样,抽样分布的中心“平衡”点是其均值,但抽样分布的标准差被称为标准误差。术语上的细微变化反映了这样一个事实:感兴趣的概率不再直接与原始测量或观察值相关,而是与从样本中计算出的某个量相关。因此,各种抽样分布的理论公式依赖于(a)假定生成原始数据的原始概率分布,以及(b)样本本身的大小。
本节将解释关键思想并提供一些示例,我将重点介绍两个简单且易于识别的统计量:单一样本均值和单一样本比例。然后,当我在第十八章讨论假设检验时,会进一步展开这个内容,在你查看第二十章到第二十二章的回归方法时,你需要理解抽样分布在评估重要模型参数中的作用。
注意
本章讨论的抽样分布理论的有效性做了一个重要假设。每当我谈论一个样本数据集时,我假设这些观测值相互独立,并且它们是独立同分布的。你会在统计学材料中经常看到这个概念——独立同分布观测值,简称 iid。
17.1.1 样本均值的分布
算术平均数可以说是总结数据集时最常用的集中趋势度量(第 13.2.1 节)。
数学上,估计样本均值所固有的变异性描述如下:正式地,表示关注的随机变量为 X̄。这表示来自“原始观察”随机变量 X 的 n 个观测样本的均值,如 x[1]、x[2]、...、x[n]。这些观测值假定具有真实的有限均值 −∞ < μ[X] < ∞ 和真实的有限标准差 0 < σ[X] < ∞。寻找样本均值的概率分布的条件取决于是否知道标准差的值。
情况 1:已知标准差
当标准差的真实值 σ[X] 已知时,以下结论成立:
• 如果 X 本身是正态分布,样本均值 X̄ 的抽样分布也是正态分布,均值为 μ[X],标准误差为
。
• 如果 X 不是正态分布,样本均值 X̄ 的抽样分布仍然近似正态分布,均值为 μ[X],标准误差为
,且随着 n → ∞,这一近似越来越精确。这被称为 中心极限定理(CLT)。
情况 2:已知标准差
实际上,你通常不会知道生成样本数据的原始测量分布的标准差真实值。在这种情况下,通常将 σ[X] 替换为 s[X],即样本数据的标准差。然而,这种替换会引入额外的变异性,影响与样本均值随机变量相关的分布。
• 样本均值 X̄ 的抽样分布的标准化值(第 16.2.2 节)遵循 t 分布,自由度 ν = n − 1;标准化是通过标准误差完成的
。
• 如果n较小,还需要假设X的分布是正态分布,才能确保基于t分布的X̄的抽样分布的有效性。
因此,X̄的抽样分布的性质取决于是否知道观测值的真实标准差,以及样本大小n。中心极限定理(CLT)指出,即使原始观测分布本身不是正态分布,正态性仍然成立,但如果n较小,这一近似就不太可靠。通常的经验法则是,只有当n ≥ 30 时才可以依赖 CLT。如果使用样本标准差s[X]来计算X̄的标准误差,则抽样分布是t分布(经过标准化)。同样,这通常只有在n ≥ 30 时才被认为是可靠的。
例子:但尼丁气温
作为一个例子,假设新西兰但尼丁一月的每日最高温度遵循正态分布,均值为 22 摄氏度,标准差为 1.5 摄氏度。那么,按照情况 1 的注释,对于样本大小n = 5,X̄的抽样分布将是正态分布,均值为 22,标准误差为
。
图 17-1 的顶部图片显示了原始测量分布以及此抽样分布。你可以使用第十六章中熟悉的代码来生成这个图。
R> xvals <- seq(16,28,by=0.1)
R> fx.samp <- dnorm(xvals,22,1.5/sqrt(5))
R> plot(xvals,fx.samp,type="l",lty=2,lwd=2,xlab="",ylab="")
R> abline(h=0,col="gray")
R> fx <- dnorm(xvals,22,1.5)
R> lines(xvals,fx,lwd=2)
R> legend("topright",legend=c("raw obs. distbn.","sampling distbn. (mean)"),
lty=1:2,lwd=c(2,2),bty="n")
在这个例子中,X̄的抽样分布显然是比与观测值相关的正态分布更高、更窄的正态分布。这是有道理的——你期望多个测量值的平均值比原始的单个测量值变动更小。此外,标准误差分母中包含的n决定了如果增加样本大小,围绕均值的分布将更加精确。同样,这是合理的——更大的样本将使均值在样本之间“变化更小”。
现在你可以提问各种概率问题;请注意,区分测量分布和抽样分布非常重要。例如,下面的代码提供了 Pr(X < 21.5),即在一月中随机选择的一天的最高温度低于 21.5 度的概率:
R> pnorm(21.5,mean=22,sd=1.5)
[1] 0.3694413
下一段代码提供了样本均值小于 21.5 度的概率 Pr(X̄ < 21.5),基于一月中随机选择的五天样本:
R> pnorm(21.5,mean=22,sd=1.5/sqrt(5))
[1] 0.2280283
图 17-1 顶部的线条阴影区域显示了这两个概率。在 R 中,可以通过运行以下代码直接将这些阴影区域添加到该图中:
R> abline(v=21.5,col="gray")
R> xvals.sub <- xvals[xvals<=21.5]
R> fx.sub <- fx[xvals<=21.5]
R> fx.samp.sub <- fx.samp[xvals<=21.5]
R> polygon(cbind(c(21.5,xvals.sub),c(0,fx.sub)),density=10)
R> polygon(cbind(c(21.5,xvals.sub),c(0,fx.samp.sub)),density=10,
angle=120,lty=2)
请注意,在之前使用polygon时,你只是指定了一个col;在这个例子中,我改用了阴影线,通过density(每英寸的线条数)和angle(线条的斜率,单位为度,默认为angle=45)来实现阴影线。

图 17-1:展示样本均值的抽样分布,n = 5,基于 N(22,1.5)的原始观察分布。上图:基于正态分布的抽样分布版本(假设σ[X]已知),与观察分布进行比较。下图:基于t分布的抽样分布版本,使用 4 个自由度(换句话说,假设使用s来计算标准误差),与标准正态分布进行比较。阴影区域表示Pr(X < 21.5),Pr(X̄ < 21.5)(实线和虚线,上图最上面)以及Pr(T < (21.5 − x̄)/(s/
)) (虚线,下图)。
为了评估概率,请注意,你需要了解控制X的参数。在实践中,你很少会拥有这些量(如情境 2 所述)。相反,你会获取一个数据样本并计算汇总统计量。
运行以下代码行会生成五个随机生成的达尼丁温度,这些温度来自于X ∼ N(22,1.5)分布:
R> obs <- rnorm(5,mean=22,sd=1.5)
R> obs
[1] 22.92233 23.09505 20.98653 20.10941 22.33888
现在,为了举例说明,假设这五个值构成了你为此特定问题所拥有的所有数据;换句话说,假设你不知道μ[X] = 22 和σ[X] = 1.5。你对μ[X]和σ[X]的最佳估计值分别是x̄和s*,如下所示:
R> obs.mean <- mean(obs)
R> obs.mean
[1] 21.89044
R> obs.sd <- sd(obs)
R> obs.sd
[1] 1.294806
可以通过以下方式计算估算的标准误差:
R> obs.mean.se <- obs.sd/sqrt(5)
R> obs.mean.se
[1] 0.5790549
由于n = 5 相对较小,你必须假设obs中的值是来自正态分布的实现,符合情境 2 中的要点。这使你能够使用具有 4 个自由度的t分布来处理X̄的抽样分布。然而,请回顾第 16.2.3 节,任何t分布通常都是放置在标准化尺度上的。因此,为了根据你计算的样本统计量找到样本五天的均值温度小于 21.5 的概率,你必须首先使用第 16.2.2 节中概述的规则来标准化这个值。将相应的随机变量标记为T,具体值为t[4],并将其存储为 R 中的对象t4。
R> t4 <- (21.5-obs.mean)/obs.mean.se
R> t4
[1] -0.6742706
这将兴趣值 21.5 放置在标准化的尺度上,使其可以与标准正态分布进行解释,或者在这种情况下是正确的(因为你使用的是估算值s而不是未知的σ[X]来计算标准误差),t[4]遵循上述的t分布,具有 4 个自由度。估算的概率如下所示。
R> pt(t4,df=4)
[1] 0.26855
请注意,当你计算从抽样分布中得到的“真实”理论概率 Pr(X̄ < 21.5)时,你得到了约 0.23 的结果(参见第 370 页),但是基于使用样本统计量进行标准化的相同概率(换句话说,基于估算值的真实理论值 Pr(T < t[4]))已经计算为 0.27(保留两位小数)。
图 17-1 的底部图像提供了t分布,ν = 4,标出了描述的概率。N(0,1)的密度也被绘制出来以供比较;它代表了早前情况 1 中的 N(22,1.5/
)抽样分布的标准化版本。你可以使用以下几行代码生成这张图像:
R> xvals <- seq(-5,5,length=100)
R> fx.samp.t <- dt(xvals,df=4)
R> plot(xvals,dnorm(xvals),type="l",lty=2,lwd=2,col="gray",xlim=c(-4,4),
xlab="",ylab="")
R> abline(h=0,col="gray")
R> lines(xvals,fx.samp.t,lty=3,lwd=2)
R> polygon(cbind(c(t4,-5,xvals[xvals<=t4]),c(0,0,fx.samp.t[xvals<=t4])),
density=10,lty=3)
R> legend("topright",legend=c("N(0,1) standard","t (4 df)"),
col=c("gray","black"),lty=2:3,lwd=c(2,2),bty="n")
考虑与样本均值相关的概率分布显然不是一项简单的工作。使用样本统计量决定了抽样分布的性质;特别是,如果你使用样本标准差来计算标准误差,那么它将是基于t分布的。然而,正如这里的例子所示,一旦这一点得以确认,计算各种概率就变得简单,并且遵循第 16.2 节中详细介绍的相同的一般规则和 R 语言功能。
17.1.2 样本比例的分布
样本比例的抽样分布以类似的方式进行解释。如果执行n次成功/失败事件的试验,你可以获得成功的比例估计;如果再执行另n次试验,新的估计可能会有所不同。正是这种变异性你正在研究的对象。
关注的随机变量
表示在任何n次试验中,成功的估计比例,每次试验都有一个定义的二元结果。它的估计值为
,其中x是样本大小n中成功的次数。让我们用π来表示相应的真实成功比例(通常是未知的)。
注意
请注意,这里的π并不是指常见的几何值 3.14(保留两位小数)。相反,它仅仅是一个标准符号,用来表示一个真实的总体比例。
的抽样分布近似为正态分布,均值为π,标准误差为
。以下是需要注意的关键点:
• 当n很大和/或π不接近 0 或 1 时,这个近似是有效的。
• 有一些经验法则可以用来确定这种有效性;其中一条经验法则是,当nπ和n(1 − π)都大于 5 时,可以假设正态近似是满意的。
• 当真实的π未知或未假设为某个特定值时,通常在所有之前的公式中用
代替它。
只要你认为正态分布的近似有效,这就是你需要关心的唯一概率分布。然而,值得注意的是,样本比例的抽样分布的标准误差直接依赖于比例π。在构造置信区间和进行假设检验时,这一点非常重要,这些内容将在第十八章中开始探讨。
让我们来看一个实际的例子。假设美国的一位政治评论员对她所在城市的选举年龄公民中,已经知道自己如何投票的比例感兴趣。她从 118 名合适的随机选取的个体那里获得了“是”或“否”的答案。在这些人中,有 80 人表示他们知道如何投票。为了调查与感兴趣比例相关的变异性,您需要考虑

其中
。在 R 中,以下代码可以给出感兴趣的估计值:
R> p.hat <- 80/118
R> p.hat
[1] 0.6779661
在样本中,大约 68% 的被调查者知道他们将在下次选举中如何投票。还要注意,根据上述经验法则,由于这两个值都大于 5,近似正态分布是有效的。
R> 118*p.hat
[1] 80
R> 118*(1-p.hat)
[1] 38
使用以下代码估算标准误差:
R> p.se <- sqrt(p.hat*(1-p.hat)/118)
R> p.se
[1] 0.04301439
然后,您可以使用以下代码绘制相应的抽样分布:
R> pvals <- seq(p.hat-5*p.se,p.hat+5*p.se,length=100)
R> p.samp <- dnorm(pvals,mean=p.hat,sd=p.se)
R> plot(pvals,p.samp,type="l",xlab="",ylab="",
xlim=p.hat+c(-4,4)*p.se,ylim=c(0,max(p.samp)))
R> abline(h=0,col="gray")
图 17-2 给出了结果。

图 17-2:根据方程式 (17.1)可视化投票例子的抽样分布。阴影区域表示 Pr(0.7 <
< 0.75),这是样本比例位于 0.7 和 0.75 之间的概率,且样本大小为 n = 118。
现在,您可以使用这个分布来描述其他相同样本大小的投票者中,已经知道自己如何投票的比例的变异性。
例如,图 17-2 中的阴影区域突出了另一个相同大小样本中,已知如何投票的选民比例位于 0.7 和 0.75 之间的概率。这个阴影区域可以通过以下代码添加:
R> pvals.sub <- pvals[pvals>=0.7 & pvals<=0.75]
R> p.samp.sub <- p.samp[pvals>=0.7 & pvals<=0.75]
R> polygon(cbind(c(0.7,pvals.sub,0.75),c(0,p.samp.sub,0)),
border=NA,col="gray")
并且通过第 16.2.2 节中介绍的 pnorm,您可以使用以下代码来计算感兴趣的概率:
R> pnorm(0.75,mean=p.hat,sd=p.se) - pnorm(0.7,mean=p.hat,sd=p.se)
[1] 0.257238
这个抽样分布表明,基于相同样本大小的另一个样本比例位于这两个值之间的机会大约是 25.7%。
练习 17.1
一位教师希望测试他学校所有 10 年级的学生,以评估他们的基础数学理解,但复印机在仅复印了六份试卷后就坏了。无奈之下,他随机挑选了六名学生参加测试。这些学生的成绩(以 65 分为满分)平均为 41.1。已知该测试的标准差为 11.3。
-
计算与平均测试分数相关的标准误差。
-
假设分数本身是正态分布的,评估如果教师再抽取一个相同样本大小的样本,平均分数位于 45 到 55 之间的概率。
-
一名学生如果答对的问题少于一半,将获得不及格成绩(F)。计算基于另一个相同大小的样本,平均成绩为 F 的概率。
一家营销公司想了解青少年更喜欢哪种能量饮料——饮料 A 还是饮料 B。它调查了 140 名青少年,结果显示只有 35%的青少年更喜欢饮料 A。
-
使用快速检查来判断是否可以使用正态分布来表示该比例的抽样分布。
-
在另一个相同大小的样本中,青少年更喜欢饮料 A 的比例大于 0.4 的概率是多少?
-
找到此抽样分布的两个值,确定所关注比例的中间 80%值。
在第 16.2.4 节中,汽车通过个体位置之间的时间使用指数分布进行建模。假设在城镇的另一端,她的朋友对类似的问题感兴趣。站在她家门外,她记录了 63 个汽车通过之间的时间。这些样本的平均时间为x̄ = 37.8 秒,标准差为s = 34.51 秒。
-
朋友检查了她的原始数据的直方图,发现她的原始数据严重偏右。简要识别并描述样本均值的抽样分布的性质,并计算适当的标准误差。
-
使用(g)中的标准误差和适当的概率分布,计算在另一个相同大小的样本中,样本均值介于以下区间的概率:
-
超过 40 秒
-
少于半分钟
-
在给定的样本均值和 40 秒之间
-
17.1.3 其他统计量的抽样分布
到目前为止,你已经研究了涉及单个样本均值或样本比例的抽样分布,尽管需要注意的是,许多问题需要更复杂的度量。然而,你可以将本节中探讨的思想应用于任何从有限样本中估计的统计量。关键始终是能够理解与点估计相关的变异性。
在一些情况下,像之前讨论的那样,抽样分布是参数化的,这意味着概率分布的数学形式本身是已知的,并且仅依赖于特定参数值的提供。这有时取决于满足某些条件,就像你在本章中应用正态分布时看到的那样。对于其他统计量,可能不知道适当抽样分布的形式——在这些情况下,你可以使用计算机模拟来获得所需的概率。
在本章剩余部分以及接下来的几章中,你将继续探索与常见检验和模型的参数抽样分布相关的统计量。
注意
估计量的变异性实际上只是问题的一方面。与此同样重要的是统计学偏差问题。当“自然变异”应与随机误差相关联时,偏差则与系统误差相关联,意思是说一个有偏统计量在样本量增大时并不会收敛到对应的真实参数值。偏差可能是由于研究设计或数据收集中的缺陷造成的,或者可能是由于对感兴趣统计量的估计器不良造成的。偏差是任何给定估计器和/或统计分析中的不良特性,除非它可以被量化并去除,否则在实践中通常是困难甚至不可能做到的。因此,我到目前为止只处理了无偏统计估计量,其中许多是你可能已经熟悉的(例如算术均值),并且我将继续假设无偏性。
17.2 置信区间
置信区间(CI)是由下限l和上限u定义的区间,用于描述在观察到的样本数据下,可能的真实总体参数值。置信区间的解释使你能够陈述一个“置信水平”,即感兴趣的真实参数值落在此上下限之间的置信度,通常以百分比的形式表示。因此,它是一个常见且有用的工具,直接来源于感兴趣统计量的抽样分布。
以下是需要注意的重要点:
• 置信度通常以百分比的形式表示,因此你会构造一个 100 × (1 − α)%的置信区间,其中 0 < α < 1 表示“尾部概率的大小”。
• 三个最常见的区间通过α = 0.1(90% 区间)、α = 0.05(95% 区间)或α = 0.01(99% 区间)来定义。
• 通俗地说,你可以这样表述置信区间(l,u)的解释:“我有 100 × (1 − α)%的信心,真实参数值位于l和u之间。”
置信区间可以通过不同的方式构造,这取决于统计量的类型,从而影响相应抽样分布的形态。对于对称分布的样本统计量,比如本章将使用的均值和比例,可以使用一个通用公式:

其中,statistic 是正在考察的样本统计量,critical value 是来自抽样分布标准化版本的值,表示与 α 相对应的值,standard error 是抽样分布的标准差。临界值和标准误差的乘积被称为区间的误差组件;从统计量值中减去误差组件得到 l,加上误差组件得到 u。
关于适当的抽样分布,置信区间(CI)所提供的仅是分布中的两个值,它们标出了密度下方区域中中央的 100 × (1 − α) 百分比区域。(这个过程在练习 17.1(f)中有简要提到。)然后,你使用置信区间来进一步解释关于由相关统计量估计的真实(通常是未知的)参数值。
17.2.1 均值的区间估计
从第 17.1.1 节你知道,单个样本均值的抽样分布主要取决于你是否知道原始测量的真实标准差 σ[X]。然后,假设这个样本均值的样本量大致为 n ≥ 30,根据中心极限定理(CLT),可以确保一个对称的抽样分布——如果你知道 σ[X] 的真实值,那么分布将是正态的;如果必须使用样本标准差 s 来估计 σ[X](这种情况在实践中更常见),那么它将是基于 t 分布,ν = n − 1 自由度。你已经看到,标准误差的定义是标准差除以 n 的平方根。对于小样本量 n,还必须假设原始观测值是正态分布的,因为中心极限定理不适用。
为了构造适当的区间,首先必须找到与 α 对应的临界值。根据定义,置信区间是对称的,因此这意味着围绕均值的中央概率是 (1 − α),其中在下尾和上尾分别是 α/2。
返回到第 17.1.1 节的例子,处理的是新西兰但尼丁 1 月的日最高气温(摄氏度)。假设你知道观测值是正态分布的,但你不知道真实的均值 μ[X](假定为 22)或真实的标准差 σ[X](假定为 1.5)。按照之前的方法,假设你已经做出了以下五个独立观测:
R> temp.sample <- rnorm(n=5,mean=22,sd=1.5)
R> temp.sample
[1] 20.46097 21.45658 21.06410 20.49367 24.92843
由于你对样本均值及其抽样分布感兴趣,因此必须计算样本均值 x̄、样本标准差 s 以及适当的样本均值标准误差,
。
R> temp.mean <- mean(temp.sample)
R> temp.mean
[1] 21.68075
R> temp.sd <- sd(temp.sample)
R> temp.sd
[1] 1.862456
R> temp.se <- temp.sd/sqrt(5)
R> temp.se
[1] 0.8329155
现在,假设目标是构建一个 95%的置信区间,用于真实的、未知的均值μ[X]。这意味着α = 0.05(尾部概率的总量)对于相关的抽样分布。考虑到你知道原始观测值是正态分布的,并且你使用的是s(而不是σ[X]),适当的分布是t分布,具有n − 1 = 4 的自由度。对于该曲线下的 0.95 的中心区域,α/2 = 0.025 必须位于任一尾部。由于 R 的q函数基于总的下尾区域操作,因此(正的)临界值是通过为适当的函数提供 1 − α/2 = 0.975 的概率来找到的。
R> 1-0.05/2
[1] 0.975
R> critval <- qt(0.975,df=4)
R> critval
[1] 2.776445
图 17-3 展示了为什么qt函数以这种方式使用(由于我在第十六章中使用了类似的代码,因此这里没有重复图 17-3 的代码)。

图 17-3:说明临界值在样本均值置信区间中的作用,使用邓尼丁温度示例。抽样分布为t分布,具有 4 个自由度,使用qt函数与α/2 = 0.025相关的对称尾部概率,得到一个 0.95 的中心区域。
请注意,当以相同临界值的负版本(即“绕均值反射”,通过使用qt(0.025,4)得到)来看时,曲线下的中心对称区域必须是 0.95。你可以通过使用pt来确认这一点。
R> pt(critval,4)-pt(-critval,4)
[1] 0.95
所以,所有的要素都已具备。你可以通过方程(17.2)和以下几行代码计算真实均值μ[X]的 95%置信区间,分别得到l和u:
R> temp.mean-critval*temp.se
[1] 19.36821
R> temp.mean+critval*temp.se
[1] 23.99329
由(19.37,23.99)给出的置信区间(CI)可以解释为:你有 95%的信心,邓尼丁(Dunedin)1 月的真实最大气温位于 19.37 到 23.99 摄氏度之间。
通过这个结果,你将均值估计的知识与样本的固有变异性结合起来,定义了一个区间,其中你可以相当有把握地认为真实均值会落在该区间内。如你所知,这里的真实均值是 22,确实包含在计算出的置信区间内。
由此,你可以轻松地改变置信区间的置信水平。只需更改临界值,正如往常一样,必须在每个尾部定义α/2。例如,可以通过以下两行代码分别计算该示例值的 80%置信区间(α = 0.2)和 99%置信区间(α = 0.01):
R> temp.mean+c(-1,1)*qt(p=0.9,df=4)*temp.se
[1] 20.40372 22.95778
R> temp.mean+c(-1,1)*qt(p=0.995,df=4)*temp.se
[1] 17.84593 25.51557
请注意,这里通过乘以向量c(-1,1)来计算,以便一次性获得上下限,并将结果返回为一个长度为 2 的向量。如同往常一样,qt函数在计算时采用完全的下尾区域,因此p设置为 1 − α/2。
这些最新的区间突出了提高置信水平对于给定置信区间的自然结果。中央区域的更高概率直接转化为更极端的临界值,导致区间更宽。这是有道理的——为了“更有信心”地估计真实的参数值,你需要考虑更大范围的可能值。
17.2.2 一个比例的区间
为样本比例建立置信区间遵循与均值相同的规则。根据第 17.1.2 节中的抽样分布知识,你可以从标准正态分布中获得临界值,对于来自样本大小为n的估计
,区间本身是通过标准误差
构建的。
让我们回到第 17.1.2 节中的例子,在该例中,118 名被调查者中有 80 人表示他们知道如何在下一次美国总统选举中投票。请回忆一下你有以下信息:
R> p.hat <- 80/118
R> p.hat
[1] 0.6779661
R> p.se <- sqrt(p.hat*(1-p.hat)/118)
R> p.se
[1] 0.04301439
构建 90%的置信区间(α = 0.1)时,来自标准化抽样分布的适当临界值如下,这意味着 Pr(−1.644854 < Z < 1.644854) = 0.9,适用于Z ∼ N(0,1):
R> qnorm(0.95)
[1] 1.644854
现在你再次跟随方程(17.2):
R> p.hat+c(-1,1)*qnorm(0.95)*p.se
[1] 0.6072137 0.7487185
你可以得出结论,你有 90%的信心认为,在下一次选举中知道自己将如何投票的选民的真实比例位于 0.61 和 0.75 之间(四舍五入到小数点后两位)。
17.2.3 其他区间
在第 17.2.1 节和 17.2.2 节中呈现的两个简单情境突出了将任何点估计(换句话说,样本统计量)与其变异性联系起来的重要性。当然,置信区间也可以为其他量构建,在接下来的章节中(作为假设检验的一部分),我将扩展对置信区间的讨论,调查两个均值、两个比例之间的差异,以及类别计数的比率。这些更复杂的统计量有自己的标准误差公式,尽管相应的抽样分布仍然是通过正态曲线和t曲线对称的(如果再次满足一些标准假设),这意味着现在熟悉的方程(17.2)公式仍然适用。
通常,置信区间旨在从感兴趣的抽样分布中标出 1 − α的中央区域,包括不对称的抽样分布。然而,在这些情况下,基于单一的标准化临界值构造对称 CI(如方程(17.2)所示)是没有太大意义的。同样,您可能不知道抽样分布的函数形式,因此不愿做出任何分布假设,比如对称性。在这些情况下,您可以基于假定的不对称抽样分布的原始分位数(或估算的原始分位数;参见第 13.2.3 节)采取另一条路径。使用特定的分位数值来标出相同的α/2 上尾和下尾区域是一种有效方法,能够保持对感兴趣的抽样分布形状的敏感性,同时仍然允许您构造一个有用的区间,描述潜在的真实参数值。
17.2.4 对置信区间解释的评论
关于任何置信区间(CI)解释的典型表述,通常会提到对真实参数值所在范围的置信度,但更正式的解释应考虑并澄清构造的概率性质。从技术上讲,给定 100(1 − α)的置信度水平,更准确的解释如下:在多次从同一总体中抽取相同大小的样本,并且在每个样本上构造相同置信度水平的 CI 时,您可以期望真实的相应参数值会落在 100(1 − α)百分比的这些区间的范围内。
这源于抽样分布的理论,它描述了多个样本之间的变异性,而不仅仅是已经取样的单个样本。乍一看,可能难以完全理解这与口语中常用的“置信声明”之间的差异,但重要的是要始终保持对技术上正确定义的意识,特别是考虑到 CI 通常是基于单个样本来估算的。
练习 17.2
一名业余跑者记录了他跑完 100 米所需的平均时间。他在相同条件下完成了 34 次冲刺,并发现这些数据的平均值为 14.22 秒。假设他知道他跑步的标准差为 σ[X] = 2.9 秒。
-
构造并解释一个 90%的置信区间,用于估计真实的平均时间。
-
重复(a),但这次假设标准差未知,且从样本中估算出 s = 2.9。这样做会如何影响区间?
在某个国家,真实的左撇子或双手灵活公民的比例未知。随机抽取了 400 人,每人被要求选择以下三种选项之一:仅右手、仅左手或双手灵活。结果显示,37 人选择了左撇子,11 人选择了双手灵活。
-
计算并解释 99%的置信区间(CI)以估算仅左撇子公民的真实比例。
-
计算并解释 99%的置信区间(CI)以估算既是左撇子或双手灵活的公民的真实比例。
在第 17.2.4 节中,CI 的技术解释是指在计算相同样本大小、来自相同总体的多个相似区间时,包含感兴趣参数真实值的区间比例。
-
你的任务是编写一个示例,使用模拟演示置信区间的这一行为。为此,请按照以下步骤进行操作:
– 设置一个矩阵(见第三章),填充
NA(见第六章),该矩阵具有 5,000 行和 3 列。– 使用第十章中的技巧编写一个
for循环,在每次 5,000 次迭代中,从具有速率参数λ[e] = 0.1 的指数分布中生成大小为 300 的随机样本(见第 16.2.4 节)。– 评估每个样本的样本均值和样本标准差,并使用这些量与适当抽样分布的临界值一起计算分布的真实均值的 95%置信区间。
– 在
for循环中,矩阵现在应按行填充你的结果。第一列将包含下限,第二列将包含上限,第三列将包含逻辑值,如果对应的区间包含真实均值 1/λ[e],则为TRUE,否则为FALSE。– 当循环完成时,计算填充矩阵第三列中
TRUE的比例。你应该发现这个比例接近 0.95;每次重新运行循环时,这个比例会有所不同。 -
创建一个绘图,将你估计的前 100 个置信区间绘制为从l到u的单独水平线,每条线叠加在另一条上。做到这一点的一种方法是首先创建一个具有预设的x和y轴限制(后者为
c(1,100))的空图,然后使用lines逐步添加每一条线,使用适当的坐标(这可以通过另一个for循环来完成)。最后,为图形添加一条红色的垂直线,表示真实均值。那些不包含真实均值的置信区间将不会与该垂直线相交。以下是该图的示例:
![image]()
第十八章:18
假设检验

在本章中,你将基于你在置信区间和抽样分布方面的经验,进一步正式阐述一个真实未知参数的值。为此,你将学习频率派的假设检验,通过使用来自相关抽样分布的概率作为反对某个关于真实值的主张的证据。当以这种方式使用概率时,它被称为p-值。在本章中,我讲解了如何解释相对基础的统计结果,但你可以将相同的概念应用于更复杂方法(如回归建模,第十九章)中的统计数据。
18.1 假设检验的组成部分
举个假设检验的例子,假设我告诉你某个特定人群中有 7%的人对花生过敏。然后,你从该人群中随机选择了 20 个人,发现其中 18 人对花生过敏。如果假设你的样本没有偏差,且真正反映了整个群体,那么你会如何看待我声称过敏人群比例为 7%的说法?
自然地,你会怀疑我的说法的正确性。换句话说,如果假设成功率为 0.07,那么从 20 次试验中观察到 18 次或更多成功的概率非常小,足以说明你有统计证据反对我的说法,即真实的过敏率为 0.07。事实上,当将X定义为 20 个样本中过敏个体的数量,并假设X ∼ BIN(20,0.07),通过评估 Pr(X ≥ 18),你将得到一个非常小的p-值。
R> dbinom(18,size=20,prob=0.07) + dbinom(19,size=20,prob=0.07) +
dbinom(20,size=20,prob=0.07)
[1] 2.69727e-19
这个p-值代表的是在你的样本中观察到结果的概率,X = 18,或更极端的结果(X = 19 或 X = 20),如果成功的机会真的为 7%。
在查看具体的假设检验及其在 R 中的实现之前,本节将介绍一些术语,这些术语是你在报告此类检验时经常会遇到的。
18.1.1 假设
正如其名称所示,在假设检验中,正式陈述一个主张以及随后的假设检验是通过零假设和备择假设来完成的。零假设被解释为基线或无变化的假设,是被假定为真的主张。备择假设是你要检验的假设,它与零假设相对立。
通常,零假设和备择假设分别用 H[0]和 H[A]表示,它们的写法如下:
H[0] : . . .
H[A] : . . .
原假设通常(但并不总是)被定义为等式 =,与零值相等。相反,备择假设(即你正在检验的情况)通常被定义为与零值的不等式。
• 当 H[A]以小于符号<定义时,它是单尾的;这也被称为左尾检验。
• 当 H[A]通过“大于”声明(>)定义时,这是单尾的;这也称为上尾检验。
• 当 H[A]仅通过“不等于”声明(≠)定义时,这是双尾的;这也称为双尾检验。
这些测试变体完全是特定于情况的,取决于当前面临的问题。
18.1.2 测试统计量
一旦假设形成,样本数据被收集,并根据假设中详细描述的参数计算统计量。测试统计量是与适当的标准化采样分布进行比较的统计量,用以得出p-值。
测试统计量通常是所关注的样本统计量的标准化或重新缩放版本。测试统计量的分布和极端性(即与零的距离)是决定p-值小的唯一因素(p-值表示反对原假设的证据强度——详见第 18.1.3 节)。具体来说,测试统计量由原始样本统计量与原假设值之间的差异以及样本统计量的标准误差共同决定。
18.1.3 p 值
p-值是用于量化证据量的概率值,用来衡量是否有证据反对原假设。如果更正式地说,p-值是指在假设原假设为真的情况下,观察到测试统计量或更极端情况的概率。
计算p-值的确切方式取决于所测试的统计量类型和 H[A]的性质。关于这一点,你将看到以下术语:
• 下尾检验意味着p-值是来自所关注采样分布的左尾概率。
• 对于上尾检验,p-值是右尾概率。
• 对于双尾检验,p-值是左尾概率和右尾概率的总和。当采样分布是对称的(例如,正态分布或t分布,如第 18.2 节和第 18.3 节中的所有示例所示),这等同于两倍于其中一条尾部的面积。
简单来说,测试统计量越极端,p-值越小。p-值越小,反对原假设 H[0]的统计证据越强。
18.1.4 显著性水平
对于每个假设检验,假定一个显著性水平,记作α。这个值用于判定测试结果的显著性。显著性水平定义了一个临界点,用来决定是否有足够的证据认为 H[0]是错误的,并支持 H[A]。
• 如果p-值大于或等于α,那么你得出结论,无法反对原假设,因此你在与 H[A]比较时保留H[0]。
• 如果 p-值小于 α,那么测试结果是 统计显著的。这意味着反对原假设的证据充分,因此你 拒绝 H[0],支持 H[A]。
常见或传统的 α 值包括 α = 0.1,α = 0.05,和 α = 0.01。
18.1.5 假设检验的批评
当你查看接下来的章节中的一些例子时,上述术语会变得更容易理解。然而,即使在这一早期阶段,认识到假设检验是容易受到合理批评的也很重要。任何假设检验的最终结果要么是保留原假设,要么是拒绝原假设,而这一决定完全依赖于一个相当随意的显著性水平 α 的选择;通常,这个值就是传统使用的某个数值。
在你开始查看例子之前,同样重要的是要注意,p-值从来不能提供 H[0] 或 H[A] 真的正确的“证据”。它只能量化反对原假设的证据,当 p-值小于 α 时,可以拒绝原假设。换句话说,拒绝原假设并不等于证明它是错误的。拒绝 H[0] 仅仅意味着样本数据表明应该更倾向于 H[A],而 p-值仅表示这种倾向的强度。
近年来,由于在某些应用研究领域过度使用甚至误用 p-值,部分入门统计学课程开始反对过度强调统计推断的这些方面。Sterne 和 Smith(2001)的一篇特别好的文章从医学研究的角度讨论了假设检验的作用及其相关问题。另一个不错的参考文献是 Reinhart(2015),它讨论了统计学中对 p-值的常见误解。
尽管如此,关于抽样分布的概率推断是,且永远将是,频率统计实践的基石。提高统计测试和建模使用及解释的最佳方式是通过对相关思想和方法的合理介绍,这样从一开始你就能理解统计显著性及其能告诉你和不能告诉你的内容。
18.2 测试方法
涉及样本均值的假设检验的有效性依赖于第 17.1.1 节中提到的相同假设和条件。特别是,在本节中,你应该假设中心极限定理成立,并且如果样本量较小(换句话说,大约小于 30),原始数据是正态分布的。你还将重点关注使用样本标准差s来估计真实标准差σ[X]的例子,因为这是你在实践中最常遇到的情况。同样,参照第 17.1.1 节,这意味着在计算临界值和p-值时,你需要使用t-分布而不是正态分布。
18.2.1 单一均值
由于你已经接触过标准误差公式,
,以及需要使用的 R 功能来从t-分布中获得分位数和概率(qt和pt),这里唯一需要引入的新概念是关于假设本身的定义以及结果的解释。
计算:单样本 t 检验
让我们直接进入一个例子——单样本t-检验。回想一下在第 16.2.2 节中的问题,其中某零食生产商对广告中 80 克包装的内容物净重均值感兴趣。假设有消费者打电话投诉——他们在一段时间内购买并精确称量了 44 个随机选择的 80 克包装(来自不同商店),并记录了以下重量:
R> snacks <- c(87.7,80.01,77.28,78.76,81.52,74.2,80.71,79.5,77.87,81.94,80.7,
82.32,75.78,80.19,83.91,79.4,77.52,77.62,81.4,74.89,82.95,
73.59,77.92,77.18,79.83,81.23,79.28,78.44,79.01,80.47,76.23,
78.89,77.14,69.94,78.54,79.7,82.45,77.29,75.52,77.21,75.99,
81.94,80.41,77.7)
客户声称他们被少给了,因为他们的数据不能来自均值为μ = 80 的分布,因此真实的均值必须小于 80。为了调查这一主张,制造商使用显著性水平α = 0.05 进行假设检验。
首先,必须定义假设,零假设为 80 克。记住,备择假设是“你所要检验的内容”;在本例中,H[A]是μ小于 80。零假设,解释为“没有变化”,将定义为μ = 80:即真实的均值实际上是 80 克。这些假设形式化如下:

其次,必须从样本中估计均值和标准差。
R> n <- length(snacks)
R> snack.mean <- mean(snacks)
R> snack.mean
[1] 78.91068
R> snack.sd <- sd(snacks)
R> snack.sd
[1] 3.056023
你的假设所要回答的问题是:在估计的标准差下,如果真实均值为 80 克,那么观察到样本均值(当n = 44)为 78.91 克或更低的概率是多少?为了回答这个问题,你需要计算相关的检验统计量。
在假设检验中,对于单一均值相对于零假设值μ[0]的检验统计量T正式表示为:

基于大小为 n 的样本,样本均值为 x̄,样本标准差为 s(分母是均值的估计标准误)。假设相关条件已满足,T 服从自由度为 ν = n − 1 的 t 分布。
在 R 中,以下代码可以为零食数据提供样本均值的标准误:
R> snack.se <- snack.sd/sqrt(n)
R> snack.se
[1] 0.4607128
然后,T 可以按以下方式计算:
R> snack.T <- (snack.mean-80)/snack.se
R> snack.T
[1] -2.364419
最后,检验统计量用于获得 p-值。回想一下,p-值是观察到 T 或更极端值的概率。 “更极端”的性质由备择假设 H[A] 决定,作为一个小于的声明,它引导你找到一个左尾的、下侧概率作为 p-值。换句话说,p-值是作为样本分布下方的面积(当前例中是自由度为 43 的 t 分布)在 T 的垂直线左侧的面积。从第 16.2.3 节可以轻松完成此操作,如下所示:
R> pt(snack.T,df=n-1)
[1] 0.01132175
你的结果表明,如果零假设 H[0] 为真,那么观察到客户样本均值 x̄ = 78.91 或更小的概率仅有 1% 多一点。由于这个 p-值小于预定义的显著性水平 α = 0.05,制造商得出结论,足够的证据可以拒绝零假设,支持备择假设,表明真实的 μ 值实际上小于 80 克。
注意,如果你找到单样本均值的相应 95% CI,如第 17.2.1 节所描述并给出:
R> snack.mean+c(-1,1)*qt(0.975,n-1)*snack.se
[1] 77.98157 79.83980
它不包括零假设值 80,这与在 0.05 水平下的假设检验结果一致。
R 函数:t.test
单样本的 t-检验结果也可以通过内置的t.test函数找到。
R> t.test(x=snacks,mu=80,alternative="less")
One Sample t-test
data: snacks
t = -2.3644, df = 43, p-value = 0.01132
alternative hypothesis: true mean is less than 80
95 percent confidence interval:
-Inf 79.68517
sample estimates:
mean of x
78.91068
该函数将原始数据向量作为 x,均值的零假设值作为 mu,以及检验的方向(即如何在适当的 t 曲线下找到 p-值)作为 alternative。alternative 参数有三种可用选项:“less” 表示 H[A] 小于;“greater” 表示 H[A] 大于;“two.sided” 表示 H[A] 不等于。α 的默认值为 0.05。如果你想要不同于 0.05 的显著性水平,必须将 1 − α 传递给 t.test 的 conf.level 参数。
注意,T 的值会在 t.test 的输出中报告,同时还包括自由度和 p-值。你还会得到一个 95% 的“区间”,但其值 -Inf 和 79.68517 与之前手动计算的区间不匹配。手动计算的区间实际上是一个双侧区间——一个使用等量误差组件的有界区间。
t.test 输出中的置信区间,则根据alternative参数进行设置。它提供了单侧置信界限。对于下尾检验,它提供了统计量的上界,使得兴趣的采样分布的整个下尾区域为 0.95,而传统的双侧区间则是中心区域。单侧界限的使用频率低于完全界定的双侧区间,该区间可以通过相关调用t.test并设置alternative="two.sided"来获得(作为组件conf.int)。
R> t.test(x=snacks,mu=80,alternative="two.sided")$conf.int
[1] 77.98157 79.83980
attr(,"conf.level")
[1] 0.95
这个结果与之前手动计算的版本一致。还要注意,相应的置信水平 1 − α作为一个属性与该组件一起存储(参见第 6.2.1 节)。
在检查零食例子的结果时,p-值约为 0.011,请记住在解释假设检验时要小心。对于此特定检验,当α设置为 0.05 时,H[0]被拒绝。但如果检验是在α = 0.01 下进行的呢?p-值大于 0.01,所以在那种情况下,H[0]将被保留,仅仅因为α值的任意变化。在这些情况下,帮助对反对原假设的证据的强度进行评论会很有帮助。对于当前的例子,你可以合理地认为存在某些证据支持 H[A],但这些证据并不特别强。
练习 18.1
-
某些品种的成年家猫据说平均体重大约为 3.5 千克。一位猫迷对此表示不同意见,并收集了 73 只该品种猫的体重样本。根据她的样本,她计算出平均体重为 3.97 千克,标准差为 2.21 千克。进行假设检验,以验证她关于真实平均体重μ不等于 3.5 千克的主张,设置适当的假设,进行分析,并解释p-值(假设显著性水平为α = 0.05)。
-
假设之前认为斐济海岸地震事件的平均震级为 4.3 里氏震级。使用现成的
quakes数据集中mag变量的数据,该数据集提供了 1000 个该地区的地震事件样本,来检验真实震级是否确实大于4.3。设置适当的假设,使用t.test(在显著性水平α = 0.01 下进行检验),并得出结论。 -
手动计算(b)部分真实均值的双侧置信区间。
18.2.2 两个均值
通常,单一样本均值的检验不足以回答你关心的问题。在许多场景中,研究者希望直接比较两个不同组别的均值,这归结为对两个均值之间真实差异的假设检验;称它们为μ[1]和μ[2]。
两组数据之间的关系影响两个样本均值差异的标准误差的具体形式,因此也影响检验统计量本身。然而,实际的均值比较通常具有相同的性质——典型的原假设通常定义为μ[1]和μ[2]相等。换句话说,两个均值之间差异的原假设值通常为零。
非配对/独立样本:非合并方差
最一般的情况是,两个数据集基于两个独立的、分开的组(也称为非配对样本)。你计算两个数据集的样本均值和样本标准差,定义感兴趣的假设,然后计算检验统计量。
当你不能假设两个总体的方差相等时,就需要进行非合并版本的两样本t检验;这一部分将首先讨论。如果你能够安全地假设方差相等,那么你可以进行合并的两样本t检验,这样可以提高结果的精确度。稍后你将会看到合并版本的检验。
对于非合并的例子,回到第 18.2.1 节中的 80 克零食包装示例。收集了原制造商 44 包零食样本(将此样本大小标记为n[1]),然后不满的消费者去收集了竞争对手零食制造商的n[2] = 31 包随机选择的 80 克零食包装。第二组测量数据存储为snacks2。
R> snacks2 <- c(80.22,79.73,81.1,78.76,82.03,81.66,80.97,81.32,80.12,78.98,
79.21,81.48,79.86,81.06,77.96,80.73,80.34,80.01,81.82,79.3,
79.08,79.47,78.98,80.87,82.24,77.22,80.03,79.2,80.95,79.17,81)
从第 18.2.1 节开始,你已经知道了第一个样本(大小为n[1] = 44)的均值和标准差——这些分别存储为snack.mean(大约 78.91)和snack.sd(大约 3.06)——可以把它们看作是x̄[1]和s[1]。现在计算新数据的相同量值,分别是x̄[2]和s[2]。
R> snack2.mean <- mean(snacks2)
R> snack2.mean
[1] 80.1571
R> snack2.sd <- sd(snacks2)
R> snack2.sd
[1] 1.213695
设原始样本的真实均值用μ[1]表示,竞争对手公司包装的新样本的真实均值用μ[2]表示。现在,你关注的是测试是否有统计证据支持μ[2]大于μ[1]的说法。这意味着假设为 H[0] : μ[1] = μ[2] 和 H[A] : μ[1] < μ[2],可以写成如下形式:
H[0] : μ[2] − μ[1] = 0
H[A] : μ[2] − μ[1] > 0
也就是说,竞争对手公司包装的真实均值与原制造商包装的均值之间的差异,当从竞争对手的均值中减去原制造商的均值时,结果大于零。“无变化”场景,即原假设,是两个均值相同,所以它们的差异为零。
现在你已经构建了假设,接下来我们来看如何实际检验它们。两个均值的差异是我们关注的量。对于来自具有真实均值μ[1]和μ[2]的总体的两个独立样本,样本均值x̄[1]和x̄[2],样本标准差s[1]和s[2](并且满足t分布的有效性条件),检验μ[2]和μ[1]之间差异的标准化检验统计量T*,按顺序给出如下:

其分布可以近似为具有ν自由度的t分布,其中

在(18.3)中,μ[0]是感兴趣的零假设值——通常在“差异”统计量的检验中为零。因此,这一项将在检验统计量的分子中消失。T的分母是这种设置下两个均值差异的标准误。
方程(18.4)右侧的└ · ┘表示一个floor操作——严格向下舍入到最接近的整数。
注意
此两样本t-检验,使用方程(18.3)进行,也称为Welch 的t-检验。这指的是使用方程(18.4),称为Welch-Satterthwaite 方程。至关重要的是,它假设两个样本具有不同的真实方差,这就是为什么它被称为非合并方差*版本的检验。
在定义两个参数集并构建假设时,一致性非常重要。在这个例子中,由于检验的目的是寻找μ[2]大于μ[1]的证据,因此μ[2]−μ[1] > 0 形成了 H[A](一个大于的、右尾检验),这种减法顺序在计算T时被镜像反转。如果你将差异定义为相反的顺序,同样的检验也可以进行。在这种情况下,你的备择假设将暗示一个左尾检验,因为如果你检验的是μ[2]大于μ[1],那么 H[A]应当正确写作μ[1]−μ[2] < 0。同样,这将相应地修改方程(18.3)中分子减法的顺序。
同样的注意事项也适用于使用t.test进行两样本比较。必须将两个样本作为参数x和y传递,但该函数在进行右尾检验时将x解释为大于y,在进行左尾检验时将x解释为小于y。因此,在对零食包装的例子进行alternative="greater"检验时,必须将snacks2传递给x:
R> t.test(x=snacks2,y=snacks,alternative="greater",conf.level=0.9)
Welch Two Sample t-test
data: snacks2 and snacks
t = 2.4455, df = 60.091, p-value = 0.008706
alternative hypothesis: true difference in means is greater than 0
90 percent confidence interval:
0.5859714 Inf
sample estimates:
mean of x mean of y
80.15710 78.91068
由于p-值为 0.008706,你可以得出结论,存在足够的证据拒绝零假设 H[0],支持备择假设 H[A](事实上,p-值显然小于规定的α = 0.1 显著性水平,这是由conf.level=0.9所隐含的)。证据表明,竞争对手制造商 80 克包装的零食的平均净重大于原制造商的平均净重。
请注意,t.test的输出报告了一个自由度值 60.091,这是公式(18.4)的未下取整结果。你还会收到一个单边置信区间(基于上述置信水平),这是由于本检验的单边性质所触发的。再次提醒,常见的双边 90%置信区间也是有用的;知道ν = └ 60.091 ┘ = 60,并使用检验统计量和标准误差(分别为公式(18.3)的分子和分母),你可以进行计算。
R> (snack2.mean-snack.mean) +
c(-1,1)*qt(0.95,df=60)*sqrt(snack.sd²/44+snack2.sd²/31)
[1] 0.3949179 2.0979120
在这里,你使用了之前存储的样本统计量snack.mean、snack.sd(原制造商样本的 44 个原始测量值的均值和标准差),snack2.mean和snack2.sd(与竞争对手制造商的 31 个观测值相对应的均值和标准差)。请注意,置信区间与公式(17.2)在第 378 页中详细说明的形式相同,并且为了提供正确的 1 − α中心区域,适当的t分布的q函数需要以 1 − α/2 作为其输入的概率值。你可以理解为“90%的置信度认为,竞争对手和原制造商之间的真实平均净重差异(按此顺序)介于 0.395 克和 2.098 克之间。”由于零不在区间内,并且区间完全为正,这支持了假设检验的结论。
独立/无配对样本:合并方差
在刚才提到的未合并方差示例中,并没有假设正在比较的两个总体的方差相等。这是一个重要的说明,因为它导致在计算检验统计量时使用了公式(18.3),并在相应的t分布中使用了公式(18.4)来计算自由度。然而,如果你可以假设方差相等,检验的精度将得到提高——你需要使用不同的标准误差公式来计算差异,并计算相关的自由度(df)。
同样,感兴趣的量是两个均值的差,表示为μ[2] − μ[1]。假设你有两个独立的样本,样本量分别为n[1]和n[2],来自具有真实均值μ[1]和μ[2]的总体,样本均值x̄[1]和x̄[2],以及样本标准差s[1]和s[2],并假设满足t-分布的有效性条件。此外,假设样本的真实方差
和
相等,即
。
注意
有一个简单的经验法则可以检查“方差相等”假设的有效性。如果较大样本标准差与较小样本标准差的比值小于 2,那么可以假设方差相等。例如,如果 s[1] > s[2],那么如果
< 2, 你可以使用合并方差的检验统计量。
该场景下的标准化检验统计量T为:

其分布为t-分布,ν = n[1] + n[2] − 2 自由度,其中:

是所有原始测量值方差的合并估计。这个值替代了公式(18.3)中分母的s[1]和s[2],从而得出了公式(18.5)。
两样本t-检验的其他所有方面与之前相同,包括适当假设的构建、μ[0]的典型零值以及p-值的计算和解释。
对于小吃包示例中的两个均值比较,你会发现很难为使用合并版本的t-检验辩护。根据经验法则,两个估算的标准差(s[1] ≊ 3.06 和 s[2] ≊ 1.21,分别代表原始和竞争厂家样本)具有一个大到小的比率,超过了 2。
R> snack.sd/snack2.sd
[1] 2.51795
尽管这相当非正式,如果无法合理假设这一点,最好坚持使用未合并版本的检验。
为了说明这一点,让我们考虑一个新例子。智商(IQ)是常用来衡量一个人聪明程度的量。IQ 分数合理地假设为正态分布,并且人群的平均智商被认为是 100。假设你有兴趣评估男性和女性之间的平均 IQ 分数是否存在差异,提出以下假设,其中n[男] = 12 和n[女] = 20:
H[0] : μ[男] − μ[女] = 0
H[A] : μ[男] − μ[女] ≠ 0
你随机抽取了以下数据:
R> men <- c(102,87,101,96,107,101,91,85,108,67,85,82)
R> women <- c(73,81,111,109,143,95,92,120,93,89,119,79,90,126,62,92,77,106,
105,111)
和往常一样,我们来计算所需的基本统计量。
R> mean(men)
[1] 92.66667
R> sd(men)
[1] 12.0705
R> mean(women)
[1] 98.65
R> sd(women)
[1] 19.94802
这些给出了样本均值x̄[男]和x̄[女],以及它们各自的样本标准差s[男]和s[女]。输入以下内容快速检查标准差的比率:
R> sd(women)/sd(men)
[1] 1.652626
你可以看到,较大样本标准差与较小样本标准差的比值小于 2,因此你可以假设在进行假设检验时,方差是相等的。
t.test命令还可以执行合并的两样本 t-检验,按照公式(18.5)和(18.6)。要执行该检验,你需要提供可选参数var.equal=TRUE(与默认的var.equal=FALSE相对,后者会触发 Welch 的t-检验)。
R> t.test(x=men,y=women,alternative="two.sided",conf.level=0.95,var.equal=TRUE)
Two Sample t-test
data: men and women
t = -0.9376, df = 30, p-value = 0.3559
alternative hypothesis: true difference in means is not equal to 0
95 percent confidence interval:
-19.016393 7.049727
sample estimates:
mean of x mean of y
92.66667 98.65000
还要注意,对于这个例子,H[A]意味着一个双尾检验,因此提供了alternative="two.sided"。
该检验的结果p-值为 0.3559,显然大于常规的 0.05 截止水平。因此,你的结论是没有足够的证据拒绝 H[0]——即没有足够的证据支持男性与女性在平均 IQ 得分上存在显著差异。
配对/相关样本
最后,我们将看看如何比较配对数据中的两个均值。这个设置与两独立样本t-检验明显不同,因为它关注的是数据的收集方式。问题在于两组观测之间的依赖性——之前,每组中的测量值被定义为独立的。这个概念对于如何执行检验有着重要的影响。
配对数据出现于形成两组观测数据的测量值是在同一个个体身上记录的,或者它们在某种重要或明显的方式上相关。一个经典的例子是“前后”观测,比如在某种干预治疗前后对每个个体进行的两次测量。这些情况依然关注每组中均值之间的差异,但与其分别处理两个数据集,配对t-检验则是通过单一均值来处理——即个体配对差异的真实均值 μ[d]。
作为一个例子,考虑一家关注于一种药物的效果的公司,该药物旨在降低静息心率(以每分钟跳动次数(bpm)表示)。测量了 16 个个体的静息心率。然后,这些个体接受了一疗程的治疗,随后再次测量他们的静息心率。数据存储在两个向量rate.before和rate.after中,具体如下:
R> rate.before <- c(52,66,89,87,89,72,66,65,49,62,70,52,75,63,65,61)
R> rate.after <- c(51,66,71,73,70,68,60,51,40,57,65,53,64,56,60,59)
很快就能清楚地看到,为什么任何比较这两组的检验都必须考虑依赖性。心率受个体年龄、体型和身体健康状况的影响。一个 60 岁以上的身体不太健康的人,静息心率基线可能高于一个健康的 20 岁年轻人,即使两人都服用了相同的药物来降低心率,他们的最终心率仍可能反映出各自的基线。如果使用任何独立样本t-检验来分析,这种药物的真实效果很可能被掩盖。
为了克服这个问题,配对的两样本 t 检验考虑每对值之间的差异。将一组 n 测量值标记为 x[1],...,x[n],另一组 n 观测值标记为 y[1],...,y[n],则差异 d 定义为 d[i] = y[i] − x[i];i = 1,...,n。在 R 中,您可以轻松计算成对差异:
R> rate.d <- rate.after-rate.before
R> rate.d
[1] -1 0 -18 -14 -19 -4 -6 -14 -9 -5 -5 1 -11 -7 -5 -2
以下代码计算这些差异的样本均值
和标准差 s[d]:
R> rate.dbar <- mean(rate.d)
R> rate.dbar
[1] -7.4375
R> rate.sd <- sd(rate.d)
R> rate.sd
[1] 6.196437
您希望查看心率减少的程度,因此此检验将关注以下假设:
H[0] : μ[d] = 0
H[A] : μ[d] < 0
给定用来获得差异的顺序或减法,心率成功降低的检测将表现为“之后”的均值小于“之前”的均值。
将所有这些用数学语言表达,感兴趣的值是两个依赖测量对之间的真实均值差异,μ[d]。这里有两组* n * 测量值,x[1],...,x[n] 和 y[1],...,y[n],它们的成对差异是 d[1],...,d[n]。必须满足t分布有效性的相关条件;在这种情况下,如果成对数 n 小于 30,则必须假设原始数据是正态分布的。检验统计量 T 由以下公式给出:

其中
是成对差异的均值,s[d] 是成对差异的样本标准差,而 μ[0] 是原假设值(通常为零)。统计量 T 遵循 t 分布,具有 n − 1 自由度。
方程式 (18.7) 的形式实际上与 (18.2) 中的检验统计量的形式相同,一旦计算了个别配对差异的样本统计量。进一步需要注意的是,n 代表的是配对的总数,而不是单个观测值的总数。
对于当前的假设检验,可以通过 rate.dbar 和 rate.sd 计算检验统计量和 p 值。
R> rate.T <- rate.dbar/(rate.sd/sqrt(16))
R> rate.T
[1] -4.801146
R> pt(rate.T,df=15)
[1] 0.000116681
这些结果表明有证据拒绝 H[0]。在 t.test 中,必须将可选的逻辑参数 paired 设置为 TRUE。
R> t.test(x=rate.after,y=rate.before,alternative="less",conf.level=0.95,
paired=TRUE)
Paired t-test
data: rate.after and rate.before
t = -4.8011, df = 15, p-value = 0.0001167
alternative hypothesis: true difference in means is less than 0
95 percent confidence interval:
-Inf -4.721833
sample estimates:
mean of the differences
-7.4375
请注意,您提供给 x 和 y 参数的数据向量的顺序遵循与非配对检验相同的规则,具体取决于所需的 alternative 值。通过使用 t.test 确认与手动计算的相同的 p 值,并且由于此值小于假设的常规显著性水平 α = 0.05,因此有效的结论是,您可以声明有统计证据表明药物确实减少了均值静息心率。你还可以进一步说你有 95%的信心,药物治疗后,真实的心率均值差异位于以下区间之间:
R> rate.dbar-qt(0.975,df=15)*(rate.sd/sqrt(16))
[1] -10.73935
和
R> rate.dbar+qt(0.975,df=15)*(rate.sd/sqrt(16))
[1] -4.135652
注意
在某些情况下,例如当你的数据强烈表明不符合正态分布时,你可能不愿意假设中央极限定理的有效性(请参考 第 17.1.1 节)。对于此处讨论的检验方法的替代方法,可以采用 非参数 技术,放宽这些分布要求。在双样本情况下,可以使用 Mann-Whitney U 检验(也称为Wilcoxon 秩和检验)。这是一个比较两个中位数的假设检验,而不是两个均值。你可以使用 R 函数 wilcox.test 来访问此方法,其帮助页面提供了有用的评论和该方法的详细参考。
练习 18.2
在 MASS 包中,你会找到数据集 anorexia,该数据集包含了 72 名患病年轻女性的治疗前后体重数据(单位为磅),这些数据来自 Hand 等人(1994)。其中一组女性为对照组(即无干预),另外两组分别为认知行为治疗组和家庭支持干预组。加载该库,并确保你能访问数据框并理解其内容。令 μ[d] 表示体重差的均值,计算方法为(治疗后体重 − 治疗前体重)。
-
无论参与者属于哪个治疗组,都需要针对所有体重数据进行适当的假设检验,并以 α = 0.05 得出结论,检验以下假设:
H[0] : μ[d] = 0
H[A] : μ[d] > 0
-
接下来,根据参与者所属的治疗组,进行三次独立的假设检验,使用相同的已定义假设。你注意到了什么?
R 中另一个现成可用的数据集是 PlantGrowth(Dobson, 1983),该数据集记录了某种植物的连续产量,并比较了在生长过程中施用两种补充剂对产量的潜在影响,目的是提高与对照组(无补充剂)相比的产量。
-
设置假设来检验对照组的平均产量是否低于接受任一治疗的植物的平均产量。确定这个检验是应该使用合并方差估计,还是 Welch 的 t 检验更为合适。
-
进行测试并得出结论(假设原始观察值符合正态分布)。
如前所述,决定是否在无配对的 t 检验中使用合并方差估计的经验法则。
-
你的任务是编写一个 包装器 函数,在根据经验法则决定是否应以
var.equal=FALSE执行后,调用t.test。请遵循以下指导原则:– 你的函数应该接受四个已定义的参数:
x和y,没有默认值,处理方式与t.test中相同;var.equal和paired,其默认值与t.test的默认值相同。– 应该包含一个省略号(第 9.2.5 节)来表示传递给
t.test的任何附加参数。– 执行时,函数应确定
paired=FALSE。如果
paired为TRUE,则无需继续进行合并方差的检验。如果
paired为FALSE,那么函数应根据经验法则自动确定var.equal的值。– 如果
var.equal的值是自动设置的,你可以假设它会覆盖用户最初提供的该参数的任何值。– 然后,适当地调用
t.test。 -
在第 18.2.2 节的文本中,尝试在所有三个示例上使用你的新函数,确保结果一致。
18.3 检验比例
在统计建模和假设检验中,重点关注均值尤其常见,因此你还必须考虑样本比例,解释为一系列n次二项试验的均值,其中结果是成功(1)或失败(0)。本节重点介绍比例的参数检验,假设目标抽样分布服从正态分布(通常称为Z检验)。
关于样本比例假设检验的设置和解释的一般规则与样本均值的假设检验相同。在本节介绍的Z检验中,你可以将这些视为关于单一比例的真实值或两个比例之间差异的检验。
18.3.1 单一比例
第 17.1.2 节介绍了单个样本比例的抽样分布应服从正态分布,其均值以真实比例π为中心,标准误差为
。假设试验是独立的,并且n不“太小”,π也不“太接近”0 或 1,这些公式在这里适用。
注意
检查 n 和π后者条件的经验法则简单地包括检查 n
和 n(1 −
)都大于 5,其中
是π的样本估计值。
值得注意的是,在涉及比例的假设检验中,标准误差本身依赖于π。这一点很重要——记住,任何假设检验都假设 H[0]在相关计算中成立。在处理比例时,这意味着在计算检验统计量时,标准误差必须使用零假设值π[0],而不是估计的样本比例
。
我将在一个例子中澄清这一点。假设一个喜爱某快餐连锁的个体注意到,他在吃了自己平常的午餐后,总是在一定时间内感到胃部不适。他偶然看到一个博客,博主认为吃这种特定食物后胃不适的几率是 20%。这个人很想知道他自己实际的胃不适率π是否与博主的值不同,因此他在29次不同的就餐中记录下自己是否经历胃不适(成功为TRUE,失败为FALSE)。这表明以下假设:
H[0] : π = 0.2
H[A] : π ≠ 0.2
这些可以根据接下来章节中讨论的通用规则进行检验。
计算:单样本 Z 检验
在检验某个成功比例π的真实值时,设
为n次试验中的样本比例,零假设值记作π[0]。你可以通过以下方式找到检验统计量:

在假设前述关于π和n大小的条件成立的情况下,Z ~ N(0,1)。
方程 (18.8)中的分母,即比例的标准误差,是相对于零假设值π[0]计算的,而不是
。正如刚才提到的,这是为了满足“真值”假设 H[0],以便在进行检验时,使得最终的p-值的解释与平常一致。标准正态分布用于根据Z找到p-值;此曲线下的方向由 H[A]的性质决定,和之前一样。
回到快餐例子,假设这些是观测数据,其中1表示胃不适,0表示其他情况。
sick <- c(0,0,1,1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,1,0,0,0,1,1,1,0,0,0,1)
该样本中的成功次数和成功概率如下所示:
R> sum(sick)
[1] 8
R> p.hat <- mean(sick)
R> p.hat
[1] 0.2758621
快速检查表明,按照经验法则,进行检验是合理的:
R> 29*0.2
[1] 5.8
R> 29*0.8
[1] 23.2
根据方程 (18.8),本例中的检验统计量Z如下所示:
R> Z <- (p.hat-0.2)/sqrt(0.2*0.8/29)
R> Z
[1] 1.021324
备择假设是双尾的,因此你需要计算对应的p-值,作为标准正态曲线下的双尾区域。对于正的检验统计量,可以通过将Z的上尾面积乘以 2 来评估。
R> 2*(1-pnorm(Z))
[1] 0.3071008
假设采用常规的α水平 0.05。给出的较高p-值 0.307 表明,在假设零假设成立的情况下,样本大小为 29 的结果并不足够不寻常,因此不能拒绝 H[0]。没有足够的证据表明该个体经历胃不适的比例与 0.2 有显著不同,正如博主所指出的那样。
你可以通过置信区间来支持这个结论。在 95%的置信度水平下,你可以计算置信区间(CI):
R> p.hat+c(-1,1)*qnorm(0.975)*sqrt(p.hat*(1-p.hat)/29)
[1] 0.1131927 0.4385314
这个区间轻松地包含了零假设值 0.2。
R 函数:prop.test
再次,R 拯救了你免于繁琐的逐步计算。现成的prop.test函数使你可以进行单样本比例检验等多项操作。该函数实际上以稍微不同的方式执行检验,使用卡方分布(将在第 18.4 节中进一步探讨)。然而,检验是等效的,prop.test返回的p-值与使用Z检验得到的值相同。
对于prop.test函数,用于单样本比例检验时,你需要提供观察到的成功次数x,总试验次数n,以及原假设值p。另外两个参数,alternative(定义 H[A]的性质)和conf.level(定义 1 − α),与t.test中的参数相同,默认值分别是"two.sided"和0.95。最后,建议在数据满足n
和n(1 −
)的经验法则时,显式设置可选参数correct=FALSE。
对于当前的例子,你可以通过以下代码进行测试:
R> prop.test(x=sum(sick),n=length(sick),p=0.2,correct=FALSE)
1-sample proportions test without continuity correction
data: sum(sick) out of length(sick), null probability 0.2
X-squared = 1.0431, df = 1, p-value = 0.3071
alternative hypothesis: true p is not equal to 0.2
95 percent confidence interval:
0.1469876 0.4571713
sample estimates:
p
0.2758621
p-值与之前得到的相同。不过,注意到报告的置信区间(CI)略有不同(基于正态分布的区间,依赖于中心极限定理)。prop.test生成的 CI 被称为威尔逊得分区间,它考虑了“成功概率”与二项分布之间的直接关联。为了简化起见,在执行涉及比例的假设检验时,你将继续使用基于正态分布的区间。
同样需要注意的是,像t.test一样,任何使用prop.test进行的一边检验只会提供一个单边的置信区间;你将在下面的例子中看到这一点。
18.3.2 两个比例
通过对标准误差的修改,基本扩展之前的过程,你可以比较来自独立总体的两个估计比例。就像两个均值的差异一样,你通常是在检验两个比例是否相同,从而使其差异为零。因此,典型的原假设值是零。
举个例子,考虑一个统计学考试的学生群体。在这个群体中,有n[1] = 233 名心理学专业的学生,其中x[1] = 180 名通过考试;另外有n[2] = 197 名地理学专业的学生,其中 175 名通过考试。假设有人声称地理学专业的学生在统计学考试中的通过率高于心理学专业的学生。
将心理学学生的真实通过率表示为π[1],将地理学学生的通过率表示为π[2],该假设可以通过以下定义的假设对进行统计检验:
H[0] : π[2] − π[1] = 0
H[A] : π[2] − π[1] > 0
就像比较两个均值一样,保持差异计算的一致顺序在整个测试计算中是非常重要的。这个例子展示了一个右尾检验。
计算:双样本 Z 检验
在数学上测试两比例之间的真实差异 π[1] 和 π[2] 时,令
[1] = x[1]/n[1] 为 π[1] 对应的 x[1] 成功次数和 n[1] 次试验的样本比例,类似地,令
[2] = x[2]/n[2] 为 π[2] 的比例。假设差异的原假设值为 π[0],则检验统计量为:

假设你可以应用上述关于 n[1]、n[2] 和 π[1]、π[2]* 的条件,你可以将 Z 视为符合正态分布 N(0,1)。
在 (18.9) 的分母中有一个新的量 p^∗。这是一个 合并 比例,计算如下:

如前所述,在这种类型的检验中,常常将原假设值,即两比例之间的真实差异,设定为零(换句话说,π[0] = 0)。
公式 (18.9) 的分母本身是用在假设检验中的两比例差异的标准误差。需要使用 p^∗ 再次体现了 H[0] 被假设为真的事实。在 (18.9) 的分母中分别使用
和
,以
的形式(即在假设检验之外的两比例差异的标准误差)会违反假设 H[0] 的“真实性”。
因此,回到心理学和地理学学生参加的统计学考试,你可以按照以下方式计算所需的各个量:
R> x1 <- 180
R> n1 <- 233
R> p.hat1 <- x1/n1
R> p.hat1
[1] 0.7725322
R> x2 <- 175
R> n2 <- 197
R> p.hat2 <- x2/n2
R> p.hat2
[1] 0.8883249
结果表明,心理学学生的样本通过率约为 77.2%,地理学学生的通过率为 88.8%;两者的差异约为 11.6%。通过检查
、n[1] 和
、n[2] 的值,你可以看到该测试满足经验法则;同样,假设标准显著性水平为 α = 0.05。
根据公式 (18.10),合并后的比例 p^∗ 如下所示:
R> p.star <- (x1+x2)/(n1+n2)
R> p.star
[1] 0.8255814
然后你按照 公式 (18.9) 计算检验统计量 Z,具体如下:
R> Z <- (p.hat2-p.hat1)/sqrt(p.star*(1-p.star)*(1/n1+1/n2))
R> Z
[1] 3.152693
根据假设,你可以找到对应的 p-值,作为标准正态曲线下右侧上尾区域的 Z 值,如下所示:
R> 1-pnorm(Z)
[1] 0.0008088606
你观察到一个显著小于 α 的 p-值,因此正式决策当然是拒绝原假设,支持备择假设。样本数据提供了足够的证据反驳 H[0],因此你可以得出结论,支持地理学学生的通过率高于心理学学生的通过率。
R 函数:prop.test
再次使用 R,你可以通过一行代码使用prop.test来进行检验。对于两个比例的比较,你将每组成功的次数作为长度为 2 的向量传递给x,并将各自的样本大小作为另一个长度为 2 的向量传递给n。请注意,条目的顺序必须与alternative的顺序一致(换句话说,在这里,“大于”要检验的比例对应x和n的第一个元素)。再次地,correct被设置为FALSE。
R> prop.test(x=c(x2,x1),n=c(n2,n1),alternative="greater",correct=FALSE)
2-sample test for equality of proportions without continuity correction
data: c(x2, x1) out of c(n2, n1)
X-squared = 9.9395, df = 1, p-value = 0.0008089
alternative hypothesis: greater
95 percent confidence interval:
0.05745804 1.00000000
sample estimates:
prop 1 prop 2
0.8883249 0.7725322
p-值与之前一系列计算得出的值相同,表明拒绝 H[0]。由于prop.test作为单侧检验调用,返回的置信区间提供了一个单侧边界。为了提供真实差异的双侧置信区间,考虑到检验结果,使用单独的
和
来构造,而不是专门使用(18.9)中的分母(假设 H[0]为真)。两个比例差异的标准误的“单独估计”版本之前已经给出(见方程(18.10)下的文字),因此可以根据以下公式计算 95%的置信区间:
R> (p.hat2-p.hat1) +
c(-1,1)*qnorm(0.975)*sqrt(p.hat1*(1-p.hat1)/n1+p.hat2*(1-p.hat2)/n2)
[1] 0.04628267 0.18530270
由此,你可以有 95%的信心,地理学学生通过考试的比例和心理学学生通过考试的比例之间的真实差异位于 0.046 到 0.185 之间。自然,这个区间也反映了假设检验的结果——它不包括零的原假设值,且完全是正的。
练习 18.3
一则护肤霜广告声称,九成使用该产品的女性会将其推荐给朋友。一位在百货商店工作的怀疑的销售员认为,愿意推荐该产品的女性用户真实比例π远小于 0.9。她随后向 89 位随机选取的购买了护肤霜的顾客询问她们是否会推荐给他人,其中 71 人回答是。
-
为此检验设置一个合适的假设对,并确定是否可以使用正态分布进行检验。
-
计算检验统计量和p-值,并使用显著性水平α = 0.1 给出你的检验结论。
-
使用你估算的样本比例,构建一个双侧 90%的置信区间,用于估计推荐该护肤霜的女性真实比例。
某国家的政治领导人好奇该国两个州的公民中支持大麻去刑化的比例。政府官员进行的小规模试调查显示,在州 1 中,445 名随机抽取的选民中有 97 人支持去刑化,而在州 2 中,419 名选民中有 90 人支持同一观点。
-
设π[1]为州 1 中支持非刑事化的公民的真实比例,π[2]为州 2 中的相同比例,进行并得出假设检验的结论,显著性水平为α=0.05,参考以下假设:
H[0] : π[2] − π[1] = 0
H[A] : π[2] − π[1] ≠ 0
-
计算并解释相应的置信区间。
尽管在 R 中有标准的现成的t-检验功能,但在写作本文时,还没有类似的功能来进行Z-检验(换句话说,即本文中描述的基于正态分布的比例检验),除非使用贡献包。
-
您的任务是编写一个相对简单的 R 函数
Z.test,该函数可以执行单样本或双样本Z-检验,遵循以下指南:– 该函数应包含以下参数:
p1和n1(无默认值),作为估计的比例和样本大小;p2和n2(默认值为NULL),在进行双样本检验时,包含第二个样本的比例和样本大小;p0(无默认值)作为零假设值;以及alternative(默认为"two.sided")和conf.level(默认为0.95),它们的使用方式与t.test中的相同。– 在进行双样本检验时,当
alternative="less"或alternative="greater"时,应测试p1是否小于或大于p2,这与在t.test中使用x和y是一样的。– 如果
p2或n2(或两者)为NULL,则该函数应使用p1、n1和p0执行单样本Z-检验。– 该函数应包含检查法则的功能,以确保在单样本和双样本设置中正态分布的有效性。如果违反了这一点,函数仍然可以完成,但应发出适当的警告信息(参见第 12.1.1 节)。
– 需要返回的只是一个列表,其中包含成员
Z(检验统计量)、P(适当的p-值——这个值可以通过alternative来确定;对于双边检验,通过判断Z是否为正值可以帮助确定),以及CI(关于conf.level的双边置信区间)。 -
重复 18.3.1 节和 18.3.2 节中的两个示例,使用
Z.test;确保您得出相同的结果。 -
调用
Z.test(p1=0.11,n1=10,p0=0.1)来在单样本设置中尝试您的警告信息。
18.4 测试类别变量
基于正态分布的Z检验特别适用于二元数据。对于更一般的类别变量(具有两个以上不同水平的变量)的统计检验,您需要使用广泛应用的卡方检验。卡方发音为kai,代表希腊字母χ,有时简写为χ²检验。
卡方检验有两种常见变体。第一种是卡方分布检验,也叫拟合优度(GOF)检验,它用于评估单一类别变量的各个水平的频率。第二种是卡方独立性检验,当你调查两个类别变量之间的频率关系时使用。
18.4.1 单一类别变量
与Z检验类似,单维卡方检验也涉及比例比较,但它适用于类别数大于二的情境。当你有k个类别(或等级)的类别变量,并希望假设它们的相对频率,从而找出有多少n个观察值落入每个定义的类别时,就使用卡方检验。在接下来的例子中,必须假设这些类别是互斥的(换句话说,一个观察值不能属于多个类别)和完备的(换句话说,这k个类别涵盖了所有可能的结果)。
我将通过以下示例来说明如何构建假设并介绍相关的思想和方法。假设一位社会学研究员对他所在城市男性面部毛发的分布情况感兴趣,并希望了解这些面部毛发类型是否在男性人群中均匀分布。他定义了一个三层次的类别变量:干净剃须(1),仅有胡须或仅有胡子(2),以及胡须和胡子(3)。他收集了 53 名随机选取的男性数据,发现以下结果:
R> hairy <- c(2,3,2,3,2,1,3,3,2,2,3,2,2,2,3,3,3,2,3,2,2,2,1,3,2,2,2,1,2,2,3,
2,2,2,2,1,2,1,1,1,2,2,2,3,1,2,1,2,1,2,1,3,3)
现在,研究问题是是否每个类别中的比例均衡代表。设π[1], π[2], 和π[3]分别表示城市中属于 1、2、3 组的男性的真实比例。因此,你需要检验以下假设:

对于这个检验,使用标准显著性水平 0.05。
替代假设的形式与之前看到的稍有不同,但它准确反映了卡方拟合优度检验的解释。在这类问题中,H[0]始终是每个组的比例等于声明的值,H[A]是数据整体与零假设中定义的比例不匹配。假设检验是在假设零假设成立的前提下进行的,反对零假设的证据将以较小的P值呈现。
计算:卡方分布检验
感兴趣的量是每个k类别中n个观察值的比例,π[1], ..., π[k],这是针对单一的互斥且完备的类别变量。原假设定义了每个比例的假定零值;分别将这些标签为π[0][(][1][)], ..., π[0][(][k][)]。检验统计量χ²定义为

其中 O[i] 是第 i 类别的 观察 计数,E[i] 是第 i 类别的 期望 计数;i = 1, ..., k。O[i] 直接从原始数据中获得,期望计数 E[i] = nπ[0][(][i][)],仅仅是总体样本量 n 与每个类别相应的零假设比例的乘积。χ² 的结果遵循 卡方分布(稍后会进一步解释),自由度为 ν = k − 1。通常情况下,根据一个非正式的经验法则,检验被认为有效,当期望计数 E[i] 中至少 80% 的值应大于或等于 5。
在这种卡方检验中,需要注意以下几点:
• 拟合优度 一词指的是观察数据与 H[0] 中假定分布的接近程度。
• 结果 (18.11) 的正端提供了反对 H[0] 的证据。因此,相应的 p-值总是作为上尾面积计算。
• 正如当前示例中所示,均匀性检验通过将零假设比例 π[0] = π[0][(][1][)] = ... = π[0][(][k][)] 稍微简化了零假设。
• 拒绝 H[0] 并不能告诉你 π[i] 的真实值。它仅仅表明它们并不完全符合 H[0]。
卡方分布依赖于自由度的指定,就像 t 分布一样。然而,与 t 曲线不同,卡方曲线本质上是单向的,只定义为非负值,并具有正的(右侧)水平渐近线(尾部趋向零)。
正是这种单向分布导致了 p-值仅定义为上尾面积;一尾或二尾面积在这类卡方检验中没有相关性。为了了解密度函数的实际形态, 图 18-1 展示了三条特定的曲线,定义了 ν = 1, ν = 5 和 ν = 10 的自由度。

图 18-1:使用不同自由度值的三种卡方密度函数实例。请注意该函数的正域及其“平坦化”和“右延展”行为,随着ν的增加。
该图像是使用相关的 d 函数 dchisq 生成的,ν 被传递给 df 参数。
R> x <- seq(0,20,length=100)
R> plot(x,dchisq(x,df=1),type="l",xlim=c(0,15),ylim=c(0,0.5),ylab="density")
R> lines(x,dchisq(x,df=5),lty=2)
R> lines(x,dchisq(x,df=10),lty=3)
R> abline(h=0,col="gray")
R> abline(v=0,col="gray")
R> legend("topright",legend=c("df=1","df=5","df=10"),lty=1:3)
当前的面部毛发示例是检验三类频率分布均匀性的测试。你可以通过 table 获取观察到的计数和相应的比例。
R> n <- length(hairy)
R> n
[1] 53
R> hairy.tab <- table(hairy)
R> hairy.tab
hairy
1 2 3
11 28 14
R> hairy.tab/n
hairy
1 2 3
0.2075472 0.5283019 0.2641509
计算检验统计量 χ² 时,观察到的计数 O[i] 存储在 hairy.tab 文件中。期望计数 E[i] 是一个直接的算术计算,计算方式为总观察数乘以零假设比例 1/3(结果存储在 expected 中),这样你可以为每个类别得到相同的值。
这些,以及每个类别对检验统计量的贡献,都以矩阵形式很好地呈现出来,该矩阵是通过cbind构造的(见第 3.1.2 节)。
R> expected <- 1/3*n
R> expected
[1] 17.66667
R> hairy.matrix <- cbind(1:3,hairy.tab,expected,
(hairy.tab-expected)²/expected)
R> dimnames(hairy.matrix) <- list(c("clean","beard OR mous.",
"beard AND mous."),
c("i","Oi","Ei","(Oi-Ei)²/Ei"))
R> hairy.matrix
i Oi Ei (Oi-Ei)²/Ei
clean 1 11 17.66667 2.5157233
beard OR mous. 2 28 17.66667 6.0440252
beard AND mous. 3 14 17.66667 0.7610063
请注意,所有期望频数都大于 5,这满足之前提到的非正式经验法则。在 R 编码方面,还需要注意,单个数字expected会被隐式回收,以匹配传递给cbind的其他向量的长度,并且你使用了dimnames属性(参见第 6.2.1 节)来注释行和列。
根据公式(18.11),检验统计量是通过第四列hairy.matrix中(O[i] − E[i])²/E[i]的贡献计算得出的。
R> X2 <- sum(hairy.matrix[,4])
R> X2
[1] 9.320755
对应的p-值是从卡方分布中取出的适当的上尾区域,ν = 3 − 1 = 2 个自由度。
R> 1-pchisq(X2,df=2)
[1] 0.009462891
这个非常小的p-值提供了证据,表明在定义的男性面部毛发类别中的真实频率并没有均匀分布为 1/3, 1/3, 1/3 的比例。请记住,检验结果并不会给出真实的比例,而只是表明这些比例并不符合 H[0]中的假设。
R 函数:chisq.test
与 t.test和prop.test类似,R 提供了一个用于执行卡方拟合优度(GOF)测试的快速函数。chisq.test函数将观察到的频率向量作为第一个参数x。对于面部毛发的例子,这一简单的代码行提供了与之前相同的结果:
R> chisq.test(x=hairy.tab)
Chi-squared test for given probabilities
data: hairy.tab
X-squared = 9.3208, df = 2, p-value = 0.009463
默认情况下,函数执行的是均匀性检验,认为类别的数量等于传递给x的向量的长度。然而,假设收集面部毛发数据的研究人员意识到他是在 11 月收集数据的,那是一个许多男性为了支持“Mo-vember”运动而蓄胡须的月份,以提高对男性健康的关注。这改变了他对清洁剃须(1)、仅有胡须或仅有小胡子(2)、以及胡须和小胡子同时存在(3)类别的真实比例的看法。他现在想测试以下内容:
H[0] : π[0][(][1][)] = 0.25; π[0][(][2][)] = 0.5; π[0][(][3][)] = 0.25
H[A] : H[0] 是错误的。
如果不想进行均匀性的拟合优度(GOF)检验,在类别的“真实”比例不相同的情况下,chisq.test函数要求你将原假设比例作为与x长度相同的向量传递给p参数。自然地,p中的每个条目必须与x中列出的类别相对应。
R> chisq.test(x=hairy.tab,p=c(0.25,0.5,0.25))
Chi-squared test for given probabilities
data: hairy.tab
X-squared = 0.5094, df = 2, p-value = 0.7751
在p-值非常高的情况下,没有证据可以拒绝 H[0]。换句话说,没有证据表明 H[0]中的假设比例是错误的。
18.4.2 两个分类变量
卡方检验也适用于具有两个互斥且穷尽的分类变量的情况——将它们称为变量A和B。它用于检测变量A和B之间是否存在某种影响关系(换句话说,依赖性),通过观察频数分布如何随着类别的变化而共同变化。如果没有关系,变量A的频数分布将与变量B的频数分布无关。因此,这种特定形式的卡方检验被称为独立性检验,并始终假设以下假设:
H[0] : 变量A和B是独立的。
(或者,A和B之间没有关系。)
H[A] : 变量A和B是不独立的。
(或者,A和B之间有关系。)
因此,为了进行检验,你需要将观察到的数据与在分布完全无关(即假设 H[0]为真)时期望看到的频数进行比较。若实际频数与期望频数偏差较大,则会导致较小的p-值,从而提供反对原假设的证据。
那么,如何最好地呈现这类数据呢?对于两个分类变量,二维结构是合适的;在 R 中,这是一种标准矩阵。例如,假设某些皮肤科医生对治疗常见皮肤病的成功率感兴趣。它们的记录显示N = 355 名患者在他们的诊所接受了四种可能治疗中的一种——一疗程药片、系列注射、激光治疗和草药治疗。治愈情况也被记录——无效、部分成功和完全成功。数据以构建的矩阵skin给出。
R> skin <- matrix(c(20,32,8,52,9,72,8,32,16,64,30,12),4,3,
dimnames=list(c("Injection","Tablet","Laser","Herbal"),
c("None","Partial","Full")))
R> skin
None Partial Full
Injection 20 9 16
Tablet 32 72 64
Laser 8 8 30
Herbal 52 32 12
以这种方式呈现频数的二维表格称为列联表。
计算:卡方独立性检验
为了计算检验统计量,假设数据以k[r] × k[c]的列联表形式呈现,换句话说,是基于两个分类变量(均为互斥且穷尽的)的计数矩阵。检验的重点是观察N个观测值在“行”变量的k[r]个水平和“列”变量的k[c]个水平之间频数的联合分布。检验统计量χ²计算公式如下:

其中,O[[][i][,] [j]] 是观察到的频数,E[[][i][,] [j]] 是第i行第j列的预期频数。每个E[[][i][,] [j]]是将第i行的总和乘以第j列的总和,再除以N得到的。

结果,χ²,遵循卡方分布,ν = (k[r] − 1) × (k[c] − 1)的自由度。同样,p-值始终是一个上尾区域,当至少 80%的E[[][i][,] [j]]大于等于 5 时,你可以认为该检验是有效的。
对于这个计算,需要注意以下几点:
• 不必假设k[r] = k[c]。
• 方程 (18.12)的功能与(18.11)相同——它涉及到每个单元格观察值和期望值之间的平方差的总和。
• (18.12)中的双重求和仅表示所有单元格的总和,意思是你可以通过
来计算总样本量N。
• 拒绝 H[0]并不能告诉你频率之间相互依赖的性质,只是表明有证据表明这两个分类变量之间可能存在某种依赖关系。
继续这个例子,皮肤科医生希望确定他们的记录是否表明有统计证据表明治疗类型与皮肤疾病治愈成功率之间存在某种关系。为了方便,分别存储行变量和列变量的类别总数k[r]和k[c]。
R> kr <- nrow(skin)
R> kc <- ncol(skin)
你在skin中有O[[][i][,] [j]],所以现在你必须计算E[[][i][,] [j]]。根据方程 (18.13),它涉及行和列的总和,你可以使用内建的rowSums和colSums函数来评估这些。
R> rowSums(skin)
Injection Tablet Laser Herbal
45 168 46 96
R> colSums(skin)
None Partial Full
112 121 122
这些结果表示每组中的总数,不考虑其他变量。要获取矩阵中所有单元格的期望频数,方程 (18.13)要求将每行和每列的总和相乘一次。你可以写一个for循环,但这会低效且不优雅。更好的做法是使用带有可选each参数的rep(参考第 2.3.2 节)。通过将列总和(成功的水平)中的每个元素重复四次,你可以利用向量化行为,将这个重复的向量与rowSums产生的较短向量相乘。然后,你可以调用sum(skin)将其除以N并重新排列成矩阵。以下几行展示了这个示例是如何一步步实现的:
R> rep(colSums(skin),each=kr)
None None None None Partial Partial Partial Partial Full
112 112 112 112 121 121 121 121 122
Full Full Full
122 122 122
R> rep(colSums(skin),each=kr)*rowSums(skin)
None None None None Partial Partial Partial Partial Full
5040 18816 5152 10752 5445 20328 5566 11616 5490
Full Full Full
20496 5612 11712
R> rep(colSums(skin),each=kr)*rowSums(skin)/sum(skin)
None None None None Partial Partial Partial Partial
14.19718 53.00282 14.51268 30.28732 15.33803 57.26197 15.67887 32.72113
Full Full Full Full
15.46479 57.73521 15.80845 32.99155
R> skin.expected <- matrix(rep(colSums(skin),each=kr)*rowSums(skin)/sum(skin),
nrow=kr,ncol=kc,dimnames=dimnames(skin))
R> skin.expected
None Partial Full
Injection 14.19718 15.33803 15.46479
Tablet 53.00282 57.26197 57.73521
Laser 14.51268 15.67887 15.80845
Herbal 30.28732 32.72113 32.99155
请注意,所有期望值都大于 5,这是首选的情况。
最好构造一个单一的对象来保存计算不同阶段的结果,以得到检验统计量,就像你在一维示例中所做的那样。由于每个阶段都是一个矩阵,你可以使用cbind将相关矩阵结合在一起,并生成具有适当维度的数组(详细参考第 3.4 节)。
R> skin.array <- array(data=cbind(skin,skin.expected,
(skin-skin.expected)²/skin.expected),
dim=c(kr,kc,3),
dimnames=list(dimnames(skin)[[1]],dimnames(skin)[[2]],
c("O[i,j]","E[i,j]",
"(O[i,j]-E[i,j])²/E[i,j]")))
R> skin.array
, , O[i,j]
None Partial Full
Injection 20 9 16
Tablet 32 72 64
Laser 8 8 30
Herbal 52 32 12
, , E[i,j]
None Partial Full
Injection 14.19718 15.33803 15.46479
Tablet 53.00282 57.26197 57.73521
Laser 14.51268 15.67887 15.80845
Herbal 30.28732 32.72113 32.99155
, , (O[i,j]-E[i,j])²/E[i,j]
None Partial Full
Injection 2.371786 2.6190199 0.01852279
Tablet 8.322545 3.7932587 0.67978582
Laser 2.922614 3.7607992 12.74002590
Herbal 15.565598 0.0158926 13.35630339
最后的步骤很简单——由 (18.12) 给出的检验统计量仅仅是 skin.array 第三层矩阵所有元素的总和。
R> X2 <- sum(skin.array[,,3])
R> X2
[1] 66.16615
此独立性检验的相应 p-值如下:
R> 1-pchisq(X2,df=(kr-1)*(kc-1))
[1] 2.492451e-12
回想一下,相关的自由度定义为 ν = (k[r] − 1) × (k[c] − 1)。
极小的 p-值为反对原假设提供了强有力的证据。适当的结论是拒绝 H[0],并指出皮肤疾病治疗类型与治愈成功率之间似乎存在关系。
R 函数:chisq.test
然而,再一次,本章的任何部分如果没有展示 R 在这些基本程序中内置的功能,将不完整。当 chisq.test 接受一个矩阵作为 x 时,其默认行为是执行一个卡方独立性检验,检验行和列频率的独立性——就像这里对皮肤疾病示例进行的手动计算一样。以下结果轻松验证了你之前的计算:
R> chisq.test(x=skin)
Pearson's Chi-squared test
data: skin
X-squared = 66.1662, df = 6, p-value = 2.492e-12
练习 18.4
HairEyeColor 是 R 中的一个现成数据集,你可能还没遇到过。这个 4 × 4 × 2 的数组提供了 592 名统计学学生的发色和眼色频率,按性别划分(Snee, 1974)。
- 在显著性水平 α = 0.01 下,对所有学生(无论性别)进行发色与眼色的卡方独立性检验,并进行解释。
在 练习 8.1 中,你访问了贡献包 car 中的 Duncan 数据集,包含了 1950 年收集的工作声望标记。如果你还没有安装该包,请安装并加载数据框。
-
Duncan的第一列是变量type,记录作为因子具有三种水平的工作类型:prof(专业或管理)、bc(蓝领)和wc(白领)。构造适当的假设并执行卡方拟合优度检验,以确定三种工作类型在数据集中是否平等地分布。-
根据显著性水平 α = 0.05 解释结果中的 p-值。
-
如果你使用显著性水平 α = 0.01,会得出什么结论?
-
18.5 误差与检验力
在讨论所有这些形式的统计假设检验时,有一个共同的主题:对 p-值的解释,以及它在假设检验中的意义。频率学派统计假设检验在许多研究领域中广泛应用,因此至少要简要探索与之直接相关的概念。
18.5.1 假设检验误差
假设检验的目的是获得一个p值,以量化反对原假设 H[0]的证据。如果p值小于预先定义的显著性水平α(通常为 0.05 或 0.01),则拒绝原假设,接受备择假设 H[A]。如前所述,这种方法被批评是正当的,因为α的选择基本上是任意的;拒绝或保留 H[0]的决定可以仅仅依赖于α值的变化。
设想一下,在某个特定的检验中,正确的结果是什么。如果 H[0]是真的,那么你就应该保留它。如果 H[A]是真的,那么你应该拒绝原假设。这种“真理”,无论如何,在实践中是无法知道的。话虽如此,从理论上考虑给定假设检验在得出正确结论方面的效果(好或坏)仍然是有意义的。
为了能够测试你拒绝或保留原假设的有效性,你必须能够识别两种类型的错误:
• 当你错误地拒绝一个真实的 H[0]时,就会发生第一类错误。在任何给定的假设检验中,第一类错误的概率等同于显著性水平α。
• 当你错误地保留一个错误的 H[0](换句话说,未能接受一个真实的 H[A])时,就会发生第二类错误。由于这取决于真实的 H[A]是什么,因此,发生这种错误的概率,标记为β,在实践中通常是无法知道的。
18.5.2 第一类错误
如果你的p值小于α,则拒绝原假设。但是,如果原假设实际上是真的,α直接定义了你错误拒绝它的概率。这被称为第一类错误。
图 18-2 提供了一个假设检验中第一类错误概率的概念性示意图,其中假设设定为 H[0] : μ = μ[0] 和 H[A] : μ > μ[0]。

图 18-2:第一类错误概率 α的概念图
原假设分布以原值μ[0]为中心;备择假设分布以其右侧的某个均值μ[A]为中心,如图 18-2 所示。如你所见,如果原假设是真的,那么在此检验中错误地拒绝它的概率将等于显著性水平α,位于原假设分布的右尾部。
模拟第一类错误
为了通过数值模拟演示第一类错误率(这里指的是随机生成假设数据样本),你可以编写代码,执行等效的重复假设检验。为了能够多次使用此代码,可以在 R 脚本编辑器中定义以下函数:
typeI.tester <- function(mu0,sigma,n,alpha,ITERATIONS=10000){
pvals <- rep(NA,ITERATIONS)
for(i in 1:ITERATIONS){
temporary.sample <- rnorm(n=n,mean=mu0,sd=sigma)
temporary.mean <- mean(temporary.sample)
temporary.sd <- sd(temporary.sample)
pvals[i] <- 1-pt((temporary.mean-mu0)/(temporary.sd/sqrt(n)),df=n-1)
}
return(mean(pvals<alpha))
}
typeI.tester函数旨在从特定的正态分布中生成ITERATIONS个样本。对于每个样本,你将进行一个关于均值的右尾检验(参考第 18.2.1 节),遵循图 18-2 中的思路,假设原假设 H[0] : μ = μ[0]和备择假设 H[A] : μ > μ[0]。
你可以减少ITERATIONS以生成较少的完整样本,这样可以加速计算时间,但会导致模拟的错误率更为波动。每个大小为n的假设原始测量的完整样本都是使用rnorm生成的,均值为mu0参数(标准差为sigma参数)。所需的显著性水平由alpha设置。在for循环中,为每个生成的样本计算样本均值和样本标准差。
如果每个样本都经过“真实”的假设检验,p值将取自自由度为n-1的t分布的右侧区域(使用pt),并依据之前在公式 (18.2)中给出的标准化检验统计量。
在每次迭代中计算出的p值存储在预定义的向量pvals中。因此,逻辑向量pvals<alpha包含相应的TRUE/FALSE值;前者逻辑值标志着原假设的拒绝,后者标志着原假设的保留。I 型错误率是通过对该逻辑向量调用mean得到的,它返回TRUE的比例(换句话说,就是“原假设被拒绝”的总体比例),该比例来自模拟的样本。请记住,样本是随机生成的,因此每次运行函数时,结果可能会稍微有所变化。
该函数之所以有效,是因为根据问题的定义,正在生成的样本来自一个均值确实设定为原假设值的分布,换句话说,μ[A] = μ[0]。因此,任何对该陈述的统计拒绝,如果p值小于显著性水平α,显然是错误的,完全是随机变异的结果。
要尝试此操作,导入该函数并执行,生成默认的ITERATIONS=10000个样本。使用标准正态分布作为原假设分布(在此情况下为“真实”分布);使每个样本的大小为 40,并将显著性水平设置为常规的α = 0.05。以下是一个示例:
R> typeI.tester(mu0=0,sigma=1,n=40,alpha=0.05)
[1] 0.0489
这表明,取自样本的 10,000 × 0.0489 = 489 个样本,得到了一个相应的检验统计量,提供了一个p值,这将错误地导致拒绝 H[0]。这个模拟的 I 型错误率接近预设的alpha=0.05。
这是另一个示例,这次使用非标准正态数据样本,α = 0.01:
R> typeI.tester(mu0=-4,sigma=0.3,n=60,alpha=0.01)
[1] 0.0108
请注意,再次提醒,数值模拟的 I 型错误率反映了显著性水平。
这些结果在理论上并不难理解——如果真实分布确实具有等于零假设值的均值,您将自然地在实践中以等于α的频率观察到这些“极端”的检验统计量值。当然,问题在于,在实践中真实分布是未知的,这再次强调了拒绝任何 H[0]不能被解读为 H[A]为真的证明。可能仅仅是您观察到的样本遵循了零假设,但由于偶然原因(无论这种偶然性多么小),产生了极端的检验统计量值。
邦费罗尼校正
第一类错误自然由于随机变异而发生这一事实尤其重要,这促使我们考虑多重检验问题。如果您进行许多假设检验,您应该小心仅报告“统计显著的结果数量”——随着假设检验数量的增加,您得到错误结果的机会也会增加。例如,在进行 20 次α = 0.05 的检验时,平均会有一次是所谓的假阳性;如果您进行 40 或 60 次检验,您不可避免地会得到更多的假阳性结果。
当进行多个假设检验时,您可以通过使用邦费罗尼校正来控制与犯第一类错误相关的多重检验问题。邦费罗尼校正建议,当执行总共N个独立的假设检验时,每个检验的显著性水平为α,您应该使用α[B] = α/N来进行任何统计显著性的解释。然而,需注意的是,这种显著性水平的校正代表了多重检验问题的最简单解决方案,并且由于其保守性,可能会受到批评,尤其当N值较大时可能会带来问题。
邦费罗尼校正及其他校正措施是为了正式化修正多重检验中第一类错误的办法。然而,通常情况下,仅意识到零假设(H[0])可能为真,即使p值被认为较小,也足够了。
18.5.3 第一类错误
第一类错误的问题可能会让人认为应该用更小的α值进行假设检验。不幸的是,事情并非如此简单;减少任何给定检验的显著性水平将直接导致犯第一类错误的机会增加。
第一类错误(Type II Error)指的是错误地保留原假设——换句话说,当实际为备择假设为真时,得到的p值大于显著性水平。对于您至今所看的相同情境(单样本均值的上尾检验),图 18-3 显示了第一类错误的概率,以阴影标示并表示为β。

图 18-3:第一类错误概率β的概念图
找到β并不像找到 I 型错误概率那么容易,因为β依赖于多个因素,其中之一是μ[A]的真实值(通常你是无法知道的)。如果μ[A]接近假设的零值μ[0],你可以想象替代分布在图 18-3 中向左平移(偏移),从而导致β的增大。同样地,继续参考图 18-3,假设减少显著性水平α。这样做意味着垂直虚线(表示相应的临界值)向右移动,也会增加β的阴影区域。直观上,这是有道理的——真实的替代值越接近零值和/或显著性水平越小,通过拒绝 H[0]检测到 H[A]的难度就越大。
如前所述,β通常无法在实践中计算出来,因为需要知道真实分布是什么。然而,这个量在给出测试在特定条件下错误保留零假设的倾向时是有用的。例如,假设你正在进行一个单样本t-检验,零假设 H[0]:μ = μ[0],备择假设 H[A]:μ > μ[0],其中μ[0] = 0,但原始测量的(真实)备择分布具有均值μ[A] = 0.5 和标准差σ = 1。假设随机样本大小为n = 30,并使用显著性水平α = 0.05,那么在任何给定的假设检验中,犯 II 型错误的概率是多少(使用零分布的相同标准差)?为了回答这个问题,重新查看图 18-3;你需要临界值,这个临界值由显著性水平(虚线垂直线)标出。如果假设σ已知,那么感兴趣的抽样分布将是正态分布,均值为μ[0] = 0,标准误差为
(参见第 17.1.1 节)。因此,假设上尾面积为 0.05,你可以通过以下方法找到临界值:
R> critval <- qnorm(1-0.05,mean=0,sd=1/sqrt(30))
R> critval
[1] 0.3003078
这表示在该特定情况下的垂直虚线(有关qnorm使用的复习请见第 16.2.2 节)。这个例子中的 II 型错误是指在替代“真实”分布下,从临界值开始,左侧尾部区域的面积:
R> pnorm(critval,mean=0.5,sd=1/sqrt(30))
[1] 0.1370303
从这里可以看出,在这些条件下进行假设检验时,错误保留零假设的概率大约为 13.7%。
模拟 II 型错误
在这里,模拟特别有用。在编辑器中,考虑以下定义的函数typeII.tester:
typeII.tester <- function(mu0,muA,sigma,n,alpha,ITERATIONS=10000){
pvals <- rep(NA,ITERATIONS)
for(i in 1:ITERATIONS){
temporary.sample <- rnorm(n=n,mean=muA,sd=sigma)
temporary.mean <- mean(temporary.sample)
temporary.sd <- sd(temporary.sample)
pvals[i] <- 1-pt((temporary.mean-mu0)/(temporary.sd/sqrt(n)),df=n-1)
}
return(mean(pvals>=alpha))
}
这个函数类似于typeI.tester。零值、原始测量的标准差、样本大小、显著性水平和迭代次数与之前相同。此外,你现在有了muA,提供了生成样本时所使用的“真实”均值μ[A]。同样,在每次迭代时,都会生成一个大小为n的随机样本,计算其均值和标准差,并使用pt从通常的标准化检验统计量中计算出适当的p值,df=n-1。(记住,由于你正在使用样本标准差s来估计测量的真实标准差σ,因此技术上使用t分布是正确的。)完成for循环后,返回p值大于或等于显著性水平alpha的比例。
在将函数导入工作空间后,你可以为这个测试模拟β。
R> typeII.tester(mu0=0,muA=0.5,sigma=1,n=30,alpha=0.05)
[1] 0.1471
我的结果表明,与之前评估的理论β值接近,尽管因为使用基于t的采样分布而非正态分布,额外的不确定性使得结果稍微偏大。再次强调,每次运行typeII.tester时,由于所有结果都基于随机生成的假设数据样本,结果都会略有不同。
转向图 18-3,你可以看到(与之前的评论一致),如果为了减少一型错误的可能性,使用α = 0.01 而非 0.05,垂直线会向右移动,从而增加二型错误的概率,其他条件保持不变。
R> typeII.tester(mu0=0,muA=0.5,sigma=1,n=30,alpha=0.01)
[1] 0.3891
影响二型错误率的其他因素
显著性水平不是唯一影响β的因素。保持α为 0.01,看看当原始测量的标准差从σ = 1 增加到σ = 1.1,再到σ = 1.2 时,会发生什么变化。
R> typeII.tester(mu0=0,muA=0.5,sigma=1.1,n=30,alpha=0.01)
[1] 0.4815
R> typeII.tester(mu0=0,muA=0.5,sigma=1.2,n=30,alpha=0.01)
[1] 0.5501
增加测量的变异性,而不改变情境中的其他任何内容,也会增加二型错误的机会。你可以想象,图 18-3 中的曲线会因为均值的标准误增大而变得更加平坦且分散,这将导致在由临界值标记的左侧尾部有更多的概率权重。相反,如果原始测量的变异性较小,那么样本均值的采样分布会变得更高更窄,这意味着β值会降低。
更小或更大的样本量将产生类似的影响。由于位于标准误差公式的分母,更小的n会导致更大的标准误差,从而使曲线变得更平坦,β增加;而更大的样本量会产生相反的效果。如果你保持最新的μ[0] = 0,μ[A] = 0.5,σ = 1.2,和α = 0.01,注意将样本量从 30 减少到 20 会导致模拟的第二类错误率增加,较最近结果 0.5501 相比,增加样本量到 40 则会改善该比率。
R> typeII.tester(mu0=0,muA=0.5,sigma=1.2,n=20,alpha=0.01)
[1] 0.7319
R> typeII.tester(mu0=0,muA=0.5,sigma=1.2,n=40,alpha=0.01)
[1] 0.4219
最后,如讨论开始时所述,μ[A]的具体值本身会影响β,正如你所期望的那样。同样,保持其他所有组件的最新值,在我的案例中,β = 0.4219,注意通过将“真实”均值从μ[A] = 0.5 改变为μ[A] = 0.4,将μ[0]的均值拉近,意味着第二类错误的概率增加;如果差异增加到μ[A] = 0.6,则情况正好相反。
R> typeII.tester(mu0=0,muA=0.4,sigma=1.2,n=40,alpha=0.01)
[1] 0.6147
R> typeII.tester(mu0=0,muA=0.6,sigma=1.2,n=40,alpha=0.01)
[1] 0.2287
总结一下,尽管这些模拟的比率应用于假设检验是针对单一均值的上尾检验的特定情况,但这里讨论的基本概念和思想适用于任何假设检验。很容易得出第一类错误率与预定义的显著性水平相匹配,因此可以通过减小α来降低。相反,控制第二类错误率是一项复杂的平衡工作,涉及样本量、显著性水平、观察变异性以及真实值与原假设之间的差异大小。这个问题在学术上较为重要,因为在实际中,“真实”通常是未知的。然而,第二类错误率与统计功效的直接关系通常在数据收集准备中起着至关重要的作用,特别是在你考虑样本量需求时,正如你将在下一部分中看到的那样。
练习 18.5
-
编写一个新的
typeI.tester版本,命名为typeI.mean。新函数应能够模拟单一均值测试的第一类错误率,支持任意方向(换句话说,单尾或双尾)。新函数应接受一个额外的参数test,该参数取字符字符串"less"、"greater"或"two.sided",以指定所需的测试类型。你可以通过如下修改typeI.tester来实现:– 不要直接在
for循环中计算并存储p-值,而是只存储测试统计量。– 当循环完成时,设置堆叠的
if-else语句,以适应三种测试类型,根据需要计算p-值。– 对于双尾检验,请记住p-值定义为零假设之外“更极端”的区域的两倍。在计算上,这意味着如果检验统计量为正,必须使用上尾区域,反之则使用下尾区域。如果该区域小于α的一半(因为它在“真实”假设检验中会被乘以 2),则应标记为拒绝零假设。
– 如果
test的值不是三种可能值之一,函数应使用stop抛出适当的错误。-
使用文本中的第一个示例设置进行实验,μ[0] = 0,σ = 1,n = 40,以及α = 0.05。调用
typeI.mean三次,使用test的三种可能选项中的每一种。你应该会发现所有模拟结果都接近 0.05。 -
重复 (i) 使用文本中的第二个示例设置,μ[0] = −4,σ = 0.3,n = 60,以及α = 0.01。你应该会发现所有模拟结果都接近α的值。
-
-
修改
typeII.tester,与修改typeI.tester的方式相同;将新函数命名为typeII.mean。模拟以下假设检验的类型 II 错误率。根据文本,假设μ[A],σ,α和n分别表示真实均值、原始观测的标准差、显著性水平和样本大小。-
H[0] : μ = −3.2;H[A] : μ ≠ −3.2
其中μ[A] = −3.3,σ = 0.1,α = 0.05,n = 25。
-
H[0] : μ = 8994;H[A] : μ < 8994
其中μ[A] = 5600,σ = 3888,α = 0.01,n = 9。
-
H[0] : μ = 0.44;H[A] : μ > 0.44
其中μ[A] = 0.4,σ = 2.4,α = 0.05,n = 68。
-
18.5.4 统计功效
对于任何假设检验,考虑其潜在的统计功效是非常有用的。功效是正确拒绝一个不真实的零假设的概率。对于一个具有类型 II 错误率β的检验,统计功效通过 1 − β 计算得出。一个检验的功效越高越好。与类型 II 错误概率的简单关系意味着所有影响β值的因素也直接影响功效。
对于前一节中讨论的相同单尾 H[0] : μ = μ[0]和 H[A] : μ > μ[0]的例子,图 18-4 显示了检验的功效——即类型 II 错误率的补数。按照惯例,假设检验的功效大于 0.8 被认为是统计显著的。

*图 18-4:统计功效 1 − *β**的概念图
您可以通过模拟在特定检验条件下数值化地评估效能。关于第二类错误的前面讨论,您可以通过从 1 中减去所有模拟结果的β来评估该特定检验的效能。例如,当μ[0] = 0,μ[A] = 0.6 时,样本大小为n = 40,σ = 1.2,α = 0.01 时,检测效能为 1 − 0.2287 = 0.7713(使用我之前得到的β的最新结果)。这意味着,在基于这些条件生成的测量样本的假设检验中,正确检测到真实均值 0.6 的概率大约为 77%。
研究人员通常对效能与样本大小之间的关系感兴趣(尽管重要的是要记住,这只是决定效能的影响因素之一)。在开始收集数据以检验某个特定假设之前,您可能会从过去的研究或初步研究中获得关于感兴趣参数潜在真实值的想法。这对于确定样本大小非常有用,例如帮助回答类似“如果真实均值实际上是μ[A],我需要多大的样本才能进行一个统计学上有效的检验,以正确拒绝 H[0]?”这样的问题。
模拟效能
在最近的检验条件下,样本大小为n = 40,您已经发现检测μ[A] = 0.6 的效能大约为 0.77。为了这个例子,假设您想找出应当如何增加n的值,以便进行一个统计学上有效的检验。为了回答这个问题,在编辑器中定义以下power.tester函数:
power.tester <- function(nvec,...){
nlen <- length(nvec)
result <- rep(NA,nlen)
pbar <- txtProgressBar(min=0,max=nlen,style=3)
for(i in 1:nlen){
result[i] <- 1-typeII.tester(n=nvec[i],...)
setTxtProgressBar(pbar,i)
}
close(pbar)
return(result)
}
power.tester函数使用第 18.5.3 节中定义的typeII.tester函数来评估单一样本均值的给定上尾假设检验的效能。它接受一个作为nvec参数传入的样本大小向量(所有其他参数通过省略号传递给typeII.tester——请参见第 11.2.4 节)。power.tester中的for循环依次遍历nvec中的每一项,为每个样本大小模拟效能,并将结果存储在一个相应的向量中,该向量会返回给用户。请记住,通过typeII.tester,该函数使用假设数据样本的随机生成,因此每次运行power.tester时,您观察到的结果可能会有所波动。
在评估许多独立样本大小的效能时可能会有轻微的延迟,因此这个函数也提供了一个很好的机会来展示进度条的实际应用(有关详细信息,请参见第 12.2.1 节)。
设置以下向量,使用冒号操作符(参见第 2.3.2 节)构造一个包含从 5 到 100(包括 100)之间的整数序列,用于检查的样本大小:
R> sample.sizes <- 5:100
导入power.tester函数后,你可以模拟这些整数的功效,用于此特定测试(ITERATIONS被减半为5000以减少整体完成时间)。
R> pow <- power.tester(nvec=sample.sizes,
mu0=0,muA=0.6,sigma=1.2,alpha=0.01,ITERATIONS=5000)
|====================================================================| 100%
R> pow
[1] 0.0630 0.0752 0.1018 0.1226 0.1432 0.1588 0.1834 0.2162 0.2440 0.2638
[11] 0.2904 0.3122 0.3278 0.3504 0.3664 0.3976 0.4232 0.4478 0.4680 0.4920
[21] 0.5258 0.5452 0.5552 0.5616 0.5916 0.6174 0.6326 0.6438 0.6638 0.6844
[31] 0.6910 0.7058 0.7288 0.7412 0.7552 0.7718 0.7792 0.7950 0.8050 0.8078
[41] 0.8148 0.8316 0.8480 0.8524 0.8600 0.8702 0.8724 0.8800 0.8968 0.8942
[51] 0.8976 0.9086 0.9116 0.9234 0.9188 0.9288 0.9320 0.9378 0.9370 0.9448
[61] 0.9436 0.9510 0.9534 0.9580 0.9552 0.9648 0.9656 0.9658 0.9684 0.9756
[71] 0.9742 0.9770 0.9774 0.9804 0.9806 0.9804 0.9806 0.9854 0.9848 0.9844
[81] 0.9864 0.9886 0.9890 0.9884 0.9910 0.9894 0.9906 0.9930 0.9926 0.9938
[91] 0.9930 0.9946 0.9948 0.9942 0.9942 0.9956
正如预期的那样,随着n的增加,检出功效稳步上升;这些结果中,常规的 80%临界值出现在 0.7950 和 0.8050 之间。如果你不想通过视觉来识别该值,你可以使用which先找到pow中至少为 0.8 的条目的索引,然后使用min返回该类别中的最小值。然后,可以在方括号中指定该索引到sample.sizes中,以得到与该模拟功效对应的n值(在此情况下为 0.8050)。这些命令可以像这样嵌套:
R> minimum.n <- sample.sizes[min(which(pow>=0.8))]
R> minimum.n
[1] 43
结果表明,如果你的样本量至少为 43,在这些特定条件下进行假设检验应该是具有统计功效的(基于此次pow中随机模拟的输出)。
如果这个测试的显著性水平放宽了会怎样呢?假设你想在显著性水平为α = 0.05 而非 0.01 的情况下进行测试(仍然是在μ[0] = 0,μ[A] = 0.6,σ = 1.2 的条件下进行上尾检验)。如果你再次查看图 18-4,这个变化意味着垂直线(临界值)会向左移动,从而减少β并增加功效。因此,这表明你需要比之前更小的样本量,换句话说,在α增加时,进行统计上有力的测试时,n < 43。
为了模拟这个情况,使用相同范围的样本量并将结果功效存储在pow2中,查看以下内容:
R> pow2 <- power.tester(nvec=sample.sizes,
mu0=0,muA=0.6,sigma=1.2,alpha=0.05,ITERATIONS=5000)
|====================================================================| 100%
R> minimum.n2 <- sample.sizes[min(which(pow2>0.8))]
R> minimum.n2
[1] 27
这个结果表明,至少需要 27 个样本量,这是相比于α = 0.01 时所需的 43 个样本量的显著减少。然而,放宽α意味着增加犯第一类错误的风险!
功效曲线
为了比较,你可以通过使用pow和pow2绘制你的模拟功效,生成一种功效曲线,使用以下代码:
R> plot(sample.sizes,pow,xlab="sample size n",ylab="simulated power")
R> points(sample.sizes,pow2,col="gray")
R> abline(h=0.8,lty=2)
R> abline(v=c(minimum.n,minimum.n2),lty=3,col=c("black","gray"))
R> legend("bottomright",legend=c("alpha=0.01","alpha=0.05"),
col=c("black","gray"),pch=1)
我的特定图像见图 18-5。一条水平线标出了 0.8 的功效值,垂直线标出了在minimum.n和minimum.n2中存储的最小样本量值。最后,添加了图例来参考每条曲线的α值。

图 18-5:单一样本均值上尾假设检验的模拟功效曲线
曲线本身显示了你所期望的情况——随着样本量的增加,检出功效也增加。你还可以注意到,当功效接近 1 时,曲线趋于平稳,这通常是功效曲线的典型特征。对于α = 0.05,曲线几乎始终位于α = 0.01 曲线之上,尽管当n超过 75 左右时,差异变得微不足道。
之前关于错误和功效的讨论突出了在解释即便是最基础的统计检验结果时需要小心。p值仅仅是一个概率,因此,无论它在任何情况下有多小,都无法单独证明或反驳一个假设。应考虑与假设检验质量相关的问题(无论是参数性检验还是非参数性检验),尽管在实践中这可能是一个挑战。然而,了解 I 型和 II 型错误,以及统计功效的概念,对于任何正式统计检验程序的实施和评估都极为有用。
练习 18.6
-
在这个练习中,您需要写出来自练习 18.5(b)中的
typeII.mean函数。使用该函数,修改power.tester,使得新函数power.mean调用typeII.mean,而不是调用typeII.tester。-
确认假设检验 H[0] : μ = 10; H[A] : μ ≠ 10,且μ[A] = 10.5,σ = 0.9,α = 0.01,n = 50 的检验功效大约为 88%。
-
记得第 18.2.1 节中关于 80 克零食包净重的假设检验,基于
snack向量中提供的n = 44 个观察值。假设如下:H[0] : μ = 80
H[A] : μ < 80
如果真实均值是μ[A] = 78.5 克,且真实的体重标准差是σ = 3.1 克,使用
power.mean来判断该检验是否在统计上有效,假设α = 0.05。如果α = 0.01,您的答案是否会改变?
-
-
继续使用零食假设检验,利用文本中的
sample.sizes向量,确定使用α = 0.05 和α = 0.01 时,统计学上有效的最低样本量。绘制显示两条功效曲线的图表。
本章中的重要代码
| 函数/操作符 | 简要描述 | 首次出现 |
|---|---|---|
t.test |
单样本和双样本t检验 | 第 18.2.1 节, 第 391 页 |
prop.test |
单样本和双样本Z检验 | 第 18.3.1 节, 第 405 页 |
pchisq |
卡方累积分布问题 | 第 18.4.1 节, 第 414 页 |
chisq.test |
卡方分布/独立性检验 | 第 18.4.1 节, 第 414 页 |
rowSums |
矩阵行总和 | 第 18.4.2 节, 第 417 页 |
colSums |
矩阵列总和 | 第 18.4.2 节, 第 417 页 |
第十九章:19
方差分析

方差分析(ANOVA),其最简单的形式,用于比较多个均值是否相等的检验。从这个意义上讲,它是对比两个均值的假设检验的直接扩展。这里有一个连续变量,通过它计算出感兴趣的均值,且至少有一个分类变量告诉你如何为这些均值定义组。在本章中,你将探索关于方差分析的相关概念,首先看看通过一个分类变量(单向 ANOVA)划分组的均值比较,然后再看看通过多个分类变量(多因素 ANOVA)划分组的均值比较。
19.1 单向 ANOVA
最简单版本的方差分析(ANOVA)被称为单因素或单向分析。简而言之,单向 ANOVA 用于检验两个或更多均值是否相等。这些均值通过一个分类的组或因子变量来划分。方差分析通常用于分析实验数据,以评估干预措施的影响。例如,你可能有兴趣比较内置chickwts数据集中,按照它们所喂食的不同食物类型划分的小鸡的平均体重。
19.1.1 假设与诊断检查
假设你有一个分类的名义型变量,它将总共N个数值型观测分为k个不同的组,其中k ≥ 2。你想要统计地比较这k个组的均值,μ[1],...,μ[k],看看它们是否可以声称是相等的。标准的假设如下:
H[0] : μ[1] = μ[2] = ... = μ[k]
H[A] : μ[1], μ[2], ... , μ[k] 并不相等
(或者,至少有一个均值不同)。
实际上,当k = 2 时,两样本t检验等同于方差分析;因此,方差分析最常用于k > 2 的情况。
为了使基本的单向 ANOVA 测试结果被认为是可靠的,以下假设需要满足:
独立性 组成k组的样本必须相互独立,每个组中的观测值必须是独立且同分布的(iid)。
正态性 每个组中的观测值应该服从正态分布,或者至少是大致如此。
方差齐性 每个组中的观测值方差应该相等,或者至少是大致相等的。
如果方差齐性或正态性假设被违反,这并不意味着你的结果完全没有价值,但会影响检测均值差异的整体有效性(请参阅第 18.5.4 节关于统计功效的讨论)。在使用 ANOVA 之前,评估这些假设的有效性始终是个好主意;我将在接下来的示例中非正式地进行这种评估。
另外值得注意的是,进行检验时,组内观察值数量不必完全相等(此时称为不平衡)。然而,组不平衡会使检验对方差相等性和正态性假设不成立时产生潜在不利影响时更加敏感。
让我们回到chickwts数据作为本例——这是根据* k * = 6 种不同饲料的小鸡体重数据。你希望比较不同饲料类型下的均值体重,看看它们是否相等。使用table来总结六个样本的大小,使用tapply(例如参见第 13.2.1 节)来获取每个组的均值,如下所示:
R> table(chickwts$feed)
casein horsebean linseed meatmeal soybean sunflower
12 10 12 11 14 12
R> chick.means <- tapply(chickwts$weight,INDEX=chickwts$feed,FUN=mean)
R> chick.means
casein horsebean linseed meatmeal soybean sunflower
323.5833 160.2000 218.7500 276.9091 246.4286 328.9167
你在第 14.3.2 节中的技能使你能够生成并排显示的箱形图,比较不同喂养方式下的小鸡体重分布。接下来的两行代码将生成图 19-1 左侧的图表:
R> boxplot(chickwts$weight~chickwts$feed)
R> points(1:6,chick.means,pch=4,cex=1.5)

图 19-1:探索 chickwts 数据。左图:按饲料类型划分的小鸡体重并排箱形图,均值由 ×标出。右图:每个饲料组的均值中心化数据的正态 QQ 图。
由于箱形图显示的是中位数而不是均值,第二行代码通过points将每个箱形图的喂养特定均值(存储在你刚刚创建的chick.means对象中)添加到箱形图中。
检查图 19-1 左侧的图表,显然看起来均值体重有所不同。然而,这种明显的差异是否具有统计学意义呢?为了弄清楚这一点,本例的方差分析(ANOVA)测试涉及以下假设:
H[0] : μ[casein] = μ[horsebean] = μ[linseed] = μ[meatmeal] = μ[soybean] = μ[sunflower]
H[A] : 均值不全相等。
假设数据独立,在执行测试之前,首先必须检查其他假设是否有效。为了检查方差是否相等,你可以使用与两样本t检验中相同的非正式经验法则。也就是说,如果最大样本标准差与最小样本标准差之比小于 2,就可以假设方差相等。对于小鸡体重数据,以下代码将帮助你进行这一检查:
R> chick.sds <- tapply(chickwts$weight,INDEX=chickwts$feed,FUN=sd)
R> chick.sds
casein horsebean linseed meatmeal soybean sunflower
64.43384 38.62584 52.23570 64.90062 54.12907 48.83638
R> max(chick.sds)/min(chick.sds)
[1] 1.680238
这个非正式结果表明,做出这个假设是合理的。
接下来,考虑原始观测值的正态性假设。在许多真实数据的例子中,这可能很难确定。然而,至少值得检查直方图和 QQ 图,寻找非正态性的迹象。你已经在第 16.2.2 节中检查了所有 71 个权重的直方图和 QQ 图,但对于方差分析(ANOVA),你需要针对观测值的分组进行此操作(也就是说,不仅仅是对所有权重的“总体”进行检查,而不考虑分组)。
为了对chickwts数据进行此操作,你需要首先通过各自的样本均值均值中心化每个重量。你可以通过取原始的体重向量并从中减去chick.means向量来做到这一点,但首先你必须重新排列并复制后者的元素,以使其与前者的元素对应。这可以通过对表示饲料类型的因子向量使用as.numeric来完成,从而获得每条记录在原始数据框中chickwts$feed水平的数值。当将该数值向量通过方括号传递给chick.means时,你会得到正确的组均值与每个观察值匹配。作为练习,你可以检查创建以下chick.meancen对象所涉及的所有元素,以确保你理解其中的过程:
R> chick.meancen <- chickwts$weight-chick.means[as.numeric(chickwts$feed)]
在当前分析的背景下,这些按组均值中心化的值也被称为残差,这是你在接下来的几章中研究回归方法时经常会遇到的术语。
现在,你可以使用残差评估整体观察值的正态性。要检查正态 QQ 图,相关的函数是qqnorm和qqline,你在第 16.2.2 节中首次遇到这两个函数。以下两行代码生成图 19-1 右侧的图像。
R> qqnorm(chick.meancen,main="Normal QQ plot of residuals")
R> qqline(chick.meancen)
基于这个图(绘制点与完美直线的接近程度),假设这些数据呈正态分布似乎是合理的,特别是当与相同样本大小的生成正态数据的 QQ 图进行比较时(一个例子在图 16-9 的左侧,第 355 页提供了说明)。
调查任何所需假设的有效性被称为诊断检查。如果你想对 ANOVA 进行更严格的诊断检查,其他的视觉诊断可能包括检查按组划分的 QQ 图(你将在第 19.3 节的示例中进行此操作)或绘制每个组的样本标准差与对应样本均值的关系。实际上,也有一些通用的正态性假设检验(例如 Shapiro-Wilk 检验或 Anderson-Darling 检验——你将在第 22.3.2 节中看到前者的使用),以及方差齐性检验(例如 Levene 检验),但在这个例子中,我将坚持使用基本的经验法则和视觉检查。
19.1.2 单因素方差分析表构建
回到图 19-1 的左侧,记住,目标是统计地评估标记为×的均值是否相等。因此,这项任务需要你考虑的不仅是每个k样本内部的变异性,还需要考虑样本间的变异性;这就是为什么这个检验被称为方差分析。
测试的过程首先通过计算与总体变异性相关的各种指标,然后计算组内和组间的变异性。这些数字涉及平方和量及其相关的自由度值。所有这些最终汇总成一个单一的检验统计量和p-值,针对上述假设。这些成分通常以表格的形式呈现,定义如下。
设 x[1], ... , x[N] 代表所有的 N 次观察,无论是哪个组;设 x[1][(][j][)], ... , xnj 表示第 j 组中的特定组观察,其中 j = 1, ... , k,且 n[1] + ... + n[k] = N。定义所有观察的“总体均值”为
。然后构建 ANOVA 表,其中 SS 代表平方和,df 代表自由度,MS 代表均方,F表示F检验统计量,p表示p-值。
| df | SS | MS | F | p | |
|---|---|---|---|---|---|
| 总体 | 1 | (1) | |||
| 组(或“因素”) | k − 1 | (2) | (5) | (5)÷(6) | p-值 |
| 误差(或“残差”) | N − k | (3) | (6) | ||
| 总和 | N | (4) |
你可以使用这些公式来计算数值:
-
Nx̄²
-
![image]()
-
(4)–(2)–(1)
-
![image]()
-
(2) ÷ (k – 1)
-
(3) ÷ (N – k)
假设有三个输入来源,它们构成了观察数据,当加在一起时,结果会得到总和行。让我们更详细地思考这些:
总体行 这与数据整体所在的尺度有关。它不会影响假设检验的结果(因为你只关心均值之间的相对差异),有时会从表格中移除,从而相应影响总和值。
组行/因素行 这与各个感兴趣组的数据相关,从而解释了组间变异性。
误差行/残差行 这解释了每个组的估计均值的随机偏差,从而解释了组内变异性。
总和行 这表示原始数据,基于前三个元素。通过差分计算误差 SS。
三个输入源每个都在第一列有一个对应的自由度(df)值,并且在第二列附有一个与 df 相关的平方和(SS)值。组间和组内的变异性通过将平方和(SS)除以自由度(df)来平均,从而得到这两个项目的均方(MS)值。测试统计量F是通过将均方组(MSG)效应除以均方误差(MSE)效应来计算的。这个测试统计量遵循F-分布(参见第 16.2.5 节),该分布需要一对自由度值,按 df[1](表示组别自由度,k−1)和 df[2](表示误差自由度,N−k)的顺序排列。像卡方分布一样,F-分布是单向的,并且p-值是从测试统计量F的上尾区域中获得的。
19.1.3 使用 aov 函数构建 ANOVA 表
正如你可能预期的那样,R 允许你使用内置的aov函数轻松构建小鸡体重测试的 ANOVA 表,方法如下:
R> chick.anova <- aov(weight~feed,data=chickwts)
然后,使用summary命令将表格打印到控制台屏幕上。
R> summary(chick.anova)
Df Sum Sq Mean Sq F value Pr(>F)
feed 5 231129 46226 15.37 5.94e-10 ***
Residuals 65 195556 3009
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
这里有几个需要说明的地方。请注意,你使用公式表示法weight~feed来指定感兴趣的测量变量体重,它由感兴趣的类别-名义变量喂养类型来建模。在这种情况下,变量名weight和feed不需要以chickwts$为前缀,因为可选的data参数已经传入了相关数据框。
请记住,来自第 14.3.2 节的内容,对于表达式weight~feed中的符号表示法,感兴趣的“结果”变量必须始终出现在~的左侧(这种符号表示法将在第二十章到第二十二章中特别重要)。
要实际查看表格,必须对调用aov函数后返回的对象应用summary命令。R 会省略第一行和最后一行(总体和总计),因为这些行与计算p-值没有直接关系。除此之外,很容易识别出feed行指的是组别行,而Residuals行指的是误差行。
注意
默认情况下,R 会在模型基础的summary输出中添加显著性星号。这些星号表示显著性区间,星号的数量会随着p-值的减小而增加,当p-值低于 0.1 的临界值时。这对于你检查多个p-值汇总的更复杂分析可能很有用,尽管并非每个人都喜欢这个功能。如果你愿意,可以通过在提示符下输入options(show.signif.stars=FALSE)来关闭此功能,或者你也可以直接在调用summary时,通过设置附加参数signif.stars=FALSE来关闭此功能。在本书中,我将保留这一功能。*
从这个例子的 ANOVA 内容中,你可以快速确认计算结果。注意,MSE(均方误差)为 3009,它被定义为误差平方和(Error SS)除以误差自由度(Error df)。实际上,在 R 中,手动计算也能得出相同的结果(表格输出已四舍五入到最接近的整数)。
R> 195556/65
[1] 3008.554
你可以通过使用前面提到的相关公式,确认表格输出中的所有其他结果。
解释基于 ANOVA 的假设检验遵循与其他任何检验相同的规则。通过理解p-值为“如果原假设 H[0]为真,你观察到当前样本统计量或更极端的情况的概率”,一个小的p-值表明反对原假设的证据。在当前的例子中,一个极小的p-值提供了强有力的证据,反对均值相同的原假设,即不同饮食对小鸡体重的影响相同。换句话说,你拒绝原假设 H[0],支持备择假设 H[A];后者表示存在差异。
与卡方检验类似,一元 ANOVA 中的原假设被拒绝并不能告诉你差异到底在哪里,仅仅表明有证据表明存在差异。需要对各个组的数据进行进一步审查,以确定存在问题的均值。在最简单的层面上,你可以回到成对的两样本t-检验,在这种情况下,你也可以使用 ANOVA 表中的 MSE 作为合并方差的估计。如果方差相等的假设成立,则这种替代是有效的,这一步是有益的,因为相应的基于t的抽样分布将利用误差自由度(Error df)(如果自由度仅基于两个特定组的样本大小,通常情况下该自由度会较低)。
习题 19.1
考虑以下数据:
| 遗址 I | 遗址 II | 遗址 III | 遗址 IV |
|---|---|---|---|
| 93 | 85 | 100 | 96 |
| 120 | 45 | 75 | 58 |
| 65 | 80 | 65 | 95 |
| 105 | 28 | 40 | 90 |
| 115 | 75 | 73 | 65 |
| 82 | 70 | 65 | 80 |
| 99 | 65 | 50 | 85 |
| 87 | 55 | 30 | 95 |
| 100 | 50 | 45 | 82 |
| 90 | 40 | 50 | |
| 78 | 45 | ||
| 95 | 55 | ||
| 93 | |||
| 88 | |||
| 110 |
这些图表提供了在新墨西哥州四个遗址上发现重要考古学发现的深度(单位:厘米)(参见 Woosley 和 Mcintyre, 1996)。将这些数据存储在你的 R 工作区,其中一个向量包含深度,另一个向量包含每个观察点的遗址。
-
生成按组分割的深度箱型图,并使用额外的点来标出样本均值的位置。
-
假设独立性成立,执行正态性和方差齐性的诊断检查。
-
执行并得出一元 ANOVA 检验的结论,以确认均值之间是否存在差异。
在第 14.4 节中,你查看了一个数据集,提供了三种鸢尾花物种的花瓣和花萼大小的测量数据。该数据集在 R 中可以通过iris访问。
-
基于正态性和方差齐性的诊断检查,决定哪四个结果测量(萼片长度/宽度和花瓣长度/宽度)适合方差分析(使用物种作为组变量)。
-
对任何合适的测量变量执行单因素方差分析。
19.2 二因素方差分析
在许多研究中,您感兴趣的数值结果变量将不仅由一个分组变量进行分类。在这些情况下,您将使用多因素方差分析而不是单因素方差分析。这种技术直接通过使用的分组变量数量来指代,二因素和三因素方差分析是其下一个和最常见的扩展。
增加分组变量的数量会使情况变得复杂一些——仅对每个变量分别执行单因素方差分析是不够的。在处理多个分类分组因素时,您必须考虑每个因素对数值结果的主效应,同时考虑其他分组因素的存在。但这还不是全部。同样重要的是额外研究交互效应的概念;如果存在交互效应,则表明一个分组变量对感兴趣的结果的影响(由其主效应指定)会根据其他分组变量的水平变化而变化。
19.2.1 一组假设
对于此说明,请用 O 表示您的数值结果变量,用 G[1] 和 G[2] 表示您的两个分组变量。在二因素方差分析中,假设应设置如下:
H[0]:G[1] 对 O 的均值没有主要(边际)影响。
G[2] 对 O 的均值没有主要(边际)影响。
G[1] 与 G[2] 对 O 的均值没有交互效应。
H[A]:在 H[0]中,各个声明分别不正确。
您可以从这些一般性假设中看出,现在您必须为这三个组成部分中的每一个获取一个p-值。
例如,让我们使用内置的 warpbreaks 数据框架(Tippett, 1950),它提供了观察到的 54 根相同长度的纱线中的“断裂”缺陷数量(列 breaks)。每根纱线根据两个分类变量进行分类:wool(纱线类型,具有 A 和 B 两个水平)和 tension(施加在该纱线上的张力水平,分别为 L、M 或 H,分别代表低、中、高)。使用 tapply,您可以查看每个分类的平均断裂数。
R> tapply(warpbreaks$breaks,INDEX=list(warpbreaks$wool,warpbreaks$tension),
FUN=mean)
L M H
A 44.55556 24.00000 24.55556
B 28.22222 28.77778 18.77778
您可以将多个分组变量作为列表的单独成员提供给 INDEX 参数(此参数给定的任何因子向量应与指定感兴趣的数据的第一个参数的长度相同)。结果作为矩阵返回给两个分组变量,作为三个分组变量的 3D 数组返回给,依此类推。
然而,对于某些分析,你可能需要以不同的格式提供之前的信息。aggregate函数与tapply类似,但它返回的是一个数据框,结果按照指定的分组变量以堆叠格式呈现(与tapply返回的数组不同)。它的调用方式基本相同。第一个参数是你感兴趣的数据向量。第二个参数by应是所需分组变量的列表,而在FUN中,你指定要对每个子集操作的函数。
R> wb.means <- aggregate(warpbreaks$breaks,
by=list(warpbreaks$wool,warpbreaks$tension),FUN=mean)
R> wb.means
Group.1 Group.2 x
1 A L 44.55556
2 B L 28.22222
3 A M 24.00000
4 B M 28.77778
5 A H 24.55556
6 B H 18.77778
在这里,我将对aggregate的调用结果存储为对象wb.means以供后续使用。
19.2.2 主效应和交互效应
我之前提到过,你可以分别对每个分组变量执行单因素方差分析,但通常这不是一个好主意。我现在将使用warpbreaks数据演示这一点(对相关诊断的快速检查显示没有明显的原因需要担忧):
R> summary(aov(breaks~wool,data=warpbreaks))
Df Sum Sq Mean Sq F value Pr(>F)
wool 1 451 450.7 2.668 0.108
Residuals 52 8782 168.9
R> summary(aov(breaks~tension,data=warpbreaks))
Df Sum Sq Mean Sq F value Pr(>F)
tension 2 2034 1017.1 7.206 0.00175 **
Residuals 51 7199 141.1
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
该输出告诉你,如果忽略tension,没有证据表明仅根据wool类型对经纱断裂的平均数量有任何差异(p-值为 0.108)。然而,如果忽略wool,则有证据表明仅根据tension存在经纱断裂的差异。
这里的问题在于,忽略了其中一个变量后,你就失去了在更细致的层面上检测差异(或更一般地说,统计关系)的能力。例如,虽然单独看wool类型似乎对经纱断裂的平均数量没有显著影响,但你无法知道如果仅仅在某一特定的tension水平下查看wool类型时,情况是否会不同。
相反,你使用双因素方差分析(two-way ANOVA)来研究这个问题。以下代码执行基于两个分组变量主效应的双因素方差分析,针对的是经纱断裂数据:
R> summary(aov(breaks~wool+tension,data=warpbreaks))
Df Sum Sq Mean Sq F value Pr(>F)
wool 1 451 450.7 3.339 0.07361 .
tension 2 2034 1017.1 7.537 0.00138 **
Residuals 50 6748 135.0
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
看一下公式。指定wool+tension在结果变量右侧,~符号允许你同时考虑这两个分组变量。结果显示,现在附加在每个分组变量上的p-值略有下降;实际上,wool的p-值约为 0.073,接近传统的显著性水平α = 0.05。为了解释结果,你保持一个分组变量不变——如果你只关注某一种类型的羊毛,仍然有统计学显著的证据表明不同tension水平之间的经纱断裂均值存在差异。如果你只关注某一特定的tension水平,考虑到两种wool类型的差异证据有所增加,但仍然不显著(假设上述的α = 0.05)。
仅考虑主效应仍然存在一定的局限性。虽然先前的分析表明在不同类别变量的各个水平之间,结果存在差异,但它并没有考虑这样一种可能性:在保持另一个变量不变时,哪一个tension或wool水平的变化可能会进一步影响经线断裂的均值差异。这个相对微妙但重要的考虑被称为交互作用。具体而言,如果在tension和wool之间对于经线断裂存在交互效应,那么这意味着均值差异的幅度和/或方向在两个分组因子的不同水平下是不相同的。
为了考虑交互作用,你需要对双因素方差分析模型代码做出小的调整。
R> summary(aov(breaks~wool+tension+wool:tension,data=warpbreaks))
Df Sum Sq Mean Sq F value Pr(>F)
wool 1 451 450.7 3.765 0.058213 .
tension 2 2034 1017.1 8.498 0.000693 ***
wool:tension 2 1003 501.4 4.189 0.021044 *
Residuals 48 5745 119.7
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
你可以明确指定交互作用作为主要效应模型公式的加上符号wool:tension,其中两个分组变量通过:分隔。(注意,在这种设置下,:运算符与第 2.3.2 节中首次讨论的创建整数序列的快捷方式无关。)
从方差分析表的输出可以看到,统计上存在交互效应的证据;也就是说,均值差异的本质依赖于因子水平本身,即使这个证据相对较弱。当然,约为 0.021 的p-值只告诉你总体上可能存在交互作用,但并没有提供交互作用的具体特征。
为了帮助理解,你可以通过交互作用图更加详细地解释这样的二项交互作用效应,R 中的interaction.plot可以提供此图。
R> interaction.plot(x.factor=wb.means[,2],trace.factor=wb.means[,1],
response=wb.means$x,trace.label="wool",
xlab="tension",ylab="mean warp breaks")
当调用interaction.plot时,需要将结果的均值传递给参数response,并将提供每个因子不同水平的向量分别传递给参数x.factor(表示水平从左到右变化的水平轴变量)和trace.factor(每个水平将产生不同的线,并在自动生成的图例中进行引用;该图例的标题会传递给trace.label)。分组变量的顺序并不重要;图形的外观会相应变化,但你的解释(应该是!)会保持一致。结果显示在图 19-2 中。
双向交互图显示了纵轴上的结果变量,并根据两个分组变量的不同水平对记录的均值进行分组。这让你可以检查改变分组变量水平对结果的潜在影响。通常,当线条(或其中的某些部分)不平行时,表明可能存在交互作用。绘制点之间的垂直分隔线表示分组变量的个别主效应。
事实证明,aggregate函数返回的列非常适合用来绘制interaction.plot。像往常一样,你可以指定常见的图形参数,比如在第 7.2 节中首次遇到的那些,来控制图表和轴注释的特定功能。在图 19-2 中,你指定了x.factor应该是wb.means矩阵的第二列,这意味着张力水平在水平轴上变化。trace.factor是wool的类型,所以这里只有两条对应于A和B的不同水平的线。response是wb.means矩阵的第三列,可以通过$x提取(查看wb.means对象;你会看到,在调用aggregate之后,包含感兴趣结果的列默认标记为x)。

图 19-2:完整双向 ANOVA 模型的交互作用图,用于 warpbreaks 数据集
考虑到图 19-2 中图表的实际外观,的确可以看到,如果张力较低,羊毛类型A的平均断裂数较高,但当张力变为中等时,B的点估计值高于A。当张力变为高时,A的平均断裂数再次高于B,尽管在高张力下,A和B之间的差异远不如低张力时那么大。(但请注意,交互作用图没有显示任何标准误差的度量,因此你必须记住,所有均值的点估计都存在变异性。)
交互作用显然不是多因素方差分析(ANOVA)特有的概念;它们在许多不同类型的统计模型中都是一个重要的考虑因素。目前,了解交互作用的基本概念就足够了。
19.3 Kruskal-Wallis 检验
在比较多个均值时,可能会遇到你不愿意假设正态性,或者在诊断检查中发现正态性假设无效的情况。在这种情况下,你可以使用Kruskal-Wallis 检验,这是一个放宽正态性要求的单因素 ANOVA 替代方法。该方法检验每个分组因素水平中测量值的“分布是否相等”。如果你假设这些组的方差相等,那么你可以将此检验视为比较多个中位数而非均值的检验。
测试的假设因此相应地发生变化。
H[0] : 各组的中位数相等。
H[A] : 各组的中位数不全相等
(或者,至少有一个组的中位数不同)。
Kruskal-Wallis 检验是一种非参数方法,因为它不依赖于标准化参数分布的分位数(换句话说,即正态分布)或其任何函数。与 ANOVA 是两样本t检验的推广一样,Kruskal-Wallis ANOVA 是 Mann-Whitney 检验对两个中位数的推广。它也被称为 Kruskal-Wallis秩和检验,且你使用卡方分布来计算p值。
将注意力转向位于MASS包中的数据框survey。这些数据记录了来自南澳大利亚阿德莱德大学一门统计学课程的 237 名大一本科生的特定特征。首先通过调用library("MASS")加载所需的包,然后在提示符下输入?survey。你可以阅读帮助文件以了解数据框中包含了哪些变量。
假设你有兴趣查看学生的年龄(Age)是否在四个吸烟类别(Smoke)之间存在差异。通过检查相关的并排箱线图和残差的正态 QQ 图(以每组为基础的均值中心化观察值),可以看出,简单的一元方差分析(ANOVA)不一定是一个好主意。以下代码(模拟了你在第 19.1.1 节中看到的步骤)生成了图 19-3 中的两幅图,表明正态性存在疑问:
R> boxplot(Age~Smoke,data=survey)
R> age.means <- tapply(survey$Age,survey$Smoke,mean)
R> age.meancen <- survey$Age-age.means[as.numeric(survey$Smoke)]
R> qqnorm(age.meancen,main="Normal QQ plot of residuals")
R> qqline(age.meancen)
由于可能存在正态性假设的违反,你因此可以选择使用 Kruskal-Wallis 检验,而不是参数化的 ANOVA。通过方差齐性检验可以进一步支持这一点,最大和最小组的标准差比率明显小于 2。
R> tapply(survey$Age,survey$Smoke,sd)
Heavy Never Occas Regul
6.332628 6.675257 5.861992 5.408822

图 19-3:并排箱线图(左)和残差的正态 QQ 图(右),根据吸烟状态划分的学生年龄观察值
在 R 中,Kruskal-Wallis 检验可以通过kruskal.test来执行。
R> kruskal.test(Age~Smoke,data=survey)
Kruskal-Wallis rank sum test
data: Age by Smoke
Kruskal-Wallis chi-squared = 3.9262, df = 3, p-value = 0.2695
这个检验的语法与aov相同。正如你可能从图 19-3 中猜到的那样,较大的* p *值表明没有反驳原假设的证据,原假设认为中位数都相等。换句话说,在四个吸烟类别的学生之间,似乎没有明显的年龄差异。
练习 19.2
再次提取quakes数据框,它描述了在斐济海岸附近发生的 1,000 次地震事件的地点、震级、深度以及检测到这些地震事件的观测站数量。
-
使用
cut(参见第 4.3.3 节)创建一个新的因子向量,根据以下三个类别定义每个事件的深度:(0,200]、(200,400]和(400,680]。 -
决定是使用单因素方差分析还是克鲁斯克尔-瓦利斯检验来比较根据(a)中三个类别拆分的检测站数量的分布,更为合适。
-
执行你在(b)中选择的检验(假设* α * = 0.01 的显著性水平),并得出结论。
如果你还没有在当前 R 会话中加载,使用library("MASS")加载MASS包。这个包包括现成的Cars93数据框,包含了 1993 年在美国出售的 93 辆汽车的详细数据(Lock, 1993; Venables 和 Ripley, 2002)。
-
使用
aggregate计算 93 辆汽车的平均长度,按两个分类变量拆分:AirBags(气囊类型—级别有Driver & Passenger、Driver only和None),以及Man.trans.avail(是否提供手动变速箱—级别为Yes和No)。 -
使用(d)中的结果生成一个交互图。是否可以观察到
AirBags与Man.trans.avail在这些汽车的平均长度上存在交互效应(如果只考虑这些变量的话)? -
为平均长度拟合一个完整的双因素方差分析模型,根据两个分组变量(假设所有相关假设均满足)。交互效应在统计上显著吗?是否有任何主效应的证据?
本章重要代码
| 函数/运算符 | 简要描述 | 首次出现 |
|---|---|---|
aov |
生成方差分析表 | 第 19.1.3 节, 第 440 页 |
aggregate |
按因子堆叠统计数据 | 第 19.2.1 节, 第 444 页 |
interaction.plot |
双因素交互图 | 第 19.2.2 节, 第 446 页 |
kruskal.test |
克鲁斯克尔-瓦利斯检验 | 第 19.3 节, 第 449 页 |
第二十章:20
简单线性回归

尽管单个统计量的简单比较测试本身有用,但你通常希望从数据中获取更多信息。在本章中,你将了解线性回归模型:一套用于精确评估变量之间如何相互关系的方法。
简单线性回归模型描述了某个特定变量(称为解释变量)对一个连续型结果变量(称为响应变量)可能产生的影响。解释变量可以是连续的、离散的或分类的,但为了引入关键概念,我将在本章的前几节专注于连续型解释变量。然后,我会讲解当解释变量是分类时,模型表示的变化。
20.1 线性关系示例
作为开始的例子,我们继续使用第 19.3 节中的数据,并稍微深入查看学生调查数据(在MASS包中的survey数据框)。如果你还没有这样做,可以在加载所需包(调用library("MASS"))后,阅读帮助文件?survey了解关于变量的详细信息。
将学生的身高绘制在y轴上,将他们的写字手跨度绘制在x轴上。
R> plot(survey$Height~survey$Wr.Hnd,xlab="Writing handspan (cm)",
ylab="Height (cm)")
图 20-1 显示了结果。

图 20-1:一张显示身高与写字手跨度的散点图,数据来自一组大一统计学学生
请注意,调用plot时使用了公式表示法(也称为符号表示法)来指定“身高对手跨度”。你也可以使用坐标向量形式的(x,y)来生成相同的散点图,即plot(survey$Wr.Hnd,survey$Height,...),但我在这里使用符号表示法,因为它更好地反映了你稍后如何拟合线性模型。
正如你所预期的那样,学生的手跨度与身高之间存在正相关关系。这个关系看起来是线性的。为了评估线性关系的强度(参见第 13.2.5 节),你可以找到估计的相关系数。
R> cor(survey$Wr.Hnd,survey$Height,use="complete.obs")
[1] 0.6009909
尽管数据框中有 237 条记录,但图表实际上并没有显示 237 个点。这是因为存在缺失的观测值(编码为NA;见第 6.1.3 节)。默认情况下,R 在绘制这样的图表时会删除任何“不完整”的数据对。为了找出被删除的有问题的观测值的数量,可以使用简短形式的逻辑操作符|(见第 4.1.3 节),并与is.na(见第 6.1.3 节)和which(见第 4.1.5 节)一起使用。然后使用length发现有 29 对缺失的观测值。
R> incomplete.obs <- which(is.na(survey$Height)|is.na(survey$Wr.Hnd))
R> length(incomplete.obs)
[1] 29
注意
因为在传递给相关系数函数cor的向量中存在NA值,所以你还必须指定可选参数use="complete.obs"。这意味着计算的统计量只考虑Wr.Hnd和Height向量中那些两者都不是NA的观测对。你可以把这个参数理解为与单变量摘要统计函数(如mean和sd)中的na.rm=TRUE*做的事情非常相似。
20.2 一般概念
线性回归模型的目的是提出一个函数,估算在给定另一个变量的特定值时,某个变量的均值。这些变量被称为响应变量(你试图找出其均值的“结果”变量)和解释变量(你已经有其值的“预测”变量)。
以学生调查的例子来说,你可能会问类似这样的问题:“如果学生的手掌宽度是 14.5 厘米,那么他们的身高预期是多少?”在这里,响应变量是身高,解释变量是手掌宽度。
20.2.1 模型定义
假设你想根据解释变量X的值来确定响应变量Y的值。简单线性回归模型表示响应的值可以用以下方程表达:

在方程(20.1)的左侧,符号Y|X表示“在给定X值的条件下Y的值。”
残差假设
你可以根据(20.1)式得出的结论的有效性,关键取决于对∊所做的假设,这些假设定义如下:
• 假设∊的值服从正态分布,即∊ ~ N(0,σ)。
• ∊是中心化的(即其均值为零)。
• ∊的方差,σ²,是常数。
∊项代表随机误差。换句话说,你假设响应的任何原始值都归因于给定值X的线性变化,加上或减去一些随机的、残差变动或正态分布的噪声。
参数
由β[0]表示的值称为截距,由β[1]表示的值称为斜率。它们合起来也被称为回归系数,并按以下方式进行解释:
• 截距β[0]被解释为当预测变量为零时,响应变量的预期值。
• 通常,斜率β[1]是关注的重点。它表示每增加一个单位的预测变量,平均响应的变化。当斜率为正时,回归线从左到右上升(当预测变量增大时,平均响应也增大);当斜率为负时,回归线从左到右下降(当预测变量增大时,平均响应减小)。当斜率为零时,表示预测变量对响应值没有影响。β[1]的绝对值越大(即远离零),回归线的升降越陡峭。
20.2.2 估计截距和斜率参数
目标是使用你的数据来估计回归参数,从而得到估计值
和
;这被称为拟合线性模型。在这种情况下,数据包括每个个体的n对观察值。所关注的拟合模型涉及对特定预测值x的平均响应值,记作ŷ,公式如下:

有时,会使用替代符号,如
[Y]或
[Y|X = x],在公式(20.2)的左侧,强调模型给出的响应的均值(即期望值)。为了简洁起见,许多人直接使用类似ŷ的符号,如这里所示。
令你观察到的n对数据分别记作X[i]和Y[i],其中i = 1, . . . , n。然后,简单线性回归函数的参数估计值为

其中
• x̄和ȳ分别是x[i]和y[i]的样本均值。
• s[x]和s[y]分别是x[i]和y[i]的样本标准差。
• ρ[xy]是基于数据估计的X和Y之间的相关性(见第 13.2.5 节)。
通过这种方式估计模型参数被称为最小二乘回归;稍后你会明白其原因。
20.2.3 使用 lm 拟合线性模型
在 R 语言中,命令lm会为你执行估计。例如,下面这一行手动创建了一个基于手掌跨度的平均学生身高的拟合线性模型对象,并将其存储在你的全局环境中,命名为survfit:
R> survfit <- lm(Height~Wr.Hnd,data=survey)
第一个参数是现在熟悉的response ~ predictor 公式,它指定了所需的模型。你不需要使用survey$前缀来从数据框中提取向量,因为你已经明确指示lm去查找提供给data参数的对象。
拟合的线性模型对象本身survfit在 R 中有一个特殊的类——即"lm"类。"lm"类的对象本质上可以看作是一个包含多个组件的列表,这些组件描述了该模型。稍后你将查看这些组件。
如果你在提示符下直接输入 "lm" 对象的名称,它将提供最基本的输出:重复你的调用并显示截距 (
) 和斜率 (
) 的估计值。
R> survfit
Call:
lm(formula = Height ~ Wr.Hnd, data = survey)
Coefficients:
(Intercept) Wr.Hnd
113.954 3.117
这揭示了本例的线性模型估计如下:

如果你对ŷ—方程 (20.2)—在不同的x值范围内进行计算,结果会绘制出一条直线。考虑到之前给出的截距定义,即当预测变量为零时响应变量的期望值,在当前示例中,这意味着一个手掌宽度为 0 厘米的学生的平均身高是 113.954 厘米(这可能是一个不太有用的陈述,因为解释变量为零的值没有实际意义;你将在第 20.4 节中考虑这些及相关问题)。斜率,即预测变量每增加 1 单位时平均响应的变化量,为 3.117。这表示,平均而言,每增加 1 厘米的手掌宽度,学生的身高估计会增加 3.117 厘米。
记住这一切后,再次运行该行代码,以绘制第 20.1 节中给出的原始数据,并在图 20-1 中显示,但现在使用 abline 添加拟合的回归线。到目前为止,你只使用了 abline 命令将完美的水平线和垂直线添加到现有图形,但当传递一个表示简单线性模型的 "lm" 类对象时,如 survfit,则会添加拟合的回归线。
R> abline(survfit,lwd=2)
这增加了图 20-2 中显示的略微加厚的对角线。

图 20-2:拟合到观测数据的简单线性回归线(实线,粗体)。两条虚线垂直线段提供了正(最左侧)和负(最右侧)残差的示例。
20.2.4 说明残差
当参数按这里所示使用(20.3)进行估计时,拟合的线称为最小二乘回归,因为它是最小化观测数据与拟合线之间的平均平方差的线。通过绘制观测值与拟合线之间的距离(正式称为残差),你可以更容易地理解这一概念,特别是图 20-2 中的个别观测值。
首先,让我们从 Wr.Hnd 和 Height 数据向量中提取两个特定的记录,并将结果向量命名为 obsA 和 obsB。
R> obsA <- c(survey$Wr.Hnd[197],survey$Height[197])
R> obsA
[1] 15.00 170.18
R> obsB <- c(survey$Wr.Hnd[154],survey$Height[154])
R> obsB
[1] 21.50 172.72
接下来,简要检查 survfit 对象成员的名称。
R> names(survfit)
[1] "coefficients" "residuals" "effects" "rank"
[5] "fitted.values" "assign" "qr" "df.residual"
[9] "na.action" "xlevels" "call" "terms"
[13] "model"
这些成员是自动构成拟合模型对象 "lm" 类的组件,如前所述。请注意,有一个名为 "coefficients" 的组件,其中包含截距和斜率估计值的数字向量。
你可以像在命名列表上进行成员引用那样提取这个组件(以及这里列出的任何其他组件):通过在提示符处输入survfit$coefficients。不过,在可能的情况下,从编程角度来说,使用“直接访问”函数提取这些组件是更为推荐的做法。对于"lm"对象的coefficients组件,你需要使用的函数是coef。
R> mycoefs <- coef(survfit)
R> mycoefs
(Intercept) Wr.Hnd
113.953623 3.116617
R> beta0.hat <- mycoefs[1]
R> beta1.hat <- mycoefs[2]
这里,回归系数从对象中提取出来,然后分别赋值给对象beta0.hat和beta1.hat。其他常见的直接访问函数包括resid和fitted;这两个函数分别涉及到“残差”和“拟合值”组件。
最后,我使用segments绘制图 20-2 中存在的垂直虚线。
R> segments(x0=c(obsA[1],obsB[1]),y0=beta0.hat+beta1.hat*c(obsA[1],obsB[1]),
x1=c(obsA[1],obsB[1]),y1=c(obsA[2],obsB[2]),lty=2)
请注意,这些虚线与拟合线在垂直轴位置相交,这些位置通过传递给y0的值反映了使用回归系数beta0.hat和beta1.hat后,方程(20.4)。
现在,假设有一组通过数据绘制的不同回归线(通过改变截距和斜率的值实现)。然后,对于每一条替代的回归线,假设你计算每个观察值的响应值与该回归线的拟合值之间的残差(垂直距离)。根据(20.3)估算的简单线性回归线是“最接近所有观察值”的那条线。这里的意思是,拟合的回归模型由通过由变量均值(x̄,ȳ)给出的坐标的估计线表示,它是那条使得残差的平方和最小的线。因此,像这样最小二乘估计的回归方程的另一个名字是最佳拟合线。
20.3 统计推断
回归方程的估计相对简单,但这仅仅是开始。现在你应该考虑从结果中可以推断出什么。在简单线性回归中,始终应该提出一个自然的问题:是否有统计证据支持预测变量与响应变量之间存在关系?换句话说,是否有证据表明解释变量的变化会影响平均结果?你可以通过类似于在第十七章中引入的思想来研究这个问题,那里你开始思考估计统计量中的变异性,并随后使用置信区间进行推断,在第十八章中进行了假设检验。
20.3.1 总结拟合模型
这种基于模型的推断在处理lm对象时会自动由 R 执行。对lm创建的对象使用summary函数,将为你提供比仅仅打印对象到控制台更详细的输出。此时,你将重点关注summary提供的两个方面的信息:与回归系数相关的显著性检验,以及所谓的决定系数(在输出中标记为R-squared)的解释,我将在稍后进行说明。
对当前模型对象survfit使用summary,你将看到以下内容:
R> summary(survfit)
Call:
lm(formula = Height ~ Wr.Hnd, data = survey)
Residuals:
Min 1Q Median 3Q Max
-19.7276 -5.0706 -0.8269 4.9473 25.8704
Coefficients:
Estimate Std. Error t value Pr(>|t|)
(Intercept) 113.9536 5.4416 20.94 <2e-16 ***
Wr.Hnd 3.1166 0.2888 10.79 <2e-16 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Residual standard error: 7.909 on 206 degrees of freedom
(29 observations deleted due to missingness)
Multiple R-squared: 0.3612, Adjusted R-squared: 0.3581
F-statistic: 116.5 on 1 and 206 DF, p-value: < 2.2e-16
20.3.2 回归系数显著性检验
让我们首先关注回归系数报告的方式。系数表的第一列包含截距和斜率的点估计(截距标记为“截距”,斜率则标记为数据框中预测变量的名称);表格还包括这些统计量的标准误差估计。可以证明,简单线性回归系数在使用最小二乘法估计时,遵循具有n − 2 自由度的t分布(其中n为模型拟合中使用的观测值数量)。每个参数都报告了标准化的t值和p-值。这些值表示一个双尾假设检验的结果,定义为
H[0] : β[j] = 0
H[A] : β[j] ≠ 0
其中j = 0 表示截距,j = 1 表示斜率,使用公式(20.1)中的符号。
关注预测变量的结果行。零的假设值下,H[0]的成立意味着预测变量对响应没有影响。这里关注的是协变量是否有任何影响,而不是这种影响的方向,因此 H[A]是双尾的(通过≠)。与任何假设检验一样,p-值越小,反对 H[0]的证据就越强。在此特定检验统计量下,p-值非常小(< 2 × 10^−¹⁶)(你可以使用第十八章中的公式确认:T = (3.116 − 0)/0.2888 = 10.79),因此你可以得出结论,存在强有力的证据反对预测变量对响应均值没有影响的说法。
对截距进行相同的检验,但对斜率参数β[1]的检验通常更为有趣(因为拒绝零假设的β[0]仅表示回归线与纵轴交点不为零),尤其是在观察数据不包含x = 0 的情况下,正如这里的情况。
从中可以得出结论,拟合模型表明,手掌跨度的增加与所研究人群的身高增加之间存在关联。每增加一厘米的手掌跨度,身高的平均增加约为 3.12 厘米。
你还可以使用方程式(17.2)和回归参数的抽样分布知识,计算估计值的置信区间;然而,R 再次为"lm"类对象提供了一个方便的函数,可以为你自动完成此操作。
R> confint(survfit,level=0.95)
2.5 % 97.5 %
(Intercept) 103.225178 124.682069
Wr.Hnd 2.547273 3.685961
对于confint函数,你需要将模型对象作为第一个参数,并将所需的置信水平作为level传递。这表明你应该有 95%的信心,β[1]的真实值位于 2.55 和 3.69 之间(保留两位小数)。和之前一样,排除零的原假设值反映了早先的统计显著结果。
20.3.3 决定系数
summary的输出还为你提供了Multiple R-squared和Adjusted R-squared的值,这些值尤其有趣。这两者都被称为决定系数;它们描述了响应中变异的比例,这部分变异可以归因于预测变量。
对于简单线性回归,第一项(未经调整的)度量是通过估计的相关系数的平方获得的(请参考第 13.2.5 节)。对于学生身高的例子,首先将Wr.Hnd和Height之间的估计相关性存储为rho.xy,然后对其平方:
R> rho.xy <- cor(survey$Wr.Hnd,survey$Height,use="complete.obs")
R> rho.xy²
[1] 0.3611901
你将得到与Multiple R-squared值相同的结果(通常用数学符号表示为R²)。这告诉你,大约 36.1%的学生身高变异可以归因于手跨度。
调整后的度量是一种替代估计,考虑了需要估计的参数数量。调整后的度量通常只有在你使用决定系数来评估拟合模型的整体“质量”,即平衡拟合优度与复杂性时,才显得重要。我将在第二十二章中讲解这一点,所以目前不会再深入探讨。
20.3.4 其他摘要输出
模型对象的summary会为你提供更有用的信息。“残差标准误”是∊项的估计标准误(换句话说,就是∊的估计方差的平方根,即σ²);下面还报告了任何缺失值。(这里“因缺失而删除的 29 对观测值”与第 20.1 节中确定的不完整观测值的数量一致。)
输出还提供了残差距离的五数总结(第 13.2.3 节),我将在第 22.3 节中进一步讲解。最终结果中,你将得到使用F分布进行的某个假设检验。这是一个关于预测变量对响应变量影响的全局检验;这一点将在第 21.3.5 节中与多元线性回归一起探讨。
你可以直接访问summary提供的所有输出,作为单独的 R 对象,而不必从整个打印的摘要中读取它们。就像names(survfit)为你提供了指示survfit独立对象内容的线索一样,以下代码会为你提供在使用summary处理survfit后可以访问的所有组件的名称。
R> names(summary(survfit))
[1] "call" "terms" "residuals" "coefficients"
[5] "aliased" "sigma" "df" "r.squared"
[9] "adj.r.squared" "fstatistic" "cov.unscaled" "na.action"
将大多数组件与打印的summary输出进行匹配是相当简单的,而且它们可以像往常一样使用美元运算符提取。例如,残差标准误差可以直接通过以下方式检索:
R> summary(survfit)$sigma
[1] 7.90878
该内容的更多细节可以在?summary.lm帮助文件中找到。
20.4 预测
为了总结这些线性回归的初步细节,现在你将看到如何将拟合模型用于预测目的。拟合统计模型的能力意味着你不仅可以理解并量化数据中关系的性质(比如学生示例中每增加 1 厘米手掌跨度,均值身高增加 3.1166 厘米的估算值),还可以预测感兴趣结果的值,即使你在原始数据集中没有实际观察到任何解释变量的值。不过,像任何统计量一样,总是需要伴随点估计或预测值提供一个分布度量。
20.4.1 置信区间还是预测区间?
使用拟合的简单线性模型,你可以计算在给定解释变量值的条件下,均值响应的点估计。为此,你只需要将感兴趣的x值代入(拟合的模型方程)。像这样的统计量总是会受到变化的影响,因此,就像在前面章节中讨论的样本统计量一样,你使用均值响应的置信区间(CI)来衡量这种不确定性。
假设已对n个观测值拟合了一个简单的线性回归线,并计算了给定x值时,均值响应的!image 百分比置信区间。

在这里,你通过减法得到下限,通过加法得到上限。
这里,ŷ是拟合值(来自回归线),对应于x;t[(][1][−] [/][2][,][n][−][2][)] 是从t分布中得出的适当临界值,具有n − 2 的自由度(换句话说,结果导致上尾区域恰好为/2);s[ɛ]是估计的残差标准误差;x̄和
分别表示预测变量的样本均值和观测值的方差。
对于观察到的响应,预测区间(PI)与置信区间在背景上有所不同。置信区间用于描述均值响应的变异性,而预测区间用于提供响应变量的个体实现可能取值的范围,前提是给定x。这个区别微妙但重要:置信区间对应均值,预测区间对应个体观察值。
让我们继续使用前面的符号。可以证明,给定x值的个体响应的 100(1 − α)百分比预测区间可以通过以下公式计算:

事实证明,(20.5)中的唯一区别是出现在平方根中的 1+。因此,在x处,预测区间比置信区间宽。
20.4.2 解释区间
继续我们的例子,假设你想确定手掌跨度为 14.5 cm 和 24 cm 的学生的平均身高。点估计本身很简单——只需将所需的x值代入回归方程 (20.4)。
R> as.numeric(beta0.hat+beta1.hat*14.5)
[1] 159.1446
R> as.numeric(beta0.hat+beta1.hat*24)
[1] 188.7524
根据模型,你可以预期,手掌跨度为 14.5 cm 和 24 cm 时,平均身高分别约为 159.14 cm 和 188.75 cm。as.numeric强制转换函数(首次在第 6.2.4 节中遇到)仅用于去除来自beta0.hat和beta1.hat对象的注释名称。
平均身高的置信区间
要找到这些估计值的置信区间,你可以使用(20.5)手动计算,但当然 R 有一个内置的predict命令可以为你完成这个任务。使用predict时,你首先需要以特定方式存储你的x值:将其作为新数据框中的一列。列名必须与原始模型拟合时所使用的预测变量名称匹配。在这个例子中,我将创建一个新的数据框xvals,并命名一列为Wr.Hnd,该列仅包含两个感兴趣的值——14.5 cm 和 24 cm 的手掌跨度。
R> xvals <- data.frame(Wr.Hnd=c(14.5,24))
R> xvals
Wr.Hnd
1 14.5
2 24.0
现在,当调用predict时,第一个参数必须是感兴趣的拟合模型对象,在这个例子中是survfit。接下来,在newdata参数中,你需要传递包含指定预测值的特别构建的数据框。在interval参数中,你必须指定"confidence"作为字符值。置信水平在这里设置为 95%,并将其传递给level(以概率的尺度)。
R> mypred.ci <- predict(survfit,newdata=xvals,interval="confidence",level=0.95)
R> mypred.ci
fit lwr upr
1 159.1446 156.4956 161.7936
2 188.7524 185.5726 191.9323
此调用将返回一个矩阵,矩阵有三列,每列的行数(以及顺序)与您在 newdata 数据框中提供的预测值相对应。第一列,标题为 fit,表示回归线上的点估计值;你可以看到这些数字与之前计算的值一致。其他列分别提供了下限和上限 CI 值,分别为 lwr 和 upr 列。在这种情况下,你可以解释为:我们有 95% 的信心认为,手掌跨度为 14.5 cm 的学生的平均身高位于 156.5 cm 到 161.8 cm 之间,手掌跨度为 24 cm 时,身高位于 185.6 cm 到 191.9 cm 之间(四舍五入到 1 位小数)。记住,这些 CI 是根据(20.5)公式通过 predict 计算的,是针对 均值 响应值的。
个别观测值的预测区间
predict 函数还会提供你的预测区间。要找到某个概率下可能个别观测值的预测区间,你只需要将 interval 参数改为 "prediction"。
R> mypred.pi <- predict(survfit,newdata=xvals,interval="prediction",level=0.95)
R> mypred.pi
fit lwr upr
1 159.1446 143.3286 174.9605
2 188.7524 172.8390 204.6659
注意,拟合值保持不变,正如方程(20.5)和(20.6)所示。然而,PIs 的宽度显著大于相应 CIs 的宽度——这是因为原始观测值本身在特定 x 值下会自然比它们的均值更具变异性。
解释也相应发生变化。这些区间描述了原始学生身高在“95% 的时间”内预测的范围。对于手掌跨度为 14.5 cm 的情况,模型预测个别观测值将在 143.3 cm 到 175.0 cm 之间,概率为 0.95;对于手掌跨度为 24 cm,预测的 PI 范围为 172.8 cm 到 204.7 cm(四舍五入到 1 位小数)。
20.4.3 绘制区间
CIs 和 PIs 都非常适合用于简单线性回归模型的可视化。使用以下代码,你可以像绘制 图 20-2 那样开始绘制数据和估计的回归线,不过这一次使用 plot 中的 xlim 和 ylim 来稍微扩展 x 和 y 轴的范围,以便容纳完整的 CI 和 PI 长度。
R> plot(survey$Height~survey$Wr.Hnd,xlim=c(13,24),ylim=c(140,205),
xlab="Writing handspan (cm)",ylab="Height (cm)")
R> abline(survfit,lwd=2)
在此基础上,你可以添加 x = 14.5 和 x = 24 时的拟合值位置,以及两组垂直线,分别表示 CIs 和 PIs。
R> points(xvals[,1],mypred.ci[,1],pch=8)
R> segments(x0=c(14.5,24),y0=c(mypred.pi[1,2],mypred.pi[2,2]),
x1=c(14.5,24),y1=c(mypred.pi[1,3],mypred.pi[2,3]),col="gray",lwd=3)
R> segments(x0=c(14.5,24),y0=c(mypred.ci[1,2],mypred.ci[2,2]),
x1=c(14.5,24),y1=c(mypred.ci[1,3],mypred.ci[2,3]),lwd=2)
对 points 的调用标记了这两个特定 x 值的拟合值。第一次调用 segments 将预测区间(PIs)绘制为加粗的垂直灰色线条,第二次绘制置信区间(CIs)为较短的垂直黑色线条。这些绘制的线段的坐标分别直接取自 mypred.pi 和 mypred.ci 对象。
你还可以生成围绕拟合回归线的“带”,标记出预测变量在所有自变量值上的一个或两个区间。从编程角度来看,对于连续变量来说,这在技术上是不可能的,但你可以通过沿* x *轴定义一个细的值序列(使用seq并设置较大的length值),然后在这个细的序列中的每个点计算置信区间和预测区间,实际上实现这一目标。然后,你只需在绘制时将结果点连接为线。

图 20-3:学生身高回归示例,带有拟合回归线和在 x = 14.5 和 x = 24 的点估计,并且对应的 95% 置信区间(黑色垂直线)和预测区间(灰色垂直线)。虚线黑色和虚线灰色线条提供了在可见的 x 值范围内响应变量的 95% 置信带和预测带。
在 R 中,这需要你重新运行如下的predict命令:
R> xseq <- data.frame(Wr.Hnd=seq(12,25,length=100))
R> ci.band <- predict(survfit,newdata=xseq,interval="confidence",level=0.95)
R> pi.band <- predict(survfit,newdata=xseq,interval="prediction",level=0.95)
这段代码的第一行创建了细的预测值序列,并将其存储为newdata参数所需的格式。置信区间和预测区间带的* y *轴坐标存储在矩阵对象ci.band和pi.band的第二列和第三列中。最后,使用lines添加四条虚线,分别对应两个区间的上限和下限,并且一个图例为图表增添了最后的修饰。
R> lines(xseq[,1],ci.band[,2],lty=2)
R> lines(xseq[,1],ci.band[,3],lty=2)
R> lines(xseq[,1],pi.band[,2],lty=2,col="gray")
R> lines(xseq[,1],pi.band[,3],lty=2,col="gray")
R> legend("topleft",legend=c("Fit","95% CI","95% PI"),lty=c(1,2,2),
col=c("black","black","gray"),lwd=c(2,1,1))
请注意,黑色虚线的置信区间带与垂直黑线相交,灰色虚线的预测区间带与垂直灰线相交,正如你所预期的那样,分别对应之前的两个* x *值。
图 20-3 显示了所有这些元素添加到图表后的最终结果。区间的“内弯”曲线是这种图表的典型特征,在置信区间中尤为明显。这一曲线的出现是因为在预测数据较多的地方,变化自然较小。有关线性模型对象的predict更多信息,请查看?predict.lm帮助文件。
20.4.4 插值与外推
在完成关于预测的介绍之前,重要的是要明确两个关键术语的定义:插值和外推。这两个术语描述了给定预测的性质。如果你指定的* x 值在观察数据的范围内,那么该预测被称为插值;如果 x 值超出了这个范围,则为外推。通过你刚才做的点预测,你可以看到 x * = 14.5 是插值的例子,而* x * = 24 是外推的例子。
通常,内插比外推更为可取——在已观察到的数据附近使用拟合模型进行预测更有意义。尽管如此,外推如果不远离该区域,仍然可以认为是可靠的。学生身高的外推例子,x = 24 就是一个典型例子。这个值超出了观察数据的范围,但从尺度上来看并不远,而且预计值的估计区间 ŷ = 188.75 cm 从视觉上看是合理的,考虑到其他观察值的分布。相比之下,在例如手掌跨度为 50 厘米的情况下使用拟合模型预测学生身高就显得不太合理了:
R> predict(survfit,newdata=data.frame(Wr.Hnd=50),interval="confidence",
level=0.95)
fit lwr upr
1 269.7845 251.9583 287.6106
这种极端的外推意味着一个手掌跨度为 50 厘米的人的平均身高几乎是 270 厘米,这显然是一个不现实的测量值。相同的情况也适用于另一个方向;截距
没有特别有用的实际解释,这意味着手掌跨度为 0 厘米的学生的平均身高大约是 114 厘米。
这里的主要信息是,在从线性模型拟合进行任何预测时,要运用常识。在结果的可靠性方面,在适当接近观测数据的值范围内进行的预测更为可靠。
练习 20.1
继续使用 MASS 包中的 survey 数据框来进行接下来的几个练习。
-
使用你拟合的学生身高与写字手掌跨度的模型
survfit,为手掌跨度为 12、15.2、17 和 19.9 厘米的学生提供均值的点估计和 99%的置信区间。 -
在第 20.1 节中,你定义了对象
incomplete.obs,这是一个数值向量,提供了在估计模型参数时被自动移除的survey记录。现在,使用incomplete.obs向量与survey数据框以及公式 (20.3)来计算 R 中的
和
。 (记住使用 mean、sd和cor函数,确保你的答案与survfit的输出一致。) -
survey数据框除了Height和Wr.Hnd之外,还包含其他一些变量。此次练习的最终目标是拟合一个简单的线性模型来预测学生的平均身高,但这次是根据他们的脉搏率(在Pulse中给出)来预测(继续假设满足第 20.2 节列出的条件)。-
拟合回归模型,并生成带有拟合线的散点图,确保你能够写下拟合的模型方程,并保持图表打开。
-
识别并解释斜率的点估计值,以及与假设 H[0] : β[1] = 0; H[A] : β[1] ≠ 0 相关的检验结果。同时找到斜率参数的 90%置信区间。
-
使用你的模型,在(i)中的图表上添加 90%置信区间和预测区间带,并添加图例以区分这些线条。
-
为当前的“脉冲高度”数据创建一个
incomplete.obs向量。使用该向量计算在(i)中拟合模型所使用的高度观测值的样本均值。然后在该均值处为图表添加一条完全水平的线(使用颜色或线型选项,以避免与其他线条混淆)。你注意到什么了吗?图表是否支持你在(ii)中的结论?
-
接下来,查看mtcars数据集的帮助文件,该数据集你在练习 13.4 中首次见到,位于第 287 页。本练习的目标是将燃油效率(以每加仑多少英里(MPG)衡量)与车辆的整体重量(以千磅为单位)进行建模。
-
绘制数据——将
mpg绘制在y轴上,将wt绘制在x轴上。 -
拟合简单线性回归模型。将拟合的直线添加到(d)中的图表上。
-
写下回归方程并解释斜率的点估计值。
wt对均值mpg的影响是否被估计为统计显著? -
对于一辆重 6,000 磅的车,产生一个点估计和相关的 95%置信区间(PI)。你相信模型能准确预测此解释变量值的观测值吗?为什么或为什么不?
20.5 理解类别预测变量
到目前为止,你已查看依赖于连续解释变量的简单线性回归模型,但也可以使用离散或类别解释变量来建模均值响应,该解释变量由k个不同的组或水平组成。你必须能够做出与第 20.2 节中相同的假设:即所有观测值相互独立,残差呈正态分布且具有相等的方差。首先,你将查看最简单的情况,其中k = 2(一个二值预测变量),这为稍微复杂一点的情况奠定基础,在该情况下类别预测变量具有多个水平(多级预测变量:k > 2)。
20.5.1 二元变量:k = 2
回到方程 (20.1),在这个方程中,回归模型被指定为 Y|X = β[0] + β[1]X + ∊,其中 Y 是响应变量,X 是预测变量,且 ∊ ~ N(0,σ²)。现在,假设你的预测变量是类别型的,只有两个可能的水平(即二元变量;k = 2),并且观测值编码为 0 或 1。在这种情况下,(20.1) 仍然成立,但模型参数 β[0] 和 β[1] 的解释不再是“截距”和“斜率”。更好的理解方式是将它们看作是两个截距,其中 β[0] 提供了当 X = 0 时响应的 基准 或 参考 值,而 β[1] 则表示当 X = 1 时对平均响应的 附加效应。换句话说,如果 X = 0,那么 Y = β[0] + ∊;如果 X = 1,那么 Y = β[0] + β[1] + ∊。像往常一样,估计是通过找到 平均 响应 ŷ
[Y |X = x],如方程 (20.2)所示,因此方程变为
。
返回到 survey 数据框,并注意你有一个 Sex 变量,记录了学生的性别。你可以查看帮助页面 ?survey 的文档,或者像这样输入:
R> class(survey$Sex)
[1] "factor"
R> table(survey$Sex)
Female Male
118 118
你会看到,性别数据列是一个具有两个水平(Female 和 Male)的因子向量,并且这两个类别的数量恰好相等(237 条记录中有一条缺失此变量的值)。
你将确定是否有统计证据表明学生的身高受性别影响。这意味着你再次对身高作为响应变量建模,但这次是将类别型的性别变量作为预测变量。
为了可视化数据,如果你按照如下方式调用 plot,你将得到一对箱线图。
R> plot(survey$Height~survey$Sex)
这是因为在 ~ 左侧指定的响应变量是数值型的,而右侧的解释变量是因子类型,在这种情况下,R 的默认行为是生成并排的箱线图。
为了进一步强调解释变量的类别性质,你可以将原始的身高和性别观测值叠加在箱线图上。为此,只需通过调用 as.numeric 将因子向量转换为数值型;可以在 points 函数调用中直接完成此操作。
R> points(survey$Height~as.numeric(survey$Sex),cex=0.5)
记住,箱线图通过中央的粗线表示中位数,而最小二乘线性回归是通过平均结果定义的,因此展示按性别分组的平均身高也是有用的。
R> means.sex <- tapply(survey$Height,INDEX=survey$Sex,FUN=mean,na.rm=TRUE)
R> means.sex
Female Male
165.6867 178.8260
R> points(1:2,means.sex,pch=4,cex=3)
你在第 10.2.3 节中接触过tapply;在此调用中,参数na.rm=TRUE与tapply的省略号匹配,并传递给mean(你需要它来确保数据中的缺失值不会导致结果为NA)。进一步调用points将这些坐标(以×符号表示)添加到图像中;图 20-4 给出了最终结果。

图 20-4:按性别划分的学生身高箱形图,带有叠加的原始观测值和样本均值(分别用小○和大×符号表示)
图表总体上表明,男性的身高往往高于女性——但是否有统计证据支持这一差异?
二元变量的线性回归模型
要用简单的线性回归模型来回答这个问题,你可以使用lm来产生最小二乘估计,就像你之前拟合的每个模型一样。
R> survfit2 <- lm(Height~Sex,data=survey)
R> summary(survfit2)
Call:
lm(formula = Height ~ Sex, data = survey)
Residuals:
Min 1Q Median 3Q Max
-23.886 -5.667 1.174 4.358 21.174
Coefficients:
Estimate Std. Error t value Pr(>|t|)
(Intercept) 165.687 0.730 226.98 <2e-16 ***
SexMale 13.139 1.022 12.85 <2e-16 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Residual standard error: 7.372 on 206 degrees of freedom
(29 observations deleted due to missingness)
Multiple R-squared: 0.4449, Adjusted R-squared: 0.4422
F-statistic: 165.1 on 1 and 206 DF, p-value: < 2.2e-16
然而,由于预测变量是一个因子向量而非数值向量,系数的报告方式略有不同。[0]的估计再次报告为(Intercept);这是学生为女性时的平均身高估计值。β[1]的估计报告为SexMale。对应的回归系数 13.139 是男性学生的平均身高与女性学生之间的估计差异。如果你查看相应的回归方程

你可以看到,假设变量 x 被定义为“个体为男性”——0 表示否/假,1 表示是/真,模型已经拟合完成。换句话说,性别变量的“女性”水平被假定为参考,明确估计的是“为男性”对平均身高的影响。对于β[0]和β[1]的假设检验是按照第 20.3.2 节中定义的相同假设进行的:
H[0] : β[j] = 0
H[A] : β[j] ≠ 0
再次强调,通常最感兴趣的是β[1]的检验,因为正是这个值告诉你是否存在统计证据表明平均响应变量受到解释变量的影响,也就是说,如果β[1]显著不同于零。
来自二元分类变量的预测
由于* x* 只有两个可能的值,因此预测非常简单。在评估方程时,唯一需要做的决定是是否使用
(换句话说,个体是否为男性)或不使用(如果个体为女性)。例如,你可以输入以下代码来创建一个包含五个额外观测值的新因子,这些额外观测值的水平名称与原始数据相同,并将新数据存储在extra.obs中:
R> extra.obs <- factor(c("Female","Male","Male","Male","Female"))
R> extra.obs
[1] Female Male Male Male Female
Levels: Female Male
然后,使用predict函数以熟悉的方式来找到预测变量的这些额外值下的平均值。 (记住,当你通过newdata参数将新数据传递给predict时,预测变量必须与最初用于拟合模型的数据形式相同。)
R> predict(survfit2,newdata=data.frame(Sex=extra.obs),interval="confidence",
level=0.9)
fit lwr upr
1 165.6867 164.4806 166.8928
2 178.8260 177.6429 180.0092
3 178.8260 177.6429 180.0092
4 178.8260 177.6429 180.0092
5 165.6867 164.4806 166.8928
从输出中你可以看到,预测结果只在两组值之间有所不同——Female的两次点估计值相同,只是
具有 90%的置信区间。Male的点估计值和置信区间也都相同,基于
的点估计。
仅凭这一例子,确实没有什么特别令人兴奋的。然而,理解 R 在使用类别预测变量时如何展示回归结果是至关重要的,特别是当考虑到第二十一章中的多元回归时。
20.5.2 多级变量:k > 2
在处理数据时,通常会遇到类别预测变量有超过两个水平的情况,即(k > 2)。这也可以被称为多级类别变量。为了处理这种更复杂的情况,同时保持参数的可解释性,你必须首先将预测变量编码为k − 1 个二元变量。
虚拟编码多级变量
为了展示如何操作,假设你想要找出响应变量Y在给定类别变量X的值时的值,其中X有k > 2 个水平(还假设线性回归模型的有效性条件——第 20.2 节——已满足)。
在回归建模中,虚拟编码是用于从类别变量如X创建多个二元变量的过程。它不同于单一的类别变量和可能的实现值。
X = 1,2,3, . . . , k
你将它重新编码为多个“是/否”变量——每个级别一个——并可能得到以下实现值:
X[(][1][)] = 0,1; X[(][2][)] = 0,1; X[(][3][)] = 0,1; . . . ; X[(][k][)] = 0,1
如你所见,X[(][i][)]表示原始X的第i级的二元变量。例如,如果某个个体在原始类别变量中有X = 2,那么X[(][2][)] = 1(是),而其他所有X[(][1][)], X[(][3][)], . . . , X[(][k][)]都将是零(否)。
假设X是一个可以取 1、2、3 或 4 中的任何一个值的变量,并且你已经对这个变量进行了六次观测:1、2、2、1、4、3。 表 20-1 显示了这些观测值及其虚拟编码的等效值X[(][1][)]、X[(][2][)]、X[(][3][)]和X[(][4][)]。
表 20-1: 具有k = 4 组的类别变量的六个观测值的虚拟编码示例
| X | X[(][1][)] | X[(][2][)] | X[(][3][)] | X[(][4][)] |
|---|---|---|---|---|
| 1 | 1 | 0 | 0 | 0 |
| 2 | 0 | 1 | 0 | 0 |
| 2 | 0 | 1 | 0 | 0 |
| 1 | 1 | 0 | 0 | 0 |
| 4 | 0 | 0 | 0 | 1 |
| 3 | 0 | 0 | 1 | 0 |
在拟合后续模型时,通常只使用k − 1 个虚拟二元变量——其中一个变量作为参考或基准水平,并被纳入模型的整体截距项中。实际上,您最终会得到一个类似于这样的估计模型,

假设 1 是参考水平。如您所见,除了整体截距项
,您还会得到k −1 个其他的估计截距项,这些项根据原始类别中的观察值会调整基准系数
。例如,根据(20.8)中施加的编码,如果一个观察值有X[(][3][)] = 1,而所有其他二元值为零(因此该观察值在原始分类变量中会有X = 3 的值),则响应的预测均值将是
。另一方面,由于参考水平定义为 1,如果观察值对于所有二元变量的值为零,这意味着该观察值原本有X = 1,预测值将仅为
。
对于这种性质的分类变量进行虚拟编码是必要的原因是,一般来说,类别之间不能像连续变量那样以相同的数值关系相互关联。例如,通常不适合认为类别 4 中的观察值是类别 2 中的“两倍”,这正是估计方法所假定的。二元存在/不存在变量是有效的,并且可以轻松地融入建模框架中。选择参考水平通常是次要的——估计系数的具体数值会相应变化,但基于拟合模型做出的任何整体解释都将保持不变。
注意
实施这种虚拟编码方法从技术上来说是一种多元回归形式,因为您现在将多个二元变量包含在模型中。然而,重要的是要意识到虚拟编码的某种人为特性——您仍然应该将多个系数看作代表一个单一的分类变量,因为二元变量 X[(][1][)],...,X[(][k][)]* 之间不是相互独立的。这就是为什么我在本章中选择定义这些模型的原因;多元回归将在第二十一章中正式讨论。*
多层变量的线性回归模型
R 使得以这种方式处理分类预测变量变得非常简单,因为在您调用lm时,它会自动为任何此类解释变量进行虚拟编码。不过,在拟合模型之前,有两件事需要检查。
-
关注的分类变量应该存储为一个(形式上无序的)因子。
-
你应该检查你是否满意分配给参考水平的类别(用于解释性目的—见第 20.5.3 节)。
当然,你还必须确保对常见假设的有效性感到满意,即∊的正态性和独立性。
为了演示所有这些定义和概念,让我们回到MASS包中的学生调查数据,并将“学生身高”作为感兴趣的响应变量。数据中包含变量Smoke,该变量描述了每个学生自报告的吸烟类型,按频率定义并分为四个类别:“heavy”(重度吸烟)、“never”(从不吸烟)、“occasional”(偶尔吸烟)和“regular”(常规吸烟)。
R> is.factor(survey$Smoke)
[1] TRUE
R> table(survey$Smoke)
Heavy Never Occas Regul
11 189 19 17
R> levels(survey$Smoke)
[1] "Heavy" "Never" "Occas" "Regul"
这里,is.factor(survey$Smoke)的结果表明你确实手头有一个因子向量,调用table可以得到每个类别中学生的数量,正如第五章中所述,你可以通过levels显式请求任何 R 因子的水平属性。
让我们问一下是否有统计证据支持吸烟频率对学生身高均值的差异。你可以使用以下两行代码绘制这些数据的箱线图;图 20-5 展示了结果。
R> boxplot(Height~Smoke,data=survey)
R> points(1:4,tapply(survey$Height,survey$Smoke,mean,na.rm=TRUE),pch=4)

图 20-5:按吸烟频率划分的观察到的学生身高箱线图;各自的样本均值用 × 标出。
从早期的 R 输出中可以看到,除非在创建时明确定义,否则因子的水平默认按字母顺序排列——Smoke就是这种情况——并且 R 会自动将第一个水平(如levels函数输出所示)设置为参考水平,在随后的模型拟合中使用该因子作为预测变量时也是如此。使用lm拟合线性模型时,从后续的summary输出中可以看到,确实是Smoke的第一个水平,“heavy”被用作参考水平:
R> survfit3 <- lm(Height~Smoke,data=survey)
R> summary(survfit3)
Call:
lm(formula = Height ~ Smoke, data = survey)
Residuals:
Min 1Q Median 3Q Max
-25.02 -6.82 -1.64 8.18 28.18
Coefficients:
Estimate Std. Error t value Pr(>|t|)
(Intercept) 173.7720 3.1028 56.005 <2e-16 ***
SmokeNever -1.9520 3.1933 -0.611 0.542
SmokeOccas -0.7433 3.9553 -0.188 0.851
SmokeRegul 3.6451 4.0625 0.897 0.371
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Residual standard error: 9.812 on 205 degrees of freedom
(28 observations deleted due to missingness)
Multiple R-squared: 0.02153, Adjusted R-squared: 0.007214
F-statistic: 1.504 on 3 and 205 DF, p-value: 0.2147
如方程式(20.8)所示,你可以得到对应于虚拟二元变量的系数估计值,这些变量对应于此示例中四个可能类别中的三个——即三个非参考水平。参考类别Heavy中的观察值仅通过
表示,首先被指定为整体的(Intercept),其他系数则提供与其他类别中的观察值相关的效应。
多级分类变量的预测
你通过预测得到点估计值,像往常一样。
R> one.of.each <- factor(levels(survey$Smoke))
R> one.of.each
[1] Heavy Never Occas Regul
Levels: Heavy Never Occas Regul
R> predict(survfit3,newdata=data.frame(Smoke=one.of.each),
interval="confidence",level=0.95)
fit lwr upr
1 173.7720 167.6545 179.8895
2 171.8200 170.3319 173.3081
3 173.0287 168.1924 177.8651
4 177.4171 172.2469 182.5874
这里,我创建了对象one.of.each用于说明;它代表四个类别中每个类别的一个观察值,作为与原始Smoke数据的类别(和水平)匹配的对象存储。例如,Occas类别中的一个学生的预测身高为 173.772 − 0.7433 = 173.0287。
然而,之前模型摘要的输出显示,所有二元虚拟变量系数都不被认为与零显著不同(因为所有p-值都太大)。结果表明,正如你可能已经怀疑的那样,基于这份个体样本,吸烟频率(或更具体地说,吸烟频率与参考水平不同)对学生平均身高没有影响。像常见的那样,基准系数
在统计上显著——但这仅表明整体截距可能不为零。(因为你的响应变量是身高的测量,显然不会接近 0 厘米,这个结果是合理的。)提供的置信区间是按照常规的t检验方式计算的。
小的R-Squared值加强了这一结论,表明几乎无法通过改变吸烟频率的类别来解释响应的变化。此外,整体的F-检验p-值相当大,大约为 0.215,表明预测变量对响应的整体影响不显著;你将在第 20.5.5 节以及稍后在第 21.3.5 节中更详细地查看这一点。
如前所述,解释这些结果——事实上,任何基于回归中的k-级分类变量的结果——时,必须以集体的方式进行。你只能声称吸烟对身高没有可察觉的影响,因为所有二元虚拟变量系数的p-值都不显著。如果其中一个水平实际上是显著的(通过一个较小的p-值),那么这将意味着在此定义的吸烟因素作为整体,确实对响应有统计学上可检测的影响(即使其他两个水平仍然与非常高的p-值相关)。这一点将在第二十一章中的多个例子中进一步讨论。
20.5.3 更改参考水平
有时你可能会决定更改自动选择的参考水平,基于此,其他任何水平的效应都会被估计。更改基准将导致不同系数的估计,意味着个别的p-值可能发生变化,但总体结果(就因素的全球显著性而言)不会受到影响。因此,更改参考水平仅是出于解释目的——有时预测变量有一个直观上自然的基准(例如,在某些临床试验分析中,“安慰剂”与“药物 A”和“药物 B”作为治疗变量),你希望基于此估计与其他可能类别的均值响应的偏差。
重新定义参考水平可以通过 R 内置的relevel函数快速实现。此函数允许您选择在给定因子向量对象定义中哪个水平优先,并因此在随后的模型拟合中作为参考水平。在当前示例中,假设您希望将不吸烟者作为参考水平。
R> SmokeReordered <- relevel(survey$Smoke,ref="Never")
R> levels(SmokeReordered)
[1] "Never" "Heavy" "Occas" "Regul"
relevel函数将Never类别移动到了新因子向量中的第一个位置。如果您继续使用SmokeReordered而不是原来的Smoke列来拟合模型,它将提供与三种不同吸烟者水平相关的回归系数估计。
值得注意的是,在回归应用中,无序因子向量与有序因子向量的处理方式有所不同。为吸烟变量正式排序,按吸烟频率递增,例如创建一个新的因子向量,似乎是合理的。然而,当有序因子向量在调用lm时被提供,R 会以不同的方式反应——它不会执行我们在这里讨论的相对简单的虚拟编码,即将效应与每个可选水平与基线(技术上称为正交对比)关联起来。相反,默认行为是基于所谓的多项式对比拟合模型,其中有序分类变量对响应的效应在更复杂的函数形式中定义。这个讨论超出了本文的范围,但可以简要地说,当您的兴趣在于“向上”移动通过有序类别集的具体功能特性时,这种方法是有益的。有关技术细节,请参见 Kuhn 和 Johnson(2013)。在本书的所有相关回归示例中,我们将专门处理无序因子向量。
20.5.4 将分类变量视为数值型
lm决定拟合模型参数的方式主要取决于您传递给函数的数据类型。如前所述,lm只有在解释变量是无序因子向量时才会进行虚拟编码。
有时候,您想要分析的分类数据并未作为因子存储在数据对象中。如果分类变量是字符向量,lm会隐式地将其强制转换为因子。然而,如果目标分类变量是数值型的,那么lm将像对待连续数值预测变量一样执行线性回归;它会估算一个回归系数,这个系数被解释为响应均值的“每单位变化”。
如果原始解释变量应该由不同组组成,那么这样做似乎不太合适。然而,在某些设置下,尤其是当变量可以自然地被视为数值离散时,这种处理不仅在统计上是有效的,而且有助于解释。
让我们暂时离开survey数据,回到现成的mtcars数据集。假设你对变量油耗(mpg,连续型)和气缸数量(cyl,离散型)感兴趣(该数据集包含四缸、六缸或八缸的汽车)。现在,自动将cyl视为一个分类变量是完全合理的。将mpg作为响应变量时,箱线图非常适合反映cyl作为预测变量的分组性质;以下代码的结果如图 20-6 左侧所示:
R> boxplot(mtcars$mpg~mtcars$cyl,xlab="Cylinders",ylab="MPG")
在拟合相关回归模型时,你必须意识到你正在指示 R 做什么。由于mtcars中的cyl列是数字型变量,而不是因子向量,直接访问数据框时,lm会将其视为连续型变量。
R> class(mtcars$cyl)
[1] "numeric"
R> carfit <- lm(mpg~cyl,data=mtcars)
R> summary(carfit)
Call:
lm(formula = mpg ~ cyl, data = mtcars)
Residuals:
Min 1Q Median 3Q Max
-4.9814 -2.1185 0.2217 1.0717 7.5186
Coefficients:
Estimate Std. Error t value Pr(>|t|)
(Intercept) 37.8846 2.0738 18.27 < 2e-16 ***
cyl -2.8758 0.3224 -8.92 6.11e-10 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Residual standard error: 3.206 on 30 degrees of freedom
Multiple R-squared: 0.7262, Adjusted R-squared: 0.7171
F-statistic: 79.56 on 1 and 30 DF, p-value: 6.113e-10
就像前面章节中一样,你已经得到了一个截距和一个斜率估计;后者在统计上显著,表明有证据反对斜率的真实值为零。你拟合的回归线是

其中ŷ是平均油耗,x是数字型变量——气缸数。对于每增加一个气缸,模型表示油耗将平均减少 2.88 MPG。
认识到你已经将连续线拟合到实际上是分类数据的这一事实非常重要。以下几行代码创建的图 20-6 右侧面板突出了这一点:
R> plot(mtcars$mpg~mtcars$cyl,xlab="Cylinders",ylab="MPG")
R> abline(carfit,lwd=2)

图 20-6:左:根据气缸数划分的mtcars数据集的油耗箱线图。右:同一数据的散点图,并叠加了拟合的回归线(将cyl视为数字型-连续型)。
一些研究人员故意将分类或离散预测变量拟合为连续变量。首先,这样做允许插值;例如,你可以使用这个模型评估一辆五缸车的平均油耗(MPG)。其次,这意味着需要估计的参数较少;换句话说,对于一个有 k 个组的分类变量,你只需要一个斜率参数,而不是 k − 1 个截距参数。最后,这可以是控制所谓“干扰变量”的一种便捷方式;这将在第二十一章中更清楚地解释。另一方面,这意味着你不再获得特定组的信息。如果某个观测值的预测变量类别的均值响应差异没有很好地线性表示,继续这样做可能会误导——重要效应的检测可能完全丧失。
至少,在拟合模型时,认识到这一点非常重要。如果你现在才意识到 R 将cyl变量拟合为连续型,并且希望将其作为分类变量来拟合模型,那么你必须提前或在实际调用lm时显式地将其转换为因子向量。
R> carfit <- lm(mpg~factor(cyl),data=mtcars)
R> summary(carfit)
Call:
lm(formula = mpg ~ factor(cyl), data = mtcars)
Residuals:
Min 1Q Median 3Q Max
-5.2636 -1.8357 0.0286 1.3893 7.2364
Coefficients:
Estimate Std. Error t value Pr(>|t|)
(Intercept) 26.6636 0.9718 27.437 < 2e-16 ***
factor(cyl)6 -6.9208 1.5583 -4.441 0.000119 ***
factor(cyl)8 -11.5636 1.2986 -8.905 8.57e-10 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Residual standard error: 3.223 on 29 degrees of freedom
Multiple R-squared: 0.7325, Adjusted R-squared: 0.714
F-statistic: 39.7 on 2 and 29 DF, p-value: 4.979e-09
在这里,通过在指定lm公式时将cyl包装在factor调用中,你可以看到你已经为cyl的各个水平(对应 6 缸和 8 缸汽车,参考水平自动设置为 4 缸汽车)获得了回归系数估计值。
20.5.5 与单因素 ANOVA 的等价性
还有一个关于具有单一名义类别预测变量的回归模型的最终观察。考虑到这些模型描述的是k个不同组的均值响应值,这让你想到了什么吗?在这个特定的情境中,你实际上做的和单因素 ANOVA (第 19.1 节)中的操作是一样的:比较多个均值并确定是否有统计证据表明至少一个均值与其他均值不同。你需要为这两种技术做出相同的独立性和正态性假设。
实际上,使用最小二乘估计法实现的具有单一类别预测变量的简单线性回归,只是进行单因素 ANOVA 的另一种方式。或者,更简洁地说,ANOVA 是最小二乘回归的特例。单因素 ANOVA 检验的结果是一个p-值,它量化了反对零假设(即组均值相等)的一种统计证据。当回归中有一个类别预测变量时,正是这个p-值会在lm对象的summary输出的最后报告——我现在已经提到过几次,这是“整体”或“全局”显著性检验(例如,在第 20.3.3 节中)。
回顾一下学生身高由吸烟状态建模的全球显著性检验的最终结果——你得到了一个p-值为 0.2147。这个值来源于一个F检验统计量 1.504,df[1] = 3 和 df[2] = 205。现在,假设你只是拿到了数据,并被要求对身高与吸烟的关系进行单因素 ANOVA 分析。使用在第 19.1 节中介绍的aov函数,你将调用如下内容:
R> summary(aov(Height~Smoke,data=survey))
Df Sum Sq Mean Sq F value Pr(>F)
Smoke 3 434 144.78 1.504 0.215
Residuals 205 19736 96.27
28 observations deleted due to missingness
这些相同的值在这里返回;你还可以找到 MSE 的平方根:
R> sqrt(96.27)
[1] 9.811728
这实际上是lm摘要中给出的“剩余标准误差”。你从吸烟状态对身高的影响得出的两个结论(一个是lm输出,另一个是 ANOVA 测试)当然也是相同的。
lm 提供的全局检验不仅仅是为了确认方差分析的结果。作为方差分析的推广,最小二乘回归模型提供的不仅仅是系数特定的检验。这个全局检验正式称为 总合 F-检验,虽然它确实等同于单一类别预测变量设置下的单因素方差分析,但它也是一个有用的整体、独立的检验,用于评估多个预测变量对结果值的统计贡献。你将在第 21.3.5 节中深入探讨这一点,当你开始使用多个解释变量来建模响应变量时。
练习 20.2
接下来的几个练习继续使用 MASS 包中的 survey 数据框。
-
survey数据集有一个名为Exer的变量,它是一个因子,k = 3 个水平,描述每个学生的运动时间:无运动、适量运动或频繁运动。获取每个类别中学生的数量,并绘制分组箱线图,展示不同运动类别下学生的身高。 -
假设观察值独立且符合正态分布,拟合一个线性回归模型,将身高作为响应变量,运动作为解释变量(虚拟编码)。预测变量的默认参考水平是什么?生成模型摘要。
-
根据(b)中拟合的模型得出结论——运动频率是否对平均身高有影响?估计效果的性质是什么?
-
预测三个运动类别中每个个体的平均身高,并提供 95% 预测区间。
-
如果你使用
aov构建方差分析表,你是否得出与身高与运动频率模型相同的结果和解释? -
如果你改变模型,使得练习变量的参考水平为“无”,那么(e)中的结果会有什么变化?你预计会有变化吗?
现在,回到已经准备好的 mtcars 数据集。这个数据框中的一个变量是 qsec,描述为赛车跑四分之一英里的时间(秒);另一个变量是 gear,表示前进挡位的数量(这个数据集中的车有 3、4 或 5 个挡位)。
-
使用数据框中的直观向量,拟合一个简单的线性回归模型,将
qsec作为响应变量,gear作为解释变量,并解释模型摘要。 -
明确将
gear转换为因子向量并重新拟合模型。将该模型摘要与(g)的摘要进行比较。你发现了什么? -
请借助与图 20-6 右图相同风格的相关图表,解释为什么你认为(g)和(h)两个模型之间存在差异。
本章重要代码
| 函数/操作符 | 简要描述 | 首次出现 |
|---|---|---|
lm |
拟合线性模型 | 第 20.2.3 节,第 455 页 |
coef |
获取估计的系数 | 第 20.2.4 节, 第 457 页 |
summary |
总结线性模型 | 第 20.3.1 节, 第 458 页 |
confint |
获取估计系数的置信区间 | 第 20.3.2 节, 第 460 页 |
predict |
从线性模型中进行预测 | 第 20.4.2 节, 第 463 页 |
relevel |
更改因子参考水平 | 第 20.5.3 节, 第 477 页 |
第二十一章:21
多元线性回归

多元线性回归是对上一章讨论的单一预测变量模型的直接推广。它允许你通过多个预测变量来建模你的连续响应变量,从而衡量多个解释变量对响应变量的联合影响。在本章中,你将看到如何以这种方式建模响应变量,并使用 R 来通过最小二乘法拟合模型。你还将探索线性建模的其他关键统计方面,如变量转换和包含交互效应。
多元线性回归是统计学实践中的一个重要部分。它允许你控制或调整多个因素对响应值的影响,而不仅仅是衡量一个解释变量的效果(在大多数情况下,结果的测量有多个因素共同作用)。这种方法的核心目标是揭示响应变量与任何解释变量(联合作用)的潜在因果关系。实际上,因果关系本身非常难以确立,但你可以通过设计良好的研究、支持有力的数据收集,及拟合能够合理衡量数据中关系的模型,来增强因果关系的证据。
21.1 术语
在你了解多元回归模型背后的理论之前,首先要清楚理解一些与变量相关的术语。
• 潜在变量影响响应变量、另一个预测变量,或者两者,但在预测模型中未被测量(或未被包括)。例如,假设一位研究人员发现家庭丢弃垃圾的量与家庭是否拥有蹦床之间存在关联。这里的潜在变量可能是家庭中孩子的数量——这一变量更可能与垃圾量的增加和拥有蹦床的几率正相关。将蹦床的拥有与垃圾增加直接关联的解释是错误的。
• 潜在变量的存在可能导致关于响应变量与其他预测变量之间因果关系的虚假结论,或者它可能掩盖一个真实的因果关系;这种错误被称为混淆。换句话说,你可以将混淆理解为一个或多个预测变量对响应变量的影响交织在一起。
• 一个 干扰 或 外部变量 是一个次要的或不相关的预测变量,它有可能混淆其他变量之间的关系,从而影响你对其他回归系数的估计。外部变量作为必要的建模元素被包含在内,但它们对响应的具体影响并不是分析的主要关注点。
一旦你开始拟合并解释回归模型,在第 21.3 节中,这些定义将变得更加清晰。我想再次强调的主要信息是,相关性并不意味着因果关系。如果拟合的模型发现预测变量(或多个预测变量)与响应之间存在统计显著关联,那么考虑潜在变量可能对结果的影响是非常重要的,并且在得出结论之前,要尽量控制任何混杂因素。多元回归模型使你能够做到这一点。
21.2 理论
在开始使用 R 来拟合回归模型之前,你需要检查具有多个预测变量的线性回归模型的技术定义。在这里,你将从数学角度了解模型的工作原理,并一窥在 R 中估计模型参数时“幕后”发生的计算过程。
21.2.1 将简单模型扩展到多重模型
与仅有一个预测变量不同,你希望根据 p > 1 个独立解释变量 X[1], X[2], . . ., X[p] 的值来确定一个连续响应变量 Y 的值。总体模型定义为:

其中,β[0], . . . , β[p] 是回归系数,并且,如前所述,你假设残差 є ~ N(0, ˙) 独立、正态分布,围绕均值分布。
在实践中,你有 n 个数据记录;每个记录为每个预测变量 X[j] 提供值;j = {1, . . ., p}。待拟合的模型是基于特定解释变量集合的实现条件下的均值响应给出的。

其中,
代表回归系数的估计值。
在简单线性回归中,当你只有一个预测变量时,回顾一下,目标是找到“最佳拟合线”。对于多个独立预测变量的线性模型,最小二乘估计的思想是类似的。然而,现在你可以在抽象意义上将响应与预测变量之间的关系看作是一个多维平面或曲面。你要找到一个表面,它最能拟合你的多变量数据,并通过最小化该表面与原始响应数据之间的总平方距离来实现。
更正式地,对于你的 n 数据记录,
是通过最小化以下总和得到的值:

其中,x [j],[i] 是第 i 个个体在解释变量 X[j] 上的观察值,y[i] 是他们的响应值。
21.2.2 矩阵形式的估计
通过数据的 矩阵表示,最小化此平方距离 (21.2) 的计算变得更加简便。在处理 n 个多变量观察值时,你可以将 公式 (21.1) 写作如下,
Y = X · + є,
其中 Y 和 є 表示 n × 1 的列矩阵,使得

这里,y[i] 和 є[i] 分别表示第 i 个个体的响应观察值和随机误差项。量 β 是一个 (p + 1) × 1 的回归系数列矩阵,而所有个体的观察预测数据和解释变量则存储在一个 n × (p + 1) 的矩阵 X 中,这个矩阵被称为 设计矩阵:

最小化 (21.2) 提供的估计回归系数值是通过以下计算得到的:

需要注意以下几点:
• 符号 · 表示矩阵乘法,上标 ^⊤ 表示转置,^−¹ 表示逆矩阵(参见 第 3.3 节)。
• 扩展 β 和 X 的大小(注意 X 中前导的 1 列)以创建 p + 1 的结构(而不仅仅是预测变量的数量 p),从而可以估计总体截距 β[0]。
• 除了 (21.3) 外,设计矩阵在估计其他量(如回归系数的标准误差)中也发挥着关键作用。
21.2.3 一个基本示例
你可以使用 第三章 中介绍的函数在 R 中手动估计 β[j](j = 0, 1, ..., p):%*%(矩阵乘法)、t(矩阵转置)和 solve(矩阵求逆)。作为一个快速示例,假设你有两个预测变量:X[1] 是连续的,X[2] 是二元的。因此,你的目标回归方程是
。假设你收集了以下数据,其中响应数据、X[1] 和 X[2] 的数据,针对 n = 8 个个体,分别列在 y、x1 和 x2 列中。
R> demo.data <- data.frame(y=c(1.55,0.42,1.29,0.73,0.76,-1.09,1.41,-0.32),
x1=c(1.13,-0.73,0.12,0.52,-0.54,-1.15,0.20,-1.09),
x2=c(1,0,1,1,0,1,0,1))
R> demo.data
y x1 x2
1 1.55 1.13 1
2 0.42 -0.73 0
3 1.29 0.12 1
4 0.73 0.52 1
5 0.76 -0.54 0
6 -1.09 -1.15 1
7 1.41 0.20 0
8 -0.32 -1.09 1
要获得线性模型中 β = [β[0], β[1], β[2]]^┬ 的点估计,你首先需要按照 (21.3) 的要求构造 X 和 Y。
R> Y <- matrix(demo.data$y)
R> Y
[,1]
[1,] 1.55
[2,] 0.42
[3,] 1.29
[4,] 0.73
[5,] 0.76
[6,] -1.09
[7,] 1.41
[8,] -0.32
R> n <- nrow(demo.data)
R> X <- matrix(c(rep(1,n),demo.data$x1,demo.data$x2),nrow=n,ncol=3)
R> X
[,1] [,2] [,3]
[1,] 1 1.13 1
[2,] 1 -0.73 0
[3,] 1 0.12 1
[4,] 1 0.52 1
[5,] 1 -0.54 0
[6,] 1 -1.15 1
[7,] 1 0.20 0
[8,] 1 -1.09 1
现在你需要做的就是执行与公式 (21.3) 相对应的那行代码。
R> BETA.HAT <- solve(t(X)
R> BETA.HAT
[,1]
[1,] 1.2254572
[2,] 1.0153004
[3,] -0.6980189
你刚刚使用最小二乘法根据 demo.data 中的观察数据拟合了你的模型,得到了估计值
、
和
。
21.3 在 R 中实现与解释
R 总是很有帮助,当你指示它拟合多元线性回归模型时,它会自动构建矩阵并执行所有必要的计算。与简单回归模型一样,你使用 lm 并在指定第一个参数的公式时包含任何额外的预测变量。为了让你能够专注于 R 语法和结果解释,我暂时只关注主效应,然后你可以在本章后面探索更复杂的关系。
在输出和解释方面,使用多个解释变量遵循的规则与第二十章中看到的相同。任何数值型连续变量(或作为此类变量处理的分类变量)都有一个斜率系数,提供“每单位变化”的数量。任何 k 类别的分类变量(因子,形式上是无序的)都采用虚拟编码,并提供 k − 1 个截距。
21.3.1 额外的预测变量
让我们首先确认一下刚才手动计算的矩阵。使用 demo.data 对象,拟合多元线性模型,并按如下方式检查该对象的系数:
R> demo.fit <- lm(y~x1+x2,data=demo.data)
R> coef(demo.fit)
(Intercept) x1 x2
1.2254572 1.0153004 -0.6980189
你将看到,你得到的正是之前在 BETA.HAT 中存储的点估计。
按照惯例,响应变量位于左侧,你在 ~ 符号的右侧指定多个预测变量;所有这些一起构成了公式参数。为了拟合包含多个主效应的模型,使用 + 来分隔你想要包含的任何变量。事实上,你已经在第 19.2.2 节中看到过这种符号,用于研究双因素方差分析。
为了研究多元线性回归模型参数估计值的解释,我们回到 MASS 包中的 survey 数据集。在第二十章中,你探讨了几种简单的线性回归模型,基于学生身高作为响应变量,以及手掌跨度(连续型)和性别(分类变量,k = 2)作为独立预测变量。你发现手掌跨度在统计学上非常显著,估计系数表明手掌跨度每增加 1 厘米,身高平均增加约 3.12 厘米。当你使用性别作为解释变量查看相同的 t-检验时,模型也提供了反对零假设的证据,表明“男性”与女性(作为参考类别)相比,平均身高多出约 13.14 厘米。
这些模型无法告诉你的是性别和手掌跨度对预测身高的联合效应。如果你在多元线性模型中同时包含这两个预测变量,你可以(在一定程度上)减少可能在单独拟合每个预测变量对身高影响时发生的混杂效应。
R> survmult <- lm(Height~Wr.Hnd+Sex,data=survey)
R> summary(survmult)
Call:
lm(formula = Height ~ Wr.Hnd + Sex, data = survey)
Residuals:
Min 1Q Median 3Q Max
-17.7479 -4.1830 0.7749 4.6665 21.9253
Coefficients:
Estimate Std. Error t value Pr(>|t|)
(Intercept) 137.6870 5.7131 24.100 < 2e-16 ***
Wr.Hnd 1.5944 0.3229 4.937 1.64e-06 ***
SexMale 9.4898 1.2287 7.724 5.00e-13 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Residual standard error: 6.987 on 204 degrees of freedom
(30 observations deleted due to missingness)
Multiple R-squared: 0.5062, Adjusted R-squared: 0.5014
F-statistic: 104.6 on 2 and 204 DF, p-value: < 2.2e-16
手掌跨度的系数现在只有大约 1.59,几乎是其在独立的简单线性回归中与身高相关的系数(3.12 厘米)的一半。尽管如此,在性别存在的情况下,它仍然具有高度的统计显著性。与其简单线性模型相比,性别的系数也有所减小,但在手掌跨度的存在下仍然显著。稍后你会解释这些新数字。
至于其余的输出,Residual standard error仍然为你提供了随机噪声项є的标准误差估计值,同时还提供了一个R-squared值。当与多个预测变量关联时,后者正式被称为多重决定系数。这个系数的计算,与单一预测变量设置中的计算方式一样,来自于模型中各变量之间的相关性。我将理论上的复杂性留给更高级的文献,但需要注意的是,R-squared仍然代表了回归模型解释的响应变量的变异性比例;在这个例子中,它约为 0.51。
如果需要,你可以继续以相同的方式添加解释变量。在第 20.5.2 节中,你曾将吸烟频率作为单独的分类预测变量来分析身高,发现该解释变量并未提供对均值响应的显著统计影响。但是,如果你控制了手掌跨度和性别,吸烟变量是否能够以统计显著的方式做出贡献呢?
R> survmult2 <- lm(Height~Wr.Hnd+Sex+Smoke,data=survey)
R> summary(survmult2)
Call:
lm(formula = Height ~ Wr.Hnd + Sex + Smoke, data = survey)
Residuals:
Min 1Q Median 3Q Max
-17.4869 -4.7617 0.7604 4.3691 22.1237
Coefficients:
Estimate Std. Error t value Pr(>|t|)
(Intercept) 137.4056 6.5444 20.996 < 2e-16 ***
Wr.Hnd 1.6042 0.3301 4.860 2.36e-06 ***
SexMale 9.3979 1.2452 7.547 1.51e-12 ***
SmokeNever -0.0442 2.3135 -0.019 0.985
SmokeOccas 1.5267 2.8694 0.532 0.595
SmokeRegul 0.9211 2.9290 0.314 0.753
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Residual standard error: 7.023 on 201 degrees of freedom
(30 observations deleted due to missingness)
Multiple R-squared: 0.5085, Adjusted R-squared: 0.4962
F-statistic: 41.59 on 5 and 201 DF, p-value: < 2.2e-16
由于吸烟是一个具有k > 2 个水平的分类变量,Smoke采用虚拟编码(以重度吸烟者为默认参考水平),为该变量的三个非参考水平提供了三个额外的截距项;第四个则并入总体截距。
在最新拟合的summary中,你可以看到,尽管手掌跨度和性别继续产生非常小的p值,但吸烟频率并未提供与零系数假设相悖的证据。与survmult中的前一个模型相比,吸烟变量对其他系数值的影响很小,并且R-squared多重决定系数几乎没有增加。
你现在可能会问,如果吸烟频率在预测均值身高方面没有实质性的帮助,是否应该将该变量从模型中完全移除?这是模型选择的主要目标:找到最适合预测结果的“最佳”模型,而不是拟合一个不必要复杂的模型(通过包含过多不必要的解释变量)。你将在第 22.2 节中查看研究人员尝试实现这一目标的几种常见方法。
21.3.2 边际效应的解释
在多元回归中,每个预测因子的估计考虑了模型中所有其他预测因子的影响。因此,特定预测因子Z的系数应解释为在保持其他所有预测因子不变的情况下,Z增加一个单位时平均反应的变化。
既然你已经确定吸烟频率在考虑性别和手掌跨度时似乎对平均身高没有明显影响,那么请将注意力重新集中到survmult,这个模型只包含性别和手掌跨度这两个解释变量。请注意以下几点:
• 对于相同性别的学生(即仅关注男性或仅关注女性),手掌跨度增加 1 厘米,预计平均身高将增加 1.5944 厘米。
• 对于手掌跨度相似的学生,男性的平均身高比女性高 9.4898 厘米。
• 当将两个估计的预测系数与各自的简单线性模型拟合进行比较时,值的差异,再加上两者在多元拟合中仍然表明反对“为零”这一零假设的证据,表明在单一预测模型中存在混杂(即手掌跨度和性别对身高反应变量的影响)。
最后一点突出了多元回归的一般用途。它表明,在此示例中,如果仅使用单一预测模型,确定每个解释变量在预测平均反应中所产生的“真实”影响是误导性的,因为身高的变化部分由性别决定,另一部分则归因于手掌跨度。值得注意的是,survmult模型的决定系数(参见第 20.3.3 节)明显高于任何单变量模型中的同一数量,因此通过使用多元回归,实际上你考虑到了更多反应变量的变化。
拟合的模型本身可以被视为

其中,“手掌跨度”是以厘米为单位提供的写手手掌跨度,“性别”则以 1(男性)或 0(女性)的方式提供。
注意
基准(总体)截距约为 137.687 厘米,表示一个手掌跨度为 0 厘米的女性的平均身高——再次强调,这在应用的背景下显然无法直接解释。在这种情况下,一些研究者会通过从每个观察值中减去该预测因子所有观测值的样本均值,将问题中的连续预测因子(或多个预测因子)中心化为零,然后使用中心化后的数据进行模型拟合。通过这种方式拟合的模型允许你使用未转换预测因子(在此例中为手掌跨度)的均值,而不是零值,以便直接解释截距估计值
。
21.3.3 可视化多元线性模型
如图所示,“性别为男性”仅将整体截距改变约 9.49 厘米:
R> survcoefs <- coef(survmult)
R> survcoefs
(Intercept) Wr.Hnd SexMale
137.686951 1.594446 9.489814
R> as.numeric(survcoefs[1]+survcoefs[3])
[1] 147.1768
因此,你也可以将(21.4)写成两个方程式。这里是女性学生的方程:
“平均身高” = 137.687 + 1.594 × “手掌宽度”
这里是男性学生的方程:
| “平均身高” | = | (137.687 + 9.4898) + 1.594 × “手掌宽度” |
|---|---|---|
| = | 147.177 + 1.594 × “手掌宽度” |
这很方便,因为它让你能够像可视化简单线性模型一样,可视化多变量模型。此代码生成了图 21-1:
R> plot(survey$Height~survey$Wr.Hnd,
col=c("gray","black")[as.numeric(survey$Sex)],
pch=16,xlab="Writing handspan",ylab="Height")
R> abline(a=survcoefs[1],b=survcoefs[2],col="gray",lwd=2)
R> abline(a=survcoefs[1]+survcoefs[3],b=survcoefs[2],col="black",lwd=2)
R> legend("topleft",legend=levels(survey$Sex),col=c("gray","black"),pch=16)
首先,绘制一个按照性别分割的身高和手掌宽度的散点图。然后,abline会添加一条对应女性的线,并基于这两个方程添加另一条对应男性的线。
尽管这个图看起来像是两个独立的简单线性模型拟合,每个模型针对性别的一个水平,但重要的是要认识到事实并非如此。你实际上是在二维画布上查看多元模型的表示,其中决定这两条可见线拟合的统计量是“联合”估算的,换句话说,是在考虑两个预测变量的情况下估算的。

图 21-1:可视化观测数据和由手掌宽度及性别建模的学生身高的拟合多元线性模型
21.3.4 计算置信区间
如同第二十章中所述,你可以通过confint轻松找到任何回归参数的置信区间,在多元回归模型中,使用survmult2,即包含吸烟频率预测变量的学生身高拟合模型的对象,调用confint后的输出如下所示:
R> confint(survmult2)
2.5 % 97.5 %
(Intercept) 124.5010442 150.310074
Wr.Hnd 0.9534078 2.255053
SexMale 6.9426040 11.853129
SmokeNever -4.6061148 4.517705
SmokeOccas -4.1312384 7.184710
SmokeRegul -4.8543683 6.696525
请注意,在之前的模型总结中,Wr.Hnd和SexMale变量在 5%的显著性水平下被证明是统计显著的,并且它们的 95%置信区间不包括零值。另一方面,与吸烟频率预测变量相关的所有虚拟变量的系数都是不显著的,并且它们的置信区间明显包含零。这反映了吸烟变量在该模型中整体上不被认为是统计显著的。
21.3.5 总体 F 检验
在第 20.5.2 节首次介绍过的多级预测变量的上下文中,你可以更一般地将总体F-检验应用于多元回归模型,作为一个包含以下假设的检验:

该检验实际上是在比较“零”模型(换句话说,只有截距的模型)所归因的误差与在所有预测变量都存在时,预测变量所归因的误差。换句话说,预测变量能够更好地拟合响应变量时,解释的误差更多,从而得到更极端的 F 统计量,因此 p-值更小。这个单一的结果使得当你有多个解释变量时,该检验尤其有用。无论你在给定模型中有多少种预测变量组合,检验方式都是相同的:一个或多个可能是连续的、离散的、二元的和/或具有 k > 2 水平的分类变量。当拟合多个回归模型时,仅输出量的大小就可能需要时间来消化和解释,并且必须小心避免第一类错误(错误地拒绝真实的零假设—参见 第 18.5 节)。
F 检验有助于简化这一过程,使你能够得出以下结论之一:
-
如果相关的 p-值小于你选择的显著性水平 α,则反对 H[0] 的证据,这表明你的回归模型——你对解释变量的组合——在预测响应变量方面显著优于如果去掉 所有 这些预测变量时的模型。
-
如果相关的 p-值大于 α,则没有反对 H[0] 的证据,这表明使用预测变量相比单独使用截距并没有明显的好处。
缺点是,检验并不能告诉你哪些预测变量(或哪些子集)对模型拟合产生了有益的影响,也无法告诉你它们的系数或各自的标准误。
你可以使用拟合回归模型中的决定系数 R² 来计算 F 检验统计量。令 p 为需要估计的回归参数个数,排除截距 β[0]。然后,

其中 n 是用于拟合模型的观测值数量(在删除了缺失值记录后)。然后,在 H[0] 假设下,如公式 (21.5) 所示,
服从 F 分布(见 第 16.2.5 节,以及 第 19.1.2 节),其自由度为 df[1] = p,df[2] = n− p−1。与公式 (21.6) 相关的 p-值是该 F 分布上尾区域的面积。
作为一个快速练习来确认这一点,回到在 第 21.3.1 节 中拟合的多元回归模型 survmult2,这是基于 survey 数据集中的手掌跨度、性别和吸烟状态来预测学生身高的模型。你可以从 summary 报告中提取多重决定系数(使用 第 20.3.4 节 中提到的技术)。
R> R2 <- summary(survmult2)$r.squared
R> R2
[1] 0.508469
这与第 21.3.1 节中的多个 R 平方值相匹配。然后,你可以得到n,即survey数据集的原始大小减去任何缺失值(在之前的summary输出中报告为 30)。
R> n <- nrow(survey)-30
R> n
[1] 207
你得到p,即估计的回归参数的数量(减去 1 用于截距)。
R> p <- length(coef(survmult2))-1
R> p
[1] 5
然后,你可以确认n − p − 1 的值,这与summary输出中的值相匹配(201 自由度):
R> n-p-1
[1] 201
最后,你可以根据(21.6)找到检验统计量
,并且你可以按如下方式使用pf函数来获得该检验的相应p-值:
R> Fstat <- (R2*(n-p-1))/((1-R2)*p)
R> Fstat
[1] 41.58529
R> 1-pf(Fstat,df1=p,df2=n-p-1)
[1] 0
你可以看到,这个例子的全局F-检验给出的p-值非常小,几乎为零。这些计算结果与summary(survmult2)输出中报告的相关结果完全一致。
回顾基于手掌跨度、性别和吸烟情况的学生身高多重回归拟合(在survmult2中)第 21.3.1 节,不难理解,两个预测变量的p-值较小,而全局F-检验基于(21.5)表明强烈反对 H[0]的证据。这突出了全局检验的“伞形”特性:尽管吸烟频率变量本身似乎没有显著的统计贡献,但该模型的F-检验仍然表明,survmult2应该优于“不含预测变量”的模型,因为手掌跨度和性别都是重要的。
21.3.6 从多重线性模型进行预测
多重回归的预测(或预测)遵循与简单回归相同的规则。重要的是要记住,为特定协变量特征(给定个体的预测变量值的集合)找到的点预测是与响应的均值(或期望值)相关联的;置信区间提供均值响应的度量;而预测区间提供原始观察值的度量。你还必须考虑插值(基于* x 值的预测,这些值落在原始观察到的协变量数据的范围内)与外推(基于 x *值的预测,这些值超出了该数据的范围)的问题。除此之外,predict的 R 语法与第 20.4 节中使用的完全相同。
例如,使用在survmult中将学生身高拟合为手掌跨度和性别的线性函数的模型,你可以估计一位手掌跨度为 16.5 厘米的男性学生的平均身高,并给出置信区间。
R> predict(survmult,newdata=data.frame(Wr.Hnd=16.5,Sex="Male"),
interval="confidence",level=0.95)
fit lwr upr
1 173.4851 170.9419 176.0283
结果表明,你的期望值约为 173.48 厘米,且你可以 95%的置信度确认真实值介于 170.94 和 176.03 之间(四舍五入到小数点后两位)。同样,手掌跨度为 13 厘米的女性的平均身高估计为 158.42 厘米,99%的预测区间为 139.76 到 177.07。
R> predict(survmult,newdata=data.frame(Wr.Hnd=13,Sex="Female"),
interval="prediction",level=0.99)
fit lwr upr
1 158.4147 139.7611 177.0684
数据集中实际上有两名雌性学生的手掌跨度为 13 厘米,如图 21-1 所示。通过你对数据框子集操作的知识,你可以检查这两条记录,并选择三种感兴趣的变量。
R> survey[survey$Sex=="Female" & survey$Wr.Hnd==13,c("Sex","Wr.Hnd","Height")]
Sex Wr.Hnd Height
45 Female 13 180.34
152 Female 13 165.00
现在,第二只雌性猫的身高落在预测区间内,但第一只雌性猫的身高显著高于上限。重要的是要意识到,从技术上讲,模型拟合和解释上并没有出错——尽管不太可能,但观察值可能会落在预测区间外,甚至是一个宽广的 99%区间内。这种情况可能有许多原因。首先,模型可能不足。例如,你可能在拟合的模型中排除了重要的预测变量,从而使得预测能力较差。其次,尽管预测值在观察数据的范围内,但它发生在这个范围的极端端点,那里数据相对稀疏,因此预测值的可靠性较差。第三,观察值本身可能以某种方式受到污染——也许个体在记录手掌跨度时不准确,在这种情况下,她的不有效观察应在模型拟合前被移除。正是以这种批判性的眼光,一个优秀的统计学家会评估数据和模型;这是我将在本章中进一步强调的技能。
练习 21.1
在MASS包中,你会找到数据框cats,它提供了 144 只家猫的性别、体重(以千克为单位)和心脏重量(以克为单位)数据(有关更多详细信息,请参见 Venables and Ripley, 2002);你可以通过调用?cats来阅读文档。使用library("MASS")加载MASS包,并通过在控制台提示符下输入cats来直接访问该对象。
-
将心脏重量绘制在纵轴上,将体重绘制在横轴上,使用不同的颜色或点字符来区分雄性和雌性猫。在图表上加上图例和适当的轴标签。
-
使用心脏重量作为响应变量,其他两个变量作为预测变量,拟合一个最小二乘多元线性回归模型,并查看模型摘要。
-
写下拟合模型的方程,并解释体重和性别的回归系数估计值。这两个系数是否具有统计显著性?这说明响应变量和预测变量之间的关系如何?
-
报告并解释决定系数和整体F检验的结果。
-
-
Tilman 的猫,Sigma,是一只体重 3.4 千克的雌性猫。使用你的模型来估计她的平均心脏重量,并提供一个 95%的预测区间。
-
使用
predict根据拟合的线性模型,在你从(a)部分绘制的图表上叠加连续的线条,分别为雄性猫和雌性猫绘制。你注意到什么?这是否反映了参数估计的统计显著性(或缺乏统计显著性)?
boot包(Davison 和 Hinkley, 1997; Canty 和 Ripley, 2015)是另一个与标准安装一起提供的 R 语言库,但不会自动加载。使用library("boot")来加载boot。你将找到一个名为nuclear的数据框,它包含关于 1960 年代末美国核电厂建设的数据(Cox 和 Snell, 1981)。
-
通过在提示符下输入
?nuclear访问文档,并检查变量的详细信息。(请注意,date存在一个错误,它提供了建筑许可证颁发的日期——它应该写为“自1900 年 1 月 1 日以来的年份,取整到最近的月份。”)使用pairs生成数据的快速散点图矩阵。 -
原始目标之一是预测这些电厂进一步建设的成本。创建一个线性回归模型并总结,模型旨在通过
t1和t2这两个描述申请和颁发各种许可证时所经历的不同时间的变量来建模cost。注意拟合模型中估计的回归系数及其显著性。 -
重新拟合模型,但这次还要考虑建筑许可证颁发日期的影响。将这个新模型的输出与之前的模型进行对比。你注意到了什么?这些信息表明了关于这些预测变量的数据关系?
-
为电厂成本拟合第三个模型,使用“许可证颁发日期”、“电厂容量”和描述电厂是否位于美国东北部的二元变量作为预测变量。写出拟合模型方程,并为每个估计的系数提供 95%的置信区间。
以下表格给出了 1961 年到 1973 年间汇编的历史数据的摘录。它涉及密歇根州底特律的年谋杀率;这些数据最初由 Fisher(1976)提出并分析,现由 Harraway(1995)在此处重现。在数据集中,你将找到每 10 万人中谋杀人数、警察人数和发放的枪支许可证数量,以及整体失业率占总人口的百分比。
| 谋杀 | 警察 | 失业率 | 枪支 |
|---|---|---|---|
| 8.60 | 260.35 | 11.0 | 178.15 |
| 8.90 | 269.80 | 7.0 | 156.41 |
| 8.52 | 272.04 | 5.2 | 198.02 |
| 8.89 | 272.96 | 4.3 | 222.10 |
| 13.07 | 272.51 | 3.5 | 301.92 |
| 14.57 | 261.34 | 3.2 | 391.22 |
| 21.36 | 268.89 | 4.1 | 665.56 |
| 28.03 | 295.99 | 3.9 | 1131.21 |
| 31.49 | 319.87 | 3.6 | 837.60 |
| 37.39 | 341.43 | 7.1 | 794.90 |
| 46.26 | 356.59 | 8.4 | 817.74 |
| 47.24 | 376.69 | 7.7 | 583.17 |
| 52.33 | 390.19 | 6.3 | 709.59 |
-
在 R 工作区中创建自己的数据框,并生成散点图矩阵。哪些变量似乎与谋杀率最强相关?
-
使用谋杀案件数作为响应变量,其他所有变量作为预测变量,拟合一个多元线性回归模型。写下模型方程并解释系数。是否可以合理地说,响应变量和预测变量之间的所有关系都是因果关系?
-
确定响应中由于三种解释变量的联合作用而产生的变化量。然后重新拟合模型,排除与最大(换句话说,“最不显著”)p值相关的预测变量。将新模型的决定系数与之前模型的决定系数进行比较。差异大吗?
-
使用(k)中的模型预测每 10 万人中谋杀案件的平均数量,假设有 300 名警察和 500 个发放的枪支许可证。将其与没有发放枪支许可证时的平均响应进行比较,并为两种预测提供 99%的置信区间。
21.4 数值变量的转换
有时,严格按照标准回归方程(21.1)定义的线性函数在捕捉响应与选择的协变量之间的关系时可能不足。例如,您可能会在两个数值变量之间的散点图中观察到曲线性,而完美的直线不一定是最佳拟合。某种程度上,可以通过在估计或模型拟合之前简单地对某些变量进行转换(通常是非线性转换)来放宽要求,即数据必须表现出线性行为,才能使线性回归模型适用。
数值转换是指对数值观察值应用数学函数以便重新缩放它们。求一个数的平方根和将温度从华氏度转换为摄氏度都是数值转换的例子。在回归分析中,转换通常仅适用于连续变量,并且可以通过多种方式进行。在本节中,您将重点关注使用两种最常见方法的示例:多项式和对数转换。然而,请注意,所使用的转换变量方法是否适当,以及可能出现的建模优势,必须根据具体情况逐一评估。
转换通常并不是解决数据趋势中非线性问题的普遍解决方案,但它至少可以改善线性模型对这些趋势的拟合程度。
21.4.1 多项式
继续前面提到的一个评论,假设你在数据中观察到一种曲线关系,这样一条直线就不再是合适的建模选择。为了更精确地拟合你的数据,可以对回归模型中的特定预测变量应用多项式或幂次变换。这是一种简单的技术,通过在关系中允许多项式曲率,使得该预测变量的变化能够以比通常更复杂的方式影响响应。你通过在模型定义中添加额外的项来实现这一点,这些项表示变量的更高次幂对响应的影响。
为了更清晰地阐述多项式曲率的概念,考虑以下在−4 到 4 之间的序列,以及由此计算出的简单向量:
R> x <- seq(-4,4,length=50)
R> y <- x
R> y2 <- x + x²
R> y3 <- x + x² + x³
在这里,你需要对原始的x值进行特定函数运算。向量y作为x的副本,显然是线性的(从技术角度来看,这是“1 阶多项式”)。你将y2赋值为x的平方,从而提供二次行为——2 阶多项式。最后,向量y3表示x的立方函数的结果,其中包括x的 3 次方——3 阶多项式。
以下三行代码分别生成图 21-2 中从左到右的图示。
R> plot(x,y,type="l")
R> plot(x,y2,type="l")
R> plot(x,y3,type="l")

图 21-2:展示x的线性(左)、二次(中)和立方(右)函数
更普遍地说,假设你有一个连续预测变量X的数据,你希望用它来建模响应Y。按通常的方式线性估计,简单模型是
;X的二次趋势可以通过多元回归
来建模;立方关系可以通过
捕捉;依此类推。从图 21-2 的图示中,可以很好地解释包括这些额外项的效果,即可以捕捉到的曲线的复杂度。在 1 阶时,线性关系不允许曲率;在 2 阶时,任何给定变量的二次函数允许一个“弯曲”;在 3 阶时,模型可以处理关系中的两个弯曲,继续添加对应于协变量更高次幂的项时,情况会如此继续。这些项相关的回归系数(在生成前述图表的代码中,所有回归系数都被假设为 1)能够控制曲率的具体表现(换句话说,控制曲率的强度和方向)。
拟合多项式变换
将注意力转回内置的mtcars数据集。考虑描述发动机排量(单位:立方英寸)的disp变量,并与响应变量(每加仑英里数)进行比较。如果你查看图 21-3 中的数据图,可以看到排量与里程之间的关系确实存在一个轻微但明显的曲线。
R> plot(mtcars$disp,mtcars$mpg,xlab="Displacement (cu. in.)",ylab="MPG")

图 21-3:mtcars数据集中的每加仑英里数与发动机排量的散点图
简单线性回归模型提供的直线真的能最好地表示这种关系吗?为了调查这一点,首先拟合那个基础的线性模型。
R> car.order1 <- lm(mpg~disp,data=mtcars)
R> summary(car.order1)
Call:
lm(formula = mpg ~ disp, data = mtcars)
Residuals:
Min 1Q Median 3Q Max
-4.8922 -2.2022 -0.9631 1.6272 7.2305
Coefficients:
Estimate Std. Error t value Pr(>|t|)
(Intercept) 29.599855 1.229720 24.070 < 2e-16 ***
disp -0.041215 0.004712 -8.747 9.38e-10 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Residual standard error: 3.251 on 30 degrees of freedom
Multiple R-squared: 0.7183, Adjusted R-squared: 0.709
F-statistic: 76.51 on 1 and 30 DF, p-value: 9.38e-10
这清晰地表明排量对里程的负线性影响的统计证据——每增加一个立方英寸的排量,平均响应下降约 0.041 英里每加仑。
现在,尝试通过在模型中添加disp的二次项来捕捉数据中的明显曲线。你可以通过两种方式实现这一点。首先,你可以通过简单地对mtcars$disp向量进行平方运算,创建一个新的向量并将结果提供给lm函数中的公式。其次,你可以直接在公式中将disp²作为一个加法项指定。如果你选择这种方法,必须将该表达式包裹在I函数调用中,如下所示:
R> car.order2 <- lm(mpg~disp+I(disp²),data=mtcars)
R> summary(car.order2)
Call:
lm(formula = mpg ~ disp + I(disp²), data = mtcars)
Residuals:
Min 1Q Median 3Q Max
-3.9112 -1.5269 -0.3124 1.3489 5.3946
Coefficients:
Estimate Std. Error t value Pr(>|t|)
(Intercept) 3.583e+01 2.209e+00 16.221 4.39e-16 ***
disp -1.053e-01 2.028e-02 -5.192 1.49e-05 ***
I(disp²) 1.255e-04 3.891e-05 3.226 0.0031 **
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Residual standard error: 2.837 on 29 degrees of freedom
Multiple R-squared: 0.7927, Adjusted R-squared: 0.7784
F-statistic: 55.46 on 2 and 29 DF, p-value: 1.229e-10
当公式中的某一项需要进行算术计算时——在本例中是disp²——才需要使用I函数对该项进行包裹,才能在模型拟合之前进行计算。
回到拟合后的多元回归模型本身,可以看到平方项的贡献具有统计学显著性——对应于I(disp²)的输出显示p-值为 0.0031。这意味着,即使考虑了线性趋势,包含二次项(引入曲线)的模型也更适合拟合数据。这个结论得到了明显更高的决定系数支持(0.7927 对比 0.7183)。你可以在图 21-4 中看到这个二次曲线的拟合效果(相关代码稍后展示)。
在这里,你可能会合理地想知道,通过在相关变量中再添加一个更高阶项,是否能进一步提高模型捕捉关系的能力。为此:
R> car.order3 <- lm(mpg~disp+I(disp²)+I(disp³),data=mtcars)
R> summary(car.order3)
Call:
lm(formula = mpg ~ disp + I(disp²) + I(disp³), data = mtcars)
Residuals:
Min 1Q Median 3Q Max
-3.0896 -1.5653 -0.3619 1.4368 4.7617
Coefficients:
Estimate Std. Error t value Pr(>|t|)
(Intercept) 5.070e+01 3.809e+00 13.310 1.25e-13 ***
disp -3.372e-01 5.526e-02 -6.102 1.39e-06 ***
I(disp²) 1.109e-03 2.265e-04 4.897 3.68e-05 ***
I(disp³) -1.217e-06 2.776e-07 -4.382 0.00015 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Residual standard error: 2.224 on 28 degrees of freedom
Multiple R-squared: 0.8771, Adjusted R-squared: 0.8639
F-statistic: 66.58 on 3 and 28 DF, p-value: 7.347e-13
输出结果显示,三次项同样提供了统计学显著的贡献。然而,如果你继续添加更高阶的项,你会发现拟合一个四阶多项式并不能改善拟合效果,几个系数被判定为不显著(四阶拟合结果未显示)。
因此,设ŷ为每加仑英里数,x为排量(单位:立方英寸),并扩展前述输出中的科学计数法,拟合后的多元回归模型为
ŷ = 50.7 − 0.3372x + 0.0011x² − 0.000001x³,
这正是图 21-4 左侧面板中 3 阶曲线所反映的内容。
绘制多项式拟合
要展示图表本身,你可以按常规方式可视化数据和car.order1中的第一个(简单线性)模型。首先,执行以下代码以开始图 21-4:
R> plot(mtcars$disp,mtcars$mpg,xlab="Displacement (cu. in.)",ylab="MPG")
R> abline(car.order1)

图 21-4:三种不同的模型,1、2、3 阶多项式,拟合“每单位排量的里程”关系,数据来自mtcars数据集。左:可视化图表的限制仅限于数据。右:可视化图表的限制大幅扩展,以说明外推的可靠性不足。
添加与任何多项式模型对应的线条要稍微困难一些,因为abline只能处理直线趋势。实现这一点的一种方法是使用predict,对代表期望解释变量值的序列中的每个值进行预测。(我更倾向于这种方法,因为它还允许你同时计算置信区间和预测区间,如果你愿意的话。)要仅为 2 阶模型添加线条,首先要在观察到的disp范围内创建所需的序列。
R> disp.seq <- seq(min(mtcars$disp)-50,max(mtcars$disp)+50,length=30)
在这里,序列通过减去和加上 50 来稍微扩大,以预测原始协变量数据范围两侧的少量值,因此曲线会接触到图表的边缘。然后进行预测,并将拟合线叠加上去。
R> car.order2.pred <- predict(car.order2,newdata=data.frame(disp=disp.seq))
R> lines(disp.seq,car.order2.pred,lty=2)
对于 3 阶多项式,你使用相同的技术,接着最后添加图例。
R> car.order3.pred <- predict(car.order3,newdata=data.frame(disp=disp.seq))
R> lines(disp.seq,car.order3.pred,lty=3)
R> legend("topright",lty=1:3,
legend=c("order 1 (linear)","order 2 (quadratic)","order 3 (cubic)"))
所有这些的结果都显示在图 21-4 的左面板上。即使你只使用了来自单一协变量disp的原始数据,这里所展示的示例也被认为是多元回归,因为在 2 阶和 3 阶模型中,除了普遍的截距β[0]外,还需要估计多个参数。
对于里程和排量数据所拟合的不同类型趋势线,清楚地展示了对关系的不同解释。从视觉上来看,你可以合理地认为,简单线性拟合不足以建模响应变量与预测变量之间的关系,但当选择 2 阶和 3 阶版本时,得出明确结论就更困难。2 阶拟合捕捉到了随着disp增加而逐渐变缓的曲线;3 阶拟合则额外考虑了一个凸起(在技术术语中为鞍点或拐点),随后在相同范围内出现更陡的下降趋势。
那么,哪个模型是“最优”的呢?在这种情况下,参数的统计显著性表明应该优先选择 3 阶模型。话虽如此,在选择不同模型时还有其他因素需要考虑,这些你将在第 22.2 节中更仔细地思考。
多项式的陷阱
与线性回归模型中的多项式项相关的一个特定缺点是,当尝试执行任何形式的外推时,拟合趋势的不稳定性。图 21-4 右侧的图显示了相同的三个拟合模型(MPG 与排量的关系),但这次排量的尺度要宽得多。如你所见,这些模型的有效性值得怀疑。尽管二阶和三阶模型在观测数据范围内较好地拟合了 MPG,但如果你稍微超出最大观测排量值的阈值,平均油耗的预测就会大幅偏离。尤其是二阶模型变得完全不合理,暗示一旦发动机排量超过 500 立方英寸,MPG 会迅速提高。如果你打算在回归模型中使用高阶项,必须牢记多项式函数的这一自然数学行为。
为了创建这个图,可以使用创建左侧图的相同代码;你只需使用xlim来扩大* x *轴的范围,并定义disp.seq对象为一个相应更宽的序列(在本例中,我只是在创建disp.seq时设置了xlim=c(10,1000),并设置了匹配的from和to限制)。
注意
像这样的模型仍然被称为 线性 回归模型,这可能有点令人困惑,因为高阶多项式拟合的趋势显然是非线性的。这是因为 线性回归 指的是定义平均响应的函数在回归参数 β[0]、β[1],...,[βp] 方面是线性的。因此,对个别变量施加的任何变换都不会影响该函数相对于系数本身的线性性质。
21.4.2 对数变换
在统计建模中,当你有正的数值观测值时,通常会对数据进行对数变换,以大幅度减少数据的总体范围,并将极端观测值拉近到中心度量值。在这个意义上,转变为对数尺度可以帮助减少严重偏斜数据的严重性(见第 15.2.4 节)。在回归建模的背景下,对数变换可以用来捕捉那些表面上“平坦化”的曲线趋势,而不会像某些多项式那样,在观测数据范围之外出现不稳定性。
如果你需要复习对数的相关知识,可以回到第 2.1.2 节;这里只需注意,对数是必须将基数提升到某个幂次才能得到x值。例如,3⁵ = 243 时,对数是 5,3 是基数,表示为 log[3] 243 = 5。由于指数函数在常见概率分布中的普遍存在,统计学家几乎专门使用自然对数(以e为底的对数)。从这里开始,假设所有提到的对数转换都指的是自然对数。
简要说明对数转换的典型行为,请查看图 21-5,该图通过以下方式实现:
R> plot(1:1000,log(1:1000),type="l",xlab="x",ylab="",ylim=c(-8,8))
R> lines(1:1000,-log(1:1000),lty=2)
R> legend("topleft",legend=c("log(x)","-log(x)"),lty=c(1,2))
这张图绘制了 1 到 1000 的整数的对数与原始值的关系,并且还绘制了负对数。你可以看到,对数转换后的值随着原始值的增加逐渐趋于平缓。

图 21-5:应用于 1 到 1000 的整数的对数函数
拟合对数转换
如前所述,在回归分析中,使用对数转换的一种情况是当完美的直线不适合观察到的关系时,用来处理这种曲线性。为了说明这一点,回到mtcars的示例,考虑将里程作为马力和变速器类型(变量hp和am)的函数。创建一个散点图,显示 MPG 与马力的关系,用不同的颜色区分自动挡车和手动挡车。
R> plot(mtcars$hp,mtcars$mpg,pch=19,col=c("black","gray")[factor(mtcars$am)],
xlab="Horsepower",ylab="MPG")
R> legend("topright",legend=c("auto","man"),col=c("black","gray"),pch=19)
图 21-6 中显示的绘制点表明,马力的曲线趋势可能比直线关系更为合适。请注意,你需要明确地将二进制数字型的mtcars$am向量强制转换为因子,以便将其用作两种颜色的向量选择器。你将在拟合线性模型后添加这些线。

图 21-6:根据变速器类型分割的 MPG 与马力的散点图,线条表示使用对数尺度马力效应的多元线性回归的结果
让我们使用马力的对数转换来尝试捕捉曲线关系。因为在这个例子中,你还想考虑变速器类型可能对响应产生的影响,所以按照惯例,这将作为一个额外的预测变量进行包含。
R> car.log <- lm(mpg~log(hp)+am,data=mtcars)
R> summary(car.log)
Call:
lm(formula = mpg ~ log(hp) + am, data = mtcars)
Residuals:
Min 1Q Median 3Q Max
-3.9084 -1.7692 -0.1432 1.4032 6.3865
Coefficients:
Estimate Std. Error t value Pr(>|t|)
(Intercept) 63.4842 5.2697 12.047 8.24e-13 ***
log(hp) -9.2383 1.0439 -8.850 9.78e-10 ***
am 4.2025 0.9942 4.227 0.000215 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Residual standard error: 2.592 on 29 degrees of freedom
Multiple R-squared: 0.827, Adjusted R-squared: 0.8151
F-statistic: 69.31 on 2 and 29 DF, p-value: 8.949e-12
输出结果表明,马力对数和变速器类型对里程的影响在统计上显著。保持变速器类型不变时,马力对数每增加一个单位,平均 MPG 将下降大约 9.24。手动变速器使得平均 MPG 增加约 4.2(根据am的编码顺序来估计——0表示自动挡,1表示手动挡;参见?mtcars)。决定系数表明,该回归解释了响应变量 82.7%的变异,表明拟合情况令人满意。
绘制对数变换拟合图
为了可视化拟合模型,你首先需要计算所有所需预测值的拟合值。以下代码创建了一系列马力值(增加和减少 20 马力),并对两种变速器类型执行所需的预测。
R> hp.seq <- seq(min(mtcars$hp)-20,max(mtcars$hp)+20,length=30)
R> n <- length(hp.seq)
R> car.log.pred <- predict(car.log,newdata=data.frame(hp=rep(hp.seq,2),
am=rep(c(0,1),each=n)))
在上述代码中,由于你想绘制am的两种可能值的预测,当使用newdata时,你需要将hp.seq重复两次。然后,当你将am的值提供给newdata时,一组hp.seq与适当复制的am值 0 配对,另一组与值 1 配对。结果是一个预测向量,其长度是hp.seq的两倍,car.log.pred,前n个元素对应自动挡汽车,后n个元素对应手动挡汽车。
现在你可以将这些线添加到图 21-6,如下所示:
R> lines(hp.seq,car.log.pred[1:n])
R> lines(hp.seq,car.log.pred[(n+1):(2*n)],col="gray")
通过检查散点图,你可以看到拟合模型似乎很好地估计了马力/变速器与 MPG 之间的联合关系。变速器类型在此模型中的统计显著性直接影响两条新增线之间的差异。如果am不显著,两条线会更接近;在这种情况下,模型会建议使用一条曲线就足以捕捉这种关系。像往常一样,将预测数据外推得过远并不是一个好主意,尽管对于对数变换趋势来说,它比多项式函数外推要稳定一些。
21.4.3 其他变换
变换可以涉及数据集中的多个变量,并且不仅限于预测变量。在他们对mtcars数据的原始研究中,Henderson 和 Velleman(1981)也注意到,你在响应变量与如马力和排量等变量之间发现的曲线关系。他们认为,使用每加仑英里数(GPM)作为响应变量,而非每加仑英里数(MPG),更能改善线性关系。这将涉及对 MPG 进行变换,即 GPM = 1/MPG。
作者们还评论了当将汽车重量纳入拟合模型时,马力和排量对每加仑英里数(GPM)的影响有限,因为这三个预测变量之间存在相对较高的相关性(这被称为多重共线性)。为了解决这个问题,作者们创建了一个新的预测变量,即马力除以重量。他们用这个变量来衡量汽车的“过度动力”,并用这个新变量代替了单独的马力或排量。这只是他们在寻找适合的建模方法过程中进行的一些实验。
为了实现这个目标,无论你选择以何种方式建模自己的数据,转化数值变量的目标应始终是拟合一个有效的模型,能够更真实、准确地表示数据和变量之间的关系。在追求这个目标时,你在回归方法的应用中如何转化数值观测值有很大的自由度。关于线性回归中转化的进一步讨论,请参考 Faraway(2005)的第七章(Chapter 7),该章节提供了一个有益的介绍。
练习 21.2
以下表格展示了伽利略在其著名的“球体”实验中收集的数据,在这些实验中,他将一个球从不同高度的坡道上滚下,并测量球从坡道底部滚出的距离。有关更多信息以及其他有趣的例子,请查阅 Dickey 和 Arnold 的《用具有历史意义的数据教学统计学》(1995)。
| 初始身高 | 距离 |
|---|---|
| 1000 | 573 |
| 800 | 534 |
| 600 | 495 |
| 450 | 451 |
| 300 | 395 |
| 200 | 337 |
| 100 | 253 |
-
在 R 中创建一个数据框架,基于此表格绘制数据点,距离放在Y-轴上。
-
伽利略认为初始身高和行进距离之间存在二次关系。
-
以身高为自变量,拟合一个二次多项式,距离作为因变量。
-
对这些数据拟合一个三次(3 阶)模型和一个四次(4 阶)模型。它们告诉你关于关系性质的什么信息?
-
-
基于你在(b)部分建立的模型,选择你认为最能代表数据的模型,并将拟合线绘制到原始数据上。为图表添加 90%的置信带,表示平均行进距离的置信区间。
faraway贡献的 R 包包含大量数据集,配合 Faraway 的线性回归教材(2005)使用。安装该包后,调用library("faraway")加载它。该数据包中的一个数据集是trees,提供了某一类型被砍伐的树木的尺寸数据(例如,参见 Atkinson, 1985))。
-
在提示符下访问数据对象并绘制体积与胸围的关系(胸围放在X-轴上)。
-
以
体积为因变量,拟合两个模型:一个是胸围的二次模型,另一个是体积和胸围的对数变换模型。写出每个模型的方程,并评论两者在决定系数和全局F-检验方面的拟合相似性(或差异)。 -
使用
predict向 (d) 中的图表添加两种模型(来自 (e))的线条。使用不同的线型;添加相应的图例。还应包括 95% 的预测区间,线型与拟合值的线型一致(注意,对于涉及响应变量和预测变量对数转换的模型,predict返回的值本身会是对数尺度的;你必须使用exp对这些值进行反向转换,才能将该模型的线条叠加到图表上)。对各自的拟合效果及其估计的预测区间进行评论。
最后,将注意力转回到 mtcars 数据框。
-
拟合并总结一个多元线性回归模型,用于根据马力、重量和排量来预测平均 MPG。
-
参考 Henderson 和 Velleman(1981)的精神,使用
I重新拟合 (g) 中的模型,以 GPM = 1/MPG 作为目标。哪个模型解释了响应变量中更多的变异?
21.5 交互项
到目前为止,你只看了预测变量如何影响结果变量的联合主效应(以及其一对一的转换)。现在,你将查看协变量之间的交互作用。预测变量之间的交互效应是对响应变量的额外变化,这种变化发生在特定的预测变量组合下。换句话说,如果对于给定的协变量配置,预测变量的值使得它们产生的效应会增强这些预测变量本身的独立主效应,那么就存在交互效应。
21.5.1 概念与动机
如图 21-7 所示的图表,通常用于帮助解释交互效应的概念。这些图表如通常一样,展示了纵轴上的平均响应值 ŷ,以及横轴上变量 x[1] 的预测值。它们还展示了一个二元分类变量 x[2],其值可以是零或一。这些假设变量在图中被标记为这样。

图 21-7:两个预测变量 x[1] 和 x[2]* 之间的交互效应概念,对平均响应值ŷ的影响。左图:仅有 x[1]* 和 x[2]* 的主效应影响 ŷ。右图:需要 x[1]* 和 x[2]* 的交互效应,除了它们的主效应之外,以便对 ŷ 进行建模。
左图展示了你在本章中迄今为止考虑的模型的限制——即 x[1] 和 x[2] 独立地影响 ŷ。而右图则清楚地显示,x[1] 对 ŷ 的影响完全取决于 x[2] 的值。在左图中,仅需 x[1] 和 x[2] 的主效应来确定 ŷ;而在右图中,除了主效应外,还需要 x[1] 和 x[2] 之间的交互效应。
注意
在估计回归模型时,您总是需要将交互作用与相关预测变量的主效应一起考虑,这是出于可解释性的原因。由于交互作用本身最好理解为主效应的增强,因此没有意义去删除主效应而保留交互作用。
一个好的交互作用例子可以参考药理学。药物之间的交互效应是相对常见的,这也是为什么医疗专业人员通常会询问您可能正在服用的其他药物。考虑他汀类药物——一种常用于降低胆固醇的药物。服用他汀类药物的人被告知避免饮用葡萄柚汁,因为葡萄柚汁含有天然化学成分,能够抑制负责药物正确代谢的酶的效能。如果一个人正在服用他汀类药物并且没有饮用葡萄柚汁,您会预期胆固醇水平与他汀使用之间存在负相关关系(无论是将“他汀使用”视为连续变量还是分类剂量变量)——随着他汀使用量的增加或肯定,胆固醇水平下降。另一方面,对于那些正在服用他汀类药物并且正在饮用葡萄柚的人,胆固醇水平与他汀使用之间的关系可能完全不同——可能是负相关减弱、无关或甚至正相关。如果是这样,由于他汀对胆固醇的影响会根据另一个变量(是否饮用葡萄柚)的值而发生变化,这就被视为这两个预测变量之间的交互作用。
交互作用可以发生在分类变量、数值变量或两者之间。最常见的是二元交互作用——即恰好两个预测变量之间的交互作用,这也是您在第 21.5.2 节到第 21.5.4 节中将重点讨论的内容。三元及更高阶的交互效应在技术上是可能的,但较少见,部分原因是它们在实际应用中较难解释。您将在第 21.5.5 节中考虑这些例子的情况。
21.5.2 一个分类变量,一个连续变量
通常,分类变量和连续变量之间的二元交互作用应该被理解为影响连续变量相对于分类变量非参考水平的斜率变化。在连续变量项存在的情况下,具有k个水平的分类变量将有k − 1 个主效应项,因此将有k − 1 个交互项,表示分类变量的所有替代水平与连续变量之间的交互作用。
在图 21-7 的右侧可以清楚地看到y[1]按x[2]类别变化的不同斜率。在这种情况下,除了x[1]和x[2]的主效应外,拟合模型中还会有一个交互项对应于x[2] = 1。这定义了需要的附加项,用以将x[2] = 0 时的x[1]斜率转换为x[2] = 1 时的新斜率。
作为一个例子,让我们访问一个新的数据集。在练习 21.2 中,你使用了faraway包(Faraway, 2005)来访问trees数据。在这个包中,你还会找到diabetes对象——一个关于 403 名非裔美国人的心血管疾病数据集(最初由 Schorling 等,1997 和 Willems 等,1997 研究并报告)。如果你还没有安装faraway,请先安装并使用library("faraway")加载它。将注意力集中在总胆固醇水平(chol——连续变量)、个体的年龄(age——连续变量)和体型类型(frame——分类变量,具有k = 3 个水平:以"small"为参考水平,"medium"和"large")。你可以在图 21-8 中查看这些数据,图表将很快生成。
你将研究按年龄和体型建模总胆固醇。预期胆固醇与年龄和体型都有关系似乎是合乎逻辑的,因此考虑到年龄对胆固醇的影响可能因个体的体型不同而异也是有道理的。为了进行调查,我们将拟合多元线性回归,并包括这两个变量之间的双向交互作用。在调用lm时,首先指定主效应,像往常一样使用+,然后通过在两个预测变量之间使用冒号(:)来指定它们之间的交互效应。
R> dia.fit <- lm(chol~age+frame+age:frame,data=diabetes)
R> summary(dia.fit)
Call:
lm(formula = chol ~ age + frame + age:frame, data = diabetes)
Residuals:
Min 1Q Median 3Q Max
-131.90 -26.24 -5.33 22.17 226.11
Coefficients:
Estimate Std. Error t value Pr(>|t|)
(Intercept) 155.9636 12.0697 12.922 < 2e-16 ***
age 0.9852 0.2687 3.667 0.00028 ***
framemedium 28.6051 15.5503 1.840 0.06661 .
framelarge 44.9474 18.9842 2.368 0.01840 *
age:framemedium -0.3514 0.3370 -1.043 0.29768
age:framelarge -0.8511 0.3779 -2.252 0.02490 *
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Residual standard error: 42.34 on 384 degrees of freedom
(13 observations deleted due to missingness)
Multiple R-squared: 0.07891, Adjusted R-squared: 0.06692
F-statistic: 6.58 on 5 and 384 DF, p-value: 6.849e-06
检查输出中的估计模型参数时,你可以看到age的主效应系数,frame两个非参考水平的主效应系数,以及age与这两个非参考水平的交互效应的两个额外项。
注意
其实在 R 中有一个快捷方式来做到这一点——交叉因子符号。之前展示的相同模型可以通过使用chol~age*frame在lm中拟合;公式中两个变量之间的符号*应该解释为“包括截距、所有主效应和交互作用”。我从现在开始会使用这个快捷方式。*
输出结果显示了age的显著性,并提供了一些证据支持frame的主效应存在。尽管交互作用的显著性较弱,但也有轻微的显著性迹象。在这种情况下,评估显著性时,若其中一个预测变量是类别型并且k > 2(即有多个水平),则按照第 20.5.2 节讨论中的规则进行评估——如果至少有一个系数显著,整个效应应被认为是显著的。
拟合模型的通用方程可以直接从输出中写出。

我使用了冒号(:)来表示交互项,以便与 R 输出格式一致。
对于类别型预测变量的参考水平——体型为“小”的情况,可以直接从输出中写出拟合模型。
“平均总胆固醇” = 155.9636 + 0.9852 × “age”
对于只包含主效应的模型,改变体型为“中型”或“大型”只会影响截距——你可以从第 20.5 节了解到,相关效应仅是加到结果中的。然而,交互作用的存在意味着,除了截距的变化,age的主效应斜率也必须根据相关的交互项进行调整。对于体型为“中型”的个体,模型为
| “平均总胆固醇” | = | 155.9636 + 0.9852 × “age” + 28.6051 − 0.3514 × “age” |
|---|---|---|
| = | 184.5687 + (0.9852 − 0.3514) × “age” | |
| = | 184.5687 + 0.6338 × “age” |
对于体型为“大”的个体,模型为
| “平均总胆固醇” | = | 155.9636 + 0.9852 × “age” + 44.9474 − 0.8511 × “age” |
|---|---|---|
| = | 200.911 + (0.9852 − 0.8511) × “age” | |
| = | 200.911 + 0.1341 × “age” |
你可以通过访问拟合模型对象的系数,轻松在 R 中计算这些值:
R> dia.coef <- coef(dia.fit)
R> dia.coef
(Intercept) age framemedium framelarge
155.9635868 0.9852028 28.6051035 44.9474105
age:framemedium age:framelarge
-0.3513906 -0.8510549
接下来,我们来计算这个向量的相关分量的总和。得到总和后,你就可以绘制拟合模型了。
R> dia.small <- c(dia.coef[1],dia.coef[2])
R> dia.small
(Intercept) age
155.9635868 0.9852028
R> dia.medium <- c(dia.coef[1]+dia.coef[3],dia.coef[2]+dia.coef[5])
R> dia.medium
(Intercept) age
184.5686904 0.6338122
R> dia.large <- c(dia.coef[1]+dia.coef[4],dia.coef[2]+dia.coef[6])
R> dia.large
(Intercept) age
200.9109973 0.1341479
这三条直线作为长度为 2 的数字向量存储,其中截距在前,斜率在后。这是abline的可选coef参数所要求的形式,它允许你将这些直线叠加到原始数据的图表上。以下代码生成了图 21-8。
R> cols <- c("black","darkgray","lightgray")
R> plot(diabetes$chol~diabetes$age,col=cols[diabetes$frame],
cex=0.5,xlab="age",ylab="cholesterol")
R> abline(coef=dia.small,lwd=2)
R> abline(coef=dia.medium,lwd=2,col="darkgray")
R> abline(coef=dia.large,lwd=2,col="lightgray")
R> legend("topright",legend=c("small frame","medium frame","large frame"),
lty=1,lwd=2,col=cols)

图 21-8:拟合的线性模型,主效应和年龄与体型对总胆固醇的交互效应
如果你检查图 21-8 中的拟合模型,可以明显看出,加入年龄和体型的交互作用使得平均总胆固醇与这两个预测变量的关系更加灵活。三条图示的线条呈非平行状态,反映了图 21-7 中展示的概念。
我通过这个过程来说明概念的运作方式,但实际上你不需要经过所有这些步骤来找到点估计(以及任何相关的置信区间)。你可以通过使用predict从拟合的线性模型中进行预测,方法与仅含主要效应模型相同。
21.5.3 两个分类变量
你在第 19.2 节中的双因素方差分析引言中接触到了两个分类解释变量之间交互作用的概念。在那里,你揭示了羊毛类型和张力对纱线的平均断裂次数之间的交互效应(基于现成的warpbreaks数据框)。然后,你通过交互作用图(图 19-2,见第 447 页)将这种交互作用进行了可视化,类似于图 21-7 中的示意图。
让我们以第 19.2.2 节中的最后一个warpbreaks例子为基础,以显式的线性回归格式实现相同的模型。
R> warp.fit <- lm(breaks~wool*tension,data=warpbreaks)
R> summary(warp.fit)
Call:
lm(formula = breaks ~ wool * tension, data = warpbreaks)
Residuals:
Min 1Q Median 3Q Max
-19.5556 -6.8889 -0.6667 7.1944 25.4444
Coefficients:
Estimate Std. Error t value Pr(>|t|)
(Intercept) 44.556 3.647 12.218 2.43e-16 ***
woolB -16.333 5.157 -3.167 0.002677 **
tensionM -20.556 5.157 -3.986 0.000228 ***
tensionH -20.000 5.157 -3.878 0.000320 ***
woolB:tensionM 21.111 7.294 2.895 0.005698 **
woolB:tensionH 10.556 7.294 1.447 0.154327
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Residual standard error: 10.94 on 48 degrees of freedom
Multiple R-squared: 0.3778, Adjusted R-squared: 0.3129
F-statistic: 5.828 on 5 and 48 DF, p-value: 0.0002772
在这里,我使用了交叉因子符号*,而不是wool + tension + wool:tension。当两个预测变量都为分类变量时,每个非参考水平的第一个预测变量与第二个预测变量的所有非参考水平组合,会产生一个交互项。在这个例子中,wool是二分类变量,只有k = 2 个水平,而tension有k = 3 个水平;因此,唯一的交互项是“中等”(M)和“高”(H)张力水平(“低”L是参考水平)与羊毛类型B(A是参考水平)组合。因此,在拟合模型中,总共包含了B、M、H、B:M和B:H的项。
这些结果与 ANOVA 分析得出的结论一致——确实存在羊毛类型和张力对平均断裂次数的交互效应,此外还有这些预测变量的主要效应。
一般的拟合模型可以理解为
| “平均断裂次数” | = | 44.556 − 16.333 × “羊毛类型 B” |
|---|---|---|
| − 20.556 × “中等张力” | ||
| − 20.000 × “高张力” | ||
| + 21.111 × “羊毛类型 B : 中等张力” | ||
| + 10.556 × “羊毛类型 B : 高张力” |
额外的交互项与主要效应的作用方式相同——当只涉及分类预测变量时,模型可以看作是对整体截距的加性项。你在任何给定预测中的使用项,取决于特定个体的协变量特征。
让我们快速看几个例子:对于低张力的羊毛 A,预估的经向断裂数是简单地为整体截距;对于高张力的羊毛 A,你有整体截距和高张力的主效应项;对于低张力的羊毛 B,你只有整体截距和羊毛类型 B 的主效应项;而对于中等张力的羊毛 B,你有整体截距、羊毛类型 B 的主效应、中等张力的主效应,以及羊毛 B 与中等张力的交互项。
你可以使用predict来估算这四种情境下的平均经向断裂数;这些估算结果会伴随 90%的置信区间:
R> nd <- data.frame(wool=c("A","A","B","B"),tension=c("L","H","L","M"))
R> predict(warp.fit,newdata=nd,interval="confidence",level=0.9)
fit lwr upr
1 44.55556 38.43912 50.67199
2 24.55556 18.43912 30.67199
3 28.22222 22.10579 34.33866
4 28.77778 22.66134 34.89421
21.5.4 两个连续变量
最后,你将查看两个预测变量都是连续变量时的情况。在这种情况下,交互项作为修饰符作用于连续平面,这个平面仅使用主效应拟合。类似于连续变量与分类预测变量之间的交互作用,两个连续自变量之间的交互作用允许一个变量的斜率受另一个变量的值影响,而这种修饰是连续的(即根据另一个连续变量的值)。
回到mtcars数据框,再次考虑 MPG 作为马力和重量的函数。接下来展示的拟合模型,除了包括两个连续预测变量的主效应外,还包含了交互作用项。正如你所看到的,有一个单独估算的交互项,并且它被认为显著不同于零。
R> car.fit <- lm(mpg~hp*wt,data=mtcars)
R> summary(car.fit)
Call:
lm(formula = mpg ~ hp * wt, data = mtcars)
Residuals:
Min 1Q Median 3Q Max
-3.0632 -1.6491 -0.7362 1.4211 4.5513
Coefficients:
Estimate Std. Error t value Pr(>|t|)
(Intercept) 49.80842 3.60516 13.816 5.01e-14 ***
hp -0.12010 0.02470 -4.863 4.04e-05 ***
wt -8.21662 1.26971 -6.471 5.20e-07 ***
hp:wt 0.02785 0.00742 3.753 0.000811 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Residual standard error: 2.153 on 28 degrees of freedom
Multiple R-squared: 0.8848, Adjusted R-squared: 0.8724
F-statistic: 71.66 on 3 and 28 DF, p-value: 2.981e-13
该模型写作如下:
| “平均 MPG” | = | 49.80842 − 0.12010 × “马力” |
|---|---|---|
| − 8.21662 × “重量” | ||
| + 0.02785 × “马力 : 重量” | ||
| = | 49.80842 − 0.12010 × “马力” | |
| − 8.21662 × “重量” | ||
| + 0.02785 × “马力” × “重量” |
这里提供的第二版本模型方程首次揭示了交互作用,该交互作用表示为两个预测变量值的乘积,这正是拟合模型用来预测响应的方式。(从技术上讲,这与至少一个预测变量是分类变量的情况相同——但是虚拟编码只会为相应项生成零和一,因此乘法实际上只是表示某一项的存在或缺失,正如你所看到的那样。)
你可以通过考虑系数的符号(+或−)来解释两个连续预测变量之间的交互作用。负值表明,随着预测变量值的增加,响应结果在计算了主效应后的数值会减少。正值,正如这里的情况,表明随着预测变量值的增加,效果会进一步增加,对平均响应产生放大作用。
从上下文来看,hp和wt的负主效应表明,重量更大、动力更强的汽车通常会降低燃油经济性。然而,交互效应的正向性表明,随着马力或重量的增加,这种对响应的影响会“减弱”。换句话说,主效应所带来的负相关关系,在预测变量值越来越大的时候,会变得“不那么极端”。
图 21-9 对比了仅包含主效应的模型版本(通过lm使用公式mpg~hp+wt获得;在本节中未显式拟合)与上述交互作用模型版本,后者作为对象car.fit进行了拟合。

图 21-9:按马力和重量绘制的平均 MPG 响应曲面,左侧为仅含主效应的模型,右侧为包含连续预测变量之间双向交互作用的模型
绘制的响应曲面显示了垂直的z轴上的平均 MPG,以及标记的两个预测变量的水平轴。你可以根据给定的马力和重量值,将预测的平均 MPG 解释为曲面上的一个点。请注意,随着你沿各自的水平轴增加任一预测变量的值,两个曲面上的 MPG(沿z轴垂直方向)都呈下降趋势。
我将在第二十五章中展示这些图是如何创建的。目前,它们仅用于突出在car.fit中交互作用的“减弱”影响。左侧的仅主效应模型显示了一个根据每个预测变量的负线性斜率递减的平面。然而,在右侧,正交互作用项的存在使得这个平面变得平坦,这意味着随着预测变量值的增加,下降的速率变得更慢。
21.5.5 高阶交互作用
如前所述,双向交互作用是回归方法应用中最常见的交互作用类型。这是因为对于三向或更高阶的交互项,你需要更多的数据来可靠地估计交互效应,并且有许多解释上的复杂性需要克服。三向交互作用比双向效应更为罕见,而四向及以上的交互作用则更为稀有。
在练习 21.1 中,你使用了boot包中的nuclear数据集(该包随标准 R 安装一起提供),该数据集包括美国核电站建设的数据。在这些练习中,你主要关注与建设许可证相关的日期和时间预测变量,以建模核电站的平均建设成本。为了简化这个例子,假设你没有这些预测变量的数据。仅使用描述电站本身特征的变量,能否充分地建模建设成本?
加载boot包并访问?nuclear帮助页面,了解变量的详细信息:cap(描述工厂容量的连续变量);cum.n(作为连续变量,描述工程师之前参与过的类似建筑数量);ne(二元变量,描述工厂是否位于美国东北部);ct(二元变量,描述工厂是否拥有冷却塔)。
以下模型以工厂的最终建设成本作为响应变量;容量的主效应;以及cum.n、ne和ct之间的主效应、所有二阶交互效应和三阶交互效应:
R> nuc.fit <- lm(cost~cap+cum.n*ne*ct,data=nuclear)
R> summary(nuc.fit)
Call:
lm(formula = cost ~ cap + cum.n * ne * ct, data = nuclear)
Residuals:
Min 1Q Median 3Q Max
-162.475 -50.368 -8.833 43.370 213.131
Coefficients:
Estimate Std. Error t value Pr(>|t|)
(Intercept) 138.0336 99.9599 1.381 0.180585
cap 0.5085 0.1127 4.513 0.000157 ***
cum.n -24.2433 6.7874 -3.572 0.001618 **
ne -260.1036 164.7650 -1.579 0.128076
ct -187.4904 76.6316 -2.447 0.022480 *
cum.n:ne 44.0196 12.2880 3.582 0.001577 **
cum.n:ct 35.1687 8.0660 4.360 0.000229 ***
ne:ct 524.1194 200.9567 2.608 0.015721 *
cum.n:ne:ct -64.4444 18.0213 -3.576 0.001601 **
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Residual standard error: 107.3 on 23 degrees of freedom
Multiple R-squared: 0.705, Adjusted R-squared: 0.6024
F-statistic: 6.872 on 8 and 23 DF, p-value: 0.0001264
在这段代码中,你通过增加与*连接的变量数量来指定高阶交互作用(使用*而不是:,因为你希望包括这三个预测变量的所有低阶效应)。
在估计结果中,cap的主效应为正,表明电力容量的增加与建设成本的增加相关。所有其他主效应为负,表面上看似表明减少的建设成本与更有经验的工程师、东北地区建造的工厂以及拥有冷却塔的工厂相关。然而,这并不是准确的陈述,因为你还没有考虑到这些预测变量的交互项。所有估计的二阶交互效应为正——无论是否有冷却塔,更多经验的工程师意味着东北地区的建设成本更高,而更多经验的工程师也意味着对于拥有冷却塔的工厂,成本更高,无论所在地区。
对于东北地区的工厂,拥有冷却塔的工厂成本也大幅增加,无论工程师的经验如何。尽管如此,负三阶交互作用表明,在计算了主效应和二阶交互效应后,具有更多经验的工程师在东北地区工作并且工厂有冷却塔时,增加的成本有所减少。
至少,这个例子突显了理解高阶交互作用的模型系数所涉及的复杂性。也有可能是由于未考虑的潜在变量,导致统计显著的高阶交互作用的出现。也就是说,显著的交互作用可能只是数据中模式的虚假表现,而这些模式可以通过包含这些遗漏预测变量的更简单的项来解释得更好(如果不是更好)。部分原因上,这促使了对适当的模型选择的重视,这是接下来讨论的重点。
练习 21.3
将注意力转回到MASS包中的cats数据框。在练习 21.1 的前几个问题中,你拟合了仅包含主效应的模型,用以根据体重和性别预测家猫的心脏重量。
-
再次拟合该模型,这一次包括两个预测变量之间的交互作用。检查模型摘要。与之前仅含主效应的版本相比,参数估计值和它们的显著性有何不同?
-
绘制心脏重量与体重的散点图,使用不同的点字符或颜色根据性别区分观察值。使用
abline添加两条线表示拟合模型。这个图与练习 21.1 (d)中的图有何不同? -
使用新模型预测 Tilman 猫的心脏重量(记住 Sigma 是一个 3.4 公斤的雌性猫),并提供 95%的预测区间。将其与之前练习中的仅主效应模型进行比较。
在练习 21.2 中,你访问了trees数据框,这是贡献的faraway包中的一部分。加载包后,访问?trees帮助文件;你将看到之前使用过的体积和树干周长的测量值,以及每棵树的高度数据。
-
在不对数据进行任何转换的情况下,拟合并检查一个仅包含主效应的模型,用于预测体积与树干周长和高度之间的关系。然后,拟合并检查包含交互作用的第二个版本的模型。
-
重复(d),但这次使用所有变量的对数变换。与未变换模型相比,变换后的模型中交互作用的显著性有何变化?这对数据中的关系有什么启示?
回到mtcars数据集,查看?mtcars帮助文件以回顾其中的变量。
-
基于
hp和factor(cyl)之间的双向交互作用及其主要效应,以及wt的主要效应,为mpg拟合一个线性模型。生成拟合的摘要。 -
解释马力与(分类的)气缸数之间交互作用的估计系数。
-
假设你有意购买一辆 1970 年代的性能车。你的妈妈建议你购买一辆“实用且经济”的车,要求平均 MPG 值至少为 25。你看到三辆车的广告:车 1 是一辆四缸、100 马力的车,重 2100 磅;车 2 是一辆八缸、210 马力的车,重 3900 磅;车 3 是一辆六缸、200 马力的车,重 2900 磅。
-
使用你的模型预测三辆车的平均 MPG;提供 95%的置信区间。仅根据点估计,你会建议哪辆车给你妈妈?
-
你仍然想要一辆最省油的车,且希望得到妈妈的批准,所以你决定偷偷地根据置信区间告诉你的信息来做决定。这样会改变你选择的车辆吗?
-
本章重要代码
| 函数/运算符 | 简要描述 | 首次出现 |
|---|---|---|
I |
包含算术项 | 第 21.4.1 节,第 505 页 |
: |
交互项 | 第 21.5.2 节,第 516 页 |
* |
交叉因子算符 | 第 21.5.3 节,第 519 页 |
第二十二章:22
线性模型选择与诊断

你现在已经花费了相当多的时间研究线性回归模型的许多方面。在这一章中,我将介绍如何使用正式的 R 工具和技术来研究回归的另两个方面,这两个方面同样重要:为你的分析选择合适的模型,并评估你所做假设的有效性。
22.1 拟合优度与复杂度
拟合任何统计模型的总体目标是忠实地表示数据及其内部关系。一般而言,拟合统计模型归结为在两者之间取得平衡:拟合优度和复杂度。拟合优度指的是获得一个最能表示响应变量与预测变量(或多个预测变量)之间关系的模型的目标。复杂度描述了一个模型的复杂程度;它始终与模型中需要估计的项数相关——更多的预测变量和附加函数(如多项式变换和交互项)的加入会使模型更复杂。
22.1.1 简约原则
统计学家将拟合优度和复杂度之间的平衡称为简约原则,其相关的模型选择目标是找到一个尽可能简单的模型(换句话说,具有相对较低的复杂度),同时又不牺牲太多的拟合优度。我们可以说,满足这一概念的模型是简约的拟合。你常常会听到研究人员谈论选择“最佳”模型——他们实际上是在指简约原则的概念。
那么,你如何决定在这种平衡上划定界限呢?自然,统计显著性在其中发挥着作用——而模型选择通常归结为评估预测变量或预测变量函数对响应的影响是否显著。为了使这一过程尽可能客观,你可以使用系统化的选择算法,例如在第 22.2 节中学习到的算法,来决定多个解释变量及其相关函数之间的选择。
22.1.2 一般指南
执行任何类型的模型选择或将多个模型进行比较,都涉及到关于是否包含可用预测变量的决策。在这个话题上,有几个指南是你应当始终遵循的。
• 首先,重要的是要记住,你不能在给定的模型中删除分类预测变量的单独水平;这样做是没有意义的。换句话说,如果某个非参考水平具有统计显著性,而其他所有水平都不显著,那么你应该将该分类变量整体视为对平均响应的统计显著贡献。只有在所有非参考系数都与缺乏证据(反对为零)相关时,你才应考虑完全删除该分类预测变量。这同样适用于涉及分类预测变量的交互项。
• 如果拟合的模型中存在交互效应,则所有相关预测变量的低阶交互项和主效应必须保留在模型中。这一点在第 21.5.1 节中有所提及,当时我讨论了交互效应作为低阶效应的扩展。例如,只有在拟合的模型中没有涉及该预测变量的交互项时,才应考虑删除该预测变量的主效应(即使该主效应的p-值很高)。
• 在使用某一解释变量的多项式变换的模型中(参考第 21.4.1 节),如果最高次幂被认为是显著的,则应保留模型中的所有低阶多项式项。例如,包含三次多项式变换的预测变量模型,必须同时包含该变量的一级和二级变换。这是由于多项式函数的数学特性——只有通过明确地将线性、二次和三次(以此类推)效应作为独立项分开,才能避免将这些效应相互混淆。
22.2 模型选择算法
模型选择算法的任务是以某种系统的方式筛选你可用的解释变量,以确定哪些变量能最好地联合描述响应,而不是像之前那样仅通过单独考察预测变量的特定组合来拟合模型。
模型选择算法可能存在争议。它有几种不同的方法,没有一种方法对于每个回归模型都是普遍适用的。不同的选择算法可能会得出不同的最终模型,正如你将看到的那样。在许多情况下,研究人员会有关于问题的额外信息或知识,这会影响决策——例如,某些预测变量必须始终包括,或者它们的包括根本没有意义。必须同时考虑这些因素,以及其他复杂情况,例如交互效应或未观察到的潜在变量对显著关系的影响,以及确保任何拟合模型在统计上是有效的(这一点你将在第 22.3 节中看到)。
记住统计学家乔治·博克斯(George Box,1919–2013)的一句名言很有帮助:“所有模型都是错误的,但有些模型是有用的。”
任何你拟合的模型都不能被假设为真,但一个经过仔细检查的拟合模型可以揭示数据的有趣特征,从而有可能通过提供这些特征的定量估计,揭示关联性和关系。
22.2.1 嵌套比较:部分 F 检验
部分F检验可能是比较多个不同模型的最直接方式。它考察两个或多个嵌套模型,其中较小、较简单的模型是较大、较复杂模型的简化版本。正式地说,假设你拟合了两个线性回归模型,如下所示:

在这里,简化模型ŷ[redu]有p个预测变量,加上一个截距项。完整模型ŷ[full]有q个预测变量项。这个符号表示q > p,并且,除了标准的截距项
外,完整模型包括了简化模型ŷ[redu]的所有p个预测变量,以及q − p个额外项。这强调了模型ŷ[redu]是嵌套在ŷ[full]*中的事实。
需要注意的是,增加回归模型中的预测变量数量总是会提高R²以及其他拟合优度的衡量标准。然而,真正的问题是,拟合优度的改善是否足够大,以至于包括任何额外的预测变量项所增加的复杂性“是值得的”。正是这个问题,部分F检验试图在嵌套回归模型的背景下回答。它的目的是测试是否包括那些额外的q − p项(这些项构成完整模型而不是简化模型)能显著改善拟合优度。部分F检验所解决的假设如下:

计算测试统计量以检验这些假设的过程遵循与 R 在总结拟合的线性模型对象时自动生成的总体F检验背后的相同思想(详见第 21.3.5 节)。将简化模型和完整模型的决定系数分别表示为
和
。如果n表示用来拟合这两个模型的数据样本大小,那么测试统计量为:

在假设(22.1)中的 H[0]下,遵循F分布,df[1] = q − p,df[2] = n − q自由度。p-值通过通常的方式,从
的上尾区域得出;它越小,越能反驳原假设,即认为一个或多个额外的参数对响应变量没有影响。
以第 21.3.1 节中的模型对象survmult和survmult2为例。survmult模型旨在基于MASS包中的survey数据框,通过写手跨和性别来预测学生的平均身高;survmult2则在这些预测变量中加入了吸烟状态。如果需要,可以回到第 21.3.1 节重新拟合这两个模型。将这些对象打印到控制台,可以预览这两个拟合结果,并且容易确认较小的模型确实嵌套在较大的模型中,其解释变量是相同的:
R> survmult
Call:
lm(formula = Height ~ Wr.Hnd + Sex,
data =
survey)
Coefficients:
(Intercept) Wr.Hnd SexMale
137.687 1.594 9.490
R>
survmult2
Call:
lm(formula = Height ~ Wr.Hnd + Sex + Smoke, data =
survey)
Coefficients:
(Intercept) Wr.Hnd SexMale SmokeNever SmokeOccas SmokeRegul
137.4056 1.6042 9.3979 -0.0442 1.5267 0.9211
一旦你拟合了嵌套模型,R 可以使用anova函数进行部分 F 检验(部分 F 检验属于方差分析方法的一部分)。要判断将Smoke作为预测变量是否能显著改善拟合效果,只需从简化模型开始,并将模型对象作为参数传入。
R> anova(survmult,survmult2)
Analysis of Variance
Table
Model 1: Height ~ Wr.Hnd + Sex
Model 2: Height ~ Wr.Hnd + Sex +
Smoke
Res.Df RSS Df Sum of
Sq F Pr(>F)
1 204
9959.2
2 201
9914.3 3 44.876 0.3033 0.823
输出提供了与计算
和
以及检验统计量
(来自式(22.2))相关的量,这些内容在结果表中以F的形式给出,是最受关注的。通过打印survmult和survmult2时得到的p和q值,你应该能够确认,例如,表格第二行中Df和Res.Df列中出现的 df[1]和 df[2]的值。
这个特定测试的结果,来自与 df[1] = 3 和 df[2] = 201 相关的检验统计量
,得到一个较高的p-值 0.823,表明没有反对 H[0] 的证据。这意味着,将Smoke加入到包含解释变量Wr.Hnd和Sex的简化模型中,在建模学生身高时并没有显著改善拟合效果。这个结论并不令人惊讶,因为之前在第 21.3.1 节中看到,所有非参考水平的Smoke都没有显著的p-值。
这就是部分 F 检验在模型选择中的应用——在当前示例中,简化模型将是更简洁的拟合模型,并且更倾向于选择它而非完整模型。
你可以在一次anova调用中比较多个嵌套模型,这对于调查例如是否包含交互项或包含预测变量的多项式变换等情况非常有用,因为自然的层次结构要求你保留任何较低阶的项。
例如,我们可以使用faraway包中的diabetes数据框,来自第 21.5.2 节,该模型拟合用于预测胆固醇水平(chol)与年龄(age)和体型(frame)之间的关系,以及这两个预测变量的交互作用。在使用部分F检验比较嵌套模型之前,你需要确保每个模型使用的是相同的记录,并且在所有预测变量中没有缺失值,这样才能确保所有“更全面”的模型使用相同的样本量进行比较。为此,你只需要首先定义一个版本的diabetes,该版本删除了在你使用的预测变量中包含缺失值的记录。
加载faraway包,并使用逻辑子集选择方法,找出并删除age或frame中存在缺失值的个体。定义这个新的diabetes对象版本:
R> diab <- diabetes[-which(is.na(diabetes$age) |
is.na(diabetes$frame)),]
现在,使用新的diab对象拟合以下四个模型:
R> dia.model1 <- lm(chol~1,data=diab)
R> dia.model2 <-
lm(chol~age,data=diab)
R> dia.model3 <-
lm(chol~age+frame,data=diab)
R> dia.model4 <-
lm(chol~age*frame,data=diab)
第一个模型仅包含截距,第二个模型加入了age作为预测变量,第三个模型包含了age和frame,第四个模型则包括了交互项。可以看到模型之间存在嵌套关系,随着每一步模型复杂度的增加,你可以比较拟合优度改善的显著性。
R> anova(dia.model1,dia.model2,dia.model3,dia.model4)
Analysis
of Variance Table
Model 1: chol ~ 1
Model 2: chol ~ age
Model 3:
chol ~ age + frame
Model 4: chol ~ age *
frame
Res.Df RSS Df Sum of
Sq F Pr(>F)
1 389
747265
2 388
712078 1 35187 19.6306 1.227e-05
***
3 386
697527 2 14551 4.0589 0.01801
*
4 384
688295 2 9233 2.5755 0.07743
.
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' '
1
如果你没有删除那些包含缺失值的记录,你会收到一个错误提示,告诉你四个模型的数据集大小不一致。
结果本身表明,包含age显著改善了chol的建模;包含frame的主效应进一步带来了轻微的改善;而包括交互效应对拟合优度的提升几乎没有证据,甚至可以说没有任何证据。从这些结果来看,你可能更倾向于使用dia.mod3,即仅包含主效应的模型,作为这四个模型中最简洁的均值胆固醇表现方式。
22.2.2 前向选择
部分F检验是检查嵌套模型的自然方式,但如果你有许多不同的模型需要拟合,尤其是当有许多预测变量时,管理起来可能会很困难。
这就是前向选择(也称为前向消除)的应用场景。其思路是从一个仅包含截距的模型开始,然后通过一系列独立的检验,确定哪些预测变量显著改善了拟合优度。接着,你会通过加入这些变量来更新模型对象,并对剩余的变量执行一系列检验,以确定哪些变量会进一步改善拟合。这个过程会一直重复,直到没有任何变量在统计学上显著提高拟合优度为止。R 语言中的add1和update函数可以执行这些检验并更新你拟合的回归模型。
你将使用boot库中的nuclear数据框作为示例,来自第 21.1 节的第 499 页和第 21.5.5 节。目标是选择最具信息量的模型来预测建筑成本。加载boot库并访问帮助文件?nuclear,以提醒自己变量定义。首先,仅用一个总截距项拟合建筑成本模型。
R> nuc.0 <- lm(cost~1,data=nuclear)
R>
summary(nuc.0)
Call:
lm(formula = cost ~ 1, data =
nuclear)
Residuals:
Min 1Q Median 3Q Max
-254.05
-151.24 -13.46 150.40
419.68
Coefficients:
Estimate
Std. Error t value
Pr(>|t|)
(Intercept) 461.56 30.07 15.35
4.95e-16 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05
'.' 0.1 ' ' 1
Residual standard error: 170.1 on 31 degrees of freedom
你从之前的实验中知道,这个特定的模型对于cost的可靠预测相当不足。所以,考虑以下代码行来开始前向选择(我已经抑制了输出,稍后会单独展示并讨论):
R>
add1(nuc.0,scope=.~.+date+t1+t2+cap+pr+ne+ct+bw+cum.n+pt,test="F")
add1的第一个参数总是你要更新的模型。第二个参数scope非常重要——你必须提供一个公式对象,定义你考虑拟合的“最完整”模型。通常你会使用.~.符号,点号表示第一个参数中模型的定义。具体来说,点号代表“已经存在的部分”。换句话说,通过scope,你告诉add1,你考虑的最完整模型是以cost作为响应变量,包含一个截距项,以及nuclear数据框中所有其他预测变量的主效应(为了演示方便,我将完整模型限制为仅包含主效应)。你无需提供数据框作为参数,因为数据已经包含在第一个参数中的模型对象里。最后,你需要告诉add1要执行的测试。这里有一些可用的变体(参见?add1),但在此你将坚持使用test="F"进行部分F检验。
现在,关注add1执行后直接提供的输出。
Single term additions
Model:
cost ~
1
Df Sum of
Sq RSS AIC F
value Pr(>F)
<none> 897172
329.72
date 1 334335 562837 316.80
17.8205 0.0002071
***
t1 1 186984 710189
324.24 7.8986 0.0086296
**
t2 1 27
897145 331.72 0.0009
0.9760597
cap 1 199673 697499
323.66 8.5881 0.0064137
**
pr 1 9037
888136 331.40 0.3052
0.5847053
ne 1 128641
768531 326.77 5.0216 0.0325885
*
ct 1 43042 854130
330.15 1.5118
0.2284221
bw 1 16205
880967 331.14 0.5519
0.4633402
cum.n 1 67938 829234
329.20 2.4579
0.1274266
pt 1 305334
591839 318.41 15.4772 0.0004575 ***
---
Signif. codes: 0 '***'
0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
输出包含一系列行,从<none>开始(表示对当前模型没有任何修改)。你会得到Sum of Sq和RSS值,这些值与计算测试统计量直接相关。自由度的差异也会被报告。另一个简洁性的度量AIC也会提供(你将在第 22.2.4 节中详细查看)。
最相关的是测试结果;使用test="F"时,每一行对应一个独立的部分F检验,比较第一个参数中的模型(ŷ[redu])与仅添加该行项后的模型(ŷ[full])。因此,通常你会通过仅添加具有最大(和“最显著”)改进的项来更新模型。
在这里,你应该能看到将date作为预测变量添加对建模cost提供了最大的显著改进。所以,接下来我们更新nuc.0,在代码中加入该项。
R> nuc.1 <- update(nuc.0,formula=.~.+date)
R>
summary(nuc.1)
Call:
lm(formula = cost ~ date, data =
nuclear)
Residuals:
Min 1Q Median 3Q Max
-176.00
-105.27 -25.24 58.63 359.46
Coefficients:
Estimate
Std. Error t value Pr(>|t|)
(Intercept)
-6553.57 1661.96 -3.943 0.000446
***
date 102.29 24.23 4.221
0.000207 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05
'.' 0.1 ' ' 1
Residual standard error: 137 on 30 degrees of
freedom
Multiple
R-squared: 0.3727, Adjusted
R-squared: 0.3517
F-statistic: 17.82 on 1 and 30 DF, p-value:
0.0002071
在update中,你将想要更新的模型作为第一个参数,第二个参数formula告诉update如何更新该模型。再次使用.~.符号,指示通过将date添加为预测因子来更新nuc.0,从而生成一个与第一个参数同类的拟合模型对象。对新模型nuc.1调用summary以查看结果。
所以,我们继续吧!再次调用add1,但这次将nuc.1作为第一个参数。
R>
add1(nuc.1,scope=.~.+date+t1+t2+cap+pr+ne+ct+bw+cum.n+pt,test="F")
Single term
additions
Model:
cost ~
date
Df Sum of
Sq RSS AIC F
value Pr(>F)
<none> 562837
316.80
t1 1 15322
547515 317.92 0.8115
0.3750843
t2 1 68161
494676 314.67 3.9959 0.0550606
.
cap 1 189732 373105 305.64
14.7471 0.0006163
***
pr 1 4027
558810 318.57 0.2090
0.6509638
ne 1 92256
470581 313.07 5.6854 0.0238671
*
ct 1 54794 508043
315.52 3.1277 0.0874906
.
bw 1 1240
561597 318.73 0.0640
0.8020147
cum.n 1 4658 558179
318.53 0.2420
0.6264574
pt 1 90587
472250 313.18 5.5628 0.0252997 *
---
Signif. codes: 0
'***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
注意,现在没有为date添加行;它已经在nuc.1中。似乎下一个最有信息量的添加项是cap。将nuc.1更新为此。
R> nuc.2 <- update(nuc.1,formula=.~.+cap)
现在继续进行,测试并更新。通过对nuc.2调用add1(此处未显示输出),你会发现下一个最显著的添加项是pt(仅略微更为显著)。更新为一个新对象nuc.3,它包含以下项:
R> nuc.3 <- update(nuc.2,formula=.~.+pt)
然后再次测试,使用add1对nuc.3进行测试。你会发现有弱证据表明可以额外加入ne的主效应,因此更新并包含它,生成nuc.4。
R> nuc.4 <- update(nuc.3,formula=.~.+ne)
在这一点上,你可以相当确定不会有更多有用的添加项,但还是通过对最新拟合模型进行最后一次add1调用来彻底检查。
R>
add1(nuc.4,scope=.~.+date+t1+t2+cap+pr+ne+ct+bw+cum.n+pt,test="F")
Single term
additions
Model:
cost ~ date + cap + pt +
ne
Df Sum of
Sq RSS AIC F value
Pr(>F)
<none> 222617
293.12
t1 1 107.0
222510 295.10 0.0125
0.9118
t2 1 19229.9 203387
292.23 2.4583
0.1290
pr 1 5230.8 217386
294.36 0.6256
0.4361
ct 1 15764.7 206852
292.77 1.9815
0.1711
bw 1 448.0
222169 295.06 0.0524
0.8207
cum.n 1 13819.9 208797
293.07 1.7209 0.2010
确实看来,如果将其纳入模型,剩余的协变量都不会显著提高拟合优度,因此你的最终模型将保持为nuc.4。
R> summary(nuc.4)
Call:
lm(formula = cost ~ date +
cap + pt + ne, data =
nuclear)
Residuals:
Min 1Q Median 3Q Max
-157.894 -38.424 -2.493 35.363 267.445
Coefficients:
Estimate
Std. Error t value Pr(>|t|)
(Intercept)
-4.756e+03 1.286e+03 -3.699 0.000975
***
date 7.102e+01 1.867e+01 3.804
0.000741
***
cap 4.198e-01 8.616e-02 4.873
4.29e-05
***
pt -1.289e+02 4.950e+01 -2.605
0.014761
*
ne 9.940e+01 3.864e+01 2.573
0.015908 *
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.'
0.1 ' ' 1
Residual standard error: 90.8 on 27 degrees of
freedom
Multiple R-squared:
0.7519, Adjusted R-squared:
0.7151
F-statistic: 20.45 on 4 and 27 DF, p-value: 7.507e-08
这种方法可能看起来有点繁琐,有时很难决定用哪个最完整的模型作为scope,但它是一个非常好的方式,让你在每个选择阶段都能保持参与,从而仔细考虑每个添加项。然而,请注意,存在一定的主观性;通过选择一个项而非另一个,可能会得出不同的最终模型,比如你可能会添加pt而不是date(它们在第一次调用add1时具有相似的显著性水平)。
22.2.3 后向选择
学习了前向选择后,理解后向选择(或淘汰法)并不难。如你所猜测的那样,前向选择是从一个简化的模型开始,通过添加项逐步构建最终模型,而后向选择则从最完整的模型开始,系统地去除项。这个过程的 R 函数是drop1,用于检查部分F检验,以及update。
前向选择与后向选择的选择通常是根据具体情况决定的。如果最完整的模型未知或难以定义和拟合,则通常倾向于使用前向选择。另一方面,如果你有一个自然且容易拟合的最完整模型,那么后向选择可能更方便实现。有时,研究人员会同时进行两者,以查看他们最终得到的模型是否有所不同(这是完全可能的情况)。
再次回顾nuclear示例。首先,定义最完整的模型,即通过所有可用协变量的主效应来预测cost(就像你在前向选择中使用scope时所做的那样)。
R> nuc.0 <-
lm(cost~date+t1+t2+cap+pr+ne+ct+bw+cum.n+pt,data=nuclear)
R>
summary(nuc.0)
Call:
lm(formula = cost ~ date + t1 + t2 + cap + pr + ne
+ ct + bw +
cum.n + pt, data =
nuclear)
Residuals:
Min 1Q Median 3Q Max
-128.608 -46.736 -2.668 39.782 180.365
Coefficients:
Estimate
Std. Error t value Pr(>|t|)
(Intercept)
-8.135e+03 2.788e+03 -2.918 0.008222
**
date 1.155e+02 4.226e+01 2.733
0.012470
*
t1 5.928e+00 1.089e+01 0.545
0.591803
t2 4.571e+00 2.243e+00 2.038
0.054390
.
cap 4.217e-01 8.844e-02 4.768
0.000104
***
pr -8.112e+01 4.077e+01 -1.990
0.059794
.
ne 1.375e+02 3.869e+01 3.553
0.001883
**
ct 4.327e+01 3.431e+01 1.261
0.221008
bw -8.238e+00 5.188e+01 -0.159
0.875354
cum.n -6.989e+00 3.822e+00 -1.829
0.081698
.
pt -1.925e+01 6.367e+01 -0.302
0.765401
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.'
0.1 ' ' 1
Residual standard error: 82.83 on 21 degrees of
freedom
Multiple R-squared:
0.8394, Adjusted R-squared:
0.763
F-statistic: 10.98 on 10 and 21 DF, p-value: 2.844e-06
显然,有几个预测变量似乎对响应变量没有显著贡献,这些相同的结果在你第一次使用drop1检查每个变量删除后对拟合优度的影响时也很明显。
R> drop1(nuc.0,test="F")
Single term
deletions
Model:
cost ~ date + t1 + t2 + cap + pr + ne + ct + bw + cum.n
+ pt
Df Sum of
Sq RSS AIC F
value Pr(>F)
<none> 144065
291.19
date 1 51230 195295
298.93 7.4677 0.0124702
*
t1 1 2034
146099 289.64 0.2965
0.5918028
t2 1 28481
172546 294.97 4.1517 0.0543902
.
cap 1 155943 300008 312.67
22.7314 0.0001039
***
pr 1 27161
171226 294.72 3.9592 0.0597943
.
ne 1 86581 230646
304.25 12.6207 0.0018835
**
ct 1 10915
154980 291.53 1.5911
0.2210075
bw 1 173
144238 289.23 0.0252
0.8753538
cum.n 1 22939 167004
293.92 3.3438 0.0816977 .
pt 1 627
144692 289.33 0.0914 0.7654015
---
Signif. codes: 0
'***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
drop1的一个方便功能是,它的scope参数是可选的。如果你不包含scope,它默认为仅截距模型作为“最简化”模型,通常这是一个合理的选择。
在直接进入删除过程之前,提醒自己你所做的事情的解释。就像在前向选择中添加任何项总是会改善拟合优度一样,在后向选择中删除任何项总是会使拟合优度变差。真正的问题是这些拟合质量变化的显著性。就像之前一样,你只希望添加那些在拟合优度上提供统计显著改进的项,在删除项时,你只希望删除那些不会导致拟合优度的统计显著恶化的项。因此,后向选择是前向选择的完全反向过程。
所以,从drop1的输出中,你需要选择从模型中删除的项,它对拟合优度的影响最小。换句话说,你在寻找的是其部分F检验中p-值最大且不显著的项——因为删除p-值显著小的项会显著恶化回归模型的预测能力。
在当前示例中,似乎预测变量bw对拟合优度的减少影响最不显著,因此让我们从nuc.0中删除该项开始更新。
R> nuc.1 <- update(nuc.0,.~.-bw)
在这个选择算法中使用update和之前一样;不过现在,你使用-符号表示删除项,遵循标准的“已经存在的”.~.符号表示法。
然后,使用最新的模型nuc.1重复此过程:
R> drop1(nuc.1,test="F")
Single term
deletions
Model:
cost ~ date + t1 + t2 + cap + pr + ne + ct + cum.n +
pt
Df Sum of
Sq RSS AIC F
value Pr(>F)
<none> 144238
289.23
date 1 55942 200180
297.72 8.5326 0.007913
**
t1 1 3124
147362 287.92 0.4765
0.497245
t2 1 30717
174955 293.41 4.6852 0.041546
*
cap 1 159976 304214 311.11
24.4005 6.098e-05
***
pr 1 27140
171377 292.75 4.1395 0.054122
.
ne 1 86408 230646
302.25 13.1795 0.001479
**
ct 1 11815
156053 289.75 1.8021 0.193153
cum.n 1 24048 168286
292.17 3.6680 0.068557
.
pt 1 930
145168 287.44 0.1419 0.710039
---
Signif. codes: 0 '***'
0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
看起来pt是下一个最合理的主效应删除项。执行删除,并将结果对象命名为nuc.2。
R> nuc.2 <- update(nuc.1,.~.-pt)
现在继续前进,通过调用drop1进行重新检查(未显示),你会发现预测变量t1表现为另一个可以删除的项。更新你的模型并删除该预测变量;将模型对象命名为nuc.3。
R> nuc.3 <- update(nuc.2,.~.-t1)
重新检查新的nuc.3与drop1。你现在应该发现ct的效应仍然不显著,因此删除该项并再次更新,得到新的nuc.4。
R> nuc.4 <- update(nuc.3,.~.-ct)
再次使用drop1进行检查,这次检查的是nuc.4。此时,你可能会对再删除任何预测变量感到犹豫,因为删除这些变量会与它们的显著性强度相关。然而需要注意的是,对于剩余的至少三个预测变量——t2、pr和cum.n,它们的统计显著性可能最多只能算是边缘显著——它们的p-值都在传统的显著性水平β = 0.01 和β = 0.05 之间。这再次强调了研究人员在选择模型时必须发挥的主动作用,尤其是在前向或后向选择算法中;是否要删除更多的变量是一个难以回答的问题,最终取决于你的判断。
让我们将nuc.4作为最终模型。总结一下,你可以看到估计的回归参数以及通常的拟合后统计数据。
R> summary(nuc.4)
Call:
lm(formula = cost ~ date + t2
+ cap + pr + ne + cum.n, data =
nuclear)
Residuals:
Min 1Q Median 3Q Max
-152.851 -53.929 -8.827 53.382 155.581
Coefficients:
Estimate
Std. Error t value Pr(>|t|)
(Intercept)
-9.702e+03 1.294e+03 -7.495 7.55e-08 ***
date 1.396e+02 1.843e+01 7.574
6.27e-08
***
t2 4.905e+00 1.827e+00 2.685
0.012685
*
cap 4.137e-01 8.425e-02 4.911
4.70e-05
***
pr -8.851e+01 3.479e+01 -2.544
0.017499
*
ne 1.502e+02 3.400e+01 4.419
0.000168
***
cum.n -7.919e+00 2.871e+00 -2.758
0.010703 *
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.'
0.1 ' ' 1
Residual standard error: 80.8 on 25 degrees of
freedom
Multiple R-squared:
0.8181, Adjusted R-squared:
0.7744
F-statistic: 18.74 on 6 and 25 DF, p-value: 3.796e-08
立刻你就可以看到,尽管两者的完整模型相同,但你在第 22.2.2 节中从前向选择得到的最终模型与这里选出的最终模型不同。这是怎么发生的呢?
简单来说,答案是模型中的预测变量相互影响。记住,当前预测变量的估计系数在你控制不同变量时很容易发生变化。随着预测变量项数的增加,这些关系变得越来越复杂,因此选择算法的顺序和方向可能会引导你走上不同的路径,并得出不同的最终结论,这正是这里发生的情况。
作为一个完美的例子,考虑nuclear数据中pt的主效应。在前向选择中,pt被加入,因为它对模型cost~date+cap的改进最为“显著”。在后向选择中,pt被早早移除,因为它在从模型cost~date+t1+t2+cap+pr+ne+ct+cum.n+pt中移除时,对拟合优度的减少最小。这意味着,对于后者模型,pt可能在预测结果方面的贡献,已经被其他现有的预测变量解释了。在较小的模型中,这一效应尚未被解释,因此pt是一个有吸引力的新增变量。
所有这一切都突显了大多数选择算法的反复无常,尽管它们的实施方式是系统的。重要的是要认识到,最终模型的拟合可能会因不同方法而异,你应该将这些选择方法视为找到最简约模型的有用指南,而不是提供普适的、最终的解决方案。
22.2.4 步进 AIC 选择法
一系列部分F-检验的应用是最常见的基于检验的模型选择方法,但这并不是研究人员可用的唯一工具。你还可以通过采用基于准则的方法来找到简约性。最著名的准则之一是赤池信息准则(Akaike’s Information Criterion,简称 AIC)。你可能已经注意到,这个值作为add1和drop1输出中的一列。
对于给定的线性模型,AIC 的计算公式如下:

这里,
是一个名为对数似然的拟合优度度量,p是模型中的回归参数数量,排除了整体截距。
的值是用于拟合模型的估计过程的直接结果,尽管其确切计算超出了本文的范围。需要知道的是,它对拟合得更好的模型取较大的值。
公式 (22.3) 产生一个值,它用
来奖励拟合优度,同时用 2 × (p + 2)来惩罚模型复杂性。与
相关的负号和p + 2 的正号意味着 AIC 值较小的模型表示更为简洁的模型。
要找到拟合线性模型的 AIC,你可以在lm函数返回的对象上使用AIC或extractAIC函数;查看这些函数的帮助文件,了解两者的技术差异。
(因此 AIC 值)没有直接的解释意义,只有在你将其与另一个模型的 AIC 进行比较时才有意义。你可以根据 AIC 选择模型,方法是找出 AIC 值最小的拟合模型。这就是为什么add1和drop1的输出中直接报告了 AIC 的原因——你可以根据导致 AIC 最小变化的项的添加或删除来决定添加或删除哪个项,而不是仅仅关注通过F检验得出的变化的显著性。
让我们更进一步,结合前向选择和后向选择的思想。逐步模型选择允许删除当前项或添加缺失项,通常是基于 AIC 来实施的。也就是说,选择添加或删除的项是基于所有可能操作中,能最大程度减少 AIC 的单一操作。这让你在探索候选模型的过程中有了更大的灵活性,最终模型的拟合是通过选择一个没有任何添加或删除能进一步降低 AIC 值的模型来决定的。
你可以通过在每个阶段使用add1或drop1自己实现逐步 AIC 选择,但幸运的是,R 提供了内建的step函数来为你完成这项工作。使用过去几章中的MASS包中的mtcars数据。让我们最终尝试获得一个关于平均油耗的模型,该模型提供了包括所有可用预测变量的机会。
首先,查看?mtcars中的文档,并再次查看数据的散点图矩阵,以帮助你回忆变量及其在 R 数据框对象中的格式。然后定义起始模型(通常称为空模型)为仅包含截距的模型。
R> car.null <- lm(mpg~1,data=mtcars)
你的起始模型可以是你喜欢的任何模型,只要它符合你为step提供的scope参数描述的模型范围。在这个例子中,定义scope为要考虑的最完整模型——设置为一个过于复杂的模型,包含wt、hp、cyl和disp之间的四阶交互作用(以及通过交叉因子操作符获得的所有相关低阶交互作用和主效应),同时包括am、gear、drat、vs、qsec和carb的主效应。这两个多级分类变量,cyl和gear,被显式转换为因子,以避免它们被当作数值型处理(参见第 20.5.4 节)。
最终模型中的交互作用潜力将突出显示add1、drop1和step的一个特别重要(且方便)的特点。这些函数都会遵循交互作用和主效应所强加的层次结构。也就是说,对于add1(和step),除非所有相关的低阶效应已经出现在当前的拟合模型中,否则交互项不会作为添加选项提供;同样地,对于drop1(和step),除非所有相关的高阶效应已经从当前拟合模型中删除,否则交互项或主效应不会作为删除选项提供。
step函数本身返回一个拟合的模型对象,并且默认会提供每个选择阶段的详细报告。现在让我们调用它;为了打印输出的方便,一些输出已经被剪切掉了,所以你可以在自己的机器上查看完整的输出。
R> car.step <-
step(car.null,scope=.~.+wt*hp*factor(cyl)*disp+am
+factor(gear)+drat+vs+qsec+carb)
Start: AIC=115.94
mpg
~
1
Df
Sum of Sq RSS AIC
+
wt 1 847.73 278.32 73.217
+
disp 1 808.89 317.16 77.397
+
factor(cyl) 2 824.78 301.26 77.752
+
hp 1 678.37 447.67 88.427
+
drat 1 522.48 603.57 97.988
+
vs 1 496.53 629.52 99.335
+
factor(gear) 2 483.24 642.80 102.003
+
am 1 405.15 720.90
103.672
+
carb 1 341.78 784.27
106.369
+
qsec 1 197.39 928.66
111.776
<none> 1126.05
115.943
Step: AIC=73.22
mpg ~
wt
Df
Sum of Sq RSS AIC
+
factor(cyl) 2 95.26 183.06 63.810
+
hp 1 83.27 195.05 63.840
+
qsec 1 82.86 195.46 63.908
+
vs 1 54.23 224.09 68.283
+
carb 1 44.60 233.72 69.628
+
disp 1 31.64 246.68 71.356
+
factor(gear)
2 40.37 237.95 72.202
<none> 278.32 73.217
+
drat 1 9.08 269.24 74.156
+
am 1 0.00 278.32 75.217
-
wt 1 847.73
1126.05 115.943
Step: AIC=63.81
mpg ~ wt +
factor(cyl)
Df
Sum of Sq RSS AIC
+
hp 1 22.281
160.78 61.657
+ wt:factor(cyl) 2 27.170 155.89
62.669
<none> 183.06
63.810
+
qsec 1 10.949
172.11 63.837
+
carb 1 9.244
173.81 64.152
+
vs 1 1.842
181.22 65.487
+
disp 1 0.110
182.95 65.791
+
am 1 0.090
182.97 65.794
+
drat 1 0.073
182.99 65.798
+
factor(gear) 2 6.682 176.38
66.620
- factor(cyl) 2 95.263
278.32 73.217
-
wt 1 118.204
301.26 77.752
Step: AIC=61.66
mpg ~ wt + factor(cyl) +
hp
--snip--
Step: AIC=55.9
mpg ~ wt + factor(cyl) + hp +
wt:hp
--snip--
Step: AIC=52.8
mpg ~ wt + hp +
wt:hp
--snip--
Step: AIC=52.57
mpg ~ wt + hp + qsec +
wt:hp
Df
Sum of
Sq RSS AIC
<none> 121.04
52.573
-
qsec 1 8.720
129.76 52.799
+ factor(gear) 2 9.482 111.56
53.962
+
am 1 1.939
119.10 54.056
+
carb 1 0.080
120.96 54.551
+
drat 1 0.012
121.03 54.570
+
vs 1 0.010
121.03 54.570
+
disp 1 0.008
121.03 54.571
+ factor(cyl) 2 0.164
120.88 56.529
-
wt:hp 1 65.018
186.06 64.331
每个输出块展示了当前模型的拟合情况、其 AIC 值,以及一个显示可能操作的表格(包括添加+、删除-或不做任何操作<none>)。每个操作单独执行后的 AIC 值会列出,并且这些潜在的单一操作会按从小到大的 AIC 值进行排序。
随着算法的进行,你会看到<none>行在表格中逐渐上升。例如,在第一个表格中,仅包含截距模型的 AIC 值为 115.94。AIC 值的最大下降将来自添加wt的主效应;在做出这个调整后,重新评估后续调整对 AIC 的影响。还需注意,wt与factor(cyl)之间的二阶交互项只在第三步考虑,前提是这些预测因子的主效应已经被添加。然而,这个特定的二阶交互项最终并没有被纳入模型,因为在第三步时,hp的主效应更优,且随后的涉及hp的交互项在第四步中能显著降低 AIC 值。事实上,在第五步,实际删除factor(cyl)的主效应被认为能最大程度地降低 AIC,因此第六步和第七步的表格中不再包含wt:factor(cyl)项。第六步建议添加qsec的主效应能略微减少 AIC,因此这一调整得以执行。第七个表格标志着算法的结束,因为此时不做任何调整即可获得最低的 AIC 值,任何其他操作都会增加 AIC(这一点通过<none>在最后表格中占据首位得以体现)。
最终模型被存储为对象car.step;通过对其进行总结,你会发现几乎 90%的响应变化被体重、马力及它们的交互作用所解释,另外还有稍微显得有些奇怪的qsec主效应(尽管该效应本身被认为在统计上并不显著)。
R> summary(car.step)
Call:
lm(formula = mpg ~ wt + hp
+ qsec + wt:hp, data =
mtcars)
Residuals:
Min 1Q Median 3Q Max
-3.8243
-1.3980 0.0303 1.1582
4.3650
Coefficients:
Estimate
Std. Error t value Pr(>|t|)
(Intercept)
40.310410 7.677887 5.250 1.56e-05 ***
wt -8.681516 1.292525 -6.717
3.28e-07
***
hp -0.106181 0.026263 -4.043
0.000395
***
qsec 0.503163 0.360768 1.395
0.174476
wt:hp 0.027791 0.007298 3.808
0.000733 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05
'.' 0.1 ' ' 1
Residual standard error: 2.117 on 27 degrees of
freedom
Multiple R-squared:
0.8925, Adjusted
R-squared: 0.8766
F-statistic: 56.05 on 4 and 27 DF, p-value:
1.094e-12
从这一点来看,似乎值得进一步探讨这个预测因子,以验证拟合模型的有效性(见第 22.3 节),并且或许可以尝试对数据进行转换(例如将 GPM 建模而非 MPG;见第 21.4.3 节),以观察这一效应是否会在之后的逐步 AIC 算法运行中持续存在。qsec出现在最终模型中,说明模型选择不仅仅基于预测因子贡献的显著性,而是基于一种标准化的衡量方法,旨在追求其对简洁性的定义。
AIC 有时因倾向于偏向更复杂的模型和更高的p值而受到批评。为平衡这一点,你可以通过增加右侧方程式(22.3)中(p + 2)的乘法贡献,来增强额外预测变量的惩罚效应;尽管在大多数情况下,标准的乘法因子为 2(在step函数中,你可以使用可选参数k来更改此值)。话虽如此,基于准则的度量在你拥有非嵌套模型时(排除了部分F-检验)非常有用,这时你可以快速比较它们,找出可能提供最简洁数据表示的模型。
习题 22.1
在第 22.2.2 节和第 22.2.3 节中,你使用了前向选择和后向选择方法来寻找一个用于预测核电站建设成本的模型(基于boot包中的nuclear数据框)。
-
使用相同的最全模型(换句话说,只包含所有现有预测因子的主效应),使用逐步 AIC 选择来找到适合数据的模型。
-
在 (a) 中找到的最终模型与之前使用前向选择和后向选择得到的模型是否匹配?它们有什么不同?
习题 21.2 在第 512 页详细介绍了伽利略的球数据。如果你还没有这样做,将这些数据作为数据框输入到你当前的 R 工作空间中。
-
对这些数据拟合五个线性模型,以距离作为响应变量——一个仅包含截距的模型和四个逐渐增加阶数 1 到 4 的多项式模型。
-
构建一个部分F-检验表,以确定你偏好的距离旅行模型。你的选择是否与习题 21.2 (b) 和 (c) 一致?
你第一次遇到diabetes数据框是在贡献的faraway包的第 21.5.2 节中,在那里你建立了总胆固醇的均值模型。加载该包并通过?diabetes检查文档,以便回顾数据集的内容。
-
在
diabetes数据框中有一些缺失值,这可能会干扰模型选择算法。定义一个新的diabetes数据框版本,删除以下变量中任何一个存在缺失值的行:chol、age、gender、height、weight、frame、waist、hip、location。提示:使用na.omit或者你对数据框中记录提取或删除的知识。你可以使用which和is.na创建需要提取或删除的行号向量,或者你可以尝试使用complete.cases函数来获得一个逻辑标志向量——查看其帮助文件以了解更多细节。 -
使用(e)中的数据框,拟合两个线性模型,以
chol为响应变量。空模型对象,命名为dia.null,应为仅含截距的模型。完全模型对象,命名为dia.full,应为包含age、gender、weight和frame之间的四阶交互作用(及所有低阶项);waist、height和hip之间的三阶交互作用(及所有低阶项);以及location的主效应的过于复杂的模型。 -
从
dia.null开始,并使用与dia.full相同的scope中的术语,通过 AIC 实现步进选择法来选择一个平均总胆固醇的模型,并总结结果。 -
使用基于部分F检验的前向选择法,采用常规显著性水平α = 0.05 来选择模型,仍然从
dia.null开始。这里的结果是否与(g)中得到的模型相同? -
步进选择法不一定从最简单的模型开始。重复(g)步骤,但这次将
dia.full设置为起始模型(如果从最复杂的模型开始,scope不需要提供任何内容)。如果从dia.full开始,AIC 最终选择的模型是什么?与(g)中的最终模型有何不同?你认为这是为什么,还是没有差别?
重新审视MASS包中的普遍使用的mtcars数据框。
- 在 22.2.4 节中,你使用了步进 AIC 选择法来建模平均 MPG。所选模型包括了
qsec的主效应。重新运行相同的 AIC 选择过程,但这次用 GPM=1/MPG 来做。这样会改变最终模型的复杂度吗?
22.2.5 其他选择算法
任何模型选择算法都始终旨在定量定义简约性,并建议在可用数据的基础上优化该定义。有一些 AIC 的替代方案,比如修正 AIC (AIC[c])或贝叶斯信息准则 (BIC),它们比默认的 AIC(在 22.3 中)对复杂度的惩罚更重。
有时,人们很容易只关注一系列模型的R²,即决定系数。然而,如 22.2.1 节中所提到的,单独依赖这一指标不足以在模型之间进行选择,因为它没有对复杂度进行惩罚,并且通常随着你继续添加预测变量而增加,无论这些变量是否具有统计学意义。调整后的 R² 统计量,用
表示,并在summary中以Adjusted R-squared报告,是原始R²的一个简单转换,考虑了相对于样本大小n的复杂度惩罚;计算公式为:

其中 p 是预测变量的数量(不包括截距项)。基于测试和标准的算法总是更可取的(因为解释
可能比较困难),但监控
作为嵌套模型之间的快速检查也很有用——更高的值指示更优的模型。
欲进一步阅读,Faraway 的第八章(2005)对基于测试和标准的模型选择程序的指导性性质进行了精彩的评论。无论您采用哪种方法,请始终记住,使用这些算法得到的最终模型仍然需要审查。
22.3 残差诊断
在前面的章节中,您已经研究了多元线性回归模型的实际方面,如拟合和解释、虚拟变量编码、变量变换等,但您尚未涉及确定模型有效性的必要方法。本章的最后部分将向您介绍模型诊断,其主要目标是确保您的回归模型有效并准确地表示数据中的关系。为此,我将重点回到早些时候在第 21.2.1 节中提到的多元线性回归模型的理论假设。
作为复习,一般来说,在拟合这些模型时,请记住以下四点:
误差 误差项ɛ定义了任何观测值与拟合的均值结果模型之间的偏差,假定其服从正态分布,均值为零,常数方差用σ²表示。给定观测值的误差也假定与其他观测值的误差独立。如果拟合模型表明违反了这些假设中的任何一项,则需要进一步调查(通常涉及重新拟合模型的变体)。
线性 假设均值响应作为回归参数β[0]、β[1]、...、β[p]的函数是线性的至关重要。尽管对单个变量的变换和交互项的存在可以在一定程度上放宽估计趋势的具体性质,但任何诊断结果表明关系是非线性的(因此未被拟合模型捕捉到)都必须进行调查。
极端或不寻常的观测值 始终检查极端数据点或对拟合模型有强烈影响的数据点——例如,记录错误的数据点应从分析中删除。
共线性 高度相关的预测变量可能会对整个模型产生不利影响,这意味着很容易误解任何包含预测变量的效应。在任何回归分析中都应避免这种情况。
在使用诊断工具拟合模型后,你会检查前三项。任何对这些假设的违反都会降低模型的可靠性,有时会严重影响结果。共线性和/或极端观测值可以通过对原始数据进行基本的统计探索(例如,查看散点图矩阵)在拟合前发现,但任何后续的影响则在拟合后进行评估。
你可以执行一些统计检验来诊断统计模型,但通常诊断检查归结为解释专门设计的图形工具的结果,这些工具旨在针对特定的假设。解释这些图形可能相当困难,只有通过经验才会变得更加容易。在这里,我将概述这些在 R 中的工具,并描述一些常见的检查内容。有关更详细的讨论,请参阅专门的回归方法书籍,例如 Chatterjee 等人(2000),Faraway(2005),或 Montgomery 等人(2012)。
22.3.1 残差的检查与解释
如果你回顾一下图 20-2 中的图表(位于 456 页),你将看到一个很好的示范,展示了将 ŷ 作为均值响应值解释结果的重要性。在假定的模型下,原始观测值与拟合直线的任何偏离都被认为是由方程(20.1)中 ɛ 项所定义的(正态分布的)误差所导致的,方程见 453 页。
当然,在实践中,你无法获得真实的误差值,因为你并不知道数据的真实模型。对于第 i 个响应观测值 y[i] 及其模型拟合值 ŷ[i],你通常会使用估计残差 e[i] = y[i] − ŷ[i] 来评估诊断图。对 summary 的调用甚至鼓励通过提供 e[i] 的五数概括在估计系数表上方进行拟合后分析。这使你能够查看它们的值,并对其分布的对称性进行初步的数值评估(这也是正态性假设所要求的——参见第 22.3.2 节)。
除了对原始残差 e[i] 进行诊断检查外,还可以使用它们的 标准化(或 Student 化)值进行一些诊断检查。标准化残差将原始残差 e[i] 重新缩放,以确保它们具有相同的方差,这在需要直接相互比较时非常重要。正式地,这通过计算
来实现,其中
是残差标准误差的估计值,h[ii] 是第 i 个观测值的 杠杆作用(你将在第 22.3.4 节中学习杠杆作用)。
可以说,最常用的残差后验分析图形工具是简单的散点图,将“观察值减去拟合值”的原始残差绘制在纵轴上,横轴则是回归模型的相应拟合值。如果关于ɛ的假设有效,那么e[i]应该随机分布在零周围(因为假设误差与响应变量的值之间没有任何关系)。散点图中的任何系统性模式都表明残差与误差假设不符——这可能是由于数据中的非线性关系或观察值之间存在依赖性(换句话说,你的数据点是相关的,因此彼此之间并不独立)。该图还可以用于检测异方差性——即残差的方差不恒定——通常表现为随着拟合值的增加,残差围绕零“张开”。
再次提醒,确保这些理论假设有效非常重要,因为它们影响回归系数估计值的有效性以及其标准误差的可靠性(进而影响统计显著性)——换句话说,它们影响你对其对响应变量影响的解释是否正确。
为了让你更好地理解这一点,请参考图 22-1 中的三张图。这些图展示了三个假设情境下的残差与拟合值的散点图。
左侧的图基本上就是你所期望看到的——残差随机分布在零周围,且其分布的范围似乎是恒定的(同方差性)。然而,在中间的图中,你可以看到残差中存在系统性行为。尽管可变性似乎在拟合值范围内保持恒定,但明显的趋势表明当前模型未能解释响应与预测变量(或预测变量)之间的某些关系。在右侧的图中,残差似乎再次随机分布在零周围。然而,它们所表现出的可变性并不恒定。除了其他因素外,这种异方差性会影响你置信区间和预测区间的可靠性。

图 22-1:假设的线性回归残差与拟合值诊断图的三种印象:随机(左)、系统性(中)、异方差性(右)
需要注意的是,即使你的图形诊断结果不像图 22-1 左侧的假设性示例那样表现良好,也不意味着应该立即放弃分析。这些图形可以作为找到适合你数据的模型的一个重要组成部分。你通常可以通过加入额外的预测变量或交互作用、改变分类变量的处理方式,或对某些连续预测变量进行非线性变换来减少非线性问题。在某些研究领域,异方差性,特别是图 22-1 中那种拟合值较高时波动性较大的情况,是常见的。解决这个问题的第一步通常是对响应变量进行简单的对数变换,然后重新检查诊断结果。
现在是时候看一个例子了。在第 22.2.4 节中,你使用逐步 AIC 选择法为 mtcars 数据选择了一个模型,用于预测 MPG,创建了对象 car.step。现在,让我们诊断一下这个拟合结果,看看模型假设是否存在问题。
当你将 plot 函数直接应用于 lm 对象时,它可以方便地生成六种类型的拟合诊断图。默认情况下,这四个图会依次生成。按照控制台中的提示 "Hit which 参数来单独选择每个图(指定整数 1 到 6;详见 ?plot.lm 文档)。残差与拟合图通过 which=1 生成;以下行会生成左侧的图,图 22-2。
R> plot(car.step,which=1)
如你所见,R 会添加一条平滑线,帮助用户解读任何趋势,尽管这一点不应该作为唯一依据来做出判断。默认情况下,零点附近的三个最极端的数据点会被标注(根据拟合模型时所用数据框的 rownames 属性)。模型公式本身会在横轴标签下方指定。
从中可以看出,car.step 的残差与拟合值图几乎没有任何值得关注的地方。没有明显的趋势,而且你可以进一步放心,因为误差(e[i])似乎在其分布中是同方差的。

图 22-2:残差与拟合值及尺度-位置诊断图,适用于 car.step 模型
规模-位置图类似于残差与拟合图,不过它不是在纵轴上绘制原始的e[i],而是提供
,即标准化残差的绝对值的平方根(用| · |表示;这使得所有负值变为正值)。这些值与横轴上的拟合值进行绘制。通过这种方式限制关注每个残差的大小,规模-位置图用于揭示每个数据点与拟合值之间偏差大小的趋势,随着拟合值的增大。这意味着,诸如异方差性(heteroscedasticity)等问题,规模-位置图可能比原始的残差与拟合图更有用。就像原始的残差与拟合图一样,你希望看到一个没有明显模式的图形,这表明没有违反任何误差假设。
图 22-2 的右图展示了car.step的规模-位置图,这是通过which=3选择的。该图还演示了如何使用add.smooth参数去除默认的平滑趋势线,并控制如何使用id.n参数标记多少个极端点。
R> plot(car.step,which=3,add.smooth=FALSE,id.n=2)
与原始的残差与拟合图类似,对于这个mtcars模型,规模-位置图似乎也没有什么值得担忧的地方。
返回到伽利略的滚球数据,这些数据最初在练习 21.2 中展示,位于第 512 页。在下面的例子中,响应变量“行驶距离”作为列d给出,解释变量“高度”作为列h,数据框gal中的数据。我将重新创建一些练习内容,给你展示几个简单的残差诊断图中值得关注的例子。执行以下代码来定义包含七个观测值的数据框,并拟合两个回归模型——第一个是关于高度的线性模型,第二个是二次模型(有关多项式变换的详细信息,请参见第 21.4.1 节)。
R> gal <-
data.frame(d=c(573,534,495,451,395,337,253),
h=c(1,0.8,0.6,0.45,0.3,0.2,0.1))
R>
gal.mod1 <- lm(d~h,data=gal)
R> gal.mod2 <- lm(d~h+I(h²),data=gal)
现在,看看图 22-3 中的三幅图,这些图是使用以下代码创建的:
R> plot(gal$d~gal$h,xlab="Height",ylab="Distance")
R>
abline(gal.mod1)
R> plot(gal.mod1,which=1,id.n=0)
R>
plot(gal.mod2,which=1,id.n=0)

图 22-3:展示了伽利略滚球数据的残差诊断。左上:原始数据及与之相对应的简单线性趋势gal.mod1叠加图。右上:仅针对线性趋势模型的残差与拟合值图。下方:针对二次模型gal.mod2的残差与拟合值图。
左上方的图展示了数据,并提供了简单线性模型的直线。尽管这清晰地捕捉到了增长趋势,但该图表明也存在一些曲率。右上方的诊断残差与拟合图表明,仅有线性趋势的模型是不充分的——系统性模式提出了关于线性模型误差假设的警示。底部图像展示了基于gal.mod2模型二次版本的残差与拟合图。包括“高度”的二次项去除了残差中的明显曲线。然而,这些最新的e[i]值似乎仍然表现出波动的系统性行为,或许暗示你应尝试一个三次模型,但在如此小的样本量下,这非常困难。
22.3.2 评估正态性
为了评估误差正态分布的假设,你可以使用正态 QQ 图,正如在第 16.2.2 节中首次讨论的那样。在调用lm对象的plot时,选择which=2来生成(标准化)残差的正态分位数-分位数图。返回car.step模型对象并输入以下代码来生成图 22-4。
R> plot(car.step,which=2)

图 22-4:来自car.step模型的残差正态 QQ 图
你可以像在第 16.2.2 节中那样解释残差的 QQ 图。灰色对角线表示真实的正态分位数,绘制的点则是估计回归误差的相应数值分位数。正态分布的数据应该接近这条直线。
对于car.step回归模型,点图大体上似乎遵循理论正态分位数所描绘的路径。存在一定的偏差,这是可以预期的,但没有明显的正态性偏离。
还有其他检测正态性的方法,例如著名的 Shapiro-Wilk 假设检验。Shapiro-Wilk 检验的原假设是数据服从正态分布,因此,较小的p值会暗示数据非正态性(详细技术细节请参见 Royston, 1982)。要执行此过程,可以使用 R 中的shapiro.test函数。首先通过rstandard提取拟合模型的标准化残差,你会发现对car.step应用此检验会得到一个较大的p值。
R>
shapiro.test(rstandard(car.step))
Shapiro-Wilk
normality test
data: rstandard(car.step)
W = 0.97105, p-value
= 0.5288
换句话说,根据这个检验,并没有证据表明car.step的残差不服从正态分布。
假设误差项正态分布有助于支持用于产生回归系数可靠估计的方法。只要你的数据是大致正态的,你就不需要太担心轻微的非正态性迹象。对数据进行一些变换,以及增加样本量,可以减少对更严重的非正态残差迹象的担忧。
22.3.3 说明异常值、杠杆值和影响力
始终重要的是调查那些与大多数观测值相比显得不寻常或极端的单个观测值。一般来说,进行数据的探索性分析,可能涉及汇总统计或散点矩阵,是一个好主意,因为它可以帮助你识别这些值——它们有可能对模型拟合产生不利影响。在继续之前,澄清一些常用的术语是很重要的。
异常值 这是一个用于描述数据中不寻常观测值的通用术语,正如你在第 13.2.6 节中看到的那样。在线性回归中,异常值通常有较大的残差,但只有当其不符合拟合模型的趋势时,才被视为异常值。异常值可以,也不一定,会显著改变拟合模型所描述的趋势。
杠杆值 这个术语指的是当前预测值的极端性。高杠杆点是那些预测值极端到足以可能显著影响拟合模型的斜率或趋势的观测值。异常值可以有高杠杆值或低杠杆值。
影响力 一个具有高杠杆值的观测值,如果确实影响了估计的趋势,就被认为是有影响力的。换句话说,影响力只有在考虑响应值与相应的预测值一起时,才能做出判断。
这些定义有一定的重叠,因此给定的观测值可以用这些术语的组合来描述。让我们来看一些假设的例子。创建以下两个包含十个假设响应(y)和解释(x)值的向量:
R> x <- c(1.1,1.3,2.3,1.6,1.2,0.1,1.8,1.9,0.2,0.75)
R> y
<- c(6.7,7.9,9.8,9.3,8.2,2.9,6.6,11.1,4.7,3)
这些将构成数据的主体部分。现在,考虑以下六个对象,p1x到p3y,它们将用于存储三个附加观测点的预测值和响应值:
p1x <- 1.2
p1y <- 14
p2x <- 5
p2y <-
19
p3x <- 5
p3y <- 5
也就是说,点 1 是 (1.2,14);点 2 是 (5,19);点 3 是 (5,5)。
接下来,以下四个lm的使用提供了四个简单的线性模型拟合。第一个是y对x的回归。接下来的三个分别将点 1、2 和 3 作为第 11 个观测值单独包含进去。
R> mod.0 <- lm(y~x)
R> mod.1 <-
lm(c(y,p1y)~c(x,p1x))
R> mod.2 <- lm(c(y,p2y)~c(x,p2x))
R> mod.3
<- lm(c(y,p3y)~c(x,p3x))
现在,你可以使用这些对象来直观地澄清异常值、杠杆值和影响力的定义,如图 22-5 所示。输入以下代码来初始化散点图,并设置x和y的轴限:
R> plot(x,y,xlim=c(0,5),ylim=c(0,20))
然后使用points、abline和text来构建图 22-5 的左上角图,如下所示:
R> points(p1x,p1y,pch=15,cex=1.5)
R> abline(mod.0)
R>
abline(mod.1,lty=2)
R> text(2,1,labels="Outlier, low leverage, low
influence",cex=1.4)
通过将p1x、p1y和mod.1替换为对应于点 2 和 3 的对象,并修改text中的labels参数,来创建中间和右侧的图。

图 22-5:线性回归上下文中异常值、杠杆值和影响力的三种定义示例。在每个图中,实线表示拟合到原始观察数据的模型,基于x和y,而虚线表示包含额外点后拟合的模型,额外点用▪表示。
在图 22-5 的左上图中,额外的点是一个异常值的例子,因为它与大部分数据分开,并且不符合原始观察结果所暗示的趋势。尽管如此,因其预测值为 1.2 (p1x),与其他 x 值相比并不显得异常,因此它被认为杠杆值较低。实际上,它靠近 x 值的整体均值,表明将其包含在 mod.1 中,主要是对原始截距的修正。你甚至可以将其归类为一个低影响点——拟合模型的整体变化似乎很小。
在右上图中,你可以看到一个示例,说明了一个不被视为异常值的观察点。尽管点 2 确实与原始的 10 个观察点有所不同,但它很好地符合仅基于x和y拟合的模型,这在回归分析中非常重要。话虽如此,点 2 被认为是一个高杠杆点,因为它位于一个极端的预测值位置,与所有其他 x 值相比(换句话说,它有潜力在响应值不同的情况下显著改变拟合结果)。就目前而言,它是一个低影响点,因为模型本身几乎不受它的包含影响。
最后,底部图展示了一个明显的异常值,位于高杠杆位置,并且具有高影响力——它远离原始的 10 个观察点,并未明显符合原始趋势;其极端的预测值意味着它具有高杠杆;而其包含则显著改变了整个模型,拉低了斜率并提高了截距。这些概念在更高维度(即当你有多个预测变量时)对于多元线性回归模型仍然适用。
22.3.4 计算杠杆值
杠杆值的计算是基于第 21.2.2 节中定义的设计矩阵结构X。具体来说,如果你有n个观察点,那么第 i 个点(i = 1, . . . , n)的杠杆值表示为 h[ii]。这些是n × n 矩阵 H 的对角线元素(第 i 行,第 i 列),其计算方式为:

在 R 中,构造你在第 22.3.3 节中定义的 10 个示例预测观测值x的设计矩阵是通过使用cbind的知识来实现的(参见第 3.1.2 节)。随后,通过矩阵乘法(%*%)、矩阵转置(t)、矩阵求逆(solve)和提取对角线元素(diag)的相应函数计算出H。然后,你可以将值h[ii]与x的值本身绘制出来。以下代码生成了图 22-6:
R> X <- cbind(rep(1,10),x)
R> hii <-
diag(X%*%solve(t(X)%*%X)%*%t(X))
R> hii
[1] 0.1033629 0.1012107
0.3487221 0.1302663 0.1001345 0.3723971 0.1711595
[8] 0.1980630 0.3261232
0.1485607
R> plot(hii~x,ylab="Leverage",main="",pch=4)
通常,你会使用内置的 R 函数hatvalues,该函数以方程式(22.4)中的矩阵代数风格命名,用于获取杠杆值(而不是手动构造设计矩阵X并自己进行计算)。只需将拟合的模型对象提供给hatvalues。你可以通过使用之前拟合到x和y数据的相应lm对象(即之前创建的mod.0)来确认你的计算结果。
R>
hatvalues(mod.0)
1 2 3 4 5 6 7
0.1033629
0.1012107 0.3487221 0.1302663 0.1001345 0.3723971
0.1711595
8 9 10
0.1980630
0.3261232 0.1485607

图 22-6:在x中绘制 10 个示例预测观测值的杠杆值
看图 22-6,将杠杆值与相应的预测值本身绘制在一起,这种趋势是合理的——杠杆值随着你离预测数据的均值越远而逐渐增大。这基本上是你在任何原始杠杆值图中看到的模式。
22.3.5 Cook 距离
当然,仅凭杠杆值不足以确定每个观测值对拟合模型的总体影响。为此,响应值也必须考虑在内。
可以说,最著名的影响度量是Cook 距离,它估计删除第i个值对拟合模型的影响大小。观测值i的 Cook 距离(记作D[i])通过以下方程给出:

事实证明,这个方程是一个特定的函数,表示某个点的杠杆值和残差。这里,值ŷ[j]是使用所有n个观测值拟合的模型对观测值j的预测均值,
表示使用不包括第i个观测值的模型对观测值j的预测均值。像往常一样,术语p是预测回归参数的数量(不包括截距),值
是残差标准误差的估计值。
简单来说,D[i]值越大,第i个观察值对拟合模型的影响越大,这意味着处于高杠杆位置的离群观察值将对应于更高的D[i]值。一个重要的问题是,D[i]的值要多大,才能认为第i个点是有影响力的?在实践中,这个问题没有普遍的答案,也没有正式的假设检验,但有几个经验法则的截止值。一个规则是,如果D[i] > 1,应该认为该点是有影响力的;另一个更为敏感的规则建议D[i] > 4/n(例如,参见 Bollen 和 Jackman, 1990;Chatterjee 等, 2000)。一般建议,比较给定拟合模型的多个 Cook 距离,而不是分析单一值,并且任何对应于相对较大D[i]值的点可能需要进一步检查。
继续使用在第 22.3.3 节中创建的对象,其中包含x和y中的 10 个示例观察值,以及在p1x和p1y中定义的额外点。拟合这些数据的线性回归模型被存储为对象mod.1。编写一些代码计算根据公式(22.5)得到的 Cook 距离度量是一个很好的练习。
为此,在 R 编辑器中输入以下代码:
x1 <- c(x,p1x)
y1 <- c(y,p1y)
n <-
length(x1)
param <- length(coef(mod.1))
yhat.full <-
fitted(mod.1)
sigma <- summary(mod.1)$sigma
cooks <-
rep(NA,n)
for(i in 1:n){
temp.y <-
y1[-i]
temp.x <-
x1[-i]
temp.model <-
lm(temp.y~temp.x)
temp.fitted <-
predict(temp.model,newdata=data.frame(temp.x=x1))
cooks[i]
<- sum((yhat.full-temp.fitted)²)/(param*sigma²)
}
首先,创建新的对象x1和y1来保存所有 11 个观察值。对象n、param和sigma提取数据集的大小、回归参数的总数(此处为 2),以及最初拟合到所有 11 个数据点的模型的估计残差标准误差。后两项,param和sigma,分别表示公式(22.5)中的(p + 1)和
。
为了存储 Cook 距离,使用rep创建了一个包含 11 个位置的向量cooks(初始化为NA)。现在,为了计算每个D[i]值,设置一个for循环(参见第十章),循环遍历从1到11的每个索引。循环的第一步是创建两个临时向量temp.x和temp.y,它们分别是去除第i个观察值后的x1和y1。基于temp.x,对temp.y进行拟合一个新的临时线性模型;然后,predict函数基于temp.model为每个 11 个预测值计算均值响应(换句话说,包括被删除的那个观察值)。这样,得到的向量temp.fitted代表了公式(22.5)中的
值。最后,sum和param与sigma²的乘积计算出D[i],并将结果存储在cooks[i]中。
在高亮并执行代码后,得到的 Cook 距离如下:
R> cooks
[1] 2.425993e-03 4.060891e-07 1.027322e-01
1.844150e-03 2.923667e-03
[6] 7.213229e-02 1.387284e-01 3.021075e-02
7.099904e-03 1.251882e-01
[11] 3.136855e-01
不出所料,最大的值是最后一个,大约为 0.314。这对应于x1和y1中的第 11 个观测点,它是最初在p1x和p1y中定义的额外点。值 0.314 小于 1,也小于 4/11 = 0.364,这是之前经验法则中定义的临界值。这与图 22-5 中左上角图像的评估相一致——即与像p3x和p3y这样的点相比,p1x和p1y的影响是微不足道的。
就像hatvalues函数为你计算杠杆值一样,内建的cooks.distance函数也为D[i]做同样的事情。你可以在cooks中确认之前的值,这些值是基于mod.1的。
R>
cooks.distance(mod.1)
1 2 3 4 5 6
2.425993e-03
4.060891e-07 1.027322e-01 1.844150e-03 2.923667e-03
7.213229e-02
7 8 9 10 11
1.387284e-01
3.021075e-02 7.099904e-03 1.251882e-01 3.136855e-01
当你在plot函数的相关用法中选择which=4时,R 会自动计算并提供 Cook 距离,作为拟合线性模型对象的诊断图。以下代码使用了之前的mod.1、mod.2和mod.3,生成了图 22-7 中的三张图像;这些对应于图 22-5 中的三组数据。
R> plot(mod.1,which=4)
R> plot(mod.2,which=4)
R>
plot(mod.3,which=4)
R> abline(h=c(1,4/n),lty=2)
图 22-7 左上方显示的D[i]与之前你手动计算并存储在cooks中的值相匹配。右上图中所有数据点的影响相对较小,这与你在图 22-5 右上图中看到的情况一致,在那里,额外的点(p2x,p2y)对整体拟合没有产生太大影响。在底部图中,abline叠加了两条水平线,分别标记了值 1(最高线)和 4/11 = 0.364,这两条线都被第 11 个点(p3x,p3y)明显突破了,就像你在图 22-5 中的对应底部图像所预期的那样。

图 22-7:在 R 中生成的 Cook 距离图的三个示例,分别基于 mod.1 (左上), mod.2 (右上),和 mod.3 (底部)
回到你之前建立的car.step模型,你使用mtcars数据集对 MPG 进行了建模,最终的拟合结果是通过第 22.2.4 节中的逐步 AIC 选择得到的。你已经查看过残差与拟合值的关系图以及 QQ 图,分别见图 22-2 和图 22-4。图 22-8 提供了该模型的 Cook 距离图,包含以下两条线:
R> plot(car.step,which=4)
R>
abline(h=4/nrow(mtcars),lty=2)
该图默认标记了残差最高的三个D[i]点;其中两个突破了 4/n = 4/32 = 0.125 的界限。根据基于汽车重量(wt)、马力(hp)和四分之一英里时间(qsec)的各类效应拟合的模型,克莱斯勒帝国和丰田卡罗拉被认为处于高杠杆位置,且残差足够大,被归类为高影响力观察值。还应注意,菲亚特 128 虽然没有突破 0.125 线,但仍然具有相当大的影响力,事实上,它也曾是残差图(图 22-2)和 QQ 图(图 22-4)中标记的极端点之一。

图 22-8:模型在car.step中的库克距离;虚线水平线标记了 4/n,适用于mtcars*数据框
这可能合理地建议你进一步调查这些高影响力的观察值。所有记录都正确吗?你的模型选择是否谨慎?模型是否有其他选择,比如额外的预测变量或转换?你可以探索这些选项,并继续监控库克距离的图(以及其他诊断图)。
无论你的结果如何,有影响力的数据点并不一定意味着你的模型存在严重问题——这只是一个帮助你检测那些在预测值组合上极端,并且残差较大的观察值的工具,这些观察值的响应值偏离了模型所预测的趋势。这在多元回归中尤其有用,因为响应与预测数据的高维性可能使得传统的单一图表可视化原始数据变得困难。
22.3.6 图形化组合残差、杠杆值和库克距离
来自plot的最后两个诊断图结合了标准化残差、杠杆值和第i个观察值的库克距离。这些组合图特别有助于你判断是高杠杆值还是大残差,或者两者共同作用,导致了高影响力的观察值。
使用数据模型mod.1、mod.2和mod.3,输入以下代码,设置which=5,以生成图 22-9 左列中的三个图像:
R>
plot(mod.1,which=5,add.smooth=FALSE,cook.levels=c(4/11,0.5,1))
R>
plot(mod.2,which=5,add.smooth=FALSE,cook.levels=c(4/11,0.5,1))
R>
plot(mod.3,which=5,add.smooth=FALSE,cook.levels=c(4/11,0.5,1))

图 22-9:标准化残差与杠杆值的组合诊断图(左列)以及库克距离与杠杆值的组合诊断图(右列),适用于三个示例模型 mod.1 (上), mod.2 (中),以及 mod.3 (下)
这些图展示了每个观测值的杠杆值在横轴上的位置,标准化残差在纵轴上的位置。作为残差和杠杆的函数,Cook 距离可以作为等高线绘制在这些散点图上。这些等高线划定了图中的空间区域,表示高影响(位于右侧极端角落处)。
越是接近零的横轴线,点的残差就越小。位于横轴左侧的点,其杠杆值较小。如果一个点根据其杠杆(x轴)位置远离横轴,它将突破标记某些D[i]值的等高线(默认值为 0.5 和 1),表示该点具有高影响力。实际上,通过这些图中从左到右等高线逐渐变窄可以看出,如果一个观测值位于高杠杆位置,则将其分类为高影响点会更容易,这完全符合直觉。在之前使用plot和which=5时,cook.levels参数的可选项用于为这三个例子包含 4/11 的经验法则等高线。
mod.1的图显示添加的点(p1x,p1y)具有较大的残差,但由于其位于低杠杆位置,因此并未突破 4/11 等高线。mod.2的图显示添加的点(p2x,p2y)位于高杠杆位置,但由于其残差较小,因此没有表现出影响力。最后,mod.3的图清楚地识别出添加的点(p3x,p3y)为高度有影响力的点——具有较大的残差和高杠杆,明显突破了高层次等高线。回顾这三个示例数据集之前的所有图,可以明显看到,这三幅图清楚地反映了每个单独添加的额外观测值的性质。
最后的诊断图使用which=6,显示了与which=5组合诊断相同的信息,但这次纵轴显示的是 Cook 距离,横轴显示的是杠杆的转换值,即h[ii]/(1 − h[ii])。这种转换放大了较大杠杆点在横轴上的位置,这一效果部分间接地表现为x轴上的“拉伸”尺度——如果你特别关注与预测变量集合相关的观测值的极端情况,这将非常有用。
因此,这些等高线现在定义了标准化残差,作为缩放杠杆和 Cook 距离的函数。以下三行代码生成了图 22-9 右侧列中的三幅图:
R> plot(mod.1,which=6,add.smooth=FALSE)
R>
plot(mod.2,which=6,add.smooth=FALSE)
R> plot(mod.3,which=6,add.smooth=FALSE)
位于右侧的点是高杠杆点;位于上方的点是高影响点。从图 22-9 的右列图中可以看出,这三个额外的点正如你所预期的那样,根据它们在mod.1、mod.2和mod.3中的特征出现。
对于一个真实数据的例子,返回到存储在car.step中的模型。输入以下代码来生成图 22-10 中的两个组合诊断图:
R>
plot(car.step,which=5,cook.levels=c(4/nrow(mtcars),0.5,1))
R>
plot(car.step,which=6,cook.levels=c(4/nrow(mtcars),0.5,1))

图 22-10:标准化残差与杠杆(左)和库克距离与杠杆(右)的组合诊断图,针对 car.step 模型
图 22-10 中的两张图片显示了 Corolla 和 Imperial 作为具有影响力的观测点,它们的D[i]值大于4/nrow(mtcars)的经验法则截断值。有趣的是,这张图揭示了 Imperial(在图 22-8 中显示其D[i]值远大于其他点)实际上有一个比 Corolla 和 Fiat 128 更小的残差。它的高影响力显然是因为其在car.step中的预测变量位置具有高杠杆性。另一方面,Fiat 128 在整个数据集中拥有最大的残差之一(这也是它在一些早期诊断图中被标记的原因),但由于其相对较低的杠杆位置(完全基于经验法则截断值),它刚好未被标记为高影响力的观测点。
任何线性回归模型都会有比其他观测值更能影响模型的观测点,这些图表旨在帮助你识别它们。但决定如何处理这些具有高度影响力的观测点可能很困难,而且是具体应用问题。虽然一个单一观测点对最终估计模型的影响过大并不理想,但在没有深思熟虑的情况下删除这些观测点也是极其不明智的,因为它们可能指示其他问题,比如当前拟合的不足或先前未检测到的趋势。
练习 22.2
在第 22.2.2 节中,你使用了boot包中的nuclear数据框来说明前向选择方法,其中为cost选择了一个模型,作为date、cap、pt和ne的主要效应的函数。
-
访问数据框;拟合并总结之前描述的模型。
-
检查原始残差与拟合值的关系图,以及残差的正态 QQ 图,并评论你的解释——在这种情况下,线性模型的误差成分的假设是否得到了满足?
-
基于库克距离,确定影响观测点的经验法则截断值。生成库克距离的图,并添加一个水平线对应于截断值。评论你的发现。
-
生成标准化残差与杠杆值的组合诊断图。设置 Cook 距离等高线,以包括(c)中的截止值以及默认等高线。解释该图—如何描述任何具有单独影响力的点?
-
基于(c)和(d),您应该能够识别出在
nuclear中对拟合模型产生最大影响的记录。为了便于讨论,假设该观察值记录错误。重新拟合(a)中的模型,这次从数据框中省略了有问题的行。总结该模型—哪些系数变化最大?生成(b)中的诊断图,并将其与之前的图进行比较。
加载faraway包并访问diabetes数据框。在练习 22.1 (g)中,您使用逐步 AIC 选择法选择了chol的模型。
-
使用
diabetes,拟合在早期练习中识别的多重线性模型,即包括主效应和age与frame之间的二阶交互作用,以及waist的主效应。通过总结拟合结果,确定diabetes中包含缺失值的记录数,这些记录已被从估算中删除。 -
生成原始残差与拟合值的散点图以及 QQ 诊断图,适用于(f)中的模型。评论误差假设的有效性。
-
调查具有影响力的点。利用熟悉的经验法则进行截止值的判断(请注意,您需要从数据框的总大小中减去缺失值的数量,以获得有效的样本量)。在标准化残差与杠杆值的组合图中,使用一次、三次和五次截止值作为 Cook 距离的等高线。
回顾在第 8.2.3 节中关于读取基于网络的文件的讨论。在那里,您调用了一个包含 308 颗钻石价格(以新加坡元为单位)的数据框,以及重量(以克拉为单位—连续型),颜色(分类—六个等级,从D,最少黄色,作为参考级别,到I,最多黄色),清晰度(分类—五个等级,包括IF,几乎无瑕,作为参考级别,VVS1,VVS2,VS1和VS2,最后一个是最不清晰的),以及认证(分类—三个不同钻石认证机构的等级,GIA为参考级,HRD和IGI)。查阅 Chu(2001)的免费文章以获取更多关于这些数据的信息。在连接互联网的情况下,运行以下代码行,这将以对象diamonds读取数据并为每个变量列命名。
R> dia.url <-
"http://www.amstat.org/publications/jse/v9n2/4cdata.txt"
R> diamonds <-
read.table(dia.url)
R> names(diamonds) <-
c("Carat","Color","Clarity","Cert","Price")
-
使用基础 R 图形或
ggplot2,为了更好地了解数据,生成价格与克拉重量的散点图,其中价格放在y轴,克拉重量放在x轴。试着使用颜色来区分不同的数据点,按以下方式:– 钻石清晰度
– 钻石颜色
– 钻石认证
-
拟合一个以
Price为响应变量的多元线性模型,并将其他变量作为预测变量。总结该模型并生成三个诊断图,帮助判断误差项的假设情况。对图表进行评论——你是否认为这是一个适合钻石价格的模型?为什么或为什么不? -
重复(j),但使用
Price的对数变换。同样,检查并评论误差假设的有效性。 -
重复(k),但在对数价格建模时,这次为
Carat包含一个额外的二次项(有关多项式变换的详细信息,请参见第 21.4.1 节)。现在,残差诊断结果如何?
22.4 共线性
拟合回归模型的最后一个方面从技术上讲并不被归类为诊断检查,但它仍然有可能对你从拟合模型中得出的结论的有效性产生不利影响,而且发生的频率足够高,因此值得在这里讨论。共线性(也称为多重共线性)是指两个或更多的解释变量彼此高度相关的情况。
22.4.1 潜在警告信号
两个预测变量之间的高度相关性意味着它们在响应变量方面所包含的信息可能存在某种程度的冗余。这是一个问题,因为它可能会破坏模型的可靠拟合能力,并且如前所述,这可能对后续的基于模型的推断产生不利影响。
在检查模型摘要时,以下项目可能是共线性的潜在警告信号:
• 全局F检验(第 21.3.5 节)结果具有统计显著性,但回归参数的各个t检验结果都不显著。
• 给定系数估计的符号与合理预期相矛盾,例如,喝更多的酒导致血液酒精含量降低。
• 参数估计与异常高的标准误差相关,或者在模型拟合到数据的不同随机记录子集时,标准误差变化剧烈。
如最后一点所示,共线性往往对系数的标准误差(以及相关的结果,如置信区间、显著性检验和预测区间)产生更为不利的影响,而不仅仅是对点预测本身的影响。在大多数情况下,您只需小心就能避免共线性。要注意已存在的变量以及数据是如何收集的。例如,确保您打算包括在模型中的任何预测因子不仅仅是另一个包含预测因子的重尺度值。还建议对数据进行探索性分析,生成总结性统计和基本的统计图表。您可以列出分类变量之间的计数,或者查看连续变量之间的估计相关系数。例如,在后者的情况下,作为一个粗略的指导,一些统计学家建议,0.8 或以上的相关性可能会导致潜在问题。
22.4.2 相关预测因子:一个简短的例子
再次考虑统计学生的survey数据,位于MASS包中。在您查看过的这些数据的多数模型中,您尝试从某些解释性变量预测学生身高,通常包括写字手的手跨度(Wr.Hnd)。帮助页面?survey显示,这些数据还收集了非写字手的手跨度(NW.Hnd)。可以合理预期,这两个变量会高度相关,这也是我之前避免使用NW.Hnd的原因。事实上,执行
R> cor(survey$Wr.Hnd,survey$NW.Hnd,use="complete.obs")
[1]
0.9483103
显示出一个高相关系数,表明学生的写字手与非写字手的手跨度之间存在强正线性关联。换句话说,这两个变量在任何给定的模型中应该代表相同的信息。
现在,您从之前拟合的模型中知道,写字手跨度对预测学生的平均身高有显著且正向的影响。以下代码通过简单线性回归快速确认了这一点。
R>
summary(lm(Height~Wr.Hnd,data=survey))
Call:
lm(formula = Height ~
Wr.Hnd, data =
survey)
Residuals:
Min 1Q Median 3Q Max
-19.7276
-5.0706 -0.8269 4.9473 25.8704
Coefficients:
Estimate
Std. Error t value Pr(>|t|)
(Intercept)
113.9536 5.4416 20.94 <2e-16
***
Wr.Hnd 3.1166 0.2888 10.79 <2e-16
***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' '
1
Residual standard error: 7.909 on 206 degrees of
freedom
(29 observations deleted due to missingness)
Multiple
R-squared: 0.3612, Adjusted
R-squared: 0.3581
F-statistic: 116.5 on 1 and 206 DF, p-value: <
2.2e-16
Wr.Hnd和NW.Hnd之间的高度正相关表明,使用NW.Hnd替代应该会产生类似的效果。
R>
summary(lm(Height~NW.Hnd,data=survey))
Call:
lm(formula = Height ~
NW.Hnd, data =
survey)
Residuals:
Min 1Q Median 3Q Max
-21.8285 -5.1397 -0.2867 4.5611 25.5750
Coefficients:
Estimate
Std. Error t value Pr(>|t|)
(Intercept)
118.0324 5.2912 22.31 <2e-16
***
NW.Hnd 2.9107 0.2818 10.33 <2e-16
***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' '
1
Residual standard error: 8.032 on 206 degrees of
freedom
(29 observations deleted due to
missingness)
Multiple R-squared:
0.3412, Adjusted R-squared:
0.338
F-statistic: 106.7 on 1 and 206 DF, p-value: <
2.2e-16
从这些结果中可以看出,确实是这种情况。
然而,看看如果您同时尝试基于这两个预测因子建模身高时会发生什么:
R>
summary(lm(Height~Wr.Hnd+NW.Hnd,data=survey))
Call:
lm(formula = Height
~ Wr.Hnd + NW.Hnd, data =
survey)
Residuals:
Min 1Q Median 3Q Max
-20.0144 -5.0533
-0.8558 4.7486 25.8380
Coefficients:
Estimate
Std. Error t value Pr(>|t|)
(Intercept)
113.9962 5.4545 20.900 <2e-16
***
Wr.Hnd 2.7451 1.0728
2.559 0.0112
*
NW.Hnd 0.3707 1.0309
0.360 0.7195
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05
'.' 0.1 ' ' 1
Residual standard error: 7.926 on 205 degrees of
freedom
(29 observations deleted due to missingness)
Multiple
R-squared: 0.3616, Adjusted
R-squared: 0.3554
F-statistic: 58.06 on 2 and 205 DF, p-value: <
2.2e-16
由于Wr.Hnd和NW.Hnd对Height的影响相互交织,同时考虑这两者会严重掩盖各自对建模响应的独立贡献。预测因子的统计显著性几乎不存在;至少,这些效应都与比单一预测因子拟合中更高的p-值相关。也就是说,整体F检验仍然高度显著,这给出了之前提到的第一个警告信号的例子。
本章中的重要代码
| 函数/操作符 | 简要描述 | 首次出现 |
|---|---|---|
anova |
部分 F 检验 | 第 22.2.1 节, 第 531 页 |
add1 |
回顾单项添加 | 第 22.2.2 节, 第 533 页 |
update |
修改拟合模型 | 第 22.2.2 节, 第 534 页 |
drop1 |
回顾单项删除 | 第 22.2.3 节, 第 538 页 |
step |
分步 AIC 模型选择 | 第 22.2.4 节, 第 543 页 |
plot(用于 lm 对象) |
模型诊断 | 第 22.3.1 节, 第 551 页 |
rstandard |
提取标准化残差 | 第 22.3.2 节, 第 555 页 |
shapiro.test |
Shapiro-Wilk 正态性检验 | 第 22.3.2 节, 第 555 页 |
hatvalues |
计算杠杆值 | 第 22.3.4 节, 第 558 页 |
cooks.distance |
计算 Cook’s 距离 | 第 22.3.5 节, 第 561 页 |
第五部分
高级图形
第二十三章:23
高级绘图自定义

许多用户最初被 R 吸引,是因为它令人印象深刻的图形灵活性以及可以轻松控制和定制结果视觉效果的能力。在本章中,你将更详细地了解基础 R 图形设备,以及如何微调你已经熟悉的图形,以最大化地利用你的可视化效果。在接下来的章节中,你将进一步扩展你在ggplot2和传统 R 图形中的技巧。
本章的大部分内容假设你已经熟悉第七章和第十四章的内容。一般来说,我还会假设你使用的是标准的基础 R 应用程序(例如,在 Mac 上是R.app,在 Windows 上是Rgui.exe—参见附录 A),因为在不同环境中工作时,一些命令的行为和可用性可能会有所不同。
根据你的操作系统,用于在计算机屏幕上渲染图形显示的软件驱动程序也不同。例如,在 Mac 上的标准R.app应用程序中,你会注意到生成实时图形时会打开一个窗口,窗口的标题可能类似于Quartz 2 []——OS X 的默认图形设备驱动程序是 Quartz 窗口系统。在 Windows 机器上,你会看到R Graphics: Device 2 (ACTIVE)。任何图形设备的编号总是从 2 开始;设备 1 被称为空设备*,意味着当前没有任何设备处于活动状态。
注意
要查看你当前 R 会话可用的设备列表,请在提示符下输入 ?Devices 。你会注意到列表中包括了像 png 和 pdf这样的命令,这些是所谓的静默图形设备,它们启用了直接文件绘图,详细内容请参见第八章。如果你希望为某个绘图使用不同的设备,可以这样做,尽管默认设备通常最为合适,特别是当你直接在屏幕上绘图时。
23.1 处理图形设备
到目前为止,你的绘图操作都是一次处理一张图像。虽然可以同时打开多个图形设备,但在任何给定的时刻,只有一个设备会被认为是活动的(横幅标题会用[]或(ACTIVE)*来高亮当前活动的设备)。当你同时处理多个图形,或者希望查看或修改某一张图而不关闭其他图形时,这非常有用。
23.1.1 手动打开新设备
你已经接触过的典型基础 R 命令(如plot、hist、boxplot等)会自动打开一个绘图设备并绘制所需的图形,如果当前没有打开设备。你也可以使用dev.new来打开新的设备窗口;这个新窗口将立即变为活动设备,之后的所有绘图命令将作用于该设备。
例如,首先关闭所有已打开的图形窗口,然后在 R 提示符下输入以下内容:
R> plot(quakes$long,quakes$lat)
这将生成一个关于 1000 个地震事件发生位置的图,数据来自准备好的 quakes 数据框。如果当前唯一可用的设备是设备 1,即空设备,那么任何刷新绘图窗口并生成新图像的绘图命令(例如这里的 plot,或者更专业的命令如 hist 或 boxplot)都会在实际绘图之前自动打开一个默认图形设备的新实例。在我的机器上,我看到 Quartz 2 []* 被打开并显示空间坐标的图像。
现在,假设你还想查看每个事件被多少个站点检测到的直方图。执行以下命令以打开一个新的绘图窗口:
R> dev.new()
这个新窗口将被编号为 3(通常它会出现在之前打开的窗口上方,所以你可能需要用鼠标将它移到一边)。重要的是,你会看到这个窗口成为了活动设备:在 Mac 上,[]*现在位于设备 3 的横幅上;在 Windows 上,设备 3 会显示 (ACTIVE),而设备 2 则会显示 (inactive)。
到此为止,你可以输入常规命令以在设备 3 中显示所需的直方图:
R> hist(quakes$stations)
如果你没有使用 dev.new,直方图将直接覆盖设备 2 上的空间位置图。
23.1.2 在设备间切换
如果想在不关闭设备 3 的情况下修改设备 2,使用 dev.set 后跟你希望激活的设备编号。以下代码激活设备 2,并重新绘制地震事件的位置,使得每个点的大小与检测到该事件的站点数量成比例。它还会整理坐标轴标签。
R> dev.set(2)
quartz
2
R> plot(quakes$long,quakes$lat,cex=0.02*quakes$stations,
xlab="Longitude",ylab="Latitude")
使用 dev.set 总是通过打印到控制台来确认新激活的设备;具体文本会根据你的操作系统和设备类型有所不同。
切换回设备 3,作为最终调整,添加一个垂直线来标示检测站点的平均数。
R> dev.set(3)
quartz
3
R> abline(v=mean(quakes$stations),lty=2)
图 23-1 显示了在进行这些修改后的两个图形设备。

图 23-1:我的两个可见图形设备,设备 2(左)和设备 3(右),显示生成和操作两个 quakes 数据图的最终结果
23.1.3 关闭设备
要关闭图形设备,可以像关闭任何窗口一样,点击鼠标上的 X,或者使用 dev.off 函数(你最初在 第八章 中看到过这个命令,用于关闭直接输出到文件的设备)。不带参数调用 dev.off() 会简单地关闭当前活动设备。否则,你可以像使用 dev.set 一样指定设备编号。要关闭空间位置图,保持直方图为活动设备,可以调用 dev.off,并传入 2 作为参数:
R> dev.off(2)
quartz
3
然后重复调用不带参数的命令以关闭剩余的设备:
R> dev.off()
null device
1
类似于dev.set,打印输出会告诉你在关闭一个设备后,哪个设备变为当前活动设备。当你关闭最后一个可用的可操作设备时,你会返回到空设备。
23.1.4 单个设备中的多个图形
你还可以控制任何设备中图形的数量。有几种方法可以做到这一点;我将在这里描述两种最简单的方法。
设置 mfrow 参数
回想一下,par函数用于控制传统 R 图形的各种图形参数。mfrow参数指示一个新的(或当前活动的)设备“隐形”地将其自己划分为指定尺寸的网格,每个单元格包含一个图形。你传递给mfrow选项的是一个长度为 2 的数值整数向量,形式为c(行数,列数);如你所猜测的那样,它的默认值是c(1,1)。
在你的 R 会话中,确保没有打开任何绘图窗口。现在,假设你希望将quakes数据的两个图形并排显示在同一设备中。你可以将mfrow设置为一个 1 × 2 的网格,向量为c(1,2)——一行图形和两列图形。
R> dev.new(width=8,height=4)
R> par(mfrow=c(1,2))
R> plot(quakes$long,quakes$lat,cex=0.02*quakes$stations,
xlab="Longitude",ylab="Latitude")
R> hist(quakes$stations)
R> abline(v=mean(quakes$stations),lty=2)
第一行使用可选参数width和height来预设新设备的尺寸(单位为英寸),使其宽度是高度的两倍。图 23-2 显示了图像的显示方式,新图形的创建会根据mfrow的值填充可用的单元格。

图 23-2:使用 mfrow 在 par 中生成一个单一图形设备中的图形网格,显示quakes数据的两个图形
如果你关闭了任何图形设备,并且在没有调用dev.new的前提下重新运行这段代码,执行par(mfrow=c(1,2))将自动打开一个默认大小为 7 × 7 英寸的图形设备。两个图形仍然会并排显示,但会被压缩。你可以手动使用鼠标调整设备大小,使其适应mfrow设置的值,然后当你重新绘制图形时,图形及其坐标轴会更清晰。你会发现,你会经常使用这种反复试验的方法来在一个设备中生成多个图形,尤其是在你不想显式调用dev.new以及设置width和height时。
请注意,任何这样使用par的操作将仅影响当前活动的设备。随后的dev.new调用会打开新的设备,举例来说,mfrow会被重置为默认的“仅一个图形”设置c(1,1)。换句话说,如果你想定制任何新图形设备(包括直接输出到文件的设备)的选项,你需要在打开设备之后、执行任何绘图命令之前设置所需的par值。
定义特定的布局
你可以使用layout函数来细化单个设备中图形的排列方式,它提供了更多的方法来个性化图形将要绘制的面板。
返回到MASS包中的学生调查数据survey。假设你想要展示三个统计图表——一个展示身高与写字手部跨度的散点图,一个展示按吸烟状态分组的身高箱线图,以及一个展示学生锻炼频率的条形图。如果你想将图表排列成一个方形设备(而不是三个图表的单行或单列),仅使用mfrow在par中可能无法达到最佳效果。你可以设置一个方形网格,使用par(mfrow=c(2,2)),但你会发现某个单元格会留下空白,因为没有图像分配给它。
使用layout时,你需要将矩阵mat作为第一个参数传入,这些参数控制一个看不见的矩形网格,就像控制mfrow选项一样。不同之处在于,你现在可以在mat中使用整数数字条目来告诉layout,哪个绘图编号将放置在哪里。请查看以下对象:
R> lay.mat <- matrix(c(1,3,2,3),2,2)
R> lay.mat
[,1] [,2]
[1,] 1 2
[2,] 3 3
该矩阵的维度创建了一个 2 × 2 的绘图单元格网格,但lay.mat中的值告诉 R,你希望图 1 占据左上单元格,图 2 占据右上单元格,图 3 则扩展到两个底部单元格。
按如下方式调用layout,将根据lay.mat静默初始化活动设备,或者在当前唯一可用的空设备的情况下打开一个新的设备并初始化它。
R> layout(mat=lay.mat)
如果你对指定结果不确定,可以使用layout.show函数查看绘图如何布局。以下代码生成的图像位于图 23-3 的左侧。
R> layout.show(n=max(lay.mat))
然后,在你通过调用library("MASS")加载了MASS包以访问survey数据后,运行以下代码行,将图形按照绘图命令执行的顺序排列,并与lay.mat中的整数匹配。
R> plot(survey$Wr.Hnd,survey$Height,
xlab="Writing handspan",ylab="Height")
R> boxplot(survey$Height~survey$Smoke,
xlab="Smoking frequency",ylab="Height")
R> barplot(table(survey$Exer),horiz=TRUE,main="Exercise")
注意,如果你已经关闭了由layout.show产生的图形,那么你需要重新初始化一个新设备,并使用相同的layout调用,这样这三个图形才能按照预期显示。结果应该类似于图 23-3 的右侧。

图 23-3:左侧:使用layout.show可视化计划的绘图布局和顺序。右侧:展示三个根据lay.mat通过layout排列的survey数据的绘图。
layout的最大好处之一是它能相较于使用mfrow par选项时,放松绘图单元格的固定性,正如你刚刚看到的那样。layout的其他参数,特别是widths和heights,甚至允许你预设mat参数中结构化的单元格的相对宽度和高度。有关详细信息,请参见?layout中的文档;你还可以在文件底部找到一些其灵活性的其他示例。
注意
这里讨论的两种方法的一个不幸后果是,一旦完成了一个图形并进入下一个,就无法编辑之前的图形。有一个split.screen函数,允许你在一个设备中设置多个“屏幕”并在它们之间切换。然而,这种方法需要大量的额外编码,并且通常在绘图区域和边距(参见下一节)方面表现不佳。许多用户(包括我自己)更倾向于使用layout,即使这意味着需要通过一些试验和错误。
23.2 绘图区域和边距
尽管绘图时主要关注的数据集或模型是被可视化的对象,但同样重要的是确保图形的注释清晰准确,以便正确解读。为此,你需要知道如何操作和绘制给定设备上所有可见区域,而不仅仅是数据所在的区域。
对于任何使用基本 R 图形创建的单个图形,图像由三个区域组成。
绘图区域是你迄今为止处理的所有区域。这是你的实际图形出现的地方,也是你通常绘制点、线、文本等的地方。绘图区域使用用户坐标系统,它反映了水平和垂直坐标轴的值和刻度。
图形区域是包含坐标轴、坐标轴标签和标题空间的区域。这些空间也称为图形边距。
外部区域,也称为外部边距,是围绕图形区域的额外空间,默认情况下不包括在内,但如果需要,可以进行指定。
你可以通过几种不同的方式明确测量和设置边距空间。一种典型的方式是使用行数—具体来说,是能在每个边缘上并排显示的文本行数。你需要按照特定的顺序指定这些行数,作为一个长度为 4 的向量;四个元素分别对应四个边:c(底部,左侧,顶部,右侧)。图形参数oma(外部边距)和mar(图形边距)用于控制这些数量;像mfrow一样,它们通过调用par进行初始化,在你开始绘制任何新图形之前。
23.2.1 默认间距
你可以通过在 R 中调用par来找到默认的图形边距设置。
R> par()$oma
[1] 0 0 0 0
R> par()$mar
[1] 5.1 4.1 4.1 2.1
你可以看到这里oma=c(0, 0, 0, 0)—默认情况下没有设置外部边距。默认的图形边距空间是mar=c(5.1, 4.1, 4.1, 2.1)—换句话说,底部有 5.1 行文本,左侧和顶部各有 4.1 行,右侧有 2.1 行。
为了说明这些区域,考虑图 23-4 左侧的图像,它是在一个新的图形设备中创建的,使用了以下代码:
R> plot(1:10)
R> box(which="figure",lty=2)

图 23-4:展示传统(基本)R 图形设备区域的示例;实线框显示图表区域,虚线框显示图形区域,点线框显示外部区域。左:默认设置。右:用户通过 par* 指定的外部和图形边距区域,分别通过* oma 和 mar 指定“文本行数”。
如果你使用 box 函数,并将可选参数 which 设置为 "figure",它会显示图形区域(另外指定 lty=2 会绘制虚线)。
如果你在屏幕上的图形设备中查看这个图表,你会注意到虚线紧贴着窗口的边缘。查看 mar 的默认值,你可以看到,相对来说,它们正确地对应图表区域四个边的间距(由默认的实线框给出)。与图表区域底部平行的最宽图形边距为 5.1 行;与图表区域右侧平行的最窄图形边距为 2.1 行。
23.2.2 自定义间距
让我们生成相同的图表,但使用定制的外部边距,使得底部、左侧、顶部和右侧区域分别为 1、4、3 和 2 行,而图形边距则分别为 4、5、6 和 7 行。以下代码的结果如 图 23-4 右侧所示。
R> par(oma=c(1,4,3,2),mar=4:7)
R> plot(1:10)
R> box("figure",lty=2)
R> box("outer",lty=3)
请注意,不规则的边距已将图表区域在默认的方形设备中压缩,以适应定义的边缘间距。如果你设置的图形参数将图表区域压缩到不存在,R 会抛出错误,提示 figure margins too large。
由于你通常需要调整边距空间以适应图表的特定注释,下面我们来看看 mtext 函数,它专门用于在图形或外部边距中生成文本。默认情况下,参数 outer 为 FALSE,意味着文本将写入图形边距。设置 outer=TRUE 则将文本定位到外部区域。如果你保持最新的图表打开,下面的几行代码将提供右侧显示的额外边距注释,见 图 23-4:
R> mtext("Figure region margins\nmar[ . ]",line=2)
R> mtext("Outer region margins\noma[ . ]",line=0.5,outer=TRUE)
在这里,你将你想要写入的文本作为第一个参数传递,而参数 line 指示文本距离内部边框的行数。mtext 中还有一个可选参数 side,它决定文本出现的位置。默认值为 3,将文本设置在顶部,但你可以设置 side=1 将文本放置在底部,使用 side=2 将其设置在左侧,使用 side=4 将其设置在右侧。详细信息可以查看 ?mtext,了解更多可用于边距文本的参数。
你还可能想要研究现成的title函数,它是mtext的专门实现,通常用于当图形的四个轴的边缘注释(超出指定main、xlab或ylab等基本功能)是主要关注点时。
23.2.3 裁剪
控制裁剪可以让你在参考图形自身的用户坐标时,在或向边缘区域绘制或添加元素。例如,你可能希望将图例放置在绘图区域外,或者你可能希望绘制一个箭头,延伸到绘图区域之外,以强调某个特定的观察点。
图形参数xpd控制基本 R 图形中的裁剪。默认情况下,xpd设置为FALSE,因此所有绘图仅限于可用的绘图区域(特殊的边缘添加函数,如mtext除外)。将xpd设置为TRUE,允许你在正式定义的绘图区域外绘制内容到图形边缘,但不能进入任何外部边缘。将xpd设置为NA,则允许在所有三个区域中绘制——绘图区域、图形边缘和外部边缘。
例如,查看图 23-5 中的图像,展示了按气缸数量划分的里程的并排箱线图,使用以下代码创建:
R> dev.new()
R> par(oma=c(1,1,5,1),mar=c(2,4,5,4))
R> boxplot(mtcars$mpg~mtcars$cyl,xaxt="n",ylab="MPG")
R> box("figure",lty=2)
R> box("outer",lty=3)
R> arrows(x0=c(2,2.5,3),y0=c(44,37,27),x1=c(1.25,2.25,3),y1=c(31,22,20),
xpd=FALSE)
R> text(x=c(2,2.5,3),y=c(45,38,28),c("V4 cars","V6 cars","V8 cars"),
xpd=FALSE)
这段代码的具体结果是图 23-5 中的左上图。我已为说明目的定义了设备区域本身,并为图形和外部边缘设置了特定的边距。通过在boxplot调用中使用xaxt="n"来抑制绘制水平轴;调用box函数添加图形和外部边缘的边界(分别为虚线和点线)。最后,调用arrows和text分别指向并注释每个箱线图;V4 车型的标签延伸到外部边距,V6 车型的标签延伸到图形区域,而 V8 车型的标签保持在绘图区域内。
请注意,图形参数xpd仅在两个“添加到当前绘图”函数arrows和text中指定,并显式设置为默认值FALSE。这意味着所有绘图都仅限于绘图区域。
如果你再次运行代码块,但现在在arrows和text的调用中将xpd=TRUE,你将得到图 23-5 右上角的图像。这允许 V6 车型的标签在边缘打印,而不是被截断。最后,重新运行代码并将xpd=NA,将生成图 23-5 中的下方图形,允许所有绘制内容超出绘图区域。
当你需要以某种方式注释主图时,这种效果通常是需要的,尤其是在图形区域没有足够空间添加内容时。我在前面章节创建的图形,例如 图 16-6 (图例位于主图之外,位于 第 349 页)和 图 17-3 (注释了关键值,位于 第 380 页),都是通过在相关函数(legend、text、segments 和 arrows)中指定 xpd=TRUE 来创建的。
如所示,通常你会在特定命令中设置 xpd(换句话说,是逐行设置),这样只有该特定命令的结果会按照给定的裁剪规则生成。这提供了更多控制,决定哪些内容能否显示在图形区域之外。不过,你也可以在初始调用 par 时,将 xpd 与 oma 和 mar 一起设置,使得 xpd 的值在该设备上“通用”。

图 23-5:展示设置 xpd=FALSE (左上角,默认), xpd=TRUE (右上角),以及 xpd=NA (底部)在相关绘图命令中启用根据用户坐标在图形区域内及外部边距绘制的行为
23.3 点选坐标交互
与图形设备的交互不一定仅限于命令方式。在典型情况下,R 可以读取你在设备内的鼠标点击。
23.3.1 静默检索坐标
locator 命令允许你查找并返回用户坐标。要查看其工作原理,首先执行 plot(1,1) 来显示一个简单的图形,其中中央有一个点。要使用 locator,你只需执行该函数(默认行为下不传递任何参数),这会“挂起”控制台,直到返回提示符。然后,在一个活动的图形设备上,你的鼠标光标会变成一个+符号(你可能需要先点击一次设备,将其带到桌面前台)。在光标为+时,你可以在设备内执行一系列(左键)鼠标点击,R 会静默记录精确的用户坐标。要停止此操作,只需右键点击终止命令(其他停止方法取决于系统,并在帮助文件 ?locator 中提及),一旦停止,设备中识别的坐标将作为包含 $x 和 $y 组件的列表返回。这些坐标会打印到控制台,除非你特别将 locator 的调用结果赋值给 R 对象。
在我的机器上,我在绘制点 (1,1) 周围的任意位置静默标识了四个点,顺时针从左上角到左下角。以下是打印到控制台的输出:
R> plot(1,1)
R> locator()
$x
[1] 0.8275456 1.1737525 1.1440526 0.8201909
$y
[1] 1.1581795 1.1534442 0.9003221 0.8630254
locator 的这种静默使用在你需要例如确定图形区域中大致的用户坐标,以便在未来放置注释时非常有用。
23.3.2 可视化选定的坐标
你还可以使用locator将所选的点绘制为单独的点或线。运行以下代码会生成图 23-6:
R> plot(1,1)
R> Rtist <- locator(type="o",pch=4,lty=2,lwd=3,col="red",xpd=TRUE)
R> Rtist
$x
[1] 0.5013189 0.6267149 0.7384407 0.7172250 1.0386740 1.2765699
[7] 1.4711542 1.2352573 1.2220592 0.8583484 1.0483300 1.0091491
$y
[1] 0.6966016 0.9941945 0.9636752 1.2819852 1.2766579 1.4891270
[7] 1.2439071 0.9630832 0.7625887 0.7541716 0.6394519 0.9618461
使用locator进行绘图时,你需要指定绘图的type,详见第七章。选择type="o"(与默认的静默值type="n"不同)会在图 23-6 中生成重叠的点和线。仅绘制点时使用type="p";仅绘制线时使用type="l"。控制其他相关特征的图形参数,如点/线类型和颜色,也可以使用,正如你在第七章中看到的常规绘图中所示。我还使用了xpd=TRUE,如前所示,这允许locator的点和/或线超出图形区域的边界。对locator的调用被直接赋值给一个新的对象Rtist,这演示了如果需要,你可以稍后使用点击的坐标。

图 23-6:使用 locator 绘制任意顺序的重叠点和线
23.3.3 临时注释
locator函数还允许你在绘图中放置临时注释,如图例——记住,由于locator返回的是有效的 R 用户坐标,这些结果可以直接作为大多数标准注释函数的位置信息。
返回到MASS包中的学生调查数据,首先通过调用library("MASS")来加载该包。以下代码会生成用来说明多个线性模型的散点图,模型将平均学生身高作为手掌宽度和性别的函数,详见第 21.3.3 节。
R> plot(survey$Height~survey$Wr.Hnd,pch=16,
col=c("gray","black")[as.numeric(survey$Sex)],
xlab="Writing handspan",ylab="Height")
对于第 21.3.3 节中的绘图(图 21-1 见第 495 页),我只是用字符串"topleft"来定位图例。这时,调用以下代码:
R> legend(locator(n=1),legend=levels(survey$Sex),pch=16,
col=c("gray","black"))
locator的一个可选参数n接受一个正整数,用于设定你希望选择的最大点数;默认值为512。如果你指定n=1,locator将在你在设备上左键单击一次后自动终止,因此你不需要通过右键单击手动退出函数。
当代码执行时,+光标将出现在图形设备上,你只需要点击一次来选择图例的位置。我选择在点云的上方空白处点击,生成了图 23-7 中的图像。

图 23-7:在散点图中临时放置图例,数据来自 survey 数据集
练习 23.1
-
在第 20.5.4 节(第 478 页)中,我给出了一个简单的线性模型代码,展示了将类别预测变量当作连续变量拟合的情况(
mtcars数据集,mpg为响应变量,cyl为解释变量)。请复现图 20-6 中的并排箱型图和散点图(带拟合线),但这次使用mfrow将这两张图以垂直列的形式在一个设备上呈现。 -
创建适当的布局矩阵,以复现以下三个图(它们在一个方形设备中呈现):
-
-
通过打开一个尺寸为 9 × 4.5 英寸的新设备,设置以下布局:
![image]()
然后,精确地生成以下组合图表:
![image]()
为了实现这一点,请注意以下事项:
– 打开设备并设置布局后,图表的边距应重置为底部、左侧、顶部和右侧分别为四行、四行、两行和一行的空间。
– 在每个图表之后,添加一个对应于图形区域的灰色框,以实现可见的分区。
– 图表 1 和图表 4 与图 23-1 和图 23-2 中显示的两张图相同。
– 图表 2 和图表 3 是散点图,分别显示了检测站的数量(y轴),以及震级和深度(x轴)。
– 不要在任何图表上放置主标题,确保坐标轴标题整洁(即,与默认设置相比)。
-
编写一个小的 R 函数,命名为
interactive.arrow。该函数的目的是通过鼠标点击两次,在任何基础 R 图形上叠加一个箭头。具体细节如下:– 你函数的关键是使用
locator来读取恰好两次鼠标点击。你可以假设每次调用该函数时,合适的活动图形设备已经打开。第一次点击应表示箭头的起点,第二次点击应表示箭头的箭头尖端(即它指向的位置)。– 在函数中,
locator返回的坐标应传递给arrows,以进行实际绘图。– 该函数应该将省略号作为第一个参数,用于传递直接给
arrows的附加参数。– 该函数应接受一个可选的逻辑参数
label,其默认值为NA,但应打算传入一个可选的字符字符串。如果label不是NA,那么在绘制箭头后,locator应该再次被调用(单独调用)来选择一个精确的坐标。该点将传递给text,以便用户可以将传入label的字符字符串作为注释(预计是用于交互式放置的箭头)。对text的调用应该始终允许完全放松裁剪(换句话说,以这种方式添加的任何文本仍然会在图形区域和外部边距中可见,如果有的话)。再看一眼第 14-6 图中最右侧的绘图,第 298 页的
quakes数据框中的震幅数据的独立箱线图。箭头和标签被外部叠加,指向箱线图总结的各种统计信息。创建相同的箱线图,并使用interactive.arrow来标注相同的特征,直到你满意为止(你可能需要使用省略号来放松每个箭头的裁剪)。我的结果如下所示:

23.4 自定义传统的 R 绘图
现在你已经熟悉了 R 在图形设备中放置和处理图形的方式,是时候关注图形的常见特征了。到目前为止,你大多数情况下都保持了默认设置。
23.4.1 用于样式和抑制的图形参数
如果你想对 R 绘图进行更精细的控制,通常你需要从一个“干净的画布”开始。为此,你需要了解在调用绘图函数时某些图形参数的默认设置,以及如何抑制诸如框线和坐标轴等元素。这就是你开始的地方。
作为示例图像,让我们将 MPG 与马力(来自现成的mtcars数据集)进行绘制,并将每个绘制点的大小设置为与每辆车的重量成比例。为了方便起见,创建以下对象:
R> hp <- mtcars$hp
R> mpg <- mtcars$mpg
R> wtcex <- mtcars$wt/mean(mtcars$wt)
最后一个对象是按样本均值缩放的汽车重量向量。这会创建一个向量,其中小于平均重量的汽车值小于 1,大于平均重量的汽车值大于 1,使得它非常适合用于cex参数来相应地缩放绘制点的大小(参见第七章)。
让我们首先关注一些通常在首次调用plot时使用的图形参数,为使用box和axis命令铺平道路。执行以下代码将给出绘图的默认外观以及它的框线、坐标轴和标签;这在第 23-8 图中的最左侧图像中显示。
R> plot(hp,mpg,cex=wtcex)
有两种坐标轴“样式”,由图形参数xaxs和yaxs控制。它们的唯一目的是决定是否在每个坐标轴的末端加上少量的水平和垂直缓冲空间,以防止数据点被截断。默认设置xaxs="r"和yaxs="r"会包括这些空间。另一种选择是将其中一个或两个设置为"i",指示绘图区域严格由数据的上下限(或由xlim和/或ylim可选提供的限值)定义,也就是没有额外的填充空间。
例如,下面这行代码会生成图 23-8 中的中间图。
R> plot(hp,mpg,cex=wtcex,xaxs="i",yaxs="i")
这个图与默认设置几乎相同,但请注意,现在坐标轴的末端没有填充空间;最极端的数据点正好位于坐标轴上。通常情况下,默认的坐标轴样式"r"是可以的,但在需要对坐标轴的刻度和相应的绘图区域进行更精细控制的情况下,额外的缓冲空间可能会带来问题。在这些情况下,你通常会看到xlim/ylim与xaxs="i"/yaxs="i"一起使用。

图 23-8:绘制mtcars数据集中的马力与英里每加仑的关系;点的大小与汽车重量成比例,仅使用plot进行绘制。左:默认外观。中:设置xaxs="i"和yaxs="i",以防止坐标轴末端的缓冲空间。右:使用xaxt、yaxt、xlab、ylab和bty来抑制所有框架、坐标轴和标签的绘制(也可以通过设置axes=FALSE和ann=FALSE来实现)。
如果你希望完全控制任何框架、坐标轴及其标签的具体外观,你可以从不添加这些元素的绘图开始,然后根据设计逐步添加。图 23-8 中的最右侧图即是通过调用来抑制这些默认绘制的结果。
R> plot(hp,mpg,cex=wtcex,xaxt="n",yaxt="n",bty="n",xlab="",ylab="")
或者
R> plot(hp,mpg,cex=wtcex,axes=FALSE,ann=FALSE)
你可以通过将参数xaxt、yaxt和bty设置为"n",并将默认的坐标轴标签xlab和ylab设置为空字符串"",或者简单地将axes和ann都设置为FALSE来实现这一点(前者抑制所有坐标轴和框架,后者抑制所有注释)。尽管第一种方式看起来可能有些复杂,但它能提供更大的灵活性,允许你逐个抑制给定绘图的每个方面(与第二种方法通过“完全”抑制来实现不同)。
23.4.2 自定义框架
当你从一个抑制框或抑制坐标轴的图开始时,要在当前活动图形设备的图形区域中添加特定的框,你可以使用box并通过bty指定其类型。例如,如果你从像图 23-8 右侧那样的图开始(只需运行最新一行代码即可得到此图),那么再调用以下代码将会给你呈现图 23-9 左侧的图像。
R> box(bty="u")
bty参数接受一个单一字符值:"o"(默认值)、"l"、"7"、"c"、"u"、"]"或"n"。在?par的帮助文件中,bty的条目告诉你,基于这些值之一,结果框的边界将遵循相应大写字母的外观,"n"除外(正如你刚才看到的,它将抑制框的显示)。

图 23-9: 向mtcars散点图添加的各种框配置
你可以使用之前学过的其他相关参数,如lty、lwd和col,进一步控制框的外观。重新绘制数据,如图 23-8 右侧所示,然后调用以下代码可以生成图 23-9 中间的图像:
R> box(bty="l",lty=3,lwd=2)
图 23-9 右侧的最终示例是通过以下代码创建的:
R> box(bty="]",lty=2,col="gray")
23.4.3 自定义坐标轴
一旦你将框调整到你想要的样子,你就可以专注于坐标轴了。axis函数允许你更详细地控制在图形区域的四个边上添加和显示坐标轴。它的第一个参数是side,并接受一个单一的整数:1(底部)、2(左侧)、3(顶部)或4(右侧)。这些数字与设置图形参数向量(如mar)时相关的边距间隔值的位置一致。
你可能想要在坐标轴上首先改变的是刻度标记的位置。默认情况下,R 使用内置函数pretty来找到每个坐标轴尺度的“整洁”值序列,但你也可以通过传递at参数到axis来设置自己的刻度标记。以下代码生成了图 23-10 左侧的图。
R> hpseq <- seq(min(hp),max(hp),length=10)
R> plot(hp,mpg,cex=wtcex,xaxt="n",bty="n",ann=FALSE)
R> axis(side=1,at=hpseq)
R> axis(side=3,at=round(hpseq))
首先,存储了一个均匀分布的 10 个值的序列,这些值跨越了hp的范围,并命名为hpseq。初始的plot调用抑制了x轴、框架和任何默认的轴标签;然而,y轴仍然按默认方式显示。接着,axis被指示在x轴上绘制(side=1),并在hpseq的位置绘制刻度标记。为了与之对比,还会在顶部(side=3)绘制一个坐标轴,但这次刻度标记是根据hpseq四舍五入到最接近的整数后绘制的。

图 23-10: 自定义mtcars散点图的坐标轴
如图左侧所示,我在底部创建的自定义 x 轴显示了 at 提供的值序列中的 10 个刻度标记。R 可能会抑制一些标签,以避免它们相互重叠,这就是此处发生的情况。由于这些“十进制”值可能不太符合美观,沿顶部绘制的坐标轴则在 hpseq 的最接近整数位置绘制了刻度标记,这是通过在前面显示的最终 axis 调用中的 round 函数实现的。严格来说,这意味着刻度标记不再完全均匀分布,但通过四舍五入,默认的坐标轴标签更短,可以在当前设备中完全显示。
从这些困难中可以看出,刻度标记的位置通常最好交给 R 来处理,除非你有特定的坐标值并且明确知道要标记出来——你将在第 23.6 节看到这个例子的展示。现在,让我们看一下你可以对坐标轴进行的一些其他调整。特别是,tcl(刻度线的长度)、las(标签的方向)和 mgp(坐标轴的间距)这几个参数无疑是最常用的。以下代码创建了图 23-10 右侧的图表。
R> hpseq2 <- seq(50,325,by=25)
R> plot(hp,mpg,cex=wtcex,axes=FALSE)
R> box(bty="l")
R> axis(side=2,tcl=-2,las=1,mgp=c(3,2.5,0))
R> axis(side=1,at=hpseq2,tcl=1.5,mgp=c(3,1.5,1))
定义一个新的序列—hpseq2—它包含所有落在数据记录范围内且相隔 25 个单位的整数后,图表被初始化。盒子和坐标轴被隐藏,但默认的变量标题(mpg 和 hp)仍然沿着坐标轴显示。
现在,添加了一个 L 形的框和 y 轴(side=2)。在后者中,tcl 参数控制每个刻度线在“平行文本行”中的长度(请回想,这是 R 图表中边距间隔的标准单位测量);它的默认值是 -0.5。当值为负时,它会将刻度线绘制到图表区域外;当值为正时,刻度线则会绘制到图表内部。对于这个 side=2 轴,tcl=-2,意味着刻度线会从图表外侧指向,但长度是通常的四倍(相当于两整行文本,而不是半行文本)。
las 参数控制每个刻度标签的方向;将其设置为 1 指示 R 将所有刻度标签 水平 显示,无论坐标轴在哪一侧。默认值 las=0 会将所有标签 与相应的坐标轴平行;另一种选择 las=2 表示标签总是 垂直 显示,且与坐标轴垂直;使用 las=3 则会将所有标签设置为 垂直 显示,无论坐标轴位置如何。
接下来,mgp 参数控制轴间距的三个方面,因此它被提供一个长度为 3 的向量,定义如下:c(轴标题, 轴标签, 轴线)。这些参数仍然以“文本行”表示。mgp 的默认值为 c(3,1,0)——这意味着,在你看到的每个轴中,标题距离绘图区域有三行文本,刻度标签距离绘图区域一行文本,而轴线本身距离绘图区域没有文本行(即它与任何绘制的绘图区域框对齐)。当在 axis 中使用时,只有 mgp 的第二个和第三个元素是相关的。在 图 23-10 右侧的绘图中,垂直轴唯一的变化是将第二个元素(轴标签的间距)设置为 2.5——将轴标签推向左侧,远离绘图区域。刻度标记本身由于 tcl 被显著延长,因此需要进行此调整,以避免轴刻度标签穿过这些刻度。尝试重新绘制图像和该轴,但不指定 mgp,你将看到不太理想的效果。
移动到 x 轴(side=1),你可以看到 hpseq2 上的刻度标记通过 at 被放置。这一次,向 tcl 提供了一个正值,指示轴线具有 向内 的刻度标记,长度为 1.5 行文本。在 mpg 中,注意向量的第三个元素现在设置为 1,意味着你希望轴线本身距离绘图区域一行文本的距离。看向 图 23-10 的右侧,你可以看到整个轴线已被向下移动,远离绘图区域。为了适应这种刻度标记标签的间距,mgp 的第二个元素已稍微增大,从默认值调整为 1.5。
23.5 专门的文本和标签表示法
接下来,你将探讨一些可以立即使用的工具,用于控制字体和显示特殊符号,如希腊字母和数学表达式。
23.5.1 字体
显示字体由两个图形参数控制:family(特定字体系列)和 font(控制粗体和斜体的整数选择器)。
可用字体取决于你的操作系统和你使用的图形设备。不过,有三种通用字体系列——"sans"(默认字体)、"serif" 和 "mono"——是始终可用的。这些字体系列可以与 font 的四个可能值配合使用——1(正常文本,默认)、2(粗体)、3(斜体)和 4(粗体和斜体)。你可以通过 par 为设备统一设置这两个图形参数,但像使用 xpd 一样,在相关的注释函数中设置 family 和 font 同样常见(如果不是更常见的话)。
图 23-11 展示了几种变体,并显示了相应的family和font值。要创建它,从一个带有预设x和y坐标范围的空白图区域开始,使用以下代码:
R> par(mar=c(3,3,3,3))
R> plot(1,1,type="n",xlim=c(-1,1),ylim=c(0,7),xaxt="n",yaxt="n",ann=FALSE)
然后,通过执行以下几行代码,完成了六种可能的变体图像:
R> text(0,6,label="sans text (default)\nfamily=\"sans\", font=1")
R> text(0,5,label="serif text\nfamily=\"serif\", font=1",
family="serif",font=1)
R> text(0,4,label="mono text\nfamily=\"mono\", font=1",
family="mono",font=1)
R> text(0,3,label="mono text (bold, italic)\nfamily=\"mono\", font=4",
family="mono",font=4)
R> text(0,2,label="sans text (italic)\nfamily=\"sans\", font=3",
family="sans",font=3)
R> text(0,1,label="serif text (bold)\nfamily=\"serif\", font=2",
family="serif",font=2)
R> mtext("some",line=1,at=-0.5,cex=2,family="sans")
R> mtext("different",line=1,at=0,cex=2,family="serif")
R> mtext("fonts",line=1,at=0.5,cex=2,family="mono")
在这里,text用于在预定的坐标处放置内容,mtext则用于将内容添加到顶部图形的边距中。

图 23-11:通过使用 family 和 font 图形参数显示字体样式
23.5.2 希腊字母
对于统计或数学技术绘图,注释有时需要使用希腊字母或数学标记。你可以使用expression函数来显示这些内容,它除了其他功能外,还能调用 R 的plotmath模式(Murrell 和 Ihaka,2000; Murrell,2011)。使用expression返回一个特殊的对象,其类名相同,随后可以将该对象传递给任何需要字符字符串的绘图函数的参数。
目前我们集中讨论希腊字母,请参见图 23-12,它是通过以下代码生成的:
R> par(mar=c(3,3,3,3))
R> plot(1,1,type="n",xlim=c(-1,1),ylim=c(0.5,4.5),xaxt="n",yaxt="n",
ann=FALSE)
R> text(0,4,label=expression(alpha),cex=1.5)
R> text(0,3,label=expression(paste("sigma: ",sigma," Sigma: ",Sigma)),
family="mono",cex=1.5)
R> text(0,2,label=expression(paste(beta," ",gamma," ",Phi)),cex=1.5)
R> text(0,1,label=expression(paste(Gamma,"(",tau,") = 24 when ",tau," = 5")),
family="serif",cex=1.5)
R> title(main=expression(paste("Gr",epsilon,epsilon,"k")),cex.main=2)

图 23-12:使用 expression 显示希腊字母
如果你只想单独显示一个特殊字符,那么像expression(alpha)这样的写法就足够了,它会在图中显示β,如代码片段中的第一次调用text所示。请注意,特殊字符的指定是没有引号的,直接使用所需符号的名称。然而,更常见的情况是你希望字符与其他组件一起出现,比如常规文本或在公式中。为此,你需要在expression的调用中使用paste,用逗号分隔各个组件。它们在后面三次调用text中得以体现。
你可以使用cex来控制大小,尽管family和font的使用仅影响普通文本(引用的文本),而不影响符号,正如最后一次调用text所示。
然后使用title函数,它允许你添加轴标题和主标题,通过提供相应的expression给main来添加标题“Grɛɛk”。在同一次调用中,我使用了cex.main=2来将其大小加倍(稍有不同的标签cex.main在这里是必需的,用来区分主标题的大小与轴标题的大小,后者由cex.lab控制)。
23.5.3 数学表达式
格式化整个数学表达式以在 R 绘图中显示稍微复杂一些,且让人联想到使用 LAT[E]X 等标记语言。由于这个原因,我在这里不会详细介绍所需的语法,但我会提供一些可能的示例,如图 23-13 所示。为了创建该图像,我首先定义了四个表达式对象,如下所示:
R> expr1 <- expression(c²==a[1]²+b[1]²)
R> expr2 <- expression(paste(pi^{x[i]},(1-pi)^(n-x[i])))
R> expr3 <- expression(paste("Sample mean: ",
italic(n)^{-1},
sum(italic(x)[italic(i)],
italic(i)==1,
italic(n))
==frac(italic(x)[1]+...+italic(x)[italic(n)],
italic(n))))
R> expr4 <- expression(paste("f(x","|",alpha,",",beta,
")"==frac(x^{alpha-1}~(1-x)^{beta-1},
B(alpha,beta))))
然后我在以下代码中使用了它们:
R> par(mar=c(3,3,3,3))
R> plot(1,1,type="n",xlim=c(-1,1),ylim=c(0.5,4.5),xaxt="n",yaxt="n",
ann=FALSE)
R> text(0,4:1,labels=c(expr1,expr2,expr3,expr4),cex=1.5)
R> title(main="Math",cex.main=2)

图 23-13:在 R 绘图中排版数学表达式的一些示例
所有的希腊字母和数学标记都包含在对expression的调用中。如果需要分离的组件(由逗号分隔),其中一些可能是常规文本(即需要用引号包围的内容),则必须使用paste来生成最终结果。以下是一些关键的注意事项:
• 上标由^表示,下标由[ ]表示;例如,c²在expr1中提供 c²,而a[1]²组件提供如图所示的内容:
.
• 你可以使用圆括号( )将组件分组,这些圆括号是可见的(例如,expr2中的(1-pi)^(n-x[i])组件),或者使用大括号{ },这些大括号不可见(例如,pi^{x[i]}组件)。
• 斜体字母变量使用italic()绘制;例如,italic(n)在expr3中产生n。
• 常见的算术操作符构造已经存在,如sum( , , )和frac( , );例如,在expr3中,调用sum(italic(x)[italic(i)],italic(i)==1,italic(n))会生成一个看起来像这样的结果:

而frac(italic(x)[1]+...+italic(x)[italic(n)],italic(n))则生成如下表达式:

• 还有其他标记工具用于正确格式化表达式,例如将引号中的常规文本与数学标记直接结合,并在组件之间创建空格而无需插入引号。这些需求取决于标记内容的位置(换句话说,作为paste调用的独立组件,或者作为操作符工具如frac的组件)。例如,查看expr4中的")"==frac( , )部分,以及通过~符号在x^{alpha-1}~(1-x)^{beta-1}之间分隔空格(这些位于分数的分子部分)。
R 中内置了大量的功能来进行此类字符串格式化,以便在图形显示中使用,这里没有涉及。如果你有兴趣查看更多内容,可以通过在提示符下输入?plotmath来访问帮助文件作为第一步。另外,R 中还有一个非常有用的演示,你可以通过输入demo(plotmath)来查看,它展示了许多可能的功能,以及expression的相关语法。
23.6 完整注释的散点图
为了提供一个包含你迄今为止所考虑的大部分概念的最终示例,让我们创建一个详细的 MPG 与马力数据的散点图,该数据在 23.4.1 节到 23.4.3 节中使用。 图 23-14 中的图像展示了最终结果,底部是最大的图,而顶部是三个较小的中间图,展示了不同制作阶段。

图 23-14:一个详细的mtcars散点图,展示了 MPG 与马力的关系,点的大小与重量成比例
首先,确保你的工作空间中有mpg、hp、wtcex和hpseq2对象(在第 23.4.1 节和第 23.4.3 节中定义),因为你将使用它们来简化代码长度。这里它们再次列出:
R> hp <- mtcars$hp
R> mpg <- mtcars$mpg
R> wtcex <- mtcars$wt/mean(mtcars$wt)
R> hpseq2 <- seq(50,325,by=25)
图形,具有稍微宽一点的右边距和 U 形框,通过以下代码开始:
R> dev.new()
R> par(mar=c(5,4,4,4))
R> plot(hp,mpg,cex=wtcex,axes=FALSE,ann=FALSE)
R> box(bty="u")
这提供了图 23-14 的左上角图像。我使用了dev.new在我的计算机上显式打开了一个新的图形设备,默认大小为 7 × 7 英寸。如果你想,可以使用dev.new提供的width和height参数来改变设备的大小。
现在添加一些坐标轴:
R> axis(2,las=1,tcl=-0.8,family="mono")
R> axis(1,at=hpseq2,labels=FALSE,tcl=-1)
这两行代码为 MPG 添加了左侧垂直坐标轴;通过tcl稍微延长了刻度线,使用las将刻度标签设置为水平,并请求使用"mono"字体。对于水平坐标轴(马力),在hpseq2中的值处绘制了更长的外部刻度线,但通过设置labels=FALSE将其标签隐藏。稍后你将填充这些标签。
很多国家并不使用 MPG 作为燃油效率的度量,而是使用“每百公里的升数”(L/100km)。因此,为了方便他们,假设你希望在图表的右侧提供第二个垂直坐标轴,标明 L/100km。为此,你需要转换公式。基于美国加仑,以下是两者之间的近似转换:

结果表明这个函数是自反的。也就是说,从 MPG 转换回 L/100km,只需在公式中交换这两个变量。
根据观察到的 MPG 数据的范围,我对转换公式进行了一些实验,得到了一个合理的 L/100km 值集合,用来标记右侧坐标轴。
R> L100 <- seq(22,7,by=-3)
R> L100
[1] 22 19 16 13 10 7
请注意,这些坐标轴是按降序排列的,为了方便起见,因为一旦你将这些值转换为 MPG,结果将是递增的:
R> MPG.L100 <- (100/L100*3.78541)/1.609
R> MPG.L100
[1] 10.69385 12.38236 14.70405 18.09729 23.52648 33.60925
这是有道理的——L/100km 的数字越小,意味着汽车的燃油效率越高。
为什么需要这些数字的 MPG 版本呢?嗯,记住,图形本身是基于 MPG 刻度的,因此为了指示 R 在右侧标出适当的刻度线,你需要 MPG“坐标系”中的 L/100km 值。
完成这些后,再次调用axis,你就得到了图 23-14 的顶部中间图像。
R> axis(4,at=MPG.L100,labels=L100,las=1,tcl=0.3,mgp=c(3,0.3,0),family="mono")
这里特别值得注意的是,你使用了at来指定 MPG 刻度上的刻度线位置,位置在MPG.L100的值处,但由于它们对应于L100中的 L/100km 序列,因此你实际提供给labels的是后者向量,以便正确标记这些刻度线。
接下来,是时候为坐标轴添加一些标题,并为水平坐标轴上的刻度线提供标签了。在此之前,构建一个expression用于 MPG 到 L/100km 的转换,以便澄清右侧垂直坐标轴。
R> express.L100 <- expression(paste(L/100,"km"%~~%frac(378.541,1.609%*%MPG)))
在express.L100中,%~~%表示“约等于”符号(≈),而%*%表示显式的乘法符号(×)。
然后,运行以下代码行,你就可以在图 23-14 中看到右上方的图像。
R> title(main="MPG by Horsepower",xlab="Horsepower",ylab="MPG",
family="serif")
R> mtext(express.L100,side=4,line=3,family="serif")
R> text(hpseq2,rep(7.5,length(hpseq2)),labels=hpseq2,srt=45,
xpd=TRUE,family="mono")
第一行提供了主标题以及x和y轴的标题,采用了"serif"样式。接着,mtext将刚刚创建的算术表达式的"serif"版本放置在右轴的适当位置(side=4),line=3。第三行将"mono"样式的刻度标签放置在hpseq2上,沿着x轴按适当的用户坐标和竖直位置7.5放置(经过一些反复试验)。由于你使用text在图形边距中绘制文本,你必须将xpd设置为TRUE。text特有的可选图形参数srt允许你旋转标签,在这里它们被旋转了45度。
现在,你已经准备好对图表进行最后的修饰了。到目前为止,点的大小按照汽车重量的比例被忽略了。提供至少一些关于这一点以及其他有助于解释关系的特征(尤其是考虑到有两个垂直坐标轴)的信息会很有帮助,比如叠加的网格。
在这样的图表上叠加网格非常简单。
R> grid(col="darkgray")
你可以使用可选参数nx和ny分别指定水平和垂直坐标轴的单元格数;如果不指定,R 会在默认的x-和y-轴刻度位置绘制网格线(正如我在这里所做的)。其他美学效果可以按照常规方法通过col(颜色)和lty(线型)等参数进行修改。
有趣的部分是现在尝试通过汽车的重量来计算绘制点的大小。你可以通过多种方式实现这一点。在这个最后的示例中,我将直接操作legend函数,以便生成图形。以下三行代码提供了最终结果。
R> legend(250,30,legend=rep(" ",3),pch=rep(1,3),pt.cex=c(1.5,1,0.5))
R> arrows(265,27,265,29,length=0.05)
R> text(locator(1),labels="Weight",cex=0.8,family="serif")
图例被放置在用户坐标(250,30)处,包含三个默认pch类型为1的点——一个大点,一个标准点,一个小点——使用pt.cex分别设置为1.5、1和0.5。我并没有通过legend参数为这三个点写标签,而是将它们设置为空字符串,由 10 个空格组成。这样做的结果是扩大了图例周围的框,腾出空间给三个点添加的内容——一个朝上的小箭头和单词Weight。找到合适的用户坐标以便箭头能够适应人为创建的空白图例框需要一些反复试验,最后通过调用交互式locator函数(正如你在第 23.3 节中看到的)来放置“Weight”文字。
使用 R 功能制作这种复杂的图形是学习如何处理语言的传统图形功能的一个很好的起点。通过反复试验和一些小技巧来达成最终结果并不罕见,尽管当然,这种方式会影响代码的健壮性。例如,即使适度地调整图形设备的大小,并尝试重新绘制前面展示的 mtcars 散点图,可能会导致图例中箭头的不对齐。如果你想了解更多内容,关于 R 图形的权威参考书是 Murrell (2011),这是一本很好的教材,适合在你掌握了此处讨论的基础知识后,深入了解 R 中所有与图形相关的内容。
练习 23.2
对于以下任务,你将使用 Chu (2001) 分析的钻石定价数据。你需要有互联网连接来完成此操作。读取数据并像之前一样命名列:
R> dia.url <- "http://www.amstat.org/publications/jse/v9n2/4cdata.txt"
R> diamonds <- read.table(dia.url)
R> names(diamonds) <- c("克拉","颜色","清晰度","证书","价格")
-
打开一个新的图形设备,大小为 6 × 6 英寸。将图形区域的边距设置为底部、左侧、顶部和右侧分别为零行、四行、二行和零行。然后,完成以下操作:
-
生成并排的箱线图,展示以认证分类的钻石价格(单位:新加坡元 SGD$)。隐藏所有坐标轴和周围的框架—请注意,
boxplot命令需要设置frame=FALSE来隐藏框架(而不是plot中的bty="n")。使用相同的命令为图形添加合适的标题。 -
接下来,插入一个垂直轴。该轴的刻度范围从 SGD$0 到 SGD$18000,步长为 SGD$2000。然而,轴应当被裁剪到绘图区域内。刻度线应指向轴内,并且长度为一行。轴标签应距离轴线半行,并且应水平显示。
-
最后,使用
locator和text结合来添加一个合适的标题,位于 y 轴的顶部;请注意,裁剪需要被放宽。使用相同的方法向每个箱线图内添加文本,表示相应的认证(如 GIA、HRD 或 IGI)。我的绘图版本如下所示:
![image]()
-
-
现在,打开一个新的图形设备,大小为 8 × 7 英寸。将图形的边距设置为底部、左侧、顶部和右侧分别为两行、五行、三行和五行。还要在底部以外的每一侧留出一行外边距空间,而底部则应有两行外边距。
-
绘制一个散点图,将钻石价格放在纵轴上,克拉重量放在横轴上。使用红色、绿色和蓝色根据认证区分点。初始图中抑制所有轴线、框框、标签和标题,但随后添加一个 U 形框。
-
添加横轴。使用
axis在 0.2 到 1.1 之间的克拉值上,以 0.1 为步长,均匀地放置刻度标记。为标签使用粗体、斜体、无衬线字体,并将标签调整为离轴线仅半行的距离。然后,在现有的刻度标记之间添加更小的、朝外的刻度标记。为此,第二次调用axis函数,将刻度放置在 0.15 到 1.05 之间的值上,步长为 0.1。将这些次级刻度标记的长度设置为四分之一行,并抑制轴标签。 -
添加纵轴。左侧的刻度应出现在 SGD\(1000 到 17000 之间。标签应水平可读,并与横轴使用相同的字体风格。右侧的轴刻度应以美元(USD\))为单位,范围是 USD$1000 到 11000,步长为 USD\(1000,并应标注为 USD\)。为此,使用转换公式 USD$ = 1.37 × SGD$。标签方向和字体应与其他轴匹配。
-
对数据的克拉重量进行二次多项式的线性模型拟合。为一系列克拉值提供该模型的预测,跨越观测值的范围;包括 95%的预测区间估计。利用这些信息在散点图上叠加灰色实线表示拟合值,并用灰色虚线表示预测区间。
-
设置表达式对象,用于标注大约的美元转换和回归方程。将转换命名为
expr1,它应类似于 USD$ ≈ 1.37 × SGD$。回归方程应类似于 Price = β[0] + β[1]Carat + β[2]Carat²;将其命名为expr2。 -
使用
mtext添加适当的主标题以及三个单独坐标轴的标题。你可能需要稍微调整每个标题的行深度,以及是否在外边距或图形边距中书写,具体取决于你的间距偏好。最右边的轴标题应使用expr1。 -
可以通过试错找到合适的坐标,或者使用第 23.1 节练习中的
interactive.arrow函数,放置一个指向拟合多项式回归线的箭头,并用expr2进行标注。 -
最后,使用
locator调用将图例放置在任何合适的位置,参考根据认证标识的点的颜色。我的图表看起来是这样的:
![image]()
-
本章节的重要代码
| 功能/操作符 | 简要描述 | 首次出现 |
|---|---|---|
dev.new |
打开新的图形设备 | 第 23.1.1 节, 第 576 页 |
dev.set |
更改活动设备 | 第 23.1.2 节, 第 577 页 |
dev.off |
关闭设备 | 第 23.1.3 节, 第 578 页 |
par |
设置图形参数 | 第 23.1.4 节, 第 579 页 |
layout |
打开新的图形设备 | 第 23.1.4 节, 第 580 页 |
box |
向图表添加框 | 第 23.2.1 节, 第 583 页 |
mtext |
在边距中写文本 | 第 23.2.2 节, 第 584 页 |
locator |
交互式坐标定位 | 第 23.3.1 节, 第 587 页 |
axis |
向图表添加坐标轴 | 第 23.4.3 节, 第 594 页 |
expression |
在图表中渲染希腊字母/数学表达式 | 第 23.5.2 节, 第 598 页 |
title |
添加主标题/坐标轴标题 | 第 23.5.2 节, 第 598 页 |
italic |
文字斜体化 | 第 23.5.3 节, 第 600 页 |
grid |
向图表添加网格 | 第 23.6 节, 第 605 页 |
第二十四章:24
进一步探索图形语法

你在第 7.4 节和第十四章中已经了解了 ggplot2 包的基础知识——这是传统 R 图形的替代方案。在本章中,你将了解这个包的几个更受欢迎和有用的功能,以及它相对年轻的“表亲”ggvis,后者提供了一个交互式的基于浏览器的体验。
24.1 使用 ggplot 还是 qplot?
到目前为止,在创建相对简单的 ggplot2 图形时,你已经使用了 qplot 函数来初始化视觉对象。实际上,更通用的 ggplot 命令是 ggplot2 的核心功能。这两个初始化函数之间有几个关键的区别:
• qplot 是 ggplot 的简化版;如果你只想快速查看数据,或者在直接使用 R 控制台时,它非常适用。
• qplot 设计上让人联想到基础 R 的 plot 函数——你传入 x 和 y 坐标向量,然后告诉它该做什么。相比之下,ggplot 更倾向于将数据参数作为数据框对象,并通过显式地添加几何层来告诉它该做什么。
• 单独调用 qplot 就可以生成一个图形。而使用 ggplot 时,必须先添加图层,才能看到任何内容。
• 要访问 ggplot2 图形的全部功能和灵活性,推荐使用 ggplot 函数;这需要提供比 qplot 更多的显式指令。
总的来说,你可以使用 qplot 或 ggplot 创建大多数图形。许多用户根据数据的形式(换句话说,是数据框还是全局环境中的单独向量)以及他们是否希望图形更精美(例如用于出版)或只是想快速查看数据(直接在控制台中操作)来做出决定。
作为语法差异的快速示例,可以翻回第 297 页上创建右侧图 14-5 直方图的代码。你可以说,对该图形所做的众多修改,确实需要比 qplot 所提供的更为模块化的方法。加载 ggplot2 包,并调用 library("ggplot2"),然后创建以下三个对象:
R> gg.static <- ggplot(data=mtcars,mapping=aes(x=hp)) +
ggtitle("Horsepower") + labs(x="HP")
R> mtcars.mm <- data.frame(mm=c(mean(mtcars$hp),median(mtcars$hp)),
stats=factor(c("mean","median")))
R> gg.lines <- geom_vline(mapping=aes(xintercept=mm,linetype=stats),
show.legend=TRUE,data=mtcars.mm)
第一个对象gg.static表示图形中始终保持不变的部分,比如如果你稍后想要实验性地添加其他特征。请注意,ggplot的调用与qplot不同,前者的第一个参数是整个数据框,这样可以访问数据框中的所有数据列,以便后续的所有几何对象或注释使用。然后,你可以添加ggtitle和labs函数来设置主标题和横轴标题。第二个对象mtcars.mm存储了马力的均值和中位数,作为一个“虚拟”数据框。最后,通过第三个对象gg.lines,将均值和中位数的线叠加到直方图上,gg.lines是对geom_vline函数的单一调用,使用的内容与早期代码中相同,只是在形式上稍作修改,以保持与最初使用ggplot的一致性。
在你调用打印ggplot2对象的命令之前,什么都不会显示(如第 7.4 节中所述)。以下调用将重现图 14-5 右侧的图像:
R> gg.static + geom_histogram(color="black",fill="white",
breaks=seq(0,400,25),closed="right") + gg.lines +
scale_linetype_manual(values=c(2,3)) + labs(linetype="")
这些部分的组合方式与图 14-5 的创建方法非常相似:将geom_histogram层添加到gg.static中以调用图形,而将gg.lines添加并通过scale_linetype_manual修改默认线条类型以标记均值和中位数。如果你想要生成没有这些线条的直方图,只需打印gg.static对象加上geom_histogram即可。
随着你对ggplot2的熟练度提高,你会发现自己根据应用的不同更倾向于使用ggplot或qplot。?ggplot中的帮助文件提供了对ggplot的典型用法的良好描述,并与qplot进行了对比。有关更多信息,请参考 Wickham 的《ggplot2: 数据分析的优雅图形》(2009)。我将在本章余下的图形中使用ggplot,以提供一些ggplot命令语法的示例,并与之前使用qplot的情况进行比较。
24.2 平滑和阴影
使用ggplot2包进行数据可视化在你想按一个或多个分类变量拆分图形特征时特别强大。特别是在你用更难通过基础 R 命令实现的功能来增强图形时,这一点尤为明显。
24.2.1 添加 LOESS 趋势
当你查看原始数据时,有时很难在不拟合参数模型(例如,通过线性回归)的情况下获得整体趋势的印象,这意味着你需要对这些趋势的性质做出假设。这就是非参数平滑的作用——你可以使用某些方法来确定数据的表现方式,而无需拟合特定的模型。这些方法是解释整体趋势的灵活工具,无论它们的形式如何,但其权衡之处在于,你无法提供响应变量和预测变量之间关系的任何具体数值细节(因为你没有估计任何系数,如斜率或截距),而且你也失去了任何可靠的外推能力。
局部加权散点图平滑(LOESS 或 LOWESS) 是一种非参数平滑技术,通过对数据的局部子集使用回归方法,逐步地在解释变量的整个范围内产生平滑趋势。
注意
有关理论细节,第六章 的《应用非参数回归(Härdle, 1990),以及《非参数回归导论(Takezawa, 2006)的第二章 和第三章 提供了 LOESS 平滑器的清晰讨论。
为了举例说明,加载 MASS 包并返回注意到 survey 数据框。首先,创建一个新的数据框对象,删除任何缺失值,以避免默认的警告消息:
R> surv <- na.omit(survey[,c("Sex","Wr.Hnd","Height")])
然后,加载 ggplot2 后,执行以下命令生成图 24-1 左侧的图像。
R> ggplot(surv,aes(x=Wr.Hnd,y=Height)) +
geom_point(aes(col=Sex,shape=Sex)) + geom_smooth(method="loess")
调用 ggplot 会初始化对象并设置默认的映射:手掌跨度作为 x 轴,身高作为 y 轴。添加 geom_point 会添加点,并使用颜色和点类型来区分男性和女性。添加 geom_smooth 会叠加 LOESS 平滑曲线。默认情况下,估算趋势的 95% 置信区间会通过一个透明的灰色阴影区域标出。

图 24-1:展示 ggplot2 (左)和基础 R 图形(右)用于显示通过 LOESS 估算的非参数趋势
现在我将演示如何使用基础 R 图形生成类似的结果。尽管有基础 R 函数,如 scatter.smooth,可以相对快速地生成带有平滑趋势的散点图,但为了能够做一些例如给置信区间区域加阴影的操作,能够一步步构建图形是非常有用的。请比较以下基础 R 代码与 ggplot2 方法的相对简便性,后者生成了图 24-1 右侧的图像:
R> plot(surv$Wr.Hnd,surv$Height,col=surv$Sex,pch=c(16,17)[surv$Sex])
R> smoother <- loess(Height~Wr.Hnd,data=surv)
R> handseq <- seq(min(surv$Wr.Hnd),max(surv$Wr.Hnd),length=100)
R> sm <- predict(smoother,newdata=data.frame(Wr.Hnd=handseq),se=TRUE)
R> lines(handseq,sm$fit)
R> polygon(x=c(handseq,rev(handseq)),
y=c(sm$fit+2*sm$se,rev(sm$fit-2*sm$se)),
col=adjustcolor("gray",alpha.f=0.5),border=NA)
第一行绘制了原始数据,第二行使用内置的 loess 函数提供了平滑的趋势——语法与 lm 完全相同。就像使用 lm 拟合的线性模型一样,在开始绘制之前,你需要为 x 轴变量设置一个精细的数值序列,用于获取点估计及其标准误差;这在第三行通过 seq 实现,然后在第四行使用 predict,并将 se 参数设置为 TRUE。这样会得到一个对象 sm,它是一个包含 $fit 和 $se 组件的列表。
然后,平滑的趋势会通过 sm$fit 调用 lines 来绘制。最后,为每个预测值计算一个粗略的 95% 置信区间,计算方式是将 sm$fit 元素加上和减去 sm$se 中相应标准误差的两倍。这是在调用 polygon 时直接完成的,它根据置信区间形成的顶点绘制了灰色带(其中,rev 命令用于反转给定 handseq 向量中的条目)。你需要通过调用现成的 adjustcolor 命令,指示灰色填充形状透明(alpha.f 参数的值从 0(完全透明)到 1(完全不透明)不等);设置 alpha.f=0.5 就设置了指定的 "gray" 的 50% 透明度。
这一切,还没有添加图例!这个例子确实揭示了基础 R 版本的图像所需的额外努力,不仅体现在脚本的长度上,还体现在整个思考构建过程(例如,正确地将多边形的顶点组合起来以形成置信区域,并记得调整填充形状的透明度,以防止任何预绘制的内容被遮盖)上。当你在这些功能上稍微变得更有雄心时,这一点变得更加明显。假设你想分别为每个性别叠加平滑线;这将需要分别估计 LOESS 函数并重新考虑绘图策略。然而,在 ggplot2 中,这个添加非常简单,只需要改变相关几何体的美学映射。以下代码生成了图 24-2:
R> ggplot(surv,aes(x=Wr.Hnd,y=Height,col=Sex,shape=Sex)) +
geom_point() + geom_smooth(method="loess")
所发生的唯一变化是,颜色和点的类型(col=Sex 和 shape=Sex)的美学映射发生了变化,因此,它不再仅限于绘制的点,而是成为了在初始化调用 ggplot 时声明的默认映射的一部分。任何后续添加的图层(如果没有重新分配映射)都将遵循这个默认设置,就像 geom_point 和 geom_smooth 一样。
注意
LOESS 和其他趋势平滑方法的实现取决于你指定的平滑程度;这由用于每个步骤/位置的局部加权子集的数据比例控制。在估计过程中,较大的比例会导致更平滑、变化较小的趋势估计,而较小的比例则会产生更具变化性的趋势估计。这个值称为 span,可以通过 loess 或 geom_smooth 中的可选参数 span 来设置。然而,对于快速的数据探索,默认值 0.75 通常是足够的。你可以尝试在本节中的示例图中进行实验,看看它对各自趋势的影响。

图 24-2:说明对数据的类别子集分别应用 LOESS 平滑方法,结果是简单的美学映射变化,使用 ggplot2 功能
24.2.2 构建平滑密度估计
平滑的思想不仅限于散点图趋势。核密度估计(KDE) 是一种基于观测数据生成概率密度函数平滑估计的方法。简而言之,KDE 的过程是为数据集中的每一个观测值分配一个缩放的概率函数(即 核),并将这些核加总,以呈现数据集整体的分布。这基本上是直方图的一个高级版本。有关理论细节,Wand 和 Jones(1995)的著作是一个很好的参考。
为了说明这种方法,考虑使用内置的 airquality 数据框;在提示符下输入 ?airquality 打开文档,它会告诉你该数据框包含了纽约市在几个月内测量的空气质量数据。使用以下代码绘制温度数据的核密度估计基本图,并显示在图 24-3 的左侧:
R> ggplot(data=airquality,aes(x=Temp)) + geom_density()
这样的图表也可以通过基础 R 图形轻松创建,使用内置的 density 命令对给定的数据向量实现 KDE。然而,ggplot2 允许你通过美学映射轻松装饰图表,这对 ggplot2 的爱好者来说是一个很大的吸引力。例如,假设你想根据观测的月份分别可视化温度的密度估计。首先,执行以下代码:
R> air <- airquality
R> air$Month <- factor(air$Month,
labels=c("May","June","July","August","September"))

图 24-3:通过 KDE 可视化 airquality 数据框中的温度分布,使用 ggplot2 功能
这会在你的工作区中创建一个 airquality 数据框的副本,并将原本是数字型的 Month 向量重新编码为因子向量(这是 ggplot2 映射所要求的),并为条目适当地标注。然后,使用 air,以下代码生成了图 24-3 右侧的图:
R> ggplot(data=air,aes(x=Temp,fill=Month)) + geom_density(alpha=0.4) +
ggtitle("Monthly temperature probability densities") +
labs(x="Temp (F)",y="Kernel estimate")
不同的密度通过不同的颜色填充来清晰标识,这在ggplot初始化时通过aes中的fill=Month设置。你还可以将alpha=0.4传递给geom_density,设置 40%的透明度,以便清楚地看到所有五条曲线。剩余的ggtitle和labs调用则简洁地整理了主标题和坐标轴标题。这些测量值的分布特征正如你所预期的那样——例如,七月是最热的月份,温度集中在比五月高得多的范围内。
注意
就像 LOESS 技术一样,核估计的概率密度函数的精确外观依赖于所使用的平滑程度。就像构建直方图时的 binwidth 一样,KDE 中的兴趣量称为带宽或平滑参数——更大的带宽在数据范围内施加更大的平滑。在这些示例中,带宽的默认选择是通过数据驱动的技术自动选择的。这种默认的平滑级别通常适用于数据的简单探索。
24.3 多图和变量映射的面板
在第 23.1.4 节中,你看到了几种传统 R 图形如何在一个图形设备中查看或布局的不同方法。类似的方式,例如在调用par时设置mfrow或使用layout对设备进行分区,无法应用于ggplot2图形。不过,仍有其他函数可以让独立的ggplot2图形填充一个设备。与其一贯的风格一致,ggplot2还提供了一种方便的方法,通过面板来处理多图形,其中所有图形都可以一次性绘制。
24.3.1 独立图形
首先,假设你已经独立创建了多个ggplot2图形,并希望将它们安排为一张单一的图像。实现这一点的快速方法是使用贡献包gridExtra中提供的grid.arrange函数 (Auguie, 2012)。通过在提示符下运行install.packages("gridExtra")来安装该包(你需要有互联网连接)。
为了说明如何使用grid.arrange,继续使用air对象——你在第 24.2 节中创建的带有Month列的airquality副本。现在,考虑以下三个ggplot2对象,我稍后会进一步解释:
R> gg1 <- ggplot(air,aes(x=1:nrow(air),y=Temp)) +
geom_line(aes(col=Month)) +
geom_point(aes(col=Month,size=Wind)) +
geom_smooth(method="loess",col="black") +
labs(x="Time (days)",y="Temperature (F)")
R> gg2 <- ggplot(air,aes(x=Solar.R,fill=Month)) +
geom_density(alpha=0.4) +
labs(x=expression(paste("Solar radiation (",ring(A),")")),
y="Kernel estimate")
R> gg3 <- ggplot(air,aes(x=Wind,y=Temp,color=Month)) +
geom_point(aes(size=Ozone)) +
geom_smooth(method="lm",level=0.9,fullrange=FALSE,alpha=0.2) +
labs(x="Wind speed (MPH)",y="Temperature (F)")
执行library("gridExtra")来加载所需的包。要在一个窗口中查看gg1、gg2和gg3,只需调用以下代码,这将生成图 24-4:
R> grid.arrange(gg1,gg2,gg3)
请注意,你可能会看到一些警告信息,告诉你air数据框中有缺失值,并建议调整显示图形的窗口大小。

图 24-4:一系列三幅ggplot2图形,展示了airquality数据,通过grid.arrange在gridExtra*包中绘制在同一设备窗口中。顶部:按天的温度时间序列,区分月份和风速,带有整体 LOESS 趋势和 95%置信区间。中部:按月分布的太阳辐射的核密度估计。底部:温度与风速的散点图,使用颜色区分月份,点大小参考臭氧水平。温度与风速的简单线性回归模型分别拟合,按月份划分,并叠加 90%置信区间。
如你所见,grid.arrange非常易于使用——你只需首先创建你的ggplot2图像并将它们存储为对象,然后将它们直接传递给排列函数。grid.arrange根据你提供的对象数量来决定如何生成最终布局(在这个例子中,是三幅图的列排布)。你可以通过改变传入对象的顺序来控制图表的排列顺序。还有更多可选的参数,你可以在文档文件?grid.arrange中阅读详细内容。
图表gg1、gg2和gg3也为我们提供了讨论更多ggplot2功能的机会。由于特别是在gg1和gg3中涉及的内容较多,我将分别讨论每个对象的代码。
gg1 第一个图表显示的是每日温度。在ggplot中设置默认美学时,我创建了一个整数序列,与air中的行数相匹配,并与相关的Temp元素配对。然后,geom_line和geom_point添加了连接线和原始观测数据,并被加入到默认的美学映射中。连接线的颜色根据Month变化。原始观测数据的颜色也根据Month变化,点的大小与风速读数成正比。我包括了一个整体的 LOESS 平滑曲线,其默认颜色被更改为"black"。我在这里保留默认映射——我不希望每个月都有单独的平滑趋势。最后,添加labs仅仅是为了澄清轴标题,正如你已经看到的那样。
gg2 第二个图表是图 24-3 右侧图表的变体。这一次,它显示了太阳辐射读数的估计密度(单位为埃)。正如你之前看到的,透明度是通过geom_density中的alpha设置的。值得注意的是,我在labs中使用了expression来近似表示埃单位符号Å,使用了ring(A)。
gg3 最后一张是温度与风速的散点图,其中可以看到负相关关系。颜色(再次为每个月分配)也设置为ggplot中的默认美学映射。在调用geom_point时,美学增强指示点的大小与臭氧读数成比例(这是为了确保相应图例的正确格式化,考虑到接下来的内容)。在这里,你可以看到geom_smooth的另一种使用方式。在设置method="lm"时,我想要叠加的线条(或多条线)是根据x和y美学映射分别作为预测变量和响应变量拟合的简单线性模型。此外,默认映射中包括因子Month,确保为每个月的温度与风速数据分别拟合简单线性模型,并进行适当的着色(需要注意的是,绘制的线条并不反映包含图中所有变量的多元线性模型)。每个回归都包括透明度为 90%的置信区间(level=0.9 和 alpha=0.2),并且通过设置fullrange=FALSE,每条回归线的范围仅限于每个月观测数据的宽度。
24.3.2 映射到类别变量的面板
如果你希望独立创建的ggplot2图形出现在同一窗口中,grid.arrange无疑是处理它们的最佳方式。然而,ggplot2包提供了一种灵活的替代方法,可以快速查看多个图形。在探索数据集时,你可能希望根据一个或多个重要类别变量的级别创建多个相同变量的图形。这种行为被称为面板化,使用ggplot2的facet_wrap或facet_grid命令是这一领域的常见方法。
让我们专注于最简单的情况,即你只有一个类别变量。继续使用air数据框对象,以下代码行创建了一个ggplot2对象,该对象显示了右侧图 24-3 中纽约气温的密度图:
R> ggp <- ggplot(data=air,aes(x=Temp,fill=Month)) + geom_density(alpha=0.4) +
ggtitle("Monthly temperature probability densities") +
labs(x="Temp (F)",y="Kernel estimate")
与其将所有密度估计一起查看,不如将每个密度估计分别绘制出来,并显示在同一个设备中,使用以下三种facet_wrap方式;结果如图 24-5 所示,分别位于左上角、右上角和底部。
R> ggp + facet_wrap(~Month)
R> ggp + facet_wrap(~Month,scales="free")
R> ggp + facet_wrap(~Month,nrow=1)

图 24-5:使用 facet_wrap 显示按月划分的温度数据的核密度估计的三种示例
facet_wrap函数自动排列多个图形;公式指定了分面变量。在之前的所有图形中,这个变量被设置为~Month,表示“按月份分面”。位于左上角的第一张图的代码没有添加其他参数,并且没有参数时,x轴和y轴的范围是固定的,这样你可以在相同的尺度上比较图形。如果你不希望如此,你可以指示坐标轴为“自由”,意味着每个图形会根据其自身内容在独立的尺度上显示。你可以在第二张图中看到这一点,位于图 24-5 的右上角,其代码指定了scales="free"。你也可以选择仅将水平或垂直轴设为自由,分别使用scales="free_x"或scales="free_y"。最后,请注意,通过使用nrow和ncol参数可以定制分面位置。在第三张图中,设置nrow=1指示 R 将图形仅放置在一行中,生成图 24-5 底部的水平排列。有关位置的更多细节,你可以在?facet_wrap文档中找到相关信息。
facet_wrap的替代方法,facet_grid,执行的操作大致相同,但如果仅通过一个分类变量进行分面时,无法将图形进行换行。公式var1 ~ var2 的含义是“按var1进行行分面,按var2进行列分面”。如果你确实只希望通过一个分组变量进行列或行的分面,只需将var1或var2中的一个替换为点(.)。例如,图 24-5 中的第三张图可以通过facet_grid轻松实现,如下所示:
R> ggp + facet_grid(.~Month)
然而,下一个例子展示了facet_grid在两个分组变量下的应用。再次关注faraway包中的diabetes数据框。在加载该包后,以下代码创建了包含diab数据框的对象,并删除了缺失值行,从而生成图 24-6:
R> diab <- na.omit(diabetes[,c("chol","weight","gender","frame","age",
"height","location")])
R> ggplot(diab,aes(x=age,y=chol)) +
geom_point(aes(shape=location,size=weight,col=height)) +
facet_grid(gender~frame) + geom_smooth(method="lm") +
labs(y="cholesterol")
初始调用ggplot命令告诉 R 使用diab数据框,并绘制总胆固醇与年龄的关系。接着,添加geom_point设置每个点的形状、大小和颜色,分别根据县位置、体重和身高的不同而变化(如你已经看到的,基于连续变量的点大小,点的颜色也会自动根据映射的美学变量变化,如果该变量不是因子的话)。

图 24-6:展示了在ggplot2中使用faraway包中的diabetes数据框进行双向分面。胆固醇水平与年龄的关系图,以及简单的线性模型拟合,按性别(行)和体型类型(列)进行分面。点的颜色和大小分别根据体重和身高来设置,两个不同的点类型区分了研究参与者来自弗吉尼亚的两个县的位置。
到目前为止,这些命令仍然仅定义了一个单一的散点图。通过添加对facet_grid的调用,公式gender~frame将图形分成不同的散点图,分别展示男性/女性(按行)以及三种体型类型:小型/中型/大型(按列)。你为每个图形设置了简单线性模型拟合,基于默认的美学映射(胆固醇与年龄的关系),并通过调用geom_smooth来实现,最后通过调用labs明确了纵轴的标题。
这些图形本身总体上揭示了你在之前对这些数据的分析中可能已注意到的一些趋势(第 21.5.2 节)。年龄的增加往往与胆固醇的均值增加相关,尽管这种关系似乎在视觉上对于男性不那么明显。左侧小体型列中的点整体较小是合理的——体型较小的人通常体重较轻。底行(女性)的图形倾向于比顶部行的图形颜色更深——这表明,平均而言,女性通常比男性矮。关于两个县的参与者之间的差异,很难分辨——看起来巴金汉县(•)和路易莎县(▴)的符号模式没有明显的系统性差异(不过请记住,如果你想理解多变量数据中复杂的潜在交互关系,适当的统计模型拟合优于仅依赖图形)。
这些优雅的图形进一步突出了ggplot2包在生成复杂图形方面的相对简便性——通常涉及根据一个或多个因素对观测数据进行分区——无论是在单独的图像还是图像的排列方面。尽管使用基础 R 方法当然仍然可以绘制类似的图形,但这种方法要求你更细致或更低级地处理数据子集的细节,以及任何变化的美学特征。这并不意味着基础 R 图形是多余的或应该被忽视(你将在第二十五章看到一些通过传统命令实现的不错的新图形)——只是通过使用 Wickham 的图形语法实现,你可以减少编码工作(并且通常会得到一个更漂亮的最终结果)。
练习 24.1
加载MASS包并查看UScereal数据的帮助文件。该数据框提供了 1990 年代初期美国市场上销售的早餐谷物的营养及其他信息。
-
创建数据框的副本,命名为
cereal。为了简化绘图,将cereal中的mfr列(制造商)转换为具有三个层次的因子,并将相应标签设置为"General Mills"、"Kelloggs"和"Other"。同时,将shelf变量(货架编号)转换为因子。 -
使用
cereal,构建并存储两个ggplot对象。-
一个卡路里与蛋白质关系的散点图。点的颜色应根据货架位置,点的形状应根据制造商来区分。包括卡路里与蛋白质关系的简单线性回归线,按货架位置拆分。确保坐标轴和图例标题整洁。
-
一组卡路里的核密度估计图,使用填充颜色区分货架位置。填充色透明度设为 50%,并确保坐标轴和图例的标题整洁。
-
-
将(b)中的两个图表排列在一个设备上。
-
创建一个显示卡路里与蛋白质关系的面板图,每个面板对应
cereal对象中定义的一个制造商。在每个散点图上叠加一个 90%跨度的 LOESS 平滑线。此外,点的颜色应根据糖分含量,点的大小应根据钠含量,点的形状应根据货架位置来决定。
加载car包(如果你还没有安装,请先下载并安装),并查看Salaries对象——一个数据框,详细记录了 2008–2009 学年期间在美国工作的 397 名学者的薪资(以美元为单位)(Fox and Weisberg, 2011)。通过查看帮助文件?Salaries,你可以了解当前的变量,除了薪资数字外,还包括每位学者的职称、性别和研究领域(作为因子),以及服务年限。
-
生成一个名为
gg1的ggplot对象,绘制一个散点图,其中纵轴为薪资,横轴为服务年限。使用颜色区分男性和女性,并显示性别特定的 LOESS 趋势,同时确保坐标轴和图例的标题易于理解。查看你的图表。 -
创建以下三个附加的绘图对象,确保坐标轴和图例标题整洁。分别命名为
gg2、gg3和gg4:-
按职称分组的薪资并排箱型图。每个箱型图应根据性别进一步分组(这可以通过默认的美学映射来完成——尝试将性别变量分配给
col或fill)。 -
按学科分组的薪资并排箱型图,每个学科按性别进一步分组,使用颜色或填充来区分。
-
使用 30%透明度填充区分职称的薪资核密度估计图。
-
-
将你的四个绘图对象(
gg1、gg2、gg3和gg4)从(e)和(f)中显示在一个设备上。 -
最后,绘制以下内容:
-
一系列使用 70%不透明填充来区分男性和女性的薪资核密度估计图,按学术职称分面显示。
-
使用颜色区分男性和女性的薪资与工作年限的散点图,按学科作为行,按学术职称作为列进行分面。每个散点图应有性别特定的简单线性回归线,并叠加置信区间带,同时具有自由的横轴刻度。
-
24.4 ggvis 中的交互式工具
在本章结束时,我将介绍“gg”家族中一个相对较新的成员,ggvis,由 Chang 和 Wickham(2015)开发。这个包使你能够设计灵活的统计图表,最终用户可以与之互动。结果以网页图形的形式提供。当图像弹出时,它会作为一个新标签页出现在你默认的网页浏览器中(如果你使用的是 RStudio IDE——参见附录 B——ggvis图形会嵌入在 Viewer 面板中)。
作为警告,请注意,ggvis在撰写时仍由其作者开发中。新的功能正在添加,bug 也在修复。如果你对该功能感兴趣,可以访问ggvis网站,网址是ggvis.rstudio.com/。该网站包含面向初学者的教程和目前可用功能的食谱。在这里,我将简要概述ggvis。
安装ggvis包及其依赖项,然后通过调用library("ggvis")加载它。同时确保你可以访问学生调查数据survey,通过加载MASS包来实现。创建以下对象以供后续示例使用:
R> surv <- na.omit(survey[,c("Sex","Wr.Hnd","Height","Smoke","Exer")])
开始一个ggvis图形的常见方式是声明感兴趣的数据框,然后调用ggvis来定义要使用的变量,最后添加各个图层。当你使用数据框中的变量时,它们必须以~为前缀,这明确告诉 R 你是在引用该数据框的列,而不是其他地方同名的对象。要在对象定义中添加函数时,不像ggplot2中使用+,而是使用%>%(称为管道)。ggplot2中的geom_函数在ggvis中以layer_为前缀。
我们从一个简单的静态图开始。上方的图 24-7 中的直方图(身高测量值)可以通过以下执行获得:
R> surv %>% ggvis(x=~Height) %>% layer_histograms()
Guessing width = 2 # range / 25
声明surv数据框后,将其传递给ggvis,指示~Height变量映射到x轴。最后,通过管道传递给layer_histograms,生成图形,并根据x映射数据的范围分配默认的 bin 宽度。
那又怎样?你已经创建了很多直方图。但如果你能够在不需要一个又一个静态图的情况下调整 binwidth 的值,那不是更好吗?ggvis 中的 input_ 命令集允许你指示生成的图形接受交互输入。参考以下代码;图 24-7 展示了我的结果。
R> surv %>% ggvis(x=~Height) %>%
layer_histograms(width=input_slider(1,15,label="Binwidth:"),fill:="gray")
Showing dynamic visualisation. Press Escape/Ctrl + C to stop.

图 24-7:来自 survey 数据框的身高观察值的直方图,使用 ggvis 。上图:默认静态图。下图:包含与 binwidth 关联的滑动按钮的结果——用户可以与之互动,并立即看到更改 bins 的效果。
这里,控制感兴趣特征的 width 参数被设置为 input_slider 的结果,从而设置了一个交互式滑动按钮。width 的滑动值范围设置为 1 到 15(包括 15),可选的 label 参数为交互式工具提供了标题。最后,使用 layer_histogram 中的 fill 设置条形的颜色。请注意,特定的赋值 fill:="gray" 使用的是 :=,而不是 =。单独使用 = 在 ggvis 中用于变量映射,即当感兴趣的特征需要传递一个可变的变量时,本质上类似于 ggplot2 中的美学映射。:= 的组合应被理解为一个常量设置,也就是说,当你简单地打算全局固定某个特征时。
一旦代码成功执行,你可以尝试滑动按钮来调整更小或更大的 binwidth。观察你的分布图解释随着调整而变化的情况很有意思。如同控制台中执行命令下方输出的文字所示,你必须退出交互式图形才能再次使用 R。按下 ESC 键将终止交互并将控制权交回给用户。
其他类型的交互功能包括 input_select(用于下拉菜单)、input_radiobuttons(单选按钮选项)和 input_checkbox(用于复选框)。你甚至可以使用 input_numeric 设置交互式文本或数字输入框。有关更多详细信息,请参阅相关帮助文件或 ggvis 网站。
作为另一个基于 surv 数据框的示例,我们尝试一个散点图。从一个简单的静态图开始,运行以下代码:
R> surv %>% ggvis(x=~Wr.Hnd,y=~Height,size:=200,opacity:=0.3) %>%
layer_points()
我在这里不会展示这个结果,但你可以从 ggvis 的调用中看到,你将绘制身高与手掌跨度之间的关系,并且你会普遍放大点的大小,同时设置 30% 不透明度的全局水平。最后一条管道传递给 layer_points 生成图像。与静态直方图的情形一样,由于没有交互性,你无需“退出”图形——控制权会立即返回到控制台提示符处。
如果想要更有趣的图形,可以尝试如下:
R> filler <- input_radiobuttons(c("Sex"="Sex","Smoking status"="Smoke",
"Exercise frequency"="Exer"),map=as.name,
label="Color points by...")
R> sizer <- input_slider(10,300,label="Point size:")
R> opacityer <- input_slider(0.1,1,label="Opacity:")
R> surv %>% ggvis(x=~Wr.Hnd,y=~Height,fill=filler,
size:=sizer,opacity:=opacityer) %>%
layer_points() %>% add_axis("x",title="Handspan") %>%
add_legend("fill",title="")
Showing dynamic visualisation. Press Escape/Ctrl + C to stop.
首先,为交互部分创建三个对象。一组单选按钮指定根据三个可能的类别变量之一(Sex、Smoke 或 Exer)来设置绘制点的颜色,两个滑块按钮控制点的大小和透明度。注意,当你打算使用数据框中的变量作为交互行为的要素时,需要将它们的精确名称作为字符字符串向量提供,并设置可选的 map=as.name;这是在定义 filler 对象时完成的。在随后的 ggvis 调用中,使用 = 将 filler 传递给 fill。对象 sizer 和 opacityer 中的两个滑块按钮通过 := 传递给相关参数,因为它们不依赖于数据框中的变量。调用 layer_points 生成图形,接下来的管道操作 add_axis 和 add_legend 仅仅是将 x 轴和图例标题从默认值调整为更整洁的形式。
图 24-8 的顶部展示了结果的截图,我已选择点的颜色根据运动频率变量变化,减少了点的大小,并选择了中到高透明度。

图 24-8: ggvis 散点图的两个例子,展示了学生调查数据中的身高与手掌跨度关系。顶部:通过单选按钮根据性别、吸烟状态或运动频率更改颜色(填充),并使用滑块按钮调整点的大小和透明度。底部:通过颜色将点按性别划分,并叠加性别特定的 LOESS 平滑曲线及相应的置信区间;其平滑跨度可以通过滑块按钮进行控制。
最后,让我们生成相同的散点图,但选择使用性别来为点着色。你可以为男性和女性分别添加 LOESS 平滑曲线,并使用滑块按钮动态控制平滑的程度。这个最后的例子,其截图出现在 图 24-8 的底部,是执行以下代码的结果:
R> surv %>% ggvis(x=~Wr.Hnd,y=~Height,fill=~Sex) %>% group_by(Sex) %>%
layer_smooths(span=input_slider(0.3,1,value=0.75,
label="Smoothing span:"),
se=TRUE) %>% layer_points() %>%
add_axis("x",title="Handspan")
Showing dynamic visualisation. Press Escape/Ctrl + C to stop.
LOESS 平滑曲线通过 layer_smooths 命令添加,其 span 参数,即感兴趣的目标参数,分配给一个 input_slider。其可能的值范围按常规设置,可选的 value 参数(在其他 input_ 函数中也适用)设置初始值,当图形首次初始化时使用。此外,se=TRUE 参数确保 95% 的置信区间与平滑趋势一同显示。请注意,管道操作 layer_smooths 被 group_by(Sex) 的管道操作所预先处理。如果没有这个,平滑曲线将仅应用于 x 和 y 数据的整体(另外需要注意的是,在写作时,你在 group_by 中不需要为变量名加上 ~)。
因此,ggvis展示了用于动态数据可视化探索的巨大潜力。这些工具在演示或网站设计等活动中特别有用,因为你可以为观众提供一种图形语法风格的交互式数据展示。如果你有兴趣使用这些工具,我强烈建议你关注ggvis网站的最新动态。
练习 24.2
确保加载了car和ggvis包。重新查看你在练习 24.1 中查看过的Salaries数据框;查看帮助文件?Salaries,以回顾当前的变量。
-
生成一个交互式散点图,纵轴为薪资,横轴为工作年限。使用单选按钮根据学术职称、研究领域或性别来为点上色。使用管道操作符
add_legend和add_axis,分别去除图例标题并整理坐标轴标题。 -
使用管道操作符
layer_densities(你尚未接触过)生成核密度估计,类似于图 24-5 中出现的估计。-
使用
ggvis创建一个静态图,展示按照学术职称分组的薪资分布的核密度估计。为此,将薪资变量分配给x,将职称变量分配给fill,然后使用管道操作符group_by显式指定按职称变量分组。最后,使用管道操作符layer_densities(在此情况下使用所有默认参数值)生成图形。你的结果应该类似于练习 24.1 中的gg4对象。 -
就像
layer_histograms中的width参数用于控制直方图外观一样,layer_densities中的adjust参数用于控制核密度估计的平滑程度。重新生成前一个图中的职称特定核密度估计,但这次图形应为交互式——实现一个滑块按钮,范围从 0.2 到 2,标签为“平滑度”,以控制平滑调整。根据需要,可以选择抑制或明确显示结果中的坐标轴和图例标题。
-
确保已经加载了MASS包,这样可以再次访问UScereal数据框。如果还没有做过此操作,请查看帮助文件?UScereal,并按照练习 24.1(a)中指定的方式重新创建cereal对象。然后执行以下操作:
-
设置一个对象,供单选按钮选择制造商、货架和维生素变量。确保每个单选按钮的标签清晰,并设置一个适当的标题标签,用于形成将点上色的选项集合。将该对象命名为
filler。 -
借用在第 24.4 节中创建的
sizer和opacityer对象,并使用你刚刚在(c)中创建的对象来控制fill,创建一个交互式的蛋白质与卡路里的散点图。整理轴标题,并抑制点的颜色填充的图例标题。最终效果在功能上应与图 24-8 中顶部截图的图形基本相同。 -
为(c)中指定的相同单选按钮创建一个新对象,用来控制点的形状(换句话说,就是用于绘制点的字符)。相应地修改标题标签。将此对象命名为
shaper。 -
最后,重新创建与(d)中完全相同的蛋白质与卡路里的交互式散点图,但这次还需要将(e)中的
shaper分配给ggvis调用中的shape修饰符。为了防止两个单选按钮组的图例重叠,你需要在代码中添加以下管道:add_legend("shape",title="",
properties=legend_props(legend=list(y=100)))
和
set_options(duration=0)
第一个管道将
shape修饰符的图例垂直向下移动,第二个管道消除了交互式图形在切换选项时默认发生的轻微“动画延迟”。再次使用额外的add_axis和add_legend调用来澄清或抑制轴和图例标题。
本章中的重要代码
| 函数/操作符 | 简要描述 | 首次出现 |
|---|---|---|
ggplot |
初始化ggplot2图表 |
第 24.1 节, 第 610 页 |
geom_smooth |
趋势线几何 | 第 24.2.1 节, 第 612 页 |
loess |
计算 LOESS(基础 R) | 第 24.2.1 节, 第 612 页 |
rev |
反转向量元素 | 第 24.2.1 节, 第 612 页 |
adjustcolor |
改变颜色的不透明度(基础 R) | 第 24.2.1 节, 第 612 页 |
geom_density |
核密度几何 | 第 24.2.2 节, 第 614 页 |
ggtitle |
添加ggplot2标题 |
第 24.2.2 节, 第 615 页 |
grid.arrange |
多个ggplot2图表 |
第 24.3.1 节, 第 616 页 |
facet_wrap |
单因素分面 | 第 24.3.2 节, 第 619 页 |
facet_grid |
两因素分面 | 第 24.3.2 节, 第 620 页 |
ggvis |
初始化ggvis图表 |
第 24.4 节, 第 624 页 |
%>% |
管道到ggvis层 |
第 24.4 节, 第 624 页 |
layer_histograms ggvis |
直方图层 | 第 24.4 节,第 624 页 |
input_slider |
交互式滑块 | 第 24.4 节,第 624 页 |
:= |
常量ggvis赋值 |
第 24.4 节,第 624 页 |
layer_points ggvis |
点层 | 第 24.4 节,第 626 页 |
input_radiobuttons |
交互式按钮 | 第 24.4 节,第 626 页 |
add_legend |
添加/修改ggvis图例 |
第 24.4 节,第 626 页 |
layer_smooths ggvis |
趋势线层 | 第 24.4 节,第 627 页 |
add_axis |
添加/修改ggvis坐标轴 |
第 24.4 节,第 627 页 |
第二十五章:25
定义颜色和在高维度中绘制图形

现在你已经掌握了一些基本的可视化技巧,你可以通过给点着色来超越标准的x和y坐标轴,依据某些附加的值或变量,或者添加一个z轴来构建 3D 图表。像这样的高维度图表允许你使用比其他方式更多的变量来直观地探索你的数据或模型。
在本章中,你将深入了解如何在 R 中处理颜色和调色板,然后你将看到四种新的图表类型:3D 散点图、等高线图、像素图和透视图。
25.1 表示和使用颜色
颜色在许多图表中起着关键作用。正如你已经看到的,颜色不仅可以纯粹用于美学增强,也可以通过区分不同的值和变量,成为解读数据/模型的重要工具。在学习一些更复杂的数据和模型可视化工具之前,了解 R 如何正式表示和处理颜色是很有用的。在本节中,你将学习常见的创建和表示特定颜色的方法,以及如何定义和使用一组协调一致的颜色;后一种方法称为调色板。
25.1.1 红绿蓝十六进制颜色代码
在指定图表中的颜色时,你迄今为止给 R 的指令通常是通过一个从1到8的整数值,或者是字符字符串的形式(请参见第 7.2.3 节中的相关注释)。为了编程目的,你需要这些颜色的更客观的表示方式。
最常见的颜色指定方法之一是指定三种原色——红色、绿色和蓝色(RGB)的不同饱和度或强度,然后将它们混合形成最终的目标颜色。标准 RGB 系统中的每个原色分量都被赋予一个从 0 到 255(包含)的整数。因此,这样的混合可以形成 256³ = 16,777,216 种可能的颜色。
你总是按(R,G,B)的顺序来表示这些值;结果通常称为三元组。例如,(0,0,0)代表纯黑色,(255,255,255)代表纯白色,(0,255,0)代表纯绿色。
col参数允许你在输入从 1 到 8 的整数时,选择八种颜色中的一种。你可以通过以下调用来找到这八种颜色:
R> palette()
[1] "black" "red" "green3" "blue" "cyan" "magenta"
[7] "yellow" "gray"
这些只是你通过在 R 提示符下输入colors()命令可以列出的 650 多个命名颜色的一个小子集。所有这些命名颜色也可以用标准 RGB 格式表示。要查找颜色的 RGB 值,只需将所需的颜色名称作为字符向量传递给内置的col2rgb函数。以下是一个示例:
R> col2rgb(c("black","green3","pink"))
[,1] [,2] [,3]
red 0 0 255
green 0 205 192
blue 0 0 203
结果是一个 RGB 值矩阵,每一列代表你指定的颜色之一。这就是 R 在 RGB 意义上,当你请求它用相应的字符字符串绘制这些颜色时的实际含义。
这些 RGB 三元组通常表示为十六进制数,这是一种在计算机中常用的数字编码系统。在 R 中,十六进制或十六进制代码是一个字符字符串,前面跟着一个#,后面是六个字母数字字符:有效字符是字母A到F和数字 0 到 9。第一个字符对表示红色分量,第二和第三个字符对分别表示绿色和蓝色。如果你有或创建一个或多个 RGB 三元组,你可以通过rgb函数将它们转换为 R 可以在后续绘图中使用的十六进制代码。这个命令接受一个 RGB 值的矩阵,但请注意,它期望每个(R, G, B)颜色是该矩阵的行(而不是例如通过col2rgb调用时提供的列)。
你还需要告诉rgb,根据标准的 RGB 格式,你的最大颜色值是 255(因为默认情况下,它将此值缩放并使用 1)。以下代码对之前调用col2rgb的结果执行矩阵转置(参见第 3.3 节),将我的三种颜色作为 RGB 三元组以行的形式放入所需的格式,并相应地指定maxColorValue:
R> rgb(t(col2rgb(c("black","green3","pink"))),maxColorValue=255)
[1] "#000000" "#00CD00" "#FFC0CB"
输出会告诉你 R 所指的 RGB 值对应的十六进制代码,分别为"black","green3"和"pink"。
我不会在这里详细讲解如何将标准的 RGB 三元组转换为十六进制,因为这超出了本书的范围,但了解 R 代表你使用 RGB 三元组创建的任何颜色作为十六进制代码是很重要的,所以当你在绘制图形时,至少应该能够识别十六进制代码。
为了更丰富的探索,让我们写一个简单的函数,用单独的颜色绘制点,并用 RGB 三元组和相应的十六进制代码为其加上标签。请在编辑器中参考以下内容:
pcol <- function(cols){
n <- length(cols)
dev.new(width=7,height=7)
par(mar=rep(1,4))
plot(1:5,1:5,type="n",xaxt="n",yaxt="n",ann=FALSE)
for(i in 1:n){
pt <- locator(1)
rgbval <- col2rgb(cols[i])
points(pt,cex=4,pch=19,col=cols[i])
text(pt$x+1,pt$y,family="mono",
label=paste("\"",cols[i],"\"","\nR: ",rgbval[1],
" G: ",rgbval[2]," B: ",rgbval[3],
"\nhex: ",rgb(t(rgbval),maxColorValue=255),
sep=""))
}
}
函数pcol接受一个参数cols,它应该是一个字符向量,包含 R 识别的颜色名称。当你执行pcol时,它会打开一个新的图形设备,并将图形边距设置为每一边各一行。一个图形开始绘制,但除了边框外,其他部分都被完全抑制。这样,你可以使用locator(见第 23.3 节)在图形区域内逐个放置点,这通过一个for循环实现。每个点代表一个cols中的颜色,locator返回其坐标后,points会在当前位置绘制一个大的颜色点,text则在每个点的右侧提供注释,注释是通过paste实现的(参见第 4.2 节)。这个注释包括 R 颜色名称、RGB 三元组和十六进制代码,它们依次叠加在一起;后两个通过col2rgb和rgb函数获得,正如前面所演示的。
以下代码设置了设备,首先将 14 个有效的 R 颜色名称(随机选择)存储在字符向量mycols中。在通过鼠标点击不同区域消耗完这些颜色后,执行过程完成。
R> mycols <- c("black","blue","royalblue2","pink","magenta","purple",
"violet","coral","lightgray","seagreen4","red","red2",
"yellow","lemonchiffon3")
R> pcol(mycols)
当我执行如这里所示的pcol时,我点击了 14 个点,在我的图形设备上生成了粗略的列。图 25-1 展示了结果。

图 25-1: 各种命名的 R 颜色及其对应的 RGB 三元组和十六进制代码。
实质上,你可以通过指定 RGB 值并获取其十六进制代码,获得任何你想要的颜色(换句话说,比 R 内置的命名颜色多得多)。这些十六进制数值可以直接用于任何传统的 R 图形函数中,在那里你指定颜色(通常是col参数)。随着章节的进展,你将看到这一点。当然,你也可以将一个十六进制代码或一组十六进制代码(例如,如果你正在创建自己的自定义颜色)分配给 R 工作区中的一个新对象,这样你就可以在后续绘图中使用它。
25.1.2 内置调色板
当你需要许多颜色时,能够实现自己的 RGB 颜色是最有用的,这些颜色的集合被称为调色板。通常,当颜色用于描述某种连续体的事物时,你会需要一个调色板,比如在图 24-6 的第 621 页上使用的不同蓝色阴影来表示高度测量。
基础 R 安装中内置了多个颜色调色板。这些由函数rainbow、heat.colors、terrain.colors、topo.colors、cm.colors、gray.colors和gray定义。除了gray之外,你可以直接指定所需颜色的数量,它们将作为一个包含十六进制代码的字符向量返回,表示该调色板整个颜色范围内等间距的颜色序列。
最容易通过可视化效果来查看这一点。以下代码从每个调色板中生成恰好 600 种颜色:
R> N <- 600
R> rbow <- rainbow(N)
R> heat <- heat.colors(N)
R> terr <- terrain.colors(N)
R> topo <- topo.colors(N)
R> cm <- cm.colors(N)
R> gry1 <- gray.colors(N)
R> gry2 <- gray(level=seq(0,1,length=N))
请注意,gray期望的是一个介于 0(完全黑)和 1(完全白)之间的数值向量来提供灰度,而不是一个单一的整数。它的对应函数gray.colors的工作原理与其它内置调色板相同,但默认的视觉范围稍微窄一些,位于黑与白之间的极限。这些可以通过可选参数start和end来重置,稍后你会看到。
接下来的代码块使用了第二十三章中的技巧,初始化了一个新的图形,并使用向量重复将 600 个点放置到points函数的一次调用中,根据十六进制代码向量为这些点着色。
R> dev.new(width=8,height=3)
R> par(mar=c(1,8,1,1))
R> plot(1,1,xlim=c(1,N),ylim=c(0.5,7.5),type="n",xaxt="n",yaxt="n",ann=FALSE)
R> points(rep(1:N,7),rep(7:1,each=N),pch=19,cex=3,
col=c(rbow,heat,terr,topo,cm,gry1,gry2))
R> axis(2,at=7:1,labels=c("rainbow","heat.colors","terrain.colors",
"topo.colors","cm.colors","gray.colors","gray"),
family="mono",las=1)
图 25-2 展示了结果。

图 25-2:展示了内置调色板的颜色范围,使用的是gray.colors的默认限制。
欲了解更多信息,请访问帮助文件?gray.colors和?gray,了解各自的灰度调色板,其它的则都在?rainbow中。
25.1.3 自定义调色板
你并不局限于使用现成的颜色设计。函数colorRampPalette允许你创建自己的调色板;你只需为同名参数提供两个或更多的期望关键颜色,它就会创建一个在这些颜色之间过渡的调色板。调用colorRampPalette的结果本身就是一个函数——其行为与前面提到的内置调色板函数完全相同。
假设你希望能够在紫色和黄色之间生成颜色。你需要指定要插值的关键颜色,并按所需的顺序将它们作为字符向量的名称,选择 R 所识别的颜色集合中的名称。以下代码行创建了这个调色板函数:
R> puryel.colors <- colorRampPalette(colors=c("purple","yellow"))
让我们再创建一个,这次选择一个颜色稍微清晰一些的调色板,以防使用它的颜色图打印为灰度图(在这种情况下,使用单色调色板是个好主意)。
R> blues <- colorRampPalette(colors=c("navyblue","lightblue"))
这里有更多的示例,这次使用了超过两种颜色:
R> fours <- colorRampPalette(colors=c("black","hotpink","seagreen4","tomato"))
R> patriot.colors <- colorRampPalette(colors=c("red","white","blue"))
创建了一些自定义调色板函数后,你现在可以像以前一样从每个范围中生成任意数量的颜色(这里使用之前存储的N值,即每个 600 个)。完成后,你可以调整之前的绘图代码,得到图 25-3 中的图像。
R> py <- puryel.colors(N)
R> bls <- blues(N)
R> frs <- fours(N)
R> pat <- patriot.colors(N)
R> dev.new(width=8,height=2)
R> par(mar=c(1,8,1,1))
R> plot(1,1,xlim=c(1,N),ylim=c(0.5,4.5),type="n",xaxt="n",yaxt="n",ann=FALSE)
R> points(rep(1:N,4),rep(4:1,each=N),pch=19,cex=3,col=c(py,bls,frs,pat))
R> axis(2,at=4:1,labels=c("peryel.colors","blues","fours","patriot.colors"),
family="mono",las=1)

图 25-3:一些使用colorRampPalette创建的自定义颜色调色板示例。
25.1.4 使用颜色调色板对连续值进行索引
你已经多次看到如何使用颜色来根据分类变量标识组(属于某一特定级别的数据简单地被赋予一个与其他数据不同的颜色),这其实很容易实现。然而,给连续值适当地分配颜色则需要更多的思考。为此有两种方法:通过分类或通过标准化连续值。我们首先来看第一种方法。
通过分类
根据连续变量为值着色的一种方法是将其转化为为分类变量着色的熟悉问题。你可以通过将连续值分箱为固定数量的k类,从你的调色板中生成k种颜色,并根据每个观测值所属的箱子匹配相应的颜色。
在第 20.1 节,你绘制了来自MASS包的survey数据的身高与书写手跨度之间的关系。这一次,我们使用颜色来额外表达非书写手跨度变量。加载该包并执行以下代码:
R> surv <- na.omit(survey[,c("Wr.Hnd","NW.Hnd","Height")])
这将创建数据框对象surv,它只包含三个必要的列。通过调用na.omit去除所有包含缺失值的行(参见第 6.1.3 节)。
现在,首先要做的是决定你的调色板。
R> NW.pal <- colorRampPalette(colors=c("red4","yellow2"))
这将生成从比例尺下端的深暗红色到上端稍微褪色的黄色的颜色(类似于内置的heat.colors调色板;见图 25-2)。接下来,你需要决定你将为连续值构建多少个箱子,k。这将决定从NW.pal生成多少种不同的颜色。对于这些数据,设置k = 5。
R> k <- 5
R> ryc <- NW.pal(k)
R> ryc
[1] "#8B0000" "#A33B00" "#BC7700" "#D5B200" "#EEEE00"
你的五种NW.pal颜色,作为十六进制代码,已经准备好。接下来,你需要实际对连续值进行分箱,可以使用cut来实现。首先,你需要设置k + 1 个分割点(参见第 4.3.3 节复习),使用seq。
R> NW.breaks <- seq(min(surv$NW.Hnd),max(surv$NW.Hnd),length=k+1)
R> NW.breaks
[1] 12.5 14.7 16.9 19.1 21.3 23.5
六个等距的值跨越学生的非书写手的跨度范围,划定了你的五个目标箱子。然后,cut根据这些箱子对非书写手的跨度进行因子化。你可以使用as.numeric特意返回索引,以从ryc中的五个有序十六进制代码提取每个观测值的适当颜色(由于打印原因,这里省略了完整输出)。
R> NW.fac <- cut(surv$NW.Hnd,breaks=NW.breaks,include.lowest=TRUE)
R> as.numeric(NW.fac)
[1] 3 4 3 4 3 3 3 4 3 3 2 4 3 3 4 4 4 5 4 3 4 4 5 4 3 3 4 4 2 3 5
[32] 3 2 3 4 1 3 5 5 3 3 5 4 3 4 5 3 2 3 4 5 3 4 3 3 4 3 3 3 4 2 3
[63] 2 3 3 3 3 4 3 5 3 3 3 --snip-
R> NW.cols <- ryc[as.numeric(NW.fac)]
R> NW.cols
[1] "#BC7700" "#D5B200" "#BC7700" "#D5B200" "#BC7700" "#BC7700"
[7] "#BC7700" "#D5B200" "#BC7700" "#BC7700" --snip--
你已经准备好绘图;以下结果见图 25-4 左侧:
R> plot(surv$Wr.Hnd,surv$Height,col=NW.cols,pch=19)

图 25-4:展示两种根据连续值为点着色的方法:通过分类(左)和通过归一化(右)
通过归一化
使用分类法通过颜色为连续值进行索引有点过于简单。你可以通过多种方式将观测值分箱,例如,你的图可能与另一个人设计的相同图表看起来完全不同。从计算的角度来看,保留你的连续数据不变是更准确(更优雅)的做法。
回想一下第 25.1.2 节中提到的内置gray调色板。这个函数的行为与其他函数略有不同。它不是简单地要求你从指定的调色板中选择颜色数量,而是要求你提供一个数值向量,以告知 R 在从 0 到 1 的连续范围内,调色板“走多远”。这种行为非常适合当前的任务,因为你的原始数据也是连续的。为了实现这一点,你需要两样东西:一种能够像gray一样行为的调色板,以及一个归一化后的连续值版本,这些值必须落在 0 到 1 的标准化范围内(包括 0 和 1)。
colorRamp函数允许你创建调色板,并且与colorRampPalette的用法相同,但返回的结果是一个颜色调色板函数,期望输入一个数值向量。你将很快看到这一点。为了将一组原始值 {x[1], ..., x[n]} 转换为,比如说, {z[1], ..., z[n]},其中 0 ≤ z[i] ≤ 1;i = 1, ..., n,你可以使用以下公式:

让我们通过在 R 编辑器中编写以下代码将其转化为一个 R 函数:
normalize <- function(datavec){
lo <- min(datavec,na.rm=TRUE)
up <- max(datavec,na.rm=TRUE)
datanorm <- (datavec-lo)/(up-lo)
return(datanorm)
}
基于 datavec 向量作为唯一参数,normalize 实现了公式 (25.1),使用可选的 na.rm 参数来确保 datavec 中的任何缺失值不会污染最小值和最大值的计算(见第 13.2.1 节)。
导入 normalize,并输入以下代码,它展示了原始的非书写手跨度值(来自你之前创建的 surv 对象)及其对应的归一化值(为了简洁,输出被省略):
R> surv$NW.Hnd
[1] 18.0 20.5 18.9 20.0 17.7 17.7 17.3 19.5 18.5 17.2 16.0 20.2
[13] 17.0 18.0 19.2 20.5 20.9 22.0 20.7 --snip-
R> normalize(surv$NW.Hnd)
[1] 0.50000000 0.72727273 0.58181818 0.68181818 0.47272727
[6] 0.47272727 0.43636364 0.63636364 --snip--
现在,你需要使用colorRamp创建一个新的颜色调色板版本。
R> NW.pal2 <- colorRamp(colors=c("red4","yellow2"))
根据归一化数据为每个观测值生成相应的颜色。
R> ryc2 <- NW.pal2(normalize(surv$NW.Hnd))
如果你实际查看ryc2中返回的对象,你会注意到它是一个 RGB 三元组矩阵,对应于你提供给colorRamp函数NW.pal2的每个归一化值(非整数值最终会被强制转换为整数)。在你将它们用于绘图之前,需要将这些值转换为十六进制代码。像在第 25.1.1 节中看到的那样,使用rgb函数,你将得到所需的向量(为打印简洁,已省略)。
R> NW.cols2 <- rgb(ryc2,maxColorValue=255)
R> NW.cols2
[1] "#BC7700" "#D3AD00" "#C48A00" "#CEA200" "#B97000" "#B97000"
[7] "#B66700" "#CA9700" "#C18100" "#B56500" --snip--
注意你在NW.cols2中获得的十六进制代码和在NW.cols中获得的十六进制代码之间的差异。在这里,你为每个独特的值获取一个十六进制代码,但对于分类后的NW.cols,你每个分箱只有一个十六进制代码(所以只有k = 5 种颜色)。
这一行生成了图 25-4 右侧的图像。
R> plot(surv$Wr.Hnd,surv$Height,col=NW.cols2,pch=19)
就这个相对简单的例子而言,两种方法之间的视觉差异很小,尽管仔细观察,你确实可以辨认出归一化版本中更平滑的颜色过渡。当你增加 k 值时,使用分类技术的视觉效果将越来越接近归一化方法。然而,通常应优先选择归一化方法,因为它更贴合你想要可视化的值的连续特性,并且对于分布偏斜的值或使用复杂色板时更为有效。
25.1.5 包含颜色图例
现在你可以在图形中有效使用颜色,你需要一个图例来参考颜色尺度。尽管使用基础 R 工具也可以创建图例,但使用 R 中的贡献功能通常会更简单。
一个有用的函数是 colorlegend 命令。这个函数位于 shape 包中(Soetaert, 2014),因此首先需要从 CRAN 下载并安装 shape 包。接下来的代码将加载该包,重新绘制最近的图形(基于之前创建的 surv 对象,并显示在图 25-4 的右侧),并绘制颜色条图例:
R> library("shape")
R> plot(surv$Wr.Hnd,surv$Height,col=NW.cols2,pch=19,
xlab="Writing handspan (cm)",ylab="Height (cm)")
R> colorlegend(NW.pal(200),zlim=range(surv$NW.Hnd),zval=seq(13,23,by=2),
posx=c(0.3,0.33),posy=c(0.5,0.9),main="Nonwriting handspan")
这个结果见于图 25-5 的左侧。
colorlegend 函数假设你已经在活动的图形设备中创建了一个图形,因此你需要先创建一个图形。你传递给 colorlegend 的第一个参数是你想要参考的值的颜色范围。使用像 ?rainbow 帮助文件中列出的颜色调色板函数或通过 colorRampPalette 创建的函数最为简便——换句话说,一个接受整数值的函数,告诉它需要生成多少种颜色。使用大量颜色时,可以得到平滑的色带,因此我使用 NW.pal(200)。接下来,使用 zlim 为 colorlegend 提供一个值的范围,这个值将被图例所参考,在这个例子中,是非写手手势的范围 range(surv$NW.Hnd)。zval 参数接受你希望在图例上标记的值。一个从 13 到 23,步长为 2 的序列被标记了出来。

图 25-5:使用来自贡献包 shape 的 colorlegend 函数实现颜色条图例的两个例子
颜色图例的位置和大小是通过posx和posy参数来控制的。这两个参数并非使用用户坐标,而是必须是一个长度为 2 的向量,在相对设备坐标下描述条形的水平(posx)和垂直(posy)长度。在这个例子中,posx=c(0.3,0.33)指示函数从设备左侧的 30%到 33%之间绘制图例的宽度,使得宽度占整个设备的 3%,并且位置靠左于中心。设置posy=c(0.5,0.9)表示你希望条形的长度覆盖设备的 40%,从底部 50%的位置到 90%之间。最后,你可以通过向main提供一个字符字符串为图例添加标题。
你可能需要通过试验和错误的方式来调整图例的位置、大小(以及使用zval设置适当的刻度标记)。posx和posy的设备特定性意味着如果你重新调整设备大小,可能需要重新评估这些参数的值。
如果你希望图例出现在默认绘图区域之外,可以很容易地在第一次调用plot时使用xlim参数来加宽绘图的横向大小,从而为绘制完整的图例腾出额外的空间。或者,你也可以通过调整图形或外边距的间距(参考第 23.2 节)来为图例腾出足够的空间,使其出现在绘图区域之外。以下代码段通过加宽右边距、重新绘制散点图,并将颜色图例插入到额外的空间中,完成了这一操作。
R> par(mar=c(5,4,4,6))
R> plot(surv$Wr.Hnd,surv$Height,col=NW.cols2,pch=19,
xlab="Writing handspan (cm)",ylab="Height (cm)")
R> colorlegend(NW.pal(200),zlim=range(surv$NW.Hnd),zval=13.5:22.5,digit=1,
posx=c(0.89,0.91),main="Nonwriting\nhandspan")
结果如图 25-5 右侧所示。图例比之前更窄,右侧仅占设备宽度的 2%,posx=c(0.89,0.91)。由于没有指定posy,colorlegend使用了默认值c(0.05,0.9),这使得颜色条几乎覆盖了设备的整个高度。新图例的刻度标记和标签现在以 1 为增量,范围从 13.5 到 22.5;请注意,要显示小数位数(也就是说,有效数字),你需要将digit参数从默认值0调整为其他值。在此,digit=1将刻度标签显示为一位小数。
你还可以控制这些图例的更多属性,包括标签样式和刻度标记的位置;有关详细信息,请参阅?colorlegend帮助文件。你也可以考虑查看贡献的plotrix包中同名的函数color.legend(Lemon, 2006),它提供了一种稍微不同的方法来绘制现有 R 图上的颜色图例。
25.1.6 不透明度
另一个有用的技能是能够指定迄今为止讨论的任何颜色和色板的透明度。所有提供十六进制代码的函数都有一个可选参数 alpha,其有效范围取决于函数(你可以快速查阅相关文档了解详细信息)。例如,rgb 函数使用 maxColorValue 来设定透明度的上限,而像 rainbow 这样的色板函数都使用 0 到 1 之间的标准化范围(就像在 第二十四章 中创建的 ggplot2 图表一样)。
默认情况下,R 在创建颜色时假定完全不透明。然而,当显式设置透明度时,十六进制代码会略有变化。此时,# 后面将不再是六个字符,而是八个字符,最后两个字符包含额外的透明度信息。考虑以下代码行,它们分别生成了四种不同版本的红色:默认、不透明度为零、40% 不透明度(0.4 × 255 = 102)和完全不透明:
R> rgb(cbind(255,0,0),maxColorValue=255)
[1] "#FF0000"
R> rgb(cbind(255,0,0),maxColorValue=255,alpha=0)
[1] "#FF000000"
R> rgb(cbind(255,0,0),maxColorValue=255,alpha=102)
[1] "#FF000066"
R> rgb(cbind(255,0,0),maxColorValue=255,alpha=255)
[1] "#FF0000FF"
请注意,第一种和最后一种颜色是相同的;只是最后一个十六进制代码明确指定了完全不透明。
你可以通过 alpha.f 参数(该参数的取值范围是 0 到 1)调整任何已有颜色的透明度,该参数属于随时可用的 adjustcolor 函数。以下代码将前一个示例中创建的默认红色十六进制代码转化为 40% 不透明的版本(即前面代码中的第三行):
R> adjustcolor(rgb(cbind(255,0,0),maxColorValue=255),alpha.f=0.4)
[1] "#FF000066"
你已经在 第 24.2.1 节 中简要接触过此命令,当时你使用基础 R 图形绘制了一个 LOESS 平滑趋势的透明灰色置信区间。该方法同样适用于使用内置或自定义色板函数生成的十六进制代码,以获得颜色向量。
你将通过内置的 quakes 数据框进行透明度测试,该数据框包含关于斐济附近 1,000 个地震事件的数据。我们来重新创建 图 13-6 中的图表,该图显示了“检测站数量”与“事件震级”之间的关系,并通过颜色来识别连续的“深度”数据。由于有许多重叠的观测点,因此降低每个点的透明度对可视化是一个不错的选择。代码如下:
R> keycols <- c("blue","red","yellow")
R> depth.pal <- colorRampPalette(keycols)
R> depth.pal2 <- colorRamp(keycols)
设置一个自定义的三色调色板,支持两种方式(换句话说,作为一个期望整数的函数 depth.pal 和作为一个期望介于 0 和 1 之间值的函数 depth.pal2;参考 第 25.1.3 节 和 25.1.4 节)。接下来,以下代码行使用标准化方法,通过在 第 25.1.4 节 中定义的 normalize 函数,根据数据集的“深度”变量获得适当的颜色,用于绘制图上的点:
R> depth.cols <- rgb(depth.pal2(normalize(quakes$depth)),maxColorValue=255,
alpha=0.6*255)
60% 不透明度的请求是通过调用 rgb 时的 alpha 参数来实现的。你可以使用以下调用来创建图表,该调用将颜色存储在 depth.cols 中:
R> plot(quakes$mag,quakes$stations,pch=19,cex=2,col=depth.cols,
xlab="Magnitude",ylab="No. of stations")
这个图表提供了另一个机会来展示 shape 包中的 colorlegend 函数。假设你已经在当前 R 会话中加载了 shape,接下来的代码行将在图表区域内绘制一个相应的颜色图例(在默认大小的设备上):
R> colorlegend(adjustcolor(depth.pal(20),alpha.f=0.6),
zlim=range(quakes$depth),zval=seq(100,600,100),
posx=c(0.3,0.32),posy=c(0.5,0.9),left=TRUE,main="Depth")
这里你可以看到另一个 adjustcolor 的使用示例,其中通过调用 depth.pal(20) 生成的颜色序列随后被减少到 60% 的不透明度,以匹配绘制的点。同样,posx 和 posy 用于定位图例,且可选的逻辑参数 left 被设置为 TRUE,使得刻度标记和颜色图例标签显示在条形的左侧。图 25-6 显示了最终结果。

图 25-6:在自定义调色板中改变颜色不透明度,用于在“站点数量”与“震级”图中索引连续的“事件深度”观察,并使用 shape 包中的 colorlegend 绘制相应的颜色图例
25.1.7 RGB 替代方案与进一步的功能
RGB 三元组并不是 R 中表示颜色的唯一方式。其他的表示方法包括色相-饱和度-明度(HSV)和色相-色度-亮度(HCL),这些可以通过内置的 hsv 和 hcl 函数来实现。这些方法的工作原理与 rgb 类似,你指定三种成分的影响强度,随后会得到相应的字符字符串十六进制代码,这些代码可以形成有效的 R 颜色,用于任何相关的绘图命令。实际上,HSV 参数化是内置调色板的实现方式,如 第 25.1.2 节中详细介绍的 rainbow 和 heat.colors。
贡献的功能提供了更大的灵活性。colorspace 包(Ihaka 等,2015)可以在不同的颜色格式之间转换,值得注意,RColorBrewer(Neuwirth, 2014)也值得一提,它直接基于 Cynthia Brewer 设计的广受好评的颜色方案(见 colorbrewer2.org/)。RColorBrewer 提供了比内置功能 colorRampPalette 和 colorRamp 更多的调色板选项。也就是说,从入门的角度来看,你会发现这里讨论的 RGB 和基础 R 功能对于大多数数据和模型的可视化探索来说已经足够。
练习 25.1
确保已加载car包。重新访问你在练习 24.1(第 622 页)和 24.2(第 628 页)中查看过的Salaries数据框,并查看帮助文件?Salaries以提醒自己有关变量的信息。你的任务是使用颜色、点大小、透明度和点字符类型,在“薪水”与“服务年限”的散点图中,反映“获得博士学位的年数”、“性别”和“职级”,完成以下步骤:
-
设置一个自定义色板,从
"black"到"red"再到"yellow2"。创建两个版本的色板——一个期望多个颜色,另一个期望一个在 0 和 1 之间的标准化值向量。 -
创建两个向量,用来控制点字符和字符扩展,遵循(i)和(ii)的指南。每个向量都可以通过根据数据框中相应因子向量的数值强制转换来进行子集/重复操作,在一行中完成。
-
使用点字符
19、17和15依次引用三个递增的学术职级。 -
为女性使用字符扩展
1,为男性使用字符扩展1.5。
-
-
使用第 25.1.4 节中定义的
normalize函数,创建一个“获得博士学位年数”变量的[0,1]标准化版本。然后,使用(a)中的适当色板,并结合rgb将这些值转换为所需的十六进制代码。 -
修改你在(c)中创建的颜色向量,调整透明度。与女性对应的颜色透明度应减少到 90%;与男性对应的颜色透明度应减少到 30%。
-
现在,开始绘制图形;将默认的图形边距调整为底部 4 行、左侧 5 行、顶部 4 行和右侧 6 行。将薪水绘制在y-轴上,将服务年限绘制在x-轴上。根据你在(d)中创建的向量设置相应的点颜色,并根据你在(b)中创建的向量设置点字符和字符扩展。整理好x-轴和y-轴的标题。
-
按照(i)和(ii)的指南,合并两个单独的图例。两个图例应该是水平放置的,且应放松裁剪以允许它们放置在图形边距中(参见第 23.2.3 节)。
-
将第一个图例放置在用户坐标
x=-5和y=265000的位置。它应使用“职级”因子向量的各个水平作为参考文本,并将这些文本与对应的pch符号进行配对。为图例添加一个合适的标题。 -
第二个图例应放置在第一个图例旁边,使用
40的x-坐标和相同的y-坐标值。该图例应显示两个点,都是红色的,类型为19,但通过改变字符扩展和透明度来引用性别的两个水平。
-
-
最后,确保
shape包已加载,并使用colorlegend函数配合从(a)中选出的适当调色板生成的 50 种颜色,来表示“自博士毕业以来的年数”。你可以将图例的水平和垂直位置保持为默认值。zlim范围应设置为与观察数据的范围一致,而通过zval设置的刻度值应为 10 到 50 之间的序列,每次增加 10。为颜色图例添加一个合适的标题。完成这些后,我的版本如下所示:
![image]()
你的下一个任务有些不同。目标是绘制标准正态概率密度函数,但使用颜色对曲线下方的多边形进行着色,表示“与均值的距离”。为此,完成以下任务:
-
从内置调色板
terrain.colors生成一个恰好包含 25 种颜色的向量,并命名为tcols。然后,使用tcols[25:1]得到它的反转版本,将这两个向量拼接在一起,形成一个新的长度为 50 的向量,其中前 25 种颜色按一种方式着色,接着这 25 种颜色按相反的方式着色。 -
接下来,创建并存储一个精确的 51 个值的均匀间隔序列,范围从−3 到 3(包括 3);命名为
vals。使用dnorm计算并存储标准正态密度曲线的对应 51 个值;命名为normvals。 -
通过绘制(i)中的值作为一条线来绘制标准正态密度曲线(记得
type="l")。在同一次调用plot中,使用第二十三章中的知识,将X轴和Y轴的样式设置为"i"类型;用空字符串隐藏两个轴的标题;将四周的框改为L形状;并禁止绘制X轴。给图形添加一个合适的主标题。 -
要在曲线下方着色不同的颜色,使用
for循环,遍历 1 到 50 的整数。在每次迭代中,循环应调用polygon(参考第 15.2.3 节)。假设你的索引变量是i,则每个多边形的顶点应由向量vals[rep(c(i,i+1),each=2)]和c(0,normvals[c(i,i+1)],0)构成。每个多边形应隐藏其边框,并根据在(h)中创建的长度为 50 的颜色向量的相应i项着色。 -
最后,确保
shape包已经加载,并使用你长度为 50 的颜色向量生成一个默认位置的颜色图例,用来表示“与均值的距离”。你可以通过vals轻松设置调用colorlegend时的zlim和zval参数。为图例添加一个合适的标题。我的结果如下所示:

25.2 三维散点图
本节将介绍如何创建 3D 散点图,这使你可以基于三个连续变量一次性绘制原始观察数据,而不仅仅是传统 2D 散点图中的两个变量。接着,你将学习如何增强你的 3D 散点图,以表示更多的变量并使其更易于解读。有多种方法可以在 R 中创建三变量散点图,但通常采用的方式是使用同名的贡献包中的 scatterplot3d 函数(Ligges 和 Mächler, 2003)。
25.2.1 基本语法
scatterplot3d 函数的语法类似于默认的 plot 函数。在后者中,你提供 x 和 y 轴坐标的向量;在前者中,你仅需提供额外的第三个向量,提供 z 轴坐标。通过这个额外的维度,你可以将这三个坐标轴视为:x 轴从左到右增加,y 轴从前景到背景增加,z 轴从下到上增加。
安装并加载 scatterplot3d 包,我们直接进入一个示例。回想一下著名的鸢尾花数据,你第一次在第 14.4 节中接触到该数据集。这个数据集包含了四个连续变量的测量值(花瓣长度/宽度和萼片长度/宽度)以及一个分类变量(花卉物种);iris 数据框可以直接从 R 提示符获取,因此无需加载任何内容。输入以下命令,以便快速访问构成数据的测量值:
R> pwid <- iris$Petal.Width
R> plen <- iris$Petal.Length
R> swid <- iris$Sepal.Width
R> slen <- iris$Sepal.Length
基本的 3D 散点图,例如花瓣长度、花瓣宽度和萼片宽度,可以通过以下代码实现:
R> library("scatterplot3d")
R> scatterplot3d(x=pwid,y=plen,z=swid)
就这么简单——此代码的结果如图 25-7 左侧所示。在这里,你可以观察到所有三个绘制变量之间的一般正相关关系。前景中还有一簇明显孤立的观察值,它们具有相对较大的萼片宽度,但花瓣测量值较小。

图 25-7:两种 3D 散点图,展示了著名的 iris 数据,花瓣宽度、花瓣长度和萼片宽度分别在 x、 y* 和* z* 轴上。左:基本的默认外观。右:整理标题并增加视觉增强,以通过颜色和垂直线条标记来强调 3D 深度和可读性。*
25.2.2 可视化增强
即便有默认绘制的盒子和 x-y 平面网格线,仍然很难清晰地感知图中点云的深度。因此,你可以对 scatterplot3d 图表进行几个可选的增强—通过给点上色来帮助更清晰地表达前景到背景的过渡,并通过设置 type="h" 参数来绘制垂直于 x-y 平面的线条。
图 25-7 中的右侧图显示了这些增强效果的图表,并且是以下操作的结果:
R> scatterplot3d(x=pwid,y=plen,z=swid,highlight.3d=TRUE,type="h",
lty.hplot=2,lty.hide=3,xlab="Petal width",
ylab="Petal length",zlab="Sepal width",
main="Iris Flower Measurements")
xlab、ylab、zlab和main控制三个坐标轴和图形本身的标题。
垂直线使得读取点的数值更加容易。默认情况下,在type="h"图中,这些线是实线,但你可以通过lty.hplot参数来修改(它的行为与标准图形参数lty相同);设置lty.hplot=2会要求使用虚线。同样,你也可以修改“不可见”边框的线型;设置lty.hide=3会要求图表将这些线绘制为点线。
设置highlight.3d=TRUE通过根据点在y轴的位置应用从红色到黑色的颜色过渡来强调 3D 深度。这是有用的,但也有一个重要的后果——这意味着你不能再使用颜色来表示图表中的第四个变量。
按照这个思路,请记住iris数据集还包含一个第四个连续变量,即花萼长度(在第 25.2.1 节中存储为slen),但你在图 25-7 中的任何图表里都没有展示它。你也没有展示花卉物种的分类变量,所以我们来解决这个问题。首先,为缺失的测量变量设置一个颜色带,利用你在第 25.1.4 节中了解到的,通过颜色调色板引用一个连续变量的知识。
R> keycols <- c("purple","yellow2","blue")
R> slen.pal <- colorRampPalette(keycols)
R> slen.pal2 <- colorRamp(keycols)
R> slen.cols <- rgb(slen.pal2(normalize(slen)),maxColorValue=255)
请注意,要运行最后一行代码,你需要在当前会话中定义第 25.1.4 节中的normalize函数。
以下代码生成了 3D 散点图,并且也使用了pch参数来区分三种不同的物种:
R> scatterplot3d(x=pwid,y=plen,z=swid,color=slen.cols,
pch=c(19,17,15)[as.numeric(iris$Species)],type="h",
lty.hplot=2,lty.hide=3,xlab="Petal width",
ylab="Petal length",zlab="Sepal width",
main="Iris Flower Measurements")
我使用了向量c(19,17,15),并将iris$Species向量强制转换为数字后传递给方括号,以将pch字符编号与如下内容配对:19对应Iris setosa(因子中的第一个水平),17对应Iris versicolor(第二个水平),15对应Iris virginica(第三个水平)(请参见图 7-5 以及第 133 页了解不同的点字符类型)。
然后,你可以插入一个参考物种的图例,使用常见的legend调用。
R> legend("bottomright",legend=levels(iris$Species),pch=c(19,17,15))
通过一些实验,你还可以包含一个颜色条图例(确保你已经加载了shape包,以便可以使用colorlegend函数,参考第 25.1.4 节)。
R> colorlegend(slen.pal(200),zlim=range(slen),zval=5:7,digit=1,
posx=c(0.1,0.13),posy=c(0.7,0.9),left=TRUE,
main="Sepal length")
所有这些的最终结果是图 25-8 中的图像。

图 25-8:一个著名的iris数据集的 3D 散点图,展示了所有五个变量,并额外使用了颜色(表示花萼长度)和点的形态(表示物种)。
通过创意地使用颜色和点类型,您现在能够在一个单一的 3D 散点图中展示五维数据。这揭示了关于数据的重要信息。例如,您现在可以识别出Iris setosa作为前景中明显分离的点组,并看到虽然Iris setosa的花瓣宽度和长度较小,且比其他两种物种(特别是Iris versicolor)的萼片宽度要大,但位于量表下端的紫色着色表明它们的萼片长度较小。
练习 25.2
确保scatterplot3d库已在您当前的 R 会话中加载。
-
回到
faraway包中的diabetes数据框(您第一次在第 21.5.2 节中查看了这些数据)。您的目标是根据以下指南,生成一个包含体重、髋部和腰围测量值的scatterplot3d图:– 髋部、腰围和体重变量应分别对应于x轴、y轴和z轴;提供整洁的轴标题。
– 使用内置功能,确保通过颜色突出显示 3D 深度。
– 选择两种不同的点字符来反映性别。
– 在左上角的空白区域放置一个简单的图例,引用这两种点字符和性别。
-
根据以下指南,创建一个内置的
airquality数据集的 3D 散点图,您第一次在第 24.2.2 节中遇到该数据集:– 使用
na.omit创建数据框的副本,移除所有包含缺失值的行,并使用该副本进行操作。– 将风速和太阳辐射分别绘制在x轴和y轴上,使用z轴绘制温度。
– 在x–y平面上应用垂直虚线,延伸至每个观测点。
–
airquality数据包含了从 5 月到 9 月五个月内的测量数据。每个绘制的点应根据这五个月的顺序,分别取pch值从1到5。– 使用从内置的
topo.colors调色板生成的 50 种颜色的向量,使用分类方法确保每个绘制的点根据其臭氧值着色。– 设置一个图例,引用根据月份的五种点类型。
– 设置一个颜色图例(使用
shape包中的功能),根据臭氧值进行引用。– 确保图表有整洁的轴、主标题和图例标题。
25.3 为绘图准备表面
在本章的其余部分,你将看到三种类型的 3D 绘图,用于可视化一个双变量表面。当你有两个变量,并且基于这两个变量定义了一个函数、估计或模型时,这种图形是必需的,并且你希望使用第三个可用轴(换句话说,就是z轴)来描绘出结果的表面。你已经通过在第 21.5.4 节中查看mtcars数据的响应面(在那里你看到了车重和马力作为自变量时,平均油耗的变化)以及在第 22.3.6 节中研究线性回归模型的诊断工具(在那里你看到了如何将库克距离表示为残差和杠杆的函数)看过双变量函数的例子。
在你开始绘制这些图形之前,理解它们是如何在 R 中创建的非常重要。你关注的函数/估计/模型应该被视为一个平面或表面,可以根据连续的二维x-y坐标发生变化。绘制一个完全连续的表面在技术上是不可能的,因为那需要你在无限多个坐标上评估函数。因此,表面的评估通常是在沿x轴和y轴均匀间隔的有限网格坐标上进行的。每个唯一坐标对下的函数结果被存储在适当大小的矩阵中的相应位置(其大小直接取决于在x和y轴上评估网格的分辨率),通常被称为z 矩阵。
由于所有传统的 R 图形命令在绘制这些双变量函数时都以相同的方式操作——使用这个z矩阵——因此理解这个矩阵是如何构建、排列和由这些命令解释的至关重要,以确保你能正确地绘制结果。在本节中,你将通过在一个假设情境中熟悉这一构造,为本章其余部分中讨论的特定绘图类型做好准备。
25.3.1 构建评估网格
假设你有一个双变量函数,它生成一个定义在x轴的 1 到 6 之间和y轴的 1 到 4 之间的连续表面。你可以使用seq在这些坐标范围内定义均匀间隔的序列;为了简化,我们就直接用整数来定义。
R> xcoords <- 1:6
R> xcoords
[1] 1 2 3 4 5 6
R> ycoords <- 1:4
R> ycoords
[1] 1 2 3 4
这意味着你计划基于在由 24 个唯一位置定义的x-y值网格上评估感兴趣的双变量函数来绘制你的表面。
当传入两个向量时,内置的expand.grid函数通过简单地将第二个向量中的每个值与第一个向量的整个长度重复,显式生成所有唯一的坐标对。
R> xycoords <- expand.grid(x=xcoords,y=ycoords)
结果以一个两列的数据框存储,共有 24 行。如果您查看 R 控制台中的xycoords对象,您会看到x值从1到6与重复的y值1配对,然后是x从1到6与y值2配对,依此类推。
实际操作中,您现在需要使用xycoords中的评估网格坐标来计算双变量函数的结果。在这个假设的例子中,假设您的双变量函数生成了从a到x的 24 个字母,分别对应于xycoords中唯一评估坐标的顺序。为了更清晰地说明这一点,看看下面将假设函数结果与每个评估坐标列绑定的示例(请注意,R 中的现成letters对象可以快速生成字母):
R> z <- letters[1:24]
R> cbind(xycoords,z)
x y z
1 1 1 a
2 2 1 b
3 3 1 c
4 4 1 d
--snip--
21 3 4 u
22 4 4 v
23 5 4 w
24 6 4 x
这强调了每个唯一的x–y评估坐标,通过expand.grid表示,都将有一个与之相关联的z值。所有这些z值一起定义了结果的曲面。
25.3.2 构建 z 矩阵
用于可视化双变量函数的 3D 图需要将对应于x–y评估网格的z值呈现为适当构建的矩阵。z矩阵的大小直接由评估网格的分辨率决定;行数对应于唯一的x网格值的数量,列数对应于唯一的y网格值的数量。
因此,您需要小心将计算得到的z值转换为矩阵。当您的z轴向量与标准expand.grid方式排列的评估网格相对应时(换句话说,坐标按增加的x值和重复的y值堆叠),确保您的结果z矩阵以默认的列优先方式填充(参见第 3.1.1 节),行和列的数量应该准确代表每个x和y值序列中的值的数量(分别是之前显示的xcoords和ycoords)。在当前的例子中,您知道结果z矩阵的大小需要是 6 × 4,因为有六个x位置和四个y位置。
以下是假设“函数结果”向量z的正确矩阵表示:
R> nx <- length(xcoords)
R> ny <- length(ycoords)
R> zmat <- matrix(z,nrow=nx,ncol=ny)
R> zmat
[,1] [,2] [,3] [,4]
[1,] "a" "g" "m" "s"
[2,] "b" "h" "n" "t"
[3,] "c" "i" "o" "u"
[4,] "d" "j" "p" "v"
[5,] "e" "k" "q" "w"
[6,] "f" "l" "r" "x"
25.3.3 构思 z 矩阵
本节最重要的收获是了解当前排列的z矩阵如何转换为基于x–y坐标的绘图。将zmat与早期的输出进行比较,您可以看到,沿着zmat的列向下移动,意味着对于给定的y坐标值,x坐标值在增加。换句话说,当这个假设的字母曲面被绘制时,沿着矩阵的一列向下移动对应于在给定的垂直y位置下,从左到右水平移动。
图 25-9 提供了这个示例表面的概念图,按zmat索引,基于xcoords和ycoords定义的 24 个唯一坐标。(生成此图形的代码包含在本书的 R 脚本文件中,可以在www.nostarch.com/bookofr/找到。)

图 25-9:绘制双变量函数的z矩阵概念图,基于 6×4 坐标网格
当你开始绘制实际的感兴趣表面时,应牢记图 25-9 中所示的z矩阵的概念。这个假设示例中使用的 6 × 4 网格较为粗糙。实际上,为了提高表面的视觉效果,你通常会使用更精细的网格,在x和y序列的分辨率上有所改进。
25.4 等高线图
显示基于二维坐标网格评估函数的表面的最常见图形之一是等高线图。等高线图可以通过一系列线条——即等高线——在二维评估网格上绘制来解释,每条等高线表示感兴趣表面上的特定水平。
25.4.1 绘制等高线
基于给定的数字z矩阵,R 函数contour用于生成连接具有相同z值的x-y坐标的等高线。
示例 1:地形图
作为示例,你将使用另一个现成的数据集——volcano对象。这个数据集是一个矩阵,包含新西兰奥克兰地区一座休眠火山上空的海拔(单位:米);有关详细信息,请参阅?volcano文档。要查看地形,你需要volcano对象(它是你的z矩阵)和相关的x和y坐标序列。在这种情况下,只需使用与volcano矩阵大小相对应的整数(可以通过调用dim轻松获取行和列的数量;请参见第 3.1.3 节)。
R> dim(volcano)
[1] 87 61
R> contour(x=1:nrow(volcano),y=1:ncol(volcano),z=volcano,asp=1)
x和y序列分别提供给x和y,而z矩阵提供给z。可选参数asp=1,指的是图形的纵横比,它强制坐标轴以 1:1 的比例处理(当单位具有物理大小解释时,比如在地理区域的绘图中,这一点非常重要——如这里的情况)。
图 25-10 展示了此示例的结果。默认情况下,R 会自动选择绘制等高线的z值,以获得视觉上美观的效果。等高线也会根据其对应的z值选择性地进行标注。观察地形,可以看到最高点位于左侧的边缘,190 米的椭圆形等高线标记着这个位置,右侧则有一个大约 160 米的凹陷。

图 25-10:使用 contour 绘制 volcano 数据的地形图
等高线不仅能显示表面上的峰值和谷值,还能显示任何此类特征的“陡峭度”。等高线之间越紧密,双变量函数的整体水平变化越快速。
示例 2:参数响应面
作为另一个例子,考虑前面提到的对mtcars数据拟合的多元线性模型——即由马力、重量及其相互作用预测的每加仑英里数(MPG)。如同在第 21.5.4 节中所示,你可以使用以下代码获取拟合的模型对象:
R> car.fit <- lm(mpg~hp*wt,data=mtcars)
R> car.fit
Call:
lm(formula = mpg ~ hp * wt, data = mtcars)
Coefficients:
(Intercept) hp wt hp:wt
49.80842 -0.12010 -8.21662 0.02785
目标是绘制响应,即平均油耗,作为前面所提到的马力和重量的函数。为此,你需要根据前面的模型评估马力和重量值的网格上的平均 MPG。以下代码正是执行这一操作。
R> len <- 20
R> hp.seq <- seq(min(mtcars$hp),max(mtcars$hp),length=len)
R> wt.seq <- seq(min(mtcars$wt),max(mtcars$wt),length=len)
R> hp.wt <- expand.grid(hp=hp.seq,wt=wt.seq)
R> nrow(hp.wt)
[1] 400
R> hp.wt[1:5,]
hp wt
1 52.00000 1.513
2 66.89474 1.513
3 81.78947 1.513
4 96.68421 1.513
5 111.57895 1.513
首先,这段代码在hp和wt两个变量上设置了均匀间隔的序列(每个序列长度为 20,涵盖观察数据的范围)——这就是你的x-和y-序列。这意味着将在 20 × 20 = 400 个独特的坐标点上评估拟合模型;这些坐标通过expand.grid获取,正如第 25.3 节中所述。
接下来,你可以使用predict获取对应的 400 个平均 MPG(z)值;由于它已经是所需格式的数据框,hp.wt可以直接传递给newdata参数。
R> car.pred <- predict(car.fit,newdata=hp.wt)
然后,你只需将结果向量排列为适当的 20 × 20 z矩阵。
R> car.pred.mat <- matrix(car.pred,nrow=len,ncol=len)
最后,你将结果绘制为等高线,如在图 25-11 中所示。
R> contour(x=hp.seq,y=wt.seq,z=car.pred.mat,levels=32:8,lty=2,lwd=1.5,
xaxs="i",yaxs="i",xlab="Horsepower",ylab="Weight",
main="Mean MPG model")
在此调用中,你可以看到可选的levels参数的使用。与让 R 自动决定在哪些z值上显示等高线不同,你可以向该参数提供一个数值向量,指定绘制等高线的具体水平。这个数值向量必须与结果的双变量函数在相同的尺度上;在这里,我要求在 32 到 8 的所有整数水平上绘制等高线。我还使用了常见的参数lty和lwd来控制等高线的外观,这里设置为虚线且比通常的略粗。
此外,特别是对于等高线图,你通常需要偏离默认的坐标轴限制样式,因为默认图形区域中包含的少量额外“填充”空间(参见第 23.4.1 节)可能显得较为突出——再看一眼图 25-10 中的火山等高线图。如前所示,设置xaxs和yaxs为"i"将限制所有绘图在x和y强加的精确限制内。

图 25-11:基于由马力和重量建模的多元线性模型的响应面等高线,数据来自 mtcars
示例 3:非参数二元密度估计(地震数据)
等高线图和本章中的其他图表还履行了一个有用的角色,即可视化二元密度函数。
在第 24.2.2 节中,您了解了核密度估计(KDE)作为一种方法,通过它可以构建数据的平滑概率密度函数估计——本质上是复杂的直方图。KDE 自然地扩展到更高维度,因此您也可以估计x-y平面中的二元观测密度。这同样涉及在固定坐标网格上可视化一个z矩阵。有关多元 KDE 的理论细节,请参阅 Wand 和 Jones(1995)。
将注意力转回到内置的quakes数据框,并回想一下 1,000 个地震事件的空间坐标图(例如,图 13-1 在第 265 页,和图 23-1 在第 578 页)。为了估计这些点的概率密度函数,您可以使用MASS包中的kde2d函数。加载MASS并执行以下代码来生成观测二维数据的核密度估计:
R> quak.dens <- kde2d(x=quakes$long,y=quakes$lat,n=100)
您将二元数据作为x和y参数提供,分别对应水平和垂直轴。可选参数n用于指定在两个轴上的评估坐标数(即实际返回估计的密度曲面的点数)。这定义了通过调用kde2d返回的矩阵大小。在这里,您要求在观测数据的范围内,沿 100 × 100 均匀间隔的网格执行 KDE。
生成的对象仅仅是一个包含三个成员的列表。通过$x和$y访问的组件包含相应轴方向上均匀间隔的评估网格坐标,$z则提供对应的z矩阵。您可以通过在命令行输入quak.dens$x或quak.dens$y来确认它们确实是增加的序列,涵盖了观测数据的范围。输入以下内容可以确认感兴趣矩阵的大小:
R> dim(quak.dens$z)
[1] 100 100
有了这些,您就拥有了显示 KDE 曲面等高线所需的所有元素。下一行代码生成默认的等高线图,如图 25-12 左上角所示。
R> contour(quak.dens$x,quak.dens$y,quak.dens$z)
contour函数有许多更多的可选参数,用于显示您的连续曲面。它也可能有助于同时查看其他数据或原始观测数据(如果它们已以某种方式被用于创建曲面,就像二元 KDE 中的情况一样)。以下代码重新绘制了quakes的核密度估计,使用未加填充的轴、与默认值不同的等高线级别和原始观测数据;您可以在图 25-12 的右上角看到结果:
R> contour(quak.dens$x,quak.dens$y,quak.dens$z,nlevels=50,drawlabels=FALSE,
xaxs="i",yaxs="i",xlab="Longitude",ylab="Latitude")
R> points(quakes$long,quakes$lat,cex=0.7)
与使用levels来确定绘制轮廓的精确等级(如示例 2 中所做)不同,你可以使用nlevels参数来指定要显示的等级数,函数将自动选择具体的值。在此调用contour时,要求绘制 50 个等级。你可以通过设置drawlabels=FALSE来抑制自动标签的显示,接着调用points将原始观测值添加到图像中。显然,平滑的轮廓线描绘了非参数密度估计,反映了数据的异质空间模式。
更改绘制轮廓的外观不一定要全局进行;你也可以单独更改每个轮廓等级的外观。例如,如果你只想在几个特定的等级上显示轮廓而不显示默认标签(以便专注于表面的形状),但仍希望能够辨认出这些轮廓的值,这时这种方式就非常有用。你也可能想要将轮廓叠加到一个已经绘制其他数据或基于模型的结果的现有图形上。图 25-12 底部的第三个地震 KDE 表面图展示了如何同时实现这两种需求。

图 25-12:地震位置空间的双变量核密度估计概率密度函数的轮廓图示例,数据来自quakes数据集
为了开始绘制图形,使用plot将地震数据的空间位置绘制为半大小的灰色点,轴的样式通过xaxs和yaxs进行设置,以去除图形区域边缘的人工填充,并添加轴标题。
R> plot(quakes$long,quakes$lat,cex=0.5,col="gray",xaxs="i",yaxs="i",
xlab="Longitude",ylab="Latitude")
然后,在调用contour之前,将要绘制轮廓的期望等级存储在一个名为quak.levs的向量中(再次强调,选择适当的轮廓等级完全取决于你绘制的表面类型;你至少需要大致了解相关的z矩阵中存储的值)。
R> quak.levs <- c(0.001,0.005,0.01,0.015)
现在,请记住,默认情况下,contour 会刷新图形设备并开始一个新的绘图,但当你向现有图形添加等高线时,你需要避免这种情况。为此,你需要明确指定add=TRUE。然后,将 quak.levs 中指定的四个水平传递给 levels,并通过 drawlabels=FALSE 来抑制标签。为了控制各个水平上等高线的外观,你需要将整数序列 4:1 提供给 lty,其中第一个条目 4 定义了 z = 0.001 时等高线的线型。第二个条目 3 指定了 z = 0.005 等高线的线型,依此类推。最后,通过单个提供的值 lwd=2 将所有绘制的等高线设置为双重粗细。(如果你希望不同的等高线有不同的线粗,也可以在此提供一个包含四个元素的向量。对于其他相关的美学,如通过 col 控制颜色,同样可以按元素指定等高线的参数。)
R> contour(quak.dens$x,quak.dens$y,quak.dens$z,add=TRUE,levels=quak.levs,
drawlabels=FALSE,lty=4:1,lwd=2)
最后,由于在 contour 中抑制了自动标签,因此在图形区域的左下角添加一个图例,通过四种不同的线型来引用等高线的值。
R> legend("bottomleft",legend=quak.levs,lty=4:1,lwd=2,
title="Kernel estimate (contours)")
注意
许多内建的和贡献的基本 R 绘图函数,默认情况下会初始化、刷新或打开一个新图形,都包含一个 add 参数,如这里所示。这使得你可以将这些函数生成的图形作为现有图形的附加部分来使用。查看相关的帮助文件,看看某个特定命令是否适用。
25.4.2 彩色填充等高线
对于等高线图的简单变体,你可以使用颜色来填充不同水平之间的空隙。结合颜色图例,这样就无需标记等高线,在某些情况下,这可以更容易地直观地解释绘制的 z-矩阵表面中的波动。
filled.contour 函数为你完成了这一切。你需要将 x 轴和 y 轴方向上逐渐增大的网格坐标序列以及相应的 z 矩阵提供给 x、y 和 z 参数,方法与 contour 中相同。指定颜色的最简单方法是将颜色调色板传递给 color.palette 参数(默认为内建的 cm.colors 调色板;请参见 图 25-2),其余的由 R 自动完成。
让我们使用示例 2 中的 mtcars 响应面来快速演示。如果你的工作空间中尚未有相关数据,请使用 第 25.4.1 节中的代码来获取相关的拟合多元线性回归模型、评估网格坐标以及相应的预测结果。在之前定义的 hp.seq、wt.seq 和 car.pred.mat 对象基础上,以下调用将生成 图 25-13:
R> filled.contour(x=hp.seq,y=wt.seq,z=car.pred.mat,
color.palette=colorRampPalette(c("white","red4")),
xlab="Horsepower",ylab="Weight",
key.title=title(main="Mean MPG",cex.main=0.8))

图 25-13:mtcars 数据集的拟合多元线性模型响应面的填充等高线图
请注意,这个图中没有使用默认的颜色调色板。相反,你为相关参数提供了一个自定义的调色板(这是通过适当调用colorRampPalette直接生成的;参考第 25.1.3 节),从下端的白色到上端的深红色。同时注意,尽管X轴和Y轴的标题像往常一样通过xlab和ylab提供,但你必须以特定的方式为颜色图例提供标题—通过在title中调用key.title参数来实现。这是因为filled.contour实际上生成了两个图,一个是图像本身,另一个是颜色图例,并使用layout命令将它们并排放置。
这种对layout的内部使用并不直接构成问题,但正如你在第 23.1.4 节中看到的,如果你想在事后为填充等高线图添加注释(例如,向现有图形中添加点),会有些复杂,因为原始用户坐标系丢失了。
回到空间quakes数据的二维核估计(如果你还没有在工作区中获得quak.dens对象,可以使用第 25.4.1 节中的代码重新创建)。以下代码使用内置的topo.colors调色板创建了密度曲面的填充等高线图,并将绘制的级别数从默认的 20 个改为 30 个。在同一个调用中,通过特别使用可选的plot.axes参数,你还可以将原始观测数据点叠加到图像上。图 25-14 显示了结果。
R> filled.contour(x=quak.dens$x,y=quak.dens$y,z=quak.dens$z,
color.palette=topo.colors,nlevels=30,xlab="Longitude",
ylab="Latitude",key.title=title(main="KDE",cex.main=0.8),
plot.axes={axis(1);axis(2);
points(quakes$long,quakes$lat,cex=0.5,
col=adjustcolor("black",alpha=0.3))})

图 25-14:空间 quakes 数据的概率密度函数核估计的填充等高线图,原始观测数据叠加其上。
看看plot.axes的使用方式;它实际上需要一段代码。当调用plot.axes时,如果你希望标记的刻度线保持显示,必须明确告诉它标记X轴和Y轴。这可以通过两次调用axis来完成(参考第 23.4.3 节—axis(1)表示X轴,axis(2)用于Y轴)。你可以通过调用points来添加数据点;在这个例子中,这些点被指示以半尺寸绘制,使用adjustcolor设置了 30%的透明度。由于你同时向plot.axes参数提供多个单独的命令,每个命令需要用分号(;)在大括号({ })内分隔。
以这种方式注释填充等高线图需要更多的预先思考,因为你需要通过调用axis手动添加坐标轴,并在调用filled.contour时执行所有随后想要的绘图操作。例如,不能像生成quakes的 KDE 表面那样生成一个填充的等高线图,然后再将points作为单独的代码行调用。如果你这样做,你会发现观察到的数据点无法正确对齐到它们在坐标轴上原始的用户坐标。
练习 25.3
记住,你曾经检查过关于核电厂建设成本的多个线性回归模型,详见第二十一章和第二十二章。现在的目标是通过等高线图视觉评估包括/排除两个连续预测变量之间的交互项的影响。重新访问nuclear数据集,该数据集在加载boot包时可用,并查看帮助文件以刷新你对其中变量的记忆。
-
根据以下指导方针,拟合并总结两个线性模型,将建设成本作为响应变量:
-
第一个应考虑两个预测变量关于建设许可证发布日期和工厂容量的主要效应。
-
第二个模型,除了两个主要效应外,应包括许可证发布日期和容量之间的交互项。
-
-
为每个响应面设置适当的* z *矩阵进行绘图。每个矩阵应基于一个 50 × 50 的评估网格,该网格通过在容量和日期变量中使用均匀间隔的序列构建。
-
在
par中指定mfrow,以便你可以将(a)(i)和(a)(ii)中的两个响应面显示在一起。它们看起来相似吗?这与(a)(ii)中的交互项的统计显著性(或缺乏显著性)有关系吗? -
为了直接比较这两个表面,使用你选择的内置颜色调色板,生成仅包含主要效应的模型的填充等高线图,并将交互式模型的等高线叠加在其上。注意以下几点:
– 这个图是通过一次
filled.contour调用实现的。回忆一下你是如何使用plot.axes在现有的颜色填充等高线图上绘制附加特征的特殊方法。– 交互式模型的等高线可以通过适当的
contour调用添加。回忆一下可选参数add的使用。– 重叠的等高线应为双倍厚度的虚线。
– 应包含* x 轴和 y *轴,并为其提供整洁的标题。
– 添加一些简短的文本,描述填充等高线与等高线的区别,并参考建设成本模型的两个版本,同时通过额外调用
text来使用来自locator的单击位置(参见第 23.3 节)。请注意,这个调用需要完全放松剪辑,以便文本能够在任何边缘区域显示。我的结果如下所示。
![image]()
-
R 中的另一个内置数据框
faithful包含黄石国家公园老忠实喷泉的等待时间和喷发持续时间的观测数据。有关详细信息,请参见?faithful中的文档。使用持续时间作为* y 轴,等待时间作为 x *轴绘制数据。 -
通过 KDE 估算这些数据的双变量密度,使用 100 × 100 的评估网格,并生成该密度的默认等高线图。
-
使用从
"darkblue"到"hotpink"的自定义调色板,绘制核密度估计的填充等高线图;将原始数据作为半尺寸的灰色点添加。适当标注坐标轴和标题。 -
将原始数据重新绘制为灰色的 3/4 大小,
2号点字符;设置坐标轴的样式,确保坐标轴的范围严格限制为观察数据的范围;并确保坐标轴标题和主标题整洁。在该图中,添加密度估计的等高线,特定水平从 0.002 到 0.014,以 0.004 为步长进行序列计算。抑制等高线的标签显示。等高线应为深红色,并且随着密度水平的增加,线宽应变粗。为每条等高线添加图例,指示密度水平。我的(g)和(h)的图像如下所示。

25.5 像素图像
像素图像可以说是最直观的连续表面视觉表示,通过有限的评估网格来近似。它的外观类似于填充的等高线图,但图像图允许您更直接地控制每个相关* z *矩阵条目的显示。
25.5.1 一个网格点 = 一个像素
将您的* z 矩阵的每个条目视为一个小矩形,其颜色表示其相对值。这些矩形,或称为像素,正是图 25-9 中虚线灰色线条所形成的 z 矩阵概念图的单元格。在第 656 页的图中有所显示。这强调了一个重要事实,即评估网格序列(无论是 x 轴还是 y 轴方向)的细密程度直接决定了每个像素的大小,从而影响生成图像的平滑度。较小的像素意味着图像的分辨率*提高。
内置的image函数用于绘制像素图像。与contour函数类似,您将* x 和 y 轴的评估网格坐标作为递增序列传递给x和y参数,并将相应的 z *矩阵传递给z。回到在第 25.4.1 节的示例 1 中首次查看的volcano数据集,以下代码生成了图 25-15:
R> image(x=1:nrow(volcano),y=1:ncol(volcano),z=volcano,asp=1)
请再次注意,使用可选参数 asp=1 来强制水平和垂直轴的纵横比为 1:1。此图由正好 87 × 61 = 5307 个像素组成;每个像素代表 volcano 矩阵中的一个特定条目。从视觉上看,这个图像在同一数据的轮廓图(见图 25-10)中的反射非常明显。

图 25-15:奥克兰火山地形的像素图像
image 命令期望传入一个颜色向量,通常通过调色板的十六进制代码提供,传递给其 col 参数。如果没有指定,默认使用内建调色板 heat.colors(12),就像 volcano 图像中的那样。然而,一个直接的问题是缺少颜色图例。来自 shape 包的 colorlegend 函数(参见第 25.1.5 节)等贡献工具在这些图像中非常有用。
现在返回到示例 2 中的 mtcars 响应面,它拟合了关于马力和重量的 MPG 多元线性回归模型(以及这两个预测变量之间的交互效应)。这里为方便起见,简化了必要对象的代码(有关操作的完整说明,请参见第 25.4.1 节):
R> car.fit <- lm(mpg~hp*wt,data=mtcars)
R> len <- 20
R> hp.seq <- seq(min(mtcars$hp),max(mtcars$hp),length=len)
R> wt.seq <- seq(min(mtcars$wt),max(mtcars$wt),length=len)
R> hp.wt <- expand.grid(hp=hp.seq,wt=wt.seq)
R> car.pred.mat <- matrix(predict(car.fit,newdata=hp.wt),nrow=len,ncol=len)
就像之前一样,你已经在 car.pred.mat 中设置了一个包含 400 个元素的矩阵,这个矩阵是基于两个连续预测变量的长度为 20 的序列。
现在,确保加载 shape 包,这样你就可以使用 colorlegend 函数。接下来的代码首先设置了一个自定义的蓝色调色板,设置了新的边距限制以扩展右侧轴的区域,然后绘制了包括颜色图例的预测 20 × 20 响应面;结果展示在 图 25-16 的左侧。
R> blues <- colorRampPalette(c("cyan","navyblue"))
R> par(mar=c(5,4,4,5))
R> image(hp.seq,wt.seq,car.pred.mat,col=blues(10),
xlab="Horsepower",ylab="Weight")
R> colorlegend(col=blues(10),zlim=range(car.pred.mat),zval=seq(10,30,5),
main="Mean\nMPG")

图 25-16:两个像素图像,展示了在示例 2 中引入的mtcars平均 MPG 响应面,并附带颜色图例。关于马力和重量变量的评估网格,左侧的图像分辨率为 20²;右侧的图像基于更细的 50² 网格。轮廓图叠加在最右侧的图上。
在较粗的评估网格下,构成响应面的像素较为显眼。你可以通过使用更细的 hp.seq 和 wt.seq 评估网格,轻松提高参数响应面的分辨率。接下来的代码通过将 len 增加到 50,并覆盖之前使用的对象来实现这一点:
R> car.fit <- lm(mpg~hp*wt,data=mtcars)
R> len <- 50
R> hp.seq <- seq(min(mtcars$hp),max(mtcars$hp),length=len)
R> wt.seq <- seq(min(mtcars$wt),max(mtcars$wt),length=len)
R> hp.wt <- expand.grid(hp=hp.seq,wt=wt.seq)
R> car.pred.mat <- matrix(predict(car.fit,newdata=hp.wt),nrow=len,ncol=len)
然后,通过以下代码生成了 图 25-16 右侧的图像:
R> par(mar=c(5,4,4,5))
R> image(hp.seq,wt.seq,car.pred.mat,col=blues(100),
xlab="Horsepower",ylab="Weight")
R> contour(hp.seq,wt.seq,car.pred.mat,add=TRUE,lty=2)
R> colorlegend(col=blues(100),zlim=range(car.pred.mat),zval=seq(10,30,5),
main="Mean\nMPG")
新绘制的表面由 50² = 2500 个像素组成,而之前的图像仅由 20² = 400 个像素组成。图像的改善显而易见。在绘制新图像时,使用的颜色数量(来自自定义的blues调色板)增加到 100,以提供更平滑的颜色过渡。还注意到,在调用contour时使用了add,将等高线叠加到图像上,从而进一步强调评估网格上的波动表面。最后,使用colorlegend添加了一个图例,为图像增添了最后的润色。
25.5.2 表面截断和空像素
由于其对z矩阵的逐字一对一表示,像素图像在你想绘制不规则覆盖或小于标准矩形评估网格(跨越x-和y-轴)的表面时,尤其有效。为了仔细展示这种操作,让我们转到由 Baddeley 和 Turner(2005)提供的spatstat包中的新数据集。使用install.package("spatstat")安装spatstat。请注意,spatstat有许多依赖项;如果在下载和安装spatstat时遇到任何问题,请参见附录 A.2.3。
示例 4:非参数双变量密度估计(Chorley-Ribble 数据)
一旦spatstat被安装并加载到当前的 R 会话中,使用library("spatstat")调用,在提示符下输入?chorley查看帮助文件。这将详细介绍 Chorley-Ribble 癌症数据——收集于 1970 年代末到 1980 年代初的英格兰某地区的 1,036 例喉癌和肺癌的空间位置(数据首次由 Diggle, 1990 分析)。chorley对象是spatstat特有的特殊类(一个"ppp"对象——平面点模式),但其组件可以像引用命名列表的成员一样提取。
观察数据的坐标可以作为$x和$y组件提取。为了查看观察数据的空间分布,以下代码给出了图 25-17 的左上角图像:
R> plot(chorley$x,chorley$y,xlab="Eastings (km)",ylab="Northings (km)")
你的目标是显示癌症分布的二维概率密度函数的核密度估计,类似于你在示例 3 中对地震数据所做的。你将使用kde2d函数来完成这个任务——执行library("MASS")以访问该函数。然后,正如你对quakes的空间位置所做的那样,观察到的 Chorley-Ribble 数据的默认 KDE 表面如下所示:
R> chor.dens <- kde2d(x=chorley$x,y=chorley$y,n=256)
请注意,指定了一个细致的 256 × 256 东西-北方向评估网格。
要显示密度估计,请使用内置的rainbow调色板,并使用可选的start和end参数将调色板的总范围限制在从红色开始到洋红/粉色结束(这些参数在第 25.1.2 节中简要提到;更多关于使用start和end的详细信息,请参考帮助文件?rainbow)。使用以下行从此调色板预存 200 种颜色:
R> rbow <- rainbow(200,start=0,end=5/6)
然后,通过调用以下命令生成图像:
R> image(x=chor.dens$x,y=chor.dens$y,z=chor.dens$z,col=rbow)
另一个chorley的组件,名为$window,包含一个不规则多边形的顶点。该多边形定义了地理研究区域,观测数据就是在这个区域内收集的。$window组件也是spatstat中另一个特殊的对象类,即"owin",表示“观测窗口”。虽然可以提取多边形的特定顶点并使用内置功能手动绘制它,但spatstat的作者提供了一个标准的plot方法来实现这一目的。
运行image命令后,调用以下代码将研究区域的边界叠加在像素图像上:
R> plot(chorley$window,add=TRUE)
最终结果显示在图 25-17 的右上角。
你会注意到,数据收集的地理区域比观测数据本身的x和y范围稍宽,因此当前的绘图未能完整显示该区域。以下代码以数字方式显示这一点:
R> chor.WIN <- chorley$window
R> range(chorley$x)
[1] 346.6 364.1
R> WIN.xr <- chor.WIN$xrange
R> WIN.xr
[1] 343.45 366.45
R> range(chorley$y)
[1] 412.6 430.3
R> WIN.yr <- chor.WIN$yrange
R> WIN.yr
[1] 410.41 431.79
研究区域的x和y范围可以作为$window组件(存储在对象chor.WIN的第一行)的$xrange和$yrange组件获得。与在原始数据上调用range的结果比较时,你可以看到整体研究区域略大一些。
当然,问题并不仅此而已。从图中还可以看出,KDE 表面已经在实际上超出研究区域的一些区域进行了估算和绘制,因此这也需要修正。(稍后会看一下。)

图 25-17:试图绘制 Chorley-Ribble 癌症数据二维核密度函数的可视化实验。左上:原始数据。右上:基于数据范围的默认kde2d结果,上面叠加了研究区域。左下:在绘制原始密度估计时扩展xlim和ylim的调用。右下:使用研究区域的整个 x 和 y 范围重新定义评估网格的修订密度估计。
首先,你可以如何确保显示整个地理区域?嗯,当然可以使用之前存储在向量WIN.xr和WIN.yr中的区域范围,并在调用image时将它们提供给熟悉的可选参数xlim和ylim。
R> image(chor.dens$x,chor.dens$y,chor.dens$z,col=rbow,
xlim=WIN.xr,ylim=WIN.yr)
R> plot(chor.WIN,add=TRUE)
这两行代码的结果显示在图 25-17 的左下角。不幸的是,原始的密度估计仍然是基于原始数据的x和y范围定义的,这导致了空白像素的边界;此外,前述的密度区域仍然位于观察窗口之外。
所有这些都强调了一个重要的事实,即z矩阵是特定于预定义评估网格的。要使你的密度估计涵盖 Chorley-Ribble 数据的地理研究区域,唯一的方法是修改你的核密度估计,使其在覆盖该区域边界的评估网格上生成。幸运的是,kde2d函数允许你通过lims参数设置评估网格的可选x-y限制。这个参数接受一个长度为 4 的数值向量,顺序为x轴的下限和上限,接着是y轴的下限和上限。以下代码使用研究区域的边界重新估计密度并绘制图形。结果显示在图 25-17 的右下角。
R> chor.dens.WIN <- kde2d(chorley$x,chorley$y,n=256,lims=c(WIN.xr,WIN.yr))
R> image(chor.dens.WIN$x,chor.dens.WIN$y,chor.dens.WIN$z,col=rbow)
R> plot(chor.WIN,add=TRUE)
这样,你就解决了确保表面覆盖所需区域的问题。然而,这也明确突出了第二个问题——实际观察到的数据严格位于定义的多边形内,但你可以看到地理区域外的像素被绘制出来,这显然不合逻辑。你可以通过将z矩阵中相关条目的值设为NA来精确控制在任何给定像素图像中绘制哪些像素。
你需要一个机制来判断你的z矩阵中某个特定单元格的条目,即chor.dens.WIN$z,是否对应于多边形内部或外部的位置(对象chor.WIN)。如果它位于外部,你会希望强制该条目为NA。通常,这种类型的决策需要你根据评估网格中的坐标值来测试矩阵中的每个元素,可能需要使用你自己的 R 函数。幸运的是,在这种情况下,spatstat包中的inside.owin函数正是做了这件事,但无论何时你需要精确控制哪些像素被绘制,哪些没有,被绘制的像素也需要类似的原则。
给定一个或多个二维(x,y)坐标和一个类为"owin"的对象,inside.owin函数返回一个相应的逻辑向量,其中坐标在定义区域内的为TRUE,其他坐标为FALSE。作为快速演示,请观察以下结果:
R> inside.owin(x=c(355,345),y=c(420,415),w=chor.WIN)
[1] TRUE FALSE
这确认了你可以从图 25-17 看到的内容——坐标(355,420)位于多边形内,而坐标(345,415)不在其中。
现在,你需要在评估网格上的每个坐标使用inside.owin函数,该网格上的z-矩阵chor.dens.WIN$z所在的区域。首先,使用expand.grid创建完整的网格坐标集,方式与第 25.3.1 节中所示相同。
R> chor.xy <- expand.grid(chor.dens.WIN$x,chor.dens.WIN$y)
R> nrow(chor.xy)
[1] 65536
对结果数据框的坐标调用nrow可以确认你有正好 256² = 65536 个网格点,这些点在chor.dens.WIN KDE 对象中被定义。接下来的调用将使用chor.xy的两列,并通过逻辑取反(使用!)生成一个逻辑向量,标记那些位于定义的地理区域外部的网格坐标。
R> chor.outside <- !inside.owin(x=chor.xy[,1],y=chor.xy[,2],w=chor.WIN)
最后的步骤现在已经到来。
R> chor.out.mat <- matrix(chor.outside,nrow=256,ncol=256)
R> chor.dens.WIN$z[chor.out.mat] <- NA
首先,为了清晰起见,将长的chor.outside向量重新组织为一个 256 × 256 的矩阵,以强调它完全对应于感兴趣的z-矩阵。然后,这个逻辑标志矩阵用于直接覆盖z-矩阵中的“外部”条目,将其设置为NA。
现在剩下的就是用新处理过的z-矩阵绘制图像。确保你已经加载了shape包,以完成颜色图例的最后处理。以下代码创建了 KDE 表面像素图,只将像素点限制在由$window定义的地理区域内:
R> dev.new(width=7.5,height=7)
R> par(mar=c(5,4,4,7))
R> image(chor.dens.WIN$x,chor.dens.WIN$y,chor.dens.WIN$z,col=rbow,
xlab="Eastings",ylab="Northings",bty="l",asp=1)
R> plot(chor.WIN,lwd=2,add=TRUE)
R> colorlegend(col=rbow,zlim=range(chor.dens.WIN$z,na.rm=TRUE),
zval=seq(0,0.02,0.0025),main="KDE",digit=4,posx=c(0.85,0.87))
首先,你打开一个新的图形设备,并将右边距加宽,以容纳颜色图例。接着,你调用image函数绘制图形,具体使用一个L形框,并保持严格的 1:1 的x-y纵横比,然后你添加一个略微加粗的区域多边形。最后,你执行colorlegend来获得一个适当位置的图例,引用颜色值(该位置和刻度线是通过一些试验和错误后确定的)。你可以在图 25-18 中看到最终结果。

图 25-18:Chorley-Ribble KDE 表面最终像素图,只包含原始收集数据的地理研究区域。
注意
在截断最初定义在整个矩形评估网格上的二元密度估计核时,从技术上讲,你不再拥有有效的概率密度函数(因为在不规则区域上的积分将无法评估为总概率 1)。一个更数学上严谨的方法需要更深入的多元 KDE 知识,超出了本书的范围。然而,能够像这样截断像素图在任何情况下都很有用,尤其是在你想在一个(可能不规则的)子集上定义你的表面时。
练习 25.4
重新查看内置的airquality数据集,并查看帮助文件以刷新你对现有变量的记忆。创建数据框的副本:选择与每日温度、风速和臭氧水平相关的列,并使用na.omit删除任何包含缺失值的记录。
-
从你在第二十四章中对这些数据的探索来看,似乎存在日温度、风速和臭氧水平之间的关联。拟合一个多元线性回归模型,旨在根据风速和臭氧水平预测平均温度,包括交互效应。总结结果对象。
-
使用(a)中的模型,基于风速和臭氧的 50 × 50 评估网格构建一个z矩阵,预测每日平均温度。
-
创建响应面像素图像,并根据以下要求叠加原始观测值:
– 应该根据底部、左侧、顶部和右侧的边距线分别初始化一个图形设备,边距线分别为 5、4、4 和 6。
– 应使用来自内建
topo.colors调色板的 20 种颜色来生成图像;包括整洁的坐标轴标题。– 重新访问在第 25.1.4 节中定义的
normalize函数,并使用内建的gray函数生成一个灰色调色板(请参阅第 25.1.2 节)基于规范化后的原始温度观测。根据风速和臭氧叠加原始观测到像素图像上,使用灰色调色板来表示相应的温度观测。– 然后,应该分别调用
shape包中的colorlegend两次。第一次应出现在右侧边距的空间,引用表面本身。第二次应使用内建的gray.colors函数,设置可选参数start=0和end=1,生成 10 种灰度用于表示原始温度观测值的图例,这个图例应位于像素图像的右上角,没有原始观测值的地方。– 两个图例应具有适当的标题,您可能需要稍微调整
posx和posy参数,以找到满意的放置位置。我的绘图练习结果如下所示。
![image]()
在第 25.5.2 节中,你使用了chorley数据集来创建一个截取了整体矩形评估网格子集的像素图像。确保在当前的 R 会话中加载了spatstat,并执行以下两行代码:
R> fire <- split(clmfires)$intentional
R> firewin <- clmfires$window
这段代码提取了记录为故意点燃的火灾位置,共计 1,786 个位置,位于西班牙的某个特定区域。空间坐标可以作为fire的$x和$y成员提取,而地理区域本身则作为多边形存储在firewin中(它与之前查看的chorley$window对象属于同一类)。更多细节请参见使用?clmfires获得的文档。
-
使用研究区域的总x和y范围,使用
MASS包中的kde2d计算空间分布的二元核密度估计。KDE 表面应基于 256 × 256 的评估网格计算。 -
使用
expand.grid与inside.owin结合,识别所有落在矩形评估网格外的点。将密度表面上所有对应的像素设置为NA。 -
构建一个截断密度的像素图,具体如下:
– 图形设备应在绘图区域的底部、左侧和顶部留有三行空间,右侧留有七行空间。
– 在生成图像时,应使用
heat.colors调色板生成的 50 种颜色。应保持 1:1 的纵横比,压制坐标轴标题,并将框类型设置为L形。– 应使用双宽度的线条将地理研究区域叠加到图像上。
– 使用
shape中的colorlegend,应在图像右侧放置一个参考密度的颜色图例,并附上合适的标题。你需要尝试调整posx参数来确定图例的位置。图例标签应从5e-6到35e-6,步长为5e-6(有关 e 记数法的解释,请参见第 2.1.3 节);同时,确保这些标签可以显示最多六位小数的精度。
供你参考,我的结果在此给出。

25.6 透视图
本章中你将看到的最后一种图形是透视图,有时也被称为线框图。与等高线图和像素图不同,后者通过线条模式和/或颜色强调表面的波动,而透视图则利用物理的第三维度,在此基础上绘制z值。
25.6.1 基本图形和角度调整
透视图在你想强调填充z矩阵的值波动特性时尤其有用。例如,在某些应用中,你可能希望能够很好地感知绘制表面中任何峰值和/或谷值的相对极值,这在像素图或等高线图中更难实现。
回顾在第 25.4.1 节和 25.5.1 节中绘制的mtcars响应面,包括等高线图和像素图。你在马力和重量变量上创建了一个 20 × 20 的评估网格,并得到了相应的 400 个z矩阵值,表示预测的平均 MPG 结果:
R> car.fit <- lm(mpg~hp*wt,data=mtcars)
R> len <- 20
R> hp.seq <- seq(min(mtcars$hp),max(mtcars$hp),length=len)
R> wt.seq <- seq(min(mtcars$wt),max(mtcars$wt),length=len)
R> hp.wt <- expand.grid(hp=hp.seq,wt=wt.seq)
R> car.pred.mat <- matrix(predict(car.fit,newdata=hp.wt),nrow=len,ncol=len)
内建的 R 函数persp用于创建透视图。它的基本用法与contour、filled.contour和image相同。你在x轴和y轴方向上的递增序列(定义了评估网格)被传递给x和y,而对应的z矩阵则传递给z。用以下代码可以显示 20 × 20 mtcars响应面图的默认外观:
R> persp(x=hp.seq,y=wt.seq,z=car.pred.mat)
该示例出现在第 25-19 图的左上方。
解读透视图是简单直接的。默认的视角显示了前景中的x轴,从左到右递增,左侧显示了y轴,从前景延伸到背景深处。这样,评估网格就平铺在三维图形的底部,z轴即你绘制表面的轴,从底部垂直向上递增。
视角是这种图形中最重要的方面之一。在persp中,你可以通过两个可选参数来控制它,theta用于水平旋转图形,phi用于调整垂直视角。两者的单位都是度;theta的默认值是0,这意味着你正对着从左到右的x轴,phi的默认值是15,让视角稍微抬高一些,以便看到从前景到背景延伸的y轴。一般来说,你可以将theta的可能值视为从0到360,代表围绕图形进行一次完整的旋转,而phi的可能值则是从90到-90,这一范围将视角从鸟瞰图直接从上方俯视到潜水员视角直接从下方仰视。
第二个示例展示了这种行为:
R> persp(x=hp.seq,y=wt.seq,z=car.pred.mat,theta=-30,phi=23,
xlab="Horsepower",ylab="Weight",zlab="mean MPG")
事实上,正是这行代码最初生成了第 21-9 图右侧的图像,位于第 523 页(当你第一次接触到多重线性回归模型中两个连续预测变量的交互项的概念时)。该图形在第 25-19 图的右上方被重新呈现。轴标题通过xlab和ylab进行了整理,zlab则用于控制第三个垂直轴的标题。此示例中theta和phi的使用稍微抬高了视角,并旋转了图形,使得原点(即表示x-y平面下限的下端点)更加突出在前景。值得注意的是,增大theta值(从0开始)会使图形顺时针水平旋转,但你也可以给该参数传递负值来使图形逆时针旋转。将theta=-30,如这里所示,与设置theta=330具有相同的效果。

图 25-19:使用persp创建的 20 × 20 mtcars 响应面透视图。左上角:默认外观。右上角:使用 theta 和 phi 调整视角。左下角:设置 ticktype="detailed" 提供详细的轴标签。右下角:使用 shade 添加深度阴影,并通过 border=NA 移除面边框线。
默认情况下,没有勾选标记或标签,只有方向箭头。你可以通过将可选的ticktype参数设置为"detailed"来解决这个问题。你可以在图 25-19 的左下角查看以下结果,该图还提供了另一个视角:
R> persp(x=hp.seq,y=wt.seq,z=car.pred.mat,theta=40,phi=30,ticktype="detailed",
xlab="Horsepower",ylab="Weight",zlab="mean MPG")
帮助文件?persp详细列出了控制任何给定透视图展示的其他许多参数。例如,你可以将表面着色为灰度,以强调图像的三维深度,或者你可以更改颜色或抑制组成表面的网格线的绘制,或者你可以更改z轴的相对长度。mtcars响应面的最终图示例展示了这些操作。以下调用的结果可以在图 25-19 的右下角看到。
R> persp(x=hp.seq,y=wt.seq,z=car.pred.mat,theta=40,phi=30,ticktype="detailed",
shade=0.6,border=NA,expand=0.8,
xlab="Horsepower",ylab="Weight",zlab="mean MPG")
这张图与前一张图使用相同的视角,通过shade参数为表面面板着色,产生一种光照效果,稍微增强了感知的深度。阴影的计算依赖于一个非负数值;设置shade=0.6会提供一个中等强度的效果。你可以尝试更大或更小的值。如果你以这种方式给表面着色,通常最好抑制默认构成表面的网格线;你可以通过设置border=NA来实现这一点(border参数也可以通过提供任何有效的 R 颜色来改变表面网格的颜色)。最后,expand参数用于调整z轴的大小。指定expand=0.8会请求一个垂直轴,其大小为评估网格中轴的 80%,从而在其中绘制出一个稍微“压扁”的棱柱。你还可以为expand指定大于 1 的值,在这种情况下,效果是沿垂直方向“拉伸”图形。
25.6.2 着色面板
与大多数传统的 R 绘图命令一样,你可以使用可选的col参数为透视表面着色。如果你希望透视表面以恒定颜色填充,只需为col提供一个单一的值。
然而,如果你对col感兴趣,通常情况下,你会希望根据波动的z值为表面着色,以突出显示二元函数值的变化。为了成功地为构成表面的各个面片进行着色,重要的是要理解,这些面片与构成同一z矩阵的像素不同。image像素由例如你的m × n大小的z矩阵条目直接表示,而persp面片应该理解为这些矩阵条目处绘制的边界线之间的空间,这样你会得到(m − 1) × (n − 1)个面片。换句话说,在透视图中,每个z矩阵条目位于绘制线条的交点处——z矩阵条目不是位于每个面片的中心。
为了说明这一点,再看一眼图 25-9 中的内容,位于 656 页。当你使用image时,R 会根据你的x和y轴评估网格序列自动计算像素大小,并根据虚线灰色线条形成的矩形绘制表面,直接表示z矩阵条目a、b、c等。当你使用persp时,然而,边界线是通过实线网格(箭头)表示的,在每个条目处交叉,因此,生成的表面的面片是由这些线之间的空间形成的,每个面片由四个相邻条目定义。图 25-20 显示了图 25-9 中假设网格的一部分,我标出了image解释的一个像素和persp解释的一个面片。这样,你可以理解为什么在图 25-9 中,图像绘图中会有 6 × 4 = 24 个像素,而透视图中会有 5 × 3 = 15 个面片。

图 25-20:说明像素图像和透视图中z矩阵处理的区别。左下角突出显示的框代表值为a的image像素,右侧突出显示的框代表由b、h、i和c值形成的persp面片。为了着色,突出显示的面片的z值将被计算为这四个条目的平均值,换句话说,(b + h + i + c)* / 4。
col参数需要指定(m − 1) × (n − 1)个面片的颜色(假设传递给z的是一个m × n的z矩阵)。如果你打算根据z值为面片着色,在 R 中找到这个方法的典型方式是,首先计算每个面片的z值,该值将是四个相邻z矩阵条目的平均值。只有这样,你才能部署第 25.1.4 节中的一种颜色分配方法。
让我们将 Chorley-Ribble 核密度估计的像素图像(示例 4;图 25-18),并加入 z 轴特定的着色,重新绘制为一个透视图。首先,确保已经加载了 spatstat 和 MASS 这两个包。然后重复之前的代码,以便在适当的评估网格上获得核估计,并将其截断到地理研究区域。
R> chor.WIN <- chorley$window
R> chor.dens.WIN <- kde2d(chorley$x,chorley$y,n=256,
lims=c(chor.WIN$xrange,chor.WIN$yrange))
R> chor.xy <- expand.grid(chor.dens.WIN$x,chor.dens.WIN$y)
R> chor.out.mat <- matrix(!inside.owin(x=chor.xy[,1],y=chor.xy[,2],w=chor.WIN),
256,256)
R> chor.dens.WIN$z[chor.out.mat] <- NA
接下来,你需要计算所有的面片 z 值;可以使用以下代码批量完成此操作:
R> zm <- chor.dens.WIN$z
R> nr <- nrow(zm)
R> nc <- ncol(zm)
R> zf <- (zm[-1,-1]+zm[-1,-nc]+zm[-nr,-1]+zm[-nr,-nc])/4
R> dim(zf)
[1] 255 255
前三行只是将 z 矩阵存储为对象 zm,并将其总行数和列数(在此情况下都是 256)分别存储为 nr 和 nc,以使代码更加简洁。
第四行是进行相关计算的地方,生成一个面片 z 值的矩阵。它通过对原始 z 矩阵的四个版本按元素逐个求和来系统地完成此操作:zm[-1,-1](省略第一行和第一列)、zm[-1,-nc](省略第一行和最后一列)、zm[-nr,-1](省略最后一行和第一列)、以及 zm[-nr,-nc](省略最后一行和最后一列)。当这四个替代矩阵按此方式相加并在最后除以 4 时,结果是一个矩阵 zf,其每个元素是原始 z 矩阵中四个相邻条目所组成的每个“矩形”的四点平均值,正如图 25-20 中的讨论和标题所述。对 zf 的最终 dim 调用确认了结果的大小。由于定义的 z 矩阵中总共有 256 × 256 的评估网格线,因此这些网格包含了总共 255 × 255 的透视面片。
繁重的工作已经完成,现在你只需要将调色板中的颜色分配给 zf 中计算出的面片 z 值。你可以使用分类方法或标准化方法来完成此操作,如第 25.1.4 节中所述;为了简便起见,我们保持使用分类方法。考虑以下代码:
R> rbow <- rainbow(200,start=0,end=5/6)
R> zf.breaks <- seq(min(zf,na.rm=TRUE),max(zf,na.rm=TRUE),length=201)
R> zf.colors <- cut(zf,breaks=zf.breaks,include.lowest=TRUE)
第一行是从第 25.5.2 节重复的,用于生成与像素图像中相同的 200 种颜色,这些颜色来自内置的 rainbow 调色板。第二行设置了一个均匀间隔的序列,跨度为计算出的面片 z 值的范围,用来形成分类方法所需的类别断点。请注意,在调用 min 和 max 时使用了 na.rm=TRUE,以避免 zf 中的所有 NA 项(记住,表面已被截断为表示地理研究区域的不规则多边形)。这个序列的长度比生成的颜色数多一个——再次参考第 25.1.4 节以了解分类方法所需的这一特性。最后,cut 将每个 zf 面片值条目分配给相应的秩,按照 200 个排序好的区间进行分类。如你所学,zf.colors 的秩随后在绘图时用于索引存储在 rbow 中的 200 种颜色。
这样,你就可以享受你劳动的成果了!以下代码绘制了 Chorley-Ribble 观察值的双变量核密度估计,并使用视角图来展示,其中通过面片着色反映表面沿 z 轴的相对高度。为了清晰地展示颜色,边框线被抑制,z 轴略微缩小,并在右侧插入了一个颜色图例(确保已加载 shape 包)。在通过 par 调用 mar 调整默认图形边距后,额外的空间被用于插入该图例。你可以在 图 25-21 中查看结果。
R> par(mar=c(0,1,0,7))
R> persp(chor.dens.WIN$x,chor.dens.WIN$y,chor.dens.WIN$z,border=NA,
col=rbow[zf.colors],theta=-30,phi=30,scale=FALSE,expand=750,
xlab="Eastings (km)",ylab="Northings (km)",zlab="Kernel estimate")
R> colorlegend(col=rbow,zlim=range(chor.dens.WIN$z,na.rm=TRUE),
zval=seq(0,0.02,0.0025),main="KDE",digit=4,
posx=c(0.85,0.87),posy=c(0.2,0.8))

图 25-21:Chorley-Ribble 密度估计的视角图,演示了根据表面 z 值变化的面片着色效果。
我在执行 persp 时加入了可选参数 scale=FALSE。这保持了 x 和 y 坐标方向上的一对一纵横比;这对于查看地理数据非常有用。不幸的是,这也迫使 z 轴上的密度估计值按照相同的方式进行缩放,这在当前图表的上下文中是没有意义的。为了避免小比例造成表面看起来过于平坦,你需要使用 expand 来人为地放大第三个轴上的表面。在这个例子中,将其乘以大约 750 的因子能够得到一个视觉上令人愉悦的结果。需要注意的是,如果你将 scale 参数保留为默认的 TRUE 值(因为在这种情况下,R 会自动为三个轴进行一对一比例缩放),这就不需要进行这种操作。
25.6.3 使用循环进行旋转
如果你想对绘制的表面有一个整体的印象,可以通过视角图进行最后一点有趣的操作。使用一个简单的 for 循环(第 10.2.1 节),通过递增 theta 或 phi,你可以对 persp 进行一系列的重复调用,每次以略微不同的角度进行。按顺序执行这些操作会产生一个动画——本质上是一个旋转表面的卡通效果,让你能够从不同的角度观察它。
在 R 编辑器中,考虑以下基本函数:
persprot <- function(skip=1,...){
for(i in seq(90,20,by=-skip)){
persp(phi=i,theta=0,...)
}
for(i in seq(0,360,by=skip)){
persp(phi=20,theta=i,...)
}
}
使用省略号(见 第 11.2.4 节),persprot 只是接受通常传递给 persp 调用的所有参数,除了 theta 和 phi。接下来是一个 for 循环,它立即调用 persp,设置 theta=0 和省略号的内容。for 循环通过改变垂直视角来进行调整,从 phi=90(鸟瞰图)开始,然后逐渐下降到轻微抬高的 phi=20。第二个 for 循环则通过改变 theta 完成一个完整的 360 度水平旋转。
唯一正式标记的参数是skip,它决定了每次迭代中phi和theta的增量。默认值skip=1会在整数角度之间移动。增加skip值会减少完成旋转所需的时间,尽管这会使动画变得更加生硬。
根据你使用的图形设备类型,你可能想尝试调整skip。请注意,并非所有图形设备类型都适合通过运行这个相对粗糙的函数来实现动画效果(例如,如果你使用的是 RStudio,这种效果并不合适——见附录 B)。不过,在 OS X 或 Windows 上的基本 R GUI 应用中,我发现persprot在默认图形设置下运行良好。
导入该函数来试试;在这里我们将其用于空间quakes位置的概率密度函数核估计的视角图,这些位置你在示例 3 中首次研究过,第 25.4.1 节有介绍。MASS包已经加载,使用以下代码在 50 × 50 的评估网格上生成密度估计。
R> quak.dens <- kde2d(x=quakes$long,y=quakes$lat,n=50)
然后你像使用persp一样使用persprot,无需指定theta或phi。
R> persprot(x=quak.dens$x,y=quak.dens$y,z=quak.dens$z,border="red3",shade=0.4,
ticktype="detailed",xlab="Longitude",ylab="Latitude",
zlab="Kernel estimate")
图 25-22 展示了一系列旋转图的截图。

图 25-22:一个旋转视角图,展示了空间地震位置的 KDE 表面,在调用自定义persprot函数之后
练习 25.5
在练习 25.3(a)中,你重新审视了来自boot包的nuclear数据集,并拟合了两个多元线性回归模型,旨在通过许可日期发放和工厂容量来建模平均建设成本——一个仅包括主效应,另一个则包括两个连续预测变量之间的交互项。
-
重新拟合两个版本的模型,并基于 50 × 50 的评估网格生成响应表面的视角图,再次考虑以下因素:
在调用
par时使用mfrow将两个视角图并排显示。在同一个par调用中,覆盖默认的图形边距,使每一侧只有一行空白(par在第二十三章中有详细探讨)。– 使用
zlim确保两个图形在相同的纵轴比例上显示,分别将每个图水平旋转 25 度,确保轴标记清晰并且标题整洁。是否有任何视觉指示表明交互项的存在对建模响应产生了有意义的影响?
-
开始一个新的图形。为了更好地了解两个表面之间的差异,生成一个视角图,展示通过对两个拟合模型中各自的z矩阵逐元素相减所得到的z矩阵。通常来说,包含交互项的效果是什么?
将注意力转回到奥克兰火山的地形信息,使用内置的 R 对象 volcano:一个 87 × 61 的海拔值矩阵(单位为米)。你在第 25.4.1 节中首次以等高线图的形式查看了这个数据。
-
使用简单的整数序列为 x 和 y 坐标绘制火山的最基本默认透视图。
-
图(c)的绘图由于多种原因显得不太吸引人。根据以下要求,制作一个更现实的火山图:
– 使用一个新的图形设备,将边距宽度重置为底部、左侧、顶部和右侧分别为 1、1、1 和 4 行。
– 帮助文件
?volcano显示火山的 x 和 y 坐标对应的 z 矩阵单位是 10 米。使用scale并调整expand,重新绘制表面,使得三个坐标轴的纵横比正确。– 使用
axes去除所有坐标轴刻度和标注。– 各个面板的颜色应根据从内置
terrain.colors调色板生成的 50 种颜色来着色,并且应去除面板的边框线。– 选择一个视觉上更具吸引力的视角。
– 使用
shape包中的colorlegend在图表右侧的空间中放置一个颜色图例,表示以米为单位的海拔高度。可以尝试不同的参数来找到合适的放置位置和刻度标记。这是我改进后的图表版本:
![image]()
在练习 25.4 中,你研究了西班牙某地区故意点燃的火灾的空间分布。确保加载了 spatstat 包,然后重新运行以下代码行以获得相关的数据对象:
R> fire <- split(clmfires)$intentional
R> firewin <- clmfires$window
-
从练习 25.4 中借用代码(d)和(e),根据 256 × 256 的评估网格(截断至研究区域)重新绘制这个分布观察值的核密度估计。然后,按照以下方式将其显示为透视图:
– 和像素图像一样,使用内置的
heat.colors调色板中的 50 种颜色,根据 z 值为各个面板着色。请注意,这个函数的截断 z-矩阵包含NA值。– 表面上的边框线应被去除,并且你应找到自己喜欢的视角。
– 使用
scale来确保正确的空间纵横比。在这样做时,你还需要通过大约 5,000,000 倍的因子调整 z 轴的扩展,以便在垂直方向上显示密度表面,这是由于在指定评估网格上密度估计的自然缩放。– 使用详细的坐标轴标签,并根据需要简单地将坐标轴命名为
"X"、"Y"和"Z"。我的产品如下所示。
![image]()
-
使用在第 25.6.3 节中定义的
persprot函数来查看(e)中的表面,设置skip=10。
本章重要代码
| 函数/操作符 | 简要描述 | 首次出现 |
|---|---|---|
palette |
列出整数颜色 | 第 25.1.1 节, 第 632 页 |
col2rgb |
命名颜色转 RGB | 第 25.1.1 节, 第 632 页 |
rgb |
RGB 转十六进制代码 | 第 25.1.1 节, 第 633 页 |
rainbow, heat.colors, gray, terrain.colors, cm.colors, topo.colors, gray.colors |
内建调色板 | 第 25.1.2 节, 第 635 页 |
colorRampPalette |
自定义调色板(整数) | 第 25.1.3 节, 第 636 页 |
colorRamp |
自定义调色板([0,1]区间) | 第 25.1.4 节, 第 640 页 |
colorlegend |
颜色图例(shape) |
第 25.1.5 节, 第 641 页 |
scatterplot3d |
3D 散点图(scatterplot3d) |
第 25.2.1 节, 第 649 页 |
expand.grid |
所有唯一的评估坐标 | 第 25.3.1 节, 第 654 页 |
letters |
字母字符 | 第 25.3.1 节, 第 655 页 |
contour |
等高线图 | 第 25.4.1 节, 第 657 页 |
kde2D |
双变量 KDE(MASS) |
第 25.4.1 节, 第 660 页 |
filled.contour |
颜色填充的等高线图 | 第 25.4.2 节, 第 664 页 |
image |
像素图像 | 第 25.5.1 节, 第 668 页 |
inside.owin |
测试区域内部(spatstat) |
第 25.5.2 节, 第 674 页 |
persp |
透视图 | 第 25.6.1 节, 第 680 页 |
第二十六章:26
交互式 3D 图

当谈到 3D 绘图时,能够从不同的角度查看它们非常重要,这样才能解读所显示的函数或表面。Adler 等人(2015)的 rgl 包提供了一些非常棒、易于使用的 R 函数,可以让你通过鼠标旋转和放大三维图形。在本章中,你将看到一些展示 rgl 功能的例子。
从内部实现来看,rgl 使用 OpenGL —— 一个标准的应用程序接口 —— 在你的计算机屏幕上渲染图形。安装 rgl(例如,通过在提示符下调用 install.packages("rgl"))后,再调用 library("rgl") 来加载它。
26.1 点云
让我们从最基本的 3D 图开始——点云。在统计学中,这个工具通常用于提供三种连续变量的散点图,就像你在创建静态 3D 散点图时看到的那样。
26.1.1 基础 3D 云图
回到内置的 iris 数据集,该数据集由对三种花卉的四个测量值组成。像在第 25.2.1 节中做的那样,在你的工作空间中创建以下四个向量以方便访问:
R> pwid <- iris$Petal.Width
R> plen <- iris$Petal.Length
R> swid <- iris$Sepal.Width
R> slen <- iris$Sepal.Length
你可以使用 rgl 的 plot3d 函数来显示一个交互式的 3D 点云。它的调用方式和散点图类似——分别将 x、y 和 z 坐标传递给 x、y 和 z 参数。以下代码行打开一个 RGL 设备,并绘制来自 iris 数据集的花瓣宽度、花瓣长度和萼片宽度的散点图:
R> plot3d(x=pwid,y=plen,z=swid)
你可能想要使用鼠标增大设备的大小,以便更清楚地查看数据。然后,通过左键点击图形并按住按钮,你可以移动鼠标,在任何你喜欢的方向旋转图形。如果右键点击图形并按住,你可以控制缩放。具体来说,右键点击并按住同时将鼠标向上移动将会缩小,右键点击并按住同时将鼠标向下移动将会放大。坐标轴的刻度线和标签会根据你的观察角度自动出现在不同的侧面。图 26-1 展示了这一图形。

图 26-1:使用 rgl 的 plot3d 函数绘制的 iris 数据的交互式 3D 散点图。 这是绘制花瓣宽度、花瓣长度和萼片宽度时的默认显示效果。
26.1.2 视觉增强和图例
你可以通过一些新的方式以及一些熟悉的方式来改变plot3d散点图的外观。例如,默认的可选参数type设置为"p"(即“点”),将点绘制为像最近的散点图那样的小圆点。要将点绘制为可见的 3D 球体,可以使用type="s"。你可以通过使用size来控制任何绘制的点或球体的大小,还可以通过使用col来控制颜色(或颜色)。legend3d函数是rgl库中legend函数的类似物,它也非常有用;它通过更改交互式图形背景图像来工作。
为了说明这些修改,让我们重新绘制相同的iris观察数据。首先,关闭任何当前打开的 RGL 图形设备。然后,执行以下命令:
R> plot3d(x=pwid,y=plen,z=swid,size=1.5,type="s",
col=c(1,2,3)[as.numeric(iris$Species)])
这将启动一个新的 RGL 设备,根据花卉物种为球体着色。像往常一样,你需要将col参数传递一个与绘制坐标长度相同的向量,它会逐一为对应的点分配颜色。你指定size参数时使用的尺度略有不同于传统 R 图形的cex参数,它会根据type的值发生变化——可以查看帮助文件?plot3d了解详细信息。通过一些实验,你可以轻松找到一个适合图形的大小值。
要添加图例,首先用鼠标调整 RGL 设备的大小到你喜欢的显示尺寸,然后执行以下命令:
R> legend3d("topright",col=1:3,legend=levels(iris$Species),pch=16,cex=2)
这会插入一个静态、不可移动的图例,通过颜色引用绘制的物种。legend3d函数实际上调用了基础 R 的legend函数,因此它们的使用方法非常方便。图例静态显示后,散点图仍然是完全交互式的,你可以继续旋转和缩放。图 26-2 展示了这一切。
legend3d函数会改变背景画布,这就是为什么在添加图例之前,你需要手动打开一个新设备并调整其大小。如果你在没有先关闭设备或重置背景的情况下,在同一个设备中创建了一个新的rgl图形,花卉物种的图例仍然会存在。如果你要创建多个rgl图形,可以随时通过调用以下命令将背景重置为默认的白色画布:
R> bg3d(color="white")
如果你尝试在最最近的图形仍然处于活动状态时使用此方法,你会看到花卉物种的图例消失,而散点图仍然存在。或者,你可以在完成时直接关闭 RGL 设备,这样任何后续图形都会使用新的设备。

图 26-2:使用plot3d重新绘制iris花瓣宽度、长度和萼片宽度数据。观察值被绘制为球体,增加了大小,并根据物种类型着色;通过legend3d添加了图例。
26.1.3 添加更多的 3D 组件
你还可以向当前的 3D 图形中添加新的观察点和线条。rgl 包含了 points3d、lines3d 和 segments3d 函数,类似于基础 R 图形中的 points、lines 和 segments。举个例子,在第 25.2.2 节中,你使用了一个可选参数,将垂直线从 x-y 平面基准点画到每个绘制的点,在 scatterplot3d 中实现这一功能。如果是在 plot3d 散点图中,你将使用 segments3d 达到相同效果。此外,你还可以通过 rgl 图形的 grid3d 函数添加默认情况下在相同平面上绘制的网格线,这是 scatterplot3d 图形的默认功能。
让我们实际操作一下。回顾一下 图 25-8 和 第 652 页。要使用 rgl 功能创建类似的图形,其中颜色用于表示第四个连续变量——花萼长度,首先重新创建调色板并为每个观察点设置颜色。这里通过对 50 种颜色的分类实现(见第 25.1.4 节)。
R> slen.pal <- colorRampPalette(c("purple","yellow2","blue"))
R> cols <- slen.pal(50)
R> slen.cols <- cut(slen,breaks=seq(min(slen),max(slen),length=51),
include.lowest=TRUE)
然后,关闭任何当前激活的 RGL 设备,或者清除聚焦设备的背景。调用 plot3d 会用适当颜色的球体开始绘制图形。
R> plot3d(x=pwid,y=plen,z=swid,type="s",size=1.5,col=cols[slen.cols],
aspect=c(1,1.75,1),xlab="Petal width",ylab="Petal length",
zlab="Sepal width")
你向 aspect 参数提供一个长度为 3 的向量,描述 x、y 和 z 轴的相对长度。通过将第二个条目改为 1.75,你将 y 轴的长度按该倍数相对于其他轴进行了拉伸。这会在 y 轴上产生拉伸效果。颜色通过使用 slen.cols 因子向量的向量索引分配,而 xlab、ylab 和 zlab 用来整理坐标轴的标题。
现在,要从 x-y 平面到每个观察点添加垂直线,你需要了解如何使用 segments3d 函数。与基础 R 中的 segments 不同,segments3d 不会将“起点”和“终点”坐标分成不同的参数(回想一下在 segments 和 arrows 中使用的 x0、y0、x1 和 y1)。相反,它将依次处理传递给 x、y 和 z 参数的每对观察点,将它们视为每个线段的起点和终点。
因此,要在现有的 RGL 设备上绘制垂直线,首先需要设置包含“起点”和“终点”位置的向量,这些位置位于 3D 空间中。考虑以下代码:
R> xfromto <- rep(pwid,each=2)
R> yfromto <- rep(plen,each=2)
R> zfromto <- rep(min(swid),times=2*nrow(iris))
R> zfromto[seq(2,length(zfromto),2)] <- swid
前两行分别通过简单地将每个观测值复制两次,设置了x和y分量的向量xfromto和yfromto。这些非常简单,因为这些坐标方向上的“从–到”值不会变化。然而,z分量会变化。你首先通过将最小的花萼宽度值min(swid)复制两倍数据集大小来创建zfromto向量,从而得到一个与xfromto和yfromto长度相同的向量。然后,zfromto的每隔一个位置会被花萼宽度向量的元素覆盖。这为所有观测值提供了“从”z值,即min(swid),并按要求(以对方式配对)与swid中的“到”z值匹配。结合xfromto和yfromto,你将得到从图表底部x-y平面(其垂直位置自动定在min(swid))到实际swid值的线条(这当然是每个球体的相应z值)。
为了帮助理解它们是如何设置的,可以将坐标向量打印到控制台屏幕,以便查看它们的内容。然后,调用segments3d将这些线条绘制到图表上。
R> segments3d(x=xfromto,y=yfromto,z=zfromto,col=rep(cols[slen.cols],each=2))
为了确保每条线的颜色与其对应的球体匹配,你还需要将cols[slen.cols]中由向量索引的颜色集合中的每个条目复制两次,这意味着颜色的“从–到”是恒定的。
然后,执行以下操作将参考网格放置在下方的x-y平面上:
R> grid3d(side="z-")
对于side参数,你需要指定要保持不变的轴(在这里是z轴)以及放置网格的位置(在这种情况下,因为你希望网格位于z轴的下端,所以你使用负号指定)。如果你想将网格放置在垂直轴的上端,即矩形棱柱的顶部,你可以指定side="z+"。
最后,你可以向图表添加一个自定义的、连续颜色的图例,用于参考花萼长度。bgplot3d函数是legend3d的一个更通用版本;它允许你指定任何绘图命令来定义 RGL 设备的背景。我们将使用shape包中的colorlegend函数来实现这一点,该函数在第 25.1.5 节中首次介绍。确保你已经加载了shape包,并且散点图的 RGL 设备大小符合你的需求。在我的机器上,我执行以下操作:
R> bgplot3d({plot.new();colorlegend(slen.pal(50),zlim=range(slen),
zval=seq(4.5,7.5,0.5),digit=1,
posx=c(0.91,0.93),posy=c(0.1,0.9),
main="Sepal length")})
bgplot3d函数可以接受多个绘图命令,这些命令需要作为一个代码块放在大括号{}内,每个命令用分号(;)分隔。在此示例中,最初调用plot.new()会静默初始化 RGL 设备的背景,以便你可以添加连续颜色的图例。如果没有这个调用,colorlegend仍然可以工作,但会发出警告。图 26-3 显示了最终结果,散点图仍然可以通过鼠标进行旋转和缩放。
旋转 3D 散点图以及你将在接下来的几节中看到的任何图形,使用简单的鼠标命令特别方便,尤其是当你在探索高维数据的可视化时。你不受限于单一视角,也不需要在实际生成图形之前手动决定视角。rgl功能还使得将额外元素添加到现有图形中变得容易——这是scatterplot3d或persp图形难以做到的。尽管如此,你可能会发现某些在传统绘图中习以为常的功能在交互式图形中很难实现。例如,rgl中没有与pch图形参数对应的功能。要绘制不同的符号,你需要设计、渲染并放置新的 3D 形状。

图 26-3:这展示了在iris数据的plot3d 3D 散点图上添加线条和平面网格,以模拟之前的scatterplot3d同一数据的示例。
练习 26.1
返回survey数据框,位于MASS包中,如果需要的话,可以查看帮助文件?survey以了解当前变量的描述。创建一个survey的副本,仅包含书写手距、非书写手距、左撇子或右撇子、性别和身高这几列。然后使用na.omit来删除包含缺失值的这一子集数据框的行。
-
生成一个基本的交互式 3D 点云,z-轴表示学生身高,x-轴表示书写手距,y-轴表示非书写手距。
-
创建一个更具信息量的散点图版本,使用颜色区分性别,使用点的大小区分左撇子和右撇子个体,按照以下指导进行:
– 首先绘制只对应右撇子个体的点。通过使用性别的数字版本来设置颜色,右撇子女性应为黑色,男性为红色。
– 将右撇子个体的绘制点大小设置为 4,并确保坐标轴标签整洁。
– 使用
points3d,将左撇子个体的点添加到现有图形中。颜色应按照与右撇子学生相同的方式根据性别分配,但这次,点的大小应设置为 10。– 调整 RGL 设备的大小,并在左上角添加一个图例,参考四种类型的点:“
Male RH”(男性 RH)、“Female RH”(女性 RH)、“Male LH”(男性 LH)和“Female LH”(女性 LH)。在设置图例时,使用pch值为19,并为右撇子和左撇子分别使用pt.cex值0.8和1.5。作为参考,我的可旋转 3D 散点图版本如下所示:
![image]()
在练习 25.2 中,您查看了内置的 airquality 数据的静态 3D 散点图。再次创建数据框的副本,省略任何包含 NA 条目的行。
-
使用
rgl功能创建一个与之前练习相似的图,分别在 x、y 和 z 轴上显示风速、太阳辐射和温度,按照以下指南:– 从内置的
topo.colors调色板中设置 50 种颜色。根据分类方法,为臭氧值设置相应的颜色索引向量。– 将观测值绘制为大小为 1 的球体,颜色与之前相同,并修改长宽比,使得 y 轴的长度是其他两个轴的 1.5 倍。提供整洁的轴标题。
– 添加相应颜色的线条,每条线代表一个观测值,从 x–y 平面垂直向上延伸,直到与绘制的球体相交。同时,在下方的 x–y 平面上放置网格。
– 修改 RGL 设备的背景,包含一个与臭氧水平相关的颜色图例;使用 60 到 95 之间以 5 为步长的数值来标记它。这是我的结果:

26.2 双变量表面
接下来,你将使用 rgl 绘制双变量表面——一个相对于 2D x–y 评估网格计算出的连续表面。在第二十五章中,你使用基础 R 图形的 contour、filled.contour、image 和 persp 绘制了这些图形。任何你能够使用这些函数绘制的图形,也可以通过 rgl 的 persp3d 函数绘制为交互式透视图。
26.2.1 基本透视表面
以 mtcars 数据集中的平均 MPG(每加仑英里数)作为马力和重量的函数响应面(首次在第 25.4.1 节中使用)作为一个简单的初步示例。在第 25.6.1 节中,你绘制了该响应面的静态基础 R 视角图。接下来的几行将重新拟合多元线性回归模型,并重新创建 20 × 20 的评估网格 x 和 y 序列:
R> car.fit <- lm(mpg~hp*wt,data=mtcars)
R> len <- 20
R> hp.seq <- seq(min(mtcars$hp),max(mtcars$hp),length=len)
R> wt.seq <- seq(min(mtcars$wt),max(mtcars$wt),length=len)
R> hp.wt <- expand.grid(hp=hp.seq,wt=wt.seq)
为了创建该表面,像之前一样使用 hp.wt 中的评估网格进行预测,但这次,包含对原始观测值的预测区间计算。
R> car.pred <- predict(car.fit,newdata=hp.wt,interval="prediction",level=0.99)
(你将在后续示例中使用该间隔。)然后,构建 z 矩阵,并用以下两行绘制一个绿色的 persp3d 表面:
R> car.pred.mat <- matrix(car.pred[,1],nrow=len,ncol=len)
R> persp3d(x=hp.seq,y=wt.seq,z=car.pred.mat,col="green")
结果如图 26-4 的左侧所示。如果你将其与图 25-19 进行比较,便可以看到它显示了相同的表面。persp3d表面所产生的默认光照和阴影效果有助于深度感知,类似于persp的shade参数。这个版本的主要优点是基于鼠标的旋转和缩放交互性。

图 26-4:两种交互式 persp3d 版本的 mtcars 响应面。左侧:绿色的默认外观。右侧:红色,70%不透明度的表面,原始数据在三维空间中叠加。两个图都可以通过鼠标旋转和缩放。
26.2.2 添加组件
persp3d绘制的表面还有一个有用的属性,那就是可以轻松地添加更多组件——这是基础 R 功能中远远不如这个方法直接的事情。你将继续使用之前为mtcars响应面创建的对象。
添加点
由于这个响应面是基于适配于马力、重量和 MPG 这三个变量的数据的模型,它会很有帮助将原始观测数据与拟合模型一起查看。为此,你可以使用points3d,它的功能就像基础 R 图形中的points。执行以下命令:
R> persp3d(x=hp.seq,y=wt.seq,z=car.pred.mat,col="red",alpha=0.7,
xlab="Horsepower",ylab="Weight",zlab="mean MPG")
R> points3d(mtcars$hp,mtcars$wt,mtcars$mpg,col="green3",size=10)
调整 RGL 设备的大小以适应你的需要并保持设备开启。这两个命令绘制了预测的平均 MPG 响应面,这次使用可选的alpha参数设置为 70%的不透明度并显示为红色,然后将原始观测数据以绿色添加到同一图像中,并略微放大它们的默认大小。你可以在图 26-4 的右侧看到这个图;你现在可以比较响应面与原始数据的拟合情况,并从任何角度查看。
添加表面
你还可以添加更多的透视表面!让我们继续使用你为图 26-4 创建的car.pred对象来添加当前图表。响应面存储在car.pred的第一列;相应的上下预测限制存储在第二列和第三列——回到第 20.4.2 节讨论线性回归模型的predict。要将这些预测边界添加到图 26-4 右侧显示的响应面,你首先需要将每个边界表面存储为一个对应于x-y评估网格的z-矩阵。
R> car.pred.lo <- matrix(car.pred[,2],nrow=len,ncol=len)
R> car.pred.up <- matrix(car.pred[,3],nrow=len,ncol=len)
然后,简单地对每一个z-矩阵调用persp3d,并使用可选的add参数设置为TRUE——这指示persp3d函数将新的图形添加到现有的图形中,而不刷新图表。
R> persp3d(x=hp.seq,y=wt.seq,z=car.pred.up,col="cyan",add=TRUE,alpha=0.5)
R> persp3d(x=hp.seq,y=wt.seq,z=car.pred.lo,col="cyan",add=TRUE,alpha=0.5)
在这里,你还将每个附加表面的颜色设置为青色,并将不透明度设置为 50%。你可以在图 26-5 左侧看到结果。通过鼠标旋转它,你将能够看到所有观测值都落在这个特定模型的 3D 99%预测区间的范围内。

图 26-5:在现有的persp3d拟合mtcars模型图中,添加进一步的表面表示 99%预测区间。左侧:绿色点表示原始观测值。右侧:原始观测值被加上文本标签,相应的线段标记了对应的残差。
另外,你可以使用原始mtcars数据框的行名属性将原始观测值标注为文本,这样你就可以在图中识别出每辆车对应的名称。在这种情况下,可以使用内置的rownames函数将名称作为字符字符串向量获取。为了在现有的 3D 图形中添加文本,rgl有一个类似于传统text函数的函数text3d。执行以下四行代码会重新绘制半透明的红色响应面,在对应的(x,y,z)坐标处添加适当的文本,并再次添加青色的预测区间:
R> persp3d(x=hp.seq,y=wt.seq,z=car.pred.mat,col="red",alpha=0.7,
xlab="Horsepower",ylab="Weight",zlab="mean MPG")
R> text3d(x=mtcars$hp,y=mtcars$wt,z=mtcars$mpg,texts=rownames(mtcars),cex=0.75)
R> persp3d(x=hp.seq,y=wt.seq,z=car.pred.up,col="cyan",add=TRUE,alpha=0.5)
R> persp3d(x=hp.seq,y=wt.seq,z=car.pred.lo,col="cyan",add=TRUE,alpha=0.5)
由于文本比绿色点更难以视觉定位,因此在拟合的表面上标出它们的位置是有意义的——而使用拟合模型的残差来实现这一点又有什么比这更好的方法呢?正如你从iris数据的 3D 散点图中知道的那样,segments3d函数非常适合这个目的。首先,你需要在三个坐标轴上设置“从–到”向量(有关segments3d的解释,请参见第 26.1 节)。
R> xfromto <- rep(mtcars$hp,each=2)
R> yfromto <- rep(mtcars$wt,each=2)
R> zfromto <- rep(car.fit$fitted.values,each=2)
R> zfromto[seq(2,2*nrow(mtcars),2)] <- mtcars$mpg
在这里,x轴和y轴的值在从“从”位置到“到”位置的过程中并没有变化,因此它们只是原始数据框中每个马力和重量条目的双重副本。你需要指示z轴的“从”值保持为模型的拟合值(换句话说,响应面的实际垂直位置),而“到”值是原始数据的z值。然后,最后一次调用segments3d函数绘制残差,每个带有text3d标签的汽车对应一条标准的黑色线段。
R> segments3d(x=xfromto,y=yfromto,z=zfromto)
花一点时间与最终产品互动,见图 26-5 右侧。
26.2.3 根据 z 值着色
persp3d图的一个优点是,你可以根据z值着色表面,而无需做任何特殊处理。回想一下,如果你使用的是基础 R 的persp函数,按z值着色将需要一个小小的变通,因为你需要计算作为每个面片四个相邻z矩阵条目的平均值来获得相关的垂直位置(见第 25.6.2 节)。
幸运的是,使用persp3d时并不需要这么做。再次以mtcars响应面为例,你可以设置所需的颜色调色板,并直接将颜色分配给z-矩阵的条目,而无需先平均每组相邻的四个值。
R> blues <- colorRampPalette(c("cyan","navyblue"))
R> blues200 <- blues(200)
R> zm <- car.pred.mat
R> zm.breaks <- seq(min(zm),max(zm),length=201)
R> zm.colors <- cut(zm,breaks=zm.breaks,include.lowest=TRUE)
然后,使用分类方法为连续值分配颜色,当在persp3d中指定col值时,只需要通过zm.colors索引blues200。
R> persp3d(x=hp.seq,y=wt.seq,z=car.pred.mat,col=blues200[zm.colors],
alpha=0.6,xlab="Horsepower",ylab="Weight",zlab="mean MPG")
图 26-6 展示了结果。

图 26-6:展示在persp3d中使用z-矩阵值直接分配颜色,生成的mtcars响应面结果
26.2.4 处理纵横比
从mtcars模型中休息一下,你现在将返回到 Chorley-Ribble 数据的双变量核密度估计,该数据在第 25.5.2 节和 25.6.2 节中使用。加载spatstat包以访问chorley数据,并使用MASS包访问kde2D函数。为了方便起见,以下代码计算使用kde2D的 KDE 表面,结果存储在chor.dens.WIN对象的$z组件中:
R> chor.WIN <- chorley$window
R> chor.dens.WIN <- kde2d(chorley$x,chorley$y,n=256,
lims=c(chor.WIN$xrange,chor.WIN$yrange))
R> chor.xy <- expand.grid(chor.dens.WIN$x,chor.dens.WIN$y)
R> chor.out.mat <- matrix(!inside.owin(x=chor.xy[,1],y=chor.xy[,2],
w=chor.WIN),
256,256)
R> chor.dens.WIN$z[chor.out.mat] <- NA
它还通过将所有位于该多边形外部的z-矩阵元素设置为NA,将表面截断,以使其落入表示地理研究区域的多边形内(你已经在第 25.5.2 节中详细学习了如何实现这一点)。
然后,执行接下来的几行代码生成 200 种颜色,这些颜色来自你之前为这个 KDE 图使用的内置rainbow调色板,并将截断后的z-矩阵条目适当地分类:
R> zm <- chor.dens.WIN$z
R> rbow <- rainbow(200,start=0,end=5/6)
R> zm.breaks <- seq(min(zm,na.rm=TRUE),max(zm,na.rm=TRUE),length=201)
R> zm.colors <- cut(zm,breaks=zm.breaks,include.lowest=TRUE)
请再次注意,区别在于你不需要像在第 25.6.2 节中那样计算面板平均值——cut直接应用于zm。
在调用persp3d之前,值得记住的是,由于你处理的是地理区域,应该考虑x轴和y轴方向的纵横比。正如你在第 26.1 节中看到的,rgl函数中的aspect参数的作用与image中的asp参数或persp中的scale/expand参数有所不同。在rgl图形中,包括persp3d,aspect请求一个长度为 3 的数值向量,用来定义x、y和z轴的相对比例,顺序为x轴、y轴和z轴。
为了确定 Chorley-Ribble 数据的适当相对比例,你需要计算定义研究区域的总x轴和y轴宽度,并找出它们的比率。
R> xd <- chor.WIN$xrange[2]-chor.WIN$xrange[1]
R> xd
[1] 23
R> yd <- chor.WIN$yrange[2]-chor.WIN$yrange[1]
R> yd
[1] 21.38
R> xd/yd
[1] 1.075772
这是通过使用spatstat多边形的$xrange和$yrange组件完成的,分别在每种情况下从上限减去下限。最终的xd/yd比率显示出你几乎达到了 1:1 的比例,尽管从技术上讲,区域在x轴上的物理宽度比在y轴上的宽度大,大约是 1.076 倍。
考虑到这一点,你可以调用persp3d正确绘制 KDE 曲面。
R> persp3d(chor.dens.WIN$x,chor.dens.WIN$y,chor.dens.WIN$z,
col=rbow[zm.colors],aspect=c(xd/yd,1,0.75),
xlab="Eastings (km)",ylab="Northings (km)",
zlab="Kernel estimate")
使用aspect来指定x轴应该根据xd/yd的比例因子相对于y轴进行缩放,y轴被视为参考比例 1,z轴应相对于y轴按 0.75 的因子进行压缩。这个设置是任意的,使得图形与图 25-21 中在第 685 页的原始persp图形相似。
让我们通过添加一个颜色图例来完成图形的绘制。确保已经加载了shape包,调整包含最近一次调用persp3d结果的 RGL 设备大小,并执行以下命令:
bgplot3d({plot.new();
colorlegend(col=rbow,zlim=range(chor.dens.WIN$z,na.rm=TRUE),
zval=seq(0,0.02,0.0025),main="KDE",digit=4,
posx=c(0.87,0.9),posy=c(0.2,0.8))})
记住,必须使用bgplot3d来更改当前 RGL 设备的背景—请参考第 26.1 节的末尾。你可能想稍微实验一下posx和posy,以找到你偏好的颜色图例位置。图 26-7 展示了我机器上的结果。

图 26-7:一个交互式 persp3d 表示的 Chorley-Ribble 核密度估计,按z轴值着色,并带有静态颜色图例
练习 26.2
返回内置的airquality数据框中的测量值。创建一个数据框的副本,包含与温度、风速、臭氧水平和月份相关的变量;删除所有包含缺失值的行。现在,你将使用rgl对之前的均温回归模型进行可视化实验。
-
重新拟合练习 25.4 中的多元线性模型,该模型对温度与主要效应和风速与臭氧的交互效应进行了回归。使用
expand.grid和predict构造响应面对应的z矩阵;包括对拟合均值的 95%置信区间估计。然后,使用rgl功能生成响应面的交互式 3D 图,并将其着色为黄色。 -
使用内置的
topo.colors调色板,重新绘制响应面,将颜色根据z值进行分配,并设置不透明度为 80%。整理轴标题,调整 RGL 设备大小,并保持图形打开。 -
如下所示,增强(b)中的图形:
-
从一个自定义调色板中生成正好五种颜色,颜色从
"red4"到"pink",并将原始的风速、臭氧和温度观察值作为点添加到响应面图上。根据月份(5 月到 9 月)使用这五种颜色按顺序为点着色。将添加的点的大小设置为10。 -
向图形添加垂直线,表示拟合模型的残差;换句话说,每个观察值应有一条垂直线将其连接到响应曲面的相应拟合值。这些添加的线条应该使用先前的自定义调色板,以匹配每个数据点的颜色。
-
添加在 (a) 中存储的模型预测的上下 95% 置信区间。添加的曲面应为灰色,并具有 50% 的不透明度。
-
在交互式图形的右上角添加一个图例,参考按月份划分的五种颜色的点/线。使用
pch值为19,cex值为2。结果应如下所示:
![image]()
-
接下来,加载 spatstat 包并重新访问 clmfires 数据集。执行以下代码行,仅关注故意点燃的火灾,并获取地理研究区域:
R> fire <- split(clmfires)$intentional
R> firewin <- clmfires$window
-
根据以下指南,将 练习 25.5 (e) 中的静态透视图在 第 689 页 上重现为交互式透视图。然后保持该图形打开。
– 计算
fire的$x和$y坐标的 KDE 曲面,限制为firewin中的研究区域,使用 256 × 256 的评估网格。– 使用内置的颜色调色板
heat.colors,根据 z 值为表面着色。将不透明度设置为 70%。– 确保 x-y 轴具有正确的比例。然后将垂直方向的纵横比缩小为相对于 y-轴的 0.6。
– 抑制 z-轴标题,但在另外两个轴上添加整齐的
"X"和"Y"标题。 -
对图形进行以下增强:
-
添加原始观察值,使其位于表面下方。为此,将每个数据点的常量 z 值设置为 z-矩阵的最小(且非
NA)值。 -
您可以通过以下方式使用
spatstat的vertices函数获取形成研究区域的非规则多边形的 x 和 y 坐标向量:R> firepoly <- vertices(firewin)
R> fwx <- firepoly$x
R> fwy <- firepoly$y
通过将这两个向量提供给
lines3d函数的适当x和y参数,将研究区域添加到位于绘制表面下方的 x-y 平面上,以包围叠加的观察值。同样,您需要指定 z 值作为所有绘制线条的最小 z-矩阵值。设置lwd=2以绘制比默认线条稍粗的线条。你的结果应该像这样:
-

26.3 三维曲面
到目前为止,您已经看到了形式为 z = f (x, y) 的二元函数,其中您的评估网格是二维的。换句话说,您通过 x 值和 y 值来评估函数 f;x 和 y 值绘制在前两个坐标轴上,f 的值则用于绘制第三维。接下来,您将绘制 三变量 函数,它可以看作是 w = f (x, y, z)。也就是说,评估网格本身是三维的,而 f 会给您一个第四个值 w,用于绘制表面。
26.3.1 三维评估坐标
在处理三变量数学函数时,您需要一个 x、一个 y 和一个 z 值来评估结果。与平面的评估网格不同,您将拥有一个位于立方体或其他三维棱镜中的评估 格子。
作为三变量函数的完美示例,您将创建一个“颜色立方体”,其中每个点是红色、绿色和蓝色的三种值的结果——有关详细信息,请参见 第 25.1.1 节。您将使用三个物理轴来反映红色、绿色和蓝色值的评估格子,结果将在 3D 空间中的相应位置绘制该颜色的点。
以下代码设置了三个坐标方向上的评估格子:
R> reds <- seq(0,255,25)
R> reds
[1] 0 25 50 75 100 125 150 175 200 225 250
R> greens <- seq(0,255,25)
R> blues <- seq(0,255,25)
R> full.rgb <- expand.grid(reds,greens,blues)
R> nrow(full.rgb)
[1] 1331
前四行生成三种颜色在标准 0 到 255 的 RGB 整数范围内等间距递增的序列。然后,您使用内置的 expand.grid 函数根据这三个序列生成包含所有唯一颜色三元组的数据框,从而得到一个包含 11³ = 1331 个特定坐标的评估格子。请注意,expand.grid 对于高维评估网格的工作方式与对二元 x-y 网格的工作方式相同(有关详细信息,请参见 第 25.3.1 节)。
最后,调用 plot3d 在每个 3D 评估坐标点上放置球体(回想一下在 第 25.1.1 节中使用的 rgb 命令):
R> plot3d(x=full.rgb[,1],y=full.rgb[,2],z=full.rgb[,3],
col=rgb(full.rgb,maxColorValue=255),type="s",
size=1.5,xlab="Red",ylab="Green",zlab="Blue")
图 26-8 展示了从两个不同角度得到的结果,您可以看到 RGB 三元组的红色、绿色和蓝色分量的强度如何控制每个点的颜色。

图 26-8:由 rgl 创建的“颜色立方体”,该立方体作为球体,其中第四维的结果(颜色本身)是评估三变量 RGB 函数的结果。
26.3.2 等值面
在每个 3D 评估坐标点绘制单独的点球时,问题暴露出来了——图 26-8 中显示了这个问题——很难看到“内部”的球体,这个问题同样使得可视化连续三变量函数变得更加复杂。
为了解决这个问题,您可以改为生成一个 等值面,它可以被看作是等高线图的三变量类比。
使用等值面时,你选择一个特定的值层次 w = f (x, y, z),并将该层次上所有的w值在 3D 空间内连接起来形成一个形状或“块”。这些块显示了在三维空间中,三变量函数在哪些位置取到了所选的值。如果你再在不同的层次上绘制这些块,你将得到一个 3D 版本的等高线图,展示哪些层次具有最高的观测密度,正如你在第 25.4.1 节中所创建的那样。
高维概率密度
回想一下第 16.2.2 节中详细介绍的单变量正态概率密度函数。首先,我将介绍高维密度函数的概念,使用双变量版本的正态分布,然后我会进一步使用三变量版本来演示等值面绘图。
为了处理多元正态分布,你可以使用mvtnorm包,可以通过调用install.packages("mvtnorm")进行安装。与单变量正态分布的rnorm函数类似,rmvnorm函数用于从指定的多元正态分布中生成随机变量。安装了mvtnorm后,执行以下代码:
R> library("mvtnorm")
R> rand2d.norm <- rmvnorm(n=500,mean=c(0,0))
R> plot(rand2d.norm,xlab="x",ylab="y")
这产生了图 26-9 左侧的图形。rmvnorm函数用于从标准双变量正态分布中生成 500 个独立的变量。你通过传递一个数值向量给mean来将这些变量围绕坐标(0,0)进行居中处理。默认情况下,x-y坐标方向上的独立标准差分量均为 1。

图 26-9:使用mvtnorm功能查看随机生成的数据,以及它们来源的标准双变量正态密度
要实际查看双变量密度函数,你需要决定* x - y 评估网格,并像往常一样使用expand.grid构造z矩阵。以下代码在两个坐标方向上设置了一个均匀间隔的序列,并使用dmvnorm函数(这是dnorm的多元版本,给定指定坐标时提供密度函数值)来填充z*矩阵:
R> vals <- seq(-3,3,length=50)
R> xy <- expand.grid(vals,vals)
R> z <- matrix(dmvnorm(xy),50,50)
然后,你可以使用contour(或persp或persp3d)来查看用于比较的rand2d.norm数据生成的密度(限制在−3 到 3 的范围内)。以下代码行生成了图 26-9 右侧的图形:
R> contour(vals,vals,z,xlab="x",ylab="y")
基本一阶等值面
现在让我们再增加一个维度——三变量正态密度函数是什么样子的?
首先,让我们看看从这个密度生成的一些数据。以下代码再次生成 500 个随机变量:
R> rand3d.norm <- rmvnorm(n=500,mean=c(0,0,0))
R> plot3d(rand3d.norm,xlab="x",ylab="y",zlab="z")
然而,由于你将一个长度为 3 的向量作为mean参数传递给rmvnorm,该函数知道你有三个维度要处理。你告诉它,你希望数据来自一个三元正态分布,且在每个坐标方向上的均值为 0、0 和 0。你可以在图 26-10 左侧看到通过plot3d生成的数据的rgl点云。
要计算并显示生成这些数据的实际三元密度函数,你需要一个 3D 评估网格,如第 26.3 节开头所述。

图 26-10:左侧:查看从标准三元正态分布随机生成的数据。右侧:三元密度函数将被绘制的 3D 评估网格的概念图。
看一看图 26-10 右侧的图表。它显示了基于在x、y和z中跨越[−3,3]的序列生成的 3D 11 × 11 × 11 评估网格。这应该能清晰地展示如何通过增加连续函数的维度来工作。这个 11 × 11 × 11 网格中的每个交点是图 25-9 中二维 6 × 4 评估网格中的每个实线交点的三维等价物,而这个 3D 网格中的每个 10³个迷你 3D 立方体是二维面片的三维等价物,如在图 25-20 的讨论中所提到的,在第 683 页。 (与图 25-9 类似,你可以在本书的网站上找到绘制此 3D 网格的代码。)
要绘制三元函数的结果,你需要评估网格的独特评估坐标。使用vals,即之前创建的跨越-3 到 3 的值序列,以下代码会生成一个包含所有 50³ = 125,000 个独特 3D 评估网格坐标的数据框。
R> xyz <- expand.grid(vals,vals,vals)
R> nrow(xyz)
[1] 125000
然后,你使用dmvnorm来获取标准三元正态分布的数值,就像在二元正态分布中做的那样。该函数会自动识别你请求的是三元密度,因为你的数据参数xyz有三列。
w <- array(dmvnorm(xyz),c(50,50,50))
请注意,结果被适当地存储为一个 50 × 50 × 50 的 3D 数组——有关array的详细信息,请参见第 3.4 节。查看 3D 数组的概念图(图 3-3 在第 53 页)并将其与图 26-10 右侧的 3D 网格进行比较。对象w中的三元正态值显然是通过一个 3D 数值块表示的,坐落在定义的 3D 空间中的每个对应的独特评估坐标上。
可以使用contour3d函数生成等高面,该函数是misc3d包的一部分(Feng and Tierney, 2008),与rgl密切配合。要使用它,你需要决定绘制等高面时的水平(或多个水平)。对于密度图,通常根据所谓的α水平等高线来做出此选择;有关更多细节,请参阅 Scott 的权威著作《多元密度理论》(1992)。简而言之,对于某个密度f,这些水平通过将等高面绘制在多元评估格点中与密度值α × max(f)对应的位置来划定(1 − α) × 100 百分比的“最密集”观察值。
对于三元标准正态分布,密度的最大值位于坐标(0,0,0)的均值处。
R> max3d.norm <- dmvnorm(c(0,0,0),mean=c(0,0,0))
R> max3d.norm
[1] 0.06349364
在生成接下来的几个图时,你需要使用这个方法。接下来,安装misc3d,用library("misc3d")加载它,然后调用contour3d。
R> contour3d(x=vals,y=vals,z=vals,f=w,level=0.05*max3d.norm)
这将在 RGL 设备中生成一个等高面,你可以根据需要旋转和缩放它;你可以在图 26-11 的左侧看到结果。你需要为contour3d提供x、y和z作为在x、y、z坐标方向上的均匀间隔序列(在这个例子中,所有这些都是由向量vals定义的)。你还需要为函数f提供相应的 3D 数组,定义三元函数的整体结果,并将你希望绘制等高面的水平(或多个水平)传递给level。在这里,我选择了α水平,以便将分布尾部的 5%概率留在外面,这意味着 95%的总质量集中在“团块”内。

图 26-11:使用misc3d包的contour3d函数生成的三元标准正态密度函数等高面。左图:在α水平为 0.05 时绘制的独立图形。右图:将相同的等高面添加到现有的rgl图中,图中包含随机生成的三元正态观察值,且不透明度为 50%。
图形与预期相符——基于你之前生成的随机数据的图形,三元密度的形状相对清晰。然而,没有缩放的话,它不过是一个统计学上的高尔夫球。通常,将数据与其来源的密度一起查看会更有帮助,而这也是很容易做到的。以下代码使用plot3d重新绘制rand3d.norm中的数据,并再次调用contour3d以在α水平为 0.05 时绘制:
R> plot3d(rand3d.norm,xlab="x",ylab="y",zlab="z")
R> contour3d(x=vals,y=vals,z=vals,f=w,level=0.05*max3d.norm,add=TRUE,alpha=0.5)
就像传统的 R contour函数一样,如果你希望使用contour3d添加到现有的rgl图中(就像这里的情况),你需要显式指定add=TRUE。你还可以使用可选的alpha参数来调整不透明度,在这个例子中降低到 50%,以便“查看”密度等高面的内部。
通过颜色和不透明度控制多个水平
玩弄透明度特别有用,特别是在你想要同时绘制多个α水平的等值面时。颜色也在这方面非常有用,作为一个变量,可以表示第四维,而无需在图形中添加额外的物理轴。
要在多个层次上查看三变量正态密度,请执行以下代码生成的图形:
R> plot3d(rand3d.norm,xlab="x",ylab="y",zlab="z")
R> contour3d(x=vals,y=vals,z=vals,f=w,
level=c(0.05,0.2,0.6,0.95)*max3d.norm,
color=c("pink","green","blue","red"),
alpha=c(0.1,0.2,0.4,0.9),add=TRUE)
图 26-12 显示了结果。在这里,你重新绘制了 500 个随机生成的三变量正态观测数据,另一个contour3d调用现在在三变量密度的四个特定α水平(0.05、0.2、0.6 和 0.95)上绘制了等高线。你使用可选的color参数将这些等高线分别渲染为粉色、绿色、蓝色和红色,并通过alpha参数逐步增加每个水平的透明度。

图 26-12:在四个不同的水平上绘制的三变量正态密度等值面,覆盖在随机生成的观测数据上。使用颜色和透明度来区分绘制函数的不同数值水平。
你应该能够看到,在这个分布的三维空间中,你可以通过类似于使用标准二维等高线来评估双变量观测分布的方式,来衡量点密度的增加。
26.3.3 示例:非参数三变量密度
要查看使用真实数据的扩展示例,请再次查看内置的quakes数据框,它包含了 1000 次地震事件的空间位置、震级和深度。
在第 25.4.1 节,你使用MASS函数kde2D构建了二维经度-纬度空间坐标的双变量核密度估计。如那里所述,KDE 自然扩展到更高维度。现在的目标是计算并可视化相同空间地震数据的密度估计,但这次基于三变量坐标——经度、纬度和深度,在三维空间中进行。
原始数据
首先,让我们看一下原始观测数据。以下代码创建了quakes数据的副本,提取了三个变量并将depth设置为负值。我这样做是为了在绘制时,使得地震深度对应于沿垂直轴向下移动,给人一种深度低于海平面的印象。
R> quak <- quakes[,c("long","lat","depth")]
R> quak$depth <- -quak$depth
以通常的rgl方式,你可以通过以下代码创建原始数据的点云:
R> plot3d(x=quak$long,y=quak$lat,z=quak$depth,
xlab="Longitude",ylab="Latitude",zlab="Depth")
图 26-13 显示了结果。如果你旋转图形,使得你直接从上方俯视,你将认出你已经绘制的二维空间模式;例如,可以参见图 13-1(第 265 页)、图 23-1(第 578 页)或图 25-12(第 662 页)。

图 26-13:查看地震事件的三维空间分布——纬度、经度和深度
计算 3D 估计
该核估计的评估网格将由经度-纬度-深度数据所在的整个 3D 空间定义,正如第 26.3.2 节中所示。
要实际计算quak数据的 3D KDE 表面,你将使用贡献包ks(Duong, 2007)的强大功能。安装该包并通过调用library("ks")加载它。ks包中的kde函数允许你使用核平滑来估计 1D 到 6D 数据的概率密度。
你传递给kde的第一个参数是你的数据,形式为矩阵或数据框,并带有标签x。注意,在使用kde时,数据对象中列的顺序非常重要。当与quak一起调用时,并考虑到之前代码中创建quak时提取的三个变量的顺序,结果 3D 核估计中的x、y和z坐标轴将分别对应经度、纬度和深度。
R> quak.dens3d <- kde(x=quak,gridsize=c(64,64,64),compute.cont=TRUE)
这与图 26-13 中显示数据的方式一致。gridsize参数指定每个轴的网格分辨率。在这个例子中,我选择了 64 × 64 × 64 的网格;默认情况下,kde会选择每个坐标方向上略宽于观测数据的评估范围。最后,为了绘制结果,指定参数compute.cont=TRUE也是有用的;稍后我会解释这样做的原因。
返回的对象有多个组成部分。3D 估计作为适当大小的数组通过$estimate成员提供;如果你想检查,执行以下代码行可以确认它与期望的网格分辨率匹配:
R> dim(quak.dens3d$estimate)
[1] 64 64 64
$eval.points组件包含一个列表,其成员是特定的评估坐标,这些坐标在三个轴上是等间距的序列。成员的数量反映了问题的维度,其顺序对应于特定轴。你可以通过以下代码提取它们:
R> x.latt <- quak.dens3d$eval.points[[1]]
R> y.latt <- quak.dens3d$eval.points[[2]]
R> z.latt <- quak.dens3d$eval.points[[3]]
如果你将这些向量打印到控制台屏幕上,你会看到每个向量的长度为 64,x.latt、y.latt和z.latt对应于与数据框quak中列的顺序匹配的变量。
等值面级别选择
选择显示的级别取决于构成三变量函数结果的值的范围。当你在调用kde时选择compute.cont=TRUE,你会自动获得一组适当的级别。这些级别在$cont组件中作为一个长度恰好为 99 的数值向量返回,表示从 1%到 99%的每个整数。
在内部,这些水平是通过计算每个原始观察数据点位置上三变量函数的结果,然后使用quantile函数获得这些密度值的所有整数百分位数(从 99 百分位到 1 百分位)(有关分位数的复习,请参见第 13.2.3 节)。这些值按降序返回;换句话说,quak.dens3d$cont[1]对应于第 99 百分位,quak.dens3d$cont[99]对应于第 1 百分位。
尽管这些值是通过与绘制三变量正态密度时实验的α水平不同的方式获得的,但当你可视化结果时,本质上你会得到相同的解释——这些值允许你在所需的估计观察“密度”水平上绘制等值面。例如,较低四分位数(即第 25 百分位数)可以通过以下方式提取:
R> quak.dens3d$cont[75]
25%
2.002741e-05
这提供了 KDE 三变量函数的值,该值估算出将最空间分散的 25%的观察与其余部分区分开来的值(换句话说,使得结果中的斑块包含了最密集的 75%的数据)。
注意
在撰写时, rgl 和 misc3d 包是 ks 的依赖项。这意味着当你加载 ks 时,它们会自动加载,因此在这种情况下,你不需要显式调用 library("rgl") 或 library("misc3d") , plot3d 和 contour3d 已经可以使用。随着开发者更新其包,这种情况可能会有所变化。
当你执行以下代码时,它首先重新绘制了基于密度估计的quak数据,然后使用下四分位点的密度值作为所需的水平,添加了相应的等值面。你可以在图 26-14 的左侧看到结果。
R> plot3d(x=quak$long,y=quak$lat,z=quak$depth,
xlab="Longitude",ylab="Latitude",zlab="Depth")
R> contour3d(x=x.latt,y=y.latt,z=z.latt,f=quak.dens3d$estimate,
color="blue",level=quak.dens3d$cont[75],add=TRUE)

图 26-14:基于使用贡献的 kde 和 contour3d 函数绘制的三变量核密度估计的等值面(3D 等高线图),在特定点密度分位数处。左侧:蓝色实线表示下四分位数——最分散的 25%点。右侧:绿色表示中位数——最分散的 50%与最密集的 50%之间的分界线,透明度减半。
看着图像,可以清楚地看到代表指定水平的 3D 等高线的蓝色斑块。三变量函数的较高水平,即密集分布的点,位于这些斑块的“内部”。换句话说,蓝色形状包含了与纬度、经度和深度相关的估计密度的最高 75%观察数据。要查看等值面内部,你可以通过调整透明度alpha来实现。
让我们取出划定下半部分和上半部分 50%估计密度值所对应观察的水平。
R> quak.dens3d$cont[50]
50%
3.649565e-05
然后,重新运行plot3d调用以重新绘制原始quak数据。之后,调用contour3d产生图 26-14 右侧的结果,让你能够透过绿色斑块看到更多信息。
R> contour3d(x=x.latt,y=y.latt,z=z.latt,f=quak.dens3d$estimate,
color="green",level=quak.dens3d$cont[50],add=TRUE,alpha=0.5)
最后,你将使用多个层次突出显示最密集聚集的前 80%的观测数据。执行以下操作:
R> qlevels <- quak.dens3d$cont[c(80,60,40,20)]
R> qlevels
20% 40% 60% 80%
1.771214e-05 2.964305e-05 4.249407e-05 9.543976e-05
这将得到四个层次——确定最密集聚集观测数据的 80%、60%、40%和 20%的分位数。然后,设置几个向量来控制每个密度层次的颜色和透明度。
R> qcols <- c("yellow","orange","red","red4")
R> qalpha <- c(0.2,0.3,0.4,0.5)
颜色和透明度的范围意味着等值面在密度增加时会变暗并变得更加不透明。
最后一次,像之前一样使用plot3d重新绘制原始quak数据。然后,只需将长度为 4 的向量传递给contour3d中的每个相应参数。
R> contour3d(x=x.latt,y=y.latt,z=z.latt,f=quak.dens3d$estimate,
color=qcols,level=qlevels,add=TRUE,alpha=qalpha)
图 26-15 展示了结果。你可以看到,地震的最紧密聚集出现在非常深的位置,且位于 3D 空间棱柱的东部边缘(可见的“三室”密度斑块是这些特定数据的一个著名特征)。
26.4 处理参数方程
到目前为止,本章中的大多数例子中,曲面是由常规评估网格或格点的坐标直接定义的,但也有一些情况,你希望可视化的最终轴不是某个评估网格的函数。这种情况在你只是想绘制常见几何形状时非常自然,但也扩展到更复杂的数学情境。
在本节中,你将从一组参数方程中绘图,这些方程共同定义了感兴趣的形状或曲面。本节假设你熟悉基本的三角函数正弦和余弦,以及角度从度到弧度的转换,因为默认情况下,R 只处理后者。话虽如此,我会在需要时引导你完成相关的计算和 R 代码。
26.4.1 简单轨迹
使用数学术语,轨迹(复数形式为loci)是满足并由特定参数方程定义的一组点。在 R 中,这些方程决定了结果对象中每个数字元素的计算方式,随后你可以使用熟悉的函数轻松地绘制它们。
注意
在讨论轨迹时,任何关于二维或三维空间的引用都指的是欧几里得空间,这就是你迄今为止处理 x、 y* 和* z轴坐标的标准方式。

图 26-15:地震观测的三维核密度估计的三个截图,分别从不同角度和不同的缩放级别拍摄。密度增加的层次通过等值面的黄色到红色变暗颜色和更高的透明度反映出来。
二维圆
让我们从一个简单的例子开始。通过这种方式定义的最易识别的形状之一是二维圆。要找到圆上的任何一点,您需要知道圆心位置及其半径,并提供一个特定的角度(通常相对于水平线)。位于圆上的任何平面二维点 (x,y) 都可以通过以下方程表示,如果您将圆心设为坐标 (a, b),半径为 r > 0,并且查看角度 θ:

如果您使用的是度数,则严格来说 0 ≤ θ < 360;要转换为弧度,必须乘以 π/180,使得 0 ≤ θ < 2π。
要根据(26.1)中的方程绘制圆,首先确定半径,然后确定圆心位置,再生成相应的 x 和 y 值。请参见以下代码:
R> radius <- 3
R> a <- 1
R> b <- -4.4
R> angle <- 0:360*(pi/180)
R> x <- a+radius*cos(angle)
R> y <- b+radius*sin(angle)
R> plot(x,y,ann=FALSE)
R> abline(v=a)
R> abline(h=b)
该圆的半径为 3,中心在 (1,−4.4)。给定定义为 angle 的序列,注意该图会在从 0 到 360 度的每个整数角度上放置一个点——我已将上限设置为恰好 360,以完整地完成旋转——然后将其转换为弧度(通过 π/180 的乘法),以便使用内置的 R 函数 cos 和 sin。圆周率的几何值 (π = 3.1415...) 存储在 R 中可直接使用的对象 pi 中(请参见帮助文件 ?Constants)。最后三行执行绘图,结果如图 26-16 所示。

图 26-16:在 R 中绘制中心为 (1,−4.4) 且半径为 3 的二维圆,遵循相关的参量方程
这里的关键要点是,y 不是直接由 x 计算得出的,就像在绘制例如线性回归模型时,您可能会获得一个均匀间隔递增的 x 序列,然后计算出相应的 y。相反,方程(26.1)联合定义了二维空间中轨迹的规则。
3D 圆柱体
绘制三维曲面的方法基本相同,只是现在您的方程式定义了在 x、y 和 z 轴上所有满足条件的点的规则。
例如,位于空心圆柱上的点可以通过以下方程定义:

要实际绘制满足这些规则的点,您需要决定一个固定的半径 r,并认识到 0 ≤ θ < 360(以度为单位),同时定义一个固定的最大高度 h,以确保 0 ≤ z ≤ h。根据这些信息,为了生成 x、y 和 z 的向量,您需要首先设置数值序列,涵盖 θ 和 z 的可能值。请参见以下代码:
R> r <- 3
R> h <- 10
R> zseq <- 0:h
R> theta <- 0:360*(pi/180)
这些行展示了半径设置为 3(r)和最大高度设置为 10(h)。z 的序列设置为 zseq 中从 0 到 10 的 11 个整数值——这将允许你在每个定义的 z 值上放置点。θ 的序列设置为 0 ≤ θ < 2π(在 theta 中,注意需要转换为弧度)。接下来,你需要这些参数值的所有唯一组合,以获取绘图所需的所有相关 (x, y, z) 坐标。你可以通过 第 25.3.1 节 使用 expand.grid 来做到这一点。
R> ztheta <- expand.grid(zseq,theta)
R> nrow(ztheta)
[1] 3971
调用 nrow 函数查看结果,表明你现在有 11 × 361 = 3971 个独特的高度-角度值。现在,你可以根据公式 (26.2) 生成 x、y 和 z 的值。你可以使用 for 循环(见 第 10.2.1 节),逐行遍历 ztheta,但更简洁的方式是使用 apply 中的隐式循环(详细内容参见 第 10.2.3 节)。
R> x <- apply(ztheta,1,function(vec) r*cos(vec[2]))
R> y <- apply(ztheta,1,function(vec) r*sin(vec[2]))
R> z <- apply(ztheta,1,function(vec) vec[1])
请注意,使用了可丢弃函数(参见 第 11.3.2 节),这些函数用于对 ztheta 每一行组成的两个元素的高度-角度向量进行操作。
你可以使用 rgl 中的 persp3d 来绘制这种参数化定义的曲面,但方式与本章前面的部分有所不同。现在,计算出的 x、y 和 z 坐标必须作为大小相同、适当排列的矩阵一起提供。这是因为 x 和 y 坐标方向上不再有均匀间隔的评估网格——连同 z 值,x 和 y 值已经通过应用 (26.2) 进行定义。在这种类型的图中,你实际上拥有一个由参数值(在此为高度和角度)的独特组合定义的 潜在 评估网格。
所有的 x、y 和 z 坐标矩阵由三个 11 × 361 的矩阵组成,这些矩阵按典型的列优先方式填充了 x、y 和 z 值。
R> xm <- matrix(x,length(zseq),length(theta))
R> ym <- matrix(y,length(zseq),length(theta))
R> zm <- matrix(z,length(zseq),length(theta))
此时,值得介绍内置的 outer 函数,它接受两个变量的值序列,生成所有独特的值组合,在每个组合上计算结果,并将结果返回为矩阵——一次性完成了 expand.grid、apply 和 matrix 三个步骤。使用这种方法,你可以通过简单地调用以下代码来创建相同的 xm、ym 和 zm:
R> xm <- outer(zseq,theta,function(z,t) r*cos(t))
R> ym <- outer(zseq,theta,function(z,t) r*sin(t))
R> zm <- outer(zseq,theta,function(z,t) z)
唯一的区别是,作为第三个参数传递的匿名函数必须明确地定义为两个单独的参数,这些参数表示必要的高度和角度参数的值。
无论你如何获得 xm、ym 和 zm,现在只需要调用 persp3d 并传入这些坐标矩阵。再调用一次 points3d,以强调这些矩阵中返回的精确评估点。接下来的两行代码的结果显示在 图 26-17 的左侧。
R> persp3d(x=xm,y=ym,z=zm,col="red")
R> points3d(x=xm,y=ym,z=zm)

图 26-17:使用 persp3d 绘制圆柱体和圆锥体,矩阵参数在三个坐标方向上。轨迹由对应的参数方程定义。圆柱体上可见的黑色环代表存储在所需矩阵xm、ym和zm中的实际评估点。
3D 圆锥体
下一个示例将展示,一旦你理解了设置 x、y 和 z 坐标矩阵的过程,你就可以轻松显示几乎任何 3D 形状或表面。将 r、h 和 θ 分别视为底面半径、最大高度和角度,一个圆锥体遵循以下方程:

使用之前的相同对象 r、h、zseq 和 theta,以下代码会修改 outer 中的可用函数,以反映(26.3)。图 26-17 右侧显示了结果。
R> xm <- outer(zseq,theta,function(z,t) (h-z)/h*r*cos(t))
R> ym <- outer(zseq,theta,function(z,t) (h-z)/h*r*sin(t))
R> zm <- outer(zseq,theta,function(z,t) z)
R> persp3d(x=xm,y=ym,z=zm,col="green")
26.4.2 数学抽象
数学的许多领域、应用数学建模和统计学都利用高维形状。为了总结这一章,实际上也是整本书的总结,让我们使用rgl来通过第 26.4.1 节中的技能,观察一些著名的抽象概念。
莫比乌斯带
一个经典的例子是 莫比乌斯带 ——一个只有一个面和一条边的连续表面。它可以通过参数方程表示:

其中

其中 −1 ≤ v ≤ 1 且 0 ≤ θ < 2π(假设角度以弧度为单位)。参数 v 控制点在带宽上的位置,θ 控制旋转角度。
你可以像之前绘制圆柱体和圆锥体一样绘制莫比乌斯带。首先,设置 v 和 θ 的可能值序列,这里设置的分辨率为 200:
R> res <- 200
R> vseq <- seq(-1,1,length=res)
R> theta <- seq(0,2*pi,length=res)
接下来,使用 outer 获取每个 x、y 和 z 坐标的 200 × 200 矩阵,如(26.4)所示。
R> xm <- outer(vseq,theta,function(v,t) (1+v/2*cos(t/2))*cos(t))
R> ym <- outer(vseq,theta,function(v,t) (1+v/2*cos(t/2))*sin(t))
R> zm <- outer(vseq,theta,function(v,t) v/2*sin(t/2))
然后,快速调用 rgl 包中的 plot3d 函数,可以展示基于定义的 vseq 和 theta 序列的 40,000 个点,这些点位于莫比乌斯带上。以下代码的结果显示在图 26-18 的左侧:
R> plot3d(x=xm,y=ym,z=zm)
让我们使用 persp3d 将莫比乌斯带显示为一个连续表面,以充分理解其一面一边的现象。图 26-18 右侧显示了以下代码的结果:
R> persp3d(x=xm,y=ym,z=zm,col="orange",axes=FALSE,xlab="",ylab="",zlab="")
请注意使用 axes 来抑制默认的框和坐标轴,并使用空字符串来移除默认的坐标轴标题,表示 xm、ym 和 zm。

图 26-18:在 R 中绘制莫比乌斯带。左:在带上的具体计算点,通过 plot3d 可视化。右:通过 persp3d 连接左侧的点形成的表面。
你也可以以更有趣的方式使用颜色来强调莫比乌斯环的环绕特性。受第 25.1.3 节中类似颜色集的启发,创建以下自定义调色板:
R> patriot.colors <- colorRampPalette(c("red4","red","white","blue",
"white","red","red4"))
该调色板特别生成了从深红色到白色再到蓝色的过渡,并且还会从蓝色回到白色和深红色,形成环绕效果。这是因为patriot.colors中的颜色将按点逐一分配给绘制的条带。
为了绘制表面,颜色向量的长度需要为 200² = 40,000,考虑到预设的res值(决定了vseq和theta的长度)。为了填充该向量,执行以下操作:
R> patcols <- patriot.colors(2*res-1)
R> stripcols <- rep(NA,res²)
R> for(i in 0:(res-1)){
+ stripcols[1:res+res*i] <- patcols[1:res+i]
+ }
第一行生成了来自patriot.colors的 399 种颜色,第二行设置了所需长度的向量,用于存储分配的颜色(stripcols)。for循环确保在第一次迭代时,将stripcols中第 1 到 200 个元素分配给patcols中的第 1 到 200 种颜色;在第二次迭代时,将stripcols中第 201 到 400 个元素分配给patcols中的第 2 到 201 种颜色,以此类推。这样就实现了颜色的环绕效果。
为了正确理解for循环,首先查看传递给outer函数的参数顺序。将vseq放在第一位,theta放在第二位,意味着结果矩阵的每一列(长度为 200)对应于在v的范围从−1 到 1 的跨度,这指的是从某一行点的一端移动到另一端,也就是沿着条带的宽度。通过使用一个从0到199(包括199)的索引变量i,循环将stripcols中每一个连续的 200 个元素(在每次迭代中通过+res*i增加)分配给 399 个patcols中的 200 个元素,方法是每次迭代时前进一个元素(通过+i增加)。这样做的结果是,循环早期绘制的点线条中颜色从红色变为白色再变为蓝色,但随着循环的进行,颜色逐渐环绕条带,直到在最后几行绘制的点线条中颜色从蓝色变为白色再变为红色。其效果是随着v和θ的变化,颜色平滑地过渡。你可以在图 26-19 中看到以下结果:
R> persp3d(x=xm,y=ym,z=zm,col=stripcols,aspect=c(2,2.5,1.5),axes=FALSE,
xlab="",ylab="",zlab="")

图 26-19:一条爱国的莫比乌斯环,通过精心构建合适的颜色向量创建。
该绘图仍然可以像往常一样在你的电脑上进行旋转和缩放。你可以尝试调整aspect,修改特定轴的长宽比,以增强最终效果;在这里,我已将x轴和y轴相对于z轴进行了拉宽。
环面
另一个常见的三维空间形状是环形圆环(复数形式为tori)。这是经典的拓扑学“有一个孔的形状”,像是一个更通俗的说法——甜甜圈。圆环的数学性质在许多领域都非常有用。
圆环的参数化可以通过以下方程实现:

在哪里
F(θ[2]; α, β) = β + α cos θ[2]
其中 0 ≤ θ[1] < 2π 且 θ[2] 同理(假设角度以弧度为单位)。固定值α和β控制“管道”的半径(换句话说,就是甜甜圈的相对厚度)以及圆环的整体大小,即从孔的中间到管道中间的距离。假设α < β,公式(26.5)给你提供了经典的环形圆环形状;你可以通过放宽α和β的条件得到不同类型的圆环。
将α = 1 和 β = 2,以下代码使用先前定义的theta对象计算 Möbius 带在 x、y 和 z 坐标方向上的矩阵,如公式(26.5)所示:
R> alpha <- 1
R> beta <- 2
R> xm <- outer(theta,theta,function(t1,t2) (beta+alpha*cos(t2))*cos(t1))
R> ym <- outer(theta,theta,function(t1,t2) (beta+alpha*cos(t2))*sin(t1))
R> zm <- outer(theta,theta,function(t1,t2) alpha*sin(t2))
如果需要,参考第 26.4.1 节来提醒自己如何使用outer。
然后,这一行展示了圆环的计算点:
R> plot3d(x=xm,y=ym,z=zm)
这样,你就得到了连续表面的最终外观:
R> persp3d(x=xm,y=ym,z=zm,col="seagreen4",axes=FALSE,xlab="",ylab="",zlab="")
图 26-20 展示了两者的结果。

图 26-20:在 R 中绘制圆环。左图:在表面上特别计算的点,通过 plot3d* 可视化。右图:通过使用* persp3d* 将左侧的点连接起来形成的形状。
之前,你使用了一个特意构建的颜色向量来给 Möbius 带着色,但你也可以通过识别定义矩阵中你想要控制的特定点,给任何这样的表面赋色。由于这是书中的最后一个例子,让我们在当前的数学“甜甜圈”上稍微玩一下,通过这个点状索引来说明这一点。
首先,甜甜圈需要看起来真实。以下代码行设置了一个长度为 200² = 40,000 的向量,用来存储你将要使用的颜色。最初,每个元素都设置为甜甜圈的颜色——棕黄色。
R> donutcols <- rep("tan",res²)
接下来,加点装饰。如果你查看点的分布(如图 26-20 左侧的图所示),你可以看到这个圆环表面上“上半部分”的绘制位置的z坐标大于零。基于这一点,你可以使用以下一行代码覆盖donutcols中相关的元素:
R> donutcols[as.vector(zm)>0] <- "pink"
最后,任何优质的甜甜圈都应该撒上糖粒。你需要一种机制来识别表面上半部分的随机位置,并为其着色。为此,你可以使用内置的sample函数从现有向量中随机选择一部分元素。例如,如果你有整数 1 到 10,并且想要随机选择四个,可以执行以下操作:
R> sample(x=1:10,size=4)
[1] 8 9 2 6
参数x接受一个向量,用于从中选择样本,size则是你希望从该向量中抽取的元素数量。注意,当执行这一行代码时,你很可能会得到一组不同的四个随机数。
有了这些知识,你可以使用以下代码制作撒花:
R> sprinkles <- c("blue","green","red","violet","yellow")
R> donutcols[sample(x=which(as.vector(zm)>0),size=300)] <- sprinkles
这段代码设置了五种不同的撒花颜色;然后从冰面区域的 300 个位置中随机选择位置;最后将五种颜色分配给这些位置。由于向量回收的特性,每种颜色的撒花数量将精确为 60,随机分布在环面的上半部分。你可以通过增加size来增加撒花数量,不过鉴于颜色数量,你应该确保size能被 5 整除。
以下调用完成了视觉效果,如图 26-21 所示:
R> persp3d(xm,ym,zm,col=donutcols,aspect=c(1,1,0.4),axes=FALSE,
xlab="",ylab="",zlab="")

图 26-21:美味的数学甜甜圈。环面表面的着色是通过识别颜色向量中对应位置并替换元素来实现的。
你可以在 Davies 和 Bryant(2013)中找到一个更为严谨(且技术性更强)的环面可视化,环面作为一种便捷的计算结构,用于生成特别定义的高维随机正态变量。
练习 26.3
确保在当前 R 会话中可以使用mvtnorm、rgl、misc3d和ks包的功能。通过指定不同的协方差矩阵,你可以控制多元正态随机变量的各个成分之间的关系,从而影响分布本身的外观。例如,在标准的三元正态密度中,三个元素(x、y、z)彼此是独立的。执行以下代码将生成 1000 个观测值,这些值来自一个非标准的三元正态分布,其中三个元素按特定方式相互关联:
R> covmat <- matrix(c(1,0.8,0.4,0.8,1,0.6,0.4,0.6,1),3,3)
R> rand3d.norm <- rmvnorm(1000,mean=c(0,0,0),sigma=covmat)
请注意,协方差矩阵covmat作为可选参数传递给sigma,并且这些点的均值仍然保持在(0,0,0)的中心位置。
-
将生成的数据绘制为交互式 3D 点云,轴标签为
"x"、"y"和"z"。你应该能够看到点云形成一个椭圆形,这与标准三元正态分布中的球形分布(见图 26-11 和 26-12)形成对比。保持图形窗口开启。 -
使用在每个轴上从−3 到 3 的 50 × 50 × 50 评估网格,使用
dmvnorm计算此三变量正态密度函数并将其存储为适当大小的数组。请注意,你还需要在使用dmvnorm时设置σ=covmat。计算密度的最大值,并利用这个最大值在点云等值面上叠加三个特定的α水平——0.1、0.5 和 0.9。将这三个等值面分别着色为"yellow"、"seagreen4"和"navyblue",并设置它们的透明度分别为 20%、40%和 60%。 -
现在,使用
ks功能基于 1000 个生成的观察数据计算 3D 核密度估计。确保返回的对象包含 99 个合理的等值层级向量。重新绘制(a)部分的点云图,在一个新的 RGL 设备中显示,然后按照以下方式调用contour3d两次。-
第一个应该仅叠加(b)部分的α水平为 0.5 的理论等值面。使用和(b)中相同的颜色和透明度。
-
第二个应该绘制从点特定 KDE 曲面估算的 50 百分位的等值面。将其设置为红色,并将透明度降低至 20%。
这是我在(b)部分的结果,左侧是(b)的结果,右侧是(c)的结果。请注意,由于 1000 个数据点是随机生成的,KDE 等值面(isosurface)的外观会有所不同,这些数据点决定了最终的估计结果。
-

MASS包中有另一个数据集你还没有接触过。Boston数据框对象包含了关于 1970 年代美国马萨诸塞州波士顿郊区房价的许多描述性观察数据(Harrison 和 Rubinfeld, 1978)。加载MASS包并查看帮助文件?Boston,了解当前的变量。
-
关注变量中平均房间数、低社会经济地位住房的百分比和中位房价——你将进行如下的 3D 散点图可视化实验:
-
使用
rgl功能绘制三个变量,分别在x、y、z轴上绘制房间数、状态和价值;提供整洁的轴标题。数据点应以灰色球体表示,大小为 0.5。保持该图像打开。 -
使用
ks功能估算这些数据的三变量密度函数。基于 64 × 64 × 64 的评估网格进行估算;确保返回 99 个观察特定密度水平的整数百分位数。使用绿色、黄色和蓝色绘制出分别表示 75 百分位、50 百分位和 10 百分位“最密集”观察的等值面轮廓。设置透明度分别为 10%、40%和 50%。 -
最后,在下方的z轴、上方的x轴和上方的y轴位置添加参考网格。
-
-
解释(d)部分的最终图形。例如,当前变量的哪些值倾向于表征波士顿郊区最常见类型的房屋?
结果如下:
![image]()
umbilic torus 是数学中另一个有趣的 3D 形状,可以通过以下参数方程定义:
x = sin(θ)F(θ, φ)
y = cos(θ)F(θ, φ)
z = sin(θ/3 − 2φ) + 2sin(θ/3 + φ)
在这些方程中,F(θ, φ) = 7 + cos(θ/3 − 2φ) + 2 cos(θ/3 + φ),并且你允许 −π ≤ θ ≤ π 和 −π ≤ φ ≤ π。
-
使用长度为 1000 的θ和φ序列,以及从内置的
rainbow调色板生成的 1000 种颜色,直接分配给col参数,生成一个交互式的 umbilic torus 3D 图。去掉盒子、坐标轴和坐标轴标题。在从不同角度查看物体时,请注意,与之前绘制的莫比乌斯带类似,这个形状只有一条边。结果如下:
![image]()
本章的重要代码
| 函数/运算符 | 简要描述 | 首次出现 |
|---|---|---|
plot3d |
交互式 3D 点云 | 第 26.1.1 节,第 692 页 |
legend3d |
添加 RGL 设备图例 | 第 26.1.2 节,第 693 页 |
bg3d |
重置 RGL 设备背景 | 第 26.1.2 节,第 693 页 |
segments3d |
添加 3D 线段 | 第 26.1.3 节,第 696 页 |
grid3d |
添加平面网格 | 第 26.1.3 节,第 696 页 |
bgplot3d |
更改/重新绘制 RGL 设备背景 | 第 26.1.3 节,第 696 页 |
persp3d |
交互式 3D 透视面 | 第 26.2.1 节,第 700 页 |
points3d |
添加 3D 点 | 第 26.2.2 节,第 701 页 |
text3d |
添加 3D 文本 | 第 26.2.2 节,第 702 页 |
rmvnorm |
随机多元正态变量 | 第 26.3.2 节,第 711 页 |
dmvnorm |
多元正态密度 | 第 26.3.2 节,第 711 页 |
contour3d |
绘制等值面 | 第 26.3.2 节,第 713 页 |
kde |
多元核估计 | 第 26.3.3 节,第 717 页 |
pi |
几何常数 π | 第 26.4.1 节,第 722 页 |
sin, cos |
正弦和余弦 | 第 26.4.1 节,第 722 页 |
outer |
外积运算 | 第 26.4.1 节,第 724 页 |
sample |
从向量中随机抽样 | 第 26.4.2 节,第 731 页 |
第二十七章:A
安装 R 和贡献包

本附录提供了更多关于如何找到 R 以及如何安装 R 和其贡献包的详细信息。R 可以通过 CRAN(全面的 R 存档网络)获取,通过 R 网站访问,网址为 www.r-project.org/。这里我只讲解基础内容,但你可以在 Hornik(2015)的 R 常见问题解答中找到大量信息,网址为 CRAN.R-project.org/doc/FAQ/R-FAQ.html。如果你在安装 R 及其包时需要帮助,应该首先访问这个页面。R 和其贡献包的安装在常见问题解答的第二部分和第五部分中有详细说明。
A.1 下载和安装 R
进入 R 网站后,点击欢迎文本中的 CRAN 镜像 链接,或点击左侧“下载”下的 CRAN 链接,如图 A-1 所示,页面会加载并要求你选择一个 CRAN 镜像。

图 A-1:R 的主页
选择一个靠近你地理位置的镜像站点并点击链接。图 A-2 展示了我本地的镜像,位于奥克兰大学;你的镜像站点会类似。

图 A-2:一个 CRAN 镜像站点。这是你将找到各种下载链接的地方。
然后点击与你操作系统相对应的链接。
• 如果你是 Windows 用户,点击 Windows 链接,在该页面选择安装文件(即二进制可执行文件)以安装 base 版本。双击该可执行文件,按照安装向导中的说明进行操作。你需要选择适合当前 Windows 安装的 32 位或 64 位版本——你可以通过进入控制面板 → 系统来查看你的版本。
• 如果你是 Mac 用户,点击 Mac OS X 链接,你将被带到一个包含预打包二进制文件的页面。写这篇文章时,仍然有两个版本可供下载:一个适用于 OS X 10.9(Mavericks)及更高版本,另一个适用于 OS X 10.6 至 10.8(此文件带有 snowleopard 标记),虽然对 Snow Leopard 的支持正在逐步取消。下载适合你操作系统的文件。下载完成后,双击该文件将立即启动安装程序;按照其中的说明进行操作。我还推荐安装 XQuartz 窗口系统,可以从 xquartz.macosforge.org/ 免费下载,它为图形设备提供支持。
• Linux 用户将被带到一个子目录,里面有按操作系统名称命名的文件夹,例如 Debian 或 Ubuntu。点击与你的系统相关的链接,你将被带到一个页面,提供逐步的命令行安装 R 的说明。
A.2 使用包
R 包(或 库)是包含在 R 中使用的代码、数据和功能的集合。熟悉加载这些库是非常必要的,这样才能访问某些特性和命令。
包有三种类型。构成软件核心功能的那些随安装一起提供,并且在你打开 R 时会自动加载。另外,一些 推荐的 包也随典型的 R 安装一起提供,但不会自动加载。最后,一个庞大的用户贡献包集合——在写作时已有超过 7000 个——极大地扩展了 R 的应用。
A.2.1 基础包
基础包 提供了编程、计算和图形生成的基本语法和命令,以及内置数据集、基本算术运算和统计功能,并且在你启动 R 时立即可用。写作时,总共有 14 个。
base compiler datasets grDevices graphics grid methods
parallel splines stats stats4 tlctk tools utils
你可以在 Kurt Hornik 的 R FAQ 中的 第 5.1.1 节 中找到每个基础包的简要描述。
A.2.2 推荐的包
在写作时,有 15 个 推荐的包,如 R FAQ 第 5.1.2 节 中所述。这些包包含在任何标准 R 安装中,并扩展了基础包的功能,包含了稍微更专业(但仍然广泛使用)的统计方法和计算工具。在本书中,你只会使用来自该列表的 MASS 和 boot。
KernSmooth MASS Matrix boot class cluster
codetools foreign lattice mgcv nlme nnet
rpart spatial survival
这些推荐包 不会 自动加载。如果你想访问这些包中的函数或数据集,必须手动加载它们,可以使用 library 命令。例如,要访问作为 MASS 部分提供的数据集,可以在提示符下执行以下命令:
R> library("MASS")
一些包在加载时会提供简短的欢迎信息,R 总是会通知你发生的任何遮蔽(参见 第 12.3.1 节)。
当你关闭当前的 R 会话时,包也会关闭,因此如果你打开另一个 R 实例并希望再次使用该包,则需要重新加载它。如果你决定在某个会话中不再需要某个包,并希望卸载它,以避免例如遮蔽问题,可以使用 detach 命令,如下所示:
R> detach("package:MASS",unload=TRUE)
你可以在本书的 第 9.1 节 和 第 12.3.1 节 中找到有关包加载和卸载的主题和技术细节。
A.2.3 贡献的包
除了这些内置和推荐的包之外,还有一个通过 CRAN 提供的大量用户贡献的包集合,涵盖了统计学与数学、计算以及图形学等各类应用。如果你访问本地的 CRAN 镜像站点,页面左侧的 Packages 链接(见图 A-2)会引导你到一个页面,提供更新的所有可用包的列表。你还会找到有用的 CRAN 任务视图网页,这是一个集合了各个主题的文章,概述了相关的包,如图 A-3 所示。这是熟悉 R 中各种专门化分析方法的一个很好的途径。
由于可用包的数量庞大,R 在安装时自然不会包含所有包,作为研究人员,你只会对某一时刻的相对小部分方法感兴趣。

图 A-3:CRAN 任务视图网页。每篇文章讨论了该领域中常用的 CRAN 包。
本书中你将使用一些贡献的包。它们中的一些用于访问特定的数据集或对象,其他的则用于其独特的功能或说明统计方法。这些包列在这里:
car faraway GGally ggplot2 ggvis
gridExtra ks misc3d mvtnorm rgl
scatterplot3d shape spatstat tseries
当你需要访问任何贡献的包时,你首先需要互联网连接来下载并安装它。包的大小通常不超过几兆字节。一旦包安装完成,你就可以像平常一样通过library函数加载它,以访问相关的功能。对于上面列出的包,你将在书中的相关部分根据需要被提示进行此操作。
接下来,你将看到几种方法来执行 R 包的下载和安装,我们以ks包(Duong, 2007)为例。
注意
贡献的 R 包在正确性、速度与效率以及用户友好性方面通常具有较好的质量。尽管在提交的包被放到 CRAN 之前,必须通过基本的兼容性检查,但通过这些检查并不意味着包的整体质量和可用性。你只能通过使用该包、研究其文档以及查阅相关出版物来评估它的质量。
在 CRAN 上查找包
CRAN 上的每个 R 包都有自己的标准网页,提供直接的下载文件链接和关于该包的重要信息。在 CRAN 的列表中找到包名并点击,或者快速搜索,例如我们这里的ks r cran。图 A-4 显示了ks网页的顶部。

图 A-4:CRAN 网页上关于 ks 包的描述信息
除了基本的信息,如版本号和维护者的姓名及联系信息外,你还会看到 Depends 字段。这对于安装很重要;如果你感兴趣的 R 包依赖于其他贡献包(并不是所有包都这样),那么你也需要安装这些包,以便你的包能够成功安装。
查看 图 A-4,你可以看到你的 R 版本需要高于 1.4,ks 需要 KernSmooth(已安装——它是 A.2.2 节中提到的推荐包之一),同时它还需要 misc3d、mvtnorm 和 rgl。幸运的是,如果你直接从 R 安装一个包,依赖项也会自动安装。
在提示符下安装包
下载并安装一个贡献的 R 包的最快方法是直接从 R 提示符使用 install.packages 命令。从全新安装的 R 开始,我在我的 iMac 上看到以下内容:
R> install.packages("ks")
--- Please select a CRAN mirror for use in this session ---
also installing the dependencies 'misc3d', 'mvtnorm', 'rgl'
--snip--
你可能会首先被要求选择一个 CRAN 镜像。列表弹出并默认仅显示安全的 HTTPS 服务器;选择 HTTP 会切换到不安全的服务器。我选择了 HTTP,找到了新西兰镜像并点击了确定,如 图 A-5 所示。完成后,所选的镜像将保持设置为默认站点,直到你重新设置它;参见 A.4.1 节。

图 A-5:选择用于下载贡献包的 CRAN 镜像站点的弹出窗口。左侧:可选的 HTTP 服务器选择(与 HTTPS 相对)。右侧:选择我的本地 HTTP 镜像。
在点击 确定 后,R 会列出将被下载和安装的任何依赖项,并呈现每个包的下载通知(在省略的输出中)。
有一些额外的注意事项:
• 你只需安装一次包,它会保存到硬盘上,以便在使用 library 调用时加载,就像往常一样。
• 你可能会被提示使用或创建一个本地文件夹来存储已安装的包。这是为了确保 R 知道在使用 library 请求包时应该从哪里获取它们。同意这样做意味着你有一个特定于用户的包库,这通常是一个好主意。
• install.packages 有许多可选参数;请查看通过在提示符下输入 ?install.packages 调用的帮助文件。例如,如果你想在控制台中指定一个 CRAN 镜像,可以将相关的 URL 作为字符字符串传递给 repos 参数,或者如果你希望防止安装依赖项,可以使用 dependencies 参数。
• 你也可以从源代码安装 R 包,也就是说,从未编译的代码安装,这些源代码可能包含比预编译的二进制版本更新的版本。如果你是 OS X 用户,R 的最新版本会询问你是否希望为有更新版本的包从源代码下载包,而这些版本比预编译的二进制版本更为新颖。要执行此操作,你的系统上需要安装某些命令行工具;如果下载失败,你可以在 download from source 提示时选择 n(“否”),继续使用二进制版本。
使用图形界面安装包
本书中使用的基本 R 图形用户界面(GUI)允许你通过控制台和菜单项下载并安装贡献包。接下来,我们简要介绍 Windows 和 OS X 版本。
在 Windows 上,点击 Packages → Install package(s)... 菜单项,如 图 A-6 左侧所示。选择一个 CRAN 镜像,一个高窗口将打开,按字母顺序列出所有可用的包。向下滚动以选择你感兴趣的包。你可以在 图 A-6 右侧看到我选择的 ks。点击 OK 以下载并安装这些包及其依赖项。

图 A-6:通过 Windows 中的 GUI 菜单启动下载并安装一个贡献的 R 包(以及任何缺失的依赖项)
对于 OS X R,点击 OS X 菜单栏中的 Packages & Data → Package Installer 项,如 图 A-7 顶部所示。当包安装程序打开时,点击 Get List 按钮以显示可用包的列表。选择你需要的包,确保在点击 Install Selected 之前勾选安装程序底部的 Install Dependencies 选项框。R 将下载并安装所需的所有内容,包括任何依赖项,如 图 A-7 底部所示。你可以选择多个包。请注意,安装程序左下角的选项允许你选择安装包存储的位置;如果你是非管理员用户,可能需要创建一个用户专用的库,如前文所述。

图 A-7:通过 OS X 中的基于 GUI 的包安装程序启动下载并安装一个贡献的 R 包(以及任何缺失的依赖项)
使用本地文件安装包
最后,你可以通过互联网浏览器从 CRAN 下载所需的包文件,就像下载其他任何东西一样,将它们存储在本地驱动器上,然后指示 R 使用这些本地文件。
在 ks 的 CRAN 网页上,你会看到如图 A-8 所示的下载部分。对于 Linux,选择包源文件。对于 Windows 或 OS X,选择相应的.zip或.pkg文件,标记为 r-release。只有在遇到兼容性问题时,才应使用 r-oldrel 和 r-devel 版本。Old sources 链接包含旧版本的归档源文件。
在 Windows 上,选择 Packages → 从本地 zip 文件安装包...(如图 A-6 所示)。这将打开一个文件浏览器,让你可以找到下载的.zip文件;R 会自动完成剩下的步骤。

图 A-8:CRAN 网页上的ks下载部分
在 OS X 上,下载完 .pkg 文件后,选择 Packages & Data → Package Installer。在安装程序顶部的下拉菜单中选择 Local Binary Package,如图 A-9 所示。要打开文件浏览器以找到本地文件,你需要点击安装程序底部的 Install... 按钮。R 会处理剩下的步骤。

图 A-9:在 OS X 上使用包安装程序从本地文件安装 R 包
需要注意的是,这种方法不会自动安装任何依赖项。你还需要安装包所依赖的其他包,及其依赖项,依此类推——因此,务必检查 CRAN 包网页上的 Depends 字段,如前所述。
从 R 提示符直接使用install.packages,或通过 GUI 自动化这个过程要容易得多。只有在自动方法因某些原因失败时,或者你正在安装一个不在 CRAN 上(或其他任何容易获取的仓库——参见第 A.4.2 节)的包,或该包不适用于你的操作系统时,你才需要进行本地文件安装。
A.3 更新 R 和已安装的包
大约每年会发布四个新的 R 版本,解决功能、兼容性问题和修复 bug。保持更新这些新版本是个好主意。R 项目的主页和任何 CRAN 镜像站点都会告诉你最新版本的发布信息,你也可以在 R 提示符下执行news()以查看更新内容。
贡献的 R 包也会定期更新,并将新的包文件上传到 CRAN。你安装的包不会自动更新,因此你需要手动更新它们。由于更新发布完全取决于维护者,所以很难预测更新发布的频率,但建议每隔几个月检查一下已安装包的最新版本,或者至少在你升级 R 版本时进行检查。
检查包更新很简单。只需调用update.packages(),不带任何参数,它会系统地检查你安装的包,并标记出有更新版本的包。
例如,在我当前的安装中,执行以下命令告诉我,MASS的更新版本可用(以及其他一些未在此输出片段中显示的软件包)。
R> update.packages()
MASS :
Version 7.3-43 installed in /Library/Frameworks/R.framework/Versions/3.2/
Resources/library
Version 7.3-44 available at http://cran.stat.auckland.ac.nz
Update (y/N/c)? y
--snip--
输入y从 CRAN 下载更新的软件包。如果有多个软件包有更新可用,R 会逐一询问你是否希望更新,你需要为每个软件包输入y(或者N或c)。
你也可以通过 R 的图形用户界面菜单进行软件包更新(或者通过本地文件安装手动更新)。在 Windows 上,选择Packages → Update packages...打开一个可用更新列表。在 OS X 上,包安装器的已填充表格中会有一列提供你当前安装的每个软件包的版本信息,以及 CRAN 上当前版本的信息,给你一个安装更高版本的选项。还有一个“Update All”按钮,这是你通常会使用的。
A.4 使用其他镜像和仓库
有时候,你可能需要更改与你的典型软件包安装过程相关联的 CRAN 镜像,或者实际上将目标仓库更改为非 CRAN 的其他仓库——有几个选项可供选择。
A.4.1 切换 CRAN 镜像
你很少需要更改 CRAN 镜像,但如果,例如,你常用的镜像站点因为某些原因无法访问,或者你希望从不同的地点使用 R,可能需要更改。要查询当前设置的仓库,可以使用getOption并传入"repos"。
R> getOption("repos")
CRAN
"http://cran.stat.auckland.ac.nz"
若要将其更改为例如墨尔本大学的镜像,只需将新的 URL 分配给repos组件,并在调用options时按如下方式操作:
R> options(repos="http://cran.ms.unimelb.edu.au/")
之后使用install.packages或update.packages时,将使用此澳大利亚镜像进行下载。
A.4.2 其他软件包仓库
CRAN 并不是唯一的 R 软件包仓库。其他仓库包括 Bioconductor(见* www.bioconductor.org/),Omegahat(见 www.omegahat.org/),以及 R-Forge(见 r-forge.r-project.org/*),还有更多其他仓库。这些仓库通常处理不同的主题。例如,Bioconductor 托管与 DNA 微阵列及其他基因组分析方法相关的软件包;Omegahat 托管专注于 Web 和 Java 应用的包。
就一般统计分析而言,CRAN 是大多数用户首选的仓库。要了解更多关于其他仓库的信息,你可以访问相关网站。
A.5 引用与编写软件包
在使用 R 及其软件包进行数据分析等研究项目时,必须以适当的方式认可这些工作的贡献。实际上,当你考虑编写自己的软件包时,了解以下内容是非常重要的。
A.5.1 引用 R 和贡献的软件包
若要引用 R 及其包,citation命令会输出相关内容。
R> citation()
To cite R in publications use:
R Core Team (2016). R: A language and environment for statistical computing.
R Foundation for Statistical Computing, Vienna, Austria. URL
https://www.R-project.org/.
A BibTeX entry for LaTeX users is
@Manual{,
title = {R: A Language and Environment for Statistical Computing},
author = {{R Core Team}},
organization = {R Foundation for Statistical Computing},
address = {Vienna, Austria},
year = {2016},
url = {https://www.R-project.org/},
}
We have invested a lot of time and effort in creating R, please cite it when
using it for data analysis. See also 'citation("pkgname")' for citing R
packages.
请注意,LAT[E]X 用户可以通过自动生成的 BIBT[E]X 条目方便地进行引用。
如果某些包在完成某个特定工作中起到了重要作用,你也可以引用这些包。以下是一个示例:
R> citation("MASS")
To cite the MASS package in publications use:
Venables, W. N. & Ripley, B. D. (2002) Modern Applied Statistics with S.
Fourth Edition. Springer, New York. ISBN 0-387-95457-0
A BibTeX entry for LaTeX users is
@Book{,
title = {Modern Applied Statistics with S},
author = {W. N. Venables and B. D. Ripley},
publisher = {Springer},
edition = {Fourth},
address = {New York},
year = {2002},
note = {ISBN 0-387-95457-0},
url = {http://www.stats.ox.ac.uk/pub/MASS4},
}
A.5.2 编写你自己的包
一旦你成为 R 语言的专家,你可能会发现自己有一系列其他人可能会觉得有用的函数、数据集和对象,或者你使用得足够频繁,以至于值得将它们打包成标准化、易于加载的格式。当然,你没有义务将你的包提交到 CRAN 或其他任何仓库,但如果你打算这样做,请注意,确实有一些严格的要求,以确保用户能够信任你的包的稳定性和兼容性。
如果你有兴趣构建自己的可安装 R 包,请参阅官方的Writing R Extensions手册,你可以通过点击主页左侧文档部分的 Manuals 链接,在任何 CRAN 镜像站点上找到该手册;你可以在图 A-2 中看到这个链接。如果你感兴趣,你也可以去寻找 Wickham 的书(2015b),这本书提供了有关 R 包编写过程的有用指导以及相关的注意事项。
第二十八章:B
使用 RStudio

尽管基础的 R 应用程序和 GUI 已经可以释放出所有可用功能,但控制台和代码编辑器的简陋外观可能会让一些人,尤其是初学者,感到不适应。为了增强日常使用 R 语言的体验,RStudio 是最优秀的集成开发环境(IDE)之一。
像 R 一样,RStudio 的桌面版本(RStudio Team, 2015)是免费的,可以在 Windows、OS X 和 Linux 系统上使用。在安装 RStudio 之前,你必须先安装 R,如附录 A 中所述(OS X 用户还需要安装 XQuartz;参见第 A.1 节))。然后,你可以从官方网站下载 RStudio,地址是www.rstudio.com/products/rstudio/download/。
RStudio 网站还提供了各种有用的支持文章和链接,以及针对各种特殊增强功能的说明,其中一些已在第 B.2 节中提到。如果你需要帮助,可以访问support.rstudio.com/hc/en-us/。特别是,你应该花时间点击文档链接;你也可以通过选择帮助 → RStudio 文档来查看。
在本附录中,你将概览 RStudio 及其最常用的工具。
B.1 基本布局和使用
RStudio IDE 分为四个窗格,你可以自定义内容和布局,以适应你的偏好。图 B-1 展示了我的设置。在其中,我正在使用第 24.4 节中的 ggvis 代码。

图 B-1:RStudio 在实际操作中的界面。四个窗格可以根据你的需要进行排列和隐藏;在这里,你可以看到代码编辑器(左上),控制台(左下),帮助页面(右上)和图形查看器(右下)。右侧的窗格还可以选择额外的标签。
你在内置编辑器中编写 R 代码,并在控制台中执行;“发送代码到控制台”的快捷键是 Windows 中的 CTRL-ENTER 或 CTRL-R,Mac 上则是
-RETURN。文本输出会像往常一样出现在控制台中。
B.1.1 编辑器功能和外观选项
RStudio 编辑器最有用的功能之一是色彩主题的代码高亮和括号匹配。这使得编程体验比基础 R 编辑器更加轻松,特别是在编写长段代码时。在编辑器或控制台中输入时,还有自动补全选项弹出。你可以在图 B-2 中看到一个示例。

图 B-2:RStudio 的自动补全功能包括每个选项的提示。
这些功能可以使用 RStudio 选项进行启用、禁用和自定义(在 Windows 和 OS X 上选择 工具 → 全局选项...;对于 OS X,你也可以选择 RStudio → 偏好设置...);你可以在图 B-3 中看到代码和外观选项。

图 B-3:代码编辑(左)和外观(右)选项窗格
B.1.2 自定义窗格
接下来,你可能需要整理四个 RStudio 窗格的排列和内容。两个窗格始终是编辑器和控制台,但你可以设置多个额外的标签页,以显示在两个实用窗格上。这些包括文件浏览器,您可以用它来搜索并打开本地计算机上的 R 脚本,图形和文档查看器,标准的 R 函数帮助文件,以及包安装器。
你可以通过 RStudio 选项中的窗格布局部分的下拉菜单和复选框配置你的实用窗格。图 B-4 展示了我当前的设置;我对默认布局所做的一项更改是将帮助文件显示在最上面的实用窗格中,将图形显示在底部,因为我经常在尝试绘图时需要参考函数文档。

图 B-4:窗格布局和排列选项
B.2 辅助工具
RStudio 为你提供了一些方便的工具,可以与 R 一起使用,我将在这里简要介绍。如果你想了解某个特定功能的更多信息,请查看支持文档 support.rstudio.com/hc/en-us/。
B.2.1 项目
RStudio 项目 帮助你在处理更复杂的工作时进行开发和文件管理。在这些情况下,你通常会使用多个脚本文件,可能希望保存单独的 R 工作区,或者你可能已经将某些 R options 设置为特定或非默认值。RStudio 使这个过程更加便捷,因此你不需要手动设置。
在 RStudio 窗口的右上角,你会看到一个 Project: (None) 按钮。点击它,你会看到一个简短的菜单,如图 B-5 所示;通过点击 新建项目 并选择 新目录 → 空项目 项目,设置一个基本的项目文件夹。
本质上,创建新项目的操作如下:
• 将工作目录设置为项目文件夹
• 默认情况下,将 R 工作区、历史记录和所有 .R 源文件保存在该文件夹中
• 创建一个 .Rproj 文件,该文件可以在以后打开已保存的项目,并存储特定于该项目设置的 RStudio 选项

图 B-5:RStudio 项目菜单;设置基本的项目目录
当你在一个特定的项目中工作时,它的名称将会替代 Project: (None) 按钮上的 (None)。
B.2.2 包安装器和更新器
RStudio 提供了一个包安装器来管理贡献包的下载和安装。您可以在所选的工具窗格中的“包”标签中找到包管理器。它仅列出您当前已安装的包及其版本号,您可以使用每个包名称旁边的复选框加载它(而不是在 R 控制台提示符下使用library)。
我的演示出现在图 B-6 中。我刚刚选择了car包的复选框,这会自动在控制台中执行相关的library调用。

图 B-6:RStudio 包安装器,显示通过勾选其复选框加载的 car 包
该图还显示了安装和更新包的按钮。要安装包,请点击安装按钮并在字段中输入您想要的包名。RStudio 会在您输入时提供选项,如图 B-7 左侧显示的ks包。确保勾选“安装依赖项”以自动安装任何额外所需的包。
要更新包,请点击更新按钮,调出图 B-7 右侧的对话框;在这里,您可以选择更新单个包或点击全选按钮更新所有包。

图 B-7:RStudio 中的包安装和更新功能
当然,如果您愿意,仍然可以直接从 RStudio 控制台提示符下使用install.packages、update.packages和library命令。
B.2.3 调试支持
RStudio 的另一个优点是它内置的代码调试工具。调试策略通常涉及能够在特定位置“暂停”代码,以检查对象和函数值的“实时”状态。具体的技术最好参考更高级的书籍,如《调试的艺术》(Matloff 和 Salzman, 2008)和《R 编程的艺术》(Matloff, 2011);另见《高级 R》(Wickham, 2015a)的第九章。但我在此提到它,因为 RStudio 提供的工具比基本的 R 命令更方便、更高级地支持调试。
当您开始编写由多个相互关联的 R 函数组成的程序时,您可能希望了解更多内容。关于 R 和 RStudio,Jonathan McPherson 在支持网站上有一篇很好的入门文章,您可以在support.rstudio.com/hc/en-us/articles/205612627-Debugging-with-RStudio/上阅读。
B.2.4 标记语言、文档和图形工具
在编写项目报告或特定分析的教程时,研究人员通常使用 标记 语言。最著名的标记语言之一,尤其在科学领域,是 LAT[E]X;它促进了对技术文档排版、格式化和布局的统一方法。
有一些专门的包将 R 代码集成到这些文档的编译过程中。这些包反过来又被集成到 RStudio 中,使你能够创建动态文档,利用 R 代码和图形,而无需在不同的应用程序之间切换。
你需要在计算机上安装 T[E]X 才能使用这些工具,安装包可以在 www.latex-project.org/ 找到。在这一节中,我将简要讨论最广泛使用的增强功能。
Sweave
Sweave (Leisch, 2002) 可以说是第一个在 R 中流行的标记语言;其功能包含在任何标准的 R 安装中。Sweave 遵循典型的 LAT[E]X 标记规则;在你的文档中,你声明特殊的字段,称为 chunks,在其中编写 R 代码,并指示将任何相应的输出显示出来;输出可以包括控制台文本和图形。当你编译 Sweave 文件(扩展名为 .Rnw)时,R 代码字段将被发送到 R 进行实时评估,结果会出现在完成产品的指定位置。要开始一个新文档,选择 文件 → 新建文件 → R Sweave,如 图 B-8 所示。有关一些示例和资源,请访问 Sweave 的主页 www.statistik.lmu.de/~leisch/Sweave/。

图 B-8:在 RStudio 中开始一个新的 Sweave 文档。编辑器用于标记和实时代码字段,你可以使用编译 PDF 按钮来渲染结果。
knitr
knitr (Xie, 2015) 是一个 R 包,作为 Sweave 的扩展,提供了一些附加功能,使得文档创建更简单、更灵活。你可以在 RStudio 选项的 Sweave 标签中选择 knitr 作为文档的“编织器”,通过选择 工具 → 全局选项... 来找到该选项(见 图 B-9)。要了解更多关于 Sweave 和 knitr 在 RStudio 中的使用,参考 Josh Paulson 的文章 support.rstudio.com/hc/en-us/articles/200552056-Using-Sweave-and-knitr/。

图 B-9:在 RStudio 选项的 Sweave 标签中选择 knitr 作为标记文档的编织器
R Markdown
R Markdown (Allaire et al., 2015) 是另一个动态文档创建工具,可以从 CRAN 下载 rmarkdown 包。与 Sweave 和 knitr 类似,它的目标是生成精美的文档,其中可以自动包含 R 代码和输出。然而,与 Sweave 和 knitr 不同,R Markdown 的一个目标是尽量减少学习复杂标记语言(如 LAT[E]X)的需求,因此它的语法相对简单。从 .Rmd 源文件开始,你可以创建多种输出文档类型,如 PDF、HTML 和 Word。
要开始一个新的 R Markdown 文档,请点击 R Markdown... 菜单项,路径为 文件 → 新建文件,如 图 B-8 左侧所示;这将打开 图 B-10 顶部显示的新建 R Markdown 对话框。在这里,你可以选择适合你项目的文档类型,然后 RStudio 编辑器会提供一个基本模板;图 B-10 底部显示了一个模板。模板还引导你访问 R Markdown 的主页 rmarkdown.rstudio.com/,如果你有兴趣深入了解,绝对值得一探。Garrett Grolemund 还提供了一系列关于使用 R Markdown 的有用链接,见 support.rstudio.com/hc/en-us/articles/205368677-R-Markdown-Dynamic-Documents-for-R/。

图 B-10:在 RStudio 中启动一个新的 R Markdown 文件。与所选输出文件类型相关的模板会自动提供。
Shiny
Shiny 是由 RStudio 团队开发的一个用于创建交互式网页应用的框架。如果你有兴趣分享你的数据、统计模型和分析结果,以及图形,你可以创建一个 Shiny 应用。R 包 shiny (Chang et al., 2015) 提供了所需的功能。Shiny 应用要求你在后台运行一个 R 会话,这会在用户通过网页浏览器与应用交互时驱动图表。
与其他 RStudio 相关工具一样,Shiny 是一个高层次的框架,旨在对用户和开发者都友好。它的重点是创建交互式可视化图形,这些图形与你在 第 24.4 节 使用 ggvis 创建的图形相似,然后你可以将它们部署到网上供任何人使用。
你可以访问 Shiny 的官网 shiny.rstudio.com/。开发团队为创建全面的教程以及大量示例投入了大量精力。一旦你熟悉了这个应用,你甚至可以使用 Shiny 通过 R Markdown 创建交互式文档——请注意 图 B-10 顶部图像中的 Shiny 文档选项。
第二十九章:参考文献
Adler, D., Murdoch, D., 和其他人 (2015). rgl: 使用 OpenGL 进行 3D 可视化。CRAN.R-project.org/package=rgl,R 包版本 0.95.1247。
Allaire, J.J., Cheng, J., Xie, Y., McPherson, J., Chang, W., Allen, J., Wickham, H., Atkins, A., 和 Hyndman, R. (2015). rmarkdown: 动态文档 for R. CRAN.R-project.org/package=rmarkdown,R 包版本 0.8.1。
Anderson, E. (1935). “加斯佩半岛的鸢尾花。” 美国鸢尾花学会学报,59,2–5。
Atkinson, A.C. (1985). 图形、变换与回归。牛津大学出版社, 英国。
Auguie, B. (2012). gridExtra: Grid 图形中的函数. CRAN.R-project.org/package=gridExtra,R 包版本 0.9.1。
Baddeley, A.J. 和 Turner, R. (2005). “spatstat: 一个用于分析空间点模式的 R 包。” 统计软件学报,12(6),1–42。
Becker, R.A., Chambers, J.M., 和 Wilks, A.R. (1988). 新 S 语言。Chapman & Hall, 英国。
Bollen, K.A. 和 Jackman, R.W. (1990). “回归诊断:异常值和影响案例的阐释性处理。” 收录于 J. Fox 和 J.S. Long(编),《现代数据分析方法》,Sage, 美国。
Canty, A. 和 Ripley, B.D. (2015). boot: 自助法 R(S-Plus)函数。CRAN.R-project.org/package=boot,R 包版本 1.3-15。
Chang, W., Cheng, J., Allaire, J., Xie, Y., 和 McPherson, J. (2015). shiny: R 的 Web 应用框架. CRAN.R-project.org/package=shiny,R 包版本 0.12.2。
Chang, W. 和 Wickham, H. (2015). ggvis: 交互式图形语法. CRAN.R-project.org/package=ggvis,R 包版本 0.4.2。
Chatterjee, S., Hadi, A.S., 和 Price, B. (2000). 通过实例进行回归分析。Wiley, 美国,第 3 版。
Chu, S. (2001). “钻石石材的 C 级定价。” 统计教育学报,9(2). www.amstat.org/publications/jse/v9n2/datasets.chu.html。
Cox, D.R. 和 Snell, E.J. (1981). 应用统计学:原理与实例。Chapman & Hall, 美国。
Davies, T.M. 和 Bryant, D.J. (2013). “关于高斯随机场的循环嵌入方法在 R 中的应用。” 统计软件学报,55(9),1–21。
Davison, A.C. 和 Hinkley, D.V. (1997). 自助法及其应用。剑桥大学出版社, 英国。
Dickey, D.A. 和 Arnold, J.T. (1995). “使用具有历史意义的数据教授统计学:伽利略的重力和运动实验。” 统计教育学报,3(1). www.amstat.org/publications/jse/v3n1/datasets.dickey.html。
Diggle, P.J. (1990). “点过程建模方法用于在预定点附近罕见现象的高发病率。” 《皇家统计学会 A 系列杂志》,153,349–362。
Dobson, A.J. (1983). 《统计建模导论》。Chapman & Hall,UK。
Duong, T. (2007). “ks: 在 R 中进行多变量数据的核密度估计和核判别分析。” 《统计软件杂志》,21(7),1–16。
Faraway, J.J. (2005). 《线性模型与 R 语言》。Chapman & Hall,USA。
Feng, D. 和 Tierney, L. (2008). “在 R 中计算和显示等值面。” 《统计软件杂志》,28(1),1–24。
Fisher, J.C. (1976). “底特律的凶杀案件:枪支的作用。” 《犯罪学》,14,387–400。
Fisher, R.A. (1936). “在分类学问题中使用多项测量。” 《优生学年鉴》,7(2),179–188。
Fox, J. 和 Weisberg, S. (2011). 《应用回归分析的 R 语言指南》。Sage,USA,第 2 版。
Golub, G.H. 和 Van Loan, C.F. (1989). 《矩阵计算》。Johns Hopkins University Press,UK,第 2 版。
Gupta, S. (2012). “R 是如何搜索和查找内容的。” blog.obeautifulcode.com/R/How-R-Searches-And-Finds-Stuff/。
Hand, D.J., Daly, F., McConway, K., Lunn, D., 和 Ostrowski, E. (主编) (1994). 《小数据集手册》。Chapman & Hall,USA。
Härdle, W. (1990). 《应用非参数回归》。Cambridge University Press,USA。
Harraway, J.A. (1995). 《应用回归方法》。奥塔哥大学出版社,新西兰。
Harrison, D. 和 Rubinfeld, D.L. (1978). “享乐价格与对清洁空气的需求。” 《环境经济学与管理学杂志》,5,81–102。
Henderson, H.V. 和 Velleman, P.E. (1981). “交互式构建多元回归模型。” 《生物统计学》,37,391–411。
Hornik, K. (2015). “R 语言常见问题解答。” CRAN.R-project.org/doc/FAQ/R-FAQ.html。
Ihaka, R., Murrell, P., Hornik, K., Fisher, J.C., 和 Zeileis, A. (2015). colorspace: 颜色空间操作。CRAN.R-project.org/package=colorspace,R 包版本 1.2-6。
James, D.A. 和 DebRoy, S. (2012). RMySQL: R 语言与 MySQL 数据库的接口。CRAN.R-project.org/package=RMySQL,R 包版本 0.9-3。
Kruschke, J.K. (2010). 《贝叶斯数据分析:R 和 BUGS 教程》。Academic Press/Elsevier,USA。
Kuhn, M. 和 Johnson, K. (2013). 《应用预测建模》。Springer,USA。
Kusnierczyk, W. (2012). rbenchmark: R 语言基准测试工具。CRAN.R-project.org/package=rbenchmark,R 包版本 1.0.0。
Leisch, F. (2002). “使用文献数据分析动态生成统计报告。” 收录于 W. Härdle 和 B. Rönz (编辑),《Compstat 2002 – 计算统计学论文集》,Physika Verlag, 德国。
Lemon, J. (2006). “Plotrix:R 中的红灯区包。” R-News,6(4),8–12。
Ligges, U. 和 Fox, J. (2008). “R 帮助台:如何避免这个循环或让它更快?” R News,8(1),46–50。
Ligges, U. 和 Mächler, M. (2003). “Scatterplot3d—一个用于可视化多变量数据的 R 包。” 统计软件期刊,8(11),1–20。
Lock, R.H. (1993). “1993 年新车数据。” 统计教育期刊,1(1)。www.amstat.org/publications/jse/v1n1/datasets.lock.html。
Matloff, N. (2011). R 编程艺术:统计软件设计之旅。No Starch Press, 美国。
Matloff, N. 和 Salzman, P.J. (2008). 调试艺术:使用 GDB、DDD 和 Eclipse。No Starch Press, 美国。
McNeil, D.R. (1977). 互动数据分析。Wiley, 美国。
Mirai Solutions GmbH (2014). XLConnect: Excel 连接器 for R。CRAN.R-project.org/package=XLConnect,R 包版本 0.2-7。
Montgomery, D.C., Peck, E.A. 和 Vining, G.G. (2012). 线性回归分析导论。Wiley, 美国,第 5 版。
Murrell, P. (2011). R 图形学。Chapman & Hall, 美国,第 2 版。
Murrell, P. 和 Ihaka, R. (2000). “在图表中提供数学注释的方法。” 计算与图形统计学期刊,9,582–599。
Neuwirth, E. (2014). RColorBrewer: ColorBrewer 调色板。CRAN.R-project.org/package=RColorBrewer,R 包版本 1.1-2。
R 核心团队 (2015). foreign: 读取由 Minitab、S、SAS、SPSS 等存储的数据。CRAN.R-project.org/package=foreign,R 包版本 0.8-66。
Reinhart, A. (2015). 统计学错误:完全错误指南。No Starch Press, 美国。
Ripley, B. 和 Lapsley, M. (2013). RODBC: ODBC 数据库访问。CRAN.R-project.org/package=RODBC,R 包版本 1.3-10。
Royston, P. (1982). “算法 AS 181:正态性检验的 W 测试。” 应用统计学,31,176–180。
RStudio 团队 (2015). RStudio: R 的集成开发环境。
RStudio, Inc., 美国。www.rstudio.com/。
Schloerke, B., Crowley, J., Cook, D., Hofmann, H., Wickham, H., Briatte, F., 和 Marbach, M. (2014). GGally: ggplot2 的扩展。CRAN.R-project.org/package=GGally,R 包版本 1.0.1。
Schorling, J.B., Roach, J., Siegel, M., Baturka, N., Hunt, D.E., Guterbock, T.M., 和 Stewart, H.L. (1997). “为农村非裔美国人开展教会基础的戒烟干预试验。” 预防医学, 26, 92–101。
Scott, D.W. (1992). 多变量密度估计:理论、实践与可视化. Wiley, 美国。
Snee, R.D. (1974). “双向列联表的图形显示。” 美国统计学家, 28, 9–12。
Soetaert, K. (2014). shape: 绘制图形形状、颜色的函数. CRAN.R-project.org/package=shape, R 包版本 1.4.2。
Sterne, J.A.C. 和 Smith, G.D. (2001). “筛选证据——显著性检验的错误在哪里?” 英国医学杂志, 322(7280), 226–231。
Takezawa, K. (2006). 非参数回归导论. Wiley, 美国。
Tippett, L.H.C. (1950). 统计学的技术应用. Wiley, 美国。
Tong, H. (1990). 非线性时间序列:一种动态系统方法. 牛津大学出版社, 英国。
Trapletti, A. 和 Hornik, K. (2013). tseries: 时间序列分析与计算金融. CRAN.R-project.org/package=tseries, R 包版本 0.10-32。
Urbanek, S. (2013). RJDBC: 通过 JDBC 接口提供对数据库的访问. CRAN.R-project.org/package=RJDBC, R 包版本 0.2-3。
Venables, W.N. 和 Ripley, B.D. (2002). 现代应用统计学与 S 语言. Springer, 美国,第 4 版。
Wand, M.P. 和 Jones, M.C. (1995). 核平滑. Chapman & Hall, 美国。
Warnes, G.R., Bolker, B., Gorjanc, G., Grothendieck, G., Korosec, A., Lumley, T., MacQueen, D., Magnusson, A., Rogers, J., 等人 (2014). gdata: 用于数据处理的各种 R 编程工具. CRAN.R-project.org/package=gdata, R 包版本 2.13.3。
Wickham, H. (2009). ggplot2:数据分析的优雅图形. Springer, 美国。
Wickham, H. (2015a). 高级 R 编程. CRC Press/Taylor & Francis, 美国。
Wickham, H. (2015b). R 包. O’Reilly Media, 美国。
Wilkinson, L. (2005). 图形语法. Springer, 美国,第 2 版。
Willems, J.P., Saunders, J.T., Hunt, D.E., 和 Schorling, J.B. (1997). “农村黑人冠心病风险因素的流行情况:一项社区基础研究。” 南方医学杂志, 90, 814–820。
Woosley, A.I. 和 Mcintyre, A.J. (1996). 米姆布雷斯·莫戈隆考古学. 新墨西哥大学出版社, 美国。
Xie, Y. (2015). 动态文档与 R 语言及 knitr. Chapman & Hall/CRC, 美国,第 2 版。
第三十章:索引
符号和数字
&(逐元素与运算符),65,319
&&(单一比较与运算符),65
<-(箭头)符号,用于赋值,22,28
<(小于)运算符,61,386
<=(小于或等于)运算符,61
>(大于)运算符,61,386
>=(大于或等于)运算符,61
*(星号)
作为交叉因子符号,517,520,524
作为乘法运算符,17,49
\(反斜杠),74,76
\\ 转义序列,用于反斜杠,76
\" 转义序列,用于双引号,76
\b 转义序列,用于退格,76
\n 转义序列,用于换行,76
\t 转义序列,用于制表符,76
{ }(花括号),666
^(指数运算符),17
:(冒号)
用于创建数字序列,24
用于分组变量,446,516,524
::(双冒号),用于指定函数版本,253
:= 运算符,624–625
$(美元符号)运算符,92,256,461
.~. 符号,534,535,539
"(双引号),7,73
=(等号),21–22
==(等于)运算符,61
!(阶乘运算符),333
!(非运算符),65,107
!=(不等于)运算符,61
/(正斜杠)
用于除法,17
用于文件路径,7
#(井号)
用于注释,6
用于十六进制颜色代码,632
∞(无穷大),104–106
-(减法运算符),17,49
·(矩阵乘法),488
≠(不等于符号),386,459
()(圆括号),17,19,601
%>%(管道)运算符,624
%*%(矩阵乘法运算符),50
|(逐元素或运算符),65,452
||(单一比较或运算符),65
+(加法运算符),17
?? 命令, 10
;(分号), 666
[](方括号)。参见 方括号([])
~(波浪号), 299, 445, 490
2D 圆形, 721–723
3D 锥体, 725
3D 圆柱体, 723–725
3D 图表。参见 3D 散点图;交互式 3D 图表
3D 散点图, 649–653
基本语法, 649–650
可视化增强, 650–653
A
α(显著性水平), 387
abline 函数, 134, 137, 456, 525
add1 函数, 533, 571
add_axis 函数, 626, 628, 629, 630
加法, 17, 18
矩阵的, 49–50
加法效应, 468
add_legend 函数, 626, 628, 629, 630
add.smooth 参数, 552
adjust 参数, 628
adjustcolor 命令, 612–613, 643–645, 665
调整后的度量, 460
Adjusted R-squared(决定系数), 460, 548
高级图表自定义。参见 自定义图表;绘图
美学映射,配合几何图形, 143–146
aggregate 函数, 444, 446–447, 450
AIC(赤池信息量准则), 541–548
AIC 函数, 542
airquality 数据框, 614, 615, 617, 653, 676, 698
赤池信息量准则(AIC), 541–548
算法, 模型选择, 529–548
向后选择, 537–541
向前选择, 533–537
嵌套比较, 529–532
步进式 AIC 选择, 541–548
all 函数, 63, 64
alpha(显著性水平), 426
alpha 参数, 643, 644, 701, 714
alpha.f 参数, 613, 643
alternative 参数, 391, 405
替代假设, 386
美国信息交换标准代码(ASCII),160
方差分析。另见 ANOVA
AND 运算符,65
Anderson-Darling 检验,438
angle 参数,370
匿名函数,236
anorexia 数据集,401
ANOVA(方差分析),435–450
Kruskal-Wallis 检验,447–450
单向,435–442
ANOVA 表格构建,439–440
使用 aov 函数构建 ANOVA 表,440–442
与之等价,481–483
假设和诊断检查,436–439
双向,443–447
主效应和交互作用,444–447
假设集,443–444
anova 函数,531,571
any 值,63,64
aov 函数,440–442,481
设置几何对象的外观常量,141–143
apply 函数,204–209,236,723
args.legend 参数,292
参数,8,222–232
默认值,设置,225–227
省略号和,176–177,228–233
懒惰求值,222–225
匹配,172–177
省略号和,176–177
精确,172–173
混合,175–176
部分,173–174
定位,174–175
缺失值检查,227–228
帮助文件中的参数部分,10
算术,18–19。另见 数学
算术运算符,在图表中显示,601
array 函数,53,58
数组,多维的,52–58
arr.ind 参数,71
arrows 函数,134,138,353,585
ASCII(美国信息交换标准代码),160
点函数,121,248
as.matrix 函数,124
as.numeric 函数,278,438,462,469,638
asp 参数,705
aspect 参数,695,705
双变量曲面上的宽高比,704–708
分配对象,21–22
as.vector 函数, 123–124
非对称分布, 326
attach 函数, 256–257
属性, 114–116
attributes 函数, 114–115, 117
自动绘图类型, 129–130
平均平方距离, 277
坐标轴, 577
自定义, 594–596
标签, 130–131, 299
间距, 595
刻度标记, 606
axis 函数, 594–595, 604
B
\b(退格)转义序列, 76
β(类型 II 错误), 424, 426
β[0](截距), 454
β[1](斜率), 454
反斜杠(\)符号, 74, 76
向后选择, 537–541
barplot 函数, 290–291, 316
条形图, 289–293
base 分布, 739
基本包, 253, 739
基本 R 图形, 139
基线假设, 386, 421
对数的底数, 19
贝叶斯信息准则(BIC), 548
贝叶斯概率, 310
伯努利分布, 332–334
组间变异性, 440
bg3d 函数, 693
bgplot3 函数, 696, 706
偏差, 377
BIC(贝叶斯信息准则), 548
双峰分布, 326
二元变量, 332, 468–472
线性回归模型, 470–471
来自的预测, 471–472
绑定, 矩阵, 41–42
二项分布, 333–337
dbinom 函数, 335–336
pbinom 函数, 336
qbinom 函数, 337
rbinom 函数, 337–338
二项分布随机变量, 339
二元表面, 699–708
添加点, 701
添加表面, 701–703
基本视角表面, 700
按 z 值着色, 703–704
处理纵横比, 704–708
函数的主体代码, 216
Bonferroni 校正, 423
boot 包,500,523,687,740
border 参数,682
Boston 数据框,733
box 函数,583
箱线图。参见 boxplots
自定义盒子,593–594
boxplot 函数,298–299,606
箱线图,298–300,469–470
并排,299–300,437,448–449
独立的,298–299
花括号区域,函数的,216
花括号,用于包含命令,666
breaks 参数,85,295
bty 参数,593
内置数据集,148–149
内置调色板,635–636
byrow 参数,40
C
c 函数,58,83
计算,控制顺序,17
调用函数,165–177
参数匹配,172–177
省略号和,176–177
精确,172–173
混合,175–176
部分,173–174
位置,174–175
范围,165–171
环境,166–168
保留和保护的名称,170–172
搜索路径,168–170
car 包,254,628,646,741
Cars93 数据框,450
大小写敏感性,7
cat 命令,74–75,219,244
类别预测变量,468–483
二元变量,468–472
改变参考水平,477–478
与单因素方差分析的等价性,481–483
交互
两者之间,519–521
与连续预测变量一起,515–519
高阶,523–526
多级变量,472–477
虚拟编码,472–474
线性回归模型,474–476
将类别变量作为数值处理,478–480
类别变量,262–263,490,515
映射到的方面,619–623
测试,410–420
单一类别变量,410–415
两个类别变量,415–420
类别
穷举的,410
互斥的,410
分类,通过颜色调色板进行连续体索引, 637–639
cbind 函数, 41–42, 98
中心极限定理(CLT), 369, 401
中心性, 267–270
cex(字符扩展)参数, 129, 693, 707
cex.lab 参数, 599
char 参数, 250
字符, 72–79
字符串的连接, 74–76
转义序列, 76–77
扩展, 129
匹配, 77–79
字符串的, 73–74
子字符串和匹配, 77–79
χ (卡伊) 符号, 410
ChickWeight 数据集, 148
chickwts 数据框, 263, 269, 279
chisq.test 函数, 414–415, 419
卡方检验
分布的, 411–414
独立性, 416–419
chorley 数据集, 678
圆形,二维, 721–723
CIs. 参见 置信区间 (CIs)
citation 命令, 748
引用 R 包, 748–749
class 函数, 117, 118, 119
类. 参见 对象类
经典概率, 310
clmfires 数据集, 707
close 函数, 249–250
CLT(中心极限定理), 369, 401
cm.colors 函数, 635
coef 参数, 518
coef 函数, 457
决定系数, 458, 460
coefficients 组件, 457
强制转换, 120–126
col(颜色)参数, 129, 605, 682, 693
col2rgb 函数, 632–634
集体方式, 477
共线性, 549, 569–572
相关预测变量,举例, 569–572
潜在的警告信号, 569
颜色, 631–648. 参见 颜色调色板
给面板上色, 682–686
控制等值面中多个级别, 714–715
包括颜色图例, 641–643
不透明度, 643–645
在图形上, 131–132
RGB 替代方案, 645–648
RGB 十六进制颜色代码, 632–635
color 参数,715
颜色调色板
内置调色板,635–636
自定义调色板,636–637
用于索引连续体,637–641
通过分类,637–639
通过归一化,639–641
colorlegend 函数,641–643,644,647,648,651,669,675,690
color.palette 参数,663
colorRamp 函数,639,640,646,690
colorRampPalette 函数,636–637,639,645–646,664
colors 函数,131,632
cols 参数,633
colSums 函数,417
列
将矩阵绑定在一起,41–42
从矩阵中提取,43–44
组合图,333,563
命令
内置,7
滚动浏览,5
代码中的逗号,13
逗号分隔值(.csv),150
注释,6
compiler 包,739
事件的补集,312–313
完成时间,250–252
复杂度与拟合优度,527–528
一般指南,528–529
节俭原则,528
综合 R 档案网络。参见 CRAN(综合 R 档案网络)
compute.cont 参数,717
字符的连接,74–76
条件概率,311
条件。参见 if 语句
锥体,三维,725
置信带,507
置信区间(CIs),378–384,461–462
的解释,382–384
对于均值,378–381
对于比例,381–382
confint 函数,460,495
conf.level 参数,405,410
混杂,486
控制台面板,5–6
连续预测变量,493,515–519,521–523
连续随机变量,318–326
的累计概率分布,323–324
的均值和方差,326–329
连续体,使用颜色调色板索引,637–641
通过分类, 637–639
通过标准化, 639–641
contour 函数, 657–658, 661–663
等高线图, 657–668
填充颜色的等高线, 663–668
绘制等高线, 657–663
非参数双变量密度估计示例, 660–663
参数响应面, 659–660
地形图示例, 657–658
contour3d 函数, 713, 719
提供的数据集, 149–150
控制流机制, 209–214. 另请参见 循环
声明 break 或 next, 209–211
repeat 语句, 211–214
cook.levels 参数, 565
Cook 距离
与残差和杠杆作用图形结合, 563–568
概述, 559–563
cooks.distance 函数, 561
coord_flip 函数, 292–293
cor 函数, 281–285, 453
correct 参数, 405
相关性, 280–285
cos 函数, 722
余弦函数, 720
计数, 271–273
cov 函数, 281–284
协方差, 280–285
协方差矩阵, 732
CRAN(综合 R 存档网络)
在上面查找包, 742
镜像链接, 737
从这里获取和安装 R, 3
切换 CRAN 镜像, 747–748
交叉因子符号 (*), 517, 520, 524
.csv(逗号分隔值), 150
cumsum 函数, 316
自定义颜色调色板, 636–637
自定义绘图, 575–608
轴, 594–596
方框, 593–594
默认样式,抑制, 592–593
字体, 597–598
图形设备,处理, 576–582
关闭, 578
手动打开新的, 576–608
一个图中的多个绘图, 578–582
之间切换, 577–578
希腊符号, 598–599
数学表达式,显示, 599–601
点选坐标交互, 586–591
特设注释, 588–591
静默获取坐标, 586–587
可视化选定坐标, 587–588
区域和边距, 582–586
剪裁, 584–586
自定义间距, 583–584
默认间距, 582–583
完全注解的散点图, 601–608
cut 函数, 638, 705
圆柱体, 3D, 723–725
D
.dat 文件格式, 155
数据框, 95–102
添加数据列, 98–100
组合, 98–100
创建, 96–98
逻辑记录子集, 100–102
data 函数, 147
数据集, 147–150, 156–157
内建, 148–149
贡献, 149–150
数据可视化, 289–308
条形图, 289–293
箱线图, 298–300
并排箱型图, 299–300
独立箱型图, 298–299
直方图, 294–298
饼图, 293–294
散点图, 300–308
绘图矩阵, 303–308
单一图形, 301–302
data.frame 类, 120
data.frame 函数, 96, 97
datasets 包, 148, 263, 739
dbeta 函数, 363
dbinom 函数, 334, 335–336
dchisq 函数, 362, 412
调试, 244
小数位数 (d.p.), 13
decreasing 参数, 26, 27, 59, 60
自由度
对于卡方分布, 362
对于 F 分布, 363
对于 t 分布, 357–358
删除
从矩阵中提取元素, 44–46
从向量中提取元素, 29
分隔符,在表格格式文件中, 150
demo(plotmath) 演示, 601
density 命令, 614
密度函数, 342–363
指数分布, 359–362
dexp 函数, 359–360
pexp 函数, 360–361
qexp 函数, 361–362
正态分布, 348–357
dnorm 函数, 350
示例, 356–357
pnorm 函数, 350–353
qnorm 函数, 353–354
rnorm 函数, 355–356
学生 t 分布, 357–359
均匀分布, 343–347
dunif 函数,344–346
punif 函数,346
qunif 函数,346–347
runif 函数,347
dependencies 参数,743
帮助文件中的描述部分,10
detach 函数,255,257,740
帮助文件中的详细信息部分,10
dev.new 函数,576,579–580
dev.off 函数,157,578
dev.set 函数,577,578
dexp 函数,359–360
df 参数,362,412
df 函数,363
df1 参数,363
df2 参数,363
d-函数,342,344
dgamma 函数,363
dgeom 函数,342
dget 命令,160,161
dhyper 函数,342
diabetes 数据框,516,531,620–621,652
diag 命令,46,48
诊断检查,438,550
对角元素提取,558
二分变量,332
digit 参数,643
digits 参数,272
dim 函数,42,49,97,114,115,657
矩阵的维度,42
dimnames 参数,115–116,413
直接访问函数,457
离散随机变量,315–318
累积分布函数,315–317
均值和方差,317–318
离散数值变量,262
一次性函数,236
卡方分布的检验,411–414。另见 概率分布;抽样分布
除法,17,18
dmultinom 函数,342
dmvnorm 函数,711,713
dnbinom 函数,342
dnorm 函数,350,648,711
dodged barplot,290
美元符号($)操作符,92,256,461
双冒号 (::), 用于指定函数版本, 253
双引号 ("), 7, 73
转义序列 (\"), 76
从源下载提示, 744
下载 R, 737–739
小数位数 (d.p.), 13
dpois 函数, 340–341
dput 命令, 160, 161
drop1 函数, 537, 571
dt 函数, 357
虚拟函数, 220
Duncan 数据集, 420
邓迪温度示例, 369–373
dunif 函数, 344–346
E
e(欧拉数), 19
each 参数, 25–26, 417
地震数据示例, 660–663
编辑器, 在 RStudio 中, 752–753
编辑器窗格, 5–6
元素级检查, 184–186
省略号, 参数和, 176–177, 228–233
else 语句, 183–184
空括号, 216
空像素, 671–679
end 参数, 635, 672
指数表示法, 20–21
环境, 166–168
全局, 166
局部, 167–168
包环境和命名空间, 166–167
等号 (=), 21–22
错误。 参见 异常处理
概览, 420–421
I 型错误, 421–423
本费罗尼校正, 423
模拟, 421–423
II 型错误, 424–428
错误率的其他影响因素, 426–428
模拟, 425–426
转义序列, 76–77
欧几里得空间, 720
欧拉数 (e), 19
评估网格, 构造, 654–655
事件, 概率和, 310
事件的补集, 312–313
两个事件的交集, 311–312
两个事件的并集, 312
精确参数匹配, 172–173
示例部分, 帮助文件, 10
Excel 文件格式, 153
异常处理, 241–248
错误和警告, 242–244
try 语句, 244–248
抑制警告信息, 246–248
在函数体内使用, 245–246
exp 函数, 19
expand 参数, 682, 688, 705
expand.grid 函数, 654–655, 659, 675, 678, 706, 709, 724
解释变量, 451, 453, 485, 490, 528, 589
显式属性, 114, 115
指数分布, 359–362
dexp 函数, 359–360
与指数函数对比, 361
pexp 函数, 360–361
qexp 函数, 361–362
指数函数, 19–20
指数, 17
expression 函数, 598–599
外部定义的辅助函数, 234–235
extractAIC 函数, 542
提取
向量中的元素
使用索引, 28–32
使用逻辑值, 68–72
从矩阵中, 43–44
多余的变量, 486
外推, 466
F
F(FALSE 的缩写), 60
F(累积分布函数), 323
facet_grid 命令, 619–620
面板
上色, 682–686
使用多个图形, 616–623
映射到分类变量的面板, 619–623
独立图形, 616–618
facet_wrap 命令, 619–620
factor 函数, 80
因子变量, 435
因子, 79–87
组合与切割, 83–86
定义和排序水平, 82–83
识别类别, 79–81
faithful 数据框, 667
FALSE 值, 27, 60
family 参数, 597
faraway 包, 513, 516, 526, 531, 620–621, 652, 741
F 分布, 363
斐波那契数列, 216–218
图形边距, 582
figure margins too large 错误, 584
图形区域, 在绘图中, 582
图形的文件格式, 157
文件路径, 7
file.choose 命令, 152, 156
filled.contour 函数,663,666,690
fitted 函数,457,560
拟合模型,518,529
拟合值,458,462,551
fitted.values 组件,457
拟合线性模型,454
五数概括,274–275
标志向量,逻辑,68
浮动点数,117
向下取整操作,394
font 参数,597
字体,597–598
for 循环,193–200
通过索引或值进行循环,194–197
嵌套,197–200
预测,498
foreign 包,156,740
formula 参数,535
向前选择,533–537
正斜杠(/),7
frac 运算符,601
频率派概率,310
from 参数,24
F-检验,部分,529–532
FUN 参数,270,272,444
function 命令,215–222,236
创建函数,218–219
return 语句,220–222
函数的文档,8–10。参见 调用函数;编写函数
G
geom 参数,296
geom_bar 函数,292–293
geom_density 函数,614–615
几何分布,342
几何平均,238
geom_histogram 函数,297,611
geom_hline 函数,145
geom_line 函数,142,145
geom_point 函数,142,145
几何形状(几何修饰符)
使用美学映射,143–146
设置外观常量,141–143
geom_segment 函数,145
geom_smooth 函数,612,613,621,630
geom_vline 函数,297,610
getOption 函数,747
getwd 函数,7
GGally 包,304,741
ggpairs 函数,304–306
ggplot 函数,609–611
ggplot2 包
使用 geoms 进行美学映射, 143–146
多个图形和变量映射的面板, 616–623
映射到分类变量的面板, 619–623
独立图形, 616–618
qplot 函数与 ggplot 函数, 609–611
使用 geoms 设置外观常量, 141–143
平滑和阴影, 611–615
添加 LOESS 趋势, 611–614
构建平滑密度估计,使用 KDE, 614–615
ggsave 函数, 159
ggtitle 函数, 610, 615
ggvis 包, 623–630, 741
全局环境, 166
.GlobalEnv, 253
拟合优度与复杂性, 527–528
一般指南, 528–529
简约原则, 528
图形语法, 139
图形参数, 129–134
图形用户界面。参见 GUI (图形用户界面)
图形设备, 576–582
关闭, 578
手动打开新, 576–608
一个图形中的多个图形, 578–582
定义特定布局, 580–582
设置 mfrow 参数, 579–580
切换, 577–578
图形文件, 157–159
graphics 包, 739
图形。参见 绘图
gray 调色板, 635, 639, 677
gray.colors 调色板, 635, 636, 677, 690
grDevices 包, 739
大于运算符 (>), 61, 386
大于或等于运算符 (>=), 61
大于语句, 386
希腊字母符号, 598–599
grep 命令, 78
grid 函数, 605
grid 包, 739
grid3d 函数, 694–696
grid.arrange 函数, 616–618
gridExtra 包, 616, 617, 741
gridsize 参数, 717
group_by 函数, 628
gsub 函数, 78
图形用户界面 (GUI)
安装包, 744–745
概述, 4–6
H
HairEyeColor 数据集, 419
井号 (#)
用于注释, 6
十六进制颜色代码, 632
hatvalues 函数,558,561,571
HCL(色调-色度-亮度),645
hcl 函数,645
表格格式文件中的标题,150
heat.colors 调色板,635,638,678,689,708
height 参数,579,603
帮助文件,8–10
辅助函数,233–236
外部定义,234–235
内部定义,235–236
异方差性,550,551
十六进制颜色代码,632
高维概率密度,710–711
高阶交互,523–526
高级编程语言,作为 R,3
hist 函数,294–297
直方图,294–298,577
同方差性,550
HSV(色调-饱和度-值),645
hsv 函数,645
色调-色度-亮度(HCL),645
色调-饱和度-值(HSV),645
超几何分布,342
假设检验,385–433
分类变量,410–420
单一分类变量,410–415
两个分类变量,415–420
组成部分,385–388
假设,386
p-值,387
显著性水平,387–388
检验统计量,387
对其的批评,388
错误,420–421
类型 I 错误,421–423
类型 II 错误,424–428
均值,388–402
单一均值,389–392
两个均值,392–402
比例,402–410
单一比例,402–405
两个比例,405–410
统计功效,428–433
功效曲线,431–433
模拟功效,429–431
I
I 函数,505
ice.river 数据集,148
单位矩阵,48
IDE(集成开发环境),751
id.n 参数,552
if 语句,179–193
else 语句和,183–184
嵌套和堆叠,186–189
独立使用,180–183
switch 函数和,189–193
ifelse 函数,用于逐元素检查, 184–186
image 函数, 668, 672, 673, 682–683
隐式属性, 114
隐式循环, 使用 apply 函数, 204–209
include.lowest 参数, 85
独立性,卡方检验, 416–419
INDEX 参数, 270, 444
索引
列表的, 90–91
通过循环, 194–197
向量元素, 28–32
向量的, 30
Inf 函数, 104, 107
无穷大 (∞), 104–106
影响, 555–557
继承, 116
input_ 函数, 624, 627
input_checkbox 函数, 626
input_numeric 函数, 626
input_radiobuttons 函数, 626
input_select 函数, 626
input_slider 函数, 624–625, 627
InsectSprays 数据集, 273, 306
inside.owin 函数, 674, 675, 678, 690
安装 R, 737–750
从 CRAN, 3
下载, 737–739
包, 739–746
基础包, 739
贡献的包, 740–746
在 CRAN 上查找, 742
提示下安装, 742–744
从 GUI 安装, 744–745
使用本地文件安装, 745–755
推荐的包, 740
更新, 746–747
使用其他镜像和仓库, 747–748
其他包仓库, 748
切换 CRAN 镜像, 747–748
install.packages 命令, 8, 742, 756
整数, 117–118
集成开发环境 (IDEs), 751
强度 (RGB 颜色的), 632
interaction.plot 函数, 446
与图坐标的交互, 点选, 586–591
临时注释, 588–591
静默获取坐标, 586–587
可视化选定坐标, 587–588
交互式 3D 图, 691–735
双变量曲面, 699–708
添加点, 701
添加曲面,701–703
基本透视曲面,700
按 z 值上色,703–704
处理纵横比,704–708
参数方程,720–735
数学抽象,725–735
简单轨迹,720–725
点云,691–699
添加更多 3D 组件,694–699
基本 3D 云,692–693
视觉增强和图例,693–694
三元曲面,709–719
3D 中的评估坐标,709–710
等值面,710–715
非参数三元密度示例,715–720
交互效应,443,514–515
类别和连续预测变量之间,515–519
两个类别变量之间,519–521
两个连续预测变量之间,521–523
高阶,523–526
interactive.arrow 函数,590,608
截距参数,估计,454–455
截距模型,542
内部定义的辅助函数,235–236
插值,480,498
四分位距(IQR),277,287
两个事件的交集,311–312
interval 参数,463
矩阵的反转,51–52
反函数,603
向内的刻度线,596
IQR(四分位距),277,287
IQR 函数,278–279
iris 数据框,649–650,692
is-dot 函数,119–120
is.factor(survey$Smoke) 函数,474
is.finite 函数,105
is.infinite 函数,105
is.integer 函数,125
is.matrix 函数,223
is.na 函数,109
is.nan 函数,107
is.null 函数,111
is.numeric 函数,125
等值面,710–715
基本一层等值面,712–714
通过颜色和不透明度控制多个层级,714–715
高维概率密度,710–711
italic 函数,600–601
斜体字母变量,601
迭代循环,237
J
jitter 函数,276
抖动, 276
.jpeg 文件, 157–158
K
k 类别, 410, 637
KDE(核密度估计), 614, 660
kde 函数, 717, 719
kde2d 函数, 660, 671, 674, 678, 690
核密度估计(KDE), 614, 660
KernSmooth 包, 740
按键快捷键, 6
key.title 参数, 664
knitr 包, 757–758
kruskal.test 函数, 448
Kruskal-Wallis 检验, 447–450
ks 包, 741
L
标签符号,专业化, 597–601
字体, 597–598
希腊符号, 598–599
数学表达式, 599–601
labels 参数, 85, 557
labs 函数, 297
lambda 参数, 341
lapply 函数, 238
las 参数, 595–596
lattice 包, 740
layer_densities 函数, 628
layer_histograms 函数, 624, 628, 630
layer_points 函数, 626
图层, 53, 139
layer_smooths 函数, 627, 628, 630
layout 函数, 580, 664
layout.show 函数, 581
懒惰求值, 222–225
最小二乘回归, 455, 456
left 参数, 645
legend 函数, 134, 138, 232, 605
legend3d 函数, 693
交互式 3D 图上的图例, 693–694
legend.text 参数, 292
length 函数, 27, 80
length.out 值, 24, 25
小于运算符 (<), 61, 386
小于或等于运算符 (<=), 61
小于语句, 386
letters 对象, 655
levels 函数, 80, 115, 661
杠杆值, 550
计算, 555–558
与残差和库克距离结合, 563–568
说明, 555–557
库。见 包
library 命令, 7–8, 148
最佳拟合线, 458, 487
线型 (lty) 参数, 129, 133, 605, 659
线宽 (lwd) 参数, 129, 133, 659
线性模型选择与诊断
拟合优度与复杂性, 527–528
一般指导原则, 528–529
简约原则, 528
模型选择算法, 529–548
向后选择法, 537–541
向前选择法, 533–537
嵌套比较, 529–532
步进 AIC 选择法, 541–548
残差诊断, 548–568
评估正态性, 554–555
计算杠杆作用, 555–558
库克距离, 559–563
图示残差、杠杆作用和库克距离的结合, 563–568
说明离群值、杠杆作用和影响, 555–557
检查和解释残差, 549–554
线性回归。见 多元线性回归;简单线性回归
线条
添加到图形, 134–139
等高线, 绘制, 657–663
非参数双变量密度估计示例, 660–663
参数化响应面, 659–660
地形图示例, 657–658
图形中的线, 133
lines 函数, 134
lines3d 函数, 694, 708
list 命令, 92, 230
列表切片, 91
list.files 函数, 151
对象列表, 89–95
定义和组件访问, 89–91
命名, 91–93
嵌套, 93–95
lm 命令, 用于拟合线性模型, 455–456
load 命令, 11
加载
包, 7–8
工作空间图像文件, 11–12
本地环境, 167–168
局部加权散点平滑(LOESS),611–614
locator 函数, 586–587, 608, 633–634
定位, 720–725
2D 圆, 721–723
3D 锥体, 725
3D 圆柱体, 723–725
LOESS(局部加权散点平滑),611–614
loess 函数,612
log 函数,20
对数刻度,509
对数,19–20,508–512
拟合对数变换,509–511
绘制对数变换拟合,511–512
逻辑标志向量,547
逻辑运算符,64–67
逻辑值,26–27,59–72
逻辑运算符,64–67
逻辑子集和提取,68–72
作为数字,67–68
关系运算符,60–64
TRUE 或 FALSE,60
对数似然,542
循环,193–209
for 循环,193–200
通过索引或值进行循环,194–197
嵌套,197–200
使用 apply 函数的隐式循环,204–209
使用旋转,686–690
while 循环,200–204
下四分位数,274
下尾概率,351
下尾检验,386,394
ls 命令,22
lty(线型)参数,129,133,605,659
lty.hplot 参数,650
潜在变量,486
lwd(线宽)参数,129,133,659
M
main(图表标题)参数,129
Mann-Whitney U 检验,401
mar 参数,582,594
边距,绘图,582–586
剪切,584–586
自定义间距,583–584
默认间距,582–583
标记工具,601
屏蔽,252–258
数据框变量区分,255–258
函数与对象的区分,252–255
质量函数,332–342,344
伯努利分布,332–334
二项分布,333–337
dbinom 函数,335–336
pbinom 函数,336
qbinom 函数,337
rbinom 函数,337–338
泊松分布,338–342
dpois 函数,340–341
ppois 函数,340–341
qpois 函数,341
rpois 函数,341–342
MASS 包, 254–255, 401, 450, 474, 611, 622, 629, 637, 660, 686, 697, 740
字符串中的字符匹配, 77–79
数学, 17–21, 599–601. 另见 矩阵; 向量
算术, 18–19
e-表示法, 20–21
指数, 19–20
对数, 19–20
数学抽象, 725–735
莫比乌斯环, 726–729
圆环, 729–735
矩阵, 303–308
加法, 49–50
代数和, 47–52
绑定在一起, 41–42
定义, 39–42
删除元素, 44–47
的维度, 42
提取元素, 43–44
填充方向, 40–41
单位矩阵, 48
求逆, 51–52
乘法, 50–51
删除和覆盖元素, 44–47
运算和代数, 47–52
的标量倍数, 49
从中子集元素, 42–47
减法, 49–50
转置, 47–48
matrix 命令, 40, 58
max 函数, 268–269
maxColorValue 参数, 633, 643
均值
连续随机变量的, 326–329
离散随机变量的, 317–318
mean 函数, 9, 268–270, 453
均方(MS), 439, 440
均方误差(MSE)效应, 440
均方组(MSG)效应, 440
均值的假设检验, 388–402
单一均值, 389–392
两个均值, 392–402
配对/依赖样本, 398–402
合并方差, 396–398
非合并方差, 393–395
median 函数, 268–269
成员引用, 90
mfrow 参数, 579–580, 581, 616, 666, 688
mgp 参数, 595–596
Microsoft Office Excel 文件格式, 153
min 函数, 268
使用镜像安装包,747–748
misc3d包,713,718,741
missing函数,227
表格式文件中的缺失值,150
莫比乌斯带,726–729
模式,326
模型诊断,548
模型选择,492,525,528
基于模型的推理,458
MS(均方),439,440
MSE(均方误差)效应,440
MSG(均方组)效应,440
mtcars数据框,287,290,478,503,514,521,526,653,664,669,702
mtext函数,584
多重共线性。参见 共线性
多维数组,52–58
定义,53–55
子集、提取和替换,55–58
多级变量,472–477
虚拟编码,472–474
线性回归模型,474–476
来自 476–477 的预测
多项分布,342
多重线性回归,485–526
在 R 中实现并解释,490–501
额外的预测变量,490–493
查找置信区间,495
解释边际效应,493–494
全面F-检验,496–498
从多重线性模型进行预测,498–501
可视化多重线性模型,494–495
交互项,514–526
概念与动机,514–515
高阶交互作用,523–526
一个分类变量,一个连续变量,515–519
两个分类变量,519–521
两个连续变量,521–523
术语,486
理论,486–489
基本示例,488–489
以矩阵形式估计,487–488
将简单模型扩展到多个模型,487
转换数值变量,501–514
对数,508–512
多项式,502–508
使用面板的多个图,616–623
将各个面映射到分类变量,619–623
独立图,616–618
Multiple R-squared(决定系数), 460
多重检验问题, 423
多因素方差分析,443
矩阵乘法, 50–51
多元数据, 264–265
多元模型, 494
mvtnorm 包, 710, 741
N
\n(换行)转义序列, 76
NA(不可用)值, 108–110
names 函数, 93, 115
命名空间, 166–167
命名对象列表, 91–93
NaN(非数字)值, 106–108
na.omit 函数, 110, 638, 653, 676, 697
na.rm 参数, 270, 469, 640, 684
自然对数, 19
ncol 参数, 40, 168, 620
ncol 函数, 42, 97
负二项分布, 342
负系数
类别预测变量, 468–476
连续预测变量, 454, 522
负幂, 20
负偏度, 326
嵌套
if 语句, 186–189
对象列表, 93–95
for 循环, 197–200
newdata 参数, 463, 471, 511, 659
换行转义序列(\n), 76
news 函数, 746
无变化假设, 386, 421
名义变量, 262。另见 类别变量
非数值类型。参见 字符;因子;逻辑值
非参数二元密度估计示例, 660–663
非参数平滑, 611
非参数三元密度示例, 715–720
计算 3D 估计, 717
等值面选择, 717–720
正态分布, 348–357
dnorm 函数, 350
示例, 356–357
pnorm 函数, 350–353
qnorm 函数, 353–354
rnorm 函数, 355–356
归一化,使用颜色调色板进行索引连续值, 639–641
normalize 函数, 640, 644, 646, 651, 677
非数值 (NaN) 值, 106–108
不可用值 (NA), 108–110
非运算符, 65
帮助文件的备注部分, 10
nrow 参数, 40, 620
nrow 函数, 97, 101, 271, 675, 723
nuclear 数据框, 500, 533, 546, 666, 687
干扰变量, 486
空设备, 576, 580
零假设, 386, 421
空值模型, 496, 542
NULL 值, 110–114
数字作为逻辑值, 67–68
数值变量, 262, 515
变换, 501–514
对数, 508–512
多项式, 502–508
数值仿真, 421
O
对象类, 116–119
多类, 119
其他数据结构, 118–119
独立向量, 117–118
面向对象编程语言, R 作为, 116
对象赋值, 21–22
观察计数, 411, 416
oma 参数, 582
忽略元素
来自矩阵, 44–47
来自向量, 28–33, 68–72
全面 F 检验, 482, 496–498
单因子分析, 435
单边语句, 386
一对一变换, 514
单因素方差分析, 443
不透明度, 643–645, 714–715
或运算符, 65
运算顺序, 18
有序变量, 262. 参见 分类变量
正交对比, 477
outer 函数, 724
图表的外部区域, 582
异常值, 285–288, 555–557
覆盖元素
来自矩阵, 44–47
来自向量, 28–33, 68–72
owin 类, 672
P
包环境, 166–167
包, 7
引用, 748–749
在 CRAN 上查找,742
RStudio 中的安装程序和更新程序,755–756
安装,8
使用图形用户界面(GUI),744–745
在提示符下,742–744
使用本地文件,745–755
加载,7–8
遮罩,254–255
更新,8
编写,749–750
在图上填充空白,592
pairs 函数,303
palette 函数,632
调色板。参见 颜色调色板
RStudio 中的窗格,定制,753–754
par 函数,579–580,666,688
参数,265–266
参数方程,720–735
数学抽象,725–735
莫比乌斯环,726–729
圆环,729–735
简单轨迹,720–725
2D 圆形,721–723
3D 锥体,725
3D 圆柱,723–725
参数响应面,659–660
括号 (()),17,19,601
简约性,528,548
部分参数匹配,173–174
部分 F 检验,529–531
paste 函数,74–75,601
模式匹配,77–79
pbeta 函数,363
pbinom 函数,334,336
pch(点字符)参数,129,133,647,651,707
pchisq 函数,362,414
pcol 函数,633,634
.pdf 文件,157–158
PEMDAS(运算顺序),18
百分比,271–273
百分位数,274–275
persp 函数,680,682–683,686
persp3d 函数,700–706,724–731
透视图,679–690
基本绘图和角度调整,679–682
上色面板,682–686
使用循环进行旋转,686–690
persprot 函数,686–687,690
pexp 函数,360–361
pf 函数,363
p-函数,331,337,342,346,361
pgamma 函数,363
pgeom 函数,342
phyper 函数,342
PI(预测区间),462
π(圆周率)符号,373,403
pi 对象,722
饼图,293–294
pie 函数,293
管道操作符(%>%),624
像素图像,668–679
一个网格点 = 一个像素,668–671
表面截断和空像素,671–679
PlantGrowth 数据集,401
plot 函数,128,129,134,230,469,551,571,610,642,662,672
plot3d 函数,692
plot.axes 参数,665
plot.new 函数,696
plotrix 包,643
绘图,127–146。参见 自定义绘图;交互式 3D 绘图;散点图
3D 散点图,649–653
基本语法,649–650
视觉增强,650–653
添加点、线和文本,134–139
自动绘图类型,129–130
条形图,289–293
箱型图,298–300,469–470
并排显示,299–300,437,448–449
独立的,298–299
颜色,131–132
等高线图,657–668
填充颜色的等高线,663–668
绘制等高线,657–663
图形参数,129–134
线条和点的外观,133
透视图,679–690
基本绘图和角度调整,679–682
着色面板,682–686
使用循环进行旋转,686–690
饼图,293–294
像素图像,668–679
一个网格点 = 一个像素,668–671
表面截断和空像素,671–679
准备表面,653–657
概念化z-矩阵,656–657
构建评估网格,654–655
构建z-矩阵,655–656
区域限制,133–134
标题和轴标签, 130–131
使用 plot 与坐标向量, 127–129
将图形写入文件, 157–159
pnbinom 函数, 342
.png 文件, 157, 159
pnorm 函数, 350–353, 376
点字符 (pch) 参数, 129, 133, 647, 651, 707
点云, 691–699
添加更多组件, 694–699
基本 3D 云图, 692–693
可视化增强和图例, 693–694
点选坐标交互, 586–591
临时注释, 588–591
静默检索坐标, 586–587
可视化选定坐标, 587–588
点
添加到二元曲面, 701
添加到图形, 134–139
图形上的, 133
points 函数, 134, 635, 661
points3d 函数, 694, 698, 701
泊松分布, 338–342, 360
dpois 函数, 340–341
ppois 函数, 340–341
qpois 函数, 341
rpois 函数, 341–342
polygon 函数, 322, 345, 352–353, 370
多项式, 502–508
拟合多项式变换, 503–506
陷阱, 508
绘制多项式拟合, 506–508
合并方差, 396–398
位置参数匹配, 174–175
正系数
分类预测变量, 468–476
连续预测变量, 454, 522
正偏态, 295, 326
功效. 参见 统计功效
ppois 函数, 340–341
ppp 对象, 671
predict 函数, 463, 659, 706
预测, 461–468. 另见 分类预测变量
置信区间或预测区间, 461–462
插值与外推, 466–468
解释区间, 462–464
平均高度的置信区间, 463
个别观测的预测区间, 463–464
绘制区间,464–466
预测区间 (PI),462
pretty 函数,594
print 命令,244
prob 参数,274
概率,309–329。另见 概率分布
事件的补集,312–313
条件概率,311
事件与,310
两个事件的交集,311–312
概览,309–310
随机变量,313–329
连续随机变量,318–326
离散随机变量,315–318
实现,314
两个事件的并集,312
概率密度,高维,710–711
概率分布,331–363
密度函数,342–363
指数分布,359–362
正态分布,348–357
学生t-分布,357–359
均匀分布,343–347
质量函数,332–342
伯努利分布,332–334
二项分布,333–337
其他质量函数,342
泊松分布,338–342
proc.time 函数,251
进度条,249–250
提示
自定义,5
安装包,742–744
比例,271–273
测试,402–410
单一比例,402–405
两个比例,405–410
prop.test 函数,405,407–410
保护名称,170–172
pt 函数,357,380
punif 函数,346
p-值,386,387,390,412
Q
q 函数,12
qbeta 函数,363
qbinom 函数,334,337
qchisq 函数,362
qexp 函数,361
qf 函数,363
q-函数,331,337,341,342,346,353
qgamma 函数,363
qgeom 函数,342
qhyper 函数,342
qnbinom 函数,342
qnorm 函数,353–354,425
qplot 函数,140–141,144,145,296,609–611
qpois 函数,341
QQ(分位数-分位数)图,353,438,554
qqline 命令,354,438
qqnorm 函数,354,438
qt 函数,357–359
二次方程,232
quakes 数据框,264,275,392,576,644,660
quantile 函数,274,278,347,718
分位数-分位数(QQ)图,353,438,554
分位数,274–275
Quartz 窗口系统,576
qunif 函数,346–347
R
.R 扩展名,5
R markdown,758–759
R 编程语言。另见 安装 R
注释,6
控制台和编辑器窗格,5–6
帮助文件和函数文档,8–10
从 CRAN 获取和安装,3
项目网站,3
保存工作并退出,11–12
第三方编辑器,11
更新,746–747
工作目录,7
R> 提示符,5,13
弧度,720
rainbow 调色板,635,643,684,704
随机变量,313–329
连续的,318–326
的累积分布函数,323–324
的均值和方差,326–329
离散的,315–318
的累积分布函数,315–317
的均值和方差,317–318
实现,314
range 函数,268,673
原始数据,描述,261–266
类别变量,262–263
数值变量,262
参数,265–266
单变量和多变量数据,264–265
rbenchmark 包,251
rbeta 函数,363
rbind 函数,41–42,98,99,322
rbinom 函数,334,337
rchisq 函数, 362
RColorBrewer 包, 645
.RData 文件, 11, 12
read.csv 函数, 154
读取和写入文件, 147–162
读取外部数据文件, 150–156
R-ready 数据集, 147–150
写入数据文件和绘图, 156–159
read.table 函数, 151, 152, 154, 155
实数根, 233
数据框中的记录, 96
递归函数, 237–240
红绿蓝。 参见 RGB(红绿蓝)
参考水平, 468, 473, 477–478
帮助文件中的参考部分, 10
区域绘图, 582–586
剪辑, 584–586
自定义间距, 583–584
默认间距, 582–583
极限或区域, 133–134
回归系数, 454
正则表达式, 72
关系运算符, 60–64
相对设备坐标, 642
relevel 函数, 477
rep 函数, 25–26
repeat 语句, 211–214, 218, 219
重复值, 25–26
replacement 参数, 78
repos 参数, 743
使用存储库安装包, 748
重新缩放变量, 348
保留字, 170–172
resid 函数, 457
残差诊断, 548–568
正态性评估, 554–555
计算杠杆值, 555–558
Cook's 距离, 559–563
图形化结合残差、杠杆值和 Cook's 距离, 563–568
可视化异常值、杠杆值和影响, 555–557
检查和解释残差, 549–554
残差标准误差, 481, 491
残差的可视化, 456–458
分辨率, 668
响应-预测数据, 563
return 语句, 216, 220–222, 223
rev 函数, 613
rexp 函数, 361
rf 函数, 363
r-函数, 342, 347
rgamma 函数, 363
RGB(红绿蓝)
替代方法, 645–648
十六进制颜色代码, 632–635
rgb 函数, 633, 634, 640, 643, 644, 709
rgeom 函数, 342
rgl 包, 691, 699, 718, 741
rhyper 函数, 342
右偏, 295, 326
环面, 729
RJDBC 包, 156
rmarkdown 包, 758
rmultinom 函数, 342
rmvnorm 函数, 710–711
RMySQL 包, 156
rnbinom 函数, 342
rnorm 函数, 355–356
.Rnw 扩展名, 757
RODBC 包, 156
循环旋转, 686–690
round 函数, 272, 335
rownames 函数, 702
行
将矩阵绑定为, 41–42
从矩阵中提取, 43–44
rowSums 函数, 417
rpois 函数, 341
.Rproj 文件, 754
R-squared 值, 476, 491
rstandard 函数, 571
RStudio, 11, 751–779
辅助工具, 754–759
标记语言、文档和图形工具, 756–759
包安装和更新器, 755–756
项目, 754–755
调试支持, 756
基本布局和使用, 752–754
自定义面板, 753–754
编辑器功能和外观选项, 752–753
rt 函数, 357
runif 函数, 347
S
S3 类结构, 116
Salaries 数据框, 622–623, 628, 646
sample 函数, 730–731
抽样分布, 367–377
样本均值的分布, 368–373
邓迪温度示例, 369–373
标准差已知, 369
标准差未知, 369
样本比例的分布, 373–377
sapply 函数, 223
饱和度, 632
save.image 命令, 11
保存
脚本, 12
工作区图像文件, 11–12
标量倍数, 矩阵的, 49
scale_fill_grey 函数, 292–293
scale_linetype_manual 函数, 297
scale_shape_manual 函数, 304
scale_x_discrete 函数, 292–293
scale_y_continuous 函数, 292–293
scatterplot3d 函数, 649, 652
scatterplot3d 包, 741
散点图, 300–308
图形矩阵, 303–308
单一图形, 301–302
scatter.smooth 函数, 612
scope 参数, 533, 539, 542
范围, 165–171
环境, 166–168
全局环境, 166
本地环境, 167–168
包环境和命名空间, 166–167
保留和保护的名称, 170–172
搜索路径, 168–170
脚本
概述, 5
保存, 12
滚动浏览命令, 5
sd(标准差), 277, 278
search 函数, 168–169, 252
搜索路径, 168–170
segments 函数, 134, 137, 345
segments3d 函数, 694–696, 702
分号, 666
seq 函数, 24–26, 169, 350, 638, 654
数值序列的创建, 24–26
setTxtProgressBar 函数, 249–250
setwd 函数, 7, 151
shade 参数, 682, 700
阴影。参见 平滑与阴影
shape 包, 644, 647, 648, 652, 653, 669, 705, 741
shapiro.test 函数, 554, 571
Shapiro-Wilk 检验, 438, 554
shiny 包, 759
简写逻辑运算符, 452
并排箱线图, 299–300
sigma 参数, 350, 422, 560
有效数字, 643
silent 参数, 244, 246
简单线性回归, 451–483
分类预测变量, 468–483
二元变量, 468–472
改变参考水平, 477–478
与单因素方差分析(ANOVA)的等价性, 481–483
多层次变量, 472–477
将分类变量当作数值变量处理, 478–480
线性关系示例, 451–453
一般概念, 453–458
模型定义, 453–454
估计截距和斜率参数, 454–455
使用 lm 拟合线性模型, 455–456
说明残差, 456–458
预测, 461–468
置信区间或预测区间, 461–462
插值与外推, 466–468
解释区间, 462–464
绘制区间, 464–466
统计推断, 458–461
决定系数, 460
其他摘要输出, 460–461
回归系数显著性检验, 459–460
总结拟合模型, 458–459
sin 函数, 722
正弦函数, 720
单一分类预测变量, 481–482
奇异矩阵, 51
skip 参数, 155–156
切片操作, 列表, 91
斜率参数估计, 454–455
平滑和阴影, 611–615
添加 LOESS 趋势, 611–614
构造平滑密度估计, 614–615
solve 函数, 247
sort 函数, 26–27, 59–60
排序, 向量, 26–27
spatstat 包, 254, 671, 674, 678, 689, 741
特殊值, 103–114
无穷大(Inf), 104–106
非数值(NaN), 106–108
不可用(NA), 108–110
NULL, 110–114
专用函数, 233–240
一次性函数, 236
辅助函数, 233–236
外部定义, 234–235
内部定义, 235–236
递归函数, 237–240
split.screen 函数, 582
离散度, 275–280. 参见 方差
电子表格工作簿读取, 153–154
sqrt 命令, 18, 278, 318
方括号 ([])
double,用于引用列表成员, 90–91
用于包含区间中的值, 85
用于向量中的索引, 28–31, 42–43, 44
用于列表切片, 91
平方根, 18
SS(平方和), 439, 440
堆叠, if 语句, 186–189
独立的箱形图, 298–299
独立的 if 语句, 180–183
标准差 (sd), 277, 278
state.abb 对象, 306
统计功率, 428–433
功率曲线, 431–433
模拟功率, 429–431
统计, 261–288
描述原始数据, 261–266
分类变量, 262–263
数值变量, 262
参数, 265–266
单变量和多变量数据, 264–265
汇总统计, 267–288
中心性, 267–270
相关性, 280–285
计数, 271–273
协方差, 280–285
五数概括, 274–275
异常值, 285–288
百分比, 271–273
百分位数, 274–275
比例, 271–273
分位数, 274–275
离散度, 275–280
step 函数, 542–543
stop 函数, 242–243
停止条件, 237
字符串
字符, 73–74
格式, 72
stringsAsFactors 参数, 97
学生* t * 分布, 357–359
style 参数, 250
子类, 119
子集元素
来自矩阵, 42–47
在向量中
使用索引, 28–33
使用逻辑值, 68–72
substr 函数, 77
子字符串和字符匹配, 77–79
sum 函数, 253
summary 函数, 275, 279, 441, 458, 461, 497
平方和(SS), 439, 440
上标, 601
suppressWarnings 函数, 247
表面截断, 671–679
survey 数据框, 448, 451, 497, 611, 697
survival 包, 740
Sweave 标记语言, 757
switch 函数, 189–193
Sys.sleep 函数, 249–250
system.time 函数, 251
Sys.time 函数, 250–251
T
T(TRUE 的缩写), 60
\t(制表符)转义序列, 76
table 函数, 268–269, 271, 436
表格, 方差分析
使用 aov 函数构建, 440–442
构造, 439–440
tapply 函数, 270, 272, 279, 436, 444
tcl 参数, 595–596, 603
t-分布, 369, 372
terrain.colors 函数, 635, 647, 688
测试。参见 假设检验
文本,添加到图表中, 134–139
字体, 597–598
希腊符号, 598–599
数学表达式, 599–601
text 函数, 134, 353, 585, 702
text3d 函数, 702
文本进度条, 249–250
theme_bw 函数, 292–293
第三方编辑器, 11
ticktype 参数, 681
times 参数, 25–26
timing, 250–252
title 函数, 598–599
标题标签,图表上的, 130–131
to 参数, 24
topo.colors 调色板, 635, 653, 665, 677, 699, 706
地形图示例, 657–658
圆环, 729–735
trace.factor 参数, 446
trace.label 参数, 446
传统 R 图形, 139
矩阵的转置, 47–48
trees 数据框, 516, 526
三模态分布, 326
三元组, 632
三变量函数, 709, 712
三变量曲面, 709–719
3D 中的评估坐标, 709–710
等值面, 710–715
基本的一阶等值面, 712–714
控制多个层次的颜色和不透明度,714–715
高维概率密度,710–711
非参数三维密度示例,715–720
计算 3D 估计,717
等值面级别选择,717–720
原始数据,716
TRUE 值,27,60
try 语句,244–248
抑制警告信息,246–248
在函数体内使用,245–246
tryCatch 函数,246
"try-error" 类,244
tseries 包,149,741
t.test 函数,391–392,397–398
t-检验
两样本,393
Welch’s,394
.txt 扩展名,150
txtProgressBar 函数,249,250,258
Type I 错误,421–423
Bonferroni 校正,423
模拟,421–423
Type II 错误,424–428
错误率的影响,426–428
模拟,425–426
type 图形参数,129
typeof 函数,119
U
脐 torus,734–735
均匀分布,343–347
dunif 函数,344–346
punif 函数,346
qunif 函数,346–347
runif 函数,347
两个事件的并集,312
units 参数,157
单变量数据,264–265
非合并方差,393–395
update 函数,533,571
update.packages 函数,8,747,756
更新、R 和已安装的包,746–747
帮助文件中的使用部分,10
USArrests,306
UScereal 数据框,622–623,629
V
值。另见 逻辑值
通过循环,194–197
非数字。见 字符;因子;逻辑值
var 函数,278
var.equal 参数,397,402
变量映射的面板和多个图,616–623
映射到分类变量的面板,619–623
独立绘图,616–618
方差,275–280
连续随机变量的,326–329
离散随机变量,317–318
R 的面向向量行为,33–37
向量,23–37
创建,23
提取元素
使用索引,28–33
使用逻辑值,68–72
索引,30
长度,27
重复值,25–26
序列,24–25
排序,26–27
独立运行,117–118
子集
使用索引,28–33
使用逻辑值,68–72
透视图的观察角度,679–682
数据可视化。参见 数据可视化
volcano 数据集,657,668
W
warning 函数,242
警告信息
概述,242–244
抑制,246–248
帮助文件中的警告部分,10
warpbreaks 数据框,443,444,519
基于网络的文件,读取,154–155
Welch 的 t 检验,394
which 函数,69,70,71,80,431,551
while 循环,200–204
width 参数,579,603,625,628
Wilcoxon 秩和检验,401
wilcox.test 函数,401
Wilson 评分区间,405
网格框架,679
工作目录,7
工作区镜像文件,保存和加载,11–12
write.csv 函数,156
write.table 函数,156
写入文件。参见 读取和写入文件
编写函数,215–240
参数,222–232
默认设置,225–227
省略号,228–233
惰性求值,222–225
缺失,检查,227–228
function 命令,215–222
创建函数,218–219
return 语句,220–222
专门函数,233–240
一次性函数,236
辅助函数,233–236
递归函数,237–240
编写 R 包,749–750
X
xaxs 参数,592
xaxt 参数,593
x.factor 参数,446–447
xlab(轴标签)参数,129,130
XLConnect 函数,153
xlim(绘图区域限制)参数,129,133,642,673
xpd 参数,584
Y
yaxs 参数,592
yaxt 参数,593
ylab(轴标签)参数,129,130
ylim(绘图区域限制)参数,129,133,673
Z
z 值,按此着色双变量表面,703–704
zlim 参数,641,647,648,688
z-矩阵,654,655
概念化,656–657
构建,655–656
Z.test 函数,409
Z-检验,402
zval 参数,641,642,648
关于作者

Tilman M. Davies 是新西兰奥塔哥大学的讲师,在该校各个学术层次教授统计学。他已经使用 R 编程语言 10 年,并在他的所有课程中教授 R 编程。他在空间点模式建模方面的研究获得了新西兰统计学会的 Worsley 奖,并且获得了新西兰皇家学会的 Marsden Fast-Start 资助,用于研究相关问题。他每年组织一次为期三天的 R 入门工作坊,这启发他写了本书作为初学者的指南。
关于技术审稿人

Debbie Leader 已是 R 用户多年。她热衷于教授大学生统计学基础,尤其喜欢指导学生掌握 R,使他们将 R 视为统计工具箱中一项宝贵的工具。Debbie 在完成奥克兰大学的统计学博士学位后,于 2010 年加入梅西大学,成为统计学高级导师。

The Book of R 使用了 New Baskerville、Futura、Dogma 和 TheSansMono Condensed 字体,采用了 Boris Veytsman 的 LAT[E]X 2[ɛ] 包 nostarch。本书由 Sheridan Books, Inc. 在密歇根州切尔西市印刷并装订。使用的纸张是 60# Finch Offset 和 60# Sappi Matte,且获得了森林管理委员会(FSC)认证。
本书采用平开装订方式,页面用冷固化、柔性胶粘合,最终的书页首尾部分与封面连接。封面实际上并未粘合到书脊上,书籍打开时能够平躺且书脊不会破裂。
资源
访问 www.nostarch.com/bookofr/ 获取资源、勘误和更多信息。
更多实用书籍来自
NO STARCH PRESS

JavaScript 数据可视化
作者:STEPHEN A. THOMAS
2015 年 3 月,384 页,$39.95
ISBN 978-1-59327-605-8
全彩印刷

R 编程艺术
统计软件设计之旅
作者:NORMAN MATLOFF
2011 年 10 月,400 页,$39.95
ISBN 978-1-59327-384-2

统计学的错误应用
全面的指南
作者:ALEX REINHART
2015 年 3 月,176 页,$24.95
ISBN 978-1-59327-620-1

漫画版回归分析指南
作者:SHIN TAKAHASHI,IROHA INOUE,以及 TREND-PRO CO., LTD.
2016 年 5 月,232 页,$24.95
ISBN 978-1-59327-728-4

漫画版统计学指南
作者:SHIN TAKAHASHI 和 TREND-PRO CO., LTD.
2008 年 11 月,232 页,$19.95
ISBN 978-1-59327-189-3

用 Python 做数学
使用编程探索代数、统计学、微积分等内容!
作者:AMIT SAHA
2015 年 8 月,264 页,$29.95
ISBN 978-1-59327-640-9
电话:
800.420.7240 或
415.863.9900
电子邮件:
SALES@NOSTARCH.COM
网站:
WWW.NOSTARCH.COM
R 语言和数据分析完全入门

《R 语言编程指南》 是一本全面的、适合初学者的 R 语言入门指南,R 是全球最流行的统计分析编程语言。即使你没有编程经验,只有数学基础,你也能找到一切所需,开始高效地使用 R 进行统计分析。
你将从基础开始,学习如何处理数据和编写简单程序,然后逐步深入,学习如何生成数据的统计总结,执行统计检验和建模。你还将学习如何使用 R 的基础图形工具和贡献包(如 ggplot2 和 ggvis)创建令人印象深刻的数据可视化,甚至使用 rgl 包创建交互式 3D 可视化。
数十个动手练习(附有可下载的解决方案)将理论与实践结合,帮助你学习:
• R 编程的基础知识,包括如何编写数据框、创建函数,以及使用变量、语句和循环
• 统计学概念,如探索性数据分析、概率、假设检验和回归建模,以及如何在 R 中实现它们
• 如何访问 R 语言的数千个函数、库和数据集
• 如何从数据中得出有效且有用的结论
• 如何创建高质量的出版图形
本书通过详细的解释、现实世界的例子和练习,帮助你深入理解统计学以及 R 语言的功能深度。将《R 语言宝典》作为你进入数据分析不断发展的世界的门户。
关于作者
提尔曼·M·戴维斯是新西兰奥塔哥大学的讲师,他在该校各个层次教授统计学和 R 语言课程。他已经使用 R 语言编程 10 年,并在所有课程中都使用它。

极客娱乐中的精华^™
“我躺平。”
本书采用耐用的装订方式,不会轻易合上。





。








。如果它为负,则没有解,应在控制台打印适当的消息。
和
给出。
的计算公式如下:

分位数、
英尺更矮的树的概率是多少?


















浙公网安备 33010602011771号