4  R包开发与管理

4.1 开发R包

4.1.1 基本流程

4.1.1.1 创建包项目

usethis::create_package(path = "../you-path/directory/")

4.1.1.2 完善meta信息

DESCRIPTION文件中,我们需要完善基本的包信息。

大量使用usethis包和的相关函数:

# license
usethis::use_mit_license("hu huaping")

# authors
utils::person(
  given = "Hu",
  family =  "Huaping", 
  email = "huhuaping01@nwsuaf.edu.cn", 
  role = c("aut", "cre"))

参考资源:

  • usethisissuess 898: creates a “non-standard license specification” that is not “standardizable” by devtools::check()

4.1.1.3 创建函数

4.1.1.4 创建数据集

# Proc 1: raw-data directory, and your raw R script and raw data
usethis::use_data_raw("scrape_proc")

# Proc 2: pgk data set construction

## step 1: write out data set, and can find it in "data/your_data.rda"
usethis::use_data(your_data, overwrite = TRUE)

## step 2: write r file, and can find it in "R/your_data.r".
## your should edited with oxygen syntactic
use_r("your_data")

## and your can use my helper function to help documenting
zoningr::document_dt(your_data)

# step 3: formally document, and can find it in "man/your_data.Rd".
document()

4.1.2 编写说明文档

4.1.2.1 readme文档

# use md
usethis::use_readme_md()
# use Rmarkdown
usethis::use_readme_rmd()
## if use Rmarkdown, you should render it regularly to keep md update.
devtools::build_readme()

4.1.3 包周期管理

4.1.3.1 版本格式

(1)包版本格式:至少两个整数,并以.-区隔。例如1.0或者0.9.1-10。tidyverse规范版本格式:<major>.<minor>.<patch>。例如发布版本号1.9.2,开发版本号1.9.2.9000

<major>.<minor>.<patch>        # released version
<major>.<minor>.<patch>.<dev>  # in-development version

(2)查看包版本:utils::package_version()

(3)编译包版本:使用函数usethis::use_version(),根据情况做出版本号决定。它会自动填充DESCRIPTION文件的版本号,同时自动添加行到NEWS.md文件。

usethis::use_version()
#> Current version is 0.1.
#> What should the new version be? (0 to exit) 
#> 
#> 1: major --> 1.0
#> 2: minor --> 0.2
#> 3: patch --> 0.1.1
#> 4:   dev --> 0.1.0.9000
#> 
#> Selection: 

4.1.4 碰到的坑

4.1.4.1 无法正常写入namespace

报错信息:document()出现如下报错。

Warning: The existing 'NAMESPACE' file was not generated by roxygen2, and will not be overwritten

诊断检查:发现NAMESPACE文件里面么有任何信息。而且document()后也确实没有写入任何信息。

解决方法:直接在NAMESPACE文件第一行添加如下信息,然后再document(),一切恢复正常!(这种方法简直令人惊诧。因为前面都是按照官方引导来做的,却还是出现如此诡异一幕。)

# Generated by roxygen2: do not edit by hand

参考资源:

4.1.4.2 不能识别非ASCII字符

报错信息:check()后出现如下报错。

Error: Portable packages must use only ASCII characters in their R code

诊断检查:

  • R函数文件的代码行中出现有中文字符

解决办法:

  • 全局添加编码说明:在R安装路径R/etc/Rprofile.site文件中,加入代码行options(encoding = "UTF-8")

  • 项目内添加编码说明:在project根目录.Rprofile文件中,加入代码行options(encoding = "UTF-8")

参考资源:

  • 队长问答Is it possible to write package documentation using non-ASCII characters with roxygen2? 链接

4.1.4.3 不可见的全局变量

报错信息:check()后出现如下报错。

checking R code for possible problems ... NOTE
  get.tbl: no visible binding for global variable '.'
  Undefined global functions or variables: .

诊断检查:

  • 这里是因为R函数文件的代码行中用到了mutate_at(., ~as.character),因此oxygen2编译中会要求明确这个”.”是什么。当然,我们的很多R函数(如dplyrpurrr等包函数)都会出现这类完全正常的引用方式。

  • 当然,oxygen2编译中会要求明确这些”Undefined global variable”也是出于严谨性的目的,本身无可指责。

