未记录事实核验 · 状态:现行内容
记录仅说明核验时间与维护状态,不保证适用于所有环境;使用前请留意正文中的版本、适用条件与风险提示。
warning 适用边界与维护说明
本文适用于学习独立观测的分类计数数据分析,包括独立性检验、适合性检验、期望频数检查及效应量计算;各代码段应按正文顺序执行,以复用前面的导入。每格期望频数至少5是本文采用的保守检查规则,放宽为低于5的格子不超过20%时,还须满足所有期望频数至少为1并结合表结构判断。Fisher示例仅演示2×2表,不是任意稀疏大表的直接替代方案;多维联合独立性不等于逐对关联。效应量示例已使用未做Yates连续性校正的统计量,其中Phi计算给出非负关联强度,不表达关联方向;固定效应量分级和残差绝对值大于2仅作辅助、探索性描述。不显著不能证明公平或独立,显著关联也不能证明因果。独立性示例的两幅堆积图实际按产品展示性别构成,不能按标题理解为各性别内部的产品偏好分布。
🟧 卡方检验(Chi-square Test)
🎯 核心概念
note 📖 **什么是卡方检验?**
卡方检验是用于检验分类变量之间是否存在关联性的非参数统计方法,通过比较观察频数与期望频数的差异来判断变量间的独立性。
核心思想:
- 检验实际观察到的频数与理论期望频数是否有显著差异
- 基于卡方分布进行统计推断
- 适用于分类数据的关联性分析
📊 两种主要卡方检验类型
1. 🔍 独立性检验 (Test of Independence)
用途:检验两个分类变量之间是否独立
适用条件:
- 数据为分类变量
- 观察值相互独立
- 期望频数≥5(每个单元格)
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from scipy.stats import chi2_contingency, chi2
import seaborn as sns
def independence_test_demo():
"""独立性检验演示"""
# 场景:研究性别与产品偏好的关系
# 调查200名消费者对三种产品的偏好
# 创建列联表数据
data = {
'产品A': [45, 25], # [男性喜欢, 女性喜欢]
'产品B': [30, 40], # [男性喜欢, 女性喜欢]
'产品C': [25, 35] # [男性喜欢, 女性喜欢]
}
# 转换为列联表
contingency_table = pd.DataFrame(data, index=['男性', '女性'])
print("=== 独立性检验:性别与产品偏好 ===")
print("观察频数表:")
print(contingency_table)
print(f"\n总样本量: {contingency_table.sum().sum()}")
# 执行卡方独立性检验
chi2_stat, p_value, dof, expected_freq = chi2_contingency(contingency_table)
print(f"\n检验结果:")
print(f"卡方统计量: {chi2_stat:.4f}")
print(f"p值: {p_value:.4f}")
print(f"自由度: {dof}")
# 期望频数
expected_df = pd.DataFrame(expected_freq,
index=contingency_table.index,
columns=contingency_table.columns)
print(f"\n期望频数表:")
print(expected_df.round(2))
# 判断结果
alpha = 0.05
if p_value < alpha:
print(f"\n结论: p值 < {alpha},拒绝原假设,性别与产品偏好有显著关联")
else:
print(f"\n结论: p值 >= {alpha},不拒绝原假设,未检出性别与产品偏好的显著关联;这不等于证明两者独立")
# 效应量计算 (Cramér's V)
n = contingency_table.sum().sum()
cramers_v = np.sqrt(chi2_stat / (n * (min(contingency_table.shape) - 1)))
print(f"Cramér's V (效应量): {cramers_v:.4f}")
# 效应量解释
if cramers_v < 0.1:
effect_size = "很小"
elif cramers_v < 0.3:
effect_size = "小"
elif cramers_v < 0.5:
effect_size = "中等"
else:
effect_size = "大"
print(f"效应量大小: {effect_size}")
# 可视化
fig, axes = plt.subplots(2, 2, figsize=(15, 10))
# 观察频数热力图
sns.heatmap(contingency_table, annot=True, fmt='d', cmap='Blues', ax=axes[0, 0])
axes[0, 0].set_title('观察频数')
# 期望频数热力图
sns.heatmap(expected_df, annot=True, fmt='.1f', cmap='Oranges', ax=axes[0, 1])
axes[0, 1].set_title('期望频数')
# 堆积条形图
contingency_table.T.plot(kind='bar', stacked=True, ax=axes[1, 0], color=['lightblue', 'lightcoral'])
axes[1, 0].set_title('各产品中的性别人数构成')
axes[1, 0].set_xlabel('产品')
axes[1, 0].set_ylabel('人数')
axes[1, 0].legend(title='性别')
# 比例堆积条形图
proportions = contingency_table.div(contingency_table.sum(axis=0), axis=1)
proportions.T.plot(kind='bar', stacked=True, ax=axes[1, 1], color=['lightblue', 'lightcoral'])
axes[1, 1].set_title('各产品中的性别比例构成')
axes[1, 1].set_xlabel('产品')
axes[1, 1].set_ylabel('比例')
axes[1, 1].legend(title='性别')
plt.tight_layout()
plt.show()
return contingency_table, chi2_stat, p_value
# 执行独立性检验
cont_table, chi2_stat, p_val = independence_test_demo()
2. 🎲 适合性检验 (Goodness of Fit Test)
用途:检验观察到的频数分布是否符合某个理论分布
适用条件:
- 数据为分类变量
- 有明确的理论分布期望
- 期望频数≥5
def goodness_of_fit_test_demo():
"""适合性检验演示"""
# 场景:检验骰子是否公平
# 投掷骰子600次,记录每个点数出现的次数
np.random.seed(42)
# 模拟骰子投掷结果(略微偏向某些数字)
dice_rolls = np.random.choice([1, 2, 3, 4, 5, 6],
size=600,
p=[0.15, 0.15, 0.18, 0.17, 0.18, 0.17]) # 不均匀概率
# 统计各点数频次
observed_freq = np.bincount(dice_rolls)[1:] # 去掉0的计数
dice_faces = np.arange(1, 7)
print("=== 适合性检验:骰子公平性 ===")
print("观察频数:")
for face, freq in zip(dice_faces, observed_freq):
print(f"点数{face}: {freq}次")
# 理论期望频数(公平骰子每个面1/6概率)
total_rolls = len(dice_rolls)
expected_freq = np.full(6, total_rolls / 6)
print(f"\n期望频数(每个点数): {expected_freq[0]:.1f}次")
# 执行适合性检验
from scipy.stats import chisquare
chi2_stat, p_value = chisquare(observed_freq, expected_freq)
print(f"\n检验结果:")
print(f"卡方统计量: {chi2_stat:.4f}")
print(f"p值: {p_value:.4f}")
print(f"自由度: {len(observed_freq) - 1}")
alpha = 0.05
if p_value < alpha:
print(f"\n结论: p值 < {alpha},拒绝原假设,骰子不公平")
else:
print(f"\n结论: p值 >= {alpha},不拒绝原假设,尚无充分证据认为骰子偏离公平分布;这不等于证明骰子公平")
# 计算标准化残差
standardized_residuals = (observed_freq - expected_freq) / np.sqrt(expected_freq)
print(f"\n各点数的标准化残差:")
for face, residual in zip(dice_faces, standardized_residuals):
print(f"点数{face}: {residual:.3f}")
# 可视化
fig, axes = plt.subplots(2, 2, figsize=(15, 10))
# 观察频数vs期望频数
x = np.arange(len(dice_faces))
width = 0.35
axes[0, 0].bar(x - width/2, observed_freq, width, label='观察频数', alpha=0.8, color='skyblue')
axes[0, 0].bar(x + width/2, expected_freq, width, label='期望频数', alpha=0.8, color='lightcoral')
axes[0, 0].set_xlabel('点数')
axes[0, 0].set_ylabel('频数')
axes[0, 0].set_title('观察频数 vs 期望频数')
axes[0, 0].set_xticks(x)
axes[0, 0].set_xticklabels(dice_faces)
axes[0, 0].legend()
# 标准化残差图
axes[0, 1].bar(dice_faces, standardized_residuals, color=['red' if abs(r) > 2 else 'blue' for r in standardized_residuals])
axes[0, 1].axhline(y=2, color='red', linestyle='--', alpha=0.7, label='±2标准差')
axes[0, 1].axhline(y=-2, color='red', linestyle='--', alpha=0.7)
axes[0, 1].set_xlabel('点数')
axes[0, 1].set_ylabel('标准化残差')
axes[0, 1].set_title('标准化残差(绝对值>2为探索性提示,非校正后的显著性结论)')
axes[0, 1].legend()
# 卡方分布图
df = len(observed_freq) - 1
x_chi2 = np.linspace(0, 20, 1000)
y_chi2 = chi2.pdf(x_chi2, df)
axes[1, 0].plot(x_chi2, y_chi2, 'b-', label=f'卡方分布(df={df})')
axes[1, 0].axvline(chi2_stat, color='red', linestyle='--', label=f'观察统计量: {chi2_stat:.2f}')
# 临界值
critical_value = chi2.ppf(0.95, df)
axes[1, 0].axvline(critical_value, color='orange', linestyle=':', label=f'临界值(α=0.05): {critical_value:.2f}')
axes[1, 0].set_xlabel('卡方值')
axes[1, 0].set_ylabel('概率密度')
axes[1, 0].set_title('卡方分布与检验统计量')
axes[1, 0].legend()
# 骰子投掷序列图(前100次)
axes[1, 1].plot(dice_rolls[:100], 'o-', markersize=3, alpha=0.7)
axes[1, 1].set_xlabel('投掷次数')
axes[1, 1].set_ylabel('点数')
axes[1, 1].set_title('骰子投掷序列(前100次)')
axes[1, 1].set_ylim(0.5, 6.5)
axes[1, 1].grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
return dice_rolls, observed_freq, expected_freq, chi2_stat, p_value
# 执行适合性检验
rolls, obs_freq, exp_freq, chi2_gof, p_gof = goodness_of_fit_test_demo()
🎯 卡方检验选择指南
📋 决策流程
def choose_chi_square_test():
"""帮助选择合适的卡方检验类型"""
print("=== 卡方检验类型选择指南 ===")
print("1. 你的研究问题是什么?")
print(" A. 检验两个分类变量是否独立;多维列联表检验联合独立性,不等同于逐对关联检验")
print(" → 使用独立性检验 (chi2_contingency)")
print(" B. 检验观察到的分布是否符合某个理论分布")
print(" → 使用适合性检验 (chisquare)")
print()
print("2. 数据要求检查:")
print(" - 数据必须是分类变量(名义或有序)")
print(" - 观察值必须相互独立")
print(" - 期望频数 ≥ 5(每个单元格)")
print(" - 样本量足够大")
choose_chi_square_test()
🏢 实际应用场景对照
| 检验类型 | 应用场景 | 例子 | Python函数 |
|---|---|---|---|
| 独立性检验 | 两个分类变量的关联 | 性别与购买偏好、教育水平与收入等级 | chi2_contingency() |
| 适合性检验 | 分布符合度 | 基因型分布、产品缺陷率、随机性检验 | chisquare() |
📐 卡方检验的假设与条件检验
🔍 期望频数检查
def check_expected_frequencies():
"""检查期望频数是否满足条件"""
# 创建一个期望频数过小的例子
small_sample = np.array([[5, 2, 1],
[3, 1, 0]])
print("=== 期望频数检查 ===")
print("观察频数表:")
print(small_sample)
# 计算期望频数
chi2_stat, p_value, dof, expected = chi2_contingency(small_sample)
print("\n期望频数表:")
print(expected.round(2))
# 检查期望频数条件
min_expected = np.min(expected)
cells_below_5 = np.sum(expected < 5)
total_cells = expected.size
print(f"\n期望频数检查:")
print(f"最小期望频数: {min_expected:.2f}")
print(f"期望频数<5的单元格数: {cells_below_5}/{total_cells}")
print(f"比例: {cells_below_5/total_cells:.1%}")
if min_expected >= 5:
print("✅ 所有期望频数≥5,满足检验条件")
elif min_expected >= 1 and cells_below_5/total_cells <= 0.2:
print("⚠️ 所有期望频数≥1,且不超过20%的单元格期望频数<5;按这一常用近似判断规则可谨慎使用,并应结合表结构评估")
else:
print("❌ 期望频数过小,不适合使用卡方检验")
print("建议: 合并类别或使用Fisher精确检验")
check_expected_frequencies()
🧮 效应量与实际意义
📊 Cramér's V 和 Phi 系数
def effect_size_demo():
"""演示卡方检验的效应量计算"""
# 创建不同关联强度的数据
datasets = {
'强关联': np.array([[80, 20], [20, 80]]),
'中等关联': np.array([[60, 40], [40, 60]]),
'弱关联': np.array([[55, 45], [45, 55]]),
'无关联': np.array([[50, 50], [50, 50]])
}
print("=== 效应量分析 ===")
results = []
for name, table in datasets.items():
# 此处检验和效应量统一采用未做Yates连续性校正的Pearson卡方统计量
chi2_stat, p_value, dof, expected = chi2_contingency(table, correction=False)
n = table.sum()
# Cramér's V
cramers_v = np.sqrt(chi2_stat / (n * (min(table.shape) - 1)))
# Phi系数(仅适用于2x2表)
if table.shape == (2, 2):
phi = np.sqrt(chi2_stat / n)
else:
phi = None
results.append({
'name': name,
'chi2': chi2_stat,
'p_value': p_value,
'cramers_v': cramers_v,
'phi': phi
})
print(f"\n{name}:")
print(f" 卡方统计量: {chi2_stat:.4f}")
print(f" p值: {p_value:.4f}")
print(f" Cramér's V: {cramers_v:.4f}")
if phi is not None:
print(f" Phi系数: {phi:.4f}")
# 可视化效应量
plt.figure(figsize=(12, 8))
names = [r['name'] for r in results]
cramers_v_values = [r['cramers_v'] for r in results]
# 效应量条形图
plt.subplot(2, 3, 1)
bars = plt.bar(names, cramers_v_values, color=['red', 'orange', 'yellow', 'lightgray'])
plt.title("Cramér's V 效应量对比")
plt.ylabel("Cramér's V")
plt.xticks(rotation=45)
# 添加效应量解释线
plt.axhline(y=0.1, color='green', linestyle='--', alpha=0.7, label='小效应(0.1)')
plt.axhline(y=0.3, color='blue', linestyle='--', alpha=0.7, label='中效应(0.3)')
plt.axhline(y=0.5, color='red', linestyle='--', alpha=0.7, label='大效应(0.5)')
plt.legend()
# 展示数据表
for i, (name, table) in enumerate(datasets.items()):
plt.subplot(2, 3, i + 2)
sns.heatmap(table, annot=True, fmt='d', cmap='Blues', cbar=False)
plt.title(f'{name}\nCramér\'s V = {cramers_v_values[i]:.3f}')
plt.tight_layout()
plt.show()
effect_size_demo()
🔧 卡方检验的替代方法
🎲 Fisher精确检验
def fisher_exact_test_demo():
"""Fisher精确检验演示(适用于小样本)"""
from scipy.stats import fisher_exact
# 小样本2x2列联表
small_table = np.array([[8, 2],
[1, 5]])
print("=== Fisher精确检验 ===")
print("观察频数表:")
print(small_table)
print(f"总样本量: {small_table.sum()}")
# Fisher精确检验
odds_ratio, p_fisher = fisher_exact(small_table)
# 卡方检验对比
chi2_stat, p_chi2, dof, expected = chi2_contingency(small_table)
print(f"\n检验结果对比:")
print(f"Fisher精确检验 p值: {p_fisher:.4f}")
print(f"卡方检验 p值: {p_chi2:.4f}")
print(f"比值比 (Odds Ratio): {odds_ratio:.4f}")
# 期望频数检查
print(f"\n期望频数:")
print(expected.round(2))
print(f"最小期望频数: {np.min(expected):.2f}")
if np.min(expected) < 5:
print("⚠️ 期望频数过小,推荐使用Fisher精确检验")
else:
print("✅ 期望频数满足条件,可使用卡方检验")
fisher_exact_test_demo()
⚠️ 使用注意事项与常见误区
🚫 常见错误
def common_mistakes_demo():
"""演示卡方检验的常见错误"""
print("=== 卡方检验常见错误 ===")
# 错误1:期望频数过小
print("错误1: 期望频数过小")
small_freq_table = np.array([[2, 1], [1, 2]])
chi2_stat, p_value, dof, expected = chi2_contingency(small_freq_table)
print(f"期望频数最小值: {np.min(expected):.2f}")
print("问题: 期望频数<5,检验结果不可靠")
print("解决: 合并类别或使用Fisher精确检验\n")
# 错误2:用于连续变量
print("错误2: 错误的数据类型")
print("卡方检验只适用于分类数据,不能用于连续变量")
print("错误做法: 将身高、收入等连续变量直接用于卡方检验")
print("正确做法: 先将连续变量分组或使用相关分析\n")
# 错误3:忽略多重比较
print("错误3: 多重比较问题")
print("同时进行多个卡方检验会增加犯第一类错误的概率")
print("解决方案: Bonferroni校正或FDR控制\n")
# 错误4:因果推断
print("错误4: 混淆关联与因果")
print("卡方检验只能说明变量间有关联,不能证明因果关系")
print("需要: 结合研究设计和领域知识进行因果推断")
common_mistakes_demo()
💡 最佳实践建议
-
数据准备:
- 确保数据为分类变量
- 检查期望频数条件
- 验证样本独立性
-
检验选择:
- 根据研究问题选择检验类型
- 小样本考虑Fisher精确检验
- 大表格考虑合并稀少类别
-
结果解释:
- 报告效应量(Cramér's V)
- 注意统计显著性vs实际意义
- 避免因果关系推断
-
后续分析:
- 检查标准化残差识别异常单元格
- 进行敏感性分析
- 考虑其他相关分析方法
🔗 相关知识
#统计学 #卡方检验 #假设检验 #Python