第5章 自动化报告

人类和计算机有各自擅长的东西,人擅长下指令,计算机擅长执行指令,而且对计算机来说,一个任务做一遍或是做一百遍可能只有时间上的区别,但一个人要是同一个任务重复做一百遍可能就抓狂了,而且容易出错。先跑个题,我想起来有个短片叫《What is that》(那是什么),讲的是一对父子和一只麻雀的故事,网上一就可以找到;当然,它的主题并不是关于人不能忍受重复劳动,我只是跑题而已。

另一方面,人还有最大的一个特征就是懒惰;懒没什么错,看怎么个懒法。有人纯粹是混日子的懒,有人是为了更高效率工作而走捷径。用R做自动化报告就是为了提高效率和保证结果可重复。

5.1 历史

在R的世界里,凡是提到自动化报告,很多人就会想到Sweave,它已经诞生十几年了。它的主要设计思想来自于文学化编程(Literate Programming),这是Knuth大神提出来的一种编程范式,它与传统的结构化编程不同。结构化编程就是写那些循环(for/while)、选择分支(if else)、函数模块之类的代码,让计算机去按设定好的程序结构去执行,而文学化编程则是把代码嵌入所谓的文学作品中,之所以说所谓的,是因为这里的文学不一定真的是那种常规意义下的文学,只是指人类语言而已,相对计算机代码而言。文学化编程的思想很简单:代码和正文混和在同一个文档中,编译的时候既可以把代码抽出来运行得到结果,也可以把正文抽出来形成软件文档。最初它是为了写软件而设计的,这种设计方式的优势显而易见:代码和文档在一起,方便互相更新和照应。比如修改了代码之后可以很快也更新相应的文档段落,而不必像传统方式那样,从源代码文件跳到文档文件中去更新(人的记忆不可靠,这事儿经常忘记,造成代码和文档不一致)。

要使用文学化编程,必须得有一些设定的规则来标记哪些是代码,哪些是正文,否则这事儿没法进行。最早的语法是这样:用<<>>=来标记代码的开始,用@标记正文的开始,凡是遇到这两类符号,也就意味着要换频道了,下文要标记为代码或正文。例如,

@
hello, I will do 1 + 2 next:

<<foo>>=
1 + 2
@

OK, I'm done now.

这一块文字中包含两段正文和一段代码。编译它的时候,计算机根据前面的规则就知道<<>>=下面的是可以运算的代码,而@下面是正文,不能当作代码运行。这就是文学化编程的基本思想,它可以很容易移植到自动化报告中来,下面我们再详细说里面的细节问题。Sweave借用了这个思想,把R代码嵌入报告中,编译报告的时候R代码被执行,源文档中的R代码在输出的时候被替换为相应的运行结果,这些结果和原来的报告正文混合起来就形成了一篇有结果的报告。这样,我们只需要维护包含源代码的源文档,让结果文档自动生成,而不要手工运行代码并复制粘贴结果到文档里,这样做既累人又容易出错。记住,只有源代码是可以信赖的。注意我并不是说它的结果一定是对的,或者源代码一定是对的,代码当然可能是错的,但源代码要是错了我们可以检查出来,而要是人工操作哪里出了岔子就很难查错,比如你本来应该点这个按钮,结果你当时点了另一个,如果没有完整的屏幕录像,恐怕追溯结果的来源时就比较困难了。源代码通常是文本文件,可以放入版本控制如GIT或SVN,记录完整的修改历史。附带说一句,版本控制(Version Control)工具是忍者必备工具,后面会用专门用一章讲,你要是不会这东西的话,别跟别人说你看过这本书。

文学化编程最早和TeX结合在一起,因为文档用TeX写漂亮嘛,但这事儿跟Knuth肯定也脱不了关系,因为这位大神就是TeX系统的作者,这是计算机世界的佳话(老人家当年不满意出版社的排版质量,一举自己写了一套高质量排版系统,并写支票奖励发现缺陷的人,奖金都是16进制的1美元,而且金额随缺陷数目递增,我跑远了,各位要是没听说过这些轶事自己搜吧)。于是,计算机代码用某种语言写,比如C语言,而文档用TeX写;要源代码可以抽代码,要文档可以抽文档,皆大欢喜。

Sweave的诞生也跟TeX绑在一起,这就为它后来的应用埋下了悲剧的种子,因为TeX不是一般人能精通的。我用了八年LaTeX,自认为对它还比较熟,但仅限于使用,要是让我去读那些LaTeX包的源代码,我几乎读不懂,太庞大太复杂了。Sweave的设计里处处是硬编码,所以它很难扩展,一直以来只能被框在TeX世界里,曲高和寡。尽管Knuth大人弄出来这样一个牛轰轰的想法,Sweave基本上也把它实现了,但这东西太难推广了。初学者编译TeX文档难免遇到一堆看不懂的错误,进而气馁,最后疏远它。我用了几年Sweave,在这方面也做了很多工作,想让它变得易用一些,比如开发了LyX模块,让用户可以在LyX里面点按钮就可以直接编译得到PDF文件。即便如此,Sweave的深层问题无法解决,很多简单的问题我等了又等(比如设置图片在TeX文档中的宽度),一直没有等到答案,屡屡想重写它,但忍者的基本素质就是忍,没事儿不要重新发明一个东西。2011年底我终于忍不住了,操起键盘重写了一个新包,叫knitr。在接下来的文字里,各位看官可能会觉得我对Sweave有所不敬,所以我得先声明一下,软件本身的质量和写开源软件的精神是两码事,我绝对尊重开源软件贡献者,这一章所有关于Sweave的吐槽都仅限于它的设计和功能,对代码不对人。

因为这里是谈历史,不妨写一个小插曲:knitr这个包的名字是怎么来的呢?Sweave实际上是由S(代表S语言,也就是R语言它娘亲)和weave(编织)组成的,Weave是文学化编程的概念,就是把文字和代码编织到一起。我在考虑包的名字的时候,由于满心要对Sweave的各种不利索吐槽,所以我决定给我的新包一个利索的名字,英语里说利索通常用neat这个词,而同时编织还有另一个词叫knit,二者发音相近,用它取名可谓一石二鸟,音意两全。knit后面加上字母r也有几重考虑:

  • R代表R语言,为什么小写?因为小写对用户来说输入方便,不用按Shift
  • knitr看起来和读起来像knitter,谐音neater,充分表达了某种要凌驾于对手之上的情绪,说得不好听就是自恋
  • knitr不是一个正常的英文单词,所以Google搜索的第一条确定一定以及肯定会是我的网站,方便用户搜到文档,在各种社交网络上它也可以作为标签,比如#knitr(微博、Twitter)

这就是一个包名背后的各种阴险考虑。当一名忍者不容易啊,你得周密布置陷阱,让别人乖乖掉进你的坑。

5.2 knitr包

你要是没学习过Sweave,最好别去花那时间,可以直接跳入knitr世界,它兼容Sweave并提供了无限的扩展性,这本书就是用它基于Markdown写的。先举一个hello world例子吧:

1 + 2
## [1] 3
dnorm(0)  # 标准正态分布在0处的密度值
## [1] 0.3989
summary(lm(dist ~ speed, data = cars))  # 一个回归
## 
## Call:
## lm(formula = dist ~ speed, data = cars)
## 
## Residuals:
##    Min     1Q Median     3Q    Max 
## -29.07  -9.53  -2.27   9.21  43.20 
## 
## Coefficients:
##             Estimate Std. Error t value Pr(>|t|)    
## (Intercept)  -17.579      6.758   -2.60    0.012 *  
## speed          3.932      0.416    9.46  1.5e-12 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 15.4 on 48 degrees of freedom
## Multiple R-squared:  0.651,  Adjusted R-squared:  0.644 
## F-statistic: 89.6 on 1 and 48 DF,  p-value: 1.49e-12

上面你看到的是R的输出,其实它的源文件只有5行,1行标记代码开始,3行R代码,1行标记正文开始,如下所示:

```{r knitr-hello}
1 + 2
dnorm(0) # 标准正态分布在0处的密度值
summary(lm(dist ~ speed, data=cars)) # 一个回归
```

我们用knitr编译这段代码,就得到了上面的输出。现在,你应该对自动化报告有一个初步了解了。Knitr的网站(http://yihui.name/knitr)中有详尽的英文文档和示例,英文方面没有障碍的忍者可以随时查阅。为了新忍入门更快,我在这里把整个故事的梗概叙述一遍,掌握了基本概念之后,再去网站里查阅细节会更容易。

5.2.1 语法

为了knitr能识别文档中的R代码,我们必须对代码文本有特殊标记,前面说的<<>>=就是一种可能的标记。同时,我们还可以设置一些运行代码的选项,这些软件方面的选项不应该是正文的一部分,所以也要用特殊标记保护起来。所有这些标记的规定,就是我们要谈的语法。对knitr而言,不同格式的文档有不同语法。

LaTeX文档(扩展名Rnw)里面仍然沿用历史规定,代码放在<<>>=之下,<<前面最多只允许有空格字符而不能有任何其它字符(一般情况下它前面什么都没有);在<<>>之间我们可以对下面的代码设置一些运行选项,例如<<foo, echo=TRUE, eval=TRUE>>=,其中第一个选项没有取值,它是这段代码的标签,这个选项是代码段的唯一标识符,所以不同代码段必须用不同的标签,它主要被用来写图片文件名(如果下面的代码会生成图片的话)以及缓存文件名,如果标签为空,那么knitr会自动生成形如unnamed-chunk-1unnamed-chunk-2之类的标签;所有自带选项参见http://yihui.name/knitr/options,选项列表很长,因为可以控制的细节非常多。@字符用来标记下面要开始正文了,它同样需要出现在一行的顶头或用空格缩进,后面可选择性跟着LaTeX注释(以%开头)。在<<>>=里面设置的代码段选项是局部选项,仅仅对当前代码段有效;我们也可以通过R对象opts_chunk设置全局选项,如:

opts_chunk$set(echo = FALSE, fig.height = 4)

它可以放在任意R代码段中(通常在一篇文档的第一个代码段中),从这句话出现开始,后面的代码段都会默认使用这里设置的选项,但注意局部选项优先于全局选项,例如,我们全局设置了echo选项为FALSE,但对一个代码段<<echo=TRUE>>=来说,它所用的echo值仍然为TRUE,相比之下,代码段<<>>=用的就是FALSE(从全局选项继承过来的)。初学者可以暂时忽略所有选项的设置,因为knitr包在设计的时候已经花了很多精力给所有的选项设置合适的默认值了。

除了代码段中可以放R代码之外,还有另一种形式的R代码称为行内代码,顾名思义,它是嵌在正文段落文字内的代码,通常比较简短,用来输出单个值,语法为\Sexpr{x},其中x代表当前环境下的变量x,这个标记会被运行并返回值嵌入原来的文本,它也是一个有用的应用,例如我们可以在正文里写“回归结果的斜率为\Sexpr{coef(fit)[2]}”,当我们在前面建了一个回归模型之后(假定名为fit),这句话编译之后就会变成含有真实数字的文字,如“回归结果的斜率为3.932”。

回顾一下,以上提到几个重要概念:

  • 代码段,就是独立段落的R代码,每段代码必须有唯一的标签
  • 行内代码,嵌入文字的小代码
  • 局部选项,在代码段上方的<<>>=标记里设置
  • 全局选项,用opts_chunk$set()设置,它对所有文档格式都通用(包括Rnw以及后面要介绍的Rhtml、Rmd等)

选项让我们可以非常灵活地控制代码的输出,例如如果我们想隐藏R代码,只显示运行结果(你给老板交报告的时候当然不能连R代码也显示,除非老板也是代码控),那么我们可以用echo选项,设置echo=FALSE就可以隐藏R代码;如果我们想让输出图形的宽度为5英寸,则可以设置fig.width=5,等等。

由于LaTeX文档入门门槛高,而网页则相对容易一些,knitr在设计之初就考虑了网页格式,它有两种可能:一是原始HTML格式,即把R代码嵌入HTML代码;二是Markdown(下文简称MD),它是非常轻量级的标记语言,可以很方便翻译为HTML语言。我一般倾向于用后者。MD的出现是为了简化HTML,把常用的HTML标签用极度简化的语法写出来,这一点值得程序设计者学习:你可以写一个无所不能但繁琐的程序,也可以写能实现常用功能但简单的程序。R就有前者的特征,尤其是很多函数有长串的参数,看着就让人发蒙,而实际上只有少数参数是常用的,当然,这一点上不必吐槽,因为R是一门基础语言,功能优先。这是题外话。

HTML文档混合R代码的语法为:以<!--begin.rcode label, opt=value开始R代码,以end.rcode-->开始正文。行内代码放在<!--rinline -->中。熟悉HTML语法的都知道,<!-- -->是HTML注释的语法。我对Sweave语法不太满意的一点也在此:文学化编程的文档最好能避免破坏原文档的语法,比如要是我来设计Sweave,我肯定不会用<<>>=语法,因为它干扰了TeX文档,我会倾向于把R代码段放在TeX注释中,这样即使不编译,这份文档也是合法的TeX文档。以下是一个简单的HTML例子:

hello, the value of 2 * pi is <!--rinline 2*pi -->

<!--begin.rcode foo-label
rnorm(5)
end.rcode-->

HTML和R代码混合的文档我们称之为R HTML文档,通常扩展名为*.Rhtml

MD文档语法为:以三个反引号和一对大括号开始R代码,以三个反引号开始正文,上面的hello world例子已经显示了代码段的基本结构。行内代码放在`r `之中。以下是一个简单的MD例子:

hello, the value of 2 * pi is `r 2*pi`

```{r foo-label}
rnorm(5)
```

R Markdown文档扩展名通常为*.Rmd

5.2.2 文本输出

在文本输出方面,knitr包支持以下功能:

  • 代码高亮(highlight=TRUE),增强可读性,有无数的高亮主题可选,仅适用于LaTeX和HTML输出,MD文档在转为HTML文档之后可以用专门的JavaScript库去高亮代码
  • 代码重排(tidy=TRUE),对那些不注意代码格式的人来说很有用,再乱的代码,到了这里也会变得相对整齐,本功能由formatR包支持
  • 执行或不执行代码(eval=TRUE/FALSE),不执行的代码段将被跳过,原样输出源代码
  • 显示/隐藏源代码(echo=TRUE/FALSE),甚至精确控制显示哪几段代码(echo取数值)
  • 显示/隐藏普通文本输出或将文本输出以原样形式输出(results='markup', 'hide', 'asis'
  • 显示/隐藏警告文本(warning=TRUE/FALSE)、错误消息(error)和普通消息(message
  • 显示/隐藏整个代码段的输出(include=TRUE/FALSE),比如我们可能想运行代码,但不把结果写入输出中

简单举两个例子,注意它们的源代码完全相同,但因为代码段选项不同,所以输出也有所不同:

# 不重排代码:tidy=FALSE, warning=TRUE
fib=function(n){if(n<2)return(n);fib(n-1)+fib(n-2)}
1:3+1:2
## Warning in 1:3 + 1:2: longer object length is not a multiple of shorter
## object length
## [1] 2 4 4
# 重排代码并隐藏警告信息:warning=FALSE
fib = function(n) {
    if (n < 2) 
        return(n)
    fib(n - 1) + fib(n - 2)
}
1:3 + 1:2
## [1] 2 4 4

默认情况下,文本输出会被加上前缀##,这是考虑到读者可能会复制文中的代码在自己的R中运行,而#是R的注释符,所以输出不会干扰代码的复制和粘贴运行。Sweave没有这个考虑,并且更糟糕的是它给源代码也加上了前缀>+,这样看报告的人想要复制代码就痛苦之极了,因为你必须把这些多余的字符去掉。

表格实际上也是纯文本构成的(你要是天天抱着Word用当然永远都不能明白这句话!),但R没有自带的表格生成函数,所以我们往往需要特殊处理。视输出格式不同,我们可以使用knitr::kable()函数或xtable包或ascii包来把R对象(尤其是数据框)转化为相应格式的表格代码,此时需要我们使用原样输出,如:

knitr::kable(head(mtcars[, 1:5]))
mpg cyl disp hp drat
Mazda RX4 21.0 6 160 110 3.90
Mazda RX4 Wag 21.0 6 160 110 3.90
Datsun 710 22.8 4 108 93 3.85
Hornet 4 Drive 21.4 6 258 110 3.08
Hornet Sportabout 18.7 8 360 175 3.15
Valiant 18.1 6 225 105 2.76

原样输出(results='asis')的含义是这样:默认情况下,输出会被装饰在一些特定的标签内,例如在LaTeX格式输出时,普通文本被放在verbatim环境中;有时候我们希望用R代码直接输出特定格式的文本,比如直接写TeX代码,那么我们可以用cat()函数直接写文本,此时我们希望写出来的文本就直接是TeX代码,而不要被放到verbatim环境中(否则几乎任何TeX代码都会被当作普通文本在TeX中原样输出,TeX的verbatim环境你懂的)。例如在一个原样输出的代码段中,我们可以用

cat('\\includegraphics{foobar}')

来插入一幅文件名叫foobar的图片文件。当然对knitr来说,这种需求几乎不可能存在,只有在Sweave旧社会才会需要这种丑陋的R代码(为什么它是丑陋的代码?)。R的图形控制非常灵活,也是我们马上要介绍的。

5.2.3 图形控制

图形是居家旅行数据分析必备之良药,当然得精雕细琢,我们介绍几个重要选项:

  • fig.path用来设置图形输出的路径,对大型报告来说,我们可能希望把各种乱七八糟的文件归类管理,所以R图形文件可以用这个选项写入单独的文件夹
  • dev设置用哪种图形设备记录图形,自带二十多种常见的图形设备,如PDF、PNG甚至tikz,具体取值参见网站上的文档
  • fig.widthfig.width设置图形文件本身的宽高尺寸(单位英寸)
  • out.widthout.height设置图片在输出文档中的宽高,这是相对Sweave来说的新选项,也是我等了很久没等到,最终刺激我自己写包的原因之一(这两个选项太容易实现了),很多用户看到这两个选项都很欣喜,一个小问题,困扰多少英雄好汉
  • fig.keep设置保留图形的方式,我们可以完全不保留,也可以只保留高层作图函数生成的图形,也可以保留低层作图函数产生的图形
  • fig.show设置图形显示的方式,可以跟在作图代码后面即刻显示出来,也可以等到代码段运行完毕之后再把该段中所有图形一气儿显示出来,也可以把所有图形显示为动画

在Sweave中我们需要设置选项告诉它R代码会有图形输出,但在knitr世界一切都是自然而然的,你不必告诉我是否有图形输出,我有魔法判断你的代码是否产生了图形(高级忍者请研究recordPlot()函数)。在各种图形格式中,tikz格式是最漂亮的,它来自tikzDevice包的贡献(设置dev='tikz'即可),本质上是TeX文件,所以图形编译的时候其中包含的文本都会被当作TeX文档中的元素编译,文档用什么字体图中就继承什么字体,图中要是有TeX数学公式,也会被编出来,看了tikz图片之后,TeX用户就会知道那个demo(plotmath)中的暗黑技巧有多么弱了。

关于图形的更多介绍,参见knitr的图形手册:https://github.com/downloads/yihui/knitr/knitr-graphics.pdf,里面有详尽的示例,读者可以对照源代码学习如何输出精美的图片。

5.2.4 缓存

自动化报告不仅仅可以用在小打小闹的计算分析上,也可以用于大规模计算,而这种情况下马上就有一个问题来了:速度。如果文档中含有超负荷的R代码,计算非常耗时,那么就不适合每次从头跑起,尽管一切可以自动化,但也不能等一个报告等得花都谢了。

什么情况下我们不需要重复运行一段代码?一个直接的想法就是,如果上次和这次运行这个文档时,这段代码没有做过任何修改(哪怕是一个空格的增删),那么应该就可以跳过它,直接加载上次运行的结果进来。这就是缓存的基本原理,而具体操作起来还有一些细节考虑,除了代码不能变之外,代码段的选项也不能变,否则输出可能会变化(比如从echo=TRUE改为echo=FALSE对输出的文本当然有影响,一个含代码一个不含)。你也许会想,代码和代码段的选项都不变的话,应该就可以确定跳过这段代码了吧?其实对严格的程序员来说,这可能仍然不够!比如,上次用R 2.14.2编译,这次用R 2.15.0编译,此时也需要考虑清空旧的缓存,重新计算,避免不同R版本导致结果差异。对knitr而言,要是考虑过于严格,可能会导致不必要的重复计算,所以它基本上只检查代码和选项是否有变化,而这些额外的要求可以由用户定制。一个缓存的代码段会在第一次运行的时候把新创建的对象都写入缓存文件,下次运行的时候从文件中直接加载它们。注意整个代码段的输出也会被以一个特殊对象写入缓存,所以下次加载缓存的时候上次运行的所有输出也会被重新写出来,仿佛代码真的被运行过一样。

我们可以用选项cache=TRUE来启用缓存,相应的cache.path选项用来设置缓存文件的路径。关于缓存还有另外一点技术上的小小说明:R里面有个概念叫延迟加载(lazy load),它对缓存非常有用,也是knitr加载缓存对象时使用的一项重要技术。延迟加载的意思是,从文件中加载R对象,但不立刻把它载入内存,只是在系统中做一个标记,表明这个对象可用,但不到真正用它的时候它不会被真正加载进内存,换句话说,knitr缓存的对象就像随时待命的士兵,只要前线召唤,它们就去投入战斗。

5.2.5 定制

灵活的API是knitr设计的一大亮点,它的可定制性体现在两类钩子(hook)函数上:代码段钩子(chunk hooks)和输出钩子(output hooks)。钩子这个名字听起来很怪,不过其实它就是一个特殊函数而已,在某些情况下会被触发执行。

代码段钩子对应着自定义的代码段选项,也就是所有默认选项之外的选项,注意knitr的代码段选项名称没有限制,你可以写任意合法取值的选项。代码段钩子函数可以通过knit_hooks对象设定。下面举个例子说明新选项和钩子函数如何关联。我们先构造一个新钩子函数叫par

knit_hooks$set(par = function(before, options, envir) {
    # 运行代码前先设置图形边距参数
    if (before) 
        par(mar = c(4, 4, 0.1, 0.1))
})

代码段钩子有固定的格式,它是一个有三个参数的函数,其中before是逻辑值,表示这个钩子在代码段之前执行(TRUE)还是之后执行(FALSE),options是一个列表,装有所有当前代码段的选项,envir是一个环境对象,是当前代码段执行的环境。代码段钩子设置好之后,每当一个代码段被运行前后,knitr都会检查这个代码段是否有一个跟钩子函数同名的选项,如果有且非空,那么就会运行钩子函数。

假设我们新造的一个选项叫par,它不是knitr自带的选项,且跟上面定义的钩子函数同名,那么对下面这个代码段来说,它被执行之前,R会先用par()函数设置图形边距参数,因为这是钩子函数定义要执行的任务:

<<good-margin, par=TRUE>>=
plot(1)
@

注意钩子函数被触发的条件是相应的选项取值非空,所以这里par取值TRUEFALSE123都无所谓。代码段钩子让我们可以把常见的次要任务抽象出来,用一个代码段选项去控制它们的执行。比如上面设定图形边距就是一个非常常见的任务,但要是把这样的代码在每个代码段中的都写一遍的话,就太啰嗦了,而且重复敲代码是大忌!每当你想复制粘贴一段代码的时候都要三思,我真的不需要想办法把这段代码抽象出去吗?

输出钩子用来装裱输出,knitr的透明性也体现在这一类钩子上,它可以把R的各类输出都交给用户,让用户决定怎么处理这些输出。所有可能的输出有:源代码、普通文本、警告消息、普通消息、错误消息和图形。每一种有一个对应的钩子函数,这些函数接收R的输出,以一定的形式包装它们,再返回输出来。以源代码为例,它的钩子名为source,如果我们定义:

<<source-hook>>=
knit_hooks$set(source = function(x, options) {
  paste('\\begin{myEnvironment}', x, '\\end{myEnvironment}')
})
@

那么在输出的时候所有R源代码都会被放在myEnvironment环境中(当然,你得事先定义好这个环境,不然LaTeX会报错)。钩子函数中,x是当前代码段的输出,options是所有选项的一个列表。

由于我们可以自定义输出的格式,我们就可以任意装潢输出的外观,例如我们可以把错误消息放在某个红色粗体环境中,把警告信息以斜体显示,等等。这个包已经自带了一系列预先定义好的钩子函数,所以除非有特殊需要,通常不需要重定义输出钩子函数。

回到最开始的话题,Sweave的设计绑定了TeX,也就是它的输出只能装在TeX环境中,所以很难移植到别的格式,一直以来,人们扩展Sweave的方式就是把那七八百行代码复制一遍,然后把里面定义死的输出修改为另一种输出,这是糟糕透顶的扩展方式,因为也许下个月R就修改了源代码,但扩展者可能就跟不上官方的更改了;pgfSweavecacheSweave以及一系列基于Sweave扩展的附加包就这样被Sweave带进了一个大坑,我就是目睹了这个坑爹的过程。程序的扩展性在设计初期一定要考虑清楚,但很多情况下,我们内心总是被一个微小的声音不断规劝:想那么多干什么,搞定这件事就好了!为了搞定一件事而失去推广性,这是开发者的大悲剧。

至此,我们知道了如何把R代码混入文档,如何标记R代码,有哪些基本选项,如何输出图形,使用缓存使文档编译加速以及定制钩子函数。下面我们介绍两套方便的编辑器,让knitr的操作更方便。

5.3 编辑器

由于R世界里悠久的LaTeX传统,过去人们花费了很大精力在融合LaTeX和Sweave上,所以很多支持R的编辑器都支持Sweave。前面说了,TeX文档写起来很痛苦,尽管输出质量的确无可匹敌。我们先介绍一款TeX克星LyX(http://www.lyx.org),再说一款R编辑器新秀RStudio(http://www.rstudio.org);当然,我推广的软件只有开源软件。

5.3.1 LyX

这事儿我说破嘴皮子了,但还得说。有经验的TeX用户不用LyX的话,就是自杀。不管你们信不信,反正我是信了。LyX提供了完美的TeX可视化界面,而背后就是纯粹的TeX代码。你看到的是加粗放大的章节标题,而不是平凡无奇的一个命令\section{foobar};你看到的是图片,而不是一个命令\includegraphics{foobar};你看到的是真正的数学公式,而不是一堆希腊字母和数学符号在一起开会。重复:它们背后都是纯粹的TeX代码!天堂有路你不走,地狱一堆代码命令你非要往里钻,这不是自杀是什么?LyX有无数的贴心自动化功能,让你即使不记得某些TeX命令,也可以通过点按钮的方式自动生成相应的代码。例如关于geometry包,请问你记得住那若干种边距的代码名称吗?

这就是我对TeX老手的劝告,但对新手而言,必须有艰苦而踏实的TeX学习过程,否则你懂了LyX的人也不懂LyX的心,缺少基本的TeX锻炼,容易把TeX文档写得比Word还难看。LyX中写好文档一键编译PDF,各种细节都给你处理得妥妥儿的,比如参考文献自动用BibTeX编译,你永远都不需要去背“一遍pdflatex,一遍bibtex,再一遍pdflatex,再一遍pdflatex”(懂我在说什么吗?不懂的话你功力不够,需要继续修炼TeX神功)。

2010年我受二导师之邀帮她讲了两节课,主要是介绍Sweave,那时候我已经用LyX + Sweave的组合有一阵子了,但这个组合的配置实在很狗血,需要研究若干暗黑技巧,不过为了上课,我写了一个超长的暗黑R脚本文件,满以为学生运行一遍就可以把各种狗血细节配置好,后来证明大败特败,学生一个个都被整糊涂了。此事让我痛下决心改革LyX对Sweave的支持,于是接下来的一年多LyX新增了Sweave模块,包含在官方发行版中,用户再也不必折腾配置,只需要在LyX文档设置中选入Sweave模块(module),LyX就会调用R和Sweave编译,前提是R在环境变量PATH中。感兴趣的读者请参考手册:https://github.com/downloads/yihui/lyx/sweave.pdf。编译的大致原理是,LyX先导出一份Rnw文件,然后Sweave来处理它,生成含有结果的tex文件,最后交给LyX去编译。

显然,后来这个故事有了新发展,随着我对Sweave的日益不满,我写完knitr包之后也顺便给LyX新增了一个knitr模块。在LyX文档设置中,选入这个模块,在编译的时候LyX会先调用R和knitr包处理文档,再交回给LyX去编译为PDF。

LyX中的knitr模块

LyX中的knitr模块

在LyX中输入R代码可以用快捷键Ctrl + L,然后按前面介绍的LaTeX类语法写(为什么是LaTeX语法?):

<<hello-world, echo=FALSE>>=
print('hello world!')
@

事实上knitr包的大多数PDF手册都是用LyX写的,读者可以在这里找到它们:

system.file("examples", package = "knitr")

注意knitr的支持从LyX 2.0.3才开始,所以如果你用的是更旧的版本的话,会无法打开这些例子。

5.3.2 RStudio

RStudio没有LyX那样的可视化界面,但它作为R世界的编辑器后起之秀,也有很多神奇的新功能。当RStudio开发者看到knitr发布之后,他们立马决定要加入相应的支持,这事儿让双方都感到很激动,因为我们都看到了一些让过去苦逼的Sweave用户掉下巴的前景。

RStudio本来是一个R代码编辑器,它的界面有足够的现代风,而且这个界面竟然同时有桌面版和服务器版,后者的出现着实让人吃惊了一番,想一下,在某个云端运行着RStudio,你可以通过浏览器直接在里面使用R,只要服务器不关机,这个R永不掉线。

RStudio的选项配置界面

RStudio的选项配置界面

RStudio中的Rnw文档,点Compile PDF按钮就可以一键生成PDF

RStudio中的Rnw文档,点Compile PDF按钮就可以一键生成PDF

刚开始的时候,RStudio支持一键通过knitr生成PDF,只要RStudio的选项配置中选的是knitr(你也可以选Sweave作为编译引擎,我就当你是要怀旧好了),并且你的R里面已经安装了knitr包。后来我们发现Sweave其实十多年前就提出了一个有用的概念,但这么多年都没有真正实现过,就是Rnw文档与PDF文档的同步。高级LaTeX用户知道,在LaTeX世界里有tex文件和PDF同步的可能,很多编辑器也支持这种同步,就是可以从tex源文件的某个位置直接跳到PDF文档中相应的页上,反之也可以跳回来,这种导航对查错和对比检查非常有用。对Rnw文档来说,支持这种同步导航就差那么一步,原因是Rnw被编译为tex文档时,两个文档的行号不一定能完全对应,比如Rnw中5行源代码可能会生成20行tex输出,这样两个文档的行号就错开了,即使tex文档支持同步导航,从Rnw跳到PDF或跳回来就跳不准了。Sweave提出了一个叫concordance的选项,用来记录Rnw和tex文件的行号差异,但具体怎么实现导航,就被遗忘在滚滚红尘中了。RStudio把这个旧摊子捡起来,拍拍土,重新开张了,不仅实现了Rnw和PDF之间互跳(Ctrl + 单击),而且实现了错误行号导航,如果编译出错,你可以直接知道是哪段代码出错了。RStudio也实现了代码段选项的自动补全,让文档写起来更快。

另一项重大突破是它支持Markdown,当然,这是在我的怂恿加拐骗下他们实现的。因为knitr原生支持MD,我又钟爱MD的简单语法及其超强变身能力,所以我大力推荐他们也支持MD。于是我们可以在RStudio中写MD混合R代码,一键编译为HTML文件。这让万千TeX门外的用户也可以步入自动化报告和可重复研究的神圣殿堂了。对此我满心激动,因为我再也不用为了推广可重复研究而先花大量时间教人TeX。

5.4 其它包

前面提到了pgfSweavecacheSweave包,它们的功能已经被knitr重写了,所以个人认为没有必要再去研究。还有一串被Sweave带到坑里的附加包,可能都没必要去学,这一串包的列表参见本包首页:http://yihui.name/knitr

文学化编程这件事情当然不是只有R在做,很多其它语言一样有相应的模块,但R有无敌的统计计算和作图能力,所以数据分析报告方面它还是有很大优势的。说实话,我的观察也有限,但我注意到一个Python包做得很不错,叫Dexy(看这位姐姐多会给自己的软件取名字):http://www.dexy.it。推荐Python爱好者深入研究一下。

5.5 网页应用

网页应用在这个时代太重要了。我2012年夏天AT&T实验室实习时接触到一位科学家叫Carlos Scheidegger,他说了一句笑话让我深为同意:如果一件东西在网上不存在,那么它就不存在。更简单的翻译是:我网,故我在。网络媒介有太多传统纸质媒介不具备的特征,例如交互式的内容、视频音频、动画、内容可复制等。想象下面的场景:

你在一本书上学习K-Means算法。首先它介绍了参考文献(如果你要看参考文献,得上图书馆继续找),然后它讲述了基本思想(用文字描述的迭代过程,循环了几遍之后你开始晕乎了),最后给出一个例子(代码打印在纸上,你只能戴上老花镜把它敲进电脑运行)。

与此对应的网页版本是:

你看到超级链接(要看的话,用鼠标戳它即可),迭代过程有一个动画演示(你知道算法一步步是怎样进行的),例子中的代码复制粘贴直接运行。

我的意思不是要全盘否定纸质媒介,只是在某些情况下(尤其是涉及到计算类的),网络媒介更方便;knitr包在开发网络应用方面也下了不少功夫,这里介绍一些相关的资源和演示

  • RPubs.com:你可以看到各式各样的基于R Markdown编译出来的网页报告
  • OpenCPU:它是一个基于R的网络开发平台,例如这里有一个knitr应用(http://public.opencpu.org/apps/knitr/),点点鼠标就可以动态生成一篇报告了
  • CRUNCH:又一个基于R的计算与服务平台,也可以调用knitr生成报告(支持比OpenCPU更好)
  • RCloud:基于Rserve包和knitr的网络应用,提供了一个可以合作编写报告的平台
  • Vistat:基于GitHub/Jekyll的在线小刊物,主要演示统计图形以及相应的代码,背后的引擎仍然是knitr,用简单的Markdown就能生成一个漂亮有用的网站

本章的自动化报告只是介绍了技术层面的东西,具体报告里写什么则是作者的事情,一旦报告的源文档写好了,将来维护就方便了,有了新数据或要重复跑任务就再也不怕不怕啦。对科学研究而言,它也是保证结果可重复的手段之一,因为报告是通过运行代码直接得到的,没有人工干预,所以一定程度上比那种复制粘贴写报告的方式安全可靠。我的最后一句话是,其实可重复的科学研究挺难的,技术上有少量难度,更多是人们的习惯太难改,他就是要复制粘贴结果到Word里,你有什么辙?