忍んでいる肉球の足跡

プログラムに関連することを雑多に扱います

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 さんに Qiitaにてコメントでコードを教えてもらったため、記述させていただきます。
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(属性エラー)系で落ちてしまう 重複が取り除かれる予定だった コレが今回エラーで落ちた原因だったようだ。 (実際のエラーは上記にも書いたようにValueError

追記

本当の原因はValueErrornp.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()を挟むことで解決することができます。しかし、今後安定した動作であるかの保証は無いです。詳細はQiitaのコメント欄にて。

壊れる凡例の例

壊れる凡例の例

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

ちなみにmatplotlib v2.1.1legend.py内にある_in_handles関数をおいてall()関数を利用してエラー部分を修正していたとしたら、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値の部分で常にTrueが出てしまう。 現在,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)では、一部エラーが出てもそのまま描画処理をしてくれるため、実用途に足るなら利用する