ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Python中的KS检验与非参数检验:从原理到实战

Python中的KS检验与非参数检验:从原理到实战 写这篇博文的念头源于我在实际数据分析里被“非正态数据”折磨过好几回。不管是实验对比、AB测试还是用户行为分析只要数据不服从正态分布传统的t检验、ANOVA就开始“失灵”。而KS检验以及一系列非参数检验方法恰恰是为这种情况准备的。今天我就把自己常用的KS检验实现以及其余几个高频非参数检验在Python中的用法一次性整理出来。内容会偏向可直接落地的实现附带原理说明和踩坑记录适合正在做数据分析、想要搞清楚“什么时候该用哪种检验”的朋友。1. KS检验两个分布的“性格”到底一不一样1.1 KS检验到底在检验什么Kolmogorov-Smirnov检验简称KS检验是一种非参数检验方法核心是比较一个样本的经验分布函数与某个理论分布函数之间的差异或者比较两个样本的经验分布函数之间的差异。用人话说就是它回答的是“这组数据的真实分布跟我想象的分布或另一组数据的分布是不是同一个东西”。这个“差异”在数学上是用经验分布函数与目标分布函数之间的最大垂直距离来度量的。假设我们有n个观测值经验分布函数F_n(x)表示样本中不超过x的比例理论分布函数F(x)则表示在假设分布下不超过x的概率。KS统计量就是D max|F_n(x) - F(x)|如果D很大说明经验分布和理论分布偏离得比较明显这时候就有理由怀疑数据并不来自那个理论分布。在Python里scipy.stats.kstest函数帮我们把这个过程封装好了只需要传入数据和指定的分布名即可。在实际项目中我最常用KS检验的场景有两个单样本场景判断数据是否服从正态分布、均匀分布、指数分布等常见分布尤其是判断是否符合正态分布。双样本场景判断两组样本比如对照组和实验组是否来自相同的分布这种情况在AB测试里极其常见。1.2 单样本KS检验验证数据是否服从指定分布先看最简单的用法。假设我们收集了一批产品页面的访问时长数据想验证它是否符合正态分布。代码如下import numpy as np from scipy import stats # 模拟一批访问时长数据比如用户在某页面的停留秒数 np.random.seed(42) duration np.random.normal(loc120, scale30, size200) # 单样本KS检验默认检验是否符合标准正态分布均值0标准差1 ks_stat, p_value stats.kstest(duration, norm) print(fKS统计量: {ks_stat:.4f}) print(fP值: {p_value:.4f})运行之后大概率会得到非常小的p值这倒不是数据有问题而是因为我们的数据均值是120、标准差是30你拿它去和标准正态分布均值0、标准差1比较当然不匹配。所以这里需要传参把样本均值和样本标准差告诉kstestmu duration.mean() std duration.std() ks_stat, p_value stats.kstest(duration, norm, args(mu, std)) print(fKS统计量: {ks_stat:.4f}) print(fP值: {p_value:.4f})args(mu, std)表示我们假设的理论分布是均值为mu、标准差为std的正态分布。如果p值大于0.05说明没有足够证据拒绝原假设可以认为数据符合正态分布。这里有一个细节要特别注意单样本KS检验的理论分布参数如果是从样本中估计出来的检验结果会偏保守也就是更容易接受“服从指定分布”的原假设。换句话说你拿样本均值当总体均值去做KS检验实际上是“放水”了。严格意义上这种场景更适合用Lilliefors检验或Anderson-Darling检验这两种方法对参数估计的情况做了修正。但很多实际项目中确实有人这么用因为方便只是在汇报结论时要留个心眼。1.3 双样本KS检验判断两组样本是否来自同一分布双样本KS检验的实现同样简单用的是scipy.stats.ks_2samp。比如我们有对照组和实验组的转化率数据想看看两组的分布是否有显著差异# 对照组转化率模拟数据 control np.random.beta(2, 20, size300) # 实验组转化率模拟数据 treatment np.random.beta(2.5, 18, size300) ks_stat, p_value stats.ks_2samp(control, treatment) print(fKS统计量: {ks_stat:.4f}) print(fP值: {p_value:.4f})不需要任何分布假设ks_2samp直接比较两组经验分布函数的最大差异。如果p值小于0.05我们就可以认为两组样本的分布存在显著差异。双样本KS检验对分布的“整体差异”非常敏感不仅包含均值差异还包含方差差异、偏态差异、分布形态差异等。和t检验只盯着均值比KS检验看的是“整个分布是不是一样的”。所以在看两组数据除了平均值之外是否在尾部、峰度上也存在差异时KS检验是很有力的补充。实际使用中ks_2samp在样本量较小时会计算精确p值样本量较大时则用渐近分布scipy 1.7.0之后还增加了modeauto参数自动选择计算方法。我的建议是能默认就默认auto模式在绝大多数情况下已经挑好了合适的计算方式不用手动折腾。1.4 KS检验的局限和容易踩的坑KS检验很好用但绝对不是万能的。有一个非常经典的局限它对分布中心位置的差异比较敏感但对分布尾部的差异不太敏感。如果你的两组数据主要在尾部有差异比如极端值的比例不同KS检验的效力可能不够。这个时候Anderson-Darling检验会更合适它对尾部分布更敏感。另一个常见的坑是样本量的影响。KS检验在样本量非常大的时候微小的分布差异也会被判定为显著。比如你有10万条数据两组数据只是均值差了0.01KS检验很可能给出p 0.05。统计学显著不等于实际显著这个一定要结合业务场景去看差异到底大不大。还有一点单样本KS检验只能比较连续分布。如果你面对的是离散分布比如二项分布、泊松分布KS检验就不是首选了需要改用卡方拟合优度检验等方法。这个区分在实操中很容易被忽略。2. 组间比较的非参数检验t检验和ANOVA的“替补阵容”2.1 非参数检验方法选择的基本逻辑在数据分析中当我们想比较不同组的差异时第一反应往往是t检验两组或ANOVA多组。但这两者都有前提条件数据近似正态分布、组间方差齐性。一旦数据严重偏态、有较多极端值、或者样本量太小导致无法验证分布假设时参数检验就不太可靠了。这时候就需要非参数检验登场。非参数检验不依赖总体分布的具体形态通常基于数据的秩rank也就是把原始数值排序后的名次。它的好处是对异常值不敏感适用范围广代价是如果数据真的服从正态分布非参数检验的功效通常略低于参数检验。我的选择逻辑一般是这样的比较两组独立样本数据正态且方差齐时用独立样本t检验不满足时用Mann-Whitney U检验。比较两组配对样本差值近似正态时用配对t检验不满足时用Wilcoxon符号秩检验。比较多组独立样本ANOVA的前提不满足时用Kruskal-Wallis H检验。比较多组相关样本重复测量ANOVA的前提不满足时用Friedman检验。下面逐个说实现和细节。2.2 Mann-Whitney U检验两组独立样本的“中位数之争”Mann-Whitney U检验也叫Wilcoxon秩和检验是最常用的两独立样本非参数检验方法。它的原假设是两组样本来自相同的总体或者更直观地说两组数据的中位数没有显著差异。实现代码group_a np.random.normal(loc100, scale15, size50) group_b np.random.normal(loc108, scale15, size50) u_stat, p_value stats.mannwhitneyu(group_a, group_b, alternativetwo-sided) print(fU统计量: {u_stat:.4f}) print(fP值: {p_value:.4f})注意alternative参数它有三个选项two-sided双侧检验两组是否有差异、less第一组是否显著小于第二组、greater第一组是否显著大于第二组。根据业务问题选择单侧还是双侧不要无脑用双侧。实际使用中我还发现一个容易忽略的点Mann-Whitney U检验对两组数据的分布形状有一定隐含假设。如果两组数据的分布形状明显不同比如一组方差很大另一组方差很小那么检验结果显著时最好谨慎归因为“中位数不同”更应该解释为“两组数据的分布位置存在差异”。这件事在统计咨询里反复被强调但落到代码层面大家往往只看p值忽略了背后的解释范围。2.3 Wilcoxon符号秩检验配对样本的差异判断配对样本的经典场景是“前测-后测”同一批用户在活动前后分别测量指标或者同一批产品在调整前后各测一次性能。这时数据不是独立的而是配对的。如果差值不服从正态分布就使用Wilcoxon符号秩检验。before np.random.normal(loc50, scale10, size40) after before np.random.normal(loc3, scale4, size40) wilcoxon_stat, p_value stats.wilcoxon(before, after) print(fWilcoxon统计量: {wilcoxon_stat:.4f}) print(fP值: {p_value:.4f})函数会自动计算两两差值然后对差值的绝对值进行排序并赋予秩次再根据正负秩次来判断差值是否显著偏离0。这里有一个实操细节如果差值为0的数据点比较多wilcoxon的处理方式在不同版本的scipy中可能不同。新版本默认情况下会自动去掉零差值但这可能导致有效样本量减少。所以当你的数据出现很多差值恰好为0时要留意结果中实际参与计算的样本量避免误判。2.4 Kruskal-Wallis H检验与Friedman检验多组比较的利器多组independent样本比较时如果ANOVA的前提不满足我一般会直接用Kruskal-Wallis H检验。它是Mann-Whitney U检验扩展到多组情况的版本先对所有数据合并排序然后比较各组的平均秩次。group1 np.random.normal(loc10, scale2, size30) group2 np.random.normal(loc12, scale2, size30) group3 np.random.normal(loc14, scale2, size30) h_stat, p_value stats.kruskal(group1, group2, group3) print(fH统计量: {h_stat:.4f}) print(fP值: {p_value:.4f})Kruskal-Wallis检验显著后还需要做事后两两比较post hoc但目前scipy标准库没有直接提供配套的非参数事后检验函数通常需要借助scikit-posthocs这个库。没有这个库的话也可以拆成多组Mann-Whitney U检验但要对p值做Bonferroni校正避免多重比较带来的假阳性膨胀。Friedman检验则是多组相关样本场景下的选择比如同一批被试在三种不同条件下测量的结果或者同一种产品在多个时间点的重复测量。输入格式是二维数组行对应区组被试列对应处理条件# 模拟20个区组、3种处理条件 data_matrix np.random.rand(20, 3) friedman_stat, p_value stats.friedmanchisquare( data_matrix[:, 0], data_matrix[:, 1], data_matrix[:, 2] ) print(fFriedman统计量: {friedman_stat:.4f}) print(fP值: {p_value:.4f})Friedman检验的原假设是各处理条件下的总体分布没有差异。如果p值显著同样需要进一步做两两比较可以用Wilcoxon符号秩检验加Bonferroni校正。3. 相关性分析里的非参数方法Spearman与Kendalls Tau3.1 为什么不用Pearson在做变量相关性分析时大家习惯性先跑一个Pearson相关系数。但Pearson相关系数有一个隐含前提两个变量之间是线性关系并且数据近似服从双变量正态分布。如果数据是单调的但非线性比如指数关系、幂律关系或者存在明显异常值Pearson相关系数很容易给出误导性的结果。替代方案就是Spearman秩相关系数和Kendalls Tau系数。它们都是基于秩而非原始数值计算的相关性因此对异常值不敏感也能捕捉单调的非线性关系。两者的区别在于计算角度不同Spearman相关系数先将两个变量分别排序得到秩次再计算秩次之间的Pearson相关系数本质上是衡量两个变量秩次之间的线性关系。Kendalls Tau通过比较所有数据点之间的序对一致性来计算的更稳健适合样本量较小或重复值较多的情况。3.2 代码实现与解读x np.random.rand(80) * 10 # 构造一个非线性单调关系加上一点噪声 y np.exp(x / 5) np.random.normal(0, 0.3, 80) spearman_rho, spearman_p stats.spearmanr(x, y) kendall_tau, kendall_p stats.kendalltau(x, y) print(fSpearman相关系数: {spearman_rho:.4f}, P值: {spearman_p:.4f}) print(fKendalls Tau: {kendall_tau:.4f}, P值: {kendall_p:.4f})即使x和y不是线性关系只要单调递增或递减Spearman和Kendall都能给出较高的相关性值而Pearson可能会低估。个人经验在探索性数据分析阶段我会同时输出Pearson、Spearman、Kendall三个系数做对比。如果三者结论一致说明相关关系比较稳健如果差异较大说明数据可能存在异常值或非线性关系需要进一步可视化排查。4. 实战案例新功能上线后的效果评估4.1 问题背景与数据准备为了把这些方法串起来我模拟一个实际分析任务某产品更新了推荐算法需要评估新算法是否真的让用户点击率有了提升。我们有两种方式验证比较用户平均点击率是否有显著提升比较整体点击率的分布形态是否发生变化。第二个问题就非常适合用KS检验。下面我会演示包括数据生成、分布检验、组间比较、结论输出的完整流程。import numpy as np import pandas as pd from scipy import stats # 模拟数据新老算法下每个用户的点击率 np.random.seed(2024) n_users 500 click_old np.random.beta(2, 8, n_users) * 100 click_new np.random.beta(2.6, 7.8, n_users) * 100 # 转成DataFrame方便后续处理 df pd.DataFrame({ algorithm: [old] * n_users [new] * n_users, click_rate: np.concatenate([click_old, click_new]) })4.2 步骤一数据是否正态在决定用参数检验还是非参数检验之前先看一眼数据分布old_data df[df[algorithm] old][click_rate] new_data df[df[algorithm] new][click_rate] # 用KS检验做正态性验证 ks_old stats.kstest(old_data, norm, args(old_data.mean(), old_data.std())) ks_new stats.kstest(new_data, norm, args(new_data.mean(), new_data.std())) print(f旧算法点击率KS检验: 统计量{ks_old.statistic:.4f}, p值{ks_old.pvalue:.4f}) print(f新算法点击率KS检验: 统计量{ks_new.statistic:.4f}, p值{ks_new.pvalue:.4f})模拟数据来自Beta分布大概率p值会小于0.05说明数据不服从正态分布。这时候t检验就站不住脚了应该转向非参数检验。4.3 步骤二双样本KS检验看整体分布ks_stat, ks_p stats.ks_2samp(old_data, new_data) print(f双样本KS检验: 统计量{ks_stat:.4f}, p值{ks_p:.4f})p值小于0.05的话说明新旧算法的点击率分布存在显著差异。但这里要注意KS检验告诉你“有差异”没说“谁更好”也没说“差在哪里”。4.4 步骤三Mann-Whitney U检验看位置差异为了进一步确认新算法是否提升了点击率水平用Mann-Whitney U检验来做组间位置比较u_stat, u_p stats.mannwhitneyu(old_data, new_data, alternativeless) print(fMann-Whitney U检验: 统计量{u_stat:.4f}, p值{u_p:.4f})这里alternativeless的含义是旧算法的点击率是否显著小于新算法。如果p 0.05说明从统计上新算法的点击率分布位置确实高于旧算法和业务预期一致。同时可以看一下两组的中位数差异median_old old_data.median() median_new new_data.median() print(f旧算法点击率中位数: {median_old:.4f}) print(f新算法点击率中位数: {median_new:.4f}) print(f中位数提升幅度: {(median_new - median_old) / median_old * 100:.2f}%)把统计显著和业务显著结合起来看结论才完整。经常会遇到p值显著但提升幅度只有0.1%的情况这时候是否上线新功能就要结合成本和收益来综合判断了。4.5 完整流程小结这个案例完整展示了非参数检验在业务分析中的典型路径先做正态性检验判断是否能用参数检验不能则用KS检验对比整体分布再用Mann-Whitney U检验确认位置差异最后结合中位数的业务变化幅度下结论。这个方法组合不仅在线上AB测试中适用在用户分群分析、广告点击率对比、实体实验数据处理中都可以复用。5. 常见问题与排查技巧实录5.1 为什么我的KS检验p值永远都很小这是新手最容易困惑的问题。大概率原因是你用了标准正态分布去检验非标准化的数据。比如原始数据均值是100、标准差是15你直接kstest(data, norm)相当于拿一个均值0、方差1的正态分布和你的数据比较当然会拒绝原假设。解决办法就是用args(mu, std)传入估计的参数。但前面也提过样本内估计参数再做KS检验检验的保守性会偏高。如果你需要更严格的正态性判断推荐使用scipy.stats.normaltest它综合了偏度和峰度的信息或者用statsmodels里的lilliefors检验它专门处理均值和方差未知时正态性检验的问题。from statsmodels.stats.diagnostic import lilliefors # 替代单样本KS检验做正态性判断 lf_stat, lf_p lilliefors(old_data, distnorm) print(fLilliefors检验: 统计量{lf_stat:.4f}, p值{lf_p:.4f})5.2 样本量对检验结果影响太大怎么办样本量越大非参数检验越容易检测出细微差异。这是假设检验的天然属性任何方法都无法避免。我的处理思路是除了看p值之外额外计算效应量effect size。对于Mann-Whitney U检验可以计算两组秩均值差异对于KS检验统计量D本身就是一种效应量度量。把p值和效应量放在一起看能避免“统计显著但实际无意义”的尴尬。5.3 多重比较时的p值校正当你需要做多组两两对比时比如3个实验组加1个对照组两两比较就有6次直接使用0.05作为显著性水平会放大假阳性概率。常规做法是Bonferroni校正也就是用原始显著性水平除以比较次数。from itertools import combinations from scipy.stats import mannwhitneyu groups { control: np.random.normal(10, 2, 40), exp1: np.random.normal(11, 2, 40), exp2: np.random.normal(12, 2, 40), exp3: np.random.normal(10.5, 2, 40) } alpha 0.05 comparisons list(combinations(groups.keys(), 2)) corrected_alpha alpha / len(comparisons) for g1, g2 in comparisons: stat, p mannwhitneyu(groups[g1], groups[g2], alternativetwo-sided) print(f{g1} vs {g2}: p{p:.4f}, 是否显著{p corrected_alpha})Bonferroni校正偏保守但胜在简单、好解释、不用额外装库。如果你的比较次数特别多也可以考虑FDRFalse Discovery Rate校正但难度更高。5.4 数据里有缺失值和重复值怎么办缺失值好办scipy的检验函数不自动处理缺失值传入包含NaN的数据会得到nan。所以做检验前一定要先清洗数据推荐用pandas的dropna()过滤。重复值对非参数检验的影响分情况轻微重复问题不大但大量重复值比如某个值出现几十次会影响秩的分配方式导致检验结果失真。尤其是Mann-Whitney U检验和Wilcoxon检验处理大量并列秩tie时会采用校正公式scipy已经在内部处理了但你要意识到数据中重复值越多检验的区分能力越弱。如果出现大量重复值建议回到业务层面看一下指标本身是不是被离散化了。比如点击率明明可以取连续值但底层上报只保留了一位小数导致大量0.1、0.2、0.3这种重复值。解决办法是尽量获取更精细的原始数据或者在分析前做合适的平滑处理。5.5 检验结果与业务直觉不符时这是最让人头疼的情况。统计上没有显著性差异但业务上明明感觉有变化。我一般会从以下角度排查一是确认检验方法选对了没有。两组独立样本却用了配对检验结果自然不可靠。二是看看数据是不是被极端值拉偏了。重尾分布里少数极端值可能让非参数检验检测不到差异此时可以结合分位数对比、分布可视化辅助判断。三是检查样本量是否不足在功效不足的情况下真实差异可能检测不出来。反之统计上有显著差异但业务上感觉变化很小这时优先看一下效应量和分布重叠程度。我曾经遇到过两组均值差异只有0.5%但因为样本量达到10万p值小于0.001最终判定为“有统计意义但业务影响有限”。这种情况在AB测试中非常常见千万不要只报p值就完事。6. 写在最后的个人经验各种非参数检验方法在Python里的实现其实都不难难点在于理解每种方法适用的场景和背后的统计含义。我自己踩过最大的坑就是把KS检验当作“万能分布比较器”不管什么数据都拿着和正态分布比而忽略了参数估计对检验结果的影响。后来转向先画Q-Q图、先看直方图再用检验做定量判断整个分析流程才稳健起来。还有一个实用的小建议在团队内部做数据分析时可以把这几种检验封装成一个自定义函数输入数据和业务参数直接输出结果和建议既能统一分析标准也方便复盘。我之前用类似的方式把KS检验、Mann-Whitney U检验、Kruskal-Wallis检验都封装过一遍后续再用到类似场景时效率确实提高不少。再分享一个小细节使用scipy.stats做检验时不同版本之间API会有细微变化比如mannwhitneyu在旧版本中没有alternative参数ks_2samp在1.7.0之后多了mode参数。如果你在公司维护的项目里跑旧版本scipy的代码升级到新版本后报错了先看官方文档的release note。这种版本差异问题在数据分析项目中非常典型不是代码写错了而是要更新调用方式。非参数检验是数据分析工具箱里非常重要的组成部分它不一定比参数检验更高级但在很多场景下更稳健。希望这篇文章能帮你把这些方法真正用起来让分析结论更经得起推敲。
返回列表