最近在学习R,主要在看《R语言入门与实践》这本书(Garrett Grolemund著,冯凌秉译,人民邮电出版社),做了一些笔记,分享给大家
目录
这篇笔记的PDF版本:https://pan.baidu.com/s/1PB874TCKAWOP65TM6euFPg
提取码:08kx
第一部分
第一章 R基础
1.1 下载、安装R和RStudio
- R下载地址:https://cran.r-project.org/mirrors.html
- 选择对应操作系统的版本,下载并安装即可
- 安装成功后的运行效果:
- RStudio下载地址:https://rstudio.com/products/rstudio/download/#download
- 安装成功后,运行的效果:
1.2 将R当成一个计算器
- 可以在R中进行各种数学运算,例如:
> 5*(7^2+1)-3
[1] 247
- 优先级:幂运算 > 乘除运算 > 加减运算
- 括号内的运算将优先进行,存在多个括号时,优先计算最内层的括号
1.3 对象
- 我们可以使用赋值符号
<-
(=
也可以,不过更推荐<-
)将数值储存在一个对象中,例如:
> a <- 5*(7^2+1)-3
> a
[1] 247
- 对象名不能以数字或一些特殊字符开头
- 对象名区分大小写,例如name和Name是不同的对象
- 如果把新的内容赋值给一个已存在的变量,则将会覆盖掉变量的原始值
- 使用
ls()
查看已经命名的对象,使用rm(对象名)
来删除某个对象 - 【注意】仅输入
rm()
会删除所有对象,慎用!
1.4 函数
- R中存在各种各样功能强大的函数,即执行某种任务的语句。例如,mean函数的作用是计算算术平均值,round函数的作用是四舍五入,sample函数的作用是从一个向量中抽取size个元素并返回,如下:
> mean(1:6)
[1] 3.5
> round(mean(1:6))
[1] 4
> sample(1:6,size=2)
[1] 5 3
- 正如上述例子中的
round(mean(1:6))
所展示的,函数可以嵌套 - 通过args函数查看函数的参数,例如:
> args(sample)
function (x, size, replace = FALSE, prob = NULL)
NULL
- 我们可以根据需要,自己编写自定义函数,例如下面是一个计算t检验效应量的函数
> #自定义函数:计算t检验的效应量
> conhens_d <- function(m1, m2, s) {
+ d <- (m1-m2)/s
+ return(d)
+ }
> #调用自定义函数
> conhens_d(23,21,2.5)
[1] 0.8
- 一些相关的信息:
-
conhens_d
是函数名 -
m1
、m2
、s
是函数中的参数,只在函数内部有效,如果想设置默认值,则可以在参数后面添加等号和数值,例如m1=23
- 花括号中的代码是函数的主体
- 通过
return()
返回值 -
#
是注释符,以#
开头的代码不会运行 - 如果指令太长,对于一条明显未完整的指令,敲一下回车键,就可以换行(会显示一个“+”,提示用户继续输入)
-
1.5 脚本
- 将代码写在脚本文件中,随后我们可以保存该文件,以便将来使用,在RStudio中的脚本窗口存在两个运行脚本的按钮:
- Source:运行脚本中的全部代码,对应的语句是
source('脚本名.R')
(另一种方法是Ctrl+A然后Ctrl+Enter) - Run:运行脚本中的某一行(快捷键Crtl+Enter)
- Source:运行脚本中的全部代码,对应的语句是
第二章 R包与帮助系统
2.1 R包
- 许多大学教授、程序员和统计学家使用R设计了数据分析的工具,并将这些工具无偿提供给他人使用,这些工具就属于R包。
- R包类似于C、C++和JavaScript中的库(library),或者Python中的包(package)
- 例如,ggplot2是一个很受欢迎的可视化包,为了使用这个包,我们首先需要确保计算机已经连接互联网,并在RStudio中的命令行输入
install.packages()
命令来安装包
install.packages('ggplot2')
- 但该步骤只是将ggplot2安装到我们的计算机中,想要使用这个包的函数,首先需要加载这个包
library('ggplot2')
- 现在,试着输入
qplot
,这是ggplot2包中的一个函数,输入后将会显示该函数的源代码
> qplot
function (x, y, ..., data, facets = NULL, margins = FALSE, geom = "auto",
xlim = c(NA, NA), ylim = c(NA, NA), log = "", main = NULL,
xlab = NULL, ylab = NULL, asp = NA, stat = NULL, position = NULL)
{
...
}
如果未使用library函数导入ggplot2包便直接输入
qplot
,结果会显示错误: 找不到对象'gplot'
,这是因为必须要载入一个包才能使用其中的函数现在试着输入如下的代码,体验一下
qplot
的功能吧
> x <- c(-1,-.8,-.6,-.4,-.2,0,.2,.4,.6,.8,1)
> y <- x^3
> qplot(x,y)
-
qplot
的含义是“quick plot”,其作用是依据两个相同长度的向量来绘制散点图,对于上述的例子,结果如下
- 如果想更新R包,则可以使用
update.packages()
命令,如果想同时更新多个R包,只需要把R包名称放入到一个向量中(第二章会介绍什么是向量),例如:
update.packages(c('ggplot2','reshape2','dplyr'))
- 如果想同时安装多个R包,也可以使用这样的方法
2.2 从帮助页面获取帮助
R的函数非常多,想要记住和学会所有函数几乎是不可能的,所幸每个R函数都有自己的帮助页面,在RStudio中输入
?函数名
即可打开相应的页面例如:
?qplot
- 此时将打开该函数的帮助页面(点击下图所示的按钮可以打开单独的页面)
- 一般来说,帮助页面包含了以下信息
简介 | |
---|---|
描述(Description) | 一段简短的有关该函数功能的描述 |
用法(Usage) | 告诉我们如何键入该函数和相应的参数名 |
参数(Arguments) | 列出该函数所包含的所有参数、各参数接受的赋值类型以及参数的作用 |
相关细节(Details) | 对函数的工作原理的进一步描述,通常会提到一些使用函数时的注意事项 |
返回值(Value) | 一段关于该函数运行后的返回值的简短描述 |
另请参阅(See also) | 与该函数相关的函数的列表 |
示例(Examples) | 确保可以无错运行的代码示例 |
- 此外,如果我们不小心忘记了某个函数的确切名称,可以使用
??函数名的关键词
来查找相关的函数,例如可以输入以下代码,查看与“plot”相关的函数
??plot
2.3 获取更多的帮助
- Stack Overflow(https://stackoverflow.com/)是一个非常流行的技术问答网站,可以在这里搜索和上传你遇到的问题
第二部分
第三章 R对象
3.1 原子型向量
- 原子型向量是R中最基本的数据类型,是一系列数值的集合
- 使用
c()
将数值或字符串合并为一个原子型向量,(c的意思是concatenate,也可以理解为collect、combine)
> x <- c(3,-2,4,7,5,-1,0)
> x
[1] 3 -2 4 7 5 -1 0
- 使用
is.vector()
查看某个对象是否是向量 - 使用
length()
获取向量的长度
> is.vector(x)
[1] TRUE
> length(x)
[1] 7
-
向量也支持计算:
- 当对象y的值为4时,
x+y
意味着x中每个元素加4 - 当y也是向量时,
x+y
意味着对应索引的元素相加
- 当对象y的值为4时,
原子型向量的6种基本类型:
简介 | |
---|---|
double(双整数型) | 也称为数值型(numeric),这个是R中默认的向量类型,例如上述例子中的向量x就属于double |
integer(整数型) | 储存整数数值,定义方法:在数值后加一个大写的L,例如y=c(1L, 2L, 3L) ,integer可以避免在double中可能出现的浮点误差问题(例如sqrt(2)^2-2 的输出结果不为0) |
character(字符型) | 储存文本,也称字符串(string),定义方法:添加单引号或双引号,例如text=c('hello','world') 。字符串也可以是数字,例如text=c('1','2','3') ,但character向量无法进行四则运算 |
logical(逻辑型) | 定义方法:大写的TRUE和FALSE,例如logic=c(TRUE, FALSE, TRUE) |
complex(复数类型)和raw(原始类型) | conplex向量用于储存复数,raw向量用于储存数据的原始字节 |
- 可以使用
typeof()
命令查看向量的类型 - 向量的常见属性有name(名称)、dim(维度)和class(类)。例如,对于上述例子中的向量x,我们可以通过
names()
添加名称:
> x <- c(3,-2,4,7,5,-1,0)
> names(x) <- c('one','two','three','four','five','six','seven')
> x
one two three four five six seven
3 -2 4 7 5 -1 0
- 使用
:
来快速定义向量
> x <- 1:6
> x
[1] 1 2 3 4 5 6
- 可以在这个方法的基础上灵活变通,例如,如果想要小数的话:
> x <- (1:6)*0.3
> x
[1] 0.3 0.6 0.9 1.2 1.5 1.8
3.2 矩阵
- 矩阵将数值储存在一个二维数组中,我们可以使用matrix函数将向量放置在矩阵中,并通过参数
nrow
、ncol
来设置矩阵的行、列的数量,例如:
> a <- c(1,2,3,4,5,6)
> m <- matrix(a,nrow=2,ncol=3)
> m
[,1] [,2] [,3]
[1,] 1 3 5
[2,] 2 4 6
3.3 数组
- 使用array函数来生成一个n维的数组,通过
dim
参数来设置维度大小
> a <- array(c(1:24), dim=c(2,3,4))
> a
, , 1
[,1] [,2] [,3]
[1,] 1 3 5
[2,] 2 4 6
, , 2
[,1] [,2] [,3]
[1,] 7 9 11
[2,] 8 10 12
, , 3
[,1] [,2] [,3]
[1,] 13 15 17
[2,] 14 16 18
, , 4
[,1] [,2] [,3]
[1,] 19 21 23
[2,] 20 22 24
3.4 类
- 使用class函数来查看对象的类,即class属性(class和type不是一个东西),例如,对于上述的数组a:
> class(a)
[1] "array"
- R使用了一个特殊的类来表述时间和日期数据
> now <- Sys.time()
> now
[1] "2020-11-03 20:35:47 CST"
> typeof(now)
[1] "double"
> class(now)
[1] "POSIXct" "POSIXt"
- 因子(factor)这个类储存的是分类数据,例如:
> gender <- factor(c('male','female','female'))
> attributes(gender)
$levels
[1] "female" "male"
$class
[1] "factor"
- factor函数使得在统计分析中加入分类变量变得简单,R时常会尝试将character向量转换成factor,除非有这样的需要,否则一般应该避免这种强制转换(使用
as.character()
函数)
3.5 强制转换
- 如果把不同类型的数据放入同一个对象中,R会将其强制转换为同一类型的数据(例如,将字符串和数字放入一个向量中,数字会被转变为字符串,将数字和逻辑型放入一个向量中,TRUE会被转换为1,FALSE会被转换为0)
- 可以使用as函数家族进行强制转换,例如:
> as.character(1)
[1] "1"
> as.logical(1)
[1] TRUE
> as.double(TRUE)
[1] 1
3.6 列表
- 列表(list)储存的是对象而非数值,通过列表,我们可以将不同类型的数据(数字啦、字符串啦、逻辑型啦)放置在一个集合里
> my_list <- list(1:10, 'text', TRUE)
> my_list
[[1]]
[1] 1 2 3 4 5 6 7 8 9 10
[[2]]
[1] "text"
[[3]]
[1] TRUE
3.7 数据框
- 数据框(data frame)是列表的二维版本。在数据框中,每一列是一个向量(和Excel表格比较相似),并且各个向量的长度要相等,例如:
> students <- data.frame(name=c('harry','ron','hermione'), gender=c('male','male','female'),age=c(17,17,17))
> students
name gender age
1 harry male 17
2 ron male 17
3 hermione female 17
- 数据框其实是一个class为data.frame的列表
- 上述的name和gender实际上将被存储为factor,如果不想被强制转换,可以在
data.frame()
中添加一个参数stringsAsFactors=FALSE
- 数据框的索引方法:数据框名称+美元符号+列的名称,例如
students$gender
可以返回students
这个数据框中名为gender
的一列(非常方便!)
> students$gender
[1] "male" "male" "female"
- 2021/3/29更新:我发现data.table比data.frame更好用,推荐大家尝试(需要先安装“data.table”包)
3.8 加载和保存数据
3.8.1 R基础包中的数据集
- R中的基础包datasets自带了很多数据集,这些数据集没有太多的分析价值,主要的作用是方便我们测试代码。可以使用以下命令来查看这些数据集的简介:
help(packages='datasets')
- 若想使用某个数据集,,只需要输入其名称即可(见第十章的笔记)
3.8.2 工作目录
当尝试加载或保存数据时,在没有额外指定文件地址的情况下,R都会在一个默认的文件目录中进行操作,这就是R的工作目录
可以使用如下代码查看R的工作目录:
> getwd()
[1] "C:\Users\韦子谦\Documents"
- 对于不同的计算机,默认的工作目录可能是不同的。以及,R无法读取中文路径的文件(至少在我自己的电脑上是这样的),最好将工作目录设置为其他的位置。改变工作目录的方法之一是setwd函数
> setwd('D:/R_project')
- 但我自己尝试的结果是,每次启动RStudio,之前用
setwd()
函数设置的工作目录就会失效,解决的方法是直接在RStudio的菜单栏里修改工作目录:“Tools”——“Globe Options”——“Gneral”——“Basic”——“R Sessions”——“Default working directory (when not in a project)”
3.8.3 读取和写入纯文本文件
纯文本文件是存储数据的常见格式之一,其将数据表存储在了文本文件中,表中每一行都对应文本中的一行,每一行的不同元素都用一些简单的符号隔开,即分隔符
常用的分隔符有空格、逗号,等等。每个文件只会使用其中一种分隔符
所有的纯文本文件都可以存储为扩展名为txt的文件,但有时候也可以使用特殊的扩展名以告诉用户该文件使用的分隔符。例如,csv(Comma-Separated Values)格式的文件使用逗号作为分隔符
要加载一个纯文本文件,可以使用read函数,举个例子:
data1 <- read.table('data.csv',sep=',',header=TRUE,na.strings='.',skip=3,nrow=5,stringsAsFactors=FALSE)
-
在这里,我们从R的工作目录加载了一个名为“data.csv”的数据集,并将其内容放置在名为
data1
的对象中,涉及到的参数如下:- sep:该参数告诉
read.table()
我们的文件使用了何种分隔符 - header:是否将第一行的数据视为变量名。如果第一行是变量名,则设置为TRUE
- na.strings:有时候,数据文件中会使用一些符号来表示缺失值(NA),该参数会告诉
read.table()
缺失值的位置,read.table()
则会将这些值读取为NA(在上面这个例子中,数据文件中用于表示缺失值的符号为“.”) - skip:有时也我们需要忽略数据文件开头的几行,此时可以用skip来指定从第几行开始读取数据
- nrow:告诉
read.table()
在读取了一定数量的数据行后就不再读取(指定的行数不包括表头) - stringsAsFactors:有时候R会将字符串类型转换为因子型,如果不想让R这么做,则可以将stringsAsFactors设置为FALSE
- sep:该参数告诉
当需要加载多个数据时,还可以通过更改全局设置来统一设定一些参数,例如:
options(stringsAsFactors=FALSE)
- read函数家族
默认设置 | 使用情形 | |
---|---|---|
read.table | sep=" ", header=FALSE | 通用读取函数 |
read.csv | sep=",", header=TRUE | 逗号分隔值(csv)的文件 |
read.delim | sep="\t", header=TRUE | 制表符分隔的文件(制表符就是Tab键敲出的那个符号,相当于缩进) |
read.csv2 | sep=";", header=TRUE, dec="," | 欧式小数点符号的csv文件 |
read.delim2 | sep="\t", header=TRUE, dec="," | 欧式小数点符号的制表符分隔的文件 |
- 在RStudio中,通过点击Environment面板的“import Dataset”,也可以将外部文档的数据导入至数据框中(该功能相当于
read.table()
命令的图形用户界面)
- 使用write函数保存数据,如下:
文件格式 | 函数及其使用方法 |
---|---|
csv | write.csv(r_object, file=filepath, row.names=FALSE) |
欧式小数点符号的csv | write.csv2(r_object, file=filepath, row.names=FALSE) |
制表符分隔 | write.table(r_object, file=filepath, sep="\t", row.names=FALSE) |
- 需要注意的是,write函数无法在计算机上创建新的文件夹,因此在保存文件时,请确保写入路径的文件夹都已创建好
- 此外,还可以使用bzfile(bzip2类型)、gzfile(gzip类型)或xzfile函数(xz类型)来压缩文件,用法如下:
write.csv(data1, file=bzfile("data1.csv.bz2"), row.names=FALSE)
- 读取压缩文件的方法如下:
read.csv("data1.csv.bz2")
3.8.4 读取和写入R文件
RDS文件可以存储单个R对象,而RData文件可以存储多个R对象
使用readRDS函数来打开RDS文件,例如:
data1 <- readRDS("data1.RDS")
- 打开RData的方法则更简单,只需运行load函数即可,其中的R对象会以其原本的名称加载到工作区中:
load("data1.RData")
【tips】RData不会告诉你读取了多少个R对象,也不会告诉你各个对象的具体名称,解决的方法之一是在命令行两侧加上括号,例如
(load("data1.RData"))
,这会强制R在读取文件时显示其中所有对象的名称将对象保存RDS文件的方法是saveRDS函数,第一个参数是R对象的名称,第二个参数是目标路径和文件名。保存为RData的方法是save函数,例如可以通过以下代码来保存a、b、c三个对象:
save(a,b,c,file="data1.RData")
- RData的缺点在于,我们可能自己都忘了里面保存了啥东西,而且如果当前的工作区存在同名对象的话,读取RData会导致同名对象被覆盖,因此保存为RDS文件会更好一些
- 相比存储为纯文本文件,存储为R文件的好处在于,因子、日期、时间或类属性等信息将得到保留,缺点则是,许多软件程序都无法读取R文件,因此其不利于数据分享,而且万一哪天你的电脑上没有了R,那么可能连打开R文件都会变得困难(我的理解是,如果你的数据还没分析完,那么可以保存为R文件以便下次继续分析,但如果数据已经处理好了,那么最终导出的结果最好是纯文本文件)
3.8.5 读取Excel
- 书中介绍的方法是XLConnect包,不过我发现目前RStudio默认使用的是readxl包(感觉通过选项菜单来导入文件比写代码要方便多了……)
3.8.6 从其它程序加载文件
- 有些时候,我们会拿到某个类型的数据,但却没有对应的原始程序,此时可以尝试使用以下R包中的函数来读取数据
文件格式 | 函数 | R包 |
---|---|---|
ERSI ArcGIS | read.shapefile | shapefiles |
MATLAB | readMat | R.matlab |
minitab | read.mtp | foreign |
SAS(永久性数据集) | read.ssd | foreign |
SAS(XPORT格式) | read.xport | foreign |
SPSS | read.spss | foreign |
Stata | read.dta | foreign |
Systat | read.systat | foreign |
3.8.7 从数据库中读取数据
- DBI包可以通过驱动程序连接至数据库,要从某个数据库中读取数据,首先需要下载其与DBI包相关联的R包,这些包为特定的数据库程序的原始驱动程序提供了API。例如,对于MySQL数据库需要使用RMySQL包,对于SQLite数据库需要使用RSQLite包,等等
第四章 R的记号体系
- R的记号体系可以帮助我们从R对象中提取值,一般的形式是先写出对象名,然后是一个中括号,例如
x[ , ]
,在R中有六种索引方式,分别为正整数、负整数、零、空格、逻辑值、名称
4.1 正整数
- 如下,
students[1,1]
返回的值是第一行第一列的元素
> students <- data.frame(name=c('harry','ron','hermione'), gender=c('male','male','female'),age=c(17,17,17))
> students
name gender age
1 harry male 17
2 ron male 17
3 hermione female 17
> students[1,1]
[1] harry
- 如果想取多个数值,可以用向量代替数值作为索引的参数,例如
students[c(1,2),1]
或students[1:2,1]
返回的是第一、第二行,第一列的两个元素
> students[1:2,1]
[1] harry ron
- 如果想提取一整行或一整列:
> students[1,] # 提取第一行
name gender age
1 harry male 17
> students[,1] # 提取第一列
[1] harry ron hermione
- 注意:如果从数据框中提取两行或两列以上的数据,将返回一个新的数据框,如果只提取一列,则只会返回一个向量
4.2 负整数
- 和正整数索引相反,负整数索引将返回负整数索引对应的元素之外的部分,例如,
students[-1,1]
将返回students
中第一行之外的所有元素,即第二、第三行
> students[-1,]
name gender age
2 ron male 17
3 hermione female 17
4.3 零索引
- 如果索引时使用0,那么返回的是一个空对象(没啥用处……)
> students[0,0]
data frame with 0 columns and 0 rows
4.4 空格索引
- 其实就是上文所说的提取一整行或一整列的方法,
students[1,]
和students[1, ]
的效果是一样的
4.5 逻辑值索引
- 这种方法将提取
TRUE
对应的元素,FALSE
对应的元素则不会提取,例如:
> students[c(TRUE, FALSE,TRUE),2]
[1] male female
4.6 名称索引
- 这是从数据框中提取列的常用方法
> students[1:3, 'name']
[1] harry ron hermione
4.7 美元符号和双中括号
- 美元符号可以很方便地提取出数据框中的某一列
> students$name
[1] harry ron hermione
- 如果一个列表中有若干个子列表,双中括号可以直接提取子列表中的元素,而不会返回一个列表对象
> l <- list(c(1,2,3), c(TRUE, FALSE))
> l
[[1]]
[1] 1 2 3
[[2]]
[1] TRUE FALSE
> l[1]
[[1]]
[1] 1 2 3
> l[[1]]
[1] 1 2 3
第五章 对象改值
5.1 就地改值
- 通过索引选择想要更改的元素,然后将新的值通过
<-
赋给该对象,例如:
> x <- c(1,2,3,4,5)
> x[1] <- 100
> x
[1] 100 2 3 4 5
- 也可以批量改值,例如:
> x[1:3] <- c(100,200,300)
> x
[1] 100 200 300 4 5
- 亦可以添加新的元素,例如:
> x[6] <- 6
> x
[1] 100 200 300 4 5 6
- 这种添加新元素的方法还可以用于在数据框中添加新变量(即,新的一列)
> students <- data.frame(name=c('harry','ron','hermione'), gender=c('male','male','female'),age=c(17,17,17))
> students$new <- 1:3
> students
name gender age new
1 harry male 17 1
2 ron male 17 2
3 hermione female 17 3
- 如果想删除某一列,则赋值为
NULL
即可
> students$new <- NULL
> students
name gender age
1 harry male 17
2 ron male 17
3 hermione female 17
- 这些操作都是即时进行的,所以在对数据框进行修改之前,最好先保留一份备份
5.2 逻辑取子集
5.2.1 逻辑测试
- R中的七种逻辑运算符
运算符 | 语法 | 判别 |
---|---|---|
> | a>b | a是否大于b? |
>= | a>=b | a是否大于或等于b? |
< | a | a是否小于b? |
<= | a<=b | a是否小于或等于b? |
== | a==b | a是否等于b? |
!= | a!=b | a是否不等于b? |
%in% | a %in% c(a,b,c) | c(a,b,c) 中是否包含a? |
- 对于每一个逻辑运算符,R会将运算符两边的值或向量进行对比,并返回逻辑值,例如:
> 3 > c(1,2,3,4,5)
[1] TRUE TRUE FALSE FALSE FALSE
【tips】:
%in%
是唯一不进行一一对比的运算符,其主要是判断左边的值是否出现在右边的对象中通过逻辑运算符,我们可以很方便地从对象中提取出我们想要的元素
> a <- c('group1','group2','group1','group2')
> b <- c(0.586, 0.762, 0.462, 0.631)
> a=='group1'
[1] TRUE FALSE TRUE FALSE
> b[a=='group1']
[1] 0.586 0.462
- 而且还可以使用多个运算符
> b[a=='group1' & b>0.5]
[1] 0.586
- 逻辑值取子集非常强大,可以帮助我们快速定位、提取和修改对象中的元素。在使用这种方法时,我们并不需要知道元素的具体位置,只需要知道如何用逻辑测试来描述这些元素即可
5.2.2 布尔运算符
- 以下几个运算符可以将多个逻辑测试的结果整合并输出为一个逻辑值(TRUE or FALSE)
运算符 | 语法 | 判别 |
---|---|---|
& | cond1 & cond2 | cond1和cond2是否同时为真? |
| | cond1 pipe cond2 | cond1和cond2是否至少有一个为真? |
xor | xor(cond1,cond2) | cond1和cond2中是否只有一个为真? |
! | !cond1 | cond1是否为假? |
any | any(cond1,cond2,cond3, ...) | 所有条件是否至少有一个为真? |
all | all(cond1,cond2,cond3, ...) | 所有条件是否同时为真? |
- 例如:
> any(1>2, 'yes'=='no',2*2==4)
[1] TRUE
5.2.3 缺失信息
- R中的特殊字符
NA
代表“not available”,可用于储存缺失信息。如果运算时存在缺少信息,那么NA
将被原封不动地保留,例如:
> 1+NA
[1] NA
- 某些情况下,
NA
的存在会使数据处理变得棘手,例如当我们想计算一个向量中所有元素的均值,但向量中存在NA
,将会导致:
> mean(c(1,2,3,NA))
[1] NA
- 此时可以添加一个可选参数
na.rm
(大部分R函数都会存在这个参数),意味着“NA remove”,例如:
> mean(c(1,2,3,NA),na.rm = TRUE)
[1] 2
- 另外,我们还可以通过
is.na
函数来判断是否存在NA
:
> is.na(NA)
[1] TRUE
> is.na(c(1,2,3,NA))
[1] FALSE FALSE FALSE TRUE
> any(is.na(c(1,2,3,NA)))
[1] TRUE
第六章 R的环境系统
6.1 R的环境系统
- R的环境系统很像电脑中的系统目录,是以层级结构存储的,其中,全局环境(globe environment)扮演着关键的作用,命令行中运行的所有命令都是在全局环境中进行的,在命令行中创建的所有对象也被存储在全局环境中(见下图)
- 任何时候,R的活动环境(active environment)都只有一个,其会将所有的新对象存储在当前的活动环境中,一般来说,全局环境就是活动环境
- 某些情况下,例如在运行函数时,R的活动环境就变成了运行时环境(runtime environment),R会在这个新的环境中运行该函数,并带着函数运行的结果回到调用该函数时的环境(也就是说,在函数内部修改外部环境的对象时,修改的只是该对象的副本)
- 当调用某个对象时,R首先会在当前环境中寻找该对象,如果没有找到,就会进入该环境的父环境(parent environment)中寻找,例如:
deal <- function(){
print(x)
}
x <- 'hello'
deal()
- 上述例子中,对象x是在全局环境中定义的,当运行deal函数的时候,R首先会在该函数的活动环境(也就是运行时环境)中寻找x,但该环境中并不存在x,因此R进入到该运行时环境的父环境(也就是全局环境)中寻找x,这就是为什么函数内部可以调用函数外部的对象,但却无法在全局环境中调用函数内部的对象
第三部分
第七章 程序
7.1 策略
-
当我们需要将一个复杂的任务编写为R程序时,可以通过如下三个策略来简化这个任务
- 将复杂任务分解成一些简单的子任务
- 使用实例
- 用通俗的语言描述解决方案,然后将其转换为R代码
-
通常可以将R程序分解为如下两种子任务
- 有序步骤(sequential step):例如河内塔问题就可以拆解成若干个步骤:把最大的圆盘移动到目标柱子、把第二大的圆盘移动到目标柱子……把最小的圆盘移动到目标柱子
- 同类情况(parallel case):以书中的简化版老虎机为例,当老虎机转出三个同样的符号时,使用一种算法来计算奖金,当转出的符号都是杠,则使用第二种算法,其余情况则使用第三种,等等
7.2 if和else语句
- 可以用if语句来统一处理某一类的情况,格式如下:
if (this) {
plan A
} else if (that) {
plan B
} else {
plan C
}
- this是某个逻辑测试或返回TRUE/FALSE的表达式,this为TRUE时运行plan A,如果this为FALSE而that为TRUE时,运行plan B,如果this和that都为FALSE,则属于else的情况,此时运行plan C
第八章 S3
- 先来看看下面这个例子,对象n的取值为1000000000,当n的class(也就是“类”)是“numeric”时,print出来的结果是1e+09,但当n的class被替换为"POSIXct"和"POSIXt"后,print出来的结果就变成了一个时间
> n <- 1000000000
> class(n)
[1] "numeric"
> n
[1] 1e+09
> class(n) <- c("POSIXct", "POSIXt")
> class(n)
[1] "POSIXct" "POSIXt"
> n
[1] "2001-09-09 09:46:40 CST"
- 在上述例子中,n的取值没有变,但是class变了,class的变化之所以会导致不同的输出结果,是因为R对于不同的class有着不同的处理方式
- class是属性的一种,属性(attribute)、泛型函数(generic function)和方法(method),三者共同构成了R的S3系统,这个系统掌管着R如何处理具有不同类的对象
8.1 属性
- 可以使用attributes函数来查看一个对象的属性,例如,
gender
这个对象就有两个属性,一个是levels属性,一个是class属性
> gender <- factor(c('male','female','female'))
> attributes(gender)
$levels
[1] "female" "male"
$class
[1] "factor"
- 又例如,刚刚提到的n,修改class之后,其属性如下:
> attributes(n)
$class
[1] "POSIXct" "POSIXt"
- 我们还可以通过attr函数来给对象添加新的属性,例如,我们给刚刚定义的n添加一个叫Hello的属性,该属性的值为World
> attr(n, 'Hello') <- "World"
- 现在用attributes函数来看看n的属性,可以发现新增了我们刚刚添加的属性
> attributes(n)
$class
[1] "POSIXct"
$Hello
[1] "World"
8.2 泛型函数
- 在控制台中输入对象名n,R对此的反应是
print(n)
,但正如上一节提到的,n的取值保持不变,当n的class改变的时候,print出来的内容是不同的,之所以print函数能根据不同情况/场合完成不同的任务(例如根据不同的class给出不同的结果),是因为print不是一个普通的函数,而是一个泛型函数
8.3 方法
- 作为一个泛型函数,
print()
的原理大致是这样的,其首先会查找一个对象的class属性,然后根据不同的class分配合理的输出显示方式 - 具体而言,当调用
print()
时,其会调用一个名为UseMethod的函数(见第十章的代码调试部分的图示),该函数会检查我们提供给print函数的第一个参数的class属性,然后将我们提供的待输出的对象交给一个新函数来处理。例如,对于一个class为POSIXct的对象,UseMethod会交给print.POSIXct函数来处理,而对于一个class为factor的对象,会交给print.factor来处理 - 在这里,print.POSIXct和print.factor被称为print函数的方法(method),这两个函数本身是普通的R函数,但特别之处在于R会调用它们去处理具有对应class属性的对象
- 可以通过methods函数来查看一个泛型函数所支持的方法,例如print函数支持将近200种方法
> methods(print)
[1] print.acf*
[2] print.anova*
[3] print.aov*
...
[183] print.xgettext*
[184] print.xngettext*
[185] print.xtabs*
8.4 小结
- 之所以称为S3系统,是由于其起源于S语言的第三个版本(S语言是R语言的前身),许多常见的R函数都是S3泛型函数,可以支持多种不同的类方法函数,S3系统使得R函数能在不同的场合有不同的表现
- R还有另外两个创建class属性行为的系统——S4系统和R5系统,虽然使用难度较大,但也提供了S3系统所没有的防护措施
- 关于S3的编程方法,可以参考Handley Wickham所著的《advanced R programming》
第九章 循环
- 循环(loop)是R用来重复完成某个任务的方法
9.1 for循环
- 格式如下,that是一个对象集合,对于出现在that中的每一个值,R都会循环运行一遍两个大括号之间的代码(也就是this)
for (value in that) {
this
}
9.2 while循环
- 格式如下,当condition为True时,R会反复循环两个大括号之间的代码,直到condition为False或用户手动停止程序(按下Esc键或RStudio控制台面板上的停止图标)
while (condition) {
code
}
9.3 repeat循环
- 格式如下,R会反复循环repeat中的代码,直到遇到了break命令
repeat{
code
if (this) {
break
}
}
9.4 小结
- 循环并非是在R中实现重复性任务自动化的唯一途径(例如,replicate函数也可以实现),遗憾的是,相比其它编程语言,循环代码在R中的运行速度更慢,但是别担心,我们可以使用向量化编程来提高代码的运行速度!
第十章 代码提速
10.1 向量化代码
- 向量化的代码可以接受一个含有多个值的向量作为输入,并且同时操作向量中的每个元素。其使用了R的三大法宝:逻辑测试,取子集和元素方式执行
- 先来看一个例子,state.division这个数据集(来源于R中自带的datasets包)记载的是美国各州所在的地区,分别为New England, Middle Atlantic, South Atlantic, East South Central, West South Central, East North Central, West North Central, Mountain, and Pacific。这50个元素与state.name中的州名相互对应
> state.division
[1] East South Central Pacific Mountain
[4] West South Central Pacific Mountain
[7] New England South Atlantic South Atlantic
[10] South Atlantic Pacific Mountain
[13] East North Central East North Central West North Central
[16] West North Central East South Central West South Central
[19] New England South Atlantic New England
[22] East North Central West North Central East South Central
[25] West North Central Mountain West North Central
[28] Mountain New England Middle Atlantic
[31] Mountain Middle Atlantic South Atlantic
[34] West North Central East North Central West South Central
[37] Pacific Middle Atlantic New England
[40] South Atlantic West North Central East South Central
[43] West South Central Mountain New England
[46] South Atlantic Pacific South Atlantic
[49] East North Central Mountain
9 Levels: New England Middle Atlantic ... Pacific
- 假如我们想计算“New England”这个地区的州的数量,那么非向量的方法会是这样的(其中一种思路):
# 非向量化的代码
count_loop <- function(){
count <- 0
for (i in 1:length(state.division)) {
if (state.division[i] == "New England") {
count <- count + 1
}
}
return(count)
}
- 在这个名为count_loop的函数中,我们定义了一个名为
count
的对象来记录州的数量,然后通过for循环来遍历state.division数据集中的每个元素,并通过条件语句来判断某个元素是否为New England
,如果条件为TRUE
,则count
加1 - 运行结果如下,“New England”有6个州
> count_loop()
[1] 6
- 向量化的代码则可以是这样的(只需要一行代码即可完成任务):
# 向量化的代码
count_set <- function(){
count <- length(state.division[state.division == "New England"])
return(count)
}
- 在这段代码中,我们先做了一个逻辑测试,即判断state.division数据集中有哪些元素的值为“New England”
> state.division == "New England"
[1] FALSE FALSE FALSE FALSE FALSE FALSE TRUE FALSE FALSE FALSE
[11] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE TRUE FALSE
[21] TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE TRUE FALSE
[31] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE TRUE FALSE
[41] FALSE FALSE FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE
- 然后我们基于返回的逻辑值,对state.division取子集(subsetting)
> state.division[state.division == "New England"]
[1] New England New England New England New England New England
[6] New England
于是,该子集的长度就是“New England”地区的州的数量啦,整个过程都是以元素方式执行的
逻辑测试,取子集和元素方式执行,就是R语言的三大法宝,用这个方法编写向量化的代码,可以让代码飞速运行
【tips】这段代码甚至还可以变得更简洁,如下
# 向量化的代码
count_set <- function(){
count <- sum(state.division == "New England")
return(count)
}
- 当然,这里的例子比较简单,所以两种风格的代码的运行效果相差无几,但对于大容量的数据,向量化代码的优势就很明显了。书中给出了一个求1000万个数值的绝对值的例子,结果是向量化代码的运行速度比非向量化的代码快30倍(我自己尝试的结果是快3倍,或许是和电脑性能有关系)
10.2 如何编写向量化的代码
- 有两种方法:
- 对于程序中的有序步骤,使用向量化的函数来完成
- 对于同类情况,使用逻辑值取子集的方式来处理,以便一次性处理完一类情况中的所有元素(for循环搭配if条件语句的组合常常能被逻辑值取子集所取代)
10.3 如何在R中编写快速的for循环
向量化代码并不意味着for循环就不应该在R中使用了,当使用for循环时,我们应该注意改善循环的效率:1)能放在循环外的代码就不要放在循环内,2)确保存储循环输出结果的对象有足够的容量
对于第二点,书中提到一个例子,将100万个数值循环放入一个名为output的对象,第一种方法是先定义一个长度为100万的output,然后循环放入数值,第二种方法是先定义一个长度为1的output,循环放入数值,每次循环都增加output的长度。结果是,第一种方法的运行时间不到两秒钟,第二种方法的运行时间则长达37分钟。这是因为,对于第一种方法,使用的都是同一个output,而第二种方法则需要不断地复制、删除旧的output,并在内存中寻找新的地方存放output的新版本,相当于让R对output反复读写了100万次
【tips】可以运用
sys.time()
命令来测试某个函数的运行时间
10.4 调试R代码
- 这个是附录里的内容,方便起见就放在第十章的最后了(附录中其他的内容也已经放在了各章节中合适的位置)
10.4.1 traceback
- 有时候我们需要在代码中调用函数,调用的函数往往又会调用别的函数,当出现错误时,可能会难以定位究竟是哪个函数出了问题
- 这时候就可以在命令行中输入
traceback()
,该命令会显示一个调用栈(call stack),即一个被调用函数的有序列表,其中第一行是出错的函数,最底层则是我们最初调用的函数 - 不过要注意,出错的原因不一定发生在第一行的函数中,但一般来说,越上层的函数,嫌疑越大
10.4.2 browser
在某个函数内添加一行代码
browser()
后,只要调用了该函数,函数运行到browser()
所在的一行时就会暂停,此时R处于一个新的模式,即浏览器模式(browser mode)(见debug部分的图)-
浏览器模式有以下几个特性:
- RStudio会显示该函数的源代码,并突出显示该函数暂停时所在的代码行
- 此时的运行环境不再是全局环境,而是该函数的运行时环境,RStudio的环境面板也将显示运行时环境的所有对象
- 命令行窗口中的命令提示符
>
会被Browse[1]>
所取代,我们可以在这里输入一些命令,来检查究竟是哪个地方出了错误 - RStudio控制台上方将出现“Next”、“Continue”和“Stop”三个按钮,作用分别是:运行该函数的下一行代码、运行该函数剩余的所有代码后退出浏览器模式、立刻中断并退出浏览器模式。此外还可以在命令行输入命令n、c、s来实现对应按钮的功能(如果函数中存在名称为n、c、s、Q的对象,那么输入这些字母将无法查看对象,想要查看的话可以用
get('n')
这样的方法)
10.4.3 断点
- 打开脚本窗口,在函数中想要暂停的一行的左边单击鼠标左键,将显示一个空心红点,即断点(break point),R会将断点识别为添加
browser()
语句的信号,当调用该函数时,运行到断点所在的地方就会暂停并进入浏览器模式(此时断点会变为实心红点)
10.4.4 debug
对于一个已经存在的函数,可以使用debug函数来进行调试(见下图),其作用相当于在函数第一行添加一句
browser()
,调试完后可以使用undebug()
来将第一行的browser()
移除,此外可以使用isdebugged()
来查看一个函数是否处于调试(debugging)模式如果嫌太麻烦,可以用
debugonce()
代替debug()
,效果是下次运行待调试函数时自动进入浏览器模式,并在调试结束后解除该函数的调试模式
10.4.5 trace
- 可以使用trace函数来将
browser()
语句添加至函数中的任意位置,例如以下代码可以将browser()
添加至名为“deal”的函数的第四行:
trace("deal",browser,at=4)
- 也可以用trace函数在函数中插入browser函数之外的函数,但这么做的时候务必确保有充分的理由
- 调试结束后,使用
untrace("deal")
来将函数恢复正常
10.4.6 recover
- 使用方法和
browser()
一样,区别是可以输入栈的编号来进入调用栈的任意层级,从而进入某个函数的运行时环境进行调试 - 通过添加全局选项,可以使R在每次报错时自动运行
recover()
命令:
options(error=recover)
- 可以通过以下代码来取消这一全局设置:
options(error=NULL)
----------2020.12.06更新----------
添加了第6、7、9、10章的笔记
----------2020.12.07更新----------
添加了第8章的笔记
----------2020.12.14更新----------
添加了第2章和附录部分的内容,修正了诸多表述和错字
----------2020.12.2更新----------
添加了目录