Python
matplotlib

Matplotlibで綺麗な論文用のグラフを作る


はじめに

論文をここ数ヶ月間書いていました。。

おかげさまでいろいろ手がつかず、モデルベース強化学習系の勉強もしたかったのですが

とにかく大変でした。さくさくかけるドクターの方々に尊敬しかありません。

研究をまとめていたので、勉強的な副産物はありませんが、論文をかくうえでとにかく綺麗にグラフを書きたかったので

そのまとめです。

同じく論文を書いている(特に研究室の後輩)方の少しもの手助けになればと思います。


環境


  • ubuntu 16.04

  • matplotlib 3.0.2

  • python 3.5 or more

なお、以降は、よくあるように

import matplotlib.pyplot as plt

でimportしていることとします。


下準備


Fontのインストール

matplotlibで論文を書くときに最初につまずくのがここです。つまずかない方は次へ

なぜか、Times New Romanが使えません

いろんなサイトに載っているように

plt.rcParams['font.family'] = 'Times New Roman' #全体のフォントを設定

とうっても次のように、

文字が太くなってしまうという現象が発生します

image.png

そしてこれはなぜかなおりません

なので、一回

import matplotlib.font_manager as fon

del fon.weight_dict['roman']
matplotlib.font_manager._rebuild()

とうってください。

これでフォントのその他の設定がリセットされて、Times New Romanが使えるようになります。

なお、これでもうまく行かない場合は

http://kenbo.hatenablog.com/entry/2018/11/28/111639 を参考に

$ sudo apt install msttcorefonts -qq

$ rm ~/.cache/matplotlib -rf

を行ってみてください。ぽちぽちボタンを押していくとインストールできるかと思います。

途中でマウスが使えなくなっても十字キーで選べばいいので落ち着いてやってみてください


画像の保存について

matplotlibは

plt.show()

でfigureを出力することができ、かつ、保存ボタンもあるのでそれで保存したくなりますが、

それだと、その時の画面のサイズ等々に依存されて保存されてかなり面倒です。

なので基本的には

fig.save_fig('test.png', bbox_inches="tight", pad_inches=0.05) # 'test.png'はpath+保存名です、figはplt.figureのオブジェクトです

で保存するようにしましょう。

なお、保存するときの, bbox_inches="tight", pad_inches=0.05は、グラフをどうやって保存するかです。

matplotlib特有の謎の余白をpad_inchesで余白を狭くできます


rcParamsで設定

matplotlibは先ほど書いたように

plt.rcParams['font.family'] = 'Times New Roman' # Fonts

と行うと、すべてのデフォルトの設定を変更することができます。

デフォルトの設定を変更するだけなので、個別のfigureでは別の設定にする

とかは全然できます。この記事内はrcParamsで基本的には設定してますが状況に応じて

各figureの引数をいじってください。

公式docs

https://matplotlib.org/users/dflt_style_changes.html

公式Documentationを探せばいろいろ例が出ていて

確かにわかるのですが、正直これを見てね!ではまとめでもなんでもないので

よく使うやつを次にまとめます。

基本的にデフォルトのグラフを次のようにし、このデフォルトのグラフをいじるとどうなるのかを書いています。

それぞれの項目を適用した場合と比較してみてください。(画像がおおきなると邪魔なので基本的には50%で表示しています。)

デフォルトのグラフの作成プログラムは下記です。

プログラムはこれに足していくものとします。

def main():

# make sample date
N = 100
x = np.linspace(0, np.pi * 2, N)
y = np.sin(x)

# plot
fig = plt.figure()
fig_1 = fig.add_subplot(111)
fig_1.plot(x, y, label="test")

fig_1.set_xlabel("x")
fig_1.set_ylabel("y")
fig_1.legend()
plt.show()

# save
fig.savefig('test.png', bbox_inches="tight", pad_inches=0.05)

if __name__ == "__main__":
main()


文字をかっこ良く

なので次のように書いてみます

plt.rcParams['font.family'] = 'Times New Roman' # font familyの設定

plt.rcParams['mathtext.fontset'] = 'stix' # math fontの設定
plt.rcParams["font.size"] = 15 # 全体のフォントサイズが変更されます。
plt.rcParams['xtick.labelsize'] = 9 # 軸だけ変更されます。
plt.rcParams['ytick.labelsize'] = 24 # 軸だけ変更されます

とします。

すると、

になります。

綺麗ですね。もちろん、他のfont famimlyも指定できますので、'sans-serif'とかもあります

数式用のフォントも、'cm'や、'stixsans'があります。


  • 数式はtex形式で

r+$マークで囲いましょう。

https://matplotlib.org/users/mathtext.html が参考になります。

fig_1.set_xlabel(r"$x$")

fig_1.set_ylabel(r"$y$")

しっかり斜体になってくれました。

