大数据可视化中容易犯的错误(翻译)

Common Plotting Pitfalls that get Worse with large data

说在前面

在看"Mastering Python for Finance"这本书的时候,做者推荐了一个收集有趣的Jupyter notebook的集合,其中涵盖的门类很是广。python

其中由James A. Bednar撰写的一篇文章,介绍在可视化大量数据的时候容易犯的错误。利用Jupyter notebook直观并且容易分享和展现的特色,Bednar直观地讨论了这些很容易掉入,但不容易意识到的陷阱。app

原文连接dom

在利用可视化(Visualization)方法来从数据(一般极大量,以致于咱们不可能一个个地分析每一个数据点)中获取信息的过程当中,咱们可能会沉浸在绘制的细节中,从而过快地下结论,而不对这些结论产生应有的怀疑。可是事实上,咱们每每由于某些绘图参数的偶然性,进入到一些陷阱中,从而产生了错误的观察和错误的结论。了解这些常见的陷阱,合理地怀疑和推敲本身的结论,才能挖掘数据中的金子,而不是垃圾。函数

原文开始

咱们会在这篇文章中讨论:大数据

  1. Overplotting(过密绘图)
  2. Oversaturation(过饱和)
  3. Undersampling(采样不足)
  4. Undersaturation(欠饱和)
  5. Underutilized range(未充分利用灰度范围)
  6. Nonuniform colormapping(非均匀颜色映射)

,以及如何减轻或者消除他们。spa

依赖安装

咱们将会须要这些包,能够运行下面的命令安装:翻译

conda install -c bokeh -c ioam holoviews colorcet matplotlib scikit-image

1. Overplotting

咱们首先未来自两个不一样来源的数据集绘制在同一坐标系下--A图中的点和B图中的点。出人意料的是,先画A仍是先画B,咱们看到的结果很是不一样。code

def blues_reds(offset=0.5,pts=300):
    blues = (np.random.normal( offset,size=pts), np.random.normal( offset,size=pts), -1*np.ones((pts)))
    reds  = (np.random.normal(-offset,size=pts), np.random.normal(-offset,size=pts),  1*np.ones((pts)))
    return hv.Points(blues, vdims=['c']), hv.Points(reds, vdims=['c'])

blues,reds = blues_reds()
blues + reds + reds*blues + blues*reds

clipboard.png

C图和D图显示的是同一组数据,可是给人大相径庭的印象:在C图中蓝色的点显得更多,而在D图中红色的点显得更多。单凭C图或者D图,咱们就会获得错误的结论。实际上两种点一样多,这里在做祟的是occlusion(闭塞)。orm

一个数据集被另外一个数据集闭塞时,叫作overplotting(过密绘图,没有找到翻译,译者本身创造的)或者overdrawing,任什么时候候咱们将一个点或者一条线叠加在另外一个点或者另外一条线上,均可能发生。不只是散点图,曲线图、3D曲面图等等都会出现occlusion。排序

2. Oversaturation

能够经过减少alpha值来减轻过密绘图。alpha是绘图软件上提供的控制透明度的参数,例如:若是alpha是0.1,当十个数据点重合的时候,涂色达到饱和。这样以来,绘图顺序的影响会变小,可是看清每一个点变得更加困难。

%%opts Points (s=50 alpha=0.1)
blues + reds + reds*blues + blues*reds

clipboard.png

这里,C和D看起来很是类似(也理应如此,由于分布是同样的),可是在一些地方(有超过十个点重合的地方),仍是有过饱和(oversaturation)问题。在这幅图里,过饱和出如今中间。检测过饱和的惟一可靠方法是透过绘制两个版本,而后比较,或者依靠检查像素值来看是否有达到饱和的像素(必要可是不充分的条件)。设置了alpha以后,当不少个点重合在一块儿,只有最后被绘制上去的十个点会影响最终的颜色(alpha=0.1)。

坏消息是,正确的alpha值和每一个数据集有关。若是一个图中较多点重合在某个特定的区域,手动调参获得的alpha值可能仍是会失实描述这个数据集。

%%opts Points (alpha=0.1)
blues,reds = blues_reds(pts=600)
blues + reds + reds*blues + blues*reds

clipboard.png

例如这里,C和D仍是看起来不一样,可是他们本该是很类似地。既然可视化数据的意义是研究数据集,那么必须根据数据集的特性,手动找出某个参数是一个很是很差的迹象。

%%opts Points (s=10 alpha=0.1 edgecolor=None)
blues + reds + reds*blues + blues*reds

clipboard.png

即使是对于很小的数据集,找到正确的数据点面积和alpha参数也不容易。对于愈来愈大的未知性质数据集,咱们更加容易犯这些错误而没意识到。

3. 采样过疏(undersampling)

咱们如今改变一下数据,换成一个单一种类的数据集。单一种类的数据集中,过饱和会模糊密度上的区别。举个例子,当alpha等于0.1,10个、20个、2000个同一种类(同一颜色)的点重合在一块儿,他们看起来是彻底同样的。

咱们生成两个中心离开一点距离的正态分布,而后把他们叠加在一块儿,且不区分两个种类(全部的点都用黑色标记)。

%%opts Points.Small_dots (s=1 alpha=1) Points.Tiny_dots (s=0.1 alpha=0.1)

def gaussians(specs=[(1.5,0,1.0),(-1.5,0,1.0)],num=100):
    """
    A concatenated list of points taken from 2D Gaussian distributions.
    Each distribution is specified as a tuple (x,y,s), where x,y is the mean
    and s is the standard deviation.  Defaults to two horizontally
    offset unit-mean Gaussians.
    """
    np.random.seed(1)
    dists = [(np.random.normal(x,s,num), np.random.normal(y,s,num)) for x,y,s in specs]
    return np.hstack([d[0] for d in dists]), np.hstack([d[1] for d in dists])
    
hv.Points(gaussians(num=600),   label="600 points",   group="Small dots") + \
hv.Points(gaussians(num=60000), label="60000 points", group="Small dots") + \
hv.Points(gaussians(num=600),   label="600 points",   group="Tiny dots")  + \
hv.Points(gaussians(num=60000), label="60000 points", group="Tiny dots")

clipboard.png

参数依然致使结果很不稳定。对于600个数据点,小点(0.1面积,alpha=1)效果很好,可是大的数据集,overplotting问题会模糊分布B的形状和密度;微小点(比小点小十倍,alpha=0.1)在大数据集D中表现很好,可是在C中很是的差,几乎一个点都看不到。

凭借现有的绘图软件地计算力,随着数据集愈来愈大,绘制所有数据的散点图会在某一个时刻变得不可能。为了解决,人们有时候简单地抽样出一万个点以后绘制。可是在A图中能看到,若是一个分布很不幸地被undersample了,咱们有可能看不出形状。要让分布的形态可见,必需要有足够的数据点在稀疏的区域,和正确的绘图参数,但这须要反复试错。

研究者有时使用热力图而不是散点图。一个热力图有若干个固定大小的网格。热力图不限制数据点的总理。热力图实际上逼近给定空间内的几率密度函数。
更粗糙的热力图将噪音抹掉来展现真实的分布,而更精细的热力图能够展现更多的细节。

咱们来看看一组表达同一个分布地热力图,惟一不一样的网格的数量(number of bins)。

def heatmap(coords,bins=10,offset=0.0,transform=lambda d,m:d, label=None):

    hist,xs,ys  = np.histogram2d(coords[0], coords[1], bins=bins)
    counts      = hist[:,::-1].T
    transformed = transform(counts,counts!=0)
    span        = transformed.max()-transformed.min()
    compressed  = np.where(counts!=0,offset+(1.0-offset)*transformed/span,0)
    args        = dict(label=label) if label else {}
    return hv.Image(compressed,bounds=(xs[-1],ys[-1],xs[1],ys[1]),**args)

hv.Layout([heatmap(gaussians(num=60000),bins) for bins in [8,20,200]])

clipboard.png

A过于粗糙,不能准确地描述分布。有足够多网格地C,逼近散点图(上图的D)。有中等数量网格的B能够磨平采样过疏;B实际上比C更忠实于数据,可是C更好的表明了抽样(译者也没明白这句话)。因此寻找一个合适的热力图网格大小须要一些经验,对比多个不一样网格大小的热力图能够提供一些帮助。至少,网格大小这个参数是一个有意义的东西,在数据而不属于绘图的细节--好比点有多透明,这些东西每每是被随机肯定下来的。

原则上来说,热力图方法能够彻底避免以上三个陷阱:

  • overplotting (过密绘图):取一个网格内的点的算术和,一个点不会模糊另外一个点
  • oversaturation (过饱和):最大和最小的计数会被自动地分配到可视灰度区间的两端
  • undersampling(采样过疏):画出来的图的大小不依赖于总的数据点的多少,咱们能够有无限多的数据输入

4. Undersaturation

固然,热力图有本身的缺陷。热力图和附带alpha的散点图都会有,又不多被意识到的一个问题是undersaturation。在这个问题出现时,大量的数据点可能会被忽略,由于它们要么分布在很是广大的网格中,或者在不少几乎透明的散点中。咱们来看看一组有着不一样的分散度(标准差)的高斯分布:

dist = gaussians(specs=[(2,2,0.02), (2,-2,0.1), (-2,-2,0.5), (-2,2,1.0), (0,0,3)],num=10000)
hv.Points(dist) + hv.Points(dist)(style=dict(s=0.1)) + hv.Points(dist)(style=dict(s=0.01,alpha=0.05))

clipboard.png

A,B,C图画的是同一组数据的:5个不一样中心,不一样标准差的高斯分布:

  1. 中心(2,2):极窄分布
  2. 中心(2,-2):窄分布
  3. 中心(-2,-2):中等分布
  4. 中心(-2,2):散分布
  5. 中心(0,0):极散分布

在A图中,最散的分布(分布5)覆盖了一切,咱们彻底看不到任何的结构。B和C好一点,但仍是不能使人满意。在B中有四个明显的高斯分布,除了最大的以外都看起来有着相同的密度,最窄的分布的乎看不到。而后,咱们尝试改变alpha,获得C。undersaturation出现了:最离散的高斯分布彻底消失了!若是咱们只看C,就会弄丢这个分布。

那么热力图能够幸免欠饱和吗?

hv.Layout([heatmap(dist,bins) for bins in [8,20,200]])

clipboard.png

很是窄的分布,表现为一些有着很是高计数的网格。由于色谱和数轴的映射是线性的,其余的网格比起来实在是太淡了。C甚至成了一篇雪白。

为了不欠饱和,你能够增长一个补偿来确保低计数(但非零)的网格被映射为一个可视颜色,剩下的色谱区间用来表达计数的差别。

hv.Layout([heatmap(dist,bins,offset=0.2) for bins in [8,20,200]]).cols(4)

clipboard.png

欠饱和被完美地消除了:像素要么是零(纯白色),要么是一个咱们选定的非背景颜色。最大的高斯分布能够被清楚地看见。

但是,五个分布不一样的高斯的结构仍是不能被辨别出来。A太粗糙了,B也过于粗糙。C的粗糙度合适,可是它看来像是只有一个最大的高斯分布,而不是五个。

5. (未利用色谱区间)Underutilized range

为何C呈现这个样子呢?这个陷阱更微妙:五个高斯分布之间,数据点密度的区别难以被眼睛捕捉,由于几乎全部的像素,要么在可视区间的底部(浅灰色),要么是在顶部(纯黑色)。其余的可视区间直接被丢弃了,彻底没被利用起来!丰富内在的结构没有被传递出来。若是若是数据点均匀地分布在0到10000区间中,那么上面的图没有问题,但现实中不多是这样。

