Matplotlibのscatterで半径１のマーカーサイズの円を描く

Pythonで使えるグラフ描写パッケージのMatplotlibには散布図を描写するための`matplotlib.pyplot.scatter`メソッドがある。各点のマーカーサイズを指定することができるがポイント単位での指定のため、グラフの軸の実スケールと対応させるようにする。数個の円を描くのであれば`matplotlib.patches.Circle`クラスでradiusを指定して使えばいいが、数百数千の円と軸のスケールを合わせたいときはscatterの方が使いやすいのでこのようなことをやっている。

## 方法

コードにコメントを入れつつ説明。

```python
import matplotlib.pyplot as plt
import numpy as np

ppi=72 # points per inche 

# 枠の範囲指定
xmin,xmax=-2.0, 2.0
ymin,ymax=-2.0, 2.0

x,y=0.0, 0.0 # 中心座標
size=1.0     # 半径

# dpiは任意の値を設定できる。デフォルトは100
fig = plt.figure(figsize=(8,8), dpi=100.0)
ax  = fig.add_axes((0.1,  0.1, 0.8, 0.8))

ax.set_aspect("equal") # アスペクト比を等しくする
                       # 今は枠のサイズ設定で最初から等しいのであってもなくてもよい

# 描写範囲の長さを取得(dpi単位)
# x軸をベースに計算しているがy軸でも同じ。アスペクト比が違えばおかしくなる
ax_length=ax.bbox.get_points()[1][0]-ax.bbox.get_points()[0][0]

# dpi単位の長さをポイント単位に変換(dpiからインチ単位にし、インチからポイント単位に変換)
ax_point = ax_length*ppi/fig.dpi

# x軸の実スケールをポイント単位に変換
xsize=xmax-xmin
fact=ax_point/xsize

# scatterのマーカーサイズは直径のポイントの二乗を描くため、実スケールの半径をポイントに変換し直径にしておく
size*=2*fact

# 二乗にして与える 
ax.scatter(x,y,s=size**2, linewidths=0)

ax.set_xlim(xmin,xmax)
ax.set_ylim(ymin,ymax)

ax.set_xticks(np.linspace(ax.get_xlim()[0], ax.get_xlim()[1], 5))
ax.set_yticks(np.linspace(ax.get_ylim()[0], ax.get_ylim()[1], 5))

ax.grid(which='both', axis='both')

plt.show()
```

axesクラスを使わない場合は描写範囲の長さの取得はこのようにすればいい。

```python
gca=fig.gca()
ax_length=gca.bbox.get_points()[1][0]-gca.bbox.get_points()[0][0]
ax_point = ax_length*ppi/fig.dpi
```

コメントでもらったが変換係数を決める部分は以下のようにしてもいい。座標とピクセルを対応付するために先に`xlim,ylim`を指定してあげる必要がある。

```
ax.set_xlim(xmin,xmax)
ax.set_ylim(ymin,ymax)

# 実スケールの座標[1,0]でのピクセルを取得し、長さ1に対するポイント数を決める。
tf = ax.transData.transform([1, 0]) * (ppi / fig.dpi)
fact = tf[0] - tf[1]
```


上記を実行するとこのような半径１の図ができる。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53817/f994d3fa-89dc-46c1-7bb5-3b6c04b45e8c.png" width="300">

ポイントは

- scatterのマーカーサイズはポイントサイズの二乗で指定される。
    - ボックスサイズの情報を取得するときは基本的にはインチやdpi単位なので頑張ってポイント(ppi)単位にする。
    - dpiはインチあたりのドット数で`matplotlib.pyplot.figure`を呼び出すときに任意の値を指定できる。デフォルトは100
    - ppiはインチあたりのポイント数で72でmatplotlibでは固定。
- 半径ではなく直径を与える。
- アスペクト比は等しい必要がある。
    - 楕円などを描きたかったら`matplotlib.patches.Circle`クラスを使う。
- 後述しているが`linewidths`は0を指定したほうがいい。


scatterの引数に配列を与えると複数の任意の半径でマーカーが描ける。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53817/91003080-d89f-9583-014f-d0fe7c6ba72d.png" width="600">

### マーカーの枠線の取り扱い

```python
import matplotlib.pyplot as plt
import numpy as np

ppi=72 # points per inche

xmin,xmax=0.0, 2.0
ymin,ymax=0.4, 1.6

x=np.array( [0.5, 1.5] )
y=np.array( [1.0, 1.0] )

size=np.array([0.5, 0.5])  # radius

fig = plt.figure(figsize=(8,8), dpi=100.0)
ax  = fig.add_axes((0.1,  0.1, 0.8, 0.8))
ax.set_aspect("equal")

ax_length=ax.bbox.get_points()[1][0]-ax.bbox.get_points()[0][0]
ax_point = ax_length*ppi/fig.dpi
xsize=xmax-xmin
fact=ax_point/xsize
size*=2*fact

ax.scatter(x[0],y[0],s=size[0]**2, linewidths=0)
ax.scatter(x[1],y[1],s=size[1]**2, edgecolors="black", linewidths=(size[1]/10))

ax.set_xlim(xmin,xmax)
ax.set_ylim(ymin,ymax)

ax.set_xticks(np.linspace(ax.get_xlim()[0], ax.get_xlim()[1], 21))
ax.set_yticks(np.linspace(ax.get_ylim()[0], ax.get_ylim()[1], 13))

ax.grid(which='both', axis='both')

plt.show()
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53817/03084a22-35e5-e496-fab7-fc5d4f3693ce.png" width="300">

上記のコードより`linewidths`を指定すると円のエッジを中心とした（二乗していない）ポイントサイズの太さのエッジラインを描くことができる。上記は(二乗していない)マーカーサイズ(ポイント単位)の1/10を指定しているのでエッジを中心に太さ0.１のエッジラインになっている。デフォルトでは`linewidth`は`None`なので指定しないでいいと思ったが明示的に0をしていしないと若干ズレるような気がする。

### マーカーの種類による振る舞い

```python
import matplotlib.pyplot as plt
import numpy as np

ppi=72 # points per inche

xmin,xmax=0.0, 2.0
ymin,ymax=0.0, 2.0

x=np.array( [0.5, 0.5, 1.5, 1.5] )
y=np.array( [0.5, 1.5, 0.5, 1.5] )

size=np.array([0.5, 0.5, 0.5, 0.5])  # radius

fig = plt.figure(figsize=(8,8), dpi=100.0)
ax  = fig.add_axes((0.1,  0.1, 0.8, 0.8))

ax_length=ax.bbox.get_points()[1][0]-ax.bbox.get_points()[0][0]
ax_point = ax_length*ppi/fig.dpi 
xsize=xmax-xmin
fact=ax_point/xsize
size*=2*fact

ax.scatter(x[0],y[0],s=size[0]**2, marker='^')
ax.scatter(x[1],y[1],s=size[1]**2, marker='x')
ax.scatter(x[2],y[2],s=size[2]**2, marker='+')
ax.scatter(x[3],y[3],s=size[3]**2, marker='*')

ax.set_xlim(xmin,xmax)
ax.set_ylim(ymin,ymax)

ax.grid(which='both', axis='both')

plt.show()
```

適当に選んだマーカーでは半径0.5を指定するとこのようになる。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53817/05b496a0-0c88-6576-ceb0-dd669b52ca28.png" width="300">


## 参考
- https://stackoverflow.com/questions/14827650/pyplot-scatter-plot-marker-size