はじめに
こんにちは,高校三年生をやっている者です.初投稿となる今回はエスカレーターのシミュレーションをご紹介しようと思います.言語はPython3です.
エスカレーターについて
エスカレーターの利用者は立ち止まる人と歩く人の2タイプに大別されます.日本では東日本は左,西日本は右というように,立ち止まる位置が地域によって違い,海外でも同様の現象が見られます.実は,エスカレーターの立ち止まる位置というのはゲーム理論におけるナッシュ均衡により決定されています.立ち止まっている人は後ろから歩いている人に衝突されるとストレスを感じ,次回以降違う側に乗りたくなるというような具合です.
シミュレーション概要
今回のシミュレーションでは立ち止まる人と歩く人を半分ずつ生成し,初期位置が左右どちらになるかはランダムに設定します.また,利用者全員の左右の位置に対する好感度を測り,その数値の大小によって次回の利用する側を決定します.これらの操作により,立ち止まる人が利用する位置と歩く人が利用する位置をそれぞれエスカレーターの片方に収束させていきます.
ソースコード・説明
それでは,ソースコードとそれに対する説明を以下に記していきます.
利用者の定義
まず,立ち止まる人(=StandHuman)と歩く人(=WalkHuman)のクラスをそれぞれ定義します.両者の違いは,止まるか歩くか(=active),速さ(=v, 物理的にはspeedのsの方が適切かと思いますが)の二点のみとなります.
class StandHuman:
    def __init__(self):
        self.active = False
        self.v = 0 #speed
        self.x = 0 #position(left/right)
        self.l_like = 0 #how much a human like to ride on the left
        self.r_like = 0 #how much a human like to ride on the right
class WalkHuman:
    def __init__(self):
        self.active = True
        self.v =1 #speed
        self.x = 0 #position(left/right)
        self.l_like = 0 #how much a human like to ride on the left
        self.r_like = 0 #how much a human like to ride on the right
利用者の生成
0から生成する人数(=NUMBER_OF_HUMANS)-1までの整数列を要素とするリストを用意し,そのリストの要素をシャッフルした後,要素が奇数か偶数かを判定することで,全利用者の利用する順番にランダム性を持たせながら立ち止まる人と歩く人を半数ずつ生成しています.
def human_random():
    odd_or_even = list(range(NUMBER_OF_HUMANS))
    random.shuffle(odd_or_even)
    for i in range(NUMBER_OF_HUMANS):
        if odd_or_even[i] % 2 == 0:
            human = StandHuman()
        else:
            human = WalkHuman()
        human_list.append(human)
エスカレーターに乗る位置(左右)の決定
エスカレーターに乗る位置は,その時々における左右それぞれへの好感度をもとに決定されます.左の好感度が高ければ左に,右の好感度が高ければ右に,左右の好感度が同値であればランダムに左右が決定されています.左右の好感度の初期値はどちらも0に設定されているため,初回利用時においてはランダムに左右が決定されることになります.
def side_decision(human):
    l = human.l_like
    r = human.r_like
    if l > r:
        human.x = 0
    elif l < r:
        human.x = 1
    else:
        random_x = random.random()
        if random_x <= 0.5:
            human.x = 0
        else:
            human.x = 1
利用者をエスカレーターに乗せる
add_human関数は利用者をエスカレーターに乗せる操作のみを行います.ただエスカレーターは便宜上,①立ち止まる人用の左 ②歩く人用の左 ③立ち止まる人用の右 ④歩く人用の右,の四列を用意しています.
def add_human(human):
    if human.active == False:
        if human.x == 0: #if left
            escalator[0][0] = human
        else: #if right
            escalator[2][0] = human
    else:
        if human.x == 0: #if left
            escalator[1][0] = human
        else: #if right
            escalator[3][0] = human
エスカレーターを動かす
 エスカレーターを一段動かします.実際のプログラムとしては利用者を
一段上に移し,最高段の場合は0(初期値)に戻しています.
def shift():
    for i in range(STEPS-1):
        escalator[0][STEPS-i-1] = escalator[0][STEPS-i-2]
        escalator[1][STEPS-i-1] = escalator[1][STEPS-i-2]
        escalator[2][STEPS-i-1] = escalator[2][STEPS-i-2]
        escalator[3][STEPS-i-1] = escalator[3][STEPS-i-2]
    escalator[0][0] = 0
    escalator[1][0] = 0
    escalator[2][0] = 0
    escalator[3][0] = 0
衝突の確認・衝突時の処理
 crash関数では衝突時の処理を行っています.ここでは,衝突したときの位置(段数)が利用者の感じるストレスを左右していて,低い段で衝突するほどストレスを感じやすいように設定しています.また,立ち止まっている人と歩いている人が感じるストレスのランダム性を維持するために両者の処理を分けています.利用者が感じたストレスはそのまま好感度の低下に繋がります.
 crash_checker関数では衝突を検出し,衝突した利用者をcrash関数に引き渡します.
def crash(front, front_position, behind):
    x = random.random() * STEPS
    if x <= 1-(front_position + 1)/STEPS: 
        if front.x == 0: #if left
            front.l_like -= x
        else: #if right
            front.r_like -= x
    y = random.random() * STEPS
    if y <= 1-(front_position + 1)/STEPS:
        if front.x == 0: #if left
            behind.l_like -= y
        else: #if right
            behind.r_like -= y
        
def crash_checker():
    for i in range(STEPS): #left
        if escalator[0][i] != 0 and escalator[1][i] != 0:
            crash(escalator[0][i], i, escalator[1][i])
    for i in range(STEPS): #right
        if escalator[2][i] != 0 and escalator[3][i] != 0:
            crash(escalator[2][i], i, escalator[3][i])
歩く人のみの処理
歩く人には“歩く”処理が必要になるため,エスカレーターを動かすshift関数とは別に,新たにwalk関数を設けます.
def end_checker_for_walker(): 
    escalator[1][STEPS-1] = 0
    escalator[3][STEPS-1] = 0
def walk():
    for i in range(STEPS):
        l = escalator[1][i]
        r = escalator[3][i]
        if l != 0:
            escalator[1][STEPS-i-1+(l.v)] = escalator[1][STEPS-i-1] 
        if r != 0:
            escalator[3][STEPS-i-1+(r.v)] = escalator[3][STEPS-i-1]
ソースコード全体
以上でソースコードの説明はすべてとなります.ソースコード全体は以下のようになります.
import random
import pandas as pd
NUMBER_OF_HUMANS = 100
TIME_END = 1000000
STEPS = 100
human_list = list()
escalator = [[0] * STEPS for i in range(4)] #[[sl], [wl], [sr], [wr]]
class StandHuman:
    def __init__(self):
        self.active = False
        self.v = 0 #speed
        self.x = 0 #position(left/right)
        self.l_like = 0 #how much a human like to ride on the left
        self.r_like = 0 #how much a human like to ride on the right
class WalkHuman:
    def __init__(self):
        self.active = True
        self.v =1 #speed
        self.x = 0 #position(left/right)
        self.l_like = 0 #how much a human like to ride on the left
        self.r_like = 0 #how much a human like to ride on the right
def human_random():
    odd_or_even = list(range(NUMBER_OF_HUMANS))
    random.shuffle(odd_or_even)
    for i in range(NUMBER_OF_HUMANS):
        if odd_or_even[i] % 2 == 0:
            human = StandHuman()
        else:
            human = WalkHuman()
        human_list.append(human)
def shift():
    for i in range(STEPS-1):
        escalator[0][STEPS-i-1] = escalator[0][STEPS-i-2]
        escalator[1][STEPS-i-1] = escalator[1][STEPS-i-2]
        escalator[2][STEPS-i-1] = escalator[2][STEPS-i-2]
        escalator[3][STEPS-i-1] = escalator[3][STEPS-i-2]
    escalator[0][0] = 0
    escalator[1][0] = 0
    escalator[2][0] = 0
    escalator[3][0] = 0
def crash(front, front_position, behind):
    x = random.random() * STEPS
    if x <= 1-(front_position + 1)/STEPS: 
        if front.x == 0: #if left
            front.l_like -= x
        else: #if right
            front.r_like -= x
    y = random.random() * STEPS
    if y <= 1-(front_position + 1)/STEPS:
        if front.x == 0: #if left
            behind.l_like -= y
        else: #if right
            behind.r_like -= y
        
def crash_checker():
    for i in range(STEPS): #left
        if escalator[0][i] != 0 and escalator[1][i] != 0:
            crash(escalator[0][i], i, escalator[1][i])
    for i in range(STEPS): #right
        if escalator[2][i] != 0 and escalator[3][i] != 0:
            crash(escalator[2][i], i, escalator[3][i])
def walk():
    for i in range(STEPS):
        l = escalator[1][i]
        r = escalator[3][i]
        if l != 0:
            escalator[1][STEPS-i-1+(l.v)] = escalator[1][STEPS-i-1] 
        if r != 0:
            escalator[3][STEPS-i-1+(r.v)] = escalator[3][STEPS-i-1]
def side_decision(human):
    l = human.l_like
    r = human.r_like
    if l > r:
        human.x = 0
    elif l < r:
        human.x = 1
    else:
        random_x = random.random()
        if random_x <= 0.5:
            human.x = 0
        else:
            human.x = 1
def add_human(human):
    if human.active == False:
        if human.x == 0: #if left
            escalator[0][0] = human
        else: #if right
            escalator[2][0] = human
    else:
        if human.x == 0: #if left
            escalator[1][0] = human
        else: #if right
            escalator[3][0] = human
