在矿物识别的机器学习任务中,数据集的质量直接决定了模型最终的性能表现。实际工业场景中采集的矿物数据往往存在两大核心问题:一是因仪器故障、人工记录失误等导致的缺失值,二是因不同矿物采集难度差异造成的样本分布不均衡。这两个问题若不妥善处理,会导致模型训练偏差、泛化能力下降,甚至完全失效。

本文以矿物识别项目为背景,详细讲解如何基于 Python 实现矿物数据集的完整预处理流程,包括数据清洗、6 种缺失值填充策略的实现、Z 标准化处理以及 SMOTE 算法的样本均衡化,最终生成可直接用于模型训练的标准化数据集。

一、项目环境与数据准备

1. 环境依赖

本项目基于 Python 生态实现,核心依赖库及版本建议如下:

# 核心库安装命令
pip install imblearn==0.12.0
  • imblearn:实现 SMOTE 过采样算法,解决样本不均衡问题

二、数据预处理核心流程

2.1 数据读取与初步清洗

首先完成数据的读取、无效数据过滤和数据类型标准化,为后续处理奠定基础:

import pandas as pd
import matplotlib.pylab as plt
from 数据清洗 import fill_data

# 读取Excel数据(xlrd引擎兼容.xls格式)
data = pd.read_excel('矿物数据.xls', engine='xlrd')

# 过滤无效标签(剔除矿物类型为E的样本)
data = data[data['矿物类型'] != 'E']
null_num = data.isnull()
null_total = null_num.sum()  # 检测每列的缺失值

# 分离特征与标签(剔除无意义的序号列和矿物类型)
X_whole = data.drop('矿物类型', axis=1).drop('序号', axis=1)
# 将矿物类型单独列出
Y_whole = data.矿物类型

# 标签编码(将字符标签A/B/C/D转换为数值0-3,适配模型输入)
label_dict = {'A': 0, 'B': 1, 'C': 2, 'D': 3}
encoded_label = [label_dict[label] for label in Y_whole]
Y_whole = pd.Series(encoded_label, name='矿物类型')

# 特征列数据类型标准化(强制转换为数值型,无法转换的标记为NaN)
# 字符串数值直接转换为float,\和空格转换为Nan
for column_name in X_whole.columns:
    X_whole[column_name] = pd.to_numeric(X_whole[column_name], errors='coerce')

2.2 特征标准化(Z 标准化)

矿物特征的量纲差异(如硬度单位为 HRC,密度单位为 g/cm³)会导致模型偏向数值范围大的特征,因此需进行 Z 标准化(均值为 0,方差为 1):

from sklearn.preprocessing import StandardScaler

# 初始化标准化器并拟合特征数据
scaler = StandardScaler()
X_whole_Z = scaler.fit_transform(X_whole)

# 转换为DataFrame,保留列名便于后续处理
X_whole = pd.DataFrame(X_whole_Z, columns=X_whole.columns)

2.3 数据集切分

将数据集按 7:3 比例切分为训练集和测试集,确保后续缺失值填充时测试集不参与训练集的统计计算(避免数据泄露):

from sklearn.model_selection import train_test_split

# 随机种子random_state=50000保证切分结果可复现
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=50000
)

三、缺失值填充:6 种策略的实现

缺失值是矿物数据的核心问题,本文在fill_data.py文件中实现了 6 种填充策略,涵盖简单统计填充、机器学习填充等不同类型,适配不同业务场景需求,并且主文件调用。

3.1 策略 1:删除缺失值(保留完整行)

核心逻辑:直接剔除包含缺失值的样本,仅保留完整数据行,是最简单的缺失值处理方式,适用于缺失率极低(<5%)的场景。

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.矿物类型

def cca_test_fill(train_data, train_label, test_data, test_label):
    """测试集缺失值处理:与训练集逻辑一致"""
    data = pd.concat([test_data, test_label], axis=1)
    data = data.reset_index(drop=True)
    df_filled = data.dropna()
    return df_filled.drop('矿物类型', axis=1), df_filled.矿物类型

3.2 策略 2-4:统计值填充(均值 / 中位数 / 众数)

统计值填充是工业界最常用的缺失值处理方式,核心逻辑是按矿物类型分组计算统计值,用同类样本的统计特征填充缺失值,避免不同类型样本的统计值交叉污染。

以均值填充为例,实现代码如下:

def mean_train_method(data):
    """按列计算均值,填充缺失值"""
    fill_values = data.mean()
    return data.fillna(fill_values)

def mean_train_fill(train_data, train_label):
    """训练集均值填充:按矿物类型分组填充"""
    data = pd.concat([train_data, train_label], axis=1)
    data = data.reset_index(drop=True)
    
    # 按矿物类型分组(0-3对应A-D)
    A = data[data['矿物类型'] == 0]
    B = data[data['矿物类型'] == 1]
    C = data[data['矿物类型'] == 2]
    D = data[data['矿物类型'] == 3]
    
    # 分组填充
    A = mean_train_method(A)
    B = mean_train_method(B)
    C = mean_train_method(C)
    D = mean_train_method(D)
    
    df_filled = pd.concat([A, B, C, D]).reset_index(drop=True)
    return df_filled.drop('矿物类型', axis=1), df_filled.矿物类型

def mean_test_fill(train_data, train_label, test_data, test_label):
    """测试集均值填充:使用训练集的统计值(避免数据泄露)"""
    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)
    
    # 训练集分组
    A_train = train_data_all[train_data_all['矿物类型'] == 0]
    B_train = train_data_all[train_data_all['矿物类型'] == 1]
    C_train = train_data_all[train_data_all['矿物类型'] == 2]
    D_train = train_data_all[train_data_all['矿物类型'] == 3]
    
    # 测试集分组
    A_test = test_data_all[test_data_all['矿物类型'] == 0]
    B_test = test_data_all[test_data_all['矿物类型'] == 1]
    C_test = test_data_all[test_data_all['矿物类型'] == 2]
    D_test = test_data_all[test_data_all['矿物类型'] == 3]
    
    # 用训练集均值填充测试集
    A = A_test.fillna(A_train.mean())
    B = B_test.fillna(B_train.mean())
    C = C_test.fillna(C_train.mean())
    D = D_test.fillna(D_train.mean())
    
    df_filled = pd.concat([A, B, C, D]).reset_index(drop=True)
    return df_filled.drop('矿物类型', axis=1), df_filled.矿物类型

中位数 / 众数填充:仅需将上述代码中的mean()替换为median()(中位数)或自定义众数计算逻辑即可

3.3 策略 5-6:机器学习填充(线性回归 / 随机森林)

对于缺失率较高(>20%)且特征间存在强相关性的矿物数据,统计值填充会丢失特征关联信息,此时可通过机器学习模型预测缺失值,核心逻辑是:将含缺失值的列作为目标变量,其他列作为特征,训练回归模型预测缺失值。

该函数的核心逻辑是:按缺失值数量升序遍历特征列,用其他已处理特征训练线性回归模型,预测并填充当前列的缺失值,测试集数据根据训练集的特征和模型预测,测试集数据不参与统计填充数据。(随机森林的逻辑同上)

from sklearn.linear_model import LinearRegression
def lr_train_fill(train_data, train_label):
    # 拼接训练集特征和标签(合并为完整DataFrame,方便后续统一处理索引)
    # train_data:训练集特征(DataFrame);train_label:训练集标签(Series,矿物类型)
    train_data_all = pd.concat([train_data, train_label], axis=1)
    
    # 重置索引(避免拼接后索引混乱,保证行索引从0开始连续)
    train_data_all = train_data_all.reset_index(drop=True)
    
    # 分离纯特征列(剔除标签列“矿物类型”,只保留需要填充的特征数据)
    train_data_X = train_data_all.drop('矿物类型', axis=1)
    
    # 统计每列的缺失值数量(isnull()返回布尔矩阵,sum()按列求和得到每列缺失值个数)
    null_num = train_data_X.isnull().sum()
    
    # 按缺失值数量升序排序列名(核心!先填充缺失少的列,保证后续填充有足够有效特征)
    # 比如:列A缺失5个值,列B缺失20个值 → 先填列A,再填列B
    null_num_sorted = null_num.sort_values(ascending=True)

    # 初始化空列表:记录已处理(无缺失/已填充)的特征列,作为后续填充的自变量
    filling_feature = []
    
    # 遍历排序后的列名(按缺失值从少到多)
    for i in null_num_sorted.index:
        # 将当前列加入“已处理特征列表”(无论是否有缺失,都加入,保证后续列能用到)
        filling_feature.append(i)
        
        # 仅处理有缺失值的列(无缺失则跳过,节省计算)
        if null_num_sorted[i] != 0:
            # 构建回归模型的自变量X:已处理特征列中剔除当前列(用其他特征预测当前列)
            # filling_feature是累积的已处理列,drop(i)排除当前待填充列
            X = train_data_X[filling_feature].drop(i, axis=1)
            
            # 构建回归模型的因变量Y:当前列的所有值(含缺失值,后续会分离)
            Y = train_data_X[i]

            # 定位当前列缺失值的行索引(找到需要填充的行)
            # train_data_X[i].isnull():当前列缺失值位置(True),index.tolist()转为索引列表
            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]
            
            # 初始化线性回归模型(默认参数,无正则化)
            regr = LinearRegression()
            
            # 训练模型:用有值样本拟合线性关系(X_train → Y_train)
            regr.fit(X_train, Y_train)
            
            # 预测缺失值:用训练好的模型预测缺失行的取值
            Y_pred = regr.predict(X_test)
            
            # 填充缺失值:将预测值赋值到当前列的缺失位置
            train_data_X.loc[row_numbers_mg_null, i] = Y_pred
            
            # 打印进度:提示当前列填充完成(方便调试)
            print('完成训练数据集中的‘{}’列数据的填充'.format(i))
    
    # 返回填充后的训练集特征 + 原标签(标签无缺失,直接返回)
    return train_data_X, train_data_all.矿物类型


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)
    
    # 统计测试集每列缺失值数量(和训练集逻辑一致)
    null_num = test_data_X.isnull().sum()
   
    # 按缺失值数量升序排序测试集列名(和训练集填充顺序保持一致)
    null_num_sorted = null_num.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:训练集已处理特征列 - 当前列(自变量)
            X_train = train_data_X[filling_feature].drop(i, axis=1)
            # Y_train:训练集当前列的完整值(因变量,无缺失)
            Y_train = train_data_X[i]
            
            # 构建测试集自变量:测试集已处理特征列 - 当前列(用于定位缺失行)
            X_test = test_data_X[filling_feature].drop(i, axis=1)
            
            # 定位测试集当前列缺失值的行索引(需要填充的行)
            row_numbers_mg_null = test_data_X[test_data_X[i].isnull()].index.tolist()
            
            # 分离测试集待预测数据:仅保留缺失行的自变量
            X_test = X_test.iloc[row_numbers_mg_null]

            # 初始化线性回归模型(和训练集一致)
            regr = LinearRegression()
            
            # 训练模型:仅用训练集数据拟合(绝对不用测试集数据!避免数据泄露)
            regr.fit(X_train, Y_train)
            
            # 预测测试集缺失值:用训练好的模型预测
            y_pred = regr.predict(X_test)
            
            # 填充测试集缺失值:将预测值赋值到缺失位置
            test_data_X.loc[row_numbers_mg_null, i] = y_pred
            
            # 打印进度:提示测试集当前列填充完成
            print('完成测试数据集中的‘{}’列数据的填充'.format(i))
    
    # 返回填充后的测试集特征 + 原标签
    return test_data_X, test_data_all.矿物类型

随机森林填充,实现代码如下:

from sklearn.ensemble import RandomForestRegressor

