エージェントシミュレーションやってみた1（コンビニ）

# エージェントシミュレーションとは？
マルチエージェントシステム（Multi-Agent System、MAS）とは、複数のエージェントから構成されるシステムであり、個々のエージェントやモノリシックなシステムでは困難な課題をシステム全体として達成する。
それぞれ異なった判定アルゴリズムなどの特徴（キャラクタリスティック）を持ったエージェントモデルを用い（よってマルチエージェントと呼ばれる）、複数かつある一定以上のエージェントを多数設定し、人工社会を構成しそれぞれ特徴の異なったエージェントの相互作用をシミュレーションするシステムである。（wikipediaより）

すなわち人や車などをエージェントとし、それらをモデリング、観察することです。大学の授業でエージェントシミュレーションをする機会があったので、少し実装してみようと思います。(pythonのTKinterを使用)


## テーマ：コンビニなどの小売店内のシミュレーション
店内にあらかじめ決められた商品を用意する。入店する客は、用意されたいずれかの商品を手に取り、レジへ向かい店を出る。商品を置く位置を変え、エージェント（客）の挙動の変化を観察する。

#### レイアウトの説明
10×10のマスの店内をしました。白のマスが通路、黒のマスが壁、ピンクのマスがレジ、緑のマスが出入り口、文字が書いてあるマスはその商品を置いています。
エージェントを追加すると青、赤、黄緑、紫、水色のいずれかで表され、欲しいものを手に入れるために移動を繰り返します。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2632237/b781fad8-3510-65e6-6596-a8c2d965bbf8.png" width=400>
ボタンがいくつかありますが、観察に必要な機能を実装してみました。

#### 客のモデリングについて
今回の店内の客は、毎ステップごとに確率で上下左右に動くこととしています。また客の購入物は、2019年度のあるコンビニにおいての実際の売り上げをもとに決めます。
![コンビニ　売れるジャンル_trim.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2632237/6b908483-dcf6-61cb-48cc-224c5e0b8bde.png)



#### 実験方法
たばこはレジの近くに置くとして、その他の商品の配置を変えていきます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2632237/f653a4c5-6f20-5189-241a-7bb32a6709c3.png" width=400>

1~4のマスにファーストフード、日配食品、加工食品、非食品を組み合わせ、毎回800人の客に買い物をさせます。

## 結果
![kekka.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2632237/df43c9e9-b7df-2531-8dbd-1d2ecb6e3d20.png)
Excelでまとめてみました。全部で24通り（4!通り）の組み合わせを試しました。
客800人の平均滞在ステップ数が一番多くなったところに星を付けています。

1番のマスに加工食品かファーストフードを置くと、平均滞在ステップ数が多くなるようです。これは前述の2019年度の実際の売り上げからして、加工食品とファーストフードを購入する割合が多いため、これらをコンビニの奥（このシミュレーションでは1番のマス）に置くと滞在時間が長くなることが分かります。

## 最後に
やはりシミュレーションである以上、現実になるべく近づけることが課題になる。そのためには客の心情や行動、経営理論などさまざまな要素を考慮する必要があると感じた。
今回参考（？）にした論文とプログラムを載せておきます。

論文：エージェントシミュレーションによる小売店舗内商品販売促進施策の分析（岸本有之）
<details><summary>プログラム（長くて読みづらいので改善の必要あり）</summary><div>

``` python
from tkinter import *
import random
import time
from dataclasses import dataclass
from pprint import pprint


FIELD_X,FIELD_Y=10,10
CELL_SIZE=50
WIDTH,HEIGHT=FIELD_X*CELL_SIZE,FIELD_Y*CELL_SIZE

tk=Tk()
tk.title("店内のシミュレーション")
canvas=Canvas(tk,width=WIDTH,height=HEIGHT)
canvas.pack()

@dataclass##客をモデリング
class customer:
    id: int
    x: int
    y: int
    status: str ##購入前(normal)→かごに入れた(grab)→購入後(checked)
    color: str
    thing: int ##ほしいもの（番号）
    count: int ##移動の回数

@dataclass
class INF:
    x: int
    y: int
    inf: int ##壁は０、通路は１、レジは２、出入り口は３、ファストフードは４、
             ##日配食品は5、加工食品は6、タバコは7、非食品は8


outline_1=[[0,0,0,0,0,2,0,2,0,0],[0,1,1,1,1,1,1,1,1,3],[0,1,0,1,0,1,0,1,1,0],
   [0,1,0,1,0,1,0,1,1,0],[0,1,0,1,0,1,0,1,1,0],[0,1,0,1,0,1,0,1,1,0],
   [0,1,0,1,0,1,0,1,1,0],[0,1,0,1,0,1,0,1,1,0],[0,1,1,1,1,1,1,1,1,0],[0,0,0,0,0,0,0,0,0,0]]##10×10のマスでシミュ

outline_2=[[0,0,0,0,0,2,0,0,0,0],[0,1,1,1,1,7,1,1,1,3],[0,1,1,1,1,1,1,1,1,0],
   [0,1,0,1,0,1,0,1,1,0],[0,1,0,1,0,1,0,1,1,0],[0,1,0,1,0,1,0,1,1,0],
   [0,8,0,5,0,4,0,6,1,0],[0,1,1,1,1,1,1,1,1,0],[0,1,1,1,1,1,1,1,1,0],[0,0,0,0,0,0,0,0,0,0]]


outline_3=[[0,0,0,0,0,2,0,0,0,0],[0,1,1,1,1,7,1,1,1,3],[0,1,1,1,1,1,1,1,1,0],
   [0,1,0,1,0,1,0,1,1,0],[0,1,0,1,0,1,0,1,1,0],[0,1,0,1,0,1,0,1,1,0],
   [0,4,0,5,0,6,0,8,1,0],[0,1,1,1,1,1,1,1,1,0],[0,1,1,1,1,1,1,1,1,0],[0,0,0,0,0,0,0,0,0,0]]

D=[]#店内の構成の情報

for x in range(10):#10×
    for y in range(10):#10
        i=outline_3[x][y]##店内の変更
        D.append(INF(x,y,i))

def rectangle(inf,color):#□を描く、infは座標
    x=inf.x*CELL_SIZE
    y=inf.y*CELL_SIZE
    canvas.create_rectangle(x,y,x+CELL_SIZE,y+CELL_SIZE,fill=color)

def rectangle_outline(inf):#四角のマスを描く(店内の構造)
    if inf.inf==0:#商品
        color="gray"
    elif inf.inf==2:#レジ
        color="pink"
    elif inf.inf==3:#出入口
        color="green"
    elif inf.inf!=0 and inf.inf!=2 and inf.inf!=3:
        color="white"
    rectangle(inf,color)
    
for x in (D):
    rectangle_outline(x)
    
##ここまでで通路、商品、レジ、出入り口の情報をINFに持たせた。(店内の構成)
###########################################################################




def move(agent,D):#客の情報(agent)、壁のリスト(D)
    while agent.status=="normal":##status==normal
        r=random.randint(1,4)
        
        if r==1:##r=1のとき右に進む
            p=10*(agent.x+1)+agent.y
            if D[p].inf != 0 and D[p].inf != 2:
                agent.x=agent.x+1
                break
        
        if r==2:##r=2のとき下に進む
            p=10*agent.x+agent.y+1
            if D[p].inf != 0 and D[p].inf != 2:
                agent.y=agent.y+1
                break
        
        if r==3:##r=3のとき左に進む
            p=10*(agent.x-1)+agent.y
            if D[p].inf != 0 and D[p].inf != 2:
                agent.x=agent.x-1
                break
        
        if r==4:##r=4のとき上に進む
            p=10*agent.x+agent.y-1
            if D[p].inf != 0 and D[p].inf != 2:
                agent.y=agent.y-1
                break


    if agent.status == "grab":##status==grab
        x=1
        y=5
        p=10*(agent.x-1)+agent.y
        if D[p].inf!=0 and agent.x>1:
            agent.x=agent.x-1
        elif D[p].inf==0 and agent.y-5>0:
            agent.y=agent.y-1
        elif D[p].inf==0 and agent.y-5<0:
            agent.y=agent.y+1

    if agent.status == "checked":
        y=9-agent.y
        if y>0:
            agent.y=agent.y+1
        elif y==0:
            agents.remove(agent)
            COMPLETE.append(agent)
                    
    
    rectangle(agent,agent.color)        


    
##移動する関数を作った↑
##################################


agents=[]#客たち
COMPLETE=[]#買い物終わったagentのリスト
color_customer=["blue2","red","green2","purple1","cyan"]##客の色


def choice_thing(list):#listはcolor_custmer
    r=random.randint(1,100)
    if 1<=r and r<=25:
        return color_customer[0]##ファーストフード
    elif 26<=r and r<=38:
        return color_customer[1]##日配食品
    elif 39<=r and r<=65:
        return color_customer[2]##加工食品
    elif 66<=r and r<=90:
        return color_customer[3]##たばこ
    elif 91<=r and r<=100:
        return color_customer[4]##非食品

def choice_color(color):
    if color=="blue2":
        return 4##ファーストフード-青

    if color=="red":
        return 5#日配食品-赤

    if color=="green2":
        return 6#加工食品-緑

    if color=="purple1":
        return 7#たばこ-紫

    if color=="cyan":
        return 8#非食品-水色

##########################################################################
def thing_char(D):#指定された場所に文字を書く
    if D.inf==4:
        canvas.create_text(D.x*CELL_SIZE+25,D.y*CELL_SIZE+25,text="ファストフード",fill="blue2")#ファスト
    if D.inf==5:
        canvas.create_text(D.x*CELL_SIZE+25,D.y*CELL_SIZE+25,text="日配食品",fill="red")#日配食品
    if D.inf==6:
        canvas.create_text(D.x*CELL_SIZE+25,D.y*CELL_SIZE+25,text="加工食品",fill="green2")#加工食品
    if D.inf==7:
        canvas.create_text(D.x*CELL_SIZE+25,D.y*CELL_SIZE+25,text="たばこ",fill="purple1")#たばこ
    if D.inf==8:
        canvas.create_text(D.x*CELL_SIZE+25,D.y*CELL_SIZE+25,text="非食品",fill="cyan")#非食品
    
def check1(agent,D):#商品を持っているか確認
    a=agent.x*10+agent.y
    if agent.thing == D[a].inf:
        agent.status="grab"

def check2(agent):#レジにいるかどうか
    if agent.status=="grab" and agent.x==1 and agent.y==5:
        agent.status="checked"
        
def check3(agent):#出入り口にいるかどうか
    if agent.status=="checked" and agent.x==1 and agent.y==9:
        agents.remove(agent)
        COMPLETE.append(agent)


def add():#客を増やす
    for x in range(100):#############★客の数★############
        i=len(agents)+1
        x=1
        y=9
        status="normal"
        c=choice_thing(color_customer)
        t=choice_color(c)
        n=0
        agents.append(customer(i,x,y,status,c,t,n))
    
def nothing(agents):
    if len(agents)==0:
        exit()

def count_move(agent):
    agent.count=agent.count+1


def many():
    for agent in agents:
        move(agent,D)
        count_move(agent)
        check1(agent,D)
        check2(agent)
        nothing(agents)

    for agent in agents:
        check3(agent)
        
        
##客を追加
btn=Button(tk,text="客を〇人追加します",command=add)#ボタンを押して客増やす
btn.pack()

##客のリストagents
def print_list():
    pprint(agents)

btn1=Button(tk,text="店内にいる客のリスト",command=print_list)
btn1.pack()

##購入済みのリスト
def print_checked():
    pprint(COMPLETE)

btn2=Button(tk,text="購入できた客のリスト",command=print_checked)
btn2.pack()

#リストの平均を計算
def cul_ave(List):
    SUM=0
    for x in List:
        SUM=SUM+x.count
    if len(List)==0:
        return 0
    return SUM/len(List)



f,n,k,t,h=[],[],[],[],[]##購入者を分類する
SUMMARY=[]


def culcu_ave():
    a=cul_ave(COMPLETE)
    for x in COMPLETE:
        if x.thing == 4:#ファストフード
            f.append(x)
        elif x.thing == 5:#日配食品
            n.append(x)
        elif x.thing == 6:#加工食品
            k.append(x)
        elif x.thing == 7:#たばこ
            t.append(x)
        elif x.thing == 8:#非食品
            h.append(x)
    F=cul_ave(f)
    N=cul_ave(n)
    K=cul_ave(k)
    T=cul_ave(t)
    H=cul_ave(h)

    print(f"<合計>\n人数:{len(COMPLETE)}人、平均:{a:.1f}回")
    print(f"<ファストフード>\n人数:{len(f)}人、平均：{F:.1f}回")
    print(f"<日配食品>\n人数:{len(n)}人、平均：{N:.1f}回")
    print(f"<加工食品>\n人数:{len(k)}人、平均：{K:.1f}回")
    print(f"<たばこ>\n人数:{len(t)}人、平均：{T:.1f}回")
    print(f"<非食品>\n人数:{len(h)}人、平均：{H:.1f}回")
    print("（小数第一位まで）")

btn3=Button(tk,text="買った人数と平均移動回数",command=culcu_ave)
btn3.pack()

def main_loop():
    canvas.delete("all")
    for x in D:
        rectangle_outline(x)
        thing_char(x)
    many()##客の数だけmove()を呼ぶ
    tk.after(50,main_loop)
    

tk.after(1000,main_loop())

#コードが読みづらくてすみません。
```
</div></details>