矿物分类实战(一):从异常值到标准化——数据清洗全流程拆解
本文基于真实矿物分类项目完整拆解工业级表格数据清洗全流程异常值处理、6种缺失值填充、标准化、SMOTE过采样严格遵循“无数据泄露”原则。所有代码均来自项目源码可直接复用。一、项目背景与数据预处理“黄金法则”1. 业务目标我们有一份矿物检测数据集包含氯、钠、镁、硫等13项化学成分特征目标是将样本划分为A/B/C/D四类矿物。原始数据存在大量非数值异常值如0.01、|、空格、缺失值以及类别不平衡问题。若直接建模模型精度会严重失真因此数据清洗是整个项目的基石。2. 数据预处理的“黄金法则”在动手写代码之前必须明确预处理顺序避免数据泄露测试集信息被提前“偷看”读取原始数据 → 删除无效类别E类 → 处理异常值转数值先划分训练集与测试集绝不能先填充再划分基于训练集的统计量填充训练集和测试集的缺失值基于训练集的均值和标准差对训练集和测试集做标准化仅对训练集做SMOTE过采样测试集保持原始分布保存清洗后的数据集供后续模型训练本文所有代码严格遵循这一流程。二、环境准备与路径管理1. 环境依赖import pandas as pd import matplotlib.pyplot as plt import filldata # 自定义缺失值填充模块后续详解 from pathlib import Path from sklearn.preprocessing import StandardScaler from sklearn.model_selection import train_test_split from imblearn.over_sampling import SMOTE注意filldata是笔者自己创建的python文件其中包含了填充数据的6种方法后续对填充的讲解只是filldatda中的部分代码。代码已经上传读者可直接开箱使用2. 项目路径自动管理pathlib应用BASE_DIR Path(__file__).resolve().parent # 脚本所在目录的绝对路径 data_path BASE_DIR / 矿物数据.xls # 原始数据文件路径 output_dir BASE_DIR / temp_data # 输出目录 output_dir.mkdir(parentsTrue, exist_okTrue) # 创建目录若不存在 细节解析Path(__file__).resolve().parent无论从何处运行脚本都能稳定获取脚本所在目录的绝对路径。mkdir(parentsTrue, exist_okTrue)相当于mkdir -p安全创建目录已存在也不报错。路径拼接使用/运算符自动适配 Windows/Linux/macOS 的路径分隔符。三、数据读取与初步清洗1. 加载数据并删除 E 类data pd.read_excel(data_path) data data[data[矿物类型] ! E] # 删除矿物类型为 E 的行 布尔索引原理data[矿物类型] ! E返回一个布尔型 SeriesTrue/Falsedata[布尔Series]选出所有 True 的行。这一步滤掉了不需要的 E 类样本。2. 拆分特征与标签X_whole data.drop([序号, 矿物类型], axis1) # 特征删除序号和标签列 Y_whole data[矿物类型] # 标签x_whole以及y_whole的展示3. 标签数值编码A/B/C/D → 0/1/2/3label_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矿物类型) 为什么用列表推导式列表推导式简洁高效将字符标签一次性转为数值。最后转回Series并保留列名便于后续合并操作。对y_whole进行标签数值编码后结果如下所示四、异常值处理字符串→数值原始数据中很多特征列混入了0.01、|、空格等非数值内容。使用pd.to_numeric强制转换无法转换的设为NaN为后续缺失值填充做准备。for column_name in X_whole.columns: X_whole[column_name] pd.to_numeric(X_whole[column_name], errorscoerce)errorscoerce的作用该参数使转换失败的字符串变成NaN而不是抛出异常。这一步将所有异常值统一为缺失值便于后续统一处理。进行异常值处理后的x_whole如下所示五、缺失值分析与可视化1. 统计缺失值数量null_num X_whole.isnull() # 每个元素是否为缺失值布尔 null_total null_num.sum() # 每列缺失值个数 print(各特征缺失值数量\n, null_total)2. 绘制缺失值分布图可选plt.rcParams[font.sans-serif] [SimHei] # 支持中文 plt.figure(figsize(12, 6)) null_total.sort_values(ascendingFalse).plot(kindbar, color#1f77b4) plt.title(各特征缺失值数量分布, fontsize14) plt.xlabel(特征名称, fontsize12) plt.ylabel(缺失值数量, fontsize12) plt.grid(axisy, linestyle--, alpha0.3) plt.tight_layout() plt.show()结果展示六、数据集划分关键步骤先切分后填充严格避免测试集信息泄露。x_train, x_test, y_train, y_test train_test_split( X_whole, Y_whole, random_state7 ) x_train x_train.reset_index(dropTrue) y_train y_train.reset_index(dropTrue) x_test x_test.reset_index(dropTrue) y_test y_test.reset_index(dropTrue) 为什么先划分如果先填充再划分填充时可能用到测试集的信息如均值导致测试集数据“污染”评估结果虚高。先划分能保证测试集在预处理阶段完全“不可见”。reset_index(dropTrue)的作用train_test_split切分后子集保留了原数据中的索引可能不连续reset_index(dropTrue)将它们重新变为 0,1,2,... 的连续整数索引避免后续合并或填充时索引错位。七、六种缺失值填充方案详解我们实现了从简单统计填充到机器学习预测填充的6种方案核心设计按矿物类型分组填充因为不同矿物的化学成分分布差异显著全局填充会引入偏差。填充方案对比方案原理适用场景删除空余行删除含缺失值的整行缺失率极低5%、样本量极大均值填充用组内均值填充数据分布正态、无极端异常值中位数填充用组内中位数填充存在极端异常值、偏态分布众数填充用组内众数填充离散型特征线性回归填充用无缺失特征构建线性回归模型预测特征间线性相关性强随机森林填充用无缺失特征构建随机森林模型预测结构化表格数据首选精度最高1. 删除空余行完整案例分析def cca_train_fill(train_data, train_label): data pd.concat([train_data, train_label], axis1) df_data data.dropna() # 删除任何含NaN的行 df_data df_data.reset_index(dropTrue) return df_data.drop(矿物类型, axis1), df_data[矿物类型] def cca_test_fill(train_data, train_label, test_data, test_label): data pd.concat([test_data, test_label], axis1) df_data data.dropna() df_data df_data.reset_index(dropTrue) return df_data.drop(矿物类型, axis1), df_data[矿物类型]注意测试集填充时同样只删除缺失行不引入任何额外信息。2. 类别内均值/中位数/众数填充三种填充代码结构一致仅统计方法不同。以均值填充为例训练集填充def mean_train_method(data): fill_value data.mean() # 计算每列均值 return data.fillna(fill_value) def mean_train_fill(train_data, train_label): data pd.concat([train_data, train_label], axis1).reset_index(dropTrue) # 按矿物类型分组0,1,2,3 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], axis0).reset_index(dropTrue) return df_filled.drop(矿物类型, axis1), df_filled[矿物类型]测试集填充复用训练集各组均值def mean_test_method(train_data, test_data): fill_value train_data.mean() # 使用训练集的均值 return test_data.fillna(fill_value) def mean_test_fill(train_data, train_label, test_data, test_label): # 分组分别用对应组的训练集均值填充测试集 ... # 结构同训练集但填充值来自训练集 为什么测试集必须用训练集的统计量如果测试集用自己的均值填充就相当于“提前看到了”测试集的分布评估结果会虚高。正确做法是测试集永远只使用训练集学到的参数均值、标准差、众数、回归模型。众数填充的applylambda详解fill_value data.apply(lambda x: x.mode().iloc[0] if len(x.mode()) 0 else None)x.mode()返回该列众数可能有多个值返回 Series.iloc[0]取第一个众数如果列全为空mode()返回空 Serieslen()为 0返回None3. 线性回归与随机森林预测填充这两种方法属于机器学习填充核心思想缺失值少的列先填充填充后作为特征去预测缺失值多的列形成迭代填充。训练集填充流程以线性回归为例def lr_train_fill(train_data, train_label): data pd.concat([train_data, train_label], axis1).reset_index(dropTrue) train_data data.drop(矿物类型, axis1) # 按缺失值数量从小到大排序 null_num train_data.isnull().sum() null_num_sorted null_num.sort_values(ascendingTrue) filling_feature [] # 记录已填充的特征 for i in null_num_sorted.index: filling_feature.append(i) if null_num_sorted[i] ! 0: # 用已填充的特征作为 X当前列作为 y X train_data[filling_feature].drop(i, axis1) y train_data[i] # 缺失值所在行 row_numbers train_data[train_data[i].isnull()].index.tolist() x_train X.drop(row_numbers) y_train y.drop(row_numbers) x_test X.iloc[row_numbers] # 训练模型预测 lr LinearRegression() lr.fit(x_train, y_train) train_data.loc[row_numbers, i] lr.predict(x_test) return train_data, data[矿物类型] 为什么按缺失值数量排序填充缺失值少的列更容易被准确预测填充后成为“可靠特征”再去帮助预测缺失值多的列整体精度更高。测试集填充复用训练集模型def lr_test_fill(train_data, train_label, test_data, test_label): # 使用训练集训练好的模型或重新用训练集数据训练预测测试集缺失值 # 关键只使用训练集数据绝不涉及测试集自身 ... # 若没有特征可用则用训练集均值兜底 if X.shape[1] 0: fill_val y.mean() test_data.loc[row_numbers, i] fill_val continue随机森林填充代码结构完全相同仅将模型换成RandomForestRegressor并可调节参数如n_estimators100, max_depth20。八、标准化Z-Score化学成分量纲差异极大氯含量可达数十万pH值仅个位数基于距离的模型SVM、逻辑回归对此敏感必须标准化。scaler StandardScaler() # 训练集fittransform x_train_scaled scaler.fit_transform(x_train_fill) # 测试集仅transform复用训练集的均值和标准差 x_test_scaled scaler.transform(x_test_fill) # 转回DataFrame保留列名 x_train_scaled pd.DataFrame(x_train_scaled, columnsx_train_fill.columns) x_test_scaled pd.DataFrame(x_test_scaled, columnsx_test_fill.columns) 为什么训练集用fit_transform测试集只用transformfit计算训练集的均值和标准差transform应用转换。测试集必须使用训练集的参数否则相当于引入了测试集信息违背“无数据泄露”原则。九、类别不平衡优化SMOTE过采样矿物样本各类别数量不均少数类样本太少会导致模型偏向多数类。我们使用SMOTE合成少数类过采样技术对训练集进行过采样生成合成样本平衡类别分布。测试集绝不做任何采样。from imblearn.over_sampling import SMOTE oversample SMOTE(k_neighbors1, random_state0) os_x_train, os_y_train oversample.fit_resample(x_train_scaled, y_train_fill) SMOTE 原理SMOTE 不是简单复制少数类而是在少数类样本之间“插值”生成新样本。k_neighbors1表示每个样本只与最近的一个邻居合成新样本避免生成的样本过于分散适用于数据量较小的情况。十、最终数据保存将清洗后的训练集和测试集保存为 Excel 文件供后续模型训练使用。# 训练集合并标签与特征打乱顺序避免模型学习原始顺序 data_train pd.concat([os_y_train, os_x_train], axis1).sample(frac1, random_state0) # 测试集合并不打乱便于后续评估 data_test pd.concat([y_test_fill, x_test_scaled], axis1) data_train.to_excel(output_dir / 训练数据集[lr填充].xlsx, indexFalse) data_test.to_excel(output_dir / 测试数据集[lr填充].xlsx, indexFalse)十一、总结经过以上全流程处理我们得到了✅ 无缺失值根据业务选择最优填充方法本例使用线性回归填充✅ 无量纲差异Z-Score 标准化✅ 训练集类别完全平衡SMOTE 过采样✅ 整个过程严格避免测试集信息泄露下一篇文章我们将基于这份清洗好的数据训练 6 种传统机器学习模型逻辑回归、随机森林、SVM、XGBoost、高斯贝叶斯、AdaBoost并通过网格搜索调优对比它们的分类效果。欢迎继续关注附全部代码已开源随系列文章逐步放出如果你在复现过程中遇到任何问题欢迎在评论区留言我们一起探讨。