今天的课程继续完善矿物分类系统,主要学习了数据缺失值处理、线性回归填充、随机森林填充、模型训练与网格搜索等内容。课程不再只介绍单个算法,而是把数据清洗、特征处理、模型训练和结果评价串联成了一套完整流程。

原始数据中包含氯、钠、镁、硫、钙、钾、碳、溴、银、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
    )
)

网格搜索虽然使用方便,但参数组合数量过多时,训练时间会迅速增加。因此实际使用时应先根据经验确定合理范围,再逐步缩小搜索区间。

总结

今天的课程围绕矿物分类项目,完成了从数据预处理到模型评价的重要步骤。首先统计各特征的缺失情况,然后依次使用线性回归和随机森林预测空缺数据,并分别处理训练集和测试集。完成填充后,再将数据保存为不同版本,使用多种分类模型比较效果,最后通过网格搜索寻找更合适的模型参数。

这次学习让我认识到,数据预处理并不是模型训练前无关紧要的准备工作。缺失值采用平均数、线性回归还是随机森林填充,可能直接改变后续模型的分类效果。真正可靠的处理方案,应当通过测试集指标和交叉验证结果进行选择,而不是只关注数据是否已经补齐。

更多推荐