探索分析中多维数据集可视化理论基础

摘要

探索分析是BI领域重要的研究方向之一,随着信息量的激增,业务数据的维度,大小,关联关系逐渐变得愈加复杂,使得即便是拥有多年经验的业务人员,也无法探知数据中蕴藏的全部规律、模式与领域知识。由此带来设计开发对应的探索分析系统,使得用户能够快速的从庞大的数据集中筛选自己关心的数据、选择关心的维度与度量来研究验证某一猜想假设,得出可以指导决策的有效结论。本文通过对探索分析系统中基本概念的定义与推理,结合图形语法理论中的可视化构建链路,给出探索分析中数据可视化的通用数学描述,从而为多维数据集可视化系统的设计与开发提供一套简洁可行的算法描述。

介绍

多维关系型数据库的可视化研究一直是BI开发中的基础领域,该领域的主要挑战是如何将数据库中的知识呈现出来,发现规律、异常并理解数据间的关系。由此,诞生了基于假设、猜想对数据库进行探索分析的需求。这种探索分析的特性是对于结果、方法与步骤的不确定性,同时要求快速改变用户研究的数据视图以及观察这些视图的方式的能力[2]。

常见的方式是将这样的一个多维的关系型数据库视为一个多维的数据立方体(cube)[6],这种方式最知名的实践之一便是数据透视表。但数据透视表在数据的直观展示能力上非常欠缺,基于这样的问题,本文从如何将数据透视表背后对应的多维数据立方体进行可视化,使用更直观图表进行展示”入手进行研究。

可视化过程

对于多维数据集可视化的构建过程,可参考The Grammar of Graphics[1]中给出的可视化计算链路:
image.png
下面详细介绍系统实现时每个步骤的工作。

1. 确认变量

第一步要根据源数据确认变量(variable),并进行分类。_这里的变量可以理解为事实表(fact table)中的字段(field)或列(column),但为了防止与视图中的字段或列混淆,本文采用变量的称呼。_BI中常见的变量分类是将变量分为维度与度量。维度与度量是分析人员根据对数据的理解与对探索问题的定义与假设给出的分类。本文采用一种对维度(dimension)与度量(measure)的更为普适的理解,即定义维度是自变量(independent variable),度量是因变量(dependent variable)[2]。根据该定义,在构建可视化时,若用户只给出了度量的定义,那么系统则需要补出默认维度,否则度量是没有意义的。同理,若用户只给出了默认维度,则需要补出默认的度量或者对定义域进行展示。

定义:

  • 维度是由若干成员值构成的集合,表示为
  • 维度集是由若干维度构成的有序集合,表示为
  • 度量名称集合与度量值集合

基于以上定义,给出rows与columns的定义: rows与columns是由维度集与度量名称集合构成的有序元组,其满足度量名称集合一定出现在维度集的后面,用符号表示为:

用户在进行可视化的声明(specification)时,会优先定义Rows与Columns,系统设计时,尽可能保证Rows与Columns满足以上定义的形式,从交互界面上讲,即添加度量永远在维度后面的限制。这也与函数计算中必须先知道自变量的取值,才能确认因变量的取值是同样的道理。

对于变量的分类尤为重要,在更进阶的BI系统中,甚至可以将变量基于其所使用的标度(scale)分为[3]

  • 类型标度(nominal)
  • 有序标度(ordinal)
  • 定距标度(interval)
  • 定比标度(ratio)

更细致的划分使得系统在进行数据分析时可以使用更精准的分析方式,如对于有序标度,计算insights时可以计算趋势[4]。否则,则只能使用较为麻烦的分布差异比较的方式来获取insights[5]。

由于自变量与因变量的设定是基于研究人员的理解与假设的,所以系统需要支持自定义维度与度量。为了减少用户的工作量,系统往往会包含一些默认的规则来帮助用户区分维度和度量、如tableau会默认将连续型变量作为度量、离散型变量作为维度[2]。这些系统甚至会根据数据特性进行更细的划分,如识别出日期格式的数据与地理数据等,这就使得系统可以更清楚这些变量的标度,并为其定制一些分析工具。

2.代数计算(求定义域)

为了理解一个函数的特性,我们不仅需要知道函数的自变量和因变量,还要知道自变量的定义域。若将可视化背后cube与切片的计算过程理解为一个函数,那么代数(algebra)计算的步骤实际上实在求函数定义域的过程。在给出rows与columns后,可以结合源数据求出可视化中的几何对象对应的视图数据:

代数计算即上述公式给出的第一部,即由rows与columns转换为方便后续进行计算的维度与度量的过程。

2.1 维度集定义域的理解

确定自变量的定义域并不是只需知道每个自变量自己的定义域即可。如下图所示,对于两个连续型维度,代数计算的步骤即根据X的定义域与Y的定义域求(X, Y)的定义域的过程。

image.png

用离散型变量举例,定义
食物维度
餐厅维度
则最终的定义域为

在计算过程中,将用户所定义的所有维度集合在一起,根据事实表中实际存在的维度元素组合作为限制,构建一颗维度树,这颗维度树实际上是维度集与度量名称集合的定义域(这种叫法不是很严谨),即

那么如何求解该定义域呢?

首先介绍两个概念Nest与Cross:
定义:设A,B为两个集合,则A,B之间的Cross操作为

定义:设A,B为两个集合,R为数据集,则A,B之间的Nest操作为



求解定义域的过程,即将行上的所有维度进行nest计算,列上的所有维度进行nest计算,最终得到的两个集合进行cross计算得到所有维度所构成集合的定义域,然后再与两个度量名称集合的cross的结果进行cross计算,即

当然这只是一种选择,也可以将所有的计算都改为cross操作。但这种行为对可视化视图呈现信息的有效性的提升的意义不大同时伴随较高的性能开销。

而实际上,从事图上看是在行与列的维度上生成了两颗维度树。但在完整的聚合运算时,可以将所有的维度放在一起构建了一颗完成的聚合树(抛弃掉空间位置的信息),然后根据视图在这颗聚合树上查询即可。这是为了最大化聚合计算的效率的一种做法。

这里可以参考fast-pivot的二维数据透视表的实现。

关于丢失掉的空间信息,需要理解cross操作与nest操作的本质区别。cross实际上保留了维度成员的在空间上分布的信息,而nest则牺牲掉了这部分信息的一种脱离原有维度空间概念的更抽象的产物。

借助下图来辅助理解一下cross(*)与nest(/)的区别:

image.png

可以看到,cross操作是计算每个维度成员的所有可能组合,而nest运算则是计算维度成员在实际数据中存在的组合。

接下来结合下面的案例来理解nest相比cross计算所丢失的那部分信息。

这里我们有一个数据集描述各个城市在某些时间段的人口数据。我们先选择城市作为维度,2000年的人口作为度量进行可视化大致了解一下数据的特性。

figure5.3.png

(注意到图5.3中有些城市会对应两个点;这是因为在历史上,美国的一些城市名称是仿照其他地方的城市名取得。)

在图5.4中将这些城市分为两组,美国与其他;借助分组,可以得到一张表意更为准确的可视化:
figure5.4.png

这张图是将三个维度变量集合进行cross操作得到的三维可视化,将group维度转化为切面,使得它们可以被绘制在一张平面上。

可以发现图中芝加哥看起来十分突兀,这是由于其人口数量较大导致的;但我们期望将这些城市以有序的形态展示出来,然而代数表达式并不允许我们这样做,这是由于group已经与其他字段进行了cross操作;两个切面是共享一个城市顺序的,其中一个有序则会导致另一个无序;这时就需要nest操作了。

如下图所示,nest操作可以视为将cross操作的得到的结果删减部分结果得到的。

nextorigin.png

这次,我们使用city/group*pop来制作图表

figure5.5.png

这时由于我们使用了nest计算,原有的空间结构被破坏掉,变为相对抽象的结构,就如同之前所示的图中,cross操作仍保留了二维的矩阵信息,但nest操作则将二维的矩阵转化为了一个一维的向量。

举一个连续型度量的例子,如下图所示。
image.png
如图所示,nest操作是基于事实表数据作为条件限制,在domain(X)与domain(Y)的交集上在进行筛选的子集。

从计算效率上的差异则是显而易见的。两个离散型维度cross操作计算出的结果是将两个集合进行笛卡尔积,得到两个集合中所有元素的组合。而nest操作则是得到所有在实际数据中存在的组合。举一个夸张的例子,如果将用户id与用户名(假设大家的用户名都不一样)进行cross,那么对于n条数据,会产生一个n^2的结果集合。而nest操作则仍会得到用户id与用户名一一对应组合的n条记录。

借助该思想也可以对OLAP算法进行优化,可以参考Pivot Table的实现与优化

2.3 度量共轴

思考:如何在同一坐标轴中展示多个度量?

正常情况下,由于度量之间的含义不确定,单位不一定统一,所以我们默认在增加新的度量时会将不同的度量放在不同的分面中展示。但有时需要将多个度量展示同一坐标轴下,这时可以借助度量名称和度量值分别作为维度和度量来生成相应的图表。
数据处理方式如下

其中

这种处理方式还可以带来额外的便利,如当用户只拖拽了一个度量的情形下(未定义维度),可以转换成度量名与度量值得方式进行可视化。即 (注,这时函数是有定义域的,可以理解为维度是MeasureName)

3. 对聚合树上的叶子节点定义标度(scale)

接下来会对维度集的定义域中的每一个元素调整对应的度量的标度,以方便后续对度量值进行一些计算操作。这一部分有两点比较重要,一是选取特定的标度转化函数来调整自己的数据,二是考虑是否统一不同分面之间的标度,或统一哪些标度。

一般可以提供一些简单的线性转化函数、归一化函数、指数/对数函数等来将我们原有的值域映射到一个新的值域上。这里就不详述每种转化函数的作用与使用场景了,系统设计时可以提供一些常见的函数供用户选择。在刻度线的设置上,可以参考The Grammar of Graphics刻度优化部分给出的评分算法。

统一标度是一个重要的功能,对于多分面的可视化,为了使得表意相同,我们需要对维度进行标度的统一。对于度量而言,则要根据实际情况做决定。统一度量的标度可以使得用户更准确的比较两个度量的大小情况,但前提是他们的单位是一致的。

用户也可以在这里尝试设置不同的标度类型以方便系统可以进行针对每种标度类型的特性运用不同的分析方式。

4. 确定视图的几何对象

根据我们之前计算的公式:

其中可以为空集。这时会产生三类最小切片视图。

4.1 确认最小切片

可视化最小切片可以分为三类

  • 离散-离散
  • 离散-连续
  • 连续-连续

    4.1.1离散-离散

    离散与离散类型所展示的结果是两个维度之间的关系,也可以理解为定义域A与定义域B结合生成的有实际含义的定义域A,B。
    image.png

4.1.2 离散-连续

离散-连续:这种可视化常常用来描述一个自变量与因变量的关系。其所展示的结果是每个离散型维度的定义域中的值对应的连续型变量经过聚合(来保证一个自变量只有唯一的因变量取值)后的值。
image.png

4.1.3 连续-连续

连续-连续:这种可视化常常用来研究两个变量之间的相关性。如果用自变量和因变量解释,如果一个连续型变量是维度

  • 两个都是维度
  • 一个维度一个度量
  • 两个度量

两个都是连续型维度的情况如下图所示,展示两个维度的定义域。

一个维度一个度量的情况就是典型的函数的定义:
image.png
对于两个度量的情况,如果用自变量和因变量解释,就是研究两个因变量之间的关系。但这会比较有迷惑性,因为当一个因变量取某个值的时候,是无法确定另一个因变量的取值的。所以这类可视化中会隐藏了一个额外的潜在的自变量。对于下图的两种情况,我们可以给出两个不同的默认维度:

  • 前者自变量为一个常量维度:
  • 后者自变量为每一条记录的唯一索引:

image.png
另一种理解方式:一旦存在自变量,就需要有唯一的因变量与之对应(否则就是自变量的数量不够),这时也就要求在计算因变量的取值时,需要根据自变量对所有事实表中的因变量取值进行聚合得到唯一的聚合值,当用户没有指定自变量时,可以将所有的实时表数据进行聚合得到数据的整体情况。当然用户也可以选择取消聚合(即允许一个自变量拥有多个潜在的因变量取值)获得散点图。

为了进一步理解该问题,可以参照下图的可视化。前者为根据用户指定的维度进行聚合,后者为根据用户指定维度进行分组但不聚合。
image.png

确认最小切片是一个纯计算的过程,此时还不涉及渲染相关工作,之前所举的例子只是为了方便理解,但实际上还并没有发生任何和渲染相关的工作。

2.3 多度量的情况

参考之前公式的最后一项,可以看到可视化时对度量的特殊处理

这里表示取行上所有的度量组成一个维度,列上所有度量组成一个维度(若集合为空,则抛弃该维度),进行cross操作。其视觉效果如下图所示

image.png
对于其他维度,则按照nest方法构建维度树即可。

这里给出各种情况的可视化最小分面的样子:

  • 最小分面取中的最后一个由所有维度的某一个成员构成的元组
  • 最小分面取中最后一个维度的成员与M的cross
  • 最小分面成员为两个度量集的cross,如上面的案例所示。



4.2 理解几何对象

这里以interval为例。interval的具体含义是在一个方向是为定值,另一个方向上接受一个自定义的数值,从0到该数值之间的区间构成的矩形为interval。

以G2为例,g2中的interval是默认了数值的映射为y轴上的高度。所以当用户想要通过直接切换position中定义的两个变量的位置来实现转置时,则会得到预期之外的图形。

1
2
chart.interval().position('year*sales');
chart.interval().position('sales*years');

image.png
(这时因为g2的interval默认会把position第二个字段作为interval的y方向上的高度值)

当然对于point,line等几何对象则不存在该问题。如果我们想要开发能够自动识别interval方向的组件,需要内置额外的判定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
class BarChart {
public container: any;
private geoms: any; // selection
private xScale: any;
private yScale: any;
private dataSource: any[];
private padding: [number, number, number, number];
private width: number;
private height: number;
constructor(props: BarProps) {
// init
}

public data(data: any[]): void {
// apply data
}
public xField(field: string, type: "ordinal" | "quantitative") {
if (type === "ordinal") {
this.xScale = d3.scaleBand()
.domain(this.dataSource.map(item => item[field]))
.range([0, this.width - this.padding[1] - this.padding[3]])
.paddingInner(0.1)
.paddingOuter(0.5);
this.geoms
.attr("x", d => {
return this.padding[3] + this.xScale(d[field]);
})
.attr("width", d => {
return this.xScale.bandwidth();
});
} else {
this.xScale = d3.scaleLinear()
.domain([0, Math.max(...this.dataSource.map(item => item[field]))])
.range([0, this.width - this.padding[1] - this.padding[3]]);
this.geoms
.attr("x", d => {
return this.padding[3];
})
.attr("width", d => {
return this.xScale(d[field]);
});
}
const xAxis = d3.axisBottom(this.xScale);
// const xAxis = this.container.append("g");
xAxis(this.container.append("g")
.attr("transform", `translate(${this.padding[3]}, ${this.height - this.padding[2]})`));
}
public yField(field: string, type: "ordinal" | "quantitative") {
if (type === "quantitative") {
this.yScale = d3.scaleLinear()
.domain([0, Math.max(...this.dataSource.map(item => item[field]))])
.range([this.height - this.padding[0] - this.padding[2], 0]);
this.geoms
.attr("y", d => {
return this.padding[0] + this.yScale(d[field])
})
.attr("height", d => {
return this.height - this.padding[2] - this.padding[0] - this.yScale(d[field]);
});
} else {
this.yScale = d3.scaleBand()
.domain(this.dataSource.map(item => item[field]))
.range([this.height - this.padding[0] - this.padding[2], 0])
.paddingInner(0.1)
.paddingOuter(0.5);
this.geoms
.attr("y", d => {
return this.padding[0] + this.yScale(d[field]);
})
.attr("height", d => {
return this.yScale.bandwidth();
});
}
const yAxis = d3.axisLeft(this.yScale);
// const xAxis = this.container.append("g");
yAxis(this.container.append("g")
.attr("transform", `translate(${this.padding[3]}, ${this.padding[0]})`));
}
}

然而这种实现存在一个典型的错误,即将两个方向上都设置为可接收连续型度量,则会产生下图所示的错误的可视化结果。
image.png
当两个方向上都是连续型度量时,必须制定其中一个度量为维度,另一个度量为度量。这样才满足interval所代表的分析含义。此时选择度量映射到interval的长度上。
image.png