因此,咱们应该将计数转换成某些更好的,能视觉上表达计数的相对区别的指数。这个指数应该能保存计数的相对区别,可是又能将他们映射在整个可视区间内。对数转换是一个选择:

hv.Layout([heatmap(dist,bins,offset=0.2,transform=lambda d,m: np.where(m,np.log1p(d),0)) for bins in [8,20,200]])

clipboard.png

很棒!C里面,咱们能够清楚看到细节。五个高斯分布不一样的离散度在C中很清楚。

还有一个疑问:为何是对数转换?对数转换奏效,其实跟咱们的标准差大体遵循一个等比数列有关。那么对于更大的,未知结构的数据,有一些指引咱们转换的原则吗?

有。咱们换个思路,其实在绘制这个数据集的时候,咱们遇到的困难源自每一个网格里的计数差距过大, 从10000(很是窄)到1(很是离散)。普通的显示器只有256个灰度,并且人类能感知不一样灰度的能力是有限的,若是把数据直接映射到灰度上,效果不会很好。既然咱们已经在上面的方法中抛弃了直接映射,而用了对数转换来克服欠饱和,那咱们能不能彻底摈弃数值映射这个思路,而使用相对排序呢?这样的图会保留顺序(order)而不是量度(magnitude)。假设显示器有100个灰度,最低的1%的网格会被分配第一个可视的灰度,下一个1%会被分配到下一个可视的灰度,……,最高的1%会被分配到灰度255(黑色)。真实的数值会被忽略掉,可是他们的相对量仍然会决定他们在屏幕上显示什么颜色。因此,分布的结构而不是数值被保存下来。

使用于图像处理包中的histogram equalization function,每一个灰度大约会有一样数量的像素:

try:
    from skimage.exposure import equalize_hist
    eq_hist = lambda d,m: equalize_hist(1000*d,nbins=100000,mask=m)
except ImportError:
    eq_hist = lambda d,m: d
    print("scikit-image not installed; skipping histogram equalization")
    
hv.Layout([heatmap(dist,bins,transform=eq_hist) for bins in [8,20,200]])

clipboard.png

C图如今很完美:五个高斯分布清晰可见,并且咱们没有使用任何随意肯定的参数。固然,咱们也丢失了原始的计数。

6. 不均匀颜色映射

每一个热力图须要一个颜色映射,也就是,一个从数值查询像素颜色的表。可视化的目的是解释数据的特性,为了这一点咱们须要挑选可让观察者客观地感知数据的颜色映射。不幸的是,绘图程序大多数经常使用的颜色映射都是不均匀的(也是很差的)。

好比,在’jet'(2015年前matlab和maplotlib使用的默认颜色映射)中,很大一部分的数值会落在绿色中很难区分的一段,在‘hot'中,落在黄色中很难区分的一段。

来看看咱们以前用的数据,在两个不一样的颜色映射下,有什么区别:

import colorcet
hv.Layout([heatmap(dist,200,transform=eq_hist,label=cmap)(style=dict(cmap=cmap)) for cmap in ["hot","cet_fire"]]).cols(2)

clipboard.png

很明显,cet_fire的表达能力更强,也更精准地表现出区块之间的密度差别。hot将全部的高密度区域都映射到亮黄色/白色中难以在官能上区分的一段,给咱们一种“过饱和”的感受(但咱们的绘制原理其实应该确保了过饱和不会出现)。幸运的是,各个语言中都有很多均匀颜色映射:colorcet包中的50多个,或者matplotlib自带的四个(viridis,plasma,inferno,magma),或者Matlab的Parula

完。

【翻译能力有限,错误在所不免,欢迎指出】

相关文章
相关标签/搜索