R-统计与机器学习研讨会-全-
R 统计与机器学习研讨会(全)
原文:
annas-archive.org/md5/e61f21209b8b5ad343780cefdc55a102译者:飞龙
前言
这本工作坊手册是一本综合资源,旨在深入探讨统计学和机器学习的核心方面。它通过实际示例和动手练习来解释关键概念,以实现全面的学习体验。从基础知识开始,本书引导你完成整个模型开发过程,涵盖从数据预处理到模型开发的各个方面。
除了关注机器学习之外,本书还深入探讨了 R 的统计能力。你将学习如何操作各种数据类型,解决从代数和微积分到概率和贝叶斯统计的复杂数学问题。文本甚至指导你通过线性回归技术以及更高级的统计方法。
在本工作坊手册结束时,你不仅将拥有统计学和机器学习的坚实基础理解,而且还将熟练使用 R 的广泛库进行数据处理和模型训练等任务。通过这种综合方法,你将准备好在未来的项目中充分利用 R 的全部功能。
本书面向的对象
从初学者到中级水平的数据科学家将从本书中获得很多收获。本科生到研究生水平的学生,以及初级到中级的高级数据科学家或从事分析相关角色的人士也将受益匪浅。
基本的线性代数和建模知识将有助于理解本书中涵盖的概念。
本书涵盖的内容
第一章,R 编程入门,介绍了 R 编程的基础知识,包括基本数据结构,如向量、矩阵、因子、DataFrames 和列表,以及控制逻辑,如循环、函数编写等。
第二章,使用 dplyr 进行数据处理,介绍了使用 dplyr 库进行常见的数据操作和处理技术,包括数据转换、聚合、选择和合并。
第三章,中级数据处理,介绍了常见的数据处理挑战,如转换数据类型、填充缺失值和字符串匹配。本章还涵盖了确保数据质量的先进技术,包括分类和文本数据。
第四章,使用 ggplot2 进行数据可视化,介绍了使用 ggplot2 进行常见绘图技术,包括库的入门级美学、几何和主题,以及中级技术,如使用统计模型、坐标系和分面叠加图形。
第五章, 数据探索分析,介绍了处理和探索不同类型数据的不同方法,包括分类数据和数值数据,以及不同的数据总结方法。本章还涵盖了一个案例研究,从数据清洗开始,一直到最后的不同可视化和分析。
第六章, 使用 R Markdown 进行有效报告,介绍了使用 R Markdown 的动态文档。与静态内容不同,使用 R Markdown 生态系统构建的输出提供了包括图表和表格在内的交互性。本章涵盖了 R Markdown 报告的基础,包括如何添加、微调和自定义图表和表格以制作交互性和有效的报告。
第七章, R 中的线性代数,涵盖了入门级的线性代数,并使用 R 中的实例进行说明,包括线性方程、向量空间以及矩阵基础,如常见的矩阵运算,例如乘法、求逆和转置。
第八章, R 中的中级线性代数,介绍了线性代数的中级主题及其在 R 中的实现,包括矩阵的行列式——矩阵的范数、秩和迹,以及特征值和特征向量。
第九章, R 中的微积分,介绍了微积分的基础及其在 R 中的实现,包括拟合函数到数据、绘图、导数和数值微分,以及积分和积分。
第十章, 概率基础,介绍了概率的基本概念及其在 R 中的实现,包括几何分布、二项分布和泊松分布等常见离散概率分布,以及正态分布和指数分布等常见连续分布。
第十一章, 统计估计,介绍了针对数值数据和分类数据的常见统计估计和推断程序。包括假设检验和置信区间等关键概念也将被涵盖。
第十二章, R 中的线性回归,介绍了简单和多元线性回归模型,涵盖了模型估计、闭式解、评估和线性回归假设等主题。
第十三章, R 中的逻辑回归,介绍了逻辑回归及其与线性回归和损失函数的关系,以及其在建模不平衡数据集中的应用。
第十四章, 贝叶斯统计,介绍了贝叶斯推理框架,涵盖了后验更新和不确定性量化等主题。
为了充分利用这本书
要充分利用本书,建议您对编程有基本的了解,理想情况下是 R 语言,尽管任何编程语言的良好基础也应该足够。熟悉基本的统计和数学概念也将有所帮助,因为本书深入探讨了统计方法和数学模型。虽然本书的结构旨在引导您从基础知识到高级主题,但先前对数据分析技术的了解将增强您的学习体验。如果您是 R 语言的初学者,您可能需要在最初几章上花更多的时间,以便熟悉编程环境和语法。
| 本书涵盖的软件 | 操作系统要求 |
|---|---|
| R | Windows, macOS, 或 Linux |
如果您正在使用本书的数字版,我们建议您亲自输入代码或从本书的 GitHub 仓库(下一节中提供链接)获取代码。这样做将帮助您避免与代码的复制和粘贴相关的任何潜在错误。
下载示例代码文件
您可以从 GitHub(github.com/PacktPublishing/The-Statistics-and-Machine-Learning-with-R-Workshop)下载本书的示例代码文件。如果代码有更新,它将在 GitHub 仓库中更新。
我们还有其他来自我们丰富的图书和视频目录的代码包,可在github.com/PacktPublishing/找到。查看它们吧!
使用的约定
在本书中使用了多种文本约定。
文本中的代码:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“最后,我们使用grid()函数绘制函数,以显示 S 型曲线的特征,并添加网格线。”
代码块设置如下:
lm_model = lm(Class_num ~ Duration, data=GermanCredit)
coefs = coefficients(lm_model)
intercept = coefs[1]
slope = coefs[2]
粗体:表示新术语、重要单词或屏幕上看到的单词。例如,菜单或对话框中的单词以粗体显示。以下是一个示例:“要在 RStudio 中创建 R Markdown 文件,我们可以选择文件 | 新建文件 | R Markdown。”
小贴士或重要注意事项
看起来像这样。
联系我们
我们始终欢迎读者的反馈。
一般反馈:如果您对本书的任何方面有疑问,请通过电子邮件发送至 customercare@packtpub.com,并在邮件主题中提及书名。
勘误:尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,如果您能向我们报告,我们将不胜感激。请访问www.packtpub.com/support/errata并填写表格。
盗版: 如果您在互联网上以任何形式发现我们作品的非法副本,如果您能提供位置地址或网站名称,我们将不胜感激。请通过版权@packt.com 与我们联系,并提供材料的链接。
如果您有兴趣成为作者: 如果您在某个领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问authors.packtpub.com。
分享您的想法
一旦您阅读了《使用 R 进行统计和机器学习研讨会》,我们非常乐意听听您的想法!请点击此处直接访问此书的亚马逊评论页面并分享您的反馈。
您的评论对我们和科技社区都非常重要,它将帮助我们确保我们提供高质量的内容。
下载此书的免费 PDF 副本
感谢您购买这本书!
您喜欢在旅途中阅读,但无法携带您的印刷书籍到任何地方吗?
您的电子书购买是否与您选择的设备不兼容?
别担心,现在每购买一本 Packt 书籍,您都可以免费获得该书的 DRM 免费 PDF 版本。
在任何地方、任何设备上阅读。直接从您最喜欢的技术书籍中搜索、复制和粘贴代码到您的应用程序中。
优惠远不止于此,您还可以获得独家折扣、时事通讯和每日免费内容的每日电子邮件。
按照以下简单步骤获取优惠:
- 扫描二维码或访问以下链接

https://packt.link/free-ebook/9781803240305
-
提交您的购买证明
-
就这样!我们将直接将您的免费 PDF 和其他优惠发送到您的电子邮件。
第一部分:统计学基础
本部分旨在为你提供统计和编程基础知识的了解,特别是关注多功能的 R 语言,这将为后续部分更高级的主题奠定基石。
在本部分结束时,你将牢固掌握对任何数据科学从业者理解至关重要的核心统计和编程概念。掌握这些基础技能后,你将准备好深入探索本书后续部分中等待你的更专业主题。
本部分包含以下章节:
-
第一章, R 入门
-
第二章, 使用 dplyr 进行数据处理
-
第三章, 中级数据处理
-
第四章, 使用 ggplot2 进行数据可视化
-
第五章, 数据探索性分析
-
第六章, 使用 R Markdown 进行有效报告
第一章:R 语言入门
在本章中,我们将介绍 R 的基础知识,这是最广泛使用的开源统计分析和建模语言。我们将从 RStudio 的介绍开始,如何进行简单的计算,常见的数组和控制逻辑,以及如何在 R 中编写函数。
到本章结束时,您将能够使用 RStudio 中的常见数据结构(如向量、列表和数据框)进行基本的计算,并能够使用不同的方法将这些计算封装在函数中。
在本章中,我们将涵盖以下内容:
-
介绍 R
-
涵盖 R 和 RStudio 的基础知识
-
R 中的常见数据结构
-
R 中的控制逻辑
-
探索 R 中的函数
技术要求
要完成本章的练习,您需要具备以下条件:
-
编写本书时 R 的最新版本,即 4.1.2
-
RStudio 桌面版的最新版本,即 2021.09.2+382
本章的所有代码均可在github.com/PacktPublishing/The-Statistics-and-Machine-Learning-with-R-Workshop/blob/main/Chapter_1/Chapter_1.R找到。
介绍 R
R 是一种流行的开源语言,支持统计分析和建模,它被统计学家广泛用于开发统计模型和进行数据分析。学习者常问的一个问题是如何在 Python 和 R 之间进行选择。对于那些对两者都较新,并且需要一个简单的模型来处理不太大的数据集的人来说,R 会是一个更好的选择。它拥有丰富的资源来支持建模和绘图任务,这些任务是在 Python 出现之前由统计学家开发的。除了其许多现成的图形和统计建模产品外,R 社区也在追赶目前由 Python 社区主导的高级机器学习,如深度学习。
两种语言之间有许多差异,近年来在许多方面也见证了越来越多的趋同。本书旨在为您提供理解和使用 R 语言进行统计和微积分的基本知识。我们希望有一天,您能够从语言本身的内部运作中提取信息,并在进行某些分析时从方法论层面进行思考。在从基础知识培养基本技能之后,具体使用哪种语言将只是个人偏好的问题。为此,R 提供了专门的实用函数,可以自动“转换”Python 代码以在 R 环境中使用,这又给我们提供了一个不必担心选择特定语言的理由。
涵盖 R 和 RStudio 的基础知识
如果你是一个初学者,很容易将 R 与 RStudio 混淆。简而言之,R 是支持各种后端计算的动力引擎,而 RStudio 是一个方便的工具,用于导航和管理相关的编码和参考资源。具体来说,RStudio 是一个 IDE,用户在其中编写 R 代码、执行分析和开发模型,无需过多担心 R 引擎所需的底层物流。RStudio 提供的界面使得开发工作比原始 R 界面更加方便和用户友好。
首先,我们需要在我们的计算机上安装 R,因为 RStudio 在安装时会附带计算能力。我们可以根据所使用的特定操作系统类型,在 cloud.r-project.org/ 选择相应的 R 版本。然后可以在 www.rstudio.com/products/rstudio/download/ 下载 RStudio 并相应地安装。在安装了这两种软件后启动 RStudio 应用程序,R 引擎将自动检测并使用。让我们通过一个练习来熟悉界面。
练习 1.01 – 探索 RStudio
RStudio 提供了一个全面的环境,用于同时处理 R 脚本和探索数据。在这个练习中,我们将查看一个基本示例,说明如何使用 RStudio 编写一个简单的脚本来存储字符串并执行简单的计算。
执行以下步骤以完成此练习:
-
启动 RStudio 应用程序并观察三个面板:
-
控制台 面板用于执行 R 命令并显示即时结果。
-
环境 面板存储当前 会话 中所有的全局变量。
-
文件 面板列出了当前工作目录中的所有文件以及其他标签页,如图 图 1**.1 所示。
注意,R 版本会以消息的形式打印在控制台中(用虚线框突出显示):
-

图 1.1 – 首次启动 RStudio 的截图
我们也可以在控制台中输入 R.version 来检索有关正在使用的 R 引擎版本的更详细信息,如图 图 1**.2 所示。检查 R 版本至关重要,因为不同的版本在运行相同代码时可能会产生不同的结果。

图 1.2 – 在控制台中输入命令以检查 R 版本
- 在保存文件后构建一个新的
test.R。以下图示说明了这一点:

图 1.3 – 创建新的 R 脚本
- 运行脚本可以通过将光标放在当前行并按 Cmd + Enter(macOS)或 Ctrl + Enter(Windows)来实现;或者,可以点击 R 脚本面板顶部的 运行 按钮,如图所示:

图 1.4 – 点击运行按钮执行脚本
-
在脚本编辑面板中输入以下命令,并在控制台以及其他面板中观察输出。首先,我们通过分配
"I am a string"创建一个test。一个变量可以用来存储一个对象,它可以是字符串、数字、数据框,甚至是函数(稍后会有更多介绍)。字符串由字符组成,是 R 中的一种常见数据类型。在脚本中创建的test变量也会反映在 环境 面板中,这是一个方便的检查点,因为我们也可以观察变量中的内容。参见 图 1**.5 的说明:# String assignment test = "I am a string" print(test)

图 1.5 – 创建字符串类型变量
我们还把一个简单的加法操作分配给 test2 并在控制台中打印出来。这些命令也通过 # 符号进行了注释,其中符号后面的内容不会执行,仅用于解释下面的代码。参见 图 1**.6 的说明:
# Simple calculation
test2 = 1 + 2
print(test2)

图 1.6 – 分配字符串并执行基本计算
-
我们也可以通过
ls()函数检查环境工作区的内容:>>> ls() "test" "test2"
此外,请注意,新创建的 R 脚本也反映在 文件 面板中。RStudio 是一个用于处理 R 的优秀一站式 IDE,并将成为本书的编程接口。我们将在更具体的上下文中介绍 RStudio 的更多功能。
注意
将一些值分配给变量的规范方式是通过 <- 运算符,而不是像示例中那样使用 = 符号。然而,作者选择使用 = 符号,因为它在屏幕上输入更快,并且在大多数情况下与 <- 符号有相同的效果。
此外,请注意 [1] 标记中的输出消息,它表示结果是一个一维输出。除非另有说明,否则我们将忽略这个标记。
上一节的练习提供了一个额外的例子,这是 R 中的一个基本操作。与其他现代编程语言一样,R 也提供了许多标准算术运算符,包括减法 (-)、乘法 (*)、除法 (/)、指数 (^) 和取模 (%%) 运算符。取模运算符返回除法操作中分子的余数。
让我们通过一个练习来了解一些常见的算术运算。
练习 1.02 – R 中的常见算术运算
这个练习将在两个数字之间执行不同的算术运算(加法、减法、乘法、除法、指数和取模):5 和 2。
在print()函数下输入命令,因为直接执行命令也会在控制台高亮显示结果:

图 1.7 – 在 R 中执行常见的算术运算
注意,这些基本的算术运算可以联合形成复杂的运算。在评估由多个运算符组成的复杂运算时,一般规则是使用括号来强制执行特定组件,以符合所需的顺序。这在大多数使用任何编程语言的数值分析中都是适用的。
但是,我们可以在 R 中期望数据采取哪些形式?
R 中的常见数据类型
R 中有五种最基本的数据类型:数值型、整型、字符型、逻辑型和因子型。任何复杂的 R 对象都可以分解为属于这五种数据类型之一的单个元素,因此包含一个或多个数据类型。这五种数据类型的定义如下:
-
1.23。即使我们最初将其赋值为整数值,变量也被视为数值型。 -
整型是一个整数,因此是数值数据类型的一个子集。
-
字符型是用于存储字符序列(包括字母、符号甚至数字)以形成字符串或文本的数据类型,由双引号或单引号包围。
-
TRUE或FALSE。它通常用于条件语句中,以确定条件之后的特定代码是否应该执行。 -
因子型是一种特殊的数据类型,用于存储包含有限数量类别(或水平)的分类变量,可以是有序或无序的。例如,将学生身高分类为矮、中、高可以表示为因子类型,以编码固有的顺序,这在作为字符类型表示时是不可用的。另一方面,无序列表,如男性和女性,也可以表示为因子类型。
让我们通过一个例子来了解这些不同的数据类型。
练习 1.03 – 理解 R 中的数据类型
R 在执行算术运算时对数据类型有严格的规定。一般来说,在评估特定语句(一段代码)时,所有变量的数据类型应该相同。对不同数据类型执行算术运算可能会产生错误。在这个练习中,我们将探讨如何检查数据类型以确保类型一致性,以及将数据类型从一种转换为另一种的不同方法:
-
我们首先创建五个变量,每个变量属于不同的数据类型。使用
class()函数检查数据类型。注意,我们可以使用分号来分隔不同的操作:>>> a = 1.0; b = 1; c = "test"; d = TRUE; e = factor("test") >>> class(a); class(b); class(c); class(d); class(e) "numeric" "numeric" "character" "logical" "factor"如预期的那样,即使
b变量最初被分配了一个整数值,它的数据类型也被转换为数值。 -
对变量进行加法运算。让我们从
a和b变量开始:>>> a + b 2 >>> class(a + b) "numeric"注意,在显示加法结果时忽略了小数点,结果仍然是数值,这可以通过
class()函数验证。现在,让我们看看
a和c之间的加法:>>> a + c Error in a + c : non-numeric argument to binary operator这次,我们在评估加法运算时由于数据类型不匹配而收到了一个错误信息。这是因为 R 中的
+加法运算符是一个二元运算符,它需要接受两个值(操作数)并产生另一个值,所有这些都需要是数值(包括整数)。当两个输入参数中的任何一个不是数值时,错误就会发生。 -
让我们尝试将
a和d相加:>>> a + d 2 >>> class(a + d) "numeric"令人惊讶的是,结果是和
a + b一样的,这表明布尔变量b在底层被转换成了一个数值。相应地,通过在变量前添加感叹号得到的布尔值FALSE,在进行与数字的算术运算时会被视为零:>>> a + !d 1注意,在需要此类转换以在特定语句中继续进行时,会隐式进行布尔转换。例如,在评估
a是否等于d时,d被转换为数值1:>>> a == d TRUE -
使用 R 中的
as.(datatype)函数系列转换数据类型。例如,
as.numeric()函数将输入参数转换为数值,as.integer()返回输入小数的整数部分,as.character()将所有输入(包括数值和布尔值)转换为字符串,而as.logical()将任何非零数值转换为TRUE,将零转换为FALSE。让我们看几个例子:>>> class(as.numeric(b)) "numeric"这表明
b变量已成功转换为数值。请注意,类型转换是 R 中的标准数据处理操作,类型不兼容是常见的错误来源,可能难以追踪:>>> as.integer(1.8) 1 >>> round(1.8) 2由于
as.integer()只返回输入的整数部分,结果总是“向下取整”到较小的整数。我们可以使用round()函数将其向上或向下取整,具体取决于小数点后第一位数字的值:>>> as.character(a) "1" >>> as.character(d) "TRUE"as.character()函数将所有输入参数转换为字符串,如双引号所示,包括数值和布尔值。转换后的值不再保持原始的算术属性。例如,转换为字符的数值不会进行加法运算。同样,转换为字符的布尔值将不再通过逻辑语句进行评估,而是被视为字符:>>> as.factor(a) 1 Levels: 1 >>> as.factor(c) test Levels: test由于输入参数中只有一个元素,因此结果的级别数只有
1,意味着原始输入本身。
注意
一个被称为 high、medium 或 low 的分类变量在自然中具有固有的顺序,而一个值作为 male 或 female 的性别变量则没有顺序。
R 中的常见数据结构
数据结构提供了一种有组织的方式来存储遵循相同或不同类型的数据点。本节将探讨 R 中使用的典型数据结构,包括向量、矩阵、数据框和列表。
向量
使用 c()。两个向量之间的算术运算与前面早些时候的单个元素示例类似,前提是它们的长度相等。两个向量的元素之间需要有逐个对应关系;如果不是,计算可能会出错。让我们看看一个练习。
练习 1.04 – 处理向量
在这个练习中,我们将创建两个长度相同的向量并将它们相加。作为扩展,我们还将尝试使用不同长度的向量进行相同的加法。我们还将对两个向量进行成对比较:
-
创建两个名为
vec_a和vec_b的向量,并提取简单的统计摘要,如mean和sum:>>> vec_a = c(1,2,3) >>> vec_b = c(1,1,1) >>> sum(vec_a) 6 >>> mean(vec_a) 2向量的总和和平均值可以使用
sum()和mean()函数分别生成。我们将在后面介绍更多总结向量的方法。 -
将
vec_a和vec_b相加:>>> vec_a + vec_b 2 3 4两个向量的加法是逐元素进行的。结果也可以保存到另一个变量中,以便进行进一步处理。那么,向一个向量中添加一个单个元素呢?
-
将
vec_a和1相加:>>> vec_a + 1 2 3 4在底层,第一个元素被广播到长度由
vec_a决定的向量c(1,1,1)中,vec_a和c(1,1):>>> vec_a + c(1,1) 2 3 4 Warning message: In vec_a + c(1, 1) : longer object length is not a multiple of shorter object length我们仍然得到相同的结果,只是多了一个警告信息,说明较长向量的长度为三不是较短向量长度为二的倍数。请注意这个警告信息。不建议这样做,因为警告可能会变成显式错误,或者在大型程序中成为潜在错误的隐含原因。
-
接下来,我们将对两个向量进行成对比较:
vec_a > vec_b FALSE TRUE TRUE vec_a == vec_b TRUE FALSE FALSE在这里,我们使用了评估运算符,如
>(大于)和==(等于),为每一对返回逻辑结果(TRUE或FALSE)。注意,R 中有多个逻辑比较运算符。常见的一些包括以下内容:
-
<表示小于 -
<=表示小于或等于 -
>表示大于 -
>=表示大于或等于 -
==表示等于 -
!=表示不等于
-
除了常见的算术运算外,我们还可能对向量的选定部分感兴趣。我们可以使用方括号来选择向量的特定元素,这与在矩阵或数据框等其他数据结构中选择元素的方式相同。方括号之间是索引,表示要选择哪些元素。例如,我们可以使用 vec_a[1] 来选择 vec_a 的第一个元素。让我们通过一个练习来看看如何以不同的方式对向量进行子集化。
练习 1.05 – 向量子集选择
我们可以将选择索引(从1开始)传递进去,以选择向量中的相应元素。我们可以通过c()组合函数包装索引,并传递到方括号中以选择多个元素。通过在第一个和最后一个索引之间写冒号,也可以通过简写符号选择多个连续索引。让我们运行不同的向量子集选择方法:
-
选择
vec_a中的第一个元素:>>> vec_a[1] 1 -
选择
vec_a中的第一个和第三个元素:>>> vec_a[c(1,3)] 1 3 -
选择
vec_a中的所有三个元素:>>> vec_a[c(1,2,3)] 1 2 3以这种方式选择多个元素不太方便,因为我们需要输入每个索引。当索引是连续的时,一个很好的简写技巧是使用由冒号分隔的起始和结束索引。例如,
1:3与c(1,2,3)相同:>>> vec_a[1:3] 1 2 3我们还可以通过在方括号内添加条件语句作为选择条件来执行更复杂的子集选择。例如,前面引入的逻辑评估返回
True或False。在方括号中标记为true的索引的元素将被选中。让我们看一个例子。 -
在
vec_a中选择大于vec_b中相应元素的元素:>>> vec_a[vec_a > vec_b] 2 3结果包含最后两个元素,因为只有第二个和第三个索引被设置为
true。
矩阵
与向量一样,矩阵是一个二维数组,由相同数据类型的元素集合组成,这些元素按固定数量的行和列排列。使用仅包含相同数据类型的数据结构通常更快,因为程序不需要区分不同类型的数据。这使得矩阵在科学计算中成为一种流行的数据结构,尤其是在涉及大量计算的优化过程中。让我们熟悉矩阵,包括创建、索引、子集和扩展矩阵的不同方法。
练习 1.06 – 创建矩阵
在 R 中创建矩阵的标准方法是调用matrix()函数,我们需要提供三个输入参数:
-
需要填充到矩阵中的元素
-
矩阵的行数
-
填充方向(按行或按列)
我们还将重命名矩阵的行和列:
-
使用
vec_a和vec_b创建一个名为mtx_a的矩阵:>>> mtx_a = matrix(c(vec_a,vec_b), nrow=2, byrow=TRUE) >>> mtx_a [,1] [,2] [,3] [1,] 1 2 3 [2,] 1 1 1首先,通过
c()函数将输入向量vec_a和vec_b组合成一个长向量,然后按顺序排列成两行(nrow=2),按行排列(byrow=TRUE)。您可以自由尝试不同的维度配置,例如在创建矩阵时设置三行两列。注意输出中的行和列名称。行通过方括号中的第一个索引进行索引,而第二个索引列。我们还可以按如下方式重命名矩阵。
-
通过
rownames()和colnames()函数重命名矩阵mtx_a:>>> rownames(mtx_a) = c("r1", "r2") >>> colnames(mtx_a) = c("c1", "c2", "c3") >>> mtx_a c1 c2 c3 r1 1 2 3 r2 1 1 1
让我们看看如何从矩阵中选择元素。
练习 1.07 – 矩阵的子集
我们仍然可以使用方括号来选择一个或多个矩阵元素。冒号简写技巧也适用于矩阵子集:
-
选择
mtx_a矩阵的第一行和第二列的元素:>>> mtx_a[1,2] 2 -
选择
mtx_a矩阵中所有行的最后两列的所有元素:>>> mtx_a[1:2,c(2,3)] c2 c3 r1 2 3 r2 1 1 -
选择
mtx_a矩阵第二行的所有元素:>>> mtx_a[2,] c1 c2 c3 1 1 1 Selecting elements by matching the row name using a conditional evaluation statement offers a more precise way of subsetting the matrix, especially when counting the exact index becomes troublesome. Name-based indexing also applies to columns. -
选择
mtx_a矩阵的第三行:>>> mtx_a[,3] r1 r2 3 1 >>> mtx_a[,colnames(mtx_a)=="c3"] r1 r2 3 1因此,我们有多种方法从矩阵中选择感兴趣的特定元素。
与向量相比,处理矩阵需要类似的算术运算。在下一个练习中,我们将探讨按行和列总结矩阵以及执行基本操作,如加法和乘法。
练习 1.08 – 矩阵的算术运算
让我们从创建一个新的矩阵开始:
-
创建另一个名为
mtx_b的矩阵,其元素是mtx_a中元素的两倍:>>> mtx_b = mtx_a * 2 >>> mtx_b c1 c2 c3 r1 2 4 6 r2 2 2 2除了乘法之外,所有标准算术运算符(如
+、-和/)都以类似元素级的方式应用于矩阵,并依赖于相同的广播机制。相同大小的两个矩阵之间的操作也是按元素进行的。 -
将
mtx_a除以mtx_b:>>> mtx_a / mtx_b c1 c2 c3 r1 0.5 0.5 0.5 r2 0.5 0.5 0.5 -
使用
rowSums()、colSums()、rowMeans()和colMeans()分别计算mtx_a的行和列总和以及平均值:>>> rowSums(mtx_a) r1 r2 6 3 >>> colSums(mtx_a) c1 c2 c3 2 3 4 >>> rowMeans(mtx_a) r1 r2 2 1 >>> colMeans(mtx_a) c1 c2 c3 1.0 1.5 2.0
在运行优化过程时,我们经常需要保存一些中间指标,如模型损失和准确度,以进行诊断。这些指标可以通过逐渐将新数据附加到当前矩阵中保存为矩阵形式。让我们看看如何按行和列扩展矩阵。
练习 1.09 – 扩展矩阵
通过cbind()函数向矩阵中添加一个或多个列,该函数按列合并新的矩阵或向量列。同样,可以通过rbind()函数按行连接额外的矩阵或向量:
-
按列将
mtx_b附加到mtx_a:>>> cbind(mtx_a, mtx_b) c1 c2 c3 c1 c2 c3 r1 1 2 3 2 4 6 r2 1 1 1 2 2 2我们可能需要重命名列,因为其中一些列有重叠。这同样适用于以下按行连接。
-
按行将
mtx_b附加到mtx_a:>>> rbind(mtx_a, mtx_b) c1 c2 c3 r1 1 2 3 r2 1 1 1 r1 2 4 6 r2 2 2 2
所以,我们已经看到了矩阵的操作。接下来是数据框如何?
数据框
数据框是一种标准的数据结构,其中变量存储为列,观测值存储为对象中的行。它是矩阵的高级版本,因为每个列的元素可以有不同的数据类型。
R 引擎自带一些默认数据集,存储为数据框。在下一个练习中,我们将探讨不同的方法来检查和理解数据框的结构。
练习 1.10 – 理解数据框
数据框是一种著名的矩形形状数据结构,类似于 Excel。让我们以 R 中的默认数据集为例进行考察:
-
加载
iris数据集:>>> data("iris") >>> dim(iris) 150 5使用
dim()函数检查维度表明iris数据集包含 150 行和五列。我们可以通过查看数据集的前几行和最后几行(观测值)来初步了解其内容。 -
使用
head()和tail()查看前五行和后五行:>>> 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 >>> tail(iris) Sepal.Length Sepal.Width Petal.Length Petal.Width Species 145 6.7 3.3 5.7 2.5 virginica 146 6.7 3.0 5.2 2.3 virginica 147 6.3 2.5 5.0 1.9 virginica 148 6.5 3.0 5.2 2.0 virginica 149 6.2 3.4 5.4 2.3 virginica 150 5.9 3.0 5.1 1.8 virginica注意,行名默认按整数顺序索引,从一开始。前四列是数值型,最后一列是字符型(或因子)。我们可以更系统地查看数据框的结构。
-
使用
str()查看数据集iris的结构:>>> str(iris) 'data.frame': 150 obs. of 5 variables: $ Sepal.Length: num 5.1 4.9 4.7 4.6 5 5.4 4.6 5 4.4 4.9 ... $ Sepal.Width : num 3.5 3 3.2 3.1 3.6 3.9 3.4 3.4 2.9 3.1 ... $ Petal.Length: num 1.4 1.4 1.3 1.5 1.4 1.7 1.4 1.5 1.4 1.5 ... $ Petal.Width : num 0.2 0.2 0.2 0.2 0.2 0.4 0.3 0.2 0.2 0.1 ... $ Species : Factor w/ 3 levels "setosa","versicolor",..: 1 1 1 1 1 1 1 1 1 1 ...str()函数总结了数据框的结构,包括观测值和变量的总数,变量名称的完整列表,数据类型,以及前几行观测值。如果列是因子,还会显示类别(水平)的数量。我们也可以通过将相同长度的向量作为列传递给
data.frame()函数来创建数据框。 -
创建一个名为
df_a的数据框,其中包含两列,分别对应vec_a和vec_b:>>> df_a = data.frame("a"=vec_a, "b"=vec_b) >>> df_a a b 1 1 1 2 2 1 3 3 1
选择数据框的元素可以与矩阵选择类似的方式进行。其他如 subset() 函数等使选择更加灵活。让我们通过一个例子来了解。
练习 1.11 – 在数据框中选择元素
在这个练习中,我们将首先查看选择特定元素集的不同方法,然后介绍 subset() 函数以执行自定义条件选择:
-
选择
df_a数据框的第二列:>>> df_a[,2] 1 1 1行级索引留空表示将选择所有行。我们也可以通过引用所有行级索引来明确表示:
>>> df_a[1:3,2] 1 1 1 Alternatively, we can use the shortcut `$` sign to reference the column name directly:df_a$b
1 1 1
The `subset()` function provides an easy and structured way to perform row-level filtering and column-level selection. Let’s see how it works in practice. -
选择
df_a中列a大于两的行:>>> subset(df_a, a>2) a b 3 3 1注意,行索引三也显示为输出的一部分。
我们可以直接在
subset()函数的上下文中使用列a,这样我们就不需要使用$符号了。我们也可以通过传递列名给select参数来选择列。 -
在
df_a中选择列a大于两的列b:>>> subset(df_a, a>2, select="b") b 3 1
数据分析中的另一个典型操作是对数据框的一个或多个变量进行排序。让我们看看在 R 中它是如何工作的。
练习 1.12 – 排序向量和数据框
order() 函数可以用来返回输入向量中元素的排名位置,然后可以通过更新索引对这些元素进行排序:
-
在
vec_c中创建c(5,1,10)向量,并按升序排序:>>> vec_c = c(5,1,10) >>> order(vec_c) 2 1 3 >>> vec_c[order(vec_c)] 1 5 10由于
vec_c中的最小元素是1,相应的排名位置是1。同样,5被设置为第二排名,10被设置为第三和最高排名。然后使用排名位置重新排列和排序原始向量,就像我们通过order()函数按默认升序对元素进行排序一样。如果我们想按降序排序呢?我们只需在输入向量中添加一个负号即可。 -
按列
a降序排序df_a数据框:>>> df_a[order(-df_a$a),] a b 3 3 1 2 2 1 1 1 1
数据框将是我们在本书中将主要使用的数据结构。让我们看看最后一个也是最复杂的数据结构:列表。
列表
列表是一种灵活的数据结构,可以容纳不同类型的数据(数值、整数、字符、逻辑、因子,甚至列表本身),每个可能具有不同的长度。这是我们迄今为止引入的最复杂结构,以结构化的方式收集各种对象。为了回顾,让我们在 图 1**.8 中比较四种数据结构的内容、数据类型和长度。一般来说,所有四种结构都可以存储任何数据类型的元素。向量(一维数组)和矩阵(二维数组)要求内容是同质数据类型。数据框包含一个或多个数据类型可能不同的向量,而列表可以包含不同数据类型的条目。矩阵和数据框遵循矩形形状,因此要求每列的长度相同。然而,列表中的条目可以具有任意长度(受内存限制),彼此不同。