texの記法で書けますのでやりたい放題できます。


最終的なプログラム

plt.rcParams['font.family'] = 'Times New Roman' # font familyの設定

plt.rcParams['mathtext.fontset'] = 'stix' # math fontの設定
plt.rcParams["font.size"] = 15 # 全体のフォントサイズが変更されます。
plt.rcParams['xtick.labelsize'] = 9 # 軸だけ変更されます。
plt.rcParams['ytick.labelsize'] = 24 # 軸だけ変更されます

# make sample date
N = 100
x = np.linspace(0, np.pi * 2, N)
y = np.sin(x)

# plot
fig = plt.figure()
fig_1 = fig.add_subplot(111)
fig_1.plot(x, y, label="test")

fig_1.set_xlabel(r"$x$")
fig_1.set_ylabel(r"$y$")

fig_1.legend()

# save
fig.savefig('test_2.png', bbox_inches="tight", pad_inches=0.05)


軸をかっこ良く


  • 軸が外側に向いているのが気持ち悪い気がするので内側に

  • 軸の太さだけ太くしたい

  • 軸によるグリッド線を出力したい

上記のプログラムに次のコードを追加してください。

plt.rcParams['xtick.direction'] = 'in' # x axis in

plt.rcParams['ytick.direction'] = 'in' # y axis in
plt.rcParams['axes.linewidth'] = 1.0 # axis line width
plt.rcParams['axes.grid'] = True # make grid

また、よくある軸を共有するやつは以下の感じでできます。


  • 軸を共有させたい

other_fig_1 = fig_1.twinx()

other_fig_1.plot(x, y_2, label="test_2") # y_2は適当に作成したものです
other_fig_1.set_ylabel(r"$y_{2}$")

さらに言うと

字が入らないけど省略しないで。。。。みたいな時


  • 軸の数字を斜めにする

もできます。

以下コードを追加してください。

fig_1.set_xticklabels(np.round(x, 2), rotation=30)


  • 小数点の設定

さて上記のfigureで若干トリッキーに軸の小数点も同時に直してますが

基本的には、

xticksで調整するのが基本だと思っています。

# fig_1.set_xticklabels(np.round(x, 2), rotation=30)

fig_1.set_xticks([0, 1, 2])

と入れてみてください。

こんな感じになるので、自分が軸を入れたいところに数字を打てばよいですね!

fig_1.set_xticks([0.1, 1.0, 2.0])とかにすると小数点第一までが表示されます。


最終的なプログラム

plt.rcParams['font.family'] = 'Times New Roman' # font familyの設定

plt.rcParams['mathtext.fontset'] = 'stix' # math fontの設定
plt.rcParams["font.size"] = 15 # 全体のフォントサイズが変更されます。
plt.rcParams['xtick.labelsize'] = 9 # 軸だけ変更されます。
plt.rcParams['ytick.labelsize'] = 24 # 軸だけ変更されます
plt.rcParams['xtick.direction'] = 'in' # x axis in
plt.rcParams['ytick.direction'] = 'in' # y axis in
plt.rcParams['axes.linewidth'] = 1.0 # axis line width
plt.rcParams['axes.grid'] = True # make grid

# make sample date
N = 100
x = np.linspace(0, np.pi * 2, N)
y = np.sin(x)
y_2 = 500. * np.cos(x)

# plot
fig = plt.figure()
fig_1 = fig.add_subplot(111)
fig_1.plot(x, y, label="test")

fig_1.set_xlabel(r"$x$")
fig_1.set_ylabel(r"$y$")
# fig_1.set_xticklabels(np.round(x, 2), rotation=30)
fig_1.set_xticks([0.1, 1.0, 2.0])
other_fig_1 = fig_1.twinx()
other_fig_1.plot(x, y_2, label="test_2")
other_fig_1.set_ylabel(r"$y_{2}$")

# 全体のfigへlegendを変更
fig.legend()

# save
fig.savefig('test_4.png', bbox_inches="tight", pad_inches=0.05)


凡例をかっこ良く

ここからは、先ほどの2軸共有はなしでいきます。

matplotlibの凡例は便利な反面、なぜか丸い四角になっており、

あこれ、matplotlibだなと一発でわかる仕様になっています。

参考は以下です。

https://matplotlib.org/api/legend_api.html

ただ、凡例に関してはとってもたくさんの指定があるので

ここでは僕が良く使用するものを紹介します。


  • 凡例を四角にする

  • 凡例の周りを黒く囲う

  • 凡例の線の長さをいじる

  • 水平方向の凡例同士の距離をいじる

  • 凡例の線と文字の距離の長さをいじる

plt.rcParams["legend.fancybox"] = False # 丸角

