How to do dynamic matplotlib plotting with a fixed

2020-04-21 01:46发布

问题:

I have a dataframe called benchmark_returns and strategy_returns. Both have the same timespan. I want to find a way to plot the datapoints in a nice animation style so that it shows all the points loading in gradually. I am aware that there is a matplotlib.animation.FuncAnimation(), however this typically is only used for a real-time updating of csv files etc but in my case I know all the data I want to use.

I have also tried using the crude plt.pause(0.01) method, however this drastically slows down as the number of points get plotted.

Here is my code so far

x = benchmark_returns.index
y = benchmark_returns['Crypto 30'] 
y2 = benchmark_returns['Dow Jones 30']
y3 = benchmark_returns['NASDAQ'] 
y4 = benchmark_returns['S&P 500']


fig, ax = plt.subplots()
line, = ax.plot(x, y, color='k')
line2, = ax.plot(x, y2, color = 'b')
line3, = ax.plot(x, y3, color = 'r')
line4, = ax.plot(x, y4, color = 'g')

def update(num, x, y, y2, y3, y4, line): 
    line.set_data(x[:num], y[:num])
    line2.set_data(x[:num], y2[:num])
    line3.set_data(x[:num], y3[:num])
    line4.set_data(x[:num], y4[:num])

    return line, line2, line3, line4,

ani = animation.FuncAnimation(fig, update, fargs=[x, y, y2, y3, y4, line], 
                              interval = 1, blit = True)
plt.show()

回答1:

You could try matplotlib.animation.ArtistAnimation. It operates similar to FuncAnimation in that you can specify the frame interval, looping behavior, etc, but all the plotting is done at once, before the animation step. Here is an example

import matplotlib.pyplot as plt
import pandas as pd
import numpy as np
from matplotlib.animation import ArtistAnimation

n = 150
x = np.linspace(0, np.pi*4, n)
df = pd.DataFrame({'cos(x)' : np.cos(x), 
                   'sin(x)' : np.sin(x),
                   'tan(x)' : np.tan(x),
                   'sin(cos(x))' : np.sin(np.cos(x))})

fig, axs = plt.subplots(nrows=2, ncols=2, figsize=(10,10))
lines = []
artists = [[]]
for ax, col in zip(axs.flatten(), df.columns.values):
    lines.append(ax.plot(df[col])[0])
    artists.append(lines.copy())

anim = ArtistAnimation(fig, artists, interval=500, repeat_delay=1000)

The drawback here is that each artist is either drawn or not, i.e. you can't draw only part of a Line2D object without doing clipping. If this is not compatible with your use case then you can try using FuncAnimation with blit=True and chunking the data to be plotted each time as well as using set_data() instead of clearing and redrawing on every iteration. An example of this using the same data from above:

import matplotlib.pyplot as plt
import pandas as pd
import numpy as np
from matplotlib.animation import FuncAnimation

n = 500
nf = 100
x = np.linspace(0, np.pi*4, n)
df = pd.DataFrame({'cos(x)' : np.cos(x), 
                   'sin(x)' : np.sin(x),
                   'tan(x)' : np.tan(x),
                   'sin(cos(x))' : np.sin(np.cos(x))})

fig, axs = plt.subplots(2, 2, figsize=(5,5), dpi=50)
lines = []
for ax, col in zip(axs.flatten(), df.columns):
    lines.append(ax.plot([], lw=0.5)[0])
    ax.set_xlim(x[0] - x[-1]*0.05, x[-1]*1.05)
    ax.set_ylim([min(df[col].values)*1.05, max(df[col].values)*1.05])
    ax.tick_params(labelbottom=False, bottom=False, left=False, labelleft=False)
plt.subplots_adjust(hspace=0, wspace=0, left=0.02, right=0.98, bottom=0.02, top=0.98)
plt.margins(1, 1)
c = int(n / nf)
def animate(i):
    if (i != nf - 1):
        for line, col in zip(lines, df.columns):
            line.set_data(x[:(i+1)*c], df[col].values[:(i+1)*c])
    else:
        for line, col in zip(lines, df.columns):
            line.set_data(x, df[col].values)        
    return lines

anim = FuncAnimation(fig, animate, interval=2000/nf, frames=nf, blit=True)


Edit

In response to the comments, here is the implementation of a chunking scheme using the updated code in the question:

x = benchmark_returns.index
y = benchmark_returns['Crypto 30'] 
y2 = benchmark_returns['Dow Jones 30']
y3 = benchmark_returns['NASDAQ'] 
y4 = benchmark_returns['S&P 500']

line, = ax.plot(x, y, color='k')
line2, = ax.plot(x, y2, color = 'b')
line3, = ax.plot(x, y3, color = 'r')
line4, = ax.plot(x, y4, color = 'g')

n = len(x)  # Total number of rows
c = 50      # Chunk size
def update(num):
    end = num * c if num * c < n else n - 1
    line.set_data(x[:end], y[:end])
    line2.set_data(x[:end], y2[:end])
    line3.set_data(x[:end], y3[:end])
    line4.set_data(x[:end], y4[:end])

    return line, line2, line3, line4,

ani = animation.FuncAnimation(fig, update, interval = c, blit = True)
plt.show()

or, more succinctly

cols = benchmark_returns.columns.values
# or, for only a subset of the columns
# cols = ['Crypto 30', 'Dow Jones 30', 'NASDAQ', 'S&P 500']
colors = ['k', 'b', 'r', 'g']
lines = []
for c, col in zip(cols, colors):
    lines.append(ax.plot(benchmark_returns.index, benchmark_returns[col].values, c=c)[0])

n = len(benchmark_returns.index)
c = 50  # Chunk size
def update(num):
    end = num * c if num * c < n else n - 1
    for line, col in zip(lines, cols):
        line.set_data(benchmark_returns.index, benchmark_returns[col].values[:end])

    return lines

anim = animation.FuncAnimation(fig, update, interval = c, blit=True)
plt.show()

and if you need it to stop updating after a certain time simply set the frames argument and repeat=False in FuncAnimation().



回答2:

You can just update the data into the line element like so:

fig = plt.figure()
ax = fig.add_subplot(111)
liner, = ax.plot()
plt.ion()
plt.show()
for i in range(len(benchmark_returns.values)):
    liner.set_ydata(benchmark_returns['Crypto 30'][:i])
    liner.set_xdata(benchmark_returns.index[:i])
    plt.pause(0.01)