About 谢益辉

Iowa State University统计系在读博士生。统计之都网站创办者;研究兴趣为统计图形及数据可视化,对统计模型方法的发展感兴趣但不喜欢纯粹抽象的数学理论,以直观、实用为学习标准;偏好以R语言为工具;Email:xie@yihui.name;个人主页:http://yihui.name

真理在缩水,还是上帝在掷骰子?

最近在Google Reader中看见科学松鼠会有两篇文章被频繁分享,名为《真理在缩水——现代科学研究方法并不尽善尽美?》()与(),下文简称《缩水》。文章很有意思,而实际上说的是我们的老本行——统计学,因此我在这里也发表一些我的想法和理解,包括这两年我在美帝学习的一些思考,部分内容受益于两位老师Kaiser和Nettleton教授,先向他们致谢(尽管他们永远都不会看到这篇文章)。同时我也要先说明一下,读这篇文章可能会很花时间(至少我花了大约二十小时写这篇文章),即使我的观点没有价值,我相信里面的引用文献是有价值的。

初读文章,我脑子里冒出的一句话是“上帝在跟我们掷骰子”,文中给出了大量的不可重复的试验,仿佛就像那些号称“具有统计学意义”(下文我再说这个所谓的“意义”)的试验结果只是若干次骰子中的一次运气好的结果而已。读完文章,我们可能不禁要问,到底是真理在缩水,还是它根本就不曾存在?下面我从四个方面来展开,分别说明人对随机性的认识、统计推断的基石、让无数英雄折腰的P值、以及可重复的统计研究。

一、感知随机

随机变量在统计分析中占据中心地位,数学上关于随机变量的定义只是一个“干巴巴的函数”,从样本空间映射到实数集,保证从实数集上的Borel域逆回去的集合仍然在原来的sigma域中即可。随机变量的性质由其分布函数刻画。写这段话的目的不是为了吓唬你,也不是为了作八股文,而是来说明我为什么不喜欢数学的理由,对我而言,我觉得有些数学工具只是为了让自己不要太心虚,相信某时某刻某个角落有个理论在支撑你,但后果是弱化了人的感知,当然,也有很多数学工具有很强的直觉性(如果可能,我想在未来下一篇文章里面总结这些问题)。我一直认为很多人对随机性的感知是有偏差的,对概率的解释也容易掉进陷阱(参见Casella & Berger的Statistical Inference第一章,例如条件概率的三囚徒问题)。

《缩水》一文发表了很多不可重复的试验案例,我们应该吃惊吗?我的回答是,未必。举两个简单的例子:

第一个例子:很多数据分析人员都很在意所谓的“离群点”,论坛上也隔三差五有人问到这样的问题(如何判断和处理离群点),而且也有很多人的做法就是粗暴地删掉,我从来都反对这种做法。除了基于“数据是宝贵的”这样简单的想法之外,另一点原因是,离群点也许并非“异类”。离群点是否真的不容易出现?请打开R或其它统计软件,生成30个标准正态分布N(0, 1)随机数看看结果,比如R中输入rnorm(30),这是我运行一次的结果:

> rnorm(30)
 [1]  1.19062761 -0.85917341  2.90110515  0.59532402 -0.05081508 -0.06814796
 [7]  2.08899701  0.76423007  0.92587075 -1.16232929 -0.68074378 -1.40437532
[13] -0.17932604 -0.72980545 -0.53850923  0.21685537 -0.35650714 -1.32591808
[19] -0.88071526 -1.25832441  0.24001498 -0.41682799 -0.09576492 -0.17059052
[25] -0.99947485  0.25108253 -0.47566842 -0.28028786  0.79856649 -0.13250974

30在现实中是一个比较小的样本量,你看到了什么?2.901?它接近3倍标准差的位置了。还有2.089?……如果你不知道这批数据真的是从标准正态分布中生成出来的,现在你会有什么反应?把2.9删掉?标准正态分布是一个在我们眼中很“正常”的分布,而一个不太大的样本量一次试验足以生成几个“离群点”,那么要是成千上万的试验中没能产生几项震惊世界的结果,你会怎样想?(上帝的骰子坏掉了)

另一个例子和统计学结合紧密一点,我们谈谈残差的QQ图。QQ图是用来检查数据正态性的一种统计图形,与腾讯无关,细节此处略去,大意是图中的点若呈直线状(大致分布在对角线上),那么可以说明数据的正态性比较好,因此QQ图经常被用在对回归模型残差的正态性诊断上。我的问题是,即使数据真的是正态分布,你是否真的会看见一些分布在直线上的点?若答案是否定的,那么我们就得重新审视我们对分布和随机的认识了。下图是一幅教科书式的QQ图(仍然基于30个正态分布随机数):

“正常的”QQ图

“正常的”QQ图(来自R语言qqnorm(rnorm(30)))

随机性并没有这么美好,即使数据真的来自正态分布,你也有可能很容易观察到歪歪扭扭的QQ图,尤其是小样本的情况下。比如下图是50次重复抽样的正态数据QQ图,它和你想象的QQ图本来的样子差多远?

library(animation)
set.seed(710)
ani.options(interval = 0.1, nmax = 50)
par(mar = c(3, 3, 2, 0.5), mgp = c(1.5, 0.5, 0), tcl = -0.3)
sim.qqnorm(n = 30, pch = 19, col = "red", last.plot = expression(abline(0, 1)))
真实的正态分布QQ图

真实的正态分布QQ图(图中直线为y = x)

正态分布是统计学中比较“正常”的一类分布(台湾学者甚至译为“常态分布”),我们尚不能很好感知它的随机性,就不必说其它“怪异”的分布了。

这是我想说的第一点,作为人类,我们对上帝的骰子至少在感知上就可能有问题(别误会,我不信教),接下来从理性角度分析一下。

二、统计推断

《缩水》一文中提到的基本上都是统计推断方法带来的结果,为了理解这些结果,我们必须三思统计推断本身的过程。一般说来,统计推断有两种途径:随机试验和(概率)统计模型,或者说基于试验的推断和基于模型的推断。前者的代表性方法为置换检验(Permutation test),不过它似乎被大多数人遗忘了,更多的人拿到数据首先想的就是用哪个统计模型和分布;实际上,置换检验是一种极具代表性的统计推理方法,可以用典型的“三段论”来说明它(参见去年江堂的文章):

  1. 要么A,要么B
  2. 若有A,则有C
  3. 若非C,则非A,于是B

置换检验的场景是,一次试验中,我们为试验单元随机分配不同的处理(treatment),为了简单起见,假设这里只有两种处理水平A和B,我们想知道两种处理在试验单元的某项指标上是否有显著差异。逻辑是这样:假设处理毫无效果,那么某一个试验对象的指标将不受处理影响,不管我们给老鼠嗑的是A药还是B药,结果都一样,那么我们可以把处理的标签随机打乱(某些A、B随机互换),打乱后A组和B组的差异不应该会和原来的差异很不一样(因为药不管用嘛),否则,我们恐怕得说,药还是管用的。就这样,我们随机打乱标签很多次,形成一个“人工生成”的AB差异分布(因为我们生成了很多个差异值),看原来的差异在这个分布的什么位置上,如果在很靠近尾巴的位置上,那么就认为P值很小。当了个当,当了个当,P值出场了。对置换检验熟悉的人是否有想过,好像我们一直没谈到什么分布的假设,那这个概率值(P值)是从哪里生出来的?答案是最初的“随机分配处理到试验单元上”。这就涉及到试验设计的一大原则:随机化。为什么要随机化?因为试验单元之间可能本来就有差异,换句话说,如果你不随机化,那么你观察到的差异有可能来自试验单元本身。比如,你从笼子里抓前10只老鼠给嗑A药,后10只老鼠给B药,这就没有随机化,前10只老鼠可能很笨或是老弱病残,容易被你抓住,而后10只老鼠跑得贼快。随机化将这些个体差异转变为了随机的误差,例如两只老鼠之间的确本身有差异,但我要是把它们随机分配给处理,那么这种个体差异就会随机进入处理组,保证统计推断有根基。如果这一点没有理解透彻,试验人员很容易在数据收集阶段就已经收集了错误的数据。《缩水》一文中的试验都是怎么做的,我没空去细究。

基于模型的推断的一大特征就是开始对随机变量做一些分布上的假设,例如两样本t检验,假设样本来自独立同方差的正态分布。仔细想想这里面的问题,对建模和理解模型结果有至关重要的作用。一个最直接的问题就是,假设条件是否可靠?George Box大人很久很久以前就说了一句被引用了无数遍的话:所有的模型都是错的,但有些是有用的。统计学方法很“滑”(用麦兜的话说),它的科学与艺术双重身份,总让人感觉拿捏不准。学数学的人可能觉得艺术太不靠谱,其它外行人士可能觉得科学太神秘。这个问题我不想作答,也无法作答,搁在一边,先说一些我曾经考虑过的问题,它们与《缩水》一文可能并没有直接联系,但应该能或多或少启发我们从源头考虑统计模型,直接上手统计模型的隐患就在于你认为一切都理所当然,随着时间推移,假设渐渐变成了“公理”和“常识”,我觉得这是很可怕的。

第一个问题是似然函数(likelihood function),它是频率学派的命脉,而我们大多数人仍然都是频率学派的“教徒”。对于离散变量来说,基于似然函数的方法如极大似然估计很容易理解:我们要找一个参数值,使得观察到的数据发生的概率最大。这里的“概率”二字应该被重重划上记号!接下来我要提一个让我曾经觉得后背发凉的问题:

为什么对连续变量来说,似然函数是密度函数的乘积?

你是否想过这个问题?我们知道连续变量取任何一个值的概率都是0,也就是说,如果按照概率的方式解释似然函数,那么连续变量的似然函数应该是0才对,换句话说,你观察到的数据发生的概率是0。现在你觉得似然函数还是一个理所当然的统计工具吗?

有一位统计学家叫J. K. Lindsey,1998年在(英国)皇家统计学会宣读了一篇论文,叫Some statistical heresies(一些统计异端邪说),如果你没见过统计学家打仗,那么这篇论文将会让你看到一场超大规模的战争,后面的讨论者中包括Murray Aitkin、D. R. Cox和J. A. Nelder等老江湖。Lindsey的文章几乎是被大家围攻了(期待我这篇文章也能被围攻),不过他对于似然函数的解释倒是让我有点茅塞顿开。细节我不展开,大意是,似然函数也是一种近似(用积分中值定理去想)。

第二个问题是渐近统计(asymptotic statistics),同样,这也是统计学家的日常工具之一,因为太常见,所以也有一种理所当然的味道。比如我们看到列联表就想到卡方检验(检验行列变量的独立性),殊不知卡方检验只是一种大样本下的近似方法。渐近统计的基石是什么?如果你要挖,我想挖到头你一定会挖到泰勒展开。至少目前我认为渐近统计“基本上就是泰勒展开的第二项(或少数情况下第三项)”。理论上泰勒展开有无穷多项,我们往往根据一些假设条件,把后面的高阶项都消灭掉,剩下一次项或二次项。比如你展开似然函数,就得到了似然比检验的卡方分布;你展开极大似然估计的一个连续函数,你就有了Delta方法(当然,需要依分布收敛的前提);就这样左展右展,展出了中心极限定理(对特征函数或母函数展开),展出了拉普拉斯近似(对对数密度函数展开)。如果你能看到这一点,就不会奇怪为什么正态分布在统计推断中有如此中心地位(谁叫正态分布的对数密度函数是个二次函数呢)。

第三个问题是,贝叶斯方法又如何?既然频率学派中几乎处处是近似,贝叶斯学派是否会好一些?我的回答是好不到哪儿去。贝叶斯的逻辑很简单,但由于灵活性太强,应用时非常摧残人的脑力,导致争议不断(先验分布怎么取、MCMC是否收敛等)。在《缩水》一文中,恐怕是没有基于贝叶斯方法的试验,因为在所谓的科学试验中,人们往往排斥“先验”这种想法,就好像先验一定代表主观、而客观一定正确一样。逻辑上,这是荒谬的。关于这些问题的重磅讨论,可参考Efron去年发表的The Future of Indirect Evidence以及文章后面Gelman他们三个人的讨论。我总感觉这不是我这个年龄应该看的东西,太哲学了。

我提到这些问题,本意是给统计学家看的,如果你是一个合格的统计学家,你自己首先应该意识到统计学的局限性,而不是拿到数据就分析。

三、万能的P值?

早些年当我还是个无知轻狂小子的时候,我曾戏称“统计软件就是为了放个P”,这里的P指的是P值,不是粗话。这话好像也不全然轻狂无知。使用统计方法的人,难道不是在追逐一个小于0.05的P值吗?如果你的结果不显著,那么肯定发表不了。换句话说,发表的结果很有可能是我们在自欺欺人。下面的漫画生动刻画了人们寻找P值的过程(来自xkcd):

Significant

Significant

重大科学发现!吃绿色的软糖会让你长痘痘!95%置信度!

当你看到95%的时候,你看不到红色的、灰色的、褐色的、橙色的、黄色的软糖……这便是《缩水》一文中说的“发表偏见”(publication bias,“偏见”翻译似乎不妥),即发表的结果是经过人工选择的,你已经不能用正常的概率意义去解读它,或者说,概率已经变了样。

插一句“统计学意义”:这个概念本来的英文是statistical significance,但是被很多专业的人翻译为“统计学意义”,我一直认为这很不妥,给人一种错觉,仿佛统计学保证了什么东西有意义一样,我提倡的译法是“统计显著性”。尤其是“由于P值小于0.05,所以具有统计学意义”这种话,我觉得应该见一句删一句。

上面的软糖问题涉及到传统的多重比较(multiple comparison)与P值调整,这里“传统”指的是要控制族错误率(Familywise Error Rate,下文简称FWER)。所谓控制FWER,也就是要使得一族(多个)统计检验中,一个第一类错误都不犯的概率控制在一定水平之下,比如0.05。让多个检验全都不犯错和单个检验不犯错(指第一类错误)显然是有区别的,比如假设所有的检验都是独立的,一个检验不犯错的概率是95%,两个都不犯错的概率就变成了95% * 95% = 90.25%,检验越多,不犯错的概率就越小。把整体的第一类错误率控制在某个alpha值之下,就意味着单个检验必须更“严格”,因此我们不能再以0.05去衡量每个检验是否显著,而要以更小的值去衡量,这就是P值的调整,老办法有Bonferroni等方法。

控制FWER显得有些苛刻,比如有10000个基因都需要检验在不同处理下的表达差异,那么要是按照传统的P值调整方法,恐怕是很难得出某个基因显著的结论(因为alpha值会被调得极其小)。FWER的目标是一个错误都不能犯,但实际上我们也许可以容忍在那些我们宣称显著的结果中,有少数其实是犯错的,就看你是不是“宁愿错杀三千,也不放过一个”了。

Efron在前面我提到的文章中把Benjamini和Hochberg在1995年的论文称为“二战以来统计界最伟大的成果”(他把第二名给了James & Stein的有偏估计),那么B&H做了什么?答案就是虚假发现率(False Discovery Rate,下文简称FDR)。FDR要控制的是在宣称显著的结论中错误的结论的比例,比如10000个基因中有100个基因显著,但实际上有5个是虚假的发现(即本来这5个基因分别在两种处理下的表达并没有差异)。尽管有错误存在,但我们认为可以承受。

初学者到这里应该可以意识到了,通过FDR选出来的结果在理论上就已经存在错误了,当然这只是小问题,更大的问题在于,FDR的定义实际上是一个期望的形式:真实的零假设个数除以被拒绝的零假设个数的期望(零假设是没有差异)。凡是涉及到期望的东西,我们都应该冷静三秒想一下,期望意味着什么?

假设有一个游戏,你获胜的概率是70%,要是赢了,你得到一百万,要是输了,你付出一百万;获利的期望是40万。现在我请你去玩,一把定输赢,你玩不玩?我相信大多数人不会玩(除非你实在太有钱了),为什么期望是赚40万你也不玩?因为期望往往是“样本量无穷大”你才能隐约看到的东西,玩一次游戏,输掉一百万的概率是30%,恐怕你不敢。

FDR是个期望,也就是你做很多次试验,平均来看,FDR在某个数值附近。一次试验中它究竟在哪儿,谁都不知道。就像(频率学派的)置信区间一样,我给你一个区间,其实你并不知道参数在不在区间里,你也无法用概率的方式去描述单个区间,比如你不能说“参数落在这个区间内的概率是95%”(只有无数次抽样重新计算区间,这无数个区间覆盖真实参数的概率才是95%)。

所以,某种意义上,概率论源于赌博,而统计学在骨子里一直都是赌博。旁观者看赌徒,总觉得他在赚钱。当然,统计学家是“高级赌徒”,他们不是随机乱赌。

四、可重复的统计研究

看到这里,你大概也脑子有点抽筋了(如果你把我提到的Lindsey和Efron的文章都看过了的话),我们换个轻松点的话题:可重复的统计研究。这不是我第一次提它,我们一直都在号召可重复的统计研究(如《Sweave:打造一个可重复的统计研究流程》)。还是老话一句,不谈道德问题,因为这是不可控的因素,我们可控的只有制度和工具。源代码不会撒谎。

我们期待学术研究过程的透明化,至少统计之都在努力。

开发R程序包之忍者篇

作为一个伪程序员,我在做与代码有关的事情时,总是抱以一个念头,即“简化手工劳动到极致”。在这篇文章里,我介绍一下目前我认为最简化的开发R包的流程。本站作者胡荣兴曾经在09年写过一篇开发R包的文章“在Windows中创建R的包的步骤”,其中小部分内容随着R本身的更新已经过时,该文面向Windows,而且介绍的都是一些正统方法,这里我介绍一条“忍者”之路,希望对大家开发R程序包有所帮助。这篇文章本来是去年年底打算写的,时至今日第四届中国R语言会议正在人民大学轰轰隆隆召开,索性把它写完,算是一份不到场的报告吧。

在我看来,R的扩展性主要体现在R包中,利用附加包的形式,我们可以把一些常规的、模式化的工作打包起来供日常使用,在R包中我们还可以为函数编写文档和说明,这样可以避免将来忘记一个函数是做什么的以及怎么用的(忘记了就查帮助,?function.name),文档是程序的重要组成部分,我个人常常认为写文档的难度不亚于写代码;此外,R包还体现了R的另一点扩展性,即它能融合其它底层语言,典型的就是C语言、C++和Fortran,但一般用户可能用不到这些功能,下文仅简要介绍一下。

一、工具

对Linux和Mac用户来说,只要装好了R,开发R包的工具就已经具备,可跳过本节。Windows用户除了安装R之外,还需要Rtools和一套LaTeX程序,典型的如MikTeX。安装Rtools的过程中有一步需要注意,就是修改环境变量PATH,这个选项是需要选上的。对于R本身,还需要把它的bin路径放到环境变量PATH中去。说了半天,什么是PATH?这是个让明白的人抓狂、不明白的人迷茫的问题,不过找它比找拉登可能还是稍微容易一点:

“我的电脑”(右键)–>“属性”–>“高级”–>“环境变量”–>“系统变量”–>PATH

这里我们可以看到一连串路径。为什么系统要有个PATH变量?原因就是为了能够脱离程序的绝对路径以命令行方式来运行程序,这样使得程序员不必担心你的程序装在什么位置。当你在命令行窗口(*nix系统下叫终端,Terminal)中敲入一个命令时,系统就会从这一系列的PATH路径中去找你敲的这个程序是否存在,如果存在就运行它。“开始”–>“运行”(或者快捷键Win + R),输入cmd回车运行,就会打开一个命令行窗口。如果你从来没用过这玩意儿,那你肯定是Windows深度中毒者,不妨先玩玩dircd ..等命令。

前面说要把R的bin路径加入PATH,这个路径在哪儿?如果你记不住自己的R装在哪儿,没关系,打开R,输入R.home('bin')就知道了。通常是类似于C:\Program Files\R\R-2.xx.x\bin\这样一个路径。说到这里,我还得绕道再说一句:安装R的时候尽量装在一个不带版本号的目录下,否则将来更新很麻烦,而且每次装新版本的R还得再修改PATH变量,因为bin路径变了。关于这一点,我只能说R core们都太严谨了,上次我在R-devel邮件列表里被一群人打了个落花流水,他们死活都不能接受把R的安装路径改成默认不带版本号的。

设置好R的路径之后,为了测试各种设置是否齐备,可以打开命令行窗口,输入一些常用命令看看能否执行:

R --version
ls
gcc --version

如果这些都没问题,就可以进入下一节了。这里敲R对命令行来说,就是看看PATH中那些路径里有没有一个路径下包含R.exe或者R.bat之类的可执行文件,这就是所谓的“脱离绝对路径运行程序”。由于*nix系统的程序管理方式和Windows不同(可执行文件通常统一放在/bin/或者/usr/bin/目录下),所以通常没有这些痛苦。

二、R包结构

写R包最好的参考莫过于R自身的手册“Writing R Extensions”(下文简称R-exts)。在R中打开HTML帮助(help.start()),就可以看见这本手册,内容很长,不过大部分都是普通用户不必关心的。我的建议如下:对新手而言,必须要了解R包的结构,所以1.1.1节1.1.3节必读,而整个第2节可能是将来需要反复参考的(除非你记性很好);已经上路的用户可以接着看一些高级话题,如命名空间(1.6节)和底层语言的使用(第5节)等。

一个最简单的包结构如下(括号中为相应解释):

pkg (包的名字,请使用一个有意义的名字,不要照抄这里的pkg三个字母)
|
|--DESCRIPTION (描述文件,包括包名、版本号、标题、描述、依赖关系等)
|--R (函数源文件)
   |--function1.R
   |--function2.R
   |--...
|--man (帮助文档)
   |--function1.Rd
   |--function2.Rd
   |--...
|--...

DESCRIPTION文件是一个纯文本文件(没有扩展名,Windows用户可以用记事本或其它文本编辑器打开),其内容参考R-exts就明白了,注意只有几个字段是必须的,其它都可选。如果你的包依赖于别的包中的函数,那么可以在Depends一行中写上那些包的名字(逗号分隔)。如果这个包只需要引入(Imports)别的包中的函数,那么就在Imports中写那些包的名字。Depends和Imports有细微差别:前者将导致加载你的包时,依赖包也被明确加载进来,用户可以直接使用这些包中的函数;后者不会导致那些包被明确加载,只有你的包在调用那些函数,但那些函数对用户是不可见的(除非用户明确加载之)。这里涉及到命名空间问题,后文详述。

除了R和man文件夹之外,还有一些可能有用的文件夹,如demo下面可以放一些演示R代码,这样用户可以使用demo()函数调用这些演示,通常这是除了示例之外的最好的展示包功能的方式;data文件夹下可以放数据,这些数据通常是通过save()函数保存成*.rda文件放到这里;src文件夹下可以放其它语言的源代码,编译安装的时候这些代码会被编译为动态链接库文件(pkg.dll或pkg.so);inst文件夹下的任何文件在安装包的时候都将被复制到包的根目录下,例如这里可以放NEWS文件(包更新的消息)或CHANGELOG文件,inst下通常还有一个重要的子文件夹就是doc,这里可以放一个Sweave文件(*.Rnw),编译安装包的时候这个文件会被Sweave编译生成LaTeX文件继而生成PDF文档,这也是关于一个包的很重要的介绍文档,称为Vignette。

对于心急的看官,到这里可以先忽略所有介绍,直接写一个DESCRIPTION文件外加一个R文件夹,底下放一个*.R脚本文件,里面包含一个函数,然后其实就可以安装使用了。但这种粗略的办法不是长久之计,如前面所说,文档很重要,提醒自己也方便他人。

三、安装R包

装包很简单,一句R CMD INSTALL pkg就可以。例如你的包文件夹路径为/a/b/c/pkg/,那么先在命令行窗口中切换到这个pkg的上层目录下,然后用前面的命令安装:

cd /a/b/c/
R CMD INSTALL pkg

这样就装好了,在R中可以通过library(pkg)加载进来使用。

四、忍者神龟

至此,似乎听起来很简单:写两个函数,扔在R文件夹下,然后R CMD INSTALL一下,完事。不写文档、觉得命名空间神马的最讨厌了的人现在的确可以退场了,接下来我们深入一些话题。

4.1 R文档与roxygen2

R-exts手册第2.1节给了一个简单的文档示例,我们可以看到R文档的语法和LaTeX很像,都是一些宏命令,如\title{我是标题}或者\description{我是描述}。当然,这些玩意儿你都可以手写,如果要稍微偷懒一下,也可以用package.skeleton()或者prompt()等函数来辅助生成Rd文件,这些函数都可以为你生成一些空模板,你自己往里面填充内容。若你的包只有一两个函数,倒也无妨,轻松写写完事,要是你想维护30个函数,那你就会觉得这种做法完全是坑爹。坑爹之处不仅在于你要么手敲这些命令要么绕道用函数生成文档模板自己填充,更在于你得在man文件夹下维护R文件夹下的函数的文档!你每次更新R函数,都得战战兢兢记住了:还有man文件夹下的某个*.Rd文件也许需要更新。

这并不是什么新鲜问题,所有的程序开发都面临这样的问题,于是有人发明了Doxygen,大意是把文档融入到源文件中,通常采取的方式就是把文档写成一种特殊的注释,这样不会影响源文件的执行(因为注释会被忽略),同时也可以从注释中动态抽取文本生成文档(如HTML或LaTeX/PDF等),这个主意相当妙。开发程序的时候只需要在同一个文件内操作即可:举头望文档,低头思函数。

roxygen2是一个R包(它的前任是roxygen,但已经停止更新了),它实现了把特定注释“翻译”为R文档的工作,例如:

##' @author Yihui Xie
##' @source \url{http://cos.name}

会被翻译为:

