seabornの細かい見た目調整をあきらめない

# はじめに
seabornの洗練されたスタイルで作ったグラフはとてもきれいです。見た目だけでなく、列の多いデータの全体像を把握するのにも威力を発揮します[^1]。特に適切に整形されたデータフレームを渡せばカテゴリの比較や全パラメータの相関を一瞥できる図が一瞬で作れる機能は、同等の図をmatplotlibで一から作る苦労を考えると驚愕に値します。データサイエンティストやkagglerに人気があるのも納得です。また、複雑なデータを扱っていないけど単に見た目の良いグラフを作りたいという人の要望にも簡単に答えてくれます。可視化のお作法的にも見た目的にもだいたい勝手にいい感じにしてくれる手軽さが売りのseabornですが、ときには自分で調整したくなるときもあります。matplotlibだと面倒な調整を手軽にやってくれるseabornらしいメソッドで解決できるならいいのですが、たまにseabornのベースであるmatplotlibの機能に直接アクセスする必要が生じます[^2]。「手軽に複雑できれいなグラフを作れる」という特徴がseabornの最大の魅力であることを考えると、多くのseabornユーザーにとってはmatplotlibのオブジェクトを直接触らないといけないタイプの調整は技術的にも心理的にもハードルが高いものでしょう。そこで本稿では、seabornでできる調整を公式ドキュメントより詳しく確認するとともに、いくつかの具体例を通してmatplotlibに直接触る必要がある細かい見た目調整のやり方を解説します。

# 目的と内容
この記事ではseabornできれいに作れる様々なプロットの紹介はしません。その代わりに、「手軽に複雑できれいなグラフを作れる」というseabornの特徴を無駄にしないためにmatplotlibに深入りするのを避けている人たち向けに、seabornに用意されているキーワードやメソッドでは調整できない部分のいじり方を紹介します。結局のところどの例もmatplotlibの見た目調整機能に行き着くのですが、"生"のmatplotlibに詳しくないseabornユーザーがああでもないこうでもないと迷うことなく最短経路で目的の機能にたどりつけるようになる内容です。また、seabornの機能で対応できる調整もまとめます。公式ドキュメントでもきちんと説明されていないものがあるのでこれだけでも有益に感じる方がいるかもしれません。

# こんな人向け
- seabornは常用してるけどたまに最後の調整がつらいと感じている。
- ドキュメントを精読したけど調整したい部分に関連する項目がなくてあきらめたことがある。
- `***Grid`系のメソッドの使い方がよくわからず使うのをあきらめたことがある。
- seabornに魅力を感じているけどお膳立てされたグラフの細かい調整に自信がないから妥協してmatplotlibを使い続けている。

# 環境
Python 3.6とJupyterを使っています。

