Synchronous Animations of Surface, Scatterplot3d and Lineplot Subplots

I have two subplots: A surface plot on the left and a scatterplot on the right. My goal is to:

Left side: Animate successive points on the surface plot, via Scatterplot3d

Right Side: Animate successive regression lines on top of the scatterplot.

My solution is modeled after https://chart-studio.plotly.com/~empet/15243/animating-traces-in-subplotsbr/#/, but I have not been able to produce any of the animations, nor the left scatterplot.

Hereā€™s my code so far:

        # Initialize figure with 2 subplots
        fig = make_subplots(rows=1, cols=2, subplot_titles=("Gradient Descent", "Linear Regression"),
                            specs=[[{'type': "surface"}, {"type": "scatter"}]])      

        # Add surface and scatter plot
        fig.add_trace(
            go.Surface(x=theta0, y=theta1, z=Js, colorscale="YlGnBu", showscale=False),
            row=1, col=1)

        fig.add_trace(
            go.Scatter(x=model.X_train_[:,1], y=model.y_train_,
                       name="ames",
                       mode="markers",
                       marker=dict(color="#1560bd")), row=1, col=2)

        # Create frames definition                       
        frames = [go.Frame(
            dict(
                name = k,
                data = [
                    go.Scatter3d(x=[theta0[k]], y=[theta1[k]], z=[cost[k]], mode='markers', marker=dict(color="red", size=16)),
                    go.Scatter(x=xx, y=yy[k], mode="lines")
                ],
                traces=[0,1])
            ) for k in range(n_frames)]

        # Update the menus
        updatemenus = [dict(type='buttons',
                            buttons=[dict(label="Play",
                                          method="animate",
                                          args=[[f'{k}' for k in range(n_frames)],
                                            dict(frame=dict(duration=100, redraw=False),
                                                 transition=dict(duration=0),
                                                 easing="linear",
                                                 fromcurrent=True,
                                                 mode="immediate")])],
                            direction="left",
                            pad=dict(r=10, t=85),
                            showactive=True, x=0.1, y=0, xanchor="right", yanchor="top")]

        sliders = [{"yanchor": "top",
                   "xanchor": "left",
                   "currentvalue": {"font": {"size": 16}, "prefix": "Iteration: ", "visible":True, "xanchor": "right"},
                   'transition': {'duration': 100.0, 'easing': 'linear'},
                   'pad': {'b': 10, 't': 50}, 
                   'len': 0.9, 'x': 0.1, 'y': 0, 
                   'steps': [{'args': [[k], {'frame': {'duration': 100.0, 'easing': 'linear', 'redraw': False},
                                      'transition': {'duration': 0, 'easing': 'linear'}}], 
                       'label': k, 'method': 'animate'} for k in range(n_frames)       
                    ]}]

        fig.update(frames=frames)
        fig.update_layout(
            xaxis=dict(range=[theta0_min, theta0_max], autorange=False, zeroline=False),
            yaxis=dict(range=[theta1_min, theta1_max], autorange=False, zeroline=False),            
            title=dict(xanchor='center', yanchor='top', x=0.5, y=0.9),        
            font=dict(family="Open Sans"),    
            updatemenus=updatemenus, 
            sliders=sliders, 
            template='plotly_white')
        if directory and filename:
            filepath = os.path.join(directory, filename)
            fig.write_html(filepath, include_mathjax='cdn')
        pio.renderers.default = "browser"
        fig.show()

Iā€™d appreciate some help here, because I am a bit in the weeds on this one.

Hi @decisionscients,

Your animation doesnā€™t work because the trace 0 is a surface, while the frames that update this trace are scatter3d traces, but they should be of the same type.
To remove this inconvenience insert in the subplot (1, 1) a dummy Scatter3d representing a small size point on your surface, such that to be invisible:

fig.add_trace(
            go.Surface(x=theta0, y=theta1, z=Js, colorscale="YlGnBu", showscale=False),
            row=1, col=1)
