How to do dynamic matplotlib plotting with pandas? [duplicate] - python

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()

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().

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)

Related

Wrong matplotlib animation

I have the following code that should draw a cycloid with animation and save it to a gif
but after running the program, a white square appears that covers everything, I can't find the reason cycloid_animation
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
from matplotlib.animation import FuncAnimation, PillowWriter
plt.rcParams['animation.html'] = 'html5'
R = 1
def circle(a, b, r):
# (a,b): the center of the circle
# r: the radius of the circle
# T: The number of the segments
T = 100
x, y = [0]*T, [0]*T
for i,theta in enumerate(np.linspace(0,2*np.pi,T)):
x[i] = a + r*np.cos(theta)
y[i] = b + r*np.sin(theta)
return x, y
# Calculate the cycloid line
thetas = np.linspace(0,4*np.pi,100)
cycloid_x = R*(thetas-np.sin(thetas))
cycloid_y = R*(1-np.cos(thetas))
cycloid_c = R*thetas
fig = plt.figure()
lns = []
trans = plt.axes().transAxes
for i in range(len(thetas)):
x,y = circle(cycloid_c[i], R, R)
ln1, = plt.plot(x, y, 'g-', lw=2)
ln2, = plt.plot(cycloid_x[:i+1] ,cycloid_y[:i+1], 'r-', lw=2)
ln3, = plt.plot(cycloid_x[i], cycloid_y[i], 'bo', markersize=4)
ln4, = plt.plot([cycloid_c[i], cycloid_x[i]], [R,cycloid_y[i]], 'y-', lw=2)
tx1 = plt.text(0.05, 0.8, r'$\theta$ = %.2f $\pi$' % (thetas[i]/np.pi), transform=trans)
lns.append([ln1,ln2,ln3,ln4,tx1])
plt.xlim(0,15)
plt.ylim(0,3)
plt.xlabel('x')
plt.ylabel('y')
plt.grid()
plt.axes().set_aspect('equal')
ani = animation.ArtistAnimation(fig, lns, interval=50)
#ani.save('cycloid_ArtistAnimation.mp4',writer='ffmpeg')
ani.save('cycloid_ArtistAnimation.gif',writer='pillow')
ani
Each time you call plt.axis() you are creating a new axis on top of the figure. Since what you want is to get the current axis and then apply the transformations, after creating the figure you should call plt.gca() to get the current axis and use that instead.
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
from matplotlib.animation import FuncAnimation, PillowWriter
plt.rcParams['animation.html'] = 'html5'
R = 1
def circle(a, b, r):
# (a,b): the center of the circle
# r: the radius of the circle
# T: The number of the segments
T = 100
x, y = [0]*T, [0]*T
for i,theta in enumerate(np.linspace(0,2*np.pi,T)):
x[i] = a + r*np.cos(theta)
y[i] = b + r*np.sin(theta)
return x, y
# Calculate the cycloid line
thetas = np.linspace(0,4*np.pi,100)
cycloid_x = R*(thetas-np.sin(thetas))
cycloid_y = R*(1-np.cos(thetas))
cycloid_c = R*thetas
fig = plt.figure()
lns = []
trans = plt.gca().transAxes #<=== HERE
for i in range(len(thetas)):
x,y = circle(cycloid_c[i], R, R)
ln1, = plt.plot(x, y, 'g-', lw=2)
ln2, = plt.plot(cycloid_x[:i+1] ,cycloid_y[:i+1], 'r-', lw=2)
ln3, = plt.plot(cycloid_x[i], cycloid_y[i], 'bo', markersize=4)
ln4, = plt.plot([cycloid_c[i], cycloid_x[i]], [R,cycloid_y[i]], 'y-', lw=2)
tx1 = plt.text(0.05, 0.8, r'$\theta$ = %.2f $\pi$' % (thetas[i]/np.pi), transform=trans)
lns.append([ln1,ln2,ln3,ln4,tx1])
plt.xlim(0,15)
plt.ylim(0,3)
plt.xlabel('x')
plt.ylabel('y')
plt.grid()
plt.gca().set_aspect('equal') #<=== And HERE
ani = animation.ArtistAnimation(fig, lns, interval=50)
#ani.save('cycloid_ArtistAnimation.mp4',writer='ffmpeg')
ani.save('cycloid_ArtistAnimation.gif',writer='pillow')

