Master the art of subplots in Python | by Ankit Gupta | Towards Data Science

Often while working with data, no matter big or small, sometimes you want to compare things side-by-side or plot different attributes or features individually. In such cases, a single figure is rendered insufficient. Thus, you need to know the art of working with subplots.
This article will focus on the concept of subplots. It will teach you six unique ways to create very simple and very complex grids in Python using Matplotlib.
“For every failure, there’s an alternative course of action. You just have to find it. When you come to a roadblock, take a detour” — Mary Kay Ash
Let’s first import some basic modules and use a fancy style sheet to give an artistic touch to our figures.
%matplotlib inline # To enable inline plotting in Jupyter Notebookimport numpy as npimport matplotlib.pyplot as pltplt.style.use('fivethirtyeight') # For better style
Let’s define some data to plot. We use our immortal sin and cosine curves for x∈(0, 3π).
x = np.linspace(0., 3*np.pi, 100) # 0 to 3*Pi in 100 stepsy_1 = np.sin(x) y_2 = np.cos(x)
Let’s now create our very first two subplots with a single row and two columns. Since theaxes object contains two subplots, you can access them using indices [0] and [1] because indexing starts at 0 in Python.
fig, axes = plt.subplots(nrows=1, ncols=2, figsize=(9, 3))axes[0].plot(x, y_1, '-', c='orange', label='sin(x)')axes[1].plot(x, y_2, '-', c='magenta', label='cos(x)')axes[0].legend(fontsize=16, frameon=False)axes[1].legend(fontsize=16, frameon=False)fig.suptitle('Subplots without shared y-axis')
Note: If you don’t like the indices notation, you can also use names for your axes as shown below, and then use them directly for plotting. The tuple (ax1, ax2) below represents the axis handles to individual subplots. Since both the above subplots have the same y-axis limits, you can remove the redundant y-axis values from the right-hand side subplot using the keyword sharey=True.
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(10, 3), sharey=True)ax1.plot(...)ax2.plot(...)
Subplots spanning multiple rows: In the above figure, the subplots were plotted in a columnar way. To plot them in two rows, you can use nrows=2, ncols=1. Now you will have to use the keyword sharex.
When you have more than 1 row and 1 column, you need two indices to access the individual subplots as shown in the code below. The indices start at 0. So, for 2 rows and 2 columns, the indices will be 0 and 1. The first and the second indices in the slice notation [i, j] correspond to the row (i) and column (j) number, respectively.
fig, axes = plt.subplots(nrows=2, ncols=2, figsize=(9, 5),                         sharey='row', sharex='row')axes[0, 0].plot(x+1, y_1+1, '-', c='orange')axes[0, 1].plot(x, y_2, '-', c='magenta')axes[1, 0].plot(x, y_1**2, '--', c='orange')axes[1, 1].plot(x, y_2**2, '--', c='magenta')axes[0, 0].set_ylabel(r'sin(x)')axes[0, 1].set_ylabel(r'cos(x)')axes[1, 0].set_ylabel(r'sin$^2$(x)')axes[1, 1].set_ylabel(r'cos$^2$(x)')fig.tight_layout()
In the above figure, you can choose how you want to share the x and the y-axes. I have chosen sharex='col' and sharey='row' which means the x-axis is shared across each column and the y-axis is shared across each row. Notice the different axes limits in the above figure to make sense of this.
As mentioned earlier, you can also use tuples to name your axes and avoid the index notation. The first tuple (ax1, ax2) corresponds to the first row subplots. Likewise, (ax3, ax4) corresponds to the second row.
fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, figsize=(9, 5))
In this method, you first create the figure object and then add the subplots manually one after the other. The following example creates a 2 x 2 grid. If you want multiple subplots to share the same x or y-axis, you can specify the corresponding axis while creating the subplot as demonstrated below.
NOTE: Here, the numbering starts at 1. So, for a 2 x 2 grid, the upper row will use the numbers(2, 2, 1) , (2, 2, 2) and the second row will use the numbers (2, 2, 3) , (2, 2, 4) , respectively. The first two indices are the number of total rows and columns, respectively while the third number specifies the subplot.
fig = plt.figure(figsize=(8, 6))ax1 = plt.subplot(2, 2, 1, frameon=True) ax1.plot(x+1, y_1+1)ax1.set_title('ax1')ax2 = plt.subplot(2, 2, 2, sharex=ax1, facecolor='orange')ax2.plot(x, y_2, '-r')ax2.set_title('Shares x-axis with ax1')ax3 = plt.subplot(2, 2, 3, sharey=ax1)ax3.plot(x, y_1**2, '-g')ax3.set_title('Shares y-axis with ax1')ax4 = plt.subplot(2, 2, 4, facecolor='orchid')ax4.plot(x, y_2**2, '-b')fig.tight_layout()
This method is useful for generating complex grids where the subplots span multiple rows or columns. Here, you create subplots at specified locations within the overall grid.
You have to first specify the overall grid size like (3, 3) in the example code below. Then you specify the starting location of the subplot using a tuple of indices in the order (row, column) where the indices start at 0. So, for a 3 x 3 grid, both the row and the column indices will be 0, 1, and 2. If you want a subplot to span multiple rows or columns, you specify the length of the span using the keywords rowspan or colspan.
def add_title(axes):    for i, ax in enumerate(axes):        ax.set_title("ax%d" % (i+1), fontsize=18)fig = plt.figure(figsize=(8, 8))ax1 = plt.subplot2grid((3, 3), (0, 0), colspan=2)ax2 = plt.subplot2grid((3, 3), (0, 2), rowspan=3)ax3 = plt.subplot2grid((3, 3), (1, 0), rowspan=2)ax4 = plt.subplot2grid((3, 3), (1, 1))ax5 = plt.subplot2grid((3, 3), (2, 1))add_title(fig.axes)
This method is also useful for generating complex grids. You need a basic understanding of the slicing and indexing notation of NumPy arrays to work with this method.
For example, the slice [0, :] means the first row (index 0) and all the columns (: represents all), the slice[1, :-1] means the second row (index 1) and all the columns except the last one (:-1 represents all but last).
import matplotlib.gridspec as gridspecfig = plt.figure(constrained_layout=True, figsize=(8, 8))spec = gridspec.GridSpec(ncols=3, nrows=3, figure=fig)ax1 = fig.add_subplot(spec[0, :])ax2 = fig.add_subplot(spec[1, :-1])ax3 = fig.add_subplot(spec[1, -1])ax4 = fig.add_subplot(spec[2, 1:])ax5 = fig.add_subplot(spec[2, 0])# Now you can plot individually as ax1.plot(), ax2.plot() etc.
This method is quite similar to Way 3 and uses the same index notation as explained above. This feature is available only in Matplotlib 3+ versions.
fig = plt.figure(constrained_layout=True, figsize=(8, 8))spec = fig.add_gridspec(3, 3)ax1 = fig.add_subplot(spec[0, :-1])ax1.set_title('ax1')ax2 = fig.add_subplot(spec[:, -1])ax2.set_title('ax2')ax3 = fig.add_subplot(spec[1:, 0])ax3.set_title('ax3')ax4 = fig.add_subplot(spec[1, 1])ax4.set_title('ax4')ax5 = fig.add_subplot(spec[-1, 1])ax5.set_title('ax5')
The latest version of Matplotlib 3.3 has introduced a new, less verbose, and a semantic way to generate complex, subplot grids. via subplot_mosaic(). You can also name your subplots as you like. You can also use short-hand ASCII notations to recreate the figure below.
The cool part is that, to generate the subplot grid shown below, you can pass the layout in the form of a list. A missing subplot is indicated as '.'. To span a subplot over two columns, you can repeat the name as I did for 'bar'. To span across rows (vertically), repeat the name vertically below in the second list. You can also use the names 'bar', 'hist', and 'scatter' to control/modify the properties of the corresponding subplots using a dictionary.
axes = plt.figure(constrained_layout=True).subplot_mosaic(                [['.', 'bar', 'bar'], # Note repitition of 'bar'                 ['hist', '.', 'scatter']])for k, ax in axes.items():    ax.text(0.5, 0.5, k, ha='center', va='center',             fontsize=36, color='magenta')# Using dictionary to change subplot propertiesaxes['bar'].set_title('A bar plot', fontsize=24)    axes['hist'].set_title('A histogram', fontsize=24)    axes['scatter'].set_title('A scatter plot', fontsize=24)
This brings me to the end of my post. If you are interested in knowing more about the latest features of Matplotlib, refer to my following articles.