R 语言实战(第2版)第一部分 入门


第一部分 入门

第 1 章 R 语言介绍

本章内容
❑ R 的安装
❑ 熟悉 R 语言
❑ 运行 R 程序

1.1 为何要使用 R

1.2 R 的获取和安装

1.3 R 的使用

R是一种区分大小写的解释型语言。你可以在命令提示符(>)后每次输入并执行一条命令,或者一次性执行写在脚本文件中的一组命令。R中有多种数据类型,包括向量、矩阵、数据框(与数据集类似)以及列表(各种对象的集合)。将在第 2 章中讨论这些数据类型。

R中的多数功能是由程序内置函数、用户自编函数和对对象的创建和操作所提供的。一个对象可以是任何能被赋值的东西。对于R来说,对象可以是任何东西(数据、函数、图形、分析结果,等等)。每一个对象都有一个类属性,类属性可以告诉R怎么对之进行处理。

一次交互式会话期间的所有数据对象都被保存在内存中。一些基本函数是默认直接可用的,而其他高级函数则包含于按需加载的程序包中。

R语句由函数和赋值构成。R使用<-,而不是传统的=作为赋值符号。例如,以下语句:

x <- rnorm(5)

创建了一个名为x的向量对象,它包含5个来自标准正态分布的随机偏差

x <- rnorm(5)
x
[1] -2.4698221 -0.5653149 0.2730340 1.1408878
[5] 0.6336379

注意 R允许使用=为对象赋值,但是这样写的R程序并不多,因为它不是标准语法。一些情况下,用等号赋值会出现问题,R程序员可能会因此取笑你。你还可以反转赋值方向。例如,rnorm(5) -> x 与上面的语句等价。重申一下,使用等号赋值的做法并不常见,不推荐使用。

1.3.1 新手上路

如果你使用的是 Windows,从开始菜单中启动 R。在 Mac 上,则需要双击应用程序文件夹中的R图标。对于 Linux,在终端窗口中的命令提示符下敲入 R 并回车。这些方式都可以启动R.

我们通过一个简单的虚构示例来直观地感受一下这个界面。假设我们正在研究生理发育问题,并收集了 10 名婴儿在出生后一年内的月龄和体重数据(见表1-1)。我们感兴趣的是体重的分布及体重和月龄的关系。

表1-1 10名婴儿的月龄和体重

年龄(月) 体重(kg) 年龄(月) 体重(kg)
01 4.4 09 7.3
03 5.3 03 6.0
05 7.2 09 10.4
02 5.2 12 10.2
11 8.5 03 6.1

注:以上为虚构数据。

代码清单1-1 给出了分析的过程。可以使用函数 c() 以向量的形式输入月龄和体重数据,此函数可将其参数组合成一个向量或列表。然后用 mean()、sd() 和 cor() 函数分别获得体重的均值和标准差,以及月龄和体重的相关度。最后使用 plot() 函数,从而用图形展示月龄和体重的关系,这样就可以用可视化的方式检查其中可能存在的趋势。函数 q() 将结束会话并允许你退出 R。

代码清单1-1 一个 R 会话示例

age <- c(1,3,5,2,11,9,3,9,12,3)
weight <- c(4.4,5.3,7.2,5.2,8.5,7.3,6.0,10.4,10.2,6.1)
mean(weight)

输出:[1] 7.06

sd(weight)

输出:[1] 2.077498

cor(age,weight)

输出:[1] 0.9075655

plot(age,weight)
q()

从代码清单1-1 中可以看到,这 10 名婴儿的平均体重是 7.06kg,标准差为 2.08kg,月龄和体重之间存在较强的线性关系(相关度=0.91)。这种关系也可以从图1-4所示的散点图中看到。不出意料,随着月龄的增长,婴儿的体重也趋于增加。

散点图1-4 的信息量充足,但过于“功利”,也不够美观。接下来的几章里,我们会讲到如何自定义图形以契合需要。

小提示 若想大致了解R能够作出何种图形,在命令行中运行demo()即可。生成的部分图形如图1-5所示。其他的演示还有demo(Hershey)、demo(persp)和demo(image)。要看到完整的演示列表,不加参数直接运行demo()即可。

图1-4 婴儿体重(千克)和年龄(月)的散点图 图1-5 函数demo()绘制的图形示例
1.3.2 获取帮助

R提供了大量的帮助功能,学会如何使用这些帮助文档可以在相当程度上助力你的编程工作。R的内置帮助系统提供了当前已安装包中所有函数①的细节、参考文献以及使用示例。你可以通过表1-2中列出的函数查看帮助文档。

① 确切地说,这里的“所有”是指那些已导出的(exported)、对用户可见的函数。

表1-2 R 中的帮助函数

函 数 功 能
help.start() 打开帮助文档首页
help(“foo”)或?foo 查看函数 foo 的帮助(引号可以省略)
help.search(“foo”)或??foo 以 foo 为关键词搜索本地帮助文档
example(“foo”) 函数 foo 的使用示例(引号可以省略)
RSiteSearch(“foo”) 以 foo 为关键词搜索在线文档和邮件列表存档
apropos(“foo”, mode=”function”) 列出名称中含有 foo 的所有可用函数
data() 列出当前已加载包中所含的所有可用示例数据集
vignette() 列出当前已安装包中所有可用的 vignette 文档
vignette(“foo”) 为主题 foo 显示指定的 vignette 文档

函数 help.start() 会打开一个浏览器窗口,我们可在其中查看入门和高级的帮助手册、常见问题集,以及参考材料。函数 RSiteSearch() 可在在线帮助手册和 R-Help 邮件列表的讨论存档中搜索指定主题,并在浏览器中返回结果。由函数 vignette() 函数返回的 vignette 文档一般是 PDF 格式的实用介绍性文章。不过,并非所有的包都提供了 vignette 文档。不难发现,R 提供了大量的帮助功能,学会如何使用这些帮助文档,毫无疑问有助于编程。我经常使用?来查看某些函数的功能(如选项或返回值)。

1.3.3 工作空间

工作空间(workspace)就是当前 R 的工作环境,它存储着所有用户定义的对象(向量、矩阵、函数、数据框、列表)。在一个 R 会话结束时,你可以将当前工作空间保存到一个镜像中,并在下次启动R时自动载入它。各种命令可在R命令行中交互式地输入。使用上下方向键查看已输入命令的历史记录。这样我们就可以选择一个之前输入过的命令并适当修改,最后按回车重新执行它。

当前的工作目录(working directory)是 R 用来读取文件和保存结果的默认目录。我们可以使用函数 getwd() 来查看当前的工作目录,或使用函数 setwd() 设定当前的工作目录。如果需要读入一个不在当前工作目录下的文件,则需在调用语句中写明完整的路径。记得使用引号闭合这些目录名和文件名。用于管理工作空间的部分标准命令见表1-3。

表1-3 用于管理R工作空间的函数