图 1.8 – 比较四种不同数据结构的内容、数据类型和长度
让我们看看如何创建一个列表。
练习 1.13 – 创建列表
在这个练习中,我们将通过不同的方式操作列表,包括创建和重命名列表,以及访问、添加和删除列表中的元素:
-
使用之前的
a、vec_a和df_a变量创建一个列表:>>> ls_a = list(a, vec_a, df_a) >>> ls_a [[1]] [1] 1 [[2]] [1] 1 2 3 [[3]] a b 1 1 1 2 2 1 3 3 1输出显示列表元素通过双方括号索引,可以用来访问列表中的条目。
-
访问列表
ls_a中的第二个条目:>>> ls_a[[2]] 1 2 3默认索引也可以重命名,以便通过名称选择条目。
-
根据原始名称重命名列表并访问
vec_a变量:>>> names(ls_a) <- c("a", "vec_a", "df_a") ls_a $a [1] 1 $vec_a [1] 1 2 3 $df_a a b 1 1 1 2 2 1 3 3 1 >>> ls_a[['vec_a']] 1 2 3 >>> ls_a$vec_a 1 2 3我们可以通过使用方括号或
$符号中的名称来访问列表中的特定条目。 -
在
ls_a列表中添加一个名为new_entry的新条目,内容为"test":>>> ls_a[['new_entry']] = "test" >>> ls_a $a [1] 1 $vec_a [1] 1 2 3 $df_a a b 1 1 1 2 2 1 3 3 1 $new_entry [1] "test"结果显示
"test"现已添加到ls_a的最后一个条目。我们还可以通过将其赋值为NULL来删除特定的条目。 -
从
ls_a中删除名为df_a的条目:>>> ls_a[['df_a']] = NULL >>> ls_a $a [1] 1 $vec_a [1] 1 2 3 $new_entry [1] "test"命名为
df_a的条目现在已成功从列表中删除。我们还可以更新列表中的现有条目。 -
将名为
vec_a的条目更新为c(1,2):>>> ls_a[['vec_a']] = c(1,2) >>> ls_a $a [1] 1 $vec_a [1] 1 2 $new_entry [1] "test"命名为
vec_a的条目现在已成功更新。
列表结构的灵活性和可扩展性使其成为存储异构数据元素的流行选择,类似于 Python 中的字典。在下一节中,我们将通过了解 R 中的控制逻辑来扩展我们的知识库,这使我们编写长程序时具有更大的灵活性和精确度。
R 中的控制逻辑
关系和逻辑运算符帮助我们比较语句,当我们向程序添加逻辑时。我们还可以通过通过循环重复遍历一系列操作来评估多个条件语句,从而增加复杂性。本节将介绍构成条件语句构建块的基本关系和逻辑运算符。
关系运算符
我们之前简要介绍了几个关系运算符,如>=和==。本节将详细介绍标准关系运算符的使用。让我们看看一些例子。
练习 1.14 – 练习使用标准关系运算符
关系运算符允许我们比较两个量,并获得比较的单个结果。我们将学习以下步骤,了解如何在 R 中表达和使用标准关系运算符:
-
使用等式运算符(
==)执行以下评估并观察输出:>>> 1 == 2 FALSE >>> "statistics" == "calculus" FALSE >>> TRUE == TRUE TRUE >>> TRUE == FALSE FALSE等式运算符通过严格评估两侧(包括逻辑数据)的两个输入参数来执行,只有当它们相等时才返回
TRUE。 -
使用不等式运算符(
!=)执行相同的评估并观察输出:>>> 1 != 2 TRUE >>> "statistics" != "calculus" TRUE >>> TRUE != TRUE FALSE >>> TRUE != FALSE TRUE不等式运算符与等式运算符正好相反。
-
使用大于和小于运算符(
>和<)执行以下评估并观察输出:>>> 1 < 2 TRUE >>> "statistics" > "calculus" TRUE >>> TRUE > FALSE TRUE在第二次评估中,字符数据之间的比较遵循从最左侧字符开始的两个字符串的成对字母顺序。在这种情况下,字母
s排在c之后,并且被编码为更高数值的数字。在第三个例子中,TRUE被转换为 1,FALSE被转换为 0,因此返回一个逻辑值TRUE。 -
使用大于等于运算符(
>=)和小于等于运算符(<=)执行以下评估并观察输出:>>> 1 >= 2 FALSE >>> 2 <= 2 TRUE注意,这些运算符由两个条件评估通过
OR运算符(|)连接而成。因此,我们可以将其分解为括号中的两个评估,从而得到与之前相同的输出:>>> (1 > 2) | (1 == 2) FALSE >>> (2 < 2) | (2 == 2) TRUE关系运算符也适用于我们之前遇到的向量,例如行级筛选以子集化数据框。
-
使用大于运算符比较
vec_a与1:>>> vec_a > 1 FALSE TRUE TRUE我们可以通过分别比较每个元素并使用
c()函数组合结果来得到相同的结果。
逻辑运算符
AND(&)、OR(|)和NOT(!)。AND运算符仅在两个操作数都为TRUE时返回TRUE,而OR运算符如果至少有一个操作数为TRUE就返回TRUE。另一方面,NOT运算符将评估结果翻转至相反。
让我们通过一个练习来了解这些逻辑运算符的使用。
练习 1.15 – 练习使用标准逻辑运算符
我们将从最广泛使用的控制逻辑AND运算符开始,确保只有在多个条件同时满足时才会执行特定操作:
-
使用
AND运算符执行以下评估并观察输出:>>> TRUE & FALSE FALSE >>> TRUE & TRUE TRUE >>> FALSE & FALSE FALSE >>> 1 > 0 & 1 < 2 TRUE结果显示,需要满足两个条件才能获得
TRUE输出。 -
使用
OR运算符执行以下评估并观察输出:>>> TRUE | FALSE TRUE >>> TRUE | TRUE TRUE >>> FALSE | FALSE FALSE >>> 1 < 0 | 1 < 2 TRUE结果显示,如果至少有一个条件评估为
TRUE,则输出为TRUE。 -
使用
NOT运算符执行以下评估并观察输出:>>> !TRUE FALSE >>> !FALSE TRUE >>> !(1<0) TRUE在第三个例子中,评估与
1 >= 0相同,返回TRUE。因此,NOT运算符在感叹号之后反转评估结果。这些运算符也可以用于在向量中执行成对逻辑评估。
-
执行以下评估并观察输出:
>>> c(TRUE, FALSE) & c(TRUE, TRUE) TRUE FALSE >>> c(TRUE, FALSE) | c(TRUE, TRUE) TRUE TRUE >>> !c(TRUE, FALSE) FALSE TRUE
此外,AND (&&) 和 OR (||) 逻辑运算符也有长格式。与之前短格式中的逐元素比较不同,长格式仅用于评估每个输入向量的第一个元素,并且这种评估仅继续到结果确定为止。换句话说,长格式在评估包含多个元素的向量时仅返回单个结果。它在现代 R 编程控制流中应用最广泛,尤其是在条件 if 语句中。
让我们看看以下示例:
>>> c(TRUE, FALSE) && c(FALSE, TRUE)
FALSE
>>> c(TRUE, FALSE) || c(FALSE, TRUE)
TRUE
两次评估都基于每个向量的第一个元素。也就是说,在两次评估中,每个向量的第二个元素都被忽略。这提供了计算上的好处,尤其是在向量很大时。由于如果可以通过评估第一个元素获得最终结果,就没有必要继续评估,因此我们可以安全地丢弃其余部分。
在第一次使用 && 进行评估时,比较两个向量的第一个元素(TRUE 和 FALSE)返回 FALSE,而继续比较第二个元素也将返回 FALSE,因此第二次比较是不必要的。在第二次使用 || 进行评估时,比较第一个元素(TRUE | FALSE)给出 TRUE,这样就无需进行第二次比较,因为结果始终会被评估为 TRUE。
条件语句
if-else 语句用于组合多个逻辑运算符的结果并决定后续动作的流程。它通常用于增加大型 R 程序的复杂性。if-else 语句遵循以下一般结构,其中首先验证评估条件。如果验证返回 TRUE,则执行 if 子句中的花括号内的表达式,其余代码将被忽略。否则,将执行 else 子句中的表达式:
if(evaluation condition){
some expression
} else {
other expression
}
让我们通过一项练习来看看如何使用 if-else 控制语句。
练习 1.16 – 练习使用条件语句
时间进行另一项练习!让我们练习使用条件语句:
-
初始化一个值为
1的x变量,并编写一个if-else条件来决定输出消息。如果x大于零,则打印出"positive",否则打印"not positive":>>> x = 1 >>> if(x > 0){ >>> print("positive") >>> } else { >>> print("not positive") >>> } "positive"if子句中的条件评估为TRUE,并且其中的代码被执行,在控制台打印出"positive"。注意,else分支是可选的,并且如果只想对输入进行一个检查,则可以将其删除。额外的if-else控制也可以嵌入到一个分支中。我们还可以使用
if-else条件控制语句添加额外的分支,其中中间部分可以重复多次。 -
初始化一个值为
0的x变量,并编写一个控制流程来确定并打印其符号:>>> x = 0 >>> if(x > 0){ >>> print("positive") >>> } else if(x == 0){ >>> print("zero") >>> } else { >>> print("negative") >>> } "zero"由于条件是依次评估的,第二个语句返回
TRUE并因此打印出"zero"。
循环
一个 if 语句;只有当条件评估为 TRUE 时,代码才会被执行。唯一的区别是,循环会继续迭代执行代码,只要条件为 TRUE。有两种类型的循环:while 循环和 for 循环。while 循环用于迭代次数未知的情况,其终止依赖于评估条件或使用 break 控制语句在运行表达式中的分离条件。for 循环用于迭代次数已知的情况。
while 循环遵循以下一般结构,其中首先评估 condition 1 以确定应该执行的外部花括号内的表达式。有一个(可选的)if 语句来决定是否根据 condition 2 需要终止 while 循环。这两个条件控制 while 循环的终止,只要任一条件评估为 TRUE,循环就会退出执行。在 if 子句中,condition 2 可以放置在 while 块内的任何位置:
while(condition 1){
some expression
if(condition 2){
break
}
}
注意,while 语句中的 condition 1 需要在某个时刻为 FALSE;否则,循环将无限期地继续,这可能导致 RStudio 中的会话过期错误。
让我们通过一个练习来看看如何使用 while 循环。
练习 1.17 – 练习 while 循环
让我们尝试一下 while 循环:
-
初始化一个值为
2的x变量,并编写一个while循环。如果x小于10,则平方它并打印其值:>>> x = 2 >>> while(x < 10){ >>> x = x² >>> print(x) >>> } 4 16while循环执行了两次,将x的值从2增加到16。在第三次评估中,x大于 10,条件语句评估为FALSE,因此退出循环。我们也可以打印出x来双重检查其值:>>> x 16 -
在平方之后添加一个条件,如果
x大于10则退出循环:>>> x = 2 >>> while(x < 10){ >>> x = x² >>> if(x > 10){ >>> break >>> } >>> print(x) >>> } 4这次只打印出一个数字。原因是当
x变为16时,if条件评估为TRUE,从而触发break语句退出while循环并忽略print()语句。让我们验证x的值:>>> x 16
让我们看看for循环,它假设以下一般结构。在这里,var是一个用于按顺序引用sequence内容的占位符,sequence可以是一个向量、一个列表或其他数据结构:
for(var in sequence){
some expression
}
对于sequence中的每个唯一变量,相同的表达式将被评估,除非触发一个显式的if条件,使用break退出循环,或者跳过剩余的代码并立即跳转到下一个迭代使用next。让我们通过一个练习来了解这些概念。
练习 1.18 – 练习使用 for 循环
接下来,让我们尝试for循环:
-
创建一个向量来存储三个字符串(
statistics、and和calculus),并打印出每个元素:>>> string_a = c("statistics","and","calculus") >>> for(i in string_a){ >>> print(i) >>> } "statistics" "and" "calculus"在这里,
for循环通过在每个迭代中将元素值按顺序分配给i变量来遍历string_a向量中的每个元素。我们也可以选择使用向量索引进行迭代,如下所示:>>> for(i in 1:length(string_a)){ >>> print(string_a[i]) >>> } "statistics" "and" "calculus"在这里,我们创建了一系列从
1到向量长度的整数索引,并将它们分配给每个迭代的i变量,然后使用它来引用string_a向量中的元素。这是一种更灵活和多变的方式来引用向量中的元素,因为我们也可以使用相同的索引来引用其他向量。直接引用元素,如前一种方法,更简洁、更易读。然而,它缺乏没有循环索引的控制和灵活性。 -
添加一个条件,如果当前元素是
"and",则退出循环:>>> for(i in string_a){ >>> if(i == "and"){ >>> break >>> } >>> print(i) >>> } "statistics"当
i的当前值是"and"时,满足if条件将退出循环。 -
添加一个条件,如果当前元素是
"and",则跳到下一个迭代:>>> for(i in string_a){ >>> if(i == "and"){ >>> next >>> } >>> print(i) >>> } "statistics" "calculus"当下一个语句被评估时,下面的
print()函数将被忽略,程序跳到下一个迭代,只打印出"statistics"和"calculus"以及"``and"元素。
到目前为止,我们已经涵盖了 R 语言中最基础的构建块。现在,我们准备进入最后一个也是最广泛使用的构建块:函数。
探索 R 语言中的函数
我们在之前的练习中使用过的sum()和mean()函数。我们也可以定义自己的函数,作为处理给定输入信号并产生输出的接口。参见图 1**.9以了解说明:

图 1.9 – 函数工作流程的说明
可以使用function关键字创建一个函数,其格式如下:
function_name = function(argument_1, argument_2, …){
some statements
}
一个函数可以分解为以下部分:
-
函数名:在 R 环境中注册并存储的函数对象的名称。我们使用此名称后跟一对括号,并在括号内(可选)输入参数来调用函数。
-
输入参数:在调用函数时用于接收输入值的占位符。参数可以是可选的(已分配默认值)或必选的(未分配默认值)。将所有参数都设置为可选的等同于函数不需要必选输入参数。然而,我们需要向必选参数传递一个特定值才能调用函数。此外,如果有的话,可选参数也可以出现在必选参数之后。
-
函数体:这是执行主要语句以完成特定操作并实现函数目的的区域。
-
return()函数。
让我们通过一个创建用户定义函数的练习来了解。
练习 1.19 – 创建用户定义的函数
现在,让我们试试看:
-
创建一个名为
test_func的函数,用于接收输入并打印出"(input) is fun"。允许选择将消息打印为大写:test_func = function(x, cap=FALSE){ msg = paste(x,"is fun!") if(cap){ msg = toupper(msg) } return(msg) }注意,我们使用
=符号而不是<-来将函数对象赋值给test_func变量。然而,在 R 中创建函数时,后者更为常见。在输入中,我们创建了两个参数:必选参数x,用于接收要打印的消息,以及可选参数cap,用于确定消息是否需要转换为大写。可选参数意味着用户可以选择不提供任何内容给此参数,以使用默认设置(即小写消息),或者通过显式传递一个值来覆盖默认行为。在函数体中,我们首先创建一个
msg变量,并通过调用paste()函数(一个用于连接两个输入参数的内置函数)来分配消息内容。如果cap参数为FALSE,则if语句将评估为FALSE,msg将直接作为函数的输出返回。否则,将触发if子句中的语句,使用toupper()函数(R 中的另一个内置函数)将msg变量转换为大写。 -
让我们看看以不同方式调用函数后会发生什么:
>>> test_func("r") "r is fun!" >>> test_func("r",cap=TRUE) "R IS FUN!" >>> test_func() Error in paste(x, "is fun!") : argument "x" is missing, with no default前两种情况按预期工作。在第三种情况下,我们没有为定义为必选参数的
x参数提供任何值。这导致错误,并未能调用函数。
摘要
在本章中,我们介绍了 R 语言的基本构建块,包括如何利用和导航 RStudio IDE,基本的算术运算(加法、减法、乘法、除法、指数和取模),常见的数据结构(向量、矩阵、数据框和列表),控制逻辑,包括关系运算符(>、==、<、>=、<=和!=)和逻辑运算符(&、|、!、&&和||),使用ifelse的条件语句,for和while循环,以及最后,R 语言中的函数。理解这些基本方面将极大地促进我们在后续章节中的学习,因为我们将逐渐引入更具挑战性的主题。
在下一章中,我们将介绍dplyr,这是数据处理和操作中最广泛使用的库之一。利用dplyr提供的各种实用函数将使处理大多数数据处理任务变得更加容易。
第二章:使用 dplyr 进行数据处理
在上一章中,我们介绍了 R 语言的基础知识。掌握这些基础知识将帮助我们更好地应对数据科学项目中最常见的任务:数据处理。数据处理是指一系列数据整理和润色步骤,将数据转换为下游分析和建模所需的目标格式。我们可以将其视为一个接受原始数据并输出所需数据的函数。然而,我们需要明确指定函数如何执行烹饪食谱并处理数据。
到本章结束时,你将能够使用 dplyr(R 中最广泛使用的数据处理库之一)执行常见的数据处理步骤,如过滤、选择、分组和聚合。
在本章中,我们将涵盖以下主题:
-
介绍
tidyverse和dplyr -
使用
dplyr进行数据转换 -
使用
dplyr进行数据聚合 -
使用
dplyr进行数据合并 -
案例研究 – 使用 Stack Overflow 数据集
技术要求
要完成本章的练习,你需要以下内容:
tidyverse包的最新版本,写作时为 1.3.1
本章中所有代码和数据均可在github.com/PacktPublishing/The-Statistics-and-Machine-Learning-with-R-Workshop/tree/main/Chapter_2找到。
介绍 tidyverse 和 dplyr
最广泛使用的包含一系列独立包的 R 库之一是 tidyverse;它包括 dplyr 和 ggplot2(将在第四章中介绍)。它可以支持大多数数据处理和可视化需求,并且与 base R 命令相比,实现起来既简单又快速。因此,建议将特定的数据处理或可视化任务外包给 tidyverse 而不是自行实现。
在我们深入数据处理的世界之前,还有另一种在 tidyverse 生态系统中使用的数据结构:tibble。tibble 是 DataFrame 的高级版本,提供了更好的格式控制,从而在代码中产生整洁的表达式。它是 tidyverse 中的核心数据结构。DataFrame 可以转换为 tibble 对象,反之亦然。让我们通过一个练习来了解这一点。
练习 2.01 – 在 tibble 和 DataFrame 之间转换
首先,我们将通过安装此包并将 iris DataFrame 转换为 tibble 格式来探索 tidyverse 生态系统:
-
安装
tidyverse包并加载dplyr包:install.packages("tidyverse") library(dplyr)安装
tidyverse包将自动安装dplyr,可以通过library()函数将其加载到我们的工作环境中。 -
加载
iris数据集并检查其数据结构:>>> data("iris") >>> class(iris) "data.frame"data()函数加载了iris数据集,这是由基础 R 提供的默认数据集,以及使用class()函数检查的 DataFrame。 -
将数据集转换为
tibble格式并验证其数据结构:>>> iris_tbl = as_tibble(iris) >>> class(iris_tbl) "tbl_df" "tbl" "data.frame"iris_tbl中有三个类属性,这意味着该对象可以用作tibble和 DataFrame。一个对象具有多个类属性支持更好的兼容性,因为我们可以对其进行不同的处理。tibble对象也支持智能打印,通过列出前几行、数据集的形状(150 行和 5 列)以及每列的数据类型。另一方面,DataFrame 在打印时只会显示其所有内容到控制台:>>> iris_tbl # A tibble: 150 x 5 Sepal.Length Sepal.Width Petal.Length Petal.Width Species <dbl> <dbl> <dbl> <dbl> <fct> 1 5.1 3.5 1.4 0.2 setosa 2 4.9 3 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 3.6 1.4 0.2 setosa 6 5.4 3.9 1.7 0.4 setosa 7 4.6 3.4 1.4 0.3 setosa 8 5 3.4 1.5 0.2 setosa 9 4.4 2.9 1.4 0.2 setosa 10 4.9 3.1 1.5 0.1 setosa # … with 140 more rows
tidyverse和dplyr提供了多个数据转换的实用函数。让我们看看一些常用的函数,例如filter()和arrange()。
使用 dplyr 进行数据转换
dplyr函数。在本节中,我们将介绍五个基本的数据转换函数:filter()、arrange()、mutate()、select()和top_n()。
使用 filter()函数切片数据集
tidyverse生态系统中最显著的亮点之一是%>%操作符,它将前面的语句作为上下文输入提供给后面的语句。使用管道操作符可以让我们在代码结构方面有更好的清晰度,同时避免了多次输入重复的上下文语句的需要。让我们通过一个练习来了解如何使用管道操作符通过filter()函数来切片iris数据集。
练习 2.02 – 使用管道操作符进行过滤
对于这个练习,我们被要求仅使用管道操作符和filter()函数保留iris数据集中的setosa物种:
-
打印
iris数据集中的所有独特物种:>>> unique(iris_tbl$Species) setosa versicolor virginica Levels: setosa versicolor virginica结果显示
Species列是一个具有三个级别的因子。 -
使用
filter()函数仅保留iris_tbl中的"setosa"物种并将结果保存在iris_tbl_subset中:iris_tbl_subset = iris_tbl %>% filter(Species == "setosa") >>> iris_tbl_subset # A tibble: 50 x 5 Sepal.Length Sepal.Width Petal.Length Petal.Width Species <dbl> <dbl> <dbl> <dbl> <fct> 1 5.1 3.5 1.4 0.2 setosa 2 4.9 3 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 3.6 1.4 0.2 setosa 6 5.4 3.9 1.7 0.4 setosa 7 4.6 3.4 1.4 0.3 setosa 8 5 3.4 1.5 0.2 setosa 9 4.4 2.9 1.4 0.2 setosa 10 4.9 3.1 1.5 0.1 setosa # … with 40 more rows管道操作符表示以下过滤操作应用于
iris_tbl对象。在这种上下文中,我们可以直接引用Species列(而不是使用iris_tbl$Species),并使用==逻辑运算符设置逐行评估的相等条件。结果显示iris_tbl_subset中存储了总共 50 行。 -
为了双重检查过滤结果,我们可以打印出
iris_tbl_subset中的独特物种:>>> unique(iris_tbl_subset$Species) setosa Levels: setosa versicolor virginica -
现在,数据集只包含
"setosa"物种。然而,Species列仍然将先前信息编码为具有三个级别的因子。这是因子数据类型的一个独特特性,其中关于总级别的信息编码在因子类型列的所有单个元素中。我们可以通过将其转换为字符来删除此类信息,如下所示:>>> unique(as.character(iris_tbl_subset$Species)) "setosa"注意,我们正在将两个函数链接在一起,从最内层的
as.character()到最外层的unique()进行评估。
filter() 函数通过使用逗号分隔条件,可以轻松地添加多个过滤条件。例如,我们可以添加另一个条件将 Sepal.Length 的最大值设置为 5,如下所示:
iris_tbl_subset = iris_tbl %>%
filter(Species == "setosa",
Sepal.Length <= 5)
>>> max(iris_tbl_subset$Sepal.Length)
5
>>> dim(iris_tbl_subset)
28 5
结果显示,现在的最大 Sepal.Length 是 5,并且从原始的 150 行中留下了 28 行。
接下来,我们将探讨如何根据特定列(或列)对 tibble 对象(或 DataFrame)进行排序。
使用 arrange() 函数对数据集进行排序
另一个常见的数据转换操作是排序,这会导致一个数据集,其中一列或多列按升序或降序排列。这可以通过 dplyr 提供的 arrange() 函数实现。让我们通过一个练习来看看不同的排序数据集的方法。
练习 2.03 – 使用 arrange() 函数进行排序
在这个练习中,我们将探讨如何按升序或降序对数据集的列进行排序,以及如何通过管道操作符将排序操作与过滤结合:
-
使用
arrange()对iris数据集中的Sepal.Length列进行升序排序:iris_tbl_sorted = iris_tbl %>% arrange(Sepal.Length) >>> iris_tbl_sorted # A tibble: 150 x 5 Sepal.Length Sepal.Width Petal.Length Petal.Width Species <dbl> <dbl> <dbl> <dbl> <fct> 1 4.3 3 1.1 0.1 setosa 2 4.4 2.9 1.4 0.2 setosa 3 4.4 3 1.3 0.2 setosa 4 4.4 3.2 1.3 0.2 setosa 5 4.5 2.3 1.3 0.3 setosa 6 4.6 3.1 1.5 0.2 setosa 7 4.6 3.4 1.4 0.3 setosa 8 4.6 3.6 1 0.2 setosa 9 4.6 3.2 1.4 0.2 setosa 10 4.7 3.2 1.3 0.2 setosa # … with 140 more rows结果显示,
arrange()函数默认按升序对特定列进行排序。现在,让我们看看如何按降序排序。 -
以降序对相同的列进行排序:
iris_tbl_sorted = iris_tbl %>% arrange(desc(Sepal.Length)) >>> iris_tbl_sorted # A tibble: 150 x 5 Sepal.Length Sepal.Width Petal.Length Petal.Width Species <dbl> <dbl> <dbl> <dbl> <fct> 1 7.9 3.8 6.4 2 virginica 2 7.7 3.8 6.7 2.2 virginica 3 7.7 2.6 6.9 2.3 virginica 4 7.7 2.8 6.7 2 virginica 5 7.7 3 6.1 2.3 virginica 6 7.6 3 6.6 2.1 virginica 7 7.4 2.8 6.1 1.9 virginica 8 7.3 2.9 6.3 1.8 virginica 9 7.2 3.6 6.1 2.5 virginica 10 7.2 3.2 6 1.8 virginica # … with 140 more rows在将列传递给
arrange()之前添加desc()函数可以反转排序顺序并实现降序排序。我们也可以传递多个列以按顺序排序它们。此外,
arrange()函数还可以与其他数据处理步骤一起使用,例如过滤。 -
在将
Species设置为"setosa"并将Sepal.Length限制在最大值5的情况下,按降序排序Sepal.Length和Sepal.Width:iris_tbl_subset_sorted = iris_tbl %>% filter(Species == "setosa", Sepal.Length <= 5) %>% arrange(desc(Sepal.Length),desc(Sepal.Width)) >>> iris_tbl_subset_sorted # A tibble: 28 x 5 Sepal.Length Sepal.Width Petal.Length Petal.Width Species <dbl> <dbl> <dbl> <dbl> <fct> 1 5 3.6 1.4 0.2 setosa 2 5 3.5 1.3 0.3 setosa 3 5 3.5 1.6 0.6 setosa 4 5 3.4 1.5 0.2 setosa 5 5 3.4 1.6 0.4 setosa 6 5 3.3 1.4 0.2 setosa 7 5 3.2 1.2 0.2 setosa 8 5 3 1.6 0.2 setosa 9 4.9 3.6 1.4 0.1 setosa 10 4.9 3.1 1.5 0.1 setosa # … with 18 more rows结果显示了两层排序,其中对于相同的
Sepal.Length值,Sepal.Width进一步按降序排序。这两个排序标准由逗号分隔,就像在filter()中分隔多个条件一样。此外,管道操作符可以按顺序连接和评估多个函数。在这种情况下,我们首先为使用
iris_tbl设置上下文,然后进行过滤和排序,这两个操作都通过管道操作符连接。
使用 mutate() 函数添加或更改列
tibble 对象或 DataFrame 实质上是由多个列组成的列表的列表。我们可能想要通过更改其内容、类型或格式来编辑现有列;这种编辑也可能导致在原始数据集中附加新列。列级编辑可以通过 mutate() 函数实现。让我们通过一个示例来看看如何与其他函数结合使用此函数。
练习 2.04 – 使用 mutate() 函数更改和添加列
在这个练习中,我们将探讨如何更改现有列的类型并添加新列以支持过滤操作:
-
将
Species列改为character类型:>>> paste("Before:", class(iris_tbl$Species)) iris_tbl = iris_tbl %>% mutate(Species = as.character(Species)) >>> paste("After:", class(iris_tbl$Species)) "Before: factor" "After: character"在这里,我们使用了
mutate()函数来改变Species的类型,该类型通过管道操作符直接在iris_tbl对象上下文中引用。 -
创建一个名为
ind的列,以指示Sepal.Width是否大于Petal.Length:iris_tbl = iris_tbl %>% mutate(ind = Sepal.Width > Petal.Length) >>> iris_tbl # A tibble: 150 x 6 Sepal.Length Sepal.Width Petal.Length Petal.Width Species ind <dbl> <dbl> <dbl> <dbl> <chr> <lgl> 1 5.1 3.5 1.4 0.2 setosa TRUE 2 4.9 3 1.4 0.2 setosa TRUE 3 4.7 3.2 1.3 0.2 setosa TRUE 4 4.6 3.1 1.5 0.2 setosa TRUE 5 5 3.6 1.4 0.2 setosa TRUE 6 5.4 3.9 1.7 0.4 setosa TRUE 7 4.6 3.4 1.4 0.3 setosa TRUE 8 5 3.4 1.5 0.2 setosa TRUE 9 4.4 2.9 1.4 0.2 setosa TRUE 10 4.9 3.1 1.5 0.1 setosa TRUE # … with 140 more rows结果显示,我们添加了一个包含逻辑值的指示列。我们可以通过
table()函数获取TRUE和FALSE值的计数:>>> table(iris_tbl$ind) FALSE TRUE 100 50 -
仅保留
Sepal.Width大于Petal.Length的行:iris_tbl_subset = iris_tbl %>% filter(ind==TRUE) >>> table(iris_tbl_subset$ind) TRUE 50由于我们本质上是在执行过滤操作,因此首先创建指示列然后过滤的两步过程可以通过在
filter()函数中直接设置过滤条件合并为一步:iris_tbl_subset2 = iris_tbl %>% filter(Sepal.Width > Petal.Length) >>> nrow(iris_tbl_subset2) 50结果与两步方法相同。
现在,让我们来介绍最后一个常用的实用函数——select()。
使用 select()函数选择列
select()函数通过选择由输入参数指定的列来工作,该参数是一个表示一个或多个列的字符串向量。当在管道操作符的上下文中使用select()时,意味着所有后续语句都是基于所选列进行评估的。当select语句是最后一个时,它返回所选列作为输出tibble对象。
让我们通过一个练习来了解从数据集中选择列的不同方法。
练习 2.05 – 使用 select()选择列
在这个练习中,我们将探讨从tibble数据集中选择列的不同方法:
-
从
iris数据集中选择前三个列:rst = iris_tbl %>% select(Sepal.Length, Sepal.Width, Petal.Length) >>> rst # A tibble: 150 x 3 Sepal.Length Sepal.Width Petal.Length <dbl> <dbl> <dbl> 1 5.1 3.5 1.4 2 4.9 3 1.4 3 4.7 3.2 1.3 4 4.6 3.1 1.5 5 5 3.6 1.4 6 5.4 3.9 1.7 7 4.6 3.4 1.4 8 5 3.4 1.5 9 4.4 2.9 1.4 10 4.9 3.1 1.5 # … with 140 more rows当你需要增加要选择的列数时,逐个输入它们会变得繁琐。另一种方法是使用冒号(
:)分隔首尾列,如下所示:rst = iris_tbl %>% select(Sepal.Length:Petal.Length) >>> rst # A tibble: 150 x 3 Sepal.Length Sepal.Width Petal.Length <dbl> <dbl> <dbl> 1 5.1 3.5 1.4 2 4.9 3 1.4 3 4.7 3.2 1.3 4 4.6 3.1 1.5 5 5 3.6 1.4 6 5.4 3.9 1.7 7 4.6 3.4 1.4 8 5 3.4 1.5 9 4.4 2.9 1.4 10 4.9 3.1 1.5 # … with 140 more rows这种方法选择所有位于
Sepal.Length和Petal.Length之间的列。使用冒号可以帮助我们一次性选择多个连续列。此外,我们还可以通过c()函数将其与其他单个列结合使用。 -
选择包含
"length"的列:rst = iris_tbl %>% select(contains("length")) >>> rst # A tibble: 150 x 2 Sepal.Length Petal.Length <dbl> <dbl> 1 5.1 1.4 2 4.9 1.4 3 4.7 1.3 4 4.6 1.5 5 5 1.4 6 5.4 1.7 7 4.6 1.4 8 5 1.5 9 4.4 1.4 10 4.9 1.5 # … with 140 more rows在这里,我们使用了
contains()函数来执行不区分大小写的字符串匹配。支持字符串匹配的其他实用函数包括starts_with()和ends_with()。让我们看一个例子。 -
选择以
"petal"开头的列:rst = iris_tbl %>% select(starts_with("petal")) >>> rst # A tibble: 150 x 2 Petal.Length Petal.Width <dbl> <dbl> 1 1.4 0.2 2 1.4 0.2 3 1.3 0.2 4 1.5 0.2 5 1.4 0.2 6 1.7 0.4 7 1.4 0.3 8 1.5 0.2 9 1.4 0.2 10 1.5 0.1 # … with 140 more rows
接下来,我们将探讨如何使用top_n()函数选择顶部行,这在根据特定列对 DataFrame 进行排序后想要检查几行时非常有用。
使用 top_n()函数选择顶部行
top_n() 函数在我们要关注特定列的前几个观测值时非常有用。它期望两个输入参数:返回的顶部观测值的数量(隐式按降序排序)和要排序的特定列。如果我们使用 arrange() 对列进行降序排序并使用 head() 返回顶部几行,机制将是相同的。让我们试试看。
练习 2.06 – 使用 top_n() 选择顶部行
在这个练习中,我们将演示如何将 top_n() 与其他动词结合使用:
-
返回具有最大
Sepal.Length的观测值:rst = iris_tbl %>% top_n(1, Sepal.Length) >>> rst # A tibble: 1 x 6 Sepal.Length Sepal.Width Petal.Length Petal.Width Species ind <dbl> <dbl> <dbl> <dbl> <chr> <lgl> 1 7.9 3.8 6.4 2 virginica FALSE我们可以看到,结果是包含最大
Sepal.Length的完整行。这也可以通过显式使用此列对数据集进行排序并返回第一行来实现,如下所示:rst = iris_tbl %>% arrange(desc(Sepal.Length)) %>% head(1) >>> rst # A tibble: 1 x 6 Sepal.Length Sepal.Width Petal.Length Petal.Width Species ind <dbl> <dbl> <dbl> <dbl> <chr> <lgl> 1 7.9 3.8 6.4 2 virginica FALSE我们还可以在
group_by()上下文中应用top_n(),这会将数据聚合到不同的组中。我们将在下一节中详细介绍数据聚合的更多细节。 -
返回每个
Species类别的最大Sepal.Length:rst = iris_tbl %>% group_by(Species) %>% top_n(1, Sepal.Length) %>% select(Species, Sepal.Length) >>> rst # A tibble: 3 x 2 # Groups: Species [3] Species Sepal.Length <chr> <dbl> 1 setosa 5.8 2 versicolor 7 3 virginica 7.9我们还可以使用
max()函数达到相同的目的:rst = iris_tbl %>% group_by(Species) %>% summarize(max_sepal_length = max(Sepal.Length)) >>> rst # A tibble: 3 x 2 Species max_sepal_length <chr> <dbl> 1 setosa 5.8 2 versicolor 7 3 virginica 7.9summarize()函数将数据集压缩为每个Species组的一行(具有最大的Sepal.Length)。关于这一点,我们稍后再谈。 -
返回最大的
Sepal.Length及其类别:rst = iris_tbl %>% group_by(Species) %>% summarize(max_sepal_length = max(Sepal.Length)) %>% top_n(1, max_sepal_length) >>> rst # A tibble: 1 x 2 Species max_sepal_length <chr> <dbl> 1 virginica 7.9此示例表明,我们可以将
top_n()与其他动词一起在多个上下文中使用。
现在,让我们结合我们在这里所涵盖的五个动词。
结合五个动词
我们到目前为止所涵盖的五个实用函数可以组合使用,从而提供一种灵活且简洁的数据处理方式。让我们通过一个涉及所有五个函数的练习来了解一下。
练习 2.07 – 结合五个实用函数
我们将在本练习中涵盖的示例是有点人为的,以便所有五个动词函数都可以使用。在本练习中,我们被要求找到具有最高 Sepal.Length 且 Sepal.Width 大于 Petal.Length 的前 100 行的平均绝对差值。
在执行此类复杂查询时,从数据集子集的条件开始,然后处理指标,逆向工作是有帮助的。在这种情况下,我们将首先使用 arrange() 函数按降序排序 Sepal.Length,并使用 head() 函数保留顶部 100 行。然后,使用 filter() 函数的另一个过滤条件来保留 Sepal.Width 大于 Petal.Length 的行。接下来,我们必须使用 mutate() 函数创建一个新列,以表示 Sepal.Length 和 Petal.Length 之间的绝对差值。最后,我们必须应用 select() 函数来关注新列并计算其平均值。以下代码块展示了详细的实现:
rst = iris_tbl %>%
top(80, Sepal.Length) %>%
filter(Sepal.Width > Petal.Length) %>%
mutate(Diff = abs(Sepal.Length - Petal.Length)) %>%
select(Diff) %>%
colMeans()
>>> rst
Diff
4.266667
接下来,我们将探讨另外两个动词:rename() 和 transmute()。
介绍其他动词
另外两个常用的动词是rename()和transmute()。rename()函数更改特定列的名称。例如,当使用count()函数时,会自动创建一个名为n的列。我们可以在管道上下文中使用rename(Count = n)来将其默认名称从n更改为Count。
另一种更改列名的方法是,在从数据集中选择列时,我们可以将传递给rename()的相同语句传递给select()函数。例如,以下代码展示了从iris数据集中选择Sepal.Length和Sepal.Width列,并将第二个列重命名为Sepal_Width:
rst = iris_tbl %>%
select(Sepal.Length, Sepal_Width=Sepal.Width)
>>> rst
# A tibble: 150 x 2
Sepal.Length Sepal_Width
<dbl> <dbl>
1 5.1 3.5
2 4.9 3
3 4.7 3.2
4 4.6 3.1
5 5 3.6
6 5.4 3.9
7 4.6 3.4
8 5 3.4
9 4.4 2.9
10 4.9 3.1
# … with 140 more rows
另一方面,transmute()函数是select()和mutate()的组合。它将返回一些可能被转换的列的子集。例如,假设我们想要计算Sepal.Length和Petal.Length之间的绝对值,并返回与Species一起的结果。我们可以使用transmute()实现这两个任务,如下所示:
rst = iris_tbl %>%
transmute(Species, Diff = abs(Sepal.Length - Petal.Length))
>>> rst
# A tibble: 150 x 2
Species Diff
<chr> <dbl>
1 setosa 3.7
2 setosa 3.5
3 setosa 3.4
4 setosa 3.1
5 setosa 3.6
6 setosa 3.7
7 setosa 3.2
8 setosa 3.5
9 setosa 3
10 setosa 3.4
# … with 140 more rows
虽然这些动词可以互换使用,但它们之间有一些技术上的差异。如图图 2**.1所示,select()函数返回指定的列而不改变这些列的值,这可以通过mutate()或transmutate()实现。mutate()和rename()在创建新的附加列时都会保留原始列在返回结果中,而select()和transmute()只返回结果中的指定列:

