エラーコードをググってたら3日経っていた

*URL名はちょっと前までよくやっていたファイル名の付け方を現したものです(実話)。素人が一生懸命スパコンやWSL等と戦った時の記録。エラーコードをググってもよくわからない世界がそこにはあるのです。

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 1Axes 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 Helpspinesのところクリックしてようやく気が付くという。もっと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 論文が落ち着いたら検証したいところですがいつ落ち着くのか。

自分への教訓

  • rcParamsaxes.spineが無に帰したせいで、seaborn経由の原因かと思いひたすらseabornのドキュメントを中心に調べていたけど、基本はやっぱりmatplotlibのドキュメントをちゃんと読みましょう。そうしていたら絶対sns.despine()よりもspines[:].set_visible(True)を発見していたはずw

  • matplotlibとseabornについて理解がちょっと深まった(初歩的な部分で)のと、これでプロットがヒートマップ以外も含めスムーズになるのは確かだけど、変なところにこだわって調べ物するのは(今はまだ)程々にしろ…。試行錯誤の過程だった調べ物で、解決してから「回り道だったな」と判明したもの以外にも、明らかに拘りすぎて今調べなくていいよねそれっていうのが多々存在したので。論文。論文優先だから君(ちなみにこのブログも論文の気晴らしにちゃちゃっと書くつもりが思ったより時間かかってしまったのでやっぱり僕はだめですねこういうところ)。

先生ごめんなさい、可及的速やかに草稿を...。

*1:ウチの研究室でグラフをExcel以外で書いている人はみんなRです、肩身が…w

*2:取り急ぎ論文用にさっさとコード量問わないもの組めって話、というかそっちはもう組んであるんですよね。コードがごちゃごちゃしていて変えたかっただけでこんなことを…w こんなんだから論文がいつまでも仕上がらない。

*3:set_style()とか暫定的なスタイル変更系のもありましたがヒートマップ用の.ipynbファイル用なら永続的変更をしたかった