Kruskal-Wallis H 检验:Python 实操与深度拓展

83 阅读4分钟

一、Kruskal-Wallis H 检验核心认知

1. 适用场景(与 ANOVA 检验的区别)

  • 传统单因素 ANOVA 检验要求数据满足正态分布方差齐性,但实际数据(如用户消费金额、产品满意度评分)常存在偏态或异常值,此时需用非参数检验替代;

  • Kruskal-Wallis H 检验(简称 K-W 检验)是非参数版的单因素 ANOVA,核心用于检验:多个独立样本的总体分布是否存在显著差异(无需假设数据正态分布、方差齐性);

  • 核心逻辑:将所有样本数据混合排序并分配秩次,通过比较各组的秩和是否存在显著差异,判断组间是否有统计学差异(原假设 H0:各组总体分布相同;备择假设 H1:至少两组总体分布不同)。

2. 关键适用条件

  • 样本:3 组及以上独立样本(若仅 2 组,建议用 Mann-Whitney U 检验,即非参数版独立样本 t 检验);

  • 数据类型:数值型变量(连续型或离散型有序数据,如评分 1-5 分);

  • 核心优势:抗异常值、不依赖分布假设,适用场景比 ANOVA 更广泛(如电商用户不同年龄段的消费差异、不同营销渠道的转化效果对比)。

二、Python 实操:Kruskal-Wallis H 检验完整流程

以 “3 个营销渠道(A/B/C)的用户转化金额” 为例(数据非正态分布,方差不齐,不满足 ANOVA 条件),演示全流程:

1. 数据准备与前提验证

import pandas as pd

import numpy as np

from scipy import stats

import seaborn as sns

import matplotlib.pyplot as plt

\# 设置中文显示

plt.rcParams\['font.sans-serif'] = \['SimHei']

plt.rcParams\['axes.unicode\_minus'] = False

\# 模拟数据(3 个渠道的转化金额,故意加入偏态和异常值)

data = pd.DataFrame({

    'channel': \['A']\*30 + \['B']\*30 + \['C']\*30,

    'conversion\_amount': np.concatenate(\[

        np.random.lognormal(3, 1, 30),  # 渠道 A:对数正态分布(偏态)

        np.random.lognormal(2.5, 1.2, 30),  # 渠道 B:对数正态分布(偏态+方差更大)

        np.random.lognormal(3.2, 0.8, 30)   # 渠道 C:对数正态分布(偏态)

    ])

})

\# 前提验证:1. 正态性检验(Shapiro-Wilk 检验);2. 方差齐性检验(Levene 检验)

for channel in \['A', 'B', 'C']:

    stat\_shapiro, p\_shapiro = stats.shapiro(data\[data\['channel']==channel]\['conversion\_amount'])

    print(f"渠道 {channel} 正态性检验 p 值:{p\_shapiro:.4f}")  # 均 05,拒绝正态假设

stat\_levene, p\_levene = stats.levene(

    data\[data\['channel']=='A']\['conversion\_amount'],

    data\[data\['channel']=='B']\['conversion\_amount'],

    data\[data\['channel']=='C']\['conversion\_amount']

)

print(f"方差齐性检验 p 值:{p\_levene:.4f}")  # 5,拒绝方差齐性假设

\# 结论:数据不满足 ANOVA 条件,改用 Kruskal-Wallis H 检验

2. Kruskal-Wallis H 检验执行

\# 提取各组数据

group\_a = data\[data\['channel']=='A']\['conversion\_amount']

group\_b = data\[data\['channel']=='B']\['conversion\_amount']

group\_c = data\[data\['channel']=='C']\['conversion\_amount']

\# 执行 Kruskal-Wallis H 检验(scipy 内置函数)

stat\_kw, p\_kw = stats.kruskal(group\_a, group\_b, group\_c)

\# 输出结果

print("="\*50)

print("Kruskal-Wallis H 检验结果")

print("="\*50)

print(f"H 统计量:{stat\_kw:.4f}")

print(f"p 值:{p\_kw:.4f}")

print("="\*50)

\# 结论判断(显著性水平 α=0.05)

alpha = 0.05

if p\_kw (f"p 值({p\_kw:.4f})alpha}),拒绝原假设!")

    print("结论:至少有两个营销渠道的用户转化金额总体分布存在显著差异")

else:

    print(f"p 值({p\_kw:.4f})≥ α({alpha}),不能拒绝原假设!")

    print("结论:三个营销渠道的用户转化金额总体分布无显著差异")

3. 事后检验(关键补充!)

Kruskal-Wallis H 检验仅能判断 “是否存在组间差异”,但无法明确 “哪两组有差异”,需通过事后检验定位:

\# 方法 1:Dunn 检验(最常用,校正多重比较误差)

from statsmodels.stats.multicomp import pairwise\_tukeyhsd

from scipy.stats import rankdata

\# 对所有数据进行秩转换(Dunn 检验基于秩次)

data\['rank'] = rankdata(data\['conversion\_amount'])

\# 执行 Dunn 检验(statsmodels 无直接函数,用 Tukey HSD 替代秩次检验,结果一致)

tukey\_result = pairwise\_tukeyhsd(

    endog=data\['rank'],  # 秩次数据

    groups=data\['channel'],

    alpha=0.05

)

print("\n事后检验(Tukey HSD 基于秩次)结果:")

print(tukey\_result)

\# 方法 2:Bonferroni 校正的 Mann-Whitney U 检验(两两对比)

print("\nBonferroni 校正的两两 Mann-Whitney U 检验:")

p\_values = \[]

pairs = \[('A', 'B'), ('A', 'C'), ('B', 'C')]

for pair in pairs:

    stat\_u, p\_u = stats.mannwhitneyu(

        data\[data\['channel']==pair\[0]]\['conversion\_amount'],

        data\[data\['channel']==pair\[1]]\['conversion\_amount']

    )

    p\_corrected = p\_u \* 3  # Bonferroni 校正:3 组共 3 对,乘以 3

    p\_values.append(p\_corrected)

    print(f"{pair\[0]} vs {pair\[1]}:校正后 p 值 = {p\_corrected:.4f}(原 p 值 = {p\_u:.4f})")

    if p\_corrected 

        print(f"  → 存在显著差异")

    else:

        print(f"  → 无显著差异")

4. 结果可视化(辅助解读)

\# 箱线图:直观展示各组数据分布差异

sns.boxplot(x='channel', y='conversion\_amount', data=data)

plt.title('不同营销渠道用户转化金额分布')

plt.xlabel('营销渠道')

plt.ylabel('转化金额')

plt.show()

\# 秩和条形图:展示各组秩和差异(K-W 检验的核心依据)

rank\_sum = data.groupby('channel')\['rank'].sum().reset\_index()

sns.barplot(x='channel', y='rank', data=rank\_sum)

plt.title('不同营销渠道的秩和对比')

plt.xlabel('营销渠道')

plt.ylabel('秩和(越大表示整体数值越高)')

plt.show()