欢迎光临
我们一直在努力

【机器学习入门】缺失值处理 + 类别不平衡:矿物等级多分类项目

文章目录

  • 一、项目背景
  • 二、环境准备与依赖
  • 三、数据集说明
  • 四、整体流程
  • 五、数据预处理主脚本详解
  • 六、缺失值填充函数库 — 六种方法详解
    • 6.1 删除空行填充(Complete Case Analysis, CCA)
    • 6.2 平均值 / 中位数填充
    • 6.3 众数填充
    • 6.4 线性回归填充 & 随机森林填充(模型预测填充)
  • 七、训练与评估主脚本详解
    • 7.1 六个分类模型
    • 7.2 评估指标提取
  • 八、总结

一、项目背景

在实际的机器学习项目中,数据缺失(Missing Value) 是最常见也最头疼的问题之一。原始数据里往往存在大量空值,如果直接丢给模型,要么报错、要么导致训练效果极差。

本项目以一份矿物数据为例,完整演示了一条经典的分类建模流水线:

  • 读取并清洗 Excel 数据;
  • 对 6 种不同的缺失值填充策略进行对比;
  • 在填充后的数据上训练 6 个经典分类模型(逻辑回归、随机森林、SVM、AdaBoost、朴素贝叶斯、XGBoost);
  • 提取各模型的召回率与准确率,横向对比不同填充方法的优劣。
  • 整个项目由三个核心脚本构成,各司其职:

    脚本作用
    数据预处理主脚本 数据预处理主函数:读数据、编码、标准化、划分、填充、过采样、保存
    填充函数库 填充函数库:实现 6 种缺失值填充方法(含训练集 / 测试集两套函数)
    训练与评估主脚本 训练主函数:对 6 种填充结果分别训练 6 个模型并输出评估指标

    二、环境准备与依赖

    pip install pandas numpy scikit-learn imbalanced-learn xgboost matplotlib openpyxl xlrd

    库用途
    pandas 数据读取与处理
    numpy 数值计算(被 sklearn 依赖)
    scikit-learn 数据划分、标准化、模型训练、评估
    imbalanced-learn SMOTE 过采样(处理类别不平衡)
    xgboost XGBoost 分类模型
    matplotlib 可视化(本项目仅注释代码中用到)
    xlrd 读取旧版 .xls 格式 Excel
    openpyxl 保存 .xlsx 文件

    注意:.xls 是 Excel 97-2003 旧格式,pandas.read_excel 读取它需要 xlrd(1.x/2.x 均可,新版 xlrd 只支持 xls 不支持 xlsx,因此保存用 openpyxl 引擎)。


    三、数据集说明

    数据集为 矿物数据.xls,共 1043 行 × 15 列(含表头),其中 14 个特征、1 个标签列。

    列名含义
    序号 样本编号(非特征,建模时剔除)
    氯、钠、镁、硫、钙、钾、碳、溴、锶、pH、硼、氟、硒 13 个矿物指标特征
    矿物类型 标签列,取值 A/B/C/D/E

    在这里插入图片描述

    类别分布(原始):

    类别ABCDE
    样本数 544 367 84 47 1

    可以看到:

    • 类别分布极不均衡,D 类仅 47 条,E 类只有 1 条;
    • 因此预处理脚本首先剔除 E 类(样本太少没有建模价值),最终用 A/B/C/D 四类共 1042 条数据建模。

    缺失值情况(剔除 E 后):

    特征缺失数特征缺失数
    886 0
    748 0
    150 0
    137 0
    131 序号 0
    65 矿物类型 0
    pH 21
    2
    1

    样本量大幅减少,其中「硫」「锶」两列缺失率超过 70%,是填充的重灾区,也是「删除空行」策略失效的根本原因。


    四、整体流程

    读取 Excel(剔除E类)

    标签编码 A/B/C/D → 0/1/2/3

    特征强制转数值(非法值→NaN)

    Z-score 标准化

    划分训练集 / 测试集(7:3)

    6 种填充方法(train / test 分别处理)

    SMOTE 过采样(仅训练集)

    保存填充后的训练 / 测试数据集

    6 个分类模型 × 6 种填充 → 评估对比


    五、数据预处理主脚本详解

    该脚本是整条流水线的「指挥中心」。核心逻辑如下:

    import os
    import pandas as pd
    import fill_data

    # 1. 读取 excel 数据,剔除类别 E
    data = pd.read_excel('矿物数据.xls')
    data = data[data['矿物类型'] != 'E']

    # 2. 统计各列缺失值(用于观察数据质量)
    null_total = data.isnull().sum()

    # 3. 划分特征与标签
    X_whole = data.drop('矿物类型', axis=1).drop('序号', axis=1)
    y_whole = data['矿物类型']

    # 4. 标签编码 A/B/C/D → 0/1/2/3
    label_dict = {'A': 0, 'B': 1, 'C': 2, 'D': 3}
    y_whole = pd.Series([label_dict[label] for label in y_whole], name='矿物类型')

    # 5. 强制转数值,非法内容置 NaN(统一数据格式,方便后续填充)
    for column_name in X_whole.columns:
    X_whole[column_name] = pd.to_numeric(X_whole[column_name], errors='coerce')

    # 6. Z-score 标准化
    from sklearn.preprocessing import StandardScaler
    scaler = StandardScaler()
    X_whole_Z = scaler.fit_transform(X_whole)
    X_whole = pd.DataFrame(X_whole_Z, columns=X_whole.columns)

    # 7. 划分训练集 / 测试集
    from sklearn.model_selection import train_test_split
    x_train_w, x_test_w, y_train_w, y_test_w = \\
    train_test_split(X_whole, y_whole, test_size=0.3, random_state=1000)

    # 8. 选择一种填充方法(下面以随机森林填充为例)
    x_train_fill, y_train_fill = fill_data.rf_train_fill(x_train_w, y_train_w)
    x_test_fill, y_test_fill = fill_data.rf_test_fill(x_train_fill, y_train_fill, x_test_w, y_test_w)

    # 9. SMOTE 过采样(缓解类别不平衡,仅针对训练集)
    from imblearn.over_sampling import SMOTE
    smote = SMOTE(k_neighbors=2, random_state=1000)
    os_x_train, os_y_train = smote.fit_resample(x_train_fill, y_train_fill)

    # 10. 保存训练 / 测试数据集(标签列放第一列)
    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)

    os.makedirs('.//temp_data', exist_ok=True) # 先创建目录再保存
    data_train.to_excel(r'.//temp_data//6_训练数据集_随机森林填充.xlsx', index=False)
    data_test.to_excel(r'.//temp_data//6_测试数据集_随机森林填充.xlsx', index=False)

  • 标签编码用字典映射:把字符串类别 A/B/C/D 映射为整数 0/1/2/3,直观且不易出错;
  • pd.to_numeric(…, errors='coerce'):把列统一转成数值,无法转换的非法内容置为 NaN,避免因「混入文本」导致后续标准化/建模报错;
  • train_test_split:test_size=0.3 表示 30% 数据做测试集,random_state=1000 固定随机种子,保证每次运行划分一致、结果可复现;
  • SMOTE 过采样:用合成少数类样本的方式平衡类别,比简单复制更有利于训练;
  • 保存时 sample(frac=1, random_state=4):把过采样后的训练集打乱顺序,避免同类样本聚集在一起。
  • 项目里其它 5 种填充方法只需把第 8 步换成 fill_data.cca/mean/median/mode/lr_train_fill 等函数,并把保存的文件名改为 1~5 对应序号即可(代码中已经用注释保留)。


    六、缺失值填充函数库 — 六种方法详解

    填充函数库提供了每个方法的 _train_fill(训练集) 与 _test_fill(测试集) 两个函数

    6.1 删除空行填充(Complete Case Analysis, CCA)

    def cca_train_fill(train_data, train_label):
    data = pd.concat([train_data, train_label], axis=1)
    data = data.reset_index(drop=True)
    df_filled = data.dropna() # 删除任何含 NaN 的行
    return df_filled.drop('矿物类型', axis=1), df_filled.矿物类型

    • 原理:直接把含缺失值的整行删除,只保留完整样本。
    • 优点:简单粗暴、不引入人为估计的偏差。
    • 致命缺点:本数据「硫」列缺失 886 行,删除后训练集 729 → 64 行、测试集 313 → 28 行,丢失了约 90% 的数据,且测试集中类别 2 直接消失。此时训练出的模型基本没有参考价值,也不满足「类别齐全」的对比前提。

    6.2 平均值 / 中位数填充

    def mean_method(data):
    fill_values = data.mean() # 求每列均值
    return data.fillna(fill_values) # 用均值填充缺失

    def median_method(data):
    fill_values = data.median() # 求每列中位数
    return data.fillna(fill_values)

    • 训练集填充时按类别分组,用「同类别样本」的均值/中位数填充——因为不同类别矿物的指标分布差异大,按类填充更合理:

    def mean_train_fill(train_data, train_label):
    data = pd.concat([train_data, train_label], axis=1).reset_index(drop=True)
    A = mean_method(data[data['矿物类型'] == 0]) # 仅用 0 类样本的均值填 0 类缺失
    B = mean_method(data[data['矿物类型'] == 1])
    C = mean_method(data[data['矿物类型'] == 2])
    D = mean_method(data[data['矿物类型'] == 3])
    df_filled = pd.concat([A, B, C, D]).reset_index(drop=True)
    return df_filled.drop('矿物类型', axis=1), df_filled.矿物类型

    • 测试集填充防数据泄露:测试集的填充值必须来自训练集,绝不能用测试集自身统计量,否则相当于把「答案」泄露给模型:

    def mean_test_method(train_data, test_data):
    fill_values = train_data.mean() # 用训练集均值
    return test_data.fillna(fill_values) # 填充测试集

    • 均值 vs 中位数:均值对异常值敏感,数据存在离群点时中位数更稳健。

    6.3 众数填充

    def mode_method(data):
    # 对每列求众数(出现次数最多的值),取第一个;空列返回 None
    fill_values = data.apply(lambda x: x.mode().iloc[0] if len(x.mode()) > 0 else None)
    return data.fillna(fill_values)

    • 原理:用该列出现频率最高的值填充。
    • 适用场景:适合分类/离散型特征;对连续型特征意义不大。
    • 小瑕疵:x.mode() 被重复计算两次,且 a = data.mode() 是冗余的无效变量(见第八节优化)。

    6.4 线性回归填充 & 随机森林填充(模型预测填充)

    这是两种基于回归模型的高级填充法,思想一致:

    把含缺失的特征当作「目标变量」,用其它无缺失的特征训练回归模型,预测缺失位置的值。

    def rf_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_sorted = train_data_X.isnull().sum().sort_values(ascending=True)
    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] # 当前缺失列为预测目标
    row_numbers_mg_null = train_data_X[train_data_X[i].isnull()].index.tolist()

    X_train = X.drop(row_numbers_mg_null) # 非缺失行 → 训练回归模型
    y_train = y.drop(row_numbers_mg_null)
    X_test = X.iloc[row_numbers_mg_null] # 缺失行 → 待预测

    rf = RandomForestRegressor(n_estimators=100, random_state=42)
    rf.fit(X_train, y_train)
    y_pred = rf.predict(X_test)
    train_data_X.loc[row_numbers_mg_null, i] = y_pred # 回填预测值
    print(f'完成训练集中的“{i}”列数据的填充')
    return train_data_X, train_data_all.矿物类型

    算法要点:

  • 先统计每列缺失数量并升序排列;
  • 从缺失最少的特征开始处理(缺失越少,用它预测其它列越可靠);
  • 处理某列时,已填充好的列作为输入特征、该列为目标,训练回归模型预测缺失值;
  • 依次处理完所有含缺失的列。
  • 线性回归 vs 随机森林:前者假设特征间是线性关系、快但欠拟合复杂关系;后者用集成树、能捕捉非线性、精度更高但更慢。所以线性回归填充用 LinearRegression,随机森林填充用 RandomForestRegressor。

    测试集函数:lr_test_fill / rf_test_fill 的思路是——用「已填充好的训练集特征」训练模型,去预测「测试集缺失列」,保证测试集不参与模型拟合(无数据泄露)。


    七、训练与评估主脚本详解

    该脚本对 6 种填充结果逐一进行同样的建模评估。核心结构如下:

    import pandas as pd
    from sklearn import metrics

    # 以平均值填充为例:读取填充后的数据
    train_data = pd.read_excel(r'.//temp_data//2_训练数据集_平均值填充.xlsx')
    train_data_x = train_data.iloc[:, 1:] # 第 2 列起为特征
    train_data_y = train_data.iloc[:, 0] # 第 1 列为标签
    test_data = pd.read_excel(r'.//temp_data//2_测试数据集_平均值填充.xlsx')
    test_data_x = test_data.iloc[:, 1:]
    test_data_y = test_data.iloc[:, 0]

    7.1 六个分类模型

    模型代码要点
    逻辑回归 LogisticRegression C=0.001, max_iter=100, penalty=None, solver='lbfgs'
    随机森林 RandomForestClassifier n_estimators=50, max_depth=20, bootstrap=False, max_features='log2'
    SVM SVC kernel='poly', C=1, degree=4, gamma=1
    AdaBoost AdaBoostClassifier 基学习器为 DecisionTreeClassifier(max_depth=2),n_estimators=200
    高斯朴素贝叶斯 GaussianNB 无参数,直接用
    XGBoost XGBClassifier learning_rate=0.05, n_estimators=200, objective='multi:softmax'

    以逻辑回归为例:

    from sklearn.linear_model import LogisticRegression
    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)
    print('平均值填充_LR的train:\\n', metrics.classification_report(train_data_y, train_predicted, digits=6))
    test_predicted = lr.predict(test_data_x)
    print('平均值填充_LR的test:\\n', metrics.classification_report(test_data_y, test_predicted, digits=6))

    7.2 评估指标提取

    原代码通过 classification_report 输出字符串的固定下标取值:

    # 生成分类报告字符串,保留6位小数
    a = metrics.classification_report(test_data_y, test_predicted, digits=6)
    # 按空白字符分割为列表
    b = a.split()
    # 提取类别0召回率
    LR_result['recall_0'] = float(b[6])
    # 提取类别1召回率
    LR_result['recall_1'] = float(b[11])
    # 提取类别2召回率
    LR_result['recall_2'] = float(b[16])
    # 提取类别3召回率
    LR_result['recall_3'] = float(b[21])
    # 提取整体准确率
    LR_result['acc'] = float(b[25])

    对分类报告 split()后 b[6]/b[11]/b[16]/b[21]/b[25] 恰好对应 4 类召回率与总体准确率。


    八、总结

    通过这个项目,可以掌握以下知识点:

  • 缺失值处理是建模前不可跳过的一步,6 种填充方法各有适用场景:
    • 删除空行:简单但数据损失大;
    • 均值/中位数:适合数值型,中位数抗异常值;
    • 众数:适合离散型;
    • 线性回归/随机森林:用模型预测填充,精度更高但更复杂;
  • 测试集填充必须「用训练集统计量」,避免数据泄露;
  • 类别不平衡 用 SMOTE 过采样缓解;
  • 本项目完整完成了矿物数据清洗、缺失值填充、数据平衡、模型训练与指标评估全流程实验。实验发现,高缺失特征会导致直接删值策略彻底失效,按类别统计填充、模型预测填充等精细化处理方式更适配本数据集。

    赞(0)
    未经允许不得转载:171主机测评 » 【机器学习入门】缺失值处理 + 类别不平衡:矿物等级多分类项目
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址