からっぽのしょこ

読んだら書く!書いたら読む!同じ事は二度調べ(たく)ない

【Python】3次元ランダムウォークの作図:3方向移動【Matplotlib】

はじめに

 PythonのMatplotlibライブラリを使ってグラフを動かそうシリーズです。
 この記事では、上下左右前後に移動する3次元のランダムウォークのアニメーションを作成します。

【前の内容】

www.anarchive-beta.com

【他の内容】

www.anarchive-beta.com

【目次】

3次元ランダムウォークの作図:3方向移動

 3方向(上下左右前後)に移動するランダムウォーク(random walk)のアニメーションを作成します。

 利用するライブラリを読み込みます。

# 利用するモジュール
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
import numpy as np


1サンプル

 次は、1つのサンプルを描画します。

 試行回数を指定して、移動する量と軸をランダムに生成します。

# 試行回数を指定
max_iter = 100

# 乱数を生成
random_val_vec  = np.random.choice([-1, 1], size=max_iter, replace=True) # 移動量
random_axis_vec = np.random.choice(['x', 'y', 'z'], size=max_iter, replace=True) # 移動軸
print(random_val_vec[:5])
print(random_val_vec.shape)
print(random_axis_vec[:5])
print(random_axis_vec.shape)
[-1  1  1 -1 -1]
(100,)
['y' 'x' 'z' 'x' 'x']
(100,)

 試行ごとに「 -1 または 1 」と「 'x''y''z' 」をランダムに(等確率で)割り当てます。'x' はx軸方向、'y' はy軸方向、'z' はz軸方向の変化に対応し、1 は正の方向、-1 は負の方向の移動に対応します。

 移動量に応じた座標を計算します。

# 試行ごとに集計
x_vec = np.cumsum(
    np.hstack([0, np.int32(random_axis_vec=='x') * random_val_vec]) # 初期値を追加
)
y_vec = np.cumsum(
    np.hstack([0, np.int32(random_axis_vec=='y') * random_val_vec]) # 初期値を追加
)
z_vec = np.cumsum(
    np.hstack([0, np.int32(random_axis_vec=='z') * random_val_vec]) # 初期値を追加
)
print(x_vec[:5])
print(x_vec.shape)
print(y_vec[:5])
print(y_vec.shape)
print(z_vec[:5])
print(z_vec.shape)
[0 0 1 1 0]
(101,)
[ 0 -1 -1 -1 -1]
(101,)
[0 0 0 1 1]
(101,)

 初期値(スタート地点・0回目の結果)を 0 として追加して、軸ごとに各試行までの和(累積和)を np.cumsum() で計算します。
 簡単な例で試してみます。

# 簡単な例で確認
val_vec  = np.array([-1, 1, -1])
axis_vec = np.array(['x', 'z', 'y'])
print(axis_vec == 'y')
print(np.int32(axis_vec == 'y'))
print(np.int32(axis_vec == 'y') * val_vec)
[False False  True]
[0 0 1]
[ 0  0 -1]

 移動軸の配列の要素を 'x''y''z' に応じて True, False に変換し、更に数値型に変換することで 1, 0 になります。そこに移動量の配列を掛けることで軸ごとの移動量が得られます。

 点の推移のグラフを作成します。

# 最終値を取得
x = x_vec[max_iter]
y = y_vec[max_iter]
z = z_vec[max_iter]

# 余白サイズを指定
margin_rate = 0.1

# グラフサイズを設定
axis_size = np.max([np.abs(x_vec), np.abs(y_vec), np.abs(z_vec)])
axis_size  = np.ceil(axis_size * (1+margin_rate)) # 余白を追加

# 矢サイズを指定
alr = 0.5

# 3Dランダムウォークを作図
fig, ax = plt.subplots(figsize=(12, 12), dpi=250, facecolor='white', 
                       subplot_kw={'projection': '3d'})
ax.quiver([-axis_size, 0, 0], [0, -axis_size, 0], [0, 0, -axis_size], 
          [2*axis_size, 0, 0], [0, 2*axis_size, 0], [0, 0, 2*axis_size], 
          arrow_length_ratio=alr/axis_size, color='black', linewidth=1) # x・y・z軸線
ax.plot(x_vec, y_vec, zs=-axis_size, zdir='z', 
        color='C0', linewidth=1, linestyle='dashed') # xy面の軌跡
ax.plot(y_vec, z_vec, zs=-axis_size, zdir='x', 
        color='C0', linewidth=1, linestyle='dashed') # yz面の軌跡
ax.plot(x_vec, z_vec, zs=axis_size, zdir='y', 
        color='C0', linewidth=1, linestyle='dashed') # xz面の軌跡
ax.scatter([x, -axis_size, x], 
           [y, y, axis_size], 
           [-axis_size, z, z], 
           s=100, fc='none', ec='blue') # 面ごとの最終地点
ax.quiver(x, y, z, 
          [0, -axis_size-x, 0], [0, 0, axis_size-y], [-axis_size-z, 0, 0], 
          arrow_length_ratio=0, color='gray', linestyle='dotted') # 点の目盛線
ax.plot(x_vec, y_vec, z_vec) # 軌跡
ax.scatter(x, y, z, s=100, color='blue') # 最終地点
ax.margins(x=0.0, y=0.0, z=0.0) # (機能しない?のでlimで謎小細工)
ax.set_xlim(xmin=-axis_size/1.05, xmax=axis_size/1.05)
ax.set_ylim(ymin=-axis_size/1.05, ymax=axis_size/1.05)
ax.set_zlim(zmin=-axis_size/1.05, zmax=axis_size/1.05)
ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_zlabel('z')
ax.set_title('iteration: {}, $(x, y, z) = ({}, {}, {})$'.format(max_iter, x, y, z), loc='left')
fig.suptitle('Random Walk', fontsize=20)
ax.set_box_aspect((1, 1, 1))
plt.show()

3次元ランダムウォーク:1サンプル

 各軸線の最小値・最大値(各方向のグラフサイズ)は Axes.set_*lim() に指定した値に余白(マージン)が追加されます。よって、axis_size の位置にプロットすると側面や底面からズレます。マージン率(マージンを含めた全体のサイズは (最大値 - 最小値) * 1.05 らしい)を0にできればいいのですがやり方が分からなかったので、回避策としてごにょごにょしています(まっとうなやり方を教えてください)。コードの分かりやすさを優先するのであれば 1/1.05 を外してください。
 点の軌道(実線)と軸ごとの軌道(破線)が近いと分かりづらいので margin_rate で調整しています。

 点の推移のアニメーションを作成します。

# 余白サイズを指定
margin_rate = 0.1

# グラフサイズを設定
axis_size = np.max([np.abs(x_vec), np.abs(y_vec), np.abs(z_vec)])
axis_size  = np.ceil(axis_size * (1+margin_rate)) # 余白を追加

# グラフオブジェクトを初期化
fig, ax = plt.subplots(figsize=(12, 12), facecolor='white', 
                       subplot_kw={'projection': '3d'})
fig.suptitle('Random Walk', fontsize=20)

# 矢サイズを指定
alr = 0.5

# 作図処理を定義
def update(i):
    
    # 前フレームのグラフを初期化
    plt.cla()
    
    # 現在値を取得
    x = x_vec[i]
    y = y_vec[i]
    z = z_vec[i]

    # 3Dランダムウォークを作図
    ax.quiver([-axis_size, 0, 0], [0, -axis_size, 0], [0, 0, -axis_size], 
              [2*axis_size, 0, 0], [0, 2*axis_size, 0], [0, 0, 2*axis_size], 
              arrow_length_ratio=alr/axis_size, color='black', linewidth=1) # x・y・z軸線
    ax.plot(x_vec[:i+1], y_vec[:i+1], zs=-axis_size, zdir='z', 
            color='C0', linewidth=1, linestyle='dashed') # xy面の軌跡
    ax.plot(y_vec[:i+1], z_vec[:i+1], zs=-axis_size, zdir='x', 
            color='C0', linewidth=1, linestyle='dashed') # yz面の軌跡
    ax.plot(x_vec[:i+1], z_vec[:i+1], zs=axis_size, zdir='y', 
            color='C0', linewidth=1, linestyle='dashed') # xz面の軌跡
    ax.scatter([x, -axis_size, x], 
               [y, y, axis_size], 
               [-axis_size, z, z], 
               s=100, fc='none', ec='blue') # 面ごとの現在地点
    ax.quiver(x, y, z, 
              [0, -axis_size-x, 0], [0, 0, axis_size-y], [-axis_size-z, 0, 0], 
              arrow_length_ratio=0, color='gray', linestyle='dotted') # 点の目盛線
    ax.plot(x_vec[:i+1], y_vec[:i+1], z_vec[:i+1]) # 軌跡
    ax.scatter(x, y, z, s=100, color='blue') # 現在地点
    ax.margins(x=0.0, y=0.0, z=0.0) # (機能しない?のでlimで謎小細工)
    ax.set_xlim(xmin=-axis_size/1.05, xmax=axis_size/1.05)
    ax.set_ylim(ymin=-axis_size/1.05, ymax=axis_size/1.05)
    ax.set_zlim(zmin=-axis_size/1.05, zmax=axis_size/1.05)
    ax.set_xlabel('x')
    ax.set_ylabel('y')
    ax.set_zlabel('z')
    ax.set_title('iteration: {}, $(x, y, z) = ({}, {}, {})$'.format(i, x, y, z), loc='left')
    ax.set_box_aspect((1, 1, 1))

# 動画を作成
ani = FuncAnimation(fig=fig, func=update, frames=max_iter+1, interval=100)

# 動画を書出
ani.save('3d_90_xyz_1.gif', dpi=100)

3次元ランダムウォーク:1サンプル

 2つの軸の変化を組み合わせごとに側面に描画しています。
 2軸のグラフは、2次元ランダムウォークのように推移していますが、1つの軸しか変化しない(1つの軸に注目すると座標が変化しないことがある)ので、異なる図です。

複数サンプル

 続いて、複数のサンプルを並べて描画します。
 作図コードについては「RandomWalk_3d_90.py at anemptyarchive/matplotlib · GitHub」を参照してください。

・座標軸のみ

3次元ランダムウォーク:複数サンプル

 等確率で移動する場合、初期値が期待値になりますが分散を持ち(散らばり)ます。

 この記事では、格子状に移動する3次元ランダムウォークを扱いました。次の記事では、全ての方向に移動する場合を扱います。

おわりに

 目的の3次元版を作れたわけですが、座標だけで3軸が埋まってしまうので時間軸を追加できないですね。作った時点では満足していたのですが、その後に2次元版を作ったらなんだか物足りなくなってしまいました。何か可視化できることはあるでしょうか。
 この記事の転載作業中に、サンプル軸の代わりにサンプルごとに別のグラフとして並べるのはできるなと思いましたが、追加で作るほど心が惹かれませんでした。と、あとがきを書いてる間に、2軸ごとの組み合わせ(側面のグラフ)に時間軸を加えたグラフを並べて、4つのグラフで表現することはできるなと思い付きました。
 1年後くらいに修正することになれば作ってみようと思います。

【次の内容】

www.anarchive-beta.com