为什么更改输入参数会导致 matplotlib 的 Axes3D.plot() 和 .scatter() 方法表现不同?

4
在发布问题的代码(如何使用Python绘制多条三维曲线图?)中,调用了两次plot方法,并且由于需要绘制的点没有被重置,因此线条会在另一条线条上面绘制。但是如果我们改用scatter方法进行尝试,则可以看到在不同位置绘制的点。为什么会产生这种行为上的变化呢?
from mpl_toolkits.mplot3d import Axes3D
import matplotlib.pyplot as plt
import numpy as np
import math as mt
from mpl_toolkits.mplot3d import Axes3D


t=2  #t can be changed

fig = plt.figure()
ax=Axes3D(fig)

#data

def unitilize(x,y,z):
    r=mt.sqrt(x**2+y**2+z**2)
    return x/r, y/r, z/r

def g_1(x,y,z):
    x=t*x                
    z=z/t                
    x,y,z=unitilize(x,y,z)
    return x,y,z

stepCnt=10000            ######step 
#########data#################
xs = np.empty((stepCnt + 1,))
ys = np.empty((stepCnt + 1,))
zs = np.empty((stepCnt + 1,))

#Setting initial values
def huatu(x,y,z):   

    xs[0], ys[0], zs[0] =unitilize(x,y,z)

    for i in range(stepCnt):
        xs[i+1],ys[i+1],zs[i+1]=g_1(xs[i], ys[i], zs[i])
    return xs,ys,zs


xs3,ys3,zs3=huatu(1,10,40)
ax.plot(xs3, ys3, zs3, color='b', marker='x')

xs2,ys2,zs2=huatu(1,0,40)
ax.plot(xs2, ys2, zs2, color='r', marker='o')
plt.show()

图表输出: 这里输入图片描述

散点图输出: 这里输入图片描述


标题似乎与问题不符。 - derekdreery
2
在调用第一个plot()方法后,值会发生改变。 xs3,ys3,zs3 = huatu(1,10,40) ax.plot(xs3,ys3,zs3,color ='b',marker ='x')xs2,ys2,zs2 = huatu(1,0,40)// 这里 但是第一个plot方法也使用了更改后的值。在scatter()的情况下,这些更改后的值不会被使用。所以我的意思是scatter()会立即发生。但是当我们调用plot()时,它不会立即发生。 - Sabeer Ebrahim
1
@SabeerEbrahim 你应该将那个评论编辑到你的问题中(在代码之后)。 - LinkBerest
1个回答

2
所以,你发现了一些非常奇怪的东西,但我无法准确地追踪到它的来源。最核心的问题是由Axes3D.plot(和实际创建这些对象的Axes.plot)绘制的线条并不复制其输入数据,而是使用视图进行操作。这意味着如果数据随后被更改,则绘图可能会发生变化。出于某种原因,也使用视图的Axes.plot则不会重现这种可变性。这可能与Axes3D对象的更新方式有关,我不太确定。
无论如何,另一方面,Axes3D.scatter创建PathCollection对象(转换为PathCollection3D),其内部工作要复杂得多。据我所知,这些对象(已在2d中)使用._offsets属性进行操作,该属性是由输入坐标构建的ndarray。按设计,这些数组与输入数据独立。
让我们比较一下plot的情况,以便更好地理解我的意思。对于通常的二维绘图:
import numpy as np
import matplotlib.pyplot as plt

fig,ax = plt.subplots()

# first set data to zero
# we'll use an ndarray as input, otherwise there's no chance to get a view
x = np.arange(3)
y = np.array([0.0,0.0,0.0])

# plot the flat line
pl, = ax.plot(x,y,'o-')

# change the axes for better comparison later; not actually relevant
ax.set_ylim([0,4])

# see that the input data are kept as views
print(pl.get_xdata().base is x)  # True
print(pl.get_ydata().base is y)  # True

# mutating x would actually change pl.get_xdata() and vice versa

# mutate y to get a nontrivial line
y[:] = [1,2,3]

# update the canvas in an interactive plot
# plt.show() probably suffices non-interactively
fig.canvas.draw()

plt.show()

结果包含原始的平坦零线:

2d plot case

请注意,中间的几个print调用验证由plot创建的线对象附加的数据确实是输入数据的视图(而不是副本),因此这里缺乏效果是由于如何修改数据在绘图上被反映。

与3D情况进行比较:

import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D

fig = plt.figure()
ax = fig.add_subplot(111,projection='3d')

# first set data to zero
# we'll use an ndarray as input, otherwise there's no chance to get a view
x = np.arange(3)
y = np.array([0.0,0.0,0.0])
z = np.array([0.0,0.0,0.0])

