在R编程实践中:不同风格的优缺点是什么?

34
最近关于require和::使用的问题引发了有关在R编程时使用哪种编程风格及其优缺点的问题。浏览源代码或者在网络上浏览,您会看到很多不同的风格。
我的主要编程风格:
- 重度向量化,我会经常使用索引(以及嵌套索引),这样有时会导致代码变得不太容易理解,但通常比其他解决方案更快。例如:x[x < 5] <- 0而不是 x <- ifelse(x < 5, x, 0) - 我倾向于嵌套函数,以避免使用临时对象过多占用内存。特别是对于操作大型数据集的函数来说,这可能是一个真正的负担。例如:y <- cbind(x,as.numeric(factor(x)))而不是y <- as.numeric(factor(x)) ; z <- cbind(x,y) - 我编写许多自定义函数,即使我只在一个sapply中使用该代码。我认为这可以使代码更易读,同时又不必创建可能仍然留在那里的对象。 - 我尽量避免使用循环,因为我认为向量化更加清晰(也更快)。
然而,我注意到人们对此的看法不同,有些人倾向于避开他们称之为我的“Perl”方式的编程风格(甚至是“Lisp”,因为在我的代码中有很多括号飞来飞去。不过我不会那么说)。
您认为在R中什么是好的编程实践?
你的编程风格是什么,你如何看待它的优缺点?

你的数据集有多少行和列?你是否经常进行分组/键入操作?如果你经常在分组数据上进行就地突变(x[x < 5] <- 0),我会倾向于使用data.table:=运算符。你的优先级是快速代码、紧凑代码还是易读性稍微降低一些的性能惩罚?此外,请展示一些自定义函数的示例,以便人们可以评论。 - smci
4个回答

21
我写代码的方式取决于编写代码的原因。如果我为我的研究(日常工作)编写数据分析脚本,我希望得到易于阅读和理解的代码,即使几个月或几年后也是如此。我不太关心计算时间。使用像lapply等向量化函数可能会导致代码难以理解和混淆,因此我尽量避免使用它们。
在这种情况下,如果lapply让我为了构造适当的匿名函数而费劲,我会使用循环来处理重复过程。对于您提到的第一个点,我会使用ifelse(),因为至少在我看来,该调用的意图比子集+替换版本更容易理解。在我的数据分析中,我更关心正确性而不是计算时间 --- 在我不在办公室的周末和晚上,我可以运行大型作业。
对于其他要点,我通常不会嵌套调用,除非它们非常简单。如果我显式列出步骤,我会发现代码更易于阅读,因此不太可能包含错误。
我经常编写自定义函数,特别是如果我将重复调用函数相当于在循环或类似结构中进行。这样,我将代码封装到主要数据分析脚本之外的自己的.R文件中,这有助于保持分析意图与分析方式分离。如果该函数有用,我可以在其他项目中使用它等。
如果我正在为软件包编写代码,我可能会以与数据分析相同的态度开始(熟悉),以获得我知道可行的东西,然后只有在想要提高计算时间时才进行优化。
我试图避免的一件事是在编码时变得太聪明,无论我编写任何内容都是如此。最终,我从不像我自以为是的那样聪明,如果我保持简单,我就不太可能像试图聪明一样经常失败。

6
赞成"过于聪明"的说法。虽然我已经习惯了使用索引,可以很容易地读懂代码并理解它的作用。但我同意对于接替我的人来说,这并不总是那么明显。 - Joris Meys
难道这不是注释的作用吗? - naught101
是的,但有些 R 代码可能非常晦涩难懂,所以即使我知道某个功能是什么,如果代码可读性和理解性好,那么它是如何实现的就更容易推测出来。 - Gavin Simpson

11

我会为各种代码块编写函数(独立的.R文件),每个函数都完成一件事情,这样可以保持简洁。我发现调试有些容易,因为traceback()会告诉你哪个函数产生了错误。

我也倾向于避免循环,除非绝对必要。如果我使用for()循环,我会感到有些污秽。:) 我会尽力使用向量化或应用程序族来完成所有工作。这并不总是最佳实践,特别是如果您需要向不太精通apply或向量化的人解释代码。

关于使用require::,我倾向于两者兼备。如果我只需要从某个包中获取一个函数,我会通过::使用它,但如果我需要多个函数,则会加载整个包。如果在包之间存在函数名称冲突,我会记住并使用::

我尝试为我要实现的每个任务找到一个函数。我相信在我之前,有人已经考虑过并制作了比我能想出的任何东西更好的函数。这有时奏效,有时则没有。

我尝试编写自己可以理解的代码。这意味着我会添加很多注释,并构建代码块,以便它们在某种程度上遵循我要实现的思想。随着函数的进行,我经常重写对象。我认为这有助于保持任务的透明度,特别是如果您稍后需要引用这些对象。当计算时间超出我的耐心时,我会考虑速度。如果一个函数需要花费太长时间才能完成,以至于我开始浏览SO,我会尝试改进它。

我发现一个带有代码折叠和语法着色功能的优秀语法编辑器(我使用Eclipse+StatET)可以省去我很多麻烦。

根据VitoshKa的帖子,我正在添加一个使用大写字母(sensu Java)作为函数名称,使用句点分隔符作为变量的命名风格。我注意到我可能还可以为函数参数选择另一种样式。


1
编辑器中的代码折叠功能加1。我也应该在Tinn-R中弄清楚这一点。如果我知道怎么做,那就可能了...但我想强调的是,apply函数族也是循环结构,尽管具有不同的副作用(请参见此问题中的讨论:https://dev59.com/73E95IYBdhLWcg3wheNR)。 - Joris Meys
2
同意,apply基本上就是“对于每个元素”,这与for循环完全相同。唯一的区别是当你习惯了apply时,你不必考虑某些细节(如按列、行、列表元素选择),代码可以非常易读。但也许只有我这样认为。 - Roman Luštrik
我每天都会遇到这样的情况,用 sapply/lapply 取代 for 循环至少可以加快一个数量级的执行速度,而不会影响可读性。所以我(几乎)不再使用“for”了。 - Aaron Statham

8
命名规则对代码的可读性非常重要。受R语言S4内部样式的启发,以下是我使用的命名规则:
- 全局函数和对象使用驼峰命名法(例如doSomething、getXyyy、upperLimit)。 - 函数以动词开头。 - 未导出的辅助函数始终以“.”开头。 - 本地变量和函数全部使用小写字母和“_”语法(如do_something、get_xyyy),这样易于区分本地和全局变量,从而使代码更加清晰。

4

为了数据处理,我尽可能多地使用SQL,至少对于像GROUP BY平均值这样的基本操作。我非常喜欢R,但有时候发现你的研究策略不够好,无法找到另一个隐藏在另一个包中的函数,这并不令人愉快。对于我的情况,SQL方言之间的差异不大,代码非常透明。大多数时候,何时开始使用R语法的阈值是相当直观的,例如:

require(RMySQL)
# selection of variables alongside conditions in SQL is really transparent
# even if conditional variables are not part of the selection
statement = "SELECT id,v1,v2,v3,v4,v5 FROM mytable
             WHERE this=5
             AND that != 6" 
mydf <- dbGetQuery(con,statement)
# some simple things get really tricky (at least in MySQL), but simple in R
# standard deviation of table rows
dframe$rowsd <- sd(t(dframe))

我认为在大多数情况下,使用SQL数据库来存储数据是一个好的实践,并且强烈建议这样做。我也正在研究TSdbi并将时间序列保存在关系数据库中,但目前还无法判断其效果如何。


1
考虑到我们正在处理大量数据集(超过100,000行的多个变量),使用SQL会得到+1分。但对于较小的数据集来说,这似乎有些过度了。 - Joris Meys
请查看John Chambers最近关于“多语言和R”的演讲。 - Dirk Eddelbuettel
@Dirk:能分享一个链接吗? - Joris Meys
2
当然,很抱歉,鉴于Dave Smith在博客上发表了这篇文章,我认为这是公开的信息:http://stat.stanford.edu/~jmc4/talks/Stanford2010_slides.pdf - Dirk Eddelbuettel
3
谁给这个点赞的,我不是在抱怨,只是不明白使用SQL有什么问题?也许那篇论文能帮到你 :) - Matt Bannert

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