Seabornがめっちゃ進化していた！

# Next-generation seaborn interface

なんか、知らない間にseabornのinterfaceがめちゃくちゃ進化していたので共有 (https://seaborn.pydata.org/nextgen/demo.html) 。みんなが何かカッコいい図を使いたいときにとりあえず使うライブラリ。それがSeaborn。しかし、結局細かい調整をしようとすると、くそ長いパラメータを与えるか、MatplotlibのAPIを直接叩く羽目になってしまう。そもそも、現状のSeabornは描画Methodsが細分化されていて、Seaborn単体でも覚えなきゃいけないこと結構多い。ということで、慣れてくるとMatplotlibで1から書いた方が早くなってきて、筆者はあまり使わなくってしまっていた。

しかし、最新のseabornのプレリリースでinterfaceが大きく変更されていて、さまざまな形式のplotを統一的な記法で表現できるように進化していた。下記の例を見てもらえば分かると思うが、この新記法はRのggplot2にそっくりなので、ggplot2を使ったことがある人には馴染みやすいかも。

```python
import seaborn as sns
sns.set_theme()
tips = sns.load_dataset("tips")

import seaborn.objects as so
(
    so.Plot(
        tips, "total_bill", "tip",
        color="smoker", marker="smoker", pointsize="size",
    )
    .facet("time")
    .add(so.Scatter())
    .configure(figsize=(7, 4))
)
```


<img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/167957/1c7c3944-771e-af9c-0810-8a69a32df4a5.png width=600>

開発者もggplotに似ていることは認めていて、https://seaborn.pydata.org/nextgen/ で、そのことについて触れている。ただ、ggplotを移植することを目的としているわけではなくて、pythonらしい書き方ができる新しいインタフェースを目指している模様。

## 使い方

以下のコマンドでalpha版をインストールすることができる。

```
pip install https://github.com/mwaskom/seaborn/archive/refs/tags/v0.12.0a0.tar.gz
```

正直、開発者の説明ページ(https://seaborn.pydata.org/nextgen/demo.html) を見てもらったほうが早いのだが、少し特徴的だなと思った点について触れていく。



### Plot object

ggplotの`ggplot`に対応するもの。データを読み込んでPlot class objectを作成することが新インタフェースで最初にしなければならないことになる。Plot objectを作成した時点では、何もデータは描画されず、空のaxes objectが作られるのみである。

```python
import seaborn as sns
sns.set_theme()
import seaborn.objects as so
tips = sns.load_dataset("tips")
so.Plot(tips, x="total_bill", y="tip")
```
<img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/167957/d9acbb63-de26-1a90-8b27-111f3e1336d9.png width=400>

ggplotと違う点としては、ggplotでいう`aes` 関数を噛ませないで、`x`,` y`などを直接指定するところだろうか。

### add method 

ggplotの`+`に対応するもの。以下のような感じでplotすることができる。

```python
so.Plot(tips, x="total_bill", y="tip", color="time").add(so.Scatter())
```
<img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/167957/af4f57d5-0cf1-919b-0d3e-7bec19edc369.png width=500>

まさしくggplot。この`add` methodは、`Scatter()`以外にも`Line()`、`Bar()`のような描画クラス(**Mark** class objects)が用意されているので、簡単に別のタイプのplotをつくることができる。ただしalpha versionには heatmap を作る機能がなかったりなど、まだ作れないplotもある。
`add` methodには、グラフの種類を指定する Mark class objects 以外にも、データを統計処理などによって変換する **Stat** class objects。描画の際のデータの重なりを解消する **Move** class objects を与えることができる。この３つ種類のobjectを組み合わせることで、複雑なデータも綺麗に視覚化することが可能になっている。以下では、Stat class objects の1つ`Agg class`とMove class objectsの1つ`Dodge class`について簡単に紹介する。

### Stats (Agg class)

既に記したように`add` methodには、単に`Scatter()`、`Line()`、`Bar()`のような `Mark class` だけではなく、dataをどのように変換するかを指定する`Agg class`を指定することができる。その結果`Mark class`は元データをそのまま描画するのではなく、`Agg class`で処理されたデータを描画することになる。 
これはめちゃくちゃ便利。

例えば、以下のようなplotを考える。

```python
fmri = sns.load_dataset("fmri").query("region == 'parietal'")
so.Plot(fmri, x="timepoint", y="signal", color="event").add(so.Scatter())
```
<img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/167957/d710d928-4c64-61a3-0be2-ad392bed8810.png width=500>


どうやら、x軸の値は離散的であるため、x軸の値が同じ群を平均化したりするとデータの全体像を掴めそうである。そういうときに使えるのが `Agg class`。

```python
fmri = sns.load_dataset("fmri").query("region == 'parietal'")
so.Plot(fmri, x="timepoint", y="signal", color="event").add(so.Scatter(), so.Agg())
```
<img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/167957/46c28cb8-cee1-fef1-da50-fad5db24931f.png width=500>  

なるほどなぁ。これは便利だ。`Agg class`を与えることで、データをxの値が同じ群ごとで平均化し、それを `Scatter()` で plotしてる。デフォルトだと`Agg()`は"mean"で統計処理してる見たいだけど`Agg(func="median")`みたいな指定もできるみたい。

`add` methodはggplotでの`+`と同じで重ねてつかうことができるので、以下のように、平均を示すline plotと分布を示すscatterの組み合わせたplotもすぐに作ることができる。（こういうのは、これまでのSeabornでは結構面倒臭かった。）

```python
(
    so.Plot(fmri, x="timepoint", y="signal", color="event")
    .add(so.Scatter())
    .add(so.Line(),so.Agg())
)
```
<img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/167957/0b7fb677-32b3-d7f0-e88f-afb72190af15.png width=500>

すごい。しかもlegendの表示も調整されていて、alpha版といいつつ結構作り込まれているようである。

### Moves (Dodge class)

実をいうと、まだ挙動がよく理解していない。。。グループ化されたbar plotがjitter plotなどが互いに重ならないように調整してくれるclassであるようだ。以下のように使う。
まず、`Dodge class`を用いなかった場合の単純なbar plotの例.

```python
so.Plot(tips, "day", "total_bill", color="time").add(so.Bar(), so.Agg())
```
<img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/167957/84825ac1-1323-95e0-a56d-46d4babc92e9.png width=500>

こんな感じで、colorの異なる2つのbarが重なってしまう。そういうときに使うのが `Dodge class`。これを使うと、


```python
so.Plot(tips, "day", "total_bill", color="time").add(so.Bar(), so.Agg(), move=so.Dodge())
```
<img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/167957/0d942d98-197b-51cb-cb9d-40fd81f12ba4.png width=500>

みたいになって、重なりを解消してくれる。
複雑なgroup化をした場合にも、重なりを自動で解消してくれるっぽい。以下がその例。

```python
(
    so.Plot(tips, "day", "total_bill", color="time", alpha="sex")
    .add(so.Bar(), so.Agg(), move=so.Dodge())
)

```
<img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/167957/161855b6-5490-8bed-dd06-db40fe7cc8f7.png width=500>

### Scale function (scale method) 

名前のとおり、scalingするためのfunction。ただ、x, yだけじゃなくて、colorとかについて簡単にscalingできるようになっていて使いやすい。以下、colorのlog scaleの変換例。

```python
planets = sns.load_dataset("planets").query("distance < 1000")
(
    so.Plot(planets, x="distance", y="orbital_period", color="mass")
    .scale(x="log", y="log", color=so.Continuous("Reds", transform="log"))
    .add(so.Scatter(fillalpha=1.0))
)
```
<img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/167957/2ace2bb0-0b1c-5298-6f03-b04dedb768df.png width=500>



### facet method

ggplotの`facet_wrap`に対応するもの。普段からggplotを利用している人には分かると思うが、facetに指定したカテゴリー名の値ごとにsubplotを作ってくれる。

### On method

個人的に、一番感動したのがこれ。`on method`をつかうことで、plotするFigure/subFigure or axes objectを指定することができる。つまり、新しいSeabornではもうFigure level functionとaxes level functionの区別がないということである。これを使うと以下のような使い方ができる。

```python
f = mpl.figure.Figure(constrained_layout=True, figsize=(8, 4))
sf1, sf2 = f.subfigures(1, 2)
(
    so.Plot(tips, x="total_bill", y="tip", color="day")
    .add(so.Scatter())
    .on(sf1)
    .plot()
)
(
    so.Plot(tips, x="total_bill", y="tip", color="day")
    .facet("day", wrap=2)
    .add(so.Scatter())
    .on(sf2)
    .plot()
)
```
<img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/167957/6f8ac92c-3b0f-1871-789a-3686bc9b332d.png width=600>


## Patchworklibの対応

せっかくなので、自作libraryの[patchworklib](https://github.com/ponnhide/patchworklib)を対応させて、ggplot+patchwork再現してみることにした。
`on method`が実装されたこともあり、対応は以前のSeabornに比べると非常に楽だった。


```python
import seaborn as sns
import seaborn.objects as so
import patchworklib as pw
sns.set_theme()
tips = sns.load_dataset("tips")
g1 = pw.load_ngs(
        so.Plot(
            tips, "total_bill", "tip",
            color="smoker", marker="smoker", pointsize="size",
        )
        .facet("time")
        .add(so.Scatter())
        .on(pw.basefigure)
        .plot()
     )
g2 = pw.load_ngs(
        so.Plot(fmri, x="timepoint", y="signal", color="event")
        .add(so.Scatter())
        .on(pw.basefigure)
        .plot()
     )

(g1.outline|g2.outline).savefig()
```
![download.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/167957/c74eb66e-5747-be1f-0165-0319c4f888c3.png)


## 今後への期待
version 0.11 までのSeabornのplotを完全再現できるようにはまだなっていないので、今後のupdateに期待したい。一方で、公式のドキュメントを読むと分かるが、描画関数(Mark function)やStat functionは自作することもできるみたいなので、現状足りないものは自分で作ればいいのかもしれない笑。
いずれにしても、このinterfaceが今後のpythonでのグラフ描画の中心的存在になっていくことはほぼ確実だと思うので、早めに慣れておくといい気がする。