5

I'm using matplotlib to create a figure with many small subplots (something like 4 rows, 8 columns). I've tried a few different ways, and the fastest that I can get matplotlib to make the axes is ~2 seconds. I saw this post about just using one axes object to display many small images, but I would like to be able to have ticks and titles on the axes. Is there any way to speed this process up, or does making axes in matplotlib just take a reasonably long time?

Here is what I have tried so far:

import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1 import ImageGrid
import time

fig = plt.figure(figsize=(10,6))

plt.clf(); t = time.time()
grid = ImageGrid(fig, 111, 
                nrows_ncols = (4, 8))
print 'Time using ImageGrid: %.2f seconds'%(time.time()-t)

plt.clf(); t = time.time()
axes = plt.subplots(4,8)
print 'Time using plt.subplots: %.2f seconds'%(time.time()-t)

plt.clf(); t = time.time()
axes = []
for row in range(0,4):
    for col in range(1,9):
        ax = plt.subplot(4,8,row*8+col)
        axes.append(ax)
print 'Time using ptl.subplot loop: %.2f seconds'%(time.time()-t)

output:

Time using ImageGrid: 4.25 seconds
Time using plt.subplots: 2.11 seconds
Time using ptl.subplot loop: 2.34 seconds

Taking Joe Kington's suggestion, I tried to pickle the figure and axes so that I would at least not need to create them every time that I run the script. However, loading the file actually takes longer:

import matplotlib.pyplot as plt
import pickle
import time

t = time.time()
fig,axes = plt.subplots(4,8,figsize=(10,6))
print 'Time using plt.subplots: %.2f seconds'%(time.time()-t)

pickle.dump((fig,axes),open('fig.p','wb'))

t = time.time()
fig,axes = pickle.load(open('fig.p','rb'))

print 'Time to load pickled figure: %.2f seconds'%(time.time()-t)

output:

Time using plt.subplots: 2.01 seconds
Time to load pickled figure: 3.09 seconds
Community
  • 1
  • 1
DanHickstein
  • 6,588
  • 13
  • 54
  • 90

1 Answers1

7

You're not going to beat subplots by any substantial amount. Creating new axes is a fairly expensive operation, and you're creating 32 of them each time. However it's only done once.

Is creating a new figure and axes really your bottleneck? If so, you're probably doing something wrong.

If you're trying to create an animation, just update the artists (e.g. image.set_data, etc) and redraw rather than making a new figure and axes each time.

(Also, axes = plt.subplots(4, 8) is incorrect. axes will be a tuple instead of a sequence of axes objects as you've currently written it. subplots returns a figure instance and an array of axes. It should be fig, axes = plt.subplots(...).)


Just to expand on my comment below about pickling:

It's often handy to cache "expensive" results when you're changing un-related parameters. This is particularly common in scientific "scripts", where you'll often have some fairly slow data parsing or intermediate calculation and a dependent set of parameters that you're semi-interactively "tweaking".

There's nothing wrong with just commenting out a few lines and explicitly saving/loading the pickled results.

However, I've found it handy to keep a decorator around that does this automatically. There are better ways to do this in many cases, but for a one-size-fits-all solution, it's quite handy:

import os
import cPickle as pickle
from functools import wraps

class Cached(object):
    def __init__(self, filename):
        self.filename = filename

    def __call__(self, func):
        @wraps(func)
        def new_func(*args, **kwargs):
            if not os.path.exists(self.filename):
                results = func(*args, **kwargs)
                with open(self.filename, 'w') as outfile:
                    pickle.dump(results, outfile, pickle.HIGHEST_PROTOCOL)
            else:
                with open(self.filename, 'r') as infile:
                    results = pickle.load(infile)
            return results
        return new_func

Then you can just do something like (presuming that the class above is in utilities.py):

import matplotlib.pyplot as plt
from utilities import Cached

@Cached('temp.pkl')
def setup():
    fig, axes = plt.subplots(8, 4)
    # And perhaps some other data parsing/whatever
    return fig, axes

fig, axes = setup()
for ax in axes.flat:
    ax.plot(range(10))
plt.show()

The decorated function will only be run if the given filename ("temp.pkl") isn't present, and the cached results stored in temp.pkl will be loaded otherwise. This will work for anything that can be pickled, not just matplotlib figures.

Beware of working with a saved state, though. If you change what setup does and re-run, the "old" results will be returned!! Be sure to manually delete the cached file if you change what the wrapped function does.

Of course, this is overkill if you're just doing it in one place. It can be handy to have around if you find yourself caching intermediate results frequently, though.

Joe Kington
  • 275,208
  • 71
  • 604
  • 463
  • Okay, good to know that I cannot do much better than plt.subplots(). I'm not making an animation, just a figure with lots of little plots. So, whenever I make a change to the figure, I need to re-run the script and wait a few seconds to set up the subplots. It might not end up being the bottleneck, but still an annoying few seconds that I need to wait. Thanks for your answer! – DanHickstein Jan 06 '14 at 17:20
  • 1
    That makes sense. A couple-second lag certainly gets annoying when you're trying to tweak various things and re-run. One thing you might do is try pickling the "blank" figure with the grid of subplots and then loading the pickled version rather than creating a new one each time. In this particular case, it's a quite bit faster. – Joe Kington Jan 06 '14 at 17:29
  • I tried the pickling method, but it actually takes longer than creating the subplots. But, perhaps I'm not using the pickle module correctly. I added this to the bottom of the question. – DanHickstein Jan 06 '14 at 18:12
  • 1
    @DanHickstein - Try a) specifying a binary pickle protocol (e.g. `pickle.dump(objects, file, pickle.HIGHEST_PROTOCOL)` and b) using `cPickle` instead of `Pickle` if you're on python 2.x. (Just do `import cPickle as pickle` instead of `import pickle`.) Pickling is about 3x faster on my machine, but that's going to depend heavily on I/O speed, etc, so it may or may not be faster on your system. – Joe Kington Jan 06 '14 at 18:16
  • Using cPickle dropped the time to 1.3 seconds. Using pickle.HIGHEST_PROTOCOL further reduced the time to 0.7 seconds. Thanks! – DanHickstein Jan 06 '14 at 18:23
  • Having just learned about decorators today, this is such an excellent example of their use. Thanks! – James Owers Oct 29 '15 at 03:30