所有文章 > AI驱动 > 正态性检验:从Q-Q图到Shapiro-Wilk的全面解析数据是否呈现正态分布

正态性检验:从Q-Q图到Shapiro-Wilk的全面解析数据是否呈现正态分布

背景

正态分布是统计学中的一种连续概率分布,也称为高斯分布,其概率密度函数呈钟形曲线,正态分布有以下几个重要特点:

  • 均值(  )为中心,数据在均值左右对称分布
  • 标准差(  )决定了分布的宽度,较小的标准差使分布更窄,较大的标准差使分布更宽
  • 分布的总面积为1,即所有可能取值的概率之和为1

正态分布的概率密度函数可以表示为:

其中,  为均值,  是方差

数据呈正态分布作用

  • 统计假设前提:许多统计方法(如t检验、ANOVA)要求数据正态分布,否则结果可能失效
  • 模型性能:线性回归等模型在处理正态分布数据时表现更佳,解释性更强
  • 特征处理:正态分布的数据更适合标准化和正则化,提升模型的稳定性和泛化能力
  • 异常值检测:正态性检验有助于识别异常值,清理数据以减少干扰
  • 算法表现:K-Means等算法对正态分布数据效果更好,深度学习也通过正态化加速收敛
  • 树模型例外:决策树类算法对正态分布不敏感,但正态数据有助于更均衡的特征划分

正态性检验是数据分析和机器学习中的重要步骤,判断数据是否呈正态分布不仅可以帮助选择合适的算法和方法,还能提升模型的性能和可解释性,如果数据不符合正态分布,可以考虑通过数据变换(如对数变换、Box-Cox变换)来改善分布特征,参考文章——特征工程——数据转换

正态分布检验方法

正态分布检验用于判断给定的数据是否来自正态分布。常见的正态性检验方法包括:

图形法

  • Q-Q图:将样本的分位数与正态分布的理论分位数进行比较。如果样本点大致沿直线分布,说明数据接近正态分布
  • P-P图:将样本的累积分布函数(CDF)与理论分布的CDF进行比较
  • 直方图:将数据的直方图与正态分布的理论密度曲线进行对比,看是否呈现钟形

统计检验法

  • Shapiro-Wilk检验:一种广泛使用的正态性检验,假设数据来自正态分布,如果p值较小(通常小于0.05),则拒绝数据来自正态分布的假设
  • Kolmogorov-Smirnov检验:用于比较样本的经验分布与理论正态分布,适合较大样本,但对均值和方差的敏感性较弱
  • Anderson-Darling检验:对分布尾部有较高的灵敏度,是Shapiro-Wilk检验的加强版
  • Jarque-Bera检验:基于样本的偏度和峰度,检验数据是否符合正态分布。适用于大样本
  • D’Agostino’s K-squared检验:基于偏度和峰度的综合检验方法,适合较大的样本

不同检验方法对数据的敏感度不同,选择时可以根据样本量和对正态性的严格要求程度来确定

代码实现

数据生成

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
plt.rcParams['font.family'] = 'Times New Roman'
plt.rcParams['axes.unicode_minus'] = False
np.random.seed(42)

# 生成一列正态分布数据,均值为0,标准差为1,数据量为1000
normal_data = np.random.normal(loc=0, scale=1, size=1000)
# 生成一列非正态分布数据(例如从均匀分布生成)
non_normal_data = np.random.uniform(low=-2, high=2, size=1000)

data = pd.DataFrame({
'Normal_Distribution': normal_data,
'Non_Normal_Distribution': non_normal_data
})
data

生成两列数据,一列为正态分布,另一列为非正态分布,接下来根据这些数据进行数据正态分布检验实现

Q-Q图

import scipy.stats as stats
# 创建Q-Q图函数
def plot_qq(data, title):
stats.probplot(data, dist="norm", plot=plt)
plt.title(title)
plt.grid(True)
# 绘制两列数据的Q-Q图
plt.figure(figsize=(12, 6),dpi=1200)
plt.subplot(1, 2, 1)
plot_qq(data['Normal_Distribution'], 'Q-Q Plot of Normal Distribution')
plt.subplot(1, 2, 2)
plot_qq(data['Non_Normal_Distribution'], 'Q-Q Plot of Non-Normal Distribution')
plt.tight_layout()
plt.savefig("Q-Q.pdf", format='pdf',bbox_inches='tight')
plt.show()

左侧图是正态分布数据的Q-Q图,点大致沿着一条直线分布,说明这列数据接近正态分布,右侧图是非正态分布数据的Q-Q图,点偏离直线较明显,说明这列数据不符合正态分布

P-P图

# 创建P-P图函数
def plot_pp(data, title):
sorted_data = np.sort(data)
cdf = stats.norm.cdf(sorted_data, np.mean(sorted_data), np.std(sorted_data))
plt.plot(cdf, np.linspace(0, 1, len(data)), marker='o', linestyle='', markersize=3)
plt.plot([0, 1], [0, 1], 'r--')
plt.title(title)
plt.xlabel('Theoretical CDF')
plt.ylabel('Empirical CDF')
plt.grid(True)

# 绘制两列数据的P-P图
plt.figure(figsize=(12, 6),dpi=1200)

plt.subplot(1, 2, 1)
plot_pp(data['Normal_Distribution'], 'P-P Plot of Normal Distribution')

plt.subplot(1, 2, 2)
plot_pp(data['Non_Normal_Distribution'], 'P-P Plot of Non-Normal Distribution')
plt.tight_layout()
plt.savefig("P-P.pdf", format='pdf',bbox_inches='tight')
plt.show()

左侧是正态分布数据的P-P图,数据点与理论的CDF曲线大致符合,说明这列数据接近正态分布,右侧是非正态分布数据的P-P图,数据点明显偏离理论的CDF曲线,说明这列数据不符合正态分布

直方图

# 绘制直方图函数
def plot_hist(data, title):
plt.hist(data, bins=30, density=True, alpha=0.6, color='g')
# 绘制正态分布曲线
mu, std = np.mean(data), np.std(data)
xmin, xmax = plt.xlim()
x = np.linspace(xmin, xmax, 100)
p = stats.norm.pdf(x, mu, std)
plt.plot(x, p, 'k', linewidth=2)
plt.title(title)
plt.grid(True)

# 绘制两列数据的直方图
plt.figure(figsize=(12, 6), dpi=1200)

plt.subplot(1, 2, 1)
plot_hist(data['Normal_Distribution'], 'Histogram of Normal Distribution')

plt.subplot(1, 2, 2)
plot_hist(data['Non_Normal_Distribution'], 'Histogram of Non-Normal Distribution')

plt.tight_layout()
plt.savefig("Histogram.pdf", format='pdf',bbox_inches='tight')
plt.show()

左侧是正态分布数据的直方图,其形状接近对称的钟形,并且与叠加的正态分布曲线高度吻合,说明数据符合正态分布,右侧是非正态分布数据的直方图,其形状较为平坦,未呈现正态分布的钟形曲线,说明数据不符合正态分布

Shapiro-Wilk检验

# 进行Shapiro-Wilk检验
shapiro_normal = stats.shapiro(data['Normal_Distribution'])
shapiro_non_normal = stats.shapiro(data['Non_Normal_Distribution'])

shapiro_results = {
'Normal_Distribution': {
'Statistic': shapiro_normal.statistic,
'p-value': shapiro_normal.pvalue
},
'Non_Normal_Distribution': {
'Statistic': shapiro_non_normal.statistic,
'p-value': shapiro_non_normal.pvalue
}
}

shapiro_results

正态分布数据:统计量为 0.9986,p 值为 0.6265,p 值大于 0.05,因此不能拒绝数据符合正态分布的假设,非正态分布数据:统计量为 0.9543,p 值非常小,p 值远小于 0.05,因此可以拒绝数据符合正态分布的假设

Kolmogorov-Smirnov检验

# 进行Kolmogorov-Smirnov检验
ks_normal = stats.kstest(data['Normal_Distribution'], 'norm', args=(np.mean(data['Normal_Distribution']), np.std(data['Normal_Distribution'])))
ks_non_normal = stats.kstest(data['Non_Normal_Distribution'], 'norm', args=(np.mean(data['Non_Normal_Distribution']), np.std(data['Non_Normal_Distribution'])))

ks_results = {
'Normal_Distribution': {
'Statistic': ks_normal.statistic,
'p-value': ks_normal.pvalue
},
'Non_Normal_Distribution': {
'Statistic': ks_non_normal.statistic,
'p-value': ks_non_normal.pvalue
}
}
ks_results

正态分布数据:统计量为 0.0215,p 值为 0.7370,p 值大于 0.05,无法拒绝数据符合正态分布的假设,非正态分布数据:统计量为 0.0678,p 值为 0.00019,p 值远小于 0.05,因此可以拒绝数据符合正态分布的假设

Anderson-Darling检验

# 进行Anderson-Darling检验
ad_normal = stats.anderson(data['Normal_Distribution'], dist='norm')
ad_non_normal = stats.anderson(data['Non_Normal_Distribution'], dist='norm')

ad_results = {
'Normal_Distribution': {
'Statistic': ad_normal.statistic,
'Critical_Values': ad_normal.critical_values,
'Significance_Level': ad_normal.significance_level
},
'Non_Normal_Distribution': {
'Statistic': ad_non_normal.statistic,
'Critical_Values': ad_non_normal.critical_values,
'Significance_Level': ad_non_normal.significance_level
}
}
ad_results

正态分布数据:统计量为 0.347,低于所有的临界值(0.574, 0.653, 0.784, 0.914, 1.088),因此我们不能拒绝数据符合正态分布的假设,非正态分布数据:统计量为 11.326,远高于所有的临界值,因此可以拒绝数据符合正态分布的假设

Jarque-Bera检验

# 进行Jarque-Bera检验
jb_normal = stats.jarque_bera(data['Normal_Distribution'])
jb_non_normal = stats.jarque_bera(data['Non_Normal_Distribution'])

jb_results = {
'Normal_Distribution': {
'Statistic': jb_normal.statistic,
'p-value': jb_normal.pvalue
},
'Non_Normal_Distribution': {
'Statistic': jb_non_normal.statistic,
'p-value': jb_non_normal.pvalue
}
}
jb_results

正态分布数据:统计量为 2.456,p 值为 0.2928,p 值大于 0.05,无法拒绝数据符合正态分布的假设,非正态分布数据:统计量为 59.919,p 值非常小,可以拒绝数据符合正态分布的假设

D’Agostino’s K-squared检验

# 进行D'Agostino's K-squared检验
dagostino_normal = stats.normaltest(data['Normal_Distribution'])
dagostino_non_normal = stats.normaltest(data['Non_Normal_Distribution'])

dagostino_results = {
'Normal_Distribution': {
'Statistic': dagostino_normal.statistic,
'p-value': dagostino_normal.pvalue
},
'Non_Normal_Distribution': {
'Statistic': dagostino_non_normal.statistic,
'p-value': dagostino_non_normal.pvalue
}
}

dagostino_results

正态分布数据:统计量为 2.576,p 值为 0.2759,p 值大于 0.05,无法拒绝数据符合正态分布的假设,非正态分布数据:统计量为 710.37,p 值极小,可以拒绝数据符合正态分布的假设

本文章转载微信公众号@Python机器学习AI