seabornでset_theme() を使っても結局ヒートマップの枠線が消された悲しさを強引に解決(plt.imshow() 不使用)※強引じゃないバージョンもあるよ
ヒートマップをseabornで書きたかった(plt.imshow()
で細かい設定するのがめんどくさそうで逃げた)
色々事情があってオーバードクターになり、必死に投稿論文を書いています(何年やってるんだ…?)。
それ故Figを作成しなくてはいけないのですが、データ量が多かったりVBAがさっぱり分からなかったり(昔パワポ用のマクロ作ったことありますが物凄く悪戦苦闘した思い出しかない)といった事情のため、基本的にJupyterLab上でmatplotlib、seabornを使ってFigを作成しています*1。
ヒートマップはplt.imshow()
を利用するパターン(Creating annotated heatmaps — Matplotlib 3.5.3 documentation)ではなく、seabornにてを描画していたんですが、どうしてもseabornの性質上「いい感じにしてくれる」せいで枠線消えちゃうんですよね、デフォルトだと。枠を付けたいならplt.imshow()
で書けよって話なんですが、個人的に設定したい部分を手軽に弄るのにはseabornを使いたかったので(見出しにもありますがimshow()
から逃亡)、なるべく少ないコードで枠を付けたかったんですが、結構格闘しました。以下、超初心者の格闘記録をば。ここに行きつくまで何時間使ったんだろうこの人は*2。もしかしてplt.imshow()
でのヒートマップ描画(自分用パラメーターのセットも含む)を習得した方が早かったのでは説もあるけど考えないことにします。
set_theme()
をも超えていくヒートマップ枠線問題
(ほぼ)デフォルト設定にて描画
とりあえず、一旦sns.reset_defaults()
にてrcParams
をデフォルトに戻した状態でヒートマップを描画してみます…って嘘つきました。カラーマップだけBlues
にしています(データはseaborn.heatmap — seaborn 0.12.0 documentationにある方法で生成)。
比較としてデータが空のAxes (title = Axes 2)
を右に並べました。カラーバー含めて2つのAxes
がなんとなく揃っている感を出すために、plt.subplots()
の引数にsharey=True
を入れたり、ax2.set_aspect('auto', share=True)
とか無茶やっているのでheatmapのsquare=True
が効かない状態です。という事で良い子はうかつにマネしないでください(僕はいつもカラーマップを別なAxes
でプロットしているのですが、今回は横着してやってないです…w)。
from __future__ import print_function %matplotlib inline import jupyter_console from matplotlib import pyplot as plt import numpy as np; np.random.seed(0) import warnings warnings.filterwarnings("ignore", category=DeprecationWarning) import ipykernel import pandas as pd import seaborn as sns
fig, (ax1, ax2) = plt.subplots(figsize=(8,3), ncols=2, sharey=True) uniform_data = np.random.rand(10, 12) ax1 = sns.heatmap(uniform_data, cmap='Blues', ax=ax1, square=True, cbar=True, ) ax1.set_yticklabels(labels=ax1.get_yticklabels(), rotation=0) ax1.set_title("Axes 1 (sns.heatmap() )") ax2.set_aspect('auto', share=True) ax2.set_title("Axes 2 (only matplotlib)") plt.tight_layout() plt.show()
Axes 2
はmatplotlib単体でプロットされているため、現在のrcParams
がそのまま適用されることで枠線などが出てきます。一方、sns.heatmap()
を用いたAxes 1
では枠線が消えています。seabornが「いい感じにしてくれた」事でrcParams
の設定が上書きされてheatmap用の設定になっているみたいです。
(なるべく楽に)枠線を付けたい→ sns.set_theme()
との出会い
さてここに枠線を付けたいときにどうすればいいか、とりあえずの理想は「デフォルトの設定を弄って何とかならないか?」という事だったのでドキュメントを見てそれっぽい関数が無いか探してみました。するとsns.set_theme()
(seaborn.set_theme — seaborn 0.12.0 documentation)を発見。リンク先をざっくり見た感じ、matplotlib単体でのプロットを含めたデフォルト設定を永続的に変更する(seabornのスタイルに手を加えることもできる)ものっぽい*3。確かにリンク中の
The seaborn theme is decomposed into several distinct sets of parameters that you can control independently:
という説明及びその直下にあるプロット例、そして最後にある
custom_params = {"axes.spines.right": False, "axes.spines.top": False} sns.set_theme(style="ticks", rc=custom_params) sns.barplot(x=["A", "B", "C"], y=[1, 3, 2])
というサンプルコードで枠線の制御をしているのを見るに「set_theme()
を使えば枠線問題何とかなるんじゃね?」となったので試してみました。
いやなんで枠だけ付かねえんだよw(指定キーワードの問題?)
まあその、この記事を書いている時点でお察しなのですが、この試みは失敗に終わっていますw
とりあえず先ほどのAxes 1
、Axes 2
をアレンジした実例を...。
# ↓辞書記述の順番とか適当なのはご愛敬…。 rcstyle = { 'axes.spines.right': True, 'axes.spines.left': True, 'axes.spines.top': True, 'axes.spines.bottom': True, # 上下左右の枠線追加 'axes.linewidth': 1.5, 'axes.edgecolor': 'k', 'xtick.bottom': True, 'ytick.left': True, # 下と左に目盛線追加 'axes.labelsize': 'medium', 'axes.titlesize': 'large', 'xtick.color': 'k', 'ytick.color': 'k', 'ytick.labelcolor': 'k' } sns.set_theme(context='notebook', style='darkgrid', # 本来はspines(枠線), ticks(軸目盛線)なし rc=rcstyle, # これでspinesとticksが入るはず ) fig, (ax1, ax2) = plt.subplots(figsize=(8,3), ncols=2, sharey=True) uniform_data = np.random.rand(10, 12) ax1 = sns.heatmap(uniform_data, cmap='Blues', ax=ax1, square=True, cbar=True, ) ax1.set_yticklabels(labels=ax1.get_yticklabels(), rotation=0) ax1.set_title("Axes 1 (sns.heatmap() )") ax2.set_aspect('auto', share=True) ax2.set_title("Axes 2 (only matplotlib)") plt.tight_layout() plt.show()
Axes 1 (heatmap)
さん?
いや何が違うんですか?一応タイトルのフォントサイズでかくするのが反映されているような気がするくらいですかね。Axes 2
が狙い通りの設定で、プロットエリアはseabornの'darkgrid'スタイルを保ちつつ、darkgridに本来ない枠線と軸目盛線が入っています。
「もしかしてseabornのheatmap描画時は軸周りのrcParams
が反映されないのか?」と思い、
'xtick.bottom': True
、'ytick.left': True
のところをコメントアウトして軸目盛線を無効化すると
いやそれは反映されるのかよ!そういや後述の方法で軸線を付けた時、線の色はrcParams
ベースの設定が反映されていたなあと思い出しました。ちなみにヒートマップのカラーバーを消しても変化なし。
たかがプロット、されどプロット。「matplotlibとseaborn分かんねえなやっぱり(個人的にExcelとかよりはやりやすいけど)」と思いつつも試行錯誤した結果2つの結論に行きつきました。
暫定的和解策
1. sns.despine()
の目的外利用(?)
注: 普通は絶対2の方がいいですw 発見した順から正直に記載している都合でこっちを1番目にしています。
懲りずにseabornのドキュメントを読んでいたらどういうわけか見つけてしまった物体。それが、sns.despine()
です。
seaborn.despine — seaborn 0.12.0 documentation内の
Remove the top and right spines from plot(s).
という記述にもある通り、基本的には上と右の枠線を消すために存在しているメソッドです。が、オプションで上下左右の軸線をどうするか指定できるので、それを利用して逆に軸線を付加することもできます。
# 前略 ax1 = sns.heatmap(uniform_data, cmap='Blues', ax=ax1, square=True, cbar=True, ) ax1.set_yticklabels(labels=ax1.get_yticklabels(), rotation=0) ax1.set_xticklabels(labels=ax1.get_xticklabels(), rotation=0) # これ↓ sns.despine(ax=ax1, right=False, top=False, left=False, bottom=False) ax1.set_title("Axes 1 (sns.heatmap() )") # 後略
我ながら酷いやり方ですが、一応ワンライナーで書けるし枠線付いたんでヨシ!2番目の正当な使い方を学習したので、多分もう使いませんが。
2. spines[''].set_visible(True)
をリストで記述 (['']内にはleftなどが入る)
多分一番シンプルなのがこれですね。
実をいうと前々から軸線の追加にあたり、spines['left'].set_visible(True)
などは使っていたんですが、今回何があれでここに書いているかというと…。実は僕、今までこれをリストで記述可能と知らず、left
, right
, top
, bottom
それぞれに関して4行で書いていたんですよ。いやあもうその…。とりあえず、なぜこれを知らなかったと凹み半分、これで楽になるなと喜び半分でドキュメントのサンプルコードを載せます。(以下2つともmatplotlib.spines — Matplotlib 3.5.3 documentationより)
- 通常のリスト形式
spines[['top', 'right']].set_visible(False)
- そして、リストで行けるということは…スライスで全指定も…。はい…。
spines[:].set_visible(False)
ということで、スライス全指定が一番平和そうですね。今回のケースに適用してみます。
# 前略 ax1 = sns.heatmap(uniform_data, cmap='Blues', ax=ax1, square=True, cbar=True, ) ax1.set_yticklabels(labels=ax1.get_yticklabels(), rotation=0) ax1.set_xticklabels(labels=ax1.get_xticklabels(), rotation=0) # スライスで全指定 ax1.spines[:].set_visible(True) ax1.set_title("Axes 1 (sns.heatmap() )") # 後略
こちらもsns.despine()
の無茶苦茶な使い方をした1と同様しっかりと枠が付きました。
なお、カラーバーに枠線が付かないですが、subplot
で別なAxes
にカラーバーをプロットして、同様のことをすれば付くかと思います(今回は作業時間等の都合上やってないですが)
JupyterLabのShow Contextural Help
でspines
のところクリックしてようやく気が付くという。もっとpythonの勉強をしようと思います。…ん?あれ?あ、もちろん論文書きます。論文優先です。論文。いやでも、Fig作成がこれで捗るようになったので(そのためにかかった時間は、サンクコスト…って扱うには掛かりすぎたな)。
ちなみに上記サンプルコード等が載っているページの大元(matplotlib.spines — Matplotlib 3.5.3 documentation)を見ていたら、matplotlib.spines.Spine
クラスのところにはpatch
がどうこうと書いてあったのですが、rcParams
周りの設定で'patch.edgecolor': 'black'
、'patch.force_edgecolor': True
を設定しても結局ヒートマップに枠線付けられなかったんですよね。ただ、今回設定したのはmatplotlib.spines.Spines
クラス(Spine
ではなくSpines
)で、そっちはcollections.abc.MutableMapping
関連のようなのでpatchは意味がなかったんでしょう、おそらく。そこら辺を弄ればデフォルトでseabornのheatmapにも枠線がつけられるのかもですが、さすがにこれ以上深追いする気力と時間は無いですね…。今のところ、ワンライナーで書ければもう充分ですw 論文が落ち着いたら検証したいところですがいつ落ち着くのか。
自分への教訓
rcParams
のaxes.spine
が無に帰したせいで、seaborn経由の原因かと思いひたすらseabornのドキュメントを中心に調べていたけど、基本はやっぱりmatplotlibのドキュメントをちゃんと読みましょう。そうしていたら絶対sns.despine()
よりもspines[:].set_visible(True)
を発見していたはずwmatplotlibとseabornについて理解がちょっと深まった(初歩的な部分で)のと、これでプロットがヒートマップ以外も含めスムーズになるのは確かだけど、変なところにこだわって調べ物するのは(今はまだ)程々にしろ…。試行錯誤の過程だった調べ物で、解決してから「回り道だったな」と判明したもの以外にも、明らかに拘りすぎて今調べなくていいよねそれっていうのが多々存在したので。論文。論文優先だから君(ちなみにこのブログも論文の気晴らしにちゃちゃっと書くつもりが思ったより時間かかってしまったのでやっぱり僕はだめですねこういうところ)。
先生ごめんなさい、可及的速やかに草稿を...。