书名:R语言编程指南
ISBN:978-7-115-46264-0
本书由人民邮电出版社发行数字版。版权所有,侵权必究。
您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。
我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。
如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。
• 著 任 坤
译 王 婷 赵孟韬 王泽贤
责任编辑 胡俊英
• 人民邮电出版社出版发行 北京市丰台区成寿寺路11号
邮编 100164 电子邮件 315@ptpress.com.cn
• 读者服务热线:(010)81055410
反盗版热线:(010)81055315
Copyright ©2016 Packt Publishing. First published in the English language under the title Learning R Programming.
All rights reserved.
本书由英国Packt Publishing公司授权人民邮电出版社出版。未经出版者书面许可,对本书的任何部分不得以任何方式或任何手段复制和传播。
版权所有,侵权必究。
R 是一个开源、跨平台的科学计算和统计分析软件包,它提供了丰富多样的统计功能和强大的数据分析功能。随着数据科学的快速发展,R已经成为数据分析领域非常流行的语言。
本书通过15章内容,向读者全面讲解了R的基础知识和编程技巧。本书不仅介绍了R的安装、基本对象、工作空间管理、基本表达式、基本对象操作、字符串的使用等基础内容,还对数据处理、R的内部机制、元编程、面向对象编程、数据库操作、数据操作进行了讲解,同时也涉及高性能计算、网页爬虫和效率提升等重要主题。
本书面向数据领域的从业人员,尤其适合想要通过学习R编程及相关工具提升数据处理效率的读者阅读,也适合计算机或统计相关专业的学生参考使用。通过阅读本书,读者将全面掌握R的相关特性及其在数据处理和分析方面的应用,极大地提升自己的专业技能。
我毕业后一直在量化投资的一线工作,每天大量的工作都是以R语言为主要工具来研究金融数据,期间也在GitHub上开发和维护着几个R扩展包,每年也参加几场R语言会议。在这个过程中,我接触了不少还在学校的初学者,或者已经步入数据相关工作的研究人员,也有一些发来邮件寻求帮助的世界各地的用户。我有这样一种感觉,我们的同学、数据研究者经常有丰富的想法,但原始数据的形式与这些想法常常有相当大的距离。许多用户是因为对工具和编程本身不够熟悉而难以自由地操作数据,因而在面对稍显复杂的问题时便止步不前。如果是这样的原因放慢了我们探索数据世界的脚步,岂不是太可惜了?于是,我萌生了一个想法,写一本关于R语言编程的书。2015年10月,Packt出版社的编辑邀请我写一本面向初学者和专业用户的R语言图书,这正合我的想法!经过一年的时间,便有了本书。
本书与其他R语言图书有一个重要的不同:该书更倾向于帮助读者系统化地认识R作为一门编程语言的设计和行为,通过许多例子和实验帮助读者弄清楚R语言中各种常用数据结构的行为,以及所有这些行为背后统一的设计原则和行为准则。对于许多初学者以及其他编程语言的用户来说,R语言是难以预料、充满怪癖的,至少不是十全十美的。但是,当了解到这些统一的行为准则后,你可能会惊叹R语言本身的一致性,以及表达数据和逻辑的灵活性。这些特性允许我们高效地进行数据探索、分析、可视化、报告等。本书将用一半的篇幅来介绍基本的R语言和对象,然后探索R语言的高级特性,让读者更加深入地理解其行为,形成一个整体的知识脉络。此时,当你写出一个R语言表达式,就能立刻猜想会发生什么,即使和想象的不一样,也能很快找到问题所在。打好了这个基础后,我们会介绍数据相关的主要技术,包括关系性与非关系性数据库,实现快速数据操作的扩展包等。掌握了语言和对象的特性以及流行的扩展工具后,我们就可以随时根据问题选择工具,因而生产力就能大幅提升,可以将主要经历投入在思考和解决业务问题,而非一知半解、绞尽脑汁地去找代码中的纰漏且摸不着头脑。最后,本书介绍了一系列工具,涵盖数据研究的多个方面,读者可以根据自己的需要继续学习。
本书原版为英文版,对于国内的读者可能阅读中文版更加方便。为了保证翻译的质量,我推荐厦门大学经济学院和王亚南经济研究院(WISE)的研究生学弟、学妹们翻译本书。他们来自于一个自发组织的数据科学小组WISER Club,经受过严格的学术训练,参与过多种数据研究项目,对R语言相关的应用已经有相当的经验,并且对推广数据科学充满热情。
希望本书能让你更加深入地了解R语言和相关工具,更加自信、自由地探索数据的海洋。
——任坤
任坤 在量化交易中使用R以及C++和C#已有近4年的时间,他一直致力于开发有用的但社区尚未提供的R包(每天工作8~10小时)。他为其他作者开发的扩展包做出过很多贡献,指出其中存在的问题并给出改进建议。他也是中国R语言大会的重要嘉宾,在R 会议上做过多次演讲。在众多社交媒体中,任坤也受到了广泛关注。此外,他对很多项目都做出了很大贡献,从其GitHub账户可见一斑:
我要感谢我的爱人,她一年前就鼓励我开始编写这本书。同时还要感谢两位编辑,Rohit Kumar Singh和Vivek Pala,以及对本书做出贡献的所有人。他们的工作使本书的编写和出版都非常顺利。
Kelly Black 佐治亚大学数学系的成员。他的研究方向是随机微分方程,在很多场景中都在使用统计软件,包括利用蒙特卡罗模拟进行数据分析、教育评估等。
我很感谢Izzat Contractor的友善和耐心,是他的帮助和指导使本书最终得以出版。
本书译者王婷、赵孟韬、王泽贤是WISER CLUB的成员。WISER CLUB是由经济学院、WISE 硕士研究生和本科生联合自发组织的数据科学互助学习组织,依托于厦门大学经济学院与王亚南经济研究院(WISE)强大的计量和统计背景以及丰富的数据科学资源。它旨在让更多的人了解、学习数据科学知识,分享数据科学在学术界和业界的最新动态,并且与学术界和业界合作向在校同学提供广阔的发展前景与机会。
本书凝结了WISER CLUB全体成员的努力付出。除三位主要译者外,感谢于海悦、杨琬妮对第7章和第15章以及王柳盈对第11章的翻译做出的重要贡献,感谢胡帆、洪祺琳、林嘉文、段孙蓬等WISER CLUB全体同学的支持与付出。
特别感谢贾茹学姐、黄耀鹏和邓光宏学长对校对工作的大力支持。
R 是为统计计算、数据分析和可视化而设计的。近年来,它已成为数据科学和统计学中最受欢迎的语言。R语言编程很大程度上涉及数据处理。对于不熟悉R语言的人来说,用R进行编程可能是一个挑战。
作为一种动态语言,R可以灵活地使用不同的数据结构,不像C++、Java和C#这类编译型语言那么严格。当我开始使用R处理和分析数据时,发现它的行为很古怪,不可预测,而且有时非常不稳定。
一些数据分析项目并没有在构建模型上做很多工作。相反,数据清洗、处理和可视化花了更多时间。事实上,在代码运行报错或返回的结果很奇怪时,找到问题的根源才是最耗时的。处理编程问题比处理专业领域内的问题更令人受挫,尤其是在遇到错误,但搞了几小时仍然毫无头绪的时候。
但是,随着项目的增多,也积累了更多的经验,我逐渐了解了对象和函数的行为,并且发现R比我想象的更优雅、更稳定。这就是我编这本书的原因,以分享我对R语言编程逐步深入的认识过程。
通过阅读本书,你会对R编程语言及其大量的工具有一个普遍一致的理解,并将学习到提高效率的最佳实践方法,更深入地了解如何使用数据,并且对如何在R中编程,以及用正确的技巧解决问题等更有信心。
第1章“快速入门”讨论了一些有关R的基础内容,包括如何部署R环境,如何在RStudio中编写代码。
第2章“基本对象”介绍基本的R对象及其性质。
第3章“工作空间管理”介绍工作目录、R环境和扩展包库的管理方法。
第4章“基本表达式”介绍R语言的基本表达式:赋值、条件和循环。
第5章“基本对象操作”讨论每个数据分析师都应该了解的基本函数,以便在R中使用基本对象。
第6章“字符串的使用”讨论与字符串相关的R对象,以及一些字符串操作技术。
第7章“数据处理”解释一些简单的读写数据的函数,并通过一些使用基本对象和函数的实际案例进行演示。
第8章“R的内部机制”通过介绍惰性计算、环境、函数和词法作用域,探讨R的计算模式。
第9章“元编程”介绍元编程技术以帮助理解语言对象和非标准化求值。
第10章“面向对象编程”阐释R中众多的面向对象编程系统:S3、S4、RC和社区提供的R6。
第11章“数据库操作”介绍在R中如何使用SQLite和MySQL等流行的关系型数据库,以及MongoDB和Redis等非关系型数据库。
第12章“数据操作”介绍如何使用data.table和dplyr处理关系型数据,以及使用rlist处理非关系型数据的技术。
第13章“高性能计算”讨论R的性能问题和提高计算性能的常用方法。
第14章“网页爬虫”讨论网页、CSS和XPath选择器的基本结构,以及如何使用rvest包从简单的网页中抓取数据。
第15章“效率提升”演示了如何利用R Markdown和shiny app结合交互式图形来提高数据分析报告和展示的效率。
运行书中的示例代码,需要安装R 3.3.0或更高版本,推荐使用RStudio开发环境。
对于第11章,运行示例代码需要一个可用的MongoDB服务器和一个Redis实例。
对于第13章,在Windows操作系统下需要安装Rtools 3.3来创建Rcpp代码,在Linux或macOS操作系统下,则需要gcc工具链。
本书主要面向从事数据相关项目并希望提高工作效率的读者,但可能不适合对编程语言和相关工具一无所知的人阅读。
本书也适用于想要系统地学习R编程语言、相关技术和推荐的扩展包及其实际应用的专业数据分析师。
书中的一些章节对于初学者来说比较高深,尽管阅读这些章节并不要求你是计算机专家或者专业的数据分析师,但我认为对基础编程概念有一些了解并具有数据处理的基本经验,会有助于对本书内容的理解。
在本书中,你会发现一些用于区分不同信息的文本样式。以下是这些样式的示例及其含义的解释。
文本中包含的代码、数据库表名、文件夹名、文件名、文件扩展名、路径、虚拟的网址(URL)、用户输入和推特名称用代码体显示,如下所示:“apply( )
函数也支持数组输入和矩阵输出。”
内联代码(变量和函数名)和代码块的样式设置如下:
x c(1, 2, 3)
class(x)
## [1] "numeric"
typeof(x)
## [1] "double"
str(x)
## num [1:3] 1 2 3
当某个函数名或变量名被选中时,相同的函数名或变量名就会高亮显示:
x rnorm(100)
y rnorm(100) * 0.5
m lm(y ~ x)
coef(m)
第一次出现的术语和重要词汇会以粗体显示。
小技巧:警告或重要的提示出现在这样的框中。
提示:提示和技巧以这种形式出现。
我们欢迎并重视读者的反馈。请让我们知道你对本书的看法——喜欢或不喜欢什么。读者的反馈对我们很重要,它有助于我们推出对读者更有价值的产品。
若想反馈给我们,可直接发送邮件到feedback@packtpub.com,并在邮件主题中注明书名。
如果你精通某一主题,并有兴趣编写或撰稿,请前往作者指南网页:www.packtpub. com/authors。
作为Packt图书的拥有者,我们准备了很多服务来最大化你的消费权益。
你可以在http://www.packtpub.com登录账户并下载本书对应的示例代码。如果你是在其他地方购买的本书,可以访问http://www.packtpub.com/support进行注册,我们会将代码文件直接发到你的邮箱。
你可以按照以下步骤下载代码文件。
1.使用电子邮箱和密码在网站登录或注册。
2.将鼠标指针悬停在顶部的SUPPORT选项卡上。
3.单击Code Downloads & Errata按钮。
4.在Search对话框中输入书名。
5.选择想要下载的代码文件对应的书名。
6.从下拉菜单中选择购买本书的途径。
7.单击Code Download按钮。
也可以访问Packt出版社网站上的网页,单击Code Files按钮下载代码文件,还可以在搜索框中输入书名来访问该网页。请注意,需要登录你的Packt账户才能执行以上步骤。
文件下载好之后,请务必使用以下软件的最新版本进行解压:
本书的代码同时托管在GitHub上:https://github.com/PacktPublishing/learningrprogramming。我们的GitHub主页上还有更多图书和视频资源:https://github.com/PacktPublishing/。快来浏览一下吧!
尽管我们已经尽全力确保本书内容的准确性,但是错误还是在所难免。如果你在我们的某本书中发现了错误(可能是文字或代码错误),请告知,我们将不胜感激。这样,你可以帮助其他读者避免困惑,并帮助我们改进本书的后续版本。如果您发现任何错误,请打开http://www.packtpub.com/submit-errata网页,选择相应图书,单击勘误表的提交表单链接,输入勘误表的详细信息。一旦您的勘误经过验证,我们将接受您提交的内容,并将勘误上传到网站,或追加到该题目现有勘误表的下面。
若想查看之前提交的勘误表,请转到https://www.packtpub.com/books/content/support,并在搜索栏中输入书名,所需信息将显示在勘误部分的下方。
网络上的盗版问题是所有媒体一直面对的问题。在Packt,我们非常严肃认真地保护版权和许可。如果你在网络上发现有关作品的任何形式的盗版版本,请立即向我们提供网址或者网站名称,以便我们及时采取补救措施。
请通过copyright@packtpub.com与我们联系,并附上可疑盗版资料的链接。
衷心地感谢你帮助我们保护作者的权益以及我们为你带来宝贵知识的能力。
如果您对本书的任何方面有任何问题,可以通过questions@packtpub.com与我们联系,我们会尽全力为你解决。
卓越的数据分析需要建立在出色的工具平台上,没有合适的工具,数据分析也是空中楼阁。即使对专家来说,没有得力的分析工具,从大数据集中直接提取模式并得到结论也几乎是不可能的。因此,一款合适的工具,例如 R,会显著地提升处理数据的效率。就我的经验而言,学习一门编程语言就类似于学习一种人类语言。通常情况下,我们先对一门语言有一个全局的了解,激发学习兴趣,然后通过一些小项目进行尝试,这是一条不错的学习路径。而在此之前,若是纠结于词汇和语言的细节就有些本末倒置了。本章对R进行全面概述,以便帮助你快速入门。
本章我们将介绍以下内容:
一旦软件和工具准备就绪,你就可以编写一个简单的R程序来体验它的基本运行方式了。接下来,我们便开始了R的学习之旅,从基础方法到高级技术和应用,一步一步领略R语言的编程之美。
R 是一门强大的编程语言和统计计算环境,也是数据探索、分析和可视化的利器。它是免费、开源的,并且具有一个活跃且强大的、快速成长的社区。在这里,用户和开发者共享彼此的经验,他们贡献了超过7500个扩展包,因此,R 可以处理众多领域的各种各样的问题(参见https://cran.r-project.org/web/views/)。
尽管R编程语言的起源只追溯到1993年,但数据相关行业普遍采用R语言编程,因此在近十年里,R已迅速成长并成为数据科学领域的通用工具。
一般来说,R不仅是一门编程语言,而且是一个综合计算环境,一个活跃且强大的社区,一个快速生长和扩大的生态系统。
作为一门编程语言,R已经演变和发展了20多年。开发者的目标非常清晰,就是使R成为一款简单易用且灵活的,能够综合执行统计计算、数据探索和可视化的工具。
然而,易用性和灵活性通常是相互冲突的。如果可以通过简单单击几个按钮就可以完成多种多样的统计分析任务,那么在实现自定义和自动化,并保证工作的可重复性时就不可能兼具灵活性。另一方面,R可以非常灵活地使用多种函数进行数据转换,构建复杂的图形等,但学习和正确地组合这些函数就会有一定的难度。R良好地平衡了易用性和灵活性,使得它在众多工具中脱颖而出。
作为一个计算环境,R 具有轻量级和安装即用的特点。相比其他著名的统计软件,例如Matlab和SAS,R更小且更容易配置。
在本书中,我们使用RStudio处理绝大部分R中的工作。该集成开发环境提供了丰富的功能,如语法高亮、自动补齐、扩展包管理、图形查看器、帮助查看器、环境查看器以及调试功能。这些功能极大地提高了用户的工作效率。
作为一个社区,R是强大且活跃的。你现在就可以访问Try R(http://tryr.codeschool.com/),通过交互式教程对R的基本知识有个初步的了解。在实际编写代码时,你可能会遇到各种各样的问题,但是不必独自解决所有问题。你可以在google上搜索一个R的问题,就会发现几乎总是可以在Stack Overflow(http://stackoverflow.com/questions/tagged/r)上找到很多解答。如果你的问题没有完全解决,也可以在上面继续提问,往往几分钟就可以得到回答。
如果你需要使用某个扩展包,并且详细地了解它的工作方式,可以访问它的在线存储库(repo)获取源代码。许多存储库托管在GitHub(https://www.github.com)上。在GitHub上,你可以做更多事情。当发现一个扩展包不能正确运行时,你可以提交一份问题报告。如果你需要的某个功能正好契合某个扩展包的开发目的,也可以提交一份需求报告。同样,如果你有兴趣解决某个扩展包的问题或者丰富它的功能,也可以加入该项目,编辑代码并发送合并请求,这样你做的更改就可以被原开发者接收到。如果你的更改被接受了,那么,恭喜你,你就会成为该扩展包的一个贡献者。令人惊奇的是,R和它的数千个扩展包就是被世界各地的贡献者们开发创建的。
作为一个生态系统,R在除IT行业以外的所有数据相关领域中迅速发展壮大。大多数用户并不是专业开发者,而是数据分析师和统计人员。这些用户可能不会写最优质的代码,但是他们有助于拓展R语言的前沿工具,任何人都可以自由地使用这些工具,而不必重新研究开发。
举个例子,假设一个计量经济学家写了一个扩展包,实现了检测某类时间序列模式的新方法。一些用户可能会发现它有趣又有用。还有一些专业用户可能改进了原来的代码使其更快、更通用。不久之后,量化投资者可能会将该方法纳入到交易策略中,因为它可以检测到通常在其投资组合中引起风险的模式。一天结束时,投资者会发现,将计量经济学家的工具应用到现实世界中,借此构建的投资组合风险较小。
这就是R生态系统的工作方式。这也是R在这些领域闪耀的原因之一:它能够快速地将IT行业之外(通常是数据科学、学术界和工业界)的前沿知识应用到生态系统中可用且合适的工具上。换句话说,它有助于将这些领域的知识和数据科学转变为生产力和价值。
在众多统计软件中,R能够脱颖而出,有以下几个方面的原因。
为了安装R,你需要前往R的官网(https://www.r-project.org/)去下载R软件(https://cran.r- project.org/mirrors.html),选择适合的镜像,并下载与你的计算机操作系统相适应的版本。在编写本书时,R最新的版本是3.2.3。本书中的例子就是在这个版本下创建和运行的,适用的操作系统是Windows和Linux,但是使用更早版本的R或者其他操作系统,其运行结果也没有显著差异。
如果你使用的是Windows操作系统,只需下载最新版的安装程序,并运行安装。安装过程简单便捷,但是仍会有许多使用者在某一些步骤上遇到问题。
当你在Windows操作系统下进行安装时,在选择安装内容时,会看到安装列表中有4项内容。其中,Core files是指R的核心库,Message translations提供了关于警告和错误的多种翻译版本。可能使你感到困惑的是选择32-bit Files选项,还是64-bit Files选项。其实不必担心,你只需了解64位版本的R比32位的在单一过程中能够处理更多数据即可。如果你的计算机是最近几年买的,那么,极有可能是64位操作系统,支持64位程序,因此,默认选择64位文件。如果你使用的是32位操作系统,很遗憾的是,你不能使用64位的R,除非你的计算机硬件支持并安装了64位的操作系统。
我建议使用默认选项进行安装,如图1-1所示。
图1-1
另一个可能使你感到困惑的选项是要不要保存许可证中的数字。检查这些选项会使其他程序识别安装的是R的哪个版本更简单。如果你确定仅在R环境中使用,请保持默认选项,直接下一步即可,如图1-2所示。
图1-2
然后,安装程序开始将文件复制到你的硬盘中,如图1-3所示。
图1-3
最终,R就部署在你的计算机中了。此时,你只能通过两种方式使用R:命令提示符(终端)或者R GUI。
如果允许安装程序创建快捷方式到桌面上,你就会发现有两个R的快捷方式。R在命令提示符中运行,R GUI在极其简单的GUI中运行。
尽管你现在就可以启动R,但不意味着就要用这种方式使用它。我强力推荐使用RStudio编辑和调试R脚本。实际上,本书就是在RStudio的R Markdown中编写的。虽然RStudio很强大,但没有正确安装R的话,它也没有办法工作。换句话说,R是底层,RStudio是前端,RStudio帮助你更好地使用R。
如果你使用 Windows 操作系统,也可以安装Rtools(http://cran.rstudio.com/bin/ windows/ Rtools/),这样就可以在R中编写、编译和调用C++代码,也可以安装包含C++代码的扩展包,并在C++的环境中进行编译。
RStudio是R编程语言强大的用户界面。它是免费、开源的,适用于多种操作系统,包括Windows、Mac和Linux。
RStudio具有非常强大的功能,能够极大地提升数据分析和可视化的工作效率。它支持语法高亮、自动补齐、多标签视图、文件管理、图形窗口、扩展包管理、集成帮助查看器、代码格式化、版本控制、交互式调试以及其他功能。
你可以从https://www.rstudio.com/products/rstudio/download下载最新发布的RStudio。如果你想体验预览版本的新功能,就需要从https://www.rstudio.com/products/rstudio/download/ preview下载预览版。注意,RStudio并没有内嵌在R中,所以你需要确保在使用RStudio时已经安装了R。
接下来,我将向你简要介绍RStudio的用户界面。
图1-4展示的是Windows操作系统中的RStudio用户界面。如果你使用Mac OS X或者Linux支持的版本,界面看起来几乎是一样的。
图1-4
你可能注意到,主窗口是由几部分构成。每个部分都称为一个窗格,分别执行不同的函数。这些窗格都是为数据分析师处理数据而精心设计的。
图1-5 展示的是内嵌在RStudio中的R控制台。在大多数情况下,控制台的工作原理和命令提示符或者终端完全一致。实际上,当你在控制台输入一条命令时,RStudio便向R引擎提交请求,由R引擎执行所有命令。RStudio 的作用就是中介传输,将用户的输入提交给R引擎,再将返回的结果呈现出来。
图1-5
使用控制台,你可以很方便地执行一条命令,定义一个变量,或者交互式地执行表达式来计算一个统计测度,转换数据,或者生成图表。
处理数据时,我们通常不在控制台输入命令。相反,我们会编写一份脚本文件,即一组命令的集合,来表示整个操作过程的逻辑流,该脚本文件可以直接读取并由R引擎执行。如图1-6 所示,脚本文件的编写就是在编辑器中完成的。编辑器可用于编辑 R 脚本、markdown 文档、网页,以及许多类型的配置文件,甚至是C++源代码。
图1-6
代码编辑器的功能比纯文本编辑器的功能多很多:它支持语法高亮、自动补齐R代码、断点调试等。具体来说,当编辑R脚本时,你可以使用以下快捷键。
环境窗格展示已经被创建的变量和函数,这些变量和函数可以重复使用。默认情况下,它展示的是全局环境中的变量,即你正在使用的工作空间,如图1-7所示。
图1-7
每次创建一个新的对象(一个变量或函数),环境窗格中就会出现一个新的元素。这个元素展示了变量名和其值的简短描述。当你改变某个符号的值,或者移除这个符号时,实际上是修改了环境,因此环境窗格会反映出你的更改。
历史窗格展示了在控制台执行过的表达式。只需双击命令行或者选中命令行再单击“To Console”,你就可以重复执行之前运行过的任务,如图1-8所示。
图1-8
历史记录保存在工作目录中的.Rhistory
文件中。
文件窗格展示了文件夹中的文件列表。你可以在文件夹之间进行操作:创建新文件夹、删除或重命名文件夹或文件等,如图1-9所示。
图1-9
如果你的工作正在一个RStudio项目上进行,在文件窗格可以很方便地查看和整理项目文件。
绘图窗格用来展示R代码生成的图形。如果你生成了多个图形,前面的图形会被保存下来,单击向前或向后便可以查看所有图形(只要你没有清除它们),如图1-10所示。
图1-10
当你调整绘图窗格的尺寸时,其中的图形会自动调整以适应窗格大小,这样一来,图形看起来就像未曾调整过一样友好,你也可以将图形导出到文件中备用。
R 的很多强大之处来自于它的扩展包。扩展包窗格展示所有安装的扩展包,你可以从CRAN中安装或更新包,或将已有包从库中移除,如图1-11所示。
图1-11
R 的强大之处也源自其非常详细的帮助文档。如图1-12所示,帮助窗格展示帮助文档,这样你就可以很方便地学习如何使用函数。
图1-12
有很多方式可以查看函数的帮助文档。
?
函数名”,并执行它。实战中,你不必记住所有的R函数,只需记住,在面对不熟悉的函数时要如何寻求帮助。
查看器窗格是一项新特征;R包的数量一直在增加,查看器窗格的引入,就是将R和现有的JavaScript库的功能相结合,使数据的展示形式更加丰富,也增加了交互功能。
图1-13就是我编写的formattable(http://renkun.me/formattable)扩展包中的一个例子,这个例子展示了在R的数据框中进行Excel的条件化格式操作的一种简单实现。
图1-13
如果你使用的是基于Linux的版本,就可以轻松地构建RStudio或RStudio Server的服务器版本。这样一来,RStudio在主机服务器(可能比你的笔记本电脑更强大更稳定)上运行,你就可以在网页浏览器中运行RStudio中的R会话。虽然用户界面没有什么差别,但是连接至服务器后,就可以像使用本地计算机一样使用服务器的计算和内存资源。
在这一小节中,我将通过一个简单的例子,演示如何在控制台中输入命令来执行计算、模型拟合和生成图形。
首先,我们创建一个向量x
,它由100个服从正态分布的随机数构成。然后,创建另一个向量y
,它也包含100个数,而且每一个数都是x
中对应数的3倍加2,再加上一些随机噪声。注意,<-是赋值操作符,我们后面会讲到。使用str( )
函数输出向量的结构:
x rnorm(100)
y rnorm(100) * 0.5
str(x)
## num [1:100] -0.4458 -1.2059 0.0411 0.6394 -0.7866 ...
str(y)
## num [1:100] -0.022 -1.536 2.067 4.348 -0.295 ...
既然我们知道x和y之间的真实关系是y = 3x+2+ε,那么,就可以利用x和y的样本拟合一个简单的线性回归,并查看线性模型如何还原线性参数(即2和3)。调用lm(y~x)拟合这个模型:
model1 lm(y ~ x)
模型拟合的结果保存在名为model1
的对象中。我们只需输入model1
或者明确地输入print(model1)
就可以查看模型的拟合情况:
model1
##
## Call:
## lm(formula = y ~ x)
##
## Coefficients:
## (Intercept)
x
## 2.051 2.973
如果你想查看更多细节,可以对model1
调用summary( )
:
summary(model1)
##
## Call:
## lm(formula = y ~ x)
##
## Residuals:
## Min 1Q Median 3Q Max
## -1.14529 -0.30477 0.03154 0.30042 0.98045
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 2.05065 0.04533 45.24 <2e-16 ***
## x 2.97343 0.04525 65.71 <2e-16 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 0.4532 on 98 degrees of freedom
## Multiple R-squared: 0.9778, Adjusted R-squared: 0.9776
## F-statistic: 4318 on 1 and 98 DF, p-value: < 2.2e-16
我们将样本点和拟合的模型展示在图1-14中:
plot(x, y, main = "Simple linear regression")
abline(model1$coefficients, col = "blue")
图1-14
上面这个例子演示了一些简单函数,帮助你对如何使用R建立一个初步印象。即使不熟悉例子中出现的符号和函数,也没有关系,后面的章节会讲解你需要了解的基本对象和函数。
在本章中,你掌握了R的基本情况和主要优点。我们已经学习了如何在Windows操作系统下安装R。为了使R编程更简单,我们选择使用RStudio。通过RStudio的用户界面,你了解了主窗口中各个窗格的功能。最后,我们通过运行几行R命令拟合了一个模型,并绘制了一幅简单的图形,对使用R有了初步的认识。
下一章将介绍R中的基本概念和数据结构,帮助你熟悉R对象的行为。之后,你便可以轻松地展示、操作和处理各种形式的数据。
学习R语言编程的第一步是熟悉基础的R对象和它们的性质。在本章中,你将学到以下内容。
“万物皆对象,万事乃函数。”——John Chambers
例如,在统计分析中,我们经常将一组数据输入到一个线性回归模型中来获得回归系数。
尽管在R中有不同类型的R对象,但在实际工作中,我们仅需提供一个包含数据集的数据框,将其代入一个线性回归模型中,获得一个包含回归结果的信息列表,最后从这一列表中提取出另一种类型的对象——数值向量,来展示回归系数。
每项工作都涉及各种各样的对象,每个对象都有不同的用途和性质。其中非常重要的一点是,要理解一个基础对象在解决现实问题的过程中是如何运作的,尤其是如何用更优雅更简练的代码,以更少的步骤来实现它。更为重要的是,越是深入地了解对象的行为,越能让你有更多的时间用在解决实际问题上,而不是花费大量时间调试编程中的小问题。
在接下来的小节中,我们将看到多种多样的R对象,它们展示了不同类型的数据结构,并且能够使数据集的分析和可视化变得简单易行。你将对这些对象如何工作,以及它们之间如何互动有一个基本的了解。
一个向量是由一组相同类型的原始值构成的序列。它可以是一组数字、一组逻辑值、一组文本或者是其他类型的值。它是所有R对象的基础数据结构之一。
在R中,有多种类型的向量,区别在于它们存储的元素类型互不相同。在接下来的小节中,我们将会看到最常使用的向量类型,包括数值向量、逻辑向量和字符向量。
一个数值向量就是由数值组成的向量。单个数值(标量数值)就是最简单的数值向量。举一个例子:
1.5
## [1] 1.5
数值向量是最常用的数据类型,也几乎是所有数据分析的基础。在其他流行的编程语言中,存在一些标量类型,例如整型、双精度型和字符串型。这些标量是构成如向量等其他容器类型的基础。然而,在R中并没有对于标量类型的正式定义。标量数值只不过是数值向量的特例,并且仅仅特殊之处在其长度为1。
当我们创建一个值的时候,会很自然地想到如何存储它以备后续使用。为了存储这个值,我们可以使用<-将这个值赋给一个“符号”。换句话说,我们为这个值(1.5)创建了一个名为x的变量:
x <- 1.5
这个值被分配给了符号x,之后我们可以用x来表示它:
x
## [1] 1.5
创建一个数值向量有许多种方法。我们可以调用numeric( )
来创建一个由0组成的指定长度的向量:
numeric (10)
## [1] 0 0 0 0 0 0 0 0 0 0
我们也可以使用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
如果想要创建一系列连续的整数值,运算符“:”能够轻松地实现这一点。
1:5
## [1] 1 2 3 4 5
准确地说,以上代码产生的是整数值向量而不是数值向量。在很多情况下,它们之间的区别并不重要。我们之后将会讨论这个问题。
产生一个数值序列,更通用的方法是使用seq( )
。例如,以下代码产生了一个数值向量,它由从1开始,到10结束,步长为2的序列组成:
seq(1, 10, 2)
## [1] 1 3 5 7 9
像seq( )
这样的函数有很多参数。我们可以通过提供其所有的参数来调用这个函数,但在大多数时候并不必要。大部分函数对其部分参数提供了合理的默认值,这使得我们能够更加轻松地调用它们。在现在这种情况下,我们只需指定想要更改的参数即可。
例如,可以通过指定length.out
这个参数来创建另外一个从3开始,长度为10的数值向量:
seq(3, length.out = 10)
## [1] 3 4 5 6 7 8 9 10 11 12
如上所示的函数调用过程,只修改了参数length.out
的取值,其他参数的值保持默认值。
尽管我们可以使用多种方法定义数值向量,但是必须始终小心地使用“:”运算符,举一个例子:
1 + 1:5
## [1] 2 3 4 5 6
从结果来看,1 + 1:5
并不意味着一个从2~5的序列,而是一个从2~6的序列。这是因为“:”比“+”具有更高的优先级,这使得首先产生的是1:5
,接着才对每一个值加1,这才产生了你在结果中所看到的序列。我们之后会讨论关于运算符优先级的问题。
与数值向量不同,一个逻辑向量储存一组TRUE
或FALSE
值。它们基本上以“是”或“否”来表示对一组逻辑问题的回答。
最简单的逻辑向量是TRUE
或者FALSE
本身:
TRUE
## [1] TRUE
获得一个逻辑向量更一般的方法是询问关于R对象的逻辑性问题。例如,我们可以询问R,1是否大于2:
1 > 2
## [1] FALSE
如果回答“是”,就返回TRUE
;如果回答是“否”,就返回FALSE
。有时,写TRUE
或者FALSE
太繁琐了,我们可以使用TRUE
的缩写T
和FALSE
的缩写F
。如果我们想同时进行多个比较,可以直接在问题中使用数值向量:
c(1, 2) > 2
## [1] FALSE FALSE
R 会将这一表达式理解为在c(1, 2)
和2之间依次进行元素比较。换句话说,这实际上等价于c(1 > 2, 2 > 2)
。
我们可以比较两个多元素数值向量,只要较长向量的长度是较短向量长度的整数倍:
c(1, 2) > c(2, 1)
## [1] FALSE TRUE
上述代码等价于c(1 > 2, 2 > 1)
。为了验证两个不同长度的向量是如何比较的,请看下面的例子:
c(2, 3) > c(1, 2, -1, 3)
## [1] TRUE TRUE TRUE FALSE
这个结果可能会使你有些困扰。以上代码的运算机制是不断地循环较短的向量并进行比较,等价于c(2 > 1, 3 > 2, 2 > −1, 3 > 3)
。更明确地说,较短的向量将会不断地循环直到和较长向量中的元素全部完成比较。
在R中,定义了一些二元逻辑运算符,例如“==”表示相等,“>”表示大于,“>=”表示大于或等于,“<”表示小于,“<=”表示小于或等于。此外,R还提供了一些其他运算符,例如用%in%
判断运算符左侧向量的每一个元素是否都包含在运算符右侧的向量中:
1 %in% c(1, 2, 3)
## [1] TRUE
c(1, 4) %in% c(1, 2, 3)
## [1] TRUE FALSE
你可能注意到了所有等式运算符都执行了循环,但是%in%
并没有。相反,它总是通过迭代左侧向量的单个元素,在上述例子中,就像c(1 %in% c(1, 2, 3), 4 %in% c(1, 2, 3))
这样完成运算。
一个字符向量是由字符串组成的向量。这里的一个字符不是指着文学意义上的单独的字母或者符号,而是一个类似this is a string
这样的字符串。双引号和单引号都可以用来生成字符向量,例如:
"hello, world!"
## [1] "hello, world!"
'hello, world!'
## [1] "hello, world!"
我们也可以使用组合函数c( )
来创建一个多元素的字符向量:
c("Hello", "World")
## [1] "Hello" "World"
使用==
来判断两个向量中处于对等位置的值是否相等,这同样适用于字符向量:
c("Hello", "World") == c('Hello', 'World')
## [1] TRUE TRUE
因为单引号 ' 和双引号 " 都可以用来生成字符串并且不影响其含义,所以上述两个字符向量相等:
c("Hello", "World") == "Hello, World"
## [1] FALSE FALSE
上述表达式产生了两个FALSE
,是因为Hello
和World
都不等于Hello, World
。两种引号之间的唯一区别是,当生成一个包含引号的字符串时,它们的行为是不同的。
如果你想要在双引号内部嵌套双引号时,需要用反斜杠(\)来转义内部的双引号,类似使用“生成一个包含了其本身的字符串(一个单元素的字符向量),你需要在字符串内部输入\ 来转义”,用以防止编译时将字符串内部的”当作字符串的末引号。
接下来的例子展示了引号的转义。我们使用函数cat( )
来生成指定文本:
cat("Is \"You\" a Chinese name?")
## Is "You" a Chinese name?
如果你感觉这不易于阅读,也可以使用 ’ 来生成该字符串,这样可以变得更简单:
cat('Is "You" a Chinese name?')
## Is "You" a Chinese name?
也就是说,双引号内部可以嵌套单引号,同样,单引号内部也可以嵌套双引号,即:"允许'存在于没有转义过的字符串中,'也允许"存在于没有转义过的字符串中。
现在我们掌握了关于生成数值向量、逻辑向量和字符向量的基本知识。实际上,在R中也有复数向量(complex vector)和原向量(raw vector)。复数向量是由复数组成的向量,例如c(1 + 2i, 2 + 3i)
。原向量主要存储用十六进制格式表示的原始二进制数据。这两种类型的向量很少使用,但是它们与之前介绍过的3种类型的向量(整数型、逻辑型、字符型)具有很多相同的性质。
在下一节中,我们将学习到几种访问向量子集的方法。通过对向量取子集,你将理解不同类型的向量间是如何相互联系的。
如果想访问一些特定元素或者向量的一个子集,使用向量子集是一个不错的方法。在这一节中,我们将展示几种不同的构建向量子集的方法。
首先,生成一个简单的数值向量并且赋值给v1
:
v1 c(1, 2, 3, 4)
接下来的每一行都是用来获取v1
的特定子集。
例如,提取第2个元素:
v1[2]
## [1] 2
也可以提取第2~4个元素:
v1[2:4]
## [1] 2 3 4
还可以获取除第3个以外的其他所有元素:
v1[-3]
## [1] 1 2 4
这个模式很清晰,我们可以在向量后面的方括号中放入任何一个数值向量获取相应的子集:
a <- c(1, 3)
v1[a]
## [1] 1 3
上述所有例子都是通过位置信息来构造子集,也就是说,通过指定元素的位置来得到一个向量的子集。方括号中使用负数将排除相应位置的元素。需要注意的一点是,在方括号中不能同时使用正数和负数:
v1[c(1, 2, -3)]
## Error in v1[c(1, 2, -3)]: 只有负下标里才能有零
如果使用向量范围之外的位置来获取子向量会发生什么呢?接下来的例子尝试获取向量v1
的子集,范围是v1
的第3~6个(不存在)元素:
v1[3:6]
## [1] 3 4 NA NA
正如我们所看到的一样,在不存在元素的位置用NA替代了缺失值。在现实世界的数据中,缺失值是普遍存在的。一方面,对所有包含NA的数据进行数值运算的结果都是NA,而不是其他不确定的结果;另一方面,因为直接假设数据中不存在缺失值并不恰当,所以我们需要付出额外的工作来处理数据。
另一种构造子集的方法是使用逻辑向量。我们可以输入与要获取向量子集的向量具有相等长度的逻辑向量,以此决定每一个元素是否要被获取:
v1[c(TRUE, FALSE, TRUE, FALSE)]
## [1] 1 3
除此之外,我们也可以给向量中特定的子集重新赋值(覆盖原值):
v1[2] <- 0
在这种情况下,v1
变成:
v1
## [1] 1 0 3 4
也可以同时覆盖处于不同位置的多个元素:
v1[2:4] <- c(0,1,3)
现在,v1
变成:
v1
## [1] 1 0 1 3
同样,重新赋值时,逻辑选择也是适用的:
v1[c(TRUE, FALSE, TRUE, FALSE)] <- c(3, 2)
正如你所期待的,此时v1
变成:
v1
## [1] 3 0 2 3
一个常用的技巧是可以通过逻辑标准来选择元素。例如,以下的代码挑选出v1
中所有不大于2的元素:
v1[v1 <= 2]
## [1] 0 2
这种方法也适用于更复杂的选择标准。下面的例子挑选出v1
中所有满足x2−x+10的元素:
v1[v1 ^ 2 - v1 + 1 >= 0]
## [1] 3 0 2 3
以下代码用0替代所有满足x <= 2的元素:
v1[v1 <= 2] <- 0
像你期待的一样,此时v1
变成:
v1
## [1] 3 0 0 3
如果我们对一个并不存在的元素重新赋值,向量将自动用NA填充未被指定的位置,当作缺失值处理:
v1[10] <- 8
v1
## [1] 3 0 0 3 NA NA NA NA NA 8
命名向量是一种不同于数值向量或逻辑向量的特定向量类型。它指的是该向量中的每一个元素都有相应的名称。我们可以在创建向量的同时对其命名:
x <- c(a = 1, b = 2, c = 3)
x
## a b c
## 1 2 3
这样就可以通过单值字符向量来访问其中的元素:
x["a"]
## a
## 1
也可以通过一个字符向量来获取多个元素:
x[c("a", "c")]
## a c
## 1 3
如果字符向量中含有重复元素,则会选出相应的重复元素。
x[c("a", "a", "c")]
## a a c
## 1 1 3
除此之外,其余所有的能够用于向量中的操作也适用于命名向量。我们可以通过names( )
获取向量中的元素名称:
names(x)
## [1] "a" "b" "c"
向量中的名称不是固定的,可以通过对向量赋予不同的字符向量来更改元素名称:
names(x) <- c("x", "y", "z")
x["z"]
## z
## 3
当不需要元素名称时,也可以用NULL
来移除原有的名称。NULL
表示一个未定义值的特殊对象:
names(x) <- NULL
x
## [1] 1 2 3
你可能会比较好奇,如果访问一个不存在的名称时会发生什么呢?我们对原始的x进行测试:
x <- c(a = 1, b = 2, c = 3)
x["d"]
## <NA>
## NA
直觉上,访问一个不存在的元素应该会报错,但结果却返回一个无名的缺失值:
names(x["d"])
## [1] NA
如果你提供的字符向量中,只有部分名称存在,其余不存在时,返回的向量长度与选择向量(该字符向量)保持一致:
x[c("a", "d")]
## a <NA>
## 1 NA
[ ]
能够创建一个向量子集,[[ ]]
可以提取向量中的元素。我们可以将一个向量比作10盒糖果,你可以使用[ ]
获取其中的3盒糖果,使用[[ ]]
打开盒子并从中取出一颗糖果。
对于简单的向量,使用[ ]
和[[ ]]
取出一个元素会产生相同的结果。但在某些情况下,它们会返回不同的结果。例如,对于命名向量,创建一个子集与提取一个元素将会产生不同的结果:
x <- c(a = 1, b = 2, c = 3)
x["a"]
## a
## 1
x[["a"]]
## [1] 1
糖果盒的比喻比较易于理解。x["a"]
使你得到标签为"a"
的糖果盒,而 x[["a"]]
则使你得到标签为"a"
的糖果盒里面的糖果。
由于[[ ]]
只能用于提取出一个元素,因此不适用于提取多个元素的情况。
x[[c(1, 2)]]
## Error in x[[c(1, 2)]]: attempt to select more than one element
此外,[[ ]]
也不能用于负整数,因为负整数意味着提取除特定位置之外的所有元素。
x[[-1]]
## Error in x[[-1]]: attempt to select more than one element
至此,我们知道了使用含有不存在的位置或名称来创建向量子集时将会产生缺失值。但当我们使用[[ ]]
提取一个位置超出范围或者对应名称不存在的元素时,该命令将会无法运行并产生错误信息:
x[["d"]]
## Error in x[["d"]]: 下标出界
对很多初学者来说,代码中同时使用[[ ]]
和[ ]
可能会感到混乱,并且容易造成误用。此时,你只需记得糖果盒的比喻即可。
有时我们需要在处理向量之前辨别向量的类型。class( )
函数用于判断任意R对象的类型:
class(c(1, 2, 3))
## [1] "numeric"
class(c(TRUE, TRUE, FALSE))
## [1] "logical"
class(c("Hello", "World"))
## [1] "character"
如果我们需要确认某一对象是否为某个特定类型的向量,可以用is.numeric( )
、is.logical( )
、is.character( )
以及其他类似函数进行判断:
is.numeric(c(1, 2, 3))
## [1] TRUE
is.numeric(c(TRUE, TRUE, FALSE))
## [1] FALSE
is.numeric(c("Hello", "World"))
## [1] FALSE
不同类型的向量可以被强制转换为一种特定类型的向量。例如,有些数据是数值字符串,如1和20。如果不进行转换处理,就无法对其进行数值运算。幸运的是,这两个字符串可以转换为数值向量。转换后,R就能够将它们识别为数值数据而不是字符串,这样我们就能对其进行数值运算了。
在演示典型的向量类型转换之前,先创建一个字符向量:
strings <- c("1", "2", "3")
class(strings)
## [1] "character"
正如前面所提到的,字符串不能够直接进行数值运算:
strings + 10
## Error in strings + 10: 二进列运算符中有非数值参数
我们可以用as.numeric( )
将字符向量转换为数值向量:
numbers <- as.numeric(strings)
numbers
## [1] 1 2 3
class(numbers)
## [1] "numeric"
现在就能够对数字进行数值运算了:
numbers + 10
## [1] 11 12 13
is.*
函数(例如is.numeric( )
、is.logical( )
以及is.character( )
)用来检验给定对象的类型,as.*
函数用来转换向量的类型:
as.numeric(c("1", "2", "3", "a"))
## Warning: 强制改变过程中产生了NA
## [1] 1 2 3 NA
as.logical(c(-1, 0, 1, 2))
## [1] TRUE FALSE TRUE TRUE
as.character(c(1, 2, 3))
## [1] "1" "2" "3"
as.character(c(TRUE, FALSE))
## [1] "TRUE" "FALSE"
这似乎意味着所有类型的向量都能转换为其他任意类型。然而,事实上,向量类型转换需要遵循一系列规则。
上述代码块的第一行试图将一个字符向量转换为数值向量,这和我们之前的示例一样。显然,最后一个元素不能被转换为数字,因此相应位置产生了缺失值。除了最后一个元素,其他都完成了转换。
将数值向量转换为逻辑向量的规则是:只有0转换为FALSE
,其他所有非零数字均转换为TRUE
。
所有数据都可以表达成字符形式,因此每种类型的向量均可转换成字符向量。然而,如果数值向量或逻辑向量被强制转换成字符向量,那么除非再被转换回来,否则转换后的向量不能直接与其他数值向量或逻辑向量进行算术运算。这就是上文所述,以下代码无法运行的原因:
c(2, 3) + as.character(c(1, 2))
## Error in c(2, 3) + as.character(c(1, 2)): 二进列运算符中有非数值参数
从上面的例子可知,尽管R没有对数据类型强加严格的规则,但是这并不意味着R足够聪明到可以自动并且精确地执行你想做的事情。在大多数情况下,最好事先确认参与运算的向量类型是正确设定的;否则,可能会发生意想不到的错误。换言之,只有使用正确类型的数据对象,才能进行正确的数学运算。
数值向量的算术运算很简单,主要遵循两个原则:对相应位置的元素进行计算,并自动循环利用较短的向量(循环补齐功能)。下面的例子展示了运算符对数值向量的作用方式:
c(1, 2, 3, 4) + 2
## [1] 3 4 5 6
c(1, 2, 3) -c(2, 3, 4)
## [1] -1 -1 -1
c(1, 2, 3) * c(2, 3, 4)
## [1] 2 6 12
c(1, 2, 3) / c(2, 3, 4)
## [1] 0.5000000 0.6666667 0.7500000
c(1, 2, 3) ^ 2
## [1] 1 4 9
c(1, 2, 3) ^ c(2, 3, 4)
## [1] 1 8 81
c(1, 2, 3, 14) %% 2
## [1] 1 0 1 0
虽然向量元素可以有名称,但并不会对其进行运算。只有左侧向量的元素名称会被保留下来,右侧向量的名称会被忽略:
c(a = 1, b = 2, c = 3) + c(b = 2, c = 3, d = 4)
## a b c
## 3 5 7
c(a = 1, b = 2, 3) + c(b = 2, c = 3, d = 4)
## a b
## 3 5 7
通过以上内容,我们已经了解了数值向量、逻辑向量和字符向量的一些基本性质。向量是最常用的数据结构,同时也是构建其他各种有用对象的基本成分。例如矩阵,它主要应用于统计学和计量经济学理论的公式化简洁表述,并且在表示二维数据和求解线性系统方面有着良好的应用。下一节将会介绍如何在R中创建矩阵,以及它是如何植根于向量的。
矩阵是一个用两个维度表示和访问的向量。因此,适用于向量的性质和方法大多也适用于矩阵。例如,每一种向量(例如数值向量或逻辑向量)都有对应的矩阵形式,即数值矩阵(numeric matrice)、逻辑矩阵(logical matrice)等。
我们可以调用matrix( )
函数将一个向量变成矩阵,方法是设定矩阵的其中一个维度。
matrix(c(1, 2, 3, 2, 3, 4, 3, 4, 5), ncol = 3)
## [,1] [,2] [,3]
## [1,] 1 2 3
## [2,] 2 3 4
## [3,] 3 4 5
设定ncol = 3
意味着我们提供的向量应该被当作一个列数为3 的矩阵(行数自动也为3)。你可能觉得原来的向量不如它的矩阵形式直观。为了让代码对用户更加友好,我们可以按行书写向量:
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
我们可能经常需要创建一个对角矩阵,使用diag( )
函数是最便捷的方式:
diag(1, nrow = 5)
## [,1] [,2] [,3] [,4] [,5]
## [1,] 1 0 0 0 0
## [2,] 0 1 0 0 0
## [3,] 0 0 1 0 0
## [4,] 0 0 0 1 0
## [5,] 0 0 0 0 1
在默认情况下,创建矩阵时不会自动分配行名和列名。当不同的行列有不同的含义时,为其命名就显得必要且直观。在创建矩阵时就可以为行和列命名:
matrix(c(1, 2, 3, 4, 5, 6, 7, 8, 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")
这里我们遇到了两个新对象:一个列表和一种函数,比如rownames(x)<-
。我们将在本章的后续内容中进行讨论。
和处理向量一样,我们不仅需要创建矩阵,也需要从中抽取数据,这称为构建矩阵子集(matrix subsetting)。
矩阵是用两个维度表示和访问的向量,可以用一个二维存取器(accessor)[ , ]
来访问,这类似于构建向量子集时用的一维存取器[ ]
。
我们可以为每个维度提供一个向量来确定一个矩阵的子集。方括号中的第1个参数是行选择器(row selector),第2个是列选择器(column selector)。与构建向量子集一样,可以在两个维度中使用数值向量、逻辑向量和字符向量。
以下代码展示了构建矩阵子集的多种方式:
m1
## c1 c2 c3
## r1 1 4 7
## r2 2 5 8
## r3 3 6 9
提取位于第1行第2列的单个元素:
m1[1, 2]
## [1] 4
也可以通过设定位置范围来构建子集:
m1[1:2, 2:3]
## c2 c3
## r1 4 7
## r2 5 8
若一个维度的参数空缺,则该维度的所有值都会被选出来:
m1[1,]
## c1 c2 c3
## 1 4 7
m1[,2]
## r1 r2 r3
## 4 5 6
m1[1:2,]
## c1 c2 c3
## r1 1 4 7
## r2 2 5 8
m1[, 2:3]
## c2 c3
## r1 4 7
## r2 5 8
## r3 6 9
负数表示在构建矩阵子集时可排除该位置,这和向量中的用法完全一致:
m1[-1,]
## c1 c2 c3
## r2 2 5 8
## r3 3 6 9
m1[,-2]
## c1 c3
## r1 1 7
## r2 2 8
## r3 3 9
注意到矩阵有行名和列名,我们可以使用字符向量来构建子集:
m1[c("r1", "r3"), c("c1", "c3")]
## c1 c3
## r1 1 7
## r3 3 9
需要注意的是,矩阵是一个用两个维度表示和访问的向量,但它本质上仍然是一个向量。因此,向量的一维存取器也可以用来构建矩阵子集:
m1[1]
## [1] 1
m1[9]
## [1] 9
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
所有适用于向量的算术运算符也适用于矩阵,就如同它们也是向量一样。这些运算符在元素上进行运算,除了一些矩阵专用的运算符,例如矩阵乘法%*%
:
m1 + m1
## c1 c2 c3
## r1 2 8 14
## r2 4 10 16
## r3 6 12 18
m1 - 2 * m1
## c1 c2 c3
## r1 -1 -4 -7
## r2 -2 -5 -8
## r3 -3 -6 -9
m1 * m1
## c1 c2 c3
## r1 1 16 49
## r2 4 25 64
## r3 9 36 81
m1 / m1
## c1 c2 c3
## r1 1 1 1
## r2 1 1 1
## r3 1 1 1
m1 ^ 2
## c1 c2 c3
## r1 1 16 49
## r2 4 25 64
## r3 9 36 81
m1 %*% m1
## c1 c2 c3
## r1 30 66 102
## r2 36 81 126
## r3 42 96 150
可以使用t( )
函数对矩阵进行转置:
t(m1)
## r1 r2 r3
## c1 1 2 3
## c2 4 5 6
## c3 7 8 9
很多情况下,向量和矩阵就够用了。然而,一些特定的问题需要使用更高维的数据结构。下一节将简要介绍数组(array),你将会看到这些数据结构有相似的性质。
数组是矩阵向更高维度的自然推广。具体来说,数组就是一个维度更高(通常情况下大于2)、可访问的向量。如果你对向量和矩阵已经很熟悉,就不会对数组的操作方式感到诧异了。
我们可以提供一个向量,然后调用array( )
函数来创建一个数组,指定数组的不同维度,有时也可以给出不同维度的行名和列名。
假设数据是0~9这10个整数,需要将其分配到3个维度中,其中第1维长度为1,第2维长度为5,第3维长度为2:
a1 <- array(c(0, 1, 2, 3, 4, 5, 6, 7, 8, 9), dim = c(1, 5, 2))
a1
## , , 1
##
## [,1] [,2] [,3] [,4] [,5]
## [1,] 0 1 2 3 4
##
## , , 2
##
## [,1] [,2] [,3] [,4] [,5]
## [1,] 5 6 7 8 9
显然,同样可以通过指定位置访问数组中的元素。
此外,还可以在创建数组时对每个维度进行命名:
a1 <- array(c(0, 1, 2, 3, 4, 5, 6, 7, 8, 9), dim = c(1, 5, 2), dimnames
= list(c("r1"), c("c1", "c2", "c3", "c4", "c5"), c("k1", "k2")))
a1
## , , k1
##
## c1 c2 c3 c4 c5
## r1 0 1 2 3 4
##
## , , k2
##
## c1 c2 c3 c4 c5
## r1 5 6 7 8 9
若存在已经创建的数组,可以提供一个包含若干个字符向量的列表,用dimnames(x)<-
对数组的各个维度命名:
a0 <- array(c(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10), dim = c(1, 5, 2))
dimnames(a0) <- list(c("r1"), c("c1", "c2", "c3", "c4", "c5"), c("k1",
"k2"))
a0
## , , k1
##
## c1 c2 c3 c4 c5
## r1 0 1 2 3 4
##
## , , k2
##
## c1 c2 c3 c4 c5
## r1 5 6 7 8 9
从数组中提取子集的原理与矩阵完全相同。我们通过给每个维度提供一个向量来提取数组子集。
a1[1,,]
## k1 k2
## c1 0 5
## c2 1 6
## c3 2 7
## c4 3 8
## c5 4 9
a1[, 2,]
## k1 k2
## 1 6
a1[,,1]
## c1 c2 c3 c4 c5
## 0 1 2 3 4
a1[1,1,1]
## [1] 0
a1[1,2:4,1:2]
## k1 k2
## c2 1 6
## c3 2 7
## c4 3 8
a1[c("r1"),c("c1", "c3"),"k1"]
## c1 c3
## 0 2
细心的读者可能会发现,原子向量、矩阵和数组的性质和操作方式几乎完全相同。最基本的共同特征就是它们都属于同质数据类(homogeneous data types),即所存储的一定是相同类型的元素。但是在R中也存在异质数据类(heterogeneous data types),即可以存储不同类型的元素,这大大提高了存储的灵活性,但同时也降低了存储效率和运行效率。
列表(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"
可以用命名参数(named arguments)为列表中的每个成分指定名称:
l1 <- list(x = 1, y = c(TRUE, FALSE), z = c("a", "b", "c"))
l1
## $x
## [1] 1
##
## $y
## [1] TRUE FALSE
##
## $z
## [1] "a" "b" "c"
有许多方法可以提取列表中的元素。最常用的方法是使用美元符号$
,通过成分的名称来提取列表元素的值。
l1 <- list(x = 1, y = c(TRUE, FALSE), z = c("a", "b", "c"), m = NULL)
l1$x
## [1] 1
l1$y
## [1] TRUE FALSE
l1$z
## [1] "a" "b" "c"
l1$m
## NULL
注意,如果访问一个不存在的成分m
,将会返回NULL
。
或者,我们可以在双层方括号中输入一个数字n,来提取列表中第n个成分的值。比如,通过以下方法取出列表l1
中第2个成分的值:
l1[[2]]
## [1] TRUE FALSE
也可以在双层方括号中输入一个列表成分的名称,以取出对应元素的值,效果类似于美元符号:
l1[["y"]]
## [1] TRUE FALSE
使用双层方括号来提取列表中的值可能会更加灵活,因为在计算之前我们有时可能不知道需要取出哪个元素:
member <- "z" # 你可以随时改变想要取出的成分
l1[[member]]
## [1] "a" "b" "c"
这里在括号中使用了一个可即时赋值的单元素字符向量。但是为什么我们要用双层方括号呢?为什么不再使用单层方括号了呢?
许多场合下,我们需要从列表中提取多个元素。由这些元素组成的列表构成了原列表的一个子集。
构建一个列表子集,我们可以用单层方括号,就像提取向量和矩阵中元素一样。我们可以取出列表中的一些元素,然后放到一个新的列表中。
这里方括号的用法与其在向量中的用法非常相似。我们可以用字符向量表示成分名称,用数值向量表示成分位置,或用逻辑向量指定选择标准,来取出列表元素②。
l1["x"]
## $x
## [1] 1
l1[c("x", "y")]
## $x
## [1] 1
##
## $y
## [1] TRUE FALSE
l1[1]
## $x
## [1] 1
l1[c(1, 2)]
## $x
## [1] 1
##
## $y
## [1] TRUE FALSE
l1[c(TRUE, FALSE, TRUE)]
## $x
## [1] 1
##
## $z
## [1] "a" "b" "c"
总而言之,我们可以说,[[
用来提取向量或列表中的一个元素,而[
用来提取一个向量或列表的子集。向量的子集是一个向量。同样的,列表的子集也是一个列表。
无论在列表创建之初是否有为各列表成分命名,我们总能通过一个用于命名的向量,为这些列表成分命名或重命名。
names(l1) <- c("A", "B", "C")
l1
## $A
## [1] 1
##
## $B
## [1] TRUE FALSE
##
## $C
## [1] "a" "b" "c"
若想移除它们的名称,可以将 l1 的名称赋值为NULL:
names(l1) <- NULL
l1
## [[1]]
## [1] 1
##
## [[2]]
## [1] TRUE FALSE
##
## [[3]]
## [1] "a" "b" "c"
一旦移除了列表成分的名称,就不能再通过名称来访问列表成分,但是仍可以使用位置索引和逻辑准则访问。
在列表中赋值和给向量赋值一样直观:
l1 <- list(x = 1, y = c(TRUE, FALSE), z = c("a", "b", "c"))
l1$x <- 0
如果给一个不存在的成分赋值,列表会自动地在对应名称或位置下增加一个新成分:
l1$m <- 4
l1
## $x
## [1] 0
##
## $y
## [1] TRUE FALSE
##
## $z
## [1] "a" "b" "c"
##
## $m
## [1] 4
也可以同时给多个列表成分赋值:
l1[c("y", "z")] <- list(y = "new value for y", z = c(1, 2))
l1
## $x
## [1] 0
##
## $y
## [1] "new value for y"
##
## $z
## [1] 1 2
##
## $m
## [1] 4
如果想要移除列表中的某些成分,只需赋值为NULL
:
l1$x <- NULL
l1
## $y
## [1] "new value for y"
##
## $z
## [1] 1 2
##
## $m
## [1] 4
还可以同时移除列表中的多个成分:
l1[c("z", "m")] <- NULL
l1
## $y
## [1] "new value for y"
R 中有许多和列表相关的函数。例如,如果我们不能确定一个对象是否是列表,可以调用is.list( )
进行判断:
l2 <- list(a = c(1, 2, 3), b = c("x", "y", "z", "w"))
is.list(l2)
## [1] TRUE
is.list(l2$a)
## [1] FALSE
这里的l2
是一个列表,但l2$a
是一个数值向量而不是列表。
我们也可以调用as.list( )
函数将一个向量转换成一个列表:
l3 <- as.list(c(a = 1, b = 2, c = 3))
l3
## $a
## [1] 1
##
## $b
## [1] 2
##
## $c
## [1] 3
通过调用unlist( )
,可以很容易地将一个列表强制转换成一个向量。该函数基本上对所有列表成分进行了转换,并把它们存储在一个类型兼容的向量中:
l4 <- list(a = 1, b = 2, c = 3)
unlist(l4)
## a b c
## 1 2 3
如果我们对一个混合了数值和文本的列表进行去列表化(unlist),则每个成分都会被转换为其所能转换成的最近类型(closest type):
l4 <- list(a = 1, b = 2, c = "hello")
unlist(l4)
## a b c
## "1" "2" "hello"
这里的l4$a
和l4$b
都是数字,可以被转换为一个字符;但是,l4$c
是一个字符向量,无法被转换为数值。因此,能够兼容这些元素的最近类型就是字符向量。
数据框是指有若干行和列的数据集。它与矩阵类似,但并不要求每列都是相同的类型。这与最常见的数据形式是一致的:每行或每条记录由不同类型的列来描述。
表2-1充分展示了数据框的特点。
表2-1
姓名 |
性别 |
年龄 |
专业 |
---|---|---|---|
Ken |
Male |
24 |
Finance |
Ashley |
Female |
25 |
Statistics |
Jennifer |
Female |
23 |
Computer Science |
我们可以调用data.frame( )
函数,对每一列提供相应类型的列向量来创建一个数据框。
persons <- data.frame(Name = c("Ken", "Ashley", "Jennifer"),
Gender = c("Male", "Female", "Female"),
Age = c(24, 25, 23),
Major = c("Finance", "Statistics", "Computer Science"))
persons
## Name Gender Age Major
## 1 Ken Male 24 Finance
## 2 Ashley Female 25 Statistics
## 3 Jennifer Female 23 Computer Science
注意到,数据框的创建方式与列表完全一致。本质上讲,数据框就是一个列表,该列表的每个成分都是一个向量,并且长度相同,以表格的形式展现。
除了根据原始数据创建数据框,我们也可以对一个列表直接调用data.frame( )
或者as.data.frame( )
将其转换为数据框:
l1 <- list(x = c(1, 2, 3), y = c("a", "b", "c"))
data.frame(l1)
## x y
## 1 1 a
## 2 2 b
## 3 3 c
as.data.frame(l1)
## x y
## 1 1 a
## 2 2 b
## 3 3 c
也可以用同样的方式将矩阵转换为数据框:
m1 <- matrix(c(1, 2, 3, 4, 5, 6, 7, 8, 9), nrow = 3, byrow = FALSE)
data.frame(m1)
## X1 X2 X3
## 1 1 4 7
## 2 2 5 8
## 3 3 6 9
as.data.frame(m1)
## V1 V2 V3
## 1 1 4 7
## 2 2 5 8
## 3 3 6 9
注意到,这种转换会自动给新数据框赋予列名。实际上,可以验证,如果矩阵已经有了列名或者行名,那么它们在转换中会被保留下来。
数据框既是列表的特例,也是矩阵的推广,因此访问这两类对象的方式都适用于数据框。
df1 <- data.frame(id = 1:5, x = c(0, 2, 1, -1, -3), y = c(0.5, 0.2, 0.1,0.5, 0.9))
df1
## id x y
## 1 1 0 0.5
## 2 2 2 0.2
## 3 3 1 0.1
## 4 4 -1 0.5
## 5 5 -3 0.9
与矩阵类似,我们也可以对数据框的行和列重命名:
colnames(df1) <- c("id", "level", "score")
rownames(df1) <- letters[1:5]
df1
## id level score
## a 1 0 0.5
## b 2 2 0.2
## c 3 1 0.1
## d 4 -1 0.5
## e 5 -3 0.9
因为数据框是由列向量组成、有着矩阵形式的列表,所以我们可以用两种操作方式来访问数据框的元素和子集。
如果把数据框看作是由向量组成的列表,我们可以沿用列表的操作方式来提取元素或构建子集。
例如,可以使用$
按列名来提取某一列的值,或者用[[
符号按照位置提取。
df1$id
## [1] 1 2 3 4 5
df1[[1]]
## [1] 1 2 3 4 5
以列表形式构建子集完全适用于数据框,同时也会生成一个新的数据框。提取子集的操作符([
)允许我们用数值向量表示列的位置,用字符向量表示列名,或用逻辑向量指定TRUE
和FALSE
的选择标准,来取出相应的列。
df1[1]
## id
## a 1
## b 2
## c 3
## d 4
## e 5
df1[1:2]
## id level
## a 1 0
## b 2 2
## c 3 1
## d 4 -1
## e 5 -3
df1["level"]
## level
## a 0
## b 2
## c 1
## d -1
## e -3
df1[c("id", "score")]
## id score
## a 1 0.5
## b 2 0.2
## c 3 0.1
## d 4 0.5
## e 5 0.9
df1[c(TRUE, FALSE, TRUE)]
## id score
## a 1 0.5
## b 2 0.2
## c 3 0.1
## d 4 0.5
## e 5 0.9
不过,以列表形式操作并不支持行选择。与此相反,以矩阵形式操作更加灵活。如果我们将数据框看作矩阵,其二维形式的存取器可以很容易地获取一个子集的元素,同时支持列选择和行选择。
换句话说,我们可以使用 [行,列] 指定行或列来提取数据框子集,[ , ] 内可以是数值向量、字符向量或者逻辑向量。
例如,选择指定的列:
df1[, "level"]
## [1] 0 2 1 -1 -3
df1[, c("id","level")]
## id level
## a 1 0
## b 2 2
## c 3 1
## d 4 -1
## e 5 -3
df1[, 1:2]
## id level
## a 1 0
## b 2 2
## c 3 1
## d 4 -1
## e 5 -3
或者,选择指定的行:
df1[1:4,]
## id level score
## a 1 0 0.5
## b 2 2 0.2
## c 3 1 0.1
## d 4 -1 0.5
df1[c("c","e"),]
## id level score
## c 3 1 0.1
## e 5 -3 0.9
也可以同时选择指定的行和列:
df1[1:4, "id"]
## [1] 1 2 3 4
df1[1:3, c("id", "score")]
## id score
## a 1 0.5
## b 2 0.2
## c 3 0.1
以矩阵形式操作会自动简化结果,也就是说,如果只提取一列,那么结果将不再是数据框形式,而仅仅返回这一列的值。即便结果只有单列,我们也可以结合使用两种操作方式来保留数据框的形式:
df1[1:4,]["id"]
## id
## a 1
## b 2
## c 3
## d 4
这里,第1组方括号以矩阵形式提取数据框的前4行和所有的列。第2组方括号再以列表形式提取列名为id
的这一列,结果即表现为数据框的形式。
另一种方法是通过设定drop = FALSE
避免简化结果:
df1[1:4, "id", drop = FALSE]
## id
## a 1
## b 2
## c 3
## d 4
如果想要数据框的子集总是保留数据框的形式,你可以设置drop = FALSE
;否则,一些特殊情况(例如只选择提取一列)可能会导致意想不到的结果,你觉得将会得到一个数据框,但却返回一个向量。
以下代码按照score >= 0.5
筛选df1
的行,并选择id
和level
两列:
df1$score >= 0.5
## [1] TRUE FALSE FALSE TRUE TRUE
df1[df1$score >= 0.5, c("id", "level")]
## id level
## a 1 0
## d 4 -1
## e 5 -3
以下代码按照行名必须在a
、d
或者e
中的准则来筛选df1
的行,并选择id
和score
两列:
rownames(df1) %in% c("a", "d", "e")
## [1] TRUE FALSE FALSE TRUE TRUE
df1[rownames(df1) %in% c("a", "d", "e"), c("id", "score")]
## id score
## a 1 0.5
## d 4 0.5
## e 5 0.9
以上两个例子都以矩阵形式对数据框进行操作,根据逻辑向量选择行,字符向量选择列。
处理列表和矩阵的两种方法都可以用来为一个数据框子集赋值。
我们可以同时使用$
和<-对列表中的成分重新赋值。
df1$score <- c(0.6, 0.3, 0.2, 0.4, 0.8)
df1
## id level score
## a 1 0 0.6
## b 2 2 0.3
## c 3 1 0.2
## d 4 -1 0.4
## e 5 -3 0.8
此外,[
也是适用的,而且它允许在一个语句中进行多重修改,但是使用[[
每次只能修改一列。
df1["score"] <- c(0.8, 0.5, 0.2, 0.4, 0.8)
df1
## id level score
## a 1 0 0.8
## b 2 2 0.5
## c 3 1 0.2
## d 4 -1 0.4
## e 5 -3 0.8
df1[["score"]] <- c(0.4, 0.5, 0.2, 0.8, 0.4)
df1
## id level score
## a 1 0 0.4
## b 2 2 0.5
## c 3 1 0.2
## d 4 -1 0.8
## e 5 -3 0.4
df1[c("level", "score")] <- list(level = c(1, 2, 1, 0, 0), score = c(0.1,
0.2, 0.3, 0.4, 0.5))
df1
## id level score
## a 1 1 0.1
## b 2 2 0.2
## c 3 1 0.3
## d 4 0 0.4
## e 5 0 0.5
当我们以列表方式对数据框进行赋值时,会遇到与构建子集时同样的问题,即只能访问列。若需要更加灵活地进行赋值操作,可以以矩阵方式进行。
df1[1:3, "level"] <- c(-1, 0, 1)
df1
## id level score
## a 1 -1 0.1
## b 2 0 0.2
## c 3 1 0.3
## d 4 0 0.4
## e 5 0 0.5
df1[1:2, c("level", "score")] <- list(level = c(0, 0), score = c(0.9, 1.0))
df1
## id level score
## a 1 0 0.9
## b 2 0 1.0
## c 3 1 0.3
## d 4 0 0.4
## e 5 0 0.5
值得注意的是,在默认情况下,数据框会以更有效地利用内存的方式来存储数据。但有时,这种存储方式会导致意想不到的问题。
例如,当我们用一个字符向量作为创建数据框的列时,R 会默认将其转换成因子,相同值只存储一次,以免重复存储占用过多内存。因子本质上是一个带有水平(level)属性的整数向量,其中“水平”是指我们事前确定的可能取值的有限集合。
我们可以通过对已经创建的数据框persons
调用str( )
来说明:
str(persons)
## 'data.frame': 3 obs. of 4 variables:
## $ Name : Factor w/ 3 levels "Ashley","Jennifer",..: 3 1 2
## $ Gender: Factor w/ 2 levels "Female","Male": 2 1 1
## $ Age : num 24 25 23
## $ Major : Factor w/ 3 levels "Computer Science",..: 2 3 1
正如我们所看到的,姓名(Name
)、性别(Gender
)和专业(Major
)不是字符向量而是因子。因为性别(Gender
)只可能是女(Female
)或男(Male
),所以它被表示为一个因子是合理的。显然使用两个整数表示这两种水平要比即使取值重复也全部保存下来的字符向量更加有效。
然而,如果某些列并不限于几个可能的取值,使用因子可能会引起问题。例如,我们想在persons
中设置一个名字。
persons[1, "Name"] <- "John"
## Warning in `[<-.factor`(`*tmp*`, iseq, value = "John"): invalid factor
## level, NA generated
persons
## Name Gender Age Major
## 1 <NA> Male 24 Finance
## 2 Ashley Female 25 Statistics
## 3 Jennifer Female 23 Computer Science
此时,出现了一个警告。之所以出现警告,是因为在最初设定Name
这一列时,相应的水平集合中并没有John
这个词。因此我们无法给第一个人赋予一个不存在的名字。将任意一个Gender
设置成Unknown
时也会出现同样的情况,原因是一样的。当我们定义一个数据框时,用一个字符向量作为数据框的一列,该列将默认被转换为因子,其取值只能从由对应字符向量的唯一值构成的水平集合中选取。
有时候这种情况令人非常恼火。特别是在内存已经很便宜的今天,这种设定真没有多大帮助。避免这种情况最简单的方式是在使用data.frame()
创建一个数据框时,设置 stringsAsFactors = FALSE
:
persons <- data.frame(Name = c("Ken", "Ashley", "Jennifer"),
Gender = factor(c("Male", "Female", "Female")),
Age = c(24, 25, 23),
Major = c("Finance", "Statistics", "Computer Science"),
stringsAsFactors = FALSE)
str(persons)
## 'data.frame': 3 obs. of 4 variables:
## $ Name : chr "Ken" "Ashley" "Jennifer"
## $ Gender: Factor w/ 2 levels "Female","Male": 2 1 1
## $ Age : num 24 25 23
## $ Major : chr "Finance" "Statistics" "Computer Science"
如果我们真的想让一个因子对象发挥它的作用,可以直接在特定的列上调用factor( )
函数,就像Gender
那一列。
对一个数据框而言,有很多实用的函数,这里我们只介绍几个最常用的。
summary( )
函数作用在数据框上,将生成一个汇总表来显示每一列的情况:
summary(persons)
## Name Gender Age Major
## Length:3 Female:2 Min. :23.0 Length:3
## Class :character Male :1 1st Qu.:23.5 Class :character
## Mode :character Median :24.0 Mode :character
## Mean :24.0
## 3rd Qu.:24.5
## Max. :25.0
对于因子Gender
,汇总了取每一个值或每一个水平的行数。对于一个数值向量,返回重要的分位数。对于其他类型的列,则显示列的长度、类型和模式。另一个常见的需求是将多个数据框按行或按列进行合并。要实现这个目的,我们可以使用rbind( )
和cbind( )
,正如函数名一样,它们分别表示按行合并和按列合并。
如果想向数据框中添加一些行,例如,在这个例子中,要添加一个人的新记录,我们可以用rbind( )
:
rbind(persons, data.frame(Name = "John", Gender = "Male", Age = 25, Major
= "Statistics"))
## Name Gender Age Major
## 1 Ken Male 24 Finance
## 2 Ashley Female 25 Statistics
## 3 Jennifer Female 23 Computer Science
## 4 John Male 25 Statistics
如果想向数据框中添加一些列,例如,添加两个新列表示每个人是否已注册和其手头的项目数量,可以使用cbind( )
:
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
还有很多可以用于数据框的实用函数,我们将在第7章继续讨论这些函数。
现实中,数据通常存储在文件中。R 提供了许多函数以便从文件中读取一个表格或将一个数据框写入文件。如果一个文件储存了一个表格,通常它都会被很好地组织起来,即按照一定规则将行和列有序地排列。大多数情况下,我们不必逐个字节地读取文件,而是调用read.table( )
或read.csv( )
等函数。
CSV(逗号分隔值,Comma-Separated Vlues)是目前最受欢迎的软件通用数据格式。CSV通常是这样组织数据的:不同列之间的值用逗号分隔开,首行默认作为表头。例如,在CSV格式中是这样存储 persons 的:
Name,Gender,Age,Major
Ken,Male,24,Finance
Ashley,Female,25,Statistics
Jennifer,Female,23,Computer Science
将数据读入 R 环境中,我们只需要调用read.csv(file)
,这里的file
是文件所在的路径。为了保证数据文件能被 R 找到,最好直接将数据文件夹放入默认工作目录中,调用getwd( )
找到该目录。我们会在下一章更详细地讨论它。
read.csv("data/persons.csv")
## Name Gender Age Major
## 1 Ken Male 24 Finance
## 2 Ashley Female 25 Statistics
## 3 Jennifer Female 23 Computer Science
如果需要将数据框保存成一个CSV文件,可以调用write.csv(file)
并调整其他参数:
write.csv(persons, "data/persons.csv", row.names = FALSE, quote = FALSE)
参数row.names = FALSE
避免存储不必要的行名,参数quote = FALSE
避免对输出中的文本加引号,这两种做法在大多数情况下都是非必要的。
R 中还有很多内置函数和扩展包可以用来读写不同格式的数据,我们将在之后的章节中继续讨论这个内容。
函数是一个可以调用的对象。本质上讲,它是一个具有内在逻辑的机制,输入一组值(形参或实参),并依据其逻辑返回一个值。
在前面的章节中,我们遇到过一些R的内置函数。例如,在is.numeric( )
函数中输入任意一个 R 对象,会返回一个判断该对象是否为数值向量的逻辑值。类似的还有is.function( )
函数,它可以判断一个给定的R对象是否为函数。
事实上,在R环境中,我们所使用的一切都是对象,所做的一切都是函数,而且,也许会令你惊讶的是,所有的函数都是对象。甚至连<-
和+
也都是带有两个参数的函数。尽管它们被称为二元运算符,其本质上也都是函数。
当我们做一些简单的、交互式的数据分析时,通常不必自己编写函数,R 的内置函数和几千个扩展包提供的函数已经够用了。
但是,如果你需要在数据操作或分析中重复某些逻辑或过程,那么 R 中内置的或扩展包中的函数可能无法充分满足你的需求。因为它们不是为了满足特定的任务需求或特定格式的数据集编写的。此时,你就需要针对特定需求自己创建函数了。
在 R 中创建函数很容易。例如我们定义一个名为add
的函数,将x
和y
相加:
add <- function(x, y){
x + y
}
上述函数的语法(x, y)
指定了函数的参数。换句话说,此函数需要两个名为x
和y
的参数。{ x + y }
是函数体,包含了一系列由x
和y
及其他可用符号表示的表达式。除非在函数内部调用return( )
,一般情况下最后一个表达式的值即为函数的返回值。最后,此函数被命名为add
,之后我们就可以使用add
来调用这个函数了。
创建一个简单或者稍微复杂的函数,与将一个向量赋值给一个变量并无本质差异。在R中,函数就像另一个对象。若要查看对象add
的内容,在控制台中输入add
即可。
add
## function(x, y){
## x + y
## }
就像在数学中一样,函数一旦定义了,就可以被调用。调用语法为:“函数名(参数1, 参数2, ...)”。请看如下示例:
add(2, 3)
## [1] 5
调用函数的过程非常清晰。就像示例中调用函数,R 首先会在环境中查找是否存在一个名为 add
的函数,然后它会明白 add
是我们刚才创建的函数。之后 R 会创建一个局部环境,并在该局部环境中将2赋值给 x
,将3赋值给 y
。下一步,将给定的参数值代入函数体中的表达式并进行计算。最终,函数返回表达式的值,结果为5。
因为 R 中的函数不是强类型的,所以它可以非常灵活。换句话说,在调用函数之前,输入对象的类型是不固定的。即使函数的最初设计是针对标量运算,当将函数“+”作用到向量上时,它也会自动拓展以适用于向量运算。例如,我们可以运行以下代码,而不必对函数做任何其他修改:
add(c(2, 3), 4)
## [1] 6 7
上面这个例子没有真正展示出动态类型的灵活性,因为在 R 中标量也是一个向量(长度为1)。举一个更具有代表性的例子:
add(as.Date("2014-06-01"), 1)
## [1] "2014-06-02"
无需检查输入类型,函数便可以将两个参数代入表达式中进行运算。其中,as.Date( )
创建了一个Date
对象,用来表示日期。这里没有对“add”函数进行任何更改,它就可以完美地作用于对象Date
。只有在两个参数上“+”没有被很好地定义时,函数才会失效。
add(list(a = 1), list(a = 2))
## Error in x + y: 二进列运算符中有非数值参数
函数是用于解决一些特定问题的特定逻辑或者过程的集合的一种合理抽象,开发人员通常希望函数具有一般性,以适用于各种各样的场景。这样就可以轻松地使用它来解决相似的问题,而无需为每个问题编写过多的专用函数。
泛化是使一个函数具有更广泛的适用性。在弱类型的编程语言(例如 R )中泛化函数非常方便的,但是当它被不正确地执行时,也会出现错误。
为了使 add( )
函数更加通用,以便可以处理各种原始代数运算,我们可以定义另一个名为calc
的函数。这个新函数包含3个参数:x
、y
和 type
,其中 x
和 y
是两个向量,type
接收一个字符向量,它表示用户想要进行哪一种代数运算。
以下代码使用控制流(flow control)执行该函数。我们稍后会讲解控制流的具体使用。虽然这里初次遇到,但应该很容易理解。在这段代码中,type
的取值决定了使用何种表达式。
calc <- function(x, y, type){
if (type == "add"){
x + y
} else if (type == "minus"){
x - y
} else if (type == "multiply"){
x * y
} else if (type == "divide"){
x / y
}else {
stop("Unknown type of operation")
}
}
一旦函数被定义,我们便可以通过提供适当的参数来调用它:
calc(2, 3, "minus")
## [1] –1
函数自动适用于数值向量:
calc(c(2, 5), c(3, 6), "divide")
## [1] 0.6666667 0.8333333
因为之前“+”已经被良好定义,所以也可以泛化calc( )
函数以适用于非数值向量:
calc(as.Date("2014-06-01"), 3, "add")
## [1] "2014-06-04"
如果提供一些无效参数:
calc(1, 2, "what")
## Error in calc(1, 2, "what"): Unknown type of operation
在这种情况下,没有满足的条件,因此最后else
代码块中的表达式被执行。stop( )
函数被调用,输出错误信息并立即终止运算。
看起来函数运行良好,也考虑了包括无效参数的所有情况。然而事实并非如此:
calc(1, 2, c("add", "minus"))
## Warning in if (type == "add") {: 条件的长度大于一,因此只能用其第一元素
## [1] 3
这里,我们没有考虑传递多元素向量给type
的情况。问题在于:当两个多元素向量比较时,也会返回一个多元素逻辑向量,这会使得 if
的判断条件含糊不清。考虑一下,if(c(TRUE, FALSE))
意味着什么呢?
为了彻底地避免这种模棱两可的情况,我们需要细化函数使错误能够更加明晰,反映更多信息。进一步地,我们只需检查向量的长度是否为 1:
calc <- function(x, y, type){
if (length(type > 1L)) stop("Only a single type is accepted")
if (type == "add"){
x + y
} else if (type == "minus"){
x - y
} else if (type == "multiply"){
x * y
} else if (type == "divide"){
x / y
}else {
stop("Unknown type of operation")
}
}
重试上述会报错的代码,我们可以查看预先检查参数后,函数如何处理异常:
calc(1, 2, c("add", "minue"))
## Error in calc(1, 2, c("add", "minue")): Only a single type is accepted
一些函数可以非常灵活,因为它们能够接受各种各样的输入值,满足广泛的需求。但是,很多情况下,更多的灵活性意味着增加更多的参数。
如果使用一个非常灵活的函数,每次需要指定几十个参数,那么查看代码时肯定会觉得一片混乱。在这种情况下,给参数设定合理默认值,将会极大地简化调用函数的代码。
使用arg = value
给一个参数设定默认值,这将使其成为一个可选参数。下面这个例子创建了一个带有可选参数的函数:
increase <- function(x, y = 1){
x + y
}
调用新函数increase( )
时,只需提供x
的取值,y
会自动取值为1,除非另有明确指定。
increase(1)
## [1] 2
increase(c(1, 2, 3))
## [1] 2 3 4
R 中许多函数都包含多个参数,其中一些被赋予了默认值。有时,设定参数默认值是一件棘手的事情,因为它高度依赖于大多数用户的使用意图。
在本章中,我们学习了数值向量、逻辑向量和字符向量的基本性质。这些向量属于同质数据类,即只能存储相同类型的元素。与之相比,列表和数据框更加灵活,因为它们可以存储不同类型的元素。还学习了如何从这些数据结构中提取子集和元素。最后,了解了关于创建和调用函数的相关内容。
理解了游戏规则之后,还需要熟悉操作环境。在下一章中,我们将介绍一些有关管理工作环境的基本但重要的内容,向你展示一些管理工作目录、环境和扩展包库的一般做法。
① 第1个成分只包含1个元素:1;第2个成分包含2个元素:TRUE和FALSE;第3个成分包含3个元素:"a" "b" 和 "c"。使用[[ ]]提取成分,使用[ ]提取成分的元素,例如l0[[2]][2] 提取第2个成分的第2个元素FALSE。
② 使用[ ]提取成分时,返回列表的子集,还是一个列表;使用[[ ]]提取成分时,返回对应成分的元素。