我与数据可视化的那点事儿

visual
Author

Hongyang Zhou

Published

September 10, 2020

Modified

September 17, 2022

学习工作中反复和图像打交道,更能感受到人类的视觉动物本性。五感中,视觉可谓是信息量最大的输入,也同时深入影响着抽象的思维逻辑。我们力图在复杂中寻找简单,高度提炼的数据是关键的一环。

从事科研若干年,关于成果展示,一句经典老话:“字不如表,表不如图。”从一开始不明所以在海报里塞入大量文字,到后来转而力求简洁清晰的图像,本质上就是随着随着认知的深入能深入浅出地发掘重要信息进行归纳总结。比如现在的文献检索,当你用关键词找到若干篇备选,也不会每一遍都从头到尾地看,而是依据本身的熟悉程度寻找核心线索和结论,着重看关键的公式和图表。如果恰巧你能在一个公式或一张图中找到这样的关键信息,而无需为了诸多细节阅览全篇,你会庆幸自己遇到了个好作者。我们喜欢图像,就像是在混乱中寻找秩序的冲动。


大家频繁接触数据做图多半是从中学开始学习几何以后。想着当年各种手绘解析几何的直线曲线图形,为后来的电子化做图打下了坚实的基础。这种经典的教学方式如今可能会受到电子化的冲击,但动手画图依旧是我们最快的理解方式。记得高中时候鸟蛋就开始玩儿几何画板,那对于搞奥赛平面几何简直就是神器:各种图形中涉及到的不动点、共线、共圆,手绘是静态的很难看出,但是用几何画板设几个不动点拖几下可能就出来了。在苏州参加集训的时候一次请来的老师讲课不经意之间在我们面前秀了一把操作,让台下所有学生羡煞了眼。鸟蛋一次还跟我讲拿几何画板看旋轮线轨迹,对当时的我来说已是超乎想象的了。后来认识了楚哥,自吹几何画板也是用得飞起,我信个七分,羡慕个三分。

关于计算机绘图,个人上手最早的经历可以追溯到大一C语言作业要求的命令行输出。当时有一类练习输出的题目就是让你在命令行里画各种图形。现在有一些仿古的绘图库,比如UnicodePlots,还能让人依稀回忆起那样的图形风格。真正开始大量实践是从大学物理实验开始的,这系列课程我连上了四个学期,每学期就占1.5个学分,占用的时间和投入的经历却几乎相当于一门3学分的课程。每周一次用一个下午或者一个晚上的时间做实验,然后拿着数据按照要求一通分析,为了写实验报告经常弄得焦头烂额。一开始全电子版的实验报告还不熟练,大部分都是手写的,只是图表是用Origin做的,误差估计啥的经常是拿着计算器手算的,最后到打印店把做好的图用A4纸打出来,裁剪好贴到手写的报告上面去。到了本科高年级的物理课和研究项目中,手写的报告几乎不复存在了,无论是实地操作的仪器中采集的数据,还是计算机模拟跑出来的数据,数据分析和处理全部电子化了。大三开始接触MATLAB进行动态绘图,大四上手LaTeX折腾得死去活来但入门后蓦然回首发现公式、图表和排版竟然可以如此简单,画图这事儿就自然而然地走进了我的日常学习之中。做本科毕业论文的时候,主要的粒子模拟是用Fortran跑的,输出的格式也是自定义的,然后传到MATLAB里面画图做动画截图分析结果。