def rf_train_fill(train_data, train_label):
    # 拼接训练集特征和标签(合并为完整DataFrame,统一处理索引)
    # train_data:训练集特征(DataFrame);train_label:训练集标签(矿物类型,Series)
    train_data_all = pd.concat([train_data, train_label], axis=1)
    
    # 重置索引(拼接后可能出现索引重复/不连续,reset_index保证索引从0开始,drop=True删除原索引列)
    train_data_all = train_data_all.reset_index(drop=True)
    
    # 分离纯特征列(剔除标签列“矿物类型”,只保留需要填充的特征数据)
    train_data_X = train_data_all.drop('矿物类型', axis=1)
    
    # 统计每列缺失值数量(isnull()返回布尔矩阵,sum()按列求和得到每列缺失值个数)
    null_num = train_data_X.isnull().sum()
    
    # 按缺失值数量升序排序列名(核心!先填充缺失少的列,后续填充时有更多有效特征)
    # 例:列A缺失10个值,列B缺失50个值 → 先填列A,列A填充后可作为列B的特征
    null_num_sorted = null_num.sort_values(ascending=True)

    # 初始化空列表:记录“已处理(无缺失/已填充)的特征列”,作为后续填充的自变量
    filling_feature = []
    
    # 遍历排序后的列名(按缺失值从少到多)
    for i in null_num_sorted.index:
        # 将当前列加入“已处理特征列表”(无论是否有缺失,都加入,保证后续列能用到)
        filling_feature.append(i)
        
        # 仅处理有缺失值的列(无缺失则跳过,节省计算资源)
        if null_num_sorted[i] != 0:
            # 构建模型自变量X:已处理特征列中剔除当前列(用其他特征预测当前列)
            # filling_feature是累积的已处理列,drop(i)排除当前待填充列
            X = train_data_X[filling_feature].drop(i, axis=1)
            
            # 构建模型因变量Y:当前列的所有值(含缺失值,后续会分离有值/缺失样本)
            Y = train_data_X[i]

            # 定位当前列缺失值的行索引(找到需要填充的行,返回索引列表)
            # train_data_X[i].isnull() → 标记当前列缺失值位置(True),index.tolist()转为索引列表
            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]
            
            # 初始化随机森林回归模型(核心参数说明)
            # n_estimators=100:构建100棵决策树(数量越多越稳定,计算成本越高)
            # random_state=42:固定随机种子,保证结果可复现
            regr = RandomForestRegressor(n_estimators=100, random_state=42)
            
            # 训练模型:用“有值样本”拟合特征和当前列的关系(捕捉非线性)
            regr.fit(X_train, Y_train)
            
            # 预测缺失值:用训练好的模型预测缺失行的取值
            Y_pred = regr.predict(X_test)
            
            # 填充缺失值:将预测值精准赋值到当前列的缺失位置
            train_data_X.loc[row_numbers_mg_null, i] = Y_pred
            
            # 打印进度:方便调试,确认每列是否填充完成
            print('完成训练数据集中的‘{}’列数据的填充'.format(i))
    
    # 返回填充后的训练集特征 + 原标签(标签无缺失,直接返回)
    return train_data_X, train_data_all.矿物类型


def rf_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)
    
    # 重置测试集索引(保证行索引从0开始,避免iloc/loc索引错误)
    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)
    
    # 统计测试集每列缺失值数量(和训练集逻辑一致)
    null_num = test_data_X.isnull().sum()
    
    # 按缺失值数量升序排序测试集列名(和训练集填充顺序保持一致,保证特征累积逻辑相同)
    null_num_sorted = null_num.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:训练集已处理特征列 - 当前列(自变量)
            X_train = train_data_X[filling_feature].drop(i, axis=1)
            # Y_train:训练集当前列的完整值(因变量,无缺失)
            Y_train = train_data_X[i]
            
            # 构建测试集自变量:测试集已处理特征列 - 当前列(用于定位缺失行)
            X_test = test_data_X[filling_feature].drop(i, axis=1)
            
            # 定位测试集当前列缺失值的行索引(需要填充的行)
            row_numbers_mg_null = test_data_X[test_data_X[i].isnull()].index.tolist()
            
            # 分离测试集待预测数据:仅保留缺失行的自变量
            X_test = X_test.iloc[row_numbers_mg_null]

            # 初始化随机森林回归模型(参数和训练集一致,保证模型结构相同)
            regr = RandomForestRegressor(n_estimators=100, random_state=42)
            
            # 训练模型:仅用训练集数据拟合(核心禁忌:绝对不用测试集数据训练)
            regr.fit(X_train, Y_train)
            
            # 预测测试集缺失值:用训练好的模型预测
            y_pred = regr.predict(X_test)
            
            # 填充测试集缺失值:将预测值赋值到缺失位置
            test_data_X.loc[row_numbers_mg_null, i] = y_pred
            
            # 打印进度:提示测试集当前列填充完成
            print('完成测试数据集中的‘{}’列数据的填充'.format(i))
    
    # 返回填充后的测试集特征 + 原标签
    return test_data_X, test_data_all.矿物类型

四、样本均衡化:SMOTE 过采样

矿物数据中不同类型样本的采集难度差异大,易出现样本不均衡(如 A 类占 80%,D 类仅占 5%),导致模型偏向多数类,少数类识别精度极低。本文采用 SMOTE(合成少数类过采样技术)解决该问题。

4.1 SMOTE 算法原理

SMOTE 通过在少数类样本的特征空间中插值生成合成样本,而非简单复制,避免模型过拟合。核心步骤:

  1. 对每个少数类样本,找到其 k 近邻(本文 k=1);
  2. 在样本与近邻之间随机生成新样本;
  3. 重复直至各类样本数量均衡。

4.2 代码实现与效果验证

from imblearn.over_sampling import SMOTE

# 1. 选择缺失值填充策略(以删除缺失值为例)
x_train_fill, y_train_fill = cca_train_fill(x_train_w, y_train_w)
x_test_fill, y_test_fill = cca_test_fill(x_train_fill, y_train_fill, x_test_w, y_test_w)

# 2. SMOTE过采样(仅对训练集处理,测试集保持原始分布)
oversampler = SMOTE(k_neighbors=1, random_state=42)
os_x_train, os_y_train = oversampler.fit_resample(x_train_fill, y_train_fill)

# 3. 可视化样本分布(验证均衡效果)
Y_whole = pd.concat([os_y_train, y_test_fill])
label_count = pd.value_counts(Y_whole)

# 绘制柱状图,标注样本数量
fig, ax = plt.subplots(figsize=(8, 6))
bars = ax.bar(label_count.index, label_count.values)
for bar in bars:
    yval = bar.get_height()
    ax.text(bar.get_x() + bar.get_width() / 2, yval,
            round(yval, 2), va='bottom', ha='center', fontsize=10)
plt.xlabel('labels')
plt.ylabel('number')
plt.show()

# 4. 生成最终训练/测试集并保存
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)

# 保存为Excel文件,便于后续模型训练
data_train.to_excel('训练集数据[删除空数据行].xlsx', index=False)
data_test.to_excel('测试集数据[删除空数据行].xlsx', index=False)

五、总结

本文以矿物识别项目为案例,完整讲解了矿物数据集预处理的核心流程,包括数据清洗、Z 标准化、6 种缺失值填充策略实现、SMOTE 样本均衡化,最终生成可直接用于模型训练的标准化数据集。核心要点总结:

  1. 缺失值处理:需根据缺失率、特征类型选择合适的填充策略,缺失率 > 20% 时优先选择机器学习填充(随机森林),保证特征关联信息不丢失;
  2. 样本均衡化:仅对训练集执行 SMOTE 过采样,测试集保持原始分布,避免模型评估偏差;
  3. 数据隔离:测试集的统计特征(如均值、中位数)必须基于训练集计算,杜绝数据泄露,保证模型泛化能力。

矿物数据预处理是模型训练的基础,合理的预处理流程可使后续模型的识别精度提升 10%-30%。实际应用中需结合业务场景、数据分布特点选择最优策略,而非盲目套用固定流程。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