解决办法:

  • 方法1:使用函数utils::globalVariables()来指明所有这些全局变量。当然,如果有很多这样的变量,那么此方法将难以为继。

参考资源:

  • Rbloger: No visible binding for global variable

  • 队长问答: no visible binding for global variable ‘.’

4.2 数据型R包的开发

4.2.1 概述

数据型R包(data package)与函数型包(functional package)的开发流程本质上并无差异,只是目的不同。对于数据型R包,主要用于数据的公开分享。总体来看R包开发中要考虑数据的使用范围和权限。

(1)如果要作为R包发布的公开数据集,则数据文件应存放于data/文件夹。

(2)如果只是开发者自己使用的数据集,则数据文件应存放于R/sysdata.rda

(3)如果想给R包的用户分享其他原始的、非R型的数据集(non-R-specific form),则应存放于inst/extdata/文件夹下。

4.3 代码调试

参看hadley “advanced R”, chapter 22 Debugging

4.3.1 tryCatch函数的正确使用

Basic Error Handing in R with tryCatch() 参看R-bloger post

4.3.2 R提醒程序执行完成

R提醒程序执行完成 参看

```{r}
library(beepr)
beep(sound=8)
```

You just have to wrap it into a function like this:

options(error = function() {beep(9)})

最终方案:

beep_on_error(expr, sound = 1)

4.3.3 设定工作日志

可以使用R包logr(见官网),来实现对工作流的日志记录。

```{r}
#| eval: false
# open log for process
library(logr)
tmp <- "data/workfolow.log"
# initiates the log at the top of the program
lf <- log_open(tmp)
# prints an object to the log
log_print("workflow smry info to print")
# close log at the end of the program.
log_close()
```

4.3.4 强制结果不输出console中

有些函数会出现某些输出,如组标准误矫正回归的摘要函数summary(out.mice <- miceadds::lm.cluster())等。

如若希望强制这些结果不输出到console中,则可以结合invisible(capture.output()))进行设定(可参看队长问答)。

f1 <- function(n, ...){
    print("Random print statement")
    cat("Random cat statement\n")
    rnorm(n = n, ...)
}
f1(2)
[1] "Random print statement"
Random cat statement
[1]  0.8014174 -0.6998394
x <- f1(2)
[1] "Random print statement"
Random cat statement
invisible(capture.output(x <- f1(2)))

4.4 pkgdown发布

4.4.1 基本过程

参看

# Run once to configure package to use pkgdown
usethis::use_pkgdown()
# write Non-vignette articles
usethis::use_article()
# Run to build the website
pkgdown::build_site()
#setting up GitHub actions to automatically build and publish your site
usethis::use_pkgdown_github_pages()

说明usethis::use_article()的好处在于:其一可以编写更多的包文档,而不增加包的体积;其二可以使用更多的R包来编写更美观的说明文档,同时可以确保正在开发的R包yourpkg不存在过多的、没必要的包依赖(避免”DESCRIPTION”设置文件里Suggests或者Imports里对依赖包的列出)。(1)对于前者,usethis::use_article()会自动添加article文档到.gitignore列表及.buildignore列表里去。(2)对于后者,我们需要做出额外的设置:“DESCRIPTION”设置文件里增加参数域Config/Needs/website,并列出article里(.Rmd)需要用到的额外R包。

4.4.2 重要设置

#_pkgdown.yml
lang: zh_CN
development:
  mode: auto

4.4.3 github两种发布方式

(1)上传本地渲染的docs/文件夹到gh-pages分支。

(2)github action自动化(commit、push)推送gh-pages分枝

4.4.4 github action报错未正确设定remote包

github action在执行r-lib/actions/setup-r-dependencies@v2块时会给出如下报错:

