这一章是对前面内容的总结和深入,主要目的在于通过R较为简单的语法实现复杂的功能,提高数据分析的效率。
包括但不限于:
- 向量和矩阵的运算,
- 逻辑运算符(与,或,非),
- if-else语句,
- 循环语句(while和for),
- 遍历向量和矩阵,
- 函数的调用和自定义,
- 包(package)的安装和导入,
- 区分
lapply()
/sapply()
/vapply()
, - 几个常用的built-in函数的用法
-
sub(), gsub()
&grepl(), grep()
用于查找和替换
6.1 向量和矩阵的比较运算
前面的章节提到过,当我们比较一个矩阵和一个常数时,输出的结果是矩阵中每一个元素与该常数进行比较的逻辑值:
my_matrix <- matrix(1:9, ncol = 3)
my_matrix < 3
6.2 与(and),或(or),非(not)
- &:表示“与”运算,当且仅当&运算符左右两边的条件都成立时,运算结果为真
- |:表示“或”运算,当运算符左右两边至少有一个条件为真时,运算结果为真;当两边条件均为假 时,运算结果为假
- !:表示“非”运算,拥有最高运算优先级,用于逆转真假。非真,为假;非假,为真。
这里需要提示的是运算优先级,通常我们会使用逻辑运算符和比较运算符,切记要先比较大小,再判断逻辑运算符,例如:
5 <= 5 & 2 < 3
判断顺序是:先判断5是否小于等于5,结果为真;然后再判断2是否小于3,结果也为真。因此:
1 & 1
的结果为1。
*注意:编程语言中的比较运算符和数学中的不等式是不同的,在数学中,我们表达自变量x
的取值范围在1到5之间可以用1 < x < 5
的方式表达,但是这种不等式的写法在R中是错误的。不等式在这里表达的意思是自变量x
既大于1又小于5,包含了“与”的含义,因此写作:1 < x & x < 5
。逻辑运算符的朝向不会影响结果
6.3 条件分支语句:if-else
这里首先介绍单一的if语句,用于判断if后面的条件是否满足,若满足则执行,反之结束判断语句:
if (condition) {
expressions
}
if-else语句用于判断某些条件(if)是否满足,若满足则执行if后面的代码;若不满足则执行else后面的代码,现在结合具体例子来分析:
*注意:语法的正确性,条件(condition)写在小括号里,执行的操作(expression)在大括号里
if (condition) {
expression 1
} else {
expression 2
}
if-else语句只是判断了两种情况,即满足和不满足。当我们需要判断多个条件时,可以在else前面加入多个else if。
if (condition1) {
expr1
} else if (condition2) {
expr2
} else if (condition3) {
expr3
} else {
expr4
}
*注意:条件分支语句的各路分支的并集,必须等于全集,否则会报错。新手会忽略最后的else,而写成else if
现在举一个例子,可以自己算一下结果是多少,然后对比答案:
# Variables related to your last day of recordings
li <- 15
fb <- 9
# Code the control-flow construct
if (li >= 15 & fb >= 15) {
sms <- 2 * (li + fb)
} else if (li < 10 & fb < 10 ) {
sms <- 0.5 * (li + fb)
} else {
sms <- li + fb
}
# Print the resulting sms to the console
sms
正确答案是24。
6.4 while循环
满足条件,执行循环体,不满足则结束循环体,并运行后面的代码。
while (condition) {
expr
}
*注意:在循环体中,要设定一些操作,使条件不再满足从而结束循环体。否则循环会一直进行下去
下面举一个汽车导航提示驾驶员减速的循环,假设某条路限速60,现在车速75,导航语音提示减速,每次踩刹车后车速减少7:
speed <- 75
while(speed > 60) {
print("前方限速60,请减速")
speed <- speed - 6
}
speed # 循环体外输出一次最终车速
更进一步,我想让导航每次提醒减速时,都告诉我当前车速,可以通过以下操作实现:
speed <- 75
while(speed > 60) {
print(paste("前方限速60,当前车速:" , speed , "。请减速"))
speed <- speed - 6
}
speed
while循环体中可以加入条件分支:
speed <- 64
while (speed > 30) {
print(paste("Your speed is",speed))
if (speed > 48 ) {
print("Slow down big time!")
speed <- speed - 11
} else {
print ("Slow down!")
speed <- speed - 6
}
}
此外,我们可以通过break
主动结束循环体,假设i <- 1
,在i
不大于10的情况下,输出i
的三倍,如果i
的三倍能被8整除,则结束循环,否则i
增加1。
i <- 1
while (i <= 10) {
print(3 * i)
if ( (3 * i) %% 8 == 0) {
break
}
i <- i + 1
}
*注意:这个例子很典型,有助于理解赋值语句和判断语句
6.5 for循环
for循环适用于容器内元素的遍历。
以向量为例,有两种主要的遍历方法:
- 使用向量中的元素进行遍历
- 使用向量元素的索引进行遍历
# 定义一个向量
names <- c("Mercury", "Venus", "Earth",
"Mars", "Jupiter", "Saturn",
"Uranus", "Neptune")
# 1 使用元素
for (name in names) {
print (name)
}
# 2 使用元素的索引
for (i in 1:length(names)) {
print(names[i])
}
采用同样的操作,对list进行遍历:
# 定义一个list:newyorkcity
nyc <- list(pop = 8405837,
boroughs = c("Manhattan", "Bronx", "Brooklyn", "Queens", "Staten Island"),
capital = FALSE)
# 元素
for (info in nyc) {
print(info)
}
# 索引
for (i in 1:length(nyc)) {
print(nyc [[i]] )
}
*注意:在使用索引选择list中的元素时,需要两层中括号
对矩阵的遍历稍微复杂一些,需要两层for循环来分别遍历矩阵的行和列。通常的思路是:外层循环对所有行进行遍历,内层循环对每一行的所有列进行遍历。下面的例子中,首先定义一个3*3矩阵,然后输出矩阵的所有元素:
m <- matrix(1:9, byrow = TRUE, nrow = 3)
for (i in 1:nrow(m)) {
for (j in 1:ncol(m)) {
print(paste("矩阵第", i, "行,第", j, "列的元素是:", m[i, j]))
}
}
for循环中同样可以加入条件分支语句,具体操作类似while,这里不展开了。
6.6 break和next
break和next都可以中断较长的循环体的执行,但二者功能不同:
- break用于提前结束当前循环体,即使循环体并未执行结束,并执行循环体后面的代码。可以理解为跳过循环体未执行的部分。
- next用于结束当前执行的这一轮循环,并继续执行下一轮循环。并不是完全跳出循环体。
在这个例子中,我们定义一个关于一周内脸书主页访问量的向量,包含7个元素。使用for循环和if-else语句判断脸书主页是否受欢迎(第一次判断),然后分别进行两次if,判断是否存在极限值。如果日访问量超过16,则中断循环,如果日访问量低于5,则中断这一轮循环并开始下一轮。
facebook <- c(16, 9, 13, 5, 2, 17, 14)
for (fb in facebook) {
if (fb > 10){
print("You are popular!")
} else {
print("Be more visible!")
}
if (fb > 16) {
print("This unbelievable!")
break
}
if (fb < 5) {
print("This is embarrassing!")
next
}
print(fb)
}
*注意facebook
向量中后三个值:2为极小值,17为极大值,14为忽略的值
6.7 文本中某个字母出现的次数
在犯罪心理学领域,通常会借助嫌疑人使用某个字母或单词的频率来确认其身份。在这里我们尝试使用for循环来计数一段文字中,字母“r”出现的次数。要想实现这个操作,需要用到strsplit()
函数来将一段文本拆分为字母或汉字。
在console中输入?strsplit
来查看帮助:
- Split the Elements of a Character Vector
- Description:
Split the elements of a character vector x into substrings according to the matches to substring split within them.
用法是:
strsplit(x, split, fixed = FALSE, perl = FALSE, useBytes = FALSE)
看来,strsplit()
可以看作paste()
的反向操作,分别用于字符串的拆分和合并。
args中的x表示被拆分的对象,split表示从哪里开始分割,后面三个args有默认值,因此在使用时不用填写。
了解用法后,开始我们的例子。规则是:计数字母“u”之前所有“r”的个数。
# 定义字符串变量和字母变量
rquote <- "r's internals are irrefutably intriguing"
chars <- strsplit(rquote, split = "") [[1]] #两层中括号,因为strsplit函数输出一个列表
# 查看拆分后的字母
chars
# 初始化计数器
rcount <- 0
# 条件for循环
for (char in chars) {
if (char == "r") {
rcount <- rcount + 1
}
if (char == "u") {
break
}
}
# 输出结果
rcount
*注意:代码中
split = ""
表示分割字符串的每一个字母,我们当然也可以在引号内填入其他字符(见6.10)
计数器最终的结果是5.
看到这里,可能会有人不明白究竟为什么要在strsplit()
后面加上[[1]]
,我们不妨研究一下:
rquote <- "r's internals are irrefutably intriguing"
chars <- strsplit(rquote, split = "") # 去掉中括号看看chars的值和类型
chars
class(chars)
如果你对代码熟悉的话,看到chars的值就能想到这是一个列表,而且列表中只有一个元素。因此我们在字符串分割之后,直接将单个字母选择出来,以便for循环看起来更简洁。
rquote <- "r's internals are irrefutably intriguing"
chars <- strsplit(rquote, split = "")[[1]]
chars
class(chars)
思考一个问题,为什么分割后的列表只有一个元素?
因为被分割的对象是一个完整的字符串,如果分割向量中的元素,那么输出的列表中元素的数量与向量中一致。
6.8 函数
R中的函数可以理解为,用于实现特定功能的黑箱。黑箱的意思是,外界不知函数对输入值进行何种操作,外界只知道函数的输出结果是什么。
掌握常用函数的用法对于提高工作效率很关键,尽量做到每当遇到需求的时候,我们能迅速反应出合适的函数。如果没有满足要求的函数,那就自己写一个。
那么我们应该如何掌握函数的用法呢?建议初学者多看文本教程,把教程里用到的函数学明白,然后根据自己的实际需要,在RStudio中查阅帮助文档。
*注意:再全面的教程也不能涵盖全部的函数,每个人的需求不一样,因此查阅文献很重要
查看帮助
以sample()
函数为例,假设我们只知道它被用来选取样本,但不知到其具体用法,那么通过两种方式调出帮助文档:
help(sample) # 方法1
?sample #方法2
运行之后你会在页面右下角“Help”标签下看到关于这个函数的所有用法,以及所需要的arguments。注意观察下面4个args,前两个没有给出默认值,表示这两个args是必须指定的,后面两个则给定了默认参数,那么我们在调用函数的时候就可以忽略。
现在来讲mean()
函数,用于求平均值。它的用法比想象的复杂一些:
同样查看帮助:
mean(x, trim = 0, na.rm = FALSE, ...)
这里的x指用于求平均值的对象,trim表示同步修剪x中的极大值和极小值部分,尤其适用于当x中包含极端值的时候,na.rm表示是否空值。
# trim 举例
x <- c(0:10, 50)
x_bar <- mean(x)
c(x_bar, mean(x, trim = 0.10)) #trim 的值为0.1,即删除数据首尾各(0.1*12=1)个数
我们来验算一下:
如果不去除极端值,那么平均数应该是105/12=8.75
去掉一个最小值0,去掉一个最大值50,平均数应该是55/10=5.5
现在试验一下对空值的处理,记住一点,如果数据中有空值,那么算出来的平均数就成了NA
。这也成为了数据处理工作中的一个指示器,当summary
输出空值时,就要考虑怎样应对空值了。
a <- c(1, 2, 3, NA)
mean(a)
mean(a, na.rm = TRUE)
嵌套函数
在前面的章节早已用过嵌套函数。当我们使用print()
函数进行输出时,我们无法同时输出字符型变量和整形变量,因此我们需要在print()
函数中嵌入paste()
函数来拼接字符串。
*在python中也有类似的要求,但是可以用大括号、f和单引号的方式直接输出不同类型的变量
print(paste("当前速度为:", speed))
自定义函数
很多时候我们想按照自己的需求DIY一个函数。思路也很简单,那就是把一个函数赋值给一个变量,这样我们就可以通过变量名来调用该函数了,使用时只需注意参数的数量和形式。
定义函数的语法如下:
my_fun <- function(arg1, arg2...) {
body
}
arg1, arg2...是所有的参数,如果在定义时对参数赋值,那么这个参数就是可选参数。
以下是几个例子:
# 定义函数,求某个数的平方
pow_2 <- function(a) {
return (a*a)
}
pow_2(2)
# 定义 函数求两个数绝对值的和
sum_abs <- function(b, c) {
return (abs(b) + abs(c))
}
sum_abs(-2, 2)
两次运行的结果都是4。
现在使用前面提到的sample()
函数,定义一个掷色子的函数,来模拟每次掷色子的过程。以后打麻将如果手头没有色子,我们可以用R写一个。
throw_dice <- function() {
num <- sample(1:6, size = 1)
num
}
throw_dice()
*注意:这个函数是没有参数的
R传递变量的值
与某些编程语言不同,在调用函数的时候,R仅仅是将变量的值传递给函数作为输入,而非存储这个变量的地址。因此R中的函数不能改变输入变量的值,除非我们采用了“重写overwrite”操作:
triple <- function(x) {
x <- 3*x
x
}
a <- 5
triple(a)
a
*若想改变a的值,只需要将triple(a)赋值给a即可,这个步骤叫做overwrite
6.9 R packages
要想更加便利地实现复杂的功能,我们可以借助包或者库。
有两个与包相关的函数:
-
install.packages()
用于安装一个指定的包 -
library()
用于载入包
以常用的数据可视化工具ggplot2为例,
install.packages("ggplot2")
这一步通常需要比较长的时间,直到console出现“package ‘ggplot2’ successfully unpacked and MD5 sums checked”,才表示安装成功。然后就可以载入包了:
library(ggplot2)
包只有在载入完成之后才能使用。
ggplot2的详细用法会在第八章数据可视化中讲述,这里只是熟悉怎样安装和载入包。
*提示:在做一个数据分析项目时,最好单独创建一个R Skript来安装包(因为通常一个项目需要多个包)这样可以减少运行时间
6.10 lapply()
lapply()
的用法如下:
lapply(X, FUN, ...)
x表示一个向量或者列表,FUN表示一个函数。lapply()
的作用就是将函数FUN应用到容器的每一个元素上,并且输出一个列表,该输出列表包含的元素数量与x一致
接下来我将使用lapply()
把一个向量中每个元素所包含的大写字母批量转换成小写,实现这一步需要tolower()
函数,我们使用的向量容纳了统计学领域的先驱以及他们出生的年份。
# 统计学先驱
pioneers <- c("GAUSS:1777", "BAYES:1702", "PASCAL:1623", "PEARSON:1857")
# 分割名字和年份
split_math <- strsplit(pioneers, split = ":")
split_math
# 把名字转换成小写
split_low <- lapply(split_math, tolower)
# 查看结构
str (split_low)
上面介绍了如何通过lapply()
将R内置函数应用于一个向量。下面尝试自定义函数:
现在我想创建单独为名字或者年份创建一个列表,于是我定义了两个函数,分别用于选择向量的第一项和第二项:
pioneers <- c("GAUSS:1777", "BAYES:1702", "PASCAL:1623", "PEARSON:1857")
split <- strsplit(pioneers, split = ":")
split_low <- lapply(split, tolower)
# 定义函数select_first()
select_first <- function(x) {
x[1]
}
# 定义函数select_second()
select_second <- function(x){
x[2]
}
# 选择第一项: names
names <- lapply(split_low, select_first)
names
# 选择第二项: years
years <- lapply(split_low, select_second)
years
lapply搭配可选参数
现在尝试使用lapply()
,将包含可选参数的函数应用到一个容器,语法格式和前面类似,只需要在小括号中额外给参数赋值。上面提到的triple函数,是将输入值乘以三,我们来扩展一下:我想通过某个函数,得到一个值的任意倍数。我们从函数的定义开始:
multiply <- function(x, factor) {
x * factor
}
lapply(list(1, 2, 3), multiply, factor = 2) # 通过参数factor,求任意倍数
6.11 sapply()
sapply()
的功能以及用法与lapply()
相同:将一个函数应用到容器(向量或列表)。
但是输出结果的形式不同。
sapply(X, FUN, ...)
sapply()
可以看作是lapply()
的用户友好版本,默认返回一个向量或者矩阵(这也限制了sapply()
的使用范围)。
看下面的例子:定义一个列表,容纳7天的气温数据,每天测量5次温度。我想借助两种apply函数输出每天的最低气温,出于教学目的,我会顺便检查数据类型以加深记忆:
temp <- list(c(3, 7, 9, 6, -1),
c(6, 9, 12, 13, 5),
c(4, 8, 3, -1, -3),
c(1, 4, 7, 2, -2),
c(5, 7, 9, 4, 2),
c(-3, 5, 8, 9, 4),
c(3, 6, 9, 4, 1))
l <- lapply(temp, min)
class(l)
s <- sapply(temp, min)
class(s)
*注意:sapply的结果更简洁
6.12 vapply()
现在介绍最后一种apply函数:
vapply()
的功能更加强大,给了我们更多输出结果的可能
vapply(X, FUN, FUN.VALUE, ..., USE.NAMES = TRUE)
与前面一致,vapply()
将函数FUN应用于X的元素。参数FUN.VALUE表示我们想要得到的输出结果的模板。NAMES默认为真,表示尽可能输出一个有名字的数组。
*注意:FUN.VALUE的值必须给定
basics <- function(x) {
c(min = min(x), mean = mean(x), max = max(x))
}
vapply(temp, basics, numeric(3))
*这里FUN.VALUE的值是numeric(3),与basics输出结果的种类一致
在basics定义中加入中位数,vapply也要做出相应的改变。
basics <- function(x) {
c(min = min(x), mean = mean(x), median = median(x), max = max(x))
}
vapply(temp, basics, numeric(4))
现在再来试试匿名函数:
我们使用匿名函数判断每天的平均气温是否大于5,输出逻辑值:
vapply(temp, function(x, y){mean(x) > y}, y = 5, logical(1))
*函数没有名字而且不需要事先定义
6.13 其他常用函数
-
abs()
:求绝对值 -
sum()
:求和 -
round()
:求近似值 -
rev()
:reverse,用于逆转顺序 -
seq()
:生成序列,包含起点、终点和步长 -
rep()
:复制列表或向量中的元素 -
sort()
:把向量按升序排列 -
append()
:合并向量或列表 -
is.* ()
:检查一个对象所属的类 -
as.* ()
:改变一个对象的类 -
unlist()
:把list转化成vector
举几个简单的例子:
- unlist,append,sort
# The linkedin and facebook lists have already been created for you
linkedin <- list(16, 9, 13, 5, 2, 17, 14)
facebook <- list(17, 7, 5, 16, 8, 13, 14)
# Convert linkedin and facebook to a vector: li_vec and fb_vec
li_vec <- c(unlist(linkedin))
fb_vec<- c(unlist(facebook))
# Append fb_vec to li_vec: social_vec
social_vec <- append(li_vec , fb_vec)
social_vec
# Sort social_vec
sort(social_vec, decreasing = TRUE)
*注意:append函数将第二个arg合并到第一个arg末尾。
- rep,seq
rep(seq(1, 7, by = 2), times = 2)
6.14 正则表达式
grepl() &grep()
最基本的形式:我们希望验证一个字符串或者一个由字符串组成的向量是否包含某种特定的格式。会用到以下函数:
-
grepl()
:如果在字符串中找到了某个表达格式,则返回TRUE -
grep()
:返回包含这个格式的字符串的索引
我们试图使用这两个函数从一堆电子邮件地址中,找出域名包含“edu”的地址,具体用法如下:
# The emails vector has already been defined for you
emails <- c("john.doe@ivyleague.edu", "education@world.gov", "dalai.lama@peace.org",
"invalid.edu", "quant@bigdatacollege.edu", "cookie.monster@sesame.tv")
# Use grepl() to match for "edu"
grepl( "edu", emails)
# Use grep() to match for "edu", save result to hits
hits <- grep("edu", emails)
# Subset emails using hits
emails[hits]
第二个邮件地址并不是我们想要的,因为它的域名部分没有“edu”。这说明我们需要更精确的匹配方法:
# The emails vector has already been defined for you
emails <- c("john.doe@ivyleague.edu", "education@world.gov", "dalai.lama@peace.org",
"invalid.edu", "quant@bigdatacollege.edu", "cookie.monster@sesame.tv")
# Use grepl() to match for .edu addresses more robustly
grepl("@.*\\.edu$", emails)
# Use grep() to match for .edu addresses more robustly, save result to hits
hits <- grep("@.*\\.edu$", emails)
# Subset emails using hits
emails[hits]
-
@
后面的内容才是域名,因此我们只需要匹配at后面的内容 -
.*
:.
表示匹配任何字符,*
表示该字符出现0次或多次。原因是,在@
和.edu
之间,可能出现不同的字符 -
\\.edu$
:两次反斜杠用于表达后面的点.
是一个真实的字符,而不是像.*
里面的点那样表示匹配。$
表示我们在字符串的末端进行匹配,而非前端(^
)
sub() & gsub()
在验证完字符串中包含的格式之后,我们可以进行下一步操作:替换
sub()
: 函数仅仅替换第一个匹配的格式,即使后面仍有匹配,也不进行替换
gsub()
: 则是替换所有匹配的格式
现在我们将所有教育域名的邮箱域名替换成“@jianshu.com”
:
sub("@.*\\.edu$", "@jianshu.com", emails)
6.15 日期和时间
日期处理
虽然我们可以用字符串表示时间和日期,就像我们写在纸上那样。但是字符串会给我们带来诸多不便,例如字符串是无法进行算数运算的。因此数据分析过程中我们通常把由字符串表达的日期转变成日期类。
以下是几种不同的表示日期单位的格式:
-
%Y
:四位数表示年,(2022) -
%y
:两位数表示年,(98) -
%m
:两位数表示月份,(01) -
%d
:两位数表示某月的几号,(31)
-
%A
:英文表示周几,(Friday) -
%a
:星期几的英文缩写,(Fri) -
%B
:英文月份,(January) -
%b
:月份的缩写,(jan)
下面举几个例子:
- string to Date
# Definition of character strings representing dates
str1 <- "May 23, '96"
str2 <- "2012-03-15"
str3 <- "30/January/2006"
# Convert the strings to dates: date1, date2, date3
date1 <- as.Date(str1, format = "%b %d, '%y")
date2 <- as.Date(str2)
date3 <- as.Date(str3, format = "%d/%B/%Y")
date1
date2
date3
# Convert dates to formatted strings
format(date1, "%A")
format(date2, "%d")
format(date3, "%b %Y")
- Ask R for today's Date and current time:
today <- Sys.Date()
today
now <- Sys.time()
now
时间处理
-
as.POSIXct()
:将一个字符串转换成时间类型 -
format()
:将POSIXct对象转换成字符串
表示时间的符号如下: