假设检验 假设检验(hypothesis testing)提供了一套严谨的框架,用来判断观测到的效应是真实的还是仅仅出于偶然。本文件涵盖原假设与备择假设、p 值、显著性水平、t 检验、卡方检验、ANOVA 以及第一类/第二类错误,这正是 A/B 测试、模型比较和科学研究中所使用的同一套逻辑。 统计学不仅仅是描述数据。很多时候你需要做出一个决定:新药有效吗?某个算法比另一个快吗?平均值变了吗?假设检验给了你一套用数据来回答这些问题的结构化框架。 思路很简单:先假设什么都没变(即「原假设」),再看数据是否极端到让这个假设难以令人置信。 原假设(null hypothesis,$H0$)是默认的主张,通常是「没有效应」或「没有差异」的陈述。
假设检验(hypothesis testing)提供了一套严谨的框架,用来判断观测到的效应是真实的还是仅仅出于偶然。本文件涵盖原假设与备择假设、p 值、显著性水平、t 检验、卡方检验、ANOVA 以及第一类/第二类错误,这正是 A/B 测试、模型比较和科学研究中所使用的同一套逻辑。
统计学不仅仅是描述数据。很多时候你需要做出一个决定:新药有效吗?某个算法比另一个快吗?平均值变了吗?假设检验给了你一套用数据来回答这些问题的结构化框架。
思路很简单:先假设什么都没变(即「原假设」),再看数据是否极端到让这个假设难以令人置信。
**原假设(null hypothesis,H_0)**是默认的主张,通常是「没有效应」或「没有差异」的陈述。例如:「平均配送时间仍然是 30 分钟」或「新模型并不比旧模型更好」。
**备择假设(alternative hypothesis,H_1 或 H_a)**是你怀疑真正成立的那个主张:「平均配送时间变了」或「新模型更好」。
你永远不会直接证明 H_1。相反,你问的是:假如 H_0 为真,我看到这么极端的数据的概率有多大?如果这个概率非常小,你就拒绝 H_0、转而接受 H_1。
**检验统计量(test statistic)**是一个单一的数字,概括了你的样本结果与 H_0 所预言的相差多远。不同的检验用不同的公式,但逻辑始终一样:度量观测值与期望值之间的距离。
**p 值(p-value)**是在假设 H_0 为真的前提下,观测到至少和你的一样极端的检验统计量的概率。p 值小意味着数据在 H_0 之下令人意外。
**显著性水平(significance level,\alpha)**是你看数据之前就定好的阈值。如果 p \le \alpha,你就拒绝 H_0。常用的选择是 \alpha = 0.05(5%)和 \alpha = 0.01(1%)。
阴影的尾部就是拒绝域。如果你的检验统计量落在那里,说明数据在 H_0 之下足够令人意外,于是你拒绝它。绿色面积展示的是某个具体检验统计量对应的 p 值。
完整步骤如下:
例题:某工厂声称他们生产的螺栓平均长度是 10 cm。你测量了 36 颗螺栓,得到样本均值 10.3 cm。已知总体标准差是 0.9 cm。是否有证据表明均值变了?
H_0:\mu = 10,H_1:\mu \neq 10,\alpha = 0.05
检验统计量(用 z 检验,因为 \sigma 已知且 n 较大):
对于 \alpha = 0.05 的双尾检验,临界值是 \pm 1.96。我们的 z = 2.0 > 1.96,所以拒绝 H_0。p 值约为 0.046,小于 0.05。
结论:有统计学上显著的证据表明螺栓的平均长度偏离了 10 cm。
**单尾检验(one-tailed test)**只检查某一个特定方向的效应(H_1:\mu > 10 或 \mu < 10)。整个 \alpha 都落在一条尾巴里,使得在该方向上更容易拒绝 H_0,但完全无法察觉反方向的效应。
**双尾检验(two-tailed test)**检查是否存在任何差异(H_1:\mu \neq 10)。\alpha 被平分到两条尾巴(每条 \alpha/2)。这更保守,但能捕捉两个方向的效应。
即便流程很好,错误也会发生。错误恰好有两种:
第一类错误(Type I Error)(假阳性,false positive):H_0 实际为真时你却拒绝了它。发生这种情况的概率就是 \alpha,你可以通过选择显著性水平来控制它。就像没着火时火警却响了。
第二类错误(Type II Error)(假阴性,false negative):H_0 实际为假时你却没能拒绝它。发生这种情况的概率是 \beta。就像真的着火了火警却一声不响。
**统计功效(power)**是 1 - \beta,即正确拒绝一个为假的 H_0 的概率。功效越高,说明你越擅长发现真实效应。功效在以下情况下会提升:
第一类错误与第二类错误之间存在张力。降低 \alpha(对假阳性更谨慎)会增加 \beta(更多假阴性)。在样本量固定时,你无法同时把两者都压到最低。
**参数检验(parametric tests)**假设数据服从某个特定的分布(通常是正态分布)。当这些假设成立时,它们更有功效。
z 检验(Z-test):当 \sigma 已知且 n 较大(n \ge 30)时,把样本均值与一个已知值比较。检验统计量:
t 分布有一个参数叫做自由度(degrees of freedom,df = n - 1)。随着 df 增大,t 分布会逼近正态分布。
t 检验有几种变体:
方差分析(ANOVA,Analysis of Variance):检验三个或更多组的均值是否相等。与其跑多次 t 检验(那样会抬高第一类错误率),ANOVA 只做一次检验,方法是把组间方差与组内方差进行比较。
一个大的 F 比值意味着组与组之间的差异超出了单凭随机波动所能预期的程度。
**非参数检验(non-parametric tests)**对数据分布做的假设更少。它们作用于排名而不是原始值,因此对离群点和非正态性都很稳健。
卡方检验(chi-square test,\chi^2):检验观测频率是否与期望频率相符,用于分类数据。例如:红、蓝、绿汽车的比例是否符合厂商声称的比例?
曼-惠特尼 U 检验(Mann-Whitney U test):独立双样本 t 检验的非参数替代。它通过比较排名来检验某一组的取值是否整体上大于另一组。
威尔科克森符号秩检验(Wilcoxon signed-rank test):配对 t 检验的非参数替代。通过考察差值的大小和方向来比较配对观测。
克鲁斯卡尔-沃利斯检验(Kruskal-Wallis test):单因素 ANOVA 的非参数替代。通过比较所有组的排名来检验多个组是否来自同一分布。
**拟合优度检验(goodness-of-fit tests)**检查你的数据是否服从某个特定的理论分布。卡方拟合优度检验把观测的箱频数与在假设分布下的期望频数进行比较。
**正态性检验(normality tests)**专门检查数据是否服从正态分布。常用的有 Shapiro-Wilk 检验(对小样本很有功效)和 Kolmogorov-Smirnov 检验(把样本 CDF 与理论 CDF 进行比较)。
在 ML 中,假设检验出现在你比较模型性能的时候。如果模型 A 达到 92% 的准确率、模型 B 达到 91%,这个差异是真实的还是只是噪声?对交叉验证得分做一次配对 t 检验就能回答这个问题。
import jax.numpy as jnp x_bar = 10.3 # 样本均值 mu_0 = 10.0 # 原假设的值 sigma = 0.9 # 已知的总体标准差 n = 36 # 样本量 alpha = 0.05 # 检验统计量 z = (x_bar - mu_0) / (sigma / jnp.sqrt(n)) print(f"z = {z:.4f}") # 用正态 CDF 的近似计算 p 值(双尾) # 对于 |z| = 2.0,p ≈ 0.0456 from jax.scipy.stats import norm p_value = 2 * (1 - norm.cdf(jnp.abs(z))) print(f"p-value = {p_value:.4f}") print(f"Reject H₀? {p_value <= alpha}")
import jax import jax.numpy as jnp key = jax.random.PRNGKey(0) mu_0 = 50.0 sigma = 10.0 n = 30 alpha = 0.05 n_experiments = 10_000 rejections = 0 for i in range(n_experiments): key, subkey = jax.random.split(key) sample = mu_0 + sigma * jax.random.normal(subkey, shape=(n,)) z = (sample.mean() - mu_0) / (sigma / jnp.sqrt(n)) p_value = 2 * (1 - __import__("jax").scipy.stats.norm.cdf(jnp.abs(z))) if p_value <= alpha: rejections += 1 print(f"Rejection rate: {rejections/n_experiments:.4f}") print(f"Expected (α): {alpha}")
import jax import jax.numpy as jnp key = jax.random.PRNGKey(99) k1, k2 = jax.random.split(key) group_a = jax.random.normal(k1, shape=(25,)) * 5 + 100 group_b = jax.random.normal(k2, shape=(25,)) * 5 + 103 # 均值略高 # 双样本 t 检验(假设方差相等) n_a, n_b = len(group_a), len(group_b) mean_a, mean_b = group_a.mean(), group_b.mean() pooled_var = ((n_a - 1) * group_a.var() + (n_b - 1) * group_b.var()) / (n_a + n_b - 2) se = jnp.sqrt(pooled_var * (1/n_a + 1/n_b)) t_stat = (mean_a - mean_b) / se print(f"T-test statistic: {t_stat:.4f}") # 曼-惠特尼:统计 group_a 的值小于 group_b 的值的次数 u_stat = jnp.sum(group_a[:, None] < group_b[None, :]) print(f"Mann-Whitney U: {u_stat}") print(f"\nGroup A mean: {mean_a:.2f}, Group B mean: {mean_b:.2f}")