缺失值处理:删除、填充、插值
一句话概述
缺失值(Missing Values)是真实数据中不可避免的问题——传感器故障、用户未填写、数据合并不一致等都可能导致缺失。处理方法分为三大类:删除法(直接丢弃含缺失值的样本或特征)、填充法(用统计量或模型预测值填充缺失值)和插值法(基于时间序列或空间关系推断缺失值)。缺失值处理不是简单的「填个数」,而是需要考虑缺失机制(MCAR/MAR/MNAR)和后续模型的特点。
💡 核心要点:① 缺失值的处理策略取决于缺失机制——随机缺失、有规律缺失还是系统性缺失 ② 简单删除适用于缺失比例极低的情况,否则会损失大量信息 ③ 均值/中位数填充最简单但会低估方差,KNN 和 MICE 等更高级方法能保持数据分布 ④ 工业界常用「缺失值指示器」——不仅填充缺失值,还额外添加一个「是否缺失」的二值特征
教学与演示
一、缺失机制:MCAR、MAR、MNAR
是什么:理解缺失值的产生机制是选择处理方法的前提。Rubin(1976)将缺失机制分为三类:MCAR(Missing Completely At Random,完全随机缺失)——缺失与任何变量无关,如随机传感器故障;MAR(Missing At Random,随机缺失)——缺失与观测到的变量有关,但与缺失值本身无关,如女性更不愿填写体重;MNAR(Missing Not At Random,非随机缺失)——缺失与缺失值本身有关,如高收入人群更不愿填写收入。
大白话 MCAR 就像「抛硬币决定要不要填」——完全随机;MAR 就像「女生更不愿意填体重」——跟其他已知信息有关;MNAR 就像「越有钱的人越不愿意填收入」——缺失本身就有信息量。第三种最麻烦,因为缺失本身就暗示了某些信息。
为什么:缺失机制决定了处理方法的有效性。对于 MCAR,简单删除或均值填充都是无偏的。对于 MAR,可以使用其他变量来预测缺失值(如用身高、性别预测体重)。对于 MNAR,任何简单方法都会引入偏差,需要使用更复杂的模型(如 Heckman 选择模型)或敏感性分析。
怎么做:
import numpy as np
# ========== 演示三种缺失机制 ==========
np.random.seed(42)
def missing_mechanisms_demo():
"""演示三种缺失机制的区别"""
n_samples = 200
# 生成完整数据
age = np.random.randint(18, 70, n_samples) # 年龄
gender = np.random.choice([0, 1], n_samples) # 性别(0=女, 1=男)
income = 5000 + 200 * age + np.random.randn(n_samples) * 2000 # 收入
weight = 50 + 0.5 * (age - 18) + 15 * gender + np.random.randn(n_samples) * 5 # 体重
print("=== 三种缺失机制 ===")
print(f"原始数据: {n_samples} 个样本,含 age, gender, income, weight")
print()
# 1. MCAR:完全随机缺失
mcar_mask = np.random.random(n_samples) < 0.2 # 20% 随机缺失
income_mcar = income.copy()
income_mcar[mcar_mask] = np.nan
print(f"MCAR (完全随机缺失): {mcar_mask.sum()} 个缺失 ({mcar_mask.mean():.0%})")
print(f" 缺失的 income 均值: {income[~mcar_mask].mean():.0f}")
print(f" 非缺失的 income 均值: {income[mcar_mask].mean():.0f}")
print(f" 两者接近 → 缺失与数据无关")
print()
# 2. MAR:缺失与性别有关
mar_prob = np.where(gender == 0, 0.4, 0.05) # 女性40%缺失,男性5%缺失
mar_mask = np.random.random(n_samples) < mar_prob
weight_mar = weight.copy()
weight_mar[mar_mask] = np.nan
print(f"MAR (随机缺失,与性别有关): {mar_mask.sum()} 个缺失 ({mar_mask.mean():.0%})")
print(f" 女性缺失比例: {mar_mask[gender==0].mean():.0%}")
print(f" 男性缺失比例: {mar_mask[gender==1].mean():.0%}")
print(f" 缺失可以通过性别预测 → MAR")
print()
# 3. MNAR:缺失与收入本身有关(高收入更不愿填写)
mnar_prob = 1 / (1 + np.exp(-(income - 10000) / 3000)) # 收入越高,缺失概率越高
mnar_mask = np.random.random(n_samples) < mnar_prob
income_mnar = income.copy()
income_mnar[mnar_mask] = np.nan
print(f"MNAR (非随机缺失,与收入本身有关): {mnar_mask.sum()} 个缺失 ({mnar_mask.mean():.0%})")
print(f" 缺失样本的真实收入均值: {income[mnar_mask].mean():.0f}")
print(f" 非缺失样本的真实收入均值: {income[~mnar_mask].mean():.0f}")
print(f" 两者显著不同 → 缺失与缺失值本身有关 → 最棘手!")
missing_mechanisms_demo()
大白话 判断缺失机制的一个简单方法:画一个「缺失 vs 非缺失」的箱线图。如果两组的其他特征分布相似 → MCAR;如果某些特征能预测缺失 → MAR;如果缺失与非缺失的「其他特征」分布也不同,且你能想到「缺失值本身」影响了缺失 → MNAR。
二、删除法
是什么:删除法是最简单的缺失值处理方式,分为两种:① 列表删除(Listwise Deletion)——删除任何包含缺失值的整行样本,② 成对删除(Pairwise Deletion)——仅在计算涉及缺失变量时删除该样本,不同分析可使用不同样本子集。此外,还可以删除缺失比例过高的特征(如缺失超过 50% 的列)。
大白话 删除法就像「把有缺页的书扔掉」——简单粗暴,但如果你只有一本缺了 3 页的书,扔掉整本书显然是浪费。如果一本书缺了 80% 的页,那确实该扔。
为什么:列表删除在 MCAR 条件下是无偏的,但会损失样本量(如果 10 个特征各有 5% 缺失,可能有 40% 的样本至少有一个缺失值)。成对删除保留了更多样本,但不同分析使用的样本集不同,导致结果不一致。特征删除适用于缺失比例极高且特征不重要的场景。
怎么做:
import numpy as np
# ========== 删除法演示 ==========
np.random.seed(42)
def deletion_methods_demo():
"""演示删除法的效果和代价"""
n_samples = 100
n_features = 10
# 生成数据,每个特征有独立 5% 的缺失概率
X = np.random.randn(n_samples, n_features)
missing_mask = np.random.random((n_samples, n_features)) < 0.05 # 5% 缺失
print("=== 删除法演示 ===")
print(f"原始数据: {n_samples} 样本 × {n_features} 特征")
print(f"每个特征独立 5% 缺失概率")
print()
# 列表删除(删除任何有缺失的行)
any_missing = missing_mask.any(axis=1) # 每行是否有缺失
complete_samples = n_samples - any_missing.sum()
print(f"列表删除后剩余样本: {complete_samples} ({complete_samples/n_samples:.0%})")
print(f"虽然每个特征只有 5% 缺失,但 {any_missing.sum()} 个样本 ({any_missing.sum()/n_samples:.0%}) 至少有一个缺失值")
print(f"信息损失: {(n_samples - complete_samples) / n_samples * 100:.1f}%")
print()
# 特征删除(删除缺失超过阈值的列)
feature_missing_rate = missing_mask.mean(axis=0)
print("各特征缺失比例:")
for i, rate in enumerate(feature_missing_rate):
print(f" 特征{i}: {rate:.0%}")
# 假设特征3缺失率超过30%就被删除
threshold = 0.30
# 人为提高特征3的缺失率
feature_missing_rate[3] = 0.35
keep_features = feature_missing_rate < threshold
print(f"\n删除缺失率 > {threshold:.0%} 的特征: 特征3 (缺失率 35%)")
print(f"保留特征数: {keep_features.sum()} (从 {n_features} 个)")
deletion_methods_demo()大白话 删除法虽然简单,但「删除」的背后是「丢弃信息」。每次删除前问自己:这些信息值得丢弃吗?有没有更好的方法来保留它们?
三、填充法
是什么:填充法(Imputation)是用估计值替代缺失值。从简单到复杂包括:① 均值/中位数/众数填充——用该特征的集中趋势填充,② 前向/后向填充——用相邻样本的值填充(适用于时间序列),③ 回归填充——用其他特征建立回归模型预测缺失值,④ KNN 填充——用 K 个最相似样本的均值填充,⑤ MICE(多重插补)——多次插补并合并结果,反映不确定性。
大白话 填充法就像「猜缺失的字」——最笨的方法是猜最常见的字(均值填充),聪明一点的方法是看看上下文(KNN 填充),最聪明的方法是把所有可能的情况都猜一遍然后取平均(多重插补)。
为什么:均值填充虽然简单,但会低估特征的方差(因为所有缺失值被填成同一个值),导致标准误估计偏小。KNN 填充利用样本间的相似性,能更好地保持数据分布。MICE(Multiple Imputation by Chained Equations)通过链式方程迭代填充,能反映填充的不确定性,是统计上最严谨的方法。
怎么做:
import numpy as np
# ========== 填充法演示 ==========
np.random.seed(42)
def imputation_methods_demo():
"""演示各种填充方法"""
n_samples = 100
# 生成完整数据
x1 = np.random.randn(n_samples) # 特征1
x2 = 0.7 * x1 + np.random.randn(n_samples) * 0.5 # 特征2与特征1相关
x3 = np.random.randn(n_samples) # 特征3(与x1无关)
# 制造缺失:x2 的 20% 随机缺失
missing_idx = np.random.choice(n_samples, int(n_samples * 0.2), replace=False)
x2_missing = x2.copy()
x2_missing[missing_idx] = np.nan
print("=== 填充法演示 ===")
print(f"特征 x2 的缺失数: {len(missing_idx)}")
print(f"x2 的真实均值: {x2.mean():.3f}, 真实标准差: {x2.std():.3f}")
print()
# 方法1:均值填充
x2_mean_fill = x2_missing.copy()
mean_val = np.nanmean(x2_missing) # 忽略 NaN 计算均值
x2_mean_fill[missing_idx] = mean_val
print(f"方法1 - 均值填充: 均值={x2_mean_fill.mean():.3f}, 标准差={x2_mean_fill.std():.3f}")
print(f" 问题:标准差被低估(所有缺失值填成同一个值)")
# 方法2:中位数填充
x2_median_fill = x2_missing.copy()
median_val = np.nanmedian(x2_missing)
x2_median_fill[missing_idx] = median_val
print(f"方法2 - 中位数填充: 均值={x2_median_fill.mean():.3f}, 标准差={x2_median_fill.std():.3f}")
print(f" 优点:对异常值更鲁棒;缺点:同样低估方差")
# 方法3:KNN 填充(简化版:用最近邻的均值)
x2_knn_fill = x2_missing.copy()
for idx in missing_idx:
# 找与缺失样本在 x1 上最近的 3 个非缺失样本
distances = np.abs(x1 - x1[idx])
distances[missing_idx] = np.inf # 排除其他缺失样本
knn_idx = np.argsort(distances)[:3] # 最近的3个
x2_knn_fill[idx] = x2[knn_idx].mean() # 用它们的 x2 均值填充
print(f"方法3 - KNN填充 (K=3): 均值={x2_knn_fill.mean():.3f}, 标准差={x2_knn_fill.std():.3f}")
print(f" 优点:利用特征间相关性,更好地保持分布")
# 方法4:缺失值指示器
missing_indicator = np.isnan(x2_missing).astype(int)
print(f"\n方法4 - 缺失值指示器:")
print(f" 额外添加一个特征 'x2_is_missing',标记哪些样本的 x2 是缺失的")
print(f" 缺失标记数量: {missing_indicator.sum()}")
print(f" 这允许模型学习「缺失本身是否有信息」——对 MNAR 特别有效!")
imputation_methods_demo()
大白话 填充法没有「最好」的方法,只有「最合适」的方法。均值填充最快但最粗糙,KNN 和 MICE 更准确但更慢。但有一个几乎所有场景都推荐的做法:添加「缺失值指示器」——告诉模型「这个值是猜的」,让模型自己决定是否信任这个猜测。
四、插值法
是什么:插值法(Interpolation)主要用于时间序列和空间数据中的缺失值处理。它利用数据的时间或空间连续性来推断缺失值。常用方法包括:线性插值(用前后两点的连线估计中间值)、样条插值(用光滑曲线拟合)、以及基于模型的插值(如 ARIMA 模型预测缺失值)。
大白话 插值法就像「看视频补帧」——如果你知道第 1 秒和第 3 秒的画面,可以猜第 2 秒大概是什么样子。时间序列数据有天然的连续性,插值比简单的均值填充合理得多。
为什么:时间序列数据具有自相关性(相邻时间点的值通常接近),这为插值提供了理论基础。线性插值假设数据在短时间间隔内线性变化,样条插值假设数据变化光滑。插值法在缺失间隔较短时效果好,缺失间隔较长时误差大。
怎么做:
import numpy as np
# ========== 插值法演示 ==========
np.random.seed(42)
def interpolation_demo():
"""演示时间序列中的插值方法"""
# 生成时间序列数据:sin 波 + 噪声
n_points = 50
t = np.linspace(0, 4 * np.pi, n_points)
y_true = np.sin(t) + np.random.randn(n_points) * 0.2
# 制造缺失(随机删除 20% 的点)
missing_idx = np.random.choice(n_points, int(n_points * 0.2), replace=False)
missing_idx.sort()
y_missing = y_true.copy()
y_missing[missing_idx] = np.nan
print("=== 时间序列插值演示 ===")
print(f"时间序列长度: {n_points}")
print(f"缺失点数量: {len(missing_idx)}")
print(f"缺失点索引: {missing_idx}")
print()
# 线性插值
y_linear = y_missing.copy()
for idx in missing_idx:
# 找前一个和后一个非缺失点
prev_idx = idx - 1
while prev_idx >= 0 and prev_idx in missing_idx:
prev_idx -= 1
next_idx = idx + 1
while next_idx < n_points and next_idx in missing_idx:
next_idx += 1
if prev_idx >= 0 and next_idx < n_points:
# 线性插值:y = y_prev + (x - x_prev)/(x_next - x_prev) * (y_next - y_prev)
alpha = (idx - prev_idx) / (next_idx - prev_idx)
y_linear[idx] = y_true[prev_idx] + alpha * (y_true[next_idx] - y_true[prev_idx])
elif prev_idx >= 0:
y_linear[idx] = y_true[prev_idx] # 前向填充
elif next_idx < n_points:
y_linear[idx] = y_true[next_idx] # 后向填充
# 计算插值误差
interpolation_errors = np.abs(y_linear[missing_idx] - y_true[missing_idx])
print(f"线性插值误差(缺失点):")
print(f" 平均绝对误差 MAE: {interpolation_errors.mean():.3f}")
print(f" 最大误差: {interpolation_errors.max():.3f}")
# 对比:如果直接用均值填充
mean_fill = y_missing.copy()
mean_fill[missing_idx] = np.nanmean(y_missing)
mean_errors = np.abs(mean_fill[missing_idx] - y_true[missing_idx])
print(f"\n均值填充误差(对比):")
print(f" 平均绝对误差 MAE: {mean_errors.mean():.3f}")
print(f" 最大误差: {mean_errors.max():.3f}")
print(f" 插值比均值填充好: {(1 - interpolation_errors.mean()/mean_errors.mean()) * 100:.0f}%")
interpolation_demo()
大白话 对于时间序列数据,插值法几乎是「降维打击」——因为你知道数据在时间上有连续性,所以可以根据前后值来推断缺失值。这比盲目填均值要准确得多。
什么用:在 AI 工业中,缺失值处理是数据预处理流水线的第一步。物联网传感器数据常用插值法处理信号丢失,金融数据常用前向填充处理停牌日的价格,医疗数据常用 MICE 处理多项检查指标的缺失。XGBoost 和 LightGBM 天然支持缺失值(在分裂时自动学习缺失值的最优处理方向),这是它们的一大优势。
哪些坑:缺失值处理不能简单套用——需要先分析缺失机制。均值填充在 MNAR 下会产生严重偏差。插值法在缺失连续段过长时误差会累积。MICE 虽然理论上严谨,但计算成本高,且需要满足 MAR 假设。填充后数据的方差和协方差结构会发生变化,可能影响后续统计推断。
概念关系图谱
| 方法 | 适用场景 | 优点 | 缺点 | 缺失机制要求 |
|---|---|---|---|---|
| 列表删除 | 缺失极少 | 简单,无偏 | 损失样本 | MCAR |
| 均值填充 | 快速处理 | 简单 | 低估方差 | MCAR |
| KNN 填充 | 特征间有相关性 | 保持分布 | 计算慢 | MAR |
| 线性插值 | 时间序列 | 利用时间结构 | 长间隔误差大 | MAR |
| MICE | 统计推断 | 反映不确定性 | 计算成本高 | MAR |
| 缺失指示器 | 任意场景 | 保留缺失信息 | 增加特征 | 通用 |
重点答疑
Q1: 缺失值处理中,删除和填充应该如何选择?
遵循「先分析,后决策」原则:① 如果缺失比例 < 5% 且缺失机制是 MCAR,列表删除最简单有效;② 如果缺失比例 5%-30%,使用填充法(KNN 或 MICE);③ 如果缺失比例 > 30%,考虑删除该特征,或将该特征作为缺失值指示器使用;④ 始终添加缺失值指示器,让模型知道「这个值是猜的」;⑤ 如果使用树模型(如 XGBoost),可以让模型自动处理缺失值。
Q2: 为什么均值填充会低估方差?
因为所有缺失值被填成了同一个值(均值),填充后的数据在缺失位置没有变异。如果真实缺失值是分散的,填充后这些位置变为完全相同的值,整体方差自然被低估。低估方差会导致置信区间过窄、假设检验过于激进(假阳性增加)。
Q3: XGBoost 和 LightGBM 如何处理缺失值?
它们使用「稀疏感知」(Sparsity-aware)算法:在分裂时,对每个特征,将缺失值样本分别尝试分到左子节点和右子节点,选择增益更大的方向。这个方向在训练过程中自动学习,不需要手动填充。这是树模型处理缺失值的一大优势——不需要预处理,模型自己会找到最优处理方式。
章节单词汇总
| 英文 | 音标 | 术语/释义 |
|---|---|---|
| MCAR | /ˌem.siː.eɪˈɑːr/ | Missing Completely At Random;完全随机缺失 |
| MAR | /ˌem.eɪˈɑːr/ | Missing At Random;随机缺失 |
| MNAR | /ˌem.en.eɪˈɑːr/ | Missing Not At Random;非随机缺失 |
| Imputation | /ˌɪmpjuˈteɪʃən/ | 插补;用估计值替代缺失值 |
| Listwise Deletion | /ˈlɪst.waɪz dɪˈliːʃən/ | 列表删除;删除含缺失值的整行 |
| MICE | /maɪs/ | Multiple Imputation by Chained Equations;链式方程多重插补 |
| KNN Imputation | /ˌkeɪ.enˈen ˌɪmpjuˈteɪʃən/ | K 近邻插补;用相似样本填充缺失 |
| Interpolation | /ɪnˌtɜːrpəˈleɪʃən/ | 插值;基于连续性推断缺失值 |
面试练习
Q1 [单选] 以下哪种缺失机制下,简单删除法是有效的?
- A. MCAR(完全随机缺失)
- B. MAR(随机缺失)
- C. MNAR(非随机缺失)
- D. 任何情况下都有效
解答:在 MCAR 下,缺失与任何变量无关,删除缺失样本后剩余样本仍是原始总体的随机子集,估计无偏。在 MAR 和 MNAR 下,删除会引入选择偏差。
Q2 [单选] 均值填充的一个主要问题是什么?
- A. 计算太慢
- B. 低估了特征的方差
- C. 引入了新的缺失值
- D. 只能用于分类特征
解答:均值填充把所有缺失值填成同一个值,导致缺失位置的变异性消失,整体方差被低估。这会使得后续的统计推断(如置信区间)过于乐观。
Q3 [多选] 以下哪些是缺失值处理的常用方法?
- A. 均值/中位数填充
- B. KNN 填充
- C. 多重插补(MICE)
- D. 缺失值指示器
解答:这些都是缺失值处理的常用方法。均值填充最简单,KNN 和 MICE 更准确,缺失值指示器是推荐的最佳实践(与其他方法配合使用)。
Q4 [单选] 缺失值指示器(Missing Indicator)的作用是什么?
- A. 删除所有缺失值
- B. 添加一个二值特征标记哪些样本的该特征是缺失的
- C. 用缺失值填充其他特征
- D. 计算缺失值的比例
解答:缺失值指示器是一个额外的二值特征,标记该样本的某特征是否缺失。这允许模型学习「缺失本身是否有信息」——对 MNAR 场景特别有效。
Q5 [单选] 对于时间序列数据,最适合的缺失值处理方法是什么?
- A. 列表删除
- B. 均值填充
- C. 插值法(线性插值、样条插值)
- D. One-Hot 编码
解答:时间序列数据具有时间连续性,插值法利用前后时间点的值来推断缺失值,充分利用了数据结构,比均值填充更准确。
Q6 [多选] 以下关于缺失值处理的说法,哪些是正确的?
- A. 缺失值处理前应该先分析缺失机制
- B. XGBoost 可以自动处理缺失值,不需要手动填充
- C. 均值填充在 MNAR 下会产生偏差
- D. 缺失值处理总是能提高模型性能
解答:分析缺失机制是选择处理方法的前提,XGBoost 有稀疏感知算法自动处理缺失值,MNAR 下均值填充有偏。但缺失值处理不保证提高性能——如果处理不当反而可能引入噪声。
Q7 [单选] MICE(多重插补)的核心思想是什么?
- A. 用均值填充所有缺失值
- B. 多次插补并合并结果,反映填充的不确定性
- C. 删除所有含缺失值的样本
- D. 用 KNN 找最近邻填充
解答:MICE 通过链式方程进行多次插补(如 5-10 次),每次生成略有不同的填充值,然后将多次分析结果合并(Rubin's rules),从而反映填充的不确定性。
Q8 [单选] 当缺失机制是 MNAR 时,为什么均值填充会产生偏差?
- A. 因为均值填充太慢
- B. 因为缺失值本身就与缺失值的大小有关,用非缺失样本的均值填充会系统性地偏离真实值
- C. 因为均值填充会删除数据
- D. 因为均值填充只能用整数
解答:MNAR 下缺失值与缺失值本身有关(如高收入者更不愿填写收入)。非缺失样本的均值系统性低于真实缺失值,用这个均值填充会产生向下的偏差。
Q9 [多选] 以下哪些模型可以自动处理缺失值?
- A. XGBoost
- B. LightGBM
- C. 线性回归
- D. 逻辑回归
解答:XGBoost 和 LightGBM 使用稀疏感知算法,在分裂时自动学习缺失值的最优处理方向。线性模型不支持缺失值输入,必须先填充。
Q10 [单选] 当某个特征的缺失比例超过 50% 时,最合理的处理方式是什么?
- A. 用均值填充
- B. 考虑删除该特征,或将其转为缺失指示器
- C. 用 KNN 填充
- D. 不做任何处理
解答:缺失超过 50% 的特征,填充的可信度很低。更好的做法是:① 如果该特征不重要,直接删除;② 如果该特征有潜在价值,将其转为缺失指示器(标记是否缺失),可能比填充更有效。