Run r-lib/actions/setup-r-dependencies@v2

  ℹ Creating lockfile '.github/pkg.lock'
  ✖ Creating lockfile '.github/pkg.lock' [9.2s]
  
  Error: 
  ! error in pak subprocess
  Caused by error: 
  ! Could not solve package dependencies:
  * deps::.: Can't install dependency kwb.utils
  * kwb.utils: Can't find package called kwb.utils.
  * local::.: Can't install dependency kwb.utils

提示无法安装R包kwb.utils。实际上这是一个在github上发布的包KWB-R/kwb.utils。在自己的包开发描述文件中,并没有正确地指明这个包的来源(如下)。(但这并不影响包开发,因为在包检查流程中通过”Build” > “Check”检查,并不会报错。)

Imports: 
    dplyr,
    fs,
    magrittr,
    kwb.utils,
    rlang,
    stringr,
    tibble,
    glue,
    utils,
    tidyselect,
    officer,
    rvg
Depends: 
    R (>= 2.10)

因此,正确的操作是需要通过Remotes: KWB-R/kwb.utils来正确指定其安装来源:

Imports: 
    dplyr,
    fs,
    magrittr,
    kwb.utils,
    rlang,
    stringr,
    tibble,
    glue,
    utils,
    tidyselect,
    officer,
    rvg
Depends: 
    R (>= 2.10)
Remotes: KWB-R/kwb.utils

4.4.5 github action安装依赖包时间过长或失败

使用pkgdown包写说明文档(articles)时,可能会用到额外其他的R包。此时使用github action自动云端渲染更新说明文档,则意味着云端后台需要安装和准备这些额外的依赖R包。这直接会带来两个问题:一是过多的R依赖包意味着github action执行时间过长;二是某些特殊的R依赖包可能安装不成功,从而中止gihub action后续进程。

理论上,pkgdown后台的github action可以通过设置并执行renv环境来避免R依赖包的上述问题。但是目前没有看到现成的设置代码(.github/workflows/pkgdown.yaml)。

目前的一个可行办法是设定包开发文件DESCRIPTION,设定如下参数(可参看pkgdown本身包开发的设定):

Config/testthat/edition: 3
Config/potools/style: explicit
Config/Needs/website: usethis, servr

using additional packages for pkgdown articles(参看问答

4.5 R启动逻辑与环境配置

4.5.1 R session及临时文件路径

默认情况下,每次运行R project都会打开一次R session,并伴随产生一个本地临时文件夹,例如RtmpoLxPcGRtmpKecsRz。正常情况下,如果关闭一次R session,这个临时文件夹就会自动删除。

有时候,因为R project的工程任务比较大,会伴随产生较多的临时文件,从而导致较大的本地硬盘空间占用。更加不幸的是,这些R session临时文件夹的默认路径往往是C:\Users\huhua\AppData\Local\Temp\RtmpoLxPcG(windows用户)。这就意味着临时文件会占用系统盘(C盘)的硬盘空间。在硬盘空间严重不足的情况下,甚至会导致内存不足而无法跑project的情况。

再加上说不清的原因,有时候即使关闭了R session,其相应的临时文件夹却没有自动删除。残留的零时文件夹可能会不断积累而变得异常大(以我本人的情况,一次检查中竟然发现有接近30Gb的硬盘占用!!)。

鉴于此,可以在用户层面来设定R session临时文件夹的默认路径。例如设定在非系统盘如D:\appData\Rsession。相关操作步骤如下:

  • 步骤1:在非系统盘创建文件夹D:\appData\Rsession

  • 步骤2:创建并打开用户层面的.Renviron配置文件。可以采用R函数usethis::edit_r_environ(scope = "user").Renviron配置文件位于C:/Users/huhua/Documents/.Renviron

  • 步骤3:在.Renviron配置文件中设定R session临时文件夹的默认路径,直接添加参数行TMP = "d:\appData\Rsession",保存,然后重新打开并运行R project。

  • 步骤4:查看路径设定结果。可以在打开的R session下运行R代码tempdir()进行查看。

4.6 Renv包报错及解决

4.6.1 为什么要用Renv?

考虑如下这些常见的R使用场景:

  • R软件版本由R 3.6.0升级到R 4.1.0

  • 某个R包发生了版本变化,例如rmarkdown v1.6升级到rmarkdown v2.11

  • A从github上拉取了B的R项目,在B的工作环境下一切运行良好,但是A则出现各种报错。

上述场景是R用户经常会碰到的工作情形,而此时Renv是用来进行R包管理的有效工具。

一方面,对于不同的R project,Renv都会给出基于项目的独立的R包路径和R包存放。这样不同的R projects就可以各行其道,相互隔绝,互不冲突。

另一方面,Renv可以把各类安装包及其历史版本(或不同来源)集中缓存在本地。如果某个R project需要安装“集中库”里已有的、正确版本的R包,R project只需要直接调用本地的缓存即可,从而避免再次远程在线下载,节约了安装时间。

最后,每当某个R project的R版本变化或R session改变时,可以直接调用缓存在R project下的历史版本包,从而实现有效的R包版本管理。

4.6.2 Renv环境设置

一般情况下,renv已经很好地管理了本地的R包“集中库”,以及各个R project的R包“项目库”。用户不需要过多的操心它们的关系。

对于R project,用户一般不会创建在系统盘内(如放在D盘等),因此R project的R包“项目库”占用硬盘情况大可不用去担心。

对于电脑硬盘容量不足的少数用户,可能会面临因为renv缓存本地的R包“集中库”占用磁盘过大,而出现系统盘(win 11 C盘)被过度占用的问题。

以我本人的电脑使用来看,缓存本地的R包“集中库”占用C盘将近6.5GB。对于win 11用户而言,本来就吃紧的C盘,直接就爆表不够用了。

这时就必须要对renv的默认路劲参数进行修改,把R包“集中库”由默认的C盘路径修改到其他非系统盘符下。

需要提前理解如下的一些基本知识:

  • renv官方的路径参数定制说明,具体见Path Customization。其中,最重要的是RENV_PATHS_ROOT路径参数的设定。

  • 关于R启动(R startup)逻辑的相关知识。其中最重要的是区分不同层次、不同权限和不同语法的启动参数控制关系,重点包括如下一些文件的设置(参看):repos.confrsession.confRenviron.siteRprofile.site.Renviron.Rprofile。具体可以参看在线资源:a)“Resources for learning R” 的第11章 “Using .Rprofile and .Renviron”。b)“What They Forgot to Teach You About R”的“Chapter 7 R Startup”

为了解决R包“集中库”由默认的C盘路径修改到其他非系统盘符下,重要的操作步骤如下:

  • 步骤1:在非系统盘下创建文件夹:例如d://appData//renv

  • 步骤2:创建并修改R软件安装路径下的Rprofile.site,进行全局性设定。具体路径为:C:\Program Files\R\R-4.1.2\etc\Rprofile.site。可以直接使用notepad等文本编辑软件进行修改设定。

注意:有人建议使用usethis::edit_r_profile()来创建和设定启动参数,但是经测试改函数仅仅基于用户或项目层面,例如:运行usethis::edit_r_profile(scope = "user")只会创建或修改’C:/Users/huhua/Documents/.Rprofile’的启动参数;运行usethis::edit_r_profile(scope = "project")只会创建或修改项目层面’D:/github/books-rworld/.Rprofile’的启动参数。这两者都达不到全局性(基于R软件版本)的启动设定要求。

  • 步骤3:在C:\Program Files\R\R-4.1.2\etc\Rprofile.site下,添加R函数命令行Sys.setenv(RENV_PATHS_ROOT = "D:\\appData/renv")

注意:上述的Rprofile.site文件是与R软件安装版本相伴随的,因此如果R安装版本发生了改变,则以上的设定在新R版本下会失效,又得重新按上述步骤设置一遍。一个好的方案是,将自己的这些环境参数设定,以R project的形式进行git维护和管理,实现可重复使用。一些公开的环境参数维护项目可以参看“Example R profiles”

4.6.3 Renv与R

首先,Renv管理R版本包库(library)是基于一位小数的R版本来设定本地R包的存放地址。例如”/renv/library/R-4.1”下会存放所有4.1.x的R版本。因此,在进行一个大版的R升级时(例如从4.1.2升级到4.3.1),工作项目的包库也会完全更新升级到新的目录下(“/renv/library/R-4.3”)。

此外,如果进行了大版本的R升级时,windows系统下还要同步升级对应的Rtools版本(下载地址)。例如RTools 4.3对应于所有的R 4.3.xRTools的安装对于renv::restore()具有重大的影响,因为R包的编译和安装与之密切相关!否则会各种报错和无法安装R包。

4.6.4 重载时排除某些R包

重载时(renv::restore()),可能会报错(因为R包的来源多种多样),此时可以暂时先排除某些R包的重载过程。

renv::restore(exclude = c("xmerit", "igraph"))

4.6.5 指定安装R包版本

有时候我们需要安装指定R包的旧版本:

renv::install("igraph@1.2.5")

4.6.6 更新R包记录

如果在另一个设备下,旧有的R包无法重载,也可以考虑更新这些包的renv记录(更新包版本,或更改安装来源),具体操作如下:

# use digest 0.6.22 from package repositories -- different ways
# of specifying the remote. use whichever is most natural
renv::record("digest@0.6.22")
renv::record(list(digest = "0.6.22"))
renv::record(list(digest = "digest@0.6.22"))

# alternatively, provide a full record as a list
digest_record <- list(
  Package = "digest",
  Version = "0.6.22",
  Source  = "Repository",
  Repository = "CRAN"
)

renv::record(list(digest = digest_record))

4.6.7 R包依赖报错

操作情形:存档R包状态。renv::snapshot()

报错信息如下(可参看):

> x <- enc2utf8('á')
> parse(text = x, encoding = "UTF-8")
Error in parse(text = x, encoding = "UTF-8") : 
  <text>:1:1: unexpected input
1: á
    ^

Rstudio console窗口提示:

Error in !deps$Dev

解决办法:

设定renv参数为:renv::settings$snapshot.type("all"),具体参考renv包关于”Capturing all dependencies”官方说明

4.7 相关软件及环境变量

4.7.1 Java配置

很多R包都会调用系统软件Java,以实现相关函数功能。例如R包xlsx就需要以来rJava包,以调用系统的Java功能。

因此,本地需要安装Java的版本软件(又分为jre版,及jdk版,后者居于主流);同时需要配置OS系统的环境变量

经验法则:开始安装和配置前务必要保证Java版本与R GUI版本是同样的系统版本,例如都是64位的软件版本,或都是32位的软件版本。对于R GUI软件的windows版本,要注意其32bit和64bit都是绑在一块下载的。因此,如果电脑操作系统(win 11)是64bit的,那么在安装R GUI时务必要在安装引导界面下,去掉其32位的安装选项!!否则你自认为它会根据OS系统的情况自动选择安装64bit版本,而实际上安装的很可能是32bit的!!具体可以通过R函数sessionInfo()进行查看!!

下面是具体的操作步骤:

  • 步骤1:确定OS操作系统的位数,例如win 11 64bit

  • 步骤2:下载安装R GUI(下载地址见cran)。例如,只安装其windows版本下的64bit版本R,安装时去掉32bit的安装选项!具体缘由见上面!!

  • 步骤3:下载并安装java软件。例如下载其windowns版本下64bit版本的jdk(下载地址见cran)。具体版本如jdk-17.0.2(版本会迭代变化),然后进行本地安装后,其默认路径在C:\Program Files\Java\jdk-17.0.2

  • 步骤4:在os系统上进行环境变量设置,配置java启动和调用路径。具体配置步骤请参看:资料1资料2 菜鸟教程

  • 步骤5:查看java是否配置正确。打开(windows终端)cmd,输入java -verion。在Rstudio中输入library(rJava)查看包安装情况是否成功。

4.8 编程思维

```{r}
#see resource:
## template: https://www.nomnoml.com/
## github: https://github.com/rstudio/nomnoml

# renv::install("rstudio/nomnoml")
require(nomnoml)
require("webshot")
#webshot::install_phantomjs()
```

4.8.1 人工编校与自动检查结合的迭代实现机制

