オペレーションズ・リサーチ

文化祭屋台の釣銭問題をモンテカルロシミュレーション

オペレーションズ・リサーチ

[PR]当サイトはアフィリエイト広告を利用しています。

当サイトのコンテンツ・情報につきまして、可能な限り正確な情報を掲載するよう努めておりますが、誤情報が入り込んだり、情報が古くなっていることもございます。

当サイトに掲載された内容によって生じた損害等の一切の責任を負いかねますのでご了承ください。

参考にしたのは以下。

松井泰子 根本俊男 宇野毅明(2008)『入門オペレーションズ・リサーチ』東海大学出版部.

小田倉 友紀“レジに準備すべき釣り銭の提案”文教大学 卒業論文.

イベントで屋台を出す場合、釣銭をいくら準備するべきかというのはなかなか面倒な問題です。

この問題に対する計算方法としてモンテカルロシミュレーションがあります。

今回は屋台で出す品物は一つの、比較的簡単な部類の問題を扱います。

※今回の例題はリアルさを意識した結果、少し難しい内容となっております。

釣銭問題の入門記事を書きましたのでよろしければそちらもご覧ください。

問題は以下。

文化祭の屋台で焼きそばを売ることにした。

焼きそばは一食300円である。

昨年一日に売れたのは200食であった。

釣銭はどのくらい用意すればよいだろうか。

条件:

支払いは10000円札、5000円札、1000円札、500円玉、100円玉に限定する。

釣銭として用意する紙幣、貨幣は5000円札、1000円札、500円玉、100円玉とする。

お客を待たせる行列を避けたい、作っているうちに冷めるなどの理由で一度に注文できるのは最大5食とする。

モンテカルロシミュレーションで重要なのは、お客がどの現金を支払うかという点です。

10000円を200人が支払うということはないでしょう。

しかし一度に一人のお客が5食、1500円の支払いとなると10000円を支払う可能性はあります。

考えなければいけないのは、一人のお客が何食頼むのかという確率分布と、それぞれの販売個数におけるお客が支払う現金の確率分布です。

以下で場合分けして確率を予想してみます。ここの匙加減で結果はかなりぶれますが、とりあえず決めてみないと始まりません。

本当は販売一回ごとに紙幣と硬貨のやり取りをすべて記録して5年分くらいのデータから求めたいところです。ですが実際は過去のデータを入手できる可能性は低く、なんとなく決めてみるしかないと思います。

①お客が1食(支払額300円)注文するとき

1万円出す確率:0.07

5千円出す確率:0.03

千円出す確率:0.45

500円硬貨を出す確率:0.15

百円硬貨を3枚出す確率:0.3

②お客が2食(支払額600円)注文するとき

1万円出す確率:0.07

5千円出す確率:0.03

千円出す確率:0.60

500円硬貨と百円硬貨を出す確率:0.15

百円硬貨を6枚出す確率:0.15

③お客が3食(支払額900円)注文するとき

1万円出す確率:0.15

5千円出す確率:0.1

千円出す確率:0.6

500円硬貨と百円硬貨を出す確率:0.1

百円硬貨を9枚出す確率:0.05

④お客が4食(支払額1200円)注文するとき

1万円出す確率:0.15

1万円と200円出す確率:0.15

5千円出す確率:0.1

5千円と200円出す確率:0.05

千円と200円出す確率:0.3

千円と500円硬貨を出す確率:0.1

500円硬貨2枚と百円硬貨を出す確率:0.1

500円硬貨1枚と百円硬貨7枚出す確率:0.03

百円硬貨を12枚出す確率:0.02

⑤お客が5食(支払額1500円)注文するとき

1万円出す確率:0.1

1万円と500円硬貨を出す確率:0.05

1万円と100円硬貨を出す確率:0.05

5千円出す確率:0.1

5千円と500円硬貨を出す確率:0.05

5千円と100円硬貨を出す確率:0.05

千円と500円硬貨を出す確率:0.3

千円と100円硬貨を出す確率:0.2

500円硬貨3枚出す確率:0.05

500円硬貨2枚と百円硬貨5枚出す確率:0.04

百円硬貨を15枚出す確率:0.01

そして一度の販売個数の確率分布は以下とする。

一食の確率:0.25

二食の確率:0.4

三食の確率:0.2

四食の確率:0.1

五食の確率:0.05

アルゴリズム:

①0から1までの乱数を発生させ、販売個数の確率分布と照合。例えば乱数が0.68だった場合、累積確率0.65から0.85の三食に分類される。

②0から1までの乱数を発生させ、支払いパターンと照合。例えば乱数が0.87だった場合、三食の累積確率0.85から0.95の千円札一枚を出す場合に分類される。

③支払いパターンで各紙幣・硬貨の量を増減。例えば上の場合は千円札をプラス1、百円硬貨をマイナス1となる。

④:①から③までを販売した個数の合計が200以上になるまで繰り返す。

最終的に各紙幣・硬貨の履歴から最も負の値の絶対値が大きい値がその日最も不足した枚数なのでそれを記録しておく。

①から④を1000回程度繰り返せば、どの程度釣銭用の現金を用意すればよいか予想できるはずである。

アルゴリズムに基づいたプログラム(Python):

import random
import copy

class Turisen:
    def __init__(self): # 初期化: インスタンス作成時に自動的に呼ばれる
        self.cash = [0,0,0,0,0]
        self.cashMin = [0,0,0,0,0]
    oneSell = [0.07,0.1,0.55,0.7,1.0]
    oneSellExchange = [[1,-1,-4,-1,-2],[0,1,-4,-1,-2],[0,0,1,-1,-2],[0,0,0,1,-2],[0,0,0,0,3]]
    twoSell = [0.07,0.1,0.7,0.85,1.0]
    twoSellExchange = [[1,-1,-4,0,-4],[0,1,-4,0,-4],[0,0,1,0,-4],[0,0,0,1,1],[0,0,0,0,6]]
    threeSell = [0.15,0.25,0.85,0.95,1.0]
    threeSellExchange = [[1,-1,-4,0,-1],[0,1,-4,0,-1],[0,0,1,0,-1],[0,0,0,1,4],[0,0,0,0,9]]
    fourSell = [0.15,0.3,0.4,0.45,0.75,0.85,0.95,0.98,1.0]
    fourSellExchange = [[1,-1,-3,-1,-3],[1,-1,-4,0,2],[0,1,-3,-1,-3],[0,1,-4,0,2],[0,0,1,0,2],[0,0,1,1,-3],[0,0,0,2,2],[0,0,0,1,7],[0,0,0,0,12]]
    fiveSell = [0.1,0.15,0.2,0.3,0.35,0.4,0.7,0.9,0.95,0.99,1.0]
    fiveSellExchange = [[1,-1,-3,-1,0],[1,-1,-4,1,0],[1,-1,-4,0,5],[0,1,-3,-1,0],[0,1,-4,1,0],[0,1,-4,0,5],[0,0,1,1,0],[0,0,1,0,5],[0,0,0,3,0],[0,0,0,2,5],[0,0,0,0,15]]
    SellNumber = [0.25,0.65,0.85,0.95,1.0]
    def classify(self):
        p1 = random.random()
        p2 = random.random()
        cashNum = 0
        exNum = 0
        exList = []
        for i in range(len(self.SellNumber)):
            if(p1 < self.SellNumber[i]):
                cashNum = i
                break
        if(cashNum == 0):
            for i in range(len(self.oneSell)):
                if(p2 < self.oneSell[i]):
                    exNum = i
                    exList = self.oneSellExchange[i]
                    break
        if(cashNum == 1):
            for i in range(len(self.twoSell)):
                if(p2 < self.twoSell[i]):
                    exNum = i
                    exList = self.twoSellExchange[i]
                    break
        if(cashNum == 2):
            for i in range(len(self.threeSell)):
                if(p2 < self.threeSell[i]):
                    exNum = i
                    exList = self.threeSellExchange[i]
                    break
        if(cashNum == 3):
            for i in range(len(self.fourSell)):
                if(p2 < self.fourSell[i]):
                    exNum = i
                    exList = self.fourSellExchange[i]
                    break
        if(cashNum == 4):
            for i in range(len(self.fiveSell)):
                if(p2 < self.fiveSell[i]):
                    exNum = i
                    exList = self.fiveSellExchange[i]
                    break
        return exList, cashNum, exNum
    def sumList(self,x):
        for i in range(5):
            self.cash[i] += x[i]
            if(self.cash[i] < self.cashMin[i]):
                self.cashMin[i] = self.cash[i]
minCashList = []
instList = []
for i in range(1000):
    allSell = 0
    instList.append(Turisen())
    for j in range(200):
        ex, cash, exNum = instList[i].classify()
        instList[i].sumList(ex)
        allSell += (cash + 1)
        if(allSell > 200):
            minCashList.append(copy.deepcopy(instList[i].cashMin))
            break
moneyList = [[],[],[],[],[]]
for i in range(1000):
    for j in range(5):
        moneyList[j].append(minCashList[i][j])
for i in range(5):
    print(i,"番目の現金の最小値")
    print(min(moneyList[i]))
freqTable1 = [0,0,0,0,0,0,0,0,0]
freqTable2 = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
freqTable3 = [0,0,0,0,0,0,0,0,0]
freqTable4 = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
for i in range(1000):
    for j in range(len(freqTable1)):
        if(moneyList[1][i] > -(j + 1) * 3):
            freqTable1[j] += 1
            break
    for j in range(len(freqTable2)):
        if(moneyList[2][i] > -(j + 1) * 5):
            freqTable2[j] += 1
            break
    for j in range(len(freqTable3)):
        if(moneyList[3][i] > -(j + 1) * 3):
            freqTable3[j] += 1
            break
    for j in range(len(freqTable4)):
        if(moneyList[4][i] > -(j + 1) * 10):
            freqTable4[j] += 1
            break
print("1番目の度数分布")
print(freqTable1)
print("2番目の度数分布")
print(freqTable2)
print("3番目の度数分布")
print(freqTable3)
print("4番目の度数分布")
print(freqTable4)

出力:

0 番目の現金の最小値

01 番目の現金の最小値

-16

2 番目の現金の最小値

-72

3 番目の現金の最小値

-17

4 番目の現金の最小値

-165

1番目の度数分布

[173, 327, 312, 146, 34, 8, 0, 0, 0]

2番目の度数分布

[115, 126, 143, 125, 150, 111, 79, 53, 50, 23, 14, 3, 5, 1, 2, 0, 0, 0, 0, 0]

3番目の度数分布

[433, 296, 174, 63, 22, 12, 0, 0, 0]

4番目の度数分布

[26, 25, 53, 80, 111, 156, 139, 119, 105, 73, 60, 25, 10, 11, 3, 2, 2, 0, 0, 0]

これを基に度数分布表を作成します。

5000円札:

階級度数相対度数累積相対度数
0~31730.1730.173
63270.3270.5
93120.3120.812
121460.1460.958
15340.0340.992
1880.0081
21001

1000円札:

階級度数相対度数累積相対度数
0~51150.1150.115
101260.1260.241
151430.1430.384
201250.1250.509
251500.150.659
301110.1110.77
35790.0790.849
40530.0530.902
45500.050.952
50230.0230.975
55140.0140.989
6030.0030.992
6550.0050.997
7010.0010.998
7520.0021
80001

500円硬貨:

階級度数相対度数累積相対度数
0~34330.4330.433
62960.2960.729
91740.1740.903
12630.0630.966
15220.0220.988
18120.0121
21001

100円硬貨:

階級度数相対度数累積相対度数
0~10260.0260.026
20250.0250.051
30530.0530.104
40800.080.184
501110.1110.295
601560.1560.451
701390.1390.59
801190.1190.709
901050.1050.814
100730.0730.887
110600.060.947
120250.0250.972
130100.010.982
140110.0110.993
15030.0030.996
16020.0020.998
17020.0021
180001

度数分布表から99%の区間を求めます。階級の中央値でもいいのですが、心配なので階級の最大値を採用します。

5000円札:15枚

1000円札:60枚

500円硬貨:18枚

100円硬貨:140枚

紙幣で払う確率を高めに設定しているので、お釣り用の100円玉がかなり必要となっています。

300円を200食となると、たくさんの小銭が動いて、最初にかなりの枚数を用意しなければいけないようです。

しかもこれは品物が1品の場合です。

想定する支払い確率でもかなり変動しますが、まあまあの目安になっているのではないでしょうか。