
Fun tutorial to learn how to make professional contour plots in Python, with incredible animated visualizations. At the intersection of machine learning, scientific computing, automated art, cartography, and video games. Section 3 is particularly interesting, as it shows all the work behind the scene, to complete this project in 20 hours when you have no idea how to start.
While both surface plots and 2D contour plots are very popular and easy to make, it is a lot more difficult, mathematically speaking, to produce 3D contour plots, also called contour maps. Surface plots are helpful in multivariate regression problems. Unlike surface plots, contour plots come with horizontal curves called contour levels. They represent confidence regions of various levels — a generalization of confidence intervals — when the underlying function is a probability distribution

In practice, contour plots are easy to generate using standard libraries. Here I compare Matplotlib with Plotly, in Python. Since Plotly is a generic library used in different programming languages, the code discussed here can easily be adapted to R or Julia.

1. Motivation Behind This Project
Not long ago, I produced a contour plot using Mathematica, see Figure 3. The source code is in section 11.3.6 in my new book, here. I was wondering if I could do it in Python, hoping to turn it into a video. I started from scratch, not knowing which function or library to use, much less the parameters and options to choose from. After over 20 hours of intense research with trials and errors, I came up with a result beyond my expectations. Part of this tutorial, discussed in section 3, is to teach you how to learn and solve problems on your own. I explain in details how I eventually did it so fast despite having no training on this particular topic. In my opinion, this has much more value than the actual solution with full source code, that I provide in section 2.
I encourage you to replicate my though process described in section 3, and do it yourself, rather than looking at my solution. You may come up with a better solution, or at least one more suitable to your own needs. Learning how to learn is an important component of my upcoming course on intuitive machine learning: it is my hope that after attending my classes (available here), you will never again need to attend other ones. My goal is to feed you by teaching you how to fish, not by giving you a fish (though I also give you some great fish in the process).

That said, there is far more than just creating 3D contour plots in this article. First, you will learn how to produce data videos. I have shared quite a few in the past (with source code), but this is probably the simplest example. The data video also illustrates that a mixture of Gaussian-like distributions is typically non Gaussian-like, and may or may not be unimodal. It is borderline art (automatically generated), and certainly a stepping stone for professionals interested in computer vision or designing video games. It is easy to image a game based on my video, entitled “flying above menacingly rising mountains”. More on data videos can be found here. More of my abstract “mathematical art” can be found on my YouTube channel, here.
Then the data video, through various rotations, give you a much better view of your data. It is also perfect to show systems that evolve over time: a time series where each observation is an image. In addition, unlike most tutorials found online, this one does a rather deep dive on a specific, rather advanced function from a library truly aimed at scientific computing. You will learn something new here, and by no means elementary. You can watch the video below: the last frame corresponds to the same distribution mixture featured in figure 3.
In particular, consider the data set used to produce the video. It consists of 300 images (video frames), representing some mixture of distributions evolving over time. It is based on synthetic data: there are about 20 parameters that drive the evolution and govern the behavior of these mountains, including a rising volcano. One of the parameters governs the decaying rate of growth of the volcano. In short, the whole video can be summarized by 20 numbers, called features in machine learning. The model used to produce the data is called a generative model.
And in the same way that images (say pictures of hand-written digits) can be summarized by 10 parameters to perform text recognition, here 20 parameters allow you to perform topography classification. Not just of static terrain, but terrain that changes over time, assuming you have access to 50,000 videos representing different topographies. You can produce the videos needed for supervised classification with the code in section 2. The next step is to use data (videos) from the real world, and leverage the model trained on synthetic data for classification.
More about synthetic data can be found here and in my new book, here. I will publish a related article on terrain generation in the next few weeks. To not miss these articles and access members-only content, subscribe to our monthly newsletter.
2. Python Source Code to Produce the Video
The source code is also available on my GitHub repository, here. Look for filenames starting with contour
. It also includes the video and an animated gif version. See also my YouTube channel, here. I was unable to fix the issue visible in Figure 2 in the Matplotlib version, despite my experience with color opacity and transparency. This glitch results in a nice optical illusion: if you watch Figure 2 long enough, it switches back and forth between the mountains perceived as seen from above, and seen from underground (revealing the inside of the mountains).
This issue is addressed in the Plotly version. This library is more comprehensive and less easy to master, but offers more possibilities. The code below corresponds to the Plotly version, including the production of the video. The choice of the colors is determined by the parameter colorscale
, set to “Peach” here. The list of available palettes is posted here.
import numpy as np
import plotly.graph_objects as go
import matplotlib
def create_3Dplot(frame):
param1 = -0.15 + 0.65*(1-np.exp(-3*frame/Nframes)) # height of volcano
param2 = -1.00 + 2.00*frame/(Nframes-1) # rotation, x
param3 = 0.75 + 1.00*(1-frame/(Nframes-1)) # rotation, y
param4 = 1.00 - 0.70*frame/(Nframes-1) # rotation z
X, Y = np.mgrid[-3:2:100j, -3:3:100j]
Z= 0.5*np.exp(-(abs(X)**2 + abs(Y)**2)) \
+ param1*np.exp(-4*((abs(X+1.5))**4.2 + (abs(Y-1.4))**4.2))
fig = go.Figure(data=[
go.Surface(
x=X, y=Y, z=Z,
opacity=1.0,
contours={
"z": {"show": True, "start": 0, "end": 1, "size": 1/60,
"width": 1, "color": 'black'} # add <"usecolormap": True>
},
showscale=False, # try <showscale=True>
colorscale='Peach')],
)
fig.update_layout(
margin=dict(l=0,r=0,t=0,b=160),
font=dict(color='blue'),
scene = dict(xaxis_title='', yaxis_title='',zaxis_title='',
xaxis_visible=False, yaxis_visible=False, zaxis_visible=False,
aspectratio=dict(x=1, y=1, z=0.6)), # resize by shrinking z
scene_camera = dict(eye=dict(x=param2, y=param3, z=param4))) # change vantage point
return(fig)
#-- main
import moviepy.video.io.ImageSequenceClip # to produce mp4 video
from PIL import Image # for some basic image processing
Nframes=300 # must be > 50
flist=[] # list of image filenames for the video
w, h, dpi = 4, 3, 300 # width and heigh in inches
fps=10 # frames per second
for frame in range(0,Nframes):
image='contour'+str(frame)+'.png' # filename of image in current frame
print("Creating image",image) # show progress on the screen
fig=create_3Dplot(frame)
fig.write_image(file=image, width=w*dpi, height=h*dpi, scale=1)
# fig.show()
flist.append(image)
# output video / fps is number of frames per second
clip = moviepy.video.io.ImageSequenceClip.ImageSequenceClip(flist, fps=fps)
clip.write_videofile('contourvideo.mp4')
You can easily add axes and labels, change font sizes and so on. The parameters to handle this are present in the source code, but turned off in the present version. Below is the code for the Matplotlib version.
import numpy as np
from mpl_toolkits.mplot3d import axes3d
import matplotlib.pyplot as plt
plt.rcParams['lines.linewidth']= 0.5
plt.rcParams['axes.linewidth'] = 0.5
plt.rcParams['axes.linewidth'] = 0.5
SMALL_SIZE = 6
MEDIUM_SIZE = 8
BIGGER_SIZE = 10
plt.rc('font', size=SMALL_SIZE) # controls default text sizes
plt.rc('axes', titlesize=SMALL_SIZE) # fontsize of the axes title
plt.rc('axes', labelsize=MEDIUM_SIZE) # fontsize of the x and y labels
plt.rc('xtick', labelsize=SMALL_SIZE) # fontsize of the tick labels
plt.rc('ytick', labelsize=SMALL_SIZE) # fontsize of the tick labels
plt.rc('legend', fontsize=SMALL_SIZE) # legend fontsize
plt.rc('figure', titlesize=BIGGER_SIZE) # fontsize of the figure title
fig = plt.figure()
ax = fig.add_subplot(111, projection="3d")
X, Y = np.mgrid[-3:3:30j, -3:3:30j]
Z= np.exp(-(abs(X)**2 + abs(Y)**2)) + 0.8*np.exp(-4*((abs(X-1.5))**4.2 + (abs(Y-1.4))**4.2))
ax.plot_surface(X, Y, Z, cmap="coolwarm", rstride=1, cstride=1, alpha=0.2)
# ax.contourf(X, Y, Z, levels=60, colors="k", linestyles="solid", alpha=0.9, antialiased=True)
ax.contour(X, Y, Z, levels=60, linestyles="solid", alpha=0.9, antialiased=True)
plt.savefig('contour3D.png', dpi=300)
plt.show()
3. How I Learned to Do it, and How You Can Do it Too
Assume you are asked to produce good quality, 3D contour plots in Python — not surface plots — and you have no idea how to start. Yet, you are familiar with Matplotlib, but rather new to Python. You have 48 hours to complete this project (two days of work at your office, minus the regular chores eating your time every day). How would you proceed? Here I explain how I did it, as I was in the same situation (self-imposed onto myself). I focus on the 3D contour plot only, as I knew beforehand how to quickly turn it into a video, as long as I was able to produce a decent single plot. My strategy is broken down into the following steps.
- I quickly realized it would be very easy to produce 2D contour or surface plots, but not 3D contour plots. I googled “contour map 3d matplotib”. Not satisfied with the results, I searched images, rather than the web. This led me to a Stackoverflow forum question here, which in turn led me to some page on the Matplotlib website, here.
- After tweaking some parameters, I was able to produce Figure 2. Unsatisfied with the result and spending quite a bit of time trying to fix the glitch, I asked for a fix on Stackoverflow. You can see my question, and the answers that were posted, here. One participant suggested to use color transparency, but this was useless, as I had tried it before without success. The second answer came with a piece of code, and the author suggested that I used Plotly instead of Matplotlib. I trusted his advice, and got his code to work after installing Plotly (I got an error message asking me to install Kaleido, but that was easy to fix). Quite exciting, but that was far from the end.
- I googled “Matplotlib vs Plotly”, to make sure it made sense using Plotly. I was convinced, especially given my interest in scientific computing. Quickly though, I realized my plots were arbitrarily truncated. I googled “plotly 3d contour map” and “plotly truncated 3d contour”, which led me to various websites including a detailed description of the
layout
andscene
parameters. This webpage was particularly useful, as it offered a solution to my problem.
- I spent a bit of time to figure out how to remove the axes and labels, as I feared they could cause problems in the video, changing from one frame to the next one, based on past experience. It took me 30 minutes to find the solution by trial and error. But then I realized that there was one problem left: in the PNG output image, the plot occupied only a small portion, even though it looked fine within the Python environment. Googling “plotly write_image” did not help. I tried to ask for a fix on Stackoverflow, but was not allowed to ask a question for another 24 hours. I asked my question in the Reddit Python forum instead.
- Eventually, shrinking the Z axis, modifying the orientation of the plot, the margins, and the dimensions of the images, I got a substantial improvement. By the time I checked for a potential answer to my Reddit question, my post had been deleted by an admin. But I had finally solved it. Well, almost.
- My concern at this point was using the correct DPI (dots per inch) and FPS (frames per second) for the video, and make sure the size of the video was manageable. Luckily, all the 300 frames (the PNG images, one per plot), now automatically generated, had the exact same physical dimension. Otherwise I would have had to resize them (which can be done automatically). Also, the rendering was good, not pixelized. So I did not have to apply anti-aliasing techniques. And here we are, I produced the video and was happy!
- So I thought. I realized, when producing the animated gif, that there was still a large portion of the images unused (blank). Not as bad as earlier, but still not good enough for me. Now I know how to crop hundreds of images automatically in Python, but instead I opted to load my video on Ezgif, and use the crop option. The final version posted in this article is this cropped version. I then produced another video, with 4 mountains, rising up, merging or shrinking according to various schedules. This might be the topic of a future article, as it is going into a new direction: video games.
About the Author

Vincent Granville is a pioneering data scientist and machine learning expert, co-founder of Data Science Central (acquired by TechTarget in 2020), founder of MLTechniques.com, former VC-funded executive, author and patent owner. Vincent’s past corporate experience includes Visa, Wells Fargo, eBay, NBC, Microsoft, and CNET. Vincent is also a former post-doc at Cambridge University, and the National Institute of Statistical Sciences (NISS).
Vincent published in Journal of Number Theory, Journal of the Royal Statistical Society (Series B), and IEEE Transactions on Pattern Analysis and Machine Intelligence. He is also the author of “Intuitive Machine Learning and Explainable AI”, available here. He lives in Washington state, and enjoys doing research on spatial stochastic processes, chaotic dynamical systems, experimental math and probabilistic number theory.
You must log in to post a comment.