\author{Yihui Xie}
\source{\url{http://cos.name}}

你可能会说,嗨,介有嘛啊!!注意这些注释是直接写在函数定义上方的,当然,这么说你还是不信。所以下面必须介绍另一门暗器,也就是传说中的编辑器Emacs。

4.2 roxygen与Emacs

如果你得手敲那些##' 注释,那我当然不会写这篇文章。曾经有两个软件我觉得我永远都学不会,一个是Emacs,另一个是Photoshop;如今只剩下一个(我也不打算学了)。Emacs是我装了卸、卸了装超过10次的软件,终于在第11次搞明白了六指琴魔是怎么个练法。如果你也是新手,那么建议安装Vincent Goulet维护的修改过的Emacs。修改之一就在于直接加入了ESS(Emacs Speaks Statistics),ESS是Emacs的一个插件,它提供了编辑器与其它统计软件(如SAS、S-Plus、R)的交互,例如可以通过快捷键把R代码发送到R里执行。

ESS本身我觉得也没啥,但ESS加上了roxygen的支持之后我就觉得这是个忍者工具了。在Emacs中,光标放在R函数上,快捷键C-c C-o一按,就如同发出一把暗器,一个roxygen注释模板立刻生成了。这一点让开发R包不知道快了多少倍。也许有读者知道我在维护一个叫animation的R包,说实话,曾经有一段时间我实在不想维护了,因为写函数写文档太麻烦,直到打通了Emacs和roxygen关。

好嘛!听起来好像不错,咋用?装好Emacs之后,先去找个参考卡片,练习两天一些基本操作(打开文件、保存文件之类的),熟悉一些基本概念(有些相当坑爹,例如剪切不叫剪切,叫杀,粘贴不叫粘贴,叫拉),当然首先得知道C代表Ctrl键、M代表Alt键。再找个ESS参考卡片,看看基本的代码发送操作。总而言之,常用的快捷键不多,不需要真的变成六指琴魔。要是陷入了快捷键连锁陷阱(自己不知道按到哪里去了),就以万能的C-g退出再来。

假设你已经装好了Emacs,现在可以任意打开一个R文件:C-x C-f,输入文件名,回车,如果存在则会打开它,如果不存在,则会新建一个文件,注意作为一个(伪)程序员,你必须永远牢记:不要老老实实打字!能用Tab键的时候尽量用,它在很多情况下都能自动补全(如路径、对象名称等)。这里的文件名应该以.R或.r为后缀,这样Emacs才知道应该用ESS来处理它,例如abc.R。现在在编辑器界面内输入一个任意函数,如

stupid_f = function(a, b){
    a + b
}

然后把光标放在stupid_f这一行上,按C-c C-o,你就会发现你的文件变成了类似这样一个东西(根据ESS不同的配置,以下结果也许不完全相同):

##' title...
##'
##' description...
##'
##' details here
##' @param a
##' @param b
##' @return
##' @author Yihui Xie <\url{http://yihui.name}>
##' @examples
stupid_f = function(a, b){
    a + b
}

接下来你的任务就是把该填的文档填上。roxygen2的常规是,第一段是标题(将来翻译为\title{}),段落之间以空行分开,第二段是描述(\description{}),然后接着是这个函数的详细描述(\details{}),它可以是若干段落,你愿意写多长就写多长。剩下的@字段就不必多解释了,参数、返回值、作者、示例等,我们可以通过M-x customize-group,回车,ess,回车,来配置ESS中这些roxygen的默认字段。一个自然而然的问题就是,哪些字段是可用的?参见roxygen2包的帮助?rd_roclet。为了完全理解Rd文档和roxygen2字段的对应关系,你最好还是读一下手册R-exts和这两个帮助页面。注意ESS有很多方便的功能,比如你在roxygen注释之后回车,下一行会自动以##' 开头;在任意一个@标签后按M-q则可以把该段落自动折叠为短行,使得文本更整齐(这是我经常用的一个功能);如果函数中有新增加或者减少参数,那么只需要再次C-c C-o就可以自动更新上面的注释了,新增加的参数会自动加上新的@param,减少的参数会被自动删掉注释。

roxygen2还实现了一些自动功能,比较重要的就是对命名空间文件NAMESPACE和描述文件DESCRIPTION的自动更新,这些我们第五节再说。先说如何从roxygen注释翻译到Rd文档,很简单:如果一个包已经按第二节的结构写好(不需要有man文件夹),函数和相应的roxygen注释都已经存在,那么用函数roxygenize()就可以把这样一个初级包翻译为一个完整R包了:

setwd('/a/b/c/')  # 先把工作目录切换到pkg之上
library(roxygen2)
roxygenize('pkg')

默认情况下新生成的R文档以及更新的NAMESPACE和DESCRIPTION都生成在包的目录下,现在pkg就是一个完整的R包,包含自动生成的man文件夹,可以直接用R CMD INSTALL pkg安装。

天有不测风云,事情到这里还没完全结束,roxygen包也有些坑爹的地方,它本来是2008年Google编程夏令营的产物,但作者自夏令营结束之后投入维护的精力似乎就越来越少,导致很多问题都一直没有修正。我在使用过程中实在忍不了,于是动手写了个基于roxygen之上的包Rd2roxygen(更新:roxygen包现在已经被roxygen2取代了,后者在更新维护中)

4.3 后悔药包Rd2roxygen

后悔药的意思是,有人看见roxygen是如此的方便,大为后悔,因为维护原始R包太费精力了,可是爹已经被坑了,已经按照R-exts的要求老老实实写了那一大把*.Rd文件,肿么办?Rd2roxygen包诞生的目的就是为了解决这个问题:roxygen是把注释翻译为Rd,而这个包倒过来,把Rd重新翻译回注释!给你后悔药吃。如果你是后悔的人中的一员,不妨参考这个包中的Rd2roxygen()函数;如果是新手,那么这个包的rab()函数可能是roxygen中的roxygenize()函数的一个很好的替代,其中我比较自豪的一个功能是它能自动整理示例代码,很多R包的示例代码都不够整齐(无空格无缩进等)。详情参见帮助文件及其介绍文档(Vignette)。简言之,现在我们使用:

library(Rd2roxygen)
rab('pkg')
## 如果要直接安装,那么rab('pkg', install=TRUE)

所有工具至此大概介绍完毕。如果你还没昏死过去,请接着读第五节。

五、九霄云外

上面的东西刚上手时可能是有点晕,熟悉之后写包就立刻风驰电掣了。下面我们再介绍几个略微高级的概念。

5.1 命名空间

命名空间(NAMESPACE)是R包管理包内对象的一个途径,它可以控制哪些R对象是对用户可见的,哪些对象是从别的包导入(import),哪些对象从本包导出(export)。为什么要有这么个玩意儿存在?主要是为了更好管理你的一堆对象。写R包时,有时候可能会遇到某些函数只是为了另外的函数的代码更短而从中抽象、独立出来的,这些小函数仅仅供你自己使用,对用户没什么帮助,他们不需要看见这些函数,这样你就可以在包的根目录下创建一个NAMESPACE文件,里面写上export(函数名)来导出那些需要对用户可见的函数。自R 2.14.0开始,命名空间是R包的强制组成部分,所有的包必须有命名空间,如果没有的话,R会自动创建。

前面我们也提到DESCRIPTION文件中有Imports一栏,这里设置的包通常是你只需要其部分功能的包,例如我只想在我的包中使用foo包中的bar()函数,那么Imports中就需要填foo,而NAMESPACE中则需要写importFrom(foo, bar),在自己的包的源代码中则可以直接调用bar()函数,R会从NAMESPACE看出这个bar()对象是从哪里来的。

roxygen注释对这一类命名空间有一系列标签,如一个函数的文档中若标记了##' @export,那么这个函数将来就会出现在命名空间文件中(被导出),若写了##' @importFrom foo bar,那么foo包的bar对象也会被写在命名空间中。这些内容参见R-exts的1.6节和roxygen2的?export帮助。

5.2 介绍文档(Vignette)

前面我们提到了inst/doc/目录,下面可以放一个Sweave文件,在R CMD INSTALL过程中这个Sweave文件会被执行并生成PDF文档,若Sweave文件中有一句注释:

%\VignetteIndexEntry{An Introduction to XXX}

那么这句话将来会出现在HTML帮助页面中(点开链接“Overview of user guides and package vignettes”),例如Rd2roxygen包或者formatR包的帮助页面中就有介绍文档的链接。

5.3 其它语言

在src目录下我们可以放置一些其它语言的源代码,里面可能包含一些函数,这些函数在被编译之后,(以C语言为例)可以在R代码中以.C('routine_name', ..., package = 'pkg')的形式调用,但要注意,如果需要用这个功能,在R目录下需要有一个zzz.R文件(这个特殊文件是用来在加载包之前加载运行的代码),里面写上:

.onLoad <- function(lib, pkg) {
    library.dynam("pkg_name", pkg, lib)  # pkg_name是你的包的名字
}

这些东西我并不在行,只介绍到这里,详细内容还请深挖R-exts。另注意楼下Rtist的评论

六、发布

等你的包写好之后,还不能立刻发布,因为还有很重要的一步要做,就是看它是否能通过R CMD check的检查,这是你的包能发布到CRAN上的前提。在命令行界面中输入(请确保pkg文件夹是一个完整的R包,即:如果你用roxygen2,那么这里要检查的是运行过roxygen2之后的产物):

R CMD check pkg

R就会开始检查这个包是否有语法错误以及是否符合规范。或者如果你用Rd2roxygen包的话,也可以在R里面调用:

library(Rd2roxgyen)
rab('pkg', check = TRUE)  # 确保pkg文件夹在当前工作目录下:getwd()

检查过程会告诉你详细的日志信息,如果有错,你立刻就能知道。这里每个函数的例子(如果有的话)都会被运行,如果例子代码有错,这里也会报错,所以这个过程也是一个很好的检查自己的示例代码能否正确运行的测试。如果没有任何错误,那么就可以向CRAN提交了。提交的内容是一个压缩包,名为pkg_x.x-x.tar.gz,它是通过R CMD build pkg生成的。提交方式是通过FTP,参见CRAN首页说明。注意上传完之后需要向CRAN管理员发一封邮件,通知他们你提交了一个包,以后每次更新时的流程也一样:FTP上传+邮件通知。目前Kurt Hornik管理Linux包的编译,Uwe Ligges负责Windows包的编译,都是人工管理,要是碰上Kurt度假去了,你就得等着了(概率很小,不过我碰到过)。

七、后话

现在CRAN上的附加包数目已经超过三千,充分体现出R的良好扩展性和社区合作,要不然不会有这么多人去写R包(尽管这三千号包肯定是良莠不齐)。我个人是07年底从动画包animation开始写起的(想当年,R包里面还有微软的CHM帮助)……过了几年又陆续写了自动整理R代码的formatR包、把Rd转化为roxygen注释的Rd2roxygen包、把R图形转化为Flash动画的R2SWF包(与邱怡轩合作)、纯粹为了搞笑好玩的fun包(未发布)、为我的《现代统计图形》书稿配备的MSG包、用图形界面调用WinBUGS或者OpenBUGS的iBUGS包等。用麦兜的歌唱就是:

大包再来两笼
大包再来两笼
大包再来两笼不怕撑
……

扯远了。

写了这么些包,有些感受。首先,写代码有两样一般人不能理解的困难,一样前面已说,就是写文档,你得解释清楚参数的含义、得给出有用的示例代码、写演示写介绍文档等等,工作量其实很大;另外一样就是对象命名,我认为从对象的命名可以看出一个程序员的成熟程度,好的程序员,给的函数名既精炼又直观,这一点上必须佩服R core团队,要是我写几千个函数,光是想名字都能把脑袋想爆了,这里顺便提一下我推荐的命名方式,要么用驼峰(someFunction),要么用下划线(some_function),尽量不要用点(some.function),因为点在R语言中有一层特殊含义(S3方法的类匹配),这也是我对animation包比较后悔的一点。其次,你最好学会一样版本控制工具,如SVN或者GIT,不仅是管理包,它在管理任何文本文件时都非常有用,也利于多人合作,我现在倾向于GIT,主要是因为一个好网站的存在(GitHub),我的所有R包都已经从原来以SVN为基础的R-Forge上搬家到了GitHub,可以在那里参考我的包是怎么写的;不会版本控制工具的人的一个典型特征就是,电脑里存在一系列这样的文件:领导汇报20100101.doc、领导汇报20100102.doc……,版本控制工具可以让你很方便回到文件的历史状态,也方便多人合作(例如将A的更新和B的更新自动合并)。最后,写包不仅是对自己工作的一个不断总结,不至于做完一件事就永远尘封之,而且也是很好的自我宣传途径,你在简历上写得天花乱坠,可能不如以一件作品更能深入人心。

又及,写完一个包之后,你可能就不会再对别人的包问:这是哪个狗日的写的文档?

又又及,也许你就或多或少能理解自由软件为嘛还没死掉。

一封统计之都读者来信及回复

厦门大学的毛家栋同学几周前给我写了一封邮件,我看了之后觉得有拿出来公开回复的价值,一方面可以省去重复回复类似邮件的劳动,另一方面我也想借此机会说明统计之都(COS)网站的一些理念。本文不属于技术文章,但若能从此打开一个高手与新手互动的局面,那就善莫大焉了(当然我不是什么高手,只是跳梁者先出来献丑而已)。在征得同意之后,我将他的邮件以及其中的问题整理并回答形成本文,原邮件中的文字以引用格式出现(方框缩进),其它文字为我所写。首先声明这只是一家之言,读者大可冷眼旁观。另外,好为人师者往往惹人厌,我也得声明本文无此意。

一、感受

对COS的初步看法如下:

[...]日前在查找有关蒲丰投针问题推广的时候偶然进入了统计之都网站,浏览了几篇文章后对这个网站[...]产生了很强的兴趣[...]

看了几篇COS上的文章,逛了几次你的主页,我对你在统计上的理念非常有认同感。目前我们学习统计最大的一个问题正是在于对所学没有充分理解,更谈不上自由应用。那些死的定理并没有通过学习活过来,所以专业课总是学一门忘一门,以至于到现在相当一部分人在用软件时还说不上来什么是P值。大学两年以来,一直困扰我的一个问题是,学术是什么?作为本科生要怎样去接触学术研究?看了你的东西,我发现你和你的朋友是一群对统计有着十分热情的人,你们的讨论常常可以感染我,我在COS浏览的这几天受到了很大的启发,认识到了统计活生生的一面,也见识了一种对待数学的态度,毫不夸张地说,你们的工作给了我一个统计专业的学生一个启蒙的过程。

COS上的讨论环境非常好,这种有秩序的,就事论事的讨论氛围是我在其他网站上从未见识过的,也是我一直梦寐以求的。COS的这一点也让我非常叹服。[...]

看到读者对COS给予好评我们当然非常高兴,对此我们应该不顾正常感谢顺序,先感谢统计之都的作者们。我们有幸能邀请到一批有趣的作者在这里写文章,“有趣”对COS很重要,它不是意味着刻意搞笑,而是以一个活人的视角写一件用自己脑子想过、用自己的双手做过的事情。我们反感枯燥无聊的大段摘抄,反对不过脑子的转载,反抗那些没有生命迹象的纯理论。COS有自己特立独行的风格,这些风格,都是基于它的愿景。我们的愿景是什么?请抬头看我们的logo中的三个词。常常有人不理解第二个词Humanity,不知道这段话是否可以解释什么是我们的人本主义。因为有这个愿景的存在,所以我们的主站文章有质感,所以我们的论坛除了讨论问题什么功能都没有(没有论坛币、没有积分、没有广告)。某种程度上,我们有些理想主义,所以我们并不关心短期利益。比如最近我们的管理团队在讨论一个人,叫黄晓捷,读者从此可以大致知道我们追求的心境。当然,最终我们将努力把COS做大做强。

二、问题

整理之后毛家栋的问题如下:

关于本科生之于学术研究。对于有志于在统计学方面做进一步探究的本科生来说,本科阶段有没有接触学术研究的必要?如果有,通常接触学术研究的途径是什么?一般本科生对专业知识的掌握不深入全面,对统计软件的运用也远非熟练。在科研活动中可以承担什么样的工作?或者说,为什么科研活动需要本科生?

回想起来,我本科没有做过什么正儿八经的学术研究,现在我也很难说是否真的有必要接触学术研究。我个人一直提倡的是用兴趣引导自己,若你对学术实在提不起兴趣,那也大可不必在意,这不是唯一的生存之道。如果说我的本科学习有一丁点经验的话,我想有两点:一是我比较成功地(强迫)培养了自己对统计学的兴趣,比如尽管数理统计不好学,但我一直试图理清里面的头绪,最终也就渐渐理出兴趣来了;二是我经常混论坛(因为论坛就是我自己搭建的),借人大统计学院网站的影响力,这论坛才能有今天的景象,其中的历史此处不谈,论坛上的问题是学习的最好催化剂,各种各样的问题促使你去开阔眼界,去反思学到的东西。这基本上就是我接触学术研究的途径。曾经也有很长时间我对收集论文和电子书感兴趣,讽刺的是这里只是“收集”,我可能看过很多论文题目和书的引言,但我很少真的去读它们(是不是这些网络上传来传去的东西都没用呢?你自己思考)。知识是永远都无法掌握全面的,甚至越学越窄;话虽这么说,我出国之后还确实感受到国内的统计基础教育不够扎实,学得不够细致,比如线性模型。但话又说回来,也许这些所谓的基础净是些没用的东西,比如有些“黑箱”预测模型,你没命地算也许就能出好结果了。你看,我到现在对专业知识的想法还在晃来晃去,所以我觉得你走一步看一步也无妨。统计软件,我觉得看几页教程,在耐心消失之后就可以看问题了,以问题驱动软件学习,“熟练”这个目标说远不远说近不近,也许某一天就不知不觉越过了。科研活动本科生承担什么工作呢?我本科录入过问卷,搜过数据,大概都是些体力劳动吧。为什么需要本科生?说为了体力劳动好像很不厚道……我觉得更多在于锻炼、学习吧。

关于信息的获取。我所处的环境信息比较闭塞——首先厦门大学比之北京上海的高校信息方面就比较闭塞,另外我校大一大二学生所在的漳州校区更是极端封闭,见不到老师——这种环境还是有一定普遍性的。统计之都给我们提供了一个很好的平台,除此,我们应该通过什么手段去接触统计学的前沿、应用等情况呢,或者说,怎样在信息上融入统计学的学术圈子呢?

我们清楚这种情况,并且我们一直想促进高校统计专业之间的直接交流,比如在这里建设高校课堂栏目,可是这个任务对我们这些业余的人来说实在太艰巨。我们能做的就是征人写文章,希望对大家有用。网络上还有很多有用的资源,都可以是学习的资料,比如维基百科(Wikipedia)及其页面内的链接、课程视频网站VideoLectures等,我唯一的建议就是不要看那些所谓的“资源帖”,一下子整理上百个网站,我几乎从不相信那些不带有自己的评论的推荐。我不是专门为COS做广告,但我相信在COS泡着肯定有用,你说的前沿在论坛上偶尔就会冒出一些相关的帖子,都是值得阅读和思考的,两个例子:

      显然这对很多人来说都是前沿。至于怎么融入圈子,除了日积月累,恐无它法。我觉得如果能坚持一个小方向切入,可能会更有效率一些,否则很容易淹死在文献海洋中。比如贝叶斯,当你看到满地的抽样时就会想为什么贝叶斯统计中需要抽样模拟,进而了解一些历史,计算机的出现怎样让贝叶斯活了过来,变得如日中天,再看一些细节,比如Gibbs抽样是怎么回事(查维基百科、),如果什么都看不懂,可以退回到盘古开天辟地的时候,密度函数是什么?条件密度是什么?总之,一步步来。

      出国是很多统计人的选择,毕竟国外的统计学教育似乎要更为先进,而且学术风气也较国内纯净。统计学(数理、生物等)的申请情况如何?是否如有的说法那样是冷门学科?对于申请出国,特别是申请PHD的同学来说,校方比较看中的是什么?这里涉及一个比较实际的问题,处理不好可能成为急功近利——怎样做出高质量(这里指的是实实在在地做东西,而非无原则地多做甚至搞欺诈)的论文呢?抑或,除了论文,还有哪些比较过硬的成果可以作为学术能力的证明呢?

      这些问题我不是适合作答的人,后面我想请今年的一位申请出国的同学江麒来专门谈谈他的经验,可能会是本站的下一篇文章。我自己的经历不靠谱,供参考。出国这件事上你可能会看到五花八门的招数,最终还得自己琢磨一种有谱的招数。

      关于数学。应该怎样对待专业课中涉及到的数学呢?对数学应该探究到一个什么样的程度呢(特别是对于想读PHD的同学来说)?国内不同高校在这一点上做的很不同,有的学校的统计系几乎就是半个数学系,而有的学校如厦大对统计专业数学的要求不会超过一般工科院系的范围。

      我对数学的态度一直都是能混过考试就够了,这一点上将有无数的人不同意我,但我就是不喜欢它,没办法。数学的价值也不会因为我喜不喜欢它而变化,这里我说的数学主要是指数理统计及其以前的数学如测度论,以我肤浅的眼光,我不觉得这些玩意儿将来对我有什么用。探究到什么程度还是取决于你的兴趣和要解决的问题的需要(当然还取决于考试考什么)。就美帝而言,当然希望你数学尽量好。

      关于专业课。统计学专业课有一定难度,但令我感到更难的是如何将所学活学活用,你在这一点上是怎么做的呢?能不能进一步介绍一点你在统计软件学习方面的经验?

      活学活用首先也得会找到能用的地方,这同时也是个攒经验的过程。这方面我不敢说做到了,但显然统计之都的一些文章做到了,简单举最近的两例:

      这里面有什么特别高深的统计学知识吗?好像也没有。有活学活用的感觉吗?好像有。为什么?因为作者在主动思考身边的事情。关于统计软件,我仍然没有什么经验,仍然是日积月累。让你用一个软件用六七年,你觉得你能不熟吗?

      三、总结

      说了半天,就两件事:你想做什么?你是否有恒心?人可以战胜任何对象,唯一不可战胜的只有时间。《士兵突击》最精彩的部分在哪里?我认为在草原。守草原,就是守自己。你守得住自己不在网上闲逛偷菜看更多新鲜事吗?

      四、延续

      如我开头所说,这篇文章只是一个引子,后面我希望能看到更多有价值的采访。COS的读者们,如果你们有想了解的人(可以是任何人:退休教授、在任老师、论坛著名ID、留学生等),不妨在此提名并附上你的问题或者邮件发给我们(contact@cos.name),我们可以在后面的文章中安排这些访谈。

      八卦读者可从这个帖子访

      统计学论文的发表流程、及统计学家的晋升和合作(内幕)

      统计学论文的发表流程、及统计学家的晋升和合作(内幕)这标题很吸引人,所有统计学相关领域的人可能都关心这几件事,但敬请降低对本文的期望。我不能再多说,否则要剧透了(看过的朋友也请不要剧透)。这段35分钟的视频讲述了统计学论文是如何发表的、统计学家在机构内如何得到晋升(影响晋升的指标),以及统计学家和生物学家如何交流和合作的种种“内幕”。新年伊始,我们也不想用大篇技术文章来“折磨”统计之都的读者们,那么,开始欣赏这部小电影吧:

      (观看本视频可能需要安装QuickTime,视频为MP4格式)

      本视频来源于Johns Hopkins Schools of Medicine and Public Health的Steve Goodman教授,已获得作者授权发布。欲转载者请自行联系原作者。Goodman教授在授权同时也表达了他对文化差异的担心,可能会有人看不懂这个片子的真正意思。如果你看完这个片子之后觉得气愤或不解(相信应该没有这样的人),请尽管联系我(谢益辉)给你解释。

      我想补充说明的是视频的第三部分,尽管我前面的“内幕”打了引号,但这里反映的其实也是一种现实:统计学家和其它专业的人打交道经常互不理解,鸡同鸭讲。这在COS论坛上也经常出现——提问的人通常夹杂太多专业术语,让统计人摸不着头脑;实验设计、数据和问题被淹没在术语中,理不清头绪。反过来也一样,统计学一会儿一个纵向数据,一会儿一个随机效应,这种方差结构那种距离量度。从某种程度上来说,随着学科的深化发展,这种现象的出现是不可避免的。关于这个问题,用下面的图示来解释再清楚不过了。

      附:攻读博士的全流程

      以下文字和图片来源于“The Illustrated Guide to a Ph.D.”,遵循CC许可证“署名-非商业”,作者为Matt Might

      Imagine a circle that contains all of human knowledge:

      By the time you finish elementary school, you know a little:

      By the time you finish high school, you know a bit more:

      With a bachelor’s degree, you gain a specialty:

      A master’s degree deepens that specialty:

      Reading research papers takes you to the edge of human knowledge:

      Once you’re at the boundary, you focus:

      You push at the boundary for a few years:

      Until one day, the boundary gives way:

      And, that dent you’ve made is called a Ph.D.:

      Of course, the world looks different to you now:

      So, don’t forget the bigger picture:

      Keep pushing.

      Sweave后传:统计报告中的大规模计算与缓存

      学无止境。我曾以为我明白了如何在Sweave中使用缓存加快计算和图形,但后来发现我并没有真的理解,直到读了另外一些手册才明白,因此本文作为前文“Sweave:打造一个可重复的统计研究流程”之续集,向大家介绍一下如何在Sweave的计算和图形中使用缓存,以节省不必要的重复计算和作图,让那些涉及到密集型计算的用户不再对Sweave感到难堪。

      如果你还没读前文,建议先从那里开始读,了解Sweave与“可重复的统计研究”的意义。简言之,Sweave是一种从代码(R代码和LaTeX)一步生成报告的工具,我们可以把整个统计分析流程融入这个工具,让我们的报告具有可重复性。然而,就普通的Sweave而言,这样做的一个明显问题就是,所有计算和作图都被融入一个文档之后,每次运行这个文档都要重复所有的计算和作图,这在很多情况下纯粹是浪费时间;比如,我只想对新添加的部分内容运行计算,而文档中的旧内容希望保持不变。这都是很合理的需求,我们需要的实际上就是一种缓存机制,将不想重复计算的对象缓存起来,需要它的时候再从缓存库中直接调出来用。

      Sweave本身做不到缓存(或者很难做到),但由于R语言的扩展性以及一些疯子和天才的存在,我们可以通过R的一些附加包来完成缓存。这里的缓存有两种(不关心细节的读者可以略过):

      • 一种是对R对象的缓存,比如数据、模型(拟合好的模型)和其它计算结果等,这个缓存容易理解,我们可以把它简单想象为把计算过程中的对象存入某些特殊的缓存库(文件),下次计算的时候先查看缓存库中是否存在我们需要的对象,如果存在,那么就直接加载进来,否则就重新计算;如(伪代码):
        ## 如果存在,就加载
        if (file.exists('x.RData')) {
            load('x.RData')
        }
        ## 如果当前工作环境中不存在x,那么就重新创建它
        if (!exists('x')) {
            x = rnorm(100000)
        }

        R对象的缓存可以通过cacheSweave包(作者Roger D. Peng)实现,它真正采用的方法比上面的伪代码当然要高级(比如用代码的MD5值作数据库名等),不过用户可以不必关心这其中的细节。这种“当需要时才加载”的方式在术语上叫“延迟加载”,即Lazy Load,这在R里面也很常见,比如很多R包的数据都是采用延迟加载的方式(加载包的时候并不立刻加载数据,而是用data(name)在需要的时候加载)。

      • 另一种是图形的缓存,这是一个不可思议的魔法。cacheSweave的帮助文档和vignette中特别提到了它只能缓存R对象,无法缓存图形和其它附属输出,而开源世界的魅力就是你总能找到一些奇妙的解决方案。R包pgfSweave借力于pgf实现了图形的缓存。pgfSweave包的两位作者Cameron Bracken和Charlie Sharpsteen都是水文学相关专业出身,却在R的世界里做了这些奇妙的工作,让我觉得有点意外,这是题外话。pgf是LaTeX世界的又一项重大发明,它提供了一套全新的绘图方式(语言),而且它的图形可以通过LaTeX直接生成PDF输出(这是pgfSweave能实现缓存的关键)。关于pgf,我们可以翻一翻它726页的手册,如果你能坚持看上10分钟,感叹超过50次,那么你一定是一位超级排版爱好者。它的图形质量之高、输出之漂亮,真的是让人(至少让我)叹为观止。同样,这里我也不详细介绍细节。pgf图形的“缓存”是通过“外置化”(externalization)来实现的,简言之,pgf图形有一种输出形式如下:
        \beginpgfgraphicnamed{graph-output-1}
        \input{graph-output-1.tikz}
        \endpgfgraphicnamed
        

        如果LaTeX在运行过程中遇到这样的代码段,那么它会把tikz图形(tikz是高层实现,pgf是基础设施)先编译为PDF图形,等下次重复运行LaTeX的时候,这段代码就不必重新运行了,LaTeX会跳过它直接引入PDF图形。这样就省去了编译tikz图形的时间,同时也能得到高质量的图形输出。

        pgfSweave从R的层面上可以输出这样的代码段,并生成一个shell脚本来编译图形为PDF;而pgfSweave也会尝试根据R代码的MD5来判断一段画图的代码是否需要重复运行——如果R代码没有变化,那么图形就不必重制。这是自然而然的“缓存”。对了,pgfSweave对Sweave的图形输出作了扩展,新增了tikz输出,这是通过另一个R包tikzDevice实现的(同样两个作者),这个包可以将R图形录制为tikz绘图代码。pgfSweave依赖于cacheSweave包,把它缓存R对象的机制也引入进来,所以我们可以不必管cacheSweave,把精力放在pgfSweave上就可以了。

      介绍完血腥的细节之后,我们就可以坐享高科技的便利了。下面我以一个统计计算中的经典算法“Gibbs抽样”为例来展示pgfSweave在密集型计算中的缓存功能。

      Gibbs抽样的最终目的是从一个多维分布f(X_1,X_2,\ldots,X_n)中生成随机数,而已知的是所有的条件分布f(X_1|X_2,\ldots,X_n)f(X_2|X_1,\ldots,X_n)、……、f(X_n|X_1,\ldots,X_{n-1}),并且假设我们可以很方便从这些条件分布中抽样。剩下的事情就很简单了:任意给定初始值x_1^{(0)},x_2^{(0)},\ldots,x_n^{(0)},括号中的数字表示迭代次数,接下来依次迭代,随机生成x_1^{(k)} \sim f(X_1|x_2^{(k-1)},\ldots,X_n^{(k-1)})x_2^{(k)} \sim f(X_2|x_1^{(k)},\ldots,x_n^{(k-1)})、……、x_n^{(k)} \sim f(X_n|x_1^{(k)},\ldots,x_{n-1}^{(k)})(迭代次数k=1,\cdots,N),也就是,根据上一次的迭代结果并已知条件分布的情况下,从条件分布中生成下一次的随机数(这个算法很简单,你什么时候看明白上标下标了就明白了)。这是个串行的过程,所以无法避免循环。

      接下来的计算没有那么复杂,我们仅仅用一个二维正态分布为例。当我们知道一个二维正态分布的分布函数时,我们也知道它的两个边际分布都是一维正态分布,而且分布的表达式也很明确,后文我会详细提到,读者也可以参考维基百科页面。生成一维正态分布的随机数对我们来说当然是小菜一碟——直接用rnorm()就可以了。这样,我们只需要交替从f(X|Y)f(Y|X)生成随机数,最终我们就能得到服从二维联合分布的随机数(对)。假设我们要从下面的二维正态分布生成随机数:

       \left[\begin{array}{c}X\\ Y\end{array}\right]\sim\mathcal{N}\left(\left[\begin{array}{c}0\\ 1\end{array}\right],\left[\begin{array}{cc} 4 & 4.2\\ 4.2 & 9\end{array}\right]\right)

      详细过程和结果参见下面这份PDF文档(点击下载):

      A Simple Demo on Caching R Objects and Graphics with pgfSweave (PDF)

      二维正态分布随机数及其等高线图

      二维正态分布随机数及其等高线图

      我们生成了50万行随机数,并画了X与Y的散点图。由于我们设定了相关系数为0.7,所以图中自然而然显现出正相关;而等高线也体现出多维正态分布的“椭球形”特征。均值在(0, 1)附近,都和理论分布吻合。所以这个Gibbs抽样还不太糟糕。

      生成上面的PDF文档和图形的LyX/Sweave源文档在这里:

      A Simple Demo on Caching R Objects and Graphics with pgfSweave (LyX)

      如果你已经按照我前面的文章配置好你的工具(即使当时配置过,现在也需要重新配置,因为我最近作了重大修改),这个文档应该可以让你重新生成我的结果。文档中有两处关键选项:

      • cache=TRUE 使得文档中的R对象可以被缓存;
      • external=TRUE 使得图形可以被缓存;

      第一次运行文档时,也许需要等待一分钟左右的时间,因为R需要长时间的Gibbs抽样计算,并编译生成的图形为PDF,但如果下一次再运行这个文档,则速度会快很多,因为不需要再计算任何东西,只需从缓存加载就可以了。我们前一次的抽样结果已经在缓存库中了。

      本文至此可以结束,但真正想享用这个结果的读者可能还要继续看几个问题:

      1. pgfSweave对图形的缓存经常有点过度,即使你改动了代码,缓存仍然不会被替换,这种情况下你可以把生成的tikz和pdf图形删掉;
      2. 中文问题:tikzDevice包好像还没有完全解决中文问题,作者正在尝试我提供的中文图形,也许在未来不久的新版本中会有完全的中文支持,但目前实际上我已经在我的自动配置脚本中解决了这个问题。我们只需要保证我们的中文LyX文档用UTF-8编码就可以了——在文档选项中使用CTeX类(以及UTF8选项),并设置语言为中文、编码为Unicode (XeLaTeX) (utf8)。这种设置至少我在Windows和MikTeX下可以成功编译中文Sweave文档。目前高亮代码功能在中文文档中还不能用,但我已经向相关作者发了补丁,等CRAN管理员回来发布新版parser包之后这个问题也会解决。所以,中文用户也不必担心,一切都(将)可以使用。
      3. 依旧是样式问题:细心的读者也许能发现上面的PDF文档中的图形和这里网页中的图形略有区别,PDF文档中图形的字体和正文字体是一致的,这也是pgf的优势,前文第5节曾提到过。
      4. 如果使用缓存,那么代码段中的对象都无法打印出来,即使用print(dat)也无法打印dat,这是因为缓存的代码段只管是否加载对象,而无法执行进一步的操作(包括打印),对于这种情况,我们应该设计好我们的Sweave文档结构,让某些代码段作计算(并缓存),某些代码段作输出(不要缓存)。

      这两篇文章都有点长,但最终的操作都极其简单。如果能理解这套工具,我想无论是写作业还是写报告,都将事半功倍(比如我就一直用它写作业)。

      祝大家在新的一年能写出漂亮、利索的统计文档,告别复制粘贴的体力劳动。