はじめに
PythonのMatplotlibライブラリを使ってグラフを動かそうシリーズです。
この記事では、上下左右に移動する2次元のランダムウォークのアニメーションを作成します。
【前の内容】
【他の内容】
【目次】
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()
点の推移のアニメーションを作成します。
# 余白サイズを指定 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)
初期値(期待値)を破線で示します。
試行回数軸を追加したグラフを作成します。
# 最終値を取得 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()
各軸線の最小値・最大値(各方向のグラフサイズ)は 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)
試行回数(時間)の変化を横軸で表します。また、試行回数ごとの各軸、両軸の変化をそれぞれ側面に描画しています。
試行回数と各軸のグラフは、1次元ランダムウォークのように推移していますが、1つの軸しか変化しない(1つの軸に注目すると座標が変化しないことがある)ので、異なる図です。
複数サンプル
続いて、複数のサンプルを並べて描画します。
作図コードについては「RandomWalk_2d_90.py at anemptyarchive/matplotlib · GitHub」を参照してください。
・座標軸のみ
等確率で移動する場合、初期値が期待値になりますが分散を持ち(散らばり)ます。
・サンプル軸あり
サンプルごとに縦に並べています。
・試行回数(時間)軸あり
回数が増えると分散が大きくなるのを確認できます。
この記事では、格子状に移動する2次元ランダムウォークを扱いました。次の記事では、全ての方向に移動する場合を扱います。
おわりに
R(ggplot2)版の記事ではできなかった3Dプロットを使えるのでいくつか作ってみました。ただし、3次元の曲線などそれぞれを1つの図(レイヤ)として2次元的に重ねて描画されているので、重なり具合や奥行きの関係を脳内で多少の解釈をしないと、認識を誤る可能性があるのでご注意ください。
そんな事情もあって見せ方が難しく試行錯誤したのですが、図を回転させると立体的に見えるんですね。あまりに格好良くできて見入ってます。まさに自画自賛です。
ちなみに作図コードは、目的の3次元版を先に作ってから1次元版を作り消化試合的に2次元版も作りました。2次元版でもこっちを後に作ったので、この記事の内容が最後にできました。続きの記事も良い出来栄えなので、眺めるだけでもぜひご覧ください。
以降の記事でも同様に、側面のグラフ(1つの軸のみに注目したグラフ)は1次元ランダムウォークになってるものだと思っていましたが、色々作っているうちに違うことに気付きました。それともランダムに留まることも含めたランダムウォークもあるのでしょうか。
【次の内容】