よし呟こう

思ったことを呟きます。

Matplotlibでレポート用グラフ作成~よく使うもの~

特定の点・範囲の強調、グラフの中へのグラフの挿入、拡大図の添付、複軸化、(特に)ヒストグラム、軸目盛の調整......

Excelでこのような細かい要望に合ったグラフを作るのはさすがに大変。多少は自由度が高いだろうとPython+matplotlibでグラフを書いています。ところが、今度はいちいち細かいことが多くて覚えられない!(毎回、前回使ったプログラムを探すところから始まる...)。そしてグラフの見た目を変えるのが超大変!

以下では私が今まで使ってきたもので便利だなあと思いながら、覚えられなくて毎回前回使ったプログラムを探すこととなっているものを載せました。要するにメモです。適宜追加していきます。

以下のコードはいつものテンプレート(スタイル関連を除く)なのでこれを基準にメモしました。

from matplotlib import pyplot as plt
from matplotlib import font_manager as fm
from matplotlib import rcParams
from matplotlib import patches
from matplotlib.ticker import ScalarFormatter

fontsize = 25
fig, ax1 = plt.subplots(figsize=(8, 6), constrained_layout=True)


▼環境

 
 
▼コンテンツ

  
    

【便利】ダミープロット

例えばax.plot()に回帰曲線、ax.scatter()に測定点を書いた場合で凡例を表示させると、普通はax.plot()由来の線か、ax.scatter()由来のシンボル(丸とか、三角とか)しか表示されません。ax.plot()のキーワードでmarkerキーワードを使うと、この場合一般の回帰曲線を考えているので、どえらいことになるのは簡単に想像できます。回帰曲線上に測定点のシンボルを載せたような凡例が欲しい!そういう時はダミープロットを使います。

ax1.plot(回帰曲線のx軸データ, 回帰曲線のy軸データ, color)
ax1.scatter(測定点のx軸データ, 測定点のy軸データ, color, marker="o")
ax1.plot([], [], color, marker="o", label="ダミープロット!")

ax.plot()ax.scatter()両方のスタイルに合わせたダミープロットを作ります。markercolorlinestyleを統一する必要があります。
 
 

単位変換した軸を一緒に描画したい

ax.secondary_yaxis([単位を変換した軸をどこに表示するか], functions=(元の単位を目的の単位に変換する関数, 目的の単位から元の単位に変換する関数))を使います。例えば、単位がラジアンで与えられるデータがあって、これをy軸に表示しているとしましょう。このデータに対応するように度数での表示を行いたいとします。このときは次のようにします。

def rad2degree(x):
    return np.rad2deg(x)

def degree2rad(x):
    return np.deg2rad(x)

secondary_ax = ax1.secondary_yaxis(
    "right", functions=(rad2degree, degree2rad))

単位を変換した軸を一緒に載せておくと、考察が捗ることがあるので使っています。ax.secondary_yaxis()はx軸版もありax.secondary_xaxis()で同様のことができます。こちらもよく使います。

ちなみに、逆関数を定義できない領域があるときは事前にax1.set_ylim()等で制限をかけておかないとエラーが出ます。例えば、np.arccos()の定義域は-1から1ですが、そのままプロットすると-2とかまで計算することになって、NaNやInfのデータが生じ、エラーとなります。故に逆関数の定義域については慎重に検討して、すべての実数をとれるわけではないときは、その範囲に合うように表示範囲を狭め、clip_onなどで端の処理をする必要があります。
 
 

画像化と保存、トリミングの手間を省いて、解像度も上げる

やり方はplt.savefigにてpad_inchesキーワードで余白の大きさを指定(小さいほど余白が小さくなります)、dpiキーワードで解像度を指定(大きいほど解像度がよくなります)するだけです。以下の例では、matplotlibではしばしばあるラベルや軸が見切れてしまう現象を回避するためのキーワードのbbox_inchesを入れています。私はpad_inches=0.11程度で満足していますが、もっと小さい値でもいいかもしれません。

plt.savefig("filename.jpg",
            bbox_inches="tight",
            pad_inches=0.11, dpi=300)

ちなみに.pdf.svg(ベクター画像)にも対応しているようです。
 
 

水平位置と垂直位置

図中にテキストを書き込むときに水平位置と垂直位置を指定すると便利がよいことが多いです。簡単に言うと、テキストの書かれた四角い箱:テキストボックスを考えたときに、ボックスのどの値を指定した座標に持ってくるか?を決定するということですね。

ax1.text(x, y, text, fontsize,
         horizontalalignment="left",
         verticalalignment="bottom")

例えばこの場合だと、水平方向をhorizontalalignmentleft、垂直方向をverticalalignmentbottomとしているので、テキストボックスの左下の座標が引数(x, y)で指定した座標になるように描画されます。

水平位置と垂直位置。horizontalalignmentで水平方向を、verticalalignmentで垂直方向を指定。
水平位置と垂直位置。horizontalalignmentで水平方向(left(左)、center(中央)、right(右))を、verticalalignmentで垂直方向(top(上部)、center(中央)、bottom(下部))を指定。デフォルトではどちらもcenterだったはず。

 
 

複数軸グラフ

