第 2 章 写代码时,有什么技巧以及规范?
2.1 变量的命名规范
忽略内存地址与变量名绑定的细节,我们可以理解为所写的代码都是将不同类型的数据储存到不同的变量中。那么我们便需要定义变量,这时便产生了变量的命名的规则。
我所了解到的比较普遍公认的变量命名规则有 Tidyverse Style Guide 以及fork了Tidyverse 命名规则的Google’s R Style Guide。
以笔者拙见,Google的命名规则使用驼峰式(例如DayOne而不是day_one)是因为很多程序员编程时常使用驼峰式。或者有一些R的base包中的函数使用点.(例如data.frame)但是据Hadley大神所说,这实际上可能会和S3对象的使用方法混淆。而且随着tidyverse在R中越来越流行,Hadley Wickham 对于R的影响越来越大,使用下划线_进行命名变得逐步流行起来。
故本文使用并且建议仅使用小写字母、数字和下划线_来进行变量命名。
例如:
<- data.frame(
df A = c(1,2,3),
B = c('a','b','c')
)<- names(df)
names_df #而不是
<- names(df)
NamesDf #或者
<- names(df) names.df
注:不建议简写TRUE为T,FALSE为F。
轶事1:至于为什么R的base包使用点.而不是_,这是因为在R的历史上,曾经还能用:=和下划线_进行赋值,不过现在早已经不可以使用这用方法进行赋值了。
2.2 操作符
2.2.1 <- 与 = 怎么用?
在上一段代码块中,我们可以见到两个操作符, <- 和 =。R语言中代码规范一般要求赋值用<-,传参用=。多数能查到的包库函数帮助文档中都遵循上述的规范。 例如data.frame的帮助文档中的示例:
<- LETTERS[1:3]
L3 <- sample(L3, 10, replace = TRUE)
fac <- data.frame(x = 1, y = 1:10, fac = fac) d
简单来说就是:定义变量用 <- ,在函数中用 =。
当然,每次需要输入两个键的<-早就被RStudio以一个快捷键的方式解决了,你可以通过输入Alt+-输入一个<-(好像自第一章以来,快捷键介绍在不断的增加…..,虽然本文会介绍一些笔者认为常用的快捷键,当然如果你想查看所有的快捷键,可以通过Alt+Shift+K方式查看RStudio中提供的所有的快捷键。)
轶事2:关于R为什么不使用其他语言的=作为赋值的操作符,那这就要追溯到R语言的前身S语言了,实际上是在很久很久以前的电脑上,<- 是可以用一个键打出来的。而且在R中直到2001年=才可以进行赋值操作。所以,使用<-进行赋值的另一个意义是,和老代码很好的兼容。
2.2.2 [ ]、[[ ]]
[ ]与[[ ]]
用形象的比喻来讲,列表像是一列火车,而[ ]操作符会将火车的其中一节车厢取出来,而[[ ]]操作符会将某一节车厢所运送的货物取出来。由于取出来的类型不同,这对于后续的数据处理是有不同的影响的,请注意这一点。
我们使用一个小例子解释这两个操作符的区别:
#定义一个含有两个元素的列表
<- list(
list_demo AA = c(1, 2, 3),
BB = c('a','b','c')
)#在R语言中,直接输入变量名便可起到打印的作用
list_demo## $AA
## [1] 1 2 3
##
## $BB
## [1] "a" "b" "c"
#查看类型
class(list_demo)
## [1] "list"
#用[ ]按位置选取第一个成分
1]
list_demo[## $AA
## [1] 1 2 3
#查看我们用[ ]操作符锁取出来的东西的类型
class(list_demo[1])
## [1] "list"
#用[[ ]]按位置选取第一个成分
1]]
list_demo[[## [1] 1 2 3
#查看我们用[[ ]]操作符锁取出来的东西的类型
class(list_demo[[1]])
## [1] "numeric"
我们定义了含有两个成分,每个成分是一个向量的列表。我们可以从输出的结果看出:使用[ ]操作符会取出某一列,其类型仍保持是list。(此示例取出一列,当然也可以取出多列)但是使用[[ ]]会将该列中的内容取出,类型变成了其中所放元素的类型。
2.2.3 需要小心使用的操作符:$
首先,$是[[ ]]的简化版本,它既有一些使用的便利,当然也存在一定使用上的问题。
便利与BUG
便利:对于一个名为data数据框,如果我们在R Script中输入data$,你会看到后面会弹出一个小的选择列表,通过上下方向键可以选择所需提取的列。这样的好处是你不必再去查看data里面有什么字段,因为他已经在选择列表中把所有的都列出来了,这对于使用者会非常的便捷。
BUG:$是部分匹配,[[ ]]是完全匹配。这是什么意思呢?请看下面的一个小例子:
#定义一个list
<- list(
list_demo AA = c(1, 2, 3),
BB = c('a','b','c')
)#取出名为A的元素(结果为NULL代表无此元素)
'A']]
list_demo[[## NULL
#取出名为A的元素(由于$的部分匹配,将错误地取出AA列,而不是返回NULL或者报错告诉你没有这一列)
$A
list_demo## [1] 1 2 3
如果我们想找list_demo中的A这一成分,使用[[ ]]我们会发现它会返回一个空值,代表没有一个叫做A的成分。但是由于$是部分匹配,它仅发现第一个字符匹配就直接将其取出,有时,这可能产生BUG。所以,慎用$。
注意:$还不支持变量作为参数传入。
<- list(
list_demo AA = c(1, 2, 3),
BB = c('a','b','c')
)names(list_demo)[1]]]
list_demo[[## [1] 1 2 3
1]]
list_demo[[## [1] 1 2 3
# 以下代码运行均会报错,$不支持位置编号选取,也不支持变量传入,仅支持字符串
#list_demo$names(list_demo)[1]
#list_demo$1
2.2.4 管道操作符 %>%
管道操作符应该算是R语言中比较有趣的操作符了。当然少不了的是他的快捷键插入使用方法:使用Ctrl + Shift + M。在linux系统中以|作为管道操作符,R语言中的管道操作符与其有着相似的作用。管道操作符来自于magrittr包,但是在加载tidyverse时便会自动加载管道操作符。当然如果你不加载tidyverse,你需要加载magrittr包以使用管道操作符。
管道操作符是什么?可以看下面的例子体会一下如歌使用管道操作符,以及使用管道操作符所带来的的区别。
# 读取数据加简单处理数据
# 方法1
<- 'xxxx/xxxx.xxx'
df_path <- import(df_path)
df1 <- filter(df1, count != 0)
df2 <- group_by(df2, id)
df3 summarise(df3, n = n())
# 方法2
<- 'xxxx/xxxx.xxx'
df_path <- import(df_path)
df <- filter(df, count != 0)
df <- group_by(df, id)
df summarise(df, n = n())
#方法3
summarise(group_by(filter(import('xxxx/xxxx.xxx'), count != 0), id))
#方法4
<- 'xxxx/xxxx.xxx'
df_path <- df_pah %>%
df import() %>%
filter(count != 0) %>%
group_by() %>%
summarise(n = n())
可以明显看出的是,由于人们的阅读习惯是从左至右,而方法3需要从内到外逐步阅读,所以方法3 并不是很利于阅读的。方法1和2是可能常规的一些写法,当然,这种方法对于细节的调整是友好的,不过方法1会储存大量的中间变量。这对于变量所占空间比较大的时候,产生很多的中间变量会加大内存的使用(*注)。方法2虽然通过使用一个变量的不断的覆写解决了这个问题,但是相比之下可能方法4更加清晰,而且不需要重复写赋值操作符以及重复写变量名,这就是管道的作用。
*注:当然,这会增加内存的使用,但是更准确的现状是R语言还是会尽量共享不同数据框的相同列以减小内存开销。
#定义一个数据框test_data
<- data.frame(
test_data A = c(1, 2, 3),
B = c(4, 5, 6)
)#对test_data新增一列作为test_data2
<- test_data %>%
test_data2 mutate(C = c(7, 8, 9))
#计算大小
::obj_size(test_data)
lobstr## 912 B
::obj_size(test_data2)
lobstr## 1,080 B
::obj_size(test_data2,test_data2)
lobstr## 1,080 B
可以看出第两个数据框的总大小是小于两个分别大小的和的。这实际上涉及到list这一类型的内存地址与变量绑定的一些细节。总之,R对于内存的管理还是较为智能的。
2.3 能读所有格式的函数,试试?
R语言中经常读的格式可能是csv,tsv,txt,excel等等。R的base包中自带了read.table、read.csv。tidyverse中的readr可以使用read_*一系列函数读取对应的格式,readxl可以用read_excel读取excel。
记不同的函数名多多少少有些麻烦,而且不同函数的参数有时是不一样的。有没有一站式解决的办法?有!那就是来自于rio包库的import,实际上这个函数是通过检测输入函数的后缀名,再调用对应的包库去读取文件。这也太方便了吧?不仅如此,rio包读取csv时会自动调用data.table中的fread函数,这是一个读取速度非常快的函数,对于超大文件经某些人测试可加速1000倍不止。而且import几乎支持所有格式:csv、psv、tsv、SAS的格式、stata的格式、json、html、excel的格式…..
当然rio包也有方便的export导出函数,可以通过后缀自动选择导出的函数进行导出
# 本文以下为读取示例
import('xxx.csv')
import('xxx.txt')
import('xxx.xlsx')