3. 道路ネットワーク上の移動モデルを作成する②

3.1. ネットワーク上の移動を表現する

前節で作成したネットワーク上を、エージェントがランダムに移動するモデルを作成します。移動するエージェントは、以下のようなルールで移動するものとします。

  1. 最初に、スタート地点と目標地点を設定する

    (ただし目標地点は、スタート地点に繋がっているノードから選ぶものとする)

  2. 目標地点まで、リンク上を移動する

  3. 目標地点に着いたら、現在地点に繋がっているノードの中からランダムに新しい目標地点を選択

  4. 以降、2と3を繰り返す

それでは、作成していきましょう。以下の操作をしていきます。

  • cityの下に「hito」という名前のエージェント種別を追加します。

  • hitoのエージェント変数には「target」と「speed」を追加します。

  • マップ出力設定でhitoを出力します。表示色はnodeとは異なる色、たとえば赤色にしておくとよいでしょう。

その上で、univ_initの末尾でhitoエージェントを1体だけ生成します。

def univ_init(self):
    
    # ノードの読み込みと作成(略)

    # リンクの読み込みと作成(略)
    
    # ここを追記(エージェントの作成)
    create_agt(Universe.city.hito)

hitoの生成が出来たら、agt_initに以下のようなルールを書き込んでください。ここでは、移動速度の定義、スタート地点の選択、スタート地点への移動、目標ノードの選択を行っています。

def agt_init(self):

    self.speed = 0.1  # 移動速度を定義
    start_node = randchoice(Universe.all_nodes)  # 開始地点のノードをランダムに選択
    self.x = start_node.x  # x座標を開始ノードに合わせる
    self.y = start_node.y  # y座標を開始ノードに合わせる
    self.target = randchoice(start_node.link)  # 目標ノードを、開始ノードから繋がっているノードからランダムに選択

次に、agt_stepでネットワーク上を目標地点に向かって進み、目標地点に着いたら次の目標地点を選択するルールを書き込みます。

def agt_step(self):

    if self.pursue(self.target, self.speed) != -1:  # 目標地点に向かって進む。到達したら、if文の中に入る
        self.target = randchoice(self.target.link)  # 目標地点を更新

ここでpursueという新たな関数が登場しました。pursueはartisocの組み込み関数で、引数に目標地点とそこへ向かう速度を指定できます。pursueは、正常終了時は-1、目標地点が近すぎて進めなかった時は進めなかった距離を戻り値として返すという特徴があります。今回はその仕様を利用し、戻り値が-1でない(つまり今回の場合は目標地点に着いた)時に新たな目標地点を更新するというルールを書いています。

それでは、モデルを再生してみてください。hitoがnode上をリンクに沿ってランダムに移動する様子が観察できるはずです。

.. image:: image_tutorial4-3-3.gif
   :align: center
   :scale: 100%

3.2. ネットワーク上の移動を出力する

最後に、ネットワーク上の移動をcsvファイルに出力してみましょう。artisoc内でしか見られないモデルの挙動を外部ファイルに出力することで、より詳細な分析等が可能になります。

出力するファイルは以下のようなものになります。目標地点にたどり着いた際、1列目にステップ数を、2列目にノード番号を記録したデータです。

step,node_number
3, 4
6, 2
...

まずは、univ_initの冒頭でファイルを作成し、ヘッダ行を記入します。

def univ_init(self):
    
    # ファイルの作成とヘッダ行の記入
    import csv
    with open("output.csv", mode="w") as f:
        fieldnames = ['step', 'node_number']
        writer = csv.DictWriter(f, fieldnames)
        writer.writeheader()
    
    # ノードとリンクの作成(略)
    
    

追記部分の1行目では、csvモジュールをインポートしています。

2行目ではwith openを用いてoutput.csvという名前のファイルを新たに作成しています。mode="w"とはデータを上書きする設定のことで、すでに同名のファイルが存在している場合はそれに上書きされます。ちなみにmode="r"で読み込み、mode="a"でファイルへの追記になります。

3行目で、fieldnamesという名前の列名リストを作成しています。これが出力ファイルのヘッダ行になります。

4行目で、writerオブジェクトを作成しています。これはcsvファイルに書き込むための道具を作ったものと考えてください。DictWriterの引数に先ほどのfieldnamesを指定することで、列名が指定されます。

5行目で、指定した列名をヘッダーに書き込みます。

続いて、エージェントがノードに到着するたびにそのステップ数とノード番号をファイルに追記するルールを作成します。データの書き込みはノードへたどり着いた際に行うため、ルールはhitoのagt_stepにあるif文の中に追記します。以下のように書いてみましょう。

def agt_step(self):
    
    # csvインポートを追記
    import csv

    if self.pursue(self.target, self.speed) != -1: 
        
        # ここから追記
        with open("output.csv", mode="a") as f:
            fieldnames = ['step', 'node_number']
            writer = csv.DictWriter(f, fieldnames=fieldnames)
            data = {'step': count_step(), 'node_number': self.target.node_number}
            writer.writerow(data)
        # ここまで追記
        
        self.target = randchoice(self.target.link)
        

まず、agt_stepの冒頭でcsvモジュールをインポートします。

if文内の追記部分の1行目で、output.csvを読み込んでいます。今回はmode="a"とし、ファイルに追記していくこととしています。

2行目でfieldnamesという列名リストを定義し、これを用いて3行目でwriterオブジェクトを作成します。これで、ファイルに書き込むための道具ができます。

4行目で、書き込むデータを作成しています。現在のステップ数とノード番号を辞書型で定義しています。

5行目で、そのデータをファイルの末尾の行に追記します。

この状態で実行してみてください。実行後、実行画面左下の「結果ファイル」画面にoutput.csvが出力されているはずです。ファイルをクリックしてダウンロードし、正しい出力がされていることを確認してください。このようにファイル出力を利用すれば、Excelや他の統計ソフトを用いた分析も可能になります。

../../_images/image_tutorial4-3-2.png ../../_images/image_tutorial4-3-1.png

3.3. ユーザ定義関数

これで、到着地点のノードを記録に残すことができました。しかし、よく考えてみると最初の出発地点のノードが記録されていません。最初の出発地点を記録するためには、agt_initで出発地点を決定した直後に、先ほどと同じルールを書いてファイル出力をすることになります。

しかし、まったく同じルールを複数の場所に書くのは望ましくありません。面倒ですし、ルールに変更が生じたら全ての箇所を修正する必要があります。モデルが大きくなってくると、同じルールがどこにあるかを管理しきれなくなってしまい、バグのもとになります。

これを防ぐためには、ユーザ定義関数を利用します。関数とはルールのまとまりを定義したものです。artisoc Cloudには多くの組み込み関数が用意されていますが、関数は自分で作成することもできます。今回は、ステップ数とノード番号をファイルに追記するという関数を作成しましょう。それを最初の出発時と各地点のノード到着時に呼び出すことで、同じルールを2度書かずに済みます。

それでは、関数を定義しましょう。hitoのルールエディタに、以下のように記述します。

def agt_init(self):
    
    # (略)

def agt_step(self):

    # (略)

# ノードをファイルに出力する関数の定義
def output_node(self, node):
    
    import csv
    with open("output.csv", mode="a") as f:
        fieldnames = ['step', 'node_number']
        writer = csv.DictWriter(f, fieldnames=fieldnames)
        data = {'step': count_step(), 'node_number': node.node_number}  # 引数nodeを使う
        writer.writerow(data)

関数を定義するには、def 関数名(self, 引数1, 引数2, ...):と記述します。今回は関数名としてoutput_nodeを指定し、引数にはnodeを指定しています。引数とは関数の内部で利用できる値のことですが、具体的な使い方をこのあとで見ていきます。改行したあと、関数内で実行するルールを記述します。インデントを1段下げることに注意しましょう。

関数内で実行するルールは、先ほど作成したファイル追記のルールとほとんど同じです。1ヵ所だけ、最後から2行目で書き込むノード番号を指定する部分で、'node_number': node.node_numberと、引数nodeを利用するよう書き換えています。こうすることで、関数を呼びだすときに出力したいノードを引数として指定することができます。

それでは、関数を呼び出す部分を作成しましょう。agt_initには以下のように追記します。

def agt_init(self):

    self.speed = 0.1
    start_node = randchoice(Universe.all_nodes)
    self.x = start_node.x 
    self.y = start_node.y
    self.target = randchoice(start_node.link)
    
    # スタート地点を出力するルールを追記
    self.output_node(start_node)

関数を呼び出すためには、self.関数名(引数1, 引数2, ...)と記述します。今回は引数として出発地点のノードstart_nodeを指定しています。これで関数を呼び出し、ステップ数と出発地点のノード番号をファイルに追記することができます。

また、agt_stepでもファイル出力の処理を関数に置き換えましょう。

def agt_step(self):

    import csv

    if self.pursue(self.target, self.speed) != -1: 
        
        # ファイル出力を関数に置き換える
        self.output_node(self.target)
        
        self.target = randchoice(self.target.link)

今度は引数として、到着したノードself.targetを指定しています。ルールとしても1行で済むため、if文の中がかなりすっきりしました。

この状態で実行し、output.csvを見てみましょう。シミュレーション開始時点、つまり0ステップ目でのノード番号が追記されていることが確認できるはずです。

3.4. 戻り値のある関数

補足として、戻り値のある関数の作成方法を解説しておきます。

今回作成した関数は、ファイル出力という操作のみを行うものでした。一方で、関数には操作の結果として戻り値 (返り値)を返すものもあります。たとえばcreate_agt関数はエージェント作成という操作のみでなく、そのエージェントを戻り値として返します。以下のような操作はこれまで何度か登場しましたが、これも戻り値を利用したものです。

one = create_agt(Universe.city.hito)  # エージェントを作成し、戻り値を変数oneに格納
one.x = 25  # 作成したエージェントのx座標に値を代入

戻り値のある関数を作成するには、関数の末尾でreturnを用います。単純な例ですが、以下の関数では引数aとbを足した結果を戻り値として返しています。

def myfunction(self, a, b):
    c = a + b
    return c

関数をうまく活用すると、複雑なルールもかなりすっきりと書けるようになります。ぜひ自分のモデルに応用してみてください。

3.5. 参考

この章のサンプルモデルは次の通りです。

チュートリアル4-3(道路ネットワーク②)

3.6. 練習問題

hitoエージェントを2人に増やし、それぞれのエージェントの行動記録を2つのファイルに分けて出力してみましょう。関数を増やすことなく、最小限の修正でこれを実現するには、どうすればよいでしょうか。

  • ヒント:output_node関数の引数にファイル名を追加します。hitoエージェントの属性としてファイル名を追加し、agt_initでidを利用してファイル名を生成し、関数を呼び出すときにそれを利用します。