こんな人にオススメ
pythonのpltでグラフ内に他のグラフを別枠で表示したいんだけどどうしたらいい?
あと、グラフ内にそのグラフの拡大図も載せたいんだけどどうしたらいい?
ということで、今回はpythonのmatplotlib.pyplot
、plt
でグラフ内にグラフを描く方法について解説する。加えてグラフ内にグラフの一部の拡大を載せる方法についても解説する。
今回の手法がいつ役立つかというと、前者では1つのグラフ内に別情報としてグラフを表示する場合、後者では拡大したい部分がごちゃごちゃしているけど全体のプロットも見せたい時とか。
使う機会はそんなにないかもしれない。実際、自分も1回しか使ったことない。けど知っていると何かの役に立ちそう。
python環境は以下。
- Python 3.9.4
- matplotlib 3.4.2
- numpy 1.20.3
下準備
import sys import matplotlib.pyplot as plt import numpy as np sys.path.append('../../') import plt_layout_template as template x = np.arange(0, 10, 0.1) y1 = x ** 1 y11 = x ** 1.1 y15 = x ** 1.5
まずは下準備としてのimport
関連とデータの作成。今回は以下の2式に登場してもらう。
$\begin{aligned} y1\ &=\ x^1 \\ y11\ &=\ y^{1.1} \\ y15\ &=\ x^{1.5} \end{aligned}$
また、これらの配列と凡例用のラベルと予めdict
としてまとめておく。
# プロットデータをまとめる ys = {'y1': y1, 'y11': y11, 'y15': y15, } # グラフラベルを予めまとめる labels = { 'y1': 'y=x', 'y11': 'y=x\$^{1.1}$', 'y15': 'y=x\$^{1.5}$', }
plt_layout_template
については2つ上のディレクトリにファイルが存在しているので、'../../'
で登っている。plt_layout_template
については以下参照。
-
-
【pltテンプレート】matplotlib.pyplotのグラフ作成テンプレート
続きを見る
プロット用の関数
def dct2plot(ax, ydct: dict, ldct: dict): """入力されたaxにldctでラベル付されたydctのyをプロットする Parameters ---------- ax : matplotlib.axes._subplots.AxesSubplot プロットするグラフの軸 ydct : dict プロットするyの値 ldct : dict プロットするyの値の凡例 """ for name, y in ydct.items(): ax.plot(x, y, label=ldct[name])
下準備最後として、予め全データをプロットするための関数を定義しておく。入力値は以下。
ax
: プロットするグラフの軸ydct
: プロットするy
の値ldct
: プロットするy
の値の凡例
この関数に先程のydct
とldct
、そしてこれから作成するそれぞれのグラフのax
を入れることで自動でプロットされる。plotly
ではあるがこの関数化の一例は以下参照。
-
-
【plotly&工夫】楽にグラフを描くためのplotlyの関数化
続きを見る
グラフ内に別のグラフを描画
まずは結論の1つ、グラフ内に別のグラフを描画するところから始める。
簡単にグラフ内グラフを作成
単純なグラフ内グラフの作成手順は以下。簡単2step。
- 大元のグラフを作成(上の画像だと直線のグラフ)
- 大元のグラフの一部に新たにグラフ枠を作成(右下のグラフ)
具体的なコードは以下。ax1.inset_axes
が右下の新たなグラフ作成を示す。
# グラフ内に他のグラフを描画 def graph_add(coord: tuple, name=''): fig, ax1 = plt.subplots(1, 1) # グラフ内グラフの基準はプロット領域の上下左右端 # 大元のグラフの基準は「プロット領域」の左端・下端が0、右端・上端が1 ax2 = ax1.inset_axes(coord) ax1.plot(x, y1, label=labels['y1']) # 同じaxに描画したらそのグラフ内にプロットは描画される ax2.plot(x, y11, label=labels['y11']) ax2.plot(x, y15, label=labels['y15']) # それぞれのaxに対して凡例をつける ax1.legend() ax2.legend() fig.savefig(f"graph_add{name}.png") graph_add( coord=[0.55, 0.1, 0.3, 0.4], )
ax1.inset_axes
の後には新たに作成したグラフ枠の位置を入れるが、その座標はax1.inset_axes(x0, y0, width, height)
で入力。x0
, y0
の基準は大元のグラフのプロット領域の左下だ。
仮にx0
,y0
をそれぞれ0
にすると以下のように大元のグラフのプロット領域左下に、新たに作成したグラフの左下が来るようになる。面倒なので新たに作成したグラフは小グラフと名づける。
graph_add( coord=[0.0, 0.0, 0.3, 0.4], name='_0' )
また、width
, height
に関しては、大元のグラフのプロット領域の左端・下端をそれぞれ0、右端・上端を1とした相対的な座標で記述される。したがって、大元のグラフを動かしても小グラフは動かない。
謎なのが、小グラフはぐりぐり動かせないということ。まあ見るだけなら関係ない。
凡例を1つの凡例に集約
さっきのグラフだと、それぞれのグラフ内に凡例が出てきて煩雑。ということで凡例を1つにまとめる。まとめるために必要な情報はプロット情報と凡例情報。これらを同時に取得できるのがax.get_legend_handles_labels()
。
戻り値としてプロット情報(handler
)と凡例情報(label
)が返されるのでこれを活用。あとは互いに結合させることで1つの凡例にできる。要するにバラバラになっている情報を1つに集約するってこと。
# グラフ内の全てのグラフの凡例を1つに集約 def graph_add_legend(): fig, ax1 = plt.subplots(1, 1) # プロット領域の上下左右端がグラフ全体の左端・下端を0、右端・上端を1で計算 ax2 = ax1.inset_axes([0.55, 0.1, 0.3, 0.4]) ax1.plot(x, y1, label=labels['y1']) # グラフ内グラフと見分けがつくように破線に変更 ax2.plot(x, y11, label=labels['y11'], linestyle='--') ax2.plot(x, y15, label=labels['y15'], linestyle='--') # それぞれのaxのプロットとラベルの情報を取得 handler1, label1 = ax1.get_legend_handles_labels() handler2, label2 = ax2.get_legend_handles_labels() # プロット情報とラベル情報を結合し、まとめた凡例として設定 ax1.legend(handler1 + handler2, label1 + label2) fig.savefig('graph_add_legend.png') graph_add_legend()
では、もしhandlerとlabelの1と2の順番を入れ替えたらどうなるのか。結果はプロットの線の表示と凡例の表示がチグハグになる。これだとどのプロットが真なのかわからなくなる。注意が必要。
# グラフ内の全てのグラフの凡例を1つに集約 # プロットと凡例の組み合わせがチグハグ def graph_add_legend_mismatch(): fig, ax1 = plt.subplots(1, 1) ax2 = ax1.inset_axes([0.55, 0.1, 0.3, 0.4]) ax1.plot(x, y1, label=labels['y1']) ax2.plot(x, y11, label=labels['y11'], linestyle='--') ax2.plot(x, y15, label=labels['y15'], linestyle='--') handler1, label1 = ax1.get_legend_handles_labels() handler2, label2 = ax2.get_legend_handles_labels() # handler1とhandler2を入れ替える ax1.legend(handler2 + handler1, label1 + label2) fig.savefig('graph_add_legend_mismatch.png') graph_add_legend_mismatch()
グラフ内に拡大した一部を描画
結論の2つ目。上のように値の最大値と最小値の差が大きいと、重なり合う部分の構造がわかりにくい時がある。そんな時に上の画像の左上の小グラフのように拡大図があるととても見やすい。
ここの子グラフだと遊んで色をつけているが、もちろん色付けは必須ではない。
俯瞰すると細かい部分がわからない
すでに述べたように、グラフ全体を見てしまうと細々した部分を見るのが難しくなる。例えば上の画像の左下の四角の領域だと、グラフの値の高い低いがわかりにくい。
かといってこの部分を拡大してしまうと全体のグラフの様子がわからなくなる。plotly
のように保存後もグリグリ動かすことが簡単なモジュールを使えばいいが、plt
だとなかなか有名ではない(あるのはある)。
ということで、以下ではグラフ内グラフで拡大図を作成する。なお、上のグラフのコードを以下に示しておく。四角はplt.Rectangle
で作成、こちらは絶対座標で四角を描いたので、グラフを動かすと四角も動く。
# 調べたいgraph範囲も描画 def graph_range(): fig, ax = plt.subplots(1, 1) # グラフプロット dct2plot(ax=ax, ydct=ys, ldct=labels) # 座標(-0.2, -0.5)を始点とする幅2, 高さ3の長方形を絶対位置で描画 # この長方形の内部の関数同士の関係がどうなのかが分かりにくい rect = plt.Rectangle((-0.2, -0.5), 2, 3, fc='None', ec='black') ax.add_patch(rect) ax.legend() fig.savefig('graph_range.png') graph_range()
グラフ内に同じプロットを表示
やることは基本的には先ほど同様、大元のグラフに小グラフを作成する。異なる点が、同じグラフを子グラフにも作成するということ。先ほどは別のグラフを作成していたが、今回は拡大ということで同じグラフを描画。
一応、小グラフにも凡例をつけたが、以降はつけないようにする。結構邪魔。
# グラフ内にグラフ全体を描画 def graph_inset(): fig, ax1 = plt.subplots(1, 1) # 元のプロット dct2plot(ax=ax1, ydct=ys, ldct=labels) # グラフ内グラフ ax2 = ax1.inset_axes([0.05, 0.35, 0.4, 0.4]) # 全プロットを描画 ax2.plot(x, y1, label='in y=x') ax2.plot(x, y11, label='in y=x$^{1.1}$') ax2.plot(x, y15, label='in y=x$^{1.5}$') ax1.legend() ax2.legend() fig.savefig('graph_inset.png') graph_inset()
子グラフの表示範囲を変更
さっきのグラフだと、大元のグラフを小グラフに複製しただけなので、拡大になるように小グラフの表示範囲を変更する。初めに黒の四角で囲った領域はplt.Rectange
にて以下の基準、幅・高さで作成。
x0
:-0.2
y0
:-0.5
width
:2
height: 3
よって、小グラフで制限する描画範囲は(x0, x0 + width)
と(y0, y0 + height)
。小グラフの凡例は予めdict
にしたが、凡例をつけると煩雑になるので凡例はつけないこととする。
# グラフ内グラフのラベルも予めまとめる in_labels = { 'y1': 'in y=x', 'y11': 'in y=x\$^{1.1}$', 'y15': 'in y=x\$^{1.5}$', } # グラフ内にグラフ全体を描画 # グラフ内グラフの表示範囲を当初の領域へ変更 def graph_inset_lim(): fig, ax1 = plt.subplots(1, 1) # 元のプロット dct2plot(ax=ax1, ydct=ys, ldct=labels) # グラフ内グラフ ax2 = ax1.inset_axes([0.05, 0.35, 0.4, 0.4]) # グラフ内グラフの表示範囲を設定 ax2.set_xlim(-0.2, 2 - 0.2) ax2.set_ylim(-0.5, 3 - 0.5) # グラフ内グラフも楽する dct2plot(ax=ax2, ydct=ys, ldct=in_labels) ax1.legend() # 表示すると鬱陶しいので非表示 # ax2.legend() fig.savefig('graph_inset_lim.png') graph_inset_lim()
拡大した一部がどこの部分かを明示
無事に拡大したい部分が拡大できるように設定したが、それがどの部分なのかがわかりにくい。最後に、小グラフが大元のグラフのうちどの部分なのかを示す。
方法は簡単で、ax1.indicate_inset_zoom(ax2)
とするだけ。超簡単。上の画像では該当範囲を赤に、枠線などの線を青にして透明度を上げている。もちろん蛇足。
# グラフ内にグラフ全体を描画 # グラフ内グラフがどの領域を拡大しているのかを描画 def graph_inset_zoom(xrange: tuple, yrange: tuple, name=''): fig, ax1 = plt.subplots(1, 1) # 元のプロット dct2plot(ax=ax1, ydct=ys, ldct=labels) # グラフ内グラフ ax2 = ax1.inset_axes([0.05, 0.35, 0.4, 0.4]) ax2.set_xlim(xrange) ax2.set_ylim(yrange) dct2plot(ax=ax2, ydct=ys, ldct=in_labels) # グラフ内グラフがどの部分を拡大しているのかを明示 ax1.indicate_inset_zoom(ax2, facecolor='red', edgecolor='blue', alpha=0.3) ax1.legend() # ax2.legend() fig.savefig(f"graph_inset_zoom{name}.png") graph_inset_zoom( xrange=(-0.2, 2 - 0.2), yrange=(-0.5, 3 - 0.5), )
しかし、このコードでも初めの問題だったどのグラフの方が値が大きいかについてはわかりにくい。ということで、最後に改良版を示しておく。
graph_inset_zoom( xrange=(-0.05, 1.1), yrange=(-0.05, 1.1), name='_kai', )
なるほど、横軸の値が大きくなると累乗部分が1.5の方が値は大きくなるけど、初めの方は累乗部分が1の方が大きいのか。すなわち累乗部分が大きい場合はスタートダッシュが切れていないっぽいな。ということがわかる。
ax.inset_axes
とfig.add_axes
おまけとして、似たようなグラフを作成できるfig.add_axes
についても触れておく。これはax.inset_axes
の代わりに書くことができるが、違いとしては、ax
に対して小グラフを描くかfig
に対して描くか。
これによって、ax.inset_axes
ではプロット領域の相対位置で小グラフを作成できたが、fig.add_axes
ではグラフ全体の相対位置で小グラフを作成することになる。
なので、基準位置(x0, y0)
をどちらも0
にすると上の画像のようにグラフの左下に小グラフが来るようになる。ただし、余白の値を使用することでinset_axes
と同じ座標系で扱うことができる。
# inset_axesに合わせるには余白の値を使う # 左端、下端が基準の相対地で表されている left = plt.rcParams['figure.subplot.left'] bottom = plt.rcParams['figure.subplot.bottom'] right = plt.rcParams['figure.subplot.right'] top = plt.rcParams['figure.subplot.top'] print(left, bottom, right, top) # 0.11 0.08 0.9 0.95
余白の値は左端と下端がそれぞれの基準で、相対値によって決められている。確認はplt.rcParams
でこれはdict
になっているので必要なkeyを指定すればいい。
この余白の値を使用して以下のように設定することで、数値部分がinset_axes
と同じ意味となる。複雑やな。
graph_add_addaxes( coord=[ left, bottom, 0.3 * (right - left), 0.4 * (top - bottom) ], name='_rc' )
初めのinset_axesが以下。同じ。
一部の拡大でより見やすく
今回はplt
を使ったグラフ内グラフの描画方法について解説した。使うことが少ないかもしれないがいつか役立つんじゃないだろうか。
執筆者が実際に使ったシーンは、指数関数的に減少するグラフの値が小さくなった時だ。要するに今回のグラフみたいな状況。plotlyを使えば拡大縮小は楽だけど一覧性はグラフ内グラフの方が上。
plt
はplotly
みたいにぐりぐり動かすことが少ない(できるのはできる)から、1枚のグラフで見やすくするのは良いのではないだろうか。
関連記事
-
-
【pltテンプレート】matplotlib.pyplotのグラフ作成テンプレート
続きを見る
-
-
【plotly&工夫】楽にグラフを描くためのplotlyの関数化
続きを見る
-
-
【plotly&バブルチャート】plotlyで各国の収入と平均寿命の時代変化をバブルチャートで描く
続きを見る
-
-
【plt vs plotly】matplotlibとgoでグラフの比較
続きを見る