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