カテゴリー

go

【plotly&3D】goで3Dグラフを作成

2021年5月18日

こんな人にオススメ

plotyで3Dプロットをする際にはどうやって書けばいいの?

それにどんなグラフが描ける?

ということで、今回はplotlygoを使用して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

運営者のメガネとです。YouTubeTwitterInstagramも運営中。

自己紹介はこちらから、お問い合わせはこちらからお願いいたします。

運営者メガネ

作成したコード全文

下準備

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のグラフ作成のgoを使用する。pxってのもあってpxの場合はサクッと簡単なグラフを作成することができる。goはカスタムに優れている反面、コードが煩雑になりやすい。

グラフ作成用の関数定義

まずは本記事で作成するグラフで使用する関数の定義から。予め関数を定義しておくことで、メインの関数がスッキリするし、他の場所で使用するときも関数を呼ぶだけでいい。

【plotly&工夫】楽にグラフを描くための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&マーカー】plotlyのマーカーのシンボル

こんな人にオススメplotlyにもmatplo ...

続きを見る

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次元配列をマップ化

こんな人にオススメ 以前、mat ...

続きを見る

等高線に関してはx, y, zで構造が同じなのでset_contours関数で定義しておいた。また、**kwargsで追加設定ができるようにもしておいた。

project_xはyz平面に等高線を投影する引数。yのところでproject_y=Trueとするとxz平面に等高線が投影される。

【plotly&kwargs】グラフ作成時の設定を後から追加できるように

こんな人にオススメ plotlyでグ} ...

続きを見る

レイアウト設定用の関数

# レイアウト作成用の関数
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】グラフのツールバーを編集する

こんな人にオススメ plotlyでグ} ...

続きを見る

と言っても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次元のデータを作成する方法は色々あるけど、ここではnumpymeshgridを使用。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)

ワイヤーフレーム(scatter版)