Python
matplotlib
python3
seaborn

seabornでhueを指定しながら、重ねてプロットしようとしたらハマった話

注意

【matplotlib v2.1.1以前】の時のエラーです。
【matplotlib v2.1.2以降】の方はあまり関係ないかもしれません。

経緯

雑にpythonのseabornを使ってグラフを書いていたら、
同じカテゴリカルデータを使って書いた2つのグラフを重ねて表示したくなった

sns.pointplot(x="x", y="fitting_y", hue="y_limit", data=result, markers="")
sns.pointplot(x="x", y="y", hue="y_limit", data=result, join=False)

と書いたら、以下のようなエラーが出てきて処理が止まった。

File "${HOME}/anaconda3/lib/python3.6/site-packages/matplotlib/legend.py", line 1344, in _in_handles
     if f_h.get_facecolor() != h.get_facecolor():
ValueError: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()

上記のエラーを修正しようとした。

経緯(図解)

こういう2つのグラフがある
これらを……

くっつける前

↓ こうしたい

くっつけた後

解決法(正攻法)

skotaroさんにコメントでコードを教えてもらったため、記述させていただきます。
groupbyを利用して描画した後、legendを利用して凡例を記述するのが正攻法です。

fig, ax = plt.subplots()
grouped = result.groupby('y_limit')
for key, gr in grouped:
    gr.plot('x', 'fitting_y', ax=ax)
    color = ax.lines[-1].get_color()
    gr.plot.scatter('x', 'y', marker='o', ax=ax, color=color)

ax.legend(ax.collections, list(grouped.groups.keys()), loc='upper left', bbox_to_anchor=(1,1))

解決法(邪道)

どうしてもseabornを利用して、プロットしたい人向け

ax = sns.pointplot(x="x", y="fitting_y", hue="y_limit", data=result, markers="")
ax.collections.clear() # 「今回の肝」 コレが無いとエラーで落ちる。
sns.pointplot(x="x", y="y", hue="y_limit", data=result, join=False, ax=ax)

hueを連続して使いたいなら、ax.collectionsに溜まる色付きラベルPathCollectionを末尾以外で随時削除するべし。

原因【matplotlib v2.1.1以前】

seabornで hueを指定した際、legend(凡例設定)が自動的に作成され、hueから自動生成されたPathCollection(色付きラベル) [※1] が凡例表示用にaxに保存される。
連続してplotする際に、二度目の関数でもhueが指定されている場合、やはりlegendを自動生成・追記しようとする。
hueからPathCollectionが自動生成されるが、これは保存済みの※1と同じもの である。
ここで 問題となるのが、axの凡例表示用PathCollectionの保存時に重複チェックがある こと。
凡例に同じ色を保存しようとすると AttributeError(属性エラー)系で落ちてしまう 重複が取り除かれる予定だった
コレが今回エラーで落ちた原因だったようだ。

追記(2018/04/23):PathCollectionは描画処理に関するものらしく記述が怪しすぎるので削除。
(実際のエラーは上記にも書いたようにValueError

追記

本当の原因はValueError
np.repeat([[True,True,True, False]],19,axis=0)
のような値を取るf_h.get_facecolor() != h.get_facecolor()について
any() or all() してせずにif文にぶち込んでいること
要するにライブラリーのバージョン固有のバグでした。

解決法の説明【matplotlib v2.1.1以前】

コレを防ぐためには、 直前のseabornのplotでaxに保存されている 凡例表示用のPathCollectionと衝突を回避する必要がある
今回は同じカテゴリカル変数を利用した描画のため、PathCollectionの削除を選択した。
凡例表示用のPathCollectionは ax.collections (type:list) に保存されているため削除関数clearを利用する

ax.collections.clear()

clear後は、その直後のseabornの関数でhue(とpalette)を指定して、グラフを重ねようとした場合にエラー落ちしなくなるようだ。

全部コードを読んだわけじゃないので推測も混じってます。
何か見識あればコメントでお願いします。

【matplotlib v2.1.2以降】のお話(追記)

    ax = sns.pointplot(x="x", y="fitting_y", hue="y_limit", data=result, markers="")
    sns.pointplot(x="x", y="y", hue="y_limit", data=result, join=False)

と連続して記述してもエラー落ちはしなくなりました。
ごっそり重複チェックの部分のコードが消去されたためです(_in_handles関数)。
しかし、凡例は壊れますので、groupbyを利用した正攻法を利用したほうが良いでしょう。

※一応、ax.collection.clear()を挟むことで解決することができます。しかし、今後安定した動作であるかの保証は無いです。詳細はコメント欄にて。

壊れる凡例の例

壊れる凡例の例

_in_handles関数が消去でなくエラー修正であったらならの妄想

ちなみにmatplotlib v2.1.1legend.py内にある_in_handles関数をおいてany()関数を利用してエラー部分を修正していたとしたら、PathCollectionの指定時にax.collections.clear()を使用せずとも、連続で書かれたseaborn.pointplotにて、壊れずに凡例の表示ができそうであった。

その他雑記(alpha値の扱いとか)

ax.collections[0].get_facecolor() != ax.collections[1].get_facecolor()

np.repeat([[True,True,True, False]],19,axis=0)
とほぼ等しくなっていた原因は、get_facecolor()関数とseaborn.pointplotの色の管理の方法に問題があった。
get_facecolor()ではalpha値まで管理されているが、seaborn.pointplot内の色の管理はseaborn.color_paletteで行われる。
そのため、palette=の指定を行った場合は、必ずalpha値が1に統一されてしまう。
どういうことかというと、内部でseaborn.color_paletteに変換されるが、seaborn.color_paletteはRGB変換処理によりalpha値の情報が削ぎ落とされてしまう。そのため、同じラベルに違う色を付けたところでalpha値の部分で常にFalseが出てしまう。
現在,seaborn.pointplotではalpha値を制御して描画する機能はついていないようだ。(たぶん)

ただしseaborn.regplotなどをみる限りalpha値を関数描画時にscatter_kws等の引数を用いて指定できるようになっているようだ。

seaborn.pointplotもそのうち機能がつくかも……しれないこともないかもしれない。

解決法の見つけ方

探し方が悪かったのかどこにも載ってなかったので、
pycharmでbreakpoint debugしながら、いじいじしてた。

ソースコード

詳細なソースコードは以下のgistのURLに置いておきますので気になった方は参照ください。
seabornでhueを使いながら、複数のグラフを重ねたかった

その他の解法とか?

  • hueを使わずgroup_byしてfor文で回して、legendを自作する(上記記載済み)
  • jupter-notebook(web brower)では、一部エラーが出てもそのまま描画処理をしてくれるため、実用途に足るなら利用する