# plot the flat line
pl, = ax.plot(x,y,z,'o-')

# change the axes to see the result; not actually relevant
ax.set_ylim([0,4])
ax.set_zlim([0,4])

# mutate y,z to get a nontrivial line
y[:] = [1,2,3]
z[:] = [1,2,3]


# update the canvas in an interactive plot
# plt.show() probably suffices non-interactively
fig.canvas.draw()

plt.show()

我们正在做完全相同的事情,只不过使用了一个三维坐标轴对象(还有一个维度),以下是结果:
如您所见,与二维情况完全相反,原始源数组的变化通过更新图表得到了很好的体现。
我不太确定这是如何发生的;Axes3D.plot将大部分问题外包给了Axes.plot(即2d部分),然后提取了沿第三维的所有数据。由于这两种情况下都是由Axes.plot创建线条,因此它们都不会复制其输入数据,这并不奇怪。
Axes3D.scatter让Axes.scatter完成2d工作。虽然我不理解plot案例在2d和3d之间的区别,但我发现这部分更容易理解:PathCollection(3D)对象要复杂得多,无法在不脱离原始数据数组的情况下组装。
因此,在您问题中的代码中,生成要绘制的数据的函数实际上改变(并返回)相同的数组xs、ys、zs。由于基本上为每个绘图使用相同的数组,因此您看到的结果取决于绘图调用是否对其数据源的变异敏感。对于Axes3D.plot而言,情况就是这样,因此对数据生成函数的第二次调用修改了第一次的绘图;而对于Axes3D.scatter,数据源的变异不会影响绘图,因此两个绘图都如预期地可见。
如果您想看到真正奇怪的事情,请尝试使用列表输入而不是ndarray的三维示例:
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D

fig = plt.figure()
ax = fig.add_subplot(111,projection='3d')

# first set data to zero, but lists this time
x = np.arange(3)
y = [0.0,0.0,0.0]
z = [0.0,0.0,0.0]

# plot the flat line
pl, = ax.plot(x,y,z,'o-')

# change the axes to see the result; not actually relevant
ax.set_ylim([0,4])
ax.set_zlim([0,4])

# mutate y,z to get a nontrivial line
y[:] = [1,2,3]
z[:] = [1,2,3]


# update the canvas in an interactive plot
# plt.show() probably suffices non-interactively
fig.canvas.draw()

plt.show()

在这种情况下,我希望输入列表被转换为ndarrays,因此突变不起作用,我们得到一个平坦的零线。但事实并非如此:3d case using list input 显然,y坐标不会改变,但z坐标会发生变化。现在这很奇怪!关键是图的底层数据数组:
print(pl._verts3d)
# (array([0, 1, 2]), array([ 0.,  0.,  0.]), [1, 2, 3])
print(pl._verts3d[2] is z)
# True

Axes3D.plot 调用 mplot3d.art3d.line_2d_to_3d 时将 z 坐标强行加入图形中,函数会从二维图中获取现有的 x 和 y 数组,并在其旁边添加 z 坐标。换句话说,Axes.plot 将输入列表 y 转换为数组,在此步骤中,对 y 的更改不会影响图形。另一方面,z 输入被单独处理,最终的结果是 z 在操作完成后没有发生变化。这就是为什么同时更改 yz 最终只会更改 z 的原因。
总之,我查看了 matplotlib 问题页面,并发现了与二维情况有关的相关讨论。解决方法似乎是设计上的,因为二维图通常不会复制它们的数据,否则会增加不必要的开销。我也可以看到三维情况是如何处理的,这会导致出现意料之外的行为。
无论如何,我认为改变传递给绘图方法的数据是不合理的。如果您确实有意这样做,请使用专门的方法,例如 pl.set_xdata()。然而,对于三维图而言,这是不可能的(其中 x/ydata 属性被重新解释为不同类型的坐标)。因此,我的建议是不要更改源数组,或者在需要更改数据时手动传递副本。不能禁止突变,但我也可以看到 matplotlib 开发人员不想在每种情况下复制每个输入。因此,最可能的解决方案是用户不应该突变其原始数据。有些事情告诉我,问问题代码的人没有意识到他们最初是在进行输入数组的突变,这意味着我们仍然会看到一种有效的用例,即有意突变输入数组的情况。

我在Matplotlib上开了一个问题(issue),我们将看到这是否只是一个奇怪的用例,或者他们是否可以/打算解决这个问题。 - Andras Deak -- Слава Україні
感谢 @Andras Deak 的详细解释。让我们期待 matplotlib 的修复。 - Sabeer Ebrahim

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接