简介:这份资源提供复合正态性的 Shapiro-Wilk 参数假设检验实现,适用于样本量 3≤n≤5000 的统计分析场景,面向需要判断数据是否服从正态分布的研究者、数据分析人员与统计学习者。其核心基于 Royston R94 算法,并对 platykurtic(低峰态)样本额外执行 Shapiro-Francia 正态性检验,兼顾两种经典方法的互补性,便于在偏态或峰态异常时交叉验证结论。压缩包为 zip 格式,仅含 1 个 m 文件,体积约 3KB,属于轻量级脚本,可直接在 MATLAB 环境中调用,无需额外依赖。目前已有 946 人学习下载,说明该实现具备一定的实用参考价值。读者可获得一套可直接运行的检验脚本,理解 Shapiro-Wilk 与 Shapiro-Francia 的算法流程、样本量适用范围及低峰态处理逻辑,并据此快速完成正态性诊断与结果解读。
1. 正态性检验的两种武器:为什么 Shapiro-Wilk 和 Shapiro-Francia 总被一起提
跑 t 检验、方差分析、线性回归之前,很多人会先做一步正态性检验。R 里shapiro.test()默认走的就是 Shapiro-Wilk,Python 的scipy.stats.shapiro也是同一套。但如果你翻过旧版统计教材或者某些遗传统计软件的输出,会看到另一个名字:Shapiro-Francia。它俩不是替代关系,而是针对不同样本量区间的互补工具。Shapiro-Wilk 在 n 介于 3 到 5000 时表现稳健,Shapiro-Francia 则在小样本到中等样本(大约 5 到 100)时对尾部偏离更敏感。实际项目中,我经常两个都跑一遍,对比 p 值和 W 统计量,判断数据是否真的偏离正态。这篇文章不扯理论推导,只讲怎么在 R 和 Python 里落地这两个检验,参数怎么设,结果怎么读,以及那些让我翻过车的坑。如果你手头有一批数据要决定用参数检验还是非参数检验,这篇笔记能直接抄作业。
2. Shapiro-Wilk 实操:从统计量到 p 值的完整链路
2.1 检验原理与选型理由
Shapiro-Wilk 的核心思想是:把样本顺序统计量与正态分布下顺序统计量的期望值做回归,回归的斜率平方就是 W 统计量。W 越接近 1,说明样本越像正态分布。它的零假设是“样本来自正态分布”,所以 p 值小于显著性水平(通常 0.05)时拒绝正态性。为什么选它而不是 Kolmogorov-Smirnov?因为 K-S 检验对参数估计后的复合假设处理不够精细,而 Shapiro-Wilk 专门针对正态性做了优化,功效更高。但要注意,Shapiro-Wilk 在样本量超过 5000 时,scipy会直接报错,R 的shapiro.test也会限制在 5000 以内。这时候要么抽样,要么换用 Shapiro-Francia 或 Anderson-Darling。
另一个选型理由是:Shapiro-Wilk 对偏度和峰度都敏感,但如果你只关心尾部行为,比如极值是否异常,Shapiro-Francia 的 W' 统计量更直接。我一般会先跑 Shapiro-Wilk 看整体,如果 p 值在 0.05 附近徘徊,再跑 Shapiro-Francia 交叉验证。
2.2 R 语言实现:shapiro.test 与参数细节
R 基础包里的shapiro.test用起来最简单,但有几个参数和限制必须知道。
# 生成一组模拟数据:正态分布和偏态分布 set.seed(123) normal_data <- rnorm(100, mean = 50, sd = 10) skewed_data <- rexp(100, rate = 0.5) # Shapiro-Wilk 检验 sw_normal <- shapiro.test(normal_data) sw_skewed <- shapiro.test(skewed_data) print(sw_normal) print(sw_skewed) # 提取统计量和 p 值 cat("Normal W:", sw_normal$statistic, "p-value:", sw_normal$p.value, "\n") cat("Skewed W:", sw_skewed$statistic, "p-value:", sw_skewed$p.value, "\n")逻辑说明:shapiro.test只接受一个数值向量,返回 W 统计量和 p 值。normal_data的 p 值应该大于 0.05,skewed_data的 p 值会远小于 0.05。参数方面,R 版本没有额外可调参数,但样本量 n 必须满足 3 ≤ n ≤ 5000。如果数据有重复值过多,W 统计量的计算会受影响,R 会给出警告“ties should not be present for the Shapiro-Wilk test”。这时候要么加微小的随机扰动,要么改用其他检验。
注意:R 的
shapiro.test不接受数据框直接输入,必须用$提取向量或者用with()。如果数据里有 NA,需要加na.rm = TRUE,但shapiro.test本身没有这个参数,得先手动剔除。
2.3 Python 实现:scipy.stats.shapiro 的边界与替代
Python 里scipy.stats.shapiro的接口和 R 类似,但返回的是(statistic, p_value)元组。样本量限制同样是 3 到 5000。
import numpy as np from scipy import stats np.random.seed(42) normal_data = np.random.normal(loc=50, scale=10, size=100) skewed_data = np.random.exponential(scale=2, size=100) # Shapiro-Wilk 检验 stat_normal, p_normal = stats.shapiro(normal_data) stat_skewed, p_skewed = stats.shapiro(skewed_data) print(f"Normal: W={stat_normal:.4f}, p={p_normal:.4f}") print(f"Skewed: W={stat_skewed:.4f}, p={p_skewed:.4f}") # 当样本量超过 5000 时,scipy 会报错 large_data = np.random.normal(size=6000) try: stats.shapiro(large_data) except Exception as e: print("Error:", e)逻辑说明:stats.shapiro返回的统计量就是 W,p 值越小越拒绝正态性。当 n > 5000 时,scipy抛出ValueError,提示“样本量太大”。这时候常见做法是随机抽样 5000 个点,或者改用stats.normaltest(基于 D'Agostino-Pearson 的偏度峰度检验)。但normaltest对样本量更敏感,大样本下容易拒绝,所以更推荐用 Shapiro-Francia 或者直接看 Q-Q 图。
参数方面,scipy.stats.shapiro没有nan_policy参数,数据里有 NaN 会直接返回 NaN。需要先用np.nan_to_num或者dropna处理。另外,shapiro对重复值的容忍度比 R 稍好,但重复值超过 20% 时结果也不可靠。
3. Shapiro-Francia 实操:小样本下的替代方案与手动实现
3.1 为什么需要 Shapiro-Francia
Shapiro-Francia 是 Shapiro-Wilk 的简化版本,用顺序统计量的期望值与样本值的相关系数平方作为统计量 W'。它的优势在于:计算更简单,对小样本(n < 50)的尾部偏离更敏感,而且没有 5000 的上限。但它的临界值表不如 Shapiro-Wilk 那么普及,很多软件不直接提供。R 的nortest包里有sf.test,Python 没有现成函数,需要手动实现或者用scipy.stats的shapiro替代。我一般在样本量小于 30 且怀疑有离群值时,会优先跑 Shapiro-Francia。
3.2 R 中 nortest 包的 sf.test 用法
nortest包提供了sf.test,用法和shapiro.test几乎一样。
# 安装并加载 nortest 包 if (!require(nortest)) { install.packages("nortest") } library(nortest) # 使用之前的数据 sf_normal <- sf.test(normal_data) sf_skewed <- sf.test(skewed_data) print(sf_normal) print(sf_skewed) # 对比 Shapiro-Wilk 和 Shapiro-Francia 的 p 值 cat("SW p:", sw_normal$p.value, "SF p:", sf_normal$p.value, "\n") cat("SW p:", sw_skewed$p.value, "SF p:", sf_skewed$p.value, "\n")逻辑说明:sf.test返回的统计量是 W',p 值含义相同。对于正态数据,两个检验的 p 值应该都大于 0.05;对于偏态数据,Shapiro-Francia 的 p 值通常更小,说明它更敏感。参数方面,sf.test没有额外参数,但样本量建议在 5 到 100 之间。如果 n 太小(比如小于 5),临界值表不准确,结果不可靠。
注意:
nortest包里的sf.test对重复值的处理比shapiro.test更宽松,但重复值超过 30% 时依然会警告。如果数据有大量重复,建议先做频数统计,或者改用lillie.test(Kolmogorov-Smirnov 的 Lilliefors 修正)。
3.3 Python 手动实现 Shapiro-Francia
Python 没有现成的 Shapiro-Francia 函数,但可以手动计算 W' 统计量,然后用近似公式求 p 值。下面是一个可复现的实现。
import numpy as np from scipy import stats def shapiro_francia(data): """ 手动实现 Shapiro-Francia 正态性检验 参数 data: 一维数组 返回: W' 统计量, p 值 """ n = len(data) if n < 5: raise ValueError("样本量至少为 5") # 排序 x = np.sort(data) # 计算正态顺序统计量的期望值 m_i # 使用 Blom 近似: m_i = Phi^{-1}((i - 0.375) / (n + 0.25)) i = np.arange(1, n + 1) m = stats.norm.ppf((i - 0.375) / (n + 0.25)) # 计算相关系数平方 numerator = np.sum(m * x) denominator = np.sqrt(np.sum(m**2) * np.sum(x**2)) W_prime = (numerator / denominator) ** 2 # 计算 p 值:使用 Royston 近似 # 变换到正态分布 mu = 0.0038915 * np.log(n)**3 - 0.083751 * np.log(n)**2 - 0.31082 * np.log(n) - 1.5861 sigma = np.exp(0.0030302 * np.log(n)**2 - 0.082676 * np.log(n) - 0.4803) z = (np.log(1 - W_prime) - mu) / sigma p_value = 1 - stats.norm.cdf(z) return W_prime, p_value # 测试 W_prime_normal, p_normal_sf = shapiro_francia(normal_data) W_prime_skewed, p_skewed_sf = shapiro_francia(skewed_data) print(f"Normal SF: W'={W_prime_normal:.4f}, p={p_normal_sf:.4f}") print(f"Skewed SF: W'={W_prime_skewed:.4f}, p={p_skewed_sf:.4f}")逻辑说明:这段代码先排序,然后用 Blom 近似计算正态顺序统计量的期望值,再求相关系数平方得到 W'。p 值部分用了 Royston 提出的近似公式,把 W' 变换到标准正态分布。参数方面,n必须大于等于 5,否则期望值计算不稳定。m的计算公式里 0.375 和 0.25 是 Blom 的经验常数,也可以用 0.5 和 0.5 替代,但 0.375 在小样本下更准。如果数据有重复值,排序后相关系数会偏高,导致 W' 偏大,p 值偏大,容易漏掉非正态性。这时候建议先做 jitter 或者用scipy.stats.shapiro交叉验证。
注意:手动实现的 p 值只是近似,和 R 的
sf.test结果可能有微小差异。如果对精度要求高,建议用 R 的nortest包,或者用scipy.stats.shapiro的结果作为参考。
4. 避坑与排查:正态性检验里那些让我翻车的细节
4.1 样本量超过 5000 时 shapiro 报错
现象:Python 的scipy.stats.shapiro在 n > 5000 时抛出ValueError,R 的shapiro.test也限制在 5000 以内。 原因:Shapiro-Wilk 的临界值表只算到 5000,超过后统计量的分布不再准确。 解决:随机抽样 5000 个点,或者改用 Shapiro-Francia(无上限),或者用stats.normaltest配合 Q-Q 图。我一般会先画 Q-Q 图,如果点基本在直线上,就不纠结 p 值了。
4.2 重复值过多导致警告
现象:R 的shapiro.test报“ties should not be present”,Python 的shapiro返回的 p 值异常大。 原因:重复值会让顺序统计量的期望值计算出现偏差,W 统计量被高估。 解决:如果重复值比例低于 20%,可以忽略警告;如果超过 20%,先做频数合并,或者改用lillie.test。我遇到过一批问卷数据,1 到 5 的 Likert 量表,重复值极多,最后直接用了 K-S 检验的 Lilliefors 修正。
4.3 p 值在 0.05 附近时的决策困境
现象:p = 0.048 或 p = 0.052,到底算不算正态? 原因:p 值只是证据强度,不是非黑即白。样本量越大,越容易拒绝正态性,即使偏离很小。 解决:结合 W 统计量和 Q-Q 图。如果 W > 0.98 且 Q-Q 图接近直线,即使 p < 0.05,也可以认为近似正态。我通常会把显著性水平设为 0.01,减少假阳性。
4.4 Shapiro-Francia 的 p 值计算偏差
现象:手动实现的 Shapiro-Francia 和 R 的sf.test结果不一致。 原因:p 值近似公式不同,Royston 的公式在小样本下误差较大。 解决:以 R 的nortest为准,或者用scipy.stats.shapiro的结果做交叉验证。如果两者结论矛盾,优先相信 Shapiro-Wilk,因为它的临界值表更精确。
4.5 数据里有 NaN 或 Inf
现象:Python 的shapiro返回 NaN,R 的shapiro.test报错“missing values”。 原因:两个检验都不处理缺失值。 解决:先剔除 NaN 和 Inf,或者用np.nan_to_num替换。但替换会引入偏差,最好直接删除。我一般会在检验前加一行data = data[np.isfinite(data)]。
5. 进阶技巧:用 Q-Q 图和 Monte Carlo 模拟验证检验结果
5.1 Q-Q 图:比 p 值更直观的判据
p 值只能告诉你“是否拒绝”,Q-Q 图能告诉你“哪里偏离”。我习惯把 Shapiro-Wilk 和 Shapiro-Francia 的 p 值放在一起,再画一张 Q-Q 图。如果 Q-Q 图的尾部点明显偏离直线,即使 p 值大于 0.05,也要警惕。
import matplotlib.pyplot as plt fig, axes = plt.subplots(1, 2, figsize=(12, 5)) # 正态数据的 Q-Q 图 stats.probplot(normal_data, dist="norm", plot=axes[0]) axes[0].set_title(f"Normal Data\nSW p={p_normal:.4f}, SF p={p_normal_sf:.4f}") # 偏态数据的 Q-Q 图 stats.probplot(skewed_data, dist="norm", plot=axes[1]) axes[1].set_title(f"Skewed Data\nSW p={p_skewed:.4f}, SF p={p_skewed_sf:.4f}") plt.tight_layout() plt.show()逻辑说明:probplot会画出样本分位数和理论分位数的散点图,红线是参考线。正态数据的点应该紧贴红线,偏态数据的尾部会明显偏离。参数方面,dist="norm"指定正态分布,也可以换成其他分布。如果点呈现 S 形,说明尾部厚;如果呈现倒 S 形,说明尾部薄。
5.2 Monte Carlo 模拟:验证检验的功效
如果你不确定某个样本量下 Shapiro-Wilk 和 Shapiro-Francia 哪个更可靠,可以跑一个 Monte Carlo 模拟,比较它们的拒绝率。
def monte_carlo_power(n, n_sim=1000, alpha=0.05): """比较 SW 和 SF 在偏态分布下的拒绝率""" sw_reject = 0 sf_reject = 0 for _ in range(n_sim): data = np.random.exponential(scale=1, size=n) _, p_sw = stats.shapiro(data) _, p_sf = shapiro_francia(data) if p_sw < alpha: sw_reject += 1 if p_sf < alpha: sf_reject += 1 return sw_reject / n_sim, sf_reject / n_sim # 测试不同样本量 for n in [10, 20, 50, 100]: sw_power, sf_power = monte_carlo_power(n) print(f"n={n}: SW power={sw_power:.3f}, SF power={sf_power:.3f}")逻辑说明:这段代码生成指数分布的样本,分别跑 Shapiro-Wilk 和 Shapiro-Francia,统计拒绝零假设的比例。拒绝率越高,说明检验越能识别出非正态性。参数方面,n_sim是模拟次数,1000 次足够稳定;alpha是显著性水平。从结果看,小样本下 Shapiro-Francia 的拒绝率通常更高,但样本量超过 50 后两者差距缩小。
注意:Monte Carlo 模拟耗时较长,建议在 Jupyter Notebook 里跑,或者用并行加速。如果只是做一次决策,直接看 Q-Q 图更快。
5.3 我的固定检查流程
从那以后我每次做正态性检验,都强制走一遍这个流程:先看样本量,n < 5000 就跑 Shapiro-Wilk,n > 5000 就抽样或者换 Shapiro-Francia;然后检查重复值和缺失值,该剔除的剔除;接着画 Q-Q 图,肉眼确认尾部行为;最后如果 p 值在 0.05 附近,再跑一次 Monte Carlo 模拟看功效。这套流程帮我省掉了不少后悔药。希望帮到你。
本文还有配套的精品资源,点击获取