1. 引言
在机器学习项目中,数据质量往往直接决定模型的上限。真实采集到的数据经常因为传感器故障、人工录入遗漏等原因出现缺失值。如果直接删除含缺失值的样本,可能会丢失大量有效信息;如果简单用均值或众数填充,又可能破坏特征之间的内在关系。本文介绍一种基于线性回归的缺失值填充方法,利用特征与目标变量之间的线性关系,分别对训练数据集和测试数据集进行填充。
2. 核心思想
线性回归填充缺失值的基本思路是:特征和目标变量之间存在一定的关系,因此可以利用这种关系对缺失的特征进行预测。具体来说,当某个特征存在缺失值时,将该特征作为目标变量,其他特征(可以包括原始的目标变量)作为输入特征,使用线性回归模型进行训练,再用训练好的模型预测缺失值。
如果多个特征都有缺失值,通常建议按照缺失值的数量从小到大进行处理。因为缺失值较少的特征对预测的要求较低,准确性可能更高。
3. 填充步骤
- 确定哪些特征包含缺失值。
- 对于包含缺失值的特征,将其作为目标变量,而其他特征(可以包括原始的目标变量)作为输入特征。如果多个特征都有缺失值,建议按照缺失值的数量从小到大进行处理。
- 在处理某个特征的缺失值时,将该特征中的已知值(即非缺失值)作为训练集,而缺失值作为需要预测的目标。此时,其他特征的相应值作为输入特征。
- 使用线性回归模型进行训练,并对缺失值进行预测。
- 将预测得到的值填充到原始数据中的相应位置。
- 重复上述步骤,直到处理完所有包含缺失值的特征。
需要注意的是,使用线性回归填充缺失值时,可能会受到模型选择和过拟合等因素的影响。因此,在实际应用中,建议对数据进行适当的预处理,如特征选择、异常值处理等,以提高填充的准确性和稳定性。同时,也可以使用交叉验证等方法来评估填充的效果。
4. 训练数据集填充
首先实现训练数据集的缺失值填充函数 lr_train_fill。该函数接收训练数据和训练标签,将两者拼接后,对每个含缺失值的特征依次进行线性回归填充。下面把代码拆成多个短小的片段,代码块中不在代码里写注释,所有解释都放在代码外的正文里,逐段讲解每一行的作用。
4.1 函数定义与数据拼接
python
def lr_train_fill(train_data, train_label):
train_data_all = pd.concat([train_data, train_label], axis=1)
train_data_all = train_data_all.reset_index(drop=True)
train_data_X = train_data_all.drop('矿物类型', axis=1)
这段代码完成函数定义和初始数据准备。第一行定义函数 lr_train_fill,接收两个参数:train_data 是特征数据,train_label 是标签数据。接下来,pd.concat 把特征和标签按列方向拼接成一张完整的表,reset_index 重置行索引,避免拼接后索引错乱。最后用 drop 去掉标签列 矿物类型,得到纯特征数据 train_data_X,后续所有填充操作都在这个特征表上进行。
4.2 统计缺失值并排序
python
null_num = train_data_X.isnull().sum()
null_num_sorted = null_num.sort_values(ascending=True)
这两行用于统计缺失情况。isnull() 把每个单元格转换为布尔值,缺失为 True,非缺失为 False;sum() 按列求和,得到每个特征的缺失数量。sort_values(ascending=True) 按缺失数量从小到大排序。这样做的原因是:缺失值越少的特征越容易预测准确,先填充它们,填充后的结果又能作为后续特征的输入,提高整体填充质量。
4.3 循环遍历特征
python
filling_feature = []
for i in null_num_sorted.index:
filling_feature.append(i)
if null_num_sorted[i] != 0:
这里开始遍历所有特征。filling_feature 是一个列表,用来累积已经处理过的特征名。循环变量 i 依次取每个特征名,先把当前特征加入列表,再判断该特征是否有缺失值。如果缺失数量不为 0,说明需要填充,进入下面的训练和预测逻辑;如果为 0,说明该特征完整,直接跳过。
4.4 构建输入特征与目标变量
python
X = train_data_X[filling_feature].drop(i, axis=1)
y = train_data_X[i]
这两行是核心的数据准备。当前特征 i 作为要预测的目标变量 y;filling_feature 列表中已经包含之前处理过的特征,把它们作为输入特征并排除当前列 i,得到输入 X。这样就能用其他特征来预测当前特征的值。
4.5 定位缺失行
python
row_numbers_mg_null = train_data_X[train_data_X[i].isnull()].index.tolist()
这一行用于找出当前特征 i 中哪些行是缺失的。先用 train_data_X[i].isnull() 得到一列布尔值,缺失位置为 True;再用它去索引数据表,拿到所有缺失行,最后通过 .index.tolist() 把这些行号转换成列表,方便后续分别切分训练集和预测集。
4.6 划分训练集与测试集
python
X_train = X.drop(row_numbers_mg_null)
y_train = y.drop(row_numbers_mg_null)
X_test = X.iloc[row_numbers_mg_null]
这里完成数据划分。当前特征 i 中非缺失的行,其输入特征和标签组成训练集 X_train 和 y_train;缺失行对应的输入特征组成测试集 X_test。这样,模型就能用完整的数据学习特征之间的关系,再对缺失部分进行预测。
4.7 训练线性回归模型
python
regr = LinearRegression()
regr.fit(X_train, y_train)
y_pred = regr.predict(X_test)
这三行完成模型训练和预测。先创建线性回归模型 regr,用 fit 在训练集上学习输入特征与目标变量之间的关系,再用 predict 对缺失行对应的输入特征进行预测,得到预测值 y_pred。
4.8 回填预测值
python
train_data_X.loc[row_numbers_mg_null, i] = y_pred
print('完成训练数据集中的\'{}\'列数据的填充'.format(i))
这两行完成回填和提示。关键的一步是 loc[row_numbers_mg_null, i] = y_pred,它把预测值写回原表中当前特征列的缺失位置,完成填充。最后打印提示信息,方便观察进度。
4.9 返回填充结果
python
return train_data_X, train_data_all['矿物类型']
函数返回两个值:填充完成后的特征数据 train_data_X,以及原始的标签列 矿物类型。这样调用方既能拿到无缺失的特征表,又能保留标签用于后续建模。
5. 测试数据集填充
测试数据集的填充逻辑与训练数据集类似,但有一个关键区别:模型只能使用训练集的数据进行训练,测试集只负责提供待填充的特征值。下面把 lr_test_fill 函数拆成更细的片段逐段讲解,代码块中同样保持纯代码,不写解释性注释。
5.1 函数定义与数据准备
python
def lr_test_fill(train_data, train_label, test_data, test_label):
train_data_all = pd.concat([train_data, train_label], axis=1)
train_data_all = train_data_all.reset_index(drop=True)
test_data_all = pd.concat([test_data, test_label], axis=1)
test_data_all = test_data_all.reset_index(drop=True)
train_data_X = train_data_all.drop('矿物类型', axis=1)
test_data_X = test_data_all.drop('矿物类型', axis=1)
函数接收四个参数:训练特征、训练标签、测试特征、测试标签。前四行分别把训练集和测试集的特征与标签拼接成完整表格,并重置索引。后两行去掉标签列,得到纯特征表 train_data_X 和 test_data_X。注意,这里训练集和测试集是分开处理的,后续模型只用训练集训练。
5.2 统计训练集缺失值并排序
python
null_num = train_data_X.isnull().sum()
null_num_sorted = null_num.sort_values(ascending=True)
这两行与训练集填充函数一致:统计训练集中每个特征的缺失数量,并按从小到大排序。这里以训练集的缺失情况为准来决定处理顺序,因为模型始终基于训练集训练,测试集只是被填充的对象。
5.3 循环遍历特征
python
filling_feature = []
for i in null_num_sorted.index:
filling_feature.append(i)
if null_num_sorted[i] != 0:
这段同样是遍历每个特征。filling_feature 列表逐个累积特征名,当当前特征 i 的缺失数量不为 0 时,进入后续填充流程;若该特征完整,则直接跳过。
5.4 构建训练输入和测试输入
python
X = train_data_X[filling_feature].drop(i, axis=1)
y = train_data_X[i]
x_test = test_data_X[filling_feature].drop(i, axis=1)
这里分别构建训练集和测试集的输入。训练集方面,当前特征 i 作为目标 y,其他已处理特征作为输入 X;测试集方面,取同样的其他特征作为输入 x_test,用于后续预测测试集中的缺失值。
5.5 剔除训练集缺失行
python
row_mg_null_train = train_data_X[train_data_X[i].isnull()].index.tolist()
X_train = X.drop(row_mg_null_train)
y_train = y.drop(row_mg_null_train)
这三行是训练准备的关键。先找出训练集中当前特征 i 为缺失值的行号,再从输入 X 和目标 y 中剔除这些行,得到干净的 X_train 和 y_train。如果不剔除,y 中会包含 NaN,导致 fit 直接报错。
5.6 定位测试集缺失行
python
row_numbers_mg_null = test_data_X[test_data_X[i].isnull()].index.tolist()
X_test = x_test.iloc[row_numbers_mg_null]
这段处理测试集。先用布尔索引找出测试集中当前特征 i 缺失的行号,再用 iloc 取出这些行对应的输入特征作为 X_test。这些缺失行就是模型需要预测填充的目标。
5.7 训练线性回归模型
python
regr = LinearRegression()
regr.fit(X_train, y_train)
这两行创建线性回归模型并用训练集拟合。注意,模型只接触训练数据,测试集完全不参与训练,这样能避免数据泄露,保证对新数据的填充是基于真实训练关系完成的。
5.8 填充测试集缺失值
python
if row_numbers_mg_null:
test_data_X.loc[row_numbers_mg_null, i] = regr.predict(X_test)
这段先判断测试集当前列是否有缺失行。如果有,就用训练好的模型对 X_test 进行预测,并把预测结果写回 test_data_X 中对应的位置,完成测试集的缺失值填充。
5.9 同步填充训练集缺失值
python
if row_mg_null_train:
train_data_X.loc[row_mg_null_train, i] = regr.predict(X.loc[row_mg_null_train])
这是最容易踩坑的地方。测试集填充完之后,还要同步把训练集中当前列的缺失值也填充掉。因为下一轮循环处理其他特征时,X 会包含当前特征列,如果训练集这一列仍有 NaN,会导致下一轮 fit 失败。所以必须把训练集也一起填干净。
5.10 打印进度信息
python
print('完成测试数据集中的{}列数据的填充'.format(i))
这一行输出当前处理完成的特征名称,方便在循环过程中观察每列的填充进度。如果运行时报错,也能通过最后打印的特征名快速定位到是哪一列出了问题。
5.11 返回填充结果
python
return test_data_X, test_data_all['矿物类型']
函数返回填充完成后的测试特征 test_data_X 和测试标签 矿物类型。调用方拿到这两个值后,就可以直接用于后续的模型评估。
6. 使用示例
假设已经准备好训练数据和测试数据,可以直接调用上述两个函数完成填充:
python
train_data_filled, train_label_filled = lr_train_fill(train_data, train_label)
test_data_filled, test_label_filled = lr_test_fill(train_data, train_label, test_data, test_label)
填充完成后,train_data_filled 和 test_data_filled 即为不含缺失值的特征数据,可以继续用于后续的模型训练和评估。
7. 常见分类模型介绍与评估
完成缺失值填充后,下一步通常是训练分类模型,对矿物类型进行预测。本节介绍三种常用的分类模型:AdaBoost、高斯朴素贝叶斯和 XGBoost,并统一使用 x_train、y_train、x_test、y_test 完成训练、预测和准确率评估。
7.1 AdaBoost 集成学习模型
AdaBoost 是一种串行式集成学习算法。它先训练一个弱学习器,再根据错误样本调整权重,让后续的学习器重点关注之前分错的样本,最终把多个弱学习器加权组合成一个强分类器。这里使用深度为 2 的决策树作为弱学习器,既能保留 AdaBoost 对困难样本的关注能力,又能通过浅层树控制过拟合。
python
from sklearn.ensemble import AdaBoostClassifier
from sklearn.tree import DecisionTreeClassifier
abf = AdaBoostClassifier(
estimator=DecisionTreeClassifier(max_depth=2),
n_estimators=200,
learning_rate=1.0,
random_state=0
)
代码中,estimator 指定基础学习器为最大深度 2 的决策树;n_estimators=200 表示最多训练 200 个弱学习器;learning_rate=1.0 控制每个弱学习器在最终投票中的贡献权重,值越小越需要更多的基学习器来配合;random_state=0 固定随机种子,保证结果可复现。
python
abf.fit(x_train, y_train)
test_predicted4 = abf.predict(x_test)
这两行先用训练集拟合模型,再对测试集进行预测,得到预测标签 test_predicted4。
python
a4 = metrics.classification_report(y_test, test_predicted4)
result4 = a4.split()
acc4 = metrics.accuracy_score(y_test, test_predicted4)
LR_result['abf准确率'] = float(f"{acc4:.5f}")
print(LR_result)
这里先调用 classification_report 生成包含精确率、召回率和 F1 的分类报告,再调用 accuracy_score 计算整体准确率。准确率被格式化为 5 位小数后写入结果字典 LR_result,键名为 abf准确率。打印结果字典可以直观看到当前已经保存的所有模型准确率。
7.2 高斯朴素贝叶斯模型
朴素贝叶斯基于贝叶斯定理,假设各特征之间相互独立。当特征为连续值时,通常使用高斯朴素贝叶斯,即假设每个特征在不同类别下都服从高斯分布。它的训练速度非常快,适合作为基准模型;如果特征独立性假设近似成立,也能取得不错的效果。
python
from sklearn.naive_bayes import GaussianNB
gnb = GaussianNB()
gnb.fit(x_train, y_train)
test_predicted5 = gnb.predict(x_test)
这段代码导入 GaussianNB,创建模型后用训练集拟合,并预测测试集。高斯朴素贝叶斯几乎不需要调参,实现简单,常用来快速验证数据是否能被较好地区分。
python
a5 = metrics.classification_report(y_test, test_predicted5)
result5 = a5.split()
acc5 = metrics.accuracy_score(y_test, test_predicted5)
LR_result['gnb准确率'] = float(f"{acc5:.5f}")
print(LR_result)
评估方式与前一个模型一致:生成分类报告、计算准确率,并把结果以 gnb准确率 为键存入字典,方便后续横向比较。
7.3 XGBoost 梯度提升树模型
XGBoost 是基于梯度提升树的高性能实现。它也采用串行集成的思想,每一棵新树都去拟合前面模型的残差,并通过二阶导数、正则化、随机抽样等手段提高训练速度和泛化能力。XGBoost 在表格型数据分类任务中通常表现稳定,但参数较多,需要根据数据规模合理设置。
python
from xgboost import XGBClassifier
xgb_model = XGBClassifier(
n_estimators=200,
max_depth=7,
learning_rate=0.05,
subsample=0.6,
colsample_bytree=0.8,
random_state=42,
objective="multi:softmax",
num_class=5
)
这里创建 XGBoost 分类器。n_estimators=200 表示树的数量;max_depth=7 限制每棵树的最大深度;较小的 learning_rate=0.05 配合较多树一起使用,有利于稳定收敛;subsample=0.6 让每棵树只使用 60% 的样本,colsample_bytree=0.8 让每棵树只使用 80% 的特征,二者都能降低过拟合;random_state=42 固定随机种子。objective 指定多分类目标,num_class 声明类别数量。这里设置的是 5 类,如果你实际的矿物类别数不是 5,需要把 num_class 改成真实类别数,例如只有 4 类时改为 num_class=4。
python
xgb_model.fit(x_train, y_train)
test_predicted6 = xgb_model.predict(x_test)
这两行完成 XGBoost 的训练和测试集预测。
python
a6 = metrics.classification_report(y_test, test_predicted6)
result6 = a6.split()
acc = metrics.accuracy_score(y_test, test_predicted6)
LR_result['xgb准确率'] = float(f"{acc:.5f}")
print(LR_result)
评估部分与前面相同,把 XGBoost 的准确率以 xgb准确率 为键保存到同一个 LR_result 字典中。
三种模型各有特点:AdaBoost 和 XGBoost 都属于集成学习,对非线性关系的学习能力更强;高斯朴素贝叶斯训练快、可解释性好,适合作为基准。最终可以根据 LR_result 里的准确率以及分类报告中的精确率、召回率进行综合选择。
8. 总结
本文介绍了基于线性回归的缺失值填充方法,并分别给出了训练数据集和测试数据集的实现代码。该方法利用特征之间的线性关系进行预测填充,相比简单的均值填充更能保留数据的内在结构。需要注意的是,线性回归填充的效果依赖于特征之间的线性相关性,实际使用时应结合数据特点进行预处理,并通过交叉验证等方式评估填充效果。