こんな人にオススメ
そういえばpythonのmatplotlib.pyplot
、plt
で複数のグラフを同時に1つの画像にするにはどうするんだ?
ということで、今回はmatplotlib.pyplot
、plt
のsubplot
を使用して複数のグラフを1つの画像として作成する。今までは1つのグラフに複数のプロットを入れるか、全グラフをバラバラにするかしかなかったが、subplot
でキレイに見やすくグラフを作成できる。
なお、使用データは1次関数から4次関数まで。
python環境は以下。
- Python 3.9.4
- numpy 1.20.3
- matplotlib 3.4.2
subplot
を使わない場合
1つのグラフに複数のプロットを入れると、縦軸の値が極端に異なっていたりしたら見づらくなる。2軸にすれば2種類は解決するが、3種類以上だと対応しにくい。
また、全グラフをバラバラに作成してしまうとその分だけ画像の数が増えてしまって見づらく管理も大変になる。
全てのプロットを1つのグラフに
全てのグラフを1つのグラフに入れる場合はシンプルにfor
でプロットを回せばいいが、今回のように1次関数から4次関数までだと、その値の差が大きすぎて1次関数と2次関数の形状の違いがわかりにくい。
もちろん次数が上がると極端に数値が大きくなるということを言いたいのであればこれでいいのだが、パッと見でグラフの値を比較したい時には不向き。
2軸グラフで対処
2軸グラフを使用すればある程度の対処は可能。しかし、今回のように1次関数と2次関数、3次関数と4次関数でそれぞれ極端に値が異なるとこちらも見づらいグラフになる。
さらに、このグラフだと青と橙、赤と緑がそれぞれのセットに見えるが実はそうではない。コードを見ればわかるが、実は赤と青、緑と橙がセットのグラフなのだ。
このままだと誤解を与えかねないので、2軸グラフでも適切とはいえないだろう。
もはや全プロットをバラバラのグラフで作成
もはやバラバラで作成するパターン。この場合は大きなグラフで閲覧することは可能だが、グラフの数だけファイルが作成されるので管理も大変だしそれぞれを閲覧するのも大変。
ということで、これらを解消するための一つの案としてsubplot
を紹介する。
下準備
import numpy as np import matplotlib.pyplot as plt # 作成したグラフをインタラクティブに操作可能にする plt.ion() # グラフ画像の端をオレンジで着色 plt.rcParams['savefig.edgecolor'] = 'darkorange' x = np.arange(10)
まずは下準備としてimport
関連と初期設定と横軸の値。plt.ion()
とすることで、ipython
を使用したときにインタラクティブにグラフを作成することが可能。
執筆者は基本的にVScodeの「Code Runner」という拡張機能を使用しているのでion()
にしなくてもいいが、Code Runnerの場合はplt
のグラフを表示して確認することができないため一応記載。
Code RunnerなどのVScodeの拡張機能については以下参照。
また、今回はグラフの端を橙色で着色してグラフの端を強調している。そのためplt.rcParams
で画像保存の際の橋の色を設定した。plt.rcParams
をすることで設定が一時的に上書きされる。解除したかったら元の設定を上書きするか、exit
してpythonを抜ければい。
シンプルに2つのプロットを並べる
まずはシンプルに2種類のグラフを横もしくは縦に並べる。グラフはfig
とax
の書き方で書くことにする。一応plt
.の方法もあるが、カスタムしやすいのはfig
, ax
の方だからそうする。
左右に並べる
左側に1次関数、右側に2次関数を配置した。マーカーはそれぞれ円形と三角形で作成。plt.figure()
内にlinewidth
引数を設定しないと保存時のグラフ枠の色付けができないので引数を設定。
fig.suptitleはグラフ全体のタイトルを示している。注意点はsubtitle
ではなくsuptitle
ということ。
# 左右で作成 # linewidthを入れないとグラフにオレンジ(darkorange)の囲いができない fig = plt.figure(linewidth=1) fig.suptitle('subplot(left & right): ax1, ax2') # 左 y1 = x ** 1 ax1 = fig.add_subplot(1, 2, 1) ax1.set_title('ax1') ax1.set_xlabel("x") ax1.set_ylabel("y1") ax1.plot(x, y1, 'o-') # 右 y2 = x ** 2 ax2 = fig.add_subplot(1, 2, 2) ax2.set_title('ax2') ax2.set_xlabel("x") ax2.set_ylabel("y2") ax2.plot(x, y2, '^-') fig.savefig('subplot_lr')
fig.add_subplot
はfig.add_subplot((行の数), (列の数), (何番目のグラフか))
という指定。「何番目か」は左上が1で右に行くにつれて+1され、右端まで行けば1行下の左端まで行っての繰り返し。
fig.add_subplot(1, 2, 1)
は1行2列のグラフの1番目(左)にプロット、fig.add_subplot(1, 2, 2)
は1行2列のグラフの2番目(右)にプロットという意味。
上下に並べる
続いては上側に1次関数、下側に2次関数を配置した。先ほどとは異なり横長なのでグラフの印象がガラリと変わる。もちろん使用しているデータは同じ。
fig.add_subplot(2, 1, 1)
は2行1列のグラフの1番目(上)にプロット、fig.add_subplot(2, 1, 2)
は2行1列のグラフの2番目(下)にプロットという意味。
# 上下で作成 fig = plt.figure(linewidth=1) fig.suptitle('subplot(top & bottom): ax1, ax2') # 上 y1 = x ** 1 ax1 = fig.add_subplot(2, 1, 1) ax1.set_title('ax1') ax1.set_xlabel("x") ax1.set_ylabel("y1") ax1.plot(x, y1, 'o-') # 下 y2 = x ** 2 ax2 = fig.add_subplot(2, 1, 2) ax2.set_title('ax2') ax2.set_xlabel("x") ax2.set_ylabel("y2") ax2.plot(x, y2, '^-') fig.savefig('subplot_tb')
4分割で四方に並べる
続いては4分割にして四方に並べる。イメージは左上、右上、左下、右下。
1から4の順番に並べる
左上、右上、左下、右下の4分割にする場合はfig.add_subplot
を2, 2
に設定して、1
, 2
, 3
, 4
の数字を最後に書く。描画の順番は左上、右上、左下、右下で1
, 2
, 3
, 4
。
# subplotを4分割で fig = plt.figure(linewidth=1) fig.suptitle('subplot: ax1, ax2, ax3, ax4') # 左上 y1 = x ** 1 ax1 = fig.add_subplot(2, 2, 1) ax1.set_title('ax1') ax1.set_xlabel("x") ax1.set_ylabel("y1") ax1.plot(x, y1, 'o-') # 右上 y2 = x ** 2 ax2 = fig.add_subplot(2, 2, 2) ax2.set_title('ax2') ax2.set_xlabel("x") ax2.set_ylabel("y2") ax2.plot(x, y2, '^-') # 左下 y3 = x ** 3 ax3 = fig.add_subplot(2, 2, 3) ax3.set_title('ax3') ax3.set_xlabel("x") ax3.set_ylabel("y3") ax3.plot(x, y3, 's-') # 右下 y4 = x ** 4 ax4 = fig.add_subplot(2, 2, 4) ax4.set_title('ax4') ax4.set_xlabel("x") ax4.set_ylabel("y4") ax4.plot(x, y4, 'd-') fig.savefig('subplot_4')
プロット順番はバラバラでもOK
もちろん描画する順番に決まりはない。今回のグラフは右下のax4
を初めに描いた。その結果、ax4
のy
ラベルが左下のax3
の下に入ってしまい、上書きされてしまっている。
# 順番はこちらで指定可能 fig = plt.figure(linewidth=1) fig.suptitle('subplot: ax4, ax1, ax2, ax3') # 右下 # 初めに描いたので一番下に描かれる y4 = x ** 4 ax4 = fig.add_subplot(2, 2, 4) ax4.set_title('ax4') ax4.set_xlabel("x") ax4.set_ylabel("y4") ax4.plot(x, y4, 'd-') # 左上 y1 = x ** 1 ax1 = fig.add_subplot(2, 2, 1) ax1.set_title('ax1') ax1.set_xlabel("x") ax1.set_ylabel("y1") ax1.plot(x, y1, 'o-') # 右上 y2 = x ** 2 ax2 = fig.add_subplot(2, 2, 2) ax2.set_title('ax2') ax2.set_xlabel("x") ax2.set_ylabel("y2") ax2.plot(x, y2, '^-') # 左下 y3 = x ** 3 ax3 = fig.add_subplot(2, 2, 3) ax3.set_title('ax3') ax3.set_xlabel("x") ax3.set_ylabel("y3") ax3.plot(x, y3, 's-') fig.savefig('subplot_4_position')
4分割して3枚しか描かない
もちろん4枚分の領域を用意したからと言って必ずしも4枚のグラフを表示する必要はない。ここではax1
, ax2
, ax3
の3つのグラフのみを用意してグラフを作成した。
ax4 = fig.add_subplot(2, 2, 4)
を入れた場合は右下にグラフの枠だけを表示することが可能。この文を入れなかった場合は完全な空白となる。
片側は大きなグラフで他方を小さく
作成するsubplot
のサイズを複数種にすることで、大きさの異なるグラフを作成することが可能。上の例ではax1
は(1, 2, 1)
の1行2列、右側のax2
とax4
はそれぞれ(2, 2, 2)
, (2, 2, 4)
で2行2列のsubplot
の2
番目と4
番目にグラフを描いている。
# ax1は1行2列、ax2, x4を2行2列で作成することでax3の位置をax1で使用することが可能 fig = plt.figure(linewidth=1) fig.suptitle('subplot: ax1, ax2, ax4') # ax1は1行2列 ax1 = fig.add_subplot(1, 2, 1) ax1.set_title('ax1') ax1.set_xlabel("x") ax1.set_ylabel("y1") y1 = x ** 1 ax1.plot(x, y1, 'o-') # ax2は2行2列 ax2 = fig.add_subplot(2, 2, 2) ax2.set_title('ax2') ax2.set_xlabel("x") ax2.set_ylabel("y2") y2 = x ** 2 ax2.plot(x, y2, '^-') # ax4も2行2列 ax4 = fig.add_subplot(2, 2, 4) ax4.set_title('ax4') ax4.set_xlabel("x") ax4.set_ylabel("y4") y4 = x ** 4 ax4.plot(x, y4, 'd-') fig.savefig('subplot_124')
for
ループで楽してsubplot
これまではいちいち全プロットを指定して作成したが、もちろんfor
ループでも作成可能。for
ループの際にはplt.subplots
(plt.subplot
もあるが別物なので注意)を使用してax
を作成する。
# 使用するマーカー一覧 markers = ['o-', '^-', 's-', 'd-'] fig, ax = plt.subplots(nrows=2, ncols=2, linewidth=1) fig.suptitle('subplot(loop): ax1, ax2, ax3, ax4') # axを展開して2次元から1次元に(2*2=4の配列になる) ax = ax.ravel() for num, m in enumerate(markers): ax[num].set_title(f"ax{num + 1}") ax[num].set_xlabel('x') ax[num].set_ylabel(f"y{num + 1}") y = x ** (num + 1) ax[num].plot(x, y, m) fig.savefig('subplot_loop')
このax
は2次元配列になっているので、.ravel()
で1次元へと展開。あとはax[num]
でax1
などを指定してグラフを作成。ループを使用することでかなりスッキリしているし、コードの修正箇所も少なくて済む。展開前後のax
は以下。
fig, ax = plt.subplots(nrows=2, ncols=2, linewidth=1) fig.suptitle('subplot(loop): ax1, ax2, ax3, ax4') print(ax) # [[<AxesSubplot:> <AxesSubplot:>] # [<AxesSubplot:> <AxesSubplot:>]] print(type(ax)) # <class 'numpy.ndarray'> print(ax.shape) # (2, 2) # axを展開して2次元から1次元に(2*2=4の配列になる) ax = ax.ravel() print(ax) # [<AxesSubplot:> <AxesSubplot:> <AxesSubplot:> <AxesSubplot:>] print(type(ax)) # <class 'numpy.ndarray'> print(ax.shape) # (4,)
軸を共有
subplot
でグラフを作成していると、軸の値が同じな場合がある。そんな時は軸の共有で軸の値を省略することが可能。
横・縦軸どちらも共有
まずは横・縦軸ともに共有した場合。共有しているので、図の左側と下側にのみラベルが振られるようになる。
markers = ['o-', '^-', 's-', 'd-'] # 横・縦軸を共有 fig, ax = plt.subplots(nrows=2, ncols=2, sharex=True, sharey=True, linewidth=1) fig.suptitle('subplot(x, y share): ax1, ax2, ax3, ax4') ax = ax.ravel() for num, m in enumerate(markers): ax[num].set_title(f"ax{num + 1}") ax[num].set_xlabel('x') ax[num].set_ylabel(f"y{num + 1}") y = x ** (num + 1) ax[num].plot(x, y, m) fig.savefig('subplot_share_xy')
一方で共有しているので値が極端に異なる場合はその違いがぱっと見ではわかりにくい。
そんな時は横軸だけ共有のように片側だけ共有することももちろん可能。
横軸だけ共有
今度は横軸のみを共有。sharex
だけにしてsharey
を消せばいい。この場合だと縦軸の値がそれぞれのグラフに割り当てられるので、極端な値の違いがあってもパッと見でプロット形状がわかりやすい。
markers = ['o-', '^-', 's-', 'd-'] # 横軸を共有 fig, ax = plt.subplots(nrows=2, ncols=2, sharex=True, linewidth=1) fig.suptitle('subplot(x share): ax1, ax2, ax3, ax4') ax = ax.ravel() for num, m in enumerate(markers): # fig.add_subplot(2, 2, X)はX=1から始まるが、ax[X]はX=0から始まるのに注意 ax[num].set_title(f"ax{num + 1}") ax[num].set_xlabel('x') ax[num].set_ylabel(f"y{num + 1}") y = x ** (num + 1) ax[num].plot(x, y, m) fig.savefig('subplot_share_x')
グラフの余白や間隔を調節
ここまで色々subplot
でグラフを作成したが、どうにもグラフの軸ラベル同士が重なることが多い。プロット領域にまでラベルが来ているのでグラフの邪魔となる。
以下ではグラフの余白やグラフ同士の間隔を調節することでグラフを見やすくしている。
数字で設定
まずはグラフの調節を数値で行う方法。plt.subplots_adjust
を使用することでグラフの余白の大きさやグラフ同士の間隔を調節することが可能。
plt.subplots_adjust
で使用した引数は以下。なお、left
, right
に関してはグラフの左端を0
、右端を1
に、top
, bottom
に関してはグラフの下端を0
、上端を1
とする。
left
: グラフ全体の左端から、左側のグラフのプロット部左端までの間隔right
: グラフ全体の左端から、右側のグラフのプロット部右端までの間隔top
: グラフ全体の下端から、上側のグラフのプロット部上端までの間隔bottom
: グラフ全体の下端から、下側のグラフのプロット部下端までの間隔wspace
: グラフの近いプロット部の左右端同士の間隔hspace
: グラフの近いプロット部の上下端同士の間隔
markers = ['o-', '^-', 's-', 'd-'] fig, ax = plt.subplots(nrows=2, ncols=2, linewidth=1) fig.suptitle('subplot(loop): ax1, ax2, ax3, ax4') ax = ax.ravel() # それぞれの値を調節 plt.subplots_adjust( left=0.1, # グラフ左を0とした時の左の余白 bottom=0.1, # グラフ左を0とした時の下の余白 right=0.95, # グラフ左を0とした時の右の余白 top=0.9, # グラフ左を0とした時の上の余白 wspace=0.3, # グラフの横の間隔 hspace=0.5, # グラフの縦の間隔 ) for num, m in enumerate(markers): ax[num].set_title(f"ax{num + 1}") ax[num].set_xlabel('x') ax[num].set_ylabel(f"y{num + 1}") y = x ** (num + 1) ax[num].plot(x, y, m) fig.savefig('subplot_adjust')
なお、新たなfig
を宣言するとplt.subplots_adjust
の設定は失われる。ずっと持続させたいなら最初に書いたplt.rcParams
を使用して設定する。
自動調節
いちいち値を調節するのが面倒な時はconstrained_layout
引数で設定することで勝手に自動調節してくれる。
fig, ax = plt.subplots(nrows=2, ncols=2, constrained_layout=True, linewidth=1) fig.suptitle('subplot(loop): ax1, ax2, ax3, ax4') ax = ax.ravel() for num, m in enumerate(markers): ax[num].set_title(f"ax{num + 1}") ax[num].set_xlabel('x') ax[num].set_ylabel(f"y{num + 1}") y = x ** (num + 1) ax[num].plot(x, y, m) fig.savefig('subplot_constrained')
また、fig.tight_layout()
を指定することでも自動調節してくれる。
fig, ax = plt.subplots(nrows=2, ncols=2, linewidth=1) fig.suptitle('subplot(tight_layout): ax1, ax2, ax3, ax4') ax = ax.ravel() for num, m in enumerate(markers): ax[num].set_title(f"ax{num + 1}") ax[num].set_xlabel('x') ax[num].set_ylabel(f"y{num + 1}") y = x ** (num + 1) ax[num].plot(x, y, m) fig.tight_layout() fig.savefig('subplot_tight_layout')
plt.GridSpec
で各グラフサイズをバラバラにする
最後に各グラフのサイズをバラバラに作成するということについて解説する。簡単なグラフに関しては既に上で解説した通りだが、ここではさらに複雑なものを作成してみる。
1グラフずつサイズ調節
このグラフでは4行3列のグリッドを作成し、このグリッドに沿うようにデータを作成する。グリッドのイメージは以下。あくまでもイメージなので、実際には多少の値の違いなどは見られるかもしれん。赤文字が行・列のグリッドの番号。その他は余白や間隔の位置。
4行3列のグリッドの内、以下のように区画を区切ることでグラフを作成した。
ax1
: 0行目 × 0, 1列目ax2
: 0行目 × 2列目ax3
: 1, 2, 3行目 × 0列目ax4
: 2, 3行目 × 1, 2列目
fig = plt.figure(linewidth=1) fig.suptitle('subplot(grid): ax1, ax2, ax3, ax4') # 4行3列のグリッドを作成 grid = plt.GridSpec(4, 3) # 0行目だけ、0と1列目の2列分を使用 ax1 = fig.add_subplot(grid[0, 0:2]) # 0行目だけ、2列目だけを使用 ax2 = fig.add_subplot(grid[0, 2:]) # 1から3行目、0列目だけを使用 ax3 = fig.add_subplot(grid[1:, 0]) # 2と3行目、1と2列目を使用 ax4 = fig.add_subplot(grid[2:, 1:]) ax1.set_title('ax1') ax1.set_xlabel("x") ax1.set_ylabel("y1") y1 = x ** 1 ax1.plot(x, y1, 'o-') ax2.set_title('ax2') ax2.set_xlabel("x") ax2.set_ylabel("y2") y2 = x ** 2 ax2.plot(x, y2, '^-') ax3.set_title('ax3') ax3.set_xlabel("x") ax3.set_ylabel("y3") y3 = x ** 3 ax3.plot(x, y3, 's-') ax4.set_title('ax4') ax4.set_xlabel("x") ax4.set_ylabel("y4") y4 = x ** 4 ax4.plot(x, y4, 'd-') fig.savefig('subplot_grid')
なお、変数grid
, ax1
, ax2
, ax3
, ax4
の値とタイプの出力値は以下。
for
ループで一気に調節
先ほどはグリッドを決めてax
を1つずつ追加してきたが、もちろんfor
ループで一気に処理することも可能。変数にグリッドの情報を入れたらいいだけ。応用。
また、ここではグラフの余白や間隔を調節して多少は見やすくしている。調節は辞書のadjust
を使用し、これをplt.GridSpec
の後で**
で展開。別にplt.GridSpec(left=...)
と書いてもいいが面倒なので展開。
# グラフ調整用の辞書 adjust = dict( left=0.1, bottom=0.1, right=0.95, top=0.9, wspace=0.6, hspace=1.1, ) fig = plt.figure(linewidth=1) fig.suptitle('subplot(loop): ax1, ax2, ax3, ax4') # 4行3列のグリッドを作成 # グリッドの調整を「**」の展開で行う grid = plt.GridSpec(4, 3, **adjust) grids = [grid[0, 1], grid[0, 2:], grid[1:, :2], grid[1:3, 2:]] markers = ['o-', '^-', 's-', 'd-'] for num, (grid, m) in enumerate(zip(grids, markers)): ax[num] = fig.add_subplot(grid) ax[num].set_title(f"ax{num + 1}") ax[num].set_xlabel('x') ax[num].set_ylabel(f"y{num + 1}") y = x ** (num + 1) ax[num].plot(x, y, m) fig.savefig('subplot_grid_loop')
グラフの調節は既に書いたようにplt.subplots_adjust
# それぞれの値を調節 plt.subplots_adjust( left=0.1, # グラフ左を0とした時の左の余白 bottom=0.1, # グラフ左を0とした時の下の余白 right=0.95, # グラフ左を0とした時の右の余白 top=0.9, # グラフ左を0とした時の上の余白 wspace=0.3, # グラフの横の間隔 hspace=0.5, # グラフの縦の間隔 )
で調節しても無視される。また、constrained_layoutを使用しても警告&無視され、sharex
, sharey
だとエラー。
# grid使用中にconstrained_layout=Trueをすると警告が出て調整されない fig = plt.figure(constrained_layout=True, linewidth=1) # UserWarning: There are no gridspecs with layoutgrids. Possibly did not call parent GridSpec with the "figure" keyword # fig.savefig('subplot_grid_loop') # sharex, shareyはエラー fig = plt.figure(linewidth=1, sharex=True, sharey=True) # TypeError: __init__() got an unexpected keyword argument 'sharex'
plt.GridSpec
なしでもグリッドは作成可能
既に上で書いたように、subplot
の作成領域の個数を調整することでもグリッドは作成可能。ここではax2
を描かずに右の真ん中を空けてみた。
adjust = { 'figure.subplot.left': 0.11, 'figure.subplot.bottom': 0.1, 'figure.subplot.top': 0.9, 'figure.subplot.right': 0.98, 'figure.subplot.hspace': 0.8, 'figure.subplot.wspace': 0.3, } for key, val in adjust.items(): plt.rcParams[key] = val fig = plt.figure(linewidth=1) fig.suptitle('subplot(unuse grid): ax4, ax1, ax3') ax4 = fig.add_subplot(1, 2, 1) ax4.set_title('ax4') ax4.set_xlabel("x") ax4.set_ylabel("y4") y4 = x ** 4 ax4.plot(x, y4, 'd-') ax1 = fig.add_subplot(3, 2, 2) ax1.set_title('ax1') ax1.set_xlabel("x") ax1.set_ylabel("y1") y1 = x ** 1 ax1.plot(x, y1, 'd-') ax3 = fig.add_subplot(3, 2, 6) ax3.set_title('ax3') ax3.set_xlabel("x") ax3.set_ylabel("y3") y3 = x ** 3 ax3.plot(x, y3, 'd-') fig.savefig('subplot_grid_unuse_grid')
最後の最後なので、グラフの余白や間隔の調節はplt.rcParams
で行った。ipython
などをしていると、そのpythonをexit
しない限りは設定が保持されるので注意。裏を返せばexit
しない限りは設定した項目が保持されるので、グラフを描くごとに設定しなくてもいい。
パッと見の理解を速める
今回はmatplotlib.pyplot
、plt
のsubplot
について簡単なグラフを作成した。最近はずっとplotly
のボタンでグラフの切り替えを設定し、複数プロットは1つのグラフに納めていた。
しかし、そうすると静止画で保存したときにボタンを押した後のグラフは見ることができない。今回のようにsubplot
を使用することでパッと見でグラフの概要を掴むことができるのでおすすめ。
パッと見で理解できることで無駄に時間を使用することがなく、スムーズに内容の話ができると思う。
関連記事
-
-
【plt&pandas】df.plot()でmatplotlibのグラフを作成
2022/8/19
こんな人にオススメ pandasのデータフ& ...
-
-
【plotly&pandas】df.plot()でPlotlyのグラフを作成
2022/8/19
こんな人にオススメ pnadasのデータフ& ...
-
-
【文字入力&グラフに反映】inputとtkinterでグラフに任意の文字を反映
2022/8/19
こんな人にオススメ グラフを作成 ...
-
-
【plt&マーカー一覧】matplotlibのマーカーをグラフ化
2022/8/19
こんな人にオススメ matplotlib.pyplot、pltのマ ...