fig.add_trace(
            go.Scatter3d(x=[theta0[0]], y=[theta1[0]], z= [Js[0][0]], 
                         mode='markers', marker_color='red', marker_size=0. 5), row=1, col=1)   
        fig.add_trace(
            go.Scatter(x=model.X_train_[:,1], y=model.y_train_,
                       name="ames",
                       mode="markers",
                       marker=dict(color="#1560bd")), row=1, col=2)

Then in the frame definition change traces=[0, 1] with traces=[1,2] because they are updating the trace of index 1, respectively 2.

With these modification each frame will display the surface (which is static) a moving 3d point on it, and a moving 2d point in the subplot (1,2).
If your animation doesnā€™t start even after these updates, please upload somewhere a few data to test your code with data.

Hi @empet

Thanks so much for jumping on this so quickly. I applied and extended the changes you recommended and was able to get the surface plot on subplot 1 and the scatterplot and regression lines on subplot 2. In addition, I changed the dot trace to a cumulative 3d line plot.

Current issues with this animation include:

  1. The cumulative line does not render
  2. Subplot 2 only appears after pressing play and double clicking subplot 2
  3. In order for the static artifacts (surface and scatterplot) to render, their traces must be added twice

The data for the plot can be downloaded from: Gradient Descent Demo Data

The code:

        fig = make_subplots(rows=1, cols=2, subplot_titles=("Gradient Descent", "Linear Regression"),
                            specs=[[{'type': "surface"}, {"type": "scatter"}]])      

        # Subplot 1, Trace 1: Gradient descent path
        fig.add_trace(
            go.Scatter3d(x=[theta0[:1]], y=[theta1[:1]], z=[cost[:1]],
                         name="Batch Gradient Descent", 
                         showlegend=False, 
                         mode='lines', line=dict(color="red")),
                         row=1, col=1)            

        # Subplot 2, Trace 2: BGD Line
        fig.add_trace(
            go.Scatter(x=xx, y=lines['BGD'][0], 
                       name="Batch Gradient Descent",
                       mode="lines", marker=dict(color="red", size=0.5)),
                       row=1, col=2)

         # Subplot 2, Trace 3: SGD Line
        fig.add_trace(
            go.Scatter(x=xx, y=lines['SGD'][0], 
                       name="Stochastic Gradient Descent",
                       mode="lines", marker=dict(color="green", size=0.5)),
                       row=1, col=2)                        
        
        # Subplot 2, Trace 4: MBGD Line
        fig.add_trace(
            go.Scatter(x=xx, y=lines['MBGD'][0], 
                       name="Minibatch Gradient Descent",
                       mode="lines", marker=dict(color="orange", size=0.5)),
                       row=1, col=2)

        # Create frames definition                       
        frames = [go.Frame(
            dict(
                name = k,
                data = [                    
                    go.Scatter3d(x=[theta0[:k+2]], y=[theta1[:k+2]], z=[cost[:k+2]], mode='lines', marker=dict(color="red", size=10)),
                    go.Scatter(x=xx, y=lines['BGD'][k], mode="lines", marker=dict(color="red")),
                    go.Scatter(x=xx, y=lines['SGD'][k], mode="lines", marker=dict(color="green")),
                    go.Scatter(x=xx, y=lines['MBGD'][k], mode="lines", marker=dict(color="orange")),
                ],
                traces=[1, 2, 3, 4])
            ) for k in range(n_frames)]

        # Update the menus
        updatemenus = [dict(type='buttons',
                            buttons=[dict(label="Play",
                                          method="animate",
                                          args=[[f'{k}' for k in range(n_frames)],
                                            dict(frame=dict(duration=100, redraw=False),
                                                 transition=dict(duration=0),
                                                 easing="linear",
                                                 fromcurrent=True,
                                                 mode="immediate")])],
                            direction="left",
                            pad=dict(r=10, t=85),
                            showactive=True, x=0.1, y=0, xanchor="right", yanchor="top")]

        sliders = [{"yanchor": "top",
                   "xanchor": "left",
                   "currentvalue": {"font": {"size": 16}, "prefix": "Iteration: ", "visible":True, "xanchor": "right"},
                   'transition': {'duration': 100.0, 'easing': 'linear'},
                   'pad': {'b': 10, 't': 50}, 
                   'len': 0.9, 'x': 0.1, 'y': 0, 
                   'steps': [{'args': [[k], {'frame': {'duration': 100.0, 'easing': 'linear', 'redraw': False},
                                      'transition': {'duration': 0, 'easing': 'linear'}}], 
                       'label': k, 'method': 'animate'} for k in range(n_frames)       
                    ]}]

        fig.update(frames=frames)
        fig.update_layout(
            xaxis=dict(range=[theta0_min, theta0_max], autorange=False, zeroline=False),
            yaxis=dict(range=[theta1_min, theta1_max], autorange=False, zeroline=False),            
            title=dict(xanchor='center', yanchor='top', x=0.5, y=0.9),        
            font=dict(family="Open Sans"),    
            updatemenus=updatemenus, 
            showlegend=True,
            sliders=sliders, 
            template='plotly_white')

        # Surface plot. Had to add twice; otherwise, the trace disappears after play.
        fig.add_trace(
            go.Surface(x=theta0, y=theta1, z=Js, colorscale="YlGnBu", 
                       showscale=False, showlegend=False),
                       row=1, col=1)                    

        fig.add_trace(
            go.Surface(x=theta0, y=theta1, z=Js, colorscale="YlGnBu", 
                       showscale=False, showlegend=False),
                       row=1, col=1)            

        # Scatterplot. Had to add twice; otherwise, the trace disappears after play.
        fig.add_trace(
            go.Scatter(x=X_train_[:,1], y=y_train_,
                       name="Ames Data",
                       mode="markers",
                       showlegend=True,
                       marker=dict(color="#1560bd")), row=1, col=2)                    
         
        fig.add_trace(
            go.Scatter(x=X_train_[:,1], y=y_train_,
                       name="Ames Data",
                       mode="markers",
                       showlegend=False,
                       marker=dict(color="#1560bd")), row=1, col=2)

        pio.renderers.default = "browser"
        fig.show()

