推断统计¶
从样本推断总体——假设检验、置信区间、p 值
🎯 核心问题¶
你在电商网站做了改版,1000 个用户中改版后的转化率从 10% 升到了 11%。
问题是:这是真正的提升,还是随机波动?
推断统计回答的就是这个问题——量化为"随机波动"的概率。
📐 假设检验框架¶
五步法¶
1. 提出假设 → H₀: 没有效果 H₁: 有效果
2. 选择检验 → t检验 / z检验 / χ²检验 / ...
3. 计算统计量 → 从数据中算出检验统计量
4. 计算 p 值 → 如果 H₀ 为真,看到当前结果的概率
5. 做出决策 → p < α:拒绝 H₀;p ≥ α:不拒绝
第一类 vs 第二类错误¶
| H₀ 为真 | H₀ 为假 | |
|---|---|---|
| 拒绝 H₀ | 第一类错误(假阳性)α | ✅ 正确决策 |
| 不拒绝 H₀ | ✅ 正确决策 | 第二类错误(假阴性)β |
决策类比:
- 第一类错误 = 把好人关进监狱(冤枉)
- 第二类错误 = 把坏人放走了(漏网)
α 通常设为 0.05,意味着接受 5% 的冤枉率。
🧪 常用检验方法¶
Z 检验 — 比例检验¶
适用场景:比较两个比例(如 A/B 测试转化率)
import numpy as np
from scipy import stats
# A:1000 人,100 人转化 (10%)
# B:1000 人,110 人转化 (11%)
n_a, c_a = 1000, 100
n_b, c_b = 1000, 110
p_a, p_b = c_a/n_a, c_b/n_b
p_pool = (c_a + c_b) / (n_a + n_b)
# Z 统计量
se = np.sqrt(p_pool * (1-p_pool) * (1/n_a + 1/n_b))
z_stat = (p_b - p_a) / se
# 双侧 p 值
p_value = 2 * (1 - stats.norm.cdf(abs(z_stat)))
print(f"Z = {z_stat:.3f}, p = {p_value:.4f}")
# p ≈ 0.46 → 不显著!1% 的差异很可能是随机波动
t 检验 — 均值比较¶
适用场景:比较两组的均值(如实验组 vs 对照组用户停留时长)
# 对照组:n=500, mean=120s, std=40s
# 实验组:n=500, mean=128s, std=42s
np.random.seed(42)
control = np.random.normal(120, 40, 500)
treatment = np.random.normal(128, 42, 500)
# 独立样本 t 检验
t_stat, p_value = stats.ttest_ind(treatment, control)
# Cohen's d(效应量)
cohens_d = (treatment.mean() - control.mean()) / np.sqrt(
(treatment.var() + control.var()) / 2
)
print(f"t = {t_stat:.3f}, p = {p_value:.4f}, d = {cohens_d:.3f}")
效应量解读: - d = 0.2 → 小效应(可能需要大样本才能检测) - d = 0.5 → 中效应(肉眼可见的差异) - d = 0.8 → 大效应(明显差异)
χ²(卡方)检验 — 分类变量独立性¶
适用场景:用户群体 × 购买行为是否相关?
# 购买 未购买
# 男性 120 380
# 女性 90 410
observed = np.array([[120, 380], [90, 410]])
chi2, p_value, dof, expected = stats.chi2_contingency(observed)
print(f"χ² = {chi2:.3f}, p = {p_value:.4f}")
# p < 0.05 → 性别与购买行为有显著关联
ANOVA — 多组比较¶
适用场景:A/B/C/D 四个版本,哪个最好?
# 4 个版本的转化率数据
a = np.random.binomial(1, 0.10, 500)
b = np.random.binomial(1, 0.11, 500)
c = np.random.binomial(1, 0.13, 500)
d = np.random.binomial(1, 0.12, 500)
f_stat, p_value = stats.f_oneway(a, b, c, d)
print(f"F = {f_stat:.3f}, p = {p_value:.4f}")
# p < 0.05 → 至少有一个版本与众不同
非参数检验 — 数据不满足正态假设时¶
# Mann-Whitney U 检验(t 检验的非参数替代)
u_stat, p_value = stats.mannwhitneyu(treatment, control, alternative='two-sided')
print(f"Mann-Whitney U = {u_stat:.0f}, p = {p_value:.4f}")
📏 置信区间¶
与其说「转化率是 11%」,不如说「我有 95% 的把握,转化率在 9.2% 到 12.8% 之间」。
def proportion_ci(successes, n, confidence=0.95):
"""比例的 Wald 置信区间"""
p = successes / n
z = stats.norm.ppf(1 - (1 - confidence) / 2)
se = np.sqrt(p * (1 - p) / n)
return p - z * se, p + z * se
ci_low, ci_high = proportion_ci(c_b, n_b)
print(f"转化率: {p_b:.3f}, 95% CI: [{ci_low:.3f}, {ci_high:.3f}]")
置信区间 vs p 值:
| 指标 | 回答的问题 | 局限 |
|---|---|---|
| p 值 | "效果为零的概率是多少?" | 不告诉你效果有多大 |
| 置信区间 | "真实效果在哪个范围?" | 不直接做决策 |
最佳实践:同时报告 p 值 + 置信区间 + 效应量。
⚠️ p 值陷阱¶
陷阱 1:p > 0.05 ≠ "没有效果"¶
可能只是样本量不够。做功效分析:
from statsmodels.stats.power import TTestIndPower
analysis = TTestIndPower()
n_needed = analysis.solve_power(
effect_size=0.2, # 小效应
power=0.80,
alpha=0.05
)
print(f"检测小效应需要每组 {n_needed:.0f} 个样本")
陷阱 2:p < 0.05 ≠ "效果很大"¶
100 万用户 × 0.01% 的提升 → p < 0.001,但业务价值为零。
陷阱 3:p-hacking¶
p-hacking 的危险之处在于,你不需要恶意——只需好奇心。 — Andrew Gelman
常见 p-hacking 手法: - 数据达到显著性就停 - 换指标重新检验 - 删除"异常值" - 分亚组分析直到找到显著结果
# 演示:20 次独立检验中,随机出现"显著结果"的概率
n_tests = 20
alpha = 0.05
prob_at_least_one = 1 - (1 - alpha)**n_tests
print(f"做 {n_tests} 次检验,至少一次假阳性的概率: {prob_at_least_one:.1%}")
# → 64%!你有一大半概率找到"显著"的随机噪声
防御:
1. 预注册分析计划(pre-registration)
2. Bonferroni 校正:α / 检验次数
3. 在独立 hold-out 集上验证
🛠 检验选择速查¶
| 要比较什么? | 数据类型 | 推荐检验 | Python 函数 |
|---|---|---|---|
| 一个比例 vs 基准 | 二元 | Z 检验 | stats.binom_test() |
| 两个比例 | 二元 | Z 检验 / χ² | stats.chi2_contingency() |
| 两组均值 | 连续 | t 检验 | stats.ttest_ind() |
| 多组均值 | 连续 | ANOVA | stats.f_oneway() |
| 配对前后 | 连续 | 配对 t 检验 | stats.ttest_rel() |
| 分布差异 | 连续/序数 | KS 检验 | stats.ks_2samp() |
| 相关性 | 连续 | Pearson r | stats.pearsonr() |
| 秩相关性 | 序数 | Spearman ρ | stats.spearmanr() |
🔗 相关资源¶
- 概率分布基础 — 推断统计的数学根基
- A/B 测试实操指南 — 从实验设计到决策
- 贝叶斯 vs 频率学派 — 两种统计哲学