Matplotlib

在本节,我们将介绍Matplotlib,最长命的用来绘制数据和图片的Python模组。它旨在和NumPy数组一起使用,并内部使用2维和3维数组来储存图片(分别是黑白和RGB图片)。本节将介绍绘制的基础以及Matplotlib的关键功能。

市面上有很多使用Matplotlib的教程,“作弊表”,和其它资源。我们在这里的目的在于提供一个比仅仅向你展示如果绘制一个“像这样的图标”该如何做更加深入一点的讨论。具体来讲,我们的目的是介绍Matplotib的面向对象API,而不是它的函数API。让我们快速地区分这两个API:

import matplotlib.pyplot as plt
# 准备50个x坐标和50个y坐标
x = np.linspace(-np.pi, np.pi, 50)
y = np.sin(x)

# 使用matplotlib的面向对象API来绘图:
# 我们的figure和axis对象:`fig` 和 `ax`
fig, ax = plt.subplots()
# 然后,我们使用这些对象来绘制并操作我们的图
ax.plot(x, y)

# 使用matplotlib的函数API来绘图:
# 单个函数调用创建一张图;方便但没那么灵活
plt.plot(x, y)

虽然调用函数API的代码更加简单,它远远没有会创建可以用来自定义绘制的图的图表(figure)(fig)和轴线(axes)(ax)对象的面向对象API那么强大或灵活。在别的教程中你很可能会看到它们在范例中使用函数API,所以在这里理解两者的区别很重要。马上,你将会学些Matplotlib的面向对象API的强大使用方法。

如果对这个绘图模组的其它信息感兴趣,请咨询Matplotlib用户指南以及它的教程

Matplotlib在Anaconda的Python模组分布中自动被包含。Matplotlib的 pyplot 子模组包含了所有核心的绘图功能,因此我们将永远需要导入 pyplot。在从Matplotlib中导入 pyplot 子模组时使用简写“plt”是标准操作:

# 导入matplotlib的pyplot子模组
import matplotlib.pyplot as plt

在网上有着很多的演示Matplotlib可视化数据的大量功能的教程和例子。在这里,我们仅仅会取有效为数据科学运用理解Matplotlib所需要的核心内容。

在Jupyter记事本中绘图

虽然Matplotlib提供了一个显示界面来帮助我们在Python壳或IPython中绘制数据,Jupyter记事本是创建和完善数据可视化的理想环境。在记事本中执行命令 %matplotlib notebook 会告诉Matplotlib去将它绘制的图在记事本中嵌入,而不是为每一张图创建一个新窗口。

# 指示Jupyter来将绘制的图在记事本中嵌入
%matplotlib notebook

在一个记事本中,这个命令只需要执行一次就可以使得绘制的图在该记事本中嵌入。这也将允许我们和在记事本中渲染的图互动,如横移,放大,和储存。我们带着在向记事本嵌入图的假设继续本节。

最后,请注意 %matplotlib notebook 并不是合法的一行Python代码。它是影响IPython命令行和Jupyter记事本行为的“魔法命令”中的一个。

使用 pyplot.subplots 来创建图表和轴线

在Matplotlib中,你有很多方法可以创建空的图表和轴线对象。在这里,我们将介绍函数 pyplot.subplots,其创建了含有可以绘制数据的轴线的图表。我们将学习如何使用关键词参数 figsize 来控制图表的大小,以及如何使用关键词参数 ncolsnrows 来设置在图表中轴线的分布网格。

在Matploblib中,Figure 对象用来存储一组或多组的 Axes 对象。数组在某一组轴线上绘制。subplots 可以用来在某个用户提供的轴线分布上创建一个图表。subplots 将默认创建只有一组轴线的图表;调用它将返回包含了图表对象和轴线对象的元组:

import matplotlib.pyplot as plt
%matplotlib notebook

# 创建一个包含一组轴线的图表。`plt.subplots()` 返回
# 元组:(图表实例,轴线实例)
fig, ax = plt.subplots()

