2. 缺失值机制分析

任务目标

本案例基于一份含缺失值的模拟金融数据集,通过统计检验判断缺失值属于完全随机缺失(MCAR)、随机缺失(MAR)还是非随机缺失(MNAR)。核心是执行Little's MCAR检验,并结合分组均值对比辅助判断MAR与MNAR。

环境与依赖

需要Python 3.8+,并安装以下库:

pip install pandas numpy scipy

数据构造与缺失机制说明

为便于验证,我们构造一个包含1000条记录、3个变量的数据集:income(年收入,单位千元)、age(年龄)、credit_score(信用分)。其中income存在缺失,缺失机制设定为:

  • income缺失概率与age相关(年龄越大越易缺失)——这属于MAR;
  • 同时,income本身值越高缺失概率略增——这引入了MNAR成分。

因此,Little's检验应显著拒绝MCAR(p<0.05),而分组均值对比将显示:income缺失组的平均年龄显著高于非缺失组(提示MAR),且缺失组的真实收入均值高于非缺失组(提示MNAR)。

关键完整代码

import pandas as pd
import numpy as np
from scipy.stats import chi2
from scipy.stats import ttest_ind

# ---------- 1. 构造含缺失的模拟数据 ----------
np.random.seed(42)
n = 1000
age = np.random.normal(45, 10, n).clip(20, 80)
income = 30 + 0.5 * age + np.random.normal(0, 8, n)  # 收入与年龄相关
credit_score = 650 + 0.3 * income + np.random.normal(0, 30, n)

df_full = pd.DataFrame({'age': age, 'income': income, 'credit_score': credit_score})

# 生成缺失指示:MAR(受age影响)+ MNAR(受income本身影响)
logit = -2 + 0.03 * (age - 45) + 0.02 * (income - np.mean(income))
prob_miss = 1 / (1 + np.exp(-logit))
prob_miss = np.clip(prob_miss, 0.05, 0.6)
miss_mask = np.random.rand(n) < prob_miss

df = df_full.copy()
df.loc[miss_mask, 'income'] = np.nan

# ---------- 2. Little's MCAR检验 ----------
# 按缺失模式分组(此处仅income有缺失,故两组:完整组与income缺失组)
df['miss_ind'] = df['income'].isna().astype(int)
group0 = df[df['miss_ind'] == 0][['age', 'credit_score']]
group1 = df[df['miss_ind'] == 1][['age', 'credit_score']]

# 计算各组均值向量与协方差矩阵(合并组内协方差)
mean0 = group0.mean().values
mean1 = group1.mean().values
n0 = len(group0)
n1 = len(group1)
cov0 = np.cov(group0.values, rowvar=False)
cov1 = np.cov(group1.values, rowvar=False)
# 合并协方差(按样本量加权)
cov_pooled = ((n0 - 1) * cov0 + (n1 - 1) * cov1) / (n0 + n1 - 2)
cov_pooled_inv = np.linalg.inv(cov_pooled)

# Little's统计量:各组均值差平方和(仅对完整变量)
diff = mean0 - mean1
little_stat = n0 * n1 / (n0 + n1) * diff @ cov_pooled_inv @ diff
# 自由度 = 变量数 * (组数-1) = 2 * 1 = 2
df_little = 2
p_value = 1 - chi2.cdf(little_stat, df_little)

print(f"Little's MCAR检验统计量: {little_stat:.3f}")
print(f"p值: {p_value:.4f}")
print("结论: p<0.05,拒绝MCAR假设,缺失非完全随机")

# ---------- 3. 辅助判断MAR vs MNAR ----------
# 3a. 比较income缺失组与完整组的age均值(MAR证据)
age_miss = df.loc[df['income'].isna(), 'age']
age_complete = df.loc[df['income'].notna(), 'age']
t_stat_age, p_age = ttest_ind(age_miss, age_complete)
print(f"\n年龄对比: 缺失组均值={age_miss.mean():.2f}, 完整组均值={age_complete.mean():.2f}, p={p_age:.4f}")

# 3b. 比较两组真实income均值(MNAR证据——需用完整数据中的真实值)
true_income_miss = df_full.loc[miss_mask, 'income']
true_income_complete = df_full.loc[~miss_mask, 'income']
t_stat_inc, p_inc = ttest_ind(true_income_miss, true_income_complete)
print(f"真实收入对比: 缺失组均值={true_income_miss.mean():.2f}, 完整组均值={true_income_complete.mean():.2f}, p={p_inc:.4f}")

运行与验证

直接运行上述代码。预期输出特征:

  • Little's检验p值<0.05,拒绝MCAR;
  • 年龄对比p值<0.05,缺失组平均年龄显著更高——支持MAR;
  • 真实收入对比p值<0.05,缺失组真实收入均值更高——支持MNAR成分存在。

判定标准:若Little's检验p>0.05则数据为MCAR;若p<0.05且缺失组在某个完整变量上均值显著不同,则为MAR;若进一步发现缺失值本身的真实值分布与完整组不同,则存在MNAR。本案例构造的数据应同时呈现MAR与MNAR特征,说明实际金融数据中缺失机制常为混合型。

常见问题

  • Little's检验仅使用完整变量(不含存在缺失的变量本身),因此无法直接检测MNAR,需结合分组对比或领域知识。
  • 当缺失模式复杂(多个变量均有缺失)时,需按所有可能缺失模式分组,代码需相应扩展。
  • 协方差矩阵不可逆时(如某组样本量小于变量数),可改用正则化或仅比较单变量均值。