変数が同じで、しかし全く別のグラフを同時に描画したいときがあります。例えば、吸光スペクトルと蛍光スペクトルは波長という変数は同じですが、強度は任意単位で別物なので、別々に描画します。同じ変数なので1つのグラフに横軸だけ同じにして描画できるとわかりやすくて良いです。この時はax.twinx()を使います。

ただしこれだけではax.legendは元の軸の値しか描画しません(ax1を作って、ax2=ax1.twinx()としてax1.legend()とすると、ax1に描画したもののラベルだけが表示されます)。そこで新しい軸のラベルもまとめて描画するように、次の例でいうところの最後の三行を加えます。

fig, ax1 = plt.subplots(figsize=(8, 6))
ax1.plot(波長, 吸収スペクトルの強度)

ax2 = ax1.twinx()
ax2.plot(波長, 蛍光スペクトルの強度)

handler1, label1 = ax1.get_legend_handles_labels()
handler2, label2 = ax2.get_legend_handles_labels()
ax1.legend(handler1 + handler2, label1 + label2)

ax.get_legend_handles_labels()でラベルを表示するためのhandleと対応するlabelが得られますが、これらを足し合わせてあげるだけです。足し合わせる順番によって、表示される順番が変わります。
 
 

階級分けした後のヒストグラム

例えばすでに4階級に分けた離散データを格納した一次元配列dataを得られたとしましょう。その時は次のようにすればよいです。

ax1.hist(data, bins=4)

連続データの場合も似た感じなので略。ヒストグラムのためにExcelではなくpython + matplotlibを使っているまであります。
 
 

Texを使いたい

ラベルに記号や数式を入れたいときがあります。もちろん「γ」のようにあらかじめ存在している特殊記号を使うのもありですが、$\gamma$のようにできると、つまりTexが使えると非常に便利です。例えば単位の上付き文字をいちいち座標指定するのは馬鹿らしいので$\mathrm{m^2}$のようにできればなんと便利なことか!(Excelではいちいち右クリックが必要でめんどくさい)。これをやるには当該文字列を$でラップします:

ax1.text(x, y, "$\gamma$")

ただ、これだけでは$も含めた文字列だとみなされるのがほとんどなので、さらにrを先頭につけてr構文にしておきます:

ax1.text(x, y, r"$\gamma$")

ところで数式を改行するときはどうすればいいでしょうか。例えば2行に改行するときは、次に示すように2つの変数を用意するのが後で見たときにわかりやすいと思います:

text1 = r"$y = f(x; t)$"
text2 = r"$R^2 = 1.000$"
ax1.text(x, y, text1 + "\n" + text2)

では変数を文字列に組み込みたいときはどうしましょうか。次に示す方法は、直接変数を組み込む方法です。

text1 = r"$y = f(x; t=" + str(100) + r")$"
text2 = r"$R^2 = 1.000$"
ax1.text(x, y, text1 + "\n" + text2)

Texではことあるごとに{}が出てくるので、フォーマット構文やf構文が使えないと思います(使えましたっけ)。したがって直接結合する方法が一番楽なのではないかなと思います。管理がめんどくさくなりますが、確実です。
 
 

スケール関連

わかりやすさを考えて単位を変えることがありますが、単位を変えずに表示したいときがあります。例えば1\,\mathrm{m}10^9\,\mathrm{nm}ですが、これをそのままmatplotlibで表示すると「10e9」のように表示されて、わかりにくい...というか、特定の分野の人が好きそうな表示になります。できれば10のなんちゃら乗として表示したい。そこでmatplotlib.ticker.ScalarFormatterを使います。

ax1.yaxis.set_major_formatter(ScalarFormatter(useMathText=True))

また、対数スケールは次のようにして実現します。例えばy軸を対数スケールにするには

ax1.set_yscale("log")

でよいです。
 
 

描画順番と重なり

matplotlibの描画順番はよくわかりません...。先に描画したはずのものが背面に向かっていたり。。。それを調整できるのがzorderキーワードです。次の例ではラベル"first"のデータの方がラベル"second"のデータよりも上側に(画面手前側に)描画されます。zorderの値が大きいほど画面手前側に描画され、小さいほど画面反対側の方向に描画されます。負の値をとることもできるので、例えばx軸やy軸などはzorder=-1などとしておいてもよいかもしれません。

ax1.plot(x1, y1, label="first", zorder=100)
ax1.plot(x2, y2, label="second", zorder=1)

ところで、matplotlibのfigureの枠に被せたいのだけど...と思われる方もいるかもしれません。どういうことかというと、例えば(0, 0)にデータ点があって、描画範囲を0以上に設定すると、0番目の点がグラフ領域の外黒枠線の下敷きになってしまうのです。これを回避するには、clip_onキーワードを使います。clip_on=Falseで、グラフの枠や余白によって消される部分が消されないようになります(逆に言えば範囲指定を誤ると大変なことになります)。

ax1.scatter(x1, y1, label="first", zorder=100, clip_on=False)
ax1.plot(x2, y2, label="second", zorder=1, clip_on=False)

 
 

軸のメモリやラベルを削除したい

例えば任意単位のもので、メモリが不要!ラベルもいらない!といった要望があったとします。そういうときは次のようにします。

ax1.tick_params(
    labelbottom=False, labelleft=True,
    labelright=False, labeltop=False,
    bottom=False, left=True,
    right=False, top=False
)

この例では、左の軸のメモリとラベル以外のすべてのメモリとラベルが削除されます(表示されません)。