LoginSignup
0
0

More than 1 year has passed since last update.

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

Posted at

概要

陰陽座の楽曲『廿弐匹目は毒蝮』の状況を、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を使うようです。次回の課題。
  • 『迦陵頻伽』は名盤。
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0