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

## 概要

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

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

https://qiita.com/AnPanda/items/ae3fbd9cee2248a7abb5

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

https://www.uta-net.com/movie/219717/

### シチュエーション

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

http://mttb.jugem.jp/?eid=1139

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

### 再現するにあたって

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のときに、途中の実行結果を表示させるために作成しました。

```py
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

```py
～～～ 『廿弐匹目は毒蝮』シミュレータ ～～～
サンプル数を入力してください。>>>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より処理は軽いはず。

```py
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に変更。
かなり理論値に近づいてますね。

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

## 所感

* Pythonの勉強以上に、確率の再勉強になりました。
* choiceを使って1本ずつ引いていく直感的なVer.1、「0」の位置を検索するよう発想を切り替えて高速化を図ったVer.2を作成しました。目的に対してどんな手段を使えばよいか、過去事例を調べたり、確率を計算して「つまり何をしてるか」を考えたりすることが重要ですね。
* 途中結果を表示させる関数を作成してみました。デバッグ系のモジュールとして同じものはありそうですが、関数定義の勉強のため。
* 処理時間の確認をまた後回しにしちゃいました。timeitを使うようです。次回の課題。
* 『迦陵頻伽』は名盤。