```python
%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`オブジェクトについての理解が必須です。必ず[この記事]( https://qiita.com/skotaro/items/08dc0b8c5704c94eafb9)に一通り目を通してください。特にPyplotインターフェースとオブジェクト指向インターフェースの区別は必須です[^3]。具体例を色々挙げていますが、結局のところ全てに共通するのは「__使っているseaborn APIに応じた方法で変えたい部分のmatplotlibオブジェクトにアクセスし、当該オブジェクトのメソッドを実行する__」という点です。Pyplotインターフェースを使った方法でも対応できるものはありますが、複数のグラフをちゃちゃっと作ってしまうseabornの特性上、matplotlibのオブジェクトに関する理解がないと「今やりたい変更はPyplotインターフェースで大丈夫か？」という判断を下せないので、横着せずにオブジェクト指向インターフェースを使った方が良いです。

# seabornの便利プロット機能は何をしているのか
seabornの便利プロット関数たちは"figureレベル"と"axesレベル"の2種類に分けられます。
[Figure-level and axes-level functions]
(https://seaborn.pydata.org/introduction.html#figure-level-and-axes-level-functions)
これらはそれぞれmatplotlibの`Figure`オブジェクト全体を管理しているか、`Axes`オブジェクトのみを扱っているかの違いです[^4]。見分け方は案外簡単で、matplotlibの`Axes`オブジェクトを指定する`ax`キーワードをとるかどうかで判断できます。seabornの関数（ドキュメントではAPIと呼ばれる）のほとんどがaxesレベルです。数の少ないfigureレベル関数とその返り値のseabornオブジェクトを以下に列挙します。灰色がかかって見にくいですが、関数名と返り値はそれぞれのドキュメントにリンクしています。

- [`relplot`](https://seaborn.pydata.org/generated/seaborn.relplot.html)（0.9.0で実装） -> [`FacetGrid`](https://seaborn.pydata.org/generated/seaborn.FacetGrid.html)
- [`catplot`](https://seaborn.pydata.org/generated/seaborn.catplot.html)（0.9.0で`factorplot`から名称変更） -> [`FacetGrid`](https://seaborn.pydata.org/generated/seaborn.FacetGrid.html)
- [`jointplot`](https://seaborn.pydata.org/generated/seaborn.jointplot.html) -> [`JointGrid`](https://seaborn.pydata.org/generated/seaborn.JointGrid.html)
- [`pairplot`](https://seaborn.pydata.org/generated/seaborn.pairplot.html) -> [`PairGrid`](https://seaborn.pydata.org/generated/seaborn.PairGrid.html)
- [`lmplot`](https://seaborn.pydata.org/generated/seaborn.lmplot.html) -> [`FacetGrid`](https://seaborn.pydata.org/generated/seaborn.FacetGrid.html)
- [`clustermap`](https://seaborn.pydata.org/generated/seaborn.clustermap.html) -> `ClusterGrid`(なぜかドキュメントなし)

[公式ギャラリー](https://seaborn.pydata.org/examples/index.html)をみるとわかりますが、複数のグラフを一気に作ってくれるものはfigureレベル関数です。ここに挙げられていないものは全てaxesレベル関数です。

ちなみに、seabornの基本となるこれら二つのカテゴリについて言及している日本語の記事は以下の二つしか見当たりませんでした。ドキュメントを読んでる人もいると思うのですが、あまり日本語で情報発信する方は少ないようです[^5]。

- [Pythonでデータ分析実践演習 ~2016 New Coder Survey 編~ - Qiita](https://qiita.com/shizuma/items/448140c2da10b4616f50)
- [matplotlib + seaborn: Pythonでグラフ描画 - Heavy Watal](https://heavywatal.github.io/python/matplotlib.html)

# seabornの`***Grid`系オブジェクト
matplotlibのラッパーであるseabornが扱うオブジェクトのほとんどはmatplotlibの`Artist`オブジェクトです。しかし、seabornの目玉機能とも言える複雑なプロットを作ってくれるfigureレベル関数向けにだけseaborn独自の`***Grid`系オブジェクトが用意されています。`***Grid`系オブジェクトは`JointGrid`とそれ以外（`FacetGrid`、`PairGrid`、`ClusterGrid`）の2種類に分けられます。これはそれぞれの見た目からなんとなく想像できます。クリックすると大きい画像が表示されます。

| `JointGrid` | `FacetGrid` | `PairGrid` | `ClusterGrid` |
|:-----:|:-----:|:-----:|:-----:|
| <img src='https://seaborn.pydata.org/_images/joint_kde.png', width=200> | <img src='https://seaborn.pydata.org/_images/faceted_histogram.png', width=200> | <img src='https://seaborn.pydata.org/_images/pair_grid_with_kde.png', width=200> | <img src='https://seaborn.pydata.org/_images/structured_heatmap.png', width=200> |

`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`属性に保持されています。

```python
# 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
```
![download.png](https://qiita-image-store.s3.amazonaws.com/0/222849/b1edec0f-5e8b-9df8-2835-8cb64359bf5e.png)


## axesレベル関数
figureレベル関数とは異なり、axesレベル関数には`Axes`オブジェクトを指定できる`ax`キーワードがあります。`ax`の指定の有無に関わらず、描画に使った`Axes`が戻り値です。`ax`に何も指定しなかった場合は`Figure`と`Axes`はそれぞれPyplotインターフェースと同様にcurrent figureとcurrent axesが設定されます[^8]。

```python
# 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
```
![download.png](https://qiita-image-store.s3.amazonaws.com/0/222849/6ae62cb8-5df6-b4e7-d5e4-f328b0e1d95d.png)

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`自体がドキュメントの説明が[いい加減](https://matplotlib.org/api/_as_gen/matplotlib.axes.Axes.set.html)なせいかあまり知られていないようですが、`Axes`の[ドキュメント](https://matplotlib.org/api/axes_api.html#the-axes-class)に登場するすべての`set_***`系メソッドの`***`部分をパラメータとして一括設定できるそこそこ便利なメソッドです。よく使われる項目を例にすると以下のような感じで使えます。titleやxscaleなどにも使えます。

```python
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')
```
![download-4.png](https://qiita-image-store.s3.amazonaws.com/0/222849/8b4668b2-e11f-14d3-c218-a2c604416a47.png)

一見便利そうに見えますが、`Grid.set`は全ての`Axes`に対して適用されてしまうので、例えば`xlabel`や`ylabel`を設定するとseabornがせっかくいい感じに消してくれた軸ラベルも表示されてしまいます。

```python
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')
```
![download.png](https://qiita-image-store.s3.amazonaws.com/0/222849/1bdcf06b-6ff4-0a14-199a-f80d01f3e3e8.png)

これをmatplotlibのメソッドで修正するには、左端と下端の`Axes`を区別する条件をつけてラベルを消していく必要があり「せっかくseabornを使っているのに何をやってるんだろうか」とむなしくなります。`Grid.set`メソッドが適しているのはこのような影響を受けない`x/ylim`や`x/yscale`、`aspect`の設定でしょう。

## `FacetGrid`独自のメソッド
`FacetGrid`を返す`relplot`、`catplot`、`lmplot`の場合、`Grid.set`向きではないラベルやタイトルの設定には以下に挙げる[`FacetGrid`独自のメソッド](https://seaborn.pydata.org/generated/seaborn.FacetGrid.html)が便利です。

|メソッド|説明|
|:--|:--|
| `set_axis_labels([x_var, y_var])`| 左端と下端のラベルを設定 |
| `set_xlabels([label])` | 下端のラベルのみ設定 |
| `set_ylabels([label])` | 左端のラベルのみ設定 |
| `set_titles([template, row_template, …])` | 全ての`Axes`の上部またはグリッド上部や右側に列や行のカテゴリや値を示すタイトルを設定 |
| `set_xticklabels([labels, step])` | 下端の数字ラベルを設定 |
| `set_yticklabels([labels])` | 左端の数字ラベルを設定 |

一気に複数の部品を変更するため、`Axes`の似たようなメソッド名とは異なりどれも`set_複数形`になっていることに注意してください。しかし、実は注意が必要なのはメソッド名だけではありません。まずは特に注意の必要ない軸ラベルの変更をみてみましょう。

### `set_axis_labels`の例
軸ラベルの変更はメソッド名さえ知っていれば簡単です。

```python
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が返ってくるのでこのようなメソッドチェーンにもできる
```
![download-1.png](https://qiita-image-store.s3.amazonaws.com/0/222849/7b5f1c03-5d73-6c63-d78a-87fc6ae7ae10.png)

### `set_titles`の例
`FacetGrid`における各`Axes`のタイトルは元データの`DataFrame`の構造に忠実に従うべきという設計思想になっているようで、seabornで用意されている`FacetGridl.set_titles`メソッドはあまり融通が利きません[^10]。[ドキュメント](https://seaborn.pydata.org/generated/seaborn.FacetGrid.html)には使い方が書いていないので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.
```

実際に使って見ましょう。

```python
# 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))
```
![download-2.png](https://qiita-image-store.s3.amazonaws.com/0/222849/a095ca97-e42e-d4dc-fd74-96ef9092b97b.png)

`tips`という`DataFrame`の中身は以下のようになっています。
![スクリーンショット 2018-12-18 1.27.58.png](https://qiita-image-store.s3.amazonaws.com/0/222849/1d0b1f7a-f4d6-f6b7-d775-bb2d631a6927.png)
上の例の`g.set_titles("{col_var}={col_name} diners"))`では、size列の値ごとに作ったtipのヒストグラムに列の名前`col_var`と値`col_name`を使ったタイトルを付けています[^11]。

`col`と`row`の両方を指定した場合のデフォルトタイトルは「row名 = row値 | col名 = col値」です。

```python
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])
```
![download-3.png](https://qiita-image-store.s3.amazonaws.com/0/222849/be7ee348-c165-2504-7fa5-7b78b922a525.png)

このとき`set_titles`は以下のように使えます。

```python
g.set_titles(template='{row_var}: {row_name}\n{col_var}: {col_name}')
g.fig
```
![download-4.png](https://qiita-image-store.s3.amazonaws.com/0/222849/12eaa4bf-4185-7ec2-b148-c07589462acb.png)

`FacetGrid`を作る際に`margin_titles`キーワードを使った場合は以下のようなことができます。

```python
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])
```
![download-5.png](https://qiita-image-store.s3.amazonaws.com/0/222849/109d97de-2a7a-3022-fa28-396d67bb79cf.png)

```python
g.set_titles(row_template='{row_var}: {row_name}', col_template='{col_var}: {col_name}')
g.fig
```
![download-6.png](https://qiita-image-store.s3.amazonaws.com/0/222849/566a20c8-1f1f-4c35-7f85-c08834368af7.png)

### `set_xticklabels`、`set_yticklabels`の例
`set_titles`ではseabornの設計思想に注意する必要がありましたが、`set_xticklabels`/`set_yticklabels`はmatplotlibの`Tick`の仕様に注意する必要があります。

```python
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', ''])
```
![download-7.png](https://qiita-image-store.s3.amazonaws.com/0/222849/bf60c940-c0ff-639c-76e7-9c49fd982430.png)

`set_xticklabels`に渡しているリストの最初と最後に謎の空string要素があります。これは描画範囲の両端の外側に一つずつ見えない`Tick`が設定されるmatplotlibの謎仕様に対応するためです。これは以下のようにするとわかります。

```python: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
```

```python: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`独自メソッド
[ドキュメント](https://seaborn.pydata.org/generated/seaborn.JointGrid.html)をみると`JointGrid`にも`set_axis_labels`メソッドがあります。メインプロットの軸ラベルを変えるだけのメソッドなので特に便利というわけでもありません。

```python
# 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)')
```
![download-8.png](https://qiita-image-store.s3.amazonaws.com/0/222849/ee6a1ef3-f67b-fd5d-0a3d-0e17b7008863.png)




# seaborn APIで設定できない項目の具体例
## 画像サイズ
これはすでに今までの例でもしれっと使っていました。axesレベル関数とfigureレベル関数で少し違います。axesレベル関数の場合はプロット関数実行前にあらかじめ画像サイズを定義できます。

```python
# axを渡す場合
fig, ax = plt.subplots(figsize=(8, 3))
sns.lineplot(x="timepoint", y="signal",
             hue="region", style="event",
             data=fmri, ax=ax)
```
```python
# 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)
```
![download-3.png](https://qiita-image-store.s3.amazonaws.com/0/222849/740d941a-ea6d-376c-b045-8d78ded6e48d.png)

`ax`キーワードで`Axes`オブジェクトを指定しない場合は、matplotlibのデフォルト設定に従うので`plt.rcParams['figure.figsize']=(8, 3)`としても上と同じ結果が得られます。ただし、当然ながらデフォルト設定を変えるとこの後に作った図のサイズも影響を受けます。

figureレベル関数の画像サイズには`plt.rcParams['figure.figsize']`は使われずseaborn関数の`height`と`aspect`から計算された設定された高さと幅が設定されます[^12]。従ってfigureレベル関数の画像サイズは描画時に指定するか、描画後に変更することができます。ただし、seabornの関数で指定するのはプロット一つずつのサイズなので実際の画像のインチ幅はheight*aspect*プロット列数、高さはheight*プロット行数になります。その他にも以下に示すようにサイズ指定タイミングが描画時と描画後かによって、プロット間の余白やtickラベルなど細かいパーツの扱いが変わってくるので少し試行錯誤する必要はあるかもしれません。

```python
grid = sns.lmplot(x="total_bill", y="tip", col="time", hue="smoker",
                  data=tips, legend_out=False)
grid.fig.set_size_inches((4, 3))
```
![download-4.png](https://qiita-image-store.s3.amazonaws.com/0/222849/dc98f5ee-0eee-1ae1-ec1b-98631a67f91f.png)


```python
# 細かい部分を気にせずに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)
```
![download-5.png](https://qiita-image-store.s3.amazonaws.com/0/222849/267b7465-d642-1360-965f-b86dd343f5bc.png)

このほかにもポスター向けやスライド向けに画像サイズとフォントサイズを同時に調整してくれる`set_context`というseabornらしいメソッドもあります。詳細は以下をご覧ください。
[seabornでMatplotlibの見た目を良くする | note.nkmk.me](https://note.nkmk.me/python-matplotlib-seaborn-basic/)

上の例では問題はありませんでしたが、`grid.fig.set_size_inches`で画像サイズを変えると凡例が微妙な位置にくることもあります。その際は後述する方法で凡例をいい位置に動かせば解決します。

## タイトルと軸ラベル
`FacetGrid`のタイトルは元データのカラム名と値をベースにしたものしかつけられないという制限がありました。また、axesレベル関数にはそもそもタイトルがつきません。一般に我々ユーザーはわがままですから、制限なく自由に設定できる方法は知っておくと良いでしょう。タイトルや軸ラベルはmatplotlibの`Figure`や`Axes`にアクセスすることで自由に修正可能です。

```python
# 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)')
```
![download-1.png](https://qiita-image-store.s3.amazonaws.com/0/222849/56e510f2-5ea4-436c-005d-3c93c1898e10.png)

`Figure`オブジェクトは`plt.gcf()`でも取得可能ですが、figureレベル関数で描画した後でないと意図通りに動きません。

```python
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)
```
![download-2.png](https://qiita-image-store.s3.amazonaws.com/0/222849/9ddb4177-4fe4-a6e3-34dc-c8035974efda.png)

axesレベル関数の場合はPyplotインターフェースでもオブジェクト指向インターフェースでも同じことができます。

```python
# axesレベル関数の例
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
```
```python
# 出力される図は上と同じ
sns.lineplot(x="timepoint", y="signal",
             hue="region", style="event",
             data=fmri)
# current axesに対する設定
plt.title('title test')
plt.xlabel('time (msec)')
```
![download-3.png](https://qiita-image-store.s3.amazonaws.com/0/222849/e416950c-bad1-56cb-2d08-25e80374b9d3.png)


## 凡例
調整したくなるグラフのパーツランキングがあれば間違いなく上位に入るであろう凡例は、いろいろといい感じにしてくれるseabornでも（いろいろと勝手にやってしまうが故に）手を加えたくなるもののようです。Stack Overflowにも[位置を変えたい](https://stackoverflow.com/questions/27019079/move-seaborn-plot-legend-to-a-different-position/)、[ダブりを消したい](https://stackoverflow.com/questions/35538882/seaborn-boxplot-stripplot-duplicate-legend)、[凡例自体を消したい](https://stackoverflow.com/questions/38131062/attributeerror-unknown-property-legend-in-seaborn)、[テキストを変えたい](https://stackoverflow.com/questions/45201514/edit-seaborn-legend)、[タイトル（もどき）を消したい](https://stackoverflow.com/questions/51579215/remove-seaborn-lineplot-legend-title)など、凡例に関して思い浮かぶ調整例がだいたい出てきます。これらの質問に対する解決方法は以下の二つの戦略のどちらかに従うか両方のいいとこ取りをしています[^13]。

1. matplotlibの`legend`メソッドを直接使って一から自分で作る（seaborn内で凡例を作らないようにするか作った後に上書き）
2. 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の[この回答](https://stackoverflow.com/a/51579663/9131000)で指摘されている通り、マーカーのない文字のみの凡例です。つまり`ax(fig).legend`メソッドの`title`キーワードや`Legend`オブジェクトの`set_title`メソッドでは変更できません。

___figureレベル関数の凡例の位置___ figureレベル関数がデフォルトで凡例作成に使っている`fig.legend`は、figureレベル関数内で作成された`Figure`オブジェクトのサイズから右端中央のちょうどよい位置を算出して凡例を配置しています。これはつまり、プロット作成後に画像サイズを変えると凡例が不恰好な位置にくる可能性が高いことを意味します。

以上の注意点を踏まえてStack Overflowの凡例関連の質問や以下の例を見ると、seabornの凡例調整が一気にクリアになると思います。以下ではよくある三つの調整（タイトル、位置、ラベル）を扱います。

### タイトルまたはタイトルもどきを変える
グルーピングに`hue`のみを使うAPIの場合は正規の凡例タイトルを変えれば良いです。

```python
# グルーピングが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')
```
![download.png](https://qiita-image-store.s3.amazonaws.com/0/222849/d4950c4f-2bd5-5df0-2967-4516d41efa67.png)
三つめの注意点の通り、画像サイズを変更したため凡例がおかしな位置にきています。これを修正する方法は後述します。

複数のグルーピング基準を設定できるAPIの場合は、タイトルもどきの正体である凡例の`label`を変える必要があります。`label`の実体は`Legend.texts`リストに入っている`Text`オブジェクトなので、`set_text`メソッドを使って該当要素を変更します。

```python
# 複数グルーピングのある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
```
![download-1.png](https://qiita-image-store.s3.amazonaws.com/0/222849/e2862ca5-aa82-a3f0-e30e-377878ec111b.png)

axesレベル関数でも`Legend`オブジェクトを取得する場所が変わっているだけでやることは同じです。

```python
# 複数グルーピングのある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
```
![download-2.png](https://qiita-image-store.s3.amazonaws.com/0/222849/4ddd14f6-a1d8-186f-9204-9e2bd0642eb3.png)

### 凡例の位置を変える
seabornの各関数の凡例に関する部分のソースを確認すると、axesレベル関数の場合は凡例の位置にはキーワードなしの`ax.legend`メソッドが実行されているのでデフォルトの`loc="best"`が指定されています。figureレベル関数の場合は`fig.legend`メソッド実行時に`loc="center right"`が指定されていています[^15]。これらを変更するキーワードはseabornには用意されていません。つまりデフォルトの位置で納得がいかない場合は、matplotlibの`Legend`オブジェクトにアクセスする必要があり、手軽なものから順に以下のような方針があり得ます。

 1. seabornで作った凡例の`_loc`属性を変える方法[^16]
 2. 位置を指定して新しく作り直す方法
    1. 簡単に作れる場合
    2. ハンドルとラベルを引き継いだ方が良い場合
 3. 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"`によって凡例は左上に配置されています。

```python
# 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)
```
![download-6.png](https://qiita-image-store.s3.amazonaws.com/0/222849/0eb16da6-5397-70ad-a6aa-56cf99c58cb5.png)

まずは一番手軽な`_loc`を変更する方法で右下に動かします。`legend`メソッドで`loc`を指定する際には`"lower right"`などの文字列が使えましたが、`_loc`では数字しか指定できません。文字列と数字の対応は[ドキュメント](https://matplotlib.org/api/_as_gen/matplotlib.pyplot.legend.html#matplotlib.pyplot.legend)を参照してください。

```python
grid.fig.axes[0].legend_._loc = 4 # lower right
grid.fig.suptitle('ax.legend_._loc = 4 (lower right)', y=1.1)
grid.fig
```
![download-7.png](https://qiita-image-store.s3.amazonaws.com/0/222849/0511822e-2c26-b029-1865-2477f519df9c.png)

次は、seabornが`ax.legend`メソッドで作った凡例を、自分で位置指定をした`ax.legend`メソッドで上書きする方法です。`Axes`オブジェクトは一つしか凡例を持てないので、もう一度`ax.legend`メソッドを実行すると先に作ったものが上書きされる仕様を利用しています。

```python
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
```
![download.png](https://qiita-image-store.s3.amazonaws.com/0/222849/7168e4d1-51fe-843e-3146-36f8dd54bf1a.png)

複数グルーピングされタイトルもどきを含むプロットも場所は`loc="best"`で制御されています。以下の例の場合は右上に配置されています。

```python
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"')
```
![download-1.png](https://qiita-image-store.s3.amazonaws.com/0/222849/5d88a969-14e9-6279-f790-9565b5fd9fa3.png)

この凡例を上書きで位置を指定する場合は、seabornが作ったタイトルもどきなどをそのまま利用するのがよいでしょう。`ax.get_legend_handles_labels`メソッドによってhandle（線やマーカー）とlabel（文字列）を取得して`ax.legend`に渡すと、面倒なタイトルもどきの部分を自分で作ることなく位置を変更できます。

```python
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')
```
![download-2.png](https://qiita-image-store.s3.amazonaws.com/0/222849/4da591df-4269-7941-55e3-06188571ee23.png)

凡例の背景色や枠線を消すために`frameon=False`としています。

次に、以下のようにfigureレベル関数の画像サイズを変更したら凡例が微妙な位置にきてしまったケースで凡例の位置を修正します。

```python
grid = sns.relplot(x="total_bill", y="tip", col="time",
            hue="smoker", size="size",
            data=tips)
grid.fig.set_size_inches((8, 3))
```
![download-3.png](https://qiita-image-store.s3.amazonaws.com/0/222849/8c2d5824-8666-c5b8-3538-a48fec6bb44e.png)

画像サイズ変更前と同じような位置に戻してみましょう。今回の凡例にもタイトルもどきが含まれるので、handleとlabelを拝借する方針でいきます。しかし、`fig.legend`メソッドで作った凡例のhandleとlabelを取得するのに、先の例で使った`ax.get_legend_handles_labels`メソッドは使えません。なぜなら`fig.legend`は`Figure`オブジェクトに所属する凡例を作るメソッドであり、`ax.get_legend_handles_labels`は`Axes`オブジェクトに所属する凡例を対象とした便利メソッドだからです。そこで`Legend`オブジェクトの属性からhandleとlabelを別々に取得します。

```python
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
```
![download-4.png](https://qiita-image-store.s3.amazonaws.com/0/222849/35b0aef8-5484-e08d-e555-9c98512f3945.png)

ところで、`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`です。

```python
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'])
```

```python
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)
```
![download-6.png](https://qiita-image-store.s3.amazonaws.com/0/222849/4ae2630f-007b-6f05-5de2-b66e953f9ee4.png)

最後に、seabornが作った`Legend`オブジェクトを上書きすることなくそのまま使って位置を変更する方法を紹介します。実際にこの方法が必要な機会は少ないでしょうが、`loc`や`bbox_to_anchor`キーワードをどう指定すればいいか理解する助けにはなるでしょう。

```python
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)
```
![download-9.png](https://qiita-image-store.s3.amazonaws.com/0/222849/a78cbd2b-fb2e-1b4d-9e53-80a1998e0c39.png)

詳細はコードのコメントをみてください。`bbox_to_anchor`はそのままで`_loc`を左上に変更するとこれらの役割がより明確にわかるでしょう。

```python
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
```
![download-10.png](https://qiita-image-store.s3.amazonaws.com/0/222849/f725f24f-08bb-a1db-a8b8-88eccbdde6c5.png)

### 凡例のラベルを変える
「凡例のラベル」というのは、直前のグラフでいうところのYesとNoです。seabornではDataFrameの保持する値が凡例のラベルにそのまま使われるので、seabornの設計思想に則ってラベルを変えるならDataFrameの値を変えるべきです。しかし、なんらかの都合により元データを変更したくない場合は、DataFrameはそのままで可視化の段階で凡例のラベルを変えることもできます。凡例のラベルの実体である`Text`オブジェクトの`set_text`メソッドで変更します。

```python
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
```
![download-11.png](https://qiita-image-store.s3.amazonaws.com/0/222849/2d8d36dd-9c79-ec3f-4174-24f5af48c8f0.png)


## カラーバー
matplotlibのカラーバーの振る舞いは基本を踏まえていないと何が起こっているか非常にわかりづらいため、細かいところが気になりだすと解決するのに平気で数時間かかることがあります。matplotlibを意識せずともカラーバーを作ってくれるseabornの場合はユーザーにとっては輪をかけてわかりづらいパーツでしょう。Stack Overflowにも[seabornユーザーによるカラーバー関連の質問は多い](https://stackoverflow.com/search?q=seaborn+colorbar)です。以前私が書いた[カラーバー関連の記事](https://qiita.com/skotaro/items/01d66a8c9902a766a2c0)を見るとわかる通り、細かいことを述べ出したらキリがないので、ここでは、修正頻度が高いだろうカラーバーの位置、タイトル、目盛り位置、目盛りラベル関連の例を示します。

まずはデフォルト設定でヒートマップを作ってみます。

```python
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)
```
![download-12.png](https://qiita-image-store.s3.amazonaws.com/0/222849/221b2013-5e88-2dc5-8350-36cfb09e45ee.png)

`heatmap`の[ドキュメント](https://seaborn.pydata.org/generated/seaborn.heatmap.html)を見ると`cbar_kws`というのがあります。ここで辞書型パラメータを指定することでmatplotlibの`fig.colorbar`にパラメータを渡すことができます。以下の例ではカラーバーの位置を上部にし、カラーバーのタイトル（実際は軸ラベル）をつけて、マイナー目盛りをつけました。

```python
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()
```
![download-13.png](https://qiita-image-store.s3.amazonaws.com/0/222849/399bfb1e-e4c7-c804-3c67-2f5ed6c8dedd.png)

`location`を指定するには`use_gridspec=False`である必要があるので注意してください[^18]。

メジャー目盛りの位置を指定するには手動かlocatorを使う方法があります。以下の例は`ticks=[200, 400, 600]`としても同じ結果が得られますが、値の表示範囲が変わった際に自動的に対応してくれるlocatorを使うことをオススメします。

```python
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()
```
![download-14.png](https://qiita-image-store.s3.amazonaws.com/0/222849/8a663e23-126f-5561-4513-14e13334a579.png)

軸ラベルや目盛りラベルのフォントサイズは以下のように指定できます。マイナー目盛りを内向きにすると、表示レイヤーの順番のせいで目盛りがカラーバーの下に埋もれてしまうので、カラーバーが一番下のレイヤーに来るように`zorder`を最小値の0にしています。

```python
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の色部分を一番下のレイヤーに
fig
```
![download.png](https://qiita-image-store.s3.amazonaws.com/0/222849/74cd2f4e-40ee-e814-a477-f397b4e42e21.png)


# 何はともあれマニュアル
2018年7月にリリースされた0.9.0でマニュアル、特に[introductionが大幅に改善](https://seaborn.pydata.org/whatsnew.html#documentation-improvements)されたようです。これまで多くの人にとっての混乱のもとだったfigureレベル関数とaxesレベル関数の区別についても明記されるようになりました。この記事の例を追えば大抵のことはできるようになると思いますが、何はともあれ使いたい関数にどのようなパラメータがあるかドキュメントで確認しておきましょう。

# seabornの見栄えの良さに酔わない
この記事のお題からはそれますが、日本語でseabornについて調べてみるとかっこいいから程度の理由で`pallete`や`hue`を濫用している人が多いのが気になりました。かっこいいからという理由だけの多色化は可視化のご法度です。seabornのドキュメントにも以下のように明記されています。
[Choosing color palettes — seaborn 0.9.0 documentation](https://seaborn.pydata.org/tutorial/color_palettes.html)

>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.
>（意訳）色使いは、きちんと使えばデータからパターンを浮かび上がらせ、下手に使えば逆にそれらを隠してしまうという意味で、プロットにおける他のどの要素よりも重要です。

多色化がご法度なのはグラフに限らずプレゼンテーションスライドでも一緒です。デザインやブランディングに力を入れている組織の人が作ったスライドを見れば、グラフを含めて色数が絞られているのに気づくでしょう。色の濫用は控えましょう。


[^1]: Qiitaでは[こちら](https://qiita.com/HiromuMasuda0228/items/babdfa175815fb3e3a94)や[こちら](https://qiita.com/hik0107/items/3dc541158fceb3156ee0)などが有名でしょうか。最近投稿された[こちら](https://qiita.com/ninomiyt/items/cda7ee0b940dd461cd09)や[こちら](https://qiita.com/T_Shinomiya/items/fb5ea81453992dfe2add)は網羅的ですね。
[^2]: [公式ドキュメント](https://seaborn.pydata.org/introduction.html)にも_"Many tasks can be accomplished with only seaborn functions, but further customization might require using matplotlib directly." (訳:だいたいseabornだけで大丈夫だけど凝った調整は直接matplotlibを使わないとダメかも)_と書いてあります。
[^3]: 問題に遭遇するたびにググって一本釣りした対症療法から断片的な知識を得てどうにかやっている人にとっての最後の砦であるStack Overflowにも質問・回答共にこれらを混同している例があります。
[^4]: ここを把握していないと混乱しやすいです。たとえばStack Overflowの[この回答](https://stackoverflow.com/a/46308024/9131000)は典型的な間違いの例で、なぜかベストアンサーになってますが質問者の問題は解決できていません。
[^5]: 直前までまとめてあきらめてしまっていた[記事](https://qiita.com/nj_ryoo0/items/9105ddfdf1b08b58398e)もありました。
[^6]: `Grid`、`FacetGrid`、`PairGrid`、`JointGrid`のコードは[axisgrid.py](https://github.com/mwaskom/seaborn/blob/master/seaborn/axisgrid.py)、`ClusterGrid`のコードは[matrix.py](https://github.com/mwaskom/seaborn/blob/master/seaborn/matrix.py)にあります。
[^7]: 例えば`FacetGrid`のコードの該当部分は[こちら](https://github.com/mwaskom/seaborn/blob/f0349b1a7f1e022cb1da8b94f2e019f04eeed397/seaborn/axisgrid.py#L299-L329)
[^8]: 例えば`regplot`のコードの該当部分は[こちら](https://github.com/mwaskom/seaborn/blob/f0349b1a7f1e022cb1da8b94f2e019f04eeed397/seaborn/regression.py#L783-L784)。current axesを設定しているため、明示されていなくてもcurrent figureが使われています。
[^9]: [このコード](https://github.com/mwaskom/seaborn/blob/master/seaborn/axisgrid.py)の`Grid`の`def set`の部分を参照。
[^10]: これは「`FacetGrid`のタイトルを変えたいけどうまくいかない」というStack Overflowの質問への[回答](https://stackoverflow.com/a/31688467/9131000)でseaborn作者のmwaskomが「データのカラム名そのものを変えるべき、無理ならmatplotlibで直に変えて」と言っていることからも分かります。可視化に伴うヒューマンエラーが減るだろうということみたいです。
[^11]: `*_var`が名前で`*_name`が値なのは直感的ではないので、間違って設定されている気もします。そのうち修正されるかもしれません。
[^12]: 例えば`relplot`なら`height=5, aspect=1`が[デフォルト](https://seaborn.pydata.org/generated/seaborn.relplot.html#seaborn.relplot)です。
[^13]: データ構造に従った可視化を重要視しているseabornの設計思想からすると、見た目の一つである凡例を変えたいならデータの表現を整えるべきということになりますが、データの表現（例えば1/0とするかYes/Noとするか）はデータサイズや諸々の事情によりそう簡単に変えられるものでなかったりするので、データをいじらずに見た目を調整することが理にかなっていることもあると思います。
[^14]: Stack Overflowではこれらのプロットで`fig.legend`により作られた凡例を`grid._legend`で取得するという回答が多いですが、アンダースコアが付いているということは[プライベート変数](https://docs.python.jp/3/tutorial/classes.html#private-variables)なので利用が推奨されるものではないはずです。本稿で紹介している例のようにpublicな`grid.fig.legends`から取得しましょう。
[^15]: [`Grid`オブジェクトの`add_legend`メソッド](https://github.com/mwaskom/seaborn/blob/master/seaborn/axisgrid.py)を読むと、ひとまずcenter rightに配置した後、凡例がプロットと重ならないように画像の幅を合わせていることがわかります。
[^16]: どうも凡例の位置を指定する`loc`キーワードはプライベート変数である`_loc`でしか変更できないようです。
[^17]: SOの[この回答](https://stackoverflow.com/questions/23238041/move-and-resize-legends-box-in-matplotlib)を参考にしました。
[^18]: そもそも`location`キーワードの存在自体が`fig.colorbar`の[ドキュメント](https://matplotlib.org/api/_as_gen/matplotlib.figure.Figure.html#matplotlib.figure.Figure.colorbar)に書かれておらず、`fig.colorbar`が内部で呼んでいる`make_axes`メソッド（[ドキュメント](https://matplotlib.org/api/colorbar_api.html#matplotlib.colorbar.make_axes )）と`make_axes_gridspec`メソッド（[ドキュメント](https://matplotlib.org/api/colorbar_api.html#matplotlib.colorbar.make_axes_gridspec)）を自分で探し出すか、Stack Overflowの[この回答](https://stackoverflow.com/a/47916308/9131000)などを（幸いにも）見つけだす必要があります。