3133
3165

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

早く知っておきたかったmatplotlibの基礎知識、あるいは見た目の調整が捗るArtistの話

Last updated at Posted at 2018-01-11

English version available on dev.to

はじめに

matplotlibで作ったグラフの細かい調整は大変です。何をどういじったらいいのかを調べるのにアホみたいに時間がかかることがあります1。「何を」の部分の名前さえわからないこともあります。解決の糸口を掴んだ後も希望通りの見た目を実現するまでの最後のアレンジに苦労することが多いです2。これらの問題は__matplotlibのグラフがどういう要素で構成されていて、それらに対してどういうことができるかを知る__ことでいくらか改善されます。私はひたすらStack Overflowの回答を読むことでいろんなつまづきを時間をかけて乗り越えてきましたが、最近になってようやく公式チュートリアルにこの苦労を回避できたはずのヒントが書いてあることに気づきました。初期にざっと目を通したのですが「なるほど、よくわからん」と判断して読み込まなかったArtistに関する簡単な説明です。この記事で、新しいユーザーが私の経験したような無駄な苦労を回避できれば、あるいはすでにある程度なんとなくわかってきたユーザーが理解を深めてもらえたら嬉しいです。

見た目調整の個別相談を始めました。記事の最後にリンクがあります。

この記事の目的と内容

「こうしたい時はこうする」といった細かいノウハウではなく、いわゆる釣りの仕方(検索の際のキーワード選び)や釣った後のさばき方一般(検索で見つけた近い解法を自分向けにアレンジする際のヒント)に役立つアレコレについて述べます。ウェブに散らばる無数の断片的で対症療法的なメモ、tips、処方箋、レシピの内容がクリアになると思います。matplotlibベースであるSeabornやPandasのプロット機能を使っている人にとっても、グラフの細かな調整をする際に役立つはずです。

本記事の大部分はmatplotlib公式チュートリアルのArtist tutorialUsage 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.plotax.plotの違いについて述べます。公式チュートリアルでも A note on the Object-Oriented API vs PyplotCoding Styles で言及されていますが、matplotlibでグラフを作るには二つの流儀(インターフェース)があります。公式ドキュメントを含めてネット上に大量にあるmatplotlibのコードにはこの二つが混在していますが、これらの違いについて明記してる例はあまり多くないように思えます。また、意味もなく二つを混ぜて使っている例も多く、これが多くの初心者のつまづきの原因になっていると思います。初めて"matplotlib プロット 方法"で検索した時のことを思い出してください。__「plt.なんとかで全部済んでる例があるのになんでax.plotとか変なのがいろいろ出てくる例もあるの?ていうかこのax使ってなくない?バカなの?」__と思いませんでしたか?私は思いました。ここではそれぞれの流儀の違いについて述べるだけです。具体的な使い方を知りたい方は以下を参照してください。

オブジェクト指向インターフェース

fig, ax = plt.subplots()などの後にax.plotなどを使う流儀です。figaxはこの記事で説明する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.subplotsfig.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()
sphx_glr_pyplot_007.png

Tutorials > Pyplot tutorial で作ってる図を見るとわかる通り、運良くデフォルト設定でもグラフの細かい部分の見栄えに問題ない場合はそこそこのものができます。ただ、オブジェクト指向という概念についてよく理解していない段階でこの流儀に慣れてしまうと、のちほど必ず混乱すると思います。私はしました。また、ある程度の段階までPyplotで図を作っても、いざ細かい調整をしようとすると、結局オブジェクト指向インターフェースのやり方に従うことになります。細かい調整が必要ない非常に簡素なグラフで事足りる時、あるいはちゃちゃっと可視化して何かを確認したい時には使えますが、人に見せる図を作る際は必ず微調整したい部分がでてくるので、早いうちからオブジェクト指向インターフェースに慣れたほうが良いです。

Figure, Axes, Axisは階層構造になっている

やりたいことがでてくるたびにググっていると、オブジェクト指向についてよく知らなくてもなんとなくmatplotlibにはfig, axなどと表記される階層構造のようなものがあることがわかってくると思います。最新のドキュメントからは削除されてしまったようですが、Matplotlib 1.5.1のFAQ > Usage にあった以下の図が最低限把握しておきたいmatplotlibの階層構造を簡潔に表しています。

fig_map.png

後ほど述べる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)

試しに何もプロットせずにFigureAxesのみを作って、それぞれの関係を示す属性を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)
output
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'

