からっぽのしょこ

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

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

はじめに

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

【前の内容】

www.anarchive-beta.com

【他の内容】

www.anarchive-beta.com

【目次】

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

 2方向(上下左右)に移動するランダムウォーク(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'], 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' 'y' 'y' 'y' 'y']
(100,)

 試行ごとに「 -1 または 1 」と「 'x' または 'y' 」をランダムに(等確率で)割り当てます。'x' はx軸方向、'y' はy軸方向の変化に対応し、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]) # 初期値を追加
)
print(x_vec[:5])
print(x_vec.shape)
print(y_vec[:5])
print(y_vec.shape)
[0 0 0 0 0]
(101,)
[ 0  1  0 -1  0]
(101,)

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

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

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

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

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

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

# 2Dランダムウォークを作図
fig, ax = plt.subplots(figsize=(8, 8), dpi=250, facecolor='white')
ax.axhline(y=0, color='red', linestyle='dashed') # 初期値
ax.axvline(x=0, color='red', linestyle='dashed') # 初期値
ax.plot(x_vec, y_vec) # 軌跡
ax.scatter(x_vec[max_iter], y_vec[max_iter], s=100, color='blue', zorder=10) # 最終地点
ax.set_xlim(xmin=-axis_size, xmax=axis_size)
ax.set_ylim(ymin=-axis_size, ymax=axis_size)
ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_title('iteration: {}, $(x, y) = ({}, {})$'.format(max_iter, x_vec[max_iter], y_vec[max_iter]), loc='left')
fig.suptitle('Random Walk', fontsize=20)
ax.grid()
ax.set_aspect('equal')
plt.show()

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


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

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

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

# グラフオブジェクトを初期化
fig, ax = plt.subplots(figsize=(8, 8), facecolor='white')
fig.suptitle('Random Walk', fontsize=20)

# 作図処理を定義
def update(i):
    
    # 前フレームのグラフを初期化
    plt.cla()
    
    # 2Dランダムウォークを作図
    ax.axhline(y=0, color='red', linestyle='dashed') # 初期値
    ax.axvline(x=0, color='red', linestyle='dashed') # 初期値
    ax.plot(x_vec[:i+1], y_vec[:i+1]) # 軌跡
    ax.scatter(x_vec[i], y_vec[i], s=100, color='blue', zorder=10) # 現在地点
    ax.set_xlim(xmin=-axis_size, xmax=axis_size)
    ax.set_ylim(ymin=-axis_size, ymax=axis_size)
    ax.set_xlabel('x')
    ax.set_ylabel('y')
    ax.set_title('iteration: {}, $(x, y) = ({}, {})$'.format(i, x_vec[i], y_vec[i]), loc='left')
    fig.suptitle('Random Walk', fontsize=20)
    ax.grid()
    ax.set_aspect('equal')

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

# 動画を書出
ani.save(filename='2d_90_xy_1.gif', dpi=250)

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

 初期値(期待値)を破線で示します。

 試行回数軸を追加したグラフを作成します。

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

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

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

# 矢サイズを指定
alr = 1.5

# 2Dランダムウォークを作図
fig, ax = plt.subplots(figsize=(10, 10), dpi=250, facecolor='white', 
                       subplot_kw={'projection': '3d'})
ax.quiver(axis_i_min, [-axis_size, 0], [0, -axis_size], 
          0, [2*axis_size, 0], [0, 2*axis_size], 
          arrow_length_ratio=alr/(2*axis_size), 
          color='black', linewidth=1) # x・y軸線
ax.quiver(axis_i_min, [axis_size, 0], [0, -axis_size], 
          axis_i_max-axis_i_min, 0, 0, 
          arrow_length_ratio=alr/max_iter, 
          color='black', linewidth=1, linestyle=['dashed']*2+['solid']*4) # 面ごとのi軸線
ax.quiver(axis_i_min, 0, 0, 
          axis_i_max-axis_i_min, 0, 0, 
          arrow_length_ratio=alr/max_iter, 
          color='black', linewidth=1) # i軸線
ax.plot(x_vec, y_vec, zs=axis_i_min, zdir='x', 
        color='C0', linewidth=1, linestyle='dashed') # xy面の軌跡
ax.plot(np.arange(max_iter+1), x_vec, zs=-axis_size, zdir='z', 
        color='C0', linewidth=1, linestyle='dashed') # ix面の軌跡
ax.plot(np.arange(max_iter+1), y_vec, zs=axis_size, zdir='y', 
        color='C0', linewidth=1, linestyle='dashed') # iy面の軌跡
ax.scatter([axis_i_min, max_iter, max_iter], 
           [x, x, axis_size], 
           [y, -axis_size, y], 
           s=100, fc='none', ec='blue') # 面ごとの最終地点
ax.quiver(max_iter, x, y, 
          [axis_i_min-max_iter, 0, 0], [0, 0, axis_size-x], [0, -axis_size-y, 0], 
          arrow_length_ratio=0, color='gray', linestyle='dotted') # 点の目盛線
ax.plot(np.arange(max_iter+1), x_vec, y_vec) # 軌跡
ax.scatter(max_iter, x, y, s=100, color='blue') # 最終地点
ax.margins(x=0.0, y=0.0, z=0.0) # (機能しない?のでlimで謎小細工)
ax.set_xlim(xmin=axis_i_min+axis_i_max*0.025, xmax=axis_i_max)
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('iteration')
ax.set_ylabel('x')
ax.set_zlabel('y')
ax.set_title('iteration: {}, $(x, y) = ({}, {})$'.format(max_iter, x, y), loc='left')
fig.suptitle('Random Walk', fontsize=20)
ax.set_box_aspect((2, 1, 1))
#ax.view_init(elev=0, azim=0) # 表示アングル
plt.show()

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

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

 試行回数軸を追加したアニメーションを作成します。

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

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

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

# 矢サイズを指定
alr = 1.5

# 作図処理を定義
def update(i):
    
    # 前フレームのグラフを初期化
    plt.cla()
    
    # 現在値を取得
    x = x_vec[i]
    y = y_vec[i]
    
    # 2Dランダムウォークを作図
    ax.quiver(axis_i_min, [-axis_size, 0], [0, -axis_size], 
              0, [2*axis_size, 0], [0, 2*axis_size], 
              arrow_length_ratio=alr/(2*axis_size), 
              color='black', linewidth=1) # x・y軸線
    ax.quiver(axis_i_min, [axis_size, 0], [0, -axis_size], 
              axis_i_max-axis_i_min, 0, 0, 
              arrow_length_ratio=alr/max_iter, 
              color='black', linewidth=1, linestyle=['dashed']*2+['solid']*4) # 面ごとのi軸線
    ax.quiver(axis_i_min, 0, 0, 
              axis_i_max-axis_i_min, 0, 0, 
              arrow_length_ratio=alr/max_iter, 
              color='black', linewidth=1) # i軸線
    ax.plot(x_vec[:i+1], y_vec[:i+1], zs=axis_i_min, zdir='x', 
            color='C0', linewidth=1, linestyle='dashed') # xy面の軌跡
    ax.plot(np.arange(i+1), x_vec[:i+1], zs=-axis_size, zdir='z', 
            color='C0', linewidth=1, linestyle='dashed') # ix面の軌跡
    ax.plot(np.arange(i+1), y_vec[:i+1], zs=axis_size, zdir='y', 
            color='C0', linewidth=1, linestyle='dashed') # iy面の軌跡
    ax.scatter([axis_i_min, i, i], 
               [x, x, axis_size], 
               [y, -axis_size, y], 
               s=100, fc='none', ec='blue') # 面ごとの最終地点
    ax.quiver(i, x, y, 
              [axis_i_min-i, 0, 0], [0, 0, axis_size-x], [0, -axis_size-y, 0], 
              arrow_length_ratio=0, color='gray', linestyle='dotted') # 点の目盛線
    ax.plot(np.arange(i+1), x_vec[:i+1], y_vec[:i+1]) # 軌跡
    ax.scatter(i, x, y, s=100, color='blue') # 現在地点
    ax.margins(x=0.0, y=0.0, z=0.0) # (機能しない?のでlimで謎小細工)
    ax.set_xlim(xmin=axis_i_min+axis_i_max*0.025, xmax=axis_i_max)
    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('iteration')
    ax.set_ylabel('x')
    ax.set_zlabel('y')
    ax.set_title('iteration: {}, $(x, y) = ({}, {})$'.format(i, x, y), loc='left')
    ax.set_box_aspect((2, 1, 1))
    #ax.view_init(elev=0, azim=0) # 表示アングル

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

# 動画を書出
ani.save(filename='2d_90_ixy_1.gif', dpi=250)

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

 試行回数(時間)の変化を横軸で表します。また、試行回数ごとの各軸、両軸の変化をそれぞれ側面に描画しています。
 試行回数と各軸のグラフは、1次元ランダムウォークのように推移していますが、1つの軸しか変化しない(1つの軸に注目すると座標が変化しないことがある)ので、異なる図です。

複数サンプル

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

・座標軸のみ

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

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

・サンプル軸あり

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

 サンプルごとに縦に並べています。

・試行回数(時間)軸あり

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

 回数が増えると分散が大きくなるのを確認できます。

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

おわりに

 R(ggplot2)版の記事ではできなかった3Dプロットを使えるのでいくつか作ってみました。ただし、3次元の曲線などそれぞれを1つの図(レイヤ)として2次元的に重ねて描画されているので、重なり具合や奥行きの関係を脳内で多少の解釈をしないと、認識を誤る可能性があるのでご注意ください。
 そんな事情もあって見せ方が難しく試行錯誤したのですが、図を回転させると立体的に見えるんですね。あまりに格好良くできて見入ってます。まさに自画自賛です。
 ちなみに作図コードは、目的の3次元版を先に作ってから1次元版を作り消化試合的に2次元版も作りました。2次元版でもこっちを後に作ったので、この記事の内容が最後にできました。続きの記事も良い出来栄えなので、眺めるだけでもぜひご覧ください。

 以降の記事でも同様に、側面のグラフ(1つの軸のみに注目したグラフ)は1次元ランダムウォークになってるものだと思っていましたが、色々作っているうちに違うことに気付きました。それともランダムに留まることも含めたランダムウォークもあるのでしょうか。

【次の内容】

www.anarchive-beta.com