如何组织大型R程序?

169

当我处理复杂的R项目时,我的脚本很快就会变得冗长和混乱。

有哪些实践方法可以采用,以便我的代码始终易于使用?我考虑到以下几点:

  • 在源文件中放置函数的位置
  • 何时将某些内容拆分到另一个源文件中
  • 主文件应包含什么内容
  • 使用函数作为组织单元(鉴于R使全局状态访问困难,是否值得这样做)
  • 缩进/换行的做法。
    • 像 ( 这样的符号应该如何处理?
    • 将 )} 这样的内容放在1或2行上?

基本上,你对组织大型R脚本有什么经验之谈?


您可能还想查看ProjectTemplate包。 - ctbrown
10个回答

76
标准答案是使用软件包 - 请参阅编写R扩展手册以及网上的不同教程。
它为您提供:
  • 一种按主题自动组织代码的方法
  • 强烈建议您编写帮助文件,让您考虑接口
  • R CMD check进行很多健全性检查
  • 添加回归测试的机会
  • 同时还有命名空间的手段。
只对代码运行source()适用于非常短的代码片段。其他所有内容都应该在软件包中 - 即使您不打算将其发布为内部存储库的内部软件包。
至于“如何编辑”部分,R Internals手册在第6节中具有出色的R编码标准。否则,我倾向于在Emacs' ESS mode中使用默认设置。 更新2008年8月13日:David Smith刚刚在Google R Style Guide上发表了博客。

9
如果您正在"有机地"增加您的源代码/分析,那么您不觉得这样做很难/繁琐吗?如果您注意到代码中的错误(在探索新的问题领域时很常见),您必须(i)修复源代码;(ii)重新安装包;(iii)将其重新加载到您的工作区?是否有一种方法可以调用library(...)来重新加载已经加载的包(上述第三步)?难道您不必关闭您的工作空间,重新启动R然后重新加载您的库/包以查看是否正确吗? - Steve Lianoglou
1
尝试在 Google 上搜索 R 代码风格。 - hadley
3
我知道这已经有点老了,但是Hadley的devtools包使得重新加载你的所有代码变得非常容易。 - Dason
1
这篇博客提供了一个非常好的快速教程(我的意见),用于构建裸骨第一个程序包:http://hilaryparker.com/2014/04/29/writing-an-r-package-from-scratch/ - statsNoob
1
这是一个有效的链接,指向Google R风格指南 - Denis Rasulev
显示剩余6条评论

53

我喜欢将不同的功能放在它们自己的文件中。

但我不喜欢 R 的软件包系统。 它相当难使用。

我更喜欢轻量级的替代方案,将文件的函数放置在一个环境中(其他语言称为“命名空间”)并附加它。例如,我制作了一个名为“util”的函数组:

util = new.env()

util$bgrep = function [...]

util$timeit = function [...]

while("util" %in% search())
  detach("util")
attach(util)

所有的代码都在util.R文件中。当你调用它时,会得到一个名为“util”的环境,因此你可以调用util$bgrep()等函数;但更重要的是,attach()调用使得直接调用bgrep()等函数也能正常工作。如果没有将所有这些函数放在自己的环境中,它们将会污染解释器的顶层命名空间(即ls()显示的那个命名空间)。

我试图模拟Python的系统,即每个文件都是一个模块。这样会更好,但这种方法似乎也可以。


谢谢,Brendan。这非常有用。while循环怎么了?if (!("util" %in% search())) attach(util)有什么问题? - Dan Goldstein
2
所以,如果你想调整它或类似的东西,你可以一遍又一遍地执行 source("util.R")。 - Brendan OConnor
你其实不需要 while 循环 -- 你只需要使用 detach(util)。我记不清它是否会在未加载时报错,但这是最安全且可行的方法。欢迎提出建议。 - Brendan OConnor
1
创建函数特定的环境并将其附加是我的首选方法。另一种以更模块化的方式实现相同目的的方法是使用 sys.sourceMyEnv <- attach(NULL, name=s_env); sys.source(file, MyEnv)。我甚至在启动时声明(在它自己的环境中!)一个名为 sys.source2 的函数,它将查找是否已经存在同名的环境,并将其替换而不是创建新的。这使得添加个人函数变得快速、简单和有点有组织 :-) - Antoine Lizée
5
刚刚看到这个,它适用于一个软件包替代方案:https://github.com/klmr/modules - ctbrown
但是如果我有两个具有相同函数或变量名称的源文件怎么办?将它们附加在一起会导致名称冲突。使用“attach”是不好的想法。 - Eldar Agalarov

34

这可能听起来有些显而易见,特别是对于程序员来说,但以下是我如何考虑代码的逻辑和物理单位。

我不知道这是否适用于你,但当我在R中工作时,很少会一开始就想着写一个大而复杂的程序。我通常从一个脚本开始,并将代码分成逻辑可分离的单元,通常使用函数。数据操作和可视化代码都放置在自己的函数中等等。这样的函数被分组放在文件的一个部分(例如,在顶部进行数据操作,然后是可视化等)。最终,您要考虑如何使脚本更易于维护并降低缺陷率。

您将函数粒度设置得多细/粗将会有所不同,并且有各种经验法则:例如15行代码,或“函数应负责执行一个任务,该任务由其名称确定”等。结果因人而异。由于R不支持按引用调用,我通常在涉及传递数据框或类似结构的情况下不会将函数设置得太细粒度。但这可能是我刚开始使用R时犯了一些愚蠢的性能错误后的过度补偿。

何时将逻辑单元提取到它们自己的物理单元中(例如源文件和更大的分组,例如软件包)?我有两种情况。首先,如果文件太大,并且在逻辑上不相关的单元之间来回滚动会让人烦恼。其次,如果我有其他程序可以重用的函数。我通常会将一些分组单元(例如数据操作功能)放入单独的文件中。然后我可以从任何其他脚本中调用此文件。

如果您要部署函数,则需要开始考虑软件包。出于各种原因(简要而言:组织文化更喜欢其他语言,对性能的担忧,GPL等),我不会在生产中或供他人重用时部署R代码。另外,我倾向于不断改进和添加源文件集合,并且在进行更改时不想处理软件包。因此,您应该查看其他与软件包相关的答案,例如Dirk的答案,以获取更多详细信息。

最后,我认为你的问题并不一定只适用于 R。我强烈推荐阅读 Steve McConnell 的 Code Complete,其中包含了许多关于这类问题和编程实践的智慧。


16

我的简洁回答:

  1. 仔细编写函数,确定足够通用的输出和输入;
  2. 限制全局变量的使用;
  3. 使用S3对象和在适当时使用S4对象;
  4. 将函数放在包中,尤其是当您的函数需要调用C / Fortran时。

我认为R在生产中的使用越来越多,因此对可重用代码的需求比以前更大。我发现解释器比以前更加稳定。毫无疑问,R比C慢100-300倍,但通常瓶颈集中在几行代码上,这些代码可以委托给C / C ++处理。如果将R在数据操作和统计分析方面的优势委派给另一种语言,那么这将是一个错误。在这些情况下,性能惩罚低,而且无论如何都值得节省开发工作量。如果只考虑执行时间,我们都应该写汇编语言。


11

我一直想弄清楚如何编写软件包,但一直没有投入时间。对于我的每个小项目,我都将所有低级别的函数放在一个名为'functions/'的文件夹中,并将它们导入到一个我明确创建的单独命名空间中。

如果环境不存在,以下代码将创建一个名为"myfuncs"的环境(使用attach),并使用sys.source将'functions/'目录中的.R文件中包含的函数填充其中。我通常将这些行放在我的主要脚本的顶部,该脚本用于“用户界面”,从中调用高级函数(调用低级函数)。

if( length(grep("^myfuncs$",search()))==0 )
  attach("myfuncs",pos=2)
for( f in list.files("functions","\\.r$",full=TRUE) )
  sys.source(f,pos.to.env(grep("^myfuncs$",search())))

当您进行更改时,您始终可以使用相同的代码行重新获取资源,或者使用类似以下内容的东西

evalq(f <- function(x) x * 2, pos.to.env(grep("^myfuncs$",search())))

为了评估你创建的环境中的添加/修改,这种方法可能有些笨拙,但避免了过于正式(但如果机会允许的话,我鼓励使用软件包系统-希望未来会迁移到那个方向)。

至于编码约定,下面链接是我见过的唯一与美学相关的内容(我喜欢它们,松散地遵循,但在R中我不使用太多花括号):

http://www1.maths.lth.se/help/R/RCC/

关于使用[,drop=FALSE]和<-作为赋值操作符的其他“约定”通常在useR!会议的各种演示文稿中提出,但我认为这些都不是严格规定(虽然[,] drop = FALSE对于您不确定预期输入的程序非常有用)。


6

我支持使用软件包。 我承认在没有必要时(即发布时)编写手册和示例文档的能力很差,但这是一种非常方便的方式来捆绑源代码。 此外,如果您认真维护自己的代码,Dirk提出的所有观点都会发挥作用。


4

我也同意。使用package.skeleton()函数开始编写代码。即使你认为你的代码可能永远不会再次运行,它可能会激励你创建更通用的代码,以后可以节省时间。

至于访问全局环境,使用<<-操作符很容易,但是这样做是不被鼓励的。


3

我还没有学会如何写包,所以我一直通过引用子脚本来组织代码。这与编写类相似但不那么复杂。虽然它在程序上不够优雅,但我发现随着时间的推移,我可以逐步构建分析过程。一旦我有一个有效的大段代码,我通常会将其移动到另一个脚本中并进行引用,因为它将使用工作区对象。也许我需要从几个来源导入数据,对它们进行排序并找到交集。我可能会将该部分放入另一个脚本中。然而,如果您想要为其他人分发您的“应用程序”,或者它使用了某些交互式输入,则包可能是一个很好的选择。作为研究人员,我很少需要分发我的分析代码,但我经常需要增强或调整它。


我曾经使用过这种方法,但后来意识到函数和包比source("next_script.R")更好。我在这里写了一篇文章:https://dev59.com/MF8e5IYBdhLWcg3w4dmk#57455236 - Arthur Yip

1
我一直在寻找正确的流程来组织一个大型R项目,去年我发现了一个叫做rsuite的软件包,它正是我在找的。这个R软件包专门用于部署大型R项目,但我发现它也可以用于小型、中型和大型R项目。以下是一些真实世界的例子链接(下面),但首先,我想解释一下使用rsuite构建R项目的新范例。
注意。我不是rsuite的创建者或开发者。
我们一直用RStudio做项目,但实际上我们所需要的不是一个项目或包,而是更大的范围。在rsuite中,你可以创建一个超级项目或主项目,它包含所有可能的标准R项目和R包的组合。
通过使用R超级项目,你不再需要Unix make 来管理底层的R项目; 你可以在顶层使用R脚本。让我给你展示一下。当你创建一个rsuite主项目时,你会得到这样的文件夹结构:

enter image description here


  1. 文件夹R是您放置项目管理脚本的地方,这些脚本将替换make

  2. 文件夹packagesrsuite保存组成超级项目的所有软件包的文件夹。您还可以复制粘贴无法从Internet访问的软件包,rsuite也会构建它们。

  3. 文件夹deploymentrsuite将写入在软件包的DESCRIPTION文件中指定的所有软件包二进制文件的位置。因此,这使得您的项目完全可重现。

  4. rsuite适用于所有操作系统的客户端。我已经测试过了。但是您也可以将其安装为RStudio的addin

  5. rsuite还允许您在其自己的conda文件夹中构建隔离的conda安装程序。这不是一个环境,而是来自您计算机上Anaconda衍生的物理Python安装程序。这与R的SystemRequirements一起使用,您可以从任何conda渠道安装所需的所有Python软件包。

  6. 您还可以创建本地存储库,在离线时或想更快地构建整个项目时拉取R软件包。

  7. 如果需要,您还可以将R项目构建为zip文件并与同事共享。只要您的同事安装了相同版本的R,它就可以运行。

  8. 另一种选择是在Ubuntu、Debian或CentOS中构建整个项目的容器。因此,与其共享已构建的zip文件,您可以共享整个Docker容器,其中包含准备好运行的项目。

