R语言程序包开发:
转载自: 挑圈联靠, 想提高文章的引用率?写个R包吧!系列传送门
01 必备工具
前言
R语言程序包是R语言的灵魂,是R语言的核心,每一个R语言用户都会使用到R包。2006年3月15日,第一个R包(coxrobust)加入CRAN,截止2020年5月17日,已经有超过15000个R包,这些R包涵盖了各个领域,解决了各种各样的问题。
R包的易用性是R广受欢迎的重要原因,R包开发简单、易学,使得各行各业的从业者,即使是非计算机人员,都可以加入到R的编程中来,带来了R繁荣的景象。
既然这么多R包,那还有写包的必要吗?答案是肯定的,现实中的问题千千万万,15000个R包并不能解决所有问题。
如果R包足够实用,确实帮大家解决科研中遇到的统计和可视化问题,大家在应用你的R包的时候自然会引用你介绍R包的文章,自然文章引用量会指数级上涨!
本教程将从零教你如何制作、发布自己的R包。
写包的好处
• 彰显自己的实力!你说你会R,凭啥?有了自己的R包,也是一种实力的体现
• 创造!code changes world!学会写代码,那么你就可实现自己的想法,甚至做出创新的事物
• 软件著作权!你的原创代码可以申请软件著作权,进一步提升自己
• 发表论文!如果你的包是一个创新,可以写论文发表,让更多的人使用
• 完善方法学!不断地修正自己的代码,不断的完善,会加深你对代码和方法学的理解
• 方便自己!即使你不准备发布你的R包,也能方便自己的工作
安装必备工具
制作R包需要的工具有:电脑1台、R软件、RStudio软件、Rtools软件、devtools包、roxygen2包。下面我来详细讲一下安装过程。
1.电脑、R软件、RStudio软件自行安装
电脑的系统可以是windows、macos、linux。
R软件及RStudio软件推荐使用最新版本。
2.Rtools软件的安装
许多人误以为Rtools是一个R包,所以会使用install.packages("Rtools")命令来安装,其实是不对的,Rtools其实是windows系统下的软件。
在windows和macos中,Rtools会随着安装RStudio的时候一起安装,如果没有安装,那么windows系统需要自行安装Rtools,macos需要安装Xcode。他们的作用是编译和解析命令。
windows系统Rtools软件的网址是:https://mirrors.tuna.tsinghua.edu.cn/CRAN/ ,根据自己当前使用的R版本,下载对应版本的Rtools软件。比如,我当前使用的R版本是3.6.2,那么我选择下载Rtools35.exe。
下载完成后,双击Rtools35.exe进行安装。
Rtools并不支持中文,我们可以选择英语,点击OK即可。
接下来会告诉你Rtools主要用来创建R语言的应用,对于windows系统来说,需要将其添加入环境变化。
安装的默认地址是C:Rtools,建议不要修改。
安装的形式,我们直接使用推荐的就可以。
最关键的一步:Add rtools to system PATH,一定要勾上,表示加入电脑的环境变量。
这里问我们要不要修改环境变量,我们看到C:Rtoolsin已经被加入到了PATH中,我们不需要再次对其进行编辑。
再次核对一下安装信息。点击Install,开始安装。
开始安装。
安装成功!
3.安装依赖包
R包的制作依赖于devtools包和roxygen2包,在这两个包出现之前,制作R包是一件十分繁琐的事情,许多事情需要手动完成,一有不慎,就会出错,修改说明文档更加痛苦。
devtools包的出现,尤其是在RStudio软件加入了devtools的功能后,R包制作变得十分简单。
如果说devtools包让制作变得简单,那么roxygen2包彻底解放了R包开始者,我们再也不用为复杂的注释而头疼,可以一边写函数,一边写注释,将精力集中到写代码中,最后运行devtools::document()命令,就可以直接将所有注释写入R的说明文档。
devtools包和roxygen2包的安装命令是
install.packages(c("devtools","roxygen2"))
4. 检查
最后,我们使用命令devtools::has_dev()来检查一下准备工作。运行这行命令,返回“Your system is ready to build packages! ”表示一切准备就绪,可以开始了。
library(devtools)
has_devel()
02 创建R包
一节,我们讲了创建R包的准备工具,这一节,开始创建R包!
1. 命名规则
R包的命名主要有以下几个规则:
• 必须以字母开头(大写或小写),如:ggplot2,不能写成2ggplot
• 只能包含字母(大写或小写),数字和点,如:covid19.analytics
• 不能和已有的包名称冲突,已有的包指CRAN和bioconductor上的包
CRAN上已有的包可以在网页https://cran.r-project.org/web/packages/available_packages_by_name.html 里查找,bioconductor上已有的包可以在网页https://bioconductor.org/packages/release/BiocViews.html#___Software 里查找。
也可以使用命令available.packages()
查看CRAN上的包,bioManager::available()
来查看bioconductor上已有的包。
在创建R包前,不仅需要确保自己的包名是有效的,而且还有起一个好名字。好名字的优点包括:
• 易记:包的名字不宜全部使用小写字母,可以使用驼峰式如MaN,或aBa,或者可以加入点,如gg.gap
• 和功能相关:比如做稳健cox回归的包叫做coxrobust
• 注意版权!已有TCGA是知名网站的名字,那么R包的名字应尽量避免重复,可以加入r,如rTCGA。
• 不宜太长:很多人会把包的名字起的过长,在CRAN和bioconductor上,大部分R包的名字在4到9个字符之间,过长的包名拼写起来十分困难,无疑增加了使用的难度。
****▲**** (上图数据来自CRAN和bioconductor,截止日期是2020-05-18)
2. 创建R包
现在,我们来创建我们的第一个R包(first package):fpkg。
首先,我们使用命令dir.create()在d盘下新建文件夹mypkg,并使用命令setwd()设置工作目录至mypkg文件夹下,以后我们创建的所有R包都在这个目录里面。
dir.create('d:/mypkg')
setwd("d:/mypkg")
接着使用usethis包中的create_package()函数来创建R包,usethis包会随着devtools包的安装而安装,随着devtools包的调用而调用。
library(devtools)
create_package('fpkg')
create_package('fpkg')命令在mypkg文件夹下创建了fpkg文件夹,这个就是我们的R包了,并且自动打开了一个新的界面:fpkg.Rproj,可以直接关闭它。fpkg文件夹(也就是fpkg包)默认创建了以下几个文件:
• .Rproj.user文件夹:RStudio的项目文件夹,不需要关注
• R文件夹:存放R代码文件夹,**重要*****
• .gitignore文件:存放git信息的文件,会被忽略
• DESCRIPTION文件:R包的说明文档,这是整个包的说明文档,**重要*****
• fpkg.Rproj文件:RStudio的项目文件,我们以后编辑R包的时候,都是通过打开它来编辑的,因为这个文件包含了创建R包的菜单,**重要****
• NAMESPACE文件:存放R包函数命名空间的文件,不需要手动编辑,**极其重要*******
create_packages()命令后会自动打开fpkg.Rproj文件,如果不想打开这个文件,可给open参数赋值FALSE,即
create_package('fpkg',open = FALSE)
2. R包中的文件夹
R包中可以包含很多文件夹,不同的文件功能不同,常用的有R文件夹、Data文件夹和vignette文件夹。
• R文件夹是存放代码的地方,也就是我们所有的R代码
• Data文件夹不像R文件夹会自动产生,你可以使用函数use_data()来添加R数据,也可以手动新建,在后面的章节中会详细讲解如何添加数据
• vignette文件夹是用来添加markdown格式的说明文档,可以使用函数use_vignette()来创建,需要注意的是,如果你要添加vignette文件夹,需要安装knitr包、rmarkdowan包和pandoc。前2者的安装可以使用命令install.packages()来安装即可,pandoc的安装参考它的网站https://pandoc.org/installing.html ,后面的章节中会详细讲解vignette的书写。
R包文件的缺陷
很遗憾!R包内的文件夹不支持二级目录,也就是R文件夹下不能再有下一级目录,这个也是R包的缺陷,因为如果函数过多,打理起来会比较麻烦。
小结
今天,创建了我们的第一个R包,它的名字叫fpkg,它是一个空包,并且我们熟悉了R包内的各个文件的作用。
03 封装包
现在,我们已经有了第1个R包:fpkg,为了让大家对制作过程有更加直观的认识,接下来,我们直接封装包。
封装R包主要有2个步骤:写入注释和建包。 有3种实现方式:菜单、命令和快捷键。
1.打开项目文件
上一节,我们介绍了R包内的常用文件,有一个特别重要的项目文件fpkg.Rproj,现在,我们双击打开它。
和RStudio普通的界面不同,在菜单栏多了可下拉的build菜单项,这里包含了封装R包常用的功能。
2.通过菜单封装包
2.1 写入注释
首先是写入注释,在build菜单下,单击Document项。
在build窗口中,我们可以看到:更新文档,更新roxygen版本的文档,载入fpkg包,最后完成更新。其实这一步就是将roxygen的注释,转化成R函数的说明文档,如何写roxygen注释我们后面会进一步讲解。
2.2 建包
写好注释后,就是建包了,使用build菜单下的Install and Restart或者Clean and Rebuild均可,选择任意一个单击即可,比如我选择Install and Restart。
选择Install and Restart之后,在build窗口,就可以看到:fpkg包被安装到library文件夹下,最后提示Done (fpkg)表示安装成功。在R的控制中,我们可以看到已经使用library()命令调用了fpkg包,fpkg已经可以使用了。
3.使用快捷键封装包
使用快捷键封装包更为简单
• 写入注释:Ctrl/Command + Shift + D,D就是document的意思 • 建包:Ctrl/Command + Shift + B,B就是build的意思
4.使用命令封装包
不论是快捷键还是菜单,本质上都是调用devtools包中的document()函数和build()函数,这并不表示使用命令封装不重要,相反,它非常重要,因为有一些信息仅仅在使用命令的时候才会给出。
• 写入注释:devtools::document() • 建包:devtools::build()
使用命令封装包后的信息,不会再build窗口中显示,而是在控制台中显示。
5.调用包
封装完R包后,我们可以使用library()命令来调用我们的包了。
没有任何报错信息,说明fpkg包制作成功。
在package窗口中,也可以找到我们的fpkg包。
小结
现在我们已经学会如何创建、封装R包了,但是fpkg还只是1个空包,没有任何内容,下一节,我们将开始向包中添加内容---函数。
04 创建函数
函数是R的核心,R是函数的集合。在R中,行使功能的单位就是函数,几个函数捆在一起就成了R包。不管是开发R包,还是学习R,会写函数是必备的技能。
R中有4种常用的函数:标准函数、闭包、中缀操作符和替换函数,也有一些常用但不可编辑的函数,如特殊操作符;还有一些不常用的函数,如非标准计算、元编程。
本节内容,我们主要介绍标准函数、闭包、中缀操作符和替换函数。
1.标准函数
1.1 标准函数的结构
标准函数由3个部分组成:函数名、参数和函数体,使用function()函数来创建。在下图中,fun_name表示函数的名字,函数function()括号中的argument表示参数,花括号内的部分就是函数体。
1.2 命名规则
标准函数名的命名规则和R包的命名规则类似:
只能包含字母(大写或小写)、数字、点和下划线
• 必须以字母开头
• 不能以点或下划线结尾
一个好的函数名,需要与他的功能相关,如plot(),告诉我们这个一个画图的函数,如sum()是一个求和的函数。函数名最好都是小写字母或数字,这样使用者使用起来更加容易,如果大写、小写、字母、下划线掺杂在一起,拼写起来就十分困难,这并不是一个好的函数名。
比如,我们要创建1个函数sum2,可以使用下面的命令创建
<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="R" cid="n15424" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">sum2 <- function(){
}</pre>
1.3 参数
如果想激活一个函数,那么就需要给他传入参数,参数的作用就是给函数传递值。一个函数可以包含有1个或多个参数,也可以没有参数。参数可以传递数值、字符串、向量、数据框、函数、表达式等数据对象。参数在初始时可以为空,也可以带有默认值。
比如,sum2函数的功能是实现2个参数a和b的和
<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="R" cid="n15429" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">sum2 <- function(a, b){
sum <- a + b
return(sum)
}</pre>
我们将a和b写在function()函数的内部,表示a和b将会向函数体传递数据。运行这个函数,再来求1和2的和。当我们运行这个函数之后,在全局环境中,就多了一个函数sum2。
在sum2(a=1, b=2)中,我们给a赋值1,给b赋值2,将参数名称省略,直接写sum2(1, 2)时,1也会赋值给a,2赋值给b,因为当省略参数名称时,会按照参数位置关系赋值,a在第1位置,b在第2位置,以此类推,不同参数位置之间使用逗号隔开。
现在,我们给b赋值默认值1
<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="R" cid="n15438" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">sum2 <- function(a, b=1){
sum <- a + b
return(sum)
}</pre>
当b有默认值时,b的赋值将不是必须,如果不给b赋值,将会使用默认的赋值,如果需要改变b的数值,那么可以随意修改。
上面所举例的a和b都是任意数值,当参数取值是有限的,比如性别(sex)取值男和女,在构建函数时,最好将所有取值列出,并使用match.arg()进行匹配。
<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="R" cid="n15443" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">return_sex <- function(sex = c('男', '女')){
sex=match.arg(arg = sex,
choices = c('男','女'))`
return(sex)
}</pre>
在march.arg()(匹配参数)函数中,有2个主要参数,arg(参数)和choices(选项),arg=sex,表示我们要匹配的参数是sex,choices = c('男','女')表示sex只能从男和女中挑选数值。
这样做的好处就是当你给sex赋值了男和女以外的数值时,会报错!
1.4 三连点
上面的a、b和sex都是有具体名称的参数,但有时候,我们并不需要设置具体的参数,或者说参数的个数可以是无数个,任意多个,那么这个时候就需要使用三连点,可以使用list()函数来获取三连点传递的数值。
我们将sum2函数的功能重新定义为任意多个数据的求和
<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="R" cid="n15453" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">sum2 <- function(...){
list = list(...)
sum = do.call(sum,list)
return(sum)
}</pre>
给sum2()函数传递数值,也就是传递给了三连点(...),所有数值都会传递给list()函数,并继续下面的运算。
如果想获取三连点输入对象的名称,可以使用do包内的get_names()函数。
<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="R" cid="n15457" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">a=1
return_names <- function(...){
do::get_names(...)
}
return_names(a,b)</pre>
1.5 无参数
函数也可以是无参数的,这样函数每次传递出来的数据都是固定的。
<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="R" cid="n15461" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">sum2 <- function(){
sum = 1+2
return(sum)
}</pre>
1.6 return的用法
return()只在函数内起作用,return()的作用是返回数值****,并且中断函数。注意,当运行了第1个return()后,函数的运行就停止,return()后的所有命令都不会运行。
<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="R" cid="n15465" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">return_test <- function(){
return(1)
return(2)
}</pre>
在returntest()函数中,有2个命令,第1个命令是return(1),第2个命令是return(2),分别用来返回1和2。当我们运行returntest()函数时,返回的结果是1,并不是2,表示运行到return(1)这行命令时返回数值,并且停止运行,没有运行return(2)这行命令。
不使用return(),函数也是可以返回数据的,但是仅仅会返回最后1个结果。
<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="R" cid="n15469" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">return_test <- function(){
1
2
}</pre>
删掉return()重新写函数returntest(),运行returntest()返回的数据是2,并没有返回1。
如果想要函数不输出结果,但是可以赋值结果,可以使用invisible()函数。invisible()函数可以使函数返回的结果不打印出来,但是可以赋值给对象。
<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="R" cid="n15473" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">return_test <- function(){
1
invisible(2)
}</pre>
image-20200607130821232
小结一下R函数返回结果的逻辑:
返回第1个return()的结果,并结束运算;
否则返回最后1个可以打印的结果;invisible()函数可以隐藏返回的结果(不打印出来),但是可以赋值。
2.闭包
现在,我们已经知道了标准函数的结构:函数名、参数、函数体,闭包区别于标准函数的地方在于闭包没有函数名。也就是说闭包是一个无名函数,仅仅包含了参数和函数体。
闭包的应用场景其实还是很多的,也许你并没有注意到,或者没有闭包这个概念,在做函数测试时,会经常用到闭包;闭包也会被用在函数内部,例如在*apply函数家族中,闭包就是常客。
我们创建1个闭包,功能是求2个数字的和
<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="R" cid="n15486" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">function(a, b){
a + b
}</pre>
同样我们可以在R中运行这个闭包。
当我们运行这个闭包后,没有返回任何错误信息或者警告,而是正常地打印了函数。
如果想返回函数值那就需要在闭包外加圆括号,并且在后面使用圆括号将参数括起来。
(闭包)(参数)
我们使用闭包求1和5的和
在*apply家族中使用闭包
不管是lapply还是sapply,后面都接了一个无名函数,这个其实就是闭包。
3.中缀函数
中缀函数也叫中缀操作符,它的出现,大大提高了函数的可读性与可操作性,我们以集合运算的set包为例。如果我们要求数据集A和B的交集,与C和D交集的并集,通过set包中的%and%、%or%和圆括号可以非常容易操作,并且易读。
<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="R" cid="n15499" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">A = c(1,2,3,4,5,6)
B = c(1,2,3,4)
C = c(7,8,9)
D = c(8,9)
library(set)
A %and% B %or% (C %and% D)</pre>
中缀函数的结构类似于夹心饼干,两边2个百分号%,中间是它的函数名,函数名不宜过于复杂,那样的话函数拼写起来将十分困难,在set包中,%and%亦可简写为%a%, %or%可简写为%o%, %not%简写为%n%,这是为了使用方便,但是却损失了可读性。
中缀操作符与标准函数的区别在于,****需要使用反引号( ` )或者单引号( ' )或者双引号( " )将“夹心饼干”引起来。我们创建1个%sum%函数,用于求2个数字的和。
<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="R" cid="n15503" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"># 使用反引号
%sum%
<- function(a, b) a + b
或者使用单引号
'%sum%'<- function(a, b) a + b
或者使用双引号
"%sum%"<- function(a, b) a + b</pre>
4.替换函数
替换函数就是替换原来的数值,和标准函数、闭包、中缀函数不同,替换函数的功能是直接替换原来的参数,不打印结果。
替换函数同样包含了函数名、参数、函数体3个部分,不同的是,****函数名和参数都是固定的。
替换函数的函数名必须是函数名+赋值符号(<-),并且使用单引号( ' )或双引号( " )或反引号( ` )包围,注意函数名和赋值号之间不要有空格,千万不要有空格。替换函数的参数必须是2个,第1个参数名称可以任意,但是第2个参数名称必须是value。
现在,我们要写1个替换函数minus<-,功能是实现a减b,并且直接替换原来的数据a。
普通的做法是
a <- a-b
我们来构建替换函数minus<-
# 使用反引号,第2个参数必须是value
``minus<-
<- function(x, value){
x - value
}
# 或者,使用单引号,第2个参数必须是value
'minus<-'<- function(a, value){
a - value
}
# 或者,使用双引号,第2个参数必须是value
"minus<-"<- function(b, value){
b - value
}
我们将a赋值为4,将a减去1,并且替换原来的a
我们看到名minus(a)=1运行之后并没有任何输出,而是直接替换了原来的a,当我们再次运行a之后,提示a的值从4变成了3 (即4-1的结果)。
有一个小技巧,在写替换函数的同时,再写一个同样名称的标准函数,例如此处的替换函数minus<-,就可以再写一个同名的标准函数minus(),这样的好处是,不用去区分函数的类型,仅需改变函数的使用方式,就可以行使不同的功能,即同名不同形式。
# 标准函数
minus <- function(x, value){
x - value
}
现在,我们有了2个minus函数,1个是标准函数minus(),另外1个是替换函数minus<-,我们想随时切换a减去1,既可以打印结果,也能替换原来的结果,就像函数colnames,既可以返回数据框或者矩阵的列名称,可以用来替换来原来的列名称,其原因就是有2个同名不同形式的函数:标准函数colnames()和替换函数colnames<-。
总结
到这里,我们详细讲解了R语言中常用的4种函数:标准函数、闭包、中缀函数和替换函数,详细讲解了函数的结构、写法、参数、函数返回结果的方法,并且解释了对于标准函数和替换函数同名不同形式的使用。
05 环境
环境是R语言中较底层的概念,理解环境可以帮助你进一步理解R的工作原理,对于使用R及R编程都非常有帮助。
1.内容、对象与环境
在电脑中,所有文件保存的位置是文件夹,比如我写了一些“文本文字”,保存在word文档:“环境.docx”中,word文档保存在文件夹“讲稿”下。注意这里的逻辑关系:“文字内容”保存在word文件“环境.docx”里面,而“word”文本(可以和多个)保存在“讲稿”文件夹下。所以,这里的逻辑关系是:
文字内容--->word文本(一个或多个)--->文件夹。
R中的数据存储关系也是这样的。例如,我们将1赋值给a
这里的1是我们的“内容”,a是承接内容的载体,a可以承接1,也可以承接2,甚至其他数据内容,a就是我们说的对象,数据对象就是用来承载数据的载体,也就相当于word文本。数字1始终是数字1,而a可以承载1也可以承载其它,所以1我们也叫做常量,a叫做变量。数据对象存储的地方,叫做环境,等价于“文件夹”。
所以,数据存储在R中的逻辑是:
内容--->对象(一个或多个,相当于word文本)--->环境(相当于文件夹)
数据内容和对象(或者叫做变量),也就是1和a,都是可以看到的。但是环境在R软件中我们是看不到的。在Rstudio软件中,我们可以看到有一个Environment窗口。平时我们查找数据的时候,也是在这个窗口下找寻的,就是因为R中的数据对象保存在环境中。
2.局部环境与全局环境
2.1 查看当前活动环境
正如我们可以有很多个文件夹,R也可以多个环境,使用environment()命令来查看当前的环境
我们可以看到,当前的活动环境是全局环境(Global Env),既然上面说了对象是保存在环境中,那么我们可以从某一个环境中,提取所有对象。
将enviroment()命令的结果赋值给g,可以看到多了一个数据g,g的类型是Environment,我们可以使用美元符号($)来提取g内的对象。
2.2 创建新环境并内写入数据
我们可以使用new.env()命令来创建一个新的环境,类似于创建一个新的文件夹。同样也是使用美元符号($)向环境内写入数据。我们使用new.env()命令创建新的环境e之后,环境e的编码是0x000001ed51d25cb0,这个环境编码我们并不需要去理解它,它代表硬盘的一个位置。e$v=9表示向e环境中的v对象写入数字9。
也可以使用assign()命令来给环境赋值,它的命令是assign(对象,内容,环境),我们将对象k保存到环境e中,k的内容是123,同时使用View()命令查看环境e。
3.局部变量与全局变量 环境是R管理和存储各种数据的地方,每一个函数在创建之初都会创建自己的环境。 我们创建一个函数e1(),用来显示它内部的环境。
e1的内部环境编码是0x000001ed50c7f200,相同的命令,我们再次运行1次
可以看到即使是完全相同的函数,每次运行时都会创建不同的环境,这是因为电脑的磁盘上并没有它们固定的地方,所以每次运行都在不同的磁盘位置。
注意:我们在前面的步骤中创建了环境e,e内包含了k和v两个数据,它的环境编码是0x000001ed51d25cb0,与此处的编码完全不同。这是因为上面的环境e是在全局环境下创建的,而函数e1()内部的e是在函数内部环境创建的,两个对象虽然都是叫做e,但是所处的环境不同,在磁盘上保存的位置就不同。
现在,我们创建函数e2(),它直接打印e我们看到函数e2内部并没有e,但是却可以打印出e,而且e的内容就是我们前面步骤中创建的环境编码0x000001ed51d25cb0,可见此时的e是函数e2直接引用的全局环境中的e。这是R中环境的另外一个特点:单向:全局环境(父环境)中的数据可以传入它的局部环境(子环境)中,而局部环境中的数据不可以传入全局环境中,如果想将局部环境中的变量传入全局环境中,需要使用全局赋值符号<<-。 我们创建一个函数e3,在函数内部的局部环境中给h赋值78,向全局环境中,添加y,赋值79
当我们是用全局赋值符号<<-将79赋值给y后,在全局环境中我们看到了它。
4.全局变量的意义 全局变量的使用并不是很广泛,主要原因是CRAN为避免造成不必要的混乱,禁止函数修改全局变量。 但是全局变量的使用其实是很广泛的,在抓bug时,使用全局变量可以方便我们查看函数的漏洞;在shinyApp制作时,虽然全局变量不能激活响应表达式,但是同样可以传递数值,给shinyApp的制作带来极大便利。
总结
环境是一个晦涩难懂的概念,但是要知道局部环境与全局环境,并且它们是单向的关系,可以使用全局赋值符号将局部环境中的变了传递至全局环境中。
06 函数注释
首先,请允许我赞美一下Hadley和RStudio团队,他们这份工作变得如此简单。
没有roxygen2包的年代,给R函数写注释是一件非常麻烦的事情,工作量极大,并且极易出错,很多时候你需要保持清醒的头脑,因为一不小心就会跳入一个坑,接着便是bug不断,还有debug、debug、debug ……
1.插入注释框架 双击打开我们的项目文件fpkg.Rproj
首先,我们写1个标准函数sum2(),用于实现两个数字的和,并将它保存在R文件夹下,文件名为sum2.R,R语言代码文件的扩展名必须是R,而文件名不一定要和函数名一样,可以是sum2.R,也可以是sum.R,但不能是中文。
插入注释框架主要有2种方法:菜单插入、快捷键插入,不管哪一种,都要先将光标定位在函数内部,只有在函数内部,RStudio才能识别这是1个函数。
1.1 菜单插入
首先,将光标定位在函数内部,然后依次点击菜单栏的Code、Insert Roxygen Skeleton,就插入了注释框架。
1.2 快捷键插入
首先,将光标定位在函数内部,然后使用快捷键Ctrl+Alt+Shift+R即可插入。
2.书写注释
普通的注释以 井号# 开头,而函数注释以 井号和单引号 #' 开头,并且后接1个空格。每一个区域的指定,都是以@开头。
2.1 标题
第1行如果不指定,默认是标题title,也可以自己使用@title指定。
书写标题时,需要在@title后空1格,标题要能准确反映出函数的功能,要注意首字母的大小写。
2.2 描述
函数的描述部分并不是必须的,所以默认是不会插入这1部分的,如果缺省了描述部分,R会自动标题来代替。
使用@description来指定描述区域。描述部分必须是1整个段落,这意味着必须以句号也就是点来结尾。如果内容多,可以换行,每行最好不要超过80个字符,并且换行后要以4个空格开头。
如果description部分写的不过瘾,还可以在@details部分进一步详细书写。
2.3 参数
使用@param来指定参数部分,@param后空1格,写参数的名称,再空1格,写参数的解释,参数的解释不能为空。
2.4 链接到其它函数
有时候,我们并不想把某一个参数的注释写的过于详细,因为其它地方有非常完备的说明,那么我们就可以使用code{link[pkg]{rdname}}这种形式来引用。
例如,我们将b参数链接到sum函数的说明文档。首先在help窗口中找到sum函数的说明文档
在标题上方,可以看到sum{base},告诉我们sum函数的说明文档,处于base包中的sum.rd文档下(rd就是r document的缩写,r说明文档),所以code{link[pkg]{rdname}}中,pkg等于base,rdname等于sum,也就是写成code{link[base]{sum}}
2.5 返回
使用@return来说明函数的返回内容。
2.6 导出函数
如果你的函数是要分享出来和别人一起使用的,那么就需要将函数从包内导出,在注释部分添加@export即可,后面不需要追加任何内容。
如果你的函数仅仅在包内使用,那么就不需要添加@export,删除即可。
2.7 引用函数
如果你想引用其它包中函数,那么需要使用@importFrom,写法是@importFrom+包名+函数名(一个或多个)例如,我们要引用base包中的sum函数,那么就可以这样写
如果我们还想引用base包中的abs函数,那么可以分开写两个importFrom
也可以写成1行
2.8 示例
每个函数都应该有1个示例,这样更加方便用户来理解它。
使用@examples来指定示例部分,例如,我们加入示例sum2(1, 2),那么就可以在@examples后面写上。
如果示例不能够被运行,或者运行时间过长,在CRAN检测时不能被通过,可以使用donttest{}来使得示例在检测时自动被忽略。
3.转义注释
函数的注释写完了之后,它并不能被R自动识别,需要将其转义才可以。转义的方法我们在第3节《R语言程序包开发(3):封装包》中已经讲解,可以使用快捷键Ctrl+Shift+D,也可以使用命令devtools::document(),还可以使用菜单build、Document,这里我推荐使用devtools::document()。
我们可以看到,写入了一个Rd文件,也就R document文件,文件名是sum2.Rd。我们可以打开它看一下。
点击man文件夹下的sum2.Rd文件,可以看到非常复杂的内容,这就是我们最开始写R包时最为痛苦的地方。
转义完之后,再次封装包即可。
******小结******
现在,我们已经可以创建包、写函数、写注释、转义注释、封装包了,如果你跟着这个教程走到这里,那么恭喜你,你已经可以完成1个R包。
07 引用R包函数
不同函数可以进行复杂的合作,这是R强大的一点。并不是所有函数都需要你从头写起,借用他人的函数,使你的工作变得更加简单。
本次教程我们将学习
1. 引用CRAN包
2. 引用Bioconductor包
3. Imports、Depends和Suggests的区别
4. 引用函数
****1.引用CRAN包(Imports、Depends和Suggests的区别)****
我们使用usethis包中的usepackage()函数来指定引用包,usepackage()函数中总共有3个参数package、type和minversion****,package表示我们要引用包的名称,type表示引用的类型,常用的引用类型有Imports、Depends和Suggests,minversion表示包的最小版本号。
例如,在fpkg包引用do包,那么package就等于"do"。
如果我们只需要使用do包中的join_inner()函数,而不是整个do包,那么引用类型就是Imports,命令如下
在返回的文字中,我们看到Adding "do" to Imports field in DESCRIPTION,意思是将do包添加在DESCRIPTION文件中的Imports部分,打开DESCRIPTION文件,可以看到多了Imports部分,而且下面有do。
如果我们需要使用整个do包,也就是fpkg包功能的实现完全依赖于do包,而不是仅仅使用某个函数,换句话,当我们library(fpkg)的时候,do包会同时被library进来,也就是同时运行了命令library(do),这种关系就是“依赖”关系(library(我)的时候,也自动library(你)),引用类型就是“依赖”,type等于“Depends”,命令如下
返回的文字告诉我们,do被从Imports部分,转移到了Depends部分,并且告诉我们尽量不用使用Depends,Imports是最多且更好的选择。之所以有这样的建议,那是因为当fpkg包对do的关系是Depends时,library(fpkg)时,会同时library(do),这样就增加了函数名称冲突的风险,而当fpkg包对do的关系是Imports,library(fpkg)包的时候,不会运行library(do),也就不会载入do包,从而避免了不必要的函数名冲突,仅仅引用了我们想引用的join_inner()函数。
可以看到,在DESCRIPTION文件中,出现了Depends部分,并且下面有了do。
如果fpkg包对do包没有引用和依赖关系,仅仅是在例子中使用了该包,那么我们仅仅是建议安装这个包,来实现我们的例子,这个时候的关系是“建议”,type等于“Suggests”。
所以,DESCRIPTION文件中的Imports、Depends和Suggests分别表示:引用、依赖和建议。
2.引用Bioconductor包
单独使用Imports、Depends和Suggests引用的都是CRAN上的包,如果想引用Bioconductor上的包,需要在前面加上biocViews:,例如,我们想在fpkg包中引用limma包,我们需要将limma写在Imports下面,在Imports前面,加上biocViews:
3.引用函数
引用包是引用函数的前提,如果想引用某个函数,必须先引用该函数所属的R包。引用函数有2中形式,通过双引号::直接使用,通过roxygen2注释引用。
例如,我们要引用do包中的join_inner函数,通过双引号来引用的方式为
<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="" cid="n15741" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">do::join_inner()</pre>
通过注释来引用的方式为
<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="" cid="n15746" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">#' @importFrom do join_inner</pre>
引用函数的方式比较简单,但是有一点需要注意:只能引用R包导出的函数,也就是只能引用双引号能够访问的函数。
小结
到这里,我们进一步充实了我们的R包,学会了如何引用包及包内的函数,熟悉了引用包的3种方式,并且学会了如何引用Bioconductor上的包。
08 写DESCRIPTION文件
DESCRIPTION文件是R包的说明文件,使用者通过阅读该文件,可以快速了解你的R包,DESCRIPTION主要包含了标题、说明、作者、通讯方式、版本号等几个部分,现在,我来详细说明一下如何书写DESCRIPTION文件。
1.标题
标题一般有字数、内容和大小写的要求
① 字数不宜过长,一般不要超过20个次,过长的标题不宜阅读
② 标题要能够体现包的功能
③ 标题的首字母需要大写,注意冠词、连词、介词的首字母需要小写
标题的格式至关重要,许多人不在意标题的格式,结果被CRAN要求反复修改。
2.描述
描述部分是一段文字,要以句号结束。描述部分要求详细,详细,详细,重要的事情说3遍啊!描述部分一定要详尽描述包的功能。这部分可以分多个段落来写,另起一段的时候,要以4个空格起写。注意不能以This package或者This function开头,切记切记。
******3.作者******
在老版本的R包中,可以直接书写,现在则需要使用person()函数来创建。在person()函数中,主要有几项是需要写的。
given:名
family:姓
middle:中间的名字
email:邮件,并不需要每个人的emial,仅给出包的拥有者即可。
role:角色
******4.角色******
每个包可以有多个作者,但是拥有者只能有1个。role的取值如下
aut:作者author
com:编译者compiler
cph:版权拥有者copyright holder
cre:包拥有者creator或者maintainer
ctb:贡献者contributor
ctr:承包者/公司contractor
dtc:贡献数据这data contrbutor
fnd:资助人/组织funder
rev:评论者reviewer
ths:指导者thesis advisor
trl:翻译者translator
5.版本号
R包必须有版本号,每次更新R 包必须有不同的版本号,非正式发布的R包,版本号可以是零点几的版本,例如0.9,正式的R包建议使用一点几以上的版本,R包的版本可以是1.0, 1.01,1.0.0.1。
6.例子
具体的写法以fastStat包为例给大家作为参照。
******小结******
通过本次教程的学习,大家学会了如何书写DESCRITION文件,这样就基本可以写一个完整的R包了。如果你刚看到这篇文章,可以戳: