如何用Python Pandas量化两列数据分布的统计差异?
Hey there! 既然你已经用柱状图直观对比了两列分类数据的分布,咱们来聊聊怎么量化它们的统计差异。针对你给出的示例数据,我整理了几种实用的方法,从统计检验到差异指标,再到结合可视化的优化:
1. 统计显著性检验:判断分布是否真的不同
如果想知道两列数据的分布是否存在统计学上的显著差异,分类数据最常用的是卡方检验(Chi-square Test),它通过对比观测频数和期望频数来判断分布是否一致。
代码实现:
import pandas as pd from scipy.stats import chi2_contingency # 你的示例数据 df = pd.DataFrame({'a':['cat','dog','bird','cat','dog','dog','dog'], 'b':['cat','cat','cat','bird','dog','dog','dog']}) # 第一步:生成列联表(Contingency Table),展示两列数据的交叉频数 contingency_table = pd.crosstab(df['a'], df['b']) print("列联表:") print(contingency_table) # 第二步:执行卡方检验 chi2_stat, p_val, dof, expected_freq = chi2_contingency(contingency_table) # 输出结果 print(f"\n卡方统计量: {chi2_stat:.4f}") print(f"p值: {p_val:.4f}") print(f"自由度: {dof}") print("期望频数(如果分布一致的理想情况):") print(expected_freq.round(2))
结果解释:
- p值是核心判断依据:如果p值 < 0.05(常用的显著性水平),说明两列分布存在显著差异;反之则没有足够证据证明差异存在。
- 你的示例数据中,p值大约是0.65,说明这两列的分布没有显著统计差异。
如果你偏好更稳健的检验,也可以用对数似然比检验,只需要在chi2_contingency中添加参数lambda_='log-likelihood'即可,逻辑和卡方检验一致。
2. 量化差异大小:描述差异的程度
如果不需要显著性判断,只是想直观描述两列分布的差异程度,可以用这些指标:
相对频率差异
直接计算每个类别在两列中的占比差值,能清晰看到每个类别的差异方向和大小:
# 计算每列的相对频率(占比) freq_a = df['a'].value_counts(normalize=True).sort_index() freq_b = df['b'].value_counts(normalize=True).sort_index() # 合并并计算差异 freq_diff = pd.DataFrame({ 'a占比': freq_a, 'b占比': freq_b, 'a-b差值': freq_a - freq_b }).round(4) print("\n相对频率差异:") print(freq_diff)
结果中,a-b差值为正表示该类别在a列占比更高,负则相反。
KL散度(Kullback-Leibler Divergence)
衡量两个概率分布的“距离”,注意它是非对称的(即从a到b的差异和从b到a的差异结果不同):
from scipy.special import rel_entr # 计算KL散度:D(a||b) 表示用b的分布去拟合a的分布时的信息损失 kl_ab = sum(rel_entr(freq_a, freq_b)) # 计算KL散度:D(b||a) kl_ba = sum(rel_entr(freq_b, freq_a)) print(f"\nKL散度(a相对于b): {kl_ab:.4f}") print(f"KL散度(b相对于a): {kl_ba:.4f}")
数值越大,说明两个分布的差异越大。
Jensen-Shannon散度
是KL散度的对称版本,取值范围在0到1之间,越接近1表示差异越大:
from scipy.spatial.distance import jensenshannon js_div = jensenshannon(freq_a, freq_b) print(f"\nJensen-Shannon散度: {js_div:.4f}")
3. 结合可视化:让差异更直观
既然你已经画了柱状图,可以在图上标注频数或差异值,让可视化更有信息量:
import matplotlib.pyplot as plt # 先计算每列的频数 counts_a = df['a'].value_counts().sort_index() counts_b = df['b'].value_counts().sort_index() # 绘图:调整柱子宽度避免重叠 fig, ax = plt.subplots() bar_width = 0.35 x = range(len(counts_a.index)) # 绘制两列的柱状图 bars_a = ax.bar([i - bar_width/2 for i in x], counts_a, width=bar_width, color='blue', alpha=0.8, label='a') bars_b = ax.bar([i + bar_width/2 for i in x], counts_b, width=bar_width, color='maroon', alpha=1, label='b') # 标注每个柱子的频数 for bar in bars_a: height = bar.get_height() ax.text(bar.get_x() + bar.get_width()/2., height, f'{height}', ha='center', va='bottom') for bar in bars_b: height = bar.get_height() ax.text(bar.get_x() + bar.get_width()/2., height, f'{height}', ha='center', va='bottom') # 标注两列的差值 for i in x: diff = counts_a.iloc[i] - counts_b.iloc[i] ax.text(i, max(counts_a.iloc[i], counts_b.iloc[i]) + 0.1, f'diff: {diff}', ha='center', va='bottom', color='red') # 调整坐标轴和图例 ax.set_xticks(x) ax.set_xticklabels(counts_a.index) ax.legend() plt.show()
这样你的柱状图不仅能看到分布,还能直接看到每个类别的数量差异,一目了然。
内容的提问来源于stack exchange,提问作者jxn
相关产品推荐
相关产品推荐

