カテゴリー

plt

【plt&グラフ内グラフ】matplotlibでグラフ内に別グラフやグラフの一部拡大を描画

2021年7月25日

こんな人にオススメ

pythonのpltでグラフ内に他のグラフを別枠で表示したいんだけどどうしたらいい?

あと、グラフ内にそのグラフの拡大図も載せたいんだけどどうしたらいい?

ということで、今回はpythonのmatplotlib.pyplotpltでグラフ内にグラフを描く方法について解説する。加えてグラフ内にグラフの一部の拡大を載せる方法についても解説する。

今回の手法がいつ役立つかというと、前者では1つのグラフ内に別情報としてグラフを表示する場合、後者では拡大したい部分がごちゃごちゃしているけど全体のプロットも見せたい時とか。

使う機会はそんなにないかもしれない。実際、自分も1回しか使ったことない。けど知っていると何かの役に立ちそう。

python環境は以下。

  • Python 3.9.4
  • matplotlib 3.4.2
  • numpy 1.20.3

運営者のメガネです。YouTubeTwitterInstagramも運営してます。

自己紹介はこちらから、お問い合わせはこちら。

運営者メガネ

下準備

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.rcParamsでデフォルトを変更した後のグラフ
【pltテンプレート】matplotlib.pyplotのグラフ作成テンプレート

こんな人にオススメmatplotlib.pyplot(plt ...

続きを見る

プロット用の関数

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の値の凡例

この関数に先程のydctldct、そしてこれから作成するそれぞれのグラフのaxを入れることで自動でプロットされる。plotlyではあるがこの関数化の一例は以下参照。

【plotly&工夫】楽にグラフを描くためのplotlyの関数化

こんな人にオススメ plotlyでグ} ...

続きを見る

グラフ内に別のグラフを描画

まずは結論の1つ、グラフ内に別のグラフを描画するところから始める。

簡単にグラフ内グラフを作成

単純なグラフ内グラフの作成手順は以下。簡単2step。

  1. 大元のグラフを作成(上の画像だと直線のグラフ)
  2. 大元のグラフの一部に新たにグラフ枠を作成(右下のグラフ)

具体的なコードは以下。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_axesfig.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を使えば拡大縮小は楽だけど一覧性はグラフ内グラフの方が上。

pltplotlyみたいにぐりぐり動かすことが少ない(できるのはできる)から、1枚のグラフで見やすくするのは良いのではないだろうか。

関連記事

plt.rcParamsでデフォルトを変更した後のグラフ
【pltテンプレート】matplotlib.pyplotのグラフ作成テンプレート

こんな人にオススメmatplotlib.pyplot(plt ...

続きを見る

【plotly&工夫】楽にグラフを描くためのplotlyの関数化

こんな人にオススメ plotlyでグ} ...

続きを見る

【plotly&バブルチャート】plotlyで各国の収入と平均寿命の時代変化をバブルチャートで描く

こんな人にオススメ 今は世 ...

続きを見る

【plt vs plotly】matplotlibとgoでグラフの比較

こんな人にオススメ 今までpyt ...

続きを見る

スイッチボット

2022/11/28

【SwitchBotロックレビュー】これからのスタンダードになりうるスマートロック

こんな人にオススメ SwitchBotからスマートロック「SwitchBotロック」が発売された ...

生活に役立つ

2022/11/28

【メガネ厳選】クソ便利に使っているサービスやアイテム達

このページでは執筆者「メガネ」が実際に使って便利だと感じているサ ...

マウス

2022/9/11

【Logicool MX ERGO vs MX Master 3】ERGOをメインにした決定的な理由

こんな疑問・お悩みを持っている人におすすめ 執筆者はLogicoolのハイエンӠ ...

完全ワイヤレスイヤホン(TWS)

2022/11/21

【ながら聴きイヤホン比較】SONY LinkBuds、ambie、BoCoはどれがおすすめ?

こんな人におすすめ 耳を塞がない開放型のイヤホンに完全ワイヤレスӟ ...

macOSアプリケーション

2022/10/15

【M1 Mac】MacBook Proに入れている便利でニッチなアプリを21個紹介する

こんな人におすすめ MacBookを購入してLINEとか必要最低限のアプリは入れた。 ...

完全ワイヤレスイヤホン(TWS)

2022/10/23

【SENNHEISER MOMENTUM True Wireless 3レビュー】高レベルでバランス型の高音質イヤホン

こんな人におすすめ SENNHEISER MOMENTUM True Wireless 3って実際のところどうなの? 評判は良い ...

完全ワイヤレスイヤホン(TWS)

2022/11/21

【SONY WF-1000XM4レビュー】神とゴミのハーフ&ハーフ

こんな人におすすめ SONYのフラグシップモデル「SONY WF-1000XM4」ってどれくらい性 ...

完全ワイヤレスイヤホン(TWS)

2022/8/19

【Nothing ear (1)レビュー】ライトな完成度、アップデートに期待

こんな人にオススメ 完全ワイヤレスイヤホン(TWS)でスケルトンボディ ...

Pythonを学びたいけど独学できる時間なんてない人へのすゝめ

執筆者は大学の研究室・大学院にて独学でPythonを習得した。

でも社会人になったら独学で行うには時間も体力もなくて大変だ。

時間がない社会人だからこそプロの教えを乞うのが効率的。

ここでは色んなタイプに合ったプログラミングスクールの紹介をする。

  • この記事を書いた人

メガネ

ベンチャー企業のWebエンジニア駆け出し。独学のPythonで天文学系の大学院を修了→新卒を1.5年で辞める→転職→今に至る。
常時金欠のガジェット好きでM1 MacBook Pro x Galaxy S22 Ultraの狂人。
人見知りで根暗だったけど、人生楽しもうと思って良い方向に狂う→人生が楽しい

ガジェットのレビューとPythonコードを記事にしています。ぜひ楽しんでください🦊
自己紹介と半生→変わって楽しいの繰り返し

-plt
-, ,