设为首页收藏本站

EPS数据狗论坛

 找回密码
 立即注册

QQ登录

只需一步,快速开始

查看: 1883|回复: 0

[数据可视化] 大数据可视化中容易犯的错误

[复制链接]

29

主题

236

金钱

388

积分

入门用户

发表于 2019-7-8 14:00:49 | 显示全部楼层 |阅读模式

主要讨论:
Overplotting(过密绘图)
Oversaturation(过饱和)
Undersampling(采样不足)
Undersaturation(欠饱和)
Underutilized range(未充分利用灰度范围)
Nonuniform colormapping(非均匀颜色映射)

依赖安装
我们将会需要这些包,可以运行下面的命令安装:
  1. conda install -c bokeh -c ioam holoviews colorcet matplotlib scikit-image
复制代码


1. Overplotting
我们首先将来自两个不同来源的数据集绘制在同一坐标系下--A图中的点和B图中的点。出人意料的是,先画A还是先画B,我们看到的结果非常不同。
  1. def blues_reds(offset=0.5,pts=300):
  2.     blues = (np.random.normal( offset,size=pts), np.random.normal( offset,size=pts), -1*np.ones((pts)))
  3.     reds  = (np.random.normal(-offset,size=pts), np.random.normal(-offset,size=pts),  1*np.ones((pts)))
  4.     return hv.Points(blues, vdims=['c']), hv.Points(reds, vdims=['c'])

  5. blues,reds = blues_reds()
  6. blues + reds + reds*blues + blues*reds
复制代码

1.png
C图和D图显示的是同一组数据,但是给人截然不同的印象:在C图中蓝色的点显得更多,而在D图中红色的点显得更多。单凭C图或者D图,我们就会得到错误的结论。实际上两种点同样多,这里在作祟的是occlusion(闭塞)。

一个数据集被另一个数据集闭塞时,叫做 overplotting (过密绘图,没有找到翻译,译者自己创造的)或者 overdrawing ,任何时候我们将一个点或者一条线叠加在另一个点或者另一条线上,都可能发生。不仅是散点图,曲线图、3D曲面图等等都会出现occlusion。

2. Oversaturation
可以通过减小alpha值来减轻过密绘图。alpha是绘图软件上提供的控制透明度的参数,例如:如果alpha是0.1,当十个数据点重合的时候,涂色达到饱和。这样以来,绘图顺序的影响会变小,但是看清每个点变得更加困难。
  1. %%opts Points (s=50 alpha=0.1)
  2. blues + reds + reds*blues + blues*reds
复制代码

2.png
这里,C和D看起来非常相似(也理应如此,因为分布是一样的),但是在一些地方(有超过十个点重合的地方),还是有过饱和(oversaturation)问题。在这幅图里,过饱和出现在中间。检测过饱和的唯一可靠方法是透过绘制两个版本,然后比较,或者依靠检查像素值来看是否有达到饱和的像素(必要但是不充分的条件)。设置了alpha之后,当很多个点重合在一起,只有最后被绘制上去的十个点会影响最终的颜色(alpha=0.1)。

坏消息是,正确的alpha值和每个数据集有关。如果一个图中较多点重合在某个特定的区域,手动调参得到的alpha值可能还是会失实描述这个数据集。
  1. %%opts Points (alpha=0.1)
  2. blues,reds = blues_reds(pts=600)
  3. blues + reds + reds*blues + blues*reds
复制代码

3.png
例如这里,C和D还是看起来不同,但是他们本该是很相似地。既然可视化数据的意义是研究数据集,那么必须根据数据集的特性,手动找出某个参数是一个非常不好的迹象。
  1. %%opts Points (s=10 alpha=0.1 edgecolor=None)
  2. blues + reds + reds*blues + blues*reds
复制代码

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

3. 采样过疏(undersampling)
我们现在改变一下数据,换成一个单一种类的数据集。单一种类的数据集中,过饱和会模糊密度上的区别。举个例子,当alpha等于0.1,10个、20个、2000个同一种类(同一颜色)的点重合在一起,他们看起来是完全一样的。

我们生成两个中心离开一点距离的正态分布,然后把他们叠加在一起,且不区分两个种类(所有的点都用黑色标记)。
  1. %%opts Points.Small_dots (s=1 alpha=1) Points.Tiny_dots (s=0.1 alpha=0.1)

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

5.png

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

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

研究者有时使用热力图而不是散点图。一个热力图有若干个固定大小的网格。热力图不限制数据点的总理。热力图实际上逼近给定空间内的概率密度函数。

更粗糙的热力图将噪音抹掉来展示真实的分布,而更精细的热力图可以展示更多的细节。

我们来看看一组表达同一个分布地热力图,唯一不同的网格的数量(number of bins)。
  1. def heatmap(coords,bins=10,offset=0.0,transform=lambda d,m:d, label=None):

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

  9. hv.Layout([heatmap(gaussians(num=60000),bins) for bins in [8,20,200]])
复制代码

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

原则上来讲,热力图方法可以完全避免以上三个陷阱:
overplotting (过密绘图):取一个网格内的点的算术和,一个点不会模糊另一个点
oversaturation (过饱和):最大和最小的计数会被自动地分配到可视灰度区间的两端
undersampling(采样过疏):画出来的图的大小不依赖于总的数据点的多少,我们可以有无限多的数据输入

4. Undersaturation
当然,热力图有自己的缺陷。热力图和附带alpha的散点图都会有,又很少被意识到的一个问题是undersaturation。在这个问题出现时,大量的数据点可能会被忽略,因为它们要么分布在非常广大的网格中,或者在很多几乎透明的散点中。我们来看看一组有着不同的分散度(标准差)的高斯分布:
  1. dist = gaussians(specs=[(2,2,0.02), (2,-2,0.1), (-2,-2,0.5), (-2,2,1.0), (0,0,3)],num=10000)
  2. hv.Points(dist) + hv.Points(dist)(style=dict(s=0.1)) + hv.Points(dist)(style=dict(s=0.01,alpha=0.05))
复制代码

7.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,就会弄丢这个分布。
那么热力图可以幸免欠饱和吗?
  1. hv.Layout([heatmap(dist,bins) for bins in [8,20,200]])
复制代码

8.png
非常窄的分布,表现为一些有着非常高计数的网格。因为色谱和数轴的映射是线性的,其他的网格比起来实在是太淡了。C甚至成了一篇雪白。

为了避免欠饱和,你可以增加一个补偿来确保低计数(但非零)的网格被映射为一个可视颜色,剩下的色谱区间用来表达计数的差异。
  1. hv.Layout([heatmap(dist,bins,offset=0.2) for bins in [8,20,200]]).cols(4)
复制代码

9.png

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

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

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

所以,我们应该将计数转换成某些更好的,能视觉上表达计数的相对区别的指数。这个指数应该能保存计数的相对区别,但是又能将他们映射在整个可视区间内。对数转换是一个选择:
  1. 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]])
复制代码

10.png
很棒!C里面,我们可以清楚看到细节。五个高斯分布不同的离散度在C中很清楚。

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

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

使用于图像处理包中的histogram equalization function,每个灰度大约会有同样数量的像素:
  1. try:
  2.     from skimage.exposure import equalize_hist
  3.     eq_hist = lambda d,m: equalize_hist(1000*d,nbins=100000,mask=m)
  4. except ImportError:
  5.     eq_hist = lambda d,m: d
  6.     print("scikit-image not installed; skipping histogram equalization")
  7.    
  8. hv.Layout([heatmap(dist,bins,transform=eq_hist) for bins in [8,20,200]])
复制代码

11.png

C图现在很完美:五个高斯分布清晰可见,而且我们没有使用任何随意确定的参数。当然,我们也丢失了原始的计数。

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

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

来看看我们之前用的数据,在两个不同的颜色映射下,有什么区别:
  1. import colorcet
  2. hv.Layout([heatmap(dist,200,transform=eq_hist,label=cmap)(style=dict(cmap=cmap)) for cmap in ["hot","cet_fire"]]).cols(2)
复制代码

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

完。
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

关闭

站长推荐上一条 /1 下一条

客服中心
关闭
在线时间:
周一~周五
8:30-17:30
QQ群:
653541906
联系电话:
010-85786021-8017
在线咨询
客服中心

意见反馈|网站地图|手机版|小黑屋|EPS数据狗论坛 ( 京ICP备09019565号-3 )   

Powered by BFIT! X3.4

© 2008-2028 BFIT Inc.

快速回复 返回顶部 返回列表