くじの景品を引く確率と期待値に特化した計算機をpythonで作った

![image.png](https://qiita-image-store.s3.amazonaws.com/0/113494/6cdc465c-cfb7-4b95-a65c-382b6342fefc.png)

# Ichiban
一番くじシミュレータ。
自作メソッドを追加できるリスト"collections.UserList"を継承して実装を易しくしています。
> 以前`collenction.UserDict`を使用して実装しましたが、大きなバグがあったので全面的に見なおし、今の形になりました。


## くじを作成
ポケモンくじのインスタンスを作成します。
A賞が1個、 B賞が2個、 C賞が2個の仮想のくじを想定します。

```python
>>> poke = Ichiban(A=1, B=2, C=2)  # 引数は残り景品の数
```

このインスタンスを使って次のことができます。

* `poke`から賞を引く確率を計算: `kuji()`メソッド
* くじを引くシミュレート: `hiku()`メソッド
* くじ自体の期待値を計算: `kakaku()`メソッド

pokeの実体は`poke.data`にリスト型として格納されています。以後`poke`を呼び出すと、リストが返ってきます。
また、ichiban.pyに記述されているメソッド以外は、UserListを継承しているため、リストのメソッドが使えます。

```python
>>> poke.data  # 残り景品
['A', 'B', 'B', 'C', 'C']
```

`poke`はリスト型なので、`len`関数を使って景品の全数を調べることができます。

```python
>>> len(poke)  # 景品の全数
5
```

今あるくじの残り景品数を見るには`poke`で参照できます。
`poke`だけを呼び出すと、景品の数をカウントして、dict(のように見える)型として返ってきます。


```python
>>> poke  # 残り景品とその数
{'A': 1, 'B': 2, 'C': 2}
```

辞書型として扱うときは`dict()`メソッドを使います。

```python
>>> poke.dict()  # 残り景品とその数
{'A': 1, 'B': 2, 'C': 2}
>>> type(poke.dict())
dict
```


## 確率計算
今あるくじを引いたときに、当てることができる景品の確率を計算します。

```python
>>> poke.kuji()  # それぞれの景品を引く確率
{'A': 0.2, 'B': 0.4, 'C': 0.4}
```

A賞が0.2、B,C賞が0.4の確率で当たることを示しています。

特定の賞の確率だけを抜き出すこともできます。
以下のようにして、`kuji()`メソッドの引数にA, Bを入れることで、A賞、B賞が当たる確率の合計を表示します。

```python
>>> poke.kuji('A', 'B')  # A賞またはB賞を引く確率
0.6
```
`


## 期待値計算
期待値は「景品が当たる確率 x 景品の価格」で計算できます。
`kakaku()`メソッドを使えば簡単に計算をすることができます。
`kakaku()`メソッドの引数には景品の価格を自分で決めて、入力します。
以下の例ではA賞が2000円、B賞が1000円、C賞が100円として計算しました。

```python
>>> poke.kakaku(A=2000, B=1000, C=100)  # 期待値計算
840.0
```

結果、このくじの価格は840円です。
一番くじの価格は620円なので、計算上、このくじを引くと220円も得です。

> 実際のくじは店側が儲かるように作られるため、くじの初期配置の期待値はくじの金額を上回ることはありません。
> しかし、くじが妙に偏って引かれて上位賞だけが残ったり、特定の商品の価格を吊り上げたりすればくじの価格を上回ることは可能です。
> あくまで、賞に価格を付けるのは計算者自身なので、極端な話、C賞が2000円相当としてC賞がたくさん余っていればくじの期待値は上がります。


`kakaku()`メソッドの引数にdefaultを追加しました。指定した景品の価格以外をdefaultの価格に設定します。defaultのデフォルト値は0です。

```python
>>> poke.kakaku(A=2000, default=100)  # A賞2000円、それ以外100円
480.0
>>> poke.kakaku(A=2000)  # defaultは指定しなければ0円
400.0
```


## シミュレート

現状の景品数

```python
>>> poke()
{'A': 1, 'B': 2, 'C': 2}
```

A賞を引いて残数を1減らす(引数が無ければ景品ランダム)

```python
>>> poke.hiku('A')
'A'
```

A賞を引いた後の景品の数

```python
>>> poke()
{'B': 2, 'C': 2}
```

A賞を引いた後の確率

```python
>>> poke.kuji()
{'B': 0.5, 'C': 0.5}
```

引数がなければ、0になっていないくじを一枚ランダムに引きます。

```python
>>> poke.hiku()
'B'

# または'C'
```

```python
>>> len(poke)  # 景品の全数
3
```


## まとめ
以上の流れをまとめると

* `poke = Ichiban(A=1, B=2, ...)`でインスタンス作成
* `poke.kuji()` で確率計算
* `poke.kakaku()`で期待値計算
* `poke.hiku()`でくじシミュレーション

# ソース

```python:ichiban.py
#!/usr/bin/env python3
"""Ichiban kuji simulator"""
from collections import UserList
import random


def sumup_list(li: list) -> dict:
    """ count duplication of list elements and return dictionary"""
    uniq_list = sorted(list(set(li)))
    return {k: li.count(k) for k in uniq_list}


class Ichiban(UserList):
    """一番くじ確率計算
    description:
        args:
            remain: 現在余っている景品とその数
        self.data:
            インスタンスの実態を表すリスト型
        self.dict():
            dict typeとして返す。valuesは個数
        self.__repr__():
            インスタンスを打った時にdictのように表示する
            (注意！インスタンスの実態はself.data<-リスト型)
        self.kuji(*keys):
            景品を引く確率計算
        self.kakaku(default=0, **val):
            期待値計算
        self.hiku(key=None):
            くじ引きシミュレート
    usage:
        >>> poke = Ichiban(A=1, B=2, C=2)  # 残り景品の数を入力
        >>> poke.data  # 残り景品
        ['A', 'B', 'B', 'C', 'C']
        >>> poke  # 残り景品とその数(__repr__で返される)
        {'A': 1, 'B': 2, 'C': 2}
        >>> poke.dict()  # 残り景品とその数(dict型で返される)
        {'A': 1, 'B': 2, 'C': 2}
        >>> len(poke)  # 残り景品の全数
        5

        # kuji(): 確率計算
        >>> poke.kuji()  # それぞれの景品を引く確率
        {'A': 0.2, 'B': 0.4, 'C': 0.4}
        >>> poke.kuji('A', 'B')  # A賞またはB賞を引く確率
        0.6

        # kakaku(): 期待値計算
        >>> poke.kakaku(A=2000, B=1000, C=100)  # A賞2000円、B賞1000円、C賞100円の価値
        840.0
        >>> poke.kakaku(A=2000, default=100)  # A賞2000円、それ以外100円
        480.0
        >>> poke.kakaku(A=2000)  # defaultは指定しなければ0円
        400.0

        # hiku(): くじシミュレート
        >>> poke.hiku('A')  # A賞を引いて残数を1減らす(引数が無しでランダム選択)
        'A'
        >>> poke  # 一個しかないA賞を引いたのでAがなくなる
        {'B': 2, 'C': 2}
        >>> poke.kuji()  # A賞を引いた後の確率
        {'B': 0.5, 'C': 0.5}
        >>> len(poke)  # A賞を引いた後の全数は1減っている
        4
    """

    def __init__(self, **remain):
        super().__init__(self)
        self.data = [x for k, v in remain.items() for x in list(k * v)]

    def dict(self):
        """return dict type data"""
        return sumup_list(self)

    def __repr__(self):
        return sumup_list(self).__repr__()

    def copy(self):
        return self.__class__(**self.dict())

    def kuji(self, *keys):
        """景品を引く確率計算
        description:
            * 現在残っている景品を引く確率を計算する
            * 引数がなければ、それぞれの景品を引く確率をdictionaryとして返す
            * 引数があれば、引数の景品を引く確率の合計をfloatとして返す
        usage:
            >> poke = Ichiban(A=1, B=2, C=2)  # 残り景品の数を入力
            >> poke.kuji()  # それぞれの景品を引く確率
            {'A': 0.2, 'B': 0.4, 'C': 0.4}
            >> poke.kuji('A', 'B')  # A賞またはB賞を引く確率
            0.6
        """
        # float type
        if keys:
            return sum([self.count(k) for k in set(keys)]) / len(self)
        # dict type
        return {k: self.count(k) / len(self) for k in self}

    def kakaku(self, default=0, **val):
        """期待値計算
        descripton:
            景品の価格を引数に取り、[価格x確率]の合計値を返す
        usage:
            >> poke = Ichiban(A=1, B=2, C=2)  # 残り景品の数を入力
            >> poke.kakaku(A=2000, B=1000, C=100)  # A賞2000円、B賞1000円、C賞100円の価値
            840.0
            >> poke.kakaku(A=2000, default=100)  # A賞2000円、それ以外100円
            480.0
            >> poke.kakaku(A=2000)  # defaultは指定しなければ0円
            400.0
        """
        prob = self.kuji()
        # if `val` has same `prob` key then `val` or `default` value
        kakaku_dic = {k: val.get(k, default) for k in prob.keys()}
        # multiply kakaku_dic[key] * kitai_dic[key] for each
        kitai_dic = {k: prob[k] * kakaku_dic[k] for k in kakaku_dic.keys()}
        return sum(kitai_dic.values())

    def hiku(self, key=None):
        """くじ引きシミュレート
        description:
            * keyが指定されれば、keyの景品を引く
            * keyが指定がされなければ、ランダムに景品を引く
        usage:
            >> poke = Ichiban(A=1, B=2, C=2)  # 残り景品の数を入力
            >> poke.hiku('A')  # A賞を引いて残数を1減らす(引数が無しでランダム選択)
            'A'
            >> poke  # 一個しかないA賞を引いたのでAがなくなる
            {'B': 2, 'C': 2}
            >> poke.kuji()  # A賞を引いた後の確率
            {'B': 0.5, 'C': 0.5}
            >> len(poke)  # A賞を引いた後の全数は1減っている
            4
        """
        index = self.index(key if key else random.choice(self))
        return self.pop(index)


if __name__ == '__main__':
    import doctest
    doctest.testmod()
```

[Gitghub - ichiban](https://github.com/u1and0/ichiban)
GUI化予定
まだpythonインタプリタ上でしか動かない