我一直在尝试使用rsuite进行完全可重复性的实验,并避免依赖全局环境中安装的软件包。这是错误的,因为一旦您安装软件包更新,项目往往就会停止工作,特别是那些具有特定参数函数调用的软件包。
我开始尝试的第一件事是使用bookdown电子书进行实验。我从来没有幸运地拥有一本可以经受时间考验超过六个月的bookdown。所以,我将原始的bookdown项目转换为遵循rsuite框架的项目。现在,我不必担心更新我的全局R环境,因为该项目在deployment文件夹中有自己的一套软件包。
接下来我做的是创建机器学习项目,但是以rsuite的方式。一个掌控项目的主项目,所有子项目和软件包都由主项目控制。这确实改变了您使用R编程的方式,使您更加高效。
之后,我开始着手开发一个名为rTorch的新软件包。这在很大程度上得益于rsuite;它让您思考并且做得更好。

不过,我有一个建议。学习 rsuite 并不容易。因为它提供了一种创建 R 项目的新方式,所以感觉很困难。不要在第一次尝试时就灰心丧气,继续攀登这座山坡,直到成功。这需要你对操作系统和文件系统有先进的知识。

我希望有一天,RStudio 可以让我们像 rsuite 一样从菜单中生成编排项目,那将是非常棒的。

链接:

RSuite GitHUb仓库

r4ds bookdown

keras and shiny 教程

moderndive-book-rsuite

interpretable_ml-rsuite

使用R进行机器学习入门-rsuite

克拉克的机器学习简介-rsuite

海德曼的Bookdown统计手册-rsuite

统计思考-rsuite

fread基准测试-rsuite

数据可视化-rsuite

H2O教程:零售分割-rsuite

电信客户流失教程

白菜菌核病RSuite


-4

R语言适合交互式使用和小型脚本,但我不会在大型程序中使用它。对于大部分编程任务,我会使用主流语言,并在R接口中进行封装。


1
有些包 (即程序) 真的很庞大。你是认真建议应该使用其他语言重写它们吗?为什么? - Eduardo Leoni
4
一个考虑因素是效率。我经常将R代码改写为C++代码,并使其运行速度提高100倍。另一个考虑因素是工具支持。R没有像Eclipse或Visual Studio这样的集成开发环境。最后,如果程序非常大,则可能执行一些R不擅长处理的非统计任务。 - John D. Cook
2
有一个插件(Stat-ET)可用,可以让Eclipse与R进行交互。我同意C++比R运行速度更快。但是,你需要多少时间来将R的东西重新编码为C++?除非你可以经常重复使用代码,否则更快的代码带来的好处与将其重新编码为C++所需的努力相比不值得。 - Thierry
2
是的,生产力与性能之间存在一种权衡。对于纯数据分析/统计工作,R通常获胜。但对于编写其他任务,如GUI、Web等,我不确定是否也是如此。我们经常在R中进行原型设计和工作,但在Python/C++中部署生产代码。使用后者,您可以获得性能以及各种任务的非常成熟和可重用的库/框架。但是,这是一个不断发展的情况,R生态系统不断发展。 - ars
我的个人建议是要习惯使用某种中间数据输出,比如数据透视表——在每个函数之后将其存储为csv文件;这可能有助于了解哪些数据属于哪个任务,特别是在使用大型或者有些奇怪的数据集原型化数据工作流程时。 - undefined
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接