Python 数据科学手册第二版(五)
原文:
zh.annas-archive.org/md5/051facaf2908ae8198253e3a14b09ec1译者:飞龙
第四部分:Matplotlib 可视化
现在我们将深入研究 Python 中用于可视化的 Matplotlib 包。Matplotlib 是一个建立在 NumPy 数组上的跨平台数据可视化库,旨在与更广泛的 SciPy 栈配合使用。它由 John Hunter 在 2002 年构思,最初作为 IPython 的补丁,用于通过 IPython 命令行从gnuplot实现交互式 MATLAB 风格的绘图。当时,IPython 的创始人 Fernando Perez 正在忙于完成他的博士论文,没有时间几个月内审查该补丁。John 将此视为自己行动的信号,于是 Matplotlib 包诞生了,版本 0.1 于 2003 年发布。当它被采纳为太空望远镜科学研究所(背后是哈勃望远镜的人们)首选的绘图包,并得到财政支持以及大幅扩展其功能时,Matplotlib 得到了早期的推广。
Matplotlib 最重要的特点之一是其与多种操作系统和图形后端的良好兼容性。Matplotlib 支持数十种后端和输出类型,这意味着无论您使用哪种操作系统或希望使用哪种输出格式,它都能正常工作。这种跨平台、面面俱到的方法一直是 Matplotlib 的一大优势。它导致了大量用户的使用,进而促使了活跃的开发者基础以及 Matplotlib 在科学 Python 社区中强大的工具和普及率。
近年来,然而,Matplotlib 的界面和风格开始显得有些过时。像 R 语言中的ggplot和ggvis以及基于 D3js 和 HTML5 canvas 的 Web 可视化工具包,常使 Matplotlib 感觉笨重和老旧。尽管如此,我认为我们不能忽视 Matplotlib 作为一个经过良好测试的跨平台图形引擎的优势。最近的 Matplotlib 版本使得设置新的全局绘图样式相对容易(参见 第三十四章),人们一直在开发新的包,利用其强大的内部机制通过更清晰、更现代的 API 驱动 Matplotlib,例如 Seaborn(在 第三十六章 讨论),ggpy,HoloViews,甚至 Pandas 本身可以作为 Matplotlib API 的封装器使用。即使有了这些封装器,深入了解 Matplotlib 的语法来调整最终的绘图输出仍然经常很有用。因此,我认为即使新工具意味着社区逐渐不再直接使用 Matplotlib API,Matplotlib 本身仍将保持数据可视化堆栈中不可或缺的一部分。
第二十五章:Matplotlib 一般提示
在我们深入研究使用 Matplotlib 创建可视化的详细信息之前,有几个有用的事情您应该了解如何使用这个包。
导入 Matplotlib
正如我们使用np简写代表 NumPy 和pd简写代表 Pandas 一样,我们将使用一些标准缩写来导入 Matplotlib:
In [1]: import matplotlib as mpl
import matplotlib.pyplot as plt
我们将最常用的是plt接口,您将在本书的这一部分中看到。
设置样式
我们将使用plt.style指令为我们的图形选择合适的美学样式。在这里,我们将设置classic样式,确保我们创建的图使用经典的 Matplotlib 样式:
In [2]: plt.style.use('classic')
在本章中,我们将根据需要调整这种样式。有关样式表的更多信息,请参阅第三十四章。
显示还是不显示?如何显示您的图形
您看不到的可视化对您没有多大用处,但是您查看 Matplotlib 图形的方式取决于上下文。Matplotlib 的最佳用法因您如何使用它而异;大致上,适用的三种上下文是在脚本中使用 Matplotlib,在 IPython 终端中使用 Matplotlib 或在 Jupyter 笔记本中使用 Matplotlib。
从脚本绘图
如果您正在脚本中使用 Matplotlib,则函数plt.show是您的好帮手。plt.show启动一个事件循环,查找所有当前活动的Figure对象,并打开一个或多个交互窗口来显示您的图形或图形。
因此,例如,您可能有一个名为myplot.py的文件,其中包含以下内容:
# file: myplot.py
import matplotlib.pyplot as plt
import numpy as np
x = np.linspace(0, 10, 100)
plt.plot(x, np.sin(x))
plt.plot(x, np.cos(x))
plt.show()
然后,您可以从命令行提示符运行此脚本,将导致一个窗口打开,并显示您的图形:
$ python myplot.py
plt.show命令在幕后做了很多工作,因为它必须与系统的交互式图形后端进行交互。此操作的详细信息在不同的系统甚至不同的安装中可能会有很大的差异,但是 Matplotlib 会尽力隐藏所有这些细节。
有一件事需要注意:plt.show命令应该在每个 Python 会话中仅使用一次,并且最常见的情况是在脚本的最后。多个show命令可能会导致不可预测的基于后端的行为,并且应该尽量避免。
从 IPython Shell 绘图
Matplotlib 在 IPython shell 中也可以无缝运行(请参阅第 I 部分)。IPython 是构建用于与 Matplotlib 很好配合的,如果您指定 Matplotlib 模式。要启用此模式,可以在启动ipython后使用%matplotlib魔术命令:
In [1]: %matplotlib
Using matplotlib backend: TkAgg
In [2]: import matplotlib.pyplot as plt
此时,任何plt绘图命令都将导致一个图形窗口打开,并且可以运行进一步的命令来更新绘图。某些更改(例如修改已经绘制的线的属性)不会自动绘制:要强制更新,请使用plt.draw。在 IPython 的 Matplotlib 模式中不需要使用plt.show。
从 Jupyter 笔记本绘图
Jupyter Notebook 是一个基于浏览器的交互式数据分析工具,可以将叙述、代码、图形、HTML 元素等多种内容组合到一个可执行文档中(参见第 I 部分)。
在 Jupyter Notebook 中进行交互式绘图可以通过 %matplotlib 命令完成,其工作方式类似于 IPython Shell。您还可以选择直接在笔记本中嵌入图形,有两种可能的选项:
-
%matplotlib inline将导致您的图形以静态图像嵌入到笔记本中。 -
%matplotlib notebook将导致交互式图形嵌入到笔记本中。
对于本书,通常会使用默认设置,图形渲染为静态图像(见图 25-1 以查看此基本绘图示例的结果):
In [3]: %matplotlib inline
In [4]: import numpy as np
x = np.linspace(0, 10, 100)
fig = plt.figure()
plt.plot(x, np.sin(x), '-')
plt.plot(x, np.cos(x), '--');
图 25-1. 基本绘图示例
将图形保存到文件中
Matplotlib 的一个很好的特性是能够以多种格式保存图形。使用 savefig 命令可以保存图形。例如,要将前面的图形保存为 PNG 文件,可以运行以下命令:
In [5]: fig.savefig('my_figure.png')
现在我们在当前工作目录中有一个名为my_figure.png的文件:
In [6]: !ls -lh my_figure.png
Out[6]: -rw-r--r-- 1 jakevdp staff 26K Feb 1 06:15 my_figure.png
为了确认它包含我们认为包含的内容,让我们使用 IPython 的 Image 对象来显示此文件的内容(见图 25-2)。
In [7]: from IPython.display import Image
Image('my_figure.png')
图 25-2. 基本绘图的 PNG 渲染
在 savefig 中,文件格式根据给定文件名的扩展名推断。根据安装的后端程序,可以使用多种不同的文件格式。可以通过图形画布对象的以下方法找到系统支持的文件类型列表:
In [8]: fig.canvas.get_supported_filetypes()
Out[8]: {'eps': 'Encapsulated Postscript',
'jpg': 'Joint Photographic Experts Group',
'jpeg': 'Joint Photographic Experts Group',
'pdf': 'Portable Document Format',
'pgf': 'PGF code for LaTeX',
'png': 'Portable Network Graphics',
'ps': 'Postscript',
'raw': 'Raw RGBA bitmap',
'rgba': 'Raw RGBA bitmap',
'svg': 'Scalable Vector Graphics',
'svgz': 'Scalable Vector Graphics',
'tif': 'Tagged Image File Format',
'tiff': 'Tagged Image File Format'}
请注意,在保存图形时,不需要使用 plt.show 或前面讨论过的相关命令。
两个界面的价格
Matplotlib 的一个可能令人困惑的特性是其双界面:一个方便的基于状态的 MATLAB 风格界面和一个更强大的面向对象界面。在这里,我将快速介绍这两者之间的区别。
MATLAB 风格界面
Matplotlib 最初被构想为 MATLAB 用户的 Python 替代方案,其语法大部分反映了这一事实。MATLAB 风格的工具包含在 pyplot (plt) 接口中。例如,以下代码对 MATLAB 用户可能看起来非常熟悉(见图 25-3 显示的结果)。
In [9]: plt.figure() # create a plot figure
# create the first of two panels and set current axis
plt.subplot(2, 1, 1) # (rows, columns, panel number)
plt.plot(x, np.sin(x))
# create the second panel and set current axis
plt.subplot(2, 1, 2)
plt.plot(x, np.cos(x));
图 25-3. 使用 MATLAB 风格界面的子图
重要的是要认识到这种接口是有状态的:它跟踪“当前”图形和坐标轴,所有plt命令都应用于这些对象。您可以使用plt.gcf(获取当前图形)和plt.gca(获取当前坐标轴)来获取对这些对象的引用。
虽然这种状态接口在简单绘图时快捷方便,但也容易遇到问题。例如,一旦创建了第二个面板,如何返回并向第一个面板添加内容?这在 MATLAB 风格接口中是可能的,但有点笨拙。幸运的是,有更好的方法。
面向对象接口
对于这些更复杂的情况以及当您希望对图形有更多控制时,可以使用面向对象的接口。与依赖“活跃”图形或坐标轴的概念不同,在面向对象的接口中,绘图函数是显式Figure和Axes对象的方法。要使用这种绘图风格重新创建之前的图形,如在图 25-4 中所示,可以执行以下操作:
In [10]: # First create a grid of plots
# ax will be an array of two Axes objects
fig, ax = plt.subplots(2)
# Call plot() method on the appropriate object
ax[0].plot(x, np.sin(x))
ax[1].plot(x, np.cos(x));
图 25-4. 使用面向对象接口的子图
对于更简单的绘图,使用哪种风格主要是偏好问题,但随着绘图变得更加复杂,面向对象的方法可能变得必不可少。在接下来的章节中,我们将根据方便性在 MATLAB 风格和面向对象接口之间切换。在大多数情况下,区别仅在于将plt.plot切换为ax.plot,但在接下来的章节中遇到的一些陷阱我将会特别提出。
第二十六章:简单线图
可能所有绘图中最简单的是单个函数 y = f ( x ) 的可视化。在这里,我们将首次创建这种类型的简单绘图。如同接下来的所有章节一样,我们将从设置用于绘图的笔记本开始,并导入我们将使用的包:
In [1]: %matplotlib inline
import matplotlib.pyplot as plt
plt.style.use('seaborn-whitegrid')
import numpy as np
对于所有的 Matplotlib 图,我们首先创建一个图形和坐标轴。在它们最简单的形式下,可以像下面这样做(见图 26-1)。
In [2]: fig = plt.figure()
ax = plt.axes()
在 Matplotlib 中,figure(一个 plt.Figure 类的实例)可以被视为一个包含所有代表坐标轴、图形、文本和标签的对象的单个容器。axes(一个 plt.Axes 类的实例)就是我们看到的上述内容:一个带有刻度、网格和标签的边界框,最终将包含构成我们可视化的绘图元素。在本书的这一部分,我通常使用变量名 fig 表示一个图形实例,使用 ax 表示一个坐标轴实例或一组坐标轴实例。
图 26-1. 一个空的网格坐标轴
一旦我们创建了一个坐标轴,就可以使用 ax.plot 方法绘制一些数据。让我们从一个简单的正弦波开始,如 图 26-2 所示。
In [3]: fig = plt.figure()
ax = plt.axes()
x = np.linspace(0, 10, 1000)
ax.plot(x, np.sin(x));
图 26-2. 一个简单的正弦波
注意最后一行末尾的分号是有意为之:它抑制了从输出中显示绘图的文本表示。
或者,我们可以使用 PyLab 接口,让图形和坐标轴在后台自动创建(参见 第 IV 部分 讨论这两种接口);如 图 26-3 所示,结果是相同的。
In [4]: plt.plot(x, np.sin(x));
图 26-3. 通过面向对象接口的简单正弦波
如果我们想要创建一个包含多条线的单个图形(参见 图 26-4),我们可以简单地多次调用 plot 函数:
In [5]: plt.plot(x, np.sin(x))
plt.plot(x, np.cos(x));
这就是在 Matplotlib 中绘制简单函数的全部内容!现在我们将深入了解如何控制坐标轴和线条的外观的更多细节。
图 26-4. 多条线的重叠绘图
调整绘图:线条颜色和样式
你可能希望对图表进行的第一个调整是控制线条的颜色和样式。plt.plot 函数接受额外的参数来指定这些内容。要调整颜色,可以使用 color 关键字,接受一个表示几乎任何想象的颜色的字符串参数。颜色可以以多种方式指定;参见 图 26-5 来查看以下示例的输出:
In [6]: plt.plot(x, np.sin(x - 0), color='blue') # specify color by name
plt.plot(x, np.sin(x - 1), color='g') # short color code (rgbcmyk)
plt.plot(x, np.sin(x - 2), color='0.75') # grayscale between 0 and 1
plt.plot(x, np.sin(x - 3), color='#FFDD44') # hex code (RRGGBB, 00 to FF)
plt.plot(x, np.sin(x - 4), color=(1.0,0.2,0.3)) # RGB tuple, values 0 to 1
plt.plot(x, np.sin(x - 5), color='chartreuse'); # HTML color names supported
图 26-5. 控制绘图元素的颜色
如果未指定颜色,则 Matplotlib 将自动循环使用一组默认颜色来绘制多条线。
同样地,可以使用 linestyle 关键字来调整线条样式(参见 图 26-6)。
In [7]: plt.plot(x, x + 0, linestyle='solid')
plt.plot(x, x + 1, linestyle='dashed')
plt.plot(x, x + 2, linestyle='dashdot')
plt.plot(x, x + 3, linestyle='dotted');
# For short, you can use the following codes:
plt.plot(x, x + 4, linestyle='-') # solid
plt.plot(x, x + 5, linestyle='--') # dashed
plt.plot(x, x + 6, linestyle='-.') # dashdot
plt.plot(x, x + 7, linestyle=':'); # dotted
图 26-6. 各种线条样式的示例
虽然对于阅读你的代码的人来说可能不太清晰,但你可以通过将 linestyle 和 color 代码合并为单个非关键字参数传递给 plt.plot 函数来节省一些按键。 图 26-7 显示了结果。
In [8]: plt.plot(x, x + 0, '-g') # solid green
plt.plot(x, x + 1, '--c') # dashed cyan
plt.plot(x, x + 2, '-.k') # dashdot black
plt.plot(x, x + 3, ':r'); # dotted red
图 26-7. 使用简写语法控制颜色和样式
这些单字符颜色代码反映了 RGB(红/绿/蓝)和 CMYK(青/洋红/黄/黑)颜色系统中的标准缩写,通常用于数字彩色图形。
还有许多其他关键字参数可用于微调图表的外观;有关详细信息,请通过 IPython 的帮助工具阅读 plt.plot 函数的文档字符串(参见 第 1 章)。
调整图表:坐标轴限制
Matplotlib 在为你的图表选择默认的轴限制方面做得相当不错,但有时更精细的控制会更好。调整限制的最基本方法是使用 plt.xlim 和 plt.ylim 函数(参见 图 26-8)。
In [9]: plt.plot(x, np.sin(x))
plt.xlim(-1, 11)
plt.ylim(-1.5, 1.5);
图 26-8. 设置坐标轴限制的示例
如果因某种原因你希望任一轴显示反向,只需反转参数的顺序(参见 图 26-9)。
In [10]: plt.plot(x, np.sin(x))
plt.xlim(10, 0)
plt.ylim(1.2, -1.2);
图 26-9. 反转 y 轴的示例
一个有用的相关方法是 plt.axis(请注意这里可能会导致 axes(带有 e)和 axis(带有 i)之间的潜在混淆),它允许更质量化地指定轴限制。例如,你可以自动收紧当前内容周围的边界,如 图 26-10 所示。
In [11]: plt.plot(x, np.sin(x))
plt.axis('tight');
图 26-10. “紧凑”布局的示例
或者,您可以指定希望有一个相等的轴比率,这样 x 中的一个单位在视觉上等同于 y 中的一个单位,如 Figure 26-11 所示。
In [12]: plt.plot(x, np.sin(x))
plt.axis('equal');
Figure 26-11. “equal” 布局示例,单位与输出分辨率匹配
其他轴选项包括 'on'、'off'、'square'、'image' 等。有关更多信息,请参阅 plt.axis 文档字符串。
绘图标签
作为本章的最后一部分,我们将简要讨论绘图的标签:标题、坐标轴标签和简单图例。标题和坐标轴标签是最简单的标签——有方法可以快速设置它们(见 Figure 26-12)。
In [13]: plt.plot(x, np.sin(x))
plt.title("A Sine Curve")
plt.xlabel("x")
plt.ylabel("sin(x)");
Figure 26-12. 坐标轴标签和标题示例
可以使用函数的可选参数调整这些标签的位置、大小和样式,这些参数在文档字符串中有描述。
当在单个坐标轴中显示多行时,创建一个标签每种线型的图例是非常有用的。再次强调,Matplotlib 提供了一种内置的快速创建这种图例的方法;通过(你猜对了)plt.legend 方法来实现。虽然有几种有效的使用方法,但我发现最简单的方法是使用 plot 函数的 label 关键字来指定每条线的标签(见 Figure 26-13)。
In [14]: plt.plot(x, np.sin(x), '-g', label='sin(x)')
plt.plot(x, np.cos(x), ':b', label='cos(x)')
plt.axis('equal')
plt.legend();
Figure 26-13. 绘图图例示例
如您所见,plt.legend 函数跟踪线型和颜色,并将其与正确的标签匹配。有关指定和格式化绘图图例的更多信息,请参阅 plt.legend 文档字符串;此外,我们将在 第二十九章 中涵盖一些更高级的图例选项。
Matplotlib 的一些注意事项
虽然大多数 plt 函数可以直接转换为 ax 方法(plt.plot → ax.plot,plt.legend → ax.legend 等),但并非所有命令都是如此。特别是用于设置限制、标签和标题的功能略有修改。为了在 MATLAB 风格函数和面向对象方法之间进行过渡,请进行以下更改:
-
plt.xlabel→ax.set_xlabel -
plt.ylabel→ax.set_ylabel -
plt.xlim→ax.set_xlim -
plt.ylim→ax.set_ylim -
plt.title→ax.set_title
在面向对象的绘图接口中,与单独调用这些函数不同,通常更方便使用 ax.set 方法一次性设置所有这些属性(见 Figure 26-14)。
In [15]: ax = plt.axes()
ax.plot(x, np.sin(x))
ax.set(xlim=(0, 10), ylim=(-2, 2),
xlabel='x', ylabel='sin(x)',
title='A Simple Plot');
Figure 26-14. 使用 ax.set 一次性设置多个属性的示例
第二十七章:简单散点图
另一种常用的图表类型是简单的散点图,它与线图非常相似。点不是通过线段连接,而是分别用点、圆或其他形状表示。我们将从设置绘图笔记本和导入我们将使用的包开始:
In [1]: %matplotlib inline
import matplotlib.pyplot as plt
plt.style.use('seaborn-whitegrid')
import numpy as np
使用 plt.plot 创建散点图
在前一章中,我们使用 plt.plot/ax.plot 来生成线图。事实证明,这个函数也可以生成散点图(参见 图 27-1)。
In [2]: x = np.linspace(0, 10, 30)
y = np.sin(x)
plt.plot(x, y, 'o', color='black');
图 27-1. 散点图示例
函数调用中的第三个参数是一个字符,代表用于绘图的符号类型。正如你可以指定 '-' 或 '--' 控制线条样式一样,标记样式也有自己一套简短的字符串代码。可用符号的完整列表可以在 plt.plot 的文档中或 Matplotlib 的在线文档中找到。大多数可能性都相当直观,并且其中一些更常见的示例在此处演示(参见 图 27-2)。
In [3]: rng = np.random.default_rng(0)
for marker in ['o', '.', ',', 'x', '+', 'v', '^', '<', '>', 's', 'd']:
plt.plot(rng.random(2), rng.random(2), marker, color='black',
label="marker='{0}'".format(marker))
plt.legend(numpoints=1, fontsize=13)
plt.xlim(0, 1.8);
图 27-2. 点数示例
进一步地,这些字符代码可以与线条和颜色代码一起使用,以绘制带有连接线的点(参见 图 27-3)。
In [4]: plt.plot(x, y, '-ok');
图 27-3. 结合线条和点标记的示例
plt.plot 的额外关键字参数可以指定线条和标记的多种属性,正如你可以在 图 27-4 中看到的。
In [5]: plt.plot(x, y, '-p', color='gray',
markersize=15, linewidth=4,
markerfacecolor='white',
markeredgecolor='gray',
markeredgewidth=2)
plt.ylim(-1.2, 1.2);
图 27-4. 自定义线条和点标记
这些选项使得 plt.plot 成为 Matplotlib 中二维图的主要工具。要了解所有可用选项的详细描述,请参考 plt.plot 文档。
使用 plt.scatter 创建散点图
创建散点图的第二种更强大的方法是 plt.scatter 函数,其用法与 plt.plot 函数非常相似(参见 图 27-5)。
In [6]: plt.scatter(x, y, marker='o');
图 27-5. 一个简单的散点图
plt.scatter 与 plt.plot 的主要区别在于,它可以用于创建散点图,其中可以单独控制或映射到数据的每个点的属性(大小、填充颜色、边缘颜色等)。
为了更好地观察重叠的结果,我们创建一个随机散点图,点具有多种颜色和大小。为了调整透明度,我们还会使用 alpha 关键字(参见 图 27-6)。
In [7]: rng = np.random.default_rng(0)
x = rng.normal(size=100)
y = rng.normal(size=100)
colors = rng.random(100)
sizes = 1000 * rng.random(100)
plt.scatter(x, y, c=colors, s=sizes, alpha=0.3)
plt.colorbar(); # show color scale
图 27-6. 在散点图中更改点的大小和颜色
注意,颜色参数自动映射到颜色比例(这里通过colorbar命令显示),点的大小以像素表示。通过这种方式,可以利用点的颜色和大小来传达可视化信息,以便可视化多维数据。
例如,我们可以使用来自 Scikit-Learn 的鸢尾花数据集,其中每个样本是三种类型的花之一,其花瓣和萼片的大小已经被仔细测量(见图 27-7)。
In [8]: from sklearn.datasets import load_iris
iris = load_iris()
features = iris.data.T
plt.scatter(features[0], features[1], alpha=0.4,
s=100*features[3], c=iris.target, cmap='viridis')
plt.xlabel(iris.feature_names[0])
plt.ylabel(iris.feature_names[1]);
图 27-7. 使用点属性来编码鸢尾花数据的特征^(1)
我们可以看到,这个散点图使我们能够同时探索数据的四个不同维度:每个点的(x, y)位置对应于萼片的长度和宽度,点的大小与花瓣的宽度相关,颜色与特定种类的花相关。像这样的多颜色和多特征散点图既可以用于数据探索,也可以用于数据展示。
绘图与散点图:关于效率的一点说明
除了plt.plot和plt.scatter中提供的不同特性外,为什么你可能选择使用一个而不是另一个?虽然对于少量数据来说这并不重要,但是随着数据集超过几千个点,plt.plot比plt.scatter效率显著更高。原因在于,plt.scatter可以为每个点渲染不同的大小和/或颜色,因此渲染器必须额外工作来构建每个点。而对于plt.plot,每个点的标记是相同的,因此确定点的外观的工作仅需一次处理整个数据集。对于大数据集,这种差异可能导致性能大不相同,因此在处理大数据集时,应优先选择plt.plot而不是plt.scatter。
可视化不确定性
对于任何科学测量,准确地考虑不确定性几乎与准确报告数字本身同样重要,甚至更重要。例如,想象我正在使用一些天体物理观测来估计哈勃常数,即宇宙膨胀速率的本地测量。我知道当前文献建议的值约为 70 (km/s)/Mpc,而我的方法测量的值为 74 (km/s)/Mpc。这些值是否一致?基于这些信息,唯一正确的答案是:没有办法知道。
假设我将这些信息与报告的不确定性一起增加:当前文献建议的值为 70 ± 2.5 (km/s)/Mpc,而我的方法测量的值为 74 ± 5 (km/s)/Mpc。现在这些值是否一致?这是一个可以定量回答的问题。
在数据和结果的可视化中,有效地显示这些误差可以使绘图传达更完整的信息。
基本误差条
一种标准的可视化不确定性的方法是使用误差条。可以通过单个 Matplotlib 函数调用创建基本的误差条,如图 27-8 所示。
In [1]: %matplotlib inline
import matplotlib.pyplot as plt
plt.style.use('seaborn-whitegrid')
import numpy as np
In [2]: x = np.linspace(0, 10, 50)
dy = 0.8
y = np.sin(x) + dy * np.random.randn(50)
plt.errorbar(x, y, yerr=dy, fmt='.k');
这里的 fmt 是一个控制线条和点的外观的格式代码,其语法与前一章和本章早些时候概述的 plt.plot 的简写相同。
图 27-8. 一个误差条示例
除了这些基本选项外,errorbar 函数还有许多选项可以微调输出结果。使用这些附加选项,您可以轻松定制误差条绘图的美学效果。特别是在拥挤的图中,我经常发现将误差条的颜色设为比点本身更浅是有帮助的(见图 27-9)。
In [3]: plt.errorbar(x, y, yerr=dy, fmt='o', color='black',
ecolor='lightgray', elinewidth=3, capsize=0);
图 27-9. 自定义误差条
除了这些选项外,您还可以指定水平误差条、单侧误差条和许多其他变体。有关可用选项的更多信息,请参阅 plt.errorbar 的文档字符串。
连续误差
在某些情况下,希望在连续量上显示误差条。虽然 Matplotlib 没有针对这种类型应用的内置便捷例程,但可以相对轻松地结合 plt.plot 和 plt.fill_between 这样的基本图形元素来得到有用的结果。
在这里,我们将执行简单的高斯过程回归,使用 Scikit-Learn API(详见第三十八章)。这是一种将非常灵活的非参数函数拟合到具有连续不确定度测量的数据的方法。我们目前不会深入讨论高斯过程回归的细节,而是专注于如何可视化这种连续误差测量:
In [4]: from sklearn.gaussian_process import GaussianProcessRegressor
# define the model and draw some data
model = lambda x: x * np.sin(x)
xdata = np.array([1, 3, 5, 6, 8])
ydata = model(xdata)
# Compute the Gaussian process fit
gp = GaussianProcessRegressor()
gp.fit(xdata[:, np.newaxis], ydata)
xfit = np.linspace(0, 10, 1000)
yfit, dyfit = gp.predict(xfit[:, np.newaxis], return_std=True)
现在我们有 xfit、yfit 和 dyfit,它们对我们数据的连续拟合进行了采样。我们可以像前面的部分一样将它们传递给 plt.errorbar 函数,但我们实际上不想绘制 1,000 个点和 1,000 个误差条。相反,我们可以使用 plt.fill_between 函数并使用浅色来可视化这个连续误差(见图 27-10)。
In [5]: # Visualize the result
plt.plot(xdata, ydata, 'or')
plt.plot(xfit, yfit, '-', color='gray')
plt.fill_between(xfit, yfit - dyfit, yfit + dyfit,
color='gray', alpha=0.2)
plt.xlim(0, 10);
图 27-10. 用填充区域表示连续不确定性
查看 fill_between 的调用签名:我们传递一个 x 值,然后是下限 y 值和上限 y 值,结果是这些区域之间的区域被填充。
得到的图形直观地展示了高斯过程回归算法的运行情况:在接近测量数据点的区域,模型受到强约束,这反映在较小的模型不确定性中。在远离测量数据点的区域,模型约束不强,模型不确定性增加。
欲了解更多关于plt.fill_between(及其紧密相关的plt.fill函数)可用选项的信息,请参阅函数文档字符串或 Matplotlib 文档。
最后,如果这对你来说有点太低级了,请参考第三十六章,在那里我们讨论了 Seaborn 包,它具有更简化的 API 来可视化这种连续误差条类型。
^(1) 这幅图的全彩版可在GitHub上找到。
第二十八章:密度和等高线图
有时候,使用等高线或彩色区域来在二维中显示三维数据是很有用的。Matplotlib 提供了三个函数可以帮助完成这个任务:plt.contour 用于等高线图,plt.contourf 用于填充等高线图,plt.imshow 用于显示图像。本章将讨论几个使用这些函数的示例。我们将从设置绘图笔记本和导入我们将使用的函数开始:
In [1]: %matplotlib inline
import matplotlib.pyplot as plt
plt.style.use('seaborn-white')
import numpy as np
可视化三维函数
我们的第一个例子演示了使用函数 z = f ( x , y ) 绘制等高线图,这里选择了特定的 f(我们在第八章中已经见过它,当时我们将其作为数组广播的示例):
In [2]: def f(x, y):
return np.sin(x) ** 10 + np.cos(10 + y * x) * np.cos(x)
可以使用 plt.contour 函数创建等高线图。它接受三个参数:x 值的网格,y 值的网格和 z 值的网格。x 和 y 值表示图表上的位置,z 值将由等高线级别表示。准备这样的数据最直接的方法之一是使用 np.meshgrid 函数,它从一维数组构建二维网格:
In [3]: x = np.linspace(0, 5, 50)
y = np.linspace(0, 5, 40)
X, Y = np.meshgrid(x, y)
Z = f(X, Y)
现在让我们看看这个标准的仅线条等高线图(见图 28-1)。
In [4]: plt.contour(X, Y, Z, colors='black');
图 28-1. 用等高线可视化三维数据
注意,当使用单一颜色时,负值用虚线表示,正值用实线表示。或者可以通过指定 cmap 参数来使用色图对线条进行颜色编码。在这里,我们还指定希望在数据范围内绘制更多线条,即 20 个等间距间隔,如图 28-2 所示。
In [5]: plt.contour(X, Y, Z, 20, cmap='RdGy');
图 28-2. 用彩色等高线可视化三维数据
这里我们选择了 RdGy(缩写表示红灰)色图,这对于显示数据的正负变化(即围绕零的正负值)是一个不错的选择。Matplotlib 提供了多种色图可供选择,你可以通过在 IPython 中对 plt.cm 模块进行制表完成来轻松浏览:
plt.cm.<TAB>
我们的图表看起来更漂亮了,但是线条之间的空白可能有些分散注意力。我们可以通过切换到使用 plt.contourf 函数创建填充等高线图来改变这一点,它与 plt.contour 的语法基本相同。
此外,我们将添加一个 plt.colorbar 命令,它会创建一个带有标记颜色信息的额外坐标轴用于图表(参见图 28-3)。
In [6]: plt.contourf(X, Y, Z, 20, cmap='RdGy')
plt.colorbar();
图 28-3. 用填充等高线可视化三维数据
颜色条清楚地表明黑色区域为“峰值”,而红色区域为“谷底”。
这种绘图的一个潜在问题是有点斑驳:颜色步骤是离散的,而不是连续的,这并不总是期望的效果。可以通过将等高线的数量设置为一个非常大的数字来解决此问题,但这将导致绘图效率较低:Matplotlib 必须为每个级别渲染一个新的多边形。生成平滑表示的更好方法是使用 plt.imshow 函数,它提供了 interpolation 参数,以生成数据的平滑二维表示(见图 28-4)。
In [7]: plt.imshow(Z, extent=[0, 5, 0, 5], origin='lower', cmap='RdGy',
interpolation='gaussian', aspect='equal')
plt.colorbar();
图 28-4. 将三维数据表示为图像
使用 plt.imshow 有一些潜在的小问题:
-
它不接受 x 和 y 网格,因此您必须手动指定图中图像的 extent [xmin, xmax, ymin, ymax]。
-
默认情况下,它遵循标准图像数组定义,其中原点在左上角,而不是大多数等高线图中的左下角。在显示网格化数据时必须更改此设置。
-
它会自动调整轴的纵横比以匹配输入数据;可以使用
aspect参数进行更改。
最后,有时将等高线图和图像图结合起来可能会很有用。例如,在这里我们将使用部分透明的背景图像(透明度通过 alpha 参数设置),并在等高线上标记标签,使用 plt.clabel 函数(见图 28-5)。
In [8]: contours = plt.contour(X, Y, Z, 3, colors='black')
plt.clabel(contours, inline=True, fontsize=8)
plt.imshow(Z, extent=[0, 5, 0, 5], origin='lower',
cmap='RdGy', alpha=0.5)
plt.colorbar();
图 28-5. 图像上标记的等高线
这三个函数的组合——plt.contour、plt.contourf 和 plt.imshow——在二维图中展示三维数据具有几乎无限的可能性。关于这些函数可用选项的更多信息,请参阅它们的文档字符串。如果您对这类数据的三维可视化感兴趣,请参阅第三十五章。
直方图、分箱和密度
简单直方图可以是理解数据集的一个很好的第一步。此前,我们看到了 Matplotlib 的直方图函数预览(见第九章),它一旦完成了常规的引入工作(见图 28-6)就能以一行代码创建一个基本的直方图。
In [1]: %matplotlib inline
import numpy as np
import matplotlib.pyplot as plt
plt.style.use('seaborn-white')
rng = np.random.default_rng(1701)
data = rng.normal(size=1000)
In [2]: plt.hist(data);
图 28-6. 简单直方图
hist 函数有许多选项可以调整计算和显示;这里有一个更加定制化的直方图示例,显示在 Figure 28-7 中。
In [3]: plt.hist(data, bins=30, density=True, alpha=0.5,
histtype='stepfilled', color='steelblue',
edgecolor='none');
图 28-7. 一个定制化的直方图
plt.hist 的文档字符串中包含更多有关其他可用定制选项的信息。当比较几个分布的直方图时,我发现使用histtype='stepfilled'与一些透明度 alpha 的组合是很有帮助的(参见 Figure 28-8)。
In [4]: x1 = rng.normal(0, 0.8, 1000)
x2 = rng.normal(-2, 1, 1000)
x3 = rng.normal(3, 2, 1000)
kwargs = dict(histtype='stepfilled', alpha=0.3, density=True, bins=40)
plt.hist(x1, **kwargs)
plt.hist(x2, **kwargs)
plt.hist(x3, **kwargs);
图 28-8. 叠加多个直方图^(1)
如果您只对计算而不是显示直方图(即计算给定箱中点的数量)感兴趣,可以使用 np.histogram 函数:
In [5]: counts, bin_edges = np.histogram(data, bins=5)
print(counts)
Out[5]: [ 23 241 491 224 21]
二维直方图和分箱
就像我们通过将数轴划分为箱来创建一维直方图一样,我们也可以通过将点分配到二维箱中来创建二维直方图。我们将简要地查看几种方法来做到这一点。让我们首先定义一些数据——从多元高斯分布中抽取的 x 和 y 数组:
In [6]: mean = [0, 0]
cov = [[1, 1], [1, 2]]
x, y = rng.multivariate_normal(mean, cov, 10000).T
plt.hist2d:二维直方图
绘制二维直方图的一个简单方法是使用 Matplotlib 的 plt.hist2d 函数(见 Figure 28-9)。
In [7]: plt.hist2d(x, y, bins=30)
cb = plt.colorbar()
cb.set_label('counts in bin')
图 28-9. 使用 plt.hist2d 绘制的二维直方图
就像 plt.hist 一样,plt.hist2d 也有许多额外选项可以微调绘图和分箱,这些选项在函数的文档字符串中有详细说明。此外,就像 plt.hist 在 np.histogram 中有对应物一样,plt.hist2d 在 np.histogram2d 中也有对应物:
In [8]: counts, xedges, yedges = np.histogram2d(x, y, bins=30)
print(counts.shape)
Out[8]: (30, 30)
对于当有超过两个维度时的直方图分箱的泛化,参见 np.histogramdd 函数。
plt.hexbin:六边形分箱
二维直方图在轴上创建了一个方块的镶嵌图案。这种镶嵌的另一个自然形状是正六边形。为此,Matplotlib 提供了 plt.hexbin 程序,它表示在六边形网格内对二维数据集进行分箱(见 Figure 28-10)。
In [9]: plt.hexbin(x, y, gridsize=30)
cb = plt.colorbar(label='count in bin')
图 28-10. 使用 plt.hexbin 绘制的二维直方图
plt.hexbin 还有许多额外选项,包括指定每个点的权重和更改每个箱中输出到任何 NumPy 聚合(权重的平均值、权重的标准差等)。
核密度估计
在多维度中估计和表示密度的另一种常见方法是 核密度估计(KDE)。这将在 第四十九章 中更详细地讨论,但现在我只想简单提一下,KDE 可以被看作是一种在空间中“扩展”点并将结果加总以获得平滑函数的方法。scipy.stats 包中存在一种极快且简单的 KDE 实现。这里是使用 KDE 的一个快速示例(参见 图 28-11)。
In [10]: from scipy.stats import gaussian_kde
# fit an array of size [Ndim, Nsamples]
data = np.vstack([x, y])
kde = gaussian_kde(data)
# evaluate on a regular grid
xgrid = np.linspace(-3.5, 3.5, 40)
ygrid = np.linspace(-6, 6, 40)
Xgrid, Ygrid = np.meshgrid(xgrid, ygrid)
Z = kde.evaluate(np.vstack([Xgrid.ravel(), Ygrid.ravel()]))
# Plot the result as an image
plt.imshow(Z.reshape(Xgrid.shape),
origin='lower', aspect='auto',
extent=[-3.5, 3.5, -6, 6])
cb = plt.colorbar()
cb.set_label("density")
图 28-11. 一个分布的核密度表示
KDE 具有一个有效的平滑长度,可以有效地在详细度和平滑度之间调节(这是普遍的偏差-方差权衡的一个例子)。选择适当的平滑长度的文献非常广泛;gaussian_kde 使用一个经验法则来尝试找到输入数据的几乎最优平滑长度。
SciPy 生态系统中还提供了其他 KDE 实现,每种实现都有其各自的优缺点;例如,可以看到 sklearn.neighbors.KernelDensity 和 statsmodels.nonparametric.KDEMultivariate。
对于基于 KDE 的可视化,使用 Matplotlib 往往显得冗长。Seaborn 库在 第三十六章 中讨论,为创建基于 KDE 的可视化提供了更紧凑的 API。
^(1) 这个图的全彩版可以在 GitHub 上找到。
第二十九章:自定义绘图图例
绘图图例赋予可视化含义,为各种绘图元素分配含义。我们之前看过如何创建简单的图例;现在我们将看看如何在 Matplotlib 中自定义图例的位置和美观性。
最简单的图例可以通过plt.legend命令创建,该命令会自动为任何有标签的绘图元素创建图例(参见图 29-1)。
In [1]: import matplotlib.pyplot as plt
plt.style.use('seaborn-whitegrid')
In [2]: %matplotlib inline
import numpy as np
In [3]: x = np.linspace(0, 10, 1000)
fig, ax = plt.subplots()
ax.plot(x, np.sin(x), '-b', label='Sine')
ax.plot(x, np.cos(x), '--r', label='Cosine')
ax.axis('equal')
leg = ax.legend()
图 29-1. 默认绘图图例
但我们可能希望以许多方式自定义这样一个图例。例如,我们可以指定位置并打开边框(参见图 29-2)。
In [4]: ax.legend(loc='upper left', frameon=True)
fig
图 29-2. 自定义绘图图例
我们可以使用ncol命令来指定图例中的列数,如图 29-3 所示。
In [5]: ax.legend(loc='lower center', ncol=2)
fig
图 29-3. 一个两列图例
我们还可以使用圆角框(fancybox)或添加阴影,更改框架的透明度(alpha 值)或更改文本周围的填充(参见图 29-4)。
In [6]: ax.legend(frameon=True, fancybox=True, framealpha=1,
shadow=True, borderpad=1)
fig
有关可用图例选项的更多信息,请参阅plt.legend的文档字符串。
图 29-4. 一个带有 fancybox 样式的图例
选择图例元素
正如我们已经看到的,默认情况下,图例包括来自绘图的所有带标签的元素。如果这不是所需的,我们可以通过使用plot命令返回的对象来微调图例中显示的元素和标签。plt.plot能够一次创建多条线,并返回创建的线实例列表。将其中任何一条传递给plt.legend将告诉它要识别哪些元素,以及我们想指定的标签(参见图 29-5)。
In [7]: y = np.sin(x[:, np.newaxis] + np.pi * np.arange(0, 2, 0.5))
lines = plt.plot(x, y)
# lines is a list of plt.Line2D instances
plt.legend(lines[:2], ['first', 'second'], frameon=True);
图 29-5. 图例元素的自定义^(1)
在实践中,我通常发现使用第一种方法更为清晰,即对你想在图例中显示的绘图元素应用标签(参见图 29-6)。
In [8]: plt.plot(x, y[:, 0], label='first')
plt.plot(x, y[:, 1], label='second')
plt.plot(x, y[:, 2:])
plt.legend(frameon=True);
请注意,图例会忽略所有未设置label属性的元素。
图 29-6. 自定义图例元素的另一种方法^(2)
点的尺寸图例
有时默认的图例设置不足以满足给定的可视化需求。例如,也许您正在使用点的大小来标记数据的某些特征,并且希望创建反映这一点的图例。这里有一个示例,我们将使用点的大小来指示加利福尼亚城市的人口。我们希望一个指定点大小比例的图例,并通过绘制一些带标签的数据项而实现这一目标(参见图 29-7)。
In [9]: # Uncomment to download the data
# url = ('https://raw.githubusercontent.com/jakevdp/
# PythonDataScienceHandbook/''master/notebooks/data/
# california_cities.csv')
# !cd data && curl -O {url}
In [10]: import pandas as pd
cities = pd.read_csv('data/california_cities.csv')
# Extract the data we're interested in
lat, lon = cities['latd'], cities['longd']
population, area = cities['population_total'], cities['area_total_km2']
# Scatter the points, using size and color but no label
plt.scatter(lon, lat, label=None,
c=np.log10(population), cmap='viridis',
s=area, linewidth=0, alpha=0.5)
plt.axis('equal')
plt.xlabel('longitude')
plt.ylabel('latitude')
plt.colorbar(label='log$_{10}$(population)')
plt.clim(3, 7)
# Here we create a legend:
# we'll plot empty lists with the desired size and label
for area in [100, 300, 500]:
plt.scatter([], [], c='k', alpha=0.3, s=area,
label=str(area) + ' km$²$')
plt.legend(scatterpoints=1, frameon=False, labelspacing=1,
title='City Area')
plt.title('California Cities: Area and Population');
图 29-7. 加利福尼亚城市的位置、地理大小和人口
图例始终引用绘图上的某个对象,因此如果我们想显示特定的形状,我们需要绘制它。在这种情况下,我们想要的对象(灰色圆圈)不在绘图上,因此我们通过绘制空列表来伪造它们。请记住,图例仅列出具有指定标签的绘图元素。
通过绘制空列表,我们创建了带标签的绘图对象,这些对象被图例捕捉,现在我们的图例告诉我们一些有用的信息。这种策略对于创建更复杂的可视化效果很有用。
多个图例
有时在设计绘图时,您可能希望向同一坐标轴添加多个图例。不幸的是,Matplotlib 并不简化这一过程:通过标准的legend接口,仅能为整个绘图创建一个图例。如果尝试使用plt.legend或ax.legend创建第二个图例,它将简单地覆盖第一个。我们可以通过从头开始创建新的图例艺术家(Artist是 Matplotlib 用于视觉属性的基类),然后使用较低级别的ax.add_artist方法手动将第二个艺术家添加到绘图中来解决此问题(参见图 29-8)。
In [11]: fig, ax = plt.subplots()
lines = []
styles = ['-', '--', '-.', ':']
x = np.linspace(0, 10, 1000)
for i in range(4):
lines += ax.plot(x, np.sin(x - i * np.pi / 2),
styles[i], color='black')
ax.axis('equal')
# Specify the lines and labels of the first legend
ax.legend(lines[:2], ['line A', 'line B'], loc='upper right')
# Create the second legend and add the artist manually
from matplotlib.legend import Legend
leg = Legend(ax, lines[2:], ['line C', 'line D'], loc='lower right')
ax.add_artist(leg);
这是查看组成任何 Matplotlib 绘图的低级艺术家对象的一瞥。如果您检查ax.legend的源代码(请记住,您可以在 Jupyter 笔记本中使用ax.legend??来执行此操作),您将看到该函数仅仅由一些逻辑组成,用于创建一个适当的Legend艺术家,然后将其保存在legend_属性中,并在绘制绘图时将其添加到图形中。
图 29-8. 分割图例
^(1) 该图的全彩版本可以在GitHub上找到。
^(2) 该图的全彩版本可以在GitHub上找到。
第三十章:定制色条
绘图图例识别离散点的离散标签。对于基于点、线或区域颜色的连续标签,带标签的色条是一个很好的工具。在 Matplotlib 中,色条被绘制为一个独立的轴,可以为绘图中颜色的含义提供关键。由于本书以黑白印刷,本章配有一个在线补充,您可以在其中查看全彩色的图表。我们将从设置用于绘图的笔记本和导入我们将使用的函数开始:
In [1]: import matplotlib.pyplot as plt
plt.style.use('seaborn-white')
In [2]: %matplotlib inline
import numpy as np
正如我们已经看到的几次,最简单的色条可以使用plt.colorbar函数创建(参见图 30-1)。
In [3]: x = np.linspace(0, 10, 1000)
I = np.sin(x) * np.cos(x[:, np.newaxis])
plt.imshow(I)
plt.colorbar();
注意
全彩色图可以在GitHub 上的补充材料中找到。
现在我们将讨论几个关于定制这些色条并在各种情况下有效使用它们的想法。
图 30-1. 一个简单的色条图例
自定义色条
可以使用cmap参数来指定色图,该参数用于创建可视化的绘图函数(参见图 30-2)。
In [4]: plt.imshow(I, cmap='Blues');
图 30-2. 一个蓝色调色板
可用色图的名称在plt.cm命名空间中;使用 IPython 的制表完成功能将为您提供内置可能性的完整列表:
plt.cm.<TAB>
但是能够选择色图只是第一步:更重要的是如何决定选择哪种!选择实际上比您最初期望的要微妙得多。
选择色图
在可视化中选择颜色的全面处理超出了本书的范围,但是对于这个主题和其他主题的有趣阅读,请参阅 Nicholas Rougier、Michael Droettboom 和 Philip Bourne 的文章“Ten Simple Rules for Better Figures”。Matplotlib 的在线文档还有一个有趣的关于色图选择的讨论(oreil.ly/Ll1ir)。
总体而言,您应该了解三种不同类型的色图:
连续色图
这些由一系列连续的颜色组成(例如,binary或viridis)。
发散色图
这些通常包含两种不同的颜色,显示与平均值的正负偏差(例如,RdBu或PuOr)。
- 定性色图
这些颜色混合没有特定的顺序(例如,rainbow或jet)。
jet色图是 Matplotlib 在 2.0 版本之前的默认色图的一个示例。它作为默认的状态非常不幸,因为定性色图通常不适合表示定量数据。其中一个问题是,定性色图通常不显示随着比例增加而亮度均匀递增的特性。
通过将 jet 色彩条转换为黑白图像,我们可以看到这一点(见 图 30-3)。
In [5]: from matplotlib.colors import LinearSegmentedColormap
def grayscale_cmap(cmap):
"""Return a grayscale version of the given colormap"""
cmap = plt.cm.get_cmap(cmap)
colors = cmap(np.arange(cmap.N))
# Convert RGBA to perceived grayscale luminance
# cf. http://alienryderflex.com/hsp.xhtml
RGB_weight = [0.299, 0.587, 0.114]
luminance = np.sqrt(np.dot(colors[:, :3] ** 2, RGB_weight))
colors[:, :3] = luminance[:, np.newaxis]
return LinearSegmentedColormap.from_list(
cmap.name + "_gray", colors, cmap.N)
def view_colormap(cmap):
"""Plot a colormap with its grayscale equivalent"""
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])
In [6]: view_colormap('jet')
图 30-3. jet 色彩映射及其不均匀的亮度比例
注意灰度图像中的明亮条纹。即使是全彩色,这种不均匀的亮度也意味着眼睛会被色彩范围的某些部分所吸引,这可能会强调数据集中不重要的部分。最好使用像viridis(Matplotlib 2.0 的默认色彩映射)这样的色彩映射,它专门设计成在整个范围内具有均匀的亮度变化;因此,它不仅与我们的色彩感知相配,而且在灰度打印时也能很好地转化(参见 图 30-4)。
In [7]: view_colormap('viridis')
图 30-4. viridis 色彩映射及其均匀的亮度比例
对于其他情况,如显示与某个均值的正负偏差,双色彩色条如RdBu(红-蓝)很有帮助。但是,正如您在 图 30-5 中所看到的,重要的是注意正/负信息在转换为灰度时会丢失!
In [8]: view_colormap('RdBu')
图 30-5. RdBu 色彩映射及其亮度
在接下来的示例中,我们将看到如何使用其中一些色彩映射。
Matplotlib 提供了大量的色彩映射选项;要查看它们的列表,可以使用 IPython 来探索 plt.cm 子模块。对于 Python 中更加原则性的颜色处理方法,可以参考 Seaborn 库中的工具和文档(见 第 36 章)。
色彩限制和扩展
Matplotlib 允许大范围的颜色条定制。颜色条本身只是 plt.Axes 的一个实例,因此我们之前看到的所有坐标轴和刻度格式化技巧都适用。颜色条具有一些有趣的灵活性:例如,我们可以缩小颜色限制,并通过设置 extend 属性在顶部和底部指示超出范围的值以三角箭头表示。例如,在显示受噪声影响的图像时(见 图 30-6),这可能会很有用。
In [9]: # make noise in 1% of the image pixels
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)
图 30-6. 指定色彩映射扩展^(1)
注意左侧面板中,默认的颜色限制对噪声像素做出了响应,并且噪声范围完全淹没了我们感兴趣的模式。在右侧面板中,我们手动设置了颜色限制,并添加了扩展以指示超出这些限制的值。结果是我们数据的可视化更加有用。
离散色彩条
色图默认是连续的,但有时您想要表示离散值。这样做的最简单方法是使用plt.cm.get_cmap函数,传递一个合适的色图名称和所需的箱数(见图 30-7)。
In [10]: plt.imshow(I, cmap=plt.cm.get_cmap('Blues', 6))
plt.colorbar(extend='both')
plt.clim(-1, 1);
图 30-7. 一个离散的色图
色图的离散版本可以像任何其他色图一样使用。
例子:手写数字
这可以应用的一个例子是,让我们看一看 Scikit-Learn 中包含的手写数字数据集的一个有趣的可视化;它包括近 2000 个8 × 8缩略图,显示各种手写数字。
现在,让我们从下载手写数字数据集和用plt.imshow可视化几个示例图像开始(见图 30-8)。
In [11]: # load images of the digits 0 through 5 and visualize several of them
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=[])
图 30-8. 手写数字数据的样本
因为每个数字由其 64 个像素的色调定义,我们可以将每个数字视为位于 64 维空间中的一个点:每个维度表示一个像素的亮度。可视化这样高维数据可能会很困难,但处理这个任务的一种方法是使用流形学习等降维技术来减少数据的维度,同时保持感兴趣的关系。降维是无监督机器学习的一个例子,我们将在第三十七章中更详细地讨论它。
推迟讨论这些细节,让我们来看一看手写数字数据的二维流形学习投影(详情见第四十六章):
In [12]: # project the digits into 2 dimensions using Isomap
from sklearn.manifold import Isomap
iso = Isomap(n_components=2, n_neighbors=15)
projection = iso.fit_transform(digits.data)
我们将使用我们的离散色图来查看结果,设置ticks和clim以改善结果色条的美观度(见图 30-9)。
In [13]: # plot the results
plt.scatter(projection[:, 0], projection[:, 1], lw=0.1,
c=digits.target, cmap=plt.cm.get_cmap('plasma', 6))
plt.colorbar(ticks=range(6), label='digit value')
plt.clim(-0.5, 5.5)
图 30-9. 手写数字像素的流形嵌入
投影还向我们展示了数据集内部的一些关系:例如,在这个投影中,2 和 3 的范围几乎重叠,表明一些手写的 2 和 3 很难区分,可能更容易被自动分类算法混淆。而像 0 和 1 这样的值则分开得更远,可能更不容易混淆。
我们将在第五部分回到流形学习和数字分类。
^(1) 本图的完整版本可以在GitHub上找到。
第三十一章:多个子图
有时将数据的不同视图并排比较会很有帮助。为此,Matplotlib 有子图的概念:一组可以在单个图中存在的较小轴。这些子图可以是插图、网格图或其他更复杂的布局。在本章中,我们将探讨创建 Matplotlib 中子图的四种常规方法。我们将首先导入要使用的包:
In [1]: %matplotlib inline
import matplotlib.pyplot as plt
plt.style.use('seaborn-white')
import numpy as np
plt.axes:手动创建子图
创建轴的最基本方法是使用plt.axes函数。正如我们之前看到的,默认情况下,这将创建一个填充整个图的标准轴对象。plt.axes还接受一个可选参数,即在图坐标系中的四个数字列表([*left*, *bottom*, *width*, *height*]),它们的取值范围从图的左下角的 0 到右上角的 1。
例如,我们可以通过将x和y位置设置为 0.65(即从图的宽度和高度的 65%开始)并将x和y范围设置为 0.2(即轴的大小为图宽度和高度的 20%)在另一个轴的右上角创建一个插图轴。图 31-1 显示了结果:
In [2]: ax1 = plt.axes() # standard axes
ax2 = plt.axes([0.65, 0.65, 0.2, 0.2])
图 31-1. 插图轴示例
在面向对象接口中,该命令的等效命令是fig.add_axes。让我们使用它来创建两个垂直堆叠的轴,如图 31-2 所示。
In [3]: fig = plt.figure()
ax1 = fig.add_axes([0.1, 0.5, 0.8, 0.4],
xticklabels=[], ylim=(-1.2, 1.2))
ax2 = fig.add_axes([0.1, 0.1, 0.8, 0.4],
ylim=(-1.2, 1.2))
x = np.linspace(0, 10)
ax1.plot(np.sin(x))
ax2.plot(np.cos(x));
现在我们有两个轴(顶部没有刻度标签),它们紧挨着:上面板的底部(在位置 0.5 处)与下面板的顶部(在位置 0.1 + 0.4 处)相匹配。
图 31-2. 垂直堆叠轴示例
plt.subplot:简单的子图网格
对齐的列或行子图是一个常见的需求,Matplotlib 提供了几个便利函数来轻松创建它们。其中最低级别的是plt.subplot,它在网格中创建一个单个子图。正如您所见,这个命令需要三个整数参数——行数、列数以及在此方案中要创建的图的索引,该索引从左上角到右下角依次排列(见图 31-3)。
In [4]: for i in range(1, 7):
plt.subplot(2, 3, i)
plt.text(0.5, 0.5, str((2, 3, i)),
fontsize=18, ha='center')
图 31-3. plt.subplot 示例
命令plt.subplots_adjust可用于调整这些图之间的间距。以下代码使用了等效的面向对象的命令fig.add_subplot;图 31-4 显示了结果:
In [5]: fig = plt.figure()
fig.subplots_adjust(hspace=0.4, wspace=0.4)
for i in range(1, 7):
ax = fig.add_subplot(2, 3, i)
ax.text(0.5, 0.5, str((2, 3, i)),
fontsize=18, ha='center')
在这里,我们使用了plt.subplots_adjust的hspace和wspace参数,它们分别指定了图的高度和宽度上的间距,单位为子图大小的百分比(在本例中,空间为子图宽度和高度的 40%)。
图 31-4. 调整边距的 plt.subplot
plt.subplots:一次性创建整个网格
当创建大量子图网格时,特别是如果您希望隐藏内部图的 x 和 y 轴标签时,刚刚描述的方法很快变得繁琐。为此,plt.subplots 是更易于使用的工具(注意 subplots 末尾的 s)。该函数不是创建单个子图,而是一行内创建完整的子图网格,返回它们的 NumPy 数组。参数是行数和列数,以及可选的关键字 sharex 和 sharey,允许您指定不同轴之间的关系。
让我们创建一个 2 × 3 的子图网格,同一行内的所有轴共享其 y 轴比例,同一列内的所有轴共享其 x 轴比例(参见 图 31-5)。
In [6]: fig, ax = plt.subplots(2, 3, sharex='col', sharey='row')
图 31-5. plt.subplots 中的共享 x 和 y 轴
通过指定 sharex 和 sharey,我们自动删除了网格内部的标签,使得绘图更清晰。生成的轴实例网格返回为 NumPy 数组,可以使用标准数组索引符号方便地指定所需的轴(参见 图 31-6)。
In [7]: # axes are in a two-dimensional array, indexed by [row, col]
for i in range(2):
for j in range(3):
ax[i, j].text(0.5, 0.5, str((i, j)),
fontsize=18, ha='center')
fig
相较于 plt.subplot,plt.subplots 更符合 Python 的传统零起始索引,而 plt.subplot 使用 MATLAB 风格的一起始索引。
图 31-6. 在子图网格中标识绘图
plt.GridSpec:更复杂的排列
要超越常规网格,创建跨越多行和列的子图,plt.GridSpec 是最佳工具。 plt.GridSpec 本身不创建图形;它是一个方便的接口,被 plt.subplot 命令识别。例如,一个具有两行三列、指定宽度和高度空间的 GridSpec 如下所示:
In [8]: grid = plt.GridSpec(2, 3, wspace=0.4, hspace=0.3)
通过这种方式,我们可以使用熟悉的 Python 切片语法指定子图的位置和范围(参见 图 31-7)。
In [9]: plt.subplot(grid[0, 0])
plt.subplot(grid[0, 1:])
plt.subplot(grid[1, :2])
plt.subplot(grid[1, 2]);
图 31-7. 使用 plt.GridSpec 创建不规则子图
这种灵活的网格对齐方式有广泛的用途。我最常在创建像 图 31-8 中显示的多轴直方图图时使用它。
In [10]: # Create some normally distributed data
mean = [0, 0]
cov = [[1, 1], [1, 2]]
rng = np.random.default_rng(1701)
x, y = rng.multivariate_normal(mean, cov, 3000).T
# Set up the axes with GridSpec
fig = plt.figure(figsize=(6, 6))
grid = plt.GridSpec(4, 4, hspace=0.2, wspace=0.2)
main_ax = fig.add_subplot(grid[:-1, 1:])
y_hist = fig.add_subplot(grid[:-1, 0], xticklabels=[], sharey=main_ax)
x_hist = fig.add_subplot(grid[-1, 1:], yticklabels=[], sharex=main_ax)
# Scatter points on the main axes
main_ax.plot(x, y, 'ok', markersize=3, alpha=0.2)
# Histogram on the attached axes
x_hist.hist(x, 40, histtype='stepfilled',
orientation='vertical', color='gray')
x_hist.invert_yaxis()
y_hist.hist(y, 40, histtype='stepfilled',
orientation='horizontal', color='gray')
y_hist.invert_xaxis()
图 31-8. 使用 plt.GridSpec 可视化多维分布
这种分布类型以及其边距经常出现,Seaborn 包中有自己的绘图 API;详见 第 36 章。
第三十二章:文本和注释
创建良好的可视化图表涉及引导读者,使图表讲述一个故事。在某些情况下,可以完全通过视觉方式讲述这个故事,无需添加文本,但在其他情况下,小的文本提示和标签是必要的。也许你会使用的最基本的注释类型是坐标轴标签和标题,但选项远不止于此。让我们看看一些数据及其如何可视化和注释,以传达有趣的信息。我们将开始设置绘图笔记本并导入将要使用的函数:
In [1]: %matplotlib inline
import matplotlib.pyplot as plt
import matplotlib as mpl
plt.style.use('seaborn-whitegrid')
import numpy as np
import pandas as pd
示例:节假日对美国出生的影响
让我们回到之前处理的一些数据,在“例子:出生率数据”中,我们生成了一个绘制整个日历年平均出生的图表。我们将从那里使用相同的清理过程开始,并绘制结果(参见图 32-1)。
In [2]: # shell command to download the data:
# !cd data && curl -O \
# https://raw.githubusercontent.com/jakevdp/data-CDCbirths/master/
# births.csv
In [3]: from datetime import datetime
births = pd.read_csv('data/births.csv')
quartiles = np.percentile(births['births'], [25, 50, 75])
mu, sig = quartiles[1], 0.74 * (quartiles[2] - quartiles[0])
births = births.query('(births > @mu - 5 * @sig) &
(births < @mu + 5 * @sig)')
births['day'] = births['day'].astype(int)
births.index = pd.to_datetime(10000 * births.year +
100 * births.month +
births.day, format='%Y%m%d')
births_by_date = births.pivot_table('births',
[births.index.month, births.index.day])
births_by_date.index = [datetime(2012, month, day)
for (month, day) in births_by_date.index]
In [4]: fig, ax = plt.subplots(figsize=(12, 4))
births_by_date.plot(ax=ax);
图 32-1. 每日平均出生数按日期统计^(1)
当我们可视化这样的数据时,注释图表的特定特征通常很有用,以吸引读者的注意。可以使用 plt.text/ax.text 函数手动完成此操作,该函数将文本放置在特定的 x/y 值处(参见图 32-2)。
In [5]: fig, ax = plt.subplots(figsize=(12, 4))
births_by_date.plot(ax=ax)
# Add labels to the plot
style = dict(size=10, color='gray')
ax.text('2012-1-1', 3950, "New Year's Day", **style)
ax.text('2012-7-4', 4250, "Independence Day", ha='center', **style)
ax.text('2012-9-4', 4850, "Labor Day", ha='center', **style)
ax.text('2012-10-31', 4600, "Halloween", ha='right', **style)
ax.text('2012-11-25', 4450, "Thanksgiving", ha='center', **style)
ax.text('2012-12-25', 3850, "Christmas ", ha='right', **style)
# Label the axes
ax.set(title='USA births by day of year (1969-1988)',
ylabel='average daily births')
# Format the x-axis with centered month labels
ax.xaxis.set_major_locator(mpl.dates.MonthLocator())
ax.xaxis.set_minor_locator(mpl.dates.MonthLocator(bymonthday=15))
ax.xaxis.set_major_formatter(plt.NullFormatter())
ax.xaxis.set_minor_formatter(mpl.dates.DateFormatter('%h'));
图 32-2. 按日期注释的每日平均出生数^(2)
ax.text 方法需要一个 x 位置、一个 y 位置和一个字符串,然后是可选的关键字,指定文本的颜色、大小、样式、对齐方式和其他属性。这里我们使用了 ha='right' 和 ha='center',其中 ha 是 水平对齐 的缩写。有关可用选项的更多信息,请参阅 plt.text 和 mpl.text.Text 的文档字符串。
转换和文本位置
在前面的示例中,我们将文本注释锚定在数据位置上。有时候,将文本锚定在轴或图的固定位置上,而与数据无关,更为可取。在 Matplotlib 中,通过修改 transform 来实现这一点。
Matplotlib 使用几种不同的坐标系统:数学上,位于 ( x , y ) = ( 1 , 1 ) 处的数据点对应于轴或图的特定位置,进而对应于屏幕上的特定像素。在数学上,这些坐标系统之间的转换相对简单,Matplotlib 在内部使用一组良好开发的工具来执行这些转换(这些工具可以在 matplotlib.transforms 子模块中探索)。
典型用户很少需要担心变换的细节,但在考虑在图上放置文本时,这些知识是有帮助的。在这种情况下,有三种预定义的变换可能会有所帮助:
ax.transData
与数据坐标相关联的变换
ax.transAxes
与轴相关联的变换(以轴尺寸为单位)
fig.transFigure
与图形相关联的变换(以图形尺寸为单位)
让我们看一个示例,使用这些变换在不同位置绘制文本(参见 图 32-3)。
In [6]: fig, ax = plt.subplots(facecolor='lightgray')
ax.axis([0, 10, 0, 10])
# transform=ax.transData is the default, but we'll specify it anyway
ax.text(1, 5, ". Data: (1, 5)", transform=ax.transData)
ax.text(0.5, 0.1, ". Axes: (0.5, 0.1)", transform=ax.transAxes)
ax.text(0.2, 0.2, ". Figure: (0.2, 0.2)", transform=fig.transFigure);
图 32-3. 比较 Matplotlib 的坐标系
Matplotlib 的默认文本对齐方式是使每个字符串开头的“.”大致标记指定的坐标位置。
transData 坐标提供与 x 和 y 轴标签关联的通常数据坐标。transAxes 坐标给出了从轴的左下角(白色框)开始的位置,作为总轴尺寸的一部分的分数。transFigure 坐标类似,但指定了从图的左下角(灰色框)开始的位置,作为总图尺寸的一部分的分数。
注意,现在如果我们更改坐标轴限制,只有 transData 坐标会受到影响,而其他坐标保持不变(参见 图 32-4)。
In [7]: ax.set_xlim(0, 2)
ax.set_ylim(-6, 6)
fig
图 32-4. 比较 Matplotlib 的坐标系
通过交互式更改坐标轴限制,可以更清楚地看到这种行为:如果您在笔记本中执行此代码,可以通过将 %matplotlib inline 更改为 %matplotlib notebook 并使用每个图的菜单与图进行交互来实现这一点。
箭头和注释
除了刻度和文本,另一个有用的注释标记是简单的箭头。
虽然有 plt.arrow 函数可用,但我不建议使用它:它创建的箭头是 SVG 对象,会受到绘图的不同纵横比的影响,使得难以正确使用。相反,我建议使用 plt.annotate 函数,它创建一些文本和箭头,并允许非常灵活地指定箭头。
这里演示了使用几种选项的 annotate 的示例(参见 图 32-5)。
In [8]: fig, ax = plt.subplots()
x = np.linspace(0, 20, 1000)
ax.plot(x, np.cos(x))
ax.axis('equal')
ax.annotate('local maximum', xy=(6.28, 1), xytext=(10, 4),
arrowprops=dict(facecolor='black', shrink=0.05))
ax.annotate('local minimum', xy=(5 * np.pi, -1), xytext=(2, -6),
arrowprops=dict(arrowstyle="->",
connectionstyle="angle3,angleA=0,angleB=-90"));
图 32-5. 注释示例
箭头样式由 arrowprops 字典控制,其中有许多可用选项。这些选项在 Matplotlib 的在线文档中有很好的记录,因此不重复在此介绍,更有用的是展示一些示例。让我们使用之前的出生率图来演示几种可能的选项(参见 图 32-6)。
In [9]: fig, ax = plt.subplots(figsize=(12, 4))
births_by_date.plot(ax=ax)
# Add labels to the plot
ax.annotate("New Year's Day", xy=('2012-1-1', 4100), xycoords='data',
xytext=(50, -30), textcoords='offset points',
arrowprops=dict(arrowstyle="->",
connectionstyle="arc3,rad=-0.2"))
ax.annotate("Independence Day", xy=('2012-7-4', 4250), xycoords='data',
bbox=dict(boxstyle="round", fc="none", ec="gray"),
xytext=(10, -40), textcoords='offset points', ha='center',
arrowprops=dict(arrowstyle="->"))
ax.annotate('Labor Day Weekend', xy=('2012-9-4', 4850), xycoords='data',
ha='center', xytext=(0, -20), textcoords='offset points')
ax.annotate('', xy=('2012-9-1', 4850), xytext=('2012-9-7', 4850),
xycoords='data', textcoords='data',
arrowprops={'arrowstyle': '|-|,widthA=0.2,widthB=0.2', })
ax.annotate('Halloween', xy=('2012-10-31', 4600), xycoords='data',
xytext=(-80, -40), textcoords='offset points',
arrowprops=dict(arrowstyle="fancy",
fc="0.6", ec="none",
connectionstyle="angle3,angleA=0,angleB=-90"))
ax.annotate('Thanksgiving', xy=('2012-11-25', 4500), xycoords='data',
xytext=(-120, -60), textcoords='offset points',
bbox=dict(boxstyle="round4,pad=.5", fc="0.9"),
arrowprops=dict(
arrowstyle="->",
connectionstyle="angle,angleA=0,angleB=80,rad=20"))
ax.annotate('Christmas', xy=('2012-12-25', 3850), xycoords='data',
xytext=(-30, 0), textcoords='offset points',
size=13, ha='right', va="center",
bbox=dict(boxstyle="round", alpha=0.1),
arrowprops=dict(arrowstyle="wedge,tail_width=0.5", alpha=0.1));
# Label the axes
ax.set(title='USA births by day of year (1969-1988)',
ylabel='average daily births')
# Format the x-axis with centered month labels
ax.xaxis.set_major_locator(mpl.dates.MonthLocator())
ax.xaxis.set_minor_locator(mpl.dates.MonthLocator(bymonthday=15))
ax.xaxis.set_major_formatter(plt.NullFormatter())
ax.xaxis.set_minor_formatter(mpl.dates.DateFormatter('%h'));
ax.set_ylim(3600, 5400);
各种选项使annotate功能强大且灵活:您可以创建几乎任何箭头样式。不幸的是,这也意味着这些功能通常必须手动调整,这在生成出版质量的图形时可能非常耗时!最后,我要指出,前述样式混合绝不是展示数据的最佳实践,而是作为展示某些可用选项的示例。
更多关于可用箭头和注释样式的讨论和示例可以在 Matplotlib 的注释教程中找到。
图 32-6. 按天平均出生率的注释^(3)
^(1) 该图的完整版本可以在GitHub找到。
^(2) 该图的完整版本可以在GitHub找到。
^(3) 该图的完整版本可以在GitHub找到。
第三十三章:自定义刻度
Matplotlib 的默认刻度定位器和格式化程序通常在许多常见情况下是足够的,但并不是每种图表类型都是最佳选择。本章将给出几个示例,调整特定图表类型的刻度位置和格式化。
然而,在我们进入示例之前,让我们再多谈一些 Matplotlib 图表的对象层次结构。Matplotlib 的目标是让每个出现在图表上的东西都有一个 Python 对象来表示:例如,回想一下Figure是包围所有图表元素的边界框。每个 Matplotlib 对象也可以作为子对象的容器:例如,每个Figure可以包含一个或多个Axes对象,每个Axes对象又包含表示图表内容的其他对象。
刻度标记也不例外。每个轴都有属性xaxis和yaxis,这些属性又包含组成轴的线条、刻度和标签的所有属性。
主要和次要刻度
在每个轴上,有一个主要刻度和一个次要刻度的概念。顾名思义,主要刻度通常较大或更显著,而次要刻度通常较小。默认情况下,Matplotlib 很少使用次要刻度,但你可以在对数图中看到它们的一种情况(见图 33-1)。
In [1]: import matplotlib.pyplot as plt
plt.style.use('classic')
import numpy as np
%matplotlib inline
In [2]: ax = plt.axes(xscale='log', yscale='log')
ax.set(xlim=(1, 1E3), ylim=(1, 1E3))
ax.grid(True);
图 33-1. 对数刻度和标签的示例
在这张图表中,每个主要刻度显示一个大的刻度标记、标签和网格线,而每个次要刻度显示一个更小的刻度标记,没有标签或网格线。
这些刻度属性——即位置和标签——可以通过设置每个轴的formatter和locator对象来自定义。让我们来看看刚刚显示的图表的 x 轴:
In [3]: print(ax.xaxis.get_major_locator())
print(ax.xaxis.get_minor_locator())
Out[3]: <matplotlib.ticker.LogLocator object at 0x1129b9370>
<matplotlib.ticker.LogLocator object at 0x1129aaf70>
In [4]: print(ax.xaxis.get_major_formatter())
print(ax.xaxis.get_minor_formatter())
Out[4]: <matplotlib.ticker.LogFormatterSciNotation object at 0x1129aaa00>
<matplotlib.ticker.LogFormatterSciNotation object at 0x1129aac10>
我们看到主要和次要刻度标签的位置都由LogLocator指定(这对于对数图是有意义的)。然而,次要刻度的标签由NullFormatter格式化:这表示不会显示任何标签。
现在我们来看几个例子,设置这些定位器和格式化程序用于不同的图表。
隐藏刻度或标签
或许最常见的刻度/标签格式化操作是隐藏刻度或标签。可以使用plt.NullLocator和plt.NullFormatter来完成,如此处所示(见图 33-2)。
In [5]: ax = plt.axes()
rng = np.random.default_rng(1701)
ax.plot(rng.random(50))
ax.grid()
ax.yaxis.set_major_locator(plt.NullLocator())
ax.xaxis.set_major_formatter(plt.NullFormatter())
图 33-2. 隐藏刻度标签(x 轴)和隐藏刻度(y 轴)的图表
我们已删除了 x 轴的标签(但保留了刻度/网格线),并从 y 轴删除了刻度(因此也删除了标签和网格线)。在许多情况下没有刻度可能很有用,例如当您想展示一组图像网格时。例如,考虑包含不同人脸图像的 图 33-3,这是监督机器学习问题中经常使用的示例(例如,参见 第 43 章):
In [6]: fig, ax = plt.subplots(5, 5, figsize=(5, 5))
fig.subplots_adjust(hspace=0, wspace=0)
# Get some face data from Scikit-Learn
from sklearn.datasets import fetch_olivetti_faces
faces = fetch_olivetti_faces().images
for i in range(5):
for j in range(5):
ax[i, j].xaxis.set_major_locator(plt.NullLocator())
ax[i, j].yaxis.set_major_locator(plt.NullLocator())
ax[i, j].imshow(faces[10 * i + j], cmap='binary_r')
图 33-3. 在图像绘制中隐藏刻度
每个图像显示在自己的轴上,并且我们将刻度定位器设置为 null,因为刻度值(在这种情况下是像素数)不传达有关此特定可视化的相关信息。
减少或增加刻度的数量
默认设置的一个常见问题是较小的子图可能会有拥挤的标签。我们可以在这里显示的图网格中看到这一点(见图 33-4)。
In [7]: fig, ax = plt.subplots(4, 4, sharex=True, sharey=True)
图 33-4. 具有拥挤刻度的默认图
特别是对于 x 轴的刻度,数字几乎重叠在一起,使它们很难辨认。调整的一种方法是使用 plt.MaxNLocator,它允许我们指定将显示的最大刻度数。在给定这个最大数目后,Matplotlib 将使用内部逻辑选择特定的刻度位置(见图 33-5)。
In [8]: # For every axis, set the x and y major locator
for axi in ax.flat:
axi.xaxis.set_major_locator(plt.MaxNLocator(3))
axi.yaxis.set_major_locator(plt.MaxNLocator(3))
fig
图 33-5. 自定义刻度数目
这样做会使事情更加清晰。如果您希望更加精确地控制定期间隔刻度的位置,还可以使用 plt.MultipleLocator,我们将在以下部分讨论。
特别的刻度格式
Matplotlib 的默认刻度格式可能不尽如人意:它作为一个广泛的默认选项效果不错,但有时你可能想做些不同的事情。考虑这个正弦和余弦曲线的图(见图 33-6)。
In [9]: # Plot a sine and cosine curve
fig, ax = plt.subplots()
x = np.linspace(0, 3 * np.pi, 1000)
ax.plot(x, np.sin(x), lw=3, label='Sine')
ax.plot(x, np.cos(x), lw=3, label='Cosine')
# Set up grid, legend, and limits
ax.grid(True)
ax.legend(frameon=False)
ax.axis('equal')
ax.set_xlim(0, 3 * np.pi);
图 33-6. 具有整数刻度的默认图
注意
全彩色图像在 GitHub 的补充材料 上可用。
这里可能有一些我们想做的改变。首先,对于这些数据来说,在 π 的倍数间隔刻度和网格线更自然。我们可以通过设置 MultipleLocator 来实现这一点,它将刻度定位在我们提供的数字的倍数上。为了保险起见,我们将添加 π/2 和 π/4 的主要和次要刻度(见图 33-7)。
In [10]: ax.xaxis.set_major_locator(plt.MultipleLocator(np.pi / 2))
ax.xaxis.set_minor_locator(plt.MultipleLocator(np.pi / 4))
fig
图 33-7. π/2 和 π/4 的倍数处的刻度
但现在这些刻度标签看起来有点傻:我们可以看到它们是π的倍数,但小数表示并不能立即传达这一点。为了解决这个问题,我们可以改变刻度的格式化方式。我们想要做的没有内置的格式化程序,所以我们将使用plt.FuncFormatter,它接受一个用户定义的函数,可以对刻度输出进行细粒度的控制(见图 33-8)。
In [11]: def format_func(value, tick_number):
# find number of multiples of pi/2
N = int(np.round(2 * value / np.pi))
if N == 0:
return "0"
elif N == 1:
return r"$\pi/2$"
elif N == 2:
return r"$\pi$"
elif N % 2 > 0:
return rf"${N}\pi/2$"
else:
return rf"${N // 2}\pi$"
ax.xaxis.set_major_formatter(plt.FuncFormatter(format_func))
fig
这好多了!请注意,我们已经利用了 Matplotlib 的 LaTeX 支持,通过在字符串中加入美元符号来指定。这对于显示数学符号和公式非常方便:在这种情况下,"$\pi$"被渲染为希腊字母π 。
图 33-8. 自定义标签的刻度
概述格式化程序和定位器
我们已经看到了一些可用的格式化程序和定位器;我将通过列出所有内置定位器选项(表 33-1)和格式化程序选项(表 33-2)来结束本章。有关更多信息,请参阅 docstrings 或 Matplotlib 文档。以下每个都在plt命名空间中可用。
表 33-1. Matplotlib 定位器选项
| 定位器类 | 描述 |
|---|---|
NullLocator | 没有刻度 |
FixedLocator | 刻度位置固定 |
IndexLocator | 用于索引图的定位器(例如,当x = range(len(y)))时 |
LinearLocator | 从最小到最大均匀间隔的刻度 |
LogLocator | 对数间隔的刻度,从最小到最大 |
MultipleLocator | 刻度和范围是基数的倍数 |
MaxNLocator | 在良好位置找到最大数量的刻度 |
AutoLocator | (默认)带有简单默认值的MaxNLocator |
AutoMinorLocator | 次要刻度的定位器 |
表 33-2. Matplotlib 格式化程序选项
| 格式化程序类 | 描述 |
|---|---|
NullFormatter | 刻度上没有标签 |
IndexFormatter | 从标签列表中设置字符串 |
FixedFormatter | 手动设置标签的字符串 |
FuncFormatter | 用户定义的函数设置标签 |
FormatStrFormatter | 使用每个值的格式字符串 |
ScalarFormatter | 标量值的默认格式化程序 |
LogFormatter | 对数轴的默认格式化程序 |
我们将在本书的其余部分看到更多这些示例。
第三十四章:自定义 Matplotlib:配置和样式表
虽然前面章节涵盖的许多主题都涉及逐个调整绘图元素的样式,但 Matplotlib 也提供了机制来一次性调整图表的整体样式。在本章中,我们将介绍一些 Matplotlib 的运行时配置(rc)选项,并查看包含一些漂亮的默认配置集的 stylesheets 功能。
手动绘图定制
在本书的这一部分中,你已经看到如何调整单个绘图设置,使其看起来比默认设置更好看一些。也可以为每个单独的图进行这些自定义。例如,这里是一个相当沉闷的默认直方图,如图 34-1 所示。
In [1]: import matplotlib.pyplot as plt
plt.style.use('classic')
import numpy as np
%matplotlib inline
In [2]: x = np.random.randn(1000)
plt.hist(x);
图 34-1. Matplotlib 默认样式下的直方图
我们可以手动调整它,使其成为一个视觉上更加愉悦的图,如你可以在图 34-2 中看到的那样。
In [3]: # use a gray background
fig = plt.figure(facecolor='white')
ax = plt.axes(facecolor='#E6E6E6')
ax.set_axisbelow(True)
# draw solid white gridlines
plt.grid(color='w', linestyle='solid')
# hide axis spines
for spine in ax.spines.values():
spine.set_visible(False)
# hide top and right ticks
ax.xaxis.tick_bottom()
ax.yaxis.tick_left()
# lighten ticks and labels
ax.tick_params(colors='gray', direction='out')
for tick in ax.get_xticklabels():
tick.set_color('gray')
for tick in ax.get_yticklabels():
tick.set_color('gray')
# control face and edge color of histogram
ax.hist(x, edgecolor='#E6E6E6', color='#EE6666');
图 34-2. 带有手动自定义的直方图
这看起来更好,你可能会认出其灵感来自 R 语言的 ggplot 可视化包。但这需要大量的工作!我们绝对不希望每次创建图表时都进行这些调整。幸运的是,有一种方法可以一次性调整这些默认设置,适用于所有图表。
更改默认设置:rcParams
每次 Matplotlib 加载时,它定义一个运行时配置,包含您创建的每个绘图元素的默认样式。可以随时使用 plt.rc 方便函数调整此配置。让我们看看如何修改 rc 参数,以便我们的默认图表看起来与之前类似。
我们可以使用 plt.rc 函数来更改其中一些设置:
In [4]: from matplotlib import cycler
colors = cycler('color',
['#EE6666', '#3388BB', '#9988DD',
'#EECC55', '#88BB44', '#FFBBBB'])
plt.rc('figure', facecolor='white')
plt.rc('axes', facecolor='#E6E6E6', edgecolor='none',
axisbelow=True, grid=True, prop_cycle=colors)
plt.rc('grid', color='w', linestyle='solid')
plt.rc('xtick', direction='out', color='gray')
plt.rc('ytick', direction='out', color='gray')
plt.rc('patch', edgecolor='#E6E6E6')
plt.rc('lines', linewidth=2)
有了这些设置定义,现在我们可以创建一个图表,并看到我们的设置如何生效(参见图 34-3)。
In [5]: plt.hist(x);
图 34-3. 使用 rc 设置的自定义直方图
让我们看看使用这些 rc 参数的简单线图的外观(参见图 34-4)。
In [6]: for i in range(4):
plt.plot(np.random.rand(10))
图 34-4. 具有自定义样式的线图
对于在屏幕上查看而不是打印的图表,我发现这比默认样式更具美感。如果你对我的审美感觉不同,好消息是你可以调整 rc 参数以适应自己的喜好!可选地,这些设置可以保存在 .matplotlibrc 文件中,你可以在 Matplotlib 文档 中了解更多。
样式表
调整整体图表样式的较新机制是通过 Matplotlib 的 style 模块,其中包括一些默认样式表,以及创建和打包自己的样式的功能。这些样式表的格式类似于之前提到的 .matplotlibrc 文件,但必须以 .mplstyle 扩展名命名。
即使您不打算创建自己的样式,您可能会在内置样式表中找到所需的内容。plt.style.available 包含可用样式的列表——为简洁起见,这里我只列出前五个:
In [7]: plt.style.available[:5]
Out[7]: ['Solarize_Light2', '_classic_test_patch', 'bmh', 'classic',
>'dark_background']
切换样式表的标准方法是调用 style.use:
plt.style.use('*`stylename`*')
但请记住,这将改变 Python 会话的剩余部分的样式!或者,您可以使用样式上下文管理器,临时设置样式:
with plt.style.context('*`stylename`*'):
make_a_plot()
为了演示这些样式,让我们创建一个函数,用于制作两种基本类型的图表:
In [8]: def hist_and_lines():
np.random.seed(0)
fig, ax = plt.subplots(1, 2, figsize=(11, 4))
ax[0].hist(np.random.randn(1000))
for i in range(3):
ax[1].plot(np.random.rand(10))
ax[1].legend(['a', 'b', 'c'], loc='lower left')
我们将使用这些样式来探索使用各种内置样式的图表外观。
注意
全彩色图像可在 GitHub 上的补充材料 中找到。
默认风格
Matplotlib 的 default 风格在 2.0 版本中有更新;我们先来看看这个(见 Figure 34-5)。
In [9]: with plt.style.context('default'):
hist_and_lines()
Figure 34-5. Matplotlib 的 default 风格
FiveThiryEight 风格
fivethirtyeight 风格模仿了流行的 FiveThirtyEight 网站 上的图形。正如您在 Figure 34-6 中看到的那样,它以鲜明的颜色、粗线条和透明的坐标轴为特征:
In [10]: with plt.style.context('fivethirtyeight'):
hist_and_lines()
Figure 34-6. fivethirtyeight 风格
ggplot 风格
R 语言中的 ggplot 包是数据科学家中流行的可视化工具。Matplotlib 的 ggplot 风格模仿了该包的默认样式(见 Figure 34-7)。
In [11]: with plt.style.context('ggplot'):
hist_and_lines()
Figure 34-7. ggplot 风格
贝叶斯方法为黑客风格
有一本名为 Probabilistic Programming and Bayesian Methods for Hackers 的简短在线书籍,由 Cameron Davidson-Pilon 撰写,其中使用 Matplotlib 创建的图表,并使用一组漂亮的 rc 参数创建了一致且视觉上吸引人的风格。这种风格在 bmh 样式表中得以再现(见 Figure 34-8)。
In [12]: with plt.style.context('bmh'):
hist_and_lines()
Figure 34-8. bmh 风格
Dark Background 风格
对于在演示中使用的图像,与其使用浅色背景,不如使用深色背景更为有用。dark_background 风格提供了这种选择(见 Figure 34-9)。
In [13]: with plt.style.context('dark_background'):
hist_and_lines()
Figure 34-9. dark_background 风格
灰度风格
你可能会发现自己正在为不接受彩色图的印刷出版物准备图表。对此,grayscale 风格(见图 34-10)可能会很有用。
In [14]: with plt.style.context('grayscale'):
hist_and_lines()
图 34-10. grayscale 风格
Seaborn 风格
Matplotlib 也有几种受 Seaborn 库启发的样式表(详见第三十六章)。我发现这些设置非常不错,通常将它们作为自己数据探索的默认设置(见图 34-11)。
In [15]: with plt.style.context('seaborn-whitegrid'):
hist_and_lines()
图 34-11. seaborn 绘图风格
请花些时间探索内置选项,并找到一个适合你的风格!在本书中,当创建图表时,我通常会使用其中一种或多种风格约定。