穴あき問題とビンゴ確率に関する計算機シミュレーション

最近，YouTubeチャンネル「予備校のノリで学ぶ「大学の数学・物理」」様において
``ビンゴせずに穴は何個まで開けられるか''
という話題で数学的な解析も含めて解説されていました．

元動画は以下のものです．

https://www.youtube.com/watch?v=1nSPJgt68bc

本記事では計算機シミュレーションを通して数学に加えてプログラミングの理解を深めていきます．
目標としては，Pythonを用いて抽選回数とビンゴの確率分布を得ることを目標とします．
それでは実装を行っていきます．
# パッケージインポート
数値計算パッケージとしてnumpy，グラフ描写のためにmatplotlibをインポートします．
```Python
import numpy as np
import matplotlib.pyplot as plt
```

# ビンゴの判定関数
ビンゴカードを$5 \times 5$の配列を使って表現し，穴が開いている部分は$-1$とします．
このように実装することでビンゴの判定が多少簡単になります．
穴が開いている部分を$-1$としたため，縦横斜めいずれかの和が$-5$となればビンゴです．
```Python
def checkBingo(card):
  for i in range(5):
    # 横列のチェック.
    if np.sum(card[i]) == -5:
      return True
    # 縦列のチェック
    if np.sum(card[:,i]) == -5:
      return True
    # 斜めのチェック
  if (card[0][0] + card[1][1] + card[2][2] + card[3][3] + card[4][4]) == -5:
      return True
  if (card[0][4] + card[1][3] + card[2][2] + card[3][1] + card[4][0]) == -5:
      return True
  return False
```
# ビンゴカード生成
ビンゴカードは$0 \sim 74$の数字と仮定しています．動画では$1 \sim 75$としていますが，Pythonの$0$番目スタートの仕様に従っています．

注意点としてはrandint関数のような一様乱数を用いてビンゴカードを生成した場合は同じ値を出力する可能性があり，ビンゴカード$1$枚の中で同じ値は存在しないという性質を満たせない可能性があります．

重複しない乱数を実現するために，$0 \sim 74$の連番整数を生成してそれをシャッフルして取り出すことで実装を行います．

生成後の後処理としてビンゴカードの真ん中に$-1$を代入して穴を開ける判定にしておきます．

```Python
numberOfcards = 10000

card = np.arange(75)
cards = np.zeros((numberOfcards,5,5))

for i in range(numberOfcards):
  np.random.default_rng().shuffle(card)
  cards[i] = card[:25].reshape(5,5)

  cards[i][2][2] = -1
```

# 抽選と結果

抽選番号を生成して番号ごとにビンゴカードに穴を開けていきます．
ビンゴしているカードの枚数と抽選回数を記録していきます．

この結果では抽選回数とビンゴしているカードの枚数の累積分布のようなものが得られます．

```Python
lot = np.arange(75)
np.random.default_rng().shuffle(lot)

result = np.zeros(len(lot))

for i in range(len(lot)):
  for j in range(numberOfcards):
    np.place(cards[j],cards[j] == lot[i],-1)
    if checkBingo(cards[j]) == True:
      result[i] += 1
print(result)
```

# 結果の描写
最後にmatplotlibを使ってデータの描画を行います．
今回は$10000$枚のビンゴカードを使ってシミュレーションを行いました．
データ数が足りないので，増やすと滑らかなグラフになることが期待できます．
```Python
data = np.zeros(len(lot))
for i in range(1,len(data)):
  data[i] = result[i] - result[i-1]
print(data)

plt.bar(np.arange(len(data)), data, width=1.0)
```
![bingo.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/281468/ceb2b9eb-e8b6-7ec3-eb7a-8f40e1536a0d.png)

# まとめ
YouTubeチャンネル「予備校のノリで学ぶ「大学の数学・物理」」様の動画を基に計算機シミュレーションを使って，理解を深めていきました．
数学と密接に関連がある計算機の世界を少しでも知っていただければ幸いです．

# ソースコードまとめ
```python
import numpy as np
import matplotlib.pyplot as plt

def checkBingo(card):
  for i in range(5):
    # 横列のチェック.
    if np.sum(card[i]) == -5:
      return True
    # 縦列のチェック
    if np.sum(card[:,i]) == -5:
      return True
    # 斜めのチェック
  if (card[0][0] + card[1][1] + card[2][2] + card[3][3] + card[4][4]) == -5:
      return True
  if (card[0][4] + card[1][3] + card[2][2] + card[3][1] + card[4][0]) == -5:
      return True
  return False

# ビンゴカードの生成 (10000枚)
numberOfcards = 10000

card = np.arange(75)
cards = np.zeros((numberOfcards,5,5))

for i in range(numberOfcards):
  np.random.default_rng().shuffle(card)
  cards[i] = card[:25].reshape(5,5)

  cards[i][2][2] = -1

lot = np.arange(75)
np.random.default_rng().shuffle(lot)

result = np.zeros(len(lot))

for i in range(len(lot)):
  for j in range(numberOfcards):
    np.place(cards[j],cards[j] == lot[i],-1)
    if checkBingo(cards[j]) == True:
      result[i] += 1
print(result)

data = np.zeros(len(lot))
for i in range(1,len(data)):
  data[i] = result[i] - result[i-1]
print(data)

plt.bar(np.arange(len(data)), data, width=1.0)
```