FuncAnimationを使って待ち行列シミュレーションをアニメーションしてみた

2024年3月8日

物流センターにトラックが到着しても、前のトラックの荷卸しが終わるまで長時間待たされるというのは配送会社の生産性を下げる要因にもなっていますが、前回はこれをPythonのSimpyライブラリーを使ってシミュレーションしました。

【作業分析3】トラックドックでの待機時間や待機台数をSimpyでシミュレーション

 

このシミュレーションは10,000台のトラックが15分間に平均1台の割合でランダムに到着する場合のシミュレーションでした。

その結果、平均待機台数は4台だけれども、多い時には29台も並ぶ場合があることがシミュレーションで示されました。

この結果はシミュレーションをするたびに変わりますが、グラフに表示すると例えば次のようになります。

 

横軸に何番目のトラックかを、縦軸に前に並んでいたトラックの台数を示しています。

例えば5,700番目くらいに到着したトラックの前には37台のトラックが並んでいたことがわかります。

ただ、これでは大雑把なことしかわかりませんし、そもそもグラフのセンスが良くありません。

そこで今回は、これをPythonのアニメーション機能を使って表示してみました。

 

◆仕事や勉強の息抜きに。。。

Pythonのアニメーション機能とは?

車の位置が少しずつ異なる絵を沢山描いておいて、それらをパラパラとめくるように表示させると車が動くように見えますね。

これがアニメーションの原理ですが、Pythonではグラフを描くのに定番のmatplotlibでこれが実現できます。

そしてArtistAnimationFuncAnimationの2つの方法があります。

ArtistAnimationは最初に100枚なら100枚の画像を用意しておいて、それらを高速で順番に表示させる方法です。

これに対してFuncAnimationは、画像を作る関数を定義しておいて、その関数で1枚ずつ画像を作りながら表示させる方法です。

前者の方がコードはわかりやすいのですが、100枚の画像をリスト形式で持っておくためプログラムが重くなってしまうという欠点があります。

そこで今回は後者のFuncAnimationを使ってアニメーションを作ってみました。

 

FuncAnimationの使い方

matplotlibからanimationモジュールをインポートすると、その中にあるFuncAnimationが使えるようになります。

FuncAnimationにはいろいろな引数を指定できるようになっていますが、最低限必要なのは次の2つです。

  • figure()のインスタンス
  • 画像を作る関数

画像を作る関数は自分で定義します。

その上でanimation.FuncAnimationのインスタンスを作ればアニメーションの出来上がりです。

 

グラフは固定でカメラだけを動かすアニメーション

それではFuncAnimationを使って、前回のM/M/1モデルのシミュレーション結果をアニメーションにしてみます。

先程のグラフでは1つの画面に10,000個もデータがあって見にくかったので、50個ずつ表示しながら右に流れていくようなアニメーションを作ってみます。

10,000個の元データはlen_in_queueのリストに入っています。

まずはanimationモジュールをインポートします。

from matplotlib import animation

アニメーションで表示したいグラフは横軸にシミュレーション回数、縦軸に待機台数なので、XとYに値を代入します。

X = range(len(len_in_queue)) # Xを代入
Y = len_in_queue # Yを代入

次にFuncAnimationで最低限必要な2つの引数を定義します。

1つ目はplt.figure()のインスタンスです。

fig = plt.figure() # plt.figure()のインスタンス

今回は1つの画面に50個のデータを表示させたいので、ついでに初期設定としてx軸の最小値を0、最大値を50としておきます。

xlim = [0,50] # x軸の最小値と最大値の初期値を0-50に設定

2つ目は画像を作る関数ですが、今回は関数名plotとして次のように定義します。

def plot(i):
    plt.cla() # 前のグラフを削除

    xlim[0]+=1 # x軸の最小値を1増やす
    xlim[1]+=1 # x軸の最大値を1増やす

    plt.plot(X, Y) # 次のグラフを作成
    plt.title("Scrolling on one-shot data")
    plt.xlim(xlim[0],xlim[1]) # x軸を描画
    plt.ylim(0,30) # y軸を描画

待機台数のリストlen_in_queueには10,000個のデータが入っているので、10,000枚の画像が作成されます。

そのため、この関数plotは10,000回呼び出されます。

そしてそのたびにplotの引数であるiは0から9,999まで1刻みで増加することになります。

(今回はこのiを使いませんが、次のアニメーションでは使います)

 

この関数でまず最初にやることは、前のループ(i-1の時)で作って表示させたグラフを消すことです。

次に表示させる範囲を右に1つずらすために、x軸の最小値と最大値を1ずつ増やします。

そして、そのずらした範囲のグラフを表示させます。

 

最後にanimation.FuncAnimationのインスタンスを作ります。

ani = animation.FuncAnimation(fig, plot) # animation.FuncAnimationのインスタンスを作成

以上を実行させると次のようなアニメーションが表示されます。

狙い通り、横軸の幅50でグラフを左から順に表示させるアニメーションが作成できました。

 

グラフを動かしながらカメラで追いかけるアニメーション

先程のアニメーションは10,000個のデータをすべて表示させておいて、それを左から50幅のカメラを動かしながら全体を見ていくというものでした。

次に10,000個のデータを1つずつグラフに描きながら、50幅のカメラに入り切らなくなったら右に動かしていくようなアニメーションを作ってみましょう。

plot関数が呼び出されるたびにiが1ずつ増えていくことを利用して、

  • X(i)やY(i)をappendでリストに追加する
  • iが50まではx軸の範囲を0~50、iが50を超えたらi-50~iにする

ということをplot関数内で記述します。

fig = plt.figure()

x_data = []
y_data = []

xlim = [0,50]
X = range(len(len_in_queue))
Y = len_in_queue

def plot(i):
plt.cla()

xlim[0] = max(0, i-50) # iが50までは0、それ以上ではiー50をx軸の最小値に設定
xlim[1] = max(50, i) # iが50までは50、それ以上ではiをx軸の最大値に設定

x_data.append(X[i]) # i番目までのX(i)の値をx_dataにリストとして代入
y_data.append(Y[i]) # i番目までのY(i)の値をy_dataにリストとして代入

plt.plot(x_data, y_data)
plt.title("Scrolling with time-series data")
plt.ylim(0,30)
plt.xlim(xlim[0],xlim[1])

ani = animation.FuncAnimation(fig, plot)

すると次のようなアニメーションになります。

少しずつグラフが描かれ、それに引きづられるようにカメラが右に動く動画になりました。

 

2つのアニメーションを同時に表示

次にこれら2つのアニメーションを並べて表示してみます。

但し、カメラの進行速度を合わせるために、1つ目のアニメーションではiが50になるまではカメラを動かさないようにします。

fig = plt.figure(figsize=(8,4), tight_layout=True)

x_data = []
y_data = []

X = range(len(len_in_queue))
Y = len_in_queue

def plot(i):
    plt.cla()

    # i=50まではx軸の表示範囲を動かさない
    plt.subplot(121)
    if i > 50:
        xlim = [i-50, i]
    else:
        xlim = [0, 50]

    plt.title("Scrolling on one-shot data")
    plt.ylim(0,30)
    plt.xlim(xlim[0],xlim[1])
    plt.plot(X, Y)          

    x_data.append(X[i])
    y_data.append(Y[i])

    plt.subplot(122)
    xlim[0] = max(0, i-50)
    xlim[1] = max(50, i)
    plt.title("Scrolling with time-series data")
    plt.ylim(0,30)
    plt.xlim(xlim[0],xlim[1])
    plt.plot(x_data, y_data)      

ani = animation.FuncAnimation(fig, plot)

すると次のようなアニメーションができました。

 

オブジェクト指向でアニメーション

matplotlibはオブジェクト指向でグラフを描く方が、細かい設定をすることができます。

そこで、先程と同じアニメーションをオブジェクト指向で書き直してみました。

次回の記事に向けての伏線です)

fig = plt.figure(figsize=(8,4), tight_layout=True)
ax1 = fig.add_subplot(1,2,1)
ax2 = fig.add_subplot(1,2,2)
line1,= ax1.plot([], [])
line2,= ax2.plot([], [])
ax1.set_xlim(0, 50)
ax1.set_ylim(0, 30)
ax2.set_xlim(0, 50)
ax2.set_ylim(0, 30)

x_data = []
y_data = []

X = tm_in_queue
Y = len_in_queue

def plot(i):
    if tm_in_queue[i] > 50:
        x0 = tm_in_queue[i]-50
        x1 = tm_in_queue[i]
        ax1.set_xlim(x0, x1)
    else:
        ax1.set_xlim(0, 50)

    line1.set_data(X, Y)
    ax1.set_title("Scrolling on one-shot data")

    x00 = max(0, tm_in_queue[i]-50)
    x11 = max(50, tm_in_queue[i])
    ax2.set_xlim(x00, x11)

    x_data.append(X[i])
    y_data.append(Y[i])

    line2.set_data(x_data, y_data)
    ax2.set_title("Scrolling with real-time data")

ani = animation.FuncAnimation(fig, plot)