第 7 章 R包开发与管理
7.1 开发R包
7.1.1 基本流程
7.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"))参考资源:
- usethis包 issuess 898: creates a “non-standard license specification” that is not “standardizable” by devtools::check()
7.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()7.1.3 包周期管理
7.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: 7.1.4 碰到的坑
7.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参考资源:
- devtools issues 1466:document() failing after upgrade
7.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? 链接
7.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函数(如- dplyr、- purrr等包函数)都会出现这类完全正常的引用方式。
- 当然, - oxygen2编译中会要求明确这些”Undefined global variable”也是出于严谨性的目的,本身无可指责。
解决办法:
- 方法1:使用函数utils::globalVariables()来指明所有这些全局变量。当然,如果有很多这样的变量,那么此方法将难以为继。
参考资源:
7.3 代码调试
参看hadley “advanced R”, chapter 22 Debugging
7.3.1 tryCatch函数的正确使用
Basic Error Handing in R with tryCatch() 参看R-bloger post
7.3.2 R提醒程序执行完成
R提醒程序执行完成 参看
You just have to wrap it into a function like this:
options(error = function() {beep(9)})最终方案:
beep_on_error(expr, sound = 1)7.3.3 设定工作日志
可以使用R包logr(见官网),来实现对工作流的日志记录。
7.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.5490191 -2.0346370## [1] "Random print statement"
## Random cat statement7.4 pkgdown发布
7.4.1 基本过程
参看
- Hadley的在线书R Packages 
- pkgdown官方说明 
# 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包。
7.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.utils7.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, servrusing additional packages for pkgdown articles(参看问答)
7.5 R启动逻辑与环境配置
7.5.1 R session及临时文件路径
默认情况下,每次运行R project都会打开一次R session,并伴随产生一个本地临时文件夹,例如RtmpoLxPcG或RtmpKecsRz。正常情况下,如果关闭一次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()进行查看。
7.6 Renv包报错及解决
7.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包版本管理。
7.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.conf、- rsession.conf、- Renviron.site、- Rprofile.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”。
7.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.x。RTools的安装对于renv::restore()具有重大的影响,因为R包的编译和安装与之密切相关!否则会各种报错和无法安装R包。
7.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))7.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”官方说明。
7.7 相关软件及环境变量
7.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。
- 步骤5:查看java是否配置正确。打开(windows终端) - cmd,输入- java -verion。在Rstudio中输入- library(rJava)查看包安装情况是否成功。
7.8 编程思维
#see resource:
## template: https://www.nomnoml.com/
## github: https://github.com/rstudio/nomnoml
# renv::install("rstudio/nomnoml")
require(nomnoml)## Loading required package: nomnoml## Loading required package: webshot7.8.1 人工编校与自动检查结合的迭代实现机制
情景说明:数据集中存在一些变量,需要进行人工处理。数据集是扩展性的,数据集容量会单调增加。新增加的数据可能也会存在一些变量,需要进行人工处理,进行编校整理。为了避免重复人工投入,早前完成的人工编校将作为历史版本进行保留备存。新增加的数据,会首先与这个保留备存文件进行比对。此时,仅需要对备存文件中未匹配到的数据行进行再次人工编校,然后再次保留备存。如此持续往复进行。
实现方法:把每次的原始文档(auto)和对应的编校文档(edited)存放在一个仓库目录下(hub/)。批量读取仓库目录(hub/)的历史存档,与新数据进行比对匹配,获得新的原始文档(auto)和对应的编校文档(edited),再次迭代保存。原理图如下:
其中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包相关函数加以实现。其中auto和edited存放在同一个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)
}最后就是人工编校新存档,并再次读取全部历史存档: