このチュートリアルでは、以下の環境を想定します。
なお、githubからダウンロードするWindows版のRepast SimphonyはEclipseとともにダウンロードされます。
ダウンロードしたexeファイルをダブルクリックしてインストールします。
エージェントシミュレーションを用いて消費行動と販売に関する分析を行うとして、以下のような消費者の行動を想定してみます。
「消費者は、ある商品を購入しようとするが、その際に、自分の嗜好にあったものを探し回って購入する。しかし、ある程度探しても見つからなければ、あきらめる。」
という行動です。これは、消費者が探索財を購入する際に、ある程度の探索コストはかけられるが、それ以上であれば、あきらめてしまう、という購買行動を想定しています。また、本チュートリアルでは、上記のシナリオを最終的な目標としますが、一度にモデルを構築するのではなく、以下の順序で構築していくこととします。
このモデルでは以下のエージェントを作成します。
では、それぞれのエージェントはJavaのクラスとして定義されますが、それぞれについて、どのようなプロパティとメソッドを定義すべきか考えていきたいと思います。
消費者エージェントでは、仮想市場に存在させるために必要となるプロパティ、商品を評価する際の属性の閾値、そして、探索にかけられるコストをプロパティとして定義します。商品選択に関わる属性は複数あるのが一般的ですが、通常は、それらの属性の全体を主観的に判断して消費者は購入の意思決定をしていますので、本チュートリアルでも、属性の閾値は、各属性の合計をイメージして設計することとします。また、探索コストも異なる値を設定できるようにします。これは、企業が消費者に掛かる探索コストを軽減させる取り組みをした際の効果を評価するためです。
ここでは、新たにMktng01というRepast Simphonyのプロジェクトを作成して実装します。プロジェクトの作り方については、前のチュートリアル(「Repast Simphonyを使ってみよう(1)」)を参照してください。
private Grid<Object> grid;
private int sumUtil; // 商品属性合計の閾値
private int searchCost; // 探索コスト
private int searchCnt = 0; // 探索回数
また、コンストラクタとしては次のようなものを定義します。
public Consumer(Grid<Object> grid, int searchCost, int sumUtil){
this.grid = grid;
this.searchCost = searchCost;
this.sumUtil = sumUtil;
}
消費者は、シミュレーションが進行するごとに、商品を探し、見つけたものを吟味するという動作をするのですが、この動作は、@ScheduledMethodのメソッドを利用して実行します。このメソッドでは、消費者エージェントがいる場所から、周りにある仮想市場のセルを探索し、探索したセルの中から、基準属性値の高いものを選択する、もし、基準属性値を満足するものがない場合には、セルを移動して、探索を続ける、という処理を行うこととします。なお、基本的なコードの書き方については、前のチュートリアルを参照してください。このメソッドは探索が自身の探索コスト内である場合にのみ処理されるようにしたいので、メソッドは次のように定義します。
@ScheduledMethod(start = 1, interval = 5)
public void evaluate(){
// 探索回数かどうかの判定
if(searchCnt<searchCost){
// 探索処理を記述する(以下に説明)
}
}
それでは、メソッド内の処理を説明します。まずは、自身の存在しているセルの変数と自分の周辺にある購入されていない商品の探索についてです。自分のいる場所は、GridPointを用いて取得します。商品の探索については、自分が存在しているセルの周辺のセルにある購入されていない商品を探して、そのセルをList型の変数に格納するという処理をかきます。ここでは、探索するセルは1セル分としています。また、購入されていない商品のエージェントをProduct.javaで定義するとします。エラーを消すために、空のProduct.javaを作成しておくとよいでしょう。
// 自分のいる場所を把握
GridPoint ptCons = grid.getLocation(this);
// 周辺セルにある商品を探し評価する
GridCellNgh<Product> nghCreator=new GridCellNgh<Product>(grid,ptCons,Product.class,1,1);
List<GridCell<Product>> gridCells=nghCreator.getNeighborhood(true);
次に、存在する商品の属性を評価して、希望を最も満足するものを探します。ここでは、対象となるセルの変数を定義するとともに、ダミーの整数を2つ定義し ます。また、購入されていない商品には、属性情報を取り出すための2つのメソッド(getAtt1()とgetAtt2())が定義されているものとします。
// 属性評価の和が最も高いProductを選択する
GridPoint prefProd = null; // 選択される商品の場所を示す
int tmp=-1; // 比較評価用の仮変数
int tmpHigh=-1; // 比較評価用の仮変数
そして、gridCellsに入っている商品を一つずつ評価していきます。
// 周りのセルを格納したListから一つずつProductエージェントを取り出して評価する
for(GridCell<Product>cell:gridCells){
// 一つずつ属性値を取り出す
for(Product prd:cell.items()){
tmp=prd.getAtt1()+prd.getAtt2();
}
// 取り出した属性値とそれまでの最高の属性値を比較して、高ければ、それを選択対象にする
if(tmp>tmpHigh){
prefProd = cell.getPoint();
tmpHigh=tmp;
}
}
ここで、tmpHighの値が正の値であれば、商品が見つかったということになるので、消費者エージェント以降、商品の探索をしなくなります。また、そうでない場合には、商品が見つからなかったということになりますので、その場合には、セルを移動して再度商品を探索することになります。ここでは、購入済みの処理をするメソッドをbuyItem、移動するメソッドをmoveTowardとして、定義します。また、探索コストの閾値として設定されているsearchCostと、実際の移動回数の比較を行うために、searchCntを加算します。
// 選択対象があれば(tmpHigh>0)ならば、buyItemでそのセルにあるProductエージェントを選択、そうでなければ、moveTowardで移動する
if(tmpHigh>0) {
buyItem(ptCons, prefProd);
}else{
moveToward(ptCons);
}
// 探索回数加算
searchCnt++;
2つのメソッドは次のようになります。ここでは、消費者エージェントを消滅させて、代わりにその位置に、購入済み商品を配置するという処理を追加しました。ここでも、無用なエラーを消すために、空の購入済み商品のクラス(SelectedProduct.java)を作成しておくとよいでしょう。
// 商品購入用のメソッド
public void buyItem(GridPoint ptCons, GridPoint ptProd){
// 選択された商品を配置
Object objProd=grid.getObjectAt(ptProd.getX(),ptProd.getY());
Context<Object> context=ContextUtils.getContext(objProd);
context.remove(objProd);
SelectedProduct selected = new SelectedProduct(grid);
context.add(selected);
grid.moveTo(selected, ptProd.getX(), ptProd.getY());
// 消費者の消去
Object objCons=grid.getObjectAt(ptCons.getX(),ptCons.getY());
context=ContextUtils.getContext(objCons);
context.remove(objCons);
}
他方の移動するメソッドは次のようになります。移動する方向については、ランダムに決定するものとし、Repast Simphonyが提供している乱数関数を使用します。
// 移動用のメソッド
public void moveToward(GridPoint ptCons){
// 前後左右のいずれかのセルに移動する
int dx = RandomHelper.nextIntFromTo(-1, 1);
int dy = RandomHelper.nextIntFromTo(-1, 1);
grid.moveTo(this, ptCons.getX() + dx, ptCons.getY() + dy);
}
商品に関するクラスを作成しないと消えないエラーがいくつか残りますが、消費者エージェントの処理は以上となります。
次に購入されていない商品エージェントを作成します。商品エージェントは、仮想空間に存在するのに必要なプロパティと、商品属性用のプロパティを持ち、また、商品属性を取り出すためのメソッドを持つ必要があります。インスタンスを作るときに、この属性値を入力する必要がありますが、入力の仕方については、 後程、説明します。
package mktng01;
import repast.simphony.space.grid.Grid;
public class Product {
private Grid<Object> grid;
private int att1; // 製品属性1
private int att2; // 製品属性2
public Product(Grid<Object> grid, int att1, int att2){
this.grid = grid;
this.att1=att1;
this.att2=att2;
}
public int getAtt1() {
return att1;
}
public void setAtt1(int att1) {
this.att1 = att1;
}
public int getAtt2() {
return att2;
}
public void setAtt2(int att2) {
this.att2 = att2;
}
}
購入された商品エージェントは、仮想空間に存在するのに必要なプロパティを持つだけですので、購入されていない商品エージェントよりも簡単な構造になります。
package mktng01;
import repast.simphony.space.grid.Grid;
public class SelectedProduct {
private Grid<Object> grid;
public SelectedProduct(Grid<Object> grid){
this.grid = grid;
}
}
以上でエージェントの作成は終了です。
初期化を行うクラスは、RepastのContextBuilderというクラスを拡張して作成します。手順は以下のようになります。
すると、以下のようなスタブが現れます。
ここで、ContextBuilder<T>
となっているところのTをObjectに変更します。また、MktngBuilder01のところにマウスを持っていき、そこで、右クリックをして、Source > Override/Implement methodsを選択します。
新たに開いたウィンドウでOKを押下すると、それまであったエラーはなくなります。
ContextBuilderは、その名の通り、Contextを作ります(Buildします)。Contextは、エージェントの集まりです。そして、そのエージェントに関連して、Projectionという構造も取ります。この例では、Gridがprojectionになります。では、実際にContextとProjectionをコーディングしてみましょう。消費者の商品購入への閾値(sumUtil)は、14~18までの間の値がランダムに設定されるものとし、探索コストの閾値(searchCost)は5で固定であるとします。また、商品の属性値(att1、att2)は、0~9までの値がランダムに選択されるものとします。
ここでも、必要に応じてライブラリをimportする必要がありますが、同名のクラスがありますので、正しくコードを入力してもエラーが消えない場合には、importするライブラリを変更してみてください。(例えば、WrapAroundBordersは、repast.simphony.continuous.WrapAroundBordersではなく、repast.simphony.space.grid.WrapAroundBordersを使う、等です)
@Override
public Context build(Context<Object> context) {
// バッチ処理用の追加
Parameters params=RunEnvironment.getInstance().getParameters();
int searchCost = params.getInteger("searchCost");
// コンテキストの設定
context.setId("Mktng01"); // コンテキスト名。context.xmlと同一である必要がある。
// gridの生成。ここでは、50×50のグリッドの空間を作成。WrapAroundBorderは空間の端が反対の端とつながっている空間
GridFactory gridFactory=GridFactoryFinder.createGridFactory(null);
Grid<Object>grid=gridFactory.createGrid("grid", context,
new GridBuilderParameters<Object>(new WrapAroundBorders(),
new SimpleGridAdder<Object>(),
true,50,50));
// 消費者エージェントを仮想空間に配置
int consCount = 50; // シナリオ1で1、シナリオ2で50
//int searchCost = 5; // 探索期間
// 消費者をグリッド空間に配置。同一のセルに複数の消費者が配置されないようにする
for(int i=0;i<consCount;i++){
// 作成した商品エージェントをランダムに配置
boolean posDuplicate = true;
while(posDuplicate){ // 新規の配置になるまで繰り返す
int x = RandomHelper.nextIntFromTo(0, 49);
int y = RandomHelper.nextIntFromTo(0, 49);
int tmpCnt=0;
for(Object o: grid.getObjectsAt(x,y))tmpCnt++; // 移動予定先にエージェントが配置済かどうか
if(tmpCnt==0){
// セルが空なら置く
int sumUtil = RandomHelper.nextIntFromTo(14, 18);
Consumer cons = new Consumer(grid, searchCost, sumUtil);
context.add(cons);
grid.moveTo(cons, x, y);
posDuplicate = false;
}
}
}
// 200の商品属性を1~10の間でランダムに付与
int prodCount=200;
for(int i=0;i<prodCount;i++){
int att1=RandomHelper.nextIntFromTo(0, 9);
int att2=RandomHelper.nextIntFromTo(0, 9);
// 作成した商品エージェントをランダムに配置
boolean posDuplicate = true;
while(posDuplicate){ // 新規の配置になるまで繰り返す
int x = RandomHelper.nextIntFromTo(0, 49);
int y = RandomHelper.nextIntFromTo(0, 49);
int tmpCnt=0;
for(Object o: grid.getObjectsAt(x,y))tmpCnt++; // 移動予定先にエージェントが配置済みかチェック
if(tmpCnt==0){
// セルが空ならその場所に置く
Product prod = new Product(grid, att1, att2);
context.add(prod);
grid.moveTo(prod, x, y);
posDuplicate = false;
}
}
}
RunEnvironment.getInstance().endAt(51); // シミュレーションの停止条件(Tick値)
return context;
}
上記のコードを概説すると、まず、ContextにIDを付与します。これは、Mktng01.rs内のcontext.xmlの中のIDと同一なものにする必要があります。そして、GridFactoryクラスを使って、Gridを作成します。ここでは、エッジがつながった、縦横50マスのグリッドをgridという名前で作成しています。
以降のコードでは、1つの消費者エージェントを仮想空間に配置するとともに、200個の商品エージェントを、仮想空間にランダムに配置するという処理を行っています。また、コードの最後にはプログラムの停止までのクロックの回数を設定してあります。ここでは、51を設定してあります。
シミュレーションを動かす前に、先ほど少し言及した、context.xmlを作成します。このcontext.xmlはシミュレーションを動かす際に、どのような画面を作るかを設定するためのものです。context.xmlはMktng01.rsフォルダーの中にあるので、フォルダーを開いて、 context.xmlをダブルクリックして、ファイルを開きます。そして、 ここに、先ほど説明したように、Gridのprojectionを追加しておきます。
<context id="Mktng01" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://repast.org/scenario/context">
<projection type="grid" id="grid"/>
</context>
以上でプログラムは完成です。
では、実際にプログラムを動かしてみましょう。Runの三角ボタンをクリックして、Mktng01 Modelを選択します。
しばらくすると、下のような画面が開きます。ここから、表示するディスプレイなどの設定を行っていきます。
まず、Scenario TreeのData Loaderノードを右クリックして、Set Data Loaderを選択します。そこで、開くSelect Data Source Typeウィンドウにおいて、Custom ContextBuiler Implementationを選択して、Nextを押下します。次の画面で、コンボボックスにMktng01.Mktng01Builderが表示され ているはずですから、そのままNextを押下して、最後の画面でOKを押下します。なお、起動した際に「XML & Model Init Context Builder」というLoaderが設定されている場合には、それを削除しておきます。
次に、Displayボタンを右クリックして、Add displayを選択します。新たに開いたウィンドウでは、ウィンドウ名はデフォルトのままで、gridを左の窓から、右の窓に移し、Nextを押下します。
次の画面では、Consumer、Product、SelectedProductを左の窓から、右の窓に移し、Nextを押下します。
次の画面で、先ほど選んだ三種類のエージェントの画面上でのスタイルを選択します。スタイルを変更するためには、変更したいエージェントが選ばれた状態で、画面右の四角ボタンを押下します。
ここでは、
としました。
残りは特に設定する必要がないので、Nextを数回押下して、最後にOKを押下することで、シミュレータの設定は終わりです。毎回設定することが無いように、ここまで来たら、コンソール画面で保存を実行しておきましょう。
では、実際に、シミュレーションを動かしてみましょう。動かすためには以下の3つのボタンを利用します。
初期化ボタンを押下すると以下のような画面が表示されます。
ここで、ステップ動作ボタンを押下すると、中心の赤い四角の周りの青色の丸が一つ黄色に変わるか、あるいは、赤い四角(消費者)がどこかのグリッドに動くはずです。黄色に変わった時には、条件を満足する商品を見つけられたときで、そうでない場合には、グリッドを移動するという動作になっています。ステップ実行をしてみると、確認できると思います。
次に、この状態から、シナリオ2に移行してみます。シナリオ1と2の違いは、消費者の数の違いですから、ここでは、先のプログラムをそのまま使って、消費者の数を増やしてみましょう。消費者の数を増やすには、ContextBuilderを使ったクラスを書き換える必要があります。
現在は、一人だけを配置するようになっていますが、商品エージェントと同じように、複数の消費者を配置するようにします。ここでは、50人の消費者を配置してみます。
// 消費者エージェントを仮想空間に配置
int consCount = 50; // シナリオ1で1、シナリオ2で50
この変更を加えたのちに、シミュレーションを動かしてみましょう。下に示すように、消費者エージェントが50人配置されることがわかります。
実際に動かしてみると、すぐに商品を発見できる消費者となかなか発見できない消費者がいることがわかります。これは、消費者が期待する商品属性の合計が10~16以上であるのに対して、市場の商品が0~9の間で商品属性を持っており、市中にある商品が必ずしも消費者の好みに合っていないためです。ここでも、動作の確認にはステップ実行が便利です。
ここまでで、市場にいる異なる好みを持った消費者が市場で商品を探し、気に入ったものを見付けた場合に購入するという行動を模擬することができました。しかし、このシミュレーションは、確率的な動きのうち、一回だけの結果ですから、実際には、複数回実施し、期待値を求めるなどする必要があります。いわゆ る、モンテカルロシミュレーションです。Repast Simphonyではバッチ処理をすることで、モンテカルロシミュレーションも実現できますが、そのやり方については、シナリオ3で説明します。
では、最後にRepast Simphonyのバッチ機能を利用して、モンテカルロシミュレーションを行ってみます。そのために、以下の設定をします。
まず、シミュレーションにおいて、変化させるパラメータを設定します。つまり、パラメータをある値に設定して、複数回シミュレーションを実施して値を求めるということを繰り返し行い、パラメータの評価を シミュレーションにおいて行うということです。
ここでは、探索コストをパラメータにして、消費者の持つ探索コストの影響を評価してみます。変数の設定は、まず、実行画面を立ち上げて、左下のタブから「Parameters」を選択します。
次に画面中の「+」ボタンを押下すると以下の画面が現れます。
ここでは、Nameを既に設定してある変数であるsearchCost、Display NameをSearchCost、Typeをint、そして、Default Valueを5としておきます。以上で、パラメータの設定は終了です。
次に、これに対応したコードの変更を行います。このsearchCostという変数は、既に設定されていますが、新たに、Mktng01Builder.java以下のようなコードを追加します。
@Override
public Context build(Context<Object> context) {
// バッチ処理用の追加
Parameters params=RunEnvironment.getInstance().getParameters();
int searchCost = params.getInteger("searchCost");
// 以下略
そして、元々あった、searchCostの定義の箇所はコメントアウトしておきます。
//int searchCost = 50; // 探索期間
続いてファイル保存の設定を行います。ファイルの保存は、実行画面の初期画面である、「Scenario Tree」タブをクリックし、「Data Sets」を追加することから始めます。「Data Sets」を右クリックして、「Add Data Set」を押下すると、以下のような画面が表示されます。
ここでは、名称をData Set、そして、Data Set TypeをAggregateを選択して、Nextを押下します。すると保存するデータを選択する画面が開きます。Standard SoucesはTick Countが選ばれていますが、そのままにし「Method Data Source」タブを押下します。
ここで、「Add」ボタンを押し、記録するエージェントを設定します。ここでは、消費者が好みの商品を選択できた数を知りたいので、Source Nameとして、Selected Product Count、Agent Typeとして、SelectedProduct、Aggregate Operationで、Countを選択します。以降は特に設定の必要はないので、Nextを押下して終了します。
つづいて、このデータを保存する設定を行います。保存は、同じく「Scenario Tree」の「Text Sinks」から行います。先ほどと同様に、Text Sinksで右クリックして、「Add File Sink」を選択します。すると、以下のような画面が表示されます。
ここで、保存したいデータを選択します。ここでは、tickとSelected Product Countの両方を保存対象とします。Nextを押下するとファイルの保存箇所をきかれるので、ここでは、outputフォルダにOutput.csvという名前で保存するようにします。これは、今回使用しているプロジェクトのフォルダにOutput.csvという名前で保存することを意味します。また、「Insert Current Time into File Name」を選択しておくと、ファイル名と拡張子の間にシミュレーション実行時間を入れてくれるので、ファイル名がぶつからなくなります。
以上で設定は終了ですので、Finishを押下して、設定を終了します。この状態でいったん、シミュレーションを実行してみましょう。実行結果が、プロジェクトフォルダ内の、outputフォルダ内に、csv形式のファイルとして保存されているはずです。
次に、バッチ処理の設定を行います。変更するパラメータとしては、以下の2つを設定します。
乱数系列を複数設定するのは、このようにしておくことで、後から期待値の計算に用いることができるためです。実際には、もっと大量の試行が必要ですが、今回は、単純のため、100回としています。バッチ処理のパラメータの設定は、プログラムの実行画面から、黄色い色の雷マークを押すことで実行できます。
最初の画面では、モデルやシナリオの設定画面が表示されます。特に変更をしなくても問題ありませんが、ファイルの保存個所を変更しておくと、結果の整理が簡単になるかもしれません
次に、同画面の上部にある「Batch Parameters」タブをクリックします。ここで、バッチ処理で実施する各設定を設定することができます。ここでは、上で書いたように、Random SeedとSearchCostをパラメータとして設定します。
設定を変更後、文字が赤くなるのですが、これは、設定が保存されていないためです。画面右下の、「Generate」ボタンを押下することで、設定が保存され、赤い字も消えます。
ここまでで、設定は終了です。この設定を保存するには、画面の上部のフロッピーディスクマーク(Save Batch Configuration)を押下します。ファイルの保存場所は、batchフォルダ内でよいでしょう。では、設定画面の上部の「Console」タブを押下し、その後、画面上部の実行ボタン(緑色の三角形)を押します。
無事にシミュレーションが終了すると、指定したフォルダ中に、output.(日時).batch_param_map.csvと、output.(日時).csvファイルが保存されているのが確認できます。
output(日時)batch_param_map.csvファイルは、シミュレーションの実行がどのように行われたかの説明となります。今回の実行では以下のような内容になるのですが、これは、最初の実行では、random seedが1、searchCostが1、2番目は、random seedが1、searchCostが3、というように実行したとなっています。
もう一つのoutput(日時).csvファイルは、各run毎に51ずつデータが記録されているのですが(各実行で51 Tick実行することとしたため)、これらのレコードがどの設定によるものなのかは、batch_param_mapファイルを使用して判定することになります。
探索コストの違いによって、消費者がどの程度商品を購入できるかについてのシミュレーション結果を以下に示します。探索コストの閾値が大きいほど、商品の購入に至る消費者が増えていることが分かります。実際問題として、消費者の探索コスト自体を変化させることは難しいかもしれません。しかし、探索にかかるコストを下げてあげれば、相対的に探索コストの閾値を上げることになると考えられます。消費者の探索コストを下げてあげる取り組みとしては、例えば、企業が広告を増やすなどが挙げられます。つまり、この結果は、広告を増やして、消費者の探索コストを下げてあげることが、購買できる消費者を増やす、つまり、販売量の増加につながる可能性があることを示唆していると言えます。
(2012年4月19日作成、2022年12月28日改版、2024年3月21日改版)