github star gitee star atomgit star PyPI Downloads AI 编程 AI 交流群

大家好,我是正在实战各种AI项目的程序员晚枫。

今天学习数据分布分析,重点掌握如何判断数据是否符合正态分布。

为什么重要?因为很多统计方法(如t检验)都假设数据正态分布。用错方法会导致错误结论。


什么是正态分布?

特征

  • 钟形曲线,对称分布
  • 均值 = 中位数 = 众数
  • 约68%数据在μ±σ内,95%在μ±2σ内

为什么要检验正态性?

  • t检验、ANOVA要求正态分布
  • 线性回归假设残差正态
  • 很多机器学习算法对分布敏感

可视化判断

直方图 + 密度曲线

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import numpy as np
import matplotlib.pyplot as plt
from scipy import stats

# 生成三种分布的数据
np.random.seed(42)
normal_data = np.random.normal(100, 15, 1000) # 正态
skewed_data = np.random.exponential(2, 1000) # 右偏
uniform_data = np.random.uniform(0, 100, 1000) # 均匀

fig, axes = plt.subplots(1, 3, figsize=(15, 4))

data_list = [normal_data, skewed_data, uniform_data]
titles = ['正态分布', '指数分布(右偏)', '均匀分布']

for ax, data, title in zip(axes, data_list, titles):
ax.hist(data, bins=30, density=True, alpha=0.7, edgecolor='black')

# 叠加理论正态曲线
mu, sigma = data.mean(), data.std()
x = np.linspace(data.min(), data.max(), 100)
ax.plot(x, stats.norm.pdf(x, mu, sigma), 'r-', linewidth=2, label='正态拟合')

ax.set_title(title)
ax.legend()

plt.tight_layout()
plt.show()

QQ图(Quantile-Quantile Plot)

原理

将数据的分位数与理论正态分布的分位数对比。如果点在一条直线上,说明符合正态分布。

Python实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import statsmodels.api as sm
import matplotlib.pyplot as plt

fig, axes = plt.subplots(1, 3, figsize=(15, 4))

for ax, data, title in zip(axes, data_list, titles):
sm.qqplot(data, line='45', ax=ax)
ax.set_title(f'{title} - QQ图')

plt.tight_layout()
plt.show()

# 解读:
# 点落在对角线上 → 正态分布
# 点呈S形 → 厚尾或薄尾
# 点呈弧形 → 偏态分布

统计检验

Shapiro-Wilk检验(小样本推荐)

1
2
3
4
5
6
7
8
9
10
11
from scipy import stats

# 正态数据
stat, p_value = stats.shapiro(normal_data[:500]) # 最多500个样本
print(f"正态数据 - W统计量: {stat:.4f}, p值: {p_value:.6f}")

# 非正态数据
stat, p_value = stats.shapiro(skewed_data[:500])
print(f"偏态数据 - W统计量: {stat:.4f}, p值: {p_value:.6f}")

# 解读:p > 0.05不能拒绝正态性假设

Kolmogorov-Smirnov检验

1
2
3
# KS检验
stat, p_value = stats.kstest(normal_data, 'norm', args=(normal_data.mean(), normal_data.std()))
print(f"KS检验 - 统计量: {stat:.4f}, p值: {p_value:.6f}")

Anderson-Darling检验

1
2
3
4
5
6
result = stats.anderson(normal_data, dist='norm')
print(f"A-D统计量: {result.statistic:.4f}")
print("临界值:", result.critical_values)
print("显著性水平:", result.significance_level)

# 如果统计量 > 临界值,拒绝正态性

偏度和峰度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from scipy import stats

# 计算偏度和峰度
skewness = stats.skew(normal_data)
kurtosis = stats.kurtosis(normal_data) # 超额峰度(减去3)

print(f"偏度: {skewness:.4f}")
print(f"峰度: {kurtosis:.4f}")

# 判断标准(近似)
if abs(skewness) < 0.5 and abs(kurtosis) < 0.5:
print("大致符合正态分布")
else:
print("偏离正态分布")

数据变换

当数据不正态时,可以尝试变换:

对数变换(右偏数据)

1
2
3
4
5
6
log_data = np.log(skewed_data[skewed_data > 0])

# 检查变换后是否正态
stats.probplot(log_data, dist="norm", plot=plt)
plt.title("对数变换后的QQ图")
plt.show()

平方根变换

1
sqrt_data = np.sqrt(skewed_data[skewed_data >= 0])

Box-Cox变换(自动选择最优变换)

1
2
3
4
5
6
7
8
9
10
from scipy.stats import boxcox

# Box-Cox要求数据为正
data_positive = skewed_data[skewed_data > 0]
boxcox_data, lambda_param = boxcox(data_positive)

print(f"最优lambda参数: {lambda_param:.4f}")
# lambda ≈ 0: 对数变换
# lambda ≈ 0.5: 平方根变换
# lambda ≈ 1: 无需变换

实战:完整分析流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from scipy import stats
import statsmodels.api as sm

def normality_analysis(data, name="数据"):
"""完整的正态性分析"""

print(f"\n=== {name}的正态性分析 ===")

# 1. 基础统计
print(f"\n【描述统计】")
print(f" 样本量: {len(data)}")
print(f" 均值: {np.mean(data):.2f}")
print(f" 中位数: {np.median(data):.2f}")
print(f" 标准差: {np.std(data, ddof=1):.2f}")
print(f" 偏度: {stats.skew(data):.4f}")
print(f" 峰度: {stats.kurtosis(data):.4f}")

# 2. Shapiro-Wilk检验
if len(data) <= 5000:
stat, p_value = stats.shapiro(data)
print(f"\n【Shapiro-Wilk检验】")
print(f" W统计量: {stat:.4f}")
print(f" p值: {p_value:.6f}")
if p_value > 0.05:
print(f" ✓ 不能拒绝正态性假设(p > 0.05)")
else:
print(f" ✗ 拒绝正态性假设(p ≤ 0.05)")

# 3. 可视化
fig, axes = plt.subplots(1, 2, figsize=(12, 4))

# 直方图+密度
axes[0].hist(data, bins=30, density=True, alpha=0.7, edgecolor='black')
mu, sigma = data.mean(), data.std()
x = np.linspace(data.min(), data.max(), 100)
axes[0].plot(x, stats.norm.pdf(x, mu, sigma), 'r-', linewidth=2, label='正态分布')
axes[0].set_title(f'{name} - 分布直方图')
axes[0].legend()

# QQ图
sm.qqplot(data, line='45', ax=axes[1])
axes[1].set_title(f'{name} - QQ图')

plt.tight_layout()
plt.savefig(f'{name}_normality.png', dpi=300)
plt.show()

return {
'mean': np.mean(data),
'std': np.std(data, ddof=1),
'skewness': stats.skew(data),
'kurtosis': stats.kurtosis(data),
'shapiro_p': p_value if len(data) <= 5000 else None
}

# 测试不同分布
np.random.seed(42)
normal = np.random.normal(100, 15, 1000)
exponential = np.random.exponential(2, 1000)
uniform = np.random.uniform(50, 150, 1000)

results = []
for data, name in [(normal, "正态数据"), (exponential, "指数数据"), (uniform, "均匀数据")]:
result = normality_analysis(data, name)
results.append(result)

# 汇总比较
summary = pd.DataFrame(results, index=["正态", "指数", "均匀"])
print("\n=== 汇总比较 ===")
print(summary)

性能对比:正态性检验方法

1
2
3
4
5
6
7
8
9
10
11
12
13
from scipy import stats
import numpy as np

data = np.random.randn(10000)

# Shapiro-Wilk(小样本首选,n<5000)
%timeit stats.shapiro(data[:5000]) # 约5ms

# D'Agostino-Pearson(大样本)
%timeit stats.normaltest(data) # 约2ms

# Kolmogorov-Smirnov
%timeit stats.kstest(data, 'norm') # 约1ms

进阶用法

QQ图详解

1
2
3
4
5
6
7
8
9
10
import statsmodels.api as sm
import matplotlib.pyplot as plt

# 标准QQ图
sm.qqplot(data, line='45', fit=True)
plt.title('QQ图:检验正态性')

# 如果点基本在45度线上 → 数据近似正态
# 如果两端偏离 → 有重尾或轻尾
# 如果整体弯曲 → 偏态分布

偏度和峰度

1
2
3
4
5
6
7
8
from scipy import stats

skewness = stats.skew(data) # 偏度:0=对称,>0=右偏,<0=左偏
kurtosis = stats.kurtosis(data) # 峰度:0=正态,>0=尖峰,<0=平峰

print(f"偏度: {skewness:.3f}")
print(f"峰度: {kurtosis:.3f}")
# 正态分布:偏度≈0,峰度≈0

常见分布拟合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 拟合多种分布,找最合适的
from scipy import stats

distributions = ['norm', 'lognorm', 'expon', 'gamma', 'beta']
results = {}

for dist_name in distributions:
dist = getattr(stats, dist_name)
params = dist.fit(data)
# KS检验
D, p = stats.kstest(data, dist_name, args=params)
results[dist_name] = {'D': D, 'p': p, 'params': params}

best = min(results, key=lambda x: results[x]['D'])
print(f"最佳拟合分布: {best}")

避坑指南

❌ 坑1:大样本下正态检验太敏感

1
2
3
4
5
# 10万条数据,轻微偏离正态就会被拒绝
stats.shapiro(data[:5000]) # 大样本几乎都拒绝正态

# 解决方案:看QQ图 + 实际偏度峰度
# 偏度|<2|, 峰度|<7| → 大致正态,很多统计方法仍然适用

❌ 坑2:忘记检查分布就用了参数检验

1
2
3
4
5
6
7
8
# t检验假设数据正态分布
# 如果数据严重偏态,应该用非参数检验

# 参数检验:t检验
stats.ttest_ind(group_a, group_b)

# 非参数替代:Mann-Whitney U检验
stats.mannwhitneyu(group_a, group_b)

实战案例:分析用户消费金额分布

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import pandas as pd
import numpy as np
from scipy import stats
import matplotlib.pyplot as plt

np.random.seed(42)
n = 10000

# 消费金额通常服从对数正态分布
raw = np.random.lognormal(mean=7, sigma=1.2, size=n)
df = pd.DataFrame({'spending': raw})

print("=== 消费金额分布分析 ===")
print(f"均值: ¥{df['spending'].mean():,.0f}")
print(f"中位数: ¥{df['spending'].median():,.0f}")
print(f"偏度: {df['spending'].skew():.2f}(右偏)")
print(f"峰度: {df['spending'].kurt():.2f}(尖峰)")

# 正态性检验
stat, p = stats.normaltest(df['spending'])
print(f"\n正态检验: p={p:.2e}{'拒绝正态' if p < 0.05 else '不能拒绝正态'}")

# 对数变换后检验
log_spending = np.log(df['spending'])
stat2, p2 = stats.normaltest(log_spending)
print(f"对数变换后正态检验: p={p2:.4f}{'近似正态' if p2 > 0.05 else '仍非正态'}")

# 分布拟合
from scipy import stats as sp_stats
params = sp_stats.lognorm.fit(df['spending'])
print(f"\n对数正态拟合参数: s={params[0]:.2f}, loc={params[1]:.2f}, scale={params[2]:.2f}")

常见分布及其Python实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import numpy as np
from scipy import stats

# 1. 正态分布 - 自然界最常见的分布
norm_data = np.random.normal(loc=0, scale=1, size=10000) # 均值0, 标准差1

# 2. 对数正态分布 - 收入、房价等
lognorm_data = np.random.lognormal(mean=0, sigma=1, size=10000)

# 3. 指数分布 - 等待时间
exp_data = np.random.exponential(scale=1, size=10000)

# 4. 泊松分布 - 计数数据(订单数、访问量)
poisson_data = np.random.poisson(lam=5, size=10000)

# 5. 均匀分布 - 随机数
uniform_data = np.random.uniform(low=0, high=1, size=10000)

# 6. 二项分布 - 成功/失败(转化率)
binom_data = np.random.binomial(n=100, p=0.3, size=10000)

# 可视化对比
import matplotlib.pyplot as plt
fig, axes = plt.subplots(2, 3, figsize=(15, 8))
for ax, data, name in zip(axes.flat,
[norm_data, lognorm_data, exp_data, poisson_data, uniform_data, binom_data],
['正态', '对数正态', '指数', '泊松', '均匀', '二项']):
ax.hist(data, bins=50, density=True, alpha=0.7)
ax.set_title(name)
plt.tight_layout()

Box-Cox变换(让数据更接近正态)

1
2
3
4
5
6
7
8
9
10
11
12
13
from scipy import stats

# 对偏态数据做Box-Cox变换
original_data = np.random.lognormal(0, 1.5, 1000)
transformed, lambda_val = stats.boxcox(original_data)

print(f"变换前偏度: {stats.skew(original_data):.2f}")
print(f"变换后偏度: {stats.skew(transformed):.2f}")
print(f"最优lambda: {lambda_val:.3f}")

# 反变换
from scipy.special import inv_boxcox
restored = inv_boxcox(transformed, lambda_val)

分布分析工作流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import pandas as pd
import numpy as np
from scipy import stats
import matplotlib.pyplot as plt

def distribution_analysis(data, name='数据'):
'''完整的分布分析流程'''
print(f"=== {name}分布分析 ===
")

# 1. 基本统计
print(f"均值: {data.mean():.2f}")
print(f"中位数: {data.median():.2f}")
print(f"标准差: {data.std():.2f}")
print(f"偏度: {data.skew():.2f} ({'右偏' if data.skew() > 0 else '左偏' if data.skew() < 0 else '对称'})")
print(f"峰度: {data.kurt():.2f} ({'尖峰' if data.kurt() > 0 else '平峰' if data.kurt() < 0 else '正态'})")

# 2. 正态性检验
if len(data) <= 5000:
stat, p = stats.shapiro(data)
test_name = 'Shapiro-Wilk'
else:
stat, p = stats.normaltest(data)
test_name = "D'Agostino"

print(f"
正态性检验({test_name}): p={p:.4f}")
print(f"结论: {'正态分布' if p > 0.05 else '非正态分布'}")

# 3. 可视化
fig, axes = plt.subplots(1, 3, figsize=(15, 4))

# 直方图 + KDE
axes[0].hist(data, bins=30, density=True, alpha=0.7)
data.plot.kde(ax=axes[0])
axes[0].set_title('分布图')

# QQ图
stats.probplot(data, plot=axes[1])
axes[1].set_title('QQ图')

# 箱线图
axes[2].boxplot(data)
axes[2].set_title('箱线图')

plt.tight_layout()
plt.savefig(f'{name}_distribution.png', dpi=150)

return {'p_value': p, 'is_normal': p > 0.05}

# 使用
result = distribution_analysis(df['salary'], '薪资')

数据变换参考

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 不同偏度的推荐变换方法
def auto_transform(data):
'''根据偏度自动选择变换方法'''
skew = data.skew()

if abs(skew) < 0.5:
print("近似正态,无需变换")
return data
elif skew > 0.5:
print("右偏分布,推荐对数变换")
if (data > 0).all():
return np.log(data)
else:
return np.log1p(data - data.min() + 1)
else:
print("左偏分布,推荐平方变换")
return data ** 2

下节预告

下一课我们将进入实战项目部分,第一个项目是销售数据分析报表。

👉 继续阅读:项目1-销售数据分析报表自动化


💬 加入学习交流群

扫码加入Python学习交流群,和数千名同学一起进步:

👉 点击加入交流群

群里不定期分享:

  • 数据分析实战案例
  • Python学习资料
  • 求职面试经验
  • 行业最新动态

推荐:AI Python数据分析实战营

🎁 限时福利:送《利用Python进行数据分析》实体书

👉 点击了解详情


课程导航

上一篇: 假设检验入门-用数据验证你的猜想

下一篇: 项目1-销售数据分析报表自动化


PS:QQ图是判断正态分布最直观的方法。记住:点越接近对角线,越像正态分布。



📚 推荐教材

主教材《Excel+Python 飞速搞定数据分析与处理(图灵出品)》

💬 联系我

平台账号/链接
微信扫码加好友
微博@程序员晚枫
知乎@程序员晚枫
抖音@程序员晚枫
小红书@程序员晚枫
B 站Python 自动化办公社区

主营业务:AI 编程培训、企业内训、技术咨询

🎓 AI 编程实战课程

想系统学习 AI 编程?程序员晚枫的 AI 编程实战课 帮你从零上手!