matplotlibやseabornのヒストグラムでビン幅をサクッとキレイに整える

# はじめに
<img width="450" src="https://images.unsplash.com/photo-1544717301-9cdcb1f5940f?ixlib=rb-1.2.1&auto=format&fit=crop&w=1650&q=80">
<a style="background-color:black;color:white;text-decoration:none;padding:4px 6px;font-family:-apple-system, BlinkMacSystemFont, &quot;San Francisco&quot;, &quot;Helvetica Neue&quot;, Helvetica, Ubuntu, Roboto, Noto, &quot;Segoe UI&quot;, Arial, sans-serif;font-size:12px;font-weight:bold;line-height:1.2;display:inline-block;border-radius:3px" href="https://unsplash.com/@icons8?utm_medium=referral&amp;utm_campaign=photographer-credit&amp;utm_content=creditBadge" target="_blank" rel="noopener noreferrer" title="Download free do whatever you want high-resolution photos from Icons8 Team"><span style="display:inline-block;padding:2px 3px"><svg xmlns="http://www.w3.org/2000/svg" style="height:12px;width:auto;position:relative;vertical-align:middle;top:-2px;fill:white" viewBox="0 0 32 32"><title>unsplash-logo</title><path d="M10 9V0h12v9H10zm12 5h10v18H0V14h10v9h12v-9z"></path></svg></span><span style="display:inline-block;padding:2px 3px">Icons8 Team</span></a>

ラベル別にデータの分布を確認するためヒストグラムを重ねてプロットすることは多いが、データ次第ではビン幅の違いが目立つケースがある。
TableauをはじめとしたBIツールを使っていても発生しないが、matplotlibやseabornは勝手に調整してくれないので自分で対応する必要がある。


# 方法
引数の`bins`を利用する。
> bins : int or sequence or str, optional
[matplotlib.pyplot.hist](https://matplotlib.org/3.1.3/api/_as_gen/matplotlib.pyplot.hist.html)

`bins`は整数値だけでなく`sequence`も受け取れるので、
range関数に最大値最小値を指定して好みの分割数を設定するだけでいい。



```py

import numpy as np
import matplotlib.pyplot as plt

# ラベルが2種類でデータの分布が異なるDataFrameを用意
df_1st = pd.DataFrame(np.random.normal(loc=20, scale=10, size=100), columns=["val"])
df_1st["target"] = "class_1"
df_2nd = pd.DataFrame(np.random.normal(loc=15, scale=20, size=100), columns=["val"])
df_2nd["target"] = "class_2"

df = pd.concat([df_1st, df_2nd])
```


## ビン幅補正前

```py
import matplotlib as plt
import seaborn as sns

# target毎にプロット
for val in df["target"].unique():
    ax = sns.distplot(df.query('target == @val')["val"], kde=False, label=f"target is {val}")

ax.legend()
```
<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/167671/bf9eaadb-4f43-3bb3-2bce-3fb6318c8e95.png">


## ビン幅補正後

```py
# 最小値
x_min = int(df["val"].min())

# 最大値
x_max = int(df["val"].max())

# 最小値から最大値の範囲で5間隔
range_bin_width = range(x_min, x_max, 5)

# target毎にプロット
for val in df["target"].unique():
    ax = sns.distplot(df.query('target == @val')["val"], bins=range_bin_width, kde=False, label=f"target is {val}")

ax.legend()
```
<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/167671/b93654fd-b4fe-43c3-c967-e3929101454e.png">


# 補足
`bins`を設定しない場合、**Freedman-Diaconis rule**と呼ばれる手法でビンの数が決定されている。
この手法は中々優秀で単一のデータをプロットする場合、概ね問題なくプロットされる。

```distributions.py
def _freedman_diaconis_bins(a):
    """Calculate number of hist bins using Freedman-Diaconis rule."""
    # From https://stats.stackexchange.com/questions/798/
    a = np.asarray(a)
    if len(a) < 2:
        return 1
    h = 2 * iqr(a) / (len(a) ** (1 / 3))
    # fall back to sqrt(a) bins if iqr is 0
    if h == 0:
        return int(np.sqrt(a.size))
    else:
        return int(np.ceil((a.max() - a.min()) / h))
```
https://github.com/mwaskom/seaborn/blob/master/seaborn/distributions.py#L24



# おわりに
プロットは美しくないプロットは見せる相手に失礼なので、
最低限キレイに整えるするのは礼儀だと思う。