研究生以后画图更成了家常便饭的活,各种project需要的图主要靠MATLAB做,而组里做研究主要靠IDL。一遍又一遍的写报告制图中,摸爬滚打学习着如何制作大小适宜、风格统一、简洁明快的图片,顺带也涉猎了大量有文档可查的功能。研一的冬季学期上了门系里的编程课,主讲老师Dan是个Python狂粉,我在这里第一次系统学习了Python并了解了著名的由已故生物学家John Hunter在2002年创造的类MATLAB绘图库Matplotlib。由于和MATLAB的渊源,我上手起来非常快,虽然不是很喜欢Python的语法,但是对Matplotlib非常认可。另一方面,IDL这门古早的语言至今还被用在大量空间物理的数据分析中,虽然我就几乎没见过年轻人说它的好话。当年理论力学课最后李毅教授曾经展示过他写的陀螺模拟程序,还有GUI界面,似乎就是IDL写的。研究生组里老板精通Fortran,Perl和IDL,用IDL写一整套的模拟数据可视化包,渡过安装的镇痛后,参照说明操作绘图倒是非常便利。然而这里的“便利”,指的是照猫画虎做既定规格的图的时候——若是遇到稍微想订制一下的出版规格的图,基于IDL的这套工具用起来真是有苦难言。当时Python已经火起来了,组里也有老师写了个Python的模拟文件处理程序,但一来我并不喜欢Python,二来这个包第一次安装时还失败了,所以就没了下文。Mathematica虽然很早就见识过它的厉害,但是我不熟,由于也更偏向符号运算,一时对我的需求也不是很多,就没有仔细折腾。远在大一的时候去曹原租住的小屋,刚进门就看见他在用Mathematica玩一个可调参数的函数,当时就被惊艳到了。同样程度的可视化通过拖拽控制槽来即时显示函数的工具,MATLAB在17年前后才引入,前后查了5年有余。更不用提后来再次惊艳我的Wolfram Alpha,虽然不怎么用,但依然感叹匪夷所思。Mathematica的创始人Stephen Wolfram是一名天才型的人物,我在校那几年和MATLAB的创始人Cleve Moler曾先后两年来过密歇根做演讲,但那天我不巧病倒了,未能去现场。

然而没有摆脱IDL的阴影仍在笼罩。本来IDL就够难用了,老板还只用命令行的IDL,更是雪上加霜。曾经我试图修改些他写的IDL脚本,先是在自己尝试的时候连个简单的循环都不能编译,后来又是看到了Fortran66中“臭名昭著”的common block遍地横飞,全然苦不堪言。折磨两年后,17年暑假我回国,励精图治开始写一个基于MATLAB的模拟数据分析包。多年的课程锻炼让我对MATLAB绘图如数家珍,所以主要的难点在于数据格式的读入。我们组这个小作坊说来也是神奇,几乎所有的研究工具都是私人定制的,其中也包括数据格式。这格式显然是没有啥文档说明的,尤其是主要使用的二进制文件,只能靠读源代码进行分析。花了一个月,我参考老板的IDL包写出了自己的MATLAB包的雏形。这也是我真正意义上对于数据处理和绘图底层架构的入坑尝试,收获了非常多的实战经验。我第一篇文章中所有的图都是拿这个MATLAB包做的。

在上计算流体力学的时候,我被Christof Fidkowski教授的电子版讲义深深震撼了:那是我学生生涯里见过的最好的教学讲义,甚至比很多教材都精致。他用来画概念图的那款软件我至今不得其名,但是自己后来在Mac上找到了一款叫Zebra的免费版类几何画板软件,也已经十分好用了。

18年底,机缘巧合我结识了Julia。关于这段故事,详情可以转见Perception. 19年初,我觉得时机已然成熟,为何不写个基于Julia的模拟数据处理包呢?有了先前MATLAB包的经验,这次改写效率高了很多,基本上一个星期就完成了第一版。期间遇到的主要问题源于我对Julia的不熟悉以及Julia本身对于文件I/O底层的支持当时还不够健全(譬如read!(iostream, @view A[:,1]) 这样的操作,在Julia 1.4以前并不原生支持,需要一些hack)。然而这次改写,加上Julia本身对于绘图后端选择的强调,让我开始思考我们需要一个什么样的数据可视化包。我们可以越俎代庖般给所有的已有绘图库中的方法添加自己写的一套wrapper,并且定制所有的参数,但是这么做的代价就是出产的包异常臃肿并且难以维护:每当遇到新的情景,新的参数,你都需要在wrapper中添加相应的改动,最终的代码就会又臭又长。这里不得不提Python中令我深受启发的特殊传参方法 *args**kwargs: Julia中类似的操作叫做splat operator ...,利用它,你可以很方便的为已有的方法构建简洁的wrapper。