4.3 Collision Modifiers (碰撞调整器)

碰撞调整器用于调整在图形绘制的时候出现的图形位置冲突覆盖的问题[1]。

  • Stack: symmetric, asymmetric(堆叠图的调整方法)
  • Dodge: symmetric, asymmetric (分组图的调整方法,把覆盖的图形偏移一定的位置)
  • jitter: uniform, normal (随机偏移)

系统在设计时可以帮助用户默认采用stack的方式,同时允许用户自定义开启/关闭/调整碰撞调整机制。
image.png

5. 统计(聚合)计算

根据维度集定义域中的元素(取值),结合调整标度后的度量,根据提供的聚合函数计算聚合后的值:

借助core-cube,只需传入对应的聚合函数即可:

1
node.aggData(aggFunc)

这里要讨论的一个重点便是区别聚合运算与指标计算,同时要确认他们的计算顺序。

简单的计算指标可以当做在原有数据的基础上增加新的列(变量)产生的。此类计算可以放在整个计算链路的第一部进行。

但也有一些指标难以定义他们的聚合方式,这往往是由指标计算过程中信息丢失造成的。如我们将中位数设为指标、那么两个子集并集的中位数是没办法根据两个子集的中位数求得的。这个时候就要在聚合时保留指标计算所依赖的原始信息。先将原始字段聚合(保证信息不丢失)后,在计算对应的指标。这部分的设计上可以参考cube-core的设计,cube-core将聚合树上每个节点对应的原始数据都保留了下来,以支持任意的指标聚合计算,但为了保留这些信息,也不得不牺牲了一定的性能。

6. 应用坐标系

使用g2的坐标系方法,只需传递坐标系类型即可完成快速切换

1
chart.coord(coord)

7.定义视觉通道

无论是d3还是g2,都提供了很方便的api使得可以将变量(字段)直接映射到某个视觉通道上。

1
2
3
4
geom.color(color)
geom.opacity(opacity)
geom.size(size)
geom.shape(shape)

但是,不同的视觉通道上的感知准确度存在着差异,可以参考下面的两张图片,比较每张图片中两个图形的大小。
image.png

(图片取自”The Future of Data Visualization” - Jeffrey Heer (Strata + Hadoop 2015))
实际上,这两张图片中两个图形的面积比是相同的,都是7倍,但人眼的视觉感知完全不一样。视觉通道感知准确度的差距可以在可视化推荐时为我们提供灵感。
以下是各个视觉感通道知准确度的比较[1]:
image.png

8. 渲染

结论

正确理解维度与度量的含义是对多维数据集进行探索分析的关键。这使得系统能够真正从逻辑上统一各种情况,并对未见过的情况作出正确的判断。相比于完全基于经验的设计方法,本文给出的方法要更灵活简介一些,在系统实现时能大幅度减少代码量,统一各种情况,抛弃掉不必要的分类讨论。本文对可视化计算链路给出了一般性的公式,基于此,可以为后续进行insights与智能图标推荐方面的研究打下基础。

参考文献

[1] L. Wilkinson and G. Wills, The Grammar of Graphics. (2nd;2. Aufl.;Second; ed.) 2005;2006;. DOI: 10.1007/0-387-28695-0.

[2] C. Stolte, D. Tang and P. Hanrahan, “Polaris: a system for query, analysis, and visualization of multidimensional relational databases,” IEEE Transactions on Visualization and Computer Graphics, vol. 8, (1), pp. 52-65, 2002.

[3] Stevens S S. On the theory of scales of measurement[J]. 1946.

[4] B. Tang et al, “Extracting top-K insights from multi-dimensional data,” in 2017, . DOI: 10.1145/3035918.3035922.

[5] T. Sellam, E. Müller and M. Kersten, “Semi-automated exploration of data warehouses,” in 2015, . DOI: 10.1145/2806416.2806538

[6] Kosslyn, Stephen Michael. “Elements of graph design”. WH Freeman, 1994.

Author: Lobay Kanna
Link: http://lobay.moe/2019/06/17/GoG/canary-base-theory/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.