原文:Imputing missing values before building an estimator。作者:The scikit-learn developers。依据 scikit-learn 1.9.1 官方实例译写;原示例代码标注 BSD-3-Clause。本文包含明确标注的编者说明,代码仅做静态审查,未运行。
缺失值并不一定意味着必须丢弃整条样本。最基础的 SimpleImputer 可以用均值、中位数或最常见值来填补空缺;但选择哪种方法,应放回最终预测任务中比较。原例固定使用随机森林回归器,在完整数据与人为制造缺失的数据之间建立对照,再分别尝试零值、均值、近邻和迭代插补。
四种插补都启用缺失指示变量,让模型除了看到补上的数值,也能知道该位置原来是否缺失。两个数据集分别是糖尿病数据和加利福尼亚房价数据:前者根据患者的十项特征预测疾病进展,后者预测加州各区域的房价中位数。两者原本没有缺失值,因此可以用完整数据作为参照。

准备数据:75% 的行各缺一个值
糖尿病数据随 scikit-learn 分发,共 442 条记录、10 个特征。加州房价数据需要下载,共 20,640 条记录、8 个特征。为缩短计算时间,原例对两个数据集都只取前 300 条;需要时可以使用全部数据。
import numpy as np
from sklearn.datasets import fetch_california_housing, load_diabetes
X_diabetes, y_diabetes = load_diabetes(return_X_y=True)
X_california, y_california = fetch_california_housing(return_X_y=True)
X_diabetes = X_diabetes[:300]
y_diabetes = y_diabetes[:300]
X_california = X_california[:300]
y_california = y_california[:300]
def add_missing_values(X_full, y_full, rng):
n_samples, n_features = X_full.shape
# 在 75% 的行中加入缺失值。
missing_rate = 0.75
n_missing_samples = int(n_samples * missing_rate)
missing_samples = np.zeros(n_samples, dtype=bool)
missing_samples[:n_missing_samples] = True
rng.shuffle(missing_samples)
missing_features = rng.randint(0, n_features, n_missing_samples)
X_missing = X_full.copy()
X_missing[missing_samples, missing_features] = np.nan
y_missing = y_full.copy()
return X_missing, y_missing
rng = np.random.RandomState(42)
X_miss_diabetes, y_miss_diabetes = add_missing_values(
X_diabetes, y_diabetes, rng
)
X_miss_california, y_miss_california = add_missing_values(
X_california, y_california, rng
)
这里先建立一个行掩码,随机打乱后选出 225 行,再为每一行随机挑一个特征置为 np.nan。目标值 y 只复制,不制造缺失。编者说明:75% 指受影响的行数,不是整个矩阵有 75% 的单元格缺失;在这两组各 300 行的数据中,缺失单元格分别占全部特征单元格的 7.5% 和 9.375%。这种随机生成方式也不能代表现实中“值越大越容易缺失”等非随机缺失机制。
把插补放进交叉验证
接下来定义统一的评分函数。完整数据直接交给 RandomForestRegressor;提供了插补器时,则把插补器和回归器串成 Pipeline。使用四折交叉验证,评分指标是负均方误差,返回四折分数的均值和标准差。
from sklearn.ensemble import RandomForestRegressor
# IterativeImputer 是实验功能,需要先显式启用。
from sklearn.experimental import enable_iterative_imputer # noqa: F401
from sklearn.impute import IterativeImputer, KNNImputer, SimpleImputer
from sklearn.model_selection import cross_val_score
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import RobustScaler
N_SPLITS = 4
def get_score(X, y, imputer=None):
regressor = RandomForestRegressor(random_state=0)
if imputer is not None:
estimator = make_pipeline(imputer, regressor)
else:
estimator = regressor
scores = cross_val_score(
estimator, X, y, scoring="neg_mean_squared_error", cv=N_SPLITS
)
return scores.mean(), scores.std()
x_labels = []
mses_diabetes = np.zeros(5)
stds_diabetes = np.zeros(5)
mses_california = np.zeros(5)
stds_california = np.zeros(5)
这里保存五种配置的结果:完整数据,以及四种插补。均方误差越小越好,而 scikit-learn 的评分接口约定分数越大越好,所以代码先拿到负 MSE,最后再统一取负号还原为 MSE。标准差无需跟着取负。
编者说明:Pipeline 的位置很关键。每一折都在训练子集上拟合插补器,再把这一折学到的变换用于验证子集;不能先对全部数据调用 fit_transform,再做交叉验证。后面的缩放器也放在这一结构内部,因此同样不会提前用到验证折的信息。
先评估完整数据
先计算没有人为缺失时的分数。这是同一回归任务下的参照,不需要插补器,也没有添加缺失指示变量。
mses_diabetes[0], stds_diabetes[0] = get_score(X_diabetes, y_diabetes)
mses_california[0], stds_california[0] = get_score(X_california, y_california)
x_labels.append("Full Data")
第一种方案:用零填补
常数插补直接把缺失位置填为 0。它很简单,但 0 的实际含义取决于特征。add_indicator=True 使模型有机会区分“本来就是零”和“因为缺失而填零”。
imputer = SimpleImputer(strategy="constant", fill_value=0, add_indicator=True)
mses_diabetes[1], stds_diabetes[1] = get_score(
X_miss_diabetes, y_miss_diabetes, imputer
)
mses_california[1], stds_california[1] = get_score(
X_miss_california, y_miss_california, imputer
)
x_labels.append("Zero Imputation")
第二种方案:用每列均值填补
均值插补用该特征在拟合数据中的均值替换缺失值。这里仍添加缺失指示变量,其余评分设置保持一致。
imputer = SimpleImputer(strategy="mean", add_indicator=True)
mses_diabetes[2], stds_diabetes[2] = get_score(
X_miss_diabetes, y_miss_diabetes, imputer
)
mses_california[2], stds_california[2] = get_score(
X_miss_california, y_miss_california, imputer
)
x_labels.append("Mean Imputation")
第三种方案:用近邻填补
KNNImputer 根据指定数量的最近邻,以加权或不加权均值估计缺失值。特征量纲相差很大时,距离的含义可能被大数值特征主导。原例因此在加州房价数据的近邻插补之前加上 RobustScaler,糖尿病数据则直接插补。
imputer = KNNImputer(add_indicator=True)
mses_diabetes[3], stds_diabetes[3] = get_score(
X_miss_diabetes, y_miss_diabetes, imputer
)
mses_california[3], stds_california[3] = get_score(
X_miss_california, y_miss_california, make_pipeline(RobustScaler(), imputer)
)
x_labels.append("KNN Imputation")
传给 get_score 的房价插补器本身也是一条 Pipeline;评分函数再把它与随机森林拼在一起。这个嵌套并不会把缩放移出交叉验证。缩放可能改善近邻选择,但原文并未保证它在每个数据集上都带来提升。
第四种方案:迭代插补
IterativeImputer 轮流把含缺失值的某个特征当作目标,用其他特征来预测它,再循环处理各个特征。原例保留默认回归器 BayesianRidge。不同特征的尺度差异可能影响预测器,因此房价数据同样先做稳健缩放。
imputer = IterativeImputer(add_indicator=True)
mses_diabetes[4], stds_diabetes[4] = get_score(
X_miss_diabetes, y_miss_diabetes, imputer
)
mses_california[4], stds_california[4] = get_score(
X_miss_california, y_miss_california, make_pipeline(RobustScaler(), imputer)
)
x_labels.append("Iterative Imputation")
mses_diabetes = mses_diabetes * -1
mses_california = mses_california * -1
启用实验模块的导入必须出现在导入 IterativeImputer 之前。示例的随机森林和缺失生成器分别固定了随机状态,但这不应被扩展解释为任何软件版本、任何运行环境都会得到逐位相同的结果。
画出五组结果,正确理解误差条
最后将两个数据集各画成一组横向柱形图。柱长是四折 MSE 的均值,xerr 对应四折分数的标准差;两张子图共享配置顺序,但拥有各自的横轴尺度。
import matplotlib.pyplot as plt
n_bars = len(mses_diabetes)
xval = np.arange(n_bars)
colors = ["r", "g", "b", "orange", "black"]
plt.figure(figsize=(12, 6))
ax1 = plt.subplot(121)
for j in xval:
ax1.barh(
j, mses_diabetes[j], xerr=stds_diabetes[j],
color=colors[j], alpha=0.6, align="center",
)
ax1.set_title("Imputation Techniques with Diabetes Data")
ax1.set_xlim(
left=np.min(mses_diabetes) * 0.9,
right=np.max(mses_diabetes) * 1.1,
)
ax1.set_yticks(xval)
ax1.set_xlabel("MSE")
ax1.invert_yaxis()
ax1.set_yticklabels(x_labels)
ax2 = plt.subplot(122)
for j in xval:
ax2.barh(
j, mses_california[j], xerr=stds_california[j],
color=colors[j], alpha=0.6, align="center",
)
ax2.set_title("Imputation Techniques with California Data")
ax2.set_yticks(xval)
ax2.set_xlabel("MSE")
ax2.invert_yaxis()
ax2.set_yticklabels([""] * n_bars)
plt.show()
原页面展示了这段代码生成的两张结果图,并记载该页面构建时脚本总耗时为 7.954 秒。本文没有运行脚本,也没有据此绘制或声称新的分数;这个耗时只属于原文的特定环境。折间标准差描述四次评分的离散程度,不能直接叫作置信区间。
原文还建议尝试其他策略,例如中位数。当数据具有较长尾部、少数大值容易主导结果时,中位数往往是更稳健的统计量。具体是否改善最终预测,仍应放回相同的数据划分、评价指标和预测器下检验。
把这个示例用于自己的任务时
编者说明:只取前 300 行是为了展示方法,不能保证样本具有代表性;随机挖掉特征也不能证明方案适用于所有真实缺失机制。糖尿病和房价在这里都是回归方法演示,文中比较不构成临床判断或房产估值承诺。add_indicator=True 的实际指示列由拟合数据中的缺失情况决定,不应未经检查就假定每个输入特征在所有折中都额外产生一列。
代码会下载房价数据,并在本地训练多个模型。静态检查未发现硬编码秘密、认证绕过或破坏性文件操作;实际下载缓存、资源占用、依赖版本兼容性和预测分数均未验证。本稿依据原例保留方法顺序、参数和计算过程,仅翻译注释、调整部分长行,并增加上述边界说明。
来源及代码归属:The scikit-learn developers,BSD-3-Clause。示意图为本文编者原创。
代码版权与许可声明
下列声明按对应项目原文保留,适用于文中相应代码;本稿的中文说明和编者校注不表示原作者对这些改动的认可。
scikit-learn 示例代码:BSD 3-Clause
许可来源:上游项目许可原文。
BSD 3-Clause License Copyright (c) 2007-2026 The scikit-learn developers. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.












暂无评论内容