2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

「Python でビンゴゲーム の確率シミュレーションしてみた」を修正してみた

Last updated at Posted at 2015-01-04

元ネタ: [Python でビンゴゲーム の確率シミュレーションしてみた]
(http://qiita.com/elzup/items/d532ffa1d326fbf75d01)

読んでておもしろかったから Python のお作法的なところをみながらちょっと修正してみた。

メイン処理を作る

if __name__ == '__main__':
    main()

main() を作るかは好みだけど、スクリプトとして実行したときの処理を分けておくことで、別ターミナルなどでインタラクティブシェルを起動しておいて、モジュールとして import してデバッグしたりするのに便利です。

例えば、generate_card() の実装を変えてみて

 def generate_card():
@@ -25,12 +33,22 @@
-    cards = []
-    for i in range(5):
-        cards.extend(sample(range(15 * i + 1, 15 * (i + 1)), 5))
-    cards[12] = HIT
-    return cards
+    def sampling(k=5):
+        for i in range(k):
+            yield sample(range(15 * i + 1, 15 * (i + 1) + 1), k)
+
+    from itertools import chain
+    card = list(chain.from_iterable(sampling()))
+    card[12] = HIT
+    return card

ipython でパフォーマンスを計測したりする。

$ ipython3-3.4 
In [1]: import orig_Bingo
In [2]: timeit -n 100 orig_Bingo.generate_card()
100 loops, best of 3: 86.4 us per loop

In [3]: import Bingo_kai
In [4]: timeit -n 100 Bingo_kai.generate_card()
100 loops, best of 3: 95.3 us per loop

argparse を使って引数処理する

try ... except でごにょごにょするぐらいなら argparse 使って引数処理してしまおう。argparse の使い方も定型的なものなので慣れてしまえば簡単です。余談だけど、clickclip のような cli ツールを最近見かけた。でも、用途や必要性があんまりよく分かってない。

    parser = argparse.ArgumentParser()
    parser.set_defaults(num=10000, times=5, verbose=False)
    parser.add_argument('-n', '--num', type=int, help='set card number')
    parser.add_argument('-t', '--times', type=int, help='set times')
    parser.add_argument(
        '-v', '--verbose', action='store_true',
        help='set verbose mode (loglevel=DEBUG)')

せっかく引数処理するようにしたから logging 使ってデバッグ出力を切り替えるようにしてみる。logging モジュールの使い方も定番ですね。

logger = getLogger(__name__)
_handler = StreamHandler()
_handler.setLevel(DEBUG)
logger.addHandler(_handler)
...
logger.setLevel(DEBUG)

doctest を書く

本当は元記事の↓に反応してこれが言いたかっただけ。

docブロックの書き方がいまいちまだわからないので勉強していこうと思う

Python の特徴の1つとして doctest がある。その関数の使い方のドキュメントと簡単なテストを兼ねると一石二鳥だったりする。

元のコードのロジックをいじらないように手を加えてたら、修正したコードの doctest も良いものじゃないのだけど、doctest 書きながら開発しようとすると、使う立場への視野も広がって自然と使いやすい関数やモジュールを実装できたりする。

追記:
うまくいなかったからリベンジした => 「Python でビンゴゲーム の確率シミュレーションしてみた」で doctest を書いてみた

    """
    >>> check_bingo([False, True, False])
    False
    >>> check_bingo([True] * 5)
    True
    >>> check_bingo([False, False, True, False, False] * 5)
    True
    >>> check_bingo([
    ... True, False, False, False, False,
    ... False, True, False, False, False,
    ... False, False, True, False, False,
    ... False, False, False, True, False,
    ... False, False, False, False, True,
    ... ])
    True
    """

random.sample を使ってるからモックがないとテスト書くの難しいかなぁ ... :sweat:

修正したコード

いざやってみたら言いたいことにあうようにあんまりうまく修正できなかった ... :disappointed_relieved:

このコードは Python3 で動く。

Bingo_kai.py
# -*- coding: utf-8 -*-

u"""BINGO の確率をシミュレーションするスクリプト
input:
    times = 抽選機を回す回数, card_num = カードの数
outpu:
    1本でも揃う確率

"""
import argparse
import sys
from logging import DEBUG, StreamHandler, getLogger
from random import sample

# 穴
HIT = True
NOHIT = False

# ログ出力
logger = getLogger(__name__)
_handler = StreamHandler()
_handler.setLevel(DEBUG)
logger.addHandler(_handler)


def generate_card():
    u"""カードを生成
    5x5で中心(3行3列)に穴が空いたカード
    B(1列目) 1 - 15
    I(2列目) 16 - 30
    N(3列目) 31 - 45
    G(4列目) 46 - 60
    O(5列目) 61 - 75
    :returns: array card, length=25

    >>> from math import floor
    >>> card = generate_card()
    >>> card_len = len(card)
    >>> card_len == 25
    True
    >>> card[floor(card_len / 2)]
    True
    """
    def sampling(k=5):
        for i in range(k):
            yield sample(range(15 * i + 1, 15 * (i + 1) + 1), k)

    from itertools import chain
    card = list(chain.from_iterable(sampling()))
    card[12] = HIT
    return card


def check_bingo(card):
    u"""BINGOしているかどうか
    判定のみかえす
    param: array
    :returns: boolean

    >>> check_bingo([False, True, False])
    False
    >>> check_bingo([True] * 5)
    True
    >>> check_bingo([False, False, True, False, False] * 5)
    True
    >>> check_bingo([
    ... True, False, False, False, False,
    ... False, True, False, False, False,
    ... False, False, True, False, False,
    ... False, False, False, True, False,
    ... False, False, False, False, True,
    ... ])
    True
    >>> check_bingo([
    ... False, False, False, False, True,
    ... False, False, False, True, False,
    ... False, False, True, False, False,
    ... False, True, False, False, False,
    ... True, False, False, False, False,
    ... ])
    True
    """
    if card.count(HIT) < 5:
        return False

    for i in range(5):
        if all(card[i * 5:(i + 1) * 5]):  # horizontal
            return True
    for i in range(5):
        if all(card[i:i + 21:5]):  # vertical
            return True
    if all(card[0:25:6]) or all(card[4:21:4]):  # skew
        return True

    return False


def print_card(card):
    msg = ''
    for i, v in enumerate(card):
        if v == HIT:
            v = 'o'
        elif v == NOHIT:
            v = 'x'
        msg += '%3s' % v
        if i % 5 == 4:
            msg += '\n'
    logger.debug(msg)


def parse_argument(argv=None):
    """
    >>> parse_argument([])
    Namespace(num=10000, times=5, verbose=False)
    >>> parse_argument(['-n', '3', '-t', '30', '-v'])
    Namespace(num=3, times=30, verbose=True)
    """
    parser = argparse.ArgumentParser()
    parser.set_defaults(num=10000, times=5, verbose=False)
    parser.add_argument('-n', '--num', type=int, help='set card number')
    parser.add_argument('-t', '--times', type=int, help='set times')
    parser.add_argument(
        '-v', '--verbose', action='store_true',
        help='set verbose mode (loglevel=DEBUG)')

    args = parser.parse_args(sys.argv[1:] if argv is None else argv)
    if args.verbose:
        logger.setLevel(DEBUG)

    return args


def do_bingo(args):
    card = generate_card()
    lots = dict(sample([(i, HIT) for i in range(1, 76)], args.times))
    card_hole = [lots.get(i, NOHIT) for i in card]
    logger.debug('lots: {}\n'.format(sorted(lots.keys())))
    print_card(card)
    print_card(card_hole)
    return card_hole


def main():
    args = parse_argument()
    hit_count = 0
    for i in range(args.num):
        card_hole = do_bingo(args)
        if check_bingo(card_hole):
            hit_count += 1

    print(str(args.times) + u"回目で1本でもBINGOしてる確率:")
    p = hit_count / args.num
    print("%s (%.1f %%)" % (str(p), p * 100))


if __name__ == '__main__':
    main()

こんな感じ。

$ python Bingo_kai.py -n 3 -t 45 -v
...
lots: [1, 3, 8, 9, 10, 11, 12, 14, 16, 17, 19, 21, 22, 24, 26, 28, 29, 30,
       31, 32, 33, 35, 36, 37, 39, 40, 41, 45, 46, 47, 49, 52, 55, 57, 60,
       61, 63, 64, 67, 68, 69, 71, 72, 73, 75]

  3 11  5  o 12
 20 18 16 26 17
 37 42  o 32 40
 50 47 48 51 49
 67 66 70 74 64

  o  o  x  o  o
  x  x  o  o  o
  o  x  o  o  o
  x  o  x  x  o
  o  x  x  x  o

45回目で1本でもBINGOしてる確率:
0.6666666666666666 (66.7 %)
2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?