函 数 功 能
getwd() 显示当前的工作目录
setwd(“mydirectory”) 修改当前的工作目录为 mydirectory
ls() 列出当前工作空间中的对象
rm(objectlist) 移除(删除)一个或多个对象
help(options) 显示可用选项的说明
options() 显示或设置当前选项
history(#) 显示最近使用过的 # 个命令(默认值为 25)
savehistory(“myfile”) 保存命令历史到文件 myfile 中(默认值为.Rhistory)
loadhistory(“myfile”) 载入一个命令历史文件(默认值为.Rhistory)
save.image(“myfile”) 保存工作空间到文件 myfile 中(默认值为.RData)
save(objectlist, file=”myfile”) 保存指定对象到一个文件中
load(“myfile”) 读取一个工作空间到当前会话中(默认值为.RData)
q() 退出 R。将会询问你是否保存工作空间

要了解这些命令是如何运作的,运行代码清单1-2 中的代码并查看结果。

代码清单1-2 用于管理 R 工作空间的命令使用示例

setwd("D:/Rprojects") 
options()
options(digits=3) 
x <- runif(20) 
summary(x) 
hist(x)
q()

首先,当前工作目录被设置为 D:/Rprojects,当前的选项设置情况将显示出来,而数字将被格式化,显示为具有小数点后三位有效数字的格式。然后,我们创建了一个包含 20 个均匀分布随机变量的向量,生成了此数据的摘要统计量和直方图。当 q() 函数被运行的时候,程序将向用户询问是否保存工作空间。如果用户输入y,命令的历史记录保存到文件 .Rhistory 中,工作空间(包含向量x)保存到当前目录中的文件 .RData 中,会话结束,R程序退出。

注意 setwd() 命令的路径中使用了正斜杠。R将反斜杠(\)作为一个转义符。即使你在Windows 平台上运行 R,在路径中也要使用正斜杠。同时注意,函数 setwd() 不会自动创建一个不存在的目录。如果必要的话,可以使用函数 dir.create() 来创建新目录,然后使用 setwd() 将工作目录指向这个新目录。

在独立的目录中保存项目是一个好主意。你也许会在启动一个 R 会话时使用 setwd() 命令指定到某一个项目的路径,后接不加选项的 load(“.RData”) 命令。这样做可以让你从上一次会话结束的地方重新开始,并保证各个项目之间的数据和设置互不干扰。在 Windows 和 Mac OS X 平台上就更简单了。跳转到项目所在目录并双击之前保存的镜像文件即可。这样做可以启动 R,载入保存的工作空间,并设置当前工作目录到这个文件夹中。

1.3.4 输入和输出

启动 R 后将默认开始一个交互式的会话,从键盘接受输入并从屏幕进行输出。不过你也可以处理写在一个脚本文件(一个包含了R语句的文件)中的命令集并直接将结果输出到多类目标中。

  1. 输入

函数 source(“filename”) 可在当前会话中执行一个脚本。如果文件名中不包含路径,R 将假设此脚本在当前工作目录中。举例来说,source(“myscript.R”) 将执行包含在文件 myscript.R 中的 R 语句集合。依照惯例,脚本文件以.R作为扩展名,不过这并不是必需的。

  1. 文本输出

函数 sink(“filename”) 将输出重定向到文件 filename 中。默认情况下,如果文件已经存在, 则它的内容将被覆盖。使用参数 append=TRUE 可以将文本追加到文件后,而不是覆盖它。参数 split=TRUE 可将输出同时发送到屏幕和输出文件中。不加参数调用命令 sink() 将仅向屏幕返回输出结果。

  1. 图形输出

虽然 sink() 可以重定向文本输出,但它对图形输出没有影响。要重定向图形输出,使用表1-4 中列出的函数即可。最后使用 dev.off() 将输出返回到终端。

表1-4 用于保存图形输出的函数

函 数 输 出
bmp(“filename.bmp”) BMP 文件
jpeg(“filename.jpg”) JPEG 文件
pdf(“filename.pdf”) PDF 文件
png(“filename.png”) PNG 文件
postscript(“filename.ps”) PostScript 文件
svg(“filename.svg”) SVG 文件
win.metafile(“filename.wmf”) Windows 图元文件

让我们通过一个示例来了解整个流程。假设我们有包含R代码的三个脚本文件 script1.R、

script2.R 和 script3.R。执行语句:

source("script1.R")

将会在当前会话中执行 script1.R 中的 R 代码,结果将出现在屏幕上。

如果执行语句:

sink("myoutput", append=TRUE, split=TRUE) 
pdf("mygraphs.pdf")
source("script2.R")

文件 script2.R 中的 R 代码将执行,结果也将显示在屏幕上。除此之外,文本输出将被追加到文件 myoutput 中,图形输出将保存到文件 mygraphs.pdf 中。

最后,如果我们执行语句:

sink() 
dev.off()
source("script3.R")

文件 script3.R 中的 R 代码将执行,结果将显示在屏幕上。这一次,没有文本或图形输出保存到文件中。整个流程大致如图1-6 所示

图1-6 使用函数source()进行输入并使用函数sink()进行输出

R 对输入来源和输出走向的处理相当灵活,可控性很强。

1.4 包

R 提供了大量开箱即用的功能,但它最激动人心的一部分功能是通过可选模块的下载和安装来实现的。目前有 5500 多个称为包( package ) 的用户贡献模块可从 http://cran.r-project.org/web/packages 下载。这些包提供了横跨各种领域、数量惊人的新功能,包括分析地理数据、处理蛋白质质谱,甚至是心理测验分析的功能。

1.4.1 什么是包

包是 R 函数、数据、预编译代码以一种定义完善的格式组成的集合。计算机上存储包的目录称为库(library)。函数 .libPaths() 能够显示库所在的位置, 函数 library() 则可以显示库中有哪些包。

R自带了一系列默认包(包括 base、datasets、utils、grDevices、graphics、stats 以及methods),它们提供了种类繁多的默认函数和数据集。其他包可通过下载来进行安装。安装好以后,它们必须被载入到会话中才能使用。命令 search() 可以告诉你哪些包已加载并可使用。

1.4.2 包的安装

有许多R函数可以用来管理包。第一次安装一个包,使用命令 install.packages() 即可。举例来说,不加参数执行命令 install.packages() 将显示一个 CRAN 镜像站点的列表,选择 其中一个镜像站点之后,将看到所有可用包的列表,选择其中的一个包即可进行下载和安装。 如果知道自己想安装的包的名称,可以直接将包名作为参数提供给这个函数。例如,包 gclus 中提供了创建增强型散点图的函数。可以使用命令 install.packages(“gclus”) 来下载和安装它。

一个包仅需安装一次。但和其他软件类似,包经常被其作者更新。使用命令 update.packages() 可以更新已经安装的包。要查看已安装包的描述,可以使用 installed.packages()命令,这将列出安装的包,以及它们的版本号、依赖关系等信息。

1.4.3 包的载入

包的安装是指从某个 CRAN 镜像站点下载它并将其放入库中的过程。要在 R 会话中使用它, 还需要使用 library() 命令载入这个包。例如,要使用 gclus 包,执行命令 library(gclus) 即可。当然,在载入一个包之前必须已经安装了这个包。在一个会话中,包只需载入一次。如果需要,你可以自定义启动环境以自动载入会频繁使用的那些包。启动环境的自定义在附录 B 中有详细描述。

1.4.4 包的使用方法

载入一个包之后,就可以使用一系列新的函数和数据集了。包中往往提供了演示性的小型数据集和示例代码,能够让我们尝试这些新功能。帮助系统包含了每个函数的一个描述(同时带有示例),每个数据集的信息也被包括其中。命令 help(package=”package_name”) 可以输出某个包的简短描述以及包中的函数名称和数据集名称的列表。使用函数help()可以查看其中任意函数或数据集的更多细节。这些信息也能以 PDF 帮助手册的形式从 CRAN 下载。

R语言编程中的常见错误

有一些错误是R的初学者和经验丰富的R程序员都可能常犯的。如果程序出错了,请检查以下几方面。

使用了错误的大小写。help()、Help() 和 HELP() 是三个不同的函数(只有第一个是正确的)。

忘记使用必要的引号。install.packages(“gclus”) 能够正常执行,然而

Install.packages(gclus) 将会报错。

在函数调用时忘记使用括号。例如,要使用 help() 而非 help。即使函数无需参数,仍需加上 ()。

在Windows 上,路径名中使用了\ R 将反斜杠视为一个转义字符。

setwd(“c:\mydata”) 会报错。正确的写法是 setwd(“c:/mydata”) 或 setwd(“c:\ \mydata”)。

使用了一个尚未载入包中的函数。函数 order.clusters() 包含在包 gclus 中。如果还没有载入这个包就使用它,将会报错。

R 的报错信息可能是含义模糊的,但如果谨慎遵守了以上要点,就应该可以避免许多错误。

1.5 批处理

多数情况下,我们都会交互式地使用 R:在提示符后输入命令,接着等待该命令的输出结果。偶尔,我们可能想要以一种重复的、标准化的、无人值守的方式执行某个R程序。例如,你可能需要每个月生成一次相同的报告,这时就可以在R中编写程序,在批处理模式下执行它。

如何以批处理模式运行R与使用的操作系统有关。在Linux或Mac OS X系统下,可以在终端窗口中使用如下命令:

R CMD BATCH options infile outfile

其中 infile 是包含了要执行的R代码所在文件的文件名,outfile 是接收输出文件的文件名,

options 部分则列出了控制执行细节的选项。依照惯例,infile 的扩展名是 .R, outfile 的扩展名为 .Rout。

对于Windows,则需使用:

"C:\Program Files\R\R-3.1.0\bin\R.exe" CMD BATCH --vanilla --slave "c:\my projects\myscript.R"

将路径调整为 R.exe 所在的相应位置和脚本文件所在位置。要进一步了解如何调用 R,包括命令行选项的使用方法,请参考 CRAN(http://cran.r-project.org)上的文档 “Introduction to R” ①。

① 中文版文档名为“R 导论”。CRAN 上的下载地址为http://cran.r-project.org/doc/contrib/Ding-R-intro_cn.pdf。

1.6 将输出用为输入:结果的重用

R的一个非常实用的特点是,分析的输出结果可轻松保存,并作为进一步分析的输入使用。让我们通过一个R中已经预先安装好的数据集作为示例阐明这一点。如果你无法理解这里涉及的统计知识,也别担心,我们在这里关注的只是一般原理。

首先,利用汽车数据 mtcars 执行一次简单线性回归,通过车身重量(wt)预测每加仑行驶的英里数(mpg)。可以通过以下语句实现:

lm(mpg~wt, data=mtcars)

结果将显示在屏幕上,不会保存任何信息。

下一步,执行回归,区别是在一个对象中保存结果:

lmfit <- lm(mpg~wt, data=mtcars)

以上赋值语句创建了一个名为 lmfit 的列表对象,其中包含了分析的大量信息(包括预测值、残差、回归系数等)。虽然屏幕上没有显示任何输出,但分析结果可在稍后被显示和继续使用。

键入 summary(lmfit) 将显示分析结果的统计概要,plot(lmfit) 将生成回归诊断图形, 而语句 cook<-cooks.distance(lmfit) 将计算和保存影响度量统计量①,plot(cook) 对其绘图。要在新的车身重量数据上对每加仑行驶的英里数进行预测,不妨使用 predict(lmfit, mynewdata)。

要了解某个函数的返回值,查阅这个函数在线帮助文档中的 “Value” 部分即可。本例中应当查阅 help(lm) 或 ?lm 中的对应部分。这样就可以知道将某个函数的结果赋值到一个对象时, 保存下来的结果具体是什么。

① 这里使用了 Cook 距离作为度量影响的统计量,详见第8章。

1.7 处理大数据集

程序员经常问我R是否可以处理大数据问题。他们往往需要处理来自互联网、气候学、遗传学等研究领域的海量数据。由于 R 在内存中存储对象,往往会受限于可用的内存量。举例来说, 在我服役了 5 年的 2G 内存 Windows PC 上,我可以轻松地处理含有 1000 万个元素的数据集(100 个变量 × 100 000 个观测)。在一台 4 G 内存的 iMac 上,我通常可以不费力地处理含有上亿元素的数据。

但是也要考虑到两个问题:数据集的大小和要应用的统计方法。R 可以处理 GB 级到 TB 级的数据分析问题,但需要专门的手段。大数据集的管理和分析问题留待附录 F 中讨论。

1.8 示例实践

我们将以一个结合了以上各种命令的示例结束本章。以下是任务描述。

(1) 打开帮助文档首页,并查阅其中的 “Introduction to R”。

(2) 安装 vcd 包(一个用于可视化类别数据的包,将在第 11 章中使用)。

(3) 列出此包中可用的函数和数据集。

(4) 载入这个包并阅读数据集 Arthritis 的描述。

(5) 显示数据集Arthritis的内容(直接输入一个对象的名称将列出它的内容)。

(6) 运行数据集 Arthritis 自带的示例。如果不理解输出结果,也不要担心。它基本上显示了接受治疗的关节炎患者较接受安慰剂的患者在病情上有了更多改善。

(7) 退出。

所需的代码如代码清单1-3 所示,图1-7 显示了结果的示例。如本例所示,我们只需使用少量 R 代码即可完成大量工作。

代码清单1-3 使用一个新的包

help.start() 
install.packages("vcd") 
help(package="vcd") 
library(vcd) 
help(Arthritis) 
Arthritis 
example(Arthritis)
q()
图1-7 代码清单1-3 的输出。(从左至右)为关节炎示例的输出结果、帮助文档首页、vcd 包的信息、Arthritis 数据集的信息,以及一幅展示关节炎治疗情况和治疗结果之间关系的图

图1-7 代码清单1-3 的输出。(从左至右)为关节炎示例的输出结果、帮助文档首页、vcd 包的信息、Arthritis 数据集的信息,以及一幅展示关节炎治疗情况和治疗结果之间关系的图

1.9 小结

本章中,我们了解了 R 的一些优点,正是这些优点吸引了学生、研究者、统计学家以及数据分析师等希望理解数据所具有意义的人。我们从程序的安装出发,讨论了如何通过下载附加包来增强R的功能。探索了 R 的基本界面,以交互和批处理两种方式运行了 R 程序,并绘制了一些示例图形。还学习了如何将工作保存到文本和图形文件中。由于 R 的复杂性,我们花了一些时间来了解如何访问大量现成可用的帮助文档。希望你对这个免费软件的强大之处有了一个总体的感觉。既然已经能够正常运行R,那么是时候把玩你自己的数据了。在下一章中,我们将着眼于 R 能够处理的各种数据类型,以及如何从文本文件、其他程序和数据库管理系统中导入数据。

第2章 创建数据集

本章内容
❑ 探索 R 中的数据结构
❑ 输入数据
❑ 导入数据
❑ 标注数据

按照个人要求的格式来创建含有研究信息的数据集,这是任何数据分析的第一步。在 R 中, 这个任务包括以下两步:

  • 选择一种数据结构来存储数据;

  • 将数据输入或导入到这个数据结构中。

本章的第一部分(2.1~2.2节)叙述了 R 中用于存储数据的多种结构。其中,2.2节描述了向量、因子、矩阵、数据框以及列表的用法。熟悉这些数据结构(以及访问其中元素的表述方法) 将十分有助于了解 R 的工作方式,因此你可能需要耐心消化这一节的内容。

本章的第二部分(2.3节)涵盖了多种向R中导入数据的可行方法。可以手工输入数据,亦可从外部源导入数据。数据源可为文本文件、电子表格、统计软件和各类数据库管理系统。举例来说,我在工作中使用的数据往往来自于 SQL 数据库。偶尔,我也会接受从 DOS 时代遗留下的数据, 或是从现有的 SAS 和 SPSS 中导出的数据。通常,你仅仅需要本节中描述的一两种方法,因此根据需求有选择地阅读即可。

创建数据集后,往往需要对它进行标注,也就是为变量和变量代码添加描述性的标签。本章的第三部分(2.4节)将讨论数据集的标注问题,并介绍一些处理数据集的实用函数(2.5节)。下面我们从基本知识讲起。

2.1 数据集的概念

数据集通常是由数据构成的一个矩形数组,行表示观测,列表示变量。表2-1 提供了一个假想的病例数据集。

表2-1 病例数据

病人编号 入院时间 年龄 糖尿病类型 病情
(PatientID) (AdmDate) (Age) (Diabetes) (Status)
1 10/15/2009 25 Type 1 Poor
2 11/01/2009 34 Type 2 Improved
3 10/21/2009 28 Type 1 Excellent
4 10/28/2009 52 Type 1 Poor

不同的行业对于数据集的行和列叫法不同。统计学家称它们为观测(observation)和变量(variable),数据库分析师则称其为记录(record)和字段(field),数据挖掘和机器学习学科的研究者则把它们叫作示例(example)和属性(attribute)。我们在本书中通篇使用术语观测和变量。

你可以清楚地看到此数据集的结构(本例中是一个矩形数组)以及其中包含的内容和数据类型。在表2-1 所示的数据集中,PatientID 是行/实例标识符,AdmDate 是日期型变量,Age 是连续型变量,Diabetes 是名义型变量,Status 是有序型变量。

R中有许多用于存储数据的结构,包括标量、向量、数组、数据框和列表。表2-1实际上对应着R中的一个数据框。多样化的数据结构赋予了 R 极其灵活的数据处理能力。

R可以处理的数据类型(模式)包括数值型、字符型、逻辑型(TRUE/FALSE)、复数型(虚数)和原生型(字节)。在 R 中,PatientID、AdmDate 和 Age 为数值型变量,而Diabetes 和 Status 则为字符型变量。另外,你需要分别告诉 R:PatientID 是实例标识符,AdmDate 含有日期数据,Diabetes 和 Status 别是名义型和有序型变量。R将实例标识符称为 rownames(行名),将类别型(包括名义型和有序型)变量称为因子(factors)。在下一节中讲解这些内容,并在第 3 章中介绍日期型数据的处理。

2.2 数据结构

R 拥有许多用于存储数据的对象类型,包括标量、向量、矩阵、数组、数据框和列表。它们在存储数据的类型、创建方式、结构复杂度,以及用于定位和访问其中个别元素的标记等方面均有所不同。图2-1给出了这些数据结构的一个示意图。

图2-1 R 中的数据结构

让我们从向量开始,逐个探究每一种数据结构。

一些定义

R 中有一些术语较为独特,可能会对新用户造成困扰。

在 R 中,对象(object)是指可以赋值给变量的任何事物,包括常量、数据结构、函数, 甚至图形。对象都拥有某种模式,描述了此对象是如何存储的,以及某个,像 print 这样的泛型函数表明如何处理此对象。

与其他标准统计软件(如SAS、SPSS和Stata)中的数据集类似,数据框(data frame)是 R 中用于存储数据的一种结构:列表示变量,行表示观测。在同一个数据框中可以存储不同类型(如数值型、字符型)的变量。数据框将是你用来存储数据集的主要数据结构。

因子(factor)是名义型变量或有序型变量。它们在 R 中被特殊地存储和处理。你将在2.2.5 节中学习因子的处理。

其他多数术语你应该比较熟悉了,它们基本都遵循统计和计算中术语的定义。

2.2.1 向量

向量是用于存储数值型、字符型或逻辑型数据的一维数组。执行组合功能的函数 c() 可用来创建向量。各类向量如下例所示:

a <- c(1, 2, 5, 3, 6, -2, 4)
b <- c("one", "two", "three")
c <- c(TRUE, TRUE, TRUE, FALSE, TRUE, FALSE)
输出内容

这里,a 是数值型向量,b 是字符型向量,而 c 是逻辑型向量。注意,单个向量中的数据必须拥有相同的类型或模式(数值型、字符型或逻辑型)。同一向量中无法混杂不同模式的数据。

注意: 标量是只含一个元素的向量,例如 f <- 3、g <- “US” 和h <- TRUE。它们用于保存常量。

通过在方括号中给定元素所处位置的数值,我们可以访问向量中的元素。例如,a[c(2, 4)] 用于访问向量 a 中的第二个和第四个元素。更多示例如下:

> a <- c("k", "j", "h", "a", "c", "m")
> 
> a[3]
[1] "h"
> 
> a[c(1, 3, 5)]
[1] "k" "h" "c"
> 
> a[2:6]
[1] "j" "h" "a" "c" "m"

最后一个语句中使用的冒号用于生成一个数值序列。例如,a <- c(2:6) 等价于 a <- c(2, 3, 4, 5, 6)。

> a <- c(2:6)
> a
[1] 2 3 4 5 6
2.2.2 矩阵

矩阵是一个二维数组,只是每个元素都拥有相同的模式(数值型、字符型或逻辑型)。可通过函数 matrix() 创建矩阵。一般使用格式为:

myymatrix <- matrix(vector, nrow=number_of_rows, ncol=number_of_columns, byrow=logical_value, dimnames=list(char_vector_rownames, char_vector_colnames))

其中 vector 包含了矩阵的元素,nrow 和 ncol 用以指定行和列的维数,dimnames 包含了可选的、以字符型向量表示的行名和列名。选项 byrow 则表明矩阵应当按行填充(byrow=TRUE) 还是按列填充(byrow=FALSE),默认情况下按列填充。代码清单2-1中的代码演示了matrix 函数的用法。

代码清单2-1 创建矩阵

> y <- matrix(1:20, nrow=5, ncol=4)
> y
     [,1] [,2] [,3] [,4]
[1,]    1    6   11   16
[2,]    2    7   12   17
[3,]    3    8   13   18
[4,]    4    9   14   19
[5,]    5   10   15   20
> cells  <- c(1,26,24,68)
> rnames  <- c("R1", "R2")
> cnames  <- c("C1", "C2")
> mymatrix <- matrix(cells, nrow=2, ncol=2, byrow=TRUE, dimnames=list(rnames, cnames))
> mymatrix
   C1 C2
R1  1 26
R2 24 68
> mymatrix <- matrix(cells, nrow=2, ncol=2, byrow=FALSE, dimnames=list(rnames, cnames))
> mymatrix
   C1 C2
R1  1 24
R2 26 68
变量信息

我们首先创建了一个 5×4 的矩阵,接着创建了一个 2×2 的含列名标签的矩阵,并按行进行填充,最后创建了一个 2×2 的矩阵并按列进行了填充。

我们可以使用下标和方括号来选择矩阵中的行、列或元素。X[i,] 指矩阵 X 中的第 i 行,X[,j] 指第j列,X[i, j] 指第 i 行第 j 个元素。选择多行或多列时,下标i和j可为数值型向量,如代码清单2-2 所示。

代码清单2-2 矩阵下标的使用

> x <- matrix(1:10, nrow=2)
> x
     [,1] [,2] [,3] [,4] [,5]
[1,]    1    3    5    7    9
[2,]    2    4    6    8   10
> x[2,]
[1]  2  4  6  8 10
> x[,2]
[1] 3 4
> x[1,4]
[1] 7
> x[1, c(4,5)] 
[1] 7 9
变量信息

首先,我们创建了一个内容为数字 1 到 10 的 2×5 矩阵。默认情况下,矩阵按列填充。然后,我们分别选择了第二行和第二列的元素。接着,又选择了第一行第四列的元素。最后选择了位于第一行第四、第五列的元素。

矩阵都是二维的,和向量类似,矩阵中也仅能包含一种数据类型。当维度超过2时,不妨使用数组(2.2.3 节)。当有多种模式的数据时,你们可以使用数据框(2.2.4 节)。

2.2.3 数组

数组(array)与矩阵类似,但是维度可以大于 2。数组可通过 array 函数创建,形式如下:

myarray <- array(vector, dimensions, dimnames)

其中 vector 包含了数组中的数据, dimensions 是一个数值型向量,给出了各个维度下标的最大值,而 dimnames 是可选的、各维度名称标签的列表。代码清单2-3 给出了一个创建三维(2×3×4) 数值型数组的示例。

代码清单2-3 创建一个数组

> dim1 <- c("A1", "A2")
> dim2 <- c("B1", "B2", "B3")
> dim3 <- c("C1", "C2", "C3", "C4")
> z <- array(1:24, c(2, 3, 4), dimnames=list(dim1, dim2, dim3))
> z
, , C1

   B1 B2 B3
A1  1  3  5
A2  2  4  6

, , C2

   B1 B2 B3
A1  7  9 11
A2  8 10 12

, , C3

   B1 B2 B3
A1 13 15 17
A2 14 16 18

, , C4

   B1 B2 B3
A1 19 21 23
A2 20 22 24

如你所见,数组是矩阵的一个自然推广。它们在编写新的统计方法时可能很有用。像矩阵一样,数组中的数据也只能拥有一种模式。从数组中选取元素的方式与矩阵相同。上例中,元素z[1,2,3]为15。

2.2.4 数据框

由于不同的列可以包含不同模式(数值型、字符型等)的数据,数据框的概念较矩阵来说更为一般。它与你通常在 SAS、SPSS 和 Stata 中看到的数据集类似。数据框将是你在 R 中最常处理的数据结构。

表2-1 所示的病例数据集包含了数值型和字符型数据。由于数据有多种模式,无法将此数据集放入一个矩阵。在这种情况下,使用数据框是最佳选择。

数据框可通过函数 data.frame() 创建:

mydata <- data.frame(col1, col2, col3,…)

其中的列向量 col1、col2、col3 等可为任何类型(如字符型、数值型或逻辑型)。每一列的名称可由函数 names 指定。代码清单2-4 清晰地展示了相应用法。

代码清单2-4 创建一个数据框

> patientID <- c(1, 2, 3, 4)
> age <- c(25, 34, 28, 52)
> diabetes <- c("Type1", "Type2", "Type1", "Type1")
> status <- c("Poor", "Improved", "Excellent", "Poor")
> patientdata <- data.frame(patientID, age, diabetes, status)
> patientdata
  patientID age diabetes    status
1         1  25    Type1      Poor
2         2  34    Type2  Improved
3         3  28    Type1 Excellent
4         4  52    Type1      Poor
变量信息

每一列数据的模式必须唯一,不过你却可以将多个模式的不同列放到一起组成数据框。由于数据框与分析人员通常设想的数据集的形态较为接近,我们在讨论数据框时将交替使用术语列和变量。

选取数据框中元素的方式有若干种。你可以使用前述(如矩阵中的)下标记号,亦可直接指定列名。代码清单2-5使用之前创建的 patientdata 数据框演示了这些方式。

代码清单2-5 选取数据框中的元素

> patientdata[1:2]
  patientID age
1         1  25
2         2  34
3         3  28
4         4  52
> patientdata[c("diabetes", "status")] 
  diabetes    status
1    Type1      Poor
2    Type2  Improved
3    Type1 Excellent
4    Type1      Poor
> patientdata$age
[1] 25 34 28 52

表示 patientdata 数据框中的变量 age

第三个例子中的记号$是新出现的。它被用来选取一个给定数据框中的某个特定变量。例如,如果你想生成糖尿病类型变量 diabetes 和病情变量 status 的列联表,使用以下代码即可:

> table(patientdata$diabetes, patientdata$status)

        Excellent Improved Poor
  Type1         1        0    2
  Type2         0        1    0

在每个变量名前都键入一次 patientdata$ 可能会让人生厌,所以不妨走一些捷径。可以联合使用函数 attach() 和 detach() 或单独使用函数 with() 来简化代码。

1. attach()detach()with()

函数 attach() 可将数据框添加到R的搜索路径中。R在遇到一个变量名以后,将检查搜索路径中的数据框。以第 1 章中的 mtcars 数据框为例,可以使用以下代码获取每加仑行驶英里数(mpg) 变量的描述性统计量,并分别绘制此变量与发动机排量(disp)和车身重量(wt)的散点图:

> summary(mtcars$mpg) 
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
  10.40   15.43   19.20   20.09   22.80   33.90 
plot(mtcars$mpg, mtcars$disp) 
plot(mtcars$mpg, mtcars$disp)
plot(mtcars$mpg, mtcars$wt)
plot(mtcars$mpg, mtcars$wt)

以上代码也可写成:

> attach(mtcars) 
> summary(mpg)
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
  10.40   15.43   19.20   20.09   22.80   33.90 
plot(mpg, disp) 
plot(mpg, disp)
plot(mpg, wt)
plot(mpg, wt)
detach(mtcars)

函数 detach() 将数据框从搜索路径中移除。值得注意的是,detach() 并不会对数据框本身做任何处理。这句是可以省略的,但其实它应当被例行地放入代码中,因为这是一个好的编程习惯。(接下来的几章中,为了保持代码片段的简约和简短,可能会不时地忽略这条良训。)

当名称相同的对象不止一个时,这种方法的局限性就很明显了。考虑以下代码:

> mpg <- c(25, 36, 47)
> attach(mtcars)
The following object is masked _by_ .GlobalEnv:

    mpg

> plot(mpg, wt)
Error in xy.coords(x, y, xlabel, ylabel, log) : 
  'x' and 'y' lengths differ
> mpg
[1] 25 36 47

这里,在数据框 mtcars 被绑定(attach)之前,你们的环境中已经有了一个名为 mpg 的对象。在这种情况下,原始对象将取得优先权,这与你们想要的结果有所出入。由于 mpg 中有 3 个元素而 disp 中有 32 个元素,故 plot 语句出错。函数 attach() 和 detach() 最好在你分析一个单独的数据框,并且不太可能有多个同名对象时使用。任何情况下,都要当心那些告知某个对象已被屏蔽(masked)的警告。

除此之外,另一种方式是使用函数 with()。可以这样重写上例:

with(mtcars, { 
    print(summary(mpg)) 
    plot(mpg, disp) 
    plot(mpg, wt)
})

在这种情况下,花括号 {} 之间的语句都针对数据框 mtcars 执行,这样就无需担心名称冲突了。如果仅有一条语句(例如 summary(mpg)),那么花括号 {} 可以省略。

函数 with() 的局限性在于,赋值仅在此函数的括号内生效。考虑以下代码:

> with(mtcars, {
+     stats <- summary(mpg)
+     stats
+ })
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
  10.40   15.43   19.20   20.09   22.80   33.90 
> stats
Error: object 'stats' not found

如果你需要创建在 with() 结构以外存在的对象,使用特殊赋值符 <<- 替代标准赋值符(<-) 即可,它可将对象保存到 with() 之外的全局环境中。这一点可通过以下代码阐明:

> with(mtcars, {
+     nokeepstats <- summary(mpg) 
+     keepstats <<- summary(mpg)
+ })
> nokeepstats
Error: object 'nokeepstats' not found
> keepstats
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
  10.40   15.43   19.20   20.09   22.80   33.90 

相对于 attach(),多数的 R 书籍更推荐使用 with()。个人认为从根本上说,选择哪一个是自己的偏好问题,并且应当根据你的目的和对于这两个函数含义的理解而定。本书中你们会交替使用这两个函数。

2. 实例标识符

在病例数据中,病人编号(patientID)用于区分数据集中不同的个体。在R中,实例标识符(case identifier)可通过数据框操作函数中的 rowname 选项指定。例如,语句:

patientdata <- data.frame(patientID, age, diabetes, status, row.names=patientID)

将 patientID 指定为 R 中标记各类打印输出和图形中实例名称所用的变量。

2.2.5 因子

如你所见,变量可归结为名义型、有序型或连续型变量。名义型变量是没有顺序之分的类别变量。糖尿病类型 Diabetes(Type1、Type2)是名义型变量的一例。即使在数据中 Type1 编码为 1 而 Type2 编码为 2,这也并不意味着二者是有序的。有序型变量表示一种顺序关系,而非数量关系。病情Status(poor、improved、excellent)是顺序型变量的一个上佳示例。我们明白,病情为poor(较差)病人的状态不如 improved(病情好转)的病人,但并不知道相差多少。连续型变量可以呈现为某个范围内的任意值,并同时表示了顺序和数量。年龄 Age 就是一个连续型变量,它能够表示像 14.5 或 22.8 这样的值以及其间的其他任意值。很清楚,15 岁的人比 14 岁的人年长一岁。

类别(名义型)变量和有序类别(有序型)变量在R中称为因子(factor)。因子在R中非常重要,因为它决定了数据的分析方式以及如何进行视觉呈现。你将在本书中通篇看到这样的例子。

函数 factor() 以一个整数向量的形式存储类别值,整数的取值范围是 [1…k](其中 k 是名义型变量中唯一值的个数),同时一个由字符串(原始值)组成的内部向量将映射到这些整数上。

举例来说,假设有向量:

diabetes <- c("Type1", "Type2", "Type1", "Type1")

语句 diabetes <- factor(diabetes) 将此向量存储为 (1, 2, 1, 1),并在内部将其关联为

1=Type1和2=Type2(具体赋值根据字母顺序而定)。针对向量 diabetes 进行的任何分析都会将其作为名义型变量对待,并自动选择适合这一测量尺度①的统计方法。

① 这里的测量尺度是指定类尺度、定序尺度、定距尺度、定比尺度中的定类尺度。

要表示有序型变量,需要为函数 factor() 指定参数 ordered=TRUE。给定向量:

status <- c("Poor", "Improved", "Excellent", "Poor")

语句 status <- factor(status, ordered=TRUE) 会将向量编码为 (3, 2, 1, 3),并在内部将这些值关联为1=Excellent、2=Improved 以及 3=Poor。另外,针对此向量进行的任何分析都会将其作为有序型变量对待,并自动选择合适的统计方法。

对于字符型向量,因子的水平默认依字母顺序创建。这对于因子status是有意义的,因为 “Excellent” “Improved” “Poor” 的排序方式恰好与逻辑顺序相一致。如果 “Poor” 被编码为 “Ailing”,会有问题,因为顺序将为 “Ailing” “Excellent” “Improved”。如果理想中的顺序是 “Poor” “Improved” “Excellent”,则会出现类似的问题。按默认的字母顺序排序的因子很少能够让人满意。

你可以通过指定 levels 选项来覆盖默认排序。例如:

status <- factor(status, order=TRUE, levels=c(“Poor”, “Improved”, “Excellent”))

各水平的赋值将为 1=Poor、2=Improved、3=Excellent。请保证指定的水平与数据中的真实值相匹配,因为任何在数据中出现而未在参数中列举的数据都将被设为缺失值。

数值型变量可以用 levels 和 labels 参数来编码成因子。如果男性被编码成 1,女性被编码成 2,则以下语句:

sex <- factor(sex, levels=c(1, 2), labels=c(“Male”, “Female”))

把变量转换成一个无序因子。注意到标签的顺序必须和水平相一致。在这个例子中,性别将被当成类别型变量,标签 “Male” 和 “Female” 将替代 1 和 2 在结果中输出,而且所有不是 1 或 2 的性别变量将被设为缺失值。

代码清单2-6 演示了普通因子和有序因子的不同是如何影响数据分析的。

代码清单2-6 因子的使用

> patientID <- c(1, 2, 3, 4) 
> age <- c(25, 34, 28, 52)
> diabetes <- c("Type1", "Type2", "Type1", "Type1")
> status <- c("Poor", "Improved", "Excellent", "Poor")       # 以向量形式输入数据
> diabetes <- factor(diabetes)
> status <- factor(status, order=TRUE)
> patientdata <- data.frame(patientID, age, diabetes, status)
> str(patientdata)                                            # 显示对象的结构
'data.frame':    4 obs. of  4 variables:
 $ patientID: num  1 2 3 4
 $ age      : num  25 34 28 52
 $ diabetes : Factor w/ 2 levels "Type1","Type2": 1 2 1 1
 $ status   : Ord.factor w/ 3 levels "Excellent"<"Improved"<..: 3 2 1 3
> summary(patientdata)                                        # 显示对象的统计概要
   patientID         age         diabetes       status 
 Min.   :1.00   Min.   :25.00   Type1:3   Excellent:1  
 1st Qu.:1.75   1st Qu.:27.25   Type2:1   Improved :1  
 Median :2.50   Median :31.00             Poor     :2  
 Mean   :2.50   Mean   :34.75                          
 3rd Qu.:3.25   3rd Qu.:38.50                          
 Max.   :4.00   Max.   :52.00 
变量信息

首先,以向量的形式输入数据。然后,将 diabetes 和 status 分别指定为一个普通因子和一个有序型因子。最后,将数据合并为一个数据框。函数 str(object) 可提供R中某个对象(本例中为数据框)的信息。它清楚地显示diabetes 是一个因子,而 status 是一个有序型因子, 以及此数据框在内部是如何进行编码的。注意,函数 summary() 会区别对待各个变量。它显示了连续型变量 age 的最小值、最大值、均值和各四分位数,并显示了类别型变量 diabetes 和 status(各水平)的频数值。

2.2.6 列表

列表(list)是 R 的数据类型中最为复杂的一种。一般来说,列表就是一些对象(或成分,component)的有序集合。列表允许你整合若干(可能无关的)对象到单个对象名下。例如,某个列表中可能是若干向量、矩阵、数据框,甚至其他列表的组合。可以使用函数 list() 创建列表:

mylist <- list(object1, object2, ...)

其中的对象可以是目前为止讲到的任何结构。你还可以为列表中的对象命名:

mylist <- list(name1=object1, name2=object2, ...)

代码清单2-7展示了一个例子。

代码清单2-7 创建一个列表

> g <- "My First List"
> h <- c(25, 26, 18, 39)
> j <- matrix(1:10, nrow=5)
> k <- c("one", "two", "three")
> mylist <- list(title=g, ages=h, j, k)   # 创建列表
> mylist                                    # 输出整个列表
$title
[1] "My First List"

$ages
[1] 25 26 18 39

[[3]]
     [,1] [,2]
[1,]    1    6
[2,]    2    7
[3,]    3    8
[4,]    4    9
[5,]    5   10

[[4]]
[1] "one"   "two"   "three"

> mylist[[2]]                                # 输出第二个成分
[1] 25 26 18 39
> mylist[["ages"]]
[1] 25 26 18 39
变量信息

本例创建了一个列表,其中有四个成分:一个字符串、一个数值型向量、一个矩阵以及一个字符型向量。可以组合任意多的对象,并将它们保存为一个列表。

你也可以通过在双重方括号中指明代表某个成分的数字或名称来访问列表中的元素。此例中, mylist[[2]] 和mylist[[“ages”]] 均指那个含有四个元素的向量。对于命名成分,

mylist$ages 也可以正常运行。由于两个原因,列表成为了 R 中的重要数据结构。首先,列表允许以一种简单的方式组织和重新调用不相干的信息。其次,许多R函数的运行结果都是以列表的形式返回的。需要取出其中哪些成分由分析人员决定。你将在后续各章发现许多返回列表的函数示例。

提醒程序员注意的一些事项

经验丰富的程序员通常会发现R语言的某些方面不太寻常。以下是这门语言中你需要了解的一些特性。

❑ 对象名称中的句点(.)没有特殊意义,但美元符号($)却有着和其他语言中的句点类似的含义,即指定一个数据框或列表中的某些部分。例如,A$x 是指数据框 A 中的变量x。

❑ R 不提供多行注释或块注释功能。你必须以 # 作为多行注释每行的开始。出于调试目的, 你也可以把想让解释器忽略的代码放到语句 if(FALSE){... } 中。将 FALSE 改为TRUE 即允许这块代码执行。

❑ 将一个值赋给某个向量、矩阵、数组或列表中一个不存在的元素时,R 将自动扩展这个数据结构以容纳新值。举例来说,考虑以下代码:

> x <- c(8, 6, 4)
> x[7] <- 10
> x
[1] 8 6 4 NA NA NA 10

通过赋值,向量 x 由三个元素扩展到了七个元素。x <- x[1:3] 会重新将其缩减回三个元素。

❑ R 中没有标量。标量以单元素向量的形式出现。

❑ R 中的下标不从 0 开始,而从 1 开始。在上述向量中,x[1] 的值为 8。

❑ 变量无法被声明。它们在首次被赋值时生成。

要了解更多,参阅 John Cook 的优秀博文 “R programming for those coming from other languages”(http://www.johndcook.com/Rlanguagefor_programmers.html))。

那些正在寻找编码风格指南的程序员不妨看看“Google’s R Style Guide”①(http://google-styleguide.googlecode.com/svn/trunk/google-r-style.html)。

① 搜索“来自Google的R语言编码风格指南”可以找到这份文档的中文版。

2.3 数据的输入

现在你已经掌握了各种数据结构,可以放一些数据进去了。作为一名数据分析人员,你通常会面对来自多种数据源和数据格式的数据,你的任务是将这些数据导入你的工具,分析数据,并汇报分析结果。R提供了适用范围广泛的数据导入工具。向R中导入数据的权威指南参见可在http://cran.r-project.org/doc/manuals/R-data.pdf 下载的R Data Import/Export手册②。

② 此手册对应的中译名为《R数据的导入和导出》,可在网上找到。

如图2-2 所示,R 可从键盘、文本文件、Microsoft Excel 和 Access、流行的统计软件、特殊格式的文件、多种关系型数据库管理系统、专业数据库、网站和在线服务中导入数据。由于我们无从得知你的数据将来自何处,故会在下文论及各种数据源。读者按需参阅即可。

图2-2 可供 R 导入的数据源
2.3.1 使用键盘输入数据

也许输入数据最简单的方式就是使用键盘了。有两种常见的方式:用 R 内置的文本编辑器和直接在代码中嵌入数据。我们首先考虑文本编辑器。

R 中的函数 edit() 会自动调用一个允许手动输入数据的文本编辑器。具体步骤如下:

(1) 创建一个空数据框(或矩阵),其中变量名和变量的模式需与理想中的最终数据集一致;

(2) 针对这个数据对象调用文本编辑器,输入你的数据,并将结果保存回此数据对象中。 在下例中,你将创建一个名为 mydata 的数据框,它含有三个变量:age(数值型)、gender

(字符型)和weight(数值型)。然后你将调用文本编辑器,键入数据,最后保存结果。

mydata <- data.frame(age=numeric(0), gender=character(0), weight=numeric(0))
mydata <- edit(mydata)

类似于 age=numeric(0) 的赋值语句将创建一个指定模式但不含实际数据的变量。注意,编辑的结果需要赋值回对象本身。函数 edit() 事实上是在对象的一个副本上进行操作的。如果你不将其赋值到一个目标,你的所有修改将会全部丢失!

在 Windows 上调用函数edit()的结果如图2-3 所示。如图2-3 所示,我已经自主添加了一些数据。单击列的标题,你就可以用编辑器修改变量名和变量类型(数值型、字符型)。你还可以通过单击未使用列的标题来添加新的变量。编辑器关闭后,结果会保存到之前赋值的对象中(本例中为 mydata)。再次调用 mydata <- edit(mydata),就能够编辑已经输入的数据并添加新的数据。语句 mydata <- edit(mydata) 的一种简捷的等价写法是 fix(mydata)。

图2-3 通过 Windows 上内建的编辑器输入数据

此外,你可以直接在你的程序中嵌入数据集。比如说,参见以下代码:

mydatatxt<-"
age gender weight
25 m 166
30 f 115
18 f 120
"
mydata<-read.table(header=TRUE,text=mydatatxt)
View(mydata)

以上代码创建了和之前用 edit() 函数所创建的一样的数据框。一个字符型变量被创建于存储原始数据,然后 read.table() 函数被用于处理字符串并返回数据框。函数 read.table() 将在下一节描述。

键盘输入数据的方式在你在处理小数据集的时候很有效。对于较大的数据集,你所期望的也许是我们接下来要介绍的方式:从现有的文本文件、Excel 电子表格、统计软件或数据库中导入数据。

2.3.2 从带分隔符的文本文件导入数据

你可以使用 read.table() 从带分隔符的文本文件中导入数据。此函数可读入一个表格格式的文件并将其保存为一个数据框。表格的每一行分别出现在文件中每一行。其语法如下:

mydataframe <- read.table(file, options)

其中,file 是一个带分隔符的 ASCII 文本文件,options 是控制如何处理数据的选项。表2-2 列出了常见的选项。

表2-2 函数 read.table() 的选项

选 项 描 述
header 一个表示文件是否在第一行包含了变量名的逻辑型变量
sep 分开数据值的分隔符。默认是 sep=””,这表示了一个或多个空格、制表符、换行或回车。使用 sep=”,”来读取用逗号来分隔行内数据的文件,使用 sep=”\t”来读取使用制表符来分割行内数据的文件
row.names 一个用于指定一个或多个行标记符的可选参数
col.names 如果数据文件的第一行不包括变量名(header=FASLE),你可以用 col.names 去指定一个包含变量名的字符向量。如果 header=FALSE 以及 col.names 选项被省略了,变量会被分别命名为 V1、V2,以此类推
na.strings 可选的用于表示缺失值的字符向量。比如说,na.strings=c(“-9”, “?”) 把 -9 和 ? 值在读取数据的时候转换成 NA
colClasses 可选的分配到每一列的类向量。比如说,colClasses=c(“numeric”, “numeric”, “character”, “NULL”, “numeric”) 把前两列读取为数值型变量,把第三列读取为字符型向量,跳过第四列,把第五列读取为数值型向量。如果数据有多余五列,colClasses 的值会被循环。当你在读取大型文本文件的时候,加上 colClasses 选项可以可观地提升处理的速度
quote 用于对有特殊字符的字符串划定界限的自负床。默认值是双引号(”)或单引号(’)
skip 读取数据前跳过的行的数目。这个选项在跳过头注释的时候比较有用
stringsAsFactors 一个逻辑变量,标记处字符向量是否需要转化成因子。默认值是 TRUE,除非它被 colClases 所覆盖。当你在处理大型文本文件的时候,设置成 stringsAsFactors=FALSE 可以提升处理速度
text 一个指定文字进行处理的字符串。如果 text 被设置了,file 应该被留空。

考虑一个名为 studentgrades.csv 的文本文件,它包含了学生在数学、科学、和社会学习的分数。文件中每一行表示一个学生,第一行包含了变量名,用逗号分隔。每一个单独的行都包含了学生的信息,它们也是用逗号进行分隔的。文件的前几行如下:

StudentID,First,Last,Math,Science,Social Studies

011,Bob,Smith,90,80,67

012,Jane,Weary,75,,80

010,Dan,”Thornton, III”,65,75,70

040,Mary,”O’Leary”,90,95,92

这个文件可以用以下语句来读入成一个数据框:

grades <- read.table(“studentgrades.csv”, header=TRUE, row.names=”StudentID”, sep=”,”)

结果如下:

> grades
First      Last Math Science Social.Studies
11    Bob    Smith    90    80    67
12    Jane    Weary    75   NA    80
10    Dan    Thornton, III    65    75    70
40    Mary    O'Leary    90    95    92

> str(grades)
'data.frame'    :  4 obs. of 5 variables:
$ First         : Factor w/ 4 levels "Bob","Dan","Jane",..: 1 3 2 4
$ Last             : Factor w/ 4 levels "O'Leary","Smith",..: 2 4 3 1
$ Math             : int 90 75 65 90
$ Science        : int 80 NA 75 95
$ Social.Studies: int 67 80 70 92

如何导入数据有很多有趣的要点。变量名Social Studies被自动地根据R的习惯所重命名。列StudentID现在是行名,不再有标签,也失去了前置的0。Jane的缺失的科学课成绩被正确地识别为缺失值。我不得不在Dan的姓周围用引号包围住,从而能够避免Thornton和III之间的空格。否则,R会在那一行读出七个值而不是六个值。我也在O’Leary左右用引号包围住了,负载R会把单引号读取为分隔符(而这不是我想要的)。最后,姓和名都被转化成为因子。

默认地,read.table()把字符变量转化为因子,这并不一定都是我们想要的情况。比如说, 很少情况下,我们才会把回答者的评论转化成为因子。你可用多种方法去掉这个行为。加上选项stringsAsFactors=FALSE对所有的字符变量都去掉这个行为。此外,你可以用colClasses 选项去对每一列都指定一个类(比如说,逻辑型、数值型、字符型或因子型)。

用以下代码导入同一个函数:

grades <- read.table("studentgrades.csv", header=TRUE, row.names="StudentID", sep=",", colClasses=c("character", "character", "character", "numeric", "numeric", "numeric"))

得到以下数据框:

> grades
First    Last    Math    Science    Social.Studies 
011    Bob    Smith    90    80    67             
012    Jane    Weary    75    NA    80             
010    Dan    Thornton, III    65    75    70             
040    Mary    O'Leary    90    95    92  

> str(grades)
'data.frame'    :  4 obs. of 5 variables:
$ First         : chr "Bob" "Jane" "Dan" "Mary"
$ Last             : chr "Smith" "Weary" "Thornton, III" "O'Leary"
$ Math             : num 90 75 65 90
$ Science        : num 80 NA 75 95
$ Social.Studies: num 67 80 70 92

注意,行名保留了前缀 0,而且 First 和 Last 不再是因子。此外,grades 作为实数而不是整数来进行排序。

函数 read.table() 还拥有许多微调数据导入方式的追加选项。更多详情,请参阅 help(read.table)。

用连接来导入数据

本章中的许多示例都是从用户计算机上已经存在的文件中导入数据。R 也提供了若干种通过连接(connection)来访问数据的机制。例如,函数 file()、gzfile()、bzfile()、xzfile()、 unz() 和 url() 可作为文件名参数使用。函数 file() 允许你访问文件、剪贴板和 C 级别的标准输入。函数 gzfile()、bzfile()、xzfile() 和 unz() 允许你读取压缩文件。 函数 url() 能够让你通过一个含有 http://、ftp:// 或 file:// 的完整 URL 访问网络上的文件,还可以为 HTTP 和 FTP 连接指定代理。为了方便,(用双引号围住的)完整的 URL 也经常直接用来代替文件名使用。更多详情,参见 help(file)。

2.3.3 导入 Excel 数据

读取一个 Excel 文件的最好方式,就是在 Excel 中将其导出为一个逗号分隔文件(csv),并使用前文描述的方式将其导入 R 中。此外,你可以用 xlsx 包直接地导入 Excel 工作表。请确保在第一次使用它之前先进行下载和安装。你也需要 xlsxjars 和 rJava 包,以及一个正常工作的Java 安装(http://java.com)。

xlsx 包可以用来对 Excel 97/2000/XP/2003/2007 文件进行读取、写入和格式转换。函数

read.xlsx()导入一个工作表到一个数据框中。最简单的格式是 read.xlsx(file, n),其中

file 是 Excel 工作簿的所在路径,n 则为要导入的工作表序号。举例说明,在 Windows 上,以下代码:

library(xlsx)
workbook <- "c:/myworkbook.xlsx" 
mydataframe <- read.xlsx(workbook, 1)

从位于 C 盘根目录的工作簿 myworkbook.xlsx 中导入了第一个工作表,并将其保存为一个数据框 mydataframe。

函数 read.xlsx() 有些选项可以允许你指定工作表中特定的行(rowIndex)和列

(colIndex),配合上对应每一列的类(colClasses)。对于大型的工作簿(比如说,100 000+个单元格),你也可以使用 read.xlsx2() 函数。这个函数用 Java 来运行更加多的处理过程,因此能够获得可观的质量提升。请查阅 help(read.xlsx) 获得更多细节。

也有其他包可以帮助你处理 Excel 文件。替代的包包含了 XLConnect 和 openxlsx 包;

XLConnect 依赖于 Java,不过 openxlsx 并不是。所有这些软件包都可以做比导入数据更加多的事情——它们也可以创建和操作 Excel 文件。那些需要创建 R 和 Excel 之间的接口的程序员应该要仔细查看这些软件包中的一个或多个。

2.3.4 导入 XML 数据

以 XML 格式编码的数据正在逐渐增多。R中有若干用于处理 XML 文件的包。例如,由Duncan Temple Lang 编写的 XML 包允许你读取、写入和操作 XML 文件。XML 格式本身已经超出了本书的范围。对使用 R 存取 XML 文档感兴趣的读者可以参阅 www.omegahat.org/RSXML,从中可以找到若干份优秀的软件包文档。

2.3.5 从网页抓取数据

网络上的数据,可以通过所谓Web数据抓取(Webscraping)的过程,或对应用程序接口(application programming interface,API)的使用来获得。

一般地说,在 Web 数据抓取过程中,用户从互联网上提取嵌入在网页中的信息,并将其保存为 R 中的数据结构以做进一步的分析。比如说,一个网页上的文字可以使用函数 readLines() 来下载到一个R的字符向量中,然后使用如 grep() 和 gsub() 一类的函数处理它。对于结构复杂的网页,可以使用 RCurl 包和 XML 包来提取其中想要的信息。更多信息和示例,请参考网站Programming with R(www.programmingr.com)上的 “Webscraping using readLines and RCurl” 一文。

API 指定了软件组件如何互相进行交互。有很多 R 包使用这个方法来从网上资源中获取数据。这些资源包括了生物、医药、地球科学、物理科学、经济学,以及商业、金融、文学、销售、新闻和运动等的数据源。

比如说,如果你对社交媒体感兴趣,可以用 twitteR 来获取 Twitter 数据,用 Rfacebook来获取 Facebook 数据,用 Rflickr 来获取 Flicker 数据。其他软件包允许你连接上如 Google、Amazon、Dropbox、Salesforce 等所提供的广受欢迎的网上服务。可以查看 CRAN Task View 中的子版块 Web Technologies and Services(https://cran.r-project.org/web/views/WebTechnologies.html)来获得一个全面的列表,此列表列出了能帮助你获取网上资源的各种 R 包。

2.3.6 导入 SPSS 数据

IBM SPSS 数据集可以通过 foreign 包中的函数 read.spss() 导入到 R 中,也可以使用Hmisc 包中的 spss.get() 函数。函数 spss.get() 是对 read.spss() 的一个封装,它可以为你自动设置后者的许多参数,让整个转换过程更加简单一致,最后得到数据分析人员所期望的结果。

首先,下载并安装 Hmisc 包( foreign 包已被默认安装):

install.packages("Hmisc")

然后使用以下代码导入数据:

library(Hmisc)
mydataframe <- spss.get("mydata.sav", use.value.labels=TRUE)

这段代码中,mydata.sav 是要导入的 SPSS 数据文件,use.value.labels=TRUE 表示让函数将带有值标签的变量导入为 R 中水平对应相同的因子,mydataframe 是导入后的 R 数据框。

2.3.7 导入 SAS 数据

R 中设计了若干用来导入 SAS 数据集的函数,包括 foreign 包中的 read.ssd(),Hmisc包中的 sas.get(),以及 sas7bdat 包中的 read.sas7bdat()。如果你安装了 SAS,sas.get() 是一个好的选择。

比如说,你想导入一个名为 clients.sas7bdat 的 SAS 数据集文件,它位于一台 Windows 机器上的 C:/mydata 文件夹中,以下代码导入了数据,并且保存为一个 R 数据框:

library(Hmisc)
datadir <- "C:/mydata"
sasexe <- "C:/Program Files/SASHome/SASFoundation/9.4/sas.exe"
mydata <- sas.get(libraryName=datadir, member="clients", sasprog=sasexe)

libraryName 是一个包含了 SAS 数据集的文件夹,member 是数据集名字(去除掉后缀名 sas7bdat),sasprog 是到 SAS 可运行程序的完整路径。有很多可用的选项;查看 help(sas.get) 获得更多细节。

你也可以在 SAS 中使用 PROC EXPORT 将 SAS 数据集保存为一个逗号分隔的文本文件,并使用 2.3.2 节中叙述的方法将导出的文件读取到 R 中。下面是一个示例:

SAS程序:

libname datadir "C:\mydata"; proc export data=datadir.clients
outfile="clients.csv" dbms=csv;
run;

R程序:

mydata <- read.table("clients.csv", header=TRUE, sep=",")

前面两种方法要求你安装了一套完整的可运行的 SAS 程序。如果你没有连接 SAS 的途径,函数 read.sas7dbat() 也许是一个好的候选项。这个函数可以直接读取 sas7dbat 格式的 SAS 数据集。这个例子的对应代码是:

library(sas7bdat)
mydata <- read.sas7bdat("C:/mydata/clients.sas7bdat")

不像 sas.get(),read.sas7dbat() 忽略了 SAS 用户自定义格式。此外,这个函数用了明显更多的时间来进行处理。尽管我使用这个包的时候比较好运,它依然应该被认为是实验性质的。

最后,一款名为 Stat/Transfer 的商业软件(在 2.3.12 节介绍)可以完好地将 SAS 数据集(包括任何已知的变量格式)保存为R数据框。与 read.sas7dbat() 一样,它也不要求安装 SAS。

2.3.8 导入 Stata 数据

要将 Stata 数据导入 R 中非常简单直接。所需代码类似于:

library(foreign)
mydataframe <- read.dta("mydata.dta")

这里,mydata.dta 是 Stata 数据集,mydataframe 是返回的 R 数据框。

2.3.9 导入 NetCDF 数据

Unidata 项目主导的开源软件库 NetCDF(Network Common Data Form,网络通用数据格式) 定义了一种机器无关的数据格式,可用于创建和分发面向数组的科学数据。NetCDF 格式通常用来存储地球物理数据。ncdf 包和 ncdf4 包为 NetCDF 文件提供了高层的 R 接口。

ncdf 包为通过 Unidata 的 NetCDF 库(版本 3 或更早)创建的数据文件提供了支持,而且在 Windows、Mac OS X 和 Linux 上均可使用。ncdf4 包支持 NetCDF 4 或更早的版本,但在 Windows 上尚不可用。

考虑如下代码:

library(ncdf)
nc <- nc_open("mynetCDFfile") myarray <- get.var.ncdf(nc, myvar)

在本例中,对于包含在 NetCDF 文件 mynetCDFfile 中的变量 myvar,其所有数据都被读取并保存到了一个名为 myarray 的 R 数组中。

值得注意的是,ncdf 包和 ncdf4 包最近进行了重大升级,使用方式可能与旧版本不同。另外, 这两个包中的函数名称也不同。请阅读在线帮助以了解详情。

2.3.10 导入 HDF5 数据

HDF5(Hierarchical Data Format,分层数据格式)是一套用于管理超大型和结构极端复杂数据集的软件技术方案。rhdf5 包为 R 提供了一个 HDF5 的接口。这个包在Bioconductor 网站上而不是 CRAN 上提供。你可以用以下代码对之进行安装:

source("http://bioconductor.org/biocLite.R")
biocLite("rhdf5")

像 XML 一样,HDF5 格式超出了本书的内容范围。如果想学习更多相关知识,可访问HDF Group 网站( http://www.hdf5group.org/ )。由 Bernd Fischer 编写的 http://www.bioconductor.org/packages/release/bioc/vignettes/rhdf5/inst/doc/rhdf5.pdf是一个 rhdf5 包的优秀指南。

2.3.11 访问数据库管理系统

R中有多种面向关系型数据库管理系统(DBMS)的接口,包括 Microsoft SQL Server、Microsoft Access、MySQL、Oracle、PostgreSQL、DB2、Sybase、Teradata 以及 SQLite。其中一些包通过原生的数据库驱动来提供访问功能,另一些则是通过 ODBC 或 JDBC 来实现访问的。使用R来访问存储在外部数据库中的数据是一种分析大数据集的有效手段(参见附录F),并且能够发挥 SQL 和 R 各自的优势。

1. ODBC 接口

在 R 中通过 RODBC 包访问一个数据库也许是最流行的方式,这种方式允许 R 连接到任意一种拥有 ODBC 驱动的数据库,这包含了前文所列的所有数据库。

第一步是针对你的系统和数据库类型安装和配置合适的 ODBC 驱动——它们并不是R的一部分。如果你的机器尚未安装必要的驱动,上网搜索一下应该就可以找到。

针对选择的数据库安装并配置好驱动后, 请安装 RODBC 包。你可以使用命令 install.packages(“RODBC”) 来安装它。RODBC 包中的主要函数列于表2-3 中。

表2-3 RODBC中的函数

函 数 描 述
odbcConnect(dsn,uid=””,pwd=””) 建立一个到 ODBC 数据库的连接
sqlFetch(channel,sqltable) 读取 ODBC 数据库中的某个表到一个数据框中
sqlQuery(channel,query) 向 ODBC 数据库提交一个查询并返回结果
sqlSave(channel,mydf,tablename=sqtable,append=FALSE) 将数据框写入或更新(append=TRUE)到 ODBC 数据库的某个表中
sqlDrop(channel,sqtable) 删除 ODBC 数据库中的某个表
close(channel) 关闭连接

RODBC 包允许 R 和一个通过 ODBC 连接的 SQL 数据库之间进行双向通信。这就意味着你不仅可以读取数据库中的数据到 R 中,同时也可以使用 R 修改数据库中的内容。假设你想将某个数据库中的两个表(Crime和Punishment)分别导入为 R 中的两个名为 crimedat 和 pundat 的数据框,可以通过如下代码完成这个任务:

library(RODBC)
myconn <-odbcConnect("mydsn", uid="Rob", pwd="aardvark") 
crimedat <- sqlFetch(myconn, Crime)
pundat <- sqlQuery(myconn, "select * from Punishment") 
close(myconn)

这里首先载入了 RODBC 包,并通过一个已注册的数据源名称(mydsn)和用户名(rob)以及密码(aardvark)打开了一个 ODBC 数据库连接。连接字符串被传递给 sqlFetch(),它将

Crime 表复制到R数据框 crimedat 中。然后我们对 Punishment 表执行了 SQL 语句 select 并将结果保存到数据框 pundat 中。最后,我们关闭了连接。

函数 sqlQuery() 非常强大,因为其中可以插入任意的有效 SQL 语句。这种灵活性赋予了你选择指定变量、对数据取子集、创建新变量,以及重编码和重命名现有变量的能力。

2. DBI 相关包

DBI 包为访问数据库提供了一个通用且一致的客户端接口。构建于这个框架之上的 RJDBC 包提供了通过 JDBC 驱动访问数据库的方案。使用时请确保安装了针对你的系统和数据库的必要

JDBC 驱动。其他有用的、基于 DBI 的包有 RMySQL、ROracle、RPostgreSQL 和 RSQLite。这些包都为对应的数据库提供了原生的数据库驱动,但可能不是在所有系统上都可用。详情请参阅 CRAN(http://cran.r-project.org)上的相应文档。

2.3.12 通过 Stat/Transfer 导入数据

在我们结束数据导入的讨论之前,值得提到一款能让上述任务的难度显著降低的商业软件。Stat/Transfer(www.stattransfer.com)是一款可在34种数据格式之间作转换的独立应用程序,其中包括R中的数据格式(见图2-4)。

图2-4  Windows 上 Stat/Transfer 的主对话框

此软件拥有 Windows、Mac 和 Unix 版本,并且支持我们目前讨论过的各种统计软件的最新版本,也可通过 ODBC 访问如 Oracle、Sybase、Informix 和 DB/2 一类的数据库管理系统。

2.4 数据集的标注

为了使结果更易解读,数据分析人员通常会对数据集进行标注。这种标注包括为变量名添加描述性的标签,以及为类别型变量中的编码添加值标签。例如,对于变量 age,你可能想附加一个描述更详细的标签 “Age at hospitalization (in years)”(入院年龄)。对于编码为 1 或 2 的性别变量 gender,你可能想将其关联到标签 “male” 和 “female” 上。

2.4.1 变量标签

遗憾的是,R 处理变量标签的能力有限。一种解决方法是将变量标签作为变量名,然后通过位置下标来访问这个变量。考虑之前病例数据框的例子。名为 age 的第二列包含着个体首次入院时的年龄。代码:

names(patientdata)[2] <- "Age at hospitalization (in years)"

将 age 重命名为 “Age at hospitalization (in years)”。很明显,新的变量名太长,不适合重复输入。作为替代,你可以使用 patientdata[2] 来引用这个变量,而在本应输出 age 的地方输出字符串 “Age at hospitalization (in years)”。很显然,这个方法并不理想,如果你能尝试想出更好的命名(例如,admissionAge)可能会更好一点。

2.4.2 值标签

函数 factor() 可为类别型变量创建值标签。继续上例,假设你有一个名为 gender 的变量, 其中 1 表示男性,2 表示女性。你可以使用代码:

patientdata$gender <- factor(patientdata$gender, 
                             levels = c(1,2), 
                             labels = c("male", "female"))

来创建值标签。

这里 levels 代表变量的实际值,而 labels 表示包含了理想值标签的字符型向量。

2.5 处理数据对象的实用函数

在本章末尾,我们来简要总结一下实用的数据对象处理函数(参见表2-4)。

函 数 用 途
length(object) 显示对象中元素/成分的数量
dim(object) 显示某个对象的维度
str(object) 显示某个对象的结构
class(object) 显示某个对象的类或类型
mode(object) 显示某个对象的模式
names(object) 显示某对象中各成分的名称
c(object, object,…) 将对象合并入一个向量
cbind(object, object, …) 按列合并对象
rbind(object, object, …) 按行合并对象
object 输出某个对象
head(object) 列出某个对象的开始部分
tail(object) 列出某个对象的最后部分
ls() 显示当前的对象列表
rm(object, object, …) 删除一个或更多个对象。语句 rm(list = ls()) 将删除当前工作环境中的几乎所有对象① 以句点.开头的隐藏对象将不受影响。
newobject <- edit(object) 编辑对象并另存为 newobject
fix(object) 直接编辑对象

我们已经讨论过其中的大部分函数。函数 head() 和 tail() 对于快速浏览大数据集的结构非常有用。例如,head(patientdata) 将列出数据框的前六行,而 tail(patientdata) 将列出最后六行。我们将在下一章中介绍length()、cbind() 和 rbind() 等函数。我们将其汇总于此, 仅作参考。

2.6 小结

数据的准备可能是数据分析中最具挑战性的任务之一。我们在本章中概述了 R 中用于存储数据的多种数据结构,以及从键盘和外部来源导入数据的许多可能方式,这是一个不错的起点。特别是,我们将在后续各章中反复地使用向量、矩阵、数据框和列表的概念。掌握通过括号表达式选取元素的能力,对数据的选择、取子集和变换将是非常重要的。

如你所见,R 提供了丰富的函数用以访问外部数据,包括普通文本文件、网页、统计软件、电子表格和数据库的数据。虽然本章的焦点是将数据导入到R中,你同样也可以将数据从 R 导出为这些外部格式。数据的导出在附录 C 中论及,处理大数据集(GB 级到 TB 级)的方法在附录 F 中讨论。

将数据集读入 R 之后,你很有可能需要将其转化为一种更有助于分析的格式(事实上,我发现处理数据的紧迫感有助于促进学习)。在第 4 章,我们将会探索创建新变量、变换和重编码已有变量、合并数据集和选择观测的方法。

在转而探讨数据管理之前,让我们先花些时间在 R 的绘图上。许多读者都是因为对 R 绘图怀有强烈的兴趣而开始学习 R 的,为了不让你们久等,我们在下一章将直接讨论图形的创建。关注的重点是管理和定制图形的通用方法,它们在本书余下章节都会用到。

第3章 图形初阶

本章内容
❑ 图形的创建和保存
❑ 自定义符号、线条、颜色和坐标轴
❑ 标注文本和标题
❑ 控制图形维度
❑ 组合多个图形

3.1 使用图形

R 是一个惊艳的图形构建平台。这里我特意使用了构建一词。在通常的交互式会话中,你可以通过逐条输入语句构建图形,逐渐完善图形特征,直至得到想要的效果。

考虑以下五行代码:

attach(mtcars) 
plot(wt, mpg) 
abline(lm(mpg~wt))
title("Regression of MPG on Weight") 
detach(mtcars)

首句绑定了数据框 mtcars。第二条语句打开了一个图形窗口并生成了一幅散点图,横轴表示车身重量,纵轴为每加仑汽油行驶的英里数。第三句向图形添加了一条最优拟合曲线。第四句添加了标题。最后一句为数据框解除了绑定。在 R 中,图形通常都是以这种交互式的风格绘制的(参见图3-1)。

图3-1 创建图形

可以通过代码或图形用户界面来保存图形。要通过代码保存图形,将绘图语句夹在开启目标图形设备的语句和关闭目标图形设备的语句之间即可。例如,以下代码会将图形保存到当前工作目录中名为 mygraph.pdf 的 PDF 文件中:

pdf("mygraph.pdf") 
    attach(mtcars) 
    plot(wt, mpg) 
    abline(lm(mpg~wt))
    title("Regression of MPG on Weight") 
    detach(mtcars)
dev.off()

除了 pdf(),还可以使用函数 win.metafile()、png()、jpeg()、bmp()、tiff()、xfig() 和 postscript() 将图形保存为其他格式。(注意,Windows 图元文件格式仅在 Windows 系统中可用。)关于保存图形输出到文件的更多细节,可以参考 1.3.4 节。

通过图形用户界面保存图形的方法因系统而异。对于 Windows,在图形窗口中选择“文件”→“另存为”,然后在弹出的对话框中选择想要的格式和保存位置即可。在 Mac 上,当 Quartz 图形窗口处于高亮状态时,点选菜单栏中的 “文件” → “另存为” 即可。其提供的输出格式仅有 PDF。在 UNIX 系统中,图形必须使用代码来保存。在附录 A 中,我们将考虑每个系统中可用的备选图形用户界面,这将给予你更多选择。

通过执行如 plot()、hist()(绘制直方图)或 boxplot() 这样的高级绘图命令来创建一幅新图形时,通常会覆盖掉先前的图形。如何才能创建多个图形并随时查看每一个呢?方法有若干。

第一种方法,你可以在创建一幅新图形之前打开一个新的图形窗口:

dev.new()
    statements to create graph 1
dev.new()
    statements to create a graph 2 
etc.

每一幅新图形将出现在最近一次打开的窗口中。

第二种方法,你可以通过图形用户界面来查看多个图形。在 Mac 上,你可以使用 Quartz 菜单中的“后退”(Back)和“前进”(Forward)来逐个浏览图形。在 Windows 上,这个过程分为两步。在打开第一个图形窗口以后,勾选“历史”(History)→“记录”(Recording)。然后使用菜单中的 “上一个”(Previous)和 “下一个”(Next)来逐个查看已经绘制的图形。

最后一种方法,你可以使用函数 dev.new()、dev.next()、dev.prev()、dev.set() 和

dev.off() 同时打开多个图形窗口,并选择将哪个输出发送到哪个窗口中。这种方法全平台适用。关于这种方法的更多细节,请参考 help(dev.cur)。

R 将在保证用户输入最小化的前提下创建尽可能美观的图形。不过你依然可以使用图形参数来指定字体、颜色、线条类型、坐标轴、参考线和标注。其灵活度足以让我们实现对图形的高度定制。

我们将以一个简单的图形作为本章的开始,接着进一步探索按需修改和强化图形的方式。然后,我们将着眼于一些更复杂的示例,以阐明其他的图形定制方法。我们关注的焦点是那些可以应用于多种 R 图形的技术。对于本书中描述的所有图形,本章讨论的方法均有效,不过第 19 章中使用 ggplot2 包创建的图形是例外。(ggplot2 包拥有自己的图形外观定制方法。)在其他各章中,我们将探索各种特定的图形,并探讨它们每一个在何时何地最有用。

3.2 一个简单的例子

让我们从表3-1 中给出的假想数据集开始。它描述了病人对两种药物五个剂量水平上的响应情况。

表3-1 病人对两种药物五个剂量水平上的响应情况

剂量 对药物 A 的响应 对药物 B 的响应
20 16 15
30 20 18
40 27 25
45 40 31
60 60 40

可以使用以下代码输入数据:

dose <- c(20, 30, 40, 45, 60)
drugA <- c(16, 20, 27, 40, 60)
drugB <- c(15, 18, 25, 31, 40)

使用以下代码可以创建一幅描述药物 A 的剂量和响应关系的图形:

plot(dose, drugA, type="b")

plot() 是 R 中为对象作图的一个泛型函数(它的输出将根据所绘制对象类型的不同而变化)。本例中,plot(X, y, type=”b”) 将 x 置于横轴,将 y 置于纵轴,绘制点集 (x, y),然后使用线段将其连接。选项 type=”b” 表示同时绘制点和线。使用 help(plot) 可以查看其他选项。结果如图3-2 所示。

图3-2 药物 A 剂量和响应的折线图

折线图将于第 11 章中详述。现在我们先来修改此图的外观。

3.3 图形参数

我们可以通过修改称为图形参数的选项来自定义一幅图形的多个特征(字体、颜色、坐标轴、标签)。一种方法是通过函数 par() 来指定这些选项。以这种方式设定的参数值除非被再次修改, 否 则 将 在 会 话 结 束 前 一 直 有 效 。 其 调 用 格 式 为 par(optionname=value, optionname=name,…)。不加参数地执行 par() 将生成一个含有当前图形参数设置的列表。添加参数 no.readonly=TRUE 可以生成一个可以修改的当前图形参数列表。

继续我们的例子,假设你想使用实心三角而不是空心圆圈作为点的符号,并且想用虚线代替实线连接这些点。你可以使用以下代码完成修改:

opar <- par(no.readonly=TRUE) 
par(lty=2, pch=17)
plot(dose, drugA, type="b") 
par(opar)

结果如图3-3所示。

图3-3 药物A剂量和响应的折线图。修改了线条类型和点的符号

首个语句复制了一份当前的图形参数设置。第二句将默认的线条类型修改为虚线(lty=2) 并将默认的点符号改为了实心三角(pch=17)。然后我们绘制了图形并还原了原始设置。线条类型和符号将在 3.3.1 节中详述。

你可以随心所欲地多次使用par()函数,即 par(lty=2, pch=17) 也可以写成:

par(lty=2) 
par(pch=17)

指定图形参数的第二种方法是为高级绘图函数直接提供 optionname=value 的键值对。这种情况下,指定的选项仅对这幅图形本身有效。你可以通过代码:

plot(dose, drugA, type="b", lty=2, pch=17)

来生成与上图相同的图形。

并不是所有的高级绘图函数都允许指定全部可能的图形参数。你需要参考每个特定绘图函数的帮助(如 ?plot、?hist 或 ?boxplot)以确定哪些参数可以以这种方式设置。下面介绍可以设定的许多重要图形参数。

3.3.1 符号和线条

如你所见,可以使用图形参数来指定绘图时使用的符号和线条类型。相关参数如表3-2 所示。

表3-2 用于指定符号和线条类型的参数

参 数 描 述
pch 指定绘制点时使用的符号(见图 3-4)
cex 指定符号的大小。cex 是一个数值,表示绘图符号相对于默认大小的缩放倍数。默认大小为 1,1.5 表示放大为默认值的 1.5 倍,0.5 表示缩小为默认值的 50%,等等
lty 指定线条类型(参见图 3-5)
lwd 指定线条宽度。lwd 是以默认值的相对大小来表示的(默认值为 1)。例如,lwd=2 将生成一条两倍于默认宽度的线条

选项 pch= 用于指定绘制点时使用的符号。可能的值如图3-4 所示。
图3-4 参数 pch 可指定的绘图符号

对于符号 21~25,你还可以指定边界颜色(col=)和填充色(bg=)。选项 lty= 用于指定想要的线条类型。可用的值如图3-5 所示。

图3-5 参数 lty 可指定的线条类型

综合以上选项,以下代码:

plot(dose, drugA, type="b", lty=3, lwd=3, pch=15, cex=2)

将绘制一幅图形,其线条类型为点线,宽度为默认宽度的3倍,点的符号为实心正方形,大小为默认符号大小的 2 倍。结果如图3-6 所示。

图3-6 药物A剂量和响应的折线图。修改了线条类型、线条宽度、点的符号和符号大小

接下来我们将讨论颜色的指定方法。

3.3.2 颜色

R 中有若干和颜色相关的参数。表3-3 列出了一些常用参数。
表3-3 用于指定颜色的参数

参 数 描 述
col 默认的绘图颜色。某些函数(如 lines 和 pie)可以接受一个含有颜色值的向量并自动循环使用。例如,如果设定 col=c(“red”, “blue”) 并需要绘制三条线,则第一条线将为红色,第二条线为蓝色,第三条线又将为红色
col.axis 坐标轴刻度文字的颜色
col.lab 坐标轴标签(名称)的颜色
col.main 标题颜色
col.sub 副标题颜色
fg 图形的前景色
bg 图形的背景色

在 R 中,可以通过颜色下标、颜色名称、十六进制的颜色值、RGB 值或 HSV 值来指定颜色。举例来说,col=1、col=”white”、col=”#FFFFFF”、col=rgb(1,1,1) 和col=hsv(0,0,1) 都是表示白色的等价方式。函数 rgb() 可基于红-绿-蓝三色值生成颜色,而 hsv() 则基于色相- 饱和度-亮度值来生成颜色。请参考这些函数的帮助以了解更多细节。

函数 colors() 可以返回所有可用颜色的名称。Earl F. Glynn 为 R 中的色彩创建了一个优秀的在线图表,参见 http://research.stowers-institute.org/efg/R/Color/Chart。R 中也有多种用于创建连续型颜色向量的函数,包括 rainbow()、heat.colors()、terrain.colors()、topo.colors() 以及 cm.colors()。举例来说,rainbow(10) 可以生成10 种连续的“彩虹型”颜色。

对于创建吸引人的颜色配对,RColorBrewer 特别受到欢迎。注意在第一次使用它之前先进行下载(install.packages(“RColorBrewer”))。安装之后,使用函数 brewer.pal(n, name) 来创建一个颜色值的向量。比如说,以下代码:

library(RColorBrewer) 
n <- 7
mycolors <- brewer.pal(n, "Set1") 
barplot(rep(1,n), col=mycolors)

从 Set1 调色板中抽取了7种用十六进制表示的颜色并返回一个向量。若要得到所有可选调色板的列表,输入 brewer.pal.info;或者输入 display.brewer.all() 从而在一个显示输出中产生每个调色板的图形。请参阅 help(RColorBrewer) 获得更加详细的帮助。

最后,多阶灰度色可使用基础安装所自带的 gray() 函数生成。这时要通过一个元素值为 0 和 1 之间的向量来指定各颜色的灰度。gray(0:10/10) 将生成 10 阶灰度色。试着使用以下代码:

n <- 10
mycolors <- rainbow(n)
pie(rep(1, n), labels=mycolors, col=mycolors) 
mygrays <- gray(0:n/n)
pie(rep(1, n), labels=mygrays, col=mygrays)

来观察这些函数的工作方式。

你可以看到,R 提供了多种创建颜色变量的方法。使用颜色参数的示例将贯穿本章。

3.3.3 文本属性

图形参数同样可以用来指定字号、字体和字样。表3-4 阐释了用于控制文本大小的参数。字体族和字样可以通过字体选项进行控制(见表3-5)。
表3-4 用于指定文本大小的参数

参 数 描 述
cex 表示相对于默认大小缩放倍数的数值。默认大小为 1,1.5 表示放大为默认值的 1.5 倍,0.5 表示缩小为默认值的 50%,等等
cex.axis 坐标轴刻度文字的缩放倍数。类似于 cex
cex.lab 坐标轴标签(名称)的缩放倍数。类似于 cex
cex.main 标 题 的 缩 放 倍 数 。 类 似 于 cex
cex.sub 副标题的缩放倍数。类似于 cex

表3-5 用于指定字体族、字号和字样的参数

参 数 描 述
font 整数。用于指定绘图使用的字体样式。1=常规,2=粗体,3=斜体,4=粗斜体,5=符号字体(以 Adobe 符号编码表示)
font.axis 坐标轴刻度文字的字体样式
font.lab 坐标轴标签(名称)的字体样式
font.main 标题的字体样式
font.sub 副标题的字体样式
ps 字体磅值(1 磅约为 1/72 英寸)。文本的最终大小为 ps*cex
family 绘制文本时使用的字体族。标准的取值为 serif(衬线)、sans(无衬线)和 mono(等宽)

举例来说,在执行语句:

par(font.lab=3, cex.lab=1.5, font.main=4, cex.main=2)

之后创建的所有图形都将拥有斜体、1.5倍于默认文本大小的坐标轴标签(名称),以及粗斜体、2 倍于默认文本大小的标题。

我们可以轻松设置字号和字体样式,然而字体族的设置却稍显复杂。这是因为衬线、无衬线和等宽字体的具体映射是与图形设备相关的。举例来说,在 Windows系统中,等宽字体映射为 TT Courier New,衬线字体映射为 TT Times New Roman,无衬线字体则映射为 TT Arial(TT代表True Type)。如果你对以上映射表示满意,就可以使用类似于 family=”serif” 这样的参数获得想要的结果。如果不满意,则需要创建新的映射。在 Windows 中,可以通过函数 windowsFont() 来创建这类映射。例如,在执行语句:

windowsFonts(
    A=windowsFont("Arial Black"),
    B=windowsFont("Bookman Old Style"), 
    C=windowsFont("Comic Sans MS")
)

之后,即可使用 A、B 和 C 作为 family 的取值。在本例的情境下,par(family=”A”) 将指定 Arial Black 作为绘图字体。(3.4.2 节中的代码清单3-2 提供了一个修改文本参数的示例。)请注意,函数 windowsFont() 仅在 Windows 中有效。在 Mac 上,请改用 quartzFonts()。

如果以 PDF 或 PostScript 格式输出图形,则修改字体族会相对简单一些。对于 PDF 格式,可以使用 names(pdfFonts()) 找出你的系统中有哪些字体是可用的, 然后使用 pdf(file= “myplot.pdf”, family=”fontname”) 来生成图形。对于以 PostScript 格式输出的图形,则可以对应地使用 names(postscriptFonts()) 和 postscript(file=”myplot.ps”, family= “fontname”)。请参阅在线帮助以了解更多信息。

3.3.4 图形尺寸与边界尺寸

最后,可以使用表3-6 列出的参数来控制图形尺寸和边界大小。

表3-6 用于控制图形尺寸和边界大小的参数

参 数 描 述
pin 以英寸表示的图形尺寸(宽和高)
mai 以数值向量表示的边界大小,顺序为“下、左、上、右”,单位为英寸
mar 以数值向量表示的边界大小,顺序为“下、左、上、右”,单位为英分①。默认值为 c(5, 4, 4, 2) + 0.1

① 一英分等于十二分之一英寸(0.21 厘米)。

代码:

par(pin=c(4,3), mai=c(1,.5, 1, .2))

可生成一幅4英寸宽、3英寸高、上下边界为 1 英寸、左边界为 0.5 英寸、右边界为 0.2 英寸的图形。让我们使用最近学到的选项来强化之前的简单图形示例。代码清单3-1 中的代码生成的图形如图3-7 所示。

代码清单3-1 使用图形参数控制图形外观

dose <- c(20, 30, 40, 45, 60)
drugA <- c(16, 20, 27, 40, 60)
drugB <- c(15, 18, 25, 31, 40)

opar <- par(no.readonly=TRUE) 
par(pin=c(2, 3))
par(lwd=2, cex=1.5) 
par(cex.axis=.75, font.axis=3)
plot(dose, drugA, type="b", pch=19, lty=2, col="red")
plot(dose, drugB, type="b", pch=23, lty=6, col="blue", bg="green") 
par(opar)
图3-7 药物 A 和药物 B 剂量与响应的折线图

首先,你以向量的形式输入了数据,然后保存了当前的图形参数设置(这样就可以在稍后恢复设置)。接着,你修改了默认的图形参数,得到的图形将为 2 英寸宽、3 英寸高。除此之外,线条的宽度将为默认宽度的两倍,符号将为默认大小的 1.5 倍。坐标轴刻度文本被设置为斜体、缩小为默认大小的 75%。之后,我们使用红色实心圆圈和虚线创建了第一幅图形,并使用绿色填充的绿色菱形加蓝色边框和蓝色虚线创建了第二幅图形。最后,我们还原了初始的图形参数设置。

值得注意的是,通过 par() 设定的参数对两幅图都有效,而在plot()函数中指定的参数仅对那个特定图形有效。

观察图3-7 可以发现,图形的呈现上还有一定缺陷。这两幅图都缺少标题,并且纵轴的刻度单位不同,这无疑限制了我们直接比较两种药物的能力。同时,坐标轴的标签(名称)也应当提供更多的信息。

下一节中,我们将转而探讨如何自定义文本标注(如标题和标签)和坐标轴。要了解可用图形参数的更多信息,请参阅 help(par)。

3.4 添加文本、自定义坐标轴和图例

除了图形参数,许多高级绘图函数(例如 plot、hist、boxplot)也允许自行设定坐标轴和文本标注选项。举例来说,以下代码在图形上添加了标题(main)、副标题(sub)、坐标轴标签(xlab、ylab)并指定了坐标轴范围(xlim、ylim)。结果如图3-8 所示。

plot(dose, drugA, type="b", 
     col="red", lty=2, pch=2, lwd=2,
     main="Clinical Trials for Drug A", 
     sub="This is hypothetical data", 
     xlab="Dosage", ylab="Drug Response", 
     xlim=c(0, 60), ylim=c(0, 70))
图3-8 药物 A 剂量和响应的折线图。添加了标题、副标题和自定义的坐标轴

再次提醒,并非所有函数都支持这些选项。请参考相应函数的帮助以了解其可以接受哪些选项。从更精细的控制和模块化的角度考虑,你可以使用本节余下部分描述的函数来控制标题、坐标轴、图例和文本标注的外观。

注意:某些高级绘图函数已经包含了默认的标题和标签。你可以通过在 plot() 语句或单独的par() 语句中添加 ann=FALSE 来移除它们。

3.4.1 标题

可以使用 title() 函数为图形添加标题和坐标轴标签。调用格式为:

title(main="main title", sub="subtitle", 
      xlab="x-axis label", ylab="y-axis label")

函数title()中亦可指定其他图形参数(如文本大小、字体、旋转角度和颜色)。举例来说, 以下代码将生成红色的标题和蓝色的副标题,以及比默认大小小 25 % 的绿色 x 轴、y 轴标签:

title(main="My Title", col.main="red", 
      sub="My Subtitle", col.sub="blue", 
      xlab="My X label", ylab="My Y label", 
      col.lab="green", cex.lab=0.75)

函数 title() 一般来说被用于添加信息到一个默认标题和坐标轴标签被 ann=FALSE 选项移除的图形中。

3.4.2 坐标轴

你可以使用函数 axis() 来创建自定义的坐标轴,而非使用 R 中的默认坐标轴。其格式为:

axis(side, at=, labels=, pos=, lty=, col=, las=, tck=, ...)

各参数已详述于表3-7 中。
表3-7 坐标轴选项

选 项 描 述
side 一个整数,表示在图形的哪边绘制坐标轴(1=下,2=左,3=上,4=右)
at 一个数值型向量,表示需要绘制刻度线的位置
labels 一个字符型向量,表示置于刻度线旁边的文字标签(如果为 NULL,则将直接使用 at 中的值)
pos 坐标轴线绘制位置的坐标(即与另一条坐标轴相交位置的值)
lty 线条类型
col 线条和刻度线颜色
lass 标签是否平行于(=0)或垂直于(=2)坐标轴
tck 刻度线的长度,以相对于绘图区域大小的分数表示(负值表示在图形外侧,正值表示在图形内侧,0 表示禁用刻度,1 表示绘制网格线);默认值为–0.01
(…) 其他图形参数

创建自定义坐标轴时,你应当禁用高级绘图函数自动生成的坐标轴。参数 axes=FALSE 将禁用全部坐标轴(包括坐标轴框架线,除非你添加了参数 frame.plot=TRUE)。参数 xaxt=”n” 和 yaxt=”n” 将分别禁用 X 轴或 Y 轴(会留下框架线,只是去除了刻度)。代码清单3-2 中是一个稍显笨拙和夸张的例子,它演示了我们到目前为止讨论过的各种图形特征。结果如图3-9 所示。

代码清单3-2 自定义坐标轴的示例

x <- c(1:10)                            # 生成数据    
y <- x                                    # 生成数据
z <- 10/x                                # 生成数据
opar <- par(no.readonly=TRUE)
par(mar=c(5, 4, 4, 8) + 0.1)            # 增加边界大小
plot(x, y, type="b", pch=21, col="red", yaxt="n", lty=3, ann=FALSE)  # 绘制 x 对 y 的图形
lines(x, z, type="b", pch=22, col="blue", lty=2)      # 添加 x 对 1/x 的直线
axis(2, at=x, labels=x, col.axis="red", las=2)         # 绘制你自己的坐标轴
axis(4, at=z, labels=round(z, digits=2), col.axis="blue", las=2, cex.axis=0.7, tck=-.01)                                             # 绘制你自己的坐标轴
mtext("y=1/x", side=4, line=3, cex.lab=1, las=2, col="blue") # 添加标题和文本
title("An Example of Creative Axes", xlab="X values", ylab="Y=X") # 添加标题和文本
par(opar)                                                 
图3-9 各种坐标轴选项的演示

到目前为止,我们已经讨论过代码清单3-2 中除 lines() 和 mtext() 以外的所有函数。使用 plot() 语句可以新建一幅图形。而使用 lines() 语句,你可以为一幅现有图形添加新的图形元素。在 3.4.4 节中,你会再次用到它,在同一幅图中绘制药物 A 和药物 B 的响应情况。函数 mtext() 用于在图形的边界添加文本。我们将在 3.4.5 节中讲到函数 mtext(),同时会在第 11 章中更充分地讨论 lines() 函数。

次要刻度线
注意,我们最近创建的图形都只拥有主刻度线,却没有次要刻度线。要创建次要刻度线,你需要使用 Hmisc 包中的 minor.tick() 函数。如果你尚未安装 Hmisc 包,请先安装它(参考 1.4.2 节)。你可以使用代码:

install.packages('Hmisc')
library(Hmisc)
minor.tick(nx=n, ny=n, tick.ratio=n)

来添加次要刻度线。其中 nx 和 ny 分别指定了 X 轴和 Y 轴每两条主刻度线之间通过次要刻度线划分得到的区间个数。tick.ratio 表示次要刻度线相对于主刻度线的大小比例。当前的主刻度线长度可以使用 par(“tck”) 获取。举例来说,下列语句将在 X 轴的每两条主刻度线之间添加 1 条次要刻度线,并在 Y 轴的每两条主刻度线之间添加 2 条次要刻度线:

minor.tick(nx=2, ny=3, tick.ratio=0.5)

次要刻度线的长度将是主刻度线的一半。3.4.4节中给出了添加次要刻度线的一个例子(代码清单3-3 和图3-10)。

3.4.3 参考线

函数 abline() 可以用来为图形添加参考线。其使用格式为:

abline(h=yvalues, v=xvalues)

函数 abline() 中也可以指定其他图形参数(如线条类型、颜色和宽度)。举例来说:

abline(h=c(1,5,7))

在 y 为 1、5、7 的位置添加了水平实线,而代码:

abline(v=seq(1, 10, 2), lty=2, col="blue")

则在 x 为 1、3、5、7、9 的位置添加了垂直的蓝色虚线。下一节的代码清单3-3 为我们的药物效果图在 y 为 30 的位置创建了一条参考线。结果如下一节的图3-10 所示。

3.4.4 图例

当图形中包含的数据不止一组时,图例可以帮助你辨别出每个条形、扇形区域或折线各代表哪一类数据。我们可以使用函数 legend() 来添加图例(果然不出所料)。其使用格式为:

legend(location,  title , legend, ...)

常用选项详述于表3-8 中。
表3-8 图例选项

选 项 描 述
location 有许多方式可以指定图例的位置。你可以直接给定图例左上角的 x、y 坐标,也可以执行 locator(1), 然后通过鼠标单击给出图例的位置,还可以使用关键字 bottom、bottomleft、left、topleft、top、topright、right、bottomright 或 center 放置图例。如果你使用了以上某个关键字,那么可以同时使用参数 inset=指定图例向图形内侧移动的大小(以绘图区域大小的分数表示)
title 图例标题的字符串(可选)
legend 图例标签组成的字符型向量
其他选项。如果图例标示的是颜色不同的线条,需要指定 col=加上颜色值组成的向量。如果图例标示的是符号不同的点,则需指定 pch=加上符号的代码组成的向量。如果图例标示的是不同的线条宽度或线条类型,请使用 lwd=或 lty=加上宽度值或类型值组成的向量。要为图例创建颜色填充的盒形(常见于条形图、箱线图或饼图),需要使用参数 fill=加上颜色值组成的向量

其他常用的图例选项包括用于指定盒子样式的 bty、指定背景色的 bg、指定大小的 cex,以及指定文本颜色的 text.col。指定 horiz=TRUE 将会水平放置图例,而不是垂直放置。关于图例的更多细节,请参考 help(legend)。这份帮助中给出的示例都特别有用。

让我们看看对药物数据作图的一个例子(代码清单3-3)。你将再次使用我们目前为止讲到的许多图形功能。结果如图3-10 所示。

代码清单3-3 依剂量对比药物 A 和药物 B 的响应情况

dose <- c(20, 30, 40, 45, 60)
drugA <- c(16, 20, 27, 40, 60)
drugB <- c(15, 18, 25, 31, 40)
opar <- par(no.readonly=TRUE) 
par(lwd=2, cex=1.5, font.lab=2)   # 增加线条、文本、符号、标签的宽度或大小
plot(dose, drugA, type="b",
     pch=15, lty=1, col="red", ylim=c(0, 60), 
     main="Drug A vs. Drug B",
     xlab="Drug Dosage", ylab="Drug Response")   # 绘制图形

lines(dose, drugB, type="b", 
      pch=17, lty=2, col="blue")                # 绘制图形

abline(h=c(30), lwd=1.5, lty=2, col="gray") 

library(Hmisc)                            # 添加次要刻度线
minor.tick(nx=3, ny=3, tick.ratio=0.5)    # 添加次要刻度线

legend("topleft", inset=.05, title="Drug Type", c("A","B"),
       lty=c(1, 2), pch=c(15, 17), col=c("red", "blue"))    # 添加图例

par(opar)
图3-10 进行标注后的图形,对比了药物A和药物B的效果

图3-10 的几乎所有外观元素都可以使用本章中讨论过的选项进行修改。除此之外,还有很多其他方式可以指定想要的选项。最后一种需要研究的图形标注是向图形本身添加文本,请阅读下一节。

3.4.5 文本标注

我们可以通过函数 text() 和 mtext() 将文本添加到图形上。text() 可向绘图区域内部添加文本,而 mtext() 则向图形的四个边界之一添加文本。使用格式分别为:

text(location, "text to place", pos, ...)
mtext("text to place", side, line=n, ...)

常用选项列于表3-9 中。
表3-9 函数 text() 和 mtext() 的选项

选 项 描 述
location 文本的位置参数。可为一对 x、y 坐标,也可通过指定 location 为 locator(1) 使用鼠标交互式地确定摆放位置
pos 文本相对于位置参数的方位。1=下,2=左,3=上,4=右。如果指定了 pos,就可以同时指定参数 offset=作为偏移量,以相对于单个字符宽度的比例表示
side 指定用来放置文本的边。1=下,2=左,3=上,4=右。你可以指定参数 line=来内移或外移文本,随着值的增加,文本将外移。也可使用 adj=0 将文本向左下对齐,或使用 adj=1 右上对齐

其他常用的选项有 cex、col 和 font(分别用来调整字号、颜色和字体样式)。

除了用来添加文本标注以外,text() 函数也通常用来标示图形中的点。我们只需指定一系列的 x、y 坐标作为位置参数,同时以向量的形式指定要放置的文本。x、y 和文本标签向量的长度应当相同。下面给出了一个示例,结果如图3-11 所示。

attach(mtcars) 
plot(wt, mpg,
     main="Mileage vs. Car Weight", 
     xlab="Weight", ylab="Mileage", 
     pch=18, col="blue")
text(wt, mpg, 
     row.names(mtcars), 
     cex=0.6, pos=4, col="red")
detach(mtcars)
图3-11 一幅散点图(车重与每加仑汽油行驶英里数)的示例,各点均添加了标签(车型)

这个例子中,我们针对数据框 mtcars 提供的 32 种车型的车重和每加仑汽油行驶英里数绘制了散点图。函数 text() 被用来在各个数据点右侧添加车辆型号。各点的标签大小被缩小了 40%, 颜色为红色。

作为第二个示例,以下是一段展示不同字体族的代码:

opar <- par(no.readonly=TRUE) 
par(cex=1.5) 
plot(1:7,1:7,type="n")
text(3,3,"Example of default text") 
text(4,4,family="mono","Example of mono-spaced text") 
text(5,5,family="serif","Example of serif text") 
par(opar)

在 Windows 系统中输出的结果如图3-12 所示。这里为了获得更好的显示效果,我们使用 par() 函数增大了字号。

图3-12 Windows中不同字体族的示例

本例所得结果因平台而异,因为不同系统中映射的常规字体、等宽字体和有衬线字体有所不同。在你的系统上,结果看起来如何呢?

3.4.6 数学标注

最后, 你可以使用类似于 TeX 中的写法为图形添加数学符号和公式。 请参阅 help(plotmath) 以获得更多细节和示例。要即时看效果,可以尝试执行 demo(plotmath)。部分运行结果如图3-13 所示。函数plotmath() 可以为图形主体或边界上的标题、坐标轴名称或文本标注添加数学符号。

图3-13 demo(plotmath) 的部分结果

同时比较多幅图形,我们通常可以更好地洞察数据的性质。所以,作为本章的结尾,下面讨论将多幅图形组合为一幅图形的方法。

3.5 图形的组合

在R中使用函数 par() 或 layout() 可以容易地组合多幅图形为一幅总括图形。此时请不要担心所要组合图形的具体类型,这里我们只关注组合它们的一般方法。后续各章将讨论每类图形的绘制和解读问题。

你可以在 par() 函数中使用图形参数 mfrow=c(nrows, ncols) 来创建按行填充的、行数为

nrows、列数为 ncols 的图形矩阵。另外,可以使用 mfcol=c(nrows, ncols) 按列填充矩阵。举例来说,以下代码创建了四幅图形并将其排布在两行两列中:

attach(mtcars)
opar <- par(no.readonly=TRUE) 
par(mfrow=c(2,2))
plot(wt,mpg, main="Scatterplot of wt vs. mpg") 
plot(wt,disp, main="Scatterplot of wt vs. disp") 
hist(wt, main="Histogram of wt")
boxplot(wt, main="Boxplot of wt") 
par(opar)
detach(mtcars)

结果如图3-14所示。

图3-14 通过 par(mfrow=c(2,2)) 组合的四幅图形

作为第二个示例,让我们依三行一列排布三幅图形。代码如下:

attach(mtcars)
opar <- par(no.readonly=TRUE) 
par(mfrow=c(3,1))
hist(wt) 
hist(mpg) 
hist(disp) 
par(opar) 
detach(mtcars)

所得图形如图3-15 所示。请注意,高级绘图函数 hist() 包含了一个默认的标题(使用 main=”” 可以禁用它,抑或使用 ann=FALSE 来禁用所有标题和标签)。

图3-15 通过 par(mfrow=c(3,1)) 组合的三幅图形

函数 layout() 的调用形式为 layout(mat),其中的 mat 是一个矩阵,它指定了所要组合的多个图形的所在位置。在以下代码中,一幅图被置于第 1 行,另两幅图则被置于第 2 行:

attach(mtcars)
layout(matrix(c(1,1,2,3), 2, 2, byrow = TRUE)) 
hist(wt)
hist(mpg) 
hist(disp) 
detach(mtcars)

结果如图3-16所示。

图3-16 使用函数 layout() 组合的三幅图形,各列宽度为默认值

为了更精确地控制每幅图形的大小,可以有选择地在 layout() 函数中使用 widths= 和 heights= 两个参数。其形式为:

❑ widths = 各列宽度值组成的一个向量

❑ heights = 各行高度值组成的一个向量

相对宽度可以直接通过数值指定,绝对宽度(以厘米为单位)可以通过函数 lcm() 来指定。

在以下代码中,我们再次将一幅图形置于第 1 行,两幅图形置于第 2 行。但第 1 行中图形的高度是第 2 行中图形高度的二分之一。除此之外,右下角图形的宽度是左下角图形宽度的三分之一:

attach(mtcars)
layout(matrix(c(1, 1, 2, 3), 2, 2, byrow = TRUE), 
       widths=c(3, 1), heights=c(1, 2))
hist(wt)
hist(mpg) 
hist(disp) 
detach(mtcars)

所得图形如图3-17所示。

图3-17 使用函数 layout() 组合的三幅图形,各列宽度为指定值

如你所见,layout() 函数能够让我们轻松地控制最终图形中的子图数量和摆放方式,以及这些子图的相对大小。请参考 help(layout) 以了解更多细节。

图形布局的精细控制

可能有很多时候,你想通过排布或叠加若干图形来创建单幅的、有意义的图形,这需要有对图形布局的精细控制能力。你可以使用图形参数 fig=完成这个任务。代码清单3-4 通过在散点图上添加两幅箱线图,创建了单幅的增强型图形。结果如图3-18 所示。

代码清单3-4 多幅图形布局的精细控制

opar <- par(no.readonly=TRUE) 
par(fig=c(0, 0.8, 0, 0.8)) 
plot(mtcars$wt, mtcars$mpg,
     xlab="Miles Per Gallon", 
     ylab="Car Weight")                            # 设置散点图
par(fig=c(0, 0.8, 0.55, 1), new=TRUE)             # 在上方添加箱线图
boxplot(mtcars$wt, horizontal=TRUE, axes=FALSE)    # 在上方添加箱线图

par(fig=c(0.65, 1, 0, 0.8), new=TRUE)             # 在右侧添加箱线图
boxplot(mtcars$mpg, axes=FALSE)                    # 在右侧添加箱线图
mtext("Enhanced Scatterplot", side=3, outer=TRUE, line=-3) 
par(opar)
图3-18 边界上添加了两幅箱线图的散点图

要理解这幅图的绘制原理,请试想完整的绘图区域:左下角坐标为 (0, 0),而右上角坐标为 (1, 1)。图3-19 是一幅示意图。参数 fig=的取值是一个形如 c(x1, x2, y1, y2) 的数值向量。

图3-19  使用图形参数fig=指定位置

第一个 fig=将散点图设定为占据横向范围00.8,纵向范围 00.8 。上方的箱线图横向占据 00.8 ,纵向 0.551 。右侧的箱线图横向占据 0.651 ,纵向 00.8。fig=默认会新建一幅图形,所以在添加一幅图到一幅现有图形上时,请设定参数 new=TRUE。

我将参数选择为 0.55 而不是 0.8,这样上方的图形就不会和散点图拉得太远。类似地,我选择了参数 0.65 以拉近右侧箱线图和散点图的距离。你需要不断尝试找到合适的位置参数。

注意

各独立子图所需空间的大小可能与设备相关。如果你遇到了 “Error in plot.new(): figure margins too large” 这样的错误,请尝试在整个图形的范围内修改各个子图占据的区域位置和大小。

你可以使用图形参数 fig=将若干图形以任意排布方式组合到单幅图形中。稍加练习,你就可以通过这种方法极其灵活地创建复杂的视觉呈现。

3.6 小结

本章中,我们回顾了创建图形和以各种格式保存图形的方法。本章的主体则是关于如何修改R绘制的默认图形,以得到更加有用或更吸引人的图形。你学习了如何修改一幅图形的坐标轴、字体、绘图符号、线条和颜色,以及如何添加标题、副标题、标签、文本、图例和参考线,看到了如何指定图形和边界的大小,以及将多幅图形组合为实用的单幅图形。

本章的焦点是那些可以应用于所有图形的通用方法(第 19 章的 ggplot2 图形是一个例外)。后续各章将着眼于特定的图形类型。例如,第6章介绍了对单变量绘图的各种方法;对变量间关系绘图的方法将于第11章讨论;在第 19 章中,我们则讨论高级的绘图方法,包括显示多变量数据的创新性方法。

在其他各章中,我们将会讨论对于某些统计方法来说特别实用的数据可视化方法。图形是现代数据分析的核心组成部分,所以我将尽力将它们整合到各类统计方法的讨论中。

在前一章中,我们讨论了一系列输入或导入数据到R中的方法。遗憾的是,现实数据极少以直接可用的格式出现。下一章,我们将关注如何将数据转换或修改为更有助于分析的形式。

第4章 基本数据管理

本章内容
❑ 操纵日期和缺失值
❑ 熟悉数据类型的转换
❑ 变量的创建和重编码
❑ 数据集的排序、合并与取子集
❑ 选入和丢弃变量

在第2章中,我们讨论了多种导入数据到 R 中的方法。遗憾的是,将你的数据表示为矩阵或数据框这样的矩形形式仅仅是数据准备的第一步。这里可以演绎 Kirk 船长在《星际迷航》“末日决战的滋味”一集中的台词(这完全验明了我的极客基因):“数据是一件麻烦事——一件非常非常麻烦的事。”在我的工作中,有多达 60% 的数据分析时间都花在了实际分析前数据的准备上。我敢大胆地说,多数需要处理现实数据的分析师可能都面临着以某种形式存在的类似问题。让我们先看一个例子。

4.1 一个示例

本人当前工作的研究主题之一是男性和女性在领导各自企业方式上的不同。典型的问题如下。

❑ 处于管理岗位的男性和女性在听从上级的程度上是否有所不同?

❑ 这种情况是否依国家的不同而有所不同,或者说这些由性别导致的不同是否普遍存在?

解答这些问题的一种方法是让多个国家的经理人的上司对其服从程度打分,使用的问题类似于:

这名经理在作出人事决策之前会询问我的意见
1 2 3 4 5
非常不同意 不同意 既不同意也不反对 同意 非常同意

结果数据可能类似于表4-1。各行数据代表了某个经理人的上司对他的评分。

表4-1 领导行为的性别差异

经理人 日 期 国 籍 性 别 年 龄 q1 q2 q3 q4 q5
1 10/24/14 US M 32 5 4 5 5 5
2 10/28/14 US F 45 3 5 2 5 5
3 10/01/14 UK F 25 3 5 5 5 2
4 10/12/14 UK M 39 3 3 4
5 05/01/14 UK F 99 2 2 1 2 1

在这里,每位经理人的上司根据与服从权威相关的五项陈述(q1 到 q5)对经理人进行评分。例如,经理人 1 是一位在美国工作的32岁男性,上司对他的评价是惯于顺从,而经理人 5 是一位在英国工作的,年龄未知(99 可能代表缺失)的女性,服从程度评分较低。日期一栏记录了进行评分的时间。

一个数据集中可能含有几十个变量和成千上万的观测,但为了简化示例,我们仅选取了 5 行 10 列的数据。另外,我们已将关于经理人服从行为的问题数量限制为 5。在现实的研究中,你很可能会使用 10 到 20 个类似的问题来提高结果的可靠性和有效性。可以使用代码清单4-1 中的代码创建一个包含表4-1 中数据的数据框。

代码清单4-1 创建 leadership 数据框

manager <- c(1, 2, 3, 4, 5)
date <- c("10/24/08", "10/28/08", "10/1/08", "10/12/08", "5/1/09") 
country <- c("US", "US", "UK", "UK", "UK")
gender <- c("M", "F", "F", "M", "F") 
age <- c(32, 45, 25, 39, 99)
q1 <- c(5, 3, 3, 3, 2)
q2 <- c(4, 5, 5, 3, 2)
q3 <- c(5, 2, 5, 4, 1)
q4 <- c(5, 5, 5, NA, 2)
q5 <- c(5, 5, 2, NA, 1)
leadership <- data.frame(manager, date, country, gender, age, 
                         q1, q2, q3, q4, q5, stringsAsFactors=FALSE)

为了解决感兴趣的问题,你必须首先解决一些数据管理方面的问题。这里列出其中一部分。

❑ 五个评分(q1 到 q5)需要组合起来,即为每位经理人生成一个平均服从程度得分。

❑ 在问卷调查中,被调查者经常会跳过某些问题。例如,为 4 号经理人打分的上司跳过了问题 4 和问题 5。你需要一种处理不完整数据的方法,同时也需要将 99 岁这样的年龄值重编码为缺失值。

❑ 一个数据集中也许会有数百个变量,但你可能仅对其中的一些感兴趣。为了简化问题, 我们往往希望创建一个只包含那些感兴趣变量的数据集。

❑ 既往研究表明,领导行为可能随经理人的年龄而改变,二者存在函数关系。要检验这种观点,你希望将当前的年龄值重编码为类别型的年龄组(例如年轻、中年、年长)。

❑ 领导行为可能随时间推移而发生改变。你可能想重点研究最近全球金融危机期间的服从行为。为了做到这一点,你希望将研究范围限定在某一个特定时间段收集的数据上(比如,2009 年 1 月 1 日到 2009 年 12 月 31 日)。

我们将在本章中逐个解决这些问题,同时完成如数据集的组合与排序这样的基本数据管理任务。然后,在第 5 章,我们会讨论一些更为高级的话题。

4.2 创建新变量

在典型的研究项目中,你可能需要创建新变量或者对现有的变量进行变换。这可以通过以下形式的语句来完成:

变量名 <- 表达式

以上语句中的“表达式”部分可以包含多种运算符和函数。表4-2 列出了 R 中的算术运算符。算术运算符可用于构造公式(formula)。

表4-2 算术运算符

运 算 符 描 述
+
-
*
/
^或** 求幂
x%%y 求余(x mod y)。5%%2 的结果为 1
x%/%y 整数除法。5%/%2 的结果为 2

假设你有一个名为 mydata 的数据框,其中的变量为 x1 和 x2,现在你想创建一个新变量 sumx 存储以上两个变量的加和,并创建一个名为 meanx 的新变量存储这两个变量的均值。如果使用代码:

sumx <- x1 + x2 
meanx <- (x1 + x2)/2

你将得到一个错误,因为 R 并不知道 x1 和 x2 来自于数据框 mydata。如果你转而使用代码:

sumx <- mydata$x1 + mydata$x2 
meanx <- (mydata$x1 + mydata$x2)/2

语句可成功执行,但是你只会得到一个数据框(mydata)和两个独立的向量(sumx 和 meanx)。这也许并不是你真的想要的。因为从根本上说,你希望将两个新变量整合到原始的数据框中。代码清单4-2 提供了三种不同的方式来实现这个目标,具体选择哪一个由你决定,所得结果都是相同的。

代码清单4-2 创建新变量

mydata<-data.frame(x1 = c(2, 2, 6, 4),
                   x2 = c(3, 4, 2, 8))

mydata$sumx <- mydata$x1 + mydata$x2 
mydata$meanx <- (mydata$x1 + mydata$x2)/2

attach(mydata) 
mydata$sumx <- x1 + x2
mydata$meanx <- (x1 + x2)/2 
detach(mydata)

mydata <- transform(mydata,
                    sumx = x1 + x2, 
                    meanx = (x1 + x2)/2)

我个人倾向于第三种方式,即 transform() 函数的一个示例。这种方式简化了按需创建新变量并将其保存到数据框中的过程。

4.3 变量的重编码

重编码涉及根据同一个变量和/或其他变量的现有值创建新值的过程。举例来说,你可能想:

❑ 将一个连续型变量修改为一组类别值;

❑ 将误编码的值替换为正确值;

❑ 基于一组分数线创建一个表示及格/不及格的变量。

要重编码数据,可以使用R中的一个或多个逻辑运算符(见表4-3)。逻辑运算符表达式可返回 TRUE 或 FALSE。

表4-3 逻辑运算符

运 算 符 描 述
< 小于
<= 小于或等于
> 大于
>= 大于或等于
== 严格等于①
!= 不等于
!x 非x
x | y x或y
x & y x和y
isTRUE(x) 测试x是否为TRUE

① 类似于其他科学计算语言,在R中比较浮点型数值时请慎用==,以防出现误判。详情参考“R FAQ” 7.31节。

不妨假设你希望将 leadership 数据集中经理人的连续型年龄变量 age 重编码为类别型变量 agecat(Young、 Middle Aged、Elder)。首先,必须将 99 岁的年龄值重编码为缺失值,使用的代码为:

leadership$age[leadership$age == 99]  <- NA

语句 variable[condition] <- expression 将仅在 condition 的值为 TRUE 时执行赋值。在指定好年龄中的缺失值后,你可以接着使用以下代码创建 agecat 变量:

leadership$agecat[leadership$age > 75] <- "Elder" 
leadership$agecat[leadership$age >= 55 &
                  leadership$age <= 75] <- "Middle Aged" 
leadership$agecat[leadership$age < 55] <- "Young"

你在 leadership$agecat 中写上了数据框的名称,以确保新变量能够保存到数据框中。

(我将中年人(Middle Aged)定义为 55 到 75 岁,这样不会让我感觉自己是个老古董。)请注意, 如果你一开始没把 99 重编码为 age 的缺失值,那么经理人 5 就将在变量 agecat 中被错误地赋值为“老年人”(Elder)。

这段代码可以写成更紧凑的:

leadership <- within(leadership,{
    agecat <- NA
    agecat[age > 75] <- "Elder" 
    agecat[age >= 55 & age <= 75] <- "Middle Aged" 
    agecat[age < 55] <- "Young" })

函数 within() 与函数 with() 类似(见2.2.4节),不同的是它允许你修改数据框。首先,创建了 agecat 变量,并将每一行都设为缺失值。括号中剩下的语句接下来依次执行。请记住 agecat 现在只是一个字符型变量,你可能更希望像 2.2.5 节讲解的那样把它转换成一个有序型因子。

若干程序包都提供了实用的变量重编码函数,特别地,car 包中的 recode() 函数可以十分简便地重编码数值型、字符型向量或因子。而 doBy 包提供了另外一个很受欢迎的函数 recodevar()。最后,R 中也自带了 cut(),可将一个数值型变量按值域切割为多个区间,并返回一个因子。

4.4 变量的重命名

如果对现有的变量名称不满意,你可以交互地或者以编程的方式修改它们。假设你希望将变量名 manager 修改为 managerID,并将 date 修改为 testDate,那么可以使用语句:

fix(leadership)

来调用一个交互式的编辑器。然后你单击变量名,然后在弹出的对话框中将其重命名(见图4-1)。

图4-1 使用 fix() 函数交互式地进行变量重命名

若以编程方式,可以通过 names() 函数来重命名变量。例如:

names(leadership)[2] <- "testDate"

将重命名 date 为 testDate,就像以下代码演示的一样:

> names(leadership)
[1] "manager" "date"  "country" "gender" "age"     "q1"             "q2" 
[8] "q3"   "q4"   "q5"
> names(leadership)[2] <- "testDate"
> leadership
manager testDate country gender age q1 q2 q3 q4 q5 
1    1 10/24/08  US   M 32 5 4 5 5 5
2    2 10/28/08  US   F 45 3 5 2 5 5
3    3 10/1/08   UK   F 25 3 5 5 5 2
4    4 10/12/08  UK   M 39 3 3 4 NA NA
5    5  5/1/09   UK   F 99 2 2 1 2 1

以类似的方式:

names(leadership)[6:10] <- c("item1", "item2", "item3", "item4", "item5")

将重命名 q1 到 q5 为 item1 到 item5。

最后,plyr 包中有一个 rename() 函数,可用于修改变量名。这个函数默认并没有被安装, 所以你首先要使用命令 install.packages(“plyr”) 对之进行安装。

rename() 函数的使用格式为:

rename(dataframe, c(oldname="newname", oldname="newname",...))

这里是一个示例:

library(plyr)
leadership <- rename(leadership, 
                     c(manager="managerID", date="testDate"))

plyr 包拥有一系列强大的数据集操作函数,你可以在http://had.co.nz/plyr 获得更多信息。

4.5 缺失值

在任何规模的项目中,数据都可能由于未作答问题、设备故障或误编码数据的缘故而不完整。在R中,缺失值以符号 NA(Not Available,不可用)表示。与 SAS 等程序不同,R 中字符型和数值型数据使用的缺失值符号是相同的。

R 提供了一些函数,用于识别包含缺失值的观测。函数 is.na() 允许你检测缺失值是否存在。假设你有一个向量:

y <- c(1, 2, 3, NA)

然后使用函数:

is.na(y)

将返回 c(FALSE, FALSE, FALSE, TRUE)。

请注意 is.na() 函数是如何作用于一个对象上的。它将返回一个相同大小的对象,如果某个元素是缺失值,相应的位置将被改写为 TRUE,不是缺失值的位置则为 FALSE。代码清单4-3 将此函数应用到了我们的 leadership 数据集上。

代码清单4-3 使用 is.na() 函数

> is.na(leadership[,6:10])
      q1     q2     q3    q4   q5 
[1,] FALSE FALSE FALSE FALSE FALSE 
[2,] FALSE FALSE FALSE FALSE FALSE 
[3,] FALSE FALSE FALSE FALSE FALSE 
[4,] FALSE FALSE FALSE TRUE TRUE 
[5,] FALSE FALSE FALSE FALSE FALSE

这里的 leadership[,6:10] 将数据框限定到第 6 列至第 10 列,接下来 is.na() 识别出了缺失值。

当你在处理缺失值的时候,你要一直记得两件重要的事情。第一,缺失值被认为是不可比较的,即便是与缺失值自身的比较。这意味着无法使用比较运算符来检测缺失值是否存在。例如, 逻辑测试 myvar == NA 的结果永远不会为 TRUE。作为替代,你只能使用处理缺失值的函数(如本节中所述的那些)来识别出 R 数据对象中的缺失值。

第二,R 并不把无限的或者不可能出现的数值标记成缺失值。再次地,这和其余像 SAS 之类类似的程序处理这类数值的方式所不同。正无穷和负无穷分别用 Inf 和 –Inf 所标记。因此 5/0 返回 Inf。不可能的值(比如说,sin(Inf))用 NaN 符号来标记(not a number,不是一个数)。若要识别这些数值,你需要用到 is.infinite() 或 is.nan()。

4.5.1 重编码某些值为缺失值

如4.3 节中演示的那样,你可以使用赋值语句将某些值重编码为缺失值。在我们的 leadership 示例中,缺失的年龄值被编码为 99。在分析这一数据集之前,你必须让 R 明白本例中的 99 表示缺失值(否则这些样本的平均年龄将会高得离谱)。你可以通过重编码这个变量完成这项工作:

leadership$age[leadership$age == 99] <- NA

任何等于 99 的年龄值都将被修改为 NA。请确保所有的缺失数据已在分析之前被妥善地编码为缺失值,否则分析结果将失去意义。

4.5.2 在分析中排除缺失值

确定了缺失值的位置以后,你需要在进一步分析数据之前以某种方式删除这些缺失值。原因是,含有缺失值的算术表达式和函数的计算结果也是缺失值。举例来说,考虑以下代码:

x <- c(1, 2, NA, 3)
y <- x[1] + x[2] + x[3] + x[4]
z <- sum(x)

由于 x 中的第 3 个元素是缺失值,所以 y 和 z 也都是 NA(缺失值)。

好在多数的数值函数都拥有一个 na.rm=TRUE 选项,可以在计算之前移除缺失值并使用剩余值进行计算:

x <- c(1, 2, NA, 3)
y <- sum(x, na.rm=TRUE)

这里,y 等于 6。

在使用函数处理不完整的数据时,请务必查阅它们的帮助文档(例如,help(sum)),检查这些函数是如何处理缺失数据的。函数 sum() 只是我们将在第 5 章中讨论的众多函数之一,使用这些函数可以灵活而轻松地转换数据。

你可以通过函数 na.omit() 移除所有含有缺失值的观测。na.omit() 可以删除所有含有缺失数据的行。在代码清单4-4 中,我们将此函数应用到了 leadership 数据集上。

代码清单4-4 使用 na.omit() 删除不完整的观测

> leadership
    manager   date     country gender age q1 q2 q3 q4 q5    # 含有缺失数据的数据框
1     1     10/24/08     US             M         32 5 4 5 5 5
2     2     10/28/08    US             F         40 3 5 2 5 5
3     3     10/01/08     UK             F         25 3 5 5 5 2
4     4     10/12/08     UK             M         39 3 3 4 NA NA
5     5     05/01/09     UK             F         NA 2 2 1 2 1

> newdata <- na.omit(leadership)
> newdata
    manager   date     country gender age q1 q2 q3 q4 q5     # 仅含完整观测的数据框
1     1     10/24/08     US             M     32 5 4 5 5 5
2     2     10/28/08     US             F     40 3 5 2 5 5
3     3     10/01/08     UK             F     25 3 5 5 5 2

在结果被保存到 newdata 之前,所有包含缺失数据的行均已从 leadership 中删除。

删除所有含有缺失数据的观测(称为行删除,listwise deletion)是处理不完整数据集的若干手段之一。如果只有少数缺失值或者缺失值仅集中于一小部分观测中,行删除不失为解决缺失值问题的一种优秀方法。但如果缺失值遍布于数据之中,或者一小部分变量中包含大量的缺失数据, 行删除可能会剔除相当比例的数据。我们将在第 18 章中探索若干更为复杂精妙的缺失值处理方法。下面,让我们谈谈日期值。

4.6 日期值

日期值通常以字符串的形式输入到 R 中,然后转化为以数值形式存储的日期变量。函数 as.Date() 用于执行这种转化。其语法为 as.Date(x, “input_format”),其中 x 是字符型数据,input_format 则给出了用于读入日期的适当格式(见表4-4)。

表4-4 日期格式

符号 含 义 示 例
%d 数字表示的日期(0~31) 01~31
%a 缩写的星期名 Mon
%A 非缩写星期名 Monday
%m 月份(00~12) 00~12
%b 缩写的月份 Jan
%B 非缩写月份 January
%y 两位数的年份 07
%Y 四位数的年份 2007

日期值的默认输入格式为 yyyy-mm-dd。语句:

mydates <- as.Date(c("2007-06-22", "2004-02-13"))

将默认格式的字符型数据转换为了对应日期。相反,

strDates <- c("01/05/1965", "08/16/1975") 
dates <- as.Date(strDates, "%m/%d/%Y")

则使用 mm/dd/yyyy 的格式读取数据。

在 leadership 数据集中,日期是以 mm/dd/yy 的格式编码为字符型变量的。因此:

myformat <- "%m/%d/%y"
leadership$date <- as.Date(leadership$date, myformat)

使用指定格式读取字符型变量,并将其作为一个日期变量替换到数据框中。这种转换一旦完成, 你就可以使用后续各章中讲到的诸多分析方法对这些日期进行分析和绘图。

有两个函数对于处理时间戳数据特别实用。Sys.Date() 可以返回当天的日期,而 date() 则返回当前的日期和时间。我写下这段文字的时间是 2021年 8月 15日下午 4:39。所以执行这些函数的结果是:

> Sys.Date()
[1] "2021-08-15"
> date()
[1] "Sun Aug 15 16:39:28 2021"

你可以使用函数 format(x, format=”output_format”) 来输出指定格式的日期值,并且可以提取日期值中的某些部分:

> today <- Sys.Date()
> format(today, format="%B %d %Y")
[1] "八月 15 2021"
> format(today, format="%A")
[1] "星期日"

format() 函数可接受一个参数(本例中是一个日期)并按某种格式输出结果(本例中使用了表4-4 中符号的组合)。这里最重要的结果是,距离周末只有两天时间了!

R的内部在存储日期时,是使用自 1970 年 1 月 1 日以来的天数表示的,更早的日期则表示为负数。这意味着可以在日期值上执行算术运算。例如:

> startdate <- as.Date("2004-02-13")
> enddate  <- as.Date("2011-01-22")
> days   <- enddate - startdate
> days
Time difference of 2535 days

显示了 2004 年 2 月 13 日和 2011 年 1 月 22 日之间的天数。

最后,也可以使用函数 difftime() 来计算时间间隔,并以星期、天、时、分、秒来表示。假设我出生于 1996 年 1 月 27 日,我现在有多大呢?

> today <- Sys.Date()
> dob  <- as.Date("1996-1-27")
> difftime(today, dob, units="weeks")
Time difference of 1333.143 weeks

很明显,我有 1333 周这么大,谁知道呢?最后一个小测验:猜猜我生于星期几?

4.6.1 将日期转换为字符型变量

你同样可以将日期变量转换为字符型变量。函数 as.character() 可将日期值转换为字符型:

strDates <- as.character(dates)

进行转换后,即可使用一系列字符处理函数处理数据(如取子集、替换、连接等)。我们将在第 5 章中详述字符处理函数。

4.6.2 更进一步

要了解字符型数据转换为日期的更多细节,请查看 help(as.Date) 和 help(strftime)。要了解更多关于日期和时间格式的知识,请参考 help(ISOdatetime)。lubridate 包中包含了许多简化日期处理的函数,可以用于识别和解析日期—时间数据,抽取日期—时间成分(例如年份、月份、日期等),以及对日期—时间值进行算术运算。如果你需要对日期进行复杂的计算,那么 timeDate 包可能会有帮助。它提供了大量的日期处理函数,可以同时处理多个时区,并且提供了复杂的历法操作功能,支持工作日、周末以及假期。

4.7 类型转换

在上节中,我们讨论了将字符数据转换为日期值以及逆向转换的方法。R 中提供了一系列用来判断某个对象的数据类型和将其转换为另一种数据类型的函数。

R 与其他统计编程语言有着类似的数据类型转换方式。举例来说,向一个数值型向量中添加一个字符串会将此向量中的所有元素转换为字符型。你可以使用表4-5 中列出的函数来判断数据的类型或者将其转换为指定类型。

表4-5 类型转换函数

判 断 转 换
is.numeric() as.numeric()
is.character() as.character()
is.vector() as.vector()
is.matrix() as.matrix()
is.data.frame() as.data.frame()
is.factor() as.factor()
is.logical() as.logical()

名为 is.datatype() 这样的函数返回 TRUE 或 FALSE,而 as.datatype() 这样的函数则将其参数转换为对应的类型。代码清单4-5 提供了一个示例。

代码清单4-5 转换数据类型

> a <- c(1,2,3)
> a
[1] 1 2 3
> is.numeric(a)
[1] TRUE
> is.vector(a)
[1] TRUE
> a <- as.character(a)
> a
[1] "1" "2" "3"
> is.numeric(a)
[1] FALSE
> is.vector(a)
[1] TRUE
> is.character(a)
[1] TRUE

当和第 5 章中讨论的控制流(如if-then)结合使用时,is.datatype() 这样的函数将成为一类强大的工具,即允许根据数据的具体类型以不同的方式处理数据。另外,某些 R 函数需要接受某个特定类型(字符型或数值型,矩阵或数据框)的数据,as.datatype() 这类函数可以让你在分析之前先行将数据转换为要求的格式。

4.8 数据排序

有些情况下,查看排序后的数据集可以获得相当多的信息。例如,哪些经理人最具服从意识? 在 R 中,可以使用 order() 函数对一个数据框进行排序。默认的排序顺序是升序。在排序变量的前边加一个减号即可得到降序的排序结果。以下示例使用 leadership 演示了数据框的排序。

语句:

newdata <- leadership[order(leadership$age),]

创建了一个新的数据集,其中各行依经理人的年龄升序排序。语句:

attach(leadership)
newdata <- leadership[order(gender, age),] 
detach(leadership)

则将各行依女性到男性、同样性别中按年龄升序排序。最后,

attach(leadership)
newdata <-leadership[order(gender, -age),]
detach(leadership)

将各行依经理人的性别和年龄降序排序。

4.9 数据集的合并

如果数据分散在多个地方,你就需要在继续下一步之前将其合并。本节展示了向数据框中添加列(变量)和行(观测)的方法。

4.9.1 向数据框添加列

要横向合并两个数据框(数据集),请使用 merge() 函数。在多数情况下,两个数据框是通过一个或多个共有变量进行联结的(即一种内联结,inner join)。例如:

total <- merge(dataframeA, dataframeB, by="ID")

将 dataframeA 和 dataframeB 按照 ID 进行了合并。类似地,

total <- merge(dataframeA, dataframeB, by=c("ID","Country"))

将两个数据框按照 ID 和 Country 进行了合并。类似的横向联结通常用于向数据框中添加变量。

用 cbind() 进行横向合并

如果要直接横向合并两个矩阵或数据框,并且不需要指定一个公共索引,那么可以直接使用 cbind() 函数:

total <- cbind(A, B) 

这个函数将横向合并对象 A 和对象 B。为了让它正常工作,每个对象必须拥有相同的行数, 以同顺序排序。

4.9.2 向数据框添加行

要纵向合并两个数据框(数据集),请使用 rbind() 函数:

total <- rbind(dataframeA, dataframeB)

两个数据框必须拥有相同的变量,不过它们的顺序不必一定相同。如果 dataframeA 中拥有 dataframeB 中没有的变量,请在合并它们之前做以下某种处理:

❑ 删除 dataframeA 中的多余变量;

❑ 在 dataframeB 中创建追加的变量并将其值设为 NA(缺失)。

纵向联结通常用于向数据框中添加观测。

4.10 数据集取子集

R拥有强大的索引特性,可以用于访问对象中的元素。也可利用这些特性对变量或观测进行选入和排除。以下几节演示了对变量和观测进行保留或删除的若干方法。

4.10.1 选入(保留)变量

从一个大数据集中选择有限数量的变量来创建一个新的数据集是常有的事。在第2章中,数据框中的元素是通过dataframe[row indices, column indices]这样的记号来访问的。你可以沿用这种方法来选择变量。例如:

newdata <- leadership[, c(6:10)]

从 leadership 数据框中选择了变量 q1、q2、q3、q4 和 q5,并将它们保存到了数据框 newdata 中。将行下标留空(,)表示默认选择所有行。 语句:

myvars <- c("q1", "q2", "q3", "q4", "q5") 
newdata <-leadership[myvars]

实现了等价的变量选择。这里,(引号中的)变量名充当了列的下标,因此选择的列是相同的。

最后,其实你可以写:

myvars <- paste("q", 1:5, sep="") 
newdata <- leadership[myvars]

本例使用 paste() 函数创建了与上例中相同的字符型向量。paste() 函数将在第 5 章中讲解。

4.10.2 剔除(丢弃)变量

剔除变量的原因有很多。举例来说,如果某个变量中有很多缺失值,你可能就想在进一步分析之前将其丢弃。下面是一些剔除变量的方法。

你可以使用语句:

myvars <- names(leadership) %in% c("q3", "q4") 
newdata <- leadership[!myvars]

剔除变量 q3 和 q4。为了理解以上语句的原理,你需要把它拆解如下。

(1) names(leadership) 生成了一个包含所有变量名的字符型向量:c(“managerID”,”testDate”,”country”,”gender”,”age”,”q1”, “q2”,”q3”,”q4”,”q5”)。

(2) names(leadership) %in% c(“q3”, “q4”) 返回了一个逻辑型向量,names(leadership) 中每个匹配 q3 或 q4 的元素的值为 TRUE,反之为FALSE:c(FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, TRUE, TRUE, FALSE)。

(3) 运算符非(!)将逻辑值反转:c(TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, FALSE, FALSE, TRUE)。

(4) leadership[c(TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, FALSE, FALSE, TRUE)]选择了逻辑值为TRUE的列,于是 q3 和 q4 被剔除了。

在知道 q3 和 q4 是第 8 个和第 9 个变量的情况下,可以使用语句:

newdata <- leadership[c(-8,-9)]

将它们剔除。这种方式的工作原理是,在某一列的下标之前加一个减号(–)就会剔除那一列。最后,相同的变量删除工作亦可通过:

leadership$q3 <- leadership$q4 <- NULL

来完成。这回你将 q3 和 q4 两列设为了未定义(NULL)。注意,NULL 与 NA(表示缺失)是不同的。

丢弃变量是保留变量的逆向操作。选择哪一种方式进行变量筛选依赖于两种方式的编码难易程度。如果有许多变量需要丢弃,那么直接保留需要留下的变量可能更简单,反之亦然。

4.10.3 选入观测

选入或剔除观测(行)通常是成功的数据准备和数据分析的一个关键方面。代码清单4-6给出了一些例子。

代码清单4-6 选入观测

newdata <- leadership[1:3,]   # 选择第 1 行到第 3 行(前三个观测)
newdata <- leadership[leadership$gender=="M" & leadership$age > 30,]    # 选择所有 30 岁以上的男性
attach(leadership)      # 使用了 attach() 函数,所以你就不必在变量名前加上数据框名称了
newdata <- leadership[gender=='M' & age > 30,] 
detach(leadership)

在以上每个示例中,你只提供了行下标,并将列下标留空(故选入了所有列)。在第一个示例中,你选择了第 1 行到第 3 行(前三个观测)。

让我们拆解第二行代码以便理解它。

(1) 逻辑比较 leadership$gender==”M” 生成了向量 c(TRUE, FALSE, FALSE, TRUE,

FALSE)。

(2) 逻辑比较 leadership$age > 30 生成了向量 c(TRUE, TRUE, FALSE, TRUE, TRUE)。

(3) 逻辑比较 c(TRUE, FALSE, FALSE, TRUE, TRUE) & c(TRUE, TRUE, FALSE, TRUE,

TRUE) 生成了向量 c(TRUE, FALSE, FALSE, TRUE, FALSE)。

(4) leadership[c(TRUE, FALSE, FALSE, TRUE, FALSE),] 从数据框中选择了第一个和第四个观测(当对应行的索引是 TRUE,这一行被选入;当对应行的索引是 FALSE,这一行被剔除)。这就满足了我们的选取准则(30 岁以上的男性)。

在本章开始的时候,我曾经提到,你可能希望将研究范围限定在 2009 年 1 月 1 日到 2009 年12 月 31 日之间收集的观测上。怎么做呢?这里有一个办法:

leadership$date <- as.Date(leadership$date, "%m/%d/%y") # 使用格式 mm/dd/yy 将开始作为字符值读入的日期转换为日期值
startdate <- as.Date("2009-01-01")     # 创建开始日期
enddate <- as.Date("2009-10-31")    # 创建结束日期
newdata <- leadership[which(leadership$date >= startdate & 
                            leadership$date <= enddate),] # 像上例一样选取那些满足你期望中准则的个案

注意,由于 as.Date() 函数的默认格式就是 yyyy-mm-dd,所以你无需在这里提供这个参数。

4.10.4 subset() 函数

前两节中的示例很重要,因为它们辅助描述了逻辑型向量和比较运算符在R中的解释方式。理解这些例子的工作原理在总体上将有助于你对 R 代码的解读。既然你已经用笨办法完成了任务, 现在不妨来看一种简便方法。

使用 subset() 函数大概是选择变量和观测最简单的方法了。两个示例如下:

newdata <- subset(leadership, age >= 35 | age < 24,
                  select=c(q1, q2, q3, q4))    # 选择所有 age 值大于等于 35 或 age 值小于 24 的行,保留了变量 q1 到 q4 
newdata <- subset(leadership, gender=="M" & age > 25, select=gender:q4) # 选择所有 25 岁以上的男性,并保留了变量 gender 到 q4(gender、q4 和其间所有列)

你在第 2 章中已经看到了冒号运算符 from:to。在这里,它表示了数据框中变量 from 到变量 to 包含的所有变量。

4.10.5 随机抽样

在数据挖掘和机器学习领域,从更大的数据集中抽样是很常见的做法。举例来说,你可能希望选择两份随机样本,使用其中一份样本构建预测模型,使用另一份样本验证模型的有效性。

sample() 函数能够让你从数据集中(有放回或无放回地)抽取大小为n的一个随机样本。你可以使用以下语句从 leadership 数据集中随机抽取一个大小为 3 的样本:

mysample <- leadership[sample(1:nrow(leadership), 3, replace=FALSE),]

sample() 函数中的第一个参数是一个由要从中抽样的元素组成的向量。在这里,这个向量是 1 到数据框中观测的数量,第二个参数是要抽取的元素数量,第三个参数表示无放回抽样。

sample() 函数会返回随机抽样得到的元素,之后即可用于选择数据框中的行。

R 中拥有齐全的抽样工具,包括抽取和校正调查样本(参见 sampling 包)以及分析复杂调查数据(参见 survey 包)的工具。其他依赖于抽样的方法,包括自助法和重抽样统计方法,详见第 12 章。

4.11 使用 SQL 语句操作数据框

到目前为止,你一直在使用 R 语句操作数据。但是,许多数据分析人员在接触 R 之前就已经精通了结构化查询语言(SQL),要丢弃那么多积累下来的知识实为一件憾事。因此,在我们结束本章之前简述一下 sqldf 包。(如果你对 SQL 不熟,请尽管跳过本节。)

在下载并安装好这个包以后(install.packages(“sqldf”)),你可以使用 sqldf() 函数在数据框上使用 SQL 中的 SELECT 语句。代码清单4-7 给出了两个示例。

代码清单4-7 使用 SQL 语句操作数据框
从数据框 mtcars 中选择所有的变量(列),保留那些使用化油器(carb)的车型(行),按照 mpg 对车型进行了升序排序,并将结果保存为数据框 newdf。参数 row.names=TRUE 将原始数据框中的行名延续到了新数据框中

> library(sqldf)
载入需要的程辑包:gsubfn
载入需要的程辑包:proto
载入需要的程辑包:RSQLite
> newdf <- sqldf("select * from mtcars where carb=1 order by mpg", row.names=TRUE)
> newdf
                mpg cyl  disp  hp drat    wt
Valiant        18.1   6 225.0 105 2.76 3.460
Hornet 4 Drive 21.4   6 258.0 110 3.08 3.215
Toyota Corona  21.5   4 120.1  97 3.70 2.465
Datsun 710     22.8   4 108.0  93 3.85 2.320
Fiat X1-9      27.3   4  79.0  66 4.08 1.935
Fiat 128       32.4   4  78.7  66 4.08 2.200
Toyota Corolla 33.9   4  71.1  65 4.22 1.835
                qsec vs am gear carb
Valiant        20.22  1  0    3    1
Hornet 4 Drive 19.44  1  0    3    1
Toyota Corona  20.01  1  0    3    1
Datsun 710     18.61  1  1    4    1
Fiat X1-9      18.90  1  1    4    1
Fiat 128       19.47  1  1    4    1
Toyota Corolla 19.90  1  1    4    1
> sqldf("select avg(mpg) as avg_mpg, avg(disp) as avg_disp, gear from mtcars where cyl in (4, 6) group by gear") # 输出四缸和六缸车型每一gear水平的mpg和disp的平均值
   avg_mpg avg_disp gear
1 20.33333 201.0333    3
2 24.53333 123.0167    4
3 25.36667 120.1333    5

经验丰富的 SQL 用户将会发现,sqldf 包是 R 中一个实用的数据管理辅助工具。请参阅项目主页(http://code.google.com/p/sqldf/)以了解详情。

4.12 小结

本章讲解了大量的基础知识。首先我们看到了R存储缺失值和日期值的方式,并探索了它们的多种处理方法。接着学习了如何确定一个对象的数据类型,以及如何将它转换为其他类型。还使用简单的公式创建了新变量并重编码了现有变量。你学习了如何对数据进行排序和对变量进行重命名,学习了如何对数据和其他数据集进行横向合并(添加变量)和纵向合并(添加观测)。最后,我们讨论了如何保留或丢弃变量,以及如何基于一系列的准则选取观测。

在下一章中,我们将着眼于 R 中不计其数的,用于创建和转换变量的算术函数、字符处理函数和统计函数。在探索了控制程序流程的方式之后,你将了解到如何编写自己的函数。我们也将探索如何使用这些函数来整合及概括数据。

在第 5 章结束时,你就能掌握管理复杂数据集的多数工具。(无论你走到哪里,都将成为数据分析师艳羡的人物!)

第5章 高级数据管理

本章内容

❑ 数学和统计函数

❑ 字符处理函数

❑ 循环和条件执行

❑ 自编函数

❑ 数据整合与重塑

在第4章,我们审视了R中基本的数据集处理方法,本章我们将关注一些高级话题。本章分为三个基本部分。在第一部分中,我们将快速浏览R中的多种数学、统计和字符处理函数。为了让这一部分的内容相互关联,我们先引入一个能够使用这些函数解决的数据处理问题。在讲解过这些函数以后,再为这个数据处理问题提供一个可能的解决方案。

接下来,我们将讲解如何自己编写函数来完成数据处理和分析任务。首先,我们将探索控制程序流程的多种方式,包括循环和条件执行语句。然后,我们将研究用户自编函数的结构,以及在编写完成后如何调用它们。

最后,我们将了解数据的整合和概述方法,以及数据集的重塑和重构方法。在整合数据时, 你可以使用任何内建或自编函数来获取数据的概述,所以你在本章前两部分中学习的内容将会派上用场。

5.1 一个数据处理难题

要讨论数值和字符处理函数,让我们首先考虑一个数据处理问题。一组学生参加了数学、科学和英语考试。为了给所有学生确定一个单一的成绩衡量指标,需要将这些科目的成绩组合起来。另外,你还想将前 20% 的学生评定为 A,接下来 20% 的学生评定为 B,依次类推。最后,你希望按字母顺序对学生排序。数据如表5-1 所示。

表5-1 学生成绩数据

学生姓名 数 学 科 学 英 语
John Davis 502 95 25
Angela Williams 600 99 22
Bullwinkle Moose 412 80 18
David Jones 358 82 15
Janice Markhammer 495 75 20
Cheryl Cushing 512 85 28
Reuven Ytzrhak 410 80 15
Greg Knox 625 95 30
Joel England 573 89 27
Mary Rayburn 522 86 18

观察此数据集,马上可以发现一些明显的障碍。首先,三科考试的成绩是无法比较的。由于它们的均值和标准差相去甚远,所以对它们求平均值是没有意义的。你在组合这些考试成绩之前, 必须将其变换为可比较的单元。其次,为了评定等级,你需要一种方法来确定某个学生在前述得分上百分比排名。再次,表示姓名的字段只有一个,这让排序任务复杂化了。为了正确地将其排序,需要将姓和名拆开。

以上每一个任务都可以巧妙地利用 R 中的数值和字符处理函数完成。在讲解完下一节中的各种函数之后,我们将考虑一套可行的解决方案,以解决这项数据处理难题。

5.2 数值和字符处理函数

本节我们将综述R中作为数据处理基石的函数,它们可分为数值(数学、统计、概率)函数和字符处理函数。在阐述过每一类函数以后,我将为你展示如何将函数应用到矩阵和数据框的列(变量)和行(观测)上(参见 5.2.6 节)。

5.2.1 数学函数

表5-2 列出了常用的数学函数和简短的用例。

表5-2 数学函数

函 数 描 述
abs(x) 绝对值
abs(-4) 返回值为 4
sqrt(x) 平方根
sqrt(25) 返回值为 5,和 25^(0.5) 等价
ceiling(x) 不小于 x 的最小整数
ceiling(3.475) 返回值为 4
floor(x) 不大于 x 的最大整数
floor(3.475) 返回值为 3
trunc(x) 向 0 的方向截取的 x 中的整数部分
trunc(5.99) 返回值为 5
round(x, digits=n) 将 x 舍入为指定位的小数
round(3.475, digits=2),返回值为 3.48
signif(x, digits=n) 将 x 舍入为指定的有效数字位数
signif(3.475, digits=2) 返回值为 3.5
cos(x)、sin(x)、tan(x) 余弦、正弦和正切
cos(2) 返回值为 –0.416
acos(x)、asin(x)、atan(x) 反余弦、反正弦和反正切
acos(-0.416) 返回值为 2
cosh(x)、sinh(x)、tanh(x) 双曲余弦、双曲正弦和双曲正切
sinh(2) 返回值为 3.627
acosh(X)、asinh(X)、atanh(X) 反双曲余弦、反双曲正弦和反双曲正切
asinh(3.627) 返回值为 2
log(x,base=n) 对 x 取以 n 为底的对数
log(x) 为了方便起见:
log10(x) • log(x) 为自然对数
• log10(x) 为常用对数
• log(10) 返回值为 2.3026
• log10(10) 返回值为 1
exp(x) 指数函数
exp(2.3026) 返回值为 10

对数据做变换是这些函数的一个主要用途。例如,你经常会在进一步分析之前将收入这种存在明显偏倚的变量取对数。数学函数也被用作公式中的一部分,用于绘图函数(例如 x 对 sin(x)) 和在输出结果之前对数值做格式化。

表5-2 中的示例将数学函数应用到了标量(单独的数值)上。当这些函数被应用于数值向量、矩阵或数据框时,它们会作用于每一个独立的值。例如,sqrt(c(4, 16, 25)) 的返回值为 c(2, 4, 5)。

5.2.2 统计函数

常用的统计函数如表5-3 所示,其中许多函数都拥有可以影响输出结果的可选参数。举例来说:

y <- mean(x)

提供了对象 x 中元素的算术平均数,而:

z <- mean(x, trim = 0.05, na.rm=TRUE)

则提供了截尾平均数,即丢弃了最大 5% 和最小 5% 的数据和所有缺失值后的算术平均数。请使用 help() 了解以上每个函数和其参数的用法。

表5-3 统计函数

函 数 描 述
mean(x) 平均数
mean(c(1,2,3,4)) 返回值为 2.5
median(x) 中位数
median(c(1,2,3,4)) 返回值为 2.5
sd(x) 标准差
sd(c(1,2,3,4)) 返回值为 1.29
var(x) 方差
var(c(1,2,3,4)) 返回值为 1.67
mad(x) 绝对中位差(median absolute deviation)
mad(c(1,2,3,4)) 返回值为 1.48
quantile(x,probs) 求分位数。其中 x 为待求分位数的数值型向量,probs 为一个由 [0,1] 之间的概率值组成的数值向量
# 求 x 的 30% 和 84% 分位点
y <- quantile(x, c(.3,.84))
range(x) 求值域
x <- c(1,2,3,4)
range(x) 返回值为 c(1,4)
diff(range(x)) 返回值为 3
sum(x) 求和
sum(c(1,2,3,4)) 返回值为 10
diff(x, lag=n) 滞后差分,lag 用以指定滞后几项。默认的 lag 值为 1
x<- c(1, 5, 23, 29)
diff(ddx) 返回值为 c(4, 18, 6)
min(x) 求最小值
min(c(1,2,3,4)) 返回值为 1
max(x) 求最大值
max(c(1,2,3,4))返回值为 4
scale(x,center=TRUE, scale=TRUE) 为数据对象 x 按列进行中心化(center=TRUE)或标准化(center=TRUE, scale=TRUE); 代码清单 5-6 中给出了一个示例
要了解这些函数的实战应用,请参考代码清单5-1。这个例子演示了计算某个数值向量的均值和标准差的两种方式。

代码清单5-1 均值和标准差的计算

# 简洁的方式
> x <- c(1,2,3,4,5,6,7,8)
> mean(x) 
[1] 4.5
> sd(x)
[1] 2.44949

# 冗长的方式
> n <- length(x)
> meanx <- sum(x)/n
> css <- sum((x - meanx)^2)
> sdx <- sqrt(css / (n-1))
> meanx
[1] 4.5
> sdx
[1] 2.44949

第二种方式中修正平方和(css)的计算过程是很有启发性的:

(1) x 等于 c(1, 2, 3, 4, 5, 6, 7, 8),x 的平均值等于 4.5(length(x) 返回了 x 中元素的数量);

(2) (x – meanx) 从 x 的每个元素中减去了 4.5,结果为 c(-3.5, -2.5, -1.5, -0.5, 0.5, 1.5, 2.5, 3.5);

(3) (x – meanx)^2 将 (x - meanx) 的每个元素求平方,结果为 c(12.25, 6.25, 2.25, 0.25, 0.25, 2.25, 6.25, 12.25);

(4) sum((x - meanx)^2) 对 (x - meanx)^2) 的所有元素求和,结果为 42。

R 中公式的写法和类似 MATLAB 的矩阵运算语言有着许多共同之处。(我们将在附录 D 中具体关注解决矩阵代数问题的方法。)

数据的标准化

默认情况下,函数 scale() 对矩阵或数据框的指定列进行均值为 0、标准差为 1 的标准化:

newdata <- scale(mydata)

要对每一列进行任意均值和标准差的标准化,可以使用如下的代码:

newdata <- scale(mydata)*SD + M 

其中的 M 是想要的均值,SD 为想要的标准差。在非数值型的列上使用 scale() 函数将会报错。要对指定列而不是整个矩阵或数据框进行标准化,你可以使用这样的代码:

newdata <- transform(mydata, myvar = scale(myvar)*10+50) 

此句将变量 myvar 标准化为均值 50、标准差为 10 的变量。你将在 5.3 节数据处理问题的解决方法中用到 scale() 函数。

5.2.3 概率函数

你可能在疑惑为何概率函数未和统计函数列在一起。(你真的对此有些困惑,对吧?)虽然根据定义,概率函数也属于统计类,但是它们非常独特,应独立设一节进行讲解。概率函数通常用来生成特征已知的模拟数据,以及在用户编写的统计函数中计算概率值。

在 R 中,概率函数形如:

[dpqr]distribution_abbreviation()

其中第一个字母表示其所指分布的某一方面:

d = 密度函数(density)

p = 分布函数(distribution function)

q = 分位数函数(quantile function)

r = 生成随机数(随机偏差)

常用的概率函数列于表5-4 中。

表5-4 概率分布

分布名称 缩 写 分布名称 缩 写
Beta 分布 beta Logistic 分布 logis
二项分布 binom 多项分布 multinom
柯西分布 cauchy 负二项分布 nbinom
(非中心)卡方分布 chisq 正态分布 norm
指数分布 exp 泊松分布 pois
F 分布 f Wilcoxon 符号秩分布 signrank
Gamma 分布 gamma t 分布 t
几何分布 geom 均匀分布 unif
超几何分布 hyper Weibull 分布 weibull
对数正态分布 lnorm Wilcoxon 秩和分布 wilcox

我们不妨先看看正态分布的有关函数,以了解这些函数的使用方法。如果不指定一个均值和一个标准差,则函数将假定其为标准正态分布(均值为 0,标准差为 1)。密度函数(dnorm)、分布函数(pnorm)、分位数函数(qnorm)和随机数生成函数(rnorm)的使用示例见表 5-5。

表5-5 正态分布函数

**表5-5 正态分布函数**

如果读者对 plot() 函数的选项不熟悉,请不要担心。这些选项在第 11 章中有详述。pretty() 在本章稍后的表5-7 中进行了解释。

1. 设定随机数种子

在每次生成伪随机数的时候,函数都会使用一个不同的种子,因此也会产生不同的结果。你可以通过函数 set.seed() 显式指定这个种子,让结果可以重现(reproducible)。代码清单5-2 给出了一个示例。这里的函数 runif() 用来生成 0 到 1 区间上服从均匀分布的伪随机数。

代码清单5-2 生成服从正态分布的伪随机数

> runif(5)
[1] 0.7596050 0.1670000 0.1864267 0.5590606
[5] 0.9861367
> runif(5)
[1] 0.9165886 0.6570878 0.9515263 0.7049961
[5] 0.6763119
> set.seed(1234)
> runif(5)
[1] 0.1137034 0.6222994 0.6092747 0.6233794
[5] 0.8609154
> set.seed(1234)
> runif(5)
[1] 0.1137034 0.6222994 0.6092747 0.6233794
[5] 0.8609154

通过手动设定种子,就可以重现你的结果了。这种能力有助于我们创建会在未来取用的,以及可与他人分享的示例。

2. 生成多元正态数据

在模拟研究和蒙特卡洛方法中,你经常需要获取来自给定均值向量和协方差阵的多元正态分布的数据。MASS 包中的 mvrnorm() 函数可以让这个问题变得很容易。其调用格式为:

mvrnorm(n, mean, sigma)

其中 n 是你想要的样本大小,mean 为均值向量,而 sigma 是方差-协方差矩阵(或相关矩阵)。代码清单5-3 从一个参数如下所示的三元正态分布中抽取 500 个观测。

——
均值向量 230.7 146.7 3.6
协方差阵 15360.8 6721.2 -47.1
6721.2 4700.9 -16.5
-47.1 -16.5 0.3

代码清单5-3 生成服从多元正态分布的数据

> library(MASS)
> options(digits=3)
> set.seed(1234)    # 设定随机数种子

> mean <- c(230.7, 146.7, 3.6)
> sigma <- matrix(c(15360.8, 6721.2, -47.1,
+                   6721.2, 4700.9, -16.5,
+                   -47.1,  -16.5,  0.3), nrow=3, ncol=3) # 指定均值向量、协方差阵
> mydata <- mvrnorm(500, mean, sigma)
> mydata <- as.data.frame(mydata)
> names(mydata) <- c("y","x1","x2")        # 生成数据
> 
> dim(mydata)
[1] 500   3
> head(mydata, n=10)        # 查看结果
       y    x1   x2
1   98.8  41.3 3.43
2  244.5 205.2 3.80
3  375.7 186.7 2.51
4  -59.2  11.2 4.71
5  313.0 111.0 3.45
6  288.8 185.1 2.72
7  134.8 165.0 4.39
8  171.7  97.4 3.64
9  167.2 101.0 3.50
10 121.1  94.5 4.10

代码清单5-3 中设定了一个随机数种子,这样就可以在之后重现结果➊。你指定了想要的均值向量和方差-协方差阵➋,并生成了 500 个伪随机观测➌。为了方便,结果从矩阵转换为数据框, 并为变量指定了名称。最后,你确认了拥有 500 个观测和 3 个变量,并输出了前 10 个观测➍。请注意,由于相关矩阵同时也是协方差阵,所以其实可以直接指定相关关系的结构。

R 中的概率函数允许生成模拟数据,这些数据是从服从已知特征的概率分布中抽样而得的。近年来,依赖于模拟数据的统计方法呈指数级增长,在后续各章中会有若干示例。

5.2.4 字符处理函数

数学和统计函数是用来处理数值型数据的,而字符处理函数可以从文本型数据中抽取信息,或者为打印输出和生成报告重设文本的格式。举例来说,你可能希望将某人的姓和名连接在一起, 并保证姓和名的首字母大写,抑或想统计可自由回答的调查反馈信息中含有秽语的实例(instance)数量。一些最有用的字符处理函数见表5-6。

表5-6 字符处理函数

函 数 描 述
nchar(x) 计算 x 中的字符数量
x <- c(“ab”, “cde”, “fghij”)
length(x) 返回值为 3 (参见表 5-7)
nchar(x[3]) 返回值为 5
substr(x, start, stop) 提取或替换一个字符向量中的子串
x <- “abcdef”
substr(x, 2, 4) 返回值为”bcd”
substr(x, 2, 4) <- “22222”(x 将变成”a222ef”)
grep(pattern, x, ignore. case=FALSE, fixed=FALSE) 在 x 中搜索某种模式。若 fixed=FALSE,则 pattern 为一个正则表达式。若 fixed=TRUE,则 pattern 为一个文本字符串。返回值为匹配的下标 grep(“A”,c(“b”,”A”,”c”),fixed=TRUE) 返回值为 2
sub(pattern, replacement, x, ignore.case=FALSE, fixed=FALSE) 在 x 中搜索 pattern,并以文本 replacement 将其替换。若 fixed=FALSE,则 pattern 为一个正则表达式。若 fixed=TRUE,则 pattern 为一个文本字符串。sub(“\s”,”.”,”Hello There”)返回值为 Hello.There。注意,”\s”是一个用来查找空白的正则表达式;使用”\s”而不用”\“的原因是,后者是 R 中的转义字符(参见 1.3.3 节)
strsplit(x, split, fixed=FALSE) 在 split 处分割字符向量 x 中的元素。若 fixed=FALSE,则 pattern 为一个正则表达式。若 fixed=TRUE,则 pattern 为一个文本字符串
y <- strsplit(“abc”, “”) 将返回一个含有 1 个成分、3 个元素的列表,包含的内容为”a” “b” “c”
unlist(y)[2] 和 sapply(y, “[“, 2)均会返回”b”
paste(…, sep=””) 连接字符串,分隔符为 sep
paste(“x”, 1:3,sep=””) 返回值为 c(“x1”, “x2”, “x3”)
paste(“x”,1:3,sep=”M”) 返回值为 c(“xM1”,”xM2” “xM3”)
paste(“Today is”, date()) 返回值为 Today is Sun Aug 15 20:54:49 2021
toupper(x) 大写转换
toupper(“abc”)返回值为”ABC”
tolower(x) 小写转换
tolower(“ABC”) 返回值为”abc”

请注意,函数 grep()、sub() 和 strsplit() 能够搜索某个文本字符串(fixed=TRUE)或某个正则表达式(fixed=FALSE,默认值为 FALSE)。正则表达式为文本模式的匹配提供了一套清晰而简练的语法。例如,正则表达式:

^[hc]?at

可匹配任意以 0 个或 1 个 h 或 c 开头、后接at的字符串。因此,此表达式可以匹配 hat、cat 和 at,但不会匹配 bat。要了解更多,请参考维基百科的 regular expression(正则表达式)条目。

5.2.5 其他实用函数

表5-7 中的函数对于数据管理和处理同样非常实用,只是它们无法清楚地划入其他分类中。

表5-7 其他实用函数

函 数 描 述
length(x) 对象 x 的长度
x <- c(2, 5, 6, 9)
length(x) 返回值为 4
seq(from, to, by) 生成一个序列
indices <- seq(1,10,2)
indices 的值为c(1, 3, 5, 7, 9)
rep(x, n) 将 x 重复 n 次
y <- rep(1:3, 2)
y 的值为 c(1, 2, 3, 1, 2, 3)
cut(x, n) 将连续型变量 x 分割为有着 n 个水平的因子
使用选项 ordered_result = TRUE 以创建一个有序型因子
pretty(x, n) 创建美观的分割点。通过选取 n+1 个等间距的取整值,将一个连续型变量 x 分割为 n 个区间。绘图中常用
cat(… , file =”myfile”, append =FALSE) 连接…中的对象,并将其输出到屏幕上或文件中(如果声明了一个的话)
firstname <- c(“Jane”)
cat(“Hello” ,firstname, “\n”)

表中的最后一个例子演示了在输出时转义字符的使用方法。\n 表示新行,\t 为制表符,\‘ 为单引号,\b 为退格,等等。(键入 ?Quotes 以了解更多。)例如,代码:

name <- "Bob"
cat( "Hello", name, "\b.\n", "Isn\'t R", "\t", "GREAT?\n")

可生成:
Hello Bob.
Isn’t R GREAT?

请注意第二行缩进了一个空格。当 cat 输出连接后的对象时,它会将每一个对象都用空格分开。这就是在句号之前使用退格转义字符(\b)的原因。不然,生成的结果将是 “Hello Bob .”。

在数值、字符串和向量上使用我们最近学习的函数是直观而明确的,但是如何将它们应用到

矩阵和数据框上呢?这就是下一节的主题。

5.2.6 将函数应用于矩阵和数据框

R 函数的诸多有趣特性之一,就是它们可以应用到一系列的数据对象上,包括标量、向量、矩阵、数组和数据框。代码清单5-4 提供了一个示例。

代码清单5-4 将函数应用于数据对象

> a <- 5
> sqrt(a)
[1] 2.24
> b <- c(1.243, 5.654, 2.99)
> round(b)
[1] 1 6 3
> c <- matrix(runif(12), nrow=3)
> c
      [,1]  [,2]   [,3]  [,4]
[1,] 0.856 0.478 0.0904 0.873
[2,] 0.350 0.395 0.0438 0.993
[3,] 0.749 0.370 0.7149 0.855
> log(c)
       [,1]   [,2]   [,3]     [,4]
[1,] -0.156 -0.738 -2.404 -0.13628
[2,] -1.050 -0.929 -3.128 -0.00751
[3,] -0.289 -0.995 -0.336 -0.15695
> mean(c)
[1] 0.564

请注意,在代码清单5-4 中对矩阵 c 求均值的结果为一个标量(0.444)。函数 mean() 求得的是矩阵中全部 12 个元素的均值。但如果希望求的是各行的均值或各列的均值呢?

R中提供了一个 apply() 函数,可将一个任意函数“应用”到矩阵、数组、数据框的任何维度上。apply()函数的使用格式为:

apply(X, MARGIN, FUN, …)

其中,x 为数据对象,MARGIN 是维度的下标,FUN 是由你指定的函数,而…则包括了任何想传递给 FUN 的参数。在矩阵或数据框中,MARGIN=1表示行,MARGIN=2 表示列。请看以下例子。

代码清单5-5 将一个函数应用到矩阵的所有行(列)

> mydata <- matrix(rnorm(30), nrow=6)  # 生成数据
> mydata
       [,1]   [,2]   [,3]   [,4]     [,5]
[1,]  0.902  1.226 -0.672  1.077 -1.75273
[2,]  0.306  2.666 -0.744 -1.253 -1.28340
[3,]  0.524  0.402  2.380  1.161 -1.13352
[4,] -0.570 -0.720 -0.735  1.535  0.00802
[5,]  0.729 -0.543 -1.811 -0.959 -0.37033
[6,] -0.410  0.814  0.266  0.392 -0.37910
>  apply(mydata, 1, mean)        # 计算每行的均值
[1]  0.1561 -0.0618  0.6668 -0.0965 -0.5909  0.1368
> apply(mydata, 2, mean)        # 计算每列的均值
[1]  0.247  0.641 -0.219  0.325 -0.819
> apply(mydata, 2, mean, trim=0.2)    # 计算每行的截尾均值
[1]  0.287  0.475 -0.471  0.418 -0.792

首先生成了一个包含正态随机数的 6×5 矩阵。然后你计算了 6 行的均值,以及 5 列的均值。最后,你计算了每列的截尾均值(在本例中,截尾均值基于中间 60% 的数据,最高和最低 20% 的值均被忽略)。

FUN 可为任意 R 函数,这也包括你自行编写的函数(参见 5.4 节),所以apply() 是一种很强大的机制。apply() 可把函数应用到数组的某个维度上,而 lapply() 和 sapply() 则可将函数应用到列表(list)上。你将在下一节中看到 sapply()(它是 lapply() 的更好用的版本)的一个示例。

你已经拥有了解决5.1 节中数据处理问题所需的所有工具,现在,让我们小试身手。

5.3 数据处理难题的一套解决方案

5.1 节中提出的问题是:将学生的各科考试成绩组合为单一的成绩衡量指标,基于相对名次(前 20%、下 20%、等等)给出从A到F的评分,根据学生姓氏和名字的首字母对花名册进行排序。代码清单5-6 给出了一种解决方案。

代码清单5-6 示例的一种解决方案

> # 步骤1
> options(digits=2)        

> Student <- c("John Davis", "Angela Williams", "Bullwinkle Moose", "David Jones", "Janice Markhammer", "Cheryl Cushing", "Reuven Ytzrhak", "Greg Knox", "Joel England", "Mary Rayburn")
> Math <- c(502, 600, 412, 358, 495, 512, 410, 625, 573, 522)
> Science <- c(95, 99, 80, 82, 75, 85, 80, 95, 89, 86)
> English <- c(25, 22, 18, 15, 20, 28, 15, 30, 27, 18)
> roster <- data.frame(Student, Math, Science, English, stringsAsFactors=FALSE)

> # 步骤2
> z <- scale(roster[,2:4])             # 计算综合得分  

> # 步骤3
> score <- apply(z, 1, mean)             # 计算综合得分
> roster <- cbind(roster, score)        # 计算综合得分

> # 步骤4
> y <- quantile(score, c(.8,.6,.4,.2))                # 对学生评分

> # 步骤5
> roster$grade[score >= y[1]] <- "A"                    # 对学生评分
> roster$grade[score < y[1] & score >= y[2]] <- "B"    # 对学生评分
> roster$grade[score < y[2] & score >= y[3]] <- "C"    # 对学生评分
> roster$grade[score < y[3] & score >= y[4]] <- "D"    # 对学生评分
> roster$grade[score < y[4]] <- "F"                    # 对学生评分

> # 步骤6
> name <- strsplit((roster$Student), " ")

> # 步骤7
> Lastname <- sapply(name, "[", 2)                      # 抽取姓氏和名字
> Firstname <- sapply(name, "[", 1)                     # 抽取姓氏和名字
> roster <- cbind(Firstname,Lastname, roster[,-1])     # 抽取姓氏和名字

> # 步骤8
> roster <- roster[order(Lastname,Firstname),]             # 根据姓氏和名字排序

> roster
    Firstname   Lastname Math Science English score grade
6      Cheryl    Cushing  512      85      28  0.35     C
1        John      Davis  502      95      25  0.56     B
9        Joel    England  573      89      27  0.70     B
4       David      Jones  358      82      15 -1.16     F
8        Greg       Knox  625      95      30  1.34     A
5      Janice Markhammer  495      75      20 -0.63     D
3  Bullwinkle      Moose  412      80      18 -0.86     D
10       Mary    Rayburn  522      86      18 -0.18     C
2      Angela   Williams  600      99      22  0.92     A
7      Reuven    Ytzrhak  410      80      15 -1.05     F

以上代码写得比较紧凑,逐步分解如下。

步骤1 原始的学生花名册已经给出了。options(digits=2) 限定了输出小数点后数字的位数,并且让输出更容易阅读:

> options(digits=2)

> Student <- c("John Davis", "Angela Williams", "Bullwinkle Moose", "David Jones", "Janice Markhammer", "Cheryl Cushing", "Reuven Ytzrhak", "Greg Knox", "Joel England", "Mary Rayburn")
> Math <- c(502, 600, 412, 358, 495, 512, 410, 625, 573, 522)
> Science <- c(95, 99, 80, 82, 75, 85, 80, 95, 89, 86)
> English <- c(25, 22, 18, 15, 20, 28, 15, 30, 27, 18)
> roster <- data.frame(Student, Math, Science, English, stringsAsFactors=FALSE)
> options(digits=2)
> roster
             Student Math Science English
1         John Davis  502      95      25
2    Angela Williams  600      99      22
3   Bullwinkle Moose  412      80      18
4        David Jones  358      82      15
5  Janice Markhammer  495      75      20
6     Cheryl Cushing  512      85      28
7     Reuven Ytzrhak  410      80      15
8          Greg Knox  625      95      30
9       Joel England  573      89      27
10      Mary Rayburn  522      86      18

步骤2 由于数学、科学和英语考试的分值不同(均值和标准差相去甚远),在组合之前需要先让它们变得可以比较。一种方法是将变量进行标准化,这样每科考试的成绩就都是用单位标准差来表示,而不是以原始的尺度来表示了。这个过程可以使用 scale() 函数来实现:

> z <- scale(roster[,2:4])
> z
        Math Science English
 [1,]  0.013   1.078   0.587
 [2,]  1.143   1.591   0.037
 [3,] -1.026  -0.847  -0.697
 [4,] -1.649  -0.590  -1.247
 [5,] -0.068  -1.489  -0.330
 [6,]  0.128  -0.205   1.137
 [7,] -1.049  -0.847  -1.247
 [8,]  1.432   1.078   1.504
 [9,]  0.832   0.308   0.954
[10,]  0.243  -0.077  -0.697
attr(,"scaled:center")
   Math Science English 
    501      87      22 
attr(,"scaled:scale")
   Math Science English 

步骤3 然后,可以通过函数 mean() 来计算各行的均值以获得综合得分,并使用函数

cbind() 将其添加到花名册中:

> score <- apply(z, 1, mean)
> roster <- cbind(roster, score)
> roster
             Student Math Science English score
1         John Davis  502      95      25  0.56
2    Angela Williams  600      99      22  0.92
3   Bullwinkle Moose  412      80      18 -0.86
4        David Jones  358      82      15 -1.16
5  Janice Markhammer  495      75      20 -0.63
6     Cheryl Cushing  512      85      28  0.35
7     Reuven Ytzrhak  410      80      15 -1.05
8          Greg Knox  625      95      30  1.34
9       Joel England  573      89      27  0.70
10      Mary Rayburn  522      86      18 -0.18

步骤4 函数 quantile() 给出了学生综合得分的百分位数。可以看到,成绩为 A 的分界点为 0.74,B 的分界点为 0.44,等等。

> y <- quantile(roster$score, c(.8,.6,.4,.2))
> y
  80%   60%   40%   20% 
 0.74  0.44 -0.36 -0.89 

步骤5 通过使用逻辑运算符,你可以将学生的百分位数排名重编码为一个新的类别型成绩变量。下面在数据框 roster 中创建了变量 grade。

> roster$grade[score >= y[1]] <- "A"
> roster$grade[score < y[1] & score >= y[2]] <- "B"
> roster$grade[score < y[2] & score >= y[3]] <- "C"
> roster$grade[score < y[3] & score >= y[4]] <- "D"
> roster$grade[score < y[4]] <- "F"
> roster
             Student Math Science English score grade
1         John Davis  502      95      25  0.56     B
2    Angela Williams  600      99      22  0.92     A
3   Bullwinkle Moose  412      80      18 -0.86     D
4        David Jones  358      82      15 -1.16     F
5  Janice Markhammer  495      75      20 -0.63     D
6     Cheryl Cushing  512      85      28  0.35     C
7     Reuven Ytzrhak  410      80      15 -1.05     F
8          Greg Knox  625      95      30  1.34     A
9       Joel England  573      89      27  0.70     B
10      Mary Rayburn  522      86      18 -0.18     C

步骤6 你将使用函数 strsplit() 以空格为界把学生姓名拆分为姓氏和名字。把 strsplit() 应用到一个字符串组成的向量上会返回一个列表:

> name <- strsplit((roster$Student), " ")
> name [[1]]
[1] "John"  "Davis"
> name [[2]]
[1] "Angela"   "Williams"
> name [[3]]
[1] "Bullwinkle" "Moose"     
> name [[4]]
[1] "David" "Jones"
> name [[5]]
[1] "Janice"     "Markhammer"
> name [[6]]
[1] "Cheryl"  "Cushing"
> name [[7]]
[1] "Reuven"  "Ytzrhak"
> name [[8]]
[1] "Greg" "Knox"
> name [[9]]
[1] "Joel"    "England"
> name [[10]]
[1] "Mary"    "Rayburn"

步骤7 你可以使用函数 sapply() 提取列表中每个成分的第一个元素,放入一个储存名字的向量 Firstname,并提取每个成分的第二个元素,放入一个储存姓氏的向量 Lastname。”[“ 是一个可以提取某个对象的一部分的函数——在这里它是用来提取列表 name 各成分中的第一个或第二个元素的。你将使用 cbind() 把它们添加到花名册中。由于已经不再需要 student 变量,可以将其丢弃(在下标中使用–1)。

> Firstname <- sapply(name, "[", 1)
> Lastname <- sapply(name, "[", 2)
> roster <- cbind(Firstname, Lastname, roster[,-1])
> roster
    Firstname   Lastname   Lastname Lastname.1 Math Science English score grade
1        John      Davis      Davis      Davis  502      95      25  0.56     B
2      Angela   Williams   Williams   Williams  600      99      22  0.92     A
3  Bullwinkle      Moose      Moose      Moose  412      80      18 -0.86     D
4       David      Jones      Jones      Jones  358      82      15 -1.16     F
5      Janice Markhammer Markhammer Markhammer  495      75      20 -0.63     D
6      Cheryl    Cushing    Cushing    Cushing  512      85      28  0.35     C
7      Reuven    Ytzrhak    Ytzrhak    Ytzrhak  410      80      15 -1.05     F
8        Greg       Knox       Knox       Knox  625      95      30  1.34     A
9        Joel    England    England    England  573      89      27  0.70     B
10       Mary    Rayburn    Rayburn    Rayburn  522      86      18 -0.18     C

步骤8 最后,可以使用函数 order() 依姓氏和名字对数据集进行排序:

> roster[order(Lastname,Firstname),]
    Firstname   Lastname   Lastname Lastname.1 Math Science English score grade
6      Cheryl    Cushing    Cushing    Cushing  512      85      28  0.35     C
1        John      Davis      Davis      Davis  502      95      25  0.56     B
9        Joel    England    England    England  573      89      27  0.70     B
4       David      Jones      Jones      Jones  358      82      15 -1.16     F
8        Greg       Knox       Knox       Knox  625      95      30  1.34     A
5      Janice Markhammer Markhammer Markhammer  495      75      20 -0.63     D
3  Bullwinkle      Moose      Moose      Moose  412      80      18 -0.86     D
10       Mary    Rayburn    Rayburn    Rayburn  522      86      18 -0.18     C
2      Angela   Williams   Williams   Williams  600      99      22  0.92     A
7      Reuven    Ytzrhak    Ytzrhak    Ytzrhak  410      80      15 -1.05     F

瞧!小事一桩!

完成这些任务的方式有许多,只是以上代码体现了相应函数的设计初衷。现在到学习控制结构和自己编写函数的时候了。

5.4 控制流

在正常情况下,R 程序中的语句是从上至下顺序执行的。但有时你可能希望重复执行某些语句,仅在满足特定条件的情况下执行另外的语句。这就是控制流结构发挥作用的地方了。

R 拥有一般现代编程语言中都有的标准控制结构。首先你将看到用于条件执行的结构,接下来是用于循环执行的结构。

为了理解贯穿本节的语法示例,请牢记以下概念:

❑ 语句(statement)是一条单独的R语句或一组复合语句(包含在花括号 { } 中的一组 R 语句,使用分号分隔);

❑ 条件(cond)是一条最终被解析为真(TRUE)或假(FALSE)的表达式;

❑ 表达式(expr)是一条数值或字符串的求值语句;

❑ 序列(seq)是一个数值或字符串序列。

在讨论过控制流的构造后,我们将学习如何编写函数。

5.4.1 重复和循环

循环结构重复地执行一个或一系列语句,直到某个条件不为真为止。循环结构包括 for 和 while 结构。

1. for 结构

for 循环重复地执行一个语句,直到某个变量的值不再包含在序列 seq 中为止。语法为:

for (var in seq) statement

在下例中:

> for (i in 1:10) print("Hello")
[1] "Hello"
[1] "Hello"
[1] "Hello"
[1] "Hello"
[1] "Hello"
[1] "Hello"
[1] "Hello"
[1] "Hello"
[1] "Hello"
[1] "Hello"

单词 Hello 被输出了 10 次。

2. while 结构

while 循环重复地执行一个语句,直到条件不为真为止。语法为:

while (cond) statement

作为第二个例子,代码:

> i <- 10
> while (i > 0) {print("Hello"); i <- i - 1}
[1] "Hello"
[1] "Hello"
[1] "Hello"
[1] "Hello"
[1] "Hello"
[1] "Hello"
[1] "Hello"
[1] "Hello"
[1] "Hello"
[1] "Hello"

又将单词 Hello 输出了 10 次。请确保括号内 while 的条件语句能够改变,即让它在某个时刻不再为真——否则循环将永不停止!在上例中,语句:

i <- i – 1
在每步循环中为对象 i 减去 1,这样在十次循环过后,它就不再大于 0 了。反之,如果在每步循环都加1的话,R 将不停地打招呼。这也是 while 循环可能较其他循环结构更危险的原因。

在处理大数据集中的行和列时,R 中的循环可能比较低效费时。只要可能,最好联用R中的内建数值/字符处理函数和 apply 族函数。

5.4.2 条件执行

在条件执行结构中,一条或一组语句仅在满足一个指定条件时执行。条件执行结构包括

if-else、ifelse 和 switch。

1. if-else 结构

控制结构 if-else 在某个给定条件为真时执行语句。也可以同时在条件为假时执行另外的语句。语法为:

if (cond) statement
if (cond) statement1 else statement2

示例如下:

if (is.character(grade)) grade <- as.factor(grade)

if (!is.factor(grade)) grade <- as.factor(grade) else print("Grade already is a factor")

在第一个实例中,如果 grade 是一个字符向量,它就会被转换为一个因子。在第二个实例中, 两个语句择其一执行。如果 grade 不是一个因子(注意符号!),它就会被转换为一个因子。如果它是一个因子,就会输出一段信息。

2. ifelse 结构

ifelse 结构是 if-else 结构比较紧凑的向量化版本,其语法为:

ifelse(cond, statement1, statement2)

若 cond 为 TRUE,则执行第一个语句;若 cond 为 FALSE,则执行第二个语句。示例如下:

ifelse(score > 0.5, print("Passed"), print("Failed")) 
outcome <- ifelse (score > 0.5, "Passed", "Failed")

在程序的行为是二元时,或者希望结构的输入和输出均为向量时,请使用 ifelse。

3. switch 结构

switch 根据一个表达式的值选择语句执行。语法为:

switch(expr, …)

其中的…表示与 expr 的各种可能输出值绑定的语句。通过观察代码清单5-7 中的代码,可以轻松地理解 switch 的工作原理。

代码清单5-7 一个 switch 示例

> feelings <- c("sad", "afraid")
> for (i in feelings) 
+     print(
+         switch(i,
+                happy = "I am glad you are happy", 
+                afraid = "There is nothing to fear", 
+                sad    = "Cheer up",
+                angry = "Calm down now"
+         )
+     )
[1] "Cheer up"
[1] "There is nothing to fear"

虽然这个例子比较幼稚,但它展示了 switch 的主要功能。你将在下一节学习如何使用 switch 编写自己的函数。

5.5 用户自编函数

R 的最大优点之一就是用户可以自行添加函数。事实上,R中的许多函数都是由已有函数构成的。一个函数的结构看起来大致如此:

myfunction <- function(arg1, arg2, ... ){
    statements
    return(object)
}

函数中的对象只在函数内部使用。返回对象的数据类型是任意的,从标量到列表皆可。让我们看一个示例。

假设你想编写一个函数,用来计算数据对象的集中趋势和散布情况。此函数应当可以选择性地给出参数统计量(均值和标准差)和非参数统计量(中位数和绝对中位差)。结果应当以一个含名称列表的形式给出。另外,用户应当可以选择是否自动输出结果。除非另外指定,否则此函数的默认行为应当是计算参数统计量并且不输出结果。代码清单5-8 给出了一种解答。

代码清单5-8 mystats():一个由用户编写的描述性统计量计算函数

mystats <- function(x, parametric=TRUE, print=FALSE) { 
    if (parametric) {
        center <- mean(x); spread <- sd(x)
    } else {
        center <- median(x); spread <- mad(x)
    }

    if (print & parametric) {
        cat("Mean=", center, "\n", "SD=", spread, "\n")
    } else if (print & !parametric) {
        cat("Median=", center, "\n", "MAD=", spread, "\n")
    }
    result <- list(center=center, spread=spread) 
    return(result)
}

要看此函数的实战情况,首先需要生成一些数据(服从正态分布的,大小为 500 的随机样本):

set.seed(1234) 
x <- rnorm(500)

在执行语句:

y <- mystats(x)
之后,y$center 将包含均值(0.00184),y$spread 将包含标准差(1.03),并且没有输出结果。如果执行语句:
y <- mystats(x, parametric=FALSE, print=TRUE)
y$center 将包含中位数(–0.0207),y$spread 将包含绝对中位差(1.001)。另外,还会输出以下结果:
Median= -0.0207
 MAD= 1 

下面让我们看一个使用了 switch 结构的用户自编函数,此函数可让用户选择输出当天日期的格式。在函数声明中为参数指定的值将作为其默认值。在函数 mydate() 中,如果未指定 type, 则 long 将为默认的日期格式:

mydate <- function(type="long") { 
    switch(type,
        long = format(Sys.time(), "%A %B %d %Y"), 
        short = format(Sys.time(), "%m-%d-%y"), 
        cat(type, "is not a recognized type\n")
    )
}

实战中的函数如下:

> mydate("long")
[1] "星期日 八月 15 2021"
> mydate("short")
[1] "08-15-21"
> mydate()
[1] "星期日 八月 15 2021"
> mydate("medium")
medium is not a recognized type

请注意,函数 cat() 仅会在输入的日期格式类型不匹配 “long” 或 “short” 时执行。使用一个表达式来捕获用户的错误输入的参数值通常来说是一个好主意。

有若干函数可以用来为函数添加错误捕获和纠正功能。你可以使用函数 warning() 来生成一条错误提示信息,用 message() 来生成一条诊断信息,或用stop()停止当前表达式的执行并提示错误。20.5 节将会更加详细地讨论错误捕捉和调试。

在创建好自己的函数以后,你可能希望在每个会话中都能直接使用它们。附录 B 描述了如何定制R环境,以使 R 启动时自动读取用户编写的函数。我们将在第 6 章和第 8 章中看到更多的用户自编函数示例。

你可以使用本节中提供的基本技术完成很多工作。第 20 章的内容更加详细地涵盖了控制流和其他编程主题。第 21 章涵盖了如何创建包。如果你想要探索编写函数的微妙之处,或编写可以分发给他人使用的专业级代码,个人推荐阅读这两章,然后阅读两本优秀的书籍,你可在本书末尾的参考文献部分找到:Venables & Ripley(2000)以及Chambers(2008)。这两本书共同提供了大量细节和众多示例。

函数的编写就讲到这里,我们将以对数据整合和重塑的讨论来结束本章。

5.6 整合与重构

R中提供了许多用来整合(aggregate)和重塑(reshape)数据的强大方法。在整合数据时, 往往将多组观测替换为根据这些观测计算的描述性统计量。在重塑数据时,则会通过修改数据的结构(行和列)来决定数据的组织方式。本节描述了用来完成这些任务的多种方式。

在接下来的两个小节中,我们将使用已包含在 R 基本安装中的数据框 mtcars。这个数据集是从 Motor Trend 杂志(1974)提取的,它描述了 34 种车型的设计和性能特点(汽缸数、排量、马力、每加仑汽油行驶的英里数,等等)。要了解此数据集的更多信息,请参阅 help(mtcars)。

5.6.1 转置

转置(反转行和列)也许是重塑数据集的众多方法中最简单的一个了。使用函数t()即可对一个矩阵或数据框进行转置。对于后者,行名将成为变量(列)名。代码清单5-9 展示了一个例子。

代码清单5-9 数据集的转置

> cars <- mtcars[1:5,1:4]
> cars
                  mpg cyl disp  hp
Mazda RX4          21   6  160 110
Mazda RX4 Wag      21   6  160 110
Datsun 710         23   4  108  93
Hornet 4 Drive     21   6  258 110
Hornet Sportabout  19   8  360 175
> t(cars)
     Mazda RX4 Mazda RX4 Wag Datsun 710 Hornet 4 Drive Hornet Sportabout
mpg         21            21         23             21                19
cyl          6             6          4              6                 8
disp       160           160        108            258               360
hp         110           110         93            110               175

为了节约空间,代码清单5-9 仅使用了 mtcars 数据集的一个子集。在本节稍后讲解 reshape2 包的时候,你将看到一种更为灵活的数据转置方式。

5.6.2 整合数据

在 R 中使用一个或多个 by 变量和一个预先定义好的函数来折叠(collapse)数据是比较容易的。调用格式为:

aggregate(x, by, FUN)

其中X是待折叠的数据对象,by 是一个变量名组成的列表,这些变量将被去掉以形成新的观测, 而 *FUN *则是用来计算描述性统计量的标量函数,它将被用来计算新观测中的值。

作为一个示例,我们将根据汽缸数和挡位数整合 mtcars 数据,并返回各个数值型变量的均值(见代码清单5-10)。

代码清单5-10 整合数据

> options(digits=3)
> attach(mtcars)
> aggdata <-aggregate(mtcars, by=list(cyl,gear), FUN=mean, na.rm=TRUE)
> aggdata
  Group.1 Group.2  mpg cyl disp  hp drat   wt qsec  vs   am gear carb
1       4       3 21.5   4  120  97 3.70 2.46 20.0 1.0 0.00    3 1.00
2       6       3 19.8   6  242 108 2.92 3.34 19.8 1.0 0.00    3 1.00
3       8       3 15.1   8  358 194 3.12 4.10 17.1 0.0 0.00    3 3.08
4       4       4 26.9   4  103  76 4.11 2.38 19.6 1.0 0.75    4 1.50
5       6       4 19.8   6  164 116 3.91 3.09 17.7 0.5 0.50    4 4.00
6       4       5 28.2   4  108 102 4.10 1.83 16.8 0.5 1.00    5 2.00
7       6       5 19.7   6  145 175 3.62 2.77 15.5 0.0 1.00    5 6.00
8       8       5 15.4   8  326 300 3.88 3.37 14.6 0.0 1.00    5 6.00

在结果中,Group.1 表示汽缸数量(4、6或8),Group.2 代表挡位数(3、4 或 5)。举例来说,拥有 4 个汽缸和 3 个挡位车型的每加仑汽油行驶英里数(mpg)均值为 21.5。

在使用 aggregate() 函数的时候,by 中的变量必须在一个列表中(即使只有一个变量)。你可以在列表中为各组声明自定义的名称, 例如 by=list(Group.cyl=cyl, Group.gears=gear)。指定的函数可为任意的内建或自编函数,这就为整合命令赋予了强大的力量。但说到力量,没有什么可以比 reshape2 包更强。

5.6.3 reshape2 包

reshape2 包①是一套重构和整合数据集的绝妙的万能工具。由于它的这种万能特性,可能学起来会有一点难度。我们将慢慢地梳理整个过程,并使用一个小型数据集作为示例,这样每一步发生了什么就很清晰了。由于 reshape2 包并未包含在R的标准安装中,在第一次使用它之前需要使用 install.packages(“reshape2”)进行安装。

① 由同一作者开发的 reshape2 包是原 reshape 的重新设计版本,功能更为强大。

大致说来,你需要首先将数据融合(melt),以使每一行都是唯一的标识符-变量组合。然后将数据重铸(cast)为你想要的任何形状。在重铸过程中,你可以使用任何函数对数据进行整合。将使用的数据集如表5-8所示。

表5-8 原始数据集(mydata)

ID Time X1 X2
1 1 5 6
1 2 3 5
2 1 6 1
2 2 2 4

在这个数据集中,测量(measurement)是指最后两列中的值(5、6、3、5、6、1、2、4)。每个测量都能够被标识符变量(在本例中,标识符是指 ID、Time 以及观测属于 X1 还是 X2)唯一地确定。举例来说,在知道 ID 为 1、Time 为 1,以及属于变量 X1 之后,即可确定测量值为第一行中的 5。

1. 融合

数据集的融合是将它重构为这样一种格式:每个测量变量独占一行,行中带有要唯一确定这个测量所需的标识符变量。要融合表5-8 中的数据,可使用以下代码:

library(reshape2)
md <- melt(mydata, id=c("ID", "Time"))

你将得到如表5-9 所示的结构。

表5-9 融合后的数据集

ID Time 变 量
1 1 X1 5
1 2 X1 3
2 1 X1 6
2 2 X1 2
1 1 X2 6
1 2 X2 5
2 1 X2 1
2 2 X2 4

注意,必须指定要唯一确定每个测量所需的变量(ID和Time),而表示测量变量名的变量(X1 或 X2)将由程序为你自动创建。

既然已经拥有了融合后的数据,现在就可以使用 dcast() 函数将它重铸为任意形状了。

2. 重铸

dcast() 函数读取已融合的数据,并使用你提供的公式和一个(可选的)用于整合数据的函数将其重塑。调用格式为:

newdata <- dcast(md, formula, fun.aggregate)

其中的 md 为已融合的数据,formula 描述了想要的最后结果,而 fun.aggregate 是(可选的)数据整合函数。其接受的公式形如:

rowvar1 + rowvar2 + … ~ colvar1 + colvar2 + …

在这一公式中,rowvar1 + rowvar2 + …定义了要划掉的变量集合,以确定各行的内容, 而 colvar1 + colvar2 + …则定义了要划掉的、确定各列内容的变量集合。参见图5-1 中的示例。

图5-1 使用函数melt()和dcast()重塑数据

由于右侧(d、e 和 f)的公式中并未包括某个函数,所以数据仅被重塑了。反之,左侧的示例(a、b 和 c)中指定了 mean 作为整合函数,从而就对数据同时进行了重塑与整合。例如,示例 (a) 中给出了每个观测所有时刻中在 X1 和 X2 上的均值;示例 (b) 则给出了 X1 和 X2 在时刻 1 和时刻 2 的均值,对不同的观测进行了平均;在 (c) 中则是每个观测在时刻 1 和时刻 2 的均值,对不同的 X1 和 X2 进行了平均。

如你所见,函数 melt() 和 dcast() 提供了令人惊叹的灵活性。很多时候,你不得不在进行分析之前重塑或整合数据。举例来说,在分析重复测量数据(为每个观测记录了多个测量的数据) 时,你通常需要将数据转化为类似于表5-9 中所谓的长格式。示例参见9.6 节。

5.7 小结

本章总结了数十种用于处理数据的数学、统计和概率函数。我们看到了如何将这些函数应用到范围广泛的数据对象上,其中包括向量、矩阵和数据框。你学习了控制流结构的使用方法:用循环重复执行某些语句,或用分支在满足某些特定条件时执行另外的语句。然后你编写了自己的函数,并将它们应用到数据上。最后,我们探索了折叠、整合以及重构数据的多种方法。

既然已经集齐了数据塑形(没有别的意思)所需的工具,你就准备好告别第一部分并进入激动人心的数据分析世界了!在接下来的几章中,我们将探索多种将数据转化为信息的统计方法和图形方法。


文章作者: 谢舟
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 谢舟 !
 本篇
R 语言实战(第2版)第一部分 入门 R 语言实战(第2版)第一部分 入门
第一部分 入门第 1 章 R 语言介绍 本章内容❑ R 的安装❑ 熟悉 R 语言❑ 运行 R 程序 1.1 为何要使用 R1.2 R 的获取和安装1.3 R 的使用R是一种区分大小写的解释型语言。你可以在命令提示符(>)后每次输入并
下一篇 
机器学习推荐两本书 机器学习推荐两本书
1、周志华教授的《机器学习》2、李航博士的《统计学习方法》第二版想要学习机器学习的同学们可以看看哈!!!
2021-01-21
  目录