Python手遊び(ソシャゲのガチャ計算:検証編)


この記事、何?

少し前に書いたちょっと珍しいことを検証してみた。

少しだけ確率について考えてみた記録。


どういう人向け?

場合分け数を少しまじめに検討してみたい人向け。

まあ・・・ついでにデレステ好きな人向け♪


ざっくり何を考えたか何をやったか

少し前にガチャのシミュレーションをやってみた。

で、たまたまかもしれない、ちょっと珍しいことが起こった。

なので、「極めて珍しいこと」が起こったのか、「そんなに珍しくないこと」なのか判断したい・・・

というか、判断するという考察ができるかを確認してみた。


具体的な方針

まずはこれ参照。

■C#手遊び (ソシャゲのガチャ計算:ほんのちょっとLinq)

https://qiita.com/siinai/items/6cabf7c18e96ca019d96

一言でいうと、44人にRを10枚ずつそろえるため、何枚でどうなるかという話。

ここで起こったこれがどうか評価しようと思った。

直接「☆5が一人残り、それ以外は☆10になること」だと絞り込みすぎなので、条件は「☆5以下が残ること」で。

C:\projects\qiita\draft\21>calc 1000

繰り返し数 = 1000
42人目: ☆5
残り1人

で、最初何も考えずに、「ん~T検定?」とか思ってみたけど、数表に頼らなくても直接計算できる気がした。

まずは、ガシャ数に対して揃うか揃わないか。

だから、結局はこれ。すでにコードできてるのでそこから流用。

やったこととしては、1/103のガチャを回すと外れる確率が(102/103)。

n回の試行で0枚手に入る(=全部外れる)確率は(102/103)^n

1回当たる確率は(102/103)^(n-1) * (1/103)^1 * 1000

2回当たる確率は(102/103)^(n-2) * (1/103)^2 * (1000*999/2)

3回当たる確率は(102/103)^(n-3) * (1/103)^3 * (1000*999*998/2/3)

...

ちなみに、後ろの(1000*999*998/2/3)とかは、1000C3(※うまく書けない。。。)=(1000!/3!)/3!=(1000*999*998)/(3*2*1)のこと。

組み合わせ分OKという話。

なので、コードがこう。

#n回の試行でm何枚手に入る確率 ※6C2=(6!/2!)/2!とかそんな感じで

plist = []

#0枚
p = (102/103)**gasha
plist.append(p)
#1枚
p = (102/103)**(gasha-1)*(1/103)**1*gasha
plist.append(p)
#2枚
p = (102/103)**(gasha-2)*(1/103)**2*(gasha*(gasha-1)/2)
plist.append(p)
#3枚
p = (102/103)**(gasha-3)*(1/103)**3*(gasha*(gasha-1)*(gasha-2)/2/3)
plist.append(p)
#4枚
p = (102/103)**(gasha-4)*(1/103)**4*(gasha*(gasha-1)*(gasha-2)*(gasha-3)/2/3/4)
plist.append(p)
...

で、例えば☆9が☆10になるかどうかという話は、その子が0枚しか取れないかどうか。

☆8が☆10になれるかどうかというのは、その子が0枚もしくは1枚しか取れないかどうか。

上記はm枚取れる場合の確率なのでm枚以下にするためにそれまでを累積して加算。

で、例えば、☆2が☆10になれるかを判断するときに7枚以下しか取れない確率を計算する、と。

で、全員分のそれぞれの確率を計算して掛け合わせると出来上がり。

出来上がりがこちら。


できあがり

import sys

args = sys.argv

#ガシャ数の確認
gasha = int(args[1])
print(gasha)

#現在の数: 9人 -> 2人 (8種類で合計44人)
current = [9, 9, 11, 7, 3, 2, 1, 2]
for i in current:
print(i)

#n回の試行でm何枚手に入る確率 ※6C2=(6!/2!)/2!とかそんな感じで
plist = []

#0枚
p = (102/103)**gasha
plist.append(p)
#1枚
p = (102/103)**(gasha-1)*(1/103)**1*gasha
plist.append(p)
#2枚
p = (102/103)**(gasha-2)*(1/103)**2*(gasha*(gasha-1)/2)
plist.append(p)
#3枚
p = (102/103)**(gasha-3)*(1/103)**3*(gasha*(gasha-1)*(gasha-2)/2/3)
plist.append(p)
#4枚
p = (102/103)**(gasha-4)*(1/103)**4*(gasha*(gasha-1)*(gasha-2)*(gasha-3)/2/3/4)
plist.append(p)
#5枚
p = (102/103)**(gasha-5)*(1/103)**5*(gasha*(gasha-1)*(gasha-2)*(gasha-3)*(gasha-4)/2/3/4/5)
plist.append(p)
#6枚
p = (102/103)**(gasha-6)*(1/103)**6*(gasha*(gasha-1)*(gasha-2)*(gasha-3)*(gasha-4)*(gasha-5)/2/3/4/5/6)
plist.append(p)
#7枚
p = (102/103)**(gasha-7)*(1/103)**7*(gasha*(gasha-1)*(gasha-2)*(gasha-3)*(gasha-4)*(gasha-5)*(gasha-6)/2/3/4/5/6/7)
plist.append(p)

print('----------------------------')
print('n回の試行でm何枚手に入る確率')
for i in range(8):
print('m=' + str(i) + ': ' + str(plist[i]))

#n回の試行でm枚未満しか手に入らない確率 ※それまでの合算
qlist = []
qlist.append(plist[0])
for i in range(7):
qlist.append(qlist[i] + plist[i + 1])

print('----------------------------')
print('n回の試行でm枚未満しか手に入らない確率')
for i in range(8):
print('m=' + str(i) + ': ' + str(qlist[i]))

#現在の保有枚数ごとに確率(qlistの補数?)をかける
result = 1
for i in range(8):
result *= (1-qlist[i])**current[i]

print('----------------------------')
print('n回の試行で揃う確率')
print(result)

#☆5以下が残る確率
result = 1
for i in range(4):
result *= (1-qlist[i])**current[4 + i]

print('----------------------------')
print('☆5以下が残る確率')
print(1 - result)

#☆6以下が残る確率
result = 1
for i in range(5):
result *= (1-qlist[i])**current[3 + i]

print('----------------------------')
print('☆6以下が残る確率')
print(1 - result)

#☆7以下が残る確率
result = 1
for i in range(6):
result *= (1-qlist[i])**current[2 + i]

print('----------------------------')
print('☆7以下が残る確率')
print(1 - result)

#☆8以下が残る確率
result = 1
for i in range(7):
result *= (1-qlist[i])**current[1 + i]

print('----------------------------')
print('☆8以下が残る確率')
print(1 - result)

#☆9以下が残る確率
result = 1
for i in range(8):
result *= (1-qlist[i])**current[i]

print('----------------------------')
print('☆9以下が残る確率')
print(1 - result)

PSを詰める過程は面白かったけど、コーディングは一瞬だった。

for文書いたりして効率化してもよかったけど、可読性を優先して・・・という言い訳の下、このままで。。。


結果

(my36) C:\projects\qiita\draft\24>python 24-02.py 1000

1000
9
9
11
7
3
2
1
2
----------------------------
n回の試行でm何枚手に入る確率
m=0: 5.793580646953595e-05
m=1: 0.0005679981026425092
m=2: 0.0027815201202934644
m=3: 0.009071755163571494
m=4: 0.02216798994627642
m=5: 0.043292780365669234
m=6: 0.07038613801281189
m=7: 0.0979885450766597
----------------------------
n回の試行でm枚未満しか手に入らない確率
m=0: 5.793580646953595e-05
m=1: 0.0006259339091120452
m=2: 0.0034074540294055096
m=3: 0.012479209192977004
m=4: 0.034647199139253425
m=5: 0.07793997950492265
m=6: 0.14832611751773456
m=7: 0.24631466259439427
----------------------------
n回の試行で揃う確率
0.3243948593012049
----------------------------
☆5以下が残る確率
0.029510603015782788
----------------------------
☆6以下が残る確率
0.08806952117197708
----------------------------
☆7以下が残る確率
0.21173981751513105
----------------------------
☆8以下が残る確率
0.41826857077521884
----------------------------
☆9以下が残る確率
0.6756051406987951

本題の検定結果はこれ。

☆5以下が残る確率

0.029510603015782788

つまり結論として、「この事象は約3%の確率で起こると考えられる。」

1%検定であれば、「有為とはいえない」、5%検定であれば、「有為とはいえる」ということですね。


感想

まあ、満足。

社会人になってから、数字と向き合うことがないか、あっても「感覚的に」しか向き合ってなかったので、ちゃんと考えるのがいい運動になった。これからそっちも深めてゆこう。

・・・といいつつ、実際とちょっと違う点を見つけた。

それぞれの確率を独立させているけど、実際にはA子ちゃんを引いたらB子ちゃんは引けないわけだからそこ、干渉する。

前回のはともかく、今回のロジックはそこを妥協しているので、極端な話、8回で44人揃う可能性が発生する(=確率が0を超える)。

まあ・・・そこ、ちゃんとするならもう一段難易度が上がりますね。

場合分け数、もう少しちゃんと考察できるようになって改めて検討してみようかなぁ?

まあ、段階的にレベルアップしてゆく、ということで。

あっ、10000回とか実践して結果を評価してみようかな。

・・・というか、してみた。


というわけで検証


方針

先日のC#のロジックをいじって、こんなcsv(tsv)を出力し、postgresqlに食わせてsqlを書いた。

0   43  9

1 43 7
1 44 6
2 41 9
2 44 6
4 44 9
6 42 8
7 43 9
7 44 9
...

1列目:何回目の試行かの回数

2列目:何人目の子が残ったか

3列目:その子の最終☆数

なので、例えば1回目の試行(先頭行:1列目が0)では、43番目の子が☆9でただ一人残った。

2回目の試行(1列目が1)は43番目の子が☆7、44番目の子が☆6で計二人残った。

4回目の試行(本来なら1列目が3)は出力がないので全員揃った。

と、こんな感じ。

で、途中で勘違いしたこともあって大数の法則が十分効く件数として、500,000回試してみた。


結果

試行ごとの最小☆数

select m, count(m)

from (
select min(col3) m
from t24
--where col1 < 1000000
group by col1
) x
group by m
order by m
;

m
count

2
65

3
584

4
3175

5
10934

6
29034

7
62172

8
104091

9
129891

計算したのが☆5以下になる確率だったので実験値だと・・・

>>> (65+584+3175+10934)/500000

0.029516

おおっ、ピッタリ・・・と思っていいよね?

実験値:0.029516

算出値:0.029510603015782788

やっぱりこうじゃないと。

計画する、算出する。そして観察する。

同じになるはずの値を二つ照合して矛盾がないことを確認する。

・・・そう、そういうことを機械学習ロジックでやりましょうね・・・はい。