カテゴリー

当サイトはアフィリエイトプログラムによる収益を得ています〈景品表示法に基づく表記です)

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のグラフ作成テンプレート

続きを見る

プロット用の関数

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の関数化

続きを見る

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

まずは結論の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のグラフ作成テンプレート

続きを見る

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

続きを見る

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

続きを見る

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

続きを見る

ガジェット

2023/9/18

【デスクツアー2022下半期】モノは少なく、でも効率的に Desk Updating #0

今回はガジェットブロガーなのにデスク環境を構築していない執筆者の ...

ライフハック

2023/9/16

【Audible vs YouTube Premium】耳で聴く音声学習コンテンツを比較

ワイヤレスイヤホンが普及し耳で学習することへのハードルが格段に下 ...

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

2023/9/18

【SENNHEISER MOMENTUM True Wireless 3レビュー】全てが整ったイヤホン

今回は高音質・高機能なSENNHEISERのフラグシップ完全ワイヤレスイヤホン「SENNH ...

ライフハック

2023/3/11

【YouTube Premiumとは】メリットしかないから全員入れ

今回はYouTube Premiumを実際に使ってみてどうなのか、どんなメリット/デメリット ...

マウス

2023/8/17

【Logicool MX ERGOレビュー】疲れない作業効率重視トラックボールマウス

こんな人におすすめ トラックボールマウスの王道Logicool MX ERGOが気になるけどऩ ...

ベストバイ

2023/9/18

【ベストバイ2022】今年買って良かったモノのトップ10

2022年ベストバイ この1年を振り返って執筆者は何を買ったのか。ガジェッ& ...

スマホ

2023/1/15

【楽天モバイル×povo2.0の併用】月1,000円の保険付きデュアルSIM運用

こんな人におすすめ 楽天モバイルとpovo2.0のデュアルSIM運用って実際のとこ ...

マウス

2023/9/16

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

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

macOSアプリケーション

2022/9/30

【Chrome拡張機能】便利で効率的に作業できるおすすめの拡張機能を18個紹介する

こんな人におすすめ Chromeの拡張機能を入れたいけど、調べても同じような ...

macOSアプリケーション

2023/5/3

【Automator活用術】Macで生産性を上げる作業の自動化術

今回はMacに標準でインストールされているアプリ「Automator」を使ってできる ...

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

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

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

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

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

  • この記事を書いた人

メガネ

Webエンジニア駆け出し。独学のPythonで天文学系の大学院を修了。常時金欠のガジェット好きでM2 Pro MacBook Pro(30万円) x Galaxy S22 Ultra(17万円)使いの狂人。自己紹介と半生→変わって楽しいの繰り返しレビュー依頼など→お問い合わせ運営者情報、TwitterX@m_ten_pa、 YouTube@megatenpa、 Threads@megatenpa

-plt
-, ,