plt.rcParams["legend.framealpha"] = 1 # 透明度の指定、0で塗りつぶしなし
plt.rcParams["legend.edgecolor"] = 'black' # edgeの色を変更
plt.rcParams["legend.handlelength"] = 1 # 凡例の線の長さを調節
plt.rcParams["legend.labelspacing"] = 5. # 垂直方向(縦)の距離の各凡例の距離
plt.rcParams["legend.handletextpad"] = 3. # 凡例の線と文字の距離の長さ
plt.rcParams["legend.markerscale"] = 2 # 点がある場合のmarker scale

としてください。

これで凡例がきれいになります。

位置は基本的にmatplotlibがいい感じの位置においてくれる仕様ですが、

論文だと右にいったり、左に凡例があったりすると見栄えが良くないので

位置をいじります。

基本的には

fig_1.legend(loc="2")

"""
| 'best' | 0 |
| 'upper right' | 1 |
| 'upper left' | 2 |
| 'lower left' | 3 |
| 'lower right' | 4 |
| 'right' | 5 |
| 'center left' | 6 |
| 'center right' | 7 |
| 'lower center' | 8 |
| 'upper center' | 9 |
| 'center' | 10 |
"""

という感じで指定ができるわけです。これと、凡例ハンドラー同士の距離等をさきほどの設定によっていじることで、うまく行くわけです。

しかし、グラフの中にあるとどうしてもかぶってしまう時があります

その時は、外に出して横一列にしてしまうほうが見栄えが良さそうです。


  • 凡例を横一列にしたい

外に出すのは結構トリッキーです。

まず、次のサイトで

bbox_to_anchorの使い方を理解してください。

これによって、どこにどのサイズの凡例の箱を作るのかを指定することができます。

例えば、

fig_1.legend(bbox_to_anchor=(0., 1.02, 1., 0.102), loc=3)

としたとすると、

0で1.02の位置に、1×0.102の箱を作って、そのloc=3、左下に凡例の箱をおいているイメージになります。

これはadd_subplotした、axisに適用しているので、0, 1.02で外に出るわけですね。

(全体のfigureに対してlegendする場合は、全体のfigureに対しての位置になります。)

ですがこれでは横長にはなりません。

そこで、

fig_1.legend(ncol=2, bbox_to_anchor=(0., 1.02, 1., 0.102), loc=3)

としましょう

このn_colは、何列で凡例のハンドルを書きますか?ってものになります

なので今回は2!とすれば、2つがその中に入ってくれます。増えたら増やせばいいです。

なんか左が空いてしまっているのがきになりますね

縁に合わせたほうが良さそうです。


  • 凡例のはじと、グラフのはじを合わせる

plt.rcParams["legend.borderaxespad"] = 0.

を追加してください

もちろん

fig_1.legend(ncol=2, borderaxespad=0., bbox_to_anchor=(0., 1.025, 1., 0.102), loc=3)

でもオーケーです。

bbox_to_anchorの値を少し変えていますが、borderaxespadを0にすると、凡例とグラフがくっついてしまうのでそれを避けるために行っています。

なお、expandというモードを設定するとaxisの幅に自動的に合わせてくれますが

凡例が少ない場合はなんか横に長いやつになってしまうので。。。

さっきのやつのほうが良さそうです

fig_1.legend(ncol=2, bbox_to_anchor=(0., 1.025, 1., 0.102), loc=3, mode='expand')


最終的なプログラム

plt.rcParams['font.family'] = 'Times New Roman' # font familyの設定

plt.rcParams['mathtext.fontset'] = 'stix' # math fontの設定
plt.rcParams["font.size"] = 15 # 全体のフォントサイズが変更されます。
plt.rcParams['xtick.labelsize'] = 9 # 軸だけ変更されます。
plt.rcParams['ytick.labelsize'] = 24 # 軸だけ変更されます
plt.rcParams['xtick.direction'] = 'in' # x axis in
plt.rcParams['ytick.direction'] = 'in' # y axis in
plt.rcParams['axes.linewidth'] = 1.0 # axis line width
plt.rcParams['axes.grid'] = True # make grid
plt.rcParams["legend.fancybox"] = False # 丸角
plt.rcParams["legend.framealpha"] = 1 # 透明度の指定、0で塗りつぶしなし
plt.rcParams["legend.edgecolor"] = 'black' # edgeの色を変更
plt.rcParams["legend.handlelength"] = 1 # 凡例の線の長さを調節
plt.rcParams["legend.labelspacing"] = 5. # 垂直(縦)方向の距離の各凡例の距離
plt.rcParams["legend.handletextpad"] = 3. # 凡例の線と文字の距離の長さ
plt.rcParams["legend.markerscale"] = 2 # 点がある場合のmarker scale
plt.rcParams["legend.borderaxespad"] = 0. # 凡例の端とグラフの端を合わせる

N = 100
x = np.linspace(0, np.pi * 2, N)
y = np.sin(x)
y_2 = np.cos(x)

# plot
fig = plt.figure()
fig_1 = fig.add_subplot(111)
fig_1.plot(x, y, label="test")
fig_1.plot(x, y_2, label="test_2")

fig_1.set_xlabel(r"$x$")
fig_1.set_ylabel(r"$y$")
# fig_1.set_xticklabels(np.round(x, 2), rotation=30)
fig_1.set_xticks([0.1, 1.0, 2.0])

fig_1.legend(ncol=2, bbox_to_anchor=(0., 1.025, 1., 0.102), loc=3)

# save
fig.savefig('test_5.png', bbox_inches="tight", pad_inches=0.05)


点をかっこ良く

これはoptionalな形のものですが、matplotlibの点付きのグラフを縁が黒い形で綺麗にします。


  • グラフ途中の点を出力したい

  • 黒丸で出力したい

fig_1.plot(x, y, marker='.', markeredgewidth=1., markeredgecolor='k', color="y", label="test")

fig_1.plot(x, y_2,marker='.', markeredgewidth=1., markeredgecolor='k', color="g", label="test_2")

圧倒的に綺麗ですね!!


最終的なプログラム

plt.rcParams['font.family'] = 'Times New Roman' # font familyの設定

plt.rcParams['mathtext.fontset'] = 'stix' # math fontの設定
plt.rcParams["font.size"] = 15 # 全体のフォントサイズが変更されます。
plt.rcParams['xtick.labelsize'] = 9 # 軸だけ変更されます。
plt.rcParams['ytick.labelsize'] = 24 # 軸だけ変更されます
plt.rcParams['xtick.direction'] = 'in' # x axis in
plt.rcParams['ytick.direction'] = 'in' # y axis in
plt.rcParams['axes.linewidth'] = 1.0 # axis line width
plt.rcParams['axes.grid'] = True # make grid
plt.rcParams["legend.fancybox"] = False # 丸角
plt.rcParams["legend.framealpha"] = 1 # 透明度の指定、0で塗りつぶしなし
plt.rcParams["legend.edgecolor"] = 'black' # edgeの色を変更
plt.rcParams["legend.handlelength"] = 1 # 凡例の線の長さを調節
plt.rcParams["legend.labelspacing"] = 5. # 垂直方向の距離の各凡例の距離
plt.rcParams["legend.handletextpad"] = 3. # 凡例の線と文字の距離の長さ
plt.rcParams["legend.markerscale"] = 2 # 点がある場合のmarker scale
plt.rcParams["legend.borderaxespad"] = 0. # 凡例の端とグラフの端を合わせる

N = 10
x = np.linspace(0, np.pi * 2, N)
y = np.sin(x)
y_2 = np.cos(x)

# plot
fig = plt.figure()
fig_1 = fig.add_subplot(111)
fig_1.plot(x, y, marker='.', markersize=10, markeredgewidth=1., markeredgecolor='k', color="y", label="test")
fig_1.plot(x, y_2, marker='.', markersize=10, markeredgewidth=1., markeredgecolor='k', color="g", label="test_2")

fig_1.set_xlabel(r"$x$")
fig_1.set_ylabel(r"$y$")
# fig_1.set_xticklabels(np.round(x, 2), rotation=30)
fig_1.set_xticks([0.1, 1.0, 2.0])

fig_1.legend(ncol=2, bbox_to_anchor=(0., 1.025, 1., 0.102), loc=3)

# save
fig.savefig('test_7.png', bbox_inches="tight", pad_inches=0.05)


texへの利用


fileの形

texへ使用することを考えると、

epsかpdfでほしいわけですが、嬉しいことに保存の際の拡張子を指定すればすぐに

pdfとepsになります。

# save

fig.savefig('test_7.eps', bbox_inches="tight", pad_inches=0.05)

僕個人としてはepsのほうが、pdflatexで作成した時に、pdfにコンバートされてエラーも少ない気がするので、そちらがおすすめです。

もちろんpdfでもいいと思いますが(なぜか、adobeで開くと、matplotlibで書きだしたやつがpdfはエラーがありますっていつも出るので)


画質と画像サイズについて

matplotlibの画像サイズはinch表記です。

もちろん、rcParamsで、変更することができます。

plt.rcParams['figure.figsize'] = (3.5, 3.5) # figure size in inch, 横×縦

また、画質についてはdpiで指定できます。

dpiは300以上はあった方が良いそうなので、300で指定しておきましょう。

一点、dpiを上げると、plt.show()でのグラフが巨大になりますが、それはそこの話だけで、保存されたfigsizeにはきいてこないと思いますのでご注意を

plt.rcParams['figure.dpi'] = 300


最後に

matplotlibでグラフを綺麗な形で出力できるようにしておくと

あとはtexのほうにそのままなげればいいのでかなり楽になります。

文字を書き込むこともできますのでそのへんもまた追記していければと思います。