Hi @decisionscients

Could you please paste here the code for reading your npz file?

I had two attempts to extract the data but each one stopped with an error.

Just only a few arrays can be extracted:

f= np.load('gradient_descent_demo.npz') 

xx, n_frames, theta0, theta1, cost = [ f[it] for it in f.files[:5]]

continuing in the same way with f.files[5:] or trying to extract all at once yields the following message:

 Object arrays cannot be loaded when allow_pickle=False

Hey @empet
Yeah, its in compressed npz format. The data loads into a dictionary containing each of the data objects as elements. Hereā€™s the code to write and read.

Save plotting data.

filepath = os.path.join(directory, ā€œdata/gradient_descent_demo.npzā€)
check_directory(os.path.dirname(filepath))
np.savez_compressed(filepath, xx=xx, n_frames=n_frames, theta0=theta0, theta1=theta1, cost=cost,
lines=lines, theta0_min=theta0_min,
theta1_min=theta1_min, theta0_max=theta0_max,
theta1_max=theta1_max, Js=Js, X_train_=X_train_,
y_train_=y_train_)

Test save

loaded = np.load(filepath)
assert np.array_equal(theta0, loaded[ā€˜theta0ā€™]), ā€œSavez_compressed error.ā€
assert n_frames == loaded[ā€˜n_framesā€™], ā€œSavez compressed error on scalerā€
assert np.array_equal(X_train_, loaded[ā€˜X_train_ā€™]), ā€œSavez_compressed error.ā€

Hi @decisionscients,

Yes,I know itā€™s a npz file The problem is that you pasted here an incomplete code leaving for people that want to answer your question to write code for reading your data. I did not ask for a lesson on npz, but just to paste here what is missing in your code to be able to run it and give you an answer.

Apologies @empet. Didnā€™t mean toā€¦ Were you able to get the data? Hoping you can help as Iā€™ve been at this for a while now. I appreciate your help!

Iā€™ m waiting for your code that reads data.
Otherwise I have to dig into your already pasted code to deduce the type of each object saved in the npz.

Hi @empet.

Iā€™ve attached the file and the code that I used to read it. Perhaps something got corrupted when I uploaded it to github. Not sure. Hope this works. Thanks again for your time on this.
Cheers,
John

(Attachment load_npz.py is missing)

(Attachment gradient_descent_demo.npz is missing)

@decisionscients

There is no attached file.
I tried again and succeeded to read all data, but not the lines:

 with np.load('gradient_descent_demo.npz') as d:
    xx = d['xx']
    n_frames = d['n_frames']
    theta0 =d['theta0']
    theta1 = d['theta1']
    cost= d['cost']
    #lines= d['lines']
    theta0_min= d['theta0_min']
    theta1_min=d['theta1_min']
    theta0_max= d['theta0_max']
    theta1_max=d['theta1_max']
    Js= d['Js']
    X_train_= d['X_train_']
    y_train_=d[ 'y_train_']

With lines I get the same error:

Object arrays cannot be loaded when allow_pickle=False

but if they are missing I cannot run your code :frowning:

If the reading code is ā€œsecretā€, please paste it in a private message by clicking on the envelope near my profile photo. It is in the same position like in this image:

Just tried sending you the file and the email was rejected by the plotly community servers as .npz and .py attachments are not permitted apparently. I also tried downloading the file from my github page and got the same error you did. Iā€™ll zip the files and see if that gets through.
j2

Ok, I was able to download the file from https://github.com/decisionscients/MLStudio/blob/master/MLStudio/demo/data/gradient_descent_demo.npz
As long as you click on the ā€˜downloadā€™ button (as opposed to ā€˜save link asā€™, it should work. Just tried it and it worked for me.

Also came across your demo on cumulative lines. I pasted the code in, but get ValueError: Mime type rendering requires nbformat>=4.2.0 but it is not installed
Not sure if that is related, but Iā€™ve confirmed that I have version 5+ loaded. Hope you are able to load the data.
Cheers,
j2

@decisionscients

After two hours of reading your code, trying to understand what you want to animate and what is wrong in your code, I finally succeeded to rewrite it almost entirely and now it is working.

I think this is a too ambitious project for a Plotly begginer First you had to start with simple animations, and progressively add new elements. Just looking at a model and applying the 2d rules to 3d layout and animation led to an unresponsive plot.
There are so many details I should emphasize, but Iā€™m stopping here :frowning:

import numpy as np
import plotly.graph_objs as go
from plotly.subplots import make_subplots

f = np.load('gradient_descent_demo.npz', allow_pickle=True)
lines = f['lines']  #lines is 0-d array (print(lines) revealed this odd type)
lines = lines.item()
#print(type(lines))

#print(lines.keys())

with np.load('gradient_descent_demo.npz') as d:
    xx = d['xx']
    n_frames = d['n_frames']
    theta0 =d['theta0']
    theta1 = d['theta1']
    cost= d['cost']
    theta0_min= d['theta0_min']
    theta1_min=d['theta1_min']
    theta0_max= d['theta0_max']
    theta1_max=d['theta1_max']
    Js= d['Js']
    X_train_= d['X_train_']
    y_train_=d[ 'y_train_']


cm, cM = cost.min(),cost.max()
jm, jM = Js.min(), Js.max()
zm, zM = min([cm, jm]), max([cM, jM])

fig = make_subplots(rows=1, cols=2, subplot_titles=("Gradient Descent", "Linear Regression"),
                   specs=[[{'type': "scene"}, {"type": "xy"}]] )
                             



fig.add_trace(go.Surface(x=theta0, y=theta1, z=Js, colorscale="greens_r", 
                       showscale=False, showlegend=False),
                       row=1, col=1)            

fig.add_trace(go.Scatter3d(x=theta0[:2], y=theta1[:2], z=cost[:2],
                         name="Batch Gradient Descent", 
                         showlegend=False, 
                         mode='lines', line=dict(color="red", width=8)),
                         row=1, col=1)            

fig.add_trace(
            go.Scatter(x=X_train_[:,1], y=y_train_,
                       name="Ames Data",
                       mode="markers",
                       showlegend=True,
                       marker=dict(color="#1560bd", size=4)), row=1, col=2)                    
         
        # Subplot 2, Trace 2: BGD Line
fig.add_trace(
            go.Scatter(x=xx, y=lines['BGD'][0], 
                       name="Batch Gradient Descent",
                       mode="lines", line=dict(color="red", width=1)),
                       row=1, col=2)

         # Subplot 2, Trace 3: SGD Line
fig.add_trace(
            go.Scatter(x=xx, y=lines['SGD'][0], 
                       name="Stochastic Gradient Descent",
                       mode="lines", line=dict(color="green", width=1)),
                       row=1, col=2)                        
        
        # Subplot 2, Trace 4: MBGD Line
fig.add_trace(
            go.Scatter(x=xx, y=lines['MBGD'][0], 
                       name="Minibatch Gradient Descent",
                       mode="lines", line=dict(color="orange", width=1)),
                       row=1, col=2)
fig.update_layout(width=900, height=550,
            scene_xaxis=dict(range=[theta0_min, theta0_max], autorange=False),
            scene_yaxis=dict(range=[theta1_min, theta1_max], autorange=False),    
            scene_zaxis=dict(range=[zm, zM], autorange=False),  
            title=dict(xanchor='center', yanchor='top', x=0.5, y=0.9),        
            font=dict(family="Open Sans"),    
            showlegend=True,
            template='plotly_white');

#fig.show()




frames = [go.Frame(
            dict(
                name = f'{k+1}',
                data = [                    
                    go.Scatter3d(x=theta0[:k+2], y=theta1[:k+2], z=cost[:k+2]),
                    go.Scatter(x=xx, y=lines['BGD'][k]),
                    go.Scatter(x=xx, y=lines['SGD'][k]),
                    go.Scatter(x=xx, y=lines['MBGD'][k]),
                ],
                traces=[1 , 3, 4, 5])
            ) for k in range(n_frames-2)]



        # Update the menus
updatemenus = [dict(type='buttons',
                            buttons=[dict(label="Play",
                                          method="animate",
                                          args=[[f'{k+1}' for k in range(n_frames-2)],
                                            dict(frame=dict(duration=10, redraw=True),
                                                 transition=dict(duration=0),
                                                 easing="linear",
                                                 fromcurrent=True,
                                                 mode="immediate")])],
                            direction="left",
                            pad=dict(r=10, t=85),
                            showactive=True, x=0.1, y=0, xanchor="right", yanchor="top")]

sliders = [{"yanchor": "top",
                   "xanchor": "left",
                   "currentvalue": {"font": {"size": 16}, "prefix": "Iteration: ", "visible":True, "xanchor": "right"},
                   'transition': {'duration': 0, 'easing': 'linear'},
                   'pad': {'b': 10, 't': 50}, 
                   'len': 0.9, 'x': 0.1, 'y': 0, 
                   'steps': [{'args': [[f'{k+1}'], {'frame': {'duration': 10, 'easing': 'linear', 'redraw': False},
                                      'transition': {'duration': 0, 'easing': 'linear'}}], 
                       'label': k, 'method': 'animate'} for k in range(n_frames-2)       
                    ]}]

fig.update(frames=frames)
fig.update_layout(
            updatemenus=updatemenus, 
            sliders=sliders
            )


fig.show('browser')