聊下图形语法“The Grammar of Graphics”

之前有一篇闲扯Tableau的文章读者蛮多的。那一篇的写作的过程也帮助我梳理了之前很多零碎的想法。正好最近几个星期又重读了一些自己领域的文献。再次整理下自己不成熟的想法。

这次主要介绍的这本《The Grammar of Graphics》是数据可视化领域最重要的著作之一,出版于1999年。作者Leland Wilkinson可能知道的人不多,但是提起以他的理论为基础的R绘图包ggplot估计很多人就知道这哥们的来头了(Btw此人曾经是Tableau Inc.的VP of Statistics,现已离职)。我想在本文里试试整理一下对图形语法也就是“The Grammar of Graphics“的理解。之所以不用书名号,是因为我并没有读完这本书。不过虽然本书只读了一半,我却接触过数量庞大的对它的引用和摘录,以及ggplot,vega在内的对这本书的形式化系统的实现的源码和文档。

好了正文开始。从一门学科的角度来看,数据可视化是个很奇怪的领域。2012年加入Tableau Research的Robert Kosara博士在论文《An Empire Built On Sand: Reexamining What We Think We Know About Visualization》里说到

If we were to design Information Visualization from scratch,
we would start with the basics: understand the principles of
perception, test how they apply to different data encodings,
build up those encodings to see if the principles still apply,
etc. Instead, visualization was created from the other end:
by building visual displays without an idea of how or if they
worked, and then finding the relevant perceptual and other
basics here and there.

简单说,数据可视化是一个没有系统化的理论或实验作为基础的学科。Robert Kosara这篇论文里的核心观点几乎就是在抱怨,“我们这些研究人员完全是赶鸭子上架的。尽瞎搞!”。

尽管数据可视化作为一个跨学科的领域缺乏坚实的理论基础。但是作为数据可视化的载体,各类的图表、尤其是有统计意义的图表的描述和绘制,却是有理论基础的,而且这个基础还是用数学描述的形式化系统。这个基础就是《The Grammar of Graphics》的内容。

我曾经在之前的文章里数次提到过,在科学研究和技术领域的牛人与我等的不同,在我看来就是思维抽象程度的不同。这本书的思想就是一个完美的例子。

大家都熟悉Excel里面是怎么列出可选图表的

Excel 2010的图表库

这是一个抽象层次很低的方式:逐个列举。如果按这种方式来描述和绘制图表,每增加一种新的图形,就需要做一份绘图逻辑、一份用户界面的向导、以及相应的格式设置的功能。一旦有大的需求变化,这个维护工作就是一场噩梦,还是怎么都醒不来的那种。

相反,《The Grammar of Graphics》中的图形语法就是一种抽象级别更高的对图表的描述方法。它的基本思路是,把图表的主体看成是数据和几何图形的视觉特征绑定的结果;同时,把图表看成一些简单的相互正交的特征组合而成的结果。比如说,数据点视觉特征(visual cue)和坐标系(coordinates)这两个特征的最常见的10(7+3)个可能的取值,就可以组合成21(7x3)种不同的图形类别。

Visual cue和坐标系的不同组合。图片摘录自《Data Points》

其实个人认为这个表格有一些不严密的地方,但是从中可以看出,这种正交特征组合思想的威力。比如,一个无填充的雷达图Radar Chart,其实只不过就是把一个普通的折线图画在了一个极坐标系里面。

使用这种语法结构,我们就可以方便并且准确的描述常用的已经被命名的,以及还没有被广泛使用的各种图表。另一方面更重要的是,在实现一个绘图系统的时候,架构的设计就会大幅简化,但是能够绘制的图形种类确丝毫没有受到影响。

一定有人会好奇这么强大的图形语法到底是什么样的。这里展示一下用一个叫做Vega的图形语法实现,描述一个bar chart的JSON是什么样的。个人有兴趣可以参考官方文档

{
  "width": 400,
  "height": 200,
  "padding": {"top": 10, "left": 30, "bottom": 20, "right": 10},

  "data": [
    {
      "name": "table",
      "values": [
        {"category":"A", "amount":28},
        {"category":"B", "amount":55},
        {"category":"C", "amount":43},
        {"category":"D", "amount":91},
        {"category":"E", "amount":81},
        {"category":"F", "amount":53},
        {"category":"G", "amount":19},
        {"category":"H", "amount":87},
        {"category":"I", "amount":52}
      ]
    }
  ],

  "signals": [
    {
      "name": "tooltip",
      "init": {},
      "streams": [
        {"type": "rect:mouseover", "expr": "datum"},
        {"type": "rect:mouseout", "expr": "{}"}
      ]
    }
  ],

  "predicates": [
    {
      "name": "tooltip", "type": "==", 
      "operands": [{"signal": "tooltip._id"}, {"arg": "id"}]
    }
  ],

  "scales": [
    { "name": "xscale", "type": "ordinal", "range": "width",
      "domain": {"data": "table", "field": "category"} },
    { "name": "yscale", "range": "height", "nice": true,
      "domain": {"data": "table", "field": "amount"} }
  ],

  "axes": [
    { "type": "x", "scale": "xscale" },
    { "type": "y", "scale": "yscale" }
  ],

  "marks": [
    {
      "type": "rect",
      "from": {"data":"table"},
      "properties": {
        "enter": {
          "x": {"scale": "xscale", "field": "category"},
          "width": {"scale": "xscale", "band": true, "offset": -1},
          "y": {"scale": "yscale", "field": "amount"},
          "y2": {"scale": "yscale", "value":0}
        },
        "update": { "fill": {"value": "steelblue"} },
        "hover": { "fill": {"value": "red"} }
      }
    },
    {
      "type": "text",
      "properties": {
        "enter": {
          "align": {"value": "center"},
          "fill": {"value": "#333"}
        },
        "update": {
          "x": {"scale": "xscale", "signal": "tooltip.category"},
          "dx": {"scale": "xscale", "band": true, "mult": 0.5},
          "y": {"scale": "yscale", "signal": "tooltip.amount", "offset": -5},
          "text": {"signal": "tooltip.amount"},
          "fillOpacity": {
            "rule": [
              {
                "predicate": {"name": "tooltip", "id": {"value": null}},
                "value": 0
              },
              {"value": 1}
            ]
          }
        }
      }
    }
  ]
}

细节我就不一一说了。可以看到,图表的每一个部分,数据(data),交互(signals),轴(axes),visual cue的数据绑定(marks),都是独立描述的;然后,数据点marks的绘制是通过把矩形rect的y和scale以及data field "amount"绑定来实现的。这就是图形语法所倡导的风格。

University of Washington的Interactive Data Lab有一个叫做Lyra的项目,个人认为是对根据图形语法来创建可视化的一个GUI上的很好的尝试。这个UI上手需要一些时间,不过大家有兴趣可以自己上手试试。

Lyra Project at: http://idl.cs.washington.edu/projects/lyra/

大致的思想说清楚了,但是《The Grammar of Graphics》如果就是说了这么个事情,它就最多只能算本畅销书而不是一本重量级学术著作了。作为学术著作,它描述这套语法的时候,用了所谓的“形式化系统”,说简单一点就是用数学符号来进行定义、演绎推理和验证;更直观一点,就是书里都是这种东西

《The Grammar of Graphics》对error bar的定义
《The Grammar of Graphics》5.1.2.2节描述nest的时候书用的集合定义

为什么要这样呢?Leland Wilkinson是个统计学家,在他的领域里,只有数学的语言是够精炼并且也绝对严密的,所以他会使用形式化系统来描述自己的思想。从抽象层次的角度,这个比起我们刚才用自然语言描述的本书的思想,就更高了一层。这样的优势很明显,一来他的书里的定义、描述就会很简短,并且可以用数学的方式来确保无误;另一方面,很多的后续工作可以依靠任何理解了这个系统的人,通过符号演算来完成。

目前为止,Leland Wilkinson首创的这一套图形语法系统已经有了不少的实现,如ggplot, 上文提到的Vega,Python世界的Bokeh,Tableau的VizQL,IBM曾经主推的vizJSON,还有不久前阿里出的G2,等等。不过图形语法严格来说只是一种思想而不是一个真正意义上实现标准,所以很不幸这些实现是无法互相兼容的。有些实现为了兼顾自己的实际使用场景,也并没有完全遵守图形语法系统的原始定义。

最后再进一步说到产品上。图形语法这东西抽象程度这么高,用户怎么用?这就要看产品把理论降维成谁都能用的友好界面的功夫了。这项工作比说起来要困难的多得多。目前为止我们看到的是,虽然图形语法把图表拆开成了正交特征,但是没有哪个可视化构建工具会让终端用户自己决定visual cue,再选坐标系,然后绑定几何属性的数据值……就算Leland Wilkinson本人曾在Tableau麾下,Tableau Desktop也还是用“列举”的办法,提供了一堆预设的chart type来给用户选择。原因很简单,图形语法的抽象,导致用户在使用过程中得花更多功夫去理解和学习,太费脑子。于是图形语法这套在研发的时候无比强大的思想工具,如果原封不动的搬到产品界面里,搞不好就成了影响用户体验的罪魁祸首。我猜测这也是所有的产品都还是沿用了图形列表的方式的原因。这就好像人人都知道古典音乐内含的审美体验要丰富得多,但是很多人就是图个消遣,懒得动脑子去学习去挖掘,所以在听众的人数上还是流行音乐占了上风。音乐界也就自然会花更多的资源在流行音乐上。

商业世界是很民主的,每个用户付的1块钱都是一样的,所以再牛的学者和理论,一旦投身了产品化,就只能去帮助公司发展更“多”的用户。本人不够聪明无法体会大牛人们要伺候我们这样一群蠢货是什么感觉。但是我猜应该就像你发明了乘法,结果大家死活就是只学得会加法。无奈之下你只好告诉他们,3x4其实是等于3+3+3+3的,大家还是用加法就好,慢慢加哈不要着急加错了。然后心里不停的骂脏话……

想起某电视剧里的一句话,“这个世界,从来都是明白人让着糊涂人”。是至理名言。

推荐阅读更多精彩内容