Pythonで使えるグラフ描写パッケージのMatplotlibには散布図を描写するためのmatplotlib.pyplot.scatter
メソッドがある。各点のマーカーサイズを指定することができるがポイント単位での指定のため、グラフの軸の実スケールと対応させるようにする。数個の円を描くのであればmatplotlib.patches.Circle
クラスでradiusを指定して使えばいいが、数百数千の円と軸のスケールを合わせたいときはscatterの方が使いやすいのでこのようなことをやっている。
方法
コードにコメントを入れつつ説明。
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クラスを使わない場合は描写範囲の長さの取得はこのようにすればいい。
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]
ポイントは
- scatterのマーカーサイズはポイントサイズの二乗で指定される。
- ボックスサイズの情報を取得するときは基本的にはインチやdpi単位なので頑張ってポイント(ppi)単位にする。
- dpiはインチあたりのドット数で
matplotlib.pyplot.figure
を呼び出すときに任意の値を指定できる。デフォルトは100 - ppiはインチあたりのポイント数で72でmatplotlibでは固定。
- 半径ではなく直径を与える。
- アスペクト比は等しい必要がある。
- 楕円などを描きたかったら
matplotlib.patches.Circle
クラスを使う。
- 楕円などを描きたかったら
- 後述しているが
linewidths
は0を指定したほうがいい。
scatterの引数に配列を与えると複数の任意の半径でマーカーが描ける。
マーカーの枠線の取り扱い
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()
上記のコードよりlinewidths
を指定すると円のエッジを中心とした(二乗していない)ポイントサイズの太さのエッジラインを描くことができる。上記は(二乗していない)マーカーサイズ(ポイント単位)の1/10を指定しているのでエッジを中心に太さ0.1のエッジラインになっている。デフォルトではlinewidth
はNone
なので指定しないでいいと思ったが明示的に0をしていしないと若干ズレるような気がする。
マーカーの種類による振る舞い
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を指定するとこのようになる。