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/err…并填写表格。
盗版: 如果您在互联网上以任何形式发现我们作品的非法副本,如果您能提供位置地址或网站名称,我们将不胜感激。请通过版权@packt.com 与我们联系,并提供材料的链接。
如果您有兴趣成为作者: 如果您在某个领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问authors.packtpub.com。
分享您的想法
一旦您阅读了《使用 R 进行统计和机器学习研讨会》,我们非常乐意听听您的想法!请点击此处直接访问此书的亚马逊评论页面并分享您的反馈。
您的评论对我们和科技社区都非常重要,它将帮助我们确保我们提供高质量的内容。
下载此书的免费 PDF 副本
感谢您购买这本书!
您喜欢在旅途中阅读,但无法携带您的印刷书籍到任何地方吗?
您的电子书购买是否与您选择的设备不兼容?
别担心,现在每购买一本 Packt 书籍,您都可以免费获得该书的 DRM 免费 PDF 版本。
在任何地方、任何设备上阅读。直接从您最喜欢的技术书籍中搜索、复制和粘贴代码到您的应用程序中。
优惠远不止于此,您还可以获得独家折扣、时事通讯和每日免费内容的每日电子邮件。
按照以下简单步骤获取优惠:
- 扫描二维码或访问以下链接
-
提交您的购买证明
-
就这样!我们将直接将您的免费 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处理文本数据。