How to create a delay between mutiple animations on the same graph (matplotlib, python)

This is a reference from a previous question
two lines matplotib animation
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
x = np.arange(130, 190, 1)
y = 97.928 * np.exp(- np.exp(- 0.1416 *( x - 146.1 )))
z = 96.9684 * np.exp(- np.exp(-0.1530*( x - 144.4)))
fig, ax = plt.subplots()
line1, = ax.plot(x, y, color = "r")
line2, = ax.plot(x, z, color = "g")
def update(num, x, y, z, line1, line2):
line1.set_data(x[:num], y[:num])
line2.set_data(x[:num], z[:num])
return [line1,line2]
ani = animation.FuncAnimation(fig, update, len(x), fargs=[x, y, z, line1, line2],
interval=295, blit=True)
ax.set_xlabel('Age (day)')
ax.set_ylabel('EO (%)')
plt.show()
I want to plot the graph such that, it first animates the green line, then the orange line.
Currently it animates both the line together.
https://i.stack.imgur.com/ZDlXu.gif
You could make the number of steps twice as long, first draw the first curve and then the other one.
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
x = np.arange(130, 190, 1)
y = 97.928 * np.exp(- np.exp(- 0.1416 * (x - 146.1)))
z = 96.9684 * np.exp(- np.exp(-0.1530 * (x - 144.4)))
fig, ax = plt.subplots()
line1, = ax.plot(x, y, color="r")
line2, = ax.plot(x, z, color="g")
def update(num, x, y, z, line1, line2):
if num < len(x):
line1.set_data(x[:num], y[:num])
line2.set_data([], [])
else:
line2.set_data(x[:num - len(x)], z[:num - len(x)])
return [line1, line2]
ani = animation.FuncAnimation(fig, update, 2 * len(x), fargs=[x, y, z, line1, line2],
interval=295, blit=True)
ax.set_xlabel('Age (day)')
ax.set_ylabel('EO (%)')
plt.show()

How can I speed up the generation of an MP4 using matplotlib's Animation Writer?

I am using matplotlib to generate a graphical animation of some data. The data has about 4 hours of collection time so I expect the animation to be about 4 hours. However, generating a smaller 60 second video takes approximately 15 minutes. Thus, the total estimated run time for generating the 4 hour video is 2.5 days. I assume I am doing something incredibly inefficient. How can I speed up the creation of an animation with matplotlib?
create_graph.py
import matplotlib.pyplot as plt
import matplotlib.animation as animation
import matplotlib
import pandas as pd
import numpy as np
matplotlib.use("Agg")
frame = pd.read_csv("tmp/total.csv")
min_time = frame.iloc[0]["time"]
max_time = frame.iloc[-1]["time"]
total_time = max_time - min_time
hertz_rate = 50
window_length = 5
save_count = hertz_rate * 100
def data_gen():
current_index_of_matching_ts = 0
t = data_gen.t
cnt = 0
while cnt < save_count:
print("Done: {}%".format(cnt/save_count*100.0))
predicted = cnt * (1.0/hertz_rate)
while frame.iloc[current_index_of_matching_ts]["time"] - min_time <= predicted and current_index_of_matching_ts < len(frame) - 1:
current_index_of_matching_ts = current_index_of_matching_ts + 1
y1 = frame.iloc[current_index_of_matching_ts]["var1"]
y2 = frame.iloc[current_index_of_matching_ts]["var2"]
y3 = frame.iloc[current_index_of_matching_ts]["var3"]
y4 = frame.iloc[current_index_of_matching_ts]["var4"]
y5 = frame.iloc[current_index_of_matching_ts]["var5"]
y6 = frame.iloc[current_index_of_matching_ts]["var6"]
y7 = frame.iloc[current_index_of_matching_ts]["var7"]
y8 = frame.iloc[current_index_of_matching_ts]["var8"]
y9 = frame.iloc[current_index_of_matching_ts]["var9"]
t = frame.iloc[current_index_of_matching_ts]["time"] - min_time
# adapted the data generator to yield both sin and cos
yield t, y1, y2, y3, y4, y5, y6, y7, y8, y9
cnt+=1
data_gen.t = 0
# create a figure with two subplots
fig, (ax1, ax2, ax3, ax4, ax5, ax6, ax7, ax8, ax9) = plt.subplots(9,1,figsize=(7,14)) # produces a video of 700 × 1400
# intialize two line objects (one in each axes)
line1, = ax1.plot([], [], lw=2, color='b')
line2, = ax2.plot([], [], lw=2, color='b')
line3, = ax3.plot([], [], lw=2, color='b')
line4, = ax4.plot([], [], lw=2, color='g')
line5, = ax5.plot([], [], lw=2, color='g')
line6, = ax6.plot([], [], lw=2, color='g')
line7, = ax7.plot([], [], lw=2, color='r')
line8, = ax8.plot([], [], lw=2, color='r')
line9, = ax9.plot([], [], lw=2, color='r')
line = [line1, line2, line3, line4, line5, line6, line7, line8, line9]
# the same axes initalizations as before (just now we do it for both of them)
for ax in [ax1, ax2, ax3, ax4, ax5, ax6, ax7, ax8, ax9]:
ax.set_ylim(-1.1, 1.1)
ax.grid()
# initialize the data arrays
xdata, y1data, y2data, y3data, y4data, y5data, y6data, y7data, y8data, y9data = [], [], [], [], [], [], [], [], [], []
my_gen = data_gen()
for index in range(hertz_rate*window_length-1):
t, y1, y2, y3, y4, y5, y6, y7, y8, y9 = my_gen.__next__()
xdata.append(t)
y1data.append(y1)
y2data.append(y2)
y3data.append(y3)
y4data.append(y4)
y5data.append(y5)
y6data.append(y6)
y7data.append(y7)
y8data.append(y8)
y9data.append(y9)
def run(data):
# update the data
t, y1, y2, y3, y4, y5, y6, y7, y8, y9 = data
xdata.append(t)
y1data.append(y1)
y2data.append(y2)
y3data.append(y3)
y4data.append(y4)
y5data.append(y5)
y6data.append(y6)
y7data.append(y7)
y8data.append(y8)
y9data.append(y9)
# axis limits checking. Same as before, just for both axes
for ax in [ax1, ax2, ax3, ax4, ax5, ax6, ax7, ax8, ax9]:
ax.set_xlim(xdata[-1]-5.0, xdata[-1])
# update the data of both line objects
line[0].set_data(xdata, y1data)
line[1].set_data(xdata, y2data)
line[2].set_data(xdata, y3data)
line[3].set_data(xdata, y4data)
line[4].set_data(xdata, y5data)
line[5].set_data(xdata, y6data)
line[6].set_data(xdata, y7data)
line[7].set_data(xdata, y8data)
line[8].set_data(xdata, y9data)
return line
ani = animation.FuncAnimation(fig, run, my_gen, blit=True, interval=20, repeat=False, save_count=save_count)
Writer = animation.writers['ffmpeg']
writer = Writer(fps=hertz_rate, metadata=dict(artist='Me'), bitrate=1800)
ani.save('lines.mp4', writer=writer)
So I am answering my own question here so if you find this enjoy!
Here are some facts
matplotlib creates high quality graphs
matplotlib generates graphs slowly relative to some other libraries like PyQWT (c++ bindings are used for speed)
Generating a real time graph of 4 hours of data would take about 20 hours on my mac.
To solve my issue, I created separate files and later joined them together. I used the multiprocessing library.
generate_graphs.py
import multiprocessing as mp
from multiprocessing import Pool
from make_video_graph_mp4 import write_chart_to_file_wrapper
total_parts = 6
if __name__ == '__main__':
#spawn is critical to not share plt across threads.
mp.set_start_method('spawn')
with Pool() as p:
print(p.map(write_chart_to_file_wrapper, [[i, total_parts] for i in range(total_parts)]))
make_video_graph_mp4.py
def write_chart_to_file(my_part, parts):
# ... code to create part my_part/parts of the video.
Writer = animation.writers['ffmpeg']
writer = Writer(fps=hertz_rate, metadata=dict(artist='Me'), bitrate=1800)
filename = 'out/videos/{}-lines{}-{}.mp4'.format(band_name, start_index, end_index)
ani.save(filename, writer=writer, dpi=100)

How to check if click is on scatter plot point with multiple markers (matplotlib)

I have a scatter plot with multiple markers. How would I change the code so that it can still check if I click on a point on the graph or not. What would the variable line be?
line = ax.scatter(x[:10],y[:10],20, c=color_tag[:10], picker=True, marker='*')
# how would I change the code, if I would like to add this line?
line = ax.scatter(x[10:20],y[10:20],20, c=color_tag[10:20], picker=True, marker='^')
img_annotations = [...] #array of AnnotationBoxObjects
def show_ROI(event):
if line.contains(event)[0]:
ind = line.contains(event)[1]["ind"]
print('onpick3 scatter:', ind, np.take(d['x'], ind), np.take(d['y'], ind)
ab = img_annotations[ind[0]]
ab.set_visible(True)
else:
for ab in img_annotations:
ab.set_visible(False)
fig.canvas.draw_idle()
fig.canvas.mpl_connect('button_press_event', show_ROI)
plt.show()
Of course the two scatter plots have to be stored in different variables. You may then also divide your annotations into two parts, those that belong to the first scatter, and those for the second. You would then loop over them and check if the event occurs in any of them.
import matplotlib.pyplot as plt
import numpy as np
x = np.arange(20)
y = np.sin(x)
fig, ax = plt.subplots()
line1 = ax.scatter(x[:10],y[:10],20, c="red", picker=True, marker='*')
line2 = ax.scatter(x[10:20],y[10:20],20, c="green", picker=True, marker='^')
ia = lambda i: plt.annotate("Annotate {}".format(i), (x[i],y[i]), visible=False)
img_annotations = [ia(i) for i in range(len(x))]
lce = [False]
def show_ROI(event):
tlce=False
for annot, line in zip([img_annotations[:10],img_annotations[10:20]], [line1, line2]):
if line.contains(event)[0]:
lce[0]=tlce=True
ind = line.contains(event)[1]["ind"]
print('onpick3 scatter:', ind)
ab = annot[ind[0]]
ab.set_visible(True)
if not tlce:
for ab in img_annotations:
ab.set_visible(False)
lce[0] = False
fig.canvas.draw_idle()
fig.canvas.mpl_connect('button_press_event', show_ROI)
plt.show()

Plotting line with marker as head

I have the following code that produces an animation of drawing a circle.
from math import cos, sin
import matplotlib.pyplot as plt
import matplotlib.animation as animation
def update_plot(num, x, y, line):
line.set_data(x[:num], y[:num])
line.axes.axis([-1.5, 1.5, -1.5, 1.5])
return line
def plot_circle():
x = []
y = []
for i in range(100):
x.append(cos(i/10.0))
y.append(sin(i/10.0))
fig, ax = plt.subplots()
line, = ax.plot(x, y, color = "k")
ani = animation.FuncAnimation(fig, update_plot, len(x), fargs=[x, y, line], interval = 1, blit = False)
plt.show()
plot_circle()
The line is longer than a full lap, and so to be able to still see the drawing when the line overlaps, I would like a marker that shows what is being drawn. I tried to add a scatter plot into the update call, like
scat = plt.scatter(0, 0)
ani = animation.FuncAnimation(fig, update_plot, len(x), fargs=[x, y, line, scat], interval = 1, blit = False)
and try to update the position of the scatter-plot point using x[num] and y[num] in update_plot without success. How can I achieve this effect?
You need to return scat in update_plot().
Here is another method, draw the line with markevery argument:
line, = ax.plot(x, y, "-o", color="k", markevery=100000)
reverse the points order:
line.set_data(x[:num][::-1], y[:num][::-1])
for example:
import numpy as np
import pylab as pl
t = np.linspace(0, 2, 100)
x = np.cos(t)
y = np.sin(t)
pl.plot(x[::-1], y[::-1], "-o", markevery=10000)
outputs:
I ended up finding a way to add the scatter plot to the same animation. The key was to use scat.set_offsets to set the data. The changes that are needed is as follows:
def update_plot(num, x, y, line, scat):
# ...
scat.set_offsets([x[num - 1], y[num - 1]])
return line, scat
def plot_circle():
# ...
scat = ax.scatter([0], [0], color = 'k') # Set the dot at some arbitrary position initially
ani = animation.FuncAnimation(fig, update_plot, len(x), fargs=[x, y, line, scat], interval = 1, blit = False)
plt.show()

Categories

Resources