layout: post
title: "R-第三遍教程"
date: 2019-11-29
tag: sas
R语言教程
前言
李东风的《R语言教程》的草稿。 还在更新中。
相关下载:
Rbook-data.zip:一些配套数据的打包文件Bookdown-template-v0-3.zip:R Markdown和bookdown的模板
书中的数学公式使用MathJax库显示, 下面是数学公式测试。 如果数学公式显示的中文不正常, 在浏览器中用鼠标右键单击中文公式, 在弹出的菜单中选择Math Settings--Math Renderer选HTML-CSS或SVG即可。
公式测试:
中文公式测试:
相对误差
使用本教程必须安装的软件包:
- tidyverse
- bookdown
- xtable
- microbenchmark
- reshape
本教程中用到的软件包列表(更新R软件后可以在一个基本R中运行如下命令):
pkgs <- c( "assertthat", "backports", "base64enc", "BH", "bindr", "bindrcpp", "bookdown", "broom", "callr", "cellranger", "cli", "clipr", "clorspace", "crayon", "curl", "DBI", "dbplyr", "dichromat", "digest", "dplyr", "evaluate", "forcats", "ggplot2", "glue", "gtable", "haven", "highr", "hms", "htmltools", "httr", "jsonlite", "knitr", "labeling", "lazyeval", "lubridate", "magrittr", "markdown", "microbenchmark", "mime", "mnormt", "modelr", "munsell", "openssl", "pillar", "pkgconfig", "plogr", "plyr", "psych", "purr", "R6", "RColorBrewer", "Rcpp", "readr", "readxl", "rematch", "reprex", "reshape2", "rlang", "rmarkdown", "rprojroot", "rstudioapi", "rvest", "scales", "selectr", "stringi", "stringr", "tibble", "tidyr", "tidyselect", "tidyverse", "utf8", "viridisLite", "whisker", "xml2", "xtable", "yaml")install.packages(unique(pkgs))
1 R语言介绍
1.1 R的历史和特点
1.1.1 R的历史
R语言来自S语言,是S语言的一个变种。S语言由Rick Becker, John Chambers等人在贝尔实验室开发, 著名的C语言、Unix系统也是贝尔实验室开发的。
S语言第一个版本开发于1976-1980,基于Fortran; 于1980年移植到Unix, 并对外发布源代码。 1984年出版的"棕皮书" (Becker and Chambers 1984) 总结了1984年为止的版本, 并开始发布授权的源代码。 这个版本叫做旧S。与我们现在用的S语言有较大差别。
1989--1988对S进行了较大更新, 变成了我们现在使用的S语言,称为第二版。 1988年出版的"蓝皮书" (Becker, Chambers, and Wilks 1988) 做了总结。
1992年出版的"白皮书" (Chambers and Hastie 1992) 描述了在S语言中实现的统计建模功能, 增强了面向对象的特性。软件称为第三版,这是我们现在用的多数版本。
1998年出版的"绿皮书" (Chambers 2008) 描述了第四版S语言,主要是编程功能的深层次改进。 现行的S系统并没有都采用第四版,S-PLUS的第5版才采用了S语言第四版。
S语言商业版本为S-PLUS, 1988年发布,现在为Tibco Software拥有。 命运多舛,多次易主。
R是一个自由软件,GPL授权, 最初由新西兰Auckland 大学的Ross Ihaka 和 Robert Gentleman于1997年发布, R实现了与S语言基本相同的功能和统计功能。 现在由R核心团队开发,但全世界的用户都可以贡献软件包。 R的网站: http://www.r-project.org/
1.1.2 R的特点
1.1.2.1 R语言一般特点
- 自由软件,免费、开放源代码,支持各个主要计算机系统;
- 完整的程序设计语言,基于函数和对象,可以自定义函数,调入C、C++、Fortran编译的代码;
- 具有完善的数据类型,如向量、矩阵、因子、数据集、一般对象等,支持缺失值,代码像伪代码一样简洁、可读;
- 强调交互式数据分析,支持复杂算法描述,图形功能强;
- 实现了经典的、现代的统计方法,如参数和非参数假设检验、线性回归、广义线性回归、非线性回归、可加模型、树回归、混合模型、方差分析、判别、聚类、时间序列分析等。
- 统计科研工作者广泛使用R进行计算和发表算法。R有上万软件包(截止2019年7月有一万四千多个)。
1.1.2.2 R语言和R软件的技术特点
- 函数编程(functional programming)。R语言虽然不是严格的functional programming语言,但可以遵照其原则编程,得到可验证的可靠程序。
- 支持对象类和类方法。基于对象的程序设计。
- 是动态类型语言,解释执行,运行速度较慢。
- 数据框是基本的观测数据类型,类似于数据库的表。
- 开源软件(Open source software)。可深入探查,开发者和用户交互。
- 可以用作C和C++、FORTRAN语言编写的算法库的接口。
- 主要数值算法采用已广泛测试和采纳的算法实现,如排序、随机数生成、线性代数(LAPACK软件包)。
1.1.2.3 推荐参考书
- R.L. Kabacoff(2012)《R语言实战》,人民邮电出版社。
- Hadley Wickham and Garrett Grolemund(2017) "R for Data Science",http://r4ds.had.co.nz/, O'Reilly
- Hadley Wickham(2014) "Advanced R", http://adv-r.had.co.nz/, Chapman & Hall/CRC The R Series
- R网站上的初学者手册"An Introduction to R"和其它技术手册。
- John M. Chambers(2008), "Software for Data Analysis-Programming with R", Springer.
- Venables, W. N. & Ripley, B. D.(2002) "Modern Applied Statistics with S", Springer
- 薛毅、陈立萍(2007)《统计建模与R软件》,清华大学出版社。
- 汤银才(2008),《R语言与统计分析》,高等教育出版社。
- 李东风(2006)《统计软件教程》,人民邮电出版社。
1.2 R的下载与安装
1.2.1 R的下载
以MS Windows操作系统为例。R的主网站在https://www.r-project.org/。 从CRAN的镜像网站下载软件,其中一个镜像如http://mirror.bjtu.edu.cn/cran/。 选"Download R for Windows-base-Download R 3.4.1 for windows" (3.4.1是版本号,应下载网站上给出的最新版本)链接进行下载。 在"Download R for Windows"链接的页面, 除了base为R的安装程序, 还有contrib为R附加的扩展软件包下载链接(一般不需要从这里下载), 以及Rtools链接, 是在R中调用C、C++和Fortran程序代码时需要用的编译工具。
RStudio(https://www.rstudio.com/)是功能更强的一个R图形界面, 在安装好R的官方版本后安装RStudio可以更方便地使用R。
1.2.2 R软件安装
下载官方的R软件后按提示安装。 安装后获得一个桌面快捷方式,如"R i386 3.4.1"(这是32位版本)。 如果是64位操作系统,可以同时安装32位版本和64位版本, 对初学者这两种版本区别不大, 尽量选用64位版本,这是将来的趋势。
安装官方的R软件后, 可以安装RStudio。 平时使用可以使用RStudio, 其界面更方便, 对R Markdown格式(.Rmd)文件支持更好。
如果使用RStudio, 每个分析项目需要单独建立一个"项目"(project), 每个项目也有一个工作文件夹。
1.2.3 辅助软件
R可以把一段程序写在一个以.r或.R为扩展名的文本文件中, 如"date.r", 称为一个_源程序_文件, 然后在R命令行用
source("date.r")
运行源程序。 这样的文件可以用记事本生成和编辑。
在MS Windows操作系统中建议使用notepad++软件, 这是MS Windows下记事本程序的增强型软件。 安装后,在MS Windows资源管理器中右键弹出菜单会有"edit with notepadpp"选项。 notepad++可以方便地在不同的中文编码之间转换。
RStudio则是一个集成环境, 可以在RStudio内进行源程序文件编辑和运行。
1.2.4 R扩展软件包的安装与管理
R扩展软件包提供了特殊功能。 以安装sos包为例。sos包用来搜索某些函数的帮助文档。 在R图形界面选菜单"程序包-安装程序包", 在弹出的"CRAN mirror"选择窗口中选择一个中国的镜像如"China (Beijing 2)", 然后在弹出的"Packages"选择窗口中选择要安装的扩展软件包名称, 即可完成下载和安装。
还可以用如下程序制定镜像网站(例子中是位于清华大学的镜像网站)并安装指定的扩展包:
options(repos=c(CRAN="http://mirror.tuna.tsinghua.edu.cn/CRAN/"))install.packages("sos")
还可以选择扩展包的安装路径, 如果权限允许, 可以选择安装在R软件的主目录内或者用户自己的私有目录位置。 由于用户的对子目录的读写权限问题, 有时不允许一般用户安装扩展包到R的主目录中。 用.libPaths()查看允许的扩展包安装位置, 在install.packages()中用lib=指定安装位置:
print(.libPaths())## [1] "D:/R/R-3.3.1/library"install.packages("sos", lib=.libPaths()[1])
在RStudio中用"Tools"菜单的"Install Packages"安装软件包。
在每一次R软件更新后, 需要重新安装原来的软件包, 这个过程很麻烦。 如果仅仅是小的版本更新, 比如从3.5.1变成3.5.2, 或者从3.4.2变成3.5.0, 可以在安装新版本后, 将老版本的library子目录中所有内容复制到新版本的library子目录中, 同时尽量不要覆盖已有的内容, 然后在基本R中(不要用RStudio)运行如下命令:
options(repos=c(CRAN="http://mirror.tuna.tsinghua.edu.cn/CRAN/"))update.packages(checkBuilt=TRUE, ask=FALSE)
这个命令也可以用来成批地更新已安装的R扩展软件包。
如果版本改变比较大, 可以用如下方法批量地重新安装原有的软件包。 首先,在更新R软件前,在原来的R中运行:
packages <- .packages(TRUE)dump("packages", file="packages-20180704.R")
这样可以获得要安装的软件包的列表。 在更新R软件后, 运行如下程序:
options(repos=c(CRAN="http://mirror.tuna.tsinghua.edu.cn/CRAN/"))source("packages-20180704.R")install.packages(packages)
安装时如果提问是否安装需要编译的源代码包, 最好选择否, 因为安装源代码包速度很慢还有可能失败。
1.3 基本R软件的用法
1.3.1 基本运行
在MS Windows操作系统中的R软件有一个R GUI软件, 即图形窗口模式的R软件,如图1.1。

R GUI中有一个命令行窗口(R Console), 以大于号为提示符, 在提示符后面键入命令, 命令的文字型结果马上显示在命令下方, 命令的图形结果单独显示在一个图形窗口中。
在命令行可以通过左右光标键移动光标到适当位置进行修改。 可以用上下光标在已经运行过的历史命令中切换, 调回已经运行过的命令, 修改后重新执行。
如果某个文件如myprog.R在当前工作目录中, 保存的都是R程序, 称这样的文件为源程序文件。 可以在命令行用如下命令运行其中的程序:
source("myprog.R")
但是, 在MS Windows操作系统中, 默认的中文编码是GB18030编码。 R源程序文件的中文编码可能是GB18030也可能是UTF-8。 UTF-8是在世界范围更通用的编码。 如果发现用如下命令运行时出现中文乱码, 可能是因为源程序用了UTF-8编码, 这时source()命令要加上编码选项如下:
source("myprog.R", encoding="UTF-8")
1.3.2 项目目录
用R进行数据分析, 不同的分析问题需要放在不同的文件夹中。 以MS Windows操作系统为例, 设某个分析问题的数据与程序放在了c:\work 文件夹中。 把R的快捷方式从桌面复制入此文件夹, 在Windows资源管理器中, 右键单击此快捷方式,在弹出菜单中选"属性", 把"快捷方式"页面的"起始位置"的内容清除为空白,点击确定按钮。 启动在work文件夹中的R快捷方式,出现命令行界面。 这时,C:\work称为当前工作目录。
在命令行运行如下命令可以显示当前工作目录位置:
getwd()## "C:/work"
显示结果中的目录、子目录、文件之间的分隔符用了/符号, 在Windows操作系统中一般应该使用\\符号, 但是, 在R的字符串中一个\需要写成两个, 所以等价的写法是"C:\\work"。
不同的分析项目需要存放在不同的文件夹中, 每个文件夹都放置一个"起始位置"为空的R快捷方式图标, 分析哪一个项目, 就从对应的快捷图标启动,而不是从桌面上的R图标启动。 这样做的好处时, 用到源文件和数据文件时, 只要文件在该项目的文件夹中, 就不需要写完全路径而只需要用文件名即可。
1.4 RStudio软件
1.4.1 介绍
RStudio软件是R软件的应用界面与增强系统, 可以在其中编辑、运行R的程序文件, 可以跟踪运行, 还可以构造文字、R结果图表融合在一起的研究报告、论文、图书、网站等。 一个运行中的RStudio界面见图1.2。

界面一般分为四个窗格, 其中编辑窗口与控制台(Console)是最重要的两个窗格。 编辑窗格用来查看和编辑程序、文本型的数据文件、程序与文字融合在一起的Rmd文件等。 控制台与基本R软件的命令行窗口基本相同, 功能有所增强。
在编辑窗口中可以用操作系统中常用的编辑方法对源文件进行编辑, 如复制、粘贴、查找、替换, 还支持基于正则表达式的查找替换(关于正则表达式见35)。
其它的一些重要窗格包括:
- Files: 列出当前项目的目录(文件夹)内容。 其中以
.R或者.r为扩展名的是R源程序文件, 单击某一源程序文件就可以在编辑窗格中打开该文件。 - Plots: 如果程序中有绘图结果, 将会显示在这个窗格。 因为绘图需要足够的空间, 所以当屏幕分辨率过低或者Plots窗格太小的时候, 可以点击"Zoom"图标将图形显示在一个单独的窗口中, 或者将图形窗口作为唯一窗格显示。 如何放大窗格见下面的使用技巧。
- Help: R软件的文档与RStudio的文档都在这里。
- Environment: 已经有定义的变量、函数都显示在这里。
- History: 以前运行过的命令都显示在这里。 不限于本次RStdudio运行期间, 也包括以前使用RStudio时运行过的命令。
- Packages: 显示已安装的R扩展包及其文档。
- Viewer, Connection, Build, Git等窗格。
1.4.2 项目
用R和RStudio进行研究和数据分析, 每个研究问题应该单独建立一个文件夹(目录)。 该问题的所有数据、程序都放在对应的文件夹中。 在RStudio中, 用"File -- New Project -- Existing Directory"选中该问题的目录, 建立一个新的"项目"(project)。
再次进入RStudio后, 用菜单"File -- Recent Projects"找到已有的项目打开, 然后就可以针对该项目进行分析了。 这样分项目进行研究的好处是, 不同项目的可以使用同名的文件而不会有冲突, 程序中用到某个文件时, 只需要写文件名而不需要写文件所在的目录。
一个项目还可以有项目本身的一些特殊设置, 用"Tools -- Project Options"菜单打开设置。
1.4.3 帮助
在RStudio中有一个单独的Help窗格, 如果需要,可以用菜单"View--Panes--Zoom help"将其放大到占据整个窗口空间。 但是,这一功能目前不支持放大显示字体的功能, 不如在浏览器中方便。
RStudio的帮助窗格中包含R软件的官方文档, 以及RStudio软件的的文档。 "Search engine and keywords"项下面有分类的帮助。 有软件包列表。
在基本R软件而不是RStudio的命令行中运行命令help.start()或者用RGUI的帮助菜单中"html帮助"可以打开系统默认的互联网浏览器, 在其中查看帮助文档。
在命令行,用问号后面跟随函数名查询某函数的帮助。 用example(函数名)的格式可以运行此函数的样例,如:
example(mean)## ## mean> x <- c(0:10, 50)## ## mean> xm <- mean(x)## ## mean> c(xm, mean(x, trim = 0.10))## [1] 8.75 5.50
有时仅知道一些方法的名字而不知道具体的扩展包和函数名称, 可以安装sos扩展包(package), 用findFn("函数名")查询某个函数, 结果显示在互联网浏览器软件中。
1.4.4 使用技巧
RStudio使用方法概要PDF下载:
1.4.4.1 使用历史
在控制台(命令行窗格)中, 除了可以用左右光标键移动光标位置, 用上下光标键调回以前运行过的命令, 还有一个重要的增强(以MS Windows操作系统为例): 键入要运行的命令的前几个字母,如book, 按"Ctrl+向上光标键", 就可以显示历史命令中以book开头的所有命令, 单击哪一个, 哪一个就自动复制到命令行。 这一技巧十分重要, 我们需要反复允许同一命令时, 这一方法让我们很容易从许多命令历史中找到所需的命令。
1.4.4.2 放大显示某一窗格
当屏幕分辨率较低时, 将整个RStudio界面分为四个窗格会使得每个窗格都没有足够的显示精度。 为此, 可以将某个窗格放大到整个窗口区域, 需要使用其它窗格时再恢复到四个窗格的状态或者直接放大其它窗格到整个窗口区域。
使用菜单"View -- Panes -- Zoom Source"可以将编辑窗格放到最大, 在MS Windows下也可以使用快捷键"Ctrl+Alt+1"。 其它操作系统也有类似的快捷键可用。 使用菜单"View -- Panes -- Show All Panes"可以显示所有四个窗格。
放大其它窗格也可以用"Ctrl + Alt + 数字",数字与窗格的对应关系为:
- 1: 编辑窗格;
- 2: 控制台(Console);
- 3: 帮助;
- 4: 历史;
- 5: 文件;
- 6: 图形;
- 7: 扩展包;
- 8: 已定义变量和函数;
- 9: 研究报告或网站结果显示。
1.4.4.3 运行程序
可以在命令行直接输入命令运行, 文字结果会显示在命令行窗口, 图形结果显示在"Plots"窗格中。 在命令行窗口(Console)中可以用左右光标键移动光标, 用上下光标键查找历史命令, 输入命令的前几个字母后用"Ctrl+向上光标键"可以匹配地查找历史命令。
一般情况下, 还是应该将R源程序保存在一个源程序文件中运行。 RStudio中"File -- New File -- R Script"可以打开一个新的无名的R源程序文件窗口供输入R源程序用。 输入一些程序后,保存文件, 然后点击"Source"快捷图标就可以运行整个文件中的所有源程序, 并会自动加上关于编码的选项。
编写R程序的正常做法是一边写一遍试验运行, 运行一般不是整体的运行而是写完一部分就运行一部分, 运行没有错误才继续编写下一部分。 在R源程序窗口中, 当光标在某一程序上的时候, 点击窗口的"Run"快捷图标或者用快捷键"Ctrl+Enter键"可以运行该行; 选中若干程序行后, 点击窗口的"Run"快捷图标或者用快捷键"Ctrl+Enter键"可以运行这些行。
1.4.4.4 中文编码问题
对于中文内容的R源程序、R Markdown源文件(.Rmd文件)、文本型数据文件(.txt,.csv), 其中的中文内容可能有不同的编码选择, 在中国国内主要使用GB18030(基本兼容于GB, GBK)和UTF-8, UTF-8是国际上更普遍使用的统一文字编码, 涉及到计算机编程时应尽可能使用此编码系统。
在RStudio中新生成的R源程序、Rmd源文件一般自动用UTF-8编码。 点击RStudio的文件窗格中显示的源文件, 可以打开该源文件, 但是因为已有源文件的编码不一定与RStudio的默认编码一致, 可以会显示成乱码。 为此, RStdio提供了"File -- Reopen with Encoding"命令, 我们主要试验其中GB18030和UTF-8两种选择一般就可以解决问题。 如果选择GB18030显示就没有乱码了, 最好再用菜单"File -- Save with Encoding"并选择UTF-8将其保存为UTF-8编码。
其它的文本格式的文件也可以类似地处理, 后面将会陆续提及。
1.4.5 Rmd文件
在科学研究中, R软件可以用来分析数据, 生成数据分析报表和图形。 R Markdown(简称Rmd)是一种特殊的文件格式, 在这种文件中, 即有R程序, 又有说明文字, 通过R和RStudio软件, 可以运行其中的程序, 并将说明文字、程序、程序的文字结果、图形结果统一地转换为一个研究报告, 支持Word、PDF、网页、网站、幻灯片等许多种输出格式。 在打开的Rmd源文件中, 也可以选择其中的某一段R程序单独运行。 所以, Rmd文件也可以作为一种特殊的R源程序文件。
用RStudio的"File -- New File -- R Markdown"菜单就可以生成一个新的Rmd文件并显示在编辑窗格中, 其中已经有了一些样例内容, 可以修改这些样例内容为自己的文字和程序。
Rmd文件中用````{r}`开头,用`````结尾的段落是R程序段, 在显示的程序段的右侧有一个向右箭头形状的小图标(类似于媒体播放图标), 点击该图标就可以运行该程序段。
打开Rmd文件后, 用编辑窗口的Knit命令可以选择将文件整个地转换为HTML(网页)或者MS Word格式, 如果操作系统中安装有LaTeX软件, 还可以以LaTeX为中间格式转换为PDF文件。
1.5 练习
- 下载R安装程序,安装R,建立work文件夹并在其中建立R的快捷方式。 Windows用户还需要下载RTools软件并安装。
- 下载RStudio软件并安装。
- 下载安装notepad++软件(仅MS Windows用户)。
- 在RStudio中下载安装sos扩展软件包。
References
Becker, R. A., and J. M. Chambers. 1984. S: An Interactive Environment for Data Analysis and Graphics. Wadsworth Advanced Books Program, Belmont CA.
Becker, Richard A., John M. Chambers, and Allan Reeve Wilks. 1988. The New S Language. Chapman; Hall, New York.
Chambers, J. M., and T. Hastie. 1992. Statistical Models in S. Chapman; Hall, New York.
Chambers, John M. 2008. Software for Data Analysis: Programming with R. Springer.
2 R语言入门运行样例
2.1 命令行界面
启动R软件后进入命令行界面,每输入一行命令,就在后面显示计算结果。 可以用向上和向下箭头访问历史命令; 可以从已经运行过的命令中用鼠标拖选加亮后, 用Ctrl+C复制后用Ctrl+V粘贴, 或用Ctrl+X一步完成复制粘贴, 粘贴的目标都是当前命令行。
如果使用RStudio软件, 有一个"Console窗格"相当于命令行界面。 在RStudio中, 可以用New File--Script file功能建立一个源程序文件(脚本文件), 在脚本文件中写程序, 然后用Run图标或者Ctrl+Enter键运行当前行或者选定的部分。
2.2 四则运算
四则运算如:
5 + (2.3 - 1.125)*3.2/1.1 + 1.23E3## [1] 1238.418
结果为1238.418, 前面显示的结果在行首加了井号, 这在R语言中表示注释。 本教程的输出前面一般都加了井号以区分于程序语句。 输出前面的方括号和序号1是在输出有多个值时提供的提示性序号, 只有单个值时为了统一起见也显示出来了。 这里1.23E3是科学记数法, 表示。 用星号*表示乘法,用正斜杠/表示除法。
用^表示乘方运算,如
2^10## [1] 1024
重要提示:关闭中文输入法,否则输入一些中文标点将导致程序错误。
2.2.1 计算例子
从52张扑克牌中任取3张, 有多少种不同的组合可能? 解答:有
种, 在R中计算如:
52*51*50/(3*2)## [1] 22100
2.2.2 练习
- 某人存入10000元1年期定期存款,年利率3%, 约定到期自动转存(包括利息)。问:
- 10年后本息共多少元?
- 需要存多少年这10000元才能增值到20000元?
- 成语说:"智者千虑,必有一失;愚者千虑,必有一得"。 设智者作判断的准确率为, 愚者作判断的准确率为, 计算智者做1000次独立的判断至少犯一次错误的概率, 与愚者做1000次独立判断至少对一次的概率。
2.3 数学函数
2.3.1 数学函数------平方根、指数、对数
例:
sqrt(6.25)## [1] 2.5exp(1)## [1] 2.718282log10(10000)## [1] 4
sqrt(6.25)表示,结果为2.5。 exp(1)表示,结果为。 log10(10000)表示,结果为。 log为自然对数。
2.3.2 数学函数------取整
例:
round(1.1234, 2)## [1] 1.12round(-1.9876, 2)## [1] -1.99floor(1.1234)## [1] 1floor(-1.1234)## [1] -2ceiling(1.1234)## [1] 2ceiling(-1.1234)## [1] -1
round(1.1234, 2)表示把1.1234四舍五入到两位小数。 floor(1.1234)表示把1.1234向下取整,结果为1。 ceiling(1.1234)表示把1.1234向上取整,结果为2。
2.3.3 数学函数------三角函数
例:
pi## [1] 3.141593sin(pi/6)## [1] 0.5cos(pi/6)## [1] 0.8660254sqrt(3)/2## [1] 0.8660254tan(pi/6)## [1] 0.5773503
pi表示圆周率。sin正弦, cos余弦, tan正切, 自变量以弧度为单位。 pi/6是。
2.3.4 数学函数------反三角函数
例:
pi/6## [1] 0.5235988asin(0.5)## [1] 0.5235988acos(sqrt(3)/2)## [1] 0.5235988atan(sqrt(3)/3)## [1] 0.5235988
asin反正弦, acos反余弦, atan反正切, 结果以弧度为单位。
2.3.5 分布函数和分位数函数
例:
dnorm(1.98)## [1] 0.05618314pnorm(1.98)## [1] 0.9761482qnorm(0.975)## [1] 1.959964
dnorm(x)表示标准正态分布密度 . pnorm(x)表示标准正态分布函数。 qnorm(y)表示标准正态分布分位数函数 。 还有其它许多分布的密度函数、分布函数和分位数函数。 例如,
qt(1 - 0.05/2, 10)## [1] 2.228139
求自由度为10的t检验的双侧临界值。 其中qt(y,df)表示自由度为df的t分布的分位数函数。
2.4 输出
2.4.1 简单输出
命令行的计算结果直接显示在命令的后面。 在用source()运行程序文件时, 需要用print()函数显示一个表达式的结果,如:
print(sin(pi/2))## [1] 1
用cat()函数显示多项内容, 包括数值和文本, 文本包在两个单撇号或两个双撇号中,如:
cat("sin(pi/2)=", sin(pi/2), "\n")## sin(pi/2)= 1
cat()函数最后一项一般是"\n", 表示换行。 忽略此项将不换行。
再次提示:要避免打开中文输入法导致误使用中文标点。
2.4.2 用sink()函数作运行记录
R使用经常是在命令行逐行输入命令(程序), 结果紧接着显示在命令后面。 如何保存这些命令和显示结果? 在R命令行中运行过的命令会被保存在运行的工作文件夹中的一个名为.Rhistory的文件中。 用sink()函数打开一个文本文件开始记录文本型输出结果。 结束记录时用空的sink()即可关闭文件不再记录。 如
sink("tmpres01.txt", split=TRUE)print(sin(pi/6))print(cos(pi/6))cat("t(10)的双侧0.05分位数(临界值)=", qt(1 - 0.05/2, 10), "\n")sink()
sink()用作输出记录主要是在测试运行中使用, 正常的输出应该使用cat()函数、write.table()、write.csv()等函数。
2.4.3 练习
-
用
cat()函数显示log10(2)=*** log10(5)=***其中
***应该代以实际函数值。 -
用
sink()函数开始把运行过程记录到文件"log001.txt"中,在命令行试验几个命令,然后关闭运行记录,查看生成的"log001.txt"的内容。
2.5 向量计算与变量赋值
R语言以向量为最小单位。用<-赋值。如
x1 <- 1:10x1## [1] 1 2 3 4 5 6 7 8 9 10
一般的向量可以用c()生成, 如
marks <- c(3, 5, 10, 5, 6)
在程序语言中,变量用来保存输入的值或计算的结果。 变量可以存放各种不同类型的值, 如单个数值、多个数值(称为向量)、单个字符串、多个字符串(称为字符型向量),等等。 单个数值称为标量。
技术秘诀:用程序设计语言的术语描述, R语言是动态类型的, 其变量的类型不需要预先声明, 运行过程中允许变量类型改变, 实际上变量赋值是一种"绑定"(binding), 将一个变量的名称(变量名)与实际的一个存储位置联系在一起。 在命令行定义的变量称为全局变量。
用print()函数显示向量或在命令行中显示向量时, 每行显示的行首会有方括号和数字序号, 代表该行显示的第一个向量元素的下标。如
12345678901:12345678920## [1] 12345678901 12345678902 12345678903 12345678904 12345678905## [6] 12345678906 12345678907 12345678908 12345678909 12345678910## [11] 12345678911 12345678912 12345678913 12345678914 12345678915## [16] 12345678916 12345678917 12345678918 12345678919 12345678920
向量可以和一个标量作四则运算, 结果是每个元素都和这个标量作四则运算,如:
x1 + 200## [1] 201 202 203 204 205 206 207 208 209 2102*x1## [1] 2 4 6 8 10 12 14 16 18 202520/x1## [1] 2520 1260 840 630 504 420 360 315 280 252
两个等长的向量可以进行四则运算, 相当于对应元素进行四则运算,如
x2 <- x1 * 3x2## [1] 3 6 9 12 15 18 21 24 27 30x2 - x1## [1] 2 4 6 8 10 12 14 16 18 20
R的许多函数都可以用向量作为自变量, 结果是自变量的每个元素各自的函数值。 如
sqrt(x1)## [1] 1.000000 1.414214 1.732051 2.000000 2.236068 2.449490 2.645751## [8] 2.828427 3.000000 3.162278
结果是1到10的整数各自的平方根。
2.6 工作空间介绍
在命令行中定义的变量, 在退出R时,会提问是否保存工作空间, 初学时可选择保存, 真正用R进行数据分析时往往不保存工作空间。 再次启动R后, 能够看到以前定义的各个变量的值。
在使用R的官方版本时, 如果在Windows中使用, 一般把不同的数据分析项目放在不同的文件夹中。 将R的程序快捷图标复制到每一个项目的文件夹中, 并用右键菜单讲快捷图标的"属性"中"起始位置"改为空白。 要分析哪一个项目的数据, 就从那个项目文件夹中的R快捷图标启动, 这样可以保证不同的项目有不同的工作空间。
如果使用RStudio软件, 也需要把不同项目放在不同文件夹, 并且每个项目在RStudio中单独建立一个"项目"(project)。 要分析那个项目的数据, 就打开那个项目。 不同项目使用不同的工作空间。
RStudio中的"Environment"窗格会显示当前已定义的R变量与函数。
2.6.1 练习
- 某人存入10000元1年期定期存款,年利率3%, 约定到期自动转存(包括利息)。列出1、2、......、10年后的本息金额。
- 显示2的1,2,......, 20次方。
- 定义x1为1到10的向量,定义x2为x1的3倍,然后退出R,再次启动R,查看x1和x2的值。
2.7 绘图示例
2.7.1 函数曲线示例
如下程序用curve()函数制作函数的曲线图, curve()函数第二、第三自变量是绘图区间:
curve(x^2, -2, 2)

类似地,函数曲线图用如下程序可制作, 用abline()函数添加参考线:
curve(sin(x), 0, 2*pi)abline(h=0)

2.7.2 条形图示例
假设有10个男生,7个女生,如下程序绘制男生、女生人数的条形图:
barplot(c("男生"=10, "女生"=7), main="男女生人数")

利用适当选项可以人为定制颜色、控制条形宽窄。 实际问题中,个数(频数)一般是从数据中统计得到的。
2.7.3 散点图示例
下面的例子用plot()函数做了散点图, plot()函数第一个自变量是各个点的横坐标值, 第二个自变量是对应的纵坐标值。
plot(1:10, sqrt(1:10))

2.7.4 R软件自带的图形示例
R软件中自带了一些演示图形。通过如下程序可以调用:
demo("graphics")demo("image")
2.7.5 练习
- 画在区间的函数图形。
- 画在区间的函数图形。
2.8 汇总统计示例
2.8.1 表格数据
统计用的输入数据典型样式是Excel表那样的表格数据。 表格数据特点:每一列应该是相同的类型(或者都是数值, 或者都是文字,或者都是日期), 每一列应该有一个名字。
这样的表格数据,一般可以保存为.csv格式: 数据项之间用逗号分开,文件本身是文本型的, 可以用普通记事本程序查看和编辑。 Excel表可以用"另存为"命令保存为.csv格式。 常用的数据库管理系统一般也可以把表保存为.csv格式。
2.8.2 读入表格数据
例如,taxsamp.csv是这样一个csv格式表格数据文件, 可以用Excel打开,也可以用记事本程序或notepad++打开。 内容见本页面后面的附录。
用如下程序可以把.csv文件读入到R中:
tax.tab <- read.csv("taxsamp.csv", header=TRUE, as.is=TRUE)print(head(tax.tab))
程序中的选项header=TRUE指明第一行作为变量名行, 选项as.is=TRUE说明字符型列要原样读入而不是转换为因子(factor)。 读入的变量tax.tab称为一个数据框 (data.frame)。 head()函数返回数据框或向量的前几项。 比较大的表最好不要显示整个表, 会使得前面的运行过程难以查看。
技巧:read.csv()的一个改进版本是readr扩展包的read_csv()函数, 此函数读入较大表格速度要快得多, 而且读入的转换设置更倾向于不做不必要的转换。 但是, 这两种输入方法的默认中文编码可能不一样。
2.8.3 练习
- 用Excel软件查看"taxsamp.csv"的内容(双击即可)。
- 用记事本程序或notepad++软件查看"taxsamp.csv"的内容。
- 读入"taxsamp.csv"到R数据框tax.tab中,查看tax.tab内容。
2.8.4 分类变量频数统计
在tax.tab中, "征收方式"是一个分类变量。 用table()函数计算每个不同值的个数,称为频数(frequency):
table(tax.tab[["征收方式"]])## ## 查帐征收 定期定额征收 定期定率征收 ## 31 16 2
类似地可以统计 "申报渠道"的取值频数:
table(tax.tab[["申报渠道"]])## ## 大厅申报 网上申报 ## 18 31
也可以用table()函数统计"征收方式"和"申报渠道"交叉分类频数,如:
table(tax.tab[["征收方式"]], tax.tab[["申报渠道"]])## ## 大厅申报 网上申报## 查帐征收 9 22## 定期定额征收 9 7## 定期定率征收 0 2
上述结果制表如下:
knitr::kable(table(tax.tab[["征收方式"]], tax.tab[["申报渠道"]]) )
| 大厅申报 | 网上申报 |
|---|---|
| 查帐征收 | 9 |
| 定期定额征收 | 9 |
| 定期定率征收 | 0 |
2.8.5 数值型变量的统计
数值型变量可以计算各种不同的统计量, 如平均值、标准差和各个分位数。 summary()可以给出最小值、最大值、中位数、四分之一分位数、四分之三分位数和平均值。如
summary(tax.tab[["营业额"]])## Min. 1st Qu. Median Mean 3rd Qu. Max. ## 0 650 2130 247327 9421 6048000
中位数是从小到大排序后排在中间的值。 四分之一和四分之三分位数类似。
统计函数以一个数值型向量为自变量, 包括sum(求和), mean(平均值), var(样本方差), sd(样本标准差), min(最小值), max(最大值), range(最小值和最大值)等。如
mean(tax.tab[["营业额"]])## [1] 247327.4sd(tax.tab[["营业额"]])## [1] 1036453
如果数据中有缺失值, 可以删去缺失值后计算统计量, 这时在mean, sd等函数中加入na.rm=TRUE选项。
2.8.6 练习
用如下程序定义一个变量x, 然后求x的平均值和最小值、最大值。
x <- c(3, 5, 10, 5, 6)
2.9 运行源程序文件
用source()函数可以运行保存在一个文本文件中的源程序。 比如,如下内容保存在文件ssq.r中:
sum.of.squares <- function(x){ sum(x^2)}
用如下source()命令运行:
source("ssq.r")
运行后就可以调用自定义函数sum.of.squares()了。
2.9.1 源文件编码
源程序文件存在编码问题。 对于源程序编码与系统默认编码不同的情况, 在source()函数中可以添加encoding=选项。 例如, 保存为UTF-8编码的源程序在简体中文MS Windows系统的R中运行, 可以在source()函数中可以添加encoding="UTF-8"选项。 保存为GBK编码的源程序文件在MAC系统的R中运行, 可以在source()函数中可以添加encoding="GBK"选项。
在RStudio中, 可以打开一个源程序文件查看与编辑。 用快捷键"Ctrl+Enter"或快捷图标"Run"可以运行当前行或者加亮选中行, 快捷图标"Source"可以运行整个文件。 如果发现中文乱码, 可以用菜单"Reopen with encoding"选择合适的编码打开, 用菜单"Save with encoding"选择需要的编码保存。
2.9.2 当前工作目录
在用source()调用源程序文件或者用read.csv()读入数据文件时, 如果不写文件名的全路径, 就认为文件位置是在所谓"当前工作目录"。 用getwd()函数可以查询当前工作目录, 用setwd()函数可以设置当前工作目录。 在RStudio中用菜单"Session--Set working directory"设置当前工作目录。
在MS Windows操作系统中使用R软件时, 一种好的做法是把某个研究项目所有数据和程序放在某个文件夹如 c:\work中, 把R的程序快捷图标复制到该目录中, 在资源管理器中对该图标调用鼠标右键菜单"属性", 从弹出对话框中,把"起始位置"一栏清除。 这样,每次从这个快捷图标启动R, 就可以自动以所在子目录为当前工作目录, 工作空间和命令历史记录也默认存放在这里。
在MS Windows操作系统的R中使用文件路径时, 要用正斜杠作为连接符, 使用反斜杠则需要成对使用, 如setwd("d:/work")或setwd("d:\\work")。
如果使用RStudio软件, 将某个研究项目所有数据和程序放在某个文件夹中, 然后建立一个新项目(project)指向该文件夹。
2.9.3 练习
编辑生成ssq.r源程序文件并用source()函数运行, 然后计算:
sum.of.squares(1:5)
2.10 附录:数据
2.10.1 公司纳税数据样例
本数据是某地区2013年12月所属税款申报信息的一个子集, 仅含20个公司的数据。
3 常量与变量
3.1 常量
R语言基本的数据类型有数值型, 逻辑型(TRUE, FALSE),文本(字符串)。 支持缺失值,有专门的复数类型。
常量是指直接写在程序中的值。
数值型常量包括整型、单精度、双精度等,一般不需要区分。写法如123, 123.45, -123.45, -0.012, 1.23E2, -1.2E-2等。 为了表示123是整型,可以写成123L。
字符型常量用两个双撇号或两个单撇号包围,如"Li Ming"或'Li Ming'。 字符型支持中文,如"李明"或'李明'。 国内的中文编码主要有GBK编码和UTF-8编码, 有时会遇到编码错误造成乱码的问题,MS Windows下R程序一般用GBK编码,但是RStudio软件采用UTF-8编码。 在R软件内字符串一般用UTF-8编码保存。
逻辑型常量只有TRUE和FALSE。
缺失值用NA表示。统计计算中经常会遇到缺失值,表示记录丢失、因为错误而不能用、节假日没有数据等。 除了数值型,逻辑型和字符型也可以有缺失值, 而且字符型的空白值不会自动辨识为缺失值,需要自己规定。 R支持特殊的Inf值,这是实数型值,表示正无穷大,不算缺失值。
复数常量写法如2.2 + 3.5i, 1i等。
3.2 变量
程序语言中的变量用来保存输入的值或者计算得到的值。 在R中,变量可以保存所有的数据类型, 比如标量、向量、矩阵、数据框、函数等。
变量都有变量名,R变量名必须以字母、数字、下划线和句点组成, 变量名的第一个字符不能取为数字。 在中文环境下,汉字也可以作为变量名的合法字符使用。 变量名是区分大小写的, y和Y是两个不同的变量名。
变量名举例: x, x1, X, cancer.tab, clean_data, diseaseData。
用<-赋值的方法定义变量。<-也可以写成=,但是<-更直观。 如
x5 <- 6.25x6 = sqrt(x5)
R的变量没有固定的类型, 给已有变量赋值为新的类型, 该变量就变成新的类型, 但一般应避免这样的行为。 R是"动态类型"语言, 赋值实际上是"绑定"(binding), 即将一个变量名与一个存储地址联系在一起, 同一个存储地址可以有多个变量名与其联系。
3.3 R数据类型
R语言基本的数据类型有数值, 逻辑型(TRUE, FALSE),文本(字符串)。 支持缺失值,有专门的复数类型。
R语言数据结构包括向量,矩阵和数据框,多维数组, 列表,对象等。数据中元素、行、列还可以用名字访问。 最基本的是向量类型。 向量类型数据的访问方式也是其他数据类型访问方式的基础。
4 数值型向量及其运算
4.1 数值型向量
向量是将若干个基础类型相同的值存储在一起, 各个元素可以按序号访问。 如果将若干个数值存储在一起可以用序号访问, 就叫做一个数值型向量。
用c()函数把多个元素或向量组合成一个向量。如
marks <- c(10, 6, 4, 7, 8)x <- c(1:3, 10:13)x1 <- c(1, 2)x2 <- c(3, 4)x <- c(x1, x2)x## [1] 1 2 3 4
10:13这样的写法表示从10到13的整数组成的向量。
用print()函数显示向量或在命令行中显示向量时, 每行显示的行首会有方括号和数字序号, 代表该行显示的第一个向量元素的下标。如
12345678901:12345678920## [1] 12345678901 12345678902 12345678903 12345678904 12345678905## [6] 12345678906 12345678907 12345678908 12345678909 12345678910## [11] 12345678911 12345678912 12345678913 12345678914 12345678915## [16] 12345678916 12345678917 12345678918 12345678919 12345678920
length(x)可以求x的长度。 长度为零的向量表示为numeric(0)。 numeric()函数可以用来初始化一个指定元素个数而元素都等于零的数值型向量, 如numeric(10)会生成元素为10个零的向量。
4.2 向量运算
4.2.1 标量和标量运算
单个数值称为标量, R没有单独的标量类型, 标量实际是长度为1的向量。
R中四则运算用+ - * / ^表示(加、减、乘、除、乘方),如
1.5 + 2.3 - 0.6 + 2.1*1.2 - 1.5/0.5 + 2^3## [1] 10.72
R中四则运算仍遵从通常的优先级规则, 可以用圆括号()改变运算的先后次序。 如
1.5 + 2.3 - (0.6 + 2.1)*1.2 - 1.5/0.5 + 2^3## [1] 5.56
除了加、减、乘、除、乘方, R还支持整除运算和求余运算。 用%/%表示整除,用%%表示求余。如
5 %/% 3## [1] 15 %% 3## [1] 25.1 %/% 2.5## [1] 25.1 %% 2.5## [1] 0.1
4.2.2 向量与标量运算
向量与标量的运算为每个元素与标量的运算, 如
x <- c(1, 10)x + 2## [1] 3 12x - 2## [1] -1 8x * 2## [1] 2 20x / 2## [1] 0.5 5.0x ^ 2## [1] 1 1002 / x## [1] 2.0 0.22 ^ x## [1] 2 1024
一个向量乘以一个标量, 就是线性代数中的数乘运算。
四则运算时如果有缺失值,缺失元素参加的运算相应结果元素仍缺失。 如
c(1, NA, 3) + 10## [1] 11 NA 13
4.2.3 等长向量运算
等长向量的运算为对应元素两两运算。 如
x1 <- c(1, 10)x2 <- c(4, 2)x1 + x2## [1] 5 12x1 - x2## [1] -3 8x1 * x2## [1] 4 20x1 / x2## [1] 0.25 5.00
两个等长向量的加、减运算就是线性代数中两个向量的加、减运算。
4.2.4 不等长向量的运算
两个不等长向量的四则运算, 如果其长度为倍数关系,规则是每次从头重复利用短的一个。 如
x1 <- c(10, 20)x2 <- c(1, 3, 5, 7)x1 + x2## [1] 11 23 15 27x1 * x2## [1] 10 60 50 140
不仅是四则运算,R中有两个或多个向量按照元素一一对应参与某种运算或函数调用时, 如果向量长度不同,一般都采用这样的规则。
如果两个向量的长度不是倍数关系,会给出警告信息。如
c(1,2) + c(1,2,3)## Warning in c(1, 2) + c(1, 2, 3): 长的对象长度不是短的对象长度的整倍数## [1] 2 4 4
4.3 向量函数
4.3.1 向量化的函数
R中的函数一般都是向量化的: 在R中, 如果普通的一元函数以向量为自变量,一般会对每个元素计算。 这样的函数包括sqrt, log10, log, exp, sin, cos, tan等许多。 如
sqrt(c(1, 4, 6.25))## [1] 1.0 2.0 2.5
为了查看这些基础的数学函数的列表,运行命令help.start(), 点击链接"Search Engine and Keywords", 找到"Mathematics"栏目, 浏览其中的"arith"和"math"链接中的说明。 常用的数学函数有:
- 舍入:
ceiling,floor,round,signif,trunc,zapsmall - 符号函数
sign - 绝对值
abs - 平方根
sqrt - 对数与指数函数
log,exp,log10,log2 - 三角函数
sin,cos,tan - 反三角函数
asin,acos,atan,atan2 - 双曲函数
sinh,cosh,tanh - 反双曲函数
asinh,acosh,atanh
有一些不太常用的数学函数:
- 贝塔函数
beta,lbeta - 伽玛函数
gamma,lgamma,digamma,trigamma,tetragamma,pentagamma - 组合数
choose,lchoose - 富利叶变换和卷积
fft,mvfft,convolve - 正交多项式
poly - 求根
polyroot,uniroot - 最优化
optimize,optim - Bessel函数
besselI,besselK,besselJ,besselY - 样条插值
spline,splinefun - 简单的微分
deriv
如果自己编写的函数没有考虑向量化问题, 可以用Vectorize()函数将其转换成向量化版本。
4.3.2 排序函数
sort(x)返回排序结果。 rev(x)返回把各元素排列次序反转后的结果。 order(x)返回排序用的下标。如
x <- c(33, 55, 11)sort(x)## [1] 11 33 55rev(sort(x))## [1] 55 33 11order(x)## [1] 3 1 2x[order(x)]## [1] 11 33 55
例子中, order(x)结果中3是x的最小元素11所在的位置下标, 1是x的第二小元素33所在的位置下标, 2是x的最大元素55所在的位置下标。
4.3.3 统计函数
sum(求和), mean(求平均值), var(求样本方差), sd(求样本标准差), min(求最小值), max(求最大值), range(求最小值和最大值)等函数称为统计函数, 把输入向量看作样本,计算样本统计量。 prod求所有元素的乘积。
cumsum和cumprod计算累加和累乘积。如
cumsum(1:5)## [1] 1 3 6 10 15cumprod(1:5)## [1] 1 2 6 24 120
其它一些类似函数有pmax, pmin, cummax, cummin等。
4.3.4 生成规则序列的函数
seq函数是冒号运算符的推广。 比如,seq(5)等同于1:5。 seq(2,5)等同于2:5。 seq(11, 15, by=2)产生11,13,15。 seq(0, 2*pi, length.out=100)产生从0到的等间隔序列, 序列长度指定为100。
从这些例子可以看出,S函数可以带自变量名调用。 每个函数的变量名和用法可以查询其帮助信息, 在命令行界面用"?函数名"的方法查询。 在使用变量名时次序可以颠倒, 比如seq(to=5, from=2)}仍等同于2:5。
rep()函数用来产生重复数值。 为了产生一个初值为零的长度为n的向量, 用x <- rep(0, n)。 rep(c(1,3), 2)把第一个自变量重复两次, 结果相当于c(1,3,1,3)。
rep(c(1,3), c(2,4))则需要利用R的一般向量化规则, 把第一自变量的第一个元素1按照第二自变量中第一个元素2的次数重复, 把第一自变量中第二个元素3按照第二自变量中第二个元素4的次数重复, 结果相当于c(1,1,3,3,3,3)。
如果希望重复完一个元素后再重复另一元素,用each=选项, 比如rep(c(1,3), each=2)结果相当于c(1,1,3,3)。
4.4 复数向量
复数常数表示如3.5+2.4i, 1i。 用函数complex()生成复数向量, 指定实部和虚部。 如complex(c(1,0,-1,0), c(0,1,0,-1))}相当于c(1+0i, 1i, -1+0i, -1i)。
在complex()中可以用mod和arg指定模和辐角,如 complex(mod=1, arg=(0:3)/2*pi)结果同上。
用Re(z)求z的实部, 用Im(z)求z的虚部, 用Mod(z)或abs(z)求z的模, 用Arg(z)求z的辐角, 用Conj(z)求z的共轭。
sqrt, log, exp, sin等函数对复数也有定义, 但是函数定义域在自变量为实数时可能有限制而复数无限制, 这时需要区分自变量类型。如
sqrt(-1)## [1] NaN## Warning message:## In sqrt(-1) : NaNs producedsqrt(-1 + 0i)## [1] 0+1i
4.5 练习
- 显示1到100的整数的平方根和立方根(提示:立方根就是三分之一次方)。
- 设有10个人的小测验成绩为:
- 把这10个成绩存入变量x;
- 从小到大排序;
- 计算
order(x),解释order(x)结果中第3项代表的意义。 - 计算这些成绩的平均值、标准差、最小值、最大值、中位数。
- 生成区间上等间隔的100个格子点存入变量x中。
5 逻辑型向量及其运算
5.1 逻辑型向量与比较运算
逻辑型是R的基本数据类型之一,只有两个值TRUE和FALSE, 缺失时为NA。逻辑值一般产生自比较,如
sele <- (log10(15) < 2); print(sele)## [1] TRUE
向量比较结果为逻辑型向量。如
c(1, 3, 5) > 2## [1] FALSE TRUE TRUE(1:4) >= (4:1)## [1] FALSE FALSE TRUE TRUE
从例子可以看出,向量比较也遵从R的向量间运算的一般规则: 向量与标量的运算是向量每个元素与标量都分别运算一次, 等长向量的运算时对应元素的运算, 不等长但长度为倍数关系的向量运算是把短的从头重复利用。
与NA比较产生NA,如
c(1, NA, 3) > 2## [1] FALSE NA TRUENA == NA## [1] NA
为了判断向量每个元素是否NA, 用is.na()函数,如
is.na(c(1, NA, 3) > 2)## [1] FALSE TRUE FALSE
用is.finite()判断向量每个元素是否Inf值。
比较运算符包括
< <= > >= == != %in%
分别表示小于、小于等于、大于、大于等于、等于、不等于、属于。 要注意等于比较用了两个等号。
%in%是比较特殊的比较, x %in% y的运算把向量y看成集合, 运算结果是一个逻辑型向量, 第个元素的值为x的第元素是否属于y的逻辑型值。 如
c(1,3) %in% c(2,3,4)## [1] FALSE TRUEc(NA,3) %in% c(2,3,4)## [1] FALSE TRUEc(1,3) %in% c(NA, 3, 4)## [1] FALSE TRUEc(NA,3) %in% c(NA, 3, 4)## [1] TRUE TRUE
函数match(x, y)起到和x %in% y运算类似的作用, 但是其返回结果不是找到与否, 而是对x的每个元素, 找到其在y中首次出现的下标,找不到时取缺失值,如
match(c(1, 3), c(2,3,4,3))## [1] NA 2
5.2 逻辑运算
为了表达如"而且", "或者"之类的复合比较, 需要使用逻辑运算把两个比较连接起来。 逻辑运算符为&, |和!, 分别表示"同时成立"、"两者至少其一成立"、"条件的反面"。 比如,设age<=3表示婴儿,sex=='女'表示女性,则 age<=3 & sex=='女'表示女婴, age<=3 | sex=='女'表示婴儿或妇女, !(age<=3 | sex=='女')表示既非婴儿也非妇女。 为了确定运算的先后次序可以用圆括号()指定。
用xor(x, y)表示x与y的异或运算, 即值不相等时为真值,相等时为假值, 有缺失值参加运算时为缺失值。
逻辑向量与逻辑标量之间的逻辑运算, 两个逻辑向量之间的逻辑运算规则遵从一般R向量间运算规则。
在右运算符是缺失值时, 如果左运算符能够确定结果真假, 可以得到非缺失的结果。 例如,TRUE | NA为TRUE, FALSE & NA为FALSE。 不能确定结果时返回NA, 比如, TRUE & NA为NA, FALSE | NA为NA。
&&和||分别为短路的标量逻辑与和短路的标量逻辑或, 仅对两个标量进行运算,如果有向量也仅使用第一个元素。 一般用在if语句、while语句中, 且只要第一个比较已经决定最终结果就不计算第二个比较。 例如
if(TRUE || sqrt(-1)>0) next
其中的sqrt(-1)部分不会执行。
这里结果为TRUE, 第二部分没有参加计算, 否则第二部分的计算会发生函数自变量范围错误。
5.3 逻辑运算函数
因为R中比较与逻辑运算都支持向量之间、向量与标量之间的运算, 所以在需要一个标量结果时要特别注意, 后面讲到的if结构、while结构都需要逻辑标量而且不能是缺失值。 这时,应该对缺失值结果单独考虑。
若cond是逻辑向量, 用all(cond)测试cond的所有元素为真; 用any(cond)测试cond至少一个元素为真。 cond中允许有缺失值,结果可能为缺失值。 如
c(1, NA, 3) > 2## [1] FALSE NA TRUEall(c(1, NA, 3) > 2)## [1] FALSEany(c(1, NA, 3) > 2)## [1] TRUEall(NA)## [1] NAany(NA)## [1] NA
函数which()返回真值对应的所有下标,如
which(c(FALSE, TRUE, TRUE, FALSE, NA))## [1] 2 3which((11:15) > 12)## [1] 3 4 5
函数identical(x,y)比较两个R对象x与y的内容是否完全相同, 结果只会取标量TRUE与FALSE两种。 如
identical(c(1,2,3), c(1,2,NA))## [1] FALSEidentical(c(1L,2L,3L), c(1,2,3))## [1] FALSE
其中第二个结果假值是因为前一向量是整数型, 后一向量是实数型。
函数all.equal()与identical()类似, 但是在比较数值型时不区分整数型与实数型, 而且相同时返回标量TRUE, 但是不同时会返回一个说明有何不同的字符串。如
all.equal(c(1,2,3), c(1,2,NA))## [1] "'is.NA' value mismatch: 1 in current 0 in target"all.equal(c(1L,2L,3L), c(1,2,3))## [1] TRUE
函数duplicated()返回每个元素是否为重复值的结果,如:
duplicated(c(1,2,1,3,NA,4,NA))## [1] FALSE FALSE TRUE FALSE FALSE FALSE TRUE
用函数unique()可以返回去掉重复值的结果。
6 字符型数据及其处理
6.1 字符型向量
字符型向量是元素为字符串的向量。 如
s1 <- c('abc', '', 'a cat', NA, '李明')
注意空字符串并不能自动认为是缺失值, 字符型的缺失值仍用NA表示。
6.2 paste()函数
针对字符型数据最常用的R函数是paste()函数。 paste()用来连接两个字符型向量, 元素一一对应连接, 默认用空格连接。 如paste(c("ab", "cd"), c("ef", "gh")) 结果相当于c("ab ef", "cd gh")。
paste()在连接两个字符型向量时采用R的一般向量间运算规则, 而且可以自动把数值型向量转换为字符型向量。 可以作一对多连接, 如paste("x", 1:3)结果相当于c("x 1", "x 2", "x 3")。
用sep=指定分隔符, 如paste("x", 1:3, sep="")结果相当于c("x1", "x2", "x3")。
使用collapse=参数可以把字符型向量的各个元素连接成一个单一的字符串, 如paste(c("a", "b", "c"), collapse="")结果相当于"abc"。
6.3 转换大小写
toupper()函数把字符型向量内容转为大写, tolower()函数转为小写。 比如,toupper('aB cd')结果为"AB CD", tolower(c('aB', 'cd'))结果相当于c("ab" "cd")。 这两个函数可以用于不区分大小写的比较, 比如,不论x的值是'JAN', 'Jan'还是'jan', toupper(x)=='JAN'的结果都为TRUE。
6.4 字符串长度
用nchar(x, type='bytes')计算字符型向量x中每个字符串的以字节为单位的长度,这一点对中英文是有差别的, 中文通常一个汉字占两个字节,英文字母、数字、标点占一个字节。 用nchar(x, type='chars')计算字符型向量x中每个字符串的以字符个数为单位的长度,这时一个汉字算一个单位。
在画图时可以用strwidth()函数计算某个字符串或表达式占用的空间大小。
6.5 取子串
substr(x, start, stop)从字符串x中取出从第start个到第stop个的子串, 如
substr('JAN07', 1, 3)## [1] "JAN"
如果x是一个字符型向量,substr将对每个元素取子串。如
substr(c('JAN07', 'MAR66'), 1, 3)## [1] "JAN" "MAR"
用substring(x, start)可以从字符串x中取出从第start个到末尾的子串。如
substring(c('JAN07', 'MAR66'), 4)## [1] "07" "66"
6.6 类型转换
用as.numeric()把内容是数字的字符型值转换为数值,如
substr('JAN07', 4, 5)## [1] "07"substr('JAN07', 4, 5) + 2000## Error in substr("JAN07", 4, 5) + 2000 : ## non-numeric argument to binary operatoras.numeric(substr('JAN07', 4, 5)) + 2000## [1] 2007as.numeric(substr(c('JAN07', 'MAR66'), 4, 5))## [1] 7 66
as.numeric()是向量化的, 可以转换一个向量的每个元素为数值型。
用as.character()函数把数值型转换为字符型,如
as.character((1:5)*5)## [1] "5" "10" "15" "20" "25"
如果自变量本来已经是字符型则结果不变。
为了用指定的格式数值型转换成字符型, 可以使用sprintf()函数, 其用法与C语言的sprintf()函数相似, 只不过是向量化的。例如
sprintf('file%03d.txt', c(1, 99, 100))## [1] "file001.txt" "file099.txt" "file100.txt"
6.7 字符串拆分
用strsplit()函数可以把一个字符串按照某种分隔符拆分开,例如
x <- '10,8,7'strsplit(x, ',', fixed=TRUE)[[1]]## [1] "10" "8" "7"sum(as.numeric(strsplit(x, ',', fixed=TRUE)[[1]]))## [1] 25
因为strsplit()的结果是一个列表, 这个函数延后再详细讲。
6.8 字符串替换功能
用gsub()可以替换字符串中的子串, 这样的功能经常用在数据清理中。 比如,把数据中的中文标点改为英文标点, 去掉空格,等等。 如
x <- '1, 3; 5'gsub(';', ',', x, fixed=TRUE)## [1] "1, 3, 5"strsplit(gsub(';', ',', x, fixed=TRUE), ',')[[1]]## [1] "1" " 3" " 5"
字符串x中分隔符既有逗号又有分号, 上面的程序用gsub()把分号都换成逗号。
更多的文本数据(字符型数据)功能参见35。
6.9 正则表达式
正则表达式(regular expression)是一种匹配某种字符串模式的方法。 用这样的方法,可以从字符串中查找某种模式的出现位置, 替换某种模式,等等。 这样的技术可以用于文本数据的预处理, 比如用网络爬虫下载的大量网页文本数据。 R中支持perl语言格式的正则表达式, grep()和grepl()函数从字符串中查询某个模式, sub()和gsub()替换某模式。 比如, 下面的程序把多于一个空格替换成一个空格
gsub('[[:space:]]+', ' ', 'a cat in a box', perl=TRUE)## [1] "a cat in a box"
正则表达式功能强大但也不容易掌握。 详见35。
7 R向量下标和子集
在R中下标与子集是极为强大的功能, 需要一些练习才能熟练掌握, 许多其它语言中需要多个语句才能完成的工作在R中都可以简单地通过下标和子集来完成。
7.1 正整数下标
对向量x, 在后面加方括号和下标可以访问向量的元素和子集。
设x <- c(1, 4, 6.25)。 x[2]取出第二个元素; x[2] <- 99修改第二个元素。 x[c(1,3)]取出第1、3号元素; x[c(1,3)] <- c(11, 13)修改第1、3号元素。 下标可重复。 例如
x <- c(1, 4, 6.25)x[2]## [1] 4x[2] <- 99; x## [1] 1.00 99.00 6.25x[c(1,3)]## [1] 1.00 6.25x[c(1,3)] <- c(11, 13); x## [1] 11 99 13x[c(1,3,1)]## [1] 11 13 11
7.2 负整数下标
负下标表示扣除相应的元素后的子集,如
x <- c(1,4,6.25)x[-2]## [1] 1.00 6.25x[-c(1,3)]## [1] 4
负整数下标不能与正整数下标同时用来从某一向量中取子集, 比如,x[c(1,-2)]没有意义。
7.3 空下标与零下标
x[]表示取x的全部元素作为子集。 这与x本身不同,比如
x <- c(1,4,6.25)x[] <- 999x## [1] 999 999 999x <- c(1,4,6.25)x <- 999x## [1] 999
x[0]是一种少见的做法, 结果返回类型相同、长度为零的向量, 如numeric(0)。 相当于空集。
当0与正整数下标一起使用时会被忽略。 当0与负整数下标一起使用时也会被忽略。
7.4 下标超界
设向量x长度为, 则使用正整数下标时下标应在中取值。 如果使用大于的下标, 读取时返回缺失值,并不出错。 给超出的下标元素赋值, 则向量自动变长, 中间没有赋值的元素为缺失值。 例如
x <- c(1,4,6.25)x[5]## [1] NAx## [1] 1.00 4.00 6.25x[5] <- 9x## [1] 1.00 4.00 6.25 NA 9.00
虽然R的语法对下标超界不视作错误, 但是这样的做法往往来自不良的程序思路, 而且对程序效率有影响, 所以实际编程中应避免下标超界。
7.5 逻辑下标
下标可以是与向量等长的逻辑表达式, 一般是关于本向量或者与本向量等长的其它向量的比较结果,如
x <- c(1,4,6.25)x[x > 3]## [1] 4.00 6.25
取出x的大于3的元素组成的子集。
逻辑下标除了用来对向量取子集, 还经常用来对数据框取取子集, 也用在向量化的运算中。 例如,对如下示性函数
输入向量x,结果y需要也是一个向量,程序可以写成
f <- function(x){ y <- numeric(length(x)) y[x >= 0] <- 1 y[x < 0] <- 0 # 此语句多余 y}
事实上,向量化的逻辑选择有一个ifelse()函数, 比如,对上面的示性函数, 如果x是一个向量, 输出y向量可以写成y <- ifelse(x>=0, 1, 0)。
要注意的是,如果逻辑下标中有缺失值, 对应结果也是缺失值。 所以,在用逻辑下标作子集选择时, 一定要考虑到缺失值问题。正确的做法是加上!is.na前提, 如
x <- c(1, 4, 6.25, NA)x[x > 2]## [1] 4.00 6.25 NAx[!is.na(x) & x > 2]## [1] 4.00 6.25
7.6 which()、which.min()、which.max()函数
函数which()可以用来找到满足条件的下标, 如
x <- c(3, 4, 3, 5, 7, 5, 9)which(x > 5)## [1] 5 7seq(along=x)[x > 5]## [1] 5 7
这里seq(along=x)会生成由x的下标组成的向量。 用which.min()、which.max求最小值的下标和最大值的下标, 不唯一时只取第一个。如
which.min(x)## [1] 1which.max(x)## [1] 7
7.7 元素名
向量可以为每个元素命名。如
ages <- c("李明"=30, "张聪"=25, "刘颖"=28)
或
ages <- c(30, 25, 28)names(ages) <- c("李明", "张聪", "刘颖")
或
ages <- setNames(c(30, 25, 28), c("李明", "张聪", "刘颖"))
这时可以用元素名或元素名向量作为向量的下标,如
ages["张聪"]## 张聪 ## 25ages[c("李明", "刘颖")]## 李明 刘颖 ## 30 28ages["张聪"] <- 26
这实际上建立了字符串到数值的映射表。
用字符串作为下标时, 如果该字符串不在向量的元素名中, 读取时返回缺失值结果, 赋值时该向量会增加一个元素并以该字符串为元素名。
带有元素名的向量也可以是字符型或其它基本类型,如
sex <- c("李明"="男", "张聪"="男", "刘颖"="女")
除了给向量元素命名外, 在矩阵和数据框中还可以给行、列命名, 这会使得程序的扩展更为容易和安全。
R允许仅给部分元素命名, 这时其它元素名字为空字符串。 不同元素的元素名一般应该是不同的, 否则在使用元素作为下标时会发生误读, 但是R语法允许存在重名。
用unname(x)返回去掉了元素名的x的副本, 用names(x) <- NULL可以去掉x的元素名。
7.8 用R向量下标作映射
R在使用整数作为向量下标时,允许使用重复下标, 这样可以把数组x看成一个的整数到 x[1], x[2], , x[n]的一个映射表, 其中是x的长度。 比如,某商店有三种礼品,编号为1,2,3, 价格分别为68, 88和168。令
price.map <- c(68, 88, 168)
设某个收银员在一天内分别售出礼品编号为3,2,1,1,2,2,3, 可以用如下的映射方式获得售出的这些礼品对应的价格:
items <- c(3,2,1,1,2,2,3)y <- price.map[items]; print(y)## [1] 168 88 68 68 88 88 168
R向量可以用字符型向量作下标, 字符型下标也允许重复, 所以可以把带有元素名的R向量看成是元素名到元素值的映射表。 比如,设sex为10个学生的性别(男、女)
sex <- c("男", "男", "女", "女", "男", "女", "女", "女", "女", "男")
希望把每个学生按照性别分别对应到蓝色和红色。 首先建立一个R向量当作映射
sex.color <- c('男'='blue', '女'='red')
用R向量sex.color当作映射,可以获得每个学生对应的颜色
cols <- sex.color[sex]; print(cols)## 男 男 女 女 男 女 女 女 女 男 ## "blue" "blue" "red" "red" "blue" "red" "red" "red" "red" "blue"
这样的映射结果中带有不必要的元素名, 用unname()函数可以去掉元素名,如
unname(cols)## [1] "blue" "blue" "red" "red" "blue" "red" "red" "red" "red" "blue"
7.9 集合运算
可以把向量x看成一个集合,但是其中的元素允许有重复。 用unique(x)可以获得x的所有不同值。如
unique(c(1, 5, 2, 5))## [1] 1 5 2
用a %in% x判断a的每个元素是否属于向量x,如
5 %in% c(1,5,2)## [1] TRUEc(5,6) %in% c(1,5,2)## [1] TRUE FALSE
与%in运算符类似, 函数match(x, table)对向量x的每个元素, 从向量table中查找其首次出现位置并返回这些位置。 没有匹配到的元素位置返回NA_integer_(整数型缺失值)。 如
match(5, c(1,5,2))## [1] 2match(5, c(1,5,2,5))## [1] 2match(c(2,5), c(1,5,2,5))## [1] 3 2match(c(2,5,0), c(1,5,2,5))## [1] 3 2 NA
用intersect(x,y)求交集,结果中不含重复元素,如
intersect(c(5, 7), c(1, 5, 2, 5))## [1] 5
用union(x,y)求并集,结果中不含重复元素,如
union(c(5, 7), c(1, 5, 2, 5))## [1] 5 7 1 2
用setdiff(x,y)求差集,即x的元素中不属于y的元素组成的集合, 结果中不含重复元素,如
setdiff(c(5, 7), c(1, 5, 2, 5))## [1] 7
用setequal(x,y)判断两个集合是否相等, 不受次序与重复元素的影响,如
setequal(c(1,5,2), c(2,5,1))## [1] TRUEsetequal(c(1,5,2), c(2,5,1,5))## [1] TRUE
7.10 练习
设文件class.csv内容如下:
name,sex,age,height,weightAlice,F,13,56.5,84Becka,F,13,65.3,98Gail,F,14,64.3,90Karen,F,12,56.3,77Kathy,F,12,59.8,84.5Mary,F,15,66.5,112Sandy,F,11,51.3,50.5Sharon,F,15,62.5,112.5Tammy,F,14,62.8,102.5Alfred,M,14,69,112.5Duke,M,14,63.5,102.5Guido,M,15,67,133James,M,12,57.3,83Jeffrey,M,13,62.5,84John,M,12,59,99.5Philip,M,16,72,150Robert,M,12,64.8,128Thomas,M,11,57.5,85William,M,15,66.5,112
用如下程序可以把上述文件读入为R数据框d.class, 并取出其中的name和age列到变量name和age中:
d.class <- read.csv('class.csv', header=TRUE, stringsAsFactors=FALSE)name <- d.class[,'name']age <- d.class[,'age']
- 求出age中第3, 5, 7号的值;
- 用变量age, 求出达到15岁及以上的那些值;
- 用变量name和age, 求出Mary与James的年龄。
- 求age中除Mary与James这两人之外的那些人的年龄值,保存到变量age1中。
- 假设向量
x长度为n, 其元素是的一个重排。 可以把x看成一个i到x[i]的映射(i在中取值)。 求向量y, 保存了上述映射的逆映射,即: 如果x[i]=j, 则y[j]=i。
8 R数据类型的性质
8.1 存储模式与基本类型
R的变量可以存储多种不同的数据类型, 可以用typeof()函数来返回一个变量或表达式的类型。比如
typeof(1:3)## [1] "integer"typeof(c(1,2,3))## [1] "double"typeof(c(1, 2.1, 3))## [1] "double"typeof(c(TRUE, NA, FALSE))## [1] "logical"typeof('Abc')## [1] "character"typeof(factor(c('F', 'M', 'M', 'F')))## [1] "integer"
注意因子的结果是integer而不是因子。 函数mode()和storage.mode()以及typeof()类似, 但返回结果有差别。 这三个函数都是与存储类型有关, 不依赖于变量和表达式的实际作用。
R中数据的最基本的类型包括logical, integer, double, character, complex, raw, 其它数据类型都是由基本类型组合或转变得到的。 character类型就是字符串类型, raw类型是直接使用其二进制内容的类型。 为了判断某个向量x保存的基本类型, 可以用is.xxx()类函数, 如is.integer(x), is.double(x), is.numeric(x), is.logical(x), is.character(x), is.complex(x), is.raw(x)。 其中is.numeric(x)对integer和double内容都返回真值。
在R语言中数值一般看作double, 如果需要明确表明某些数值是整数, 可以在数值后面附加字母L,如
is.integer(c(1, -3))## [1] FALSEis.integer(c(1L, -3L))## [1] TRUE
整数型的缺失值是NA, 而double型的特殊值除了NA外, 还包括Inf, -Inf和NaN, 其中NaN也算是缺失值, Inf和-Inf不算是缺失值。 如:
c(-1, 0, 1)/0## [1] -Inf NaN Infis.na(c(-1, 0, 1)/0)## [1] FALSE TRUE FALSE
对double类型,可以用is.finite()判断是否有限值, NA、Inf, -Inf和NaN都不是有限值; 用is.infinite()判断是否Inf或-Inf; is.na()判断是否NA或NaN; is.nan()判断是否NaN。
严格说来, NA表示逻辑型缺失值, 但是当作其它类型缺失值时一般能自动识别。 NA_integer_是整数型缺失值, NA_real_是double型缺失值, NA_character_是字符型缺失值。
在R的向量类型中, integer类型、double类型、logical类型、character类型、还有complex类型和raw类型称为原子类型(atomic types), 原子类型的向量中元素都是同一基本类型的。 比如, double型向量的元素都是double或者缺失值。 除了原子类型的向量, 在R语言的定义中, 向量还包括后面要讲到的列表(list), 列表的元素不需要属于相同的基本类型, 而且列表的元素可以不是单一基本类型元素。 用typeof()函数可以返回向量的类型, 列表返回结果为"list":
typeof(list("a", 1L, 1.5))## [1] "list"
原子类型的各个元素除了基本类型相同, 还不包含任何嵌套结构,如:
c(1, c(2,3, c(4,5)))## [1] 1 2 3 4 5
8.2 类属
R具有一定的面向对象语言特征, 其数据类型有一个class属性, 函数class()可以返回变量类型的类属, 比如
typeof(factor(c('F', 'M', 'M', 'F')))## [1] "integer"mode(factor(c('F', 'M', 'M', 'F')))## [1] "numeric"storage.mode(factor(c('F', 'M', 'M', 'F')))## [1] "integer"class(factor(c('F', 'M', 'M', 'F')))## [1] "factor"class(as.numeric(factor(c('F', 'M', 'M', 'F'))))## [1] "numeric"
R有一个特殊的NULL类型, 这个类型只有唯一的一个NULL值, 表示不存在。 要把NULL与NA区分开来, NA是有类型的(integer、double、logical、character等), NA表示存在但是未知。 用is.null()函数判断某个变量是否取NULL。
8.3 类型转换
可以用as.xxx()类的函数在不同类型之间进行强制转换。 如
as.numeric(c(FALSE, TRUE))## [1] 0 1as.character(sqrt(1:4))## [1] "1" "1.4142135623731" "1.73205080756888"## [4] "2"
类型转换也可能是隐含的,比如, 四则运算中数值会被统一转换为double类型, 逻辑运算中运算元素会被统一转换为logical类型。 逻辑值转换成数值时,TRUE转换成1, FALSE转换成0。
在用c()函数合并若干元素时, 如果元素基本类型不同, 将统一转换成最复杂的一个,复杂程度从简单到复杂依次为: `logical。 如
c(FALSE, 1L, 2.5, "3.6")## [1] "FALSE" "1" "2.5" "3.6"
不同类型参与要求类型相同的运算时,也会统一转换为最复杂的类型, 如:
TRUE + 10## [1] 11paste("abc", 1)## [1] "abc 1"
8.4 属性
除了NULL以外, R的变量都可以看成是对象, 都可以有属性。 在R语言中, 属性是把变量看成对象后, 除了其存储内容(如元素)之外的其它附加信息, 如维数、类属等。 对象x的所有属性可以用attributes()读取, 如
x <- table(c(1,2,1,3,2,1)); print(x)## ## 1 2 3 ## 3 2 1attributes(x)## $dim## [1] 3## ## $dimnames## $dimnames[[1]]## [1] "1" "2" "3"## ## ## $class## [1] "table"
table()函数用了输出其自变量中每个不同值的出现次数,称为频数。 从上例可以看出, table()函数的结果有三个属性,前两个是dim和dimnames, 这是数组(array)具有的属性; 另一个是class属性,值为"table"。 因为x是数组,可以访问如
x[1]## 1 ## 3x["3"]## 3 ## 1
也可以用attributes()函数修改属性, 如
attributes(x) <- NULLx## [1] 3 2 1
如上修改后x不再是数组,也不是table。
class属性是特殊的。 如果一个对象具有class属性, 某些所谓"通用函数(generic functions)"会针对这样的对象进行专门的操作, 比如, print()函数在显示向量和回归结果时采用完全不同的格式。 这在其它程序设计语言中称为"重载"(overloading)。
可以用attr(对象, "属性名")读取和修改单个属性。 向量的元素名是names属性,例如
ages <- c("李明"=30, "张聪"=25, "刘颖"=28)names(ages)## [1] "李明" "张聪" "刘颖"attr(ages, "names")## [1] "李明" "张聪" "刘颖"attr(ages, "names") <- NULLages## [1] 30 25 28
还可以用unname()函数返回一个去掉了names属性的副本。
8.5 str()函数
用print()函数可以显示对象内容。 如果内容很多, 显示行数可能也很多。 用str()函数可以显示对象的类型和主要结构及典型内容。例如
s <- 101:200attr(s,'author') <- '李小明'attr(s,'date') <- '2016-09-12'str(s)## int [1:100] 101 102 103 104 105 106 107 108 109 110 ...## - attr(*, "author")= chr "李小明"## - attr(*, "date")= chr "2016-09-12"
8.6 关于赋值
要注意的是, 在R中赋值本质上是把一个存储的对象与一个变量名联系在一起(binding), 多个变量名可以指向同一个对象。 对于基本的数据类型如数值型向量, 两个指向相同对象的变量当一个变量被修改时自动制作副本,如
x <- 1:5y <- xy[3] <- 0x## [1] 1 2 3 4 5y## [1] 1 2 0 4 5
这里如果y没有与其它变量指向同一对象, 则修改时直接修改该对象而不制作副本。
但是对于有些比较复杂的类型, 两个指向同一对象的变量是同步修改的。 这样的类型的典型代表是闭包(closure), 它带有一个环境,环境的内容是不自动制作副本的。
9 R日期时间
9.1 R日期和日期时间类型
R日期可以保存为Date类型, 一般用整数保存,数值为从1970-1-1经过的天数。
R中用一种叫做POSIXct和POSIXlt的特殊数据类型保存日期和时间, 可以仅包含日期部分,也可以同时有日期和时间。 技术上,POSIXct把日期时间保存为从1970年1月1日零时到该日期时间的时间间隔秒数, 所以数据框中需要保存日期时用POSIXct比较合适, 需要显示时再转换成字符串形式; POSIXlt把日期时间保存为一个包含年、月、日、星期、时、分、秒等成分的列表, 所以求这些成分可以从POSIXlt格式日期的列表变量中获得。 日期时间会涉及到所在时区、夏时制等问题, 比较复杂。
基础的R用as.Date()、as.POSIXct()等函数生成日期型和日期时间型, R扩展包lubridate提供了多个方便函数, 可以更容易地生成、转换、管理日期型和日期时间型数据。
library(lubridate)## ## 载入程辑包:'lubridate'## The following object is masked from 'package:base':## ## date
9.2 从字符串生成日期数据
函数lubridate::today()返回当前日期:
today()## [1] "2019-08-29"
函数lubridate::now()返回当前日期时间:
now()## [1] "2019-08-29 21:35:07 CST"
结果显示中出现的CST是时区, 这里使用了操作系统提供的当前时区。 CST不是一个含义清晰的时区, 在不同国家对应不同的时区, 在中国代表中国标准时间(北京时间)。
用lubridate::ymd(), lubridate::mdy(), lubridate::dmy()将字符型向量转换为日期型向量,如:
ymd(c("1998-3-10", "2018-01-17", "18-1-17"))## [1] "1998-03-10" "2018-01-17" "2018-01-17"mdy(c("3-10-1998", "01-17-2018"))## [1] "1998-03-10" "2018-01-17"dmy(c("10-3-1998", "17-01-2018"))## [1] "1998-03-10" "2018-01-17"
在年号只有两位数字时,默认对应到1969-2068范围。
lubridate::make_date(year, month, day)可以从三个数值构成日期向量。 如
make_date(1998, 3, 10)## [1] "1998-03-10"
lubridate包的ymd、mdy、dmy等函数添加hms、hm、h等后缀, 可以用于将字符串转换成日期时间。 如
ymd_hms("1998-03-16 13:15:45")## [1] "1998-03-16 13:15:45 UTC"
结果显示中UTC是时区, UTC是协调世界时(Universal Time Coordinated)英文缩写, 是由国际无线电咨询委员会规定和推荐, 并由国际时间局(BIH)负责保持的以秒为基础的时间标度。 UTC相当于本初子午线(即经度0度)上的平均太阳时, 过去曾用格林威治平均时(GMT)来表示. 北京时间比UTC时间早8小时, 以1999年1月1日0000UTC为例, UTC时间是零点, 北京时间为1999年1月1日早上8点整。
在Date()、as.DateTime()、ymd()等函数中, 可以用tz=指定时区, 比如北京时间可指定为tz="Etc/GMT+8"或 tz="Asia/Shanghai"。
lubridate::make_datetime(year, month, day, hour, min, sec) 可以从最多六个数值组成日期时间, 其中时分秒缺省值都是0。 如
make_datetime(1998, 3, 16, 13, 15, 45.2)## [1] "1998-03-16 13:15:45 UTC"
用lubridate::as_date()可以将日期时间型转换为日期型,如
as_date(as.POSIXct("1998-03-16 13:15:45"))## [1] "1998-03-16"
用lubridate::as_datetime()可以将日期型数据转换为日期时间型,如
as_datetime(as.Date("1998-03-16"))## [1] "1998-03-16 UTC"
9.3 日期显示格式
用as.character()函数把日期型数据转换为字符型, 如
x <- as.POSIXct(c('1998-03-16', '2015-11-22'))as.character(x)## [1] "1998-03-16" "2015-11-22"
在as.character()中可以用format选项指定显示格式,如
as.character(x, format='%m/%d/%Y')## [1] "03/16/1998" "11/22/2015"
格式中"%Y"代表四位的公元年号, "%m"代表两位的月份数字, "%d"代表两位的月内日期号。
"15Mar98"这样的日期在英文环境中比较常见, 但是在R中的处理比较复杂。 在下面的例子中,R日期被转换成了类似"Mar98"这样的格式, 在format选项中用了"%b"代表三英文字母月份缩写, 但是因为月份缩写依赖于操作系统默认语言环境, 需要用Sys.setlocale()函数设置语言环境为"C"。示例程序如下
x <- as.POSIXct(c('1998-03-16', '2015-11-22'))old.lctime <- Sys.getlocale('LC_TIME')Sys.setlocale('LC_TIME', 'C')## [1] "C"as.character(x, format='%b%y')## [1] "Mar98" "Nov15"Sys.setlocale('LC_TIME', old.lctime)## [1] "Chinese (Simplified)_China.936"
format选项中的"%y"表示两位数的年份, 应尽量避免使用两位数年份以避免混淆。
包含时间的转换如
x <- as.POSIXct('1998-03-16 13:15:45')as.character(x)## [1] "1998-03-16 13:15:45"as.character(x, format='%H:%M:%S')## [1] "13:15:45"
这里"%H"代表小时(按24小时制), "%M"代表两位的分钟数字, "%S"代表两位的秒数。
9.4 访问日期时间的组成值
lubridate包的如下函数可以取出日期型或日期时间型数据中的组成部分:
year()取出年month()取出月份数值mday()取出日数值yday()取出日期在一年中的序号,元旦为1wday()取出日期在一个星期内的序号, 但是一个星期从星期天开始, 星期天为1,星期一为2,星期六为7。hour()取出小时minute()取出分钟second()取出秒
比如, 2018-1-17是星期三, 则
month(as.POSIXct("2018-1-17 13:15:40"))## [1] 1mday(as.POSIXct("2018-1-17 13:15:40"))## [1] 17wday(as.POSIXct("2018-1-17 13:15:40"))## [1] 4
lubridate的这些成分函数还允许被赋值, 结果就修改了相应元素的值,如
x <- as.POSIXct("2018-1-17 13:15:40")year(x) <- 2000month(x) <- 1mday(x) <- 1x## [1] "2000-01-01 13:15:40 CST"
update()可以对一个日期或一个日期型向量统一修改其组成部分的值, 如
x <- as.POSIXct("2018-1-17 13:15:40")y <- update(x, year=2000)y## [1] "2000-01-17 13:15:40 CST"
update()函数中可以用year, month, mday, hour, minute, second等参数修改日期的组成部分。
9.5 日期舍入计算
lubridate包提供了floor_date(), round_date(), ceiling_date()等函数, 对日期可以用unit=指定一个时间单位进行舍入。 时间单位为字符串, 如seconds, 5 seconds, minutes, 2 minutes, hours, days, weeks, months, years等。
比如,以10 minutes为单位, floor_date()将时间向前归一化到10分钟的整数倍, ceiling_date()将时间向后归一化到10分钟的整数倍, round_date()将时间归一化到最近的10分钟的整数倍, 时间恰好是5分钟倍数时按照类似四舍五入的原则向上取整。 例如
x <- ymd_hms("2018-01-11 08:32:44")floor_date(x, unit="10 minutes")## [1] "2018-01-11 08:30:00 UTC"ceiling_date(x, unit="10 minutes")## [1] "2018-01-11 08:40:00 UTC"round_date(x, unit="10 minutes")## [1] "2018-01-11 08:30:00 UTC"
如果单位是星期, 会涉及到一个星期周期的开始是星期日还是星期一的问题。 用参数week_start=7指定开始是星期日, week_start=1指定开始是星期一。
9.6 日期计算
在lubridate的支持下日期可以相减, 可以进行加法、除法。 lubridate包提供了如下的三种与时间长短有关的数据类型:
- 时间长度(duration),按整秒计算
- 时间周期(period),如日、周
- 时间区间(interval),包括一个开始时间和一个结束时间
9.6.1 时间长度
R的POSIXct日期时间之间可以相减,如
d1 <- ymd_hms("2000-01-01 0:0:0")d2 <- ymd_hms("2000-01-02 12:0:5")di <- d2 - d1; di## Time difference of 1.500058 days
结果显示与日期之间差别大小有关系, 结果是类型是difftime。
lubridate包提供了duration类型, 处理更方便:
as.duration(di)## [1] "129605s (~1.5 days)"
lubridate的dseconds(), dminutes(), dhours(), ddays(), dweeks(), dyears()函数可以直接生成时间长度类型的数据,如
dhours(1)## [1] "3600s (~1 hours)"
lubridate的时间长度类型总是以秒作为单位, 可以在时间长度之间相加, 也可以对时间长度乘以无量纲数,如
dhours(1) + dseconds(5)## [1] "3605s (~1 hours)"dhours(1)*10## [1] "36000s (~10 hours)"
可以给一个日期加或者减去一个时间长度, 结果严格按推移的秒数计算, 如
d2 <- ymd_hms("2000-01-02 12:0:5")d2 - dhours(5)## [1] "2000-01-02 07:00:05 UTC"d2 + ddays(10)## [1] "2000-01-12 12:00:05 UTC"
时间的前后推移在涉及到时区和夏时制时有可能出现未预料到的情况。
用unclass()函数将时间长度数据的类型转换为普通数值, 如:
unclass(dhours(1))## [1] 3600
9.6.2 时间周期
时间长度的固定单位是秒, 但是像月、年这样的单位, 因为可能有不同的天数, 所以日历中的时间单位往往没有固定的时长。
lubridate包的seconds(), minutes(), hours(), days(), weeks(), years()函数可以生成以日历中正常的周期为单位的时间长度, 不需要与秒数相联系, 可以用于时间的前后推移。 这些时间周期的结果可以相加、乘以无量纲整数:
years(2) + 10*days(1)## [1] "2y 0m 10d 0H 0M 0S"
lubridate的月度周期因为与已有函数名冲突, 所以没有提供, 需要使用lubridate::period(num, units="month")的格式, 其中num是几个月的数值。
为了按照日历进行日期的前后平移, 而不是按照秒数进行日期的前后平移, 应该使用这些时间周期。 例如,因为2016年是闰年, 按秒数给2016-01-01加一年,得到的并不是2017-01-01:
ymd("2016-01-01") + dyears(1)## [1] "2016-12-31"
使用时间周期函数则得到预期结果:
ymd("2016-01-01") + years(1)## [1] "2017-01-01"
9.6.3 时间区间
lubridate提供了%--%运算符构造一个时间期间。 时间区间可以求交集、并集等。
构造如:
d1 <- ymd_hms("2000-01-01 0:0:0")d2 <- ymd_hms("2000-01-02 12:0:5")din <- (d1 %--% d2); din## [1] 2000-01-01 UTC--2000-01-02 12:00:05 UTC
对一个时间区间可以用除法计算其时间长度,如
din / ddays(1)## [1] 1.500058din / dseconds(1)## [1] 129605
生成时间区间, 也可以用lubridate::interval(start, end)函数,如
interval(ymd_hms("2000-01-01 0:0:0"), ymd_hms("2000-01-02 12:0:5"))## [1] 2000-01-01 UTC--2000-01-02 12:00:05 UTC
可以指定时间长度和开始日期生成时间区间, 如
d1 <- ymd("2018-01-15")din <- as.interval(dweeks(1), start=d1); din## [1] 2018-01-15 UTC--2018-01-22 UTC
注意这个时间区间表面上涉及到8个日期, 但是实际长度还是只有7天, 因为每一天的具体时间都是按零时计算, 所以区间末尾的那一天实际不含在内。
用lubridate::int_start()和lubridate::int_end()函数访问时间区间的端点,如:
int_start(din)## [1] "2018-01-15 UTC"int_end(din)## [1] "2018-01-22 UTC"
可以用as.duration()将一个时间区间转换成时间长度, 用as.period()将一个时间区间转换为可变时长的时间周期个数。
用lubridate::int_shift()平移一个时间区间,如
din2 <- int_shift(din, by=ddays(3)); din2## [1] 2018-01-18 UTC--2018-01-25 UTC
用lubridate::int_overlaps()判断两个时间区间是否有共同部分,如
int_overlaps(din, din2)## [1] TRUE
时间区间允许开始时间比结束时间晚, 用lubridate::int_standardize()可以将时间区间标准化成开始时间小于等于结束时间。 lubridate()现在没有提供求交集的功能,一个自定义求交集的函数如下:
int_intersect <- function(int1, int2){ n <- length(int1) int1 <- lubridate::int_standardize(int1) int2 <- lubridate::int_standardize(int2) sele <- lubridate::int_overlaps(int1, int2) inter <- rep(lubridate::interval(NA, NA), n) if(any(sele)){ inter[sele] <- lubridate::interval(pmax(lubridate::int_start(int1[sele]), lubridate::int_start(int2[sele])), pmin(lubridate::int_end(int1[sele]), lubridate::int_end(int2[sele]))) } inter}
测试如:
d1 <- ymd(c("2018-01-15", "2018-01-18", "2018-01-25"))d2 <- ymd(c("2018-01-21", "2018-01-23", "2018-01-30"))din <- interval(d1, d2); din## [1] 2018-01-15 UTC--2018-01-21 UTC 2018-01-18 UTC--2018-01-23 UTC## [3] 2018-01-25 UTC--2018-01-30 UTCint_intersect(rep(din[1], 2), din[2:3])## [1] 2018-01-18 UTC--2018-01-21 UTC NA--NA
此自定义函数还可以进一步改成允许两个自变量长度不等的情形。
9.7 基本R软件的日期功能
9.7.1 生成日期和日期时间型数据
对yyyy-mm-dd或yyyy/mm/dd格式的数据, 可以直接用as.Date()转换为Date类型,如:
x <- as.Date("1970-1-5"); x## [1] "1970-01-05"as.numeric(x)## [1] 4
as.Date()可以将多个日期字符串转换成Date类型,如
as.Date(c("1970-1-5", "2017-9-12"))## [1] "1970-01-05" "2017-09-12"
对于非标准的格式,在as.Date()中可以增加一个format选项, 其中用%Y表示四位数字的年, %m表示月份数字,%d表示日数字。如
as.Date("1/5/1970", format="%m/%d/%Y")## [1] "1970-01-05"
用as.POSIXct()函数把年月日格式的日期转换为R的标准日期, 没有时间部分就认为时间在午夜。如
as.POSIXct(c('1998-03-16'))## [1] "1998-03-16 CST"as.POSIXct(c('1998/03/16'))## [1] "1998-03-16 CST"
年月日中间的分隔符可以用减号也可以用正斜杠, 但不能同时有减号又有斜杠。
待转换的日期时间字符串,可以是年月日之后隔一个空格以"时:分:秒"格式带有时间。如
as.POSIXct('1998-03-16 13:15:45')## [1] "1998-03-16 13:15:45 CST"
用as.POSIXct()可以同时转换多项日期时间,如
as.POSIXct(c('1998-03-16 13:15:45', '2015-11-22 9:45:3'))## [1] "1998-03-16 13:15:45 CST" "2015-11-22 09:45:03 CST"
转换后的日期变量有class属性,取值为POSIXct与POSIXt, 并带有一个tzone(时区)属性。
x <- as.POSIXct(c('1998-03-16 13:15:45', '2015-11-22 9:45:3'))attributes(x)## $class## [1] "POSIXct" "POSIXt" ## ## $tzone## [1] ""
在as.POSIXct()函数中用format参数指定一个日期格式。如
as.POSIXct('3/13/15', format='%m/%d/%y')## [1] "2015-03-13 CST"
如果日期仅有年和月,必须添加日(添加01为日即可)才能读入。 比如用'1991-12'表示1991年12月,则如下程序将其读入为'1991-12-01':
as.POSIXct(paste('1991-12', '-01', sep=''), format='%Y-%m-%d')## [1] "1991-12-01 CST"
又如
old.lctime <- Sys.getlocale('LC_TIME')Sys.setlocale('LC_TIME', 'C')## [1] "C"as.POSIXct(paste('01', 'DEC91', sep=''), format='%d%b%y')## [1] "1991-12-01 CST"Sys.setlocale('LC_TIME', old.lctime)## [1] "Chinese (Simplified)_China.936"
把'DEC91'转换成了'1991-12-01'。
如果明确地知道时区, 在as.POSIXct()和as.POSIXlt()中可以加选项tz=字符串。 选项tz的缺省值为空字符串, 这一般对应于当前操作系统的默认时区。 但是,有些操作系统和R版本不能使用默认值, 这时可以为tz指定时区, 比如北京时间可指定为tz='Etc/GMT+8'。如
as.POSIXct('1949-10-01', tz='Etc/GMT+8')## [1] "1949-10-01 -08"
9.7.2 取出日期时间的组成值
把一个R日期时间值用as.POSIXlt()转换为POSIXlt类型, 就可以用列表元素方法取出其组成的年、月、日、时、分、秒等数值。 如
x <- as.POSIXct('1998-03-16 13:15:45')y <- as.POSIXlt(x)cat(1900+y$year, y$mon+1, y$mday, y$hour, y$min, y$sec, '\n')## 1998 3 16 13 15 45
注意year要加1900,mon要加1。 另外,列表元素wday取值1-6时表示星期一到星期六, 取值0时表示星期天。
对多个日期,as.POSIXlt()会把它们转换成一个列表(列表类型稍后讲述), 这时可以用列表元素year, mon, mday等取出日期成分。如
x <- as.POSIXct(c('1998-03-16', '2015-11-22'))as.POSIXlt(x)$year + 1900## [1] 1998 2015
9.7.3 日期计算
因为Date类型是用数值保存的,所以可以给日期加减一个整数,如:
x <- as.Date("1970-1-5")x1 <- x + 10; x1## [1] "1970-01-15"x2 <- x - 5; x2## [1] "1969-12-31"
所有的比较运算都适用于日期类型。
可以给一个日期加减一定的秒数,如
as.POSIXct(c('1998-03-16 13:15:45')) - 30## [1] "1998-03-16 13:15:15 CST"as.POSIXct(c('1998-03-16 13:15:45')) + 10## [1] "1998-03-16 13:15:55 CST"
但是两个日期不能相加。
给一个日期加减一定天数, 可以通过加减秒数实现,如
as.POSIXct(c('1998-03-16 13:15:45')) + 3600*24*2## [1] "1998-03-18 13:15:45 CST"
这个例子把日期推后了两天。
用difftime(time1, time2, units='days')计算time1减去time2的天数, 如
x <- as.POSIXct(c('1998-03-16', '2015-11-22'))c(difftime(x[2], x[1], units='days'))## Time difference of 6460 days
函数结果用c()包裹以转换为数值, 否则会带有单位。
调用difftime()时如果前两个自变量中含有时间部分, 则间隔天数也会带有小数部分。如
x <- as.POSIXct(c('1998-03-16 13:15:45', '2015-11-22 9:45:3'))c(difftime(x[2], x[1], units='days'))## Time difference of 6459.854 days
difftime()中units选项还可以取为 'secs', 'mins', 'hours'等。
9.8 练习
设文件dates.csv中包含如下内容:
"出生日期","发病日期""1941/3/8","2007/1/1""1972/1/24","2007/1/1""1932/6/1","2007/1/1""1947/5/17","2007/1/1""1943/3/10","2007/1/1""1940/1/8","2007/1/1""1947/8/5","2007/1/1""2005/4/14","2007/1/1""1961/6/23","2007/1/2""1949/1/10","2007/1/2"
把这个文件读入为R数据框dates.tab, 运行如下程序定义date1和date2变量:
date1 <- dates.tab[,'出生日期']date2 <- dates.tab[,'发病日期']
- 把date1、date2转换为R的POSIXct日期型。
- 求date1中的各个出生年。
- 计算发病时的年龄,以周岁论(过生日才算)。
- 把date2中发病年月转换为'monyy'格式,这里mon是如FEB这样英文三字母缩写, yy是两数字的年份。
- 对诸如'FEB91', 'OCT15'这样的年月数据, 假设00---20表示21世纪年份,21---99表示20实际年份。 编写R函数,输入这样的字符型向量, 返回相应的POSIXct格式日期, 具体日期都取为相应月份的1号。 这个习题和后两个习题可以预习函数部分来做。
- 对R的POSIXct日期,写函数转换成'FEB91', 'OCT15'这样的年月表示, 假设00---20表示21世纪年份,21---99表示20实际年份。
- 给定两个POSIXct日期向量birth和work, birth为生日,work是入职日期, 编写R函数, 返回相应的入职周岁整数值(不到生日时周岁值要减一)。
10 R因子类型
10.1 因子
R中用因子代表数据中分类变量, 如性别、省份、职业。 有序因子代表有序量度,如打分结果,疾病严重程度等。
用factor()函数把字符型向量转换成因子,如
x <- c("男", "女", "男", "男", "女")sex <- factor(x)sex## [1] 男 女 男 男 女## Levels: 男 女attributes(sex)## $levels## [1] "男" "女"## ## $class## [1] "factor"
因子有class属性,取值为"factor", 还有一个levels(水平值)属性, 此属性可以用levels()函数访问,如
levels(sex)## [1] "男" "女"
因子的levels属性可以看成是一个映射, 把整数值1,2,映射成这些水平值, 因子在保存时会保存成整数值1,2,等与水平值对应的编号。 这样可以节省存储空间, 在建模计算的程序中也比较有利于进行数学运算。
事实上, read.csv()函数的默认操作会把输入文件的字符型列自动转换成因子, 这对于性别、职业、地名这样的列是合适的, 但是对于姓名、日期、详细地址这样的列则不合适。 所以,在read.csv()调用中经常加选项stringsAsFactors=FALSE选项禁止这样的自动转换,还可以用colClasses选项逐个指定每列的类型。
用as.numeric()可以把因子转换为纯粹的整数值,如
as.numeric(sex)## [1] 1 2 1 1 2
因为因子实际保存为整数值, 所以对因子进行一些字符型操作可能导致错误。 用as.character()可以把因子转换成原来的字符型,如
as.character(sex)## [1] "男" "女" "男" "男" "女"
为了对因子执行字符型操作(如取子串), 保险的做法是先用as.character()函数强制转换为字符型。
factor()函数的一般形式为
factor(x, levels = sort(unique(x), na.last = TRUE), labels, exclude = NA, ordered = FALSE)
可以用选项levels自行指定各水平值, 不指定时由x的不同值来求得。 可以用选项labels指定各水平的标签, 不指定时用各水平值的对应字符串。 可以用exclude选项指定要转换为缺失值(NA)的元素值集合。 如果指定了levels, 则当自变量x的某个元素等于第个水平值时输出的因子对应元素值取整数, 如果该元素值没有出现在levels中则输出的因子对应元素值取NA。 ordered取真值时表示因子水平是有次序的(按编码次序)。
在使用factor()函数定义因子时, 如果知道自变量元素的所有可能取值, 应尽可能使用levels=参数指定这些不同可能取值, 这样, 即使某个取值没有出现, 此变量代表的含义和频数信息也是完整的。 自己指定levels=的另一好处是可以按正确的次序显示因子的分类统计值。
因为一个因子的levels属性是该因子独有的, 所以合并两个因子有可能造成错误。如
li1 <- factor(c('男', '女'))li2 <- factor(c('男', '男'))c(li1, li2)## [1] 1 2 1 1
结果不再是因子。 正确的做法是
factor(c(as.character(li1), as.character(li2)))## [1] 男 女 男 男## Levels: 男 女
即恢复成字符型后合并, 然后再转换为因子。 在合并两个数据框时也存在这样的问题。 当然,如果在定义li1和li2 时都用了levels=c('男', '女')选项, c(li1, li2)也能给出正确结果。
10.2 table()函数
用table()函数统计因子各水平的出现次数(称为频数或频率)。 也可以对一般的向量统计每个不同元素的出现次数。 如
table(sex)## sex## 男 女 ## 3 2
对一个变量用table函数计数的结果是一个特殊的有元素名的向量, 元素名是自变量的不同取值, 结果的元素值是对应的频数。 单个因子或单个向量的频数结果可以用向量的下标访问方法取出单个频数或若干个频数的子集。
10.3 tapply()函数
可以按照因子分组然后每组计算另一变量的概括统计。 如
h <- c(165, 170, 168, 172, 159)tapply(h, sex, mean)## 男 女 ## 168.3333 164.5000
这里第一自变量h与与第二自变量sex是等长的, 对应元素分别为同一人的身高和性别, tapply()函数分男女两组计算了身高平均值。
10.4 forcats包的因子函数
library(forcats)
在分类变量类数较多时,往往需要对因子水平另外排序、合并等, forcats包提供了一些针对因子的方便函数。
forcats::fac_reorder()可以根据不同因子水平分成的组中另一数值型变量的统计量值排序。 如:
set.seed(1)fac <- sample(c("red", "green", "blue"), 30, replace=TRUE)fac <- factor(fac, levels=c("red", "green", "blue"))x <- round(100*(10+rt(30,2)))res1 <- tapply(x, fac, sd); res1## red green blue ## 370.9222 138.3185 1129.2587barplot(res1)

如果希望按照统计量次序对因子排序, 可以用forcats::fct_reorder()函数, 如
fac2 <- fct_reorder(fac, x, sd)res2 <- tapply(x, fac2, sd)barplot(res2)

新的因子fac2的因子水平次序已经按照变量x的标准差从小到大排列。
有时在因子水平数较多时仅想将特定的一个或几个水平次序放到因子水平最前面, 可以用forcats::fct_relevel()函数, 如:
levels(fac)## [1] "red" "green" "blue"fac3 <- fct_relevel(fac, "blue"); levels(fac3)## [1] "blue" "red" "green"
fct_relevel()第一个参数是要修改次序的因子, 后续可以有多个字符型参数表示要提前的水平。
forcats::fct_reorder2(f, x, y)也调整因子f的水平的次序, 但是根据与每组中最大的x值相对应的y值大小调整次序, 这样在作多个因子水平对应的曲线图时可以比较容易地区分多条曲线。
forcats::fct_recode()可以修改每个水平的名称, 如:
fac4 <- fct_recode( fac, "红"="red", "绿"="green", "蓝"="blue")table(fac4)## fac4## 红 绿 蓝 ## 13 10 7
fct_recode()在修改水平名时允许多个旧水平对应到一个新水平, 从而合并原来的水平。 如果合并很多, 可以用fct_collapse()函数, 如
compf <- fct_collapse( comp, "其它"=c("", "无名", "无应答"), "联想"=c("联想", "联想集团"), "百度"=c("百度", "百度集团") )
如果某个因子频数少的水平很多, 在统计时有过多水平不易展示主要的类别, 可以用forcats::fct_lump(f)合并, 缺省地从最少的类合并一直到"其它"类超过其它最小的类之前, 可以用n=参数指定要保留多少个类。
10.5 练习
设文件class.csv中包含如下内容:
name,sex,age,height,weightAlice,F,13,56.5,84Becka,F,13,65.3,98Gail,F,14,64.3,90Karen,F,12,56.3,77Kathy,F,12,59.8,84.5Mary,F,15,66.5,112Sandy,F,11,51.3,50.5Sharon,F,15,62.5,112.5Tammy,F,14,62.8,102.5Alfred,M,14,69,112.5Duke,M,14,63.5,102.5Guido,M,15,67,133James,M,12,57.3,83Jeffrey,M,13,62.5,84John,M,12,59,99.5Philip,M,16,72,150Robert,M,12,64.8,128Thomas,M,11,57.5,85William,M,15,66.5,112
用如下程序把该文件读入为R数据框d.class, 其中的sex列已经自动转换为因子。 取出其中的sex和age列到变量sex和age中
d.class <- read.csv('class.csv', header=TRUE)sex <- d.class[,'sex']age <- d.class[,'age']
- 统计并显示列出sex的不同值频数;
- 分男女两组分别求年龄最大值;
- 把sex变量转换为一个新的因子,F显示成"Female",M显示成"Male"。
11 R矩阵和数组
11.1 R矩阵
矩阵用matrix函数定义,实际存储成一个向量,根据保存的行数和列数对应到矩阵的元素, 存储次序为按列存储。 定义如
A <- matrix(11:16, nrow=3, ncol=2); print(A)## [,1] [,2]## [1,] 11 14## [2,] 12 15## [3,] 13 16B <- matrix(c(1,-1, 1,1), nrow=2, ncol=2, byrow=TRUE); print(B)## [,1] [,2]## [1,] 1 -1## [2,] 1 1
matrix()函数把矩阵元素以一个向量的形式输入, 用nrow和ncol规定行数和列数,向量元素填入矩阵的缺省次序是按列填入, 用byrow=TRUE选项可以转换成按行填入。
用nrow()和ncol()函数可以访问矩阵的行数和列数,如
nrow(A)## [1] 3ncol(A)## [1] 2
矩阵有一个dim属性,内容是两个元素的向量, 两个元素分别为矩阵的行数和列数。dim属性可以用dim()函数访问。如
attributes(A)## $dim## [1] 3 2dim(A)## [1] 3 2
函数t(A)返回A的转置。
11.2 矩阵子集
用A[1,]取出A的第一行,变成一个普通向量。 用A[,1]取出A的第一列,变成一个普通向量。 用A[c(1,3),1:2]取出指定行、列对应的子矩阵。 如
A## [,1] [,2]## [1,] 11 14## [2,] 12 15## [3,] 13 16A[1,]## [1] 11 14A[,1]## [1] 11 12 13A[c(1,3), 1:2]## [,1] [,2]## [1,] 11 14## [2,] 13 16
用colnames()函数可以给矩阵每列命名, 也可以访问矩阵列名, 用rownames()函数可以给矩阵每行命名, 也可以访问矩阵行名。如
colnames(A) <- c('X', 'Y')rownames(A) <- c('a', 'b', 'c')A## X Y## a 11 14## b 12 15## c 13 16
矩阵可以有一个dimnames属性, 此属性是两个元素的列表(列表见稍后部分的介绍), 两个元素分别为矩阵的行名字符型向量与列名字符型向量。 如果仅有其中之一,缺失的一个取为NULL。
有了列名、行名后,矩阵下标可以用字符型向量, 如
A[,'Y']## a b c ## 14 15 16A['b',]## X Y ## 12 15A[c('a', 'c'), 'Y']## a c ## 14 16
注意在对矩阵取子集时, 如果取出的子集仅有一行或仅有一列, 结果就不再是矩阵而是变成了R向量, R向量既不是行向量也不是列向量。 如果想避免这样的规则起作用, 需要在方括号下标中加选项drop=FALSE, 如
A[,1,drop=FALSE]## X## a 11## b 12## c 13
取出了A的第一列, 作为列向量取出, 所谓列向量实际是列数等于1的矩阵。 如果用常量作为下标, 其结果维数是确定的,不会出问题; 如果用表达式作为下标, 则表达式选出零个、一个、多个下标, 结果维数会有不同, 加drop=FALSE则是安全的做法。
矩阵也可以用逻辑下标取子集,比如
A## X Y## a 11 14## b 12 15## c 13 16A[A[,1]>=2,'Y']## a b c ## 14 15 16
矩阵本质上是一个向量添加了dim属性, 实际保存还是保存成一个向量, 其中元素的保存次序是按列填入, 所以, 也可以向对一个向量取子集那样, 仅用一个正整数向量的矩阵取子集。如
A## X Y## a 11 14## b 12 15## c 13 16A[c(1,3,5)]## [1] 11 13 15
为了挑选矩阵的任意元素组成的子集而不是子矩阵, 可以用一个两列的矩阵作为下标, 矩阵的每行的两个元素分别指定一个元素的行号和列号。 如
ind <- matrix(c(1,1, 2,2, 3,2), ncol=2, byrow=TRUE)A## X Y## a 11 14## b 12 15## c 13 16ind## [,1] [,2]## [1,] 1 1## [2,] 2 2## [3,] 3 2A[ind]## [1] 11 15 16
用c(A)或A[]返回矩阵A的所有元素。 如果要修改矩阵A的所有元素, 可以对A[]赋值。
对矩阵A,diag(A)访问A的主对角线元素组成的向量。 另外,若x为正整数值标量,diag(x)返回x阶单位阵; 若x为长度大于1的向量, diag(x)返回以x的元素为主对角线元素的对角矩阵。
11.3 cbind()和rbind()函数
若x是向量,cbind(x)把x变成列向量, 即列数为1的矩阵, rbind(x)把x变成行向量。
若x1, x2, x3是等长的向量, cbind(x1, x2, x3)把它们看成列向量并在一起组成一个矩阵。 cbind()的自变量可以同时包含向量与矩阵,向量的长度必须与矩阵行数相等。 如
cbind(c(1,2), c(3,4), c(5,6))## [,1] [,2] [,3]## [1,] 1 3 5## [2,] 2 4 6cbind(A, c(1,-1,10))## X Y ## a 11 14 1## b 12 15 -1## c 13 16 10
cbind()的自变量中也允许有标量, 这时此标量被重复使用。 如
cbind(1, c(1,-1,10))## [,1] [,2]## [1,] 1 1## [2,] 1 -1## [3,] 1 10
rbind()用法类似, 可以等长的向量看成行向量上下摞在一起, 可以是矩阵与长度等于矩阵列数的向量上下摞在一起, 向量长度为1也可以。
11.4 矩阵运算
11.4.1 四则运算
矩阵可以与标量作四则运算,结果为每个元素进行相应运算,如
A## X Y## a 11 14## b 12 15## c 13 16C1 <- A + 2; C1## X Y## a 13 16## b 14 17## c 15 18C2 <- A / 2; C2## X Y## a 5.5 7.0## b 6.0 7.5## c 6.5 8.0
当运算为矩阵乘以一个标量时, 就是线性代数中的矩阵的数乘运算。
两个同形状的矩阵进行加、减运算, 即对应元素相加、相减, 用A + B,A - B表示,如
C1 + C2## X Y## a 18.5 23.0## b 20.0 24.5## c 21.5 26.0C1 - C2## X Y## a 7.5 9.0## b 8.0 9.5## c 8.5 10.0
这就是线性代数中矩阵的加、减运算。
对两个同形状的矩阵, 用*表示两个矩阵对应元素相乘(注意这不是线性代数中的矩阵乘法), 用/表示两个矩阵对应元素相除。 如
C1 * C2## X Y## a 71.5 112.0## b 84.0 127.5## c 97.5 144.0C1 / C2## X Y## a 2.363636 2.285714## b 2.333333 2.266667## c 2.307692 2.250000
11.4.2 矩阵乘法
用%*%表示矩阵乘法而不是用*表示, 注意矩阵乘法要求左边的矩阵的列数等于右边的矩阵的行数。 如
A## X Y## a 11 14## b 12 15## c 13 16B## [,1] [,2]## [1,] 1 -1## [2,] 1 1C3 <- A %*% B; C3## [,1] [,2]## a 25 3## b 27 3## c 29 3
11.4.3 向量与矩阵相乘
矩阵与向量进行乘法运算时, 向量按需要解释成列向量或行向量。 当向量左乘矩阵时,看成行向量; 当向量右乘矩阵时,看成列向量。 如
B## [,1] [,2]## [1,] 1 -1## [2,] 1 1c(1,1) %*% B## [,1] [,2]## [1,] 2 0B %*% c(1,1)## [,1]## [1,] 0## [2,] 2c(1,1) %*% B %*% c(1,1)## [,1]## [1,] 2
注意矩阵乘法总是给出矩阵结果, 即使此矩阵已经退化为行向量、列向量甚至于退化为标量也是一样。 如果需要,可以用c()函数把一个矩阵转换成按列拉直的向量。
11.4.4 内积
设x, y是两个向量, 计算向量内积, 可以用sum(x*y)表示。
设, 是两个矩阵, 是广义的内积, 也称为叉积(crossprod), 结果是一个矩阵, 元素为的每列与的每列计算内积的结果。 在R中可以表示为crossprod(A, B), 可以表示为crossprod(A)。 要注意的是,crossprod()的结果总是矩阵, 所以计算两个向量的内积用sum(x,y)而不用crossprod(x,y)。
11.4.5 外积
R向量支持外积运算, 记为%o%, 结果为矩阵。 x %o% y的第行第列元素等于x[i]乘以y[j]。 如
c(1,2,3) %o% c(1, -1)## [,1] [,2]## [1,] 1 -1## [2,] 2 -2## [3,] 3 -3
这种运算还可以推广到x的每一元素与y的每一元素进行其它的某种运算, 而不限于乘积运算,可以用outer(x,y,f)完成, 其中f是某种运算,或者接受两个自变量的函数。
11.5 逆矩阵与线性方程组求解
用solve(A)求A的逆矩阵,如
solve(B)## [,1] [,2]## [1,] 0.5 0.5## [2,] -0.5 0.5
用solve(A,b)求解线性方程组中的, 如
solve(B, c(1,2))## [1] 1.5 0.5
求解了线性方程组
11.6 apply()函数
apply(A, 2, FUN)把矩阵A的每一列分别输入到函数FUN中, 得到对应于每一列的结果,如
D <- matrix(c(6,2,3,5,4,1), nrow=3, ncol=2); D## [,1] [,2]## [1,] 6 5## [2,] 2 4## [3,] 3 1apply(D, 2, sum)## [1] 11 10
apply(A, 1, FUN)把矩阵A的每一行分别输入到函数FUN中, 得到与每一行对应的结果,如
apply(D, 1, mean)## [1] 5.5 3.0 2.0
如果函数FUN返回多个结果, 则apply(A, 2, FUN)结果为矩阵, 矩阵的每一列是输入矩阵相应列输入到FUN的结果, 结果列数等于A的列数。如
apply(D, 2, range)## [,1] [,2]## [1,] 2 1## [2,] 6 5
如果函数FUN返回多个结果, 为了对每行计算FUN的结果, 结果存入一个与输入的矩阵行数相同的矩阵, 应该用t(apply(A, 1, FUN))的形式, 如
t(apply(D, 1, range))## [,1] [,2]## [1,] 5 6## [2,] 2 4## [3,] 1 3
11.7 多维数组
矩阵是多维数组(array)的特例。 矩阵是这样的两下标数据的存贮格式, 三维数组是这样的三下标数据的存贮格式, 维数组则是有个下标的数据的存贮格式。 实际上, 给一个向量添加一个dim属性就可以把它变成多维数组。
多维数组的一般定义语法为
数组名 <- array(数组元素, dim=c(第一下标个数, 第二下标个数, ..., 第s下标个数))
其中数组元素的填入次序是第一下标变化最快, 第二下标次之, 最后一个下标是变化最慢的。 这种次序称为FORTRAN次序。
下面是一个三维数组定义例子。
ara <- array(1:24, dim=c(2,3,4)); ara## , , 1## ## [,1] [,2] [,3]## [1,] 1 3 5## [2,] 2 4 6## ## , , 2## ## [,1] [,2] [,3]## [1,] 7 9 11## [2,] 8 10 12## ## , , 3## ## [,1] [,2] [,3]## [1,] 13 15 17## [2,] 14 16 18## ## , , 4## ## [,1] [,2] [,3]## [1,] 19 21 23## [2,] 20 22 24
这样的数组保存了。 三维数组ara可以看成是4个矩阵。 取出其中一个如ara[,,2](取出第二个矩阵)
ara[,,2]## [,1] [,2] [,3]## [1,] 7 9 11## [2,] 8 10 12
多维数组可以利用下标进行一般的子集操作, 比如ara[,2, 2:3] 是的值, 结果是一个矩阵:
ara[,2,2:3]## [,1] [,2]## [1,] 9 15## [2,] 10 16
多维数组在取子集时如果某一维下标是标量, 则结果维数会减少, 可以在方括号内用drop=FALSE选项避免这样的规则发生作用。
类似于矩阵, 多维数组可以用一个矩阵作为下标, 如果是三维数组,矩阵就需要有3列, 四维数组需要用4列矩阵。 下标矩阵的每行对应于一个数组元素。
12 数据框
12.1 数据框
统计分析中最常见的原始数据形式是类似于数据库表或Excel数据表的形式。 这样形式的数据在R中叫做数据框(data.frame)。 数据框类似于一个矩阵,有行、列, 但各列允许有不同类型:数值型向量、因子、字符型向量、日期时间向量。 同一列的数据类型相同。 在R中数据框是一个特殊的列表, 其每个列表元素都是一个长度相同的向量。 事实上,数据框还允许一个元素是一个矩阵, 但这样会使得某些读入数据框的函数发生错误。
函数data.frame()可以生成数据框,如
d <- data.frame( name=c("李明", "张聪", "王建"), age=c(30, 35, 28), height=c(180, 162, 175), stringsAsFactors=FALSE)print(d)## name age height## 1 李明 30 180## 2 张聪 35 162## 3 王建 28 175
data.frame()函数会将字符型列转换成因子, 加选项stringsAsFactors=FALSE可以避免这样的转换。
数据框每列叫做一个变量, 每列都有名字,称为列名或变量名, 可以用names()函数和colnames()函数访问。 如
names(d)## [1] "name" "age" "height"colnames(d)## [1] "name" "age" "height"
给names(d)或colnames(d)赋值可以修改列名。
用as.data.frame(x)可以把x转换成数据框。 如果x是一个向量, 转换结果是以x为唯一一列的数据框。 如果x是一个列表并且列表元素都是长度相同的向量, 转换结果中每个列表变成数据框的一列。 如果x是一个矩阵,转换结果把矩阵的每列变成数据框的一列。
数据框是一个随着R语言前身S语言继承下来的概念, 现在已经有一些不足之处, tibble包提供了tibble类, 这是数据框的一个改进版本。
12.2 数据框内容访问
数据框可以用矩阵格式访问,如
d[2,3]## [1] 162
访问单个元素。
d[[2]]## [1] 30 35 28
访问第二列,结果为向量。
d[,2]## [1] 30 35 28
也访问第二列,但是这种作法与tibble不兼容, 所以应避免使用。
按列名访问列可用如
d[["age"]]## [1] 30 35 28d[,"age"]## [1] 30 35 28d$age## [1] 30 35 28
其中第二种做法与tibble不兼容,应避免使用。
因为数据框的一行不一定是相同数据类型, 所以数据框的一行作为子集, 结果还是数据框,而不是向量。如
x <- d[2,]; x## name age height## 2 张聪 35 162is.data.frame(x)## [1] TRUE
可以同时取行子集和列子集,如
d[1:2, 'age']## [1] 30 35d[1:2, c('age', 'height')]## age height## 1 30 180## 2 35 162d[d[,'age']>=30,]## name age height## 1 李明 30 180## 2 张聪 35 162
与矩阵类似地是, 用如d[,'age'], d[,2]这样的方法取出的数据框的单个列是向量而不再是数据框。 但是,如果取出两列或者两列以上, 结果则是数据框。 如果取列子集时不能预先知道取出的列个数, 则子集结果有可能是向量也有可能是数据框, 容易造成后续程序错误。 对一般的数据框, 可以在取子集的方括号内加上drop=FALSE选项, 确保取列子集的结果总是数据框。 数据框的改进类型tibble在取出列子集时保持为tibble格式。
对数据框变量名按照字符串与集合进行操作可以实现复杂的列子集筛选。
数据框每一行可以有行名, 这在原始的S语言和传统的R语言中是重要的技术, 但是在改进类型tibble中则取消了行名, 需要用列名实现功能一般改用left_join()函数实现。
比如,每一行定义行名为身份证号,则可以唯一识别各行。 下面的例子以姓名作为行名:
rownames(d) <- d$named$name <- NULLd## age height## 李明 30 180## 张聪 35 162## 王建 28 175
用数据框的行名可以建立一个值到多个值的对应表。 比如,有如下的数据框:
dm <- data.frame( '年级'=1:6, '出游'=c(0, 2, 2, 2, 2, 1), '疫苗'=c(T, F, F, F, T, F))
其中"出游"是每个年级安排的出游次数, "疫苗"是该年级有全体无计划免疫注射。 把年级变成行名,可以建立年级到出游次数与疫苗注射的对应表:
rownames(dm) <- dm[['年级']]dm[['年级']] <- NULL
这样,假设某个社区的小学中抽取的4个班的年级为 c(2,1,1,3), 其对应的出游和疫苗注射信息可查询如下:
x <- c(2,1,1,3)dm[as.character(x),]## 出游 疫苗## 2 2 FALSE## 1 0 TRUE## 1.1 0 TRUE## 3 2 FALSE
结果中包含了不必要也不太合适的行名,可以去掉,以上程序改成:
x <- c(2,1,1,3)xx <- dm[as.character(x),]rownames(xx) <- NULLxx## 出游 疫苗## 1 2 FALSE## 2 0 TRUE## 3 0 TRUE## 4 2 FALSE
如果要从多个值建立映射, 比如,从省名与县名映射到经度、纬度, 可以预先用paste()函数把省名与县名合并在一起, 中间以适当字符(如`-``)分隔, 以这样的合并字符串为行名。
对于代替数据框的tibble类型, 如果要实现行名的功能, 可以将行名作为单独的一列, 然后用dplyr包的inner_join()、left_join()、full_join()等函数横向合并数据集。 参见27.15。
12.3 数据框与矩阵的区别
数据框不能作为矩阵参加矩阵运算。 需要时,可以用as.matrix()函数转换数据框或数据框的子集为矩阵。 如
d2 <- as.matrix(d[,c("age", "height")])d3 <- crossprod(d2); d3## age height## age 2909 15970## height 15970 89269
这里crossprod(A)表示。
12.4 gl()函数
可以用数据框保存试验结果, 对有多个因素的试验, 往往需要生成多个因素完全搭配并重复的表格。 函数gl()可以生成这样的重复模式。 比如,下面的例子:
d4 <- data.frame( group=gl(3, 10, length=30), subgroup=gl(5,2,length=30), obs=gl(2,1,length=30))print(d4)## group subgroup obs## 1 1 1 1## 2 1 1 2## 3 1 2 1## 4 1 2 2## 5 1 3 1## 6 1 3 2## 7 1 4 1## 8 1 4 2## 9 1 5 1## 10 1 5 2## 11 2 1 1## 12 2 1 2## 13 2 2 1## 14 2 2 2## 15 2 3 1## 16 2 3 2## 17 2 4 1## 18 2 4 2## 19 2 5 1## 20 2 5 2## 21 3 1 1## 22 3 1 2## 23 3 2 1## 24 3 2 2## 25 3 3 1## 26 3 3 2## 27 3 4 1## 28 3 4 2## 29 3 5 1## 30 3 5 2
结果的数据框d有三个变量: group是大组,共分3个大组,每组10个观测; subgroup是子组,在每个大组内分为5个子组,每个子组2个观测。 共有个观测(行)。
gl()第一个参数是因子水平个数, 第二个参数是同一因子水平连续重复次数, 第三个参数是总共需要的元素个数, 所有水平都出现后则重复整个模式直到长度满足要求。
12.5 tibble类型
tibble类型是一种改进的数据框。 readr包的read_csv()函数是read.csv()函数的一个改进版本, 它将CSV文件读入为tibble类型,如文件class.csv的读入:
library(tibble)library(readr)t.class <- read_csv("class.csv")## Parsed with column specification:## cols(## name = col_character(),## sex = col_character(),## age = col_double(),## height = col_double(),## weight = col_double()## )t.class## # A tibble: 19 x 5## name sex age height weight## <chr> <chr> <dbl> <dbl> <dbl>## 1 Alice F 13 56.5 84 ## 2 Becka F 13 65.3 98 ## 3 Gail F 14 64.3 90 ## 4 Karen F 12 56.3 77 ## 5 Kathy F 12 59.8 84.5## 6 Mary F 15 66.5 112 ## 7 Sandy F 11 51.3 50.5## 8 Sharon F 15 62.5 112. ## 9 Tammy F 14 62.8 102. ## 10 Alfred M 14 69 112. ## 11 Duke M 14 63.5 102. ## 12 Guido M 15 67 133 ## 13 James M 12 57.3 83 ## 14 Jeffrey M 13 62.5 84 ## 15 John M 12 59 99.5## 16 Philip M 16 72 150 ## 17 Robert M 12 64.8 128 ## 18 Thomas M 11 57.5 85 ## 19 William M 15 66.5 112
tibble类型的类属依次为tbl_df, tbl, data.frame:
class(t.class)## [1] "spec_tbl_df" "tbl_df" "tbl" "data.frame"
用as_tibble()可以将一个数据框转换为tibble, dplyr包提供了filter()、select()、arrange()、mutate() 等函数用来对tibble选取行子集、列子集,排序、修改或定义新变量,等等。 见27。
可以用tibble()函数生成小的tibble,如
t.bp <- tibble( `序号`=c(1,5,6,9,10,15), `收缩压`=c(145, 110, "未测", 150, "拒绝", 115))t.bp## # A tibble: 6 x 2## 序号 收缩压## <dbl> <chr> ## 1 1 145 ## 2 5 110 ## 3 6 未测 ## 4 9 150 ## 5 10 拒绝 ## 6 15 115
用tribble可以按类似于CSV格式输入一个tibble, 如
t.bp <- tribble(~`序号`,~`收缩压`,1,145,5,110,6,"未测",9,150,10,"拒绝",15,115)t.bp## # A tibble: 6 x 2## 序号 收缩压## <dbl> <chr> ## 1 1 145 ## 2 5 110 ## 3 6 未测 ## 4 9 150 ## 5 10 拒绝 ## 6 15 115
注意tribble()中数据每行末尾也需要有逗号, 最后一行末尾没有逗号。 这比较适用于在程序中输入小的数据集。
tibble与数据框的一大区别是在显示时不自动显示所有内容, 这样可以避免显示很大的数据框将命令行的所有显示都充满。 可以在print()用n=和width=选项指定要显示的行数和列数。
另外,用单重的方括号取列子集时, 即使仅取一列, 从tibble取出的一列结果仍是tibble而不是向量, 这时应使用双方括号格式或$格式。 因为这个原因有些原来的程序输入tibble会出错, 这时可以用as.data.frame()转换成数据框。 如:
t.bp[,"收缩压"]## # A tibble: 6 x 1## 收缩压## <chr> ## 1 145 ## 2 110 ## 3 未测 ## 4 150 ## 5 拒绝 ## 6 115t.bp[["收缩压"]]## [1] "145" "110" "未测" "150" "拒绝" "115"
tibble在定义时不需要列名为合法变量名, 但是作为变量名使用时需要用反单撇号包裹。 tibble不使用行名, 需要行名时, 将其保存为tibble的一列。 原来用行名完成的功能, 可以改用dplyr包的left_join()等函数, 这些函数进行数据框的横向连接。
12.6 练习
假设class.csv已经读入为R数据框d.class, 其中的sex列已经自动转换为因子。
- 显示d.class中年龄至少为15的行子集;
- 显示女生且年龄至少为15的学生姓名和年龄;
- 取出数据框中的age变量赋给变量x。
13 列表类型
13.1 列表
R中列表(list)类型来保存不同类型的数据。 一个主要目的是提供R分析结果输出包装: 输出一个变量, 这个变量包括回归系数、预测值、残差、检验结果等等一系列不能放到规则形状数据结构中的内容。 实际上,数据框也是列表的一种, 但是数据框要求各列等长, 而列表不要求。
列表可以有多个元素, 但是与向量不同的是, 列表的不同元素的类型可以不同, 比如, 一个元素是数值型向量, 一个元素是字符串, 一个元素是标量, 一个元素是另一个列表。
定义列表用函数list(), 如
rec <- list(name="李明", age=30, scores=c(85, 76, 90))rec## $name## [1] "李明"## ## $age## [1] 30## ## $scores## [1] 85 76 90
用typeof()函数判断一个列表, 返回结果为list。 可以用is.list()函数判断某个对象是否列表类型。
13.2 列表元素访问
列表的一个元素也可以称为列表的一个"变量", 单个列表元素必须用两重方括号格式访问,如
rec[[3]]## [1] 85 76 90rec[[3]][2]## [1] 76rec[["age"]]## [1] 30
如果使用单重方括号对列表取子集, 结果还是列表而不是列表元素,如
rec[3]## $scores## [1] 85 76 90
列表的单个元素也可以用$格式访问,如
rec$age## [1] 30
列表一般都应该有元素名, 元素名可以看成是变量名, 列表中的每个元素看成一个变量。 用names()函数查看和修改元素名。 如
names(rec)## [1] "name" "age" "scores"names(rec)[names(rec)=='scores'] <- '三科分数'names(rec)## [1] "name" "age" "三科分数"rec[["三科分数"]]## [1] 85 76 90
可以修改列表元素内容。 如
rec[["三科分数"]][2] <- 0print(rec)## $name## [1] "李明"## ## $age## [1] 30## ## $三科分数## [1] 85 0 90
直接给列表不存在的元素名定义元素值就添加了新元素, 而且不同于使用向量,对于列表而言这是很正常的做法,比如
rec[['身高']] <- 178print(rec)## $name## [1] "李明"## ## $age## [1] 30## ## $三科分数## [1] 85 0 90## ## $身高## [1] 178
把某个列表元素赋值为NULL就删掉这个元素。 如
rec[['age']] <- NULLprint(rec)## $name## [1] "李明"## ## $三科分数## [1] 85 0 90## ## $身高## [1] 178
在list()函数中允许定义元素为NULL,这样的元素是存在的,如:
li <- list(a=120, b='F', c=NULL); li## $a## [1] 120## ## $b## [1] "F"## ## $c## NULL
但是,要把已经存在的元素修改为NULL值而不是删除此元素, 或者给列表增加一个取值为NULL的元素, 这时需要用单重的方括号取子集, 这样的子集会保持其列表类型, 给这样的子列表赋值为list(NULL),如:
li['b'] <- list(NULL)li['d'] <- list(NULL)li## $a## [1] 120## ## $b## NULL## ## $c## NULL## ## $d## NULL
13.3 列表类型转换
用as.list()把一个其它类型的对象转换成列表; 用unlist()函数把列表转换成基本向量。如
li1 <- as.list(1:3)li1## [[1]]## [1] 1## ## [[2]]## [1] 2## ## [[3]]## [1] 3li2 <- list(x=1, y=c(2,3))unlist(li2)## x y1 y2 ## 1 2 3
13.4 返回列表的函数示例--strsplit()
strsplit()输入一个字符型向量并指定一个分隔符, 返回一个项数与字符型向量元素个数相同的列表, 列表每项对应于字符型向量中一个元素的拆分结果。 如
x <- c('10, 8, 7', '5, 2, 2', '3, 7, 8', '8, 8, 9')res <- strsplit(x, ','); res## [[1]]## [1] "10" " 8" " 7"## ## [[2]]## [1] "5" " 2" " 2"## ## [[3]]## [1] "3" " 7" " 8"## ## [[4]]## [1] "8" " 8" " 9"
为了把拆分结果进一步转换成一个数值型矩阵, 可以使用sapply()函数如下:
t(sapply(res, as.numeric))## [,1] [,2] [,3]## [1,] 10 8 7## [2,] 5 2 2## [3,] 3 7 8## [4,] 8 8 9
sapply()函数是apply类函数之一, 稍后再详细进行讲解。
14 工作空间
R把在命令行定义的变量都保存到工作空间中, 在退出R时可以选择是否保存工作空间。 这也是R与其他如C、Java这样的语言的区别之一。
用ls()命令可以查看工作空间中的内容。
随着多次在命令行使用R, 工作空间的变量越来越多, 使得重名的可能性越来越大, 而且工作空间中变量太多也让我们不容易查看其内容。 在命令行定义的变量称为"全局变量", 在编程实际中, 全局变量是需要慎用的。
可以用rm()函数删除工作空间中的变量,格式如
rm(d, h, name, rec, sex, x)
要避免工作空间杂乱, 最好的办法还是所有的运算都写到自定义函数中。 自定义函数中定义的变量都是临时的, 不会保存到工作空间中。 这样,仅需要时才把变量值在命令行定义, 这样的变量一般是读入的数据或自定义的函数 (自定义函数也保存在工作空间中)。
可以定义如下的sandbox()函数:
sandbox <- function(){ cat('沙盘:接连的空行回车可以退出。\n') browser()}
运行sandbox()函数,将出现如下的browser命令行:
沙盘:接连的空行回车可以退出。Called from: sandbox()Browse[1]>
提示符变成了"Browser[n]",其中n代表层次序号。 在这样的browser命令行中随意定义变量, 定义的变量不会保存到工作空间中。 用"Q"命令可以退出这个沙盘环境, 接连回车也可以退出。
15 R输入输出
15.1 输入输出的简单方法
15.1.1 简单的输出
用print()函数显示某个变量或表达式的值, 如
x <- 1.234print(x)## [1] 1.234y <- c(1,3,5)print(y[2:3])## [1] 3 5
在命令行使用R时, 直接以变量名或表达式作为命令可以起到用print()函数显示的相同效果。
用cat()函数把字符串、变量、表达式连接起来显示, 其中变量和表达式的类型一般是标量或向量,不能是矩阵、列表等复杂数据。 如
cat("x =", x, "\n")## x = 1.234cat("y =", y, "\n")## y = 1 3 5
注意cat()显示中需要换行需要在自变量中包含字符串"\n", 即换行符。
cat()默认显示在命令行窗口, 为了写入指定文件中, 在cat()调用中用file=选项, 这时如果已有文件会把原有内容覆盖, 为了在已有文件时不覆盖原有内容而是在末尾添加, 在cat()中使用append=TRUE选项。 如:
cat("=== 结果文件 ===\n", file="res.txt")cat("x =", x, "\n", file="res.txt", append=TRUE)
函数sink()可以用来把命令行窗口显示的运行结果转向保存到指定的文本文件中, 如果希望保存到文件的同时也在命令行窗口显示, 使用split=TRUE选项。如
sink("allres.txt", split=TRUE)
为了取消这样的输出文件记录, 使用不带自变量的sink()调用,如
sink()
在R命令行环境中定义的变量、函数会保存在工作空间中, 并在退出R会话时可以保存到硬盘文件中。 用save()命令要求把指定的若干个变量(直接用名字,不需要表示成字符串) 保存到用file=指定的文件中, 随后可以用load()命令恢复到工作空间中。 虽然允许保存多个变量到同一文件中, 但尽可能仅保存一个变量, 而且使用变量名作为文件名。 用save()保存的R特殊格式的文件是通用的, 不依赖于硬件和操作系统。 如
save(scores, file="scores.RData")load("scores.RData")
对于一个数据框, 可以用write.csv()或readr::write_csv()将其保存为逗号分隔的文本文件, 这样的文件可以很容易地被其它软件识别访问, 如Microsoft Excel软件可以很容易地把这样的文件读成电子表格。 用如
da <- tibble('name'=c('李明', '刘颖', '张浩'), 'age'=c(15, 17, 16))write_csv(da, path="mydata.csv")
结果生成的mydata.csv文件内容如下:
name,age李明,15刘颖,17张浩,16
但是,在Microsoft的中文版Windows操作系统中, 默认编码是GB编码, 用write_csv()生成的CSV文件总是使用UTF-8编码, 系统中的MS Office 软件不能自动识别这样编码的CSV文件。 write.csv()函数不存在这个问题。
15.1.2 简单的输入
用scan()函数可以输入文本文件中的数值向量, 文件名用file=选项给出。 文件中数值之间以空格分开。如
cat(1:12, "\n", file="d:/work/x.txt")x <- scan("d:/work/x.txt")
程序中用全路径给出了输入文件位置, 注意路径中用了正斜杠/作为分隔符, 如果在MS Windows环境下使用\作为分隔符, 在R的字符串常量中\必须写成\\。
如果scan()中忽略输入文件参数, 此函数将从命令行读入数据。 可以在一行用空格分开多个数值, 可以用多行输入直到空行结束输入。
这样的方法也可以用来读入矩阵。 设文件mat.txt包含如下矩阵内容:
3 4 25 12 107 8 61 9 11
可以先把文件内容读入到一个R向量中, 再利用matrix()函数转换成矩阵, 注意要使用byrow=TRUE选项, 而且只要指定ncol选项, 可以忽略nrow选项。如
M <- matrix(scan('mat.txt', quiet=TRUE), ncol=3, byrow=TRUE)M
scan()中的quite=TRUE选项使得读入时不自动显示读入的数值项数。
上面读入数值矩阵的方法在数据量较大的情形也可以使用, 与之不同的是, read.table()或readr::read_table()函数也可以读入这样的数据, 但是会保存成数据框而不是矩阵, 而且read.table()函数在读入大规模的矩阵时效率很低。
15.2 读取CSV文件
对于保存在文本文件中的电子表格数据, R可以用read.csv(), read.table(), read.delim(), read.fwf()等函数读入, 但是建议在readr包的支持下用read_csv(), read_table2(), read_delim(), read_fwf()等函数读入, 这些将读入的数据框保存为tibble类型, tibble是数据框的一个变种, 改善了数据框的一些不适当的设计。 readr的读入速度比基本R软件的read.csv()等函数的速度快得多, 速度可以相差10倍, 也不自动将字符型列转换成因子, 不自动修改变量名为合法变量名, 不设置行名。
对于中小规模的数据, CSV格式作为文件交换格式比较合适, 兼容性强, 各种数据管理软件与统计软件都可以很容易地读入和生成这样格式的文件, 但是特别大型的数据读入效率很低。
CSV格式的文件用逗号分隔开同一行的数据项, 一般第一行是各列的列名(变量名)。 对于数值型数据, 只要表示成数值常量形式即可。 对于字符型数据, 可以用双撇号包围起来, 也可以不用撇号包围。 但是, 如果数据项本身包含逗号, 就需要用双撇号包围。 例如,下面是一个名为testcsv.csv的文件内容, 其中演示了内容中有逗号、有双撇号的情况。
id,words1,"PhD"2,Master's degree 3,"Bond,James"4,"A ""special"" gift"
为读入上面的内容,只要用如下程序:
d <- read_csv("testcsv.csv")
读入的数据框显示如下:
# A tibble: 4 × 2 id words <int> <chr>1 1 PhD2 2 Master's degree3 3 Bond,James4 4 A "special" gift
read_csv()还可以从字符串读入一个数据框,如
d.small <- read_csv("name,x,yJohn, 33, 95Kim, 21, 64Sandy, 49, 100")d.small## # A tibble: 3 x 3## name x y## <chr> <dbl> <dbl>## 1 John 33 95## 2 Kim 21 64## 3 Sandy 49 100
read_csv()的skip=选项跳过开头的若干行。 当数据不包含列名时, 只要指定col_names=FALSE, 变量将自动命名为X1, X2, ..., 也可以用col_names=指定各列的名字,如
d.small <- read_csv("John, 33, 95Kim, 21, 64Sandy, 49, 100", col_names=c("name", "x", "y") )d.small## # A tibble: 3 x 3## name x y## <chr> <dbl> <dbl>## 1 John 33 95## 2 Kim 21 64## 3 Sandy 49 100
read_csv()将空缺的值读入为缺失值, 将"NA"也读入为缺失值。 可以用na=选项改变这样的设置。 也可以将带有缺失值的列先按字符型原样读入, 然后再进行转换。
CSV文件是文本文件,是有编码问题的, 尤其是中文内容的文件。 readr包的默认编码是UTF-8编码。 例如,文件bp.csv以GBK编码(有时称为GB18030编码, 这是中文Windows所用的中文编码)保存了如下内容:
序号,收缩压1,1455,1106, 未测9,15010, 拒绝15,115
如果直接用read_csv():
d <- read_csv("bp.csv")
可能在读入时出错,或者访问时出错。 为了读入用GBK编码的中文CSV文件, 需要利用locale参数和locale()函数:
d <- read_csv("bp.csv", locale=locale(encoding="GBK"))## Parsed with column specification:## cols(## 序号 = col_double(),## 收缩压 = col_character()## )d## # A tibble: 6 x 2## 序号 收缩压## <dbl> <chr> ## 1 1 145 ## 2 5 110 ## 3 6 未测 ## 4 9 150 ## 5 10 拒绝 ## 6 15 115
对每列的类型, readr用前1000行猜测合理的类型, 并在读取后显示猜测的每列类型。
但是有可能类型改变发生在1000行之后。 col_types选项可以指定每一列的类型, 如"col_double()", "col_integer()", "col_character()", "col_factor()", "col_date()", "col_datetime"等。 cols()函数可以用来规定各列类型, 并且有一个.default参数指定缺省类型。 对因子,需要在col_factor()中用lelvels=指定因子水平。
可以复制readr猜测的类型作为col_types的输入, 这样当数据变化时不会因为偶尔猜测错误而使得程序出错。如
d <- read_csv("bp.csv", locale=locale(encoding="GBK"), col_types=cols( `序号` = col_integer(), `收缩压` = col_character() ))d## # A tibble: 6 x 2## 序号 收缩压## <int> <chr> ## 1 1 145 ## 2 5 110 ## 3 6 未测 ## 4 9 150 ## 5 10 拒绝 ## 6 15 115
当猜测的文件类型有问题的时候, 可以先将所有列都读成字符型, 然后用type_convert()函数转换, 如:
d <- read_csv("filename.csv", col_types=cols(.default = col_character()))d <- type_convert(d)
读入有错时,对特大文件可以先少读入一些行, 用nmax=可以指定最多读入多少行。 调试成功后再读入整个文件。
设文件class.csv内容如下:
name,sex,age,height,weightAlice,F,13,56.5,84Becka,F,13,65.3,98Gail,F,14,64.3,90Karen,F,12,56.3,77Kathy,F,12,59.8,84.5Mary,F,15,66.5,112Sandy,F,11,51.3,50.5Sharon,F,15,62.5,112.5Tammy,F,14,62.8,102.5Alfred,M,14,69,112.5Duke,M,14,63.5,102.5Guido,M,15,67,133James,M,12,57.3,83Jeffrey,M,13,62.5,84John,M,12,59,99.5Philip,M,16,72,150Robert,M,12,64.8,128Thomas,M,11,57.5,85William,M,15,66.5,112
最简单地用read_csv()读入上述CSV文件,程序如:
d.class <- read_csv('class.csv')## Parsed with column specification:## cols(## name = col_character(),## sex = col_character(),## age = col_double(),## height = col_double(),## weight = col_double()## )knitr::kable(d.class)
| name | sex | age | height | weight |
|---|---|---|---|---|
| Alice | F | 13 | 56.5 | 84.0 |
| Becka | F | 13 | 65.3 | 98.0 |
| Gail | F | 14 | 64.3 | 90.0 |
| Karen | F | 12 | 56.3 | 77.0 |
| Kathy | F | 12 | 59.8 | 84.5 |
| Mary | F | 15 | 66.5 | 112.0 |
| Sandy | F | 11 | 51.3 | 50.5 |
| Sharon | F | 15 | 62.5 | 112.5 |
| Tammy | F | 14 | 62.8 | 102.5 |
| Alfred | M | 14 | 69.0 | 112.5 |
| Duke | M | 14 | 63.5 | 102.5 |
| Guido | M | 15 | 67.0 | 133.0 |
| James | M | 12 | 57.3 | 83.0 |
| Jeffrey | M | 13 | 62.5 | 84.0 |
| John | M | 12 | 59.0 | 99.5 |
| Philip | M | 16 | 72.0 | 150.0 |
| Robert | M | 12 | 64.8 | 128.0 |
| Thomas | M | 11 | 57.5 | 85.0 |
| William | M | 15 | 66.5 | 112.0 |
从结果看出,读入后显示了每列的类型。 对性别变量,没有自动转换成因子, 而是保存为字符型。 为了按自己的要求转换各列类型, 用了read_csv()的coltypes=选项和cols()函数如下:
ct <- cols( .default = col_double(), name=col_character(), sex=col_factor(levels=c("M", "F")))d.class <- read_csv('class.csv', col_types=ct)str(d.class)## Classes 'spec_tbl_df', 'tbl_df', 'tbl' and 'data.frame': 19 obs. of 5 variables:## $ name : chr "Alice" "Becka" "Gail" "Karen" ...## $ sex : Factor w/ 2 levels "M","F": 2 2 2 2 2 2 2 2 2 1 ...## $ age : num 13 13 14 12 12 15 11 15 14 14 ...## $ height: num 56.5 65.3 64.3 56.3 59.8 66.5 51.3 62.5 62.8 69 ...## $ weight: num 84 98 90 77 84.5 ...## - attr(*, "spec")=## .. cols(## .. .default = col_double(),## .. name = col_character(),## .. sex = col_factor(levels = c("M", "F"), ordered = FALSE, include_na = FALSE),## .. age = col_double(),## .. height = col_double(),## .. weight = col_double()## .. )
其中str()函数可以显示数据框的行数(obs.)和变量数(variables), 以及每个变量(列)的类属等信息。
除了read_csv()函数以外, R扩展包readr还提供了其它的从文本数据读入数据框的函数, 如read_table2(), read_tsv(), read_fwf()等。 这些函数读入的结果保存为tibble。 read_table2()读入用空格作为间隔的文本文件, 同一行的两个数据项之间可以用一个或多个空格分隔, 不需要空格个数相同, 也不需要上下对齐。 read_tsv()读入用制表符分隔的文件。 read_fwf()读入上下对齐的文本文件。
另外, read_lines()函数将文本文件各行读入为一个字符型向量。 read_file()将文件内容读入成一整个字符串, read_file_raw()可以不管文件编码将文件读入为一个二进制字符串。
对特别大的文本格式数据, data.table扩展包的fread()读入速度更快。
readr包的write_excel_csv()函数将tibble保存为csv文件, 总是使用UTF-8编码,结果可以被MS Office读取。
文本格式的文件都不适用于大型数据的读取与保存。 大型数据可以通过数据库接口访问, 可以用R的save()和load()函数按照R的格式访问, 还有一些特殊的针对大数据集的R扩展包。
15.3 Excel表访问
15.3.1 借助于文本格式
为了把Microsoft Excel格式的数据读入到R中, 最容易的办法是在Excel软件中把数据表转存为CSV格式, 然后用read.csv()读取。
为了把R的数据框保存为Excel格式, 只要用write.csv()把数据框保存成CSV格式,然后在Excel中打开即可。 例如,下面的程序演示了write.csv()的使用:
d1 <- tibble("学号"=c("101", "103", "104"), "数学"=c(85, 60, 73), "语文"=c(90, 78, 80))write.csv(d1, file="tmp1.csv", row.names=FALSE)
保存在文件中的结果显示如下:
学号,数学,语文101,85,90103,60,78104,73,80
15.3.2 使用剪贴板
为了把Excel软件中数据表的选中区域读入到R中, 可以借助于剪贴板。 在Excel中复制选中的区域,然后在R中用如
myDF <- read.delim("clipboard")
就可以把选中部分转换成一个R的数据框。 如果复制的区域不含列名, 应加上header=FALSE选项。
这种方法也可以从R中复制数据到在Excel中打开的电子表格中, 例如
write.table(iris, file="clipboard", sep = "\t", col.names = NA)
首先把指定的数据框(这里是iris)写入到了剪贴板, 然后在用Excel软件打开的工作簿中只要粘贴就可以。 上述程序中write.table()函数把指定的数据框写入到指定的文件中, 其中的col.names=NA选项是一个特殊的约定, 这时保存的文件中第一行是列名, 如果有行名的话,行名所在的列对应的列名是空白的(但是存在此项)。
如果从R中复制数据框到打开的Excel文件中时不带行名, 但是带有列名,可以写这样一个通用函数
write.clipboard <- function(df){ write.table(df, file="clipboard", sep='\t', row.names=FALSE)}
15.3.3 利用readxl扩展包
readxl扩展包的readxl()函数利用独立的C和C++库函数读入.xls和.xlsx格式的Excel文件。一般格式为
read_excel(path, sheet = 1, col_names = TRUE, col_types = NULL, na = "", skip = 0)
结果返回读入的表格为一个数据框。 各个自变量为:
-
path: 要读入的Excel文件名,可以是全路径,路径格式要符合所用操作系统要求。 -
sheet: 要读入哪一个工作簿(sheet),可以是整数序号,也可以是工作簿名称的字符串。 -
col_names: 是否用第一行内容作为列名,缺省为是。
col_types
: 可以在读入时人为指定各列的数据类型,缺省时从各列内容自动判断,有可能会不够准确。人为指定时,指定一个对应于各列的字符型向量,元素可取值为:
blank: 自动判断该列;numeric: 数值型;date: 日期;text: 字符型。
15.3.4 利用RODBC访问Excel文件
还可以用RODBC扩展包访问Excel文件。 这样的方法不需要借助于CSV文件这个中间格式。 RODBC是一个通过ODBC协议访问数据文件与数据库的R扩展包。
先给出把R数据框保存为Excel文件的例子。 如下的程序定义了两个数据框:
d1 <- data.frame("学号"=c("101", "103", "104"), "数学"=c(85, 60, 73), "语文"=c(90, 78, 80))d2 <- data.frame("学号"=c("101", "103", "104"), "性别"=c("女", "男", "男"))
在写入到Excel文件时,如果文件已经存在,会导致写入失败。 比如,要写入到testwrite.xls中, 可以用如下程序在文件已存在时先删除文件:
fname <- "testwrite.xls"if(file.exists(fname)) file.remove(fname)
其中file.exsits()检查文件是否已存在, file.remove()删除指定文件。
使用RODBC比较麻烦, 需要先用odbcConnectExcel()函数打开目的文件, 然后可以用sqlSave()函数把数据框保存到目的文件中, 保存完毕后需要用close()函数关闭打开的目的文件。 目前RODBC的odbcConnectExcel()只能在32位版本的R软件中使用, 而且操作系统中必须安装有32位的ODBC驱动程序。 示例如下(需要使用32位R软件且需要操作系统中有32位版本的ODBC驱动程序):
library(RODBC)con <- odbcConnectExcel(fname, readOnly=FALSE)res <- sqlSave(con, d1, tablename="成绩", rownames=F, colnames=F, safer=T)res <- sqlSave(con, d2, tablename="性别", rownames=F, colnames=F, safer=T)close(con)
用odbcConnectExcel2007()可以访问或生成Excel 2007/2010版本的.xlsx文件, 此函数可以用在64位的R软件中, 但是这时需要操作系统中安装有64位的ODBC驱动程序,而不能有32位的ODBC驱动程序。 如果安装了Office软件,Office软件是32位的, 相应的ODBC驱动程序必须也是32位的; Office软件是64位的, 相应的ODBC驱动程序必须也是64位的。
RODBC对Excel文件的支持还有一些其它的缺点, 比如表名不规范, 数据类型自动转换不一定合理等。 在Excel中读入或者保存CSV格式会使得问题变得简单。 大量数据或大量文件的问题就不应该使用Excel来管理了, 一般会使用关系数据库系统, 如Oracle, MySQL等。
为了读入Excel文件内容, 先用odbcConnectExcel()函数打开文件, 用sqlFetch()函数读入一个数据表为R数据框, 读取完毕后用close()关闭打开的文件。 如
require(RODBC)con <- odbcConnectExcel('testwrite.xls')rd1 <- sqlFetch(con, sqtable='成绩')close(con)
读入的表显示如下:
学号 数学 语文1 101 85 902 103 60 783 104 73 80
15.3.5 用RODBC访问Access数据库
RODBC还可以访问其他微机数据库软件的数据库。 假设有Access数据库在文件c:/Friends/birthdays.mdb中, 内有两个表Men和Women, 每个表包含域Year, Month, Day, First Name, Last Name, Death。 域名应尽量避免用空格。
下面的程序把女性记录的表读入为R数据框:
require(RODBC)con <- odbcConnectAccess("c:/Friends/birthdays.mdb")women <- sqlFetch(con, sqtable='Women')close(con)
RODBC还有许多与数据库访问有关的函数, 比如,sqlQuery()函数可以向打开的数据库提交任意符合标准的SQL查询。
15.4 使用专用接口访问数据库
15.4.1 访问Oracle数据库
Oracle是最著名的数据库服务器软件。 要访问的数据库, 可以是安装在本机上的, 也可以是安装在网络上某个服务器中的。 如果是远程访问, 需要在本机安装Oracle的客户端软件。
假设已经在本机安装了Oracle服务器软件, 并设置orcl为本机安装的Oracle数据库软件或客户端软件定义的本地或远程Oracle数据库的标识, test和oracle是此数据库的用户名和密码, testtab是此数据库中的一个表。
为了在R中访问Oracle数据库服务器中的数据库, 在R中需要安装ROracle包。 这是一个源代码扩展包, 需要用户自己编译安装。 在MS Windows环境下, 需要安装R软件和RTools软件包(在CRAN网站的Windows版本软件下载栏目中)。 在MS Windows命令行窗口,用如下命令编译R的ROracle扩展包:
set OCI_LIB32=D:\oracle\product\10.2.0\db_1\binset OCI_INC=D:\oracle\product\10.2.0\db_1\oci\includeset PATH=D:\oracle\product\10.2.0\db_1\bin;C:\Rtools\bin;C:\Rtools\gcc-4.6.3\bin;"%PATH%"C:\R\R-3.2.0\bin\i386\rcmd INSTALL ROracle_1.2-1.tar.gz
其中的前三个set命令设置了Oracle数据库程序或客户端程序链接库、头文件和可执行程序的位置, 第三个set命令还设置了RTools编译器的路径。 这些路径需要根据实际情况修改。 这里的设置是在本机运行的Oracle 10g服务器软件的情况。 最后一个命令编译ROracle扩展包,相应的rcmd程序路径需要改成自己的安装路径。
如果服务器在远程服务器上, 设远程服务器的数据库标识名为ORCL, 本机需要安装客户端Oracle instant client软件, 此客户端软件需要与服务器同版本号, 如instantclient-basic-win32-10.2.0.5.zip, 这个软件不需要安装, 只需要解压到一个目录如 C:\instantclient_10_2中。 在本机(以MS Windows操作系统为例)中, 双击系统,选择高级--环境变量, 增加如下三个环境变量:
NLS_LANG = SIMPLIFIED CHINESE_CHINA.ZHS16GBKORACLE_HOME = C:\instantclient_10_2TNS_ADMIN = C:\instantclient_10_2
并在环境变量PATH的值的末尾增加Oracle客户端软件所在的目录 verb|C:\instantclient_10_2, 并与前面内容用分号分开。
然后,在client所在的目录 C:\instantclient_10_2 中增加如下内容的tnsnames.ora`文件
orcl = (DESCRIPTION = (ADDRESS = (PROTOCOL = TCP)(HOST = 192.168.1.102 ) (PORT = 1521)) (CONNECT_DATA = (SERVER = DEDICATED) (SERVICE_NAME = orcl) ) )
其中HOST的值是安装Oracle服务器的服务器的IP地址, orcl是一个服务器实例名, 能够在服务器端的tnsnames.ora文件中查到, 等号前面的orcl是对数据库给出的客户端别名, 这里就干脆用了和服务器端的数据库标识名相同的名字orcl。
不论是在本机的数据框服务器还是在本机安装设置好客户端后, 在R中用如下的程序可以读入数据库中的表:
require(ROracle)drv <- dbDriver("Oracle")conn <- dbConnect(drv, username="test", password="oracle", dbname="orcl")rs <- dbSendQuery(conn, "select * from testtab")d <- fetch(rs)
可以用dbGetTable()取出一个表并存入R数据框中。 用dbSendQuery()发出一个SQL命令, 用fetch()可以一次性取回或者分批取回, 在表行数很多时这种方法更适用。
15.4.2 MySQL数据库访问
MySQL是高效、免费的数据库服务器软件, 在很多行业尤其是互联网行业占有很大的市场。 为了在R中访问MySQL数据库, 只要安装RMySQL扩展包(有二进制版本)。 假设服务器地址在 192.168.1.111, 可访问的数据库名为 world, 用户为 test, 密码为 mysql。 设world库中有表country。
在R中要访问MySQL数据框,首先要建立与数据库服务器的连接:
con <- dbConnect(RMySQL::MySQL(), dbname='world', username='test', password='mysql', host='192.168.1.111')
下列代码列出world库中的所有表, 然后列出其中的country表的所有变量:
dbListTables(con)dbListFields(con, 'country')
下列代码取出country表并存入R数据框d.country中:
d.country <- dbReadTable(con, 'country')
下列代码把R中的示例数据框USArrests写入MySQL库world的表arrests中:
data(USArrests)dbWriteTable(con, 'arrests', USArrests, overwrite=TRUE)
当然,这需要用户对该库有写权限。
可以用dbGetQuery()执行一个SQL查询并返回结果,如
dbGetQuery(con, 'select count(*) from arrests')
当表很大时,可以用dbSendQuery()发送一个SQL命令, 返回一个查询结果指针对象, 用dbFetch()从指针对象位置读取指定行数, 用dbHasCompleted()判断是否已读取结束。如
res <- dbSendQuery(con, "SELECT * FROM country")while(!dbHasCompleted(res)){ chunk <- dbFetch(res, n = 5) print(chunk[,1:2])}dbClearResult(res)
数据库使用完毕时, 需要关闭用dbConnect()打开的连接:
dbDisconnect(con)
15.5 文件访问
15.5.1 连接
输入输出可以针对命令行,针对文件,R支持扩展的文件类型, 称为"连接(connection)"。
函数file()生成到一个普通文件的连接, 函数url()生成一个到指定的URL的连接, 函数gzfile, bzfile, xzfile, unz支持对 压缩过的文件的访问(不是压缩包,只对一个文件压缩)。这些函数大概的用法如下:
file("path", open="", blocking=T, encoding = getOption("encoding"), raw = FALSE)url(description, open = "", blocking = TRUE, encoding = getOption("encoding"))textConnection(description, open="r", local = FALSE, encoding = c("", "bytes", "UTF-8"))gzfile(description, open = "", encoding = getOption("encoding"), compression = 6)bzfile(description, open = "", encoding = getOption("encoding"), compression = 9)xzfile(description, open = "", encoding = getOption("encoding"), compression = 6)unz(description, filename, open = "", encoding = getOption("encoding"))
生成连接的函数不自动打开连接。 给定一个未打开的连接, 读取函数从中读取时会自动打开连接, 函数结束时自动关闭连接。 用open()函数打开连接,返回一个句柄; 生成连接时可以用open参数要求打开连接。 要多次从一个连接读取时就应该先打开连接, 读取完毕用close函数关闭。
函数textConnection()打开一个字符串用于读写。
在生成连接与打开连接的函数中用open参数指定打开方式, 取值为:
r---文本型只读;w---文本型只写;a---文本型末尾添加;rb---二进制只读;wb---二进制只写;ab---二进制末尾添加;r+或r+b---允许读和写;w+或w+b---允许读和写,但刚打开时清空文件;a+或a+b---末尾添加并允许读。
15.5.2 文本文件访问
函数readLines(), scan()可以从一个文本型连接读取。
给定一个打开的连接con, 用readLines函数可以把文件各行读入为 字符型向量的各个元素,不包含文件中用来分开各行的换行标志。 可以指定要读的行数。 如
ll <- readLines(file('class.csv'))print(ll)
用writeLines函数可以把一个字符型向量各元素作为不同行写入一个文本型连接。如
vnames <- strsplit(ll, ',')[[1]]writeLines(vnames, con='class-names.txt')
其中的con参数应该是一个打开的文本型写入连接, 但是可以直接给出一个要写入的文件名。
15.5.3 二进制文件访问
函数save用来保存R变量到文件, 函数load用来从文件中读取保存的R变量。
函数readBin和writeBin对R变量进行二进制文件存取。
如果要访问其它软件系统的二进制文件, 请参考R手册中的"R Data Import/Export Manual"。
15.5.4 字符型连接
函数textConnection打开一个字符串用于读取或写入, 是很好用的一个R功能。 可以把一个小文件存放在一个长字符串中, 然后用textConnection读取,如
fstr <-"name,score王芳,78孙莉,85张聪,80"d <- read.csv(textConnection(fstr), header=T)print(d)
读取用的textConnection的参数是一个字符型变量。
在整理输出结果时,经常可以向一个字符型变量连接写入, 最后再输出整个字符串值。 例如:
tc <- textConnection("sres", open="w")cat('Trial of text connection.\n', file=tc)cat(1:10, '\n', file=tc, append=T)close(tc)print(sres)
注意写入用的textConnection 的第一个参数是保存了将要写入的字符型变量名的字符串, 而不是变量名本身, 第二个参数表明是写入操作, 使用完毕需要用close关闭。
15.6 中文编码问题
读写文本格式的数据, 或者用readLines()、readr::read_lines()读写文本文件, 可能会遇到中文编码不匹配的问题。 这里总结一些常用解决方法, 所用的操作系统为中文Windows10, 在RStudio中运行,R版本为3.4.3。 常见的中文编码有GBK(或GB18030, GB), UTF-8, UTF-8有BOM标志等。
可以用iconvlist()查看R支持的编码名称。
假设有如下的含有中文的文件:
序号,收缩压1,1455,1106, 未测9,15010, 拒绝15,115
这个文件是在中文版MS Office的Excel软件中输入后, 用Office的"文件------另存为------.csv格式"生成的, 结果的编码是GBK编码, 或GB18030编码。 文件下载: bp.csv
我们用工具软件将其转换成UTF-8无BOM格式,下载链接: bp-utf8nobom.csv
转为UTF-8有BOM格式,下载链接: bp-utf8bom.csv
15.6.1 用基本R的读取函数读取
与所用操作系统默认编码相同的文本文件, R基本软件的read.csv()、read.table()、readLines()函数都可以正常读取, 所以bp.csv文件可以正常读取,如
read.csv("bp.csv")## 序号 收缩压## 1 1 145## 2 5 110## 3 6 未测## 4 9 150## 5 10 拒绝## 6 15 115readLines("bp.csv")## [1] "序号,收缩压" "1,145" "5,110" "6, 未测" "9,150" ## [6] "10, 拒绝" "15,115"
但是另外两个以UTF-8编码的文件则不能正确读入:
read.csv("bp-utf8nobom.csv")## Error in make.names(col.names, unique = TRUE) : invalid multibyte string 2readLines("bp-utf8bom.csv")## [1] "锘垮簭鍙\xb7,鏀剁缉鍘\x8b" "1,145" ## [3] "5,110" "6, 鏈祴" ## [5] "9,150" "10, 鎷掔粷" ## [7] "15,115"
读取UTF-8编码无BOM的文件时, 在read.csv()和read.table()等函数中加fileEncoding="UTF-8"选项可以纠正编码问题:
read.csv("bp-utf8nobom.csv", fileEncoding="UTF-8")## 序号 收缩压## 1 1 145## 2 5 110## 3 6 未测## 4 9 150## 5 10 拒绝## 6 15 115
读取UTF-8编码无BOM或者有BOM的文件时, 在readLines()函数中加encoding="UTF-8"选项可以纠正编码问题:
readLines("bp-utf8nobom.csv", encoding="UTF-8")## [1] "序号,收缩压" "1,145" "5,110" "6, 未测" "9,150" ## [6] "10, 拒绝" "15,115"readLines("bp-utf8bom.csv", encoding="UTF-8")## [1] "<U+FEFF>序号,收缩压" "1,145" "5,110" "6, 未测" "9,150" ## [6] "10, 拒绝" "15,115"
但是,UTF-8有BOM标志的文本文件不能被read.csv()识别:
read.csv("bp-utf8bom.csv", fileEncoding="UTF-8")## invalid input found on input connection 'bp-utf8bom.csv'## incomplete final line found by readTableHeader on 'bp-utf8bom.csv'
15.6.2 用readr包读取
readr包的read_csv()、read_table2()、read_lines()函数默认从UTF-8编码的文件中读取, 无BOM或者有BOM都可以。 如:
read_csv("bp-utf8nobom.csv")## Parsed with column specification:## cols(## 序号 = col_double(),## 收缩压 = col_character()## )## # A tibble: 6 x 2## 序号 收缩压## <dbl> <chr> ## 1 1 145 ## 2 5 110 ## 3 6 未测 ## 4 9 150 ## 5 10 拒绝 ## 6 15 115read_csv("bp-utf8bom.csv")## Parsed with column specification:## cols(## 序号 = col_double(),## 收缩压 = col_character()## )## # A tibble: 6 x 2## 序号 收缩压## <dbl> <chr> ## 1 1 145 ## 2 5 110 ## 3 6 未测 ## 4 9 150 ## 5 10 拒绝 ## 6 15 115read_lines("bp-utf8nobom.csv")## [1] "序号,收缩压" "1,145" "5,110" "6, 未测" "9,150" ## [6] "10, 拒绝" "15,115"read_lines("bp-utf8bom.csv")## [1] "序号,收缩压" "1,145" "5,110" "6, 未测" "9,150" ## [6] "10, 拒绝" "15,115"
但是,对GBK编码的文件,不能直接读取:
read_csv("bp.csv")read_lines("bp.csv")## [1] "<d0><f2><U+00BA><c5>,<ca><d5><cb><f5><U+0479>" "1,145" ## [3] "5,110" "6, δ<U+00B2><e2>" ## [5] "9,150" "10, <U+00BE><U+073E><f8>" ## [7] "15,115"
为了读取GBK(或GB18030)编码的文件, 在read_csv()和read_lines()函数中加入 locale=locale(encoding="GBK")选项:
read_csv("bp.csv", locale=locale(encoding="GBK"))## Parsed with column specification:## cols(## 序号 = col_double(),## 收缩压 = col_character()## )## # A tibble: 6 x 2## 序号 收缩压## <dbl> <chr> ## 1 1 145 ## 2 5 110 ## 3 6 未测 ## 4 9 150 ## 5 10 拒绝 ## 6 15 115read_lines("bp.csv", locale=locale(encoding="GBK"))## [1] "序号,收缩压" "1,145" "5,110" "6, 未测" "9,150" ## [6] "10, 拒绝" "15,115"
15.6.3 输出文件的编码
write.csv()、writeLines()生成的含有中文的文件的编码默认为操作系统的默认中文编码, 这里是GB18030。
readr的write_csv()、write_lines()函数生成的含有中文的文件的编码默认UTF-8无BOM。 如
write_csv(tibble("姓名"=c("张三", "李四")), "tmp.csv")
结果生成的文件编码为UTF-8无BOM, 这样的文件可以被R的readr::read_csv()正确读取, 但是不能被MS Excel软件正确读取。
write_lines()输出的文件也是编码为UTF-8无BOM。
write_excel_csv()可以生成带有UTF-8有BOM的CSV文件, 这样的文件可以被MS Office正确识别:
write_excel_csv(tibble("姓名"=c("张三", "李四")), "tmp2.csv")
15.6.4 分批读写
readLines()、readr::read_lines()、 writeLines()、readr::writeLines()支持分批读写。 这需要预先打开要读取和写入的文件, 所有内容都处理一遍以后关闭读取和写入的文件。
使用file()函数打开文件用于读写, 使用close()函数关闭打开的文件。 打开文件时可以用encoding=指定编码, 但是readr::read_lines()不支持分批读入。
下面的程序每次从UTF-8无BOM编码的bp-utf8nobom.csv读入两行, 不加处理第写入tmp.csv中, 使用readLines()和writeLines(), 读入时用encoding="UTF-8"指定编码, 写出时不指定编码,结果是操作系统默认的GBK:
fin <- file("bp-utf8nobom.csv", "rt", encoding="UTF-8")fout <- file("tmp.csv", "wt")repeat{ lines <- readLines(fin, n=2) print(lines) if(length(lines)==0) break writeLines(lines, fout)}close(fout)close(fin)## [1] "序号,收缩压" "1,145" ## [1] "5,110" "6, 未测"## [1] "9,150" "10, 拒绝"## [1] "15,115"## character(0)
file()中的encoding="UTF-8"特指UTF-8无BOM的格式, 有BOM的UTF-8编码文件无法用上述方法打开。
上面的例子生成的结果tmp.csv使用了中文Windows系统的默认编码GBK编码。 为了生成UTF-8无BOM的结果, 可以在上述程序中打开输出文件时加选项encoding="UTF-8"。 即
fin <- file("bp-utf8nobom.csv", "rt", encoding="UTF-8")fout <- file("tmp.csv", "wt", encoding="UTF-8")...
readr::read_lines()不支持从一个文件分批读入。 readr::write_lines()可以用append=TRUE选项向一个文件分批写出。
15.7 目录和文件管理
目录和文件管理函数:
getwd()---返回当前工作目录。setwd(path)---设置当前工作目录。list.files()或dir()---查看目录中内容。list.files(pattern='.*[.]r$')可以列出所有以".r"结尾的文件。file.path()---把目录和文件名组合得到文件路径。file.info(filenames)---显示文件的详细信息。file.exists()---查看文件是否存在。file.access()---考察文件的访问权限。create.dir()---新建目录。file.create()---生成文件。file.remove()或unlink()---删除文件。unlink()可以删除目录。file.rename()---为文件改名。file.append()---把两个文件相连。file.copy()---复制文件。basename()和dirname()--- 从一个全路径文件名获取文件名和目录。
16 程序控制结构
16.1 表达式
R是一个表达式语言, 其任何一个语句都可以看成是一个表达式。 表达式之间以分号分隔或用换行分隔。 表达式可以续行, 只要前一行不是完整表达式(比如末尾是加减乘除等运算符, 或有未配对的括号)则下一行为上一行的继续。 若干个表达式可以放在一起组成一个复合表达式, 作为一个表达式使用,复合表达式的值为最后一个表达式的值, 组合用大括号表示, 如:
{ x <- 15 x}
16.2 分支结构
分支结构包括if结构:
if (条件) 表达式1
或
if (条件) 表达式1 else 表达式2
其中的"条件"为一个标量的真或假值, 表达式可以是用大括号包围的复合表达式。 如
if(is.na(lambda)) lambda <- 0.5
又如
if(x>1) { y <- 2.5} else { y <- -y}
多个分支,可以在中间增加else if,如:
x <- c(0.05, 0.6, 0.3, 0.9)for(i in seq(along=x)){ if(x[i] <= 0.2){ cat("Small\n") } else if(x[i] <= 0.8){ cat("Medium\n") } else { cat("Large\n") }}
16.2.1 用逻辑下标代替分支结构
R是向量化语言,尽可能少用标量运算。 比如,x为一个向量,要定义y与x等长, 且y的每一个元素当且仅当x的对应元素为正数时等于1, 否则等于零。
这样是错误的:
if(x>0) y <- 1 else y <- 0
正解为:
y <- numeric(length(x))y[x>0] <- 1y
函数ifelse()可以根据一个逻辑向量中的多个条件, 分别选择不同结果。如
x <- c(-2, 0, 1)y <- ifelse(x >=0, 1, 0); print(y)## [1] 0 1 1
函数switch()可以建立多分枝结构。
16.3 循环结构
16.3.1 计数循环
为了对向量每个元素、矩阵每行、矩阵每列循环处理,语法为
for(循环变量 in 序列) 语句
其中的语句一般是复合语句。 如:
x <- rnorm(5)y <- numeric(length(x))for(i in 1:5){ if(x[i]>=0) y[i] <- 1 else y[i] <- 0}print(y)## [1] 1 1 1 0 1
其中rnorm(5)会生成5个标准正态分布随机数。 numeric(n)生成有n个0的数值型向量(基础类型为double)。
如果需要对某个向量x按照下标循环, 获得所有下标序列的标准写法是seq(along=x), 而不用1:n的写法, 因为在特殊情况下n可能等于零,这会导致错误下标, 而seq(along=x)在x长度为零时返回零长度的下标。
例如,设序列满足, , 求:
x <- 0.0s <- 0n <- 5for(i in 1:n){ x <- 2*x + 1 s <- s + x}print(s)## [1] 57
在R中应尽量避免for循环: 其速度比向量化版本慢一个数量级以上, 而且写出的程序不够典雅。 比如,前面那个示性函数例子实际上可以简单地写成
x <- rnorm(5)y <- ifelse(x >= 0, 1, 0)print(y)## [1] 0 0 1 0 0
16.3.2 while循环和repeat循环
用
while(循环继续条件) 语句
进行当型循环。 其中的语句一般是复合语句。 仅当条件成立时才继续循环, 而且如果第一次条件就已经不成立就一次也不执行循环内的语句。
用
repeat 语句
进行无条件循环(一般在循环体内用if与break退出)。 其中的语句一般是复合语句。 如下的写法可以制作一个直到型循环:
repeat{ ... if(循环退出条件) break}
直到型循环至少执行一次, 每次先执行...代表的循环体语句, 然后判断是否满足循环退出条件, 满足条件就退出循环。
用break语句退出所在的循环。 用next语句进入所在循环的下一轮。
例如, 常量的值可以用泰勒展开式表示为
R函数exp(1)``e``e
e0 <- exp(1.0)s <- 1.0x <- 1k <- 0repeat{ k <- k+1 x <- x/k s <- s + x if(x < .Machine$double.eps) break}err <- s - e0cat("k=", k, " s=", s, " e=", e0, " 误差=", err, "\n")## k= 18 s= 2.718282 e= 2.718282 误差= 4.440892e-16
其中.Machine$double.eps称为机器, 是最小的加1之后可以使得结果大于1的正双精度数, 小于此数的正双精度数加1结果还等于1。 用泰勒展开公式计算的结果与exp(1)得到的结果误差在左右。
16.4 R中判断条件
if语句和while语句中用到条件。 条件必须是标量值, 而且必须为TRUE或FALSE, 不能为NA或零长度。 这是R编程时比较容易出错的地方。
16.5 管道控制
数据处理中经常会对同一个变量(特别是数据框)进行多个步骤的操作, 比如,先筛选部分有用的变量,再定义若干新变量,再排序。 R的magrittr包提供了一个%>%运算符实现这样的操作流程。 比如,变量x先用函数f(x)进行变换,再用函数g(x)进行变换, 一般应该写成g(f(x)),用%>%运算符,可以表示成 x %>% f() %>% g()。 更多的处理,如h(g(f(x)))可以写成 x %>% f() %>% g() %>% h()。 这样的表达更符合处理发生的次序,而且插入一个处理步骤也很容易。
处理用的函数也可以带有其它自变量,在管道控制中不要写第一个自变量。 某个处理函数仅有一个自变量时,可以省略空的括号。
tibble类型的数据框尤其适用于如此的管道操作。
将管道控制开始变量设置为.,可以定义一个函数。
magrittr包定义了%T%运算符, x %T% f()返回x本身而不是用f()修改后的返回值f(x), 这在中间步骤需要显示或者绘图但是需要进一步对输入数据进行处理时有用。
magrittr包定义了%$%运算符, 此运算符的作用是将左运算元的各个变量(这时左运算元是数据框或列表)暴露出来, 可以直接在右边调用其中的变量,类似于with()函数的作用。
magrittr包定义了%<>%运算符, 用在管道链的第一个连接, 可以将处理结果存入最开始的变量中, 类似于C语言的+=运算符。
如果一个操作是给变量加b,可以写成add(b), 给变量乘b,可以写成multiply_by(b)。
17 函数
17.1 函数基础
17.1.1 介绍
在现代的编程语言中使用自定义函数, 优点是代码复用、模块化设计。
在编程时, 把编程任务分解成小的模块,每个模块用一个函数实现, 可以降低复杂性,防止变量混杂。
函数的自变量是只读的, 函数中定义的局部变量只在函数运行时起作用, 不会与外部或其它函数中同名变量混杂。
函数返回一个对象作为输出, 如果需要返回多个变量, 可以用列表进行包装。
17.1.2 函数定义
函数定义使用function关键字,一般格式为
函数名
<-function(形式参数表) 函数体
函数体是一个表达式或复合表达式(复合语句), 以复合表达式中最后一个表达式为返回值, 也可以用return(x)返回x的值。 如果函数需要返回多个结果, 可以打包在一个列表(list)中返回。 形式参数表相当于函数自变量,可以是空的, 形式参数可以有缺省值, R的函数在调用时都可以用"形式参数名=实际参数"的格式输入自变量值。
下面的例子没有参数,仅画一个示例图:
f <- function() { x <- seq(0, 2*pi, length=50) y1 <- sin(x) y2 <- cos(x) plot(x, y1, type='l', lwd=2, col='red', xlab='x', ylab='') lines(x, y2, lwd=2, col='blue') abline(h=0, col='gray')}f()
注意此自定义函数虽然没有参数, 但是在定义与调用时都不能省略圆括号。
自定义函数也可以是简单的一元函数, 与数学中一元函数基本相同,例如
f <- function(x) 1/sqrt(1 + x^2)
基本与数学函数相对应。 定义中的自变量x叫做形式参数或形参(formal arguments)。 函数调用时,形式参数得到实际值,叫做实参(actual arguments)。 R函数有一个向量化的好处, 在上述函数调用时,如果形式参数x的实参是一个向量, 则结果也是向量,结果元素为实参向量中对应元素的变换值。 如
f(0)## [1] 1f(c(-1, 0, 1, 2))## [1] 0.7071068 1.0000000 0.7071068 0.4472136
第一次调用时,形式参数x得到实参0, 第二次调用时,形式参数x得到向量实参c(-1, 0, 1, 2)。
函数实参是向量时, 函数体中也可以计算对向量元素进行汇总统计的结果。 例如,设是一个总体的简单随机样本, 其样本偏度统计量定义如下:
其中
f <- function(x) { n <- length(x) xbar <- mean(x) S <- sd(x) n/(n-1)/(n-2)*sum( (x - xbar)^3 ) / S^3}
函数体的最后一个表达式是函数返回值。
在函数体最后一个表达式中巧妙地利用了R的向量化运算 ((x - xbar)^3)与内建函数(sum)。 这比用for循环计算效率高得多, 计算速度相差几十倍。
请比较如下两个表达式:
n/(n-1)/(n-2)*sum( (x - xbar)^3 ) / S^3 n/(n-1)/(n-2)*sum( ((x - xbar)/S)^3 )
这两个表达式的值相同。 表面上看,第二个表达式更贴近原始数学公式, 但是在编程时, 需要考虑计算效率问题, 第一个表达式关于只需要除一次, 而第二个表达关于除了次, 所以第一个表达式效率更高。
函数定义中的形式参数可以有多个, 还可以指定缺省值。 例如
fsub <- function(x, y=0){ cat("x=", x, " y=", y, "\n") x - y}
这里x, y是形式参数, 其中y指定了缺省值为0, 有缺省值的形式参数在调用时可以省略对应的实参, 省略时取缺省值。
实际上, "function(参数表) 函数体"这样的结构本身也是一个表达式, 其结果是一个函数对象。 在通常的函数定义中, 函数名只不过是被赋值为某个函数对象, 或者说是"绑定"(bind)到某个函数对象上面。 R允许使用没有函数名的函数对象。
因为函数也是R对象, 也可以拥有属性。 所谓对象, 就是R的变量所指向的各种不同类型的统称。
一个自定义R函数由三个部分组成: 函数体body(),即要函数定义内部要执行的代码; formals(),即函数的形式参数表以及可能存在的缺省值; environment(),是函数定义时所处的环境, 这会影响到参数表中缺省值与函数体中非局部变量的的查找。 注意,函数名并不是函数对象的必要组成部分。 如
body(fsub)## {## cat("x=", x, " y=", y, "\n")## x - y## }formals(fsub)## $x## ## ## $y## [1] 0environment(fsub)## <environment: R_GlobalEnv>
"环境"是R语言比较复杂的概念, 后面再详细解释。
17.1.3 函数调用
函数调用时最基本的调用方式是把实参与形式参数按位置对准, 这与我们在数学中使用多元函数的习惯类似。 例如
fsub(3, 1)## x= 3 y= 1## [1] 2
相当于以x=3, y=1调用。
调用时可选参数可以省略实参,如
fsub(3)## x= 3 y= 0## [1] 3
相当于以x=3, y=0调用。
R函数调用时全部或部分形参对应的实参可以用"形式参数名=实参"的格式给出, 这样格式给出的实参不用考虑次序, 不带形式参数名的则按先后位置对准。 如
fsub(x=3, y=1)## x= 3 y= 1## [1] 2fsub(y=1, x=3)## x= 3 y= 1## [1] 2fsub(x=3)## x= 3 y= 0## [1] 3fsub(3, y=1)## x= 3 y= 1## [1] 2fsub(1, x=3)## x= 3 y= 1## [1] 2fsub(x=3, 1)## x= 3 y= 1## [1] 2
注意作为好的程序习惯应该避免fsub(x=3, 1)这样的做法。 虽然R的语法没有强行要求, 调用R函数时, 如果既有按位置对应的参数又有带名参数, 按位置对应的参数都写在前面, 带名参数写在后面, 不遵守这样的约定容易使得程序被误读。
R的形参、实参对应关系可以写成一个列表, 如fsub(3, y=1)中的对应关系可以写成列表 list(3, y=1), 如果调用函数的形参、实参对应关系保存在列表中, 可以用函数do.call()来表示函数调用,如
do.call(fsub, list(3, y=1))
与
fsub(3, y=1)
效果相同。
在自定义R函数的形参中, 还允许有一个特殊的...形参(三个小数点)。 在函数调用时,所有没有形参与之匹配的实参, 不论是带有名字还是不带有名字的, 都自动归入这个参数, 这个参数的类型是一个列表。 虽然很奇怪, 这个语法在R里面是常用的, 通常用来把函数内调用的其它函数的实参传递进来。
例如,sapply(X, FUN, ...)中的形式参数FUN需要函数实参, 此函数有可能需要更多的参数。 例如,为了把1:5的每个元素都减去2,可以写成
sapply(1:5, fsub, y=2)## x= 1 y= 2 ## x= 2 y= 2 ## x= 3 y= 2 ## x= 4 y= 2 ## x= 5 y= 2## [1] -1 0 1 2 3
或
sapply(1:5, fsub, 2)## x= 1 y= 2 ## x= 2 y= 2 ## x= 3 y= 2 ## x= 4 y= 2 ## x= 5 y= 2## [1] -1 0 1 2 3
实际上,R语法中的大多数运算符如+, -, *, /, [, [[, (, {等都是函数。 这些特殊名字的函数要作为函数使用,需要使用反向单撇号`包围,比如
1 + 2## [1] 3`+`(1, 2)## [1] 3
效果相同。
这样,为了给1:5每个元素减去2,还可以写成
sapply(1:5, `-`, 2)## [1] -1 0 1 2 3
或
sapply(1:5, "-", 2)## [1] -1 0 1 2 3
在后一写法中sapply的第二参数用了函数名字符串作为实参。
17.2 变量作用域
17.2.1 全局变量和工作空间
在所有函数外面(如R命令行)定义的变量是全局变量。 在命令行定义的所有变量都保存在工作空间 (workspace)中。 用ls()查看工作空间内容。 ls()中加上pattern选项可以指定只显示符合一定命名模式的变量,如
ls(pattern='^tmp[.]')
显示所有以tmp.开头的变量。 用object.size()函数查看变量占用存储大小。
因为R的函数调用时可以读取工作空间中的全局变量值, 工作空间中过多的变量会引起莫名其妙的程序错误。 用rm()函数删除指定的变量。 rm()中还可以用list参数指定一个要删除的变量名表。如
rm(list=ls(pattern='^tmp[.]'))
用save()函数保存工作空间中选择的某些变量; 用load()函数载入保存在文件中的变量。 如
save(my.large.data, file='my-large-data.RData')load('my-large-data.RData')
实际上,R的工作空间是R的变量搜索路径中的一层, 大体相当于全局变量空间。 R的已启用的软件包中的变量以及用命令引入的变量也在这个搜索路径中。
17.2.2 局部变量
在计算机语言中, "变量"实际是计算机内存中的一段存储空间。 函数的参数(自变量)在定义时并没有对应的存储空间, 所以也称函数定义中的参数为"形式参数"。
函数的形式参数在调用时被赋值为实参值(这是一般情形), 形参变量和函数体内被赋值的变量都是局部的。 这一点符合函数式编程(functional programming)的要求。 所谓局部变量, 就是仅在函数运行时才存在, 一旦退出函数就不存在的变量。
17.2.2.1 自变量的局部性
在函数被调用时, 形式参数(自变量)被赋值为实际的值(称为实参), 如果实参是变量,形式参数实际变成了实参的一个副本, 在函数内部对形式参数作任何修改在函数运行完成后都不影响原来的实参变量, 而且函数运行完毕后形式参数对应的变量不再存在。
在下例中, 在命令行定义了全局变量xv, xl, 然后作为函数f()的自变量值(实参)输入到函数中, 函数中对两个形式参数作了修改, 函数结束后实参变量xv, xl并未被修改,形参变量也消失了。 例子程序如下:
xv <- c(1,2,3)xl <- list(a=11:15, b='James')if(exists("x")) rm(x)f <- function(x, y){ cat('输入的 x=', x, '\n') x[2] <- -1 cat('函数中修改后的 x=', x, '\n') cat('输入的y为:\n'); print(y) y[[2]] <- 'Mary' cat('函数中修改过的y为:\n'); print(y)}f(xv, xl)## 输入的 x= 1 2 3 ## 函数中修改后的 x= 1 -1 3 ## 输入的y为:## $a## [1] 11 12 13 14 15## ## $b## [1] "James"## ## 函数中修改过的y为:## $a## [1] 11 12 13 14 15## ## $b## [1] "Mary"## cat('函数运行完毕后原来变量xv不变:', xv, '\n')## 函数运行完毕后原来变量xv不变: 1 2 3 cat('函数运行完毕后原来变量xl不变::\n'); print(xl)## 函数运行完毕后原来变量xl不变::## $a## [1] 11 12 13 14 15## ## $b## [1] "James"## cat('函数运行完毕后形式参数x不存在::\n'); print(x)## 函数运行完毕后形式参数x不存在::## Error in print(x) : object 'x' not found
R语言的这种特点对于传递超大的数据是不利的, 所以R中会容纳超大数据的类型往往涉及成修改副本时不占用不必要的额外存储空间, 比如,tibble类型就有这样的特点。
17.2.2.2 修改自变量
为了修改某个自变量, 在函数内修改其值并将其作为函数返回值, 赋值给原变量。
比如定义了如下函数:
f <- function(x, inc=1){ x <- x + inc x}
调用如
x <- 100cat('原始 x=', x, '\n')## 原始 x= 100x <- f(x)cat('修改后 x=', x, '\n')## 修改后 x= 101
17.2.2.3 函数内的局部变量
在函数内部用赋值定义的变量都是局部变量, 即使在工作空间中有同名的变量, 此变量在函数内部被赋值时就变成了局部变量, 原来的全局变量不能被修改。 局部变量在函数运行结束后就会消失。 如
if('x' %in% ls()) rm(x)f <- function(){ x <- 123 cat('函数内:x = ', x, '\n')}f()cat('函数运行完毕后:x=', x, '\n')## 函数内:x = 123 > cat('函数运行完毕后:x=', x, '\n')## Error in cat("函数运行完毕后:x=", x, "\n") : object 'x' not found
再比如, 下面的函数试图知道自己被调用了多少次, 但是因为每次函数调用完毕局部变量就消失, 这样的程序不能达到目的:
f <- function(){ if(!exists("runTimes")){ runTimes <- 1 } else { runTimes <- runTimes + 1 } print(runTimes)}f()## [1] 1f()## [1] 1
这个问题可以用R的closure来解决。
17.2.3 在函数内访问全局变量
函数内部可以读取全局变量的值,但一般不能修改全局变量的值。 在现代编程指导思想中, 全局变量容易造成不易察觉的错误, 应谨慎使用, 当然,也不是禁止使用, 有些应用中不使用全局变量会使得程序更复杂且低效。
在下面的例子中, 在命令行定义了全局变量x.g, 在函数f()读取了全局变量的值, 但是在函数内给这样的变量赋值, 结果得到的变量就变成了局部变量, 全局变量本身不被修改:
x.g <- 9999f <- function(x){ cat('函数内读取:全局变量 x.g = ', x.g, '\n') x.g <- -1 cat('函数内对与全局变量同名的变量赋值: x.g = ', x.g, '\n')}f()## 函数内读取:全局变量 x.g = 9999 ## 函数内对与全局变量同名的变量赋值: x.g = -1cat('退出函数后原来的全局变量不变: x.g =', x.g, '\n')## 退出函数后原来的全局变量不变: x.g = 9999
在函数内部如果要修改全局变量的值,用 <<-代替<-进行赋值。如
x.g <- 9999f <- function(x){ cat('函数内读取:全局变量 x.g = ', x.g, '\n') x.g <<- -1 cat('函数内用"<<-"对全局变量变量赋值: x.g = ', x.g, '\n')}f()## 函数内读取:全局变量 x.g = 9999 ## 函数内用"<<-"对全局变量变量赋值: x.g = -1cat('退出函数后原来的全局变量被修改了: x.g =', x.g, '\n')## 退出函数后原来的全局变量被修改了: x.g = -1
后面将进一步解释函数在嵌套定义时<<-的不同含义。
17.3 函数进阶
17.3.1 嵌套定义与句法作用域(lexical scoping)
R语言允许在函数体内定义函数。 比如,
x <- -1f0 <- function(x){ f1 <- function(){ x + 100 } f1()}
其中内嵌的函数f1()称为一个closure(闭包)。
内嵌的函数体内在读取某个变量值时, 如果此变量在函数体内还没有被赋值, 它就不是局部的, 会向定义的外面一层查找, 外层一层找不到,就继续向外查找。 上面例子f1()定义中的变量x不是局部变量, 就向外一层查找, 找到的会是f0的自变量x,而不是全局空间中x。 如
f0(1)## [1] 101
最后x+100中x取的是f0的实参值x=1, 而不是全局变量x=-1。
这样的变量查找规则叫做句法作用域(lexical scoping), 即函数运行时查找变量时, 从其定义时的环境向外层逐层查找, 而不是在运行时的环境中查找。 句法作用域指的是可能有多个同名变量时查找变量按照定义时的环境查找, 不是指查找变量值的规则。
例如,
f0 <- function(){ f1 <- function(){ x <- -1 f2 <- function(){ x + 100 } f2() } x <- 1000 f1()}f0()## [1] 99
其中f2()运行时, 用到的x是f1()函数体内的局部变量x=-1, 而不是被调用时f0()函数体内的局部变量x=1000, 所以结果是-1 + 100 = 99。
"句法作用域"指的是函数调用时查找变量是查找其定义时的变量对应的存储空间, 而不是定义时变量所取的历史值。 函数运行时在找到某个变量对应的存储空间后, 会使用该变量的当前值,而不是函数定义的时候该变量的历史值。 例如
f0 <- function(){ x <- -1 f1 <- function(){ x + 100 } x <- 1000 f1()}f0()## [1] 1100
结果为什么不是-1 + 100 = 99而是1000 + 100 = 1100? 这是因为, f1()在调用时, 使用的x是f0函数体内局部变量x的值, 但是要注意的是程序运行时会访问该变量的当前值,即1000, 而不是函数定义的时候x的历史值-1。 这个规则叫做"动态查找"(dynamic lookup), 句法作用域与动态查找一个说的是如何查找某个变量对应的存储空间, 一个说的是使用该存储空间何时的存储值, 程序运行时两个规则需要联合使用。
句法作用域不仅适用于查找变量, 也适用于函数体内调用别的函数时查找函数。 查找函数的规则与查找变量规则相同。
17.3.1.1 辅助嵌套函数
有时内嵌函数仅仅是函数内用来实现模块化的一种工具, 和正常的函数作用相同,没有任何特殊作用。 例如,如下的程序在自变量x中输入一元二次方程的三个系数, 输出解:
solve.sqe <- function(x){ fd <- function(a, b, c) b^2 - 4*a*c d <- fd(x[1], x[2], x[3]) if(d >= 0){ return( (-x[2] + c(1,-1)*sqrt(d))/(2*x[1]) ) } else { return( complex(real=-x[2], imag=c(1,-1)*sqrt(-d))/(2*x[1]) ) }}
在这个函数中内嵌的函数fd仅起到一个计算二次判别式公式的作用, 没有用到任何的闭包特性, 其中的形参变量a, b, c都是局部变量。 运行如
solve.sqe(c(1, -2, 1))## [1] 1 1solve.sqe(c(1, -2, 0))## [1] 2 0solve.sqe(c(1, -2, 2))## [1] 1+1i 1-1i
17.3.1.2 泛函
许多函数需要用函数作为参数,称这样的函数为泛函 。 比如,apply类函数。 这样的函数具有很好的通用性, 因为需要进行的操作可以输入一个函数来规定, 输入的函数规定什么样的操作,
用户可以自定义这样的函数。 比如,希望对一个数据框中所有的数值型变量计算某种统计量, 用来计算统计量的函数作为参数输入:
summary.df.numeric <- function(df, FUN, ...){ vn <- names(df) vn <- vn[vapply(df, is.numeric, TRUE)] if(length(vn) > 0){ sapply(df[,vn, drop=FALSE], FUN, ...) } else { numeric(0) }}
这里参数FUN是用来计算统计量的函数。 例如对d.class中每个数值型变量计算最小值:
d.class <- readr::read_csv("class.csv")## Parsed with column specification:## cols(## name = col_character(),## sex = col_character(),## age = col_double(),## height = col_double(),## weight = col_double()## )summary.df.numeric(d.class, min, na.rm=TRUE)## age height weight ## 11.0 51.3 50.5
17.3.1.3 函数工厂
利用嵌套定义在函数内的函数, 可以解决上面的记录函数已运行次数的问题。如
f.gen <- function(){ runTimes <- 0 function(){ runTimes <<- runTimes + 1 print(runTimes) }}f <- f.gen()f()## [1] 1f()## [1] 2
在此例中,f.gen中有局部变量runTimes, f.gen()的输出是一个函数, 输出结果保存到变量名f中, 所以f是一个函数, 调用f时,查找变量runTimes时, 如果f的局部变量中没有runTimes, 就从其定义的环境中逐层向外查找, 在f定义中用了<<-赋值, 这样赋值的含义是逐层向外查找变量是否存在, 在哪里找到变量就给那里的该变量赋值。 f调用时向外查找到的变量在f.gen的局部空间中, 这是f函数的定义环境, 函数的定义环境是随函数本身一同保存的, 所以起到了把变量值runTimes与函数共同使用的效果。 定义在函数内的函数称为一个closure(闭包)。 closure最重要的作用就是定义能够保存历史运行状态的函数。
上面的f.gen这样的函数称为一个函数工厂, 因为它的结果是一个函数, 而且是一个闭包。 闭包在R中的主要作用是带有历史状态的函数。
下面的例子也用了closure, 可以显示从上次调用到下次调用之间经过的时间:
make_stop_watch <- function(){ saved.time <- proc.time()[3] function(){ t1 <- proc.time()[3] td <- t1 - saved.time saved.time <<- t1 cat("流逝时间(秒):", td, "\n") invisible(td) }}ticker <- make_stop_watch()ticker()## 流逝时间(秒): 0 for(i in 1:1000) sort(runif(10000))ticker()## 流逝时间(秒): 1.53
其中proc.time()返回当前的R会话已运行的时间, 结果在MS Windows系统中有三个值,分别是用户时间、系统时间、流逝时间, 其中流逝时间比较客观。
17.3.2 懒惰求值
R函数在调用执行时, 除非用到某个形式变量的值才求出其对应实参的值。 这一点在实参是常数时无所谓, 但是如果实参是表达式就不一样了。 形参缺省值也是只有在函数运行时用到该形参的值时才求值。
例如,
f <- function(x, y=ifelse(x>0, TRUE, FALSE)){ x <- -111 if(y) x*2 else x*10}f(5)## [1] -1110
可以看出,虽然形参x输入的实参值为5, 但是这时形参y并没按x=5被赋值为TRUE, 而是到函数体中第二个语句才被求值, 这时x的值已经变成了-111, 故y的值是FALSE。
17.3.3 递归调用
在函数内调用自己叫做递归调用。 递归调用可以使得许多程序变得简单, 但是往往导致程序效率很低, 需谨慎使用。
R中在递归调用时, 最好用 Recall 代表调用自身, 这样保证函数即使被改名(在R中函数是一个对象, 改名后仍然有效)递归调用仍指向原来定义。
斐波那契数列是如下递推定义的数列:
这个数列可以用如下递归程序自然地实现:
fib1 <- function(n){ if(n == 0) return(0) else if(n == 1) return(1) else if(n >=2 ) { Recall(n-1) + Recall(n-2) }}for(i in 0:10) cat('i =', i, ' x[i] =', fib1(i), '\n')## i = 0 x[i] = 0 ## i = 1 x[i] = 1 ## i = 2 x[i] = 1 ## i = 3 x[i] = 2 ## i = 4 x[i] = 3 ## i = 5 x[i] = 5 ## i = 6 x[i] = 8 ## i = 7 x[i] = 13 ## i = 8 x[i] = 21 ## i = 9 x[i] = 34 ## i = 10 x[i] = 55
17.3.4 向量化
自定义的函数,如果其中的计算都是向量化的, 那么函数自动地可以接受向量作为输入,结果输出向量。 比如,将每个元素都变成原来的平方的函数:
f <- function(x){ x^2}
如果输入一个向量,结果也是向量,输出的每个元素是输入的对应元素的相应的平方值。
但是,如下的分段函数:
其一元函数版本可以写成
g <- function(x){ if(abs(x) <= 1) { y <- x^2 } else { y <- 1 } y}
但是这个函数不能处理向量输入,因为if语句的条件必须是标量条件。 一个容易想到的修改是
gv <- function(x){ y <- numeric(length(x)) sele <- abs(x) <= 1 y[sele] <- x[sele]^2 y[!sele] <- 1.0 y}
或者
gv <- function(x){ ifelse(abs(x) <= 1, x^2, 1)}
对于没有这样简单做法的问题,可以将原来的逻辑包在循环中,如
gv <- function(x){ y <- numeric(length(x)) for(i in seq(along=x)){ if(abs(x[i]) <= 1) { y[i] <- x[i]^2 } else { y[i] <- 1 } } y}
函数Vectorize可以将这样的操作自动化。如
g <- function(x){ if(abs(x) <= 1) { y <- x^2 } else { y <- 1 } y}gv <- Vectorize(g)gv(c(-2, -0.5, 0, 0.5, 1, 1.5))## [1] 1.00 0.25 0.00 0.25 1.00 1.00
17.3.5 纯函数与副作用
理想的自定义函数最好是像一般的数学函数那样, 只要输入相同,输出也不变, 而且除了利用输出值之外不能对程序环境做其它改变。 这样的函数称为"纯函数"。 R的函数不能修改实参的值, 这有助于实现纯函数的要求。
如果函数对相同的输入可以有不同的输出当然不是纯函数, 例如R中的随机数函数(sample(), runif(), rnorm等)。
如果函数除了输出之外还在其它方面影响了运行环境, 这样的函数就不是纯函数。 所有画图函数(plot等)、输出函数(cat, print, save等)都是这样的函数。 这些对运行环境的改变叫做"副作用"(side effects)。 又比如,library()函数会引入新的函数和变量, setwd(), Sys.setenv(), Sys.setlocale()会改变R运行环境, options(), par()会改变R全局设置。 自定义R函数中如果调用了非纯函数也就变成了非纯函数。 编程中要尽量控制副作用而且要意识到副作用的影响, 尤其是全局设置与全局变量的影响。
有些函数不可避免地要修改运行环境, 如果可能的话, 在函数结束运行前, 应该恢复对运行环境的修改。 为此,可以在函数体的前面部分调用on.exit()函数, 此函数的参数是在函数退出前要执行的表达式或复合表达式。
例如, 绘图的函数中经常需要用par()修改绘图参数, 这会使得后续程序出错。 为此,可以在函数开头保存原始的绘图参数, 函数结束时恢复到原始的绘图参数。 如
f <- function(){ opar <- par(mfrow=c(1,2)) on.exit(par(opar)) plot((-10):10) plot((-10):10, ((-10):10)^2)}f()

