图例可以将离散的点标示为离散的标签。对于建立在不同颜色之上的连续的值(点线面)来说,标注了的颜色条是非常方便的工具。Matplotlib 的颜色条是独立于图表之外的一个类似于比色卡的图形,用来展示图表中不同颜色的数值含义。本节内容中的所有带色彩的图都可以在(https://github.com/wangyingsm/Python-Data-Science-Handbook)中找到。我们还是首先导入本节需要的包和模块:
import matplotlib.pyplot as pltplt.style.use('classic')
import numpy as np
通过plt.colorbar函数可以创建最简单的颜色条,在本节中我们会多次看到:
x = np.linspace(0, 10, 1000)I = np.sin(x) * np.cos(x[:, np.newaxis])
plt.imshow(I)
plt.colorbar();plt.show()
我们下面来讨论如何个性化颜色条以及在不同的场合高效的使用它们。
自定义颜色条
颜色条可以通过cmap参数指定使用的色谱系统(或叫色图):
plt.imshow(I, cmap='gray');所有可用的色图都可以在plt.cm模块中找到;在 IPython 中使用 Tab 自动补全功能能列出所有的色图列表:
plt.cm.但是知道在哪里选择色图只是第一步:更重要的是在各种选项中选出合适的色图。这个选择比你预料的要微妙的多。
选择色图
在可视化方案中选择颜色完整的介绍说明超出了本书的范围,如果你对这个课题和相关内容有兴趣,可以参考文章["绘制更漂亮图表的 10 个简单规则"](http://journals.plos.org/ploscompbiol/article?id=10.1371/journal.pcbi.1003833)。Matplotlib 的在线文档也有一章关于色图选择的有趣讨论。
通常来说,你应该注意以下三种不同类型的色图:
- 序列色图:这类型的色谱只包括一个连续序列的色系(例如binary或viridis)。
- 分化色图:这类型的色谱包括两种独立的色系,这两种颜色有着非常大的对比度(例如RdBu或PuOr)。
- 定性色图:这类型的色图混合了非特定连续序列的颜色(例如rainbow或jet)。
jet色图,在 Matplotlib 2.0 版本之前都是默认的色图,是定性色图的一个例子。jet作为默认色图的位置其实有点尴尬,因为定性图通常都不是对定量数据进行展示的好选择。原因是定性图通常都不能在范围增加时提供亮度的均匀增长。
我们可以通过将jet颜色条转换为黑白来看到这点:
from matplotlib.colors import LinearSegmentedColormapdef grayscale_cmap(cmap):
"""返回给定色图的灰度版本"""
cmap = plt.cm.get_cmap(cmap) # 使用名称获取色图对象
colors = cmap(np.arange(cmap.N)) # 将色图对象转为RGBA矩阵,形状为N×4
# 将RGBA颜色转换为灰度
# 参考 http://alienryderflex.com/hsp.html
RGB_weight = [0.299, 0.587, 0.114] # RGB三色的权重值
luminance = np.sqrt(np.dot(colors[:, :3] ** 2, RGB_weight)) # RGB平方值和权重的点积开平方根
colors[:, :3] = luminance[:, np.newaxis] # 得到灰度值矩阵
# 返回相应的灰度值色图
return LinearSegmentedColormap.from_list(cmap.name + "_gray", colors, cmap.N)
def view_colormap(cmap):
"""将色图对应的灰度版本绘制出来"""
cmap = plt.cm.get_cmap(cmap)
colors = cmap(np.arange(cmap.N))
cmap = grayscale_cmap(cmap)
grayscale = cmap(np.arange(cmap.N))
fig, ax = plt.subplots(2, figsize=(6, 2),
subplot_kw=dict(xticks=[], yticks=[]))
ax[0].imshow([colors], extent=[0, 10, 0, 1])
ax[1].imshow([grayscale], extent=[0, 10, 0, 1])view_colormap('jet')
注意一下上面的灰度图中亮条纹的位置。即使在上述彩色图中,也出现了这种不规则的亮条纹,这会导致眼睛被区域中亮条纹所吸引,这很可能造成阅读者被不重要的数据集部分干扰了。更好的选择是使用类似viridis这样的色图(Matplotlib 2.0 后默认色图),它们被设计为有着均匀的亮度变化。因此它们无论是在彩色图中还是在灰度图中都有着同样的亮度变化:
view_colormap('viridis')如果你更喜欢彩虹方案,另一个好的选择是使用cubehelix色图:
view_colormap('cubehelix')对于其他的情况,例如某种正负分布的数据集,双色颜色条如RdBu(Red-Blue)会很常用。然而正如你从下面例子看到的,如果将双色颜色条转化为灰度的话,正负或两级的信息就会丢失:
view_colormap('RdBu')后面我们会看到更多使用这些色图的例子。
Matplotlib 中有大量可用的色图;要看到它们的列表,你可以使用 IPython 来探索plt.cm模块。要在 Python 中更加正规的使用颜色,你可以查看 Seaborn 库的工具和文档。
颜色限制和扩展
Matplotlib 允许你对颜色条进行大量的自定义。颜色条本身就是一个plt.Axes对象,因此所有轴和刻度定制的技巧都可以应用在上面。颜色条也有着一些有趣的自定义行为:例如,我们可以缩小颜色的范围并且通过设置extend参数将超出范围之外的数值展示为顶部和底部的三角箭头形状。这对于展示一些受到噪声干扰的数据时非常方便:
x = np.linspace(0, 10, 1000)I = np.sin(x) * np.cos(x[:, np.newaxis])
# 在I数组中人为生成不超过1%的噪声
speckles = (np.random.random(I.shape) < 0.01)
I[speckles] = np.random.normal(0, 3, np.count_nonzero(speckles))
plt.figure(figsize=(10, 3.5))
# 不考虑去除噪声时的颜色分布
plt.subplot(1, 2, 1)
plt.imshow(I, cmap='RdBu')
plt.colorbar()
# 设置去除噪声时的颜色分布
plt.subplot(1, 2, 2)
plt.imshow(I, cmap='RdBu')
plt.colorbar(extend='both')
plt.clim(-1, 1);
plt.show()
注意到在左边的图表中,默认的颜色阈值是包括了噪声的,因此整体的条纹形状都被噪声数据冲刷淡化了。而右边的图表,我们手动设置了颜色的阈值,并在绘制颜色条是加上了extend参数来表示超出阈值的数据。对于我们的数据来说,右图比左图要好的多。
离散颜色条
色图默认是连续的,但是在某些情况下你可能需要展示离散值。最简单的方法是使用plt.cm.get_cmap()函数,在传递某个色图名称的同时,还额外传递一个颜色分桶的数量值参数给该函数:
plt.imshow(I, cmap=plt.cm.get_cmap('Blues', 6))plt.colorbar()
plt.clim(-1, 1);
离散色图的使用方式和其他色图没有任何区别。
例子:手写数字
最后我们来看一个很有实用价值的例子,让我们实现对一些手写数字图像数据的可视化分析。这个数据包含在 Sciki-Learn 中,以供包含有将近 2,000 张8*8 大小的不同笔迹的手写数字缩略图。
首先,我们下载这个数据集,然后使用plt.imshow()将其中部分数据展示出来:
# 读取数字0-5的手写图像,然后使用Matplotlib展示头64张缩略图from sklearn.datasets import load_digits
digits = load_digits(n_class=6)
fig, ax = plt.subplots(8, 8, figsize=(6, 6))
for i, axi in enumerate(ax.flat):
axi.imshow(digits.images[i], cmap='binary')
axi.set(xticks=[], yticks=[])
plt.show()
因为每个数字都是使用 64 个像素点渲染出来的,我们可以认为每个数字是一个 64 维空间中的点:每个维度代表这其中一个像素的灰度值。但是要在图表中将这么高维度空间的联系可视化出来是非常困难的。有一种做法是使用降维技术,比方说使用流形学习来减少数据的维度然而不会丢失数据中有效的信息。
我们来看一下将这些手写数字图像数据映射到二维流形学习当中:
# 使用Isomap将手写数字图像映射到二维流形学习中from sklearn.manifold import Isomap
iso = Isomap(n_components=2)
projection = iso.fit_transform(digits.data)
我们使用离散颜色条来展示结果,设置ticks和clim来进一步美化结果的颜色条:
# 绘制图表结果plt.scatter(projection[:, 0], projection[:, 1], lw=0.1,
c=digits.target, cmap=plt.cm.get_cmap('cubehelix', 6))
plt.colorbar(ticks=range(6), label='digit value')
plt.clim(-0.5, 5.5)
plt.show()
我们从流形学习中的映射中可以观察到一些有趣现象:例如,图表中 5 和 3 有一些重叠的部分,这表示一些手写体中 5 和 3 是比较难以辨别的,因此对于自动识别算法来说这是比较容易混淆的部分。而 0 和 1,它们在图表中距离很远,这表示两者比较容易辨别,不太可能造成混淆。这个图表分析与我们的直觉一致,因为 5 和 3 显然比 0 和 1 看起来更加接近。