十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

非参数检验与KS检验原理及Python实现全指南

非参数检验与KS检验原理及Python实现全指南 说实话我最早做数据分析的时候根本没有“非参数检验”这个概念。当时拿到两份数据第一反应就是跑 t 检验天天盯着 p 值看。直到有一次在生产环境里被真实数据打脸样本明显偏态方差也不齐t 检验的结果忽高忽低业务方拿着报告反问我“这结论到底靠不靠谱”。从那以后我才认真把 KS 检验和别的非参数检验挨个过了一遍发现它们就是数据分布不正常时的“保底方案”。这篇文章就是想把我沉淀下来的东西写清楚什么时候必须放弃参数检验KS 检验到底是怎样一个逻辑以及用 Python 做 Mann–Whitney U、Wilcoxon、Kruskal–Wallis、卡方独立性检验、Friedman 和 Spearman 秩相关时具体代码怎么写、哪些地方容易踩坑。不管你是做数据分析、用户研究还是打比赛、做毕业设计只要拿到的是小样本、偏态数据、有序分类数据这篇文章应该都能帮上忙。1. 非参数检验什么时候用先别急着跑 t 检验1.1 参数检验的三个前提假设很多人在学校学统计的时候t 检验是每个人都绕不开的入门工具。但实际工作中你会发现t 检验不是你想跑就能跑的它背后挂着三个硬性条件样本独立、总体近似正态、各组方差齐性。这三条看起来简单做起来是真难。我看过太多号称“大数据分析”的项目样本量根本没到中心极限定理能发挥作用的程度数据画出来明显是个长尾分布用户活跃时长、订单金额、响应耗时这类业务字段基本都是右偏的再加上分组之间方差经常差出好几倍。这时候你硬用 t 检验第一类错误和第二类错误都会失控p 值可能大得离谱也可能小得吓人根本没有参考价值。当然有人会说“样本量大了就不怕违反正态性”。这话只对了一半样本量上来以后t 检验对均值的抽样分布确实会慢慢趋于正态但离群值的影响、方差不齐带来的检验功效损失并不会因为样本量大就自动消失。所以更稳妥的做法是先画图看分布、用检验做判断真的要违反假设了就老老实实换非参数方法。1.2 非参数检验的本质把数值变成序非参数检验的聪明之处在于它不关心数据到底服从哪个分布只关心数据的相对大小和顺序。也就是说先把所有数值从小到大排个序然后不再看原始数值只看每个样本落在第几名。只要数据是有序的这个排序就有意义分布长得再奇怪也不影响后续推断。举个身边的例子。你问用户“对产品满意吗”给 1 到 5 分的李克特量表得到的是典型的定序数据。你说“4 分和 5 分之间的差距等于 3 分和 4 分之间的差距吗”这个问题根本没法回答。但你能确定 5 分比 4 分更满意4 分比 3 分更满意。这时候用均值、用 t 检验逻辑上就是说不通的正确的做法是用基于秩次的非参数检验。代价当然也有。如果数据本来就是正态分布、方差齐性非参数检验的功效比参数检验低一些容易把真正存在的差异判成“不显著”。所以我的习惯是越靠近“完美正态小样本”的场景越优先参数检验一旦分布诡异或者数据本身就是等级变量就直接切到非参数赛道。2. KS 检验原理与 Python 实现从手算 D 统计量开始2.1 单样本 KS 检验判断数据到底是不是来自某个分布KS 检验的中文全称是柯尔莫哥洛夫-斯米尔诺夫检验它是非参数检验里比较“硬核”的一个因为它的原假设很明确样本来自某个指定的理论分布。比如你想验证这批数据是不是正态分布、是不是均匀分布、是不是指数分布都可以用它。核心逻辑其实不复杂。你先画出样本的经验分布函数再画出目标分布的理论累积分布函数然后计算两条曲线之间的最大垂直距离这个距离就是检验统计量 D。D 越大说明样本分布离目标分布越远越有理由拒绝原假设。你可以想象把两条 CDF 曲线叠在一张图上一个是台阶状的样本经验分布一个是光滑的理论分布曲线D 就是它们之间那个最大的竖向空隙。我先把 D 统计量手写出来这样原理和直觉就能对上了。import numpy as np from scipy import stats np.random.seed(2024) data np.random.normal(loc0.2, scale1.1, size80) # 手写经验分布函数 sorted_data np.sort(data) n len(data) def empirical_cdf(x): return np.searchsorted(sorted_data, x, sideright) / n # 构造理论累积分布函数 from scipy.stats import norm theoretical_cdf lambda x: norm.cdf(x, loc0.2, scale1.1) # 手动计算 D max |F_n(x) - F_0(x)| d_manual np.max(np.abs(empirical_cdf(sorted_data) - theoretical_cdf(sorted_data))) # 用 scipy 直接算 d_scipy, p_value stats.kstest(data, norm, args(0.2, 1.1)) print(手动计算的 D:, d_manual) print(scipy 计算的 D:, d_scipy) print(p 值:, p_value)这里有个细节要留意stats.kstest的第二个参数可以是分布名称的字符串比如norm但如果这个分布有额外的形状参数必须用args传进去而且传的一定是“理论分布参数”不能是随便拍的数。如果 p 值小于 0.05说明样本分布和指定的理论分布差异显著你就可以比较有把握地说“这组数据不符合这个分布假设”。2.2 双样本 KS 检验两组样本的分布是否一致双样本 KS 检验是实际业务里用得更多的场景。它不关心两组数据各自的均值、方差具体是多少只关心两组样本是否来自同一个整体分布。原假设是两组数据的总体分布相同。这个检验对分布形态很敏感位置不同能检测出来尺度不同能检测出来甚至两组数据形状完全不同但均值相近它也能检测出来。正因为这个特点我把双样本 KS 检验当成“特征筛选利器”来用。做风控模型或者用户增长分析的时候我经常把某个特征按正负样本或者转化组和未转化组拆开然后跑一次ks_2samp看这个特征对分组的区分能力到底有多大。group_a np.random.gamma(shape2, scale1, size100) group_b np.random.gamma(shape2, scale1.3, size100) d_stat, p_value stats.ks_2samp(group_a, group_b) print(双样本 KS 统计量:, d_stat) print(p 值:, p_value)如果 D 很大、p 很小两组数据的分布就不一样说明这个特征能帮你把两个群体分开。画图辅助判断效果更直观直接画两组数据的累计分布曲线看它们中间的最大“裂缝”在哪里这个裂缝对应的横坐标往往就是人群分层的边界。2.3 一个关键修正估计参数后的 KS 检验不能用这里有个特别隐蔽的坑我第一次用的时候差点得出完全相反的结论。假设你要检验一批数据是不是正态分布但不知道真实均值和标准差。很多人会直接用样本均值、样本标准差去填args(mean, std)然后跑kstest得到 p 值很大开心地认为“数据服从正态分布”。这个结论是错的。因为当你用样本估计参数时你和理论分布之间就已经“对过答案”了D 统计量会系统性偏小p 值会系统性偏大。换句话说检验变得太宽松了本来不该通过的也能被放行。正确的做法是用专门的 Lilliefors 检验import statsmodels.api as sm from statsmodels.stats.diagnostic import lilliefors ks_stat, p_value lilliefors(data, distnorm) print(Lilliefors 检验 D:, ks_stat) print(Lilliefors 检验 p:, p_value)这个检验对“先用样本估计参数、再检验分布”这件事做了修正。记住一个口诀不知道理论参数就别直接用原始 KS知道理论参数也就是有明确的出厂标准或业务规定才可以直接用kstest。3. 其余常用非参数检验的 Python 实现3.1 两组独立样本Mann–Whitney U 检验如果说 KS 检验比的是“两条分布曲线之间的距离”那 Mann–Whitney U 检验更侧重“两组数据的整体排名比较”。它的原假设是两组样本来自同一总体或者从两组里随机各抽一个数两个数的大小关系没有倾向性。反过来理解如果一组数据普遍大于另一组这个检验就会返回很小的 p 值。这个检验非常契合 A/B 测试场景尤其是转化金额、使用时长这类长尾数据。比如用户分为对照组和实验组每组几百人支付金额明显偏态这时用 Mann–Whitney U 就比 t 检验稳得多。control [123, 145, 156, 178, 89, 234, 111, 167, 98, 210] experiment [167, 189, 224, 156, 245, 267, 201, 232, 178, 190] u_stat, p_value stats.mannwhitneyu(control, experiment, alternativetwo-sided) print(U 统计量:, u_stat) print(p 值:, p_value)重点提醒不同版本的 scipy 对alternative的默认值有过调整老版本默认是单侧新版本默认是双侧。如果你发现同一个数据在不同机器上跑出来的 p 值差了一倍第一反应应该查 scipy 版本。3.2 两组配对样本Wilcoxon 符号秩检验Wilcoxon 符号秩检验是配对样本的主力方法。它对应的是参数检验里的配对 t 检验适用于同一个个体在前后两个时间点的测量或者同一个实验对象接受了两次不同处理的情况。比如我们给一批学员做培训记录培训前后的测验成绩想判断培训到底有没有用。这时候数据天然配对每个学员的前后成绩不能当成两个独立样本处理直接跑 Mann–Whitney U 是不对的。before np.array([72, 68, 74, 80, 66, 77, 71, 69, 75, 78]) after np.array([78, 75, 82, 84, 70, 79, 76, 73, 81, 85]) diff after - before w_stat, p_value stats.wilcoxon(diff) print(Wilcoxon 统计量:, w_stat) print(p 值:, p_value)需要注意stats.wilcoxon可以接收两个原始数组也可以接收差值数组。如果差值里有 0它默认是扔掉还是保留不同版本行为不一样建议自己先处理数据确定到底怎么对待 0 差值。3.3 多组独立样本Kruskal–Wallis H 检验当分组数大于等于三组而且数据不满足正态性或方差齐性时就该用 Kruskal–Wallis H 检验。它是 Mann–Whitney U 检验的多组推广也是单因素方差分析的非参数替代。它的原假设是各组数据的总体位置参数相同也就是各组的“平均水平排名”没有差异。原理上它把所有的观测值混在一起排序再比较各组平均秩的差异。如果某几组的平均秩明显不同H 统计量就会偏大。group1 [23, 25, 28, 30, 27, 29] group2 [30, 33, 35, 31, 34, 36] group3 [20, 22, 19, 24, 21, 19] h_stat, p_value stats.kruskal(group1, group2, group3) print(H 统计量:, h_stat) print(p 值:, p_value)这里要特别强调Kruskal–Wallis 显著只代表“至少有一组和其他组不一样”并不能告诉你具体是哪几组有差异。后续还要做两两比较并且要对多重比较做校正不然假阳性率会高得离谱。3.4 分类变量关系卡方独立性检验前面几种方法处理的大都是连续或有序数据如果数据本身是分类变量的频次比如“男/女”和“买/不买”这时候就要用卡方独立性检验。它的核心思想是如果两个分类变量相互独立那么列联表里每个格子的实际频数应该和期望频数差不多。把每个格子实际值和期望值的偏差平方、标准化再加起来就得到卡方统计量。observed_table np.array([[30, 12], [18, 40]]) chi2_stat, p_value, dof, expected stats.chi2_contingency(observed_table) print(卡方统计量:, chi2_stat) print(自由度:, dof) print(p 值:, p_value) print(期望频数表:, expected)stats.chi2_contingency还支持连续性修正。对 2x2 的列联表默认会做耶茨连续性修正让结果更保守一些。如果你的表格是 2x2 且某些格子期望频数小于 5那建议改用stats.fisher_exact做 Fisher 精确检验结果更可靠。3.5 重复测量与相关性Friedman 检验与 Spearman 秩相关Friedman 检验适合多组配对数据比如让同一个算法在 3 个不同数据集上跑或者同一批用户在多个时间点被追踪测量。它是重复测量方差分析的非参数替代也是对多个相关样本有没有差异的检验。time1 [5.1, 4.9, 5.3, 4.8, 5.0] time2 [5.5, 5.2, 5.4, 5.1, 5.3] time3 [5.8, 5.7, 5.6, 5.5, 5.9] fr_stat, p_value stats.friedmanchisquare(time1, time2, time3) print(Friedman 统计量:, fr_stat) print(p 值:, p_value)Friedman 检验要求每个时间点或者每个条件下的样本量完全一致。如果你的数据有缺失需要在跑检验之前先补齐或者剔除否则函数会直接报错。Spearman 秩相关则是衡量两个变量之间有没有单调相关关系。和 Pearson 相比它不要求线性关系也不要求数据服从正态分布只需要变量本身能排序。比如研究用户使用时长和满意度的关系满意度是 1 到 5 的整数用 Spearman 就比 Pearson 合理得多。usage_hours np.array([1.2, 2.5, 0.8, 3.4, 5.6, 2.1, 4.8, 3.0]) satisfaction np.array([2, 3, 2, 4, 5, 3, 4, 4]) rho, p_value stats.spearmanr(usage_hours, satisfaction) print(Spearman 相关系数:, rho) print(p 值:, p_value)3.6 如何快速选择检验方法我把上面这些方法整理成了一张选型表写代码之前对着这张表选基本不会出大错。研究问题数据特征检验方法scipy 函数一组数据是否符合某个分布连续单样本 KSkstest两组独立样本是否有差异连续/偏态/有序Mann–Whitney Umannwhitneyu两组配对样本是否有差异连续/偏态/有序Wilcoxon 符号秩wilcoxon多组独立样本是否有差异连续/偏态/有序Kruskal–Wallis Hkruskal多组配对样本是否有差异连续/偏态/有序Friedman 检验friedmanchisquare两个分类变量是否独立分类频数卡方独立性chi2_contingency两个连续变量是否单调相关连续/有序Spearman 秩相关spearmanr4. 实操中踩过的坑和排查思路4.1 坑 1KS 检验 p 值虚高因为你偷偷用了样本参数前面讲 Lilliefors 的时候提过这个问题但在实际项目里我见过太多人反复踩。典型场景是老板让你验证数据是否正态分布你拿到 1000 个样本直接算均值、标准差然后把它们传给kstestp 值显示 0.3于是放心地说“满足正态性”接着就去做 t 检验、方差分析。这套流程错得隐蔽因为代码不报错结果看起来也正常。但实际上你已经在检验过程中“作弊”了样本参数本身已经包含了样本分布的信息再去对照自己拟合出来的分布当然容易“自圆其说”。我的建议是检查分布是否正态优先用 Q-Q 图配合lilliefors或shapiro。shapiro对小样本更友好lilliefors对中等样本量不错大样本下这些检验很容易把微小偏离判定为显著所以还要结合图看偏离程度是否影响后续分析。4.2 坑 2Mann-Whitney 的版本差异导致结论翻车有段时间我们内部有个统计报表A 同事用服务器上的 scipy 跑出来的 p 值是 0.02B 同事在本机跑出来变成 0.04。两个人数据一模一样代码几乎一样只有 scipy 版本不一样。问题就出在mannwhitneyu的默认alternative参数上。早期版本没有暴露这个参数默认使用双侧检验后来版本加了参数默认值也是two-sided但不同改造版本、不同调用习惯下容易出现单侧双侧混乱。从此之后我养成了一个习惯调用mannwhitneyu时永远显式声明alternativetwo-sided并且保证项目里锁定 scipy 版本。统计分析最忌讳的就是“同样的数据在不同环境结果不同”版本管理和参数显式化能省掉一大半这种麻烦。4.3 坑 3Wilcoxon 遇到全零差值和缺失值Wilcoxon 符号秩检验的原理是对差值取绝对值后排序如果差值为 0这个样本就无法提供“正向还是负向”的信息。scipy 默认的处理方式是剔除零差值但这会让有效样本量变小。如果大部分差值都是 0比如一项政策前后数据根本没变化Wilcoxon 检验的结果会非常不可靠。更常见的是缺失值问题。很多人拿到的前后测数据不是一个完全配对表前一列有 100 条后一列有 90 条直接传给wilcoxon会报错。正确的做法是先把两个数组按 ID 对齐把没有配对上的样本删掉再做检验。这个“对齐”操作用 pandas 的merge或dropna都能解决但很多人会忽略。4.4 坑 4卡方检验明显违反复用条件卡方独立性检验的应用条件经常被忽视。第一样本要独立同一个人的行为数据不能拆成两半塞进列联表第二期望频数不能太小经验法则是 80% 以上的格子期望频数要大于 5期望频数更小的格子应该用 Fisher 精确检验或者蒙特卡洛模拟。我自己吃过一次亏。当时分析两个版本按钮的点击偏好样本量总共才 40 人20 个格子里的期望频数有三四个都小于 5卡方检验的 p 值说明“有显著差异”实际上这个结论几乎没有说服力。后来用 Fisher 精确检验一测p 值完全不同。所以只要列联表稀疏千万别迷信卡方。4.5 坑 5多元比较后的假阳性Kruskal–Wallis 检验显著后自然的想法是做两组两组的 Mann–Whitney U 检验找出到底是哪两组有差异。问题是如果你有三组就做 3 次两两比较有六组就要做 15 次。每次比较都有 5% 的犯错误风险做 10 次以上至少有一次假阳性的概率就会飙升到 40% 以上。解决办法有两个方向一是用 Bonferroni 校正把显著性水平除以比较次数简单粗暴但偏保守二是用 Dunn 检验或其他专为多重比较设计的后续检验统计功效更好。Python 里可以找找相关的现成库比如scikit-posthocs做这件事很方便。5. 怎么把非参数检验放进真实分析流程5.1 我的分析流程先看图再检验最后补效应量写代码只是最后一步真正的高手在跑检验之前就已经做了很多准备。我现在做统计分析基本固定走五步第一步画图。直方图、箱线图、Q-Q 图先扔出来看分布形态、看离群点、看组间差异的大致方向。第二步判断数据类型和分组方式。是连续还是有序是配对还是独立样本决定了该用哪个检验方法。第三步跑对应的非参数检验得到 p 值。第四步如果 p 值显著顺手计算效应量。比如 Mann–Whitney U 可以计算 Cliffs deltaKruskal–Wallis 可以计算 epsilon squared。p 值告诉你“有没有差异”效应量告诉你“差异有多大”业务方真正关心的往往是后者。第五步输出结论时把图和统计量一起放上去这样即使对方不了解统计检验也能从图上看出趋势。这套流程看着简单但能逼着你不盲目信任 p 值也能让你在业务汇报时更有底气。5.2 一个完整示例三组人群用户满意度对比最后给你一个完整的例子。假设我们要比较三个不同客户群体的满意度数据是 1 到 10 的整数值样本量每组 30 人左右数据分布明显左偏。这时候我一般会这样处理。import numpy as np import pandas as pd from scipy import stats # 业务数据模拟三组用户的满意度评分 np.random.seed(7) group_a np.random.randint(1, 8, size30) group_b np.random.randint(2, 9, size30) group_c np.random.randint(3, 10, size30) # 1. 分组画出箱线图确认形态这里省略绘图代码 # 2. 多组独立样本 顺序数据选 Kruskal-Wallis h_stat, p_value stats.kruskal(group_a, group_b, group_c) print(Kruskal-Wallis H:, h_stat, p:, p_value) # 3. 如果 p 显著再做两两比较并做 Bonferroni 校正 groups [group_a, group_b, group_c] names [A, B, C] comparisons [(0, 1), (0, 2), (1, 2)] alpha 0.05 n_comparisons len(comparisons) for i, j in comparisons: u_stat, p_val stats.mannwhitneyu(groups[i], groups[j], alternativetwo-sided) p_adjusted min(p_val * n_comparisons, 1.0) print(f{names[i]} vs {names[j]}: 原始 p{p_val:.4f}, 校正后 p{p_adjusted:.4f}) # 4. 计算效应量这里用 epsilon squared n_total sum(len(g) for g in groups) epsilon_sq h_stat / (n_total ** 2 - 1) / n_total * (n_total 1) print(Kruskal-Wallis 效应量 epsilon^2:, epsilon_sq)这个脚本基本就是我在小型用户研究项目里的模板。你可以在它的基础上扩展比如把绘图步骤加进去把结果统一汇总成 DataFrame也可以封装成函数以后换数据就直接复用。最后再分享一个小技巧不要只看 p 值一定要把检验统计量本身的数值也记下来。因为 p 值受样本量和效应大小双重影响对同样的业务差异样本增加以后 p 值很容易突然变显著但统计量和效应量的大小变化是渐变的。记录原始检验统计量多对比几次你对数据的理解会比别人深得多。
返回列表