缺失值处理:删除、填充、插值

一句话概述

缺失值(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()
\text{三种缺失机制的形式化定义}\(\begin{aligned} \text{MCAR: } & P(R|X_{\text{obs}}, X_{\text{miss}}) = P(R) \\ \text{MAR: } & P(R|X_{\text{obs}}, X_{\text{miss}}) = P(R|X_{\text{obs}}) \\ \text{MNAR: } & P(R|X_{\text{obs}}, X_{\text{miss}}) \neq P(R|X_{\text{obs}}) \end{aligned}\)
大白话 判断缺失机制的一个简单方法:画一个「缺失 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% 的特征,填充的可信度很低。更好的做法是:① 如果该特征不重要,直接删除;② 如果该特征有潜在价值,将其转为缺失指示器(标记是否缺失),可能比填充更有效。