情景说明:数据集中存在一些变量,需要进行人工处理。数据集是扩展性的,数据集容量会单调增加。新增加的数据可能也会存在一些变量,需要进行人工处理,进行编校整理。为了避免重复人工投入,早前完成的人工编校将作为历史版本进行保留备存。新增加的数据,会首先与这个保留备存文件进行比对。此时,仅需要对备存文件中未匹配到的数据行进行再次人工编校,然后再次保留备存。如此持续往复进行。

实现方法:把每次的原始文档(auto)和对应的编校文档(edited)存放在一个仓库目录下(hub/)。批量读取仓库目录(hub/)的历史存档,与新数据进行比对匹配,获得新的原始文档(auto)和对应的编校文档(edited),再次迭代保存。原理图如下:

#{nomnoml, svg=TRUE, echo=FALSE,fig.height=9}
#stroke: black
#direction: down
#.box: fill=#8f8 dashed visual=ellipse

[zone_clean0] - filter() [<choice>is.na(dname)|nchar_nname<4|is.na(zzname)]

[<choice> is.na(dname)|nchar_nname<4|is.na(zzname)] -> [zone_clean40]

[<transceiver>get.hubs(..)] -> dir_tar [dt_hubs]

[dt_hubs]  -y  [<transceiver>anti_join()]
[zone_clean40]  -x [<transceiver>anti_join()]

[anti_join()] -> [dt_newcome]
    
[zone_clean40]  -x [<transceiver>left_join(x,y)]
[dt_hubs]  -y  [<transceiver>left_join(x,y)]

[left_join(x,y)] -> [zone_clean4]

[dt_newcome]-[<choice> nrow(.) ==0]
  [<choice> nrow(.) ==0] -> [<transceiver> get.hubs(dir)]

[dt_newcome]-[<choice> nrow(.) >0]
  [<choice> nrow(.) >0] - [createWorkbook('hub')]
  
  [createWorkbook('hub')|
     [addWorksheet(wb, 'auto')] --[writeData()]
     [addWorksheet(wb, 'edited')] -- [writeData()]
       [writeData()] -- [saveWorkbook()]
       [saveWorkbook()] --[xlsx]
       [edited] mannual-->  [xlsx]
  ]

[createWorkbook('hub')] ->  [<transceiver> get.hubs(dir)]

[dt_hubs] <- dir_tar [get.hubs(dir)]

其中get.hubs(dir)为批量读取函数,具体函数为:

# match hub and edited by hand!!
## Helper function to read all hubs
get.hubs <- function(dir){
  out <- tibble(
    path = list.files(dir,full.names = T)
  ) %>%
    mutate(
      dt = map(.x = path, 
               .f = openxlsx::read.xlsx, sheet="edited")
    ) %>%
    unnest(dt) %>%
    select(-path)
}

dir_tar <- "data-sql/hub/"
dt_hubs <- get.hubs(dir = dir_tar)

匹配比对可以使用anti_join()函数:

## match hubs,return all rows from x without a match in y
dt_newcome <- anti_join(
  x = zone_clean40, 
  y = select(dt_hubs,unit), 
  by="unit" )

自动化写入存档可以通过openxlsx包相关函数加以实现。其中autoedited存放在同一个xlsx文档中,而后者需要进行人工编校:

## write newcomers
if (nrow(dt_newcome ) >0) {
  ## specify path
  n_z4 <- nrow(dt_newcome)
  today <- today()
  out_path <- paste0(dir_tar, 
                     today,
                     "_n", n_z4, 
                     ".xlsx")
  ## create workbook, write data, save workbook
  wb <- openxlsx::createWorkbook("hub")
  addWorksheet(wb, "auto")
  addWorksheet(wb, "edited")
  writeData(wb, sheet = "auto", x = dt_newcome)
  writeData(wb, sheet = "edited", x = dt_newcome)
  saveWorkbook(wb, file = out_path, overwrite = TRUE)
}

最后就是人工编校新存档,并再次读取全部历史存档:

## you should edited the newcomers by hand
## now -----!!!!!

## read the full latest hubs again !
dt_hubs <- get.hubs(dir = dir_tar)