图 2.1 – 按目的和关系总结四个动词
现在我们已经知道了如何转换数据,我们可以进一步通过聚合和总结来使数据更具可解释性和可展示性。我们将在下一节中介绍不同的数据聚合方法。
使用 dplyr 进行数据聚合
数据聚合指的是一组技术,它以聚合级别总结数据集,并在更高级别上描述原始数据集。与数据转换相比,它对输入和输出都操作在行级别。
我们已经遇到了一些聚合函数,例如计算列的平均值。本节将介绍dplyr提供的最广泛使用的聚合函数中的一些。我们将从count()函数开始,它返回指定输入列每个类别的观测数/行数。
使用count()函数计数观测值
count()函数会根据输入参数自动将数据集分组到不同的类别,并返回每个类别的观测数。输入参数可以包括数据集的一个或多个列。让我们通过一个练习来应用它到iris数据集。
练习 2.08 – 按物种计数观测值
本练习将使用count()函数获取每个独特物种的观测值数量,然后使用filter()函数添加过滤条件:
-
计算在
iris数据集中每种独特物种类型的观测值数量:rst = iris_tbl %>% count(Species) >>> rst # A tibble: 3 x 2 Species n <chr> <int> 1 setosa 50 2 versicolor 50 3 virginica 50输出是一个包含两列的
tibble数据集,其中第一列包含Species中的唯一类别,第二列(默认命名为n)是对应的行计数。让我们看看如何在计数操作之前进行过滤。
-
对那些
Sepal.Length和Sepal.Width之间的绝对差值大于Petal.Length和Petal.Width的观测值进行精确计数。按降序返回结果:rst = iris_tbl %>% filter(abs(Sepal.Length-Sepal.Width) > abs(Petal.Length-Petal.Width)) %>% count(Species, sort=TRUE) >>> rst # A tibble: 3 x 2 Species n <chr> <int> 1 setosa 45 2 versicolor 33 3 virginica 28在这里,我们添加了一个过滤条件,在计数之前保留满足指定标准的行。我们启用了
sort参数来按降序排列结果。
count()函数本质上结合了两个步骤:按指定列的每个类别进行分组,然后计数观测值的数量。结果证明,我们可以使用下一节中介绍的group_by()和summarize()函数完成相同的任务。
通过group_by()和summarize()进行数据聚合
count()是一种有用的数据聚合方法。然而,它是两个更通用聚合函数group_by()和summarize()的特例,这两个函数通常一起使用。group_by()函数根据输入参数中的一个或多个列将原始数据集分割成不同的组,而summarize()函数则将特定类别内的所有观测值汇总并折叠成一个指标,在count()的情况下,这个指标可能是行数。
可以在summarize()函数中使用多个汇总函数。典型的一些包括以下:
-
sum(): 对特定组的所有观测值求和 -
mean(): 计算所有观测值的平均值 -
median(): 计算所有观测值的中位数 -
max(): 计算所有观测值的最大值 -
min(): 计算所有观测值的最小值
让我们通过一个使用group_by()和summarize()计算不同汇总统计的练习来了解。
练习 2.09 – 使用 group_by()和 summarize()汇总数据集
本练习涵盖使用group_by()和summarize()函数提取计数和均值统计,结合之前介绍的一些动词,包括filter()、mutate()和arrange():
-
获取每种独特类型的
Species的观测值计数:rst = iris_tbl %>% group_by(Species) %>% summarise(n=n()) >>> rst # A tibble: 3 x 2 Species n <chr> <int> 1 setosa 50 2 versicolor 50 3 virginica 50在前面的代码中,我们使用了
n()函数来获取观测值的数量,并将结果分配给名为n的列。计数是在根据唯一的Species类型对观测值进行分组之后进行的。 -
添加与之前练习相同的过滤器,并按降序排序结果:
rst = iris_tbl %>% filter(abs(Sepal.Length-Sepal.Width) > abs(Petal.Length-Petal.Width)) %>% group_by(Species) %>% summarise(n=n()) %>% arrange(desc(n)) >>> rst # A tibble: 3 x 2 Species n <chr> <int> 1 setosa 45 2 versicolor 33 3 virginica 28在此代码块中,首先应用过滤条件以限制分组操作到观察值的子集。在
arrange()函数中,我们直接使用n列按降序排序。 -
基于相同的过滤条件创建一个逻辑列,并使用
Species进行两级分组。然后,创建一个逻辑列来计算平均Sepal.Length:rst = iris_tbl %>% mutate(ind = abs(Sepal.Length-Sepal.Width) > abs(Petal.Length-Petal.Width)) %>% group_by(Species, ind) %>% summarise(mean_sepal_length=mean(Sepal.Length)) >>> rst # A tibble: 6 x 3 # Groups: Species [3] Species ind mean_sepal_length <chr> <lgl> <dbl> 1 setosa FALSE 5 2 setosa TRUE 5.01 3 versicolor FALSE 5.78 4 versicolor TRUE 6.02 5 virginica FALSE 6.39 6 virginica TRUE 6.74我们可以在
group_by()函数中放入多个分类列以执行多级分组。注意,结果包含基于Species的Groups属性,表明tibble对象具有分组结构。让我们学习如何移除该结构。 -
使用
ungroup()移除返回的tibble对象中的分组结构:rst = iris_tbl %>% mutate(ind = abs(Sepal.Length-Sepal.Width) > abs(Petal.Length-Petal.Width)) %>% group_by(Species, ind) %>% summarise(mean_sepal_length=mean(Sepal.Length)) %>% ungroup() >>> rst # A tibble: 6 x 3 Species ind mean_sepal_length <chr> <lgl> <dbl> 1 setosa FALSE 5 2 setosa TRUE 5.01 3 versicolor FALSE 5.78 4 versicolor TRUE 6.02 5 virginica FALSE 6.39 6 virginica TRUE 6.74现在,结果包含一个正常的
tibble对象,其中包含Species和ind每个唯一组合的平均花萼长度。
现在我们已经知道如何转换和聚合一个数据集,我们将介绍如何通过合并和连接来处理多个数据集。
使用 dplyr 进行数据合并
在实际数据分析中,我们需要的信息不一定局限于一个表,而是分散在多个表中。将数据存储在单独的表中是内存高效的,但不是分析友好的。数据合并是将不同的数据集合并到一个表中以方便数据分析的过程。在连接两个表时,需要有一个或多个存在于两个表中的列,或键,作为连接的共同基础。
本节将介绍不同的连接表和组合分析的方法,包括内部连接、左连接、右连接和全连接。以下列表显示了这些连接类型的动词及其定义:
-
inner_join(): 根据匹配的关键值返回两个表中的共同观察值。 -
left_join(): 返回左表中的所有观察值和右表中匹配的观察值。注意,如果右表中存在重复的关键值,将自动在左表中创建并添加额外的行。空单元格将填充为NA。更多内容请参考练习。 -
right_join(): 返回右表中的所有观察值和左表中匹配的观察值。空单元格将填充为NA。 -
full_join(): 返回两个表中的所有观察值。空单元格将填充为NA。
图 2**.2 使用维恩图说明了这四种连接方式:

图 2.2 – 实践中常用的四种连接方式
让我们通过练习来了解这四种连接方式。
练习 2.10 – 数据集连接
此练习将创建两个虚拟 tibble 数据集,并应用不同的连接动词将它们合并:
-
按照此代码块中的步骤创建两个虚拟数据集:
a = 1:3 tbl_A = tibble(key_A=a, col_A=2*a) tbl_B = tibble(key_B=a+1, col_B=3*a) >>> tbl_A # A tibble: 3 x 2 key_A col_A <int> <dbl> 1 1 2 2 2 4 3 3 6 >>> tbl_B # A tibble: 3 x 2 key_B col_B <dbl> <dbl> 1 2 3 2 3 6 3 4 9两个虚拟数据集都有三行两列,第一列是用于连接的关键列。
-
对两个数据集执行内部连接:
rst = tbl_A %>% inner_join(tbl_B, by=c("key_A"="key_B")) >>> rst # A tibble: 2 x 3 key_A col_A col_B <dbl> <dbl> <dbl> 1 2 4 3 2 3 6 6前面的代码显示,匹配的键是通过
c()函数在by参数中指定的。由于"key_A"和"key_B"只有两个共同的值,内连接操作后的结果表是一个 2x3 的tibble,只保留"key_A"作为键列,以及来自两个表的所有其他非键列。它只保留具有精确匹配的观测值,并且无论在哪个方向上与哪个表连接,工作方式都是相同的。我们还可以在
by参数中传递额外的匹配键(表将根据这些键进行合并),同时遵循相同的格式来执行多级合并。让我们看看如何执行左连接。
-
执行两个数据集的左连接:
rst = tbl_A %>% left_join(tbl_B, by=c("key_A"="key_B")) >>> rst # A tibble: 3 x 3 key_A col_A col_B <dbl> <dbl> <dbl> 1 1 2 NA 2 2 4 3 3 3 6 6注意,结果表包含整个
tbl_A以及一个额外的列col_B,该列是从tbl_B引用的。由于col_B中没有 1,因此col_B中相应的单元格显示为NA。一般来说,任何无法匹配的单元格在结果表中都将假设为NA的值。注意,当
col_B中有多个具有重复值的行时,左连接后的结果表也会自动创建一个重复行,因为现在它是一个从左到右的一对二映射。让我们看看一个例子。 -
创建另一个具有重复键值的表,并与
tbl_A执行左连接:tbl_C = tbl_B %>% bind_rows(tbl_B[1,]) tbl_C[nrow(tbl_C),"col_B"] = 10 >>> tbl_C # A tibble: 4 x 2 key_B col_B <dbl> <dbl> 1 2 3 2 3 6 3 4 9 4 2 10在这里,我们使用了
bind_rows()函数来追加一个新行,其中"key_B"的值与第一行相同,而col_B的值不同。让我们看看当我们将其与tbl_A连接时会发生什么:rst = tbl_A %>% left_join(tbl_C, by=c("key_A"="key_B")) >>> rst # A tibble: 4 x 3 key_A col_A col_B <dbl> <dbl> <dbl> 1 1 2 NA 2 2 4 3 3 2 4 10 4 3 6 6在右侧表的键列中存在重复值是常见的问题来源,这些问题可能难以追踪。经验丰富的数据科学家应该特别注意在左连接之前和之后检查数据集的维度,以避免这种潜在的不期望的结果。现在,让我们看看如何执行右连接。
-
将
tbl_A与tbl_B执行右连接:rst = tbl_A %>% right_join(tbl_B, by=c("key_A"="key_B")) >>> rst # A tibble: 3 x 3 key_A col_A col_B <dbl> <dbl> <dbl> 1 2 4 3 2 3 6 6 3 4 NA 9同样,
tbl_B中的所有观测值都保留,col_A中的缺失值用NA填充。此外,键列被命名为"key_A"而不是"key_B"。 -
执行
tbl_A和tbl_B的全连接:rst = tbl_A %>% full_join(tbl_B, by=c("key_A"="key_B")) >>> rst # A tibble: 4 x 3 key_A col_A col_B <dbl> <dbl> <dbl> 1 1 2 NA 2 2 4 3 3 3 6 6 4 4 NA 9使用全连接,保留两个表的所有匹配结果,缺失值用
NA填充。当我们不希望从源表中遗漏任何观测值时,可以使用此方法。
这四个连接语句可以重复使用以连接多个表,并可与之前覆盖的任何数据转换动词结合使用。例如,我们可以删除具有NA值的行,并仅保留complete行,这应该在内连接中给出相同的结果。这可以通过使用tidyr包提供的实用函数drop_na()来实现,该函数专门设计用于tidyverse生态系统中的数据清理:
library(tidyr)
rst = tbl_A %>%
full_join(tbl_B, by=c("key_A"="key_B")) %>%
drop_na()
>>> rst
# A tibble: 2 x 3
key_A col_A col_B
<dbl> <dbl> <dbl>
1 2 4 3
2 3 6 6
我们还可能想用 0 替换NA值,这可以通过tidyr提供的replace_na()函数实现。在下面的代码中,我们指定了每个感兴趣列的替换值,并将它们包装在一个列表中传递给replace_na():
rst = tbl_A %>%
full_join(tbl_B, by=c("key_A"="key_B")) %>%
replace_na(list(col_A=0, col_B=0))
>>> rst
# A tibble: 4 x 3
key_A col_A col_B
<dbl> <dbl> <dbl>
1 1 2 0
2 2 4 3
3 3 6 6
4 4 0 9
注意,还有其他合并选项,例如半连接和反连接,分别对应于semi_join()和anti_join()函数。半连接只返回第一个参数表中在第二个表中存在匹配的行。尽管与全连接操作类似,但半连接只保留第一个表中的列。反连接操作,另一方面,是半连接的相反操作,只返回第一个表中不匹配的行。由于许多合并操作,包括这两个,都可以使用我们在本节中介绍的基本操作推导出来,因此我们不会详细介绍这些稍微复杂一些的连接操作。相反,我们鼓励您探索使用这四个基本的连接函数来实现复杂的操作,而不是依赖于其他快捷连接函数。
接下来,我们将通过一个案例研究来观察如何使用本章中介绍的功能来转换、合并和聚合数据集。
案例研究 – 使用 Stack Overflow 数据集
本节将介绍一个练习,帮助您练习基于公共 Stack Overflow 数据集的不同数据转换、聚合和合并技术,该数据集包含一系列与 Stack Overflow 平台上发布的技术问题和答案相关的表。支持的原数据已上传到本书的配套 GitHub 仓库。我们将直接从源 GitHub 链接使用readr包下载,readr是tidyverse提供的一个易于使用、快速且友好的包,可以轻松读取各种数据源,包括来自网络的源。
练习 2.11 – 使用 Stack Overflow 数据集
让我们开始这个练习:
-
从 GitHub 下载关于问题、标签及其映射表的三个数据源:
library(readr) df_questions = read_csv("https://raw.githubusercontent.com/PacktPublishing/The-Statistics-and-Machine-Learning-with-R-Workshop/main/Chapter_2/data/questions.csv") >>> df_questions # A tibble: 294,735 x 3 id creation_date score <dbl> <date> <dbl> 1 22557677 2014-03-21 1 2 22557707 2014-03-21 2 3 22558084 2014-03-21 2 4 22558395 2014-03-21 2 5 22558613 2014-03-21 0 6 22558677 2014-03-21 2 7 22558887 2014-03-21 8 8 22559180 2014-03-21 1 9 22559312 2014-03-21 0 10 22559322 2014-03-21 2 # … with 294,725 more rows问题数据集包含问题 ID、创建日期和分数,分数表示(正)赞同票数和(负)反对票数。我们可以使用
summary()函数检查分数的范围:>>> summary(df_questions$score) Min. 1st Qu. Median Mean 3rd Qu. Max. -21.000 0.000 1.000 1.904 2.000 2474.000该数据集包含每个标签的 ID 和内容。为了分析标签,我们需要使用相关的映射键将这三个数据集合并成一个。
-
通过左连接将
df_question_tags中的标签 ID 引用到df_questions中:df_all = df_questions %>% left_join(df_question_tags, by=c("id"="question_id")) >>> df_all # A tibble: 545,694 x 4 id creation_date score tag_id <dbl> <date> <dbl> <dbl> 1 22557677 2014-03-21 1 18 2 22557677 2014-03-21 1 139 3 22557677 2014-03-21 1 16088 4 22557677 2014-03-21 1 1672 5 22557707 2014-03-21 2 NA 6 22558084 2014-03-21 2 6419 7 22558084 2014-03-21 2 92764 8 22558395 2014-03-21 2 5569 9 22558395 2014-03-21 2 134 10 22558395 2014-03-21 2 9412 # … with 545,684 more rows注意,当比较
df_questions和df_all时,行数几乎翻了一番。您可能已经注意到这是由于一对一关系:一个问题通常有多个标签,所以在左连接操作期间,每个标签都会作为单独的行附加到左表中。 -
让我们继续参考
df_tags中的标签:df_all = df_all %>% left_join(df_tags, by=c("tag_id"="id")) >>> df_all # A tibble: 545,694 x 5 id creation_date score tag_id tag_name <dbl> <date> <dbl> <dbl> <chr> 1 22557677 2014-03-21 1 18 regex 2 22557677 2014-03-21 1 139 string 3 22557677 2014-03-21 1 16088 time-complexity 4 22557677 2014-03-21 1 1672 backreference 5 22557707 2014-03-21 2 NA NA 6 22558084 2014-03-21 2 6419 time-series 7 22558084 2014-03-21 2 92764 panel-data 8 22558395 2014-03-21 2 5569 function 9 22558395 2014-03-21 2 134 sorting 10 22558395 2014-03-21 2 9412 vectorization # … with 545,684 more rows接下来,我们将对标签进行一些分析,从计算它们的频率开始。
-
按降序统计每个非
NA标签的出现次数:df_all = df_all %>% filter(!is.na(tag_name)) rst = df_all %>% count(tag_name, sort = TRUE) >>> rst # A tibble: 7,840 x 2 tag_name n <chr> <int> 1 ggplot2 28228 2 dataframe 18874 3 shiny 14219 4 dplyr 14039 5 plot 11315 6 data.table 8809 7 matrix 6205 8 loops 5149 9 regex 4912 10 function 4892 # … with 7,830 more rows在这里,我们首先使用
filter()删除tag_name为NA的行,然后使用count()函数计算计数。结果显示,dplyr是 Stack Overflow 上最受欢迎的 R 相关标签之一,这是一个好兆头,因为它表明我们正在学习有用的和流行的东西。 -
计算每年标签的数量:
library(lubridate) rst = df_all %>% mutate(year = year(creation_date)) %>% count(year) >>> rst # A tibble: 12 x 2 year n <dbl> <int> 1 2008 18 2 2009 874 3 2010 3504 4 2011 8787 5 2012 18251 6 2013 34998 7 2014 50749 8 2015 66652 9 2016 76056 10 2017 90462 11 2018 96819 12 2019 49983结果显示,每年标签的数量都在增加,2019 年是一个特殊情况,因为数据在 2019 年中途结束(在此处验证)。请注意,我们使用了
tidyverse中的lubricate包的year()函数将日期格式的列转换为相应的年份:>>> max(df_all$creation_date) "2019-07-01" -
计算每月标签的平均出现次数。
我们需要推导出标签的月度出现次数来计算它们的平均值。首先,我们必须为每个标签创建两个列来表示月份和年月:
df_all = df_all %>% mutate(month = month(creation_date), year_month = format(creation_date, "%Y%m")) >>> df_all # A tibble: 497,153 x 7 id creation_date score tag_id tag_name month year_month <dbl> <date> <dbl> <dbl> <chr> <dbl> <chr> 1 22557677 2014-03-21 1 18 regex 3 201403 2 22557677 2014-03-21 1 139 string 3 201403 3 22557677 2014-03-21 1 16088 time-complexity 3 201403 4 22557677 2014-03-21 1 1672 backreference 3 201403 5 22558084 2014-03-21 2 6419 time-series 3 201403 6 22558084 2014-03-21 2 92764 panel-data 3 201403 7 22558395 2014-03-21 2 5569 function 3 201403 8 22558395 2014-03-21 2 134 sorting 3 201403 9 22558395 2014-03-21 2 9412 vectorization 3 201403 10 22558395 2014-03-21 2 18621 operator-precedence 3 201403 # … with 497,143 more rows然后,我们必须计算每年每月标签的出现次数:
rst1 = df_all %>% count(year_month, month) >>> rst1 # A tibble: 130 x 3 year_month month n <chr> <dbl> <int> 1 200809 9 13 2 200811 11 4 3 200812 12 1 4 200901 1 8 5 200902 2 10 6 200903 3 7 7 200904 4 24 8 200905 5 3 9 200906 6 12 10 200907 7 100 # … with 120 more rows最后,我们必须对每个月份的所有年份进行平均:
rst2 = rst1 %>% group_by(month) %>% summarise(avg_num_tag = mean(n)) >>> rst2 # A tibble: 12 x 2 month avg_num_tag <dbl> <dbl> 1 1 3606. 2 2 3860. 3 3 4389. 4 4 4286. 5 5 4178. 6 6 4133. 7 7 3630. 8 8 3835. 9 9 3249. 10 10 3988. 11 11 3628. 12 12 3125.结果显示,3 月份标签的平均出现次数最高。也许学校刚刚开学,人们在 3 月份更加积极地学习和提问。
-
计算每个标签的计数、最小值、平均分数和最大分数,并按计数降序排序:
rst = df_all %>% group_by(tag_name) %>% summarise(count = n(), min_score = min(score), mean_score = mean(score), max_score = max(score)) %>% arrange(desc(count)) >>> rst # A tibble: 7,840 x 5 tag_name count min_score mean_score max_score <chr> <int> <dbl> <dbl> <dbl> 1 ggplot2 28228 -9 2.61 666 2 dataframe 18874 -11 2.31 1241 3 shiny 14219 -7 1.45 79 4 dplyr 14039 -9 1.95 685 5 plot 11315 -10 2.24 515 6 data.table 8809 -8 2.97 685 7 matrix 6205 -10 1.66 149 8 loops 5149 -8 0.743 180 9 regex 4912 -9 2 242 10 function 4892 -14 1.39 485 # … with 7,830 more rows在这里,我们在
group_by()和summarize()的上下文中使用了多个汇总函数来计算指标。
摘要
在本章中,我们介绍了数据转换、聚合和合并的基本函数和技术。对于行级别的数据转换,我们学习了常见的实用函数,如filter()、mutate()、select()、arrange()、top_n()和transmute()。对于数据聚合,它将原始数据集总结成一个更小、更简洁的概览视图,我们介绍了count()、group_by()和summarize()等函数。对于数据合并,它将多个数据集合并成一个,我们学习了不同的连接方法,包括inner_join()、left_join()、right_join()和full_join()。尽管还有其他更高级的连接函数,但我们工具箱中涵盖的基本工具已经足够我们完成相同任务。最后,我们通过 Stack Overflow 数据集的案例研究进行了说明。本章学到的技能在许多数据分析任务中都将非常有用。
在下一章中,我们将介绍一个更高级的自然语言处理主题,这将使我们进一步使用tidyverse处理文本数据。
第三章:中间数据处理
上一章介绍了dplyr提供的用于数据处理的常用函数集。例如,在描述和提取数据集的统计信息时,我们可以使用group_by()和summarize()函数遵循拆分-应用-组合程序。本章从上一章继续,重点关注中间数据处理技术,包括转换分类和数值变量以及重塑数据框。除此之外,我们还将介绍用于处理文本数据的字符串操作技术,其格式与我们迄今为止所使用的整洁表格格式根本不同。
到本章结束时,你将能够执行更高级的数据操作,并将你的数据处理技能扩展到基于字符串的文本,这对于自然语言处理领域是基本的。
本章将涵盖以下主题:
-
转换分类和数值变量
-
重塑数据框
-
操作字符串数据
-
使用
stringr -
介绍正则表达式
-
使用整洁文本挖掘
技术要求
要完成本章的练习,你需要具备以下条件:
-
编写本文时,
rebus包的最新版本是 0.1-3 -
编写本文时,
tidytext包的最新版本是 0.3.2 -
编写本文时,
tm包的最新版本是 0.7-8
本章的所有代码和数据都可在以下链接找到:github.com/PacktPublishing/The-Statistics-and-Machine-Learning-with-R-Workshop/tree/main/Chapter_3。
转换分类和数值变量
如前一章所述,我们可以使用dplyr中的mutate()函数来转换现有变量并创建新变量。具体的转换取决于变量的类型和我们希望其具有的形状。例如,我们可能希望根据映射字典更改分类变量的值,基于现有变量的过滤条件组合创建新变量,或者将数值变量分组到新的变量中的不同范围。让我们依次查看这些场景。
重新编码分类变量
在许多情况下,你可能需要重新编码变量的值,例如将国家的简称映射到相应的全称。让我们创建一个模拟的tibble数据集来展示这一点。
在以下代码中,我们创建了一个students变量,用于存储有关年龄、国家、性别和身高的信息。这是一个小型模拟数据集,但对于演示目的来说已经足够好了:
students = tibble(age = c(26, 30, 28, 31, 25, 29, 30, 29),
country = c('SG', 'CN', 'US', 'UK','CN', 'SG', 'IN', 'SG'),
gender = c('F', 'F', 'M', 'M', 'M', 'F', 'F', 'M'),
height = c(168, 169, 175, 178, 170, 170, 172, 180))
现在,让我们通过一个将country变量的值转换为全称的示例来了解这个过程。
练习 3.1 – 将国家变量值转换为全称
这个练习将使用 dplyr 包中的 recode() 函数将现有的短国家名称映射到相应的全称:
-
通过使用
recode()函数并提供映射表来添加一个新列,将短国家名称转换为相应的全称:students_new = students %>% mutate(country_fullname = recode(country, "SG"="Singapore", "CN"="China", "UK"="United Kingdom", "IN"="India")) >>> students_new # A tibble: 8 x 5 age country gender height country_fullname <dbl> <chr> <chr> <dbl> <chr> 1 26 SG F 168 Singapore 2 30 CN F 169 China 3 28 UK M 175 United Kingdom 4 31 UK M 178 United Kingdom 5 25 CN M 170 China 6 29 SG F 170 Singapore 7 30 IN F 172 India 8 29 SG M 180 Singapore在这里,我们提供了映射字典作为
recode()函数的参数,该函数在左侧列中搜索键,并将右侧列中对应的值分配给country_fullname。请注意,新创建的列假定是字符类型。 -
执行相同的转换,并将结果存储为
factor类型:students_new = students_new %>% mutate(country_fullname2 = recode_factor(country, "SG"="Singapore", "CN"="China", "UK"="United Kingdom", "IN"="India")) >>> students_new # A tibble: 8 x 6 age country gender height country_fullname country_fullname2 <dbl> <chr> <chr> <dbl> <chr> <fct> 1 26 SG F 168 Singapore Singapore 2 30 CN F 169 China China 3 28 UK M 175 United Kingdom United Kingdom 4 31 UK M 178 United Kingdom United Kingdom 5 25 CN M 170 China China 6 29 SG F 170 Singapore Singapore 7 30 IN F 172 India India 8 29 SG M 180 Singapore Singapore我们可以看到,使用
recode_factor()后,生成的变量country_fullname2是一个因子。
当我们想要创建的新列依赖于现有列的复杂组合时,我们可以求助于下一节中介绍的 case_when() 函数。
使用 case_when() 创建变量
case_when() 函数提供了一个在创建新变量时设置多个 if-else 条件的便捷方式。它接受一系列双向公式,其中左侧包含筛选条件,右侧提供与先前条件匹配的替换值。函数内部的语法遵循 逻辑条件(们) ~ 替换值 的模式,这些条件按顺序进行评估,其中可以在逻辑条件中使用多个变量。序列的末尾是一个 TRUE ~ 默认值 的情况,如果所有先前条件评估为 FALSE,则将该值分配给变量。
让我们通过一个练习来创建一个基于多个 if-else 条件的新变量,这些条件涉及多个列。
练习 3.2 – 使用多个条件和列创建新变量
在这个练习中,我们将创建一个新变量,该变量指示学生的年龄和地区。
创建一个新变量类型,用于识别学生是否来自亚洲,以及他们是否在 20 多岁或 30 多岁,分别假设 asia_20+ 和 asia_30+ 作为值。如果没有匹配项,将值设置为 others:
students_new = students %>%
mutate(type = case_when(age >= 30 & country %in% c("SG","IN","CN") ~ "asia_30+",
age < 30 & age >= 20 & country %in% c("SG","IN","CN") ~ "asia_20+",
TRUE ~ "others"))
>>> students_new
# A tibble: 8 x 5
age country gender height type
<dbl> <chr> <chr> <dbl> <chr>
1 26 SG F 168 asia_20+
2 30 CN F 169 asia_30+
3 28 UK M 175 others
4 31 UK M 178 others
5 25 CN M 170 asia_20+
6 29 SG F 170 asia_20+
7 30 IN F 172 asia_30+
8 29 SG M 180 asia_20+
在这里,我们使用了 & 符号来组合多个评估 年龄 和 国家 的 AND 条件。当序列中的前一个条件都不评估为 TRUE 时,函数将落入包含所有条件的 TRUE 情况,并将 others 作为默认值分配。
接下来,我们将查看将数值列转换为不同的箱/类别。
使用 cut() 对数值变量进行分箱
可以使用 cut() 函数将数值列划分为不同的类别。对于数值列,它将值分配给相应的预定义区间,并根据分配的区间对值进行编码。生成的区间列假定是一个有序因子类型。
cut()函数有三个关键参数:x用于接受要分组的数值向量,breaks用于接受一个数值向量作为切割点,这可能包括负无穷大-Inf和正无穷大Inf,以及labels用于指示结果区间的标签。
让我们通过使用cut()将age列转换为不同的年龄组进行一次练习。
练习 3.3 – 将年龄列分为三个组
在这个练习中,我们将使用cut()函数将age列的值分配到以下范围之一:(-infinity, 25)、[26, 30]或[31, infinity]:
-
将
age列分为三个区间,断点为25和30(右侧包含),并将它们存储在一个名为age_group的新列中:students_new = students %>% mutate(age_group = cut(x = age, breaks = c(-Inf, 25, 30, Inf), labels = c("<=25", "26-30", ">30"))) >>> students_new # A tibble: 8 x 5 age country gender height age_group <dbl> <chr> <chr> <dbl> <fct> 1 26 SG F 168 26-30 2 30 CN F 169 26-30 3 28 UK M 175 26-30 4 31 UK M 178 >30 5 25 CN M 170 <=25 6 29 SG F 170 26-30 7 30 IN F 172 26-30 8 29 SG M 180 26-30这里,我们可以看到
age_group是一个有序因子,有三个层级。几个切割函数在没有特定截止点的情况下执行自动分组。例如,
cut_interval()将原始向量切割成指定数量的等间隔组,而cut_number()将输入向量转换为指定数量的组,其中每个组大约有相同数量的观测值。tidyverse包提供了这两个函数。让我们尝试一下。 -
使用
cut_interval()将age列分为三个等长的组:students_new = students %>% mutate(age_group = cut_interval(age, n=3)) >>> students_new # A tibble: 8 x 5 age country gender height age_group <dbl> <chr> <chr> <dbl> <fct> 1 26 SG F 168 [25,27] 2 30 CN F 169 (29,31] 3 28 UK M 175 (27,29] 4 31 UK M 178 (29,31] 5 25 CN M 170 [25,27] 6 29 SG F 170 (27,29] 7 30 IN F 172 (29,31) 8 29 SG M 180 (27,29)age_group列现在由三个代表等长区间的层级组成。让我们使用summary()函数检查每个层级的计数:>>> summary(students_new$age_group) [25,27] (27,29) (29,31) 2 3 3 -
使用
cut_interval()将age列分为具有相等观测数的三个组:students_new = students %>% mutate(age_group = cut_number(age, n=3)) >>> students_new # A tibble: 8 x 5 age country gender height age_group <dbl> <chr> <chr> <dbl> <fct> 1 26 SG F 168 [25,28.3] 2 30 CN F 169 (29.7,31] 3 28 UK M 175 [25,28.3] 4 31 UK M 178 (29.7,31] 5 25 CN M 170 [25,28.3] 6 29 SG F 170 (28.3,29.7] 7 30 IN F 172 (29.7,31) 8 29 SG M 180 (28.3,29.7)截止点现在假设为小数点,以使观测值的计数大约相等,如下面的代码所验证:
>>> summary(students_new$age_group) [25,28.3] (28.3,29.7) (29.7,31) 3 2 3
到目前为止,我们已经探讨了不同的方法来转换现有的分类或数值变量,并基于特定条件创建新变量。接下来,我们将探讨如何转换和重塑整个 DataFrame,以方便我们的分析。
重塑 DataFrame
由分类和数值列组合而成的 DataFrame 可以用宽格式和长格式表示。例如,studentsDataFrame 被认为是长格式,因为所有国家都存储在country列中。根据处理的具体目的,我们可能希望为数据集中的每个唯一国家创建一个单独的列,这会增加 DataFrame 的列数,并将其转换为宽格式。
通过spread()和gather()函数可以在宽格式和长格式之间进行转换,这两个函数都由tidyr包提供,属于tidyverse生态系统。让我们看看它在实际中的应用。
使用spread()将长格式转换为宽格式
有时会需要将长格式 DataFrame 转换为宽格式。spread() 函数可以将具有多个类别的分类列转换为由 key 参数指定的多个列,每个类别作为 DataFrame 中的单独列添加。列名将是分类列的唯一值。value 参数指定在调用 spread() 函数时要在这些附加列中展开和填充的内容。让我们通过一个练习来了解。
练习 3.4 – 将长格式转换为宽格式
在这个练习中,我们将使用 spread() 将 students DataFrame 转换为宽格式:
-
使用
country作为key参数,height作为value参数,使用spread()将学生转换为宽格式。将结果 DataFrame 存储在students_wide中:students_wide = students %>% spread(key = country, value = height) >>> students_wide # A tibble: 7 x 6 age gender CN IN SG UK <dbl> <chr> <dbl> <dbl> <dbl> <dbl> 1 25 M 170 NA NA NA 2 26 F NA NA 168 NA 3 28 M NA NA NA 175 4 29 F NA NA 170 NA 5 29 M NA NA 180 NA 6 30 F 169 172 NA NA 7 31 M NA NA NA 178我们可以看到原始的
height列消失了,并增加了四个附加列。这四个列对应于唯一的各国,这些列的值由身高填充。如果特定国家的对应身高不可用,则使用NA填充缺失的组合。如果我们想为这些
NA值指定默认值,我们可以在spread()中设置fill参数。 -
使用四舍五入的平均身高来填充结果宽格式中的
NA值。将结果 DataFrame 存储在students_wide2中:avg_height = round(mean(students$height)) students_wide2 = students %>% spread(key = country, value = height, fill = avg_height) >>> students_wide2 # A tibble: 7 x 6 age gender CN IN SG UK <dbl> <chr> <dbl> <dbl> <dbl> <dbl> 1 25 M 170 173 173 173 2 26 F 173 173 168 173 3 28 M 173 173 173 175 4 29 F 173 173 170 173 5 29 M 173 173 180 173 6 30 F 169 172 173 173 7 31 M 173 173 173 178
从长格式转换为宽格式在分析和展示方面可能很有帮助,因为我们可以直观地比较特定年龄和性别组合下所有国家的身高。然而,这会带来额外的存储成本,如之前显示的多个 NA 值所示。
现在,让我们学习如何将宽格式 DataFrame 转换为长格式。
使用 gather() 函数将宽格式转换为长格式
当我们处于相反的情况,即给定的数据是宽格式时,我们可以使用 gather() 函数将其转换为长格式,以便进行更方便的后续处理。例如,通过将四个国家列压缩到 key 变量中,并将所有身高存储在 gather() 中指定的 value 变量下,我们可以继续使用我们之前介绍过的基于两列(而不是四列或更多)的常规分割-应用-组合处理。
gather() 函数也使用 key 和 value 参数来指定长格式表中结果的 key 和 value 列的名称。此外,我们还需要指定用于填充 key 列的列名和 value 列中的值。当需要指定许多相邻的列时,我们可以通过传递起始和结束列名来使用 : 运算符选择所有中间的列。让我们通过一个练习来了解。
练习 3.5 – 将宽格式转换为长格式
本练习将宽格式化的students_wide DataFrame 转换回其原始的长格式:
-
通过指定
key列为country和value列为height,并将CN、IN、SG和UK列的值分别用于填充key和value列,将students_wide转换为长格式:students_long = students_wide %>% gather(key = "country", value = "height", CN:UK) >>> students_long # A tibble: 28 x 4 age gender country height <dbl> <chr> <chr> <dbl> 1 25 M CN 170 2 26 F CN NA 3 28 M CN NA 4 29 F CN NA 5 29 M CN NA 6 30 F CN 169 7 31 M CN NA 8 25 M IN NA 9 26 F IN NA 10 28 M IN NA # … with 18 more rows我们可以看到,由于
students_wide中原本就存在缺失值,height列中添加了几行缺失值。让我们使用dplyr中的drop_na()函数来删除它们。 -
删除
height列中的NA值行:students_long = students_long %>% drop_na(height) >>> students_long # A tibble: 8 x 4 age gender country height <dbl> <chr> <chr> <dbl> 1 25 M CN 170 2 30 F CN 169 3 30 F IN 172 4 26 F SG 168 5 29 F SG 170 6 29 M SG 180 7 28 M UK 175 8 31 M UK 178通过这样,我们已经获得了长格式的 DataFrame。现在,让我们验证它是否与原始的
studentsDataFrame 相同。 -
使用
all_equal()验证students_long是否与students相同:>>> all_equal(students, students_long, ignore_row_order = T, ignore_col_order = T) TRUEdplyr中的all_equal()函数比较两个数据集并检查它们是否相同。它提供了一种灵活的方法来进行等价比较,并支持忽略行和/或列的顺序。结果显示,我们已经成功转换回原始数据集。
通过这样,我们已经探讨了不同的方法来重塑 DataFrame。接下来,我们将介绍如何处理字符串数据。
操作字符串数据
字符串类型在现实生活中的数据中很常见,例如姓名和地址。分析字符串数据需要正确清理原始字符,并将文本数据块中嵌入的信息转换为可量化的数值摘要。例如,我们可能想要找到所有遵循特定模式的学生的匹配姓名。
本节将介绍通过正则表达式定义不同模式的方法,以检测、分割和提取字符串数据。让我们从字符串的基础知识开始。
创建字符串
有时,单个引号(')也被用来表示字符串,尽管通常建议除非字符本身包含双引号,否则使用双引号。
创建字符串有多种方式。以下练习介绍了初始化字符类型字符串的几种不同方法。
练习 3.6 – 在 R 中表达字符串
在这个练习中,我们将探讨如何在 R 中创建字符串:
-
尝试在 R 控制台中键入以下字符串:
>>> "statistics workshop" "statistics workshop"字符串被正确打印出来。让我们看看如果我们用双引号包裹
statistics会发生什么。 -
在字符串中给
statistics添加双引号:>>> ""statistics" workshop" Error: unexpected symbol in """statistics"这次,出现了一个错误,因为 R 将第二个双引号视为字符串的结束引号。可以通过在字符串中使用双引号时,将外部引号切换为单引号来避免这个错误。
-
用单引号包裹前面的字符串:
>>> '"statistics" workshop' "\"statistics\" workshop"现在,R 正确解释了字符串,并将单引号对内的所有内容视为一个整体字符串。请注意,结果字符串在控制台中仍然用双引号打印。字符串内的两个双引号前面也有一个反斜杠(
\)。这被称为转义序列,用于指示双引号作为字符的原始解释,而不是字符串的开始。转义序列是在字符串中包含特殊字符的有用方式。我们也可以在字符串内部手动添加转义字符,以强制正确解释,这将打印出与之前相同的结果。
-
在字符串内部双引号之前添加转义序列:
>>> "\"statistics\" workshop" "\"statistics\" workshop"使用反斜杠打印字符串序列不方便阅读。为了美化输出,我们可以将确切字符串传递给
writeLines()函数。 -
使用
writeLines()打印相同的字符串:>>> writeLines("\"statistics\" workshop") "statistics" workshop
接下来,我们将探讨如何将数字转换为字符串以进行更好的解释。
将数字转换为字符串
如我们之前所学,数字可以通过as.character()函数转换为字符串。然而,直接读取和报告像123000这样的大数字会很不方便。我们通常会以更易读的方式表达,例如 123,000,或者以更简洁的方式,例如 1.23e+05,后者遵循科学表示法,其中 e+05 等于 105。此外,我们可能还想显示浮点数小数点后有限位数的数字。
所有这些都可以通过format()函数实现,这在将数字作为字符串转换和打印时遵循不同格式时非常有用。让我们看看在实践中是如何做到这一点的。
练习 3.7 – 使用format()将数字转换为字符串
这个练习将使用format()将数字转换为漂亮且易于阅读的字符串:
-
通过在
format()函数中指定big.mark参数,将逗号作为千位分隔符添加到123000:>>> format(123000, big.mark = ",") "123,000"注意,现在结果是带有逗号的字符类型字符串。
-
将
123000转换为科学格式:>>> format(123000, scientific = TRUE) "1.23e+05"使用科学格式是表示大数字的简洁方式。我们还可以通过指定显示的数字位数来缩短一个长的浮点数。
-
通过指定
digits参数,仅显示1.256的三个数字:>>> format(1.256, digits = 3) "1.26"结果被四舍五入并转换为字符串,显示指定数量的三个数字。我们也可以使用
round()函数达到相同的四舍五入效果。 -
将
1.256四舍五入到两位小数:>>> round(1.256, digits = 2) 1.26这次,结果仍然是数值型,因为
round()不涉及类型转换。
在下一节中,我们将探讨连接多个字符串。
连接字符串
当有多个字符串时,我们可以使用paste()将它们连接并合并成一个字符串。如果我们想在程序中打印长而定制的消息而不是手动输入,这变得很重要。
paste() 函数接受任意数量的字符串输入作为参数,并将它们组合成一个。让我们看看它是如何工作的。
练习 3.8 – 使用 paste() 组合字符串
在这个练习中,我们将探讨不同的方法来组合多个字符串输入:
-
将
statistics和workshop字符串连接起来生成statistics workshop:>>> paste("statistics", "workshop") "statistics workshop"在这里,我们可以看到两个字符串之间自动添加了一个空格。这是由
sep参数控制的,它指定了字符串之间的填充内容,并假定默认值为空格。我们可以选择通过传递一个分隔字符来覆盖默认行为。 -
移除中间的空格并生成
statisticsworkshop:>>> paste("statistics", "workshop", sep = "") "statisticsworkshop" Let’s see what happens when we connect a single string to a vector of strings. -
将
statistics和workshop向量与course连接:>>> paste(c("statistics", "workshop"), "course") "statistics course" "workshop course"结果显示
course被添加到向量的每个元素中。这是通过底层的回收操作完成的,其中course被回收以便可以与向量中的每个字符串组合。这类似于 Python 中的广播机制。我们也可以通过指定
collapse参数来移除向量结构并将所有元素组合成一个字符串。 -
将之前的输出压缩成一个由
+分隔的单个字符串:>>> paste(c("statistics", "workshop"), "course", collapse = " + ") "statistics course + workshop course"在将组合向量的所有组件插入并按指定参数分隔后,结果是单个折叠的字符串。
到目前为止,我们已经了解了处理字符串数据的基本知识。tidyverse 生态系统提供的 stringr 包提供了许多方便的函数,如果我们想要对字符串有更灵活的控制,这将在本节中介绍。
使用 stringr 处理字符串
stringr 包提供了一套连贯的函数,所有这些函数都以 str_ 开头,旨在使字符串处理尽可能容易。
让我们从 stringr 的基本函数开始,通过复制上一个练习中的相同结果。
stringr 的基础知识
stringr 包中的 str_c() 函数可以像 paste() 一样连接多个字符串,具有类似的功能。让我们看看它的实际应用。
练习 3.9 – 使用 paste() 组合字符串
在这个练习中,我们将使用 str_c() 重复 练习 3.8 中的相同操作:
-
在
statistics和workshop之间添加一个分隔空格来连接:>>> str_c("statistics", "workshop", sep = " ") "statistics workshop"我们可以使用
sep参数来指定字符串之间的分隔符。 -
将
statistics和workshop向量与course结合:>>> str_c(c("statistics", "workshop"), "course", sep = " ") "statistics course" "workshop course"相同的回收行为也出现在这里。
-
将前面的输出压缩成一个由
+分隔的单个字符串:>>> str_c(c("statistics", "workshop"), "course", sep = " ", collapse = " + ") "statistics course + workshop course"
有两个其他常见的 stringr 函数:str_length(),它返回字符串的长度,和 str_sub(),它从字符串中减去部分内容:
-
例如,我们可以得到向量中每个字符串的长度,如下面的代码片段所示。
>>> str_length(c("statistics", "workshop")) 10 8 -
或者,我们可以使用来自基础 R 的
nchar()函数来达到相同的结果,如下所示:>>> nchar(c("statistics", "workshop")) 10 8 -
我们还可以使用
str_sub()通过提供起始和结束索引来提取字符串的一部分:>>> str_sub(c("statistics", "workshop"), start = 1, end = 3) "sta" "wor"
提取字符串的一部分是查找字符串中模式的一种方式。在下一节中,我们将介绍一种比位置索引更高级的字符串匹配方法。
字符串中的模式匹配
在字符串中匹配模式是提取文本数据中的信息的一种常见方式。当找到匹配时,我们可以根据匹配拆分或替换字符串,添加如匹配次数等额外数据,或执行其他基于文本的分析。让我们通过几个练习来熟悉字符串匹配。
练习 3.10 – 在字符串中定位匹配
在这个练习中,我们将介绍三个在字符串中定位匹配时常用的函数,包括使用 str_detect() 检测匹配、使用 str_subset() 选择具有匹配的向量字符串,以及使用 str_count() 在字符串中计算匹配次数:
-
在包含
statistics和workshop的字符串向量中检测stat的出现:>>> str_detect(c("statistics", "workshop"), "stat") TRUE FALSEstr_detect()函数在输入字符串中查找指定的模式,并返回一个与输入向量长度相同的逻辑向量,其中TRUE表示匹配,否则为FALSE。 -
选择包含
stat的字符串子集:>>> str_subset(c("statistics", "workshop"), "stat") "statistics"str_subset()函数一次完成检测和选择。它将仅返回与指定模式匹配的字符串。 -
计算前一个向量中每个字符串中
t的出现次数:>>> str_count(c("statistics", "workshop"), "t") 3 0str_count()函数返回一个与输入向量长度相同的整数向量,显示每个字符串中特定匹配的频率。
接下来,我们将探讨如何根据特定的匹配来拆分字符串。
拆分字符串
根据特定模式拆分字符串可以通过 str_split() 函数实现,该函数假设与之前函数具有相似的命名和参数设置。然后原始字符串可以被分解成更小的部分以支持更精细的分析。让我们看看它是如何使用的。
练习 3.11 – 使用 str_split() 拆分字符串
这个练习将使用 str_split() 根据特定的匹配条件将字符串分解成更小的部分:
-
在
&符号处拆分statistics & machine learning workshop字符串:>>> str_split(c("statistics & machine leaning workshop"), "&") [[1]] [1] "statistics " " machine leaning workshop"结果是一个包含两个元素的向量列表,位于第一个条目中。请注意,结果子字符串中都有空格,这表明使用了精确的模式匹配来拆分字符串。然后我们可以将空格包含在匹配模式中,以移除结果子字符串中的空格。
-
在匹配模式中包含前导和尾随空格:
>>> str_split(c("statistics & machine leaning workshop"), " & ") [[1]] [1] "statistics" "machine leaning workshop"通过这种方式,子字符串中的空格已经被移除。如下面的代码片段所示,由于结果被包裹在一个列表中,我们可以遵循列表索引规则来访问相应的元素。
-
从上一个结果中访问第二个元素:
>>> str_split(c("statistics & machine leaning workshop"), " & ")[[1]][2] "machine leaning workshop" In this example, the first original string is split into two substrings while the second is split into three. Each original string corresponds to an entry in the list and can assume a different number of substrings. The resulting DataFrame will assume the same number of rows as the input vector and the same number of columns as the longest entry in the list.
接下来,我们将查看如何在字符串中替换匹配的模式。
替换字符串
str_replace() 和 str_replace_all() 函数使用 replacement 参数指定的新文本替换匹配项。区别在于 str_replace() 只替换第一个匹配项,而 str_replace_all() 如其名称所示替换所有匹配项。
让我们尝试使用两个函数将 & 符号替换为 and:
>>> str_replace(c("statistics & machine leaning workshop", "stats & ml & workshop"), pattern = "&", replacement = "and")
"statistics and machine leaning workshop" "stats and ml & workshop"
>>> str_replace_all(c("statistics & machine leaning workshop", "stats & ml & workshop"), pattern = "&", replacement = "and")
"statistics and machine leaning workshop" "stats and ml and workshop"
我们可以看到,第二个字符串中的所有 & 符号都被替换为 and。再次强调,替换特定匹配项涉及两个步骤:定位匹配项(如果有的话),然后执行替换。str_replace() 和 str_replace_all() 函数一次完成这两个步骤。
在下一节中,我们将遇到一个需要结合这些 stringr 函数的挑战。
整合起来
通常,一个特定的字符串处理任务会涉及使用多个 stringr 函数。这些函数结合在一起可以对文本数据进行有用的转换。让我们通过一个练习来整合到目前为止我们所学的知识。
练习 3.12 – 使用多个函数转换字符串
在这个练习中,我们将使用不同的基于字符串的函数将 statistics and machine leaning workshop 转换为 stats & ml workshop。首先,我们将 and 替换为 & 符号,分割字符串,并处理各个部分。让我们看看如何实现这一点:
-
创建一个
title变量来存储字符串,并将and替换为&:>>> title = "statistics and machine leaning workshop" >>> title = str_replace(title, pattern = "and", replacement = "&") >>> title "statistics & machine leaning workshop"在这里,我们使用
str_replace()将and替换为&。 -
使用
&将title分割成子字符串:>>> a = str_split(title, " & ") >>> a [[1]] [1] "statistics" "machine leaning workshop"在这里,我们使用
str_split()将原始字符串分割成更小的子字符串。注意,匹配模式中也添加了额外的空格。我们现在将处理这些单个部分。 -
将
statistics转换为stats:>>> b = str_c(str_sub(a[[1]][1], 1, 4), str_sub(a[[1]][1], -1, -1)) >>> b "stats"在这里,我们使用
str_sub()提取了前四个字符,即stat,以及最后一个字符s,然后使用str_c()将它们连接起来。注意-1表示字符串的最后一个位置索引。现在,我们可以开始处理第二部分。
-
使用空格分割
a变量的第二个元素:>>> c = unlist(str_split(a[[1]][2], " ")) >>> c "machine" "leaning" "workshop"在这里,我们使用
str_split()使用空格分割machine leaning workshop字符串,并使用unlist()将结果从列表转换为向量。我们这样做是为了在后续引用中节省一些打字,因为返回的列表中只有一个条目。现在,我们可以通过提取
machine和learning的第一个字符并将它们组合起来形成ml来重复类似的步骤。 -
根据前面的输出形成
ml:>>> d = str_c(str_sub(c[1], 1, 1), str_sub(c[2], 1, 1)) >>> d "ml"现在,我们可以将所有处理过的组件组合成一个字符串。
-
使用前面的输出形成最终的预期字符串:
>>> e = str_c(b, "&", d, c[3], sep = " ") >>> e "stats & ml workshop"
在下一节中,我们将学习更多使用正则表达式的先进模式匹配技术。
正则表达式介绍
一个rebus包。它是stringr的一个好伴侣,提供了便于字符串操作和使构建正则表达式更加容易的实用函数。记住,当你第一次使用它时,通过install.package("rebus")安装此包。
rebus包有一个特殊的操作符%R%,用于连接匹配条件。例如,为了检测一个字符串是否以特定的字符开始,比如s,我们可以指定模式为START %R% "s"并将其传递给str_detect()函数的模式参数,其中START是一个特殊关键字,用于指示字符串的开始。同样,END关键字表示字符串的结束。它们一起在rebus库中被称为锚点。让我们看看以下示例:
>>> str_detect(c("statistics", "machine learning"), pattern = START %R% "s")
TRUE FALSE
我们也可以在控制台中输入START。结果是箭头符号,这正是 vanilla 正则表达式中用来指示字符串开始的字符:
>>> START
<regex> ^
此外,str_view()是另一个有用的函数,它可视化字符串的匹配部分。运行以下命令将弹出一个带有高亮显示匹配部分的 HTML 查看器面板:
>>> str_view(c("statistics", "machine learning"), pattern = START %R% "s")
这在图 3.1中显示:

图 3.1 – 使用 str_view()在查看器面板中可视化匹配结果
让我们通过一个练习来了解 rebus 中各种模式匹配函数的更多内容。
练习 3.13 – 使用 rebus 应用正则表达式
在这个练习中,我们将应用不同的正则表达式来匹配字符串中的预期模式:
-
运行以下命令来创建一个字符串向量。注意,这些字符串设计得简单但足以展示我们将要介绍的匹配函数的目的:
>>> texts = c("stats 101", "machine learning", "R 101 ABC workshop", "101 R workshop") -
搜索以
learning结尾的向量中的字符串:>>> str_subset(texts, pattern = "learning" %R% END) "machine learning"这里,我们在
pattern参数中使用了END关键字来指示字符串应以learning结尾。 -
搜索包含任何字符后跟
101的字符串:>>> str_subset(texts, pattern = ANY_CHAR %R% "101") "stats 101" "R 101 ABC workshop"注意
ANY_CHAR是一个特殊关键字,是一个通配符,表示任何单个字符,在正常正则表达式中对应于点(.),如下面的代码所示:>>> ANY_CHAR <regex> .由于模式表示任何字符后跟
101,因此由于存在101,选出了两个字符串。101 R workshop没有被选中,因为没有字符在101之前。 -
搜索第三个字符为
a的字符串:>>> str_subset(texts, pattern = START %R% ANY_CHAR %R% ANY_CHAR %R% "a") "stats 101"这里,我们通过传递两个通配符关键字在开头来指定第三个字符为
a。 -
搜索以
stats或R开头的字符串:>>> str_subset(texts, pattern = START %R% or("stats", "R")) "stats 101" "R 101 ABC workshop"or()函数在指定多个匹配条件时很有用。 -
搜索包含一个或多个
a或A字符的字符串:>>> str_subset(texts, pattern = one_or_more(char_class("aA"))) "stats 101" "machine learning" "R 101 ABC workshop"在这里使用了两个新函数。
char_class()函数强制匹配输入参数中指定的允许字符之一,而one_or_more()函数表示括号内包含的模式可以重复一次或多次。
接下来,我们将介绍 tidytext 包,它允许我们方便地处理非结构化文本数据和 tidyverse 生态系统。
使用整洁文本进行挖掘
tidytext 包通过遵循整洁数据原则来处理非结构化文本,该原则规定数据应以结构化、矩形形状和类似 tibble 的对象表示。在文本挖掘的情况下,这需要将单个单元格中的文本转换为 DataFrame 中的每行一个标记。
对于一组文本(称为语料库)的另一种常用表示是文档-词矩阵,其中每一行代表一个文档(这可能是一句简短的句子或一篇长篇文章),每一列代表一个术语(整个语料库中唯一的单词,例如)。矩阵中的每个单元格通常包含一个代表性统计量,例如出现频率,以指示术语在文档中出现的次数。
在接下来的几节中,我们将深入了解这两种表示,并探讨如何将文档-词矩阵转换为整洁数据格式以进行文本挖掘。
使用 unnest_tokens() 将文本转换为整洁数据
让我们创建一个稍微不同的虚拟数据集,如下所示:
texts = c("stats 101", "Machine Learning", "R and ML workshop", "R workshop & Statistics with R")
texts_df = tibble(id = 1:length(texts), text = texts)
>>> texts_df
# A tibble: 4 x 2
id text
<int> <chr>
1 1 stats 101
2 2 Machine Learning
3 3 R and ML workshop
4 4 R workshop & Statistics with R
在这个数据集中,texts 列包含任意长度的文本。尽管它存储为 tibble 对象,但它并不非常适合整洁文本分析。例如,texts 列中的每一行都包含多个单词,这使得推导出诸如单词频率之类的统计总结变得具有挑战性。当每一行对应于所有文本的单个单词时,获取这些统计量会容易得多。
注意,在文本挖掘中查看单词级信息是常见的,尽管我们也可以扩展到其他变化,如单词对或甚至句子。用于文本挖掘的分析单位称为 tidytext 包中的 unnest_tokens() 函数。如果你还没有这样做,请记住安装并加载此包。
unnest_tokens() 函数接受两个输入:用于存储结果标记的列,以及将文本分解为标记的列。此外,unnest_tokens() 函数在将数据转换为整洁文本 DataFrame 时还处理其他方面。让我们通过一个练习来了解更多关于这个函数的信息。
练习 3.14 – 使用 unnest_tokens() 构建整洁文本
在这个练习中,我们将使用 unnest_tokens() 函数来构建整洁的文本并提取词频:
-
使用
unnest_tokens()将texts_df转换为整洁文本格式,并将包含标记的列命名为unit_token。将结果存储在tidy_df中:>>> tidy_df <- texts_df %>% unnest_tokens(unit_token, text) >>> tidy_df # A tibble: 13 x 2 id unit_token <int> <chr> 1 stats 2 1 101 3 2 machine 4 2 learning 5 3 r 6 3 and 7 3 ml 8 3 workshop 9 4 r 10 4 workshop 11 4 statistics 12 4 with 13 4 r注意,
unnest_tokens()默认使用单词级别的标记化;因此,unit_token列包含从相应文本中提取的所有单词标记,每个单词占一行。注意,由于unnest_tokens()默认移除所有标点符号并将所有单词转换为小写,&符号已从结果中移除。其余的列,如id,被保留并复制为原始文本字符串中的每个单词。我们还可以通过指定
token和 n 参数,使用双词(bigram)表示将texts_df转换为整洁数据:>>> tidy_df2 <- texts_df %>% unnest_tokens(unit_token, text, token = "ngrams", n = 2) >>> tidy_df2 # A tibble: 9 x 2 id unit_token <int> <chr> 1 1 stats 101 2 2 machine learning 3 3 r and 4 3 and ml 5 3 ml workshop 6 4 r workshop 7 4 workshop statistics 8 4 statistics with 9 4 with r我们可以看到,生成的标记由原始文本中的每个连续单词对组成。同样,在底层执行了标点符号的移除和转换为小写。
我们可以轻松地从可用的整洁数据中推导出单词频率分布。
-
从
tidy_df中推导单词计数:>>> tidy_df %>% count(unit_token, sort = TRUE) # A tibble: 10 x 2 unit_token n <chr> <int> 1 r 3 2 workshop 2 3 101 1 4 and 1 5 learning 1 6 machine 1 7 ml 1 8 statistics 1 9 stats 1 10 with 1在这里,我们使用了
count()函数来计算每个唯一单词的频率。我们还可以通过其他dplyr操作来叠加此分析,例如从单词计数中移除停用词(例如,the和a)。停用词是文本挖掘中不传达额外意义的常见单词,通常从语料库中移除。我们可以使用get_stopwords()函数检查英语停用词列表,如下所示:>>> get_stopwords() # A tibble: 175 x 2 word lexicon <chr> <chr> 1 i snowball 2 me snowball 3 my snowball 4 myself snowball 5 we snowball 6 our snowball 7 ours snowball 8 ourselves snowball 9 you snowball 10 your snowball # … with 165 more rows -
移除停用词后,推导单词频率。将结果存储在
tidy_df2中:>>> tidy_df2 = tidy_df %>% filter(!(unit_token %in% get_stopwords()$word)) %>% count(unit_token, sort = TRUE) >>> tidy_df2 # A tibble: 8 x 2 unit_token n <chr> <int> 1 r 3 2 workshop 2 3 101 1 4 learning 1 5 machine 1 6 ml 1 7 statistics 1 8 stats 1我们可以看到,结果中已经移除了
and和with。
接下来,我们将以文档-词矩阵的形式处理文本,这是在构建使用文本数据的机器学习模型时最常用的格式。
处理文档-词矩阵
我们可以将之前的整洁 DataFrame 转换为文档-词矩阵,也可以从文档-词矩阵转换回来。由于在前一个练习中我们使用了单词(unigram)表示,我们将继续使用单词频率,并查看如何在前面的练习中在整洁数据和文档-词矩阵之间进行转换。
常用的文本挖掘包是 tm。在继续进行以下练习之前,请记住安装并加载此包。
练习 3.15 – 转换为和从文档-词矩阵
在这个练习中,我们将以整洁格式获取单词频率表,然后将其转换为稀疏文档-词矩阵。稀疏矩阵是一种特殊的数据结构,它包含相同数量的信息,但比典型的 DataFrame 占用更少的内存空间。最后,我们将查看如何将文档-词矩阵转换回整洁格式:
-
使用前一个练习中的
tidy_df从每个文档和单词标记推导单词频率计数,并将结果保存到count_df中:>>> count_df = tidy_df %>% group_by(id, unit_token) %>% summarise(count=n()) >>> count_df # A tibble: 12 x 3 # Groups: id [4] id unit_token count <int> <chr> <int> 1 1 101 1 2 1 stats 1 3 2 learning 1 4 2 machine 1 5 3 and 1 6 3 ml 1 7 3 r 1 8 3 workshop 1 9 4 r 2 10 4 statistics 1 11 4 with 1 12 4 workshop 1在这里,第四个文档中
r出现了两次,其他所有单词都只出现一次。我们将将其转换为文档-词矩阵格式。 -
使用
tm包中的cast_dtm()函数将count_df转换为文档-词矩阵,并将结果存储在dtm中:>>> dtm = count_df %>% cast_dtm(id, unit_token, count) >>> dtm <<DocumentTermMatrix (documents: 4, terms: 10)>> Non-/sparse entries: 12/28 Sparsity : 70% Maximal term length: 10 Weighting : term frequency (tf)结果显示,我们总共有四篇文档和 10 个术语。稀疏度高达 70%,因为大多数单词只出现在各自的文档中。此外,表示文档中单词的统计量是词频。
我们还可以通过将其特别转换为普通矩阵来查看整个表格:
>>> as.data.frame(as.matrix(dtm), stringsAsFactors=False) 101 stats learning machine and ml r workshop statistics with 1 1 1 0 0 0 0 0 0 0 0 2 0 0 1 1 0 0 0 0 0 0 3 0 0 0 0 1 1 1 1 0 0 4 0 0 0 0 0 0 2 1 1 1现在,我们有了标准的文档-词矩阵。请注意,我们可以使用其他统计方法,例如
tf-idf,来表示矩阵中的每个单元格,或者甚至使用多个数值的向量来表示文档中的每个单词。后者被称为将dtm转换回整洁格式:>>> tidy_dtm = tidy(dtm) >>> tidy_dtm # A tibble: 12 x 3 document term count <chr> <chr> <dbl> 1 1 101 1 2 1 stats 1 3 2 learning 1 4 2 machine 1 5 3 and 1 6 3 ml 1 7 3 r 1 8 4 r 2 9 3 workshop 1 10 4 workshop 1 11 4 statistics 1 12 4 with 1现在,我们拥有与之前相同的数据整洁格式。
摘要
在本章中,我们讨论了几种中间数据处理技术,从结构化表格数据到非结构化文本数据。首先,我们介绍了如何转换分类和数值变量,包括使用recode()重新编码分类变量,使用case_when()创建新变量,以及使用cut()对数值变量进行分箱。接下来,我们探讨了如何重塑 DataFrame,包括使用spread()将长格式 DataFrame 转换为宽格式,以及使用gather()反向转换。我们还深入探讨了字符串的处理,包括如何创建、转换和格式化字符串数据。
此外,我们还介绍了有关stringr包的一些基本知识,该包提供了许多有用的实用函数,以简化字符串处理任务。常见的函数包括str_c()、str_sub()、str_subset()、str_detect()、str_split()、str_count()和str_replace()。这些函数可以组合起来创建一个强大且易于理解的字符串处理管道。
然后,我们介绍了使用rebus包的正则表达式,该包提供了与stringr配合良好的便利模式匹配功能。其函数和关键字易于阅读,包括START、END、ANY_CHAR、or()、one_or_more()等。
最后,我们介绍了使用tidytext包处理整洁文本数据。将一组文本数据转换为整洁格式,可以轻松利用tidyverse生态系统中的许多实用函数。unnest_tokens()函数通常用于整理原始文本,整洁的输出也可以转换为文档-词矩阵,这是开发机器学习模型的标准数据结构。
文本挖掘是一个很大的主题,我们在这章中只介绍了最基本的内容。希望这里展示的基本内容能够鼓励你进一步探索tidyverse生态系统提供的潜在功能。
在下一章中,我们将转换方向,介绍数据可视化,将我们处理过的数据转换为可视和可操作的见解。
第四章:使用 ggplot2 进行数据可视化
上一章介绍了中级数据处理技术,重点是处理字符串数据。当原始数据经过转换和处理,变成干净和结构化的形状后,我们可以通过在图表中可视化干净数据来将分析提升到下一个层次,这正是我们本章的目标。
到本章结束时,您将能够使用 ggplot2 软件包绘制标准图表,并添加自定义设置以呈现出色的视觉效果。
在本章中,我们将涵盖以下主题:
-
介绍
ggplot2 -
理解图形语法
-
图形中的几何形状
-
控制图形主题
技术要求
要完成本章的练习,您需要拥有以下软件包的最新版本:
-
ggplot2软件包,版本 3.3.6。或者,安装tidyverse软件包并直接加载ggplot2。 -
ggthemes软件包,版本 4.2.4。
在我编写本书时,前述列表中提到的软件包版本都是最新的。
本章中所有代码和数据均可在github.com/PacktPublishing/The-Statistics-and-Machine-Learning-with-R-Workshop/tree/main/Chapter_4找到。
介绍 ggplot2
通过图表传达信息通常比单独的表格更有效、更具视觉吸引力。毕竟,人类在处理视觉信息方面要快得多,比如在图像中识别一辆汽车。在构建 机器学习(ML)模型时,我们通常对训练和测试损失曲线感兴趣,该曲线以折线图的形式表示随着模型训练时间的延长,训练集和测试集损失逐渐减少。观察性能指标有助于我们更好地诊断模型是否 欠拟合 或 过拟合——换句话说,当前模型是否过于简单或过于复杂。请注意,测试集用于近似未来的数据集,最小化测试集错误有助于模型泛化到新的数据集,这种方法被称为 经验风险最小化。欠拟合是指模型在训练集和测试集上都表现不佳,这是由于拟合能力不足造成的,而过拟合则意味着模型在训练集上表现良好,但在测试集上表现不佳,这是由于模型过于复杂造成的。无论是欠拟合还是过拟合,都会导致测试集上的错误频率高,从而降低泛化能力。
良好的可视化技能也是良好沟通者的标志。创建良好的可视化需要仔细设计界面,同时满足关于可实现性的技术限制。当被要求构建机器学习模型时,大部分时间通常花在数据处理、模型开发和微调上,只留下极小的一部分时间来向利益相关者传达建模结果。有效的沟通意味着即使对于该领域外的人来说,机器学习模型虽然是一个黑盒解决方案,但仍然可以透明且充分地向内部用户解释和理解。由ggplot2等提供的有意义的强大可视化,这是tidyverse生态系统中专注于图形的特定包,是有效沟通的绝佳促进者;其输出通常比基础 R 提供的默认绘图选项更具视觉吸引力和吸引力。毕竟,随着你在企业阶梯上的攀升和更多地从观众的角度思考,创建良好的可视化将成为一项基本技能。良好的演示技巧将和(如果不是比)你的技术技能(如模型开发)同样重要?
本节将向您展示如何通过构建简单而强大的图表来达到良好的视觉沟通效果,使用的是ggplot2包。这将有助于揭开使用 R 的现代可视化技术的神秘面纱,并为您准备更高级的可视化技术。我们将从一个简单的散点图示例开始,并使用包含一系列与汽车相关的观察数据的mtcars数据集介绍ggplot2包的基本绘图语法,该数据集在加载ggplot2时自动加载到工作环境中。
构建散点图
散点图是一种二维图表,其中两个变量的值(通常是数值类型)唯一确定图表上的每个点。当我们想要评估两个数值变量之间的关系时,散点图是首选的图表类型。
让我们通过一个练习来绘制使用mtcars数据集的汽车气缸数(cyl变量)和每加仑英里数(mpg变量)之间的关系图。
练习 4.1 – 使用 mtcars 数据集构建散点图
在这个练习中,我们将首先检查mtcars数据集的结构,并使用ggplot2生成一个双变量散点图。按照以下步骤进行:
-
加载并检查
mtcars数据集的结构,如下所示:>>> library(ggplot2) >>> str(mtcars) 'data.frame': 32 obs. of 11 variables: $ mpg : num 21 21 22.8 21.4 18.7 18.1 14.3 24.4 22.8 19.2 ... $ cyl : num 6 6 4 6 8 6 8 4 4 6 ... $ disp: num 160 160 108 258 360 ... $ hp : num 110 110 93 110 175 105 245 62 95 123 ... $ drat: num 3.9 3.9 3.85 3.08 3.15 2.76 3.21 3.69 3.92 3.92 ... $ wt : num 2.62 2.88 2.32 3.21 3.44 ... $ qsec: num 16.5 17 18.6 19.4 17 ... $ vs : num 0 0 1 1 0 1 0 1 1 1 ... $ am : num 1 1 1 0 0 0 0 0 0 0 ... $ gear: num 4 4 4 3 3 3 3 4 4 4 ... $ carb: num 4 4 1 1 2 1 4 2 2 4 ...结果显示,
mtcarsDataFrame 包含 32 行和 11 列,这是一个相对较小且结构化的数据集,易于处理。接下来,我们将绘制cyl和mpg之间的关系图。 -
使用
ggplot()和geom_point()函数根据cyl和mpg变量生成散点图。使用theme层放大标题和两轴上的文本大小:>>> ggplot(mtcars, aes(x=cyl, y=mpg)) + geom_point() + theme(axis.text=element_text(size=18), axis.title=element_text(size=18,face="bold"))如图 4**.1所示,生成的结果包含 32 个点,其位置由
cyl和mpg的组合唯一确定。截图表明,随着cyl的增加,mpg的值呈下降趋势,尽管在cyl的三个组内也存在明显的组内变异:

图 4.1 – cyl 和 mpg 之间的散点图
注意,aes()函数将cyl映射到x轴,将mpg映射到y轴。当映射关系没有明确显示时,我们通常假设第一个参数对应于水平轴,第二个对应于垂直轴。
生成散点图的脚本由两个高级函数组成:ggplot()和geom_point()。ggplot()函数在第一个参数中指定要使用的数据集,在第二个参数中指定分别绘制在两个轴上的变量,使用aes()函数包装(更多内容将在后面介绍)。geom_point()函数强制显示为散点图。这两个函数通过特定的+运算符连接在一起,表示将第二层操作叠加到第一层。
此外,请注意,ggplot()将cyl变量视为数值,如水平轴上的额外标签5和7所示。我们可以通过以下方式验证cyl的独立值:
>>> unique(mtcars$cyl)
6 4 8
显然,我们需要将其视为一个分类变量,以避免不同值之间的不必要插值。这可以通过使用factor()函数包装cyl变量来实现,该函数将输入参数转换为分类输出:
>>> ggplot(mtcars, aes(factor(cyl), mpg)) +
geom_point() +
theme(axis.text=element_text(size=18),
axis.title=element_text(size=18,face="bold"))
结果图示在图 4**.2中。通过显式地将cyl转换为分类变量,水平轴正确地表示了每个唯一cyl值的点分布:

图 4.2 – 将 cyl 转换为分类变量后的散点图
到目前为止,我们已经学习了如何通过转换到所需类型后传入感兴趣的变量来构建散点图。这与其他类型的图表类似,遵循一套标准的语法规则。接下来,我们将探讨这些基本规则以了解它们的共性。
理解图形语法
之前的例子包含了在绘图时需要指定的三个基本层:数据、美学和几何形状。每一层的主要目的如下列出:
-
数据层指定要绘制的数据集。这对应于我们之前指定的
mtcars数据集。 -
美学层指定了与缩放相关的项目,这些项目将变量映射到图表的视觉属性。例如,包括用于x轴和y轴的变量、大小和颜色,以及其他图表美学。这对应于我们之前指定的
cyl和mpg变量。 -
几何层指定了用于数据的视觉元素,例如通过点、线或其他形式呈现数据。我们在前面的例子中设置的
geom_point()命令告诉图表以散点图的形式显示。
其他层,如主题层,也有助于美化图表,我们将在后面介绍。
前面的例子中的geom_point()层还暗示我们可以通过更改下划线后的关键字轻松切换到另一种类型的图表。例如,如以下代码片段所示,我们可以使用geom_boxplot()函数将散点图显示为每个独特的cyl值的箱线图:
>>> ggplot(mtcars, aes(factor(cyl), mpg)) +
geom_boxplot() +
theme(axis.text=element_text(size=18),
axis.title=element_text(size=18,face="bold"))
执行此命令将生成图 4.3所示的输出,该输出将每个不同的cyl值的一组点作为箱线图进行可视化。使用箱线图是检测异常值(如位于第三个箱线图外的两个极端点)的一种极好方式:

图 4.3 – 使用箱线图可视化相同的图表
类似地,我们可以通过调整美学层来改变之前散点图中点的颜色和大小。让我们通过一个练习来看看如何实现这一点。
练习 4.2 – 改变散点图中点的颜色和大小
在这个练习中,我们将使用美学层根据disp和hp变量修改最后散点图中显示的点的颜色和大小。disp变量衡量发动机排量,而hp变量表示总马力。因此,点的颜色和大小将根据disp和hp的不同值而变化。按照以下步骤进行:
-
通过在
aes()函数中将disp传递给color参数来改变散点图中点的颜色。同时,也将图例的size参数放大:>>> ggplot(mtcars, aes(factor(cyl), mpg, color=disp)) + geom_point() + theme(axis.text=element_text(size=18), axis.title=element_text(size=18,face="bold"), legend.text = element_text(size=20))执行此命令将生成图 4.4所示的输出,其中每个点的颜色渐变根据
disp的值而变化:

图 4.4 – 向散点图添加颜色
-
通过在
aes()函数中将hp传递给size参数,如下所示,来改变散点图中点的尺寸:>>> ggplot(mtcars, aes(factor(cyl), mpg, color=disp, size=hp)) + geom_point()执行此命令将生成图 4.5所示的输出,其中每个点的尺寸也根据
hp的值而变化:

图 4.5 – 改变散点图中点的尺寸
虽然现在图表看起来更加丰富,但在向单个图表添加维度时要小心。在我们的当前示例中,单个图表包含四个维度的信息:cyl、mpg、disp 和 hp。人类大脑擅长处理二维或三维视觉,但在面对更高维度的图表时可能会感到困难。展示风格取决于我们想要传达给观众的信息。与其将所有维度混合在一起,不如构建一个只包含两个或三个变量的单独图表进行说明可能更有效。记住——在模型开发中,有效的沟通在于传达给观众的信息质量,而不是视觉输出的丰富性。
以下练习将让我们更详细地查看不同层级的各个组件。
练习 4.3 – 使用平滑曲线拟合构建散点图
在这个练习中,我们将构建一个散点图,并拟合一个穿过点的平滑曲线。添加平滑曲线有助于我们检测点之间的整体模式,这是通过使用 geom_smooth() 函数实现的。按照以下步骤进行:
-
使用
hp和mpg构建散点图,并使用geom_smooth()进行平滑曲线拟合,使用disp进行着色,并通过在geom_point()中设置alpha=0.6来调整点的透明度:>>> ggplot(mtcars, aes(hp, mpg, color=disp)) + geom_point(alpha=0.6) + geom_smooth() + theme(axis.text=element_text(size=18), axis.title=element_text(size=18,face="bold"), legend.text = element_text(size=20))执行前面的命令会生成图 4.6 所示的输出,其中中心蓝色曲线代表最佳拟合点的模型,周围的界限表示不确定性区间。我们将在后面的章节中更详细地讨论模型的概念:

图 4.6 – 在散点图中拟合点之间的平滑曲线
由于图形是基于叠加层概念构建的,我们也可以通过从一些组件开始,将它们存储在变量中,然后向图形变量添加额外的组件来生成一个图。让我们看看以下步骤是如何实现的。
-
使用与之前相同的透明度级别,使用
hp和mpg构建散点图,并将其存储在plt变量中:>>> plt = ggplot(mtcars, aes(hp, mpg)) + geom_point(alpha=0.6) + theme(axis.text=element_text(size=18), axis.title=element_text(size=18,face="bold")) >>> plt如图 4.7 所示,直接打印出
plt会生成一个工作图,这表明一个图也可以作为一个对象存储:

图 4.7 – 使用 hp 和 mpg 生成散点图
-
使用
disp着色点,并像这样向之前的图表添加平滑曲线拟合:>>> plt = plt + geom_point(aes(color=disp)) + geom_smooth() >>> plt执行这些命令将生成与图 4.6 所示相同的图。因此,我们可以构建一个基础图,将其保存在变量中,并通过添加额外的图层规格来调整其视觉属性。
我们还可以通过指定相关参数来对散点图中点的尺寸、形状和颜色进行更精细的控制,所有这些都可以在以下练习中完成。
练习 4.4 – 控制散点图中点的尺寸、形状和颜色
在这个练习中,我们将通过不同的输入参数来控制散点图中点的几个视觉属性。这些控制由 geom_point() 函数提供。按照以下步骤进行:
-
生成
hp和mpg之间的散点图,并使用disp为点着色。将点显示为大小为4的圆圈:>>> ggplot(mtcars, aes(hp, mpg, color=disp)) + geom_point(shape=1, size=4) + theme(axis.text=element_text(size=18), axis.title=element_text(size=18,face="bold"), legend.text = element_text(size=20))执行此命令将生成 图 4.8 中所示的输出,其中我们看到点被放大成不同颜色的圆圈:

图 4.8 – 使用较大尺寸的圆圈作为点的散点图
注意,在 geom_point() 中设置 shape=1 将点显示为圆圈。我们可以通过更改此参数以其他形式展示它们。例如,以下命令将点可视化成较小尺寸的三角形:
>>> ggplot(mtcars, aes(hp, mpg, color=disp)) +
geom_point(shape=2, size=2) +
theme(axis.text=element_text(size=18),
axis.title=element_text(size=18,face="bold"),
legend.text = element_text(size=20))
这在 图 4.9 中显示:

图 4.9 – 在散点图中将点可视化成三角形
接下来,我们将探讨如何通过填充点的内部颜色来使散点图更具视觉吸引力。
-
使用
aes()函数中的cyl(在将其转换为因子类型后)填充之前散点图的颜色,并在geom_point()函数中将shape参数设置为21,size设置为5,透明度(通过alpha)设置为0.6:>>> ggplot(mtcars, aes(wt, mpg, fill = factor(cyl))) + geom_point(shape = 21, size = 5, alpha = 0.6) + theme(axis.text=element_text(size=18), axis.title=element_text(size=18,face="bold"), legend.text = element_text(size=20))观察到 图 4.10 中的输出,现在图表看起来更具视觉吸引力,其中三组点分布在
hp和mpg的不同范围内。一个敏锐的读者可能会想知道为什么我们设置shape=21,而点仍然被可视化成圆圈。这是因为21是一个特殊值,允许填充圆圈的内部颜色,以及它们的轮廓或外部颜色:

图 4.10 – 散点图中点的内部颜色填充
注意,除了在图上可视化点之外,我们还可以将它们作为文本标签来展示,这在特定场景中可能更有信息量。也可能出现多个点重叠的情况,使得难以区分它们。让我们看看如何处理这种情况,并通过以下练习以不同的方式展示点。
练习 4.5 – 散点图中展示点的不同方式
在这个练习中,我们将学习两种不同的方式来展示散点图中的点:显示文本标签和抖动重叠的点。这两种技术都将为我们的绘图工具包增加更多灵活性。按照以下步骤进行:
-
使用
row.names()根据每行的名称可视化品牌名称,并使用geom_text()将它们绘制在hp对mpg的先前散点图上:>>> ggplot(mtcars, aes(hp, mpg)) + geom_text(label=row.names(mtcars)) + theme(axis.text=element_text(size=18), axis.title=element_text(size=18,face="bold"))执行此命令将生成如图 图 4**.11 所示的输出,其中品牌名称取代了点。然而,一些品牌名称彼此重叠,使得难以识别它们的特定文本。让我们看看如何解决这个问题:

图 4.11 – 在散点图中显示品牌名称
-
通过将
position_jitter()函数传递给geom_text()函数的position参数来调整重叠文本:>>> ggplot(mtcars, aes(hp, mpg)) + geom_text(label=row.names(mtcars), fontface = "bold", position=position_jitter(width=20,height=20)) + theme(axis.text=element_text(size=18), axis.title=element_text(size=18,face="bold"))执行此命令将生成如图 图 4**.12 所示的输出,其中我们额外指定了
fontface参数为bold以提高清晰度。通过更改position_jitter()函数的width和height参数并将其传递给geom_text()函数的position参数,我们成功调整了图表中文本的位置,使其现在更易于视觉理解:

图 4.12 – 抖动文本的位置
接下来,我们将探讨如何抖动重叠点。
-
按如下方式生成
cyl因素与mpg的散点图:>>> ggplot(mtcars, aes(factor(cyl), mpg)) + geom_point() + theme(axis.text=element_text(size=18), axis.title=element_text(size=18,face="bold"))执行此命令将生成如图 图 4**.13 所示的输出,其中我们故意使用了
cyl分类型变量来显示多个点在图上重叠:

图 4.13 – 可视化具有重叠点的散点图
让我们调整重叠点的位置,使它们在视觉上可区分,从而给我们一个关于有多少这样的点排列在单个位置上的感觉。请注意,抖动意味着在这种情况下向点添加随机位置调整。
-
使用
geom_jitter()函数对点进行抖动,如下所示:>>> ggplot(mtcars, aes(factor(cyl), mpg)) + geom_jitter() + theme(axis.text=element_text(size=18), axis.title=element_text(size=18,face="bold"))执行此命令将生成如图 图 4**.14 所示的输出,其中
cyl每个类别的点现在彼此分离,而不是排列在同一条线上。添加随机抖动因此有助于通过随机扰动来视觉上分离重叠的点:

图 4.14 – 随机抖动重叠点
接下来,我们将探讨确定图中显示的视觉元素的图形几何形状。
图形中的几何形状
上一节主要介绍了散点图。在本节中,我们将介绍两种额外的常见图表类型:条形图和折线图。我们将讨论构建这些图表的不同方法,重点关注可以用来控制图形特定视觉属性的几何形状。
理解散点图中的几何关系
让我们回顾一下散点图,并放大几何层。几何层决定了图表的实际外观,这是我们视觉交流中的基本层。在撰写本文时,我们有超过 50 种几何形状可供选择,所有这些都以 geom_ 关键字开头。
在决定使用哪种几何形状时,有一些总体指南适用。例如,以下列表包含典型散点图可能适用的几何形状类型:
-
点,将数据可视化表示为点
-
抖动,向散点图添加位置抖动
-
拟合线,在散点图上添加一条线
-
平滑,通过拟合趋势线并添加置信界限来平滑图表,以帮助识别数据中的特定模式
-
计数,在散点图的每个位置计数并显示观测值的数量
每个几何层都与其自己的美学配置相关联,包括强制性和可选设置。例如,geom_point() 函数需要 x 和 y 作为强制参数来唯一定位图表上的点,并允许可选设置,如 alpha 参数来控制透明度级别,以及 color 和 fill 来管理点的着色,以及它们的 shape 和 size 参数,等等。
由于几何层提供层特定控制,我们可以在美学层或几何层中设置一些视觉属性。例如,以下代码生成了与图4.15中显示的相同图表,其中着色可以在基本的 ggplot() 函数或特定于层的 geom_point() 函数中设置:
>>> ggplot(mtcars, aes(hp, mpg, color=factor(cyl))) +
geom_point() +
theme(axis.text=element_text(size=18),
axis.title=element_text(size=18,face="bold"),
legend.text = element_text(size=20))
>>> ggplot(mtcars, aes(hp, mpg)) +
geom_point(aes(col=factor(cyl))) +
theme(axis.text=element_text(size=18),
axis.title=element_text(size=18,face="bold"),
legend.text = element_text(size=20))
这会产生以下图表:

图 4.15 – 使用特定于层的几何控制生成相同的散点图
当我们在图表中显示多个层(不一定是不同类型)时,层特定控制带来的灵活性就显现出来了。在接下来的练习中,我们将看到如何一起使用多个几何层。
练习 4.6 – 使用多个几何层
在这个练习中,我们将在之前的散点图上显示不同 cyl 组的 hp 和 mpg 的平均值。一旦从原始 mtcars 数据集中获得,可以通过叠加另一个几何层,采用相同类型的散点图来添加额外的平均统计信息。按照以下步骤进行:
-
使用
dplyr库计算每个cyl组所有列的平均值,并将结果存储在一个名为tmp的变量中:>>> library(dplyr) >>> tmp = mtcars %>% group_by(factor(cyl)) %>% summarise_all(mean) >>> tmp # A tibble: 3 × 12 `factor(cyl)` mpg cyl disp hp drat wt qsec <fct> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> 1 4 26.7 4 105. 82.6 4.07 2.29 19.1 2 6 19.7 6 183\. 122. 3.59 3.12 18.0 3 8 15.1 8 353\. 209. 3.23 4.00 16.8 # … with 4 more variables: vs <dbl>, am <dbl>, # gear <dbl>, carb <dbl>我们可以看到,使用
summarize_all()函数获取所有列的平均值的摘要统计信息,这是一个将输入函数应用于每个组的所有列的实用函数。在这里,我们传递mean函数来计算列的平均值。结果存储在tmp中的tibble对象包含了cyl三个组中所有变量的平均值。需要注意的是,在添加额外的几何层时,基础美学层期望每个几何层中具有相同的列名。在
ggplot()函数中的基础美学层适用于所有几何层。让我们看看如何添加一个额外的几何层作为散点图来展示不同cyl组中平均hp和mpg值。 -
添加一个额外的散点图层来展示每个
cyl组的平均hp和mpg值作为大正方形:>>> ggplot(mtcars, aes(x=hp, y=mpg, color=factor(cyl))) + geom_point() + geom_point(data=tmp, shape=15, size=6) + theme(axis.text=element_text(size=18), axis.title=element_text(size=18,face="bold"), legend.text = element_text(size=20))执行此命令将生成图 4.16中所示的输出,其中大正方形(通过在第二个
geom_point层中设置shape=15和size=6获得)来源于tmp数据集,这是通过附加几何层中的data参数指定的。注意,平均
hp和mpg值会自动左连接到现有数据集中,这显示了每个cyl组中不同的hp和mpg值。为了确保两个几何层在绘图时相互兼容,我们需要确保所有匹配的坐标(x和y参数)存在于相应的原始数据集中:

图 4.16 – 可视化每个 cyl 组的平均 hp 和 mpg 值
此图由两个几何层组成,其中第一层将每个观测值绘制为小圆圈,第二层将每个cyl组的hp和mpg的平均值绘制为大正方形。添加额外层遵循相同的原理,只要每个层的源数据包含基础美学层中指定的列名。
为了进一步说明多个层需要匹配坐标的需求,让我们在控制台中尝试输入以下命令,其中我们只选择传递给第二个几何层的原始数据中的mpg和disp列。如输出所示,期望有hp列,如果没有它将抛出错误:
>>> ggplot(mtcars, aes(x=hp, y=mpg, color=factor(cyl))) +
geom_point() +
geom_point(data=tmp[,c("mpg","disp")], shape=15, size=6)
Error in FUN(X[[i]], ...) : object 'hp' not found
在下一节中,我们将探讨一种新的绘图类型:条形图,以及与其相关的几何层。
引入条形图
条形图以条形的形式显示分类或连续变量的某些统计信息(如频率或比例)。在多种类型的条形图中,直方图是一种特殊的条形图,它显示了单个连续变量的分箱分布。因此,绘制直方图始终涉及一个连续输入变量,使用 geom_histogram() 函数并仅指定 x 参数来实现。在内部,该函数首先将连续输入变量切割成离散的箱。然后,它使用内部计算的 count 变量来指示每个箱中要传递给 y 参数的观测数。
让我们看看如何在以下练习中构建直方图。
练习 4.7 – 构建直方图
在这个练习中,我们将探讨在显示直方图时应用位置调整的不同方法。按照以下步骤进行:
-
使用
geom_histogram()层构建hp变量的直方图,如下所示:>>> ggplot(mtcars, aes(x=hp)) + geom_histogram() + theme(axis.text=element_text(size=18), axis.title=element_text(size=18,face="bold")) `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.执行此命令将生成 图 4.17 中所示的输出,以及关于分箱的警告信息。这是因为默认的分箱值不适合,因为条形之间存在多个间隙,这使得对连续变量的解释变得困难。我们需要使用
binwidth参数微调每个箱的宽度:

图 4.17 – 为 hp 绘制直方图
-
调整
binwidth参数以使直方图连续并移除警告信息,如下所示:>>> ggplot(mtcars, aes(x=hp)) + geom_histogram(binwidth=40) + theme(axis.text=element_text(size=18), axis.title=element_text(size=18,face="bold"))这将产生以下输出:

图 4.18 – 显示连续直方图
制作看起来连续的直方图取决于数据,并且需要尝试和错误。在这种情况下,设置 binwidth=40 似乎对我们有效。
接下来,我们将通过更改条形的着色将分组引入之前的直方图。
-
根据因子的
cyl使用fill参数以不同颜色填充条形:>>> ggplot(mtcars, aes(x=hp, fill=factor(cyl))) + geom_histogram(binwidth=40) + theme(axis.text=element_text(size=18), axis.title=element_text(size=18,face="bold"), legend.text = element_text(size=20))执行此命令将生成 图 4.19 中所示的输出,其中每个条形代表不同的
cyl组。然而,一个机敏的读者可能会立即发现,对于某些有两种颜色的条形,很难判断它们是重叠的还是堆叠在一起的。确实,直方图的默认设置是position="stack",这意味着条形默认是堆叠的。为了消除这种混淆,我们可以明确地显示条形并排:

图 4.19 – 为直方图的条形着色
-
通过设置
position="dodge"来并排显示条形,如下所示:>>> ggplot(mtcars, aes(x=hp, fill=factor(cyl))) + geom_histogram(binwidth=40, position="dodge") + theme(axis.text=element_text(size=18), axis.title=element_text(size=18,face="bold"), legend.text = element_text(size=20))执行此命令将生成 图 4.20 中所示的输出,其中条形现在并排显示。我们可以进一步调整
binwidth参数以减少条形之间的间隙:

图 4.20 – 并排条形图
最后,我们还可以将统计数据以比例而不是计数的形式显示。
-
通过执行以下代码来显示按
cyl分类的hp的前一个直方图作为比例:>>> ggplot(mtcars, aes(x=hp, fill=factor(cyl))) + geom_histogram(binwidth=40, position="fill") + ylab("Proportion") + theme(axis.text=element_text(size=18), axis.title=element_text(size=18,face="bold"), legend.text = element_text(size=20))执行此命令将生成图 4.21中所示的输出,其中
ylab()函数用于更改y轴的标签。由于每个箱子的比例需要加起来等于1,因此图表包含等高的条形,每个条形包含一个或多个组。对于包含多个组的每个箱子,每种颜色的高度代表落在该特定cyl组内的观测值的比例。这种图表通常在我们只关心每个组的相对百分比而不是绝对计数时使用:

图 4.21 – 以比例显示条形图
如前所述,直方图是一种特殊的条形图。经典的条形图包含x轴上的分类变量,其中每个位置代表落在该特定类别中的观测数的计数。可以使用geom_bar()函数生成条形图,该函数允许与geom_histogram()相同的定位调整。让我们通过以下练习来学习其用法。
练习 4.8 – 构建条形图
在这个练习中,我们将通过cyl和gear来可视化观测值的计数作为条形图。按照以下步骤进行:
-
使用
cyl作为x轴,在堆叠条形图中绘制每个独特的cyl和gear组合的观测值计数。>>> ggplot(mtcars, aes(x=factor(cyl), fill=factor(gear))) + geom_bar(position="stack") + theme(axis.text=element_text(size=18), axis.title=element_text(size=18,face="bold"), legend.text = element_text(size=20))执行此命令将生成图 4.22中所示的输出,其中条形的高度代表特定
cyl和gear组合的观测数计数:

图 4.22 – 按 cyl 和 gear 堆叠的条形图
我们还可以使用各自组中观测值的比例/百分比来表示条形图。
-
将条形图转换为基于百分比的图表,以显示每个组合的分布,如下所示:
>>> ggplot(mtcars, aes(x=factor(cyl), fill=factor(gear))) + geom_bar(position="fill") + theme(axis.text=element_text(size=18), axis.title=element_text(size=18,face="bold"), legend.text = element_text(size=20))执行此命令将生成图 4.23中所示的输出:

图 4.23 – 将条形图可视化成比例
如前所述,我们还可以将条形图从堆叠转换为并排。
-
按照以下方式将之前的信息可视化成并排条形图:
>>> ggplot(mtcars, aes(x=factor(cyl), fill=factor(gear))) + geom_bar(position="dodge") + theme(axis.text=element_text(size=18), axis.title=element_text(size=18,face="bold"), legend.text = element_text(size=20))执行此命令将生成图 4.24中所示的输出:

图 4.24 – 并排条形图
我们还可以自定义条形图,使条形部分重叠。这可以通过使用position_dodge()函数实现,如下所示,其中我们调整width参数以将重叠的条形抖动到一定程度:
>>> ggplot(mtcars, aes(x=factor(cyl), fill=factor(gear))) +
geom_bar(position = position_dodge(width=0.2)) +
theme(axis.text=element_text(size=18),
axis.title=element_text(size=18,face="bold"),
legend.text = element_text(size=20))
执行此命令将生成图 4.25中显示的输出:

图 4.25 – 调整柱状图中重叠的条形
接下来,我们将查看另一种流行的绘图类型:线形图。
介绍线形图
线形图显示一个变量的值随着另一个变量的变化而变化。与散点图一样,线形图可以被认为是通过线连接的散点。它主要用于描述两个变量之间的关系。例如,当两个变量相互正相关时,增加一个变量会导致另一个变量似乎成比例增加。在线形图上可视化这种关系可能会在两个变量之间产生一个正斜率的趋势线。
线形图中最广泛使用的一种类型是时间序列图,其中特定指标(如股价)的值被显示为时间的函数(如每日)。在下面的练习中,我们将使用由 base R 提供的JohnsonJohnson数据集,查看 1960 年至 1981 年间 Johnson & Johnson 的季度收益。我们将探索不同的可视化线形图的方法,以及一些针对时间序列数据的数据处理。
练习 4.9 – 绘制时间序列图
在这个练习中,我们将查看将时间序列数据可视化成线形图。按照以下步骤进行:
-
通过执行以下代码来检查
JohnsonJohnson数据集的结构:>>> str(JohnsonJohnson) Time-Series [1:84] from 1960 to 1981: 0.71 0.63 0.85 0.44 0.61 0.69 0.92 0.55 0.72 0.77 ...输出表明该数据集是一个从
1960到1981的单变量(即单一变量)时间序列。打印其内容(仅显示前五行)也告诉我们,频率是季度性的,使用年-季度作为时间序列中每个数据点的唯一索引:>>> JohnsonJohnson Qtr1 Qtr2 Qtr3 Qtr4 1960 0.71 0.63 0.85 0.44 1961 0.61 0.69 0.92 0.55 1962 0.72 0.77 0.92 0.60 1963 0.83 0.80 1.00 0.77 1964 0.92 1.00 1.24 1.00让我们将它转换为熟悉的 DataFrame 格式,以便于数据操作。
-
将其转换为名为
JohnsonJohnson2的 DataFrame,包含两列:qtr_earning用于存储季度时间序列,date用于存储近似日期:>>> library(zoo) >>> JohnsonJohnson2 = data.frame(qtr_earning=as.matrix(JohnsonJohnson), date=as.Date(as.yearmon(time(JohnsonJohnson)))) >>> head(JohnsonJohnson2, n=3) qtr_earning date 1 0.71 1960-01-01 2 0.63 1960-04-01 3 0.85 1960-07-01date列是通过从JohnsonJohnson时间序列对象中提取time索引得到的,使用as.yearmon()显示为年月格式,然后使用as.Date()转换为日期格式。我们还将添加两个额外的指示列,用于后续的绘图。
-
添加一个
ind指示列,如果日期等于或晚于1975-01-01,则其值为TRUE,否则为FALSE。同时,从date变量中提取季度并存储在qtr变量中:>>> JohnsonJohnson2 = JohnsonJohnson2 %>% mutate(ind = if_else(date >= as.Date("1975-01-01"), TRUE, FALSE), qtr = quarters(date)) >>> head(JohnsonJohnson2, n=3) qtr_earning date ind qtr 1 0.71 1960-01-01 FALSE Q1 2 0.63 1960-04-01 FALSE Q2 3 0.85 1960-07-01 FALSE Q3在此命令中,我们使用了
quarters()函数从一个日期格式化的字段中提取季度。接下来,我们将绘制季度收益作为时间序列。 -
使用线形图将
qtr_earning作为date的函数进行绘图,如下所示:>>> ggplot(JohnsonJohnson2, aes(x=date, y=qtr_earning)) + geom_line() + theme(axis.text=element_text(size=18), axis.title=element_text(size=18,face="bold"))执行此命令将生成如图 图 4**.26 所示的输出,其中我们将
date列指定为 x 轴,将qtr_earning指定为 y 轴,然后是geom_line()层:

图 4.26 – 季度收益的时间序列图
季度收益的线图显示长期上升趋势和短期波动。时间序列预测的主题集中在使用这些结构组件(如趋势和季节性)来预测未来值。
此外,我们还可以对时间序列进行着色编码,以便不同的线段根据另一个分组变量显示不同的颜色。
-
根据列
ind指定线图的颜色,如下所示:>>> ggplot(JohnsonJohnson2, aes(x=date, y=qtr_earning, color=ind)) + geom_line() + theme(axis.text=element_text(size=18), axis.title=element_text(size=18,face="bold"), legend.text = element_text(size=20))执行此命令将生成如图 图 4**.27 所示的输出,其中我们在基础美学层中设置
color=ind以更改颜色。请注意,由于这两个线段实际上是图表上分别绘制的独立时间序列,因此它们是断开的:

图 4.27 – 两种不同颜色的线图
当分组变量中有多个类别时,我们也可以绘制多条线,每条线将假设不同的颜色。
-
分别绘制每个季度的时序图,如下所示:
>>> ggplot(JohnsonJohnson2, aes(x=date, y=qtr_earning, color=qtr)) + geom_line() + theme(axis.text=element_text(size=18), axis.title=element_text(size=18,face="bold"), legend.text = element_text(size=20))执行前面的命令将生成如图 图 4**.28 所示的输出,其中
1980:

图 4.28 – 每个季度的年度时序图
在下一节中,我们将查看主题层,它控制图形的样式元素。
控制图形中的主题
主题层指定了图上所有非数据相关的属性,如背景、图例、轴标签等。适当控制图中的主题可以通过突出关键信息并引导用户注意我们想要传达的信息来帮助视觉沟通。
主题层控制了以下三种类型的视觉元素,如下所示:
-
文本,用于指定轴标签的文本显示(例如,颜色)
-
行,用于指定轴的视觉属性,如颜色和线型
-
矩形,用于控制图形的边框和背景
所有三种类型都使用以 element_ 开头的函数指定,包括例如 element_text() 和 element_line() 的示例。我们将在下一节中介绍这些函数。
调整主题
主题层可以轻松地作为现有图的一个附加层应用。让我们通过一个练习来看看如何实现这一点。
练习 4.10 – 应用主题
在这个练习中,我们将查看如何调整之前时间序列图的与主题相关的元素,包括移动图例和更改轴的属性。按照以下步骤进行:
-
通过叠加一个主题层,并将其
legend.position参数指定为"bottom",在底部显示前一个时间序列图的图例。同时,增大轴和图例中文字的字体大小:>>> ggplot(JohnsonJohnson2, aes(x=date, y=qtr_earning, color=qtr)) + geom_line() + theme(legend.position="bottom", axis.text=element_text(size=18), axis.title=element_text(size=18,face="bold"), legend.text = element_text(size=20))执行此命令将生成如图图 4.29所示的输出,其中图例现在被移动到图的底部:

图 4.29 – 在底部显示图例
我们也可以通过向legend.position参数提供坐标信息来将图例放置在图的任何位置。坐标从左下角开始,值为(0,0),一直延伸到右上角,值为(1,1)。由于图的左上部分看起来比较空旷,我们可能考虑将图例移动到那里以节省一些额外空间。
-
通过提供一对适当的坐标来将图例移动到左上角:
>>> tmp = ggplot(JohnsonJohnson2, aes(x=date, y=qtr_earning, color=qtr)) + geom_line() + theme(legend.position=c(0.1,0.8), axis.text=element_text(size=18), axis.title=element_text(size=18,face="bold"), legend.text = element_text(size=20)) >>> tmp在这里,我们将图例的位置指定为
(0.1, 0.8)。通常,使用坐标系配置适当的位置需要尝试和错误。我们还将结果保存在名为tmp的变量中,稍后将使用它。生成的图如图图 4.30所示:

图 4.30 – 基于坐标的位置调整图例
接下来,我们将调整轴的属性。
-
基于前一个图,使用
element_text()函数在axis.title属性上更改轴标题的颜色为蓝色。同时,使用element_line()函数在axis.line属性上使轴的线条为实线黑色:>>> tmp = tmp + theme( axis.title=element_text(color="blue"), axis.line = element_line(color = "black", linetype = "solid") ) >>> tmp执行此命令将生成如图图 4.31所示的输出,其中我们使用了
element_text()和element_line()函数来调整标题(axis.title)和轴的线条(axis.line)的视觉属性(color和linetype):

图 4.31 – 更改轴的标题和线条
最后,我们还可以更改默认的背景和网格。
-
通过执行以下代码来移除前一个图中默认的网格和背景:
>>> tmp = tmp + theme( panel.grid.major = element_blank(), panel.grid.minor = element_blank(), panel.background = element_blank() ) >>> tmp在这里,我们使用
panel.grid.major和panel.grid.minor来访问网格属性,使用panel.background来访问图的背景属性。element_blank()移除所有现有配置,并指定为这三个属性。结果如图图 4.32所示:

图 4.32 – 移除网格和背景设置
注意,我们还可以将主题层保存到变量中,并将其作为叠加应用到其他图中。我们将整个图或特定的图层配置作为一个变量,这使得将其扩展到多个图变得方便。
除了创建我们自己的主题之外,我们还可以利用 ggplot2 提供的内置主题层。如列表所示,这些内置主题提供了现成的解决方案,以简化绘图:
-
theme_gray(),我们之前使用的默认主题 -
theme_classic(),在科学绘图中最常用的传统主题 -
theme_void(),它移除了所有非数据相关的属性 -
theme_bw(),主要用于配置透明度级别时
例如,我们可以使用 theme_classic() 函数生成与之前相似的图表,如下面的代码片段所示:
>>> ggplot(JohnsonJohnson2, aes(x=date, y=qtr_earning,
color=qtr)) +
geom_line() +
theme_classic() +
theme(axis.text=element_text(size=18),
axis.title=element_text(size=18,face="bold"),
legend.text = element_text(size=20))
执行此命令会生成如图 图 4**.33 所示的输出:

图 4.33 – 使用现成的主题设置
除了内置的主题之外,ggthemes 包还提供了额外的主题,进一步扩展了我们的主题选择。让我们在下一节中探索这个包。
探索 ggthemes
ggthemes 包包含多个预构建的主题。就像使用 dplyr 可以显著加速我们的数据处理任务一样,使用预构建的主题也可以与从头开始开发相比,简化我们的绘图工作。让我们看看这个包中可用的几个主题。
练习 4.11 – 探索主题
在这个练习中,我们将探索 ggthemes 提供的一些额外的现成主题。记住在继续下面的代码示例之前,下载并加载这个包。我们将涵盖两个主题函数。按照以下步骤进行:
-
在上一个图表上应用
theme_fivethirtyeight主题,如下所示:>>> ggplot(JohnsonJohnson2, aes(x=date, y=qtr_earning, color=qtr)) + geom_line() + theme_fivethirtyeight() + theme(axis.text=element_text(size=18), axis.title=element_text(size=18,face="bold"), legend.text = element_text(size=20))执行此命令会生成如图 图 4**.34 所示的输出,其中图例位于底部:

图 4.34 – 应用 theme_fivethirtyeight 主题
-
应用
theme_tufte()主题,如下所示:>>> ggplot(JohnsonJohnson2, aes(x=date, y=qtr_earning, color=qtr)) + geom_line() + theme_tufte() + theme(axis.text=element_text(size=18), axis.title=element_text(size=18,face="bold"), legend.text = element_text(size=20))执行此命令会生成如图 图 4**.35 所示的输出,这是科学论文中常用的绘图类型。请注意,学术论文中的图表建议只显示必要的信息。这意味着背景等额外配置是不被鼓励的。另一方面,现实生活中的图表则更倾向于在实用性和美观性之间保持一个合理的平衡:

图 4.35 – 应用 theme_tufte 主题
在本节中,我们探讨了控制图表中与主题相关的元素,这在我们进行微调和自定义图表时提供了很大的灵活性。
概述
在本章中,我们介绍了基于ggplot2包的基本图形技术。我们首先回顾了基本的散点图,并学习了在图表中开发层面的语法。为了构建、编辑和改进一个图表,我们需要指定三个基本层面:数据、美学和几何。例如,用于构建散点图的geom_point()函数允许我们控制图表上点的尺寸、形状和颜色。我们还可以使用geom_text()函数将它们显示为文本,除了使用点来表示之外。
我们还介绍了由几何层提供的层特定控制,并展示了使用条形图和折线图的示例。条形图可以帮助表示分类变量的频率分布和连续变量的直方图。折线图支持时间序列数据,并且如果绘制得当,可以帮助识别趋势和模式。
最后,我们还介绍了主题层,它允许我们控制图表中所有与数据无关的视觉方面。结合基础 R 的内置主题和ggthemes的现成主题,我们有多种选择,可以加速绘图工作。
在下一章中,我们将介绍探索性数据分析(EDA),这是许多数据分析建模任务中常见且必要的一步。
第五章:探索性数据分析
上一章介绍了使用ggplot2的基本绘图原则,包括各种几何形状和主题层的应用。结果证明,清理和整理原始数据(在第第二章和第三章中介绍)以及数据可视化(在第第四章中介绍)属于典型数据科学项目工作流程的第一阶段——即探索性数据分析(EDA)。我们将通过本章的一些案例研究来介绍这一内容。我们将学习如何应用本书前面介绍过的编码技术,并专注于通过 EDA 的视角分析数据。
在本章结束时,你将了解如何使用数值和图形技术揭示数据结构,发现变量之间的有趣关系,以及识别异常观测值。
在本章中,我们将涵盖以下主题:
-
EDA 基础
-
实践中的 EDA(探索性数据分析)
技术要求
要完成本章的练习,你需要具备以下条件:
-
在撰写本文时,
yfR包的最新版本为 1.0.0 -
在撰写本文时,
corrplot包的最新版本为 0.92
本章的代码和数据可在以下链接找到:github.com/PacktPublishing/The-Statistics-and-Machine-Learning-with-R-Workshop/blob/main/Chapter_5/chapter_5.R。
EDA 基础
当面对以表格(DataFrame)形式存在于 Excel 中的新数据集或数据集时,EDA 帮助我们洞察数据集中变量的潜在模式和异常。这是在构建任何预测模型之前的一个重要步骤。俗话说,垃圾输入,垃圾输出。当用于模型开发输入变量存在问题时,如缺失值或不同尺度,所得到的模型可能会表现不佳、收敛缓慢,甚至在训练阶段出现错误。因此,理解你的数据并确保原材料是正确的,是保证模型后期表现良好的关键步骤。
这就是 EDA 发挥作用的地方。EAD(探索性数据分析)不是一种僵化的统计程序,而是一套探索性分析,它使你能够更好地理解数据中的特征和潜在关系。它作为过渡性分析,指导后续建模,涉及我们之前学到的数据操作和可视化技术。它通过各种形式的视觉辅助工具帮助总结数据的显著特征,促进重要特征的提取。
EDA 有两种主要类型:如均值、中位数、众数和四分位数范围等描述性统计,以及如密度图、直方图、箱线图等图形描述。
一个典型的 EDA 流程包括分析分类和数值变量,包括在单变量分析中独立分析以及在双变量和多变量分析中结合分析。常见做法包括分析一组给定变量的分布,并检查缺失值和异常值。在接下来的几节中,我们将首先分析不同类型的数据,包括分类和数值变量。然后,我们将通过案例研究来应用和巩固前几章中涵盖的技术,使用 dplyr 和 ggplot2。
分析分类数据
在本节中,我们将探讨如何通过图形和数值总结来分析两个分类变量。我们将使用来自漫威漫画宇宙的漫画角色数据集,如果你是漫威超级英雄的粉丝,这个数据集可能对你来说并不陌生。该数据集由 read_csv() 函数发布,该函数来自 readr 包,它是 tidyverse 宇宙的数据加载部分,如下代码片段所示:
>>> library(readr)
>>> df = read_csv("https://raw.githubusercontent.com/fivethirtyeight/data/master/comic-characters/marvel-wikia-data.csv")
>>> head(df,5)
# A tibble: 16,376 × 13
page_id name urlslug ID ALIGN EYE HAIR SEX GSM ALIVE APPEARANCES
<dbl> <chr> <chr> <chr> <chr> <chr> <chr> <chr> <chr> <chr> <dbl>
1 1678 "Spider-Man (Pet… "\\/Sp… Secr… Good… Haze… Brow… Male… NA Livi… 4043
2 7139 "Captain America… "\\/Ca… Publ… Good… Blue… Whit… Male… NA Livi… 3360
3 64786 "Wolverine (Jame… "\\/Wo… Publ… Neut… Blue… Blac… Male… NA Livi… 3061
4 1868 "Iron Man (Antho… "\\/Ir… Publ… Good… Blue… Blac… Male… NA Livi… 2961
5 2460 "Thor (Thor Odin… "\\/Th… No D… Good… Blue… Blon… Male… NA Livi… 2258
6 2458 "Benjamin Grimm … "\\/Be… Publ… Good… Blue… No H… Male… NA Livi… 2255
7 2166 "Reed Richards (… "\\/Re… Publ… Good… Brow… Brow… Male… NA Livi… 2072
8 1833 "Hulk (Robert Br… "\\/Hu… Publ… Good… Brow… Brow… Male… NA Livi… 2017
9 29481 "Scott Summers (… "\\/Sc… Publ… Neut… Brow… Brow… Male… NA Livi… 1955
10 1837 "Jonathan Storm … "\\/Jo… Publ… Good… Blue… Blon… Male… NA Livi… 1934
# … with 16,366 more rows, and 2 more variables: `FIRST APPEARANCE` <chr>, Year <dbl>
打印 DataFrame 显示,该数据集包含 16,376 行和 13 列,包括角色名称、ID 等等。
在下一节中,我们将探讨如何使用计数统计量来总结两个分类变量。
使用计数总结分类变量
在本节中,我们将介绍分析两个分类变量的不同方法,包括使用列联表和条形图。列联表是展示两个分类变量每个唯一组合中观测值总计数的有用方式。让我们通过一个练习来了解如何实现这一点。
练习 5.1 – 总结两个分类变量
在这个练习中,我们将关注两个分类变量:ALIGN(表示角色是好人、中立还是坏人)和 SEX(表示角色的性别)。首先,我们将查看每个变量的唯一值,然后总结组合后的相应总计数:
-
检查
ALIGN和SEX的唯一值:>>> unique(df$ALIGN) "Good Characters" "Neutral Characters" "Bad Characters" NA >>> unique(df$SEX) "Male Characters" "Female Characters" "Genderfluid Characters" "Agender Characters" NA结果显示,这两个变量都包含
NA值。让我们删除ALIGN或SEX中任一包含NA值的观测值。 -
使用
filter语句函数在df中删除ALIGN或SEX中包含NA值的观测值:>>> df = df %>% filter(!is.na(ALIGN), !is.na(SEX))我们可以通过检查结果 DataFrame 的维度和
ALIGN和SEX中NA值的计数来验证是否已成功删除包含NA值的行:>>> dim(df) 12942 13 >>> sum(is.na(df$ALIGN)) 0 >>> sum(is.na(df$SEX)) 0接下来,我们必须创建一个列联表来总结每个唯一值组合的频率。
-
创建
ALIGN和SEX之间的列联表:>>> table(df$ALIGN, df$SEX) Agender Characters Female Characters Genderfluid Characters Male Characters Bad Characters 20 976 0 5338 Good Characters 10 1537 1 2966 Neutral Characters 13 640 1 1440我们可以看到,大多数角色是男性且是坏的。在所有男性角色中,大多数是坏的,而女性角色中好的或中性的角色占主导地位。让我们使用条形图直观地呈现和分析比例。
-
使用
ggplot2在这两个变量之间创建一个条形图:>>> library(ggplot2) >>> ggplot(df, aes(x=SEX, fill=ALIGN)) + geom_bar() + theme(axis.text=element_text(size=18), axis.title=element_text(size=18,face="bold"), legend.position = c(0.2, 0.8), legend.key.size = unit(2, 'cm'), legend.text = element_text(size=20)) Figure 5*.1*. Here, we used the properties in the theme layer to adjust the size of labels on the graph. For example, axis.text and axis.title are used to increase the size of texts and titles along the axes, legend.position is used to move the legend to the upper-left corner, and legend.key.size and legend.text are used to enlarge the overall display of the legend:

图 5.1 – ALIGN 和 SEX 的条形图
由于Agender Characters和Genderfluid Characters的总计数非常有限,我们可以在绘制条形图时移除这两个组合:
>>> df %>%
filter(!(SEX %in% c("Agender Characters", "Genderfluid Characters"))) %>%
ggplot(aes(x=SEX, fill=ALIGN)) +
geom_bar()
运行此命令生成图 5.2:

图 5.2 – 从条形图中移除低计数组合
使用计数在比较不同的组合时可能并不直观。在这种情况下,将计数转换为比例将有助于在相对尺度上呈现信息。
将计数转换为比例
在本节中,我们将回顾一个涵盖列联表中条件比例的练习。与之前的无条件列联表不同,在双向列联表的任一维度上进行条件处理会导致比例分布的不同。
练习 5.2 – 总结两个分类变量
在这个练习中,我们将学习如何使用比例表达之前的列联表,并将其转换为基于指定维度的条件分布:
-
使用比例表达之前的列联表。避免使用科学记数法(例如,e+10)并保留三位小数:
>>> options(scipen=999, digits=3) >>> count_df = table(df$ALIGN, df$SEX) >>> prop.table(count_df) Agender Characters Female Characters Genderfluid Characters Male Characters Bad Characters 0.0015454 0.0754134 0.0000000 0.4124556 Good Characters 0.0007727 0.1187606 0.0000773 0.2291763 Neutral Characters 0.0010045 0.0494514 0.0000773 0.1112656列联表中的值现在表示为比例。由于比例是通过将之前的绝对计数除以总和得到的,我们可以通过求和表中的所有值来验证比例的总和是否等于一:
>>> sum(prop.table(count_df)) 1 -
在对行(此处为
ALIGN变量)进行条件处理后,获取作为比例的列联表:>>> prop.table(count_df, margin=1) Agender Characters Female Characters Genderfluid Characters Male Characters Bad Characters 0.003158 0.154089 0.000000 0.842753 Good Characters 0.002215 0.340496 0.000222 0.657067 Neutral Characters 0.006208 0.305635 0.000478 0.687679我们可以通过计算行的求和来验证条件:
>>> rowSums(prop.table(count_df, margin=1)) Bad Characters Good Characters Neutral Characters 1 1 1在此代码中,设置
margin=1表示行级条件。我们也可以通过设置margin=2进行列级条件练习。 -
在对列(例如,
SEX变量)进行条件处理后,获取作为比例的列联表:>>> prop.table(count_df, margin=2) Agender Characters Female Characters Genderfluid Characters Male Characters Bad Characters 0.465 0.310 0.000 0.548 Good Characters 0.233 0.487 0.500 0.304 Neutral Characters 0.302 0.203 0.500 0.148同样,我们可以通过计算列的求和来验证条件:
>>> colSums(prop.table(count_df, margin=2)) Agender Characters Female Characters Genderfluid Characters Male Characters 1 1 1 1 -
在对
SEX应用相同的过滤条件后,在条形图中绘制无条件的比例。将Y轴的标签改为比例:>>> df %>% filter(!(SEX %in% c("Agender Characters", "Genderfluid Characters"))) %>% ggplot(aes(x=SEX, fill=ALIGN)) + geom_bar(position="fill") + ylab("proportion") + theme(axis.text=element_text(size=18), axis.title=element_text(size=18,face="bold"), legend.key.size = unit(2, 'cm'), legend.text = element_text(size=20))运行此命令生成图 5.3,其中很明显,坏角色主要是男性角色:

图 5.3 – 在条形图中可视化无条件的比例
我们也可以通过在条形图中切换这两个变量从不同的角度获得类似的结果:
>>> df %>%
filter(!(SEX %in% c("Agender Characters", "Genderfluid Characters"))) %>%
ggplot(aes(x=ALIGN, fill=SEX)) +
geom_bar(position="fill") +
ylab("proportion") +
theme(axis.text=element_text(size=18),
axis.title=element_text(size=18,face="bold"),
legend.key.size = unit(2, 'cm'),
legend.text = element_text(size=20))
运行此命令会生成 图 5**.4,其中 ALIGN 被用作 x 轴,而 SEX 被用作分组变量:

图 5.4 – 条形图中切换变量
接下来,我们将探讨使用边缘分布和分面条形图描述一个分类变量。
边缘分布和分面条形图
边缘分布指的是在整合其他变量后的一个变量的分布。这意味着我们感兴趣的是某个特定变量的分布,无论其他变量如何分布。
在我们之前存储在 count_df 中的双向列联表中,我们可以通过对所有可能的 ALIGN 值求和来推导出 SEX 的边缘分布,以频率计数的形式。也就是说,我们可以执行列求和以获得 SEX 的边缘计数,如下面的代码片段所示:
>>> colSums(count_df)
Agender Characters Female Characters Genderfluid Characters Male Characters
43 3153 2 9744
这与直接获取 SEX 中不同类别的计数具有相同的效果:
>>> table(df$SEX)
Agender Characters Female Characters Genderfluid Characters Male Characters
43 3153 2 9744
现在,如果我们想为另一个变量的每个类别获取一个变量的边缘分布怎么办?这可以通过 ggplot2 实现,如下面的代码片段所示:
>>> df %>%
filter(!(SEX %in% c("Agender Characters", "Genderfluid Characters"))) %>%
ggplot(aes(x=SEX)) +
geom_bar() +
facet_wrap(~ALIGN) +
theme(axis.text=element_text(size=15),
axis.title=element_text(size=15,face="bold"),
strip.text.x = element_text(size = 30))
运行此代码生成 图 5**.5,其中包含三个并排的条形图,分别表示坏、好和中性角色。这实际上是重新排列了 图 5**.4 中的堆叠条形图。请注意,可以通过使用 facet_wrap 函数添加分面,其中 ~ALIGN 表示分面将使用 ALIGN 变量执行。请注意,我们使用了 strip.text.x 属性来调整分面网格标签的文本大小:

图 5.5 – 分面条形图
此外,我们还可以通过在将其转换为因子后覆盖 ALIGN 的级别来调整单个条形分面的顺序:
>>> df$ALIGN = factor(df$ALIGN, levels = c("Bad Characters", "Neutral Characters", "Good Characters"))
再次运行相同的分面代码现在将生成 图 5**.6,其中分面的顺序是根据 ALIGN 中的级别确定的:

图 5.6 – 在分面条形图中排列分面的顺序
在下一节中,我们将探讨探索数值变量的不同方法。
分析数值数据
在本节中,我们将探讨使用不同类型的图表来总结 Marvel 数据集中的数值数据。由于数值/连续变量可以假设的值有无限多,因此之前使用的频率表不再适用。相反,我们通常将值分组到预先指定的区间中,这样我们就可以处理范围而不是单个值。
练习 5.3 – 探索数值变量
在这个练习中,我们将使用点图、直方图、密度图和箱线图来描述 Year 变量的数值变量:
-
使用
summary()函数获取Year变量的摘要:>>> summary(df$Year) Min. 1st Qu. Median Mean 3rd Qu. Max. NA's 1939 1973 1989 1984 2001 2013 641 -
生成
Year变量的点状图:>>> ggplot(df, aes(x=Year)) + geom_dotplot(dotsize=0.2) + theme(axis.text=element_text(size=18), axis.title=element_text(size=18,face="bold"))执行此命令生成图 5.7,其中每个点代表在x轴对应位置上的一个观测值。相似观测值随后堆叠在顶部。需要注意的是,当观测值数量变得很大时,使用点图可能不是最佳选择,因为由于
ggplot2的技术限制,y轴变得没有意义:

图 5.7 – 使用点图总结年份变量
-
构建
Year变量的直方图:>>> ggplot(df, aes(x=Year)) + geom_histogram() + theme(axis.text=element_text(size=18), axis.title=element_text(size=18,face="bold"))执行此命令生成图 5.8,其中
Year的每个值被分组到不同的区间,然后计算每个区间内的观测值数量以表示每个区间的宽度。请注意,默认的区间数量是 30,但可以使用bins参数覆盖。因此,直方图展示了底层变量的分布形状。我们还可以将其转换为密度图以平滑区间之间的步骤:

图 5.8 – 使用直方图总结年份变量
-
构建
Year变量的密度图:>>> ggplot(df, aes(x=Year)) + geom_density() + theme(axis.text=element_text(size=18), axis.title=element_text(size=18,face="bold"))执行此命令生成图 5.9,其中分布以平滑的线条表示。请注意,当数据集中有大量观测值时,建议使用密度图:

图 5.9 – 使用密度图总结年份变量
-
构建
Year变量的箱线图:>>> ggplot(df, aes(x=Year)) + geom_boxplot() + theme(axis.text=element_text(size=18), axis.title=element_text(size=18,face="bold"))

图 5.10 – 使用箱线图总结年份变量
执行此命令生成图 5.10,其中中间的箱体代表大多数观测值(第 25 到第 75 百分位数),箱体中的中线表示中位数(第 50 百分位数),而延伸的胡须包括几乎所有“正常”的观测值。在这个例子中,异常观测值(没有异常值)将表示为胡须范围之外的点。
我们也可以通过SEX添加一个分面层,并观察不同性别下箱线图的变化。
-
在之前的箱线图中使用
SEX变量添加一个分面层:>>> ggplot(df, aes(x=Year)) + geom_boxplot() + facet_wrap(~SEX) + theme(axis.text=element_text(size=18), axis.title=element_text(size=18,face="bold"), strip.text.x = element_text(size = 30))执行此命令生成图 5.11。如图所示,大多数女性角色比许多男性角色晚出现,并且近年来女性角色比男性角色多:

图 5.11 – 根据性别分面箱线图
在下一节中,我们将探讨如何可视化高维数据。
高维可视化
之前的例子使用了分面来展示数值变量在每个分类变量的唯一值中的分布。当存在多个分类变量时,我们可以应用相同的技巧并相应地扩展分面。这使我们能够在包含多个分类变量的更高维度中可视化相同的数值变量。让我们通过一个可视化Year按ALIGN和SEX分布的练习来了解。
练习 5.4 – 可视化Year按ALIGN和SEX
在这个练习中,我们将使用ggplot2中的facet_grid()函数,通过密度图和直方图来可视化Year在每个唯一的ALIGN和SEX组合中的分布:
-
在对
SEX应用相同的过滤条件后,构建Year按ALIGN和SEX的密度图:>>> df %>% filter(!(SEX %in% c("Agender Characters", "Genderfluid Characters"))) %>% ggplot(aes(x=Year)) + geom_density() + facet_grid(ALIGN ~ SEX, labeller = label_both) + facet_grid(ALIGN ~ SEX, labeller = label_both) + theme(axis.text=element_text(size=18), axis.title=element_text(size=18,face="bold"), strip.text.x = element_text(size = 30), strip.text.y = element_text(size = 12))执行此命令生成图 5.12,我们在这里使用了
facet_grid()函数创建了六个直方图,列由第一个参数ALIGN分割,行由第二个参数SEX分割。结果显示,对于所有不同的ALIGN和SEX组合,趋势都在上升(制作了更多的电影)。然而,由于Y轴只显示相对密度,我们需要切换到直方图来评估发生的绝对频率。注意,我们使用了strip.text.y属性来调整分面网格标签沿Y轴的文本大小:

图 5.12 – Year按ALIGN和SEX的密度图
-
使用直方图构建相同的图表:
>>> df %>% filter(!(SEX %in% c("Agender Characters", "Genderfluid Characters"))) %>% ggplot(aes(x=Year)) + geom_histogram() + facet_grid(ALIGN ~ SEX, labeller = label_both) + theme(axis.text=element_text(size=18), axis.title=element_text(size=18,face="bold"), strip.text.x = element_text(size = 30), strip.text.y = element_text(size = 12))执行此命令生成图 5.13,我们可以看到近年来优秀男女角色的数量稳步上升:

图 5.13 – Year按ALIGN和SEX的直方图
在下一节中,我们将尝试不同的方法来测量数值变量的中心集中度。
测量中心集中度
测量数值变量的集中趋势或中心集中度有不同方法。根据上下文和目的,中心测度通常用来表示一个数值变量的典型观测值。
最流行的中心测度是均值,它是通过计算数字列表的平均值来得到的。换句话说,我们可以通过将所有观测值相加然后除以观测值的数量来获得均值。这可以通过在 R 中使用mean()函数实现。
另一个中心测度是中位数,它是将数字列表从小到大排序后的中间值。这可以通过在 R 中使用median()函数实现。
第三种中心度量是众数,它代表数字列表中最常见的观测值。由于没有内置的函数来计算众数,我们必须编写一个自定义函数,根据出现次数使用table()函数来获取最频繁的观测值。
在决定中心度量之前,观察分布的形状是很重要的。首先,请注意,平均值通常会被拉向偏斜分布的长尾,这是一个从数字列表(如之前的密度图)推断出的连续分布。换句话说,平均值对观测中的极端值很敏感。另一方面,中位数不会受到这种敏感性的影响,因为它只是将有序观测值分成两半的度量。因此,当处理偏斜的连续分布时,中位数是一个更好、更合理的中心度量候选者,除非对极端值(通常被视为异常值)进行了额外的处理。
让我们通过一个练习来看看如何获得三个中心度量。
练习 5.5 – 计算中心度量
在这个练习中,我们将计算APPEARANCES的平均值、中位数和众数,它表示每个角色的出现次数:
-
计算
APPEARANCES的平均值:>>> mean(df$APPEARANCES) NANA结果表明,APPEARANCES的观测值中存在NA值。为了验证这一点,我们可以查看这个连续变量的摘要:>>> summary(df$APPEARANCES) Min. 1st Qu. Median Mean 3rd Qu. Max. NA's 1 1 3 20 9 4043 749的确,存在相当多的
NA值。为了在移除这些NA观测值后计算平均值,我们可以在mean()函数中启用na.rm参数:>>> mean(df$APPEARANCES, na.rm = TRUE) 19.8 -
计算
APPEARANCES的平均值:>>> median(df$APPEARANCES, na.rm = TRUE) 3当平均值和中位数差异很大时,这是一个明显的迹象,表明我们正在处理一个偏斜分布。在这种情况下,
APPEARANCES变量非常偏斜,中位数角色出现三次,最受欢迎的角色出现高达 4,043 次。 -
计算
APPEARANCES的众数:>>> mode <- function(x){ ux <- unique(x) ux[which.max(tabulate(match(x, ux)))] } >>> mode(df$APPEARANCES) 1在这里,我们创建了一个名为
mode()的自定义函数来计算数值变量的众数,其中我们首先使用unique()函数提取一个唯一值的列表,然后使用tabulate()和match()函数计算每个唯一值出现的次数,最后使用which.max()函数获取最大值的索引。结果显示,大多数角色在整个漫威漫画的历史中只出现了一次。现在,让我们详细分析通过
ALIGN的平均值和众数。 -
通过每个
ALIGN级别计算APPEARANCES的平均值和众数:>>> df %>% group_by(ALIGN) %>% summarise(mean_appear = mean(APPEARANCES, na.rm=TRUE), median_appear = median(APPEARANCES, na.rm=TRUE)) ALIGN mean_appear median_appear <fct> <dbl> <dbl> 1 Bad Characters 8.64 3 2 Neutral Characters 20.3 3 3 Good Characters 35.6 5结果显示,好人角色比坏人角色出现得更频繁。
接下来,我们将探讨如何测量连续变量的变异性。
测量变异性
与集中趋势一样,可以使用多个指标来衡量连续变量的变异性或分散性。其中一些对异常值敏感,如方差和标准差,而其他对异常值稳健,如四分位数间距(IQR)。让我们通过一个练习来了解如何计算这些指标。
注意,在箱线图中使用的是稳健的度量,如中位数和 IQR,尽管与给定变量的完整密度相比,隐藏了更多细节。
练习 5.6 - 计算连续变量的变异性
在这个练习中,我们将手动和通过内置函数计算不同的变异度指标。我们将从方差开始,它是通过计算每个原始值与平均值之间的平均平方差来计算的。请注意,这就是总体方差是如何计算的。为了计算样本方差,我们需要调整平均操作,即在方差计算中使用的总观测数减去 1。
此外,方差是原始单位的平方版本,因此不易解释。为了在相同的原始尺度上衡量数据的变异性,我们可以使用标准差,它是通过对方差取平方根来计算的。让我们看看如何在实践中实现这一点:
-
计算移除
NA值后的APPEARANCES的总体方差。保留两位小数:>>> tmp = df$APPEARANCES[!is.na(df$APPEARANCES)] >>> pop_var = sum((tmp - mean(tmp))²)/length(tmp) >>> formatC(pop_var, digits = 2, format = "f") "11534.53"在这里,我们首先从
APPEARANCES中移除NA值,并将结果保存在tmp中。接下来,我们从tmp的每个原始值中减去tmp的平均值,将结果平方,求和所有值,然后除以tmp中的观测数。这本质上遵循方差的定义,即衡量每个观测值相对于中心趋势的平均变异性——换句话说,就是平均值。我们也可以计算样本方差。
-
计算
APPEARANCES的样本方差:>>> sample_var = sum((tmp - mean(tmp))²)/(length(tmp)-1) >>> formatC(sample_var, digits = 2, format = "f") "11535.48"结果现在与总体方差略有不同。请注意,为了计算样本均值,我们在分母中简单地使用一个更少的观测值。这种调整是必要的,尤其是在我们处理有限的样本数据时,尽管随着样本量的增加,差异变得很小。
我们也可以通过调用
var()函数来计算样本方差。 -
使用
var()计算样本方差:>>> formatC(var(tmp), digits = 2, format = "f") "11535.48"结果与我们的先前手动计算的样本方差一致。
为了获得与原始观测值相同的单位的变异性度量,我们可以计算标准差。这可以通过使用
sd()函数来实现。 -
使用
sd()计算标准差:>>> sd(tmp) 107.4变异性的另一个度量是四分位数间距(IQR),它是第三四分位数和第一四分位数之间的差值,并量化了大多数值的范围。
-
使用
IQR()计算四分位数间距:>>> IQR(tmp) 8我们也可以通过调用
summary()函数来验证结果,该函数返回不同的四分位数值:>>> summary(tmp) Min. 1st Qu. Median Mean 3rd Qu. Max. 1.0 1.0 3.0 19.8 9.0 4043.0如前所述,方差和标准差等度量对数据中的极端值敏感,而四分位数范围(IQR)是对异常值稳健的度量。我们可以评估从
tmp中移除最大值后这些度量的变化。 -
从
tmp中移除最大值后的标准差和四分位数范围(IQR):>>> tmp2 = tmp[tmp != max(tmp)] >>> sd(tmp2) 101.04 >>> IQR(tmp2) 8结果显示,移除最大值后 IQR 保持不变,因此比标准差更稳健的度量。
我们还可以通过另一个分类变量的不同级别来计算这些度量。
-
计算每个
ALIGN级别的APPEARANCES的标准差、四分位数范围(IQR)和计数:>>> df %>% group_by(ALIGN) %>% summarise(sd_appear = sd(APPEARANCES, na.rm=TRUE), IQR_appear = IQR(APPEARANCES, na.rm=TRUE), count = n()) ALIGN sd_appear IQR_appear count <fct> <dbl> <dbl> <int> 1 Bad Characters 26.4 5 6334 2 Neutral Characters 112. 8 2094 3 Good Characters 161. 14 4514
接下来,我们将更深入地探讨连续变量分布中的偏度。
处理偏斜分布
除了平均值和标准差之外,我们还可以使用模态和偏度来描述连续变量的分布。模态指的是连续分布中存在的峰的数量。例如,单峰分布,我们迄今为止最常见的形式是钟形曲线,整个分布中只有一个峰值。当有两个峰时,它可以变成双峰分布;当有三个或更多峰时,它变成多峰分布。如果没有可辨别的模态,并且分布在整个支持区域(连续变量的范围)上看起来平坦,则称为均匀分布。图 5.14总结了不同模态的分布:

图 5.14 – 分布中不同类型的模态
另一方面,连续变量可能向左或向右偏斜,或者围绕中心趋势对称。右偏斜分布在其分布的右尾包含更多的极端值,而左偏斜分布在其左侧有长尾。图 5.15说明了分布中不同类型的偏度:

图 5.15 – 分布中的不同类型偏度
分布也可以将它的偏斜归因于连续变量中的异常值。当数据中有多个异常值时,敏感的度量如平均值和方差将变得扭曲,导致分布向异常值偏移。让我们通过一个练习来了解如何处理分布中的偏斜和异常值。
练习 5.7 – 处理偏斜和异常值
在这个练习中,我们将探讨如何处理包含许多极端值,特别是数据中的异常值的偏斜分布:
-
通过
ALIGN可视化APPEARANCES的密度图,对于自 2000 年以来的观测值。设置透明度为0.2:>>> tmp = df %>% filter(Year >= 2000) >>> ggplot(tmp, aes(x=APPEARANCES, fill=ALIGN)) + geom_density(alpha=0.2) + theme(axis.text=element_text(size=18), axis.title=element_text(size=18,face="bold"), legend.position = c(0.8, 0.8), legend.key.size = unit(2, 'cm'), legend.text = element_text(size=20))执行此命令生成图 5.16,其中所有三个分布都相当右偏斜,这是数据中存在许多异常值的明显迹象:

图 5.16 – 由 ALIGN 生成的APPEARANCES密度图
-
移除
APPEARANCES值超过 90 百分位的观测值并生成相同的图表:>>> tmp = tmp %>% filter(APPEARANCES <= quantile(APPEARANCES, 0.9, na.rm=TRUE)) >>> ggplot(tmp, aes(x=log(APPEARANCES), fill=ALIGN)) + geom_density(alpha=0.2) + theme(axis.text=element_text(size=18), axis.title=element_text(size=18,face="bold"), legend.position = c(0.8, 0.8), legend.key.size = unit(2, 'cm'), legend.text = element_text(size=20))执行此命令生成图 5.17,其中所有三个分布都比之前更少地右偏斜。移除异常值是处理极端值的一种方法,尽管移除的观测值中的信息已经丢失。为了控制异常值的影响并同时保留它们的存在,我们可以使用
log()函数对连续变量进行变换,将其转换为对数尺度。让我们看看这在实践中是如何工作的:

图 5.17 – 移除异常值后,由 ALIGN 生成的APPEARANCES密度图
-
对
APPEARANCES应用对数变换并重新生成相同的图表:>>> ggplot(tmp, aes(x=log(APPEARANCES), fill=ALIGN)) + geom_density(alpha=0.2) + theme(axis.text=element_text(size=18), axis.title=element_text(size=18,face="bold"), legend.position = c(0.8, 0.8), legend.key.size = unit(2, 'cm'), legend.text = element_text(size=20))执行此命令生成图 5.18,其中三个密度图呈现出双峰分布,而不是之前的右偏斜。因此,使用对数函数对连续变量进行变换可以将原始值转换为更受控制的尺度:

图 5.18 – 应用对数变换后,由 ALIGN 生成的APPEARANCES密度图
在下一节中,我们将通过一个案例研究来提高我们在对新的数据集进行 EDA 时的技能。
实践中的 EDA
在本节中,我们将分析一个由 2021 年五大公司股票价格组成的数据库。首先,我们将探讨如何下载和处理这些股票指数,然后进行单变量分析和基于相关性的双变量分析。
获取股票价格数据
要获取特定股票代码的每日股票价格,我们可以使用yfR包从 Yahoo! Finance 下载数据,这是一个包含大量市场和资产金融数据的庞大仓库,在学术界和工业界都得到了广泛的应用。以下练习说明了如何使用yfR下载股票数据。
练习 5.8 – 下载股票价格
在这个练习中,我们将探讨如何指定不同的参数,以便我们可以从 Yahoo! Finance 下载股票价格,包括股票代码和日期范围:
-
安装并加载
yfR包:>>> install.packages("yfR") >>> library(yfR)注意,在
install.packages()函数中,我们需要将包名用一对双引号括起来。 -
指定起始日期和结束日期参数,以及股票代码,以确保它们覆盖 Facebook(现在为
META)、Netflix(NFLX)、Google(GOOG)、Amazon(AMZN)和 Microsoft(MSFT):>>> first_date = as.Date("2021-01-01") >>> last_date = as.Date("2022-01-01") >>> my_ticker <- c('META', 'NFLX', 'GOOG', 'AMZN', 'MSFT')这里,起始日期和结束日期格式化为
Date类型,股票名称连接成一个向量。 -
使用
yf_get()函数下载股票价格并将结果存储在df中:>>> df <- yf_get(tickers = my_ticker, first_date = first_date, last_date = last_date)执行此命令会生成以下消息,显示已成功下载所有五只股票的数据。由于一年中有 252 个交易日,每只股票在 2021 年有 252 行数据:
── Running yfR for 5 stocks | 2021-01-01 --> 2022-01-01 (365 days) ── ℹ Downloading data for benchmark ticker ^GSPC ℹ (1/5) Fetching data for AMZN - found cache file (2021-01-04 --> 2021-12-31) - got 252 valid rows (2021-01-04 --> 2021-12-31) - got 100% of valid prices -- Got it! ℹ (2/5) Fetching data for GOOG - found cache file (2021-01-04 --> 2021-12-31) - got 252 valid rows (2021-01-04 --> 2021-12-31) - got 100% of valid prices -- Good stuff! ℹ (3/5) Fetching data for META ! - not cached - cache saved successfully - got 252 valid rows (2021-01-04 --> 2021-12-31) - got 100% of valid prices -- Mais contente que cusco de cozinheira! ℹ (4/5) Fetching data for MSFT - found cache file (2021-01-04 --> 2021-12-31) - got 252 valid rows (2021-01-04 --> 2021-12-31) - got 100% of valid prices -- All OK! ℹ (5/5) Fetching data for NFLX - found cache file (2021-01-04 --> 2021-12-31) - got 252 valid rows (2021-01-04 --> 2021-12-31) - got 100% of valid prices -- Youre doing good! ℹ Binding price data ── Diagnostics ─────────────────────────────────────── Returned dataframe with 1260 rows -- Time for some tea? ℹ Using 156.6 kB at /var/folders/zf/d5cczq0571n0_x7_7rdn0r640000gn/T//Rtmp7hl9eR/yf_cache for 1 cache files ℹ Out of 5 requested tickers, you got 5 (100%)让我们检查数据集的结构:
>>> str(df) tibble [1,260 × 11] (S3: tbl_df/tbl/data.frame) $ ticker : chr [1:1260] "AMZN" "AMZN" "AMZN" "AMZN" ... $ ref_date : Date[1:1260], format: "2021-01-04" ... $ price_open : num [1:1260] 164 158 157 158 159 ... $ price_high : num [1:1260] 164 161 160 160 160 ... $ price_low : num [1:1260] 157 158 157 158 157 ... $ price_close : num [1:1260] 159 161 157 158 159 ... $ volume : num [1:1260] 88228000 53110000 87896000 70290000 70754000 ... $ price_adjusted : num [1:1260] 159 161 157 158 159 ... $ ret_adjusted_prices : num [1:1260] NA 0.01 -0.0249 0.00758 0.0065 ... $ ret_closing_prices : num [1:1260] NA 0.01 -0.0249 0.00758 0.0065 ... $ cumret_adjusted_prices: num [1:1260] 1 1.01 0.985 0.992 0.999 ... - attr(*, "df_control")= tibble [5 × 5] (S3: tbl_df/tbl/data.frame) ..$ ticker : chr [1:5] "AMZN" "GOOG" "META" "MSFT" ... ..$ dl_status : chr [1:5] „OK" „OK" „OK" „OK" ... ..$ n_rows : int [1:5] 252 252 252 252 252 ..$ perc_benchmark_dates: num [1:5] 1 1 1 1 1 ..$ threshold_decision : chr [1:5] "KEEP" "KEEP" "KEEP" "KEEP" ...下载的数据包括每日开盘价、收盘价、最高价和最低价等信息。
在以下章节中,我们将使用调整后的价格字段 price_adjusted,该字段已调整公司事件,如拆股、股息等。通常,我们在分析股票时使用它,因为它代表了股东的实际财务表现。
单变量分析个别股票价格
在本节中,我们将基于股票价格进行图形分析。由于股票价格是数值型的时间序列数据,我们将使用直方图、密度图和箱线图等图表进行可视化。
练习 5.9 – 下载股票价格
在本练习中,我们将从五只股票的时间序列图开始,然后生成适合连续变量的其他类型图表:
-
为五只股票生成时间序列图:
>>> ggplot(df, aes(x = ref_date, y = price_adjusted, color = ticker)) + geom_line() + theme(axis.text=element_text(size=18), axis.title=element_text(size=18,face="bold"), legend.text = element_text(size=20))执行此命令生成 图 5.19,其中 Netflix 在股票价值方面领先。然而,它也遭受了巨大的波动,尤其是在 2021 年 11 月左右:

图 5.19 – 五只股票的时间序列图
-
为五只股票中的每一只生成一个直方图,每个直方图有 100 个区间:
>>> ggplot(df, aes(x=price_adjusted, fill=ticker)) + geom_histogram(bins=100) + theme(axis.text=element_text(size=18), axis.title=element_text(size=18,face="bold"), legend.text = element_text(size=20))执行此命令生成 图 5.20,显示 Netflix 在股票价值方面具有最大的平均值和方差。Google 和 Amazon 似乎具有相似的分布,Facebook 和 Microsoft 也是如此:

图 5.20 – 五只股票的直方图
-
为五只股票中的每一只生成一个密度图。将透明度设置为
0.2:>>> ggplot(df, aes(x=price_adjusted, fill=ticker)) + geom_density(alpha=0.2) + theme(axis.text=element_text(size=18), axis.title=element_text(size=18,face="bold"), legend.text = element_text(size=20))执行此命令生成 图 5.21,与直方图相比,这些图现在在视觉上更清晰:

图 5.21 – 五只股票的密度图
-
为五只股票中的每一只生成一个箱线图:
>>> ggplot(df, aes(ticker, price_adjusted, fill=ticker)) + geom_boxplot() + theme(axis.text=element_text(size=18), axis.title=element_text(size=18,face="bold"), legend.text = element_text(size=20))执行此命令生成 图 5.22。箱线图擅长显示每只股票的中心趋势和变异。例如,Netflix 在所有五只股票中具有最大的平均值和方差:

图 5.22 – 五只股票的箱线图
-
获取每只股票的平均值、标准差、四分位数范围和计数:
>>> df %>% group_by(ticker) %>% summarise(mean = mean(price_adjusted, na.rm=TRUE), sd = sd(price_adjusted, na.rm=TRUE), IQR = IQR(price_adjusted, na.rm=TRUE), count = n()) # A tibble: 5 × 5 ticker mean sd IQR count <chr> <dbl> <dbl> <dbl> <int> 1 AMZN 167. 8.00 10.7 252 2 GOOG 126\. 18.4 31.1 252 3 META 321\. 34.9 44.2 252 4 MSFT 273\. 37.2 58.5 252 5 NFLX 558\. 56.0 87.5 252
在下一节中,我们将查看每对股票之间的成对相关性。
相关性分析
相关性衡量两个变量之间协变的强度。有几种方法可以计算相关性的具体值,其中皮尔逊相关是最广泛使用的。皮尔逊相关是一个介于 -1 到 1 之间的值,其中 1 表示两个完全且正相关的变量,-1 表示完美的负相关性。完美的相关性意味着一个变量的值的变化总是与另一个变量的值的变化成比例。例如,当 y = 2x 时,变量 x 和 y 之间的相关性为 1,因为 y 总是正比于 x 而变化。
我们不必手动计算所有变量之间的成对相关性,可以使用 corrplot 包自动计算和可视化成对相关性。让我们通过一个练习来看看如何实现这一点。
练习 5.10 – 下载股票价格
在这个练习中,我们首先将之前的 DataFrame 从长格式转换为宽格式,以便每个股票都有一个单独的列,表示不同日期/行之间的调整价格。然后,将使用宽格式数据集生成成对相关性图:
-
使用
tidyr包中的spread()函数将之前的数据集转换为宽格式,并将结果保存在wide_df中:>>> library(tidyr) >>> wide_df <- df %>% select(ref_date, ticker, price_adjusted) %>% spread(ticker, price_adjusted)在这里,我们首先选择三个变量,其中
ref_date作为行级日期索引,ticker的唯一值作为要分散在 DataFrame 中的列,price_adjusted用于填充宽 DataFrame 的单元格。有了这些,我们可以检查新数据集的前几行:>>> head(wide_df) # A tibble: 6 × 6 ref_date AMZN GOOG META MSFT NFLX <date> <dbl> <dbl> <dbl> <dbl> <dbl> 1 2021-01-04 159. 86.4 269. 214. 523. 2 2021-01-05 161. 87.0 271. 215. 521. 3 2021-01-06 157. 86.8 263. 209. 500. 4 2021-01-07 158. 89.4 269. 215. 509. 5 2021-01-08 159. 90.4 268. 216. 510. 6 2021-01-11 156. 88.3 257. 214. 499.现在,DataFrame 已经从长格式转换为宽格式,这将有助于稍后创建相关性图。
-
使用
corrplot包中的corrplot()函数生成相关性图(如果尚未安装,请先安装):>>> install.packages("corrplot") >>> library(corrplot) >>> cor_table = cor(wide_df[,-1]) >>> corrplot(cor_table, method = "circle")执行这些命令会生成 图 5**.23。每个圆圈代表对应股票之间相关性的强度,其中更大、更暗的圆圈表示更强的相关性:

图 5.23 – 每对股票之间的相关性图
注意,相关性图依赖于 cor_table 变量,该变量存储成对相关性作为表格,如下所示:
>>> cor_table
AMZN GOOG META MSFT NFLX
AMZN 1.000 0.655 0.655 0.635 0.402
GOOG 0.655 1.000 0.855 0.945 0.633
META 0.655 0.855 1.000 0.692 0.267
MSFT 0.635 0.945 0.692 1.000 0.782
NFLX 0.402 0.633 0.267 0.782 1.000
变量之间的高度相关性可能好也可能不好。当需要预测的因变量(也称为目标结果)与自变量(也称为预测变量、特征或协变量)高度相关时,我们更愿意将这个特征包含在预测模型中,因为它与目标变量的协变很高。另一方面,当两个特征高度相关时,我们倾向于忽略其中一个并选择另一个,或者应用某种正则化和特征选择方法来减少相关特征的影响。
摘要
在本章中,我们介绍了进行 EDA 的基本技术。我们首先回顾了分析和管理分类数据的常见方法,包括频率计数和条形图。然后,当我们处理多个分类变量时,我们介绍了边缘分布和分面条形图。
接下来,我们转向分析数值变量,并涵盖了敏感度量,如集中趋势(均值)和变异(方差),以及稳健度量,如中位数和四分位数间距。有几种图表可用于可视化数值变量,包括直方图、密度图和箱线图,所有这些都可以与另一个分类变量结合使用。
最后,我们通过使用股票价格数据进行了案例研究。我们首先从 Yahoo! Finance 下载了真实数据,并应用所有 EDA 技术来分析数据,然后创建了一个相关性图来指示每对变量之间协变的强度。这使我们能够对变量之间的关系有一个有帮助的理解,并启动预测建模阶段。
在下一章中,我们将介绍 r markdown,这是一个广泛使用的 R 包,用于生成交互式报告。
第六章:使用 R Markdown 进行有效报告
上一章介绍了不同的绘图技术,所有这些都是静态的。在本章中,我们将更进一步,讨论如何使用 R Markdown 一致地生成图表和表格。
到本章结束时,您将学习到 R Markdown 报告的基础知识,包括如何添加、微调和自定义图表和表格以制作交互式和有效的报告。您还将了解如何生成有效的 R Markdown 报告,这可以为您的演示增添色彩。
本章将涵盖以下主题:
-
R Markdown 基础
-
生成财务分析报告
-
自定义 R Markdown 报告
技术要求
要完成本章的练习,您需要拥有以下软件包的最新版本:
-
rmarkdown, 版本 2.17 -
quantmod, 版本 0.4.20 -
lubridate, 版本 1.8.0
请注意,前面提到的软件包版本是在我编写本书时的最新版本。本章的所有代码和数据均可在以下网址找到:github.com/PacktPublishing/The-Statistics-and-Machine-Learning-with-R-Workshop/tree/main/Chapter_6。
R Markdown 基础
R Markdown 是一种格式化语言,可以帮助您有效地动态地从数据中揭示洞察力,并以 PDF、HTML 文件或网络应用程序的形式生成报告。它允许您通过本书前面介绍的各种图表和表格形式整理您的分析,并以一致、整洁、透明的方式展示,便于其他分析师轻松复制。无论是在学术界还是工业界,证明您分析的可重复性是您工作的一个基本品质。当其他人可以轻松复制并理解您在分析中所做的工作时,这会使沟通更加容易,并使您的工作更加值得信赖。由于所有输出都是基于代码的,因此您在展示初步工作并返回进行进一步修改时,可以轻松地微调分析,这是现实数据分析中常见的迭代过程。
使用 R Markdown,您可以将代码及其输出(包括图表和表格)一起展示,并添加周围文本作为上下文。它与使用 Python 的 Jupyter Notebook 类似,但它在 tidyverse 生态系统支持下具有优势。
R Markdown 基于 Markdown 语法,这是一种易于遵循的标记语言,允许用户从纯文本文件创建类似网页的文件。让我们先下载 R Markdown 软件包,并创建一个简单的起始文件。
开始使用 R Markdown
R Markdown 允许我们创建高效的报告来总结我们的分析,并将结果传达给最终用户。要在 RStudio 中启动 R Markdown,我们首先需要下载 rmarkdown 包并将其加载到控制台,这可以通过以下命令完成:
>>> install.packages("rmarkdown")
>>> library(rmarkdown)
R Markdown 有一种专门的文件类型,以 .Rmd 结尾。要创建 R Markdown 文件,我们可以在 RStudio 中选择 文件 | 新建文件 | R Markdown;这将显示 图 6**.1 中的窗口。左侧面板包含我们可以选择的不同格式,其中 文档 是一组常见的文件类型,如 HTML、PDF 和 Word,演示 以类似 PowerPoint 的演示模式呈现 R Markdown 文件,Shiny 在 R Markdown 文件中添加交互式 Shiny 组件(交互式小部件),从模板 提供了一系列启动模板以加速报告生成:

图 6.1 – 创建 R Markdown 文件
让我们从 my first rmarkdown 开始,并点击 .Rmd 文件,将创建一个包含基本指令的文件。并非所有这些信息都会被使用,因此熟悉常见组件后,您可以自由删除脚本中的不必要代码。
R Markdown 文档由三个组件组成:文件的元数据、报告的文本和分析的代码。我们将在以下各节中查看这些组件。
了解 YAML 标题
如 图 6**.2 所示,R Markdown 脚本的顶部是一组由两组三个连字符 --- 包裹的元数据标题信息,并包含在 YAML 标题中。YAML,一种人类可读的数据序列化语言,是用于配置文件中分层数据结构的语法。在这种情况下,默认信息包括标题、输出格式和日期,以键值对的形式表示。标题中的信息会影响整个文档。例如,要生成 PDF 文件,我们只需在输出配置中将 html_document 切换为 pdf_document。这是标题中所需的最小信息集,尽管鼓励您添加作者信息(通过 图 6**.2 中的相同初始窗口)以显示您的工作版权:

图 6.2 – 默认 R Markdown 脚本的 YAML 标题
在设置好标题信息并假设所有额外的代码都已删除后,我们可以通过点击 test.Rmd 来编译 R Markdown 文件为 HTML 文件:

图 6.3 – 使用 Knit 按钮将 R Markdown 文件转换为 HTML 文件
编译 R Markdown 文件将生成一个在单独的预览窗口中打开的 HTML 文件。它还会在同一文件夹中保存一个名为test.html的 HTML 文件。
接下来,我们将学习更多关于 R Markdown 文件主体结构和语法的知识,包括文本格式化和处理代码块。
格式化文本信息
文本信息的重要性与您为分析和建模编写的代码相当,甚至更高。好的代码通常有很好的文档,当您的最终用户是非技术性的时,这一点尤为重要。在适当的位置放置背景信息、假设、上下文和决策过程是您技术分析的重要伴侣,除了分析的透明性和一致性之外。在本节中,我们将回顾我们可以用来格式化文本的常用命令。
练习 6.1 – 在 R Markdown 中格式化文本
在这个练习中,我们将使用 R Markdown 生成图 6.4中显示的文本:

图 6.4 – 使用 R Markdown 生成的 HTML 文件示例文本
文本包括标题、一些斜体或粗体的单词、一个数学表达式和四个无序列表项。让我们看看如何生成这个文本:
-
使用
#符号编写一级标题:# Introduction to statistical model注意,我们使用的井号越多,标题就会越小。请记住,在井号和文本之间添加一个空格。
-
通过将文本包裹在
* *中以实现斜体和$$以实现数学表达式,来编写中间句子:A *statistical model* takes the form $y=f(x)+\epsilon$, where -
通过在每个项目前使用
*并使用** **将文本包裹起来以实现粗体,来生成无序列表:* $x$ is the **input** * $f$ is the **model** * $\epsilon$ is the **random noise** * $y$ is the **output**注意,我们可以轻松地将输出文件从 HTML 切换到 PDF,只需将
output: html_document更改为output: pdf_document。结果输出显示在图 6.5中:

图 6.5 – 使用 R Markdown 生成的 PDF 文件示例文本
将 R Markdown 文件编译成 PDF 文档可能需要您安装额外的包,例如 LaTeX。当出现错误提示说该包不可用时,只需在控制台中安装此包,然后再进行编译。我们还可以使用编译按钮的下拉菜单来选择所需的输出格式。
此外,YAML 标题中日期键的值是一个字符串。如果您想自动显示当前日期,可以将字符串替换为````pyr Sys.Date()"``.
These are some of the common commands that we can use in a .Rmd file to format the texts in the resulting HTML file. Next, we will look at how to write R code in R Markdown.
Writing R code
In R Markdown, the R code is contained inside code chunks enclosed by three backticks, ```` py ,这在 R Markdown 文件中用于将代码与文本分开。代码块还伴随着对应于所使用语言和其他配置的规则和规范,这些规则和规范位于花括号{}内。代码块允许我们渲染基于代码的输出或在报告中显示代码。
下面的代码片段展示了示例代码块,其中我们指定语言类型为 R 并执行赋值操作:
```{r}
a = 1
```py
除了输入代码块的命令外,我们还可以在工具栏中点击代码图标(以字母c开头)并选择 R 语言的选项,如图图 6.6所示。请注意,您还可以使用其他语言,如 Python,从而使 R Markdown 成为一个多功能的工具,允许我们在一个工作文件中使用不同的编程语言:

图 6.6 – 插入 R 代码块
每个代码块都可以通过点击每个代码块右侧的绿色箭头来执行,结果将显示在代码块下方。例如,图 6.7显示了执行赋值和打印变量后的输出:

图 6.7 – 执行代码块
我们还可以在代码块的大括号中指定其他选项。例如,我们可能不希望在生成的 HTML 文件输出中包含特定的代码块。为了隐藏代码本身并只显示代码的输出,我们可以在代码块的相关配置中添加echo=FALSE,如下面的代码块所示:
```{r echo=FALSE}
a = 1
a
```py
图 6.8显示了生成的 HTML 文件中的两种不同类型的输出:

图 6.8 – 在 HTML 文件中显示和隐藏源代码
此外,当我们加载当前会话中的包时,我们可能在控制台得到一个警告消息。在 R Markdown 中,这样的警告消息也会出现在生成的 HTML 中。要隐藏警告消息,我们可以在配置中添加warning=FALSE。例如,在下面的代码片段中,我们在加载dplyr包时隐藏了警告消息:
```{r warning=FALSE}
library(dplyr)
```py
图 6.9比较了加载包时显示或隐藏警告消息的两个场景:

图 6.9 – 加载包时隐藏警告消息
在这些构建块介绍完毕后,我们将在下一节进行案例研究,使用谷歌股票价格数据生成财务分析报告。
生成财务分析报告
在本节中,我们将分析来自 Yahoo! Finance 的谷歌股票数据。为了方便数据下载和分析,我们将使用quantmod包,该包旨在帮助量化交易者开发、测试和部署基于统计的贸易模型。让我们安装这个包并将其加载到控制台:
>>> install.packages("quantmod")
>>> library(quantmod)
接下来,我们将使用 R Markdown 生成 HTML 报告,并介绍数据查询和分析的基础知识。
获取和显示数据
让我们通过一个练习来生成一个初始报告,该报告会自动从 Yahoo! Finance 查询股票数据,并显示数据集中的基本信息。
练习 6.2 – 生成基本报告
在这个练习中,我们将设置一个 R Markdown 文件,下载谷歌的股价数据,并显示数据集的一般信息:
-
创建一个名为
Financial analysis的空 R Markdown 文件,并在 YAML 文件中设置相应的output、date和author:--- title: "Financial analysis" output: html_document date: "2022-10-12" author: "Liu Peng" --- -
创建一个代码块来加载
quantmod包并使用getSymbols()函数查询谷歌的股价数据。将结果数据存储在df中。同时隐藏结果 HTML 文件中的所有消息,并添加必要的文本说明:# Analyzing Google's stock data since 2007 Getting Google's stock data ```{r warning=FALSE, message=FALSE} library(quantmod) df = getSymbols("GOOG", auto.assign=FALSE) ```py在这里,我们指定
warning=FALSE以隐藏加载包时的警告消息,message=FALSE以隐藏调用getSymbols()函数时生成的消息。我们还指定auto.assign=FALSE将结果 DataFrame 分配给df变量。另外,请注意,我们可以在代码块内添加文本作为注释,这些注释将被视为以井号#开头的典型注释。 -
通过三个单独的代码块计算总行数并显示 DataFrame 的前两行和最后两行。为代码添加相应的文本作为文档:
Total number of observations of `df` ```{r} nrow(df) ```py Displaying the first two rows of `df` ```{r} head(df, 2) ```py Displaying the last two rows of `df` ```{r} tail(df, 2) ```py注意,我们使用
` `来表示文本中的内联代码。到目前为止,我们可以编织 R Markdown 文件以观察生成的 HTML 文件,如图图 6.10所示。经常检查输出是一个好习惯,这样就可以及时纠正任何潜在的不期望的错误:

图 6.10 – 显示 HTML 输出
-
使用
chart_Series()函数绘制每日收盘价的时间序列图:Plotting the stock price data ```{r} chart_Series(df$GOOG.Close,name="Google Stock Price") ```py将此代码块添加到 R Markdown 文档中并编织它,将生成与图 6.11中所示相同的输出文件,并增加一个额外的图形。
chart_Series()函数是quantmod提供的用于绘图的实用函数。我们也可以根据前一章讨论的ggplot包来绘制它:

图 6.11 – 自 2017 年以来谷歌的每日股价
除了从代码生成图形外,我们还可以将链接和图片包含在输出中。此图片可以从本地驱动器或从网络加载。在下面的代码片段中,我们添加了一行带有超链接的文本,指向一个示例图片,并在下一行直接从 GitHub 读取显示该图片:
The following image can be accessed [here](https://github.com/PacktPublishing/The-Statistics-and-Machine-Learning-with-R-Workshop/blob/main/Chapter_6/Image.png).

注意,我们通过将单词 here 放在方括号内并跟在括号中的超链接后面来添加一个超链接。要添加图片,我们可以在方括号前添加一个感叹号。我们还可以通过在图片链接后添加 {width=250px} 来指定图片的大小。
在 R Markdown 中编译前面的代码生成 图 6**.12:

图 6.12 – 从 GitHub 可视化图像
接下来,我们将执行数据分析并将结果以文本形式显示。
执行数据分析
加载数据集后,我们可以在生成的输出文档中执行数据分析并展示洞察力,所有这些都是自动且一致的。例如,我们可以展示特定时期内股票价格的高级统计数据,如平均、最大和最小价格。这些统计数据可以嵌入到文本中,使演示风格更加自然和自包含。
练习 6.3 – 执行简单的数据分析
在这个练习中,我们将提取 Google 的年度最高、平均和最低股价。为此,我们首先将数据集从其原始的 xts 格式转换为 tibble 对象,然后使用 dplyr 总结这些统计量。最后,我们将在 HTML 文档的文本中显示这些信息:
-
加载
dplyr和tibble包,并将df从xts格式转换为tibble格式。将生成的tibble对象存储在df_tbl中:library(dplyr) library(tibble) df_tbl = df %>% as_tibble() %>% add_column(date = index(df), .before = 1)在这里,我们将使用
as_tibble()函数将xts对象转换为tibble格式,然后使用add_column()函数在 DataFrame 的开头插入一个日期列。日期信息作为索引存在于原始的xts对象中。 -
存储自 2022 年以来的年度最高、平均和最低收盘价,分别存储在
max_ytd、avg_ytd和min_ytd中:max_ytd = df_tbl %>% filter(date >= as.Date("2022-01-01")) %>% summarise(price = max(GOOG.Close)) %>% .$price avg_ytd = df_tbl %>% filter(date >= as.Date("2022-01-01")) %>% summarise(price = mean(GOOG.Close)) %>% .$price min_ytd = df_tbl %>% filter(date >= as.Date("2022-01-01")) %>% summarise(price = min(GOOG.Close)) %>% .$price对于每个统计量,我们首先按日期过滤,然后根据
GOOG.Close列提取相关统计量。最后,我们将结果作为单个标量值返回,而不是 DataFrame。 -
以文本形式显示这些统计量:
Google's **highest** year-to-date stock price is `r max_ytd`. Google's **average** year-to-date stock price is `r avg_ytd`. Google's **lowest** year-to-date stock price is `r min_ytd`.如 图 6**.13 所示,编译文档将统计量输出到 HTML 文件中,这使得我们可以在 HTML 报告中引用代码结果:

图 6.13 – 提取简单统计量并在 HTML 格式中显示
在下一节中,我们将探讨如何在 HTML 报告中添加图表。
在报告中添加图表
在 HTML 报告中添加图表的方式与在 RStudio 控制台中相同。我们只需在代码块中编写绘图代码,然后编译 R Markdown 文件后,图表就会出现在生成的报告中。让我们通过一个练习来可视化使用上一章中介绍的 ggplot2 包的股票价格。
练习 6.4 – 使用 ggplot2 添加图表
在这个练习中,我们将通过线形图可视化过去三年的平均月收盘价。我们还将探索报告中图表的不同配置选项:
-
创建一个包含 2019 年至 2021 年间月度平均收盘价的数据库:
library(ggplot2) library(lubridate) df_tbl = df_tbl %>% mutate(Month = factor(month(date), levels = as.character(1:12)), Year = as.character(year(date))) tmp_df = df_tbl %>% filter(Year %in% c(2019, 2020, 2021)) %>% group_by(Year, Month) %>% summarise(avg_close_price = mean(GOOG.Close)) %>% ungroup()在这里,我们首先创建两个额外的列,分别称为
Month和Year,这些列是基于日期列通过lubridate包中的month()和year()函数派生出来的。我们还把Month列转换为因子类型的列,其级别在 1 到 12 之间,这样当我们在后面绘制月度价格图时,这个列可以遵循特定的顺序。同样,我们将Year列设置为字符类型的列,以确保它不会被ggplot2解释为数值变量。接下来,我们通过
Year对df_tbl变量进行筛选,按Year和Month分组,并计算GOOG.Close的平均值,然后使用ungroup()函数从保存在tmp_df中的结果 DataFrame 中移除分组结构。 -
在线形图中将每年的月度平均收盘价作为单独的线条绘制。更改相应的图表标签和文本大小:
p = ggplot(tmp_df, aes(x = Month, y = avg_close_price, group = Year, color = Year)) + geom_line() + theme(axis.text=element_text(size=16), axis.title=element_text(size=16,face="bold"), legend.text=element_text(size=20)) + labs(titel = "Monthly average closing price between 2019 and 2021", x = "Month of the year", y = "Average closing price") p在代码块中运行前面的命令将生成图 6**.14中显示的输出。请注意,我们还添加了标题和一些文本,以指出代码的目的和上下文。在编织 R Markdown 文件后,代码和输出会自动显示,这使得 R Markdown 成为生成透明、吸引人和可重复的技术报告的绝佳选择:

图 6.14 – 添加图表以显示过去三年的月度平均收盘价
我们还可以配置图表的大小和位置。
-
通过在代码块的配置部分设置
fig.width=5和fig.height=3来缩小图表的大小,并显示输出图形:Control the figure size via the `fig.width` and `fig.height` parameters. ```{r fig.width=5, fig.height=3} p ```py使用这些添加的命令编织文档会产生图 6.15:

图 6.15 – 改变图表的大小
-
将图表的位置对齐,使其位于文档的中心:
Align the figure using the `fig.align` parameter. ```{r fig.width=5, fig.height=3, fig.align='center'} p ```py使用这些添加的命令编织文档会产生图 6.16:

图 6.16 – 改变图表的位置
-
为图表添加标题:
Add figure caption via the `fig.cap` parameter. ```{r fig.width=5, fig.height=3, fig.align='center', fig.cap='图 1.1 2019 年至 2021 年间的月度平均收盘价'} p ```py使用这些添加的命令编织文档会产生图 6.17:

图 6.17 – 为图表添加标题
除了图形外,表格也是报告中常用的一种用于呈现和总结信息的方式。我们将在下一节中探讨如何生成表格。
向报告中添加表格
当报告用户对深入了解细节或进一步分析感兴趣时,以表格形式呈现信息是图形对应物的良好补充。对于最终用户来说,能够访问和使用报告中的数据起着关键作用,因为这给了他们更多控制权,可以控制报告中已经预处理好的信息。换句话说,基于 R Markdown 的 HTML 报告不仅以图形形式总结信息以便于消化,还提供了关于特定数据源的详细信息作为表格,以促进即席分析。
我们可以使用 knitr 包中的 kable() 函数添加表格,该函数是支持在每个代码块中执行代码的核心引擎,然后在对 R Markdown 文档进行编织时进行动态报告生成。请注意,在通过 kable() 将数据作为表格展示之前进行预处理和清理数据是一个好的实践;这项任务应该只涉及展示一个干净且有序的表格。
让我们通过一个练习来看看如何向报告中添加干净的表格。
练习 6.5 – 使用 kable() 添加表格
在这个练习中,我们将以表格形式展示 tmp_df 变量的前五行,然后演示不同的表格显示配置选项:
-
使用
knitr包中的kable()函数显示tmp_df的前五行:# Adding tables Printing `tmp_df` as a static summary table via the `kable()` function. ```{r} library(knitr) kable(tmp_df[1:5,]) ```py使用这些添加的命令编织文档会产生 图 6**.18:

图 6.18 – 向报告中添加表格
-
使用
col.names参数更改表格的列名:Changing column names via the `col.names` parameter. ```{r} kable(tmp_df[1:5,], col.names=c("Year", "Month", "Average closing price")) ```py使用这些添加的命令编织文档会产生 图 6**.19:

图 6.19 – 更改表格中的列名
我们还可以使用 align 参数修改表格内的列对齐方式。默认情况下,数值列的列对齐在右侧,其他所有类型的列对齐在左侧。如 图 6**.19 所示,Year(字符类型)和 Month(因子类型)列左对齐,而 Average closing price(数值)列右对齐。对齐方式按列指定,使用单个字母表示,其中 "l" 表示左对齐,"c" 表示居中对齐,"r" 表示右对齐。
-
使用
align参数将所有列对齐到中心:Align the table via the `align` argument. ```{r} kable(tmp_df[1:5,], col.names=c("Year", "Month", "Average closing price"), align="ccc") ```py在这里,我们指定
align="ccc"以将所有列对齐到中心。使用这些添加的命令编织文档会产生 图 6**.20:

图 6.20 – 使表格的所有列居中
最后,我们还可以为表格添加一个标题。


浙公网安备 33010602011771号