ここからわかるのは以下の関係です。AxesSubplotplt.subplotsで作った場合のAxesオブジェクトと思って問題ありません。

  • Figureは自分の下のAxesを知っているけどその下のAxisは知らない
  • Axesは自分の上のFigureと下のAxisを知っている
  • Axisは自分の上のAxesとその上のFigureを知っている
  • Figureは複数のAxesを持てる(printで表示されているのが[]で囲まれたリストなので)
  • Axesは一つのFigureにしか属せない(printで表示されているのがリストではないので)
  • AxesXAxisYAxisを一つずつしか持てない(同上)
  • XAxisYAxisは一つのAxes、一つのFigureにしか属せない(同上)

グラフに表示されているものは全てArtist

執筆時の最新版2.1.1の Tutorials > Introductory > Usage Guide には、階層構造の代わりにグラフの構成要素を細かく示した"解剖図"6が掲載されています7。これを見ると「この、なに、軸?枠?」の部分はSpinesと呼ばれていることがわかります。

anatomy1.png

データを示す線や点、x軸やy軸、描画領域を表す枠や文字などグラフに表示される全ての要素はArtistと呼ばれます。Artistはcontainer(容器)とprimitive(原始的なものという意味)の二種類に分類されます。先の階層構造で示したFigureAxesAxisはcontainerに、プロットの線(Line2D)や点(PathCollection)あるいは文字(Text)はprimitiveに相当します。解剖図にあるtickやlabelは名前こそ違いますが実体はLine2DTextオブジェクトです。これらの関係を簡単に図示すると以下のようになります。各primitiveを生成するAxesメソッドの例を右側に示しました。

スクリーンショット 2018-01-11 1.00.17.png

前項のサンプルコードの結果を見るとわかりますが、Axisは実際にはXAxisYAxisという名前です。Axisの下にはさらTickという目盛り関連の線や文字のためのcontainerがあります。containerはその名の通りprimitiveを入れる箱を持っています。このとき、containerの階層構造にあった「一つしか持てない」という制限はなく、いくつでもおなじprimitiveを持つことができます。例えば何もプロットしていないaxLine2Dオブジェクトを入れる箱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)
output
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)

download-9.png

四つの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.legendax.legend

ところで、fig.legendsfig.legendメソッドによって追加された凡例を入れる箱です。「ax.legendがあるじゃん」と思いますよね。凡例を追加する際によく使われるax.legendメソッドは、そのaxに所属するArtistからしか凡例を作ってくれません。一方、fig.legendメソッドは所属する全てのAxesArtistから凡例をかき集めて一括表示してくれます。例えば左右の軸に異なるスケールのプロットをした時に素直に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()

download-1.png

これは多くの場合望ましくはないでしょう。凡例を一つにまとめるには、最後のax.legend()の代わりに、axax1の凡例の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

download.png

実は、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

download-1.png

Axes

グラフの見た目の調整はAxesとその下のAxisTickが主な舞台です。これらについて例を挙げながら述べます。以下はAxesArtistを保持する箱です。「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.plotax.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.plotax.scatterLine2Dオブジェクトと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')
output
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>

download.png

プロットしたオブジェクトの使いまわしは非推奨

「プロットで生成されたオブジェクトはリストに追加される」ということを知ると、「あるAxesでプロットしたLine2Dオブジェクトを他のAxeslines属性に追加すればプロット(線)を使いまわせるのかな?」と思いがちですが、これはArtist tutorialの Axes container の項目でわざわざ明記されている通りやるべきではありません。ax.plotax.linesLine2Dオブジェクトを追加する他にもいろいろとやっているからです。試しにやってみてもうまく表示されません。

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) # 空リストに要素を追加

download-1.png

また、Line2DAxesに追加する際に必要な設定をいろいろやってくれるAxes.add_lineというメソッドを試してみるとエラーになります。

ax2.add_line(line)
output
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に使い回すことはできない__のだろうことが予想されます。これはFigureAxesAxisの階層構造のところで調べた上下関係とも矛盾しませんし、各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
output
fig: 4707121584
ax1: 4707121136
line.fig: 4707121584
line.axes: 4707121136

この辺を全てクリアできるなら使いまわしも可能でしょうが、当初の「リストに追加すればよい?」という思いつきの手順からは相当逸脱するので避けるのが妥当でしょう。

Axis

AxisAxesほどいろんなものを描画する必要がないので、軸のラベルの文字とtick関連の情報程度しか持っていません。しかし、__デフォルトのままでは微調整したくなる見た目であることが多いわりに、細かい設定の仕方がわかりにくいグラフのパーツの代表格__とも言ってよいでしょう。この説明を読むといろいろなレシピの理解がいくらか深まると思います。

公式チュートリアルにはFigureAxesにあったような表がないので、同様のものを自分で作って見ました。

Axis 属性 説明
Axis.label 軸のラベルになるTextオブジェクト一つ
Axis.majorTicks major tickのラベルや位置、線を管理するTickオブジェクトのリスト
Axis.minorTicks minor tickのラベルや位置、線を管理するTickオブジェクトのリスト

Axesの例の最後でax.set_xlabelax.set_ylabelで軸のラベルを設定しました。これはaxをいじっているように見えますが、実はaxに属するXAxislabel属性を変更しています。先ほどの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本
output
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にはその下のAxisTickを設定するための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範囲にプロット

download-2.png

軸の見た目の調整はできればFormatterLocator

AxisArtistの他にFormatterLocatorと呼ばれる軸の見た目の調整には欠かせない地味に重要なものが設定されています。これらはプロットするデータに応じて軸の範囲をスケールする際にtickの位置やtickラベルのフォーマットを自動的に調整する役割を担っています9ax.set_xticksを使った上の例でXAxisYAxisFormatterLocatorを確認してみます。

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())
output
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です。一方Locatorax.set_xticksでticksの場所を指定したxではFixedLocatorに、何もしていないyではデフォルトのAutoLocatorになっています。FormatterLocatorの種類とそれぞれの効果については以下の公式ドキュメントを見るとなんとなくわかると思います。
Gallery > Tick formatters
Gallery > Tick locators

試しに先ほどのax.set_xticksの例でFormatterLocatorを使ってみます。

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を呼ぶ

download-3.png

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

download-4.png

あまりよい仕上がりではないですが、FormatterLocatorが担っている役割ははっきりしたと思います。

ちなみに、ax.plotにはマニュアルには明記されていない単位を指定できるオプションxunitsがあります。公式ドキュメントの Gallery > Radian ticks の指示通りにbasic_unitモジュールをimportしてmatplotlib.units.ConversionInterfaceクラスを定義してやると、FormatterLocatorをわざわざ指定せずに理想に近い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)
sphx_glr_radian_demo_001.png

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オブジェクトは実際だとXTickYTickという名前になっています。1stや2nd tickなどはXTickではグラフ下と上、YTickでは左と右のtickのことを指しています。おそらくx軸のtickのラベルをグラフ上部にも表示したい場合などに使うものです。

AxesにもAxisにもTickをいじるためのset_***系のヘルパーメソッドがたくさんあるので、Tickを直接触る必要はほとんどないでしょう。あるいはAxes.tick_paramsでもtickの向きや表示の有無などを一括設定できます。tickの位置やラベルは前述の通りAxisFormatterLocatorで設定します。

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
GalleryTutorials Matplotlib ExamplesThumbnail 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杯で相談に乗ります。

  1. このインタビューではmatplotlibの開発リーダー自身もたまにドキュメントから情報を探すのに苦労してることを告白しています。

  2. おそらく現在の詳しさの度合いに関係なく大半のmatplotlibユーザーが通ったことのある道ではないでしょうか。

  3. なぜaxisでなくaxesなのかと思う人も多いでしょう。後述している通り、matplotlibにおいては両者は異なるオブジェクトを意味します。

  4. gcfgcaは get current figure/axesの略です。

  5. ほとんどの場合はどちらでも好きな方を使えばよいと思いますが、GUIでいじれるインタラクティブなグラフを別ウィンドウで出すなどする場合は後者の例の方がよいようです。

  6. 解剖図を作成するコードもあります。tick関連の設定が参考になります。 https://matplotlib.org/gallery/showcase/anatomy.html

  7. もちろんこの他にもArtistはあります。全体像は artist Module — Matplotlib 2.1.1 documentation を参照してください。四角で囲まれた名前をクリックするとそれぞれのドキュメントに移動できます。

  8. 2.0.2のマニュアルを見るとhandlerとlabelが必須だったので、あまり便利ではなかったようです。

  9. 軸の見た目の調整について検索すると必ず遭遇するやつです。この記事で述べているようなことを知らないと、なかなか何をやっているか理解できず最後の一歩の調整に難儀するやつです。私は大嫌いでした。

  10. これにはちょっとびっくりしました。雰囲気でmatplotlibを使ってる人もけっこう多いのでしょう。

3133
3165
6

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3133
3165

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?