Steve Brunton | Probability Bootcamp | 笔记 | 第一部分:概率导论 | Lecture 7 | 二项分布与多项分布

目录

    • 前言
    • [1. 引言](#1. 引言)
    • [2. 二项式系数](#2. 二项式系数)
    • [3. 定义多项分布](#3. 定义多项分布)
    • [4. 公式推导](#4. 公式推导)
    • [5. 结语](#5. 结语)
    • 结语
    • 参考

前言

学习 Steven Brunton 讲授的概率与统计入门概述视频,本篇文章记录第七讲:二项分布与多项分布,记录下个人学习笔记,和大家一起分享交流😄

videohttps://www.youtube.com/playlist?list=PLMrJAkhIeNNR3sNYvfgiKgcStwuPSts9V

1. 引言

我们一直在研究如何计算概率问题,比如:一副扑克牌中能组成同花顺的手牌有多少种可能,或者出现满堂红的手牌组合有多少种,归根结底,这需要计算特定事件在所有可能情况中出现的概率。

在所有扑克牌组合中,需要统计出同花、满堂红等特定牌型的出现次数,这本质上就是统计事件数量的问题 --- 比如计算所有可能的扑克手牌总数,或者统计包含单张 K 的手牌组合数量等。

我们总结出了一些非常实用的计算公式,现在我要写下一个重要公式,这个公式与二项式系数密切相关,之后我们会将其推广到多项式分布,这个公式在计算可能出现的扑克牌组合数量时非常实用,当我们从一副 52 张扑克牌中发出 5 张手牌时,其分布符合二项式分布。

如果我发两副扑克手牌,假设有两个玩家在玩,那么发两副或三副手牌分别由多少种发牌方式?这属于多项式分布的范畴,这套公式极其实用,是计算各类复杂概率问题的必备工具。

因此,我们能收集到的无序样本数量就是无序样本的总数,从总数为 n n n 的物件中抽取样本,那么 n n n 就是 52 张牌,我抽到的样本就是一手 5 张牌,无序样本的数量计算方式为:从总量 n n n 中无放回抽取,即当我发出第一张牌后,牌堆只剩 n − 1 n-1 n−1 张牌,发出第二张牌后,牌堆仅剩 50 张牌,因此,无放回的无序样本数量等于组合数 ( n r ) \binom{n}{r} (rn) ,记住 n C r nC_r nCr 是其简写形式,我们有时称之为 "从 n n n 中选取 r r r 个样本",这个公式的字面含义就是 " n n n 选 r r r ",记住,这个公式的计算方式是:

( n r ) = n ! ( n − r ) !   r ! \binom{n}{r} = \frac{n!}{(n-r)! \, r!} (rn)=(n−r)!r!n!

这就是计算公式,这就是我们计算扑克牌可能发牌组合数的方法,从一副 52 张的扑克牌中,不考虑发牌顺序且无放回抽取,总共可以发出多少种不同的 5 张牌组合?无论是先拿到 7 还是先拿到 9,最终组合结果都是相同的,这就是从 n n n 个元素中无序抽取 r r r 的样本的组合数,记作 ( n r ) \binom{n}{r} (rn) ,这个数值与二项式系数密切相关。

2. 二项式系数

我想展示这个是因为它非常有趣 --- 你可能在帕斯卡三角形和多项式展开系数的计算中见过它,若取 ( x + y ) 2 (x+y)^2 (x+y)2 或者更一般地考虑 ( x + y ) n (x+y)^n (x+y)n ,展开后将得到:

( x + y ) n = ∑ k = 0 n ( n k ) ( x k y n − k ) (x+y)^n = \sum_{k=0}^{n} \binom{n}{k} (x^ky^{n-k}) (x+y)n=k=0∑n(kn)(xkyn−k)

若干形如 x k y n − k x^ky^{n-k} xkyn−k 的项之和,求和符号从 k = 0 k=0 k=0 到 n n n ,这些单项式的系数将是 ( n k ) \binom{n}{k} (kn) ,这些就是二项式系数,之所以称为 "二项式",是因为我们实际上是将两个项相加后求幂,因此这个多项式就是所有这些单项式的总和,其中系数正是二项式系数,要计算这些系数,可以通过帕斯卡三角形来求解,想必大家对此早已熟悉。

现在观察系数,帕斯卡三角形的顶端数字是 1,因此 ( x + y ) 0 (x+y)^0 (x+y)0 的结果就是 1, ( x + y ) 1 (x+y)^1 (x+y)1 展开后, x x x 和 y y y 的系数都是 1,帕斯卡三角形中的这些数字就是对应的系数, ( x + y ) 2 (x+y)^2 (x+y)2 展开后的系数是 1、2、1,展开式是 x 2 + 2 x y + y 2 x^2 + 2xy + y^2 x2+2xy+y2 , ( x + y ) 3 (x+y)^3 (x+y)3 展开后得到 x 3 + 3 x 2 y + 3 x y 2 + y 3 x^3 + 3x^2y + 3xy^2 + y^3 x3+3x2y+3xy2+y3 ,注意这个三角形中,每个新项都是它上方两项之和, ( x + y ) 4 (x+y)^4 (x+y)4 的展开系数为 1、4、6、4、1, ( x + y ) 5 (x+y)^5 (x+y)5 的展开系数为 1、5、10、10、5、1。

因此,我们可以直接从帕斯卡三角形中读出这个多项式展开的系数,构造起来非常简单,这个巧妙思路与概率论中频繁出现的 ( n k ) \binom{n}{k} (kn) 二项式系数密切相关,你会发现这种对称结构非常优美,蕴含着精妙的数学美感,这确实令人着迷。

但你会发现,当 n → ∞ n \rightarrow \infty n→∞ 时,比如计算 ( x + y ) 100 (x+y)^{100} (x+y)100 的展开式,这些系数的分布将逐渐收敛于正态分布,这种趋势在此已初现端倪,可以看出这个分布逐渐趋于正态分布。

事实上,你或许会想在计算机上编写代码实现这个现象,尝试取较大的 n n n 值构建三角形,逐行绘制每一层的分布,最终会收敛到优美的高斯分布(即正态分布)。

Python 示例代码如下:

python 复制代码
"""
帕斯卡三角形(二项式系数)逐渐趋近正态分布的可视化。

数学关系:
    C(n, k) / 2^n = P(X = k),  X ~ Binomial(n, 1/2)

当 n 足够大时,将横坐标标准化:
    z = (k - n/2) / sqrt(n/4)

标准化后的二项分布逐渐趋近标准正态分布 N(0, 1)。
"""

import argparse
import math
from typing import Iterable

import matplotlib.pyplot as plt
import numpy as np
from matplotlib.animation import FuncAnimation


def pascal_row(n: int) -> list[int]:
    """返回帕斯卡三角形第 n 行,即 (x+y)^n 的全部二项式系数。"""
    if n < 0:
        raise ValueError("n 必须是非负整数。")
    return [math.comb(n, k) for k in range(n + 1)]


def binomial_probability(n: int) -> tuple[np.ndarray, np.ndarray]:
    """
    返回 X ~ Binomial(n, 1/2) 的横坐标 k 和概率 P(X=k)。

    使用递推方式计算概率,避免直接把超大的组合数转换为浮点数:
        p_0 = 2^(-n)
        p_(k+1) = p_k * (n-k)/(k+1)
    """
    if n < 0:
        raise ValueError("n 必须是非负整数。")

    k = np.arange(n + 1, dtype=float)

    if n == 0:
        return k, np.array([1.0])

    probabilities = np.empty(n + 1, dtype=float)
    probabilities[0] = math.exp(-n * math.log(2.0))

    for i in range(n):
        probabilities[i + 1] = probabilities[i] * (n - i) / (i + 1)

    # 修正可能出现的微小浮点误差。
    probabilities /= probabilities.sum()
    return k, probabilities


def standard_normal_pdf(z: np.ndarray) -> np.ndarray:
    """标准正态分布 N(0,1) 的概率密度函数。"""
    return np.exp(-0.5 * z**2) / math.sqrt(2.0 * math.pi)


def standardized_binomial(n: int) -> tuple[np.ndarray, np.ndarray]:
    """
    将 Binomial(n, 1/2) 标准化,并把离散概率转换为近似密度。

    z = (k - μ) / σ
    density ≈ P(X=k) / Δz = P(X=k) * σ
    """
    k, probability = binomial_probability(n)

    if n == 0:
        return np.array([0.0]), np.array([1.0])

    mean = n / 2.0
    std = math.sqrt(n) / 2.0
    z = (k - mean) / std
    density = probability * std
    return z, density


def print_pascal_rows(max_n: int) -> None:
    """在终端打印从第 0 行到第 max_n 行的帕斯卡三角形。"""
    if max_n < 0:
        raise ValueError("max_n 必须是非负整数。")

    rows = [" ".join(map(str, pascal_row(n))) for n in range(max_n + 1)]
    width = len(rows[-1])

    for n, row in enumerate(rows):
        print(f"n={n:>2}: {row.center(width)}")


def plot_selected_rows(rows: Iterable[int]) -> None:
    """绘制若干指定行,并与标准正态分布进行比较。"""
    rows = sorted(set(rows))
    if not rows or rows[0] < 1:
        raise ValueError("用于标准化绘图的 n 必须是正整数。")

    figure, axis = plt.subplots(figsize=(11, 7))

    z_gaussian = np.linspace(-4.5, 4.5, 1000)
    axis.plot(
        z_gaussian,
        standard_normal_pdf(z_gaussian),
        linewidth=3,
        label="Standard normal distribution",
    )

    for n in rows:
        z, density = standardized_binomial(n)
        axis.plot(
            z,
            density,
            marker="o" if n <= 20 else None,
            markersize=4,
            linewidth=1.8,
            label=f"Pascal row n={n}",
        )

    axis.set_title("Pascal Triangle Coefficients Converging to a Gaussian Distribution")
    axis.set_xlabel(r"Standardized position  $z=(k-n/2)/\sqrt{n/4}$")
    axis.set_ylabel("Probability density")
    axis.set_xlim(-4.5, 4.5)
    axis.grid(alpha=0.3)
    axis.legend()
    figure.tight_layout(rect=[0, 0, 1, 0.94])
    plt.show()


def plot_original_distribution(n: int) -> None:
    """以原始横坐标 k 绘制第 n 行系数归一化后的二项分布。"""
    k, probability = binomial_probability(n)

    figure, axis = plt.subplots(figsize=(11, 6))
    axis.bar(k, probability, width=0.85)

    mean = n / 2.0
    std = math.sqrt(n) / 2.0
    x = np.linspace(max(0.0, mean - 4.5 * std), min(float(n), mean + 4.5 * std), 1000)

    gaussian_approximation = (
        np.exp(-0.5 * ((x - mean) / std) ** 2)
        / (std * math.sqrt(2.0 * math.pi))
    )
    axis.plot(x, gaussian_approximation, linewidth=3, label="Gaussian approximation")

    axis.set_title(
        rf"Normalized Coefficients of $(x+y)^{{{n}}}$: "
        rf"$P(X=k)=\binom{{{n}}}{{k}}/2^{{{n}}}$"
    )
    axis.set_xlabel("k")
    axis.set_ylabel("Probability")
    axis.grid(axis="y", alpha=0.3)
    axis.legend()
    figure.tight_layout()
    plt.show()


def animate_convergence(
    max_n: int = 100,
    interval_ms: int = 100,
    save_path: str | None = None,
) -> None:
    """逐行绘制 n=1,...,max_n,动态展示其向标准正态分布收敛。"""
    if max_n < 1:
        raise ValueError("max_n 必须至少为 1。")

    figure, axis = plt.subplots(figsize=(11, 7))
    z_gaussian = np.linspace(-4.5, 4.5, 1000)

    axis.plot(
        z_gaussian,
        standard_normal_pdf(z_gaussian),
        linewidth=3,
        label="Standard normal distribution",
    )
    binomial_line, = axis.plot([], [], linewidth=2, marker="o", markersize=3)
    title = axis.set_title(
        r"Pascal row $n=1$: "
        r"$\binom{n}{k}/2^n \longrightarrow N(0,1)$",
        pad=12,
    )

    axis.set_xlabel(r"Standardized position  $z=(k-n/2)/\sqrt{n/4}$")
    axis.set_ylabel("Probability density")
    axis.set_xlim(-4.5, 4.5)
    axis.set_ylim(0.0, 0.45)
    axis.grid(alpha=0.3)
    axis.legend()

    def update(n: int):
        z, density = standardized_binomial(n)
        binomial_line.set_data(z, density)
        binomial_line.set_marker("o" if n <= 30 else "")
        title.set_text(
            rf"Pascal row $n={n}$: "
            rf"$\binom{{n}}{{k}}/2^n \longrightarrow N(0,1)$"
        )
        return binomial_line, title

    animation = FuncAnimation(
        figure,
        update,
        frames=range(1, max_n + 1),
        interval=interval_ms,
        blit=False,
        repeat=True,
    )

    figure.tight_layout()

    if save_path:
        try:
            animation.save(save_path, writer="pillow", fps=max(1, 1000 // interval_ms))
            print(f"动画已保存到:{save_path}")
        except Exception as exc:
            raise RuntimeError(
                "保存 GIF 失败,请先安装 Pillow:pip install pillow"
            ) from exc
    else:
        plt.show()


def parse_rows(text: str) -> list[int]:
    """把 '2,5,10,30,100' 转换为整数列表。"""
    try:
        rows = [int(item.strip()) for item in text.split(",") if item.strip()]
    except ValueError as exc:
        raise argparse.ArgumentTypeError("rows 必须是用逗号分隔的整数。") from exc

    if not rows or any(n < 1 for n in rows):
        raise argparse.ArgumentTypeError("rows 中的每个 n 都必须是正整数。")
    return rows


def main() -> None:
    parser = argparse.ArgumentParser(
        description="可视化帕斯卡三角形系数向正态分布收敛的现象。"
    )
    parser.add_argument(
        "--mode",
        choices=["triangle", "selected", "original", "animate"],
        default="selected",
        help=(
            "triangle: 打印帕斯卡三角形;"
            "selected: 比较若干行;"
            "original: 绘制某一行的原始分布;"
            "animate: 动态逐行绘制。"
        ),
    )
    parser.add_argument(
        "--n",
        type=int,
        default=100,
        help="用于 triangle、original 或 animate 模式的最大阶数,默认 100。",
    )
    parser.add_argument(
        "--rows",
        type=parse_rows,
        default=[2, 5, 10, 30, 100],
        help="selected 模式中的行号,例如:--rows 2,5,10,30,100",
    )
    parser.add_argument(
        "--interval",
        type=int,
        default=100,
        help="动画每帧间隔,单位毫秒,默认 100。",
    )
    parser.add_argument(
        "--save",
        type=str,
        default=None,
        help="动画保存路径,例如:--save pascal_to_gaussian.gif",
    )
    args = parser.parse_args()

    if args.n < 0:
        parser.error("--n 不能为负数。")
    if args.interval <= 0:
        parser.error("--interval 必须大于 0。")

    if args.mode == "triangle":
        print_pascal_rows(args.n)
    elif args.mode == "selected":
        plot_selected_rows(args.rows)
    elif args.mode == "original":
        if args.n < 1:
            parser.error("original 模式要求 --n 至少为 1。")
        plot_original_distribution(args.n)
    elif args.mode == "animate":
        if args.n < 1:
            parser.error("animate 模式要求 --n 至少为 1。")
        animate_convergence(
            max_n=args.n,
            interval_ms=args.interval,
            save_path=args.save,
        )


if __name__ == "__main__":
    main()

执行指令如下:

shell 复制代码
python .\pascal_gaussian.py --mode animate --n 100 --save pascal_to_gaussian.gif

生成的动态图如下所示:

需要注意:并不是帕斯卡三角形中的原始系数直接收敛到正态分布,而是归一化后的系数:

P ( X = k ) = ( n k ) 2 n P(X=k) = \frac{\binom{n}{k}}{2^n} P(X=k)=2n(kn)

构成参数为 n n n 和 p = 1 2 p=\frac{1}{2} p=21 的二项分布。进一步令

z = k − n 2 n 4 , z = \frac{k-\frac{n}{2}}{\sqrt{\frac{n}{4}}}, z=4n k−2n,

标准化后的分布会随着 n n n 增大趋近标准正态分布 N ( 0 , 1 ) N(0,1) N(0,1) 。

还记得《价格猜猜猜》节目里的 Plinko 游戏吗?把球从顶部落下,经过格栅弹跳后最终掉进不同的收集槽里,这种装置有时被称为高尔顿板,德语中称作 Galtonbrett,若将大量珠子撒在布满钉子的格板上,经过弹跳后,它们会逐渐下落并最终形成高斯分布。

帕斯卡三角形正是快速计算多项式展开系数的工具,归根结底,这些系数就是无处不在的二项式系数 --- 组合数 C n k C_n^k Cnk ,我们在概率计算中经常用到它们。

所以如果我想计算从一副 52 张牌中能抽出多少种五张扑克组合,直接用组合数 C 52 5 C_{52}^{5} C525 就行了,对吧?计算起来非常简单。

而多项式分布则是将其推广到更高维分布的方法,适用于更复杂的纸牌游戏场景,假设现在我要发两副扑克牌,我和朋友一起玩,现在有两副五张牌的手牌,这种情况有多少种可能?最终得到的结果就是多项式分布,其计算过程也相当简便。

3. 定义多项分布

下面我们来推导下,那么有 n n n 个物体,划分为 r r r 个类别,其规模分别为 n 1 , n 2 , ... , n r n_1,n_2,\ldots,n_r n1,n2,...,nr ,比如我处理两副牌时,可能会各发五张牌,类似这样的情况,而且顺序不重要。将 n n n 个对象划分为 r r r 个类别,且不考虑顺序,那么样本总数就是下面这种组合数:

( n n 1 n 2 ... n r ) \binom{n}{n_1n_2 \ldots n_r} (n1n2...nrn)

接下来我将说明这个符号的定义, n 1 n 2 ... n r n_1n_2 \ldots n_r n1n2...nr 并非乘积关系,不是 n 1 × n 2 × ... × n r n_1 \times n_2 \times \ldots \times n_r n1×n2×...×nr 的形式,这并非从 n n n 个元素中选取这个大数值的组合,这是一种新的计数记号,明白吗?

其定义方式如下:

( n n 1 n 2 ... n r ) = n ! n 1 ! n 2 ! ⋯ n r ! \binom{n}{n_1n_2 \ldots n_r} = \frac{n!}{n_1 \ ! \ n_2! \ \cdots \ n_r!} (n1n2...nrn)=n1 ! n2! ⋯ nr!n!

还有一点需要补充说明,这里有个前提条件:这些 n i n_i ni 的总和必须等于 n n n ,因此,整副 52 张牌会被分成 r r r 手牌,想象一下你在玩黑桃牌戏:整副牌分给四位玩家后,会形成四手牌,所有手牌相加正好是 52 张,也就是说,这里假设 n 1 + n 2 + ⋯ + n r = n n_1 + n_2 + \cdots + n_r = n n1+n2+⋯+nr=n ,对吧?

这与计算从 52 张牌中发出 5 张手牌的组合数略有不同,但原理相通,同样的原理也可用于计算从 52 张牌中抽取 5 张手牌的组合数,这里假设将整个集合 n n n 划分为这 r r r 个组别,其计算公式如上所示。

4. 公式推导

通过基本原理推导这个公式的过程其实很有帮助,因为这能让你更熟练地掌握这类计算,所以这个公式并非凭空而来,完全可以通过列举所有可能性地方式来推导出这个公式,接下来我们来看看这个公式的推导过程。

这个概率的推导依据是:可能发生的事件总数等于从 n n n 个元素中选取 n 1 , n 2 , ... , n r n_1,n_2,\ldots, n_r n1,n2,...,nr 个元素的组合数。首先分配 n 1 n_1 n1 张牌,有多少种方法可以做到这一点?这就是我们前面提到的 ( n n 1 ) \binom{n}{n_1} (n1n) ,现在我要分配第二组,这就是 ( n − n 1 n 2 ) \binom{n-n_1}{n_2} (n2n−n1) ,现在剩下 n − n 1 − n 2 n-n_1-n_2 n−n1−n2 张牌,我要用这些来分配 n 3 n_3 n3 ,所以是 ( n − n 1 − n 2 n 3 ) \binom{n-n_1-n_2}{n_3} (n3n−n1−n2) ,依次类推,直到最后一项,即:

( n − n 1 − n 2 − ⋯ − n r n r ) \binom{n-n_1-n_2-\cdots -n_r}{n_r} (nrn−n1−n2−⋯−nr)

其中每一项的简答排列都可以计算出来,这些本质上都是二项系数:从总数 n n n 种抽取 n 1 n_1 n1 张牌的组合数,以此类推,我们可以列出所有这些组合数的计算公式,然后将它们相乘,你会发现这些项会以非常巧妙地方式相互抵消,最终得到下面这个简洁的公式:

( n n 1 n 2 ... n r ) = n ! n 1 ! n 2 ! ⋯ n r ! \binom{n}{n_1n_2 \ldots n_r} = \frac{n!}{n_1 \ ! \ n_2! \ \cdots \ n_r!} (n1n2...nrn)=n1 ! n2! ⋯ nr!n!

5. 结语

OK,让我们从整体视角来看,这就是所谓的多项系数,利用这个公式可以计算多种概率问题,在工厂生产零件时,若将合格品与不合格品分为两类,分别记为 n 1 n_1 n1 和 n 2 n_2 n2 ,这种分类情况就符合多项式分布。

通过这个多项式分布,可以计算在采样中出现特定数量不合格品的概率,用这个方法可以计算特定扑克牌型的概率,比如同花顺、四条等组合出现的可能性,能计算的情况非常丰富。

假设总人数为偶数,设为 2 n 2n 2n 人,有多少种方法可以将这些人两两配对?这些人能配成多少对组合?这是道课后习题,你需要针对不同的 n n n 值进行计算。

那如果是委员会决策呢?你正在组建一个分委员会,现有七位成员,需组建三个分委员会:两人组、三人组和两人组,从七人中选出这些小组委员会共有多少种方式?

这类问题可以通过多项式分布来计算,它是二项式分布的推广形式,这些二项式系数实在精妙绝伦,它是多项式展开的系数,源自帕斯卡三角形,当 n n n 足够大时,最终会收敛于正态分布。

OK,以上就是本讲的全部内容。

我们将在后续课程中演示如何从二项分布推导出正态分布,但我想让大家先对这些概念有个直观认识,因为借助基础的 ( n r ) \binom{n}{r} (rn) 组合数及其多项式推广,我们就能计算各种有趣事件的概率 --- 比如纸牌游戏中的各类情形。

结语

本讲从二项式系数 ( n r ) \binom{n}{r} (rn) 出发,串联起帕斯卡三角形、多项式展开与高尔顿板,直观展示了归一化后的二项式系数随 n n n 增大逐渐收敛于高斯分布的过程 --- 这为中心极限定理埋下了伏笔。在此基础上,我们将二项式系数推广为多项分布:

( n n 1 n 2 ... n r ) = n ! n 1 ! n 2 ! ⋯ n r ! \binom{n}{n_1 n_2 \ldots n_r} = \frac{n!}{n_1! \ n_2! \ \cdots \ n_r!} (n1n2...nrn)=n1! n2! ⋯ nr!n!

这一公式是解决 "将 n n n 个对象划分为 r r r 个无序组别" 的通用计数工具,可广泛应用于扑克牌型、委员会分组、质量抽检等场景。掌握从二项到多项的推广逻辑,就掌握了一套处理复杂组合计数问题的核心方法论 🤗。

参考

相关推荐
xcLeigh6 个月前
Python入门:Python3 statistics模块全面学习教程
开发语言·python·学习·模块·python3·statistics
陈晨辰熟稳重7 个月前
20260113-np.random.multinomial 与 torch.multinomial
pytorch·python·numpy·采样·multinomial
头发没了还会再长7 个月前
Basic statistics - 12. One-way ANOVA Basics
统计学·statistics
头发没了还会再长7 个月前
Basic statistics - 10. The unpaired t-test | Independent samples t-test
统计学·statistics
头发没了还会再长7 个月前
Basic statistics -11. The paired t-test | explained with a sample example
统计学·statistics
头发没了还会再长8 个月前
Basic statistics - 07. The degrees of freedom - explained with a simple example
统计学·statistics
头发没了还会再长8 个月前
Basic statistics - 05. Confidence intervals
statistics
头发没了还会再长8 个月前
Basic statistics - 02. The Normal distribution
统计学·statistics