为什么 pyplot.contour() 要求 Z 是一个二维数组?

Why does pyplot.contour() require Z to be a 2D array?

matplotlib.pyplot.contour() 函数接受 3 个输入数组 XYZ
数组 XY 指定点的 x 和 y 坐标,而 Z 指定在点处评估的感兴趣函数的相应值。

我知道 np.meshgrid() 可以轻松生成用作 contour():

参数的数组
X = np.arange(0,5,0.01)
Y = np.arange(0,3,0.01)

X_grid, Y_grid = np.meshgrid(X,Y)
Z_grid = X_grid**2 + Y_grid**2

plt.contour(X_grid, Y_grid, Z_grid)  # Works fine

这很好用。方便的是,这也很好用:

plt.contour(X, Y, Z_grid)  # Works fine too

但是,为什么 Z 输入 需要 是一个二维数组?

为什么像下面这样的东西是不允许的,即使它指定了所有相同的数据正确对齐?

plt.contour(X_grid.ravel(), Y_grid.ravel(), Z_grid.ravel())  # Disallowed

还有,当指定onlyZ时(没有相应的XY)语义是什么?

plt.contour背后算法的实际代码可以在_countour.cpp中找到。它相当复杂 C-code,因此很难准确地遵循它,但如果我试图编写一些 contours-generating 代码,我会按以下方式进行。在边界处选择一些点 (x, y) 并固定其 z 值。遍历附近的点并选择 z-value 最接近第一个点的 z-value 的点。继续迭代新点,选择附近的 z-value 最接近所需的点(但请检查你没有 return 到你刚访问过的点,所以你必须进入一些 "direction"), 并继续直到你得到一个循环或到达某个边界。

似乎在 _counter.cpp 中实现了一些接近(但有点复杂)的东西。

正如您从算法的非正式描述中看到的那样,要继续,您必须找到与当前点 "nearby" 的点。如果您有一个矩形点网格(需要大约 4 或 8 次迭代,如下所示:(x[i+1][j], y[i+1][j])(x[i][j+1], y[i][j+1])(x[i-1][j], y[i-1][j]) 等),这很容易做到。但是如果你有一些随机选择的点(没有任何特定的顺序),这个问题就变得困难了:你必须遍历所有你必须找到附近的点并进行下一步。 complexity of such step is O(n),其中 n 是点的数量(通常是图片大小的正方形)。所以如果你没有矩形网格,算法会变得很慢。

这就是为什么您实际上需要三个二维数组,它们对应于位于某个矩形网格上的某些点的 x、y 和 z。

正如您正确提到的,xy 可以是一维数组。在这种情况下,相应的二维数组用 meshgrid 重建。但是,在这种情况下,无论如何您都必须将 z 作为二维数组。

如果仅指定 z,则 xyrange 的适当长度。

编辑。您可以尝试 "fake" two-dimensional xyz 数组,而 xy 不会形成一个矩形网格来检查我的假设是否正确。

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

x = np.random.uniform(-3, 3, size=10000)
y = np.random.uniform(-3, 3, size=10000)
z = x**2 + y**2
X, Y, Z = (u.reshape(100, 100) for u in (x, y, z))
plt.contour(X, Y, Z)

如您所见,如果 (x, y, z) 只是一些随机点,则图片看起来与正确的图形并不相似。

现在让我们假设 x 被排序为@dhrummel 在评论中建议的预处理步骤。请注意,我们不能同时对 xy 进行排序,因为它们不是独立的(我们希望保留相同的点)。

x = np.random.uniform(-3, 3, size=10000)
y = np.random.uniform(-3, 3, size=10000)
z = x**2 + y**2
xyz = np.array([x, y, z]).T
x, y, z = xyz[xyz[:, 0].argsort()].T
assert (x == np.sort(x)).all()
X, Y, Z = (u.reshape(100, 100) for u in (x, y, z))
plt.contour(X, Y, Z)

同样,图片不正确,因为 y 没有像我们使用矩形网格而不是一些随机点时那样(在每一列中)排序。

查看 the documentation of contour 发现有几种方法可以调用此函数,例如contour(Z)contour(X,Y,Z)。所以您会发现它根本不需要任何 XY 值。

但是,为了绘制等高线,函数必须知道底层网格。 Matplotlib 的 contour 基于矩形网格。但即便如此,允许 contour(z),其中 z 是一维数组,将无法知道应该如何绘制该字段。在 contour(Z) 的情况下,其中 Z 是一个二维数组,它的形状明确地设置了绘图的网格。

一旦网格已知,可选的 XY 数组是否被展平就变得不重要了;这实际上是文档告诉我们的内容:

X and Y must both be 2-D with the same shape as Z, or they must both be 1-D such that len(X) is the number of columns in Z and len(Y) is the number of rows in Z.

也很明显 plt.contour(X_grid.ravel(), Y_grid.ravel(), Z_grid.ravel()) 无法生成等高线图,因为所有关于网格形状的信息都丢失了,等高线函数无法知道如何解释数据。例如。如果 len(Z_grid.ravel()) == 12,底层网格的形状可以是 (1,12), (2,6), (3,4), (4,3), (6,2), (12,1).

中的任何一种

一个可能的出路当然是允许一维数组并引入参数 shape,例如 plt.contour(x,y,z, shape=(6,2))。然而,情况并非如此,因此您必须接受 Z 需要是 2D 的事实。

但是,如果您正在寻找一种方法来获得具有扁平(散乱)数组的计数图,则可以使用 plt.tricontour()

plt.tricontour(X_grid.ravel(), Y_grid.ravel(), Z_grid.ravel()) 

此处将使用 Delaunay 三角剖分在内部生成一个三角形网格。因此,即使是完全随机化的点也会产生不错的结果,如下图所示,将其与给予 contour.

的相同随机点进行比较

(这​​里是code to produce this picture)

X 和 Y 为二维的原因如下。 Z 与坐标轴系统中的每个 (x,y) 坐标相匹配 "depth" 以创建具有 x、y 和 z 坐标的 3D 图。

现在假设我们要指向坐标轴系统中的任意点。 我们可以为此 point.For 示例 (0,0) 提供 x 和 y 坐标 (x,y)。 现在考虑 x 值为 1 的 "line"。这一行有许多 n 个 y 值,看起来像:

如果我们为所有 x 值和 y 值绘制这条线,我们将得到 smth。喜欢:

如您所见,我们有一个 2D 注释,它由 2 个 2D 数组组成,其中一个用于 x 值,其形状为:

1 2 3 4 5 6 7 8 9 10
1 2 3 4 5 6 7 8 9 10
1 2 3 4 5 6 7 8 9 10
1 2 3 4 5 6 7 8 9 10
1 2 3 4 5 6 7 8 9 10
1 2 3 4 5 6 7 8 9 10
1 2 3 4 5 6 7 8 9 10
1 2 3 4 5 6 7 8 9 10
1 2 3 4 5 6 7 8 9 10
1 2 3 4 5 6 7 8 9 10
#--> Two dimensional x values array

还有一个用于 y 值,其形状为:

10 10 10 10 10 10 10 10 10 10
9 9 9 9 9 9 9 9 9 9 
8 8 8 8 8 8 8 8 8 8
...
1 1 1 1 1 1 1 1 1 1
0 0 0 0 0 0 0 0 0 0
#--> Two dimensional y values array

这两个一起提供坐标系内每个点的 (x,y) 坐标。现在我们可以为每个点绘制 "depth" 表示 Z 值(z 坐标)。 现在也很明显为什么 Z 变量必须是具有形状 (len(x),len(y)) 的二维,否则它无法为所有点提供值。

可以通过向函数提供二维 x、y 和 z 数组来实现此行为,或者:向函数提供一维 x 和 y 数组,函数在内部使用 x 和 y 值创建二维网格某事像 X,Y=np.meshgrid(x,y) 但是 z 必须是二维的。

让我简单地解释一下,因为我认为Z也不应该是二维的。 contourf() 需要 X 和 Y 来构建它自己的 space,以及关系 Z(X,Y) 来构建一个完整的 space,而不是仅仅使用几个点和 1D X,Y, Z信息.

假设您想绘制一个 three-dimensional 图表。您有一组 x 点和一组 y 点。目标是为每对 xy 生成一个值 z,或者换句话说,您需要一个函数 f 以便它生成一个值 z 这样 z = f(x, y).

这是一个很好的例子(取自 MathWorks):

xy坐标分别在右下角和左下角。您将有一个函数 f,这样对于每对 xy,我们生成一个 z 值。因此,在您提供的代码中,numpy.meshgrid 调用将生成两个二维数组,这样对于每个唯一的空间位置,我们将观察到该唯一的 xy 值位置。

例如,让我们用一个非常小的例子:

In [1]: import numpy as np

In [2]: x, y = np.meshgrid(np.linspace(-1, 1, 3), np.linspace(-1, 1, 3))
In [3]: x
Out[3]:
array([[-1.,  0.,  1.],
       [-1.,  0.,  1.],
       [-1.,  0.,  1.]])

In [4]: y
Out[4]:
array([[-1., -1., -1.],
       [ 0.,  0.,  0.],
       [ 1.,  1.,  1.]])

例如,看一下第 2 行和第 1 列(顺便说一下,我从 0 开始编制索引)。这意味着在这个空间位置,我们将有坐标 x = 0.y = 1numpy.meshgrid 为我们提供了在该特定坐标处生成 z 值所需的 xy 对。为了方便起见,它只是分成两个二维数组。

现在最后要放入 z 变量的是它应该使用函数 f 并处理 x 中每个值的输出及其对应的 y.

明确地说,您需要制定一个 z 二维数组,这样:

z = [f(-1, -1) f(0, -1) f(1, -1)]
    [f(-1,  0) f(0,  0) f(1,  0)]
    [f(-1,  1) f(0,  1) f(1,  1)]

仔细观察 xy 项的空间排列。我们为每对 xy 值生成 9 个唯一值。 x 值的范围从 -1 到 1,y 也是如此。为 z 生成此二维数组后,您可以使用 contourf 绘制水平集,以便每条等高线将为您提供所有可能的 xy 的集合等于 z 的相同值的值。此外,在每对相邻的不同线条之间,我们用相同的颜色填充它们之间的区域。

让我们用一个实际的例子来结束这个。假设我们有函数 f(x, y) = exp(-(x**2 + y**2) / 10)。这是标准差为 sqrt(5).

的 2D 高斯分布

因此,让我们生成一个 xy 值的网格,使用它来生成 z 值并绘制一个 contourf 图:

import numpy as np
import matplotlib.pyplot as plt
x = np.linspace(-1, 1, 101)
y = x
x, y = np.meshgrid(x, y)
z = np.exp(-(x**2 + y**2) / 10)       
fig,ax2 = plt.subplots(1)    
ax2.contourf(x,y,z)
plt.show()

我们得到: