見出し画像

【Python】陰陽座『廿弐匹目は毒蝮』をシミュレート

概要

陰陽座の楽曲『廿弐匹目は毒蝮』の状況を、Pythonでシミュレートしてみました。

前回の投稿では、「ほんのわずかな確率でも、繰り返していれば起こる」ことをシミュレーションしました。理論上の数値と実際の結果を比較してみるのが楽しかったです。

他のネタでもやってみようと思い、この曲のことを思い出しました。


シチュエーション

「この茂みから100回に1回、もしくは1%の確率で毒蛇が出る」と言われたとします。人によりますが、これは概ね「ではこの1回は大丈夫だろう」と思える確率だと言えるでしょう。しかし、確率1%が持つ本当の意味は“100回やれば必ず1回出る”ではありませんし、逆に“100回やるまでは出ない”という保証もまったくありません。そういうときに限って、中途半端な“22回目”くらいのところで出たりもする…

こんなことを音楽で表現してしまう、瞬火兄上の発想力には敬服します。


再現するにあたって

Pythonでシミュレーションするにあたって、以下のように解釈しています。

  • 100本の枝があり、そのうち1本が毒蝮

  • ランダムに1本引いて、毒蝮でない場合は、残りの枝の中から再度引く

  • 毒蝮に当たるまで引き続ける(99本目まで引けたら完走=ほんほんえーい)


発生確率

1本目で毒蝮に当たる確率は、瞬火兄上の記載どおり 1/100=1% です。
2本目を引く時に毒蝮に当たる確率は、全体が99本になっているので、 1/99=1.0101…% にです。
22本目を引く時に毒蝮に当たる確率は、全体が79本になっているので、 1/79=1.2658…% です。
(n本目を引く時に毒蝮に当たる確率は、 1/(101-n) と表現できそうです)

22本目を引く時に毒蝮に当たる確率は1.3%にも満たない、と考えると低い確率のように見えますが、これは「21本目まで毒蝮を当たっていない」ことが前提にあります。
100本の中から毒蝮に当たるまで引き続けたときに、22本目で当たる確率は、
「21本目まで毒蝮に当たっていない確率」×「79本の中から毒蝮に当たる確率」になります。

「21本目まで毒蝮を当たっていない確率」をさらに分解すると、
「100本の中から毒蝮に当たらない確率」×「99本の中から毒蝮に当たらない確率」× … × 「80本の中から毒蝮に当たらない確率」です。
(99/100) * (98/99) * … * (79/80) = 79/100

つまり、「21本目まで毒蝮に当たっていない確率」×「79本の中から毒蝮に当たる確率」は、
(79/100) * (1/79) = 1/100
22本目で毒蝮に当たる確率は、1/100=1%であることが確認できました。
結局、22本目で当たる確率も、1本目でいきなり当たる確率も、99本目まで当たりを引かない確率も、等しく1/100です。

また、「22本目までに毒蝮に当たる確率」は、
1 - (79/100) + (1/79) = 22/100
あるいは、1~22本目それぞれで毒蝮に当たる確率が1/100なので、
(1/100) * 22 = 22/100
と計算できます。2割強と、まあまあ起こりえそうな感じになってきます。


コード Ver.1

0から99のリストからrandom.choiceしていき、0=毒蝮が出るまでループさせています。
progress関数は、is_progress変数がTrueのときに、途中の実行結果を表示させるために作成しました。

import random
from statistics import mean

def progress(*args):
    is_progress = True
    if is_progress:
        for i in range(len(args)):
            print(args[i], end=' ')
        print('')

done = 0
results = []

print('~~~ 『廿弐匹目は毒蝮』シミュレータ ~~~')

while True:
    try:
        turn = int(input('サンプル数を入力してください。>>>'))
        break
    except:
        print('半角数字を入力してください。')

progress('--------')

for t in range(turn):
    pick_list =  [num for num in range(0, 100)] 
    for i in range(100):
        pick = random.choice(pick_list)
        if pick == 0:
            progress(i + 1,': 毒蝮!')
            results.append(i + 1)
            break
        else:
            pick_list.remove(pick)
            if i == 98:
                progress('ほんほんえーい!')
                done += 1
                break

result_22 = results.count(22)
result_1_to_22 = sum(i <= 22 for i in results)

print('--------')
print(f'サンプル数 : {turn}')
print(f'失敗時の平均回数 : {mean(results):.2f}')
print(f'ほんほんえーい : {done} -> {(done/turn):.2%}')
print(f'『廿弐匹目は毒蝮』 : {result_22} -> {(result_22/turn):.2%}')
print(f'『廿弐匹目以内で毒蝮』 : {result_1_to_22} -> {(result_1_to_22/turn):.2%}')


実行結果 Ver1

~~~ 『廿弐匹目は毒蝮』シミュレータ ~~~
サンプル数を入力してください。>>>20
-------- 
95 : 毒蝮!
89 : 毒蝮!
45 : 毒蝮!
95 : 毒蝮!
3 : 毒蝮!
3 : 毒蝮!
48 : 毒蝮!
68 : 毒蝮!
61 : 毒蝮! 
79 : 毒蝮!
22 : 毒蝮!
12 : 毒蝮!
45 : 毒蝮!
66 : 毒蝮!
16 : 毒蝮!
24 : 毒蝮!
27 : 毒蝮!
74 : 毒蝮! 
79 : 毒蝮!
84 : 毒蝮!
--------
サンプル数 : 20
失敗時の平均回数 : 51.75
ほんほんえーい : 0 -> 0.00%
『廿弐匹目は毒蝮』 : 1 -> 5.00%
『廿弐匹目以内で毒蝮』 : 5 -> 25.00%


コード Ver.2

リストをシャッフル(全体をsample)して、0=毒蝮の位置を調べるバージョンです。
シャッフルしたリストの2番目(内部的には1)以降から順に引いていくイメージなので、シャッフル後の先頭が「0」だったら、完走=ほんほんえーい になります。
ループが1か所減ってるので、choiseより処理は軽いはず。

import random
from statistics import mean

def progress(*args):
    is_progress = False
    if is_progress:
        for i in range(len(args)):
            print(args[i], end=' ')
        print('')

done = 0
results = []

print('~~~ 『廿弐匹目は毒蝮』シミュレータ ~~~')

while True:
    try:
        turn = int(input('サンプル数を入力してください。>>>'))
        break
    except:
        print('半角数字を入力してください。')

progress('--------')

pick_list =  [num for num in range(0, 100)] 
for t in range(turn):
    shuffled_list = random.sample(pick_list, len(pick_list))
    pick = shuffled_list.index(0)
    if pick == 0:
        progress('ほんほんえーい!')
        done += 1
    else:
        progress(pick, ': 毒蝮!')
        results.append(pick)

result_22 = results.count(22)
result_1_to_22 = sum(i <= 22 for i in results)

print('--------')
print(f'サンプル数 : {turn}')
print(f'失敗時の平均回数 : {mean(results):.2f}')
print(f'ほんほんえーい : {done} -> {(done/turn):.2%}')
print(f'『廿弐匹目は毒蝮』 : {result_22} -> {(result_22/turn):.2%}')
print(f'『廿弐匹目以内で毒蝮』 : {result_1_to_22} -> {(result_1_to_22/turn):.2%}')


実行結果 Ver2 (試行回数 100000回)

十数秒で完了しています。is_progress変数はFalseに変更。
かなり理論値に近づいてますね。

~~~ 『廿弐匹目は毒蝮』シミュレータ ~~~
サンプル数を入力してください。>>>100000
--------
サンプル数 : 100000
失敗時の平均回数 : 49.97
ほんほんえーい : 985 -> 0.98%
『廿弐匹目は毒蝮』 : 1012 -> 1.01%
『廿弐匹目以内で毒蝮』 : 21978 -> 21.98%


所感

  • Pythonの勉強以上に、確率の再勉強になりました。

  • choiceを使って1本ずつ引いていく直感的なVer.1、「0」の位置を検索するよう発想を切り替えて高速化を図ったVer.2を作成しました。目的に対してどんな手段を使えばよいか、過去事例を調べたり、確率を計算して「つまり何をしてるか」を考えたりすることが重要ですね。

  • 途中結果を表示させる関数を作成してみました。デバッグ系のモジュールとして同じものはありそうですが、関数定義の勉強のため。

  • 処理時間の確認をまた後回しにしちゃいました。timeitを使うようです。次回の課題。

  • 『迦陵頻伽』は名盤。



#最近の学び #Python #ランダム #確率 #偶然 #陰陽座

いいなと思ったら応援しよう!

あんぱんだ | 視える化推進エンジニア
いつも図書館で本を借りているので、たまには本屋で新刊を買ってインプット・アウトプットします。

この記事が参加している募集