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_1
和 pmf_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()
图像为: