9

当我做 hackathon 时我在做什么 (2)

 3 years ago
source link: https://zhuanlan.zhihu.com/p/345326308
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

书接上文: 当我做 hackathon 时我在做什么(1)

前文中提到,我做的第二个项目是个可视化的项目,名字叫 deneb。deneb 是天鹅座的一等星,也是夏季大三角和北十字两个星群的端点之一。deneb 是对 vega-lite 的封装,受 同样封装了 vega-ltie,深得我喜爱的 Python 的库 altair 的启发。嗯,deneb - vega - altair,聪明的你一定想到了我为什么起这样一个名字:

6jIRVnf.jpg!mobile

为什么是 vega-lite?

在数据可视化这块,我自己走了不少弯路。我最早的启蒙工具是 matplotlib [1],它很容易上手,照着例子很快就能做出还算不错的图表。后来我发现了基于 matplotlib 的 seaborn [2],提供了对统计相关的图表一个高阶的抽象,很多在 matplotlib 下很多行代码才能表达出来的图表,seaborn 一两行就搞定,非常给力。之后,因为希望做出来的图表可以有更多的交互,我又转向了 plotly [3]。plotly 使用起来更加简单,但其背后的思路和 matplotlib 一脉相承:你需要定义 fig,描述你需要绘制哪种类型的图表,x 轴,y 轴数据等信息。plotly 之所以能够交互,是因为其背后是一套 javascript 库,最终渲染出来的是一段 html 代码。如果你需要能够对可视化的图表做简单的动画,plotly 也能胜任。

我一度以为 plotly 是我的真命天子,直到有一天我敲开了 altair [4] 这个潘多拉魔盒。

altair 让我了解到其背后的 vega-lite [5],以及 vega-lite 背后的那本被称作 GG(The Grammar of Graphics)的旷世奇书。这本书的作者是 Leland Wilkinson,是数据可视化领域的大牛,他的著作影响了一代人。如果你对 GG 感兴趣,可以 youtube 里搜索 Leland 的大名,看看他对自己思想的解读。

为啥我说 GG 是旷世奇书呢?因为仅仅看了一些介绍,以及书中思想的一些片段,我就受益匪浅,感觉对数据可视化的认知提升了一个级别。比如 GG 里提到,「饼图是极坐标下的柱状图」。你品,你仔细品。

feIJBrm.jpg!mobile

我们平时做可视化,首先接触的是各种图表的分类,但 Leland 认为:

Taxonomies of charts are harmful, just like goto in programming languages.

他觉得我们在做数据分析的时候,更多是一种探索,而分类是反探索的,因为当你用某种类型的图表来表达数据的时候,你已经对如何分析数据有了先入为主的看法。

那么什么是图表呢?Leland 认为:函数(Graph)在有限的的作用域下(Frame)通过美感(Aesthetic)表达出来,就是图表(Graphic)。

具体如何表达呢?通过组合坐标系,方面,统计方式,形状,标度,美感,再加上数据本身,共同作用出一个合适的图表:

iq2Yzqu.jpg!mobile

这种方式打破了传统图表的分类法,更贴近如何去探索数据本身。

我很喜欢这里的 Aesthetics。图表是数据的视觉编码,好的视觉编码一定是要具备美感。美感可以通过大小,颜色等方面表达出来,其中最重要的表达手段,或者说视觉通道就是颜色。颜色可以描述变量的模式/规律,可以做类别标注,也可以起高亮和强调的作用。

GG 这本书除了把这些概念介绍地很透彻,还对图形的表达做了完整的形式化表述,也正因为如此,很多工具直接在 GG 的基础上进行开发,比如 R 里的 ggplot。vega 受 GG 和 ggplot2 的启发诞生,随后更加精简,更受大家欢迎的 vega-lite 又在 vega 的基础上产生。受 vega-lite 的影响,altair 开始崛起,而我受 altair 的影响,萌发了在 Elixir 下复刻 altair 的想法。

yqYjqiF.jpg!mobile

好了,关于 GG 的故事就先讲这么多,等我通读完这本大部头后,有空可以单开一文讲讲我对可视化的认知。

如何在 Elixir 上「复刻」一个 Altair

在做这次 hackathon 之前,我已经有了还算丰富的 altair 的使用经验,但我并未太多研究 vega-lite 本身。所以在做 deneb 的过程,其实就是我自己学习 vega-lite,然后把 vega-lite 的代码用 Elixir 封装起来的一个过程。vega-lite 主要有这样几种对象:

  • mark:这是属于 Geometric Objects 范畴的东西,就是你用什么图形来表述数据。比如 "bar"。
  • encoding:其中包含了坐标系和 axis / color / size 的声明,属于 Coordinate System / Aesthetics 范畴的东西。encoding 中也可以声明部分 statistics 范畴的东西。
  • transform:在视图层对数据的各种处理,属于 Statistics 范畴的东西。
  • facet/layer/concat/repeat:视图层的各种组合,属于 Facets 范畴的东西。
  • selection:定义了互动相关的操作。

下面是一个最简单的 vega-lite 的代码,完全由 JSON 表述:

{
  "$schema": "https://vega.github.io/schema/vega-lite/v4.json",
  "description": "A simple bar chart with embedded data.",
  "data": {
    "values": [
      {"a": "A", "b": 28}, {"a": "B", "b": 55}, {"a": "C", "b": 43},
      {"a": "D", "b": 91}, {"a": "E", "b": 81}, {"a": "F", "b": 53},
      {"a": "G", "b": 19}, {"a": "H", "b": 87}, {"a": "I", "b": 52}
    ]
  },
  "mark": "bar",
  "encoding": {
    "x": {"field": "a", "type": "nominal", "axis": {"labelAngle": 0}},
    "y": {"field": "b", "type": "quantitative"}
  }
}

所以,对于 deneb 来说,就是提供优雅的接口把 Elixir struct 翻译成 vega-lite 里的 JSON object。为了达到这个目标,我们需要提供对 vega-lite 语法在 Elixir 上的封装。我认为封装有几层:

  1. 传递给 deneb 要绘制的数据,和绘制这个数据所用的 vega-lite 表达,deneb 将其组合成一个可以展示的 JSON 数据。
  2. 传递给 deneb 要绘制的数据,和绘制这个数据所用的 elixir structs,deneb 将其组合并翻译成一个可以展示的 JSON 数据。
  3. 在 2 的基础上进一步封装,让每个域都有其 Elixir 语法。
  4. 在 3 的基础上提供数据校验和足够清晰的出错信息。

在 altair 接口中,已经完全没有 vega-lite 的表达式了,取而代之是对应的 Python 表达式,如果用户撰写的代码有误,Altair 能够清晰地展示错误,帮你定位问题。所以altair 实现到了第四级。然而 altair 付出的代价是四万七千行 Python 代码。就算我脑子里有个 Python-to-Elixir 的代码转换器可以逐行翻译,让我抄四万多行代码一天也抄不完。

所以,我打算一步步来。先实现第一层,让 deneb 用最小的代价跑起来。比如上面的那段代码,对应的 Elixir 代码如下:

%{
	mark: "bar",
	encoding: {
	  x: %{field: "a", type: "nominal", axis: %{labelAngle: 0}},
	  y: %{field: "b", type: "quantitative"}
	}
}
|> Chart.new()
|> Deneb.to_json(data)

有了这个基础,我再一步步把几个主要对象映射到 Elixir,最终形成这样的代码:

:bar
|> Mark.new()
|> Chart.new(Encoding.new(%{
	  x: %{field: "a", type: "nominal", axis: %{labelAngle: 0}},
	  y: %{field: "b", type: "quantitative"}
}))
|> Deneb.to_json(data)

是不是感觉两个变化并不大?但这些对象内部有一些校验,保证输入的正确性。

我虽然很喜欢使用 altair,但学会了 altair 并不能保证我同时会写 vega-lite 语法,因为 altair 自己已经成为一个厚重的 DSL,完全包裹住了 vega-lite。这其实对学习 vega-lite 不够友好。

所以,我认为 deneb 实现到第 2 层至第 3 层的封装和抽象就足够了。一来是留给我的时间不多了,二来我觉得过于厚重的封装不是那么有必要,vega-lite 自己的语法表现力足够且并不复杂。

有了基础的 deneb 的实现,接下来就是如何把生成的 vega-lite JSON 展示成图表。我需要定义一个 Viewer,用于将 JSON 数据放入一段 javascript 中,然后加载到 html 页面中。我参考了 altair_viewer,实现得不费吹灰之力。至此,用户想生成一个复杂的图形,比如证券分析里经常使用的蜡烛图,可以用几行代码轻松表述:

BvMZRru.jpg!mobile

难道就这么简单?

qQ7BNnR.jpg!mobile

当然,事情绝对不会那么简单,brick wall 总是会不期而至的。

第五次撞墙:IElixir 和 jupyter notebook

完成 ex_polars 就像打完我自己的淮海战役一样,做 deneb 的过程是摧枯拉朽,几乎不费太大的力气。一切开发妥当后,我在 Jupyter notebook 上运行我心心念念的第一个最简单的柱状图,结果,jupyter notebook 没有任何输出。我查看 chrome 的 console error,没有任何报错,这下麻烦了,如果在这里卡住,那真的就是功亏一篑啦。毕竟,一个无法支持 notebook 的可视化库,还好意思说自己为 data science 所生?

Jupyter Notebook 本不支持 Elixir,但它充分考虑了语言级别的扩展性,提供了一个 ZeroMQ 接口和 kernel 交互消息,因此,其它语言可以实现对应的 ZMQ 接口,和 Jupyter 通信。下图展示了 IPython Kernel 如何跟 Jupyter 通讯的:

Zfu6Nj3.jpg!mobile

Elixir 生态圈里有个 IElixir,仿照 IPython,做了对 Jupyter 的支持。IElixir 实现了基本的消息通讯,但有些细节似乎没有测试过。比如对 html 片段的支持。这也是为什么我在做 ExPolars 时, 在 Jupyter notebook 里,一切操作都正常,因为那些输出都是简单的 text;而当我想输出 deneb 生成的包含 vega-lite spec 的 html 片段时,IElixir 就无法正常工作了。

既然我定位到问题可能出在 html 上,那么,问题的解决并不麻烦。我只需在合适的地方加入打印,看 IElixir 的输出,一步步缩小问题的范围即可。最后,我成功解决了问题,并给 IElixir 的作者提交了一个 PR(还有什么比这个 PR 更能彰显 OSS-a-thon 的意义的?):

BNrIBbr.jpg!mobile

享受胜利的喜悦

当第一张图表输出到 Jupyter notebook 的输出框里时,我激动地跳了起来。一旁搭乐高的小贝茫然地看着我,不知所措中就被我抡起来往空中抛了三次。然后我又趴着示意她骑大马,绕着三楼的空地蜿蜿蜒蜒走了一圈。

随后的几个小时,就是查漏补缺,即兴发挥的时刻。我为 ExPolars 提供了 plot_singleplot_repeatplot_by_type 几个快速生成图表的功能,对标 pandas 的 df.plot 功能。比如,一行代码实现下面的可视化:

ee6zAbq.jpg!mobile

以及,一行代码实现上文中的 candlestick:

FbARRzV.jpg!mobile

注意看这幅图,它是两个 chart 组合而成的,还使用了 selection 来提供交互。用户在选择小图的时候,大图会随之而动。

参考资料

我的 hackathon 项目:

我的 hackathon 项目:

tyrchen/ex_polars github.com tyrchen/deneb github.com

感兴趣的同学可以关注。本文中提到的其它项目:

[1] matplotlib: matplotlib.org

[2] seaborn: seaborn.pydata.org

[3] plotly: plotly.com

[4] altair: altair-viz.github.io

[5] vega-lite: vega.github.io/vega-lite

贤者时刻

四天的 hackathon 结束后,我无比满意四天前的我的选择。因为这个选择,让我一次又一次遇见新鲜。世间一切,都是遇见,就像冷遇见暖,就有了雨,春遇到冬,有了岁月;天遇见地,有了永恒;人遇见了人,有了生命。

献上一曲小宝最近弹的 Arabesque:


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK