ディールプログラムを作る - Pythonの集合を使う

私は古典的なゲームプレイのメークディールの結果を信じていなかったことが主な理由です( httpsを参照)。 ://math.stackexchange.com/questions/608957/monty-hall-problem-extended )この小さなプログラムを作成しました... 3つのドアがあり、クイズマスターがドアを開けてあなたがいたらそれからドアのあなたの最初の選択からあなたのチャンスは33%から67%に上がる!このプログラムはドアの数とクイズマスターが開くドアの数を選ぶことができる一般化されたバージョンです、それは最初の選択のための価格のチャンス(基本的にドアの数の上の1)そしてあなたがドアが開かれた後に選択したドアを変更してください。

私はPythonセットを使用したエレガントな解決策に心から惹かれましたが、これが最も効率的な方法かどうか疑問に思います。他の方法と比較すると、より多くのドアと開くドアがあれば比較的効率的になるようです。

ご意見ありがとうございます。

#!/usr/bin/env python
'''  application of Make a deal statistics
     application is using sets {}
     for reference:
 https://math.stackexchange.com/questions/608957/monty-hall-problem-extended
'''
import random


def Make_a_Deal(doors, doors_to_open):
    '''  Generalised function of Make_a_Deal. Logic should be self explanatory
         Returns win_1 for the option when no change is made in the choice of
         door and win_2 when the option to change is taken.'''

    win_1, win_2 = False, False

    doors_set = set(range(1, doors+1))
    price_set = set(random.sample(doors_set, 1))
    choice1_set = set(random.sample(doors_set, 1))
    open_set = set(random.sample(doors_set.difference(price_set).
                   difference(choice1_set), doors_to_open))
    choice2_set = set(random.sample(doors_set.difference(open_set).
                      difference(choice1_set), 1))
    win_1 = choice1_set.issubset(price_set)
    win_2 = choice2_set.issubset(price_set)

    return win_1, win_2


def main():
    '''  input:
         - throws: number of times to Make_a_Deal (must be > 0)
         - doors: number of doors to choose from (must be > 2)
         - doors_to_open: number of doors to be opened before giving the option
           to change the initial choice (must be > 0 and <= doors-2)'''

    try:
        throws = int(input('how many throws: '))
        doors = int(input('how many doors: '))
        doors_to_open = int(input('how many doors to open: '))
        if (throws < 1) or (doors < 3) or \
                (doors_to_open > doors-2) or (doors_to_open < 1):
            print('invalid input')
            return

    except Exception as e:
        print('invalid input: ', e)
        return

    number_of_wins_1, number_of_wins_2, counter = 0, 0, 0

    while counter < throws:
        win_1, win_2 = Make_a_Deal(doors, doors_to_open)

        if win_1:
            number_of_wins_1 += 1
        if win_2:
            number_of_wins_2 += 1

        counter += 1
        print('completion is {:.2f}%'.
              format(100*counter/throws), end='\r')

    print('number of wins option 1 is {:.2f}%: '.
          format(100*number_of_wins_1/counter))
    print('number of wins option 2 is {:.2f}%: '.
          format(100*number_of_wins_2/counter))


if __name__ == '__main__':
    main()
10
nl ru de

4 答え

これは\ $ O(1)\ $空間と時間で行うことができ、そこではそれを\ $ O(n)\ $空間と時間で行います。私はあなたのコードをきれいにし、Graipherのコードと大体同じになりました:

def make_a_deal(doors, doors_to_open):
    doors = range(1, doors+1)
    price = set(random.choice(doors))
    choice_1 = set(random.choice(doors))
    doors = set(doors)
    open_doors = set(random.sample(doors - price - choice_1, doors_to_open))
    choice_2 = set(random.sample(doors - open_doors - choice_1, 1))
    return choice_1 <= price, choice_2 <= price

しかし、それを早くするためにあなたができるいくつかの変更があります。

  1. pricechoice_1 を生成したら、ユーザーが獲得したかどうかを確認し、 True、False を返します。
  2. price をランダムに選択する必要はありません。これは、常に価格が最初になるまでドアを回転させることができるためです。そのため、 price = 0 にすることができます。
  3. 上記を拡張すると、0が価格の唯一のドアであることがわかりました。したがって、\ 1からn \ $はすべて敗者のドアです。これから私達が選んだドアの一つが敗者のドアであったことがわかったので、そのドアを外して他のすべてのドアを下に移動させることができます。これは最後のドアを外すのと同じです。
  4. 上記を拡張して、ランダムに選択されているため、\ $ k \ $より多くの敗者の扉を削除できます。
  5. 2番目の選択肢は\ $ 0 \からn - k - 1 \ $の範囲です。

そして、あなたは\ $ O(1)\ $スペースと時間を得ることができます:

def make_a_deal(doors, doors_to_open):
    price = 0
    choice_1 = random.randrange(doors)
    if choice_1 == price:
        return True, False

    choice_2 = random.randrange(doors - 1 - doors_to_open)
    return False, choice_2 == price
5
追加された
@Baldrickk random.randrange は範囲からランダムに選択します。私がするのは、OPが行ったように、ドアを別のグループに移動することです。それらから選択しないでください。
追加された 著者 Taamer,
どのドアが賞品を隠していても問題は技術的には同じですが、シミュレートされている問題により厳密に一致させるためにランダムな選択をします。
追加された 著者 mike,

set のサンプリングにヘルパー関数を使用した2番目の @ Graipherの回答しかし、戻り値を random.sample から set に自動的に変換する次の定義を使用することで、解決策を改善できます。

def random_choice(population, k=1):
    return {*random.sample(population, k)}

あなたのdocstringはコードの読者に意図されているように思われるのであまり意味がありません。代わりに、ユーザーのためにあなたのdocstringを書いてください、どのようにではなく、機能/モジュールがしているかについて文書化してください。


あなたはその名前の中で変数の型を明示する必要はなく、格納されているものを反映する名前を選ぶのであって、方法を選ばない。


入力の収集は main から切り離します。たとえ多すぎるとしても、妥当性は確認できます。つまり、throwsが正でない場合は、throwは発生せず、統計は0になります。あなたの main をあなたのデータが来たり来たりする場所から不可知論者にして、再利用性とテストを容易にしましょう。あなたが本当に進行状況を報告したいのであれば、代わりに振る舞いをオプションにして簡単に相互交換可能にするためにコールバックを受け入れるべきです。

main とは別に入力の収集を行うと、 inputargparse 、またはそれに関するGUI。


ここでは一般的な Exception をキャッチしないでください。 ValueError だけが気になります。たとえば KeyboardInterrupt のような例外に対して Invalid input が表示されるのを防ぎます。

エラーメッセージを sys.stderr に出力し、オプションで sys.exit()


明示的なカウンタで forwhile の上で使用することを優先します。


改善案:

#!/usr/bin/env python3

"""Application of Make a deal statistics

Application is using sets {}

For reference:
    https://math.stackexchange.com/questions/608957/monty-hall-problem-extended
"""

import sys
import random


def random_choice(population, k=1):
    return {*random.sample(population, k)}


def make_a_deal(doors, doors_to_open):
    """Generalised function of Make a Deal.

    Returns a pair of boolean indicating:
     - wether the original choice was a win for the first element
     - wether the changed choice was a win for the second element
    """

    doors = set(range(doors))
    prize = random_choice(doors)
    original_choice = random_choice(doors)

    opened = random_choice(doors - prize - original_choice, doors_to_open)
    changed_choice = random_choice(doors - opened - original_choice)

    return original_choice <= prize, changed_choice <= prize


def main(throws, doors, doors_to_open, progression_callback=None):
    """Gather statistics about repeated simulations of Make a Deal game.

    Parameters:
     - throws: number of simulations to run
     - doors: number of doors to choose from (must be > 2)
     - doors_to_open: number of doors to be opened before giving the
    option to change the initial choice (must be > 0 and <= doors - 2)
    """
    if doors < 3:
        raise ValueError('doors must be at least 3')

    if not (0 < doors_to_open <= doors - 2):
        raise ValueError('doors to open mismatch the number of doors')

    original_wins = changed_wins = 0
    for throw in range(throws):
        original_win, changed_win = make_a_deal(doors, doors_to_open)

        if original_win:
            original_wins += 1

        if changed_win:
            changed_wins += 1

        if progression_callback is not None:
            progression_callback(throw/throws)

    return original_wins/throws, changed_wins/throws


def show_completion(ratio):
    print('Completion is {:.2%}'.format(ratio), end='\r')


if __name__ == '__main__':
    try:
        throws = int(input('How many throws: '))
        doors = int(input('How many doors: '))
        doors_to_open = int(input('How many doors to open: '))
        wins1, wins2 = main(throws, doors, doors_to_open, show_completion)
    except ValueError as e:
        print('Invalid input:', file=sys.stderr, end=' ')
        sys.exit(e)

    print('Number of wins without changing is {:.2%}'.format(wins1))
    print('Number of wins after changing is {:.2%}'.format(wins2))
4
追加された
おかげで、たくさんの機能、特にrandom_choiceが直接セットを作り、関数show_completionをmainに渡してくれました。
追加された 著者 Farhat,

