如何动画一个散点情节

我正在尝试做一个散点图的动画,其中颜色和大小的点在动画的不同阶段发生变化。对于数据,我有两个 ndarray,x 值和 y 值:

data.shape = (ntime, npoint)
x.shape = (npoint)
y.shape = (npoint)

现在我要绘制一个类型的散点图

pylab.scatter(x,y,c=data[i,:])

并创建一个动画在索引 i。我如何做到这一点?

124491 次浏览

Suppose you have a scatter plot, scat = ax.scatter(...), then you can

  • change the positions

          scat.set_offsets(array)
    

where array is a N x 2 shaped array of x and y coordinates.

  • change the sizes

          scat.set_sizes(array)
    

where array is a 1D array of sizes in points.

  • change the color

          scat.set_array(array)
    

where array is a 1D array of values which will be colormapped.

Here's a quick example using the animation module.
It's slightly more complex than it has to be, but this should give you a framework to do fancier things.

(Code edited April 2019 to be compatible with current versions. For the older code see revision history)

import matplotlib.pyplot as plt
import matplotlib.animation as animation
import numpy as np


class AnimatedScatter(object):
"""An animated scatter plot using matplotlib.animations.FuncAnimation."""
def __init__(self, numpoints=50):
self.numpoints = numpoints
self.stream = self.data_stream()


# Setup the figure and axes...
self.fig, self.ax = plt.subplots()
# Then setup FuncAnimation.
self.ani = animation.FuncAnimation(self.fig, self.update, interval=5,
init_func=self.setup_plot, blit=True)


def setup_plot(self):
"""Initial drawing of the scatter plot."""
x, y, s, c = next(self.stream).T
self.scat = self.ax.scatter(x, y, c=c, s=s, vmin=0, vmax=1,
cmap="jet", edgecolor="k")
self.ax.axis([-10, 10, -10, 10])
# For FuncAnimation's sake, we need to return the artist we'll be using
# Note that it expects a sequence of artists, thus the trailing comma.
return self.scat,


def data_stream(self):
"""Generate a random walk (brownian motion). Data is scaled to produce
a soft "flickering" effect."""
xy = (np.random.random((self.numpoints, 2))-0.5)*10
s, c = np.random.random((self.numpoints, 2)).T
while True:
xy += 0.03 * (np.random.random((self.numpoints, 2)) - 0.5)
s += 0.05 * (np.random.random(self.numpoints) - 0.5)
c += 0.02 * (np.random.random(self.numpoints) - 0.5)
yield np.c_[xy[:,0], xy[:,1], s, c]


def update(self, i):
"""Update the scatter plot."""
data = next(self.stream)


# Set x and y data...
self.scat.set_offsets(data[:, :2])
# Set sizes...
self.scat.set_sizes(300 * abs(data[:, 2])**1.5 + 100)
# Set colors..
self.scat.set_array(data[:, 3])


# We need to return the updated artist for FuncAnimation to draw..
# Note that it expects a sequence of artists, thus the trailing comma.
return self.scat,




if __name__ == '__main__':
a = AnimatedScatter()
plt.show()

enter image description here

If you're on OSX and using the OSX backend, you'll need to change blit=True to blit=False in the FuncAnimation initialization below. The OSX backend doesn't fully support blitting. The performance will suffer, but the example should run correctly on OSX with blitting disabled.


For a simpler example, which just updates the colors, have a look at the following:

import matplotlib.pyplot as plt
import numpy as np
import matplotlib.animation as animation


def main():
numframes = 100
numpoints = 10
color_data = np.random.random((numframes, numpoints))
x, y, c = np.random.random((3, numpoints))


fig = plt.figure()
scat = plt.scatter(x, y, c=c, s=100)


ani = animation.FuncAnimation(fig, update_plot, frames=range(numframes),
fargs=(color_data, scat))
plt.show()


def update_plot(i, data, scat):
scat.set_array(data[i])
return scat,


main()

Here is the thing. I used to a user of Qt and Matlab and I am not quite familiar with the animation system on the matplotlib.

But I do have find a way that can make any kind of animation you want just like it is in matlab. It is really powerful. No need to check the module references and you are good to plot anything you want. So I hope it can help.

