新年早々、新型コロナウィルスの緊急事態宣言が11都府県に再発令されるなど、2021年も前途多難な年になりそうです。
そういうご時世にもかかわらず(だからこそ) 、この連載では今年も「式( ソフトウェア ) 」を「玩( もてあそ ) ぶ」 、浮世離れした話題をまったりと綴っていくつもりなので、興味ある人はお付き合いをよろしくお願いします。
さて今回は、昨今しばしば問題となる、ソシャゲのガチャの確率をソフトウェア的に考えてみました。
ガチャと提供確率
ソシャゲのガチャに大金を投じてしまう「廃課金」者の存在は、しばらく前から社会問題化しています。その結果、ガチャの際にはキャラの提供確率を表示することが義務化され、最近のガチャ画面には「★5キャラの提供割合は○○%」などと明示されるようになりました。
さて、それでは、提供割合が「1%」の場合、何回くらいガチャを回せば目的のキャラが手に入るのでしょうか?
カプセルトイを排出する物理的な「ガチャガチャ」のように、100のクジの中に1つだけ「当たり」があって、「 外れ」クジは引くごとに除外されていくのであれば、1回目に当たる確率は1/100 だけど2回目だと1/99 、3回目だと1/98 …… となって、最悪でも100回引けば当たりは得られるはずです。
それに対し、ソシャゲのガチャのように、何回引いても各回の当選確率が1/100 の場合、必ずしも100回引けば当たりが得られるわけではありません。
高校数学の「確率」で取り上げるように、この手の問題は「外れ」の側から攻めると簡単です。すなわち、当選確率1%のガチャを引く場合、1回目が外れる確率は99/100、2回目に外れる確率も99/100、3回目に外れる確率も99/100……なので、このガチャを100回引いて全て外れる確率は (99/100)^100 = 0.99^100 = 0.366 となり、この値を1から引いた0.634 が、このガチャを100回引いた際に少なくとも1回は当たる確率 となります。逆に言うと、当選確率1%のガチャでは4割弱(36.6%)の人は100回引いても当たらない 、ということです。
一方、期待値的に見ると「1%の当選確率」は「100人引けば1人は当たる 」ことを意味します。多少バラつきがあるので100人では当選者が出なくても、1000人引けば10人前後、10000人引けば100人前後の人は初回で当たりを得られるはずです。
このあたり、確率と期待値的にはそうなると頭では分かっているものの、「 楽に当たる人がいる一方、なぜ自分は当たらないのか」を感覚的に理解するのはなかなか難しいので、だいたい何回くらい引けば当たりうるのかを考えてみたくなりました。こういう際にはコンピュータを使った「シミュレーション」が便利です。
この手の試行錯誤的な作業は、対話的に処理を進められるPythonが得意です。まずは当選確率をrate 、参加者数をplayer 、当たるまで何回かかったかを記録するリストをcounter[] として、当選確率1%のガチャに1000人が挑戦し、当たりを得るまで各自が何回くらい引かないといけないかを計算してみました。
$ python
Python 3.7.7 (default, Nov 20 2020, 21:28:25)
[GCC 10.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import random
>>> rate = 0.01
>>> player = 1000
>>> counter = []
>>> for i in range(player) :
... c = 1
... while random.random() > rate :
... c += 1
... counter.append(c)
...
>>>
random.random() は0から1の間の一様乱数を返す関数で、この結果がrate(=0.01)を下回った場合が「当たり」となります。counter[]には当たりを引くまでにかかった回数が記録されます。まずはcounter[]の中身を見てみましょう。
>>> print(counter)
[1, 27, 202, 64, 160, 327, 7, 17, 38, 2, 33, 7, 135, 327,
19, 18, 39, 80, 1, 36, 37, 138, 83, 20, 106, 196, 22, 5,
....
245, 7, 50, 169, 24, 81, 69, 41, 18, 21, 138, 167]
ざっと見、初回で当たりを引けた幸運な人もいるものの、200回、300回かかった人も散見されます。何人くらいが初回で当たりを引けたかを確認するために、counter[]をソートしてみます。
>>> print(sorted(counter))
[1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3,
4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
...
442, 445, 451, 451, 458, 466, 498, 532, 581, 595, 650, 671, 785]
今回の例では、1000人のうち1度目に引けた幸運な人は4人で、2度目に引けたのは8人、3度目も8人、4度目は9人、という結果になりました。一方、引くまでに100回以上かかった人も362人ほどいました。
>>> [x for x in counter if x > 100 ]
[532, 198, 170, 157, 198, 118, 151, 581, 357, 426, 164, 129,
231, 327, 203, 207, 151, 205, 220, 111, 297, 107, 118, 263,
...
113, 595, 187, 203, 187, 333, 177, 167, 116, 140]
>>> len([x for x in counter if x > 100 ])
362
先に「当選確率1%のガチャでは、4割弱(36.6%)の人は100回引いても当たらない」と述べました。その結論はシミュレーション結果からも裏付けられるようです。
試行回数の度数分布
もう少し細かく見るために、10回までに当たった人が何人、20回までに当たった人が何人……みたいにまとめてみることにします。これくらいになると対話的に実行するよりもスクリプトにして実行する方が楽なので、こんなコードを書いてみました。
1 import random
2
3 rate = 0.01
4 player = 1000
5 counter = []
6
7 for i in range(player) :
8 c = 1
9 while random.random() > rate :
10 c += 1
11 counter.append(c)
12
13 hist = [0,0,0,0,0,0,0,0,0,0,0]
14 for i in counter:
15 gr = int((i-1) / 10)
16 if gr > 10 :
17 hist[10] += 1
18 else:
19 hist[gr] += 1
20
21 print(hist)
13行目のhist[]が、10回以内の当選者数(hist[0]) 、11回から20回以内の当選者数(hist[1])…… を記録するカウンターで、100回以上かかった人はhist[10]にまとめています。なお、15行目で1を引いている(i-1)のは、10回目、20回目……等、キリ番で当たった人を前のグループに収めたいためです。このスクリプトを走らせるとこんな結果となりました。
$ python code_01.py
[88, 97, 90, 61, 73, 50, 49, 58, 59, 31, 344]
この例では、10回以内に当たった幸運な人が88人、20回までには97人、30回までには90人……ということになりました。
運のいい人を数えあげるために、当選者数を積みあげて累積を表示するような処理を追加しました。27行目の"{:3}"は数字を3ケタで表示せよ、という指示です。
22
23 sum =0
24 label = 1
25 for i in hist :
26 sum += i
27 print("{:3}-{:3} : {:3} {:4}".format(label, label+9, i, sum))
28 label += 10
このスクリプトを走らせると、下のような結果となります。左端が当選するまでの回数、中央がそのグループに該当する人数、右端が累積人数です。なお、このスクリプトは乱数を使っているため、実行するたびに結果の細部は変わり、先の例と同じ数字にはなりません。
$ python code_01.py
[86, 97, 81, 53, 59, 43, 55, 49, 46, 33, 398]
1- 10 : 86 86
11- 20 : 97 183
21- 30 : 81 264
31- 40 : 53 317
41- 50 : 59 376
51- 60 : 43 419
61- 70 : 55 474
71- 80 : 49 523
81- 90 : 46 569
91-100 : 33 602
101-110 : 398 1000
この結果では500人目は"71 - 80"の中に入るようなので、半分くらいの人は70回前後 で当たりを引けるようです。
もう少し詳しく見るために、参加者を「運」がいい順に並べ、250人目、500人目、750人目が何回くらいで当選しているかを表示するために、こんな処理を追加してみました。
29 c = 0
30 for i in sorted(counter) :
31 if c == int(player/4) or c == int(player/4)*2 or c == int(player/4)*3 :
32 print("order {} : {}".format(c,i))
33 c += 1
34
35 print("last:{}".format(i))
実行するとこんな結果になります。
$ python code01.py
[106, 85, 80, 72, 68, 56, 48, 50, 39, 35, 361]
1- 10 : 106 106
...
101-110 : 361 1000
order 250 : 28
order 500 : 68
order 750 : 140
last:576
この試行では、運のいい1/4の人は28回以内、全体の半数は68回以内、やや運が悪めの1/4の人は140回以内に当選するものの、残り1/4の人は140回以上かかり、最悪は576回、という結果になりました。
先に述べたように、この計算は乱数を使っているので試行の度に結果は変わるため、何度か走らせて確認したところ、最初の1/4はおよそ30回、中央値は70回前後、3/4は140回前後になるようです。すなわち提供確率1%のガチャを引く場合、当たるまでの回数は「クジ運がいい」人でおよそ30回 、「 人並」な人は70回 、「 クジ運が悪い」人は140回 くらいが目安になりそうです。
もちろん、この数字はシミュレーションの結果であって「これだけ回せば出る」ようなものではないものの、自分のクジ運から見てこれくらいの回数がかかり、そのための費用はこれくらいになりそうだ、というのを事前に理解しておけば、「 期間限定」とか「特別キャンペーン」とかの煽りを目にしても、ハマりすぎることを多少は抑制できるのではないでしょうか?(笑
このコードを書いてる時、「 あれ、これって昔、どこかでやったような……」と考えてみたら、「 ガチャの抽選」というのは「当たり」か「外れ」しかなく、しかも各回ごとに確率が同じ試行の繰り返しだから、統計学で言う「ベルヌーイ試行 」そのもので、その結果は「二項分布 」になるはずだ、ということに気がつきました。
改めて確認したところ、「 二項分布」は一定の試行回数の中で何回成功したかという分布で、その逆の失敗した回数の分布は「負の二項分布」 、特に今回のように最初に成功するまで何回かかったかという分布は「幾何分布 」と呼ばれる分布になるそうです。
もちろん、統計の授業とかでもサイコロやコイントスを例に、期待値や分散の計算方法を勉強したものの、数式を変形して導いた結果にはいまいち実感を感じられなかったのに対し、このように自分の書いたコードを元に「二項分布」の理論に辿りつくと、「 なるほど」と腑に落ちた気になりました。(笑
次回は、このあたりの結果をグラフ化し、視覚的に分かりやすくしてみる予定です。