乙Py先生のプログラミング教室
初学者のためのプログラミング学習サイト

確率

あの伝説の国士無双13面待ち
これが出る確率を求めてみました。

解説動画はこちら


国士無双13面待ちが出る確率は?


今回は麻雀ネタです。

麻雀における最高峰の役「役満」の一つに
「国士無双」という役満があります

13種類の么九牌(一九字牌)を
各1枚以上集める特殊な形の役満です。


13面待ち
萬子・索子・筒子の1・9、東・南・西・北・白・發・中の
計13種類をすべて揃えた状態でアガリ待ちの状態のことです。

13



この13面待ちがどれくらいの確率でやってくるのか
プログラムで求めていきたいと思います。



1.接待麻雀モード

自分一人がずっと国士無双13面待ちを狙い
他はただパイを捨て続けるとした場合の確率です。



簡易に求めるコード
import random

# ヤオ九牌(1, 9, 字牌)のインデックス定義
YAOCHU_INDICES = [0, 8, 9, 17, 18, 26, 27, 28, 29, 30, 31, 32, 33]

def is_kokushi_13men_machi(tehai):
    """
    国士無双13面待ちの判定
    13種類のヤオ九牌がすべて1枚ずつ揃っているか
    """
    for idx in YAOCHU_INDICES:
        if tehai[idx] < 1:
            return False
    return True

def simulate_one_game_kokushi():
    # 山の作成(136枚)
    yama = []
    for i in range(34):
        yama.extend([i] * 4)
    random.shuffle(yama)

    # 全員の手牌(4人分)
    players_tehai = [[0] * 34 for _ in range(4)]
    
    # 配牌(各13枚)
    for p in range(4):
        for _ in range(13):
            pai = yama.pop()
            players_tehai[p][pai] += 1

    turn = 0
    while len(yama) > 14:
        turn += 1
        for p in range(4):
            # --- 1. ツモ ---
            tsumo_pai = yama.pop()
            players_tehai[p][tsumo_pai] += 1

            # --- 2. 判定(プレイヤー0のみ国士無双を判定) ---
            if p == 0 and is_kokushi_13men_machi(players_tehai[p]):
                return True, turn, p

            # --- 3. 捨て牌選択 ---
            if p == 0:
                # プレイヤー0: 国士無双を狙う戦略
                discard_candidates = []
                
                # 優先順位1: 中張牌を捨てる
                for i in range(34):
                    if i not in YAOCHU_INDICES and players_tehai[p][i] > 0:
                        discard_candidates.extend([i] * players_tehai[p][i])
                
                # 優先順位2: 被っているヤオ九牌を捨てる
                if not discard_candidates:
                    for i in YAOCHU_INDICES:
                        if players_tehai[p][i] >= 2:
                            discard_candidates.append(i)

                # 優先順位3: どれでも(13面待ち完成中など)
                if not discard_candidates:
                    discard_candidates = [i for i, count in enumerate(players_tehai[p]) if count > 0]
                
                discard_pai = random.choice(discard_candidates)
            else:
                # 他のプレイヤー: ツモ切り(今引いた牌をそのまま捨てる)
                discard_pai = tsumo_pai

            players_tehai[p][discard_pai] -= 1
            
            # 山がなくなったら終了(流局)
            if len(yama) <= 14:
                break
                
    return False, turn, None

def run_kokushi_experiment(episodes_per_step=10000):
    print(f"国士無双13面待ちシミュレーション ({episodes_per_step}局)\n")
    print("成立回数 | 成立確率 | 平均巡目")
    print("-----------------------------------------")

    success_count = 0
    total_turns = 0
        
    for _ in range(episodes_per_step):
        is_clear, turns, winner = simulate_one_game_kokushi()
        if is_clear:
            success_count += 1
            total_turns += turns
    
    prob = (success_count / episodes_per_step) * 100
    avg_turn = total_turns / success_count if success_count > 0 else 0
    print(f"{success_count:4}回  | {prob:6.2f}% | {avg_turn:5.2f}巡")

run_kokushi_experiment(100000)
国士無双13面待ちシミュレーション (100000局)

成立回数 | 成立確率 | 平均巡目
-----------------------------------------
 120回  |   0.12% | 16.44巡



接待麻雀モードになっていますが
10万局中120回
確率0.12%でした。



2.他家が確率でアガるモード

先ほどのプレイヤーがずっと国士無双を狙う状態で
他家が10巡目以降、確率でアガられてしまう状態の
確率を求めています。


3.13面待ちからアガる確率

他家が確率でアガるモードに加えて
13面待ちから、上がれたかどうかの
確率も求めました。

こちらはぜひ動画で確率をご覧ください。



まとめ

国士無双13面待ちは、それなりにアガるプレイヤーがいても
10000局に1回位は見れると推測されるので
年間300試合、試合平均11局だと、3-4年に1回は
13面待ちの実況が見れるかもしれません。

13面待ちでアガリの確率はさらに低いので(一生に一度出せるかどうか)
もう10年は見れないかもしれないですね。


今回は麻雀ネタでしたが
次回も多分麻雀ネタです。
それでは。



今回は昔懐かしい
カイジの沼のシミュレーションです。

解説動画はこちら




カイジの沼のシミュレーション

沼とは

漫画『賭博破戒録カイジ』に登場する架空のパチンコ台

帝愛グループの運営するカジノに設置されている
パチンコの中でも特別な台で玉が1発あたり4000円
挑戦は最低300万円からという設定

その一方で、今までの挑戦者が消費した玉が
全てプールされる仕組みになっており
運良く大当たりを引ければ総取りで莫大な額の金を手にできるとか


仕掛け・役物

ここからは確率を計算するための
沼のギミックについてです。

1.釘の森

釘が密集した配置になっている「釘の森」
通常時の設定Cは1/100程度

2.スル

開閉する羽根のある電動役物「スルー」
釘の森を突破した玉を更にふるい落とす
確率は不明なので
ここでは通過率を1/5としておきます

3.3段クルーン

沼の本丸である「三段クルーン」
円形の皿に穴が3つ/4つ/5つ空いていて
当たりの穴に入ると次の段に進める

数学的にはクルーンに入ってさえしまえば
約1/60の確立で当たる


というような設定にはなっていますが
漫画の設定上ではこの台にはイカサマがあり
この確率では入らないようにはなっています。

しかし、今回は考慮しないで
イカサマなしだとどうなるかを見ていきたいと思います。



沼のシミュレーションコード


簡易な確率計算で大当たりしたかどうかだけ返す関数を用いて
シミュレーションしていきます。
最大回数は10万回(4億円相当)とします。

import numpy as np
import random
import pandas as pd
import matplotlib.pyplot as plt

# 確率でヒットするかどうかを判定する
def is_hit(n: int) -> bool:
    return random.randint(1, n) == 1

def numa_charenge():
  # 釘の森の通過
  mori = is_hit(100)
  if not mori:
    return False

  # スルーの通過
  throw = is_hit(5)
  if not throw:
    return False

  # クルーン1段目通過
  first = is_hit(3)
  if not first:
    return False

  # クルーン2段目通過
  second = is_hit(4)
  if not second:
    return False

  # クルーン3段目通過
  third = is_hit(5)
  if not third:
    return False
  else:
    return True


# 1000回施行して測ってみる
ball_price = 4000 # パチンコ玉の価格

def numa_result(num):
  all_result = []
  for a in range(1000):
    cumulative = 0
    for i in range(num):
      cumulative += ball_price
      is_atari = numa_charenge()
      if is_atari:
        all_result.append([i+1, cumulative,1])
        break
      if i == num-1:
        all_result.append([i+1, cumulative,0])
  return all_result

num = 100000 # 1回あたりの最大施行回数
all_result = numa_result(num)

df = pd.DataFrame(all_result,columns=["balls","cumulative","hit"])
df.head()
balls cumulative hit
0 30959 123836000 1
1 29604 118416000 1
...

plt.figure(figsize=(12, 6))
df["cumulative"].hist(bins=20)
plt.show()
download

おおよそこのような結果になります。


どれくらい注ぎ込めば当たるのか?


シミュレーション結果は使った額の累計を入れているので
それを用いれば、その金額以下の回数と
全体の回数から大体の確率を求められます。
times_300 = len(df[df["cumulative"]<3000000])
print(f"300万円で大当たりになる可能性 : {times_300/1000*100} % ")

なおこのシミュレーションの結果は
幾何分布に近似するため以下のようなコードで
回数あたりの大当たり確率の理論値が出せます。
# 幾何分布上の理論値
for i in range(10):
  p = 1 / 30000
  n = (i+1) * 10000
  prob = 1 - (1 - p)**n
  print(f"{i+1:02}万回で当たる確率 : {prob:.6f}")
01万回で当たる確率 : 0.283473
02万回で当たる確率 : 0.486589
03万回で当たる確率 : 0.632127
04万回で当たる確率 : 0.736409
05万回で当たる確率 : 0.811130
06万回で当たる確率 : 0.864669
07万回で当たる確率 : 0.903032
08万回で当たる確率 : 0.930520
09万回で当たる確率 : 0.950215
10万回で当たる確率 : 0.964328


シミュレーション結果もこの理論値に
近似してますね。


まとめ


沼の設定が玉が1発あたり4000円
3万発に1回の確率(100x5x3x4x5)
で当たるのだとしたら3億注ぎ込めば
9割くらいは大当たりするのではないか?

あくまでイカサマが無い事が前提ですが.....
イカサマさえなければ無尽蔵にお金を注ぎ込めるなら
勝てる気がしなくもない今日この頃でした。

それでは

今回は直感に反する確率の逆説の問題集です


解説動画はこちら



直感に反する確率の問題

なんとなくコレかなと思ったけど
答えはそれと違っていたというような
直感に反する答えが出そうな問題です。


問題とともに
回答に使用したコードを載せておきます。



1.確率の逆説


第1問

サイコロを2回振るとき 少なくとも1回は1が出る確率は
1. 3分の1より小さい
2. ちょうど3分の1(33.3%)
3. 3分の1より大きい

# サイコロを2回振るとき 少なくとも1回は1が出る確率
import random

num = 1000000
count = 0
for _ in range(num):
    dices = [random.choice([1,2,3,4,5,6]),random.choice([1,2,3,4,5,6])]
    if 1 in dices:
        count+=1

print(f"少なくとも1が1回はでた回数 : {count}")
print(f"{count * 100 / num}%")



第2問

学校のクラス30人の生徒の中に誕生日が同じ人がいる確率は
1. 約30%
2. 約50%
3. 約70%
4. 約90%


# n人の生徒の中に誕生日が同じ人がいる確率
import random
from collections import Counter

birthdays = list(range(365))
num = 30
cnt, all_cnt = 0,0
for i in range(1000000):
    all_cnt+=1
    tmp = random.choices(birthdays,k=num)
    c = Counter(tmp)
    mx = c.most_common(1)
    if mx[0][1]>1:
        cnt+=1
print(f"誕生日が同じ人がいた回数 : {cnt}")
print(f"誕生日が同じ人がいる確率 : {cnt * 100 /all_cnt}%")




2.ギャンブラーの誤謬

過去の結果が現在の結果に影響すると誤って考える心理


第3問

コインを4回投げてすべて表が出た場合、次の投げで裏が出る確率は
1. 50%より小さい
2. ちょうど50%
3. 50%より大きい


# コインを投げて4回表が出た後の出目の確率
import random

def simulate_coin_tosses(trials):
    result = {'表':0,'裏':0}
    coins = [random.choice(['表', '裏']) for _ in range(trials)]
    for i in range(0, trials-5):
        if coins[i:i+4] == ['表', '表', '表', '表']:
            next = coins[i+4]
            result[next]+=1
    return result['表'], result['裏']

# 試行回数
k = 1000000
total_heads, total_tails = simulate_coin_tosses(k)

print(f"試行回数: {k}")
print(f"表の合計回数: {total_heads}")
print(f"裏の合計回数: {total_tails}")
print(f"4回表が出た後の裏が出る確率は : {total_tails * 100 / (total_heads + total_tails):.6}%")




第4問

コインを1000回投げた時に、連続して表が出る最高回数は
1. 8回より少ない
2. 8-10回くらい
3. 10回より多い


# コインをnum回投げて、連続して表が出る最高回数
import random

def max_consecutive_heads(trials):
    coins = [random.choice(['表', '裏']) for _ in range(trials)]
    #print(coins)
    #print(f"表の数 : {coins.count('表')}")
    max_count, current_count = 0, 0
    for coin in coins:
        if coin == '表':
            current_count += 1
            max_count = max(max_count, current_count)
        else:
            current_count = 0
    return max_count

# 試行回数
num = 1000
max_heads = [max_consecutive_heads(num) for _ in range(1000)]
mean_heads = sum(max_heads) / len(max_heads)
print(f"試行回数: {num}")
print(f"連続して出現した表の最高回数: {mean_heads}")



答えは
シミュレーションプログラムを実行するか
動画をご覧ください。




そんなに確率高くないだろうと思っていたら
意外と高い確率だったり
意外と低かったり

そんなこともあるんじゃないかと思います。

プログラムを使えば
色々シミュレーションして
おおよその確率を求めることができるので

さまざまな事象の確率を求めたいなら
プログラミングを習得するのが
おすすめです。


今回はここまでです
それでは。

 

今回は最近流行りのポーカー
に関する確率のシミュレーションです

解説動画はこちら


 

ポーカーについて

ポーカーはトランプ5枚の手札の組み合わせで
役を作るゲームです。

1.ハイカード(強いカードの所持 2 < A)
2.ワンペア(同じ数字の組み合わせが1つ)
3.ツーペア(同じ数字の組み合わせが2つ)
4.スリーカード(同じ数字3つ)
5.ストレート(2,3,4,5,6 などの数字の並び)
6.フラッシュ(同じスートの組み合わせ HDCS)
7.フルハウス(スリーカードに加えて、同じ数字の組み合わせが1つ)
8.フォーカード(同じ数字4つ)
9.ストレートフラッシュ(ストレート + フラッシュ)
10.ロイヤルフラッシュ(AKQJTのフラッシュ)


テキサスホールデムルール


ポーカーのルールの一つで
プレイヤーそれぞれに配られた2枚のカードと
プレイヤー全員が共有する公開された
コミュニティカード"枚の計7枚で役を作り
チップをベットするなどの駆け引きを行うゲームルールです
(ベット周りの詳細なルールは割愛)


ここからはテキサスホールデムの
初期手札による勝率がどうなるのかを
検証するシミュレーションプログラムについてです。




役を計算するプログラム

シミュレーションを行うには
ポーカーの役の判定を行うプログラムが必要ですね

import random
from collections import Counter
from itertools import combinations

# ポーカーの役
POKER_HANDS = {
    0: "ハイカード",
    1: "ワンペア",
    2: "ツーペア",
    3: "スリーカード",
    4: "ストレート",
    5: "フラッシュ",
    6: "フルハウス",
    7: "フォーカード",
    8: "ストレートフラッシュ",
    9: "ロイヤルストレートフラッシュ"
}

# カードのランクとスート(HA=ハートのエース, D2=ダイヤの2)
RANKS = "23456789TJQKA"
SUITS = "HDSC"  # ハート, ダイヤ, スペード, クラブ
DECK = [s + r for r in RANKS for s in SUITS]

# 役の評価(スコアをタプルで返す)
def evaluate_hand(hand):
    ranks = sorted([RANKS.index(c[1]) for c in hand], reverse=True)
    suits = [c[0] for c in hand]
    rank_counts = Counter(ranks)
    flush = len(set(suits)) == 1
    straight = len(rank_counts) == 5 and (max(ranks) - min(ranks) == 4 or set(ranks) == {12, 3, 2, 1, 0})  # A-2-3-4-5対応

    # 役の判定(同率はRankで比較)
    if straight and flush:
        if set(ranks) == {12, 11, 10, 9, 8}:  # A, K, Q, J, 10
            return (9, max(ranks))  # ロイヤルストレートフラッシュ
        return (8, max(ranks)) # ストレートフラッシュ
    if 4 in rank_counts.values():
        return (7, max(k for k, v in rank_counts.items() if v == 4))  # フォーカード
    if 3 in rank_counts.values() and 2 in rank_counts.values():
        return (6, max(k for k, v in rank_counts.items() if v == 3))  # フルハウス
    if flush:
        return (5, ranks)  # フラッシュ
    if straight:
        return (4, max(ranks))  # ストレート
    if 3 in rank_counts.values():
        return (3, max(k for k, v in rank_counts.items() if v == 3))  # スリーカード
    if list(rank_counts.values()).count(2) == 2:
        return (2, sorted([k for k, v in rank_counts.items() if v == 2], reverse=True))  # ツーペア
    if 2 in rank_counts.values():
        return (1, max(k for k, v in rank_counts.items() if v == 2))  # ワンペア
    return (0, ranks)  # ハイカード

これを用いてシミュレーションを行なっていきます。



テキサスホールデムのシミュレーション

初期手札2枚と公開札5枚で、役を作り、勝率がどうなるか
4人で対戦をする際の勝率を出す
シミュレーションプログラムです。

user_num のところが対戦人数になるので
変更すれば、その人数での確率を求めることができます。
# モンテカルロ法で勝率計算
def monte_carlo_win_rate(my_hand, num_simulations=10000):
    wins = 0
    user_num = 3
    for _ in range(num_simulations):
        deck = DECK.copy()
        for card in my_hand:
            deck.remove(card)

        # 4人分の手札をランダムに配る
        random.shuffle(deck)
        opponent_hands = [deck[i * 2: (i + 1) * 2] for i in range(user_num)]
        community_cards = deck[6:11]

        # 各プレイヤーのベストハンドを評価
        my_best = max(evaluate_hand(list(comb)) for comb in combinations(my_hand + community_cards, 5))
        opponent_best = [max(evaluate_hand(list(comb)) for comb in combinations(hand + community_cards, 5)) for hand in opponent_hands]

        # 自分が最も強い手を持っているか判定
        if my_best > max(opponent_best):
            wins += 1

    return wins / num_simulations

# 例: 自分の手札をセットして勝率を計算
my_hand = ["HA", "HK"]  # ハートのエース・キング
win_rate = monte_carlo_win_rate(my_hand, num_simulations=5000)
print(f"勝率: {win_rate:.2%}")
勝率: 33.02%

カードの組み合わせを変えれば
その都度計算が行えます。



初期カードの組み合わせでの勝率

2種の13 * 13 枚の組み合わせにおける
勝率を計算してみましょう。

RANKS = "23456789TJQKA"
H_DECK = [s + r for r in RANKS for s in "H"]
D_DECK = [s + r for r in RANKS for s in "D"]
combi = [[h, d] for h in H_DECK for d in D_DECK]
result = {":".join(c):0 for c in combi}
for my_hand in combi:
  result[":".join(my_hand)] = monte_carlo_win_rate(my_hand, num_simulations=5000)

これで、13x13=169通りの結果が出せます。

これをヒートマップにしてみましょう。


勝率をヒートマップにする


import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt

# キーのソート用の関数
def get_rank_order(card):
    return RANKS.index(card[1])

# キーを分離してデータフレームを作成
data_list = []
for key, value in result.items():
    row_key, col_key = key.split(':')
    data_list.append({'row': row_key, 'col': col_key, 'value': value})

df = pd.DataFrame(data_list)
unique_rows = sorted(df['row'].unique(), key=get_rank_order)
unique_cols = sorted(df['col'].unique(), key=get_rank_order)

# ピボットテーブルを作成
pivot_df = df.pivot(index='row', columns='col', values='value')

# インデックスと列を順序通りに並び替え
pivot_df = pivot_df.reindex(index=unique_rows, columns=unique_cols)

# ヒートマップの作成
plt.figure(figsize=(10, 8))
sns.heatmap(pivot_df,
            cmap='RdYlGn',
            vmin=0,
            vmax=1,
            annot=True,
            fmt='.2f',
            cbar_kws={'label': 'Value'})

plt.title('Win rate for first hand combination')
plt.tight_layout()
plt.show()
heatmap

やはり、最初にワンペアを持っているだけで勝率は高くなりますね
とはいえ、それだけでは勝てないのが
このテキサスホールデムルールの面白いところ

初期手札の読み合いやベットなどの駆け引き
この辺りが組み合わさることで
ゲーム性が高くかなり面白いものになっています。

とはいえ
日本ではまだまだ、カジノがないので
ポーカーを楽しむには
単純なゲームとしてのポーカーしか出来ません

オンラインで展開されているものは
ほぼ日本国内では違法ではあるので
手を出さないように気を付けないといけません!!!

カジノが出来たら
もっともっとシミュレーションしましょう。

今回はここまでです
それでは

今回は年末の飲み会の余興で
よく用いられる「ビンゴゲーム」
にまつわるものを検証してみました。

解説動画はこちら



ビンゴゲームとは

知らない方は少ないと思いますが
飲み会の余興などで行われる
安易なパーティーゲームの一種です。

ルールや遊び方は
5行x5列のマス目に
1 - 75までの数字が書かれた紙が配られ

ゲームマスターが数字を選び
手持ちの紙の数値が合えば穴を開ける
縦・横・斜めで5マス分の穴が空いたらクリア
というものです。

真ん中のマス目はFreeポケットになっていて
最初から開けられる物も有ります。

そんなビンゴゲームのアルアルを
色々考えてみました。


1. ビンゴ用紙を作ってみる

プログラム上で75個の数値から
25個を選んで、並べてあげれば
ビンゴ用紙(風の出力)の完成です。

import random

numbers = [i for i in range(1,76)]

# ランダムで数値を25個取得する
select = random.sample(numbers,25)
print(select)
print()

for i,n in enumerate(select):
    if i%5==0:
        print()
    print(n,end="\t")
[23, 24, 4, 64, 71, 29, 19, 6, 58, 70, 33, 68,
13, 25, 50, 12, 63, 11, 73, 22, 20, 36, 32, 51, 2]

23 24 4 64 71
29 19 6 58 70
33 68 13 25 50
12 63 11 73 22
20 36 32 51 2

import numpy as np
import pandas as pd

df = pd.DataFrame(np.reshape(select,[5, 5]))
print(df.to_string(index=False,header=False,col_space=8))
pandasで綺麗に描画するとこんなコードです。


もっとたくさん作りたい人向けには
画像化するコードをお送りします。
import random
from PIL import Image, ImageDraw, ImageFont
import matplotlib.pyplot as plt

font = ImageFont.truetype('Arial.ttf', 32)
numbers = [i for i in range(1,76)]

# 作る個数を指定する
n = 2
for i in range(n):
    select = random.sample(numbers,25)
    im = Image.new('RGB', (500, 500), "silver")
    draw = ImageDraw.Draw(im)
    count = 0
    for x in range(5):
      for y in range(5):
        x0,x1,y0,y1 = x*100, x*100+100, y*100, y*100+100
        draw.rectangle((x0, y0, x1, y1), fill="white", outline="black", width=4)
        if count!=12:
            draw.text((x0+32, y0+32), "{0:02}".format(select[count]), (0, 0, 0), font=font, align='center')
        else:
            draw.text((x0+32, y0+32), "●", (0, 0, 0), font=font, align='center')
        count+=1
    plt.imshow(im)
    plt.axis("off")
    plt.show()
bingooo

こんな感じで真ん中フリーポケットの
ランダム数字がかかれた用紙が出来るので
あとはそれをプリントすれば良いですね。

お金をかけたく無い人には
丁度良いかもしれません。



2.ビンゴゲームにおける色々な数字

2-1.25マスの数値の並びの組み合わせ

75 個の数字から
25 個取り出して並べる順列の総数は
こんな感じの数式で表せます。

s
プログラムでは組み合わせや
順列の数を求める事ができます。

import math

n = 75
k = 25

# p = n! / (n - k)!
p = math.perm(n, k)

print(p)
print("{:,}".format(p))
815712000579973729423615859451974909952000000
815,712,000,579,973,729,423,615,859,451,974,909,952,000,000


とてつも無い桁になりますね
ビンゴカードを全通りプリントするのは
辞めた方が良さそうです。


2-2.何回くらいでビンゴになるのか


順列数は多すぎるので全通りは実験出来ません。

代わりにビンゴシミュレーターで
100万回試行してみました。
# ビンゴの場合True , ビンゴでない場合はFalse
def check_bingo(b):
    # 横
    for i in range(5):
        if sum(b[i*5:i*5+5])==0:
            return True
    # 縦
    for i in range(5):
        b1,b2,b3,b4,b5 = b[i],b[i+5],b[i+10],b[i+15],b[i+20]
        if sum([b1,b2,b3,b4,b5])==0:
            return True
    # 斜め
    d1 = [b[d*6] for d in range(5)]
    d2 = [b[d*4] for d in range(1,6)]
    if sum(d1)==0 or sum(d2)==0:
        return True
    return False

# リストの中に数値がある場合はインデックスを、ない場合は-1を返す
def find_index(b,x):
    return b.index(x) if x in b else -1

# 何ターンでビンゴしたかをカウントする
def bingo_count(bingo,numbers):
    count=0
    for num in numbers:
        index = find_index(bingo,num)
        if index!=-1:
            bingo[index] = 0
        count+=1
        if check_bingo(bingo):
            return count
        else:
            continue
    return count

import random
numbers = [i for i in range(1,76)]
random.shuffle(numbers)

n = 1000000
calc = {k:0 for k in range(1,76)}
for i in range(n):
    # ランダムで数値を25個取得してビンゴ用紙を作る
    bingo = random.sample(numbers,25)
    # 真ん中は開ける(数値0は穴開き)
    bingo[12] = 0
    ans = bingo_count(bingo,numbers)
    calc[ans]+=1

# 結果
print("総回数 : {0}回".format(n))
tmp = []
for k,v in calc.items():
    r = v / n * 100
    print("{0:02} 回目, : {1:06}回 , {2:.05}%".format(k,v,r))
    tmp.append([k,v,r])

実行するのにすごく時間が掛かります。
これを描画してみましょう。
# 描画
import matplotlib.pyplot as plt
import pandas as pd

df_b = pd.DataFrame(tmp,columns=["num","count","rate"])
df_b["cumsum"] = df_b["rate"].cumsum()

fig, ax1 = plt.subplots(figsize=(16,8))
ax2 = ax1.twinx()
ax1.bar(df_b["num"],df_b["rate"])
ax2.plot(df_b["cumsum"],color="red")
plt.show()
kakuritu


だいたい
16回くらいやれば、10%くらいの人がビンゴ
41回くらいやれば、半分くらいの人がビンゴ
49回くらいやれば、80%くらいの人がビンゴ
になるようです。




2-3.ビンゴにならずに開けられる数の最大数


1つづつ穴を開けていくと
穴の組み合わせが多すぎるため大変です。

ビンゴ用紙に25個穴が空いた状態から穴を塞いで
ビンゴにならないかどうかを判定して
測定する形にしました。
# 結果のプリント用
def print_bingo(bingo):
    for i,b in enumerate(bingo):
        if i%5==0:
            print()
        print(b,end="\t")
    print()

# ビンゴでない場合True , ビンゴの場合はFalse
def check_bingo(b):
    # 横
    for i in range(5):
        if sum(b[i*5:i*5+5])==5:
            return False
    # 縦
    for i in range(5):
        b1,b2,b3,b4,b5 = b[i],b[i+5],b[i+10],b[i+15],b[i+20]
        if sum([b1,b2,b3,b4,b5])==5:
            return False
    # 斜め
    d1 = [b[d*6] for d in range(5)]
    d2 = [b[d*4] for d in range(1,6)]
    if sum(d1)==5 or sum(d2)==5:
        return False
    return True

count = 0
all_count = 0
seq = [i for i in range(0,25)]

combi = list(itertools.combinations(seq,5))
for c in combi:
    all_count +=1

    # ビンゴを用意(1==穴あき)
    bingo = [1] * 25

    # 穴なしに戻す(0==穴なし)
    for i in c:
        bingo[i]=0

    # ビンゴでないかどうかの判定
    if check_bingo(bingo):
        print(c)
        print_bingo(bingo)
        print()
        count +=1

print(count,all_count)
(0, 6, 12, 18, 24)

0 1 1 1 1
1 0 1 1 1
1 1 0 1 1
1 1 1 0 1
1 1 1 1 0

・・・・
48 53130

結果として
穴が20個まではビンゴにならずに
開けられるパターンが存在します。

そんなに穴が開いた状態になった人
いるんでしょうかねえ

逆にめちゃくちゃ運が悪い方を
決めることが出来ます。

シミュレーション結果を用いれば
どれくらいでプレゼントが捌けるのか
計算しやすくなると思いますので
幹事の方は逆算してみると
捗るかも知れません。

今回はビンゴゲームについての
数値検証でした。

それでは。


このページのトップヘ