こんな人にオススメ
ploty
で3Dプロットをする際にはどうやって書けばいいの?
それにどんなグラフが描ける?
ということで、今回はplotly
のgo
を使用して3次元グラフを作成する。python初心者とかが見たらすげーとなるグリグリグラフだ。
そもそも3次元のグラフを2次元平面であるディスプレイ上に映すこと自体に難しさがあるんだけど、グラフを見やすく動かしやすくすることで、この難関を突破できるだろう。
python環境は以下。
- Python 3.9.7
- numpy 1.21.3
- matplotlib 3.4.3
- plotly 5.3.1
- plotly-orca 3.4.2
作成したコード全文
下準備
import sys import numpy as np import matplotlib.cm as cm import plotly import plotly.graph_objects as go import plotly.io as pio sys.path.append('../../') import plotly_layout_template as template
まずは下準備としての 続きを見るimport
関連。plotly_layout_template
は自作のテンプレート。これを使用することで簡単にキレイなグラフを作成することができる。
【随時更新 備忘録】plotlyのグラフ即席作成コード
今回はplotly
のグラフ作成のgo
を使用する。px
ってのもあってpx
の場合はサクッと簡単なグラフを作成することができる。go
はカスタムに優れている反面、コードが煩雑になりやすい。
グラフ作成用の関数定義
まずは本記事で作成するグラフで使用する関数の定義から。予め関数を定義しておくことで、メインの関数がスッキリするし、他の場所で使用するときも関数を呼ぶだけでいい。
-
-
【plotly&工夫】楽にグラフを描くためのplotlyの関数化
続きを見る
今回は3Dの散布図とsurface
プロット(面プロット)を使用するのでこれらを定義しておく。あとはレイアウト関連やグラフ保存関連。
scatter
プロット用の関数
# scatterプロット用の関数 def scatter3d(x, y, z, name, colorscale, symbol=None, mode='lines+markers'): d = go.Scatter3d( mode=mode, x=x, y=y, z=z, name=name, marker=dict(symbol=symbol, size=3, color=z, colorscale=colorscale,), line=dict(color=z, colorscale=colorscale,), ) return d
まずは散布図用の関数の定義。引数はx
, y
, z
軸の値とプロットの名称、色指定としてのカラースケールにした。
あとはお好みでマーカーを変更したりプロットのモードを変更したりできるようにした。マーカーについては以下。
-
-
【plotly&マーカー】plotlyのマーカーのシンボル
続きを見る
marker
のサイズを3にしているのは、記事に載せる時にデフォルトではデカすぎたから。適宜調節してほしい。
surface
プロット用の関数
# surfaceプロット用の関数 def surface(x, y, z, name, colorscale, hidesurface=False): # 等高線の設定 def set_contours(**kwargs): contours = dict( show=True, # 等高線を表示する usecolormap=True, # 等高線に色付けをする highlightcolor='red', # 形状のハイライトの色 highlightwidth=16, # 形状のハイライトの線の太さ width=16, **kwargs, ) return contours d = go.Surface( x=x, y=y, z=z, name=name, colorscale=colorscale, opacity=0.7, # surfaceの透明度 # 等高線 contours=dict( x=set_contours(project_x=True), # x面(x軸方向?)に投影線を表示する, y=set_contours(), z=set_contours() ), # カラーバーの設定 colorbar=dict( x=0.8, title="colorbar", # 枠線、目盛線の設定 outlinecolor='black', ticks='outside', tickcolor='black', ), hidesurface=hidesurface, # 面部分を表示するか否か ) return d
お次はsurface
プロット用の関数。さっきの散布図は線や点でグラフを作成するんだけど、surface
だと面でグラフを作成する。
go
の場合は専用のgo.Surface
が用意されているのでこれを活用。追加で等高線が表示されるようにも設定しておいた。
また、面の場合は値の大小で色を自動で変化させると見やすいので、その値と色の関係性を示すためにカラーバーも追加しておいた。カラーバーはヒートマップなどにも使用される。
-
-
【plotly&heatmap】go.Heatmapで2次元配列をマップ化
続きを見る
等高線に関してはx
, y
, z
で構造が同じなのでset_contours
関数で定義しておいた。また、**kwargs
で追加設定ができるようにもしておいた。
project_x
はyz平面に等高線を投影する引数。y
のところでproject_y
=Trueとするとxz平面に等高線が投影される。
レイアウト設定用の関数
# レイアウト作成用の関数 def set_layout(title, **kwargs): layout = go.Layout( template=template.plotly_layout(), # タイトルはいつも通りのタイトルに title=dict(text=title, x=0.3, y=0.9,), # x, y, z軸のタイトルはsceneに入れる scene=dict(xaxis_title='X', yaxis_title='Y', zaxis_title='Z',), # 3Dは凡例が離れるので中心に寄せる # itemsizingで凡例のプロット点のサイズを大きくする legend=dict(x=0.8, y=0.9, itemsizing='constant',), **kwargs, # 追加で設定したい項目はここ ) return layout
グラフレイアウトの設定で自作のテンプレートを反映させる。テンプレートを使用しない場合はこの部分をコメントアウト、もしくは消せばいい。
指定しているのはグラフタイトルと軸タイトル(軸ラベル)。軸タイトルに関しては引数scene
を噛ませてあげないといけないので注意。
また、デフォルトの位置設定だとグラフタイトルや凡例が左右端に寄せられてしまうので位置調節を行なった。ここもテンプレートで記述してもいいだろう。
グラフ保存用の関数
# グラフ保存用の関数 def save(fig, config, save_name): pio.orca.config.executable = '/Applications/orca.app/Contents/MacOS/orca' pio.write_html(fig, f"{save_name}.html", config=config,) pio.write_image(fig, f"{save_name}.png")
グラフ保存用の関数。保存形式はhtmlとpng。config
はグラフの右上に存在するツールバーのようなもの。
config
を設定しておくとこのツールバーの内容を追加したり削除したりできる。htmlで使用することで、保存後のhtmlファイルでもツールバーの設定が引き継がれる。
-
-
【plotly&config】グラフのツールバーを編集する
続きを見る
と言っても3Dのグラフだとconfig
が特殊なので反映されない。
グラフ表示と保存用の関数
# グラフの表示と保存 def show_save(data, layout, save_name): # グラフの表示と保存 config = template.plotly_config() fig = go.Figure(data=data, layout=layout) fig.show(config=config) save(fig=fig, config=config, save_name=save_name)
グラフ表示とグラフ保存を一括で行う関数も作成。これでshow_save
とその引数だけで表示と保存が行える。
グラフの保存には先程のsave
関数を使用。
螺旋プロット
まずは螺旋状にプロットする方法を解説。と言っても上で定義した関数を組み合わせるだけでグラフを作成することができるので簡単。
螺旋構造は線と点で作成するのでscatter3d
関数を使用。x
, y
をcos関数、sin関数にしつつ、z
を増やすことで螺旋構造にできる。
また、ここでは2種類の螺旋を作成したが、見分けがつきやすいようにプロットに変化をつけておいた。
- 名称(
name
):center
(原点中心)とshift
(原点からズラした) - マーカー(
symbol
):circle
(円形)とsquare
(四角形) - カラースケール(
colorscale
):Jet_r
(赤から青)とPlotly3
(青から紫)
あとはこれらをscatter3d
関数に入れてレイアウトの設定やグラフの表示・保存を行えばいい。関数を定義しておくと見通しが良い。
def spiral(save_name, **kwargs): t = np.linspace(0, 20, 100) x, y, z = np.cos(t), np.sin(t), t # プロットデータの作成 plot = [] for num in range(2): # 2種類の螺旋を作成 name = ['center', 'shift'][num] # プロットの名称 symbol = ['circle', 'square'][num] # マーカーの種類 colorscale = ['Jet_r', 'Plotly3'][num] # プロットのカラースケール d = scatter3d( x=x + num, y=y + num, z=z, name=name, symbol=symbol, colorscale=colorscale, ) plot.append(d) # レイアウトの設定 layout = set_layout(title='spiral', **kwargs) # グラフの表示と保存 show_save(data=plot, layout=layout, save_name=save_name) spiral(save_name='spiral')
グラフ表示時のカメラ位置を設定
さっきは普通にグラフを作成してできたできた、ってことだったけど、グラフによれば上のグラフのようにプロットが見づらいこともあるだろう。
plotly
の3Dグラフの場合は初期のカメラ(グラフをどこから見るか)の位置を決めることができる。とりあえず上のグラフのコードは以下。
マーカーが2Dとは異なってかなり数が減るようなので、このマーカーに合わせて色を適当に決めてマーカーごとに直線をプロットするように設定した。
def simple_scatters(save_name, **kwargs): T = np.linspace(0, 20, 10) x, y, z = T, T, T # symbolは2Dとは異なる symbols = [ 'circle', 'circle-open', 'square', 'square-open', 'diamond', 'diamond-open', 'cross', 'x', ] # カラースケールは好みで決めた colorscales = [ 'Viridis', 'Turbo', 'Blackbody', 'Hot', 'gray', 'Peach', 'Mint', 'Agsunset', ] # プロットデータの作成 plot = [] for num, (symbol, colorscale) in enumerate(zip(symbols, colorscales)): d = scatter3d( x=x + num, y=y, z=z, name=f"{symbol} | {colorscale}", symbol=symbol, colorscale=colorscale, mode='markers', ) plot.append(d) # レイアウトの設定 name = 'simple scatters' layout = set_layout( title=name, legend_title='symbol | color', # グラフタイトルと凡例タイトル **kwargs, ) # グラフの表示と保存 show_save(data=plot, layout=layout, save_name=save_name) simple_scatters(save_name='simple_scatters')
で、カメラの位置を変更するにはscene_camera
という引数のeye
をレイアウトに適用する。これはベクトルで指定しているっぽい。詳しくは公式をチェック。
右上から見たグラフで表示したい場合はx=0, y=0, z=1の
ように設定すればいい。ただ、z=1
にするとかなり寄った感じになるので適宜調節が必要。
# 初期カメラ位置 camera = dict(eye=dict(x=0, y=2.5, z=0),) simple_scatters(save_name='simple_scatters_camera', scene_camera=camera,)
レイアウトの設定追加は可変長キーワード引数のkwargs
で対応している。できるグラフは以下。
surface
プロット
surface
プロットは上のグラフでわかるように、面の構造を持ったグラフだ。なので今までのように1次元のデータをグラフ化するのではなく、2次元のデータをグラフ化する。
2次元のデータを作成する方法は色々あるけど、ここではnumpy
のmeshgrid
を使用。x
, y
の1次元の値から自動でx
, y
の2次元の値を作成してくれる。
上のグラフだとこんな感じのデータを作成した。
x = np.arange(-5, 5 + 0.1, 0.1) y = np.arange(-5, 5 + 0.1, 0.1) X, Y = np.meshgrid(x, y) Z = X ** 2 + Y ** 2 print(X) # [[-5. -4.9 -4.8 ... 4.8 4.9 5. ] # [-5. -4.9 -4.8 ... 4.8 4.9 5. ] # [-5. -4.9 -4.8 ... 4.8 4.9 5. ] # ... # [-5. -4.9 -4.8 ... 4.8 4.9 5. ] # [-5. -4.9 -4.8 ... 4.8 4.9 5. ] # [-5. -4.9 -4.8 ... 4.8 4.9 5. ]] print(Y) # [[-5. -5. -5. ... -5. -5. -5. ] # [-4.9 -4.9 -4.9 ... -4.9 -4.9 -4.9] # [-4.8 -4.8 -4.8 ... -4.8 -4.8 -4.8] # ... # [ 4.8 4.8 4.8 ... 4.8 4.8 4.8] # [ 4.9 4.9 4.9 ... 4.9 4.9 4.9] # [ 5. 5. 5. ... 5. 5. 5. ]] print(Z) # [[50. 49.01 48.04 ... 48.04 49.01 50. ] # [49.01 48.02 47.05 ... 47.05 48.02 49.01] # [48.04 47.05 46.08 ... 46.08 47.05 48.04] # ... # [48.04 47.05 46.08 ... 46.08 47.05 48.04] # [49.01 48.02 47.05 ... 47.05 48.02 49.01] # [50. 49.01 48.04 ... 48.04 49.01 50. ]]
このデータを上で定義したsurface
関数でグラフ化する。引数はx
, y
, z
の値と名称、面と値の色を関係付けるcolorscale
、そして面を表示するか否かのhidesurface
。
上のグラフでは面を表示しているのでhidesurface=False
、すなわち関数のデフォルト値にしている。
# surfaceプロット def surface_plot(save_name, hidesurface=False, **kwargs): x = np.arange(-5, 5 + 0.1, 0.1) y = np.arange(-5, 5 + 0.1, 0.1) X, Y = np.meshgrid(x, y) Z = X ** 2 + Y ** 2 # プロットデータの作成 plot = [] d = surface( x=X, y=Y, z=Z, name='simple_surface', colorscale='Jet', hidesurface=hidesurface, # 面部分を表示するか否か ) plot.append(d) # レイアウトの設定 layout = set_layout(title='sinple_surface', **kwargs) # グラフの表示と保存 show_save(data=plot, layout=layout, save_name=save_name) surface_plot(save_name='simple_surface')
ワイヤーフレーム(面を隠す)
逆に面を隠すようにすると、線(ワイヤー)だけが残ってワイヤーフレームのグラフに変化する。骨組みだけになる。
go.Surface
の場合だとhidesurface=True
にするだけなのでかなり簡単に設定できる。
# ワイヤーフレーム surface_plot(save_name='wire_frame', hidesurface=True)