机器学习项目实战:缺失值填充、随机森林与网格搜索
今天的课程继续完善矿物分类系统,主要学习了数据缺失值处理、线性回归填充、随机森林填充、模型训练与网格搜索等内容。课程不再只介绍单个算法,而是把数据清洗、特征处理、模型训练和结果评价串联成了一套完整流程。
原始数据中包含氯、钠、镁、硫、钙、钾、碳、溴、银、pH、硼、氟、硒等特征,目标变量为矿物类型。由于部分特征存在大量空值,如果不先处理,许多机器学习模型将无法正常训练。
一、检查数据中的缺失值
首先将训练特征与标签合并,再统计每一列的缺失数量:
train_data_all = pd.concat(
[train_data, train_label],
axis=1
).reset_index(drop=True)
train_data_X = train_data_all.drop(
"矿物类型",
axis=1
)
null_num = train_data_X.isnull().sum()
null_num_sorted = null_num.sort_values(
ascending=True
)
print(null_num_sorted)
isnull() 会判断每个位置是否为空,sum() 则统计每列空值的数量。将结果按照从小到大排序后,可以先处理缺失较少的特征。
课程中采用这种顺序,是因为后面要利用其他特征预测当前缺失特征。如果先填补缺失较少的列,随着处理不断进行,可用于训练的数据会越来越完整,也能为缺失较多的特征提供更多输入信息。
二、使用线性回归填补缺失值
常见的缺失值处理方法包括删除缺失行、填入平均数、中位数或众数。这些方法比较简单,但没有充分利用特征之间的关系。
如果不同特征之间存在相关性,就可以把存在缺失的特征当作预测目标,把其他已经完成处理的特征作为输入,训练一个回归模型预测空缺位置。
例如,当前需要填补“镁”这一列:
氯、钠、溴、硼等特征 → 线性回归模型 → 镁
代码首先准备已经可以使用的特征:
from sklearn.linear_model import LinearRegression
filling_feature = []
for i in null_num_sorted.index:
filling_feature.append(i)
if null_num_sorted[i] != 0:
X = train_data_X[
filling_feature
].drop(i, axis=1)
y = train_data_X[i]
这里的 i 表示当前准备填充的特征。X 是其他已经处理过的特征,y 是当前特征。
接着找出当前特征的缺失位置:
row_numbers_null = train_data_X[
train_data_X[i].isnull()
].index.tolist()
根据这些行号,将数据划分成两部分:
X_train = X.drop(row_numbers_null)
y_train = y.drop(row_numbers_null)
X_test = X.iloc[row_numbers_null]
其中:
X_train是当前特征不为空的样本;y_train是这些样本对应的真实值;X_test是当前特征为空、需要进行预测的样本。
然后创建并训练线性回归模型:
regr = LinearRegression()
regr.fit(X_train, y_train)
y_pred = regr.predict(X_test)
最后把模型预测的结果写回原来的空缺位置:
train_data_X.loc[
row_numbers_null,
i
] = y_pred
print("完成特征 {} 的填充".format(i))
完整的训练集填充函数可以整理为:
def lr_train_fill(train_data, train_label):
train_data_all = pd.concat([train_data, train_label],axis=1).reset_index(drop=True)
train_data_X = train_data_all.drop("矿物类型",axis=1)
null_num = train_data_X.isnull().sum()
null_num_sorted = null_num.sort_values()
filling_feature = []
for feature in null_num_sorted.index:
filling_feature.append(feature)
if null_num_sorted[feature] == 0:
continue
X = train_data_X[filling_feature].drop(feature, axis=1)
y = train_data_X[feature]
null_rows = train_data_X[train_data_X[feature].isnull()].index.tolist()
X_train = X.drop(null_rows)
y_train = y.drop(null_rows)
X_test = X.loc[null_rows]
model = LinearRegression()
model.fit(X_train, y_train)
y_pred = model.predict(X_test)
train_data_X.loc[ null_rows,feature] = y_pred
return train_data_X, train_data_all["矿物类型"]
这种方法的核心思想并不复杂:先使用完整数据学习不同特征之间的关系,再利用这种关系估计空缺值。
三、填充测试集中的缺失数据
训练集填补完成后,测试集也可能存在空值。处理测试集时,可以将已经填好的训练数据作为模型训练基础,再预测测试集中的缺失部分。
代码首先分别合并训练数据和测试数据:
train_data_all = pd.concat(
[train_data, train_label],
axis=1
).reset_index(drop=True)
test_data_all = pd.concat(
[test_data, test_label],
axis=1
).reset_index(drop=True)
train_data_X = train_data_all.drop(
"矿物类型",
axis=1
)
test_data_X = test_data_all.drop(
"矿物类型",
axis=1
)
然后统计测试集各列的缺失数量:
null_num = test_data_X.isnull().sum()
null_num_sorted = null_num.sort_values()
对于存在空值的特征,可以用训练集中该特征的完整值作为标签,训练回归模型后对测试集的缺失位置进行预测:
for feature in null_num_sorted.index:
if null_num_sorted[feature] == 0:
continue
null_rows = test_data_X[
test_data_X[feature].isnull()
].index.tolist()
feature_columns = [
col for col in train_data_X.columns
if col != feature
]
X_train = train_data_X[feature_columns]
y_train = train_data_X[feature]
X_test = test_data_X.loc[
null_rows,
feature_columns
]
model = LinearRegression()
model.fit(X_train, y_train)
y_pred = model.predict(X_test)
test_data_X.loc[
null_rows,
feature
] = y_pred
这里要注意,测试数据不能参与模型训练。测试集的作用是评价模型面对未知数据时的表现,如果提前将测试信息泄露给训练过程,最终得到的评价结果会过于乐观。
四、随机森林回归填充
线性回归假设输入特征与目标之间主要存在线性关系。但真实数据中的关系往往更复杂,因此课程又使用随机森林回归器进行填充。
随机森林由多棵决策树组成。每棵树根据部分样本和部分特征进行学习,最终将多棵树的结果综合起来。与单棵决策树相比,随机森林通常具有更好的稳定性,也能够表示复杂的非线性关系。
模型的创建方式如下:
from sklearn.ensemble import RandomForestRegressor
regr = RandomForestRegressor(
n_estimators=100,
random_state=42
)
其中:
n_estimators=100表示建立100棵决策树;random_state=42用来固定随机结果,保证程序多次运行时具有可重复性。
随机森林填充与线性回归填充的流程基本一致,只需要更换模型:
model = RandomForestRegressor(
n_estimators=100,
random_state=42
)
model.fit(X_train, y_train)
y_pred = model.predict(X_test)
train_data_X.loc[
null_rows,
feature
] = y_pred
可以将其封装为函数:
def rf_fill(data_X):
null_num = data_X.isnull().sum()
null_num_sorted = null_num.sort_values()
filling_feature = []
for feature in null_num_sorted.index:
filling_feature.append(feature)
if null_num_sorted[feature] == 0:
continue
X = data_X[filling_feature].drop(feature, axis=1)
y = data_X[feature]
null_rows = data_X[data_X[feature].isnull()].index.tolist()
X_train = X.drop(null_rows)
y_train = y.drop(null_rows)
X_test = X.loc[null_rows]
model = RandomForestRegressor(n_estimators=100,random_state=42)
model.fit(X_train, y_train)
y_pred = model.predict(X_test)
data_X.loc[null_rows,feature] = y_pred
return data_X
线性回归计算速度较快、结果容易解释,但对复杂关系的拟合能力有限;随机森林可以拟合非线性关系,但模型更复杂、运行时间也更长。实际使用时不能只凭经验判断,需要通过后续分类效果比较不同填充方案。
五、保存处理后的数据
完成训练集和测试集填充后,课程将结果重新合并并保存为 Excel 文件:
data_train = pd.concat([os_y_train, os_X_train],axis=1).sample(frac=1,random_state=4)
data_test = pd.concat([y_test_fill, X_test_fill],axis=1)
data_train.to_excel("./temp_data/训练数据集[线性回归填充].xlsx",index=False)
data_test.to_excel("./temp_data/测试数据集[线性回归填充].xlsx",index=False)
训练集通过 sample(frac=1) 打乱顺序,避免同一类别的数据连续排列。random_state=4 用于固定打乱结果。
课程中不仅生成了线性回归填充的数据,还保留了平均值、中位数、众数、删除空数据行和随机森林填充等多个版本。这样可以让相同分类模型在不同数据集上运行,比较哪种处理方式更适合当前项目。
六、使用分类模型评价填充结果
缺失值填充的目标并不是让表格看起来完整,而是提高后续分类模型的实际效果。因此,填充完成后还需要使用逻辑回归、随机森林、支持向量机、高斯朴素贝叶斯、AdaBoost、XGBoost以及神经网络等模型进行测试。
以逻辑回归为例:
from sklearn.linear_model import LogisticRegression
from sklearn import metrics
lr = LogisticRegression(
C=0.001,
max_iter=100,
penalty="none",
solver="lbfgs"
)
lr.fit(train_data_X, train_data_y)
train_predicted = lr.predict(train_data_X)
test_predicted = lr.predict(test_data_X)
print(
metrics.classification_report(
train_data_y,
train_predicted
)
)
print(
metrics.classification_report(
test_data_y,
test_predicted,
digits=6
)
)
classification_report() 会输出每个类别的精确率、召回率、F1值以及样本数,同时给出总体准确率。
课程还将不同类别的召回率和总准确率保存到字典中:
report = metrics.classification_report(
test_data_y,
test_predicted,
output_dict=True
)
LR_result = {
"recall_0": report["0"]["recall"],
"recall_1": report["1"]["recall"],
"recall_2": report["2"]["recall"],
"recall_3": report["3"]["recall"],
"acc": report["accuracy"]
}
只观察总体准确率可能会掩盖少数类别识别较差的问题。例如某一类别的样本特别多,即使模型只擅长识别这个类别,也可能得到较高准确率。因此还要关注各类别的召回率以及宏平均指标。
七、使用网格搜索选择参数
模型效果不仅与算法有关,也与参数设置密切相关。例如逻辑回归中的 C、随机森林中的树数量和最大深度、支持向量机中的核函数都会影响结果。
网格搜索会提前给出多组候选参数,然后尝试所有参数组合,并通过交叉验证选出效果最好的一组:
from sklearn.model_selection import GridSearchCV
from sklearn.linear_model import LogisticRegression
model = LogisticRegression(
max_iter=1000
)
param_grid = {
"C": [0.001, 0.01, 0.1, 1, 10],
"solver": ["lbfgs", "liblinear"]
}
grid_search = GridSearchCV(
estimator=model,
param_grid=param_grid,
cv=5,
scoring="accuracy",
n_jobs=-1
)
grid_search.fit(
train_data_X,
train_data_y
)
print("最优参数:", grid_search.best_params_)
print("交叉验证得分:", grid_search.best_score_)
cv=5 表示使用五折交叉验证。数据会被分成五份,每次使用其中四份训练、一份验证,重复五次后计算平均结果。与只进行一次训练集划分相比,交叉验证得到的结果更加稳定。
确定最优参数后,再使用最佳模型预测测试集:
best_model = grid_search.best_estimator_
test_predicted = best_model.predict(
test_data_X
)
print(
metrics.classification_report(
test_data_y,
test_predicted
)
)
网格搜索虽然使用方便,但参数组合数量过多时,训练时间会迅速增加。因此实际使用时应先根据经验确定合理范围,再逐步缩小搜索区间。
总结
今天的课程围绕矿物分类项目,完成了从数据预处理到模型评价的重要步骤。首先统计各特征的缺失情况,然后依次使用线性回归和随机森林预测空缺数据,并分别处理训练集和测试集。完成填充后,再将数据保存为不同版本,使用多种分类模型比较效果,最后通过网格搜索寻找更合适的模型参数。
这次学习让我认识到,数据预处理并不是模型训练前无关紧要的准备工作。缺失值采用平均数、线性回归还是随机森林填充,可能直接改变后续模型的分类效果。真正可靠的处理方案,应当通过测试集指标和交叉验证结果进行选择,而不是只关注数据是否已经补齐。
更多推荐

所有评论(0)