跳到主要内容

KNN 插补

一、直观理解:向“最像你的几个人”借数值​

KNN 插补(K-Nearest Neighbors Imputation)的假设很生活化:若两个人在已观测特征上很相似,那么他们在缺失特征上的取值也往往接近。于是,当样本 ii 的第 jj 个变量缺失时,先在特征空间里找到与 ii 最靠近的 KK 个“邻居”,再用邻居们在变量 jj 上的取值(平均、加权平均或众数)来填空。

好比问卷里有人没填“每周运动时长”,但他的年龄、BMI、作息与另外五个人很像,而这五个人都填了运动时长——与其填全体均值,不如借这五个人的水平来估。

与均值插补相比,KNN 插补:

  • 利用了多变量局部结构,同列也可以填出不同的值;
  • 更能保持一定的局部变异与条件关系;
  • 但依赖距离定义,受量纲、无关变量、高维稀疏和 KK 选择影响很大。

它仍然是一种单次确定性(或加权)填补(除非你再加随机性),一般不会像多重插补那样正确传播“估计缺失值”的不确定性到最终标准误。

二、适用场景与优缺点​

类型说明
适用场景数值型特征为主,变量之间存在可借力的相关 / 冗余;缺失不是极端高比例;样本量和维数适中,距离还有意义;需要比全局均值更“个性化”的基线,但又不想上完整贝叶斯 / 链式方程。
核心优点能使用其他变量信息;实现思路清晰;对局部非线性关系有一定适应性;连续与(经适当处理的)类别特征都可扩展。
局限与缺点距离被量纲支配,必须先缩放;高维下“最近邻不近”;计算量随 nn 增大;邻居若也大量缺失会失效;KK 与距离度量敏感;默认实现常难严格处理训练 / 测试隔离;对 MNAR 无特效。

使用 KNN 插补前通常需要完成以下检查:

  1. 先处理明显的错误值与常量列,避免污染距离;
  2. 对参与距离的变量做标准化或稳健缩放;
  3. 决策:标签 / 响应变量是否允许进入距离(预测任务中通常禁止,防泄漏);
  4. 缺失模式是否导致某些样本几乎没有可用坐标;
  5. 用“人为遮盖已知值再还原”的方式选择 KK 与权重。

三、核心原理:距离、邻居与聚合​

1. 在观测坐标上定义距离​

设样本 a,ba,b 在同时非缺失的变量集合 SabS_{ab} 上可比较。常用平方欧氏距离的平均形式(减少“重叠维度多少不同”的不公):

d(a,b)=1∣Sab∣∑j∈Sab(zaj−zbj)2d(a,b)=\sqrt{ \frac{1}{|S_{ab}|} \sum_{j\in S_{ab}} \big(z_{aj}-z_{bj}\big)^2 }

其中 zz 是缩放后的特征。若 ∣Sab∣|S_{ab}| 太小,这对邻居不可靠,应设最小重叠阈值。

2. 寻找 KK 近邻​

对目标样本 ii,在候选集(通常是训练集中该特征 jj 已观测的样本)里取 d(i,⋅)d(i,\cdot) 最小的 KK 个,记为 NK(i)N_K(i)。

3. 聚合邻居的取值​

连续变量常用距离倒数加权:

x^ij=∑l∈NK(i)wil xlj∑l∈NK(i)wil,wil=1d(i,l)+ε\hat x_{ij} = \frac{\sum_{l\in N_K(i)} w_{il}\, x_{lj}} {\sum_{l\in N_K(i)} w_{il}}, \qquad w_{il}=\frac{1}{d(i,l)+\varepsilon}

也可用简单算术平均(wil=1w_{il}=1)。类别变量则对邻居做多数表决,或按权重求和再取最大类。

4. 与均值插补的关系​

当 K→nK\to n 且权重均匀、又不用其他条件信息时,KNN 插补退化为接近全局均值的行为。KK 越小越局部,也越容易过拟合噪声邻居。

5. 算法流程(训练期)​

  1. 选定用于距离的特征集并缩放;
  2. 对每个缺失位置 (i,j)(i,j),在“jj 已观测”的样本中找邻居;
  3. 聚合得到 x^ij\hat x_{ij};
  4. 可选:多次迭代(用已填补值更新距离)直到稳定——迭代能用上更多信息,也可能传播早期错误。

四、关键决策 / 如何选参数​

1. KK 怎么选​

KK特点
1方差大,照搬单个邻居,不稳定
3–10常用搜索区间
过大趋向全局平均,失去 KNN 意义

选择方法:在训练集上随机遮盖一部分原本已知的条目,用候选 KK 插补后算 MAE / RMSE(连续)或准确率(类别),取验证误差最小者。可与交叉验证结合。

2. 距离度量​

  • 全连续:标准化后的欧氏 / 曼哈顿;
  • 混合类型:Gower 距离更合适,但实现更重;
  • 高维:可先 PCA 到中等维再找邻居,或特征筛选后 KNN。

3. 权重​

  • 均匀权重:简单;
  • 距离倒数 / 高斯核:更强调近邻,通常略好。

4. 缩放​

必须在训练折上估均值方差(或分位数)再变换。用原始量纲时,“收入”会压倒“年龄”。

5. 训练 / 测试隔离​

正确做法:邻居只从训练集里找;测试样本的缺失用训练样本作为邻居来填。若把测试点两两互当邻居,或先全表插补再划分,都会泄漏。

6. 计算预算​

朴素法对每个缺失样本扫全表,O(n2p)O(n^2 p) 量级。nn 很大时需子采样邻居库、KD 树(低维)或近似最近邻。

五、MATLAB 示例​

下面给出不依赖工具箱的清晰实现,便于竞赛复现;若有 Bioinformatics Toolbox,可用 knnimpute,但注意其默认面向基因矩阵(常为基因×样本)且要自行处理好转置与缩放。

rng(5);

% 四列相关特征;人为制造缺失
N = 40;
z = randn(N, 1);
Xtrue = [z, 0.8*z + 0.2*randn(N,1), -0.5*z + 0.3*randn(N,1), randn(N,1)];
X = Xtrue;
% 随机遮盖约 15% 条目
mask = rand(size(X)) < 0.15;
X(mask) = NaN;

% 训练 / 测试划分
tr = 1:30;
te = 31:40;
Xtr = X(tr, :);
Xte = X(te, :);

% ----- 仅用训练集做缩放 -----
mu = mean(Xtr, 1, "omitnan");
sg = std(Xtr, 0, 1, "omitnan");
sg(sg == 0 | isnan(sg)) = 1;

Ztr = (Xtr - mu) ./ sg;
Zte = (Xte - mu) ./ sg;

K = 5;
epsilon = 1e-8;

% 用训练集插补训练集(邻居来自训练)
Ztr_imp = knnImputeWithRef(Ztr, Ztr, K, epsilon);
% 用训练集插补测试集
Zte_imp = knnImputeWithRef(Zte, Ztr, K, epsilon);

% 还原量纲
Xtr_imp = Ztr_imp .* sg + mu;
Xte_imp = Zte_imp .* sg + mu;

disp("测试集插补前:");
disp(Xte);
disp("测试集插补后:");
disp(Xte_imp);

% ----- 遮盖实验选 K -----
Ks = [1, 3, 5, 7, 10];
mae = zeros(size(Ks));
known = ~isnan(Xtr);
for t = 1:numel(Ks)
Xtmp = Xtr;
% 在已知位置再遮盖一部分做验证
cand = find(known);
nHide = floor(0.1 * numel(cand));
hide = cand(randperm(numel(cand), nHide));
Xtmp(hide) = NaN;

Ztmp = (Xtmp - mu) ./ sg;
Zhat = knnImputeWithRef(Ztmp, Ztmp, Ks(t), epsilon);
Xhat = Zhat .* sg + mu;
mae(t) = mean(abs(Xhat(hide) - Xtr(hide)));
end
disp(table(Ks', mae', 'VariableNames', {'K', 'MAE_on_masked'}));

% ===== 本地函数:用参考集 ref 为 query 插补 =====
function Qimp = knnImputeWithRef(Q, ref, K, epsilon)
Qimp = Q;
[nQ, p] = size(Q);
nR = size(ref, 1);
for i = 1:nQ
missCols = find(isnan(Q(i, :)));
if isempty(missCols)
continue;
end
for jj = 1:numel(missCols)
j = missCols(jj);
% 候选邻居:参考集中 j 列已知
cand = find(~isnan(ref(:, j)));
if isempty(cand)
% 回退:该列参考均值
Qimp(i, j) = mean(ref(:, j), "omitnan");
continue;
end
% 计算距离(仅在双方均已知的特征上)
d = inf(numel(cand), 1);
for t = 1:numel(cand)
r = cand(t);
both = ~isnan(Q(i, :)) & ~isnan(ref(r, :));
if nnz(both) == 0
continue;
end
diff = Q(i, both) - ref(r, both);
d(t) = sqrt(mean(diff.^2));
end
% 取 K 个最近且距离有限
valid = isfinite(d);
cand = cand(valid);
d = d(valid);
if isempty(cand)
Qimp(i, j) = mean(ref(:, j), "omitnan");
continue;
end
Kuse = min(K, numel(cand));
[ds, ord] = sort(d, "ascend");
nb = cand(ord(1:Kuse));
ds = ds(1:Kuse);
w = 1 ./ (ds + epsilon);
vals = ref(nb, j);
Qimp(i, j) = sum(w .* vals) / sum(w);
end
end
end

六、如何解释结果​

KNN 插补给出的 x^ij\hat x_{ij} 应解释为:

在已选特征度量下,与样本 ii 最相似且该变量可用的 KK 个训练样本的局部聚合值。

报告建议写明:

  1. 参与距离的变量、是否含标签;
  2. 缩放方法与参数来源(训练折);
  3. 距离定义、最小重叠维度、权重形式;
  4. KK 的选择依据(遮盖实验 / CV 曲线);
  5. 插补前后分布、相关结构的变化;
  6. 对测试集邻居库的界定;
  7. 下游模型指标相对均值插补、直接删除的提升;
  8. 失败回退策略(无邻居时用列均值等)。

不要把插补值写成“真实测量被恢复”,尤其在邻居相似度不高时,它只是条件期望的粗糙近似。

七、常见误区​

1. 不标准化就算欧氏距离​

收入、年龄、评分混在一起时,邻居关系基本被大尺度变量绑架。

2. 把预测目标纳入距离特征​

分类 / 回归竞赛中这会造成严重泄漏,验证分数虚高。

3. K=1K=1 且不做稳定性检查​

单个邻居若异常,缺失会被准确复制成异常。

4. 全表插补完成后再切分训练测试​

测试信息通过邻居关系渗入训练填补,评估偏乐观。

5. 高维稀疏仍强行 KNN​

“最近邻”可能并不相似。应先降维 / 选特征,或换模型插补。

八、小结​

KNN 插补借助“相似样本取值相近”的假设,用 KK 个近邻的聚合值填补缺失,是介于全局均值与重型模型插补之间的实用方法。

它的核心思想可以概括为:

空格不必填成全班平均数;先找到几个最像的人,再向他们借答案。

用好它的关键是:合理缩放、隔离训练邻居库、认真选 KK,并在高维与高缺失时保持警惕。它能改善局部结构的利用,但依旧是单次填补,不能替代对缺失机制的讨论,也不能自动给出正确的不确定度。