Think Bayes Note 8: 骰子点数之和

1. 点数之和

  在《龙与地下城》角色扮演游戏中,玩家通过投掷 3 个 6 面骰子来决定自己角色的属性。求投掷 3 个 6 面骰子点数之和的分布。

  这个问题可以从通过模拟或枚举的方法来计算。

1.1. 模拟

  可以通过多次模拟投掷 3 个骰子的过程,并汇总结果,得到结果的分布。定义代表单个骰子的 Die 类如下:

class Die(Pmf):
    def __init__(self, sides):
        super().__init__()
        for x in range(1, sides + 1):
            self.set(x, 1)
        self.normalize()

其中,__init__() 的参数 sides 为骰子的面数,初始化时,设置骰子每个面出现的概率相等。模拟代码为:

d6 = Die(6)
dice = [d6] * 3
pmf_sum_sim = sample_sum(dice, 1000, 'sim')

d6 是一个 6 面骰子, dice 是包含 3 个 d6 的列表;sample_sum() 用于从 dice 中进行采样,代码为:

def random_sum(dists):
    return sum(dist.random() for dist in dists)

def sample_sum(dists, n, name=''):
    values = [random_sum(dists) for _ in range(n)]
    pmf = Pmf(name=name)
    for value in values:
        pmf.incr(value)
    pmf.normalize()
    return pmf

random_sum() 的参数 dists 是一个分布的列表,它依调用每个分布的 random() 方法并对结果求和,这里相当于计算投掷 3 个 6 面骰子得到的点数之和。sample_sum() 则会对 dists 进行 n 次试验,得到一个结果列表,生成一个 Pmf。

1.2. 枚举

  除了像上面一样通过模拟投掷 3 个 6 面骰子的过程来得到骰子点数和的分布,还可以枚举所有可能的结果并计算其概率。代码如下:

pmf_sum_exact = d6 + d6 + d6
pmf_sum_exact.name = 'exact'

Pmf 重载了加号操作符如下:

class Pmf(DfWrapper):
    # ...
    def __add__(self, other):
        pmf = Pmf()
        for v1, p1 in self.iter_items():
            for v2, p2 in other.iter_items():
                pmf.incr(v1 + v2, p1 * p2)
        return pmf
    # ...

__add__() 会计算自身和 other 两个 Pmf 相加,

使用加号计算 a 和 b 两个 Pmf 相加时,会得到一个新的 Pmf,该 Pmf 中包含 a 和 b 中的值相加的所有可能结果,以及对应的概率。

  绘制模拟和枚举的结果:

pmf_sum_sim.plot_with([pmf_sum_exact])

图像为:

2.最大化

  假设我们使用这 3 个 6 面骰子连续投掷了 6 轮,记下每一轮中三个骰子点数之和,其最大值的分布是怎样的呢?这个问题依旧可以从模拟和枚举的角度来计算。

2.1. 模拟

  模拟的代码如下:

rolls = [pmf_sum_exact] * 6
pmf_max_sim = sample_max(rolls, 1000, 'sim')

这里 rolls 为 6 个 pmf_sum_exact 的列表, pmf_sum_exact 是前面得到的投掷 3 个 6 面骰子的和的分布。sample_max()rolls 中采样最大值,代码为:

def random_max(dists):
    return max(dist.random() for dist in dists)

def sample_max(dists, n, name=''):
    values = [random_max(dists) for _ in range(n)]
    # print(values)
    pmf = Pmf(name=name)
    for value in values:
        pmf.incr(value)
    pmf.normalize()
    return pmf

这里 sample_max()random_max() 与前面的 sample_sum()random_sum() 类似,只是将用到 sum 的地方换成了 max

2.2. 枚举

  定义 pmf_max() 如下:

def pmf_max(pmf_1, pmf_2, name=''):
    name = name if name else pmf_1.name
    pmf = Pmf(name=name)
    for v1, p1 in pmf_1.iter_items():
        for v2, p2 in pmf_2.iter_items():
            pmf.incr(max(v1, v2), p1 * p2)
    return pmf

pmf_max() 用于求两个 Pmf 中最大值得概率,它会遍历 pmf_1pmf_2 中的所有的条目,取较大的值,并更新其概率。

  枚举的过程如下:

pmf_max_exact = pmf_sum_exact.copy(name='exact')
for i in range(5):
    pmf_max_exact = pmf_max(pmf_max_exact, pmf_sum_exact)

这里先复制了一份 pmf_sum_exact 做为 pmf_max_exact,然后连续 5 次调用 pmf_max(pmf_max_exact, pmf_sum_exact),即得到投掷 6 轮后最大值的分布。

  绘制模拟和枚举的结果:

pmf_max_sim.plot_with([pmf_max_exact])

图像为:

2.3. 使用 CDF 计算最大值的分布

  前面模拟和枚举的方法都涉及大量迭代,计算速度较慢。可以使用 CDF 来加速计算。由 CDF 的定义,有:

\begin{equation}
CDF(x) = P(X \leq x)
\end{equation}

  设 $X$ 的 CDF 为 $CDF_X$,$Y$ 的 CDF 为 $CDF_Y$,现从 $X$ 中选出一个值 $x$、从 $Y$ 中选出一个值 $y$,计算最大值 $z = max(x, y)$,对于一个任意值 $z_0$,若要 $z \leq z_0$,则要求 $x \leq z_0$ 且 $y \leq a$。如果 $X$ 和 $Y$ 是独立的,则有:

\begin{equation}
CDF_Z(z) = CDF_X(z)CDF_Y(z)
\end{equation}

  如果从 $CDF_X$ 中选出 $k$ 个值 $x_1, x_2,…,x_k$,则 $z = max(x_1, x_2,…,x_k)$ 的 CDF 为:

\begin{equation}
CDF_Z(z) = CDF_X(z)^k
\end{equation}

  据此可以在 Cdf 类中实现 max() 方法如下:

class Cdf(DfWrapper):
    # ...
    def max(self, k):
        cdf = self.copy()
        cdf.d.prob = self.d.prob ** k
        return cdf
    # ...

该方法会计算重复进行 $k$ 次试验所得结果的最大值的 CDF。

  回到使用这 3 个 6 面骰子投掷 6 轮求最大值分布的问题,代码如下:

cdf_sum_exact = pmf_sum_exact.make_cdf()
cdf_best_attr = cdf_sum_exact.max(6)
pmf_best_attr = cdf_best_attr.make_pmf()
pmf_best_attr.plot()

图像为:

3. 混合分布

  在前面的问题中,我们只使用了 6 面骰子,假设我们有一盒骰子,其中有:

  • 2 个 4 面骰子
  • 3 个 6 面骰子
  • 2 个 8 面骰子
  • 1 个 12 面骰子
  • 1 个 20 面骰子

  现在从盒子中随机选择一个投掷,得到的点数的分布是怎样的呢?

  可以使用如下代码构造一个 Pmf,包含各种骰子的 Pmf 以及选中它的概率:

pmf_dice = Pmf()

pmf_dice.set(Die(4), 2)
pmf_dice.set(Die(6), 3)
pmf_dice.set(Die(8), 2)
pmf_dice.set(Die(12), 1)
pmf_dice.set(Die(20), 1)
pmf_dice.normalize()

  定义 make_mixture() 用于计算混合概率:

def make_mixture(meta_pmf, name='mix'):
    mix = Pmf(name=name)
    for pmf, p1 in meta_pmf.iter_items():
        for x, p2 in pmf.iter_items():
            mix.incr(x, p1 * p2)
    return mix

  从盒子中随机选择一个投掷,得到的点数的分布为:

mix = make_mixture(pmf_dice)
mix.plot_bar()

图像为: