3468 字

漫谈 base R 与 ggplot2

这几天心里颇不宁静。纽约的单日新冠确诊数又出了新纪录,工作邮箱里好几封通知群聚感染的邮件,很多还是完全免疫甚至打了加强针的。感觉新变种可以自立山头看作新病毒了,而面世仅一年的疫苗似乎特异性已经不那么乐观了。不过更强的传染性倒不见得有更强的症状,但大概率这病毒是打算跟人类长期共存了,这就有点抗生素与致病菌的剧本了。我理解国内清零政策很大程度是因为放开后医疗系统会崩溃,不过长期清零对于一种不断变异增强传染性的病毒而言是否具有可持续性我想已经在被论证了,同样完全躺平现在看也不是什么好的应对策略,解决问题不能非此即彼,总是要根据实际情况动脑子的。说到这我就来填一个很早就想填的坑,是关于R绘图系统的。

不知道从什么时候开始,tidyverse 跟六角贴纸开始满天飞了,在我看来工具是用来解决问题而不是创造问题的,围绕工具形成的文化现象或类似价值观的东西属于副产品,但推广价值观这类副产品显然要比推广软件容易,因此到一定程度总会出现一些反客为主的声音。我对此不置可否,只是想说工具一定要能解决问题才有意义,在解决问题上,将问题清楚描述出来要比直接问某某工具咋做某某图更重要。

我接触 R 是在十年前,那个时候提到 R 的绘图系统,更多指的是 base R 与 grid/Lattice 这两套系统,前者强调的是最高程度的自定义自由度而后者侧重的则是一个函数给出想要的图形。显然,前者更适合原始意义的绘图比较适合开发者而后者更适合具体应用场景的用户,后者的一个显著优势是默认出图就足够漂亮而显著缺点则是自定义比较费劲。用户从来都是最难伺候的,总有用户既想要最大程度的默认好看又需要一定程度的自定义(说的就是科研狗),此时 ggplot 就出现了。

ggplot 里面的 gg 代表的是图形语法,本来 ggplot 就是 Hadley 理论转实践的尝试,原始的包在08年就不更新了,现在 CRAN 上也没有了,后面 ggplot2 里的那个2就是致敬最原始的版本 ggplot 。ggplot2 最显著的优点就是用图形语法结合了 base R 与 grid 系统的理念,默认足够好看,自定义掌握了一套通用层层叠加语法后也很容易上手。不过,我也得替 grid/Lattice 说句话,他们也是支持自定义的,Lattice 到今天也处于活跃开发状态,并且跟 R 一起发布。不过他们的自定义显然没有上升到价值观层面,跟 base R 的逻辑更像,支持公式化的数据表达,支持对象化操作,也可以通过 update 这个函数来有限调节自定义的细节。不过有句老话说“革命不彻底就是彻底不革命”,用户从来不会过多理会开发历史,基本都是颜狗。ggplot2 也是依赖 grid 包来构建的,包括了所有 Lattice 的优点但学起来更容易,所以很快用户就倒向了这套绘图系统。不过,我们现在常看到的 base R 与 ggplot 的对比很大程度是从可视化结果上来的,但别忘了 ggolot 也是 R 语言的产物,理论上研究清楚了 grid 包也可以搞出类似的东西,真正的区别是用户的使用逻辑。

base R 的使用逻辑更像是白纸画图,从坐标轴到图像都是可以随意自定义的,与之对应的就是需要用户了解一大堆底层命令。base R需要用户对可视化有很具体的了解,例如先用纸笔草图画出雏形,然后通过排列组合基本的作图元素重现在绘图区里。举个最简单的例子,想在条形图上加个误差线,就需要分解为具体的两部分:1)误差线长度与起点坐标,2)然后从起点坐标平行y轴画一条线段,线段末端可以把原有箭头末端里箭头的角度从锐角改成90度垂直。然后我们就能看到误差线了。

data("iris")
mean <- by(iris$Sepal.Length,iris$Species,mean)
sd <- by(iris$Sepal.Length,iris$Species,sd)
temp <- barplot(mean,ylim=c(0,max(mean)+2*max(sd)))
arrows(temp,mean+sd, temp, mean-sd, angle=90, code=3, length=0.1)

这个简单到不能再简单的功能偏巧默认的条形图 barplot 里没有,函数文档里那个误差线又不是平头的,所以很多用户马上认为 base R 做不了。单就这个例子而言,其实条形图本来就不应该有误差线,真正需要表示不确定性范围应该用箱形图或提琴图来展示真实分布,但你要是去读学术论文会发现时至今日还是有大量图用条形图加误差线,有的还要加星号表示显著差异。这些从作图角度而言完全就是缝合怪,但始作俑者后人不断。很多用户只知道最终要看到什么样的图,但完全不关心图背后的统计学意义,然后就到处问哪个函数能做这样的图,追求形似。这是我认为现在还需要学 base R 作图的一个重要原因,那就是用户一定要有把图形元素拆解回原始数据的能力,知道自己再干什么而不是盲目追求炫酷,这太浮躁。

在 ggplot 这套系统里,图片实际被拆解成了三个部分:数据、映射与可视化方式。数据最好是一个数据框,映射解决变量对应在图片上的维度例如哪个是x?哪个是y?哪个分组用颜色/大小/形状来表示,可视化方式就是指定出图的具体形式。其实这个逻辑在 base R 里也是成立的,只不过最后这个不同可视化方式会对应不同函数,然后里面参数一大堆名字还不一样。ggplot则是把这些都规范到一种形式里去了,而且可以通过加号这个函数层层叠加可视化方式,而各自可视化方式内部也可以重新进行坐标映射。此外,ggplot事实上也支持在作图过程中执行一定的计算,例如平滑或者汇总,这类计算都归到stat_系列的函数里了。如果打算自定义某个维度,可以统一用 scale_XXX_manual 来进行修改,这里XXX对应的是你映射的维度,例如 scale_fill_manual 对应的就是自定义填充颜色。注意,这里是映射后的图片里的维度,而映射则一般都是在可视化方式的 aes 里定义的,用来把数据中的某个维度指定到图片里的维度里。此外,ggplot 自然也支持根据分组信息的多图可视化,这里就是facet_grid函数来统一管理,这里 ggplot 的价值观是每一张图都要尽可能清楚展示数据,且一幅图讲一个故事,然后通过坐标对其进行比较。也就是说,ggplot里画双坐标轴这种图就不太推荐,虽然也可以自定义后来个形似,但其实双坐标轴图在可视化方式里面的地位大概跟饼图或3D柱形图差不多,属于人厌狗嫌那个组的。所有的双坐标轴图理论上都可以并应该拆成两幅图来描述两件不同的事,如果两件事相同,那么双坐标轴总可以转为单坐标轴。

从我个人经验而言,如果你掌握了 base R 作图,转到 ggplot 作图非常轻松,会省掉一大堆需要自定义的东西,统一用他们的函数体系来画就行。反之,如果掌握了 ggplot 的语法,学 base R 应该也很轻松,因为base R绘图函数里的映射与可视化方式也都有现成的包或函数,只是可能参数名字乱一些。说 ggplot 对新手友好,一大部分指的是默认美观,还有一部分原因就是从设计上就阻止了很多类似3D柱形图或双坐标轴的存在。但搞笑的是在爆栈网上从来不缺人去问这类图如何在ggplot体系里实现,也不缺大神给出解决方法。因此,我一度认为限制一定的自由度其实也对培养良好的可视化习惯有帮助,但现实却是很多用户既没学到ggplot里的可视化原则,又产生了对 base R 莫名其妙的优越感,一抬手就是管道化教条式的层层叠加代码,一行代码能做到的非学习牛津大学贝利学院优秀毕业生、大英帝国爵级司令勋章获得者、大不列颠及北爱尔兰联合王国内阁常任秘书汉弗莱·阿普比的语言风格去卡形式,这就距离解决问题比较遥远了。

其实 base R 与 ggplot 之争的背后存在一个代码风格统一的问题,如果选 ggplot ,确实代码更容易读,但问题是很多初学者的水平大概就在复制代码改变量名的水平,完全不知道代码的意义,这样层层堆出来的代码甚至会重复定义,也没啥可读性。究其原因,代码风格应该是初学者到了中级水平才应该考虑的问题,初学者最应该了解的是可视化的逻辑,然后结合自己具体的问题去练习并累积经验。不过,很多初学者都是excel打底的,脑子里全是哪个对话框画什么样的图这种思维,这种情况不论是转 base R 还是 ggplot ,他们脑子里的预期都是一个函数出图解决问题,根本不关心图是如何画出来的,这就很容易变成教条化的用户,记住步骤但不知道原因。实话说很多基于ggplot体系的作图包本质上就是把需要用户自定义的部分自己强制定义一遍然后就上线了,这种包完全迎合了某些领域的独特可视化品味,后面跟了一批教条化的用户,这样的默认可用的软件包我觉得并不利于数据分析人员理解自己的数据。

但凡学用编程语言进行可视化,起码是要知道自己在做什么的。看到一张漂亮的图,可以尝试分析图片元素,然后尝试自己将其组合起来。有这种想法后,base R 也好,ggplot 也好,学明白一个就基本也会另一个了,甚至说迁移到 python 的 matplotlib 或其他交互式作图系统都不困难。但如果搞不清楚原理,那么换一个软件就只能继续到网上复制现成的代码。我倒不是鄙视从网上复制现成的代码,毕竟这事我也没少做,但总要有个学习的过程才能掌握。

软件优劣之争在我看来很多都是鸡同鸭讲,很多比较都是在特殊应用场景下才有明显区别。但每个人的最终应用场景毕竟是不同的,解决问题的意义显著大于跟工具分高下的意义。显然编程语言绘图的能力范围更多受限于使用者的能力而非工具本身,因此这样的工具优劣争论还是少一些吧,争到最后大概率都成了人身攻击。