执行此代码将打开一个有着一组空轴线的新图表。我们可以使用 Axes 实例 ax 来绘制数据,为图添加标签和标题,并添加格线。Figure 实例 fig 控制图表的更高层次的特征,如轴线的分布,标签的对齐,和其它要素。fig 可能在存储图表时最为常用。让我们绘制正弦波的一个周期并储存该图表。

绘制并储存图表

[2]:
# 使用subplots来储存一个有一组轴线的图表并绘制正弦波上的点

import numpy as np
import matplotlib.pyplot as plt
%matplotlib notebook


# 创建一个图表和一组轴线
fig, ax = plt.subplots()

# 准备50个x坐标和50个y坐标
x = np.linspace(-np.pi, np.pi, 50)
y = np.sin(x)

# 在轴线对象上绘制数据。绘制对应的x-y坐标对,由
# `x` 中的x坐标和 `y` 中的y坐标提供
ax.plot(x, y)

# `fig` 对象负责储存整个图表和它的所有的轴线。以下
# 命令将该图表储存为一张png图片到此代码执行的原始文件夹中
fig.savefig("sinewave_plt.png");  # 最后的分号仅仅用来压制命令行的输出

subplots 函数可以接受关键词参数 figsize 来控制图表的大小;它接受包含图表宽和长度的元组(单位为英寸)。轴线对象将拉扯以满足提供的图表大小。

[3]:
# 设定使用 `subplots` 创建的图表的大小
# 创建一个6英寸x2英寸的图表
fig, ax = plt.subplots(figsize=(6,2))
ax.plot(x, y);

关键词参数 nrowsncols 指示该图表去储存 nrows x ncols 个轴线对象并将其在一个同样大小的网格中显示。在这个情况下,subplots 返回的元组的第二个成员不是单个轴线对象,而是一个2维的形状为 (nrows, ngrids) 的NumPy数组,其包含了对应的轴线。

让我们创建一个有着6组轴线的图表并将其在一个 \(2 \times 3\) 的网格中显示:

[4]:
# 演示轴线对象控制的功能之多。

# 创建一额有着多组轴线的对象
fig, axs = plt.subplots(2, 3, figsize=(8, 3)) # 6组轴线,在2x3的网格上
# 请注意,我们也可以为我们的轴线使用元组解包:
# fig, (ax1, ax2, ax3, ax4, ax5, ax6) = plt.subplots(2, 3)


# 用来输入到我们函数的x值范围
x = np.linspace(0, 4*np.pi, 1000)

# 行0,列0:绘制 y = x**2
axs[0, 0].plot(x, x**2, linestyle='--', color='orange')

# 行0,列1:绘制 y = sin(x)
axs[0, 1].plot(x, np.sin(x), color='green')

# 行0,列2:绘制 y = cos(x)
axs[0, 2].plot(x, np.cos(x), linestyle=':')

# 行1,列0:绘制 y = exp(x)
axs[1, 0].plot(x, np.exp(x), color='red', linestyle='-.')

# 行1,列1:绘制 y = sqrt(x)
axs[1, 1].plot(x, np.sqrt(x))

# 行1,列2:绘制 plot y = x
axs[1, 2].plot(x, x, color='purple');

阅读理解:创建基础的图

使用 pyplot.subplots 来创建一个有着两张分别在左右的子图(也就是说它们在两列中)的图表。在左边的子图中,使用100个在域 \([0, 1]\) 之间的点来绘制 \(f(x) = e^{-x}\)。在右边的子图中,使用一条绿色的线来在任何你想要的域上绘制 \(f(x) = x^2\)

轴线对象

轴线(axes)对象用来控制图中数据的样貌。快速扫过轴线对象的官方说明文档会告诉你它几乎是Matplotlib中“统治所有的对象”(译者注:原作者引用了魔戒的“one ring to rule them all“来将轴线描述为the”one object to rule them all”。恕译者文化低翻译不来),因为它有着巨量的功能。它允许我们控制进行的绘制的种类;以下是你可以创建的各种图中的一部分:

  • Axes.plot:有着数据标记的线

  • Axes.scatter:x-y标记的分布图,没有线

  • Axes.hist:直方图

  • Axes.bar:条形统计图

  • Axes.pie:饼图

  • Axes.imshow:在轴线中绘图

  • Axes.contour:等高线图

更多的是,轴线对象控制轴的刻度(如,对数刻度vs线性刻度)。它允许我们创建轴标签,标题,并控制更加细致的特征,如网格线,轴上标记的频繁度,和很多其它设定。总而言之,你将会使用轴线对象来控制图的大部分特征。因此,本文强烈建议你熟悉轴线对象可用的函数。我们在这里只能演示它能力的一部分;我们将会优先演示它最核心的功能并以此创建多样的图。

如上一个例子所见的一般,ax.plot 创建一个由线段连接数据点的图。请注意,它默认不会绘制每个数据点的标记。我们可以使用 markerlinestyle 关键词参数来分别设置数据点的标记的样子和连接它们的线段的样子。你可以在这里找到可用的标记风格,并在这里找到可用的线段风格。

设某个 Axes 实例 axax.plot 将会绘制那组轴线;Matplotlib将会自动循环各种颜色来区分每组绘制的数据。我们可以为每个数据组提供一个标签,并使用 ax.legend() 来显示这些标签。

[5]:
# 在一组轴线上绘制多个曲线

fig, ax = plt.subplots()

x = np.linspace(-2*np.pi, 2*np.pi, 32)

# 圆和虚线
ax.plot(x, np.sin(x)/(2*x), marker="o", linestyle="--", label='a')

# 正方形和虚点线
ax.plot(x, np.sin(x)/(3*x), marker="s", linestyle="-.", label='b')

# 朝上的三角形和点线
ax.plot(x, np.sin(x)/(4*x), marker="^", linestyle=":", label='c')

# 朝左的三角形,没有线
ax.plot(x, np.sin(x)/(5*x), marker="<", linestyle="", label='d')

# 默认:实线,没有标记
ax.plot(x, np.sin(x)/(6*x), label='e')

# 在右上角添加标签的说明
ax.legend(loc="upper right")

# 为轴线标注标签和标题
ax.set_xlabel("x label")
ax.set_ylabel("y label")
ax.set_title("Title")

# 打开x-y网格线
ax.grid(True)

想再,我们将提供可以使用轴线对象创建的不同图的大杂烩。返回的 \(2 \times 3\) 图网格将演示以下:

图 (0, 0)

  • 使用 Axes.set_yscale 来设置y轴为对数刻度。

  • 使用LaTeX格式的等式为标签。

图 (0, 1)

  • 使用 Axes.fill_between 来涂色曲线的错误区间。

  • alpha 关键词参数允许你设置涂色的透明度。

  • 设置 color=C0 允许你对应Matplotlib默认颜色循环的颜色0。

图 (0, 2)

  • 使用 Axes.errorbar 来绘制有着离散错误区间的曲线。

图 (1, 0)

  • 使用 Axes.scatter 来为两组x-y坐标绘制标记但不用线段连接标记。

图 (1, 1)

  • 使用 Axes.hist 来创建直方图(histogram),其中有50个框(bin),来显示高斯分布的数据。

图 (1, 2)

  • 使用 Axes.imshow 来将一个矩阵(2维NumPy数组)绘制为一张图片。矩阵的值是随机的,且矩阵左下一半的三角部分被设为0.

  • 成员的大小由Matplotlib默认的翠绿色颜色图显示。

[6]:
# 演示图类型的多样性
fig, axs = plt.subplots(nrows=2, ncols=3)

################# 图 (0, 0) ##########################
# 演示设置y轴为对数刻度
x = np.linspace(0, 4, 1000)
y = x**2
axs[0, 0].plot(x, y, linestyle='--', color='red', label=r'$x^2$')
axs[0, 0].plot(x, np.exp(x), linestyle='--', color='blue', label=r'$e^x$')
axs[0, 0].set_yscale("log")
axs[0, 0].legend()


################# 图 (0, 1) ##########################
# 演示 `ax.fill_between` 和 `alpha`
x = np.linspace(0, 10, 1000)
y = np.sqrt(x)
err_lower, err_upper = np.random.random((2, 1000))*0.2  # 创建的错误区间
axs[0, 1].plot(x, y)

# 在这之间涂色:(y - err_lower) 和 (y + err_upper)
# alpha 设置颜色的不透明度:0.2 使得涂色的区间半透明
axs[0, 1].fill_between(x, y-err_lower, y+err_upper, color='C0', alpha=0.2)


################# 图 (0, 2) ##########################
# 演示错误区间图
x = np.arange(10)
y = np.sqrt(x)

# 每个 y 对应着低和高的错误边界,在2维数组中由行区分
yerr = np.random.random((2, 10))
axs[0, 2].errorbar(x, y, yerr, color="C2")


################# 图 (1, 0) ##########################
# 演示分布图
x = np.arange(10)
y = np.random.random(10)
axs[1, 0].scatter(x, y, marker='x')
axs[1, 0].scatter(x, 1-y, marker='o')


################# 图 (1, 1) ##########################
# 演示直方图
# 随机从高斯分布获取 1000 个数字,存入形状为 (1000,) 的NumPy数组。
data = np.random.randn(1000)
# 在一个50框的直方图中显示。
axs[1, 1].hist(data, bins=50)


################# 图 (1, 2) ##########################
# 将2维数组绘制为图片

# 随机的 10x10 数组,其左下三角为 0
matrix = np.triu(np.random.rand(10, 10))
axs[1, 2].imshow(matrix);

图表对象(储存图表)

Axes 实例一同被 plt.subplots 创建的是一个 Figure 对象。像 Axes 一样,Figure 也有着大量的功能可供使用。它的说明文档可以在这里找到。它的核心功能之一在于通过方法 Figure.savefig 来储存图表。在提供文件名的同时,你可以设置各种文件拓展名,如pdf,jpg,png,和eps。图片将会以对应的格式储存。同时,你可以通过设置参数 dpi(dots per inch)来控制图片的分辨率。

在储存图之外,Figure 对象可以用来获取和设置关于我们图表的信息,如图表的大小(单位是英寸),它的分辨率,和它的背景颜色。

[7]:
fig, ax = plt.subplots(figsize=(3, 3))
x = np.linspace(-np.pi, np.pi, 50)
y = np.sin(x)
ax.plot(x, y)


# 将图表存为pdf
fig.savefig("sinewave_plt.pdf")

# 将图表存为一个200dpi的png
fig.savefig("sinewave_plt.png", dpi=200);

阅读理解:储存图

在同一个子图中,x范围 \([-\pi, \pi]\) 上,绘制振幅(amplitude)分别为1,2,4,和8的正弦函数的图(例,\(2\sin{x}\) 是振幅为2的正弦函数)。将x轴标签设为 "x [radians]"。将该图表存为“sine_waves.png”。然后,打开这张png文件并确定它和你想象的长得一样。

显示图片

Matplotlib也提供了显示图片的支持。使用 Axes 对象,我们将使用它的 imshow 方法来显示一张图片。在这里,“图片”可以指任何“看起来像”图片的东西。比如说,一个2维的NumPy数组可以被理解为一张黑白照片,其行和列为像素为止,其值为像素亮度。图片也可以是 \(r \times c \times 3\) 形状的数组,其中 \(r\)\(c\) 对应像素的行和列,而3代表了3个颜色频道红绿蓝(RGB)的像素值,或 \(r \times c \times 4\) 形状的数组,其颜色频道为红绿蓝和代表透明度的阿尔法(alpha)(RGBA)。

让我们绘制一些图。让我们创建一张50x75x3的RGB图来分为三个竖条。最左的条为红,中间的条为绿,且右边的条为蓝。我们将创建一个成员为0的NumPy数组并对其按照此规律设置值。我们的数组将储存无正负的8比特整数(unsigned 8-bit integers)(也就是“uint8”);一个uint8数可以储存 \(2^8 = 256\) 个不同的值:0,1,…,255。因此,一个值为 (255, 0, 0) 的RGB频道会是红的,(0, 255, 0) 会是绿的,且 (0, 0, 255) 会是蓝的。我们也可以提供浮点数数组,而不是uint8值数组。在这个情况下,我们的颜色值将会在范围 \([0, 1]\) 中。以上内容都在 plt.imshow 中有记录。

[8]:
# 生成并显示RGB图
# 图片的形状为 高度=50,宽度=75,颜色频道=3
# 频道0:红
# 频道1:绿
# 频道2:蓝
my_image = np.zeros((50, 75, 3)) # 形状为 (50, 75, 3) 的数组

# 设置列0-25之间的红频道
my_image[:, :25, 0] = 255

# 置列25-50之间的绿频道
my_image[:, 25:50, 1] = 255

# 置列50-75之间的蓝频道
my_image[:, 50:75, 2] = 255

my_image = my_image.astype('uint8')

fig, ax = plt.subplots()
ax.imshow(my_image);

现在,让我们创建一个“量级”或“黑白”图片,也就是只有一个颜色频道的图片。让我们创建一个 15x25 的图片,其值在它25列中从0到100线性增加。这个增加的过程在每一行都一样。我们可以从Matplotlib的各个颜色图中选择来显示这张图片。在考上的图中,我们将使用黑白灰。在靠下的图中,我们选择感官上统一的“翠绿色”(viridis)颜色图。

[9]:
# 生成并显示黑白图
fig, (ax1, ax2) = plt.subplots(nrows=2)

# 在形状为25的数组中创建一个线性的0-100的增加过程,
# 然后将其广播成一个形状为 (15, 25) 的数组
my_image = np.broadcast_to(np.linspace(0, 100, 25), (15, 25))
ax1.imshow(my_image, cmap="gray")
ax2.imshow(my_image, cmap="viridis");

最后,请注意 plt.imsave 可以将你的NumPy数组作为图片文件存储,且 plt.imread 可以读取图片文件并将其读为一个NumPy数组(所以 imsaveimread 是互相的逆操作)。

Matplotlib之外

虽然Matplotlib是Python最长命的绘图模组,它并不是唯一的玩家,它也不是为所有类的可视化工作最好的工具。比如说,Matplotlib并不适合绘制大量数据,且它对动态的交互图支持也不好。所以说,花些时间来了解其它的一些设计更加专注于特定工作并使用更加现代的绘图技巧的Python绘图模组是很值得的。

ToyPlot提供了一个简约方便的界面来开发漂亮的可交互的动态图。它全盘接受了电子发表的独特能力并支持可复制性。

Bokeh是一个可交互的可视化模组,其目标平台为现代网络浏览器。它的目的是提供优雅,简短的创建多用可视媒体的能力,以及通过对大数据集或数据流提供高效率的交互能力来进行拓展它的功能。

Plotly有一个可以在线创造可互动,发布级别质量的图表的Python绘图模组。它和Bokeh提供类似的功能并使用JavaScript来创建优雅的互动可视化功能。Plotly和Bokeh有着它们对应的长处和短处。

阅读理解答案:

创建基础的图:解

使用 pyplot.subplots 来创建一个有着两张分别在左右的子图(也就是说它们在两列中)的图表。在左边的子图中,使用100个在域 \([0, 1]\) 之间的点来绘制 \(f(x) = e^{-x}\)。在右边的子图中,使用一条绿色的线来在任何你想要的域上绘制 \(f(x) = x^2\)

import matplotlib.pyplot as plt
import numpy as np
%matplotlib notebook

fig, ax = plt.subplots(ncols=2)

x_left = np.linspace(0, 1, 100) # 100个在 [0, 1] 均匀分布的点
ax[0].plot(x_left, np.exp(-x_left))

x_right = np.linspace(-10, 10, 500)
ax[1].plot(x_right, (x_right)**2, color="green")

储存图:解

在同一个子图中,x范围 \([-\pi, \pi]\) 上,绘制振幅(amplitude)分别为1,2,4,和8的正弦函数的图(例,\(2\sin{x}\) 是振幅为2的正弦函数)。将x轴标签设为 "x [radians]"。将该图表存为“sine_waves.png”。然后,打开这张png文件并确定它和你想象的长得一样。

fig, ax = plt.subplots()
x = np.linspace(-np.pi, np.pi, 200)

for amp in [1, 2, 4, 8]:
    ax.plot(x, amp * np.sin(x))

ax.set_xlabel("x [radians]")
fig.savefig("sine_waves.png")