seabornの洗練されたスタイルで作ったグラフはとてもきれいです。見た目だけでなく、列の多いデータの全体像を把握するのにも威力を発揮します 1 。特に適切に整形されたデータフレームを渡せばカテゴリの比較や全パラメータの相関を一瞥できる図が一瞬で作れる機能は、同等の図をmatplotlibで一から作る苦労を考えると驚愕に値します。データサイエンティストやkagglerに人気があるのも納得です。また、複雑なデータを扱っていないけど単に見た目の良いグラフを作りたいという人の要望にも簡単に答えてくれます。可視化のお作法的にも見た目的にもだいたい勝手にいい感じにしてくれる手軽さが売りのseabornですが、ときには自分で調整したくなるときもあります。matplotlibだと面倒な調整を手軽にやってくれるseabornらしいメソッドで解決できるならいいのですが、たまにseabornのベースであるmatplotlibの機能に直接アクセスする必要が生じます 2 。「手軽に複雑できれいなグラフを作れる」という特徴がseabornの最大の魅力であることを考えると、多くのseabornユーザーにとってはmatplotlibのオブジェクトを直接触らないといけないタイプの調整は技術的にも心理的にもハードルが高いものでしょう。そこで本稿では、seabornでできる調整を公式ドキュメントより詳しく確認するとともに、いくつかの具体例を通してmatplotlibに直接触る必要がある細かい見た目調整のやり方を解説します。
目的と内容この記事ではseabornできれいに作れる様々なプロットの紹介はしません。その代わりに、「手軽に複雑できれいなグラフを作れる」というseabornの特徴を無駄にしないためにmatplotlibに深入りするのを避けている人たち向けに、seabornに用意されているキーワードやメソッドでは調整できない部分のいじり方を紹介します。結局のところどの例もmatplotlibの見た目調整機能に行き着くのですが、"生"のmatplotlibに詳しくないseabornユーザーがああでもないこうでもないと迷うことなく最短経路で目的の機能にたどりつけるようになる内容です。また、seabornの機能で対応できる調整もまとめます。公式ドキュメントでもきちんと説明されていないものがあるのでこれだけでも有益に感じる方がいるかもしれません。
こんな人向け
***Grid
系のメソッドの使い方がよくわからず使うのをあきらめたことがある。
Python 3.6とJupyterを使っています。
%matplotlib inline
import seaborn as sns
import matplotlib.pyplot as plt
print(sns.__version__)
# 0.9.0
このあとの例ではJupyter notebookにinline表示されたpng画像を貼っています。Jupyter notebookのinline画像はfig.savefig
メソッドでbbox_inches='tight'
を指定した際のものに相当します。つまりbbox_inches
を指定せずにファイルに出力すると端が欠けている可能性があるので注意してください。
matplotlibに関する前提知識
matplotlibのオブジェクト指向インターフェースとArtist
オブジェクトについての理解が必須です。必ずこの記事に一通り目を通してください。特にPyplotインターフェースとオブジェクト指向インターフェースの区別は必須です3。具体例を色々挙げていますが、結局のところ全てに共通するのは「使っているseaborn APIに応じた方法で変えたい部分のmatplotlibオブジェクトにアクセスし、当該オブジェクトのメソッドを実行する」という点です。Pyplotインターフェースを使った方法でも対応できるものはありますが、複数のグラフをちゃちゃっと作ってしまうseabornの特性上、matplotlibのオブジェクトに関する理解がないと「今やりたい変更はPyplotインターフェースで大丈夫か?」という判断を下せないので、横着せずにオブジェクト指向インターフェースを使った方が良いです。
seabornの便利プロット機能は何をしているのか
seabornの便利プロット関数たちは"figureレベル"と"axesレベル"の2種類に分けられます。
Figure-level and axes-level functions
これらはそれぞれmatplotlibのFigure
オブジェクト全体を管理しているか、Axes
オブジェクトのみを扱っているかの違いです4。見分け方は案外簡単で、matplotlibのAxes
オブジェクトを指定するax
キーワードをとるかどうかで判断できます。seabornの関数(ドキュメントではAPIと呼ばれる)のほとんどがaxesレベルです。数の少ないfigureレベル関数とその返り値のseabornオブジェクトを以下に列挙します。灰色がかかって見にくいですが、関数名と返り値はそれぞれのドキュメントにリンクしています。
relplot
(0.9.0で実装) -> FacetGrid
catplot
(0.9.0でfactorplot
から名称変更) -> FacetGrid
jointplot
-> JointGrid
pairplot
-> PairGrid
lmplot
-> FacetGrid
clustermap
-> ClusterGrid
(なぜかドキュメントなし)
公式ギャラリーをみるとわかりますが、複数のグラフを一気に作ってくれるものはfigureレベル関数です。ここに挙げられていないものは全てaxesレベル関数です。
ちなみに、seabornの基本となるこれら二つのカテゴリについて言及している日本語の記事は以下の二つしか見当たりませんでした。ドキュメントを読んでる人もいると思うのですが、あまり日本語で情報発信する方は少ないようです5。
Pythonでデータ分析実践演習 ~2016 New Coder Survey 編~ - Qiita
matplotlib + seaborn: Pythonでグラフ描画 - Heavy Watal
seabornの***Grid
系オブジェクト
matplotlibのラッパーであるseabornが扱うオブジェクトのほとんどはmatplotlibのArtist
オブジェクトです。しかし、seabornの目玉機能とも言える複雑なプロットを作ってくれるfigureレベル関数向けにだけseaborn独自の***Grid
系オブジェクトが用意されています。***Grid
系オブジェクトはJointGrid
とそれ以外(FacetGrid
、PairGrid
、ClusterGrid
)の2種類に分けられます。これはそれぞれの見た目からなんとなく想像できます。クリックすると大きい画像が表示されます。
JointGrid
だけグリッド(碁盤の目)っぽくないですね。ソースコードを見ると、グリッドっぽい見た目の後者三つはまさにGrid
クラスを継承していますが、JointGrid
だけGrid
を継承していません6。この分類は後ほど説明するseabornでできる調整を知る際に必要な知識です。
描画に使われたmatplotlibオブジェクトにアクセスする
seabornがちゃちゃっと描いてくれたかっこいいグラフは(驚くことに)全てmatplotlibがベースになっています。では、seabornというラッパー(wrapper=包み紙)をはぎとって中にあるmatplotlibに触れるにはどうすればいいのでしょうか。
figureレベル関数
複数のグラフをだーっと並べてくれるfigureレベル関数はmatplotlibのFigure
オブジェクトとAxes
オブジェクトを関数内で自分で用意しているため7、ユーザーが用意したFigure
やAxes
を指定することはできません。関数内で作られたFigure
とAxes
は、figureレベル関数の戻り値である各***Grid
オブジェクトのfig
およびaxes
属性に保持されています。
# https://seaborn.pydata.org/examples/anscombes_quartet.html より
sns.set(style="ticks")
df = sns.load_dataset("anscombe") # seabornに含まれるサンプルデータの一つを読み込み
grid = sns.lmplot(x="x", y="y", col="dataset", hue="dataset", data=df,
col_wrap=2, ci=None, palette="muted", height=2,
scatter_kws={"s": 50, "alpha": 1})
print(type(grid))
print(type(grid.fig))
# <class 'seaborn.axisgrid.FacetGrid'>
# <class 'matplotlib.figure.Figure'>
# FacetGridは1D numpy arrayとして、FigureはリストとしてAxesを保持
print(type(grid.axes))
print(type(grid.fig.axes))
# <class 'numpy.ndarray'>
# <class 'list'>
# 中身は同じ
print(id(grid.fig.axes[0]) == id(grid.axes[0]))
# True
axesレベル関数
figureレベル関数とは異なり、axesレベル関数にはAxes
オブジェクトを指定できるax
キーワードがあります。ax
の指定の有無に関わらず、描画に使ったAxes
が戻り値です。ax
に何も指定しなかった場合はFigure
とAxes
はそれぞれPyplotインターフェースと同様にcurrent figureとcurrent axesが設定されます8。
# https://seaborn.pydata.org/examples/errorband_lineplots.html より
sns.set(style="darkgrid")
plt.figure(figsize=(4, 3)) # デフォルトだと大きいので小さめに
fmri = sns.load_dataset("fmri")
ax = sns.lineplot(x="timepoint", y="signal",
hue="region", style="event",
data=fmri)
print(type(ax))
# <class 'matplotlib.axes._subplots.AxesSubplot'>
print(id(ax) == id(plt.gca()))
# True
print(id(ax.figure) == id(plt.gcf()))
# True
current figure/axesを使っているということは、Axes
を指定せずにaxesレベル関数で作ったグラフはPyplotインターフェースのplt.figure(figsize=(4, 3))
やplt.xlabel
などが使えます。Axes
を指定した場合は当然Pyplotインターフェースの代わりにオブジェクト指向インターフェースのax.set_title
やax.set_xlabel
などが使えます。
seaborn APIでは手の届かない見た目調整のエッセンスは実はこの部分で、あとは個々のmatplotlibオブジェクトに対して適切なメソッドを使うだけです。
seaborn APIで設定できる項目
seaborn APIではできないことの話に入る前にできることを確認します。まずaxesレベル関数にはマーカーや線のサイズなどプロット本体の設定項目、より具体的に言うとax.plot
やax.scatter
に渡せる項目しかありません。これは「〜プロットと呼ばれる図を作る」というaxesレベル関数の役割を考えると理解できて、付属部品にすぎない軸ラベルやタイトルに関する設定はドキュメントを見てもありません。つまり、axesレベル関数の場合はマーカーや線などのプロット本体以外の調整は関数の戻り値であるmatplotlibのAxes
を受け取ってそのメソッドを使えということです。
一方、figureレベル関数の場合は戻り値の***Grid
系オブジェクトのメソッドを使うと以下のようにいろいろな設定ができます。
FacetGrid
、PairGrid
、ClusterGrid
で使えるset
メソッド
これは三つのオブジェクトが継承している大元のGrid
で定義されているメソッドですが、実際は全てのAxes
に対してmatplotlibのAxes.set
メソッドにパラメータをそのまま渡して実行しているだけです9。Axes.set
自体がドキュメントの説明がいい加減なせいかあまり知られていないようですが、Axes
のドキュメントに登場するすべてのset_***
系メソッドの***
部分をパラメータとして一括設定できるそこそこ便利なメソッドです。よく使われる項目を例にすると以下のような感じで使えます。titleやxscaleなどにも使えます。
ax = plt.gca()
ax.scatter(3*np.random.rand(30), 3*np.random.rand(30))
ax.set(ylim=(0,3), ylabel='y position', xlim=(0,3), xlabel='x position', aspect='equal')
# 以下と同じ
# ax.set_ylim((0,3))
# ax.set_ylabel('y position')
# ax.set_xlim((0,3))
# ax.set_xlabel('x position')
# ax.set_aspect('equal')
一見便利そうに見えますが、Grid.set
は全てのAxes
に対して適用されてしまうので、例えばxlabel
やylabel
を設定するとseabornがせっかくいい感じに消してくれた軸ラベルも表示されてしまいます。
sns.set(style="ticks")
df = sns.load_dataset("anscombe")
grid = sns.lmplot(x="x", y="y", col="dataset", hue="dataset", data=df,
col_wrap=2, ci=None, palette="muted", height=2,
scatter_kws={"s": 50, "alpha": 1})
grid.set(xlabel='xx', ylabel='yy')
これをmatplotlibのメソッドで修正するには、左端と下端のAxes
を区別する条件をつけてラベルを消していく必要があり「せっかくseabornを使っているのに何をやってるんだろうか」とむなしくなります。Grid.set
メソッドが適しているのはこのような影響を受けないx/ylim
やx/yscale
、aspect
の設定でしょう。
FacetGrid
独自のメソッド
FacetGrid
を返すrelplot
、catplot
、lmplot
の場合、Grid.set
向きではないラベルやタイトルの設定には以下に挙げるFacetGrid
独自のメソッドが便利です。
一気に複数の部品を変更するため、Axes
の似たようなメソッド名とは異なりどれもset_複数形
になっていることに注意してください。しかし、実は注意が必要なのはメソッド名だけではありません。まずは特に注意の必要ない軸ラベルの変更をみてみましょう。
set_axis_labels
の例
軸ラベルの変更はメソッド名さえ知っていれば簡単です。
df = sns.load_dataset("anscombe")
grid = sns.lmplot(x="x", y="y", col="dataset", hue="dataset", data=df,
col_wrap=2, ci=None, palette="muted", height=2,
scatter_kws={"s": 50, "alpha": 1})
grid.set_axis_labels('x (sec)', 'y (m)') # x_var='x (sec)'などとしてもよい
# 以下も同じになる
# grid.set_xlabels('x (sec)').set_ylabels('y (m)')
# selfが返ってくるのでこのようなメソッドチェーンにもできる
set_titles
の例
FacetGrid
における各Axes
のタイトルは元データのDataFrame
の構造に忠実に従うべきという設計思想になっているようで、seabornで用意されているFacetGridl.set_titles
メソッドはあまり融通が利きません10。ドキュメントには使い方が書いていないのでdocstringを見ると{col_var}
や{col_name}
といった指定キーを使ってフォーマットしろとありますが、いまいちなんのことかわかりません。
set_titlesのdocstring
Signature: g.set_titles(template=None, row_template=None, col_template=None, **kwargs)
Docstring:
Draw titles either above each facet or on the grid margins.
Parameters
template : string
Template for all titles with the formatting keys {col_var} and
{col_name} (if using a `col` faceting variable) and/or {row_var}
and {row_name} (if using a `row` faceting variable).
row_template:
Template for the row variable when titles are drawn on the grid
margins. Must have {row_var} and {row_name} formatting keys.
col_template:
Template for the row variable when titles are drawn on the grid
margins. Must have {col_var} and {col_name} formatting keys.
実際に使って見ましょう。
# https://seaborn.pydata.org/generated/seaborn.FacetGrid.html より
sns.set(style="ticks", color_codes=True)
tips = sns.load_dataset("tips")
g = sns.FacetGrid(tips, col="size", col_wrap=3)
g = g.map(plt.hist, "tip", bins=np.arange(0, 13), color="c"
g.set_titles("{col_var}={col_name} diners"))
g.fig.set_size_inches((6, 4))
tips
というDataFrame
の中身は以下のようになっています。
上の例のg.set_titles("{col_var}={col_name} diners"))
では、size列の値ごとに作ったtipのヒストグラムに列の名前col_var
と値col_name
を使ったタイトルを付けています11。
col
とrow
の両方を指定した場合のデフォルトタイトルは「row名 = row値 | col名 = col値」です。
kws = dict(s=50, linewidth=.5, edgecolor="w")
g = sns.FacetGrid(tips, col="smoker", row="sex")
g = g.map(plt.scatter, "total_bill", "tip", color="m", **kws)
g.set(xlim=(0, 60), ylim=(0, 12), xticks=[10, 30, 50], yticks=[2, 6, 10])
このときset_titles
は以下のように使えます。
g.set_titles(template='{row_var}: {row_name}\n{col_var}: {col_name}')
g.fig
FacetGrid
を作る際にmargin_titles
キーワードを使った場合は以下のようなことができます。
kws = dict(s=50, linewidth=.5, edgecolor="w")
g = sns.FacetGrid(tips, col="smoker", row="sex", margin_titles=True)
g = g.map(plt.scatter, "total_bill", "tip", color="m", **kws)
g.set(xlim=(0, 60), ylim=(0, 12), xticks=[10, 30, 50], yticks=[2, 6, 10])
g.set_titles(row_template='{row_var}: {row_name}', col_template='{col_var}: {col_name}')
g.fig
set_xticklabels
、set_yticklabels
の例
set_titles
ではseabornの設計思想に注意する必要がありましたが、set_xticklabels
/set_yticklabels
はmatplotlibのTick
の仕様に注意する必要があります。
tips = sns.load_dataset("tips")
g = sns.FacetGrid(tips, col="size", col_wrap=3)
g = g.map(plt.hist, "tip", bins=np.arange(0, 13), color="c")
g.set_titles("{col_var}={col_name} diners")
g.fig.set_size_inches((6, 4))
g.set_xticklabels(['', 'zero', 'five', 'ten', ''])
set_xticklabels
に渡しているリストの最初と最後に謎の空string要素があります。これは描画範囲の両端の外側に一つずつ見えないTick
が設定されるmatplotlibの謎仕様に対応するためです。これは以下のようにするとわかります。
set_xticklabels適用前
[l for l in g.axes[0].get_xticklabels()]
# [Text(-5.0, 0, '−5'), # 謎tick
# Text(0.0, 0, '0'),
# Text(5.0, 0, '5'),
# Text(10.0, 0, '10'),
# Text(15.0, 0, '15')] # 謎tick
set_xticklabels適用後
[l for l in g.axes[0].get_xticklabels()]
# [Text(-5.0, 0, ''), # 謎tick
# Text(0.0, 0, 'zero'),
# Text(5.0, 0, 'five'),
# Text(10.0, 0, 'ten'),
# Text(15.0, 0, '')] # 謎tick
JointGrid
独自メソッド
ドキュメントをみるとJointGrid
にもset_axis_labels
メソッドがあります。メインプロットの軸ラベルを変えるだけのメソッドなので特に便利というわけでもありません。
# https://seaborn.pydata.org/generated/seaborn.JointGrid.html より
g = sns.JointGrid(x="total_bill", y="tip", data=tips, space=0)
g.plot_joint(sns.kdeplot, cmap="Blues_d")
g.plot_marginals(sns.kdeplot, shade=True)
g.fig.set_size_inches((4,4))
g.set_axis_labels('total bill (USD)', 'tip (USD)')
画像サイズ
これはすでに今までの例でもしれっと使っていました。axesレベル関数とfigureレベル関数で少し違います。axesレベル関数の場合はプロット関数実行前にあらかじめ画像サイズを定義できます。
# axを渡す場合
fig, ax = plt.subplots(figsize=(8, 3))
sns.lineplot(x="timepoint", y="signal",
hue="region", style="event",
data=fmri, ax=ax)
# axを渡さない場合はpyplotインターフェースでもよい
# 出力は上の例と同じ
plt.figure(figsize=(8, 3))
# plt.rcParams['figure.figsize']=(8, 3) # matplotlibのデフォルト設定を変えても同じ結果
ax = sns.lineplot(x="timepoint", y="signal",
hue="region", style="event",
data=fmri)
ax
キーワードでAxes
オブジェクトを指定しない場合は、matplotlibのデフォルト設定に従うのでplt.rcParams['figure.figsize']=(8, 3)
としても上と同じ結果が得られます。ただし、当然ながらデフォルト設定を変えるとこの後に作った図のサイズも影響を受けます。
figureレベル関数の画像サイズにはplt.rcParams['figure.figsize']
は使われずseaborn関数のheight
とaspect
から計算された設定された高さと幅が設定されます12。従ってfigureレベル関数の画像サイズは描画時に指定するか、描画後に変更することができます。ただし、seabornの関数で指定するのはプロット一つずつのサイズなので実際の画像のインチ幅はheight*aspect*プロット列数、高さはheight*プロット行数になります。その他にも以下に示すようにサイズ指定タイミングが描画時と描画後かによって、プロット間の余白やtickラベルなど細かいパーツの扱いが変わってくるので少し試行錯誤する必要はあるかもしれません。
grid = sns.lmplot(x="total_bill", y="tip", col="time", hue="smoker",
data=tips, legend_out=False)
grid.fig.set_size_inches((4, 3))
# 細かい部分を気にせずに4インチx3インチになりそうな単一プロットのアスペクト比を設定
grid = sns.lmplot(x="total_bill", y="tip", col="time", hue="smoker",
data=tips, legend_out=False, height=3, aspect=4/3/2)
このほかにもポスター向けやスライド向けに画像サイズとフォントサイズを同時に調整してくれるset_context
というseabornらしいメソッドもあります。詳細は以下をご覧ください。
seabornでMatplotlibの見た目を良くする | note.nkmk.me
上の例では問題はありませんでしたが、grid.fig.set_size_inches
で画像サイズを変えると凡例が微妙な位置にくることもあります。その際は後述する方法で凡例をいい位置に動かせば解決します。
タイトルと軸ラベル
FacetGrid
のタイトルは元データのカラム名と値をベースにしたものしかつけられないという制限がありました。また、axesレベル関数にはそもそもタイトルがつきません。一般に我々ユーザーはわがままですから、制限なく自由に設定できる方法は知っておくと良いでしょう。タイトルや軸ラベルはmatplotlibのFigure
やAxes
にアクセスすることで自由に修正可能です。
# figureレベル関数の例
sns.set(style="ticks")
grid = sns.lmplot(x="x", y="y", col="dataset", hue="dataset", data=df,
col_wrap=2, ci=None, palette="muted", height=2,
scatter_kws={"s": 50, "alpha": 1})
grid.fig.suptitle('suptitle via grid.fig', y=1.02)
# grid.axesはnumpy arrayなのでravelかflatで一次元化します
for ax, title in zip(grid.axes.ravel(), ['a', 'b', 'c', 'd']):
ax.set_title(f'changed title {title}')
# for ax in grid.axes[2:]: # 自分で変えるAxesを指定する場合
for ax in grid._bottom_axes: # 下端のAxesだけにアクセスできるプライベート変数を使う場合
ax.set_xlabel('x (sec)')
Figure
オブジェクトはplt.gcf()
でも取得可能ですが、figureレベル関数で描画した後でないと意図通りに動きません。
fig = plt.gcf() # この段階のcurrent figureはlmplotには使われない
grid = sns.lmplot(x="x", y="y", col="dataset", hue="dataset", data=df,
col_wrap=2, ci=None, palette="muted", height=2,
scatter_kws={"s": 50, "alpha": 1})
# plt.gcf before lmplot
print(id(fig) == id(grid.fig))
# False
# plt.gcf after lmplot
fig = plt.gcf()
print(id(fig) == id(grid.fig))
# True
fig.suptitle('suptitle via gcf', y=1.02)
sns.set(style="darkgrid")
fmri = sns.load_dataset("fmri")
plt.figure(figsize=(4, 3)) # デフォルトだと大きいので小さめに
ax = sns.lineplot(x="timepoint", y="signal",
hue="region", style="event",
data=fmri)
ax.set_title('title test')
ax.set_xlabel('time (msec)')
# lineplot実行後ならax = plt.gca()でも同じAxesを取得できる
print(id(ax) == id(plt.gca()))
# True
# 出力される図は上と同じ
sns.lineplot(x="timepoint", y="signal",
hue="region", style="event",
data=fmri)
# current axesに対する設定
plt.title('title test')
plt.xlabel('time (msec)')
調整したくなるグラフのパーツランキングがあれば間違いなく上位に入るであろう凡例は、いろいろといい感じにしてくれるseabornでも(いろいろと勝手にやってしまうが故に)手を加えたくなるもののようです。Stack Overflowにも位置を変えたい、ダブりを消したい、凡例自体を消したい、テキストを変えたい、タイトル(もどき)を消したいなど、凡例に関して思い浮かぶ調整例がだいたい出てきます。これらの質問に対する解決方法は以下の二つの戦略のどちらかに従うか両方のいいとこ取りをしています13。
matplotlibのlegend
メソッドを直接使って一から自分で作る(seaborn内で凡例を作らないようにするか作った後に上書き)
seabornがだいたいいい感じに作ってくれたmatplotlibのLegend
オブジェクトにアクセスして修正する
axesレベル関数で作った単一のプロットの場合は1で簡単に済むこともありますが、例えば「タイトルと軸ラベル」の最後に示したhue
とstyle
を使ったlineplot
の凡例を全て自分で作るのはかなり難しいです。seabornが自動的に作った凡例は、たとえ修正が必要だろうが複雑なプロットであるほどありがたい存在なので、できれば2で解決していきたいものです。ただ、例えば凡例の位置の調整は2だけでは非常に厄介なので、seabornの作った凡例の内容をコピーして作り直すという1と2の合わせ技が最適な場合もあります。seabornの凡例関連の挙動は少し癖があるので、具体例を紹介する前に知っておくべき注意事項を挙げておきます。
seabornの凡例における注意点
Legendオブジェクトの場所 Grid
クラスを継承した***Grid
オブジェクトを返すrelplot
, catplot
, pairplot
, lmplot
はデフォルトではmatplotlibのfig.legend
メソッドを使って凡例を作っています。このとき作られたLegend
オブジェクトはfig.legends
属性にリスト要素として格納されています14。ただし、legend_out
キーワードのあるlmplot
とcatplot
ではlegend_out=False
を指定した場合、左上の最初のAxes
でax.legend
が実行されます。従ってLegend
オブジェクトはax.legend_
が保持しています。
タイトルとタイトルもどき hue
でしかグルーピングできないrelplot
, lineplt
などの凡例タイトルはLegend
オブジェクトのset_title
メソッドを使った正式なタイトルです。しかし、hue
やsize
により複数のグルーピングを指定できるlmplot
, pairplot
, boxplot
などの凡例にタイトルのように表示されるカラム名の実体は、Stack Overflowのこの回答で指摘されている通り、マーカーのない文字のみの凡例です。つまりax(fig).legend
メソッドのtitle
キーワードやLegend
オブジェクトのset_title
メソッドでは変更できません。
figureレベル関数の凡例の位置 figureレベル関数がデフォルトで凡例作成に使っているfig.legend
は、figureレベル関数内で作成されたFigure
オブジェクトのサイズから右端中央のちょうどよい位置を算出して凡例を配置しています。これはつまり、プロット作成後に画像サイズを変えると凡例が不恰好な位置にくる可能性が高いことを意味します。
以上の注意点を踏まえてStack Overflowの凡例関連の質問や以下の例を見ると、seabornの凡例調整が一気にクリアになると思います。以下ではよくある三つの調整(タイトル、位置、ラベル)を扱います。
タイトルまたはタイトルもどきを変える
グルーピングにhue
のみを使うAPIの場合は正規の凡例タイトルを変えれば良いです。
# グルーピングがhueのみのAPIはlegendのset_titleが使える
grid = sns.lmplot(x="total_bill", y="tip", col="time", hue="smoker",
data=tips)
grid.fig.set_size_inches((9, 3))
lg = grid.fig.legends[0] # legend_out=Falseの場合は lg = grid.fig.axes[0].legend_
print(type(lg))
# <class 'matplotlib.legend.Legend'>
print(lg.texts)
# [Text(0, 0, 'Yes'), Text(0, 0, 'No')]
print(lg.get_title())
# Text(0, 0, 'smoker')
lg.set_title('changed\nvia set_title')
三つめの注意点の通り、画像サイズを変更したため凡例がおかしな位置にきています。これを修正する方法は後述します。
複数のグルーピング基準を設定できるAPIの場合は、タイトルもどきの正体である凡例のlabel
を変える必要があります。label
の実体はLegend.texts
リストに入っているText
オブジェクトなので、set_text
メソッドを使って該当要素を変更します。
# 複数グルーピングのあるAPIはラベルを変える
grid = sns.relplot(x="total_bill", y="tip", col="time",
hue="smoker", size="size",
data=tips)
grid.fig.set_size_inches((8, 3))
lg = grid.fig.legends[0]
print(lg.texts) # タイトルもどきとラベルは同じオブジェクト
# [Text(0, 0, 'smoker'), Text(0, 0, 'Yes'), Text(0, 0, 'No'), Text(0, 0, 'size'), Text(0, 0, '0'), Text(0, 0, '2'), Text(0, 0, '4'), Text(0, 0, '6')]
print(lg.get_title()) # 正式なタイトルはない
# Text(0, 0, '')
lg.texts[0].set_text('SMOKER') # 元はsmoker
lg.texts[3].set_text('SIZE') # 元はsize
# 複数グルーピングのあるAPIなのでラベルを変える
plt.figure(figsize=(4, 3))
ax = sns.lineplot(x="timepoint", y="signal",
hue="region", style="event",
data=fmri)
lg = ax.legend_
print(lg.texts)
# [Text(0, 0, 'region'), Text(0, 0, 'parietal'), Text(0, 0, 'frontal'), Text(0, 0, 'event'), Text(0, 0, 'stim'), Text(0, 0, 'cue')]
print(lg.get_title())
# Text(0, 0, '')
lg.texts[0].set_text('REGION') # 元はregion
lg.texts[3].set_text('EVENT') # 元はevent
凡例の位置を変える
seabornの各関数の凡例に関する部分のソースを確認すると、axesレベル関数の場合は凡例の位置にはキーワードなしのax.legend
メソッドが実行されているのでデフォルトのloc="best"
が指定されています。figureレベル関数の場合はfig.legend
メソッド実行時にloc="center right"
が指定されていています15。これらを変更するキーワードはseabornには用意されていません。つまりデフォルトの位置で納得がいかない場合は、matplotlibのLegend
オブジェクトにアクセスする必要があり、手軽なものから順に以下のような方針があり得ます。
seabornで作った凡例の_loc
属性を変える方法16
位置を指定して新しく作り直す方法
簡単に作れる場合
ハンドルとラベルを引き継いだ方が良い場合
seabornで作った凡例のbbox_to_anchor
を変える方法17
たいていの場合は一番手軽な1で済むでしょう。1がダメなら2でloc
とbbox_to_anchor
を指定して微調整すれば解決すると思います。したがってわざわざ3を使う意味はないと思いますが、何らかの事情で凡例の作り直しが許されない場合に使えるかもしれません。あるいはmatplotlibのパーツの位置調整でよく出てくるbbox_to_anchor
がどういうものか理解する助けになると思います。
figureレベル関数でlegend_out=False
を使った場合、つまりax.legend
メソッドで作られた凡例の場合を例にします。axesレベル関数でもほぼ同じコードが使えます。以下の例ではloc="best"
によって凡例は左上に配置されています。
# legend_out=Falseだとax.legendのloc='best'がデフォルト
grid = sns.lmplot(x="total_bill", y="tip", col="time", hue="smoker",
data=tips, legend_out=False)
grid.fig.set_size_inches((9, 3))
grid.fig.suptitle('default: loc="best"', y=1.1)
まずは一番手軽な_loc
を変更する方法で右下に動かします。legend
メソッドでloc
を指定する際には"lower right"
などの文字列が使えましたが、_loc
では数字しか指定できません。文字列と数字の対応はドキュメントを参照してください。
grid.fig.axes[0].legend_._loc = 4 # lower right
grid.fig.suptitle('ax.legend_._loc = 4 (lower right)', y=1.1)
grid.fig
次は、seabornがax.legend
メソッドで作った凡例を、自分で位置指定をしたax.legend
メソッドで上書きする方法です。Axes
オブジェクトは一つしか凡例を持てないので、もう一度ax.legend
メソッドを実行すると先に作ったものが上書きされる仕様を利用しています。
grid.fig.axes[0].legend(title='smoker\nvia ax.legend', loc='center right')
grid.fig.suptitle('ax.legend + title, loc', y=1.1)
grid.fig
複数グルーピングされタイトルもどきを含むプロットも場所はloc="best"
で制御されています。以下の例の場合は右上に配置されています。
plt.figure(figsize=(4, 3))
# Plot the responses for different events and regions
ax = sns.lineplot(x="timepoint", y="signal",
hue="region", style="event",
data=fmri)
ax.set_title('default: loc="best"')
この凡例を上書きで位置を指定する場合は、seabornが作ったタイトルもどきなどをそのまま利用するのがよいでしょう。ax.get_legend_handles_labels
メソッドによってhandle(線やマーカー)とlabel(文字列)を取得してax.legend
に渡すと、面倒なタイトルもどきの部分を自分で作ることなく位置を変更できます。
plt.figure(figsize=(4, 3))
ax = sns.lineplot(x="timepoint", y="signal",
hue="region", style="event",
data=fmri)
# lineplotが作った凡例の材料を取得
handles, labels = ax.get_legend_handles_labels()
# 上書きする際に位置を指定
ax.legend(handles, labels, loc='upper left', bbox_to_anchor=(1, 1), frameon=False)
ax.set_title('ax.get_legend_handles_labels()\n+ax.legend+loc, bbox_to_anchor')
凡例の背景色や枠線を消すためにframeon=False
としています。
次に、以下のようにfigureレベル関数の画像サイズを変更したら凡例が微妙な位置にきてしまったケースで凡例の位置を修正します。
grid = sns.relplot(x="total_bill", y="tip", col="time",
hue="smoker", size="size",
data=tips)
grid.fig.set_size_inches((8, 3))
画像サイズ変更前と同じような位置に戻してみましょう。今回の凡例にもタイトルもどきが含まれるので、handleとlabelを拝借する方針でいきます。しかし、fig.legend
メソッドで作った凡例のhandleとlabelを取得するのに、先の例で使ったax.get_legend_handles_labels
メソッドは使えません。なぜならfig.legend
はFigure
オブジェクトに所属する凡例を作るメソッドであり、ax.get_legend_handles_labels
はAxes
オブジェクトに所属する凡例を対象とした便利メソッドだからです。そこでLegend
オブジェクトの属性からhandleとlabelを別々に取得します。
lg = grid.fig.legends[0] # Figureは複数の凡例を保持できるのでリストの最初の凡例を指定する
handles = lg.legendHandles # handleが保持されているリスト
labels = [t.get_text() for t in lg.texts] # labelを保持するTextオブジェクトのリストから文字列のみを抽出
lg.remove() # 削除
# fig.legendよりもax.legendのほうが位置を指定しやすい(transformを使う必要がない)
grid.fig.axes[1].legend(handles, labels, loc='center left', bbox_to_anchor=(1, 0.5),
frameon=False, )
grid.fig
ところで、fig.legend
メソッドは「fig
に所属する各Axes
オブジェクトからhandleとlabelをかき集めて凡例を作る」という仕様になっています。つまりfig
に含まれるAxes
のどれかがhandleとlabelを保持しているはずなので、fig.legend
が作ったLegend
オブジェクトにアクセスしなくとも、しかるべきAxes
オブジェクトに対してax.get_legend_handles_labels
を使えばhandleとlabelが一気に取得できるはずです。seabornのfigureレベル関数の場合はgrid.fig.axes[0]
がしかるべきAxes
です。
print(grid.fig.axes[0].get_legend_handles_labels())
output
([<matplotlib.collections.PathCollection at 0x13f8c0128>,
<matplotlib.collections.PathCollection at 0x13f9c9fd0>,
<matplotlib.collections.PathCollection at 0x13f9f2860>,
<matplotlib.collections.PathCollection at 0x13f9f2be0>,
<matplotlib.collections.PathCollection at 0x13f9f2f60>,
<matplotlib.collections.PathCollection at 0x13f9fa0f0>,
<matplotlib.collections.PathCollection at 0x13f9fa748>,
<matplotlib.collections.PathCollection at 0x13f8af7f0>],
['smoker', 'Yes', 'No', 'size', '0', '2', '4', '6'])
grid = sns.relplot(x="total_bill", y="tip", col="time",
hue="smoker", size="size",
data=tips)
grid.fig.set_size_inches((8, 3))
grid.fig.legends[0].remove() # Figureの凡例を削除
handles, labels = grid.fig.axes[0].get_legend_handles_labels()
# 今度は右axes外側の右上へ
grid.fig.axes[1].legend(handles, labels, loc='upper left', bbox_to_anchor=(1, 1), frameon=False, )
grid.fig.suptitle('ax.get_legend_handles_labels()\n+ax.legend+loc, bbox_to_anchor', y=1.15)
最後に、seabornが作ったLegend
オブジェクトを上書きすることなくそのまま使って位置を変更する方法を紹介します。実際にこの方法が必要な機会は少ないでしょうが、loc
やbbox_to_anchor
キーワードをどう指定すればいいか理解する助けにはなるでしょう。
grid = sns.lmplot(x="total_bill", y="tip", col="time", hue="smoker",
data=tips, legend_out=False)
grid.fig.set_size_inches((8, 3))
ax = grid.fig.axes[0] # 凡例のあるaxes
lg = ax.legend_ # Legendオブジェクト
lg._loc = 4 # lower right
# Axes描画領域の左下を(0, 0)右上を(1, 1)と考えたときのbounding box(凡例の位置を指定するのに使う領域または点)
bb = lg.get_bbox_to_anchor().inverse_transformed(ax.transAxes)
# Axes描画領域の左下を(0, 0)右上を(1, 1)と考えたときの凡例の位置指定に使う座標
lg_x, lg_y = 0.3, 0.5
# bounding boxと呼んではいるが四隅を同じ座標にして点として利用する
bb.set_points(np.array([[lg_x, lg_y],
[lg_x, lg_y]]))
# 凡例をanchorする座標として新しいbounding boxを指定(アンカー=元はいかりを下ろすという意味)
# _locはbouding boxのどの四隅に凡例を持っていくかという意味。bouding boxが点の場合は「凡例のどの四隅を点に寄せるか」という意味。
lg.set_bbox_to_anchor(bb, transform = ax.transAxes)
grid.fig.suptitle(f'loc="lower right", bbox_to_anchor: ({lg_x}, {lg_y}) set by bb.set_points', y=1.1)
# bounding boxの点を黒のバツで表示。
ax.scatter([lg_x], [lg_y], transform=ax.transAxes, marker='x', color='k', zorder=3)
詳細はコードのコメントをみてください。bbox_to_anchor
はそのままで_loc
を左上に変更するとこれらの役割がより明確にわかるでしょう。
lg._loc = 2 # upper left
grid.fig.suptitle(f'loc="upper left", bbox_to_anchor: ({lg_x}, {lg_y}) set by bb.set_points', y=1.1)
grid.fig
凡例のラベルを変える
「凡例のラベル」というのは、直前のグラフでいうところのYesとNoです。seabornではDataFrameの保持する値が凡例のラベルにそのまま使われるので、seabornの設計思想に則ってラベルを変えるならDataFrameの値を変えるべきです。しかし、なんらかの都合により元データを変更したくない場合は、DataFrameはそのままで可視化の段階で凡例のラベルを変えることもできます。凡例のラベルの実体であるText
オブジェクトのset_text
メソッドで変更します。
grid = sns.relplot(x="total_bill", y="tip", col="time",
hue="smoker", size="size",
data=tips)
grid.fig.set_size_inches((8, 3))
lg = grid.fig.legends[0]
lg.texts[1].set_text('1') # 元はYes
lg.texts[2].set_text('0') # 元はNo
カラーバー
matplotlibのカラーバーの振る舞いは基本を踏まえていないと何が起こっているか非常にわかりづらいため、細かいところが気になりだすと解決するのに平気で数時間かかることがあります。matplotlibを意識せずともカラーバーを作ってくれるseabornの場合はユーザーにとっては輪をかけてわかりづらいパーツでしょう。Stack Overflowにもseabornユーザーによるカラーバー関連の質問は多いです。以前私が書いたカラーバー関連の記事を見るとわかる通り、細かいことを述べ出したらキリがないので、ここでは、修正頻度が高いだろうカラーバーの位置、タイトル、目盛り位置、目盛りラベル関連の例を示します。
まずはデフォルト設定でヒートマップを作ってみます。
flights_long = sns.load_dataset("flights")
flights = flights_long.pivot("month", "year", "passengers")
fig, ax = plt.subplots(figsize=(6, 6))
sns.heatmap(flights, annot=True, fmt="d", linewidths=.5, ax=ax)
heatmap
のドキュメントを見るとcbar_kws
というのがあります。ここで辞書型パラメータを指定することでmatplotlibのfig.colorbar
にパラメータを渡すことができます。以下の例ではカラーバーの位置を上部にし、カラーバーのタイトル(実際は軸ラベル)をつけて、マイナー目盛りをつけました。
fig, ax = plt.subplots(figsize=(6, 6))
sns.heatmap(flights, annot=True, fmt="d", linewidths=.5, ax=ax,
cbar_kws = dict(use_gridspec=False, location="top",
label='No. of passengers'))
# minor tickはColorbarオブジェクトのメソッドを利用
ax.collections[0].colorbar.minorticks_on()
location
を指定するにはuse_gridspec=False
である必要があるので注意してください18。
メジャー目盛りの位置を指定するには手動かlocatorを使う方法があります。以下の例はticks=[200, 400, 600]
としても同じ結果が得られますが、値の表示範囲が変わった際に自動的に対応してくれるlocatorを使うことをオススメします。
from matplotlib.ticker import MultipleLocator
fig, ax = plt.subplots(figsize=(6, 6))
sns.heatmap(flights, annot=True, fmt="d", linewidths=.5, ax=ax,
cbar_kws = dict(use_gridspec=False, location="top",
label='No. of passengers', ticks=MultipleLocator(200)))
ax.collections[0].colorbar.minorticks_on()
軸ラベルや目盛りラベルのフォントサイズは以下のように指定できます。マイナー目盛りを内向きにすると、表示レイヤーの順番のせいで目盛りがカラーバーの下に埋もれてしまうので、カラーバーが一番下のレイヤーに来るようにzorder
を最小値の0にしています。
cax = ax.collections[0].colorbar.ax # ColorbarオブジェクトからカラーバーのAxesオブジェクトにアクセス
# cax = fig.axes[-1] # これでもよい
cax.tick_params(which='minor', direction='in')
cax.tick_params(which='major', labelsize=20)
cax.xaxis.label.set_fontsize(20)
cax.collections[0].set_zorder(0) # colorbarの色部分を一番下のレイヤーに
何はともあれマニュアル
2018年7月にリリースされた0.9.0でマニュアル、特にintroductionが大幅に改善されたようです。これまで多くの人にとっての混乱のもとだったfigureレベル関数とaxesレベル関数の区別についても明記されるようになりました。この記事の例を追えば大抵のことはできるようになると思いますが、何はともあれ使いたい関数にどのようなパラメータがあるかドキュメントで確認しておきましょう。
seabornの見栄えの良さに酔わない
この記事のお題からはそれますが、日本語でseabornについて調べてみるとかっこいいから程度の理由でpallete
やhue
を濫用している人が多いのが気になりました。かっこいいからという理由だけの多色化は可視化のご法度です。seabornのドキュメントにも以下のように明記されています。
Choosing color palettes — seaborn 0.9.0 documentation
Color is more important than other aspects of figure style because color can reveal patterns in the data if used effectively or hide those patterns if used poorly.
(意訳)色使いは、きちんと使えばデータからパターンを浮かび上がらせ、下手に使えば逆にそれらを隠してしまうという意味で、プロットにおける他のどの要素よりも重要です。
多色化がご法度なのはグラフに限らずプレゼンテーションスライドでも一緒です。デザインやブランディングに力を入れている組織の人が作ったスライドを見れば、グラフを含めて色数が絞られているのに気づくでしょう。色の濫用は控えましょう。