1. 从"数据像不像正态"说起:为什么这一步绕不过去
很多人做数据分析,拿到一列数字的第一反应是直接算均值、跑t检验、上线性模型。但只要样本量不大、分布稍微偏一点,这些基于正态假设的方法就会给出离谱的结论。我见过太多案例:明明数据是明显的右偏分布,硬套t检验,p值看着显著,实际结论完全站不住脚。问题就出在——没有先做分布检验。
正态分布检验是统计建模的地基。一元正态检验针对单个变量,判断它是否服从正态分布;多元正态检验则针对多个变量的联合分布,判断它们的联合分布是否近似多元正态。后者在多元统计分析、结构方程模型、判别分析、Hotelling's T²检验等场景里是前置条件。R语言在这块的工具链非常成熟,从可视化到正式检验,从一元到多元,都有对应的函数和包。
这篇文章面向的是已经会用R做基本数据处理、但还没系统掌握分布检验流程的人。我会把一元和多元正态检验的完整链路拆开讲:先讲清楚每种方法背后的逻辑,再给可直接运行的代码,最后补上那些文档里不会写、但实操中一定会遇到的坑。文中所有数据均为模拟生成,不涉及任何真实数据源。
需要提前说明一点:标题里提到"也可以用于其他分布的检验",这不是凑数。正态检验的很多思路——比如经验分布对比、分位数对比、拟合优度——可以平移到指数分布、均匀分布、伽马分布等任意分布的检验上。我会在最后一节专门讲这个迁移方法。
2. 一元正态检验:三条路线,各有各的适用边界
一元正态检验的方法大致分三类:可视化诊断、基于经验分布的正式检验、基于矩的检验。很多人只记住shapiro.test(),但不同方法在不同样本量、不同偏度下的表现差异很大,选错了会得出相反结论。
2.1 可视化诊断:先看图,再决定要不要检验
正式检验之前,我强烈建议先画图。原因很简单:检验给出的是"是否拒绝正态假设"的二元结论,而图能告诉你偏离正态的方向和程度。一个p值0.04的检验结果,可能只是轻微厚尾,也可能是有明显的双峰——这两种情况的处理方式完全不同。
最常用的两张图是直方图叠加密度曲线和Q-Q图。
set.seed(42) # 生成三组模拟数据:标准正态、右偏、双峰 x_norm <- rnorm(500, mean = 50, sd = 10) x_skew <- rgamma(500, shape = 2, scale = 5) x_bimodal <- c(rnorm(250, 30, 5), rnorm(250, 70, 5)) # 直方图 + 密度曲线 par(mfrow = c(1, 3)) hist(x_norm, prob = TRUE, breaks = 30, main = "近似正态", col = "lightblue") lines(density(x_norm), col = "red", lwd = 2) hist(x_skew, prob = TRUE, breaks = 30, main = "右偏", col = "lightgreen") lines(density(x_skew), col = "red", lwd = 2) hist(x_bimodal, prob = TRUE, breaks = 30, main = "双峰", col = "lightyellow") lines(density(x_bimodal), col = "red", lwd = 2)Q-Q图更精细,它把样本分位数和理论正态分位数做对比。点落在对角线上说明吻合,偏离方向能直接读出偏度和峰度信息。
par(mfrow = c(1, 3)) qqnorm(x_norm, main = "Q-Q: 近似正态"); qqline(x_norm, col = "red") qqnorm(x_skew, main = "Q-Q: 右偏"); qqline(x_skew, col = "red") qqnorm(x_bimodal, main = "Q-Q: 双峰"); qqline(x_bimodal, col = "red")读Q-Q图的经验:右偏数据的点会在右上方向上翘、左下方向下弯;左偏相反;厚尾数据两端点会明显偏离对角线,中间贴合;双峰数据会呈现S形或分段偏离。这些形态特征比p值更有诊断价值。
注意:直方图的分箱数(breaks参数)会显著影响视觉判断。分箱太少会掩盖双峰,太多会显得毛糙。我一般用
breaks = 30起步,样本量超过1000时用breaks = "FD"让R自动按Freedman-Diaconis规则选。
2.2 基于经验分布的正式检验:Shapiro-Wilk、K-S与Anderson-Darling
可视化只能定性,正式检验给的是定量证据。R里常用的有三个:Shapiro-Wilk、Kolmogorov-Smirnov、Anderson-Darling。
Shapiro-Wilk检验是小样本(n < 5000)下的首选,统计量W越接近1越正态。它的原假设是"数据来自正态分布",p值小于显著性水平就拒绝。
shapiro.test(x_norm) # p值应该很大,不拒绝 shapiro.test(x_skew) # p值应该很小,拒绝 shapiro.test(x_bimodal)实测下来,Shapiro-Wilk对偏度敏感,对峰度也敏感,综合性能在n<5000时是最好的。但样本量超过5000时R会报错,因为大样本下它对微小偏离也极度敏感,几乎必然拒绝。
Kolmogorov-Smirnov检验(ks.test)的原假设是"数据来自某个指定分布"。用它检验正态时,必须指定均值和标准差,否则默认对比标准正态,结果没意义。
# 错误做法:不指定参数,默认对比N(0,1) ks.test(x_norm, "pnorm") # 结果不可信 # 正确做法:用样本估计的参数 ks.test(x_norm, "pnorm", mean = mean(x_norm), sd = sd(x_norm))这里有个大坑:用样本估计的参数去做K-S检验,p值会偏大(偏保守),因为参数是从同一批数据估的,相当于"用数据拟合数据"。严格的K-S检验要求参数已知。实践中如果非要用,可以用Lilliefors修正,R里nortest包的lillie.test()实现了这个修正。
Anderson-Darling检验(nortest::ad.test)对尾部偏离更敏感,适合关注极端值的场景。它的统计量给尾部更大权重,所以对厚尾、离群点比K-S更灵敏。
library(nortest) ad.test(x_norm) ad.test(x_skew) lillie.test(x_norm) # Lilliefors修正的K-S三个检验的对比我整理成表:
| 检验方法 | 函数 | 适用样本量 | 敏感方向 | 参数要求 |
|---|---|---|---|---|
| Shapiro-Wilk | shapiro.test | n < 5000 | 偏度+峰度 | 无需指定 |
| K-S | ks.test | 任意 | 整体分布 | 需指定参数 |
| Lilliefors | lillie.test | 任意 | 整体分布 | 无需指定 |
| Anderson-Darling | ad.test | 任意 | 尾部 | 无需指定 |
2.3 基于偏度峰度的检验:当样本量很大时怎么办
样本量上万时,Shapiro-Wilk用不了,K-S又太敏感。这时候可以转向偏度-峰度检验。思路是:正态分布的偏度为0、峰度为3(超额峰度为0)。计算样本偏度和峰度,看它们是否显著偏离理论值。
library(moments) skewness(x_norm) # 接近0 kurtosis(x_norm) # 接近3 # 构造检验统计量(大样本下近似正态) n <- length(x_norm) sk <- skewness(x_norm) ku <- kurtosis(x_norm) # 偏度的标准误约为 sqrt(6/n) z_sk <- sk / sqrt(6/n) # 超额峰度的标准误约为 sqrt(24/n) z_ku <- (ku - 3) / sqrt(24/n) c(skew_z = z_sk, kurt_z = z_ku)moments包还提供了agostino.test(偏度检验)和anscombe.test(峰度检验),它们做了更精细的变换,小样本下也比直接算z值靠谱。
agostino.test(x_norm) anscombe.test(x_norm)我的经验是:大样本下不要迷信单一检验的p值。n=10000时,哪怕数据只是轻微偏离正态,几乎所有检验都会拒绝。这时候要看偏离的实际程度——偏度绝对值小于0.5、超额峰度绝对值小于1,通常可以认为"近似正态",后续参数方法仍然稳健。这是中心极限定理给的底气。
3. 多元正态检验:一元方法的简单叠加为什么不够
一元正态检验做完,很多人会想:那我对每个变量分别做一遍Shapiro-Wilk,全通过了是不是就说明联合分布是多元正态?不是。这是多元正态检验里最常见的误解。
多元正态要求的是任意线性组合都服从一元正态。每个边缘分布正态,联合分布不一定正态——经典反例是:X和Y各自标准正态,但联合分布可以是两个正态的混合,边缘看着正常,联合却有双峰或非线性相关结构。所以必须用专门的多元方法。
3.1 马氏距离与卡方Q-Q图:最直观的多元诊断
多元正态的一个关键性质:如果数据服从p维多元正态,那么每个观测的马氏距离平方服从自由度为p的卡方分布。这个性质直接给出了一个诊断工具——卡方Q-Q图。
library(MASS) set.seed(123) # 生成二元正态数据 Sigma <- matrix(c(1, 0.6, 0.6, 1), nrow = 2) X <- mvrnorm(500, mu = c(0, 0), Sigma = Sigma) # 计算马氏距离平方 center <- colMeans(X) S <- cov(X) md2 <- mahalanobis(X, center, S) # 卡方Q-Q图 p <- ncol(X) qqplot(qchisq(ppoints(500), df = p), md2, main = "卡方Q-Q图", xlab = "卡方理论分位数", ylab = "马氏距离平方") abline(0, 1, col = "red")如果点大致落在对角线上,说明多元正态假设合理。偏离对角线,尤其是右上角上翘,说明存在离群点或厚尾。
这个方法的优势是把p维问题降到了一维,可视化非常直观。但它对离群点敏感,一个极端值就能让整张图变形。所以通常配合下面的正式检验一起用。
3.2 Mardia检验:偏度和峰度的多元推广
Mardia检验是多元正态检验里最常用的正式方法,它把一元偏度峰度推广到多元。Mardia偏度衡量分布的不对称性,Mardia峰度衡量尾部厚度。MVN包提供了完整实现。
library(MVN) result <- mvn(data = X, mvnTest = "mardia") result$multivariate_normality输出会给出Mardia偏度和峰度的统计量及p值。两个p值都大于显著性水平,才不拒绝多元正态。
MVN包还支持其他检验方法,通过mvnTest参数切换:
mvn(X, mvnTest = "hz") # Henze-Zirkler检验 mvn(X, mvnTest = "royston") # Royston检验(适合n < p的情况) mvn(X, mvnTest = "dh") # Doornik-Hansen检验各方法的适用场景:
| 方法 | 参数值 | 适用条件 | 特点 |
|---|---|---|---|
| Mardia | "mardia" | n > p,大样本 | 经典,对峰度敏感 |
| Henze-Zirkler | "hz" | 任意 | 综合性能好,计算稍慢 |
| Royston | "royston" | n < 5000 | 小样本友好 |
| Doornik-Hansen | "dh" | 大样本 | 基于变换,稳健 |
我一般先用Henze-Zirkler做初筛,因为它对偏度和峰度都敏感,综合性能在模拟研究中表现最好。如果样本量小(比如n=50),换Royston。Mardia作为补充,看偏度和峰度分别的情况。
3.3 多元离群点检测:检验失败时先找"捣乱分子"
多元正态检验拒绝原假设,很多时候不是整体分布不对,而是少数离群点在作祟。这时候直接下"数据不正态"的结论太草率,应该先做离群点检测。
基于马氏距离的检测是最直接的:距离超过卡方临界值的观测就是候选离群点。
# 用卡方临界值标记离群点 alpha <- 0.001 threshold <- qchisq(1 - alpha, df = p) outliers <- which(md2 > threshold) length(outliers) # 可视化 plot(md2, pch = 19, col = ifelse(md2 > threshold, "red", "blue"), ylab = "马氏距离平方", main = "多元离群点检测") abline(h = threshold, col = "red", lty = 2)MVN包也内置了离群点检测:
mvn(X, multivariateOutlierMethod = "quan") # 分位数法 mvn(X, multivariateOutlierMethod = "adj") # 调整法找到离群点后,不要急着删。先判断它是数据录入错误还是真实极端值。录入错误直接修正;真实极端值要结合业务判断——如果它代表一个重要的少数群体,删掉会丢失信息,这时候可以考虑稳健统计方法,或者对数据做变换。
实操心得:我处理过一批数据,Mardia检验p值小于0.001,看着完全不正态。结果用马氏距离一查,500个样本里只有3个离群点。剔除后重新检验,p值0.3,完全通过。如果当时直接放弃多元方法,就白白浪费了这批数据。
4. 当正态假设不成立:变换与稳健替代方案
检验做完,如果数据确实不正态,有两条路:变换数据让它接近正态,或者换用不依赖正态假设的方法。两条路各有适用场景,选错了会引入新问题。
4.1 Box-Cox变换:找到最优的幂变换参数
Box-Cox变换是一族幂变换,通过极大似然估计找最优的λ参数。λ=1是恒等变换,λ=0是对数变换,λ=0.5是平方根变换。
library(MASS) # 对右偏数据做Box-Cox bc <- boxcox(x_skew ~ 1, lambda = seq(-2, 2, 0.1)) lambda_opt <- bc$x[which.max(bc$y)] lambda_opt # 应用变换 x_transformed <- (x_skew^lambda_opt - 1) / lambda_opt shapiro.test(x_transformed)Box-Cox有个硬性要求:数据必须全为正数。有零或负数时用不了,这时候可以用Yeo-Johnson变换,它把定义域扩展到了全体实数。car包的yjPower函数实现了这个。
library(car) yj <- yjPower(x_skew, lambda = 0.5) # 需要先估计lambda变换的代价是可解释性下降。对数变换后的均值,反变换回去不是原尺度的均值。所以如果业务方需要原尺度的结论,变换要慎用,或者只在建模阶段用、报告时反变换。
4.2 非参数方法:不假设分布也能做推断
如果变换效果不好,或者业务上不允许变换,那就换非参数方法。常见的对应关系:
| 参数方法 | 非参数替代 | R函数 |
|---|---|---|
| 单样本t检验 | Wilcoxon符号秩检验 | wilcox.test |
| 两独立样本t检验 | Mann-Whitney U检验 | wilcox.test |
| 单因素方差分析 | Kruskal-Wallis检验 | kruskal.test |
| 重复测量方差分析 | Friedman检验 | friedman.test |
| Pearson相关 | Spearman相关 | cor(method="spearman") |
非参数方法不依赖正态假设,但检验效能会低一些——同样样本量下,如果数据其实接近正态,非参数方法的p值会偏大。所以不是"不正态就无脑上非参数",而是要看偏离程度。轻微偏离(偏度<1)时,参数方法通常仍然稳健,尤其是大样本。
4.3 稳健统计量:折中方案
介于两者之间的是稳健统计方法,比如用中位数代替均值、用M估计代替最小二乘、用稳健协方差矩阵代替样本协方差。MASS包的rlm做稳健回归,robustbase包提供了稳健的多元方法。
library(robustbase) # 稳健协方差估计 cov_robust <- covMcd(X) # 用稳健中心做马氏距离 md2_robust <- mahalanobis(X, cov_robust$center, cov_robust$cov)稳健方法的好处是保留参数方法的解释框架,同时对离群点不敏感。代价是计算复杂、理论门槛高,报告时也要额外说明用了什么估计量。
5. 把正态检验的思路迁移到其他分布
标题里说"也可以用于其他分布的检验",这不是空话。正态检验的三大思路——经验分布对比、分位数对比、拟合优度——可以平移到任意分布。
5.1 任意分布的K-S检验
ks.test的第二个参数可以是任意分布函数。检验指数分布:
x_exp <- rexp(500, rate = 0.5) ks.test(x_exp, "pexp", rate = 0.5)检验均匀分布:
x_unif <- runif(500, min = 0, max = 10) ks.test(x_unif, "punif", min = 0, max = 10)检验伽马分布:
x_gamma <- rgamma(500, shape = 3, rate = 2) # 参数需要先估计 fit <- MASS::fitdistr(x_gamma, "gamma") ks.test(x_gamma, "pgamma", shape = fit$estimate["shape"], rate = fit$estimate["rate"])同样的坑:参数从数据估计时,K-S的p值偏保守。严格做法是用Lilliefors类修正,或者用参数自助法(parametric bootstrap)模拟p值分布。
5.2 通用Q-Q图:一行代码适配任意分布
Q-Q图的原理是分位数对比,只要指定理论分布的分位数函数,就能画任意分布的Q-Q图。
# 通用Q-Q图函数 qq_any <- function(data, dist_func, params = list(), main = "Q-Q图") { n <- length(data) theoretical <- do.call(dist_func, c(list(p = ppoints(n)), params)) observed <- sort(data) plot(theoretical, observed, main = main, xlab = "理论分位数", ylab = "样本分位数", pch = 19) abline(0, 1, col = "red") } # 指数分布Q-Q图 qq_any(x_exp, qexp, list(rate = 0.5), "指数分布Q-Q图") # 均匀分布Q-Q图 qq_any(x_unif, qunif, list(min = 0, max = 10), "均匀分布Q-Q图")这个函数的好处是通用。换分布只需要换dist_func和params,可视化逻辑完全复用。
5.3 拟合优度检验:卡方检验的通用框架
对于离散分布或者分箱后的连续分布,卡方拟合优度检验是通用工具。
# 检验泊松分布 x_pois <- rpois(500, lambda = 4) observed <- table(x_pois) # 合并频数过小的类别 lambda_hat <- mean(x_pois) expected <- dpois(as.numeric(names(observed)), lambda_hat) * length(x_pois) # 合并期望频数小于5的类别 chisq.test(observed, p = expected / sum(expected))卡方检验的关键约束:每个类别的期望频数不小于5。不满足时要合并类别,否则p值不可信。这是实操中最容易忽略的点。
5.4 分布选择的决策流程
面对一批数据,怎么选检验方法?我总结了一个决策流程:
- 先画图:直方图+Q-Q图,定性判断分布形态和偏离方向。
- 看样本量:n<5000用Shapiro-Wilk(一元)或Royston(多元);n大用Anderson-Darling或偏度峰度。
- 看维度:一元用
shapiro.test,多元用MVN::mvn。 - 查离群点:检验拒绝时先做离群点检测,排除"少数捣乱分子"。
- 决定策略:轻微偏离用参数方法+稳健标准误;中度偏离做变换;严重偏离或变换无效用非参数。
- 报告结果:无论用哪种方法,都要报告检验统计量、p值、以及你做了什么处理。
这个流程不是死的,但按它走能避免大部分低级错误。
6. 那些文档里不会写的实操坑
这一节是我这些年踩过的坑的汇总,每一条都对应一个具体的翻车场景。
坑一:Shapiro-Wilk的样本量上限。n超过5000时R直接报错"sample size must be between 3 and 5000"。很多人不知道这个限制,数据一多就卡住。解决方案:大样本改用ad.test或偏度峰度检验,或者随机抽样5000个做检验。
坑二:K-S检验的参数陷阱。ks.test(x, "pnorm")不指定参数时,对比的是标准正态N(0,1)。如果你的数据均值是50,这个检验必然拒绝,但拒绝的原因不是"不正态",而是"均值不是0"。这个错误极其常见,我见过不止一个项目栽在这上面。
坑三:多元检验对样本量的要求。Mardia检验在n < p时无法计算(协方差矩阵奇异)。Henze-Zirkler在n很小时功效很低。经验规则:n至少是p的10倍,多元检验结果才比较可靠。样本不够时,要么降维,要么用一元方法逐个检验并说明局限。
坑四:多重检验校正。对10个变量分别做Shapiro-Wilk,每个用0.05的显著性水平,至少拒绝一个的概率是1-0.95^10≈40%。也就是说,即使数据全正态,你也有四成概率"发现"不正态。这时候要用Bonferroni或FDR校正。
p_values <- apply(X, 2, function(v) shapiro.test(v)$p.value) p_adjusted <- p.adjust(p_values, method = "BH") # Benjamini-Hochberg坑五:变换后的反变换。对数变换后做的置信区间,反变换回去不是原尺度均值的置信区间,而是中位数的。这个区别在报告结果时经常被忽略,导致结论偏差。
坑六:p值不是唯一标准。n=100000时,偏度0.05也会被检验出"显著不正态"。但偏度0.05在实际中完全可以忽略。统计显著不等于实际显著,这是分布检验里最需要牢记的一条。
坑七:MVN包的图形输出。mvn(X, multivariatePlot = "qq")会直接画图,但在RStudio的某些版本里图形设备会冲突,导致图出不来。解决方法是先pdf(NULL)或者用dev.new()开新设备。
坑八:模拟数据的可复现性。所有涉及随机数的代码,务必在开头set.seed()。我见过太多人跑出来的结果和别人对不上,就是因为没设种子。种子值本身不重要,重要的是固定。
7. 完整实战:从数据生成到结论输出的全流程
把前面所有内容串起来,走一遍完整流程。假设我们有一批模拟的多元数据,需要判断能否用多元正态方法。
set.seed(2024) library(MASS) library(MVN) library(moments) # 第一步:生成模拟数据(含少量离群点) n <- 300 Sigma <- matrix(c(1, 0.5, 0.3, 0.5, 1, 0.4, 0.3, 0.4, 1), nrow = 3) X <- mvrnorm(n, mu = c(10, 20, 30), Sigma = Sigma) # 人为加入5个离群点 X[1:5, ] <- X[1:5, ] * 3 # 第二步:一元检验(逐个变量) univ_results <- data.frame( variable = paste0("V", 1:3), shapiro_p = NA, skewness = NA, kurtosis = NA ) for (i in 1:3) { univ_results$shapiro_p[i] <- shapiro.test(X[, i])$p.value univ_results$skewness[i] <- skewness(X[, i]) univ_results$kurtosis[i] <- kurtosis(X[, i]) } print(univ_results) # 第三步:多元检验 mvn_result <- mvn(X, mvnTest = "hz") print(mvn_result$multivariate_normality) # 第四步:离群点检测 outlier_result <- mvn(X, multivariateOutlierMethod = "quan") print(outlier_result$multivariate_outlier) # 第五步:剔除离群点后重新检验 X_clean <- X[-c(1:5), ] mvn_clean <- mvn(X_clean, mvnTest = "hz") print(mvn_clean$multivariate_normality) # 第六步:可视化确认 par(mfrow = c(1, 2)) mvn(X_clean, multivariatePlot = "qq")这个流程的关键在于迭代:检验→发现离群点→剔除→重新检验→可视化确认。不要指望一次通过。实际数据里,离群点、厚尾、轻微偏度几乎必然存在,重要的是判断它们是否严重到影响后续分析。
关于剔除离群点,我的原则是:能解释的才剔除。如果离群点对应数据录入错误、设备故障、或者明确的异常事件,剔除合理;如果只是"看着不顺眼",那要谨慎,因为可能是真实的重要信号。剔除后必须在报告中说明剔除了几个、依据是什么。
最后说一个我自己的习惯:把检验结果和可视化图一起存档。p值会随着样本量变化,但图不会骗人。半年后回头看,图能让你快速回忆起当时的数据形态,而一堆p值只会让你困惑。这个习惯在长期项目中特别有用。