不久后我意识到Julia中三维绘图分析功能相比于Tecplot和ParaView来说实在是太简陋了,于是四处寻找解决办法。Tecplot是一款组里一直在使用的三维数据可视化软件,功能很强大,可惜是收费的。它的数据格式除了最新的高性能并行规格作为商业机密保密以外其它的都有详细的说明,所以组里的计算程序能够直接输出该老格式的文件。遗憾的是,这个格式并不能直接被ParaView读取。ParaView是一款业界非常出名的三维可视化开源软件,同他的兄弟VisIt一样师出VTK,也已经深耕了二三十年。VTK则是一套历史悠久的计算机图形学底层库,定义了诸多基础几何结构的表示方法,也是现在诸多可视化的软件的基石。这时候我也临近毕业,一心的想法就是不能栓死在付费小众软件上,而应向着更通用的格式靠拢,于是详细了解了VTK的基本格式,包括传统的和现代基于XML的,并且上手了ParaView的操作流程。更巧的是,一位Julia社区中的法国人搞出了一个生成基于XML格式的VTK文件的开源包,打通了最后一个关节。虽然从Tecplot的PLT到VTK格式转换需要些时间,但是受惠于XML中的压缩算法文件大小可以减小2-10倍,在我看来是非常重要的优势。这里的逻辑有点绕,但结局是我搭好了Julia+ParaView的可视化分析生态,并成功应用到了第二篇科学文章的数据分析之中。


当我开始芬兰的博士后工作后,这边的组里又是一套全新的C++代码+数据格式+自定义Python包。如果是个写的好维护的好的Python包我也就直接用了,可是我进组两个月屡次要求开发者在文档缺失的情况下给一个绘图包的使用教学,一路被各种莫名其妙的借口拖到看不见尽头……我一念之下自己动手从零开始用Julia写了一个新的。所幸这次数据格式的文档还不错,Python理解起来也不难,第一版的数据读取+简易绘图模块大概两周弄出来了。这次的新挑战在于,原先的Python代码头重脚轻的逻辑让我看得连连叹息:在读取数据这块,它定义了Python class中最核心的方法是一个通用参数的接口,之后在各个地方再调用基于这个通用方法的实现。众所周知,Python是一门本来就不是以速度闻名的动态语言,这么一折腾更是代码又长运行又慢。如果我照着原样翻译一遍,当然可以立刻在Julia中运行,但是运行效率上一定是没谱的。于是凭借两年来写Julia的经验、对文件格式的理解、以及对Python代码的分析,我从头构建了整个数据流的逻辑,结果是几十KB的小文件读取快了10倍,1MB左右的快了2倍,30MB以上的基本持平——根据我先前的经验,这部分主要的时间都消耗在了系统I/O上面,越大的文件越是如此;理论上Python的I/O由于是用C实现的,不会比任何语言慢。这不能反映出我的Julia代码写得好,只能说明原先的Python代码的确存在设计问题。

后来陆陆续续我不断在丰富这个Julia包的功能、构建完整的测试和文档,并且注册到了官方包管理目录中以方便安装使用。基于Python Matplotlib的绘图后端代码,我用200行实现了原先Python中1200行实现的功能。核心的设计理念就是用户不需要额外阅读我写的文档才能知道如何作图:如果他有需求,可以直接在Matplotlib或者其他后端库的详细文档中找到相应的实现方法。这样大大节约像我这样中间开发者的工作,利于长久维护。还是那句话,不是友军太给力,奈何敌军不争气。最开始的时候,我其实也可以选择全面更新已有的Python代码,但是出于对劝服先前维护者改动的疑虑、所有功能重新实现工作量的担忧、以及淘汰老旧错误使用习惯的代价,我放弃了走这条路的念头。用新工具很多时候不得不从头造轮子,但是相应的我们没有历史的负担,不必念及旧情。我花了不少时间尝试一些细节上的优化,许多都是无疾而终,但也并非无功而返:失败带来的都是宝贵的经验,做你不知道的或者别人没做过的尝试才能淬炼手艺。于是临近一年时间这个包的版本号也即将达到0.8,而我的如意算盘则是在手头的新数据分析完发文章的那一天达成1.0的目标。让我们拭目以待。


学习Julia的过程中,我也一直在跟进社区里数据可视化方面的进展。早期的Julia中是没有自己的绘图库的;当时的做法主要是搭好了和各种其他语言的接口,直接调用其他语言中成名已久的绘图包,比如Matplotlib和GGPlot。为了方便使用,Plots.jl的主要作者设计了一套前后端的逻辑,后端是这些调用的库,前端则是统一的Julia语法。然而由于各种后台库之间的差异,使用起来时常会遇到前端不支持的功能或者不能调整的地方,最终还不如直接调用底层库方便。同时逐步诞生的是一个原生的Julia基于OpenGL的绘图库Makie,概念很新颖,但是尤其是早期又慢又难用,开发人手不足缺失众多功能,仍处在尝鲜的阶段。曾经我就这方面的问题在论坛问了很多,最终得出结论在19年当时若想有productive ready的工具还是得用PyPlot,其他包里不足之处还是得靠时间去磨。

如今两年过去了,传统上time-to-first-plot的知名问题随着新版本的问世得到很大改善,但依旧还达不到Python的水准;Makie临近1.0版本,文档逐步丰富,接口逐渐统一,多图排版得到了显著提升,自定义数据接口也修复了bug,但是默认的流程下启动速度还是太慢。很多人期待着Makie最终能取代Plots.jl成为Julia的招牌绘图库,毕竟亲儿子,需要秀肌肉。按如今的发展势头,再等两年说不定我可以正式向别人推荐这个包了。


我还折腾过好一段时间图形化界面GUI。MATLAB中提供了自定义的GUI编写模块,从最古老的GUIDE,到现在的App Designer。早先是写VisAna的时候想搞一个简单的快速浏览数据的用户界面,弄了一半,数据能通过菜单读取了,能画一张最基础的图。没继续弄的原因,就是我发现这是一个大坑:很多细节的调整非常花时间,并且做出来了可能也没人用。对于像我自己这样的,直接写脚本或者命令行绘图是更高效的方式;对于不想直接敲代码写函数的,可能也不会亲自去画图。于是这个GUI的想法变成了一个很鸡肋的功能,我也半途而废了。一次在运动之后我和睿豪聊起这个事儿,他笑了笑说我应该去搞计算机——我想着设计个没人用的用户界面如此麻烦,还真不适合我这个缺少艺术细胞的人做。 有天跟老板Gabor抱怨模型输入参数检查很费心的时候,他不知道从我们代码库的哪个角落给我翻出来一个基于HTML的GUI,可以修改并检查模型的输入参数。我在组里已经干了好几年了也从没听人谈起过这个,也没见人用过,可见这东西做出来是吃力不讨好。

最早上Python课的时候,我就玩过相关的GUI开发包,虽然很简陋,但是对我而言很新奇。18年我参加完ISSS13的workshop以后,对一个日本研究组开发的以教学为主要目的的PIC代码很感兴趣。当时这套代码是从Fortran改到MATLAB的,并且添加了个GUI,能调输入参数,也能实时监控输出,的确适合教学;但由于年代久远,GUI部分是用GUIDE写的,正巧App Designer刚出来不久,我借着东风把原始的代码改成了兼容App Designer的版本,大致了解了一个App的实现框架。20年底楚哥跟我抱怨过他老板写的MATLAB程序多么难用,我一看是个裸的脚本加上一个GUIDE的壳,框架好不好另说一没文档说明二没数据测试,也就放弃帮他改进了。如今的MATLAB App Designer明显是更加趋近于Java风格的,毕竟整个MATLAB的前端和可视化部分都是Java实现的。

Julia中也有封装的一些做App或者图形化界面的包,比如GTK、Electron之类的,但是毕竟不是原生的支持有限,很容易碰壁。结合先前的经验,我也就没有继续探索下去。设计前端的界面真是个体力活,码农那么多,Java那么火,也是因为需要大量人力来完善面向普通用户的图形界面。新时代密集型劳动,码农称谓诚不欺我。


科学绘图有别于艺术绘画,但依旧可以拥有风格甚至幽默。xkcd就是一个风靡在PhD群体中的漫画库。在Matplotlib中使用plt.xkcd(),或者在PyPlot中使用xkcd(),你可能会惊讶地发现加入点小曲折的线段无意间消解了你过于规矩的焦虑:

real programmers

curve fitting