数学の具体的な問題にPythonを使って、数学もPythonも同時に学んでしまいましょう。今回はPythonを使った確率・統計の問題として、カードめくりの問題をみてみたいと思います。ランダムな要素のある馴染み深い身近なゲームとしては、カードを使ったゲームが思い浮かぶでしょう。トランプやUNO、花札や麻雀も大まかな括りではカードゲームということができます。ランダムに配られるカードですが、どのカードが何枚あるかというゲームの枠組み、山に残っているカード、手持ちのカード、オープンにされたカードなどの情報から、確率をもとに戦略的に勝利を目指します。個別のゲームでそれぞれの状況に応じて、最適な戦略を問うのは数学的にも面白い問題です。
ただ本記事では個別具体の問題には立ち入らず、また数学に関しても深入りせず、Pythonでカードめくりを実装する方法について解説したいと思います。
カードめくり:等しい確率
1から5の数字が書かれたカードを1枚ずつ、合計5枚用意したカードの山から3枚だけランダムに引いたとき、手札になるカードの組み合わせを出力するプログラムを書いてみます。
コイントス
カードめくりの前に、ウィーミングアップとしてコインの裏表をうらなうコイントスを考えてみましょう。Pythonには(擬似)乱数が用意されています。
import random as rd rd.randint(0, 1) # randint(a,b) で a 以上 b 以下の整数をランダムに出力
そこで、1を表に、0を裏に対応させることで、ランダムな出力をコイントスだと思うことができます。たとえば、コイントスを10回だけ試行する場合には
import random as rd def r(k): return rd.randint(0, 1) # ゼロか1をランダムに出力する関数 R = [] for k in range(10): R.append(r(k)) print(R)
とします。たとえば
のような結果が出力されます。もちろんいま乱数を使っているので、出力される結果は入力のたびに変わります。実際に何度か同じコマンドを実行してみると、毎回出力が変わることが確認できると思います。
本記事は、以下の過去記事の発展的な内容になっています。あわせて読んでいただければさいわいです:
カードめくり
さて、カードの山から1枚カードを引くというのは、このrandint関数を使って処理することができるでしょう。つまり、ランダムに出力される1から5の数字が、そのまま引いたカードに書かれた数字だと思えます。しかし、一度引いたカードは手札に加えているため、山からなくなってしまいます。この情報をケアしつつ、2枚目、3枚目のカードを引く処理をしないといけません。そこで山に残っているカードをリストyamaによって
yama = [1,1,1,1,1]
として記録しておきます。yamaの k番目の要素が、山にある数字 kのカードの枚数に対応します。次に、ランダムに1から5までの数字を出力しましょう。
num_card = 5 import random as rd def card(k): return rd.randint(1, num_card)
出た数字のカードが山に残っている場合、つまりリストyamaの要素が1である場合にはリストtefudaに加え、リストyamaから対応するカードの枚数を1つ減らすという処理を、リストtefudaの要素数が3になるまで続けます。
num_open = 3 tefuda = [] while( len(tefuda) != num_open ): c = card(0) if yama[c-1] == 1: tefuda.append(c) yama[c-1] = yama[c-1] -1
以上のようにして1から5の数字が書かれたカードを1枚ずつ、合計5枚用意したカードの山から3枚だけランダムに引くというカードめくりが、Pythonで実現することができました。以下、重複になりますがプログラムをまとめて書いておきます。
num_card = 5 num_open = 3 yama = [1]*num_card tefuda = [] import random as rd def card(k): return rd.randint(1, num_card) while( len(tefuda) != num_open ): c = card(0) if yama[c-1] != 0: tefuda.append(c) yama[c-1] = yama[c-1] -1 tefuda
カードめくり:重みづけられた確率
上の例では、各数字の書かれたカードは1枚ずつ入っており、山に残っている数字のどれを引くかは確率的に均等でした。いま問題をほんの少し複雑にして、例えば次のような計12枚のカードの山
数字 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
枚数 | 1 | 3 | 4 | 3 | 1 |
を考えてみましょう。まずはリストyamaの初期値を
yama_new = [1,3,4,3,1]
とすれば良いように思います。問題は、ランダムに1から5までの数字を出すときに、数字の出る確率が均等でないうえに、山に残ったカードに応じて確率が変わることを考慮する必要がある点です。そこで数字のセットnumbersを用意し
numbers = [1,2,3,4,5]
リストyama_newによって重み付けられた確率でその要素が出力できれば良いことがわかります。このような処理もrandomライブラリに実装されていて、choices関数を用います。
rd.choices(numbers, k=1, weights = yama_new)[0]
ここで2つ目の引数 k は選び出す個数を指定するもので、今の場合、カードを1枚ずつ引いていきたいので k=1 としています。またchoices関数はリスト形式での出力なので、要素を取り出すために末尾に [0] をつけています。
これが実際に、問題設定のような山に応じたカードめくりになっているかどうか、試行回数を大きくとって(ここでは6000回としてみます)統計的に確認してみましょう。カードを引く前の状態の山からカードを1枚だけ引くとき、1と5が出る確率はそれぞれ1/12で、2と4が出るのはそれぞれ1/4、3が出るのは1/3です。全試行回数が6000回の場合には、順に500回、1500回、2000回程度になるはずです。ヒストグラムを作成してみましょう。
num_trial = 6000 T = [] for k in range(num_trial): T.append(rd.choices(numbers, k=1, weights = yama_new)[0]) import matplotlib.pyplot as plt plt.hist(T, bins=5, range=(1, 6)) plt.show()
より
といったヒストグラムを得て、うまくいっていることがわかります。
さて、以上の準備のもとに、12枚の山からカードを3枚だけ引いたときの手札を出力してみます。大きな変更点は、card関数を置き換えたことです。
num_open = 3 yama_new = [1,3,4,3,1] numbers = [1,2,3,4,5] tefuda = [] import random as rd def card_new(k): return rd.choices(numbers, k=1, weights = yama_new)[0] while( len(tefuda) != num_open ): c = card_new(0) if yama_new[c-1] != 0: tefuda.append(c) yama_new[c-1] = yama_new[c-1] -1 tefuda
より
といった出力を得ます。このとき確認のために残っている山を見てみると
yama_new
などとなって正しく処理が行えていそうなことがわかります。
最後に、定義した関数cardが正しく動作していたことを確認するために、カードを引いた後の残りの山からカードを1枚だけ引くという試行のヒストグラムを出力してみると
num_trial = 9000 T = [] for k in range(num_trial): T.append(card_new(k)) import matplotlib.pyplot as plt plt.hist(T, bins=5, range=(1, 6)) plt.show()
より
などとなって、確かに山に残ったカードの枚数を反映していることがわかります。
今回は、乱数を使ってコンピュータ上でカードめくりを実現してみました。