R语言编程:基于tidyverse

978-7-115-60380-7
作者: 张敬信
译者:
编辑: 胡俊英
分类: R语言

图书目录:

详情

这是一本基于 tidyverse 入门 R 语言编程的书,本书从基本的编程语法讲起,适合编程零基础的读者阅读。本书结合新的 R 语言编程范式,让读者学习更高效率的 R 编程,尤其是真正用整洁优雅的数据化编程思维解决一系列数据问题,包括数据清洗、数据处理、数据可视化、统计建模、文档沟通等,并在附录中将透视表、网络爬虫、高性能计算、机器学习等典型应用囊括其中,为读者提供了丰富的R实用编程案例,也可作为一本 R 语言语法大全的工具书。 本书面向热爱R语言编程的读者,适合统计学、数据分析、数据可视化等领域的读者阅读参考,也可以作为高等院校相关专业的 R 语言教材。

图书摘要

版权信息

书名:R语言编程——基于tidyverse

ISBN:978-7-115-60380-7

本书由人民邮电出版社发行数字版。版权所有,侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。

如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。


版  权

著    张敬信

责任编辑 胡俊英

人民邮电出版社出版发行  北京市丰台区成寿寺路11号

邮编 100164  电子邮件 315@ptpress.com.cn

网址 http://www.ptpress.com.cn

读者服务热线:(010)81055410

反盗版热线:(010)81055315

读者服务:

微信扫码关注【异步社区】微信公众号,回复“e60380”获取本书配套资源以及异步社区15天VIP会员卡,近千本电子书免费畅读。

内 容 提 要

这是一本基于tidyverse入门R语言编程的书,本书从基本的编程语法讲起,适合编程零基础的读者阅读。本书结合新的R语言编程范式,让读者学习更高效率的R编程,尤其是真正用整洁优雅的数据化编程思维解决一系列数据问题,包括数据清洗、数据处理、数据可视化、统计建模、文档沟通等,并在附录中将透视表、网络爬虫、高性能计算、机器学习等典型应用囊括其中,为读者提供了丰富的R实用编程案例,也可作为一本R语言语法大全的工具书。

本书面向热爱R语言编程的读者,适合统计学、数据分析、数据可视化等领域的读者阅读参考,也可以作为高等院校相关专业的R语言教材。

彩  图

图2.1 tidyverse优雅编程思维(核心编程思想)

图2.3 tidyverse整洁工作流

图3.3 简单的分组散点图

图3.6 带全局光滑曲线的散点图

图3.19 手动设置离散变量颜色并修改对应图例

图3.20 使用调色板颜色设置离散变量颜色

图3.21 使用渐变色设置连续变量颜色

图3.25 标注均值和标准差的小提琴图

图3.29 堆叠条形图t

图3.35 选择主题

图3.36 在ggplot生成的图中使用中文字体

图3.42 人口金字塔图

图3.44 折线图与面积图

图3.46 饼图

图3.48 用gganimate绘制动态可视化图形

图4.1 不同均值标准差对应的正态分布

图4.4 标记频率的水平条形图

前  言

R语言是以统计和分析见长的专业的编程语言,具有优秀的绘图功能,且开源免费,有丰富的扩展包和活跃的社区。R语言的这些优质特性,使其始终在数据统计分析领域的SAS、Stata、SPSS、Python、Matlab等同类软件中占据领先地位。

R语言曾经最为人们津津乐道的是Hadley开发的ggplot2包,其泛函式图层化语法赋予了绘图一种“优雅”美。近年来,R语言在国外蓬勃发展,ggplot2这个“点”自2016年以来,已被Hadley“连成线、张成面、形成体(系)”,从而形成了tidyverse包。该包将“数据导入、数据清洗、数据操作、数据可视化、数据建模、可重现与交互报告”整个数据科学流程整合起来,以“现代的”“优雅的”方式和管道式、泛函式编程技术实现。不夸张地说,用tidyverse操作数据比pandas更加好用、易用!再加上可视化本来就是R所擅长的,可以说R在数据科学领域不次于Python。

这种整洁、优雅的tidy流,又带动了R语言在很多研究领域涌现出一系列tidy风格的包:tidymodels(统计与机器学习)、mlr3verse(机器学习)、rstatix(应用统计)、tidybayes(贝叶斯模型)、tidyquantmodeltime(金融)、fpp3timetk(时间序列)、quanteda(文本挖掘)、tidygraph(网络图)、sf(空间数据分析)、tidybulk(生物信息)、sparklyr(大数据)等。

在机器学习和数据挖掘领域,曾经的R包总是在单打独斗,如今也正在从整合技术方面迎头赶上Python,出现了tidy风格的tidymodels包,以及新一代的用于机器学习的mlr3verse包,这些包基于R6类面向对象、data.table神速数据底层和开创性的Graph-流模式(图/网络流有别于通常的线性流)。

写作本书的目的

我发现近几年出现的R语言新技术很少有人问津,绝大多数R语言的教师和学习者,以及教材、博客文章仍在沿用那些过时的、晦涩的R语法,对R语言的印象仍停留在几年前:语法晦涩难懂、速度慢,做统计分析和绘图还行,没有统一的机器学习框架,无法用于深度学习、大数据、工业部署等。

有感于此,我想写一本用新R的技术,方便新手真正快速入门R语言编程的书,来为R语言正名。我是一名大学数学教师,热爱编程、热爱R语言,奉行终身学习的理念,一直喜欢跟踪和学习新知识、新技能。我对编程和R语言有一些独到的理解,因为我觉得数学语言与编程语言是相通的,都是用语法元素来表达和解决问题,我想把这些理解和体会用简洁易懂的方式表达出来。

希望这本书能让你学到正确的编程思想,学到新的R语言编程知识和编程思维,能真正让你完成R语言入门或将R知识汰旧换新。

本书的目标读者

没有R语言基础,想要系统地学习R语言编程,特别是想要学习新兴R技术的人。

具备一定的R语言基础,想升级R语言编程技术的人。

想要理解编程思想,锻炼向量化编程思维、函数式编程思维,以及想要真正掌握数据思维的人。

想要以R语言为工具,从事统计分析、数据挖掘、机器学习工作的人,特别是想学习使用机器学习包(tidymodelsmlr3verse)的人。

高校里对R语言及相关课程有需求的师生及科研人员,特别是将来想要在时间序列、金融、空间数据分析、文本挖掘等领域使用fpp3modeltimetidyquantsfquanteda等包的人。

本书特色

1.内容新颖

本书绝大部分内容参考新版本R包的相关文档,全面采用新的R语言技术编写,特别强调“整洁流、管道流、泛函流”数据科学思维(tidyverse)。

2.真正融入编程思维

很多R语言编程书只是罗列编程语法,很难让初学者学透它们。本书真正融入编程思维:由编程思想引导,了解编程语法到底是怎么回事,应该用于何处以及怎么使用。

3.精心准备实例

讲解编程语法必须配以合适的实例来演示,也建议读者一定要将编程语法讲解与配套实例结合起来阅读,比起将实例代码调试通过,更重要的是借助实例代码透彻地理解编程语法所包含的编程思维。本书后半部分是R语言在应用统计、探索性数据分析、文档沟通方面的应用,所配案例力求能让读者上手使用。

4.程序代码优雅、简洁、高效

本书程序代码都是基于tidyverse编写的,自然就很优雅。此外,本书尽量采用向量化编程和泛函式编程,更体现其简洁、高效。可以说,读者如果用这本书入门R语言,或者更新你的R知识库,就会自动跳过写烦琐、低效代码的阶段,直接进入“高手级”的行列。

本书内容安排

本书的结构是围绕如何学习R语言编程来展开的,全书内容共分为6章。 冯国双老师在《白话统计》中写道:

“一本书如果没有作者自己的观点,而只是知识的堆叠,那么这类书是没有太大价值的。”

尤其在当前网络发达的时代,几乎任何概念和知识点都可以从网络上查到。但有一点你很难查到,对于编程书来说,那就是编程思维。本书最大的特点之一就是无论是讲编程思想还是讲编程语法知识点,都把编程思维融入进去。

很多人学编程始终难以真正入门,学习编程语言要在编程思想的指导下才能事半功倍。本书的导语就先来谈编程思维,包括如何理解编程语言,用数学建模的思维引领读者从理解实际问题到自己写代码解决问题,了解R语言的编程思想(面向函数、面向对象、面向向量)。

第1章讲述R语言编程的基本语法,同时涉及向量化编程、函数式编程。这些语法在其他编程语言中也是相通的,包括搭建R语言环境以及常用数据结构(存放数据的容器),例如向量、矩阵、数据框、因子、字符串(及正则表达式)、日期时间,此外还涉及分支结构、循环结构、自定义函数等。这些基本语法是编写R代码的基本元素,学透它们非常重要,只有学透它们才能将其任意组合、恰当使用,以写出解决具体问题的R代码。同样是讲R语言的基本语法,本书的不同之处在于,用tidyverse中更一致、更好用的相应包加以代替,例如用tibble代替data.frame、用forcats包处理因子、用stringr讲字符串(及正则表达式)、用lubridate包讲日期时间、在循环结构中用purrr包的map_* 函数代替apply系列函数,另外还特别讲到泛函式编程。

第2章正式进入tidyverse核心部分,即数据操作。本章侧重讲解数据思维,先简单介绍tidyverse包以及编程技术之管道操作,接着围绕各种常用数据操作展开,包括数据读写(各种常见数据文件的读写及批量读写、用R连接数据库、中文编码问题及解决办法),数据连接(数据按行/列拼接、SQL数据库连接),数据重塑(“脏”数据变“整洁”数据,长宽表转换、拆分与合并列),数据操作(选择列、筛选行、对行进行排序、修改列、分组汇总)、其他数据操作(按行汇总、窗口函数、滑窗迭代、整洁计算),以及data.table基本使用(常用数据操作的dplyr语法与data.table语法对照)。tidyverse最大的优势就是以“管道流”和“整洁语法”操作数据,这些语法真正让数据操作从Base R的晦涩、难记、难用,到tidyverse的“一致”“整洁”、好记、好用,甚至比Python的pandas还好用!为了最大限度地降低理解负担,本书特意选用中文的学生成绩数据作为演示数据,让读者只关心语法就好。另外,tidyverse的这些数据操作,实际上已经在语法层面涵盖了日常Excel数据操作、SQL数据库操作,活用tidyverse数据操作语法已经可以完成很多常见任务。

第3章,可视化与建模技术。可视化只介绍流行的可视化包ggplot2,先从ggplot2的图层化绘图语法开始,依次介绍ggplot2的几大部件:数据、映射、几何对象、标度、统计变换、坐标系、位置调整、分面、主题、输出;接着介绍功能上的图形分类:类别比较图、数据关系图、数据分布图、时间序列图、局部整体图、地理空间图和动态交互图,对每一类图形分别选择其中有代表性的用实例加以演示。建模技术包括三项内容:(1)用broom包提取统计模型结果为整洁数据框,方便后续访问和使用;(2)modelr包中一些有用的辅助建模函数;(3)批量建模技术,例如要对各地区的数据分别建立模型、提取模型结果,当然这可以用for循环实现,但这里采用更加优雅的分组嵌套以及mutate+map_*实现。

第4章,应用统计。R语言是专业的统计分析软件,广泛应用于统计分析与计算领域。本章将从4个方面展开:(1)描述性统计,介绍适合描述不同数据的统计量、统计图、列联表;(2)参数估计,主要介绍点估计与区间估计,包括用Bootstrap法估计置信区间,以及常用的参数估计方法(最小二乘估计、最大似然估计);(3)假设检验,介绍假设检验原理,基于理论的假设检验(以方差分析、卡方检验为例,并用整洁的rstatix包实现),以及基于重排的假设检验(以t检验为例,用infer包实现);(4)回归分析,从线性回归原理、回归诊断,借助具体实例讲解多元线性回归的整个过程,并介绍广泛应用于机器学习的梯度下降法以及广义线性模型原理。

第5章,探索性数据分析。主要讨论三方面内容:(1)数据清洗,包括缺失值探索与处理、异常值识别与处理;(2)特征工程,包括特征缩放(标准化、归一化、行规范化、数据平滑)、特征变换(非线性特征、正态性变换、连续变量离散化)、基于PCA的特征降维;(3)探索变量间的关系,包括分类变量之间、分类变量与连续变量之间、连续变量之间的关系。

第6章,文档沟通,讨论如何进行可重复研究,用R Markdown家族生成各种文档,介绍R markdown的基本使用,R与Latex交互编写期刊论文、PPT、图书、R与Git/GitHub交互进行版本控制、用R Shiny轻松制作交互网络应用程序(Web App)以及开发和发布R包的工作流程。

附录部分是对正文内容的补充和扩展,分别介绍R6类面向对象编程、错误与调试、用R实现Excel中的VLOOKUP与透视表、非等连接与滚动连接、R与网络爬虫、R与高性能计算、R机器学习框架—mlr3versetidymodels

大家可以根据自己的需求选择阅读的侧重点,不过我还是希望你能够按照顺序完整地阅读,这样才能彻底地更新一遍你的R知识,避免将Base R与tidyverse混用,因为二者在编写R代码时不是一种思维,强行搭在一起反而效率低。

本书所用的软件

本书在编写时,使用当时最新的R语言版本4.2.2和RStudio-2022.07.2-576,使用的R包主要是tidyverse 1.3.2系列。

本书的配套资源下载

本书的R程序均作为R markdown中的代码调试通过,所有示例的数据、R程序、教学PPT都可以在异步社区官网、GitHub(https://github.com/zhjx19/introR)、码云(https://gitee.com/zhjx19/introR)下载。

致谢

感谢Hadley的《R数据科学》(R for Data Science)一书让我实现了tidy方式的数据科学入门;感谢Desi Quintans和Jeff Powell的Working in the Tidyverse一书让我真正开始对用tidyverse操作数据产生兴趣。也正是这些启蒙和启发令本书得以诞生。

感谢我的爱人及岳父岳母,在家庭生活方面给予我诸多照顾,让我能安心地创作;特别感谢我远在河北老家的母亲和弟弟,在我无能为力的时候,照顾生病住院和在家养病的父亲,免去了我的后顾之忧。

感谢Hadley开发的tidyverse包让R语言用起来如有神助,感谢谢益辉开发的rmarkdown/ bookdown帮助我高效地编写书稿,感谢黄湘云&叶飞整合的ElegantBookdown模板。

感谢知乎平台及知乎上的读者们,你们让本书有机会为广大的读者知晓。感谢“tidy-R”“Use R!”“数据之美”QQ群的群主和群里的很多朋友,大家一起学习R语言,一起解答问题,非常开心!也谢谢大家对我的支持以及对本书的期待,你们给了我写作的动力!谢谢群友们帮忙指出书中的错误,特别感谢好友楚新元、“随鸟走天涯”“庄闪闪”等,对本书部分章节中的内容给予很好的建议和很大的帮助。感谢“无安书”等人在“tidy-R”群热心解答群友问题。

感谢胡俊英编辑通过知乎平台找到我,并全力促成了本书的出版,为本书的出版做了大量认真细致的工作。感谢在工作和生活中帮助过我的领导、同事、朋友们,感谢你们,正是因为有了你们,才有了本书的面世。

本书是在黑龙江省哲学社科项目青年项目:全面二孩政策对黑龙江省人口的影响及对策研究(项目号:17TJC134)资助下完成,在此一并表示感谢!

关于勘误

虽然花了很多时间和精力去核对书中的文字、代码和图片,但因为时间仓促和水平有限,本书仍难免会有一些错误和纰漏。如果大家发现问题或有什么疑问,恳请反馈给我,也非常欢迎大家与我探讨R语言编程相关的技术,相关信息可发到我的邮箱zhjx_19@hrbcu.edu.cn,或者在本书的读者群“tidy-R语言2”QQ群(222427909)在线交流,也可以在我的知乎(知乎昵称“张敬信”)专栏相关文章下面评论或私信,我肯定会努力解答疑问或者指出一个正确的方向。

导  语

开篇先来谈一谈,我所理解的编程之道。具体讨论怎么学习编程语言、R语言与数据科学、R语言编程思想,特别是向量化编程思维。

温馨提示

导语部分为了阐述需要,会涉及一些R语言代码,读者可以先忽略代码细节,只当它们是计算过程,把关注点放在所传达的编程思维上。

0.1 怎么学习编程语言

编程语言是人与计算机沟通的一种语言形式,根据设计好的编程元素和语法规则,严格规范地表达我们想要做的事情的每一步(程序代码),使计算机能够明白并正确执行,最终得到期望的结果。

编程语言和数学语言很像,数学语言是最适合表达科学理论的形式语言,用数学符号、数学定义和逻辑推理可以规范地表达科学理论。

很多人说:“学数学,就是靠大量刷题;学编程,就是照着别人的代码敲代码”。

我不认可这种观点,这样的学习方法事倍功半,关键是这样做你学不会真正的数学,也学不会真正的编程!

那么应该怎么学习编程语言呢?

打个比方,要成为一个好的厨师,首先得熟悉各种常见食材的特点,掌握各种基本的烹饪方法,然后就能根据客人需要随意组合食材和烹饪方法制作出各种可口的大餐。

数学的食材就是定义,烹饪方法就是逻辑推理,一旦你真正掌握了定义和逻辑推理,各种基本的数学题都不在话下,而且你还学会了数学知识。

同理,编程的食材和烹饪方法就是编程元素语法规则,例如数据结构(如容器)、分支/循环结构、自定义函数等。一旦你掌握了这些编程元素语法规则,根据问题的需要,你就能自由组合并优化它们,从而写出代码解决问题。

学习任何一门编程语言,根据我的经验,有这么几点建议(步骤)。

(1)理解该编程语言的核心思想,比如Python面向对象,R语言面向函数也面向对象。另外,高级编程语言还倡导向量化编程。读者应在核心思想的引领下学习编程语言并思考如何编写代码。

(2)学习该编程语言的基础知识,这些基础知识本质上是相通的,只是不同的编程语言有其对应的编程语法,相关的基础知识包括数据类型及数据结构(容器)、分支/循环结构、自定义函数、文件读写、可视化等。

(3)前两步完成之后,就算基本入门[1]了。读者可以根据需要,结合遇到的问题,借助网络搜索或他人的帮助,分析问题并解决问题,逐步提升编程技能,用得越多会得到越多,也越熟练。

[1] 至少要经历过一种编程语言的入门,再学习其他编程语言就会快很多。

以上是学习编程语言的正确、快速、有效的方法,切忌不学基础语法,用到什么就学什么,基于别人的代码乱改。这样的结果是,自以为节省时间,实际上是浪费了更多的时间,关键是始终无法入门,更谈不上将来提高。其中的道理也很简单,总是在别人的代码上乱改,是学不到真正的编程知识的,也很难真正地入门编程。当你完整地学习了编程语法,再去基于别人的代码进行修改,这实际上是在验证你所学的编程知识,那么你的编程水平自然也会逐步提高。

再来谈一个学编程过程中普遍存在的问题:如何跨越“能看懂别人的代码”到“自己写代码”之间的鸿沟。

绝大多数人在编程学习过程中,都要经历这样一个过程:

第1步:学习基本语法

第2步:能看懂并调试别人的代码

↓(“编程之门”)

第3步:自己编写代码

前两步没有任何难度,谁都可以做到。从第2步到第3步是一个“坎”,很多人困惑于此而无法真正进入“编程之门”。网上也有很多讲到如何跨越这一步的文章,但基本都是脱离实际操作的空谈(比如照着编写书上的代码之类),往往治标不治本(只能提升编程基本知识)。

我所倡导的理念也很简单,无非就是“分解问题 + 实例梳理 + ‘翻译’及调试”,具体如下:

将难以入手的大问题分解为可以逐步解决的小问题;

用计算机的思维去思考并解决每个小问题;

借助类比的简单实例和代码片段,梳理出详细的算法步骤;

将详细的算法步骤用编程语法逐片段地“翻译”成代码并调试通过。

高级编程语言的程序代码通常是逐片段调试的,借助简单的实例按照算法步骤从上一步的结果调试得到下一步的结果,依次向前推进直至到达最终的结果。另外,写代码时,随时跟踪并关注每一步的执行结果,观察变量、数据的值是否到达你所期望的值,非常有必要!

这是我用数学建模的思维得出的比较科学的操作步骤。为什么大家普遍感觉在自己写代码解决具体问题时有些无从下手呢?

这是因为你总想一步就从问题代码,没有中间的过程,即使编程高手也做不到。当然,编程高手也许能缩减这个过程,但不能省略这个过程。其实你平时看编程书是被表象“欺骗”了:编程书上只介绍问题(或者简单分析问题)紧接着就提供代码,给人的感觉就是应该从问题直接到代码,其实不然。

改变从问题直接到代码的固化思维,可以参考前面说的步骤(分解问题+实例梳理+“翻译”及调试)去操作,每一步是不是都不难解决?这样一来,自然就从无从下手转变到能锻炼自己独立写代码了。

开始你或许只能通过写代码解决比较简单的问题,但是慢慢就会有成就感,再加上慢慢锻炼,写代码的能力会越来越强,能解决的问题也会越来越复杂。当然这一切的前提是,你已经真正掌握了基本编程语法,可以随意取用。当然二者也是相辅相成和共同促进的。

好,说清了这个道理,接下来用一个具体的小案例来演示一下。

例0.1 计算并绘制ROC曲线

ROC曲线是二分类机器学习模型的性能评价指标,已知测试集或验证集中每个样本的真实类别及其模型预测概率值,就可以计算并绘制ROC曲线。

先来梳理一下问题,ROC曲线是在不同分类阈值上对比真正率(TPR)与假正率(FPR)的曲线。

分类阈值就是根据预测概率判定预测类别的阈值,要让该阈值从0到1以足够小的步长变化,对于每个阈值c(如0.85),则当预测概率≥0.85时,判定为“Pos”;当预测概率时,判定为“Neg”。这样就得到了预测类别。

根据真实类别和预测类别,就能计算混淆矩阵,各单元格含义如图0.1所示。

图0.1 混淆矩阵示意图

进一步就可以计算:

有一个阈值,就能计算一组TPRFPR,循环迭代并计算所有的TPRFPR,且将相关数值进行保存。再以FPRx轴,以TPRy轴进行绘制,就可以得到ROC曲线。

在此,我们梳理一下经过分解后的问题。

让分类阈值以某步长在[1,0]上变化取值。

对某一个阈值:

计算预测类别;

计算混淆矩阵;

计算TPRFPR

循环迭代,计算所有阈值的TPRFPR

根据TPRFPR数据绘制ROC曲线。

下面以一个小数据为例,借助代码片段来推演上述过程。现在读者不用纠结于代码,更重要的是体会自己写代码并解决实际问题的过程。

library(tidyverse)
df = tibble(
  ID = 1:10, 
  真实类别 = c(“Pos”,”Pos”,”Pos”,”Neg”,”Pos”,”Neg”,”Neg”,”Neg”,”Pos”,”Neg”),
  预测概率 = c(0.95,0.86,0.69,0.65,0.59,0.52,0.39,0.28,0.15,0.06))
knitr::kable(df)

以上代码的运行结果如表0.1所示。

表0.1 真实类别和预测概率

ID

真实类别

预测概率

1

Pos

0.95

2

Pos

0.86

3

Pos

0.69

4

Neg

0.65

5

Pos

0.59

6

Neg

0.52

7

Neg

0.39

8

Neg

0.28

9

Pos

0.15

10

Neg

0.06

先对某一个阈值,计算对应的TPRFPR,这里以为例。

计算预测类别,实际上就是if-else语句根据条件赋值,当然是用整洁的tidyverse来做。顺便多做一件事情:把类别变量转化为因子型,以保证“Pos”和“Neg”的正确顺序,与混淆矩阵中一致。

c = 0.85
df1 = df %>% 
  mutate(
    预测类别 = ifelse(预测概率 >= c, “Pos”, “Neg”), 
    预测类别 = factor(预测类别, levels = c(“Pos”, “Neg”)),
    真实类别 = factor(真实类别, levels = c(“Pos”, “Neg”)))
knitr::kable(df1)

上述代码的运行结果如表0.2所示。

表0.2 真实类别、预测概率和预测类别

ID

真实类别

预测概率

预测类别

1

Pos

0.95

Pos

2

Pos

0.86

Pos

3

Pos

0.69

Neg

4

Neg

0.65

Neg

5

Pos

0.59

Neg

6

Neg

0.52

Neg

7

Neg

0.39

Neg

8

Neg

0.28

Neg

9

Pos

0.15

Neg

10

Neg

0.06

Neg

计算混淆矩阵,实际上就是统计交叉频数(例如真实值为“Pos”且预测值也为“Pos”的情况有多少,等等)。用R自带的table()函数就能搞定:

cm = table(df1$预测类别, df1$真实类别)
cm
 
##      
##       Pos Neg
##   Pos   2   0
##   Neg   3   5

计算TPRFPR比较简单,根据计算公式,从混淆矩阵中取值进行计算即可。这里咱们再高级一点,用向量化编程来实现。向量化编程是对一列矩阵中的数同时做同样的操作,既提升程序效率又大大简化代码。

向量化编程关键是要用整体考量的思维来思考和表示运算。比如这里计算TPRFPR,通过观察可以发现:混淆矩阵的第1行的各个元素,都除以其所在列的和,正好是TPRFPR

cm[“Pos”,] / colSums(cm)
 
## Pos Neg 
## 0.4 0.0

这就完成了本问题的核心部分。接下来,要进行循环迭代,对每个阈值都计算一遍TPRFPR。用for循环当然可以,但咱们仍然更高级一点,使用泛函式编程。

先把上述计算封装成一个自定义函数,该函数只要接收一个原始的数据框df和一个阈值c,就能返回来你想要的TPRFPR。然后,再把该函数应用到数据框df和一系列的阈值上,循环迭代自然就完成了。这就是泛函式编程

cal_ROC = function(df, c) {
  df = df %>% 
  mutate(
    预测类别 = ifelse(预测概率 >= c, “Pos”, “Neg”), 
    预测类别 = factor(预测类别, levels = c(“Pos”, “Neg”)),
    真实类别 = factor(真实类别, levels = c(“Pos”, “Neg”)))
  cm = table(df$预测类别, df$真实类别)
  t = cm[“Pos”,] / colSums(cm)
  list(TPR = t[[1]], FPR = t[[2]])
}

测试一下这个自定义函数,能不能算出来刚才的结果:

cal_ROC(df, 0.85)
 
## $TPR
## [1] 0.4
## 
## $FPR
## [1] 0

没问题,下面将该函数应用到一系列阈值上(循环迭代) ,并一步到位将每次计算的两个结果按行合并到一起,这就彻底完成了数据计算:

c = seq(1, 0, -0.02)
rocs = map_dfr(c, cal_ROC, df = df)
head(rocs)      # 查看前6个结果
 
## # A tibble: 6 x 2
##     TPR   FPR
##   <dbl> <dbl>
## 1   0       0
## 2   0       0
## 3   0       0
## 4   0.2     0
## 5   0.2     0
## 6   0.2     0

最后,用著名的ggplot2包绘制ROC曲线图形,如图0.2所示:

rocs %>% 
  ggplot(aes(FPR, TPR)) +
  geom_line(size = 2, color = “steelblue”) +
  geom_point(shape = “diamond”, size = 4, color = “red”) +
  theme_bw()

以上就是我所主张的学习编程的正确方法,我认为照着别人的编程书敲代码不是学习编程的好方法。

图0.2 绘制ROC曲线

0.2 R语言与数据科学

0.2.1 什么是数据科学

数据科学是综合了统计学、计算机科学和领域知识的交叉学科,其基本内容就是用数据的方法研究科学,用科学的方法研究数据。数据科学与当前热门的人工智能、数据挖掘、机器学习、深度学习、大数据之间的关系,如图0.3所示。

图0.3 数据科学的位置

Hadley Wickham定义了数据科学的工作流程,如图0.4所示,即数据导入、数据清洗、数据变换、数据可视化、数据建模以及文档沟通,整个分析和探索过程,我们应当训练这样的数据思维。

图0.4 数据科学的工作流程

0.2.2 什么是R语言

1992年,新西兰奥克兰大学统计学教授Ross Ihaka和Robert Gentleman,为了便于给学生教授统计学课程,设计并开发了R语言(他们名字的首字母都是R)。

R语言发展过程中的重要事件:

2000年,R 1.0.0发布;

2005年,ggplot2包(2018.8—2019.8下载量超过1.3亿次);

2016年,Rstudio公司推出tidyverse包(数据科学当前最新R包);

2022年,R 4.1.2发布,目前CRAN上的R包数量为18985,近两年增速明显加快。

TIOBE是世界级的编程语言排行榜,能够反映编程语言的火热程度。这几年Python排名一路飙升,甚至冲到了第一。R语言属于统计分析语言,近年一直在10至20名之间徘徊,曾经短暂地冲到过第8名(2020年8月)。2022年12月,排名第11位,如图0.5所示。

图0.5 TIOBE最新编程语言排名

IEEE Spectrum发布的2021年度编程语言排行榜,从涵盖社交网站、开源代码网站和求职网站的8个信息源:CareerBuilder、GitHub、Google、Hacker News、IEEE、Reddit、Stack Overflow和Twitter,按照11个指标收集数据,最终得到了数十种编程语言流行度的整体排名,如图0.6所示。

图0.6 IEEE Spectrum 2021年度编程语言排行榜

2019年权威机构KDnuggets做过调研,调研结果显示数据科学领域最受欢迎的编程语言包括Python和R:

Python更全能,适合将来做程序员或在企业工作;

R语言更侧重数据统计分析,适合将来做科研学术。

R语言是用于统计分析、图形表示和报告的编程语言:

R语言是统计学家开发的,为统计计算、数据分析和可视化而设计;

R语言适合做数据处理和数据建模(数据预处理、数据探索性分析、识别数据隐含的模式、数据可视化)。

R语言的优势如下:

免费且开源,软件体量小,可以根据需要安装扩展包,兼容各种常见操作系统,有强大且活跃的社区;

专门为统计和数据分析开发的语言,有丰富的扩展包;

拥有顶尖水准的制图功能;

面向对象和函数,比Python简单易学。

在热门的机器学习领域,有足以媲美Python的sklearn机器学习库的R机器学习包mlr3versetidymodels(参见附录F)。

0.2.3 改变了R的人

Hadley Wickham博士是为统计应用领域做出过突出贡献的统计学家,被称为改变了R的人,图0.7所示的是著名的R语言专家—Hadley Wickham。

图0.7 R语言专家—Hadley Wickham

2019年,在国际统计学年会上,Hadley被授予COPSS奖,该奖项是国际统计学领域的最高奖项,被誉为“统计学界的诺贝尔奖”。他现在是Rstudio首席科学家,同时也是奥克兰大学、斯坦福大学和赖斯大学的统计系兼职教授。为了使数据科学更简洁、高效、有趣,他编写了大量知名的R包,主要包括下面这些。

数据科学相关的包tidyverse

ggplot2用于数据可视化。

dplyr用于数据操作。

tidyr用于数据清洗。

stringr用于处理字符串。

lubridate用于处理日期时间。

数据导入相关的包

readr用于读入.csv/fwf文件。

readxl用于读入.xls/.xlsx文件。

haven用于读入SAS/SPSS/Stata文件。

httr用于与Web交互的APIs。

rvest用于网页爬虫。

xml2用于读入XML文件。

R开发工具

devtools用于开发R包。

roxygen2用于生成内联(in-line)文档。

testthat用于单元测试。

pkgdown用于创建美观的包网页。

Hadley还出版过一系列图书,包括:

《R数据科学》(R for Data Science)介绍用R做数据科学的关键工具。

《ggplot2:数据分析与图形艺术》(ggplot2: Elegant Graphics for Data Analysis)展示如何使用ggplot2创建有助于理解数据的图形。

《高级R语言编程指南》(Advanced R)帮助你掌握R语言,以及使用R语言的深层技巧。

《R包开发》(R Packages)讲授良好的R软件项目实践,科学地创建R包:打包文件、生成文档、测试代码。

0.3 R语言编程思想

0.3.1 面向对象

R语言是一种基于对象的编程语言,即在定义类的基础上,创建与操作对象,而数值向量、函数、图形等都是对象。Python的一切皆为对象也适用于R语言。

a = 1L
class(a)
 
## [1] “integer”
 
b = 1:10
class(b)
 
## [1] “integer”
 
f = function(x) x + 1
class(f)
 
## [1] “function”

早期的R语言和底层R语言中的面向对象编程是通过泛型函数来实现的,以S3类、S4类为代表。新出现的R6类更适合用来实现通常所说的面向对象编程,包括类、属性、方法、继承、多态等概念。

面向对象的内容是R语言编程的高级内容,本书不做具体展开,只在附录中提供一个用R6类面向对象编程的简单示例。

0.3.2 面向函数

笼统地来说,R语言的主要工作就是对数据应用操作。这个操作就是函数,包括R语言自带的函数,各种扩展包里的函数以及自定义的函数。

所以,使用R语言的大部分时间都是在与函数打交道,学会了使用函数,R语言也就学会了一半,很多人说R简单易学,也是因此。

代码中的函数是用来实现某个功能。很多时候,我们使用R语言自带的或来自其他包中的现成函数就够了。

那么,如何找到并使用现成函数解决自己想要解决的问题?比如想做线性回归,通过查资料知道是用R语言自带的lm()函数实现。那么先通过以下命令打开该函数的帮助,如图0.8所示:

?lm

图0.8 R函数的帮助页面

执行“?函数名”(若函数来自扩展包需要事先加载包),在Rstudio右下角窗口打开函数帮助界面,一般至少包括如下内容:

函数描述(Description);

函数语法格式(Usage);

函数参数说明(Arguments);

函数返回值(Value);

函数示例(Examples)。

先阅读函数描述、参数说明、返回值,再调试示例,我们就能快速掌握该函数的使用方法。

函数包含很多参数,常用参数往往只是前几个。比如lm()的常用参数如下所示。

formula:设置线性回归公式形式“因变量~自变量+自变量”。

data:提供数据(框)。

接下来使用自带的mtcars数据集演示,按照函数参数要求的对象类型提供实参:

head(mtcars)
 
##                    mpg cyl disp  hp drat    wt  qsec vs am gear carb
## Mazda RX4         21.0   6  160 110 3.90 2.620 16.46  0  1    4    4
## Mazda RX4 Wag     21.0   6  160 110 3.90 2.875 17.02  0  1    4    4
## Datsun 710        22.8   4  108  93 3.85 2.320 18.61  1  1    4    1
## Hornet 4 Drive    21.4   6  258 110 3.08 3.215 19.44  1  0    3    1
## Hornet Sportabout 18.7   8  360 175 3.15 3.440 17.02  0  0    3    2
## Valiant           18.1   6  225 105 2.76 3.460 20.22  1  0    3    1
 
model = lm(mpg ~ disp, data = mtcars)
summary(model)      # 查看回归汇总结果
 
## 
## Call:
## lm(formula = mpg ~ disp, data = mtcars)
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -4.8922 -2.2022 -0.9631  1.6272  7.2305 
## 
## Coefficients:
##              Estimate Std. Error t value Pr(>|t|)    
## (Intercept) 29.599855   1.229720  24.070  < 2e-16 ***
## disp        -0.041215   0.004712  -8.747 9.38e-10 ***
## ---
## Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ‘ 1
## 
## Residual standard error: 3.251 on 30 degrees of freedom
## Multiple R-squared:  0.7183, Adjusted R-squared:  0.709 
## F-statistic: 76.51 on 1 and 30 DF,  p-value: 9.38e-10

所有的R函数,即使是陌生的,也都可以这样来使用。

编程中一种重要的思维就是函数式思维,包括自定义函数(把解决某问题的过程封装成函数)和泛函式编程(把函数依次应用到一系列的对象上)。

如果找不到现成的函数解决自己的问题,那就需要自定义函数,R自定义函数的基本语法如下:

函数名 = function(输入1, ..., 输入n) {
  ...
  return(输出)        # 若有多个输出, 需要打包成一个list
}

比如,想要计算很多圆的面积,就有必要把如何计算一个圆的面积定义成函数,需要输入半径,才能计算圆的面积:

AreaCircle = function(r) {
  S = pi * r * r
  return(S)
}

有了函数之后,再计算圆的面积,你只需要把输入给函数,它就能在内部进行相应处理,把你想要的输出结果返回给你。如果想批量计算圆的面积,按泛函式编程思维,只需要将该函数依次应用到一系列的半径上即可。

比如计算半径为5的圆的面积和批量计算半径为247的圆的面积,代码如下所示:

AreaCircle(5)
 
## [1] 78.53982
 
rs = c(2,4,7)
map_dbl(rs, AreaCircle)      # purrr包
 
## [1]  12.56637  50.26548 153.93804

定义函数就好比创造一个模具,调用函数就好比用模具批量生成产品。使用函数最大的好处就是将某个功能封装成模具,从而可以反复使用。这就避免了写大量重复的代码,程序的可读性也大大加强。

0.3.3 向量化编程

高级编程语言提倡向量化编程[2],说白了就是对一列数据、矩阵或多维数组中的数据同时做同样的操作,既提升程序效率又大大简化代码。

[2] 向量化编程中的向量,泛指向量、矩阵、多维数组。

向量化编程关键是要用整体考量的思维来思考和表示运算,这需要用到线性代数的知识,其实我觉得线性代数最有用的知识就是用向量、矩阵表示运算。

比如考虑n元一次线性方程组:

若从整体的角度来考量,可以引入矩阵和向量:

前面的n元一次线性方程组,可以向量化表示为:

可见,向量化表示大大简化了表达式。这放在编程中,就相当于本来用两层for循环才能表示的代码,简化为短短一行代码。

向量化编程其实并不难,关键是要转变思维方式:很多人学完C语言的“后遗症”,就是首先想到的总是使用for循环。想摆脱这种思维,可以调动头脑里的线性代数知识,尝试用向量、矩阵表示,长此以往,向量化编程思维就有了。

下面以计算决策树算法中的样本经验熵为例来演示向量化编程。

对于分类变量D表示第类数据所占的比例,则D的样本经验熵为:

其中,表示集合包含的元素个数。

在实际需求中,我们经常遇到要把数学式子变成代码,与前文所谈到的一样,首先你要看懂式子,用简单实例逐代码片段调试就能解决。

以著名的“西瓜书”(《机器学习》)中的西瓜分类数据中的因变量“好瓜”为例,表示是否为好瓜,取值为“是”和“否”:

y = c(rep(“是”, 8), rep(“否”, 9))
y
 
##  [1] “是” “是” “是” “是” “是” “是” “是” “是” “否” “否” “否” “否” “否”
## [14] “否” “否” “否” “否”

则D分为两类:为好瓜类,为坏瓜类。

从内到外先要计算,用向量化的思维同时计算,就是统计各分类的样本数,再除以总样本数:

table(y)                   # 计算各分类的频数, 得到向量
 
## y
## 否 是 
##  9  8
 
p = table(y) / length(y)   # 向量除以标量
p
 
## y
##        否        是 
## 0.5294118 0.4705882

继续代入公式计算,谨记R自带的函数天然就接受向量做输入参数:

log(p)                     # 向量取对数
 
## y
##         否         是 
## -0.6359888 -0.7537718
 
p * log(p)                 # 向量乘以向量, 对应元素做乘法
 
## y
##         否         是 
## -0.3366999 -0.3547161
 
- sum(p * log(p))          # 向量求和
 
## [1] 0.6914161

看着挺复杂的公式用向量化编程之后,核心代码只有两行:计算p和最后一行。这个实例虽然简单,但基本涉及所有常用的向量化操作:

向量与标量做运算;

向量与向量做四则运算;

把函数作用到向量。

拓展学习

读者如果想进一步了解R语言基础知识,建议大家阅读王敏杰编写的《数据科学中的R语言》。

读者如果想进一步了解R6面向对象,建议大家阅读Hadley编写的Advance R的第14章。

资源与支持

本书由异步社区出品,社区(https://www.epubit.com)为您提供相关资源和后续服务。

您还可以扫码右侧二维码, 关注【异步社区】微信公众号,回复“e60380”直接获取,同时可以获得异步社区15天VIP会员卡,近千本电子书免费畅读。

配套资源

本书提供配套资源(源代码 + 配套数据 + 配套课件),要获得以上配套资源,请在异步社区本书页面中单击,跳转到下载界面,按提示进行操作即可。注意:为保证购书读者的权益,该操作会给出相关提示,要求输入提取码进行验证。

勘误

作者和编辑尽最大努力来确保书中内容的准确性,但难免会存在疏漏。欢迎您将发现的问题反馈给我们,帮助我们提升图书的质量。

若读者发现错误,请登录异步社区,按书名搜索,进入本书页面,单击“提交勘误”,输入勘误信息,单击“提交”按钮即可。本书的作者和编辑会对读者所提交的勘误进行审核,确认并接受后,将赠予读者异步社区的100积分(积分可用于在异步社区兑换优惠券、样书或奖品)。

扫码关注本书

扫描下方二维码,读者会在异步社区微信服务号中看到本书信息及相关的服务提示。

与我们联系

我们的联系邮箱是contact@epubit.com.cn。

如果读者对本书有任何疑问或建议,请发邮件给我们,并请在邮件标题中注明本书书名,以便我们更高效地做出反馈。

如果读者有兴趣出版图书、录制教学视频,或者参与图书翻译、技术审校等工作,可以发邮件给我们;有意出版图书的作者也可以到异步社区在线投稿(直接访问www.epubit. com/selfpublish/submission即可)。

如果读者来自学校、培训机构或企业,想批量购买本书或异步社区出版的其他图书,也可以发邮件给我们。

如果读者在网上发现有针对异步社区出品图书的各种形式的盗版行为,包括对图书全部或部分内容的非授权传播,请将怀疑有侵权行为的链接发邮件给我们。这一举动是对作者权益的保护,也是我们持续为广大读者提供有价值的内容的动力之源。

关于异步社区和异步图书

“异步社区”是人民邮电出版社旗下IT专业图书社区,致力于出版精品IT图书和相关学习产品,为作译者提供优质出版服务。异步社区创办于2015年8月,提供大量精品IT图书和电子书,以及高品质技术文章和视频课程。更多详情请访问异步社区官网https://www.epubit.com。

“异步图书”是由异步社区编辑团队策划出版的精品IT专业图书的品牌,依托于人民邮电出版社近40年的计算机图书出版积累和专业编辑团队,相关图书在封面上印有异步图书的LOGO。异步图书的出版领域包括软件开发、大数据、人工智能、测试、前端、网络技术等。

异步社区

微信服务号

1 基础语法

本章介绍R语言基本语法,也是与其他编程语言相通的部分,包括搭建R环境、常用数据类型(数据结构)、控制结构(分支、循环)、自定义函数。

本章的目的是让读者打好R语言基本语法的基础,训练函数式编程思维基本语法编程元素语法规则,所有程序都是用它们组合出来的。函数式编程是训练数据思维的基础。函数式编程和数据思维是R语言编程的核心思维,这些也是学习本书和学习R语言的关键所在。

1.1 搭建R环境及常用操作

1.1.1 搭建R环境

打开R语言原生官网速度较慢,建议直接到R镜像站,目前国内有11个镜像站,我最近常用的是北京外国语大学的镜像站,其网址为:

https://mirrors.bfsu.edu.cn/CRAN/

读者可以根据自己的操作系统,下载相应的R语言版本即可(本书采用的是R-4.2.2)。在Windows系统安装R时,可根据系统选择32位或64位版本,建议取消勾选Message Translations。

建议安装在D盘,不要有中文路径,且路径名称不要有空格。

切记:若Windows系统用户名为中文,要先改成英文!

注意,最好保证计算机里有且只有一个版本的R,否则RStudio不会自动关联R,需要手动关联到其中一个R版本。

安装完成后,打开R,界面如图1.1所示。

图1.1 R 4.2.2软件界面

1.安装RStudio

不要直接使用R,建议使用更好用的R语言集成开发环境Rstudio。

下载并安装(或直接下载zip版解压)RStudio之后,RStudio将自动关联已安装的R。打开RStudio,操作界面各窗格及功能简介如图1.2所示。

图1.2 RStudio操作界面

2.一些必要的设置

切换安装扩展包的国内镜像源(任选其一),操作界面如图1.3所示。

图1.3 为RStudio设置国内镜像源

Tools -> Global Options… -> Options -> Packages,单击Change可修改镜像源,比如本书使用了北京外国语大学镜像源(Beijing Foreign Studies University)。

设置保存文件的默认编码方式为UTF-8,操作界面如图1.4所示。

图1.4 R Studio设置code编码

Tools -> Global Options… -> code -> Saving,在Default text encoding框,单击change,将相关设置修改为UTF-8

这样保存出来的各种R文件,都将采用UTF-8编码,这能够尽量避免含有中文字符的文件在其他计算机上打开显示乱码。

建议顺便再勾选Code -> Display下的Rainbow parentheses选项,这样代码中的配对括号将用不同颜色匹配。

另外,还建议在General -> Workspace菜单下取消勾选“Restore .RData into workspace at startup”,并将其下的“save workspace to .RData on exit:”改为“Never.”,这可以避免每次打开RStudio都加载之前的内存数据。

1.1.2 常用操作

1.安装包

扩展包(package),简称包。通常R包都来自CRAN,R包的审核比较严格,包的质量相对更有保障。建议使用命令安装R包,以下命令用于安装openxlsx包:

install.packages(“openxlsx”)

openxlsx为包名,必须要加引号(在R中,单双引号可通用)。

有些包不能自动安装,可以手动从网上搜索并下载.zip.tar.gz文件到本地,再手动安装(不建议手动安装),手动安装可依次单击Tools -> Install Packages,修改Install from,然后浏览安装,如图1.5所示。

图1.5 手动安装R包

手动安装包经常容易安装失败,通常是因为没有先安装该包的依赖包,故需要去包的网页查询其依赖包。若确定未安装对应的依赖包,则需要先安装它们。因为这往往又涉及依赖包的依赖包,所以最好不要手动安装包。另外,建议大家尽量用最新版本的R。

GitHub也是R包的重要来源,有些作者自己开发的R包只放在GitHub,也有很多CRAN R包的最新开发版位于GitHub,大家可以先安装devtoolsremotes包,再通过install_github()安装GitHub来源的包:

devtools::install_github(“tidyverse/dplyr”)  # 或者
remotes::install_github(“tidyverse/dplyr”)

在以上命令中,::的前面是包名,这是不单独加载包而使用包中函数的写法。

tidyverseGitHub用户名,dplyr为该用户的名为dplyrrepository(仓库),也是包名。此外,通过“包名::”前缀可以访问包的内部函数。注意,不是所有的仓库都是R包(含有DESCRIPTION文件是R包的标志)。

若因为网络等因素,导致直接从GitHub安装包失败,也可以将整个包文件夹从网页下载下来,解压缩到当前路径(或提供完整路径),再从本地安装:

install.packages(“dplyr-master”, repos=NULL, type=”source”)

另外,在R中生物信息领域自成一派,有专门的包,可以从bioconductor网站获取:

我们需要先安装BiocManager包,再用install()函数安装bioconductor来源的包:

BiocManager::install(“openxlsx”)

实用场景:R包默认安装在 .../R-4.x.x/library路径下。

你在自己的计算机上搭建好R语言环境,并安装好了很多常用包,然后又想到一台没有R环境、没有联网的计算机上复现代码。

具体方法:你只需要在那台计算机上安装相同版本的R软件,并安装到相同路径下,将新的library文件夹完全替换为自己计算机里的library文件夹即可1,这样运行起R代码跟自己计算机的效果没有任何区别。

[1]  可以用添加压缩包再解压的方式,这样速度能快一些。

2.加载包

library(openxlsx)

3.更新包

update.packages(“openxlsx”)
update.packages()                # 更新所有包

4.删除包

remove.packages(“openxlsx”)

5.获取或设置当前路径

getwd()
setwd(“D:/R-4.2.2/tests”)

特别注意

路径中的“\”必须用“/ ”或“\\”代替。

提示:关于更新R版本和更新包,笔者一般是紧跟最新R版本,顺便重新安装一遍所有包。为了省事,笔者是将所有自己常用包的安装命令(install.package(“...”))都放在一个R脚本中,并选择国内镜像,再全部运行即可。

6.赋值

R标准语法中赋值不是用 =,而是 <- 或 ->,代码如下:

x <- 1:10
x + 2
 
##  [1]  3  4  5  6  7  8  9 10 11 12

R也允许用“=”赋值,建议用更现代和简洁的“=”赋值。

在R中,为一行代码添加注释语句用 #。

7.基本运算

数学运算

+ - * /^(求幂)、%%(按模求余[2])、%/%(整除)。

[2]  可以对小数按模求余,例如5.4 %% 2.3结果为0.8

比较运算

><>=<===!=

identical(x,y)—判断两个对象是否严格相等;

all.equal(x,y)dplyr::near(x,y)—判断两个浮点数是否近似相等(误差为1.5e−8)。

0L == 0
 
## [1] TRUE
 
identical(0L, 0)
 
## [1] FALSE
 
sqrt(2)^2 == 2
 
## [1] FALSE
 
identical(sqrt(2)^2, 2)
 
## [1] FALSE
 
all.equal(sqrt(2)^2, 2)
 
## [1] TRUE
 
dplyr::near(sqrt(2)^2, 2)
 
## [1] TRUE

逻辑运算

| (或) & (与)! (非)xor()(异或)

&&||是短路运算,即遇到TRUE(FALSE)则返回TRUE(FALSE)而不继续往下计算;

&|是向量运算符,对向量中所有元素分别进行运算。

8.基本数据类型

R的基本数据类型

numeric— 数值型,又分为integer(整数型)和double(浮点型);

logical— 逻辑型,只有TRUEFALSE,或TF

character— 字符型,引号[3]括起来的若干字符。

[3]  在R中,单双引号通用。

R中用NA表示缺失值,NULL表示空值,NaN表示非数,Inf表示无穷大。

对于R中的大多数函数,NA具有“传染性”,即NA参与的运算,结果会变成NA。R自带的很多函数都提供na.rm参数设置,以便于设定计算时是否忽略缺失值。

特别要注意:判断x是否为NA,不是用x==NA,而是用is.na(x)

可用函数class(x) / typeof(x) / mode(x)查看对象x的类型。

在展现数据的细节方面,mode()性能最弱,class()性能一般,typeof()性能最强。

str(x)可以显示对象x的结构。

9.保存和载入数据

save(x, file = “data/dat.Rda”)
load(“data/dat.Rda”)

关于相对路径与绝对路径

编程中的文件路径,可以用绝对路径也可以用相对路径。

绝对路径是从盘符开始的完整路径,比如E:/R语言/data/a123.csv

相对路径是相对于当前路径的路径,因为通常操作的文件都是在当前路径,那么“从盘符到当前路径”这部分是大家所共有的,所以可以省略不写,只写从当前路径再往下的路径即可。比如,当前文件夹E:/R语言中有data文件夹,里面有数据文件a123.csv,要想访问到它的路径,只需写data/a123.csv

10.清屏和清除内存变量

按“Ctrl + L”组合键或单击命令窗口右上角的“小刷子”图标可对命令窗口清屏。

若要清除当前变量,使用以下命令:

rm(x)                       # 清除变量x
rm(list = ls(all = TRUE))   # 清除所有当前变量

注意:

单击Environment窗口的“小刷子”图标也可以清除所有当前变量。

11.获取帮助

学习编程语言最好的资料就是帮助

函数帮助

命令窗口执行:

?plot

则在help窗口打开plot()函数的帮助:包括函数来自哪个包、函数的描述、参数说明、更多解释、实例等。

在线帮助(需联网)

若想根据某算法的名字或关键词,搜索哪个包能实现该算法:

RSiteSearch(“network”)

其他主要网络资源

R官方镜像站(例如本书作者所使用的就是北京外国语大学的镜像站https://mirrors.bfsu. edu.cn/CRAN/),镜像站下的各种资源,建议读者去详细了解。比如,常用的是包的帮助文档:在镜像站,单击左侧的Packages,再单击“sort by name”,则出现所有可用的CRAN包列表。单击某个包名,则进入该包的介绍页,比如tidyverse包的官方介绍页如图1.6所示。

图1.6 tidyverse包的官方介绍页

Reference manual是参考手册,包含该包所有函数和自带数据集的说明,供查阅使用;Vignettes是包的作者写的使用文档,它是该包的最佳学习资料。

在使用R语言的过程中遇到各种问题,建议将报错信息设置为英文:Sys.setenv(LANGUAGE = "en"), 建议用bing国际版搜索报错信息,更容易找到答案。另外,GitHub是丰富的程序代码仓库,在bing搜索时,加上GitHub关键词,可能有意想不到的收获。

其他开放的R社区如下:

Stack overflow;

R-Bloggers;

Tidyverse;

Rstudio;

统计之都。

12.R Script与R Project

R脚本是单个可执行的R代码文件,后缀名为 .R,单击New File按钮,选择R Script,或使用快捷键(Ctrl + Shift + N),可以新建R脚本。

R脚本中都是可执行的R代码和注释,选中部分代码,单击Run按钮即可运行选中的代码。

R项目(Project)是完成某个项目或任务的一系列文件的合集(文件夹),包括数据文件、若干R脚本及其他附件,还包含一个 *.Rproj文件;

强烈建议读者使用 R 项目和相对路径,这样能系统地管理服务于共同目的的一系列文件,可以方便移动文件,而不需要做任何有关路径的代码修改就能成功运行。

创建R项目:单击Create a Project按钮,进入创建R Project向导,如图1.7所示。

图1.7 创建R Project向导

若在某个已存在的文件夹下创建项目,则选择Existing Directory;若需要新建文件夹创建项目,则选择New Directory。

创建完成后,在文件夹下出现一个 *.Rproj文件,双击它(关联RStudio打开),则进入该R项目,可以完成访问、编辑文件和运行脚本等操作。

13.R Markdown

后缀名为 .Rmd的交互式文档,是markdown语法与R脚本的结合,可以将可执行的R代码和不可执行的文字叙述,融为一个文件。

单击New File按钮,选择R Markdown即可创建,建议优先使用自带的模板和来自网络的现成模板。

R Markdown适合编写包含R语言代码的学习笔记、演示文档、论文等,可以生成docx pptxhtmlpdf等多种文档格式。更多有关R Markdown的内容将在第6章展开讨论。

此外,近期RStudio推出了新一代文档沟通工具Quarto,其用法与R Markdown基本一致,具体使用方法请参阅官方文档。

1.2 数据结构:向量、矩阵、多维数组

数据结构是为了便于存储不同类型的数据而设计的数据容器。学习数据结构,就是要把各个数据容器的特点、适合存取什么样的数据理解透彻,只有这样才能在实践中选择最佳的数据容器,数据容器选择得合适与否,直接关系到代码是否高效简洁,甚至关系到能否解决问题。

R中常用的数据结构可划分为:

同质数据类型(homogeneous),即所存储的一定是相同类型的元素,包括向量、矩阵、多维数组;

异质数据类型(heterogeneous),即可以存储不同类型的元素,这大大提高了存储的灵活性,但同时也降低了存储效率和运行效率,包括列表、数据框。

另外,还有字符串、日期时间数据、时间序列数据、空间地理数据等。

R中的数据结构还有一种从广义向量(可称之为序列)[4]的角度进行划分。

[4]  广义向量由一系列可以根据位置索引的元素构成,元素可以是复杂类型的,也可以是不同类型的。

原子向量:各个值都是同类型的,包括6种类型:logicalintegerdoublecharactercomplexraw,其中integerdouble也统称为numeric

列表:各个值可以是不同类型的,NULL表示空向量(长度为0的向量)。

向量有两个属性:type(类型)和length(长度);还能以属性的方式向向量中任意添加额外的metadata(元数据),属性可用来创建扩展向量,以执行一些新的操作。常用的扩展向量有:

基于整数型向量构建的因子;

基于数值型向量构建的日期和日期时间;

基于数值型向量构建的时间序列;

基于列表构建的数据框和tibble

列表是序列,从这个角度有助于理解purrr::map_*()系列的泛函式编程。

1.2.1 向量(一维数据)

向量是由一组相同类型的原子值构成的序列,可以是一组数值、一组逻辑值、一组字符串等。

常用的向量有数值向量、逻辑向量、字符向量。

1.数值向量

数值向量就是由数值组成的向量,单个数值是长度为1的数值向量,例如:

x = 1.5
x
 
## [1] 1.5

我们可以用numeric()创建一个全为0的指定长度的数值向量,如下所示:

numeric(10)
 
##  [1] 0 0 0 0 0 0 0 0 0 0

在R中经常用函数c()将多个对象合并到一起:

c(1, 2, 3, 4, 5)
 
## [1] 1 2 3 4 5
 
c(1, 2, c(3, 4, 5))   # 将多个数值向量合并成一个数值向量
 
## [1] 1 2 3 4 5

创建等差的数值向量,用:或者函数seq(),基本格式为:

seq(from, to, by, length.out, along.with, ...)

from设置首项(默认为1)。

to设置尾项。

by设置等差值(默认为1或 −1)。

length.out设置序列长度。

along.with以该参数的长度作为序列长度。

1:5                     # 同seq(5)或seq(1,5)
 
## [1] 1 2 3 4 5
 
seq(1, 10, 2)           # 从1开始, 到10结束, 步长为2
 
## [1] 1 3 5 7 9
 
seq(3, length.out=10)
 
##  [1]  3  4  5  6  7  8  9 10 11 12

创建重复的数值向量用函数rep(),基本格式为:

rep(x, times,length.out, each, ...)

x:为要重复的序列。

times:设置序列的重复次数。

length.out:设置所产生的序列的长度。

each:设置每个元素分别重复的次数(默认为1)。

x = 1:3
rep(x, 2)
 
## [1] 1 2 3 1 2 3
 
rep(x, each = 2)
 
## [1] 1 1 2 2 3 3
 
rep(x, c(2, 1, 2))              # 按照规则重复序列中的各元素
 
## [1] 1 1 2 3 3
 
rep(x, each = 2, length.out = 4)
 
## [1] 1 1 2 2
 
rep(x, times = 3, each = 2)
 
##  [1] 1 1 2 2 3 3 1 1 2 2 3 3 1 1 2 2 3 3

向量可以做“+、−、*、/”四则运算,即对应元素分别做运算的向量化运算。注意,将R中两个不同长度的向量做运算,短的会自动循环补齐以配合长的。

2:3 + 1:5
 
## [1] 3 5 5 7 7

2.逻辑向量

逻辑向量是由逻辑值(TRUEFALSE,或简写为TF)组成的向量。

对向量做逻辑运算,得到的结果是逻辑向量:

c(1, 2) > c(2, 1)         # 等价于c(1 > 2, 2 > 1)
 
## [1] FALSE  TRUE
 
c(2, 3) > c(1, 2, -1, 3)  # 等价于c(2 > 1, 3 > 2, 2 > -1, 3 > 3)
 
## [1]  TRUE  TRUE  TRUE FALSE

除了比较运算符外,还可以用 %in% 判断元素是否属于集合:

c(1, 4) %in% c(1, 2, 3)   # 左边向量每一个元素是否属于右边集合
 
## [1]  TRUE FALSE

在构造筛选行的条件时,经常有人用错语法,请参考以下规则。

%in% 表示属于,用于判断(左边)元素是否属于(右边)集合。

== 表示等于,用于判断(左边)元素是否等于(右边)元素。

3.字符向量

字符(串)向量,是由一组字符串组成的向量,在R中单引号和双引号都可以用来生成字符向量。

“hello, world!”
 
## [1] “hello, world!”
 
c(“Hello”, “World”)
 
## [1] “Hello” “World”
 
c(“Hello”, “World”) == “Hello, World”
 
## [1] FALSE FALSE

要想字符串中出现单引号或双引号,可以将单双引号错开,或者用转义字符“\”来做转义,用writeLines()函数输出纯字符串内容,如下所示:

‘Is “You” a Chinese name?’
# [1] “Is \”You\” a Chinese name?”
writeLines(“Is \”You\” a Chinese name?”)
# Is “You” a Chinese name?

R中还有不常用的复数向量、原始型(raw)向量。

4.访问向量子集

访问向量子集即访问向量的一些特定元素或者某个子集。注意,R中的索引是从1开始的。

使用元素的位置来访问,形式如下所示:

v1 = c(1, 2, 3, 4)
v1[2]                # 第2个元素
v1[2:4]              # 第2~4个元素
v1[-3]               # 除了第3个之外的元素

也可以访问任意位置的数值向量,但是注意索引不能既有正数又有负数:

v1[c(1,3)]
v1[c(1, 2, -3)]     # 报错

访问不存在的位置也是可以的,此时返回NA

v1[3:6]

使用逻辑向量来访问,输入与向量相同长度的逻辑向量,以此决定每一个元素是否要被获取:

v1[c(TRUE, FALSE, TRUE, FALSE)]

这可以引申为“根据条件访问向量子集”:

v1[v1 <= 2]       # 同v1[which(v1 <= 2)]或subset(v1, v1<=2)
v1[v1 ^ 2 - v1 >= 2]
which.max(v1)     # 返回向量v1中最大值所在的位置
which.min(v1)     # 返回向量v1中最小值所在的位置

5.为向量子集赋值,替换相应元素

为向量子集赋值,就是先访问到向量子集,再赋值。

v1[2] = 0
v1[2:4] = c(0, 1, 3)
v1[c(TRUE, FALSE, TRUE, FALSE)] = c(3, 2)
v1[v1 <= 2] = 0

注意,若对不存在的位置赋值,前面将用NA补齐:

v1[10] = 8
v1

6.为向量元素命名

你可以在创建向量的同时对其每个元素命名,代码如下:

x = c(a = 1, b = 2, c = 3)
x
 
## a b c 
## 1 2 3

命名后,就可以通过名字来访问向量元素,代码如下:

x[c(“a”, “c”)]
x[c(“a”, “a”, “c”)]    # 重复访问也是可以的
x[“d”]                 # 访问不存在的名字

获取向量元素的名字,代码如下:

names(x)      
 
## [1] “a” “b” “c”

更改向量元素的名字,代码如下:

names(x) = c(“x”, “y”, “z”)
x[“z”]
 
## z 
## 3

移除向量元素的名字,代码如下:

names(x) = NULL
x
 
## [1] 1 2 3

[ ] 与[[ ]] 的区别

[ ]可以提取对象的子集,[[ ]]可以提取对象内的元素。

二者的区别:以向量为例,可以将一个向量比作10盒糖果,你可以使用[ ]获取其中的3盒糖果,使用[[ ]]打开盒子并从中取出一颗糖果。

对于未对元素命名的向量,使用[ ][[ ]]取出一个元素会产生相同的结果。但对于已对元素命名的向量,二者会产生不同的结果,如下所示:

x = c(a = 1, b = 2, c = 3)
x[“a”]          # 取出标签为”a”的糖果盒
 
## a 
## 1
 
x[[“a”]]        # 取出标签为”a”的糖果盒里的糖果
 
## [1] 1

由于[[ ]]只能用于提取出一个元素,不适用于提取多个元素的情况,因此[[ ]]不能用于负整数,负整数意味着提取除特定位置之外的所有元素。

使用含有不存在的位置或名称来创建向量子集时将会产生缺失值。但当使用[[ ]]提取一个位置超出范围或者对应名称不存在的元素时,该命令将会无法运行并产生错误信息。

例如,以下三个语句会报错:

x[[c(1, 2)]]
x[[-1]]
x[[“d”]]

7.对向量排序

向量排序函数sort(),基本格式为:

sort(x, decreasing, na.last, ...)

x:为排序对象(数值型或字符型)。

decreasing:默认为FALSE即升序,TRUE为降序。

na.last:默认为FALSE,若为TRUE,则将向量中的NA值放到序列末尾。

函数order()可以返回元素排好序的索引,以其结果作为索引访问元素,正好是排好序的向量。

函数rank()的返回值是该向量中对应元素的“排名”,参数“ties. method”用于设置相同值的处理方法。

x = c(1,5,8,2,9,7,4)
sort(x)
 
## [1] 1 2 4 5 7 8 9
 
order(x)     # 默认升序,排名第2的元素在原向量的在4个位置
 
## [1] 1 4 7 2 6 3 5
 
x[order(x)]  # 同sort(x)
 
## [1] 1 2 4 5 7 8 9
 
rank(x)      # 默认升序,第2个元素排在第4位
 
## [1] 1 4 6 2 7 5 3

函数rev()可将序列进行反转,即把1,2,3变成3,2,1

1.2.2 矩阵(二维数据)

矩阵是用两个维度表示和访问的向量。因此,适用于向量的性质和方法大多也适用于矩阵,矩阵也要求元素是同一类型,如数值矩阵、逻辑矩阵等。

1.创建矩阵

函数matrix()将一个向量创建为矩阵,其基本格式为:

matrix(x, nrow, ncol, byrow, dimnames, ...)

x:为数据向量作为矩阵的元素;

nrow:设定行数;

ncol:设定列数;

byrow:设置是否按行填充,默认为FALSE(按列填充);

dimnames:用字符型向量表示矩阵的行名和列名。

matrix(c(1, 2, 3, 
         4, 5, 6, 
         7, 8, 9), nrow = 3, byrow = FALSE)
 
##      [,1] [,2] [,3]
## [1,]    1    4    7
## [2,]    2    5    8
## [3,]    3    6    9
 
matrix(c(1, 2, 3, 
         4, 5, 6, 
         7, 8, 9), nrow = 3, byrow = TRUE)
 
##      [,1] [,2] [,3]
## [1,]    1    2    3
## [2,]    4    5    6
## [3,]    7    8    9

为矩阵的行列命名:

matrix(1:9, nrow = 3, byrow = TRUE, 
       dimnames = list(c("r1","r2","r3"), c("c1","c2","c3"))) 
 
##    c1 c2 c3
## r1  1  2  3
## r2  4  5  6
## r3  7  8  9

也可以创建后再命名:

m1 = matrix(c(1, 2, 3, 4, 5, 6, 7, 8, 9), ncol = 3)
rownames(m1) = c(“r1”, “r2”, “r3”)
colnames(m1) = c(“c1”, “c2”, “c3”)

特殊矩阵:

diag(1:4, nrow = 4)      # 对角矩阵
 
##      [,1] [,2] [,3] [,4]
## [1,]    1    0    0    0
## [2,]    0    2    0    0
## [3,]    0    0    3    0
## [4,]    0    0    0    4

函数as.vector()可将矩阵转化为向量,其元素是按列读取的。

2.访问矩阵子集

矩阵是用两个维度表示和访问的向量,可以用一个二维存取器[,]来访问,这类似于构建向量子集时用的一维存取器[]

我们可以为每个维度提供一个向量来确定一个矩阵的子集。方括号中的第1个参数是行选择器,第2个参数是列选择器。与构建向量子集一样,我们可以在两个维度中使用数值向量、逻辑向量和字符向量。

m1[1,2]              # 提取第1行,第2列的单个元素
m1[1:2, 2:4]         # 提取第1至2行,第2至4列的元素
m1[c(“r1”,”r3”), c(“c1”,”c3”)]  # 提取行名为r1和r3,列名为c1和c3的元素

若一个维度空缺,则选出该维度的所有元素:

m1[1,]              # 提取第1行,所有列元素
m1[,2:4]            # 提取所有行,第2至4列的元素

负数表示在构建矩阵子集时可排除该位置,这和向量中的用法一致:

m1[-1,]             # 提取除了第1行之外的所有元素
m1[,-c(2,4)]        # 提取除了第2和4列之外的所有元素

注意,矩阵是用两个维度表示和访问的向量,但它本质上仍然是向量。因此,向量的一维存取器也可以用来构建矩阵子集:

m1[3:7]
 
## [1] 3 4 5 6 7

由于向量只包含相同类型的元素,矩阵也是如此,因此它们的操作方式也相似。若输入一个不等式,则返回同样大小的逻辑矩阵:

m1 > 3
 
##       c1   c2   c3
## r1 FALSE TRUE TRUE
## r2 FALSE TRUE TRUE
## r3 FALSE TRUE TRUE

根据逻辑矩阵可以选择矩阵元素或赋值:

m1[m1 > 3]   # 注意选出来的结果是向量
 
## [1] 4 5 6 7 8 9

3.矩阵运算

A+BA-BA*BA/B:矩阵四则运算要求矩阵同型,类似MATLAB中的点运算,分别将对应位置的元素做四则运算;

A %*% B:矩阵乘法要求矩阵A的列数等于矩阵B的行数。

1.2.3 多维数组(多维数据)

具体来说,多维数组就是一个维度更高(通常大于2)、可访问的向量,是向量/矩阵向更高维度的自然推广。多维数组也要求元素是同一类型。

1.创建多维数组

用函数array()将一个向量创建为多维数组,基本格式为:

array(x, dim, dimnames, ...)

x:为数据向量作为多维数组的元素。

dim:设置多维数组各维度的维数。

dimnames:设置多维数组各维度的名称。

a1 = array(1:24, dim = c(3, 4, 2))
a1
 
##,, 1
## 
##      [,1] [,2] [,3] [,4]
## [1,]    1    4    7   10
## [2,]    2    5    8   11
## [3,]    3    6    9   12
## 
##,, 2
## 
##      [,1] [,2] [,3] [,4]
## [1,]   13   16   19   22
## [2,]   14   17   20   23
## [3,]   15   18   21   24

也可以在创建数组时对每个维度进行命名:

a1 = array(1:24, dim = c(3, 4, 2), 
           dimnames=list(c("r1","r2","r3"), 
                         c("c1","c2","c3","c4"), c("k1","k2")))

或者创建之后再命名[5]

[5]  下方代码中的list用于创建列表(参见1.3节)。

a1 = array(1:24, dim = c(3, 4, 2))
dimnames(a1) = list(c(“r1”,”r2”,”r3”), 
                    c("c1","c2","c3","c4"), c("k1","k2"))

2.访问多维数组子集

第3个维度姑且称为“页”:

a1[2,4,2]           # 提取第2行,第4列,第2页的元素
a1[“r2”,”c4”,”k2”]  # 提取第r2行,第c4列,第k2页的元素
a1[1,2:4,1:2]       # 提取第1行,第2至4列,第1至2页的元素
a1[,,2]             # 提取第2页的所有元素
dim(a1)             # 返回多维数组a1的各维度的维数

在想象多维数组时,为了便于形象地理解,可以将其维度依次想象成与“书”相关的概念:行、列、页、本、层、架、室……

1.3 数据结构:列表、数据框、因子

1.3.1 列表

列表(list)可以包含不同类型的对象,甚至可以包括其他列表。列表的灵活性使得它非常有用。

例如,用R拟合一个线性回归模型,其返回结果就是一个列表,其中包含了线性回归的详细结果,如线性回归系数(数值向量)、残差(数值向量)、QR分解(包含一个矩阵和其他对象的列表)等。因为这些结果全都被打包到一个列表中,就可以很方便地提取所需信息,而无须每次调用不同的函数。

列表最大的好处就是能够将多个不同类型的对象打包到一起,以便可以根据位置和名字访问它们。

1.创建列表

可以用函数list()创建列表。不同类型的对象可以放入同一个列表中。

例如,创建一个列表,包含3个成分:一个单元素的数值向量、一个两元素的逻辑向量和一个长度为3的字符向量:

l0 = list(1, c(TRUE, FALSE), c(“a”, “b”, “c”))
l0
 
## [[1]]
## [1] 1
## 
## [[2]]
## [1]  TRUE FALSE
## 
## [[3]]
## [1] “a” “b” “c”

可以在创建列表时,为列表的每个成分指定名字:

l1 = list(A = 1, B = c(TRUE, FALSE), C = c(“a”, “b”, “c”))
l1
 
## $A
## [1] 1
## 
## $B
## [1]  TRUE FALSE
## 
## $C
## [1] “a” “b” “c”

也可以创建列表后再对列表成分命名或修改名字:

names(l1) = NULL      # 移除列表成分的名字
names(l1) = c(“x”,”y”,”z”)

2.从列表中提取成分的内容

提取列表中某成分下的内容,最常用的方法是用$,通过成分名字来提取该成分下的内容:

l1$y
l1$m                 # 访问不存在的成分m, 将会返回NULL

也可以用[[n]]来提取列表第n个成分的内容,n也可以换成成分的名字:

l1[[2]]              # 同l1[[“y”]]

[[]]提取列表中某个成分的内容则更加灵活,可用在函数调用中,通过参数来传递成分的名字或索引:

p = “y”              #想要提取其内容的成分名字
l1[[p]]

3.提取列表子集

R语言也经常需要从列表中提取多个成分及其内容,由这些成分组成的列表构成了原列表的一个子集。

就像提取向量和矩阵的子集一样,提取一个列表子集是用[],可以取出列表中的一些成分,作为一个新的列表。

[]中可以用字符向量表示成分名字,用数值向量表示成分位置,或用逻辑向量指定是否选择来取出列表成分。

l1[“x”]                    # 同l1[1]
l1[c(“x”, “z”)]            # 同l1[c(1, 3)], l1[c(TRUE, FALSE, TRUE)]

[]提取若干成分时,返回列表的子集,还是一个列表;用[[ ]]提取单个成分的元素,返回的是对应成分的元素。

总之,[]用于提取对象的子集,类型仍是该对象;[[]]用于提取对象的内容(即下一级元素)。

4.为列表的成分赋值

即先访问(提取)到列表的成分,再赋以相应的值。注意,若给一个不存在的成分赋值,列表会自动地在对应名称或位置下增加一个新成分。

l1$x = 0   # 将列表的成分x赋值为0

也可以同时给多个列表成分赋值:

l1[c(“x”, “y”)] = list(x = “new value for y”, y = c(3, 1))

若要移除列表中的某些成分,只需赋值为NULL

l1[c(“z”, “m”)] = NULL

5.列表函数

用函数as.list()可将向量转换成列表:

l2 = as.list(c(a = 1, b = 2))
l2
 
## $a
## [1] 1
## 
## $b
## [1] 2

通过去列表化函数unlist()可将一个列表打破成分界线,强制转换成一个向量[6]

[6]  若列表的成分具有不同类型,则自动向下兼容到同一类型。

unlist(l2)
 
## a b 
## 1 2

为了方便操作列表,tidyverse系列中的purrr包提供了一系列列表相关的函数,建议读者查阅并使用。

pluck():同[[]]提取列表中的元素。

keep():保留满足条件的元素。

discard():删除满足条件的元素。

compact():删除列表中的空元素。

append():在列表末尾增加元素。

flatten():摊平列表(只摊平一层)。

1.3.2 数据框(数据表)

R语言中用于统计分析的样本数据,都是按数据框类型操作的。

数据框是指有若干行和列的数据集,它与矩阵类似,但并不要求所有列都是相同的类型。本质上讲,数据框就是一个列表,它的每个成分都是一个向量,并且长度相同,以表格的形式展现。总之,数据框是由列向量组成、有着矩阵形式的列表

数据框与常见的数据表是一致的:每一列代表一个变量属性,每一行代表一条样本数据。以表1.1所示的数据表为例。

表1.1 数据表示例

Name

Gender

Age

Major

Ken

Male

24

Finance

Ashley

Female

25

Statistics

Jennifer

Female

23

Computer Science

R中自带的数据框是data.frame,建议改用更现代的数据框:tibble[7]

[7]  读者若习惯用R自带的data.frame,只需要换个名字,将tibble改为data.frame即可。

Hadley在tibble包中引入了一种tibble数据框,以代替data.frame,而且tidyverse包都是基于tibble数据框的。

tibble对比data.frame的优势如下所示。

tibble()data.frame()做的更少:不改变输入变量的类型(注:R 4.0.0之前默认将字符串转化为因子),不会改变变量名,不会创建行名。

tibble对象的列名可以是R中的“非法名”:非字母开头、包含空格,但定义和使用变量时都需要用反引号`括起来。

tibble在输出时不自动显示所有行,避免数据框较大时显示出很多内容。

[]选取列子集时,即使只选取一列,返回结果仍是tibble,而不自动简化为向量。

1.创建数据框

tibble()根据若干列向量创建tibble

library(tidyverse)        # 或tibble
persons = tibble(
  Name = c(«Ken», «Ashley», «Jennifer»),
  Gender = c(«Male», «Female», «Female»),
  Age = c(24, 25, 23),
  Major = c(«Finance», «Statistics», «Computer Science»))
persons
 
## # A tibble: 3 x 4
##   Name     Gender   Age Major           
##   <chr>    <chr>  <dbl> <chr>           
## 1 Ken      Male      24 Finance         
## 2 Ashley   Female    25 Statistics      
## 3 Jennifer Female    23 Computer Science

tribble()通过按行录入数据的方式创建tibble

tribble(
  ~Name, ~Gender, ~Age, ~Major,
  «Ken», «Male», 24, «Finance»,
  «Ashley», «Female», 25, «Statistics»,
  «Jennifer», «Female», 23, «Computer Science»)

as_tibble()可将data.framematrix这种各成分等长度的list转换为tibble

将不等长的列表转化为数据框:

a = list(A = c(1, 3, 4), B = letters[1:4])a ## $A## [1] 1 3 4## ## $B## [1] “a” “b” “c” “d” # lengths()获取list中每个成分的长度map_dfc(a, `length<-`, max(lengths(a))) # map循环参阅1.6.2节 ## # A tibble: 4 x 2## A B ## <dbl> <chr>## 1 1 a ## 2 3 b ## 3 4 c ## 4 NA d

数据框既是列表的特例,也是广义的矩阵,因此访问这两类对象的方式都适用于数据框。例如与矩阵类似,对数据框的各列重命名,代码如下:

df = tibble(id = 1:4, 
            level = c(0, 2, 1, -1), 
            score = c(0.5, 0.2, 0.1, 0.5))
names(df) = c(“id”, “x”, “y”)
df
 
## # A tibble: 4 x 3
##      id     x     y
##   <int> <dbl> <dbl>
## 1     1     0   0.5
## 2     2     2   0.2
## 3     3     1   0.1
## 4     4    -1   0.5

2.提取数据框的元素、子集

数据框是由列向量组成、有着矩阵形式的列表,可以用两种操作方式来访问数据框的元素和子集。

(1)以列表方式提取数据框的元素、子集

若把数据框看作由向量组成的列表,则可以沿用列表的操作方式来提取元素或构建子集。例如,可以用 $ 按列名来提取某一列的值,或者用[[]]按照位置或列名提取。

例如,提取列名为x的值,并得到向量:

df$x                  # 同df[[“x”]], df[[2]]
 
## [1]  0  2  1 -1

以列表形式构建子集完全适用于数据框,同时也会生成一个新的数据框。提取子集的操作符[]允许用数值向量表示列的位置,用字符向量表示列名,或用逻辑向量指定是否选择。

例如,提取数据框的一列或多列,可以得到子数据框:

df[1]                 # 提取第1列, 同df[“id”]
 
## # A tibble: 4 x 1
##      id
##   <int>
## 1     1
## 2     2
## 3     3
## 4     4
 
df[1:2]               # 同df[c(“id”,”x”)], df[c(TRUE,TRUE,FALSE)]
 
## # A tibble: 4 x 2
##      id     x
##   <int> <dbl>
## 1     1     0
## 2     2     2
## 3     3     1
## 4     4    -1
(2)以矩阵方式提取数据框的元素、子集

以列表形式操作并不支持行选择,以矩阵形式操作则更加灵活。若将数据框看作矩阵,其二维形式的存取器可以很容易地获取一个子集的元素,同时支持列选择和行选择。

换句话说,可以使用[i, j]指定行或列来提取数据框子集,[,]内可以是数值向量、字符向量或者逻辑向量。

若行选择器为空,则只选择列(所有行):

df[, “x”]
 
## # A tibble: 4 x 1
##       x
##   <dbl>
## 1     0
## 2     2
## 3     1
## 4    -1
 
df[, c(“x”,”y”)]   # 同df[,2:3]
 
## # A tibble: 4 x 2
##       x     y
##   <dbl> <dbl>
## 1     0   0.5
## 2     2   0.2
## 3     1   0.1
## 4    -1   0.5

若列选择器为空,则只选择行(所有列):

df[c(1,3),]
 
## # A tibble: 2 x 3
##      id     x     y
##   <int> <dbl> <dbl>
## 1     1     0   0.5
## 2     3     1   0.1

同时选择行和列:

df[1:3, c(“id”,”y”)]
 
## # A tibble: 3 x 2
##      id     y
##   <int> <dbl>
## 1     1   0.5
## 2     2   0.2
## 3     3   0.1

根据条件筛选数据。例如用y >= 0.5筛选df的行,并选择idy两列:

df[df$y >= 0.5, c(“id”,”y”)]
 
## # A tibble: 2 x 2
##      id     y
##   <int> <dbl>
## 1     1   0.5
## 2     4   0.5

按列名属于集合 {x, y, w} 来筛选df的列,并选择前两行:

ind = names(df) %in% c(“x”,”y”,”w”)
df[1:2, ind]
 
## # A tibble: 2 x 2
##       x     y
##   <dbl> <dbl>
## 1     0   0.5
## 2     2   0.2

3.给数据框赋值

给数据框赋值就是选择要赋值的位置,再准备好同样大小且格式匹配的数据,赋值给那些位置即可,同样有列表方式和矩阵方式。

(1)以列表方式给数据框赋值

$[[ ]]对数据框的某列赋值

df$y = c(0.6,0.3,0.2,0.4)   # 同df[[“y”]] = c(0.6,0.3,0.2,0.4)

利用现有列,创建(计算)新列:

df$z = df$x + df$y 
df
 
## # A tibble: 4 x 4
##      id     x     y     z
##   <int> <dbl> <dbl> <dbl>
## 1     1     0   0.5   0.5
## 2     2     2   0.2   2.2
## 3     3     1   0.1   1.1
## 4     4    -1   0.5  -0.5
 
df$z = as.character(df$z)   # 转换列的类型
df
 
## # A tibble: 4 x 4
##      id     x     y z    
##   <int> <dbl> <dbl> <chr>
## 1     1     0   0.5 0.5  
## 2     2     2   0.2 2.2  
## 3     3     1   0.1 1.1  
## 4     4    -1   0.5 -0.5

[]可以对数据框的一列或多列进行赋值:

df[“y”] = c(0.8,0.5,0.2,0.4)
df[c(“x”, “y”)] = list(c(1,2,1,0), c(0.1,0.2,0.3,0.4))
(2)以矩阵方式给数据框赋值

以列表方式对数据框进行赋值时,也是只能访问列。若需要更加灵活地进行赋值操作,可以通过矩阵方式进行。

df[1:3,”y”] = c(-1,0,1)
df[1:2,c(“x”,”y”)] = list(c(0,0), c(0.9,1.0))

4.一些有用的函数

把函数str()glimpse()作用在R对象上,可以显示该对象的结构:

str(persons)
 
## tibble [3 x 4] (S3: tbl_df/tbl/data.frame)
##  $ Name  : chr [1:3] “Ken” “Ashley” “Jennifer”
##  $ Gender: chr [1:3] “Male” “Female” “Female”
##  $ Age   : num [1:3] 24 25 23
##  $ Major : chr [1:3] “Finance” “Statistics” “Computer Science”

summary()作用在数据框或列表上,将生成各列或各成分的汇总信息:

summary(persons)
 
##      Name              Gender               Age      
##  Length:3           Length:3           Min.   :23.0  
##  Class :character   Class :character   1st Qu.:23.5  
##  Mode  :character   Mode  :character   Median :24.0  
##                                        Mean   :24.0  
##                                        3rd Qu.:24.5  
##                                        Max.   :25.0  
##     Major          
##  Length:3          
##  Class :character  
##  Mode  :character  
##                    
##                    
## 

我们经常需要将多个数据框(或矩阵)按行或按列进行合并。用函数rbind()增加行(样本数据),要求宽度(列数)相同;用cbind()函数增加列(属性变量),要求高度(行数)相同。

例如,向persons数据框中添加一个新记录:

rbind(persons, 
      tibble(Name = «John», Gender = «Male», 
             Age = 25, Major = «Statistics»))
 
## # A tibble: 4 x 4
##   Name     Gender   Age Major           
##   <chr>    <chr>  <dbl> <chr>           
## 1 Ken      Male      24 Finance         
## 2 Ashley   Female    25 Statistics      
## 3 Jennifer Female    23 Computer Science
## 4 John     Male      25 Statistics

persons数据框中添加两个新列,分别表示每个人是否已注册及其手头的项目数量:

cbind(persons, Registered = c(TRUE, TRUE, FALSE), Projects = c(3, 2, 3))
 
##       Name Gender Age            Major Registered Projects
## 1      Ken   Male  24          Finance       TRUE        3
## 2   Ashley Female  25       Statistics       TRUE        2
## 3 Jennifer Female  23 Computer Science      FALSE        3

rbind()cbind()不会修改原始数据,而是生成一个添加了行或列的新数据框。

函数expand.grid()可生成多个属性水平值的所有组合(笛卡儿积)形式的数据框:

expand.grid(type = c(“A”,”B”), class = c(“M”,”L”,”XL”))
 
##   type class
## 1    A     M
## 2    B     M
## 3    A     L
## 4    B     L
## 5    A    XL
## 6    B    XL

1.3.3 因子

数据(变量)可划分为:定量数据(数值型)、定性数据(分类型),定性数据又分为名义型(无好坏顺序之分,如性别)、有序型(有好坏顺序之分,如疗效)。

R提供了因子(factor)这一数据结构(容器),专门用来存放名义型和有序型的分类变量。因子本质上是一个带有水平(level)属性的整数向量,其中“水平”是指事前确定可能取值的有限集合。例如,性别有两个水平属性:男、女。

直接用字符向量也可以表示分类变量,但它只有字母顺序,不能规定想要的顺序,也不能表达有序分类变量。所以,有必要把字符型的分类变量转化为因子型,这更便于对其做后续描述汇总、可视化、建模等。

1.创建与使用因子

函数factor()用来创建因子,基本格式为:

factor(x, levels, labels, ordered, ...)

x:为创建因子的数据向量。

levels:指定因子的各水平值,默认为x中不重复的所有值。

labels:设置各水平名称(前缀),与水平名称一一对应。

ordered:设置是否对因子水平排序,默认FALSE为无序因子,TRUE为有序因子。

该函数还包含参数exclude:指定有哪些水平是不需要的(设为NA);nmax用于设定水平数的上限。

若不指定参数levels,则因子水平默认按字母顺序。

比如,现有6个人的按等级划分的成绩数据,先以字符向量创建,并对其排序:

x = c(“优”, “中”, “良”, “优”, “良”, “良”)     # 字符向量
x
 
## [1] “优” “中” “良” “优” “良” “良”
 
sort(x)                                       # 排序是按字母顺序
 
## [1] “良” “良” “良” “优” “优” “中”

它的顺序只能是字母顺序,如果想规定顺序:中、良、优,正确的做法就是创建因子,用levels指定想要的顺序:

x1 = factor(x, levels = c(“中”, “良”, “优”))  # 转化因子型
x1
 
## [1]优 中 良 优 良 良
## Levels: 中 良 优
 
as.numeric(x1)                                # x的存储形式: 整数向量
 
## [1] 3 1 2 3 2 2

注意,不能直接将因子数据当字符型操作,需要用as.character()转化。

转化为因子型后,数据向量显示出来(外在表现)与原来是一样的,但这些数据的内在存储已经变了。因子的内在存储与外在表现如图1.8所示。因子型是以整数向量存储的,将各水平值按照规定的顺序分别对应到整数,将原向量的各个值分别用相应的整数存储,输出和使用的时候再换回对应的水平值。整数是有顺序的,这样就相当于在不改变原数据的前提下规定了顺序,同时也节省了存储空间。

图1.8 因子的内在存储与外在表现

注意,标签(labels)是因子水平(levels)的别名。

变成因子型后,无论是排序、统计频数、绘图等,都有了顺序:

sort(x1)
 
## [1]中 良 良 良 优 优
## Levels: 中 良 优
 
table(x1)
 
## x1
## 中 良 优 
##  1  3  2
 
ggplot(tibble(x1), aes(x1)) +
  geom_bar()

所生成的条形图结果如图1.9所示,x轴的条形顺序是想要的中、良、优。

图1.9 用因子控制条形顺序

levels()函数可以访问或修改因子的水平值,这也将改变数据的外在表现:

levels(x1) = c(“Fair”, “Good”, “Excellent”)    # 修改因子水平
x1
 
## [1] Excellent Fair      Good      Excellent Good      Good     
## Levels: Fair Good Excellent

有时候你可能更希望让水平的顺序与其在数据集中首次出现的次序相匹配,这时可以设置参数levels = unique(x)

转化为因子型的另一个好处是,可以“识错”:因子数据只认识出现在水平值中的值,对于未出现在水平值中的值将被识别为NA

很多人将因子固有的顺序与有序因子混淆,二者不是一回事:上述反复提到的顺序,可称为因子固有的顺序,正是有了它,才能方便地按想要的顺序进行排序、统计频数、绘图等;而无序因子与有序因子,是与变量本身的数据类型相对应的,名义型(无顺序好坏之分的分类变量)用无序因子存放,有序型(有顺序好坏之分的分类变量)用有序因子存放,该区分是用于不同类型的数据,建模时适用不同的模型。

示例的成绩数据是有好坏之分的,因此创建为有序因子:

x2 = factor(x, levels = c(“中”, “良”, “优”), ordered = TRUE)
x2
 
## [1]优 中 良 优 良 良
## Levels: 中 < 良 < 优

如果对x2做排序、统计频数、绘图,你会发现与使用无序因子时没有任何区别,它们的区别体现在对其建模时所适用的模型不同。

2.有用函数

函数table()可以统计因子各水平的出现次数(频数),也可以统计向量中每个不同元素的出现次数,其返回结果为命名向量。

table(x)
 
## x
## 良 优 中 
##  3  2  1

函数cut(),用来做连续变量离散化:将数值向量切分为若干区间段,并返回因子。基本格式为:

cut(x, breaks, labels, ...)

x:要切分的数值向量。

breaks:切分的界限值构成的向量,或表示切分段数的整数。

该函数还包含参数right用于设置区间段是否左开右闭,include.lowest用于设置是否包含下界,ordered_result用于设置是否对结果因子排序。

Age = c(23,15,36,47,65,53)
cut(Age, breaks = c(0,18,45,100), 
    labels = c(«Young»,»Middle»,»Old»))
 
## [1] Middle Young  Middle Old    Old    Old   
## Levels: Young Middle Old

函数gl()用来生成有规律的水平值组合因子。对于多因素试验设计,用该函数可以生成多个因素完全组合,基本格式为:

gl(n, k, length, labels, ordered, ...)

n:为因子水平个数。

k:为同一因子水平连续重复次数。

length:为总的元素个数,默认为n*k,若不够则自动重复。

labels:设置因子水平值。

ordered:设置是否为有序,默认为FALSE

tibble(
  Sex = gl(2, 3, length = 12, labels = c(«男”,”女”)),
  Class = gl(3, 2, length = 12, labels = c(«甲”,”乙”,”丙”)),
  Score = gl(4, 3, length = 12, labels = c(«优”,”良”,”中”, “及格”)))
 
## # A tibble: 12 x 3
##   Sex   Class Score
##   <fct> <fct> <fct>
## 1男    甲    优   
## 2男    甲    优   
## 3男    乙    优   
## 4女    乙    良   
## 5女    丙    良   
## 6女    丙    良   
## # ... with 6 more rows

3.forcats包

tidyverse系列中的forcats包是专门为处理因子型数据而设计的,forcats包提供了一系列操作因子的方便函数。

as_factor():转化为因子,默认按水平值的出现顺序。

fct_count():计算因子各水平频数、占比,可按频数排序。

fct_c():合并多个因子的水平。

改变因子水平的顺序。

fct_relevel():手动对水平值重新排序。

fct_infreq():按高频优先排序。

fct_inorder():按水平值出现的顺序排序。

fct_rev():将顺序反转。

fct_reorder():根据其他变量或函数结果排序(绘图时有用)。

修改水平。

fct_recode():对水平值逐个重编码。

fct_collapse():按自定义方式合并水平。

fct_lump_*():将多个频数小的水平合并为其他。

fct_other():将保留之外或丢弃的水平合并为其他。

增加或删除水平。

fct_drop():删除若干水平。

fct_expand:增加若干水平。

fct_explicit_na():为NA设置水平。

读者需要明白这样一个基本逻辑:操作因子是操作一个向量,该向量更多的时候是以数据框的一列的形式存在的。我们来演示一下更常用的操作数据框中的因子列的方法,这会涉及数据操作和绘图的语法,这部分知识在第2~3章才会讲到。你只需要知道大意并理解因子操作部分即可。

mpg列是汽车数据集,class列是分类变量车型,先统计各种车型的频数,共有7类;对该列做因子合并,合并为5类+Other类,再统计频数,这里将频数少的类合并为Other类:

count(mpg, class)
 
## # A tibble: 7 x 2
##   class          n
##   <chr>      <int>
## 1 2seater        5
## 2 compact       47
## 3 midsize       41
## 4 minivan       11
## 5 pickup        33
## 6 subcompact    35
## # ... with 1 more row
 
mpg1 = mpg %>% 
  mutate(class = fct_lump(class, n = 5)) 
count(mpg1, class)
 
## # A tibble: 6 x 2
##   class          n
##   <fct>      <int>
## 1 compact       47
## 2 midsize       41
## 3 pickup        33
## 4 subcompact    35
## 5 suv           62
## 6 Other         16

若直接对class各类绘制条形图,是按水平顺序,此时频数会参差不齐;改用根据频数多少进行排序,则条形图变得整齐易读,对比效果见图1.10。

p1 = ggplot(mpg, aes(class)) +
  geom_bar() +
  theme(axis.text.x = element_text(angle = 45, vjust = 1, hjust = 1))
p2 = ggplot(mpg, aes(fct_infreq(class))) +
  geom_bar() +
  theme(axis.text.x = element_text(angle = 45, vjust = 1, hjust = 1))
library(patchwork)
p1 | p2

图1.10 按频数排序的条形图

1.4 数据结构:字符串、日期时间

1.4.1 字符串

字符串是用双引号或单引号括起来的若干字符,建议用双引号,除非字符串中包含双引号。字符串构成的向量,简称为字符向量。

字符串处理不是R语言的主要功能,但也是必不可少的,数据清洗、可视化等操作都会用到。

tidyverse系列中的stringr包提供了一系列接口一致的、简单易用的字符串操作函数,足以代替R自带的字符串函数。这些函数都是向量化的,即作用在字符向量上,对字符向量中的每个字符串做某种操作。

library(stringr)

1.字符串的长度(即包含字符个数)

str_length(c(“a”, “R for data science”, NA))
 
## [1]  1 18 NA
 
str_pad(c(“a”, “ab”, “abc”), 3)       # 填充到长度为3
 
## [1] “  a” “ ab” “abc”
 
str_trunc(“R for data science”, 10)   # 截断到长度为10
 
## [1] “R for d...”
 
str_trim(c(“a  “, “b  “, “a b”))      # 移除空格
 
## [1] “a”   “b”   “a b”

后三个函数都包含参数side=c(“both”, “left”, “right”)用于设定操作的方向。

2.字符串合并

str_c(..., sep = “”, collapse = NULL)

sep:设置间隔符,默认为空字符;

collapse:指定间隔符,将字符向量中的所有字符串合并为一个字符串。

str_c(“x”, 1:3, sep = “”)  # 同paste0(“x”, 1:3), paste(“x”, 1:3, sep=””)   
 
## [1] “x1” “x2” “x3”
 
str_c(“x”, 1:3, collapse = “_”)
 
## [1] “x1_x2_x3”
str_c(“x”, str_c(sprintf(“%03d”, 1:3)))
## [1] “x001” “x002” “x003”

注意,上述代码中的1:3自动向下兼容以适应字符串运算,效果同c(“1”,”2”,”3”)

将字符串重复n次,基本格式为:

str_dup(string, times)

string:为要重复的字符向量。

times:为重复的次数。

str_dup(c(“A”,”B”), 3)
 
## [1] “AAA” “BBB”
 
str_dup(c(“A”,”B”), c(3,2))
 
## [1] “AAA” “BB”

3.字符串拆分

str_split(string, pattern)          # 返回列表   
str_split_fixed(string, pattern, n) # 返回矩阵,n控制返回的列数

string:要拆分的字符串。

pattern:指定拆分的分隔符,可以是正则表达式。

x = “10,8,7”
str_split(x, “,”)
 
## [[1]]
## [1] “10” “8”  “7”
 
str_split_fixed(x, “,”, n = 2)
 
##      [,1] [,2] 
## [1,] “10” “8,7”

4.字符串格式化输出

只要在字符串内使用“{变量名}”,那么函数str_glue()str_glue_data就可以将字符串中的变量名替换成变量值,后者的参数.x支持引入数据框、列表等,相关的代码示例如下所示。

str_glue(“Pi = {pi}”)
 
## Pi = 3.14159265358979
 
name = “ 李明”
tele = “13912345678”
str_glue(“姓名: {name}”, “电话号码: {tele}”, .sep=”;”)
 
## 姓名: 李明;电话号码: 13912345678
df = mtcars[1:3,]
str_glue_data(df, “{rownames(df)} 总功率为 {hp} kW.”)
## Mazda RX4 总功率为 110 kW.
## Mazda RX4 Waq 总功率为 110 kW.
## Datsun 710 总功率为 93 kW.

5.字符串排序

str_sort(x, decreasing, locale, ...)
str_order(x, decreasing, locale, ...)

默认decreasing = FALSE表示升序,前者返回排好序的元素,后者返回排好序的索引;参数locale可设定语言,默认为 “en”(即英语)。

x = c(“banana”, “apple”, “pear”)
str_sort(x)     
 
## [1] “apple”  “banana” “pear”
 
str_order(x)
 
## [1] 2 1 3
 
str_sort(c(“香蕉”, “苹果”, “梨”), locale = “ch”)
 
## [1] “梨” “苹果” “香蕉”

6.检测匹配

str_detect(string, pattern, negate=FALSE)—检测是否存在匹配。

str_which(string, pattern, negate=FALSE)—查找匹配的索引。

str_count(string, pattern)—计算匹配的次数。

str_locate(string, pattern)—定位匹配的位置。

str_starts(string, pattern)—检测是否以pattern开头。

str_ends(string, pattern)—检测是否以pattern结尾。

string:要检测的字符串。

pattern:匹配的模式,可以是正则表达式。

negate:默认为FALSE,表示正常匹配;若为TRUE,则为反匹配(即找不匹配的情况)。

x
 
## [1] “banana” “apple”  “pear”
 
str_detect(x, “p”)
 
## [1] FALSE  TRUE  TRUE
 
str_which(x, “p”)
 
## [1] 2 3
 
str_count(x, “p”)
 
## [1] 0 2 1
 
str_locate(x, “a.”)   # 正则表达式, .匹配任一字符
 
##      start end
## [1,]     2   3
## [2,]     1   2
## [3,]     3   4

7.提取字符串子集

根据指定的起始和终止位置提取子字符串,基本格式为:

str_sub(string, start = 1, end = -1)

例如:

str_sub(x, 1, 3)  
 
## [1] “ban” “app” “pea”
 
str_sub(x, 1, 5)     # 若长度不够, 则尽可能多地提取
 
## [1] “banan” “apple” “pear”
 
str_sub(x, -3, -1)
 
## [1] “ana” “ple” “ear”

提取字符向量中匹配的字符串,基本格式为:

str_subset(string, pattern, negate=FALSE)

negate = TRUE, 则返回不匹配的字符串。

str_subset(x, “p”)
 
## [1] “apple” “pear”

8.提取匹配的内容

str_extract(string, pattern)  
str_match(string, pattern)

str_extract()只提取匹配的内容。

str_match()提取匹配的内容以及各个分组捕获,并返回矩阵,矩阵的每行对应于字符向量中的一个字符串,每行的第一个元素是匹配内容,其他元素是各个分组捕获,没有匹配则为NA

x = c(“1978-2000”, “2011-2020-2099”)
pat = “\\d{4}”          # 正则表达式, 匹配4位数字
str_extract(x, pat)  
 
## [1] “1978” “2011”
 
str_match(x, pat)
 
##      [,1]  
## [1,] “1978”
## [2,] “2011”

9.修改字符串

用新字符串替换查找到的匹配字符串。

做字符替换,基本格式为:

str_replace(string, pattern, replacement)

pattern:要替换的子字符串或模式。

replacement:要替换为的新字符串。

x
 
## [1] “1978-2000”      “2011-2020-2099”
 
str_replace(x, “-”, “/”)
 
## [1] “1978/2000”      “2011/2020-2099”

10.其他函数

大小写转化。

str_to_upper():转换为大写。

str_to_lower():转换为小写。

str_to_title():转换标题格式(单词首字母大写)。

str_to_lower(“I love r language.”)
 
## [1] “i love r language.”
 
str_to_upper(“I love r language.”)
 
## [1] “I LOVE R LANGUAGE.”
 
str_to_title(“I love r language.”)
 
## [1] “I Love R Language.”

str_conv(string, encoding):转化字符串的字符编码。

str_view(string, pattern, match):在Viewer窗口输出(正则表达式)模式匹配结果。

word(string, start, end, sep = “ “):从英文句子中提取单词。

str_wrap(string, width = 80, indent = 0, exdent = 0):调整段落格式。

关于stringr

以上用于查找匹配的各个函数,只是查找第一个匹配,要想查找所有匹配,各个函数都有另一个版本(加后缀_all),例如str_extract_all()

以上各个函数中的参数pattern都支持用正则表达式(Regular Expression)表示模式。

1.4.2 日期时间

日期时间值通常以字符串形式传入R中,然后转化为以数值形式存储的日期时间变量。

R的内部日期是以1970年1月1日至今的天数来存储,内部时间则是以1970年1月1日零时至今的秒数来存储。

tidyverse系列的lubridate包提供了更加方便的函数,可以生成、转换、管理日期时间数据,足以代替R自带的日期时间函数。

library(lubridate)

1.识别日期时间

today()
 
## [1] “2021-09-20”
 
now()
 
## [1] “2021-09-20 21:07:18 CST”
 
as_datetime(today())   # 日期型转日期时间型
 
## [1] “2021-09-20 UTC”
 
as_date(now())         # 日期时间型转日期型
 
## [1] “2021-09-20”

无论年、月、日、时、分、秒按什么顺序及以什么间隔符分隔,总能正确地识别成日期时间值:

ymd(“2020/03~01”)
 
## [1] “2020-03-01”
 
myd(“03202001”)
 
## [1] “2020-03-01”
 
dmy(“03012020”)
 
## [1] “2020-01-03”
 
ymd_hm(“2020/03~011213”)
 
## [1] “2020-03-01 12:13:00 UTC”

注意:

根据需要可以任意组合(如ymd_h/myd_hm/dmy_hms),还可以用参数tz ="…" 指定时区。

我们也可以用make_date()make_datetime()从日期时间组件创建日期时间:

make_date(2020, 8, 27)
 
## [1] “2020-08-27”
 
make_datetime(2020, 8, 27, 21, 27, 15)
 
## [1] “2020-08-27 21:27:15 UTC”

2.格式化输出日期时间

format()函数输出日期时间:

d = make_date(2020, 3, 5)
format(d, ‘%Y/%m/%d’)
 
## [1] “2020/03/05”

stamp()函数,按给定模板格式输出日期时间:

t = make_datetime(2020, 3, 5, 21, 7, 15)
fmt = stamp(“Created on Sunday, Jan 1, 1999 3:34 pm”)
fmt(t)
 
## [1] “Created on Sunday, 03 05, 2020 21:07下午”

3.提取日期时间数据的组件

日期时间数据中的“年、月、日、周、时、分、秒”等称为其组件。常用的日期时间组件如表1.2所示。

表1.2 常用的日期时间组件

符号

描述

示例

%d

数字表示的日期

(01~31)

%a

缩写的星期名

Mon

%A

非缩写的星期名

Monday

%w

数字表示的星期几

(0~6),0为周日

%m

数字表示的月份

(00~12)

%b

缩写月份

Jan

%B

非缩写月份

January

%y

二位数年份

21

%Y

四位数年份

2021

%H

24小时制小时

(00~23)

%I

12小时制小时

(01~12)

%p

AM/PM指示

AM/PM

%M

十进制分钟

(00~60)

%S

十进制秒

(00~60)

t = ymd_hms(“2020/08/27 21:30:27”)
t
 
## [1] “2020-08-27 21:30:27 UTC”
 
year(t)
 
## [1] 2020
 
quarter(t)            # 第几季度
 
## [1] 3
 
month(t)
 
## [1] 8
 
day(t)
 
## [1] 27
 
yday(t)               # 当年的第几天
 
## [1] 240
 
hour(t)
 
## [1] 21
 
minute(t)
 
## [1] 30
 
second(t)
 
## [1] 27
 
weekdays(t)
 
## [1] “星期四”
 
wday(t)               # 数值表示本周的第几天, 默认周日是第1天
 
## [1] 5
 
wday(t,label = TRUE)  # 字符因子型表示本周第几天
 
## [1]周四
 
## Levels: 周日 < 周一 < 周二 < 周三 < 周四 < 周五 < 周六
 
week(t)               # 当年的第几周
 
## [1] 35
 
tz(t)                 # 时区
 
## [1] “UTC”

with_tz()将时间数据转换为另一个时区的同一时间;用force_tz()将时间数据的时区强制转换为另一个时区:

with_tz(t, tz = “America/New_York”)
 
## [1] “2020-08-27 17:30:27 EDT”
 
force_tz(t, tz = “America/New_York”)
 
## [1] “2020-08-27 21:30:27 EDT”

还可以模糊提取(取整)不同的时间单位:

round_date(t, unit=”hour”)      # 四舍五入取整到小时
 
## [1] “2020-08-27 22:00:00 UTC”

注意:

类似地,向下取整用floor_date(),向上取整用ceiling_date()

rollback(dates, roll_to_first=FALSE, preserve_hms=TRUE):回滚到上月最后一天或本月第一天。

4.时间段数据

interval():计算两个时间点的时间间隔,返回时间段数据。

begin = ymd_hm(“2019-08-10 14:00”)
end = ymd_hm(“2020-03-05 18:15”)
gap = interval(begin, end)  # 同begin %--% end
gap
 
## [1] 2019-08-10 14:00:00 UTC--2020-03-05 18:15:00 UTC
 
time_length(gap, “day”)     # 计算时间段的长度为多少天
 
## [1] 208.1771
 
time_length(gap, “minute”)  # 计算时间段的长度为多少分钟
 
## [1] 299775
 
t %within% gap              # 判断t是否属于该时间段
 
## [1] FALSE

duration():用“数值+时间单位”存储时段的长度。

duration(100, units = “day”)
 
## [1] “8640000s (~14.29 weeks)”
 
int = as.duration(gap)
int
 
## [1] “17986500s (~29.74 weeks)”

period():和duration()基本相同。

二者的区别duration基于数值线,不考虑闰年和闰秒;period基于时间线,考虑闰年和闰秒。

比如,在duration中,1年总是365.25天;而在period中,平年有365天,闰年有366天。

固定单位的时间段

period时间段:years()months()weeks()days()hours()minutes()seconds()

duration时间段:dyears()dmonths()dweeks()ddays()dhours()dminutes()dseconds()

dyears(1)
 
## [1] “31557600s (~1 years)”
 
years(1)
 
## [1] “1y 0m 0d 0H 0M 0S”

5.日期的时间的计算

用“时间点+时间段”可以生成一个新的时间点:

t + int    
 
## [1] “2021-03-24 01:45:27 UTC”
 
leap_year(2020)             # 判断是否闰年
 
## [1] TRUE
 
ymd(20190305) + years(1)    # 加period的一年
 
## [1] “2020-03-05”
 
ymd(20190305) + dyears(1)   # 加duration的一年, 365天
 
## [1] “2020-03-04 06:00:00 UTC”
 
t + weeks(1:3)
 
## [1] “2020-09-03 21:30:27 UTC” “2020-09-10 21:30:27 UTC”
## [3] “2020-09-17 21:30:27 UTC”

除法运算:

gap / ddays(1)             # 除法运算, 同time_length(gap,’day’)
 
## [1] 208.1771
 
gap %/% ddays(1)           # 整除
 
## [1] 208
 
gap %% ddays(1)            #余数
 
## [1] 2020-03-05 14:00:00 UTC--2020-03-05 18:15:00 UTC
 
as.period(gap %% ddays(1))
 
## [1] “4H 15M 0S”

月份加运算:%m+%,表示日期按月数增加。例如,生成每月同一天的日期数据:

date = as_date(“2019-01-01”)
date %m+% months(0:11)
 
##  [1] “2019-01-01” “2019-02-01” “2019-03-01” “2019-04-01” “2019-05-01”
##  [6] “2019-06-01” “2019-07-01” “2019-08-01” “2019-09-01” “2019-10-01”
## [11] “2019-11-01” “2019-12-01”

用“pretty_dates()”可以生成近似的时间刻度:

x = seq.Date(as_date(“2019-08-02”), by = “year”, length.out = 2)
pretty_dates(x, 12)
 
##  [1] “2019-08-01 UTC” “2019-09-01 UTC” “2019-10-01 UTC”
##  [4] “2019-11-01 UTC” “2019-12-01 UTC” “2020-01-01 UTC”
##  [7] “2020-02-01 UTC” “2020-03-01 UTC” “2020-04-01 UTC”
## [10] “2020-05-01 UTC” “2020-06-01 UTC” “2020-07-01 UTC”
## [13] “2020-08-01 UTC” “2020-09-01 UTC”

1.4.3 时间序列

为了研究某一事件的规律,依据时间发生的顺序将事件在多个时刻的数值记录下来,就构成了一个时间序列,用表示。

例如,国家或地区的年度财政收入、股票市场的每日波动、气象变化、工厂按小时观测的产量等。另外,随温度、高度等变化而变化的离散序列,也可以看作时间序列。

1.ts对象

Base R提供的ts数据类型是专门为时间序列设计的,一个时间序列数据其实就是一个数值型向量,且每个数都有一个时刻与之对应。

ts()函数生成时间序列,基本格式如下:

ts(data, start=1, end, frequency=1, ...) 

data:数值向量或矩阵。

start:设置起始时刻。

end:设置结束时刻。

frequency:设置时间频率,默认为1,表示一年有1个数据。

ts(data = 1:10, start = 2010, end = 2019)     # 年度数据
 
## Time Series:
## Start = 2010 
## End = 2019 
## Frequency = 1 
##  [1]  1  2  3  4  5  6  7  8  9 10
 
ts(data = 1:10, start = 2010, frequency = 4)  # 季度数据
 
##      Qtr1 Qtr2 Qtr3 Qtr4
## 2010    1    2    3    4
## 2011    5    6    7    8
## 2012    9   10

同理,对于月度数据,frequency = 12;对于周度数据,frequency = 52;对于日度数据,frequency = 365

2.tsibble

fpp3生态下的tsibble包提供了整洁的时间序列数据结构tsibble

时间序列数据无非就是“指标数据+时间索引”(或者再加“分组索引”)。

注意:

多元时间序列就是包含多个指标列。

分组时间序列数据首先是一个数据框,若有分组变量需采用“长格式”作为一列(长宽格式及转化参见2.4节),只需要指定时间索引、分组索引,就能变成时间序列数据结构。

例如,现有3个公司2017年的日度股票数据(tibble格式),其中存放3只股票的Stock列为分组索引:

load(“data/stocks.rda”)
stocks
 
## # A tibble: 753 x 3
##   Date       Stock  Close
##   <date>     <chr>  <dbl>
## 1 2017-01-03 Google  786.
## 2 2017-01-03 Amazon  754.
## 3 2017-01-03 Apple   116.
## 4 2017-01-04 Google  787.
## 5 2017-01-04 Amazon  757.
## 6 2017-01-04 Apple   116.
## # ... with 747 more rows

as_tsibble()将数据框转化为时间序列对象tsibble, 只需要指定时间索引(index)、分组索引(key):

library(fpp3)
stocks = as_tsibble(stocks, key = Stock, index = Date)
stocks
 
## # A tsibble: 753 x 3 [1D]
## # Key:       Stock [3]
##   Date       Stock  Close
##   <date>     <chr>  <dbl>
## 1 2017-01-03 Amazon  754.
## 2 2017-01-04 Amazon  757.
## 3 2017-01-05 Amazon  780.
## 4 2017-01-06 Amazon  796.
## 5 2017-01-09 Amazon  797.
## 6 2017-01-10 Amazon  796.
## # ... with 747 more rows

tsibble对象非常便于后续处理和探索:

stocks %>%
  group_by_key() ٪>٪ 
  index_by(weeks = ~ yearweek(.)) ٪>٪    # 周度汇总
  summarise(max_week = mean(Close))
 
## # A tsibble: 156 x 3 [1W]
## # Key:       Stock [3]
##   Stock     weeks max_week
##   <chr>    <week>    <dbl>
## 1 Amazon 2017 W01     772.
## 2 Amazon 2017 W02     805.
## 3 Amazon 2017 W03     809.
## 4 Amazon 2017 W04     830.
## 5 Amazon 2017 W05     827.
## 6 Amazon 2017 W06     818.
## # ... with 150 more rows
 
autoplot(stocks)                         # 可视化

可视化结果如图1.11所示。

图1.11 可视化股票数据

1.5 正则表达式

正则表达式是根据字符串规律按一定法则,简洁地表达一组字符串的表达式。正则表达式通常就是从貌似无规律的字符串中发现规律,进而概括性地表达它们所共有的规律或模式,以便于操作和处理它们,这是真正的化繁为简,以简驭繁的典范。

几乎所有的高级编程语言都支持正则表达式,正则表达式广泛应用于文本挖掘、数据预处理,例如:

检查文本中是否含有指定的特征词;

找出文本中匹配特征词的位置;

从文本中提取信息;

修改文本。

正则表达式包括只能匹配自身的普通字符(如英文字母、数字、标点等)和被转义了的特殊字符(称为“元字符”)。

1.5.1 基本语法

1.常用的元字符

正则表达式中常用的元字符如表1.3所示。

表1.3 常用的元字符

符号

描述

.

匹配除换行符“/n”以外的任意字符

\\

转义字符,匹配元字符时,使用“\\元字符”

|

表示或者,即“|”前后的表达式任选一个

^

匹配字符串的开始

$

匹配字符串的结束

( )

提取匹配的字符串,即括号内的看作一个整体,即指定子表达式

[ ]

可匹配方括号内任意一个字符

{ }

前面的字符或表达式的重复次数:{n}表示重复n次;{n,}表示重复n次到更多次;{n, m}表示重复n~m次

*

前面的字符或表达式重复0次或更多次

+

前面的字符或表达式重复1次或更多次

?

前面的字符或表达式重复0次或1次

其他编程语言中的转义字符一般是“\”。默认情况下,正则表达式区分大小写,要创建忽略大小写的正则表达式,代码如下:

pat = fixed(pattern, ignore_case = TRUE)

在多行模式下,^$就表示行的开始和结束,创建多行模式的正则表达式的代码如下:

pat = regex(“^\\(.+?\\)$”, multiline = TRUE)

2.特殊字符类及其反义

正则表达式中常用的特殊字符及其反义如表1.4所示。

表1.4 特殊字符类及其反义

符号

描述

\\d\\D

匹配数字,匹配非数字

\\s\\S

匹配空白符,匹配非空白符

\\w\\W

匹配字母或数字或下划线或汉字,匹配非\w字符

\\b\\B

匹配单词的开始或结束的位置,匹配非\b的位置

\\h\\H

匹配水平间隔,匹配非水平间隔

\\v\\V

匹配垂直间隔,匹配非垂直间隔

[^...]

匹配除……以外的任意字符

\\S+:匹配不包含空白符的字符串。

\\d:匹配数字,同[0-9]

[a-zA-Z0-9]:匹配字母和数字。

 [\\p{han}][\u4e00-\u9fa5]匹配汉字。

[^aeiou]:匹配除aeiou之外的任意字符,即匹配辅音字母。

3.POSIX字符类

正则表达式中还可以使用POSIX字符类,如表1.5所示。

表1.5 POSIX字符类

符号

描述

[[:lower:]]

小写字母

[[:upper:]]

大写字母

[[:alpha:]]

大小写字母

[[:digit:]]

数字0~9

[[:alnum:]]

字母和数字

[[:blank:]]

空白符包括空格、制表符、换行符、中文全角空格等

[[:cntrl:]]

控制字符

[[:punct:]]

标点符号包括“!”“”“#”“%”“&”“”“()”“*”“+”“-”“.”“/”“:”“;”等

[[:space:]]

空格字符:空格、制表符、垂直制表符、回车、换行符、换页符

[[:xdigit:]]

十六进制数字:09AFaf

[[:print:]]

打印字符:[[:alpha:]][[:punct:]][[:space:]]

[[:graph:]]

图形化字符:[[:alpha:]][[:punct:]]

4.运算优先级

圆括号括起来的表达式最优先,其次是表示重复次数的操作(即“*”“+”“{ }”);再次是连接运算(即几个字符放在一起,如abc);最后是或运算(|)。

另外,正则表达式还有若干高级用法,常用的有零宽断言分组捕获,这些将在后面的实例中进行演示。

1.5.2 若干实例

以上正则表达式语法组合起来使用,就能产生非常强大的匹配效果,对于匹配到的内容,根据需要可以提取它们,也可以替换它们。

正则表达式与stringr包连用

若只是调试和查看正则表达式的匹配效果,可用str_view()及其_all后缀版本,匹配结果将在RStudio的Viewer窗口显示,在原字符向量中高亮显示匹配内容,非常直观。

若要提取正则表达式匹配到的内容,则用str_extract()及其_all后缀版本。

若要替换正则表达式匹配到的内容,则用str_replace()及其_all后缀版本。

使用正则表达式关键在于能够从貌似没有规律的字符串中发现规律性,再将规律性用正则表达式语法表示出来。下面看几个正则表达式比较实用的实例。

例1.1 直接匹配

该方法适合想要匹配的内容具有一定规律性,该规律性可用正则表达式表示出来。比如,数据中包含字母、符号、数值,我们想提取其中的数值,可以按正则表达式语法规则直接把要提取的部分表示出来:

x = c(“CDK弱(+)10%+”, “CDK(+)30%-”, “CDK(-)0+”, “CDK(++)60%*”)
str_view(x, “\\d+%”)
str_view(x, “\\d+%?”)

str_view()常用于调试正则表达式,匹配结果显示在Viewer窗口,如图1.12所示。

图1.12 Viewer窗口显示匹配效果

\\d表示匹配一位数字,+表示前面数字重复1次或多次,%原样匹配%

若后面不加“?”则必须匹配到%才会成功,故第3个字符串就不能成功匹配;若后面加上“?”则表示匹配前面的%0次或1次,从而能成功匹配到第3个字符串。

例1.2 用零宽断言匹配两个标志之间的内容

该方法适合想要匹配的内容没有规律性,但该内容位于两个有规律性的标志之间,标志也可以是开始和结束。

通常想要匹配的内容不包含两边的“标志”,这就需要用零宽断言。简单来说,就是引导语法既要匹配到“标志”,但又不包含“标志”。左边标志的引导语法是(?<=标志),右边标志的引导语法是(?=标志),而把真正要匹配的内容放在它们中间。

比如,来自问卷星“来自IP”数据,想要提取IP和地址信息。

x = c(“175.10.237.40(湖南-长沙)”, “114.243.12.168(北京-北京)”, 
      "125.211.78.251(黑龙江-哈尔滨)”)
# 提取省份
str_extract(x, “\\(.*-”)           # 此处作为对比,不用零宽断言
 
## [1] “(湖南-”   “(北京-”   “(黑龙江-”
 
str_extract(x, “(?<=\\().*(?=-)”)  # 用零宽断言
 
## [1] “湖南”   “北京”   “黑龙江”
 
# 提取IP
# str_extract(x, “\\d.*\\d”)       # 直接匹配
str_extract(x, “^.*(?=\\()”)       # 用零宽断言
 
## [1] “175.10.237.40”  “114.243.12.168” “125.211.78.251”

省份(或直辖市)位于两个标志“(”和“-”之间,但又不包含该标志,这就需要用到零宽断言。

IP位于两个标志“开始”和“(”之间,左边用开始符号^,右边用零宽断言。

再比如,用零宽断言提取专业信息(位于“级”和数字之间):

x = c(“18级能源动力工程2班”, “19级统计学1班”)
str_extract(x, “(?<=级).*?(?=[0-9])”)
 
## [1] “能源动力工程” “统计学”

再看两个的复杂的零宽断言,涉及出现次数。例如,提取句子中的最后一个单词:

x = c(“I am a teacher”, “She is a beautiful girl”)
str_extract(x, “(?<= )[^ ]+$”)
 
## [1] “teacher” “girl”  

零宽断言以空格为左标志,匹配内容是非空格出现1次或多次直到结尾,结果就是作为左标志的空格只能是句子中的最后一个空格。

再比如,提取以“kc/”为左标志,直到第3个下划线之前的内容:

x = “D:/paper/1.65_kc_ndvi/kc/forest_kc_historical_ACCESS-ESM1-5_west_1981_2014.tif”
str_extract(x, “(?<=kc/)([^_]+_){2}[^_]+”)
 
## [1] “forest_kc_historical”

匹配内容是:非下划线出现1次或多次(即1个单词)接1个下划线,上述部分重复2次,再接一个非下划线出现1次或多次(即1个单词),结果就是恰好匹配到第3个下划线出现之前。

关于懒惰匹配

正则表达式通常都是贪婪匹配,即重复直到文本中能匹配的最长范围,例如匹配小括号:

str_extract(“(1st) other (2nd)”, “\\(.+\\)”)
 
## [1] “(1st) other (2nd)”

若想只匹配到第1个右小括号,则需要懒惰匹配,在重复匹配后面加上“?”即可:

str_extract(“(1st) other (2nd)”, “\\(.+?\\)”)
 
## [1] “(1st)”

例1.3 分组捕获

在正则表达式中可以用圆括号来分组,作用是:

确定优先规则;

组成一个整体;

拆分出整个匹配中的部分内容(称为捕获);

捕获内容供后续引用或者替换。

比如,来自瓜子二手车的数据:若汽车型号是中文,则品牌与型号中间有空格;若汽车型号为英文或数字,则品牌与型号中间没有空格。

若用正则表达式匹配“字母或数字”并分组,然后捕获该分组内容并添加空格以替换原内容,代码如下所示:

x = c(“宝马X3 2016款”, “大众 速腾2017款”, “宝马3系2012款”)
str_replace(x, “([a-zA-Z0-9])”, “ \\1”)
 
## [1] “宝马 X3 2016款” “大众 速腾 2017款” “宝马 3系 2012款”

后续操作就可以用空格拆分列(见2.4.4节)。

现有6位数字表示的时分秒数据,想用lubridate::hms()解析成时间类型,但是在时分秒之间用冒号或空格分隔才能正确解析。下面分组捕获两组数字,并分别替换为该两位数字加冒号,然后再解析成时间类型:

x = c(“194631”, “174223”) #数值型也可以
x = str_replace_all(x, “(\\d{2})”, “ \\1:”)
x
 
## [1] “19:46:31:” “17:42:23:”
hms(x)
## [1] “19H 46M 31S” “17H 42M 23S”

更多分组的引用还有\\2\\3等。例如,纠正电影的年份和国别出现顺序不一致的情况,可以通过代码统一将信息转换成“国别_年份”,代码如下所示:

x = c(“独行月球2022_Chinese”,”蜘蛛侠USA_2021”,”人生大事2022_Chinese”)
str_replace(x, “(\\d+)_(.+)”,”\\2_\\1”)
 
## [1] “独行月球Chinese_2022” “蜘蛛侠USA_2021” “人生大事Chinese_2022”

最后,再推荐一个来自GitHub的包inferregex,该包可以推断正则表达式,用函数infer_regex()可根据字符串推断正则表达式。

1.6 控制结构

程序中的控制结构是指分支结构和循环结构。

1.6.1 分支结构

正常程序结构与一步一步解决问题是一致的,即顺序结构,过程中可能需要为不同情形选择不同的支路,即分支结构,还需要用条件语句做判断以实现具体的分支,如图1.13所示。

图1.13 分支结构示意图

R语言中条件语句的一般格式为:

1.一个分支

if(条件) {
  执行体
}

2.两个分支

if(条件) {
  执行体1
} else {
  执行体2
}

例如,计算,代码如下:

if(x < 0) {
  y = -x
} else {
  y = x
}

3.多个分支

if(条件1) {
  执行体1
} else if(条件2) {
  执行体2
} else {
  执行体n
}

多个分支的意思是,若满足“条件1”,则执行“执行体1”;若满足“条件2”,则执行“执行体2”;其他的情形,则执行“执行体n”。若有需要,中间可以有任意多个else if块。

特别注意:

分支的本意就是,不同分支之间不存在交叉(重叠)。

另一种多分支的写法是用switch():

x = “b”
v = switch(x, “a”=”apple”, “b”=”banana”, “c”=”cherry”)
v
 
## [1] “banana”

它的一个应用场景是:在自定义函数时,若需要根据参数的不同执行不同的代码块。关于自定义函数详见1.7.1节。

例1.4 实现将百分制分数转化为五级制分数

if(score >= 90) {
  res = "优”
} else if(score >= 80) {
  res = "良” 
} else if(score >= 70) {
  res = "中”
} else if(score >= 60) {
  res = "及格”
} else {
  res = "不及格”
}

注意:若先写“score >=60”,结果就不对了。

关于“条件”

“条件”用逻辑表达式表示,必须返回一个逻辑值TRUEFALSE

多个逻辑表达式,可以通过逻辑运算符组合以表示复杂条件;

多个逻辑值的逻辑向量可以借助函数any()all()得到一个逻辑值;

函数ifelse()可简化代码,仍以计算为例:

ifelse(x < 0, -x, x)

1.6.2 循环结构

编程时可以减少代码重复的两个工具,一个是循环,另一个是函数。

循环用来对多个同类输入做相同事情(即迭代),例如对向量的每个元素做相同操作,对数据框的不同列做相同操作,对不同的数据集做相同操作等。循环结构如图1.14所示。

图1.14 循环结构示意图

R语言循环迭代的三层境界如下所示。

第一层:for循环、while循环、repeat循环。

第二层:apply函数族。

第三层:purrr泛函式编程。

关于跳出循环有以下两种方式。

用关键字next跳出本次循环,进入下次循环。

用关键词break跳出循环。

实用场景

关于“for循环运行速度慢”的说法,实际上已经过时了,现在的R、MATLAB等软件经过多年的优化已经不慢了,之所以表现出来慢,是因为你没有注意两个关键点:

提前为保存循环结果分配存储空间;

为循环体中涉及的数据选择合适的数据结构。

apply函数族和purrr泛函式编程能够更加高效简洁地实现一般的for循环和while循环,但这并不代表for循环、while循环就没用了,它们可以在更高的层次使用(相对于在逐元素级别使用)。

1.for循环

(1)基本的for循环
library(tidyverse)
df = as_tibble(iris[,1:4])

用“复制-粘贴”法,计算前4列的均值:

mean(df[[1]])
 
## [1] 5.843333
 
mean(df[[2]])
 
## [1] 3.057333
 
mean(df[[3]])
 
## [1] 3.758
 
mean(df[[4]])
 
## [1] 1.199333

为了避免“粘贴-复制”操作多于两次,我们改用for循环实现:

output = vector(“double”, 4)             # 1.输出
for (i in 1:4) {                         # 2.迭代器
  output[i] = mean(df[[i]])              # 3.循环体
}
output
 
## [1] 5.843333 3.057333 3.758000 1.199333

for循环有三个组件,即输出、迭代器、循环体。

输出:output = vector("double", 4)

在循环开始之前,最好为输出结果分配足够的存储空间,这样效率更高。若每循环一次,就用c()合并一次,效率会很低下。

通常是用vector()函数创建一个给定长度的空向量,它有两个参数:向量类型(logical、integer、double、character等)、向量长度。

迭代器:i in 1:4

循环方式:每次for循环将为i赋一个14中的值,可将i理解为代词it

有时候会用1:length(df), 但更安全的做法是用seq_along(df),它能保证即使不小心遇到长度为0的向量时,仍能正确工作。

循环体:output[i] = mean(df[[i]])

即执行具体操作的代码,它将重复执行,每次对应不同的i值。

第1次迭代将执行:output[1] = mean(df[[1]])

第2次迭代将执行:output[2] = mean(df[[2]])

……

(2)for循环的几种常用操作

循环模式

根据数值索引迭代:for(i in seq_along(xs)), 在迭代中使用x[i]

根据元素值迭代:for(x in xs), 在迭代中使用x

根据名字迭代:for(nm in names(xs)), 在迭代中使用x[nm]

若要创建命名向量并作为输出,可按如下方式命名结果向量:

results = vector(“list”, length(x))
names(results) = names(x)

用数值索引迭代是最常用的形式,因为名字和元素都可以根据索引提取:

for (i in seq_along(x)) {
    name = names(x)[i]
    value = x[i]
}

将每次循环得到的结果合并为一个整体对象

这种情形在for循环中经常遇到。此时要尽量避免“每循环一次,就做一次拼接”,这样效率很低。更好的做法是先将结果保存为列表,等循环结束再通过unlist()purrr::flatten_dbl()将列表转换成一个向量。

先创建空列表,再将每次循环的结果依次存入列表:

output = list()       # output = NULL也行
# output = vector(“list”, 3)
for(i in 1:3) {
  output[[i]] = c(i, i^2)
}

另外两种类似的情形如下。

生成一个长字符串。不是用str_c()函数将上一次的迭代结果拼接到一起,而是将结果保存为字符向量,再用函数str_c(output, collapse= " ")合并为一个单独的字符串。

生成一个大的数据框。不是依次用rbind()函数合并每次迭代的结果,而是将结果保存为列表,再用dplyr::bind_rows(output)函数合并成一个单独的数据框,或者直接一步到位用purrr::map_dfr()

所以,遇到上述模式时,要先转化为更复杂的结果对象,最后再做合并。

2.while循环

适用于迭代次数未知的情况。

while循环更简单,因为它只包含两个组件:条件、循环体:

while (condition) {
  # 循环体
}

While循环是比for循环更一般的循环,因为for循环总可以改写为while循环,但while循环不一定能改写为for循环:

for (i in seq_along(x)) {
  # 循环体
}
# 等价于
i = 1
while (i <= length(x)) {
  # 循环体
  i = i + 1
}

下面用while循环实现:反复随机生成标准正态分布随机数(关于生成随机数详见1.7.2节),若值大于1则停止:

set.seed(123)    # 设置随机种子, 让结果可重现
while(TRUE) {
  x = rnorm(1)
  print(x)
  if(x > 1) break
}
 
## [1] -0.5604756
## [1] -0.2301775
## [1] 1.558708

while循环并不常用,但在模拟时也较常用,特别是预先不知道迭代次数的情形。

3.repeat循环

重复执行循环体,直到满足退出条件:

repeat{
  # 循环体
  if(退出条件) break
}

注意,repeat循环至少会执行一次。

repeat循环等价于:

while (TRUE) {
  # 循环体
  if(退出条件) break
}

例如,用如下泰勒公式近似计算

s = 1.0
x = 1
k = 0
 
repeat{
  k = k + 1
  x = x / k
  s = s + x
  if(x < 1e-10) break
}
 
stringr::str_glue(“迭代 {k} 次, 得到e = {s}”)
## 迭代14次, 得到e = 2.71828182845823

4.apply函数族

建议弃用apply函数族,直接用purrr::map系列。

(1)apply()函数

apply()函数是最常用的可以代替for循环的函数,可以对矩阵、数据框、多维数组,按行或列或页进行循环计算,即将逐行或逐列或逐页的元素分别传递给函数FUN进行迭代计算。其基本格式为:

apply(x, MARGIN, FUN, ...)

x:为数据对象(矩阵、多维数组、数据框)。

MARGIN:1表示按行,2表示按列,3表示按页。

FUN:表示要作用的函数。

x = matrix(1:6, ncol = 3)
x
 
##      [,1] [,2] [,3]
## [1,]    1    3    5
## [2,]    2    4    6
 
apply(x, 1, mean)          # 按行求均值
 
## [1] 3 4
 
apply(x, 2, mean)          # 按列求均值
 
## [1] 1.5 3.5 5.5
 
apply(df, 2, mean)         # 对前文df计算各列的均值
 
## Sepal.Length  Sepal.Width Petal.Length  Petal.Width 
##     5.843333     3.057333     3.758000     1.199333
(2)tapply()函数

该函数可以按照因子分组,实现逐分组迭代:

height = c(165, 170, 168, 172, 159)
sex = factor(c(“男”, “女”, “男”, “男”, “女”))
tapply(height, sex, mean)
 
##       男       女 
## 168.3333 164.5000

注意heightsex是等长的向量,对应元素分别为同一人的身高和性别,tapply()函数分男女两组计算了身高平均值。

(3)lapply()函数

lapply()函数是一个最基础的循环操作函数,用来对vectorlistdata.frame逐元、逐成分、逐列分别应用函数FUN,并返回和x长度相同的list对象。其基本格式为:

lapply(x, FUN, ...)

x:为数据对象(列表、数据框、向量)。

FUN:表示要作用的函数。

lapply(df, mean)      # 对前文df计算各列的均值
# $Sepal.Length
# [1] 5.843333
# 
# $Sepal.Width
# [1] 3.057333
# 
# $Petal.Length
# [1] 3.758
# 
# $Petal.Width
# [1] 1.199333
(4)sapply()函数

sapply()函数是lapply()的简化版本,只是多了一个参数simplify,若simplify=FALSE,则与lapply()相同;若simply = TRUE,则将输出的list简化为向量或矩阵。其基本格式为:

sapply(x, FUN, simplify = TRUE, ...)
sapply(df, mean)      # 对前文df计算各列的均值
 
## Sepal.Length  Sepal.Width Petal.Length  Petal.Width 
##     5.843333     3.057333     3.758000     1.199333

5.purrr泛函式循环迭代

相对于apply族,purrr泛函式循环迭代提供了更多的一致性、规范性和便利性,更容易记住和使用。

(1)几个基本概念

循环迭代

循环迭代就是将函数依次应用(映射)到序列的每一个元素上,做相同的操作。而序列是由一系列可以根据位置索引的元素构成,元素可以很复杂,也可以是不同类型的。原子向量和列表都是序列。

泛函式编程

泛函其实就是函数的函数,在编程中表示把函数作用在函数上,或者说函数包含其他函数作为参数。

循环迭代本质上就是将一个函数依次应用(映射)到序列的每一个元素上,用泛函式表示即map(x, f)[8]

[8]  将序列(要操作的数据)作为第一个参数x,是为了便于使用管道。

purrr泛函式编程解决循环迭代问题的逻辑是:针对序列中每个单独的元素,怎么处理它能得到正确的结果,将这个过程定义为函数,再map(映射)到序列中的每一个元素,将得到的多个结果(每个元素作用后返回一个结果),再打包到一起返回,并且可以根据想要的结果类型选用对应的map后缀。

对循环迭代返回类型的控制

map系列函数都有后缀形式,以决定循环迭代之后返回的数据类型,这是purrrapply函数族更先进和便利的一大优势。常用后缀如下。

map_chr(.x, .f):返回字符型向量。

map_lgl(.x, .f):返回逻辑型向量。

map_dbl(.x, .f):返回实数型向量。

map_int(.x, .f):返回整数型向量。

map_dfr(.x, .f):返回数据框列表,再通过bind_rows按行合并为一个数据框。

map_dfc(.x, .f):返回数据框列表,再通过bind_cols按列合并为一个数据框。

purrr风格的公式

在序列上进行循环迭代(应用函数),经常需要自定义函数,但有些简单的函数如果也用function定义,未免显得麻烦和啰嗦。purrr包提供了对purrr风格的公式(匿名函数)的支持,解决了这一问题。如果读者熟悉其他语言的匿名函数,很自然地就能习惯purrr风格的公式。

前面提到,purrr包实现迭代循环是用map(x, f),其中f是要应用的函数,想用匿名函数来写它,它要应用在序列x上,就是要和序列x相关联,那么就限定用序列参数名关联好了,即将该序列参数名作为匿名函数的参数使用:

一元函数:序列参数是.x,比如, 其purrr风格的公式就写为:~ .x ^ 2 + 1

二元函数:序列参数是 .x.y,比如, 其purrr风格的公式就写为:~ .x ^ 2 - 3 * .y

多元函数:序列参数是 ..1, ..2, ..3等,比如, 其purrr风格的公式就写为:~ log(..1 + ..2 + ..3)

所有序列参数可以用“...”代替,比如,sum(...)sum(..1, ..2, ..3)

(2)map():依次应用一元函数到一个序列的每个元素
map(.x, .f, ...)
map_*(.x, .f, ...)

.x为序列。

.f为要应用的一元函数,或purrr风格公式(匿名函数)。

... 可用于设置函数.f的其他参数。

map()函数的作用机制如图1.15所示。

图1.15 map()函数的作用机制

map()返回结果列表,基本同lapply()。例如,计算前文df,每列的均值,即依次将mean()函数应用到第1列、第2列……并控制返回结果为double向量:

map(df, mean)
 
## $Sepal.Length
## [1] 5.843333
## 
## $Sepal.Width
## [1] 3.057333
## 
## $Petal.Length
## [1] 3.758
## 
## $Petal.Width
## [1] 1.199333

说明df是数据框(特殊的列表),作为序列其元素依次是:df[[1]], df[[2]]……所以,map(df, mean)相当于依次计算:mean(df[[1]]), mean(df[[2]])……

返回结果是double型数值,更好的做法是,控制返回类型为数值向量,只需使用以下方法:

map_dbl(df, mean)
 
## Sepal.Length  Sepal.Width Petal.Length  Petal.Width 
##     5.843333     3.057333     3.758000     1.199333

另外,mean()函数还有其他参数(如na.rm),若上述计算过程需要设置忽略缺失值,只需使用以下方法:

map_dbl(df, mean, na.rm = TRUE)       # 数据不含NA, 故结果同上(略)
map_dbl(df, ~mean(.x, na.rm = TRUE))  # purrr风格公式写法

有了map()函数,对于自定义只接受标量的一元函数,比如f(x), 想要让它支持将向量作为输入,根本不需要改造原函数,只需按以下方式操作:

map_*(xs, f)                         # xs表示若干个x构成的序列
(3)map2():依次应用二元函数到两个序列的每对元素
map2(.x, .y .f, ...)
map2_*(.x, .y, .f, ...)

.x为序列1。

.y为序列2。

.f为要应用的二元函数或purrr风格公式(匿名函数)。

...可用于设置函数.f的其他参数。

map2()函数的作用机制如图1.16所示。

图1.16 map2()函数的作用机制

例如,根据身高、体重数据计算BMI指数:

height = c(1.58, 1.76, 1.64)
weight = c(52, 73, 68)
 
cal_BMI = function(h, w) w / h ^ 2     # 定义计算BMI的函数
map2_dbl(height, weight, cal_BMI)
 
## [1] 20.83000 23.56663 25.28257

说明:序列1的元素为height[[1]], height[[2]]……

序列2的元素为weight[[1]], weight[[2]]……

因此,map2_dbl(height, weight, cal_BMI)相当于依次计算:

cal_BMI(height[[1]], weight[[1]]), cal_BMI(height[[2]], weight[[2]]), ……

更简洁的purrr风格公式写法(此处省略了自定义函数)如下:

map2_dbl(height, weight, ~ .y / .x^2) 

同样,有了map2()函数,对于自定义只接受标量的二元函数,比如f(x, y), 想要让它支持将向量作为输入,根本不需要改造原函数,只需按以下方式操作:

map2_*(xs, ys, f)            # xs, ys分别表示若干个x, y构成的序列
(4)pmap():应用多元函数到多个序列的每组元素,可以实现对数据框逐行迭代

因为pmap()函数是在多个长度相同的列表上进行迭代,而长度相同的列表就是数据框,所以,pmap()的多元迭代就是依次在数据框的每一行上进行迭代!

pmap(.l, .f, ...)
pmap_*(.l, .f, ...)

.l为数据框,

.f为要应用的多元函数

...可设置函数.f的其他参数

注意:.f”是几元函数,对应的数据框“.l”就有几列,“.f”将依次在数据框“.l”的每一行上进行迭代。

pmap()函数的作用机制如图1.17所示。

图1.17 pmap()函数的作用机制

例如,分别生成不同数量不同均值和标准差的正态分布随机数,代码如下。

df = tibble(
  n = c(1, 3, 5),
  mean = c(5, 10, -3),
  sd = c(1, 5, 10))
df
 
## # A tibble: 3 x 3
##       n  mean    sd
##   <dbl> <dbl> <dbl>
## 1     1     5     1
## 2     3    10     5
## 3     5    -3    10
 
set.seed(123)
pmap(df, rnorm)
 
## [[1]]
## [1] 4.439524
## 
## [[2]]
## [1]  8.849113 17.793542 10.352542
## 
## [[3]]
## [1]  -1.707123  14.150650   1.609162 -15.650612  -9.868529

说明:这里的rnorm(n, mean, sd)是三元函数,pmap(df, rnorm)相当于将三元函数rnorm()依次应用到数据框df的每一行上,即依次执行以下代码:

rnorm(1, 5, 1), rnorm(3, 10, 5), rnorm(5, -3, 10)

注意,这里df中的列名,必须与rnorm()函数的参数名相同(列序随便)。若要避免这种局限,可以使用purrr风格的公式写法:

pmap(df, ~ rnorm(..1, ..2, ..3))    # 或者简写为
pmap(df, ~ rnorm(...))

pmap_*()提供了一种行化操作数据框的办法。

pmap_dbl(df, ~ mean(c(...)))        # 按行求均值
 
## [1] 2.333333 6.000000 4.000000
 
pmap_chr(df, str_c, sep = “-”)      # 将各行拼接在一起 
 
## [1] “1-5-1”   “3-10-5”  “5--3-10”

其他purrr函数

imap_*(.x, .f):带索引的map_*()系列在迭代的时候既迭代元素,又迭代元素的索引(位置或名字),purrr风格公式中用 .y表示索引。

invoke_map_*(.f, .x, ...):将多个函数依次应用到序列,相当于依次执行:.f[[1]](.x,...), .f[[2]](.x, ...)……

walk系列:walk(.l, .f, ...), walk2(.l, .f, ...), pwalk(.l, .f, ...)

将函数依次作用到序列上,不返回结果。有些批量操作是没有或不关心返回结果的,例如批量保存数据到文件、批量绘图保存到文件等。

modify系列:modify(.x, .f, ...), modify2(.x, .y, .f, ...), modify_ depth(.x,.depth, .f, ...)

将函数“.f”依次作用到序列“.x”,并返回修改后的序列“.x”。

reduce():可先对序列前两个元素应用函数,再对结果与第3个元素应用函数,再对结果与第4个元素应用函数……直到所有的元都被“reduced”。

reduce(1:100, sum)是对1~100求累加和。

reduce()可用于批量数据连接。

accumulate():与reduce()作用方式相同,不同之处是reduce()只返回最终的结果,而accumulate()会返回所有中间结果。

1.7 自定义函数

编程中的函数是用来实现某个功能,其一般形式为:

(返回值1,..., 返回值m) = 函数名(输入1, ..., 输入n)

你只要把输入给它,它就能在内部进行相应处理,把你想要的返回值给你。

这些输入和返回值,在函数定义时,都要有固定的类型(模具)限制,叫作形参(形式上的参数);在函数调用时,必须给它对应类型的具体数值,才能真正地去做处理,这叫作实参(实际的参数)。定义函数就好比创造一个模具,调用函数就好比用模具批量生成 产品。

使用函数最大的好处,就是将实现的某个功能封装成模具,从而可以反复使用。这就避免了写大量重复的代码,程序的可读性也大大加强。

以前文的将百分制分数转化为五级制分数为例,如果有一个百分制分数,就这样转化一次,那么如果有10个学生分数,就得写100多行代码。因此有必要封装成一个函数。

1.7.1 自定义函数

1.自定义函数的一般语法

在R中,自定义函数的一般格式为:

函数名 = function(输入1, ..., 输入n) {
  函数体
  return(返回值)
}

注意return并不是必需的,默认将函数体最后一行的值作为返回值,也就是说“return(返回值)”完全可以换成“返回值”。

2.自定义一个函数

我们想要自定义一个函数,能够实现把百分制分数转化为五级制分数的功能。

基于前面对函数的理解,我们按以下步骤进行。

第一步,分析输入和输出,设计函数外形。

输入有几个,分别是什么,适合用什么数据结构存放。

输出有几个,分别是什么,适合用什么数据结构存放。

对于本问题,输入有1个,百分制分数,适合采用数值型存放;输出有1个,五级制分数,适合采用字符串存放。

然后就可以设计自定义函数的外形,如下所示:

Score_Conv = function(score) {
# 实现将一个百分制分数转化为五级分数
# 输入参数: score为数值型, 百分制分数
# 返回值: res为字符串型, 五级分数
...
}

函数名和变量可以随便起名,但是建议使用有具体含义的单词。另外,为函数增加注释是一个好习惯。这些都是为了代码的可读性。

第二步,梳理功能的实现过程。

前言中在谈到“如何自己写代码”时讲到:“分解问题 + 实例梳理 + ‘翻译’及调试”,完全适用于这里,不再赘述。

拿一组本例中(只有一个)具体的形参的值作为输入,比如76分,分析怎么得到对应的五级分数“良”。这依赖于对五级分数界限的选取,选定之后做分支判断即可实现,即像前文的条件语句中的示例那样。

复杂的功能就需要更耐心地梳理和思考甚至借助一些算法,当然也离不开对代码片段的调试。

score = 76
if(score >= 90) {
  res = "优”
} else if(score >= 80) {
  res = "良” 
} else if(score >= 70) {
  res = "中”
} else if(score >= 60) {
  res = "及格”
} else {
  res = "不及格”
}
res
 
## [1] “中”

拿一组具体的形参值作为输入,通过逐步调试可以得到正确的返回值结果,这一步骤非常关键和有必要。

第三步,将第二步的代码封装到函数体。

基本就是原样作为函数体放入函数,原来的变量赋值语句不需要了,只需要形参。具体代码如下所示:

Score_Conv = function(score) {
  if(score >= 90) {
    res = "优”
  } else if(score >= 80) {
    res = "良” 
  } else if(score >= 70) {
    res = "中”
  } else if(score >= 60) {
    res = "及格”
  } else {
    res = "不及格”
  }
  res
}

3.调用函数

要调用自定义函数,必须要先加载到当前变量窗口(内存),有两种方法:

需要选中并执行函数代码;

将函数保存为同名的Score_Conv.R文件,然后执行source(“Score_Conv.R”)

之后就可以调用函数了,给它一个实参76,输出结果为“中”:

Score_Conv(76)
 
## [1] “中”

关于向函数传递参数

要调用一个函数,比如f(x, y),首先要清楚其形参xy所要求的类型,假设x要求是数值向量,y要求是单个逻辑值。

那么,要调用该函数,首先需要准备与形参类型相符的实参(同名异名均可),比如

a = c(3.56, 2.1)
b = FALSE

再调用函数:

f(a, b)      # 同直接给值: f(c(3.56,2.1), FALSE)

调用函数时若不指定参数名,则默认是根据位置关联形参,即以x = a, y = b的方式进入函数体。

调用函数时若指定参数名,则根据参数名关联形参,位置不再重要,比如:

f(y = b, x = a)      # 效果同上

4.向量化改进

我们希望自定义函数也能处理向量输入,即输入多个百分制分数,能一下都转化为五级分数。这也是所谓的“向量化编程”思维,就是要习惯用向量(矩阵)去思考、去表达。

方法一:修改自定义函数

将输入参数设计为数值向量,函数体也要相应地修改,借助循环依次处理向量中的每个元素,就相当于再套一层for循环。

Score_Conv2 = function(score) {
  n = length(score)
  res = vector("character", n)
  for(i in 1:n) {
    if(score[i] >= 90) {
      res[i] = "优”
    } else if(score[i] >= 80) {
      res[i] = "良” 
    } else if(score[i] >= 70) {
      res[i] = "中”
    } else if(score[i] >= 60) {
      res[i] = "及格”
    } else {
      res[i] = "不及格”
    }
  }
  res
}
 
# 测试函数
scores = c(35, 67, 100)
Score_Conv2(scores)
 
## [1] “不及格” “及格”   “优”

方法二:借助apply族或map系列函数

简单的循环语句基本都可以改用apply族或map系列函数实现,其作用相当于依次“应用”某函数到序列的每个元素上。

也就是说,不需要修改原函数,直接就能实现向量化操作:

scores = c(35, 67, 100)
map_chr(scores, Score_Conv)         
 
## [1] “不及格” “及格”   “优”

5.处理多个返回值

若自定义函数需要有多个返回值,R的处理方法是,将多个返回值放入一个列表(或数据框),再返回一个列表。

例如,用自定义函数计算一个数值向量的均值和标准差:

MeanStd = function(x) {
  mu = mean(x)
  std  = sqrt(sum((x-mu)^2) / (length(x)-1))
  list(mu=mu, std=std)
}
# 测试函数
x = c(2, 6, 4, 9, 12)
MeanStd(x)
 
## $mu
## [1] 6.6
## 
## $std
## [1] 3.974921

6.默认参数值

有时候需要为输入参数设置默认值。以前面的计算数值向量的均值和标准差的函数为例。我们知道,标准差的计算公式有两种形式,一种是总体标准差除以n,另一种是样本标准差除以n − 1。

此时,没有必要写两个版本的函数,只需要再增加一个指示参数,将使用较多的版本设为默认即可。

MeanStd2 = function(x, type = 1) {
  mu = mean(x)
  n = length(x)
  if(type == 1) {
    std  = sqrt(sum((x - mu) ^ 2) / (n - 1))
  } else {
    std  = sqrt(sum((x - mu) ^ 2) / n)
  }  
  list(mu = mu, std = std)
}
# 测试函数
x = c(2, 6, 4, 9, 12)
# MeanStd2(x)                  # 同MeanStd(x)
MeanStd2(x, 2)
 
## $mu
## [1] 6.6
## 
## $std
## [1] 3.555278

type = 1来指示表意并不明确,可以用表意更明确的字符串来指示,这就需要用到switch(),让不同的指示值等于相应的代码块,因为代码块往往是多行,需要用大括号括起来,注意分支与分支之间的逗号不能少。

MeanStd3 = function(x, type = “sample”) {
  mu = mean(x)
  n = length(x)
  switch(type,
         "sample" = {
           std = sqrt(sum((x - mu) ^ 2) / (n - 1))
           },
          "population" = {
            std = sqrt(sum((x - mu) ^ 2) / n)
          })
  list(mu = mu, std = std)
}
MeanStd3(x)
 
## $mu
## [1] 6.6
## 
## $std
## [1] 3.974921
 
MeanStd3(x, “population”)
 
## $mu
## [1] 6.6
## 
## $std
## [1] 3.555278

7.“...”参数

一般函数参数只接受一个对象,即使不指定参数名,也会按位置对应参数。例如:

my_sum = function(x, y) {
    sum(x, y)
}
my_sum(1, 2)
 
## [1] 3

但是,如果想对3个数求和,怎么办?直接用my_sum(1, 2, 3)会报错。

...”是一个特殊参数,可以接受任意多个对象,并作为一个列表传递它们:

dots_sum = function(...) {
    sum(...)
}
dots_sum(1)
 
## [1] 1
 
dots_sum(1, 2, 3, 4, 5)
 
## [1] 15

几乎所有R的自带函数都在用“...”传递参数。 若参数“...”后面还有其他参数,为了避免歧义,调用函数时需要对其随后的参数进行命名。

1.7.2 R自带的函数

除了自定义函数,还可以使用现成的函数。

来自Base R的函数:可直接使用。

来自各种扩展包的函数:需载入包,或加上包名前缀: “  包名::函数名()”。

这些函数的使用,可以通过“?函数名”查阅其帮助,以及查阅包页面的Reference manual和Vignettes(若有)。

下面对常用的R自带的函数做分类总结。

1.基本数学函数

round(x, digits)     # IEEE 754标准的四舍五入, 保留n位小数
signif(x, digits)    # 四舍五入, 保留n位有效数字
ceiling(x)           # 向上取整, 例如ceiling(pi)为4
floor(x)             # 向下取整, 例如floor(pi)为3
sign(x)              # 符号函数
abs(x)               # 取绝对值
sqrt(x)              # 求平方根
exp(x)               # e的x次幂
log(x, base)         # 对x取以……为底的对数, 默认以e为底
log2(x)              # 对x取以2为底的对数
log10(x)             # 对x取以10为底的对数
Re(z)                # 返回复数z的实部
Im(z)                # 返回复数z的虚部
Mod(z)               # 求复数z的模
Arg(z)               # 求复数z的辐角
Conj(z)              # 求复数z的共轭复数

2.三角函数与双曲函数

sin(x)               # 正弦函数
cos(x)               # 余弦函数
tan(x)               # 正切函数
asin(x)              # 反正弦函数
acos(x)              # 反余弦函数
atan(x)              # 反正切函数
sinh(x)              # 双曲正弦函数
cosh(x)              # 双曲余弦函数
tanh(x)              # 双曲正切函数
asinh(x)             # 反双曲正弦函数
acosh(x)             # 反双曲余弦函数
atanh(x)             # 反双曲正切函数

3.矩阵函数

nrow(A)              # 返回矩阵A的行数
ncol(A)              # 返回矩阵A的列数
dim(A)               # 返回矩阵x的维数 (几行×几列) 
colSums(A)           # 对矩阵A的各列求和
rowSums(A)           # 对矩阵A的各行求和
colMeans(A)          # 对矩阵A的各列求均值
rowMeans(A)          # 对矩阵A的各行求均值
t(A)                 # 对矩阵A转置
det(A)               # 计算方阵A的行列式
crossprod(A, B)      # 计算矩阵A与B的内积, t(A) %*% B
outer(A, B)          # 计算矩阵的外积 (叉积), A ٪o٪ B
diag(x)              # 取矩阵对角线元素,或根据向量生成对角矩阵
diag(n)              # 生成n阶单位矩阵
solve(A)             # 求逆矩阵 (要求矩阵可逆) 
solve(A, B)          # 解线性方程组AX=B   
ginv(A)              # 求矩阵A的广义逆(Moore-Penrose逆), MASS包
eigen()              # 返回矩阵的特征值与特征向量(列)
kronecker(A, B)      # 计算矩阵A与B的Kronecker积
svd(A)               # 对矩阵A做奇异值分解,A=UDV’
qr(A)                # 对矩阵A做QR分解: A=QR, Q为酉矩阵, R为阶梯形矩阵
chol(A)              # 对正定矩阵A做Choleski分解, A=P’P,P为上三角矩阵
A[upper.tri(A)]      # 提取矩阵A的上三角矩阵
 
A[lower.tri(A)]      # 提取矩阵A的下三角矩阵

4.概率函数

factorial(n)         # 计算n的阶乘
choose(n, k)         # 计算组合数 
gamma(x)             # Gamma函数 
beta(a, b)           # beta函数 
combn(x, m)          # 生成x中任取m个元的所有组合, x为向量或整数n

例如:

combn(4, 2)
 
##      [,1] [,2] [,3] [,4] [,5] [,6]
## [1,]    1    1    1    2    2    3
## [2,]    2    3    4    3    4    4
 
combn(c(“甲”,”乙”,”丙”,”丁”), 2)
 
##      [,1] [,2] [,3] [,4] [,5] [,6]
## [1,] “甲” “甲” “甲” “乙” “乙” “丙”
## [2,] “乙” “丙” “丁” “丙” “丁” “丁”

在R中,常用的概率函数有密度函数、分布函数、分位数函数、生成随机数函数,其写法为:

d = 密度函数(density)

p = 分布函数(distribution)

q = 分位数函数(quantile)

r = 生成随机数(random)

上述“4个字母 + 分布的缩写”,就构成通常的概率函数。常用的概率分布及缩写如表1.6所示。

dnorm(3, 0, 2) # 正态分布N(0, 4) 在3处的密度值 ## [1] 0.0647588 pnorm(1:3, 1, 2) # N(1,4)分布在1,2,3处的分布函数值 ## [1] 0.5000000 0.6914625 0.8413447 # 命中率为0.02, 独立射击400次, 至少击中两次的概率1 - sum(dbinom(0:1, 400, 0.02))  ## [1] 0.9971655 pnorm(2, 1, 2) - pnorm(0, 1, 2) # X~N(1, 4), 求P{0<X<=2} ## [1] 0.3829249 qnorm(1-0.025,0,1) # N(0,1)的0.975分位数 ## [1] 1.959964

生成随机数[9]

[9]  自然界中的随机现象是真正随机发生且不可重现的,计算机中模拟的随机现象包括生成随机数、随机抽样,这并不是真正的随机,而是可以重现的。通过设置相同的起始种子值就可以重现,故称为“伪随机”。

set.seed(123)        # 设置随机种子, 以重现随机结果
rnorm(5, 0, 1)       # 生成5个服从N(0,1)分布的随机数
 
## [1] -0.56047565 -0.23017749  1.55870831  0.07050839  0.12928774

表1.6 常用的概率分布及缩写

分布名称

缩写

参数及默认值

二项分布

binom

size, prob

多项分布

multinom

size, prob

负二项分布

nbinom

size, prob

几何分布

geom

prob

超几何分布

hyper

m, n, k

泊松分布

pois

lambda

均匀分布

unif

min=0, max=1

指数分布

exp

rate=1

正态分布

norm

mean=0, sd=1

对数正态分布

lnorm

meanlog=0, stdlog=1

t分布

t

df

卡方分布

chisq

df

F分布

f

df1, df2

Wilcoxon符号秩分布

signrank

n

Wilcoxon秩和分布

wilcox

m, n

柯西分布

cauchy

location=0, scale=1

Logistic分布

logis

location=0, scale=1

Weibull分布

weibull

shape, scale=1

Gamma分布

gamma

shape, scale=1

Beta分布

beta

shape1, shape2

随机抽样

sample()函数,用来从向量中重复或非重复地随机抽样,基本格式为:

sample(x, size, replace = FALSE, prob)

x:向量或整数。

size:设置抽样次数。

replace:设置是否重复抽样。

prob:设定抽样权重。

set.seed(2020)
sample(c(“正”,”反”), 10, replace=TRUE)  # 模拟抛10次硬币
 
##  [1] “反” “反” “正” “反” “反” “正” “正” “反” “反” “反”
 
sample(1:10, 10, replace=FALSE)         # 随机生成1~10的某排列
 
##  [1]  1  8  9  2  7  5  6  3  4 10

5.统计函数

min(x)              # 求最小值
cummin(x)           # 求累计最小值
max(x)              # 求最大值
cummax(x)           # 求累计最大值
range(x)            # 求x的范围:[最小值,最大值] (向量) 
sum(x)              # 求和
cumsum(x)           # 求累计和
prod(x)             # 求积
cumprod(x)          # 求累计积
mean(x)             # 求平均值
median(x)           # 求中位数
quantile(x, pr)     # 求分位数, x为数值向量, pr为概率值
sd(x)               # 求标准差
var(x)              # 求方差
cov(x)              # 求协方差
cor(x)              # 求相关系数
scale(x, center=TRUE, scale=FALSE)  # 对数据做中心化: 减去均值
scale(x, center=TRUE, scale=TRUE)   # 对数据做标准化

自定义归一化函数:

rescale = function(x, type=1) {
  # type=1正向指标, type=2负向指标
  rng = range(x, na.rm = TRUE)
  if (type == 1) {
    (x - rng[1]) / (rng[2] - rng[1])
  } else {
    (rng[2] - x) / (rng[2] - rng[1])
  }
}
 
x = c(1, 2, 3, NA, 5)
rescale(x)
 
## [1] 0.00 0.25 0.50   NA 1.00
 
rescale(x, 2)
 
## [1] 1.00 0.75 0.50   NA 0.00

6.时间序列函数

lag()函数,用来计算时间序列的滞后,基本格式为:

lag(x, k, ...)

x:为数值向量/矩阵或一元/多元时间序列;

k:为滞后阶数,默认为1。


diff()函数,用来计算时间序列的差分,基本格式为:

diff(x, lag = 1, difference = 1, ...)

x:为数值向量/矩阵;

lag:为滞后阶数,默认为1;

difference:为差分阶数,默认为1。

阶滞后为

x = ts(1:8, frequency = 4, start = 2015)
x
 
##      Qtr1 Qtr2 Qtr3 Qtr4
## 2015    1    2    3    4
## 2016    5    6    7    8
 
stats::lag(x, 4)       # 避免被dplyr::lag()覆盖
 
##      Qtr1 Qtr2 Qtr3 Qtr4
## 2014    1    2    3    4
## 2015    5    6    7    8

的一阶差分为,二阶差分为……

x = c(1, 3, 6, 8, 10)
x
 
## [1]  1  3  6  8 10
 
diff(x, differences = 1)
 
## [1] 2 3 2 2
 
diff(x, differences = 2)
 
## [1]  1 -1  0
 
diff(x, lag = 2, differences = 1)
 
## [1] 5 5 4

7.其他函数

unique(x, ...)           # 返回唯一值, 即去掉重复元素或观测
duplicated(x, ...)       # 判断元素或观测是否重复(多余), 返回逻辑值向量
anyDuplicated(x, ...)    # 返回重复元素或观测的索引
rle(x)                   # 统计向量中连续相同值的长度
inverse.rle(x)           # rle()的反向版本, x为list(lengths, values)
dput(x, file)            # 方便创建最小可重现案例用于向他人提问
get()/mget()             # 根据名字获取一个或多个当前对象的值
# 文件操作函数
list.files(path,pattern, ...)      # 列出某路径下的匹配的文件路径
file.create(...)
file.exists(...)
file.remove(...)
file.rename(from, to)
file.append(file1, file2)
file.copy(from, to, overwrite, ...)       

拓展学习

读者如果想进一步了解R语言的基本语法,建议大家阅读Hadley编写的《R数据科学》(R for Data Science)、Advanced R,任坤编写的《R语言编程指南》,李东风编写的《R语言教程》。

读者如果想进一步了解R语言与时间序列,建议大家阅读Hyndman编写的Forecasting: Principles and Practice, 3rd Edition

读者如果想进一步了解因子、字符串、日期时间、泛函式循环迭代,建议大家了解forcats包、stringr包、lubridate包、purrr包文档及相关资源。

读者服务:

微信扫码关注【异步社区】微信公众号,回复“e60380”获取本书配套资源以及异步社区15天VIP会员卡,近千本电子书免费畅读。

相关图书

R语言医学多元统计分析
R语言医学多元统计分析
Python与R语言数据科学实践
Python与R语言数据科学实践
R数据挖掘实战
R数据挖掘实战
R语言机器学习实战
R语言机器学习实战
R语言高效能实战:更多数据和更快速度
R语言高效能实战:更多数据和更快速度
R语言金融分析与建模
R语言金融分析与建模

相关文章

相关课程