R-语言实战第三版-全-

R 语言实战第三版(全)

原文:R in Action, Third Edition

译者:飞龙

协议:CC BY-NC-SA 4.0

前置内容

前言

没有图片或对话的书有什么用?

——爱丽丝,《爱丽丝梦游仙境》

它是神奇的,有宝藏可以满足微妙和粗俗的欲望;但它不是为胆小的人准备的。

——Q,“Q 是谁?”《星际迷航:下一代》

当我开始写这本书时,我花了很多时间去寻找一个好的引言来开始。我最终找到了两个。R 是一个探索、可视化和理解数据的奇妙灵活的平台和语言。我选择了来自《爱丽丝梦游仙境》的引言来捕捉今天统计分析的风味——一个探索、可视化和解释的互动过程。

第二个引言反映了普遍认为 R 难以学习的观点。我希望向你展示,这并不一定如此。R 广泛而强大,拥有如此多的分析和图形功能(最后统计超过 50,000 个),它很容易让新手和经验丰富的用户都感到害怕。但表面上的疯狂是有原因的。有了指导和说明,你可以导航可用的巨大资源,选择你需要来完成工作的工具,以风格、优雅、效率——以及一点点酷。

几年前,当我申请一个新的统计咨询职位时,我第一次接触到了 R。潜在的雇主在面试前的材料中问我是否熟悉 R。遵循招聘人员的标准建议,我立刻说“是”,并开始学习它。我是一名经验丰富的统计学家和研究人员,有 25 年作为 SAS 和 SPSS 程序员的经验,并且精通六种编程语言。这能有多难?这是著名的最后遗言。

当我试图尽快学习这门语言(面试即将来临)时,我发现要么是关于语言底层结构的巨著,要么是针对特定高级统计方法的密集论文,这些论文是由和为该领域专家所写。在线帮助以斯巴达风格编写,更像参考而不是教程。每次当我以为我已经掌握了 R 的整体组织和功能时,我都会发现一些新的东西,让我感到无知和渺小。

为了理解这一切,我将 R 视为一个数据科学家。我思考了成功处理、分析和理解数据需要什么,包括

  • 访问数据(将数据从多个来源导入应用程序)

  • 清洗数据(编码缺失数据,修复或删除错误编码的数据,将变量转换为更有用的格式)

  • 注释数据(记住每一部分代表什么)

  • 总结数据(获取描述性统计以帮助描述数据)

  • 可视化数据(因为一张图片确实胜过千言万语)

  • 模拟数据(揭示关系和测试假设)

  • 准备结果(创建出版物质量的表格和图形)

然后,我试图理解如何使用 R 完成这些任务中的每一个。因为我通过教学来学习最好,所以我最终创建了一个网站 (www.statmethods.net) 来记录我所学到的知识。

然后,大约一年后,Marjan Bace,Manning 出版社的出版人,打电话问我是否愿意写一本关于 R 的书。我已经写了 50 篇期刊文章、4 本技术手册、许多本书的章节,以及一本关于研究方法的书,所以这能有多难呢?冒着重复的风险——这是最后的遗言。

第一版于 2011 年出版,第二版于 2015 年出版。我大约两年半前开始着手编写第三版。描述 R 总是一个不断变化的目标,但最近几年发生了一场某种意义上的革命。这得益于大数据的增长、tidyverse (tidyverse.org) 软件被广泛采用、新的预测分析和机器学习方法的快速发展,以及新的、更强大的数据可视化技术的发展。我希望第三版能够公正地反映这些重要的变化。

您手中所持的这本书是我多年前一直希望拥有的。我试图为您提供一本 R 的指南,让您能够快速访问这个伟大开源项目的力量,而无需经历所有的挫折和焦虑。希望您喜欢它。

P.S. 我被提供了这份工作,但我没有接受。但是学习 R 让我的职业生涯走向了我从未预料到的方向。生活有时真的很幽默。

致谢

许多人努力使这本书变得更好。他们包括以下人员:

  • Marjan Bace,Manning 出版社的出版人,他最初邀请我写这本书。

  • Sebastian Stirling、Jennifer Stout 和 Karen Miller,分别负责第一版、第二版和第三版的发展编辑。他们各自投入了大量的时间来帮助我组织材料、阐明概念,并使文本更加有趣。

  • Mike Shepard,技术校对员,他帮助揭示了一些容易混淆的领域,并为测试代码提供了独立和专业的视角。我依赖他细致的审查和深思熟虑的判断。

  • Aleks Dragosavljević,我的审稿编辑,他帮助获得审稿人和协调审稿过程。

  • Deirdre Hiam,她帮助这本书顺利通过了生产过程,以及她的团队:Suzanne G. Fox,我的校对编辑,和 Katie Tennant,我的校对员。

  • 那些花费自己宝贵时间仔细阅读材料、寻找错别字并提出宝贵实质性建议的同行审稿人:Alain Lompo、Alessandro Puzielli、Arav Agarwal、Ashley Paul Eatly、Clemens Baader、Daniel C Daugherty、Daniel Kenney-Jung、Erico Lendzian、James Frohnhofer、Jean-François Morin、Jenice Tom、Jim Frohnhofer、Kay Engelhardt、Kelvin Meeks、Krishna Shrestha、Luis Felipe Medeiro Alves、Mario Giesel、Martin Perry、Nick Drozd、Nicole Koenigstein、Robert Samohyl、Tiklu Ganguly、Tom Jeffries、Ulrich Gauger、Vishal Singh。

  • 在书完成之前就购买了这本书的许多 Manning Early Access Program (MEAP) 参与者,提出了很好的问题,指出了错误,并提出了有用的建议。

每位贡献者都使这本书变得更好、更全面。

我还想感谢许多为使 R 成为一个如此强大的数据分析平台做出贡献的软件作者。他们不仅包括核心开发者,还包括那些无私地创建和维护贡献包的个人,极大地扩展了 R 的功能。附录 E 提供了本书中描述的贡献包作者的列表。特别是,我想提到 John Fox、Hadley Wickham、Frank E. Harrell, Jr.、Deepayan Sarkar 和 William Revelle,我非常钦佩他们的作品。我已经尽力准确地代表他们的贡献,并且我对本书中无意中包含的任何错误或扭曲负有完全责任。

我真的应该首先感谢我的妻子和伴侣,Carol Lynn。尽管她对统计学或编程没有内在的兴趣,但她阅读了每一章多次,并提出了无数次的纠正和建议。没有任何人比阅读另一人的多元统计分析更伟大的爱了。同样重要的是,她以优雅、支持和爱意忍受了我写作这本书时的漫长夜晚和周末。我无法给出合理的解释,为什么我会这么幸运。

我还想感谢另外两个人。一个是我的父亲,他对科学的热爱是鼓舞人心的,并让我对数据的价值有了认识。我非常想念他。另一个是我的研究生导师 Gary K. Burger,当我以为我想成为一名临床医生时,Gary 让我对统计学和教学产生了兴趣。这完全是他的错。

关于这本书

如果你拿起这本书,你可能有一些需要收集、总结、转换、探索、建模、可视化或展示的数据。如果是这样,那么 R 就是为你准备的。R 已经成为全球统计学、预测分析和数据可视化的语言。它提供了目前可用的最广泛的方法来理解数据,从最基础的到最复杂和前沿的。

作为开源项目,它可以在包括 Windows、macOS 和 Linux 在内的各种平台上免费使用。它正在不断开发中,每天都有新的程序被添加。此外,R 还得到了一个庞大且多样化的数据科学家和程序员社区的支援,他们乐于向用户提供帮助和建议。

虽然 R 可能最出名的是其创建美丽和复杂图表的能力,但它几乎可以处理任何统计问题。基础安装提供了数百个数据管理、统计和图形功能,直接从盒子里出来。但其中一些最强大的功能来自于贡献作者提供的数千个扩展(包)。

这种广度是有代价的。对于新用户来说,可能很难把握 R 是什么以及它能做什么。即使是经验最丰富的 R 用户也可能惊讶地发现他们之前不知道的功能。

R in Action, 第三版》为您提供了对 R 的引导性介绍,对平台及其功能有一个 2,000 英尺的高度视角。它将向您介绍基础安装中最重要的函数以及 70 多个最有用的贡献包。在整个书中,目标是实际应用——如何理解您的数据并将这种理解传达给他人。当您完成时,您应该对 R 的工作原理、它能做什么以及您可以去哪里学习更多有很好的把握。您将能够应用各种数据可视化技术,并具备解决基本和高级数据分析问题的技能。

第三版的新内容

第三版有许多变化,包括对 tidyverse 数据管理和分析方法的广泛覆盖。以下是一些更显著的变化。

第二章(创建数据集)现在包括对 readrreadxlhaven 包的介绍,用于导入数据。还有一个关于 tibbles 的新章节,这是数据框的现代更新。

第三章(基本数据管理)和第五章(高级数据管理)包括对 dplyrtidyr 包的介绍,用于数据管理、转换和汇总。

第四章(图形入门)、第六章(基本图形)、第十一章(中级图形)和第十九章(高级图形)都是新章节,对 ggplot2 及其扩展提供了广泛的覆盖。

第十六章(聚类分析)提供了改进的图形和关于评估数据聚类能力的新章节。

第十七章(分类)有一个关于使用分解和 Shapley 值图来理解黑盒模型的新章节。

第十八章(缺失数据的高级方法)已经扩展,新增了关于 k 近邻和随机森林方法对缺失值插补的新内容。

第二十章(高级编程)有关于非标准评估和可视调试的新章节。

第二十一章(创建动态报告)扩展了 R Markdown 的覆盖范围,并增加了关于参数化报告和常见编码错误的新章节。

第二十二章(创建包)已完全重写,以包含用于简化包创建的新工具。还有关于如何通过 CRAN、GitHub 和软件生成的网站分享和推广您的包的新章节。

附录 A(图形用户界面)已更新,以反映该领域的快速变化。

附录 B(自定义启动环境)已修订,包括新的自定义方法和对可重复研究潜在副作用敏感性的提高。

附录 F(处理大型数据集)涵盖了适用于大于 RAM 数据集的新包、针对千兆级问题的分析方法和将 R 与云服务集成的内容。

本书中有关于使用 RStudio 进行编程、调试、报告编写和包创建的新章节,散布全书。最后,全书进行了许多更新和修正。

适合阅读此书的人群

《R 实战,第三版》 应该对任何处理数据的人都有吸引力。不假设有统计编程或 R 语言的背景。尽管本书对新手来说很容易理解,但应该有足够的新颖和实用材料,甚至能满足经验丰富的 R 专家的需求。

没有统计背景但想使用 R 来操作、总结和绘图数据的用户会发现第 1-6 章、第十一章和第十九章很容易理解。第七章和第十章假设统计学为一学期课程;第八章、第九章和第 12-18 章的读者将受益于两个学期的统计学。第 20-22 章更深入地探讨了 R 语言,没有统计学先决条件。我尽量让每个章节都让初学者和专家数据分析师都能找到有趣和有用的内容。

本书如何组织:路线图

本书旨在为您提供一个 R 平台的导游,重点关注那些最适用于操作、可视化和理解数据的技巧。本书共有 22 章,分为 5 部分:“入门”,“基本方法”,“中级方法”,“高级方法”和“扩展技能”。七个附录中涵盖了其他主题。

第一章从介绍 R 及其作为数据分析平台如此有用的特性开始。本章涵盖了如何获取程序以及如何通过在线可用的扩展增强基本安装。本章的其余部分用于探索用户界面和了解如何运行您的第一个程序。

第二章涵盖了将数据输入 R 的许多方法。本章的前半部分介绍了 R 用于存储数据的结构。后半部分讨论了从键盘、文本文件、网页、电子表格、统计软件包和数据库导入数据到 R 的方法。

第三章涵盖了基本的数据管理,包括排序、合并和子集化数据集,以及变换、重新编码和删除变量。

第四章通过图形语法介绍数据可视化。我们回顾了创建图形、修改它们以及以各种格式保存图形的方法。

建立在第三章内容的基础上,第五章涵盖了使用函数(数学、统计、字符)和控制结构(循环、条件执行)进行数据管理的应用。然后我们讨论了如何编写自己的 R 函数以及如何以各种方式重塑和汇总数据。

第六章演示了创建常见单变量图形的方法,例如条形图、饼图、直方图、密度图、箱线图、树状图和点图。每种图形都有助于理解单个变量的分布。

第七章首先展示了如何总结数据,包括描述性统计和交叉表的使用。然后我们探讨了解释两个变量之间关系的基本方法,包括相关系数、t 检验、卡方检验和非参数方法。

第八章介绍了回归方法,用于建模数值结果变量与一组一个或多个数值预测变量之间的关系。详细讨论了拟合这些模型、评估其适当性和解释其意义的方法。

第九章通过方差分析和其变体来考虑基本实验设计的分析。在这里,我们通常对治疗组合或条件如何影响数值结果感兴趣。还涵盖了评估分析适当性和可视化结果的方法。

第十章详细介绍了功效分析。从假设检验的讨论开始,本章重点介绍了如何确定在给定置信度下检测到特定大小治疗效应所需的样本量。这可以帮助你规划可能产生有用结果的实验和准实验研究。

第十一章在第六章的基础上扩展了内容,涵盖了创建有助于可视化两个或更多变量之间关系的图形。这些包括各种类型的二维和三维散点图、散点图矩阵、线图、自相关图和 mosaic 图。

第十二章介绍了在数据来自未知或混合分布、样本量小、异常值是问题、或者基于理论分布设计适当的测试过于复杂且数学上难以处理的情况下,效果良好的分析方法。它们包括重采样和自助法——这些是易于在 R 中实现的计算密集型方法。

第十三章在第八章回归方法的基础上进行了扩展,涵盖了非正态分布的数据。章节从讨论广义线性模型开始,然后专注于尝试预测结果变量为分类(逻辑回归)或计数(泊松回归)的情况。

多变量数据问题的一个挑战是简化。第十四章描述了将大量相关变量转换成较小的一组不相关变量的方法(主成分分析),以及揭示给定变量集下潜在结构的方法(因子分析)。适当分析中涉及的许多步骤都进行了详细说明。

第十五章描述了创建、操作和建模时间序列数据的方法。它涵盖了可视化分解时间序列数据以及预测未来值的指数和 ARIMA 方法。

第十六章展示了将观测值聚类到自然发生组中的方法。章节从讨论综合聚类分析中的常见步骤开始,然后介绍了层次聚类和分区方法。还介绍了确定适当聚类数量的几种方法。

第十七章介绍了用于将观测值分类到组中的流行监督机器学习方法。依次考虑了决策树、随机森林和支持向量机。你还将了解评估每种方法准确性的方法。还介绍了理解结果的新方法。

为了保持我展示数据分析实用方法的尝试,第十八章探讨了处理缺失数据值这一普遍问题的现代方法。R 语言支持多种优雅的方法来分析不完整的数据集。这里描述了其中一些最好的方法,并提供了何时使用哪些方法以及何时避免使用哪些方法的指导。

第十九章通过深入探讨自定义坐标轴、颜色方案、字体、图例、注释和绘图区域来结束对图形的讨论。你将学习如何将多个图形组合成一个单一的图表。最后,你将学习如何将静态图形转换为基于网络的交互式可视化。

第二十章涵盖了高级编程技术。你将了解面向对象编程技术和调试方法。本章还提供了高效编程的技巧。如果你正在寻求更深入地了解 R 语言的工作原理,这将特别有帮助,并且它是第二十二章的先决条件。

第二十一章描述了从 R 语言内部创建吸引人报告的几种方法。你将学习如何从你的 R 代码生成网页、报告、文章,甚至书籍。生成的文档可以包括你的代码、结果表、图表和评论。

最后,第二十二章提供了一个创建 R 包的逐步指南。这将使你能够创建更复杂的程序,高效地记录它们,并与他人分享。关于分享和推广你的包的方法将在详细讨论。

后记会指引你到许多关于 R 语言的最佳互联网网站,帮助你加入 R 社区,解答问题,并跟上这个快速变化的产品的发展。

最后,但同样重要的是,七个附录(A 至 G)扩展了文本的范围,包括如 R 图形用户界面、定制和升级 R 安装、将数据导出到其他应用程序、使用 R 进行矩阵代数(类似于 MATLAB)、以及处理非常大的数据集等有用的主题。

我们还提供了一章额外的章节,仅在出版商的网站上提供,网址为www.manning.com/books/r-in-action-third-edition。在线第二十三章涵盖了lattice包,这是 R 语言中数据可视化的另一种方法。

数据挖掘者的建议

数据挖掘是分析领域的一个分支,专注于在大数据集中发现模式。许多数据挖掘专家正在转向 R 语言,因为它具有前沿的分析能力。如果你是一名正在转向 R 语言的数据挖掘者,希望尽快掌握这门语言,我建议以下阅读顺序:第一章(介绍),第二章(数据结构和与你的环境相关的数据导入部分),第四章(基本数据管理),第七章(描述性统计),第八章(第 1、2 和 6 节,回归),第十三章(第二部分,逻辑回归),第十六章(聚类),第十七章(分类),以及附录 F(处理大型数据集)。然后根据需要回顾其他章节。

关于代码

为了使这本书尽可能广泛地适用,我选择了来自心理学、社会学、医学、生物学、商业和工程等多个学科的例子。这些例子都不需要对该领域有专门的知识。

这些示例中使用的数据集被选中是因为它们提出了有趣的问题,并且它们很小。这让你能够专注于描述的技术,并快速理解涉及的过程。当你学习新方法时,越小越好。数据集包含在 R 的基本安装中,或者可以通过在线提供的附加包获得。

本书使用以下排版约定:

  • 代码列表使用等宽字体,应直接输入。

  • 等宽字体也用于一般文本中,以表示代码单词或先前定义的对象。

  • 代码列表中的 斜体 表示占位符。你应该用适当的文本和值替换它们,以解决手头的问题。例如,path_to_my_file 将被替换为计算机上文件的实际路径。

  • R 是一种交互式语言,它通过提示(默认为 >)来指示用户输入下一行。本书中的许多列表都捕捉了交互会话。当你看到以 > 开头的代码行时,不要输入提示。

  • 使用代码注释代替内联注释(Manning 书籍中的常见约定)。此外,一些注释带有编号的子弹点(如 ❶),它们引用文本中稍后出现的解释。

  • 为了节省空间或使文本更易读,交互会话的输出可能包括额外的空白或省略与讨论点无关的文本。

你可以从本书的 liveBook(在线)版本中获取可执行的代码片段,网址为 livebook.manning.com/book/r-in-action-third-edition。书中示例的完整代码可以从 Manning 网站 www.manning.com/books/r-in-action-third-edition 和 GitHub www.github.com/rkabacoff/RiA3 下载。为了最大限度地利用本书,我建议你在阅读时尝试这些示例。

最后,有一个常见的格言说,如果你问两位统计学家如何分析一个数据集,你会得到三个答案。这个断言的反面是,每个答案都会让你更接近对数据的理解。我并不声称某个特定的分析是最佳或唯一的方法。使用本书中教授的技能,我邀请你玩转数据,看看你能学到什么。R 是交互式的,最好的学习方式就是实验。

liveBook 讨论论坛

购买《R in Action,第三版》包括免费访问 liveBook,这是曼宁的在线阅读平台。使用 liveBook 的独家讨论功能,您可以在全球范围内或针对特定章节或段落附加评论。为自己做笔记、提问和回答技术问题,以及从作者和其他用户那里获得帮助都非常简单。要访问论坛,请访问 livebook.manning.com/book/r-in-action-third-edition/discussion。您还可以在 livebook.manning.com/discussion 上了解更多关于曼宁论坛和行为准则的信息。

曼宁对读者的承诺是提供一个场所,让读者之间以及读者与作者之间可以进行有意义的对话。这不是对作者参与特定数量活动的承诺,作者对论坛的贡献仍然是自愿的(且未付费)。我们建议您尝试向作者提出一些挑战性的问题,以免他的兴趣转移。只要该书有售,论坛和先前讨论的存档将可通过出版社的网站访问。

关于作者

Dr. Robert Kabacoff 是韦斯利安大学定量分析学教授,同时也是一位经验丰富的数据科学家,在商业、医疗保健和政府环境中提供统计编程和数据分析支持已有 30 多年。他教授过本科生和研究生数据分析与统计编程课程,并管理着位于 statmethods.net 的 Quick-R 网站,以及位于 rkabacoff.github.io/datavis 的 R 数据可视化网站。

关于封面插图

《R in Action,第三版》封面上的图像是“来自扎达尔的人”,取自尼古拉·阿森诺维奇(Nikola Arsenović)拍摄于 19 世纪中叶的克罗地亚传统服饰专辑。

在那些日子里,人们通过他们的着装很容易就能识别出他们住在哪里,以及他们的职业或社会地位。曼宁通过基于几个世纪前丰富多样的地区文化的封面设计,庆祝计算机行业的创新和主动性,这些文化通过像这样的收藏品中的图片被重新带回生活。

第一部分. 入门

欢迎来到R in Action!R 是目前最受欢迎的数据分析和可视化平台之一。它是免费的开源软件,适用于 Windows、macOS 和 Linux 操作系统。本书将为你提供掌握这一综合软件并有效应用于你自己的数据的技能。

本书分为四个部分。第一部分涵盖了安装软件、学习导航界面、导入数据以及将数据整理成可用于进一步分析的格式的基础知识。

第一章全部关于熟悉 R 环境。本章从对 R 及其作为现代数据分析强大平台的特征的概述开始。在简要描述如何获取和安装软件之后,本章通过一系列简单示例探索用户界面。接下来,你将学习如何通过从在线存储库免费下载的扩展(称为贡献包)来增强基本安装的功能。本章以一个示例结束,让你能够测试你的新技能。

一旦你熟悉了 R 的界面,接下来的挑战就是将数据导入程序中。在我们信息丰富的世界中,数据可以来自许多来源,并以多种格式存在。第二章涵盖了将数据导入 R 的各种方法。本章的前半部分介绍了 R 用来存储数据的结构,并描述了如何手动输入数据。后半部分讨论了从文本文件、网页、电子表格、统计软件包和数据库导入数据的方法。

数据很少以可以直接使用的格式出现。你通常必须花费大量时间将来自不同来源的数据组合起来,清理混乱的数据(错误编码的数据、不匹配的数据、缺失的数据),并创建新的变量(组合变量、转换变量、重新编码变量),然后才能回答你的问题。第三章涵盖了 R 中的基本数据管理任务,包括排序、合并和子集数据集以及转换、重新编码和删除变量。

许多用户第一次接触 R 是因为它强大的图形功能。第四章提供了图形语法ggplot2的概述。你将从构建一个简单的图形开始,然后逐个添加功能,直到创建一个全面的数据可视化。你还将学习如何进行基本的图形定制,并将你的工作保存为各种图像格式。

第五章基于第三章的内容。它涵盖了在数据管理中使用数值(算术、三角和统计)和字符函数(字符串子集、连接和替换)的应用。本节中使用了综合示例来展示许多函数。接下来,讨论了控制结构(循环、条件执行),你将学习如何编写自己的 R 函数。编写自定义函数允许你通过将许多编程步骤封装成一个单一、灵活的函数调用来扩展 R 的功能。最后,讨论了重新组织(重塑)和汇总数据的有效方法。重塑和汇总在准备数据以供进一步分析时通常很有用。

完成第一部分后,你将彻底熟悉在 R 环境中进行编程。你将具备进入或访问数据、清理数据以及为后续分析准备数据的所需技能。你还将获得创建、定制和保存各种图表的经验。

1 R 简介

本章涵盖

  • 安装 R 和 RStudio

  • 理解 R 语言

  • 运行程序

近年来,我们分析数据的方式发生了巨大的变化。随着个人电脑和互联网的出现,我们可用的数据量急剧增长。公司拥有与消费者互动的数 TB 数据,政府、学术和私人研究机构在各个研究主题上都有广泛的存档和调查数据。从这些庞大的数据存储中获取信息(更不用说智慧)已经成为一个行业。同时,以易于访问和易于消化的方式呈现信息变得越来越具有挑战性。

数据分析的科学(统计学、心理测量学、计量经济学和机器学习)与数据的爆炸性增长保持了同步。在个人电脑和互联网出现之前,学术研究人员开发了新的统计方法,并将它们作为理论论文发表在专业期刊上。程序员可能需要数年才能适应这些方法,并将它们纳入广泛可用的统计软件包中。现在,新的方法每天都在出现。统计研究人员在易于访问的网站上发布新的和改进的方法,以及产生它们的代码。

个人电脑的出现对我们分析数据的方式产生了另一种影响。当在大型机上进行数据分析时,计算机时间宝贵且难以获得。分析师们会仔细设置计算机运行,包括他们认为可能需要的所有参数和选项。该程序产生的输出可能长达数十或数百页。分析师会筛选这些输出,提取有用材料并丢弃其余部分。许多流行的统计软件包(如 SAS 和 SPSS)最初是在这个时期开发的,并且在某种程度上仍然遵循这种方法。

由于个人电脑提供的廉价和便捷的访问,现代数据分析的模式已经发生了转变。而不是一次性设置完整的数据分析,这个过程已经变得高度交互式,每个阶段的输出都作为下一阶段的输入。图 1.1 显示了典型的分析。在任何时候,循环可能包括转换数据、填补缺失值、添加或删除变量、拟合统计模型,并再次通过整个流程。当分析师认为他们已经深入理解了数据并回答了所有可以回答的相关问题时,这个过程才会停止。

图片

图 1.1 典型数据分析步骤

个人电脑(尤其是高分辨率显示器的可用性)的出现也影响了结果的理解和展示方式。一张图片确实可以胜过千言万语,人类擅长从视觉展示中提取有用信息。现代数据分析越来越多地依赖于图形展示来揭示意义和传达结果。

数据分析师现在需要从广泛的数据源(数据库管理系统、文本文件、统计软件包、电子表格和网页)中获取数据,合并数据片段,清洗和注释它们,使用最新的方法进行分析,以有意义和图形吸引人的方式展示结果,并将结果纳入吸引人的报告中,以便分发给利益相关者和公众。正如你将在本章中看到的,R 是一个综合性的软件包,非常适合实现这些目标。

1.1 为什么使用 R?

R 是一种用于统计计算和图形的语言和环境,类似于最初在贝尔实验室开发的 S 语言。它是一个开源的数据分析解决方案,得到了一个庞大且活跃的全球研究社区的支撑。但许多流行的统计和绘图软件包也都可以使用(例如 Microsoft Excel、SAS、IBM SPSS、Stata 和 Minitab)。为什么选择 R?

R 有许多值得推荐的特点:

  • 大多数商业统计软件平台的价格是数千美元,如果不是数万美元。R 是免费的!如果你是教师或学生,这些好处是显而易见的。

  • R 是一个全面的统计平台,提供了各种数据分析技术。几乎任何类型的数据分析都可以在 R 中完成。

  • R 包含了其他软件包中尚未提供的先进统计程序。实际上,每周都会有新的方法可供下载。如果你是 SAS 用户,想象一下每隔几天就有一个新的 SAS PROC。

  • R 拥有最先进的图形功能。如果你想可视化复杂的数据,R 拥有最全面和强大的功能集。

  • R 是一个强大的交互式数据分析和平滑探索的平台。从其诞生之初,它就被设计来支持图 1.1 中概述的方法。例如,任何分析步骤的结果都可以轻松保存、操作,并用作其他分析的输入。

  • 从多个来源获取可用的数据可能具有挑战性。R 可以轻松地从各种来源导入数据,包括文本文件、数据库管理系统、统计软件包和专门的数据存储。它还可以将这些系统中的数据写出来。R 还可以直接从网页、社交媒体网站和广泛的在线数据服务中访问数据。

  • R 提供了一个无与伦比的编程新统计方法的平台,方式简单直接。它易于扩展,并提供了一种自然语言,可以快速编程最近发表的方法。

  • R 的功能可以集成到用其他语言编写的应用程序中,包括 C++、Java、Python、PHP、Pentaho、SAS 和 SPSS。这允许你在熟悉的语言中继续工作,同时将 R 的功能添加到你的应用程序中。

  • R 可以在包括 Windows、Unix 和 macOS 在内的多种平台上运行。它很可能在你的任何一台电脑上都能运行。(我甚至见过安装 R 在 iPhone 上的指南,这很令人印象深刻,但可能不是一个好主意。)

  • 如果你不想学习一门新语言,有许多图形用户界面(GUIs)可供选择,它们通过菜单和对话框提供 R 的强大功能。

你可以在图 1.2 中看到 R 的图形功能示例。这张图描述了六个行业中男性和女性工作经验与工资之间的关系,数据来自 1985 年的美国人口普查。从技术上讲,这是一个散点矩阵图,性别通过颜色和符号来表示。趋势是通过线性回归线来描述的。如果你对“散点图”和“回归线”这些术语不熟悉,不要担心。我们将在后面的章节中介绍它们。

图片

图 1.2 六个行业中男性和女性工资与工作经验之间的关系。这样的图表可以用 R 中几行代码轻松创建(使用mosaicData包创建的图表)。

从这张图中可以得出的更有趣的发现是

  • 经验与工资之间的关系因性别和行业而异。

  • 在服务业,无论是男性还是女性,工资似乎并不随着经验的增加而提高。

  • 在管理职位上,男性的工资往往随着经验的增加而提高,但女性的工资则不然。

这些差异是真实的,还是可以解释为随机抽样变异?我们将在第八章中进一步讨论这个问题。重要的是,R 允许你以简单直接的方式创建优雅、信息丰富、高度定制的图表。在其他统计语言中创建类似的图表可能会很困难、耗时或不可能。

不幸的是,R 的学习曲线可能很陡峭。因为它可以做很多事情,所以文档和帮助文件非常庞大。此外,由于许多功能来自独立贡献者创建的可选模块,这些文档可能分散且难以找到。实际上,掌握 R 能做什么是一项挑战。

这本书的目标是让访问 R 变得快速和简单。我们将浏览 R 的许多功能,涵盖足够的材料来帮助你开始数据处理,并提供当你需要学习更多时去哪里学习的指南。让我们从安装程序开始吧。

1.2 获取和安装 R

R 可从综合 R 存档网络(CRAN)免费获得,网址为 cran.r-project.org。预编译的二进制文件适用于 Linux、macOS 和 Windows。请按照你选择平台上的安装说明安装基本产品。稍后,我们将讨论通过可选模块(也称为包)添加功能,这些包也来自 CRAN。

1.3 使用 R

R 是一个区分大小写的解释型语言。你可以在命令提示符(>)中逐个输入命令,或者从一个源文件中运行一组命令。它有各种各样的数据类型,包括向量、矩阵、数据框(类似于数据集)和列表(对象的集合)。我将在第二章中讨论这些数据类型。

大多数功能都是通过内置和用户创建的函数以及对象的创建和操作来提供的。一个 对象 基本上是可以赋予值的任何东西。对于 R 来说,这几乎包括一切(数据、函数、图表、分析结果等等)。每个对象都有一个 类属性(基本上是一个或多个相关的文本描述符),它告诉 R 如何打印、绘图、总结或以其他方式操作该对象。

在交互式会话期间,所有对象都保存在内存中。基本函数默认可用。其他函数包含在可以按需附加到当前会话的包中。

语句由函数和赋值组成。R 使用 <- 符号进行赋值,而不是典型的 = 符号。例如,以下语句

x <- rnorm(5)

创建一个名为 x 的向量对象,其中包含来自标准正态分布的五个随机偏差值。

注意 R 允许使用 = 符号进行对象赋值。尽管如此,你不会发现很多程序是这样编写的,因为这不是标准语法,在某些情况下可能无法工作,而且 R 程序员会取笑你。你还可以反转赋值方向。例如,rnorm(5) -> x 等同于前面的语句。再次强调,这样做并不常见,本书也不推荐这样做。

注释以 # 符号开头。R 解释器会忽略 # 符号后面的任何文本。一个示例程序在第 1.3.1 节中给出。

1.3.1 开始使用

使用 R 的第一步当然是安装它。安装说明可在 CRAN 上找到。一旦安装了 R,启动它。如果你使用 Windows,可以从开始菜单启动 R。在 Mac 上,双击应用程序文件夹中的 R 图标。在 Linux 上,在终端窗口的命令提示符中输入 R。这些操作中的任何一个都会启动 R 界面(见图 1.3 中的示例)。

图片

图 1.3 Windows 上 R 界面的示例

为了了解界面,让我们通过一个简单的例子来操作。假设你正在研究身体发展,并且你已经收集了 10 个婴儿的年龄和体重

他们生命中的第一年(见表 1.1)。你感兴趣的是体重的分布及其与年龄的关系。

表 1.1 10 名婴儿的年龄和体重

年龄(月) 体重(kg)
01 4.4
03 5.3
05 7.2
02 5.2
11 8.5
09 7.3
03 6.0
09 10.4
12 10.2
03 6.1
注意:这些数据是虚构的。

列表 1.1 展示了分析。年龄和体重数据使用函数 c() 作为向量输入,该函数将其参数组合成一个向量或列表。体重的平均值和标准差,以及年龄和体重之间的相关性,分别由函数 mean()sd()cor() 提供。最后,使用 plot() 函数将年龄与体重绘制在一起,允许你直观地检查趋势。q() 函数结束会话并允许你退出。

列表 1.1 一个示例 R 会话

> age <- c(1,3,5,2,11,9,3,9,12,3)
> weight <- c(4.4,5.3,7.2,5.2,8.5,7.3,6.0,10.4,10.2,6.1)
> mean(weight)
[1] 7.06
> sd(weight)
[1] 2.077498
> cor(age,weight)
[1] 0.9075655
> plot(age,weight)

从列表 1.1 可以看出,这 10 名婴儿的平均体重为 7.06 千克,标准差为 2.08 千克,年龄(以月为单位)和体重(以千克为单位)之间存在强烈的线性关系(相关系数 = 0.91)。这种关系也可以在图 1.4 的散点图中看到。不出所料,随着婴儿的成长,他们往往会变得更重。

图 1.4 婴儿体重(kg)与年龄(月)的散点图

图 1.4 中的散点图很有信息量,但有些实用且不够吸引人。在后面的章节中,你将了解到如何创建更吸引人和复杂的图表。

提示:为了了解 R 在图形方面的能力,请参阅《使用 R 进行数据可视化》(rkabacoff.github.io/datavis)和《Top 50 ggplot2 可视化 – 主列表》(r-statistics.co/Top50-Ggplot2-Visualizations-MasterList-R-Code.html)中描述的图表。

1.3.2 使用 RStudio

R 的标准接口非常基础,仅提供用于输入代码行的命令提示符。对于实际项目,你可能需要一个更全面的工具来编写代码和查看输出。为 R 开发了几个这样的工具,称为集成开发环境(IDE),包括带有 StatET 的 Eclipse、R 的 Visual Studio 和 RStudio 桌面版。

RStudio 桌面版(www.rstudio.com)无疑是最受欢迎的选择。它提供了一个多窗口、多标签的环境,包括导入数据、编写整洁代码、调试错误、可视化输出和编写报告的工具。

RStudio 作为开源产品免费提供,并且可以轻松安装在 Windows、Mac 和 Linux 上。由于 RStudio 是 R 的接口,因此在安装 RStudio 桌面版之前,请确保已安装 R。

TIP 您可以通过从菜单栏中选择工具>全局选项...来自定义 RStudio 界面。在常规选项卡中,我建议取消选中启动时恢复 .RData 到工作区,并将保存工作区到 .RData 设置为永不。这将确保每次运行 RStudio 时都有一个干净的启动。

让我们使用 RStudio 重新运行列表 1.1 中的代码。如果您使用的是 Windows,请从开始菜单启动 RStudio。在 Mac 上,双击应用程序文件夹中的 RStudio 图标。在 Linux 上,在终端窗口的命令提示符中输入 rstudio。这三个平台都将显示相同的界面(见图 1.5)。

图像

图 1.5 RStudio 桌面

脚本窗口

从文件菜单中选择新建文件> R 脚本。一个新的脚本窗口将在屏幕的右上角打开(见图 1.5 A)。将列表 1.1 中的代码输入到这个窗口中。

当您键入时,编辑器提供语法高亮和代码补全(见图 1.6)。例如,当您键入 plot 时,将出现一个弹出窗口,显示所有以您已键入的字母开头的函数。您可以使用上箭头和下箭头键从列表中选择一个函数,并按 Tab 键选择它。在函数(括号内)中按 Tab 键查看函数选项。在引号内按 Tab 键完成文件路径。

图像

图 1.6 脚本窗口

要执行代码,请突出显示/选择它并点击运行按钮或按 Ctrl+Enter。按 Ctrl+Shift+Enter 将运行整个脚本。

要保存脚本,请按保存图标或从菜单栏中选择文件>保存。从打开的对话框中选择名称和位置。按照惯例,脚本文件以 .R 扩展名结尾。如果当前版本尚未保存,脚本文件名将以红色带星号的形式出现在窗口选项卡中。

控制台窗口

代码在控制台窗口中运行(见图 1.5 B)。这基本上与您使用基本 R 界面时看到的控制台相同。您可以使用运行命令从脚本窗口提交代码,或者直接在此窗口的命令提示符(>)中输入交互式命令。

如果命令提示符变为加号(+),则解释器正在等待一个完整的语句。如果语句太长而无法在一行中显示,或者代码中有不匹配的括号,这通常会发生。按 Esc 键可以返回到命令提示符。

此外,按上箭头和下箭头键将循环显示过去的命令。您可以使用 Enter 键编辑一个命令并重新提交它。点击扫帚图标将清除窗口中的文本。

环境和历史窗口

创建的任何对象(例如,本例中的 ageweight)将出现在环境窗口中(见图 1.5 C)。执行命令的记录将被保存在历史窗口中(环境选项卡右侧)。

绘图窗口

从脚本中创建的任何图表都将出现在图表窗口中(图 1.5 D)。此窗口的工具栏允许您在已创建的图表之间切换。此外,您还可以打开缩放窗口以查看不同大小的图表,以多种格式导出图表,以及删除您迄今为止创建的一个或所有图表。

1.3.3 获取帮助

R 提供了广泛的帮助功能,学会如何导航这些功能将大大有助于您的编程工作。内置的帮助系统提供了当前已安装包中任何函数的详细信息、参考和示例。您可以通过执行表 1.2 中列出的任何函数来获取帮助。

帮助信息也通过 RStudio 界面提供。在脚本窗口中,将光标放在函数名上并按 F1 键可打开帮助窗口。

表 1.2 R 帮助函数

函数 操作
help.start() 通用帮助
help("*foo*")?*foo* 关于函数 foo 的帮助
help(package = "*foo*") 帮助信息,关于名为 foo 的包
help.search("*foo*")??*foo* 在帮助系统中搜索字符串 foo 的实例
example("*foo*") 函数 foo 的示例(引号可选)
data() 列出当前已加载包中包含的所有可用示例数据集
vignette() 列出当前已安装包中所有可用的 vignette
vignette("*foo*") 显示特定于主题 foo 的 vignette

函数 help.start() 打开一个浏览器窗口,可以访问入门和高级手册、常见问题解答和参考材料。或者,从菜单中选择帮助 > R 帮助。vignette() 函数返回的 vignette 是以 PDF 或 HTML 格式提供的实用入门文章。并非所有包都有 vignette。

所有帮助文件都有类似的格式(见图 1.7)。帮助页面包含标题和简要描述,随后是函数的语法和选项。计算细节在“详细信息”部分提供。在“相关内容”部分描述并链接到相关函数。帮助页面几乎总是以说明函数典型用法的示例结束。

图 1.7 帮助窗口

如您所见,学会如何导航 R 的广泛帮助功能无疑将有助于您的编程工作。我很少不使用 ? 来查找某些函数的功能(如选项或返回值)。

1.3.4 工作空间

工作空间是当前 R 工作环境,包括任何用户定义的对象(向量、矩阵、函数、数据框和列表)。当前工作目录是 R 将从中读取文件并将结果默认保存到的目录。你可以使用getwd()函数来识别当前工作目录。你可以使用setwd()函数来设置当前工作目录。如果你需要输入不在当前工作目录中的文件,请在调用中使用完整的路径名。始终用引号括起来自操作系统的文件和目录名称。表 1.3 列出了一些管理工作空间的标准命令。

表 1.3 管理 R 工作空间的函数

函数 操作
getwd() 列出当前工作目录
setwd("*mydirectory*") 将当前工作目录更改为 mydirectory
ls() 列出当前工作空间中的对象
rm(*objectlist*) 删除(删除)一个或多个对象
help(*options*) 提供有关可用选项的信息
options() 允许你查看或设置当前选项
save.image("*myfile*") 将工作空间保存到 myfile(默认 = .RData)
save(*objectlist*, file="*myfile*") 将特定对象保存到文件
load("*myfile*") 将工作空间加载到当前会话

要查看这些命令的实际效果,请查看以下列表。

列表 1.2 管理 R 工作空间所使用的命令示例

setwd("C:/myprojects/project1")
options()                                 
options(digits=3)                         

首先,将当前工作目录设置为 C:/myprojects/project1. 然后,显示当前选项设置,数字格式化为小数点后三位打印。

注意setwd()命令路径名中的正斜杠。R 将反斜杠(\)视为转义字符。即使你在 Windows 平台上使用 R,也应在路径名中使用正斜杠。此外,请注意setwd()函数不会创建不存在的目录。如果需要,你可以使用dir.create()函数创建目录,然后使用setwd()更改到其位置。

1.3.5 项目

将你的项目保存在单独的目录中是个好主意。RStudio 为此提供了一个简单的机制。选择文件 > 新建项目 ... 并指定新建目录以在全新的工作目录中开始一个项目,或者选择现有目录以将项目与现有工作目录关联。你所有的程序文件、命令历史、报告输出、图表和数据都将保存在项目目录中。你可以通过使用 RStudio 应用右上角的“项目”下拉菜单轻松地在项目之间切换。

项目文件很容易让人感到不知所措。我建议在主项目文件夹内创建几个子文件夹。我通常创建一个 data 文件夹来存放原始数据文件,一个 img 文件夹用于图像文件和图形输出,一个 docs 文件夹用于项目文档,以及一个 reports 文件夹用于报告。我将 R 脚本和 README 文件放在主目录中。如果 R 脚本有顺序,我会给它们编号(例如,01_import_data.R、02_clean_data.R 等)。README 是一个包含作者、日期、利益相关者和他们的联系方式以及项目目的的文本文件。六个月之后,这将提醒我我做了什么以及为什么这么做。

1.4 软件包

R 默认就提供了广泛的功能,但其中一些最令人兴奋的功能作为可选模块提供,你可以下载并安装它们。有超过 10,000 个用户贡献的模块,称为 软件包,你可以从 cran.r-project.org/web/packages 下载。它们提供了极其广泛的新功能,从地理空间数据分析到蛋白质质量光谱处理,再到心理测试分析!你将在本书中使用许多这些可选软件包。

一组被称为 tidyverse 的软件包集合特别值得关注。这是一个相对较新的集合,它提供了一种简明、一致且直观的数据操作和分析方法。tidyverse 软件包(如 tidyrdplyrlubridatestringrggplot2)提供的优势正在改变数据科学家在 R 中编写代码的方式,我们将会经常使用这些软件包。实际上,描述如何使用这些软件包进行数据分析和可视化的机会是撰写本书第三版的主要动机。

1.4.1 什么是软件包?

软件包是一组以良好定义的格式组织的 R 函数、数据和编译代码。存储软件包的计算机目录称为 。函数 .libPaths() 会显示你的库在哪里,而函数 library() 会显示你已保存到库中的软件包。

R 默认提供了一套标准软件包(包括 basedatasetsutilsgr-Devicesgraphicsstatsmethods)。它们提供了广泛的功能和默认可用的数据集。其他软件包可供下载和安装。一旦安装,它们必须被加载到会话中才能使用。命令 search() 会告诉你哪些软件包已加载并准备好使用。

1.4.2 安装软件包

几个 R 函数允许你操作软件包。要首次安装软件包,使用 install.packages() 命令。例如,gclus 软件包包含用于创建增强散点图的函数。你可以使用命令 install.packages("gclus") 下载并安装该软件包。

你只需要安装一次包。但像任何软件一样,包通常会被其作者更新。使用update.packages()命令更新你已安装的任何包。要查看你的包的详细信息,你可以使用installed.packages()命令。它列出了你拥有的包,包括它们的版本号、依赖关系和其他信息。

你也可以使用 RStudio 界面安装和更新包。选择“包”选项卡(从右下角的窗口)。在上右角的选项卡窗口中的搜索框中输入名称(或部分名称)。在要安装的包旁边放置勾选标记,然后点击安装按钮。或者,点击更新按钮来更新已安装的包。

1.4.3 加载包

安装包会从 CRAN 镜像站点下载它并将其放置在你的库中。要在 R 会话中使用它,你需要使用library()命令来加载包。例如,要使用gclus包,输入命令library(gclus)

当然,在加载包之前,你必须已经安装了它。在给定会话中,你只需要加载一次包。如果你愿意,你可以自定义启动环境,以便自动加载你最常用的包。附录 B 解释了如何自定义启动。

1.4.4 了解包

当你加载一个包时,一组新的函数和数据集变得可用。提供了小型示例数据集和示例代码,让你可以尝试新功能。帮助系统包含每个函数的描述(包括示例)以及包含的每个数据集的信息。输入help(package="package_name")会提供包的简要描述以及包含的函数和数据集索引。使用help()加上任何这些函数或数据集的名称可以提供更多详细信息。相同的信息可以从 CRAN 下载为 PDF 手册。要使用 RStudio 界面获取包的帮助,请点击“包”选项卡(右下角的窗口),在搜索窗口中输入包的名称,然后点击包的名称。

R 编程中的常见错误

初学者和经验丰富的 R 程序员经常犯一些常见的错误。如果你的程序生成错误,请务必检查以下内容:

  • 使用错误的字母大小写——help(), Help(), 和 HELP() 是三个不同的函数(只有第一个会起作用)。

  • 忘记在需要时使用引号——install.packages("gclus") 是有效的,而 install.packages(gclus) 会生成错误。

  • 在函数调用中忘记包含括号——例如,help() 是有效的,但 help 则不行。即使没有选项,你仍然需要括号。

  • 在 Windows 的路径名中使用反斜杠(\)——R 将反斜杠字符视为转义字符。setwd("c:\mydata") 会生成错误。请使用 setwd("c:/mydata")setwd("c:\\mydata") 代替。

  • 使用未加载的包中的函数——函数 order .clusters() 包含在 gclus 包中。如果你在加载包之前尝试使用它,你会得到一个错误。

R 中的错误消息可能难以理解,但如果你注意以下几点,你应该可以避免看到很多错误。

1.5 将输出作为输入:重用结果

R 最有用的设计特性之一是分析输出可以轻松保存并用作其他分析的输入。让我们通过一个例子来了解一下,使用 R 预先安装的数据集之一。如果你不理解涉及的统计,不要担心。我们在这里关注的是一般原则。

R 随带了许多内置数据集,可用于练习数据分析。其中一个名为 mtcars 的数据集包含了从 Motor Trend 杂志道路测试收集的 32 辆汽车的信息。假设我们感兴趣的是描述汽车的燃油效率与重量之间的关系。

首先,我们可以运行一个简单的线性回归,预测每加仑英里数(mpg)与汽车重量(wt)之间的关系。这是通过以下函数调用实现的:

lm(mpg~wt, data=mtcars)

结果显示在屏幕上,并且没有保存任何信息。

或者,运行回归,但将结果存储在对象中:

lmfit <- lm(mpg~wt, data=mtcars) 

赋值创建了一个名为 lmfit 的列表对象,其中包含分析的大量信息(包括预测值、残差、回归系数等)。尽管没有输出到屏幕,但结果可以显示并进一步操作。

输入 summary(lmfit) 会显示结果摘要,而 plot(lmfit) 会生成诊断图。语句 cook<-cooks.distance(lmfit) 生成并存储影响统计量,而 plot(cook) 则将它们绘制成图。要从新数据集中的汽车重量预测每加仑英里数,你会使用 predict(lmfit, mynewdata)

要查看函数返回的内容,请查看该函数的 R 帮助页面的值部分。这里你会查看 help(lm)?lm。这会告诉你当你将函数的结果赋给对象时,保存了什么信息。

1.6 与大数据集一起工作

程序员经常问我 R 是否可以处理大数据问题。通常,他们处理来自网络研究、气候学或遗传学的海量数据。由于 R 在内存中存储对象,你通常受限于可用的 RAM 量。例如,在我的 9 年老 Windows PC 上,它有 2 GB 的 RAM,我可以轻松处理包含 1000 万个元素(100 个变量和 10 万个观测值)的数据集。在 4 GB RAM 的 iMac 上,我通常可以轻松处理 1 亿个元素。

但有两个问题需要考虑:数据集的大小和将要应用的统计方法。R 可以处理从千兆到太字节范围的数据分析问题,但需要专门的程序。附录 F 讨论了非常大数据集的管理和分析。

1.7 通过示例进行工作

我们将以一个示例结束本章,该示例将许多这些想法联系起来。这里是任务:

  1. 打开一般帮助并查看 R 简介部分。

  2. 安装 vcd 包(一个用于可视化分类数据的包,你将在第十一章中使用)。

  3. 列出此包中可用的函数和数据集。

  4. 加载包并阅读数据集 Arthritis 的描述。

  5. 打印出 Arthritis 数据集(输入对象名称将列出它)。

  6. 运行随 Arthritis 数据集提供的示例。如果你不理解结果,不用担心;它基本上显示接受治疗的关节炎患者改善程度远大于接受安慰剂的患者。

所需的代码在以下列表中提供,结果样本显示在图 1.8 中。正如这个简短练习所展示的,你可以用很少的代码完成很多事情。

列表 1.3 使用新包

help.start()
install.packages("vcd")
help(package="vcd")
library(vcd)
help(Arthritis)
Arthritis
example(Arthritis)

图 1.8 执行列表 1.3 中的代码时的 RStudio 窗口

在本章中,我们探讨了使 R 成为试图理解其数据含义的学生、研究人员、统计学家和数据分析师有吸引力的选择的一些优势。我们介绍了程序的安装,并讨论了如何通过下载额外的包来增强 R 的功能。我们探索了基本界面并制作了一些简单的图表。由于 R 可以是一个复杂的程序,我们花了些时间来看如何访问广泛可用的帮助。希望你能感受到这款免费软件的强大之处。

现在,你已经安装并运行了 R 和 RStudio,是时候将你的数据添加进来了。在下一章中,我们将探讨 R 可以处理的数据类型以及如何从文本文件、其他程序和数据库管理系统导入它们。

摘要

  • R 提供了一个全面、高度交互的环境,用于分析和可视化数据。

  • RStudio 是一个集成开发环境,它使 R 编程更容易、更高效。

  • 包是免费的可添加模块,可以极大地扩展 R 平台的功能。

  • R 有一个广泛的支持系统,学习如何使用它将极大地促进你有效编程的能力。

2 创建数据集

本章涵盖

  • 探索 R 数据结构

  • 使用数据输入

  • 导入数据

  • 数据集标注

任何数据分析的第一步是创建一个包含所需研究信息的数据集,并以满足您需求的形式组织。在 R 中,这项任务涉及

  • 选择用于存储数据的数据结构

  • 将数据输入或导入到数据结构中

本章的 2.1 和 2.2 节描述了 R 可以用来存储数据的丰富结构。特别是,2.2 节描述了向量、因子、矩阵、数据框、列表和 tibbles。熟悉这些结构(以及用于访问其中元素的符号)将极大地帮助您理解 R 的工作原理。您可能需要花时间仔细研究这一节。

第 2.3 节涵盖了将数据导入 R 的许多方法。数据可以手动输入或从外部来源导入。这些数据源可以包括文本文件、电子表格、统计软件包和数据库管理系统。例如,我处理的数据通常以逗号分隔的文本文件或 Excel 电子表格的形式出现。不过,有时我也会收到 SAS 和 SPSS 数据集或通过连接 SQL 数据库的数据。你很可能只需要使用本节中描述的方法中的一两种,因此请随意选择适合您情况的方法。

创建数据集后,您通常会对其进行标注,为变量和变量代码添加描述性标签。本章的 2.4 节探讨了数据集的标注,而 2.5 节回顾了一些用于处理数据集的有用函数。让我们从基础知识开始。

2.1 理解数据集

数据集通常是一个矩形数组,行代表观测值,列代表变量。表 2.1 提供了一个假设的患者数据集示例。

表 2.1 患者数据集

PatientID AdmDate Age Diabetes Status
1 10/15/2018 25 Type1 Poor
2 11/01/2018 34 Type2 Improved
3 10/21/2018 28 Type1 Excellent
4 10/28/2018 52 Type1 Poor

不同的传统对数据集的行和列有不同的称呼。统计学家称它们为观测值和变量,数据库分析师称它们为记录和字段,而数据挖掘和机器学习领域的专家称它们为示例和属性。我在本书中使用了 观测值变量 这两个术语。

您可以区分数据集的结构(在这种情况下,一个矩形数组)和包含的内容或数据类型。在表 2.1 所示的数据集中,PatientID 是行或案例标识符,AdmDate 是日期变量,Age 是连续(定量)变量,Diabetes 是名义变量,而 Status 是有序变量。名义变量和有序变量都是分类变量,但有序变量中的类别具有自然顺序。

R 拥有多种用于存储数据的结构,包括标量、向量、数组、数据框和列表。表 2.1 对应于 R 中的数据框。这种结构的多样性为 R 语言在处理数据方面提供了很大的灵活性。

R 可以处理的数据类型包括数值、字符、逻辑(TRUE/FALSE)、复数(虚数)和原始(字节)。在 R 中,PatientIDAdmDateAge是数值变量,而DiabetesStatus是字符变量。此外,您需要告诉 R PatientID是一个案例标识符,AdmDate包含日期,以及DiabetesStatus分别是名义变量和有序变量。R 将案例标识符称为rownames,将分类变量(名义、有序)称为factors。我们将在下一节中介绍这些内容。您将在第三章中了解日期。

2.2 数据结构

R 拥有多种用于存储数据的对象,包括标量、向量、矩阵、数组、数据框和列表。它们在可以存储的数据类型、创建方式、结构复杂性和用于标识和访问单个元素的符号方面有所不同。图 2.1 显示了这些数据结构的图解。让我们依次查看每个结构,从向量开始。

图片

图 2.1 R 数据结构

一些定义

一些术语是 R 特有的,因此对新用户来说可能会感到困惑。

在 R 中,任何可以分配给变量的东西都是对象。这包括常量、数据结构、函数,甚至是图表。对象有一个模式(描述对象如何存储)和一个(告诉通用函数如print如何处理它)。

R 中的数据框是一种结构,用于存储数据,类似于标准统计软件包(例如 SAS、SPSS 和 Stata)中找到的数据集。列是变量,行是观测值。你可以在同一个数据框中拥有不同类型的变量(例如,数值或字符)。数据框是您存储数据集的主要结构。

因子是名义或有序变量。在 R 中,它们被存储和处理得特别。你将在 2.2.5 节中了解因子。

R 中使用的其他大多数术语都应该对你来说很熟悉,并且遵循统计学和计算中的一般术语。

2.2.1 向量

向量是一维数组,可以存储数值数据、字符数据或逻辑数据。组合函数 c()用于形成向量。以下是每种类型向量的示例:

a <- c(1, 2, 5, 3, 6, -2, 4)
b <- c("one", "two", "three")
c <- c(TRUE, TRUE, TRUE, FALSE, TRUE, FALSE)

在这里,a是数值向量,b是字符向量,c是逻辑向量。请注意,向量中的数据必须只属于一种类型或模式(数值、字符或逻辑)。你无法在同一个向量中混合模式。

注意:标量是单元素向量。例如,f <- 3g <- "US"h <- TRUE。它们用于存储常量。

你可以使用方括号内的位置数值向量来引用向量的元素。与 C++、Java 和 Python 等编程语言不同,R 的位置索引从 1 开始而不是从 0 开始。例如,a[c(1, 3)] 指的是向量 a 的第一个和第三个元素。以下是一些额外的示例:

> a <- c("k", "j", "h", "a", "c", "m")
> a[3]
[1] "h"
> a[c(1, 3, 5)]
[1] "k" "h" "c"
> a[2:6]  
[1]  "j" "h" "a" "c" "m"

最后一条语句中使用的冒号运算符生成一个数字序列。例如,a <- c(2:6) 等同于 a <- c(2, 3, 4, 5, 6)

2.2.2 矩阵

矩阵是一个二维数组,其中每个元素具有相同的模式(数值、字符或逻辑)。矩阵使用 matrix 函数创建。一般格式为

*myymatrix* <- matrix(vector, *nrow=number_of_rows, ncol=number_of_columns,*
 *byrow=logical_value, dimnames=list(* 
 *char_vector_rownames, char_vector_colnames))*

其中 vector 包含矩阵的元素,nrowncol 指定行和列的维度,而 dimnames 包含可选的行和列标签,这些标签存储在字符向量中。选项 byrow 表示矩阵是按行填充(byrow=TRUE)还是按列填充(byrow=FALSE)。默认是按列填充。以下列表展示了 matrix 函数的用法。

列表 2.1 创建矩阵

> y <- matrix(1:20, nrow=5, ncol=4)                       ❶
> y
     [,1] [,2] [,3] [,4]
[1,]    1    6   11   16
[2,]    2    7   12   17
[3,]    3    8   13   18
[4,]    4    9   14   19
[5,]    5   10   15   20
> cells    <- c(1,26,24,68)    
> rnames   <- c("R1", "R2")
> cnames   <- c("C1", "C2")     
> mymatrix <- matrix(cells, nrow=2, ncol=2, byrow=TRUE,
                     dimnames=list(rnames, cnames))       ❷
> mymatrix
   C1 C2
R1  1 26
R2 24 68
> mymatrix <- matrix(cells, nrow=2, ncol=2, byrow=FALSE,    
                     dimnames=list(rnames, cnames))       ❸
> mymatrix   
  C1 C2
R1  1 24
R2 26 68

❶ 创建一个 5 × 4 矩阵

❷ 按行填充的 2 × 2 矩阵

❸ 按列填充的 2 × 2 矩阵

首先,你创建一个 5 × 4 矩阵 ❶。然后你创建一个带有标签的 2 × 2 矩阵并按行填充 ❷。最后,你创建一个 2 × 2 矩阵并按列填充 ❸。

你可以通过使用子脚本和方括号来识别矩阵的行、列或元素。X[i,] 指的是矩阵 X 的第 i 行,X[,j] 指的是第 j 列,而 X[i,j] 分别指的是第 ij 个元素。子脚本 ij 可以是数值向量,以选择多行或多列,如下面的列表所示。

列表 2.2 使用矩阵子脚本

> x <- matrix(1:10, nrow=2)
> x
     [,1] [,2] [,3] [,4] [,5]
[1,]    1    3    5    7    9
[2,]    2    4    6    8   10
> x[2,]                                         
  [1]  2  4  6  8 10
> x[,2]                                          
[1] 3 4
> x[1,4]                                         
[1] 7
> x[1, c(4,5)]                                   
[1] 7 9

首先,你创建一个包含数字 1 到 10 的 2 x 5 矩阵。默认情况下,矩阵是按列填充的。然后选择第二行的元素,接着选择第二列的元素。接下来,选择第一行和第四列的元素。最后,选择第一行和第四、第五列的元素。

矩阵是二维的,并且像向量一样,只能包含一种数据类型。当维度超过两个时,你使用数组(参见 2.2.3 节)。当存在多种数据模式时,你使用数据框(参见 2.2.4 节)。

2.2.3 数组

数组与矩阵类似,但可以具有超过两个维度。它们使用以下形式的 array 函数创建:

*myarray <- array(vector, dimensions, dimnames*)

其中 vector 包含数组的元素,dimensions 是一个数值向量,给出每个维度的最大索引,而 dimnames 是一个可选的维度标签列表。以下列表给出了创建一个三维(2 × 3 × 4)数字数组的示例。

列表 2.3 创建数组

> dim1 <- c("A1", "A2")
> dim2 <- c("B1", "B2", "B3")
> dim3 <- c("C1", "C2", "C3", "C4")
> z <- array(1:24, c(2, 3, 4), dimnames=list(dim1, dim2, dim3))
> z
, , C1
   B1 B2 B3
A1  1  3  5
A2  2  4  6

, , C2
   B1 B2 B3
A1  7  9 11
A2  8 10 12

, , C3
   B1 B2 B3
A1 13 15 17
A2 14 16 18

, , C4
   B1 B2 B3
A1 19 21 23
A2 20 22 24

如您所见,数组是矩阵的自然扩展。它们在创建执行统计计算的函数时可能很有用。像矩阵一样,它们必须是单一模式。识别元素遵循您之前看到的矩阵方法。在前面的例子中,z[1,2,3]元素是 15。

2.2.4 数据框

与矩阵相比,数据框更通用,因为不同的列可以包含不同模式的数据(数值、字符等)。它类似于您通常在 SAS、SPSS 和 Stata 中看到的数据集。数据框是您在 R 中处理的最常见的数据结构。

表 2.1 中的患者数据集包含数值和字符数据。由于存在多种数据模式,您不能将数据包含在矩阵中。在这种情况下,数据框是首选的结构。

数据框是通过data.frame()函数创建的:

mydata <- data.frame(*col1, col2, col3,...*)

其中col1col2col3等是任何类型的列向量(如字符、数值或逻辑)。可以使用names函数提供每列的名称。以下列表清楚地说明了这一点。

列表 2.4 创建数据框

> patientID <- c(1, 2, 3, 4)
> age <- c(25, 34, 28, 52)
> diabetes <- c("Type1", "Type2", "Type1", "Type1")
> status <- c("Poor", "Improved", "Excellent", "Poor")
> patientdata <- data.frame(patientID, age, diabetes, status)
> patientdata
  patientID age diabetes    status
1         1  25    Type1      Poor
2         2  34    Type2  Improved
3         3  28    Type1 Excellent
4         4  52    Type1      Poor

每列必须只包含一种模式(例如,数值、字符、逻辑),但您可以组合不同模式的列来形成数据框。因为数据框接近分析师通常认为的数据集,所以在讨论数据框时,我们将交替使用变量这两个术语。

有几种方法可以识别数据框的元素。您可以使用之前使用的下标符号(例如,使用矩阵),或者指定列名。使用之前创建的patientdata数据框,以下列表演示了这些方法。

列表 2.5 指定数据框的元素

> patientdata[1:2]
  patientID age
1         1  25
2         2  34
3         3  28
4         4  52
> patientdata[c("diabetes", "status")]
  diabetes    status
1    Type1      Poor
2    Type2  Improved
3    Type1 Excellent    
4    Type1      Poor
 > patientdata$age        ❶
[1] 25 34 28 52

❶ 表示患者数据框中的年龄变量

第三个例子中的$符号是新的。它用于指明给定数据框中的特定变量。例如,如果您想按状态交叉表糖尿病类型,可以使用以下代码:

> table(patientdata$diabetes, patientdata$status)

        Excellent Improved Poor
  Type1         1        0    2
  Type2         0        1    0 

每次在变量名开头输入patientdata$可能会感到繁琐,但有一些快捷方式可用。例如,with()函数可以简化您的代码。

使用 with

考虑内置的数据框mtcars,它包含 32 辆汽车的燃油效率数据。以下代码

  summary(mtcars$mpg)
  plot(mtcars$mpg, mtcars$disp)
  plot(mtcars$mpg, mtcars$wt)

提供了每加仑英里数(mpg)变量的摘要,以及 mpg 与发动机排量和汽车重量的图表。您可以将此代码简洁地写成

with(mtcars, {
  summary(mpg)
  plot(mpg, disp)
  plot(mpg, wt)
})

{}括号内的语句是相对于mtcars数据框进行评估的。如果只有一个语句(例如,summary(mpg)),则{}括号是可选的。

with()函数的限制在于赋值仅存在于函数括号内。考虑

> with(mtcars, {
   stats <- summary(mpg)
   stats
  })
   Min. 1st Qu. Median    Mean 3rd Qu.   Max. 
  10.40   15.43   19.20   20.09   22.80   33.90 
> stats
Error: object 'stats' not found

如果你需要创建在with()结构之外存在的对象,请使用特殊的赋值运算符(<<-)而不是标准的一个(<-)。它将对象保存到with()调用之外的全球环境中。这可以通过以下代码演示:

> with(mtcars, {
   nokeepstats <- summary(mpg)
   keepstats <<- summary(mpg)
})
> nokeepstats
Error: object 'nokeepstats' not found
> keepstats
   Min. 1st Qu. Median    Mean 3rd Qu.   Max. 
    10.40   15.43   19.20   20.09   22.80   33.90

案例标识符

在患者数据示例中,patientID用于识别数据集中的观测值。在 R 中,案例标识符可以通过data.frame()函数中的rowname选项进行指定。例如,以下语句

patientdata <- data.frame(patientID, age, diabetes, 
                          status, row.names=patientID)

指定patientID为在 R 生成的各种打印输出和图表中用于标记案例的变量。

2.2.5 因素

正如你所看到的,变量可以被描述为名义的、有序的或连续的。名义变量是分类的,没有隐含的顺序。DiabetesType1Type2)是一个名义变量的例子。即使Type1在数据中编码为 1,Type2编码为 2,也没有隐含的顺序。有序变量隐含着顺序,但没有数量。Statuspoorimprovedexcellent)是有序变量的一个好例子。你知道一个状况较差的患者不如状况改善的患者,但不知道差多少。连续变量可以取某个范围内的任何值,并且隐含着顺序和数量。Age(年)是一个连续变量,可以取如14.522.8以及介于两者之间的任何值。你知道 15 岁的人比 14 岁的人年长 1 岁。

在 R 中,分类(名义)和有序分类(有序)变量被称为因子。因子在 R 中至关重要,因为它们决定了数据如何被分析和以视觉方式呈现。你将在本书的各个部分看到这方面的例子。

factor()函数将分类值存储为范围在[1... k](其中k是名义变量中唯一值的数量)的整数向量,以及一个映射到这些整数的字符字符串内部向量(原始值)。

例如,假设你有以下向量:

diabetes <- c("Type1", "Type2", "Type1", "Type1")

语句diabetes <- factor(diabetes)将此向量存储为(1, 2, 1, 1),并将其与内部关联的1=Type12=Type2(赋值是按字母顺序的)相关联。对diabetes向量进行的任何分析都将将该变量视为名义变量,并选择适合此测量水平的统计方法。

对于表示有序变量的向量,你需要在factor()函数中添加参数ordered=TRUE。给定以下向量

status <- c("Poor", "Improved", "Excellent", "Poor")

语句status <- factor(status, ordered=TRUE)将向量编码为(3, 2, 1, 3),并将其内部关联的值设置为1=Excellent2=Improved3=Poor。此外,对这一向量进行的任何分析都将将该变量视为有序变量,并选择适当的统计方法。

默认情况下,字符向量的因子等级按字母顺序创建。这对于 status 因子来说有效,因为“Excellent”、“Improved”、“Poor”的顺序是有意义的。如果“Poor”被编码为“Ailing”,则会出现问题,因为顺序将是“Ailing”、“Excellent”、“Improved”。如果期望的顺序是“Poor”、“Improved”、“Excellent”,则也会存在类似的问题。对于有序因子,字母顺序的默认值很少足够。

你可以通过指定等级选项来覆盖默认设置。例如,

status <- factor(status, order=TRUE, 
                 levels=c("Poor", "Improved", "Excellent"))

将等级分配为 1=Poor2=Improved3=Excellent。确保指定的等级与您的实际数据值相匹配。任何不在列表中的数据值将被设置为 missing

数值变量可以使用 levelslabels 选项编码为因子。如果原始数据中将性别编码为 1 表示男性,2 表示女性,那么

sex <- factor(sex, levels=c(1, 2), labels=c("Male", "Female"))

将变量转换为无序因子。注意标签的顺序必须与等级的顺序相匹配。在这个例子中,性别将被视为分类变量,输出中将出现 "Male""Female" 标签而不是 12,任何最初未编码为 12 的性别值将被设置为 missing

以下列表演示了指定因子和有序因子如何影响数据分析。

列表 2.6 使用因子

> patientID <- c(1, 2, 3, 4)                                      ❶
> age <- c(25, 34, 28, 52)
> diabetes <- c("Type1", "Type2", "Type1", "Type1")
> status <- c("Poor", "Improved", "Excellent", "Poor")
> diabetes <- factor(diabetes)                                          
> status <- factor(status, order=TRUE)
> patientdata <- data.frame(patientID, age, diabetes, status)
> str(patientdata)                                                ❷
‘data.frame’:   4 obs. of  4 variables:                                      
 $ patientID: num  1 2 3 4                               
 $ age      : num  25 34 28 52
 $ diabetes : Factor w/ 2 levels "Type1","Type2": 1 2 1 1
 $ status   : Ord.factor w/ 3 levels "Excellent"<"Improved"<..: 3 2 1 3
> summary(patientdata)                                            ❸
   patientID         age         diabetes       status 
 Min.  :1.00   Min.  :25.00   Type1:3   Excellent:1     
 1st Qu.:1.75   1st Qu.:27.25   Type2:1   Improved :1  
 Median :2.50   Median :31.00             Poor     :2  
 Mean   :2.50   Mean   :34.75                          
 3rd Qu.:3.25   3rd Qu.:38.50                          
 Max.  :4.00   Max.  :52.00                          

❶ 将数据作为向量输入

❷ 显示对象结构

❸ 显示对象摘要

首先,你将数据作为向量输入 ❶。然后指定 diabetes 是一个因子,status 是一个有序因子。最后,将数据组合到一个数据框中。函数 str(object) 提供了有关 R 中对象(在这种情况下是数据框)的信息。输出表明 diabetes 是一个因子,status 是一个有序因子,以及它们是如何内部编码的。请注意,summary() 函数对变量有不同的处理方式 ❸。它为连续变量 age 提供最小值、最大值、平均值和四分位数,为分类变量 diabetesstatus 提供频率计数。

2.2.6 列表

列表是 R 数据类型中最复杂的。基本上,列表是有序对象(组件)的集合。列表允许你将各种(可能无关的)对象汇集在一个名称下。例如,列表可以包含向量、矩阵、数据框甚至其他列表的组合。你可以使用 list() 函数创建列表:

mylist <- list(*object1*, *object2*, ...)

其中对象是迄今为止看到的任何结构。可选地,你可以在列表中命名对象:

mylist <- list(*name1=object1*, *name2=object2*, ...)

以下列表显示了一个示例。

列表 2.7 创建列表

> g <- "My First List"
> h <- c(25, 26, 18, 39)
> j <- matrix(1:10, nrow=5)
> k <- c("one", "two", "three")
> mylist <- list(title=g, ages=h, j, k)       ❶
> mylist                                      ❷
$title
[1] "My First List"

$ages
[1] 25 26 18 39

[[3]]
     [,1] [,2]
[1,]    1    6
[2,]    2    7
[3,]    3    8
[4,]    4    9
[5,]    5   10

[[4]]
[1] "one"   "two"   "three"

> mylist[[2]]                                 ❸
[1] 25 26 18 39
> mylist[["ages"]]
[[1] 25 26 18 39

❶ 创建列表

❷ 打印整个列表

❸ 打印第二个组件

在这个例子中,你创建了一个包含四个组件的列表:一个字符串、一个数值向量、一个矩阵和一个字符向量。你可以组合任意数量的对象并将它们保存为列表。

您也可以通过指定组件编号或双括号内的名称来指定列表的元素。在这个例子中,mylist[[2]]mylist [["ages"]] 都指向同一个包含四个元素的数值向量。对于命名组件,mylist$ages 也会起作用。列表是重要的 R 结构,原因有两个。首先,它们允许您以简单的方式组织和回忆不同的信息。其次,许多 R 函数的结果返回列表。分析员需要提取所需的组件。您将在后面的章节中看到许多返回列表的函数的例子。

2.2.7 Tibbles

在继续之前,值得提一下 tibbles,它们是具有专门行为的数据框,旨在使它们更有用。它们可以通过 tibble()as_tibble() 函数从 tibble 包中创建。要安装 tibble 包,请使用 install.packages("tibble")。以下是一些它们吸引人的特性描述。

Tibbles 打印的格式比标准数据框更紧凑。此外,变量标签描述了每列的数据类型:

library(tibble)
mtcars <- as_tibble(mtcars)
mtcars

# A tibble: 32 x 11
     mpg   cyl  disp    hp  drat    wt  qsec    vs    am  gear  carb
 * <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
 1  21       6  160    110  3.9   2.62  16.5     0     1     4     4
 2  21       6  160    110  3.9   2.88  17.0     0     1     4     4
 3  22.8     4  108     93  3.85  2.32  18.6     1     1     4     1
 4  21.4     6  258    110  3.08  3.22  19.4     1     0     3     1
 5  18.7     8  360    175  3.15  3.44  17.0     0     0     3     2
 6  18.1     6  225    105  2.76  3.46  20.2     1     0     3     1
 7  14.3     8  360   245  3.21  3.57  15.8     0     0     3     4
 8  24.4     4  147\.   62  3.69  3.19  20       1     0     4     2
 9  22.8     4  141\.   95  3.92  3.15  22.9     1     0     4     2
10  19.2     6  168\.  123  3.92  3.44  18.3     1     0     4     4
# ... with 22 more rows

Tibbles 从不将字符变量转换为因子。在 R 的旧版本(R 4.0 之前),函数如 read.table()data.frame()as.data.frame() 默认将字符数据转换为因子。您必须向这些函数添加 stringsAsFactors = FALSE 选项来抑制此行为。

Tibbles 从不更改变量的名称。如果正在导入的数据集有一个名为 Last Address 的变量,基础 R 函数会将名称转换为 Last.Address,因为 R 变量名称不使用空格。Tibbles 会保持名称不变,并使用反引号(例如,`Last Address`)使变量名称在语法上正确。

对 tibble 进行子集操作总是返回一个 tibble。例如,使用 mtcars[,"mpg"]mtcars 数据框进行子集操作会返回一个向量,而不是一个单列数据框。R 会自动简化结果。要获取一个单列数据框,您必须包含 drop = FALSE 选项(mtcars[, "mpg", drop = FALSE])。相比之下,如果 mtcars 是一个 tibble,那么 mtcars[, "mpg"] 会返回一个单列的 tibble。结果不会被简化,这使得您可以轻松预测子集操作的结果。

最后,tibbles 不支持行名。可以使用 rownames_to_column() 函数将数据框中的行名转换为 tibble 中的变量。

Tibbles 很重要,因为许多流行的包,如 readrtidyrdplyrpurr,将数据框保存为 tibbles。虽然 tibbles 被设计成“数据框的现代版本”,但请注意,它们可以与数据框互换使用。任何需要数据框的函数都可以接受 tibble,反之亦然。要了解更多信息,请参阅 r4ds.had.co.nz/tibbles.html

程序员注意事项

经验丰富的程序员通常会发现 R 语言的一些方面很独特。以下是您应该注意的语言特性:

  • 逗号 (.) 在对象名称中没有特殊意义。美元符号 (\() 在其他面向对象的语言中与逗号有类似的意义,可以用来标识数据框或列表的部分。例如,`A\)x指的是数据框A中的变量x`。

  • R 不提供多行或块注释。您必须以 # 开头每一行多行注释。为了调试目的,您还可以用语句 if(FALSE){...} 将您希望解释器忽略的代码包围起来。将 FALSE 改为 TRUE 允许代码执行。

  • 将值分配给向量、矩阵、数组或列表中不存在的元素会扩展该结构以容纳新值。例如,考虑以下情况:

    > x <- c(8, 6, 4) 
    > x[7] <- 10
    > x
    [1]  8  6  4 NA NA NA 10
    

向量 x 通过赋值从 3 个元素扩展到 7 个元素。x <- x[1:3] 会将其缩小回 7 个元素。

  • R 没有标量值。标量值表示为一元素向量。

  • R 中的索引从 1 开始,而不是从 0 开始。在之前的向量中,x[1] 的值是 8。

  • 变量不能声明。它们在第一次赋值时才会存在。

要了解更多信息,请参阅 John Cook 的优秀博客文章,“R Language for Programmers” (mng.bz/6NwQ)。寻找风格指导的程序员也可能想查看 Hadley Wickham 的 The Tidyverse Style Guide (style.tidyverse.org/)。

2.3 数据输入

现在您有了数据结构,您需要将一些数据放入其中。作为一名数据分析师,您通常面临来自各种来源和格式的数据。您的任务是导入数据到您的工具中,分析数据,并报告结果。R 提供了广泛的数据导入工具。R 中导入数据的 definitive guide 是可在 mng.bz/urwn 获取的 R Data Import/Export 手册。

如图 2.2 所示,R 可以从键盘、文本文件、Microsoft Excel 和 Access、流行的统计软件包、各种关系型数据库管理系统、专用数据库、以及网站和在线服务导入数据。由于您永远不知道数据会从哪里来,我们将在这里介绍它们。您只需阅读您将要使用的内容。

图 2.2 可以导入 R 的数据源

2.3.1 从键盘输入数据

输入数据最简单的方法可能是从键盘输入。有两种常见方法:通过 R 的内置文本编辑器输入数据和直接将数据嵌入到您的代码中。我们首先考虑编辑器。

R 中的 edit() 函数调用一个文本编辑器,允许您手动输入数据。以下是步骤:

  1. 创建一个空的数据框(或矩阵),其中包含您希望在最终数据集中拥有的变量名称和模式。

  2. 在此数据对象上调用文本编辑器,输入您的数据,并将结果保存到数据对象中。

以下示例创建了一个名为mydata的数据框,包含三个变量:age(数值)、gender(字符)和weight(数值)。然后您调用文本编辑器,添加您的数据,并保存结果:

mydata <- data.frame(age=numeric(0), gender=character(0),  
                     weight=numeric(0))
mydata <- edit(mydata)

类似于age=numeric(0)的赋值创建了一个特定模式但无实际数据的变量。请注意,编辑的结果会赋值回对象本身。edit()函数在对象的副本上操作。如果您不指定目的地,您所做的所有编辑都将丢失。

图 2.3 展示了在 Windows 平台上调用edit()函数的结果。在此图中,我添加了一些数据。如果您点击列标题,编辑器会为您提供更改变量名称和类型的选项(数值或字符)。您可以通过点击未使用列的标题来添加变量。当文本编辑器关闭时,结果将保存到指定的对象中(在本例中为mydata)。再次调用mydata <- edit(mydata)允许您编辑已输入的数据并添加新数据。mydata <- edit(mydata)的快捷键是fix(mydata)

图 2.3 在 Windows 平台上通过内置编辑器输入数据

或者,您可以直接在程序中嵌入数据。例如,以下代码创建了一个与使用edit()函数创建的数据框相同的数据框:

mydatatxt <- "
age gender weight
25 m 166
30 f 115
18 f 120
"
mydata <- read.table(header=TRUE, text=mydatatxt)

创建了一个包含原始数据的字符串,并使用read.table()函数处理该字符串,返回一个数据框。read.table()函数将在下一节中详细介绍。

当您处理小型数据集时,键盘数据输入可能很方便。对于大型数据集,您将想要使用下一节中描述的方法从现有的文本文件、Excel 电子表格、统计软件包或数据库管理系统导入数据。

2.3.2 从分隔符文本文件导入数据

您可以使用read.table()函数从分隔符文本文件导入数据,该函数读取表格格式的文件并将其保存为数据框。表格的每一行在文件中表现为一行。语法是

*mydataframe* <- read.table(*file*, *options*)

其中file是一个分隔符 ASCII 文件,而options是控制数据处理方式的参数。表 2.2 列出了最常见的选项。

表 2.2 read.table()选项

选项 描述
header 一个逻辑值,指示文件是否在第一行包含变量名称。
sep 分隔数据值的分隔符。默认为sep="",表示一个或多个空格、制表符、换行符或回车符。使用sep=","读取逗号分隔的文件,使用sep="\t"读取制表符分隔的文件。
row.names 一个可选参数,指定一个或多个变量作为行标识符。
col.names 如果数据文件的第一行不包含变量名(header=FALSE),可以使用 col.names 指定包含变量名的字符向量。如果 header=FALSE 且省略了 col.names 选项,变量将被命名为 V1V2 等等。
na.strings 一个可选的字符向量,指示缺失值代码。例如,na.strings=c("-9", "?") 将读取数据时每个 -9? 值转换为 NA
colClasses 一个可选的向量,用于指定列的类别。例如,colClasses=c("numeric", "numeric", "character", "NULL", "numeric") 将前两列读取为数值型,将第三列读取为字符型,跳过第四列,并将第五列读取为数值型。如果数据中有超过五列,colClasses 中的值将循环使用。当你读取大型文本文件时,包括 colClasses 选项可以显著加快处理速度。
quote 用于分隔包含特殊字符的字符串的字符(或字符序列)。默认情况下,这是双引号(")或单引号(')。
skip 在开始读取数据之前要跳过的数据文件中的行数。此选项对于跳过文件中的标题注释很有用。
stringsAsFactors 一个逻辑值,指示字符变量是否应转换为因子。在 R 4.0 之前,默认值为 TRUE。对于更近期的版本,默认值现在是 FALSE,除非被 colClasses 覆盖。当你处理大型文本文件时,设置 stringsAsFactors=FALSE 可以加快处理速度。
text 指定要处理的文本字符串的字符字符串。如果指定了 text,则留空 file。第 2.3.1 节提供了一个示例。

考虑一个名为 studentgrades.csv 的文本文件,其中包含学生的数学、科学和社会研究成绩。文件的每一行代表一个学生。第一行包含变量名,用逗号分隔。随后的每一行包含一个学生的信息,也用逗号分隔。文件的前几行如下所示:

StudentID,First,Last,Math,Science,Social Studies
011,Bob,Smith,90,80,67
012,Jane,Weary,75,,80
010,Dan,"Thornton, III",65,75,70
040,Mary,"O'Leary",90,95,92

可以使用以下代码将文件导入数据框:

grades <- read.table("studentgrades.csv", header=TRUE, 
    row.names="StudentID", sep=",") 

结果是

> grades

   First             Last Math Science Social.Studies
11   Bob            Smith   90      80             67
12  Jane            Weary   75      NA             80
10   Dan    Thornton, III   65      75             70
40  Mary          O'Leary   90      95             92

> str(grades)

'data.frame':   4 obs. of  5 variables:
 $ First         : chr  "Bob" "Jane" "Dan" "Mary"
 $ Last          : chr  "Smith" "Weary" "Thornton, III" "O'Leary"
 $ Math          : int  90 75 65 90
 $ Science       : int  80 NA 75 95
 $ Social.Studies: int  67 80 70 92

关于数据导入的几个有趣之处需要注意。变量名 Social Studies 会自动重命名为遵循 R 规范的名称。StudentID 列现在是行名,不再有标签,并且丢失了前导零。Jane 缺失的科学成绩被正确地读取为缺失值。我不得不在 Dan 的姓氏 ThorntonIII 之间的逗号周围加上引号以避免 R 将其视为七个值而不是六个。我还必须在 O'Leary 周围加上引号。否则,R 会将单引号读取为字符串分隔符,这不是我想要的。

stringsAsFactors 选项

read.table()data.frame()as.data.frame() 函数中,stringsAsFactors 选项控制字符变量是否自动转换为因子。在 R 版本 4.0.0 之前,默认值为 TRUE。从 R 4.0.0 开始,默认值为 FALSE。如果您使用的是 R 的较旧版本,则前一个示例中的 FirstLast 变量现在是因子而不是字符变量。

将字符变量转换为因子可能并不总是希望的。例如,将包含受访者评论的字符变量转换为因子几乎没有理由。此外,您可能想要操作或挖掘变量中的文本,一旦转换为因子,这就会变得很困难。

您可以通过多种方式来抑制这种行为。包括 stringsAsFactors=FALSE 选项将关闭所有字符变量的此行为。或者,您可以使用 colClasses 选项为每一列指定一个类(例如,逻辑、数值、字符或因子)。

让我们指定每个变量的类来导入相同的数据:

grades <- read.table("studentgrades.csv", header=TRUE,
             row.names="StudentID", sep=",",
             colClasses=c("character", "character", "character",  
                           "numeric", "numeric", "numeric"))
> grades

    First             Last Math Science Social.Studies
011   Bob            Smith   90      80             67
012  Jane            Weary   75      NA             80
010   Dan    Thornton, III   65      75             70
040  Mary          O'Leary   90      95             92

> str(grades)

'data.frame':   4 obs. of  5 variables:
 $ First         : chr  "Bob" "Jane" "Dan" "Mary"
 $ Last          : chr  "Smith" "Weary" "Thornton, III" "O'Leary"
 $ Math          : num  90 75 65 90
 $ Science       : num  80 NA 75 95
 $ Social.Studies: num  67 80 70 92

现在行名保留了前导零,并且 FirstLast 不是因子(即使在 R 的早期版本中)。此外,成绩以实数值而不是整数值存储。

read.table() 函数提供了许多选项来微调数据导入。有关详细信息,请参阅 help(read.table)

通过连接导入数据

本章中的许多示例都是从您计算机上存在的文件导入数据。R 还提供了几种机制来通过连接访问数据。例如,可以使用 file()gzfile()bzfile()xzfile()unz()url() 函数来代替文件名。file() 函数允许您访问文件、剪贴板和 C 级标准输入。gzfile()bzfile()xzfile()unz() 函数允许您读取压缩文件。

url() 函数允许您通过包含 http://, ftp:// 或 file:// 的完整 URL 访问互联网文件。对于 HTTP 和 FTP,可以指定代理。为了方便,通常可以直接使用带双引号的完整 URL 来代替文件名。有关详细信息,请参阅 help(file)

基础 R 还提供了 read.csv()read.delim() 函数来导入矩形文本文件。这些函数只是调用 read.table() 并使用特定默认值的包装函数。例如,read.csv() 使用 header =TRUEsep="," 调用 read.table(),而 read.delim() 使用 header=TRUEsep="\t" 调用 read.table()。详细信息请参阅 read.table() 帮助。

readr 包为读取矩形文本文件提供了对基础 R 函数的强大替代。主要函数是 read_delim(),辅助函数 read_csv()read_tsv() 分别用于读取逗号分隔和制表符分隔的文件。安装该包后,可以使用以下代码读取之前的数据:

library(readr)
grades <- read_csv("studentgrades.csv")

该包还提供了导入固定宽度文件(数据出现在特定列中)、表格文件(列由空白字符分隔)和网页日志文件的功能。

readr 包中的函数在速度上比基础 R 的函数有显著优势。当读取大型数据文件时,这可以是一个巨大的优势。此外,它们非常擅长猜测每列的正确数据类型(数值、字符、日期和日期时间)。最后,与 R 4.0.0 之前的 base R 函数不同,它们不会默认将字符数据转换为因子。readr 包中的函数将数据作为 tibbles(具有一些特殊功能的数据框)返回。要了解更多信息,请参阅 readr.tidyverse.org

2.3.3 从 Excel 导入数据

读取 Excel 文件的最佳方式是将它从 Excel 导出为逗号分隔的文件,然后使用前面描述的方法将其导入 R。或者,您可以直接使用 readxl 包导入 Excel 工作表。在使用之前,请确保已下载并安装它。

readxl 包可以用来读取 Excel 文件的 .xls 和 .xlsx 版本。read_excel() 函数将工作表导入数据框作为 tibble。最简单的格式是 read_excel(file, n),其中 file 是 Excel 工作簿的路径,n 是要导入的工作表编号,工作表的第一行包含变量名。例如,在 Windows 平台上,代码

library(readxl)
workbook <- "c:/myworkbook.xlsx"
mydataframe <- read_xlsx(workbook, 1)

从存储在 C: 驱动的 myworkbook.xlsx 工作簿中导入第一个工作表,并将其保存为数据框 mydataframe

read_excel() 函数提供了选项,允许您指定特定的单元格范围(例如,range = "Mysheet!B2:G14"),以及每列的类别(col_types)。有关详细信息,请参阅 help(read_excel)

其他可以帮助您处理 Excel 文件的包包括 xlsxXLConnectopenxlsxxlsxXLConnect 包依赖于 Java,而 openxlsx 不依赖。与 readxl 不同,这些包不仅可以导入工作表,还可以创建和操作 Excel 文件。需要开发 R 与 Excel 之间接口的程序员应该检查这些包中的一个或多个。

2.3.4 从 JSON 导入数据

越来越多的数据以 JSON(JavaScript 对象表示法)格式提供。R 有几个用于处理 JSON 的包。例如,jsonlite 包允许您读取、写入和操作 JSON 对象。数据可以直接从 JSON 文件导入到 R 数据框中。关于 JSON 的内容超出了本文的范围;如果您感兴趣,请参阅 jsonlite 的示例文档 (cran.r-project.org/web/packages/jsonlite/)).

2.3.5 从网络导入数据

数据可以通过网络爬虫或使用应用程序编程接口 (APIs) 获取。网络爬虫用于提取特定网页中嵌入的信息,而 APIs 允许您与网络服务和在线数据存储进行交互。

通常,网络爬虫用于从网页中提取数据并将其保存到 R 结构中以便进一步分析。例如,网页上的文本可以使用 readLines() 函数下载到 R 字符向量,并使用 grep()gsub() 等函数进行操作。rvest 包提供了可以简化从网页中提取数据的函数。它受到了 Python 库 Beautiful Soup 的启发。RCurlXML 包也可以用来提取信息。有关更多信息,包括示例,请参阅网站 ProgrammingR 上的“使用 R 进行网络爬虫的示例”(www.programmingr.com)。

APIs 指定了软件组件之间应该如何交互。几个 R 包使用这种方法从可访问的 Web 资源中提取数据。这些数据源包括生物学、医学、地球科学、物理科学、经济学和商业、金融、文学、营销、新闻和体育。

例如,如果您对社交媒体感兴趣,可以通过 twitteR 访问 Twitter 数据,通过 Rfacebook 访问 Facebook 数据,通过 Rflickr 访问 Flickr 数据。其他包允许您访问由 Google、Amazon、Dropbox、Salesforce 等提供的流行网络服务。有关可以帮助您访问基于 Web 资源的 R 包的完整列表,请参阅 CRAN 任务视图“Web 技术和服务”(mng.bz/370r)。

2.3.6 从 SPSS 导入数据

可以通过 haven 包中的 read_spss() 函数将 IBM SPSS 数据集导入 R。首先,下载并安装该包:

install.packages("haven")

然后使用以下代码导入数据:

library(haven)
mydataframe <- read_spss("mydata.sav")

导入的数据集是一个数据框(作为 tibble),包含导入的 SPSS 值标签的变量被分配了 labelled 类。您可以使用以下代码将这些标记变量转换为 R 因子:

labelled_vars <- names(mydataframe)[sapply(mydataframe, is.labelled)]
for (vars in labelled_vars){
  mydataframe[[vars]] = as_factor(mydataframe[[vars]])
}

haven 包提供了读取压缩的 (.zsav) 或传输格式 (.por) 的 SPSS 文件的附加功能。

2.3.7 从 SAS 导入数据

可以使用 haven 包中的 read_sas() 函数导入 SAS 数据集。安装包后,使用以下命令导入数据:

library(haven)
mydataframe <- read_sas("mydata.sas7bdat")

如果用户还有一个变量格式的目录,它们也可以导入并应用于数据:

mydataframe <- read_sas("mydata.sas7bdat", 
                             catalog_file = "mydata.sas7bcat") 

在任何情况下,结果都是一个保存为 tibble 的数据框。

或者,有一个名为 Stat/Transfer 的商业产品(在第 2.3.10 节中描述),它能够出色地将 SAS 数据集(包括任何现有的变量格式)保存为 R 数据框。

2.3.8 从 Stata 导入数据

从 Stata 导入 R 数据非常直接。再次使用 haven 包:

library(haven)
mydataframe <- read_dta("mydata.dta")

在这里,mydata.dta是 Stata 数据集,而mydataframe是结果 R 数据框,保存为 tibble 格式。

2.3.9 访问数据库管理系统

R 可以与各种关系型数据库管理系统(DBMSs)接口,包括 Microsoft SQL Server、Microsoft Access、MySQL、Oracle、PostgreSQL、DB2、Sybase、Teradata 和 SQLite。一些包通过本地数据库驱动程序提供访问,而其他包则通过 ODBC 或 JDBC 提供访问。使用 R 访问存储在外部 DBMS 中的数据可以是一种分析大型数据集的有效方法(参见附录 F),并且可以利用 SQL 和 R 的双重力量。

ODBC 接口

在 R 中访问 DBMS 最流行的方法可能是通过RODBC包,该包允许 R 连接到任何具有 ODBC 驱动程序的 DBMS。这包括前面列出的所有 DBMS。

第一步是安装并配置适合您平台和数据库的适当 ODBC 驱动程序(这些驱动程序不是 R 的一部分)。如果所需的驱动程序尚未安装到您的机器上,网络搜索应为您提供选项(可以从db.rstudio.com/best-practices/drivers/开始)。

一旦为所选数据库安装并配置了相应的驱动程序,请安装RODBC包。您可以使用install.packages("RODBC")命令进行安装。表 2.3 列出了RODBC包含的主要函数。

表 2.3 RODBC函数

函数 描述
odbcConnect(*dsn*,uid="",pwd="") 打开与 ODBC 数据库的连接
sqlFetch(*channel,sqltable*) 从 ODBC 数据库读取表到数据框
sqlQuery(*channel,query*) 向 ODBC 数据库提交查询并返回结果
sqlSave(*channel,mydf,*tablename = *sqltable*,append=FALSE) 将数据框写入或更新到 ODBC 数据库中的表(如果append=TRUE
sqlDrop(*channel,sqltable*) 从 ODBC 数据库中删除表
close(*channel*) 关闭连接

RODBC包允许 R 与 ODBC 连接的 SQL 数据库之间进行双向通信。这意味着您不仅可以从连接的数据库中读取数据到 R,还可以使用 R 更改数据库本身的内容。假设您想从 DBMS 中导入两个表(Crime 和 Punishment)到两个名为crimedatpundat的 R 数据框中。您可以使用类似以下代码来完成此操作:

library(RODBC)                            
myconn <-odbcConnect("mydsn", uid="Rob", pwd="aardvark")        
crimedat <- sqlFetch(myconn, Crime)                
pundat <- sqlQuery(myconn, "select * from Punishment")        
close(myconn) 

在这里,您加载RODBC包并通过注册的数据源名称(mydsn)、安全 UID(rob)和密码(aardvark)打开与 ODBC 数据库的连接。连接字符串传递给sqlFetch,它将表 Crime 复制到 R 数据框crimedat中。然后,您对表 Punishment 运行 SQL select语句并将结果保存到数据框pundat中。最后,您关闭连接。

sqlQuery() 函数功能强大,因为可以插入任何有效的 SQL 语句。这种灵活性允许你选择特定的变量,对数据进行子集化,创建新变量,以及重新编码和重命名现有变量。

与 DBI 相关的包

DBI包提供了一个通用的、一致的客户端接口到 DBMS。在这个框架的基础上,RJDBC包通过 JDBC 驱动程序提供对 DBMS 的访问。请确保为你的平台和数据库安装必要的 JDBC 驱动程序。其他有用的基于 DBI 的包包括RMySQLROracleRPostgreSQLRSQLite。这些包为它们各自的数据库提供原生数据库驱动程序,但可能并非所有平台都可用。有关详细信息,请参阅 CRAN 上的文档(cran.r-project.org)。

2.3.10 通过 Stat/Transfer 导入数据

在我们结束对导入数据的讨论之前,值得提一下一个可以显著简化任务的商业产品。Stat/Transfer (www.stattransfer.com) 是一个独立的应用程序,可以在 34 种数据格式之间传输数据,包括 R(见图 2.4)。

图 2.4 Stat/Transfer 在 Windows 中的主对话框

Stat/Transfer 适用于 Windows、Mac 和 Unix 平台。它支持我们之前讨论过的统计软件的最新版本,以及通过 ODBC 访问的 DBMS,如 Oracle、Sybase、Informix 和 DB/2。

2.4 数据集标注

数据分析师通常会对数据集进行标注,以便更容易解释结果。标注通常包括向变量名添加描述性标签,以及对用于分类变量的代码添加值标签。例如,对于变量age,你可能希望附加更描述性的标签“Age at hospitalization (in years)。”对于变量gender,编码为 1 或 2,你可能希望关联标签“male”和“female。”

2.4.1 变量标签

不幸的是,R 处理变量标签的能力有限。一种方法是将变量标签用作变量的名称,然后通过其位置索引来引用变量。考虑之前的例子,其中有一个包含患者数据的 data frame。第二列,age,包含个人首次住院时的年龄。以下代码

names(patientdata)[2] <- "Age at hospitalization (in years)"

age重命名为"Age at hospitalization (in years)"。显然,这个新名字太长了,重复输入会很麻烦。相反,你可以将这个变量称为patientdata[2],而字符串"Age at hospitalization (in years)"将打印在原本显示年龄的地方。显然,这不是一个理想的方法,你可能更愿意尝试想出更好的变量名(例如,admissionAge)。

2.4.2 值标签

factor() 函数可以用来为分类变量创建值标签。继续上面的例子,假设你有一个名为 gender 的变量,男性编码为 1,女性编码为 2。你可以使用以下代码创建值标签

patientdata$gender <- factor(patientdata$gender,
                             levels = c(1,2),
                             labels = c("male", "female"))

在这里,levels 表示变量的实际值,而 labels 指的是包含所需标签的字符向量。

2.5 用于处理数据对象的有用函数

我们将本章以对在处理数据对象时有用的函数的简要总结结束(见表 2.4)。

表 2.4 处理数据对象函数

函数 目的
length(*object*) 返回元素/组件的数量
dim(*object*) 返回对象的维度
str(*object*) 返回对象的结构。
class(*object*) 返回对象的类
mode(*object*) 确定对象是如何存储的
names(*object*) 返回对象中组件的名称
c(*object, object,*...) 将对象组合成一个向量。
cbind(*object, object,* ...) 将对象作为列组合
rbind(*object, object,*...) 将对象作为行组合
object 打印对象
head(*object*) 列出对象的第一个部分
tail(object) 列出对象的最后一个部分
ls() 列出当前对象
rm(*object, object,* ...) 删除一个或多个对象。rm(list = ls()) 语句从工作环境中删除大多数对象。
*newobject* <- edit(*object*) 编辑 object 并将其保存为 newobject
fix(*object*) 在原地编辑对象

我们已经讨论了这些函数中的大部分。head()tail() 对于快速扫描大型数据集非常有用。例如,head(patientdata) 列出了数据框的前六行,而 tail(patientdata) 列出了最后六行。我们将在下一章中介绍 length()cbind()rbind() 等函数;它们在这里作为参考。

正如你所看到的,R 提供了丰富的函数来访问外部数据。附录 C 介绍了将数据从 R 导出为其他格式的方法,附录 F 介绍了处理大型数据集(千兆到太字节范围)的方法。

一旦你将数据集导入 R,你很可能需要将它们转换成更便于使用的格式。在第三章中,我们将探讨创建新变量、转换和重新编码现有变量、合并数据集和选择观察值的方法。

摘要

  • R 提供了各种用于存储数据的对象,包括向量、矩阵、数据框和列表。

  • 你可以从外部源导入数据到 R 数据框中,包括文本文件、Excel 工作表、Web API、统计软件包和数据库。

  • 有大量的函数用于描述、修改和组合数据结构。

3 基本数据管理

本章涵盖

  • 操作日期和缺失值

  • 理解数据类型转换

  • 创建和重新编码变量

  • 排序、合并和子集数据集

  • 选择和删除变量

在第二章中,我们介绍了将数据导入 R 的各种方法。不幸的是,将数据以矩阵或数据框的矩形排列形式准备好,只是分析准备的第一步。为了引用《星际迷航》中“末日审判的滋味”一集中队长柯克的台词(并最终证明我的极客身份),“数据是一团糟——非常非常糟糕的一团糟。”在我的工作中,任何数据分析项目 60%的时间都花在清理和组织数据上。我敢打赌,大多数现实世界的数据分析师也是如此。让我们来看一个例子。

3.1 一个工作示例

我目前在工作中研究的一个主题是男性和女性在领导组织的方式上存在哪些差异。典型的问题可能包括

  • 管理职位上的男性和女性在向上级让步的程度上是否存在差异?

  • 这是否因国家而异,还是这些性别差异是普遍存在的?

解决这些问题的方法之一是让多个国家的上司使用以下问题对他们的经理的顺从行为进行评分。

这位经理在做出人事决策之前会征求我的意见。
1
强烈不同意

结果数据可能类似于表 3.1 中的数据。每一行代表上司对经理给出的评分。

表 3.1 领导行为中的性别差异

经理 日期 国家 性别 年龄 q1 q2 q3 q4 q5
1 10/24/14 US M 32 5 4 5 5 5
2 10/28/14 US F 45 3 5 2 5 5
3 10/01/14 US F 25 3 5 5 5 2
4 10/12/14 US M 39 3 3 4
5 05/01/14 US F 99 2 2 1 2 1

在这里,每位经理根据与其相关的五个陈述(q1 至 q5)对其权威的顺从程度进行评分。例如,经理 1 是一位 32 岁的男性,在美国工作,并被其上司评价为顺从,而经理 5 是一位年龄不详的女性(99 可能表示信息缺失),在英国工作,并在顺从行为上得分较低。日期列捕捉了评分的时间。

尽管数据集可能有数十个变量和数千个观测值,但我只包括了 10 列和 5 行来简化示例。此外,我还将涉及经理顺从行为的条目限制为 5 个。在现实世界的研究中,你可能会使用 10 到 20 个这样的条目来提高结果的可靠性和有效性。你可以使用以下代码创建包含表 3.1 中数据的数据框。

列表 3.1 创建领导数据框

leadership <- data.frame(
   manager = c(1, 2, 3, 4, 5),
   date    = c("10/24/08", "10/28/08", "10/1/08", "10/12/08", "5/1/09"),
   country = c("US", "US", "UK", "UK", "UK"),
   gender  = c("M", "F", "F", "M", "F"),
   age     = c(32, 45, 25, 39, 99),
   q1      = c(5, 3, 3, 3, 2),
   q2      = c(4, 5, 5, 3, 2),
   q3      = c(5, 2, 5, 4, 1),
   q4      = c(5, 5, 5, NA, 2),
   q5      = c(5, 5, 2, NA, 1)
)

为了解决感兴趣的问题,你必须首先处理几个数据管理问题。以下是一个部分列表:

  • 五个评分(q1 到 q5)需要合并,从而从每个经理那里得到一个单一的均值差异分数。

  • 在调查中,受访者经常跳过问题。例如,老板对经理的 4 分评价跳过了问题 4 和 5。你需要一种处理不完整数据的方法。你还需要将像 99 这样的年龄值重新编码为缺失

  • 数据集中可能有数百个变量,但你可能只对其中几个感兴趣。为了简化问题,你将想要创建一个只包含感兴趣变量的新数据集。

  • 过去的研究表明,领导行为可能会随着管理者年龄的变化而变化。为了检验这一点,你可能想将当前年龄值重新编码到一个新的年龄分类中(例如,年轻、中年、老年)。

  • 领导行为可能会随时间而改变。你可能想专注于最近全球金融危机期间的差异行为。为此,你可能想将研究限制在特定时间段收集的数据(例如,2009 年 1 月 1 日至 2009 年 12 月 31 日)。

我们将在本章中逐一解决这些问题,以及其他基本的数据管理任务,例如合并和排序数据集。然后,在第五章中,我们将探讨一些高级主题。

3.2 创建新变量

在典型的研究项目中,你需要创建新变量和转换现有变量。这是通过以下形式的语句完成的:

*variable <- expression*

可以在语句的expression部分包含各种运算符和函数。表 3.2 列出了 R 的算术运算符。

表 3.2 算术运算符

运算符 描述
+ 加法
- 减法
* 乘法
/ 除法
^** 幂运算
x%%y 模数(x mod y):例如,5%%21
x%/%y 整数除法:例如,5%/%22

给定数据框leadership,假设你想创建一个新变量total_score,它将变量 q1 到 q5 相加,并创建一个名为mean_score的新变量,该变量平均这些变量。如果你使用以下代码

total_score  <-  q1 + q2 + q3 + q4 + q5
mean_score <- (q1 + q2 + q3 + q4 + q5)/5

你会得到一个错误,因为 R 不知道q1q2q3q4q5是从数据框leadership中来的。如果你使用下面的代码代替

total_score  <-  leadership$q1 + leadership$q2 + leadership$q3 + 
                   leadership$q4 + leadership$q5
mean_score <- (leadership$q1 + leadership$q2 + leadership$q3 + 
                   leadership$q4 + leadership$q5)/5

这些语句将成功,但你最终会得到一个数据框(leadership)和两个单独的向量(total_scoremean_score)。这可能不是你想要的结果。最终,你想要将新变量纳入原始数据框中。以下列表提供了两种实现这一目标的方法。你可以选择其中一种;结果将是相同的。

列表 3.2 创建新变量

leadership$total_score  <-  leadership$q1 + leadership$q2 + leadership$q3 + 
                              leadership$q4 + leadership$q5
leadership$mean_score <- (leadership$q1 + leadership$q2 + leadership$q3 + 
                            leadership$q4 + leadership$q5)/5

leadership <- transform(leadership,
                    total_score  =  q1 + q2 + q3 + q4 + q5,
                    mean_score = (q1 + q2 + q3 + q4 + q5)/5)

个人而言,我更喜欢第二种方法,即使用transform()函数的例子。它简化了包含尽可能多的新变量,并将结果保存到数据框中。

3.3 重新编码变量

重新编码涉及根据同一变量和/或其他变量的现有值创建变量的新值。例如,你可能想要

  • 将连续变量转换为一系列类别

  • 将错误编码的值替换为正确值

  • 根据一组截止分数创建通过/失败变量

要重新编码数据,你可以使用一个或多个 R 的逻辑运算符(见表 3.3)。逻辑运算符是返回 TRUEFALSE 的表达式。

表 3.3 逻辑运算符

运算符 描述
< 小于
<= 小于或等于
> 大于
>= 大于或等于
== 精确等于
!= 不等于
!x x
`x y`
x & y xy
isTRUE(x) 测试 x 是否为 TRUE

假设你想要将领导数据集中管理员的年龄从连续变量 age 重新编码为分类变量 agecatYoungMiddle AgedElder)。首先,你必须将 age 的值 99 重新编码,以表示该值缺失,可以使用如下代码

leadership$age[leadership$age  == 99]     <- NA

语句 variable[condition] <- expression 只有在 conditionTRUE 时才会进行赋值。

一旦指定了 age 的缺失值,然后你可以使用以下代码创建 agecat 变量:

leadership$agecat[leadership$age  > 75]   <- “Elder”
leadership$agecat[leadership$age >= 55 & 
                  leadership$age <= 75]   <- “Middle Aged”
leadership$agecat[leadership$age  < 55]   <- “Young”

你在 leadership$agecat 中包含数据框名称,以确保新变量被保存回数据框。 (我将中年定义为 55 至 75,这样我就不会觉得那么老了。)请注意,如果你没有首先将 99 重新编码为 age缺失,经理 5 就会被错误地赋予 agecat 的值“Elder”。

这段代码可以更紧凑地写成

leadership <- within(leadership,{
                     agecat <- NA
                     agecat[age > 75]              <- "Elder"
                     agecat[age >= 55 & age <= 75] <- "Middle Aged"
                     agecat[age < 55]              <- "Young" })

within() 函数与 with() 函数(第 2.2.4 节)类似,但它允许你修改数据框。首先,创建变量 agecat 并将其设置为数据框每一行的缺失值。然后,执行花括号内的剩余语句。记住,agecat 是一个字符变量;你很可能想要将其转换为有序因子,如第 2.2.5 节所述。

几个包提供了有用的重新编码函数;特别是,car 包的 recode() 函数可以非常简单地重新编码数值和字符向量以及因子。doBy 包提供了 recodeVar(),另一个流行的函数。最后,R 内置了 cut() 函数,它允许你将数值变量的范围划分为区间,并返回一个因子。

3.4 重命名变量

如果你对自己的变量名不满意,你可以交互式地或以编程方式更改它们。假设你想要将变量 manager 改为 managerID,将 date 改为 testDate。你可以使用以下语句调用交互式编辑器:

fix(leadership)

然后,你点击变量名,并在显示的对话框中重命名它们(见图 3.1)。

图 3.1 使用 fix() 函数交互式重命名变量

通过编程方式,您可以通过 names() 函数重命名变量。例如,以下语句

names(leadership)[2] <- "testDate"

如以下代码所示,将 date 重命名为 testDate

> names(leadership)
 [1] "manager" "date"    "country" "gender"  "age"     "q1"      "q2"     
 [8] "q3"      "q4"      "q5"    
> names(leadership)[2] <- "testDate"
> leadership
  manager testDate country gender age q1 q2 q3 q4 q5
1       1 10/24/08      US      M  32  5  4  5  5  5
2       2 10/28/08      US      F  45  3  5  2  5  5
3       3  10/1/08      UK      F  25  3  5  5  5  2
4       4 10/12/08      UK      M  39  3  3  4 NA NA
5       5   5/1/09      UK      F  99  2  2  1  2  1

以类似的方式,以下语句

names(leadership)[6:10] <- c("item1", "item2", "item3", "item4", "item5")

将 q1 到 q5 重命名为 item1 到 item5。

3.5 缺失值

在任何规模的项目中,由于漏题、设备故障或数据编码不当,数据很可能会不完整。在 R 中,缺失值用符号 NA(不可用)表示。与 SAS 等程序不同,R 使用相同的缺失值符号来表示字符和数值数据。

R 提供了多个函数用于识别包含缺失值的观测值。函数 is.na() 允许您测试是否存在缺失值。假设您有如下向量:

y <- c(1, 2, 3, NA)

然后以下函数返回 c(FALSE, FALSE, FALSE, TRUE)

is.na(y) 

注意 is.na() 函数在对象上的工作方式。它返回一个大小相同的对象,如果元素是缺失值,则条目被替换为 TRUE,如果元素不是缺失值,则条目被替换为 FALSE。以下列表将此应用于领导力示例。

列表 3.3 应用 is.na() 函数

> is.na(leadership[,6:10])
        q1    q2    q3    q4    q5
[1,] FALSE FALSE FALSE FALSE FALSE
[2,] FALSE FALSE FALSE FALSE FALSE
[3,] FALSE FALSE FALSE FALSE FALSE
[4,] FALSE FALSE FALSE  TRUE  TRUE
[5,] FALSE FALSE FALSE FALSE FALSE 

在这里,leadership[,6:10] 限制了数据框到第 6 到 10 列,而 is.na() 识别了哪些值是缺失的。

当您在 R 中处理缺失值时,需要记住两个重要的事情。首先,缺失值被认为是不可比较的,即使是与自身比较。这意味着您不能使用比较运算符来测试缺失值的存在。例如,逻辑测试 myvar == NA 永远不会返回 TRUE。相反,您必须使用缺失值函数(如 is.na())来识别 R 数据对象中的缺失值。

其次,R 不将无限或不可能的值表示为缺失值。这与其他程序(如 SAS)处理此类数据的方式不同。正无穷和负无穷分别用符号 Inf–Inf 表示。因此,5/0 返回 Inf。不可能的值(例如,sin(Inf))用符号 NaN(非数字)表示。要识别这些值,您需要使用 is .infinite()is.nan()

3.5.1 将值重新编码为缺失

如我们在 3.3 节中看到的,您可以使用赋值来将值重新编码为 缺失。在领导力示例中,缺失的 age 值被编码为 99。在分析此数据集之前,您必须让 R 知道在这种情况下值 99 代表 缺失——否则,这个老板样本的平均年龄将会偏差很大。您可以通过重新编码变量来完成此操作:

leadership$age[leadership$age == 99] <- NA

任何等于 99 的年龄值都更改为 NA。在分析数据之前,请确保任何缺失数据都正确编码为缺失值,否则结果将没有意义。

3.5.2 从分析中排除缺失值

一旦你确定了缺失值,在进一步分析数据之前,你需要以某种方式消除它们。原因是包含缺失值的算术表达式和函数会产生缺失值。例如,考虑以下代码:

x <- c(1, 2, NA, 3)
y <- x[1] + x[2] + x[3] + x[4]
z <- sum(x)

由于x的第三个元素缺失,yz都将为NA(缺失)。

幸运的是,大多数数值函数都有一个na.rm=TRUE选项,该选项在计算之前删除缺失值,并将函数应用于剩余的值:

x <- c(1, 2, NA, 3)
y <- sum(x, na.rm=TRUE)

在这里,y等于 6。

当使用包含不完整数据的函数时,务必通过查看其在线帮助(例如,help(sum))来检查该函数如何处理缺失数据。sum()函数是我们将在第五章考虑的许多函数之一。函数允许你以灵活和简便的方式转换数据。

你可以使用na.omit()函数删除任何包含缺失数据的观测值,该函数会删除任何包含缺失数据的行。让我们在下面的列表中将此应用于领导力数据集。

列表 3.4 使用na.omit()删除不完整观测值

> leadership    
  manager     date country gender age q1 q2 q3 q4 q5      ❶
1       1 10/24/08      US      M  32  5  4  5  5  5      ❶
2       2 10/28/08      US      F  40  3  5  2  5  5      ❶
3       3 10/01/08      UK      F  25  3  5  5  5  2      ❶
4       4 10/12/08      UK      M  39  3  3  4 NA NA      ❶
5       5 05/01/09      UK      F  NA  2  2  1  2  1      ❶

> newdata <- na.omit(leadership)    
> newdata    
  manager     date country gender age q1 q2 q3 q4 q5      ❷
1       1 10/24/08      US      M  32  5  4  5  5  5      ❷
2       2 10/28/08      US      F  40  3  5  2  5  5      ❷
3       3 10/01/08      UK      F  25  3  5  5  5  2      ❷

❶ 包含缺失数据的数据框

❷ 仅包含完整案例的数据框

在将结果保存到newdata之前,任何包含缺失数据的行都会从领导力数据集中删除。

删除所有包含缺失数据的观测值(称为列表删除)是处理不完整数据集的几种方法之一。如果只有少数值缺失,或者它们集中在少数观测值中,列表删除可以提供解决缺失值问题的良好解决方案。但如果缺失值分布在整个数据中,或者少数变量中有大量数据缺失,列表删除可能会排除大量数据。我们将在第十八章中探讨处理缺失值的几种更复杂的方法。接下来,让我们看看日期。

3.6 日期值

日期通常以字符字符串的形式输入到 R 中,然后转换为存储为数值的日期变量。as.Date()函数用于进行这种转换。语法是as.Date(x, "input_format"),其中x是字符数据,input_format给出了读取日期的适当格式(见表 3.4)。

表 3.4 日期格式

符号 含义 示例
%d 日期作为数字(0–31) 01–31
%a %A 简写星期非简写星期 MonMonday
%m 月份(01–12) 01–12
%b %B 简写月份非简写月份 JanJanuary
%y %Y 两位数年份四位数字年份 072007

输入日期的默认格式是 yyyy-mm-dd。以下语句

mydates <- as.Date(c("2007-06-22", "2004-02-13"))

使用此默认格式将字符数据转换为日期。相比之下,

strDates <- c("01/05/1965", "08/16/1975")
dates <- as.Date(strDates, "%m/%d/%Y")

使用 mm/dd/yyyy 格式读取数据。

在领导力数据集中,日期以 mm/dd/yy 格式编码为字符变量。因此

myformat <- "%m/%d/%y"
leadership$date <- as.Date(leadership$date, myformat)

使用指定的格式读取字符变量并将其替换为数据帧中的日期变量。一旦变量以日期格式存在,你就可以使用后面章节中涵盖的广泛分析技术来分析和绘制日期。

两个函数特别适用于时间戳数据。Sys.Date()返回今天的日期,而date()返回当前的日期和时间。在我写这篇文章的时候,是 2021 年 7 月 7 日晚上 6:43。执行这些函数会产生

> Sys.Date()
[1] "2021-07-20"
> date()
[1] "Tue Jul 20 18:43:40 2021"

你可以使用format(x, format="output_format")函数以指定格式输出日期并提取日期的部分:

> today <- Sys.Date()
> format(today, format="%B %d %Y")
[1] "July 20 2021"
> format(today, format="%A")
[1] "Tuesday"

format()函数接受一个参数(在这种情况下是一个日期)并应用一个输出格式(在这种情况下,由表 3.4 中的符号组成)。这里的重要结果是,距离周末只剩两天了!

当 R 内部存储日期时,它们被表示为自 1970 年 1 月 1 日起的天数,较早的日期使用负值。这意味着你可以对它们进行算术运算。例如,

> startdate <- as.Date("2020-02-13")
> enddate   <- as.Date("2021-01-22")
> days      <- enddate - startdate
> days
Time difference of 344 days 

显示了 2020 年 2 月 13 日和 2021 年 1 月 22 日之间的天数。

最后,你也可以使用difftime()函数来计算时间间隔,并以秒、分钟、小时、天或周的形式表示。假设我是在 1956 年 10 月 12 日出生的。我多大了?:

> today <- Sys.Date()
> dob   <- as.Date("1956-10-12")
> difftime(today, dob, units="weeks")
Time difference of 3380 weeks  

显然,我已经 3,380 周大了。谁知道呢?额外加分:我是在星期几出生的?

3.6.1 将日期转换为字符变量

你还可以将日期变量转换为字符变量。可以使用as.character()函数将日期值转换为字符值:

strDates <- as.character(dates)

这种转换允许你将一系列字符函数应用于数据值(子集、替换、连接等)。我们将在第 5.2.4 节中详细讨论字符函数。

3.6.2 进一步学习

要了解更多关于将字符数据转换为日期的信息,请查看help(as.Date)help(strftime)。要了解更多关于日期和时间的格式化信息,请参阅help(ISOdatetime)lubridate包包含许多简化日期处理的函数,包括用于识别和解析日期时间数据、提取日期时间组件(例如,年、月、日、小时和分钟)以及在对日期时间进行算术计算。如果你需要使用日期进行复杂计算,timeDate包也可以提供帮助。它提供了一系列处理日期的函数,可以同时处理多个时区,并提供复杂的日历操作,识别工作日、周末和假日。

3.7 类型转换

我们刚刚讨论了如何将字符数据转换为日期值以及相反的操作。R 提供了一套函数来识别对象的数据类型并将其转换为不同的数据类型。

R 中的类型转换与其他统计编程语言中的转换类似。例如,将字符字符串添加到数值向量会将向量中的所有元素转换为字符值。你可以使用表 3.5 中列出的函数来测试数据类型并将其转换为给定类型。

表 3.5 类型转换函数

测试 转换
is.numeric() as.numeric()
is.character() as.character()
is.vector() as.vector()
is.matrix() as.matrix()
is.data.frame() as.data.frame()
is.factor() as.factor()
is.logical() as.logical()

形式为 is.datatype() 的函数返回 TRUEFALSE,而 as.datatype() 将参数转换为该类型。以下列表提供了一个示例。

列表 3.5 从一种数据类型转换为另一种数据类型

> a <- c(1,2,3)
> a
[1] 1 2 3
> is.numeric(a)
[1] TRUE
> is.vector(a)
[1] TRUE
> a <- as.character(a)
> a
[1] "1" "2" "3"
> is.numeric(a)
[1] FALSE
> is.vector(a)
[1] TRUE
> is.character(a)
[1] TRUE

当与第五章中将要讨论的流程控制(如 if-then)结合使用时,is.datatype() 函数可以成为一个强大的工具,允许你根据数据类型以不同的方式处理数据。此外,一些 R 函数需要特定类型的数据(字符或数值、矩阵或数据框),而 as.datatype() 允许你在分析之前将数据转换为所需的格式。

3.8 排序数据

有时,以排序顺序查看数据集可以告诉你很多关于数据的信息。例如,哪些经理最谦逊?在 R 中,你使用 order() 函数对数据框进行排序。默认情况下,排序顺序是升序。在排序变量前加一个负号以表示降序。以下示例说明了使用领导数据框进行排序。

语句

newdata <- leadership[order(leadership$age),]

创建一个新的数据集,其中包含按经理年龄从年轻到老排序的行。语句

newdata <- leadership[order(leadership$gender, leadership$age),]

将行按女性排序,然后在每个性别内按年龄从年轻到老排序。

最后,

newdata <-leadership[order(leadership$gender, -leadership$age),]

按性别排序行,然后在每个性别内按年龄从老到少排序经理。

3.9 合并数据集

如果你的数据存在于多个位置,在继续之前你需要将其合并。本节将向你展示如何向数据框添加列(变量)和行(观测值)。

3.9.1 向数据框添加列

要水平合并两个数据框(数据集),你使用 merge() 函数。在大多数情况下,两个数据框通过一个或多个公共键变量(即内部连接)连接。例如,

total <- merge(dataframeA, dataframeB, by="ID")

通过 ID 合并 dataframeAdataframeB。同样,

total <- merge(dataframeA, dataframeB, by=c("ID","Country")) 

通过 IDCountry 合并两个数据框。这种类型的水平连接通常用于向数据框添加变量。

使用 cbind() 进行水平连接

如果你水平连接两个矩阵或数据框,并且不需要指定公共键,你可以使用 cbind() 函数:

total <- cbind(A, B)

此函数水平连接对象 AB。为了使函数正常工作,每个对象必须具有相同数量的行,并且必须按相同的顺序排序。

3.9.2 向数据框添加行

要垂直连接两个数据框(数据集),请使用rbind()函数:

total <- rbind(dataframeA, dataframeB) 

两个数据框必须具有相同的变量,但它们不必按相同的顺序排列。如果dataframeAdataframeB没有的变量,那么在连接它们之前,你可以执行以下操作之一:

  • 删除dataframeA中的额外变量。

  • dataframeB中创建额外的变量并将它们设置为NA(缺失)。

垂直连接通常用于向数据框中添加观测值。

3.10 子集数据集

R 具有强大的索引功能,用于访问对象的元素。这些功能可以用来选择和排除变量、观测值或两者。以下几节将演示几种保留或删除变量和观测值的方法。

3.10.1 选择变量

从较大的数据集中选择有限数量的变量来创建新的数据集是很常见的。第二章展示了数据框的元素是通过使用表示法dataframe[row indices, column indices]来访问的。你可以使用这个表示法来选择变量。例如,

newdata <- leadership[, c(6:10)] 

从领导数据框中选择变量q1q2q3q4q5,并将它们保存到数据框newdata中。留空行索引(())默认选择所有行。

以下语句

vars <- c("q1", "q2", "q3", "q4", "q5")
newdata <-leadership[, vars]

实现相同的变量选择。在这里,变量名(用引号括起来)作为列索引输入,从而选择相同的列。

如果为数据框提供了一个索引集,R 假定你正在子集化列。在以下语句中,逗号被假定为

newdata <- leadership[vars]

并对相同的变量集进行子集化。

最后,你可以使用

myvars <- paste("q", 1:5, sep="") 
newdata <- leadership[myvars]

此示例使用paste()函数创建与上一个示例相同的字符向量。paste()函数将在第五章中介绍。

3.10.2 删除变量

有许多原因需要排除变量。例如,如果一个变量有很多缺失值,你可能在进一步分析之前想要删除它。让我们看看一些排除变量的方法。

你可以使用以下语句排除变量q3q4

myvars <- names(leadership) %in% c("q3", "q4") 
newdata <- leadership[!myvars]

要理解为什么这样做有效,你需要将其分解:

  1. names(leadership)生成一个包含变量名的字符向量:

    c("managerID","testDate","country","gender","age","q1","q2","q3","q4","q5")
    
  2. names(leadership)%in%c("q3", "q4")返回一个逻辑向量,对于names(leadership)中与q3q4匹配的每个元素返回TRUE,否则返回FALSE

    c(FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, TRUE, TRUE, FALSE)
    
  3. 非(!)运算符反转逻辑值:

    c(TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, FALSE, FALSE, TRUE)
    
  4. leadership[c(TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, FALSE, FALSE, TRUE)]选择具有TRUE逻辑值的列,因此排除了q3q4

知道q3q4是第八和第九个变量,你可以使用以下语句来排除它们:

newdata <- leadership[c(-8,-9)]

这之所以有效,是因为在列索引前加上负号(-)可以排除该列。

最后,同样的删除操作可以通过以下方式完成:

leadership$q3 <- leadership$q4 <- NULL

在这里,你将列q3q4设置为未定义的(NULL)。请注意,NULLNA(缺失)不同。

删除变量是保留变量的逆过程。选择哪种方式取决于哪种方式更容易编码。如果有许多变量需要删除,可能更容易保留剩下的变量,反之亦然。

3.10.3 选择观测值

选择或排除观测值(行)通常是成功数据准备和分析的关键方面。以下列表中给出了几个示例。

列表 3.6 选择观测值

newdata <- leadership[1:3,]                        ❶

newdata <- leadership[leadership$gender=="M" &   ❷❸
                      leadership$age > 30,]        ❸

❶ 询问第 1 至 3 行(前三个观测值)

❸ 选择所有 30 岁以上的男性

这些示例中的每一个都提供了行索引,并留出列索引空白(因此选择所有列)。让我们分析 ❷ 中的代码行来理解它:

  1. 逻辑比较 leadership$gender=="M" 产生向量 c(TRUE, FALSE, FALSE, TRUE, FALSE)

  2. 逻辑比较 leadership$age > 30 产生向量 c(TRUE, TRUE, FALSE, TRUE, TRUE)

  3. 逻辑比较 c(TRUE, FALSE, FALSE, TRUE, FALSE) & c(TRUE, TRUE, FALSE, TRUE, TRUE) 产生向量 c(TRUE, FALSE, FALSE, TRUE, FALSE)

  4. leadership[c(TRUE, FALSE, FALSE, TRUE, FALSE),] 从数据框中选择第一个和第四个观测值(当行索引为 TRUE 时,行被包含;当它是 FALSE 时,行被排除)。这符合选择标准(30 岁以上的男性)。

在本章的开头,我建议您可能希望将您的分析限制在 2009 年 1 月 1 日至 2009 年 12 月 31 日期间收集的观测值。您如何做到这一点?这里有一个解决方案:

leadership$date <- as.Date(leadership$date, "%m/%d/%y")     ❶

startdate <- as.Date("2009-01-01")                          ❷
enddate   <- as.Date("2009-12-31")                          ❸

newdata <- leadership[which(leadership$date >= startdate &  ❹
           leadership$date <= enddate),]                    ❹

❶ 将最初以字符值读入的日期值转换为日期值,格式为 mm/dd/yy

❷ 创建起始日期

❸ 创建结束日期

❹ 选择符合您所需标准的情况,如前一个示例所示

注意,as.Date() 函数的默认格式是 yyyy-mm-dd,因此您在这里不需要提供它。

3.10.4 subset() 函数

前两个示例中的例子很重要,因为它们有助于描述在 R 中逻辑向量和比较运算符是如何被解释的。理解这些例子的工作方式将有助于您一般地解释 R 代码。既然您已经用困难的方式完成了这些事情,让我们看看一种捷径。

subset() 函数可能是选择变量和观测值的最简单方法。以下有两个示例:

newdata <- subset(leadership, age >= 35 | age < 23,    ❶
                  select=c(q1, q2, q3, q4))            ❶

newdata <- subset(leadership, gender=="M" & age > 25,  ❷
                  select=gender:q4)                    ❷

❶ 选择所有年龄大于或等于 35 或小于 23 的行。保留变量 q1 至 q4。

❷ 选择所有 25 岁以上的男性,并保留变量 gender 通过 q4(gender、q4 以及它们之间的所有列)。

您在第二章中看到了冒号运算符用于生成数字序列。在 subset 函数中,from:to 返回数据框中 from 变量和 to 变量之间的所有变量,包括这两个变量。

3.10.5 随机样本

在数据挖掘和机器学习中,从更大的数据集中进行抽样是很常见的。例如,你可能想要选择两个随机样本,从一个样本中创建预测模型,并在另一个样本上验证其有效性。sample() 函数允许你从一个数据集中随机抽取大小为 n 的样本(带或不带替换)。

你可以使用以下语句从领导数据集中随机抽取大小为 3 的样本:

mysample <- leadership[sample(1:nrow(leadership), 3, replace=FALSE),] 

sample() 的第一个参数是从中选择元素的向量。在这里,向量是从 1 到数据框中观测值的数量。第二个参数是要选择的元素数量,第三个参数表示不进行替换的抽样。sample() 返回随机抽取的元素,然后用于从数据框中选择行。

R 提供了广泛的抽样功能,包括绘制和校准调查样本(参见 sampling 包)以及分析复杂的调查数据(参见 survey 包)。第十二章描述了依赖于抽样的其他方法,包括自助法和重抽样统计。

3.11 使用 dplyr 操作数据框

到目前为止,我们已经使用基础 R 函数操作了 R 数据框。dplyr 包提供了一系列快捷方式,允许你以简化的方式完成相同的数据管理任务。它正迅速成为最受欢迎的 R 数据管理包之一。

3.11.1 基本 dplyr 函数

dplyr 包提供了一组函数,可用于选择变量和观测值、转换变量、重命名变量以及排序行。表 3.6 列出了相关函数。

表 3.6 dplyr 用于操作数据框的函数

函数 用途
select() 选择变量/列
filter() 选择观测值/行
mutate() 转换或重新编码变量
rename() 重命名变量/列
recode() 重新编码变量值
arrange() 按变量值排序行

让我们回到表 3.1 中创建的数据框,并在表 3.7 中重现以方便起见。

表 3.7 领导行为中的性别差异

经理 日期 国家 性别 年龄 q1 q2 q3 q4 q5
1 10/24/14 美国 M 32 5 4 5 5 5
2 10/28/14 美国 F 45 3 5 2 5 5
3 10/01/14 英国 F 25 3 5 5 5 2
4 10/12/14 英国 M 39 3 3 4
5 05/01/14 英国 F 99 2 2 1 2 1

这次,我们将使用 dplyr 函数来操作数据集。代码在列表 3.7 中提供。由于 dplyr 不是 R 基础包的一部分,首先需要安装它(install.packages("dplyr"))。

列表 3.7 使用 dplyr 操作数据

leadership <- data.frame(
   manager = c(1, 2, 3, 4, 5),
   date    = c("10/24/08", "10/28/08", "10/1/08", "10/12/08", "5/1/09"),
   country = c("US", "US", "UK", "UK", "UK"),
   gender  = c("M", "F", "F", "M", "F"),
   age     = c(32, 45, 25, 39, 99),
   q1      = c(5, 3, 3, 3, 2),
   q2      = c(4, 5, 5, 3, 2),
   q3      = c(5, 2, 5, 4, 1),
   q4      = c(5, 5, 5, NA, 2),
   q5      = c(5, 5, 2, NA, 1)
   )   

library(dplyr)                                                  ❶

leadership <- mutate(leadership,                                ❷
                     total_score = q1 + q2 + q3 + q4 + q5,      ❷
                     mean_score = total_score / 5)              ❷

leadership$gender <- recode(leadership$gender,                  ❸
                            "M" = "male", "F" = "female")       ❸

leadership <- rename(leadership, ID = "manager", sex = "gender")❹

leadership <- arrange(leadership, sex, total_score)             ❺

leadership_ratings <- select(leadership, ID, mean_score)        ❻

leadership_men_high <- filter(leadership,                       ❼
                              sex == "male" & total_score > 10) ❼

❶ 加载 dplyr 包。

❷ 创建两个汇总变量。

❸ 将 M 和 F 重新编码为男性和女性。

❹ 重命名经理和性别变量。

❺ 按性别排序数据,然后在性别内按总分排序。

❻ 创建包含评分变量的新数据框。

❼ 创建一个新的数据框,包含总分超过 10 的男性。

首先,加载 dplyr 包。然后使用 mutate() 函数创建总分和平均分。格式是

*dataframe* <- mutate(*dataframe,* 
                    *newvar1 = expression,* 
                    *newvar2 = expression, ...*).

新变量被添加到数据框中。

接下来,使用 recode() 函数修改 gender 变量的值。格式是

*vector* <- recode(*vector,* 
                 *oldvalue1 = newvalue2,* 
                 *oldvalue2 = newvalue2, ...*).

没有给定新值的向量值保持不变。例如,

x <- c("a", "b", "c")
x <- recode(x, "a" = "apple", "b" = "banana")
x
[1] "apple" "banana" "c"

对于数值,使用反引号来引用原始值:

> y <- c(1, 2, 3)
> y <- recode(y, `1` = 10, `2` = 15)
> y
[1] 10 15 3

接下来,使用 rename() 函数更改变量名。格式是

*dataframe* <- rename(*dataframe,* 
                    *newname1 = "oldname1",* 
                    *newname2 = "oldname2", ...*).

然后使用 arrange() 函数对数据进行排序。首先,按 sex 升序排序行(女性随后是男性)。接下来,在每组性别内按 total_score 升序排序行(低分到高分)。使用 desc() 函数来反转排序顺序。例如,

leadership <- arrange(leadership, sex, desc(total_score))   

将按 sex 升序排序数据,并在每个性别内按降序(高分到低分 total_scores)排序。

select 语句用于选择或排除变量。在这种情况下,选择了变量 IDmean_score

select() 函数的格式是

*dataframe* <- select(dataframe, *variablelist1, variablelist2, ...*) 

变量列表通常是变量名,不带引号。可以使用冒号运算符(:)来选择变量范围。此外,可以使用函数来选择包含特定文本字符串的变量。例如,以下语句

leadership_subset <- select(leadership, 
                             ID, country:age, starts_with("q")) 

将选择变量 IDcountrysexageq1q2q3q4q5。有关可用于辅助变量选择的函数列表,请参阅 help(select_helpers)

使用负号(-)来排除变量。该语句

leadership_subset <- select(leadership, -sex, -age)

会包括所有变量,除了 sexage

最后,使用 filter() 函数选择满足给定一组标准的数据框中的观测值或行。在这里,保留总分大于 10 的男性。格式是

*dataframe* <- filter(*dataframe, expression*)

如果表达式为 TRUE,则保留行。可以使用表 3.3 中的任何逻辑运算符,并且可以使用括号来澄清这些运算符的优先级。例如,

extreme_men <- filter(leadership, 
                       sex == "male" & 
                       (mean_score < 2 | mean_score > 4))

将创建一个包含所有平均分低于 2 或高于 4 的男性经理的数据框。

3.11.2 使用管道运算符链式调用语句

dplyr 包允许您使用 magrittr 包提供的管道运算符(%>%)以紧凑的格式编写代码。考虑以下三个语句:

high_potentials <- filter(leadership, total_score > 10)
high_potentials <- select(high_potential, ID, country, mean_score)
high_potentials <- arrange(high_potential, country, mean_score)  

这些语句可以使用管道运算符重写为单个语句:

high_potentials <- filter(leadership, total_score > 10) %>%
   select(ID, country, mean_score) %>%
   arrange(country, mean_score)  

%>% 运算符(发音为 THEN)将左侧的结果传递到右侧函数的第一个参数。以这种方式重写的语句通常更容易阅读。

虽然我们已经介绍了基本的 dplyr 函数,但该包还包含用于汇总、组合和重构数据的函数。这些附加函数将在第五章中讨论。

3.12 使用 SQL 语句操作数据框

到目前为止,您一直在使用 R 语句和函数来操作数据。但许多数据分析师在熟悉结构化查询语言(SQL)的情况下来到 R。失去所有这些积累的知识将是一件遗憾的事情。因此,在我们结束之前,让我简要地提及 sqldf 软件包。(如果您不熟悉 SQL,请随意跳过本节。)

下载并安装该软件包(install.packages("sqldf"))后,您可以使用 sqldf() 函数将 SQL SELECT 语句应用于数据框。以下列出两个示例。

列表 3.8 使用 SQL 语句操作数据框

> library(sqldf)                                                   ❶
> newdf <- sqldf("select * from mtcars where carb=1 order by mpg", ❶
                  row.names=TRUE)                                  ❶
> newdf                                                            ❶
                mpg cyl  disp  hp drat   wt qsec vs am gear carb
Valiant        18.1   6 225.0 105 2.76 3.46 20.2  1  0    3    1
Hornet 4 Drive 21.4   6 258.0 110 3.08 3.21 19.4  1  0    3    1
Toyota Corona  21.5   4 120.1  97 3.70 2.46 20.0  1  0    3    1
Datsun 710     22.8   4 108.0  93 3.85 2.32 18.6  1  1    4    1
Fiat X1-9      27.3   4  79.0  66 4.08 1.94 18.9  1  1    4    1
Fiat 128       32.4   4  78.7  66 4.08 2.20 19.5  1  1    4    1
Toyota Corolla 33.9   4  71.1  65 4.22 1.83 19.9  1  1    4    1

> sqldf("select avg(mpg) as avg_mpg, avg(disp) as avg_disp, gear   ❷
              from mtcars where cyl in (4, 6) group by gear")      ❷
  avg_mpg avg_disp gear
1    20.3      201    3
2    24.5      123    4
3    25.4      120    5

❶ 从数据框 mtcars 中选择所有变量(列),仅保留有一个化油器(carb)的汽车(行),按 mpg 升序排序,并将结果保存为数据框 newdf。选项 row.names=TRUE 将原始数据框的行名传递到新数据框中。

❷ 打印具有四个或六个气缸(cyl)的汽车在每个变速器级别(gear)内的平均 mpg 和 disp。

经验丰富的 SQL 用户会发现 sqldf 软件包是 R 中数据管理的有用补充。更多详情请参阅项目主页 (github.com/ggrothendieck/sqldf)。

概述

  • 创建新变量和重新编码现有变量是数据管理的重要部分。

  • 函数允许您存储和操作缺失值和日期值。

  • 变量可以从一种类型(例如,数值)转换为另一种类型(例如,字符)。

  • 根据一组标准,您可以保留(或删除)观测值和变量。

  • 可以水平合并数据集(添加变量)或垂直合并数据集(添加观测值)。

4 开始使用图形

本章涵盖

  • 介绍ggplot2

  • 创建一个简单的双变量(两个变量)图

  • 使用分组和分面创建多变量图

  • 以多种格式保存图形

在许多场合,我向客户展示了精心制作的以数字和文本形式呈现的统计结果,但他们的眼睛却变得空洞,房间里充满了蝉鸣。然而,当我把同样的信息以图表的形式呈现给他们时,那些客户却有了热情的“啊哈!”时刻。通常,通过观察图表,我可以看到数据中的模式或检测到数据值中的异常——这些模式或异常在我进行更正式的统计分析时完全忽略了。

人类在从视觉表示中辨别关系方面非常擅长。一个精心制作的图表可以帮助您在成千上万的信息中进行有意义的比较,提取其他方法难以找到的模式。这也是统计图形领域的进步对数据分析产生如此重大影响的原因之一。数据分析师需要查看他们的数据,而 R 在这方面表现出色。

R 语言通过许多独立软件开发者的贡献在多年中自然增长。这导致了 R 中创建图形的四种不同方法的产生——baselatticeggplot2grid图形。在本章以及剩余章节的大部分内容中,我们将重点关注ggplot2,这是目前 R 中最强大和最受欢迎的方法。

由 Hadley Wickham(2009a)编写的ggplot2包提供了一种基于 Wilkinson(2005)描述的图形语法和 Wickham(2009b)扩展的系统来创建图形。ggplot2包旨在提供一个全面、基于语法的系统,以统一和连贯的方式生成图形,使用户能够创建新的和创新的数据可视化。

本章将指导您通过使用可视化来解答以下问题,了解创建ggplot2图形的主要概念和函数:

  • 工人的过去经验和他们的薪水之间有什么关系?

  • 我们如何简单地总结这种关系?

  • 这种关系对男性和女性是否不同?

  • 工作人员的行业是否重要?

我们将从显示工人经验和工资之间关系的简单散点图开始。然后在每个部分中,我们将添加新的功能,直到我们制作出一个单一的高质量出版物图表,以解决这些问题。在每一步中,我们将对这些问题有更深入的了解。

为了回答这些问题,我们将使用包含在 mosaicData 包中的 CPS85 数据框。该数据框包含从 1985 年 当前人口调查 中随机选取的 534 个个体,包括他们的工资、人口统计信息和工作经验。在继续之前,请确保已安装 mosaicDataggplot2(install.packages(c("mosaicData", "ggplot2"))).

4.1 使用 ggplot2 创建图表

ggplot2 包使用一系列函数分层构建图表。我们将从一个简单的图表开始,逐步添加元素来构建一个复杂的图表。默认情况下,ggplot2 图表显示在灰色背景上,带有白色参考线。

4.1.1 ggplot

构建图表的第一个函数是 ggplot() 函数。它指定以下内容:

  • 包含要绘制数据的 DataFrame。

  • 将变量映射到图表的视觉属性。映射放置在 aes() 函数中(代表美学或“你可以看到的东西”)。

以下代码生成了图 4.1 中的图表:

library(ggplot2)
library(mosaicData)
ggplot(data = CPS85, mapping = aes(x = exper, y = wage))

图 4.1 将工人经验和工资映射到 x 轴和 y 轴

为什么图表是空的?我们指定了 exper 变量应映射到 x 轴,而 wage 变量应映射到 y 轴,但我们还没有指定要在图上放置 什么。在这种情况下,我们希望用点来表示每个参与者。

4.1.2 几何对象

几何对象 是可以放置在图上的几何形状(点、线、条形和阴影区域)。它们通过以 geom_ 开头的函数添加。目前,有 37 种不同的几何对象可用,且列表正在增长。表 4.1 描述了更常见的几何对象,以及每个几何对象常用的选项。

表 4.1 几何函数

函数 添加 选项
geom_bar() 条形图 color, fill, alpha
geom_boxplot() 箱线图 color, fill, alpha, notch, width
geom_density() 密度图 color, fill, alpha, linetype
geom_histogram() 直方图 color, fill, alpha, linetype, binwidth
geom_hline() 水平线 color, alpha, linetype, size
geom_jitter() 振荡点 color, size, alpha, shape
geom_line() 折线图 colorvalpha, linetype, size
geom_point() 散点图 color, alpha, shape, size
geom_rug() 针状图 color, side
geom_smooth() 拟合线 method, formula, color, fill, linetype, size
geom_text() 文本注释 许多;请参阅该函数的帮助文档
geom_violin() 小提琴图 color, fill, alpha, linetype
geom_vline() 垂直线 color, alpha, linetype, size

我们将使用 geom_point() 函数添加点,创建散点图。在 ggplot2 图表中,函数通过使用 + 符号链接在一起来构建最终的图表:

library(ggplot2)
library(mosaicData)
ggplot(data = CPS85, mapping = aes(x = exper, y = wage)) +
  geom_point()

图 4.2 显示了结果。

图 4.2 工作经验与工资的散点图

看起来,随着经验的增加,工资也会增加,但关系较弱。图表还表明存在一个异常值。有一个人工资远高于其他人。我们将删除这个案例并重新绘制图表:

CPS85 <- CPS85[CPS85$wage < 40, ] 
ggplot(data = CPS85, mapping = aes(x = exper, y = wage)) +
  geom_point()

图 4.3 显示了新的图表。

图 4.3 删除异常值后的工作经验与工资的散点图

geom_函数中可以指定多个选项(见表 4.1)。geom_point()的选项包括colorsizeshapealpha。这些分别控制点的颜色、大小、形状和透明度。颜色可以通过名称或十六进制代码指定。形状和线型可以通过表示图案或符号的名称或数字指定。点的大小用从 0 开始的正实数指定。大数字产生更大的点大小。透明度从 0(完全透明)到 1(完全不透明)。添加一定程度的透明度可以帮助可视化重叠的点。这些选项的更详细描述在第十九章中。

让我们将图 4.3 中的点放大,半透明,并改为蓝色。我们还将使用theme(在第 4.1.7 节和第十九章中描述)将灰色背景改为白色。以下代码生成了图 4.4 中的图表:

ggplot(data = CPS85, mapping = aes(x = exper, y = wage)) +
  geom_point(color = "cornflowerblue", alpha = .7, size = 1.5) +
  theme_bw()

我可能会说,这个图表更吸引人(至少如果你有彩色输出),但它并没有增加我们的见解。如果图表中有一条总结经验与工资之间趋势的线,那将是有帮助的。

图 4.4 删除异常值后的工作经验与工资的散点图,修改了点的颜色、透明度和点的大小。应用了bw主题(深色背景)。

我们可以使用geom_smooth()函数添加这一行。选项控制线的类型(线性、二次、非参数),线的粗细,线的颜色,以及置信区间的存在与否。这些内容在第十一章中都有讨论。在这里,我们请求一个线性回归线(method = lm,其中lm代表线性模型):

ggplot(data = CPS85, mapping = aes(x = exper, y = wage)) +
  geom_point(color = "cornflowerblue", alpha = .7, size = 1.5) +
  geom_smooth(method = "lm") +
  theme_bw()

结果显示在图 4.5 中。

图 4.5 工作经验与工资的散点图,带有最佳拟合线

从这条线我们可以看出,平均而言,工资似乎随着经验的增加而适度增加。本章只使用了两个 geom。在未来的章节中,我们将使用其他 geom 来创建各种图表类型,包括条形图、直方图、箱线图、密度图等。

4.1.3 分组

在上一节中,我们将图形特征如颜色和透明度设置为 常量 值。然而,我们也可以将变量值映射到几何对象的颜色、形状、大小、透明度、线型和其他视觉特征。这使得多个观测值可以在单个图形中叠加(称为 分组)。

让我们在图形中添加 sex 并通过 colorshapelinetype: 来表示它:

ggplot(data = CPS85, 
       mapping = aes(x = exper, y = wage, 
                     color = sex, shape = sex, linetype = sex)) +
  geom_point(alpha = .7, size = 1.5) +
  geom_smooth(method = "lm", se = FALSE, size = 1.5) +
  theme_bw()

默认情况下,第一组(女性)由粉色填充的圆圈和实线粉色表示,而第二组(男性)由青色填充的三角形和虚线青色表示。图 4.6 显示了新的图形。

图 4.6 工作经验与工资的散点图,点按性别着色,并为男性和女性分别绘制最佳拟合线

注意,color=sex,shape=sex,linetype=sex 选项放置在 aes() 函数中,因为我们正在将一个变量映射到一个美学。添加 geom_smooth 选项(se = FALSE)用于抑制置信区间,使图形不那么复杂,更容易阅读。size = 1.5 选项使线条略粗。

简化图形

通常,我们的目标是创建尽可能简单的图形,同时准确传达信息。在本章的图形中,我可能会将性别映射到颜色。将映射添加到形状和线型会使图形显得过于复杂。我添加它们是为了创建在本书的颜色(电子书)和灰度(印刷)格式中更容易阅读的图形。

现在看起来男性比女性赚的钱更多(更高的线)。此外,男性和工资之间的关系可能比女性更强(线更陡)。

4.1.4 尺度

如我们所见,aes() 函数用于将变量映射到图形的视觉特征。尺度指定了这些映射如何发生。例如,ggplot2 自动创建带有刻度标记、刻度标记标签和轴标签的图形轴。通常它们看起来很好,但偶尔您可能希望对其外观有更大的控制权。代表组别的颜色是自动选择的,但您可能希望根据您的品味或出版物的要求选择不同的颜色集。

尺度函数(以 scale_ 开头)允许您修改默认的缩放。表 4.2 列出了一些常见的缩放函数。

表 4.2 一些常见的尺度函数

函数 描述
scale_x_continuous(), scale_y_continuous() 对定量变量的 xy 轴进行缩放。选项包括 breaks 用于指定刻度标记,labels 用于指定刻度标记标签,以及 limits 用于控制显示值的范围。
scale_x_discrete(), scale_y_discrete() 与上述用于表示分类变量的轴相同。
scale_color_manual() 指定用于表示分类变量级别的颜色。values选项指定颜色。颜色表可以在www.stat.columbia.edu/~tzheng/files/Rcolor.pdf找到。

在下一个图表中,我们将更改 x 轴和 y 轴的缩放比例以及代表男性和女性的颜色。代表exper的 x 轴将从 0 到 60 以 10 为增量,代表wage的 y 轴将从 0 到 30 以 5 为增量。女性将被编码为偏红色,男性将被编码为偏蓝色。以下代码生成了图 4.7 中的图表:

ggplot(data = CPS85,
       mapping = aes(x = exper, y = wage, 
                     color = sex, shape=sex, linetype=sex)) +
   geom_point(alpha = .7, size = 3) +
   geom_smooth(method = "lm", se = FALSE, size = 1.5) +
   scale_x_continuous(breaks = seq(0, 60, 10)) +
   scale_y_continuous(breaks = seq(0, 30, 5)) +
   scale_color_manual(values = c("indianred3", "cornflowerblue")) +
 theme_bw()

图 4.7 展示了工人经验与工资的散点图,具有自定义的 x 轴和 y 轴以及针对性别的自定义颜色映射

断点由值向量定义。在这里,seq()函数提供了一个快捷方式。例如,seq(0, 60, 10)生成一个从 0 开始,以 10 为增量,到 60 结束的数值向量。

x 轴和 y 轴上的数字更好,颜色也更吸引人(个人看法)。然而,工资是以美元计算的。我们可以使用scale包更改 y 轴上的标签,以表示美元,该包为美元、欧元、百分比等提供了标签格式化。

安装scales包(install.packages("scales")),然后运行以下代码:

ggplot(data = CPS85,
       mapping = aes(x = exper, y = wage, 
                                   color = sex, shape=sex, linetype=sex)) +
     geom_point(alpha = .7, size = 3) +
     geom_smooth(method = "lm", se = FALSE, size = 1.5) +
     scale_x_continuous(breaks = seq(0, 60, 10)) +
     scale_y_continuous(breaks = seq(0, 30, 5),
                        label = scales::dollar) +
     scale_color_manual(values = c("indianred3", "cornflowerblue")) +
  theme_bw()

图 4.8 提供了结果。

图 4.8 展示了工人经验与工资的散点图,具有自定义的 x 轴和 y 轴以及针对性别的自定义颜色映射。工资以美元格式打印。

我们肯定正在取得进展。下一个问题是经验、工资和性别之间的关系是否在每个工作部门都是相同的。让我们为每个工作部门重复这个图表一次来探索这一点。

4.1.5 分面

如果组在并排的图表中而不是在单个图表中重叠,关系可能更清晰。分面会为给定变量的每个级别(或变量的组合)复制一个图表。您可以使用facet_wrap()facet_grid()函数创建分面图表。语法在表 4.3 中给出,其中varrowvarcolvar是因子。

表 4.3 ggplot2分面函数

语法 结果
facet_wrap(~var, ncol=n) var的每个级别分别排列成 n 列
facet_wrap(~var, nrow=n) var的每个级别分别排列成 n 行
facet_grid(rowvar~colvar) rowvarcolvar的每个组合创建单独的图表,其中rowvar代表行,colvar代表列
facet_grid(rowvar~.) rowvar的每个级别创建单独的图表,排列成单列
facet_grid(.~colvar) colvar的每个级别创建单独的图表,排列成单行

在这里,面元将由职业变量的八个级别定义。由于每个面元都将比单独一个面板图更小,我们将从 geom_point() 中省略 size=3,从 geom_smooth() 中省略 size=1.5。这将减小点的大小和线的大小,与之前的图表相比,在分面图中看起来更好。以下代码生成了图 4.9:

ggplot(data = CPS85,
       mapping = aes(x = exper, y = wage, 
                     color = sex, shape = sex, linetype = sex)) +
  geom_point(alpha = .7) +
  geom_smooth(method = "lm", se = FALSE) +
  scale_x_continuous(breaks = seq(0, 60, 10)) +
  scale_y_continuous(breaks = seq(0, 30, 5),
                     label = scales::dollar) +
  scale_color_manual(values = c("indianred3", "cornflowerblue")) +
  facet_wrap(~sector) +
  theme_bw()

图 4.9 展示了工人经验与工资的关系散点图,具有自定义的 x 轴和 y 轴以及针对性别的自定义颜色映射。为八个职业领域的每个领域提供了单独的图表(面元)。

看起来,男性和女性之间的差异取决于考虑的职业领域。例如,对于男性经理来说,经验和工资之间存在强烈的正相关关系,但对于女性经理则没有。在某种程度上,这一点也适用于销售人员。对于男性和女性服务人员来说,似乎不存在经验和工资之间的关系。在任何情况下,男性赚得稍微多一些。对于女性文职人员来说,工资随着经验的增加而上升,但对于男性文职人员来说可能会下降(这里的关系可能不显著)。到目前为止,我们已经对工资与经验之间的关系有了深刻的见解。

4.1.6 标签

图表应该易于解释,并且信息标签是实现这一目标的关键元素。labs() 函数为坐标轴和图例提供自定义标签。此外,还可以添加自定义标题、副标题和说明。让我们在以下代码中修改每个部分:

ggplot(data = CPS85, 
       mapping = aes(x = exper, y = wage,
                 color = sex, shape=sex, linetype=sex)) +
    geom_point(alpha = .7) +
    geom_smooth(method = "lm", se = FALSE) +
    scale_x_continuous(breaks = seq(0, 60, 10)) +
    scale_y_continuous(breaks = seq(0, 30, 5),
                       label = scales::dollar) +
    scale_color_manual(values = c("indianred3", 
                                "cornflowerblue")) +
    facet_wrap(~sector) +
    labs(title = "Relationship between wages and experience",
       subtitle = "Current Population Survey",
       caption = "source: http://mosaic-web.org/",
       x = " Years of Experience",
       y = "Hourly Wage",
       color = "Gender", shape = "Gender", linetype = "Gender") +
  theme_bw()

图 4.10 展示了该图。

图 4.10 展示了八个职业领域的工人经验与工资的关系散点图,每个领域都有单独的图表(面元)和自定义标题和标签

现在查看者不需要猜测标签 exprwage 的含义或数据来源。

4.1.7 主题

最后,我们可以使用主题来微调图表的外观。主题函数(以 theme_ 开头)控制背景颜色、字体、网格线、图例位置和其他与数据无关的图表特征。让我们使用一个更简洁的主题。从图 4.4 开始,我们使用了一个产生白色背景和浅灰色参考线的主题。让我们尝试一个更简约的主题。以下代码生成了图 4.11:

ggplot(data = CPS85, 
       mapping = aes(x = exper, y = wage,
                 color = sex, shape=sex, linetype=sex)) +
    geom_point(alpha = .7) +
    geom_smooth(method = "lm", se = FALSE) +
    scale_x_continuous(breaks = seq(0, 60, 10)) +
    scale_y_continuous(breaks = seq(0, 30, 5),
                       label = scales::dollar) +
    scale_color_manual(values = c("indianred3", 
                                "cornflowerblue")) +
    facet_wrap(~sector) +
    labs(title = "Relationship between wages and experience",
       subtitle = "Current Population Survey",
       caption = "source: http://mosaic-web.org/",
       x = " Years of Experience",
       y = "Hourly Wage",
       color = "Gender", shape = "Gender", linetype = "Gender") +
  theme_minimal()

图 4.11 展示了工人经验与工资的关系散点图,每个八个职业领域都有单独的图表(面元),自定义标题和标签,以及更简洁的主题

这是我们完成的图表,准备发布。当然,这些发现是初步的。它们基于一个有限的样本量,并且没有进行统计测试来评估差异是否可能是由于偶然变化。第八章将描述适用于此类数据的适当测试。主题在第十九章中更详细地描述。

4.2 ggplot2 详细信息

在结束本章之前,有三个重要主题需要考虑:aes()函数的位置,将ggplot2图形作为 R 对象处理,以及将您的图形保存用于报告和网页的各种方法。

4.2.1 放置数据和映射选项

使用ggplot2创建的图始终以ggplot函数开始。在先前的示例中,data=mapping=选项放置在这个函数中。在这种情况下,它们适用于随后的每个geom函数。

您也可以直接在 geom 中放置这些选项。在这种情况下,它们只适用于该特定 geom。考虑以下图形:

ggplot(CPS85, aes(x = exper, y = wage, color = sex)) +
           geom_point(alpha = .7, size = 1.5) + 
           geom_smooth(method = "lm", se = FALSE, size = 1)  +
           scale_color_manual(values = c("lightblue", "midnightblue")) +
           theme_bw()

图 4.12 显示了结果图。

图像

图 4.12 按性别显示经验和工资的散点图,其中aes(color=sex)放置在ggplot()函数中。映射应用于geom_point()geom_smooth(),为男性和女性产生单独的点颜色,以及为所有工人产生单独的最佳拟合线。

由于性别到颜色的映射出现在ggplot()函数中,因此它适用于geom_pointgeom_smooth。点的颜色表示性别,并为男性和女性产生单独的彩色趋势线。与以下内容比较

ggplot(CPS85,   aes(x = exper, y = wage)) +            
          geom_point(aes(color = sex), alpha = .7, size = 1.5) +             
          geom_smooth(method = "lm", se = FALSE, size = 1) + 
          scale_color_manual(values = c("lightblue", "midnightblue")) +   
          theme_bw()

图 4.13 显示了结果图。

图像

图 4.13 按性别显示经验和工资的散点图,其中aes(color=sex)放置在geom_point()函数中。映射应用于点颜色,为男性和女性产生单独的点颜色,但为所有工人产生单独的最佳拟合线。

由于性别到颜色的映射只出现在geom_point()函数中,因此它只在那里使用。为所有观测值创建一条趋势线。

本书中的大多数示例都将数据和映射选项放置在ggplot函数中。此外,省略了data=mapping=短语,因为第一个选项始终指数据,第二个选项始终指映射。

4.2.2 图形作为对象

ggplot2图形可以保存为命名的 R 对象(列表),进一步操作,然后打印或保存到磁盘。考虑以下列表中的代码。

列表 4.1 使用ggplot2图形作为对象

data(CPS85 , package = "mosaicData")                      ❶
CPS85 <- CPS85[CPS85$wage < 40,]                          ❶

myplot <- ggplot(data = CPS85,                            ❷
            aes(x = exper, y = wage)) +                   ❷
       geom_point()                                       ❷

myplot                                                    ❸

myplot2 <- myplot + geom_point(size = 3, color = "blue")  ❹
myplot2                                                   ❹

myplot + geom_smooth(method = "lm") +                     ❺
  labs(title = "Mildly interesting graph")                ❺

❶ 准备数据。

❷ 创建散点图并将其保存为 myplot。

❸ 显示 myplot。

❹ 将点放大并变为蓝色,保存为 myplot2,并显示图形。

❺ 显示带有最佳拟合线和标题的 myplot。

首先,导入数据并移除异常值。然后,创建一个简单的经验与工资的散点图,并将其保存为 myplot。接下来,打印该图。然后通过改变点的大小和颜色修改该图,保存为 myplot2,并打印。最后,给原始图添加最佳拟合线和标题,并打印。请注意,这些更改不会被保存。

将图表保存为对象的能力允许您继续使用和修改它们。这可以节省大量时间(并帮助您避免腕管综合征)。在下一节中,我们将看到,当以编程方式保存图表时,这也很方便。

4.2.3 保存图表

您可以通过 RStudio 图形用户界面或通过您的代码保存由 ggplot2 创建的图表。要使用 RStudio 菜单保存图表,请转到“图表”选项卡并选择“导出”(见图 4.14)。

图 4.14 使用 RStudio 界面保存图表

可以使用 ggsave() 函数通过代码保存图表。您可以指定要保存的图表、其大小和格式以及保存位置。考虑以下示例:

ggsave(file="mygraph.png", plot=myplot, width=5, height=4)

这将 myplot 保存为当前工作目录中名为 mygraph.png 的 5" × 4" PNG 文件。您可以通过更改文件扩展名以不同的格式保存图表。表 4.4 列出了最常见的格式描述。

表 4.4 图像文件格式

扩展名 格式
pdf 可移植文档格式
jpeg JPEG
tiff 标签图像文件格式
png 可移植网络图形
svg 可缩放矢量图形
wmf Windows 元文件

PDF、SVG 和 WMF 格式是矢量格式——它们可以无模糊或像素化地缩放。其他格式是位图——它们在缩放时将像素化。这在将小图像放大时尤为明显。PNG 格式是网页图像的常用格式。JPEG 和 TIF 格式通常保留用于照片。

WMF 格式通常推荐用于将在 Microsoft Word 或 PowerPoint 文档中出现的图表。MS Office 不支持 PDF 或 SVG 文件,WMF 格式可以很好地缩放。然而,WMF 文件将丢失任何已设置的透明度设置。

如果省略了 plot= 选项,则保存最近创建的图表。以下代码是有效的,并将图表保存为磁盘上的 PDF 文档:

ggplot(data=mtcars, aes(x=mpg)) + geom_histogram()
ggsave(file="mygraph.pdf")

查看 help(ggsave) 获取更多详细信息。

4.2.4 常见错误

在使用 ggplot2 几年后,我发现经常犯两个错误。第一个是省略或放置了错误的闭括号。这种情况最常发生在 aes() 函数之后。考虑以下代码:

ggplot(CPS85, aes(x = exper, y = wage, color = sex) +
  geom_point()

注意第一行末尾缺少一个闭括号。我无法告诉你我犯了多少次这样的错误。

第二个错误是将赋值与映射混淆。以下代码生成了图 4.15 中的图表:

ggplot(CPS85, aes(x = exper, y = wage, color = "blue")) +
  geom_point() 

aes() 函数用于将 变量 映射到图表的视觉特征。分配常量值应在 aes() 函数外部完成。正确的代码应该是

ggplot(CPS85, aes(x = exper, y = wage) +
  geom_point(color = "blue")

点是红色(而不是蓝色),图例也很奇怪。发生了什么?

图 4.15 在 aes() 函数中放置赋值语句

摘要

  • ggplot2 软件包提供了一种语言和语法,用于创建全面的数据可视化。

  • 散点图描述了两个定量变量之间的关系。

  • 趋势线可以添加到散点图中以总结其关系。

  • 您可以使用颜色、形状和大小来表示观察组的集合。

  • 分面图对于绘制多个组的数据非常有用。

  • 您可以使用刻度、标签和主题自定义图表。

  • 图表可以保存为多种格式。

5 高级数据管理

本章涵盖

  • 使用数学和统计函数

  • 利用字符函数

  • 循环和条件执行

  • 编写自己的函数

  • 聚合和重塑数据

在第三章中,我们回顾了在 R 中管理数据集的基本技术。在本章中,我们将关注高级主题。本章分为三个基本部分。在第一部分,我们将快速浏览 R 中用于数学、统计和字符操作的许多函数。为了使这一部分具有相关性,我们从可以使用这些函数解决的问题的数据管理问题开始。在介绍函数本身之后,我们将查看数据管理问题的可能解决方案之一。

接下来,我们将介绍如何编写自己的函数来完成数据管理和分析任务。首先,我们将探讨控制程序流程的方法,包括循环和条件语句的执行。然后,我们将研究用户编写的函数的结构以及如何创建后调用它们。

然后,我们将探讨聚合和汇总数据的方法,以及重塑和重新结构化数据集的方法。在聚合数据时,你可以指定使用任何适当的内置或用户编写的函数来完成汇总,因此你在本章前两部分学到的主题将真正对你有所帮助。

5.1 数据管理挑战

为了开始我们关于数值和字符函数的讨论,让我们考虑一个数据管理问题。一群学生在数学、科学和英语科目中参加了考试。你想要将这些分数合并,以确定每个学生的单个表现指标。此外,你想要将 A 等级分配给前 20% 的学生,B 等级分配给下一个 20%,依此类推。最后,你想要按字母顺序排序学生。表 5.1 展示了数据。

表 5.1 学生考试成绩

学生 数学 科学 英语
约翰·戴维斯 502 95 25
安吉拉·威廉姆斯 600 99 22
比尔温克尔·麋鹿 412 80 18
大卫·琼斯 358 82 15
詹妮斯·马克哈默 495 75 20
谢丽尔·卡辛 512 85 28
雷温·伊茨拉克 410 80 15
格雷格·诺克斯 625 95 30
乔尔·英格兰 573 89 27
玛丽·雷伯恩 522 86 18

当你查看这个数据集时,几个障碍立即显现。首先,三门考试的分数不可比。它们的平均值和标准差差异很大,所以平均它们没有意义。在合并之前,你必须将考试分数转换为可比单位。其次,你需要一种方法来确定学生在这些分数上的百分位数排名,以分配等级。第三,有一个单独的姓名字段,这使排序学生的任务复杂化。你需要将他们的名字拆分为名和姓,才能正确排序。

每个这些任务都可以通过巧妙地使用 R 的数值和字符函数来完成。在下一节中描述的函数工作完毕后,我们将考虑这个数据管理挑战的可能解决方案。

5.2 数值和字符函数

在本节中,我们将回顾 R 中的函数,这些函数可以用作操作数据的基本构建块。它们可以分为数值(数学、统计、概率)和字符函数。在我回顾每种类型之后,我将向您展示如何将函数应用于矩阵和数据框的列(变量)和行(观测值)(参见 5.2.6 节)。|

5.2.1 数学函数

表 5.2 列出了常见的数学函数及其简短示例。

表 5.2 数学函数

函数 描述
abs(*X*) 绝对值abs(-4) 返回 4
sqrt(*X*) 平方根sqrt(25) 返回 5。这等同于 25^(0.5)
ceiling(*X*) 不小于 x 的最小整数ceiling(3.475) 返回 4
floor(*X*) 不大于 x 的最大整数floor(3.475) 返回 3
trunc(*X*) x 中的值截断到 0 的整数trunc(5.99) 返回 5
round(*X*, digits=*`n`*) x 四舍五入到指定的十进制位数round(3.475, digits=2) 返回 3.48
signif(*X*, digits=*`n`*) x 四舍五入到指定的有效数字位数signif(3.475, digits=2) 返回 3.5
cos(*X*), sin(*X*), tan(*X*) 余弦,正弦和正切cos(2) 返回 –0.416
acos(*X*), asin(*X*), atan(*X*) 反余弦,反正弦和反正切acos(-0.416) 返回 2
cosh(*X*), sinh(*X*), tanh(*X*) 双曲余弦,双曲正弦和双曲正切sinh(2) 返回 3.627
acosh(*X*), asinh(*X*), atanh(*X*) 双曲反余弦,双曲反正弦和双曲反正切asinh(3.627) 返回 2

| log(*X*,base=*`n`*)```log(*X*)log10(X) | Logarithm of *x* to the base *n`*For convenience:

  • log(*X*) is the natural logarithm.

  • log10(*X*) is the common logarithm.

  • log(10) returns 2.3026.

  • log10(10) returns 1.

|
| exp(*X*) | Exponential functionexp(2.3026) returns 10. |

Data transformation is one of the primary uses for these functions. For example, you often transform positively skewed variables such as income to a log scale before further analyses. Mathematical functions are also used as components in formulas, in plotting functions (for example, x versus sin(x)), and in formatting numerical values prior to printing.

The examples in table 5.2 apply mathematical functions to scalars (individual numbers). When these functions are applied to numeric vectors, matrices, or data frames, they operate on each individual value; for example: sqrt(c(4, 16, 25)) returns c(2, 4, 5).

5.2.2 Statistical functions

Table 5.3 presents common statistical functions. Many of these functions have optional parameters that affect the outcome. For example,


y <- mean(x)

provides the arithmetic mean of the elements in object x, and


z <- mean(x, trim = 0.05, na.rm=TRUE)

provides the trimmed mean, dropping the highest and lowest 5% of scores and any missing values. Use the help() function to learn more about each function and its arguments.

Table 5.3 Statistical functions

| Function | Description |
| mean(*X*) | Meanmean(c(1,2,3,4)) returns 2.5. |
| median(*X*) | Medianmedian(c(1,2,3,4)) returns 2.5. |
| sd(*X*) | Standard deviationsd(c(1,2,3,4)) returns 1.29. |
| var(*X*) | Variancevar(c(1,2,3,4)) returns 1.67. |
| mad(*X*) | Median absolute deviationmad(c(1,2,3,4)) returns 1.48. |
| quantile(*X*, probs) | Quantiles where x is the numeric vector, where quantiles are desired and probs is a numeric vector with probabilities in [0,1]# 30th and 84th percentiles of x``y <- quantile(x, c(.3,.84)) |
| range(*X*) | Rangex <- c(1,2,3,4)``range(x) returns c(1,4).diff(range(x)) returns 3. |
| sum(*X*) | Sumsum(c(1,2,3,4)) returns 10. |
| diff(*X*, lag=*n*) | Lagged differences, with lag indicating which lag to use. The default lag is 1.x<- c(1, 5, 23, 29)``diff(x) returns c(4, 18, 6). |
| min(*X*) | Minimummin(c(1,2,3,4)) returns 1. |
| max(*X*) | Maximummax(c(1,2,3,4)) returns 4. |
| scale(x, center=TRUE,``scale=TRUE) | Column center (center=TRUE) or standardize (center=TRUE, scale=TRUE) data object x. An example is given in listing 5.6. |

To see these functions in action, look at the next listing. This example demonstrates two ways to calculate the mean and standard deviation of a vector of numbers.

Listing 5.1 Calculating the mean and standard deviation


> x <- c(1,2,3,4,5,6,7,8)
> 
> mean(x)                     ❶

[1] 4.5                       ❶

> sd(x)                       ❶

[1] 2.449490                  ❶

> n <- length(x)              ❷
> 
> meanx <- sum(x)/n           ❷
> 
> css <- sum((x - meanx)²)   ❷
> 
> sdx <- sqrt(css / (n-1))    ❷
> 
> meanx                       ❷

[1] 4.5                       ❷

> sdx                         ❷

[1] 2.449490                  ❷

❶ Short way

❷ Long way

It’s instructive to view how the corrected sum of squares (css) is calculated in the second approach:

  1. x equals c(1, 2, 3, 4, 5, 6, 7, 8), and mean x equals 4.5 (length(x) returns the number of elements in x).

  2. (x meanx) subtracts 4.5 from each element of x, resulting in c(-3.5, -2.5, -1.5, -0.5, 0.5, 1.5, 2.5, 3.5)

  3. (x meanx)² squares each element of (x - meanx), resulting in

    
    c(12.25, 6.25, 2.25, 0.25, 0.25, 2.25, 6.25, 12.25)
    
    
  4. sum((x - meanx)²) sums each of the elements of (x - meanx)²), resulting in 42.

Writing formulas in R has much in common with matrix-manipulation languages such as MATLAB (we’ll look more specifically at solving matrix algebra problems in appendix D).

Standardizing data

By default, the scale() function standardizes the specified columns of a matrix or data frame to a mean of 0 and a standard deviation of 1:


newdata <- scale(mydata)

To standardize each column to an arbitrary mean and standard deviation, you can use code similar to the following:


newdata <- scale(mydata)*SD + M

where M is the desired mean and SD is the desired standard deviation. Using the scale() function on non-numeric columns produces an error. To standardize a specific column rather than an entire matrix or data frame, you can use code such as this:


newdata <- transform(mydata, myvar = scale(myvar)*10+50)

This code standardizes the variable myvar to a mean of 50 and standard deviation of 10. You’ll use the scale() function in the solution to the data management challenge in section 5.3.

5.2.3 Probability functions

You may wonder why probability functions aren’t listed with the statistical functions (that was really bothering you, wasn’t it?). Although probability functions are statistical by definition, they’re unique enough to deserve their own section. Probability functions are often used to generate simulated data with known characteristics and to calculate probability values within user-written statistical functions.

In R, probability functions take the form


[dpqr]*distribution_abbreviation*()

where the first letter refers to the aspect of the distribution returned:

d = Density

p = Distribution function

q = Quantile function

r = Random generation (random deviates)

Table 5.4 lists the common probability functions.

Table 5.4 Probability distributions

| Distribution | Abbreviation | Distribution | Abbreviation |
| Beta | beta | Logistic | logis |
| Binomial | binom | Multinomial | multinom |
| Cauchy | cauchy | Negative binomial | nbinom |
| Chi-squared (noncentral) | chisq | Normal | norm |
| Exponential | exp | Poisson | pois |
| F | f | Wilcoxon signed rank | signrank |
| Gamma | gamma | T | t |
| Geometric | geom | Uniform | unif |
| Hypergeometric | hyper | Weibull | weibull |
| Lognormal | lnorm | Wilcoxon rank sum | wilcox |

To see how these work, let’s look at functions related to the normal distribution. If you don’t specify a mean and a standard deviation, the standard normal distribution is assumed (mean=0, sd=1). Table 5.5 gives examples of the density (dnorm), distribution (pnorm), quantile (qnorm), and random deviate generation (rnorm) functions.

Table 5.5 Normal distribution functions

| Problem | Solution |
| Plot the standard normal curve on the interval [–3,3] (see figure below). |


library(ggplot2)

x <- seq(from = -3, to = 3, by = 0.1)

y = dnorm(x)

data <- data.frame(x = x, y=y)

ggplot(data, aes(x, y)) +

    geom_line()  +

    labs(x = "正态偏差",

            y = "密度") +

    scale_x_continuous(

        breaks = seq(-3, 3, 1))

|
| What is the area under the standard normal curve to the left of z=1.96? | pnorm(1.96) equals 0.975. |
| What is the value of the 90th percentile of a normal distribution with a mean of 500 and a standard deviation of 100? | qnorm(.9, mean=500, sd=100) equals 628.16. |
| Generate 50 random normal deviates with a mean of 50 and a standard deviation of 10. | rnorm(50, mean=50, sd=10) |

Setting the seed for random number generation

Each time you generate pseudo-random deviates, a different seed, and therefore different results, are produced. To make your results reproducible, you can specify the seed explicitly using the set.seed() function. An example is given in the next listing. Here, the runif() function is used to generate pseudo-random numbers from a uniform distribution on the interval 0 to 1.

Listing 5.2 Generating pseudo-random numbers from a uniform distribution


> runif(5)

[1] 0.8725344 0.3962501 0.6826534 0.3667821 0.9255909

> runif(5)

[1] 0.4273903 0.2641101 0.3550058 0.3233044 0.6584988

> set.seed(1234)
> 
> runif(5)

[1] 0.1137034 0.6222994 0.6092747 0.6233794 0.8609154

> set.seed(1234)
> 
> runif(5)

[1] 0.1137034 0.6222994 0.6092747 0.6233794 0.8609154

By setting the seed manually, you’re able to reproduce your results. This ability can be helpful in creating examples you can access in the future and share with others.

Generating multivariate normal data

In simulation research and Monte Carlo studies, you often want to draw data from a multivariate normal distribution with a given mean vector and covariance matrix. The draw.d.variate.normal() function in the MultiRNG package makes this easy. After installing and loading the package, the function call is


draw.d.variate.normal(n, nvar, mean, sigma)

where n is the desired sample size, nvar is the number of variables, mean is the vector of means, and sigma is the variance-covariance (or correlation) matrix. Listing 5.3 samples 500 observations from a three-variable multivariate normal distribution for which the following are true:

| Mean vector | 230.7 | 146.7 | 3.6 |
| Covariance matrix | 15360.8 | 6721.2 | -47.1 |
| 6721.2 | 4700.9 | -16.5 |
| -47.1 | -16.5 | 0.3 |

Listing 5.3 Generating data from a multivariate normal distribution


> install.packages("MultiRNG")
> 
> library(MultiRNG)
> 
> options(digits=3)
> 
> set.seed(1234)                                                ❶
> 
> mean <- c(230.7, 146.7, 3.6)
> 
> sigma <- matrix(c(15360.8, 6721.2, -47.1,

                    6721.2, 4700.9, -16.5,                     ❷

                    -47.1,  -16.5,   0.3), nrow=3, ncol=3)    ❷

> mydata <- draw.d.variate.normal(500, 3, mean, sigma)          ❸
> 
> mydata <- as.data.frame(mydata)                               ❸
> 
> names(mydata) <- c("y","x1","x2")                             ❸
> 
> dim(mydata)                                                   ❹

[1] 500 3                                                       ❹

> head(mydata, n=10)                                            ❹

    y    x1   x2

1   81.1 122.6 3.69

2  265.1 110.4 3.49

3  365.1 235.3 2.67

4  -60.0  14.9 4.72

5  283.9 244.8 3.88

6  293.4 163.9 2.66

7  159.5  51.5 4.03

8  163.0 137.7 3.77

9  160.7 131.0 3.59

10 120.4  97.7 4.11

❶ Sets the random number seed

❷ Specifies the mean vector and covariance matrix

❸ Generates data

❹ Views the results

In listing 5.3, you set a random number seed so you can reproduce the results at a later time. You specify the desired mean vector and variance-covariance matrix and generate 500 pseudo-random observations. For convenience, the results are converted from a matrix to a data frame, and the variables are given names. Finally, you confirm that you have 500 observations and 3 variables, and you print out the first 10 observations. Note that because a correlation matrix is also a covariance matrix, you could have specified the correlation structure directly.

The MultiRNG package allows you to generate random data from 10 other multivariate distributions, including multivariate versions of the T, uniform, Bernoulli, hypergeometric, beta, multinomial, Laplace, and Wishart distributions.

The probability functions in R allow you to generate simulated data, sampled from distributions with known characteristics. Statistical methods that rely on simulated data have grown exponentially in recent years, and you’ll see several examples of these in later chapters.

5.2.4 Character functions

Whereas mathematical and statistical functions operate on numerical data, character functions extract information from textual data or reformat textual data for printing and reporting. For example, you may want to concatenate a person’s first name and last name, ensuring that the first letter of each is capitalized. Or you may want to count the instances of obscenities in open-ended feedback. Table 5.6 lists some of the most useful character functions.

Table 5.6 Character functions

| Function | Description |
| nchar(*X*) | Counts the number of characters of x.x <- c("ab", "cde", "fghij")``length(x) returns 3 (see table 5.7).nchar(x[3]) returns 5. |
| substr(*X*, *start*, *stop*) | Extracts or replaces substrings in a character vector.x <- "abcdef"``substr(x, 2, 4) returns bcd.substr(x, 2, 4) <- "22222" (x is now "a222ef"). |
| grep(*pattern*, *X*, ignore.case=FALSE, fixed=FALSE) | Searches for pattern in X. If fixed=FALSE, then pattern is a regular expression. If fixed=TRUE, then pattern is a text string. Returns the matching indices.grep("A", c("b","A","ac", "Aw"), fixed=TRUE) returns c(2, 4). |
| sub(*pattern*, *replacement*, *X*, ignore.case=FALSE, fixed=FALSE) | Finds pattern in x and substitutes the replacement text. If fixed=FALSE, then pattern is a regular expression. If fixed=TRUE, then pattern is a text string.sub("\\s",".","Hello There") returns Hello.There. Note that "\s" is a regular expression for finding whitespace; use "\\s" instead, because "\" is R’s escape character (see section 1.3.4). |
| strsplit(*X*, *split,* fixed=FALSE) | Splits the elements of character vector x at split. If fixed=FALSE, then pattern is a regular expression. If fixed=TRUE, then pattern is a text string.y <- strsplit("abc", "") returns a one-component, three-element list containing"a" "b" "c"``unlist(y)[2] and sapply(y, "[", 2) both return "b". |
| paste(..., sep="") | Concatenates strings after using the sep string to separate them.paste("x", 1:3, sep="") returns c("x1", "x2", "x3").paste("x",1:3,sep="M") returns c("xM1","xM2" "xM3").paste("Today is", date()) returnsToday is Thu Jul 22 10:36:14 2021 |
| toupper(*X*) | Uppercase.toupper("abc") returns "ABC". |
| tolower(*X*) | Lowercase.tolower("ABC") returns "abc". |

Note that the functions grep(), sub(), and strsplit() can search for a text string (fixed=TRUE) or a regular expression (fixed=FALSE); FALSE is the default. Regular expressions provide a clear and concise syntax for matching a pattern of text. For example, the regular expression


^[hc]?at

matches any string that starts with zero or one occurrences of h or c, followed by at. The expression therefore matches hat, cat, and at, but not bat. To learn more, see the regular expression entry in Wikipedia. Helpful tutorials include Ryans Regular Expression Tutorial (ryanstutorials.net/regular-expressions-tutorial/) and an engaging interactive tutorial from RegexOne (regexone.com).

5.2.5 Other useful functions

The functions in table 5.7 are also quite useful for data management and manipulation, but they don’t fit cleanly into the other categories.

Table 5.7 Other useful functions

| Function | Description |
| length(*X*) | Returns the length of object x.x <- c(2, 5, 6, 9)``length(x) returns 4. |
| seq(*from*, to, by) | Generates a sequence.indices <- seq(1,10,2)``indices is c(1, 3, 5, 7, 9). |
| rep(*X*, n) | Repeats x n times.y <- rep(1:3, 2)``y is c(1, 2, 3, 1, 2, 3). |
| cut(*X*, n) | Divides the continuous variable x into a factor with n levels. To create an ordered factor, include the option ordered_result = TRUE. |
| cat(... , file = "myfile", append = FALSE) | Concatenates the objects in ... and outputs them to the screen or to a file (if one is declared).name <- c("Jane")``cat("Hello" , name, "\n") |

The last example in the table demonstrates the use of escape characters in printing. Use \n for new lines, \t for tabs, \' for a single quote, \b for backspace, and so forth (type ?Quotes for more information). For example, the code


name <- "Bob"

cat( "Hello", name, "\b.\n", "Isn\'t R", "\t", "GREAT?\n")

produces


Hello Bob.

Isn't R        GREAT?

Note that the second line is indented one space. When cat concatenates objects for output, it separates each by a space. That’s why you include the backspace (\b) escape character before the period. Otherwise, it would produce "Hello Bob".

How you apply the functions covered so far to numbers, strings, and vectors is intuitive and straightforward, but how do you apply them to matrices and data frames? That’s the subject of the next section.

5.2.6 Applying functions to matrices and data frames

One of the interesting features of R functions is that they can be applied to a variety of data objects (scalars, vectors, matrices, arrays, and data frames). The following listing provides an example.

Listing 5.4 Applying functions to data objects


> a <- 5
> 
> sqrt(a)

[1] 2.236068

> b <- c(1.243, 5.654, 2.99)
> 
> round(b)

[1] 1 6 3

> c <- matrix(runif(12), nrow=3)
> 
> c

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

[1,] 0.4205 0.355 0.699 0.323

[2,] 0.0270 0.601 0.181 0.926

[3,] 0.6682 0.319 0.599 0.215

> log(c)

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

[1,] -0.866 -1.036 -0.358 -1.130

[2,] -3.614 -0.508 -1.711 -0.077

[3,] -0.403 -1.144 -0.513 -1.538

> mean(c)

[1] 0.444

Notice that the mean of matrix c in listing 5.4 results in a scalar (0.444). The mean() function takes the average of all 12 elements in the matrix. But what if you want the three row means or the four column means?

R provides a function, apply(), that allows you to apply an arbitrary function to any dimension of a matrix, array, or data frame. The format for the apply() function is


apply(x, MARGIN, FUN, ...)

where x is the data object, MARGIN is the dimension index, FUN is a function you specify, and ... are any parameters you want to pass to FUN. In a matrix or data frame, MARGIN=1 indicates rows and MARGIN=2 indicates columns. Look at the following examples.

Listing 5.5 Applying a function to the rows (columns) of a matrix


> mydata <- matrix(rnorm(30), nrow=6)              ❶

> mydata

        [,1]   [,2]    [,3]   [,4]   [,5]

[1,]  0.71298  1.368 -0.8320 -1.234 -0.790

[2,] -0.15096 -1.149 -1.0001 -0.725  0.506

[3,] -1.77770  0.519 -0.6675  0.721 -1.350

[4,] -0.00132 -0.308  0.9117 -1.391  1.558

[5,] -0.00543  0.378 -0.0906 -1.485 -0.350

[6,] -0.52178 -0.539 -1.7347  2.050  1.569

> apply(mydata, 1, mean)                           ❷

[1] -0.155 -0.504 -0.511  0.154 -0.310  0.165

> apply(mydata, 2, mean)                           ❸

[1] -0.2907  0.0449 -0.5688 -0.3442  0.1906

> apply(mydata, 2, mean, trim=0.2)                 ❹

[1] -0.1699  0.0127 -0.6475 -0.6575  0.2312

❶ Generates data

❷ Calculates the row means

❸ Calculates the column means

❹ Calculates the trimmed column means

You start by generating a 6 × 5 matrix containing random normal variates ❶. Then you calculate the six row means ❷ and five column means ❸. Finally, you calculate the trimmed column means (in this case, means based on the middle 60% of the data, with the bottom 20% and top 20% of the values discarded) ❹.

Because FUN can be any R function, including a function that you write yourself (see section 5.4), apply() is a powerful mechanism. Whereas apply() applies a function over the margins of an array, lapply() and sapply() apply a function over a list. You’ll see an example of sapply() (which is a user-friendly version of lapply()) in the next section.

You now have all the tools you need to solve the data challenge presented in section 5.1, so let’s give it a try.

5.2.7 A solution for the data management challenge

Your challenge from section 5.1 is to combine subject test scores into a single performance indicator for each student, grade each student from A to F based on their relative standing (top 20%, next 20%, and so on), and sort the roster by last name followed by first name. The following listing gives a solution.

Listing 5.6 A solution to the learning example


> options(digits=2)                                                 ❶
> 
> Student <- c("John Davis", "Angela Williams", "Bullwinkle Moose",

            "David Jones", "Janice Markhammer", "Cheryl Cushing",

            "Reuven Ytzrhak", "Greg Knox", "Joel England",

            "Mary Rayburn")

> Math <- c(502, 600, 412, 358, 495, 512, 410, 625, 573, 522)
> 
> Science <- c(95, 99, 80, 82, 75, 85, 80, 95, 89, 86)
> 
> English <- c(25, 22, 18, 15, 20, 28, 15, 30, 27, 18)
> 
> roster <- data.frame(Student, Math, Science, English,

                    stringsAsFactors=FALSE)

> z <- scale(roster[,2:4])                                          ❷❹
> 
> score <- apply(z, 1, mean)                                        ❸❹
> 
> roster <- cbind(roster, score)                                    ❸❹
> 
> y <- quantile(score, c(.8,.6,.4,.2))                              ❺❼
> 
> roster$grade <- NA                                                ❻❼
> 
> roster$grade[score >= y[1]] <- "A"                                ❻❼
> 
> roster$grade[score < y[1] & score >= y[2]] <- "B"                 ❻❼
> 
> roster$grade[score < y[2] & score >= y[3]] <- "C"                 ❻❼
> 
> roster$grade[score < y[3] & score >= y[4]] <- "D"                 ❻❼
> 
> roster$grade[score < y[4]] <- "F"                                 ❻❼
> 
> name <- strsplit((roster$Student), " ")                           ❽❿
> 
> Lastname <- sapply(name, "[", 2)                                  ❾❿
> 
> Firstname <- sapply(name, "[", 1)                                 ❾❿
> 
> roster <- cbind(Firstname,Lastname, roster[,-1])                  ❾❿
> 
> roster <- roster[order(Lastname,Firstname),]                      ⓫⓬
> 
> roster

    Firstname   Lastname 数学 科学 英语 分数 等级

6      Cheryl    Cushing  512      85      28  0.35     C

1        John      Davis  502      95      25  0.56     B

9        Joel    England  573      89      27  0.70     B

4       David      Jones  358      82      15 -1.16     F

8        Greg       Knox  625      95      30  1.34     A

5      Janice Markhammer  495      75      20 -0.63     D

3  Bullwinkle      Moose  412      80      18 -0.86     D

10       Mary    Rayburn  522      86      18 -0.18     C

2      Angela   Williams  600      99      22  0.92     A

7      Reuven    Ytzrhak  410      80      15 -1.05     F

❶ Step 1

❷ Step 2

❸ Step 3

❹ Obtains the performance scores

❺ Step 4

❻ Step 5

❼ Grades the students

❽ Step 6

❾ Step 7

❿ Extracts the last and first names

⓫ Step 8

⓬ Sorts by last and first names

The code is dense, so let’s walk through the solution step by step.

  1. The original student roster is given. options(digits=2) limits the number of digits printed after the decimal place and makes the printouts easier to read:

    
    > 选项(数字=2)
    > 
    > roster
    
                学生 数学 科学
    
    1         John Davis  502      95      25
    
    2    Angela Williams  600      99      22
    
    3   Bullwinkle Moose  412      80      18
    
    4        David Jones  358      82      15
    
    5  Janice Markhammer  495      75      20
    
    6     Cheryl Cushing  512      85      28
    
    7     Reuven Ytzrhak  410      80      15
    
    8          Greg Knox  625      95      30
    
    9       Joel England  573      89      27
    
    10      Mary Rayburn  522      86      18
    
    
  2. Because the math, science, and English tests are reported on different scales (with widely differing means and standard deviations), you need to make them comparable before combining them. One way to do this is to standardize the variables so that each test is reported in standard deviation units rather than in their original scales. You can do this with the scale() function:

    
    > z <- scale(roster[,2:4])
    > 
    > z
    
            数学 科学 英语
    
    [1,]  0.013   1.078    0.587
    
    [2,]  1.143   1.591    0.037
    
    [3,] -1.026  -0.847   -0.697
    
    [4,] -1.649  -0.590   -1.247
    
    [5,] -0.068  -1.489   -0.330
    
    [6,]  0.128  -0.205    1.137
    
    [7,] -1.049  -0.847   -1.247
    
    [8,]  1.432   1.078    1.504
    
    [9,]  0.832   0.308    0.954
    
    [10,]  0.243  -0.077   -0.697
    
    
  3. You can then get a performance score for each student by calculating the row means using the mean() function and adding them to the roster using the cbind() function:

    
    > score <- apply(z, 1, mean)
    > 
    > roster <- cbind(roster, score)
    > 
    > roster
    
                学生 数学 科学 英语 分数
    
    1         John Davis  502      95      25  0.56
    
    2    Angela Williams  600      99      22  0.92
    
    3   Bullwinkle Moose  412      80      18 -0.86
    
    4        David Jones  358      82      15 -1.16
    
    5  Janice Markhammer  495      75      20 -0.63
    
    6     Cheryl Cushing  512      85      28  0.35
    
    7     Reuven Ytzrhak  410      80      15
    
    8          Greg Knox  625      95      30  1.34
    
    9       Joel England  573      89      27  0.70
    
    10      Mary Rayburn  522      86      18 -0.18
    
    
  4. The quantile() function gives you the percentile rank of each student’s performance score. You see that the cutoff for an A is 0.74, for a B is 0.44, and so on:

    
    > y <- quantile(roster$score, c(.8,.6,.4,.2))
    > 
    > y
    
    80%   60%   40%   20%
    
    0.74  0.44 -0.36 -0.89
    
    
  5. Using logical operators, you can recode students’ percentile ranks into a new categorical grade variable. This code creates the variable grade in the roster data frame:

    
    > roster$grade <- NA
    > 
    > roster$grade[score >= y[1]] <- "A"
    > 
    > roster$grade[score < y[1] & score >= y[2]] <- "B"
    > 
    > 罗列$等级[score < y[2] & score >= y[3]] <- "C"
    > 
    > 罗列$等级[score < y[3] & score >= y[4]] <- "D"
    > 
    > 罗列$等级[score < y[4]] <- "F"
    > 
    > 罗列
    
                学生 数学 科学 英语 分数 等级
    
    1         John Davis  502      95      25  0.56     B
    
    2    Angela Williams  600      99      22  0.92     A
    
    3   Bullwinkle Moose  412      80      18 -0.86     D
    
    4        David Jones  358      82      15 -1.16     F
    
    5  Janice Markhammer  495      75      20 -0.63     D
    
    6     Cheryl Cushing  512      85      28  0.35     C
    
    7     Reuven Ytzrhak  410      80      15 -1.05     F
    
    8          Greg Knox  625      95      30  1.34     A
    
    9       Joel England  573      89      27  0.70     B
    
    10      Mary Rayburn  522      86      18 -0.18     C
    
    
  6. You use the strsplit() function to break the student names into first name and last name at the space character. Applying strsplit() to a vector of strings returns a list:

    
    > name <- strsplit((roster$Student), " ")
    > 
    > name
    
    [[1]]
    
    [1] "John"  "Davis"
    
    [[2]]
    
    [1] "Angela"   "Williams"
    
    [[3]]
    
    [1] "Bullwinkle" "Moose"
    
    [[4]]
    
    [1] "David" "Jones"
    
    [[5]]
    
    [1] "Janice"     "Markhammer"
    
    [[6]]
    
    [1] "Cheryl"  "Cushing"
    
    [[7]]
    
    [1] "Reuven"  "Ytzrhak"
    
    [[8]]
    
    [1] "Greg" "Knox"
    
    [[9]]
    
    [1] "Joel"    "England"
    
    [[10]]
    
    [1] "Mary"    "Rayburn"
    
    
  7. You use the sapply() function to take the first element of each component and put it in a Firstname vector, and the second element of each component and put it in a Lastname vector. "[" is a function that extracts part of an object—here the first or second component of the list name. You use cbind() to add these elements to the roster. Because you no longer need the student variable, you drop it (with the –1 in the roster index):

    
    > 名字 <- sapply(name, "[", 1)
    > 
    > 姓氏 <- sapply(name, "[", 2)
    > 
    > 罗列 <- cbind(名字, 姓氏, 罗列[,-1])
    > 
    > 罗列
    
        名字   姓氏 数学 科学 英语 分数 等级
    
    1        John      Davis  502      95      25  0.56     B
    
    2      Angela   Williams  600      99      22  0.92     A
    
    3  Bullwinkle      Moose  412      80      18 -0.86     D
    
    4       David      Jones  358      82      15 -1.16     F
    
    5      Janice Markhammer  495      75      20 -0.63     D
    
    6      Cheryl    Cushing  512      85      28  0.35     C
    
    7      Reuven    Ytzrhak  410      80      15 -1.05     F
    
    8        Greg       Knox  625      95      30  1.34     A
    
    9        Joel    England  573      89      27  0.70     B
    
    10       Mary    Rayburn  522      86      18 -0.18     C
    
    
  8. Finally, you sort the dataset by first and last name using the order() function:

    
    > 罗列[order(姓氏,名字),]
    
        名字   姓氏 数学 科学 英语 分数 等级
    
    6      Cheryl    Cushing  512      85      28  0.35     C
    
    1        John      Davis  502      95      25  0.56     B
    
    9        Joel    England  573      89      27  0.70     B
    
    4       David      Jones  358      82      15 -1.16     F
    
    8        Greg       Knox  625      95      30  1.34     A
    
    5      Janice Markhammer  495      75      20 -0.63     D
    
    3  Bullwinkle      Moose  412      80      18 -0.86     D
    
    10       Mary    Rayburn  522      86      18 -0.18     C
    
    2      Angela   Williams  600      99      22  0.92     A
    
    7      Reuven    Ytzrhak  410      80      15 -1.05     F
    
    

Voilà! Piece of cake!

There are many other ways to accomplish these tasks, but this code helps capture the flavor of these functions. Now it’s time to look at control structures and user-written functions.

5.3 Control flow

In the normal course of events, the statements in an R program are executed sequentially from the top of the program to the bottom. But there are times that you’ll want to execute some statements repetitively while executing other statements only if certain conditions are met. This is where control-flow constructs come in.

R has the standard control structures you’d expect to see in a modern programming language. First, we’ll go through the constructs used for conditional execution, followed by the constructs used for looping.

For the syntax examples throughout this section, keep the following in mind:

  • statement is a single R statement or a compound statement (a group of R statements enclosed in curly braces {} and separated by semicolons).

  • cond is an expression that resolves to TRUE or FALSE.

  • expr is a statement that evaluates to a number or character string.

  • seq is a sequence of numbers or character strings.

After we discuss control-flow constructs, you’ll learn how to write your own functions.

5.3.1 Repetition and looping

Looping constructs repetitively execute a statement or series of statements until a condition isn’t true. These include the for and while structures.

for

The for loop executes a statement repetitively for each value in the vector seq. The syntax is


for (var in seq) 语句

In this example,


for (i in 1:10)  print("Hello")

the word Hello is printed 10 times.

while

A while loop executes a statement repetitively until the condition is no longer true. The syntax is


while (cond) statement

In a second example, the code


i <- 10

while (i > 0) {print("Hello"); i <- i - 1}

once again prints the word Hello 10 times. Make sure the statements inside the brackets modify the while condition so that sooner or later, it’s no longer true—otherwise, the loop will never end! In the previous example, the statement


i <- i – 1

subtracts 1 from object i on each loop, so that after the tenth loop, it’s no longer larger than 0. If you instead added 1 on each loop, R would never stop saying Hello. This is why while loops can be more dangerous than other looping constructs.

Looping in R can be inefficient and time consuming when you’re processing the rows or columns of large datasets. Whenever possible, it’s better to use R’s built-in numerical and character functions in conjunction with the apply family of functions.

5.3.2 Conditional execution

In conditional execution, a statement or statements are executed only if a specified condition is met. These constructs include if-else, ifelse, and switch.

if-else

The if-else control structure executes a statement if a given condition is true. Optionally, a different statement is executed if the condition is false. The syntax is


if (cond) statement

if (cond) statement1 else statement2

Here are some examples:


if (is.character(等级)) 等级 <- as.factor(等级)

if (!is.factor(等级)) 等级 <- as.factor(等级) else print(“Grade already

    is a factor”)

In the first instance, if grade is a character vector, it’s converted into a factor. In the second instance, one of two statements is executed. If grade isn’t a factor (note the ! symbol), it’s turned into one. If it’s a factor, then the message is printed.

ifelse

The ifelse construct is a compact and vectorized version of the if-else construct. The syntax is


ifelse(cond, statement1, statement2)

The first statement is executed if cond is TRUE. If cond is FALSE, the second statement is executed. Here are some examples:


ifelse(score > 0.5, print("Passed"), print("Failed"))

outcome <- ifelse (score > 0.5, "Passed", "Failed")

Use ifelse when you want to take a binary action or when you want to input and output vectors from the construct.

switch

switch chooses statements based on the value of an expression. The syntax is


switch(expr, ...)

where ... represents statements tied to the possible outcome values of expr. It’s easiest to understand how switch works by looking at the example in the following listing.

Listing 5.7 A switch example


> feelings <- c("sad", "afraid")
> 
> for (i in feelings)

    print(

    switch(i,

        happy  = "I am glad you are happy",

        afraid = "There is nothing to fear",

        sad    = "Cheer up",

        angry  = "Calm down now"

    )

    )

[1] "Cheer up"

[1] "There is nothing to fear"

This is a silly example, but it shows the main features. You’ll learn how to use switch in user-written functions in the next section.

5.4 User-written functions

One of R’s greatest strengths is the user’s ability to add functions. In fact, many of R’s functions are functions of existing functions. The structure of a function looks like this:


myfunction <- function(arg1, arg2, ... ){

statements

return(object)

}

Objects in the function are local to the function. The object returned can be any data type, from scalar to list. Let’s look at an example.

Say you’d like to have a function that calculates the central tendency and spread of data objects. The function should give you a choice between parametric (mean and standard deviation) and nonparametric (median and median absolute deviation) statistics. The results should be returned as a named list. Additionally, the user should have the choice of automatically printing the results or not. Unless otherwise specified, the function’s default behavior should be to calculate parametric statistics and not print the results. One solution is given in the following listing.

Listing 5.8 mystats(): a user-written function for summary statistics


mystats <- function(x, parametric=TRUE, print=FALSE) {

if (parametric) {

    center <- mean(x); spread <- sd(x)

} else {

    center <- median(x); spread <- mad(x)

}

if (print & parametric) {

    cat("Mean=", center, "\n", "SD=", spread, "\n")

} else if (print & !parametric) {

    cat("Median=", center, "\n", "MAD=", spread, "\n")

}

result <- list(center=center, spread=spread)

return(result)

}

To see this function in action, first generate some data (a random sample of size 500 from a normal distribution):


set.seed(1234)

x <- rnorm(500)

After executing the statement


y <- mystats(x)

y$center contains the mean (0.00184), and y$spread contains the standard deviation (1.03). No output is produced. If you execute the statement


y <- mystats(x, parametric=FALSE, print=TRUE)

y$center contains the median (–0.0207), and y$spread contains the median absolute deviation (1.001). In addition, the following output is produced:


Median= -0.0207

MAD= 1

Next, let’s look at a user-written function that uses the switch construct. This function gives the user a choice regarding the format of today’s date. Values that are assigned to parameters in the function declaration are taken as defaults. In the mydate() function, long is the default format for dates if type isn’t specified:


mydate <- function(type="long") {

switch(type,

    long =  format(Sys.time(), "%A %B %d %Y"),

    short = format(Sys.time(), "%m-%d-%y"),

    cat(type, "is not a recognized type\n")

)

}

Here’s the function in action:


> mydate("long")

[1] "Saturday July 24 2021"

> mydate("short")

[1] "07-24-21"

> mydate()

[1] "Saturday July 24 2021"

> mydate("medium")

medium is not a recognized type

Note that the cat() function is executed only if the entered type doesn’t match "long" or "short". It’s usually a good idea to have an expression that catches user-supplied arguments that have been entered incorrectly.

Several functions can help add error trapping and correction to your functions. You can use the function warning``() to generate a warning message, message() to generate a diagnostic message, and stop() to stop execution of the current expression and carry out an error action. I will discuss error trapping and debugging more fully in section 20.6.

After creating your own functions, you may want to make them available in every session. Appendix B describes how to customize the R environment so that user-written functions are loaded automatically at startup. We’ll look at additional examples of user-written functions in later chapters.

You can accomplish a great deal using the basic techniques provided in this section. Control flow and other programming topics are covered in greater detail in chapter 20. Creating a package is covered in chapter 22. If you’d like to explore the subtleties of function writing, or you want to write professional-level code that you can distribute to others, I recommend reading these two chapters and then reviewing three excellent books listed in the References section: Venables and Ripley (2000), Chambers (2008), and Wickham(2019). Together, they provide a significant level of detail and breadth of examples.

Now that we’ve covered user-written functions, we’ll end this chapter with a discussion of data aggregation and reshaping.

5.5 Reshaping data

When you reshape data, you alter the structure (rows and columns) determining how the data is organized. The three most common reshaping tasks are (1) transposing a dataset; (2) converting a wide dataset to a long dataset; and (3) converting a long dataset to a wide dataset. Each is described in the following sections.

5.5.1 Transposing

Transposing (reversing rows and columns) is perhaps the simplest method of reshaping a dataset. Use the t() function to transpose a matrix or a data frame. In the latter case, the data frame is converted to a matrix first, and row names become variable (column) names.

We’ll illustrate transposing using the mtcars data frame that’s included with the base installation of R. This dataset, extracted from Motor Trend magazine (1974), describes the design and performance characteristics (number of cylinders, displacement, horsepower, mpg, and so on) for 34 automobiles. To learn more about the dataset, see help(mtcars).

The following listing shows an example of the transpose operation. A subset of the dataset in used to conserve space on the page.

Listing 5.9 Transposing a dataset


> cars <- mtcars[1:5,1:4]
> 
> cars

                mpg cyl disp  hp

Mazda RX4         21.0   6  160 110

Mazda RX4 Wag     21.0   6  160 110

Datsun 710        22.8   4  108  93

Hornet 4 Drive    21.4   6  258 110

Hornet Sportabout 18.7   8  360 175

> t(cars)

    Mazda RX4 Mazda RX4 Wag Datsun 710 Hornet 4 Drive Hornet Sportabout

mpg         21            21       22.8           21.4              18.7

cyl          6             6        4.0            6.0               8.0

disp       160           160      108.0          258.0             360.0

hp         110           110       93.0          110.0             175.0

The t() function always returns a matrix. Since a matrix can only have one type (numeric, character, or logical), the transpose operation works best when all the variables in the original dataset are numeric or logical. If there are any character variables in the dataset, the entire dataset will be converted to character values in the resulting transpose.

5.5.2 Converting from wide to long dataset formats

A rectangular dataset is typically in either wide or long format. In wide format, each row represents a unique observation. Table 5.8 shows an example. The table contains the life expectancy estimates for four countries in 1990, 2000, and 2010. It is part of a much larger dataset obtained from Our World in Data (ourworldindata.org/life-expectancy). Note that each row represents the data gathered on a country.

Table 5.8 Life expectancy by year and country—wide format

| ID | Country | LExp1990 | LExp2000 | LExp2010 |
| AU | Australia | 76.9 | 79.6 | 82.0 |
| CN | China | 69.3 | 72.0 | 75.2 |
| PRK | North Korea | 69.9 | 65.3 | 69.6 |

In long format, each row represents a unique measurement. Table 5.9 shows an example with the same data in long format.

Table 5.9 Life expectancy by year and country—long format

| ID | Country | Variable | LifeExp |
| AU | Australia | LExp1990 | 76.9 |
| CN | China | LExp1990 | 69.3 |
| PRK | North Korea | LExp1990 | 69.9 |
| AU | Australia | LExp2000 | 79.6 |
| CN | China | LExp2000 | 72.0 |
| PRK | North Korea | LExp2000 | 65.3 |
| AU | Australia | LExp2010 | 82.0 |
| CN | China | LExp2010 | 75.2 |
| PRK | North Korea | LExp2010 | 69.6 |

Different types of data analysis can require different data formats. For example, if you want to identify countries that have similar life expectancy trends over time, you could use cluster analysis (chapter 16). Cluster analysis requires data that is in wide format. On the other hand, you may want to predict life expectancy from country and year using multiple regression (chapter 8). In this case, the data would have to be in long format.

While most R functions expect wide format data frames, some require the data to be in a long format. Fortunately, the tidyr package provides functions that can easily convert data frames from one format to the other. Use install.packages("tidyr") to install the package before continuing.

The gather() function in the tidyr package converts a wide format data frame to a long format data frame. The syntax is


longdata <- gather(widedata, key, value, variable list)

where

  • widedata is the data frame to be converted.

  • key specifies the name to be used for the variable column (Variable in this example).

  • value specifies the name to be used for the value column (LifeExp in this example).

  • variable list specifies the variables to be stacked (LExp1990, LExp2000, LExp2010 in this example).

The following listing shows an example.

Listing 5.10 Converting a wide format data frame to a long format


> library(tidyr)
> 
> data_wide <- data.frame(ID = c("AU", "CN", "PRK"),

                        Country = c("Australia", "China", "North Korea"),

                        LExp1990 = c(76.9, 69.3, 69.9),

                        LExp2000 = c(79.6, 72.0, 65.3),

                        LExp2010 = c(82.0, 75.2, 69.6))

> data_wide

ID     Country LExp1990 LExp2000 LExp2010

1  AU   Australia     76.9     79.6     82.0

2  CN       China     69.3     72.0     75.2

3 PRK North Korea     69.9     65.3     69.6

> data_long <- gather(data_wide, key="Variable", value="Life_Exp",

                    c(LExp1990, LExp2000, LExp2010))

> data_long

ID     Country Variable Life_Exp

1  AU   Australia LExp1990     76.9

2  CN       China LExp1990     69.3

3 PRK North Korea LExp1990     69.9

4  AU   Australia LExp2000     79.6

5  CN       China LExp2000     72.0

6 PRK North Korea LExp2000     65.3

7  AU   Australia LExp2010     82.0

8  CN       China LExp2010     75.2

9 PRK North Korea LExp2010     69.6

The spread() function in the tidyr package converts a long format data frame to a wide format data frame. The format is


widedata <- spread(longdata, key, value)

where

  • longdata is the data frame to be converted.

  • key is the column containing the variable names.

  • value is the column containing the variable values.

Continuing the example, the code in the following listing is used to convert the long format data frame back to a wide format.

Listing 5.11 Converting a long format data frame to a wide format


> data_wide <- spread(data_long, key=Variable, value=Life_Exp)
> 
> data_wide

ID     Country Variable Life_Exp

1  AU   Australia     76.9     79.6     82.0

2  CN       China     69.3     72.0     75.2

3 PRK North Korea     69.9     65.3     69.6

To learn more about the long and wide data formats, see Simon Ejdemyr’s excellent tutorial (sejdemyr.github.io/r-tutorials/basics/wide-and-long/).

5.6 Aggregating data

When you aggregate data, you replace groups of observations with summary statistics based on those observations. Data aggregation can be a precursor to statistical analyses or a method of summarizing data for presentation in tables or graphs.

It’s relatively easy to collapse data in R using one or more by variables and a defined function. In base R, the aggregate() is typically used. The format is


aggregate(x, by, FUN)

where x is the data object to be collapsed, by is a list of variables that will be crossed to form the new observations, and FUN is a function used to calculate the summary statistics that will make up the new observation values. The by variables must be enclosed in a list (even if there’s only one).

As an example, let’s aggregate the mtcars data by number of cylinders and gears, returning means for each of the numeric variables.

Listing 5.12 Aggregating data with the aggregate() function


> options(digits=3)
> 
> aggdata <-aggregate(mtcars,

                    通过以下方式分组:by=list(mtcars$cyl,mtcars$gear),

                    FUN=mean, na.rm=TRUE)

> aggdata

Group.1 Group.2  mpg cyl disp  hp drat   wt qsec  vs   am gear carb

1       4       3 21.5   4  120  97 3.70 2.46 20.0 1.0 0.00    3 1.00

2       6       3 19.8   6  242 108 2.92 3.34 19.8 1.0 0.00    3 1.00

3       8       3 15.1   8  358 194 3.12 4.10 17.1 0.0 0.00    3 3.08

4       4       4 26.9   4  103  76 4.11 2.38 19.6 1.0 0.75    4 1.50

5       6       4 19.8   6  164 116 3.91 3.09 17.7 0.5 0.50    4 4.00

6       4       5 28.2   4  108 102 4.10 1.83 16.8 0.5 1.00    5 2.00

7       6       5 19.7   6  145 175 3.62 2.77 15.5 0.0 1.00    5 6.00

8       8       5 15.4   8  326 300 3.88 3.37 14.6 0.0 1.00    5 6.00

In these results, Group.1 represents the number of cylinders (4, 6, or 8), and Group.2 represents the number of gears (3, 4, or 5). For example, cars with 4 cylinders and 3 gears have a mean of 21.5 miles per gallon (mpg). Here we used the mean function, but any function in R or any user-defined function that computes summary statistics can be used.

There are two limitations to this code. First, Group.1 and Group.2 are terribly uninformative variable names. Second, the original cyl and gear variables are included in the aggregated data frame. These columns are now redundant.

You can declare custom names for the grouping variables from within the list. For instance, by=list(Cylinders=cyl, Gears=gear will replace Group.1 and Group.2 with Cylinders and Gears. The redundant columns can be dropped from the input data frame using bracket notation (mtcars[-c(2, 10]). The following listing shows an improved version.

Listing 5.13 Improved code for aggregating data with aggregate()


> aggdata <-aggregate(mtcars[-c(2, 10)],

            通过以下方式分组:by=list(Cylinders=mtcars$cyl, Gears=mtcars$gear),

            FUN=mean, na.rm=TRUE)

> aggdata

气缸数 汽轮齿数 英里/加仑 排量 马力 比例传动比 重量 四分之一英里加速时间 燃油喷射方式 发动机类型 匹配器

1         4     3 21.5  120  97 3.70 2.46 20.0 1.0 0.00 1.00

2         6     3 19.8  242 108 2.92 3.34 19.8 1.0 0.00 1.00

3         8     3 15.1  358 194 3.12 4.10 17.1 0.0 0.00 3.08

4         4     4 26.9  103  76 4.11 2.38 19.6 1.0 0.75 1.50

5         6     4 19.8  164 116 3.91 3.09 17.7 0.5 0.50 4.00

6         4     5 28.2  108 102 4.10 1.83 16.8 0.5 1.00 2.00

7         6     5 19.7  145 175 3.62 2.77 15.5 0.0 1.00 6.00

8         8     5 15.4  326 300 3.88 3.37 14.6 0.0 1.00 6.00

The dplyr package provides a more natural method of aggregating data. Consider the code in the next listing.

Listing 5.14 Aggregating data with the dplyr package


> mtcars %>%

    group_by(cyl, gear) %>%

    summarise_all(list(mean), na.rm=TRUE)

# 一个 tibble:8 x 11

# 分组:   cyl [3]

    cyl  gear   mpg  disp    hp  drat    wt  qsec    vs    am  carb

<dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>

1     4     3  21.5  120\.   97   3.7   2.46  20.0   1    0     1

2     4     4  26.9  103\.   76   4.11  2.38  19.6   1    0.75  1.5

3     4     5  28.2  108\.  102   4.1   1.83  16.8   0.5  1     2

4     6     3  19.8  242\.  108\.  2.92  3.34  19.8   1    0     1

5     6     4  19.8  164\.  116\.  3.91  3.09  17.7   0.5  0.5   4

6     6     5  19.7  145   175   3.62  2.77  15.5   0    1     6

7     8     3  15.0  358\.  194\.  3.12  4.10  17.1   0    0     3.08

8     8     5  15.4  326   300\.  3.88  3.37  14.6   0    1     6

分组变量保留其名称,并且在数据中不重复。我们将在第七章讨论汇总统计时,进一步探讨dplyr强大的汇总功能。

现在你已经收集了你需要的工具来整理你的数据(没有开玩笑),你准备好告别第一部分,进入令人兴奋的数据分析世界!在接下来的章节中,我们将开始探索将数据转化为信息的许多统计和图形方法。

摘要

  • 基础 R 包含数百个数学、统计和概率函数,这些函数对于操作数据非常有用。它们可以应用于各种数据对象,包括向量、矩阵和数据框。

  • 条件执行和循环的函数允许你重复执行某些语句,并在满足特定条件时执行其他语句。

  • 你可以轻松编写自己的函数,极大地增强你程序的功能。

  • 在进行进一步分析之前,数据通常需要聚合和/或重构。

第二部分:基本方法

在第一部分,我们探讨了 R 环境,并讨论了如何从各种来源输入数据,将其合并和转换,以及为后续分析做准备。一旦你的数据已经输入并清理完毕,下一步通常是逐个探索变量。这为你提供了关于每个变量分布的信息,这对于理解样本的特征、识别意外或问题值以及选择合适的统计方法很有用。接下来,通常一次研究两个变量。这可以帮助你发现变量间的基本关系,并在开发更复杂的模型时是一个有用的第一步。

第二部分专注于获取数据基本信息的图形和统计技术。第六章描述了可视化单个变量分布的方法。对于分类变量,这包括条形图、饼图和较新的树状图。对于数值变量,这包括直方图、密度图、箱线图、点图以及不太知名的提琴图。每种类型的图表都有助于理解单个变量的分布。

第七章描述了用于总结单个变量和双变量关系的统计方法。本章首先介绍了基于整个数据集和感兴趣子组的数值数据的描述性统计。接下来,描述了使用频率表和交叉表来总结分类数据的方法。本章最后讨论了理解两个变量之间关系的基本推断方法,包括双变量相关、卡方检验、t 检验和非参数方法。

当你完成这本书的这一部分时,你将能够使用 R 中可用的基本图形和统计方法来描述你的数据,探索组间差异,并识别变量之间的显著关系。

6 基本图表

本章涵盖

  • 使用条形图、箱线图和点图绘制数据

  • 创建饼图和树状图

  • 使用直方图和核密度图

每当我们分析数据时,我们首先应该做的是查看它。对于每个变量,最常见的值是什么?存在多少可变性?是否有任何异常观测值?R 提供了丰富的数据可视化函数。在本章中,我们将探讨有助于您理解单个分类或连续变量的图表。这个主题包括

  • 可视化变量的分布

  • 比较两个或多个组中变量的分布

在这两种情况下,变量可以是连续的(例如,汽车里程作为每加仑英里数)或分类的(例如,治疗结果作为无、一些或明显)。在后面的章节中,我们将探讨显示变量之间更复杂关系的图表。

以下章节探讨了条形图、饼图、树状图、直方图、核密度图、箱线图、小提琴图和点图的用法。其中一些可能您已经熟悉,而其他一些(如树状图或小提琴图)可能对您来说是新的。目标,一如既往,是更好地理解您的数据,并将这种理解传达给他人。让我们从条形图开始。

6.1 条形图

条形图通过垂直或水平条形显示分类变量的分布(频率)。使用ggplot2包,我们可以使用以下代码创建条形图

ggplot(*data*, aes(*x=catvar*) + geom_bar()

其中data是一个数据框,catvar是一个分类变量。

在以下示例中,您将绘制一项研究新治疗类风湿性关节炎的结果。数据包含在vcd包中分发的Arthritis数据框中。此包不包括在默认 R 安装中,因此在使用之前请安装它(install.packages("vcd"))。请注意,vcd包不是创建条形图所必需的。您安装它是为了访问Arthritis数据集。

6.1.1 简单条形图

在关节炎研究中,变量Improved记录了接受安慰剂或药物的患者结果:

> data(Arthritis, package="vcd")
> table(Arthritis$Improved)

  None   Some Marked 
    42     14     28

在这里,您可以看到 28 名患者显示出明显的改善,14 名患者显示出一些改善,而 42 名患者没有改善。我们将在第七章中更全面地讨论使用table()函数来获取单元格计数。

您可以使用垂直或水平条形图来绘制这些计数。以下列表显示了代码,图 6.1 显示了生成的图形。

列表 6.1 简单条形图

library(ggplot2)
ggplot(Arthritis, aes(x=Improved)) + geom_bar() +      ❶
  labs(title="Simple Bar chart",                       ❶
       x="Improvement",                                ❶
       y="Frequency")                                  ❶

ggplot(Arthritis, aes(x=Improved)) + geom_bar() +      ❷
  labs(title="Horizontal Bar chart",                   ❷
       x="Improvement",                                ❷
       y="Frequency") +                                ❷
  coord_flip()                                         ❷

❶ 简单条形图

❷ 水平条形图

图 6.1 简单的垂直和水平条形图

如果您有很长的标签会发生什么?在第 6.1.4 节中,您将看到如何调整标签以避免重叠。

6.1.2 堆叠、分组和填充条形图

关节炎研究中的核心问题是,“安慰剂组和治疗组的改善水平之间是如何变化的?”table()函数可以用来生成变量的交叉表:

> table(Arthritis$Improved, Arthritis$Treatment)

        Treatment
Improved Placebo Treated
  None        29      13
  Some         7       7
  Marked       7      21

虽然交叉表很有帮助,但条形图使结果更容易理解。两个分类变量之间的关系可以用堆叠分组填充条形图来绘制。接下来的列表提供了代码,图 6.2 显示了图表。

列表 6.2  堆叠、分组和填充条形图

library(ggplot2)
ggplot(Arthritis, aes(x=Treatment, fill=Improved)) +    ❶
  geom_bar(position = "stack") +                        ❶
  labs(title="Stacked Bar chart",                       ❶
       x="Treatment",                                   ❶
       y="Frequency")                                   ❶

ggplot(Arthritis, aes(x=Treatment, fill=Improved)) +    ❷
  geom_bar(position = "dodge") +                        ❷
  labs(title="Grouped Bar chart",                       ❷
       x="Treatment",                                   ❷
       y="Frequency")                                   ❷

ggplot(Arthritis, aes(x=Treatment, fill=Improved)) +    ❸
  geom_bar(position = "fill") +                         ❸
  labs(title="Filled Bar chart",                        ❸
       x="Treatment",                                   ❸
       y="Proportion")                                  ❸

❶ 堆叠条形图

❷ 分组条形图

❸ 填充条形图

在堆叠条形图中,每个部分代表给定治疗(安慰剂、治疗)和改善(无、一些、显著)水平组合内案例的频率或比例。每个治疗水平分别堆叠这些部分。分组条形图在每个治疗水平内将代表改善的部分并排放置。填充条形图是一个重新缩放的堆叠条形图,使得每个条形的高度为 1,部分高度代表比例。

填充条形图特别适用于比较一个分类变量的比例在另一个分类变量的水平上的变化。例如,图 6.2 中的填充条形图清楚地显示了与接受安慰剂的病人相比,有显著改善的治疗病人的比例更高。

图 6.2 堆叠、分组和填充条形图

6.1.3 平均条形图

条形图不必基于计数或频率。你可以通过使用适当的统计量总结数据,并将结果传递给ggplot2来创建表示平均值、中位数、百分比、标准差等条形图。

在以下图表中,我们将绘制 1970 年美国各地区的平均文盲率。内置的 R 数据集state.x77包含了各州的文盲率,数据集state.region包含了各州的地区名称。以下列表提供了创建图 6.3 中图表所需的代码。

列表 6.3  按排序平均值绘制的条形图

> states <- data.frame(state.region, state.x77)    
> library(dplyr)                                   
> plotdata <- states %>% 
    group_by(state.region) %>%                                    ❶
    summarize(mean = mean(Illiteracy))                            ❶
  plotdata                                                        ❶

# A tibble: 4 x 2
  state.region   mean
  <fct>         <dbl>
1 Northeast      1   
2 South          1.74
3 North Central  0.7 
4 West           1.02

> ggplot(plotdata, aes(x=reorder(state.region, mean), y=mean)) +  ❷
    geom_bar(stat="identity") +                                   ❷
    labs(x="Region",                                              ❷
         y="",                                                    ❷
         title = "Mean Illiteracy Rate")                          ❷

❶ 按地区生成平均值

❷ 在排序条形图中绘制平均值

图 6.3 按文盲率排序的美国地区平均文盲率条形图

首先,计算每个地区的平均文盲率 ❶。接下来,按升序排序平均值,并以条形的形式绘制 ❷。通常,geom_bar()函数计算并绘制单元格计数,但添加stat="identity"选项会强制函数绘制提供的数字(在这种情况下是平均值)。使用reorder()函数按递增的平均文盲率对条形进行排序。

当绘制均值等汇总统计量时,指出涉及估计的变异性是一种良好的做法。变异性的一种度量是统计量的标准误差——对统计量在假设重复样本中预期变化的估计。以下图形(图 6.4)使用均值的标准误差添加了误差线。

列表 6.4  带有误差线的均值条形图

> plotdata <- states %>%                                           ❶
    group_by(state.region) %>%                                     ❶
    summarize(n=n(),                                               ❶
              mean = mean(Illiteracy),                             ❶
              se = sd(Illiteracy)/sqrt(n))                         ❶

> plotdata

# A tibble: 4 x 4
  state.region      n  mean     se
  <fct>         <int> <dbl>  <dbl>
1 Northeast         9  1    0.0928
2 South            16  1.74 0.138 
3 North Central    12  0.7  0.0408
4 West             13  1.02 0.169 

> ggplot(plotdata, aes(x=reorder(state.region, mean), y=mean)) +   ❷
    geom_bar(stat="identity", fill="skyblue") +
    geom_errorbar(aes(ymin=mean-se, ymax=mean+se), width=0.2) +    ❸
    labs(x="Region",
         y="",
         title = "Mean Illiteracy Rate",
         subtitle = "with standard error bars")

❶ 按地区生成均值和标准误差

❷ 在排序条形图中绘制均值

❸ 添加误差线

为每个地区计算了均值和标准误差 ❶。然后按文盲率升序绘制条形。颜色从默认的深灰色变为较浅的色调(天蓝色),以便在下一步添加的误差线更加突出 ❷。最后,绘制误差线 ❸。geom_errorbar()函数中的width选项控制误差线的水平宽度,纯粹是美学上的——它没有统计意义。除了显示平均文盲率外,我们还可以看到,中北部地区的均值最可靠(变异性最小),而西部地区的均值最不可靠(变异性最大)。

图 6.4 按比率排序的美国地区平均文盲率条形图。每个条形都添加了均值的标准误差。

6.1.4 调整条形图

有几种方法可以调整条形图的外观。最常见的是自定义条形颜色和标签。我们将逐一查看。

条形图颜色

可以为条形图的区域和边框选择自定义颜色。在geom_bar()函数中,选项fill="color"为区域分配颜色,而color="color"为边框分配颜色。

填充与颜色

通常,ggplot2使用填充来指定具有面积(如条形、饼图切片、箱形)的几何对象的颜色,当提到没有面积(如线条、点和边框)的几何对象的颜色时使用颜色。

例如,以下代码

data(Arthritis, package="vcd")
ggplot(Arthritis, aes(x=Improved)) + 
   geom_bar(fill="gold", color="black") +
   labs(title="Treatment Outcome")

生成图 6.5 中的图形。

图 6.5 带有自定义填充和边框颜色的条形图

在上一个示例中,分配了单色。颜色也可以映射到分类变量的级别。例如,以下代码

ggplot(Arthritis, aes(x=Treatment, fill=Improved)) +    
  geom_bar(position = "stack", color="black") +  
  scale_fill_manual(values=c("red", "grey", "gold")) +                      
  labs(title="Stacked Bar chart",                       
       x="Treatment",                                   
       y="Frequency")                                   

生成图 6.6 中的图形。

图 6.6 按改进映射的自定义填充颜色的堆叠条形图

在这里,条形填充颜色映射到变量Improved的级别。scale_fill_manual()函数指定None为红色,Some为灰色,Marked improvement 为金色。颜色名称可以从www.stat.columbia.edu/~tzheng/files/Rcolor.pdf获取。第十九章讨论了选择颜色的其他方法。

条形图标签

当有多个条形或长标签时,条形图标签往往会重叠并变得难以阅读。考虑以下示例。ggplot2包中的mpg数据集描述了 1999 年和 2008 年 38 种流行汽车模型的燃油经济数据。每个模型都有几种配置(传动类型、气缸数量等)。假设我们想要统计数据集中每种模型的实例数量。以下代码

ggplot(mpg, aes(x=model)) + 
   geom_bar() +
   labs(title="Car models in the mpg dataset", 
        y="Frequency", x="")

生成图 6.7。

图片

图 6.7 带重叠标签的条形图

即使戴上眼镜(或一杯酒),我也无法阅读这个。两个简单的调整可以使标签可读。首先,我们可以将数据作为水平条形图绘制(图 6.8):

ggplot(mpg, aes(x=model)) + 
   geom_bar() +
   labs(title="Car models in the mpg dataset", 
        y="Frequency", x="") +
   coord_flip()

图片

图 6.8 水平条形图避免标签重叠。

第二,我们可以倾斜标签文本并使用较小的字体(图 6.9):

ggplot(mpg, aes(x=model)) + 
   geom_bar() +
   labs(title="Model names in the mpg dataset", 
        y="Frequency", x="") +
   theme(axis.text.x = element_text(angle = 45, hjust = 1, size=8))

图片

图 6.9 倾斜标签文本和较小标签字体的条形图

第十九章更详细地讨论了theme()函数。除了条形图外,饼图也是显示分类变量分布的流行工具。我们将在下一节考虑它们。

6.2 饼图

饼图在商业世界中无处不在,但大多数统计学家,包括 R 文档的作者,都对其不屑一顾。他们推荐使用条形图或点图而不是饼图,因为人们能够更准确地判断长度而不是体积。也许正因为如此,R 中的饼图选项与其他统计平台相比严重受限。

然而,有时饼图是有用的。特别是,它们可以很好地捕捉部分与整体的关系。例如,饼图可以用来显示大学中终身教授中女性的百分比。

您可以使用pie()函数在基础 R 中创建饼图,但正如我所说的,功能有限,且图形不吸引人。为了解决这个问题,我创建了一个名为ggpie的包,允许您使用ggplot2创建各种饼图(请勿发送愤怒邮件!)。您可以使用以下代码从我的 GitHub 仓库安装它:

if(!require(remotes)) install.packages("remotes")
remotes::install_github("rkabacoff/ggpie")

基本语法是

ggpie(data, x, by, offset, percent, legend, title)

其中

  • data是数据框。

  • x是要绘制的分类变量。

  • by是可选的第二分类变量。如果存在,将为该变量的每个级别生成一个饼图。

  • offset表示饼图标签与原点的距离。值为0.5时,标签将放置在切片的中心,而值大于1.0时,标签将放置在切片外部。

  • percent是逻辑值。如果为FALSE,则抑制百分比打印。

  • legend是逻辑值。如果为FALSE,则省略图例,每个饼图切片将进行标注。

  • title是可选的标题。

其他选项(在ggpie网站上描述)允许您自定义饼图的外观。让我们创建一个显示mpg数据集中汽车类别分布的饼图:

library(ggplot2)
library(ggpie)
ggpie(mpg, class)

图 6.10 显示了结果。从图中我们可以看到,26%的汽车是 SUV,而只有 2%是双座车。

图 6.10 显示mpg数据帧中每个汽车类别的百分比的饼图

在下一个版本(图 6.11)中,移除了图例,并为每个饼图切片添加了标签。此外,标签放置在饼图区域外,并添加了一个标题:

ggpie(mpg, class, legend=FALSE, offset=1.3, 
         title="Automobiles by Car Class")

图 6.11 饼图标签显示在饼图外

在最后的例子(图 6.12)中,按年份显示了汽车类别的分布。

ggpie(mpg, class, year, 
      legend=FALSE, offset=1.3, title="Car Class by Year")

图 6.12 按年份显示汽车类别分布的饼图

在 1999 年至 2008 年之间,汽车类别的分布似乎保持相当稳定。ggpie包可以创建更复杂和定制的饼图。有关详细信息,请参阅文档(rkabacoff.github.io/ggpie)。

6.3 树状图

饼图的一个替代方案是树状图,它使用与变量级别成比例的矩形来显示分类变量的分布。我们将使用treemapify包创建树状图。在继续之前,请确保安装它(install.packages("treemapify"))。

我们将首先创建一个显示mpg数据帧中汽车制造商分布的树状图。以下列表显示了代码,图 6.13 显示了生成的图表。

列表 6.5  简单的树状图

library(ggplot2)
library(dplyr)
library(treemapify)

plotdata <- mpg %>% count(manufacturer)     ❶

ggplot(plotdata,                            ❷
       aes(fill = manufacturer,             ❷
           area = n,                        ❷
           label = manufacturer)) +         ❷
geom_treemap() +                            ❷
geom_treemap_text() +                       ❷
theme(legend.position = "none")             ❷

❶ 总结数据

❷ 创建树状图

首先,我们计算manufacturer变量每个级别的频率计数❶。这些信息传递给ggplot2以创建图表❷。在aes()函数中,fill指的是分类变量,area是每个级别的计数,而label是用于标记单元格的选项变量。geom_treemap()函数创建树状图,geom_treemap_text()函数为每个单元格添加标签。theme()函数用于抑制图例,在这里它是多余的,因为每个单元格都有标签。

图 6.13 显示mpg数据集中汽车制造商分布的树状图。矩形大小与每个制造商的汽车数量成比例。

如您所见,树状图可以用来可视化具有许多级别的分类变量(与饼图不同)。在下一个例子中,添加了第二个变量——drivetrain。绘制了前轮驱动、后轮驱动和四轮驱动的制造商的汽车数量。下一个列表提供了代码,图 6.14 显示了该图。

列表 6.6  具有子分组的树状图

library(ggplot2)
library(dplyr)
library(treemapify)
plotdata <- mpg %>%                                                    ❶
  count(manufacturer, drv)                                             ❶
  plotdata$drv <- factor(plotdata$drv,                                 ❷
                       levels=c("4", "f", "r"),                        ❷
                       labels=c("4-wheel", "front-wheel", "rear"))     ❷

ggplot(plotdata,                                                       ❸
       aes(fill = manufacturer,                                        ❸
           area = n,                                                   ❸
           label = manufacturer,                                       ❸
           subgroup=drv)) +                                            ❸
  geom_treemap() +                                                     ❸
  geom_treemap_subgroup_border() +                                     ❸
  geom_treemap_subgroup_text(                                          ❸
    place = "middle",                                                  ❸
    colour = "black",                                                  ❸
    alpha = 0.5,                                                       ❸
    grow = FALSE) +                                                    ❸
  geom_treemap_text(colour = "white",                                  ❸
                    place = "centre",                                  ❸
                    grow=FALSE) +                                      ❸
  theme(legend.position = "none")                                      ❸

❶ 计算单元格计数

❷ 提供了更好的传动系统标签

❸ 创建树状图

首先,计算每个制造商-驱动方式组合的频率 ❶。接下来,为drivetrain变量提供更好的标签 ❷。将新的数据框传递给ggplot2以生成树状图 ❸。aes()函数中的子组选项为每种drivetrain类型创建单独的子图。geom_treemap_border()geom_treemap_subgroup_text()分别添加子组的边框和标签。每个函数中的选项控制其外观。子组文本居中,并赋予一定的透明度(alpha=0.5)。文本字体保持恒定大小,而不是填充区域(grow=FALSE)。树状图单元格文本以白色字体打印,在每个单元格中居中,并且不会填充盒子。

从图 6.14 的图形中可以看出,例如,现代有前轮驱动汽车,但没有后轮驱动或四轮驱动汽车。主要拥有后轮驱动汽车的生产商是福特和雪佛兰。许多四轮驱动汽车由道奇制造。

图 6.14 按驱动方式分类的汽车制造商树状图

现在我们已经介绍了饼图和树状图,让我们继续讨论直方图。与条形图、饼图和树状图不同,直方图描述了连续变量的分布。

6.4 直方图

直方图通过将分数范围分为指定数量的箱,并在 x 轴上显示每个箱的频率,来显示连续变量的分布。您可以使用以下方式创建直方图:

ggplot(data, aes(x = *contvar*)) + geom_histogram()

其中data是一个数据框,contvar是一个连续变量。使用ggplot包中的mpg数据集,我们将检查 2008 年 117 种汽车配置的城市每加仑英里数(cty)的分布。以下列表创建了直方图的四个变体,图 6.15 显示了结果图形。

列表 6.7  直方图

library(ggplot2)
library(scales)

data(mpg)
cars2008 <- mpg[mpg$year == 2008, ]

ggplot(cars2008, aes(x=cty)) +                                  ❶
   geom_histogram() +                                           ❶
   labs(title="Default histogram")                              ❶

ggplot(cars2008, aes(x=hwy)) +                                  ❷
   geom_histogram(bins=20, color="white", fill="steelblue") +   ❷
   labs(title="Colored histogram with 20 bins",                 ❷
       x="City Miles Per Gallon",                               ❷
       y="Frequency")

ggplot(cars2008, aes(x=hwy, y=..density..)) +                   ❸
   geom_histogram(bins=20, color="white", fill="steelblue") +   ❸
   scale_y_continuous(labels=scales::percent) +                 ❸
  labs(title="Histogram with percentages",                      ❸
       y= "Percent".                                            ❸
       x="City Miles Per Gallon")                               ❸

ggplot(cars2008, aes(x=cty, y=..density..)) +                   ❹
   geom_histogram(bins=20, color="white", fill="steelblue") +   ❹
   scale_y_continuous(labels=scales::percent) +                 ❹
   geom_density(color="red", size=1) +                          ❹
   labs(title="Histogram with density curve",                   ❹
        y="Percent" ,                                           ❹
       x="Highway Miles Per Gallon")                            ❹

❶ 简单直方图

❷ 带有 20 个箱的彩色直方图

❸ 带有百分比的直方图

❹ 带有密度曲线的直方图

图 6.15 直方图示例

第一个直方图 ❶ 展示了未指定任何选项时的默认绘图。在这种情况下,创建了 30 个箱。对于第二个直方图 ❷,指定了 20 个箱、深蓝色填充和白色边框颜色。此外,还添加了更详细标签。箱的数量可以显著影响直方图的外观。尝试调整bins值,直到找到一个能够很好地捕捉分布的值是个好主意。使用 20 个箱时,分布似乎有两个峰值——一个在 13 mpg 左右,另一个在 20.5 mpg 左右。

第三个直方图 ❸ 以百分比而不是频率绘制数据。这是通过将内置变量 ..density.. 分配给 y 轴来实现的。使用 scales 包格式化 y 轴为百分比。在运行此代码部分之前,请确保安装该包(install.packages("scales"))。

第四个直方图 ❹ 与之前的图表相似,但增加了密度曲线。密度曲线是核密度估计,将在下一节中描述。它提供了对分数分布的更平滑的描述。使用 geom_density() 函数以红色和略大于默认线条厚度的宽度绘制核曲线。密度曲线还暗示了双峰分布(两个峰值)。

6.5 核密度图

在上一节中,您看到了一个叠加在直方图上的核密度图。从技术上讲,核密度估计是估计随机变量概率密度函数的非参数方法。基本上,我们试图绘制一个平滑的直方图,其中曲线下的面积等于 1。尽管数学超出了本文的范围,但密度图可以是一种有效查看连续变量分布的方法。密度图的格式为

ggplot(data, aes(x = *contvar*)) + geom_density()

其中 data 是一个数据框,contvar 是一个连续变量。再次,让我们绘制 2008 年汽车的城市每加仑英里数(cty)的分布。下一列表提供了三个核密度示例,图 6.16 显示了结果。

列表 6.8 核密度图

library(ggplot2)
data(mpg)
cars2008 <- mpg[mpg$year == 2008, ]

ggplot(cars2008, aes(x=cty)) +                   ❶
   geom_density() +                              ❶
   labs(title="Default kernel density plot")     ❶

ggplot(cars2008, aes(x=cty)) +                   ❷
   geom_density(fill="red") +                    ❷
   labs(title="Filled kernel density plot",      ❷
        x="City Miles Per Gallon)                ❷

> bw.nrd0(cars2008$cty)                          ❸
1.408                                            ❸

ggplot(cars2008, aes(x=cty)) +                   ❹
   geom_density(fill="red", bw=.5) +             ❹
   labs(title="Kernel density plot with bw=0.5", ❹
        x="City Miles Per Gallon")               ❹

❶ 默认密度图

❷ 填充密度图

❸ 打印默认带宽

❹ 使用较小带宽的密度图

首先给出默认核密度图 ❶。在第二个示例中,曲线下的面积用红色填充。曲线的平滑度由带宽参数控制,该参数从正在绘制的数据中计算得出 ❷。代码 bw.nrd0(cars2008$cty) 显示此值(1.408)❸。使用较大的带宽将给出更平滑的曲线,但细节较少。较小的值将给出更锯齿状的曲线。第三个示例使用较小的带宽(bw=.5),使我们能够看到更多细节 ❹。与直方图的 bins 参数一样,尝试几个带宽值以查看哪个值可以帮助您最有效地可视化数据是个好主意。

核密度图可以用于比较组。这是一个高度未被充分利用的方法,可能是因为普遍缺乏易于访问的软件。幸运的是,ggplot2 包很好地填补了这一空白。

图 6.16 核密度图

对于这个例子,我们将比较四缸、六缸和八缸汽车的 2008 年城市油耗估计。只有少数几辆汽车有五缸,因此我们将它们从分析中排除。下一列表展示了代码。图 6.17 和 6.18 显示了生成的图表。

列表 6.9 比较核密度图

data(mpg, package="ggplot2")                                          ❶
cars2008 <- mpg[mpg$year == 2008 & mpg$cyl != 5,]                     ❶
cars2008$Cylinders <- factor(cars2008$cyl)                            ❶

ggplot(cars2008, aes(x=cty, color=Cylinders, linetype=Cylinders)) +   ❷
  geom_density(size=1)  +                                             ❷
  labs(title="Fuel Efficiecy by Number of Cylinders",                 ❷
       x = "City Miles per Gallon")                                   ❷

ggplot(cars2008, aes(x=cty, fill=Cylinders)) + 
  geom_density(alpha=.4) +                                            ❸
  labs(title="Fuel Efficiecy by Number of Cylinders",                 ❸
       x = "City Miles per Gallon")                                   ❸

❶ 准备数据

❷ 绘制密度曲线

❸ 绘制填充密度曲线

首先,加载数据的副本,并保留四、六或八缸汽车 2008 年的数据 ❶。气缸数(cyl)被保存为分类因子(Cylinders)。这种转换是必要的,因为ggplot2期望分组变量是分类的(而cyl被存储为连续变量)。

Cylinders变量的每个级别绘制一个核密度曲线 ❷。颜色(红色、绿色、蓝色)和线型(实线、点线、虚线)都映射到气缸数。最后,用填充曲线生成相同的图表 ❸。添加了透明度(alpha=0.4),因为填充曲线重叠,我们希望能够看到每一个。

图 6.17 按气缸数划分的城市 mpg 值的核密度曲线

图 6.18 城市 mpg 值的填充核密度曲线,按气缸数划分

以灰度打印

默认情况下,ggplot2选择的颜色在以灰度打印时可能难以区分。这是本书硬拷贝版本中图 6.18 的情况。当需要灰度图表时,您可以在代码中添加scale_fill_grey()scale_color_grey()函数。这将产生一种在黑白打印时效果良好的颜色方案。或者,您可以使用sp包中的bpy.colors()函数选择颜色。这将选择一种蓝色-粉色-黄色颜色方案,这种颜色方案在彩色和黑白打印机上打印效果都很好。然而,您必须喜欢蓝色、粉色和黄色!

重叠的核密度图可以是一种强大的方法,用于比较结果变量上的观测组。在这里,您可以同时看到分布的形状和组之间的重叠程度。(故事的寓意是,我的下一辆车将有四个气缸——或者一个电池。)

箱线图也是可视化分布和组间差异的一种奇妙(且更常用的)图形方法。我们将在下一节讨论它们。

6.6 箱线图

箱线图通过绘制其五个数值摘要来描述连续变量的分布:最小值、下四分位数(25 百分位数)、中位数(50 百分位数)、上四分位数(75 百分位数)和最大值。它还可以显示可能为异常值(值在± 1.5 × QR 范围之外,其中 IQR 是四分位距,定义为上四分位数减去下四分位数)的观测值。例如,以下代码生成了图 6.19 所示的图表:

ggplot(mtcars, aes(x="", y=mpg)) +
  geom_boxplot() +
  labs(y = "Miles Per Gallon", x="", title="Box Plot")

在图 6.19 中,我手动添加了注释来展示组成部分。默认情况下,每根胡须延伸到最极端的数据点,这不超过箱子的四分位距的 1.5 倍。超出此范围的值被描绘为点。

图 6.19 添加了手写注释的箱线图

例如,在这个汽车样本中,中位数的 mpg 是 17,50%的分数在 14 到 19 之间,最小值是 9,最大值是 35。我是如何从图中如此精确地读取这些信息的?运行boxplot.stats(mtcars$mpg)会打印出构建图形所使用的统计数据(换句话说,我作弊了)。有四个异常值(大于上四分位数 26)。在正态分布中,这些值预期发生的概率不到 1%。

6.6.1 使用平行箱线图比较组

箱线图是用于比较定量变量在分类变量水平上的分布的有用方法。再次,让我们比较四缸、六缸和八缸汽车的城市油耗,但这次我们将使用 1999 年和 2008 年的数据。由于只有少数五缸汽车,我们将删除它们。我们还将把yearcyl从连续的数值变量转换为分类(分组)因素:

library(ggplot2)
cars <- mpg[mpg$cyl != 5, ]
cars$Cylinders <- factor(cars$cyl)
cars$Year <- factor(cars$year)

代码

ggplot(cars, aes(x=Cylinders, y=cty)) + 
  geom_boxplot() +
  labs(x="Number of Cylinders", 
       y="Miles Per Gallon", 
       title="Car Mileage Data")

生成图 6.20 所示的图形。你可以看到,基于油耗,组间有很好的分离,随着汽缸数的增加,燃油效率下降。四缸组中也有四个异常值(油耗异常高的汽车)。

图片

图 6.20 汽车油耗与汽缸数的箱线图

箱线图非常灵活。通过添加notch=TRUE,你可以得到带凹槽的箱线图。如果两个箱子的凹槽不重叠,那么有强有力的证据表明它们的均值不同(Chambers et al., 1983, p. 62)。以下代码为油耗示例创建了带凹槽的箱线图:

ggplot(cars, aes(x=Cylinders, y=cty)) + 
  geom_boxplot(notch=TRUE, 
               fill="steelblue",
               varwidth=TRUE) +
  labs(x="Number of Cylinders", 
       y="Miles Per Gallon", 
       title="Car Mileage Data")

fill选项用深色填充箱线图。在标准箱线图中,箱宽没有意义。添加varwidth=TRUE会根据每个组中观测值的平方根绘制箱宽。

你可以在图 6.21 中看到,四缸、六缸和八缸汽车的汽车中位数的油耗不同。油耗显然随着汽缸数的增加而减少。此外,八缸汽车的数量比四缸或六缸汽车少(尽管差异微妙)。

最后,你可以为多个分组因素生成箱线图。以下代码提供了城市每加仑英里数与汽缸数按年份的箱线图(见图 6.21)。已添加scale_fill_manual()函数来自定义填充颜色:

ggplot(cars, aes(x=Cylinders, y=cty, fill=Year)) +           
  geom_boxplot() +                                           
  labs(x="Number of Cylinders",                              
       y="Miles Per Gallon",                                 
       title="City Mileage by # Cylinders and Year") +    
  scale_fill_manual(values=c("gold", "green"))      

图片

图 6.21 汽车油耗与汽缸数的带凹槽箱线图

如图 6.22 所示,再次清楚地表明,中位数的油耗随着汽缸数的增加而减少。此外,对于每个组,1999 年至 2008 年间油耗都有所增加。

图片

图 6.22 汽车油耗与年份和汽缸数的箱线图

6.6.2 小提琴图

在我们结束对箱线图的讨论之前,值得考察一种称为 小提琴图 的变体。小提琴图是箱线图和核密度图的组合。你可以使用 geom_violin() 函数创建一个。在下面的列表中,我们将向图 6.23 中的箱线图添加小提琴图。

列表 6.10  小提琴图

library(ggplot2)
cars <- mpg[mpg$cyl != 5, ]
cars$Cylinders <- factor(cars$cyl)

ggplot(cars, aes(x=Cylinders, y=cty)) + 
  geom_boxplot(width=0.2, 
              fill="green") +
  geom_violin(fill="gold", 
              alpha=0.3) +
  labs(x="Number of Cylinders", 
       y="City Miles Per Gallon", 
       title="Violin Plots of Miles Per Gallon")

箱线图的宽度设置为 0.2,以便它们可以适合在小提琴图中。小提琴图设置为图 6.23 的透明度级别为 0.3,以便箱线图仍然可见。

图片

图 6.23 mpg 与汽缸数的对比小提琴图

小提琴图基本上是在箱线图上以镜像方式叠加的核密度图。中间的线是中位数,黑色方框从下四分位数到上四分位数,细黑线代表触须。点代表异常值。外部形状提供了核密度图。在这里我们可以看到,八缸车的油耗分布可能是双峰的——这是一个仅使用箱线图就无法揭示的事实。小提琴图尚未真正流行起来。再次强调,这可能是由于缺乏易于访问的软件;时间会证明一切。

我们将以查看点图结束本章。与之前看到的图表不同,点图绘制了变量的每个值。

6.7 点图

点图提供了一种在简单水平尺度上绘制大量标记值的方法。你可以使用 dotchart() 函数创建它们,格式如下

ggplot(*data*, aes(x=*contvar*, y=c*atvar*)) + geom_point()

data 是一个数据框,contvar 是一个连续变量,而 catvar 是一个分类变量。以下是一个使用 mpg 数据集中 2008 年汽车的高速油耗的例子。高速油耗按车型平均:

library(ggplot2)
library(dplyr)
plotdata <- mpg %>%
  filter(year == "2008") %>%
  group_by(model) %>%
  summarize(meanHwy=mean(hwy))

> plotdata

# A tibble: 38 x 2
   model              meanHwy
   *<chr>*                *<dbl>*
 1 4runner 4wd           18.5
 2 a4                    29.3
 3 a4 quattro            26.2
 4 a6 quattro            24  
 5 altima                29  
 6 c1500 suburban 2wd    18  
 7 camry                 30  
 8 camry solara          29.7
 9 caravan 2wd           22.2
10 civic                 33.8
# ... with 28 more rows

ggplot(plotdata, aes(x=meanHwy, y=model)) + 
  geom_point() +
  labs(x="Miles Per Gallon", 
       y="", 
       title="Gas Mileage for Car Models")

结果图显示在图 6.24 中。

此图允许你看到每个车型在相同水平轴上的 mpg。点图通常在排序后最有用。以下代码按最低到最高里程对汽车进行排序:

ggplot(plotdata, aes(x=meanHwy, y=reorder(model, meanHwy))) + 
  geom_point() +
  labs(x="Miles Per Gallon", 
       y="", 
       title="Gas Mileage for Car Models")

结果图见图 6.25。要按降序绘图,请使用 reorder (model, -meanHwy)

你可以从这个例子中的点图中获得显著的洞察力,因为每个点都有标签,每个点的值本身就有意义,而且点以促进比较的方式排列。但随着数据点的数量增加,点图的效用会降低。

图片

图 6.24 每个车型 mpg 的点图

图片

图 6.25 按里程排序的车型 mpg 的点图

摘要

  • 条形图(以及在一定程度上饼图和树状图)可以用来了解分类变量的分布。

  • 堆叠、分组和填充条形图可以帮助你了解组在分类结果上的差异。

  • 直方图、箱线图、小提琴图和点图可以帮助你可视化连续变量的分布。

  • 重叠的核密度图和平行箱线图可以帮助你可视化连续结果变量上的组间差异。

7 基本统计

本章涵盖了

  • 描述性统计

  • 频率和列联表

  • 相关性和协方差

  • t 检验

  • 非参数统计

在前面的章节中,你学习了如何将数据导入 R,并使用各种函数来组织和转换数据,使其成为有用的格式。然后我们回顾了基本的数据可视化方法。

一旦你的数据得到适当的组织,并且你已经开始从视觉上探索它,下一步通常是数值上描述每个变量的分布,然后是探索所选变量之间的关系,一次两个。目标是回答如下问题:

  • 这些天汽车的平均油耗是多少?具体来说,在汽车品牌和型号的调查中,每加仑英里数(均值、标准差、中位数、范围等)的分布是什么?

  • 在一项新药试验之后,药物组与安慰剂组的结果(无改善、有些改善、显著改善)是什么?参与者的性别对结果有影响吗?

  • 收入和预期寿命之间的相关性是多少?它与零的差异是否显著?

  • 你在美国的不同地区犯罪后更有可能被判入狱吗?地区间的差异在统计上是否显著?

在本章中,我们将回顾 R 函数,用于生成基本的描述性和推断性统计。首先,我们将查看定量变量的位置和尺度度量。然后,你将学习如何为分类变量生成频率和列联表(以及相关的卡方检验)。接下来,我们将检查可用于连续和有序变量的各种相关系数形式。最后,我们将通过参数方法(t 检验)和非参数方法(曼-惠特尼 U 检验、克鲁斯卡尔-沃利斯检验)来研究组间差异。尽管我们的重点是数值结果,但我们将参考图形方法来可视化这些结果。

本章中涵盖的统计方法通常在第一年的本科生统计学课程中教授。如果你对这些方法不熟悉,McCall(2000)和 Kirk(2008)是两个很好的参考文献。或者,对于涵盖的每个主题,许多在线资源(如维基百科)都提供了丰富的信息。

7.1 描述性统计

在本节中,我们将探讨连续变量的集中趋势、变异性和分布形状的度量。为了说明目的,我们将使用你在第一章中首次看到的“汽车道路测试”(mtcars)数据集中的几个变量。我们的重点将放在每加仑英里数(mpg)、马力(hp)和重量(wt)上:

> myvars <- c("mpg", "hp", "wt")
> head(mtcars[myvars])
                   mpg   hp   wt
Mazda RX4         21.0  110  2.62
Mazda RX4 Wag     21.0  110  2.88
Datsun 710        22.8   93  2.32
Hornet 4 Drive    21.4  110  3.21
Hornet Sportabout 18.7  175  3.44
Valiant           18.1  105  3.46

首先,我们将查看所有 32 辆车的描述性统计量。然后,我们将按变速器类型(am)和发动机气缸配置(vs)进行考察。前者编码为0=自动, 1=手动,后者编码为0=V 形1=直列

7.1.1 方法大全

当涉及到计算描述性统计量时,R 拥有丰富的资源。让我们从基本安装版中包含的函数开始。然后我们将查看通过使用用户贡献的包可用的扩展。

在基本安装中,你可以使用summary()函数来获取描述性统计量。以下列表提供了一个示例。

列表 7.1 使用summary()进行描述性统计

> myvars <- c("mpg", "hp", "wt")
> summary(mtcars[myvars])
      mpg             hp              wt      
 Min.   :10.4   Min.   : 52.0   Min.   :1.51  
 1st Qu.:15.4   1st Qu.: 96.5   1st Qu.:2.58  
 Median :19.2   Median :123.0   Median :3.33  
 Mean   :20.1   Mean   :146.7   Mean   :3.22  
 3rd Qu.:22.8   3rd Qu.:180.0   3rd Qu.:3.61  
 Max.   :33.9   Max.   :335.0   Max.   :5.42  

summary()函数提供了数值变量的最小值、最大值、四分位数和平均值,以及因子和逻辑向量的频率。你可以使用第五章中的apply()sapply()函数来提供你选择的任何描述性统计量。apply()函数与矩阵一起使用,而sapply()函数与数据框一起使用。sapply()函数的格式如下

sapply(x, *FUN*, *options*)

其中x是数据框,FUN是任意函数。如果存在options,它们会被传递给FUN。你可以插入这里的典型函数包括mean()sd()var()min()max()median()length()range()quantile()。函数fivenum()返回 Tukey 的五数摘要(最小值、下四分位数、中位数、上四分位数和最大值)。

令人惊讶的是,基本安装版不提供偏度和峰度的函数,但你可以添加自己的。下一列表中的示例提供了几个描述性统计量,包括偏度和峰度。

列表 7.2 使用sapply()进行描述性统计

> mystats <- function(x, na.omit=FALSE){
                if (na.omit)
                    x <- x[!is.na(x)]
                m <- mean(x)
                n <- length(x)
                s <- sd(x)
                skew <- sum((x-m)³/s³)/n
                kurt <- sum((x-m)⁴/s⁴)/n - 3
                return(c(n=n, mean=m, stdev=s, 
                       skew=skew, kurtosis=kurt))
              }

> myvars <- c("mpg", "hp", "wt")
> sapply(mtcars[myvars], mystats)
            mpg      hp       wt
n         32.000   32.000  32.0000
mean      20.091  146.688   3.2172
stdev      6.027   68.563   0.9785
skew       0.611    0.726   0.4231
kurtosis  -0.373   -0.136  -0.0227               

在这个样本中,汽车的均值 mpg 为 20.1,标准差为 6.0。分布向右偏斜(+0.61),并且相对于正态分布来说略平坦(–0.37)。如果你绘制数据图,这一点最为明显。注意,如果你想要省略缺失值,可以使用sapply(mtcars[myvars], mystats, na.omit =TRUE)

7.1.2 更多的方法

几个用户贡献的包提供了描述性统计的函数,包括Hmiscpastecspsychskimrsummytools。由于空间限制,我们只演示前三个,但你可以用这五个中的任何一个生成有用的摘要。因为这些包不包括在基本分布中,所以你需要在首次使用时安装它们(见第 1.4 节)。

Hmisc包中的describe()函数返回变量的数量和观测值的数量,缺失值和唯一值的数量,平均值、四分位数以及最高和最低的五个值。以下列表提供了一个示例。

列表 7.3 使用Hmisc包中的describe()进行描述性统计

> library(Hmisc)
> myvars <- c("mpg", "hp", "wt")
> describe(mtcars[myvars])

 3  Variables      32  Observations
---------------------------------------------------------------------------
mpg 
n missing  unique  Mean    .05   .10     .25   .50    .75    .90    .95
32      0    25   20.09 12.00  14.34  15.43  19.20  22.80  30.09  31.30

lowest : 10.4 13.3 14.3 14.7 15.0, highest: 26.0 27.3 30.4 32.4 33.9 
---------------------------------------------------------------------------
hp 
n missing  unique    Mean    .05     .10   .2     .50   .75   .90     .95
32       0     22   146.7  63.65  66.00 96.50 123.00 180.00 243.50 253.55 

lowest :  52  62  65  66  91, highest: 215 230 245 264 335 
---------------------------------------------------------------------------
wt 
n missing  unique    Mean    .05    .10    .25    .50    .75    .90   .95
32      0      29   3.217  1.736  1.956  2.581  3.325  3.610  4.048 5.293

lowest : 1.513 1.615 1.835 1.935 2.140, highest: 3.845 4.070 5.250 5.345 5.424 
---------------------------------------------------------------------------

pastecs 包包含一个名为 stat.desc() 的函数,它提供了一系列广泛的描述性统计。格式如下

stat.desc(*x*, basic=TRUE, desc=TRUE, norm=FALSE, p=0.95)

其中 x 是一个数据框或时间序列。如果 basic=TRUE(默认值),则提供值数、空值、缺失值、最小值、最大值、范围和总和。如果 desc=TRUE(也是默认值),则还会提供中位数、均值、均值的标准误差、均值的 95% 置信区间、方差、标准差和变异系数。最后,如果 norm=TRUE(非默认值),则返回正态分布统计信息,包括偏度和峰度(及其统计显著性)以及 Shapiro–Wilk 正态性检验。使用 p-value 选项来计算均值的置信区间(默认为 .95)。下一个列表提供了一个示例。

列表 7.4 在 pastecs 包中使用 stat.desc() 进行描述性统计

> library(pastecs)
> myvars <- c("mpg", "hp", "wt")
> stat.desc(mtcars[myvars])
                mpg       hp      wt
nbr.val       32.00   32.000  32.000
nbr.null       0.00    0.000   0.000
nbr.na         0.00    0.000   0.000
min           10.40   52.000   1.513
max           33.90  335.000   5.424
range         23.50  283.000   3.911
sum          642.90 4694.000 102.952
median        19.20  123.000   3.325
mean          20.09  146.688   3.217
SE.mean        1.07   12.120   0.173
CI.mean.0.95   2.17   24.720   0.353
var           36.32 4700.867   0.957
std.dev        6.03   68.563   0.978
coef.var       0.30    0.467   0.304

似乎这还不够,psych 包还有一个名为 describe() 的函数,它提供了非缺失观测值的数量、均值、标准差、中位数、截断均值、中位数绝对偏差、最小值、最大值、范围、偏度、峰度和均值的标准误差。你可以在下面的列表中看到一个示例。

列表 7.5 在 psych 包中使用 describe() 进行描述性统计

> library(psych)
Attaching package: 'psych'
        The following object(s) are masked from package:Hmisc :
         describe 
> myvars <- c("mpg", "hp", "wt")
> describe(mtcars[myvars])
    var  n   mean    sd median trimmed   mad   min    max
mpg   1 32  20.09  6.03  19.20   19.70  5.41 10.40  33.90
hp    2 32 146.69 68.56 123.00  141.19 77.10 52.00 335.00
wt    3 32   3.22  0.98   3.33    3.15  0.77  1.51   5.42
     range skew kurtosis    se
mpg  23.50 0.61    -0.37  1.07
hp  283.00 0.73    -0.14 12.12
wt    3.91 0.42    -0.02  0.17

我告诉你,这是富得流油的尴尬!

注意:在先前的例子中,psychHmisc 包都提供了一个名为 describe() 的函数。R 是如何知道使用哪一个的?简单来说,最后加载的包具有优先权,如列表 7.5 所示。在这里,psych 包是在 Hmisc 包之后加载的,并且会打印出一个消息,表明 Hmisc 中的 describe() 函数被 psych 中的函数覆盖。当你输入 describe() 函数时,R 会首先搜索 psych 包并执行它。如果你想使用 Hmisc 版本,你可以输入 Hmisc::describe(mt)。函数仍然存在。你必须给 R 提供更多信息才能找到它。

现在你已经知道了如何生成整个数据的描述性统计,让我们回顾一下如何获取数据的子组的统计信息。

7.1.3 按组进行描述性统计

当比较个体或观测值的组时,通常关注的是每个组的描述性统计,而不是整个样本。可以使用基础 R 的 by() 函数生成组统计信息。格式如下

by(*data, INDICES, FUN*)

其中 data 是一个数据框或矩阵,INDICES 是一个因子或因子列表,它定义了组,而 FUN 是一个作用于数据框所有列的任意函数。下一个列表提供了一个示例。

列表 7.6 使用 by() 按组进行描述性统计

> dstats <- function(x)sapply(x, mystats)
> myvars <- c("mpg", "hp", "wt")
> by(mtcars[myvars], mtcars$am, dstats)

mtcars$am: 0
             mpg        hp        wt
n          19.000    19.0000   19.000
mean       17.147   160.2632    3.769
stdev       3.834    53.9082    0.777
skew        0.014    -0.0142    0.976
kurtosis   -0.803    -1.2097    0.142
---------------------------------------- 
mtcars$am: 1
             mpg        hp        wt
n          13.0000    13.000   13.000
mean       24.3923   126.846    2.411
stdev       6.1665    84.062    0.617
skew        0.0526     1.360    0.210
kurtosis   -1.4554     0.563   -1.174

在这种情况下,dstats() 将列表 7.2 中的 mystats() 函数应用于数据框的每一列。将其放在 by() 函数中,你可以得到 am 的每个级别的汇总统计信息。

在下一个例子(列表 7.7)中,为两个 by 变量(amvs)生成了汇总统计量,并且每个组的统计结果都使用自定义标签打印出来。此外,在计算统计量之前,省略了缺失值。

列表 7.7:由多个变量定义的组的描述性统计

> dstats <- function(x)sapply(x, mystats, na.omit=TRUE)
> myvars <- c("mpg", "hp", "wt")
> by(mtcars[myvars], 
     list(Transmission=mtcars$am,
          Engine=mtcars$vs), 
     FUN=dstats)

Transmission: 0
Engine: 0
                mpg          hp         wt
n        12.0000000  12.0000000 12.0000000
mean     15.0500000 194.1666667  4.1040833
stdev     2.7743959  33.3598379  0.7683069
skew     -0.2843325   0.2785849  0.8542070
kurtosis -0.9635443  -1.4385375 -1.1433587
----------------------------------------------------------------- 
Transmission: 1
Engine: 0
                mpg          hp          wt
n         5.0000000   6.0000000  6.00000000
mean     19.5000000 180.8333333  2.85750000
stdev     4.4294469  98.8158219  0.48672117
skew      0.3135121   0.4842372  0.01270294
kurtosis -1.7595065  -1.7270981 -1.40961807
----------------------------------------------------------------- 
Transmission: 0
Engine: 1
                mpg          hp         wt
n         7.0000000   7.0000000  7.0000000
mean     20.7428571 102.1428571  3.1942857
stdev     2.4710707  20.9318622  0.3477598
skew      0.1014749  -0.7248459 -1.1532766
kurtosis -1.7480372  -0.7805708 -0.1170979
----------------------------------------------------------------- 
Transmission: 1
Engine: 1
                mpg         hp         wt
n         7.0000000  7.0000000  7.0000000
mean     28.3714286 80.5714286  2.0282857
stdev     4.7577005 24.1444068  0.4400840
skew     -0.3474537  0.2609545  0.4009511
kurtosis -1.7290639 -1.9077611 -1.3677833

虽然前面的例子使用了 mystats() 函数,但你也可以使用 Hmiscpsych 包中的 describe() 函数,或者 pastecs 包中的 stat.desc() 函数。实际上,by() 函数提供了一个通用的机制,可以重复对任何子组进行任何分析。

7.1.4 使用 dplyr 交互式汇总数据

到目前为止,我们一直关注生成给定数据框的全面描述性统计方法。然而,在交互式、探索性数据分析中,我们的目标是回答有针对性的问题。在这种情况下,我们希望从特定的观察组中获得有限数量的统计信息。

在第 3.11 节中介绍的 dplyr 包为我们提供了快速灵活地完成此任务的工具。summarize()summarize_all() 函数可以用来计算任何统计量,而 group_by() 函数可以用来指定计算这些统计量的组。

作为演示,让我们使用 carData 包中的 Salaries 数据框提出并回答一系列问题。该数据集包含美国一所大学 2008-2009 年九个月的美元薪资(salary),涉及 397 名教职员工。这些数据是作为持续监测男性和女性教职员工薪资差异的持续努力的一部分而收集的。

在继续之前,请确保已安装 carDatadplyr 包(install.packages(c("carData", "dplyr")))。然后加载这些包

library(dplyr)
library(carData)

我们现在准备好对数据进行查询。

397 名教授的薪资中位数和薪资范围是多少?

> Salaries %>%
    summarize(med = median(salary), 
              min = min(salary), 
              max = max(salary))
     med   min    max
1 107300 57800 231545

Salaries 数据集被传递给 summarize() 函数,该函数计算薪资的中位数、最小值和最大值,并将结果作为一行 tibble(数据框)返回。九个月薪资的中位数是 $107,300,至少有一个人赚得超过 $230,000。显然,我需要要求加薪。

按性别和职称,教职员工的数量、中位薪资和薪资范围是多少?

> Salaries %>%
    group_by(rank, sex) %>%
    summarize(n = length(salary),
              med = median(salary), 
              min = min(salary), 
              max = max(salary))

  rank      sex        n     med   min    max
  <fct>     <fct>  <int>   <dbl> <int>  <int>
1 AsstProf  Female    11  77000  63100  97032
2 AsstProf  Male      56  80182  63900  95079
3 AssocProf Female    10  90556\. 62884 109650
4 AssocProf Male      54  95626\. 70000 126431
5 Prof      Female    18 120258\. 90450 161101
6 Prof      Male     248 123996  57800 231545

当在 by_group() 语句中指定分类变量时,summarize() 函数为它们级别的每个组合生成一行统计信息。在每一所学院的职称中,女性的中位薪资低于男性。此外,这所大学有大量的男性正教授。

按性别和职称,教职员工的平均服务年限和自获得博士学位以来的年限是多少?

> Salaries %>%
    group_by(rank, sex) %>%
    select(yrs.service, yrs.since.phd) %>%
    summarize_all(mean)

  rank      sex    yrs.service yrs.since.phd
  <fct>     <fct>        <dbl>         <dbl>
1 AsstProf  Female        2.55          5.64
2 AsstProf  Male          2.34          5   
3 AssocProf Female       11.5          15.5 
4 AssocProf Male         12.0          15.4 
5 Prof      Female       17.1          23.7 
6 Prof      Male         23.2          28.6

summarize_all() 函数为每个非分组变量(yrs.serviceyrs.since.phd)计算汇总统计量。如果你想要每个变量的多个统计量,请以列表形式提供。例如,summarize_all(list(mean=mean, std=sd)) 将为每个变量计算均值和标准差。男性和女性在助理教授和副教授级别上的经验历史相当。然而,女性正教授的经验年数少于她们的男性同行。

dplyr 方法的一个优点是结果以 tibbles(数据框)的形式返回。这允许你进一步分析这些汇总结果,绘制它们,并将它们重新格式化以供打印。它还提供了一个简单的机制来聚合数据。

通常情况下,数据分析师都有自己的偏好,选择哪些描述性统计量来展示,以及他们喜欢如何格式化这些统计量。这可能是为什么有这么多变体可供选择。选择最适合你的一个,或者创建你自己的!

7.1.5 可视化结果

分布特性的数值汇总很重要,但它们不能替代视觉表示。对于定量变量,你有直方图(第 6.4 节)、密度图(第 6.5 节)、箱线图(第 6.6 节)和点图(第 6.7 节)。它们可以提供通过依赖少量描述性统计量容易错过的见解。

到目前为止考虑的函数提供了定量变量的汇总。下一节中的函数允许你检查分类变量的分布。

7.2 频率和列联表

在本节中,我们将查看来自分类变量的频率和列联表,以及独立性检验、关联度测量和图形显示结果的方法。我们将使用基本安装中的函数,以及 vcdgmodels 包中的函数。在以下示例中,假设 ABC 代表分类变量。

本节的数据来自 vcd 包中包含的 Arthritis 数据集。数据来自 Koch 和 Edward(1988 年)的研究,代表了一种针对类风湿性关节炎的新治疗方法的双盲临床试验。以下是前几个观测值:

> library(vcd)
> head(Arthritis)
    ID      Treatment       Sex     Age     Improved
1   57      Treated         Male    27      Some
2   46      Treated         Male    29      None
3   77      Treated         Male    30      None
4   17      Treated         Male    32      Marked
5   36      Treated         Male    46      Marked
6   23      Treated         Male    58      Marked

处理(安慰剂,治疗),性别(男性,女性),以及改善(无,一些,显著)都是分类因素。在下一节中,你将根据数据创建频率和列联表(交叉分类)。

7.2.1 生成频率表

R 提供了多种创建频率和列联表的方法。最重要的函数列于表 7.1 中。

表 7.1 创建和操作列联表的函数

函数 描述
table(*var1*, *var2*, ..., *varN*) N 个分类变量(因素)创建一个 N 方列联表
xtabs(*formula, data*) 根据公式和矩阵或数据框创建一个N维列联表
prop.table(*table, margins*) 将表条目表示为由margins定义的边际表的分数
margin.table(*table, margins*) 计算由margins定义的边际表的条目总和
addmargins(*table, margins*) 在表上添加摘要margins(默认为总和)
ftable(*table*) 创建一个紧凑的、“扁平”的列联表

在接下来的几节中,我们将使用这些函数中的每一个来探索分类变量。我们将从简单的频率开始,然后是双向列联表,最后是多维列联表。第一步是使用table()xtabs()函数创建一个表,然后使用其他函数对其进行操作。

一维表

你可以使用table()函数生成简单的频率计数。以下是一个示例:

> mytable <- with(Arthritis, table(Improved))
> mytable
Improved
  None   Some  Marked 
   42     14     28

你可以使用prop.table()将这些频率转换为比例

> prop.table(mytable)
Improved
  None   Some  Marked 
 0.500  0.167  0.333

或者使用prop.table()*100转换为百分比:

> prop.table(mytable)*100
Improved
  None   Some  Marked 
  50.0   16.7   33.3

在这里,你可以看到 50%的研究参与者有一些或显著的改善(16.7 + 33.3)。

二维表

对于双向表,table()函数的格式是

mytable <- table(*A*, *B*)

其中A是行变量,B是列变量。或者,xtabs()函数允许你使用公式样式输入创建列联表。格式是

*mytable* <- xtabs(~ *A* + *B*, data=*mydata*)

其中mydata是一个矩阵或数据框。一般来说,要交叉分类的变量出现在公式的右边(即~的右边),由加号分隔。如果一个变量出现在公式的左边,它被假定为频率向量(如果数据已经过分类很有用)。

对于Arthritis数据,你有

> mytable <- xtabs(~ Treatment + Improved, data=Arthritis)
> mytable
          Improved
Treatment  None  Some  Marked
  Placebo   29    7      7
  Treated   13    7     21

你可以使用margin.table()prop.table()函数分别生成边际频率和比例。对于行总和和行比例,你有

> margin.table(mytable, 1)
Treatment
Placebo Treated 
   43      41 
> prop.table(mytable, 1)
           Improved
Treatment   None   Some   Marked
  Placebo  0.674  0.163   0.163
  Treated  0.317  0.171   0.512

索引(1)指的是xtabs()语句中的第一个变量——行变量。每行的比例加起来等于 1。查看表格,你可以看到 51%的接受治疗的人有显著改善,而接受安慰剂的人只有 16%。

对于列总和和列比例,你有

> margin.table(mytable, 2)
Improved
   None   Some  Marked 
    42     14     28 
> prop.table(mytable, 2)
            Improved
Treatment   None   Some   Marked
  Placebo  0.690  0.500   0.250
  Treated  0.310  0.500   0.750

这里,索引(2)指的是xtabs()语句中的第二个变量——即列。每列的比例加起来等于 1。

使用此语句可以获得单元格比例:

> prop.table(mytable)
            Improved
Treatment   None    Some   Marked
  Placebo  0.3452  0.0833  0.0833
  Treated  0.1548  0.0833  0.2500

所有单元格比例的总和加起来等于 1。

你可以使用addmargins()函数将这些表的边际总和添加到其中。例如,以下代码添加了一个Sum行和列:

> addmargins(mytable)
                Improved
Treatment   None    Some   Marked    Sum
  Placebo    29       7       7       43
  Treated    13       7      21       41
  Sum        42      14      28       84
> addmargins(prop.table(mytable))
                Improved
Treatment   None    Some   Marked    Sum
  Placebo  0.3452  0.0833  0.0833  0.5119
  Treated  0.1548  0.0833  0.2500  0.4881
  Sum      0.5000  0.1667  0.3333  1.0000

当使用addmargins()时,默认是为表中的所有变量创建总和边际。相比之下,以下代码仅添加一个Sum列:

> addmargins(prop.table(mytable, 1), 2)
                Improved
Treatment   None    Some   Marked    Sum
  Placebo   0.674   0.163   0.163    1.000
  Treated   0.317   0.171   0.512    1.000

同样,此代码添加了一个Sum行:

> addmargins(prop.table(mytable, 2), 1)
            Improved
Treatment   None    Some   Marked
  Placebo   0.690   0.500   0.250
  Treated   0.310   0.500   0.750
  Sum       1.000   1.000   1.000

在表中,你可以看到 25%的显著改善的患者接受了安慰剂。

注意:table()函数默认忽略缺失值(NAs)。要包括NA作为频率计数中的有效类别,请包含表格选项useNA="ifany"

创建双向表的第三种方法是gmodels包中的CrossTable()函数。CrossTable()函数生成类似于 SAS 中的PROC FREQ或 SPSS 中的CROSSTABS的双向表。以下列表提供了一个示例。

列表 7.8 使用CrossTable的二维表

> library(gmodels)
> CrossTable(Arthritis$Treatment, Arthritis$Improved)

   Cell Contents
|-------------------------|
|                       N |
| Chi-square contribution |
|           N / Row Total |
|           N / Col Total |
|         N / Table Total |
|-------------------------|

Total Observations in Table:  84 

                    | Arthritis$Improved 
Arthritis$Treatment |      None |      Some |    Marked | Row Total | 
--------------------|-----------|-----------|-----------|-----------|
            Placebo |        29 |         7 |         7 |        43 | 
                    |     2.616 |     0.004 |     3.752 |           | 
                    |     0.674 |     0.163 |     0.163 |     0.512 | 
                    |     0.690 |     0.500 |     0.250 |           | 
                    |     0.345 |     0.083 |     0.083 |           | 
--------------------|-----------|-----------|-----------|-----------|
            Treated |        13 |         7 |        21 |        41 | 
                    |     2.744 |     0.004 |     3.935 |           | 
                    |     0.317 |     0.171 |     0.512 |     0.488 | 
                    |     0.310 |     0.500 |     0.750 |           | 
                    |     0.155 |     0.083 |     0.250 |           | 
--------------------|-----------|-----------|-----------|-----------|
       Column Total |        42 |        14 |        28 |        84 | 
                    |     0.500 |     0.167 |     0.333 |           | 
--------------------|-----------|-----------|-----------|-----------|

CrossTable()函数有选项可以报告百分比(行、列和单元格);指定小数位数;生成卡方、Fisher 和 McNemar 独立性检验;报告期望值和残差值(皮尔逊、标准化和调整标准化);将缺失值视为有效;用行和列标题注释;并以 SAS 或 SPSS 风格输出格式化。有关详细信息,请参阅help(CrossTable)

如果你有两个以上的分类变量,你将处理多维表。我们将在下一部分考虑这些。

多维表

table()xtabs()都可以用于根据三个或更多分类变量生成多维表。margin.table()prop.table()addmargins()函数自然扩展到超过两个维度。此外,ftable()函数可以用于以紧凑和吸引人的方式打印多维表。以下列表提供了一个示例。

列表 7.9 三维列联表

> mytable <- xtabs(~ Treatment+Sex+Improved, data=Arthritis)   ❶
> mytable          
, , Improved = None   

           Sex
Treatment  Female  Male
  Placebo      19    10
  Treated       6     7

, , Improved = Some

           Sex
Treatment  Female  Male
  Placebo       7     0
  Treated       5     2

, , Improved = Marked

           Sex
Treatment  Female  Male
  Placebo       6     1
  Treated      16     5

> ftable(mytable)                
                   Sex Female Male
Treatment Improved                
Placebo   None             19   10
          Some              7    0
          Marked            6    1
Treated   None              6    7
          Some              5    2
          Marked           16    5

> margin.table(mytable, 1)                                    ❷

Treatment 
Placebo Treated                                   
     43      41 
> margin.table(mytable, 2)        
Sex
Female   Male 
    59     25 
> margin.table(mytable, 3)
Improved
  None   Some Marked 
    42     14     28 
> margin.table(mytable, c(1, 3))                              ❸
         Improved
Treatment None Some Marked                          
  Placebo   29    7      7
  Treated   13    7     21
 > ftable(prop.table(mytable, c(1, 2)))                       ❹
                 Improved  None  Some Marked
Treatment Sex                                           
Placebo   Female          0.594 0.219  0.188
          Male            0.909 0.000  0.091
Treated   Female          0.222 0.185  0.593
          Male            0.500 0.143  0.357

> ftable(addmargins(prop.table(mytable, c(1, 2)), 3))     
                 Improved  None  Some Marked   Sum
Treatment Sex                                     
Placebo   Female          0.594 0.219  0.188 1.000
          Male            0.909 0.000  0.091 1.000
Treated   Female          0.222 0.185  0.593 1.000
          Male            0.500 0.143  0.357 1.000

❶ 单元频率

❷ 边际频率

❸ 处理 × 改进边际频率

❹ 处理 × 性别改进比例

❶处的代码生成了三维分类的单元频率。该代码还演示了如何使用ftable()函数打印出更紧凑和吸引人的表格版本。

❷处的代码生成了TreatmentSexImproved的边际频率。因为你使用公式~Treatment+Sex + Improved创建了表格,所以Treatment通过索引1引用,Sex通过索引2引用,而Improved通过索引3引用。

❸处的代码生成了Treatment x Improved分类的边际频率,按Sex求和。❹提供了每个Treatment × Sex组合中NoneSomeMarked改进的病人比例。在这里,你可以看到 36%的接受治疗的男性有显著的改善,而接受治疗的女性中有 59%。一般来说,比例将在prop.table()调用中未包含的索引上求和为 1(在这个例子中是第三个索引,即Improved)。你可以在最后一个示例中看到这一点,其中你在第三个索引上添加了一个总和边缘。

如果你想要百分比而不是比例,可以将结果表乘以 100。例如,此语句

ftable(addmargins(prop.table(mytable, c(1, 2)), 3)) * 100

生成此表:

                   Sex Female  Male   Sum
Treatment Improved                       
Placebo   None           65.5  34.5 100.0
          Some          100.0   0.0 100.0
          Marked         85.7  14.3 100.0
Treated   None           46.2  53.8 100.0
          Some           71.4  28.6 100.0
          Marked         76.2  23.8 100.0

列联表告诉你表中每个变量组合的案例频率或比例,但你可能也对表中的变量是否相关或独立感兴趣。独立性检验将在下一节中介绍。

7.2.2 独立性检验

R 提供了多种测试分类变量独立性的方法。本节中描述的三种测试分别是卡方独立性检验、Fisher 精确检验和 Cochran-Mantel-Haenszel 检验。

独立性卡方检验

你可以将 chisq.test() 函数应用于双向表,以产生行和列变量的独立性卡方检验。请参见下一列表中的示例。

列表 7.10 独立性卡方检验

> library(vcd)
> mytable <- xtabs(~Treatment+Improved, data=Arthritis)          
> chisq.test(mytable)                                          
        Pearson’s Chi-squared test
data:  mytable                                                 
 X-squared = 13.1, df = 2, p-value = 0.001463               ❶

> mytable <- xtabs(~Improved+Sex, data=Arthritis)              
> chisq.test(mytable)                                           
        Pearson's Chi-squared test                                
data:  mytable  
 X-squared = 4.84, df = 2, p-value = 0.0889                ❷

Warning message:    
In chisq.test(mytable) : Chi-squared approximation may be incorrect

❶ 治疗和改善不是独立的。

❷ 性别和改善是独立的。

从结果来看,似乎治疗接受情况与改善程度之间存在关联(p < .01)。但似乎没有发现患者性别与改善程度之间的关联(p > .05)。p 值是在假设总体中行和列变量的独立性成立的情况下,获得样本结果的概率。因为概率很小❶,所以你拒绝治疗类型和结果独立的假设。因为概率❷并不小,所以假设结果和性别独立是合理的。列表 7.10 中的警告信息产生是因为表格中的六个单元格(男性,有些改善)的期望值小于 5,这可能会使卡方近似无效。

Fisher 精确检验

你可以通过 fisher.test() 函数进行 Fisher 精确检验。Fisher 精确检验评估的是列联表中行和列的独立性零假设,其中边际是固定的。格式是 fisher.test(mytable),其中 mytable 是一个双向表。以下是一个示例:

> mytable <- xtabs(~Treatment+Improved, data=Arthritis)
> fisher.test(mytable)
        Fisher's Exact Test for Count Data
data:  mytable 
p-value = 0.001393
alternative hypothesis: two.sided

与许多统计软件包不同,fisher.test() 函数可以应用于任何具有两个或更多行和列的双向表,而不仅仅是 2 × 2 独立性表。

Cochran-Mantel-Haenszel 检验

mantelhaen.test() 函数提供了对第三变量每个层中两个名义变量条件独立性的零假设的 Cochran-Mantel-Haenszel 卡方检验。以下代码测试了 TreatmentImproved 变量在 Sex 的每个水平上是否独立的假设。该测试假设不存在三重交互作用(Treatment × Improved × Sex):

> mytable <- xtabs(~Treatment+Improved+Sex, data=Arthritis)
> mantelhaen.test(mytable)
        Cochran-Mantel-Haenszel test
data:  mytable 
Cochran-Mantel-Haenszel M² = 14.6, df = 2, p-value = 0.0006647

结果表明,在 Sex 的每个水平上,接受的治疗和报告的改善情况并不是独立的(即在控制性别的情况下,接受治疗的人比接受安慰剂的人改善更多)。

7.2.3 关联度量

上一节中的显著性检验评估是否存在足够的证据来拒绝变量之间独立性的零假设。如果你可以拒绝零假设,你的兴趣自然会转向关联性度量,以衡量现有关系的强度。vcd 包中的 assocstats() 函数可以用来计算双向表的 phi 系数、列联系数和 Cramér 的 V 值。以下是一个示例。

列表 7.11 双向表的关联性度量

> library(vcd)
> mytable <- xtabs(~Treatment+Improved, data=Arthritis)
> assocstats(mytable)
                    X² df  P(> X²)
Likelihood Ratio 13.530  2 0.0011536
Pearson          13.055  2 0.0014626

Phi-Coefficient   : 0.394 
Contingency Coeff.: 0.367 
Cramer's V        : 0.394

通常情况下,较大的数值表示更强的关联性。vcd 包还提供了一个 kappa() 函数,可以计算混淆矩阵的 Cohen 的 kappa 和加权 kappa(例如,两个将一组对象分类到类别中的评委之间的一致程度)。

7.2.4 结果的可视化

R 具有探索分类变量之间关系的机制,这些机制远远超出了大多数其他统计平台所发现的。你通常使用条形图来可视化一维中的频率(参见第 6.1 节)。vcd 包提供了用于使用镶嵌图和关联图可视化多维数据集中分类变量之间关系的优秀函数(参见第 11.4 节)。最后,ca 包中的对应分析函数允许你使用各种几何表示来可视化列联表中行与列之间的关系(Nenadic´和 Greenacre,2007)。

这结束了关于列联表的讨论,直到我们在第十一章和第十九章中探讨更高级的主题。接下来,让我们看看各种类型的相关系数。

7.3 相关系数

相关系数用于描述定量变量之间的关系。符号(正号或负号)表示关系的方向(正相关或负相关),而数值表示关系的强度(从 0 表示没有关系到 1 表示完全可预测的关系)。

在本节中,我们将探讨各种相关系数以及显著性检验。我们将使用 R 基础安装中可用的 state.x77 数据集。它提供了 1977 年 50 个美国州的人口、收入、文盲率、预期寿命、谋杀率和高中毕业率的数据。还有温度和土地面积指标,但我们将删除它们以节省空间。使用 help(state .x77) 可以了解更多关于该文件的信息。除了基础安装外,我们还将使用 psychggm 包。

7.3.1 相关类型的种类

R 可以产生各种相关系数,包括皮尔逊、斯皮尔曼、肯德尔、偏相关、多项相关和多项序列相关。让我们逐一来看。

皮尔逊、斯皮尔曼和肯德尔相关系数

皮尔逊积矩相关系数评估两个定量变量之间线性关系的程度。斯皮尔曼秩相关系数评估两个秩次变量之间的关系程度。肯德尔 tau 也是一种非参数的秩相关度量。

cor()函数产生所有三个相关系数,而cov()函数提供协方差。有许多选项,但生成相关性的简化格式如下:

cor(x, use= , method= ) 

选项在表 7.2 中描述。

表 7.2 cor/cov选项

选项 描述
x 矩阵或数据框。
use 指定缺失数据的处理方式。选项有all.obs(假设没有缺失数据——缺失数据将产生错误)、everything(任何涉及缺失值的案例的相关性都将设置为missing)、complete.obs(逐行删除)和pairwise.complete.obs(成对删除)。
method 指定相关类型。选项有pearsonspearmankendall

默认选项是use="everything"method="pearson"。以下列表提供了一个示例。

列表 7.12 协方差和相关系数

> states<- state.x77[,1:6]
> cov(states)
           Population Income Illiteracy Life Exp  Murder  HS Grad
Population   19931684 571230    292.868 -407.842 5663.52 -3551.51
Income         571230 377573   -163.702  280.663 -521.89  3076.77
Illiteracy        293   -164      0.372   -0.482    1.58    -3.24
Life Exp         -408    281     -0.482    1.802   -3.87     6.31
Murder           5664   -522      1.582   -3.869   13.63   -14.55
HS Grad         -3552   3077     -3.235    6.313  -14.55    65.24

> cor(states)
           Population Income Illiteracy Life Exp Murder HS Grad
Population     1.0000  0.208      0.108   -0.068  0.344 -0.0985
Income         0.2082  1.000     -0.437    0.340 -0.230  0.6199
Illiteracy     0.1076 -0.437      1.000   -0.588  0.703 -0.6572
Life Exp      -0.0681  0.340     -0.588    1.000 -0.781  0.5822
Murder         0.3436 -0.230      0.703   -0.781  1.000 -0.4880
HS Grad       -0.0985  0.620     -0.657    0.582 -0.488  1.0000
> cor(states, method="spearman")
           Population Income Illiteracy Life Exp Murder HS Grad
Population      1.000  0.125      0.313   -0.104  0.346  -0.383
Income          0.125  1.000     -0.315    0.324 -0.217   0.510
Illiteracy      0.313 -0.315      1.000   -0.555  0.672  -0.655
Life Exp       -0.104  0.324     -0.555    1.000 -0.780   0.524
Murder          0.346 -0.217      0.672   -0.780  1.000  -0.437
HS Grad        -0.383  0.510     -0.655    0.524 -0.437   1.000

第一次调用产生方差和协方差,第二次提供皮尔逊积矩相关系数,第三次产生斯皮尔曼秩相关系数。例如,您可以看到收入与高中毕业率之间存在强烈的正相关,而文盲率与预期寿命之间存在强烈的负相关。

注意,默认情况下您会得到方阵(所有变量与所有其他变量交叉)。您也可以生成非方阵,如下面的示例所示:

> x <- states[,c("Population", "Income", "Illiteracy", "HS Grad")]
> y <- states[,c("Life Exp", "Murder")]
> cor(x,y)
           Life Exp Murder
Population   -0.068  0.344
Income        0.340 -0.230
Illiteracy   -0.588  0.703
HS Grad       0.582 -0.488

当您对一组变量与另一组变量之间的关系感兴趣时,这个函数版本特别有用。请注意,结果不会告诉您相关性是否显著不同于 0(即,是否有足够的证据基于样本数据来得出结论,认为总体相关性不同于 0)。为此,您需要进行显著性测试(在 7.3.2 节中描述)。

部分相关

部分相关是指两个定量变量之间的相关关系,同时控制一个或多个其他定量变量。您可以使用ggm包中的pcor()函数来提供部分相关系数。ggm包默认未安装,因此请确保在首次使用时安装它。格式如下:

pcor(*u*, *S*)

其中u是一个数字向量,前两个数字是要相关变量的索引,其余数字是条件变量的索引(即部分化的变量)。S是变量之间的协方差矩阵。以下示例将有助于澄清这一点:

> library(ggm)
> colnames(states)
[1] "Population" "Income" "Illiteracy" "Life Exp" "Murder" "HS Grad"  
> pcor(c(1,5,2,3,6), cov(states))
[1] 0.346             

在这种情况下,0.346 是在控制收入、文盲率和高中毕业率(分别对应变量 236)的影响下,人口(变量 1)与谋杀率(变量 5)之间的相关系数。在社会科学中,使用偏相关系数是很常见的。

其他类型的相关性

polycor 包中的 hetcor() 函数可以计算包含数值变量之间的皮尔逊积矩相关系数、数值和有序变量之间的多项式相关系数、有序变量之间的多项式相关系数以及二元变量的四分位相关系数的异质相关矩阵。多项式、多项式和四分位相关系数假设有序或二元变量是从潜在的正态分布中派生出来的。有关更多信息,请参阅此包的文档。

7.3.2 测试相关性的显著性

一旦生成了相关系数,你如何测试它们的统计显著性?典型的零假设是没有关系(即总体中的相关系数为 0)。你可以使用 cor.test() 函数来测试单个皮尔逊、斯皮尔曼和肯德尔相关系数。简化格式如下

cor.test(*x*, *y*, alternative = , method = )

其中 xy 是要相关联的变量,alternative 指定是双尾还是单尾测试("two.side""less""greater"),而 method 指定要计算的相关类型("pearson""kendall""spearman")。当研究假设是总体相关系数小于 0 时,使用 alternative="less"。当研究假设是总体相关系数大于 0 时,使用 alternative="greater"。默认情况下,假设 alternative="two.side"(总体相关系数不等于 0)。以下列表提供了一个示例。

列表 7.13 测试相关系数的显著性

> cor.test(states[,3], states[,5])

        Pearson's product-moment correlation

data:  states[, 3] and states[, 5] 
t = 6.85, df = 48, p-value = 1.258e-08
alternative hypothesis: true correlation is not equal to 0 
95 percent confidence interval:
 0.528 0.821 
sample estimates:
  cor 
0.703 

此代码测试了零假设,即预期寿命与谋杀率之间的皮尔逊相关系数为 0。假设总体相关系数为 0,你预计在 1000 万次中只会看到 0.703 这样大的样本相关系数不到一次(即 p=1.258e-08)。鉴于这种情况不太可能发生,你将拒绝零假设,支持研究假设,即预期寿命与谋杀率之间的总体相关系数不是 0。

很遗憾,使用 cor.test() 你一次只能测试一个相关性。幸运的是,psych 包中提供的 corr.test() 函数允许你更进一步。corr.test() 函数可以生成皮尔逊、斯皮尔曼和肯德尔相关系数矩阵的相关性和显著性水平。以下列表提供了一个示例。

列表 7.14 通过 corr.test() 计算相关矩阵和显著性测试

> library(psych)
> corr.test(states, use="complete")

Call:corr.test(x = states, use = "complete")
Correlation matrix 
           Population Income Illiteracy Life Exp Murder HS Grad
Population       1.00   0.21       0.11    -0.07   0.34   -0.10    
Income           0.21   1.00      -0.44     0.34  -0.23    0.62
Illiteracy       0.11  -0.44       1.00    -0.59   0.70   -0.66
Life Exp        -0.07   0.34      -0.59     1.00  -0.78    0.58
Murder           0.34  -0.23       0.70    -0.78   1.00   -0.49
HS Grad         -0.10   0.62      -0.66     0.58  -0.49    1.00

Sample Size 
[1] 50

Probability value 
           Population Income Illiteracy Life Exp Murder HS Grad
Population       0.00   0.15       0.46     0.64   0.01     0.5     
Income           0.15   0.00       0.00     0.02   0.11     0.0
Illiteracy       0.46   0.00       0.00     0.00   0.00     0.0
Life Exp         0.64   0.02       0.00     0.00   0.00     0.0
Murder           0.01   0.11       0.00     0.00   0.00     0.0
HS Grad          0.50   0.00       0.00     0.00   0.00     0.0        

use=选项可以是"pairwise""complete"(分别表示成对或列表删除缺失值)。method=选项是"pearson"(默认值)、"spearman""kendall"。在这里,你可以看到文盲率和预期寿命之间的相关性(-0.59)与零显著不同(p=0.00),这表明随着文盲率的上升,预期寿命往往会下降。然而,人口规模和高中毕业率之间的相关性(-0.10)与 0 没有显著差异(p=0.5)。

其他显著性测试

在 7.4.1 节中,我们研究了部分相关。psych包中的pcor.test()函数可以用来测试在控制一个或多个额外变量的情况下,两个变量的条件独立性,假设多元正态性。格式是

pcor.test(*r*, *q*, *n*)

其中r是由pcor()函数产生的部分相关,q是正在控制的变量数量,n是样本大小。

在离开这个主题之前,我应该提到,psych包中的r.test()函数也提供了一些有用的显著性测试。该函数可以用来测试以下内容:

  • 相关系数的显著性

  • 两个独立相关系数之间的差异

  • 共享单个变量的两个相关系数之间的差异

  • 基于完全不同变量的两个相关系数之间的差异

有关详细信息,请参阅help(r.test)

7.3.3 可视化相关性

通过散点图和散点图矩阵可以可视化相关性背后的双变量关系,而相关图提供了一种独特且强大的方法,以有意义的方式比较大量相关系数。第十一章涵盖了这两者。

7.4 t 检验

研究中最常见的活动是比较两组。接受新药的患者是否比使用现有药物的患者显示出更大的改善?一个制造过程是否比另一个制造过程产生的缺陷更少?两种教学方法中哪一种最具有成本效益?如果你的结果变量是分类的,你可以使用第 7.3 节中描述的方法。在这里,我们将重点关注结果变量是连续且假设为正态分布的组间比较。

对于这个示例,我们将使用与MASS包一起分发的UScrime数据集。它包含了关于 1960 年 47 个美国州惩罚制度对犯罪率影响的信息。感兴趣的结果变量将是Prob(监禁的概率)、U1(14-24 岁城市男性的失业率)和U2(35-39 岁城市男性的失业率)。分类变量So(南方州的指示变量)将作为分组变量。数据已被原始作者进行了缩放。(我考虑将这一节命名为“旧南方的犯罪与惩罚”,但更冷静的头脑占了上风。)

7.4.1 独立 t 检验

在南方犯罪你是否更有可能被监禁?感兴趣的比较是南方各州与非南方各州,因变量是被监禁的概率。可以使用双组独立 t 检验来检验两个总体均值相等的假设。在这里,你假设两组是独立的,并且数据是从正态总体中抽取的。格式可以是

t.test(*y ~ x, data*) 

其中 y 是数值型,x 是二元变量,或者

t.test(*y1, y2*)

其中 y1y2 是数值向量(每个组的因变量)。可选的 data 参数指的是包含变量的矩阵或数据框。与大多数统计软件包不同,默认测试假设方差不等,并应用威尔士自由度修正。你可以添加 var.equal=TRUE 选项来指定方差相等和合并方差估计。默认情况下,假设双尾备择假设(即,均值不同,但方向未指定)。你可以添加选项 alternative="less"alternative="greater" 来指定方向性测试。

以下代码使用双尾测试且不假设方差相等,比较南方(组 1)和非南方(组 0)各州被监禁的概率:

> library(MASS)
> t.test(Prob ~ So, data=UScrime)

        Welch Two Sample t-test

data:  Prob by So 
t = -3.8954, df = 24.925, p-value = 0.0006506                           
alternative hypothesis: true difference in means is not equal to 0 
95 percent confidence interval:
 -0.03852569 -0.01187439 
sample estimates:
mean in group 0 mean in group 1 
     0.03851265      0.06371269

你可以拒绝南方各州和非南方各州被监禁概率相等的假设(p < .001)。

注意:因为因变量是比例,你可能尝试在执行 t 检验之前将其转换为正态分布。在当前情况下,所有合理的因变量转换(Y/1-Ylog(Y/1-Y)arcsin(Y),和 arcsin(sqrt(Y)))都会得出相同的结论。第八章详细介绍了转换。

7.4.2 相关 t 检验

作为第二个例子,你可能想知道年轻男性(14–24 岁)的失业率是否高于老年男性(35–39 岁)。在这种情况下,两组并不独立。你不会期望阿拉巴马州年轻和老年男性的失业率无关。当两组的观测值相关时,你有一个相关组设计。前后比较或重复测量设计也会产生相关组。

相关 t 检验假设组间差异呈正态分布。在这种情况下,格式为

t.test(*y1*, *y2*, paired=TRUE) 

其中 y1y2 是两个相关组的数值向量。结果如下:

> library(MASS)
> sapply(UScrime[c("U1","U2")], function(x)(c(mean=mean(x),sd=sd(x))))
       U1    U2
mean 95.5 33.98                
sd   18.0  8.45

> with(UScrime, t.test(U1, U2, paired=TRUE))

        Paired t-test

data:  U1 and U2 
t = 32.4066, df = 46, p-value < 2.2e-16
alternative hypothesis: true difference in means is not equal to 0 
95 percent confidence interval:
 57.67003 65.30870 
sample estimates:
mean of the differences 
               61.48936

均值差异(61.5)足够大,足以拒绝老年和年轻男性失业率均值相同的假设。年轻男性的失业率更高。实际上,如果总体均值相等,获得如此大的样本差异的概率小于 0.00000000000000022(即,2.2e–16)。

7.4.3 当有超过两个组时

如果你想比较超过两组,你可以假设数据是从正态总体中独立采样的,可以使用方差分析(ANOVA)。ANOVA 是一种涵盖许多实验和准实验设计的综合方法,因此它拥有自己的章节。你可以随时放弃这一节,跳转到第九章。

7.5 组间差异的非参数检验

如果你无法满足 t 检验或方差分析(ANOVA)的参数假设,你可以转向非参数方法。例如,如果结果变量严重偏斜或具有序数性质,你可能希望使用本节中的技术。

7.5.1 比较两组

如果两组是独立的,你可以使用 Wilcoxon 秩和检验(更通俗地称为 Mann-Whitney U 检验)来评估观察值是否来自相同的概率分布(即,一个群体中获得更高分数的概率是否大于另一个群体)。格式可以是

wilcox.test(*Y* ~ *X*, *data*) 

其中y是数值型,x是二元变量,或者

wilcox.test(*y1*, *y2*) 

其中y1y2是每个组的输出变量。可选的data参数指的是包含变量的矩阵或数据框。默认是双尾检验。你可以添加exact选项以产生精确检验,以及-alternative="less"alternative="greater"来指定方向性检验。

如果你将 Mann-Whitney U 检验应用于上一节中的监禁率问题,你会得到以下结果:

> with(UScrime, by(Prob, So, median))

So: 0
[1] 0.0382
-------------------- 
So: 1
[1] 0.0556

> wilcox.test(Prob ~ So, data=UScrime)

        Wilcoxon rank sum test

data:  Prob by So 
W = 81, p-value = 8.488e-05 
alternative hypothesis: true location shift is not equal to 0

再次,你可以拒绝南部和非南部各州监禁率相同的假设(p < .001)。

Wilcoxon 符号秩检验为相关样本 t 检验提供了一个非参数替代方案。在组别配对且正态性假设不成立的情况下适用。其格式与 Mann-Whitney U 检验相同,但需要添加paired=TRUE选项。让我们将其应用于上一节中的失业问题:

> sapply(UScrime[c("U1","U2")], median)
U1 U2 
92 34 

> with(UScrime, wilcox.test(U1, U2, paired=TRUE))

        Wilcoxon signed rank test with continuity correction

data:  U1 and U2 
V = 1128, p-value = 2.464e-09                                       
alternative hypothesis: true location shift is not equal to 0 

再次,你得出与配对 t 检验相同的结论。

在这种情况下,参数 t 检验及其非参数等价检验得出相同的结论。当 t 检验的假设合理时,参数检验更有效(如果存在差异,更有可能发现)。当假设明显不合理时(例如,等级排序数据),非参数检验更合适。

7.5.2 比较超过两组

当你比较超过两个组时,你必须转向其他方法。考虑 7.3 节中的 state.x77 数据集。它包含美国各州的人口、收入、文盲率、预期寿命、谋杀率和高中毕业率数据。如果你想比较该国四个地区(东北、南、北中、西)的文盲率怎么办?这被称为 单因素设计,对于解决这个问题,既有参数方法也有非参数方法。

如果无法满足 ANOVA 设计的假设,可以使用非参数方法来评估组间差异。如果组是独立的,Kruskal-Wallis 检验是一种有用的方法。如果组是相关的(例如,重复测量或随机区组设计),则 Friedman 检验更为合适。

Kruskal-Wallis 检验的格式为

kruskal.test(*y ~ A*, *data*)

其中 y 是数值结果变量,A 是具有两个或更多级别的分组变量(如果有两个级别,则等同于 Mann-Whitney U 检验)。对于 Friedman 检验,格式为

friedman.test(*y ~ A* | *B, data*)

其中 y 是数值结果变量,A 是分组变量,B 是识别匹配观察值的分组变量。在这两种情况下,data 是一个可选参数,指定包含变量的矩阵或数据框。

让我们应用 Kruskal-Wallis 检验来解决文盲问题。首先,你必须将地区标识符添加到数据集中。这些标识符包含在 R 基础安装中提供的 state.region 数据集中:

states <- data.frame(state.region, state.x77)

现在你可以应用这个测试:

> kruskal.test(Illiteracy ~ state.region, data=states)
        Kruskal-Wallis rank sum test
data:  states$Illiteracy by states$state.region 
Kruskal-Wallis chi-squared = 22.7, df = 3, p-value = 4.726e-05    

显著性检验表明,该国的四个地区(东北、南、北中、西)的文盲率并不相同(p <.001)。

虽然你可以拒绝无差异的零假设,但这个检验并不能告诉你 哪些 地区之间存在显著差异。为了回答这个问题,你可以使用 Wilcoxon 检验一次比较两组。一种更优雅的方法是应用多重比较程序,该程序在控制 I 类错误率(发现不存在差异的概率)的同时计算所有成对比较。我已经创建了一个名为 wmc() 的函数,可用于此目的。它使用 Wilcoxon 检验一次比较两组,并使用 p.adj() 函数调整概率值。

说实话,我在章节标题中相当夸张地使用了 基本 的定义,但因为这个函数非常适合这里,我希望你能理解。你可以从 rkabacoff.com/RiA/wmc.R 下载包含 wmc() 的文本文件。以下列表使用此函数比较了四个美国地区的文盲率。

列表 7.15 非参数多重比较

> source("https://rkabacoff.com/RiA/wmc.R")            ❶
> states <- data.frame(state.region, state.x77)
> wmc(Illiteracy ~ state.region, data=states, method="holm")

Descriptive Statistics                                 ❷

        West North Central Northeast South
n      13.00         12.00       9.0 16.00
median  0.60          0.70       1.1  1.75
mad     0.15          0.15       0.3  0.59

Multiple Comparisons (Wilcoxon Rank Sum Tests)         ❸
Probability Adjustment = holm

        Group.1       Group.2  W       p    
1          West North Central 88 8.7e-01    
2          West     Northeast 46 8.7e-01    
3          West         South 39 1.8e-02   *
4 North Central     Northeast 20 5.4e-02   .
5 North Central         South  2 8.1e-05 ***
6     Northeast         South 18 1.2e-02   *
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

❶ 访问函数

❷ 基本统计

❸ 成对比较

source()函数下载并执行定义wmc()函数的 R 脚本❶。该函数的格式为wmc(``y ~ A``, data``, method``),其中y是数值结果变量,A是分组变量,data是包含这些变量的数据框,而method是用于限制 I 类错误的途径。列表 7.15 使用 Holm(1979)开发的一种调整方法,该方法提供了对家族错误率(在一系列比较中犯一个或多个 I 类错误的概率)的强控制。有关其他方法的描述,请参阅help(p.adjust)

wmc()函数首先为每个组提供样本大小、中位数和中位数绝对偏差❷。西方地区的文盲率最低,而南方地区最高。然后该函数生成六个统计比较(西部与中部北部、西部与东北部、西部与南部、中部北部与东北部、中部北部与南部,以及东北部与南部)❸。你可以从双尾 p 值(p)中看出,南部与其他三个地区存在显著差异,而其他三个地区在 p < .05 水平上彼此之间没有差异。

7.6 可视化组间差异

在第 7.4 节和第 7.5 节中,我们探讨了比较组之间的统计方法。检查组间差异的视觉表现也是全面数据分析策略的一个关键部分。它允许你评估差异的大小,识别任何影响结果分布特征的因素(例如偏斜、双峰或异常值),并评估测试假设的适当性。R 提供了广泛的图形方法来比较组,包括在第 6.6 节中介绍的箱线图(简单、带缺口和小提琴图),在第 6.5 节中介绍的重叠核密度图,以及在第九章中讨论的用于在方差分析框架中可视化结果的图形方法。

摘要

  • 描述性统计可以以数值方式描述定量变量的分布。R 中的许多包都提供了数据框的描述性统计。包的选择主要是一个个人偏好的问题。

  • 频率表和交叉表总结了分类变量的分布。

  • t 检验和 Mann-Whitney U 检验可以用来比较两个组在定量结果上的差异。

  • 可以使用卡方检验来评估两个分类变量之间的关联。相关系数用于评估两个定量变量之间的关联。

  • 数据可视化通常应伴随数值摘要和统计测试。否则,你可能会错过数据的重要特征。

第三部分:中级方法

虽然本书的第二部分涵盖了基本的图形和统计方法,但第三部分讨论了中级方法。在第八章中,我们从描述两个变量之间的关系转向使用回归模型来模拟数值结果变量与一组数值和/或分类预测变量之间的关系。建模数据通常是一个复杂、多步骤、交互的过程。第八章逐步介绍了拟合线性模型、评估其适用性和解释其意义的方法。

第九章通过方差分析和其变体来考虑基本实验和准实验设计的分析。我们感兴趣的是治疗组合或条件如何影响数值结果变量。本章介绍了在 R 中用于执行方差分析、协方差分析、重复测量方差分析、多因素方差分析和多元方差分析的函数。它还讨论了评估这些分析适用性的方法和可视化结果的方法。

在设计实验和准实验研究时,确定样本量是否足够以检测感兴趣的效果(功效分析)是很重要的。否则,为什么要进行研究?第十章详细介绍了功效分析。从假设检验的讨论开始,演示重点介绍了如何使用 R 函数确定检测给定大小治疗效果的必要样本量,并具有给定的置信度。这可以帮助您规划可能产生有用结果的研究。

第十一章在第六章的基础上扩展了材料,通过介绍创建图表来帮助您可视化两个或更多变量之间的关系。这包括各种类型的 2D 和 3D 散点图、散点图矩阵、线图和气泡图。它还介绍了非常有用但不太为人所知的 corrgrams 和 mosaic plots。

第八章和第九章中描述的线性模型假设结果或响应变量不仅是数值的,而且是从正态分布中随机抽取的。在某些情况下,这种分布假设是不可行的。第十二章介绍了在数据是从未知或混合分布中抽取的、样本量较小、异常值是一个问题,或者基于理论分布设计适当的测试在数学上难以处理的情况下,效果良好的分析方法。它们包括重采样和 bootstrap 方法——这些在 R 中强大实现的计算密集型方法。本章描述的方法将使您能够为不符合传统参数假设的数据设计假设检验。

完成第三部分后,你将拥有分析实践中遇到的大多数常见数据分析问题的工具。你还将能够创建一些精美的图表!

8 回归

本章涵盖

  • 配置和解释线性模型

  • 评估模型假设

  • 在竞争模型中选择

在许多方面,回归分析是统计学的核心。这是一个广泛的术语,用于描述一组用于从一个或多个预测变量(也称为独立或解释变量)预测响应变量(也称为依赖、标准或结果变量)的方法。一般来说,回归分析可以用来识别与响应变量相关的解释变量,描述涉及的关系形式,并提供一个从解释变量预测响应变量的方程。

例如,一位运动生理学家可能会使用回归分析来开发一个预测人在跑步机上锻炼时预期燃烧的卡路里数量的方程。响应变量是燃烧的卡路里(从消耗的氧气量计算得出),预测变量可能包括锻炼持续时间(分钟)、在目标心率下花费的时间百分比、平均速度(英里/小时)、年龄(年)、性别和体质指数(BMI)。

从理论的角度来看,分析将有助于回答以下问题:

  • 锻炼持续时间与燃烧的卡路里之间有什么关系?它是线性的还是曲线的?例如,在某个点之后,锻炼对燃烧的卡路里的影响是否会减少?

  • 努力(在目标心率的时间百分比,平均行走速度)如何影响?

  • 这些关系对年轻人和老年人、男性和女性、胖人和瘦人是否相同?

从实际的角度来看,分析将有助于回答以下问题:

  • 一个 BMI 为 28.7 的 30 岁男性,如果他以每小时 4 英里的平均速度行走 45 分钟,并且 80%的时间保持在目标心率范围内,他可以预期燃烧多少卡路里?

  • 你需要收集多少变量才能准确预测一个人在行走时燃烧的卡路里数量?

  • 你的预测将有多准确?

由于回归分析在现代统计学中扮演着如此核心的角色,我们将在本章中对其进行深入探讨。首先,我们将探讨如何配置和解释回归模型。接下来,我们将回顾一系列用于识别这些模型潜在问题及其解决方法的技术。第三,我们将探讨变量选择的问题。在所有潜在的预测变量中,你如何决定哪些变量应该包含在你的最终模型中?第四,我们将讨论一般化的问题。当你将模型应用于现实世界时,它将如何表现?最后,我们将考虑相对重要性。在你的模型中,哪个预测变量最重要,第二个最重要的,以及最不重要的?

如您所见,我们覆盖了大量的内容。有效的回归分析是一个交互式、整体的过程,包含许多步骤,并且需要相当多的技巧。而不是将其拆分成多个章节,我选择在一个章节中呈现这个主题,以捕捉这种风味。因此,这将是有史以来最长和最复杂的章节。坚持到最后,您将拥有解决各种研究问题所需的所有工具。我保证!

8.1 回归的多种面貌

术语“回归”可能会令人困惑,因为存在许多专门的品种(见表 8.1)。此外,R 具有强大的全面功能来拟合回归模型,众多的选项可能会令人困惑。例如,在 2005 年,Vito Ricci 创建了一个包含超过 205 个 R 函数的列表,这些函数用于生成回归分析(mng.bz/NJhu)。

表 8.1 回归分析种类

回归类型 典型用途
简单线性 从一个定量解释变量预测定量响应变量
多项式 从一个定量解释变量预测定量响应变量,其中关系被建模为 n 次多项式
多元线性 从两个或更多解释变量预测定量响应变量
多层 从具有层次结构的数据预测响应变量(例如,学校内的班级内的学生)。也称为层次、嵌套或混合模型。
多变量 从一个或多个解释变量预测多个响应变量
逻辑 从一个或多个解释变量预测分类响应变量
泊松 从一个或多个解释变量预测表示计数的响应变量
Cox 比例风险 从一个或多个解释变量预测事件发生的时间(死亡、故障、复发)
时间序列 使用相关误差对时间序列数据进行建模
非线性 从一个或多个解释变量预测定量响应变量,其中模型的形式是非线性的
非参数 从一个或多个解释变量预测定量响应变量,其中模型的形式是从数据中推导出来的,而不是事先指定的
鲁棒 使用一种对有影响观测值效应具有抵抗力的方法从一个或多个解释变量预测定量响应变量

在本章中,我们将关注属于普通最小二乘法(OLS)回归范畴的回归方法,包括简单线性回归、多项式回归和多元线性回归。第十三章将涵盖其他类型的回归模型,包括逻辑回归和泊松回归。

8.1.1 使用 OLS 回归的场景

在 OLS 回归中,定量因变量是从预测变量的加权和中预测出来的,其中权重是从数据中估计的参数。让我们看看一个具体的例子(无意中提到),这个例子是从 Fwa(2006 年)那里松散改编的。

工程师希望识别与桥梁退化(如年龄、交通量、桥梁设计、建筑材料和方法、施工质量和天气条件)相关的最重要因素,并确定这些关系的数学形式。她从代表性桥梁样本中收集了这些变量的数据,并使用 OLS 回归模型对这些数据进行建模。

该方法高度互动。她拟合了一系列模型,检查它们是否符合潜在统计假设,探索任何意外或异常发现,并最终从众多可能模型中选择“最佳”模型。如果成功,结果将帮助她

  • 通过确定众多收集到的变量中哪些对预测桥梁退化有用,以及它们的相对重要性,来关注重要变量。

  • 通过提供一个可以用于预测新案例(预测变量的值已知,但桥梁退化的程度未知)的桥梁退化预测方程,寻找可能陷入麻烦的桥梁。

  • 通过识别不寻常的桥梁来利用偶然性。如果她发现某些桥梁的退化速度比模型预测的要快或慢得多,对这些异常值的研究可能会得出重要的发现,这有助于她理解桥梁退化的机制。

桥梁可能对你没有吸引力。我是一个临床心理学家和统计学家,我对土木工程知之甚少。但一般原则适用于物理、生物和社会科学中惊人的广泛问题。以下每个问题也可以使用 OLS 方法来解决:

  • 地表径流盐度和铺砌道路表面积之间的关系是什么?(Montgomery,2007 年)

  • 用户体验的哪些方面导致了大型多人在线角色扮演游戏(MMORPGs)的过度使用?(Hsu,Wen 和 Wu,2009 年)

  • 哪些教育环境的质量与更高的学生成绩分数最密切相关?

  • 血压、盐摄入量和年龄之间的关系形式是什么?这对男性和女性是否相同?

  • 体育场和专业运动对大都市区发展的影响是什么?(Baade 和 Dye,1990 年)

  • 什么因素导致了州际间啤酒价格的差异?(Culbertson 和 Bradford,1991 年)(这个问题引起了你的注意!)

我们的主要限制是我们提出有趣问题、设计有用的响应变量来衡量以及收集适当数据的能力。

8.1.2 你需要知道什么

在本章的剩余部分,我将描述如何使用 R 函数来拟合 OLS 回归模型,评估拟合度,检验假设,并在竞争模型之间进行选择。我假设你已经接触过通常在大学二年级统计学课程中教授的最小二乘回归。但我已经努力将数学符号保持在最低限度,并关注实际问题而不是理论问题。有几本优秀的教材涵盖了本章概述的统计材料。我最喜欢的是 John Fox 的 应用回归分析和广义线性模型(2008 年版)(用于理论)和 R 和 S-Plus 应用回归指南(2002 年版)(用于应用)。它们都为本章的主要来源。Licht(1995 年)提供了一个很好的非技术性概述。

8.2 OLS 回归

在本章的大部分内容中,我们将使用 OLS 从一组预测变量(也称为将响应变量回归到预测变量——因此得名)预测响应变量。OLS 回归拟合形式为,其中 n 是观测数,k 是预测变量的数量。(尽管我已经尽力将方程式排除在这些讨论之外,但这确实是简化事情的地方之一。)在这个方程中:

  • 是第 i 个观测值的因变量预测值(具体来说,它是基于预测值集合的 Y 分布的估计均值)。

  • 是第 i 个观测值的第 j 个预测值。

  • 是截距(当所有预测变量都等于零时 Y 的预测值)。

  • 是第 j 个预测值的回归系数(表示 X[j] 单位变化时 Y 的变化)。

我们的目标是选择模型参数(截距和斜率)以最小化实际响应值与模型预测值之间的差异。具体来说,模型参数的选择是为了最小化残差平方和:

正确解释 OLS 模型的系数,你必须满足一系列统计假设:

  • 正态性—对于固定的自变量值,因变量呈正态分布。

  • 独立性Y[i] 值彼此独立。

  • 线性关系—因变量与自变量呈线性关系。

  • 同方差性—因变量的方差不随独立变量水平的改变而改变。(我本可以称之为常数方差,但使用同方差性这个词让我感觉更聪明。)

如果违反这些假设,你的统计显著性检验和置信区间可能不准确。请注意,OLS 回归还假设独立变量是固定的且无误差测量,但在实践中通常放宽此假设。

8.2.1 使用 lm() 拟合回归模型

在 R 中,拟合线性模型的基本函数是 lm()。其格式为

myfit <- lm(*formula, data*)

其中 formula 描述要拟合的模型,而 data 是包含用于拟合模型的数据的数据框。结果对象(在本例中为 myfit)是一个包含有关拟合模型大量信息的列表。公式通常写作

Y ~ X1 + X2 + ... + Xk

其中 ~ 将左侧的响应变量与右侧的预测变量分开,预测变量由加号分隔。其他符号可以以各种方式修改公式(见表 8.2)。

表 8.2 R 公式中常用符号

符号 用法
~ 将左侧的响应变量与右侧的解释变量分开。例如,从 xzw 预测 y 的代码为 y ~ x + z + w
+ 分隔预测变量
: 表示预测变量之间的交互。从 xzxz 之间的交互预测 y 的代码为 y ~ x + z + x:z
* 表示所有可能交互的快捷方式。代码 y ~ x * z * w 展开为 y ~ x + z + w + x:z + x:w + z:w + x:z:w
^ 表示直到指定程度的交互。代码 y ~ (x + z + w)² 展开为 y ~ x + z + w + x:z + x:w + z:w
. 数据框中除因变量外的所有其他变量的占位符。例如,如果数据框包含变量 xyzw,则代码 y ~ 会展开为 y ~ x + z + w
- 减号从方程中删除变量。例如,y ~ (x + z + w)² x:w 展开为 y ~ x + z + w + x:z + z:w
-1 抑制截距。例如,公式 y ~ x -1 拟合 yx 的回归,并强制直线通过原点 x=0
I() 括号内的元素按算术方式解释。例如,y ~ x + (z + w)² 展开为 y ~ x + z + w + z:w。相比之下,代码 y ~ x + I((z + w)²) 展开为 y ~ x + h,其中 h 是通过平方 zw 的和创建的新变量。
function 公式中可以使用数学函数。例如,log(y) ~ x + z + wxzw 预测 log(y)

除了lm()之外,表 8.3 还列出了在生成简单或多元回归分析时有用的几个函数。这些函数中的每一个都是应用于lm()返回的对象,以基于该拟合模型生成更多信息。

表 8.3 在拟合线性模型时有用的其他函数

函数 操作
summary() 显示拟合模型的详细结果
coefficients() 列出拟合模型的模型参数(截距和斜率)
confint() 提供模型参数的置信区间(默认为 95%)
fitted() 列出拟合模型中的预测值
residuals() 列出拟合模型中的残差值
anova() 为拟合模型生成 ANOVA 表或比较两个或多个拟合模型的 ANOVA 表
vcov() 列出模型参数的协方差矩阵
AIC() 打印赤池信息准则
plot() 生成用于评估模型拟合的诊断图
predict() 使用拟合模型对新数据集的响应值进行预测

当回归模型包含一个因变量和一个自变量时,这种方法被称为简单线性回归。当只有一个预测变量但包含变量的幂(例如,X)时,它被称为多项式回归。当有多个预测变量时,它被称为多元线性回归。我们将从一个简单线性回归的例子开始,然后过渡到多项式和多元线性回归的例子,最后以一个包含预测变量之间交互作用的多元回归例子结束。

8.2.2 简单线性回归

让我们通过一个简单的回归示例来查看表 8.3 中的函数。基础安装中的women数据集提供了 15 名 30 至 39 岁女性的身高和体重。假设你想根据身高预测体重。拥有从身高预测体重的方程可以帮助你识别超重或体重不足的个人。以下列表提供了分析,图 8.1 显示了结果图。

列表 8.1 简单线性回归

> fit <- lm(weight ~ height, data=women)
> summary(fit)

Call:
lm(formula=weight ~ height, data=women)

Residuals:
   Min     1Q Median     3Q    Max 
-1.733 -1.133 -0.383  0.742  3.117 

Coefficients:                                                   
            Estimate Std. Error t value Pr(>|t|)    
(Intercept) -87.5167     5.9369   -14.7  1.7e-09 ***
height        3.4500     0.0911    37.9  1.1e-14 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 '' 1 

Residual standard error: 1.53 on 13 degrees of freedom
Multiple R-squared: 0.991,      Adjusted R-squared: 0.99 
F-statistic: 1.43e+03 on 1 and 13 DF,  p-value: 1.09e-14 

> women$weight

 [1] 115 117 120 123 126 129 132 135 139 142 146 150 154 159 164

> fitted(fit)

     1      2      3      4      5      6      7      8      9 
112.58 116.03 119.48 122.93 126.38 129.83 133.28 136.73 140.18 
    10     11     12     13     14     15 
143.63 147.08 150.53 153.98 157.43 160.88 

> residuals(fit)

    1     2     3     4     5     6     7     8     9    10    11 
 2.42  0.97  0.52  0.07 -0.38 -0.83 -1.28 -1.73 -1.18 -1.63 -1.08 
   12    13    14    15 
-0.53  0.02  1.57  3.12

> plot(women$height,women$weight, 
       xlab="Height (in inches)", 
       ylab="Weight (in pounds)")
> abline(fit)

图 8.1 从身高预测体重的散点图和回归线

从输出中,你可以看到预测方程是

体重 = −87.52 + 3.45×身高

由于身高为 0 是不可能的,你不会尝试对截距给出物理解释。它仅仅成为一个调整常数。从 Pr(>|t|) 列中,你可以看到回归系数(3.45)与零显著不同(p < 0.001),这表明每增加 1 英寸身高,预期体重将增加 3.45 磅。多重 R 平方(0.991)表明该模型解释了体重变化的 99.1%。多重 R 平方也是实际值和预测值之间的相关系数的平方(即 R² = r[ŷv])。残差标准误差(1.53 磅)可以被视为使用此模型从身高预测体重的平均误差。F 统计量测试预测变量是否一起预测响应变量高于随机水平。由于简单回归中只有一个预测变量,在这个例子中,F 测试等同于对高度回归系数的 t 测试。

为了演示目的,我们打印出了实际值、预测值和残差值。显然,最大残差出现在身高低和高的地方,这也可以在图中(图 8.1)看到。

图表明,你可能可以通过使用一条有弯曲的线来提高预测。例如,形式为

可能会更好地拟合数据。多项式回归允许你从一个解释变量预测响应变量,其中关系的形式是 n 次多项式。

8.2.3 多项式回归

图 8.1 中的图表明,你可能可以通过使用带有二次项的回归(即 X²)来提高你的预测。你可以使用以下语句拟合二次方程

fit2 <- lm(weight ~ height + I(height²), data=women)

新术语 I(height²) 需要解释。height² 将一个高度平方项添加到预测方程中。I() 函数将括号内的内容视为一个 R 表达式。你需要这样做,因为 ^ 运算符在公式中有特殊含义,你不想在这里调用它(见表 8.2)。

下面的列表显示了拟合二次方程的结果。

列表 8.2 多项式回归

> fit2 <- lm(weight ~ height + I(height²), data=women)
> summary(fit2)

Call:
lm(formula=weight ~ height + I(height²), data=women)

Residuals:
    Min      1Q  Median      3Q     Max 
-0.5094 -0.2961 -0.0094  0.2862  0.5971 

Coefficients:
             Estimate Std. Error t value Pr(>|t|)    
(Intercept) 261.87818   25.19677   10.39  2.4e-07 ***
height       -7.34832    0.77769   -9.45  6.6e-07 ***
I(height²)   0.08306    0.00598   13.89  9.3e-09 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1 

Residual standard error: 0.384 on 12 degrees of freedom
Multiple R-squared: 0.999,      Adjusted R-squared: 0.999 
F-statistic: 1.14e+04 on 2 and 12 DF,  p-value: <2e-16 

> plot(women$height,women$weight,
       xlab="Height (in inches)",
       ylab="Weight (in lbs)")
> lines(women$height,fitted(fit2))

从这个新的分析中,预测方程是

weight = 261.88 − 7.35×height + 0.083×height²

并且两个回归系数在 p < 0.0001 的水平上都是显著的。解释的方差量增加到了 99.9%。平方项的显著性(t = 13.89,p < .001)表明包含二次项提高了模型拟合度。如果你看 fit2 的图(图 8.2),你可以看到曲线确实提供了更好的拟合。

图 8.2 根据身高预测的重量二次回归

线性模型与非线性模型

注意,这个多项式方程仍然属于线性回归的范畴。它是线性的,因为方程涉及预测变量的加权总和(在这个例子中是身高和身高的平方)。即使是如下模型

被认为是参数意义上的线性模型,并使用以下公式进行拟合

相比之下,这里有一个真正非线性模型的例子:

这种形式的非线性模型可以用 nls() 函数拟合。

通常,一个 n 次方的多项式会产生一个有 n - 1 个弯曲的曲线。要拟合一个三次多项式,你会使用

fit3 <- lm(weight ~ height + I(height²) +I(height³), data=women)

虽然可能存在更高次的多项式,但我很少发现超过三次方的项是必要的。

8.2.4 多元线性回归

当存在多个预测变量时,简单线性回归变为多元线性回归,分析变得更加复杂。从技术上讲,多项式回归是多元回归的一个特例。二次回归有两个预测变量(XX²),三次回归有三个预测变量(XX² 和 X³)。让我们看一个更一般的例子。

我们将使用基础包中的 state.x77 数据集来演示这个例子。假设你想探索一个州的谋杀率与其他特征之间的关系,包括人口、文盲率、平均收入和霜冻水平(低于冰点的天数平均值)。

因为 lm() 函数需要一个数据框(并且 state.x77 数据集包含在一个矩阵中),你可以用以下代码简化你的生活:

states <- as.data.frame(state.x77[,c("Murder", "Population", 
                        "Illiteracy", "Income", "Frost")])

此代码创建了一个名为 states 的数据框,其中包含你感兴趣的变量。你将在本章的剩余部分使用这个新的数据框。

在多元回归中,一个好的第一步是检查变量之间的关系,一次检查两个变量。双变量相关系数由 cor() 函数提供,散点图由 car 包中的 scatterplotMatrix() 函数生成(见以下列表和图 8.3)。

列表 8.3 检查双变量关系

> states <- as.data.frame(state.x77[,c("Murder", "Population", 
                          "Illiteracy", "Income", "Frost")])

> cor(states)
           Murder Population Illiteracy Income Frost
Murder       1.00       0.34       0.70  -0.23 -0.54
Population   0.34       1.00       0.11   0.21 -0.33
Illiteracy   0.70       0.11       1.00  -0.44 -0.67
Income      -0.23       0.21      -0.44   1.00  0.23
Frost       -0.54      -0.33      -0.67   0.23  1.00

> library(car)
> scatterplotMatrix(states, smooth=FALSE, main="Scatter Plot Matrix")

图 8.3 states 数据的因变量和自变量的散点矩阵,包括线性拟合和平滑拟合,以及边缘分布(核密度图和地毯图)

默认情况下,scatterplotMatrix() 函数在非对角线位置提供变量的散点图,并在这些图上叠加平滑(loess)和线性拟合线。主对角线包含每个变量的密度和地毯图。通过 smooth=FALSE 参数抑制平滑线。

您可以看到谋杀率可能是双峰的,并且每个预测变量都有一定程度上的偏斜。谋杀率随着人口和文盲率的增加而上升,随着收入水平和霜冻的减少而下降。同时,较冷州的文盲率较低,人口较少,收入较高。

现在让我们使用 lm() 函数拟合多元回归模型。

列表 8.4 多元线性回归

> states <- as.data.frame(state.x77[,c("Murder", "Population", 
                          "Illiteracy", "Income", "Frost")])

> fit <- lm(Murder ~ Population + Illiteracy + Income + Frost, 
            data=states)
> summary(fit)

Call:
lm(formula=Murder ~ Population + Illiteracy + Income + Frost, 
    data=states)

Residuals:
    Min      1Q  Median      3Q     Max 
-4.7960 -1.6495 -0.0811  1.4815  7.6210 

Coefficients:
            Estimate Std. Error t value Pr(>|t|)    
(Intercept) 1.23e+00   3.87e+00    0.32    0.751    
Population  2.24e-04   9.05e-05    2.47    0.017 *  
Illiteracy  4.14e+00   8.74e-01    4.74  2.2e-05 ***
Income      6.44e-05   6.84e-04    0.09    0.925    
Frost       5.81e-04   1.01e-02    0.06    0.954    
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.v 0.1 'v' 1 

Residual standard error: 2.5 on 45 degrees of freedom
Multiple R-squared: 0.567,      Adjusted R-squared: 0.528 
F-statistic: 14.7 on 4 and 45 DF,  p-value: 9.13e-08

当存在多个预测变量时,回归系数表示在保持所有其他预测变量不变的情况下,预测变量单位变化导致的因变量增加。例如,Illiteracy 的回归系数为 4.14,这意味着文盲率增加 1% 与谋杀率增加 4.14% 相关,在控制人口、收入和温度的情况下。该系数在 p < .0001 的水平上与零有显著差异。另一方面,Frost 的系数与零没有显著差异(p = 0.954),这意味着在控制其他预测变量的情况下,FrostMurder 之间没有线性关系。综合来看,预测变量解释了各州谋杀率变异的 57%。

到目前为止,我们假设预测变量之间没有交互作用。在下一节中,我们将考虑它们确实存在交互作用的案例。

8.2.5 带有交互作用的多元线性回归

最有趣的研究发现之一涉及预测变量之间的交互作用。考虑 mtcars 数据框中的汽车数据。假设您对汽车重量和马力对里程的影响感兴趣。您可以拟合一个包含这两个预测变量及其交互作用的回归模型,如下一列表所示。

列表 8.5 带有显著交互项的多元线性回归

> fit <- lm(mpg ~ hp + wt + hp:wt, data=mtcars)
> summary(fit)

Call:
lm(formula=mpg ~ hp + wt + hp:wt, data=mtcars)

Residuals:
   Min     1Q Median     3Q    Max 
-3.063 -1.649 -0.736  1.421  4.551 

Coefficients:
            Estimate Std. Error t value Pr(>|t|)    
(Intercept) 49.80842    3.60516   13.82  5.0e-14 ***
hp          -0.12010    0.02470   -4.86  4.0e-05 ***
wt          -8.21662    1.26971   -6.47  5.2e-07 ***
hp:wt        0.02785    0.00742    3.75  0.00081 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1 

Residual standard error: 2.1 on 28 degrees of freedom
Multiple R-squared: 0.885,      Adjusted R-squared: 0.872 
F-statistic: 71.7 on 3 and 28 DF,  p-value: 2.98e-13

您可以从 Pr(>|t|) 列中看到,马力和汽车重量之间的交互作用是显著的。这意味着什么?两个预测变量之间的显著交互作用表明,一个预测变量与响应变量之间的关系取决于另一个预测变量的水平。在这里,这意味着每加仑行驶里程与马力之间的关系取决于汽车重量。

预测mpg的模型是mpg = 49.81 – 0.12 × hp – 8.22 × wt + 0.03 × hp × wt。为了解释交互作用,你可以插入不同的wt值并简化方程。例如,你可以尝试wt的均值(3.2)以及均值上下一个标准差(分别为 2.2 和 4.2)。对于wt=2.2,方程简化为mpg = 49.81 – 0.12 × hp – 8.22 × (2.2) + 0.03 × hp × (2.2) = 31.41 – 0.06 × hp。对于wt=3.2,这变为mpg = 23.37 – 0.03 × hp。最后,对于wt=4.2,方程变为mpg = 15.33 – 0.003 × hp。你可以看到,随着重量增加(2.2,3.2,4.2),从单位增加hp带来的mpg的预期变化减少(0.06,0.03,0.003)。

你可以使用effects包中的effect()函数来可视化交互作用。格式是

plot(effect(*term, mod,, xlevels*), multiline=TRUE)

其中term是要绘制的引用模型项,modlm()返回的拟合模型,xlevels是一个指定要设置为常数值的变量及其值的列表。multiline=TRUE选项将叠加正在绘制的线,而lines选项指定每条线的类型(其中 1 = 实线,2 = 虚线,3 = 点线等)。对于前面的模型,这变为

library(effects)
plot(effect("hp:wt", fit,, list(wt=c(2.2,3.2,4.2))), 
     lines=c(1,2,3), multiline=TRUE)

图 8.4 显示了生成的图形。

从这张图中你可以看到,随着汽车重量的增加,马力与每加仑英里数之间的关系减弱。对于wt=4.2,线几乎水平,这表明随着hp的增加,mpg不会改变。

不幸的是,拟合模型只是分析的第一步。一旦你拟合了一个回归模型,在你可以对所得到的推断有信心之前,你需要评估你是否已经满足了你方法背后的统计假设。这是下一节的主题。

图 8.4 hp*wt的交互作用图。此图显示了在三个wt值下mpghp之间的关系。

8.3 回归诊断

在上一节中,你使用了lm()函数来拟合一个 OLS 回归模型,并使用summary()函数来获取模型参数和摘要统计信息。不幸的是,这个打印输出中没有任何东西告诉你你拟合的模型是否合适。你对回归参数推断的信心取决于你满足 OLS 模型统计假设的程度。尽管列表 8.4 中的summary()函数描述了模型,但它没有提供有关满足模型统计假设程度的信息。

这为什么很重要?数据中的不规则性或预测变量与响应变量之间关系的误指定可能导致你选择一个极其不准确的模型。一方面,你可能会得出结论,预测变量和响应变量之间没有关系,而实际上它们是有关系的。另一方面,你可能会得出结论,预测变量和响应变量之间有关系,而实际上它们没有关系。你也可能得到一个在现实世界应用时预测效果不佳的模型,伴随着重大且不必要的误差。

让我们看看应用于第 8.2.4 节中states多重回归问题的confint()函数的输出:

> states <- as.data.frame(state.x77[,c("Murder", "Population", 
                          "Illiteracy", "Income", "Frost")])
> fit <- lm(Murder ~ Population + Illiteracy + Income + Frost, data=states)
> confint(fit)
                2.5 %   97.5 %
(Intercept) -6.55e+00 9.021318
Population   4.14e-05 0.000406
Illiteracy   2.38e+00 5.903874
Income      -1.31e-03 0.001441
Frost       -1.97e-02 0.020830

结果表明,你可以有 95%的信心认为,在文盲率上升 1%的情况下,区间[2.38, 5.90]包含了谋杀率的真实变化。此外,由于Frost的置信区间包含 0,你可以得出结论,温度的变化与谋杀率无关,其他变量保持不变。但你对这些结果的信心强度仅取决于你拥有的证据,证明你的数据满足模型背后的统计假设。

一套称为回归诊断的技术提供了评估回归模型适当性的必要工具,可以帮助你发现并纠正问题。我们将从使用 R 的基础安装中提供的函数的标准方法开始。然后我们将探讨通过car包提供的更新、改进的方法。

8.3.1 一种典型的方法

R 的基础安装提供了许多评估回归分析中统计假设的方法。最常见的方法是将plot()函数应用于lm()函数返回的对象。这样做会产生四个图表,这些图表对于评估模型拟合很有用。将这种方法应用于简单的线性回归示例

fit <- lm(weight ~ height, data=women)
par(mfrow=c(2,2))
plot(fit)
par(mfrow=c(1,1))

产生了图 8.5 所示的图表。par(mfrow=c(2,2))语句用于将plot()函数产生的四个图表组合成一个大的两行两列图表。第二个par()函数将你带回到单个图表。

图片

图 8.5 关于体重对身高的回归诊断图

要理解这些图表,请考虑 OLS 回归的假设:

  • 正态性——如果对于一组固定的预测值,因变量是正态分布的,那么残差值应该以 0 为均值正态分布。正态 Q-Q 图(右上角)是标准化残差与在正态性下预期的值之间的概率图。如果你已经满足了正态性假设,那么这个图上的点应该落在 45 度直线上。因为它们没有,所以你显然违反了正态性假设。

  • 独立性——你不能从这些图中判断依赖变量值是否独立。你必须使用你对数据收集方式的理解。没有先验理由相信一个女性的体重会影响另一个女性的体重。如果你发现数据是从家庭中抽取的,你可能需要调整你的独立性假设。

  • 线性关系——如果依赖变量与独立变量呈线性关系,那么残差与预测值(即拟合值)之间不应存在系统性关系。换句话说,模型应该捕捉数据中存在的所有系统性方差,留下只有随机噪声。在残差与拟合图(左上角)中,你看到明显的曲线关系,这表明你可能需要在回归中添加一个二次项。

  • 同方差性——如果你已经满足了常数方差假设,那么在尺度-位置图(左下角)中的点应该是在水平线周围的一个随机带。你似乎满足了这一假设。

最后,残差与杠杆图(右下角)提供了关于你可能希望关注的单个观测值的信息。该图识别了异常值、高杠杆点和有影响力的观测值。具体来说,

  • 异常值是指那些拟合回归模型预测不佳的观测值(即具有较大的正或负残差)。

  • 具有高杠杆值的观测值具有不寻常的预测值组合。也就是说,它在预测空间中是一个异常值。依赖变量值不用于计算观测值的杠杆值。

  • 有影响力的观测值是指对模型参数确定有不成比例影响的观测值。有影响力的观测值是通过称为 Cook 距离或 Cook D 的统计量来识别的。

老实说,我发现残差与杠杆图难以阅读且没有用。你将在后面的章节中看到这个信息的更好表示。

尽管这些标准诊断图很有帮助,但现在 R 中已经有了更好的工具,我推荐使用它们而不是plot(fit)方法。

8.3.2 一种改进的方法

car包提供了一些函数,这些函数可以显著提高你拟合和评估回归模型的能力(见表 8.4)。

表 8.4 回归诊断的有用函数(car包)

函数 目的
qqPlot() 分位数比较图
durbinWatsonTest() 自相关误差的 Durbin-Watson 测试
crPlots() 组成加残差图
ncvTest() 非常数误差方差得分测试
spreadLevelPlot() 扩散水平图
outlierTest() 博费里尼异常值测试
avPlots() 添加变量图
influencePlot() 回归影响图
vif() 方差膨胀因子

让我们逐一查看它们,通过将它们应用于我们的多元回归示例来应用它们。

正态性

qqPlot() 函数提供了比基础包中 plot() 函数提供的方法更准确的方法来评估正态性假设。它将学生化残差(也称为 学生化删除残差刀切残差)与具有 np – 1 个自由度的 t 分布进行比较,其中 n 是样本大小,p 是回归参数的数量(包括截距)。代码如下:

library(car)
states <- as.data.frame(state.x77[,c("Murder", "Population",
                        "Illiteracy", "Income", "Frost")])
fit <- lm(Murder ~ Population + Illiteracy + Income + Frost, data=states)
qqPlot(fit, labels=row.names(states), id=list(method="identify"),
       simulate=TRUE, main="Q-Q Plot")

qqPlot() 函数生成了图 8.6 所示的概率图。选项 id=list(method="identify") 使得图形交互式——在图形绘制后,鼠标点击图中的点会使用函数 labels 选项中指定的值对其进行标记。按下 Esc 键或图形右上角的完成按钮可以关闭这种交互模式。在这里,我识别了内华达州。当 simulate=TRUE 时,使用参数化自助法生成 95% 置信区间。(自助法在第十二章中讨论。)

图像

图 8.6 学生化残差 Q-Q 图

除了内华达州之外,所有点都接近直线,并且位于置信区间内,这表明你相当好地满足了正态性假设。但你应该肯定地看看内华达州。它有一个大的正残差(实际值减去预测值),表明该模型低估了该州的谋杀率。具体来说,

> states["Nevada",]

       Murder Population Illiteracy Income Frost
Nevada   11.5        590        0.5   5149   188

> fitted(fit)["Nevada"]

  Nevada 
3.878958 

> residuals(fit)["Nevada"]

  Nevada 
7.621042 

> rstudent(fit)["Nevada"]

  Nevada 
3.542929 

在这里,你可以看到谋杀率是 11.5%,但模型预测的谋杀率是 3.9%。你需要问的问题是,“为什么内华达州的谋杀率比根据人口、收入、文盲率和温度预测的要高?”任何(没有看过 Casino)的人想猜一猜吗?

误差独立性

如前所述,评估因变量值(以及因此残差)是否独立的最佳方式是依据你对数据收集方式的了解。例如,时间序列数据通常表现出自相关性——时间上更接近的观测值彼此之间比与时间上较远的观测值更相关。car 包提供了一个用于检测此类序列相关错误的 Durbin-Watson 测试函数。你可以使用以下代码将 Durbin-Watson 测试应用于多重回归问题:

> durbinWatsonTest(fit)
 lag Autocorrelation D-W Statistic p-value
   1          -0.201          2.32   0.282
 Alternative hypothesis: rho != 0

非显著 p 值(p = 0.282)表明不存在自相关性,反之,误差是独立的。滞后值(本例中为 1)表示每个观测值正在与数据集中相邻的观测值进行比较。尽管适用于时间依赖性数据,但对于这种方式的集群数据,该测试的适用性较低。请注意,durbinWatsonTest() 函数使用自助法(见第十二章)来推导 p 值。除非你添加选项 simulate=FALSE,否则每次运行测试时都会得到一个略有不同的值。

线性

你可以通过使用成分加残差图(也称为部分残差图)来寻找依赖变量与独立变量之间关系的非线性证据。该图由car包中的crPlots()函数生成。你正在寻找任何与指定的线性模型有系统的偏离。

要为变量k创建一个成分加残差图,你绘制以下点

其中残差基于完整模型(包含所有预测因子),且i = 1 ... n。每个图中直线由给出。每个图还提供了一个局部加权回归线(loess 线,一种平滑的非参数拟合线)。第十一章将描述 loess 线。生成这些图的代码如下:

> library(car)
> crPlots(fit)

图 8.7 提供了结果图。这些图中的任何非线性都表明,你可能没有充分地模拟该预测因子的函数形式在回归中。如果是这样,你可能需要添加曲线成分,如多项式项,变换一个或多个变量(例如,使用log(X)而不是X),或者放弃线性回归,转而使用其他回归变体。变换将在本章后面讨论。

图 8.7 回归中谋杀率与州特征成分加残差图

成分加残差图确认你已经满足了线性假设。线性模型的形态似乎适用于这个数据集。

同方差性

car包还提供了两个用于识别非恒定误差方差的实用函数。ncvTest()函数产生一个关于恒定误差方差假设的得分检验,与误差方差随拟合值水平变化的备择假设相对。显著结果表明异方差性(非恒定误差方差)。

spreadLevelPlot()函数创建一个绝对标准化残差与拟合值的散点图,并叠加最佳拟合线。这两个函数将在下一列表中演示。

列表 8.6 评估同方差性

> library(car)
> ncvTest(fit)

Non-constant Variance Score Test 
Variance formula: ~ fitted.values 
Chisquare=1.7    Df=1     p=0.19 

> spreadLevelPlot(fit)

Suggested power transformation:  1.2 

分数检验是非显著的(p = 0.19),这表明您已经满足了常数方差假设。您也可以在散点图水平(图 8.8)中看到这一点。点围绕最佳拟合线的水平线形成一个随机的水平带。如果您违反了这一假设,您会期望看到一条非水平线。列表 8.6 中建议的幂变换是建议的幂 p (Y^p),它将稳定非常数误差方差。例如,如果图显示了非水平趋势,并且建议的幂变换是 0.5,那么在回归方程中使用√Y而不是Y可能会导致满足同方差性的模型。如果建议的幂是 0,您将使用对数变换。在当前示例中,没有异方差性的证据,建议的幂接近 1(不需要变换)。

图片

图 8.8 用于评估常数误差方差的散点图水平

8.3.3 多重共线性

在离开回归诊断这一节之前,让我们关注一个与统计假设无直接关系但有助于您解释多元回归结果的问题。想象一下,您正在进行一项关于握力的研究。您的自变量包括出生日期(DOB)和年龄。您将握力对 DOB 和年龄进行回归,并发现 F 检验在 p < .001 时具有显著的整体效果。但当您查看 DOB 和年龄的个体回归系数时,您发现它们都是非显著的(也就是说,没有证据表明它们与握力相关)。发生了什么?

问题在于 DOB 和年龄在舍入误差范围内完全相关。回归系数衡量一个预测变量对响应变量的影响,同时保持所有其他预测变量不变。这相当于在保持年龄不变的情况下观察握力和年龄的关系。这个问题被称为多重共线性。它导致模型参数的置信区间很大,使得对单个系数的解释变得困难。

可以使用一个称为方差膨胀因子(VIF)的统计量来检测多重共线性。对于任何预测变量,VIF 的平方根表示该变量的回归参数置信区间相对于无相关预测变量的模型扩展的程度(因此得名)。VIF 值由car包中的vif()函数提供。一般来说,VIF > 10 表示存在多重共线性问题。以下列表提供了代码。结果表明,这些预测变量不存在多重共线性问题。

列表 8.7 评估多重共线性

> library(car)
> vif(fit) 

Population Illiteracy     Income      Frost 
       1.2        2.2        1.3        2.1 

> vif(fit) > 10 # problem?

Population Illiteracy     Income      Frost 
     FALSE      FALSE      FALSE      FALSE

8.4 不寻常的观测值

综合回归分析还将包括对异常观测值的筛选——异常值、高杠杆观测值和有影响力的观测值。这些数据点需要进一步调查,要么是因为它们在某种程度上与其他观测值不同,要么是因为它们对结果产生了不成比例的影响。让我们逐一查看。

8.4.1 异常值

异常值是模型预测不佳的观测值。它们具有异常大的正或负残差(Y[i]Ŷ[i])。正残差表示模型低估了响应值,而负残差表示高估。

你已经看到了一种识别异常值的方法。图 8.6 的 Q-Q 图中位于置信带之外的点被认为是异常值。一个粗略的规则是,标准化残差大于 2 或小于-2 的值得注意。

car 包还提供了一种用于异常值的统计测试。outlierTest() 函数报告最大绝对学生化残差的 Bonferroni 调整后的 p 值:

  > library(car) 
  > outlierTest(fit)
       rstudent unadjusted p-value Bonferroni p
Nevada      3.5            0.00095        0.048

在这里,你可以看到内华达州被识别为异常值(p = 0.048)。请注意,此函数测试单个最大(正或负)残差是否作为异常值具有显著性。如果不显著,数据集中没有异常值。如果显著,你必须删除它并重新运行测试,以查看是否存在其他异常值。

8.4.2 高杠杆点

具有高杠杆作用的观测值是相对于其他预测因子的异常值。换句话说,它们具有不寻常的预测值组合。响应值不涉及确定杠杆。

通过帽子统计量识别具有高杠杆作用的观测值。对于给定的数据集,平均帽子值是 p/n,其中 p 是模型中估计的参数数量(包括截距)和 n 是样本大小。粗略地说,帽子值大于平均帽子值两倍或三倍的观测值应该进行检查。下面的代码绘制了 hat 值:

hat.plot <- function(fit) {
              p <- length(coefficients(fit))
              n <- length(fitted(fit))
              plot(hatvalues(fit), main="Index Plot of Hat Values")
              abline(h=c(2,3)*p/n, col="red", lty=2)
              identify(1:n, hatvalues(fit), names(hatvalues(fit)))
            }
hat.plot(fit)

图 8.9 显示了结果图。

在平均帽子值的两倍和三倍处画水平线。定位函数将图形置于交互模式。点击感兴趣的点,直到用户按下 Esc 键或图形右上角的完成按钮。

图片

图 8.9 用于评估具有高杠杆作用的观测值的帽子值索引图

在这里,你可以看到阿拉斯加和加利福尼亚在预测值方面特别不寻常。阿拉斯加的收入比其他州高得多,而人口和温度较低。加利福尼亚的人口比其他州高得多,而收入和温度也更高。与其他 48 个观测值相比,这些州是不典型的。

高杠杆观察值可能是有影响力的观察值,也可能不是。这取决于它们是否也是异常值。

8.4.3 有影响力的观察值

有影响力的观察对模型参数的值有不成比例的影响。想象一下,如果你发现你的模型在移除单个观察值后发生了显著变化。正是这种担忧促使你检查你的数据以寻找有影响力的点。

识别有影响力的观察值有两种方法:Cook 距离(或 D 统计量)和添加变量图。粗略地说,Cook 的 D 值大于 4/(nk – 1),其中n是样本大小,k是预测变量的数量,表明有影响力的观察值。你可以使用以下代码创建 Cook 的 D 图(图 8.10):

cutoff <- 4/(nrow(states)-length(fit$coefficients)-2)
plot(fit, which=4, cook.levels=cutoff)
abline(h=cutoff, lty=2, col="red")

图像

图 8.10 Cook’s D 图用于识别有影响力的观察值

该图表识别出阿拉斯加、夏威夷和内华达是有影响力的观察值。删除这些州将对回归模型中截距和斜率的值产生显著影响。请注意,尽管在寻找有影响力的观察值时撒网广泛是有用的,但我通常发现 1 的截止点比 4/(nk – 1)更普遍有用。给定 D = 1 的标准,数据集中的观察值似乎都不会显得有影响力。

Cook’s D 图可以帮助识别有影响力的观察值,但它们不提供有关这些观察值如何影响模型的信息。添加变量图在这方面有所帮助。对于单个响应变量和k个预测变量,你会创建k个添加变量图,如下所示。

对于每个预测变量Xk,绘制响应变量对其他k – 1 个预测变量的回归残差与Xk对其他k – 1 个预测变量的回归残差之间的图。可以使用car包中的avPlots()函数创建添加变量图:

library(car)
avPlots(fit, ask=FALSE, d=list(method="identify"))

图 8.11 提供了相应的图表。图表一个接一个地生成,用户可以点击点来识别它们。按 Esc 键或图表右上角的完成按钮以移动到下一个图表。在这里,我在左下角的图表中识别了阿拉斯加。

图像

图 8.11 评估有影响力的观察值影响的添加变量图

每个图中的直线是该预测变量的实际回归系数。你可以通过想象如果删除代表该观察值的点,这条线会如何变化来看到有影响力的观察值的影响。例如,看看左下角的 Murder | Others 与 Income | Others 的图表。你可以看到,消除标记为阿拉斯加的点会将线向负方向移动。实际上,删除阿拉斯加将收入回归系数从正的(.00006)变为负的(–.00085)。

您可以使用 car 包中的 influencePlot() 函数将异常值、杠杆和影响力图的信息合并到一个高度信息化的图中:

library(car)
influencePlot(fit, id="noteworthy", main="Influence Plot",
              sub="Circle size is proportional to Cook's distance")

结果图(图 8.12)识别了特别值得注意的观测值。特别是,它显示内华达州和罗德岛是异常值,加利福尼亚州和夏威夷有高杠杆作用,内华达州和阿拉斯加是具有影响力的观测值。

id="noteworthy" 替换为 id=list(method="identify") 允许您通过鼠标点击交互式地识别点(结束于 ESC 或按下完成按钮)。

图片

图 8.12 影响图。垂直轴上+2 或-2 以上的州被认为是异常值。水平轴上 0.2 或 0.3 以上的州具有高杠杆作用(预测值的不寻常组合)。圆圈大小与影响力成正比。由大圆圈表示的观测值可能对模型的参数估计有不成比例的影响。

8.5 纠正措施

在过去 16 页中学习了回归诊断之后,您可能会问,“如果你发现了问题,你会怎么做?”处理回归假设违反有四种方法:

  • 删除观测值

  • 变量转换

  • 添加或删除变量

  • 使用另一种回归方法

让我们逐一看看。

8.5.1 删除观测值

删除异常值可以经常改善数据集对正态性假设的拟合。具有影响力的观测值通常也会被删除,因为它们对结果有不成比例的影响。删除最大的异常值或具有影响力的观测值,并重新拟合模型。如果仍然存在异常值或具有影响力的观测值,则重复此过程,直到获得可接受的拟合。

再次,我强烈建议在考虑删除观测值时要谨慎。有时您可以确定观测值是异常值,因为记录数据时的错误,或者因为未遵循协议,或者因为测试对象误解了指示。在这些情况下,删除有问题的观测值似乎是完全合理的。

在其他情况下,异常观测值可能是您收集的数据中最有趣的事情。揭示为什么一个观测值与其他观测值不同可以为当前的主题以及您可能没有考虑到的其他主题提供深刻的见解。我们的一些最大进步来自于偶然注意到某些事情不符合我们的先入之见(请原谅我的夸张)。

8.5.2 变量转换

当模型不符合正态性、线性或同方差性假设时,转换一个或多个变量通常可以改善或纠正这种情况。转换通常涉及将变量 Y 替换为 Y^λ。表 8.5 给出了常见的 λ 值及其解释。如果 Y 是一个比例,则通常使用对数变换 [loge (Y/1-Y)]。当 Y 极度偏斜时,对数变换通常很有帮助。

表 8.5 常见转换

λ -2 -1 -0.5 0 0.5 1 2
转换 1/Y² 1/Y 1/√Y log(Y) Y Y²

当模型违反了正态性假设时,你通常会尝试对响应变量进行转换。你可以使用 car 包中的 powerTransform() 函数来生成最可能使变量 X^λ 正态化的幂 λ 的最大似然估计。这种转换称为 Box-Cox 转换。在下一个列表中,这种转换应用于 states 数据。

列表 8.8 Box-Cox 转换到正态性

> library(car)
> summary(powerTransform(states$Murder))
bcPower Transformation to Normality 

              Est.Power Std.Err. Wald Lower Bound Wald Upper Bound
states$Murder       0.6     0.26            0.088              1.1

Likelihood ratio tests about transformation parameters
                      LRT df  pval
LR test, lambda=(0) 5.7  1 0.017
LR test, lambda=(1) 2.1  1 0.145

结果表明,你可以通过将其替换为 Murder0.6 来标准化变量 Murder。因为 0.6 接近 0.5,你可以尝试平方根转换来改善模型对正态性的拟合。但在这个情况下,λ = 1 的假设不能被拒绝(p = 0.145),因此没有强有力的证据表明在这种情况下需要转换。这与图 8.9 中的 Q-Q 图的结果一致。

解释对数转换

对数转换通常用于使高度偏斜的分布变得不那么偏斜。例如,变量 income 通常右偏斜,有更多的人位于刻度较低的一端,而少数人收入非常高。当响应变量已被对数转换时,我们如何解释回归系数?

我们通常将 X 的回归系数解释为 YX 单位变化时的预期变化。考虑模型 Y = 3 + 0.6X。我们预测当 X 增加 1 个单位时,Y 将增加 0.6。同样,X 增加 10 个单位将与 Y 增加 0.6(10)或 6 个点的变化相关联。

然而,如果模型是 loge(Y) = 3 + 0.6X,那么 X 的一个单位变化会使 Y 的预期值乘以 e^(0.6) = 1.06。因此,X 增加 1 个单位将预测 Y 增加 6%。X 增加 10 个单位将使 Y 的预期值乘以 e^(0.6(10)) = 1.82。因此,X 增加 10 个单位将预测 Y 增加 82%。

要了解更多关于线性回归中解释对数转换的信息,请参阅 Kenneth Benoit 的优秀指南 (kenbenoit.net/assets/courses/ME104/logmodels2.pdf)。

当线性假设被违反时,转换预测变量通常有助于解决问题。car 包中的 boxTidwell() 函数可以用来生成预测变量幂的最大似然估计,这可以改善线性。以下是一个将 Box-Tidwell 转换应用于从人口和文盲率预测州谋杀率的模型的例子:

> library(car)
> boxTidwell(Murder~Population+Illiteracy,data=states)

           MLE of lambda Score Statistic (z) Pr(>|z|)
Population       0.86939             -0.3228   0.7468
Illiteracy       1.35812              0.6194   0.5357

结果表明尝试变换Population(0.87)和`Population`(1.36)以实现更大的线性。但Population(p = .75)和Illiteracy(p = .54)的得分检验表明,这两个变量都不需要变换。再次强调,这些结果与图 8.7 中的成分加残差图一致。

最后,响应变量的变换可以帮助处理异方差性(非恒定误差方差)的情况。您在列表 8.8 中看到,car包中的spreadLevel Plot()函数提供了一个用于提高同方差性的幂变换。同样,在states示例中,常数误差方差假设得到满足,因此不需要进行变换。

关于变换的注意事项

统计学中有一个古老的笑话:如果你不能证明 A,就证明 B,然后假装它是 A。(对于统计学家来说,这相当有趣。)这里的相关性在于,如果你变换了变量,你的解释必须基于变换后的变量,而不是原始变量。如果变换有意义,例如收入的对数或距离的倒数,解释就更容易。但你怎么解释自杀意念频率与抑郁立方根之间的关系?如果变换没有意义,你应该避免它。

8.5.3 添加或删除变量

改变模型中的变量将影响模型的拟合度。有时,添加一个重要变量可以纠正我们讨论过的许多问题。删除一个麻烦的变量也可以达到同样的效果。

删除变量对于处理多重共线性尤为重要。如果你的唯一目标是进行预测,那么多重共线性不是一个问题。但如果你想要对个体预测变量进行解释,那么你必须处理它。最常见的方法是删除一个参与多重共线性的变量(即 VIF > 10 的变量之一)。另一种选择是使用 lasso 或 ridge 回归,这些是旨在处理多重共线性情况的多重回归的变体。

8.5.4 尝试不同的方法

正如您刚才看到的,处理多重共线性的一种方法是通过拟合不同类型的模型(在本例中为岭回归或 lasso 回归)。如果有异常值和/或影响性观测值,您可以拟合一个稳健回归模型,而不是 OLS 回归。如果您违反了正态性假设,您可以拟合一个非参数回归模型。如果存在显著的非线性,您可以尝试非线性回归模型。如果您违反了误差独立性的假设,您可以拟合一个特别考虑误差结构的模型,例如时间序列模型或多级回归模型。最后,您可以考虑广义线性模型,以拟合在 OLS 回归假设不成立的情况下广泛的各种模型。

我们将在第十三章讨论这些替代方法中的一些。何时尝试改进 OLS 回归模型的拟合度,何时尝试不同的方法,这是一个复杂的问题。这通常基于对主题知识的了解和对哪种方法将提供最佳结果的评估。

说到最佳结果,现在让我们转向决定将哪些预测变量包含在回归模型中的问题。

8.6 选择“最佳”回归模型

在开发回归方程时,你隐含地面临从许多可能的模型中进行选择的问题。你应该包含所有研究变量,还是删除对预测没有显著贡献的变量?你应该添加多项式和/或交互项来提高拟合度?最终回归模型的选择总是涉及预测精度(尽可能好地拟合数据的模型)和简洁性(简单且可复制的模型)之间的折衷。在所有条件相同的情况下,如果你有两个具有大致相等预测精度的模型,你更倾向于选择更简单的一个。本节描述了在竞争模型之间进行选择的方法。单词“最佳”用引号括起来,因为没有单一的标准可以用来做出决定。最终的决定需要调查者的判断。(把它想象成职业保障。)

8.6.1 比较模型

你可以使用基础安装中的anova()函数比较两个嵌套模型的拟合度。嵌套模型是指其项完全包含在另一个模型中的模型。在states多元回归模型中,你发现IncomeFrost的回归系数不显著。你可以测试一个不包含这两个变量的模型是否与包含它们的模型一样好(见以下列表)。

列表 8.9 使用anova()函数比较嵌套模型

> states <- as.data.frame(state.x77[,c("Murder", "Population", 
                          "Illiteracy", "Income", "Frost")])
> fit1 <- lm(Murder ~ Population + Illiteracy + Income + Frost,
          data=states)
> fit2 <- lm(Murder ~ Population + Illiteracy, data=states)
> anova(fit2, fit1)

Analysis of Variance Table

Model 1: Murder ~ Population + Illiteracy
Model 2: Murder ~ Population + Illiteracy + Income + Frost
  Res.Df     RSS  Df     Sum of Sq      F Pr(>F)
1     47 289.246                           
2     45 289.167  2     0.079 0.0061     0.994

在这里,模型 1 嵌套在模型 2 中。anova()函数提供了一个同时测试,即IncomeFrost是否在PopulationIlliteracy之上增加了线性预测。由于测试不显著(p = .994),你得出结论,它们没有增加线性预测,因此你有理由从模型中删除它们。

阿卡伊克信息准则(AIC)为比较模型提供了另一种方法。该指数考虑了模型的统计拟合度和实现这种拟合所需的参数数量。具有较小 AIC 值的模型——表示使用较少参数即可达到适当的拟合——更受欢迎。该准则由AIC()函数提供(见以下列表)。

列表 8.10 使用 AIC 比较模型

> fit1 <- lm(Murder ~ Population + Illiteracy + Income + Frost,
          data=states)
> fit2 <- lm(Murder ~ Population + Illiteracy, data=states)
> AIC(fit1,fit2)

     df      AIC
fit1  6 241.6429
fit2  4 237.6565

AIC 值表明,没有IncomeFrost的模型是更好的模型。请注意,尽管 ANOVA 方法需要嵌套模型,但 AIC 方法不需要。

比较两个模型相对简单,但当有 4 个、10 个或 100 个可能的模型需要考虑时,你该怎么办?这就是下一节的主题。

8.6.2 变量选择

从更大的候选变量池中选择最终预测变量集的两种流行方法是逐步方法和所有子集回归。

逐步回归

在逐步选择中,变量一次添加到或从模型中删除,直到达到某个停止标准。例如,在正向逐步回归中,你一次添加一个预测变量到模型中,直到添加变量不再提高模型质量为止。在反向逐步回归中,你从一个包含所有预测变量的模型开始,然后逐个删除它们,直到删除变量会降低模型质量为止。在逐步逐步回归(通常称为逐步以避免听起来愚蠢)中,你结合了正向和反向逐步方法。变量一次添加到模型中,但在每一步,模型中的变量都会重新评估,并且那些对模型没有贡献的变量会被删除。预测变量可能在最终解决方案达到之前被多次添加到和从模型中删除。

步骤回归方法的实现因用于进入或删除变量的标准而异。基础 R 中的step()函数使用 AIC 标准执行逐步模型选择(正向、反向或逐步)。下面的列表将反向逐步回归应用于多元回归问题。

列表 8.11 反向逐步回归

> states <- as.data.frame(state.x77[,c("Murder", "Population", 
                          "Illiteracy", "Income", "Frost")])

> fit <- lm(Murder ~ Population + Illiteracy + Income + Frost,
          data=states)
> step(fit, direction="backward")

Start:  AIC=97.75
Murder ~ Population + Illiteracy + Income + Frost

             Df Sum of Sq    RSS     AIC
- Frost       1     0.021 289.19  95.753
- Income      1     0.057 289.22  95.759
<none>                    289.17  97.749
- Population  1    39.238 328.41 102.111
- Illiteracy  1   144.264 433.43 115.986

Step:  AIC=95.75
Murder ~ Population + Illiteracy + Income

             Df Sum of Sq    RSS     AIC
- Income      1     0.057 289.25  93.763
<none>                    289.19  95.753
- Population  1    43.658 332.85 100.783
- Illiteracy  1   236.196 525.38 123.605

Step:  AIC=93.76
Murder ~ Population + Illiteracy

             Df Sum of Sq    RSS     AIC
<none>                    289.25  93.763
- Population  1    48.517 337.76  99.516
- Illiteracy  1   299.646 588.89 127.311

Call:
lm(formula = Murder ~ Population + Illiteracy, data = states)

Coefficients:
(Intercept)   Population   Illiteracy  
  1.6515497    0.0002242    4.0807366  

你从模型中的所有四个预测变量开始。对于每一步,AIC 列提供了删除该行中列出的变量后得到的模型 AIC。<none>的 AIC 值是如果没有变量被删除的模型 AIC。在第一步中,Frost被删除,AIC 从 97.75 降至 95.75。在第二步中,Income被删除,AIC 降至 93.76。删除更多变量会增加 AIC,因此过程停止。

逐步回归是有争议的。尽管它可能找到一个好的模型,但无法保证它会找到“最佳”模型,因为并非每个可能模型都被评估。试图克服这一局限性的方法是“所有子集回归”。

所有子集回归

在所有子集回归中,检查了每个可能模型。分析师可以选择显示所有可能的结果,或者要求显示每个子集大小的nbest模型(一个预测变量、两个预测变量,等等)。例如,如果nbest=2,则显示两个最佳的单预测变量模型,然后是两个最佳的二预测变量模型,然后是两个最佳的三预测变量模型,直到包含所有预测变量的模型。

所有子集回归是通过leaps包中的regsubsets()函数执行的。你可以选择 R-squared、调整后的 R-squared 或 Mallows Cp 统计量作为报告“最佳”模型的准则。

正如你所见,R-squared 是预测变量在响应变量中解释的方差量。调整后的 R-squared 与此类似,但考虑了模型中的参数数量。随着预测变量的增加,R-squared 总是增加。当预测变量的数量相对于样本量很大时,这可能导致显著的过拟合。调整后的 R-squared 试图提供一个更诚实的总体 R-squared 估计——一个不太可能利用数据中偶然变化的估计。

在下面的列表中,我们将对所有子集回归应用于states数据。leaps包以图表的形式展示结果,但我发现很多人对此图表感到困惑。下面的代码以表格的形式展示了相同的结果,我认为这将更容易理解。

列表 8.12 所有子集回归

library(leaps)
states <- as.data.frame(state.x77[,c("Murder", "Population", 
                        "Illiteracy", "Income", "Frost")])

leaps <-regsubsets(Murder ~ Population + Illiteracy + Income +
                   Frost, data=states, nbest=4)

subsTable <- function(obj, scale){
  x <- summary(leaps)
  m <- cbind(round(x[[scale]],3), x$which[,-1])
  colnames(m)[1] <- scale
  m[order(m[,1]), ]
}

subsTable(leaps, scale="adjr2)

  adjr2 Population Illiteracy Income Frost
1 0.033          0          0      1     0
1 0.100          1          0      0     0
1 0.276          0          0      0     1
2 0.292          1          0      0     1
3 0.309          1          0      1     1
3 0.476          0          1      1     1
2 0.480          0          1      1     0
2 0.481          0          1      0     1
1 0.484          0          1      0     0
4 0.528          1          1      1     1
3 0.539          1          1      1     0
3 0.539          1          1      0     1
2 0.548          1          1      0     0

表格中的每一行代表一个模型。第一列表示模型中的预测变量数量。第二列是用于描述每个模型拟合度的尺度(在本例中为调整后的 R-squared),行按此尺度排序。(注意:可以使用其他尺度值代替adjr2。有关选项列表,请参阅?regsubsets。)行中的 1 和 0 表示哪些变量被包含或排除在模型之外。

例如,基于单一预测变量Income的模型具有 0.033 的调整后的 R-square。具有预测变量PopulationIlliteracyIncome的模型具有 0.539 的调整后的 R-square。相比之下,仅使用预测变量PopulationIlliteracy的模型具有 0.548 的调整后的 R-square。在这里,你可以看到具有较少预测变量的模型实际上具有更大的调整后的 R-square(这是未经调整的 R-square 不可能发生的情况)。表格表明,双预测变量模型(PopulationIlliteracy)是最好的。

在大多数情况下,所有子集回归比逐步回归更可取,因为考虑了更多的模型。但当预测变量的数量很大时,该过程可能需要大量的计算时间。一般来说,自动变量选择方法应被视为模型选择的辅助工具,而不是主导力量。一个拟合良好但无意义的模型并不能帮助你。最终,你应该根据你对主题知识的了解来指导自己。

8.7 深入分析

我们将通过考虑评估模型泛化能力和预测变量相对重要性的方法来结束对回归的讨论。

8.7.1 交叉验证

在上一节中,我们探讨了选择回归方程中包含的变量的方法。当描述是你的主要目标时,选择和解释回归模型标志着你工作的结束。但是,当你的目标是预测时,你可以合理地询问,“这个方程在现实世界中表现如何?”

根据定义,回归技术获得的是针对给定数据集最优的模型参数。在 OLS 回归中,模型参数被选择以最小化预测(残差)的平方误差之和,反之,最大化响应变量(R 平方)中解释的方差量。因为方程已经针对给定数据集进行了优化,所以它不太可能在新数据集上表现良好。

我们本章以一个研究生理学家为例开始,他想要预测个体从运动持续时间、强度、年龄、性别和 BMI 中燃烧的卡路里数量。如果你将 OLS 回归方程拟合到这些数据上,你会得到一组独特的模型参数,这些参数最大化了这一特定观察集的 R 平方。但我们的研究人员想要使用这个方程来预测一般个体的卡路里消耗,而不仅仅是原始研究中的个体。你知道这个方程在新观察样本中表现不会那么好,但你会损失多少?交叉验证是评估回归方程泛化能力的一种有用方法。

在交叉验证中,一部分数据被选为训练样本,另一部分被选为保留样本。在训练样本上开发回归方程,然后将其应用于保留样本。因为保留样本没有参与模型参数的选择,所以在这个样本上的表现是对模型在新数据上运行特性的更准确估计。

在 k 折交叉验证中,样本被分为k个子样本。每个k个子样本都作为保留组,而从剩余k - 1 个子样本中合并的观察值作为训练组。记录应用于k个保留样本的k个预测方程的性能,然后取平均值。(当k等于n,即观察值的总数时,这种方法称为“刀切法”。)

你可以使用bootstrap包中的crossval()函数执行 k 折交叉验证。以下列表提供了一个函数(称为shrinkage()),用于使用 k 折交叉验证交叉验证模型的 R 平方统计量。

列表 8.13:k 折交叉验证 R 平方函数

shrinkage <- function(fit, k=10, seed=1){
  require(bootstrap)

  theta.fit <- function(x,y){lsfit(x,y)}                     
  theta.predict <- function(fit,x){cbind(1,x)%*%fit$coef}     

  x <- fit$model[,2:ncol(fit$model)]                         
  y <- fit$model[,1] 

  set.seed(seed)
  results <- crossval(x, y, theta.fit, theta.predict, ngroup=k)  
  r2    <- cor(y, fit$fitted.values)²                         
  r2cv  <- cor(y, results$cv.fit)²
  cat("Original R-square =", r2, "\n")
  cat(k, "Fold Cross-Validated R-square =", r2cv, "\n")
}

使用这个列表,你可以定义你的函数,创建一个预测值和预测值的矩阵,获取原始的 R 平方和残差标准误差,以及获取交叉验证的 R 平方和残差标准误差。(第十二章详细介绍了自助法。)

然后使用shrinkage()函数对states数据进行 10 折交叉验证,使用包含所有四个预测变量的模型:

> states <- as.data.frame(state.x77[,c("Murder", "Population", 
         "Illiteracy", "Income", "Frost")])
> fit <- lm(Murder ~ Population + Income + Illiteracy + Frost, data=states)
> shrinkage(fit)

Original R-square = 0.567 
10 Fold Cross-Validated R-square = 0.356 

你可以看到,基于样本的 R-square(0.567)过于乐观。对于这个模型将用新数据解释的谋杀率变化量的更好估计是交叉验证的 R-square(0.356)。(注意,观测值是随机分配到 k 个组中的,因此提供了一个随机数种子以使结果可重复。)

你可以在变量选择中使用交叉验证,通过选择一个表现出更好泛化能力的模型。例如,具有两个预测变量(人口和Illiteracy)的模型比完整模型具有更小的 R-square 缩减:

> fit2 <- lm(Murder ~ Population + Illiteracy,data=states)
> shrinkage(fit2)

Original R-square = 0.567 
10 Fold Cross-Validated R-square = 0.515 

这可能使得双预测器模型成为一个更有吸引力的替代方案。

在所有其他条件相同的情况下,基于更大训练样本且更能代表目标人群的回归方程将具有更好的交叉验证效果。你将获得更小的 R-squared 缩减,并做出更准确的预测。

8.7.2 相对重要性

到目前为止,在本章中,我们一直在问,“哪些变量对预测结果有用?”但通常你的真正兴趣是,“哪些变量在预测结果中最为重要?”你隐含地希望根据相对重要性对预测变量进行排序。提出第二个问题可能有实际依据。例如,如果你可以根据相对重要性对领导实践进行排序,以组织成功为标准,你就可以帮助管理者专注于他们最需要发展的行为。

如果预测变量不相关,这将很简单。你会根据预测变量与响应变量的相关性对预测变量进行排序。然而,在大多数情况下,预测变量彼此相关,这显著增加了任务的复杂性。

已经尝试了许多方法来开发一种评估预测变量相对重要性的手段。最简单的方法是比较标准化回归系数,它描述了预测变量标准差变化时响应变量的预期变化(以标准差单位表示),同时保持其他预测变量不变。你可以在 R 中使用scale()函数将你的数据集中的每个变量标准化到均值为 0 和标准差为 1,然后将数据集提交给回归分析之前获得标准化回归系数。(注意,因为scale()函数返回一个矩阵,而lm()函数需要一个数据框,你需要在中间步骤中进行转换。)多回归问题的代码和结果如下所示:

> states <- as.data.frame(state.x77[,c("Murder", "Population", 
                          "Illiteracy", "Income", "Frost")])
> zstates <- as.data.frame(scale(states))
> zfit <- lm(Murder~Population + Income + Illiteracy + Frost, data=zstates)
> coef(zfit)

(Intercept)  Population      Income  Illiteracy       Frost 
 -9.406e-17   2.705e-01   1.072e-02   6.840e-01   8.185e-03

在这里,你可以看到在控制人口、收入和温度的情况下,文盲率的每增加一个标准差会导致谋杀率增加 0.68 个标准差。使用标准化回归系数作为指导,文盲率 是最重要的预测因子,而 霜冻 是最不重要的。

已经尝试了许多其他方法来量化相对重要性,这可以被认为是每个预测因子对 R-square 的贡献,无论是单独还是与其他预测因子结合。几种可能的相对重要性方法被 Ulrike Grömping 编写的 relaimpo 包所捕捉(mng.bz/KDYF)。

一种名为相对权重的新的方法显示出显著的潜力。该方法紧密地近似了通过添加预测变量到所有可能的子模型中获得的平均 R-square 增加量(Johnson,2004;Johnson 和 LeBreton,2004;LeBreton 和 Tonidandel,2008)。下一个列表中提供了一个生成相对权重的函数。

列表 8.14 relweights() 用于计算预测因子的相对重要性

relweights <- function(fit,...){                         
  R <- cor(fit$model)   
  nvar <- ncol(R)          
  rxx <- R[2:nvar, 2:nvar] 
  rxy <- R[2:nvar, 1]      
  svd <- eigen(rxx)        
  evec <- svd$vectors                           
  ev <- svd$values         
  delta <- diag(sqrt(ev))  
  lambda <- evec %*% delta %*% t(evec)        
  lambdasq <- lambda ^ 2   
  beta <- solve(lambda) %*% rxy           
  rsquare <- colSums(beta ^ 2)                   
  rawwgt <- lambdasq %*% beta ^ 2    
  import <- (rawwgt / rsquare) * 100 
  import <- as.data.frame(import)
  row.names(import) <- names(fit$model[2:nvar])   
  names(import) <- "Weights"
  import <- import[order(import),1, drop=FALSE]
  dotchart(import$Weights, labels=row.names(import),
     xlab="% of R-Square", pch=19,
     main="Relative Importance of Predictor Variables", 
     sub=paste("Total R-Square=", round(rsquare, digits=3)),
     ...)  
return(import)
}

注意:列表 8.16 中的代码是从 Johnson 博士慷慨提供的 SPSS 程序中改编的。有关相对权重的推导解释,请参阅 Johnson,2000。

在下一个列表中,relweights() 函数被应用于 states 数据,其中谋杀率由人口、文盲率、收入和温度预测。

列表 8.15 应用 relweights() 函数

> states <- as.data.frame(state.x77[,c("Murder", "Population", 
         "Illiteracy", "Income", "Frost")])
> fit <- lm(Murder ~ Population + Illiteracy + Income + Frost, data=states)
> relweights(fit, col="blue")

           Weights
Income        5.49
Population   14.72
Frost        20.79
Illiteracy   59.00

从结果图(图 8.13)中可以看出,模型解释的总方差(R-square = 0.567)被分配给了预测变量。文盲率 解释了 59% 的 R-square,霜冻 解释了 20.79%,等等。根据相对权重方法,文盲率 具有最大的相对重要性,其次是 霜冻人口收入,顺序如下。

图片

图 8.13 states 多元回归问题的相对权重点图。较大的权重表示相对更重要的预测因子。例如,文盲率 解释了总解释方差(0.567)的 59%,而 收入 只解释了 5.49%。因此,在这个模型中,文盲率 的相对重要性大于 收入

相对重要性度量(特别是相对权重方法)具有广泛的应用。它们比标准化回归系数更接近我们对于相对重要性的直观理解,我预计在未来的几年里它们的使用将会显著增加。

摘要

  • 回归分析是一种高度交互和迭代的分析方法,涉及拟合模型、评估其与统计假设的拟合度、修改数据和模型,并重新拟合以达到最终结果。

  • 回归诊断用于评估数据与统计假设的拟合程度,并选择修改模型或数据以更接近这些假设的方法。

  • 可用于选择最终回归模型中包含的变量的方法有很多,包括使用显著性检验、拟合统计量以及自动化解决方案,如逐步回归和所有子集回归。

  • 交叉验证可用于评估预测模型在新数据样本上的可能性能。

  • 相对权重法可用于解决变量重要性这一棘手问题:确定哪些变量对于预测结果最为重要。

9 方差分析

本章涵盖了

  • 使用 R 模拟基本实验设计

  • 拟合和解释 ANOVA 类型模型

  • 评估模型假设

在第八章中,我们探讨了用于从定量预测变量预测定量响应变量的回归模型,但并没有理由我们不能将名义或有序因素作为预测变量包括在内。当因素作为解释变量时,我们的焦点通常从预测转移到理解组间差异,这种方法被称为 方差分析(ANOVA)。ANOVA 方法用于分析各种实验和准实验设计。本章提供了 R 函数分析常见研究设计的概述。

首先,我们将探讨设计术语,然后一般性地讨论 R 拟合 ANOVA 模型的方法。接着,我们将探索几个示例,以说明常见设计的分析。在这个过程中,你将治疗焦虑症、降低血液胆固醇水平、帮助怀孕的老鼠生下胖宝宝、确保猪长牙、促进植物的呼吸,并了解哪些货架需要避免。

除了基本安装之外,在示例中你将使用 carrrcovmultcompeffectsMASSdplyrggplot2mvoutlier 这些包。在尝试示例代码之前,请确保安装它们。

9.1 术语速成课程

实验设计一般而言,尤其是方差分析(ANOVA)有其特定的语言。在讨论这些设计的分析之前,我们将快速回顾一些重要术语。我们将通过一系列越来越复杂的实验设计来介绍最重要的概念。

假设你对焦虑症的治疗感兴趣。两种流行的焦虑症治疗方法是认知行为疗法(CBT)和眼动脱敏与再加工(EMDR)。你招募了 10 名焦虑症患者,并将其中一半随机分配接受五周的 CBT 治疗,另一半接受五周的 EMDR 治疗。在治疗结束时,每位患者都被要求完成状态-特质焦虑量表(STAI),这是一种焦虑的自我报告测量工具。图 9.1A 概述了该设计。

图 9.1 三种方差分析(ANOVA)设计

在这个设计中,治疗是一个 组间 因素,有两个水平(CBT、EMDR)。它被称为组间因素,因为患者被分配到一组,并且只有一组。没有患者同时接受 CBT 和 EMDR。s 字符代表受试者(患者)。STAI 是 因变量,而治疗是 自变量。由于每个治疗条件中的观察数量相等,你有一个 平衡设计。当设计中的单元格样本量不相等时,你有一个 不平衡设计

图 9.1A 中的统计设计被称为单因素方差分析,因为有一个单一的分类变量。具体来说,它是一个单因素组间方差分析。在方差分析设计中,效应主要通过 F 检验来评估。如果治疗因素的 F 检验显著,你可以得出结论,两种治疗在五周治疗后平均 STAI 分数存在差异。

如果你感兴趣的是 CBT 对焦虑随时间的影响,你可以将所有 10 名患者都放在 CBT 组中,并在治疗结束时以及六个月后再次评估他们。图 9.1B 显示了这种设计。

时间是一个组内因素,有两个水平(五周,六个月)。它被称为组内因素,因为每个患者在两个水平下都被测量。统计设计是一个单因素组内方差分析。由于每个受试者被测量多次,该设计也被称为重复测量方差分析。如果时间因素的 F 检验显著,你可以得出结论,患者的平均 STAI 分数在五周和六个月之间发生了变化。

如果你既对治疗差异以及时间变化感兴趣,你可以结合前两种研究设计,随机分配五名患者到认知行为治疗(CBT)组和五名患者到眼动脱敏与再加工(EMDR)组,并在治疗结束(五周)和六个月后评估他们的 STAI 结果(见图 9.1C)。

通过将治疗和时间都作为因素,你可以检查治疗(时间平均),时间(治疗类型平均),以及治疗和时间的交互作用的影响。前两者被称为主效应,而交互作用(不出所料)被称为交互效应

当你交叉两个或更多因素,就像这里所做的那样,你有一个因子方差分析设计。交叉两个因素产生一个双向方差分析,交叉三个因素产生一个三向方差分析,依此类推。当一个因子设计包括组间和组内因素时,它也被称为混合模型方差分析。当前的设计是一个双向混合模型因子方差分析(哇!)。

在这种情况下,你将有三项 F 检验:一项用于治疗,一项用于时间,一项用于治疗×时间的交互作用。治疗因素显著表明 CBT 和 EMDR 在影响焦虑方面存在差异。时间因素显著表明焦虑从第五周到六个月随访期间发生了变化。治疗×时间交互作用显著表明两种焦虑治疗方法在时间上的影响存在差异(即,从五周到六个月焦虑的变化对两种治疗方法来说是不同的)。

现在让我们稍微扩展一下设计。众所周知,抑郁症可能会影响治疗效果,并且抑郁症和焦虑症常常同时发生。尽管受试者被随机分配到治疗条件中,但在研究开始时,两个治疗组的患者抑郁症水平可能存在差异。任何治疗后的差异可能是由预先存在的抑郁症差异造成的,而不是由于你的实验操作。因为抑郁症也可能解释依赖变量的组间差异,所以它是一个混杂因素。而且因为你对抑郁症不感兴趣,所以它被称为干扰变量。

如果你使用自我报告的抑郁症量表(如招募患者时的贝克抑郁量表 BDI)记录了抑郁症水平,你可以在评估治疗效果的影响之前对任何治疗组的抑郁症差异进行统计调整。在这种情况下,BDI 被称为协变量,设计被称为协方差分析(ANCOVA)

最后,你在这项研究中记录了一个单一的依赖变量(STAI)。你可以通过包括额外的焦虑测量(如家庭评估、治疗师评估以及评估焦虑对日常生活功能影响的测量)来提高这项研究的有效性。当存在多个依赖变量时,设计被称为多变量方差分析(MANOVA)。如果存在协变量,则称为多变量协方差分析(MANCOVA)

现在你已经掌握了基本术语,你准备好让你的朋友们惊叹,让新认识的人眼花缭乱,并学习如何使用 R 拟合 ANOVA/ANCOVA/MANOVA 模型。

9.2 拟合 ANOVA 模型

虽然 ANOVA 和回归方法分别发展,但在功能上它们都是一般线性模型的特殊情况。你可以使用第八章中用于回归的相同lm()函数来分析 ANOVA 模型。但本章你将主要使用aov()函数。lm()aov()的结果是等效的,但aov()函数以 ANOVA 方法学家更熟悉的方式呈现这些结果。为了完整性,我将在本章末尾提供一个使用lm()的示例。

9.2.1 aov()函数

aov()函数的语法是aov(formula, data=dataframe)。表 9.1 描述了可以在公式中使用的特殊符号。在这个表中,y是依赖变量,字母ABC代表因素。

表 9.1 R 公式中使用的特殊符号

符号 用法
~ 将左侧的响应变量与右侧的解释变量分开。例如,从ABC预测y的编码为y ~ A + B + C
: 表示变量之间的交互作用。从AB以及AB之间的交互作用预测y的编码为y ~ A + B + A:B
* 表示变量的完全交叉。代码 y ~ A*B*C 展开为 y ~ A + B + C + A:B + A:C + B:C + A:B:C
^ 表示到指定程度的交叉。代码 y ~ (A+B+C)² 展开为 y ~ A + B + C + A:B + A:C + A:B
. 表示所有剩余变量。代码 y ~ . 展开为 y ~ A + B + C

表 9.2 提供了几个常见研究设计的公式。在此表中,小写字母是定量变量,大写字母是分组因子,Subject 是受试者的唯一标识变量。

表 9.2 常见研究设计的公式

设计 公式
单因素方差分析 y ~ A
具有一个协变量的单因素协方差分析 y ~ x + A
双因素方差分析 y ~ A * B
具有两个协变量的双因素协方差分析 y ~ x1 + x2 + A * B
随机区组 y ~ B + A (其中 B 是一个区组因子)
单因素组内方差分析 y ~ A + Error(Subject/A)
具有一个组内因子 (W) 和一个组间因子 (B) 的重复测量方差分析 y ~ B * W + Error(Subject/W)

我们将在本章后面深入探讨这些设计的几个示例。

9.2.2 公式项的顺序

当公式中存在多个因子且设计不平衡或存在协变量时,公式中效应出现的顺序很重要。当这两种条件中的任何一种存在时,方程右侧的变量将相互关联。在这种情况下,没有明确的方法来划分它们对因变量的影响。例如,在一个具有不等数量观察值的处理组合的两因素方差分析中,模型 y ~ A*B 不会产生与模型 y ~ B*A 相同的结果。

默认情况下,R 使用类型 I(顺序)方法计算方差分析效应(见侧边栏“顺序很重要!”)。第一个模型可以写成 y ~ A + B + A:B。生成的 R 方差分析表将评估

  • Ay 的影响

  • 在控制 A 的情况下,By 的影响

  • 在控制 AB 的主效应的情况下,AB 的交互作用

顺序很重要!

当自变量相互关联或与协变量关联时,没有明确的方法来评估这些变量对因变量的独立贡献。考虑一个具有 AB 因子和因变量 y 的不平衡两因素设计。该设计有三个效应:AB 的主效应以及 A × B 的交互作用。假设你正在使用公式 Y ~ A + B + A:B 来建模数据,那么在方程右侧的效应中划分 y 的方差有三个典型方法。

类型 I(顺序)

效应会调整那些在公式中较早出现的效应。A 是未调整的。B 调整了 AA:B 交互作用调整了 AB

类型 II (分层)

效应调整以适应同一或更低级别的其他效应。A 调整以适应 BB 调整以适应 AA:B 交互作用调整以适应 AB

III 类(边际)

模型中的每个效应都会调整以适应模型中的其他效应。A 调整以适应 BA:BB 调整以适应 AA:BA:B 交互作用调整以适应 AB

R 默认采用 I 类方法。其他程序如 SAS 和 SPSS 默认采用 III 类方法。

样本大小不平衡程度越大,项的顺序对结果的影响就越大。一般来说,更基本的效果应该更早地列在公式中。特别是,协变量应该首先列出,然后是主效应,接着是双向交互作用,然后是三向交互作用,依此类推。对于主效应,更基本的变量应该首先列出。因此,性别应该列在治疗之前。底线是:当研究设计不是正交的(即,当因素和/或协变量相关时),在指定效应顺序时要小心。

在具体示例之前,请注意,car 包中的 Anova() 函数(不要与标准 anova() 函数混淆)提供了使用 II 类或 III 类方法而不是 aov() 函数使用的 I 类方法的选择。如果你担心将你的结果与 SAS 和 SPSS 等其他包提供的结果相匹配,你可能想使用 Anova() 函数。有关详细信息,请参阅 help(Anova, package="car")

9.3 单因素方差分析

在单因素方差分析中,你感兴趣的是比较由分类分组因素定义的两个或多个组的因变量均值。此例来自 multcomp 包中的 cholesterol 数据集,由 Westfall、Tobias、Rom 和 Hochberg(1999)提供。50 名患者接受了五种胆固醇降低药物方案之一(trt)。三种治疗方案涉及相同的药物,每天一次 20 毫克(1time)、每天两次 10 毫克(2times)或每天四次 5 毫克(4times)。剩下的两种条件(drugDdrugE)代表竞争性药物。哪种药物方案产生了最大的胆固醇降低(响应)?以下列表提供了分析。

列表 9.1 单因素方差分析(ANOVA)

> library(dplyr)
> data(cholesterol, package="multcomp")
> plotdata <- cholesterol %>%                             ❶
    group_by(trt) %>%                                     ❶
    summarize(n = n(),                                    ❶
              mean = mean(response),                      ❶
              sd = sd(response),                          ❶
              ci = qt(0.975, df = n - 1) * sd / sqrt(n))  ❶
> plotdata                                        

  trt        n  mean    sd    ci
  <fct>  <int> <dbl> <dbl> <dbl>
1 1time     10  5.78  2.88  2.06
2 2times    10  9.22  3.48  2.49
3 4times    10 12.4   2.92  2.09
4 drugD     10 15.4   3.45  2.47
5 drugE     10 20.9   3.35  2.39

> fit <- aov(response ~ trt, data=cholesterol)            ❷

> summary(fit)    

            Df Sum Sq   Mean Sq    F value           Pr(>F)    
trt          4   1351       338       32.4     9.8e-13  ***
Residuals   45    469        10                    
--- 
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1    

> library(ggplot2)                                        ❸
> ggplot(plotdata,                                        ❸
       aes(x = trt, y = mean, group = 1)) +               ❸
    geom_point(size = 3, color="red") +                   ❸
    geom_line(linetype="dashed", color="darkgrey") +      ❸
    geom_errorbar(aes(ymin = mean - ci,                   ❸
                      ymax = mean + ci),                  ❸
                  width = .1) +                           ❸
    theme_bw() +                                          ❸
    labs(x="Treatment",                                   ❸
         y="Response",                                    ❸
         title="Mean Plot with 95% Confidence Interval")  ❸

❶ 组样本大小、均值、标准差和 95% 置信区间

❷ 组间差异检验(ANOVA)

❸ 绘制组均值和置信区间

观察输出结果,你可以看到每种药物方案都有 10 名患者接受治疗 ❶。从均值来看,drugE 产生了最大的胆固醇降低效果,而 1time 产生了最小的效果 ❷。标准差在五个组中相对恒定,范围从 2.88 到 3.48。我们假设我们研究的每个治疗组都是可能接受治疗的更大潜在患者群体中的一个样本。对于每种治疗,样本均值 +/– ci 给我们一个区间,我们 95% 置信这个区间包含真实的总体均值。治疗 ANOVA F 检验(trt)是显著的(p < .0001),提供了证据表明五种治疗并不都同样有效 ❷。

使用 ggplot2 函数创建一个显示组均值及其置信区间的图表 ❸ 图 9.2 提供了带有 95% 置信限的治疗均值图,允许你清楚地看到这些治疗差异。

图 9.2 五种降胆固醇药物方案的 95% 置信区间治疗组均值

通过在图 9.2 中包含置信区间,我们展示了我们对总体均值估计的确定性(或不确定性)程度。

9.3.1 多重比较

治疗 ANOVA F 检验告诉你五种药物方案并不都同样有效,但它并没有告诉你 哪些 治疗方案彼此不同。你可以使用多重比较程序来回答这个问题。例如,TukeyHSD() 函数提供了组均值之间所有配对差异的检验,如下一列表所示。

列表 9.2 Tukey HSD 配对组比较

> pairwise <- TukeyHSD(fit)                                     ❶
> pairwise

Fit: aov(formula = response ~ trt)

$trt
               diff    lwr   upr p adj
2times-1time   3.44 -0.658  7.54 0.138
4times-1time   6.59  2.492 10.69 0.000
drugD-1time    9.58  5.478 13.68 0.000
drugE-1time   15.17 11.064 19.27 0.000
4times-2times  3.15 -0.951  7.25 0.205
drugD-2times   6.14  2.035 10.24 0.001
drugE-2times  11.72  7.621 15.82 0.000
drugD-4times   2.99 -1.115  7.09 0.251
drugE-4times   8.57  4.471 12.67 0.000
drugE-drugD    5.59  1.485  9.69 0.003

> plotdata <- as.data.frame(pairwise[[1]])                       ❷
> plotdata$conditions <- row.names(plotdata)                     ❷

> library(ggplot2)                                               ❸
> ggplot(data=plotdata, aes(x=conditions, y=diff)) +             ❸
    geom_point(size=3, color="red") +                            ❸
    geom_errorbar(aes(ymin=lwr, ymax=upr, width=.2)) +           ❸
    geom_hline(yintercept=0, color="red", linetype="dashed") +   ❸
       labs(y="Difference in mean levels", x="",                 ❸
         title="95% family-wise confidence level") +             ❸
    theme_bw() +                                                 ❸
    coord_flip()                                                 ❸

❶ 计算配对比较

❷ 创建结果数据集

❸ 绘制结果

例如,1time2times 的平均胆固醇降低量之间没有显著差异(p = 0.138),而 1time4times 之间的差异是显著的(p < .001)。

图 9.3 绘制了配对比较。在这个图中,包含 0 的置信区间表示治疗之间没有显著差异(p > 0.5)。在这里,我们可以看到最大的均值差异是在 drugE1time 之间,并且差异是显著的(置信区间不包括 0)。

在继续之前,我应该指出,我们本可以使用基础图形创建图 9.3 中的图表。在这种情况下,代码将简单为 plot(pairwise)。使用 ggplot2 方法的优点是它创建了一个更吸引人的图表,并允许你完全自定义图表以满足你的需求。

图 9.3 Tukey HSD 配对均值比较图

multcomp包中的glht()函数提供了一套更全面的多种均值比较方法,你可以用于线性模型(如本章所述)和广义线性模型(在第十三章中介绍)。以下代码重现了 Tukey HSD 检验,以及结果的不同图形表示(图 9.4):

> tuk <- glht(fit, linfct=mcp(trt="Tukey")) 
> summary(tuk)

     Simultaneous Tests for General Linear Hypotheses

Multiple Comparisons of Means: Tukey Contrasts

Fit: aov(formula = response ~ trt, data = cholesterol)

Linear Hypotheses:
                     Estimate Std. Error t value Pr(>|t|)    
2times - 1time == 0     3.443      1.443   2.385  0.13812    
4times - 1time == 0     6.593      1.443   4.568  < 0.001 ***
drugD - 1time == 0      9.579      1.443   6.637  < 0.001 ***
drugE - 1time == 0     15.166      1.443  10.507  < 0.001 ***
4times - 2times == 0    3.150      1.443   2.182  0.20504    
drugD - 2times == 0     6.136      1.443   4.251  < 0.001 ***
drugE - 2times == 0    11.723      1.443   8.122  < 0.001 ***
drugD - 4times == 0     2.986      1.443   2.069  0.25120    
drugE - 4times == 0     8.573      1.443   5.939  < 0.001 ***
drugE - drugD == 0      5.586      1.443   3.870  0.00308 ** 
---
Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1
(Adjusted p values reported -- single-step method)

> labels1 <- cld(tuk, level=.05)$mcletters$Letters
> labels2 <- paste(names(labels1), "\n", labels1)
> ggplot(data=fit$model, aes(x=trt, y=response)) +
    scale_x_discrete(breaks=names(labels1), labels=labels2) +
    geom_boxplot(fill="lightgrey") +
    theme_bw() +
    labs(x="Treatment",
         title="Distribution of Response Scores by Treatment",
         subtitle="Groups without overlapping letters differ significantly 
         (p < .05)")

图片

图 9.4 展示了multcomp包提供的 Tukey HSD 检验

cld()函数中的level选项提供了要使用的显著性水平(在本例中为 0.05,或 95%的置信度)。

具有相同字母的组(由箱线图表示)没有显著不同的均值。你可以看到1time2times没有显著差异(它们都有字母a),以及2times4times也没有显著差异(它们都有字母b),但1time4times是不同的(它们没有共享相同的字母)。我个人觉得图 9.4 比图 9.3 更容易阅读。它还有提供每个组内分数分布信息的优势。

从这些结果中,你可以看到每天四次服用 5 毫克剂量的降胆固醇药物比每天一次服用 20 毫克剂量更好。竞争对手drugD并不优于这种每天四次的治疗方案。但竞争对手drugE优于both drugD和针对目标药物的三个剂量策略。

多重比较方法是一个复杂且快速发展的研究领域。要了解更多信息,请参阅 Bretz, Hothorn, and Westfall (2010)。

9.3.2 评估测试假设

正如你在上一章中看到的,结果的置信度取决于你的数据满足统计检验假设的程度。在一元方差分析(ANOVA)中,假设因变量呈正态分布,并且在每个组中具有相等的方差。你可以使用 Q-Q 图来评估正态性假设:

> library(car)
> fit <- aov(response ~ trt, data=cholesterol)
> qqPlot(fit, simulate=TRUE, main="Q-Q Plot")

图 9.5 提供了图表。默认情况下,具有最高标准化残差的两项观测值通过数据框的行号识别。数据落在 95%置信区间内,表明正态性假设得到了较好的满足。

图片

图 9.5 展示了学生化残差的正态性检验。残差是实际值与预测值的差,而学生化残差则是这些残差除以其标准差的估计值。如果学生化残差呈正态分布,它们应该围绕直线聚集。

R 提供了几个用于方差(同质性)相等的测试。例如,你可以使用以下代码执行 Bartlett 测试:

> bartlett.test(response ~ trt, data=cholesterol)

        Bartlett test of homogeneity of variances

data:  response by trt 
Bartlett's K-squared = 0.5797, df = 4, p-value = 0.9653

Bartlett 检验表明,五个组之间的方差没有显著差异(p = 0.97)。其他可能的检验包括 Fligner-Killeen 检验(由fligner.test()函数提供)和 Brown-Forsythe 检验(由HH包中的hov()函数提供)。尽管没有展示,其他两个检验也得出相同的结论。

最后,方差分析方法对异常值的存在可能很敏感。你可以使用car包中的outlierTest()函数来测试异常值:

> library(car)
> outlierTest(fit)

No Studentized residuals with Bonferroni  p < 0.05
Largest |rstudent|:
   rstudent unadjusted p-value Bonferroni p
19 2.251149           0.029422           NA

从输出结果中,你可以看到胆固醇数据中没有异常值(当 p > 1 时出现NA)。结合 Q-Q 图、Bartlett 检验和异常值检验,数据似乎很好地符合方差分析模型。这反过来又增加了你对结果的信心。

9.4 一元方差分析协方差(ANCOVA)

一元方差分析协方差(ANCOVA)将一元方差分析扩展到包括一个或多个定量协变量。此例来自multcomp包中的litter数据集(参见 Westfall 等,1999)。怀孕的雌鼠被分为四个治疗组;每个组接受不同剂量的药物(0、5、50 或 500)。每个窝的平均出生体重是因变量,妊娠时间被包括为协变量。以下列表给出了分析。

列表 9.3 一元方差分析协方差(ANCOVA)

> library(multcomp)
> library(dplyr)
> litter %>%
    group_by(dose) %>%
    summarise(n=n(), mean=mean(gesttime), sd=sd(gesttime))

  dose      n  mean    sd
  <fct> <int> <dbl> <dbl>
1 0        20  22.1 0.438
2 5        19  22.2 0.451
3 50       18  21.9 0.404
4 500      17  22.2 0.431

> fit <- aov(weight ~ gesttime + dose, data=litter)                             
> summary(fit)
            Df Sum Sq Mean Sq F value  Pr(>F)   
gesttime     1  134.3  134.30   8.049 0.00597 **
dose         3  137.1   45.71   2.739 0.04988 * 
Residuals   69 1151.3   16.69                   
---
Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1

summarise()函数中,你可以看到每个剂量水平上的窝数不等,零剂量(无药物)有 20 窝,剂量 500 有 17 窝。根据组均值,无药物组的平均窝重最高(32.3)。ANCOVA F 检验表明,在控制妊娠时间后,(a)妊娠时间与出生体重相关,(b)药物剂量与出生体重相关。在控制妊娠时间后,不同药物剂量的平均出生体重并不相同。

由于你使用了协变量,你可能希望获得调整后的组均值——也就是说,在部分消除协变量影响后得到的组均值。你可以使用effects库中的effect()函数来计算调整后的均值:

> library(effects)
> effect("dose", fit)

 dose effect
dose
   0    5   50  500 
32.4 28.9 30.6 29.3

这些是每个治疗剂量的平均窝重,在统计调整了妊娠时间初始差异后得到的。在这种情况下,调整后的均值与summarise()函数产生的未调整均值差异很大。effects包提供了一种强大的方法,用于获取复杂研究设计的调整均值并直观地展示它们。有关更多详细信息,请参阅 CRAN 上的包文档。

与上一节中提到的一元方差分析示例类似,剂量 F 检验表明处理组之间的平均出生重量不同,但它并没有告诉你哪些平均值彼此不同。再次强调,你可以使用multcomp包提供的多重比较程序来计算所有成对平均值的比较。此外,multcomp包还可以用来测试关于平均值的具体用户定义假设。

假设你对无药条件是否与三药条件不同感兴趣。以下列表中的代码可以用来测试这个假设。

列表 9.4 使用用户提供的对比进行多重比较

> library(multcomp)
> contrast <- rbind("no drug vs. drug" = c(3, -1, -1, -1))
> summary(glht(fit, linfct=mcp(dose=contrast)))

Multiple Comparisons of Means: User-defined Contrasts

Fit: aov(formula = weight ~ gesttime + dose)

Linear Hypotheses:
                      Estimate Std. Error t value Pr(>|t|)  
no drug vs. drug == 0    8.284      3.209   2.581   0.0120 *
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1 

对比c(3, -1, -1, -1)指定了第一组与其他三组的平均值进行比较。具体来说,正在测试的假设是

图片

或者

图片

其中μ[n]是剂量n的平均窝重。该假设通过 t 统计量(本例中为 2.581)进行测试,该统计量在 p < .05 水平上是显著的。因此,你可以得出结论,无药组比药物条件下的出生重量更高。可以添加其他对比到rbind()函数中(有关详细信息,请参阅help(glht))。

9.4.1 评估测试假设

ANCOVA 设计对 ANOVA 设计中描述的相同正态性和方差齐性假设,并且你可以使用本节 9.3.2 中描述的相同程序来测试这些假设。此外,标准的 ANCOVA 设计假设回归斜率同质性。在这种情况下,假设预测出生重量与妊娠时间之间的回归斜率在四个处理组中是相同的。可以通过在 ANCOVA 模型中包含妊娠×剂量交互项来获得回归斜率同质性的检验。显著的交互作用将意味着妊娠与出生重量之间的关系取决于剂量变量的水平。以下列表提供了代码和结果。

列表 9.5 检验回归斜率的同质性

> library(multcomp)
> fit2 <- aov(weight ~ gesttime*dose, data=litter)
> summary(fit2)
              Df Sum Sq Mean Sq F value Pr(>F)   
gesttime       1    134     134    8.29 0.0054 **
dose           3    137      46    2.82 0.0456 * 
gesttime:dose  3     82      27    1.68 0.1789   
Residuals     66   1069      16                  
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1 

交互作用不显著,支持斜率相等的假设。如果假设不可行,你可以尝试转换协变量或因变量,使用考虑单独斜率的模型,或者采用不需要回归斜率同质性的非参数 ANCOVA 方法。有关后者的示例,请参阅sm包中的sm.ancova()函数。

9.4.2 可视化结果

我们可以使用ggplot2来可视化因变量、协变量和因素之间的关系。例如,

pred <- predict(fit)
library(ggplot2)
ggplot(data = cbind(litter, pred),
       aes(gesttime, weight)) + geom_point() +
   facet_wrap(~ dose, nrow=1) + geom_line(aes(y=pred)) +
   labs(title="ANCOVA for weight by gesttime and dose") +
   theme_bw() +
   theme(axis.text.x = element_text(angle=45, hjust=1),
         legend.position="none")

生成如图 9.6 所示的图表。

图片

图 9.6 展示了四个药物处理组中妊娠时间与出生重量的关系图

在这里,您可以看到预测出生重量的回归线在每个组中都是平行的,但截距不同。随着妊娠时间的增加,出生重量增加。此外,您还可以看到零剂量组的截距最大,而五剂量组的截距最低。这些线是平行的,因为它们被指定为平行的。如果您使用了代码

ggplot(data = litter, aes(gesttime, weight)) + 
       geom_point() + geom_smooth(method="lm", se=FALSE) +
       facet_wrap(~ dose, nrow=1)

相反,您会生成一个允许斜率和截距按组变化的图表。这种方法对于可视化回归斜率同质性不成立的情况很有用。

9.5 双因素方差分析

在双因素方差分析中,受试者被分配到由两个因素的交叉分类形成的组。本例使用基础安装中的ToothGrowth数据集来演示双因素组间方差分析。六十只豚鼠被随机分配到接受三种水平的抗坏血酸(0.5 毫克、1 毫克或 2 毫克)和两种递送方法(橙汁或维生素 C)之一,限制条件是每种治疗组合有 10 只豚鼠。因变量是牙齿长度。以下列表显示了分析的代码。

列表 9.6 双因素方差分析

> library(dplyr)
> data(ToothGrowth)
> ToothGrowth$dose <- factor(ToothGrowth$dose)             ❶
> stats <- ToothGrowth %>%                                 ❷
    group_by(supp, dose) %>%                               ❷
    summarise(n=n(), mean=mean(len), sd=sd(len),           ❷
              ci = qt(0.975, df = n - 1) * sd / sqrt(n))   ❷
> stats

# A tibble: 6 x 6
# Groups:   supp [2]
  supp  dose      n  mean    sd    ci
  <fct> <fct> <int> <dbl> <dbl> <dbl>
1 OJ    0.5      10 13.2   4.46  3.19
2 OJ    1        10 22.7   3.91  2.80
3 OJ    2        10 26.1   2.66  1.90
4 VC    0.5      10  7.98  2.75  1.96
5 VC    1        10 16.8   2.52  1.80
6 VC    2        10 26.1   4.80  3.43

> fit <- aov(len ~ supp*dose, data=ToothGrowth)            ❸
> summary(fit)

            Df Sum Sq Mean Sq F value   Pr(>F)    
supp         1  205.4   205.4  15.572 0.000231 ***
dose         2 2426.4  1213.2  92.000  < 2e-16 ***
supp:dose    2  108.3    54.2   4.107 0.021860 *  
Residuals   54  712.1    13.2                     
---
Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1

❶ 准备数据

❷ 计算摘要统计量

❸ 拟合双因素方差分析模型

首先,将剂量变量转换为因子,这样aov()函数会将其视为分组变量而不是数值协变量 ❶。接下来,计算每种治疗组合的摘要统计量(样本量、均值、标准差和均值的置信区间) ❷。样本量表明您有一个平衡的设计(设计中每个单元格的样本量相等)。将双因素方差分析模型拟合到数据 ❸,summary()函数表明主效应(suppdose)以及这些因素之间的交互作用都是显著的。

您可以通过多种方式可视化结果,包括基础 R 中的interaction.plot()函数、gplots包中的plotmeans()函数以及HH包中的interaction2wt()函数。在图 9.7 之后的代码中,我们将使用ggplot2来绘制这个双因素方差分析的均值和均值的 95%置信区间。使用ggplot2的一个优点是我们可以根据我们的研究和美学需求自定义图表。图 9.7.展示了生成的图表。

图片

图 9.7 显示了剂量和递送机制对牙齿生长的交互作用。均值图是使用ggplot2代码创建的。

library(ggplot2)
pd <- position_dodge(0.2)
ggplot(data=stats, 
       aes(x = dose, y = mean, 
           group=supp, 
           color=supp, 
           linetype=supp)) +
  geom_point(size = 2, 
             position=pd) +
  geom_line(position=pd) +
  geom_errorbar(aes(ymin = mean - ci, ymax = mean + ci), 
                width = .1, 
                position=pd) +
  theme_bw() + 
  scale_color_manual(values=c("blue", "red")) +
  labs(x="Dose",
       y="Mean Length",
       title="Mean Plot with 95% Confidence Interval")

图表显示,无论是橙汁还是维生素 C,牙齿生长都与抗坏血酸剂量成正比。对于 0.5 毫克和 1 毫克的剂量,橙汁产生的牙齿生长比维生素 C 多。对于 2 毫克的抗坏血酸,两种递送方法产生的生长相同。

虽然我没有涵盖模型假设检验和均值比较程序,但它们是您迄今为止看到的方法的自然扩展。此外,设计是平衡的,因此您不必担心效应的顺序。

9.6 重复测量方差分析

在重复测量方差分析中,受试者被测量多次。本节重点介绍一个包含一个组内因素和一个组间因素的重复测量方差分析(这是一种常见的设计)。我们将从生理生态学领域取例。生理生态学家研究生物系统的生理和生化过程如何对环境因素的变异做出反应(鉴于全球变暖的现实,这是一个关键的研究领域)。包含在基本安装中的CO2数据集包含了对草种Echinochloa crus-galli(Potvin,Lechowicz 和 Tardif,1990)南北植物耐寒性研究的结果。冷却植物的光合速率与在几个环境 CO[2]浓度下的非冷却植物的光合速率进行了比较。一半的植物来自魁北克,另一半来自密西西比。

在本例中,我们将关注冷却植物。因变量是二氧化碳吸收量(uptake)以毫升/升为单位,自变量是Type(魁北克与密西西比)和周围 CO[2]浓度(conc),共有七个水平(从 95 到 1000 微摩尔/平方米秒)。Type是组间因素,而conc是组内因素。Type已经存储为因素,但您需要将conc转换为因素才能继续。分析将在下一列表中展示。

列表 9.7 具有组间和组内因素的重复测量方差分析

> data(CO2)
> CO2$conc <- factor(CO2$conc)
> w1b1 <- subset(CO2, Treatment=='chilled')
> fit <- aov(uptake ~ conc*Type + Error(Plant/(conc)), w1b1)
> summary(fit)

Error: Plant
          Df Sum Sq Mean Sq F value Pr(>F)   
Type       1   2667    2667    60.4 0.0015 **
Residuals  4    177      44                  
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Error: Plant:conc
          Df Sum Sq Mean Sq F value  Pr(>F)    
conc       6   1472   245.4    52.5 1.3e-12 ***
conc:Type  6    429    71.5    15.3 3.7e-07 ***
Residuals 24    112     4.7                    
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1               

> library(dplyr)
> stats <- CO2 %>%
   group_by(conc, Type) %>%
   summarise(mean_conc = mean(uptake))

> library(ggplot2)
> ggplot(data=stats, aes(x=conc, y=mean_conc, 
          group=Type, color=Type, linetype=Type)) +
   geom_point(size=2) +
   geom_line(size=1) +
   theme_bw() + theme(legend.position="top") +
   labs(x="Concentration", y="Mean Uptake", 
        title="Interaction Plot for Plant Type and Concentration")       

方差分析表表明,Type和浓度的主效应以及Type × 浓度的交互作用在 0.01 水平上均显著。图 9.8 显示了交互作用的图示。在这种情况下,我省略了置信区间,以避免图表过于复杂。

为了展示交互作用的另一种表示,使用了geom_boxplot()函数来绘制相同的数据。图 9.9 提供了结果:

library(ggplot2)
ggplot(data=CO2, aes(x=conc, y=uptake, fill=Type)) +
  geom_boxplot() +
  theme_bw() + theme(legend.position="top") +
  scale_fill_manual(values=c("aliceblue", "deepskyblue"))+
  labs(x="Concentration", y="Uptake", 
       title="Chilled Quebec and Mississippi Plants")

图片

图 9.8 环境 CO[2]浓度和植物类型对 CO[2]吸收的交互作用

图片

图 9.9 环境 CO[2]浓度和植物类型对 CO[2]吸收的交互作用

从任一图表中,您都可以看到来自魁北克的植物比密西西比州的植物有更高的二氧化碳吸收量。在较高的环境 CO[2]浓度下,这种差异更为明显。

注意数据集通常是宽格式,其中列是变量,行是观测值,每个受试者有一个单独的行。9.4 节中的litter数据框是一个很好的例子。在处理重复测量设计时,通常在拟合模型之前需要将数据转换为长格式。在长格式中,因变量的每个测量值都放在自己的行中。CO2数据集遵循这种形式。幸运的是,第五章(5.5.2 节)中描述的tidyr包可以轻松地将您的数据重新组织成所需的格式。

混合模型设计的多种方法

本节中的 CO[2]示例使用传统的重复测量方差分析(ANOVA)进行分析。该方法假设任何组内因素的协方差矩阵遵循称为球性的特定形式。具体来说,它假设组内因素任何两个水平之间的差异的方差是相等的。在现实世界的数据中,这个假设不太可能成立。这导致了许多替代方法,包括以下几种:

  • 使用lme4包中的lmer()函数来拟合线性混合模型(Bates,2005)

  • 使用car包中的Anova()函数调整传统的测试统计量以考虑球性不足(例如,Geisser-Greenhouse 校正)

  • 使用nlme包中的gls()函数来拟合具有指定方差-协方差结构的广义最小二乘模型(UCLA,2009)

  • 使用多元方差分析来建模重复测量数据(Hand,1987)

这些方法的覆盖范围超出了本文的范围。如果您想了解更多信息,请参阅 Pinheiro 和 Bates(2000)以及 Zuur 等人(2009)。

到目前为止,本章中介绍的所有方法都假设存在一个单一的因变量。在下一节中,我们将简要讨论包括多个结果变量的设计。

9.7 多元方差分析(MANOVA)

如果存在多个因变量(结果变量),您可以使用多元方差分析(MANOVA)同时测试它们。以下示例基于MASS包中的UScereal数据集。数据集来自 Venables 和 Ripley(1999)。在这个例子中,您感兴趣的是美国谷物的卡路里、脂肪和糖含量是否因货架位置而异,其中 1 是底层货架,2 是中间货架,3 是顶层货架。"Calories"、"fat"和"sugars"是因变量,"shelf"是自变量,有三个水平(1、2 和 3)。以下列表展示了分析。

列表 9.8 一元 MANOVA

> data(UScereal, package="MASS")
> shelf <- factor(UScereal$shelf) 
> shelf <- factor(shelf)
> y <- cbind(UScereal$calories, UScereal$fat, UScereal$sugars)
> colnames(y) <- c("calories", "fat", "sugars")
> aggregate(y, by=list(shelf=shelf), FUN=mean)

    shelf calories   fat sugars
1       1      119 0.662    6.3
2       2      130 1.341   12.5
3       3      180 1.945   10.9

> cov(y)

         calories   fat sugars
calories   3895.2 60.67 180.38
fat          60.7  2.71   4.00
sugars      180.4  4.00  34.05

> fit <- manova(y ~ shelf)
> summary(fit)

          Df Pillai approx F num Df den Df Pr(>F)    
shelf      2  0.402     5.12      6    122  1e-04 ***
Residuals 62                                         
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1 

> summary.aov(fit)          ❶

Response calories :
            Df Sum Sq Mean Sq F value  Pr(>F)    
shelf        2  50435   25218    7.86 0.00091 ***
Residuals   62 198860    3207                    
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

 Response fat :
            Df Sum Sq Mean Sq F value Pr(>F)  
shelf        2   18.4    9.22    3.68  0.031 *
Residuals   62  155.2    2.50                 
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

 Response sugars :
            Df Sum Sq Mean Sq F value Pr(>F)   
shelf        2    381     191    6.58 0.0026 **
Residuals   62   1798      29                  
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

❶ 打印单变量结果

首先,将货架变量转换为因子,以便在分析中代表分组变量。接下来,使用cbind()函数形成三个依赖变量(caloriesfatsugars)的矩阵。aggregate()函数提供货架均值,cov()函数提供谷物之间的方差和协方差。

manova()函数提供了组间差异的多元测试。显著的 F 值表明三个组在营养指标集合上有所不同。请注意,货架变量被转换为因子,以便它可以表示分组变量。

由于多元测试是显著的,您可以使用summary.aov()函数来获取单因素方差分析。在这里,您可以看到三个组在单独考虑的每个营养指标上都有所不同。最后,您可以使用均值比较程序(如TukeyHSD)来确定三个因变量中哪些货架彼此不同(此处省略以节省空间)。

9.7.1 评估测试假设

一元方差分析(MANOVA)的两个基本假设是多元正态性和方差协方差矩阵的同质性。第一个假设指出,依赖变量的向量共同遵循多元正态分布。您可以使用 Q-Q 图来评估此假设(有关如何工作的统计解释,请参阅侧边栏“理论插曲”)。

理论插曲

如果您有一个 p × 1 的多元正态随机向量x,其均值为µ,协方差矩阵为,那么xµ之间的马氏距离的平方是具有 p 个自由度的卡方分布。Q-Q 图将样本的卡方分布分位数与马氏 D 平方值进行绘图。在多大程度上,点沿着斜率为 1、截距为 0 的直线分布,就有证据表明数据是多元正态的。

代码显示在下一条列表中,结果图显示在图 9.10 中。

列表 9.9 评估多元正态性

> center <- colMeans(y)
> n <- nrow(y)
> p <- ncol(y)
> cov <- cov(y)
> d <- mahalanobis(y,center,cov)
> coord <- qqplot(qchisq(ppoints(n),df=p),
    d, main="Q-Q Plot Assessing Multivariate Normality",
    ylab="Mahalanobis D2")
> abline(a=0,b=1)
> identify(coord$x, coord$y, labels=row.names(UScereal))

如果数据遵循多元正态分布,则点将落在直线上。identify()函数允许您交互式地识别图中的点。点击每个感兴趣的点,然后按 ESC 键或完成按钮。在这里,数据集似乎违反了多元正态性,这主要是由于 Wheaties Honey Gold 和 Wheaties 的观测值。您可能想要删除这两个案例并重新运行分析。

图片

图 9.10 评估多元正态性的 Q-Q 图

方差-协方差矩阵同质性的假设要求每个组的协方差矩阵相等。这个假设通常使用 Box 的 M 测试来评估。R 不包括 Box 的 M 测试函数,但网络搜索将提供适当的代码。不幸的是,该测试对正态性的违反很敏感,导致在大多数典型情况下被拒绝。这意味着我们还没有一个很好的方法来评估这个重要的假设(但请参阅 Anderson (2006) 和 Silva 等人 (2008) 的有趣替代方法,这些方法在 R 中尚未提供)。

最后,你可以使用 mvoutlier 包中的 aq.plot() 函数来测试多元异常值。这个例子中的代码看起来是这样的:

library(mvoutlier)
outliers <- aq.plot(y)
outliers

尝试一下,看看你得到什么!

9.7.2 稳健 MANOVA

如果多元正态性或方差-协方差矩阵同质性的假设不可靠,或者如果你担心多元异常值,你可能想考虑使用稳健或非参数版本的 MANOVA 测试。rrcov 包中的 Wilks.test() 函数提供了一个稳健的单因素 MANOVA 版本。vegan 包中的 adonis() 函数可以提供相当于非参数 MANOVA 的结果。以下列表将 Wilks.test() 应用于示例。

列表 9.10 稳健单因素 MANOVA

> library(rrcov)
> Wilks.test(y,shelf,method="mcd")

        Robust One-way MANOVA (Bartlett Chi2)

data:  x
Wilks' Lambda = 0.511, Chi2-Value = 23.96, DF = 4.98, p-value =
0.0002167
sample estimates:
  calories    fat  sugars
1      120  0.701    5.66
2      128  1.185   12.54
3      161  1.652   10.35

从结果中,你可以看到使用对异常值和 MANOVA 假设违反都不敏感的稳健测试仍然表明,位于顶部、中间和底部货架上的谷物在营养特征上有所不同。

9.8 ANOVA 作为回归

在 9.2 节中,我们指出 ANOVA 和回归都是同一通用线性模型的特殊情况。因此,本章中的设计可以使用 lm() 函数进行分析。但要理解输出,你需要了解 R 在拟合模型时如何处理分类变量。

考虑 9.3 节中的一元 ANOVA 问题,它比较了五种降低胆固醇的药物方案(trt)的影响:

> library(multcomp)
> levels(cholesterol$trt)

[1] "1time"  "2times" "4times" "drugD"  "drugE"

首先,让我们使用 aov() 函数拟合模型:

> fit.aov <- aov(response ~ trt, data=cholesterol)
> summary(fit.aov)

            Df   Sum Sq  Mean Sq  F value     Pr(>F)    
trt          4  1351.37   337.84   32.433  9.819e-13 ***
Residuals   45   468.75    10.42

现在,让我们使用 lm() 拟合相同的模型。在这种情况下,你得到的结果将在下一个列表中显示。

列表 9.11 9.3 节中 ANOVA 问题的回归方法

> fit.lm <- lm(response ~ trt, data=cholesterol)
> summary(fit.lm)

Coefficients:
            Estimate Std. Error t value   Pr(>|t|)    
(Intercept)    5.782      1.021   5.665   9.78e-07 ***
trt2times      3.443      1.443   2.385     0.0213 *  
trt4times      6.593      1.443   4.568   3.82e-05 ***
trtdrugD       9.579      1.443   6.637   3.53e-08 ***
trtdrugE      15.166      1.443  10.507   1.08e-13 ***

Residual standard error: 3.227 on 45 degrees of freedom
Multiple R-squared: 0.7425,     Adjusted R-squared: 0.7196 
F-statistic: 32.43 on 4 and 45 DF,  p-value: 9.819e-13

你在看什么?因为线性模型需要数值预测变量,当 lm() 函数遇到因子时,它会用表示水平之间对比的数值变量集替换该因子。如果因子有 k 个水平,就会创建 k - 1 个对比变量。R 提供了五种内置方法来创建这些对比变量(见表 9.3)。你也可以创建自己的(我们在这里不会介绍这一点)。默认情况下,对于无序因子使用处理对比,对于有序因子使用正交多项式。

表 9.3 内置对比

对比 描述
contr.helmert 将第二级与第一级对立,第三级与前两级平均值对立,第四级与前三级平均值对立,依此类推。
contr.poly 对立性用于基于正交多项式的趋势分析(线性、二次、三次等)。用于具有等距水平的有序因素。
contr.sum 对立性被限制为总和为零。也称为偏差对立性,它们比较每个级别的平均值与所有级别的总体平均值。
contr.treatment 将每个水平与基线水平(默认为第一级)对立。也称为虚拟编码
contr.SAS contr.treatment类似,但基线水平是最后一个水平。这会产生类似于大多数 SAS 过程所使用的对立性系数。

在处理对立性中,因素的第一级成为参考组,每个后续级别都与它进行比较。您可以通过contrasts()函数查看编码方案:

> contrasts(cholesterol$trt)
       2times  4times  drugD  drugE
1time       0       0      0      0
2times      1       0      0      0
4times      0       1      0      0
drugD       0       0      1      0
drugE       0       0      0      1

如果患者处于drugD状态,则变量drugD等于 1,而变量2times4timesdrugE各自等于零。对于第一组,您不需要变量,因为四个指标变量中的每一个为零可以唯一确定患者处于1times状态。

在列表 9.11 中,变量trt2times代表1time2times级别之间的对立性。同样,trt4times1time4times之间的对立性,依此类推。您可以从输出中的概率值中看到,每种药物状态都与第一级(1time)有显著差异。

您可以通过指定contrasts选项来更改lm()中使用的默认对立性。例如,您可以通过使用

fit.lm <- lm(response ~ trt, data=cholesterol, contrasts="contr.helmert")

您可以通过options()函数在 R 会话期间更改默认对立性。例如,

options(contrasts = c("contr.SAS", "contr.helmert"))

将无序因素的默认对立性设置为contr.SAS,有序因素设置为contr.helmert。尽管我们已将讨论限制在对线性模型中使用对立性的应用,但请注意,它们适用于 R 中的其他建模函数。这包括第十三章中涵盖的广义线性模型。

摘要

  • 方差分析(ANOVA)是一组在分析实验和准实验研究数据时经常使用的统计方法。

  • ANOVA 方法在研究定量结果变量与一个或多个分类解释变量之间的关系时特别有用。

  • 如果定量结果变量与具有多于两个水平的分类解释变量相关,则进行事后检验以确定哪些水平/组在该结果上有所不同。

  • 当有两个或更多分类解释变量时,可以使用因子方差分析(ANOVA)来研究它们对结果变量的独特和联合影响。

  • 当一个或多个定量干扰变量的影响在统计上得到控制(消除)时,这种设计被称为协方差分析(ANCOVA)。

  • 当存在多个结果变量时,这种设计被称为多元方差分析或协方差分析。

  • 方差分析(ANOVA)和多元回归是广义线性模型的两种等价表达。这两种方法的不同术语、R 函数和输出格式反映了它们在不同研究领域中各自独立的起源。当研究关注组间差异时,ANOVA 的结果通常更容易理解和向他人传达。

10 效力分析

本章涵盖

  • 确定样本量需求

  • 计算效应量

  • 评估统计效力

作为一名统计顾问,我经常被问到,“我需要多少个受试者来进行我的研究?”有时问题是这样表达的:“我有 x 个可用于这项研究的人。这项研究值得做吗?”类似的问题可以通过 效力分析 来回答,这是实验设计中一套重要的技术。

效力分析允许你确定在给定置信度下检测到给定大小效应所需的样本量。相反,它允许你在样本量限制下确定在给定置信度水平下检测到给定大小效应的概率。如果概率过低,你明智地改变或放弃实验将是明智的。

在本章中,你将学习如何进行各种统计检验的效力分析,包括比例检验、t 检验、卡方检验、平衡单因素方差分析、相关检验和线性模型。由于效力分析适用于假设检验情况,我们将从对零假设显著性检验(NHST)的简要回顾开始。然后,我们将回顾在 R 中执行效力分析的方法,主要关注pwr包。最后,我们将考虑 R 中可用的其他效力分析方法。

10.1 假设检验的快速回顾

为了帮助你理解效力分析的步骤,我们将简要回顾一般的统计假设检验。如果你有统计背景,可以自由跳到第 10.2 节。

在统计假设检验中,你指定关于总体参数的假设(你的 零假设,或 H[0])。然后,你从这个总体中抽取一个样本,并计算一个用于对总体参数进行推断的统计量。假设零假设是真实的,你计算获得观察到的样本统计量或更极端的统计量的概率。如果概率足够小,你将拒绝零假设,转而支持其对立面(称为 备择研究 假设,H[1])。

以下是一个说明过程的例子。假设你感兴趣的是评估手机使用对驾驶员反应时间的影响。你的零假设是 H[0]: µ[1] – µ[2] = 0,其中 µ[1] 是使用手机的驾驶员的平均反应时间,µ[2] 是无手机的驾驶员的平均反应时间(在这里,µ[1] – µ[2] 是感兴趣的总体参数)。如果你拒绝这个零假设,你将剩下备择或研究假设:H[1]: µ[1] – µ[2] ≠ 0。这相当于 µ[1] ≠ µ[2],即两种条件下的平均反应时间不相等。

从个体中选取样本并随机分配到两种条件之一。在第一种条件下,参与者在一个模拟器中应对一系列驾驶挑战,同时使用手机通话。在第二种条件下,参与者完成相同的挑战,但不使用手机。对每个个体的总体反应时间进行评估。

根据样本数据,你可以计算统计量(X̄[1] − X̄[2])/(s − √n),其中 X̄[1]和 X̄[2]是两种条件下的样本反应时间均值,s 是合并样本标准差,n是每个条件下的参与者数量。如果零假设是真的,并且你可以假设反应时间是正态分布的,那么这个样本统计量将遵循具有 2n – 2 个自由度的 t 分布。利用这一事实,你可以计算得到如此大或更大的样本统计量的概率。如果概率(p)小于某个预定的截止值(例如 p < .05),你将拒绝零假设,支持备择假设。这个预定的截止值(0.05)被称为检验的显著性水平

注意,你使用样本数据来对从中抽取的总体进行推断。你的零假设是所有使用手机的驾驶员的平均反应时间与所有未使用手机的驾驶员的平均反应时间没有差异,而不仅仅是你的样本中的驾驶员。你决策的四个可能结果如下:

  • 如果零假设是错误的,并且统计检验导致你拒绝它,那么你做出了正确的决定。你正确地确定反应时间受到手机使用的影响。

  • 如果零假设是真的,而你没有拒绝它,那么你又做出了正确的决定。反应时间不受手机使用的影响。

  • 如果零假设是真的,但你拒绝了它,那么你犯了第一类错误。你得出结论说手机使用会影响反应时间,但实际上并没有。

  • 如果零假设是错误的,但你未能拒绝它,那么你犯了第二类错误。手机使用会影响反应时间,但你未能察觉这一点。

表 10.1 中展示了这些结果的每一个。

表 10.1 假设检验

决策
拒绝 H[0]
实际 H[0] true 第一类错误
H[0] false 正确 第二类错误

零假设显著性检验的争议

零假设显著性检验并非没有争议,批评者提出了许多关于它的担忧,尤其是在心理学领域。他们指出,对 p 值的广泛误解、对统计显著性而非实际显著性的依赖、零假设永远不会完全正确,并且对于足够大的样本量总会被拒绝的事实,以及 NHST 实践中存在的一些逻辑不一致。

对这个主题的深入讨论超出了本书的范围。对此感兴趣的读者可参考 Harlow, Mulaik, 和 Steiger(1997)。

在规划研究时,研究人员通常会特别关注四个数量(见图 10.1)

  • 样本量指的是实验设计中每个条件/组的观测数量。

  • 显著性水平(也称为alpha)定义为犯第一类错误的概率。显著性水平也可以理解为发现不存在的效果的概率。

  • 功效定义为犯第二类错误的概率的补数。功效可以理解为发现存在效应的概率。

  • 效应量是指在备择或研究假设下的效应大小。效应量的公式取决于假设检验中使用的统计方法。

图 10.1 研究设计功效分析中考虑的四个主要数量。给定任意三个,你可以计算出第四个。

尽管研究人员直接控制样本量和显著性水平,但功效和效应量受到更间接的影响。例如,当你放宽显著性水平(换句话说,使拒绝零假设更容易),功效会增加。同样,增加样本量也会增加功效。

你的研究目标通常是最大化统计测试的功效,同时保持可接受的显著性水平,并尽可能使用最小的样本量。也就是说,你希望最大限度地提高发现真实效应的机会,同时最大限度地减少发现不存在效应的机会,同时将研究成本控制在合理范围内。

四个数量(样本量、显著性水平、功效和效应量)之间有着密切的关系。给定任意三个,你可以确定第四个。你将使用这一事实在本章的剩余部分进行各种功效分析。在下一节中,我们将探讨使用 R 包 pwr 实现功效分析的方法。稍后,我们将简要介绍一些在生物学和遗传学中使用的非常专业的功效函数。

10.2 使用 pwr 包实现功效分析

由 Stéphane Champely 开发的 pwr 包实现了 Cohen(1988)概述的功效分析。表 10.2 列出了一些较为重要的函数。对于每个函数,用户可以指定四个数量中的三个(样本量、显著性水平、功效、效应量),第四个将被计算。

表 10.2 pwr 包函数

函数 ... 的功效计算
pwr.2p.test 双样本比例(样本量相等)
pwr.2p2n.test 双样本比例(样本量不等)
pwr.anova.test 平衡单因素方差分析
pwr.chisq.test 卡方检验
pwr.f2.test 一般线性模型
pwr.p.test 比例(单样本)
pwr.r.test 相关系数
pwr.t.test T 检验(单样本,双样本,配对)
pwr.t2n.test T 检验(两个样本,n 不等)

在这四个量中,效应量通常是最难指定的。计算效应量通常需要一些与所涉及的措施相关的经验和对以往研究的了解。但如果你对给定研究中预期的效应量一无所知,你能做什么呢?你将在第 10.2.7 节中探讨这个难题。在本节的其余部分,你将了解pwr函数在常见统计检验中的应用。在调用这些函数之前,请确保安装并加载pwr包。

10.2.1 T 检验

当你将要使用的统计检验是 t 检验时,pwr.t.test() 函数提供了一系列有用的功效分析选项。其格式为

pwr.t.test(n=, d=, sig.level=, power=, alternative=)

其中

  • n 是样本大小。

  • d 是效应量,定义为标准化均值差异。

图片

  • sig.level 是显著性水平(默认为 0.05)。

  • power 是功效水平。

  • type 是双样本 t 检验("two.sample")、单样本 t 检验("one.sample")或配对样本 t 检验("paired")。默认情况下是双样本检验。

  • alternative 表示统计检验是双尾("two.sided")还是单尾("less""greater")。默认情况下是双尾检验。

让我们通过一个例子来分析。继续第 10.1 节中关于手机使用和驾驶反应时间的实验,假设你将使用双尾独立样本 t 检验来比较手机条件下参与者的平均反应时间与未受干扰驾驶的参与者的平均反应时间。

假设你从以往的经验中知道反应时间有 1.25 秒的标准差。此外,假设反应时间差 1 秒被认为是一个重要的差异。因此,你希望进行一项研究,能够检测到效应量 d = 1/1.25 = 0.8 或更大的效应。此外,你想要有 90%的把握检测到这种差异(如果存在),并且有 95%的把握不会宣布差异是显著的,而实际上它是由随机变异性引起的。你的研究中需要多少参与者?

将这些信息输入到pwr.t.test() 函数中,你将得到以下结果:

> library(pwr)
> pwr.t.test(d=.8, sig.level=.05, power=.9, type="two.sample",         
             alternative="two.sided")

     Two-sample t test power calculation 

              n = 34
              d = 0.8
      sig.level = 0.05
          power = 0.9
    alternative = two.sided

 NOTE: n is number in *each* group

结果表明,你需要每组有 34 名参与者(总共 68 名参与者)才能以 90%的确定性检测到 0.8 的效应量,并且错误地得出存在差异的结论的概率不超过 5%,而实际上并不存在差异。

让我们改变问题。假设在比较两种条件时,你想要能够检测到总体均值之间 0.5 个标准差差异。你想要将错误声明总体均值不同的几率限制在 100 分之 1。此外,你只能负担得起在研究中包含 40 名参与者。在这些限制条件下,你能够检测到这种大差异的概率是多少?

假设每个条件下将放置相同数量的参与者,你有

> pwr.t.test(n=20, d=.5, sig.level=.01, type="two.sample", 
             alternative="two.sided")

     Two-sample t test power calculation 

              n = 20
              d = 0.5
      sig.level = 0.01
          power = 0.14
    alternative = two.sided

 NOTE: n is number in *each* group

每组有 20 名参与者,先验显著性水平为 0.01,以及因变量标准差为 1.25 秒,声明 0.625 秒或更小差异不显著的几率小于 14%(d = 0.5 = 0.625/1.25)。相反,有 86%的几率你会错过你正在寻找的效果。你可能需要认真重新考虑是否投入时间和精力进行这项研究。

之前的例子假设两组的样本大小相等。如果两组的样本大小不相等,函数

pwr.t2n.test(n1=, n2=, d=, sig.level=, power=, alternative=)

可以使用。在这里,n1n2是样本大小,其他参数与pwer.t.test相同。尝试改变输入到pwr.t2n.test函数的值,看看对输出的影响。

10.2.2 ANOVA

pwr.anova.test()函数为平衡单因素方差分析提供了功效分析选项。格式是

pwr.anova.test(k=, n=, f=, sig.level=, power=) 

其中k是组数,n是每组中共同的样本大小。

对于单因素方差分析(ANOVA),效应大小通过f来衡量,其中

让我们尝试一个例子。对于比较五个组的单因素方差分析,当效应大小为 0.25 且采用显著性水平为 0.05 时,计算每组所需的样本大小以获得 0.80 的功效。代码如下:

> pwr.anova.test(k=5, f=.25, sig.level=.05, power=.8)

     Balanced one-way analysis of variance power calculation 

              k = 5
              n = 39
              f = 0.25
      sig.level = 0.05
          power = 0.8

 NOTE: n is number in each group

因此,总样本大小是 5 × 39,即 195。请注意,此示例要求你估计五个组的均值以及共同方差。当你不知道预期什么时,第 10.2.7 节中描述的方法可能会有所帮助。

10.2.3 相关系数

pwr.r.test()函数为相关系数的检验提供了功效分析。格式是

pwr.r.test(n=, r=, sig.level=, power=, alternative=)

其中n是观测值的数量,r是效应大小(通过线性相关系数衡量),sig.level是显著性水平,power是功效水平,而alternative指定了双尾("two.sided")或单尾("less""greater")的显著性检验。

例如,假设你正在研究抑郁和孤独感之间的关系。你的零假设和备择假设是

H[0]: ρ ≤ 0.25 对比 H[1]: ρ > 0.25

其中ρ是这两个心理变量之间的总体相关系数。你已将显著性水平设置为 0.05,并希望以 90%的置信度拒绝H[0](如果它是错误的)。你需要多少个观测值?以下代码提供了答案:

> pwr.r.test(r=.25, sig.level=.05, power=.90, alternative="greater")

     approximate correlation power calculation (arctangh transformation) 

              n = 134
              r = 0.25
      sig.level = 0.05
          power = 0.9
    alternative = greater

因此,你需要评估 134 名参与者的抑郁和孤独感,以 90%的置信度确保你将拒绝错误的零假设。

10.2.4 线性模型

对于线性模型(如多元回归),可以使用pwr.f2.test()函数进行功效分析。格式如下

pwr.f2.test(u=, v=, f2=, sig.level=, power=) 

其中uv是分子和分母自由度,f2是效应大小。

f2的第一个公式适用于评估一组预测变量对结果的影响。第二个公式适用于评估一组预测变量(或协变量)对另一组预测变量(或协变量)的影响。

假设你对老板的领导风格是否会影响员工满意度超过与工作相关的工资和福利感兴趣。领导风格通过四个变量进行评估,而工资和福利与三个变量相关。以往的经验表明,工资和福利大约占员工满意度变化的 30%。从实际的角度来看,如果领导风格至少比这个数字高 5%,将会很有趣。假设显著性水平为 0.05,为了以 90%的置信度识别这种贡献,需要多少名受试者?

在这里,sig.level=0.05,power=0.90,u=3(总预测变量数减去集合 B 中预测变量的数量),效应大小为f2 = (.35 – .30)/(1 – .35) = 0.0769。将此输入函数中,得到以下结果:

> pwr.f2.test(u=3, f2=0.0769, sig.level=0.05, power=0.90)

     Multiple regression power calculation 

              u = 3
              v = 184.2426
             f2 = 0.0769
      sig.level = 0.05
          power = 0.9

在多元回归中,分母自由度等于Nk – 1,其中N是观测数,k是预测变量的数量。在这种情况下,N – 7 – 1 = 185,这意味着所需的样本量是N = 185 + 7 + 1 = 193。

10.2.5 比例检验

可以使用pwr.2p.test()函数对比较两个比例进行功效分析。格式如下

pwr.2p.test(h=, n=, sig.level=, power=) 

其中h是效应大小,n是每组中共同的样本大小。效应大小h定义为

可以使用函数ES.h(p1, p2)计算。

对于不等的n,所需的函数是

pwr.2p2n.test(h =, n1 =, n2 =, sig.level=, power=)

可以使用alternative=选项来指定双尾("two.sided")或单尾("less"或"greater")检验。双尾检验是默认选项。

假设你怀疑一种流行的药物可以使 60%的用户症状缓解。如果一种新的(且更昂贵的)药物可以使 65%的用户症状改善,那么如果你想要检测这种大的差异,你需要在比较这两种药物的研究中包含多少名参与者?

假设你想要以 90% 的置信度得出结论,新药比现有药物更好,并且以 95% 的置信度确信你不会错误地得出这个结论。你将使用单尾检验,因为你只对评估新药是否比标准药物更好感兴趣。代码如下:

> pwr.2p.test(h=ES.h(.65, .6), sig.level=.05, power=.9, 
              alternative="greater")

     Difference of proportion power calculation for binomial 
     distribution (arcsine transformation) 

              h = 0.1033347
              n = 1604.007
      sig.level = 0.05
          power = 0.9
    alternative = greater

 NOTE: same sample sizes

根据这些结果,你需要进行一项研究,其中 1,605 人接受新药,1,605 人接受现有药物,以满足标准。

10.2.6 卡方检验

卡方检验通常用于评估两个分类变量之间的关系。零假设通常是变量之间是独立的,而研究假设则认为它们不是独立的。pwr.chisq.test() 函数可用于在应用卡方检验时评估功效、效应量或所需的样本量。格式如下:

pwr.chisq.test(w =, N = , df = , sig.level =, power = ) 

其中 w 是效应量,N 是总样本量,df 是自由度。在这里,效应量 w 定义为

求和从 1 到 m,其中 m 是列联表中的单元格数。ES.w2(P) 函数可用于计算在双向列联表中对应于备择假设的效应量。在这里,P 是假设的双向概率表。

作为一个简单的例子,让我们假设你正在研究种族与提拔之间的关系。你预计你的样本中 70% 将是白人,10% 将是非裔美国人,20% 将是西班牙裔。此外,你认为 60% 的白人倾向于被提拔,而 30% 的非裔美国人和 50% 的西班牙裔则不是。你的研究假设是提拔的概率遵循表 10.3 中的值。

表 10.3 基于研究假设预期被提拔的个人比例

种族 提拔 未提拔
白人 0.42 0.28
非裔美国人 0.03 0.07
西班牙裔 0.10 0.10

例如,你预计人口中有 42% 将是提拔的白人(.42 = .70 × .60)和 7% 将是未被提拔的非裔美国人(.07 = .10 × .70)。假设显著性水平为 0.05,并且你希望的功效水平为 0.90。双向列联表中的自由度是 (r – 1) × (c – 1),其中 r 是行数,c 是列数。你可以使用以下代码计算假设的效应量:

> prob <- matrix(c(.42, .28, .03, .07, .10, .10), byrow=TRUE, nrow=3)
> ES.w2(prob)

[1] 0.1853198

使用这些信息,你可以这样计算所需的样本量:

> pwr.chisq.test(w=.1853, df=2, sig.level=.05, power=.9)

     Chi squared power calculation 

              w = 0.1853
              N = 368.5317
             df = 2
      sig.level = 0.05
          power = 0.9

 NOTE: N is the number of observations

结果表明,一个有 369 名参与者的研究将足以在给定的效应量、功效和显著性水平下检测种族与提拔之间的关系。

10.2.7 在新情况下选择适当效应量

在功效分析中,预期效应量是最难确定的参数。通常需要你对主题领域和采用的措施有经验。例如,可以使用过去研究的数据来计算效应量,然后可以用这些效应量来规划未来的研究。

但当研究情况完全新颖且你没有经验可以借鉴时,你能做什么呢?在行为科学中,Cohen(1988 年)试图为各种统计检验的“小”、“中”和“大”效应量提供基准。表 10.4 提供了这些指南。

表 10.4 Cohen 的效应量基准

统计方法 效应量度量 效应量建议指南
t 检验 d 0.20
ANOVA f 0.10
线性模型 f2 0.02
比例检验 h 0.20
卡方检验 w 0.10

当你不知道可能存在什么效应量时,这张表可能提供一些指导。例如,如果你使用的是包含 5 个组、每组 25 个受试者的一元方差分析,显著性水平为 0.05,拒绝错误零假设(即发现真实效应)的概率是多少?

使用pwr.anova.test()函数和表 10.3 中的f行建议,得到以下结果:

pwr.anova.test(k=5, n=25, sig.level=0.05, f=c(.10, .25, .40))

     Balanced one-way analysis of variance power calculation 
              k = 5
              n = 25
              f = 0.10, 0.25, 0.40
      sig.level = 0.05
          power = 0.1180955, 0.5738000, 0.9569163

       NOTE: n is number in each group

功效为检测小效应为 0.118,检测中等效应为 0.574,检测大效应为 0.957。鉴于样本量限制,你只有在效应量很大时才有可能发现效应。

重要的是要记住,Cohen 的基准只是从一系列社会研究研究中得出的普遍建议,可能不适用于你的特定研究领域。一种替代方法是改变研究参数,并注意对样本量和功效等事物的影响。例如,再次假设你想使用一元方差分析和 0.05 的显著性水平来比较五个组。以下列表计算了检测各种效应量所需的样本量,并将结果绘制在图 10.2 中。

列表 10.1 在一元方差分析中检测显著效应的样本量

library(pwr)
es <- seq(.1, .5, .01)                                               
nes <- length(es)

samsize <- NULL                                                      
for (i in 1:nes){                                                    
    result <- pwr.anova.test(k=5, f=es[i], sig.level=.05, power=.9)  
    samsize[i] <- ceiling(result$n)                                  
}                                                                    

plotdata <- data.frame(es, samsize)
library(ggplot2)
ggplot(plotdata, aes(x=samsize, y=es)) +
  geom_line(color="red", size=1) +
  theme_bw() +
  labs(title="One Way ANOVA (5 groups)",
       subtitle="Power = 0.90,  Alpha = 0.05",
       x="Sample Size (per group)",
       y="Effect Size") 

图像

图 10.2 在一个包含五个组的一元方差分析中检测各种效应量所需的样本量(假设功效为 0.90,显著性水平为 0.05)

这样的图表可以帮助你估计各种条件对实验设计的影响。例如,增加每组 200 个以上的观察值似乎没有带来多少效益。我们将在下一节中查看另一个绘图示例。

10.3 创建功效分析图

在离开 pwr 包之前,让我们看看一个更复杂的绘图示例。假设你想查看在一系列效应大小和功率水平下,声明相关系数统计显著的必要样本量。你可以使用 pwr.r.test() 函数和 for 循环来完成此任务,如下面的列表所示。

列表 10.2 检测各种大小相关性的样本量曲线

library(pwr)
r <- seq(.1,.5,.01)                                             ❶
p <- seq(.4,.9,.1)                                              ❶

df <- expand.grid(r, p)
colnames(df) <- c("r", "p")

for (i in 1:nrow(df)){                                          ❷
    result <- pwr.r.test(r = df$r[i],                           ❷
                         sig.level = .05, power = df$p[i],      ❷
                         alternative = "two.sided")             ❷
    df$n[i] <- ceiling(result$n)                                ❷
}                                                               ❷

library(ggplot2)                                                ❸
ggplot(data=df,                                                 ❸
       aes(x=r, y=n, color=factor(p))) +                        ❸
  geom_line(size=1) +                                           ❸
  theme_bw() +                                                  ❸
  labs(title="Sample Size Estimation for Correlation Studies",  ❸
       subtitle="Sig=0.05 (Two-tailed)",                        ❸
       x="Correlation Coefficient (r)",                         ❸
       y="Samsple Size (n)",                                    ❸
       color="Power")                                           ❸

❶ 设置相关性和功率值的范围

❷ 获取样本量

❸ 绘制功率曲线

列表 10.2 使用 seq() 函数生成一系列效应大小 r(在 H[1] 下的相关系数)和功率水平 p ❶。expand.grid() 函数创建一个包含这两个变量所有组合的数据框。然后,一个 for 循环遍历数据框的行,计算该行的相关性和功率水平的样本量(n),并将结果保存 ❷。ggplot2 包为每个功率水平绘制样本量与相关性的曲线图 ❸。图 10.3 显示了生成的图表。如果你以灰度阅读此章节,线条颜色可能难以区分。从底部开始,线条代表功率为 0.4、0.5、0.6,直到 0.9。

如图表所示,你需要大约 75 个样本量来检测 0.20 的相关性,置信度为 40%。你需要大约 185 个额外的观察值(n = 260)来检测相同的相关性,置信度为 90%。通过简单的修改,这种方法可以用于创建广泛统计测试的样本量和功率曲线图。

图 10.3 在各种功率水平下检测显著相关性的样本量曲线

我们将简要回顾本章中其他对功率分析有用的 R 函数。

10.4 其他包

R 中的许多其他包在研究规划阶段可能很有用。表 10.5 列出了几个。其中一些包含通用工具,而另一些则高度专业化。表中最后四个特别关注遗传研究中的功率分析。全基因组关联研究(GWAS)用于识别与可观察性状的遗传关联。例如,这些研究将关注为什么有些人会患上特定类型的心脏病。

表 10.5 专业的功率分析包

目的
asypow 通过渐近似然比方法进行功率计算
longpower 长期数据的样本量计算
PwrGSD 组成序列设计的功率分析
pamm 混合模型中随机效应的功率分析
powerSurvEpi 在流行病学研究中进行生存分析的功率和样本量计算
powerMediation 线性、逻辑、泊松和 Cox 回归中中介效应的功率和样本量计算
semPower 结构方程模型(SEM)的功率分析
powerpkg 影响同胞对和传递不平衡检验(TDT)设计的功率分析
powerGWASinteraction GWAS 交互作用的功率计算
gap 用于病例-队列设计中功率和样本量计算的函数
ssize.fdr 用于微阵列实验的样本量计算

最后,MBESS 和WebPower包包含了一系列可用于各种形式功率分析和样本量确定的函数。这些函数特别适用于行为学、教育学和社会科学的研究者。

摘要

  • 功率分析有助于确定在给定置信度下区分给定大小效果所需的样本量。它还可以告诉你,对于给定样本量检测这种效果的几率。你可以直接看到限制错误地宣布效果显著的可能性(I 型错误)与正确识别真实效果的几率(功率)之间的权衡。

  • pwr包提供的函数可用于执行常见统计方法(包括 t 检验、卡方检验和比例检验、方差分析和回归)的功率和样本量确定。第 10.4 节最后介绍了更多专业的方法。

  • 功率分析通常是一个交互过程。研究者会改变样本量、效果大小、期望的显著性水平和期望的功率等参数,以观察它们之间的相互影响。这些结果被用来规划更有可能产生有意义结果的研究。来自过去研究(尤其是关于效应量)的信息可用于设计更有效和高效的未来研究。

  • 功率分析的一个重要附带好处是它鼓励人们从单一关注二元假设检验(即效果是否存在)转向对所考虑效果大小的欣赏。期刊编辑越来越多地要求作者在报告研究结果时包括效应量和 p 值。这有助于你确定研究的应用意义,并提供可用于规划未来研究的信息。

11 中级图表

本章涵盖

  • 可视化双变量和多变量关系

  • 使用散点和折线图进行工作

  • 理解 corrgrams

  • 使用马赛克图

在第六章(基本图表)中,我们考虑了广泛的各种图表类型,用于显示单个分类或连续变量的分布。第八章(回归)回顾了在从一组预测变量预测连续结果变量时有用的图形方法。在第九章(方差分析)中,我们考虑了特别适用于可视化组在连续结果变量上的差异的技术。在许多方面,本章是我们之前讨论主题的延续和扩展。

在本章中,我们将专注于显示两个变量(双变量关系)和多个变量(多变量关系)之间关系的图形方法。例如:

  • 汽车油耗与汽车重量之间有什么关系?这是否会因汽车气缸数量而变化?

  • 您如何在一张图上展示汽车的油耗、重量、排量和后轴比之间的关系?

  • 当绘制来自大型数据集(例如,10,000 个观测值)的两个变量的关系图时,您如何处理您可能会看到的巨大数据点重叠?换句话说,当您的图表变成一大团模糊时,您该怎么办?

  • 您如何在一次可视化中同时展示三个变量之间的多变量关系(给定一个 2D 计算机屏幕或一张纸,以及略低于最新《星球大战》电影的预算)?

  • 您如何显示几棵树随时间增长的情况?

  • 您如何在一张图上可视化十几个变量之间的相关性?这如何帮助您理解数据的结构?

  • 您如何可视化“泰坦尼克号”乘客生存与阶级、性别和年龄之间的关系?从这样的图表中您可以学到什么?

这些是本章描述的方法可以回答的问题类型。我们将使用的数据集是可能性的示例。一般技术是最重要的。如果您对汽车特性或树木生长不感兴趣,请插入您自己的数据。

我们将从散点图和散点图矩阵开始。然后我们将探索各种类型的折线图。这些方法在研究中是众所周知的,并且被广泛使用。接下来,我们将回顾 corrgrams 在可视化相关性以及马赛克图在可视化分类变量间的多变量关系中的应用。这些方法也很有用,但在研究人员和数据分析师中却知之甚少。您将看到如何使用这些方法中的每一个来更好地理解您的数据,并将这些发现传达给他人。

11.1 散点图

如你所见,散点图描述了两个连续变量之间的关系。在本节中,我们将从一个双变量关系的描述开始(xy)。然后,我们将探讨通过叠加额外信息来增强此图的方法。接下来,你将学习如何将多个散点图组合成散点图矩阵,以便一次查看多个双变量关系。我们还将回顾许多数据点重叠的特殊情况,这限制了你对数据的描绘能力,并讨论几种克服这种困难的方法。最后,我们将通过添加第三个连续变量将二维图扩展到三维,这将包括 3D 散点图和气泡图。每种都可以帮助你一次理解三个变量之间的多变量关系。

我们将首先可视化汽车重量与燃油效率之间的关系。以下列表提供了一个示例。

列表 11.1 一个带有最佳拟合线的散点图

data(mtcars)                                                  ❶ 
ggplot(mtcars, aes(x=wt, y=mpg)) +  geom_point()              ❷  
  geom_smooth(method="lm", se=FALSE, color="red") +           ❸ 
  geom_smooth(method="loess", se=FALSE,                       ❹ 
              color="blue", linetype="dashed") +
  labs(title = "Basic Scatter Plot of MPG vs. Weight",        ❺
       x = "Car Weight (lbs/1000)",
       y = "Miles Per Gallon")     

❶ 加载数据

❷ 创建散点图

❸ 添加线性拟合

❹ 添加局部加权回归拟合

❺ 添加注释

图 11.1 显示了结果图。

图 11.1 汽车里程与重量的散点图,叠加线性拟合和 loess 拟合线

列表 11.1 中的代码加载了内置数据框mtcars的新副本 ❶,并使用填充圆圈作为绘图符号创建了一个基本的散点图 ❷。正如预期的那样,随着汽车重量的增加,每加仑英里数减少,尽管这种关系并不完全线性。第一个geom_smooth()函数添加了一条线性拟合线(实线红色) ❸。se=FALSE选项抑制了线的 95%置信区间。第二个geom_smooth()函数添加了一条局部加权回归(loess)拟合线(虚线蓝色) ❹。loess 线是一种基于局部加权多项式回归的非参数拟合线,为数据提供了一个平滑的趋势线。有关算法的技术细节,请参阅 Cleveland (1981)。Josh Starmer 在 YouTube 上提供了一个高度直观的 loess 拟合线解释(www.youtube.com/watch?v=Vf7oJ6z2LCc)。

如果我们想分别查看 4、6 和 8 缸汽车的重量与燃油效率之间的关系,这可以通过ggplot2和少量对先前代码的简单修改轻松完成。图 11.2 提供了该图。

列表 11.2 一个带有单独最佳拟合线的散点图

ggplot(mtcars, 
       aes(x=wt, y=mpg, 
           color=factor(cyl), 
           shape=factor(cyl))) +
  geom_point(size=2) +
  geom_smooth(method="lm", se=FALSE) +
  geom_smooth(method="loess", se=FALSE, linetype="dashed") +
  labs(title = "Scatter Plot of MPG vs. Weight",
       subtitle = "By Number of Cylinders",
       x = "Car Weight (lbs/1000)",
       y = "Miles Per Gallon",
       color = "Number of \nCylinders",
       shape = "Number of \nCylinders") +
  theme_bw()                                                     

图 11.2 带有子组和单独估计拟合线的散点图

通过在aes()函数中将气缸数映射到颜色和形状,三个组(4、6 或 8 个气缸)通过颜色和绘图符号以及单独的线性拟合和 loess 拟合线进行区分。由于cyl变量是数值型的,因此使用factor(cyl)将变量转换为离散类别。

您可以使用span参数控制 loess 线的平滑度。默认值为geom_smooth(method="loess", span=0.75)。较大的值会导致更平滑的拟合。在本例中,loess 线过度拟合了数据(过于紧密地跟随点)。span=4(未显示)的值提供了一个更平滑的拟合。

散点图有助于您一次可视化两个定量变量之间的关系,但如果你想要查看汽车油耗、重量、排量(立方英寸)和后轴比之间的双变量关系呢?当存在多个定量变量时,您可以使用散点图矩阵来表示它们之间的关系。

11.1.1 散点图矩阵

R 中提供了许多用于创建散点图矩阵的有用函数。基础 R 提供了pairs()函数来创建简单的散点图矩阵。第 8.2.4 节(多重线性回归)展示了使用scatterplotMatrix函数从car包创建散点图矩阵的方法。

在本节中,我们将使用GGally包中的ggpairs()函数创建散点图矩阵的ggplot2版本。正如您将看到的,这种方法提供了创建高度定制图形的选项。在继续之前,请确保安装GGally包(install.packages("GGally"))。

首先,让我们为mtcars数据框中的mpgdispdratwt变量创建一个默认的散点图矩阵:

library(GGally)
ggpairs(mtcars[c("mpg","disp","drat", "wt")])

图 11.3 显示了生成的图形。

默认情况下,矩阵的主对角线包含每个变量的核密度曲线(详细信息请参阅第 6.5 节)。每加仑英里数呈右偏态(有几个高值),后轴比似乎呈双峰分布。六个散点图位于主对角线下方。每加仑英里数与发动机排量的散点图位于这两个变量的交叉点(第二行,第一列),表明存在负相关关系。每对变量之间的皮尔逊相关系数位于主对角线上方。每加仑英里数与发动机排量之间的相关系数为-0.848(第一行,第二列),支持我们的结论:随着发动机排量的增加,油耗会降低。

图片

图 11.3 由ggpairs()函数创建的散点图矩阵

接下来,我们将创建一个高度定制的散点图矩阵,添加拟合线、直方图和个性化主题。ggpairs()函数允许您为创建主对角线、主对角线下方和主对角线上方的绘图指定单独的函数。以下列表提供了代码。

列表 11.3 带拟合线、直方图和相关性系数的散点图矩阵

library(GGally)

diagplots <- function(data, mapping) {                                     ❶
  ggplot(data = data, mapping = mapping) +                                 ❶
    geom_histogram(fill="lightblue", color="black")                        ❶
}                                                                          ❶

lowerplots <- function(data, mapping) {                                    ❷
    ggplot(data = data, mapping = mapping) +                               ❷
      geom_point(color="darkgrey") +                                       ❷
      geom_smooth(method = "lm", color = "steelblue", se=FALSE) +          ❷
      geom_smooth(method="loess", color="red", se=FALSE, linetype="dashed")❷
}                                                                          ❷
upperplots <- function(data, mapping) {                                    ❸
    ggally_cor(data=data, mapping=mapping,                                 ❸
               display_grid=FALSE, size=3.5, color="black")                ❸
}                                                                          ❸

mytheme <-  theme(strip.background = element_blank(),                      ❹
                  panel.grid       = element_blank(),                      ❹
                  panel.background = element_blank(),                      ❹
                  panel.border = element_rect(color="grey20", fill=NA))    ❹

ggpairs(mtcars,                                                            ❺
        columns=c("mpg","disp", "drat", "wt"),                             ❺
        columnLabels=c("MPG", "Displacement",                              ❺
                       "R Axle Ratio", "Weight"),                          ❺
        title = "Scatterplot Matrix with Linear and Loess Fits",           ❺
        lower = list(continuous = lowerplots),                             ❺
        diag =  list(continuous = diagplots),                              ❺
        upper = list(continuous = upperplots)) +                           ❺
        mytheme                                                            ❺

❶ 主对角线上的绘图函数

❷ 主对角线下方的绘图函数

❸ 主对角线上方的绘图函数

❹ 定制主题

❺ 生成散点图矩阵

首先,定义了一个函数用于使用浅蓝色条带和黑色边框创建直方图 ❶。接下来,创建了一个函数用于生成带有深灰色点的散点图,最佳拟合线为钢蓝色,以及一条虚线红色洛伦兹平滑线。置信区间被抑制(se=FALSE) ❷。指定了第三个函数用于显示相关系数 ❸。此函数使用 ggally_cor() 函数获取并打印系数,而大小和颜色选项影响外观,displayGrid 选项抑制网格线。还添加了一个自定义主题 ❹。此可选步骤消除了面元条和网格线,并将每个单元格包围在一个灰色框中。

最后,ggpairs() 函数 ❺ 使用这些函数在图 11.4 中创建自定义图形。columns 选项指定变量,columnLabels 选项提供描述性名称。lowerdiagupper 选项指定用于创建矩阵每个部分的单元格图的函数。这种方法在设计最终图形时提供了很大的灵活性。

R 提供了许多创建散点图矩阵的其他方法。你可能想探索 lattice 包中的 splom() 函数,TeachingDemos 包中的 pairs2() 函数,HH 包中的 xysplom() 函数,ResourceSelection 包中的 kdepairs() 函数,以及 SMPracticals 包中的 pairs.mod()。每个都添加了自己的独特风格。分析师们一定喜欢散点图矩阵!

图 11.4 使用 ggpairs() 函数创建的散点图矩阵和用户提供的散点图、直方图和相关性函数

11.1.2 高密度散点图

当数据点之间存在显著重叠时,散点图在观察关系方面变得不那么有用。考虑以下虚构的例子,其中 10,000 个观测值落入两个重叠的数据点集群:

set.seed(1234)
n <- 10000
c1 <- matrix(rnorm(n, mean=0, sd=.5), ncol=2)
c2 <- matrix(rnorm(n, mean=3, sd=2), ncol=2)
mydata <- rbind(c1, c2)
mydata <- as.data.frame(mydata)
names(mydata) <- c("x", "y")

如果你使用以下代码在这些变量之间生成一个标准的散点图

ggplot(mydata, aes(x=x, y=y)) + geom_point() +
  ggtitle("Scatter Plot with 10,000 Observations")

你将获得图 11.5 中的图形。

图 11.5 包含 10,000 个观测值和显著重叠数据点的散点图。请注意,数据点的重叠使得难以辨别数据浓度最大的地方。

图 11.5 中数据点的重叠使得难以辨别 xy 之间的关系。R 提供了几种图形方法,当这种情况发生时可以使用,包括分箱、颜色和透明度来指示图上任何点的重叠数据点的数量。

smoothScatter() 函数使用核密度估计来生成散点图的平滑颜色密度表示。代码

with(mydata,
     smoothScatter(x, y, 
                   main="Scatter Plot Colored by Smoothed Densities"))

生成了图 11.6 中的图形。

图 11.6 使用 smoothScatter() 绘制平滑密度估计的散点图。密度很容易从图中读取。

使用另一种方法,ggplot2 包中的 geom_hex() 函数提供了双变量分箱到六边形单元格(听起来比看起来好)。基本上,绘图区域被分成一个六边形网格,每个单元格中的点数使用颜色或阴影显示。将此函数应用于数据集

ggplot(mydata, aes(x=x, y=y)) + 
  geom_hex(bins=50) +
  scale_fill_continuous(trans = 'reverse') +
  ggtitle("Scatter Plot with 10,000 Observations")

给出图 11.7 中的散点图。

默认情况下,geom_hex() 使用较浅的颜色来表示更高的密度。在您的代码中,函数 scale_fill_continuous(trans = 'reverse') 确保使用较深的颜色来表示密度更高的区域。我认为这更直观,并且与用于可视化大型数据集的其他 R 函数的方法相匹配。

注意,hexbin 包中的 hexbin() 函数以及 IDPmisc 包中的 iplot() 函数可以用来为大型数据集创建可读的散点图矩阵。请参阅 ?hexbin?iplot 以获取示例。

图像

图 11.7 使用六边形分箱显示每个点的观测数。数据集中度容易看出,并且可以从图例中读取计数。

11.1.3 3D 散点图

散点图和散点图矩阵显示双变量关系。如果您想同时可视化三个定量变量的交互作用,可以使用 3D 散点图。

例如,假设你对汽车油耗、重量和排量之间的关系感兴趣。您可以使用 scatterplot3d 包中的 scatterplot3d() 函数来描绘它们之间的关系。格式如下

scatterplot3d(*x, y, z*) 

其中 x 在水平轴上绘制,y 在垂直轴上绘制,z 以透视方式绘制。继续上述示例,

library(scatterplot3d)
with(mtcars,
     scatterplot3d(wt, disp, mpg,
                      main="Basic 3D Scatter Plot"))

生成图 11.8 中的 3D 散点图。

图像

图 11.8 3D 散点图:每加仑英里数、汽车重量和排量

scatterplot3d() 函数提供了许多选项,包括指定符号、坐标轴、颜色、线条、网格、突出显示和角度。例如,以下代码

library(scatterplot3d) 
with(mtcars,
     scatterplot3d(wt, disp, mpg,
                   pch=16,
                   highlight.3d=TRUE,
                   type="h",
                   main="3D Scatter Plot with Vertical Lines"))              

生成一个带有突出显示的 3D 散点图,增强了深度印象,并且有垂直线将点连接到水平平面(见图 11.9)。

图像

图 11.9 带有垂直线和阴影的 3D 散点图

作为最后的例子,让我们将之前的图形添加一个回归平面。代码如下

library(scatterplot3d) 
s3d <-with(mtcars,
           scatterplot3d(wt, disp, mpg,
                         pch=16,
                         highlight.3d=TRUE,
                         type="h",
          main="3D Scatter Plot with Vertical Lines and Regression Plane"))
fit <- lm(mpg ~ wt+disp, data=mtcars)
s3d$plane3d(fit)

图 11.10 显示了结果图形。

图像

图 11.10 带有垂直线、阴影和叠加回归平面的 3D 散点图

该图表允许您通过多重回归方程从汽车重量和排量来可视化每加仑英里数的预测。平面代表预测值,点代表实际值。平面到点的垂直距离是残差。位于平面以上的点是预测不足,而位于线以下的点是预测过度。第八章介绍了多重回归。

11.1.4 旋转 3D 散点图

如果您能够与三维散点图进行交互,那么三维散点图就更容易解释。R 提供了多种机制来旋转图表,这样您就可以从多个角度看到绘制的点。

例如,您可以使用 rgl 包中的 plot3d() 函数创建一个交互式 3D 散点图。它创建了一个可以鼠标旋转的旋转 3D 散点图。其格式为

plot3d(*x, y, z*)

其中 xyz 是表示点的数值向量。您还可以添加 colsize 等选项来分别控制点的颜色和大小。继续上面的例子,尝试以下代码:

library(rgl)
with(mtcars,
     plot3d(wt, disp, mpg, col="red", size=5))

您应该得到图 11.11 中描述的图表。使用鼠标旋转坐标轴。我认为您会发现能够在三维空间中旋转散点图使得图表更容易理解。

图 11.11 由 rgl 包中的 plot3d() 函数生成的旋转 3D 散点图

您可以使用 car 包中的 scatter3d() 函数执行类似的功能:

library(car)
with(mtcars,
     scatter3d(wt, disp, mpg))

图 11.12 显示了结果。

scatter3d() 函数可以包括各种回归曲面,如线性、二次、平滑和加性。线性曲面是默认选项。此外,还有交互式识别点的选项。有关更多详细信息,请参阅 help(scatter3d)

图 11.12 由 car 包中的 scatter3d() 函数生成的旋转 3D 散点图

11.1.5 气泡图

在上一节中,我们使用三维散点图显示了三个定量变量之间的关系。另一种方法是创建一个二维散点图,并使用绘制点的尺寸来表示第三个变量的值。这种方法称为 气泡图

这里给出了气泡图的一个简单示例:

ggplot(mtcars, 
   aes(x = wt, y = mpg, size = disp)) +
   geom_point() +
   labs(title="Bubble Plot with point size proportional to displacement",
        x="Weight of Car (lbs/1000)",
        y="Miles Per Gallon")

生成的散点图显示了汽车重量与燃油效率之间的关系,其中点的大小与每辆车的发动机排量成比例。图 11.13 展示了该图表。

图 11.13 汽车重量与每加仑英里数的气泡图,其中点的大小与发动机排量成比例

我们可以通过选择不同的点形状和颜色,并添加透明度来处理点重叠,来改善默认的外观。我们还将增加气泡大小的可能范围,以便更容易区分。最后,我们将使用颜色来添加气缸数作为第四个变量。以下列表给出了代码,图 11.14 显示了图表。在灰度图中颜色难以区分,但在彩色图中很容易辨认。

列表 11.4 改进的气泡图

ggplot(mtcars, 
       aes(x = wt, y = mpg, size = disp, fill=factor(cyl))) +
  geom_point(alpha = .5, 
             color = "black", 
             shape = 21) +
  scale_size_continuous(range = c(1, 10)) +
  labs(title = "Auto mileage by weight and horsepower",
       subtitle = "Motor Trend US Magazine (1973-74 models)",
       x = "Weight (1000 lbs)",
       y = "Miles/(US) gallon",
       size = "Engine\ndisplacement",
       fill = "Cylinders") +
  theme_minimal()  

图 11.14 改进的气泡图。拥有更多发动机气缸的汽车往往重量更大,发动机排量更大,燃油效率更差。

通常,使用 R 的统计学家倾向于避免气泡图,原因与避免饼图相同:人类通常在判断体积方面比距离更困难。但气泡图在商业界很受欢迎,所以我在这里包括它们。

我确实有很多关于散点图要说的。这种对细节的关注部分原因是散点图在数据分析中的核心地位。尽管简单,但它们可以帮助你立即以直接的方式可视化数据,揭示可能被忽视的关系。

11.2 折线图

如果你从左到右连接散点图中的点,你就得到了一个折线图。随基础安装提供的 Orange 数据集包含五棵橙树的年龄和周长数据。考虑第一棵橙树的生长情况,如图 11.15 所示。左边的图是散点图,右边的图是折线图。正如你所见,折线图特别适合传达变化。图 11.15 中的图表是用列表 11.5 中的代码创建的。

图 11.15 散点图与折线图比较。折线图有助于读者看到数据中的增长和趋势。

列表 11.5 散点图与折线图

library(ggplot2)
tree1 <- subset(Orange, Tree == 1)
ggplot(data=tree1, 
       aes(x=age, y=circumference)) +
  geom_point(size=2) +
  labs(title="Orange Tree 1 Growth",
       x = "Age (days)",
       y = "Circumference (mm)") +
  theme_bw()

ggplot(data=tree1, 
       aes(x=age, y=circumference)) +
  geom_point(size=2) +
  geom_line() +
  labs(title="Orange Tree 1 Growth",
       x = "Age (days)",
       y = "Circumference (mm)") +
  theme_bw()

两个图表的代码之间唯一的区别是添加了 geom_line() 函数。表 11.1 给出了该函数的常见选项。每个选项都可以分配一个值或映射到一个分类变量。

表 11.1 geom_line() 选项

选项 影响
size 线条厚度
color 线条颜色
linetype 线型模式(例如,虚线)

图 11.16 显示了可能的线型。

图 11.16 ggplot2 线型。你可以指定名称或编号。

为了演示创建更复杂的折线图,让我们绘制五棵橙树随时间增长的情况。每棵树将有自己的独特线条和颜色。代码在下一列表中显示,结果在图 11.17 中。

列表 11.6 显示五棵橙树随时间增长的折线图

library(ggplot2)
ggplot(data=Orange,
        aes(x=age, y=circumference, linetype=Tree, color=Tree)) +
  geom_point() +
  geom_line(size=1) +
  scale_color_brewer(palette="Set1") +
  labs(title="Orange Tree Growth",
       x = "Age (days)",
       y = "Circumference (mm)") +
  theme_bw()

图 11.17 显示五棵橙树随时间增长的折线图

在列表 11.6 中,aes()函数将树编号映射到线型和颜色。scale_color_brewer()函数用于选择调色板。由于我在色彩选择上存在困难(即,我非常擅长选择好的颜色),我严重依赖预定义的调色板,如RColorBrewer包提供的那些。第十九章(高级图形)详细描述了调色板。

你可以从图中看到,树 4 和树 2 在测量的整个范围内表现出最大的增长,并且树 4 在大约 664 天时超过了树 2。默认情况下,图例列出的线条顺序与图表中出现的顺序相反(图例中从上到下是图表中从下到上)。为了使顺序从上到下匹配,请添加

+ guides(color = guide_legend(reverse = TRUE), 
         linetype = guide_legend(reverse = TRUE))

列表 11.6 中的代码。在下一节中,你将探索一次性检查多个相关系数的方法。

11.3 相关图

相关矩阵是多变量统计的一个基本方面。哪些变量彼此强烈相关,哪些不相关?某些变量簇是否以特定方式相关?随着变量数量的增加,这些问题可能更难回答。"相关图"是用于可视化相关矩阵数据的一个相对较新的工具。

一旦你看过一次相关图,解释它就会变得容易。考虑mtcars数据框中变量的相关性。这里有 11 个变量,每个变量测量 32 辆汽车的一些方面。你可以使用以下代码获取相关性:

> round(cor(mtcars), 2)
       mpg   cyl  disp    hp  drat    wt  qsec    vs    am  gear  carb
mpg   1.00 -0.85 -0.85 -0.78  0.68 -0.87  0.42  0.66  0.60  0.48 -0.55
cyl  -0.85  1.00  0.90  0.83 -0.70  0.78 -0.59 -0.81 -0.52 -0.49  0.53
disp -0.85  0.90  1.00  0.79 -0.71  0.89 -0.43 -0.71 -0.59 -0.56  0.39
hp   -0.78  0.83  0.79  1.00 -0.45  0.66 -0.71 -0.72 -0.24 -0.13  0.75
drat  0.68 -0.70 -0.71 -0.45  1.00 -0.71  0.09  0.44  0.71  0.70 -0.09
wt   -0.87  0.78  0.89  0.66 -0.71  1.00 -0.17 -0.55 -0.69 -0.58  0.43
qsec  0.42 -0.59 -0.43 -0.71  0.09 -0.17  1.00  0.74 -0.23 -0.21 -0.66
vs    0.66 -0.81 -0.71 -0.72  0.44 -0.55  0.74  1.00  0.17  0.21 -0.57
am    0.60 -0.52 -0.59 -0.24  0.71 -0.69 -0.23  0.17  1.00  0.79  0.06
gear  0.48 -0.49 -0.56 -0.13  0.70 -0.58 -0.21  0.21  0.79  1.00  0.27
carb -0.55  0.53  0.39  0.75 -0.09  0.43 -0.66 -0.57  0.06  0.27  1.00

哪些变量最相关?哪些变量相对独立?是否存在任何模式?没有显著的时间和精力(以及可能还需要一套彩色笔来标注),仅从相关矩阵中是无法轻易判断的。

你可以使用corrgram包中的corrgram()函数显示相同的相关矩阵(见图 11.18)。代码如下:

library(corrgram)
corrgram(mtcars, order=TRUE, lower.panel=panel.shade,
         upper.panel=panel.pie, text.panel=panel.txt,
         main="Corrgram of mtcars intercorrelations")

图片

图 11.18 mtcars数据框中变量之间的相关图。行和列已使用主成分分析重新排序。

要解释这个图表,首先从单元格的下半部分(主对角线以下的单元格)开始。默认情况下,蓝色和从左下到右上的网格表示两个在该单元格相遇的变量之间的正相关。相反,红色和从左上到右下的网格表示负相关。颜色越深、饱和度越高,相关性的幅度就越大。接近零的弱相关性看起来较淡。在当前图表中,行和列已重新排序(使用第十四章将要讨论的主成分分析)以将具有相似相关模式的变量聚集在一起。

您可以从阴影单元格中看到,gearamdratmpg彼此之间呈正相关。您还可以看到,wtdispcylhpcarb彼此之间呈正相关。但第一组变量与第二组变量呈负相关。您还可以看到,carbam之间的相关性较弱,vsgearvsam以及dratqsec之间的相关性也较弱。

单元格的上三角使用饼图显示相同的信息。在这里,颜色扮演着同样的角色,但相关性的强度由填充饼图的切片大小表示。正相关从 12 点开始顺时针填充饼图。负相关逆时针填充饼图。

corrgram()函数的格式是

corrgram(*x*, order=, panel=, text.panel=, diag.panel=)

其中x是一个数据框,每行有一个观测值。当order=TRUE时,使用相关矩阵的主成分分析对变量进行重新排序。重新排序可以帮助使双变量关系的模式更加明显。

选项panel指定要使用的非对角面板类型。或者,您可以使用lower.panelupper.panel选项来选择主对角线以上和以下的不同选项。text.paneldiag.panel选项指的是主对角线。表 11.2 描述了允许的值。

表 11.2 corrgram()函数的面板选项

放置 面板选项 描述
非对角线 panel.pie 饼图的填充部分表示相关性的大小。
panel.shade 阴影的深度表示相关性的大小。
panel.ellipse 绘制置信椭圆和光滑线
panel.pts 绘制散点图
panel.conf 打印相关系数及其置信区间
panel.cor 打印相关系数,但不包括其置信区间
主对角线 panel.txt 打印变量名
panel.minmax 打印最小值、最大值和变量名
panel.density 打印核密度图和变量名

让我们尝试第二个例子。代码

library(corrgram)
corrgram(mtcars, order=TRUE, lower.panel=panel.ellipse,
         upper.panel=panel.pts, text.panel=panel.txt,
         diag.panel=panel.minmax, 
         main="Corrgram of mtcars data using scatter plots 
               and ellipses")

生成图 11.19 中的图形。在这里,您在下半三角使用光滑拟合线和使用置信椭圆,在上半三角使用散点图。

图 11.19 mtcars数据框中变量之间的相关性的 Corrgram。下三角包含光滑的最佳拟合线和置信椭圆,上三角包含散点图。对角面板包含最小值和最大值。行和列已使用主成分分析重新排序。

为什么散点图看起来很奇怪?

图 11.19 中绘制的几个变量具有有限的允许值。例如,齿轮的数量是 3、4 或 5。汽缸的数量是 4、6 或 8。am(变速器类型)和vs(V/S)都是二元的。这解释了上对角线中看起来奇怪的散点图。

总是小心选择适合数据形式的统计方法。将这些变量指定为有序或无序因素可以作为有用的检查。当 R 知道一个变量是分类或有序的,它会尝试应用适合该测量水平的统计方法。

我们将以另一个示例结束。以下代码

corrgram(mtcars, order=TRUE, lower.panel=panel.shade,
         upper.panel=panel.cor,
         main="Corrgram of mtcars data using shading and coefficients")

产生图 11.20 中的图形。在这里,你使用下三角的着色和变量顺序来强调相关性模式,并在上三角打印相关值。

图 11.20 mtcars数据框中变量的相关性的 Corrgram。下三角被着色以表示相关性的大小和方向。行和列已使用主成分分析重新排序。相关系数打印在上三角。

在继续之前,我应该指出,你可以通过corrgram()函数控制使用的颜色。为此,在colorRampPalette()函数中指定四种颜色,并使用col.regions选项包含结果。以下是一个示例:

library(corrgram) 
cols <- colorRampPalette(c("darkgoldenrod4", "burlywood1",
                           "darkkhaki", "darkgreen"))
corrgram(mtcars, order=TRUE, col.regions=cols,
         lower.panel=panel.shade, 
         upper.panel=panel.conf, text.panel=panel.txt,
         main="A Corrgram (or Horse) of a Different Color")

尝试一下,看看你得到什么结果。

Corrgrams 可以用来检查大量定量变量之间的双变量关系。由于它们相对较新,最大的挑战是教育接收者如何解释它们。了解更多信息,请参阅 Michael Friendly 的“Corrgrams: Exploratory Displays for Correlation Matrices”,www.datavis.ca/papers/corrgram.pdf

11.4 网状图

到目前为止,我们一直在探索可视化定量/连续变量之间关系的方法。但如果你的变量是分类的呢?当你查看单个分类变量时,你可以使用条形图或饼图。如果有两个分类变量,你可以使用堆叠条形图(第 6.1.2 节)。但如果有超过两个分类变量,你该怎么办呢?

一种方法是使用网状图,其中多维列联表中的频率由与其单元格频率成比例的嵌套矩形区域表示。可以使用颜色和/或阴影来表示拟合模型的残差。有关详细信息,请参阅 Meyer、Zeileis 和 Hornick(2006)或 Michael Friendly 的优秀教程(cran.r-project.org/web/packages/vcdExtra/vignettes/vcd-tutorial.pdf)。

马赛克图可以使用 vcd 库中的 mosaic() 函数创建(R 的基本安装中有一个 mosaicplot() 函数,但我推荐使用 vcd 包,因为它具有更丰富的功能)。作为一个例子,考虑基本安装中可用的 Titanic 数据集。它描述了乘客的生存或死亡数量,按舱位(1 级、2 级、3 级、船员)、性别(男性、女性)和年龄(儿童、成人)进行交叉分类。这是一个经过充分研究的数据集。你可以使用以下代码查看交叉分类:

> ftable(Titanic)
                   Survived  No Yes
Class Sex    Age                   
1st   Male   Child            0   5
             Adult          118  57
      Female Child            0   1
             Adult            4 140
2nd   Male   Child            0  11
             Adult          154  14
      Female Child            0  13
             Adult           13  80
3rd   Male   Child           35  13
             Adult          387  75
      Female Child           17  14
             Adult           89  76
Crew  Male   Child            0   0
             Adult          670 192
      Female Child            0   0
             Adult            3  20

mosaic() 函数可以调用如下

mosaic(*table*)

其中 表格 是以数组形式表示的列联表,

mosaic(*formula*, *data*=)

其中 公式 是标准的 R 公式,而 数据 指定了一个数据框或表格。添加选项 shade=TRUE 会根据拟合模型的 Pearson 残差(默认为独立性)给图形着色,而选项 legend=TRUE 会显示这些残差的图例。

例如,两者都

library(vcd)
mosaic(Titanic, shade=TRUE, legend=TRUE)

library(vcd)
mosaic(~Class+Sex+Age+Survived, data=Titanic, shade=TRUE, legend=TRUE)

将生成图 11.21 中所示的图形。公式版本让你对图中变量选择和位置的控制能力更强。

图 11.21 描述了根据舱位、性别和年龄划分的泰坦尼克号幸存者马赛克图

这张图片中包含了大量的信息。例如,当一个人从经济舱升到头等舱时,生存率急剧上升。大多数孩子都在三等舱和二等舱。头等舱中的大多数女性都幸存了下来,而只有大约一半的三等舱女性幸存。船员中的女性很少,导致图表底部的“Survived”(否,是)标签在这个群体中重叠。继续观察,你会看到更多有趣的事实。记得要观察矩形的相对宽度和高度。你还能从那个夜晚学到什么?

扩展马赛克图通过颜色和阴影来表示拟合模型的残差。在这个例子中,蓝色阴影表示比预期更频繁发生的交叉分类,假设生存与舱位、性别和年龄无关。红色阴影表示在独立性模型下比预期更少发生的交叉分类。务必运行示例,以便你可以看到彩色结果。该图表明,与独立性模型相比,头等舱女性幸存者更多,男性船员死亡者更多。与舱位、性别和年龄独立的情况下相比,三等舱男性幸存者更少。如果你想更详细地探索马赛克图,尝试运行 example(mosaic)

在本章中,我们考虑了多种显示两个或多个变量之间关系的技巧,包括二维和三维散点图、散点图矩阵、气泡图、折线图、corrgrams 和马赛克图。其中一些方法是标准技术,而其他方法则不太为人所知。

结合展示单变量分布(第六章)、探索回归模型(第八章)和可视化组间差异(第九章)的方法,你现在拥有了一个全面的工具箱,用于可视化和从你的数据中提取意义(而且 fame and fortune is surely near at hand!)。在后面的章节中,你将通过额外的专业技巧来扩展你的技能,包括潜在变量模型的图形(第十四章)、时间序列(第十五章)、聚类数据(第十六章)、缺失数据(第十八章)以及创建基于一个或多个变量的图形的技术(第十九章)。

摘要

  • 散点图和散点图矩阵允许你一次可视化两个定量变量之间的关系。这些图可以通过显示趋势的线性拟合线和局部加权回归拟合线来增强。

  • 当你基于大量数据创建散点图时,那些绘制密度而不是点的绘图方法尤其有用。

  • 可以使用 3D 散点图或 2D 气泡图来探索三个定量变量之间的关系。

  • 时间变化可以通过折线图有效地描述。

  • 大型相关系数矩阵以表格形式表示难以理解,但通过 corrgrams——相关系数矩阵的视觉图示,则易于探索。

  • 可以使用马赛克图来可视化两个或更多分类变量之间的关系。

12 重抽样统计和自助法

本章涵盖

  • 理解排列检验的逻辑

  • 将排列检验应用于线性模型

  • 使用自助法获得置信区间

在第七章、第八章和第九章中,我们回顾了假设观察数据来自正态分布或某些其他已知理论分布的统计方法,这些方法用于检验假设并估计总体参数的置信区间。但在许多情况下,这种假设是不合理的。基于随机化和重抽样的统计方法可以用于数据来自未知或混合分布的情况,样本量较小,异常值是一个问题,或者基于理论分布设计适当的测试过于复杂且数学上难以处理。

在本章中,我们将探讨两种使用随机化的广泛统计方法:排列检验和自助法。从历史上看,这些方法仅对经验丰富的程序员和专家统计学家可用。R 中的贡献包现在使它们对更广泛的数据分析人员变得容易获得。

我们还将重新审视最初使用传统方法(例如,t 检验、卡方检验、方差分析和回归)分析的问题,并看看它们如何使用这些稳健、计算密集型的方法来处理。要充分利用 12.2 节,请务必先阅读第七章。第八章和第九章作为 12.3 节的前提。

12.1 排列检验

排列检验,也称为随机化重新随机化检验,已经存在了几十年,但直到高速计算机的出现才使其变得实用。要理解排列检验的逻辑,考虑一个假设问题:10 名受试者被随机分配到两种治疗条件之一(A 或 B),并记录了一个结果变量(得分)。实验结果在表 12.1 中呈现。

表 12.1 假设的双组问题

治疗 A 治疗 B
40 57
57 64
45 55
55 62
58 65

图 12.1 也显示了数据。是否有足够的证据得出结论,治疗的影响不同?

图 12.1 表 12.1 中假设治疗数据的条形图

在参数方法中,你可能会假设数据来自具有相同方差的正态总体,并应用双尾独立组 t 检验。零假设是治疗 A 的总体均值等于治疗 B 的总体均值。你会从数据中计算 t 统计量,并将其与理论分布进行比较。如果观察到的 t 统计量足够极端,比如说在理论分布中间 95%的值之外,你会拒绝零假设,并宣布在 0.05 的显著性水平上两组的总体均值不相等。

置换检验采用不同的方法。如果两种治疗方法真正等效,则分配给观察到的分数的标签(治疗 A 或治疗 B)是任意的。为了检验两种治疗方法之间的差异,你可以遵循以下步骤:

  1. 按照参数方法计算观察到的 t 统计量;称这个为 t0。

  2. 将所有 10 个分数放在一个单独的组中。

  3. 随机分配 5 个分数给治疗 A 和 5 个分数给治疗 B。

  4. 计算并记录新的观察到的 t 统计量。

  5. 对将 5 个分数分配给治疗 A 和 5 个分数分配给治疗 B 的每一种可能方式重复步骤 3-4。共有 252 种可能的排列。

  6. 将 252 个 t 统计量按升序排列。这是基于(或基于)样本数据的经验分布。

  7. 如果 t0 落在经验分布中间 95%之外,则在 0.05 的显著性水平上拒绝两个处理组的总体均值相等的零假设。

注意,在置换和参数方法中都计算了相同的 t 统计量。但与将统计量与理论分布比较以确定它是否足够极端以拒绝零假设不同,它是与从观察数据排列中创建的经验分布进行比较。这种逻辑可以扩展到大多数经典统计检验和线性模型。

在前面的例子中,经验分布是基于数据的所有可能排列。在这种情况下,置换检验被称为精确检验。随着样本量的增加,形成所有可能排列所需的时间可能会变得难以承受。在这种情况下,你可以使用蒙特卡洛模拟从所有可能的排列中进行抽样。这样做提供了一个近似检验。

如果你觉得假设数据是正态分布的不可靠,担心异常值的影响,或者觉得数据集太小,不适合标准参数方法,那么置换检验提供了一个极好的替代方案。R 有一些目前可用的最全面和复杂的包来执行置换检验。本节剩余部分将重点介绍两个贡献包:coin包和lmPerm包。coin包提供了一个全面的框架,用于应用于独立性问题的置换检验,而lmPerm包提供了用于方差分析和回归设计的置换检验。我们将依次考虑每个包。在继续之前,请确保安装它们(install.packages(c("coin", "lmPerm")))。

设置随机数种子

在继续之前,重要的是要记住排列检验在执行近似测试时使用伪随机数从所有可能的排列中进行抽样。因此,每次执行测试时结果都会改变。在 R 中设置随机数种子允许你固定生成的随机数。这在你想与他人分享示例时特别有用,因为如果使用相同的种子进行调用,结果总是会相同。将随机数种子设置为1234(即set.seed(1234))将允许你复制本章中展示的结果。

12.2 使用 coin 包进行排列检验

coin包提供了一个通用的框架,用于将排列检验应用于独立性问题。使用此包,你可以回答以下问题:

  • 响应是否独立于分组分配?

  • 两个数值变量是否独立?

  • 两个分类变量是否独立?

使用包中提供的便利函数(见表 12.2),你可以执行大多数在第七章中涵盖的传统统计测试的排列检验等效。

表 12.2 提供排列检验替代传统测试的coin函数

测试 coin函数
双样本和 K 样本排列检验 oneway_test( y ~ A``)
威尔科克森-曼-惠特尼秩和检验 wilcox_test( y ~ A )
克鲁斯卡尔-沃利斯检验 kruskal_test( y ~ A )
皮尔逊卡方检验 chisq_test( A ~ B )
科克伦-曼特尔-汉森检验 cmh_test( A ~ B &#124; C )
线性-线性关联检验 lbl_test( D ~ E``)
斯皮尔曼检验 spearman_test( y ~ x )
弗里德曼检验 friedman_test( y ~ A &#124; C )
威尔科克森符号秩检验 wilcoxsign_test( y1 ~ y2 )

coin函数列中,yx是数值变量,AB是分类因素,C是分类分组变量,DE是有序因素,而y1y2是匹配的数值变量。

表 12.2 中列出的每个函数都采用以下形式

*function_name*( *formula*, *data*, distribution= )

其中

  • formula描述了要测试的变量之间的关系。示例在表中给出。

  • data标识一个数据框。

  • distribution指定了在零假设下经验分布应该如何推导。可能的值是exactasymptoticapproximate

如果distribution="exact",则零假设下的分布是精确计算的(即,从所有可能的排列中)。分布也可以通过其渐近分布(distribution="asymptotic")或通过蒙特卡洛重采样(distribution="approximate(nresample=n)")来近似,其中n表示用于近似精确分布的随机重复次数。默认值为 10,000 次重复。目前,distribution="exact"仅适用于双样本问题。

注意:在coin包中,分类变量和有序变量必须分别编码为因子和有序因子。此外,数据必须存储在数据框中。

在本节的剩余部分,你将应用表 12.2 中描述的几个排列检验来解决前几章的问题。这将允许你将结果与更传统的参数和非参数方法进行比较。我们将通过考虑高级扩展来结束对coin包的讨论。

12.2.1 独立双样本和 k 样本检验

首先,让我们比较一下独立样本 t 检验与应用于表 12.2 中假设数据的单因素精确检验。结果如下所示。

列表 12.1 假设数据的 t 检验与单因素排列检验

> library(coin)
> score <- c(40, 57, 45, 55, 58, 57, 64, 55, 62, 65)
> treatment <- factor(c(rep("A",5), rep("B",5)))
> mydata <- data.frame(treatment, score)
> t.test(score~treatment, data=mydata, var.equal=TRUE)

        Two Sample t-test

data:  score by treatment
t = -2.345, df = 8, p-value = 0.04705
alternative hypothesis: true difference in means is not equal to 0
95 percent confidence interval:
 -19.0405455  -0.1594545
sample estimates:
mean in group A mean in group B 
           51.0            60.6 

> oneway_test(score~treatment, data=mydata, distribution="exact")

        Exact Two-Sample Fisher-Pitman Permutation Test

data:  score by treatment (A, B)
Z = -1.9147, p-value = 0.07143
alternative hypothesis: true mu is not equal to 0

传统 t 检验表明存在显著的组间差异(p < .05),而精确检验则没有(p > 0.071)。只有 10 个观测值,我更倾向于相信排列检验的结果,并在得出最终结论之前尝试收集更多数据。

接下来,考虑 Wilcoxon–Mann–Whitney U 检验。在第七章中,我们使用wilcox.test()函数考察了南方与南方以外美国各州监禁概率的差异。使用精确的 Wilcoxon 秩和检验,你会得到

> library(MASS)
> UScrime$So <- factor(UScrime$So)
> wilcox_test(Prob ~ So, data=UScrime, distribution="exact")

        Exact Wilcoxon Mann-Whitney Rank Sum Test

data:  Prob by So (0, 1) 
Z = -3.7, p-value = 8.488e-05
alternative hypothesis: true mu is not equal to 0

这表明在南方各州监禁的可能性更大。注意,在前面的代码中,数值变量So被转换为因子。这是因为coin包要求所有分类变量都被编码为因子。此外,你可能已经注意到这些结果与第七章中wilcox.test()函数的结果完全一致。这是因为wilcox.test()默认情况下也计算精确分布。

最后,考虑 k 样本检验。在第九章中,你使用单因素方差分析来评估五种药物方案对 50 名患者胆固醇降低的影响。可以使用此代码进行近似的 k 样本排列检验:

> library(multcomp)
> set.seed(1234)
> oneway_test(response~trt, data=cholesterol, 
  distribution=approximate(nresample=9999))

            Approximative K-Sample Fisher-Pitman Permutation Test

data:  response by trt (1time, 2times, 4times, drugD, drugE)
chi-squared = 36.381, p-value < 1e-04

在这里,参考分布基于数据的 9,999 次排列。随机数种子被设置为使你的结果与我的相同。显然,不同组别中的患者反应存在明显差异。

12.2.2 列联表中的独立性

你可以使用排列检验通过chisq_test()cmh_test()函数来评估两个分类变量的独立性。后者函数用于数据在第三个分类变量上分层时。如果两个变量都是有序的,你可以使用lbl_test()函数来测试线性趋势。

在第七章中,你使用卡方检验来评估关节炎治疗与改善之间的关系。治疗有两个水平(安慰剂和治疗组),改善有三个水平(无、一些和显著)。改善变量被编码为有序因子。

如果你想要执行卡方检验的排列版本,可以使用以下代码:

> library(coin)
> library(vcd)
> Arthritis <- transform(Arthritis, 
  Improved=as.factor(as.numeric(Improved)))
> set.seed(1234)
> chisq_test(Treatment~Improved, data=Arthritis,
             distribution=approximate(nresample=9999))

                    Approximative Pearson Chi-Squared Test

data:  Treatment by Improved (1, 2, 3)
chi-squared = 13.055, p-value = 0.0018

这给出了基于 9999 次重复的近似卡方检验。你可能会问为什么将变量Improved从有序因子转换为分类因子。(好问题!)如果你保留它为有序因子,coin()将生成线性×线性趋势检验而不是卡方检验。尽管在这种情况下趋势检验可能是一个好的选择,但保持为卡方检验允许你将结果与第七章中的结果进行比较。

12.2.3 数字变量之间的独立性

spearman_test()函数提供了两个数值变量独立性的排列检验。在第七章中,我们考察了美国各州文盲率和谋杀率之间的相关性。你可以通过排列检验来测试这种关联,以下代码:

> states <- as.data.frame(state.x77)
> set.seed(1234)
> spearman_test(Illiteracy~Murder, data=states, 
                distribution=approximate(B=9999))

            Approximative Spearman Correlation Test

data:  Illiteracy by Murder
Z = 4.7065, p-value < 1e-04
alternative hypothesis: true rho is not equal to 0

基于约 9999 次重复的近似排列检验,可以拒绝独立性假设。注意,state.x77是一个矩阵。它必须被转换成数据框才能在coin包中使用。

12.2.4 依赖的双样本和 k 样本检验

当不同组别的观测值已经匹配或使用了重复测量时,使用依赖样本检验。对于两个配对组的排列检验,可以使用wilcoxsign_test()函数。对于超过两个组的情况,使用friedman_test()函数。

在第七章中,我们比较了 14 至 24 岁城市男性的失业率(U1)与 35 至 39 岁城市男性的失业率(U2)。由于这两个变量针对美国的每个州都有报告,你有一个两个依赖组的实验设计(state是匹配变量)。你可以使用精确的 Wilcoxon 符号秩检验来查看两个年龄组的失业率是否相等:

> library(coin)
> library(MASS)
> wilcoxsign_test(U1~U2, data=UScrime, distribution="exact")

        Exact Wilcoxon-Signed-Rank Test

data:  y by x (neg, pos) 
         stratified by block 
Z = 5.9691, p-value = 1.421e-14
alternative hypothesis: true mu is not equal to 0

根据结果,你会得出结论:失业率存在差异。

12.2.5 进一步探讨

coin包提供了一个通用的框架,用于测试一组变量是否独立于另一组变量(可选地在阻断变量上进行分层)相对于任意备择假设,通过近似排列检验。特别是,independence_test()函数让你可以从排列的角度接近大多数传统检验,并为传统方法未涵盖的情况创建新的和独特的统计检验。这种灵活性是有代价的:需要高水平的统计知识才能恰当地使用该函数。请参阅包附带的小册子(通过vignette("coin")访问)以获取更多详细信息。

在下一节中,你将了解lmPerm包。此包提供了一种排列方法来处理线性模型,包括回归和方差分析。

12.3 使用 lmPerm 包进行排列检验

lmPerm包提供了对线性模型排列方法的支撑。特别是,lmp()aovp()函数是经过修改的lm()aov()函数,用于执行排列检验而不是常规理论检验。

lmp()aovp()函数的参数与lm()aov()函数的参数相似,增加了perm=参数。perm=选项可以取ExactProbSPR的值。Exact基于所有可能的排列产生精确检验。Prob从所有可能的排列中进行抽样。抽样会继续进行,直到估计的标准差低于估计 p 值的 0.1。停止规则由可选的Ca参数控制。最后,SPR使用顺序概率比检验来决定何时停止抽样。请注意,如果观测值的数量大于 10,perm="Exact"将自动默认为perm="Prob"。精确检验仅适用于小问题。

为了了解这是如何工作的,您将应用排列方法于简单回归、多项式回归、多元回归、单因素方差分析、单因素协方差分析和双向因子设计。

12.3.1 简单和多项式回归

在第八章中,您使用线性回归研究了 15 名女性的体重与身高之间的关系。使用lmp()而不是lm()会生成以下列表中的排列检验结果。

列表 12.2 简单线性回归的排列检验

> library(lmPerm)
> set.seed(1234)
> fit <- lmp(weight~height, data=women, perm="Prob")
[1] "Settings:  unique SS : numeric variables centered"
> summary(fit)

Call:
lmp(formula = weight ~ height, data = women, perm = "Prob")

Residuals:
   Min     1Q Median     3Q    Max 
-1.733 -1.133 -0.383  0.742  3.117 

Coefficients:
       Estimate Iter Pr(Prob)    
height     3.45 5000   <2e-16 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1 

Residual standard error: 1.5 on 13 degrees of freedom
Multiple R-Squared: 0.991,      Adjusted R-squared: 0.99 
F-statistic: 1.43e+03 on 1 and 13 DF,  p-value: 1.09e-14 

要拟合一个二次方程,您可以使用下一列表中的代码。

列表 12.3 多项式回归的排列检验

> library(lmPerm)
> set.seed(1234)
> fit <- lmp(weight~height + I(height²), data=women, perm="Prob")
[1] "Settings:  unique SS : numeric variables centered"
> summary(fit)

Call:
lmp(formula = weight ~ height + I(height²), data = women, perm = "Prob")

Residuals:
    Min      1Q  Median      3Q     Max 
-0.5094 -0.2961 -0.0094  0.2862  0.5971 

Coefficients:
            Estimate Iter Pr(Prob)    
height       -7.3483 5000   <2e-16 ***
I(height²)   0.0831 5000   <2e-16 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1 

Residual standard error: 0.38 on 12 degrees of freedom
Multiple R-Squared: 0.999,      Adjusted R-squared: 0.999 
F-statistic: 1.14e+04 on 2 and 12 DF,  p-value: <2e-16

如您所见,使用排列检验测试这些回归非常简单,并且对底层代码的改变很小。输出结果也与lm()函数产生的结果相似。请注意,增加了一个Iter列,表示达到停止规则所需的迭代次数。

12.3.2 多元回归

在第八章中,使用多元回归预测了 50 个美国州的人口、文盲率、收入和霜冻对谋杀率的影响。将lmp()函数应用于此问题会产生以下列表。

列表 12.4 多元回归的排列检验

> library(lmPerm)
> set.seed(1234)
> states <- as.data.frame(state.x77)
> fit <- lmp(Murder~Population + Illiteracy+Income+Frost,
             data=states, perm="Prob")
[1] "Settings:  unique SS : numeric variables centered"
> summary(fit)

Call:
lmp(formula = Murder ~ Population + Illiteracy + Income + Frost, 
    data = states, perm = "Prob")

Residuals:
     Min       1Q   Median       3Q      Max 
-4.79597 -1.64946 -0.08112  1.48150  7.62104 

Coefficients:
            Estimate Iter Pr(Prob)    
Population 2.237e-04   51   1.0000    
Illiteracy 4.143e+00 5000   0.0004 ***
Income     6.442e-05   51   1.0000    
Frost      5.813e-04   51   0.8627    
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '. ' 0.1 ' ' 1 

Residual standard error: 2.535 on 45 degrees of freedom
Multiple R-Squared: 0.567,      Adjusted R-squared: 0.5285 
F-statistic: 14.73 on 4 and 45 DF,  p-value: 9.133e-08  

回顾第八章,当使用常规理论时,PopulationIlliteracy都是显著的(p < 0.05)。基于排列检验,Population变量不再显著。当两种方法不一致时,您应该更仔细地查看您的数据。可能是不变性假设不可靠,或者存在异常值。

12.3.3 单因素方差分析和协方差分析

第九章中讨论的每个方差分析设计都可以通过排列检验来完成。首先,让我们看看第 9.1 节中关于治疗方案对胆固醇降低影响的单因素方差分析问题。代码和结果将在下一列表中给出。

列表 12.5 一因素方差分析的排列检验

> library(lmPerm)
> library(multcomp)
> set.seed(1234)
> fit <- aovp(response~trt, data=cholesterol, perm="Prob")
[1] "Settings:  unique SS "
> anova(fit)
Component 1 :
            Df R Sum Sq R Mean Sq Iter  Pr(Prob)    
trt          4  1351.37    337.84 5000 < 2.2e-16 ***
Residuals   45   468.75     10.42                   
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '. ' 0.1 ' ' 1 

结果表明,治疗效应并不都是相等的。

本节中的第二个例子将排列检验应用于单因素协方差分析。这个问题来自第九章,其中你研究了在控制妊娠时间的情况下,四种药物剂量对大鼠窝重量的影响。下一列表显示了排列检验和结果。

列表 12.6 单因素协方差分析的排列检验

> library(lmPerm)
> set.seed(1234)
> fit <- aovp(weight ~ gesttime + dose, data=litter, perm="Prob")
[1] "Settings:  unique SS : numeric variables centered"
> anova(fit)
Component 1 :
            Df R Sum Sq R Mean Sq Iter Pr(Prob)    
gesttime     1   161.49   161.493 5000   0.0006 ***
dose         3   137.12    45.708 5000   0.0392 *  
Residuals   69  1151.27    16.685                  
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

根据 p 值,在控制妊娠时间的情况下,四种药物剂量对窝重量的影响并不相等。

12.3.4 双因素方差分析

你将通过应用排列检验来结束本节内容。在第九章中,你研究了维生素 C 对豚鼠牙齿生长的影响。两个操纵因素是剂量(三个水平)和递送方式(两个水平)。每个治疗组合放置了 10 只豚鼠,从而形成了一个平衡的 3×2 因子设计。排列检验将在下一列表中提供。

列表 12.7 双因素方差分析的排列检验

> library(lmPerm)
> set.seed(1234)
> fit <- aovp(len~supp*dose, data=ToothGrowth, perm="Prob")
[1] "Settings:  unique SS : numeric variables centered"
> anova(fit)
Component 1 :
            Df R Sum Sq R Mean Sq Iter Pr(Prob)    
supp         1   205.35    205.35 5000  < 2e-16 ***
dose         1  2224.30   2224.30 5000  < 2e-16 ***
supp:dose    1    88.92     88.92 2032  0.04724 *  
Residuals   56   933.63     16.67                  
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1 

在显著性水平为 0.05 时,所有三个效应在统计上都与零不同。在显著性水平为 0.01 时,只有主效应是显著的。

重要的是要注意,当aovp()应用于方差分析设计时,它默认使用唯一平方和(也称为SAS Type III 平方和)。每个效应都会调整以适应其他每个效应。R 中参数方差分析设计的默认值是顺序平方和(SAS Type I 平方和)。每个效应都会调整以适应那些在模型中出现较早的效应。对于平衡设计,两种方法将达成一致,但对于不平衡设计,每个单元的观测数不相等,它们就不会。不平衡程度越大,不一致性越大。如果需要,在aovp()函数中指定seqs=TRUE将产生顺序平方和。有关第一类和第三类平方和的更多信息,请参阅第 9.2 节。

12.4 对排列检验的附加评论

排列检验为依赖于对潜在抽样分布的了解的检验提供了一个强大的替代方案。在这些排列检验中的每一个,你都能够测试统计假设,而不必依赖于正态、t、F 或卡方分布。

你可能已经注意到,基于正态理论的测试结果与之前章节中排列方法的测试结果非常接近。这些问题中的数据表现良好,方法之间的一致性是对正态理论方法在这种情况下工作得有多好的证明。

排列检验在数据明显非正态(例如,高度偏斜)、存在异常值、样本量小或不存在参数检验的情况下表现得尤为出色。但如果原始样本是对感兴趣人群的糟糕代表,那么没有任何测试,包括排列检验,能够改善生成的推断。

排列检验主要用于生成可以用来检验零假设的 p 值。它们可以帮助回答“是否存在效应?”的问题。使用排列方法来获得置信区间和测量精度的估计比较困难。幸运的是,这是自助法擅长的领域。

12.5 自助法

自助法通过从原始样本中重复随机有放回抽样来生成一个测试统计量或一组测试统计量的经验分布。它允许你在不需要假设特定潜在理论分布的情况下生成置信区间和测试统计假设。

用一个例子最容易展示自助法的逻辑。比如说,你想计算样本均值的 95%置信区间。你的样本有 10 个观测值,样本均值为 40,样本标准差为 5。如果你愿意假设均值的抽样分布是正态分布的,那么(1 – α/2)%的置信区间可以使用以下公式计算:

其中 t 是具有 n – 1 个自由度的 t 分布的上 1 – α/2 临界值。对于 95%的置信区间,你有 40 – 2.262(5/3.163) < µ < 40 + 2.262 -(5/3.162) 或 36.424 < µ < 43.577。你预计以这种方式创建的 95%置信区间将围绕真实的总体均值。

但如果你不愿意假设均值的抽样分布是正态分布的怎么办?你可以使用自助法:

  1. 从样本中随机选择 10 个观测值,每次选择后进行有放回抽样。一些观测值可能被选择多次,而一些可能一次也没有被选择。

  2. 计算并记录样本均值。

  3. 重复前两步 1,000 次。

  4. 将 1,000 个样本均值按从小到大的顺序排列。

  5. 找到代表 2.5%和 97.5%百分位的样本均值。在这种情况下,这是从底部和顶部数的第 25 个数。这些就是你的 95%置信极限。

在这种情况下,如果样本均值很可能呈正态分布,那么自助法带来的好处很少。然而,在许多情况下,自助法是有优势的。如果你想要样本中位数或两个样本中位数之差的置信区间怎么办?这里没有简单的正态理论公式,而自助法是首选的方法。如果潜在分布未知;如果异常值是一个问题;如果样本量小;或者如果参数方法不存在,自助法通常可以是一个有用的方法来生成置信区间和检验假设。

12.6 使用 boot 包进行自助法

boot 包提供了广泛的用于自举和相关重采样方法的设施。您可以自举单个统计量(例如,中位数)或一组统计量(例如,回归系数集)。在使用之前,请确保下载并安装 boot 包(install.packages("boot"))。

自举过程可能看起来很复杂,但一旦您回顾了示例,它应该就会变得有意义。

通常,自举涉及三个主要步骤:

  1. 编写一个函数,该函数返回感兴趣的统计量或统计量。如果有一个单个统计量(例如,中位数),则该函数应返回一个数字。如果有多个统计量(例如,回归系数集),则该函数应返回一个向量。

  2. 通过 boot() 函数处理此函数以生成统计量(s)的 R 自举重复。

  3. 使用 boot.ci() 函数获取步骤 2 中生成的统计量(s)的置信区间。

现在具体来说。主要的自举函数是 boot()。其格式为

*bootobject* <- boot(data=, statistic=, R=, ...) 

表 12.3 描述了参数。

表 12.3 boot() 函数的参数

参数 描述
data 一个向量、矩阵或数据框
statistic 产生要自举的 k 个统计量的函数(如果自举单个统计量,则 k = 1)。该函数应包含一个索引参数,boot() 函数可以使用该参数为每个重复选择案例(请参阅文本中的示例)。
R 自举重复的数量
... 传递给产生感兴趣统计量的函数的附加参数

boot() 函数调用统计函数 R 次。每次,它从整数 1:nrow(data) 中生成一组有放回的随机索引。这些索引用于统计函数以选择样本。在样本上计算统计量,并将结果累积在 bootobject 中。表 12.4 描述了 bootobject 结构。

表 12.4 boot() 函数返回的对象的元素

元素 描述
t0 应用到原始数据的 k 个统计量的观测值
t 一个 R × k 矩阵,其中每一行是 k 个统计量的自举重复

您可以通过 bootobject$t0bootobject$t. 访问这些元素。

一旦生成了自举样本,您可以使用 print()plot() 来检查结果。如果结果看起来合理,您可以使用 boot.ci() 函数来获取统计量(s)的置信区间。格式为

boot.ci(*bootobject*, conf=, type= ) 

表 12.5 给出了参数。

表 12.5 boot.ci() 函数的参数

参数 描述
bootobject boot() 函数返回的对象。
conf 所需的置信区间(默认:conf=0.95)
type 返回的置信区间的类型。可能的值是 normbasicstudpercbcaall(默认:type="all"

type 参数指定了获取置信限的方法。perc 方法(百分位法)在样本均值示例中进行了演示。bca 提供了一个对偏差进行简单调整的区间。我发现 bca 在大多数情况下更可取。参见 Mooney 和 Duval(1993)以了解这些方法。

在接下来的几节中,我们将探讨重抽样单一统计量和一系列统计量。

12.6.1 重抽样单一统计量

mtcars 数据集包含了 1974 年《Motor Trend》杂志报道的 32 辆汽车的信息。假设你正在使用多元回归来预测汽车的油耗(每 1000 磅的重量和发动机排量(立方英寸))。除了标准的回归统计量之外,你希望得到 R 平方值(由预测变量解释的响应变量变异百分比)的 95%置信区间。可以使用非参数重抽样法来获得置信区间。

第一个任务是编写一个获取 R 平方值的函数:

rsq <- function(formula, data, indices) {
         d <- data[indices,]
         fit <- lm(formula, data=d)
         return(summary(fit)$r.square)
} 

该函数从回归中返回 R 平方值。d <- data[indices,] 语句对于 boot() 能够选择样本是必需的。

你可以使用以下代码绘制大量重抽样重复(例如,1000 次):

library(boot)
set.seed(1234)
results <- boot(data=mtcars, statistic=rsq, 
                R=1000, formula=mpg~wt+disp)

可以使用以下方式打印 boot 对象:

> print(results)

ORDINARY NONPARAMETRIC BOOTSTRAP

Call:
boot(data = mtcars, statistic = rsq, R = 1000, formula = mpg ~ 
    wt + disp)

Bootstrap Statistics :
     original      bias     std. error
t1* 0.7809306  0.01333670   0.05068926

并使用 plot(results) 进行绘图。图 12.2 显示了结果图。

图片

图 12.2 重抽样 R 平方值的分布

在图 12.2 中,你可以看到重抽样 R 平方值的分布不是正态分布的。可以使用以下方法获得 R 平方值的 95%置信区间:

> boot.ci(results, type=c("perc", "bca"))
BOOTSTRAP CONFIDENCE INTERVAL CALCULATIONS
Based on 1000 bootstrap replicates

CALL : 
boot.ci(boot.out = results, type = c("perc", "bca"))

Intervals : 
Level     Percentile            BCa          
95%   ( 0.6753,  0.8835 )   ( 0.6344,  0.8561 )
Calculations and Intervals on Original Scale
Some BCa intervals may be unstable

从这个例子中你可以看到,不同的生成置信区间的方法会导致不同的区间。在这种情况下,偏差调整区间与百分位法中等程度不同。在任何情况下,零假设 H[0]: R-square = 0 都会被拒绝,因为零不在置信区间内。

在本节中,你估计了一个单一统计量的置信限。在下一节中,你将估计多个统计量的置信区间。

12.6.2 重抽样多个统计量

在前面的例子中,重抽样被用来估计单一统计量(R 平方)的置信区间。继续这个例子,让我们获得一系列统计量的 95%置信区间。具体来说,让我们获取三个模型回归系数(截距、汽车重量和发动机排量)的置信区间。

首先,创建一个返回回归系数向量的函数:

bs <- function(formula, data, indices) {                
        d <- data[indices,]
        fit <- lm(formula, data=d)
        return(coef(fit))                                    
}

然后,使用此函数进行 1000 次重抽样:

library(boot)
set.seed(1234)
results <- boot(data=mtcars, statistic=bs,
                R=1000, formula=mpg~wt+disp)
> print(results)
ORDINARY NONPARAMETRIC BOOTSTRAP
Call:
boot(data = mtcars, statistic = bs, R = 1000, formula = mpg ~ 
    wt + disp)

Bootstrap Statistics :
    original   bias    std. error
t1*  34.9606  0.137873     2.48576
t2*  -3.3508 -0.053904     1.17043
t3*  -0.0177 -0.000121     0.00879

当对多个统计量进行自助法时,需要向plot()boot.ci()函数添加一个索引参数,以指示分析bootobject$t的哪一列。在这个例子中,索引 1 代表截距,索引 2 是汽车重量,索引 3 是发动机排量。要绘制汽车重量的结果,请使用

plot(results, index=2)

图 12.3 显示了图表。

图片

图 12.3 汽车重量自助回归系数的分布

要获取汽车重量和发动机排量的 95%置信区间,请使用

> boot.ci(results, type="bca", index=2)
BOOTSTRAP CONFIDENCE INTERVAL CALCULATIONS
Based on 1000 bootstrap replicates

CALL : 
boot.ci(boot.out = results, type = "bca", index = 2)

Intervals : 
Level       Bca          
95%   (-5.477, -0.937 )
Calculations and Intervals on Original Scale 

> boot.ci(results, type="bca", index=3) 

BOOTSTRAP CONFIDENCE INTERVAL CALCULATIONS
Based on 1000 bootstrap replicates

CALL : 
boot.ci(boot.out = results, type = "bca", index = 3)

Intervals : 
Level       BCa          
95%   (-0.0334, -0.0011 )   
Calculations and Intervals on Original Scale

注意:前一个例子每次都重新抽取整个数据样本。如果你可以假设预测变量具有固定水平(这在计划实验中很典型),那么你最好只重新抽取残差项。参见 Mooney 和 Duval(1993,第 16-17 页)的简单解释和算法。

在我们离开自助法之前,值得解决两个经常出现的问题:

  • 原始样本需要多大?

  • 需要多少次重复?

对于第一个问题,没有简单的答案。有些人认为,只要样本能够代表总体,原始样本量在 20-30 之间就足够了。从感兴趣的总体中进行随机抽样是确保原始样本代表性的最可靠方法。至于第二个问题,我发现大多数情况下 1000 次重复就足够了。计算机能力便宜,如果你需要,总是可以增加重复次数。

关于排列检验和自助法有许多有用的信息来源。一个很好的起点是 Yu(2003)的一篇在线文章。Good(2006)提供了关于重抽样的全面概述,并包括 R 代码。Mooney 和 Duval(1993)提供了关于自助法的良好、易于理解的介绍。关于自助法的权威来源是 Efron 和 Tibshirani(1998)。最后,还有一些优秀的在线资源,包括 Simon(1997)、Canty(2002)、Shah(2005)和 Fox(2002)。

摘要

  • 重抽样统计量和自助法是计算密集型方法,允许你在不参考已知理论分布的情况下测试假设和形成置信区间。

  • 当你的数据来自未知的总体分布,当存在严重的异常值,当样本量小,以及当没有现有的参数方法来回答感兴趣的假设时,它们尤其有价值。

  • 它们特别令人兴奋,因为它们提供了一种在标准数据假设显然不成立或当你没有其他方法来处理问题时回答问题的途径。

  • 然而,它们并不是万能的。它们不能将差数据变成好数据。如果你的原始样本不能代表感兴趣的总体,或者样本量太小,无法准确反映总体,那么这些技术将无济于事。

第四部分。高级方法

在本书的这一部分,我们将考虑高级统计分析方法,以完善您的数据分析工具箱。本部分的方法在数据挖掘和预测分析这一不断发展的领域中发挥着关键作用。

第十三章在第八章的回归方法基础上进行了扩展,涵盖了非正态分布数据的参数方法。本章从广义线性模型的讨论开始,然后专注于您试图预测的结果变量是分类变量(逻辑回归)或计数变量(泊松回归)的情况。

由于多元数据固有的复杂性,处理大量变量可能具有挑战性。第十四章描述了两种流行的探索和简化多元数据的方法。主成分分析可用于将许多相关变量组合成更小的复合变量集。因子分析包括一系列技术,用于揭示给定变量集背后的潜在结构。第十四章提供了执行每个步骤的逐步说明。

第十五章探讨了时间依赖性数据。分析师经常面临理解趋势和预测未来事件的需求。第十五章提供了对时间序列数据分析和预测的全面介绍。在描述时间序列数据的一般特征后,介绍了两种最流行的预测方法(指数和 ARIMA)。

聚类分析是第十六章的主题。虽然主成分分析和因子分析通过将单个变量组合成复合变量来简化多元数据,但聚类分析试图通过将单个观测值组合成称为的子组来简化多元数据。簇包含彼此相似且与其他簇中的案例不同的案例。本章考虑了确定数据集中存在的簇数量以及将这些观测值组合到这些簇中的方法。

第十七章讨论了分类这个重要主题。在分类问题中,分析师试图从一组(可能很大)的预测变量中开发一个模型来预测新案例的组别(例如,良好信用/不良信用风险、良性/恶性、通过/未通过)。考虑了许多方法,包括逻辑回归、决策树、随机森林和支持向量机。还描述了评估结果分类模型有效性的方法。

在实践中,研究人员必须经常处理不完整的数据集。第十八章考虑了缺失数据值这一普遍问题的现代方法。R 支持分析不完整数据集的多种优雅方法。这里描述了其中一些最好的方法,以及关于使用哪些方法以及避免哪些方法的指导。

完成第四部分后,你将拥有管理各种复杂数据分析问题的工具。这包括对非正态结果变量进行建模、处理大量相关变量、将大量案例减少到更少的同质集群、开发预测未来值或分类结果的模型,以及处理混乱和不完整的数据。

13 广义线性模型

本章涵盖

  • 构建广义线性模型

  • 预测分类结果

  • 模型计数数据

在第八章(回归)和第九章(方差分析)中,我们探讨了可以用来从一组连续和/或分类预测变量预测正态分布响应变量的线性模型。但在许多情况下,假设因变量是正态分布的(甚至连续的)是不合理的。例如:

  • 结果变量可能是分类的。二元变量(例如,是/否、通过/失败、活着/死去)和多分类变量(例如,差/好/优秀、共和党/民主党/独立)显然不是正态分布的。

  • 结果变量可能是一个计数(例如,一周内发生的交通事故数量,每天饮酒的数量)。这样的变量只取有限数量的值,永远不会是负数。此外,它们的均值和方差通常相关(这对于正态分布变量来说是不成立的)。

广义线性模型 将线性模型框架扩展到包括明显非正态的因变量。

在本章中,我们将从广义线性模型和用于估计它们的 glm() 函数的简要概述开始。然后我们将关注该框架中两种流行的模型:逻辑回归(其中因变量是分类的)和泊松回归(其中因变量是计数变量)。

为了激发讨论,你将应用广义线性模型来解决两个标准线性模型难以解决的问题:

  • 哪些个人、人口统计和关系变量可以预测婚外情?在这种情况下,结果变量是二元的(有外遇/无外遇)。

  • 抗癫痫药物对在八周期间经历的癫痫发作次数有什么影响?在这种情况下,结果变量是一个计数(癫痫发作次数)。

你将应用逻辑回归来解决第一个问题,并使用泊松回归来解决第二个问题。在这个过程中,我们将考虑每种技术的扩展。

13.1 广义线性模型和 glm()函数

许多流行的数据分析方法都包含在广义线性模型的框架内。在本节中,我们将简要探讨这种方法的背后的一些理论。如果你愿意,可以安全地跳过这一节,稍后再回来。

假设你想建立一个响应变量 Y 与一组 p 个预测变量 X[1] ... X[p] 之间的关系模型。在标准线性模型中,你假设 Y 是正态分布的,并且关系的形式是

图片

该方程表示反应变量的条件均值是预测变量的线性组合。β[j] 是指定 X[j] 单位变化时 Y 预期变化的参数,β[0] 是所有预测变量均为 0 时 Y 的预期值。你可以说,通过应用适当的权重到 X 变量并将它们相加,你可以预测具有给定 X 值的观察值的 Y 分布的均值。

注意,你没有对预测变量 X[j] 做出分布假设。与 Y 不同,没有要求它们必须是正态分布的。事实上,它们通常是分类的(例如,方差分析设计)。此外,允许预测变量的非线性函数。你经常将此类预测变量包括为 X² 或 X[1] × X[2]。重要的是,方程在参数(β[0],β[1],...,β[p])上是线性的。

在广义线性模型中,你拟合形式为的模型

其中 g(µ[Y]) 是条件均值(称为 连接函数)的函数。此外,你放宽了 Y 是正态分布的假设。相反,你假设 Y 遵循指数族成员的分布。你指定连接函数和概率分布,并通过迭代最大似然估计过程推导出参数。

13.1.1 glm() 函数

广义线性模型通常通过 R 中的 glm() 函数进行拟合(尽管还有其他专门的函数可用)。该函数的形式类似于 lm(),但包含额外的参数。函数的基本格式如下

glm(*formula*, family=*family*(link=*function*), data=)

其中,概率分布(family)和相应的默认连接函数(function)在表 13.1 中给出。

表 13.1 glm() 参数

Family Default link function
binomial (link = "logit")
gaussian (link = "identity")
gamma (link = "inverse")
inverse.gaussian (link = "1/mu²")
poisson (link = "log")
quasi (link = "identity", variance = "constant")
quasibinomial (link = "logit")
quasipoisson (link = "log")

glm() 函数允许你拟合多种流行的模型,包括逻辑回归、泊松回归和生存分析(此处未考虑)。你可以如下演示前两种模型。假设你有一个单一的反应变量(Y),三个预测变量(X1, X2, X3),以及包含数据的 mydata 数据框。

逻辑回归适用于反应变量为二分(0 或 1)的情况。模型假设 Y 遵循二项分布,并且你可以拟合形式为的线性模型

其中 π = µ[Y]Y 的条件均值(即给定一组 X 值时 Y = 1 的概率),(π/1 – π) 是 Y = 1 的几率,log(π/1 – π) 是对数几率,或 logit。在这种情况下,log(π/1 – π) 是连接函数,概率分布是二项分布,逻辑回归模型可以使用

glm(Y~X1+X2+X3, family=binomial(link="logit"), data=mydata)

逻辑回归在 13.2 节中描述得更详细。

泊松回归适用于响应变量是在给定时间段内发生事件数量的情况。泊松回归模型假设 Y 符合泊松分布,并且可以拟合形式为的线性模型

中的相同结果,其中 λY 的均值(和方差)。在这种情况下,连接函数是 λ 的对数,概率分布是泊松分布,泊松回归模型可以使用

glm(Y~X1+X2+X3, family=poisson(link="log"), data=mydata)

泊松回归在 13.3 节中描述。

值得注意的是,标准线性模型也是广义线性模型的一个特例。如果你让连接函数 g(µ[Y]) = µ[Y] 或恒等函数,并指定概率分布是正态分布(高斯分布),那么

glm(Y~X1+X2+X3, family=gaussian(link="identity"), data=mydata)

将产生与

lm(Y~X1+X2+X3, data=mydata)

总结来说,广义线性模型通过拟合条件均值响应的 函数(而不是条件均值响应)并假设响应变量遵循 指数 分布族(而不是仅限于正态分布)来扩展标准线性模型。参数估计是通过最大似然而不是最小二乘法得到的。

13.1.2 支持函数

当分析标准线性模型时,你与 lm() 一起使用的许多函数都有 glm() 的对应版本。表 13.2 给出了一些常用函数。

表 13.2 支持的 glm() 函数

函数 描述
summary() 显示拟合模型的详细结果
coefficients(), coef() 列出拟合模型的模型参数(截距和斜率)
confint() 提供模型参数的置信区间(默认为 95%)
residuals() 列出拟合模型的残差值
anova() 生成比较两个拟合模型的方差分析表
plot() 生成用于评估模型拟合的诊断图
predict() 使用拟合模型预测新数据集的响应值
deviance() 拟合模型的偏差
df.residual() 拟合模型的残差自由度

我们将在后面的章节中探讨这些函数的示例。在下一节中,我们将简要考虑模型充分性的评估。

13.1.3 模型拟合和回归诊断

模型充分性的评估对于广义线性模型和标准(OLS)线性模型同样重要。不幸的是,在统计界关于适当的评估程序上存在较少的共识。一般来说,你可以使用第八章中描述的技术,但要注意以下事项。

在评估模型充分性时,你通常会希望绘制以原始响应变量的度量单位表示的预测值与偏差类型残差的关系图。例如,一个常见的诊断图可以是

plot(predict(model, type="response"), 
    residuals(model, type= "deviance"))  

其中 modelglm() 函数返回的对象。

R 提供的帽子值、学生化残差和 Cook 的 D 统计量将是近似值。此外,在确定识别问题观察值的截止值上没有普遍共识。值必须相对判断。一种方法是为每个统计量创建索引图,并寻找异常大的值。例如,你可以使用以下代码创建三个诊断图:

plot(hatvalues(*model*))
plot(rstudent(*model*))
plot(cooks.distance(*model*))

或者,你可以使用以下代码

library(car)
influencePlot(*model*)

创建一个综合图。在后一个图中,水平轴是杠杆,垂直轴是学生化残差,绘制的符号与 Cook 距离成正比。

当响应变量具有许多值时,诊断图通常最有帮助。当响应变量只能取有限数量的值(例如,逻辑回归)时,这些图的效用会降低。

关于广义线性模型的回归诊断的更多信息,请参阅 Fox(2008)和 Faraway(2006)。在本章的其余部分,我们将详细考虑两种最流行的广义线性模型形式:逻辑回归和泊松回归。

13.2 逻辑回归

当你需要从一组连续和/或分类预测变量预测二元结果时,逻辑回归是有用的。为了演示这一点,让我们探索包含在Affairs数据框中的不忠数据,该数据框由AER包提供。在使用之前,请确保下载并安装该包(使用install.packages("AER"))。

不忠数据,被称为 Fair 的 Affairs,基于 1969 年由《心理学今天》进行的横断面调查,并在 Greene(2003)和 Fair(1978)中描述。它包含 9 个变量,收集于 601 名参与者,包括过去一年中受访者参与婚外性交的频率,以及他们的性别、年龄、结婚年限、是否有孩子、他们的宗教性(从 1 = 反对到 5 = 非常反对的 5 点量表),教育、职业(Hollingshead 7 点分类,反向编号)以及他们对婚姻的数值自我评价(从 1 = 非常不幸福到 5 = 非常幸福)。

让我们看看一些描述性统计:

> data(Affairs, package="AER")
> summary(Affairs)
    affairs          gender         age         yearsmarried    children 
 Min.   : 0.000   female:315   Min.   :17.50   Min.   : 0.125   no :171  
 1st Qu.: 0.000   male  :286   1st Qu.:27.00   1st Qu.: 4.000   yes:430  
 Median : 0.000                Median :32.00   Median : 7.000            
 Mean   : 1.456                Mean   :32.49   Mean   : 8.178            
 3rd Qu.: 0.000                3rd Qu.:37.00   3rd Qu.:15.000            
 Max.   :12.000                Max.   :57.00   Max.   :15.000            
 religiousness     education       occupation        rating     
 Min.   :1.000   Min.   : 9.00   Min.   :1.000   Min.   :1.000  
 1st Qu.:2.000   1st Qu.:14.00   1st Qu.:3.000   1st Qu.:3.000  
 Median :3.000   Median :16.00   Median :5.000   Median :4.000  
 Mean   :3.116   Mean   :16.17   Mean   :4.195   Mean   :3.932  
 3rd Qu.:4.000   3rd Qu.:18.00   3rd Qu.:6.000   3rd Qu.:5.000  
 Max.   :5.000   Max.   :20.00   Max.   :7.000   Max.   :5.000  

> table(Affairs$affairs)
  0   1   2   3   7  12 
451  34  17  19  42  38

从这些统计数据中,你可以看到,52%的受访者是女性,72%有孩子,样本的中位年龄为 32 岁。关于响应变量,75%的受访者表示在过去一年中没有不忠行为(451/601)。报告的邂逅次数最多的是 12 次(6%)。

虽然记录了不检点的数量,但你的兴趣在于二进制结果(是否出轨/没有出轨)。你可以使用以下代码将出轨转换为一个名为ynaffair的二分因素:

> Affairs$ynaffair <- ifelse(Affairs$affairs > 0, 1, 0) 
> Affairs$ynaffair <- factor(Affairs$ynaffair, 
                             levels=c(0,1), 
                             labels=c("No","Yes"))
> table(Affairs$ynaffair)
No Yes 
451 150

这个二分因素现在可以用作逻辑回归模型中的结果变量:

> fit.full <- glm(ynaffair ~ gender + age + yearsmarried + children + 
                  religiousness + education + occupation +rating,
                  data=Affairs, family=binomial())
> summary(fit.full)

Call:
glm(formula = ynaffair ~ gender + age + yearsmarried + children + 
    religiousness + education + occupation + rating, family = binomial(), 
    data = Affairs)

Deviance Residuals: 
   Min      1Q  Median      3Q     Max  
-1.571  -0.750  -0.569  -0.254   2.519  

Coefficients:
              Estimate Std. Error z value Pr(>|z|)    
(Intercept)     1.3773     0.8878    1.55  0.12081    
gendermale      0.2803     0.2391    1.17  0.24108    
age            -0.0443     0.0182   -2.43  0.01530 *  
yearsmarried    0.0948     0.0322    2.94  0.00326 ** 
childrenyes     0.3977     0.2915    1.36  0.17251    
religiousness  -0.3247     0.0898   -3.62  0.00030 ***
education       0.0211     0.0505    0.42  0.67685    
occupation      0.0309     0.0718    0.43  0.66663    
rating         -0.4685     0.0909   -5.15  2.6e-07 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1 

(Dispersion parameter for binomial family taken to be 1)

    Null deviance: 675.38  on 600  degrees of freedom
Residual deviance: 609.51  on 592  degrees of freedom
AIC: 627.5

Number of Fisher Scoring iterations: 4

从回归系数的 p 值(最后一列)中,你可以看到性别、孩子是否存在、教育和职业可能对等式没有显著的贡献(你不能拒绝参数为 0 的假设)。让我们拟合一个没有它们的第二个等式,并测试这个简化模型是否与数据拟合得一样好:

> fit.reduced <- glm(ynaffair ~ age + yearsmarried + religiousness +
                     rating, data=Affairs, family=binomial())
> summary(fit.reduced)
Call:
glm(formula = ynaffair ~ age + yearsmarried + religiousness + rating, 
    family = binomial(), data = Affairs)

Deviance Residuals: 
   Min      1Q  Median      3Q     Max  
-1.628  -0.755  -0.570  -0.262   2.400  

Coefficients:
              Estimate Std. Error z value Pr(>|z|)    
(Intercept)     1.9308     0.6103    3.16  0.00156 ** 
age            -0.0353     0.0174   -2.03  0.04213 *  
yearsmarried    0.1006     0.0292    3.44  0.00057 ***
religiousness  -0.3290     0.0895   -3.68  0.00023 ***
rating         -0.4614     0.0888   -5.19  2.1e-07 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1 

(Dispersion parameter for binomial family taken to be 1)

    Null deviance: 675.38  on 600  degrees of freedom
Residual deviance: 615.36  on 596  degrees of freedom
AIC: 625.4

Number of Fisher Scoring iterations: 4

简化模型中的每个回归系数都具有统计学意义(p < .05)。因为两个模型是嵌套的(fit.reducedfit.full的子集),你可以使用anova()函数来比较它们。对于广义线性模型,你将想要一个卡方版本的这个测试:

> anova(fit.reduced, fit.full, test="Chisq")
Analysis of Deviance Table

Model 1: ynaffair ~ age + yearsmarried + religiousness + rating
Model 2: ynaffair ~ gender + age + yearsmarried + children + 
    religiousness + education + occupation + rating
  Resid. Df Resid. Dev Df Deviance P(>|Chi|)
1       596        615                      
2       592        610  4     5.85      0.21

非显著的卡方值(p = 0.21)表明,具有四个预测变量的简化模型与具有九个预测变量的完整模型拟合得一样好,这加强了你认为性别、孩子、教育和职业不会显著增加预测的信念,除了方程中的其他变量。因此,你可以基于这个更简单的模型进行解释。

13.2.1 解释模型参数

让我们看看回归系数:

> coef(fit.reduced)
  (Intercept)           age  yearsmarried religiousness        rating 
        1.931        -0.035         0.101        -0.329        -0.461

在逻辑回归中,被建模的响应是Y = 1 的对数几率。回归系数给出了预测变量单位变化时响应对数几率的改变,同时保持所有其他预测变量不变。

因为对数几率难以解释,你可以对它们进行指数化,以便将结果放在几率尺度上:

> exp(coef(fit.reduced))
  (Intercept)           age  yearsmarried religiousness        rating 
        6.895         0.965         1.106         0.720         0.630

现在,你可以看到,已婚年限增加一年,婚外邂逅的几率会增加 1.106 倍(保持年龄、宗教信仰和婚姻评级不变)。相反,每增加一年年龄,婚外情事的几率会乘以 0.965 倍。婚外情事的几率随着已婚年限的增加而增加,随着年龄、宗教信仰和婚姻评级的降低而减少。因为预测变量不能等于 0,所以截距在这种情况下没有意义。

如果需要,你可以使用confint()函数来获取系数的置信区间。例如,exp(confint(fit.reduced))将打印出每个系数在几率尺度上的 95%置信区间。

最后,预测变量中的一个单位的变化可能本身并不有趣。对于二元逻辑回归,当预测变量变化 n 个单位时,响应变量上更高值的几率变化为 exp(β[j])^n。如果结婚年限增加 1 年将婚外情的几率乘以 1.106,那么增加 10 年会将几率乘以 1.106¹⁰,即 2.7,保持其他预测变量不变。

13.2.2 评估预测变量对结果概率的影响

对于我们中的许多人来说,用概率而不是几率来思考更容易。你可以使用predict()函数来观察改变预测变量的水平对结果概率的影响。第一步是创建一个包含你感兴趣的预测变量值的一个人工数据集。然后你可以使用这个人工数据集与predict()函数一起预测这些值发生结果事件的概率。

让我们应用这个策略来评估婚姻评级对婚外情概率的影响。首先,创建一个人工数据集,其中年龄、结婚年限和宗教信仰设置为它们的平均值,婚姻评级从 1 到 5 变化:

> testdata <- data.frame(rating=c(1, 2, 3, 4, 5), age=mean(Affairs$age),
                         yearsmarried=mean(Affairs$yearsmarried),
                         religiousness=mean(Affairs$religiousness))
> testdata
  rating  age yearsmarried religiousness
1      1 32.5         8.18          3.12
2      2 32.5         8.18          3.12
3      3 32.5         8.18          3.12
4      4 32.5         8.18          3.12 
5      5 32.5         8.18          3.12

接下来,使用测试数据集和预测方程来获取概率:

> testdata$prob <- predict(fit.reduced, newdata=testdata, type="response")
  testdata
  rating  age yearsmarried religiousness  prob
1      1 32.5         8.18          3.12 0.530
2      2 32.5         8.18          3.12 0.416
3      3 32.5         8.18          3.12 0.310
4      4 32.5         8.18          3.12 0.220
5      5 32.5         8.18          3.12 0.151

从这些结果中,你可以看到,当婚姻被评为 1 = 非常不幸福时,婚外情的概率从 0.53 下降到当婚姻被评为 5 = 非常幸福时的 0.15(保持年龄、结婚年限和宗教信仰不变)。现在看看年龄的影响:

> testdata <- data.frame(rating=mean(Affairs$rating),
                         age=seq(17, 57, 10),                 
                         yearsmarried=mean(Affairs$yearsmarried),
                         religiousness=mean(Affairs$religiousness))
> testdata
  rating age yearsmarried religiousness
1   3.93  17         8.18          3.12
2   3.93  27         8.18          3.12
3   3.93  37         8.18          3.12
4   3.93  47         8.18          3.12
5   3.93  57         8.18          3.12

> testdata$prob <- predict(fit.reduced, newdata=testdata, type="response")
> testdata
  rating age yearsmarried religiousness    prob
1   3.93  17         8.18          3.12   0.335
2   3.93  27         8.18          3.12   0.262
3   3.93  37         8.18          3.12   0.199
4   3.93  47         8.18          3.12   0.149
5   3.93  57         8.18          3.12   0.109

在这里,你可以看到,随着年龄从 17 岁增加到 57 岁,婚外遇的概率从 0.34 下降到 0.11,保持其他变量不变。使用这种方法,你可以探索每个预测变量对结果的影响。

13.2.3 过度离散

从二项分布中抽取数据的预期方差为 σ² = n**π(1 − π),其中 n 是观测数的数量,π 是属于 Y = 1 组的概率。过度离散发生在响应变量的观察方差大于从二项分布中预期的方差。过度离散可能导致测试标准误差扭曲和显著性测试不准确。

当存在过度离散时,你仍然可以使用glm()函数拟合逻辑回归,但在这个情况下,你应该使用准二项分布而不是二项分布。

检测过度离散的一种方法是将二项模型的残差偏差与残差自由度进行比较。如果这个比率

图像

比较大于 1,你有过度离散的证据。应用这个方法到“婚外情”的例子中,你有

> deviance(fit.reduced)/df.residual(fit.reduced)
[1] 1.032

这接近于 1,表明没有过度离散。

您还可以测试过度离散。为此,您需要拟合模型两次,但在第一次拟合中,您使用family="binomial",在第二次拟合中,您使用family="quasibinomial"。如果第一次情况中返回的glm()对象称为fit,而第二次情况中返回的对象称为fit.od,那么

pchisq(summary(fit.od)$dispersion * fit$df.residual,  
       fit$df.residual, lower = F)

提供了检验零假设 H[0]: φ = 1 与备择假设 H[1]: φ ≠ 1 的 p 值。如果 p 值较小(例如,小于 0.05),则拒绝零假设。

将此应用于Affairs数据集,您有

> fit <- glm(ynaffair ~ age + yearsmarried + religiousness + 
             rating, family = binomial(), data = Affairs)
> fit.od <- glm(ynaffair ~ age + yearsmarried + religiousness +
                rating, family = quasibinomial(), data = Affairs)
> pchisq(summary(fit.od)$dispersion * fit$df.residual,  
         fit$df.residual, lower = F)

[1] 0.34

得到的 p 值(0.34)显然不显著(p > 0.05),这加强了您对过度离散不是问题的信念。当我们讨论 Poisson 回归时,我们将回到过度离散的问题。

13.2.4 扩展

R 中提供了几个逻辑回归扩展和变体:

  • 稳健逻辑回归——robustbase包中的glmRob()函数可以用来拟合稳健广义线性模型,包括稳健逻辑回归。当拟合包含异常值和有影响力的观察值的逻辑回归模型时,稳健逻辑回归可能很有帮助。

  • 多项逻辑回归——如果响应变量有超过两个无序类别(例如,已婚/丧偶/离婚),您可以使用mlogit包中的mlogit()函数拟合多项逻辑回归。或者,您可以使用nnet包中的multinom()函数。

  • 有序逻辑回归——如果响应变量是一组有序类别(例如,信用风险为差/好/优秀),您可以使用MASS包中的polyr()函数拟合有序逻辑回归。

能够用多个类别(有序和无序)建模响应变量是一个重要的扩展,但它以更大的解释复杂性为代价。在这些情况下评估模型拟合和回归诊断也将更加复杂。

Affairs示例中,婚外联系的数量被二分化为是/否响应变量,因为我们的兴趣集中在受访者过去一年是否有婚外情。如果我们对数量——过去一年的遭遇次数——感兴趣,我们会直接分析计数数据。分析计数数据的一种流行方法是 Poisson 回归,这是我们接下来要讨论的主题。

13.3 Poisson 回归

Poisson 回归在预测由一组连续和/或分类预测变量表示的计数结果变量时非常有用。Coxe、West 和 Aiken(2009)提供了一个全面且易于理解的 Poisson 回归介绍。

为了说明泊松回归模型的拟合以及分析中可能出现的一些问题,我们将使用robustbase包中提供的 Breslow 癫痫数据(Breslow,1993)。具体来说,我们将考虑抗癫痫药物治疗对治疗开始后八周内发生的癫痫发作次数的影响。在继续之前,请确保已安装robustbase包。

在随机分配到药物或安慰剂条件之前和之后,对患有简单或复杂部分性癫痫的患者在八周期间内报告的年龄和癫痫发作次数进行了收集。Ysum(随机化后八周期间的癫痫发作次数)是响应变量。治疗条件(Trt)、年龄(Age)和基线八周期间报告的癫痫发作次数(Base)是预测变量。基线癫痫发作次数和年龄包括在内,因为它们可能对响应变量有影响。我们感兴趣的是,在考虑这些协变量后,是否存在药物治疗减少癫痫发作的证据。

首先,让我们看看数据集的摘要统计:

> data(epilepsy, package="robustbase")
> names(epilepsy)
 [1] "ID"    "Y1"    "Y2"    "Y3"    "Y4"    "Base"  "Age"   "Trt"   "Ysum" 
[10] "Age10" "Base4"

> summary(breslow.dat[6:9])
      Base            Age              Trt          Ysum      
 Min.   :  6.0   Min.   :18.0   placebo  :28   Min.   :  0.0  
 1st Qu.: 12.0   1st Qu.:23.0   progabide:31   1st Qu.: 11.5  
 Median : 22.0   Median :28.0                  Median : 16.0  
 Mean   : 31.2   Mean   :28.3                  Mean   : 33.1  
 3rd Qu.: 41.0   3rd Qu.:32.0                  3rd Qu.: 36.0  
 Max.   :151.0   Max.   :42.0                  Max.   :302.0  

注意,尽管数据集中有 11 个变量,但我们只关注前面描述的 4 个变量。基线和随机化后的癫痫发作次数都高度偏斜。让我们更详细地看看响应变量。以下代码生成了图 13.1 中的图形:

library(ggplot2)
   ggplot(epilepsy, aes(x=Ysum)) +
  geom_histogram(color="black", fill="white") + 
  labs(title="Distribution of seizures", 
       x="Seizure Count",
       y="Frequency") + 
  theme_bw()
ggplot(epilepsy, aes(x=Trt, y=Ysum)) +
  geom_boxplot() + 
  labs(title="Group comparisons", x="", y="") + 
  theme_bw()

图像

图 13.1 治疗后癫痫发作次数的分布(来源:Breslow 癫痫数据)

你可以清楚地看到因变量的偏斜性质和可能存在的异常值。乍一看,药物条件下的癫痫发作次数似乎较少,方差也较小。(你可能会期望泊松分布的数据伴随较小的均值会有较小的方差。)与标准 OLS 回归不同,这种方差的异质性在泊松回归中不是问题。

下一步是拟合泊松回归:

> fit <- glm(Ysum ~ Base + Age + Trt, data=epilepsy, family=poisson())
> summary(fit)

Call:
glm(formula = Ysum ~ Base + Age + Trt, family = poisson(), data = epilepsy)

Deviance Residuals: 
   Min      1Q  Median      3Q     Max  
-6.057  -2.043  -0.940   0.793  11.006  

Coefficients:
              Estimate Std. Error z value Pr(>|z|)    
(Intercept)   1.948826   0.135619   14.37  < 2e-16 ***
Base          0.022652   0.000509   44.48  < 2e-16 ***
Age           0.022740   0.004024    5.65  1.6e-08 ***
Trtprogabide -0.152701   0.047805   -3.19   0.0014 ** 
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1 

(Dispersion parameter for poisson family taken to be 1)

    Null deviance: 2122.73  on 58  degrees of freedom
Residual deviance:  559.44  on 55  degrees of freedom
AIC: 850.7

Number of Fisher Scoring iterations: 5

输出提供了偏差、回归参数、标准误差以及这些参数是否为 0 的检验。请注意,每个预测变量在p < 0.05 的水平上都是显著的。

13.3.1 解释模型参数

模型系数是通过coef()函数获得的,或者在summary()函数输出中的系数表中查看:

> coef(fit)
 (Intercept)         Base          Age Trtprogabide 
      1.9488       0.0227       0.0227      -0.1527 

在泊松回归中,被建模的因变量是条件均值对数 logeAge 的回归参数 0.0227 表示,在基线癫痫发作和治疗条件不变的情况下,年龄每增加一年,对数平均癫痫发作次数增加 0.02。截距是当每个预测变量等于 0 时的对数平均癫痫发作次数。由于年龄不能为零,且所有参与者基线癫痫发作次数都不是零,因此在这种情况下截距没有意义。

通常,在因变量的原始尺度(癫痫发作次数,而不是对数癫痫发作次数)上解释回归系数要容易得多。为了完成这个任务,需要将系数进行指数化:

> exp(coef(fit))
 (Intercept)         Base          Age Trtprogabide 
       7.020        1.023        1.023        0.858 

现在,你可以看到,在保持其他变量不变的情况下,年龄每增加一年会将预期癫痫发作次数乘以 1.023。这意味着年龄增加与癫痫发作次数增加有关。更重要的是,Trt(即从安慰剂转换为普瑞巴林)的每单位变化将预期癫痫发作次数乘以 0.86。在保持基线癫痫发作次数和年龄不变的情况下,你预计药物组与安慰剂组相比,癫痫发作次数将减少 14%(即,1-0.86)。

重要的是要记住,与逻辑回归中的指数化参数一样,泊松模型中的指数化参数对响应变量有乘法效应,而不是加法效应。此外,与逻辑回归一样,你必须评估你的模型是否存在过度离散。

13.3.2 过度离散

在泊松分布中,方差和均值是相等的。当响应变量的观测方差大于泊松分布预测的方差时,泊松回归中就会发生过度离散。由于在处理计数数据时经常遇到过度离散,并且可能会对结果的解释产生负面影响,我们将花一些时间来讨论它。

过度离散可能发生的原因有几个(Coxe 等人,2009 年):

  • 忽略一个重要的预测变量可能导致过度离散。

  • 过度离散也可能由称为 状态依赖性 的现象引起。在观测值中,假设计数中的每个事件都是独立的。对于癫痫数据,这意味着对于任何患者,癫痫发作的概率与其他癫痫发作的概率是独立的。但这种假设通常是不成立的。对于一个给定个体,第一次癫痫发作的概率不太可能与已经发作了 39 次的 40 次癫痫发作的概率相同。

  • 在纵向研究中,过度离散可能由重复测量数据中固有的聚类引起。我们在这里不会讨论纵向泊松模型。

如果存在过度离散,而你又没有在模型中考虑它,你将得到标准误差和置信区间太小,显著性检验太宽松(也就是说,你会发现实际上并不存在的影响)。

与逻辑回归一样,如果残差偏差与残差自由度的比率远大于 1,则表明存在过度离散。对于癫痫数据,这个比率是

> deviance(fit)/df.residual(fit)
[1] 10.17

这显然远大于 1。

qcc包提供了泊松情况下的过度离散测试。(务必在首次使用前下载并安装此包。)你可以使用以下代码对癫痫数据进行过度离散测试:

> library(qcc)
> qcc.overdispersion.test(breslow.dat$sumY, type="poisson")
Overdispersion test Obs.Var/Theor.Var Statistic p-value
       poisson data              62.9      3646       0

毫不奇怪,显著性检验的 p 值小于 0.05,强烈表明存在过度离散。

你仍然可以使用glm()函数拟合数据模型,只需将family="poisson"替换为family="quasipoisson"。这样做与存在过度离散时的逻辑回归方法类似:

> fit.od <- glm(sumY ~ Base + Age + Trt, data=breslow.dat,
                family=quasipoisson())
> summary(fit.od)

Call:
glm(formula = sumY ~ Base + Age + Trt, family = quasipoisson(), 
    data = breslow.dat)

Deviance Residuals: 
   Min      1Q  Median      3Q     Max  
-6.057  -2.043  -0.940   0.793  11.006  

Coefficients:
             Estimate Std. Error t value Pr(>|t|)    
(Intercept)   1.94883    0.46509    4.19  0.00010 ***
Base          0.02265    0.00175   12.97  < 2e-16 ***
Age           0.02274    0.01380    1.65  0.10509    
Trtprogabide -0.15270    0.16394   -0.93  0.35570    
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1 

(Dispersion parameter for quasipoisson family taken to be 11.8)

    Null deviance: 2122.73  on 58  degrees of freedom
Residual deviance:  559.44  on 55  degrees of freedom
AIC: NA

Number of Fisher Scoring iterations: 5

注意,准泊松方法中的参数估计与泊松方法产生的估计相同,但标准误差要大得多。在这种情况下,较大的标准误差导致了Trt(和Age)的 p 值大于 0.05。当你考虑过度离散时,没有足够的证据表明药物方案在控制基线癫痫发作率和年龄后能比接受安慰剂减少癫痫发作次数。

请记住,这个例子仅用于演示目的。结果不应被用来推断关于 progabide 在现实世界中的有效性的任何内容。我不是医生——至少不是医学医生——我甚至不在电视上扮演医生。

我们将通过对一些重要变体和扩展的讨论来结束对泊松回归的探索。

13.3.3 扩展

R 为基本的泊松回归模型提供了几个有用的扩展,包括允许变化时间段的模型、纠正过多零值的模型以及当数据包括异常值和有影响力的观测值时有用的稳健模型。我将分别描述每个。

带有不同时间段的泊松回归

我们对泊松回归的讨论仅限于测量固定时间段内计数的响应变量(例如,八周期间的癫痫发作次数,过去一年的交通事故次数,或一天内的亲社会行为次数)。时间长度在观测值之间是恒定的。但你可以拟合泊松回归模型,允许每个观测值的时间段变化。在这种情况下,结果变量是一个比率。

要分析比率,你必须包括一个变量(例如,时间),该变量记录每个观测值计数发生的时间长度。然后你将模型从

或者,等价地,

图片

为了拟合这个新模型,你需要在glm()函数中使用offset选项。例如,假设在 Breslow 研究中,患者在随机分组后参与的时间从 14 天到 60 天不等。你可以使用癫痫发作的频率作为因变量(假设你已经记录了每个患者的天数),并拟合以下模型

fit <- glm(Ysum ~ Base + Age + Trt, data=epilepsy, 
           offset= log(time), family=poisson)

其中Ysum是患者在研究期间随机分组后发生的癫痫发作次数。在这种情况下,你假设这种频率不会随时间变化(例如,4 天发生 2 次癫痫发作等同于 20 天发生 10 次癫痫发作)。

零膨胀泊松回归

有时,数据集中零计数的数量大于泊松模型预测的数量。这可能发生在人口中有一个永远不会参与所计数行为的子群体时。例如,在逻辑回归部分中描述的Affairs数据集中,原始结果变量(婚外情)计算了参与者过去一年中经历的婚外性交次数。很可能有一个忠诚的婚姻伴侣群体,无论研究的时间长短,他们都不会有婚外情。这些被称为结构零(主要是由该群体中的摇摆者)。

在这种情况下,你可以使用一种称为零膨胀泊松回归的方法来分析数据,该方法同时拟合两个模型——一个预测谁会或不会出轨,另一个预测如果排除了始终忠诚的人,参与者会有多少次出轨。想象一下,这是一个结合了逻辑回归(用于预测结构零)和泊松回归模型(预测非结构零的观察值的计数)的模型。零膨胀泊松回归可以使用pscl包中的zeroinfl()函数进行拟合。

稳健泊松回归

最后,robustbase包中的glmRob()函数可以用来拟合稳健的广义线性模型,包括稳健的泊松回归。如前所述,这在存在异常值和有影响力的观察值时可能很有帮助。

进一步探讨

广义线性模型是一个复杂且数学上复杂的主题,但有许多优秀的资源可以帮助你了解它们。关于这个主题的一个很好的简短介绍是 Dunteman 和 Ho(2006)。关于广义线性模型的经典(和高级)文本由 McCullagh 和 Nelder(1989)提供。Dobson 和 Barnett(2008)以及 Fox(2008)提供了全面且易于理解的表达。Faraway(2006)和 Fox(2002)在 R 的背景下提供了出色的介绍。

摘要

  • 广义线性模型允许你分析决定性非正态的响应变量,包括分类结果和离散计数。

  • 当分析具有二分(是/否)结果的调查时,可以使用逻辑回归。

  • Poisson 回归可用于分析当结果以计数或比率进行测量时的研究。

  • 与第八章中描述的线性模型相比,回归诊断在广义线性模型中可能更困难。特别是,你应该评估逻辑回归和 Poisson 回归模型是否存在过度分散。如果发现过度分散,在拟合模型时考虑使用如准二项分布或准泊松分布等其他误差分布。

14 主成分分析与因子分析

本章涵盖

  • 主成分分析

  • 探索性因子分析

  • 理解其他潜在变量模型

多变量数据中最具挑战性的方面之一是信息的纯粹复杂性。如果你有一个包含 100 个变量的数据集,你如何理解所有存在的相互关系?即使有 20 个变量,当你试图理解各个变量之间的关系时,也需要考虑 190 对相关系数。探索和简化复杂多变量数据的两种相关但不同的方法是主成分分析和探索性因子分析。

主成分分析 (PCA) 是一种数据降维技术,它将大量相关变量转换成一组较小的、不相关的变量,称为 主成分。例如,你可能使用 PCA 将 30 个相关(可能冗余)的环境变量转换成 5 个不相关的综合变量,尽可能保留原始变量集中的信息。

相比之下,探索性因子分析 (EFA) 是一套旨在揭示给定变量集中潜在结构的方法。它寻找一组较小的潜在或 潜在 变量,这些变量可以解释观测或 显性 变量之间的关系。例如,数据集 Harman74.cor 包含了 145 名七年级和八年级儿童接受的 24 项心理测试之间的相关性。如果你对这份数据应用 EFA,结果建议 276 个测试相关性可以由儿童在 4 个潜在因素(语言能力、处理速度、推理和记忆)上的能力来解释。这 24 项心理测试是观测或显性变量,而四个潜在因素或潜在变量是从这些观测变量的相关性中推导出来的。

图 14.1 展示了 PCA 和 EFA 模型的差异。主成分(PC1 和 PC2)是观测变量(X1 到 X5)的线性组合。用于形成线性组合的权重被选择以最大化每个主成分所解释的方差,同时保持这些成分不相关。

图片

图 14.1 比较主成分分析和因子分析模型。图示显示了观测变量(X1 到 X5)、主成分(PC1、PC2)、因子(F1、F2)和误差(e1 到 e5)。

相反,因素(F1 和 F2)被认为是观察变量的基础或“原因”,而不是它们的线性组合。误差(e1 到 e5)代表由因素未解释的观察变量的方差。圆圈表示因素和误差不是直接可观察的,而是从变量之间的相关性中推断出来的。在这个例子中,因素之间的弯曲箭头表示它们是相关的。在 EFA 模型中,相关因素是常见的,但不是必需的。

本章中描述的方法需要大量样本以推导出稳定的解。构成适当样本大小的问题有些复杂。直到最近,分析师使用诸如“因子分析需要比变量多 5-10 倍的主题”之类的经验法则。最近的研究表明,所需的样本大小取决于因素的个数、与每个因素相关的变量的个数以及因素集如何解释变量的方差(Bandalos 和 Boehm-Kaufman,2009)。我敢说,如果你有几百个观测值,你可能是安全的。在本章中,我们将研究人工小问题,以保持输出(和页数)可管理。

我们将首先回顾 R 中可用于执行 PCA 或 EFA 的函数,并简要概述涉及步骤。然后我们将仔细研究两个 PCA 示例,接着是一个扩展的 EFA 示例。本章末尾提供了 R 中可用于拟合潜在变量模型的其它包的简要概述。这次讨论包括用于验证性因子分析、结构方程建模、对应分析和潜在类别分析的包。

14.1 R 中的主成分分析和因子分析

在 R 的基础安装中,PCA 和 EFA 的函数分别是princomp()factanal()。在本章中,我们将关注psych包中提供的函数。它们提供了比基础版本更多的有用选项。此外,结果将以社会科学家更熟悉的度量标准报告,更有可能与其他统计软件包(如 SAS 和 IBM SPSS)中相应程序提供的输出相匹配。

表 14.1 列出了与psych包最相关的函数。在尝试本章中的示例之前,请务必安装该包。

表 14.1 psych包中有用的因子分析函数

函数 描述
principal() 带有可选旋转的主成分分析
fa() 通过主轴、最小残差、加权最小二乘或最大似然进行因子分析
fa.parallel() 平行分析的光谱图
factor.plot() 绘制因子或主成分分析的结果
fa.diagram() 绘制因素或主成分加载矩阵的图形
scree``() 因子和主成分分析的 Scree 图

EFA(以及在一定程度上 PCA)对于新用户来说常常令人困惑,因为它们描述了广泛的方法,每种方法都需要几个步骤(和决策)才能达到最终结果。最常见的步骤如下:

  1. 准备数据。PCA 和 EFA 都是从观测变量之间的相关性中推导出它们的解。你可以将原始数据矩阵或相关矩阵输入到 principal``()fa() 函数中。如果输入原始数据,相关矩阵将自动计算。在继续之前,务必筛选数据中的缺失值。默认情况下,psych 包在计算相关性时使用成对删除。

  2. 选择一个因子模型。决定 PCA(数据降维)或 EFA(揭示潜在结构)更适合你的研究目标。如果你选择 EFA 方法,你还需要选择一个特定的因子方法(例如,最大似然法)。

  3. 决定提取多少个成分/因子。

  4. 提取成分/因子。

  5. 旋转成分/因子。

  6. 解释结果。

  7. 计算成分或因子得分。

在本章的剩余部分,我们将仔细考虑每个步骤,从 PCA 开始。在本章末尾,你将找到 PCA/EFA 可能步骤的详细流程图(图 14.7)。一旦你阅读了中间的材料,这张图将更有意义。

14.2 主成分

PCA 的目标是用尽可能多的信息替换大量相关变量,同时用更少的无关变量。这些派生变量,称为 主成分,是观测变量的线性组合。具体来说,第一个主成分

PC[1] = a[1]X[1] + a[2]X[2] + ...+ a[k] X[k]

是 k 个观测变量的加权组合,它解释了原始变量集中最大的方差。第二个主成分是在约束条件下解释原始变量最大方差的最小组合,即它与第一个主成分 正交(不相关)。每个后续成分都最大化解释的方差量,同时保持与所有先前成分的不相关性。从理论上讲,你可以提取与变量数量一样多的主成分。但从实际观点来看,你希望可以用一个更小的成分集来近似整个变量集。让我们看一个简单的例子。

数据集 USJudgeRatings 包含了美国州高级法院法官的律师评级。数据框包含 43 个观测值和 12 个数值变量。表 14.2 列出了这些变量。

表 14.2 USJudgeRatings 数据集中的变量

变量 描述 变量 描述
CONT 律师与法官的接触次数 PREP 诉讼准备
INTG 司法诚信 FAMI 熟悉法律
DMNR 行为 ORAL 声音良好的口头裁决
DILG 勤奋 WRIT 声音良好的书面裁决
CFMG 案件流程管理 PHYS 体能
DECI 快速决策 RTEN 值得保留

从实际的角度来看,你能用更少的组合变量来总结 11 个评估评分(从INTGRTEN)吗?如果是这样,你需要多少个,它们将如何定义?因为目标是简化数据,所以你会使用 PCA 来解决这个问题。数据是原始分数格式,没有缺失值。因此,你的下一步是决定你需要多少个主成分。

14.2.1 选择提取成分的数量

在决定在主成分分析(PCA)中保留多少个成分时,有几个标准可供选择。它们包括

  • 根据先前经验和理论确定成分数量

  • 选择需要解释变量中某些阈值累积方差数量的成分数量(例如,80%)

  • 通过检查变量之间的k × k相关矩阵的特征值来选择保留成分的数量

最常见的方法是基于特征值。每个成分都与相关矩阵的特征值相关联。第一个主成分与最大的特征值相关联,第二个主成分与第二大的特征值相关联,依此类推。凯撒-哈里斯标准建议保留特征值大于 1 的成分。特征值小于 1 的成分解释的方差少于单个变量所包含的方差。在卡特尔斯克皮尔测试中,特征值与它们的成分编号相对应。此类图通常显示一个弯曲或肘部,并且保留此尖锐断裂点以上的成分。最后,你可以运行模拟,从与原始矩阵大小相同的随机数据矩阵中提取特征值。如果一个基于真实数据的特征值大于一组随机数据矩阵中相应平均特征值,则保留该成分。这种方法称为平行分析(参见 Hayton, Allen, 和 Scarpello,2004 年,以获取更多详细信息)。

你可以通过fa.parallel()函数同时评估所有三个特征值标准。对于 11 个评分(不包括CONT变量),必要的代码如下:

library(psych)
fa.parallel(USJudgeRatings[,-1], fa="pc", n.iter=100, 
            show.legend=FALSE, main="Scree plot with parallel analysis")
abline(h=1)

此代码生成了图 14.2 所示的图形。该图显示了基于观察到的特征值的斯克皮尔测试(以直线段和 x 表示),从 100 个随机数据矩阵中推导出的平均特征值(以虚线表示),以及大于 1 的特征值标准(以 y = 1 的水平线表示)。使用abline()函数在 y = 1 处添加水平线。

图片

图 14.2 评估保留USJudgeRatings示例中主成分的数量。折线图(带 x 的线)、大于 1 的特征值标准(水平线)和 100 次模拟的平行分析(虚线)表明应保留单个成分。

所有三个标准都表明,单个成分适合总结这个数据集。你的下一步是使用principal()函数提取主成分。

14.2.2 提取主成分

如前所述,principal()函数从原始数据矩阵或相关矩阵开始执行主成分分析。其格式为

principal(*r*, nfactors=, rotate=, scores=)

其中

  • r是一个相关矩阵或原始数据矩阵。

  • nfactors指定要提取的主成分数量(默认为 1)。

  • rotate表示要应用的旋转(默认为 varimax;见 14.2.3 节)。

  • scores指定是否计算主成分得分(默认为 false)。

要提取第一个主成分,你可以使用以下列表中的代码。

列表 14.1 USJudgeRatings的主成分分析

> library(psych)
> pc <- principal(USJudgeRatings[,-1], nfactors=1)
> pc

Principal Components Analysis
Call: principal(r = USJudgeRatings[, -1], nfactors=1)
Standardized loadings based upon correlation matrix
      PC1   h2    u2
INTG 0.92 0.84 0.157
DMNR 0.91 0.83 0.166
DILG 0.97 0.94 0.061
CFMG 0.96 0.93 0.072
DECI 0.96 0.92 0.076
PREP 0.98 0.97 0.030
FAMI 0.98 0.95 0.047
ORAL 1.00 0.99 0.009
WRIT 0.99 0.98 0.020
PHYS 0.89 0.80 0.201
RTEN 0.99 0.97 0.028

                 PC1
SS loadings    10.13
Proportion Var  0.92
[... additional output omitted ...]

在这里,你正在输入不带CONT变量的原始数据,并指定应提取一个未旋转的成分。(旋转在 14.3.3 节中解释。)因为 PCA 是在相关矩阵上进行的,所以在提取成分之前,原始数据会自动转换为相关矩阵。

标有 PC1 的列包含成分的载荷,即观察变量与主成分(s)的相关性。如果你提取了多个主成分,则会有 PC2、PC3 等列。成分载荷用于解释成分的意义。你可以看到每个变量都与第一个成分(PC1)高度相关。因此,它似乎是一个一般评估维度。

标有 h2 的列包含成分的共同性——每个变量由成分解释的方差量。u2 列包含成分的独特性——未由成分解释的方差量(或 1 – h2)。例如,物理能力(PHYS)评分的 80%的方差由第一个主成分解释,而 20%则不是。PHYS是单个成分解决方案表示最差的变量。

标有 SS Loadings 的行包含与成分相关的特征值。特征值是与特定成分相关的标准化方差(在这种情况下,第一个成分的值为10)。最后,标有 Proportion Var 的行表示每个成分解释的方差量。在这里,你可以看到第一个主成分解释了 11 个变量中的 92%的方差。

让我们考虑第二个例子,这个例子导致解决方案包含多个主成分。数据集Harman23.cor包含 305 名女孩的八个身体测量数据。在这种情况下,数据集由变量之间的相关性组成,而不是原始数据(见表 14.3)。

表 14.3 305 名女孩的身体测量之间的相关性(Harman23.cor

身高 臂展 前臂 下肢 体重 比特罗直径 胸围 胸围宽度
身高 1.00 0.85 0.80 0.86 0.47 0.40 0.30 0.38
臂展 0.85 1.00 0.88 0.83 0.38 0.33 0.28 0.41
前臂 0.80 0.88 1.00 0.80 0.38 0.32 0.24 0.34
下肢 0.86 0.83 0.8 1.00 0.44 0.33 0.33 0.36
体重 0.47 0.38 0.38 0.44 1.00 0.76 0.73 0.63
比特罗直径 0.40 0.33 0.32 0.33 0.76 1.00 0.58 0.58
胸围 0.30 0.28 0.24 0.33 0.73 0.58 1.00 0.54
胸围 0.38 0.41 0.34 0.36 0.63 0.58 0.54 1.00
来源:H. H. Harman,《现代因子分析》,第三版修订版(芝加哥大学出版社,1976 年),表 2.3。

再次,你希望用更少的派生变量替换原始的物理测量。你可以使用以下代码确定要提取的成分数量。在这种情况下,你需要识别相关矩阵(Harman23.cor对象的cov组件)并指定样本大小(n.obs):

library(psych)
fa.parallel(Harman23.cor$cov, n.obs=302, fa="pc", n.iter=100,
            show.legend=FALSE, main="Scree plot with parallel analysis")
abline(h=1)

图 14.3 显示了结果图。

图 14.3 评估保留身体测量示例的主成分数量。斯克里普图(带有 x 的线条)、特征值大于 1 的标准(水平线)以及与 100 次模拟的平行分析(虚线)表明应保留两个成分。

从图中可以看出,建议使用两个成分的解决方案。与第一个例子一样,凯撒-哈里斯标准、斯克里普测试和平行分析意见一致。这并不总是如此,你可能需要提取不同数量的成分,并选择看起来最有用的解决方案。下一个列表从相关矩阵中提取了前两个主成分。

列表 14.2 身体测量的主成分分析

> library(psych)
> pc <- principal(Harman23.cor$cov, nfactors=2, rotate="none")
> pc

Principal Components Analysis
Call: principal(r = Harman23.cor$cov, nfactors = 2, rotate = "none")
Standardized loadings based upon correlation matrix
                PC1   PC2   h2    u2
height         0.86 -0.37 0.88 0.123
arm.span       0.84 -0.44 0.90 0.097
forearm        0.81 -0.46 0.87 0.128
lower.leg      0.84 -0.40 0.86 0.139
weight         0.76  0.52 0.85 0.150
bitro.diameter 0.67  0.53 0.74 0.261
chest.girth    0.62  0.58 0.72 0.283
chest.width    0.67  0.42 0.62 0.375

                PC1  PC2
SS loadings    4.67 1.77
Proportion Var 0.58 0.22
Cumulative Var 0.58 0.81

[... additional output omitted ...]

如果你检查列表 14.2 中的 PC1 和 PC2 列,你会看到第一个成分解释了物理测量中 58%的方差,而第二个成分解释了 22%。这两个成分共同解释了 81%的方差。这两个成分共同解释了身高变量中 88%的方差。

通过检查载荷来解释成分和因子。第一个成分与每个物理测量值呈正相关,看起来是一个一般的大小因子。第二个成分对比前四个变量(身高、臂展、前臂和下肢),与后四个变量(体重、比特直径、胸围和胸宽)。因此,它似乎是一个长度与体积的因子。从概念上讲,这不是一个容易处理的构造。每当提取了两个或更多成分时,你可以旋转解决方案以使其更易于解释。这是我们接下来要讨论的主题。

14.2.3 旋转主成分

旋转是一组数学技术,用于将成分载荷矩阵转换为一个更易于解释的矩阵。它们通过尽可能净化成分来实现这一点。旋转方法在结果成分是否保持不相关(正交旋转)或允许相关(斜交旋转)方面有所不同。它们在净化定义上也有所不同。最流行的正交旋转是Varimax 旋转,它试图净化载荷矩阵的列,使得每个成分由一个有限的变量集定义(也就是说,每一列有几个大的载荷和许多非常小的载荷)。将 Varimax 旋转应用于身体测量数据,你将得到下一列表中提供的结果。你将在 14.4 节中看到一个斜交旋转的例子。

列表 14.3 使用 Varimax 旋转的主成分分析

> rc <- principal(Harman23.cor$cov, nfactors=2, rotate="varimax")
> rc

Principal Components Analysis
Call: principal(r = Harman23.cor$cov, nfactors = 2, rotate = "varimax")
Standardized loadings based upon correlation matrix
                RC1  RC2   h2    u2
height         0.90 0.25 0.88 0.123
arm.span       0.93 0.19 0.90 0.097
forearm        0.92 0.16 0.87 0.128
lower.leg      0.90 0.22 0.86 0.139
weight         0.26 0.88 0.85 0.150
bitro.diameter 0.19 0.84 0.74 0.261
chest.girth    0.11 0.84 0.72 0.283
chest.width    0.26 0.75 0.62 0.375

                RC1  RC2
SS loadings    3.52 2.92
Proportion Var 0.44 0.37
Cumulative Var 0.44 0.81

[... additional output omitted ...]

列名从 PC 变为 RC,以表示旋转后的成分。查看 RC1 列的载荷,可以看到第一个成分主要是由前四个变量(长度变量)定义的。RC2 列的载荷表明第二个成分主要是由第 5 个到第 8 个变量(体积变量)定义的。请注意,这两个成分仍然是不相关的,并且它们共同解释了变量,效果相同。你可以看到旋转后的解决方案同样好地解释了变量,因为变量的共同度没有变化。此外,两个成分旋转解决方案解释的累积方差(81%)没有变化。但是,每个单独成分解释的方差比例已经改变(第 1 个成分从 58%变为 44%,第 2 个成分从 22%变为 37%)。这种方差在成分间的分散是常见的,技术上,你现在应该称它们为成分而不是主成分(因为单个成分的方差最大化特性已经保留)。

最终目标是用一个较小的派生变量集替换一个较大的相关变量集。为此,你需要获得每个观察值在成分上的得分。

14.2.4 获取主成分得分

USJudgeRatings示例中,你从原始数据中提取了一个描述律师对 11 个变量评分的单个主成分。principal()函数使得获取每个参与者在此派生变量上的得分变得容易(见下一列表)。

列表 14.4 从原始数据中获取成分得分

> library(psych)
> pc <- principal(USJudgeRatings[,-1], nfactors=1, score=TRUE)
> head(pc$scores)
                      PC1
AARONSON,L.H. -0.1857981
ALEXANDER,J.M. 0.7469865
ARMENTANO,A.J. 0.0704772
BERDON,R.I.   1.1358765
BRACKEN,J.J. -2.1586211
BURNS,E.B.    0.7669406

principal()函数的选项scores=TRUE时,主成分得分保存在返回对象中的scores元素中。如果你愿意,现在你可以通过以下方式获取律师和法官之间发生的联系次数与他们对法官评价之间的相关性

> cor(USJudgeRatings$CONT, pc$score)
              PC1
[1,] -0.008815895

显然,律师的熟悉程度和他们的意见之间没有关系!

当主成分分析基于相关矩阵且原始数据不可用时,显然无法为每个观测值获得主成分得分。但你可以获得用于计算主成分的系数。

在身体测量数据中,你拥有身体测量之间的相关性,但你没有这些 305 个女孩的个体测量数据。你可以使用以下列表中的代码来获取评分系数。

列表 14.5 获取主成分评分系数

> library(psych)
> rc <- principal(Harman23.cor$cov, nfactors=2, rotate="varimax")
> round(unclass(rc$weights), 2)
                 RC1   RC2
height          0.28 -0.05
arm.span        0.30 -0.08
forearm         0.30 -0.09
lower.leg       0.28 -0.06
weight         -0.06  0.33
bitro.diameter -0.08  0.32
chest.girth    -0.10  0.34
chest.width    -0.04  0.27

组件得分是通过以下公式获得的

PC1 = 0.28*height + 0.30*arm.span + 0.30*forearm + 0.29*lower.leg - 
      0.06*weight - 0.08*bitro.diameter - 0.10*chest.girth - 
      0.04*chest.width 

PC2 = -0.05*height - 0.08*arm.span - 0.09*forearm - 0.06*lower.leg + 
       0.33*weight + 0.32*bitro.diameter + 0.34*chest.girth +    
       0.27*chest.width

这些方程假设物理测量已经被标准化(均值=0,标准差=1)。请注意,PC1 的权重倾向于在 0.3 或 0.0 左右。PC2 也是如此。作为一个实际问题,你可以通过将第一个复合变量作为前四个变量标准化得分的平均值来进一步简化你的方法。同样,你可以将第二个复合变量定义为第二个四个变量标准化得分的平均值。在实践中,我通常会这样做。

小吉夫征服了世界

在数据分析师中,关于 PCA 和 EFA 存在相当多的混淆。其中一个原因是历史的,可以追溯到一个小程序叫做 Little Jiffy(不是开玩笑)。Little Jiffy 是最受欢迎的早期因子分析程序之一,它默认为主成分分析,提取特征值大于 1 的成分,并将它们旋转到方差最大化解。这个程序被广泛使用,以至于许多社会科学家认为这种默认行为等同于 EFA。后来的许多统计软件包也将这些默认值纳入它们的 EFA 程序中。

如我在下一节中希望您看到的,PCA 和 EFA 之间存在重要和根本性的差异。要了解更多关于 PCA/EFA 混淆的信息,请参阅 Hayton、Allen 和 Scarpello(2004)。

如果你的目标是寻找解释你的观测变量的潜在潜在变量,你可以转向因子分析。这是下一节的主题。

14.3 探索性因子分析

EFA(主成分分析)的目标是通过揭示数据下更基本的未观察到的变量集合来解释一组观察变量的相关性。这些假设的、未观察到的变量被称为因子。(每个因子被假定为解释两个或多个观察变量之间的方差,所以技术上它们被称为共同因子。)

模型可以表示为

X[i] = a[1]F[1] + a[2]F[2] + ... + a[p]F[p] + U[i]

其中 X[i] 是第 i 个观察变量 (i = 1... k), F[j] 是共同因子 (j = 1...p), 且 p < k. U[i] 是变量 X[i] 中独特于该变量的部分(不是由共同因子解释的)。a[i] 是每个因子对观察变量组成的贡献程度。如果我们回到本章开头的Harman74.cor示例,我们会说,个人在 24 个观察到的心理测试中的得分是由于他们 4 个潜在心理结构能力的加权组合。

虽然 PCA 和 EFA 模型不同,但许多步骤看起来很相似。为了说明这个过程,你将应用 EFA 到六个心理测试之间的相关性。112 个人接受了六个测试,包括一个非言语的一般智力测量(一般)、一个图片完成测试(图片)、一个积木设计测试(积木)、一个迷宫测试(迷宫)、一个阅读理解测试(阅读)和一个词汇测试(词汇)。你能用更少的潜在或潜在心理结构来解释这些测试的参与者得分吗?

变量之间的协方差矩阵在数据集ability.cov中提供。你可以使用cov2cor()函数将其转换为相关矩阵:

> options(digits=2)
> covariances <- ability.cov$cov
> correlations <- cov2cor(covariances)
> correlations
        general picture blocks maze reading vocab
general    1.00    0.47   0.55 0.34    0.58  0.51
picture    0.47    1.00   0.57 0.19    0.26  0.24
blocks     0.55    0.57   1.00 0.45    0.35  0.36
maze       0.34    0.19   0.45 1.00    0.18  0.22
reading    0.58    0.26   0.35 0.18    1.00  0.79
vocab      0.51    0.24   0.36 0.22    0.79  1.00

因为你在寻找解释数据的假设结构,所以你会使用 EFA 方法。与 PCA 一样,下一个任务是决定提取多少个因子。

14.3.1 决定提取多少个共同因子

要决定提取多少个因子,请转向fa.parallel()函数:

> library(psych)
> covariances <- ability.cov$cov
> correlations <- cov2cor(covariances)
> fa.parallel(correlations, n.obs=112, fa="both", n.iter=100,
              main="Scree plots with parallel analysis")
> abline(h=c(0, 1))

图 14.4 显示了结果图。注意你已经要求函数显示主成分分析和共同因子方法的结果,以便你可以进行比较(fa = "both")。

在这个图表中要注意几个方面。如果你采用了 PCA 方法,你可能选择了其中一个成分(特征值测试,平行分析)或两个成分(特征值大于 1)。当不确定时,通常最好是过度因子化而不是不足因子化,因为过度因子化往往会导致对“真实”解的扭曲减少。

观察 EFA 结果,一个两因子解决方案明显指示。前两个特征值(三角形)在斯克里测试的弯曲点之上,并且也高于基于 100 个模拟数据矩阵的平均特征值。对于 EFA,Kaiser-Harris 标准是特征值超过 0 的数量,而不是 1。((大多数人没有意识到这一点,所以在聚会上打赌是个好方法。)在这种情况下,Kaiser-Harris 标准也建议两个因子。

图片

图 14.4 评估心理测试示例中保留的因子数量。PCA 和 EFA 的结果都显示出来。PCA 结果建议一个或两个成分。EFA 结果建议两个因子。

14.3.2 提取共同因子

现在你已经决定提取两个因子,你可以使用fa()函数来获得你的解决方案。fa()函数的格式是

fa(*r*, nfactors=, n.obs=, rotate=, scores=, fm=)

其中

  • r是相关矩阵或原始数据矩阵。

  • nfactors指定要提取的因子数量(默认为 1)。

  • n.obs是观测数量(如果输入相关矩阵)。

  • rotate指定要应用的旋转(默认为 oblimin)。

  • scores指定是否计算因子得分(默认为 false)。

  • fm指定因子化方法(默认为 minres)。

与 PCA 不同,有许多提取共同因子的方法,包括最大似然(ml)、迭代主轴(pa)、加权最小二乘(wls)、广义加权最小二乘(gls)和最小残差(minres)。统计学家倾向于更喜欢最大似然方法,因为它有一个定义良好的统计模型。有时这种方法无法收敛,在这种情况下,迭代主轴选项通常效果很好。要了解更多关于不同方法的信息,请参阅 Mulaik (2009)和 Gorsuch (1983)。

对于这个例子,你将使用迭代主轴法(fm = "pa")提取未旋转的因子。下一个列表给出了结果。

列表 14.6 无旋转的主轴因子化

> fa <- fa(correlations, nfactors=2, rotate="none", fm="pa")
> fa
Factor Analysis using method =  pa
Call: fa(r = correlations, nfactors = 2, rotate = "none", fm = "pa")
Standardized loadings based upon correlation matrix
         PA1   PA2   h2   u2
general 0.75  0.07 0.57 0.43
picture 0.52  0.32 0.38 0.62
blocks  0.75  0.52 0.83 0.17
maze    0.39  0.22 0.20 0.80
reading 0.81 -0.51 0.91 0.09
vocab   0.73 -0.39 0.69 0.31
                PA1  PA2
SS loadings    2.75 0.83
Proportion Var 0.46 0.14
Cumulative Var 0.46 0.60
[... additional output deleted ...]

你可以看到,这两个因子解释了六个心理测试中 60%的方差。然而,当你检查载荷时,它们并不容易解释。旋转它们应该会有所帮助。

14.3.3 旋转因子

你可以使用正交旋转或斜交旋转从 14.3.2 节中的两因子解决方案进行旋转。让我们尝试两种方法,这样你可以看到它们之间的区别。首先,尝试正交旋转(在下一个列表中)。

列表 14.7 使用正交旋转进行因子提取

> fa.varimax <- fa(correlations, nfactors=2, rotate="varimax", fm="pa")
> fa.varimax
Factor Analysis using method =  pa
Call: fa(r = correlations, nfactors = 2, rotate = "varimax", fm = "pa")
Standardized loadings based upon correlation matrix
         PA1  PA2   h2   u2
general 0.49 0.57 0.57 0.43
picture 0.16 0.59 0.38 0.62
blocks  0.18 0.89 0.83 0.17
maze    0.13 0.43 0.20 0.80
reading 0.93 0.20 0.91 0.09
vocab   0.80 0.23 0.69 0.31

                PA1  PA2
SS loadings    1.83 1.75
Proportion Var 0.30 0.29
Cumulative Var 0.30 0.60

[... additional output omitted ...]

观察因子载荷,因子确实更容易解释。阅读和词汇负荷在第一个因子上,而图片完成、块设计和迷宫负荷在第二个因子上。一般非言语智力测量负荷在两个因子上。这表明六个心理测试(显变量)之间的相关性可能由两个潜在的潜在变量(言语智力因子和非言语智力因子)解释。

通过使用正交旋转,你人为地迫使两个因子不相关。如果你允许两个因子相关,你会找到什么?你可以尝试斜旋转,如promax(见下一列表)。

列表 14.8 斜旋转因子提取

> fa.promax <- fa(correlations, nfactors=2, rotate="promax", fm="pa")
> fa.promax
Factor Analysis using method =  pa
Call: fa(r = correlations, nfactors = 2, rotate = "promax", fm = "pa")
Standardized loadings based upon correlation matrix
          PA1   PA2   h2   u2
general  0.36  0.49 0.57 0.43
picture -0.04  0.64 0.38 0.62
blocks  -0.12  0.98 0.83 0.17
maze    -0.01  0.45 0.20 0.80
reading  1.01 -0.11 0.91 0.09
vocab    0.84 -0.02 0.69 0.31

                PA1  PA2
SS loadings    1.82 1.76
Proportion Var 0.30 0.29
Cumulative Var 0.30 0.60

 With factor correlations of 
     PA1  PA2
PA1 1.00 0.57
PA2 0.57 1.00
[... additional output omitted ...]

正交解和斜解之间存在几个差异。在正交解中,注意力集中在因子结构矩阵(变量与因子的相关性)。在斜解中,需要考虑三个矩阵:因子结构矩阵、因子模式矩阵和因子互相关矩阵。

因子模式矩阵是一个标准化的回归系数矩阵。它们提供了从因子预测变量的权重。因子互相关矩阵给出了因子之间的相关性。

在列表 14.8 中,PA1 和 PA2 列中的值构成了因子模式矩阵。它们是标准化的回归系数,而不是相关性。检查这个矩阵的列仍然用于命名因子(尽管这里有一些争议)。再次,你会找到一个言语因子和非言语因子。

因子互相关矩阵表明两个因子之间的相关性为 0.57,这是一个相当大的数值。如果因子互相关性较低,你可能需要回到正交解以保持简单。

因子结构矩阵(或因子载荷矩阵)未提供,但你可以使用公式 F = P × Phi 容易地计算出它,其中 F 是因子载荷矩阵,P 是因子模式矩阵,Phi 是因子互相关矩阵。执行乘法的一个简单函数如下:

fsm <- function(oblique) {
if (class(oblique)[2]=="fa" & is.null(oblique$Phi)) {
    warning("Object doesn't look like oblique EFA")
} else {    
    P <- unclass(oblique$loading)
    F <- P %*% oblique$Phi
    colnames(F) <- c("PA1", "PA2")
    return(F)    
}
}

将此应用于示例,你得到

> fsm(fa.promax)
         PA1  PA2
general 0.64 0.69
picture 0.33 0.61
blocks  0.44 0.91
maze    0.25 0.45
reading 0.95 0.47
vocab   0.83 0.46

现在,你可以回顾变量与因子之间的相关性。将它们与正交解中的因子载荷矩阵进行比较,你会发现这些列并不那么纯粹。这是因为你允许潜在因子相关。尽管斜方法更复杂,但它通常是数据的更现实模型。

你可以使用factor.plot()fa.diagram()函数绘制正交或斜解。以下代码

factor.plot(fa.promax, labels=rownames(fa.promax$loadings))

生成图 14.5 中的图形。

图像

图 14.5 ability.cov.vocabreading 在第一个因子(PA1)上加载,而 blockspicturemaze 在第二个因子(PA2)上加载。一般智力测试同时加载在两个因子上。

以下代码

fa.diagram(fa.promax, simple=FALSE)

生成图 14.6。如果你设置 simple=TRUE,则只显示每个项目的最大载荷。该图显示了每个因子的最大载荷以及因子之间的相关性。当存在多个因子时,此类图很有帮助。

图 14.6 ability.cov 心理测试数据的斜两因子解图

当你处理现实生活中的数据时,你不太可能将因子分析应用于变量如此少的数据库。我们在这里这样做是为了保持事情的可管理性。如果你想测试你的技能,尝试对包含在 Harman74.cor 中的 24 个心理测试进行因子分析。以下代码

library(psych)
fa.24tests <- fa(Harman74.cor$cov, nfactors=4, rotate="promax") 

应该能帮助你入门。

14.3.4 因子得分

与 PCA 相比,EFA 的目标不太可能是计算因子得分。但通过包括 score=TRUE 选项(当有原始数据时),这些得分很容易从 fa() 函数中获得。此外,评分系数(标准化回归权重)在返回的对象的 weights 元素中可用。

对于 ability.cov 数据集,你可以使用以下方法获得计算两因子斜解的因子得分估计的贝塔权重:

> fa.promax$weights
         [,1]  [,2]
general 0.080 0.210
picture 0.021 0.090
blocks  0.044 0.695
maze    0.027 0.035
reading 0.739 0.044
vocab   0.176 0.039

与计算精确的成分得分不同,因子得分只能进行估计。存在几种方法。fa() 函数使用回归方法。要了解更多关于因子得分的信息,请参阅 DiStefano、Zhu 和 Mîndrila(2009)。

在继续之前,让我们简要回顾一下其他对探索性因子分析有用的 R 包。

14.3.5 其他 EFA 相关包

R 包含了其他几个有用的贡献包,用于进行因子分析。FactoMineR 包提供了 PCA 和 EFA 以及其他潜在变量模型的方法。它提供了许多我们在这里没有考虑的选项,包括使用数值和分类变量。FAiR 包使用遗传算法估计因子分析模型,该算法允许对模型参数施加不等式约束。GPArotation 包提供了许多额外的因子旋转方法。最后,nFactors 包提供了确定数据中潜在因子数量的复杂技术。

14.4 其他潜在变量模型

EFA 只是统计学中广泛使用的潜在变量模型之一。我们将以对 R 中可以拟合的其他模型的简要描述结束本章。这些包括测试先验理论,可以处理混合数据类型(数值和分类),或者仅基于分类多路表的模型。

在 EFA 中,你允许数据确定要提取的因素数量及其含义。但你可以从一个理论开始,这个理论关于一组变量背后的因素数量,变量如何加载到这些因素上,以及因素如何相互关联。然后你可以将这个理论与收集到的数据集进行测试。这种方法称为*确认性因子分析(CFA**)。

CFA 是称为*结构方程模型(SEM**)的方法论的一个子集。SEM 不仅允许你提出潜在因素的数量和组成,还允许你提出这些因素如何相互影响。你可以将 SEM 视为确认性因子分析(针对变量)和回归分析(针对因素)的组合。结果输出包括统计检验和拟合指数。R 中有几个用于 CFA 和 SEM 的优秀包,包括semOpenMxlavaan

可以使用ltm包将潜在模型拟合到测试和问卷中的项目。这种方法通常用于创建大规模标准化测试,如学术能力评估测试(SAT)和研究生入学考试(GRE)。

潜在类别模型(其中假设潜在因素是分类的而不是连续的)可以使用FlexMixlcmmrandomLCApoLCA包进行拟合。lcda包执行潜在类别判别分析,而lsa包执行潜在语义分析,这是一种在自然语言处理中使用的方 法。

ca包提供了简单和多重对应分析的函数。这些方法允许你分别探索双向和多向表中分类变量的结构。

最后,R 包含了许多用于*多维尺度分析(MDS**)的方法。MDS 旨在检测一组测量对象(例如,国家)之间的相似性和距离背后的潜在维度。基础安装中的cmdscale()函数执行经典 MDS,而MASS包中的isoMDS()函数执行非度量 MDS。vegan包也包含经典和非度量 MDS 的函数。

摘要

  • 主成分分析(PCA)是一种有用的数据降维方法,可以用较少的无关组合变量替换许多相关变量。

  • 探索性因子分析(EFA)包含了一系列用于识别潜在或未观察到的结构(因素)的方法,这些因素可能是一组观察或显性变量的基础。

  • 虽然 PCA 的目标通常是总结数据和降低其维度,但 EFA 可以用作假设生成工具,这在试图理解变量之间的关系时非常有用。它通常在社会科学理论发展中使用。

  • 主成分分析(PCA)和探索性因子分析(EFA)都是多步骤的过程,需要数据分析师在每一步做出选择。图 14.7 展示了这些步骤。

图片

图 14.7 主成分/探索性因素分析决策图

15 时间序列

本章涵盖

  • 创建一个时间序列

  • 将时间序列分解为组成部分

  • 开发预测模型

  • 预测未来的值

全球变暖的速度有多快,10 年后会有什么影响?除了第 9.6 节中的重复测量方差分析外,前面各章都集中在 横截面 数据上。在横截面数据集中,变量是在一个时间点测量的。相比之下,纵向 数据涉及在时间上反复测量变量。通过随时间跟踪一个现象,可以对其了解很多。

在本章中,我们将检查在一定时间段内以固定时间间隔记录的观察结果。我们可以将这些观察结果排列成一种形式为 Y[1],Y[2],Y[3],... ,Y[t],... ,Y[T]时间序列,其中 Y[t] 表示在时间 tY 的值,而 T 是序列中观察的总数。

考虑两个在图 15.1 中显示的非常不同的时间序列。左侧的序列包含 1960 年至 1980 年间每季度强生公司股票的季度收益(美元)。共有 84 个观察值:21 年中的每个季度都有一个。右侧的序列描述了从 1749 年到 1983 年由瑞士联邦天文台和东京天文台记录的每月平均相对太阳黑子数。太阳黑子时间序列要长得多,共有 2,820 个观察值——235 年中的每月一个。

图片

图 15.1 时间序列图:(左)1960 年至 1980 年间强生公司每股季度收益(美元),(右)从 1749 年到 1983 年记录的每月平均相对太阳黑子数

时间序列数据的研究涉及两个基本问题:发生了什么(描述),接下来会发生什么(预测)?对于强生公司的数据,你可能想知道,

  • 强生公司的股价随时间变化吗?

  • 存在季度效应吗?股价在一年中是否以规律的方式上升和下降?

  • 你能预测未来的股价,并且准确度如何?

对于太阳黑子数据,你可能想知道,

  • 哪些统计模型最能描述太阳黑子活动?

  • 哪些模型比其他模型更适合数据?

  • 在特定时间点的太阳黑子数量是否可预测,如果是的话,准确度如何?

准确预测股价的能力对我的(希望是)早期退休到热带岛屿上是有相关性的,而预测太阳黑子活动的能力则与我在该岛屿上的手机信号接收有关。

预测时间序列的未来值,或称为预测,是人类的一项基本活动,对时间序列数据的研究在现实世界中有着重要的应用。经济学家使用时间序列数据来理解和预测金融市场的变化。城市规划者使用时间序列数据来预测未来的交通需求。气候学家使用时间序列数据来研究全球气候变化。企业使用时间序列数据来预测产品需求和未来的销售。卫生官员使用时间序列数据来研究疾病的传播并预测特定地区未来病例的数量。地震学家研究时间序列数据来预测地震。在每种情况下,对历史时间序列的研究都是过程不可或缺的一部分。由于不同的方法可能对不同类型的时间序列效果最佳,因此在本章中我们将探讨许多例子。

描述时间序列数据和预测未来值的方法有很多。如果你处理时间序列数据,你会发现 R 拥有一些最全面的分析能力。本章探讨了最常见的描述和预测方法以及用于执行它们的 R 函数。表 15.1 列出了你将分析的时间序列数据。它们包含在 R 的基础安装中。这些数据集在特征和最佳拟合模型方面差异很大。

表 15.1 本章使用的数据集

时间序列 描述
AirPassengers 1949–1960 年每月的航空公司乘客数量
JohnsonJohnson 每季度强生公司每股收益
nhtemp 1912–1971 年康涅狄格州纽黑文的平均年气温
Nile 尼罗河的流量
sunspots 1749–1983 年每月的太阳黑子数量

我们将从创建和操作时间序列、描述和绘制它们以及将它们分解为水平、趋势、季节性和不规则(误差)成分的方法开始。然后我们将转向预测,从使用时间序列值加权平均的流行指数建模方法开始,用于预测未来的值。接下来,我们将考虑一组称为自回归积分移动平均(ARIMA)模型的预测技术,这些技术使用最近数据点之间的相关性以及最近预测误差之间的相关性来做出未来的预测。在整个过程中,我们将考虑评估模型拟合度和预测准确性的方法。本章以对这些主题了解更多资源的描述结束。

为了重现本章的分析,请确保在继续之前安装xtsforecasttseriesdirectlabels包(install.packages(c("xts", "forecast", "tseries", "directlabels")))。

15.1 在 R 中创建时间序列对象

要在 R 中处理时间序列,你必须将其放置在 时间序列对象 中——这是一个包含观测值和观测值日期指定的 R 结构。一旦数据在时间序列对象中,你就可以使用众多函数来操作、建模和绘制它。

R 包提供了多种用于存储时间序列的结构(参见侧边栏“R 中的时间序列对象”)。在本章中,我们将使用 xts 包提供的 xts 类。它支持规则和不规则时间序列,并具有许多用于操作时间序列数据的函数。

R 中的时间序列对象

在 R 提供的众多用于存储时间序列数据的对象中容易迷失方向。基础 R 包含 ts 用于存储具有规则时间间隔的单个时间序列,以及 mts 用于具有规则间隔的多个时间序列。zoo 包提供了一个可以存储不规则间隔时间序列的类,而 xts 包提供了一个包含更多支持函数的 zoo 类的超集。其他流行的格式包括 tsibbletimeSeriesirtstis。幸运的是,tsbox 包提供了将数据框转换为这些格式以及将一个时间序列格式转换为另一个格式的函数。

要创建 xts 时间序列,你将使用

library(xts)
*myseries* <- xts(*data*, *index*)

其中 data 是一个数值向量,index 是一个表示值观察时间的日期向量。以下列表显示了一个示例。数据包括从 2018 年 1 月开始的两年的月度销售额。

列表 15.1 创建时间序列对象

library(xts)
sales <- c(18, 33, 41,  7, 34, 35, 24, 25, 24, 21, 25, 20,          
           22, 31, 40, 29, 25, 21, 22, 54, 31, 25, 26, 35)
date  <- seq(from = as.Date("2018/1/1"), 
               to = as.Date("2019/12/1"), 
               by = "month")

sales.xts <- xts(sales, date)                                           

xts 格式的时间序列对象可以使用方括号 [] 表示法进行子集。例如,sales.xts["2018"] 将返回 2018 年所有的数据。指定 sales.xts ["2018-3/2019-5"] 将返回从 2018 年 3 月到 2019 年 5 月所有的数据。

还有一些 apply 函数旨在对时间序列对象的每个不同时间段执行函数。它们对于将时间序列聚合到更大的时间段特别有用。其格式为

*newseries* <- apply.*period*(*x*, *FUN*, ...)

其中 period 可以是 dailyweeklymonthlyquarterlyyearlyx 是一个 xts 时间序列对象,FUN 是要应用的函数,... 是传递给 FUN 的参数。

例如,quarterlies <- apply.quarterly(sales.xts, sum) 将返回一个包含八个季度销售总额的时间序列。sum 函数可以被替换为 meanmedianminmax 或任何返回单个值的函数。

forecast 包中的 autoplot() 函数可用于将时间序列数据绘制为 ggplot2 图表。以下列表提供了两个示例。

列表 15.2 绘制时间序列

library(ggplot2)
library(forecast)
autoplot(sales.xts)                                             ❶

autoplot(sales.xts) + 
  geom_line(color="blue") +                                     ❷
  scale_x_date(date_breaks="1 months",                          ❸
               date_labels="%b %y") +
  labs(x="", y="Sales", title="Customized Time Series Plot") +
  theme_bw() +                                                  ❹
  theme(axis.text.x = element_text(angle = 90, vjust = 0.5, hjust=1),
        panel.grid.minor.x=element_blank())

❶ 默认图表

❷ 设置线条颜色

❸ 指定 x 轴标签

❹ 调整主题

在第一个示例中,使用 autoplot() 函数创建 ggplot2 图 ❶。图 15.2 显示了该图表。

图 15.2 列表 15.1 中销售数据的时序图。这是 autoplot() 函数提供的默认格式。

在第二个例子中,图表被修改以使其更具吸引力。线条颜色被改为蓝色 ❷。使用 scale_x_date() 函数为 x 轴提供更好的标签 ❸。data_breaks 选项指定刻度标记之间的距离,可以取如 "1 day""2 weeks""5 years" 或适当的任何值。date_labels 选项指定标签的格式。在这里 "%b %y" 指定月份(3 个字母)和年份(2 位数字),中间有空格。参见第 4.6 节中的这些代码表。最后,选择了一个黑白主题,x 轴标签旋转 90 度,并抑制了垂直的次要网格线 ❹。图 15.3 显示了定制的图表。

图 15.3 列表 15.1 中销售数据的时序图。该图表通过颜色、更好的标签和更干净的主题元素进行了定制。

基础 R 中附带的时间序列示例(表 15.1)实际上是 ts 格式,但幸运的是,本章中介绍的功能可以处理 tsxts 格式的时间序列。

15.2 平滑和季节分解

就像分析师在尝试建模数据之前先使用描述性统计和图表来探索数据集一样,在尝试构建复杂模型之前,对时间序列进行数值和视觉描述应该是第一步。在本节中,我们将查看如何平滑时间序列以阐明其总体趋势,以及如何分解时间序列以观察任何季节性影响。

15.2.1 使用简单移动平均进行平滑

调查时间序列的第一步是绘制它,如列表 15.1 所示。考虑尼罗河时间序列。它记录了从 1871 年到 1970 年尼罗河在阿斯旺的年流量。该序列的图可以在图 15.4 的左上角面板中看到。时间序列似乎在下降,但年与年之间的变化很大。

时间序列通常具有显著的不规则或误差成分。为了在数据中辨别任何模式,你通常会想要绘制一条平滑曲线来抑制这些波动。平滑时间序列的最简单方法之一是使用简单移动平均。例如,每个数据点可以用该观测值及其前后一个观测值的平均值来代替。这被称为中心移动平均。中心移动平均被定义为

S[t] = (Y[t-q] + ... + Y[t] + ... + Y[t+q])/(2q + 1)

其中 S[t] 是时间 t 的平滑值,k = 2q + 1 是平均的观测数。k 值通常选择为奇数(在这个例子中,3)。在使用中心移动平均时,不可避免地会失去序列两端的 (k – 1) / 2 个观测值。

R 中有几个函数可以提供简单移动平均,包括TTR包中的SMA()zoo包中的rollmean(),以及forecast包中的ma()。在这里,你将使用ma()函数来平滑随 R 基础安装一起提供的Nile时间序列。

下一个列表中的代码使用 k 等于 3, 7 和 15 的值绘制了原始时间序列和平滑版本。图 15.3 显示了这些图表。

列表 15.3 简单移动平均

library(forecast)
library(ggplot2)

theme_set(theme_bw())
ylim <- c(min(Nile), max(Nile))

autoplot(Nile) +
  ggtitle("Raw time series") +
  scale_y_continuous(limits=ylim)

autoplot(ma(Nile, 3)) + 
  ggtitle("Simple Moving Averages (k=3)") +
  scale_y_continuous(limits=ylim)

autoplot(ma(Nile, 7)) +
  ggtitle("Simple Moving Averages (k=7)") +
  scale_y_continuous(limits=ylim)

autoplot(ma(Nile, 15)) +
  ggtitle("Simple Moving Averages (k=15)") +
  scale_y_continuous(limits=ylim)

随着 k 的增加,图表变得越来越平滑。挑战在于找到 k 的值,既能突出数据中的主要模式,又不会过度或不足平滑。这更多的是艺术而非科学,你可能会在确定一个值之前尝试几个 k 的值。从图 15.4 的图表中,确实可以看出在 1892 年至 1900 年之间河流流量有所下降。其他变化则有待解释。例如,1941 年至 1961 年之间可能存在一个小的上升趋势,但这也可能是随机变化。

图像

图 15.4 显示了从 1871 年至 1970 年在阿斯旺测量的尼罗河年流量时间序列(左上角)。其他图表是使用简单移动平均法在三个平滑级别(k = 3, 7, 和 15)上的平滑版本。

对于周期性大于 1 的时间序列数据(即具有季节性成分的数据),你将希望超越对整体趋势的描述。你可以使用季节分解来检查季节性和一般趋势。

15.2.2 季节分解

具有季节性方面的时间序列数据(如月度或季度数据)可以分解为趋势成分、季节成分和不规则成分。趋势成分 捕捉随时间变化的水平变化。季节成分 捕捉由于年份时间而产生的周期性效应。不规则(或误差成分 捕捉趋势和季节性效应未描述的影响。

分解可以是加法或乘法。在加法模型中,各成分相加给出时间序列的值。具体来说,

Y[t] = 趋势[t] + 季节[t] + 不规则[t]

其中,时间 t 的观测值是时间 t 的趋势、时间 t 的季节性效应以及时间 t 的不规则效应的总和。

在乘法模型中,由以下方程给出

Y[t] = 趋势[t] × 季节[t] × 不规则[t]

趋势、季节和不规则影响是相乘的。图 15.5 显示了示例。

图像

图 15.5 由不同组合的趋势、季节和不规则成分构成的时间序列示例

在第一幅图(a)中,没有趋势或季节性成分,唯一的影响是围绕给定水平的随机波动。在第二幅图(b)中,随着时间的推移存在上升趋势以及随机波动。在第三幅图(c)中,存在季节性效应和随机波动,但没有整体趋势偏离水平线。在第四幅图(d)中,所有三个成分都存在:上升趋势、季节性效应和随机波动。在最后的图(e)中,也看到了所有三个成分,但在这里它们以乘性方式结合。注意变异性与水平成正比:随着水平的增加,变异性也增加。这种基于当前序列水平的放大(或可能的衰减)强烈表明存在乘性模型。

一个例子可以使加性和乘性模型之间的区别更清晰。考虑一个记录了 10 年期间摩托车月度销售的时序。在具有加性季节效应的模型中,11 月和 12 月摩托车销量通常会增加 500 辆(由于圣诞节购物热潮),而在 1 月(销量通常会放缓)会减少 200 辆。季节性增加或减少与当前销量无关。

在具有乘性季节效应的模型中,11 月和 12 月的摩托车销量通常会增加 20%,而在 1 月会减少 10%。在乘性情况下,季节效应的影响与当前销量成正比,这与加性模型不同。在许多情况下,乘性模型更符合实际情况。

将时间序列分解为趋势、季节性和不规则成分的流行方法是使用局部加权回归的季节分解。在 R 中,可以使用 stl() 函数实现。其格式为

stl(*ts*, s.window=, t.window=)

其中 ts 是要分解的时间序列,s.window 控制季节效应随时间变化的快慢,t.window 控制趋势随时间变化的快慢。设置 s.window="periodic" 强制季节效应在年份间保持一致。只需要 tss.window 参数。有关详细信息,请参阅 help(stl)

stl() 函数只能处理加性模型,但这并不是一个严重的限制。通过对数变换可以将乘性模型转换为加性模型:

log(Y[t]) = log(Trend[t] × Seasonal[t] × Irregular[t])

= log(Trend[t]) + log(Seasonal[t]) + log(Irregular[t])

将加性模型拟合到对数变换后的序列后,结果可以回变换到原始尺度。让我们来看一个例子。

时间序列 AirPassengers 随 R 的基本安装提供,描述了 1949 年至 1960 年之间每月国际航空公司乘客总数(以千为单位)。图 15.6 的顶部显示了数据的图表。从图中可以看出,序列的变异性随着水平的增加而增加,这表明存在乘性模型。

图 15.6 下半部分的图显示了通过对每个观测值取对数创建的时间序列。方差已稳定,对数序列看起来是加性分解的合适候选。这是使用列表 15.4 中的 stl() 函数完成的。

图 15.6 AirPassengers 时间序列图(顶部)。该时间序列包含 1949 年至 1960 年之间每月的国际航空公司乘客总数(以千为单位)。对时间序列进行对数变换(底部)可以稳定方差,并更好地拟合加性季节分解模型。

列表 15.4 使用 stl() 进行季节分解

> library(forecast)
> library(ggplot2)
> autoplot(AirPassengers)                          ❶
> lAirPassengers <- log(AirPassengers)
> autoplot(lAirPassengers, ylab="log(AirPassengers)")

> fit <- stl(lAirPassengers, s.window="period")    ❷
> autoplot(fit)

> fit$time.series                                  ❸

         seasonal trend  remainder
Jan 1949 -0.09164 4.829 -0.0192494
Feb 1949 -0.11403 4.830  0.0543448
Mar 1949  0.01587 4.831  0.0355884
Apr 1949 -0.01403 4.833  0.0404633
May 1949 -0.01502 4.835 -0.0245905
Jun 1949  0.10979 4.838 -0.0426814
... output omitted ...

> exp(fit$time.series)

         seasonal trend remainder
Jan 1949   0.9124 125.1    0.9809
Feb 1949   0.8922 125.3    1.0558
Mar 1949   1.0160 125.4    1.0362
Apr 1949   0.9861 125.6    1.0413
May 1949   0.9851 125.9    0.9757
Jun 1949   1.1160 126.2    0.9582

... output omitted ...

❶ 绘制时间序列图

❷ 分解时间序列

❸ 每个观测值的成分

首先,绘制并变换时间序列 ❶。执行季节分解并将其保存到名为 fit 的对象中 ❷。绘制结果给出图 15.7 中的图形,显示了 1949 年至 1960 年的时间序列、季节性、趋势和不规则成分。请注意,季节性成分已被约束为每年保持相同(使用 s.window="period" 选项)。趋势是单调递增的,季节性效应表明夏季(可能是假期期间)的乘客更多。右侧的灰色条是幅度指南——每个条代表相同的幅度。这很有用,因为每个图形的 y 轴都不同。

图 15.7 使用 stl() 函数对对数 AirPassengers 时间序列进行季节分解。时间序列(数据)被分解为季节性、趋势和不规则成分。

stl() 函数返回的对象包括一个名为 time.series 的组件,它包含每个观测值的趋势、季节和不规则部分 ❸。在这种情况下,fit$time.series 基于对数时间序列。exp(fit$time .series) 将分解转换回原始度量。检查季节性效应表明,7 月份(乘数为 1.24)的乘客数量增加了 24%,而在 11 月份(乘数为 0.80)的乘客数量减少了 20%。

forecast 包提供了用于可视化季节分解的额外工具。以下列表展示了创建月度图和季节性图的示例。

列表 15.5 月度和季节性图

library(forecast)
library(ggplot2)
library(directlabels)

ggmonthplot(AirPassengers)  +                         ❶
  labs(title="Month plot: AirPassengers",             ❶
       x="",                                          ❶
       y="Passengers (thousands)")                    ❶

p <- ggseasonplot(AirPassengers) + geom_point() +     ❷
  labs(title="Seasonal plot: AirPassengers",          ❷
       x="",                                          ❷
       y="Passengers (thousands)")                    ❷
direct.label(p)                                       ❷

❶ 月度图

❷ 季节性图

图 15.8 AirPassengers 时间序列的月度图。月度图显示了每个月的子序列(1949 年至 1960 年所有 1 月份的值连接,所有 2 月份的值连接,依此类推),以及每个子序列的平均值。每个月都有一个均匀递增的趋势,乘客最倾向于在 7 月和 8 月出行。

月度图(图 15.8)显示了每个月的子序列(所有 1 月份的值相连,所有 2 月份的值相连,依此类推),以及每个子序列的平均值。从这张图中可以看出,趋势在每个月都以大致均匀的方式增加。此外,7 月和 8 月是乘客数量最多的月份。

季节图(图 15.9)显示了按年份划分的子序列。再次,你看到相似的图案,每年乘客数量都在增加,并且具有相同的季节性模式。默认情况下,ggplot2包会为年份变量创建图例。directlabels包用于将年份标签直接放置在图表上,紧邻时间序列中的每条线。

图 15.9 AirPassengers时间序列的季节图(底部)。每个图都显示了一个逐年增加的趋势和相似的季节性模式。

注意,尽管你已经描述了时间序列,但你并没有预测任何未来的值。在下一节中,我们将考虑使用指数模型来预测超出可用数据的情况。

15.3 指数预测模型

指数模型是预测时间序列未来值的最流行方法之一。它们比许多其他类型的模型更简单,但它们可以在广泛的领域中提供良好的短期预测。它们在模型的时间序列组件上有所不同。一个简单的指数模型(也称为单指数模型)拟合一个在时间i处具有恒定水平和不规则成分的时间序列,但没有趋势或季节成分。一个双指数模型(也称为霍尔特指数平滑)拟合一个具有水平和趋势的时间序列。最后,一个三指数模型(也称为霍尔特-温特斯指数平滑)拟合一个具有水平、趋势和季节成分的时间序列。

指数模型可以使用forecast包中提供的ets()函数进行拟合。ets()函数的格式为

ets(*ts*, model="ZZZ") 

其中ts是一个时间序列,model由三个字母指定。第一个字母表示误差类型,第二个字母表示趋势类型,第三个字母表示季节类型。允许的字母是A表示加法,M表示乘法,N表示无,Z表示自动选择。表 15.2 显示了常见模型的示例。

表 15.2 简单、双指数和三指数预测模型拟合函数

类型 参数拟合 函数
simple level ets(ts, model="ANN") ses(ts)
double level, slope ets(ts, model="AAN") holt(ts)
triple level, slope, seasonal ets(ts, model="AAA") hw(ts)

ses()holt()hw()函数是针对ets()函数的便利包装,具有预定义的默认值。首先,我们将查看最基本指数模型:简单指数平滑。

15.3.1 简单指数平滑

简单指数平滑使用现有时间序列值的加权平均来对未来的短期值进行预测。权重选择使得随着时间的推移,观测值对平均值的指数衰减影响。

简单指数平滑模型假设时间序列中的观测值可以描述为

Y[t] = 水平 + 不规则[t]

时间 Y[t][+1] 的预测(称为 1 步预测)表示为

Y[t][+1] = c[0]Y[t] + c[1]Y[t][−1] + c[2]Y[t][−2] + ...

其中 c[i] = α(1−α)^i, t = 0,1,2, ... 且 0 ≤ α ≤ 1。c[i] 权重之和为 1,一步预测可以看作是当前值和所有过去时间序列值的加权平均。alpha (α) 参数控制权重的衰减速率。alpha 越接近 1,近期观测值的权重就越大。alpha 越接近 0,过去观测值的权重就越大。alpha 的实际值通常由计算机选择以优化拟合标准。一个常见的拟合标准是实际值和预测值之间平方误差的和。以下示例将有助于阐明这些概念。

nhtemp 时间序列包含康涅狄格州纽黑文从 1912 年到 1971 年的平均年气温(华氏度)。图 15.10 显示了时间序列的线形图。

没有明显的趋势,并且年度数据缺乏季节成分,因此简单指数模型是一个合理的起点。使用 ses() 函数进行一步预测的代码如下所示。

列表 15.6 简单指数平滑

> library(forecast)
> fit <- ets(nhtemp, model="ANN")     ❶
> fit

ETS(A,N,N) 

Call:
 ets(y = nhtemp, model = "ANN") 

  Smoothing parameters:
    alpha = 0.1819 

  Initial states:
    l = 50.2762 

  sigma:  1.1455

     AIC     AICc      BIC 
265.9298 266.3584 272.2129 

> forecast(fit, 1)                    ❷

     Point Forecast  Lo 80  Hi 80  Lo 95  Hi 95
1972          51.87 50.402 53.338 49.625 54.115

> autoplot(forecast(fit, 1)) +
  labs(x = "Year", 
       y = expression(paste("Temperature (", degree*F,")",)),
       title = "New Haven Annual Mean Temperature")

> accuracy(fit)                       ❸

                ME  RMSE   MAE  MPE  MAPE   MASE
Training set 0.146 1.126 0.895 0.242 1.749 0.751 

❶ 符合模型

❷ 一步预测

❸ 打印准确度指标

ets(mode="ANN") 语句将简单指数模型拟合到 nhtemp 时间序列 ❶。A 表示误差是可加的,而 NN 表示没有趋势和季节成分。alpha(0.18)的相对较低值表明在预测中考虑了远期和近期观测值。此值自动选择以最大化模型与给定数据集的拟合度。

使用 forecast() 函数来预测时间序列 k 步的未来值。格式为 forecast(``fit, k)。此系列的一步预测为 51.9°F,95% 置信区间为 (49.6°F 至 54.1°F) ❷。图 15.10 显示了时间序列、预测值以及 80%和 95%置信区间的图示。

图 15.10 康涅狄格州纽黑文的平均年气温以及使用 ets() 函数进行的一步预测

forecast 包还提供了一个 accuracy() 函数,该函数显示时间序列预测中最常用的预测准确度指标 ❸。表 15.3 描述了每一个。e[t] 代表每个观察值(Y[i]Ŷ[i])的错误或不规则成分。

表 15.3 预测准确度指标

指标 缩写 定义
平均误差 ME mean(e[t])
均方根误差 RMSE sqrt(mean(e[t]²))
平均绝对误差 MAE mean(|e[t]|)
平均百分比误差 MPE mean(100 × e[t] / Y[t])
平均绝对百分比误差 MAPE mean(|100 × e[t] / Y[t]|)
平均绝对缩放误差 MASE mean(|q[t]|) 其中 q[t] = e[t] / (1/(T–1) * sum(|y[t]y[t][–1]|)),T 是观察数量,求和从 t = 2 到 t = T

平均误差和平均百分比误差可能不是很有用,因为正负误差可能会相互抵消。RMSE 给出平均平方误差的平方根,在这种情况下是 1.13°F。平均绝对百分比误差将误差报告为时间序列值的百分比。它是无单位的,可以用于比较不同时间序列的预测准确度。但它假设有一个具有真实零点的测量尺度(例如,每天乘客数量)。因为华氏尺度没有真实零点,所以在这里不能使用。平均绝对缩放误差是最新的准确度指标,用于比较不同尺度上时间序列的预测准确度。没有一种预测准确度的最佳度量。RMSE 无疑是最为人所知且最常引用的。

简单指数平滑假设不存在趋势或季节性成分。下一节将考虑可以同时容纳这两种成分的指数模型。

15.3.2 Holt 和 Holt–Winters 指数平滑

Holt 指数平滑方法可以拟合具有总体水平和趋势(斜率)的时间序列。时间 t 的观测值模型为

Y[t] = 水平 + 斜率 × t + 不规则[t]

一个 alpha 平滑参数控制水平的指数衰减,一个 beta 平滑参数控制斜率的指数衰减。同样,每个参数的范围从 0 到 1,较大的值在计算季节性效应时给予近期观察更大的权重。

Holt–Winters 指数平滑方法可用于拟合具有总体水平、趋势和季节性成分的时间序列。在这里,模型是

Y[t] = 水平 + 斜率 × t + s[t] + 不规则[t]

其中 s[t] 表示时间 t 的季节性影响。除了 alpha 和 beta 参数外,一个 gamma 平滑参数控制季节性成分的指数衰减。与其他参数一样,它介于 0 到 1 之间,较大的值在计算季节性效应时给予近期观察更大的权重。

在第 15.2 节中,您将描述国际航空公司每月总客流量(以对数千为单位)的时间序列分解为加法趋势、季节性和不规则成分。让我们使用指数模型来预测未来的旅行。同样,您将使用对数值,以便加法模型适合数据。以下列表中的代码应用了 Holt-Winters 指数平滑方法来预测AirPassengers时间序列的下一个五个值。

列表 15.7 具有水平、斜率和季节性成分的指数平滑

> library(forecast)
> fit <- ets(log(AirPassengers), model="AAA")      
> fit

ETS(A,A,A) 

Call:
 ets(y = log(AirPassengers), model = "AAA") 

    Smoothing parameters:                               ❶
    alpha = 0.6975 
    beta  = 0.0031 
    gamma = 1e-04 

  Initial states:
    l = 4.7925 
    b = 0.0111 
    s = -0.1045 -0.2206 -0.0787 0.0562 0.2049 0.2149
           0.1146 -0.0081 -0.0059 0.0225 -0.1113 -0.0841

  sigma:  0.0383

    AIC    AICc     BIC 
-207.17 -202.31 -156.68 

>accuracy(fit)

                     ME    RMSE      MAE       MPE    MAPE    MASE
Training set -0.0018307 0.03607 0.027709 -0.034356 0.50791 0.22892
> pred <- forecast(fit, 5)                              ❷
> pred
         Point Forecast  Lo 80  Hi 80  Lo 95  Hi 95
Jan 1961         6.1093 6.0603 6.1584 6.0344 6.1843
Feb 1961         6.0925 6.0327 6.1524 6.0010 6.1841
Mar 1961         6.2366 6.1675 6.3057 6.1310 6.3423
Apr 1961         6.2185 6.1412 6.2958 6.1003 6.3367
May 1961         6.2267 6.1420 6.3115 6.0971 6.3564

> autoplot(pred) +
  labs(title = "Forecast for Air Travel",
       y = "Log(AirPassengers)", 
       x ="Time")

> pred$mean <- exp(pred$mean)                           ❸
> pred$lower <- exp(pred$lower)                         ❸
> pred$upper <- exp(pred$upper)                         ❸
> p <- cbind(pred$mean, pred$lower, pred$upper)
> dimnames(p)[[2]] <- c("mean", "Lo 80", "Lo 95", "Hi 80", "Hi 95")
> p                                                             

           mean  Lo 80  Lo 95  Hi 80  Hi 95
Jan 1961 450.04 428.51 417.53 472.65 485.08
Feb 1961 442.54 416.83 403.83 469.85 484.97
Mar 1961 511.13 477.01 459.88 547.69 568.10
Apr 1961 501.97 464.63 446.00 542.30 564.95
May 1961 506.10 464.97 444.57 550.87 576.15

❶ 平滑参数

❷ 未来预测

❸ 在原始尺度上进行预测

水平(.70)、趋势(.0004)和季节性成分(.003)的平滑参数在❶中给出。趋势的值较低(.0001)并不意味着没有斜率;它表示从早期观察中估计的斜率不需要更新。

forecast()函数为未来五个月生成预测❷,并在图 15.11 中绘制。由于预测是在对数尺度上,因此使用指数运算来获得原始度量的预测:乘客数量(以千为单位)❸。矩阵pred$mean包含点预测,矩阵pred$lowerpred$upper分别包含 80%和 95%的置信下限和上限。使用exp()函数将预测返回到原始尺度,并使用cbind()创建单个表。因此,模型预测 3 月将有 509,200 名乘客,95%置信区间为 454,900 至 570,000。

图像

图 15.11 基于 Holt-Winters 指数平滑模型的五年预测,预测的是以千为单位的国际航空公司乘客数量的对数。数据来自AirPassengers时间序列。

15.3.3 ets()函数和自动预测

ets()函数具有额外的功能。您可以使用它来拟合具有乘法成分的指数模型,添加阻尼成分,并执行自动预测。让我们逐一考虑这些功能。

在上一节中,您将加法指数模型拟合到AirPassengers时间序列的对数。作为替代,您也可以将乘法模型拟合到原始数据。函数调用将是ets(AirPassengers, model="MAM")。趋势仍然是加法的,但季节性和不规则成分被假定为乘法的。在这种情况下使用乘法模型,准确性统计和预测值将报告在原始度量(千名乘客)上——这是一个明显的优势。

ets()函数还可以拟合阻尼成分。时间序列预测通常假设趋势将永远持续下去(比如房地产市场,对吗?)。阻尼成分会迫使趋势在一段时间内达到水平渐近线。在许多情况下,阻尼模型能做出更现实的预测。

最后,你可以调用ets()函数来自动选择最适合数据的模型。让我们将自动指数模型拟合到本章引言中描述的强生公司数据。以下列表中的代码允许软件选择一个最佳拟合模型。

列表 15.8 使用ets()函数进行自动指数预测

> library(forecast)
> fit <- ets(JohnsonJohnson)
> fit

ETS(M,M,M) 

Call:
 ets(y = JohnsonJohnson) 

    Smoothing parameters:
    alpha = 0.2776 
    beta  = 0.0636 
    gamma = 0.5867 

  Initial states:
    l = 0.6276 
    b = 0.0165 
    s = -0.2293 0.1913 -0.0074 0.0454

  sigma:  0.0921

   AIC   AICc    BIC 
163.64 166.07 185.52 

> autoplot(forecast(fit)) +
  labs(x = "Time",
       y = "Quarterly Earnings (Dollars)",
       title="Johnson and Johnson Forecasts")

因为没有指定模型,软件将在广泛的模型中进行搜索,以找到最小化拟合标准(默认为对数似然)的模型。选定的模型具有乘法趋势、季节性和误差成分。图 15.12 给出了图表,以及对未来八个季度的预测(在这种情况下默认为八个季度)。

图 15.12 带趋势和季节成分的乘法指数平滑预测。预测值用虚线表示,80%和 95%的置信区间分别用浅蓝色和深蓝色表示。

如前所述,指数时间序列建模因其能在许多情况下提供良好的短期预测而受到欢迎。第二种流行的方法是 Box-Jenkins 方法,通常称为 ARIMA 模型。这些将在下一节中描述。

15.4 ARIMA 预测模型

在预测的自回归积分移动平均(ARIMA)方法中,预测值是近期实际值和近期预测误差(残差)的线性函数。ARIMA 是一种复杂的预测方法。在本节中,我们将仅讨论非季节性时间序列的 ARIMA 模型。

在描述 ARIMA 模型之前,需要定义几个术语,包括滞后、自相关、偏自相关、差分和稳定性。这些内容将在下一节中讨论。

15.4.1 前置概念

当你对时间序列进行滞后处理时,你会将其向后移动给定数量的观测值。考虑来自尼罗河时间序列的前几个观测值,如表 15.4 所示。滞后 0 是未移动的时间序列。滞后 1 是将时间序列向左移动一个位置。滞后 2 是将时间序列向左移动两个位置,依此类推。可以使用函数lag(ts,k)对时间序列进行滞后,其中ts是时间序列,k是滞后数。

表 15.4 在不同滞后下的尼罗河时间序列

滞后 1869 1870 1871 1872 1873 1874 1875 ...
0 1120 1160 963 1210 1160 ...
1 1120 1160 963 1210 1160 1160 ...
2 1120 1160 963 1210 1160 1160 813 ...

自相关衡量时间序列中观测值之间的关系。AC[k]是一组观测值(Y[t])与较早的k个周期的观测值(Y[t−k])之间的相关性。因此,AC[1]是滞后 1 和滞后 0 时间序列之间的相关性,AC[2]是滞后 2 和滞后 0 时间序列之间的相关性,依此类推。绘制这些相关性(AC[1],AC[2],...,AC[k])将产生一个自相关函数(ACF)图。ACF 图用于选择 ARIMA 模型的最佳参数,并评估最终模型的拟合度。

可以使用forecast包中的Acf()函数生成自相关图。其格式为Acf(*ts*),其中ts是原始时间序列。Nile时间序列的自相关图(k = 1 到 18),将在稍后图 15.13 的上半部分提供。

偏自相关Y[t]Y[t−k]之间的相关性,同时消除了两者之间的所有Y值的影响(Y[t][-1],Y[t][-2],...,Y[t-k][+1])。对于多个k值,也可以绘制偏自相关。可以使用forecast包中的Pacf()函数生成 PACF 图。函数调用为Pacf(*ts*),其中ts是要评估的时间序列。PACF 图也用于确定 ARIMA 模型的最合适的参数。图 15.13 的下半部分给出了Nile时间序列的结果。

ARIMA 模型旨在拟合平稳时间序列(或可以变为平稳的时间序列)。在平稳时间序列中,序列的统计属性不会随时间变化。例如,Y[t]的均值和方差是恒定的。此外,任何滞后k的自相关也不会随时间变化。

在进行 ARIMA 模型的拟合之前,可能需要将时间序列的值进行转换以实现常数方差。对数转换在这里通常很有用,正如你在 15.1.3 节中看到的。其他转换,如 8.5.2 节中描述的 Box-Cox 转换,也可能有所帮助。

由于平稳时间序列假定具有恒定的均值,因此它们不能有趋势成分。许多非平稳时间序列可以通过差分变为平稳。在差分中,时间序列Y[t]的每个值被替换为Y[t][-1] – Y[t]。对时间序列进行一次差分可以去除线性趋势。进行第二次差分可以去除二次趋势。第三次差分可以去除三次趋势。很少需要差分超过两次。

你可以使用diff()函数对时间序列进行差分。其格式为diff(*ts*, differences=*d*),其中d表示时间序列ts被差分的次数。默认值为d=1forecast包中的ndiffs()函数可用于帮助确定d的最佳值。其格式为ndiffs(*ts*)

平稳性通常通过时间序列图的可视检查来评估。如果方差不是常数,则对数据进行变换。如果有趋势,则对数据进行差分。您还可以使用称为Augmented Dickey–Fuller (ADF**)测试的统计程序来评估平稳性的假设。在 R 中,tseries包中的adf.test``()函数执行此测试。格式为adf.test(*ts*``),其中ts*是要评估的时间序列。显著的结果表明平稳性。

总结来说,ACF 和 PCF 图用于确定 ARIMA 模型的参数。平稳性是一个重要的假设,变换和差分被用来帮助实现平稳性。掌握这些概念后,我们现在可以转向拟合具有自回归(AR)成分、移动平均(MA)成分或两者成分(ARMA)的模型。最后,我们将检查包含 ARMA 成分和差分以实现平稳性(积分)的 ARIMA 模型。

15.4.2 ARMA 和 ARIMA 模型

在阶数为 p 的自回归模型中,时间序列中的每个值都是通过前 p 个值的线性组合进行预测的

AR(p): Y[t] = µ + β[1]Y[t][−1] + β[2]Y[t][−2] + ... + β[p]**Y[t−p] + ε[t]

其中 Y[t] 是序列的给定值,µ 是序列的均值,β 是权重,ε[t] 是不规则成分。在阶数为 q 的移动平均模型中,时间序列中的每个值都是通过 q 个前误差值的线性组合进行预测的。在这种情况下,

MA(q): Y[t] = µ + θ[1]ε[t][−1] + θ[2]ε[t][−2] + ... + θ[p]**ε[t−q] + ε[t]

其中,ε表示预测误差,θ表示权重。(需要注意的是,这里描述的移动平均并不是第 15.1.2 节中描述的简单移动平均。)

结合两种方法可以得到形式为 ARMA(p, q)的模型

Y[t] = µ + β[1]Y[t][−1] + β[2]Y[t][−2] + ... + β[p]**Y[t−p]θ[1]ε[t][−1] − θ[2]ε[t][−2] − ... − θ[p]**ε[t−q] + ε[t]

从过去 p 个值和 q 个残差预测时间序列的每个值。

ARIMA(p, d, q)模型是一种时间序列经过 d 次差分后的模型,预测值由前 p 个实际值和 q 个前误差值得到。预测值经过非差分积分处理以得到最终预测。

ARIMA 建模的步骤如下:

  1. 确保时间序列是平稳的。

  2. 确定一个合理的模型或多个模型(p 和 q 的可能值)。

  3. 拟合模型。

  4. 评估模型的拟合度,包括统计假设和预测准确性。

  5. 进行预测。

让我们依次应用每个步骤来拟合一个 ARIMA 模型到Nile时间序列。

确保时间序列是平稳的

首先,您绘制时间序列并评估其平稳性(参见列表 15.7 和图 15.13 的上半部分)。方差似乎在观察的年份中保持稳定,因此不需要转换。可能存在趋势,这得到了ndiffs()函数结果的支持。

列表 15.9 转换时间序列并评估平稳性

> library(forecast)
> library(tseries)
> autoplot(Nile)
> ndiffs(Nile)

[1] 1

> dNile <- diff(Nile)                                              
> autoplot(dNile)
> adf.test(dNile)

    Augmented Dickey-Fuller Test

data:  dNile 
Dickey-Fuller = -6.5924, Lag order = 4, p-value = 0.01
alternative hypothesis: stationary 

图 15.13 显示了从 1871 年到 1970 年阿斯旺尼罗河年流量的时间序列(顶部)以及一次差分后的时间序列(底部)。差分消除了原始图中明显的下降趋势。

时间序列进行了一次差分(滞后=1 是默认值)并保存为dNile。差分后的时间序列在图 15.13 的下半部分绘制,看起来更平稳。对差分序列应用 ADF 测试表明它现在是平稳的,因此您可以继续下一步。

确定一个或多个合理模型

根据 ACF 和 PACF 图选择可能的模型:

autoplot(Acf(dNile))
autoplot(Pacf(dNile))

图 15.14 显示了结果图。

图 15.14 差分后的尼罗河时间序列的自相关和偏自相关图

目标是确定参数 p、d 和 q。您已经从上一节中知道 d = 1。您通过比较 ACF 和 PACF 图与表 15.5 中给出的指南来获取 p 和 q。

表 15.5 选择 ARIMA 模型的指南

模型 ACF PACF
ARIMA(p, d, 0) 逐渐衰减至零 滞后 p 后的零值
ARIMA(0, d, q) 滞后 q 后的零值 逐渐衰减至零
ARIMA(p, d, q) 逐渐衰减至零 逐渐衰减至零

表 15.5 中的结果是理论上的,实际的 ACF 和 PACF 可能不会完全匹配,但它们可以用来给出尝试的合理模型的粗略指南。对于图 15.13 中的“尼罗河”时间序列,似乎在滞后 1 处有一个大的自相关,并且随着滞后时间的增加,偏自相关逐渐衰减至零。这表明尝试 ARIMA(0, 1, 1)模型。

拟合模型

使用Arima()函数拟合 ARIMA 模型。格式为Arima(*ts*, order=c(q, d, q))。以下列表给出了将 ARIMA(0, 1, 1)模型拟合到尼罗河时间序列的结果。

列表 15.10 拟合 ARIMA 模型

> library(forecast)
> fit <- arima(Nile, order=c(0,1,1))                                 
> fit

Series: Nile 
ARIMA(0,1,1)                    

Coefficients:
          ma1
      -0.7329
s.e.   0.1143

sigma² estimated as 20600:  log likelihood=-632.55
AIC=1269.09   AICc=1269.22   BIC=1274.28

> accuracy(fit)

                 ME  RMSE   MAE    MPE  MAPE   MASE
Training set -11.94 142.8 112.2 -3.575 12.94 0.8089

注意,您将模型应用于原始时间序列。通过指定 d = 1,它为您计算一阶差分。移动平均系数(–0.73)与 AIC 一起提供。如果您拟合其他模型,AIC 可以帮助您选择哪个模型最合理。较小的 AIC 值表示更好的模型。准确度指标可以帮助您确定模型是否具有足够的准确性。这里河流水平的平均绝对百分比误差为 13%。

评估模型拟合

如果模型是合适的,残差应该是均值为零的正态分布,并且对于每个可能的滞后,自相关系数都应该为零。换句话说,残差应该是正态且独立分布的(它们之间没有关系)。可以使用以下列表中的代码评估这些假设。

列表 15.11 评估模型拟合

> library(ggplot2)
> df <- data.frame(resid = as.numeric(fit$residuals))   ❶
> ggplot(df, aes(sample = resid)) +                     ❷
      stat_qq() + stat_qq_line() +
      labs(title="Normal Q-Q Plot")     

> Box.test(fit$residuals, type="Ljung-Box")             ❸
    Box-Ljung test

data:  fit$residuals 
X-squared = 1.3711, df = 1, p-value = 0.2416

❶ 提取残差

❷ 创建 Q-Q 图

❸ 测试所有滞后阶数的自相关系数为零

首先,从 fit 对象中提取残差并保存到数据框中。然后使用 qq_* 函数生成 Q-Q 图(图 15.15)。正态分布的数据应该沿着这条线分布。在这种情况下,结果看起来很好。

图 15.15:用于确定时间序列残差的正态性的正态 Q-Q 图。期望正态分布的值会沿着这条线分布。

Box.test() 函数提供了一个测试,表明所有自相关系数都为零。结果并不显著,表明自相关系数与零没有差异。这个 ARIMA 模型似乎很好地拟合了数据。

进行预测

如果模型没有满足正态残差和零自相关的假设,那么就需要修改模型、添加参数或尝试不同的方法。一旦选择了最终模型,就可以用它来预测未来的值。在下一个列表中,使用 forecast 包中的 forecast() 函数来预测三年后的值。

列表 15.12 使用 ARIMA 模型进行预测

> forecast(fit, 3)

     Point Forecast    Lo 80     Hi 80    Lo 95    Hi 95
1971       798.3673 614.4307  982.3040 517.0605 1079.674
1972       798.3673 607.9845  988.7502 507.2019 1089.533
1973       798.3673 601.7495  994.9851 497.6663 1099.068

> autoplot(forecast(fit, 3)) + labs(x="Year", y="Annual Flow")

autoplot() 函数用于绘制图 15.16 中的预测结果。点估计由黑色线条表示,而 80% 和 95% 的置信区间分别由深蓝色和浅蓝色带表示。

图 15.16:从拟合的 ARIMA(0,1,1) 模型对 尼罗河 时间序列的三年预测。黑色线条代表点估计,浅蓝色和深蓝色带分别代表 80% 和 95% 的置信区间界限。

15.4.3 自动 ARIMA 预测

在 15.2.3 节中,你使用了 forecast 包中的 ets() 函数来自动选择最佳指数模型。该包还提供了一个 auto.arima() 函数来选择最佳 ARIMA 模型。下面的列表将此方法应用于章节引言中描述的 sunspots 时间序列。

列表 15.13 自动 ARIMA 预测

> library(forecast)
> fit <- auto.arima(sunspots)
> fit
Series: sunspots 
ARIMA(2,1,2)                    
Coefficients:
       ar1     ar2    ma1    ma2
      1.35  -0.396  -1.77  0.810
s.e.  0.03   0.029   0.02  0.019

sigma² estimated as 243:  log likelihood=-11746
AIC=23501   AICc=23501   BIC=23531

> forecast(fit, 3)

         Point Forecast       Lo 80    Hi 80      Lo 95    Hi 95
Jan 1984      40.437722  20.4412613 60.43418   9.855774 71.01967
Feb 1984      41.352897  18.2795867 64.42621   6.065314 76.64048
Mar 1984      39.796425  15.2537785 64.33907   2.261686 77.33116

> accuracy(fit)
                   ME RMSE   MAE MPE MAPE MASE
Training set -0.02673 15.6 11.03 NaN  Inf 0.32 

该函数选择了一个 p = 2, d = 1, q = 2 的 ARIMA 模型。这些值是在大量可能的模型中使 AIC 准则最小化的值。由于序列中存在零值(这两个统计量的缺点),MPE 和 MAPE 准确度会爆炸。绘制结果和评估拟合留给你作为练习。

预测的注意事项

预测有着悠久而多样的历史,从早期的巫师预测天气到现代数据科学家预测最近选举的结果。预测是科学和人类本性的基础。

虽然这些方法在理解和预测各种现象时可能至关重要,但重要的是要记住,它们每个都涉及外推——超越数据。它们假设未来的条件将反映当前的条件。2007 年做出的金融预测假设 2008 年及以后的经济持续增长。正如我们今天所知道的那样,事情并没有完全按照这种方式发展。重大事件可以改变时间序列的趋势和模式,而且你尝试预测得越远,不确定性就越大。

15.5 深入学习

关于时间序列分析和预测有许多优秀的书籍。预测:原理与实践 (otexts.com/fpp2, 2018) 是由 Rob Hyndman 和 George Athanasopoulos 编写的一本清晰简洁的在线教科书,其中包含了贯穿始终的 R 代码。我强烈推荐它。此外,Cowpertwait 和 Metcalfe (2009) 撰写了一本关于使用 R 分析时间序列的优秀文本。Shumway 和 Stoffer (2010) 提供了一种更高级的处理方法,其中也包括了 R 代码。最后,你可以查阅 CRAN 任务视图关于时间序列分析 (cran.r-project.org/web/views/TimeSeries.html),其中包含了对 R 所有时间序列能力的全面总结。

摘要

  • R 提供了广泛的数据结构来存储时间序列数据。基础 R 提供了用于存储一个 (ts) 或多个 (mts) 观测序列的类。xtszoo 包扩展了这一功能,以包括记录在非规律间隔的观测。

  • 存储为 xts 对象的时间序列数据可以很容易地使用方括号 [] 符号进行子集化,并使用 apply.period 函数进行聚合。

  • forecast 包提供了几个用于可视化探索时间序列数据的函数。autoplot() 函数可以用来将时间序列数据绘制为 ggplot2 图表。ma() 函数可以用来平滑时间序列中的不规则性,以突出趋势。stl() 函数可以用来将时间序列分解为趋势、季节性和不规则(残差)成分。

  • forecast 包还可以用来预测时间序列的未来值。我们介绍了两种流行的预测方法:指数模型和自回归积分移动平均(ARIMA)模型。

16 聚类分析

本章涵盖

  • 识别观察值的紧密子组(聚类)

  • 确定存在的聚类数量

  • 获得聚类的嵌套层次结构

  • 获得离散的聚类

聚类分析是一种数据降维技术,旨在揭示数据集中观察值的子组。它允许您将大量观察值减少到更少的聚类或类型。聚类被定义为比其他组中的观察值更相似的观察值组。这不是一个精确的定义,这一事实导致了大量聚类方法的出现。

聚类分析在生物和行为科学、市场营销和医学研究中得到广泛应用。例如,一位心理学家可能会对抑郁患者的症状和人口统计数据进行聚类,试图揭示抑郁的亚型。希望找到这样的亚型可能会导致更具有针对性和有效的治疗方法,以及对这种疾病的更好理解。市场营销研究人员使用聚类分析作为客户细分策略。客户根据其人口统计数据和购买行为的相似性被安排到不同的群体中。随后,营销活动将针对一个或多个这些子群体进行定制。医学研究人员使用聚类分析来帮助整理从 DNA 微阵列数据中获得的基因表达模式。这有助于他们理解正常的生长和发育以及许多人类疾病的潜在原因。

最受欢迎的两种聚类方法是层次聚类法划分聚类法。在层次聚类法中,每个观察值最初都是一个单独的聚类。然后,每次合并两个聚类,直到所有聚类合并成一个单一的聚类。在划分方法中,您指定K:您要寻找的聚类数量。然后,观察值被随机分成K组,并重新排列以形成紧密的聚类。

在这些广泛的方法中,有许多聚类算法可供选择。对于层次聚类,最受欢迎的是单链接、完全链接、平均链接、质心法和沃德法。对于划分聚类,最受欢迎的是 k-means 和基于中位数划分(PAM)。每种聚类方法都有其优缺点,我们将在后面讨论。

本章中的例子集中在食品和葡萄酒上(我怀疑我的朋友们不会感到惊讶)。层次聚类被应用于flexclust包中包含的nutrient数据集,以回答以下问题:

  • 根据 5 种营养指标,27 种鱼类、家禽和肉类的相似性和差异是什么?

  • 这些食品能否有意义的聚类成更少的组?

将使用分区方法评估 178 个意大利葡萄酒样品的 13 项化学分析。数据包含在rattle包提供的wine数据集中。在这里,问题是

  • 数据中是否存在葡萄酒亚型?

  • 如果存在,那么有多少亚型,它们的特征是什么?

事实上,葡萄酒样品代表三种品种(记录为Type)。这将允许你评估聚类分析恢复潜在结构的效果。

尽管聚类分析有许多方法,但它们通常遵循一系列相似的步骤。这些常见步骤在 16.1 节中描述。层次聚类在 16.3 节中描述,分区方法在 16.4 节中介绍。一些最终建议和注意事项在 16.6 节中提供。要运行本章的示例,请确保安装clusterNbClustflexclustfMultivarggplot2ggdendrofactoextraclusterabilityrattle包。rattle包也将在第十七章中使用。

16.1 聚类分析的常见步骤

与因子分析(第十四章)一样,有效的聚类分析是一个多步骤过程,有多个决策点。每个决策都可能影响结果的质量和有用性。本节描述了全面聚类分析的 11 个典型步骤:

  1. 选择合适的属性。 第一步(也许是最重要的一步)是选择你认为可能对识别和理解数据中观察组之间差异重要的变量。例如,在抑郁症研究中,你可能想评估以下一个或多个:心理症状;身体症状;发病年龄;发作的数量、持续时间和时间;住院次数;自我护理的功能状态;社会和工作历史;当前年龄;性别;种族;社会经济地位;婚姻状况;家族医疗史;以及先前治疗反应。复杂的聚类分析无法弥补变量选择不当的缺点。

  2. 缩放数据。 如果分析中的变量范围不同,范围最大的变量将对结果产生最大影响。这通常是不希望的,因此分析师在继续之前会缩放数据。最流行的方法是将每个变量标准化到均值为 0 和标准差为 1。其他替代方法包括将每个变量除以其最大值或减去变量的均值并除以变量的中位数绝对偏差。以下代码片段展示了这三种方法:

    df1 <- apply(mydata, 2, function(x){(x-mean(x))/sd(x)})
    df2 <- apply(mydata, 2, function(x){x/max(x)})
    df3 <- apply(mydata, 2, function(x){(x – mean(x))/mad(x)})
    

    在本章中,你将使用scale()函数将变量标准化到均值为 0 和标准差为 1。这与第一个代码片段(df1)等效。

  3. 筛选异常值。 许多聚类技术对异常值敏感,这可能会扭曲获得的聚类解决方案。您可以使用outliers包中的函数来筛选(并删除)单变量异常值。mvoutlier包包含可以用来识别多变量异常值的函数。另一种选择是使用对异常值存在具有鲁棒性的聚类方法。基于中位数划分(第 16.4.2 节)是后一种方法的例子。

  4. 计算距离。 尽管聚类算法差异很大,但它们通常需要测量要聚类的实体之间的距离。两个观测值之间最流行的距离度量是欧几里得距离,但曼哈顿、Canberra、非对称二进制、最大和 Minkowski 距离度量也是可用的(有关详细信息,请参阅?dist)。在本章中,始终使用欧几里得距离。计算欧几里得距离在第 16.2 节中进行了介绍。

  5. 选择聚类算法。 接下来,您选择一种聚类数据的方法。层次聚类适用于较小的问题(例如,150 个观测值或更少)以及当需要嵌套分组层次结构时。划分方法可以处理更大的问题,但需要提前指定聚类数量。

    一旦您选择了层次或划分方法,您必须选择一个特定的聚类算法。同样,每个算法都有其优点和缺点。第 16.3 节和第 16.4 节描述了最流行的算法。您可能希望尝试多个算法,以查看结果对方法选择的鲁棒性。

  6. 获得一个或多个聚类解决方案。 此步骤使用步骤 5 中选择的方 法。

  7. 确定存在的聚类数量。 为了获得最终的聚类解决方案,您必须决定数据中存在多少个聚类。这是一个棘手的问题,已经提出了许多方法。通常涉及提取各种数量的聚类(例如,2 到K),并比较解决方案的质量。NbClust包中的NbClust()函数提供了 26 个不同的指标,以帮助您做出这个决定(巧妙地展示了这个问题是如何悬而未决的)。NbClust在本章中得到了广泛应用。

  8. 获得最终的聚类解决方案。 一旦确定了聚类数量,就会执行最终的聚类以提取该数量的子组。

  9. 可视化结果。 可视化可以帮助您确定聚类解决方案的意义和有用性。层次聚类的结果通常以树状图的形式呈现。划分结果通常使用双变量聚类图进行可视化。

  10. 解释聚类。 一旦获得聚类解决方案,就必须解释(并可能命名)聚类。聚类中的观测值有什么共同之处?它们如何与其他聚类中的观测值不同?这一步骤通常通过为每个变量按聚类获得汇总统计来实现。对于连续数据,计算每个聚类中每个变量的均值或中位数。对于混合数据(包含分类变量的数据),汇总统计还将包括众数或类别分布。

  11. 验证结果。 验证聚类解决方案涉及提出问题:“这些分组在某种意义上是真实的,而不是这个数据集或统计技术的独特方面的表现吗?”如果使用不同的聚类方法或不同的样本,是否会得到相同的聚类?fpcclvclValid每个包都包含用于评估聚类解决方案稳定性的函数。

由于观测值之间的距离计算是聚类分析的一个基本组成部分,因此下面将详细描述。

16.2 计算距离

每个聚类分析都以计算每个要聚类的实体之间的距离、相似度或邻近度开始。两个观测值之间的欧几里得距离由以下公式给出

其中 ij 是观测值,P 是变量的数量。换句话说,两个观测值之间的欧几里得距离是每个变量上平方差的和的平方根。

考虑flexclust包提供的nutrient数据集。该数据集包含 27 种肉类、鱼类和家禽的营养成分测量值。前几个观测值如下所示

> data(nutrient, package="flexclust")
> head(nutrient, 4)

             energy protein fat calcium iron
BEEF BRAISED    340      20  28       9  2.6
HAMBURGER       245      21  17       9  2.7
BEEF ROAST      420      15  39       7  2.0
BEEF STEAK      375      19  32       9  2.6

和前两个(红烧牛肉和汉堡)之间的欧几里得距离是

基础 R 安装中的dist()函数可用于计算矩阵或数据框中所有行(观测值)之间的距离。格式为dist(``x``,method=),其中x是输入数据,默认method="euclidean"。该函数默认返回一个下三角矩阵,但可以使用as.matrix()函数使用标准括号符号访问距离。对于nutrient数据框,

> d <- dist(nutrient)
> as.matrix(d)[1:4,1:4]

             BEEF BRAISED HAMBURGER BEEF ROAST BEEF STEAK
BEEF BRAISED          0.0      95.6       80.9       35.2
HAMBURGER            95.6       0.0      176.5      130.9
BEEF ROAST           80.9     176.5        0.0       45.8
BEEF STEAK           35.2     130.9       45.8        0.0

较大的距离表示观测值之间的差异较大。一个观测值与自身的距离是 0。正如预期的那样,dist()函数提供了红烧牛肉和汉堡之间的相同距离,与手工计算相同。

混合数据类型的聚类分析

欧几里得距离通常是连续数据的距离度量选择。但如果存在其他变量类型,则需要其他相似性度量。您可以使用 cluster 包中的 daisy() 函数获取具有任何组合的二进制、名义、有序和连续属性的观测值之间的相似性矩阵。cluster 包中的其他函数可以使用这些相似性进行聚类分析。例如,agnes() 提供了聚合层次聚类,而 pam() 提供了基于中位数划分。

注意,nutrient 数据框中的距离在很大程度上受 energy 变量的贡献所主导,该变量的范围要大得多。缩放数据将有助于平衡每个变量的影响。在下一节中,您将应用层次聚类分析到此数据集。

16.3 层次聚类分析

如所述,在聚合层次聚类中,每个案例或观测值最初都是一个单独的簇。然后每次合并两个簇,直到所有簇合并成一个包含所有观测值的单个簇。算法如下:

  1. 将每个观测值(行、案例)定义为簇。

  2. 计算每个簇与每个其他簇之间的距离。

  3. 将具有最小距离的两个簇合并。这减少了簇的数量一个。

  4. 重复步骤 2 和 3,直到所有簇都合并成一个包含所有观测值的单个簇。

层次聚类算法之间的主要区别在于它们对簇距离的定义(步骤 2)。表 16.1 列出了五种最常见的层次聚类方法及其定义的两个簇之间的距离。

表 16.1 层次聚类方法

簇方法 定义两个簇之间的距离
单链接 一个簇中的点与另一个簇中的点的最短距离
完全链接 一个簇中的点与另一个簇中的点的最长距离
平均链接 一个簇中的每个点与另一个簇中的每个点的平均距离(也称为 UPGMA [未加权配对组平均])
质心 两个簇的质心(变量均值向量)之间的距离。对于单个观测值,质心是变量的值。
瓦德(Ward) 两个簇之间所有变量的方差分析平方和的总和

单链聚类倾向于找到细长的、雪茄形的聚类。它也常见到一种称为链式的现象——不相似的观测值被合并到同一个聚类中,因为它们与它们之间的中间观测值相似。完全链聚类倾向于找到直径大致相等的紧凑聚类。它也可能对异常值敏感。平均链聚类在这两者之间提供了一个折中方案。它不太可能形成链式结构,并且对异常值不太敏感。它还倾向于将具有小方差差异的聚类合并在一起。

沃德方法倾向于将具有少量观测值的聚类合并在一起,并且倾向于产生观测值数量大致相等的聚类。它也可能对异常值敏感。由于其对聚类距离定义简单且易于理解,中心点方法提供了一个有吸引力的替代方案。它也比其他层次聚类方法对异常值不太敏感。但它可能不如平均链或沃德方法表现得好。

可以使用hclust()函数实现层次聚类。其格式为hclust(*d*, method=),其中d是由dist()函数生成的距离矩阵,方法包括"single""complete""average""centroid""ward"

在本节中,你将应用平均链聚类方法对第 16.2 节中引入的nutrient数据进行处理,以识别基于营养信息的 27 种食品类型之间的相似性、差异和分组。以下列表提供了执行聚类的代码。

列表 16.1 平均链聚类营养数据

data(nutrient, package="flexclust")
row.names(nutrient) <- tolower(row.names(nutrient))
nutrient.scaled <- scale(nutrient)                                  

d <- dist(nutrient.scaled)                                          

fit.average <- hclust(d, method="average")

library(ggplot2)
library(ggdendro)                          
ggdedgrogram(fit.average) + labs(title="Average Linkage Clustering")

首先,导入数据,并将行名设置为小写(因为我讨厌大写标签)。由于变量范围差异很大,它们被标准化为均值为 0 和标准差为 1。计算 27 种食品类型之间的欧几里得距离,并执行平均链聚类。最后,使用ggplot2ggdendro包将结果绘制成树状图(见图 16.1)。

图片

图 16.1 营养数据的平均链聚类

树状图显示了项目如何组合成聚类,并且是从下往上读取的。每个观测值最初都是它自己的聚类。然后,最接近的两个观测值(红烧牛肉和熏火腿)被合并。接下来,烤猪肉和炖猪肉被合并,然后是罐装鸡肉和罐装金枪鱼。在第四步中,红烧牛肉/熏火腿聚类和烤猪肉/炖猪肉聚类被合并(现在该聚类包含四个食品项目)。这个过程一直持续到所有观测值都被合并成一个单一的聚类。高度维度表示聚类合并的准则值。对于平均链聚类,这个准则值是每个聚类中每个点与其他聚类中每个点之间的平均距离。

如果你的目标是了解食物类型在营养方面的相似性或差异性,那么图 16.1 可能就足够了。它创建了 27 个项目中相似性/差异性的层次视图。罐装金枪鱼和鸡肉相似,并且两者与罐装蛤蜊都大相径庭。但如果最终目标是将这些食物分配到更少的(希望是有意义的)组中,则需要额外的分析来选择合适的簇数。

NbClust 包提供了许多指标来确定聚类分析中最佳簇数。它们之间并不保证会达成一致。事实上,它们可能不会。但可以使用这些结果作为选择可能的候选值 K(簇数)的指南。NbClust() 函数的输入包括要聚类的矩阵或数据框、要使用的距离度量以及聚类方法,以及要考虑的最小和最大簇数。它返回每个聚类指标以及每个指标提出的最佳簇数。下面的列表将此方法应用于营养数据的平均链聚类。

列表 16.2 选择簇数

> library(NbClust)
> library(factoextra)
> nc <- NbClust(nutrient.scaled, distance="euclidean", 
                min.nc=2, max.nc=15, method="average")
> fviz_nbclust(nc)

在这里,有两个标准有利于零个簇,一个标准有利于一个簇,四个标准有利于两个簇,依此类推。结果使用 fviz_nbclust() 函数绘制(图 16.2)。拥有最多投票数的簇数被认为是最佳簇数。在出现平局的情况下,通常会选择簇数较少的解决方案。

图片

图 16.2 使用 NbClust 包提供的 26 个标准推荐的簇数

虽然图中建议有两个簇,但你也可以尝试 3 个、5 个和 15 个簇的解决方案,并选择最具解释意义的那个。下面的列表探讨了 5 个簇的解决方案。

列表 16.3 获取最终的簇解决方案

> clusters <- cutree(fit.average, k=5)                                  ❶
> table(clusters)

clusters
 1  2  3  4  5 
 7 16  1  2  1  

> nutrient.scaled$clusters <- clusters

> library(dplyr)
> profiles <- nutrient.scaled %>%                                        ❷
     group_by(clusters) %>%                                              ❷
     summarize_all(median)                                               ❷

   > profiles %>% round(3) %>% data.frame()  

  cluster energy protein    fat calcium    iron
1       1  1.310   0.000  1.379  -0.448  0.0811
2       2 -0.370   0.235 -0.487  -0.397 -0.6374
3       3 -0.468   1.646 -0.753  -0.384  2.4078
4       4 -1.481  -2.352 -1.109   0.436  2.2709
5       5 -0.271   0.706 -0.398   4.140  0.0811

> library(colorhcplot)                                                   ❸
> cl <-factor(clusters, levels=c(1:5),                                   ❸
              labels=paste("cluster", 1:5))                              ❸
> colorhcplot(fit.average, cl, hang=-1, lab.cex=.8, lwd=2,               ❸
              main="Average-Linkage Clustering\n5 Cluster Solution")     ❸

❶ 分配案例

❷ 描述簇

❸ 绘制结果

使用 cutree() 函数将树切割成 5 个簇 ❶。第一个簇有 7 个观测值,第二个簇有 16 个观测值,依此类推。然后使用 dplyr 函数为每个簇获取中位数轮廓 ❷。最后,重新绘制树状图,并使用 colorhcplot 函数来识别 5 个簇 ❸。在这里,cl 是一个带有簇标签的因子,hang=-1 将标签对齐在图的底部,lab.cex 控制标签的大小(这里为默认值的 80%),lwd 控制树状图线的宽度。图 16.3 显示了结果。如果你使用的是文本的打印版,请确保运行此代码。在灰度图中,颜色区分难以辨认。

图片

图 16.3 使用 5 簇解决方案的营养数据平均链聚类

鲨鱼形成自己的簇,其钙含量比其他食物组高得多。牛肉心也是一个单例,富含蛋白质和铁。蛤蜊簇蛋白质含量低,铁含量高。包含烤牛肉和炖猪肉的簇中的项目能量和脂肪含量高。最后,最大的组(鲭鱼到蓝鱼)铁含量相对较低。

当你预期嵌套聚类和有意义层次结构时,层次聚类特别有用。这在生物科学中通常是这种情况。但是,层次算法在贪婪的意义上,一旦一个观测值被分配到某个簇,它就不能在以后重新分配。此外,层次聚类在可能包括数百甚至数千个观测值的大样本中难以应用。分区方法在这些情况下可以很好地工作。

16.4 分区聚类分析

在分区方法中,观测值被分为K组,并重新排列以形成根据给定标准可能的最紧密的簇。本节考虑两种方法:k-means 和基于中位数(PAM)的分区。

16.4.1 K-means 聚类

最常见的分区方法是 k-means 聚类分析。从概念上讲,k-means 算法如下:

  1. 选择K个质心(随机选择K行)。

  2. 将每个数据点分配到其最近的质心。

  3. 将质心重新计算为簇中所有数据点的平均值(即,质心是p-长度均值向量,其中p是变量的数量)。

  4. 将数据点分配到其最近的质心。

  5. 继续执行步骤 3 和 4,直到观测值不再重新分配或达到最大迭代次数(R 默认为 10)。

此方法的实现细节可能有所不同。

R 使用 Hartigan 和 Wong(1979 年)的高效算法,将观测值划分为k组,使得观测值到其分配的簇中心的平方和最小。这意味着在步骤 2 和 4 中,每个观测值被分配到具有最小值的簇中,

其中k是簇,x[ij]是第i个观测值的第j个变量的值,x̄[kj]是第k个簇的第j个变量的均值,p是变量的数量。

K-means 聚类方法可以处理比层次聚类方法更大的数据集。此外,观测值不会被永久性地分配到某个簇中——当这样做能改善整体解决方案时,它们会被移动。但是,使用均值意味着所有变量都必须是连续的,并且这种方法可能会受到异常值的影响。它还在存在非凸(例如,U 形)簇的情况下表现不佳。

R 中 k-means 函数的格式为kmeans(*x, centers*),其中x是数值数据集(矩阵或数据框),而centers是要提取的簇的数量。该函数返回簇成员资格、质心、平方和(组内、组间、总平方和)以及簇大小。

由于 k-means 聚类分析从随机选择的 k 个质心开始,每次调用函数时都可能得到不同的解决方案。使用set.seed()函数以确保结果可重复。此外,这种聚类方法可能对质心的初始选择敏感。kmeans()函数有一个nstart选项,它尝试多个初始配置并报告最佳配置。例如,添加nstart=25生成 25 个初始配置。这种方法通常被推荐。

与层次聚类不同,k-means 聚类要求您提前指定要提取的簇的数量。同样,NbClust包可以用作指南。此外,k-means 解决方案中组内总平方和与簇数量之间的图表可能很有帮助。图表中的弯曲(类似于第 14.2.1 节中描述的 Scree 测试中的弯曲)可以表明合适的簇数量。

该图可以使用以下函数生成:

wssplot <- function(data, nc=15, seed=1234){
  require(ggplot2)
  wss <- numeric(nc)
  for (i in 1:nc){
    set.seed(seed)
    wss[i] <- sum(kmeans(data, centers=i)$withinss)
  }
  results <- data.frame(cluster=1:nc, wss=wss)
  ggplot(results, aes(x=cluster,y=wss)) +
    geom_point(color="steelblue", size=2) +
    geom_line(color="grey") +
    theme_bw() +
    labs(x="Number of Clusters",
         y="Within groups sum of squares")
}

data参数是要分析的数值数据集,nc是考虑的最大簇数,而seed是随机数种子。

让我们将 k-means 聚类应用于包含 178 个意大利葡萄酒样品的 13 个化学测量的数据集。数据最初来自 UCI 机器学习仓库(www.ics.uci.edu/~mlearn/MLRepository.html),但您将通过rattle包在这里访问它们。在此数据集中,观测值代表三种葡萄酒品种,如第一个变量(Type)所示。您将删除此变量,执行聚类分析,并查看您是否可以恢复已知的结构。

列表 16.4 葡萄酒数据的 k-means 聚类

> data(wine, package="rattle")
> library(NbClust)
> library(factoextra)
> head(wine)

  Type Alcohol Malic  Ash Alcalinity Magnesium Phenols Flavanoids
1    1   14.23  1.71 2.43       15.6       127    2.80       3.06
2    1   13.20  1.78 2.14       11.2       100    2.65       2.76
3    1   13.16  2.36 2.67       18.6       101    2.80       3.24
4    1   14.37  1.95 2.50       16.8       113    3.85       3.49
5    1   13.24  2.59 2.87       21.0       118    2.80       2.69
6    1   14.20  1.76 2.45       15.2       112    3.27       3.39
  Nonflavanoids Proanthocyanins Color  Hue Dilution Proline
1          0.28            2.29  5.64 1.04     3.92    1065
2          0.26            1.28  4.38 1.05     3.40    1050
3          0.30            2.81  5.68 1.03     3.17    1185
4          0.24            2.18  7.80 0.86     3.45    1480
5          0.39            1.82  4.32 1.04     2.93     735
6          0.34            1.97  6.75 1.05     2.85    1450

> df <- scale(wine[-1])                                     ❶
> head(df)

  Alcohol Malic   Ash Alcalinity Magnesium Phenols Flavanoids
1    1.51 -0.56  0.23      -1.17      1.91    0.81       1.03
2    0.25 -0.50 -0.83      -2.48      0.02    0.57       0.73
3    0.20  0.02  1.11      -0.27      0.09    0.81       1.21
4    1.69 -0.35  0.49      -0.81      0.93    2.48       1.46
5    0.29  0.23  1.84       0.45      1.28    0.81       0.66
6    1.48 -0.52  0.30      -1.29      0.86    1.56       1.36
  Nonflavanoids Proanthocyanins Color   Hue Dilution Proline
1         -0.66            1.22  0.25  0.36     1.84    1.01
2         -0.82           -0.54 -0.29  0.40     1.11    0.96
3         -0.50            2.13  0.27  0.32     0.79    1.39
4         -0.98            1.03  1.18 -0.43     1.18    2.33
5          0.23            0.40 -0.32  0.36     0.45   -0.04
6         -0.18            0.66  0.73  0.40     0.34    2.23

> wssplot(df)                                                 ❷
> set.seed(1234)                                              ❷
> nc <- NbClust(df, min.nc=2, max.nc=15, method="kmeans")     ❷
> fviz_nbclust(nc)                                            ❷

> set.seed(1234)
> fit.km <- kmeans(df, 3, nstart=25)                          ❸
> fit.km$size

[1] 62 65 51

> fit.km$centers                                               

  Alcohol Malic   Ash Alcalinity Magnesium Phenols Flavanoids Nonflavanoids
1    0.83 -0.30  0.36      -0.61     0.576   0.883      0.975        -0.561
2   -0.92 -0.39 -0.49       0.17    -0.490  -0.076      0.021        -0.033
3    0.16  0.87  0.19       0.52    -0.075  -0.977     -1.212         0.724
  Proanthocyanins Color   Hue Dilution Proline
1           0.579  0.17  0.47     0.78    1.12
2           0.058 -0.90  0.46     0.27   -0.75
3          -0.778  0.94 -1.16    -1.29   -0.41

> aggregate(wine[-1], by=list(cluster=fit.km$cluster), mean)

  cluster Alcohol Malic Ash Alcalinity Magnesium Phenols Flavanoids
1       1      14   1.8 2.4         17       106     2.8        3.0
2       2      12   1.6 2.2         20        88     2.2        2.0
3       3      13   3.3 2.4         21        97     1.6        0.7
  Nonflavanoids Proanthocyanins Color  Hue Dilution Proline
1          0.29             1.9   5.4 1.07      3.2    1072
2          0.35             1.6   2.9 1.04      2.8     495
3          0.47             1.1   7.3 0.67      1.7     620

❶ 标准化数据

❷ 确定簇的数量

❸ 执行 k-means 聚类分析

由于变量范围不同,它们在聚类之前需要进行标准化 ❶。接下来,使用wssplot()NbClust()函数确定簇的数量 ❷。图 16.4 表明,当从一到三个簇移动时,组内平方和有明显的下降。在三个簇之后,这种下降趋势减弱,表明三个簇的解决方案可能适合数据。在图 16.5 中,NbClust包提供的 23 个标准中有 19 个建议三个簇的解决方案。请注意,并非所有 30 个标准都可以用于每个数据集。

图片

图 16.4 绘制组内平方和与提取簇数的关系图。从一到三个簇(之后减少很少)的急剧下降表明了三簇解决方案。

使用kmeans()函数获得最终的簇解决方案,并打印簇中心点❸。因为函数提供的中心点基于标准化数据,所以使用aggregate()函数以及簇成员资格来确定原始度量中每个簇的变量均值。

图像

图 16.5 使用NbClust包提供的 26 个标准推荐簇数

比较簇的最简单方法是通过簇轮廓图。以下列表继续了列表 16.4 中的示例。

列表 16.5 簇轮廓图

library(ggplot2)
library(tidyr)
means <- as.data.frame(fit.km$centers)                                ❶
means$cluster <- 1:nrow(means)                                        ❶

plotdata <- gather(means, key="variable", value="value", -cluster)    ❷

ggplot(plotdata,                                                      ❸
       aes(x=variable,
           y=value,
           fill=variable,
           group=cluster)) +
  geom_bar(stat="identity") +
  geom_hline(yintercept=0) +
  facet_wrap(~cluster) +
  theme_bw() +
  theme(axis.text.x=element_text(angle=90, vjust=0),
        legend.position="none") +
  labs(x="", y="Standardized scores",
       title = "Mean Cluster Profiles")

❶ 准备均值轮廓

❷ 将数据转换为长格式

❸ 将轮廓绘制为分面条形图

首先,我们得到标准化变量上的簇均值并添加一个表示簇成员资格的变量❶。然后,我们将这个宽格式数据框转换为长格式(宽到长格式转换在第 5.5.2 节中描述)❷。最后,我们将轮廓绘制为分面条形图❸。图 16.6 显示了结果。平均簇轮廓有助于您了解使每个簇独特的东西。例如,与簇 2 和簇 4 相比,簇 1 在酒精、酚类、原花青素和脯氨酸上的平均得分较高。

图像

图 16.6 标准化数据中每个簇的均值轮廓。此图表有助于识别每个簇的独特特征。

另一种可视化簇分析结果的方法是双变量簇图。该图通过将每个观测值(酒)的坐标绘制在由 13 个检测变量得出的前两个主成分上创建。 (主成分在第十四章中描述。)每个点的颜色和形状标识其簇成员资格。点标签代表数据中每款酒行的编号。此外,每个簇周围都围绕着可以包含该簇所有点的最小椭圆。

可以使用factoextra包中的fviz_cluster()函数创建双变量簇图:

library(factoextra) 
fviz_cluster(fit.km, data=df)

图 16.7 显示了图表。我们可以看到簇 1 和簇 3 最不相似。酒 4 和酒 19 相似,而酒 4 和酒 171 非常不同。

图像

图 16.7 将 178 种酒分为 3 组的簇图。每种酒都绘制在数据的第一个和第二个主成分上。这些图表可以帮助我们看到酒与酒之间以及簇与簇之间的相似性/差异性。

簇分析通常是一种无监督技术,因为我们没有试图预测的输出变量。然而,在葡萄酒示例中,数据集中实际上有三个葡萄酒品种(Type)。

k-means 聚类如何揭示Type变量中包含的数据的实际结构?Type(葡萄酒品种)和聚类成员资格的交叉表如下所示

> ct.km <- table(wine$Type, fit.km$cluster)
> ct.km   
     1  2  3
  1 59  0  0
  2  3 65  3
  3  0  0 48

您可以使用flexclust包提供的调整后的兰德指数来量化类型和聚类之间的一致性。

> library(flexclust)
> randIndex(ct.km)
[1] 0.897

调整后的兰德指数提供了两个分区之间一致性的度量,考虑了偶然性。它介于-1(无一致性)到 1(完全一致性)之间。葡萄酒品种类型与聚类解决方案之间的一致性为 0.9。不错——我们喝点酒如何?

16.4.2 基于中位数的聚类

由于它基于均值,k-means 聚类方法可能对异常值敏感。基于中位数的聚类(PAM)提供了一种更稳健的解决方案。PAM 不是使用质心(变量均值的向量)来表示每个聚类,而是通过其最具代表性的观测值(称为中位数)来识别每个聚类。与 k-means 使用欧几里得距离不同,PAM 可以基于任何距离度量。因此,它可以适应混合数据类型,并且不仅限于连续变量。

PAM 算法如下:

  1. 随机选择K个观测值(每个称为中位数)。

  2. 计算每个观测值与每个中位数之间的距离/不相似度。

  3. 将每个观测值分配到其最近的中位数。

  4. 计算每个观测值与其中位数之间的距离之和(总成本)。

  5. 选择一个不是中位数的点,并将其与其中位数交换。

  6. 将每个点重新分配到其最近的中位数。

  7. 计算总成本。

  8. 如果这个总成本更小,则保留新点作为中位数。

  9. 重复步骤 5-8,直到中位数不再改变。

在[PAM 方法中潜在数学的好的、工作过的例子可以在en.wikipedia.org/wiki/k-medoids找到(我通常不引用维基百科,但这是一个很好的例子)。

您可以使用cluster包中的pam()函数进行基于中位数聚类。格式为pam(*x*, *k*, metric="euclidean", stand=FALSE),其中x是一个数据矩阵或数据框,k是聚类数量,metric是使用的距离/不相似度度量类型,而stand是一个逻辑值,表示在计算此度量之前是否应对变量进行标准化。在以下列表中,PAM 应用于葡萄酒数据。

列表 16.6 葡萄酒数据的基于中位数的聚类

> library(cluster)
> set.seed(1234)
> fit.pam <- pam(wine[-1], k=3, stand=TRUE)          ❶
> fit.pam$medoids                                    ❷

     Alcohol Malic  Ash Alcalinity Magnesium Phenols Flavanoids
[1,]    13.5  1.81 2.41       20.5       100    2.70       2.98
[2,]    12.2  1.73 2.12       19.0        80    1.65       2.03
[3,]    13.4  3.91 2.48       23.0       102    1.80       0.75
     Nonflavanoids Proanthocyanins Color  Hue Dilution Proline
[1,]          0.26            1.86   5.1 1.04     3.47     920
[2,]          0.37            1.63   3.4 1.00     3.17     510
[3,]          0.43            1.41   7.3 0.70     1.56     750

❶ 标准化聚类数据

❷ 打印中位数

注意,中位数是包含在wine数据集中的实际观测值。在这种情况下,它们是观测值 36、107 和 175,并且它们已被选中以代表三个聚类。

还要注意,在这个例子中,PAM 的表现不如 k-means 好:

> ct.pam <- table(wine$Type, fit.pam$clustering)

     1  2  3
  1 59  0  0
  2 16 53  2
  3  0  1 47

> randIndex(ct.pam)
[1] 0.699

调整后的兰德指数已从 0.9(k-means)降至 0.7。创建聚类轮廓图和双变量聚类图留作练习。

16.5 避免不存在的聚类

在我完成这次讨论之前,需要提醒一点。聚类分析是一种旨在识别数据集中凝聚子群的方法。它在这方面做得非常好。事实上,它太好了,甚至可以在不存在聚类的地方找到聚类。

考虑以下代码:

library(fMultivar)
library(ggplot2)
set.seed(1234)
df <- rnorm2d(1000, rho=.5)
df <- as.data.frame(df)
ggplot(df, aes(x=V1, y=V2)) + 
  geom_point(alpha=.3) + theme_minimal() + 
  labs(title="Bivariate Normal Distribution with rho=0.5")

fMultivar包中的rnorm2d()函数用于从相关系数为 0.5 的双变量正态分布中抽取 1,000 个观测值。图 16.8 显示了生成的图形。显然,这些数据中没有聚类。

图 16.8 双变量正态数据(n = 1000)。这些数据中没有聚类。

然后使用wssplot()NbClust()函数来确定存在的聚类数量:

wssplot(df)
library(NbClust)
library(factoextra)
nc <- NbClust(df, min.nc=2, max.nc=15, method="kmeans")
fviz_nbclust(nc)

图 16.9 和 16.10 展示了结果。

图 16.9 双变量正态数据中组内平方和与 k-means 聚类数量的关系图

图 16.10 NbClust包中根据标准推荐的用于双变量正态数据的聚类数量。建议有两个聚类。

两种方法都表明至少有两个聚类。如果你使用 k-means 进行两聚类分析,

library(ggplot2)
fit <- kmeans(df, 2)
df$cluster <- factor(fit$cluster)
ggplot(data=df, aes(x=V1, y=V2, color=cluster, shape=cluster)) +  
       theme_minimal() +
       geom_point(alpha=.5) + 
       ggtitle("Clustering of Bivariate Normal Data")

你会得到图 16.11 中显示的两个聚类图。

图 16.11 双变量正态数据的 K-means 聚类分析,提取出两个聚类。请注意,聚类是对数据的任意划分。

显然,这种划分是人为的。这里没有真实的聚类。你如何避免这种错误?虽然这不是万无一失的,但我发现两种方法是有帮助的。第一种是clusterability包提供的 DIP 测试:

> library(clusterability)
> clusterabilitytest(df[-3], "dip")

Null Hypothesis: number of modes = 1
Alternative Hypothesis: number of modes > 1
p-value: 0.9655 
Dip statistic: 0.00823

df[-3]从数据中删除了因子变量(聚类成员)。零假设是只有一个聚类(模态)。由于 p > .05,我们不能拒绝这个假设。数据不支持聚类结构。

另一种方法使用由NbClust报告的立方聚类准则(CCC)。CCC 通常有助于揭示不存在结构的情况。代码如下:

CCC = nc$All.index[, 4]
k <- length(CCC)
plotdata <- data.frame(CCC = CCC, k = seq_len(k))
ggplot(plotdata, aes(x=k, y=CCC)) +
  geom_point() + geom_line() +
  theme_minimal() +
  scale_x_continuous(breaks=seq_len(k)) +
  labs(x="Number of Clusters")

生成的图形显示在图 16.12 中。当两个或更多聚类的 CCC 值都是负数且递减时,分布通常是单峰的。

图 16.12 双变量正态数据的立方聚类准则图。它正确地表明不存在聚类。

聚类分析(或你对它的解释)能够找到错误聚类的能力使得聚类分析的验证步骤变得重要。如果你试图识别在某种意义上真实的聚类(而不是一个方便的划分),确保结果稳健且可重复。尝试不同的聚类方法,并使用新的样本重复研究结果。如果相同的聚类始终被恢复,你可以更有信心地相信结果。

16.6 进一步探讨

聚类分析是一个广泛的主题,R 语言目前提供了应用这一方法的最全面的功能之一。要了解更多关于这些功能的信息,请参阅 CRAN 任务视图中的聚类分析与有限混合模型(cran.r-project.org/web/views/Cluster.html)。此外,Tan、Steinbach 和 Kumar(2006)有一本关于数据挖掘技术的优秀书籍,其中包括一个关于聚类分析的清晰章节,您可以免费下载(www-users.cs.umn.edu/~kumar/dmbook/ch8.pdf)。最后,Everitt、Landau、Leese 和 Stahl(2011)编写了一本关于这个主题的实用且备受推崇的教科书。

摘要

  • 聚类分析是将观测值排列成紧密群体的常见方法。

  • 由于我们对“聚类”或“聚类之间的距离”没有统一的定义,因此已经开发了许多聚类方法。

  • 聚类分析中最受欢迎的两个类别是层次聚类和划分聚类。每个类别中都有许多聚类方法。没有一种方法在所有情况下都是最好的。

  • 在确定数据集中聚类数量时,也没有一种最佳方法。尝试几种不同的方法并选择最有意义或最实用的方法可能是有价值的。

  • 聚类分析可以揭示是否存在聚类!如果您的目标是将数据划分为方便的、紧密的群体(例如,客户细分),这可能就足够了。然而,如果您正在寻求揭示具有理论意义的自然发生的群体(例如,基于症状和病史的抑郁症亚型),那么通过用新数据重复验证您的发现是很重要的。

17 分类

本章涵盖

  • 使用决策树进行分类

  • 构建随机森林分类器

  • 创建支持向量机

  • 评估分类精度

  • 理解复杂模型

数据分析师经常需要从一组预测变量中预测一个分类结果。一些例子包括

  • 根据个人的人口统计和财务历史预测个人是否会偿还贷款

  • 根据患者的症状和生命体征判断一个 ER 患者是否正在经历心脏病发作

  • 根据关键词、图像、超文本、标题信息和来源判断一封电子邮件是否为垃圾邮件

这些案例中的每一个都涉及从一组预测变量(也称为特征)中预测一个二元分类结果(良好信用风险/不良信用风险;心脏病发作/无心脏病发作;垃圾邮件/非垃圾邮件)。目标是找到一种准确的方法将新案例分类到两个组之一。

监督机器学习领域提供了许多用于预测分类结果的分类方法,包括逻辑回归、决策树、随机森林、支持向量机和人工神经网络。前四种在本章中讨论。人工神经网络超出了本书的范围。参见 Ciaburro 和 Venkateswaran(2017)以及 Chollet 和 Allaire(2018)以了解更多相关信息。

监督学习从包含预测变量和结果值的观察值集合开始。然后,数据集被分为训练样本和测试样本。使用训练样本中的数据开发一个预测模型,并使用测试样本中的数据测试其准确性。需要这两个样本,因为分类技术最大化给定数据集的预测。如果使用生成模型的数据来评估其有效性,估计将过于乐观。通过将训练样本上开发的分类规则应用于单独的测试样本,可以获得更现实的准确性估计。一旦创建了有效的预测模型,就可以使用它来预测只有预测变量已知的情况下的结果。

在本章中,您将使用rpartrattlepartykit包来创建和可视化决策树;使用randomForest包来拟合随机森林;以及使用e1071包来构建支持向量机。逻辑回归将通过基础 R 安装中的glm()函数进行拟合。在开始之前,请确保安装必要的包:

pkgs <- c("rpart", "rattle", "partykit", 
          "randomForest", "e1071")
install.packages(pkgs, depend=TRUE)

本章中使用的首要示例来自威斯康星州乳腺癌数据,该数据最初发布在 UCI 机器学习库中。目标是开发一个模型,从细针穿刺组织抽吸(从皮肤下肿块或团块中用细空心针取出的组织样本)的特征来预测患者是否患有乳腺癌。

17.1 准备数据

威斯康星州乳腺癌数据集作为逗号分隔的文本文件可在 UCI 机器学习服务器上获得(archive.ics.uci.edu/ml)。该数据集包含 699 个细针穿刺样本,其中 458 个(65.5%)为良性,241 个(34.5%)为恶性。数据集包含 11 个变量,文件中不包含变量名。16 个样本有缺失数据,并在文本文件中以问号(?)编码。

变量如下:

  • ID

  • 块状厚度

  • 细胞大小均匀性

  • 细胞形状均匀性

  • 边缘粘附

  • 单个上皮细胞大小

  • 无核裸露

  • 稀疏染色质

  • 正常核仁

  • 有丝分裂

  • 类别

第一个变量是一个 ID 变量(你将删除它),最后一个变量(类别)包含结果(编码为2=良性4=恶性)。你还将排除包含缺失值的观测值。

对于每个样本,记录了之前发现与恶性相关的九个细胞学特征。这些变量中的每一个都从 1(最接近良性)评分到 10(最不典型)。但没有任何一个预测因子可以单独区分良性和恶性样本。挑战在于找到一组分类规则,可以用来从这些九个细胞特征的某些组合中准确预测恶性。有关详细信息,请参阅 Mangasarian 和 Wolberg(1990)。

在以下列表中,包含数据的逗号分隔的文本文件从 UCI 存储库下载,并随机分为训练样本(70%)和测试样本(30%)。

列表 17.1 准备乳腺癌数据

loc <- "http://archive.ics.uci.edu/ml/machine-learning-databases"
ds  <- "breast-cancer-wisconsin/breast-cancer-wisconsin.data"
url <- paste(loc, ds, sep="/")

breast <- read.table(url, sep=",", header=FALSE, na.strings="?")
names(breast) <- c("ID", "clumpThickness", "sizeUniformity",
                   "shapeUniformity", "maginalAdhesion", 
                   "singleEpithelialCellSize", "bareNuclei", 
                   "blandChromatin", "normalNucleoli", "mitosis", "class")

df <- breast[-1]
df$class <- factor(df$class, levels=c(2,4), 
                   labels=c("benign", "malignant"))
df <- na.omit(df)

set.seed(1234)
index <- sample(nrow(df), 0.7*nrow(df))
train <- df[index,]
test <- df[-index,]
table(train$class)
table(test$class)

训练样本有 478 个案例(302 个良性,176 个恶性),测试样本有 205 个案例(142 个良性,63 个恶性)。

训练样本将用于创建分类方案,使用逻辑回归、决策树、条件决策树、随机森林和支撑向量机。测试样本将用于评估这些方案的有效性。通过在整个章节中使用相同的示例,你可以比较每种方法的结果。

17.2 逻辑回归

逻辑回归是一种广义线性模型,常用于从一组数值变量中预测二元结果(有关详细信息,请参阅第 13.2 节)。在基础 R 安装中使用的glm()函数用于拟合模型。分类预测因子(因子)会自动替换为一组虚拟编码变量。威斯康星州乳腺癌数据中的所有预测因子都是数值的,因此不需要虚拟编码。下一个列表提供了数据集的逻辑回归分析。

列表 17.2 使用glm()进行逻辑回归

> fit.logit <- glm(class~., data=train, family=binomial())   ❶
> summary(fit.logit)                                         ❷

Call:
glm(formula = class ~ ., family = binomial(), data = train)

Deviance Residuals: 
    Min       1Q   Median       3Q      Max  
-3.6141  -0.1204  -0.0744   0.0236   2.1845  

Coefficients:
                         Estimate Std. Error z value Pr(>|z|)    
(Intercept)              -9.68650    1.29722  -7.467 8.20e-14 ***
clumpThickness            0.48002    0.15244   3.149  0.00164 ** 
sizeUniformity            0.05643    0.29272   0.193  0.84714    
shapeUniformity           0.13180    0.31643   0.417  0.67703    
maginalAdhesion           0.40721    0.14038   2.901  0.00372 ** 
singleEpithelialCellSize -0.03274    0.18095  -0.181  0.85643    
bareNuclei                0.44744    0.11176   4.004 6.24e-05 ***
blandChromatin            0.48257    0.19220   2.511  0.01205 *  
normalNucleoli            0.23550    0.12903   1.825  0.06798 .  
mitosis                   0.66184    0.28785   2.299  0.02149 *  
---
Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1

> prob <- predict(fit.logit, test, type="response")           ❸
> logit.pred <- factor(prob > .5, levels=c(FALSE, TRUE),      ❸
                       labels=c("benign", "malignant"))       ❸
> logit.perf <- table(test$class, logit.pred,                 ❹
                      dnn=c("Actual", "Predicted"))           ❹
> logit.perf

           Predicted
Actual      benign malignant
  benign       140         2
  malignant      3        60

❶ 拟合逻辑回归

❷ 检验模型

❸ 对新案例进行分类

❹ 评估预测准确性

首先,使用class作为因变量,其余变量作为预测变量,拟合一个逻辑回归模型 ❶。该模型基于训练数据框中的案例。接下来显示模型的系数 ❷。第 13.2 节提供了解释逻辑模型系数的指南。

接下来,使用训练数据集上开发的预测方程对测试数据集中的案例进行分类。默认情况下,predict()函数预测恶性结果的 log odds。通过使用type="response"选项,返回获得恶性分类的概率而不是 log odds。在下一条线中,概率大于 0.5 的案例被分类为恶性组,而概率小于或等于 0.5 的案例被分类为良性。

最后,打印出实际状态和预测状态(称为混淆矩阵)的交叉表 ❹。它显示 140 个良性案例被分类为良性,60 个恶性案例被分类为恶性。

在测试样本中,正确分类的案例总数(也称为准确率)为(140 + 60)/ 205 或 98%。在 17.6 节中更详细地讨论了评估分类方案准确性的统计方法。

在继续之前,请注意,有三个预测变量(sizeUniformityshapeUniformity 和singleEpithelialCellSize)的系数在 p < .10 水平上与零没有差异。对于具有非显著系数的预测变量,你将如何处理?如果有的话。

在预测上下文中,通常从最终模型中删除此类变量是有用的。当大量非信息性预测变量向系统中添加噪声时,这一点尤为重要。

在这种情况下,可以使用逐步逻辑回归生成一个变量更少的较小模型。通过添加或删除预测变量以获得具有较小 AIC 值的模型。在当前上下文中,你可以使用

logit.fit.reduced <- step(fit.logit)

以获得一个更简约的模型。简化后的模型排除了之前提到的三个变量。当用于预测测试数据集中的结果时,这个简化模型表现同样出色。试一试。

我们接下来要考虑的方法涉及决策树或分类树的创建。

17.3 决策树

决策树在数据挖掘领域很受欢迎。它们涉及在预测变量上创建一系列二元分割,以创建一个可以用于将新观测值分类为两组之一的树。在本节中,我们将探讨两种类型的决策树:经典树和条件推断树。

17.3.1 经典决策树

构建经典决策树的过程从二元结果变量(在本例中为良性/恶性)和一组预测变量(九项细胞学测量)开始。算法如下:

  1. 选择将数据分为两组的最佳预测变量,使得两组的输出纯度(同质性)最大化(即在一个组中尽可能多的良性病例和在另一个组中的恶性病例)。如果预测变量是连续的,选择一个分割点,以最大化两组的纯度。如果预测变量是分类的(在本例中不适用),将类别组合起来,以获得具有最大纯度的两组。

  2. 将数据分为这两个组,并对每个子组继续进行这个过程。

  3. 重复步骤 1 和 2,直到子组包含少于最小观察数或没有分割可以降低超过指定阈值的纯度。

    最终集合中的子组被称为终端节点。每个终端节点根据该节点样本中输出最频繁的值被分类为输出的一类或另一类。

  4. 要对案例进行分类,将其沿树向下运行到终端节点,并分配在步骤 3 中分配的模态输出值。

不幸的是,这个过程往往会产生一个过大且过度拟合的树。因此,新的案例分类效果不佳。为了补偿,你可以通过选择具有最低 10 折交叉验证预测误差的树来修剪树。然后,这个修剪后的树将用于未来的预测。

在 R 中,可以使用rpart包中的rpart()prune()函数来生长和修剪决策树。以下列表创建了一个用于将细胞数据分类为良性或恶性的决策树。

列表 17.3 使用rpart()创建经典决策树

> library(rpart)
> dtree <- rpart(class ~ ., data=train, method="class",        ❶
                 parms=list(split="information"))              ❶
> dtree$cptable

          CP nsplit  rel error    xerror       xstd
1 0.79545455      0 1.00000000 1.0000000 0.05991467
2 0.07954545      1 0.20454545 0.3068182 0.03932359
3 0.01704545      2 0.12500000 0.1590909 0.02917149
4 0.01000000      5 0.07386364 0.1704545 0.03012819

> plotcp(dtree)

> dtree.pruned <- prune(dtree, cp=.01705)                      ❷

> library(rattle)
> fancyRpartPlot(dtree.pruned,  sub="Classification Tree")

> dtree.pred <- predict(dtree.pruned, test, type="class")      ❸
> dtree.perf <- table(test$class, dtree.pred, 
                      dnn=c("Actual", "Predicted"))
> dtree.perf
           Predicted
Actual      benign malignant
  benign       136         6
  malignant      3        60

❶ 生长树

❷ 修剪树

❸ 对新案例进行分类

首先,使用rpart()函数❶生长树。你可以使用print(dtree)summary(dtree)来检查拟合的模型(此处未显示)。树可能太大,需要修剪。

要选择最终的树大小,检查由rpart()返回的列表中的cptable组件。它包含有关各种树大小的预测误差的数据。复杂性参数(cp)用于惩罚较大的树。树的大小由分支分割的数量(nsplit)定义。具有 n 个分割的树有 n + 1 个终端节点。rel误差列包含给定大小的树在训练样本中的误差率。交叉验证误差(xerror)基于 10 折交叉验证(也使用训练样本)。xstd列包含交叉验证误差的标准误差。

plotcp()函数绘制交叉验证误差与复杂性参数(见图 17.1)的关系。对于最终树大小的一个好选择是具有在最小交叉验证误差值一个标准误差范围内的交叉验证误差的最小树。

图 17.1 复杂度参数与交叉验证误差的关系。虚线是标准差规则的上限(0.16 + 0.03 = 0.19)。该图表明应选择线下最左侧的 cp 值对应的树。

最小交叉验证误差为 0.16,标准误差为 0.03。在这种情况下,选择交叉验证误差在 0.16 ± 0.03(即介于 0.13 和 0.19 之间)的最小树。查看列表 17.3 中的 cptable 表,具有两个分割(交叉验证误差 = 0.16)的树符合这一要求。等效地,您可以选择图 17.1 中线下最大复杂度参数对应的树大小。结果再次表明,具有两个分割(三个终端节点)的树。

prune() 函数使用复杂度参数来将树裁剪到所需的大小。它接受完整的树,并根据所需的复杂度参数剪掉最不重要的分割。根据前面的讨论,我们将使用 prune (dtree, cp=0.01705),它返回一个具有所需大小 ❷ 的树。该函数返回复杂度参数低于指定 cp 值的最大树。

rattle 包中的 fancyRpartPlot() 函数用于绘制最终决策树的吸引人图表(见图 17.2)。此函数有几个选项(有关详细信息,请参阅 ?fancyRpartPlot)。type=2 选项(默认)在每个节点下方绘制分割标签。此外,还显示了每个类的比例以及每个节点中的观测值百分比。要分类一个观测值,从树的顶部开始,如果条件为真则移动到左侧分支,否则移动到右侧。继续沿着树向下移动,直到遇到终端节点。使用节点的标签对观测值进行分类。

图片

图 17.2 用于预测癌症状态的常规(裁剪)决策树。从树的顶部开始,如果条件为真则向左移动,否则向右移动。当一个观测值遇到终端节点时,对其进行分类。每个节点包含该节点中类的概率以及样本的百分比。

最后,使用 predict() 函数对测试样本中的每个观测值进行分类 ❸。提供了实际状态与预测状态之间的交叉表。测试样本的整体准确率为 96%。请注意,决策树可能会偏向于选择具有许多级别或许多缺失值的预测变量。

17.3.2 条件推断树

在继续讨论随机森林之前,让我们看看传统决策树的一个重要变体,称为条件推断树。条件推断树与传统树类似,但变量和分割是根据显著性测试而不是纯度/同质性度量来选择的。显著性测试是置换测试(在第十二章中讨论)。

在这种情况下,算法如下:

  1. 计算每个预测变量与结果变量之间关系的 p 值。

  2. 选择 p 值最低的预测变量。

  3. 探索所选预测变量和因变量上所有可能的二元分割(使用置换检验)并选择最显著的分割。

  4. 将数据分为这两组,并对每个子组继续进行过程。

  5. 继续直到分割不再显著或达到最小节点大小。

party包中的ctree()函数提供了条件推断树。在下一段代码中,为乳腺癌数据生成了一个条件推断树。

列表 17.4 使用ctree()创建条件推断树

library(partykit)
fit.ctree <- ctree(class~., data=train)
plot(fit.ctree, main="Conditional Inference Tree",
     gp=gpar(fontsize=8))

> ctree.pred <- predict(fit.ctree, test, type="response")
> ctree.perf <- table(test$class, ctree.pred, 
                      dnn=c("Actual", "Predicted"))
> ctree.perf

          Predicted
Actual      benign malignant
  benign       138         4
  malignant      2        61

注意,条件推断树不需要剪枝,并且该过程相对自动化。此外,partykit包提供了吸引人的绘图选项。图 17.3 绘制了条件推断树。每个节点的阴影区域代表该节点中恶性案例的比例。

图片

图 17.3 乳腺癌数据的条件推断树

以类似图 17.3 的图形显示 rpart()树

如果您使用rpart()创建经典决策树,但希望使用类似图 17.3 的图形显示结果树,partykit包可以提供帮助。您可以使用语句plot(as.party(an.rpart.tree))创建所需的图形。例如,尝试使用列表 17.3 中创建的dtree.pruned对象创建类似图 17.3 的图形,并将结果与图 17.2 中展示的图形进行比较。

传统方法和条件方法生成的决策树可能存在显著差异。在当前示例中,它们的准确性相似(96%对 97%),但树结构相当不同。在下文中,将生成并组合大量决策树以将案例分类到不同的组别。

17.4 随机森林

随机森林是一种监督学习的集成学习方法。开发了多个预测模型,并将结果汇总以提高分类率。您可以在 Leo Breiman 和 Adele Cutler 撰写的mng.bz/7Nul找到关于随机森林的全面介绍。

随机森林的算法涉及对案例和变量进行采样以创建大量决策树。每个案例由每个决策树进行分类。然后使用该案例最常见的分类作为结果。

假设N是训练样本中的案例数量,M是变量数量。然后算法如下:

  1. 通过从训练集中有放回地采样N个案例来生成大量决策树。

  2. 在每个节点采样m < M个变量。这些变量被认为是该节点分割的候选变量。值m对每个节点相同。

  3. 不进行剪枝,完整地生成每棵树(最小节点大小设置为 1)。

  4. 终端节点根据该节点中案例的众数分配给一个类别。

  5. 通过将新案例发送到所有树并采取投票的方式进行分类——多数决定。

通过使用在构建树时未选择的案例进行分类来获得袋外(OOB)错误估计。当没有测试样本时,这是一个优点。随机森林也提供变量重要性的自然度量,您将看到这一点。

随机森林是通过randomForest包中的randomForest()函数生成的。默认的树的数量是 500,默认在每个节点上抽取的变量数量是sqrt(M),最小节点大小是 1。

下面的列表提供了预测乳腺癌数据中恶性肿瘤状态的代码和结果。

列表 17.5 随机森林

> library(randomForest)
> set.seed(1234)
> fit.forest <- randomForest(class~., data=train,      ❶
                             importance=TRUE)          ❶
> fit.forest

Call:
 randomForest(formula = class ~ ., data = train, importance = TRUE) 
               Type of random forest: classification
                     Number of trees: 500
No. of variables tried at each split: 3

        OOB estimate of  error rate: 2.93%
Confusion matrix:
          benign malignant class.error
benign       293         9  0.02980132
malignant      5       171  0.02840909

> randomForest::importance(fit.forest, type=2)         ❷

                         MeanDecreaseGini
clumpThickness                   9.794852
sizeUniformity                  58.635963
shapeUniformity                 49.754466
maginalAdhesion                  8.373530
singleEpithelialCellSize        16.814313
bareNuclei                      36.621347
blandChromatin                  25.179804
normalNucleoli                  14.177153
mitosis                          2.015803

> forest.pred <- predict(fit.forest, test)             ❸
> forest.perf <- table(test$class, forest.pred,        ❸
                       dnn=c("Actual", "Predicted"))   ❸
> forest.perf

           Predicted
Actual      benign malignant
  benign       140         2
  malignant      3        60

❶ 生成森林

❷ 确定变量重要性

❸ 分类新案例

首先,使用randomForest()函数通过从训练样本中重复抽取 489 个观测值以及在每个树的每个节点上抽取 3 个变量来生成 500 棵传统的决策树❶。

随机森林可以通过information=TRUE选项提供变量重要性的自然度量,并通过importance()函数打印出来❷。rattlerandomForest包都有一个名为importance()的函数。由于我们在本章中加载了这两个包,因此在代码中使用randomForest::importance()以确保调用正确的函数。由type=2选项指定的相对重要性度量是从该变量分割中平均所有树的节点纯度(异质性)的总减少量。节点纯度用基尼系数来衡量。sizeUniformity是最重要的变量,而mitosis是最不重要的。

最后,使用随机森林对测试样本进行分类,并计算预测准确性❸。请注意,测试样本中缺失值的案例不会被分类。预测准确性(总体为 98%)非常出色。

虽然randomForest包提供基于传统决策树的森林,但party包中的cforest()函数可以用来生成基于条件推断树的随机森林。如果预测变量高度相关,使用条件推断树的随机森林可能提供更好的预测。

与其他分类方法相比,随机森林通常非常准确。此外,它们可以处理大型问题(许多观测值和变量)、训练集中大量缺失数据,以及变量数量远大于观测值数量的情况。提供 OOB 错误率和变量重要性度量也是显著的优点。

一个显著的缺点是理解分类规则(有 500 棵树!)并将其传达给他人很困难。此外,你需要存储整个森林来对新案例进行分类。

随机森林是一个黑盒模型。预测值进入,准确的预测结果出来,但很难理解盒子里(模型)发生了什么。我们将在第 17.7 节中解决这个问题。

我们将要考虑的最终分类模型是支持向量机。

17.5 支持向量机

支持向量机(SVMs)是一组监督机器学习模型,可用于分类和回归。它们目前很受欢迎,部分原因是它们在开发准确预测模型方面的成功,部分原因是支撑该方法的优雅数学。我们将专注于使用 SVMs 进行二元分类。

支持向量机(SVMs)寻求在多维空间中分离两个类别的最优超平面。超平面被选择以最大化两个类别最近点之间的间隔。间隔边界上的点被称为支持向量(它们有助于定义间隔),间隔的中间是分离超平面。

对于一个N-维空间(即,有N个预测变量),最优超平面(也称为线性决策表面)有N - 1 个维度。如果有两个变量,表面是一条线。对于三个变量,表面是一个平面。对于 10 个变量,表面是一个 9 维超平面。试图想象它会让你头疼。

考虑图 17.4 中所示的两个维度的例子。圆圈和三角形代表两组。间隔是间隙,由两条虚线之间的距离表示。虚线上的点(实心圆圈和三角形)是支持向量。在二维情况下,最优超平面是间隙中间的黑线。在这个理想化的例子中,两组是线性可分的——直线可以完全分离两组而不出错。

图像

图 17.4 展示了两组线性可分的两组分类问题。分离超平面由实心黑线指示。间隔是线到两侧虚线的距离。实心圆圈和三角形是支持向量。

使用二次规划来识别最优超平面,以在约束条件下优化间隔,即一侧的数据点具有+1 的输出值,另一侧的数据具有-1 的输出值。如果数据点几乎可分(不是所有点都在一侧或另一侧),则优化中添加一个惩罚项来考虑错误,并产生软间隔

但是数据可能是根本非线性的。在图 17.5 的例子中,没有任何一条线能够正确地分离圆和三角形。支持向量机(SVMs)使用核函数将数据转换到更高维度的空间,希望它们将变得更加线性可分。想象一下将图 17.5 中的数据以这种方式转换,使得圆从页面上抬起。一种方法是将二维数据转换到三维空间,使用

图片

图片

图 17.5 展示了两个组非线性可分的二组分类问题。这些组不能通过超平面(线)来分离。

然后,你可以使用一张刚性纸片(即在现在是三维空间中的二维平面)将三角形和圆分开。

支持向量机的数学非常复杂,超出了本书的范围。Statnikov, Aliferis, Hardin, 和 Guyon (2011) 提供了一个清晰且直观的 SVM 介绍,它深入到了相当多的概念细节,而没有陷入高等数学的泥潭。

在 R 中,可以使用kernlab包中的ksvm()函数和e1071包中的svm()函数来使用 SVMs。前者更强大,但后者使用起来更简单。下一个列表中的例子使用了后者(简单是好的)来为威斯康星乳腺癌数据开发 SVM。

列表 17.6 支持向量机

> library(e1071)
> set.seed(1234)
> fit.svm <- svm(class~., data=train)
> fit.svm

Call:
svm(formula = class ~ ., data = train)

Parameters:
   SVM-Type:  C-classification 
 SVM-Kernel:  radial 
       cost:  1 

Number of Support Vectors:  84

> svm.pred <- predict(fit.svm, test)
> svm.perf <- table(test$class, 
                    svm.pred, dnn=c("Actual", "Predicted"))
> svm.perf

            Predicted
Actual      benign malignant
  benign       138         4
  malignant      0        63

由于具有较大方差的预测变量通常对 SVMs 的发展有更大的影响,svm()函数默认情况下在拟合模型之前将每个变量缩放到均值为 0 和标准差为 1。正如你所见,预测精度(99%)非常好。

17.5.1 调整 SVM

默认情况下,svm()函数使用径向基函数(RBF)将样本映射到更高维度的空间。RBF 核通常是一个很好的选择,因为它是一个非线性映射,可以处理类标签和预测变量之间的非线性关系。

当使用 RBF 核拟合 SVM 时,有两个参数可以影响结果:gammacost。Gamma 是一个核参数,它控制着分离超平面的形状。较大的 gamma 值通常会导致更多的支持向量。Gamma 也可以被视为一个参数,它控制着训练样本达到的范围,较大的值意味着远,较小的值意味着近。Gamma 必须大于零。

成本参数表示犯错误的代价。较大的值会严重惩罚错误,导致更复杂的分类边界。训练样本中的误分类将更少,但过拟合可能导致在新样本中的预测能力较差。较小的值会导致更平的分类边界,但可能导致欠拟合。与 gamma 一样,成本总是正的。

默认情况下,svm()函数将 gamma 设置为 1 /(预测因子数量)并将 cost 设置为 1。但不同的 gamma 和 cost 组合可能会导致更有效的模型。您可以尝试通过逐个调整参数值来拟合 SVM,但网格搜索更有效。您可以使用tune.svm()函数为每个参数指定一个值范围。tune.svm()拟合每个值的组合并报告每个的性能。下一个列表提供了一个示例。

列表 17.7 调整 RBF 支持向量机

> set.seed(1234)
> tuned <- tune.svm(class~., data=train,                       ❶
                    gamma=10^(-6:1),                           ❶
                    cost=10^(-10:10))                          ❶
> tuned                                                        ❷

Parameter tuning of ‘svm’:

- sampling method: 10-fold cross validation 

- best parameters:
 gamma cost
  0.01    1

- best performance: 0.03355496

> fit.svm <- svm(class~., data=train, gamma=.01, cost=1)       ❸
> svm.pred <- predict(fit.svm, na.omit(test))                  ❹
> svm.perf <- table(na.omit(test)$class,                       ❹
                    svm.pred, dnn=c("Actual", "Predicted"))    ❹
> svm.perf                                                     ❹

           Predicted
Actual      benign malignant
  benign       139         3
  malignant      1        62

❶ 改变参数

❷ 打印最佳模型

❸ 使用这些参数拟合模型

❹ 评估交叉验证性能

首先,使用 RBF 核和变化的 gamma 和 cost 值 ❶ 拟合一个 SVM 模型。指定了 8 个 gamma 值(从 0.000001 到 10)和 21 个 cost 值(从 0.0000000001 到 100000000)。总共拟合并比较了 168 个模型(8 × 21)。在训练样本中具有最少 10 折交叉验证错误的模型具有 gamma = 0.01 和 cost = 1。

使用这些参数值,一个新的支持向量机(SVM)被拟合到训练样本 ❸。然后使用该模型在测试样本中预测结果 ❹,并显示错误数量。调整模型 ❷ 对错误数量的影响几乎可以忽略不计。对于这个例子来说,这并不令人惊讶。默认参数值(cost = 1,gamma = 0.111)与调整后的值(cost = 1,gamma = 0.01)非常相似。在许多情况下,调整 SVM 参数将导致更大的收益。

如所述,SVM 之所以受欢迎,是因为它们在许多情况下都表现良好。它们还可以处理变量数量远大于观测数量的情况。这使得它们在生物医学领域非常受欢迎,在典型的 DNA 微阵列研究中收集的变量数量可能比可用案例数量大一个或两个数量级。

支持向量机的一个缺点是,与随机森林一样,产生的分类规则难以理解和传达。同样,它们基本上是一个黑盒。此外,当从大型训练样本构建模型时,SVM 的扩展性不如随机森林。但一旦构建了一个成功的模型,对新观测的分类扩展性相当好。

17.6 选择最佳预测解

在第 17.4 节至第 17.5 节中,使用几种监督机器学习技术将细针穿刺样本分类为恶性或良性。哪种方法最准确?为了回答这个问题,我们需要在二元分类的上下文中定义术语 准确

  • 最常报告的统计量是 准确率,即分类器正确率的频率。尽管信息量很大,但准确率本身是不够的。还需要额外的信息来评估分类方案的有效性。

  • 考虑一套将个人分类为精神分裂症或非精神分裂症的规则。精神分裂症是一种罕见的疾病,在普通人群中患病率约为 1%。如果你将所有人分类为非精神分裂症,你将有 99%的时间是正确的。但这不是一个好的分类器,因为它还会将每个精神分裂症患者错误地分类为非精神分裂症患者。除了准确性之外,你还应该问这些问题:

    • 正确识别的精神分裂症患者占多少百分比?

    • 正确识别的非精神分裂症患者占多少百分比?

    • 如果一个人被分类为精神分裂症,这种分类正确的可能性有多大?

    • 如果一个人被分类为非精神分裂症,这种分类正确的可能性有多大?

这些问题涉及分类器的灵敏度、特异性、阳性预测力和阴性预测力。每个都在表 17.1 中定义。

表 17.1 预测准确度指标

统计量 解释
灵敏度 当真实结果为阳性时获得阳性分类的概率(也称为真正率或召回率)
特异性 当真实结果为阴性时获得阴性分类的概率(也称为真正率)
阳性预测值 具有阳性分类的观察结果被正确识别为阳性的概率(也称为精确度)
阴性预测值 具有阴性分类的观察结果被正确识别为阴性的概率
准确率 正确识别的观察结果比例(也称为 ACC)

以下列表提供了一个用于计算这些统计数据的函数。

列表 17.8 评估二元分类准确度的函数

performance <- function(table, n=2){
  if(!all(dim(table) == c(2,2))) 
      stop("Must be a 2 x 2 table")
  tn = table[1,1]                                            ❶
  fp = table[1,2]                                            ❶
  fn = table[2,1]                                            ❶
  tp = table[2,2]                                            ❶
  sensitivity = tp/(tp+fn)                                   ❷
  specificity = tn/(tn+fp)                                   ❷
  ppp = tp/(tp+fp)                                           ❷
  npp = tn/(tn+fn)                                           ❷
  hitrate = (tp+tn)/(tp+tn+fp+fn)                            ❷
  result <- paste("Sensitivity = ", round(sensitivity, n) ,  ❸
      "\nSpecificity = ", round(specificity, n),             ❸
      "\nPositive Predictive Value = ", round(ppp, n),       ❸
      "\nNegative Predictive Value = ", round(npp, n),       ❸
      "\nAccuracy = ", round(hitrate, n), "\n", sep="")      ❸
  cat(result)                                                ❸
}

❶ 提取频率

❷ 计算统计数据

❸ 打印结果

performance() 函数接受一个包含真实结果(行)和预测结果(列)的表,并返回五个准确度指标。首先,提取了真正负值(良性组织被识别为良性)、假阳性(良性组织被识别为恶性)、假阴性(恶性组织被识别为良性)和真正阳性(恶性组织被识别为恶性)的数量 ❶。接下来,使用这些计数来计算灵敏度、特异性、阳性预测值和阴性预测值以及准确度 ❷。最后,格式化并打印结果 ❸。

在以下列表中,performance() 函数被应用于本章开发的五个分类器中的每一个。

列表 17.9 乳腺癌数据分类器的性能

> performance(logit.perf)
Sensitivity = 0.95
Specificity = 0.99
Positive Predictive Value = 0.97
Negative Predictive Value = 0.98
Accuracy = 0.98

> performance(dtree.perf)
Sensitivity = 0.95
Specificity = 0.96
Positive Predictive Value = 0.91
Negative Predictive Value = 0.98
Accuracy = 0.96

> performance(ctree.perf)
Sensitivity = 0.97
Specificity = 0.97
Positive Predictive Value = 0.94
Negative Predictive Value = 0.99
Accuracy = 0.97

> performance(forest.perf)
Sensitivity = 0.95
Specificity = 0.99
Positive Predictive Value = 0.97
Negative Predictive Value = 0.98
Accuracy = 0.98

> performance(svm.perf)
Sensitivity = 0.98
Specificity = 0.98
Positive Predictive Value = 0.95
Negative Predictive Value = 0.99
Accuracy = 0.98

这些分类器(逻辑回归、传统决策树、条件推断树、随机森林和支持向量机)在每个准确度指标上都表现出色。这并不总是如此!

在这个特定实例中,奖项似乎授予了支持向量机模型(尽管差异如此之小,它们可能是由偶然造成的)。对于 SVM 模型,98%的恶性肿瘤被正确识别(灵敏度),98%的良性样本被正确识别(特异性),总体正确分类的百分比是 98%(准确率)。恶性肿瘤的诊断在 95%的时间内是正确的(阳性预测值),良性诊断在 99%的时间内是正确的(阴性预测值)。对于癌症的诊断,特异性(正确识别为恶性肿瘤的恶性肿瘤样本比例)尤为重要。

尽管这超出了本章的范围,但通常可以通过以特异性换取灵敏度或反之来提高分类系统。在逻辑回归模型中,predict()用于估计一个案例属于恶性肿瘤组的概率。如果概率大于 0.5,该案例将被分配到该组。这个 0.5 值被称为阈值截止值。如果你改变这个阈值,你可以在牺牲特异性的情况下提高分类模型的灵敏度。predict()还可以为决策树、随机森林和 SVM 生成概率(尽管语法因方法而异)。

通常使用接收者操作特征(ROC)曲线来评估改变阈值值的影响。ROC 曲线绘制了灵敏度与特异性的对比图,针对一系列的阈值值。然后,你可以选择一个在给定问题中灵敏度与特异性最佳平衡的阈值。许多 R 包可以生成 ROC 曲线,包括ROCRpROC。这些包中的分析函数可以帮助你为特定场景选择最佳阈值值,或者比较不同分类算法生成的 ROC 曲线,以选择最有效的方法。欲了解更多信息,请参阅 Kuhn 和 Johnson(2013)。Fawcett(2005)提供了更深入的讨论。

17.7 理解黑盒预测

分类模型用于做出具有重大人类后果的实际工作决策。想象一下,你申请了银行贷款并被拒绝,你想要了解原因。如果决策是基于逻辑回归或分类树模型,原因可以通过查看前者的模型系数或后者的决策树来确定。但如果分类是基于随机森林、支持向量机模型或人工神经网络模型呢?直到最近,答案还是“因为计算机说了这样”,这是一个非常令人不满意的答案!

近年来,有一种趋势是使用称为可解释人工智能(XAI,ema.drwhy.ai)的方法和技术来理解黑盒模型。XAI 的目标是更好地理解黑盒模型在一般情况下(全局理解)以及在进行个别预测时(局部理解)是如何工作的。

考虑一个名叫 Alex 的患者。Alex 的活检产生了以下实验室值:

    bareNuclei = 9,
    sizeUniformity = 1,    
    shapeUniformity = 1, 
    blandChromatin = 7, 
    maginalAdhesion = 1, 
    mitosis = 3,
    normalNucleoli = 3,
    clumpThickness = 6,
    singleEpithelialCellSize = 3

将这些值通过第 17.4 节中开发的随机森林模型运行,得到恶性预测(概率为 0.658)。在本节的剩余部分,我们将使用 XAI 技术来探索我们的随机森林模型是如何产生这种诊断的。我们将使用DALEX包,所以在继续之前请确保安装它(install.packages("DALEX"))。

17.7.1 分解图

我们的目标是将 Alex 的预测分数划分为对最终分类(恶性概率为 0.658)的独特贡献。为了实现这一点,我们将使用分解值

  1. 使用训练样本,计算所有观察到的平均预测响应值(在本例中为恶性概率)(0.365)。将其称为截距。

  2. 对于所有bareNuclei等于9的观察值,计算平均预测响应(0.544)。在这种情况下,bareNuclei的贡献是 0.544 - 0.365 = +0.179。

  3. 对于所有bareNuclei等于9sizeUniformity等于1的观察值,计算平均预测响应(0.476)。sizeUniformity的贡献是 0.476 - 0.544 = -0.068。

  4. 对于所有bareNuclei等于9sizeUniformity等于1shapeUniformity等于1的观察值,计算平均预测响应(0.42)。shapeUniformity的贡献是-0.05。

  5. 继续进行,直到你包括了观察值的所有预测值。个别预测值的贡献将汇总为该案例的模型预测。正贡献增加恶性诊断的可能性。负贡献降低这种可能性。贡献的大小评估了变量对最终预测的影响。

以下列表提供了计算分解值的代码,图 17.6 显示了分解图。

列表 17.10 使用分解图理解黑盒预测

> library(DALEX)

> alex <- data.frame(                                                 ❶
        bareNuclei = 9,                                               ❶
        sizeUniformity = 1,                                           ❶
        shapeUniformity = 1,                                          ❶
        blandChromatin = 7,                                           ❶
        maginalAdhesion = 1,                                          ❶
        mitosis = 3,                                                  ❶
        normalNucleoli = 3,                                           ❶
        clumpThickness = 6,                                           ❶
        singleEpithelialCellSize = 3                                  ❶
    )

> predict(fit.forest, alex, type="prob")                             ❷

  benign malignant
1  0.278     0.722

set.seed(1234)                                                        ❸
explainer_rf_malignant <-                                             ❸
  explain(fit.forest, data = train,  y = train$class == "malignant",  ❸
  predict_function = function(m, x) predict(m, x, type = "prob")[,2]) ❸

rf_pparts <- predict_parts(explainer=explainer_rf_malignant,          ❹
                           new_observation = alex,                    ❹
                           type = "break_down")                       ❹

plot(rf_pparts)                                                       ❹

❶ 探索的观察值

❷ 预测该观察值的结局

❸ 构建解释器对象

❹ 生成分解图

在加载DALEX包后,感兴趣的案例以数据框的形式输入❶。predict()函数将案例通过随机森林运行并产生预测。由于type="prob",返回良性结果和恶性结果的概率❷。接下来,创建一个DALEX explainer对象❸。该对象以随机森林模型、训练数据、结果变量以及用于预测结果的功能作为参数。在这里,我们指定我们想要预测恶性类别。使用预测函数中的[,2]返回仅恶性概率。最后,我们使用explainer对象和要解释的观测值生成分解图❹。

图片

图 17.6 展示了某个人随机森林预测的分解图。恶性活检的平均预测概率为 0.365。根据这个个体的得分,预测概率为 0.722。由于这个概率大于 0.5,预测结果是活检代表恶性。也可以看到每个预测值对这一个体的个体贡献。绿色条表示对预测结果的正面贡献,而红色条表示对预测结果的负面贡献。

观察图 17.6,可以看到对于 Alex 来说,bareNuclei = 9clumpThickness = 6 对恶性诊断有重大贡献。sizeUniformity = 1shapeUniformity = 1 的得分降低了细胞为恶性的概率。考虑所有九个预测值,导致恶性概率为 0.658。由于 0.658 > 0.50,随机森林模型将 Alex 的样本分类为恶性。

17.7.2 绘制 Shapley 值

分解图对于理解个体从黑盒模型获得特定预测的原因非常有帮助。然而,请注意分解是顺序相关的。如果两个或多个预测变量之间存在交互效应,不同变量顺序将得到不同的分解结果。

Shapley 加性解释SHAP值通过计算许多预测变量排序的分解贡献并平均每个变量的结果来绕过对顺序的依赖。继续列表 17.10 中的示例,

set.seed(1234)
rf_pparts = predict_parts(explainer = explainer_rf_malignant, 
                          new_observation = alex, 
                          type = "shap")
plot(rf_pparts)

产生图 17.7 的图。

图片

图 17.7 展示了某个人随机森林预测的 SHAP 值图

箱线图显示了不同变量排序下变量的分解贡献范围。条形表示平均分解值。较长的条形表示对个体预测的影响更大。正(绿色)条形表示对恶性诊断的贡献,而负(红色)条形表示对良性诊断的贡献。

讨论中的 XAI 方法是无模型特定的—they 可以与任何机器学习方法一起使用,包括本章中涵盖的方法。《DALEX》包提供了许多其他此类统计信息,值得一看。此外,谁不喜欢 DALEX?(双关语!)

17.8 深入学习

使用机器学习技术构建预测模型是一个复杂、迭代的过程。常见的步骤包括

  1. 数据拆分—可用数据被分割成训练集和测试集。

  2. 数据预处理—选择预测变量,并可能进行转换。例如,支持向量机通常与标准化预测变量工作得最好。高度相关的变量可能被组合成复合变量或被删除。缺失值必须被估计或删除。

  3. 模型构建—开发了许多可能的候选预测模型。大多数模型都会有需要调整的超参数。超参数是控制学习过程的模型参数,通过试错法进行选择。分类树中的复杂性参数、随机森林中分割的候选变量数量,以及支持向量机中的成本和 gamma 参数都是例子。您会调整这些参数,并选择导致预测性和鲁棒性最强的模型。

  4. 模型比较—在拟合一系列模型之后,它们的性能会被比较,并选择一个最终模型。

  5. 模型发布—一旦选择了最终模型,它就被用来进行未来的预测。由于环境可能会变化,因此重要的是要持续评估模型随时间的变化的有效性。

由于机器学习技术存在于许多不同的包中,并且使用不同的语法,任务进一步复杂化。为了简化工作流程,已经开发了综合或元包。这些包作为其他包的包装器,为构建预测模型提供了简化和一致的接口。学习这些方法之一可以极大地简化构建有效预测系统的工作。

最受欢迎的三种方法分别是caret包、mlr3框架和tidymodels框架(框架由几个相互关联的包组成)。caret包(topepo.github.io/caret)可能是最成熟的包,支持超过 230 种机器学习算法。mlr3框架(mlr3.mlr-org.com/)结构高度严谨,对面向对象程序员有吸引力。tidymodels框架(www.tidymodels.org/)是最新的,也许是最灵活的。由于它是由负责caret包的同一个人创建的,我怀疑它最终会取代caret包。如果你打算用 R 进行机器学习,我建议你选择这些框架之一,并深入研究。你选择哪个取决于个人喜好。

要了解更多关于 R 中支持预测和分类的函数,请查看 CRAN 任务视图中的机器学习和统计学习(mng.bz/I1Lm)。其他好的资源包括 Kuhn 和 Johnson(2013 年)、Forte(2015 年)、Lanz(2015 年)和 Torgo(2017 年)的书籍。

摘要

  • 数据科学中的一个常见任务是预测二元分类结果(通过/不通过;成功/失败;生存/死亡;良性/恶性),这些预测对个人可能产生现实世界的后果。

  • 预测模型可以从相对简单(逻辑回归、分类树)到极其复杂(随机森林、支持向量机、人工神经网络)不等。

  • 模型通常具有超参数(控制学习过程的参数),这些参数必须通过试错来调整。

  • 预测模型的有效性是通过使用性能指标如准确率、灵敏度、特异性、阳性预测力和阴性预测力来评估的。

  • 已经开发出探索高度复杂的黑盒模型的新技术。

  • 开发预测模型的过程涉及众多迭代步骤,这些步骤可以通过使用综合包或框架,如caretmlr2tidymodels来简化。

18 缺失数据的先进方法

本章涵盖

  • 识别缺失数据

  • 可视化缺失数据模式

  • 删除缺失值

  • 填充缺失值

在前面的章节中,我们专注于分析完整的数据集(即没有缺失值的数据集)。尽管这样做有助于简化统计和图形方法的展示,但在现实世界中,缺失数据无处不在。

在某些方面,缺失数据的影响是一个我们大多数人希望避免的主题。统计学书籍可能不会提及它,或者可能将讨论限制在几段之内。统计软件包提供使用可能不是最佳方法的自动处理缺失数据。尽管大多数数据分析(至少在社会科学领域)都涉及缺失数据,但这个主题在期刊文章的方法和结果部分很少被提及。鉴于缺失值出现的频率以及它们的存在可能使研究结果无效的程度,可以说这个主题在专业书籍和课程之外没有得到足够的关注。

数据可能因为许多原因而缺失。调查参与者可能忘记回答一个或多个问题,拒绝回答敏感问题,或者因疲劳而未能完成一个漫长的问卷。研究参与者可能错过预约或提前退出研究。记录设备可能故障,互联网连接可能丢失,或者数据可能被错误编码。分析师甚至可能计划某些数据缺失。例如,为了提高研究效率或降低成本,您可能选择不收集所有参与者的所有数据。最后,数据可能因您永远无法确定的原因而丢失。

不幸的是,大多数统计方法都假设您正在处理完整矩阵、向量和数据框。在大多数情况下,您必须在解决导致您收集数据的具体问题之前消除缺失数据。您可以通过删除具有缺失数据的案例或用合理的替代值替换缺失数据来消除缺失数据。在两种情况下,最终结果都是一个没有缺失值的数据集。

在本章中,我们将探讨处理缺失数据的传统和现代方法。我们将主要使用VIMmicemissForest软件包。命令install.packages(c("VIM", "mice", "missForest"))将下载并安装它们。

为了激发讨论,我们将查看VIM包中提供的哺乳动物睡眠数据集(sleep)(不要与描述药物对睡眠影响的sleep数据集混淆,该数据集包含在基本安装中)。数据来自 Allison 和 Chichetti(1976)的研究,该研究考察了 62 种哺乳动物睡眠与生态和体质变量之间的关系。作者们对为什么动物的睡眠需求从一种物种到另一种物种会有所不同感兴趣。睡眠变量作为因变量,而生态和体质变量作为自变量或预测变量。

睡眠变量包括梦境睡眠的长度(Dream)、非梦境睡眠(NonD)以及它们的总和(Sleep)。体质变量包括体重(以千克计,BodyWgt)、脑重(以克计,BrainWgt)、寿命(以年计,Span)和妊娠时间(以天计,Gest)。生态变量包括物种被猎食的程度(Pred)、睡眠期间暴露的程度(Exp)以及它们面临的总体危险(Danger)。生态变量是在 1(低)到 5(高)的五点评分量表上测量的。

在他们的原始文章中,Allison 和 Chichetti 将他们的分析限制在具有完整数据的物种上。我们将更进一步,使用多重插补方法分析所有 62 个案例。

18.1 处理缺失数据的步骤

如果你刚开始研究缺失数据,你会发现自己面前有一系列令人眼花缭乱的方法、批评和方法论。这个领域的经典文本是 Little 和 Rubin(2002)。在 Allison(2001)、Schafer 和 Graham(2002)、Enders(2010)以及 Schlomer、Bauman 和 Card(2010)中可以找到优秀的、易于理解的综述。一种全面的方法通常包括以下步骤:

  1. 识别缺失数据。

  2. 检查缺失数据的原因。

  3. 删除包含缺失数据的案例或用合理的替代数据值替换(插补)缺失值。

不幸的是,识别缺失数据通常是唯一明确的步骤。了解数据为何缺失取决于你对生成数据的过程的理解。决定如何处理缺失值将取决于你对哪些程序将产生最可靠和准确的结果的估计。

缺失数据的分类系统

统计学家通常将缺失数据分为三种类型。这些类型通常用概率术语描述,但基本思想是直接的。我们将使用睡眠研究中梦境的测量(其中 12 个动物有缺失值)依次说明每种类型:

  • 完全随机缺失——如果一个变量的缺失数据与任何其他观测或未观测变量无关,那么这些数据就是完全随机缺失(MCAR)。如果这 12 只动物梦境睡眠缺失没有系统性原因,那么这些数据被认为是 MCAR。请注意,如果每个有缺失数据的变量都是 MCAR,你可以认为完整案例是从更大的数据集中抽取的简单随机样本。

  • 随机缺失——如果一个变量的缺失数据与其它的观测变量相关,但与其自身的未观测值无关,那么这些数据就是随机缺失(MAR)。例如,如果体重较轻的动物更有可能缺失梦境睡眠的数据(可能是因为观察较小的动物更困难),并且“缺失性”与动物花在梦境中的时间无关,那么这些数据被认为是 MAR。在这种情况下,一旦控制了体重,梦境睡眠数据的缺失与否就是随机的。

  • 非随机缺失——如果一个变量的缺失数据既不是 MCAR 也不是 MAR,那么这些数据就不是随机缺失(NMAR)。例如,如果花较少时间做梦的动物也更有可能缺失梦境值(可能是因为测量较短的持续时间更困难),那么这些数据被认为是 NMAR。

大多数处理缺失数据的方法都假设数据要么是 MCAR(完全随机缺失)要么是 MAR(随机缺失)。在这种情况下,你可以忽略产生缺失数据的机制,并且在替换或删除缺失数据后,直接对感兴趣的关系进行建模。

对于 NMAR 数据,正确分析可能很困难。当数据是 NMAR 时,你必须同时建模产生缺失值的机制以及感兴趣的关系。(分析 NMAR 数据的方法包括使用选择模型和模式混合。NMAR 数据的分析可能很复杂,超出了本书的范围。)

图像

图 18.1 处理不完整数据的方法,以及支持它们的 R 包

处理缺失数据的方法有很多,并且不能保证它们会产生相同的结果。图 18.1 描述了用于处理不完整数据的一系列方法以及支持它们的 R 包。

对缺失数据方法的全面回顾需要一本书来阐述(Enders [2010] 是一个很好的例子)。在本章中,我们将回顾探索缺失值模式的方法,并重点关注处理不完整数据的四种最流行方法:理性方法、列表删除、单次插补和多次插补。我们将以对其他方法的简要讨论结束,包括在特殊情况下有用的那些方法。

18.2 识别缺失值

首先,让我们回顾第 3.5 节中介绍的材料,并在此基础上进行扩展。R 使用符号NA(不可用)表示缺失值,使用符号NaN(非数字)表示不可能的值。此外,符号Inf-Inf分别表示正无穷和负无穷。函数is.na()is.nan()is.infinite()可以分别用来识别缺失、不可能和无限值。每个函数返回TRUEFALSE。表 18.1 给出了示例。

表 18.1 is.na()is.nan()is.infinite()函数的返回值示例

x is.na(x) is.nan(x) is.infinite(x)
x <- NA TRUE FALSE FALSE
x <- 0 / 0 TRUE TRUE FALSE
x <- 1 / 0 FALSE FALSE TRUE

这些函数返回一个与它的参数大小相同的对象,如果元素是正在测试的类型,则每个元素被替换为TRUE,否则为FALSE。例如,设y <- c(1, 2, 3, NA)。那么is.na(y)将返回向量c(FALSE, FALSE, FALSE, TRUE)

函数complete.cases()可以用来识别矩阵或数据框中不包含缺失值的行。它返回一个逻辑向量,对于包含完整案例的每一行返回TRUE,对于有缺失值的每一行返回FALSE

让我们将此应用于睡眠数据集:

# load the dataset
data(sleep, package="VIM")

# list the rows that do not have missing values
sleep[complete.cases(sleep),]

# list the rows that have one or more missing values
sleep[!complete.cases(sleep),]

检查输出结果显示,有 42 个案例数据完整,20 个案例有一个或多个缺失值。

因为逻辑值TRUEFALSE等同于数值 1 和 0,所以sum()mean()函数可以用来获取有关缺失数据的有用信息。考虑以下内容:

> sum(is.na(sleep$Dream))
[1] 12
> mean(is.na(sleep$Dream))
[1] 0.19
> mean(!complete.cases(sleep))
[1] 0.32

结果表明,变量Dream有 12 个值缺失。在这个变量上,19%的案例有缺失值。此外,数据集中 32%的案例有一个或多个缺失值。

在识别缺失值时有两个需要注意的事项。首先,complete.cases()函数只识别NANaN为缺失值。无限值(Inf–Inf)被视为有效值。其次,你必须使用缺失值函数,如本节中所述,来识别 R 数据对象中的缺失值。逻辑比较如myvar == NA永远不会为真。

现在你已经知道了如何通过编程方式识别缺失值,让我们看看帮助探索缺失数据发生可能模式的工具。

18.3 探索缺失值模式

在决定如何处理缺失数据之前,确定哪些变量有缺失值、缺失的数量以及缺失的组合将非常有用。在本节中,我们将回顾探索缺失值模式的各种表格、图形和相关性方法。最终,你想要了解数据缺失的原因。这个答案将影响你如何进行进一步的分析。

18.3.1 可视化缺失值

mice 包中的 md.pattern() 函数在矩阵或数据框中生成缺失数据模式的表格。此外,它还以图形的形式绘制这个表格。将此函数应用于 sleep 数据集,生成以下列表和图 18.2 中的图形。

列表 18.1 使用 md.pattern() 的缺失值模式

> library(mice)
> data(sleep, package="VIM")
> md.pattern(sleep, rotate.names=TRUE)
   BodyWgt BrainWgt Pred Exp Danger Sleep Span Gest Dream NonD   
42       1        1    1   1      1     1    1    1     1    1  0
 2       1        1    1   1      1     1    0    1     1    1  1
 3       1        1    1   1      1     1    1    0     1    1  1
 9       1        1    1   1      1     1    1    1     0    0  2
 2       1        1    1   1      1     0    1    1     1    0  2
 1       1        1    1   1      1     1    0    0     1    1  2
 2       1        1    1   1      1     0    1    1     0    0  3
 1       1        1    1   1      1     1    0    1     0    0  3
         0        0    0   0      0     4    4    4    12   14 38

表格主体中的 1 和 0 表示缺失值模式,其中 0 表示给定列变量缺失值,1 表示非缺失值。第一行描述了 无缺失值 的模式(所有元素都是 1)。第二行描述了 除了 Span 外无缺失值 的模式。第一列表示每个缺失数据模式中的案例数量,最后一列表示每个模式中存在缺失值的变量数量。在这里,您可以看到有 42 个没有缺失数据的案例,2 个案例仅缺失 Span。有 9 个案例同时缺失 NonDDream 值。该数据集总共有 (42 × 0) + (2 × 1) + ... + (1 × 3) = 38 个缺失值。最后一行给出了每个变量的总缺失值数。

虽然来自 md.pattern() 函数的表格输出很紧凑,但我经常发现通过视觉方式更容易识别模式。在图 18.2 中,每一行代表一个模式。深蓝色表示存在值,而浅红色表示缺失值。左侧的数字表示模式中的案例数量,右侧的数字表示模式中缺失变量的数量,底部的数字表示每个变量的缺失值数量。在这里,您可以看到有 42 个没有缺失数据的案例,2 个案例仅缺失 Span。有 9 个案例同时缺失 NonDDream 值。该数据集总共有 (42 × 0) + (2 × 1) + ... + (1 × 3) = 38 个缺失值。最后一行给出了每个变量的总缺失值数。

图 18.2 由 md.pattern() 函数总结的缺失值模式。每一行代表缺失(浅红色)和非缺失(深蓝色)数据的一个模式。

VIM 包提供了许多用于可视化数据集中缺失值模式的函数。在这里,我们将查看其中三个最有用的:aggr()matrixplot()marginplot()

aggr() 函数单独为每个变量以及每个变量的组合绘制缺失值的数量。它为图 18.2 提供了一个很好的替代方案。例如,以下代码

library("VIM")
aggr(sleep, prop=FALSE, numbers=TRUE)

生成图 18.3 的图形。

图 18.3 aggr() 生成的睡眠数据集的缺失值模式图

您可以看到变量 NonD 有最多的缺失值(14 个),并且有两种哺乳动物缺失 NonDDreamSleep 分数。四十二种哺乳动物没有缺失数据。

语句 aggr(sleep, prop=TRUE, numbers=TRUE) 生成相同的图表,但显示的是比例而不是计数。选项 numbers=FALSE(默认值)抑制了数字标签。请注意,随着数据集中变量数量的增加,此图表的标签可能会严重变形。你可以通过手动减小标签大小来修复此问题。包括参数 cex.lab=``ncex.axis=``n,和 cex.number=``n(其中 n 是小于 1 的数字)将分别缩小轴、变量和数字标签的大小。

matrixplot() 函数生成一个显示每个案例数据的图表。图 18.4 展示了使用 matrixplot(sleep, sort="BodyWgt") 创建的图形。在这里,数值数据被缩放到区间 [0, 1],并以灰度颜色表示,较浅的颜色代表较小的值,较深的颜色代表较大的值。默认情况下,缺失值用红色表示。请注意,在图 18.4 中,红色已被手工替换为交叉线,以便在灰度下查看缺失值。图 18.4 中的行按 BodyWgt 排序。

图 18.4 展示了 sleep 数据集中按案例(行)实际和缺失值的矩阵图。矩阵按 BodyWgt 排序。

marginplot() 函数在两个变量之间生成散点图,并在图表的边缘显示有关缺失值的信息。考虑梦境睡眠量和哺乳动物妊娠长度之间的关系。以下语句

marginplot(sleep[c("Gest","Dream")], pch=20, 
           col=c("darkgray", "red", "blue"))

生成图 18.5 中的图形。pchcol 参数是可选的,它们提供了对绘图符号和颜色的控制。

图 18.5 展示了梦境睡眠量和妊娠长度之间的散点图,边缘包含缺失数据的信息

图形主体显示了 GestDream 之间的散点图(基于两个变量的完整案例)。在左侧边缘,箱线图显示了具有(深灰色)和没有(红色)Gest 值的哺乳动物的 Dream 分布。(注意,在灰度图中,红色是较深的色调。)四个红色点代表缺失 Gest 得分的哺乳动物的 Dream 值。在底部边缘,GestDream 的角色被反转。你可以看到妊娠长度和梦境睡眠之间存在负相关关系,并且对于缺失妊娠得分的哺乳动物,梦境睡眠往往较高。同时缺失两个变量值的观测数以蓝色打印在两个边缘的交叉点(左下角)。

VIM 包含许多图表,可以帮助你理解缺失数据在数据集中的作用,并且值得探索。有生成散点图、箱线图、直方图、散点图矩阵、平行图、地毯图和包含缺失值信息的气泡图的函数。

18.3.2 使用相关系数探索缺失值

在我们继续之前,还有一种值得注意的方法。你可以在数据集中用编码为1表示缺失和0表示存在的指示变量替换数据。得到的矩阵有时被称为影子矩阵。将这些指示变量相互之间以及与原始(观测)变量进行相关分析可以帮助你看到哪些变量倾向于同时缺失,以及一个变量的缺失性与其他变量的值之间的关系。

考虑以下代码:

x <- as.data.frame(abs(is.na(sleep)))

数据框x的元素如果是sleep对应元素缺失则为1,否则为0。你可以通过查看每个的前几行来看到这一点:

> head(sleep, n=5)
   BodyWgt BrainWgt NonD Dream Sleep Span Gest Pred Exp Danger
1 6654.000   5712.0   NA    NA   3.3 38.6  645    3   5      3
2    1.000      6.6  6.3   2.0   8.3  4.5   42    3   1      3
3    3.385     44.5   NA    NA  12.5 14.0   60    1   1      1
4    0.920      5.7   NA    NA  16.5   NA   25    5   2      3
5 2547.000   4603.0  2.1   1.8   3.9 69.0  624    3   5      4

> head(x, n=5)
  BodyWgt BrainWgt NonD Dream Sleep Span Gest Pred Exp Danger
1       0        0    1     1     0    0    0    0   0      0
2       0        0    0     0     0    0    0    0   0      0
3       0        0    1     1     0    0    0    0   0      0
4       0        0    1     1     0    1    0    0   0      0
5       0        0    0     0     0    0    0    0   0      0

以下声明

y <- x[which(apply(x,2,sum)>0)]

提取具有一些(但不是全部)缺失值的变量,并且

cor(y)

给出这些指示变量之间的相关系数:

        NonD  Dream  Sleep   Span   Gest
NonD   1.000  0.907  0.486  0.015 -0.142
Dream  0.907  1.000  0.204  0.038 -0.129
Sleep  0.486  0.204  1.000 -0.069 -0.069
Span   0.015  0.038 -0.069  1.000  0.198
Gest  -0.142 -0.129 -0.069  0.198  1.000

在这里,你可以看到DreamNonD倾向于同时缺失(r = 0.91)。在较小的程度上,SleepNonD也倾向于同时缺失(r = 0.49),以及SleepDream也倾向于同时缺失(r = 0.20)。

最后,你可以查看一个变量中的缺失值与其他变量的观测值之间的关系:

> cor(sleep, y, use="pairwise.complete.obs")
           NonD  Dream   Sleep   Span   Gest
BodyWgt   0.227  0.223  0.0017 -0.058 -0.054
BrainWgt  0.179  0.163  0.0079 -0.079 -0.073
NonD         NA     NA      NA -0.043 -0.046
Dream    -0.189     NA -0.1890  0.117  0.228
Sleep    -0.080 -0.080      NA  0.096  0.040
Span      0.083  0.060  0.0052     NA -0.065
Gest      0.202  0.051  0.1597 -0.175     NA
Pred      0.048 -0.068  0.2025  0.023 -0.201
Exp       0.245  0.127  0.2608 -0.193 -0.193
Danger    0.065 -0.067  0.2089 -0.067 -0.204
Warning message:
In cor(sleep, y, use = "pairwise.complete.obs") :
  the standard deviation is zero

在这个相关矩阵中,行是观测变量,列是表示缺失性的指示变量。你可以忽略相关矩阵中的警告信息和NA值;它们是我们方法的艺术品。

从相关矩阵的第一列中,你可以看到非做梦睡眠分数更有可能缺失于体重较高的哺乳动物(r = 0.227)、妊娠期(r = 0.202)和睡眠暴露(r = 0.245)。其他列以类似方式读取。这个表中的相关性都不是特别大或引人注目,这表明数据与 MCAR 偏差很小,可能是 MAR。

注意,你永远不能排除数据是非缺失完全随机(NMAR)的可能性,因为你不知道缺失数据的值是什么。例如,你不知道哺乳动物做梦的数量与这个变量值缺失的概率之间是否存在关系。在没有强有力的外部证据的情况下,我们通常假设数据要么是完全随机缺失(MCAR)要么是随机缺失(MAR)。

18.4 理解缺失数据的原因和影响

你可以确定缺失数据的数量、分布和模式,以评估导致缺失数据的潜在机制以及缺失数据对你回答实质性问题的能力的影响。特别是,你想要回答以下问题:

  • 缺失数据占数据的百分比是多少?

  • 缺失数据是否集中在少数几个变量中或广泛分布?

  • 缺失值看起来是否是随机的?

  • 缺失数据彼此之间或与观测数据的相关性是否表明存在产生缺失值的可能机制?

这些问题的答案有助于确定哪些统计方法最适合分析你的数据。例如,如果缺失数据集中在几个相对不重要的变量中,你可能能够删除这些变量并继续正常分析。如果少量数据(比如说,少于 10%)在数据集中随机分布(MCAR),你可能能够将分析限制在具有完整数据的案例上,并仍然得到可靠和有效的结果。如果你可以假设数据是 MCAR 或 MAR,你可能能够应用多重插补方法来得出有效的结论。如果数据是 NMAR,你可以转向专门的方法,收集新的数据,或者选择一个更容易且更有回报的职业。

这里有一些例子:

  • 在一项最近使用纸质问卷的调查中,我发现几个项目倾向于同时缺失。很明显,这些项目聚集在一起是因为参与者没有意识到问卷的第三页有背面——背面包含了这些项目。在这种情况下,数据可以考虑为 MCAR。

  • 在另一项研究中,一个教育变量在全球领导风格调查中经常缺失。调查发现,欧洲参与者更有可能留空这一项。结果证明,某些国家的参与者认为这些类别没有意义。在这种情况下,数据最可能是 MAR。

  • 最后,我参与了一项关于抑郁症的研究,其中老年患者比年轻患者更有可能省略描述抑郁情绪的项目。访谈显示,老年患者不愿意承认这些症状,因为这违反了他们关于“保持坚强”的价值观念。不幸的是,还发现严重抑郁的患者更有可能省略这些项目,因为感到绝望和注意力难以集中。在这种情况下,必须将数据视为 NMAR。

正如你所见,识别模式只是第一步。你需要将你对研究主题和数据收集过程的理解应用于确定缺失值的来源。

现在我们已经考虑了缺失数据的来源和影响,让我们看看标准统计方法如何被调整以适应它们。我们将关注四种流行的方法:一种用于恢复数据的有理方法,一种涉及删除缺失数据的传统方法,一种用于插补单个缺失值的方法,以及一种使用模拟来考虑缺失数据对结论影响的方法。在这个过程中,我们将简要介绍专门情况的方法和已经过时应该被淘汰的方法。目标将保持不变:尽可能准确地回答导致你收集数据的实质性问题,考虑到信息不完整的情况。

18.5 处理不完整数据的理性方法

在理性方法中,你使用变量之间的数学或逻辑关系来尝试填补或恢复缺失值。一些例子将有助于阐明这种方法。

sleep数据集中,变量SleepDreamNonD变量的总和。如果你知道一种哺乳动物的任意两个得分,你就可以推导出第三个得分。因此,如果某些观测值缺失了三个变量中的一个,你可以通过加法或减法恢复缺失的信息。

作为第二个例子,考虑关注代际群体(例如,沉默的一代、早期婴儿潮一代、晚期婴儿潮一代、X 一代、千禧一代)之间工作/生活平衡差异的研究,其中群体是根据他们的出生年份定义的。参与者被要求提供他们的出生日期和年龄。如果出生日期缺失,你可以通过知道他们的年龄和完成调查的日期来恢复他们的出生年份(因此他们的代际群体)。

使用逻辑关系恢复缺失数据的一个例子来自一系列领导力研究,其中参与者被问及他们是否是管理者(是/否)以及他们直接下属的数量(整数)。如果他们留空管理者问题但表明他们有一个或多个直接下属,那么推断他们是管理者是合理的。

作为最后的例子,我经常参与性别研究,比较男性和女性的领导风格和有效性。参与者完成包括他们的名字(名和姓)、他们的性别以及他们领导方法和影响的详细评估的调查。如果参与者留空性别问题,我必须估算值以将他们包括在研究中。在一项最近对 66,000 名管理者的研究中,11,000 名(17%)缺失性别值。

为了解决这个问题,我采用了以下理性过程。首先,我将名字和性别进行交叉制表。一些名字与男性相关联,一些与女性相关联,还有一些与两者都相关。例如,William出现了 417 次,并且总是男性。相反,名字Chris出现了 237 次,但有时是男性(86%)有时是女性(14%)。如果一个名字在数据集中出现了超过 20 次,并且总是与男性或女性相关联(但从不与两者都相关),我假设这个名字代表单一性别。我使用这个假设为特定性别的名字创建了一个性别查找表。通过使用这个查找表为缺失性别值的参与者,我能够恢复 7,000 个案例(63%的缺失响应)。

理性方法通常需要创造力、周到以及一定程度的数据管理技能。数据恢复可能是精确的(如睡眠示例所示)或近似的(如性别示例所示)。在下一节中,我们将探讨通过删除观测值来创建完整数据集的方法。

18.6 删除缺失数据

处理缺失数据最常见的方法是简单地删除它们。这通常涉及删除具有大量缺失数据的变量(列),然后删除包含任何剩余变量(列表删除)缺失数据的观测值。一个不太常见的选择是仅删除特定分析中涉及的缺失数据(例如,成对删除)。每种方法将在下面进行描述。

18.6.1 完整案例分析(列表删除)

在完整案例分析中,只有包含每个变量有效数据值的观测值被保留用于进一步分析。实际上,这涉及到删除任何包含一个或多个缺失值的行,也被称为列表删除案例删除逐个删除。大多数流行的统计软件包将列表删除作为处理缺失数据的默认方法。事实上,它如此普遍,以至于许多执行回归或方差分析等分析的分析师甚至可能没有意识到存在一个缺失值问题需要解决!

可以使用complete.cases()函数来保存没有缺失数据的矩阵或数据框的案例(行):

newdata <- mydata[complete.cases(mydata),]

同样的结果可以使用na.omit函数实现:

newdata <- na.omit(mydata)

在这两个语句中,在结果保存到newdata之前,会从mydata中删除任何缺失数据的行。

假设你对睡眠研究中变量的相关性感兴趣。应用列表删除,你会在计算相关性之前删除所有带有缺失数据的哺乳动物:

> options(digits=1)
> cor(na.omit(sleep))
         BodyWgt BrainWgt NonD Dream Sleep  Span  Gest  Pred  Exp Danger
BodyWgt     1.00     0.96 -0.4 -0.07  -0.3  0.47  0.71  0.10  0.4   0.26
BrainWgt    0.96     1.00 -0.4 -0.07  -0.3  0.63  0.73 -0.02  0.3   0.15
NonD       -0.39    -0.39  1.0  0.52   1.0 -0.37 -0.61 -0.35 -0.6  -0.53
Dream      -0.07    -0.07  0.5  1.00   0.7 -0.27 -0.41 -0.40 -0.5  -0.57
Sleep      -0.34    -0.34  1.0  0.72   1.0 -0.38 -0.61 -0.40 -0.6  -0.60
Span        0.47     0.63 -0.4 -0.27  -0.4  1.00  0.65 -0.17  0.3   0.01
Gest        0.71     0.73 -0.6 -0.41  -0.6  0.65  1.00  0.09  0.6   0.31
Pred        0.10    -0.02 -0.4 -0.40  -0.4 -0.17  0.09  1.00  0.6   0.93
Exp         0.41     0.32 -0.6 -0.50  -0.6  0.32  0.57  0.63  1.0   0.79
Danger      0.26     0.15 -0.5 -0.57  -0.6  0.01  0.31  0.93  0.8   1.00
The correlations in this table are based solely on the 42 mammals that have 
complete data on all variables. (Note that the statement cor(sleep, use="complete.obs") 
would have produced the same results.) 

如果你想要研究寿命和妊娠期长度对梦境睡眠量的影响,你可以使用列表删除的线性回归:

> fit <- lm(Dream ~ Span + Gest, data=na.omit(sleep))
> summary(fit)

Call:
lm(formula = Dream ~ Span + Gest, data = na.omit(sleep))

Residuals:
   Min     1Q Median     3Q    Max 
-2.333 -0.915 -0.221  0.382  4.183 

Coefficients:
             Estimate Std. Error t value Pr(>|t|)    
(Intercept)  2.480122   0.298476    8.31  3.7e-10 ***
Span        -0.000472   0.013130   -0.04    0.971    
Gest        -0.004394   0.002081   -2.11    0.041 *  
---
Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ‘ 1 

Residual standard error: 1 on 39 degrees of freedom
Multiple R-squared: 0.167,      Adjusted R-squared: 0.125 
F-statistic: 3.92 on 2 and 39 DF,  p-value: 0.0282 

在这里,你可以看到,对于妊娠期较短的哺乳动物来说,它们的梦境睡眠时间更长(在控制寿命的情况下),而当控制妊娠期时,寿命与梦境睡眠无关。该分析基于 42 个完整数据案例。

在上一个例子中,如果将data=na.omit(sleep)替换为data=sleep会发生什么?像许多 R 函数一样,lm()使用的是有限制的列表删除定义。在这种情况下,任何在函数拟合的变量(例如,梦境、寿命和妊娠期)上存在缺失数据的案例都会被删除。分析将基于 42 个案例。

列表删除假设数据是 MCAR(即,完整观测值是完整数据集的随机子样本)。在当前例子中,我们假设使用的 42 只哺乳动物是收集到的 62 只哺乳动物的随机子样本。在 MCAR 假设被违反到一定程度时,得到的回归参数将会存在偏差。删除所有带有缺失数据的观测值也会通过减少可用样本量来降低统计功效。在当前例子中,列表删除将样本量减少了 32%。

18.6.2 可用案例分析(成对删除)

成对 删除 通常被认为是处理具有缺失值的数据集时的另一种选择。在成对删除中,只有当观测值缺失了特定分析中涉及的变量数据时,才会删除观测值。考虑以下代码:

> cor(sleep, use="pairwise.complete.obs")
         BodyWgt BrainWgt NonD Dream Sleep  Span Gest  Pred  Exp Danger
BodyWgt     1.00     0.93 -0.4  -0.1  -0.3  0.30  0.7  0.06  0.3   0.13
BrainWgt    0.93     1.00 -0.4  -0.1  -0.4  0.51  0.7  0.03  0.4   0.15
NonD       -0.38    -0.37  1.0   0.5   1.0 -0.38 -0.6 -0.32 -0.5  -0.48
Dream      -0.11    -0.11  0.5   1.0   0.7 -0.30 -0.5 -0.45 -0.5  -0.58
Sleep      -0.31    -0.36  1.0   0.7   1.0 -0.41 -0.6 -0.40 -0.6  -0.59
Span        0.30     0.51 -0.4  -0.3  -0.4  1.00  0.6 -0.10  0.4   0.06
Gest        0.65     0.75 -0.6  -0.5  -0.6  0.61  1.0  0.20  0.6   0.38
Pred        0.06     0.03 -0.3  -0.4  -0.4 -0.10  0.2  1.00  0.6   0.92
Exp         0.34     0.37 -0.5  -0.5  -0.6  0.36  0.6  0.62  1.0   0.79
Danger      0.13     0.15 -0.5  -0.6  -0.6  0.06  0.4  0.92  0.8   1.00

在这个例子中,任何两个变量之间的相关性都是基于这两个变量的所有可用观测值(忽略其他变量)。BodyWgtBrainWgt 之间的相关性是基于所有 62 只哺乳动物(这两个变量都有数据的哺乳动物数量)。DreamBodyWgt 之间的相关性是基于 50 只哺乳动物,而 DreamNonD 之间的相关性是基于 48 只哺乳动物。

虽然成对删除看起来使用了所有可用数据,但实际上每个计算都是基于数据的不同子集。这可能导致扭曲和难以解释的结果。我建议避免这种方法。

接下来,我们将考虑一种采用整个数据集(包括缺失数据的案例)的方法。

18.7 单一填补

单一填补 中,每个缺失值都替换为一个合理的替代值(即一个合理的猜测)。在本节中,我们将介绍三种方法,并描述何时使用(或不使用)每种方法。

18.7.1 简单填补

简单填补 中,一个变量的缺失值被替换为一个单一值(例如,均值、中位数或众数)。使用均值替换,你可以用非缺失值的均值 1.972 替换 Dream 变量上的每个缺失值。

简单填补的一个优点是它解决了缺失值问题,而没有减少可用于分析的数据样本量。由于简单填补简单易行,因此它变得相当流行。然而,对于非完全随机缺失(MCAR)的数据,它会产生有偏的结果。如果缺失的数据量适中到较大,简单填补可能会低估标准误差,扭曲变量之间的相关性,并在统计测试中产生错误的 p 值。我建议避免在大多数缺失值问题上使用这种方法。

18.7.2 K-最近邻填补

k-最近邻填补 的理念很简单。对于一个有一个或多个缺失值的观测值,找到最相似但具有这些值的观测值,并使用这些案例来进行填补。

例如,考虑以下来自 sleep 数据框的观测值:

BodyWgt BrainWgt NonD Dream Sleep Span Gest Pred Exp Danger
   1.41     17.5  4.8   1.3   6.1   34   NA    1   2      1

这个观测值在妊娠变量(Gest)上有缺失值。为了填补这个缺失值,你可以

  1. 根据其他九个变量,在数据框中找到最接近(最相似)的 k 个观测值。

  2. 对这些 k 个观测值上的 Gest 值进行汇总。例如,取 kGest 值的平均值。

  3. 用这个汇总值替换缺失值。

这些步骤将重复应用于每个包含缺失值的观测值。

要使用这种方法,必须回答三个问题。我们应该如何定义最近邻?我们应该使用多少个最近邻?我们应该如何汇总值?

VIM 包中的 kNN() 函数执行 k 近邻插补。默认设置如下:

  • 最近邻被定义为与目标观测值具有最小 Gower 距离(Kowarik 和 Templ,2016)的案例。与第十六章中描述的欧几里得距离不同,Gower 距离可以用于包含定量和分类变量的数据。定义最近邻时使用所有可用变量。

  • 对于每个具有缺失值的观测值,确定五个最近邻。

  • 定量缺失值的汇总值是 k 个最近邻值的中间值。对于分类缺失值,使用众数(最频繁出现的类别)。

每个这些默认值都可以由用户修改。有关详细信息,请参阅 help(kNN)

以下列出 sleep 数据框的 kNN 插补应用。

列表 18.2 睡眠数据框的 K 近邻插补

> library(VIM)
> head(sleep)

   BodyWgt BrainWgt NonD Dream Sleep Span Gest Pred Exp Danger
1 6654.000   5712.0   NA    NA   3.3 38.6  645    3   5      3
2    1.000      6.6  6.3   2.0   8.3  4.5   42    3   1      3
3    3.385     44.5   NA    NA  12.5 14.0   60    1   1      1
4    0.920      5.7   NA    NA  16.5   NA   25    5   2      3
5 2547.000   4603.0  2.1   1.8   3.9 69.0  624    3   5      4
6   10.550    179.5  9.1   0.7   9.8 27.0  180    4   4      4

> sleep_imp <- kNN(sleep, imp_var=FALSE)

> head(sleep_imp)

   BodyWgt BrainWgt NonD Dream Sleep Span Gest Pred Exp Danger
1 6654.000   5712.0  3.2   0.8   3.3 38.6  645    3   5      3
2    1.000      6.6  6.3   2.0   8.3  4.5   42    3   1      3
3    3.385     44.5 12.8   2.4  12.5 14.0   60    1   1      1
4    0.920      5.7 10.4   2.4  16.5  3.2   25    5   2      3
5 2547.000   4603.0  2.1   1.8   3.9 69.0  624    3   5      4
6   10.550    179.5  9.1   0.7   9.8 27.0  180    4   4      4

sleep_imp 数据框中,所有缺失值都已用插补值替换。默认情况下,kNN() 函数会在数据框中添加 10 个新变量(BodyWgt_impBrainWgt_imp,...,Danger_imp),用 TRUE/FALSE 编码来指示哪些值已被插补。设置 var_imp=FALSE 会抑制这些附加变量。

kNN 插补是小型到中等规模数据集(例如,< 1000 个观测值)的一个优秀选项。由于需要在数据框中的每个缺失数据观测值与其他所有观测值之间计算距离,因此它不适合较大的问题。在这种情况下,下面描述的方法可能非常有效。

18.7.3 missForest

对于更大的 数据集,可以使用随机森林(第十七章)来插补缺失值。如果你有 p 个变量 X[1],X[2],...,X[p]),步骤如下:

  1. 使用均值替换每个定量变量的缺失值。使用众数替换每个分类变量的缺失值。跟踪缺失值的位置。

  2. 返回变量 X[1] 的缺失数据。创建一个没有此变量缺失值的案例训练数据集。使用训练集,构建随机森林模型(第十七章)来预测 X[1]。使用该模型为具有缺失 X[1] 值的案例插补 X[1]。

  3. 对变量 X[2] 通过 X[p] 重复步骤 2。

  4. 重复步骤 2 和 3,直到插补值的变化不超过指定的量。

实际上执行这些操作比描述它们要容易!missForest 包中的 missForest() 函数可以用来。使用 sleep 数据框,以下列出代码。

列表 18.3 睡眠数据框的随机森林插补

> library(missForest)
> set.seed(1234)
> sleep_imp <- missForest(sleep)$ximp

  missForest iteration 1 in progress...done!
  missForest iteration 2 in progress...done!
  missForest iteration 3 in progress...done!
  missForest iteration 4 in progress...done!
  missForest iteration 5 in progress...done!
  missForest iteration 6 in progress...done!

> head(sleep_imp)
   BodyWgt BrainWgt      NonD  Dream Sleep   Span Gest Pred Exp Danger
1 6654.000   5712.0  3.391857 1.1825   3.3 38.600  645    3   5      3
2    1.000      6.6  6.300000 2.0000   8.3  4.500   42    3   1      3
3    3.385     44.5 10.758000 2.4300  12.5 14.000   60    1   1      1
4    0.920      5.7 11.572000 2.7020  16.5  7.843   25    5   2      3
5 2547.000   4603.0  2.100000 1.8000   3.9 69.000  624    3   5      4
6   10.550    179.5  9.100000 0.7000   9.8 27.000  180    4   4      4

设置了一个随机数种子,以确保结果可重复。在这种情况下,在插补值稳定之前,需要对数据框进行六次迭代。

kNN()函数类似,missForest()函数可以处理定量和分类数据。随机森林方法需要中等或大型数据集(例如,500 个以上案例)以避免过拟合的问题。对于较小的数据集,kNN方法往往更有效。

如果假设检验是分析的重点,那么使用一种考虑缺失值引入的不确定性的插补方法是个好主意。多重插补就是这样一种方法。

18.8 多重插补

多重 插补(MI)提供了一种基于重复模拟的缺失值处理方法。MI 通常是复杂缺失值问题的首选方法。在 MI 中,从具有缺失值的存在数据集中生成一组完整的数据集(通常是 3 到 10 个)。蒙特卡洛方法用于填充每个模拟数据集中的缺失数据。对每个模拟数据集应用标准统计方法,并将结果合并,以提供考虑缺失值引入的不确定性的估计结果和置信区间。R 中有良好的实现,例如Ameliamicemi包。

在本节中,我们将重点关注mice(通过链式方程的多变量插补)包提供的方法。为了理解mice包的工作原理,请考虑图 18.6 中的示意图。

图 18.6 应用方法对缺失数据进行多重插补的步骤

图 18.6 应用mice方法对缺失数据进行多重插补的步骤

mice()函数从缺失数据的数据框开始,返回一个包含多个完整数据集的对象(默认为五个)。每个完整数据集是通过在原始数据框中插补缺失数据创建的。插补有一个随机成分,因此每个完整数据集略有不同。然后使用with()函数依次对每个完整数据集应用统计模型(例如,线性或广义线性模型)。最后,pool()函数将单独分析的结果合并为单个结果集。这个最终模型中的标准误差和 p 值正确地反映了缺失值和多重插补产生的不确定性。

mice()函数是如何插补缺失值的?

缺失值通过Gibbs 抽样进行插补。默认情况下,每个有缺失值的变量都是根据数据集中所有其他变量进行预测的。这些预测方程用于插补缺失数据的可能值。该过程会迭代,直到在缺失值上达到收敛。对于每个变量,你可以选择预测模型的格式(称为基本插补方法)以及进入它的变量。

默认情况下,预测均值匹配用于替换连续变量的缺失数据,而对于目标变量是 二元的(具有两个水平的因子)或 多分类的(具有超过两个水平的因子),分别使用逻辑回归或多项逻辑回归。其他基本插补方法包括贝叶斯线性回归、判别函数分析、两级正态插补和从观测值中进行随机抽样。您也可以提供自己的方法。

基于 mice 包的分析通常符合以下结构:

library(mice)
imp <- mice(*data, m*)
fit <- with(imp, *analysis*)
pooled <- pool(fit)
summary(pooled)

其中

  • data 是一个包含缺失值的矩阵或数据框。

  • imp 是一个包含 m 个插补数据集的列表对象,以及有关如何进行插补的信息。默认情况下,m = 5。

  • analysis 是一个公式对象,指定要对每个 m 个插补数据集应用哪种统计分析。例如,lm() 用于线性回归模型,glm() 用于广义线性模型,gam() 用于广义加性模型。括号内的公式给出了 ~ 左侧的响应变量和右侧的预测变量(由 + 符号分隔)。

  • fit 是一个包含 m 个单独统计分析结果的列表对象。

  • pooled 是一个包含这些 m 个统计分析平均结果的列表对象。

让我们将多重插补应用于 sleep 数据集。您将重复第 18.6 节的分析,但这次使用所有 62 种哺乳动物。将随机数生成器的种子值设置为 1,234,以便您的结果将与以下结果匹配:

> library(mice)
> data(sleep, package="VIM")
> imp <- mice(sleep, seed=1234)

 [...output deleted to save space...]

> fit <- with(imp, lm(Dream ~ Span + Gest))
> pooled <- pool(fit)
> summary(pooled)
         term estimate std.error statistic   df  p.value
1 (Intercept)  2.59669   0.24861    10.445 52.0 2.29e-14
2        Span -0.00399   0.01169    -0.342 55.6 7.34e-01
3        Gest -0.00432   0.00146    -2.961 55.2 4.52e-03

在这里,您可以看到 Span 的回归系数并不显著(p ≅ 0.07),而 Gest 的系数在 p < 0.01 水平上是显著的。如果您将这些结果与第 18.6 节中产生的完整案例分析结果进行比较,您会发现在这种情况下您会得出相同的结论。在控制生命周期的条件下,妊娠长度与梦境睡眠量之间存在(统计上)显著的负相关关系。尽管完整案例分析是基于具有完整数据的 42 种哺乳动物,但当前分析是基于从完整的 62 种哺乳动物中收集的信息。

您可以通过查看 imp 对象的子组件来查看插补。例如,

> imp$imp$Dream
     1   2   3   4   5
1  0.0 0.5 0.5 0.5 0.3
3  0.5 1.4 1.5 1.5 1.3
4  3.6 4.1 3.1 4.1 2.7
14 0.3 1.0 0.5 0.0 0.0
24 3.6 0.8 1.4 1.4 0.9
26 2.4 0.5 3.9 3.4 1.2
30 2.6 0.8 2.4 2.2 3.1
31 0.6 1.3 1.2 1.8 2.1
47 1.3 1.8 1.8 1.8 3.9
53 0.5 0.5 0.6 0.5 0.3
55 2.6 3.6 2.4 1.8 0.5
62 1.5 3.4 3.9 3.4 2.2

显示了 12 种哺乳动物中 Dream 变量缺失数据的 5 个插补值。审查这些矩阵有助于您确定插补值是否合理。睡眠长度为负值可能会让您感到惊讶(或噩梦)。

您可以通过 complete() 函数查看每个 m 个插补数据集。格式是

complete(imp, action=#) 

其中 # 指定了 m 个合成的完整数据集之一。例如,

> dataset3 <- complete(imp, action=3)
> dataset3
    BodyWgt BrainWgt NonD Dream Sleep  Span  Gest Pred Exp Danger
1  6654.000  5712.00  3.2   0.5   3.3  38.6 645.0    3   5      3
2     1.000     6.60  6.3   2.0   8.3   4.5  42.0    3   1      3
3     3.385    44.50 11.0   1.5  12.5  14.0  60.0    1   1      1
4     0.920     5.70 13.2   3.1  16.5   7.0  25.0    5   2      3
5  2547.000  4603.00  2.1   1.8   3.9  69.0 624.0    3   5      4
6    10.550   179.50  9.1   0.7   9.8  27.0 180.0    4   4      4
[...output deleted to save space...]

显示了由多重插补过程创建的五个完整数据集中的第三个。

由于篇幅限制,我们只简要考虑了mice包中提供的 MI 实现。miAmelia包也包含有价值的方法。如果你对缺失数据的多重插补方法感兴趣,我推荐以下资源:

  • 由 Martijn W. Heymans 和 Iris Eekhout 编写的在线书籍《使用 SPSS 和(R)Studio 的缺失数据应用分析》(bookdown.org/mwheymans/bookmi/)

  • van Buuren 和 Groothuis-Oudshoorn(2010)以及 Yu-Sung、Gelman、Hill 和 Yajima(2010)的文章

  • “Amelia II: 缺失数据程序” (gking.harvard.edu/amelia)

每个都可以帮助你加强和扩展对这个重要但未充分利用的方法论的理解。

18.9 缺失数据的其他方法

R 支持处理缺失数据的其他几种方法。尽管这些方法不像前面描述的方法那样广泛适用,但表 18.2 中描述的包提供了在特定情况下可能有用的函数。

表 18.2 处理缺失数据的专用方法

描述
norm 多变量正态数据缺失值的最大似然估计
cat 分析具有缺失值的分类变量数据集
longitudinalData 包括用于填充缺失时间序列值的插值例程等实用函数
kmi Kaplan-Meier 多重插补,用于带有缺失数据的生存分析
mix 混合分类和连续数据的多重插补
pan 多变量面板或聚类数据的多重插补

请参阅 CRAN 缺失数据任务视图(cran.r-project.org/web/views/MissingData.html),以了解 R 提供的所有方法的综述。

摘要

  • 统计方法需要完整的数据集,但大多数现实世界的数据都包含缺失值。

  • VIMmice包中的函数可用于探索数据集中缺失值的分布。

  • 列删除是处理缺失值最流行的方法,并且是许多统计程序的默认方法。如果删除大量数据,可能会导致功效损失。

  • kNNmissForest是填充缺失值的优秀方法。前者适用于中小型数据集,而后者适用于大型数据集。

  • 多重插补通过模拟来处理缺失值给统计推断问题带来的不确定性。

  • 除非缺失数据量非常小,否则通常应避免简单的插补(例如,均值替换)和成对删除。

第五部分. 扩展你的技能

在本节的最后部分,我们将探讨一些高级主题,这些主题将提升你作为 R 程序员的能力。第十九章详细介绍了如何使用 ggplot2 包自定义图形,从而完成了我们对图形的讨论。你将学习如何修改图形的标题、标签、坐标轴、颜色、字体、图例等。你还将学习如何将多个图形组合成一个整体图像,并将静态图形转换为交互式网络图形。

第二十章从更深入的角度回顾了 R 语言。这包括对 R 的面向对象编程特性、环境操作和高级函数编写的讨论。还提供了编写高效代码和调试程序的技巧。尽管第二十章比本书中的其他章节更技术性,但它提供了大量实用的建议,用于开发更有用的程序。

第二十一章完全关于报告编写。R 提供了强大的功能,可以从数据动态生成吸引人的报告。在本章中,你将学习如何创建网页、PDF 文档和文字处理文档(包括 Microsoft Word 文档)形式的报告。

在整本书中,你已经使用了各种包来完成工作。在第二十二章中,你将学习如何编写你自己的包。这可以帮助你组织和记录你的工作,创建更复杂和全面的软件解决方案,并与他人分享你的成果。与他人分享一个有用的函数包也可以是一种回馈 R 社区的美妙方式(同时让你的名声远播)。

完成第五部分后,你将对 R 的工作原理以及它提供的用于创建更复杂图形、软件和报告的工具有更深的理解。

19 高级图表

本章涵盖

  • 自定义 ggplot2 图表

  • 添加注释

  • 将多个图表组合成单个图表

  • 创建交互式图表

在 R 中创建图表有许多方法。我们专注于 ggplot2 的使用,因为它具有一致的语法、灵活性和全面性。ggplot2 包在第四章中介绍,涵盖了 Geoms、缩放、分面和标题。在第六章中,我们创建了条形图、饼图、树状图、直方图、核密度图、箱线和小提琴图以及点图。第八章和第九章涵盖了回归和方差分析模型的图形。第十一章讨论了散点图、散点图矩阵、气泡图、折线图、相关图和马赛克图。其他章节涵盖了可视化当前主题的图表。

本章将继续介绍 ggplot2,但重点是自定义——创建一个精确满足您需求的图表。图表有助于您发现模式并描述趋势、关系、差异、组成和数据的分布。自定义 ggplot2 图表的主要原因是增强您探索数据或向他人传达发现的能力。次要目标是满足组织或出版商的外观和感觉要求。

在本章中,我们将探讨使用 ggplot2 缩放函数来自定义坐标轴和颜色。我们将使用 theme() 函数来自定义图表的整体外观和感觉,包括文本、图例、网格线和绘图背景的显示。我们将使用 Geoms 来添加注释,例如参考线和标签。此外,还将使用 patchwork 包将多个图表组合成一个完整的图表。最后,将使用 plotly 包将静态的 ggplot2 图表转换为交互式网络图形,让您更全面地探索数据。

ggplot2 包提供了大量自定义图表元素的选择。仅 theme() 函数就有超过 90 个参数!在这里,我们将重点关注最常用的函数和参数。如果您正在以灰度阅读本章,我鼓励您运行代码,以便您可以看到彩色图表。使用简单的数据集,这样您可以专注于代码本身。

到目前为止,您应该已经安装了 ggplot2dplyr 包。在继续之前,您还需要几个额外的包,包括用于数据的 ISLRgapminder,以及用于增强图形的 ggrepelshowtextpatchworkplotly。您可以使用 install.packages(c("ISLR", "gapminder", "scales", "showtext", "ggrepel", "patchwork", "plotly")) 安装它们。

19.1 修改缩放

ggplot2 中的尺度函数控制变量值到特定绘图特性的映射。例如,scale_x_continuous() 函数创建了一个将定量变量的值映射到 x-轴上位置的映射。scale_color_discrete() 函数创建了一个将分类变量的值与颜色值之间的映射。在本节中,您将使用尺度函数来自定义图表的轴和绘图颜色。

19.1.1 自定义轴

ggplot2 中,图表中的 x-轴和 y-轴由 scale_x_*scale_y_* 函数控制,其中 * 指定尺度的类型。表 19.1 列出了最常见的函数。自定义这些轴的主要原因是使数据更容易阅读或使趋势更明显。

表 19.1 指定轴尺度的函数

函数 描述
scale_x_continuous, scale_y_continuous 连续数据的尺度
scale_x_binned, scale_y_binned 对连续数据进行分箱的尺度
scale_x_discrete,scale_y_discrete 对离散(分类)数据的尺度
scale_x_log10, scale_y_log10 对数尺度(基数为 10)上的连续数据尺度
scale_x_date, scale_y_date 日期数据的尺度。其他变体包括日期时间和时间。

自定义连续变量的轴

在第一个例子中,我们将使用 mtcars 数据框,这是一个包含 32 辆汽车特征的数据库集。mtcars 数据框包含在基础 R 中。让我们绘制以 1000 磅为单位的汽车重量 (wt) 与燃油效率 (mpg) 的关系图:

library(ggplot2)
ggplot(data = mtcars, aes(x = wt, y = mpg)) +
  geom_point() +
  labs(title = "Fuel efficiency by car weight") 

图 19.1 显示了图表。默认情况下,主要刻度有标签。对于 mpg,这些刻度在 10 到 35 之间以 5 点间隔出现。小刻度在主要刻度之间均匀分布,但没有标签。

图 19.1 默认的 ggplot2 散点图,显示 mtcars 数据集中的 32 辆汽车每加仑英里数(1000 磅)与汽车重量之间的关系

在这个图中,最重的汽车的重量是多少?第三轻的汽车的 mpg 是多少?从这些轴上确定值需要一些工作。我们可能想要调整 x-轴和 y-轴,以便更容易从图中读取值。

由于 wtmpg 是连续变量,我们将使用 scale_x_continuous()scale_y_continuous() 函数来修改轴。表 19.2 列出了这些函数的常见选项。

表 19.2 一些常见的 scale_*_continuous 选项

参数 描述
name 尺度的名称。与使用 labs(x = , y = ) 函数相同。
breaks 主要刻度标记位置的数值向量。主要刻度自动标记,除非被 labels 选项覆盖。使用 NULL 来抑制刻度。
minor_breaks 小刻度标记的位置的数值向量。小刻度没有标签。使用 NULL 来抑制小刻度。
n.breaks 整数,指导主要断点的数量。该数值被视为建议。函数可能会根据需要调整此数值以确保吸引人的断点标签。
labels 字符向量,提供替代断点标签(必须与断点长度相同)
limits 长度为 2 的数值向量,给出最小和最大值
position 轴放置(对于y轴为左/右,对于x轴为上/下)

让我们进行以下更改。对于重量,

  • 将轴标签为“重量(1000 磅)。”

  • 将刻度范围设置为 1.5 到 5.5。

  • 使用 10 个主要断点。

  • 抑制次要断点。

对于每加仑英里数,

  • 将轴标签为“每加仑英里数。”

  • 将刻度范围设置为 10 到 35。

  • 在 10、15、20、25、30 和 35 处放置主要断点。

  • 在每加仑间隔处放置次要断点。

下面的列表给出了代码。

列表 19.1 使用定制轴绘制燃油效率与汽车重量关系图

library(ggplot2)
ggplot(mtcars, aes(x = wt, y = mpg)) + 
  geom_point() +
  scale_x_continuous(name = "Weight (1000 lbs.)",   ❶
                     n.breaks = 10,                 ❶
                     minor_breaks = NULL,           ❶
                     limits = c(1.5, 5.5)) +        ❶
  scale_y_continuous(name = "Miles per gallon",     ❷
                     breaks = seq(10, 35, 5),       ❷
                     minor_breaks = seq(10, 35, 1), ❷
                     limits = c(10, 35)) +          ❷
  labs(title = "Fuel efficiency by car weight")

❶ 修改x

❷ 修改y

图 19.2 显示了新的图表。我们可以看到最重的汽车大约有 5.5 吨,第三轻的汽车每加仑获得 34 英里。请注意,您为wt指定了 10 个主要断点,但图表只有 9 个。n.breaks参数被视为建议。如果它提供了更好的标签,则参数可能被替换为接近的数值。我们将在稍后继续使用此图表。

图 19.2 ggplot2根据汽车重量(1000 磅)绘制的每加仑英里数散点图,已修改 x 轴和 y 轴。现在更容易读取点的值。

为分类变量定制轴

之前的例子涉及为连续变量定制轴。在下一个例子中,您将定制分类变量的轴。数据来自ISLR包中的Wage数据框。该数据框包含 2011 年在美国中西部地区收集的 3000 名男性工人的工资和人口统计信息。让我们绘制此样本中种族与教育之间的关系。代码如下

library(ISLR)
library(ggplot2)
ggplot(Wage, aes(race, fill = education)) +
  geom_bar(position = "fill") +
  labs(title = "Participant Education by Race")

和图 19.3 显示了图表。

图 19.3 2011 年 3000 名中西部男性工人按种族划分的教育样本

注意,比赛和教育标签上的编号实际上是在数据中编码的:

> head(Wage[c("race", "education")], 4)
           race       education
231655 1\. White    1\. < HS Grad
86582  1\. White 4\. College Grad
161300 1\. White 3\. Some College
155159 3\. Asian 4\. College Grad

我们可以通过删除种族类别标签上的数字(它们不是有序类别)、在y轴上使用百分比格式、使用更好的刻度标签以及按百分比重新排序种族类别(优先级更高)来改进图表。您还可能希望删除“种族:其他”类别,因为该群体的组成是未知的。

修改分类变量的刻度涉及使用scale_*_discrete()函数。表 19.3 列出了常见选项。您可以使用limits参数(和/或省略)对离散值进行排序,并使用labels参数更改它们的标签。

表 19.3 一些常见的 scale_*_discrete 选项

参数 描述
name 尺度名称。与使用 labs(x = , y = ) 函数相同。
breaks 一个包含刻度的字符向量
limits 一个定义刻度值及其顺序的字符向量
labels 一个字符向量,给出标签(长度必须与 breaks 相同)。使用 labels=abbreviate 将缩短长标签。
position 轴放置(对于 y-轴是左/右,对于 x-轴是上/下)

以下列表给出了修订后的代码,图 19.4 展示了图表。

列表 19.2 按种族划分的教育绘图,具有自定义轴

library(ISLR)
library(ggplot2)
library(scales)
ggplot(Wage, aes(race, fill=education)) +
  geom_bar(position="fill") +
  scale_x_discrete(name = "",                                       ❶
                   limits = c("3\. Asian", "1\. White", "2\. Black"),  ❶
                   labels = c("Asian", "White", "Black")) +         ❶
  scale_y_continuous(name = "Percent",                              ❷
                     label = percent_format(accuracy=2),            ❷
                     n.breaks=10) +                                 ❷
  labs(title="Participant Education by Race")

❶ 修改 x-轴

❷ 修改 y-轴

水平轴表示一个分类变量,因此它使用 scale_x_discrete() 函数进行自定义。使用 limits 对比赛类别进行重新排序,并使用 labels 进行重新标记。其他类别通过在这些规范中省略而被从图中省略。通过将名称设置为 "" 来删除轴标题。

垂直轴表示一个数值变量,因此它使用 scale_y_continuous() 函数进行自定义。该函数用于修改轴标题和更改轴标签。scales 包中的 percent_format() 函数重新格式化轴标签为百分比。accuracy=2 参数指定每个百分比打印的显著数字位数。

scales 包在格式化轴方面非常有用。有格式化货币值、日期、百分比、逗号、科学记数法等选项。有关详细信息,请参阅 scales.r-lib.org/ggh4xggprism 包提供了额外的自定义轴功能,包括对主刻度和副刻度的更多自定义。

在前面的示例中,教育被表示为一个离散的颜色刻度。我们将考虑自定义颜色。

图 19.4 2011 年 3000 名中大西洋地区男性工人的参与教育按种族划分。种族类别已重新排列和重新标记。其他类别已被省略。x-轴标签已被省略,y-轴现在格式化为百分比。

19.1.2 自定义颜色

ggplot2 包提供了将分类和数值变量映射到颜色方案的功能。表 19.4 描述了这些函数。scale_color_*() 函数用于点、线、边框和文本。scale_fill_*() 函数用于具有面积的对象,如矩形和椭圆。

调色板可以是顺序的发散的定性的。顺序调色板用于将颜色映射到单调的数值变量。发散调色板用于具有有意义的中点或零点的数值变量。它是两个共享中值端点的顺序调色板。例如,发散调色板通常用于表示相关系数的值(见第 11.3 节)。定性的颜色标尺将分类变量的值映射到离散颜色。

表 19.4 指定颜色尺度的函数

函数 描述
scale_color_gradient()``scale_fill_gradient() 连续变量的渐变色标尺。指定low颜色和high颜色。使用*_gradient2()版本来指定lowmidhigh颜色。
scale_color_steps()``scale_fill_steps() 连续变量的分箱渐变色标尺。指定low颜色和high颜色。使用*_steps2()版本来指定lowmidhigh颜色。
scale_color_brewer()``scale_fill_brewer() 来自 ColorBrewer(colorbrewer2.org)的顺序、发散和定性颜色方案。主要参数是palette=。查看?scale_color_brewer以获取调色板列表。
scale_color_grey()``scale_fill_gray() 顺序灰色颜色标尺。可选参数是start(低端的灰色值)和end(高端的灰色值)。默认值分别为 0.2 和 0.8。
scale_color_manual()``scale_fill_manual() 通过在值参数中指定颜色向量来为离散变量创建自己的颜色标尺。
scale_color_virdis_*``scale_fill_virdis_* 来自viridisLite包的 Viridis 颜色标尺。旨在被具有常见形式色盲的观众所感知,并且黑白打印效果良好。使用*_d表示离散,*_c表示连续,*_b表示分箱标尺。例如,scale_fill_virdis_d()将提供对离散变量的安全颜色填充。option参数提供四种颜色方案变化("inferno""plasma""viridis"(默认)和"cividis")。

连续颜色调色板

让我们看看将连续定量变量映射到颜色调色板的示例。在图 19.1 中,燃油效率被绘制在汽车重量上。我们将通过将发动机排量映射到点颜色来向图表中添加第三个变量。由于发动机排量是一个数值变量,你创建一个颜色渐变来表示其值。以下列表展示了几个可能性。

列表 19.3 连续变量的颜色渐变

library(ggplot2)
p <- ggplot(mtcars, aes(x=wt, y=mpg, color=disp)) +
  geom_point(shape=19, size=3) +
  scale_x_continuous(name = "Weight (1000 lbs.)",
                     n.breaks = 10,
                     minor_breaks = NULL,
                     limits=c(1.5, 5.5)) +
  scale_y_continuous(name = "Miles per gallon",
                     breaks = seq(10, 35, 5),
                     minor_breaks = seq(10, 35, 1),
                     limits = c(10, 35))

p + ggtitle("A. Default color gradient")

p + scale_color_gradient(low="grey", high="black") +
  ggtitle("B. Greyscale gradient")

p + scale_color_gradient(low="red", high="blue") +
  ggtitle("C. Red-blue color gradient")

p + scale_color_steps(low="red", high="blue") +
  ggtitle("D. Red-blue binned color Gradient")

p + scale_color_steps2(low="red", mid="white", high="blue",
                                   midpoint=median(mtcars$disp)) +
  ggtitle("E. Red-white-blue binned gradient")

p + scale_color_viridis_c(direction = -1) +
  ggtitle("F. Viridis color gradient")

代码创建图 19.5 中的图表。ggtitle()函数等同于本书其他地方使用的labs(title=)。如果你正在阅读本书的灰度版本,请确保亲自运行代码,以便你能欣赏到颜色变化。

图 19.5 展示了汽车重量与燃油效率的关系图。颜色用于表示发动机排量。展示了六种配色方案。A 是默认方案。B 是灰度。C 和 D 从红色到蓝色渐变,但 D 被分成了五个离散颜色。E 从红色到白色(中位数)到蓝色渐变。F 使用了 Viridis 颜色方案。在每个图表中,发动机排量随着汽车重量的增加而增加,油耗降低。

图 A 使用了ggplot2的默认颜色。图 B 显示了灰度图。图 C 和 D 使用红色到蓝色的渐变。分箱颜色渐变将连续渐变分成离散值(通常是五个)。图 E 展示了发散颜色渐变,从红色(低)到白色(中点)到蓝色(高)。最后,图 F 展示了 Viridis 颜色渐变。图 F 中的direction = -1选项反转了颜色锚点,导致更大的发动机排量对应更深的颜色。

定性颜色调色板

下面的列表展示了定性颜色方案。在这里education是映射到离散颜色的分类变量。图 19.6 提供了结果图表。

列表 19.4 分类变量的颜色方案

library(ISLR)
library(ggplot2)
p <- ggplot(Wage, aes(race, fill=education)) +
  geom_bar(position="fill") +
  scale_y_continuous("Percent", label=scales::percent_format(accuracy=2),
                     n.breaks=10) +
  scale_x_discrete("",
                   limits=c("3\. Asian", "1\. White", "2\. Black"),
                   labels=c("Asian", "White", "Black"))

p + ggtitle("A. Default colors")

p + scale_fill_brewer(palette="Set2") +
     ggtitle("B. ColorBrewer Set2 palette")

p + scale_fill_viridis_d() +
  ggtitle("C. Viridis color scheme")

p + scale_fill_manual(values=c("gold4", "orange2", "deepskyblue3", 
                               "brown2", "yellowgreen")) +
  ggtitle("D. Manual color selection")

图 A 使用了ggplot2的默认颜色。图 B 使用了 ColorBrewer 的定性调色板 Set2。其他定性 ColorBrewer 调色板包括 Accent、Dark2、Paired、Pastel1、Pastel2、Set1 和 Set3。图 C 展示了默认的 Viridis 离散方案。最后,图 D 展示了手动方案,证明我自己挑选颜色是不合适的。

R 包为ggplot2图表提供了广泛的各种颜色调色板。Emil Hvitfeldt 创建了一个综合的仓库在github.com/Emil Hvitfeldt/r-color-palettes(最后统计有近 600 种!)!选择您认为吸引人且能最有效地传达信息的方案。读者能否轻松地看到您试图强调的关系、差异、趋势、组成或异常?

图 19.6 展示了 2011 年 3000 名中大西洋地区男性工人的种族参与教育情况。显示了四种不同的配色方案。A 是默认方案。B 和 C 是预设的配色方案。在 D 中,颜色由用户指定。

19.2 修改主题

ggplot2 theme()函数允许您自定义图表的非数据组件。该函数的帮助文档(?theme)描述了用于修改图表标题、标签、字体、背景、网格线和图例的参数。

例如,在以下代码中

ggplot(mtcars, aes(x = wt, y = mpg)) +
  geom_point()+ 
  theme(axis.title = element_text(size = 14, color = "blue"))

theme()函数以 14 点蓝色字体渲染x轴和y轴的标题。函数通常用于提供theme参数的值(见表 19.5)。

表 19.5 主题元素

函数 描述
element_blank() 空白元素(用于移除文本、线条等)。
element_rect() 指定矩形特征。参数包括fillcolorsizelinetype。最后三个与边框相关。
element_line() 指定线条特征。参数包括colorsizelinetypelineend"round""butt""square")和arrow(使用grid::arrow()函数创建)。
element_text() 指定文本特征。参数包括family(字体家族)、face"plain""italic""bold""bold.italic")、size(以磅为单位的文本大小)、hjust([0,1]中的水平对齐)、vjust([0,1]中的垂直对齐)、angle(以度为单位的角)和color

首先,我们将查看一些预配置的主题,这些主题可以同时更改多个元素,以提供一致的外观和感觉。然后我们将深入了解自定义单个主题元素。

19.2.1 预包装主题

ggplot2包包含八个预配置的主题,可以通过theme_*()函数应用于ggplot2图形。列表 19.5 和图 19.7 展示了其中四个最受欢迎的。theme_grey()函数是默认主题,而theme_void()创建了一个完全空的主题。

图像

图 19.7 四个预配置主题的示例。默认情况下,ggplot2使用theme_grey()

列表 19.5 四个预配置ggplot2主题的演示

library(ggplot2)
p <- ggplot(data = mtcars, aes(x = wt, y = mpg)) +
  geom_point() +
  labs(x = "Weight (1000 lbs)",
       y = "Miles per gallon")

p + theme_grey() + labs(title = "theme_grey")  
p + theme_bw() + labs(title = "theme_bw")  
p + theme_minimal() + labs(title = "theme_minimal")  
p + theme_classic() + labs(title = "theme_classic")

ggthemeshbrthemesxaringanthemertgamthemecowplottvthemesggdark包提供了额外的主题。每个都可以从 CRAN 获取。此外,一些组织为员工提供预配置的主题,以确保报告和演示文稿的外观一致。

除了预配置的主题外,您还可以修改单个主题元素。在以下章节中,您将使用主题参数来自定义字体、图例和其他图形元素。

19.2.2 自定义字体

使用排版来帮助传达意义,同时不会分散或混淆读者的注意力(见mng.bz/5Z1q)。例如,Google 的 Roboto 和 Lora 字体通常被推荐用于清晰度。基础 R 具有有限的本地字体处理能力。showtext包大大扩展了这些能力,允许您向图形添加系统字体和 Google Fonts。

步骤是

  1. 加载本地和/或 Google 字体。

  2. showtext设置为输出图形设备。

  3. ggplot2theme()函数中指定字体。

当考虑本地字体时,位置、数量和类型在计算机之间有很大差异。要使用除 R 默认字体之外的本地字体,您需要知道系统上字体文件的名称和位置。目前支持包括 TrueType 字体(*.ttf, .ttc)和 OpenType 字体(.otf)在内的格式。

font_paths() 函数列出了字体文件的位置,而 font_files() 列出了字体文件及其特性。列表 19.6 提供了一个简短的函数,用于在您的本地系统上定位字体文件。在这里,该函数用于定位 Comic Sans MS 字体的字体文件。由于结果取决于您的系统(我在使用 Windows PC),您的结果可能会有所不同。

列表 19.6 定位本地字体文件

> findfont <- function(x){
    suppressMessages(require(showtext))
    suppressMessages(require(dplyr))
    filter(font_files(), grepl(x, family, ignore.case=TRUE)) %>%
      select(path, file, family, face)
   }

> findfont("comic")

                path        file        family        face
1 C:/Windows/Fonts   comic.ttf Comic Sans MS     Regular
2 C:/Windows/Fonts comicbd.ttf Comic Sans MS        Bold
3 C:/Windows/Fonts  comici.ttf Comic Sans MS      Italic
4 C:/Windows/Fonts  comicz.ttf Comic Sans MS Bold Italic

一旦定位了本地字体文件,请使用 font_add() 将它们加载。例如,在我的机器上

font_add("comic", regular = "comic.ttf", 
          bold = "comicbd.ttf", italic="comici.ttf")

将 Comic Sans MS 字体在 R 中以任意名称“comic”提供。

要加载 Google 字体 (fonts.google.com/),请使用以下语句

font_add_google(name, family)

其中 name 是 Google 字体的名称,而 family 是您将在后续代码中使用的任意名称。例如,

font_add_google("Schoolbell", "bell")

在名称 bell 下加载了 Schoolbell Google 字体。

一旦加载了字体,showtext_auto() 语句将 showtext 设置为新图形的输出设备。

最后,使用 theme() 函数来指示图表的哪些元素将使用哪些字体。表 19.6 列出了与文本相关的主题参数。您可以使用 element_text() 指定字体家族、外观、大小、颜色和方向。

表 19.6 theme() 与文本相关的参数

参数 描述
axis.title, axis.title.x, axis.title.y 轴标题
axis.textaxis.title 相同的变体 轴上的刻度标签
legend.text, legend.title 图例项标签和图例标题
plot.title, plot.subtitle, plot.caption 图表标题、副标题和说明
strip.text, strip.text.x, strip.text.y 分面标签

以下列表演示了使用来自我的机器的两个本地字体(Comic Sans MS 和 Caveat)和两个 Google 字体(Schoolbell 和 Gochi Hand)自定义ggplot2图表。图 19.8 显示了该图表。

图 19.8 展示了使用多种字体的图表(标题使用 Schoolbell,副标题使用 Gochi Hand,来源使用 Caveat,轴标题和文本使用 Comic Sans MS)

列表 19.7 在ggplot2图表中自定义字体

library(ggplot2)
library(showtext)

font_add("comic", regular = "comic.ttf",                             ❶
         bold = "comicbd.ttf", italic="comici.ttf")                  ❶
font_add("caveat", regular = "caveat-regular.ttf",                   ❶
         bold = "caveat-bold.ttf")                                   ❶

font_add_google("Schoolbell", "bell")                                ❷
font_add_google("Gochi Hand", "gochi")                               ❷

showtext_auto()                                                      ❸

ggplot(data = mtcars, aes(x = wt, y = mpg)) +
  geom_point() +
  labs(title = "Fuel Efficiency by Car Weight",
       subtitle = "Motor Trend Magazine 1973",
       caption = "source: mtcars dataset",
       x = "Weight (1000 lbs)",
       y = "Miles per gallon") +

   theme(plot.title    = element_text(family = "bell", size=14),    ❹
         plot.subtitle = element_text(family = "gochi"),            ❹
         plot.caption  = element_text(family = "caveat", size=15),  ❹
         axis.title    = element_text(family = "comic"),            ❹
         axis.text     = element_text(family = "comic",             ❹
                                      face="italic", size=8))       ❹

❶ 加载本地字体

❷ 加载 Google 字体

❸ 使用 showtext 作为图形设备

❹ 指定图表字体

图 19.8 展示了生成的图表,该图表仅用于演示目的。在单个图表中使用多种字体往往会分散注意力,并削弱图表旨在传达的信息。选择一种或两种最能突出信息的字体,并坚持使用。一个有用的起点是 Tiffany France 的为您的数据可视化选择字体 (mng.bz/nrY5)。

19.2.3 自定义图例

ggplot2包在变量映射到颜色、填充、形状、线型或大小(基本上是不涉及位置尺度的任何缩放)时创建图例。您可以使用表 19.7 中的theme()参数修改图例的外观。

最常用的参数是 legend.position。将参数设置为 topright(默认)、bottomleft 允许您将图例放置在图的任何一边。或者,一个包含两个元素的数值向量(xy)将图例放置在 x 轴和 y 轴上,其中 x 坐标范围从 0-左到 1-右,y 坐标范围从 0-底到 1-顶。

表 19.7 与绘图图例相关的 theme() 参数

参数 描述
legend.background, legend.key 图例和图例键(符号)的背景。使用 element_rect() 指定。
legend.title, legend.text 图例标题和文本的文本特征。使用 element_text() 指定值。
legend.position 图例的位置。值是 "none""left""right""bottom""top" 或两个元素的数值向量(每个介于 0-左/底和 1-右/顶之间)。
legend.justification 如果 legend.position 使用一个包含两个元素的数值向量设置,legend.justification 提供图例内的 锚点,作为一个包含两个元素的向量。例如,如果 legend.position = c(1, 1)legend.justification = c(1, 1),则锚点是图例的右下角。此锚点放置在图的右上角。
legend.direction 图例方向为 “horizontal”“vertical”
legend.title.align, legend.text.align 图例标题和文本的对齐方式(从 0-左到 1-右的数字)。

让我们为 mtcars 数据框创建一个散点图。将 wt 放在 x 轴上,mpg 放在 y 轴上,并根据发动机气缸数对点进行着色。使用表 19.7,通过以下方式自定义图表:

  • 将图例放置在图的右上角

  • 为图例命名“圆柱体”

  • 水平列出图例类别而不是垂直列出

  • 将图例背景设置为浅灰色并移除键元素(彩色符号)周围的背景

  • 在图例周围放置白色边框

以下列表提供了代码,图 19.9 显示了生成的图表。

列出 19.8 自定义图例

library(ggplot2)
ggplot(mtcars, aes(wt, mpg, color = factor(cyl))) +
         geom_point(size=3) +
  scale_color_discrete(name="Cylinders") +
  labs(title = "Fuel Efficiency for 32 Automobiles",
       x = "Weight (1000 lbs)",
       y = "Miles per gallon") +
  theme(legend.position = c(.95, .95),
        legend.justification = c(1, 1),
        legend.background = element_rect(fill = "lightgrey",
                                         color = "white",
                                         size = 1),
        legend.key = element_blank(),
        legend.direction = "horizontal")

图 19.9 带有自定义图例的图表。图例的右上角放置在图的左上角。图例水平打印,带有灰色背景、实线白色边框和标题。

再次,这个图表是为了演示目的提供的。如果它放在右侧并垂直(在这种情况下是默认设置),实际上更容易将图例与图表相关联。参见 数据可视化标准 (mng.bz/6m15) 以获取有关图例格式的建议。

19.2.4 自定义绘图区域

表 19.8 中的 theme() 参数允许您自定义绘图区域。最常见的变化是背景颜色和主网格线和副网格线。列表 19.9 演示了为分面散点图自定义绘图区域许多功能的示例。图 19.10 展示了结果图。

表 19.8 与绘图区域相关的 theme() 参数

参数 描述
plot.background 整个图的背景。使用 element_rect() 指定。
plot.margin 整个图周围的边距。使用 units() 函数和大小指定顶部、右侧、底部和左侧边距。
panel.background 绘图区域的背景。使用 element_rect() 指定。
strip.background 分面条标签的背景
panel.grid, panel.grid.major, panel.grid.minor, panel.grid.major.x panel.grid.major.y panel.grid.minor.x panel.grid.minor.y 网格线、主网格线、副网格线,或特定主网格线或副网格线。使用 element_line() 指定。
axis.line, axis.line.x, axis.line.y, axis.line.x.top, axis.line.x.bottom, axis.line.y.left, axis.line.y.right 轴上的线条(axis.line),每个平面的线条(axis.line.xaxis.line.y),或每个轴的单独线条(axis.line.x.bottom等)。使用 element_line() 指定。

列表 19.9 自定义绘图区域

library(ggplot2)
mtcars$am <- factor(mtcars$am, labels = c("Automatic", "Manual")) 
ggplot(data=mtcars, aes(x = disp, y = mpg)) +                     
  geom_point(aes(color=factor(cyl)), size=2) +                      ❶
  geom_smooth(method="lm", formula = y ~ x + I(x²),                ❷
              linetype="dotted", se=FALSE) +
  scale_color_discrete("Number of cylinders") +
  facet_wrap(~am, ncol=2) +                                         ❸
  labs(title = "Mileage, transmission type, and number of cylinders",
       x = "Engine displacement (cu. in.)",
       y = "Miles per gallon") +
  theme_bw() +                                                      ❹
  theme(strip.background = element_rect(fill = "white"),            ❺
        panel.grid.major = element_line(color="lightgrey"),
        panel.grid.minor = element_line(color="lightgrey",
                                        linetype="dashed"),
        axis.ticks = element_blank(),
        legend.position = "top",
        legend.key = element_blank())

❶ 分组散点图

❷ 拟合线

❸ 分面

❹ 设置黑白主题

❺ 修改主题

代码创建了一个图,其中发动机排量(disp)位于x轴上,每加仑英里数(mpg)位于y轴上。气缸数(cyl)和变速器类型(am)最初编码为数值,但转换为因子以进行绘图。对于cyl,转换为因子确保每个气缸数只有一个颜色。对于am,这提供了比 0 和 1 更好的标签。

创建了一个带有放大点的散点图,按气缸数着色❶。然后添加了一个最佳拟合的二次曲线❷。二次拟合线允许一条有弯曲的线(见第 8.2.3 节)。然后添加了每个变速器类型的分面图❸。

为了修改主题,我们首先使用 theme_bw()❹,然后使用 theme() 函数❺进行修改。条带背景色设置为白色。主网格线设置为实线浅灰色,副网格线设置为虚线浅灰色。轴刻度标记被移除。最后,图例放置在图的顶部,图例键(符号)具有空白背景。

图 19.10 带拟合线的分面散点图。最终主题基于黑白主题的修改版本。

19.3 添加注释

注释允许您向图中添加更多信息,使读者更容易识别关系、分布或异常观察。最常见的注释是参考线和文本标签。添加这些注释的函数列在表 19.9 中。

表 19.9 添加注释的函数

函数 描述
geom_text, geom_label geom_text() 向图表添加文本。geom_label() 类似,但会在文本周围绘制矩形。
geom_text_repel, geom_label_repel 这些是来自 ggrepel 包的函数。它们与 geom_text()geom_label() 类似,但可以避免文本重叠。
geom_hline, geom_vline, geom_abline 添加水平、垂直和对角参考线
geom_rect 向图中添加矩形。用于突出显示绘图中的区域。

标记点

在图 19.1 中,我们绘制了汽车重量 (wt) 与燃油效率 (mpg) 之间的关系。然而,读者无法确定哪些点代表哪些汽车,除非参考原始数据集。以下列表将此信息添加到图表中。图 19.11 显示了图表。

列表 19.10 标记点的散点图

library(ggplot2)
ggplot(data = mtcars, aes(x = wt, y = mpg)) +
  geom_point(color = "steelblue") +
  geom_text(label = row.names(mtcars)) +
  labs(title = "Fuel efficiency by car weight",
       x = "Weight (1000 lbs)",
       y = "Miles per gallon")

图 19.11 汽车重量与每加仑英里数的散点图。点用汽车名称标记。

由于文本重叠,生成的图表难以阅读。ggrepel 包通过重新定位文本标签以避免重叠来解决这个问题。我们将使用此包来标记点并重新创建图表。此外,我们还将添加一条参考线和标签,指示中位数每加仑英里数。以下列表提供了代码,图 19.12 显示了图表。

列表 19.11 使用 ggrepel 标记点的散点图

library(ggplot2)
library(ggrepel)
ggplot(data = mtcars, aes(x= wt, y = mpg)) +
  geom_point(color = "steelblue") +
  geom_hline(yintercept = median(mtcars$mpg),               ❶
             linetype = "dashed",                           ❶
             color = "steelblue") +                         ❶
  geom_label(x = 5.2, y = 20.5,                             ❷
             label = "median MPG",                          ❷
             color = "white",                               ❷
             fill = "steelblue",                            ❷
             size = 3) +                                    ❷
  geom_text_repel(label = row.names(mtcars), size = 3) +    ❸
  labs(title = "Fuel efficiency by car weight",
       x = "Weight (1000 lbs)",
       y = "Miles per gallon")

❶ 参考线

❷ 参考线标签

❸ 点标签

参考线指示哪些汽车高于或低于中位数每加仑英里数 ❶。该线使用 geom_label ❷ 标记。正确放置线标签 (x, y) 需要一些实验。最后,使用 geom_text_repel() 函数标记点 ❸。标签的大小也从默认的 4 减小到 3。此图表更容易阅读和理解。

图 19.12 汽车重量与每加仑英里数的散点图。点用汽车名称标记。ggrepel 包已用于重新定位标签以避免文本重叠。此外,还添加了一条参考线和标签。

标记条形图

可以向条形图添加标签以阐明分类变量的分布或堆叠条形图的组成。向每个条形添加百分比标签是一个两步过程。首先,计算每个条形的百分比。然后使用这些百分比直接创建条形图,并通过 geom_text() 函数添加标签。以下列表显示了此过程。图 19.13 显示了图表。

列表 19.12 向条形图添加百分比标签

library(ggplot2)
library(dplyr)
library(ISLR)

plotdata <- Wage %>%                                          ❶
  group_by(race) %>%                                          ❶
  summarize(n = n()) %>%                                      ❶
  mutate(pct = n / sum(n),                                    ❶
         lbls = scales::percent(pct),                         ❶
         race = factor(race, labels = c("White", "Black",     ❶
                                        "Asian", "Other")))   ❶

plotdata

## # A tibble: 4 x 4
##   race         n    pct lbl  
##   <fct>    <int>  <dbl> <chr>
## 1 1\. White  2480 0.827  82.7%
## 2 2\. Black   293 0.0977 9.8% 
## 3 3\. Asian   190 0.0633 6.3% 
## 4 4\. Other    37 0.0123 1.2%

ggplot(data=plotdata, aes(x=race, y=pct)) +
  geom_bar(stat = "identity", fill="steelblue") +             ❷
  geom_text(aes(label = lbls),                                ❸
            vjust = -0.5,                                     ❸
            size = 3) +                                       ❸
  labs(title = "Participants by Race",
       x = "", 
       y="Percent") +
  theme_minimal()

❶ 计算百分比

❷ 添加条形

❸ 添加条形标签

每个种族类别的百分比是计算出来的 ❶,并使用scales包中的percent()函数创建格式化的标签(lbls)。然后使用这些汇总数据创建条形图 ❷。geom_bar()函数中的stat = "identity"选项告诉ggplot2使用提供的y-值(条形高度),而不是计算它们。然后使用geom_text()函数打印条形标签 ❸。vjust = -0.5参数将文本略微抬高于条形上方。

图 19.13 带有百分比标签的简单条形图

你也可以在堆叠条形图中添加百分比标签。在下面的列表中,图 19.4 中的填充条形图被复制并添加了百分比标签。图 19.14 显示了最终的图形。

列表 19.13 向堆叠(填充)条形图添加百分比标签

library(ggplot2)
library(dplyr)
library(ISLR)

plotdata <- Wage %>%                                             ❶
  group_by(race, education) %>%                                  ❶
  summarize(n = n()) %>%                                         ❶
  mutate(pct = n/sum(n),                                         ❶
         lbl = scales::percent(pct))                             ❶

ggplot(plotdata, aes(x=race, y=pct, fill=education)) +
  geom_bar(stat = "identity", 
           position="fill", 
           color="lightgrey") +
  scale_y_continuous("Percent",                                  ❷
                     label=scales::percent_format(accuracy=2),   ❷
                     n.breaks=10) +                              ❷
  scale_x_discrete("",                                           ❷
                   limits=c("3\. Asian", "1\. White", "2\. Black"), ❷
                   labels=c("Asian", "White", "Black")) +        ❷
  geom_text(aes(label = lbl),                                    ❸
            size=3,                                              ❸
            position = position_stack(vjust = 0.5)) +            ❸
  labs(title="Participant Education by Race",
       fill = "Education") +
  theme_minimal() +                                              ❹
  theme(panel.grid.major.x=element_blank())                      ❹

❶ 计算百分比

❷ 自定义 y 轴和 x 轴

❸ 添加百分比标签

❹ 自定义主题

这段代码与之前的代码类似。根据教育组合计算每个种族的百分比 ❶,并使用这些百分比生成条形图。x-轴和y-轴被自定义以匹配列表 19.2 ❷。接下来,使用geom_text()函数添加百分比标签 ❸。position_stack()函数确保每个堆叠段的百分比放置正确。最后,指定了图形和填充标题,并选择了一个没有x-轴网格线的最小主题 ❹(它们不需要)。

图 19.14 带有百分比标签的堆叠(填充)条形图

突出细节

一个最终的例子展示了如何使用注释在复杂图表中突出信息。gapminder包中的gapminder数据框包含了从 1952 年到 2002 年每 5 年记录的 142 个国家的平均年度预期寿命。柬埔寨的预期寿命与其他亚洲国家在这组数据中差异很大。让我们创建一个突出这些差异的图表。列表 19.14 提供了代码,图 19.15 显示了结果。

图 19.15 33 个亚洲国家的平均预期寿命趋势。柬埔寨的趋势被突出显示。尽管每个国家的趋势都是积极的,但柬埔寨在 1967 年到 1977 年之间有急剧下降。

列表 19.14 突出众多趋势中的一个

library(ggplot2)
library(dplyr)
library(gapminder)
plotdata <- gapminder %>%
  filter(continent == "Asia")                          ❶

plotdata$highlight <- ifelse(plotdata$country %in%      ❷
                             c("Cambodia"), "y", "n")   ❷

ggplot(plotdata, aes(x = year, y = lifeExp,             ❸
                     group = country,                   ❸
                     size = highlight,                  ❸
                     color = highlight)) +              ❸
  scale_color_manual(values=c("lightgrey", "red")) +    ❸
  scale_size_manual(values=c(.5, 1)) +                  ❸
  geom_line() +                                         ❸
  geom_label(x=2000, y= 52, label="Cambodia",           ❹
             color="red", size=3) +                     ❹
  labs(title="Life expectancy for Asian countries",
       x="Year",
       y="Life expectancy") +
  theme_minimal() +                                        
  theme(legend.position="none",
        text=element_text(size=10))

❶ 筛选亚洲国家

❷ 为柬埔寨创建指标变量

❸ 可视化突出柬埔寨

❹ 添加注释标签

首先,数据被子集化,只包括亚洲国家 ❶。接下来,创建了一个二元变量来表示柬埔寨与其他国家 ❷。寿命预期与年份作图,并为每个国家绘制一条单独的线 ❸。柬埔寨的线比其他国家的线粗,颜色为红色。所有其他国家的线都为浅灰色。为柬埔寨的线添加了一个标签 ❹。最后,指定了图形和坐标轴标签,并添加了一个最小主题。图例(大小、颜色)被抑制(不需要),基本文本大小减小。

观察图形,可以清楚地看出每个国家的平均寿命预期都有所增加。但柬埔寨的趋势相当不同,在 1967 年至 1977 年之间有一个显著的下降。这很可能是由于波尔布特和红色高棉在那个时期实施的种族灭绝。

19.4 组合图形

将相关的 ggplot2 图形组合成一个整体的图形,通常有助于强调关系和差异。我在创建文本中的几个图形时使用了这种方法(例如,见图 19.7)。patchwork 包提供了一种简单而强大的语言来组合图形。要使用它,将每个 ggplot2 图形保存为一个对象。然后使用竖线(|)运算符水平组合图形,使用正斜杠(/)运算符垂直组合图形。您可以使用括号 () 创建图形的子组。图 19.16 展示了各种图形排列方式。

图片

图 19.16 patchwork 包提供了一套简单的算术符号,用于在单个图形中排列多个图形。

让我们从 mtcars 数据框中创建几个与 mpg 相关的图形,并将它们组合成一个单一的图形。以下列表给出了代码,图 19.17 显示了该图形。

列表 19.15 使用 patchwork 包组合图形

library(ggplot2)
library(patchwork)

p1 <- ggplot(mtcars, aes(disp, mpg)) +                       ❶
  geom_point() +                                             ❶
  labs(x="Engine displacement",                              ❶
       y="Miles per gallon")                                 ❶

p2 <- ggplot(mtcars, aes(factor(cyl), mpg)) +                ❶
  geom_boxplot() +                                           ❶
  labs(x="Number of cylinders",                              ❶
       y="Miles per gallon")                                 ❶

p3 <- ggplot(mtcars, aes(mpg)) +                             ❶
  geom_histogram(bins=8, fill="darkgrey", color="white") +   ❶
  labs(x = "Miles per gallon",                               ❶
       y = "Frequency")                                      ❶

(p1 | p2) / p3 +                                             ❷
  plot_annotation(title = 'Fuel Efficiency Data') &          ❷
  theme_minimal() +                                          ❷
  theme(axis.title = element_text(size=8),                   ❷
        axis.text = element_text(size=8))                    ❷

❶ 创建了三个图形。

❷ 图形被组合成一个单一的图形。

创建了三个单独的图形,并保存为 p1p2p3 ❶。代码 (p1| p2)/p3 表示前两个图形应放置在第一行,第三个图形应占据整个第二行 ❷。

图片

图 19.17 使用 patchwork 包将三个 ggplot2 图形组合成一个图形。

生成的图形也是一个 ggplot2 图形,可以进行编辑。plot_annotation() 函数为组合图形添加了一个标题(而不是为子图之一添加)。最后,修改了主题。注意使用连字符(&)来添加主题元素。如果您使用了加号(+),则更改只会应用于最后一个子图(p3)。& 符号表示主题函数应该应用于每个子图(p1p2p3)。

patchwork 包有许多其他选项。要了解更多信息,请参阅包参考网站(patchwork.data-imaginist.com/)。

19.5 制作交互式图形

除了少数例外,本书中的图表都是静态图像。创建交互式图表有几个原因。它们允许你专注于有趣的结果,并调用附加信息来理解模式、趋势和异常观察。此外,它们通常比静态图表更具吸引力。

R 中有几个包可以用来创建交互式可视化,包括leafletrbokehrChartshighlighterplotly。在本节中,我们将重点关注plotly

Plotly R 开源绘图库 (plotly.com/r/)可以用来创建高端交互式可视化。一个关键优势是它能够将静态ggplot2图表转换为交互式网络图形。

使用plotly包创建交互式图表是一个简单的两步过程。首先,将ggplot2图表保存为一个对象。然后将该对象传递给ggplotly()函数。

在列表 19.16 中,使用ggplot2创建了一个每加仑英里数与发动机排量之间的散点图。点被着色以表示发动机气缸数。然后,将图表传递给plotly包中的ggplotly()函数,生成一个基于网页的交互式可视化。图 19.18 提供了一个截图。

列表 19.16 将ggplot2图表转换为交互式plotly图表

library(ggplot2)
library(plotly)
mtcars$cyl <- factor(mtcars$cyl)
mtcars$name <- row.names(mtcars)

p <- ggplot(mtcars, aes(x = disp, y= mpg, color = cyl)) +
  geom_point()
ggplotly(p)

当你将鼠标悬停在图表上时,图表右上角将出现一个工具栏,允许你缩放、平移、选择区域、下载图像等(见图 19.19)。此外,当鼠标光标移动到图表区域时,将弹出工具提示。默认情况下,工具提示显示用于创建图表的变量值(在本例中为dispmpgcyl)。此外,点击图例中的键(符号)可以切换数据的开和关。这允许你轻松地关注数据的子集。

图片

图 19.18 从静态ggplot2图表创建的plotly交互式网络图形的截图

图片

图 19.19 plotly图表工具栏。理解这些工具的最简单方法是逐一尝试。

有两种简单的方法可以自定义工具提示。你可以在ggplot aes()函数中包含label1 = var1label2 = var2等,以向工具提示中添加额外的变量。例如,

p <- ggplot(mtcars, aes(x = disp, y= mpg, color = cyl, 
                        label1 = gear, label2 = am))) +
  geom_point()
ggplotly(p)

将创建一个包含dispmpgcylgearam的工具提示。

或者,你可以使用aes()函数中的非官方text参数来从任意文本字符串构建工具提示。以下列表提供了一个示例,图 19.20 提供了结果的截图。

列表 19.17 定制plotly工具提示

library(ggplot2)
library(plotly)
mtcars$cyl <- factor(mtcars$cyl)
mtcars$name <- row.names(mtcars)

p <- ggplot(mtcars,
            aes(x = disp, y=mpg, color=cyl,
                text = paste(name, "\n",
                             "mpg:", mpg, "\n",
                             "disp:", disp, "\n",
                             "cyl:", cyl, "\n",
                             "gear:", gear))) +
  geom_point()
ggplotly(p, tooltip=c("text"))

图片

图 19.20 从ggplot2代码中创建的具有自定义工具提示的交互式plotly图表的截图

text 方法让你对提示信息有极大的控制权。你甚至可以在文本字符串中包含 HTML 标记,从而进一步自定义文本输出。

本章介绍了自定义 ggplot2 图表的各种方法。请记住,自定义的目标是增强你对数据的洞察力,并改善这些洞察力与他人的沟通。任何偏离这些目标的添加到图表中的元素都只是装饰(也称为图表垃圾)。始终尝试避免图表垃圾!请参阅 数据可视化标准 (xdgov.github.io/data-design-standards/) 获取更多建议。

摘要

  • ggplot2 的缩放函数将变量值映射到图表的视觉方面。它们特别适用于自定义坐标轴和颜色调色板。

  • ggplot2 theme() 函数控制图表的非数据元素。它用于自定义字体、图例、网格线和绘图背景的外观。

  • ggplot2 geom 函数通过添加有用的信息(如参考线和标签)来注释图表很有用。

  • 使用 patchwork 包可以将两个或多个图表合并为单个图表。

  • 几乎任何 ggplot2 图表都可以使用 plotly 包从静态图像转换为交互式网络图形。

20 高级编程

本章涵盖了

  • 深入了解 R 语言

  • 使用 R 的面向对象编程特性创建通用函数

  • 调整代码以提高运行效率

  • 查找和纠正编程错误

前几章介绍了对应用程序开发重要的话题,包括数据类型(第 2.2 节)、控制流(第 5.4 节)和函数创建(第 5.5 节)。本章将从更高级和更详细的视角回顾 R 作为编程语言的这些方面。到本章结束时,您将更好地了解 R 语言的工作方式,这将帮助您创建自己的函数,最终,您的自己的包。

在详细讨论函数创建的细节之前,包括作用域和环境的作用,我们将先回顾对象、数据类型和控制流。本章介绍了 R 的面向对象编程方法,并讨论了通用函数的创建。最后,我们将介绍编写高效代码生成和调试应用程序的技巧。掌握这些主题将帮助您理解他人应用程序中的代码,并帮助您创建新的应用程序。在第二十二章中,您将有机会从头到尾创建一个有用的包,以将这些技能付诸实践。

20.1 语言回顾

R 是一种面向对象、函数式、数组式编程语言,其中对象是特殊的数据结构,存储在 RAM 中,并通过名称或符号访问。对象的名称由大写和小写字母、数字 0-9、点号和下划线组成。名称区分大小写,不能以数字开头,点号被视为普通字符,没有特殊含义。

与 C 和 C++等语言不同,你无法直接访问内存位置。数据、函数以及几乎所有可以存储和命名的其他内容都是对象。此外,名称和符号本身也是可以被操作的对象。所有对象在程序执行期间都存储在 RAM 中,这对分析大规模数据集有重大影响。

每个对象都有属性:描述对象特征的元信息。可以使用attributes()函数列出属性,并使用attr()函数设置属性。一个关键属性是对象的类。R 函数使用有关对象类信息来确定如何处理对象。对象的类可以使用class()函数读取和设置。本章和下一章将给出一些示例。

20.1.1 数据类型

有两种基本数据类型:原子向量和通用向量。原子向量是只包含单一数据类型的数组。通用向量,也称为列表,是原子向量的集合。列表是递归的,因为它们也可以包含其他列表。本节将详细考虑这两种类型。

与许多其他编程语言不同,您不需要声明对象的类型或为其分配空间。类型从对象的内部内容隐式确定,大小根据对象包含的类型和元素数量自动增长或缩小。

原子向量

原子向量是包含单一数据类型(逻辑、实数、复数、字符或原始)的数组。例如,以下每个都是一维原子向量:

passed <- c(TRUE, TRUE, FALSE, TRUE)
ages <- c(15, 18, 25, 14, 19)
cmplxNums <- c(1+2i, 0+1i, 39+3i, 12+2i)
names <- c("Bob", "Ted", "Carol", "Alice")

类型为 raw 的向量包含原始字节,这里不做讨论。

许多 R 数据类型都是具有特殊属性的原子向量。例如,R 没有标量类型。标量是一个只有一个元素的原子向量。因此,k <- 2k <- c(2) 的简写。

矩阵是一个具有维度属性 dim 的原子向量,该属性包含两个元素(行数和列数)。例如,从一个一维数值向量 x 开始:

> x <- c(1,2,3,4,5,6,7,8)
> class(x)
[1] "numeric"
> print(x)
{1] 1 2 3 4 5 6 7 8

然后添加一个 dim 属性:

> attr(x, "dim") <- c(2,4)

对象 x 现在是一个 2 × 3 的 matrix 类矩阵:

> print(x)
     [,1] [,2] [,3] [,4]
[1,]    1    3    5    7
[2,]    2    4    6    8

> class(x)
[1] "matrix" "array"
> attributes(x)
$dim
[1] 2 2

可以通过添加 dimnames 属性来附加行和列名:

> attr(x, "dimnames") <- list(c("A1", "A2"), 
                                 c("B1", "B2", "B3", "B4"))
> print(x)
   B1 B2 B3 B4
A1  1  3  5  7
A2  2  4  6  8

最后,可以通过移除 dim 属性将矩阵返回为一维向量:

> attr(x, "dim") <- NULL 
> class(x)
[1] "numeric"
> print(x)
[1] 1 2 3 4 5 6 7 8 

数组是一个具有 dim 属性的原子向量,该属性包含三个或更多元素。同样,您使用 dim 属性设置维度,并可以使用 dimnames 属性附加标签。与一维向量一样,矩阵和数组可以是逻辑、数值、字符、复数或原始类型。但您不能在单个矩阵或数组中混合类型。

attr() 函数允许您创建任意属性并将它们与一个对象关联。属性存储有关对象的信息,并且可以被函数用来确定它们如何被处理。

有许多用于设置属性的专用函数,包括 dim()dimnames()names()row.names()class()tsp()。后者用于创建时间序列对象。这些专用函数对可以设置的值有限制。除非您正在创建自定义属性,否则始终使用这些专用函数是个好主意。它们的限制和它们产生的错误消息使编码错误的可能性降低,并且更明显。

泛型向量或列表

列表 是原子向量和/或其他列表的集合。数据框是列表的一种特殊类型,其中集合中的每个原子向量具有相同的长度。考虑随 base R 安装一起提供的 iris 数据框。它描述了在 150 棵植物上进行的四个物理测量,以及它们的物种(setosa、versicolor 或 virginica):

> head(iris)
  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
6          5.4         3.9          1.7         0.4  setosa

这个数据框实际上是一个包含五个原子向量的列表。它有一个names属性(变量名称的字符向量),一个row.names属性(标识单个植物的数值向量),以及一个值为"data.frame"的类属性。每个向量代表数据框中的一列(变量)。这可以通过使用unclass()函数打印数据框并使用attributes()函数获取属性来轻松看到:

unclass(iris)
attributes(iris)

为了节省空间,这里省略了输出。

理解列表非常重要,因为 R 函数经常将它们作为值返回。让我们看看使用第十六章中提到的聚类分析技术的一个例子。聚类分析使用一系列方法来识别观察值自然发生的分组。

让我们将 k-means 聚类分析(第 16.4.1 节)应用于iris数据。假设数据中存在三个聚类,观察观察值(行)是如何分组的。我们将忽略物种变量,仅使用每棵植物的物理测量值来形成聚类。所需的代码是

set.seed(1234)
fit <- kmeans(iris[1:4], 3)

对象fit包含哪些信息?kmeans``()函数的帮助页面表明该函数返回一个包含七个组件的列表。str()函数显示对象的结构,unclass()函数可以用来直接检查对象的内容。length()函数指示对象包含多少个组件,names()函数提供这些组件的名称。您可以使用attributes()函数来检查对象的属性。这里探讨了kmeans``()返回的对象的内容:

  > names(fit)
[1] "cluster"      "centers"      "totss"        "withinss"    
[5] "tot.withinss" "betweenss"    "size"         "iter"        
[9] "ifault" 

> unclass(fit)
$cluster
  [1] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
 [29] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 3 2 2 2
 [57] 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 2 2 2 2 2 2
 [85] 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 2 3 3 3 3 2 3 3 3 3 3
[113] 3 2 2 3 3 3 3 2 3 2 3 2 3 3 2 2 3 3 3 3 3 2 3 3 3 3 2 3
[141] 3 3 2 3 3 3 2 3 3 2

$centers
  Sepal.Length Sepal.Width Petal.Length Petal.Width
1        5.006       3.428        1.462       0.246
2        5.902       2.748        4.394       1.434
3        6.850       3.074        5.742       2.071

$totss
[1] 681.4

$withinss
[1] 15.15 39.82 23.88

$tot.withinss
[1] 78.85

$betweenss
[1] 602.5

$size
[1] 50 62 38

$iter
[1] 2

$ifault
[1] 0

执行sapply(fit, class)返回对象中每个组件的类:

> sapply(fit, class)
     cluster      centers        totss     withinss tot.withinss 
   "integer"     "matrix"    "numeric"    "numeric"    "numeric" 
   betweenss         size         iter       ifault 
   "numeric"    "integer"    "integer"    "integer" 

在这个例子中,cluster是一个包含聚类成员资格的整数向量,centers是一个包含聚类质心(每个聚类的每个变量的均值)的矩阵。size组件是一个包含每个三个聚类的植物数量的整数向量。要了解其他组件,请参阅help(``kmeans``)中的值部分。

索引

学习如何提取列表中的信息是 R 编程的关键技能。任何数据对象的元素都可以通过索引来提取。在深入探讨列表之前,让我们看看如何从原子向量中提取元素。

元素是通过使用*object*[*index*]来提取的,其中*object*是向量,index是一个整数向量。如果原子向量的元素已经被命名,index也可以是一个包含这些名称的字符向量。请注意,在 R 中,索引从 1 开始,而不是像许多其他语言中的 0。

这里有一个例子,使用这种方法对一个没有命名的原子向量进行操作:

> x <- c(20, 30, 40)
> x[3]
[1] 40
> x[c(2,3)]
[1] 30 40

对于具有命名元素的原子向量,您可以使用

> x <- c(A=20, B=30, C=40)
> x[c(2,3)]
 B  C 
30 40 
> x[c("B", "C")]
 B  C 
30 40

对于列表,可以使用*object*[index*]提取组件(原子向量或其他列表),其中index是一个整数向量。如果组件有名称,可以使用名称字符向量。

继续使用 k-means 的例子,

>fit[c(2, 7)]
$centers
  Sepal.Length Sepal.Width Petal.Length Petal.Width
1     5.006000    3.428000     1.462000    0.246000
2     5.901613    2.748387     4.393548    1.433871
3     6.850000    3.073684     5.742105    2.071053

$size
[1] 50 62 38

返回一个包含两个组件的列表(聚类均值和聚类大小)。每个组件都包含一个矩阵。

重要的是要注意,使用单括号从列表中提取子集始终返回一个列表。例如,

>fit[2]
$centers
  Sepal.Length Sepal.Width Petal.Length Petal.Width
1     5.006000    3.428000     1.462000    0.246000
2     5.901613    2.748387     4.393548    1.433871
3     6.850000    3.073684     5.742105    2.071053

返回一个包含一个组件的列表,而不是矩阵。要提取组件的内容,请使用object[[index]]。例如,

> fit[[2]]
  Sepal.Length Sepal.Width Petal.Length Petal.Width
1        5.006       3.428        1.462       0.246
2        5.902       2.748        4.394       1.434
3        6.850       3.074        5.742       2.071

返回一个矩阵。这个区别可能很重要,取决于你对结果的处理方式。如果你想将结果传递给需要一个矩阵作为输入的函数,请使用双括号表示法。

要提取单个命名组件的内容,可以使用 $ 符号。在这种情况下,object[["name"]]object$name 是等效的:

> fit$centers
  Sepal.Length Sepal.Width Petal.Length Petal.Width
1        5.006       3.428        1.462       0.246
2        5.902       2.748        4.394       1.434
3        6.850       3.074        5.742       2.071

这也解释了为什么 $ 符号与数据框一起使用。考虑 iris 数据框。数据框是列表的特殊情况,其中每个变量都表示为一个组件。例如,iris$Sepal.Length 等同于 iris[["Sepal .Length"]],并返回花瓣长度的 150 元素向量。

可以组合符号来获取组件内的元素。例如,

> fit[[2]][1, ]
Sepal.Length  Sepal.Width Petal.Length  Petal.Width 
       5.006        3.428        1.462        0.246

提取 fit 的第二个组件(均值的矩阵)并返回第一行(每个变量的第一个聚类的均值)。在最终方括号后面的逗号后的空白表示返回所有四列。

通过提取函数返回的列表的组件和元素,你可以进一步处理结果。例如,要使用线形图绘制聚类中心,可以使用以下列表中的代码。

列表 20.1 从 k-means 聚类分析中绘制聚类中心

> set.seed(1234)
> fit <- kmeans(iris[1:4], 3)                                    ❶
> means <- fit$center                                            ❶
> means <- as.data.frame(means)                                  ❶
> means$cluster <- row.names(means)                              ❶

> library(tidyr)                                                 ❷
> plotdata <- gather(means,                                      ❷
                     key="variable",                             ❷
                     value="value",                              ❷
                     -cluster)                                   ❷
> names(plotdata) <- c("Cluster", "Measurement", "Centimeters")  ❷
> head(plotdata)

  Cluster  Measurement Centimeters
1       1 Sepal.Length       5.006
2       2 Sepal.Length       5.902
3       3 Sepal.Length       6.850
4       1  Sepal.Width       3.428
5       2  Sepal.Width       2.748
6       3  Sepal.Width       3.074

library(ggplot2)                                                 ❸
ggplot(data=plotdata,                                            ❸
       aes(x=Measurement, y=Centimeters, group=Cluster)) +       ❸
       geom_point(size=3, aes(shape=Cluster, color=Cluster)) +   ❸
       geom_line(size=1, aes(color=Cluster)) +                   ❸
       labs(title="Mean Profiles for Iris Clusters") +           ❸
       theme_minimal()                                           ❸

❶ 获取聚类均值

❷ 将数据重塑为长格式

❸ 绘制线形图

首先,提取聚类中心的矩阵(行是聚类,列是变量均值)并将其转换为数据框。添加聚类编号作为附加变量 ❶。然后使用tidyr包(见第 5.5.2 节)将数据框重塑为长格式 ❷。最后,使用ggplot2包绘制数据 ❸。图 20.1 显示了生成的图形。

图 20.1 使用 k-means 聚类从 iris 数据集中提取的三个聚类的中心(均值)的图形

这种类型的图形是可能的,因为所有绘制的变量都有相同的测量单位(厘米)。如果聚类分析涉及不同尺度的变量,必须在绘图之前标准化数据,并在 y 轴上标注为标准化分数等。有关详细信息,请参阅第 16.1 节。

现在你可以用结构表示数据并解包结果,让我们看看流程控制。

20.1.2 控制结构

当 R 解释器处理代码时,它是按顺序逐行读取的。如果一行不是一个完整的语句,它将读取额外的行,直到可以构造出一个完整的语句。例如,如果你想要计算 3 + 2 + 5,

> 3 + 2 + 5
[1] 10

将会工作。所以也会

> 3 + 2 + 
  5
[1] 10

第一行末尾的 + 符号表示该语句尚未完成。但是

> 3 + 2
[1] 5
> + 5
[1] 5

明显是不行的,因为 3 + 2 被解释为一个完整的语句。

有时,你需要非顺序地处理代码。你可能想要有条件地执行代码或重复一个或多个语句多次。本节描述了三个在编写函数时特别有用的控制流函数:forififelse

for 循环

for() 函数允许你重复执行一个语句。其语法是

for(var in seq){
     statements
}

其中 var 是一个变量名,seq 是一个计算结果为向量的表达式。如果只有一个语句,大括号是可选的:

> for(i in 1:5) print(1:i)
[1] 1
[1] 1 2
[1] 1 2 3
[1] 1 2 3 4
[1] 1 2 3 4 5

> for(i in 5:1)print(1:i)
[1] 1 2 3 4 5
[1] 1 2 3 4
[1] 1 2 3
[1] 1 2
[1] 1

注意,var 在函数退出后仍然存在。这里,i 等于 1。

在前面的例子中,seq 参数是一个数值向量。它也可以是一个字符向量。例如,

> vars <- c("mpg", "disp", "hp", "wt")
> for(i in vars) hist(mtcars[[i]])

将创建四个直方图。

if() 和 else

if() 函数允许你有条件地执行语句。if() 构造的语法是

if(condition){
    statements
} else {
    statements
}

条件应该是一个单元素逻辑向量(TRUEFALSE)并且不能缺失(NA)。else 部分是可选的。如果只有一个语句,大括号也是可选的。

例如,考虑以下代码片段:

if(interactive()){
    plot(x, y)
} else {
    png("myplot.png")
    plot(x, y)
    dev.off()
}

如果代码正在交互式运行,interactive() 函数返回 TRUE,并将图形发送到屏幕。否则,图形将保存到磁盘。你将在第二十二章中广泛使用 if() 函数。

ifelse()

ifelse() 函数是 if() 的向量化版本。向量化允许函数在不进行显式循环的情况下处理对象。ifelse() 的格式是

ifelse(test, yes, no)

其中 test 是已被强制转换为逻辑模式的对象,yestest 的真元素返回值,notest 的假元素返回值。

假设你有一个从涉及六个统计测试的统计分析中提取的 p 值向量,并且你想要标记在 p < .05 水平上显著的测试。这可以通过以下代码实现:

> pvalues <- c(.0867, .0018, .0054, .1572, .0183, .5386)
> results <- ifelse(pvalues <.05, "Significant", "Not Significant")
> results

[1] "Not Significant" "Significant"     "Significant"    
[4] "Not Significant" "Significant"     "Not Significant"

ifelse() 函数遍历 pvalues 向量,并返回一个包含值 "Significant""Not Significant" 的字符向量,具体取决于 pvalues 的对应元素是否大于 .05。

可以通过使用显式循环达到相同的结果

pvalues <- c(.0867, .0018, .0054, .1572, .0183, .5386)
results <- vector(mode="character", length=length(pvalues))
for(i in 1:length(pvalues)){
  if (pvalues[i] < .05) results[i] <- "Significant" 
  else results[i] <- "Not Significant"
}

向量化版本更快更高效。

还有其他控制结构,包括 whilerepeatswitch,但这里介绍的是最常用的。现在你已经有了数据结构和控制结构,我们可以讨论创建函数。

20.1.3 创建函数

几乎 R 中的所有内容都是一个函数。甚至像+-/*这样的算术运算符实际上也是函数。例如,2 + 2等价于"+"(2, 2)。本节描述函数语法。第 20.2 节检查作用域。

函数语法

函数的语法是

functionname <- function(parameters){
                         statements
                         return(value)
}

如果有多个参数,它们之间用逗号分隔。

参数可以通过关键字、位置或两者同时传递。此外,参数可以有默认值。考虑以下函数:

f <- function(x, y, z=1){
    result <- x + (2*y) + (3*z)
    return(result)
}

> f(2,3,4)
[1] 20
> f(2,3)
[1] 11
> f(x=2, y=3)
[1] 11
> f(z=4, y=2, 3)
[1] 19

在第一种情况下,参数是通过位置传递的(x = 2, y = 3, z = 4)。在第二种情况下,参数是通过位置传递的,而z默认为 1。在第三种情况下,参数是通过关键字传递的,而z再次默认为 1。在最后一种情况下,yz是通过关键字传递的,而x被认为是第一个未明确指定的参数(x=3)。这也表明通过关键字传递的参数可以以任何顺序出现。

参数是可选的,但即使没有传递值,也必须包含括号。return()函数返回函数产生的对象。它也是可选的,如果它缺失,则返回函数中最后一条语句的结果。

你可以使用args()函数查看参数名称和默认值:

> args(f)
  function (x, y, z = 1) 
  NULL

args()函数是为交互式查看设计的。如果你需要以编程方式获取参数名称和默认值,请使用formals()函数,它返回包含必要信息的列表。

参数是通过值传递,而不是通过引用传递。考虑以下函数声明:

result <- lm(height ~ weight, data=women)

数据集women不是直接访问的。会创建一个副本并将其传递给函数。如果women数据集非常大,RAM 可能会迅速耗尽。当你处理大数据问题时,这可能会成为一个问题,你可能需要使用特殊技术(见附录 F)。

对象作用域

R 中对象的作用域(如何将名称解析为内容)很复杂。在典型情况下,

  • 在任何函数外部创建的对象是全局的(可以在任何函数中解析)。在函数内部创建的对象是局部的(仅限于函数内部可用)。

  • 局部对象在函数执行结束时会被丢弃。只有通过return()函数(或使用像<<-这样的运算符)返回的对象在函数执行完成后才能访问。

  • 全局对象可以在函数内部访问(读取),但不能修改(再次,除非使用<<-运算符)。

  • 通过参数传递给函数的对象不会被函数修改。传递的是对象的副本,而不是对象本身。

这里有一个简单的例子:

> x <- 2
> y <- 3
> z <- 4
> f <- function(w){
         z <- 2
         x <- w*y*z
         return(x)
 }
> f(x)
[1] 12
> x
[1] 2
> y
[1] 3
> z
[1] 4

在这个例子中,x的副本被传递给函数f``(),但原始对象没有被修改。y的值是从环境中获得的。尽管z存在于环境中,但函数中设置的值被使用,并且不会改变环境中的值。

要更好地理解作用域规则,我们需要讨论环境。

20.2 与环境一起工作

在 R 中,一个环境由一个框架和封装组成。一个框架是一组符号值对(对象名称及其内容),而一个封装是指向一个封装环境的指针。封装环境也称为父环境。R 允许你在语言内部操作环境,从而实现对作用域和函数与数据的细粒度控制。

在交互式会话中,当你第一次看到 R 提示符时,你处于全局环境。你可以使用new.env()函数创建一个新的环境,并使用assign()函数在该环境中创建赋值。可以使用get()函数从环境中检索对象值。环境是允许你控制对象作用域的数据结构(你也可以将它们视为存储对象的地方)。以下是一个例子:

> x <- 5
> myenv <- new.env()
> assign("x", "Homer", env=myenv)
> ls()
[1] "myenv" "x"       
> ls(myenv)
[1] "x" 
> x
[1] 5
> get("x", env=myenv)
[1] "Homer"

一个名为x的对象存在于全局环境中,其值为5。另一个也称为x的对象存在于myenv环境中,其值为"Homer"。使用单独的环境可以防止这两个对象混淆。

除了使用assign()get()函数外,你还可以使用$符号。例如,

> myenv <- new.env()
> myenv$x <- "Homer"
> myenv$x
[1] "Homer"

产生相同的结果。

parent.env()函数显示父环境。继续上面的例子,myenv的父环境是全局环境:

> parent.env(myenv)
<environment: R_GlobalEnv>

全局环境的父环境是空环境。有关详细信息,请参阅help(environment)

因为函数是对象,它们也有环境。这在考虑函数闭包(包含它们创建时存在的状态的函数)时很重要。考虑一个由另一个函数创建的函数:

trim <- function(p){
    trimit <- function(x){
      n <- length(x)
      lo <- floor(n*p) + 1
      hi <- n + 1 - lo
      x <- sort.int(x, partial = unique(c(lo, hi)))[lo:hi]
    }
    trimit
}

trim(p)函数返回一个函数,该函数从向量中裁剪掉p百分比的最高和最低值:

> x <- 1:10
> trim10pct <- trim(.1)
> y <- trim10pct(x)
> y
[1] 2 3 4 5 6 7 8 9
> trim20pct <- trim(.2)
> y <- trim20pct(x)
> y
  [1] 3 4 5 6 7 8

这之所以有效,是因为p的值在trimit()函数的环境中,并且与函数一起保存:

> ls(environment(trim10pct))
[1] "p"      "trimit"
> get("p", env=environment(trim10pct))
[1] 0.1

这里的教训是,在 R 中,函数包括它们创建时环境中的对象。这一事实有助于解释以下行为:

> makeFunction <- function(k){
    f <- function(x){
      print(x + k)
    }
  }

> g <- makeFunction(10)
> g(4)
[1] 14
> k <- 2
> g(5)
[1] 15

g()函数无论全局环境中的k值是多少,都使用k=10,因为当函数创建时k等于 10。同样,你可以通过以下方式看到这一点:

> ls(environment(g))
[1] "f" "k"
> environment(g)$k
[1] 10

通常,对象的值是从其局部环境获得的。如果对象在其局部环境中找不到,R 将在父环境中搜索,然后是父父环境,依此类推,直到找到对象。如果 R 达到空环境但仍未找到对象,它将抛出一个错误。这称为词法作用域

要了解更多关于环境和词法作用域的信息,请参阅 Christopher Bare 的“R 中的环境”(mng.bz/uPYM) 和 Darren Wilkinson 的“R 中的词法作用域和函数闭包”(mng.bz/R286)。

20.3 非标准评估

R 有一个默认的方法来决定何时以及如何评估表达式。考虑以下示例。创建一个名为 mystats 的函数:

mystats <- function(data, x){
   x <- data[[x]]
   c(n=length(x), mean=mean(x), sd=sd(x))
  }

接下来,调用函数而不引用传递给 x 的值:

> mystats(mtcars, mpg)

 Error in (function(x, i, exact) if (is.matrix(i)) as.matrix(x)[[i]] else .subset2(x,  : object 'mpg' not found 

R 是贪婪的。一旦你将表达式(名称或函数)用作函数的参数,R 就会尝试对其进行评估。你得到错误是因为 R 尝试查找名称 mpg,但未在工作区(在这种情况下是全局环境)中找到它。

如果你引用传递给 x 的值,函数将成功运行。字符串字面量将直接传递(无需查找):

> mystats(mtcars, "mpg")
        n      mean        sd 
32.000000 20.090625  6.026948 

这是 R 的默认行为,被称为 标准评估(SE**)

但如果你真的喜欢第一种调用函数的方式?在这种情况下,你可以按如下方式修改函数:

mystats <- function(data, x){
   x <- as.character(substitute(x))
   x <- data[[x]]
   c(n=length(x), mean=mean(x), sd=sd(x))
  }

substitute() 函数传递对象名称而不对其进行评估。然后通过 as.character() 将名称转换为字符串。现在两者

> mystats(mtcars, mpg)
        n      mean        sd 
32.000000 20.090625  6.026948 

> mystats(mtcars, "mpg")
        n      mean        sd 
32.000000 20.090625  6.026948 

运行!这被称为 非标准评估(NSE)library() 函数使用 NSE,这就是为什么 library(ggplot2)library("ggplot2") 都可以工作。

为什么使用 NSE 而不是 SE?这基本上归结为易用性。以下两个语句是等价的。

df <- mtcars[mtcars$mpg < 20 & mtcars$carb==4, 
             c("disp", "hp", "drat", "wt")]

df <- subset(mtcars, mpg < 20 & carb == 4, disp:wt)

第一个语句使用 SE,而第二个语句使用 NSE。第二个语句更容易输入,可能也更容易阅读。但为了使用户更容易使用,程序员在创建函数时必须更加努力。

基础 R 有几个函数用于控制表达式何时以及如何被评估。Tidyverse 包如 dplyrtidyrggplot2 使用它们自己的 NSE 版本,称为 tidy evaluation。如果你想在你的函数中包含 tidyverse 的函数,你必须使用 rlang 包自动提供的工具。

特别是,你可以使用 enquo()!! 来引用和取消引用参数。例如,

myscatterplot <- function(data, x, y){
  require(ggplot2)
  x <- enquo(x)
  y <- enquo(y)
  ggplot(data, aes(!!x, !!y)) + geom_point() + geom_smooth()
}

   myscatterplot(mtcars, wt, mpg)

运行正常。试一试。

对于多个参数,使用 enquos()!!! 来引用和取消引用传递给 ... 的值。三个点是一个占位符,用于表示传递给函数的一个或多个参数(名称或表达式):

mycorr <- function(data, ..., digits=2){
  require(dplyr)
  vars <- enquos(...)
  data %>% select(!!!vars) %>% cor() %>% round(digits) 
}

mycorr(mtcars, mpg, wt, disp, hp)

rlang 的最新版本提供了快捷方式。之前的函数也可以写成如下形式

myscatterplot <- function(data, x, y){
  require(ggplot2)
  ggplot(data, aes({{x}}, {{y}})) + geom_point() + geom_smooth()
}

mycorr <- function(data, ..., digits=2){
  require(dplyr)
  data %>% select(...) %>% cor() %>% round(digits) 
}

两对花括号和三点符号消除了显式使用 enquo()enquos() 的需要,但它们不会与 rlang 0.4.0 之前的版本一起工作。

R 语言赋予程序员对代码如何以及何时被评估的相当大的控制权。但是,权力越大,责任也越大(并且眼睛后面会有一种钝痛感)。非线性结构方程模型(NSE)是一个非常复杂的话题,本节仅简要介绍了在编写自己的函数时经常遇到的一些元素。要了解更多信息,请参阅 Brodie Gaslam 的讨论([www.brodieg.com/2020/05/05/on-nse/](http://www.brodieg.com/2020/05/05/on-nse/))和 Hadley Wickham 的讨论(adv-r.hadley.nz)。

20.4 面向对象编程

R 语言是一种基于通用函数使用的面向对象编程(OOP)语言。每个对象都有一个类属性,用于确定当将对象的副本传递给通用函数(如print()plot()summary())时运行什么代码。

R 语言有几种面向对象编程(OOP)模型,包括 S3、S4、RC 和 R6。S3 模型较老,更简单,结构更少。S4 模型较新,结构更复杂。两者都包含在基础 R 语言中。S3 方法更容易使用,并且 R 语言中的大多数应用都使用这种模型。在本节中,我们将重点关注 S3 模型,并在最后简要讨论其局限性和 S4 如何尝试解决这些问题。RC 和 R6 使用较少,这里不做介绍。

20.4.1 通用函数

R 语言使用对象的类来确定在调用通用函数时要采取什么行动。考虑以下代码:

summary(women)
fit <- lm(weight ~ height, data=women)
summary(fit)

在第一种情况下,summary()函数为数据框women中的每个变量生成描述性统计量。在第二种情况下,summary()生成线性回归模型的描述。这是如何发生的?

让我们看看summary()的代码:

> summary
function (object, ...) UseMethod("summary")

现在让我们看看women数据框和fit对象的类:

> class(women)
[1] "data.frame"
> class(fit)
[1] "lm"

函数call summary(women)如果存在,将执行函数summary.data.frame(women),否则执行summary.default(women)。同样,summary(fit)如果存在,将执行函数summary.lm(fit),否则执行summary.default(fit)UseMethod()函数将对象调度到具有与对象类匹配扩展的通用函数。

要列出所有可用的 S3 通用函数,请使用methods()函数:

> methods(summary)
 [1] summary.aov             summary.aovlist        
 [3] summary.aspell*         summary.connection     
 [5] summary.data.frame      summary.Date           
 [7] summary.default         summary.ecdf*          
                 ...output omitted...  
[31] summary.table           summary.tukeysmooth*   
[33] summary.wmc*           

   Non-visible functions are asterisked

返回的函数数量取决于您在计算机上安装的包。在我的计算机上,已经为 33 个类定义了单独的summary()函数!

您可以通过不使用括号(summary.data.framesummary.lmsummary .default)输入函数名来查看前一个示例中函数的代码。不可见函数(方法列表中带有星号的函数)不能这样查看。在这种情况下,您可以使用getAnywhere()函数来查看它们的代码。要查看summary.ecdf()的代码,请输入getAnywhere(summary.ecdf)。查看现有代码是获取您自己函数灵感的好方法。

你已经看到了 numericmatrixdata.framearraylmglmtable 等类,但对象的类可以是任何任意字符串。此外,通用函数不必是 print()plot()summary()。任何函数都可以是通用的。以下列表定义了一个名为 mymethod() 的通用函数。

列表 20.2 一个任意通用函数的示例

> mymethod <- function(x, ...) UseMethod("mymethod")       ❶
> mymethod.a <- function(x) print("Using A")               ❶
> mymethod.b <- function(x) print("Using B")               ❶
> mymethod.default <- function(x) print("Using Default")   ❶

> x <- 1:5  
> y <- 6:10
> z <- 10:15
> class(x) <- "a"                                          ❷
> class(y) <- "b"                                          ❷

> mymethod(x)                                              ❸
[1] "Using A"                                              ❸
> mymethod(y)                                              ❸
[1] "Using B"                                              ❸
> mymethod(z)                                              ❸
[1] "Using Default"                                        ❸

> class(z) <- c("a", "b")                                  ❹
> mymethod(z)                                              ❹
[1] "Using A"                                              ❹

> class(z) <- c("c", "a", "b")                             ❺
> mymethod(z)                                              ❺
[1] "Using A"                                              ❺

❶ 定义一个通用函数

❷ 将类分配给对象

❸ 将通用函数应用于对象

❹ 将通用函数应用于具有两个类的对象

❺ 通用函数对类 "c" 没有默认值

在这个例子中,为类 ab 的对象定义了 mymethod() 通用函数。还定义了一个 default() 函数 ❶。然后定义了对象 xyz,并将一个类分配给 xy ❷。接下来,将 mymethod() 应用到每个对象上,并调用适当的函数 ❸。对于对象 z 使用默认方法,因为该对象具有 integer 类,且没有定义 mymethod.integer() 函数。

一个对象可以被分配到多个类中(例如,建筑、住宅和商业)。R 如何在这种情况下确定调用哪个通用函数?当 z 被分配两个类 ❹ 时,第一个类用于确定调用哪个通用函数。在最后的例子 ❺ 中,没有 mymethod.c() 函数,所以使用下一个类(a)。R 从左到右搜索类列表,寻找第一个可用的通用函数。

20.4.2 S3 模型的局限性

S3 对象模型的主要局限性是任何类都可以分配给任何对象。没有完整性检查。在下一个例子中,数据框 women 被分配了 lm 类,这是不合理的,会导致错误:

> class(women) <- "lm"
> summary(women)
Error in if (p == 0) { : argument is of length zero

S4 面向对象模型更加正式和严谨,旨在避免 S3 方法非结构化方法带来的困难。在 S4 方法中,类被定义为包含特定类型信息的抽象对象(即类型变量)。对象和方法构造形式上定义,并强制执行规则。但使用 S4 模型进行编程更为复杂和受限。要了解更多关于 S4 面向对象模型的信息,请参阅 Chistophe Genolini 所著的《(并非如此)简短的 S4 介绍》(mng.bz/1VkD)。

20.5 编写高效的代码

在程序员中有一句话:“一个高级用户会花一个小时调整他们的代码,使其运行快一秒。” R 是一种敏捷的语言,大多数 R 用户不必担心编写高效的代码。使你的代码运行更快的最简单方法是增强你的硬件(RAM、处理器速度等)。一般来说,编写易于理解和维护的代码比优化其速度更重要。但在处理大型数据集或高度重复的任务时,速度可能成为一个问题。

几种编码技术可以帮助使你的程序更高效:

  • 只读取你需要的数据。

  • 在可能的情况下使用向量化而不是循环。

  • 创建正确大小的对象,而不是反复调整大小。

  • 使用并行化处理重复的独立任务。

让我们逐一查看每个部分。

20.5.1 高效的数据输入

当导入数据时,只读取你需要的数据,尽可能向导入函数提供更多信息。另外,尽可能选择优化的函数。

假设你想要从一个包含大量变量和行的逗号分隔文本文件中访问三个数值变量(年龄、身高、体重)、两个字符变量(种族、性别)和一个日期变量(出生日期)。read.csv(基础 R)、freaddata.table 包)和 read_csvreadr 包)函数都可以完成这项工作,但后两者在处理大型文件时速度要快得多。

此外,你可以通过仅选择必要的变量并指定它们的类型(这样函数就不必花费时间猜测)来加快你的代码速度。例如,

library(readr)
mydata <- read_csv(mytextfile,
                 col_types=cols_only(age="i", height="d", weight="d",
                                     race="c", sex="c", dob="D"))

将会比

mydata <- read_csv(mytextfile)

单独。在这里 i=整数d=双精度浮点数c=字符D=日期。参见 ?read_csv 获取其他变量类型的信息。

20.5.2 向量化

在可能的情况下使用向量化而不是循环。在这里,向量化意味着使用设计为以高度优化的方式处理向量的 R 函数。基安装中的示例包括 ifelse()colSums()colMeans()rowSums()rowMeans()matrixStats 包提供了许多额外计算的优化函数,包括计数、总和、乘积、集中趋势和离散度度量、分位数、排名和分箱。dplyrdata.table 等包也提供了高度优化的函数。

考虑一个有 100 万行和 10 列的矩阵。让我们使用循环和 colSums() 函数再次计算列和。首先,创建矩阵:

set.seed(1234)
mymatrix <- matrix(rnorm(10000000), ncol=10)

接下来,创建一个名为 accum``() 的函数,该函数使用 for 循环来获取列和:

accum <- function(x){
   sums <- numeric(ncol(x))
   for (i in 1:ncol(x)){
        for(j in 1:nrow(x)){
            sums[i] <- sums[i] + x[j,i]
        }
   }
}

system.time() 函数可以用来确定运行函数所需的 CPU 和实际时间:

> system.time(accum(mymatrix))
   user  system elapsed 
  25.67    0.01   25.75 

使用 colSums() 函数计算相同的和

> system.time(colSums(mymatrix))
     user  system elapsed 
   0.02    0.00    0.02 

在我的机器上,向量化函数运行速度比之前快了超过 1,200 倍。你的结果可能会有所不同。

20.5.3 正确地设置对象大小

初始化对象到它们所需的最终大小并填充值比从较小的对象开始并追加值来增长对象更有效率。假设你有一个包含 100,000 个数值的向量 x。你想要得到一个包含这些值的平方的向量 y

> set.seed(1234)
> k <- 100000
  > x <- rnorm(k)

一种方法如下:

>  y <- 0
>  system.time(for (i in 1:length(x)) y[i] <- x[i]²)
   user  system elapsed 
  10.03    0.00   10.03 

y 开始是一个包含一个元素的向量,并增长到包含 x 的平方值的 100,000 个元素的向量。在我的机器上大约需要 10 秒。

如果你首先将 y 初始化为一个包含 100,000 个元素的向量,

>  y <- numeric(length=k)
>  system.time(for (i in 1:k) y[i] <- x[i]²)
   user  system elapsed 
   0.23    0.00    0.24 

相同的计算不到一秒钟即可完成。您避免了 R 不断调整对象大小所花费的大量时间。

如果您使用向量化,

> y <- numeric(length=k)
> system.time(y <- x²)
   user  system elapsed 
      0       0       0

该过程甚至更快。请注意,指数、加法、乘法等操作都是向量化函数。

20.5.4 并行化

并行化涉及将任务分成块,同时在两个或更多核心上同时运行这些块,并合并结果。这些核心可能位于同一台计算机上,也可能位于集群中不同的机器上。需要重复独立执行数值密集型函数的任务可能从并行化中受益。这包括许多蒙特卡洛方法,包括自助法。

R 中的许多包支持并行化(参见 Dirk Eddelbuettel 的“CRAN 任务视图:使用 R 进行高性能和并行计算”,mng.bz/65sT)。在本节中,您将使用foreachdoParallel包来查看单台计算机上的并行化。foreach包支持foreach循环结构(遍历集合中的元素)并简化了并行执行循环。doParallel包为foreach包提供并行后端。

在主成分分析和因子分析中,一个关键步骤是确定从数据中提取适当数量的成分或因子(参见第 14.2.1 节)。一种方法涉及反复对具有与原始数据相同行数和列数的随机数据导出的相关矩阵进行特征值分析。以下列表显示了分析,它比较了并行和非并行版本。要执行此代码,您需要安装这两个包。

列表 20.3 使用foreachdoParallel进行并行化

> library(foreach)                                         ❶
> library(doParallel)                                      ❶
> registerDoParallel(cores=detectCores())                  ❶

> eig <- function(n, p){                                   ❷
             x <- matrix(rnorm(100000), ncol=100)          ❷
             r <- cor(x)                                   ❷
             eigen(r)$values                               ❷
  }                                                        ❷

> n <- 1000000 
> p <- 100
> k <- 500

> system.time(                                             ❸
    x <- foreach(i=1:k, .combine=rbind) %do% eig(n, p)     ❸
   )                                                       ❸

    user  system elapsed 
   10.97    0.14   11.11

> system.time(                                             ❹
     x <- foreach(i=1:k, .combine=rbind) %dopar% eig(n, p) ❹
   )                                                       ❹

   user  system elapsed 
   0.22    0.05    4.24

❶ 加载包并注册核心数

❷ 定义函数

❸ 正常执行

❹ 并行执行

首先,加载了包,并注册了核心数(在我的机器上是四个)❶。接下来,定义了用于特征值分析的函数❷。在这里,分析了 100,000 × 100 个随机数据矩阵。使用foreach%do%执行了 500 次eig()函数❸。%do%运算符按顺序运行函数,.combine=rbind选项将结果作为行追加到对象x中。最后,使用%dopar%运算符并行运行该函数❹。在这种情况下,并行执行比顺序执行快约 2.5 倍。

在此示例中,eig()函数的每次迭代都是数值密集型的,不需要访问其他迭代,也不涉及磁盘 I/O。这是从并行化中受益最大的那种情况。并行化的缺点是它可能会使代码的可移植性降低——没有保证其他人会有与您相同的硬件配置。

本节中描述的四个效率度量可以帮助解决日常编码问题。但它们在帮助你处理真正的大型数据集(例如,那些在太字节范围内的数据集)方面只能走这么远。当你处理 大型 数据集时,需要使用附录 F 中描述的方法。

定位瓶颈

“为什么我的代码运行这么慢?” R 提供了用于分析程序以识别最耗时函数的工具。将需要分析的代码放在 Rprof()Rprof(NULL) 之间。然后执行 summaryRprof() 以获取每个函数执行时间的摘要。有关详细信息,请参阅 ?Rprof?summaryRprof

Rstudio 也可以分析你的代码。从 RStudio 的分析菜单中选择 开始分析,运行你的代码,完成后点击红色 停止分析 按钮。结果会以表格和图形的形式展示。

当程序无法执行或给出无意义的结果时,效率就变得微不足道了。接下来将考虑揭示编程错误的方法。

20.6 调试

调试 是寻找和减少程序中错误或缺陷数量的过程。如果程序一开始就能正常工作那将是极好的。如果独角兽住在我的邻居家那也会很棒。但在除了最简单的程序之外的所有程序中,都会发生错误。确定这些错误的原因并修复它们是耗时的。在本节中,我们将探讨常见的错误来源以及可以帮助揭示错误的工具。

20.6.1 常见错误来源

以下是一些在 R 中函数失败的一些常见原因:

  • 对象名称拼写错误,或者对象不存在。

  • 函数调用中的一个或多个参数指定不当。这可能发生在参数名称拼写错误、省略了必需的参数、参数值类型错误(例如,需要一个列表时却提供了一个向量),或者参数值输入顺序错误(当省略了参数名称时)。

  • 对象的内容不是用户预期的。特别是,错误通常是由于传递了 NULL 或包含 NaNNA 值的对象给无法处理这些值的函数而引起的。

第三个原因比你想象的更常见。这是由于 R 对错误和警告的简洁处理方式导致的。

考虑以下示例。对于基础安装中的 mtcars 数据集,你想要为变量 am(变速类型)提供一个更具信息量的标题和标签。接下来,你想要比较自动变速汽车和手动变速汽车的油耗:

> mtcars$Transmission <- factor(mtcars$a, 
                                levels=c(1,2), 
                                labels=c("Automatic", "Manual"))
> aov(mpg ~ Transmission, data=mtcars)
Error in `contrasts<-`(`*tmp*`, value = contr.funs[1 + isOF[nn]]) : 
  contrasts can be applied only to factors with 2 or more levels

哎呀!(尴尬,但这就是我说的。)发生了什么?

你没有收到“对象 xxx 未找到”的错误,所以你可能没有拼写函数、数据框或变量名。让我们看看传递给 aov() 函数的数据:

> head(mtcars[c("mpg", "Transmission")])
                   mpg Transmission
Mazda RX4         21.0    Automatic
Mazda RX4 Wag     21.0    Automatic
Datsun 710        22.8    Automatic
Hornet 4 Drive    21.4         <NA>
Hornet Sportabout 18.7         <NA>
Valiant           18.1         <NA>

> table(mtcars$Transmission)

Automatic    Manual 
       13         0

没有手动变速的汽车。回顾原始数据集,变量 am 被编码为 0=自动1=手动(而不是 1=自动2=手动)。

factor() 函数愉快地完成了您的要求,没有警告或错误。它将所有手动变速的汽车设置为自动变速,所有自动变速的汽车设置为缺失。由于只有一个组可用,方差分析失败了。确认函数的每个输入都包含预期的数据可以为您节省数小时的令人沮丧的侦探工作。

20.6.2 调试工具

虽然检查对象名称、函数参数和函数输入可以揭示许多错误来源,但有时您必须深入了解函数及其调用函数的内部工作原理。在这些情况下,R 伴随的内部调试器可能很有用。表 20.1 列出了一些有用的调试函数。

表 20.1 内置调试函数

函数 操作
debug() 标记函数为调试
undebug() 取消标记函数为调试
browser() 允许逐行执行函数的执行。在调试过程中,输入 n 或按 Enter 键执行当前语句并进入下一行。输入 c 继续执行到函数的末尾而不逐行执行。输入 where 显示调用堆栈,而 Q 立即停止执行并跳转到顶层。其他 R 命令如 ls()print() 和赋值语句也可以在调试器提示符下提交。
trace() 修改函数以允许临时插入调试代码
untrace() 取消跟踪并删除临时代码
traceback() 打印导致最后一个未捕获错误的函数调用序列

debug() 函数将函数标记为调试。当函数执行时,会调用 browser() 函数,允许您逐行执行函数的执行。undebug() 函数关闭此功能,允许函数正常执行。您可以使用 trace() 函数临时将调试代码插入到函数中。这在调试无法直接编辑的基函数和 CRAN 贡献函数时特别有用。

如果一个函数调用其他函数,确定错误发生的位置可能很困难。在这种情况下,错误发生后立即执行 traceback() 函数将列出导致错误的函数调用序列。最后一个调用是产生错误的调用。

让我们看看一个例子。mad() 函数计算数值向量的中值绝对偏差。您将使用 debug() 来探索这个函数的工作方式。以下列表显示了调试会话。

列表 20.4 一个示例调试会话

> args(mad)                                          ❶
function (x, center = median(x), constant = 1.4826, 
    na.rm = FALSE, low = FALSE, high = FALSE) 
NULL

> debug(mad)                                         ❷
> mad(1:10)

debugging in: mad(x)
debug: {
    if (na.rm) 
        x <- x[!is.na(x)]
    n <- length(x)
    constant * if ((low || high) && n%%2 == 0) {
        if (low && high) 
            stop("'low' and 'high' cannot be both TRUE")
        n2 <- n%/%2 + as.integer(high)
        sort(abs(x - center), partial = n2)[n2]
    }
    else median(abs(x - center))
} 

Browse[2]> ls()                                      ❸
[1] "center"   "constant" "high"     "low"      "na.rm"    "x"       

Browse[2]> center
[1] 5.5

Browse[2]> constant
[1] 1.4826

Browse[2]> na.rm
[1] FALSE

Browse[2]> x
 [1]  1  2  3  4  5  6  7  8  9 10  

Browse[2]> n                                         ❹
debug: if (na.rm) x <- x[!is.na(x)]

Browse[2]> n
debug: n <- length(x)

Browse[2]> n
debug: constant * if ((low || high) && n%%2 == 0) {
    if (low && high) 
        stop("'low' and 'high' cannot be both TRUE")
    n2 <- n%/%2 + as.integer(high)
    sort(abs(x - center), partial = n2)[n2]
} else median(abs(x - center))

Browse[2]> print(n)                                           
[1] 10

Browse[2]> where
where 1: mad(x)

Browse[2]> c                                         ❺
exiting from: mad(x)
[1] 3.7065

> undebug(mad)

❶ 查看形式参数

❷ 设置要调试的函数

❸ 列出对象

❹ 单步执行代码

❺ 恢复连续执行

首先,使用 arg() 函数显示 mad() 函数的参数名称和默认值 ❶。然后使用 debug(mad) 设置调试标志 ❷。现在,每次调用 mad() 时,都会执行 browser() 函数,允许您逐行遍历函数。

当调用 mad() 时,会话进入 browser() 模式。函数的代码被列出但不会执行。此外,提示符更改为 Browse[n]>,其中 n 表示 浏览器级别。该数字随着每次递归调用而递增。

browser() 模式下,可以执行其他 R 命令。例如,ls() 列出函数执行过程中给定点的存在对象 ❸。键入对象名称将显示其内容。如果对象名称为 ncQ,您必须使用 print(n)print(c)print(Q) 来查看其内容。您可以通过键入赋值语句来更改对象值。

通过键入字母 n 或按 Return 或 Enter 键,您可以逐行执行函数中的语句 ❹。where 语句指示您在正在执行的函数调用栈中的位置。对于单个函数,这并不很有趣,但如果您有调用其他函数的函数,它可能很有帮助。

键入 c 将退出单步模式并执行当前函数的剩余部分 ❺。键入 Q 将退出函数并返回到 R 提示符。

debug() 函数在您有循环并想查看值如何变化时很有用。您还可以直接在代码中嵌入 browser() 函数以帮助定位问题。假设您有一个变量 X,它应该永远不会是负数。添加以下代码

if (X < 0) browser()

允许您在问题发生时探索函数的当前状态。当函数足够调试后,您可以移除额外的代码。(我最初写了“完全调试”,但这几乎从未发生,所以我将其改为“足够调试”,以反映程序员的现实。)

20.6.3 支持调试的会话选项

当您有调用函数的函数时,两个会话选项可以帮助调试。通常,当 R 遇到错误时,它会打印错误消息并退出函数。设置 options(error=traceback) 会在错误发生时立即打印调用栈(导致错误的函数调用序列)。这可以帮助您确定哪个函数生成了错误。

设置 options(error=recover) 也会在发生错误时打印调用栈。此外,它会提示您从列表中选择一个函数,然后在相应的环境中调用 browser()。键入 c 将您返回到列表,而键入 0 则退出回到 R 提示符。

使用这个recover()模式让你可以探索从调用函数序列中选择的任何函数中的任何对象的内容。通过选择性地查看对象的内容,你通常可以确定问题的来源。要返回 R 的默认状态,设置options(error=NULL)。下一个列表显示了一个玩具示例。

列表 20.5 使用recover()的示例调试会话

f <- function(x, y){                        ❶
       z <- x + y                           ❶
       g(z)                                 ❶
}                                           ❶
g <- function(x){                           ❶
       z <- round(x)                        ❶
       h(z)                                 ❶
}                                           ❶
h <- function(x){                           ❶
       set.seed(1234)                       ❶
       z <- rnorm(x)                        ❶
       print(z)                             ❶
}                                           ❶
> options(error=recover)
> f(2,3)
[1] -1.207  0.277  1.084 -2.346  0.429
> f(2, -3)                                  ❷
Error in rnorm(x) : invalid arguments

Enter a frame number, or 0 to exit   

1: f(2, -3)
2: #3: g(z)
3: #3: h(z)
4: #3: rnorm(x)

Selection: 4                                ❸
Called from: rnorm(x)

Browse[1]> ls()
[1] "mean" "n"    "sd"  

Browse[1]> mean
[1] 0

Browse[1]> print(n)
[1] -1

Browse[1]> c

Enter a frame number, or 0 to exit   
1: f(2, -3)
2: #3: g(z)
3: #3: h(z)
4: #3: rnorm(x)

Selection: 3                                ❹
Called from: h(z)

Browse[1]> ls()
[1] "x"

Browse[1]> x
[1] -1
Browse[1]> c

Enter a frame number, or 0 to exit   

1: f(2, -3)
2: #3: g(z)
3: #3: h(z)
4: #3: rnorm(x)

Selection: 2                                ❺
Called from: g(z)

Browse[1]> ls()
[1] "x" "z"

Browse[1]> x
[1] -1

Browse[1]> z
[1] -1

Browse[1]> c

Enter a frame number, or 0 to exit   

1: f(2, -3)
2: #3: g(z)
3: #3: h(z)
4: #3: rnorm(x)

Selection: 1                               ❻
Called from: f(2, -3)

Browse[1]> ls()
[1] "x" "y" "z"

Browse[1]> x
[1] 2

Browse[1]> y
[1] -3

Browse[1]> z
[1] -1

Browse[1]> print(f)
function(x, y){
     z <- x + y
     g(z)
}
Browse[1]> c

Enter a frame number, or 0 to exit   

1: f(2, -3)
2: #3: g(z)
3: #3: h(z)
4: #3: rnorm(x)

Selection: 0  

> options(error=NULL) 

❶ 创建函数

❷ 输入导致错误的值

❸ 检查rnorm()

❹ 检查h(z)

❺ 检查g(z)

❻ 检查f(2, -3)

代码首先创建了一系列函数。函数f()调用函数g()。函数g()调用函数h()。执行f(2, 3)没有问题,但f(2, -3)会抛出错误。由于设置了options(error=recover),交互式会话立即进入恢复模式。函数调用堆栈被列出,你可以选择在browser()模式下检查哪个函数。

输入4将你移动到rnorm()函数中,其中ls()列出对象;你可以看到n等于-1,这在rnorm()中是不允许的。这显然是问题所在,但要看到n如何变成-1,你需要向上查看调用堆栈。

输入c返回菜单,输入3将你移动到h(z)函数中,其中x等于-1。输入c2将你移动到g(z)函数中。在这里,xz都是-1。最后,向上移动到f(2, -3)函数,可以看到z是-1,因为x等于2y等于-3

注意到使用print()来查看函数代码。当你调试的不是自己编写的代码时,这很有用。通常,你可以输入函数名来查看代码。在这个例子中,f是浏览器模式中的一个保留词,表示“完成当前循环或函数的执行”;这里显式使用print()函数来避免这个特殊含义。

最后,按c键返回菜单,按0键返回正常的 R 提示符。或者,在任何时候输入Q键也可以返回 R 提示符。

要了解更多关于一般调试和特定于恢复模式的调试信息,请参阅 Roger Peng 的出色文章“R 交互式调试工具简介”(mng.bz/GPR6)。

20.6.4 使用 RStudio 的视觉调试器

除了前面描述的工具之外,RStudio 还提供了与前面章节中材料平行的视觉调试支持。

首先,让我们调试基础 R 中的mad()函数。

> debug(mad)
> mad(1:10)

RStudio 界面发生变化(见图 20.2)。

图像

图 20.2 在 RStudio 中可视化调试mad()函数

上左窗格显示当前函数。一个绿色箭头指示当前行。上右窗格显示函数环境中的对象及其在当前行的值。随着您执行函数的每一行,这些值将发生变化。右下窗格显示调用栈。由于只有一个函数,这并不有趣。最后,左下窗格显示浏览器模式的控制台。这是您将花费大部分时间的地方。

您可以在控制台窗口的浏览器提示符中输入前两个部分中的每个命令。或者,您也可以点击此窗格顶部的代码执行图标(见图 20.2)。从左到右,它们将

  • 执行下一行代码

  • 进入当前函数调用

  • 执行当前函数或循环的剩余部分

  • 继续执行直到下一个断点(此处未涉及)

  • 退出调试模式

以这种方式,您可以逐步执行函数并观察每行代码执行后的情况。

接下来,让我们调试列表 20.5 中的代码。

f <- function(x, y){                            
       z <- x + y
       g(z)
}
g <- function(x){
       z <- round(x)
       h(z)
}
h <- function(x){
       set.seed(1234)
       z <- rnorm(x)
       print(z)
}
> options(error=recover)
> f(2,-3)

界面将变为类似于图 20.3 的样子。

上左窗格显示的函数是引发错误的函数。由于函数会调用其他函数,因此调用栈现在变得有趣。通过点击函数的调用栈窗口(右下窗格),您可以查看每个函数中的代码、它们的调用顺序以及它们传递的值。当函数在回溯列表中高亮显示时,其对象的值将在右上窗格的环境窗口中显示。这样,您可以观察函数调用和值传递的过程,直到错误发生。

要了解更多关于 RStudio 可视化调试器的信息,请参阅文章“使用 RStudio IDE 调试”(mng.bz/4M65)和视频“R 调试简介”(mng.bz/Q2R1)。

图片

图 20.3 在 RStudio 中可视化查看调用栈。函数 f(2,-3) 调用 g(z),它又调用 h(z),然后调用 rnorm(x),这是错误发生的地方。您可以通过点击回溯面板中的适当行来查看每个函数及其值。

20.7 进一步学习

关于 R 的高级编程,有多个优秀的信息来源。R 语言定义(mng.bz/U4Cm)是一个很好的起点。“R 和 S-PLUS 中的框架、环境和作用域”由 John Fox 撰写(mng.bz/gxw8),是一篇很好的文章,可以帮助你更好地理解作用域。“R 搜索和查找内容”由 Suraj Gupta 撰写(mng.bz/2o5B),是一篇博客文章,可以帮助你理解标题所暗示的内容。要了解更多关于高效编码的信息,请参阅 Noam Ross 撰写的“FasteR! HigheR! StrongeR!—A Guide to Speeding Up R Code for Busy People”(mng.bz/Iq3i)。最后,两本书非常有帮助。高级 R 由 Hadley Wickham 撰写(adv-r.hadley.nz/)和 R Programming for Bioinformatics(Chapman & Hall,2009)由 Robert Gentleman 撰写,是程序员想要深入了解的全面且易于理解的文本。我强烈推荐它们给任何想要成为更有效的 R 编程人员。

摘要

  • R 有丰富的数据结构,包括原子和泛型向量。学习如何从中提取信息是有效 R 编程的关键技能。

  • 每个 R 对象都有一个类和可选的属性。

  • 控制结构如 for()if()/else() 允许你条件性地和非顺序地执行代码。

  • 环境提供了一种查找对象名称及其内容的机制。它们提供了对名称和函数作用域的精细控制。

  • R 有几个面向对象编程的系统。其中,S3 系统是最常见的。

  • 非标准评估(nonstandard evaluation)允许程序员自定义表达式如何以及何时被评估。

  • 加速代码的两种方法是矢量化(vectorization)和并行处理(parallel processing)。对代码进行性能分析可以帮助你找到速度瓶颈。

  • R 有丰富的调试工具。RStudio 提供了这些工具的图形界面。它们共同使得查找逻辑和编码错误变得更加容易(但很少容易!)。

21 创建动态报告

本章涵盖

  • 将结果发布到网络上

  • 将 R 结果整合到 Microsoft Word 或 Open Document 报告中

  • 创建动态报告,其中更改数据会更改报告

  • 使用 R、Markdown 和 LaTeX 创建可发布的文档

  • 避免常见的 R Markdown 错误

在前面的章节中,您已经访问了数据,清理了数据,描述了其特征,建立了模型,并可视化了结果。下一步是

  1. 放松一下,也许可以去迪士尼世界。

  2. 将结果传达给他人。

如果您选择了 1,请带上我。如果您选择了 2,欢迎来到现实世界。

研究并不随着最后的数据分析或图表的完成而结束。您几乎总是需要将结果传达给他人。这意味着将分析整合到某种类型的报告中。

有三种常见的报告场景。在第一种情况下,您创建的报告包括您的代码和结果,这样您就可以记住六个月前做了什么。从单一的综合文档中重建所做的工作比从一系列相关文件中更容易。

在第二种情况下,您必须为教师、主管、客户、政府机构、互联网受众或期刊编辑生成报告。清晰度和吸引力很重要,而且报告可能只需要创建一次。

在第三种情况下,您需要定期生成特定类型的报告。这可能是一份关于产品或资源使用的月度报告,一份周度财务分析,或者每小时更新的网站流量报告。在任何情况下,数据都会变化,但分析和报告的结构保持不变。

将 R 输出整合到报告中的方法之一是运行分析,将每个图表和文本表格剪切并粘贴到文字处理文档中,并重新格式化结果。这种方法通常耗时、效率低下且令人沮丧。尽管 R 创建了最先进的图形,但其文本输出却非常落后——使用空格对齐列的单倍间距文本表。重新格式化它们并非易事。而且如果数据发生变化,您将不得不再次完成整个流程!

考虑到这些限制,您可能会觉得 R 对您不起作用。别担心。(好吧,有点担心——这是一种重要的生存机制。)R 提供了一种优雅的解决方案,使用一种名为 R Markdown 的标记语言将 R 代码和结果整合到报告中(rmarkdown.rstudio.com)。此外,数据可以与报告绑定,以便更改数据会更改报告。这些动态报告可以保存为

  • 网页

  • Microsoft Word 文档

  • Open Document 文件

  • Beamer、HTML5 和 PowerPoint 幻灯片

  • 可发布的 PDF 或 PostScript 文档

例如,假设你正在使用回归分析来研究一组女性体重和身高的关系。R Markdown 允许你获取由lm()函数生成的等宽输出:

> lm(weight ~ height, data=women)

Call:
lm(formula = weight ~ height, data = women)

Residuals:
    Min      1Q  Median      3Q     Max 
-1.7333 -1.1333 -0.3833  0.7417  3.1167 

Coefficients:
             Estimate Std. Error t value Pr(>|t|)    
(Intercept) -87.51667    5.93694  -14.74 1.71e-09 ***
height        3.45000    0.09114   37.85 1.09e-14 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Residual standard error: 1.525 on 13 degrees of freedom
Multiple R-squared:  0.991,     Adjusted R-squared:  0.9903 
F-statistic:  1433 on 1 and 13 DF,  p-value: 1.091e-14

并将其转换为如图 21.1 所示的网页。在本章中,你将学习如何做到这一点。

图片

图 21.1 回归分析保存到网页

动态文档和可重复研究

学术界内部正在兴起一股支持可重复研究的强大运动。可重复研究的目的是通过包括必要的数据和软件代码来促进科学发现的复制,这些代码用于在报告它们的出版物中重现发现。这允许读者自己验证发现,并为他们自己提供在自身工作中更直接地建立结果的机会。本章描述的技术,包括将数据和源代码嵌入文档中,直接支持这一努力。

21.1 报告的模板方法

本章采用模板方法来生成报告。报告从一个包含报告文本、格式化语法和 R 代码块的模板文件开始。

模板文件被处理,R 代码被执行,应用了格式化语法,并生成了一份报告。不同的选项控制了 R 输出在报告中的包含方式。图 21.2 展示了使用 R Markdown 模板创建网页的一个简单示例。

图片

图 21.2 从包含 Markdown 语法、报告文本和 R 代码块的文本文件创建网页

模板文件(example.Rmd)是一个包含四个组件的纯文本文件:

  • 元数据—元数据(称为 YAML 标题)由一对三个短横线(---)括起来,包含有关文档和所需输出的信息。

  • 报告文本—任何解释性短语和文本。在这里,报告文本是“我的报告”、“这里有一些数据”、“图表”和“这里有一个图表”。

  • 格式化语法—控制报告格式的标签。在这个文件中,Markdown 标签用于格式化结果。Markdown 是一种简单的标记语言,可以用来将纯文本文件转换为结构有效的 HTML 或 XHTML。第一行中的井号#不是一个注释。它产生一个一级标题。##产生一个二级标题,以此类推。

  • R 代码—要执行的 R 语句。在 R Markdown 文档中,R 代码块被```{r}```包围。第一个代码块列出数据集的前六行,第二个代码块生成一个散点图。在这个例子中,代码和结果都输出到报告中,但选项允许你控制每个代码块打印的内容。

模板文件被传递到rmarkdown包中的render()函数,并创建了一个名为 example.html 的网页。该网页包含报告文本和 R 结果。

本章的示例基于描述性统计、回归和方差分析问题。它们都不代表数据的完整分析。本章的目标是学习如何将 R 结果纳入各种类型的报告中。

根据您开始时使用的模板文件以及用于处理它的函数,可以创建不同的报告格式(HTML 网页、Microsoft Word 文档、OpenOffice Writer 文档、PDF 报告、文章、幻灯片和书籍)。报告是动态的,这意味着更改数据和重新处理模板文件将产生一个新的报告。

21.2 使用 R 和 R Markdown 创建报告

在本节中,您将使用 rmarkdown 包来创建由 Markdown 语法和 R 代码生成的文档。当文档被处理时,R 代码将被执行,输出将被格式化并嵌入到最终的文档中。您可以使用这种方法生成各种格式的报告。以下是步骤:

  1. 安装 rmarkdown 包 (install.packages("rmarkdown"))。这将安装包括 knitr 在内的几个其他包。如果您使用的是 RStudio 的最新版本,您可以跳过此步骤,因为您已经拥有了必要的包。

  2. 安装 Pandoc (johnmacfarlane.net/pandoc/),这是一个免费的应用程序,适用于 Windows、macOS 和 Linux。它可以将文件从一种标记格式转换为另一种格式。再次提醒,RStudio 用户可以跳过此步骤。

  3. 如果您想创建 PDF 文档,请安装 LaTeX 编译器,它可以将 LaTeX 文档转换为高质量的排版 PDF 文档。完整的安装包括 Windows 的 MiKTeX (www.miktex.org)、Mac 的 MacTeX (tug.org/mactex) 和 Linux 的 TeX Live (www.tug.org/texlive)。我推荐一个轻量级的跨平台安装,称为 TinyTex (yihui.org/tinytex)。要安装它,请运行

    install.packages("tinytex")

    tinytex::install_tinytex()

  4. 虽然这不是强制性的,但安装 broom 包 (install.packages("broom")) 是一个好主意。该包中的 tidy() 函数可以将超过 135 个 R 统计函数的结果导出到数据框中,以便包含在报告中。查看 methods(tidy) 以查看它可以输出的对象的综合列表。

  5. 最后,安装 kableExtra (install.packages("kableExtra"))。knitr 包中的 kable 函数可以将矩阵或数据框转换为 LaTeX 或 HTML 表格,以便包含在报告中。kableExtra 包包括用于样式化表格输出的函数。

软件设置完成后,您就可以开始了。

要使用 Markdown 语法在文档中包含 R 输出(值、表格、图形),首先创建一个包含

  • YAML 标头

  • 报告文本

  • Markdown 语法

  • R 代码块(由分隔符包围的 R 代码)

文本文件应具有 .Rmd 扩展名。

列表 22.1 展示了一个示例文件(命名为 women.Rmd)。要生成 HTML 文档,使用以下命令处理此文件

library(rmarkdown)
render("women.Rmd")

或点击 RStudio Knit 按钮。图 21.1 显示了结果。

列表 21.1 women.Rmd:一个嵌入 R 代码的 R Markdown 模板

---                                                              ❶
title: "Regression Report"                                       ❶
author: "RobK"                                                   ❶
date: "7/8/2021"                                                 ❶
output: html_document                                            ❶
---                                                              ❶

# Heights and weights                                            ❷

```{r echo = FALSE}                                              ❸

options(digits=3)                                                ❸

n    <- nrow(women)                                              ❸

fit  <- lm(weight ~ height, data=women)                          ❸

sfit <- summary(fit)                                             ❸

b    <- coefficients(fit)                                        ❸

```                                                              ❸

Linear regression was used to model the relationship between 
weights and height in a sample of `r n` women. The equation      ❹
**weight = `r b[1]` +  `r b[2]` * height**                       ❹
accounted for `r round(sfit$r.squared,2)`% of the variance       ❹
in weights. The ANOVA table is given below.

```{r echo=FALSE}                                                ❺

library(broom)                                                   ❺

library(knitr)                                                   ❺

library(kableExtra)                                              ❺

results <- tidy(fit)                                             ❺

tbl <- kable(results)                                            ❺

kable_styling(tbl, "striped", full_width=FALSE, position="left") ❺

```                                                              ❺

The regression is plotted in the following figure.

```{r fig.width=5, fig.height=4}

library(ggplot2)

ggplot(data=women, aes(x=height, y=weight)) +

    geom_point() + geom_smooth(method="lm", formula=y~x)


❶ YAML 标题

❷ 二级标题

❸ R 代码块

❹ R 内联代码

❺ 格式化结果表格

报告以 YAML 标题 ❶ 开始,指示标题、作者、日期和输出格式。日期是硬编码的。要动态插入当前日期,将 `"7/8/2021"` 替换为 ``"`r Sys.Date()`"``(包括双引号)。在 YAML 标题中,只需要输出字段。表 21.1 列出了最常见的输出选项。完整的列表可在 RStudio ([`rmarkdown.rstudio.com/lesson-9.html`](https://rmarkdown.rstudio.com/lesson-9.html)) 中找到。

表 21.1 R Markdown 文档输出选项

| 输出选项 | 描述 |
| --- | --- |
| `html_document` | HTML 文档 |
| `pdf_document` | PDF 文档 |
| `word_document` | Microsoft Word 文档 |
| `odt_document` | Open Document Text 文档 |
| `rtf_document` | 富文本文档 |

接下来是第一级标题 ❷。它指示“身高和体重”应以大号粗体字打印。表 21.2 展示了其他 Markdown 语法的示例。

表 21.2 Markdown 代码及其生成的输出

| Markdown 语法 | 生成的 HTML 输出 |
| --- | --- |
| `# Heading 1``## Heading 2``...``###### Heading 6` | `<h1>Heading 1</h1>``<h2>Heading 2</h2>``...``<h6>Heading 6</h6>` |
| 文本之间有一或多个空白行 | 将文本分隔成段落 |
| 行尾有两个或多个空格 | 添加换行符 |
| `*I mean it*` | `<em>I mean it</em>` |
| `**I really mean it**` | `<strong>I really mean it</strong>` |
| `* item 1``* item 2` | `<ul>``<li> item 1 </li>``<li> item 2 </li>``</ul>` |
| `1\. item 1``2\. item 2` | `<ol>``<li> item 1 </li>``<li> item 2 </li>``</ol>` |
| `[Google](http://google.com)` | `<a href="http://google.com">Google</a>` |
| `![我的文本](https://github.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/r-ia-3e/img/图片路径)` | `<img src="图片路径", alt="我的文本">` |
| `\newpage` | 分页符——开始新的一页。(这是`rmarkdown`识别的 LaTeX 命令。) |

接下来是一个 R 代码块❸。Markdown 文档中的 R 代码由```` ```{r options} ````和```` ``` ````分隔。当文件被处理时,R 代码将被执行,并将结果插入。`echo=FALSE`选项将代码从输出中省略。表 21.3 列出了代码块选项。

表 21.3 代码块选项

| 选项 | 描述 |
| --- | --- |
| `echo` | 是否在输出中包含 R 源代码 `(TRUE)` 或不包含 `(FALSE)` |
| `results` | 是否输出原始结果 `(asis)` 或隐藏结果 `(hide)` |
| `warning` | 是否在输出中包含警告 `(TRUE)` 或不包含 `(FALSE)` |
| `message` | 是否在输出中包含信息性消息 `(TRUE)` 或不包含 `(FALSE)` |
| `error` | 是否在输出中包含错误消息 `(TRUE)` 或不包含 `(FALSE)` |
| `cache` | 保存结果,并且只有当代码发生变化时才重新运行代码块 |
| `fig.width` | 图形的宽度(英寸) |
| `fig.height` | 图形的宽度(英寸) |

简单的 R 输出(数字或字符串)也可以直接放置在报告文本中。内联 R 代码允许您自定义单个句子中的文本。内联代码放置在`` `r ``和`` ` ``标签之间。在回归示例中,样本大小、预测方程和 R 平方值嵌入在第一段❹中。

下一个 R 代码块创建了一个格式良好的 ANOVA 表❺。`tidy()`函数来自`broom`包,将回归结果导出为数据框(tibble)。`kable()`函数来自`knitr`包,将此数据框转换为 HTML 代码,`kable_styling()`函数来自`kableExtra`包,设置表格宽度和对齐方式,并添加颜色条纹。有关其他格式选项,请参阅`help(kable_ styling)`。

`kable()`和`kable_styling()`函数简单易用。R 中有几个包可以创建更复杂的表格并提供更多的样式选项。这些包括`xtable`、`expss`、`gt`、`huxtable`、`flextable`、`pixiedust`和`stargaze`r。每个包都有其优缺点(请参阅[`mng.bz/aKy9`](http://mng.bz/aKy9)和[`mng.bz/gxO8`](http://mng.bz/gxO8)以获取讨论)。使用最适合您需求的包。

R Markdown 文件的最后一部分打印出结果的`ggplot2`图形。图形大小设置为 5 英寸宽和 4 英寸高。默认为 7 英寸宽和 5 英寸高。

使用 RStudio 创建和处理 R Markdown 文档

RStudio 使得从 Markdown 文档中渲染报告变得特别容易。

如果你从 GUI 菜单中选择文件 > 新建文件 > R Markdown,你会看到下面的对话框。

![](https://github.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/r-ia-3e/img/CH21_F02_UN01_Kabacoff3.png)

在 RStudio 中创建新 R Markdown 文档的对话框

选择您想要生成的报告类型,RStudio 将为您创建一个骨架文件。使用您的文本和代码编辑它,然后从 Knit 下拉列表中选择渲染选项。就是这样!

![图片](https://github.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/r-ia-3e/img/CH21_F02_UN02_Kabacoff3.png)

从 R Markdown 文档生成 HTML、PDF 或 Word 报告的下拉菜单

Markdown 语法便于快速创建简单的文档。要了解更多关于 Markdown 的信息,请选择 HELP > Markdown 快速参考,或访问 Pandoc Markdown 参考页面 ([`mng.bz/0r8E`](http://mng.bz/0r8E))。

如果您想创建复杂的文档,如出版物质量的文章和书籍,那么您可能需要考虑使用 LaTeX 作为您的标记语言。在下一节中,您将使用 LaTeX 和 `knitr` 包来创建高质量的排版文档。

## 21.3 使用 R 和 LaTeX 创建报告

LaTeX 是一个用于高质量排版的文档准备系统,它对 Windows、macOS 和 Linux 平台免费提供。LaTeX 允许您创建美观、复杂的多部分文档,并且只需更改几行代码,就可以将一种类型的文档(如文章)转换为另一种类型的文档(如报告)。这是一款功能极其强大的软件,因此具有相当大的学习曲线。

如果您不熟悉 LaTeX,您可能想在继续之前阅读 Tobias Oetiker、Hubert Partl、Irene Hyna 和 Elisabeth Schlegl 所著的《LaTeX 2e 不那么简短的介绍》([`tobi.oetiker.ch/lshort/lshort.pdf`](https://tobi.oetiker.ch/lshort/lshort.pdf)) 或印度 TEX 用户组的《LaTeX 教程:入门》([`mng.bz/2c0O`](http://mng.bz/2c0O))。这门语言绝对值得学习,但要掌握它需要一些时间和耐心。一旦您熟悉了 LaTeX,创建动态报告就是一个简单的过程。

`knitr` 包允许您使用与之前创建网页类似的技术在 LaTeX 文档中嵌入 R 代码。如果您已安装 `rmarkdown` 或使用 RStudio,您已经拥有 `knitr`。如果没有,现在安装它(`install.packages("knitr")`)。此外,您还需要一个 LaTeX 编译器;有关详细信息,请参阅第 21.2 节。

在本节中,您将创建一个报告,描述患者对各种药物的反应,使用 `multcomp` 包中的数据。如果您在第九章中没有安装它,请确保在继续之前运行 `install.packages("multcomp")`。

要使用 R 和 LaTeX 生成报告,您首先创建一个扩展名为 .Rnw 的文本文件,其中包含报告文本、LaTeX 标记代码和 R 代码块。列表 21.2 提供了一个示例。每个 R 代码块以分隔符 `<<options>>=` 开始,以分隔符 `@` 结束。表 21.3 列出了代码块选项。使用 `\Sexpr{R code}` 语法包含内联 R 代码。当 R 代码被评估时,数字或字符串将被插入到文本中的该位置。

然后文件将由 `knit2pdf()` 函数进行处理:

library(knitr)
knit2pdf("drugs.Rnw")


或者通过在 RStudio 中按编译 PDF 按钮。在此步骤中,R 代码块被处理,并且根据选项,用 LaTeX 格式的 R 代码和输出替换。默认情况下,`knit("drugs.Rnw")` 输入文件 drugs.Rnw 并输出文件 drugs.tex。然后,.tex 文件通过 LaTeX 编译器运行,创建一个 PDF 文件。图 21.3 显示了生成的 PDF 文档。

列表 21.2 drugs.Rnw:一个嵌入 R 代码的 LaTeX 模板

\documentclass[11pt]{article}
\title{Sample Report}
\author{Robert I. Kabacoff, Ph.D.}
\usepackage{float}
\usepackage[top=.5in, bottom=.5in, left=1in, right=1in]{geometry}
\begin{document}
\maketitle
<<echo=FALSE, results='hide', message=FALSE>>=
library(multcomp)
library(dplyr)
library(xtable)
library(ggplot2)
df <- cholesterol
@

\section{Results}

Cholesterol reduction was assessed in a study
that randomized \Sexpr{nrow(df)} patients
to one of \Sexpr{length(unique(df$trt))} treatments.
Summary statistics are provided in
Table \ref{table:descriptives}.

<<echo=FALSE, results='asis'>>=
descTable <- df %>%
group_by(trt) %>%
summarize(N = n(),
Mean = mean(response, na.rm=TRUE),
SD = sd(response, na.rm=TRUE)) %>%
rename(Treatment = trt)
print(xtable(descTable, caption = "Descriptive statistics
for each treatment group", label = "table:descriptives"),
caption.placement = "top", include.rownames = FALSE)
@

The analysis of variance is provided in Table \ref{table:anova}.

<<echo=FALSE, results='asis'>>=
fit <- aov(response ~ trt, data=df)
print(xtable(fit, caption = "Analysis of variance",
label = "table:anova"), caption.placement = "top")
@

\noindent and group distributions are plotted in Figure \ref{figure:tukey}.

\begin{figure}[H]\label{figure:tukey}
\begin{center}

<<echo=FALSE, fig.width=4, fig.height=3>>=
ggplot(df, aes(x=trt, y=response )) +
geom_boxplot() +
labs(y = "Response", x="Treatment") +
theme_bw()
@

\caption{Distribution of response times by treatment.}
\end{center}
\end{figure}
\end{document}


`knitr` 包在 [`yihui.name/knitr`](http://yihui.name/knitr) 和 Yihui Xie 的书籍 *使用 R 和 knitr 创建动态文档*(Chapman & Hall,2013)中有文档说明。要了解更多关于 LaTeX 的信息,请查看前面提到的教程并访问 [www.latex-project.org](https://www.latex-project.org/)。

### 21.3.1 创建参数化报告

您可以在运行时向报告传递参数,这样您就可以自定义输出,而无需更改 R Markdown 模板。参数在带有 `params` 关键字的 YAML 标题中的部分定义。您使用 `$` 符号在代码的主体中访问参数值。

![](https://github.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/r-ia-3e/img/CH21_F03_Kabacoff3.png)

图 21.3 文件 drugs.Rnw 通过 `knit2pdf()` 函数处理,生成排版的 PDF 文档(drugs.pdf)。

考虑以下列表中的参数化报告。此 R Markdown 文档从雅虎财经 ([`finance.yahoo.com/`](https://finance.yahoo.com/)) 下载四家大型科技股(苹果、亚马逊、谷歌和微软)的股票价格,并使用 `ggplot2` 进行绘图(见图 21.4)。

![](https://github.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/r-ia-3e/img/CH21_F04_Kabacoff3.png)

图 21.4 从列表 21.3 中的参数化 R Markdown 文件生成的动态报告

默认情况下,代码报告了从文件编织时间起过去 30 天的股票表现。报告使用 `tidyquant` 包获取股票价格,因此在编织文档之前安装它 (`install.packages("tidyquant")`)。

列表 21.3 带参数的 R Markdown 报告(techstocks.Rmd)


title: "Tech Stock Monthly Report"
author: "IT Department"
output: html_document
params: ❶
enddate: !r Sys.Date() ❶
startdate: !r Sys.Date() - 30 ❶


Largest tech stocks

One month performance for

  • Apple (AAPL)
  • Amazon (AMZN)
  • Alphabet/Google (GOOG)
  • Microsoft (MSFT)

library(tidyquant)

library(ggplot2)

tickers = c("AAPL", "MSFT", "AMZN", "GOOG")

prices <- tq_get(tickers,                    ❷

                from = params$startdate,    ❷

                to = params$enddate,        ❷

                get = "stock.prices")       ❷

ggplot(prices, aes(x=date, y=adjusted, color=symbol)) +

geom_line(size=1) + facet_wrap(~symbol, scale="free") +

theme_bw() +

theme(legend.position="none") +

scale_x_date(date_labels="%m-%d") +

scale_color_brewer(palette="Set1") +

labs(title="股票价格图表",

    subtitle = paste(params$startdate, "至", params$enddate),

    x = "日期",

    y="调整价格")

Source: Yahoo Finance


❶ 定义参数值

❷ 使用参数值获取股票报价

参数 `enddate` 和 `startdate` 在 YAML 标题中定义 ❶。您可以硬编码这些值,或者使用 `!r` 包含评估为所需值的 R 代码。在此,`enddate` 设置为当前日期,`startdate` 设置为 30 天前。

当代码运行时,`tidyquant` 包中的 `tq_get()` 函数会下载由 `params$startdate` 和 `params$enddate` 定义的日期范围内的每日股票价格(符号、日期、开盘价、最高价、最低价、收盘价、成交量、调整后价格)❷。然后使用 `ggplot2` 进行绘图。

你可以通过向 `render` 函数提供一个值列表来在运行时覆盖参数值。例如,

render("techstocks.Rmd", params=list(startdate="2021-01-01",
enddate="2019-01-31"))


将绘制 2019 年 1 月的每日股票表现。或者,如果你运行 `render("techstocks.Rmd",` `params="ask")`(或者在 RStudio 中点击 Knit > Knit with parameters...),你将被提示输入 `startdate` 和 `enddate` 的值。你提供的值将覆盖默认值。包括参数值给你的报告增加了额外的交互性。要了解更多信息,请参阅 Yihui Xie、J.J Allaire 和 Garret Grolemund 的书籍 *R Markdown: The Definitive Guide*(Chapman & Hall,2019)的第 15.3 节。在线版本可在 [`bookdown.org/yihui/rmarkdown/`](https://bookdown.org/yihui/rmarkdown/) 找到。

## 21.4 避免常见的 R Markdown 问题

R Markdown 是在 R 中创建动态和吸引人的报告的强大工具。但有一些简单的常见错误需要避免(见表 21.4)。

表 21.4 修正常见的 R Markdown 错误

| 规则 | 正确 | 错误 |
| --- | --- | --- |
| YAML 标题中的缩进很重要。只有缩进子字段。 | `---``title: "Tech Stock Monthly Report"``author: "IT Department"``output: html_document``params:``enddate: 2019-01-31``startdate: 2019-01-01``---` | `---``title: "Tech Stock Monthly Report"``author: "IT Department"``output: html_document``params:``enddate: 2019-01-31``startdate: 2019-01-01``---` |
| 在标题标记后放置一个空格。 | # 这是一个一级标题。 | #This is a level one heading. |
| 列表应该由空行开头和结尾,并且星号标记后应该有一个空格。可选地,R 代码块可以标记,但每个标记必须是唯一的。 | `Here is a list`* `item one`* `item two````` ```{r anova1} ````R code```` ``` ```````` ```{r anova2} ````R code```` ``` ```` | `Here is a list`*`item one`*`item two````` ```{r anova} ````R code```` ``` ```````` ```{r anova} ````R code```` ``` ```` |
| 你不能在 R 代码块中安装包。 | 在 R Markdown 文档外部安装 `ggplot2`。然后使用```` ```r{} `````library(ggplot2)````` ``` ```` | ```` ```r{} `````install.packages(ggplot2)``library(ggplot2)````` ``` ```` |

在渲染 R Markdown 文档时发生的错误可能比简单 R 代码中的错误更难调试。如果你确保每个 R 代码块独立运行,并且你小心避免上述列出的错误,事情应该会进行得更顺利。

在继续之前,还有另一个话题要提一下。这更多的是关于低效而不是真正的错误。假设你创建了一个 R Markdown 文档,其中包含介绍性文本和一个执行耗时分析的 R 代码块。如果你编辑文本并重新编译文档,即使结果没有变化,你也会重新运行 R 代码。

为了避免这种情况,你可以缓存 R 代码块的结果。添加 R 代码块的 `option` `cache` `=` `TRUE` 将结果保存到文件夹中,并且只有在 R 代码本身发生变化时,代码才会在未来重新运行。如果代码没有变化,将插入保存的结果。请注意这里——缓存不会捕捉到基础数据的变化,只会捕捉到代码本身的变化。如果你更改了数据但未更改数据文件名,代码将不会重新运行。在这种情况下,你可以添加代码块的选项 `cache.extra` `=` `file.mtime(``mydatafile``)`,其中 `mydatafile` 是数据集的路径。如果数据文件的时间戳已更改,代码块将重新运行。

## 21.5 进一步探索

在本章中,你已经看到了几种将 R 结果纳入报告的方法。报告是动态的,因为更改数据和重新处理代码会导致报告更新。此外,可以通过传递参数来修改报告。你学习了创建网页、排版文档、Open Document Format 报告和 Microsoft Word 文档的方法。

本章中描述的模板方法有几个优点。通过将执行统计分析所需的代码直接嵌入到报告模板中,你可以确切地看到结果是如何计算的。六个月之后,你可以轻松地看到当时做了什么。你还可以修改统计分析或添加新数据,并立即以最小的努力重新生成报告。此外,你避免了剪切和粘贴以及重新格式化结果的需要。这本身就是值得的。

本章中提到的模板在意义上是静态的,即它们的结构是固定的。尽管这里没有涉及,你也可以使用这些方法来创建各种专家报告系统。例如,R 代码块的结果可以依赖于提交的数据。如果提交了数值变量,可以生成散点图矩阵。或者,如果提交了分类变量,可以生成马赛克图。以类似的方式,生成的解释性文本可以依赖于分析的结果。使用 R 的 `if/then` 构造和文本编辑函数,如 `grep` 和 `substr`,可以创造出无穷无尽的可定制性。基本上,你将模板写入临时位置,并在编织之前通过代码进行编辑。你可以使用这种方法创建一个复杂的专家系统。

要了解更多关于 R Markdown 的信息,请参阅 Yihui Xie、Christophe Dervieux 和 Emily Riederer 编著的 *R Markdown 烹饪书* ([`bookdown.org/yihui/rmarkdown-cookbook/`](https://bookdown.org/yihui/rmarkdown-cookbook/)),Yihui Xie、J.J Allaire 和 Garret Grolemund 编著的 *R Markdown: The Definitive Guide* ([`bookdown.org/yihui/rmarkdown/`](https://bookdown.org/yihui/rmarkdown/)),以及 RStudio 的 *R Markdown* 网站 ([`rmarkdown.rstudio.com/`](https://rmarkdown.rstudio.com/))。

## 摘要

+   R Markdown 可以用来创建结合文本、代码和输出的吸引人的报告。

+   报告可以输出为 HTML、Word、Open Document、RTF 和 PDF 格式。

+   R Markdown 文档可以进行参数化,允许你在运行时传递参数给代码,从而创建更动态的报告。

+   可以使用 LaTeX 标记语言来生成高度定制的复杂报告。学习曲线可能很陡峭。

+   R Markdown(以及 LaTeX)模板方法可以用来促进研究可重复性、支持文档编写并增强结果沟通。


# 22 创建软件包

本章涵盖

+   创建软件包的函数

+   添加软件包文档

+   构建软件包并与他人共享

在前面的章节中,你通过使用其他人提供的函数来完成大多数任务。这些函数来自基础 R 安装中的软件包或从 CRAN 下载的贡献软件包。

安装一个新的软件包扩展了 R 的功能。例如,安装 `mice` 软件包为你提供了处理缺失数据的新方法。安装 `ggplot2` 软件包为你提供了数据可视化的新方法。R 中许多最强大的功能都来自贡献软件包。

从技术上来说,一个软件包只是一个由函数、文档和数据以标准化格式保存的集合。软件包允许你以定义良好且完全文档化的方式组织你的函数,并便于与他人共享你的程序。

你可能想要创建软件包的几个原因:

+   使一组常用函数易于访问,并提供如何使用它们的文档。

+   创建一组示例和数据集,可以分发给课堂上的学生。

+   创建一个程序(一组相互关联的函数),可以用来解决一个重要的分析问题(例如,填补缺失值)。

+   通过组织研究数据、分析代码和文档到一个便携和标准化的格式来促进研究可重复性。

创建一个有用的软件包也是向他人介绍自己并回馈 R 社区的好方法。软件包可以直接共享或通过在线仓库如 CRAN 和 GitHub 进行共享。

在本章中,你将有机会从头到尾开发一个软件包。到本章结束时,你将能够构建自己的 R 软件包(并享受这种成就带来的自豪感和炫耀的权利)。

你将要开发的软件包名为 `edatools`。它提供了描述数据框内容的函数。这些函数故意设计得简单,这样你就可以专注于创建软件包的过程,而不会陷入代码细节。在第 22.1 节中,你将对 `edatools` 软件包进行测试。然后在第 22.2 节中,你将从零开始构建软件包的副本。

## 22.1 edatools 软件包

*探索性数据分析(EDA**)* 是一种通过描述数据的特征来理解数据的方法,这些特征通过统计摘要和可视化来展示。`edatools` 软件包是名为 `qacBase` 的综合 EDA 软件包的一个小子集(*[`rkabacoff.github.io/qacBase`](http://rkabacoff.github.io/qacBase)*)。

`edatools` 包包含描述和可视化数据框内容的函数。该包还包含一个名为 `happiness` 的数据集,其中包含 460 位个人对关于生活满意度的调查的回应。调查描述了个人对陈述“我大多数时间都很快乐”的同意程度评级,以及如收入、教育、性别、种族和子女数量等人口统计变量。评级采用从 *强烈不同意* 到 *强烈同意* 的 6 点量表。数据是虚构的,包含在内以使用户能够在一个包含多种类型变量和每个变量不同缺失数据级别的数据框中进行实验。

您可以使用以下代码安装 `edatools` 包:

if(!require(remotes)) install.packages("remotes")
remotes::install_github("rkabacoff/edatools")


这将从 `GitHub` 下载包并将其安装到您的默认 R 库中。

`edatools` 包有一个主要函数 `contents()`,用于收集有关数据框的信息,以及 `print()` 和 `plot()` 函数用于显示结果。以下列表展示了这些函数,图 22.1 提供了相应的图表。

列表 22.1 使用 `edatools` 包描述数据框

library(edatools)
help(contents)
df_info <- contents(happiness)
df_info

Data frame: happiness
460 observations and 11 variables
size: 0.1 Mb
pos varname type n_unique n_miss pct_miss
1 ID* character 460 0 0.000
2 Date Date 12 0 0.000
3 Sex factor 2 0 0.000
4 Race factor 8 92 0.200
5 Age integer 73 46 0.100
6 Education factor 13 23 0.050
7 Income numeric 415 46 0.100
8 IQ numeric 45 322 0.700
9 Zip character 10 0 0.000
10 Children integer 11 0 0.000
11 Happy ordered 6 18 0.039

plot(df_info)


![图片](https://github.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/r-ia-3e/img/CH22_F01_Kabacoff3.png)

图 22.1 描述 `happiness` 数据框内容的图表。有 11 个变量,`IQ` 变量的 70%数据缺失,`Race` 变量的 20%数据缺失。数据框包含 6 种不同的数据类型(有序因子、数值、整数等)。

打印输出列出了变量的数量和观测值数量以及数据框的大小(以兆字节为单位)。显示每个变量在数据框中的位置、名称、类型、唯一值的数量以及缺失值的数量和百分比。可以作为唯一标识符的变量(每行都有一个唯一值)被标记。图表按类型排列并按颜色编码变量,条形图的长度表示可用于分析的案例百分比。

从表格和图表中,您可以看到数据框有 11 个变量和 460 个观测值。它占用 0.1 兆字节的 RAM。`ID` 是一个唯一标识符,数据中有 10 个邮政编码,`IQ` 数据的 70%缺失。正如预期的那样,幸福评级(`happy`)是一个有序因子,有六个水平。您还能看到什么?

下一节描述了构建 R 包的一般步骤。在接下来的几节中,您将按顺序逐步构建 `edatools` 包。

## 22.2 创建包

创建 R 包曾经是一项艰巨的任务,仅限于一小部分经过高度训练的 R 专业人士(包括秘密握手)。随着用户友好型开发工具的出现,这个过程变得更加直接。然而,它仍然是一个详细的多步骤过程。步骤如下

1.  安装开发工具。

1.  创建一个包项目。

1.  添加函数。

1.  添加函数文档。

1.  添加一个一般帮助文件。

1.  添加样本数据和样本数据文档。

1.  添加一个示例文档。

1.  编辑 DESCRIPTION 文件。

1.  构建并安装包。

步骤 5-7 是可选的,但良好的开发实践。以下各节将依次介绍每个步骤。你也可以从 http://rkabacoff.com/RiA/edatools.zip 下载成品,以节省一些输入时间。

### 22.2.1 安装开发工具

在本章中,我假设你在构建包时使用的是 RStudio。此外,还有一些你想要安装的支持包。首先,使用以下命令安装`devtools`、`usethis`和`roxygen2`包:`install.packages(c("devtools", "usethis", "roxygen2"), depend=TRUE)`。`devtools`和`usethis`包包含简化并自动化包开发的函数。`roxygen2`包简化了包文档的创建。

剩余的软件是特定情况的:

+   包文档通常是 HTML 格式。如果你想以 PDF 格式创建文档(例如,手册和/或 vignettes),你需要安装 LaTeX,一个高质量的排版系统。有几种软件发行版可供选择。我推荐 TinyTex([`yihui.org/tinytex/`](https://yihui.org/tinytex/)),这是一个轻量级、跨平台的发行版。你可以使用以下代码安装它:

    ```
    install.packages("tinytex")
    tinytex::install_tinytex()
    ```

+   `pkgdown`包将帮助你为你的包创建一个网站。这是第 22.3.4 节中描述的可选步骤。

+   最后,如果你在 Windows 平台上,并且打算构建包含 C、C++或 Fortran 代码的 R 包,你需要安装 Rtools.exe([`cran.r-project.org/bin/windows/Rtools`](http://cran.r-project.org/bin/windows/Rtools))。安装说明在下载页面提供。macOS 和 Linux 用户已经拥有了必要的工具。虽然我在第 22.4 节中简要描述了外部编译代码的使用,但我们在这本书中不会使用它。

### 22.2.2 创建包项目

一旦你有了必要的工具,下一步就是创建一个包项目。当你运行以下代码时

library(usethis)
create_package("edatools")


使用`create_project()`函数创建的 RStudio 项目具有图 22.2 所示的文件夹结构。你将自动进入该项目,当前工作目录设置为`edatools`文件夹。

![](https://github.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/r-ia-3e/img/CH22_F02_Kabacoff3.png)

图 22.2 `edatools`项目文件夹的内容,由`create_project()`函数创建

函数代码将放在 R 文件夹中。其他重要的文件是 DESCRIPTION 和 NAMESPACE 文件。我们将在后面的章节中讨论它们。`.gitignore`、`.Rbuildignore`和`edatools.Rproj`是支持文件,允许你自定义包创建过程的一些方面。你可以保持它们不变。

### 22.2.3 编写包函数

`edatools`包由三个函数组成:`contents()`、`print .contents()`和`plot.contents()`。第一个是主要函数,用于收集有关数据框的信息,其他的是用于打印和绘图结果的 S3 面向对象泛型函数(见第 20.4.1 节)。

函数代码文件放置在 22.2.2 节中创建的 R 文件夹中。将每个函数放置在单独的以.R 扩展名的文本文件中是个好主意。这并不是严格必要的,但它使得组织工作更加容易。此外,函数名称和文件名称不需要匹配,但再次强调,这是良好的编码实践。每个文件都有一个包含以字符`#`开头的注释集的头信息。R 解释器会忽略这些行,但你会使用`roxygen2`包将这些注释转换为你的包文档。这些头注释将在 22.2.4 节中讨论。

列表 22.2 至 22.4 提供了文件。

收集数据框信息

contents.R 文本文件中的`contents()`函数描述了一个数据框,并将结果保存到一个列表中。函数调用为`contents(data)`,其中`data`是工作环境中数据框的名称。

列表 22.2 contents.R 文件的内容

' @title Description of a data frame

' @description

' \code{contents} provides describes the contents

' of a data frame.

'

' @param data a data frame.

'

' @importFrom utils object.size

'

' @return a list with 4 components:

' \describe{

' \item{dfname}

' \item{nrow}

' \item{ncol}

' \item{size}

' \item{varinfo}

' }

'

' @details

' For each variable in a data frame, \code{contents} describes

' the position, type, number of unique values, number of missing

' values, and percent of missing values.

'

' @seealso \link{print.contents}, \link

'

' @export

'

' @examples

' df_info <- contents(happiness)

' df_info

' plot(df_info)

contents <- function(data){

if(!(is.data.frame(data))){ ❶
stop("You need to input a data frame") ❶
}

dataname <- deparse(substitute(data)) ❷

size <- object.size(data) ❸

varnames <- colnames(data) ❹
colnames <- c("pos", "variable", "type", "n_unique", ❹
"n_miss", "pct_miss") ❹
pos = seq_along(data) ❹
varname <- colnames(data) ❹
type = sapply(data, function(x)class(x)[1]) ❹
n_unique = sapply(data, function(x)length(unique(x))) ❹
n_miss = sapply(data, function(x)sum(is.na(x))) ❹
pct_miss = n_miss/nrow(data) ❹
varinfo <- data.frame( ❹
pos, varname, type, n_unique, n_miss, pct_miss ❹
) ❹

results <- list(dfname=dataname, size=size, ❺
nrow=nrow(data), ncol=ncol(data), ❺
varinfo=varinfo) ❺
class(results) <- c("contents", "list") ❺
return(results) ❺
} ❺


❶ 检查输入

❷ 保存数据框名称

❸ 获取数据框大小

❹ 收集变量信息

❺ 返回结果

当函数被调用时,它会检查输入是否为数据框。如果不是,将生成错误❶。接下来,将数据框的名称记录为文本❷,并将数据框的大小记录为字节❸。对于每个变量,记录并保存其位置、名称、类型、唯一值的数量以及缺失值的数量和百分比,并将这些信息保存到一个名为`varinfo`的数据框中❹。最后,将结果打包并作为列表返回❺。列表包含五个组件,这些组件在表 22.1 中进行了总结。此外,将列表的类设置为`c("contents", "list")`。这是创建用于处理结果的泛型函数的关键步骤。

表 22.1 `contents()`函数返回的列表对象

| 组件 | 描述 |
| --- | --- |
| `dfname` | 数据框的名称 |
| `size` | 数据框的大小 |
| `nrow` | 行数 |
| `ncol` | 列数 |
| `varinfo` | 包含变量信息的数据框(位置、名称、类型、唯一值的数量以及缺失值的数量和百分比) |

虽然列表提供了所需的所有信息,但你很少直接访问其组件。相反,你会创建通用的`print()`和`plot()`函数,以更简洁和更有意义的方式呈现这些信息。我们将在下一节中考虑这些通用函数。

打印和绘图函数

任何广度上的分析函数通常都带有通用的`print()`和`summary()`函数。`print()`提供了关于对象的基本或原始信息,而`summary()`提供了更详细或处理过的(总结的)信息。当在给定上下文中绘制图形有意义时,通常会包括`plot()`函数。你只需要这个包的`print`和`plot`函数。

根据第 20.4.1 节中描述的 S3 OOP 指南,如果一个对象具有类属性 `"foo"`,那么 `print(x)` 将执行 `print.foo(x)`(如果存在)或 `print .default(x)`。对于 `plot()` 也是如此。因为 `contents()` 函数返回一个类为 `"contents"` 的对象,你需要定义 `print .contents``()` 和 `plot.contents()` 函数。以下列表给出了 `print.contents()` 函数。

列表 22.3 print.R 文件的内容

' @title Description of a data frame

'

' @description

' \code{print.contents} prints a concise description of a data frame.

'

' @param x an object of class \code{contents}.

' @param digits number of significant digits to use for percentages.

' @param ... additional arguments passed to the print function.

'

' @return NULL

' @method print contents

' @export

'

' @examples

' df_info <- contents(happiness)

' print(df_info, digits=4)

print.contents <- function(x, digits=2, ...){

if (!inherits(x, "contents")) ❶
stop("Object must be of class 'contents'") ❶

cat("Data frame:", x\(dfname, "\n") ❷ cat(x\)nrow, "observations and", x\(ncol, "variables\n") x\)varinfo\(varname <- ifelse(x\)varinfo\(n_unique == x\)nrow, ❸
paste0(x\(varinfo\)varname, "*"), ❸
x\(varinfo\)varname)
cat("size:", format(x\(size, units="Mb"), "\n") ❹ print(x\)varinfo, digits=digits, row.names=FALSE, ...) ❺
}


❶ 检查输入

❷ 打印标题

❸ 识别唯一标识符

❹ 打印数据框大小

❺ 打印变量信息

首先,函数检查输入是否具有类"`contents`" ❶。接下来,打印出包含数据框名称、变量数量和观测数量的标题 ❷。如果某一列中的唯一值数量等于观测数量,则该变量唯一标识每个案例。这样的变量在其名称后带有星号 (*) ❸。接下来,打印出数据框的大小(以兆字节为单位) ❹。最后,打印出变量信息的表格 ❺。

最后一个函数 `plot``()`,通过水平 `ggplot2` 条形图可视化 `contents()` 函数返回的结果。以下列表显示了代码。

列表 22.4 plot.R 文件的内容

' @title Visualize a data frame

'

' @description

' \code{plot.contents} visualizes the variables in a data frame.

'

' @details

' For each variable, the plot displays

' \itemize{

' \item{type (\code{numeric},

' \code{integer},

' \code{factor},

' \code{ordered factor},

' \code{logical}, or \code{date})}

' \item

' }

' Variables are sorted by type and the total number of variables

' and cases are printed in the caption.

'

' @param x an object of class \code{contents}.

' @param ... additional arguments (not currently used).

'

' @import ggplot2

' @importFrom stats reorder

' @method plot contents

' @export

' @return a \code{ggplot2} graph

' @seealso See \link{contents}.

' @examples

' df_info <- contents(happiness)

' plot(df_info)

plot.contents <- function(x, ...){
if (!inherits(x, "contents")) ❶
stop("Object must be of class 'contents'") ❶

classes <- x\(varinfo\)type ❷

pct_n <- 100 *(1 - x\(varinfo\)pct_miss) ❷

df <- data.frame(var = x\(varinfo\)varname, ❷
classes = classes, ❷
pct_n = pct_n, ❷
classes_n = as.numeric(as.factor(classes))) ❷

ggplot(df, ❸
aes(x=reorder(var, classes_n),
y=pct_n,
fill=classes)) +
geom_bar(stat="identity") +
labs(x="", y="Percent Available",
title=x\(dfname, caption=paste(x\)nrow, "cases",
x$ncol, "variables"),
fill="Type") +
guides(fill = guide_legend(reverse=TRUE)) + ❹
scale_y_continuous(breaks=seq(0, 100, 20)) +
coord_flip() + ❺
theme_minimal()
}


❶ 检查输入

❷ 生成要绘制的图形数据

❸ 绘制条形图

❹ 反转图例顺序

❺ 反转 x 轴和 y 轴

再次,你检查传递给函数的对象类别 ❶。接下来,变量名称、类别和非缺失数据的百分比被整理成一个数据框。每个变量的类别被附加为一个数字,用于在绘图时按类型对变量进行排序 ❷。数据以条形图的形式绘制 ❸,图例的顺序被反转,以便颜色垂直对齐条形 ❹。最后,x 轴和 y 轴被翻转,从而得到一个水平条形图 ❺。当你有很多变量或变量名称很长时,水平条形图可以帮助避免标签重叠。

### 22.2.4 添加函数文档

每个 R 包都遵循相同的强制文档指南。包中的每个函数都必须以相同的方式使用 LaTeX 进行文档化。每个函数都放置在一个单独的 .R 文件中,该函数的文档(用 LaTeX 编写)放置在一个 .Rd 文件中。.R 和 .Rd 文件都是文本文件。

这种方法有两个限制。首先,文档存储在与它描述的函数分开的地方。如果你更改了函数代码,你必须找到文档并对其进行更改。其次,用户必须学习 LaTeX。如果你认为 R 的学习曲线很陡峭,那么等你开始使用 LaTeX 时会更陡峭!

`roxygen2` 包可以显著简化文档的创建。您在每个.R 文件的开头放置注释,这些注释将作为函数的文档。这些注释以字符`#'`开头,并使用一组简单的标记标签(称为*roclets*)。表 22.2 列出了常见标签。R 解释器将这些行视为注释并忽略它们。但是当文件由`roxygen2`处理时,以`#'`开头的行将用于自动生成 LaTeX (.Rd) 文档文件。

表 22.2 与 `roxygen2` 一起使用的标签

| 标签 | 描述 |
| --- | --- |
| `@title` | 函数标题 |
| `@description` | 一行函数描述 |
| `@details` | 多行函数描述(第一行后缩进) |
| `@parm` | 函数参数 |
| `@export` | 使此函数对您的包用户可用 |
| `@import` | 从一个包中导入所有函数以供自己的包函数使用 |
| `@importFrom` | 选择性地从包中导入函数以供自己的包函数使用 |
| `@return` | 函数返回的值 |
| `@author` | 作者和联系地址 |
| `@examples` | 使用该函数的示例 |
| `@note` | 关于函数操作的任何说明 |
| `@references` | 关于函数使用的方法的参考文献 |
| `@seealso` | 链接到相关函数 |

除了表 22.2 中的标签外,还有一些标记元素在创建文档时很有用:

+   `\code{*text*}`以代码字体打印文本。

+   `\link{*function*}`生成一个超文本链接,以帮助另一个 R 函数。

+   `\href{*URL*}{*text*}`添加一个超链接。

+   `\item{*text*}`可用于生成项目符号列表。

列表 22.2 中 `contents()` 函数的 `roxygen2` 注释在列表 22.5 中重现。首先,指定函数的标题和描述 ❶。接下来,描述参数 ❷。标签可以按任何顺序出现。

列表 22.5 `roxygen2` 对 `contents()` 函数的注释

' @title Description of a data frame ❶

' ❶

' @description ❶

' \code{contents} provides describes the contents ❶

' of a data frame. ❶

'

' @param data a data frame. ❷

'

' @importFrom utils object.size ❸

'

' @return a list with 4 components: ❹

' \describe{ ❹

' \item{dfname}{name of data frame} ❹

' \item{nrow}{number of rows} ❹

' \item{ncol}{number of columns} ❹

' \item{size}{size of the data frame in bytes} ❹

' \item{varinfo}{data frame of overall dataset characteristics} ❹

' }

'

' @details

' For each variable in a data frame, \code{contents} describes

' the position, type, number of unique values, number of missing

' values, and percent of missing values.

'

' @seealso \link{print.contents}, \link

'

' @export ❺

'

' @examples

' df_info <- contents(happiness)

' df_info

' plot(df_info)


❶ 标题和描述

❷ 参数

❸ 导入函数

❹ 返回值

❺ 导出函数

`@importFrom` 标签表示此函数使用 `utils` 包中的 `object.size()` 函数 ❸。当您想从给定包中使一组有限的函数可用时使用 `@importFrom`。使用 `@import` 函数使包中的所有函数都可用。例如,`plot.contents()` 函数使用 `@import ggplot2` 标签使所有 `ggplot2` 函数对该函数可用。

`@return` 标签表示函数返回的内容(在本例中是一个列表)。它将出现在帮助文档的 `Value` 标题下。使用 `@details` 提供有关函数的更多信息。`@seealso` 标签提供对其他感兴趣函数的超链接。`@export` 标签使函数对用户可用 ❺。如果你省略它,函数仍然可以被包中的其他函数访问,但不能直接从命令行调用。对于不应该由用户直接调用的函数,应省略 `@export` 标签。

最后,`@examples` 标签允许你包含一个或多个演示函数的示例。请注意,这些示例应该能正常工作!否则,用 `\dontrun{}` 标记代码包围示例代码。例如,使用

@examples
\dontrun{
contents(prettyflowers)
}


如果没有名为 `prettyflowers` 的数据框。由于代码被 `\dontrun{}` 包围,它不会抛出错误。`\dontrun{}` 标记通常用于示例需要很长时间才能运行的情况。

### 22.2.5 添加一个通用的帮助文件(可选)

通过为每个函数添加 `roxygen2` 注释,在文档和构建步骤(第 22.3 节)期间生成帮助文件。当包安装并加载时,输入 `?contents` 或 `help(contents)` 将会显示该函数的帮助文件。但包名称本身没有帮助信息。换句话说,`help(edatools)` 将会失败。

用户如何知道哪些包函数可用?一种方法是通过输入 `help(package="edatools")`,但你可以通过添加另一个文件到文档中使其更容易。将文件 `edatools.R`(列表 22.6)添加到 R 文件夹中。

列表 22.6 `edatools.R` 文件的内容

' @title Functions for exploring the contents of a data frame.

'

' @details

' edatools provides tools for exploring the variables in

' a data frame.

'

' @docType package

' @name edatools-package

' @aliases edatools

NULL
... this file must end with a blank line after the NULL...


注意,此文件的最后一行必须是空的。当包构建时,调用 `help(edatools)` 将会生成一个包含可点击链接到函数索引的包描述。

### 22.2.6 添加样本数据到包中(可选)

当你创建一个包时,包括一个或多个可以用来演示其函数的数据集是一个好主意。对于 `edatools` 包,你将添加 `happiness` 数据框。它包含六种类型的变量以及每个变量不同数量的缺失数据。

要将数据框添加到包中,首先将其放置在内存中。以下代码将 `happiness` 数据框加载到全局环境中:

load(url("http://rkabacoff.com/RiA/happiness.rdata"))


接下来,运行

library(usethis)
use_data(happiness)


这将创建一个名为 data 的文件夹(如果不存在),并将 `happiness` 数据框作为名为 happiness.rda 的压缩 .rda 文件放入其中。

你还需要创建一个 .R 文件来记录数据框。以下列表给出了代码。

列表 22.7 `happiness.R` 文件的内容

' @title Happiness Dataset

'

' @description

' A data frame containing a happiness survey and demographic data.

' This data are fictitious.

'

' @source

' The data were randomly generated using functions from the

' \href{https://cran.r-project.org/web/packages/wakefield/index.html}

' package.

'

' @format A data frame with 460 rows and 11 variables:

' \describe{

' \item{\code{ID}}

' \item{\code{Date}}

' \item{\code{Sex}}{factor. Sex coded as \code{Male} or \code{Female}.}

' \item{\code{Race}}

' \item{\code{Age}}

' \item{\code{Education}}

' \item{\code{Income}}

' \item{\code{IQ}}{double. Adult intelligence quotient. This

' variable has a large amount of missing data.}

' \item{\code{Zip}}

' \item{\code{Children}}

' \item{\code{Happy}}{factor. Agreement with the statement

' "I am happy most of the time", coded as \code{Strongly Disagree} ,

' \code{Disagree}, \code{Neutral}, \code{Agree}, or

' \code{Strongly Agree}.}

' }

"happiness"


注意,列表 22.7 中的代码完全由注释组成。将其放置在 R 文件夹中与函数代码文件一起。

### 22.2.7 添加一个示例文章(可选)

如果你包含一篇简要的文章来描述其功能和用途,人们更有可能使用你的包。要创建一个示例文章,运行

library(usethis)
use_vignette("edatools", "Introduction to edatools")


这将创建一个名为 vignettes 的文件夹(如果不存在),并在其中放置一个名为 edatools.Rmd 的 RMarkdown 模板文件。它还会在 RStudio 中打开文件以供编辑。第 21.2 节描述了编辑 RMarkdown 文档。以下列表显示了完成的文档。

列表 22.8 `edatools` 文档


title: "Introduction to edatools"
output: rmarkdown::html_vignette
vignette: >
%\VignetteIndexEntry{Introduction to edatools}
%\VignetteEngine{knitr::rmarkdown}
%\VignetteEncoding


knitr::opts_chunk$set(

collapse = TRUE,

comment = "#>"

)

The edatools package is a demonstration project for learning how to
build an R package. It comes from chapter 22 of R in Action (3rd ed.).

The package has one main function for describing a data frame, and two generic functions.


library(edatools)

df_info <- contents(happiness)

print(df_info, digits=3)

plot(df_info)


当包安装后,用户可以使用 `vignette` `("edatools")` 访问文档。

### 22.2.8 编辑 DESCRIPTION 文件

每个包都有一个包含元数据(如包标题、版本、作者、许可和包依赖)的 DESCRIPTION 文件。当你在 22.2.2 节中调用 `build_package("edatools")` 时,该文件会自动创建。`usethis` 包的其他函数将向 DESCRIPTION 文件添加更多信息。

首先,指出你的包运行所需的贡献包。`edatools` 包依赖于 `ggplot2` 包来运行。执行

library(usethis)
use_package("ggplot2")


将此要求添加到 DESCRIPTION 文件中。如果你有多个依赖项,你可以多次调用此命令。你不需要指定任何基础 R 包。当 `edtools` 安装时,如果缺少所需的贡献包,它们也会被安装。

接下来,指出包发布的许可协议。常见的许可类型包括 MIT、GPL-2 和 GPL-3。请参阅 [www.r-project.org/Licenses](http://www.r-project.org/Licenses) 了解许可描述。我们将使用 MIT 许可,它基本上意味着你可以对这种 AS IS 软件做任何你想做的事情,只要保留版权声明。输入

use_mit_license("your name here")


在控制台中添加许可到你的包。

最后,手动编辑 DESCRIPTION 文件。由于它是一个简单的文本文件,你可以在 RStudio 或任何文本编辑器中编辑它。以下列表给出了 DESCRIPTION 文件的最终版本。

列表 22.9 DESCRIPTION 文件内容

Package: edatools
Title: Functions for Exploratory Data Analysis
Version: 0.0.0.9000
Authors@R:
person(given = "Robert",
family = "Kabacoff",
role = c("aut", "cre"),
email = "rkabacoff@wesleyan.edu")
Description: This package contains functions for
exploratory data analysis. It is a demonstration
package for the book R in Action (3rd ed.).
License: MIT + file LICENSE
Encoding: UTF-8
LazyData: true
Roxygen: list(markdown = TRUE)
RoxygenNote: 7.1.1
Depends:
R (>= 2.10)
Imports:
ggplot2
Suggests:
rmarkdown,
knitr
VignetteBuilder: knitr


你需要编辑的唯一字段是 `Title`、`Version`、`Authors@R` 和 `Description`。`Title` 应该是单行句型。`Version` 号码由你决定。约定是 *`major.minor.patch.dev`*。请参阅 [`r-pkgs.org/release.html`](https://r-pkgs.org/release.html) 了解详细信息。

在 `Authors@R:` 部分,指定每个贡献者和他们的角色。在这里,我输入了我的名字,并表明我是一个完整作者(`"auth"`)和包维护者(`"cre"`)。当然,当你创建自己的包时,不要使用我的名字(除非这个包真的很优秀!)请参阅 [`mng.bz/eMlP`](http://mng.bz/eMlP) 了解参与者角色。

`描述:`部分可以跨越多行,但必须在第一行之后缩进。`LazyData:` `yes`语句表示该包中的数据集(在本例中为`happiness`)应在包加载时即可使用。如果设置为`no`,则用户必须使用`data(happiness)`来访问数据集。

### 22.2.9 构建和安装包

终于到了构建包的时候了。(真的,我保证。)此时,你的包应该具有图 22.3 中描述的结构。斜体文件夹和文件是可选的。(注意,plot.R 和 print.R 文件仅因为`edatools`包包含专门的打印和绘图函数而必需。)

![图片](https://github.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/r-ia-3e/img/CH22_F03_Kabacoff3.png)

图 22.3 在构建步骤之前`edatools`包的文件和文件夹结构。斜体文件夹和文件是可选的。

RStudio 有一个用于构建包的构建选项卡(见图 22.4)。点击更多下拉菜单并选择配置构建工具。这将弹出图 22.5 中的对话框。确保两个选项都已勾选。然后点击配置按钮以弹出 Roxygen 选项对话框。确保前六个选项已勾选(见图 22.6)。

![图片](https://github.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/r-ia-3e/img/CH22_F04_Kabacoff3.png)

图 22.4 RStudio 构建选项卡。使用此选项卡上的选项创建包文档、构建和安装完成的包。

![图片](https://github.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/r-ia-3e/img/CH22_F05_Kabacoff3.png)

图 22.5 项目选项对话框。确保两个复选框都已勾选。

![图片](https://github.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/r-ia-3e/img/CH22_F06_Kabacoff3.png)

图 22.6 `Roxygen`选项对话框。确保前六个选项已勾选。

现在有两个简单的步骤。首先,通过选择更多 > 文档(或 Ctrl+Shift+D)来构建文档。这将把.R 文件中的`roxygen2`注释转换为 LaTeX 帮助文件(以.Rd 结尾)。.Rd 文件放置在名为 man(意为手册)的文件夹中。文档命令还会向 DESCRIPTION 文件和 NAMESPACE 文件添加信息。

下面的列表给出了生成的 NAMESPACE 文件,该文件控制了函数的可见性。所有函数都直接对包用户可用,还是有些函数被其他函数内部使用?在当前情况下,所有函数都对用户可用。它还使贡献包中的函数对您的包可用。在这种情况下,`object.size()`、`reorder()`和所有`ggplot2`函数都可以调用。要了解更多关于命名空间的信息,请参阅[`adv-r.had.co.nz/Namespaces.html`](http://adv-r.had.co.nz/Namespaces.html)。

列表 22.10 NAMESPACE 文件内容

Generated by roxygen2: do not edit by hand

S3method(plot,contents)
S3method(print,contents)
export(contents)
import(ggplot2)
importFrom(stats,reorder)
importFrom(utils,object.size)


接下来,点击安装和重启按钮。这将构建包,将其安装到你的 R 本地库中,并将其加载到会话中。此时,你的包将具有图 22.7 中描述的文件结构。恭喜,你的包现在可以使用了!

我的 vignette 在哪里?

如果您在软件包中添加了 vignette,并遵循本节中的说明,您将看不到 vignette。为什么?因为默认情况下,“安装和重启”按钮不会构建 vignette。由于这可能是一个耗时的过程,所以假设开发者不会希望在每次重新构建软件包时都这样做。当您构建一个*源代码包*(第 22.2.9 节)时,vignette 将被构建并添加到其中。要在您的系统上查看它,请从源代码包安装软件包(包选项卡 > 安装 > 从包存档文件安装)。或者,您可以运行以下代码:

library(devtools)
build_vignettes()
install(build_vignettes=TRUE)


![](https://github.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/r-ia-3e/img/CH22_F07_Kabacoff3.png)

图 22.7 运行“文档和安装及重启”后的软件包结构。再次说明,可选文件和文件夹以斜体显示。

在继续之前,您应该检查软件包中可能存在的问题。在“构建”选项卡上点击“检查”按钮会对软件包进行广泛的完整性检查。在与其他人共享软件包之前运行这些检查是个好主意。如果您发现任何错误,请纠正它们,重新编写文档(如果文档已更改),并重新构建软件包。

## 22.3 分享您的软件包

一旦您创建了一个有用的软件包,您可能希望与他人分享它。有几种常见的 R 软件包分发方法,包括以下这些:

+   分发源代码或二进制包文件

+   将软件包提交到 CRAN

+   在 GitHub 上托管软件包

+   创建一个软件包网站

在本节中,我们将逐一查看。

### 22.3.1 分发源代码包文件

您可以将您的软件包打包成一个单一的压缩文件,通过电子邮件或云服务发送给他人。在“构建”选项卡上,点击更多 > 构建源代码包。这将在一个名为 edatools 的文件夹之上创建一个源代码包文件。在这个例子中,edatools_0.0.0.90000.tar.gz 将出现在 edatools 文件夹的一级目录上。文件名中的版本号是从 DESCRIPTION 文件中获取的。现在,软件包已经是一个可以发送给他人或提交给 CRAN 的格式。

收件人可以通过“包”选项卡选择安装,并指定从包存档文件安装。或者,他们可以使用以下代码

install.packages(choose.files(), repos = NULL, type="source")


这将弹出一个对话框,允许他们选择源代码包。

如果您已安装 LaTeX 发行版(第 22.2.1 节),您还可以为您的软件包创建 PDF 手册。运行

library(devtools)
build_manual()


在控制台中,并在父目录中创建 PDF 手册。

### 22.3.2 提交到 CRAN

综合 R 档案网络(CRAN)是分发贡献软件包的主要服务。今天早上,计数为 17,788,但这个数字已经过时了。要将您的软件包贡献给 CRAN,请遵循以下四个步骤:

1.  阅读 CRAN 仓库政策([`cran.r-project.org/web/packages/policies.html`](http://cran.r-project.org/web/packages/policies.html)).

1.  确保软件包通过所有检查,没有错误或警告(构建选项卡 > 检查)。否则,软件包将被拒绝。

1.  创建一个源包文件(见第 22.3.1 节)。

1.  提交包。要通过网页表单提交,请使用[`cran.r-project.org/submit.html`](http://cran.r-project.org/submit.html)上的提交表单。你将收到一封自动确认电子邮件,需要接受。

    但请不要上传你刚刚创建的`edatools`包到 CRAN。你现在有了创建自己包的工具。

### 22.3.3 在 GitHub 上托管

许多包开发者即使在 CRAN 上发布了包,也会在 GitHub 上托管他们的包([`www.github.com`](http://www.github.com))。GitHub 是一个流行的 Git 仓库托管服务,拥有许多附加功能。托管包在 GitHub 上有几个很好的理由:

+   你的包可能还没有准备好进入主流(即,它还没有完全开发)。

+   你希望允许其他人与你一起工作在包上或给你反馈和建议。

+   你希望将生产版本托管在 CRAN 上,将当前开发版本托管在 GitHub 上。

+   你希望在开发过程中使用 Git 版本控制。(很好,但不是必需的。)

在 GitHub 上托管你的包对个人和组织都是免费的。

要在 GitHub 上托管包,首先在你的包中添加一个 REAME.md 文件。在控制台输入

library(usethis)
use_readme_md()


这将在编辑器中放置一个名为 README.md 的文本文件。该文件使用类似于 RMarkdown 的简单 Markdown 语言。在 RStudio 中,你可以通过帮助>Markdown 快速参考来获取详细信息。以下列表显示了`edatools`包的 README.md 文件。

列表 22.11 README.md 文件的内容

edatools

This is a demonstration package for the book [R in Action (3rd ed.)]
(https://www.manning.com/books/r-in-action-third-edition).
It contains functions for exploratory data analysis.

Installation

You can install this package with the following code:


if(!require(remotes)){

install.packages("remotes")

}

remotes::install_github("rkabacoff/edatools")

Example

This is a basic example which shows you how to describe a data frame:


library(edatools)

df_info<- contents(happiness)

df_info

plot(df_info)


保存文件,你就可以在 GitHub 上托管包了。

要托管包,

1.  注册账户并登录。

1.  点击“新建”以创建一个新的仓库。在下一页,给它起一个与你的包相同的名字(见图 22.8)。使用默认选项并点击“创建仓库”。

    ![](https://github.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/r-ia-3e/img/CH22_F08_Kabacoff3.png)

    图 22.8 GitHub 创建新仓库页面。输入包名并点击创建仓库。

1.  在下一页,选择“上传现有文件”。这个选项可能很难看到(见图 22.9)。请注意,这假设你想要直接上传包文件。如果你使用 Git 版本控制,过程将不同。有关 Happy Git and GitHub 的用户详情([`happygitwithr.com/`](https://happygitwithr.com/))。

1.  在下一页,上传你的包文件夹的内容(包中的文件和文件夹,而不是包文件夹本身)。点击页面底部的“提交更改”按钮。不要忘记这一步,否则文件将不会显示。

1.  给每个人分配 URL([`github.com/youraccountname/packagename`](https://github.com/youraccountname/packagename))。

![](https://github.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/r-ia-3e/img/CH22_F09_Kabacoff3.png)

图 22.9 GitHub 快速设置或新建仓库页面。选择上传现有文件。

你可以在 [`github.com/Rkabacoff/edatools`](https://github.com/Rkabacoff/edatools) 查看关于 `edatools` 软件包的 GitHub 仓库。

### 22.3.4 创建一个软件包网站

专门的网站可以是一个很好的推广和支持您软件包的方式。您可以使用 `pkgdown` 软件包([`pkgdown.r-lib.org/`](https://pkgdown.r-lib.org/))创建这样的网站。在软件包项目中,输入

library(pkgdown)
build_site()


在控制台中。这将为您项目添加一个名为 docs 的文件夹,其中包含创建网站所需的 HTML、层叠样式表和 JavaScript 文件。只需将这些文件放在 Web 服务器上,就可以使您的软件包网站可用。

GitHub Pages 为您的项目提供免费的网站。只需将 docs 文件夹上传到您的 GitHub 软件包仓库(第 22.3.3 节)。转到仓库设置,点击 GitHub Pages 链接。对于源,选择 main、/docs,然后保存(见图 22.10)。您的网站现在可在 [`youraccountname.github.io/packagename`](https://youraccountname.github.io/) 上访问。

![](https://github.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/r-ia-3e/img/CH22_F10_Kabacoff3.png)

图 22.10 GitHub Pages 设置页面

`edatools` 软件包网站是 [`rkabacoff.github.io/edatools`](https://rkabacoff.github.io/edatools)(图 22.11)。

![](https://github.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/r-ia-3e/img/CH22_F11_Kabacoff3.png)

图 22.11 基于 GitHub Pages 的 `edatools` 网站

注意,REAME.md 文件的内容提供了主页的文本。`edatools` 演示文档链接到了“入门”标签。每个函数都在“参考”标签中进行了描述和演示。

在构建网站之前,通过编辑 docs 文件夹中的 _pkgdown.yml 文件,可以轻松地自定义您的网站。有关详细信息,请参阅“自定义您的网站”([`mng.bz/pJw2`](http://mng.bz/pJw2))。

## 22.4 进一步学习

在本章中,用于创建 `edtools` 软件包的所有代码都是 R 代码。实际上,大多数软件包都包含完全用 R 编写的代码。但您也可以从 R 中调用编译后的 C、C++、Fortran 和 Java 代码。外部代码通常在这样做可以提高执行速度或作者希望在 R 代码中使用现有的外部库时包含。

包含编译后的外部代码有几种方法。有用的基础 R 函数包括 `.C()`、`.Fortran()`、`.External()` 和 `.Call()`。还有一些软件包被编写来简化这个过程,包括 `inline`(C、C++、Fortran)、`Rcpp`(C++)和 `rJava`(Java)。将外部编译代码添加到 R 软件包超出了本书的范围。请参阅 [`r-pkgs.org/src.html`](https://r-pkgs.org/src.html) 了解更多信息。

有许多优秀的资源可以帮助你学习更多关于开发 R 包的知识。“Writing R Extensions”由 R 核心团队编写([`cran.r-project.org/doc/manuals/r-release/R-exts.html`](https://cran.r-project.org/doc/manuals/r-release/R-exts.html))是关于这个主题的原始文本。Hadley Wickham 和 Jenny Bryan 的《R Packages》([`r-pkgs.org/`](https://r-pkgs.org/))包含大量信息,并且可以在网上免费获取。Cosima Meyer 和 Dennis Hammershmidt 提供了一篇关于“如何在 CRAN 上编写和发布你的 R 包”的全面博客文章([`mng.bz/O14o`](http://mng.bz/O14o))。

R 包是组织你常用函数、开发完整应用程序以及与他人分享你结果的绝佳方式。在这个可重复研究的时代,它也可以是捆绑一个大型项目所有数据和代码的绝佳方式。在本章中,你从头到尾创建了一个完整的 R 包。尽管一开始包看起来很复杂,但一旦掌握了流程,创建它们就变得相当简单。现在,开始工作吧!并且记住,享受这个过程!

## 摘要

+   R 包是一组以标准化格式保存的函数、文档和数据。

+   R 包可以用来简化对常用函数的访问、解决复杂的分析问题、与他人共享代码和数据,并为 R 社区做出贡献。

+   开发一个包是一个多步骤的过程。例如`devtools`、`usethis`和`roxygen2`这样的包可以简化这一过程。

+   步骤包括创建包项目、编写函数和文档、构建和安装包,以及将包分发给他人。

+   可以通过分发源代码或二进制包文件、提交到 CRAN 以及/或托管在 GitHub 上来与他人共享包。

+   你可以使用`pkgdown`包和 GitHub 页面轻松创建一个支持你的包的网站。它也可以成为你数据科学组合的一部分。


# 后记。进入兔子洞

本书涵盖了广泛的主题,包括主要主题如 R 开发环境、数据管理、传统统计模型和统计图形。我们还探讨了诸如重采样统计、缺失值插补和交互式图形等隐藏的宝藏。R 的伟大(或许可以说是令人恼火)之处在于总有更多东西可以学习。

R 是一个庞大、健壮且不断发展的统计平台和编程语言。随着如此多的新包、频繁的更新和新方向,如何保持最新?幸运的是,许多网站支持这个活跃的社区,并提供关于平台和包更改、新方法和大量教程的覆盖。我列出了我最喜欢的网站:

+   统计计算 R 项目 ([www.r-project.org](http://www.r-project.org))

    官方 R 网站,您了解所有 R 事物的第一站。该网站包括广泛的文档,包括“R 简介”、“R 语言定义”、“编写 R 扩展”、“R 数据导入/导出”、“R 安装和管理”和“R 常见问题解答”。

+   R 杂志 ([`journal.r-project.org`](http://journal.r-project.org))

    一份免费、经过同行评审的期刊,包含关于 R 项目和贡献包的文章。

+   R 博客园 ([www.r-bloggers.com](http://www.r-bloggers.com))

    一个中央枢纽(博客聚合器),收集关于 R 的博客作者的博客内容。它每天包含新的文章。我对此上瘾。

+   CRAN 橙子 ([`dirk.eddelbuettel.com/cranberries`](http://dirk.eddelbuettel.com/cranberries))

    一个汇总有关新和更新包的信息的网站,并为每个 CRAN 包提供链接。

+   统计软件杂志 ([www.jstatsoft.org](http://www.jstatsoft.org))

    一份免费、经过同行评审的期刊,包含关于统计计算的文章、书评和代码片段。包含关于 R 的频繁文章。

+   CRAN 任务视图 ([`cran.r-project.org/web/views`](http://cran.r-project.org/web/views))

    任务视图是关于在不同学术和研究领域中如何使用 R 的指南。它们包括关于给定区域的可用包和方法的描述。目前,有 41 个任务视图可用(见下表)。

| CRAN 任务视图 |
| --- |
| 贝叶斯推断 | 使用 R 进行模型部署 |
| 化学计量学和计算物理学 | 多变量统计 |
| 临床试验设计、监控和分析 | 自然语言处理 |
| 聚类分析和有限混合模型 | 数值数学 |
| 使用 R 的数据库 | 官方统计和调查方法 |
| 微分方程 | 优化和数学规划 |
| 概率分布 | 药代动力学数据分析 |
| 计量经济学 | 系统发育学,特别是比较方法 |
| 生态和环境数据分析 | 心理测量模型和方法 |
| 实验设计(DoE)和实验数据分析 | 可重复研究 |
| 极值分析 | 坚韧统计方法 |
| 实证金融 | 社会科学统计学 |
| 函数数据分析 | 空间 |
| 统计遗传学 | 处理和分析时空数据 |
| 图形、图形设备和可视化 | 生存分析 |
| 使用 R 进行高性能和并行计算 | 统计教学 |
| 水文数据和建模 | 时间序列分析 |
| 机器学习和统计学习 | 跟踪数据的处理和分析 |
| 医学图像分析 | 网络技术和服务 |
| 元分析 | R 中的图形模型 |
| 缺失数据 |  |

+   B Book of R ([`www.bigbookofr.com/`](https://www.bigbookofr.com/))

    本网站包含免费 R 相关电子书的存档列表。

+   R 邮件列表 ([`www.r-project.org/mail.html`](https://www.r-project.org/mail.html))

    这个电子邮件列表是询问 R 问题的最佳地方。存档也是可搜索的。在提问前请务必阅读常见问题解答(FAQ)。

+   Cross Validated ([`stats.stackexchange.com`](http://stats.stackexchange.com))

    这是一个面向对统计学和数据科学感兴趣的人的问答网站。这是一个发布关于 R 的问题并查看其他人都在问什么的好地方。如果你遇到了难题,这是一个绝佳的去处。

+   Quick-R ([www.statmethods.net](http://www.statmethods.net))

    这是我的 R 网站。它包含了超过 80 个关于 R 主题的简要教程。过度的谦逊使我无法再说更多。

+   使用 R 进行数据可视化 ([`rkabacoff.github.io/datavis`](http://rkabacoff.github.io/datavis))

    这是我的 R 图形网站。与上面的免责声明相同。

R 社区是一个有帮助、充满活力和令人兴奋的群体。欢迎来到仙境。


# 附录 A. 图形用户界面

您是第一个来到这里的,对吧?默认情况下,R 提供了一个简单的 *命令行界面* (CLI)。用户在命令行提示符(默认为 `>`)中输入语句,每个命令一次执行。对于许多数据分析师来说,CLI 是 R 的一个重大限制。

已尝试创建更多图形界面,范围从与 R 交互的代码编辑器(如 RStudio)到特定函数或包的 GUI(如 BiplotGUI),再到允许您通过菜单和对话框交互来构建分析的完整 GUI(如 R Commander)。

表 A.1 列出了几个非常有用的代码编辑器,这些编辑器允许您编辑和执行 R 代码,并包括语法高亮、语句完成、对象探索、项目管理以及在线帮助。RStudio 是迄今为止最受欢迎的 R 程序员的 *集成开发环境* (IDE),但拥有选择总是好的。

表 A.1 集成开发环境和语法编辑器

| 名称 | 网址 |
| --- | --- |
| RStudio 桌面版 | [`www.rstudio.com/products/rstudio/`](https://www.rstudio.com/products/rstudio/) |
| R Tools for Visual Studio | [`mng.bz/VGjr`](http://mng.bz/VGjr) |
| Eclipse 与 StatET 插件 | [`projects.eclipse.org/projects/science.statet`](https://projects.eclipse.org/projects/science.statet) |
| Architect | [`www.getarchitect.io/`](https://www.getarchitect.io/) |
| ESS(Emacs Speaks Statistics) | [`ess.r-project.org`](http://ess.r-project.org) |
| Atom 编辑器与 Rbox | [`atom.io/`](https://atom.io/) 和 [`atom.io/packages/rbox`](https://atom.io/packages/rbox) |
| Notepad++ 与 NppToR(仅限 Windows) | [`notepad-plus-plus.org`](http://notepad-plus-plus.org) 和 [`sourceforge.net/projects/npptor`](http://sourceforge.net/projects/npptor) |

表 A.2 列出了几个有希望的、完整的 R GUI。R 可用的 GUI 比 SAS 或 IBM SPSS 提供的 GUI 稍显不全面和不成熟,但它们正在快速发展。

表 A.2 R 的综合图形用户界面

| 名称 | 网址 |
| --- | --- |
| JGR/Deducer | [`rforge.net/JGR/`](http://rforge.net/JGR/) 和 [`www.deducer.org`](http://www.deducer.org) |
| R AnalyticFlow | [`r.analyticflow.com/en/`](http://r.analyticflow.com/en/) |
| jamovi | [`www.jamovi.org/jmv/`](https://www.jamovi.org/jmv/) |
| JASP | [`jasp-stats.org/`](https://jasp-stats.org/) |
| Rattle(数据挖掘用) | [`rattle.togaware.com`](http://rattle.togaware.com) |
| R Commander | [`socialsciences.mcmaster.ca/jfox/Misc/Rcmdr/`](https://socialsciences.mcmaster.ca/jfox/Misc/Rcmdr/) |
| RkWard | [`rkward.kde.org/`](https://rkward.kde.org/) |
| Radiant | [`radiant-rstats.github.io/docs/install.html`](https://radiant-rstats.github.io/docs/install.html) |

我最喜欢的用于入门统计课程的 GUI 是 R Commander(如图 A.1 所示)。

![](https://github.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/r-ia-3e/img/APPA_F01_Kabacoff3.png)

图 A.1 R Commander GUI

最后,有几个应用程序允许您为 R 函数(包括用户编写的函数)创建 GUI 包装器。这些包括 R GUI 生成器(RGG)([`rgg.r-forge.r-project.org`](http://rgg.r-forge.r-project.org)) 以及来自 CRAN 的 `fgui` 和 `twiddler` 软件包。目前最全面的方法是 `Shiny` ([`shiny.rstudio.com/`](https://shiny.rstudio.com/)),它让您能够轻松创建带有交互式访问 R 函数的 Web 应用程序和仪表板。


# 附录 B. 自定义启动环境

程序员喜欢做的第一件事之一就是自定义他们的启动环境,以符合他们偏好的工作方式。自定义启动环境允许您设置 R 选项、指定工作目录、加载常用包、加载用户编写的函数、设置默认 CRAN 下载站点,并执行任何数量的维护任务。

您可以通过站点初始化文件 (Rprofile.site) 或目录初始化文件 (.Rprofile) 来自定义 R 环境。这些是包含在启动时执行的 R 代码的文本文件。

启动时,R 将从 R_HOME/etc 目录中源文件 Rprofile.site,其中 `R_HOME` 是一个环境变量。然后它将在当前工作目录中寻找要源文件的 .Rprofile 文件。如果 R 找不到此文件,它将在用户的主目录中寻找。您可以使用 `Sys.getenv("R`*_*`HOME")`、`Sys.getenv ("HOME")` 和 `getwd()` 分别识别 R_HOME、HOME 和当前工作目录的位置。

您可以在这些文件中放置两个特殊函数。`.First()` 函数在每个 R 会话开始时执行,而 `.Last()` 函数在每个会话结束时执行。Rprofile.site 文件的示例在列表 B.1 中展示。

列表 B.1 样本 .Rprofile

options(digits=4) ❶
options(show.signif.stars=FALSE) ❶
options(scipen=999) ❶
options

options(prompt="> ") ❷
options(continue=" ") ❷

options(repos = c(CRAN = "https://cran.rstudio.com/")) ❸

.libPaths("C:/my_R_library") ❹

.env <- new.env() ❺
.env\(h <- utils::head ❺ .env\)t <- utils::tail ❺
.env\(s <- base::summary ❺ .env\)ht <- function(x){ ❺
base::rbind(utils::head(x), utils::tail(x)) ❺
} ❺
.env$phelp <- function(pckg){ ❺
utils::help(package = deparse(substitute(pckg))) ❺
} ❺
attach(.env) ❺

.First <- function(){ ❻
v <- R.Version() ❻
msg1 <- paste0(v\(version.string, ' -- ', ' "', v\)nickname, '"') ❻
msg2 <- paste0("Platform: ", v$platform) ❻
cat("\f") ❻
cat(msg1, "\n", msg2, "\n\n", sep="") ❻

if(interactive()){ ❻
suppressMessages(require(tidyverse)) ❻
} ❻

} ❻

.Last <- function(){ ❼
cat("\nGoodbye at ", date(), "\n") ❼
} ❼


❶ 设置常用选项

❷ 设置 R 交互式提示符

❸ 设置 CRAN 镜像默认值

❹ 设置本地库路径

❺ 创建快捷键

❻ 启动函数

❼ 会话结束函数

让我们看看这个 .Rprofile 实现了什么:

+   打印的输出被定制。有效数字的位数设置为 4(默认为 7),系数摘要表上不打印星号,并且抑制了科学记数法。

+   提示符的第一行设置为 ">", 而续行使用空格(而不是 "+" 符号)。

+   `install.packages()` 命令的默认 CRAN 镜像站点设置为 cran.rstudio.com。

+   为已安装的包定义了一个个人目录。设置 `.libPaths` 值允许您在 R 目录树之外为包创建本地库。这在升级期间保留包可能很有用。

+   为 `tail()`、`head()` 和 `summary()` 函数定义了一键快捷键。定义了新的函数 `ht()` 和 `phelp()`。`ht()` 结合了 head 和 tail 的输出。`phelp()` 列出包中的函数(带有链接的帮助)。

+   `.First()` 函数

    +   用较短的定制欢迎信息替换标准欢迎信息。在我的机器上,新的欢迎信息只是这样:

        R 版本 4.1.0 (2021-05-18)—"Camp Pontanezen"

        平台:x86_64-w64-mingw32/x64 (64 位)

    +   将 `tidyverse` 包加载到交互会话中。`tidyverse` 的启动信息被抑制。

+   `.Last()` 函数打印一个定制的告别信息。这也是执行任何清理活动的绝佳位置,包括存档命令历史、程序输出和数据文件。

警告!如果你在 .Rprofile 文件中定义函数或加载包,而不是在脚本中,它们将变得不太便携。你的代码现在依赖于启动文件才能正确运行。在其他缺少此文件的机器上,它们将无法运行。

如果你想给你的 .Rprofile 文件加速,可以看看 Jumping Rivers 的 `rprofile` 包([`www.jumpingrivers.com/blog/customising-your-rprofile/`](https://www.jumpingrivers.com/blog/customising-your-rprofile/))。还有其他方法可以自定义启动环境,包括使用命令行选项和环境变量。更多详情请参阅“R 简介”手册中的 `help(Startup)` 和附录 B([`cran.r-project.org/doc/manuals/R-intro.pdf`](http://cran.r-project.org/doc/manuals/R-intro.pdf))。


# 附录 C. 从 R 导出数据

在第二章中,我们回顾了将数据导入 R 的多种方法。但有时,您可能想要走另一条路——从 R 导出数据——以便数据可以存档或导入到外部应用中。在本附录中,您将学习如何将 R 对象输出到分隔文本文件、Excel 电子表格或统计应用(如 SPSS、SAS 或 Stata)。

## C.1 分隔文本文件

您可以使用 `write.table()` 函数将 R 对象输出到分隔文本文件。格式如下

write.table(X, outfile, sep=delimiter, quote=TRUE, na="NA")


其中 `x` 是对象,`outfile` 是目标文件。例如,以下语句

write.table(mydata, "mydata.txt", sep=",")


将数据集 `mydata` 保存为当前工作目录中名为 mydata.txt 的逗号分隔文件。要保存输出文件到其他位置,请包含路径(例如,`"c:/myprojects/mydata.txt"`)。将 `sep=","` 替换为 `sep="\t"` 将数据保存为制表符分隔文件。默认情况下,字符串用引号(`""`)括起来,缺失值以 `NA` 写入。

## C.2 Excel 电子表格

`xlsx` 包中的 `write.xlsx()` 函数可用于将 R 数据框保存到 Excel 2007 工作簿。格式如下

library(xlsx)
write.xlsx(X, outfile, col.Names=TRUE, row.names=TRUE,
sheetName="Sheet 1", append=FALSE)


例如,以下语句

library(xlsx)
write.xlsx(mydata, "mydata.xlsx")


将数据框 `mydata` 导出为当前工作目录中名为 mydata.xlsx 的 Excel 工作簿中的工作表(默认为 Sheet 1)。默认情况下,数据集中的变量名用于创建电子表格中的列标题,行名放置在电子表格的第一列。如果 mydata.xlsx 已经存在,则会被覆盖。

`xlsx` 包是操作 Excel 工作簿的强大工具。有关更多详细信息,请参阅包文档。

## C.3 统计应用

`foreign` 包中的 `write.foreign()` 函数可用于将数据框导出到外部统计应用。创建了两个文件:一个包含数据的自由格式文本文件和一个包含将数据读入外部统计应用的指令的代码文件。格式如下

write.foreign(dataframe, datafile, codefile, package=package)


例如,以下代码

library(foreign)
write.foreign(mydata, "mydata.txt", "mycode.sps", package="SPSS")


将数据框 `mydata` 导出为当前工作目录中名为 mydata.txt 的自由格式文本文件,以及一个名为 mycode.sps 的 SPSS 程序,该程序可用于读取文本文件。`package` 的其他值包括 `"SAS"` 和 `"Stata"`。

要了解更多关于从 R 导出数据的信息,请参阅“R 数据导入/导出”文档,可在 [`cran.r-project.org/doc/manuals/R-data.pdf`](http://cran.r-project.org/doc/manuals/R-data.pdf) 获取。


# 附录 D. R 中的矩阵代数

本书描述的许多函数都作用于矩阵。矩阵操作深深植根于 R 语言中。表 D.1 描述了特别重要的运算符和函数,用于解决线性代数问题。在表中,`A` 和 `B` 是矩阵,`x` 和 `b` 是向量,`k` 是标量。

表 D.1 矩阵代数函数和运算符

| 运算符或函数 | 描述 |
| --- | --- |
| `+ - * / ^` | 分别为逐元素加法、减法、乘法、除法和指数运算 |
| `A %*% B` | 矩阵乘法 |
| `A %o% B` | 外积:`AB'` |
| `cbind(A, B, ...)` | 水平组合矩阵或向量。返回一个矩阵。 |
| `chol(A)` | `A` 的 Cholesky 分解。如果 `R <- chol(A)`,那么 `chol(A)` 包含上三角因子,使得 `R'R = A`。 |
| `colMeans(A)` | 返回包含 `A` 的列平均值的向量 |
| `crossprod(A)` | 返回 `A'A` |
| `crossprod(A,B)` | 返回 `A'B` |
| `colSums(A)` | 返回包含 `A` 的列和的向量 |
| `diag(A)` | 返回包含主对角线元素的向量 |
| `diag(x)` | 创建一个主对角线元素为 `x` 的对角矩阵 |
| `diag(k)` | 如果 `k` 是一个标量,则创建一个 `k` × `k` 的单位矩阵。自己去想吧。 |

| `eigen(A)` | `A` 的特征值和特征向量。如果 `y <- eigen(A)`,那么

+   `y$val` 是 `A` 的特征值。

+   `y$vec` 是 `A` 的特征向量。

|

| `ginv(A)` | `A` 的摩尔-彭罗斯广义逆。 (需要 `MASS` 包。) |
| --- | --- |

| `qr(A)` | `A` 的 QR 分解。如果 `y <- qr(A)`,那么

+   `y$qr` 有一个包含分解的上三角和一个包含分解信息的下三角。

+   `y$rank` 是 `A` 的秩。

+   `y$qraux` 是一个包含 Q 的附加信息的向量。

+   `y$pivot` 包含有关旋转策略的信息。

|

| `rbind(A, B, ...)` | 垂直组合矩阵或向量。返回一个矩阵。 |
| --- | --- |
| `rowMeans(A)` | 返回包含 `A` 的行平均值的向量 |
| `rowSums(A)` | 返回包含 `A` 的行和的向量 |
| `solve(A)` | `A` 是一个方阵时,返回 `A` 的逆 |
| `solve(A, b)` | 求解方程 `b = Ax` 中的向量 `x` |

| `svd(A)` | `A` 的单值分解。如果 `y <- svd(A)`,那么

+   `y$d` 是包含 `A` 的奇异值的向量。

+   `y$u` 是一个矩阵,其列包含 `A` 的左奇异向量。

+   `y$v` 是一个矩阵,其列包含 `A` 的右奇异向量。

|

| `t(A)` | `A` 的转置 |
| --- | --- |

几个用户贡献的包特别适用于矩阵代数。`matlab` 包包含用于尽可能接近地复制 MATLAB 函数调用的包装函数和变量。这些函数可以帮助您将 MATLAB 应用程序和代码移植到 R。同时,还有一个有用的速查表,可以在 [`mathesaurus.sourceforge.net/octave-r.html`](http://mathesaurus.sourceforge.net/octave-r.html) 将 MATLAB 语句转换为 R 语句。

`Matrix` 包包含将 R 扩展到支持高度密集或稀疏矩阵的函数。它提供了对 BLAS(基本线性代数子程序)、LAPACK(密集矩阵)、TAUCS(稀疏矩阵)和 UMFPACK(稀疏矩阵)例程的高效访问。

最后,`matrixStats` 包提供了对矩阵的行和列进行操作的方法,包括计算计数、总和、乘积、中心趋势、离散度等函数。每个函数都针对速度和高效内存使用进行了优化。


# 附录 E. 本书使用的包

R 从无私的作者贡献中获得了其广度和力量的很大一部分。表 E.1 列出了本书中描述的用户贡献包,以及它们出现的章节。一些包的作者太多,无法在此列出。此外,许多包都得到了贡献者的增强。有关详细信息,请参阅包文档。

表 E.1 本书使用的贡献包

| 包 | 作者 | 描述 | 章节 |
| --- | --- | --- | --- |
| `AER` | Christian Kleiber 和 Achim Zeileis | 来自 Christian Kleiber 和 Achim Zeileis 的《使用 R 的应用计量经济学》一书中的函数、数据集、示例、演示和示例 | 13 |
| `boot` | S 原版由 Angelo Canty 提供,R 版由 Brian Ripley 提供。 | Bootstrap 函数 | 12 |
| `bootstrap` | S 原版由 Rob Tibshirani 提供,R 版由 Friedrich Leisch 转译。 | 软件和来自 B. Efron 和 R. Tibshirani 的《Bootstrap 简介》的数据(Chapman and Hall, 1993)(bootstrap、交叉验证、Jackknife) | 8 |
| `broom` | David Robinson, Alex Hayes 和 Simon Couch | 总结 tidy tibbles 中的关键信息 | 21 |
| `car` | John Fox, Sanford Weisberg 和 Brad Price | 伴随应用回归的函数 | 8, 9, 11 |
| `carData` | John Fox, Sanford Weisberg 和 Brad Price | 伴随应用回归的数据集 | 7 |

表 E.2 本书使用的贡献包

| 包 | 作者 | 描述 | 章节 |
| --- | --- | --- | --- |
| `cluster` | Martin Maechler, Peter Rousseeuw(Fortran 原版),Anja Struyf(S 原版)和 Mia Hubert(S 原版) | 聚类分析的方法 | 16 |
| `clusterability` | Zachariah Neville, Naomi Brownstein, Maya Ackerman 和 Andreas Adolfsson | 执行数据集聚类趋势的测试 | 16 |
| `coin` | Torsten Hothorn, Kurt Hornik, Mark A. van de Wiel 和 Achim Zeileis | 在排列检验框架中的条件推断程序 | 12 |
| `colorhcplot` | Damiano Fantini | 使用颜色突出显示的组构建树状图 | 16 |
| `corrgram` | Kevin Wright | 绘制 corrgram | 11 |
| `DALEX` | Przemyslaw Biecek, Szymon Maksymiuk 和 Hubert Baniecki | 用于探索和解释的模型无关语言 | 17 |
| `devtools` | Hadley Wickham, Jim Hester 和 Winston Chang | 使开发 R 包更容易的工具 | 22 |
| `directlabels` | Toby Dylan Hocking | 多颜色图的直接标签 | 15 |
| `doParallel` | Michelle Wallig, 微软公司和 Steve Weston | `foreach` 并行适配器,用于 `parallel` 包 | 20 |
| `dplyr` | Hadley Wickham, Romain François, Lionel Henry 和 Kirill Müller | 数据操作语法的语法 | 3, 5, 6, 7, 9, 16, 19, 21 |
| `e1071` | David Meyer, Evgenia Dimitriadou, Kurt Hornik, Andreas Weingessel 和 Friedrich Leisch | 维也纳科技大学统计学、概率论小组的杂项函数 | 17 |
| `effects` | John Fox 和 Jangman Hong | 线性、广义线性、多项 logit 和比例优势 logit 模型的效应显示 | 8, 9 |
| `factoextra` | Alboukadel Kassambara | 提取和可视化多元数据分析的结果 | 16 |
| `flexclust` | Friedrich Leish 和 Evgenia Dimnitriadou | 灵活的聚类算法 | 16 |
| `foreach` | Michelle Wallig, 微软公司, 和 Steve Weston | 为 R 提供 foreach 循环结构 | 20 |
| `forecast` | Rob J. Hyndman 和许多其他作者 | 时间序列和线性模型的预测函数 | 15 |
| `gapminder` | Jennifer Bryan | 来自 [Gapminder.org](http://gapminder.org/) 的数据摘录 | 19 |
| `Ggally` | Barret Schloerke 和许多其他作者 | 扩展 `ggplot2` 的功能 | 11 |
| `ggdendro` | Andrie de Vries 和 Brian D. Ripley | 使用 `ggplot2` 创建树状图和树形图 | 16 |
| `ggm` | Giovanni M. Marchetti, Mathias Drton 和 Kayvan Sadeghi | 用于边缘化、条件化和最大似然拟合的工具 | 7 |
| `ggplot2` | Hadley Wickam 和许多其他作者 | 图形语法的实现 | 4, 6, 9, 10, 11, 12, 15, 16, 18, 19, 20, 21 |
| `ggrepel` | Kamil Slowikowski | 使用 `ggplot2` 自动定位非重叠文本标签 | 19 |
| `gmodels` | Gregory R. Warnes;包括 Ben Bolker、Thomas Lumley 和 Randall C. Johnson 贡献的 R 源代码和/或文档。Randall C. Johnson 的贡献受版权保护(SAIC-Frederick, Inc., 2005)。 | 用于模型拟合的各种 R 编程工具 | 7 |
| `haven` | Hadley Wickham 和 Evan Miller | 导入和导出 SPSS、Stata 和 SAS 文件 | 2 |
| `Hmisc` | Frank E. Harrell, Jr. | 数据分析、高级图形、实用操作等方面的杂项函数 | 7 |
| `ISLR` | Gareth James, Daniela Witten, Trevor Hastie 和 Rob Tibshirani | 《统计学习导论及其在 R 中的应用》的数据 | 19 |
| `kableExtra` | Hao Zhu | 构建复杂的 HTML 和 LaTeX 表格 | 21 |
| `knitr` | Yihui Xie | R 中动态报告生成的通用包 | 21 |
| `leaps` | Thomas Lumley,使用 Alan Miller 的 Fortran 代码 | 回归子集选择,包括穷举搜索 | 8 |
| `lmPerm` | Bob Wheeler | 线性模型的排列检验 | 12 |
| `MASS` | 由 Venables 和 Ripley 创作的原版;R 版由 Brian Ripley 转换,遵循 Kurt Hornik 和 Albrecht Gebhardt 早期的工作 | 支持 Venables 和 Ripley 的《S 的现代应用统计》第四版(Springer,2003)的函数和数据集 | 7, 9, 12, 13, 14, 附录 D |
| `mice` | Stef van Buuren 和 Karin Groothuis-Oudshoorn | 通过链式方程进行多元插补 | 18 |
| `mosaicData` | Randall Pruim, Daniel Kaplan 和 Nicholas Horton | 来自 Project MOSAIC 的数据集 | 4 |
| `multcomp` | 托尔斯坦·霍恩、弗兰克·布雷茨·彼得·韦斯特法尔、理查德·M·海伯格和安德烈·舒特岑迈斯特 | 在参数模型中,包括线性、广义线性、线性混合效应和生存模型中的一般线性假设的同步测试和置信区间 | 9, 12, 21 |
| `MultiRNG` | 哈坎·德米塔斯、拉万·阿洛齐和兰·高 | 11 个多元分布的伪随机数生成 | 5 |
| `mvoutlier` | 莫里茨·格施万特纳和彼得·菲尔茨莫瑟 | 基于稳健方法的多元异常值检测 | 9 |
| `naniar` | 尼古拉斯·蒂尔尼、迪·库克、迈尔斯·麦克贝恩和科林·费 | 缺失数据的结构、摘要和可视化 | 18 |
| `NbClust` | 马利卡·查拉德、纳迪亚·加扎利、维罗尼克·博特和阿扎姆·尼卡法斯 | 确定聚类数量的指标研究 | 16 |
| `partykit` | 托尔斯坦·霍恩、海迪·索贝尔和阿奇姆·泽莱伊斯 | 递归分割工具包 | 17 |
| `pastecs` | 弗雷德里克·伊班内斯、菲利普·格罗斯让和米歇尔·埃蒂安 | 空间时间生态序列分析包 | 7 |
| `patchwork` | 托马斯·林·佩德森 | 由多个图表组成的组合 | 19 |
| `pkgdown` | 霍德尔·威克汉姆和杰伊·赫塞尔贝斯 | 为包创建吸引人的 HTML 文档和网站 | 22 |
| `plotly` | 卡森·西维特和许多其他作者 | 通过 plotly.js 创建交互式网络图形 | 19 |
| `psych` | 威廉·雷维尔 | 心理学、心理测量和人格研究程序 | 7, 14 |
| `pwr` | 斯蒂芬·尚佩利 | 功率分析的基本函数 | 10 |
| `qcc` | 卢卡·斯库尔卡 | 质量控制图 | 13 |
| `randomForest` | 最初由 Fortran 编写,作者为 Leo Breiman 和 Adele Cutler;R 语言版本由 Andy Liaw 和 Matthew Wiener 移植 | Breiman 和 Cutler 的随机森林用于分类和回归 | 17 |
| `rattle` | 格雷厄姆·威廉姆斯、马克·韦尔·卡尔普、埃德·科克斯、安东尼·诺兰、丹尼斯·怀特、达尼埃莱·梅德里、阿卡巴尔·瓦利杰(随机森林的 OOB AUC)和布莱恩·里普利([print.summary.nnet](http://print.summary.nnet)的原始作者) | R 语言数据挖掘的图形用户界面 | 16, 17 |
| `readr` | 霍德尔·威克汉姆和吉姆·赫斯特 | 灵活导入矩形文本数据 | 2 |
| `readxl` | 霍德尔·威克汉姆和詹妮弗·布莱恩 | 导入 Excel 文件 | 2 |
| `rgl` | 丹尼尔·阿德勒和邓肯·默多克 | 3D 可视化设备系统(OpenGL) | 11 |
| `rmarkdown` | JJ·阿莱尔、叶辉和许多其他作者 | 将 R Markdown 文档转换为各种文档 | 21 |
| `robustbase` | 马丁·迈歇勒和许多其他作者 | 使用稳健方法分析数据的工具 | 13 |
| `rpart` | 特里·瑟诺、贝丝·阿特金森和布莱恩·里普利(R 语言初始版本作者) | 递归分割和回归树 | 17 |
| `rrcov` | 瓦伦丁·托多罗夫 | 具有高破坏点的稳健位置和散布估计以及稳健的多变量分析 | 9 |
| `scales` | 霍德尔·威克汉姆和达娜·塞德尔 | 数据可视化的缩放函数 | 6, 19 |
| `scatterplot3d` | Uwe Ligges | 绘制三维(3D)点云图 | 11 |
| `showtext` | Yixuan Qiu 和其他多位作者 | 在 R 图中使用各种类型的字体 | 19 |
| `sqldf` | G. Grothendieck | 使用 SQL 操作 R 数据框 | 3 |
| `tidyquant` | Matt Dancho 和 Davis Vaughan | 定量金融分析的整洁工具 | 21 |
| `tidyr` | Hadley Wickham | 整洁混乱数据的工具,包括数据旋转、嵌套和去嵌套 | 5, 16, 20 |
| `treemapify` | David Wilkins | 提供 `ggplot2` 地图元素用于绘制树状图 | 6 |
| `tseries` | Adrian Trapletti 和 Kurt Hornik | 时间序列分析和计算金融 | 15 |
| `usethis` | Hadley Wickham 和 Jennifer Bryan | 自动化包和项目设置任务 | 22 |
| `vcd` | David Meyer, Achim Zeileis 和 Kurt Hornik | 可视化分类数据的功能 | 1, 6, 7, 11, 12 |
| `VIM` | Matthias Templ, Andreas Alfons 和 Alexander Kowarik | 可视化和缺失值插补 | 18 |
| `xtable` | David B. Dahl 和其他多位作者 | 将表格导出到 LaTeX 或 HTML | 21 |
| `xts` | Jeffrey A. Ryan 和 Joshua M. Ulrich | 统一处理不同基于时间的数据类别 | 15 |


# 附录 F. 处理大型数据集

R 将其所有对象存储在虚拟内存中。对于我们大多数人来说,这个设计决策导致了快速的交互式体验,但对于处理大型数据集的分析师来说,它可能导致程序执行缓慢和内存相关错误。

内存限制主要取决于 R 的构建版本(32 位与 64 位)以及涉及的操作系统版本。以“无法分配大小为”开头的错误信息通常表明无法获得足够的连续内存,而以“无法分配长度为”开头的错误信息则表明已超出地址限制。当处理大型数据集时,如果可能的话,尽量使用 64 位构建版本。查看`help(Memory)`获取更多信息。

当处理大型数据集时,有三个问题需要考虑:提高编程效率以加快执行速度、外部存储数据以限制内存问题,以及使用专门设计的统计程序以有效地分析大量数据。首先,我们将考虑每个问题的简单解决方案。然后,我们将转向更全面(且更复杂)的解决方案,以处理*大数据*。

## F.1 高效编程

几个编程技巧可以帮助你在处理大型数据集时提高性能:

+   当可能时,将计算向量化。使用 R 内置的用于操作向量、矩阵和列表的函数(例如,`ifelse`、`colMeans`和`rowSums`),并在可行的情况下避免使用循环(`for`和`while`)。

+   使用矩阵而不是数据框(它们的开销更小)。

+   当导入分隔符文本文件时,使用来自`data.table`包的优化函数`fread()`或来自`vroom`包的`vroom()`。它们比基础 R 的`read.table()`函数快得多。

+   初始时正确设置对象的大小,而不是通过追加值从小对象增长。

+   对于重复的、独立的和数值密集型任务,使用并行化。

+   在尝试在完整数据集上运行之前,在数据样本上测试程序以优化代码并删除错误。

+   删除临时对象和不再需要的对象。调用`rm(list=ls())`将从内存中删除所有对象,提供一个干净的起点。可以使用`rm(object)`删除特定对象。删除大对象后,调用`gc()`将启动垃圾回收,确保对象从内存中删除。

+   使用 Jeromy Anglim 在其博客条目“R 中的内存管理:一些技巧和窍门”中描述的函数`.ls.objects()`来按大小(MB)排序列出所有工作空间对象([`mng.bz/xGpq`](http://mng.bz/xGpq))。此函数将帮助你找到并处理内存消耗者。

+   分析程序以查看每个函数花费了多少时间。你可以使用`Rprof()`和`summaryRprof()`函数完成此操作。`system.time()`函数也可以有所帮助。`profr`和`prooftools`包提供了可以帮助你分析分析输出的函数。

+   使用编译的外部例程来加速程序执行。当需要更优化的子例程时,您可以使用`Rcpp`包将 R 对象传输到 C++函数并返回。

第 20.5 节提供了向量化、高效数据输入、正确设置对象大小和并行化的示例。

对于大型数据集,提高代码效率只能走这么远。当您遇到内存限制或代码执行缓慢时,请考虑升级您的硬件。您还可以将数据存储在外部,并使用专门的分析例程。

## F.2 在 RAM 之外存储数据

有几个包可用于在 R 的主内存之外存储数据。该策略涉及将数据存储在外部数据库或磁盘上的二进制平面文件中,然后按需访问。表 F.1 列出了几个有用的包。

表 F.1 R 包用于访问大型数据集

| 包 | 描述 |
| --- | --- |
| `bigmemory` | 支持创建、存储、访问和操作庞大矩阵。矩阵分配到共享内存和内存映射文件。 |
| `disk.frame` | 一个基于磁盘的数据操作框架,用于处理大于 RAM 的数据集 |
| `ff` | 提供了存储在磁盘上的数据结构,但表现得像它们在 RAM 中一样 |
| `filehash` | 实现了一个简单的键值数据库,其中字符串键与存储在磁盘上的数据值相关联 |
| `ncdf`、`ncdf4` | 提供了访问 Unidata NetCDF 数据文件的接口 |
| `RODBC`、`RMySQL`、`ROracle`、`RPostgreSQL`、`RSQLite` | 每个都提供了访问外部关系型数据库管理系统的接口。 |

这些包有助于克服 R 在数据存储上的内存限制。但是,当您尝试在合理的时间内分析大型数据集时,您还需要专门的方法。以下是一些最有用的方法。

## F.3 内存外数据分析包

R 提供了几个用于分析大型数据集的包:

+   `biglm`和`speedglm`包以内存高效的方式对大型数据集进行线性模型和广义线性模型的拟合。这为处理大型数据集提供了`lm()`和`glm()`类型的功能。

+   几个包提供了用于处理由`bigmemory`包生成的庞大矩阵的分析函数。`biganalytics`包提供了 k-means 聚类、列统计以及`biglm`的包装器。`bigrf`包可用于拟合分类和回归森林。`bigtabulate`包提供了`table()`、`split()`和`tapply()`功能,而`bigalgebra`包提供了高级线性代数函数。

+   当与`ff`包一起使用时,`biglars`包为太大而无法存储在内存中的数据集提供了最小角回归、lasso 和逐步回归。

+   `data.table` 包提供了一个 `data.frame` 的增强版本,包括更快的聚合;更快的有序和重叠范围连接;以及通过引用按组快速添加、修改和删除列(无需复制)。您可以使用 `data.table` 结构处理大型数据集(例如,RAM 中 100 GB),并且它与任何期望数据框的 R 函数兼容。

这些包针对特定目的适应大型数据集,并且相对容易使用。接下来将描述用于分析千兆级数据的更全面解决方案。

## F.4 与海量数据集一起工作的综合解决方案

当我撰写这本书的第一版时,在这个部分我能说的最多就是,“好吧,我们正在尝试。”从那时起,结合高性能计算(HPC)和 R 语言的项目的数量激增。本节提供了使用 R 处理千兆级数据集的一些更流行方法的指针。每种方法都需要对 HPC 和其他软件平台(如 Hadoop,一个用于在分布式计算环境中处理大型数据集的免费 Java 软件框架)的使用有一定了解。

表 F.2 描述了使用 R 处理大规模数据集的开源方法。最流行的方法是 RHadoop 和 sparklyr。

表 F.2 R 开源大数据平台

| 方法 | 描述 |
| --- | --- |
| RHadoop | 在 R 环境中使用 Hadoop 管理和分析数据的软件。由五个相互连接的包组成:`rhbase`、`ravro`、`rhdfs`、`plyrmr` 和 `rmr2`。有关详细信息,请参阅 [`github.com/RevolutionAnalytics/RHadoop/wiki`](https://github.com/RevolutionAnalytics/RHadoop/wiki)。 |
| RHIPE | *R 和 Hadoop 集成编程环境*。一个 R 包,允许用户在 R 中运行 Hadoop MapReduce 作业。有关信息,请参阅 [`github.com/delta-rho/RHIPE`](https://github.com/delta-rho/RHIPE)。 |
| Hadoop Streaming | Hadoop streaming ([`hadoop.apache.org/docs/r1.2.1/streaming.html`](https://hadoop.apache.org/docs/r1.2.1/streaming.html)) 是一个用于创建和运行以任何语言作为映射器或/或归约器的 Map/Reduce 作业的实用程序。`HadoopStreaming` 包支持使用 R 编写这些脚本。有关信息,请参阅 [`cran.rstudio.com/web/packages/HadoopStreaming/`](https://cran.rstudio.com/web/packages/HadoopStreaming/)。 |
| RHIVE | 一个通过 HIVE 查询促进分布式计算的 R 扩展。RHIVE 支持在 R 中轻松使用 HIVE SQL,以及在 Hive 中使用 R 对象和 R 函数。有关信息,请参阅 [`github.com/nexr/RHive`](https://github.com/nexr/RHive)。 |
| pbdR | “使用 R 进行大数据编程。”通过简单界面访问可扩展、高性能库(如 MPI、ZeroMQ、ScaLAPACK 和 netCDF4 以及 PAPI)的包,实现 R 中的数据并行。pbdR 软件还支持在大型计算集群上使用单程序多数据(SPMD)模型。有关详细信息,请参阅 [`r-pbd.org`](http://r-pbd.org)。 |
| sparklyr | 一个提供 Apache Spark R 接口的包。支持连接到本地和远程 Apache Spark 集群,提供 `'dplyr'` 兼容的后端和 Spark 机器学习算法的接口。有关详细信息,请参阅 [`cran.r-project.org/web/packages/sparklyr`](https://cran.r-project.org/web/packages/sparklyr)。 |

云服务提供了一种现成的、可扩展的基础设施,具有可能巨大的内存和存储资源。为 R 用户最受欢迎的云服务由亚马逊、微软和谷歌提供。虽然不是云解决方案,但甲骨文也向 R 用户提供了大数据计算(见表 F.3)。

表 F.3 大数据项目商业平台

| 方法 | 描述 |
| --- | --- |
| 亚马逊网络服务 (AWS) | 使用 R 与 AWS 有几种方法。R 包 `paws`(R 中 AWS 的包)提供了从 R 内部访问 AWS 服务的完整套件(见 [`paws-r.github.io/`](https://paws-r.github.io/))。`aws.ec2` 包是 AWS EC2 REST API 的简单客户端包([`github.com/cloudyr/aws.ec2`](https://github.com/cloudyr/aws.ec2))。Louis Aslett 维护的亚马逊机器镜像使得将 RStudio 服务器部署到亚马逊 EC2 服务变得相对简单([`www.louisaslett.com/RStudio_AMI/`](https://www.louisaslett.com/RStudio_AMI/))。 |
| 微软 Azure | 数据科学虚拟机是 Azure 云平台上的虚拟机镜像,专为数据科学构建。它们包括 Microsoft R Open、Microsoft 机器学习服务器、RStudio 桌面和 RStudio 服务器。有关详细信息,请参阅 [`azure.microsoft.com/en-us/ services/virtual-machines/data-science-virtual-machines/`](https://azure.microsoft.com/en-us/services/virtual-machines/data-science-virtual-machines/)。另外,请参阅 `AzureR`,这是一系列用于从 R 操作 Azure 的包([`github.com/Azure/AzureR`](https://github.com/Azure/AzureR))。 |
| 谷歌云服务 | `bigrquery` 包提供了对谷歌 BigQuery API 的接口([`github.com/r-dbi/bigrquery`](https://github.com/r-dbi/bigrquery))。`googleComputeEngineR` 包提供了 R 对 Google Cloud Compute Engine API 的接口。它使得为 R(包括 RStudio 和 Shiny)部署云资源变得尽可能简单([`cloudyr.github.io/googleComputeEngineR/`](https://cloudyr.github.io/googleComputeEngineR/))。 |
| Oracle R 高级分析 Hadoop | 这是一个 R 包集合,提供了对 HIVE 表、Hadoop 基础设施、Oracle 数据库表和本地 R 环境的接口。它还包括一系列预测分析技术。请参阅 Oracle 帮助文件([`mng.bz/K4jn`](http://mng.bz/K4jn))。 |

除了表 F.3 中的资源外,考虑 cloudyr 项目([`cloudyr.github.io/`](https://cloudyr.github.io/)),这是一个旨在使使用 R 进行云计算更容易的倡议。它包含了一系列用于融合 R 和云服务优势的包。

在任何语言中处理从千兆到太字节范围的数据库集都可能具有挑战性。每种方法都伴随着一个显著的学习曲线。书籍《使用 R 和 Hadoop 进行大数据分析》(Prajapati,2013)是使用 R 与 Hadoop 的一个有用资源。对于 Spark,阅读《精通 Spark 与 R》([https://therinspark.com/](https://therinspark.com/))和《使用任意代码从 R 中利用 Spark 进行性能优化》([https://sparkfromr.com/](https://sparkfromr.com/))都非常有帮助。最后,查看 CRAN 任务视图:“*使用 R 进行高性能和并行计算*”([https://cran.r-project.org/web/views/HighPerformanceComputing.html](https://cran.r-project.org/web/views/HighPerformanceComputing.html))。这是一个快速变化和发展的领域,所以请确保经常检查任务视图。


# 附录 G. 更新 R 安装

作为消费者,我们理所当然地认为我们可以通过“检查更新”选项来更新软件。在第一章中,我提到可以使用 `update.packages()` 函数下载并安装贡献包的最新版本。不幸的是,更新 R 安装本身可能更复杂。

如果你想要将 R 安装从 5.1.0 更新到 6.1.1,你必须有创意。(当我写这段话时,当前版本实际上是 4.1.1,但我希望这本书在未来几年里看起来时尚且最新。)这里描述了两种方法:使用 `installr` 包的自动化方法和适用于所有平台的手动方法。

## G.1 自动安装(仅限 Windows)

如果你是一个 Windows 用户,可以使用 `installr` 包来更新 R 安装。首先安装该包并加载它:

install.packages("installr")
library(installr)


这在 RGui 中添加了一个更新菜单(见图 G.1)。

![](https://github.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/r-ia-3e/img/APPG_F01_Kabacoff3.png)

图 G.1 `installr` 包添加到 Windows RGui 中的更新菜单

该菜单允许你安装 R 的新版本,更新现有包,并安装其他有用的软件产品(如 RStudio)。目前,`installr` 包仅适用于 Windows 平台。对于 macOS 用户或不想使用 `installr` 的 Windows 用户,更新 R 通常是一个手动过程。

## G.2 手动安装(Windows 和 macOS)

从 CRAN ([`cran.r-project.org/bin`](http://cran.r-project.org/bin)) 下载并安装 R 的最新版本相对简单。复杂因素在于,新安装中不包括自定义设置(包括之前安装的贡献包)。在我的当前设置中,我安装了 500 多个贡献包。我真的不希望每次升级我的 R 安装时都要手动写下它们的名称并重新安装!

网上关于如何优雅且高效地更新 R 安装有很多讨论。这里描述的方法既不优雅也不高效,但我发现它在 Windows 和 macOS 平台上都工作得很好。

在这种方法中,你使用 `installed.packages()` 函数将包列表保存到 R 目录树之外的位置,然后使用该列表与 `install.packages()` 函数下载并安装最新贡献包到新的 R 安装中。以下是步骤:

1.  如果你有一个自定义的 Rprofile.site 文件(见附录 B),请将其保存到 R 外部。

1.  启动你的当前版本 R,并输入以下语句:

    ```
    oldip <- installed.packages()[,1]
    save(oldip, file="path/installedPackages.Rdata")
    ```

    其中 `path` 是 R 之外的一个目录。

1.  下载并安装 R 的新版本。

1.  如果你在步骤 1 中保存了自定义版本的 Rprofile.site 文件,请将其复制到新安装中。

1.  启动 R 的新版本,并输入以下语句:

    ```
    load("path/installedPackages.Rdata")
    newip <- installed.packages()[,1]
    for(i in setdiff(oldip, newip)){ 
      install.packages(i)
    }
    ```

    `path` 是在第 2 步中指定的位置。

1.  删除旧安装(可选)。

这种方法只会安装来自 CRAN 的包。它不会找到来自其他位置的包。您必须单独查找和下载这些包。幸运的是,这个过程会显示一个无法安装的包列表。在我的最后一次安装中,`globaltest` 和 `Biobase` 找不到。因为我从 Bioconductor 网站获取了它们,所以我能够通过以下代码安装它们:

source("http://bioconductor.org/biocLite.R")
biocLite("globaltest")
biocLite("Biobase")


第 6 步涉及可选的删除旧安装包。在 Windows 机器上,可以同时安装多个 R 版本。如果需要,可以通过开始菜单 > 控制面板 > 卸载程序来卸载旧版本。在 macOS 平台上,新版本的 R 将覆盖旧版本。要在 macOS 上删除任何残留文件,请使用 Finder 转到 /Library/Frameworks/R.frameworks/versions/ 目录,并删除代表旧版本的文件夹。

显然,手动更新现有的 R 版本比这样一款复杂的软件所期望的要复杂得多。我希望能有一天,这个附录只需简单地说“选择检查更新选项”来更新 R 安装包。

## G.3 更新 R 安装包(Linux)

在 Linux 平台上更新 R 安装包的过程与在 Windows 和 macOS 机器上使用的过程有很大不同。此外,它还因 Linux 发行版(Debian、Red Hat、SUSE 或 Ubuntu)而异。有关详细信息,请参阅 [`cran.r-project.org/bin/linux`](http://cran.r-project.org/bin/linux)。


# 参考文献

Allison, T. 和 D. Chichetti. 1976\. “哺乳动物的睡眠:生态和体质相关因素。” *科学* 194 (4266): 732–734.

Anderson, M. J. 2006\. “基于距离的多变量分散同质性检验。” *生物统计学* 62: 245–253.

Baade, R. 和 R. Dye. 1990\. “体育场和专业体育对大都市区发展的影响。” *增长与变化* 21: 1–14.

Bandalos, D. L. 和 M. R. Boehm-Kaufman. 2009\. “探索性因子分析中的四个常见误解。” 在 C. E. Lance 和 R. J. Vandenberg 编著的 *统计和方法神话与都市传说* 中,61–87\. 纽约:路透社。

Bates, D. 2005\. “在 R 中拟合线性混合模型。” *R 新闻* 5 (1): 27–30\. [www.r-project.org/doc/Rnews/Rnews_2005-1.pdf](http://www.r-project.org/doc/Rnews/Rnews_2005-1.pdf).

Breslow, N. 和 D. Clayton. 1993\. “广义线性混合模型中的近似推理。” *美国统计学会杂志* 88:9–25.

Bretz, F., T. Hothorn, 和 P. Westfall. 2010\. *使用 R 的多重比较*。佛罗里达州博卡拉顿:查普曼与霍尔出版社。

Canty, A. J. 2002\. “R 中的重采样方法:boot 包。” *R 新闻* 2 (3): 2–7\. [www.r-project.org/doc/Rnews/Rnews_2002-3.pdf](http://www.r-project.org/doc/Rnews/Rnews_2002-3.pdf).

Chambers, J. M. 2008\. *数据分析软件:使用 R 的编程*。纽约:斯普林格出版社。

Chambers, J., W. Cleveland, B. Kleiner, 和 P. Tukey. 1983. *数据分析的图形方法*。Wadsworth & Brooks/Cole 统计/概率系列。波士顿:Duxbury 出版社。

Chollet, F., 和 J. Allaire. 2018\. *使用 R 的深度学习*。纽约:曼宁出版社。

Ciaburro, G. 和 B. Venkateswaran. 2017\. *使用 R 的神经网络*。伯明翰:Packt 出版社。

Cleveland, W. 1981\. “LOWESS:通过稳健局部加权回归平滑散点图的程序。” *美国统计学家* 35:54.

_____. 1993\. *数据可视化*。新泽西州 summit:霍巴特出版社。

Cohen, J. 1988\. *行为科学统计功效分析*,第 2 版。新泽西州希尔兹代尔:劳伦斯·埃尔鲍姆出版社。

Cowpertwait, P. S. 和 A. V. Metcalfe. 2009\. *使用 R 的入门时间序列分析*。新西兰奥克兰:斯普林格出版社。

Coxe, S., S. West, 和 L. Aiken. 2009\. “计数数据的分析:泊松回归及其替代方法的温和介绍。” *人格评估杂志* 91: 121–136.

Culbertson, W. 和 D. Bradford. 1991\. “啤酒的价格:州际比较的一些证据。” *国际工业组织杂志* 9: 275–289.

DiStefano, C., M. Zhu, 和 D. Mîndrila. 2009\. “理解和使用因子得分:应用研究者的考虑。” *实用评估、研究和评估* 14 (20). [`pareonline.net/pdf/v14n20.pdf`](http://pareonline.net/pdf/v14n20.pdf).

Dobson, A. 和 A. Barnett. 2008\. *广义线性模型导论*,第 3 版。佛罗里达州博卡拉顿:查普曼与霍尔出版社。

Dunteman, G. 和 M-H Ho. 2006\. *广义线性模型导论*。加利福尼亚州千橡市:赛奇出版社。

Efron, B. and R. Tibshirani. 1998\. *Bootstrap 导论*. 纽约:Chapman & Hall 出版社。

Enders, C. K. 2010\. *应用缺失数据分析*. 纽约:Guilford 出版社。

Everitt, B. S., S. Landau, M. Leese, and D. Stahl. 2011\. *聚类分析*, 第 5 版. 伦敦:Wiley 出版社。

Fair, R. C. 1978\. “婚外情理论。” *政治经济学杂志* 86: 45–61。

Faraway, J. 2006\. *使用 R 扩展线性模型:广义线性、混合效应和非参数回归模型*. 博卡拉顿,佛罗里达州:Chapman & Hall 出版社。

Fawcett, T. 2005\. “ROC 分析的导论。” *模式识别信件* 27: 861–874。

Forte, R. M. 2015\. *用 R 掌握预测分析*. 伯明翰:Packt 出版社。

Fox, J. 2002\. *R 和 S-Plus 应用回归指南*. 千橡市,加利福尼亚州:Sage 出版社。

_____. 2002\. “回归模型的 Bootstrap。” [`mng.bz/pY9m`](http://mng.bz/pY9m)。

_____. 2008\. *应用回归分析和广义线性模型*. 千橡市,加利福尼亚州:Sage 出版社。

Fwa, T.,编. 2006\. *公路工程手册*, 第 2 版. 博卡拉顿,佛罗里达州:CRC 出版社。

Gentleman, R. 2009\. *生物信息学中的 R 编程*. 博卡拉顿,佛罗里达州:Chapman & Hall/CRC 出版社。

Good, P. 2006\. *重抽样方法:数据分析实用指南*, 第 3 版. 波士顿:Birkhäuser 出版社。

Gorsuch, R. L. 1983\. *因子分析*, 第 2 版. 希尔斯代尔,新泽西州:劳伦斯·厄尔巴姆出版社。

Greene, W. H. 2003\. *计量经济学分析*, 第 5 版. 上萨德尔河,新泽西州:普伦蒂斯·霍尔出版社。

Hand, D. J. and C. C. Taylor. 1987\. *多元方差分析和重复测量*. 伦敦:Chapman & Hall 出版社。

Harlow, L., S. Mulaik, and J. Steiger. 1997\. *如果没有显著性检验会怎样?* 马哈瓦,新泽西州:劳伦斯·厄尔巴姆出版社。

Hartigan, J. A. and M. A. Wong. 1979\. “K-Means 聚类算法。” *应用统计学* 28: 100–108。

Hayton, J. C., D. G. Allen, and V. Scarpello. 2004\. “探索性因素分析中的因子保留决策:平行分析教程。” *组织研究方法* 7: 191–204。

Hsu, S., M. Wen, and M. Wu. 2009\. “探索用户体验作为 MMORPG 成瘾预测因素。” *计算机与教育* 53: 990–999。

Johnson, J. 2000\. “多元行为研究。” 35: 1–19\. ResearchGate。

_____. 2004\. “影响相对权重的因素:样本和测量误差的影响。” *组织研究方法* 7: 283–299。

Johnson, J. and J. LeBreton. 2004\. “组织研究中相对重要性指数的历史和使用。” *组织研究方法* 7: 238–257。

Kirk, R. 2008\. *统计学:导论*, 第 5 版. 加利福尼亚州:Wadsworth 出版社。

Kowarik, A. and M. Templ. 2016\. "使用 R 包 VIM 进行插补。" *统计软件杂志*,74: 1-16。

Koch, G. and S. Edwards. 1988\. “分类数据的临床效率试验。” 在 *缺失数据统计分析* 第 2 版,由 R. J. A. Little 和 D. Rubin 著。霍博肯,新泽西州:John Wiley & Sons,2002 年出版。

Kuhn, M. and K. Johnson. 2013\. *应用预测建模*. 纽约:Springer 出版社。

LeBreton, J. M and S. Tonidandel. 2008\. “Multivariate Relative Importance: Extending Relative Weight Analysis to Multivariate Criterion Spaces.” *Journal of Applied Psychology* 93: 329–345.

Lanz, B. 2015\. *Machine Learning with R*, 2nd ed. Birmingham: Packt.

Licht, M. 1995\. “Multiple Regression and Correlation.” In *Reading and Understanding Multivariate Statistics*, edited by L. Grimm and P. Yarnold. Washington, DC: American Psychological Association, 19–64.

Little, R. J. A., and D. B. Rubin. (2002). Statistical Analysis with Missing Data (2nd ed.). New Jersey: John Wiley.

Mangasarian, O. L. and W. H. Wolberg. 1990\. “Cancer Diagnosis via Linear Programming.” *SIAM News*, 23: 1–18.

McCall, R. B. 2000\. *Fundamental Statistics for the Behavioral Sciences*, 8th ed. New York: Wadsworth.

McCullagh, P. and J. Nelder. 1989\. *Generalized Linear Models*, 2nd ed. Boca Raton, FL: Chapman & Hall.

Meyer, D., A. Zeileis, and K. Hornick. 2006\. “The Strucplot Framework: Visualizing Multi-way Contingency Tables with vcd.” *Journal of Statistical Software* 17 (3): 1–48\. [www.jstatsoft.org/v17/i03/paper](http://www.jstatsoft.org/v17/i03/paper).

Montgomery, D. C. 2007\. *Engineering Statistics*. Hoboken, NJ: John Wiley & Sons.

Mooney, C. and R. Duval. 1993\. *Bootstraping: A Nonparametric Approach to Statistical Inference*. Monterey, CA: Sage.

Mulaik, S. 2009\. *Foundations of Factor Analysis*, 2nd ed. Boca Raton, FL: Chapman & Hall.

Nenadić, O. and M. Greenacre. 2007\. “Correspondence Analysis in R, with Two- and Three-Dimensional Graphics: The ca Package.” *Journal of Statistical Software* 20 (3). [www.jstatsoft.org/v20/i03/paper](http://www.jstatsoft.org/v20/i03/paper).

Pinheiro, J. C. and D. M. Bates. 2000\. *Mixed-Effects Models in S and S-PLUS*. New York: Springer.

Potvin, C., M. J. Lechowicz, and S. Tardif. 1990\. “The Statistical Analysis of Ecophysiological Response Curves Obtained from Experiments Involving Repeated Measures.” *Ecology* 71: 1389–1400.

Schafer, J. and J. Graham. 2002\. “Missing Data: Our View of the State of the Art.” *Psychological Methods* 7: 147–177.

Schlomer, G., S. Bauman, and N. Card. 2010\. “Best Practices for Missing Data Management in Counseling Psychology.” *Journal of Counseling Psychology* 57: 1–10.

Shah, A. 2005\. “Getting Started with the boot Package in R for Statistical Inference.” [www.mayin.org/ajayshah/KB/R/documents/boot.html](http://www.mayin.org/ajayshah/KB/R/documents/boot.html).

Shumway, R. H. and D. S. Stoffer. 2010\. *Time Series Analysis and Its Applications*. New York: Springer.

Silva, R. B., D. F. Ferreirra, and D. A. Nogueira. 2008\. “Robustness of Asymptotic and Bootstrap Tests for Multivariate Homogeneity of Covariance Matrices.” *Ciênc. agrotec*. 32: 157–166.

Simon, J. 1997\. “Resampling: The New Statistics.” [www.resample.com/intro-text-online/](http://www.resample.com/intro-text-online/).

Snedecor, G. W. and W. G. Cochran. 1988\. *Statistical Methods*, 8th ed. Ames, IA: Iowa State University Press.

Statnikov, A., C. F. Aliferis, D. P. Hardin, and I. Guyon. 2011\. *生物医学中支持向量机的温和介绍*(第 1 卷:*理论与方法*). 新泽西州哈肯萨克:世界科学出版社.

Torgo, L. 2017\. *数据挖掘与 R:通过案例学习(第 2 版)*. 佛罗里达州博卡拉顿:Chapman & Hall/CRC.

UCLA: 学术技术服务,统计咨询组. 2009\. “使用 R 进行重复测量分析。” [`mng.bz/a9c7`](http://mng.bz/a9c7).

van Buuren, S. and K. Groothuis-Oudshoorn. 2011\. “R 中的链式方程多元插补(MICE)。” *统计软件杂志*,45(3),1-67\. [`mng.bz/3EH5`](http://mng.bz/3EH5).

Venables, W. N. and B. D. Ripley. 1999\. *现代应用统计* *with S-PLUS*,第 3 版. 纽约:Springer.

_____. 2000\. *S 编程*. 纽约:Springer.

Westfall, P. H., Y. Hochberg, D. Rom, R. Wolfinger, and R. Tobias. 1999\. *使用 SAS 系统进行多重比较和多重检验*. 北卡罗来纳州卡里:SAS 研究所.

Wickham, H. 2009a. *ggplot2*:*数据分析中的优雅图形*. 纽约:Springer.

_____. 2009b. “图形的分层语法。” *计算与图形统计杂志* 19: 3–28.

_____. 2019\. *高级 R,第 2 版*. 佛罗里达州博卡拉顿:Chapman & Hall/CRC.

Wilkinson, L. 2005\. *图形语法*. 纽约:Springer-Verlag.

Yu, C. H. 2003\. “重抽样方法:概念、应用和论证。” *实用评估、研究和评估* 8 (19). [`pareonline.net/getvn.asp?v=8&n=19`](http://pareonline.net/getvn.asp?v=8&n=19).

Yu-Sung, S., A. Gelman, J. Hill, and M. Yajima. 2011\. “R 中的多重插补(mi):打开黑盒的窗口。” *统计软件杂志* 45 (2). [www.jstatsoft.org/v45/i02/paper](http://www.jstatsoft.org/v45/i02/paper).

Zuur, A. F., E. Ieno, N. Walker, A. A. Saveliev, and G. M. Smith. 2009\. *使用 R 的生态学混合效应模型及其扩展*. 纽约:Springer.
posted @ 2025-11-19 09:21  绝不原创的飞龙  阅读(68)  评论(0)    收藏  举报