Pythonには公式のスタイルガイド、 PEP8 があります。変数名と関数名には lower_case を使用することをお勧めします。 docstrings に関するスタイルガイドもあります。 PEP257 では、三重二重引用符の使用をお勧めします( "" "だから、このように" "" )。

あなたのモジュール docstring には、 "n個のドアを持つ拡張モンティ - ホール問題、そのうちのk個はゲームマスターによって開かれています"のような1文章の要約を含めるべきです。


random.choiceset に対して機能しないのは残念です。なぜならそれらはインデックス化できないからです。ただし、代わりに random.sample(x、1)を使用している場合が多いので、それを関数に含めることをお勧めします。

def random_choice(x):
    return random.sample(x, 1)[0]

これは、 set(random_choice(x))の代わりに {random_choice(x)} を使用する必要があることも意味します。これは単一の要素であり、リストではなくなったためです。一つの要素で。


doors_set.difference(price_set).difference(choice1_set) is the same as doors_set - price_set - choice1_set, which is shorter and at least as readable.

Similarly, choice1_set.issubset(price_set) is equivalent to choice1_set <= price_set (even though here the more expressive version is probably easier to understand).

Pythonで変数を宣言する必要はありません。特にその値をオーバーライドする場合( win_1win_2 )は特にです。

私は option_1option_2no_changechange のわかりやすい名前に変更しました。

二度と doors を使用しないので、名前から末尾の _set をすべて削除します。

あなたが0か1で始まるあなたのドアを列挙しても違いはありません、それで私は簡単のために set(range(doors))を使います。

これで、 make_a_deal 関数は次のようになります。

def make_a_deal(doors, doors_to_open):
    '''  Generalised function of Make_a_Deal. Logic should be self explanatory
         Returns win_1 for the option when no change is made in the choice of
         door and win_2 when the option to change is taken.'''

    doors = set(range(doors))
    price = {random_choice(doors)}
    choice1 = {random_choice(doors)}
    open_doors = set(random.sample(doors - price - choice1, doors_to_open))
    choice2 = {random_choice(doors - open_doors - choice1)}
    no_change = choice1 <= price
    change = choice2 <= price

    return no_change, change
3
追加された
ありがとう、非常に有効な入力を船上に持っていこう。あなたはここでセットを使うことの効率についてコメントすることができますか?
追加された 著者 Farhat,

解決策に同意できない場合は、問題を自分でテストすることをお勧めします。そのことをお勧めします。しかし、あなたがしているのは統計的問題のシミュレーションなので、これを最適化し、より良くするための不幸な解決策は、単純に数学を使って確率の公式を計算することです。

このために、さらに一般化します。複数の賞がある場合があります。しかし、ホストは賞金を背後にしてドアを開くことができないため、開くことができるドアの数が制限されます。

\ $ n \ $扉、\ $ p \ $賞品があり、ホストが\ $ d \ $扉を開いた場合、扉を切り替えずに勝利する確率は単純に\ $ \ frac {p} {n} \ $です。ホストがドアを使って行うことは私たちの最初の選択には影響しません。また、ゲームが進行するために、ホストは\ n-p-1 \ $を超えてドアを開くことができません。

切り替えて勝利する確率を計算するために、シナリオを2つのケースに分けます。

  1. 最初の推測では正しかった
  2. 最初の推測では賞品を選びませんでした

\ $(1)\ $は確率\ $ \ frac {p} {n} \ $で発生し、逆に\ $(2)\ $は確率\ $ \ frac {np} {n} \ $で発生します。 。

ケース\ $(1)\ $の場合、最初に選んだドアの後ろに賞があります。そのため、残りの閉じたドアの後ろに\ $ p-1 \ $賞があります。ホストが\ $ d \ $ドアを開いているので、切り替える$ \ n-d-1 \ $ドアがあります。

case \ $(2)\ $の場合は、切り替えることができるドアの後ろに\ $ p \ $賞品がありますが、選択できるドアはまだあります。

確率を掛けると、次のようになります。

$$ P(勝ち\切替え)= \ frac {p} {n} \ frac {p-1} {nd-1} + \ frac {np} {n} \ frac {p} {nd-1} = \ frac {p} {n} \ frac {n-1} {nd-1} $$

これを考慮すると、私のpython実装は次のようになります。

def get_win_percentage(n, d, p, switching):
    if d > n-p-1:
        return 0
    if not switching:
        return p/n
    return (p/n)*(n-1)/(n-d-1)

これはあなたの質問に直接答えないことを私は知っていますが、あなたは他のユーザーからたくさんの良いフィードバックを得ているので、私はこの方法で貢献したいと思いました。

3
追加された
確率計算の背景のおかげで、これは私にシミュレーションをテストするための良い機能を与えるでしょう。
追加された 著者 Farhat,