书名: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
(贝叶斯模型)、tidyquant
和modeltime
(金融)、fpp3
和
timetk
(时间序列)、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语言为工具,从事统计分析、数据挖掘、机器学习工作的人,特别是想学习使用机器学习包(tidymodels
和mlr3verse
)的人。
高校里对R语言及相关课程有需求的师生及科研人员,特别是将来想要在时间序列、金融、空间数据分析、文本挖掘等领域使用fpp3
、modeltime
、tidyquant
、sf
、quanteda
等包的人。
本书绝大部分内容参考新版本R包的相关文档,全面采用新的R语言技术编写,特别强调“整洁流、管道流、泛函流”数据科学思维(tidyverse)。
很多R语言编程书只是罗列编程语法,很难让初学者学透它们。本书真正融入编程思维:由编程思想引导,了解编程语法到底是怎么回事,应该用于何处以及怎么使用。
讲解编程语法必须配以合适的实例来演示,也建议读者一定要将编程语法讲解与配套实例结合起来阅读,比起将实例代码调试通过,更重要的是借助实例代码透彻地理解编程语法所包含的编程思维。本书后半部分是R语言在应用统计、探索性数据分析、文档沟通方面的应用,所配案例力求能让读者上手使用。
本书程序代码都是基于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机器学习框架—mlr3verse
和tidymodels
。
大家可以根据自己的需求选择阅读的侧重点,不过我还是希望你能够按照顺序完整地阅读,这样才能彻底地更新一遍你的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语言代码,读者可以先忽略代码细节,只当它们是计算过程,把关注点放在所传达的编程思维上。
编程语言是人与计算机沟通的一种语言形式,根据设计好的编程元素和语法规则,严格规范地表达我们想要做的事情的每一步(程序代码),使计算机能够明白并正确执行,最终得到期望的结果。
编程语言和数学语言很像,数学语言是最适合表达科学理论的形式语言,用数学符号、数学定义和逻辑推理可以规范地表达科学理论。
很多人说:“学数学,就是靠大量刷题;学编程,就是照着别人的代码敲代码”。
我不认可这种观点,这样的学习方法事倍功半,关键是这样做你学不会真正的数学,也学不会真正的编程!
那么应该怎么学习编程语言呢?
打个比方,要成为一个好的厨师,首先得熟悉各种常见食材的特点,掌握各种基本的烹饪方法,然后就能根据客人需要随意组合食材和烹饪方法制作出各种可口的大餐。
数学的食材就是定义,烹饪方法就是逻辑推理,一旦你真正掌握了定义和逻辑推理,各种基本的数学题都不在话下,而且你还学会了数学知识。
同理,编程的食材和烹饪方法就是编程元素和语法规则,例如数据结构(如容器)、分支/循环结构、自定义函数等。一旦你掌握了这些编程元素和语法规则,根据问题的需要,你就能自由组合并优化它们,从而写出代码解决问题。
学习任何一门编程语言,根据我的经验,有这么几点建议(步骤)。
(1)理解该编程语言的核心思想,比如Python面向对象,R语言面向函数也面向对象。另外,高级编程语言还倡导向量化编程。读者应在核心思想的引领下学习编程语言并思考如何编写代码。
(2)学习该编程语言的基础知识,这些基础知识本质上是相通的,只是不同的编程语言有其对应的编程语法,相关的基础知识包括数据类型及数据结构(容器)、分支/循环结构、自定义函数、文件读写、可视化等。
(3)前两步完成之后,就算基本入门[1]了。读者可以根据需要,结合遇到的问题,借助网络搜索或他人的帮助,分析问题并解决问题,逐步提升编程技能,用得越多会得到越多,也越熟练。
[1] 至少要经历过一种编程语言的入门,再学习其他编程语言就会快很多。
以上是学习编程语言的正确、快速、有效的方法,切忌不学基础语法,用到什么就学什么,基于别人的代码乱改。这样的结果是,自以为节省时间,实际上是浪费了更多的时间,关键是始终无法入门,更谈不上将来提高。其中的道理也很简单,总是在别人的代码上乱改,是学不到真正的编程知识的,也很难真正地入门编程。当你完整地学习了编程语法,再去基于别人的代码进行修改,这实际上是在验证你所学的编程知识,那么你的编程水平自然也会逐步提高。
再来谈一个学编程过程中普遍存在的问题:如何跨越“能看懂别人的代码”到“自己写代码”之间的鸿沟。
绝大多数人在编程学习过程中,都要经历这样一个过程:
第1步:学习基本语法
第2步:能看懂并调试别人的代码
↓(“编程之门”)
第3步:自己编写代码
前两步没有任何难度,谁都可以做到。从第2步到第3步是一个“坎”,很多人困惑于此而无法真正进入“编程之门”。网上也有很多讲到如何跨越这一步的文章,但基本都是脱离实际操作的空谈(比如照着编写书上的代码之类),往往治标不治本(只能提升编程基本知识)。
我所倡导的理念也很简单,无非就是“分解问题 + 实例梳理 + ‘翻译’及调试”,具体如下:
● 将难以入手的大问题分解为可以逐步解决的小问题;
● 用计算机的思维去思考并解决每个小问题;
● 借助类比的简单实例和代码片段,梳理出详细的算法步骤;
● 将详细的算法步骤用编程语法逐片段地“翻译”成代码并调试通过。
高级编程语言的程序代码通常是逐片段调试的,借助简单的实例按照算法步骤从上一步的结果调试得到下一步的结果,依次向前推进直至到达最终的结果。另外,写代码时,随时跟踪并关注每一步的执行结果,观察变量、数据的值是否到达你所期望的值,非常有必要!
这是我用数学建模的思维得出的比较科学的操作步骤。为什么大家普遍感觉在自己写代码解决具体问题时有些无从下手呢?
这是因为你总想一步就从问题到代码,没有中间的过程,即使编程高手也做不到。当然,编程高手也许能缩减这个过程,但不能省略这个过程。其实你平时看编程书是被表象“欺骗”了:编程书上只介绍问题(或者简单分析问题)紧接着就提供代码,给人的感觉就是应该从问题直接到代码,其实不然。
改变从问题直接到代码的固化思维,可以参考前面说的步骤(分解问题+实例梳理+“翻译”及调试)去操作,每一步是不是都不难解决?这样一来,自然就从无从下手转变到能锻炼自己独立写代码了。
开始你或许只能通过写代码解决比较简单的问题,但是慢慢就会有成就感,再加上慢慢锻炼,写代码的能力会越来越强,能解决的问题也会越来越复杂。当然这一切的前提是,你已经真正掌握了基本编程语法,可以随意取用。当然二者也是相辅相成和共同促进的。
好,说清了这个道理,接下来用一个具体的小案例来演示一下。
ROC曲线是二分类机器学习模型的性能评价指标,已知测试集或验证集中每个样本的真实类别及其模型预测概率值,就可以计算并绘制ROC曲线。
先来梳理一下问题,ROC曲线是在不同分类阈值上对比真正率(TPR)与假正率(FPR)的曲线。
分类阈值就是根据预测概率判定预测类别的阈值,要让该阈值从0到1以足够小的步长变化,对于每个阈值c
(如0.85),则当预测概率≥0.85时,判定为“Pos”;当预测概率时,判定为“Neg”。这样就得到了预测类别。
根据真实类别和预测类别,就能计算混淆矩阵,各单元格含义如图0.1所示。
图0.1 混淆矩阵示意图
进一步就可以计算:
有一个阈值,就能计算一组TPR和FPR,循环迭代并计算所有的TPR和FPR,且将相关数值进行保存。再以FPR为x轴,以TPR为y轴进行绘制,就可以得到ROC曲线。
在此,我们梳理一下经过分解后的问题。
● 让分类阈值以某步长在[1,0]上变化取值。
● 对某一个阈值:
❏ 计算预测类别;
❏ 计算混淆矩阵;
❏ 计算TPR和FPR。
● 循环迭代,计算所有阈值的TPR和FPR。
● 根据TPR和FPR数据绘制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 |
先对某一个阈值,计算对应的TPR和FPR,这里以为例。
计算预测类别,实际上就是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
计算TPR和FPR比较简单,根据计算公式,从混淆矩阵中取值进行计算即可。这里咱们再高级一点,用向量化编程来实现。向量化编程是对一列矩阵中的数同时做同样的操作,既提升程序效率又大大简化代码。
向量化编程关键是要用整体考量的思维来思考和表示运算。比如这里计算TPR和FPR,通过观察可以发现:混淆矩阵的第1行的各个元素,都除以其所在列的和,正好是TPR和FPR。
cm[“Pos”,] / colSums(cm)
## Pos Neg
## 0.4 0.0
这就完成了本问题的核心部分。接下来,要进行循环迭代,对每个阈值都计算一遍TPR和FPR。用for
循环当然可以,但咱们仍然更高级一点,使用泛函式编程。
先把上述计算封装成一个自定义函数,该函数只要接收一个原始的数据框df
和一个阈值c
,就能返回来你想要的TPR和FPR。然后,再把该函数应用到数据框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.3所示。
图0.3 数据科学的位置
Hadley Wickham定义了数据科学的工作流程,如图0.4所示,即数据导入、数据清洗、数据变换、数据可视化、数据建模以及文档沟通,整个分析和探索过程,我们应当训练这样的数据思维。
图0.4 数据科学的工作流程
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机器学习包mlr3verse
或tidymodels
(参见附录F)。
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包:打包文件、生成文档、测试代码。
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类面向对象编程的简单示例。
笼统地来说,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
的圆的面积和批量计算半径为2
、4
和7
的圆的面积,代码如下所示:
AreaCircle(5)
## [1] 78.53982
rs = c(2,4,7)
map_dbl(rs, AreaCircle) # purrr包
## [1] 12.56637 50.26548 153.93804
定义函数就好比创造一个模具,调用函数就好比用模具批量生成产品。使用函数最大的好处就是将某个功能封装成模具,从而可以反复使用。这就避免了写大量重复的代码,程序的可读性也大大加强。
高级编程语言提倡向量化编程[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。异步图书的出版领域包括软件开发、大数据、人工智能、测试、前端、网络技术等。
异步社区
微信服务号
本章介绍R语言基本语法,也是与其他编程语言相通的部分,包括搭建R环境、常用数据类型(数据结构)、控制结构(分支、循环)、自定义函数。
本章的目的是让读者打好R语言基本语法的基础,训练函数式编程思维。基本语法是编程元素和语法规则,所有程序都是用它们组合出来的。函数式编程是训练数据思维的基础。函数式编程和数据思维是R语言编程的核心思维,这些也是学习本书和学习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软件界面
不要直接使用R,建议使用更好用的R语言集成开发环境Rstudio。
下载并安装(或直接下载zip版解压)RStudio之后,RStudio将自动关联已安装的R。打开RStudio,操作界面各窗格及功能简介如图1.2所示。
图1.2 RStudio操作界面
● 切换安装扩展包的国内镜像源(任选其一),操作界面如图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
都加载之前的内存数据。
扩展包(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
,大家可以先安装devtools
或remotes
包,再通过install_github()
安装GitHub
来源的包:
devtools::install_github(“tidyverse/dplyr”) # 或者
remotes::install_github(“tidyverse/dplyr”)
在以上命令中,::
的前面是包名,这是不单独加载包而使用包中函数的写法。
tidyverse
是GitHub
用户名,dplyr
为该用户的名为dplyr
的repository
(仓库),也是包名。此外,通过“包名::”前缀可以访问包的内部函数。注意,不是所有的仓库都是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] 可以用添加压缩包再解压的方式,这样速度能快一些。
library(openxlsx)
update.packages(“openxlsx”)
update.packages() # 更新所有包
remove.packages(“openxlsx”)
getwd()
setwd(“D:/R-4.2.2/tests”)
特别注意:
路径中的“\”必须用“/ ”或“\\”代替。
提示:关于更新R版本和更新包,笔者一般是紧跟最新R版本,顺便重新安装一遍所有包。为了省事,笔者是将所有自己常用包的安装命令(install.package(“...”)
)都放在一个R脚本中,并选择国内镜像,再全部运行即可。
R标准语法中赋值不是用 =,而是 <- 或 ->,代码如下:
x <- 1:10
x + 2
## [1] 3 4 5 6 7 8 9 10 11 12
R也允许用“=
”赋值,建议用更现代和简洁的“=
”赋值。
在R中,为一行代码添加注释语句用 #。
● 数学运算
❏ + - * /
、^
(求幂)、%%
(按模求余[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)
而不继续往下计算;
而&
和|
是向量运算符,对向量中所有元素分别进行运算。
● R的基本数据类型
❏ numeric
— 数值型,又分为integer
(整数型)和double
(浮点型);
❏ logical
— 逻辑型,只有TRUE
和FALSE
,或T
和F
;
❏ 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
的结构。
save(x, file = “data/dat.Rda”)
load(“data/dat.Rda”)
关于相对路径与绝对路径
编程中的文件路径,可以用绝对路径也可以用相对路径。
绝对路径是从盘符开始的完整路径,比如E
:/R
语言
/data/a123.csv
。
相对路径是相对于当前路径的路径,因为通常操作的文件都是在当前路径,那么“从盘符到当前路径”这部分是大家所共有的,所以可以省略不写,只写从当前路径再往下的路径即可。比如,当前文件夹E:/R
语言中有data
文件夹,里面有数据文件a123.csv
,要想访问到它的路径,只需写data/a123.csv
。
按“Ctrl + L”组合键或单击命令窗口右上角的“小刷子”图标可对命令窗口清屏。
若要清除当前变量,使用以下命令:
rm(x) # 清除变量x
rm(list = ls(all = TRUE)) # 清除所有当前变量
注意:
单击Environment窗口的“小刷子”图标也可以清除所有当前变量。
学习编程语言最好的资料就是帮助。
● 函数帮助
命令窗口执行:
?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;
● 统计之都。
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项目,可以完成访问、编辑文件和运行脚本等操作。
后缀名为 .Rmd
的交互式文档,是markdown
语法与R脚本的结合,可以将可执行的R代码和不可执行的文字叙述,融为一个文件。
单击New File按钮,选择R Markdown即可创建,建议优先使用自带的模板和来自网络的现成模板。
R Markdown适合编写包含R语言代码的学习笔记、演示文档、论文等,可以生成docx
、
pptx
、
html
、
pdf
等多种文档格式。更多有关R Markdown的内容将在第6章展开讨论。
此外,近期RStudio推出了新一代文档沟通工具Quarto,其用法与R Markdown基本一致,具体使用方法请参阅官方文档。
数据结构是为了便于存储不同类型的数据而设计的数据容器。学习数据结构,就是要把各个数据容器的特点、适合存取什么样的数据理解透彻,只有这样才能在实践中选择最佳的数据容器,数据容器选择得合适与否,直接关系到代码是否高效简洁,甚至关系到能否解决问题。
R中常用的数据结构可划分为:
● 同质数据类型(homogeneous),即所存储的一定是相同类型的元素,包括向量、矩阵、多维数组;
● 异质数据类型(heterogeneous),即可以存储不同类型的元素,这大大提高了存储的灵活性,但同时也降低了存储效率和运行效率,包括列表、数据框。
另外,还有字符串、日期时间数据、时间序列数据、空间地理数据等。
R中的数据结构还有一种从广义向量(可称之为序列)[4]的角度进行划分。
[4] 广义向量由一系列可以根据位置索引的元素构成,元素可以是复杂类型的,也可以是不同类型的。
● 原子向量:各个值都是同类型的,包括6种类型:logical
、integer
、double
、character
、complex
、raw
,其中integer
和double
也统称为numeric
。
● 列表:各个值可以是不同类型的,NULL
表示空向量(长度为0的向量)。
向量有两个属性:type
(类型)和length
(长度);还能以属性的方式向向量中任意添加额外的metadata
(元数据),属性可用来创建扩展向量,以执行一些新的操作。常用的扩展向量有:
● 基于整数型向量构建的因子;
● 基于数值型向量构建的日期和日期时间;
● 基于数值型向量构建的时间序列;
● 基于列表构建的数据框和tibble
。
列表是序列,从这个角度有助于理解purrr::map_*()
系列的泛函式编程。
向量是由一组相同类型的原子值构成的序列,可以是一组数值、一组逻辑值、一组字符串等。
常用的向量有数值向量、逻辑向量、字符向量。
数值向量就是由数值组成的向量,单个数值是长度为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
逻辑向量是由逻辑值(TRUE
或FALSE
,或简写为T
或F
)组成的向量。
对向量做逻辑运算,得到的结果是逻辑向量:
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%
表示属于,用于判断(左边)元素是否属于(右边)集合。
● ==
表示等于,用于判断(左边)元素是否等于(右边)元素。
字符(串)向量,是由一组字符串组成的向量,在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)向量。
访问向量子集即访问向量的一些特定元素或者某个子集。注意,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中最小值所在的位置
为向量子集赋值,就是先访问到向量子集,再赋值。
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
你可以在创建向量的同时对其每个元素命名,代码如下:
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”]]
向量排序函数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
。
矩阵是用两个维度表示和访问的向量。因此,适用于向量的性质和方法大多也适用于矩阵,矩阵也要求元素是同一类型,如数值矩阵、逻辑矩阵等。
函数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()
可将矩阵转化为向量,其元素是按列读取的。
矩阵是用两个维度表示和访问的向量,可以用一个二维存取器[,]
来访问,这类似于构建向量子集时用的一维存取器[]
。
我们可以为每个维度提供一个向量来确定一个矩阵的子集。方括号中的第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
● A+B
,
A-B
,
A*B
,
A/B
:矩阵四则运算要求矩阵同型,类似MATLAB中的点运算,分别将对应位置的元素做四则运算;
● A %*% B
:矩阵乘法要求矩阵A的列数等于矩阵B的行数。
具体来说,多维数组就是一个维度更高(通常大于2)、可访问的向量,是向量/矩阵向更高维度的自然推广。多维数组也要求元素是同一类型。
用函数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"))
第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的各维度的维数
在想象多维数组时,为了便于形象地理解,可以将其维度依次想象成与“书”相关的概念:行、列、页、本、层、架、室……
列表(list)可以包含不同类型的对象,甚至可以包括其他列表。列表的灵活性使得它非常有用。
例如,用R拟合一个线性回归模型,其返回结果就是一个列表,其中包含了线性回归的详细结果,如线性回归系数(数值向量)、残差(数值向量)、QR分解(包含一个矩阵和其他对象的列表)等。因为这些结果全都被打包到一个列表中,就可以很方便地提取所需信息,而无须每次调用不同的函数。
列表最大的好处就是能够将多个不同类型的对象打包到一起,以便可以根据位置和名字访问它们。
可以用函数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”)
提取列表中某成分下的内容,最常用的方法是用$
,通过成分名字来提取该成分下的内容:
l1$y
l1$m # 访问不存在的成分m, 将会返回NULL
也可以用[[n]]
来提取列表第n
个成分的内容,n
也可以换成成分的名字:
l1[[2]] # 同l1[[“y”]]
用[[]]
提取列表中某个成分的内容则更加灵活,可用在函数调用中,通过参数来传递成分的名字或索引:
p = “y” #想要提取其内容的成分名字
l1[[p]]
R语言也经常需要从列表中提取多个成分及其内容,由这些成分组成的列表构成了原列表的一个子集。
就像提取向量和矩阵的子集一样,提取一个列表子集是用[]
,可以取出列表中的一些成分,作为一个新的列表。
在[]
中可以用字符向量表示成分名字,用数值向量表示成分位置,或用逻辑向量指定是否选择来取出列表成分。
l1[“x”] # 同l1[1]
l1[c(“x”, “z”)] # 同l1[c(1, 3)], l1[c(TRUE, FALSE, TRUE)]
用[]
提取若干成分时,返回列表的子集,还是一个列表;用[[ ]]
提取单个成分的元素,返回的是对应成分的元素。
总之,[]
用于提取对象的子集,类型仍是该对象;[[]]
用于提取对象的内容(即下一级元素)。
即先访问(提取)到列表的成分,再赋以相应的值。注意,若给一个不存在的成分赋值,列表会自动地在对应名称或位置下增加一个新成分。
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
用函数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()
:摊平列表(只摊平一层)。
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
,而不自动简化为向量。
用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.frame
和matrix
这种各成分等长度的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
数据框是由列向量组成、有着矩阵形式的列表,可以用两种操作方式来访问数据框的元素和子集。
若把数据框看作由向量组成的列表,则可以沿用列表的操作方式来提取元素或构建子集。例如,可以用 $
按列名来提取某一列的值,或者用[[]]
按照位置或列名提取。
例如,提取列名为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
以列表形式操作并不支持行选择,以矩阵形式操作则更加灵活。若将数据框看作矩阵,其二维形式的存取器可以很容易地获取一个子集的元素,同时支持列选择和行选择。
换句话说,可以使用[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
的行,并选择id
和y
两列:
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
给数据框赋值就是选择要赋值的位置,再准备好同样大小且格式匹配的数据,赋值给那些位置即可,同样有列表方式和矩阵方式。
用 $
或[[ ]]
对数据框的某列赋值
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))
以列表方式对数据框进行赋值时,也是只能访问列。若需要更加灵活地进行赋值操作,可以通过矩阵方式进行。
df[1:3,”y”] = c(-1,0,1)
df[1:2,c(“x”,”y”)] = list(c(0,0), c(0.9,1.0))
把函数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
数据(变量)可划分为:定量数据(数值型)、定性数据(分类型),定性数据又分为名义型(无好坏顺序之分,如性别)、有序型(有好坏顺序之分,如疗效)。
R提供了因子(factor)这一数据结构(容器),专门用来存放名义型和有序型的分类变量。因子本质上是一个带有水平(level)属性的整数向量,其中“水平”是指事前确定可能取值的有限集合。例如,性别有两个水平属性:男、女。
直接用字符向量也可以表示分类变量,但它只有字母顺序,不能规定想要的顺序,也不能表达有序分类变量。所以,有必要把字符型的分类变量转化为因子型,这更便于对其做后续描述汇总、可视化、建模等。
函数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
做排序、统计频数、绘图,你会发现与使用无序因子时没有任何区别,它们的区别体现在对其建模时所适用的模型不同。
函数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
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 按频数排序的条形图
字符串是用双引号或单引号括起来的若干字符,建议用双引号,除非字符串中包含双引号。字符串构成的向量,简称为字符向量。
字符串处理不是R语言的主要功能,但也是必不可少的,数据清洗、可视化等操作都会用到。
tidyverse
系列中的stringr
包提供了一系列接口一致的、简单易用的字符串操作函数,足以代替R自带的字符串函数。这些函数都是向量化的,即作用在字符向量上,对字符向量中的每个字符串做某种操作。
library(stringr)
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”)
用于设定操作的方向。
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”
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”
只要在字符串内使用“{
变量名
}
”,那么函数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.
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] “梨” “苹果” “香蕉”
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
根据指定的起始和终止位置提取子字符串,基本格式为:
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”
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”
用新字符串替换查找到的匹配字符串。
做字符替换,基本格式为:
str_replace(string, pattern, replacement)
pattern
:要替换的子字符串或模式。
replacement
:要替换为的新字符串。
x
## [1] “1978-2000” “2011-2020-2099”
str_replace(x, “-”, “/”)
## [1] “1978/2000” “2011/2020-2099”
● 大小写转化。
❏ 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)表示模式。
日期时间值通常以字符串形式传入R中,然后转化为以数值形式存储的日期时间变量。
R的内部日期是以1970年1月1日至今的天数来存储,内部时间则是以1970年1月1日零时至今的秒数来存储。
tidyverse系列的lubridate
包提供了更加方便的函数,可以生成、转换、管理日期时间数据,足以代替R自带的日期时间函数。
library(lubridate)
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”
用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下午”
日期时间数据中的“年、月、日、周、时、分、秒”等称为其组件。常用的日期时间组件如表1.2所示。
表1.2 常用的日期时间组件
符号 |
描述 |
示例 |
---|---|---|
|
数字表示的日期 |
(01~31) |
|
缩写的星期名 |
Mon |
|
非缩写的星期名 |
Monday |
|
数字表示的星期几 |
(0~6),0为周日 |
|
数字表示的月份 |
(00~12) |
|
缩写月份 |
Jan |
|
非缩写月份 |
January |
|
二位数年份 |
21 |
|
四位数年份 |
2021 |
|
24小时制小时 |
(00~23) |
|
12小时制小时 |
(01~12) |
|
AM/PM指示 |
AM/PM |
|
十进制分钟 |
(00~60) |
|
十进制秒 |
(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)
:回滚到上月最后一天或本月第一天。
● 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”
用“时间点+时间段”可以生成一个新的时间点:
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”
为了研究某一事件的规律,依据时间发生的顺序将事件在多个时刻的数值记录下来,就构成了一个时间序列,用表示。
例如,国家或地区的年度财政收入、股票市场的每日波动、气象变化、工厂按小时观测的产量等。另外,随温度、高度等变化而变化的离散序列,也可以看作时间序列。
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
。
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.3所示。
表1.3 常用的元字符
符号 |
描述 |
---|---|
|
匹配除换行符“/n”以外的任意字符 |
|
转义字符,匹配元字符时,使用“ |
|
表示或者,即“ |
|
匹配字符串的开始 |
|
匹配字符串的结束 |
|
提取匹配的字符串,即括号内的看作一个整体,即指定子表达式 |
|
可匹配方括号内任意一个字符 |
|
前面的字符或表达式的重复次数: |
|
前面的字符或表达式重复0次或更多次 |
|
前面的字符或表达式重复1次或更多次 |
|
前面的字符或表达式重复0次或1次 |
其他编程语言中的转义字符一般是“\”。默认情况下,正则表达式区分大小写,要创建忽略大小写的正则表达式,代码如下:
pat = fixed(pattern, ignore_case = TRUE)
在多行模式下,^
和$
就表示行的开始和结束,创建多行模式的正则表达式的代码如下:
pat = regex(“^\\(.+?\\)$”, multiline = TRUE)
正则表达式中常用的特殊字符及其反义如表1.4所示。
表1.4 特殊字符类及其反义
符号 |
描述 |
---|---|
|
匹配数字,匹配非数字 |
|
匹配空白符,匹配非空白符 |
|
匹配字母或数字或下划线或汉字,匹配非 |
|
匹配单词的开始或结束的位置,匹配非 |
|
匹配水平间隔,匹配非水平间隔 |
|
匹配垂直间隔,匹配非垂直间隔 |
|
匹配除……以外的任意字符 |
● \\S+
:匹配不包含空白符的字符串。
● \\d
:匹配数字,同[0-9]
。
● [a-zA-Z0-9]
:匹配字母和数字。
● [\\p{han}]
或[\u4e00-\u9fa5]
:匹配汉字。
● [^aeiou]
:匹配除aeiou
之外的任意字符,即匹配辅音字母。
正则表达式中还可以使用POSIX字符类,如表1.5所示。
表1.5 POSIX字符类
符号 |
描述 |
---|---|
|
小写字母 |
|
大写字母 |
|
大小写字母 |
|
数字0~9 |
|
字母和数字 |
|
空白符包括空格、制表符、换行符、中文全角空格等 |
|
控制字符 |
|
标点符号包括“ |
|
空格字符:空格、制表符、垂直制表符、回车、换行符、换页符 |
|
十六进制数字: |
|
打印字符: |
|
图形化字符: |
圆括号括起来的表达式最优先,其次是表示重复次数的操作(即“*”“+”“{ }”
);再次是连接运算(即几个字符放在一起,如abc
);最后是或运算(|
)。
另外,正则表达式还有若干高级用法,常用的有零宽断言和分组捕获,这些将在后面的实例中进行演示。
以上正则表达式语法组合起来使用,就能产生非常强大的匹配效果,对于匹配到的内容,根据需要可以提取它们,也可以替换它们。
正则表达式与stringr
包连用
若只是调试和查看正则表达式的匹配效果,可用str_view()
及其_all
后缀版本,匹配结果将在RStudio的Viewer窗口显示,在原字符向量中高亮显示匹配内容,非常直观。
若要提取正则表达式匹配到的内容,则用str_extract()
及其_all
后缀版本。
若要替换正则表达式匹配到的内容,则用str_replace()
及其_all
后缀版本。
使用正则表达式关键在于能够从貌似没有规律的字符串中发现规律性,再将规律性用正则表达式语法表示出来。下面看几个正则表达式比较实用的实例。
该方法适合想要匹配的内容具有一定规律性,该规律性可用正则表达式表示出来。比如,数据中包含字母、符号、数值,我们想提取其中的数值,可以按正则表达式语法规则直接把要提取的部分表示出来:
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个字符串。
该方法适合想要匹配的内容没有规律性,但该内容位于两个有规律性的标志之间,标志也可以是开始和结束。
通常想要匹配的内容不包含两边的“标志”,这就需要用零宽断言。简单来说,就是引导语法既要匹配到“标志”,但又不包含“标志”。左边标志的引导语法是(?<=
标志
)
,右边标志的引导语法是(?=
标志
)
,而把真正要匹配的内容放在它们中间。
比如,来自问卷星“来自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)”
在正则表达式中可以用圆括号来分组,作用是:
● 确定优先规则;
● 组成一个整体;
● 拆分出整个匹配中的部分内容(称为捕获);
● 捕获内容供后续引用或者替换。
比如,来自瓜子二手车的数据:若汽车型号是中文,则品牌与型号中间有空格;若汽车型号为英文或数字,则品牌与型号中间没有空格。
若用正则表达式匹配“字母或数字”并分组,然后捕获该分组内容并添加空格以替换原内容,代码如下所示:
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.13所示。
图1.13 分支结构示意图
R语言中条件语句的一般格式为:
if(条件) {
执行体
}
if(条件) {
执行体1
} else {
执行体2
}
例如,计算,代码如下:
if(x < 0) {
y = -x
} else {
y = x
}
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节。
if(score >= 90) {
res = "优”
} else if(score >= 80) {
res = "良”
} else if(score >= 70) {
res = "中”
} else if(score >= 60) {
res = "及格”
} else {
res = "不及格”
}
注意:若先写“score
>=60
”,结果就不对了。
关于“条件”
● “条件”用逻辑表达式表示,必须返回一个逻辑值TRUE
或FALSE
;
● 多个逻辑表达式,可以通过逻辑运算符组合以表示复杂条件;
● 多个逻辑值的逻辑向量可以借助函数any()
和all()
得到一个逻辑值;
● 函数ifelse()
可简化代码,仍以计算为例:
ifelse(x < 0, -x, x)
编程时可以减少代码重复的两个工具,一个是循环,另一个是函数。
循环用来对多个同类输入做相同事情(即迭代),例如对向量的每个元素做相同操作,对数据框的不同列做相同操作,对不同的数据集做相同操作等。循环结构如图1.14所示。
图1.14 循环结构示意图
R语言循环迭代的三层境界如下所示。
● 第一层:for
循环、while
循环、repeat
循环。
● 第二层:apply
函数族。
● 第三层:purrr
泛函式编程。
关于跳出循环有以下两种方式。
● 用关键字next
跳出本次循环,进入下次循环。
● 用关键词break
跳出循环。
实用场景
关于“for
循环运行速度慢”的说法,实际上已经过时了,现在的R、MATLAB等软件经过多年的优化已经不慢了,之所以表现出来慢,是因为你没有注意两个关键点:
● 提前为保存循环结果分配存储空间;
● 为循环体中涉及的数据选择合适的数据结构。
apply
函数族和purrr
泛函式编程能够更加高效简洁地实现一般的for
循环和while
循环,但这并不代表for
循环、while
循环就没用了,它们可以在更高的层次使用(相对于在逐元素级别使用)。
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
赋一个1
~
4
中的值,可将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]])
● ……
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()
。
所以,遇到上述模式时,要先转化为更复杂的结果对象,最后再做合并。
适用于迭代次数未知的情况。
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
循环并不常用,但在模拟时也较常用,特别是预先不知道迭代次数的情形。
重复执行循环体,直到满足退出条件:
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
建议弃用apply
函数族,直接用purrr::map
系列。
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
tapply()
函数该函数可以按照因子分组,实现逐分组迭代:
height = c(165, 170, 168, 172, 159)
sex = factor(c(“男”, “女”, “男”, “男”, “女”))
tapply(height, sex, mean)
## 男 女
## 168.3333 164.5000
注意,height
与sex
是等长的向量,对应元素分别为同一人的身高和性别,tapply()
函数分男女两组计算了身高平均值。
lapply()
函数lapply()
函数是一个最基础的循环操作函数,用来对vector
、list
、data.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
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
相对于apply
族,purrr
泛函式循环迭代提供了更多的一致性、规范性和便利性,更容易记住和使用。
循环迭代
循环迭代就是将函数依次应用(映射)到序列的每一个元素上,做相同的操作。而序列是由一系列可以根据位置索引的元素构成,元素可以很复杂,也可以是不同类型的。原子向量和列表都是序列。
泛函式编程
泛函其实就是函数的函数,在编程中表示把函数作用在函数上,或者说函数包含其他函数作为参数。
循环迭代本质上就是将一个函数依次应用(映射)到序列的每一个元素上,用泛函式表示即map(x, f)
[8]。
[8] 将序列(要操作的数据)作为第一个参数x,是为了便于使用管道。
purrr
泛函式编程解决循环迭代问题的逻辑是:针对序列中每个单独的元素,怎么处理它能得到正确的结果,将这个过程定义为函数,再map
(映射)到序列中的每一个元素,将得到的多个结果(每个元素作用后返回一个结果),再打包到一起返回,并且可以根据想要的结果类型选用对应的map
后缀。
对循环迭代返回类型的控制
map
系列函数都有后缀形式,以决定循环迭代之后返回的数据类型,这是purrr
比apply
函数族更先进和便利的一大优势。常用后缀如下。
● 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)
。
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构成的序列
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构成的序列
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,..., 返回值m) = 函数名(输入1, ..., 输入n)
你只要把输入给它,它就能在内部进行相应处理,把你想要的返回值给你。
这些输入和返回值,在函数定义时,都要有固定的类型(模具)限制,叫作形参(形式上的参数);在函数调用时,必须给它对应类型的具体数值,才能真正地去做处理,这叫作实参(实际的参数)。定义函数就好比创造一个模具,调用函数就好比用模具批量生成 产品。
使用函数最大的好处,就是将实现的某个功能封装成模具,从而可以反复使用。这就避免了写大量重复的代码,程序的可读性也大大加强。
以前文的将百分制分数转化为五级制分数为例,如果有一个百分制分数,就这样转化一次,那么如果有10个学生分数,就得写100多行代码。因此有必要封装成一个函数。
在R中,自定义函数的一般格式为:
函数名 = function(输入1, ..., 输入n) {
函数体
return(返回值)
}
注意,return
并不是必需的,默认将函数体最后一行的值作为返回值,也就是说“return(
返回值)
”完全可以换成“返回值”。
我们想要自定义一个函数,能够实现把百分制分数转化为五级制分数的功能。
基于前面对函数的理解,我们按以下步骤进行。
第一步,分析输入和输出,设计函数外形。
● 输入有几个,分别是什么,适合用什么数据结构存放。
● 输出有几个,分别是什么,适合用什么数据结构存放。
对于本问题,输入有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
}
要调用自定义函数,必须要先加载到当前变量窗口(内存),有两种方法:
● 需要选中并执行函数代码;
● 将函数保存为同名的Score_Conv.R
文件,然后执行source(“Score_Conv.R”)
。
之后就可以调用函数了,给它一个实参76,输出结果为“中”:
Score_Conv(76)
## [1] “中”
关于向函数传递参数
要调用一个函数,比如f(x, y)
,首先要清楚其形参x
和y
所要求的类型,假设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) # 效果同上
我们希望自定义函数也能处理向量输入,即输入多个百分制分数,能一下都转化为五级分数。这也是所谓的“向量化编程”思维,就是要习惯用向量(矩阵)去思考、去表达。
方法一:修改自定义函数
将输入参数设计为数值向量,函数体也要相应地修改,借助循环依次处理向量中的每个元素,就相当于再套一层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] “不及格” “及格” “优”
若自定义函数需要有多个返回值,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
有时候需要为输入参数设置默认值。以前面的计算数值向量的均值和标准差的函数为例。我们知道,标准差的计算公式有两种形式,一种是总体标准差除以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
一般函数参数只接受一个对象,即使不指定参数名,也会按位置对应参数。例如:
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的自带函数都在用“...
”传递参数。 若参数“...
”后面还有其他参数,为了避免歧义,调用函数时需要对其随后的参数进行命名。
除了自定义函数,还可以使用现成的函数。
● 来自Base R的函数:可直接使用。
● 来自各种扩展包的函数:需载入包,或加上包名前缀: “ 包名::函数名()”。
这些函数的使用,可以通过“?
函数名
”查阅其帮助,以及查阅包页面的Reference manual和Vignettes(若有)。
下面对常用的R自带的函数做分类总结。
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的共轭复数
sin(x) # 正弦函数
cos(x) # 余弦函数
tan(x) # 正切函数
asin(x) # 反正弦函数
acos(x) # 反余弦函数
atan(x) # 反正切函数
sinh(x) # 双曲正弦函数
cosh(x) # 双曲余弦函数
tanh(x) # 双曲正切函数
asinh(x) # 反双曲正弦函数
acosh(x) # 反双曲余弦函数
atanh(x) # 反双曲正切函数
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的下三角矩阵
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
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
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
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会员卡,近千本电子书免费畅读。