自动化报告人类和计算机有各自擅长的东西,人擅长下指令,计算机擅长执行指令,而且对计算机来说,一个任务做一遍或是做一百遍可能只有时间上的区别,但一个人要是同一个任务重复做一百遍可能就抓狂了,而且容易出错。先跑个题,我想起来有个短片叫《What is that》(那是什么),讲的是一对父子和一只麻雀的故事,网上一搜就可以找到;当然,它的主题并不是关于人不能忍受重复劳动,我只是跑题而已。 另一方面,人还有最大的一个特征就是懒惰;懒没什么错,看怎么个懒法。有人纯粹是混日子的懒,有人是为了更高效率工作而走捷径。用R做自动化报告就是为了提高效率和保证结果可重复。 历史在R的世界里,凡是提到自动化报告,很多人就会想到Sweave,它已经诞生十几年了。它的主要设计思想来自于文学化编程(Literate Programming),这是Knuth大神提出来的一种编程范式,它与传统的结构化编程不同。结构化编程就是写那些循环(for/while)、选择分支(if else)、函数模块之类的代码,让计算机去按设定好的程序结构去执行,而文学化编程则是把代码嵌入所谓的文学作品中,之所以说所谓的,是因为这里的文学不一定真的是那种常规意义下的文学,只是指人类语言而已,相对计算机代码而言。文学化编程的思想很简单:代码和正文混和在同一个文档中,编译的时候既可以把代码抽出来运行得到结果,也可以把正文抽出来形成软件文档。最初它是为了写软件而设计的,这种设计方式的优势显而易见:代码和文档在一起,方便互相更新和照应。比如修改了代码之后可以很快也更新相应的文档段落,而不必像传统方式那样,从源代码文件跳到文档文件中去更新(人的记忆不可靠,这事儿经常忘记,造成代码和文档不一致)。 要使用文学化编程,必须得有一些设定的规则来标记哪些是代码,哪些是正文,否则这事儿没法进行。最早的语法是这样:用
这一块文字中包含两段正文和一段代码。编译它的时候,计算机根据前面的规则就知道 文学化编程最早和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也有几重考虑:
这就是一个包名背后的各种阴险考虑。当一名忍者不容易啊,你得周密布置陷阱,让别人乖乖掉进你的坑。 knitr包你要是没学习过Sweave,最好别去花那时间,可以直接跳入knitr世界,它兼容Sweave并提供了无限的扩展性,这本书就是用它基于Markdown写的。先举一个hello world例子吧: 1 + 2
dnorm(0) # 标准正态分布在0处的密度值
summary(lm(dist ~ speed, data = cars)) # 一个回归
上面你看到的是R的输出,其实它的源文件只有5行,1行标记代码开始,3行R代码,1行标记正文开始,如下所示:
我们用knitr编译这段代码,就得到了上面的输出。现在,你应该对自动化报告有一个初步了解了。Knitr的网站(http:///knitr)中有详尽的英文文档和示例,英文方面没有障碍的忍者可以随时查阅。为了新忍入门更快,我在这里把整个故事的梗概叙述一遍,掌握了基本概念之后,再去网站里查阅细节会更容易。 语法为了knitr能识别文档中的R代码,我们必须对代码文本有特殊标记,前面说的 LaTeX文档(扩展名 回顾一下,以上提到几个重要概念:
选项让我们可以非常灵活地控制代码的输出,例如如果我们想隐藏R代码,只显示运行结果(你给老板交报告的时候当然不能连R代码也显示,除非老板也是代码控),那么我们可以用 由于LaTeX文档入门门槛高,而网页则相对容易一些,knitr在设计之初就考虑了网页格式,它有两种可能:一是原始HTML格式,即把R代码嵌入HTML代码;二是Markdown(下文简称MD),它是非常轻量级的标记语言,可以很方便翻译为HTML语言。我一般倾向于用后者。MD的出现是为了简化HTML,把常用的HTML标签用极度简化的语法写出来,这一点值得程序设计者学习:你可以写一个无所不能但繁琐的程序,也可以写能实现常用功能但简单的程序。R就有前者的特征,尤其是很多函数有长串的参数,看着就让人发蒙,而实际上只有少数参数是常用的,当然,这一点上不必吐槽,因为R是一门基础语言,功能优先。这是题外话。 HTML文档混合R代码的语法为:以
MD文档语法为:以三个反引号和一对大括号开始R代码,以三个反引号开始正文,上面的hello world例子已经显示了代码段的基本结构。行内代码放在
文本输出在文本输出方面,knitr包支持以下功能:
简单举两个例子,注意它们的源代码完全相同,但因为代码段选项不同,所以输出也有所不同: # 不重排代码:tidy=FALSE, warning=TRUE
fib=function(n){if(n<2)return(n);fib(n-1)+fib(n-2)}
1:3+1:2
# 重排代码并隐藏警告信息:warning=FALSE
fib = function(n) {
if (n < 2)
return(n)
fib(n - 1) + fib(n - 2)
}
1:3 + 1:2
默认情况下,文本输出会被加上前缀 表格实际上也是纯文本构成的(你要是天天抱着Word用当然永远都不能明白这句话!),但R没有自带的表格生成函数,所以我们往往需要特殊处理。视输出格式不同,我们可以使用xtable或ascii包来把R对象(尤其是数据框)转化为相应格式的表格代码,此时需要我们使用原样输出,如: library(xtable)
xtable(head(mtcars[, 1:5]))
原样输出( cat('\\includegraphics{foobar}')
来插入一幅文件名叫 图形控制图形是居家旅行数据分析必备之良药,当然得精雕细琢,我们介绍几个重要选项:
在Sweave中我们需要设置选项告诉它R代码会有图形输出,但在knitr世界一切都是自然而然的,你不必告诉我是否有图形输出,我有魔法判断你的代码是否产生了图形(高级忍者请研究 关于图形的更多介绍,参见knitr的图形手册:https://github.com/downloads/yihui/knitr/knitr-graphics.pdf,里面有详尽的示例,读者可以对照源代码学习如何输出精美的图片。 缓存自动化报告不仅仅可以用在小打小闹的计算分析上,也可以用于大规模计算,而这种情况下马上就有一个问题来了:速度。如果文档中含有超负荷的R代码,计算非常耗时,那么就不适合每次从头跑起,尽管一切可以自动化,但也不能等一个报告等得花都谢了。 什么情况下我们不需要重复运行一段代码?一个直接的想法就是,如果上次和这次运行这个文档时,这段代码没有做过任何修改(哪怕是一个空格的增删),那么应该就可以跳过它,直接加载上次运行的结果进来。这就是缓存的基本原理,而具体操作起来还有一些细节考虑,除了代码不能变之外,代码段的选项也不能变,否则输出可能会变化(比如从 我们可以用选项 定制灵活的API是knitr设计的一大亮点,它的可定制性体现在两类钩子(hook)函数上:代码段钩子(chunk hooks)和输出钩子(output hooks)。钩子这个名字听起来很怪,不过其实它就是一个特殊函数而已,在某些情况下会被触发执行。 代码段钩子对应着自定义的代码段选项,也就是所有默认选项之外的选项,注意knitr的代码段选项名称没有限制,你可以写任意合法取值的选项。代码段钩子函数可以通过 knit_hooks$set(par = function(before, options, envir) {
# 运行代码前先设置图形边距参数
if (before)
par(mar = c(4, 4, 0.1, 0.1))
})
代码段钩子有固定的格式,它是一个有三个参数的函数,其中 假设我们新造的一个选项叫
注意钩子函数被触发的条件是相应的选项取值非空,所以这里 输出钩子用来装裱输出,knitr的透明性也体现在这一类钩子上,它可以把R的各类输出都交给用户,让用户决定怎么处理这些输出。所有可能的输出有:源代码、普通文本、警告消息、普通消息、错误消息和图形。每一种有一个对应的钩子函数,这些函数接收R的输出,以一定的形式包装它们,再返回输出来。以源代码为例,它的钩子名为
那么在输出的时候所有R源代码都会被放在 由于我们可以自定义输出的格式,我们就可以任意装潢输出的外观,例如我们可以把错误消息放在某个红色粗体环境中,把警告信息以斜体显示,等等。这个包已经自带了一系列预先定义好的钩子函数,所以除非有特殊需要,通常不需要重定义输出钩子函数。 回到最开始的话题,Sweave的设计绑定了TeX,也就是它的输出只能装在TeX环境中,所以很难移植到别的格式,一直以来,人们扩展Sweave的方式就是把那七八百行代码复制一遍,然后把里面定义死的输出修改为另一种输出,这是糟糕透顶的扩展方式,因为也许下个月R就修改了源代码,但扩展者可能就跟不上官方的更改了;pgfSweave和cacheSweave以及一系列基于Sweave扩展的附加包就这样被Sweave带进了一个大坑,我就是目睹了这个坑爹的过程。程序的扩展性在设计初期一定要考虑清楚,但很多情况下,我们内心总是被一个微小的声音不断规劝:想那么多干什么,搞定这件事就好了!为了搞定一件事而失去推广性,这是开发者的大悲剧。 至此,我们知道了如何把R代码混入文档,如何标记R代码,有哪些基本选项,如何输出图形,使用缓存使文档编译加速以及定制钩子函数。下面我们介绍两套方便的编辑器,让knitr的操作更方便。 编辑器由于R世界里悠久的LaTeX传统,过去人们花费了很大精力在融合LaTeX和Sweave上,所以很多支持R的编辑器都支持Sweave。前面说了,TeX文档写起来很痛苦,尽管输出质量的确无可匹敌。我们先介绍一款TeX克星LyX(http://www./),再说一款R编辑器新秀RStudio(http://www./);当然,我推广的软件只有开源软件。 LyX这事儿我说破嘴皮子了,但还得说。有经验的TeX用户不用LyX的话,就是自杀。不管你们信不信,反正我是信了。LyX提供了完美的TeX可视化界面,而背后就是纯粹的TeX代码。你看到的是加粗放大的章节标题,而不是平凡无奇的一个命令 这就是我对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在环境变量 显然,后来这个故事有了新发展,随着我对Sweave的日益不满,我写完knitr包之后也顺便给LyX新增了一个knitr模块。在LyX文档设置中,选入这个模块,在编译的时候LyX会先调用R和knitr包处理文档,再交回给LyX去编译为PDF。 在LyX中输入R代码可以用快捷键
事实上knitr包的大多数PDF手册都是用LyX写的,读者可以在这里找到它们: system.file("examples", package = "knitr")
注意knitr的支持从LyX 2.0.3才开始,所以如果你用的是更旧的版本的话,会无法打开这些例子。 RStudioRStudio没有LyX那样的可视化界面,但它作为R世界的编辑器后起之秀,也有很多神奇的新功能。当RStudio开发者看到knitr发布之后,他们立马决定要加入相应的支持,这事儿让双方都感到很激动,因为我们都看到了一些让过去苦逼的Sweave用户掉下巴的前景。 RStudio本来是一个R代码编辑器,它的界面有足够的现代风,而且这个界面竟然同时有桌面版和服务器版,后者的出现着实让人吃惊了一番,想一下,在某个云端运行着RStudio,你可以通过浏览器直接在里面使用R,只要服务器不关机,这个R永不掉线。 刚开始的时候,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提出了一个叫 另一项重大突破是它支持Markdown,当然,这是在我的怂恿加拐骗下他们实现的。因为knitr原生支持MD,我又钟爱MD的简单语法及其超强变身能力,所以我大力推荐他们也支持MD。于是我们可以在RStudio中写MD混合R代码,一键编译为HTML文件。这让万千TeX门外的用户也可以步入自动化报告和可重复研究的神圣殿堂了。对此我满心激动,因为我再也不用为了推广可重复研究而先花大量时间教人TeX。 其它包前面提到了pgfSweave和cacheSweave包,它们的功能已经被knitr重写了,所以个人认为没有必要再去研究。还有一串被Sweave带到坑里的附加包,可能都没必要去学,这一串包的列表参见本包首页:http:///knitr。 文学化编程这件事情当然不是只有R在做,很多其它语言一样有相应的模块,但R有无敌的统计计算和作图能力,所以数据分析报告方面它还是有很大优势的。说实话,我的观察也有限,但我注意到一个Python包做得很不错,叫Dexy(看这位姐姐多会给自己的软件取名字):http://www./。推荐Python爱好者深入研究一下。 本章的自动化报告只是介绍了技术层面的东西,具体报告里写什么则是作者的事情,一旦报告的源文档写好了,将来维护就方便了,有了新数据或要重复跑任务就再也不怕不怕啦。对科学研究而言,它也是保证结果可重复的手段之一,因为报告是通过运行代码直接得到的,没有人工干预,所以一定程度上比那种复制粘贴写报告的方式安全可靠。我的最后一句话是,其实可重复的科学研究挺难的,技术上有少量难度,更多是人们的习惯太难改,他就是要复制粘贴结果到Word里,你有什么辙? |
|