添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
485
467

More than 5 years have passed since last update.

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

Last updated at Posted at 2019-01-04

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を使っています。

    %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とそれ以外(FacetGridPairGridClusterGrid)の2種類に分けられます。これはそれぞれの見た目からなんとなく想像できます。クリックすると大きい画像が表示されます。

    JointGridだけグリッド(碁盤の目)っぽくないですね。ソースコードを見ると、グリッドっぽい見た目の後者三つはまさにGridクラスを継承していますが、JointGridだけGridを継承していません6。この分類は後ほど説明するseabornでできる調整を知る際に必要な知識です。

    描画に使われたmatplotlibオブジェクトにアクセスする

    seabornがちゃちゃっと描いてくれたかっこいいグラフは(驚くことに)全てmatplotlibがベースになっています。では、seabornというラッパー(wrapper=包み紙)をはぎとって中にあるmatplotlibに触れるにはどうすればいいのでしょうか。

    figureレベル関数

    複数のグラフをだーっと並べてくれるfigureレベル関数はmatplotlibのFigureオブジェクトとAxesオブジェクトを関数内で自分で用意しているため7、ユーザーが用意したFigureAxesを指定することはできません。関数内で作られたFigureAxesは、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に何も指定しなかった場合はFigureAxesはそれぞれ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_titleax.set_xlabelなどが使えます。

    seaborn APIでは手の届かない見た目調整のエッセンスは実はこの部分で、あとは個々のmatplotlibオブジェクトに対して適切なメソッドを使うだけです。

    seaborn APIで設定できる項目

    seaborn APIではできないことの話に入る前にできることを確認します。まずaxesレベル関数にはマーカーや線のサイズなどプロット本体の設定項目、より具体的に言うとax.plotax.scatterに渡せる項目しかありません。これは「〜プロットと呼ばれる図を作る」というaxesレベル関数の役割を考えると理解できて、付属部品にすぎない軸ラベルやタイトルに関する設定はドキュメントを見てもありません。つまり、axesレベル関数の場合はマーカーや線などのプロット本体以外の調整は関数の戻り値であるmatplotlibのAxesを受け取ってそのメソッドを使えということです。

    一方、figureレベル関数の場合は戻り値の***Grid系オブジェクトのメソッドを使うと以下のようにいろいろな設定ができます。

    FacetGridPairGridClusterGridで使えるsetメソッド

    これは三つのオブジェクトが継承している大元のGridで定義されているメソッドですが、実際は全てのAxesに対してmatplotlibのAxes.setメソッドにパラメータをそのまま渡して実行しているだけです9Axes.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に対して適用されてしまうので、例えばxlabelylabelを設定すると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/ylimx/yscaleaspectの設定でしょう。

    FacetGrid独自のメソッド

    FacetGridを返すrelplotcatplotlmplotの場合、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

    colrowの両方を指定した場合のデフォルトタイトルは「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_xticklabelsset_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関数のheightaspectから計算された設定された高さと幅が設定されます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のFigureAxesにアクセスすることで自由に修正可能です。

    # 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で簡単に済むこともありますが、例えば「タイトルと軸ラベル」の最後に示したhuestyleを使った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キーワードのあるlmplotcatplotではlegend_out=Falseを指定した場合、左上の最初のAxesax.legendが実行されます。従ってLegendオブジェクトはax.legend_が保持しています。

    タイトルとタイトルもどき hueでしかグルーピングできないrelplot, linepltなどの凡例タイトルはLegendオブジェクトのset_titleメソッドを使った正式なタイトルです。しかし、huesizeにより複数のグルーピングを指定できる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でlocbbox_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.legendFigureオブジェクトに所属する凡例を作るメソッドであり、ax.get_legend_handles_labelsAxesオブジェクトに所属する凡例を対象とした便利メソッドだからです。そこで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オブジェクトを上書きすることなくそのまま使って位置を変更する方法を紹介します。実際にこの方法が必要な機会は少ないでしょうが、locbbox_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について調べてみるとかっこいいから程度の理由でpalletehueを濫用している人が多いのが気になりました。かっこいいからという理由だけの多色化は可視化のご法度です。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.
    (意訳)色使いは、きちんと使えばデータからパターンを浮かび上がらせ、下手に使えば逆にそれらを隠してしまうという意味で、プロットにおける他のどの要素よりも重要です。

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

  • 485
    467
    0

    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
    Sign up Login
    485
    467