English version available on dev.to
はじめに
matplotlibで作ったグラフの細かい調整は大変です。何をどういじったらいいのかを調べるのにアホみたいに時間がかかることがあります1。「何を」の部分の名前さえわからないこともあります。解決の糸口を掴んだ後も希望通りの見た目を実現するまでの最後のアレンジに苦労することが多いです2。これらの問題は__matplotlibのグラフがどういう要素で構成されていて、それらに対してどういうことができるかを知る__ことでいくらか改善されます。私はひたすらStack Overflowの回答を読むことでいろんなつまづきを時間をかけて乗り越えてきましたが、最近になってようやく公式チュートリアルにこの苦労を回避できたはずのヒントが書いてあることに気づきました。初期にざっと目を通したのですが「なるほど、よくわからん」と判断して読み込まなかったArtist
に関する簡単な説明です。この記事で、新しいユーザーが私の経験したような無駄な苦労を回避できれば、あるいはすでにある程度なんとなくわかってきたユーザーが理解を深めてもらえたら嬉しいです。
見た目調整の個別相談を始めました。記事の最後にリンクがあります。
この記事の目的と内容
「こうしたい時はこうする」といった細かいノウハウではなく、いわゆる釣りの仕方(検索の際のキーワード選び)や釣った後のさばき方一般(検索で見つけた近い解法を自分向けにアレンジする際のヒント)に役立つアレコレについて述べます。ウェブに散らばる無数の断片的で対症療法的なメモ、tips、処方箋、レシピの内容がクリアになると思います。matplotlibベースであるSeabornやPandasのプロット機能を使っている人にとっても、グラフの細かな調整をする際に役立つはずです。
本記事の大部分はmatplotlib公式チュートリアルのArtist tutorialとUsage Guide(執筆時バージョンは2.1.1)を簡単に日本語でまとめたものです。Artist
に関して言及した日本語の記事は
などがありましたが、あまりわかりやすいものではなかったのでもう少し噛み砕いて書いてみました。
こんな人向け
- matplotlib入門 - りんごがでている などをざっと読んで手元のデータをプロットできるようになったけれど、報告書や論文用の図を作るに当たって体裁を整えたくなってきた。その都度ググってなんとか自分のやりたいことを解決してくれるStack Overflowなどの回答を探し出せているが、自分がやりたいことによく似てるけど少し違う回答しか見つからず、かゆいところの手前までしか届かない孫の手にむきーっとなっている。
- この、なに、軸?枠?の太さを変えたいんだけど__名前がわからなくてやり方を探すのに苦労したことがある。あるいはあきらめた。__
- 言われるがままにやってみたら期待通りの図になったけど、何をやっているか理解しきれていなくて気持ち悪いし応用が利かない。
- 複数のやり方があるっぽいけど、なんでそんなことになっているのかよくわからないしどれを採用すればいいのかわからない。
釣りはどうでもいいからかゆいところまで届く孫の手が欲しい
あなたのかゆいところがどこかにもよりますが、Qiitaにはこういった孫の手がありました。
また、英語かつver. 1.3ベースの情報ですが、こんなのもありました。
ただし、この後の内容を読むとこれらの孫の手も伸び縮みさせたり曲げられるようになるはずです。
準備
Python 3.6、matplotlib 2.1.1を使っています。
%matplotlib inline
import matplotlib.pyplot as plt
import numpy as np
Jupyter notebookのinlineコマンドを使っていることを前提にしているので、この後の例ではplt.show()
は省略されています。
matplotlibにはグラフを作る際の二つの流儀がある
Artist
の話の前に、新しいユーザーが絶対に知っておくべきplt.plot
とax.plot
の違いについて述べます。公式チュートリアルでも A note on the Object-Oriented API vs Pyplot や Coding Styles で言及されていますが、matplotlibでグラフを作るには二つの流儀(インターフェース)があります。公式ドキュメントを含めてネット上に大量にあるmatplotlibのコードにはこの二つが混在していますが、これらの違いについて明記してる例はあまり多くないように思えます。また、意味もなく二つを混ぜて使っている例も多く、これが多くの初心者のつまづきの原因になっていると思います。初めて"matplotlib プロット 方法"で検索した時のことを思い出してください。__「plt.なんとか
で全部済んでる例があるのになんでax.plot
とか変なのがいろいろ出てくる例もあるの?ていうかこのax
使ってなくない?バカなの?」__と思いませんでしたか?私は思いました。ここではそれぞれの流儀の違いについて述べるだけです。具体的な使い方を知りたい方は以下を参照してください。
- Tutorials > Introductory > The Lifecycle of a plot
- Tutorials > Introductory > Pyplot tutorial
- matplotlib入門 - りんごがでている
オブジェクト指向インターフェース
fig, ax = plt.subplots()
などの後にax.plot
などを使う流儀です。fig
やax
はこの記事で説明するArtist
と呼ばれるオブジェクトの一種です。一番シンプルな例はこんな感じです。
fig, ax = plt.subplots()
ax.plot(x,y)
fig = plt.figure()
ax = fig.add_subplot(1,1,1)
ax.plot(x, y)
fig = plt.gcf()
やax = plt.gca()
もありますが、これらは主にPyplotインターフェースからオブジェクト指向インターフェースに切り替える時に使います。前述した「二つの流儀を混ぜて使っている例」はこれらを無自覚に使っているコードのことです。ユーザー自身に二つの流儀を切り替えているという自覚があるなら問題ないのですが、二つの流儀について認識していない初心者が読むと余計な疑問が生まれてしまいます。plt.subplots
やfig.add_subplot
を使って最初からオブジェクト指向インターフェースを使うことを勧めます。
Pyplotインターフェース
plt.なんとか
で全部済ませる流儀です。matplotlibの元となったMATLABを模した流儀だそうです。オブジェクト指向方式のように何を操作の対象にするか明示的に指定しなくとも、current figureやcurrent axes3と呼ばれるオブジェクトを自動で設定してくれます4。
# https://matplotlib.org/tutorials/introductory/pyplot.html より
def f(t):
return np.exp(-t) * np.cos(2*np.pi*t)
t1 = np.arange(0.0, 5.0, 0.1)
t2 = np.arange(0.0, 5.0, 0.02)
plt.figure(1)
plt.subplot(211)
plt.plot(t1, f(t1), 'bo', t2, f(t2), 'k')
plt.subplot(212)
plt.plot(t2, np.cos(2*np.pi*t2), 'r--')
plt.show()
Tutorials > Pyplot tutorial で作ってる図を見るとわかる通り、運良くデフォルト設定でもグラフの細かい部分の見栄えに問題ない場合はそこそこのものができます。ただ、オブジェクト指向という概念についてよく理解していない段階でこの流儀に慣れてしまうと、のちほど必ず混乱すると思います。私はしました。また、ある程度の段階までPyplotで図を作っても、いざ細かい調整をしようとすると、結局オブジェクト指向インターフェースのやり方に従うことになります。細かい調整が必要ない非常に簡素なグラフで事足りる時、あるいはちゃちゃっと可視化して何かを確認したい時には使えますが、人に見せる図を作る際は必ず微調整したい部分がでてくるので、早いうちからオブジェクト指向インターフェースに慣れたほうが良いです。
Figure
, Axes
, Axis
は階層構造になっている
やりたいことがでてくるたびにググっていると、オブジェクト指向についてよく知らなくてもなんとなくmatplotlibにはfig
, ax
などと表記される階層構造のようなものがあることがわかってくると思います。最新のドキュメントからは削除されてしまったようですが、Matplotlib 1.5.1のFAQ > Usage にあった以下の図が最低限把握しておきたいmatplotlibの階層構造を簡潔に表しています。
後ほど述べるTick
が含まれていない図ですが、階層構造の理解には十分です。この図から以下の二点が読み取れます。
-
Figure
オブジェクトにAxes
オブジェクトが属している -
Axes
オブジェクトにはAxis
オブジェクトが属している
この辺の話は日本語だと以下のページでも述べられています。
この階層構造を知っていると前述したシンプルな例で最初にやっていることの意味が理解できます5。
fig, ax = plt.subplots() # Figureオブジェクトとそれに属する一つのAxesオブジェクトを同時に作成
ax.plot(x,y)
fig = plt.figure() # Figureオブジェクトを作成
ax = fig.add_subplot(1,1,1) # figに属するAxesオブジェクトを作成
ax.plot(x, y)
試しに何もプロットせずにFigure
とAxes
のみを作って、それぞれの関係を示す属性をprint
で表示して見ます。
fig = plt.figure()
ax = fig.add_subplot(1,1,1) # 何もプロットしていないAxesでもAxisは自動的に作られる
print('fig.axes:', fig.axes)
print('ax.figure:', ax.figure)
print('ax.xaxis:', ax.xaxis)
print('ax.yaxis:', ax.yaxis)
print('ax.xaxis.axes:', ax.xaxis.axes)
print('ax.yaxis.axes:', ax.yaxis.axes)
print('ax.xaxis.figure:', ax.xaxis.figure)
print('ax.yaxis.figure:', ax.yaxis.figure)
print('fig.xaxis:', fig.xaxis)
fig.axes: [<matplotlib.axes._subplots.AxesSubplot object at 0x1167b0630>]
ax.figure: Figure(432x288)
ax.xaxis: XAxis(54.000000,36.000000)
ax.yaxis: YAxis(54.000000,36.000000)
ax.xaxis.axes: AxesSubplot(0.125,0.125;0.775x0.755)
ax.yaxis.axes: AxesSubplot(0.125,0.125;0.775x0.755)
ax.xaxis.figure: Figure(432x288)
ax.yaxis.figure: Figure(432x288)
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-21-b9f2d5d9fe09> in <module>()
9 print('ax.xaxis.figure:', ax.xaxis.figure)
10 print('ax.yaxis.figure:', ax.yaxis.figure)
---> 11 print('fig.xaxis:', fig.xaxis)
AttributeError: 'Figure' object has no attribute 'xaxis'
ここからわかるのは以下の関係です。AxesSubplot
はplt.subplots
で作った場合のAxes
オブジェクトと思って問題ありません。
-
Figure
は自分の下のAxes
を知っているけどその下のAxis
は知らない -
Axes
は自分の上のFigure
と下のAxis
を知っている -
Axis
は自分の上のAxes
とその上のFigure
を知っている -
Figure
は複数のAxes
を持てる(print
で表示されているのが[]で囲まれたリストなので) -
Axes
は一つのFigure
にしか属せない(print
で表示されているのがリストではないので) -
Axes
はXAxis
とYAxis
を一つずつしか持てない(同上) -
XAxis
とYAxis
は一つのAxes
、一つのFigure
にしか属せない(同上)
グラフに表示されているものは全てArtist
執筆時の最新版2.1.1の Tutorials > Introductory > Usage Guide には、階層構造の代わりにグラフの構成要素を細かく示した"解剖図"6が掲載されています7。これを見ると「この、なに、軸?枠?」の部分はSpinesと呼ばれていることがわかります。
データを示す線や点、x軸やy軸、描画領域を表す枠や文字などグラフに表示される全ての要素はArtist
と呼ばれます。Artist
はcontainer(容器)とprimitive(原始的なものという意味)の二種類に分類されます。先の階層構造で示したFigure
、Axes
、Axis
はcontainerに、プロットの線(Line2D
)や点(PathCollection
)あるいは文字(Text
)はprimitiveに相当します。解剖図にあるtickやlabelは名前こそ違いますが実体はLine2D
とText
オブジェクトです。これらの関係を簡単に図示すると以下のようになります。各primitiveを生成するAxes
メソッドの例を右側に示しました。
前項のサンプルコードの結果を見るとわかりますが、Axis
は実際にはXAxis
とYAxis
という名前です。Axis
の下にはさらTick
という目盛り関連の線や文字のためのcontainerがあります。containerはその名の通りprimitiveを入れる箱を持っています。このとき、containerの階層構造にあった「一つしか持てない」という制限はなく、いくつでもおなじprimitiveを持つことができます。例えば何もプロットしていないax
でLine2D
オブジェクトを入れる箱ax.lines
を見ると空リストが表示されます。ax.plot
は他の細々した設定とともにこのリストにLine2D
オブジェクトを追加していきます。
x = np.linspace(0, 2*np.pi, 100)
fig = plt.figure() # Figureを作成
ax = fig.add_subplot(1,1,1) # Axesを作成
print('ax.lines before plot:\n', ax.lines) # Axes.linesは空リスト
line1, = ax.plot(x, np.sin(x), label='1st plot') # Axes.linesにLine2Dを追加+その他の設定
print('ax.lines after 1st plot:\n', ax.lines)
line2, = ax.plot(x, np.sin(x+np.pi/8), label='2nd plot') # Axes.linesにLine2Dを追加+その他の設定
print('ax.lines after 2nd plot:\n', ax.lines)
ax.legend()
print('line1:', line1)
print('line2:', line2)
ax.lines before plot:
[]
ax.lines after 1st plot:
[<matplotlib.lines.Line2D object at 0x1171ca748>]
ax.lines after 2nd plot:
[<matplotlib.lines.Line2D object at 0x1171ca748>, <matplotlib.lines.Line2D object at 0x117430550>]
line1: Line2D(1st plot)
line2: Line2D(2nd plot)
四つのcontainerが持てるArtist
を以下にまとめます。
Figure
ほかのcontainerでも同じですがFigure
に属するArtist
はそれぞれに対応する箱に入っていると考えるとよいです。箱は属性(attribute)を呼ぶことで中身を見れます。
Figure 属性 |
内容 |
---|---|
fig.axes |
Axes オブジェクトのリスト(AxesSubplot も含む) |
fig.patch |
Figure 全体の背景となるRectangle オブジェクトの長方形一つだけ。 |
fig.images |
FigureImages のリスト (useful for raw pixel displayとあるけどよくわからない) |
fig.legends |
Figure に属するLegend オブジェクトのリスト(Axes.legends とは異なる。下の記述参照。) |
fig.lines |
Figure に属するLine2D オブジェクトのリスト (あまり使わない。Axes.lines 参照) |
fig.patches |
Figure に属するpatchのリスト(ようは図形。ほとんど使わない。Axes.patches 参照) |
fig.texts |
Figure に属するText オブジェクトのリスト |
fig.axes
だけは意味合いが若干異なりますが、属性名が単数形だと一つだけ、複数形だとリストになっていることがわかります。これはこの後の例でも同じです。上の表にわざわざ「Figure
に属する」と書いたのには理由があります。グラフに図形やテキストなどのArtist
を追加するときに、__Figure
に追加するかAxes
に追加するかによって座標の原点が異なる__からです。どちらを使うのが適切かはやりたいことや実装の方法によって変わるでしょう。
fig.legend
とax.legend
ところで、fig.legends
はfig.legend
メソッドによって追加された凡例を入れる箱です。「ax.legend
があるじゃん」と思いますよね。凡例を追加する際によく使われるax.legend
メソッドは、そのax
に所属するArtist
からしか凡例を作ってくれません。一方、fig.legend
メソッドは所属する全てのAxes
のArtist
から凡例をかき集めて一括表示してくれます。例えば左右の軸に異なるスケールのプロットをした時に素直にax.legend
を使うと凡例が二つできていしまいます。ちなみにc='C1'
についてはこちらを参照してください。
x = np.linspace(0, 2*np.pi, 100)
fig = plt.figure()
ax = fig.add_subplot(111)
ax.plot(x, np.sin(x), label='sin(x)')
ax1 = ax.twinx()
ax1.plot(x, 2*np.cos(x), c='C1', label='2*cos(x)')
ax.legend()
ax1.legend()
これは多くの場合望ましくはないでしょう。凡例を一つにまとめるには、最後のax.legend()
の代わりに、ax
とax1
の凡例のhandlerとlabelのリストを結合してax.legend
に渡すという有名な解決法があります。
handler, label = ax.get_legend_handles_labels()
handler1, label1 = ax1.get_legend_handles_labels()
ax.legend(handler+handler1, label+label1, loc='upper center', title='ax.legend')
# ax1.legendは更新されないのでそのまま残る
fig
実は、ver 2.1からは引数なしのfig.legend
メソッドを使ってhandlerやlabelを気にせずもっとシンプルに書けるようになっています8。デフォルトではFigure
の座標が使われますが、凡例の配置はAxes
座標ベースの方が都合の良い場合が多いです。下の例のようにbbox_transform
オプションを使うとax
に対する座標系に変更できます。
fig.legend(loc='upper right', bbox_to_anchor=(1,1), bbox_transform=ax.transAxes, title='fig.legend\nax.transAxes')
fig
Axes
グラフの見た目の調整はAxes
とその下のAxis
とTick
が主な舞台です。これらについて例を挙げながら述べます。以下はAxes
がArtist
を保持する箱です。「Axes
に属する」のは自明なので省略しました。
Axes 属性 |
説明 |
---|---|
ax.artists |
Artist オブジェクトのリスト |
ax.patch |
Axes の背景となるRectangle オブジェクト一つ |
ax.collections |
Collection オブジェクトのリスト |
ax.images |
AxesImage オブジェクトのリスト |
ax.legends |
Legend オブジェクトのリスト(2.1.1ではなくなった模様) |
ax.lines |
Line2D オブジェクトのリスト |
ax.patches |
Patch オブジェクトのリスト |
ax.texts |
Text オブジェクトのリスト |
ax.xaxis |
XAxis オブジェクト一つ |
ax.yaxis |
YAxis オブジェクト一つ |
プロットしたり二次元データを描画する際に使うax.plot
やax.imshow
などは、これらのcontainerに対応するオブジェクトを追加すると同時にいろいろな処理も一緒にしてくれるヘルパーメソッドと呼ばれてます。主なプロット関連のヘルパーメソッドは以下です。
ヘルパーメソッド | 生成されるArtist
|
格納場所 |
---|---|---|
ax.annotate |
Annotate |
ax.texts |
ax.bar |
Rectangle |
ax.patches |
ax.errorbar |
Line2D & Rectangle LineCollection
|
ax.lines & ax.patches ax.collections
|
ax.fill |
Polygon |
ax.patches |
ax.hist |
Rectangle |
ax.patches |
ax.imshow |
AxesImage |
ax.images |
ax.legend |
Legend |
ax.legends |
ax.plot |
Line2D |
ax.lines |
ax.scatter |
PathCollection |
ax.collections |
ax.text |
Text |
ax.texts |
以下の例ではax.plot
とax.scatter
でLine2D
オブジェクトとPathCollection
オブジェクトが対応したリストに追加される様子がわかります。
x = np.linspace(0, 2*np.pi, 100)
fig = plt.figure() # Figureを作成
ax = fig.add_subplot(1,1,1) # Axesを作成
print('ax.lines before plot:\n', ax.lines) # Axes.linesは空リスト
line1, = ax.plot(x, np.sin(x), label='1st plot') # Axes.linesにLine2Dを追加
print('ax.lines after 1st plot:\n', ax.lines)
line2, = ax.plot(x, np.sin(x+np.pi/8), label='2nd plot') # Axes.linesにLine2Dを追加
print('ax.lines after 2nd plot:\n', ax.lines)
print('ax.collections before scatter:\n', ax.collections)
scat = ax.scatter(x, np.random.rand(len(x)), label='scatter') # Axes.collectionsにPathCollectionを追加
print('ax.collections after scatter:\n', ax.collections)
ax.legend()
print('line1:', line1)
print('line2:', line2)
print('scat:', scat)
ax.set_xlabel('x value')
ax.set_ylabel('y value')
ax.lines before plot:
[]
ax.lines after 1st plot:
[<matplotlib.lines.Line2D object at 0x1181d16d8>]
ax.lines after 2nd plot:
[<matplotlib.lines.Line2D object at 0x1181d16d8>, <matplotlib.lines.Line2D object at 0x1181d1e10>]
ax.collections before scatter:
[]
ax.collections after scatter:
[<matplotlib.collections.PathCollection object at 0x1181d74a8>]
line1: Line2D(1st plot)
line2: Line2D(2nd plot)
scat: <matplotlib.collections.PathCollection object at 0x1181d74a8>
プロットしたオブジェクトの使いまわしは非推奨
「プロットで生成されたオブジェクトはリストに追加される」ということを知ると、「あるAxes
でプロットしたLine2D
オブジェクトを他のAxes
のlines
属性に追加すればプロット(線)を使いまわせるのかな?」と思いがちですが、これはArtist tutorialの Axes container の項目でわざわざ明記されている通りやるべきではありません。ax.plot
はax.lines
にLine2D
オブジェクトを追加する他にもいろいろとやっているからです。試しにやってみてもうまく表示されません。
x = np.linspace(0, 2*np.pi, 100)
fig = plt.figure()
ax1 = fig.add_subplot(2,1,1) # 上のsubplot
line, = ax1.plot(x, np.sin(x), label='ax1 line') # Line2Dオブジェクト
ax1.legend()
ax2 = fig.add_subplot(2,1,2) # 下のsubplot
ax2.lines.append(line) # 空リストに要素を追加
また、Line2D
をAxes
に追加する際に必要な設定をいろいろやってくれるAxes.add_line
というメソッドを試してみるとエラーになります。
ax2.add_line(line)
ValueError: Can not reset the axes. You are probably trying to re-use an artist in more than one Axes which is not supported
このエラーメッセージから、おそらく__一度どれかのcontainerに格納されたArtist
を他のcontainerに使い回すことはできない__のだろうことが予想されます。これはFigure
とAxes
とAxis
の階層構造のところで調べた上下関係とも矛盾しませんし、各Artist
には所属するcontainerが属性として登録(紐付け)されていることとも合致します。
print('fig:', id(fig)) # FigureのオブジェクトID
print('ax1:', id(ax1)) # AxesのオブジェクトID
print('line.fig:', id(line.figure)) # lineが属するFigure
print('line.axes:', id(line.axes)) # lineが属するAxes
fig: 4707121584
ax1: 4707121136
line.fig: 4707121584
line.axes: 4707121136
この辺を全てクリアできるなら使いまわしも可能でしょうが、当初の「リストに追加すればよい?」という思いつきの手順からは相当逸脱するので避けるのが妥当でしょう。
Axis
Axis
はAxes
ほどいろんなものを描画する必要がないので、軸のラベルの文字とtick関連の情報程度しか持っていません。しかし、__デフォルトのままでは微調整したくなる見た目であることが多いわりに、細かい設定の仕方がわかりにくいグラフのパーツの代表格__とも言ってよいでしょう。この説明を読むといろいろなレシピの理解がいくらか深まると思います。
公式チュートリアルにはFigure
やAxes
にあったような表がないので、同様のものを自分で作って見ました。
Axis 属性 |
説明 |
---|---|
Axis.label |
軸のラベルになるText オブジェクト一つ |
Axis.majorTicks |
major tickのラベルや位置、線を管理するTick オブジェクトのリスト |
Axis.minorTicks |
minor tickのラベルや位置、線を管理するTick オブジェクトのリスト |
Axes
の例の最後でax.set_xlabel
とax.set_ylabel
で軸のラベルを設定しました。これはax
をいじっているように見えますが、実はax
に属するXAxis
のlabel
属性を変更しています。先ほどのplot2回とscatter1回を描画した例のXAxis
で上の表の内容を確認します。
xax = ax.xaxis # XAxisオブジェクトを取得
print('xax.label:', xax.label) # ax.set_xlabelで設定したラベル
print('xax.majorTicks:\n', xax.majorTicks) # 0から6の7本+図の外側の見えない2本
print('xax.minorTicks:\n', xax.minorTicks) # 図の外側の見えない2本
xax.label: Text(0.5,17.2,'x value')
xax.majorTicks:
[<matplotlib.axis.XTick object at 0x117ae4400>, <matplotlib.axis.XTick object at 0x117941128>, <matplotlib.axis.XTick object at 0x11732c940>, <matplotlib.axis.XTick object at 0x1177d0470>, <matplotlib.axis.XTick object at 0x1177d0390>, <matplotlib.axis.XTick object at 0x1175058d0>, <matplotlib.axis.XTick object at 0x1175050b8>, <matplotlib.axis.XTick object at 0x117bf65c0>, <matplotlib.axis.XTick object at 0x117bf6b00>]
xax.minorTicks:
[<matplotlib.axis.XTick object at 0x117ab5940>, <matplotlib.axis.XTick object at 0x117b540f0>]
ax.set_***
はその場しのぎの調整
Axes
にはその下のAxis
やTick
を設定するためのax.set_***
系のヘルパーメソッドがたくさんあります。ただ、どれも__一度設定するとその後の変更に自動的に対応してくれない静的な変更__になります。xlabel
の場合は特に問題ありませんが、例えば、以下のようにax.set_xticks
で一本目のプロットのx範囲に合わせてtickの位置を変えたあとに、その範囲を超えるプロットを追加してもxticksはいい感じに調整されません。
x = np.linspace(0, 2*np.pi, 100)
fig = plt.figure()
ax = fig.add_subplot(1,1,1)
line1, = ax.plot(x, np.sin(x), label='')
ax.set_xticks([0, 0.5*np.pi, np.pi, 1.5*np.pi, 2*np.pi]) # line1のxの範囲に合わせてxticksを設定
line2, = ax.plot(1.5*x, np.sin(x), label='') # line1よりも広いx範囲にプロット
軸の見た目の調整はできればFormatter
とLocator
で
Axis
はArtist
の他にFormatter
とLocator
と呼ばれる軸の見た目の調整には欠かせない地味に重要なものが設定されています。これらはプロットするデータに応じて軸の範囲をスケールする際にtickの位置やtickラベルのフォーマットを自動的に調整する役割を担っています9。ax.set_xticks
を使った上の例でXAxis
とYAxis
のFormatter
とLocator
を確認してみます。
xax = ax.xaxis
yax = ax.yaxis
print('xax.get_major_formatter()', xax.get_major_formatter())
print('yax.get_major_formatter()', yax.get_major_formatter())
print('xax.get_major_locator():', xax.get_major_locator())
print('yax.get_major_locator():', yax.get_major_locator())
xax.get_major_formatter() <matplotlib.ticker.ScalarFormatter object at 0x118af4d68>
yax.get_major_formatter() <matplotlib.ticker.ScalarFormatter object at 0x118862be0>
xax.get_major_locator(): <matplotlib.ticker.FixedLocator object at 0x1188d5908>
yax.get_major_locator(): <matplotlib.ticker.AutoLocator object at 0x118aed1d0>
Formatter
はxとyどちらもデフォルトのScalarFormatter
です。一方Locator
はax.set_xticks
でticksの場所を指定したxではFixedLocator
に、何もしていないyではデフォルトのAutoLocator
になっています。Formatter
やLocator
の種類とそれぞれの効果については以下の公式ドキュメントを見るとなんとなくわかると思います。
Gallery > Tick formatters
Gallery > Tick locators
試しに先ほどのax.set_xticks
の例でFormatter
とLocator
を使ってみます。
import matplotlib.ticker as ticker # FormatterとLocatorはTickerモジュールが必要
ax.xaxis.set_major_locator(ticker.MultipleLocator(0.5*np.pi)) # 0.5*piごとにtickを書く
fig # jupyter notebookでは一度作ったFigureを再度表示させるにはfigを呼ぶ
ax.set_xticks
の例とは違い、描画範囲全体にtickが配置(locate)されています。次はtick labelのフォーマットを変更します。
@ticker.FuncFormatter # FuncFormatterだけはデコレータを使える。
def major_formatter_radian(x, pos):
return '{}$\pi$'.format(x/np.pi) # おそらく良い書き方ではない
ax.xaxis.set_major_formatter(major_formatter_radian)
fig
あまりよい仕上がりではないですが、Formatter
とLocator
が担っている役割ははっきりしたと思います。
ちなみに、ax.plot
にはマニュアルには明記されていない単位を指定できるオプションxunits
があります。公式ドキュメントの Gallery > Radian ticks の指示通りにbasic_unit
モジュールをimportしてmatplotlib.units.ConversionInterface
クラスを定義してやると、Formatter
やLocator
をわざわざ指定せずに理想に近いradianやdegree単位の軸が書けます。
import numpy as np
from basic_units import radians, degrees, cos
from matplotlib.pyplot import figure, show
x = [val*radians for val in np.arange(0, 15, 0.01)]
fig = figure()
fig.subplots_adjust(hspace=0.3)
ax = fig.add_subplot(211)
line1, = ax.plot(x, cos(x), xunits=radians)
ax = fig.add_subplot(212)
line2, = ax.plot(x, cos(x), xunits=degrees)
Tick
ようやく一番下の階層に到達しました。Tick
は軸上の小さな線(tick)とそこに添えられる文字(tick label)しか持ちません。
Tick 属性 |
説明 |
---|---|
Tick.tick1line | 1st ticklineのLine2D オブジェクト |
Tick.tick2line | 2nd ticklineのLine2D オブジェクト |
Tick.gridline |
Line2D オブジェクト |
Tick.label1 | 1st tick labelのText オブジェクト |
Tick.label2 | 2nd tick labelのText オブジェクト |
Tick.gridOn | グリッドを書くかのboolean |
Tick.tick1On | 1st ticklineを書くかのboolean |
Tick.tick2On | 2nd ticklineを書くかのboolean |
Tick.label1On | 1st tick labelを書くかのboolean |
Tick.label2On | 2nd tick labelを書くかのboolean |
Axis
のときと同様に、Tick
オブジェクトは実際だとXTick
とYTick
という名前になっています。1stや2nd tickなどはXTick
ではグラフ下と上、YTick
では左と右のtickのことを指しています。おそらくx軸のtickのラベルをグラフ上部にも表示したい場合などに使うものです。
Axes
にもAxis
にもTick
をいじるためのset_***
系のヘルパーメソッドがたくさんあるので、Tick
を直接触る必要はほとんどないでしょう。あるいはAxes.tick_params
でもtickの向きや表示の有無などを一括設定できます。tickの位置やラベルは前述の通りAxis
のFormatter
とLocator
で設定します。
各Artist
のデフォルト設定のカスタム
ここまで把握できれば Tutorials > Customizing matplotlib > A sample matplotlibrc file
に羅列された項目にざっと目を通して、何に対してどんな設定ができるのか確認してみると良いと思います。
matplotlibrcファイルを作らなくても一貫したスタイルで作図したい都度に
plt.rcParams['lines.linewidth'] = 2
などとしてデフォルト値を設定できます。
読める人はぜひ改善された公式ドキュメントへ
おそらくmatplotlibのArtist
について日本語で言及してる記事は現時点では冒頭で紹介したものと本稿くらいですが10、英語で探すともっとあります。例えば公式サイトで紹介されている外部サイトは体系だって説明しているものが多いです。しかし、英語が読める人はひとまず公式ドキュメントにさっと目を通してみてください。「matplotlibは公式ドキュメントが散らかっていてわかりにくい」と思っている人が多いでしょうが、実は2017年10月にリリースされた2.1.0でかなり改善されました。執筆時の最新版2.1.1(2017年12月リリース)と改善直前の2.0.2(2017年5月リリース)で対応するページを見比べると一目瞭然です。
2.1.1 | 2.0.2 |
---|---|
Gallery と Tutorials | Matplotlib Examples と Thumbnail gallery |
Overview | Overview |
改善前はそもそもExamplesとGalleryの違いがよくわからない上に、チュートリアルがなかったのでとりあえずExamplesを一つずつ見てまわりやりたいことに似てるものを探すしかありませんでした。改善後は基本的な項目がレベル別とカテゴリ別にTutorialsにまとめられ、象徴的な図とタイトルも添えられ一覧性が良くなっています。ドキュメントの改善に関しては現在の開発リーダーであるThomas Caswell氏が2015年からmatplotlibに資金援助しているNumFOCUSのインタビューで述べています。
Matplotlib Lead Dev on Why He Can't Fix the Docs | NumFOCUS
個別相談始めました
matplotlib 最後の一歩
見た目の微調整に苦労してる方、コーヒー1杯で相談に乗ります。
-
このインタビューではmatplotlibの開発リーダー自身もたまにドキュメントから情報を探すのに苦労してることを告白しています。 ↩
-
おそらく現在の詳しさの度合いに関係なく大半のmatplotlibユーザーが通ったことのある道ではないでしょうか。 ↩
-
なぜaxisでなくaxesなのかと思う人も多いでしょう。後述している通り、matplotlibにおいては両者は異なるオブジェクトを意味します。 ↩
-
gcf
とgca
は get current figure/axesの略です。 ↩ -
ほとんどの場合はどちらでも好きな方を使えばよいと思いますが、GUIでいじれるインタラクティブなグラフを別ウィンドウで出すなどする場合は後者の例の方がよいようです。 ↩
-
解剖図を作成するコードもあります。tick関連の設定が参考になります。 https://matplotlib.org/gallery/showcase/anatomy.html ↩
-
もちろんこの他にも
Artist
はあります。全体像は artist Module — Matplotlib 2.1.1 documentation を参照してください。四角で囲まれた名前をクリックするとそれぞれのドキュメントに移動できます。 ↩ -
2.0.2のマニュアルを見るとhandlerとlabelが必須だったので、あまり便利ではなかったようです。 ↩
-
軸の見た目の調整について検索すると必ず遭遇するやつです。この記事で述べているようなことを知らないと、なかなか何をやっているか理解できず最後の一歩の調整に難儀するやつです。私は大嫌いでした。 ↩
-
これにはちょっとびっくりしました。雰囲気でmatplotlibを使ってる人もけっこう多いのでしょう。 ↩