The basic idea is to use the time event inside PyQt( I am sure other Gui system on the Python like wxPython and TraitUi has the same inner mechanism to make an event response. But I just don't know how). Every time a PyQt's Timer event is called I refresh the whole canvas and redraw the whole picture, I know the speed and performance may be slowly influenced but it is not that much.

Here is a little example of it:

import sys
from PyQt4 import QtGui


from matplotlib.figure import Figure
from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas


import numpy as np




class Monitor(FigureCanvas):
def __init__(self):
self.fig = Figure()
self.ax = self.fig.add_subplot(111)


FigureCanvas.__init__(self, self.fig)
self.x = np.linspace(0,5*np.pi,400)
self.p = 0.0
self.y = np.sin(self.x+self.p)




self.line = self.ax.scatter(self.x,self.y)


self.fig.canvas.draw()


self.timer = self.startTimer(100)




def timerEvent(self, evt):
# update the height of the bars, one liner is easier
self.p += 0.1
self.y = np.sin(self.x+self.p)
self.ax.cla()
self.line = self.ax.scatter(self.x,self.y)


self.fig.canvas.draw()






if __name__ == "__main__":
app = QtGui.QApplication(sys.argv)
w = Monitor()
w.setWindowTitle("Convergence")
w.show()
sys.exit(app.exec_())

You can adjust the refresh speed in the

        self.timer = self.startTimer(100)

I am just like you who want to use the Animated scatter plot to make a sorting animation. But I just cannot find a so called "set" function. So I refreshed the whole canva.

Hope it helps..

I wrote celluloid to make this easier. It's probably easiest to show by example:

import matplotlib.pyplot as plt
from matplotlib import cm
import numpy as np
from celluloid import Camera


numpoints = 10
points = np.random.random((2, numpoints))
colors = cm.rainbow(np.linspace(0, 1, numpoints))
camera = Camera(plt.figure())
for _ in range(100):
points += 0.1 * (np.random.random((2, numpoints)) - .5)
plt.scatter(*points, c=colors, s=100)
camera.snap()
anim = camera.animate(blit=True)
anim.save('scatter.mp4')

enter image description here

It uses ArtistAnimation under the hood. camera.snap captures the current state of the figure which is used to create the frames in the animation.

Edit: To quantify how much memory this uses I ran it through memory_profiler.

Line #    Mem usage    Increment   Line Contents
================================================
11     65.2 MiB     65.2 MiB   @profile
12                             def main():
13     65.2 MiB      0.0 MiB       numpoints = 10
14     65.2 MiB      0.0 MiB       points = np.random.random((2, numpoints))
15     65.2 MiB      0.1 MiB       colors = cm.rainbow(np.linspace(0, 1, numpoints))
16     65.9 MiB      0.6 MiB       fig = plt.figure()
17     65.9 MiB      0.0 MiB       camera = Camera(fig)
18     67.8 MiB      0.0 MiB       for _ in range(100):
19     67.8 MiB      0.0 MiB           points += 0.1 * (np.random.random((2, numpoints)) - .5)
20     67.8 MiB      1.9 MiB           plt.scatter(*points, c=colors, s=100)
21     67.8 MiB      0.0 MiB           camera.snap()
22     70.1 MiB      2.3 MiB       anim = camera.animate(blit=True)
23     72.1 MiB      1.9 MiB       anim.save('scatter.mp4')

To summarize this:

  • Creating 100 plots used 1.9 MiB.
  • Making the animation used 2.3 MiB.
  • This method of making animations used 4.2 MiB of memory in sum.

Why Not try this

import numpy as np
import matplotlib.pyplot as plt


x=np.random.random()
y=np.random.random()


fig, ax = plt.subplots()
ax.scatter(x,y,color='teal')
ax.scatter(y,x,color='crimson')
ax.set_xlim([0,1])
ax.set_ylim([0,1])


for i in np.arange(50):
x=np.random.random()
y=np.random.random()
bha=ax.scatter(x,y)
plt.draw()
plt.pause(0.5)
bha.remove()


plt.show()

TL/DR: If you are having trouble with the ax.set_... methods for animating your scatter plot, you can try to just clear the plot each frame (i.e., ax.clear()) and re-plot things as desired. This is slower, but might be useful when you want to change a lot of things in a small animation.


Here is an example demonstrating this "clear" approach:

import itertools


import matplotlib.pyplot as plt
import matplotlib.animation as animation
import numpy as np


# set parameters
frames = 10
points = 20
np.random.seed(42)


# create data
data = np.random.rand(points, 2)


# set how the graph will change each frame
sizes = itertools.cycle([10, 50, 150])
colors = np.random.rand(frames, points)
colormaps = itertools.cycle(['Purples', 'Blues', 'Greens', 'Oranges', 'Reds'])
markers = itertools.cycle(['o', 'v', '^', 's', 'p'])


# init the figure
fig, ax = plt.subplots(figsize=(5,5))


def update(i):
# clear the axis each frame
ax.clear()


# replot things
ax.scatter(data[:, 0], data[:, 1],
s=next(sizes),
c=colors[i, :],
cmap=next(colormaps),
marker=next(markers))


# reformat things
ax.set_xlabel('world')
ax.set_ylabel('hello')


ani = animation.FuncAnimation(fig, update, frames=frames, interval=500)
ani.save('scatter.gif', writer='pillow')

enter image description here

The tutorials I have seen from matplotlib and other sources do not seem to use this approach, but I have seen others (as well as myself) suggest it on this site. I see some pros & cons, but I would appreciate anyone else's thoughts:

Pros

  • You can avoid using the set_... methods for the scatter plot (i.e. .set_offsets(), .set_sizes(), ...), which have more obscure and less-detailed documentation (though the leading answer will get you very far here!). Plus, there are different methods for different plot types (e.g. you use set_data for lines, but not for scatter points). By clearing the axis, you determine the plotted elements each frame like any other plot in matplotlib.
  • Even more so, it is unclear if some properties are set-able, such as the marker type (as commented) or the colormap. I wouldn't know how to create the above plot using ax.set_..., for example, because of the marker and colormap changes. But this is pretty basic with ax.scatter().

Cons

  • It can be much slower; i.e. clearing and redrawing everything appears to be more expensive than the set... methods. So for large animations, this approach can be kind of painful.
  • Clearing the axis also clears things like the axis labels, axis limits, other text, etc. So, those sorts of formatting things need to be included in update (else they will be gone). This can be annoying if you want some things to change, but others to stay the same.

Of course, the speed is a big con. Here is an example showing the difference. Given this data:

import matplotlib.pyplot as plt
import matplotlib.animation as animation
import numpy as np


np.random.seed(42)
frames = 40


x = np.arange(frames)
y = np.sin(x)
colors = itertools.cycle(['red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet'])
data = [(np.random.uniform(-1, 1, 10) + x[i],
np.random.uniform(-1, 1, 10) + y[i])
for i in range(frames)]

You can plot using the set... method:

fig, ax = plt.subplots()


s = ax.scatter([], [])


ax.set_xlim(-2, frames+2)
ax.set_ylim(min(y) - 1, max(y) + 1)


def update(i):
s.set_offsets(np.column_stack([data[i][0], data[i][1]]))
s.set_facecolor(next(colors))


ani = animation.FuncAnimation(fig, update, frames=frames, interval=100)
ani.save('set.gif', writer='pillow')

Or the "clear" method:

fig, ax = plt.subplots()


def update(i):
ax.clear()
ax.scatter(data[i][0], data[i][1], c=next(colors))
ax.set_xlim(-2, frames+2)
ax.set_ylim(min(y) - 1, max(y) + 1)


ani = animation.FuncAnimation(fig, update, frames=frames, interval=100)
ani.save('clear.gif', writer='pillow')

To get this figure:

enter image description here

Using %%time, we can see that clearing and replotting takes (more than) twice as long:

  • for set...: Wall time: 1.33 s
  • for clear: Wall time: 2.73 s

Play with the frames parameter to test this at different scales. For smaller animations (less frames/data), the time difference between the two methods is inconsequential (and for me, sometimes causes me to prefer the clearing method). But for larger cases, using set_... can save significant time.