统计学深度指南

文档概述

本指南系统梳理统计学从经典频率学派到现代贝叶斯学派的完整理论体系,涵盖点估计、区间估计、假设检验、参数估计方法以及EM算法等核心内容,为数据科学与机器学习提供方法论基础。

关键词

序号关键词英文核心公式
1频率学派Frequentist基于重复抽样的推断
2贝叶斯学派Bayesian$P(\theta
3点估计Point Estimation
4区间估计Interval Estimation
5假设检验Hypothesis Testing vs
6p值p-value$P(T(X) \geq t_{obs}
7最大似然估计MLE$\hat{\theta}{MLE} = \arg\max\theta \prod_i p(x_i
8EM算法EM Algorithm$Q(\theta
9置信区间Confidence Interval
10似然比检验Likelihood Ratio Test
11费舍尔信息Fisher Information
12充分统计量Sufficient Statistic$p(x

零、统计学入门:统计思维是什么?

0.1 你每天都在用统计思维

先别被”统计学”这三个字吓到。其实你每天都在用统计思维,只是没意识到而已。

比如你去买西瓜,拍一拍听声音挑瓜——这本质上就是在做统计推断:你根据有限的”样本”(拍西瓜的声音)来推断这个瓜好不好(总体特征)。如果你挑了十个瓜,总结出”声音越清脆越甜”的经验,这就是在从数据中学习规律——恭喜你,你已经在无意识地做机器学习了。

统计学就是这套”从局部推断整体”的科学方法的系统化。它要回答的核心问题是:我看到的这点数据,到底能不能代表真相?

0.2 统计思维跟机器学习到底什么关系

很多人觉得统计学和机器学习是两个不相干的东西,其实它们亲如父子。

机器学习本质上就是在做统计推断:你有一堆数据,想知道背后有什么规律。线性回归、逻辑回归是统计模型;决策树、随机森林是统计方法的升级版;神经网络是统计的非线性函数逼近;连ChatGPT都是对海量文本数据的统计建模。

区别只在于:传统统计学更关心”能不能证明因果”,机器学习更关心”能不能预测准确”。但到了实操层面,两者用到的数学工具几乎一模一样——概率论、数理统计、线性代数、优化算法。

所以,学好统计学,你学机器学习就等于有了扎实内功。

0.3 这份指南怎么用

这份指南分两部分:前半部分是给人类看的统计学,用大白话解释核心概念,配上Python代码演示;后半部分是给机器看的统计学,是原有理论体系的完整梳理。

如果你刚开始学,先看前半部分;如果你是进阶选手,直接翻后半部分;如果你是来面试的,前半部分的直觉理解 + 后半部分的公式 = 王炸组合。


一、描述性统计:用真实数据说话

1.1 均值、中位数、众数——三个”平均”有什么区别

说到”平均”,大部分人第一反应是”加起来除以个数”。这叫均值(Mean)。但平均其实有三种:

  • 均值:所有数加起来除以个数,对极端值( outliers )很敏感
  • 中位数:排完序之后最中间那个, robustness 强,不受极端值影响
  • 众数:出现次数最多的那个,适合分类数据

举例你就懂了:

import numpy as np
 
# 10个人的工资(单位:万/年)
salaries = np.array([8, 9, 10, 11, 12, 13, 14, 15, 16, 200])
 
print(f"均值: {np.mean(salaries):.2f}")      # 30.8 ——被首富拉高了
print(f"中位数: {np.median(salaries):.2f}")  # 12.5 ——更反映普通人水平
print(f"众数: {np.argmax(np.bincount(salaries.astype(int)))}")  # 没有重复,不适用

这个例子就是统计里著名的”被平均”问题——我和首富一平均,人人都是亿万富翁。所以看到”平均工资”这种指标,先问问是均值还是中位数。

1.2 方差和标准差——数据有多”散”

知道平均值只是第一步。你还需要知道数据有多”散”。

方差(Variance)衡量每个数据点离均值有多远:

标准差就是方差的平方根 ,跟原始数据单位一致,更容易解释。

print(f"方差: {np.var(salaries, ddof=0):.2f}")    # 总体方差
print(f"样本方差: {np.var(salaries, ddof=1):.2f}") # 样本方差(分母n-1)
print(f"标准差: {np.std(salaries, ddof=1):.2f}")  # 标准差

注意这里有个 ddof 参数:ddof=0 是总体方差(除以n),ddof=1 是样本方差(除以n-1)。为什么除以n-1?因为这样才是对总体方差的无偏估计——这是统计学里一个很精妙的 trick 。

1.3 分位数和箱线图——看看数据的”身材”

只知道均值和方差还不够,你得看看数据的分布”身材”。

分位数(Quantile)把数据分成几个等份:四分位数把数据分成四等份,中位数就是50%分位数。

q1 = np.percentile(salaries, 25)  # 25%分位数
q3 = np.percentile(salaries, 75)  # 75%分位数
iqr = q3 - q1  # 四分位距(IQR)
 
print(f"Q1: {q1}, Q3: {q3}, IQR: {iqr}")

IQR 是统计学里的一个神器:它定义了什么是”正常范围”。任何超出 的数据点,都被认为是异常值(outlier)。在那个工资数据里,200万那位就是异常值。

import matplotlib.pyplot as plt
 
plt.boxplot(salaries)
plt.title("工资分布箱线图")
plt.ylabel("工资(万/年)")
plt.show()

箱线图能一眼看出数据的:最小值、Q1、中位数、Q3、最大值,以及异常值。数据分析师面试必考这个图。

1.4 相关性——两个变量什么关系

想知道身高和体重有没有关系?想知道广告投放和销售额有没有关系?这就要用到相关系数(Correlation Coefficient)。

皮尔逊相关系数 衡量两个连续变量线性关系的强弱,取值范围是

  • :完美正相关
  • :完美负相关
  • :完全无关
  • :强相关
  • :弱相关
# 身高和体重的数据
heights = np.array([160, 165, 170, 175, 180, 185])
weights = np.array([55, 60, 68, 75, 82, 88])
 
correlation = np.corrcoef(heights, weights)[0, 1]
print(f"相关系数: {correlation:.4f}")  # 约0.99,强正相关
 
# 用scipy算更精确
from scipy import stats
r, p_value = stats.pearsonr(heights, weights)
print(f"相关系数: {r:.4f}, p值: {p_value:.6f}")

一个重要警告:相关不等于因果!后面会专门讲这个。


二、概率分布——数据的”长相”

2.1 为什么要学概率分布

概率分布描述的是”数据长什么样”。知道了数据的分布,你就能做很多事:

  • 预测某个值出现的概率
  • 计算数据的置信区间
  • 做假设检验
  • 理解机器学习模型的输出

现实中最常见的分布就那么几种:

2.2 正态分布——“中间多两头少”

正态分布(Gaussian Distribution)是统计学里yyds,自然界和人类社会的大量现象都近似正态分布:人的身高、测量误差、考试成绩……

import matplotlib.pyplot as plt
import numpy as np
from scipy import stats
 
x = np.linspace(-4, 4, 1000)
 
# 不同参数的正态分布
for mu, sigma, label in [(0, 1, 'μ=0, σ=1'), (0, 0.5, 'μ=0, σ=0.5'), (1, 1, 'μ=1, σ=1')]:
    y = stats.norm.pdf(x, mu, sigma)
    plt.plot(x, y, label=label)
 
plt.legend()
plt.title("正态分布")
plt.xlabel("x")
plt.ylabel("概率密度")
plt.show()

正态分布的”68-95-99.7法则”超级实用:

  • 68%的数据落在 范围内
  • 95%的数据落在 范围内
  • 99.7%的数据落在 范围内
mu, sigma = 0, 1
# 落在1个标准差内的概率
prob_1sigma = stats.norm.cdf(mu + sigma, mu, sigma) - stats.norm.cdf(mu - sigma, mu, sigma)
print(f"1σ范围内概率: {prob_1sigma:.3f}")  # 约0.683
 
# 落在2个标准差内的概率
prob_2sigma = stats.norm.cdf(mu + 2*sigma, mu, sigma) - stats.norm.cdf(mu - 2*sigma, mu, sigma)
print(f"2σ范围内概率: {prob_2sigma:.3f}")  # 约0.954

2.3 其他常见分布

二项分布:做n次独立试验,每次成功概率为p,成功次数的分布。比如抛10次硬币,正面出现3次的概率。

# 抛10次硬币,正面出现3次的概率
n, p = 10, 0.5
prob = stats.binom.pmf(3, n, p)
print(f"P(X=3) = {prob:.4f}")
 
# 正面出现不超过3次的概率
prob_cum = stats.binom.cdf(3, n, p)
print(f"P(X≤3) = {prob_cum:.4f}")

泊松分布:描述单位时间内随机事件发生次数的分布。比如一小时内到达银行的顾客数、网页每分钟的点击量。

# 平均每小时5个顾客,一小时来了8个的概率
lambd = 5
prob = stats.poisson.pmf(8, lambd)
print(f"P(X=8) = {prob:.4f}")

指数分布:描述两次事件发生间隔时间的分布。

# 平均等待时间10分钟,等了15分钟以上的概率
mean_wait = 10
prob = stats.expon.cdf(15, scale=mean_wait, loc=0)
print(f"P(等待>15分钟) = {1 - prob:.4f}")

2.4 用分布做概率计算

学会用 scipy.stats 计算概率,是数据分析师的基本功:

from scipy import stats
 
# 正态分布:X ~ N(100, 15),求P(X < 110)
prob = stats.norm.cdf(110, loc=100, scale=15)
print(f"P(X < 110) = {prob:.4f}")  # 约0.7475
 
# 求95%分位数(即μ±1.96σ)
quantile = stats.norm.ppf(0.975, loc=100, scale=15)  # 上尾2.5%的点
print(f"95%分位数: {quantile:.2f}")
 
# 已知考试成绩服从N(70, 10²),小明考了85分,排名大约多少?
z_score = (85 - 70) / 10  # z分数
percentile = stats.norm.cdf(z_score)
print(f"超过约{percentile*100:.1f}%的考生")

三、假设检验入门:如何用数据做决策

3.1 一个生活中的假设检验例子

假设检验听起来高大上,其实你每天都在用。

想象一下:你习惯点外卖,每次同一家店,送到的时间差不多都是30分钟。但今天送了一个小时。你会怎么想?

A. 今天是意外,正常 B. 今天有问题,店变慢了

你大脑里做的事情,其实就是假设检验:

  • 原假设 :“今天跟平时一样,送到时间还是30分钟左右”
  • 备择假设 :“今天确实变慢了”

然后你收集证据(今天的送达时间),看看这个证据在原假设下有多”离谱”。如果太离谱,就拒绝 ,接受

3.2 假设检验的核心逻辑

统计假设检验的逻辑是一样的,但更严格:

  1. 提出假设:原假设 通常是”没有效应”或”跟原来一样”;备择假设 是你怀疑的那个
  2. 选择检验方法:根据数据类型和问题类型选择合适的检验
  3. 计算统计量:把数据代入公式,得到一个数
  4. 看 p 值:这个数在原假设下出现的概率有多大
  5. 做决策:p 值小就拒绝 ,p 值大就不能拒绝

3.3 t检验——比较两组数据有没有差异

最常见的检验是 t 检验,用来比较两组数据的均值有没有显著差异。

场景:某公司想看看新推出的培训课程有没有效果。对20名员工做了培训,测了培训前后的绩效分数。

import numpy as np
from scipy import stats
 
# 培训前绩效
before = np.array([72, 80, 75, 68, 85, 78, 82, 70, 76, 88,
                   74, 79, 83, 71, 77, 81, 69, 84, 73, 86])
 
# 培训后绩效
after = np.array([78, 85, 80, 72, 90, 82, 88, 75, 81, 92,
                  79, 84, 87, 76, 82, 86, 74, 89, 78, 90])
 
# 配对t检验(同一个人的前后对比)
t_stat, p_value = stats.ttest_rel(before, after)
 
print(f"培训前均值: {np.mean(before):.2f}")
print(f"培训后均值: {np.mean(after):.2f}")
print(f"平均提升: {np.mean(after - before):.2f}分")
print(f"t统计量: {t_stat:.4f}")
print(f"p值: {p_value:.6f}")
 
if p_value < 0.05:
    print("结论:培训有显著效果(p < 0.05)")
else:
    print("结论:没有足够证据表明培训有效果")

两组独立样本的t检验(比如比较两个班的成绩):

class_a = np.array([78, 82, 75, 88, 72, 85, 79, 83, 76, 81])
class_b = np.array([85, 79, 88, 92, 78, 86, 84, 90, 82, 87])
 
t_stat, p_value = stats.ttest_ind(class_a, class_b)
print(f"独立样本t检验 p值: {p_value:.4f}")

3.4 卡方检验——检验分类数据

t 检验适合连续数据,卡方检验(Chi-square test)适合分类数据。

场景:某电商想知道用户的购买行为和性别有没有关系。

# 用户购买行为与性别的列联表
# 行:性别(男/女),列:是否购买(否/是)
data = np.array([
    [100, 60],  # 男性:100人没买,60人买了
    [80, 90]    # 女性:80人没买,90人买了
])
 
chi2, p_value, dof, expected = stats.chi2_contingency(data)
 
print(f"卡方统计量: {chi2:.4f}")
print(f"自由度: {dof}")
print(f"p值: {p_value:.6f}")
print(f"\n期望频数表:\n{expected}")
 
if p_value < 0.05:
    print("结论:购买行为与性别显著相关")
else:
    print("结论:没有足够证据表明购买行为与性别相关")

四、p值到底是什么意思——别再误用了

4.1 p值的正确理解

p 值是统计学里被误解最深的一个概念。网上随便搜搜,就能看到无数人对 p 值的误用。

p值的精确定义

p值是在原假设 为真的前提下,观察到当前数据(或者更极端数据)的概率。

举例:某药厂说他们的新药能降低血压。你做了实验,p值 = 0.03。

正确的解读:如果这个药其实没效果( 为真),那么有约3%的概率,仅凭随机波动也能观察到像今天这样”看起来有效”的数据。

错误的解读

❌ 错误理解✅ 正确理解
p值 = 0.03 → 这个药有3%的概率无效p值 = 0.03 → 如果药无效,看到这种数据的概率是3%
p值 = 0.03 → 这个结论有97%的把握是对的p值衡量的是”数据的不寻常程度”,不是”结论的正确性”
p < 0.05 → 效应是真实存在的p < 0.05 → 数据提供了拒绝 的证据,但不证明效应为真
p > 0.05 → 两个因素完全没关系p > 0.05 → 数据不足以证明有关系

4.2 p值的”可复制性危机”

心理学和医学领域有个著名的”可复制性危机”:很多当年 p < 0.05 的研究结论,后来重复实验发现根本不成立。

原因在于:即使原假设为真,p值也有5%的概率会小于0.05。这叫”第一类错误”。

更扎心的是这个数字:假设你做了1000个实验,实际上有500个原假设是对的,500个是错的。对于这500个真原假设,约5%(25个)的p值会小于0.05,你会误以为有效果。对于假原假设,如果统计功效是80%,你能发现400个。

最后你宣布”显著”的结果里,有400个真阳性 + 25个假阳性 = 425个。但其中 25/425 ≈ 6%其实是假的

如果原假设先验概率更高(比如更离谱的假设),这个比例会更高。

4.3 实际操作建议

# 模拟演示:p值的分布
import numpy as np
from scipy import stats
import matplotlib.pyplot as plt
 
# 当H0为真时,p值服从均匀分布[0,1]
np.random.seed(42)
p_values_H0_true = [stats.ttest_rel(np.random.randn(20) + 5, 
                                    np.random.randn(20) + 5)[1] 
                    for _ in range(1000)]
 
plt.hist(p_values_H0_true, bins=50, edgecolor='black', alpha=0.7)
plt.axvline(x=0.05, color='r', linestyle='--', label='α=0.05')
plt.xlabel('p值')
plt.ylabel('频数')
plt.title('H0为真时,p值的分布(应该均匀)')
plt.legend()
plt.show()
 
# 看看有多少"显著"结果其实是假的
significant = sum(1 for p in p_values_H0_true if p < 0.05)
print(f"1000次实验,H0为真但p<0.05的次数: {significant}(理论值约50)")

所以,用p值做决策时:

  1. 不要只看p值:同时看效应大小和置信区间
  2. 不要迷信0.05:这个阈值是Fisher随便选的,不是物理定律
  3. 预注册你的实验:在收集数据前就声明你的分析计划,避免” p hacking ”
  4. 尝试 replication:单次显著结果不可靠,能重复才可靠

五、置信区间的直觉——95%置信区间不是95%的概率包含真值

5.1 置信区间到底是什么

置信区间(Confidence Interval)是统计学里另一个被严重误解的概念。

最常见的误解:“我们有95%的信心认为真实值落在这个区间内。”

这句话听起来很直观,但其实是错的

正确的理解:如果重复抽样100次,用同样的方法构造100个置信区间,约有95个会包含真实的参数值。

区别在哪里?真实参数是固定的(只是你不知道),区间是随机的。你不能说”参数有95%的概率落在区间内”——参数要么在里面,要么不在里面,没有概率可言。

5.2 用Python直观理解置信区间

import numpy as np
from scipy import stats
import matplotlib.pyplot as plt
 
np.random.seed(42)
 
# 假设真实均值是100,标准差是15
true_mean = 100
true_std = 15
sample_size = 30
n_experiments = 100
 
# 记录有多少个区间包含了真值
containing_true = 0
 
plt.figure(figsize=(14, 8))
intervals = []
 
for i in range(n_experiments):
    sample = np.random.normal(true_mean, true_std, sample_size)
    sample_mean = np.mean(sample)
    sample_std = np.std(sample, ddof=1)
    se = sample_std / np.sqrt(sample_size)
    
    # 95%置信区间
    ci_lower = sample_mean - stats.t.ppf(0.975, sample_size - 1) * se
    ci_upper = sample_mean + stats.t.ppf(0.975, sample_size - 1) * se
    
    intervals.append((ci_lower, ci_upper, sample_mean))
    
    # 检查是否包含真值
    contains = ci_lower <= true_mean <= ci_upper
    if contains:
        containing_true += 1
        color = 'blue'
    else:
        color = 'red'
    
    plt.errorbar(i, sample_mean, yerr=[[sample_mean-ci_lower], [ci_upper-sample_mean]], 
                 fmt='o', color=color, alpha=0.7, capsize=3)
 
plt.axhline(y=true_mean, color='green', linestyle='-', linewidth=2, label=f'真实均值 = {true_mean}')
plt.xlabel('实验编号')
plt.ylabel('样本均值')
plt.title(f'95%置信区间演示({containing_true}/{n_experiments}个区间包含真值)')
plt.legend()
plt.tight_layout()
plt.show()
 
print(f"\n{containing_true}/{n_experiments}个置信区间包含了真实均值")
print(f"包含率: {containing_true/n_experiments*100:.1f}%(理论值95%)")

运行完这段代码,你会看到:蓝色的线表示包含真值的区间,红色的是不包含的。总体来看,大约95%的区间是蓝色的。

5.3 置信区间的宽度跟什么有关

置信区间的宽度取决于三个因素:

  1. 样本量 n:n 越大,区间越窄( 的关系)
  2. 数据的变异程度(标准差):变异越大,区间越宽
  3. 置信水平:要95%置信水平,区间比90%的宽
# 样本量对置信区间宽度的影响
true_std = 15
sample_sizes = [10, 30, 50, 100, 200, 500]
widths = []
 
for n in sample_sizes:
    # 理论上的置信区间宽度
    se = true_std / np.sqrt(n)
    width = 2 * stats.norm.ppf(0.975) * se
    widths.append(width)
 
plt.plot(sample_sizes, widths, 'bo-')
plt.xlabel('样本量')
plt.ylabel('置信区间宽度')
plt.title('样本量越大,置信区间越窄')
plt.grid(True)
plt.show()

这就是为什么大公司的用户调查往往只需要几千份问卷就能得到可靠的结论——增加样本量的边际收益递减,1000份和2000份差距不大,但100份和200份差距就很大。


六、A/B测试实战:用Python做统计显著性检验

6.1 A/B测试是什么

A/B测试是互联网公司做产品决策的核心方法。简单说就是:同时让两组用户看到不同版本,看哪个效果更好

比如一个电商网站想优化”立即购买”按钮的颜色:

  • A组看到红色按钮
  • B组看到绿色按钮
  • 跑一周,看哪个组的转化率更高

这就是一个活生生的假设检验问题。

6.2 用Python实现完整的A/B测试

import numpy as np
from scipy import stats
import matplotlib.pyplot as plt
 
np.random.seed(42)
 
# 模拟A/B测试数据
# A组:10000人,转化率15%
# B组:10000人,转化率16%(实际上B组更好)
 
n_a = 10000
n_b = 10000
p_a = 0.15  # A组真实转化率
p_b = 0.16  # B组真实转化率
 
# 生成模拟数据
conversions_a = np.random.binomial(1, p_a, n_a)
conversions_b = np.random.binomial(1, p_b, n_b)
 
# 计算转化率
rate_a = np.mean(conversions_a)
rate_b = np.mean(conversions_b)
 
print(f"A组转化率: {rate_a:.4f} ({rate_a*100:.2f}%)")
print(f"B组转化率: {rate_b:.4f} ({rate_b*100:.2f}%)")
print(f"相对提升: {(rate_b - rate_a) / rate_a * 100:.2f}%")

6.3 方法一:z检验

# 两组比例的z检验
from statsmodels.stats.proportion import proportions_ztest
 
# [转化人数A, 转化人数B]
count = [np.sum(conversions_a), np.sum(conversions_b)]
# [样本量A, 样本量B]
nobs = [n_a, n_b]
 
z_stat, p_value = proportions_ztest(count, nobs, alternative='two-sided')
 
print(f"\n=== z检验 ===")
print(f"z统计量: {z_stat:.4f}")
print(f"p值: {p_value:.4f}")
 
if p_value < 0.05:
    print("结论:两组转化率存在显著差异")
else:
    print("结论:没有足够证据表明两组转化率有差异")

6.4 方法二:置信区间法

# 用置信区间判断差异
pooled_p = (np.sum(conversions_a) + np.sum(conversions_b)) / (n_a + n_b)
se = np.sqrt(pooled_p * (1 - pooled_p) * (1/n_a + 1/n_b))
 
diff = rate_b - rate_a
ci_lower = diff - 1.96 * se
ci_upper = diff + 1.96 * se
 
print(f"\n=== 置信区间法 ===")
print(f"转化率差异: {diff:.4f} ({diff*100:.2f}%)")
print(f"95%置信区间: [{ci_lower:.4f}, {ci_upper:.4f}]")
print(f"95%置信区间: [{ci_lower*100:.2f}%, {ci_upper*100:.2f}%]")
 
if ci_lower > 0:
    print("结论:置信区间不包含0,B组显著更好")
elif ci_upper < 0:
    print("结论:置信区间不包含0,A组显著更好")
else:
    print("结论:置信区间包含0,差异不显著")

6.5 方法三:贝叶斯方法

传统频率学派告诉你”有没有显著差异”,贝叶斯方法直接告诉你”B比A好的概率有多大”:

# 贝叶斯A/B测试:用Beta分布建模转化率
# 先验:Beta(1, 1) = 均匀分布(无信息先验)
# 后验:Beta(1 + 转化数, 1 + 未转化数)
 
alpha_prior, beta_prior = 1, 1
 
alpha_a = alpha_prior + np.sum(conversions_a)
beta_a = beta_prior + (n_a - np.sum(conversions_a))
alpha_b = alpha_prior + np.sum(conversions_b)
beta_b = beta_prior + (n_b - np.sum(conversions_b))
 
# 从后验分布抽样
n_samples = 100000
samples_a = np.random.beta(alpha_a, beta_a, n_samples)
samples_b = np.random.beta(alpha_b, beta_b, n_samples)
 
# 计算B比A好的概率
prob_b_better = np.mean(samples_b > samples_a)
prob_a_better = np.mean(samples_a > samples_b)
 
print(f"\n=== 贝叶斯A/B测试 ===")
print(f"B比A好的概率: {prob_b_better:.4f} ({prob_b_better*100:.2f}%)")
print(f"A比B好的概率: {prob_a_better:.4f} ({prob_a_better*100:.2f}%)")
 
# 后验均值
print(f"\nA组转化率后验均值: {np.mean(samples_a):.4f}")
print(f"B组转化率后验均值: {np.mean(samples_b):.4f}")
 
# 可视化后验分布
plt.figure(figsize=(10, 5))
plt.hist(samples_a, bins=50, alpha=0.5, label=f'A组 (μ={np.mean(samples_a):.4f})', density=True)
plt.hist(samples_b, bins=50, alpha=0.5, label=f'B组 (μ={np.mean(samples_b):.4f})', density=True)
plt.xlabel('转化率')
plt.ylabel('概率密度')
plt.title(f'贝叶斯A/B测试后验分布\nB比A好的概率: {prob_b_better*100:.1f}%')
plt.legend()
plt.show()

贝叶斯方法的好处是:结果可以直接解读为”B比A好的概率是X%“,产品经理和市场人员一听就懂。不像 p 值那样容易被误读。


七、相关与因果——统计学告诉你为什么相关性不等于因果

7.1 经典案例:冰淇淋销量和溺水人数

统计数据显示,冰淇淋销量越高的月份,溺水人数也越多。你能得出”吃冰淇淋导致溺水”的结论吗?

当然不能!真正的原因是夏天。夏天冰淇淋卖得好,夏天游泳的人也多,溺水人数自然也多。

这里冰淇淋销量和溺水人数有相关性(correlated),但没有因果关系(causal relationship)。第三变量(夏天)是它们的共同原因。

7.2 为什么相关性不等于因果

相关性不等于因果,因为存在混淆变量(confounding variable)。混淆变量同时影响两个变量,让你误以为它们有因果关系。

常见场景:

相关混淆变量真正因果
喝咖啡 ↔ 肺癌吸烟喝咖啡不导致肺癌
鞋码 ↔ 阅读能力年龄鞋码大是因为年龄大
收入 ↔ 健康生活习惯收入影响但不等于健康

7.3 用Python演示混淆变量

import numpy as np
import matplotlib.pyplot as plt
from scipy import stats
 
np.random.seed(42)
 
# 模拟:年龄 同时影响 鞋码 和 阅读能力
n = 200
age = np.random.randint(5, 15, n)  # 年龄 5-14岁
 
# 鞋码 = 年龄 × 2 + 噪声
shoe_size = age * 2 + np.random.normal(0, 1, n)
 
# 阅读能力 = 年龄 × 3 + 噪声
reading = age * 3 + np.random.normal(0, 2, n)
 
# 鞋码和阅读能力看起来相关
r, p = stats.pearsonr(shoe_size, reading)
print(f"鞋码和阅读能力相关系数: {r:.4f} (p={p:.2e})")
 
plt.figure(figsize=(12, 5))
 
plt.subplot(1, 2, 1)
plt.scatter(shoe_size, reading, alpha=0.5)
plt.xlabel('鞋码')
plt.ylabel('阅读能力')
plt.title(f'鞋码 vs 阅读能力(相关系数={r:.3f}\n发现了什么?')
 
plt.subplot(1, 2, 2)
# 用颜色表示年龄
scatter = plt.scatter(shoe_size, reading, c=age, cmap='viridis', alpha=0.5)
plt.colorbar(scatter, label='年龄')
plt.xlabel('鞋码')
plt.ylabel('阅读能力')
plt.title('同样的数据,加上年龄信息\n真相大白:都是年龄在搞鬼')
 
plt.tight_layout()
plt.show()

7.4 如何建立因果关系

统计学能告诉你相关,但不能直接告诉你因果。要建立因果关系,需要:

  1. 随机对照实验(RCT):黄金标准。随机分配处理组和对照组,排除所有混淆因素
  2. 工具变量(IV):找到只通过”处理”影响”结果”的变量
  3. 倾向得分匹配(PSM):给观察数据”做实验”
  4. 双重差分(DID):比较处理组和对照组在处理前后的变化差异
  5. 断点回归(RDD):利用自然发生的”断点”

八、回归分析入门:从散点图到线性回归

8.1 线性回归是什么

线性回归是统计学和机器学习中最基础的模型。简单说就是:找到一条直线,能最好地代表散点的趋势

“最好”的标准是最小二乘法:让所有点到直线的垂直距离的平方和最小。

import numpy as np
import matplotlib.pyplot as plt
from scipy import stats
import statsmodels.api as sm
 
# 房价数据:面积(平方米)和价格(万)
np.random.seed(42)
 
area = np.array([50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150])
# 真实关系:价格 = 0.8 × 面积 + 10 + 噪声
price = 0.8 * area + 10 + np.random.normal(0, 5, len(area))
 
# 用numpy做简单线性回归
slope, intercept, r_value, p_value, std_err = stats.linregress(area, price)
 
print(f"斜率: {slope:.4f}")
print(f"截距: {intercept:.4f}")
print(f"R²: {r_value**2:.4f}")
print(f"p值: {p_value:.6f}")
print(f"\n回归方程: 价格 = {intercept:.2f} + {slope:.2f} × 面积")
 
# 可视化
plt.figure(figsize=(10, 6))
plt.scatter(area, price, color='blue', s=60, alpha=0.7, label='实际数据')
 
# 画回归线
area_line = np.linspace(40, 160, 100)
predicted = intercept + slope * area_line
plt.plot(area_line, predicted, 'r-', linewidth=2, label=f'回归线: y = {intercept:.2f} + {slope:.2f}x')
 
# 预测值和残差
predicted_price = intercept + slope * area
residuals = price - predicted_price
 
# 画残差
for i in range(len(area)):
    plt.vlines(area[i], predicted_price[i], price[i], colors='gray', linestyles='dashed', alpha=0.5)
 
plt.xlabel('面积(平方米)', fontsize=12)
plt.ylabel('价格(万)', fontsize=12)
plt.title('房价预测:线性回归', fontsize=14)
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()

8.2 用statsmodels做完整的回归分析

numpy的 linregress 只能做简单回归(一个自变量)。statsmodels 可以做多元回归,还能给出详细的统计报告:

import statsmodels.api as sm
 
# 添加常数项(截距)
X = sm.add_constant(area)
model = sm.OLS(price, X)
results = model.fit()
 
print(results.summary())

这个summary输出的内容很丰富,重点看:

  • R-squared:模型解释了变异的比例。R² = 0.95 意味着模型解释了95%的房价差异
  • coef(系数):每个自变量的系数
  • P>|t|(p值):检验系数是否显著不为零
  • [0.025 0.975]:95%置信区间

8.3 多元线性回归:考虑更多因素

房价不只跟面积有关,还跟地段、房龄、装修程度等有关。多元回归可以同时考虑多个因素:

np.random.seed(42)
 
n = 100
area = np.random.uniform(50, 150, n)           # 面积
district = np.random.choice([0, 1, 2], n)      # 地段(0=郊区,1=普通,2=核心)
age = np.random.uniform(0, 30, n)              # 房龄(年)
 
# 真实价格 = 0.5×面积 + 10×地段 - 1×房龄 + 20 + 噪声
price = 0.5 * area + 10 * district - 1 * age + 20 + np.random.normal(0, 5, n)
 
# 构建特征矩阵
X = np.column_stack([area, district, age])
X = sm.add_constant(X)
 
model = sm.OLS(price, X)
results = model.fit()
 
print(results.summary())
 
print(f"\n解读:")
print(f"- 面积每增加1平方米,价格增加约{results.params[1]:.2f}万")
print(f"- 地段每升一级,价格增加约{results.params[2]:.2f}万")
print(f"- 房龄每增加1年,价格下降约{results.params[3]:.2f}万")

8.4 回归诊断:检查模型是否靠谱

线性回归有若干假设,不满足的话结果就不靠谱:

# 回归诊断
predicted = results.fittedvalues
residuals = results.resid
 
fig, axes = plt.subplots(2, 2, figsize=(12, 10))
 
# 1. 残差 vs 拟合值图:检查同方差性和线性假设
axes[0, 0].scatter(predicted, residuals, alpha=0.5)
axes[0, 0].axhline(y=0, color='r', linestyle='--')
axes[0, 0].set_xlabel('拟合值')
axes[0, 0].set_ylabel('残差')
axes[0, 0].set_title('残差 vs 拟合值')
 
# 2. Q-Q图:检查正态性
from scipy import stats
stats.probplot(residuals, dist="norm", plot=axes[0, 1])
axes[0, 1].set_title('Q-Q图(检验残差正态性)')
 
# 3. 残差直方图
axes[1, 0].hist(residuals, bins=20, edgecolor='black', alpha=0.7)
axes[1, 0].set_xlabel('残差')
axes[1, 0].set_ylabel('频数')
axes[1, 0].set_title('残差分布')
 
# 4. Scale-Location图:检查同方差性
standardized_residuals = residuals / np.std(residuals)
axes[1, 1].scatter(predicted, np.sqrt(np.abs(standardized_residuals)), alpha=0.5)
axes[1, 1].set_xlabel('拟合值')
axes[1, 1].set_ylabel('√|标准化残差|')
axes[1, 1].set_title('Scale-Location图')
 
plt.tight_layout()
plt.show()

好的残差图应该是:残差 vs 拟合值没有明显模式,Q-Q图接近对角线。如果残差有明显的喇叭形、曲线形或尾巴,那模型就有问题。


九、ANOVA:比较多个组的差异

9.1 为什么要用ANOVA

之前学的t检验只能比较两组。如果你想比较三组甚至更多组呢?

一个办法是多次用t检验两两比较。但这会严重增加第一类错误率(假阳性)——比较次数越多,误判概率越高。

ANOVA(方差分析)就是为多组比较设计的。它通过比较组间方差组内方差来判断各组均值是否有显著差异。

9.2 单因素ANOVA实战

场景:比较三种不同 diet(饮食方案)的减重效果。

import numpy as np
from scipy import stats
import matplotlib.pyplot as plt
import statsmodels.api as sm
from statsmodels.formula.api import ols
 
np.random.seed(42)
 
# 三组人的减重数据(公斤)
diet_a = np.array([2.1, 3.5, 2.8, 1.9, 3.2, 2.5, 2.7, 3.0, 2.3, 2.9])  # 低碳水
diet_b = np.array([1.5, 2.1, 1.8, 2.3, 1.6, 2.0, 1.7, 2.2, 1.9, 1.4])  # 低脂肪
diet_c = np.array([3.2, 4.1, 3.5, 3.8, 4.0, 3.6, 3.9, 3.3, 4.2, 3.7])  # 间歇性禁食
 
# 可视化
plt.figure(figsize=(10, 5))
plt.boxplot([diet_a, diet_b, diet_c], labels=['低碳水', '低脂肪', '间歇性禁食'])
plt.ylabel('减重(公斤)')
plt.title('三种饮食方案的减重效果对比')
plt.grid(True, alpha=0.3)
plt.show()
 
# ANOVA检验
f_stat, p_value = stats.f_oneway(diet_a, diet_b, diet_c)
 
print(f"=== 单因素ANOVA ===")
print(f"F统计量: {f_stat:.4f}")
print(f"p值: {p_value:.6f}")
 
if p_value < 0.05:
    print("结论:三种饮食方案的减重效果存在显著差异")
else:
    print("结论:没有足够证据表明三种饮食方案减重效果不同")

9.3 用statsmodels做更详细的ANOVA

# 整理数据格式
import pandas as pd
 
df = pd.DataFrame({
    'weight_loss': np.concatenate([diet_a, diet_b, diet_c]),
    'diet': ['低碳水']*10 + ['低脂肪']*10 + ['间歇性禁食']*10
})
 
# 拟合线性模型
model = ols('weight_loss ~ C(diet)', data=df).fit()
 
# ANOVA表
anova_table = sm.stats.anova_lm(model, typ=2)
print("\n=== ANOVA表 ===")
print(anova_table)
 
# 事后检验:到底哪两组有差异?
from statsmodels.stats.multicomp import pairwise_tukeyhsd
 
tukey = pairwise_tukeyhsd(df['weight_loss'], df['diet'], alpha=0.05)
print("\n=== Tukey HSD事后检验 ===")
print(tukey)

Tukey HSD(Turkey诚实显著差异检验)会告诉你具体哪两组之间有显著差异。它控制了整个比较的第一类错误率,不会像多次t检验那样”膨胀”误判概率。


十、Bootstrap方法:用重采样理解不确定性

10.1 Bootstrap的直观理解

传统统计推断需要假设数据的分布(比如正态分布),然后用公式推导标准误差。但有时候数据不符合这些假设,或者根本不知道用什么分布。

Bootstrap提供了一个”暴力美学”解决方案:既然我不知道总体长什么样,那我就用样本自己来估计

核心思想:从样本中有放回地抽样,生成”新的样本”(叫bootstrap样本),然后计算统计量。重复几千次,看看统计量的分布是什么样的。

10.2 Bootstrap实战

import numpy as np
import matplotlib.pyplot as plt
 
np.random.seed(42)
 
# 假设这是你收集到的数据(总体分布未知)
# 比如100个用户的客单价
data = np.array([158, 245, 189, 312, 276, 198, 167, 234, 289, 345,
                 176, 223, 267, 194, 318, 256, 201, 178, 298, 245,
                 167, 234, 289, 312, 189, 223, 267, 178, 298, 345,
                 198, 256, 167, 234, 289, 312, 189, 223, 267, 178,
                 298, 345, 158, 245, 189, 312, 276, 198, 167, 234,
                 289, 345, 176, 223, 267, 194, 318, 256, 201, 178,
                 298, 245, 167, 234, 289, 312, 189, 223, 267, 178,
                 298, 345, 198, 256, 167, 234, 289, 312, 189, 223,
                 267, 178, 298, 345, 158, 245, 189, 312, 276, 198])
 
n = len(data)
sample_mean = np.mean(data)
n_bootstrap = 10000
 
# Bootstrap:生成10000个bootstrap样本
bootstrap_means = []
for _ in range(n_bootstrap):
    bootstrap_sample = np.random.choice(data, size=n, replace=True)
    bootstrap_means.append(np.mean(bootstrap_sample))
 
bootstrap_means = np.array(bootstrap_means)
 
# 计算标准误差(bootstrap的标准差就是标准误差的估计)
se_bootstrap = np.std(bootstrap_means)
 
# 计算95%置信区间(百分位法)
ci_lower = np.percentile(bootstrap_means, 2.5)
ci_upper = np.percentile(bootstrap_means, 97.5)
 
print(f"样本均值: {sample_mean:.2f}")
print(f"Bootstrap标准误差: {se_bootstrap:.2f}")
print(f"95%置信区间: [{ci_lower:.2f}, {ci_upper:.2f}]")
 
# 可视化
plt.figure(figsize=(12, 5))
 
plt.subplot(1, 2, 1)
plt.hist(bootstrap_means, bins=50, edgecolor='black', alpha=0.7, density=True)
plt.axvline(sample_mean, color='red', linewidth=2, label=f'样本均值 = {sample_mean:.2f}')
plt.axvline(ci_lower, color='green', linestyle='--', linewidth=2, label=f'95% CI下界 = {ci_lower:.2f}')
plt.axvline(ci_upper, color='green', linestyle='--', linewidth=2, label=f'95% CI上界 = {ci_upper:.2f}')
plt.xlabel('均值估计')
plt.ylabel('概率密度')
plt.title('Bootstrap均值分布')
plt.legend()
 
plt.subplot(1, 2, 2)
plt.hist(data, bins=30, edgecolor='black', alpha=0.7, density=True)
plt.xlabel('客单价')
plt.ylabel('概率密度')
plt.title('原始数据分布')
plt.tight_layout()
plt.show()

10.3 Bootstrap vs 传统方法

传统方法假设数据服从正态分布,然后推导公式。Bootstrap不需要这个假设,只要样本能代表总体就行。

# 对比:传统方法的置信区间
from scipy import stats
 
se_traditional = stats.sem(data)  # 标准误差的传统估计
ci_lower_trad = sample_mean - 1.96 * se_traditional
ci_upper_trad = sample_mean + 1.96 * se_traditional
 
print(f"\n=== 对比 ===")
print(f"传统方法 95%CI: [{ci_lower_trad:.2f}, {ci_upper_trad:.2f}]")
print(f"Bootstrap 95%CI: [{ci_lower:.2f}, {ci_upper:.2f}]")
print(f"两者差距很小,说明数据大致符合正态分布假设")

当两者差距很大时,说明数据分布有问题,或者样本量太小,Bootstrap的结果反而更可靠。


十一、贝叶斯统计入门:用Beta分布理解后验分布

11.1 从抛硬币理解贝叶斯

贝叶斯统计的核心是贝叶斯定理:

  • 先验:看到数据前,你对参数的信念
  • 似然:如果参数是 ,看到这些数据的概率
  • 后验:看到数据后,更新后的信念

用抛硬币来理解:你想估计一枚硬币正面朝上的概率

11.2 Beta-Bernoulli模型

import numpy as np
import matplotlib.pyplot as plt
from scipy import stats
 
# 抛硬币10次,正面5次,反面5次
n_heads = 5
n_total = 10
 
# 先验:Beta(1, 1) = 均匀分布(抛硬币前什么都不知道)
alpha_prior, beta_prior = 1, 1
 
# 后验:Beta(α + 正面次数, β + 反面次数)
alpha_post = alpha_prior + n_heads
beta_post = beta_prior + (n_total - n_heads)
 
print(f"先验: Beta({alpha_prior}, {beta_prior})")
print(f"后验: Beta({alpha_post}, {beta_post})")
print(f"后验均值: {alpha_post / (alpha_post + beta_post):.3f}")
print(f"后验中位数: {(alpha_post - 1/3) / (alpha_post + beta_post - 2/3):.3f}")
 
# 可视化先验和后验
theta = np.linspace(0.01, 0.99, 1000)
 
prior_pdf = stats.beta.pdf(theta, alpha_prior, beta_prior)
post_pdf = stats.beta.pdf(theta, alpha_post, beta_post)
 
plt.figure(figsize=(10, 6))
plt.plot(theta, prior_pdf, 'b-', linewidth=2, label=f'先验 Beta({alpha_prior}, {beta_prior})')
plt.plot(theta, post_pdf, 'r-', linewidth=2, label=f'后验 Beta({alpha_post}, {beta_post})')
plt.xlabel('θ(正面概率)')
plt.ylabel('概率密度')
plt.title('贝叶斯更新:从先验到后验')
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()

11.3 后验预测分布

贝叶斯方法还能做后验预测:考虑参数的不确定性,预测未来数据的分布。

# 从后验分布抽样
n_samples = 10000
theta_samples = np.random.beta(alpha_post, beta_post, n_samples)
 
# 预测再抛一次硬币正面朝上的概率分布
future_probs = theta_samples
 
plt.figure(figsize=(10, 5))
plt.hist(future_probs, bins=50, edgecolor='black', alpha=0.7, density=True)
plt.xlabel('未来正面概率')
plt.ylabel('概率密度')
plt.title(f'后验预测分布\n(基于{B(n_total, {n_heads})次数据)')
plt.axvline(x=np.mean(future_probs), color='red', linestyle='--', label=f'均值={np.mean(future_probs):.3f}')
plt.axvline(x=np.percentile(future_probs, 2.5), color='green', linestyle='--', label=f'2.5%分位={np.percentile(future_probs, 2.5):.3f}')
plt.axvline(x=np.percentile(future_probs, 97.5), color='green', linestyle='--', label=f'97.5%分位={np.percentile(future_probs, 97.5):.3f}')
plt.legend()
plt.show()
 
# 95%可信区间(贝叶斯)
ci_low = np.percentile(future_probs, 2.5)
ci_high = np.percentile(future_probs, 97.5)
print(f"95%可信区间: [{ci_low:.3f}, {ci_high:.3f}]")
print(f"解读:我们有95%的概率认为真实θ落在这个区间内")

注意这个95%可信区间和频率学派95%置信区间的区别:

  • 可信区间(贝叶斯):参数有95%的概率落在这个区间里
  • 置信区间(频率):如果重复抽样,95%的区间会包含真实参数

贝叶斯的解释更直观,更符合人们的直觉需求。


十二、统计思维在机器学习中的应用

12.1 正则化就是贝叶斯先验

L2正则化(岭回归)和L1正则化(Lasso)在机器学习中无处不在。从统计角度看,它们就是贝叶斯先验

  • 无正则化 = 无先验假设,参数完全由数据决定(最大似然估计)
  • L2正则化 = 假设参数服从正态先验( ridge regression = Bayesian MAP with Gaussian prior)
  • L1正则化 = 假设参数服从Laplace先验(自动做特征选择)
import numpy as np
import matplotlib.pyplot as plt
from sklearn.linear_model import Ridge, Lasso, LinearRegression
from sklearn.model_selection import cross_val_score
 
np.random.seed(42)
n_samples, n_features = 100, 20
 
# 生成有噪声的数据:只有5个特征真正有用
X = np.random.randn(n_samples, n_features)
true_beta = np.zeros(n_features)
true_beta[:5] = np.array([3, -2, 1.5, 2.5, -1.5])
y = X @ true_beta + np.random.randn(n_samples) * 2
 
# 不同正则化强度的效果
alphas = np.logspace(-3, 2, 50)
ridge_coefs = []
lasso_coefs = []
 
for alpha in alphas:
    ridge = Ridge(alpha=alpha).fit(X, y)
    lasso = Lasso(alpha=alpha, max_iter=10000).fit(X, y)
    ridge_coefs.append(ridge.coef_)
    lasso_coefs.append(lasso.coef_)
 
ridge_coefs = np.array(ridge_coefs)
lasso_coefs = np.array(lasso_coefs)
 
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
 
# Ridge回归:系数被压缩但不会归零
for i in range(n_features):
    axes[0].plot(alphas, ridge_coefs[:, i], alpha=0.5)
axes[0].axhline(y=0, color='k', linestyle='--', alpha=0.3)
axes[0].set_xscale('log')
axes[0].set_xlabel('正则化强度 α')
axes[0].set_ylabel('系数值')
axes[0].set_title('Ridge回归(L2正则化)\n系数被收缩但不会归零')
 
# Lasso回归:系数可以归零(做特征选择)
for i in range(n_features):
    axes[1].plot(alphas, lasso_coefs[:, i], alpha=0.5)
axes[1].axhline(y=0, color='k', linestyle='--', alpha=0.3)
axes[1].set_xscale('log')
axes[1].set_xlabel('正则化强度 α')
axes[1].set_ylabel('系数值')
axes[1].set_title('Lasso回归(L1正则化)\n系数可以归零(特征选择)')
 
plt.tight_layout()
plt.show()

12.2 交叉验证就是用数据估计泛化误差

机器学习模型在训练集上的误差叫”训练误差”,在新数据上的误差叫”泛化误差”。交叉验证(Cross-Validation)就是用有限的数据同时做训练和验证。

from sklearn.model_selection import cross_val_score
from sklearn.linear_model import Ridge
from sklearn.datasets import make_regression
 
X, y = make_regression(n_samples=200, n_features=100, noise=10, random_state=42)
 
# 不同正则化强度的交叉验证
alphas = [0.01, 0.1, 1, 10, 100]
cv_scores = []
 
for alpha in alphas:
    ridge = Ridge(alpha=alpha)
    scores = cross_val_score(ridge, X, y, cv=5, scoring='neg_mean_squared_error')
    cv_scores.append(-scores.mean())
    print(f"alpha={alpha:6}: CV MSE = {-scores.mean():.2f}{scores.std():.2f})")
 
best_alpha = alphas[np.argmin(cv_scores)]
print(f"\n最优alpha: {best_alpha}")

12.3 偏差-方差分解指导模型选择

任何模型的预测误差都可以分解为三部分:

import numpy as np
import matplotlib.pyplot as plt
from sklearn.linear_model import LinearRegression
from sklearn.preprocessing import PolynomialFeatures
from sklearn.pipeline import make_pipeline
 
np.random.seed(42)
n = 100
 
# 真实函数:y = sin(x) + 噪声
x = np.sort(np.random.uniform(0, 2 * np.pi, n))
y = np.sin(x) + np.random.normal(0, 0.3, n)
 
x_test = np.linspace(0, 2 * np.pi, 200)
y_test = np.sin(x_test)
 
plt.figure(figsize=(14, 10))
 
degrees = [1, 3, 10, 30]  # 多项式次数:1=线性,30=高次
train_errors = []
test_errors = []
 
for idx, degree in enumerate(degrees):
    model = make_pipeline(PolynomialFeatures(degree), LinearRegression())
    model.fit(x.reshape(-1, 1), y)
    
    y_train_pred = model.predict(x.reshape(-1, 1))
    y_test_pred = model.predict(x_test.reshape(-1, 1))
    
    train_error = np.mean((y - y_train_pred) ** 2)
    test_error = np.mean((y_test - y_test_pred) ** 2)
    train_errors.append(train_error)
    test_errors.append(test_error)
    
    plt.subplot(2, 2, idx + 1)
    plt.scatter(x, y, alpha=0.5, label='训练数据')
    plt.plot(x_test, y_test, 'g-', linewidth=2, label='真实函数')
    plt.plot(x_test, y_test_pred, 'r--', linewidth=2, label=f'预测 (degree={degree})')
    plt.title(f'多项式次数={degree}\n训练误差={train_error:.3f}, 测试误差={test_error:.3f}')
    plt.legend()
    plt.ylim(-2, 2)
 
plt.tight_layout()
plt.show()
 
# 绘制误差分解
plt.figure(figsize=(10, 5))
degrees_all = list(range(1, 31))
train_errs = []
test_errs = []
 
for d in degrees_all:
    model = make_pipeline(PolynomialFeatures(d), LinearRegression())
    model.fit(x.reshape(-1, 1), y)
    train_errs.append(np.mean((y - model.predict(x.reshape(-1, 1)))**2))
    test_errs.append(np.mean((y_test - model.predict(x_test.reshape(-1, 1)))**2))
 
plt.plot(degrees_all, train_errs, 'b-o', label='训练误差(偏差↓方差↑)')
plt.plot(degrees_all, test_errs, 'r-o', label='测试误差(过拟合)')
plt.axvline(x=3, color='green', linestyle='--', alpha=0.7, label='最优复杂度')
plt.xlabel('模型复杂度(多项式次数)')
plt.ylabel('均方误差')
plt.title('偏差-方差权衡:训练误差越低 ≠ 测试误差越低')
plt.legend()
plt.grid(True, alpha=0.3)
plt.yscale('log')
plt.show()

这个图完美展示了过拟合和欠拟合:

  • degree=1(欠拟合):偏差高,方差低,训练和测试误差都高
  • degree=30(过拟合):偏差低,方差高,训练误差低但测试误差爆炸
  • degree=3(刚好):偏差和方差达到平衡,测试误差最低

这就是机器学习中最重要的 tradeoff :模型太简单学不到规律(欠拟合),太复杂把噪声也学进去了(过拟合)。


十三、数据可视化:用matplotlib/seaborn展示统计结果

13.1 常用统计图表一览

做数据分析,可视化是展示结果的必备技能:

import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from scipy import stats
 
plt.style.use('seaborn-v0_8-whitegrid')
np.random.seed(42)
 
# 生成示例数据
tips = sns.load_dataset("tips")  # seaborn自带的小费数据集
print(tips.head())
 
fig, axes = plt.subplots(2, 3, figsize=(15, 10))
 
# 1. 直方图 + KDE:看分布
axes[0, 0].hist(tips['total_bill'], bins=20, edgecolor='black', alpha=0.7, density=True)
tips['total_bill'].plot.kde(ax=axes[0, 0], color='red', linewidth=2)
axes[0, 0].set_xlabel('账单金额')
axes[0, 0].set_title('账单金额分布(直方图+KDE)')
 
# 2. 箱线图:看分布特征和异常值
tips.boxplot(column='total_bill', by='day', ax=axes[0, 1])
axes[0, 1].set_title('不同日期的账单分布')
axes[0, 1].set_xlabel('')
 
# 3. 散点图 + 回归线:看两个变量的关系
axes[0, 2].scatter(tips['total_bill'], tips['tip'], alpha=0.5)
# 加回归线
slope, intercept, r, p, se = stats.linregress(tips['total_bill'], tips['tip'])
x_line = np.linspace(tips['total_bill'].min(), tips['total_bill'].max(), 100)
axes[0, 2].plot(x_line, slope * x_line + intercept, 'r-', linewidth=2)
axes[0, 2].set_xlabel('账单金额')
axes[0, 2].set_ylabel('小费')
axes[0, 2].set_title(f'账单 vs 小费 (r={r:.3f})')
 
# 4. 小提琴图:结合了箱线图和KDE的优点
sns.violinplot(data=tips, x='day', y='total_bill', ax=axes[1, 0])
axes[1, 0].set_title('小提琴图:看分布形状')
 
# 5. 热力图:看相关性矩阵
corr = tips[['total_bill', 'tip', 'size']].corr()
im = axes[1, 1].imshow(corr, cmap='coolwarm', vmin=-1, vmax=1)
axes[1, 1].set_xticks(range(3))
axes[1, 1].set_yticks(range(3))
axes[1, 1].set_xticklabels(['账单', '小费', '人数'], rotation=45)
axes[1, 1].set_yticklabels(['账单', '小费', '人数'])
for i in range(3):
    for j in range(3):
        axes[1, 1].text(j, i, f'{corr.iloc[i, j]:.2f}', ha='center', va='center', fontsize=12)
plt.colorbar(im, ax=axes[1, 1])
axes[1, 1].set_title('相关性热力图')
 
# 6. 分面图:用颜色区分多分类
colors = {'Male': 'blue', 'Female': 'red'}
for sex in tips['sex'].unique():
    subset = tips[tips['sex'] == sex]
    axes[1, 2].scatter(subset['total_bill'], subset['tip'], 
                        c=colors[sex], label=sex, alpha=0.5)
axes[1, 2].set_xlabel('账单金额')
axes[1, 2].set_ylabel('小费')
axes[1, 2].set_title('按性别分组的散点图')
axes[1, 2].legend()
 
plt.suptitle('常用统计图表', fontsize=16, y=1.02)
plt.tight_layout()
plt.show()

13.2 seaborn做高级可视化

seaborn是matplotlib的高级封装,几行代码就能做出很漂亮的图:

import seaborn as sns
import matplotlib.pyplot as plt
 
tips = sns.load_dataset("tips")
 
# 配对图:一次性看所有数值变量的关系
g = sns.pairplot(tips, hue='sex', diag_kind='kde', plot_kws={'alpha': 0.5})
g.fig.suptitle('配对图:所有数值变量的关系', y=1.02)
plt.show()
 
# 联合分布图:散点图 + 边缘分布
g = sns.jointplot(data=tips, x='total_bill', y='tip', kind='reg', height=8)
g.ax_joint.set_xlabel('账单金额')
g.ax_joint.set_ylabel('小费')
plt.show()
 
# 分类统计图
plt.figure(figsize=(12, 5))
 
plt.subplot(1, 2, 1)
sns.barplot(data=tips, x='day', y='tip', hue='sex', ci='sd', capsize=0.1)
plt.title('各日期小费均值(带误差棒)')
 
plt.subplot(1, 2, 2)
sns.pointplot(data=tips, x='day', y='tip', hue='sex', 
              errorbar='sd', capsize=0.1, dodge=True)
plt.title('各日期小费趋势点图')
 
plt.tight_layout()
plt.show()

十四、新手最常犯的统计错误

14.1 把相关性当成因果

这是排名第一的错误。“冰淇淋销量和溺水人数相关”不等于”吃冰淇淋导致溺水”。控制混淆变量或做随机实验才能建立因果关系。

14.2 忽视基线和样本量

一个药物试验让血压降低了10mmHg,显著吗?

  • 如果基线血压是180mmHg,降低10mmHg意义很大
  • 如果基线血压是120mmHg,降低10mmHg反而可能是危险的低血压

只看效应大小不够,还要看相对于基线的大小(效应量)。同时,p值显著性和样本量强相关:大样本下微小差异也会显著。

# 演示:样本量越大,任何微小差异都显著
np.random.seed(42)
 
effect = 0.01  # 真实效应很小
results = []
 
for n in [10, 50, 100, 500, 1000, 5000]:
    # 两组数据,真实均值差0.01
    group1 = np.random.normal(0, 1, n)
    group2 = np.random.normal(effect, 1, n)
    _, p = stats.ttest_ind(group1, group2)
    results.append((n, p))
 
print("样本量 vs p值(真实差异=0.01):")
for n, p in results:
    sig = "显著" if p < 0.05 else "不显著"
    print(f"n={n:5d}: p={p:.6f} ({sig})")

14.3 p值 hacking:挑数据直到显著

选了20个指标做检验,只有一个p<0.05,然后就宣布”发现了重要关系”——这是p值 hacking,是科研造假的重灾区。

解决方法:预注册分析计划,或者用Bonferroni校正控制多重比较。

# Bonferroni校正
n_tests = 20
alpha = 0.05
alpha_corrected = alpha / n_tests
print(f"校正前alpha: {alpha}")
print(f"校正后alpha: {alpha_corrected:.6f}")
print(f"只有p值 < {alpha_corrected:.6f} 才算显著")

14.4 忽视数据的分布假设

很多统计方法假设数据服从正态分布。做t检验前先看看数据分布:

from scipy import stats
 
data = np.random.exponential(scale=2, size=1000)  # 指数分布,明显不是正态
 
# Shapiro-Wilk正态性检验
stat, p = stats.shapiro(data[:50])  # 用子样本,大数据集会失效
print(f"Shapiro-Wilk检验 p值: {p:.6f}")
 
# 如果p < 0.05,说明数据显著偏离正态
# 此时应该用非参数检验(如Mann-Whitney U检验)
if p < 0.05:
    print("数据不服从正态分布,考虑用非参数检验")

14.5 混淆”不显著”和”没有效应”

p > 0.05 不代表没有效应,只代表没有足够证据证明有效应。这是两码事。

比如一个新药试验p=0.08,可能的原因是:

  • 样本量太小(统计功效不足)
  • 效应确实存在但很小
  • 测量误差太大
  • 真的没效应

不能因为p>0.05就轻易下结论说”没效果”。

14.6 忽视辛普森悖论

当数据被分层汇总时,总体的相关方向可能和每个分层内部的相关方向相反。这就是辛普森悖论。

# 辛普森悖论演示
# 系里有两个班:A班男生录取率低,B班男生录取率也低
# 但总体看男生录取率反而更高?
 
data = {
    'A班': {'男生': {'申请': 100, '录取': 60}, '女生': {'申请': 10, '录取': 5}},
    'B班': {'男生': {'申请': 10, '录取': 1}, '女生': {'申请': 100, '录取': 80}},
}
 
print("=== 辛普森悖论演示 ===")
for dept, groups in data.items():
    male_rate = groups['男生']['录取'] / groups['男生']['申请']
    female_rate = groups['女生']['录取'] / groups['女生']['申请']
    print(f"{dept}: 男生录取率={male_rate:.2%}, 女生录取率={female_rate:.2%}")
 
total_male = 100 + 10
total_female = 10 + 100
total_male_accepted = 60 + 1
total_female_accepted = 5 + 80
 
print(f"\n总体: 男生录取率={total_male_accepted/total_male:.2%}, 女生录取率={total_female_accepted/total_female:.2%}")
print("\n每个班女生录取率都更高,但总体反而男生录取率更高!")
print("原因:A班录取率高但女生少,B班录取率低但女生多")

做数据分析时,一定要注意数据分层,避免辛普森悖论。


十五、统计学派别之争:频率学派 vs 贝叶斯学派

15.1 哲学基础的分歧

统计学历史上最深刻的争论发生在频率学派(Frequentist)与贝叶斯学派(Bayesian)之间,这一争论的本质是对”概率”本质的不同理解。

频率学派将概率解释为长期频率。在这个框架下,未知参数 是固定的常数,“概率”只能应用于可重复随机实验的长期频率。置信区间的解释是:若重复抽样100次,约95次包含真实参数值。代表性人物包括Fisher、Neyman和Pearson。

贝叶斯学派将概率解释为主观信念度。参数 被视为随机变量,可以用概率分布描述。在观测数据前,参数服从先验分布 ;观测数据后,根据贝叶斯定理更新为后验分布

Jeffreys (1939) 的经典表述

“Every increase in knowledge may be supposed to decrease the entropy, that is to say, the disorder of our probability distribution.”

15.2 两种方法的形式化对比

频率学派方法

  • 目标:估计未知参数 的点值或构造置信区间
  • 工具:样本均值 、样本方差 、枢轴量
  • 评估标准:无偏性 、有效性(方差最小)、一致性

贝叶斯方法

  • 目标:获得参数的后验分布
  • 工具:贝叶斯定理、先验分布、后验预测分布
  • 评估标准:可信区间(直接给出参数落在某区间的概率)
方面频率学派贝叶斯学派
参数性质固定未知常数随机变量
概率解释长期频率主观信念
先验信息通常忽略显式编码
置信区间覆盖概率直接概率解释
计算复杂度通常较低通常较高

15.3 实用主义视角

现代统计实践中,两种方法各有优劣:

  • 频率学派优势:计算简单,不需要先验选择,客观性较强
  • 贝叶斯优势:自然融合先验信息,不确定性量化完整,可处理复杂层次模型

经验贝叶斯(Empirical Bayes)在两者之间架起桥梁:用数据来估计先验超参数,既保留了贝叶斯框架的灵活性,又减少了主观性。


十六、点估计与区间估计

16.1 点估计的基本概念

点估计是用一个具体数值 来估计未知参数 的方法。

评价标准

  1. 无偏性

    • 样本均值 是总体均值 的无偏估计
    • 样本方差 的无偏估计(注意除以 而非
  2. 有效性:在所有无偏估计中,方差最小者为有效估计

    • Cramér-Rao下界给出了无偏估计方差的下限
  3. 一致性:当样本量 时,

    • 一致性保证了估计随数据增加而趋近真实值
  4. 均方误差(MSE):

    • MSE统一衡量了估计的偏差和方差

16.2 矩估计法

矩估计法(Method of Moments, MoM)通过样本矩匹配总体矩来估计参数。

设总体有 个未知参数 ,令前 阶样本矩等于对应总体矩:

解此方程组即得参数估计。矩估计法简单直观,但通常不如MLE高效。

16.3 区间估计

区间估计给出参数的一个区间范围,同时声明对该区间可靠程度的信心。

置信区间的频率学派定义:

其中 是置信区间, 是置信水平(如95%)。

置信区间的常见误解

“95%置信区间”并不意味着”参数有95%的概率落在这个区间内”。正确的解释是:如果重复抽样100次构造置信区间,约95个区间会包含真实参数值。参数的取值是固定的,区间才是随机的。


十七、假设检验与p值(理论部分)

17.1 假设检验框架

假设检验是决定是否拒绝原假设 的统计方法。

  • 原假设 :通常表示”无效应”或”现状”
  • 备择假设 :研究者希望证明的命题
  • 检验统计量 :汇总数据的函数
  • 拒绝域:使 被拒绝的 值区域

两类错误

错误类型定义记作
第一类错误 为真但被拒绝$\alpha = P(\text{reject } H_0
第二类错误 为假但未拒绝$\beta = P(\text{fail to reject } H_0

势函数,描述检验在不同参数值下的拒绝概率。

17.2 p值的精确定义

p值是在原假设成立的前提下,观察到比实际数据更极端结果的概率:

p值的解读:

  • 小p值(如 ):在 成立时,观测数据是”小概率事件”,倾向于拒绝
  • 大p值:数据与 相符,无法拒绝

p值的常见误用

  1. p值不等于” 为真的概率”(那是后验概率)
  2. p值不能衡量效应大小或实际重要性
  3. p值具有”可复制性危机”:即使实验设计完美, 的结果中也约有32%是虚假的(假设检验的先验概率为50%)

17.3 似然比检验

似然比检验(Likelihood Ratio Test, LRT)是构造检验的通用方法:

Wilks定理表明,在一定正则条件下,当 成立且样本量足够大时:

其中 是约束参数的个数。


十八、最大似然估计(MLE)

18.1 MLE的定义与性质

最大似然估计是统计学中最重要的估计方法之一。设观测数据 来自密度 ,似然函数定义为:

对数似然为便于计算:

MLE定义为:

MLE的优良性质

  1. 一致性
  2. 渐近正态性
    • 其中 费舍尔信息
  3. 渐近有效性:在正则条件下,MLE达到Cramér-Rao下界
  4. 不变性:若 的MLE,则 的MLE

18.2 MLE的计算方法

解析求解:对数似然求导令其为零

数值优化:对于复杂模型,使用:

  • 牛顿-拉夫森法(Newton-Raphson)
  • Fisher得分法(Fisher Scoring)
  • EM算法(见下节)
  • 梯度下降法

正态分布参数的MLE

,则:

  • (样本均值)
  • (注意除以 而非 ,是有偏的)

虽然 有偏,但它仍是 的MLE。修正偏差后得到无偏估计


十九、EM算法详解

19.1 隐变量的引入

EM算法(Expectation-Maximization)专门用于处理隐变量(latent variable)模型。当数据中存在不可观测的隐变量 时,直接最大化似然函数往往困难重重。

观测数据的似然

直接计算这个积分通常不可行。

19.2 EM的两步迭代

E步(Expectation):计算隐变量后验分布下的期望对数似然

M步(Maximization):最大化 函数得到新参数

EM算法的关键洞察:直接优化 困难,但优化完整数据似然 加上隐变量的期望往往容易

19.3 EM的收敛性

EM算法单调收敛到局部最优解(不是全局最优)。每次迭代保证:

EM的几何直观

可以将EM视为在似然曲面上交替进行”上升”(E步找到更好的下界)和”最大化”(M步移动到下界的峰值)。这就是为什么EM属于坐标上升方法。

19.4 EM的变体

变体改进
GEM(广义EM)M步只需增加 函数值,不必全局最大化
ECM(期望条件最大化)在约束条件下交替最大化各参数
ECME(ECM扩展)使用更快的似然值更新
ECMM(期望条件最大化替代)使用不同的充分统计量更新
PX-EM(参数扩展EM)引入扩展参数空间加速收敛

硬币实验的EM算法

假设有两枚硬币A和B,正面概率不同。观测数据是混合的抛掷结果(不知道每次用哪枚硬币)。EM算法通过迭代:E步估计每次抛掷使用各硬币的概率,M步用这些概率更新硬币的正面概率。


二十、非参数统计与稳健统计

20.1 非参数估计方法

非参数统计不假设数据来自特定的参数化分布,提供了更灵活的推断工具。

核密度估计(Kernel Density Estimation, KDE):

给定样本 ,密度函数 的核估计为:

其中 是核函数(满足 ), 是带宽参数。

20.2 Bootstrap方法

Bootstrap通过重采样来估计统计量的分布。

经验Bootstrap

  1. 从原始样本 有放回地抽取 个样本
  2. 计算目标统计量
  3. 重复 次(如
  4. 的分布近似真实分布

Bootstrap的局限性

  • 不适用于小样本(
  • 当统计量的分布不稳健时效果差
  • 需要计算资源支持大量重采样

二十一、实战项目:用Python做完整的统计分析

21.1 项目背景

假设你是某电商公司的数据分析师,老板想知道:

  1. 不同用户群体的消费行为有什么差异?
  2. 促销活动对GMV有没有显著提升?
  3. 如何预测用户是否会购买某件商品?

我们用真实数据来演示完整的统计分析流程。

21.2 数据探索

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from scipy import stats
import warnings
warnings.filterwarnings('ignore')
 
plt.rcParams['font.sans-serif'] = ['Arial Unicode MS']
plt.rcParams['axes.unicode_minus'] = False
 
# 加载数据(模拟电商用户行为数据)
np.random.seed(42)
n_users = 5000
 
data = pd.DataFrame({
    'user_id': range(1, n_users + 1),
    'age': np.random.normal(35, 10, n_users).clip(18, 70).astype(int),
    'gender': np.random.choice(['男', '女'], n_users, p=[0.6, 0.4]),
    'city_level': np.random.choice(['一线', '二线', '三线'], n_users, p=[0.3, 0.4, 0.3]),
    'monthly_income': np.random.lognormal(10.5, 0.5, n_users).astype(int),
    'promotion_exposed': np.random.binomial(1, 0.5, n_users),
    'purchase_amount': np.where(
        np.random.binomial(1, 0.3, n_users) == 1,
        np.random.lognormal(7, 1, n_users).astype(int),
        0
    )
})
 
# 添加促销增效(使促销组的购买金额平均高20%)
promotion_effect = data['promotion_exposed'] * 0.2 * data['purchase_amount']
data['purchase_amount'] = (data['purchase_amount'] + promotion_effect).astype(int)
 
print("=== 数据概览 ===")
print(data.head())
print(f"\n数据形状: {data.shape}")
print(f"\n数据统计描述:")
print(data.describe())

21.3 分组比较分析

# 不同性别用户的消费差异
print("\n=== 性别与消费 ===")
gender_stats = data.groupby('gender')['purchase_amount'].agg(['mean', 'median', 'std', 'count'])
print(gender_stats)
 
# t检验:男性和女性消费有显著差异吗?
male_purchase = data[data['gender'] == '男']['purchase_amount']
female_purchase = data[data['gender'] == '女']['purchase_amount']
 
t_stat, p_value = stats.ttest_ind(male_purchase, female_purchase)
print(f"\nt检验结果: t={t_stat:.4f}, p={p_value:.4f}")
if p_value < 0.05:
    print("结论:男性和女性的消费存在显著差异")
else:
    print("结论:男性和女性的消费没有显著差异")

21.4 促销活动效果评估

# 促销活动效果分析
print("\n=== 促销活动效果 ===")
 
# 购买转化率
promo_converted = data[data['promotion_exposed'] == 1]['purchase_amount'] > 0
non_promo_converted = data[data['promotion_exposed'] == 0]['purchase_amount'] > 0
 
promo_rate = promo_converted.mean()
non_promo_rate = non_promo_converted.mean()
 
print(f"促销组购买率: {promo_rate:.2%}")
print(f"非促销组购买率: {non_promo_rate:.2%}")
 
# 卡方检验
from scipy.stats import chi2_contingency
contingency_table = pd.crosstab(data['promotion_exposed'], data['purchase_amount'] > 0)
chi2, p, dof, expected = chi2_contingency(contingency_table)
print(f"\n卡方检验: χ²={chi2:.4f}, p={p:.6f}")
 
# 购买金额对比
promo_amount = data[data['promotion_exposed'] == 1]['purchase_amount']
non_promo_amount = data[data['promotion_exposed'] == 0]['purchase_amount']
 
t_stat, p_value = stats.ttest_ind(promo_amount, non_promo_amount)
print(f"金额对比t检验: t={t_stat:.4f}, p={p_value:.6f}")
 
# 可视化
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
 
# 箱线图:促销 vs 非促销
data.boxplot(column='purchase_amount', by='promotion_exposed', ax=axes[0])
axes[0].set_xlabel('是否接触促销 (0=否, 1=是)')
axes[0].set_ylabel('购买金额')
axes[0].set_title('促销对购买金额的影响')
plt.sca(axes[0])
plt.xticks([1, 2], ['否', '是'])
 
# 柱状图:购买转化率
rates = [non_promo_rate, promo_rate]
axes[1].bar(['非促销', '促销'], rates, color=['steelblue', 'coral'])
axes[1].set_ylabel('购买转化率')
axes[1].set_title('促销对购买转化的影响')
for i, v in enumerate(rates):
    axes[1].text(i, v + 0.01, f'{v:.1%}', ha='center')
 
# 分组对比
city_promo = data.groupby(['city_level', 'promotion_exposed'])['purchase_amount'].mean().unstack()
city_promo.plot(kind='bar', ax=axes[2])
axes[2].set_xlabel('城市等级')
axes[2].set_ylabel('平均购买金额')
axes[2].set_title('不同城市等级的促销效果')
axes[2].legend(['非促销', '促销'])
axes[2].tick_params(axis='x', rotation=0)
 
plt.tight_layout()
plt.show()

21.5 用户购买预测建模

from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report, confusion_matrix, roc_curve, auc
 
# 准备数据
data['is_purchased'] = (data['purchase_amount'] > 0).astype(int)
X = data[['age', 'monthly_income']]
y = data['is_purchased']
 
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
 
# 训练逻辑回归模型
model = LogisticRegression(random_state=42)
model.fit(X_train, y_train)
 
# 预测
y_pred = model.predict(X_test)
y_pred_proba = model.predict_proba(X_test)[:, 1]
 
# 评估
print("\n=== 逻辑回归模型评估 ===")
print(classification_report(y_test, y_pred))
 
# 混淆矩阵可视化
cm = confusion_matrix(y_test, y_pred)
plt.figure(figsize=(8, 6))
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues')
plt.xlabel('预测值')
plt.ylabel('真实值')
plt.title('混淆矩阵')
plt.show()
 
# ROC曲线
fpr, tpr, thresholds = roc_curve(y_test, y_pred_proba)
roc_auc = auc(fpr, tpr)
 
plt.figure(figsize=(8, 6))
plt.plot(fpr, tpr, color='darkorange', lw=2, label=f'ROC曲线 (AUC = {roc_auc:.3f})')
plt.plot([0, 1], [0, 1], color='navy', lw=2, linestyle='--')
plt.xlabel('假阳性率')
plt.ylabel('真阳性率')
plt.title('ROC曲线')
plt.legend(loc="lower right")
plt.show()

21.6 分析报告总结

print("""
=== 数据分析报告摘要 ===
 
一、数据概况
- 总用户数: {:,}
- 平均年龄: {:.1f}
- 平均月收入: {:,.0f}
- 总体购买率: {:.1%}
 
二、关键发现
 
1. 用户画像
   - 男性用户占比 {:.1%}
   - 一线城市用户占比 {:.1%}
   - 平均购买金额: {:.0f}
 
2. 促销效果
   - 促销组购买率: {:.1%} vs 非促销组: {:.1%}
   - 促销提升购买率: {:.1%}个百分点
   - 促销活动效果显著 (p < 0.05)
 
3. 预测模型
   - 模型AUC: {:.3f}
   - 年龄和收入是购买的重要预测因子
 
三、建议
 
1. 继续加大促销活动投入,促销ROI明显
2. 针对高收入年轻用户群体精准营销
3. 一线城市用户转化率更高,可优先投放资源
 
四、分析局限性
 
1. 数据为模拟数据,可能与实际情况有差异
2. 未考虑季节性因素和竞品影响
3. 因果推断需进一步实验验证
""".format(
    n_users,
    data['age'].mean(),
    data['monthly_income'].mean(),
    data['is_purchased'].mean(),
    (data['gender'] == '男').mean(),
    (data['city_level'] == '一线').mean(),
    data['purchase_amount'].mean(),
    promo_rate,
    non_promo_rate,
    promo_rate - non_promo_rate,
    roc_auc
))

二十二、常见统计分布速查表

22.1 分布速查

分布适用场景关键参数sciphy函数
正态 N(μ,σ²)测量误差、身高、考试成绩μ=均值, σ²=方差norm.pdf(x, μ, σ)
二项 Bin(n,p)n次试验成功次数n=次数, p=成功概率binom.pmf(k, n, p)
泊松 Poi(λ)单位时间事件次数λ=平均发生率poisson.pmf(k, λ)
指数 Exp(λ)事件间隔时间λ=率参数expon.pdf(x, scale=1/λ)
t分布小样本均值推断df=自由度t.pdf(x, df)
χ²分布方差推断、拟合优度df=自由度chi2.pdf(x, df)
F分布方差比较d1, d2=自由度f.pdf(x, d1, d2)
Beta(a,b)概率的先验/后验a,b=形状参数beta.pdf(x, a, b)
Gamma(a,b)非负变量建模a=形状, b=率gamma.pdf(x, a, scale=1/b)

22.2 分布可视化

import numpy as np
import matplotlib.pyplot as plt
from scipy import stats
 
x = np.linspace(-5, 5, 1000)
 
fig, axes = plt.subplots(2, 3, figsize=(15, 10))
 
# 正态分布
for mu, sigma in [(0, 1), (0, 0.5), (1, 1)]:
    axes[0, 0].plot(x, stats.norm.pdf(x, mu, sigma), label=f'μ={mu}, σ={sigma}')
axes[0, 0].set_title('正态分布')
axes[0, 0].legend()
 
# t分布 vs 正态
axes[0, 1].plot(x, stats.norm.pdf(x), 'b-', label='正态', linewidth=2)
for df in [5, 10, 30]:
    axes[0, 1].plot(x, stats.t.pdf(x, df), '--', label=f't(df={df})')
axes[0, 1].set_title('t分布 vs 正态分布')
axes[0, 1].legend()
 
# 卡方分布
for df in [2, 5, 10]:
    axes[0, 2].plot(x[x > 0], stats.chi2.pdf(x[x > 0], df), label=f'df={df}')
axes[0, 2].set_title('卡方分布')
axes[0, 2].legend()
 
# 二项分布
for n, p in [(10, 0.5), (20, 0.3), (50, 0.1)]:
    k = np.arange(0, n + 1)
    axes[1, 0].plot(k, stats.binom.pmf(k, n, p), 'o-', label=f'n={n}, p={p}')
axes[1, 0].set_title('二项分布')
axes[1, 0].legend()
 
# 泊松分布
for lam in [2, 5, 10]:
    k = np.arange(0, 30)
    axes[1, 1].plot(k, stats.poisson.pmf(k, lam), 'o-', label=f'λ={lam}')
axes[1, 1].set_title('泊松分布')
axes[1, 1].legend()
 
# Beta分布
for a, b in [(0.5, 0.5), (1, 1), (2, 5), (5, 2)]:
    x_b = np.linspace(0.01, 0.99, 100)
    axes[1, 2].plot(x_b, stats.beta.pdf(x_b, a, b), label=f'a={a}, b={b}')
axes[1, 2].set_title('Beta分布')
axes[1, 2].legend()
 
plt.tight_layout()
plt.show()

二十三、统计学速查公式卡片

23.1 描述性统计

均值: x̄ = Σxᵢ / n
方差: s² = Σ(xᵢ - x̄)² / (n-1)
标准差: s = √s²
中位数: 排序后第n/2个数(偶数时取平均)
四分位距: IQR = Q3 - Q1
相关系数: r = Σ(xᵢ-x̄)(yᵢ-ȳ) / √[Σ(xᵢ-x̄)²Σ(yᵢ-ȳ)²]

23.2 推断统计

标准误差: SE = s / √n
z分数: z = (x - μ) / σ
t统计量: t = (x̄ - μ₀) / (s/√n)
置信区间: x̄ ± tₐ/₂,ₙ₋₁ × s/√n

23.3 假设检验决策树

开始
  ↓
数据类型是?
  ├── 连续变量
  │     ├── 比较1组 vs 已知值 → 单样本t检验
  │     ├── 比较2组配对 → 配对t检验
  │     ├── 比较2组独立 → 独立t检验
  │     └── 比较≥3组 → ANOVA
  │
  └── 分类变量
        ├── 比较2组比例 → z检验
        ├── 比较2×2列联表 → 卡方检验
        └── 比较≥2×2表 → 卡方检验

参考文献

  1. Lehmann, E. L., & Casella, G. (1998). Theory of Point Estimation (2nd ed.). Springer.
  2. Casella, G., & Berger, R. L. (2002). Statistical Inference (2nd ed.). Duxbury Press.
  3. Gelman, A., Carlin, J. B., Stern, H. S., Dunson, D. B., Vehtari, A., & Rubin, D. B. (2013). Bayesian Data Analysis (3rd ed.). CRC Press.
  4. McLachlan, G. J., & Krishnan, T. (2008). The EM Algorithm and Extensions (2nd ed.). Wiley.
  5. Wasserman, L. (2010). All of Statistics: A Concise Course in Statistical Inference. Springer.
  6. Vapnik, V. N. (1998). Statistical Learning Theory. Wiley.
  7. Pearl, J. (2009). Causality: Models, Reasoning, and Inference (2nd ed.). Cambridge University Press.
  8. Friedman, J., Hastie, T., & Tibshirani, R. (2001). The Elements of Statistical Learning (2nd ed.). Springer.
  9. Efron, B., & Tibshirani, R. J. (1994). An Introduction to the Bootstrap. CRC Press.
  10. ISRS. (2021). International Statistics Literacy Standards. International Association for Statistical Education.

相关文档