def main():
    for i in range(TIME_END):
        shift() 
        crash_checker()
        walk() 
        crash_checker()
        h = human_list[i % NUMBER_OF_HUMANS]
        side_decision(h)
        add_human(h) 
human_random()
main()
シミュレーション結果
今回は試行回数を100万回,利用者数を100人(立:50人,歩:50人),エスカレーターの段数を100段に設定してシミュレーションしました.以下の結果を見る限り上手く収束してくれているという印象を受けます.
エスカレーターの結果
指定した試行回数に到達した時のエスカレーターを簡易的に可視化したものを以下に掲載しています.また,左右の列における立ち止まっている人と歩いている人の総数と両者の割合も出力しました.(s:立ち止まっている人,w:歩いている人,0:空き)
| s 0 | 0 0 |
| 0 0 | 0 w |
| s 0 | 0 w |
| s 0 | 0 w |
| s 0 | 0 w |
| s 0 | 0 0 |
| 0 0 | 0 0 |
| s 0 | 0 0 |
| s 0 | 0 0 |
| s 0 | 0 0 |
| 0 0 | 0 0 |
| 0 0 | 0 0 |
| 0 0 | 0 0 |
| 0 0 | 0 0 |
| 0 0 | 0 w |
| 0 0 | 0 w |
| s 0 | 0 w |
| 0 0 | 0 w |
| 0 0 | 0 0 |
| s 0 | 0 0 |
| 0 0 | 0 0 |
| s 0 | 0 0 |
| 0 0 | 0 0 |
| 0 0 | 0 0 |
| s 0 | 0 0 |
| 0 0 | 0 0 |
| s 0 | 0 0 |
| 0 0 | 0 0 |
| 0 0 | 0 0 |
| s 0 | 0 0 |
| 0 0 | 0 0 |
| s 0 | 0 0 |
| s 0 | 0 0 |
| s 0 | 0 0 |
| 0 0 | 0 0 |
| 0 0 | 0 0 |
| 0 0 | 0 0 |
| s 0 | 0 w |
| s 0 | 0 w |
| s 0 | 0 w |
| s 0 | 0 w |
| s 0 | 0 0 |
| s 0 | 0 0 |
| s 0 | 0 0 |
| s 0 | 0 w |
| s 0 | 0 w |
| s 0 | 0 w |
| 0 0 | 0 0 |
| s 0 | 0 0 |
| 0 0 | 0 0 |
| 0 0 | 0 w |
| s 0 | 0 0 |
| s 0 | 0 0 |
| s 0 | 0 w |
| 0 0 | 0 0 |
| 0 0 | 0 0 |
| s 0 | 0 0 |
| 0 0 | 0 w |
| 0 0 | 0 w |
| s 0 | 0 0 |
| s 0 | 0 w |
| 0 0 | 0 0 |
| s 0 | 0 0 |
| s 0 | 0 0 |
| 0 0 | 0 w |
| 0 0 | 0 w |
| s 0 | 0 0 |
| 0 0 | 0 0 |
| s 0 | 0 0 |
| s 0 | 0 w |
| 0 0 | 0 0 |
| 0 0 | 0 w |
| s 0 | 0 0 |
| s 0 | 0 w |
| s 0 | 0 0 |
| 0 0 | 0 0 |
| s 0 | 0 0 |
| 0 0 | 0 w |
| 0 w | 0 w |
| 0 0 | 0 w |
| 0 w | 0 0 |
| 0 w | 0 0 |
| s 0 | 0 0 |
| 0 0 | 0 w |
| 0 0 | 0 0 |
| 0 0 | 0 0 |
| s 0 | 0 0 |
| s 0 | 0 w |
| 0 0 | 0 0 |
| 0 w | 0 0 |
| 0 0 | 0 w |
| s 0 | 0 0 |
| 0 0 | 0 0 |
| 0 w | 0 w |
| s 0 | 0 0 |
| 0 0 | 0 0 |
| s 0 | 0 w |
| 0 0 | 0 0 |
| s 0 | 0 0 |
| 0 0 | 0 w |
# ----------------------------------------------#
WalkHuman/StandHuman of left:  0.1 W:S= 5 50
WalkHuman/StandHuman of right:  34.0 W:S= 34 0
好感度(ストレス)の比較
全利用者(最初に生成した100人)がそれぞれエスカレーターの左右どちらを好むか(嫌がらないか)を表にして比較しました.
            Left  Right
StandHuman    50      0
WalkHuman      5     45
おわりに
 結果としてはまずまずのシミュレーションになったかと思います.私自身初めてのシミュレーション作成であったため詰めの甘い部分が多くあるのではないかと思われますが,今後も可能な限り改良を重ねたいと思います.