如果函数中需要多次调用on.exit()指定多个恢复动作, 除第一个调用的on.exit()以外都应该加上add=TRUE选项。
17.4 程序调试
17.4.1 跟踪调试
函数定义一般都包含多行,所以一般不在命令行定义函数, 而是把函数定义写在源程序文件中, 用source命令调入。 用source命令调入运行的程序与在命令行运行的效果基本相同, 这样定义的变量也是全局变量。
考虑如下函数定义:
f <- function(x){ for(i in 1:n){ s <- s + x[i] }}
运行发现有错误:
f(1:5)## Error in f(1:5) : object 'n' not found
简单的函数可以直接仔细检查发现错误, 用cat, print等输出中间结果查找错误。 R提供了一个browser()函数, 在程序中插入对browser()函数的调用, 可以进入跟踪调试状态, 可以实时地查看甚至修改运行时变量的值。
程序运行遇到browser()函数时程序进入Browser的调试命令行。 在调试命令行,用n命令逐句运行, 用s命令跟踪进调用的函数内部逐句运行, 用c命令恢复正常运行, 用Q命令强项终止程序运行。 可以如同在R命令行一样查看变量的值或修改变量的值。 在RStudio中进入跟踪状态后有相应的运行控制图标, 可以用鼠标点击某行程序的行号设置断点, 重新source()之后就可以在断点处进入跟踪状态。
为调试如上函数f的程序, 在定义中插入对browser()的调用如:
f <- function(x){ browser() for(i in 1:n){ s <- s + x[i] }}
调试运行过程如下:
f(1:5)## Called from: f(1:5)## Browse[1]> n## debug at #3: for (i in 1:n) {## s <- s + x[i]## }## Browse[2]> n## Error in f(1:5) : object 'n' not found
发现是在for(i in 1:n)行遇到未定义的变量n。
在源文件中把出错行改为for(i in 1:length(x)), 再次运行, 发现在运行s <- s + x[i]行时,遇到未定义的变量s。 这是忘记初始化引起的。 在for语句前添加s <- 0语句,函数定义变成:
f <- function(x){ browser() s <- 0 for(i in 1:length(x)){ s <- s + x[i] }}
再次运行, 在Browse[1]>提示下命令c表示恢复正常运行, 程序不显示错误但是也没有显示求和结果。 检查可以看出错误是忘记把函数返回值写在函数定义最后。
在函数定义最后添加s一行, 再次运行,程序结果与手工验算结果一致。 函数变成
f <- function(x){ browser() n <- length(x) s <- 0 for(i in 1:n){ s <- s + x[i] } s}
自定义函数应该用各种不同输入测试其正确性和稳定性。 比如,上面的函数当自变量x为零长度向量时应该返回0才合适, 但是上面的写法会返回一个numeric(0)结果, 这个结果表示长度为零的向量:
f(numeric(0))## Called from: f(numeric(0))## Browse[1]> c## numeric(0)
程序输入了零长度自变量,我们期望其输出为零而不是numeric(0)。 在自变量x为零长度时, 函数中for(i in 1:length(x)应该一次都不进入循环, 跟踪运行可以发现实际对i=1和i=0共运行了两轮循环。 把这里的1:length(x)改成seq(along=x)解决了问题, seq(along=x)生成x的下标序列, 如果x是零长度的则下标序列为零长度向量。
函数不需要修改后, 可以把对browser()的调用删除或注释掉。 函数最终修改为:
f <- function(x){ s <- 0 for(i in seq(along=x)){ s <- s + x[i] } s}
这里只是用这个简单函数演示如何调试程序, 求向量和本身是不需要我们去定义新函数的, sum函数本来就是完成这样的功能。 实际上,许多我们认为需要自己编写程序作的事情, 在R网站都能找到别人已经完成的程序。
17.4.2 出错调试选项
比较长的程序在调试时如果从开头就跟踪, 比较耗时。可以设置成出错后自动进入跟踪模式, 检查出错时的变量值。只要进行如下设置:
options(error=recover)
则在出错后可以选择进入出错的某一层函数内部, 在browser环境中跟踪运行。
例如,如上设置后, 前面那个求向量元素和的例子程序按最初的定义, 运行时出现如下的选择:
## Error in f(1:5) : object 'n' not found## ## Enter a frame number, or 0 to exit ## ## 1: f(1:5)## ## Selection: f(1:5)## ## Selection: 1## Called from: top level ## Browse[1]>
在Selection后面输入了1,就进入了函数内部跟踪。 用Q终止运行并退出整个browser跟踪。 当函数调用函数时可以选择进入哪一个函数进行跟踪。
17.4.3 警告处理
有些警告信息实际是错误, 用options()的warn参数可以设置警告级别, 如设置warn=2则所有警告当作错误处理。 设置如
options(warn=2)
17.4.4 stop()、warning()、message()
编写程序时应尽可能提前发现不合法的输入和错误的状态。 发现错误时, 可以用stop(s)使程序运行出错停止, 其中s是一个字符型对象, 用来作为显示的出错信息。
发现某些问题后如果不严重, 可以不停止程序运行, 但用warning(s)提交一个警告信息, 其中s是字符型的警告信息。 警告信息的显示可能比实际运行要滞后一些。
函数message()与stop()、warning()类似, 不算是错误或者警告, 但仍算是某种非正常的信息输出。
17.4.5 预防性设计
在编写自定义函数时, 可以检查自变量输入以确保输入符合要求。 函数stopifnot可以指定自变量的若干个条件, 当自变量不符合条件时自动出错停止。
例如,函数f()需要输入两个数值型向量x, y, 需要长度相等, 可以用如下的程序
f <- function(x, y){ stopifnot(is.numeric(x), is.numeric(y), length(x)==length(y)) ## 函数体程序语句...}
17.5 函数式编程介绍
R可以算是一个函数式语言(functional language):
- R语言的设计主要用函数求值来进行运算;
- R的用户主要使用函数调用来访问R的功能。
按照函数式编程的要求, 每个R函数必须功能清晰、定义确切。 比较容易控制的函数是纯函数, 纯函数必须像数学中单值函数那样给定自变量输入有唯一确定的输出。 比如,多个函数用全局变量传递信息,就不能算是纯函数。
R支持类(class)和方法(method), 实际提供了适用于多种自变量的通用函数(generic function), 不同自变量类型调用该类特有的方法, 但函数名可以保持不变。
函数式编程语言提供了定义纯函数的功能。 这样的函数不能有副作用(side effects): 函数返回值包含了函数执行的所有效果。 函数定义仅由对所有可能的自变量值确定返回值来确定, 不依赖于任何外部信息(也就不能依赖于全局变量与系统设置值)。 函数定义返回值的方式是隐含地遍历所有可能的参数值给出返回值, 而不是用过程式的计算来修改对象的值。
函数式编程的目的是提供可理解、可证明正确的软件。 R虽然带有函数式编程语言特点, 但并不强求使用函数式编程规范。 典型的函数式编程语言如Haskel, Lisp的运行与R的显式的、顺序的执行方式相差很大。
17.5.1 函数式编程的要求
- 没有副作用。调用一个函数对后续运算没有影响, 不管是再次调用此函数还是调用其它函数。 这样,用全局变量在函数之间传递信息就是不允许的。 其它副作用包括写文件、打印、绘图等, 这样的副作用对函数式要求破坏不大。
- 不受外部影响。函数返回值只依赖于其自变量及函数的定义。
- 不受赋值影响。 函数定义不需要反复对内部对象(所谓"状态变量")赋值或修改。
R只能部分满足这些要求。 一个R函数是否满足这些要求不仅要看函数本身, 还要看函数内部调用的其它函数。
像options()函数这样修改全局运行环境的功能会破坏函数式要求。 尽可能让自己的函数不依赖于options()中的参数。
与具体硬件、软件环境有关的一些因素也破坏纯函数要求, 如不同的硬件常数、精度等。 调用操作系统的功能对函数式要求破坏较大。 减少赋值主要需要减少循环,可以用R的向量化方法解决。
17.5.2 Map、Reduce、Filter
R提供了 Map, Reduce, Filter, Find, Negate, Position等支持函数式编程的工具函数。 这些函数包含对列表每一项进行变换, 列表数据汇总,列表元素筛选等功能。
17.5.2.1 Map函数
Map()以一个函数作为参数, 可以对其它参数的每一对应元素进行变换, 结果为列表。
例如, 对数据框d, 如下的程序可以计算每列的平方和:
d <- data.frame( x = c(1, 7, 2), y = c(3, 5, 9))Map(function(x) sum(x^2), d)## $x## [1] 54## ## $y## [1] 115
实际上,这个例子也可以用lapply()改写成
lapply(d, function(x) sum(x^2))## $x## [1] 54## ## $y## [1] 115
Map()比lapply()增强的地方在于它允许对多个列表的对应元素逐一处理。 例如,为了求出d中每一行的最大值,可以用
Map(max, d$x, d$y)## [[1]]## [1] 3## ## [[2]]## [1] 7## ## [[3]]## [1] 9
可以用unlist()函数将列表结果转换为向量,如
unlist(Map(max, d$x, d$y))## [1] 3 7 9
17.5.2.2 Reduce函数
Reduce函数把输入列表(或向量)的元素逐次地用给定的函数进行合并计算。 例如,
Reduce(sum, 1:4)## [1] 10
实际执行的是。 当然,求的和只需要sum(1:4), 但是Reduce可以对元素为复杂类型的列表进行逐项合并计算。
例如,intersect函数可以计算两个集合的交集; 对多个集合,如何计算交集? 下面的例子产生了4个集合, 然后反复调用intersect()求出了交集:
set.seed(2)x <- replicate(4, sample(1:5, 5, replace=TRUE), simplify=FALSE); x## [[1]]## [1] 5 1 5 1 4## ## [[2]]## [1] 5 1 2 3 1## ## [[3]]## [1] 3 2 3 1 1## ## [[4]]## [1] 4 3 1 5 3intersect(intersect(intersect(x[[1]], x[[2]]), x[[3]]), x[[4]])## [1] 1
也可以用magrittr包的%>%符号写成:
library(magrittr)x[[1]] %>% intersect(x[[2]]) %>% intersect(x[[3]]) %>% intersect(x[[4]])## [1] 1
还可以写成循环:
y <- x[[1]]for(i in 2:4) y <- intersect(y, x[[i]])y## [1] 1
都比较繁琐。
利用Reduce函数,只要写成
Reduce(intersect, x)## [1] 1
Reduce函数还可以用right参数选择是否从右向左合并, 用参数init给出合并初值, 用参数accumulate要求保留每一步合并的结果(累计)。 这个函数可以把很多仅适用于两个运算元的运算推广到多个参数的情形。
17.5.2.3 Filter、Find、Position函数
Filter(f, x)用一个判断真假的一元函数f作为筛选规则, 从列表或向量x中筛选出用f作用后为真值的元素子集。 f必须返回标量的TRUE或者FALSE, 这样的函数称为示性函数(predicate functions)。 例如
f <- function(x) x > 0 & x < 1Filter(f, c(-0.5, 0.5, 0.8, 1))## [1] 0.5 0.8
当然,这样的简单例子完全可以改写成:
f <- function(x) x > 0 & x < 1x <- c(-0.5, 0.5, 0.8, 1)x[x>0 & x < 1]## [1] 0.5 0.8
但是,对于比较复杂的判断, 特别是需要用许多个语句计算后进行的判断, 就需要把判断写成一个函数, 然后可以用Filter比较简单地表达按照判断规则取子集的操作。
Find()作用与Filter()类似, 但是仅返回满足条件的第一个, 也可以用参数right=TRUE要求返回满足条件的最后一个。
Position()作用与Find()类似, 但不是返回满足条件的元素而是返回第一个满足条件的元素所在的下标位置。
18 R程序效率
18.1 R的运行效率
R是解释型语言,在执行单个运算时, 效率与编译代码相近; 在执行迭代循环时, 效率较低, 与编译代码的速度可能相差几十倍。 R以向量、矩阵为基础运算单元, 在进行向量、矩阵运算时效率很高, 应尽量采用向量化编程。
另外,R语言的设计为了方便进行数据分析和统计建模, 有意地使语言特别灵活, 比如, 变量为动态类型而且内容可修改, 变量查找在当前作用域查找不到可以向上层以及扩展包中查找, 函数调用时自变量仅在使用其值时才求值(懒惰求值), 这样的设计都为运行带来了额外的负担, 使得运行变慢。
在计算总和、元素乘积或者每个向量元素的函数变换时, 应使用相应的函数,如sum, prod, sqrt, log等。
对于从其它编程语言转移到R语言的学生, 如果不细究R特有的编程模式, 编制的程序可能效率比正常R程序慢上几十倍, 而且繁琐冗长。
为了提高R程序的运行效率, 需要尽可能利用R的向量化特点, 尽可能使用已有的高效函数, 还可以把运行速度瓶颈部分改用C++、FORTRAN等编译语言实现, 可以用R的profiler工具查找运行瓶颈。 对于大量数据的长时间计算, 可以借助于现代的并行计算工具。
对已有的程序, 仅在运行速度不满意时才需要进行改进, 否则没必要花费宝贵的时间用来节省几秒钟的计算机运行时间。 要改善运行速度, 首先要找到运行的瓶颈, 这可以用专门的性能分析(profiling)功能实现。 R软件中的Rprof()函数可以执行性能分析的数据收集工作, 收集到的性能数据用summaryRprof()函数可以显示运行最慢的函数。 如果使用RStudio软件,可以用Profile菜单执行性能数据收集与分析, 可以在图形界面中显示程序中哪些部分运行花费时间最多。
在改进已有程序的效率时, 第一要注意的就是不要把原来的正确算法改成一个速度更快但是结果错误的算法。 这个问题可以通过建立试验套装, 用原算法与新算法同时试验看结果是否一致来避免。 多种解决方案的正确性都可以这样保证, 也可以比较多种解决方案的效率。
本章后面部分描述常用的改善性能的方法。 对于涉及到大量迭代的算法,如果用R实现性能太差不能满足要求, 可以改成C++编码,用Rcpp扩展包连接到R中。 Rcpp扩展包的使用将单独讲授。
R的运行效率也受到内存的影响, 占用内存过多的算法有可能受到物理内存大小限制无法运行, 过多复制也会影响效率。
如果要实现一个比较单纯的不需要利用R已有功能的算法, 发现用R计算速度很慢的时候, 也可以考虑先用Julia语言实现。 Julia语言设计比R更先进,运算速度快得多, 运算速度经常能与编译代码相比, 缺点是刚刚诞生几年的时间, 可用的软件包还比较少。
18.2 向量化编程
18.2.1 示例1
假设要计算如下的统计量:
其中 是样本中位数。 用传统的编程风格, 把这个统计量的计算变成一个R函数,可能会写成:
f1 <- function(x){ n <- length(x) mhat <- median(x) s <- 0.0 for(i in 1:n){ s <- s + abs(x[i] - mhat) } s <- s/n return(s)}
用R的向量化编程,函数体只需要一个表达式:
f2 <- function(x) mean( abs(x - median(x)) )
其中x - median(x)利用了向量与标量运算结果是向量每个元素与标量运算的规则, abs(x - median(x))利用了abs()这样的一元函数如果以向量为输入就输出每个元素的函数值组成的向量的规则,mean(...)避免了求和再除以n的循环也不需要定义多余的变量n。
显然,第二种做法的程序比第一种做法简洁的多, 如果多次重复调用, 第二种做法的计算速度比第一种要快几十倍甚至上百倍。 在R中, 用system.time()函数可以求某个表达式的计算时间, 返回结果的第3项是流逝时间。 下面对x采用10000个随机数, 并重复计算1000次,比较两个程序的效率:
nrep <- 1000x <- runif(10000)y1 <- numeric(nrep); y2 <- y1system.time(for(i in 1:nrep) y1[i] <- f1(x) )[3]## elapsed ## 10.08system.time(for(i in 1:nrep) y1[i] <- f2(x) )[3]## elapsed ## 0.48
速度相差二十倍以上。
有一个R扩展包microbenchmark可以用来测量比较两个表达式的运行时间。 如:
x <- runif(10000)microbenchmark::microbenchmark( f1(x), f2(x))## Unit: microseconds## expr min lq mean median uq max neval## f1(x) 1313.411 1471.226 2229.3293 1706.920 2099.402 27576.50 100## f2(x) 257.542 268.594 345.7897 314.602 373.461 2010.47 100
就平均运行时间(单位:毫秒)来看,f2()比f1()快大约30倍。
18.2.2 示例2
假设要编写函数计算
其它
利用传统思维,程序写成
f1 <- function(x){ n <- length(x) y <- numeric(n) for(i in seq(along=x)){ if(x[i] >= 0) y[i] <- 1 else y[i] <- 0 } y}
实际上,y <- numeric(n)使得y的每个元素都初始化为0, 所以程序中else y[i] <- 0可以去掉。
利用向量化与逻辑下标,程序可以写成:
f2 <- function(x){ n <- length(x) y <- numeric(n) y[x >= 0] <- 1 y}
但是,利用R中内建函数ifelse(), 可以把函数体压缩到仅用一个语句:
f2 <- function(x) ifelse(x >= 0, 1, 0)
18.2.3 示例3
考虑一个班的学生存在生日相同的概率。 假设一共有365个生日(只考虑月、日)。 设一个班有n个人, 当n大于365时{至少两个人有生日相同}是必然事件(概率等于1)。
当小于等于365时:
至少有两人同生日个人生日彼此不同
对来计算对应的概率。 完全用循环(两重循环),程序写成:
f1 <- function(){ ny <- 365 x <- numeric(ny) for(n in 1:ny){ s <- 1 for(j in 0:(n-1)){ s <- s * (365-j)/365 } x[n] <- 1 - s } x}
注意, 不能先计算 和再相除, 这会造成数值溢出。
用prod()函数可以向量化内层循环:
f2 <- function(){ ny <- 365 x <- numeric(ny) for(n in 1:ny){ x[n] <- 1 - prod((365:(365-n+1))/365) } x}
程序利用了向量与标量的除法, 以及内建函数prod()。
把程序用cumprod()函数改写, 可以完全避免循环:
f3 <- function(){ ny <- 365 x <- 1 - cumprod((ny:1)/ny) x}
用microbenchmark比较:
microbenchmark::microbenchmark( f1(), f2(), f3())## Unit: microseconds## expr min lq mean median uq max neval## f1() 2534.807 2577.730 2679.48244 2615.256 2712.9270 3408.187 100## f2() 323.855 333.365 414.81692 344.160 369.3485 5868.456 100## f3() 1.028 1.542 2.52415 2.056 2.5700 25.189 100
f2()比f1()快大约7倍, f3()比f2()又快了大约160倍, f3()比f1()快了一千倍以上!
18.3 减少显式循环
显式循环是R运行速度较慢的部分, 有循环的程序也比较冗长, 与R的向量化简洁风格不太匹配。 另外, 在循环内修改数据子集,例如数据框子集, 可能会先制作副本再修改, 这当然会损失很多效率。 R 3.1.0版本以后列表元素在修改时不制作副本。
前面已经指出, 利用R的向量化运算可以减少很多循环程序。
R中的有些运算可以用内建函数完成, 如sum, prod, cumsum, cumprod, mean, var, sd等。 这些函数以编译程序的速度运行, 不存在效率损失。
R的sin, sqrt, log等函数都是向量化的, 可以直接对输入向量的每个元素进行变换。
对矩阵,用apply函数汇总矩阵每行或每列。 colMeans, rowMeans可以计算矩阵列平均和行平均, colSums, rowSums可以计算矩阵列和与行和。
apply类函数有多个, 包括apply, sapply, lapply, tapply, vapply, replicate等。 这些函数不一定能提高程序运行速度, 但是使用这些函数更符合R的程序设计风格, 使程序变得简洁, 当然, 程序更简洁并不等同于程序更容易理解, 要理解这样的程序, 需要更多学习与实践。
18.3.1 lapply()、sapply()和vapply()函数
对列表, lapply函数操作列表每个元素,格式为
lapply(X, FUN)
其中X是一个列表或向量, FUN是一个函数(可以是有名或无名函数), 结果也总是一个列表, 结果列表的第个元素是将X的第个元素输入到FUN中的返回结果。 如果输入不是列表, 就转换为列表再对每一元素做变换。
sapply与lapply函数类似, 但是sapply试图简化输出结果为向量或矩阵, 在不可行时才和lapply返回列表结果。 如果X长度为零,结果是长度为零的列表; 如果FUN(X[i])都是长度为1的结果, sapply()结果是一个向量; 如果FUN(X[i])都是长度相同且长度大于1的向量, sapply()结果是一个矩阵, 矩阵的第列保存FUN(X[i])的结果。 因为sapply()的结果类型的不确定性, 在自定义函数中应慎用。
vapply()函数与sapply()函数类似, 但是它需要第三个参数即函数返回值类型的例子,格式为
vapply(X, FUN, FUN.VALUE)
其中FUN.VALUE是每个FUN(X[i])的返回值的例子, 要求所有FUN(X[i])结果类型和长度相同。
18.3.1.1 示例1:数据框变量类型
typeof()函数求变量的存储类型,如
d.class <- read.csv('class.csv', header=TRUE)typeof(d.class[,'age'])## [1] "integer"
这里d.class是一个数据框, 数据框也是一个列表, 每个列表元素是数据框的一列。 如下程序使用sapply()求每一列的存储类型:
sapply(d.class, typeof)## name sex age height weight ## "integer" "integer" "integer" "double" "double"
注意因为CSV文件读入为数据框时把姓名、性别都转换成了因子, 所以这两个变量的存储类型也是整数。 为了避免这样的转换, 在read.csv()中使用选项stringsAsFactors=FALSE选项。
关于一个数据框的结构, 用str()函数可以得到更为详细的信息:
str(d.class)## 'data.frame': 19 obs. of 5 variables:## $ name : Factor w/ 19 levels "Alfred","Alice",..: 2 3 5 10 11 12 15 16 17 1 ...## $ sex : Factor w/ 2 levels "F","M": 1 1 1 1 1 1 1 1 1 2 ...## $ age : int 13 13 14 12 12 15 11 15 14 14 ...## $ height: num 56.5 65.3 64.3 56.3 59.8 66.5 51.3 62.5 62.8 69 ...## $ weight: num 84 98 90 77 84.5 ...
18.3.1.2 示例2:strsplit()函数结果处理
假设有4个学生的3次小测验成绩, 每个学生的成绩记录到了一个以逗号分隔的字符串中,如:
s <- c('10, 8, 7', '5, 2, 2', '3, 7, 8', '8, 8, 9')
对单个学生,可以用strsplit()函数把三个成绩拆分,如:
strsplit(s[1], ',', fixed=TRUE)[[1]]## [1] "10" " 8" " 7"
注意这里strspli()的结果是仅有一个元素的列表, 用了"[[...]]"格式取出列表元素。 拆分的结果可以用as.numeric()转换为有三个元素的数值型向量:
as.numeric(strsplit(s[1], ',', fixed=TRUE)[[1]])## [1] 10 8 7
还可以求三次小测验的总分:
sum(as.numeric(strsplit(s[1], ',', fixed=TRUE)[[1]]))## [1] 25
用strsplit()处理有4个字符串的字符型向量s, 结果是长度为4的列表:
tmp.res <- strsplit(s, ',', fixed=TRUE); tmp.res## [[1]]## [1] "10" " 8" " 7"## ## [[2]]## [1] "5" " 2" " 2"## ## [[3]]## [1] "3" " 7" " 8"## ## [[4]]## [1] "8" " 8" " 9"
用sapply()和as.numeric()可以把列表中所有字符型转为数值型, 并以矩阵格式输出:
sapply(tmp.res, as.numeric)## [,1] [,2] [,3] [,4]## [1,] 10 5 3 8## [2,] 8 2 7 8## [3,] 7 2 8 9
但是,在通用程序中使用sapply()有可能会发生结果类型可变的情况。 为此, 上面可以用vapply()改写成
vapply(tmp.res, as.numeric, numeric(3))## [,1] [,2] [,3] [,4]## [1,] 10 5 3 8## [2,] 8 2 7 8## [3,] 7 2 8 9
其中第三个参数numeric(3)给出了对tmp.res中的任意一项应用as.numeric函数的结果的例子。
如果需要求每个学生的小测验总分, 只要对结果矩阵每列求和:
colSums(vapply(tmp.res, as.numeric, numeric(3)))## [1] 25 9 18 25
使用apply类函数的程序写法简洁, 但是对于初学者需要比较长的时间来读懂, 需要更长的时间用到自己的程序中。
上例中的嵌套调用用magrittr包的管道运算符更容易理解:
library(magrittr)s %>% strsplit(",", fixed=TRUE) %>% sapply(as.numeric) %>% colSums()## [1] 25 9 18 25
18.3.1.3 示例3: 不等长结果
调用sapply(列表, 函数)时, 如果"函数"结果长度有变化, 结果只能以列表输出。 这时,sapply与lapply返回相同的结果。 一般地, sapply试图把结果简化为向量、矩阵、多维数组, 在无法简化时就返回列表; lapply总是返回列表。
设数据框d中有两列数,希望将每列变成没有重复值的。 数据例子如下:
d1 <- data.frame(x1=c(1,3,3,2), x2=c(3,5,5,3)); d1## x1 x2## 1 1 3## 2 3 5## 3 3 5## 4 2 3
因为x1和x2两列的无重复值个数不同,结果只能是列表:
sapply(d1, unique)## $x1## [1] 1 3 2## ## $x2## [1] 3 5
与lapply(d, unique)效果相同。
如果使用了vapply,在遇到结果长度变化时会明确报错,如:
vapply(d1, unique, numeric(3))## Error in vapply(d1, unique, numeric(3)) : values must be length 3,## but FUN(X[[2]]) result is length 2
在以上的例子中, 输入的d1数据框如果无重复个数相同,sapply结果就是矩阵,而lapply结果仍然是列表:
d2 <- data.frame(x1=c(1,3,3,2), x2=c(3,5,5,7)); d2## x1 x2## 1 1 3## 2 3 5## 3 3 5## 4 2 7sapply(d2, unique)## x1 x2## [1,] 1 3## [2,] 3 5## [3,] 2 7lapply(d2, unique)## $x1## [1] 1 3 2## ## $x2## [1] 3 5 7
sapply()的这种特点会造成编程判断困难, 所以在不能确定函数结果长度是否保持不变时, 应该用lapply()代替sapply(), lapply()总是返回列表。 如果能够确定函数结果长度保持不变, 在通用程序中应该用vapply()取代sapply(), 使得程序结果总是一致的。
18.3.1.4 示例4:无名函数
sapply、vapply和lapply中要做的函数变换可以当场定义, 不需要函数名。
仍使用示例2的数据,任务是从输入的逗号分隔成绩中求每个学生的三科总分。
用strsplit()拆分可得列表, 每个列表是由三个成绩字符串的字符型向量。如下代码可以求得总分:
s <- c('10, 8, 7', '5, 2, 2', '3, 7, 8', '8, 8, 9')sapply( strsplit(s, ',', fixed=TRUE), function(ss) sum(as.numeric(ss)) )## [1] 25 9 18 25
这里没有预先定义处理函数, 也没有函数名, 而是直接对sapply的第二自变量使用了一个无名函数。 实际上,R的函数定义也是函数名被赋值为一个函数对象。
但是,如果sapply()函数中的无名函数访问其它变量的话, 容易产生作用域问题。
18.3.2 replicate()函数
replicate()函数用来执行某段程序若干次, 类似于for()循环但是没有计数变量。 常用于随机模拟。 replicate()的缺省设置会把重复结果尽可能整齐排列成一个多维数组输出。
语法为
replicate(重复次数, 要重复的表达式)
其中的表达式可以是复合语句, 也可以是执行一次模拟的函数。
下面举一个简单模拟例子。 设总体为, 取样本量, 重复地生成模拟样本共组, 输出每组样本的样本均值和样本标准差。 模拟可以用如下的replicate()实现:
set.seed(1)replicate(6, { x <- rnorm(5, 0, 1); c(mean(x), sd(x)) })## [,1] [,2] [,3] [,4] [,5] [,6]## [1,] 0.1292699 0.1351357 0.03812297 0.4595670 0.08123054 -0.3485770## [2,] 0.9610394 0.6688342 1.49887443 0.4648177 1.20109623 0.7046822
结果是一个矩阵,矩阵行数与每次模拟的结果(均值、标准差)个数相同, 这里第一行都是均值,第二行都是标准差; 矩阵每一列对应于一次模拟。此结果转置可能更合适。
18.3.3 Map()和mapply()
lapply()、sapply()、vapply()只能针对单个列表X的每个元素重复处理。 如果有两个列表X和Y要进行对应元素的处理, 用这三个函数不容易做到, 这时可以用Map()或mapply()。 Map()的格式为
Map(f, ...)
其中f是一个函数, Map的其它参数都是每次取出对应的元素作为f()的输入。 Map()的结果总是列表。
例如,下面有两个向量:
x <- c(1, 7, 2)y <- c(3, 5, 9)
为了求得x和y的每个对应元素的最大值,可以用
Map(max, x, y)## [[1]]## [1] 3## ## [[2]]## [1] 7## ## [[3]]## [1] 9
结果是一个列表。 为了把列表转换为普通向量, 可以用unlist()函数,如
unlist(Map(max, x, y))## [1] 3 7 9
这个例子演示了Map的用法。 实际上,为了求多个向量对应元素的最大值, 可以用pmax函数,如
pmax(x, y)## [1] 3 7 9
mapply()函数与Map()类似, 但是可以自动简化结果类型, 可以看成是sapply()推广到了可以对多个输入的对应元素逐项处理。 mapply()可以用参数MoreArgs指定逐项处理时一些共同的参数。 简单的调用格式为
mapply(FUN, ...)
如
mapply(max, x, y)## [1] 3 7 9
18.4 R的计算函数
R中提供了大量的数学函数、统计函数和特殊函数, 可以打开R的HTML帮助页面, 进入"Search Enging & Keywords"链接, 查看其中与算术、数学、优化、线性代数等有关的专题。
这里简单列出一部分常用函数, 对函数filter, fft, convolve进行说明。
18.4.1 数学函数
常用数学函数包括abs, sign, log, log10, sqrt, exp, sin, cos, tan, asin, acos, atan, atan2, sinh, cosh, tanh。 还有gamma, lgamma(伽玛函数的自然对数)。
用于取整的函数有ceiling, floor, round, trunc, signif, as.integer等。 这些函数是向量化的一元函数。
choose(n,k)返回从中取的组合数。 factorial(x)返回结果。 combn(x,m)返回从集合中每次取出个的所有不同取法, 结果为一个矩阵,矩阵每列为一种取法的个元素值。
18.4.2 概括函数
sum对向量求和, prod求乘积。
cumsum和cumprod计算累计, 得到和输入等长的向量结果。
diff计算前后两项的差分(后一项减去前一项)。
mean计算均值,var计算样本方差或协方差矩阵, sd计算样本标准差, median计算中位数, quantile计算样本分位数。 cor计算相关系数。
colSums, colMeans, rowSums, rowMeans对矩阵的每列或每行计算总和或者平均值, 效率比用apply函数要高。
rle和inverse.rle用来计算数列中"连"长度及其逆向恢复, "连"经常用在统计学的随机性检验中。
18.4.3 最值
max和min求最大和最小, cummax和cummin累进计算。
range返回最小值和最大值两个元素。
对于max, min, range, 如果有多个自变量可以把这些自变量连接起来后计算。
pmax(x1,x2,...)对若干个等长向量计算对应元素的最大值, 不等长时短的被重复使用。 pmin类似。 比如,pmax(0, pmin(1,x))把x限制到内。
18.4.4 排序
sort返回排序结果。 可以用decreasing=TRUE选项进行降序排序。 sort可以有一个partial=选项, 这样只保证结果中partial=指定的下标位置是正确的。 比如:
sort(c(3,1,4,2,5), partial=3)## [1] 2 1 3 4 5
只保证结果的第三个元素正确。 可以用来计算样本分位数估计。
在sort()中用选项na.last指定缺失值的处理, 取NA则删去缺失值, 取TRUE则把缺失值排在最后面, 取FALSE则把缺失值排在最前面。
order返回排序用的下标序列, 它可以有多个自变量, 按这些自变量的字典序排序。 可以用decreasing=TRUE选项进行降序排序。 如果只有一个自变量,可以使用sort.list函数。
rank计算秩统计量,可以用ties.method指定同名次处理方法, 如ties.method=min取最小秩。
order, sort.list, rank也可以有 na.last选项,只能为TRUE或FALSE。
unique()返回去掉重复元素的结果, duplicated()对每个元素用一个逻辑值表示是否与前面某个元素重复。 如
unique(c(1,2,2,3,1))## [1] 1 2 3duplicated(c(1,2,2,3,1))## [1] FALSE FALSE TRUE FALSE TRUE
rev反转序列。
18.4.5 一元定积分integrate
integrate(f, lower, upper)对一元函数f计算从lower到upper的定积分。 使用自适应算法保证精度。 如:
integrate(sin, 0, pi)## 2 with absolute error < 2.2e-14
函数的返回值不仅仅包含定积分数值, 还包含精度等信息。
18.4.6 一元函数求根uniroot
uniroot(f, interval)对函数f在给定区间内求一个根, interval为区间的两个端点。 要求f在两个区间端点的值异号。 即使有多个根也只能给出一个。 如
uniroot(function(x) x*(x-1)*(x+1), c(-2, 2))## $root## [1] 0## ## $f.root## [1] 0## ## $iter## [1] 1## ## $init.it## [1] NA## ## $estim.prec## [1] 2
对于多项式, 可以用polyroot函数求出所有的复根。
18.4.7 离散傅立叶变换fft
R中fft函数使用快速傅立叶变换算法计算离散傅立叶变换。 设x为长度n的向量, y=fft(x),则
y[k] = sum(x * complex( argument = -2*pi * (0:(n-1)) * (k-1)/n))
即
注意没有除以
另外,若y=fft(x), z=fft(y, inverse=T), 则 x == z/length(x)。
快速傅立叶变换是数值计算中十分常用的工具, R软件包fftw可以进行优化的快速傅立叶变换。
18.4.8 用filter函数作迭代
R在遇到向量自身迭代时很难用向量化编程解决, filter函数可以解决其中部分问题。 filter函数可以进行卷积型或自回归型的迭代。 语法为
filter(x, filter, method = c("convolution", "recursive"), sides=2, circular =FALSE, init)
下面用例子演示此函数的用途。
18.4.8.1 示例1:双侧滤波
对输入序列, 希望进行如下滤波计算:
其中
假设保存在向量x中, 保存在向量f中, 保存在向量y中, 无定义部分取NA, 程序可以写成
y <- filter(x, f, method="convolution", sides=2)
比如,设, , 则
用filter()
y <- filter(c(1,3,7,12,17,23), c(0.1, 0.5, 0.4), method="convolution", sides=2)y## Time Series:## Start = 1 ## End = 6 ## Frequency = 1 ## [1] NA 2.6 5.9 10.5 15.6 NA
18.4.8.2 示例2: 单侧滤波
对输入序列, 希望进行如下滤波计算:
其中
假设保存在向量x中, 保存在向量f中, 保存在向量y中, 无定义部分取NA, 程序可以写成
y <- filter(x, f, method="convolution", sides=1)
比如,设, , 则
程序为
y <- filter(c(1,3,7,12,17,23), c(0.1, 0.5, 0.4), method="convolution", sides=1)y## Time Series:## Start = 1 ## End = 6 ## Frequency = 1 ## [1] NA NA 2.6 5.9 10.5 15.6
18.4.8.3 示例3: 自回归迭代
设输入, 要计算
其中 已知。
设保存在向量x中,保存在向量a中, 保存在向量y中。
如果都等于零, 可以用如下程序计算:
filter(x, a, method="recursive")
如果保存在向量b中(注意与时间顺序相反), 可以用如下程序计算:
filter(x, a, method="recursive", init=b)
比如, 设, , , 则
迭代程序和结果为
y <- filter(c(0.1, -0.2, -0.1, 0.2, 0.3, -0.2), c(0.9, 0.1), method="recursive")print(y, digits=3)## Time Series:## Start = 1 ## End = 6 ## Frequency = 1 ## [1] 0.1000 -0.1100 -0.1890 0.0189 0.2981 0.0702
这个例子中, 如果已知, 迭代程序和结果为:
y <- filter(c(0.1, -0.2, -0.1, 0.2, 0.3, -0.2), c(0.9, 0.1), init=c(200, 100), method="recursive")print(y, digits=6)## Time Series:## Start = 1 ## End = 6 ## Frequency = 1 ## [1] 190.100 190.890 190.711 190.929 191.207 190.979
18.5 并行计算
现代桌面电脑和笔记本电脑的CPU通常有多个核心或虚拟核心(线程), 如2核心或4虚拟核心。 通常R运行并不能利用全部的CPU能力, 仅能利用其中的一个虚拟核心。 使用特制的BLAS库(非R原有)可以并发置信多个线程, 一些R扩展包也可以利用多个线程。 利用多台计算机、多个CPU、CPU中的多核心和多线程同时完成一个计算任务称为并行计算。
想要充分利用多个电脑、多个CPU和CPU内的虚拟核心, 技术上比较复杂, 涉及到计算机之间与进程之间的通讯问题, 在要交流的数据量比较大时会造成并行计算的瓶颈。
实际上, 有些问题可以很容易地进行简单地并行计算。 比如, 在一个统计研究中, 需要对100组参数组合进行模拟, 评估不同参数组合下模型的性能。 假设研究人员有两台安装了R软件的计算机, 就可以在两台计算机上进行各自50组参数组合的模拟, 最后汇总在一起就可以了。
R的parallel包提供了一种比较简单的利用CPU多核心的功能, 思路与上面的例子类似, 如果有多个任务互相之间没有互相依赖, 就可以分解到多个计算机、多个CPU、多个虚拟核心中并行计算。 最简单的情形是一台具有单个CPU、多个虚拟核心的台式电脑或者笔记本电脑。 但是, 统计计算中最常见耗时计算任务是随机模拟, 随机模拟要设法避免不同进程的随机数序列的重复可能, 以及同一进程中不同线程的随机数序列的重复可能。
parallel包提供了parLapply()、parSapply()、parApply()函数, 作为lapply()、sapply()、apply()函数的并行版本, 与非并行版本相比, 需要用一个临时集群对象作为第一自变量。
18.5.1 例1:完全不互相依赖的并行运算
考虑如下计算问题:
下面的程序取n为一百万,k为2到21,循环地用单线程计算。
f10 <- function(k=2, n=1000){ s <- 0.0 for(i in seq(n)) s <- s + 1/i^k s}f11 <- function(n=1000000){ nk <- 20 v <- sapply(2:(nk+1), function(k) f10(k, n)) v}system.time(f11())[3]## elapsed## 2.87
因为对不同的k, f0(k)计算互相不依赖, 也不涉及到随机数序列, 所以可以简单地并行计算而没有任何风险。 先查看本计算机的虚拟核心(线程)数:
library(parallel)detectCores()## [1] 8
用makeCluster()建立临时的有8个节点的单机集群:
nNodes <- 8cpucl <- makeCluster(nNodes)
用parSapply()或者parLapply()关于并行地循环:
f12 <- function(n=1000000){ f10 <- function(k=2, n=1000){ s <- 0.0 for(i in seq(n)) s <- s + 1/i^k s } nk <- 20 v <- parSapply(cpucl, 2:(nk+1), function(k) f10(k, n)) v}system.time(f12())[3]## elapsed## 1.19
并行版本速度提高了140%左右。
并行执行结束后, 需要解散临时的集群, 否则可能会有内存泄漏:
stopCluster(cpucl)
注意并行版本的程序还需要一些在每个计算节点上的初始化, 比如调入扩展包,定义函数, 初始化不同的随机数序列等。 parallel包的并行执行用的是不同的进程, 所以传送给每个节点的计算函数要包括所有的依赖内容。 比如,f2()中内嵌了f0()的定义, 如果不将f0()定义在f2()内部, 就需要预先将f0()的定义传递给每个节点。
parallel包的clusterExport()函数可以用来把计算所依赖的对象预先传送到每个节点。 比如, 上面的f2()可以不包含f0()的定义, 而是用clusterExport()预先传递:
cpucl <- makeCluster(nNodes)clusterExport(cpucl, c("f10"))f13 <- function(n=1000000){ nk <- 20 v <- parSapply(cpucl, 2:(nk+1), function(k) f10(k, n)) v}system.time(f13())[3]## elapsed ## 1.08 stopCluster(cpucl)
如果需要在每个节点预先执行一些语句, 可以用clusterEvalQ()函数执行,如
clusterEvalQ(cpucl, library(dplyr))
18.5.2 例2:使用相同随机数序列的并行计算
为了估计总体中某个比例的置信区间, 调查了一组样本, 在个受访者中选"是"的比例为。 令为标准正态分布的双侧分位数, 参数的近似置信区间为
称为Wilson置信区间。
假设要估计不同, , 情况下, 置信区间的覆盖率(即置信区间包含真实参数的概率)。 可以将这些参数组合定义成一个列表, 列表中每一项是一种参数组合, 对每一组合分别进行随机模拟,估计覆盖率。 因为不同参数组合之间没有互相依赖的关系, 随机数序列完全可以使用同一个序列。
不并行计算的程序示例:
wilson <- function(n, x, conf){ hatp <- x/n lam <- qnorm((conf+1)/2) lam2 <- lam^2 / n p1 <- (hatp + lam2/2)/(1 + lam2) delta <- lam / sqrt(n) * sqrt(hatp*(1-hatp) + lam2/4) / (1 + lam2) c(p1-delta, p1+delta)}f20 <- function(cpar){ set.seed(101) conf <- cpar[1] n <- cpar[2] p0 <- cpar[3] nsim <- 100000 cover <- 0 for(i in seq(nsim)){ x <- rbinom(1, n, p0) cf <- wilson(n, x, conf) if(p0 >= cf[1] && p0 <= cf[2]) cover <- cover+1 } cover/nsim}f21 <- function(){ dp <- rbind(rep(c(0.8, 0.9), each=4), rep(rep(c(30, 100), each=2), 2), rep(c(0.5, 0.1), 4)) lp <- as.list(as.data.frame(dp)) res <- sapply(lp, f20) res}system.time(f21())[3]## elapsed ## 4.3
约运行4.3秒。
改为并行版本:
library(parallel)nNodes <- 8cpucl <- makeCluster(nNodes)clusterExport(cpucl, c("f20", "wilson"))f22 <- function(){ dp <- rbind(rep(c(0.8, 0.9), each=4), rep(rep(c(30, 100), each=2), 2), rep(c(0.5, 0.1), 4)) lp <- as.list(as.data.frame(dp)) res <- parSapply(cpucl, lp, f20) res}system.time(f22())[3]## elapsed ## 1.25 stopCluster(cpucl)
运行约1.25秒, 速度提高240%左右。 这里模拟了8种参数组合, 每种参数组合模拟了十万次, 每种参数组合模拟所用的随机数序列是相同的。
18.5.3 例3:使用独立随机数序列的并行计算
大量的耗时的统计计算是随机模拟, 有时需要并行计算的部分必须使用独立的随机数序列。 比如,需要进行一千次重复模拟,每次使用不同的随机数序列, 可以将其分解为10组模拟,每组模拟一百万次, 这就要求这10组模拟使用的随机数序列不重复。
R中实现了L'Ecuyer的多步迭代复合随机数发生器, 此随机数发生器周期很长, 而且很容易将发生器的状态前进指定的步数。 parallel包的nextRNGStream()函数可以将该发生器前进到下一段的开始, 每一段都足够长, 可以用于一个节点。
以Wilson置信区间的模拟为例。 设, , , 取重复模拟次数为1千万次,估计Wilson置信区间的覆盖率。 单线程版本为:
f31 <- function(nsim=1E7){ set.seed(101) n <- 30; p0 <- 0.01; conf <- 0.95 cover <- 0 for(i in seq(nsim)){ x <- rbinom(1, n, p0) cf <- wilson(n, x, conf) if(p0 >= cf[1] && p0 <= cf[2]) cover <- cover+1 } cover/nsim}system.time(cvg1 <- f31())[3]## elapsed ## 42.61
单线程版本运行了大约43秒。
改成并行版本。 比例2多出的部分是为每个节点分别计算一个随机数种子将不同的种子传给不同节点。 parallel包的clusterApply()函数为临时集群的每个节点分别执行同一函数, 但对每个节点分别使用列表的不同元素作为函数的自变量。
library(parallel)nNodes <- 8cpucl <- makeCluster(nNodes)each.seed <- function(s){ assign(".Random.seed", s, envir = .GlobalEnv)}RNGkind("L'Ecuyer-CMRG")set.seed(101)seed0 <- .Random.seedseeds <- as.list(1:nNodes)for(i in 1:nNodes){ # 给每个节点制作不同的种子 seed0 <- nextRNGStream(seed0) seeds[[i]] <- seed0}## 给每个节点传送不同种子:junk <- clusterApply(cpucl, seeds, each.seed)f32 <- function(isim, nsimsub=10000){ n <- 30; p0 <- 0.01; conf <- 0.95 cover <- 0 for(i in seq(nsimsub)){ x <- rbinom(1, n, p0) cf <- wilson(n, x, conf) if(p0 >= cf[1] && p0 <= cf[2]) cover <- cover+1 } cover}clusterExport(cpucl, c("f32", "wilson"))f33 <- function(nsim=1E7){ nbatch <- 40 nsimsub <- nsim / nbatch cvs <- parSapply(cpucl, 1:nbatch, f32, nsimsub=nsimsub) print(cvs) sum(cvs)/(nsim*nbatch)}system.time(cvg2 <- f33())[3]## [1] 963759 963660 963885 963739 963714 964171 963615 963822 963720 963939## elapsed ## 13.63stopCluster(cpucl)
并行版本运行了大约14秒,速度提高约210%。 从两个版本各自一千万次重复模拟结果来看, 用随机模拟方法得到的覆盖率估计的精度大约为3位有效数字。
更大规模的随机模拟问题, 可以考虑使用多CPU的计算工作站或者服务器, 或用多台计算机通过高速局域网组成并行计算集群。
还有一种选择是租用云计算服务。
19 随机模拟
19.1 随机数
随机模拟是统计研究的重要方法, 另外许多现代统计计算方法(如MCMC)也是基于随机模拟的。 R中提供了多种不同概率分布的随机数函数, 可以批量地产生随机数。 一些R扩展包利用了随机模拟方法,如boot包进行bootstrap估计。
所谓随机数,实际是"伪随机数", 是从一组起始值(称为种子), 按照某种递推算法向前递推得到的。 所以,从同一种子出发,得到的随机数序列是相同的。
为了得到可重现的结果, 随机模拟应该从固定不变的种子开始模拟。 用set.seed(k)指定一个编号为k的种子, 这样每次从编号k种子运行相同的模拟程序可以得到相同的结果。
还可以用set.seed()加选项kind=指定后续程序要使用的随机数发生器名称, 用normal.kind=指定要使用的正态分布随机数发生器名称。
R提供了多种分布的随机数函数,如runif(n)产生n个标准均匀分布随机数, rnorm(n)产生n个标准正态分布随机数。 例如:
round(runif(5), 2)## [1] 0.44 0.56 0.93 0.23 0.22round(rnorm(5), 2)## [1] -0.20 1.10 -0.02 0.16 2.02
注意因为没有指定种子,每次运行会得到不同的结果。
在R命令行运行
?Distributions
可以查看R中提供的不同概率分布。
19.2 sample()函数
sample()函数从一个有限集合中无放回或有放回地随机抽取, 产生随机结果。
例如,为了设随机变量取值于正面反面, 且正面反面, 如下程序产生的10个随机抽样值:
sample(c('正面', '反面'), size=10, prob=c(0.7, 0.3), replace=TRUE)## [1] "反面" "反面" "反面" "反面" "正面"## [6] "正面" "正面" "正面" "反面" "反面"
sample()的选项size指定抽样个数, prob指定每个值的概率, replace=TRUE说明是有放回抽样。
如果要做无放回等概率的随机抽样, 可以不指定prob和replace(缺省是FALSE)。 比如,下面的程序从1:10随机抽取4个:
sample(1:10, size=4)## [1] 1 5 8 10
如果要从中等概率无放回随机抽样直到每一个都被抽过,只要用如:
sample(10)## [1] 3 5 9 2 10 7 4 1 6 8
这实际上返回了的一个重排。
dplyr包的sample_n()函数与sample()类似, 但输入为数据框, 输出为随机抽取的数据框行子集。
19.3 随机模拟示例
19.3.1 线性回归模拟
考虑如下线性回归模型
假设有样本量lm() 。 样本可以模拟产生。
模型中的自变量可以用随机数产生, 比如,用sample()函数从中随机有放回地抽取个。 模型中的随机误差项可以用rnorm()产生。 产生一组样本的程序如:
n <- 10; a <- 10; b <- 2x <- sample(1:10, size=n, replace=TRUE)eps <- rnorm(n, 0, 0.5)y <- a + b * x + eps
如下程序计算线性回归:
lm(y ~ x)## ## Call:## lm(formula = y ~ x)## ## Coefficients:## (Intercept) x ## 9.697 2.054
如下程序计算线性回归的多种统计量:
summary(lm(y ~ x))## ## Call:## lm(formula = y ~ x)## ## Residuals:## Min 1Q Median 3Q Max ## -1.1066 -0.5459 0.2078 0.4937 0.8185 ## ## Coefficients:## Estimate Std. Error t value Pr(>|t|) ## (Intercept) 9.69730 0.43855 22.11 1.85e-08 ***## x 2.05418 0.08634 23.79 1.04e-08 ***## ---## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1## ## Residual standard error: 0.6929 on 8 degrees of freedom## Multiple R-squared: 0.9861, Adjusted R-squared: 0.9843 ## F-statistic: 566 on 1 and 8 DF, p-value: 1.037e-08
如下程序返回一个矩阵, 包括的估计值、标准误差、t检验统计量、检验p值:
summary(lm(y ~ x))$coefficients## Estimate Std. Error t value Pr(>|t|)## (Intercept) 9.697300 0.43855145 22.11211 1.848411e-08## x 2.054176 0.08633973 23.79178 1.037188e-08
如下程序把上述矩阵的前两列拉直成一个向量返回:
c(summary(lm(y ~ x))$coefficients[,1:2])## [1] 9.69729998 2.05417628 0.43855145 0.08633973
这样得到 这四个值。
用replicate(, 复合语句)执行多次模拟, 返回向量或矩阵结果, 返回矩阵时,每列是一次模拟的结果。 下面是线性回归整个模拟程序,写成了一个函数。
reg.sim <- function( a=10, b=2, sigma=0.5, n=10, B=1000){ set.seed(1) resm <- replicate(B, { x <- sample(1:10, size=n, replace=TRUE) eps <- rnorm(n, 0, 0.5) y <- a + b * x + eps c(summary(lm(y ~ x))$coefficients[,1:2]) }) resm <- t(resm) colnames(resm) <- c('a', 'b', 'SE.a', 'SE.b') cat(B, '次模拟的平均值:\n') print( apply(resm, 2, mean) ) cat(B, '次模拟的标准差:\n') print( apply(resm, 2, sd) )}
运行测试:
set.seed(1)reg.sim()## 1000 次模拟的平均值:## a b SE.a SE.b ## 9.9970476 1.9994490 0.3639505 0.0592510 ## 1000 次模拟的标准差:## a b SE.a SE.b ## 0.37974881 0.06297733 0.11992515 0.01795926
可以看出,标准误差作为的标准差估计, 与多次模拟得到多个样本计算得到的标准差估计是比较接近的。 结果中的平均值为0.363, 1000次模拟的的样本标准差为0.393,比较接近; 的平均值为0.0594, 1000次模拟的的样本标准差为0.0637,比较接近。
19.3.2 核密度的bootstrap置信区间
R自带的数据框faithful内保存了美国黄石国家公园Faithful火山的272次爆发持续时间和间歇时间。 为估计爆发持续时间的密度,可以用核密度估计方法, R函数density可以执行此估计, 返回个格子点上的密度曲线坐标:
x <- faithful$eruptionsest0 <- density(x)plot(est0)

这个密度估计明显呈现出双峰形态。
核密度估计是统计估计, 为了得到其置信区间(给定每个坐标,真实密度的单点的置信区间), 采用如下非参数bootstrap方法:
重复次, 每次从原始样本中有重复地抽取与原来大小相同的一组样本, 对这组样本计算核密度估计, 结果为, 每组样本估计个格子点的密度曲线坐标, 横坐标不随样本改变。
对每个横坐标, 取bootstrap得到的个的0.025和0.975样本分位数, 作为真实密度的bootstrap置信区间。
在R中利用replicate()函数实现:
set.seed(1)resm <- replicate(10000, { x1 <- sample(x, replace=TRUE) density(x1, from=min(est0$x), to=max(est0$x))$y})CI <- apply(resm, 1, quantile, c(0.025, 0.975))plot(est0, ylim=range(CI), type='n')polygon(c(est0$x, rev(est0$x)), c(CI[1,], rev(CI[2,])), col='grey', border=FALSE)lines(est0, lwd=2)

程序中用set.seed(1)保证每次运行得到的结果是不变的, replicate()函数第一参数是重复模拟次数, 第二参数是复合语句, 这些语句是每次模拟要执行的计算。 在每次模拟中, 用带有replace=TRUE选项的sample()函数从样本中有放回地抽样得到一组bootstrap样本, 每次模拟的结果是在指定格子点上计算的核密度估计的纵坐标。 replicate()的结果为一个矩阵, 每一列是一次模拟得到的纵坐标集合。 对每个横坐标格子点,用quantile()函数计算个bootstrap样本的2.5%和97.5%分位数, 循环用apply()函数表示。 polygon()函数指定一个多边形的顺序的顶点坐标用col=指定的颜色填充, 本程序中实现了置信下限与置信上限两条曲线之间的颜色填充。 lines()函数绘制了与原始样本对应的核密度估计曲线。