机器学习8:矿物种类检测——数据预处理
·
前言
在机器学习项目中,数据预处理往往占据整个工作流70%以上的时间。一个高质量的数据集是模型成功的基础,而数据预处理正是构建这个基础的关键环节。本文以一个矿物种类分类任务为例,完整展示从原始数据到可用于建模的标准数据集的全流程,涵盖数据清洗、缺失值处理、标签编码、数据标准化、类别不平衡处理等核心步骤。
一、项目背景
1.1 任务目标
根据矿物的多种物理/化学特征(如密度、硬度、化学成分等),预测其所属类别(A/B/C/D四类)。这是一个典型的多分类问题,适用于地质勘探、矿物鉴定等实际应用场景。
1.2 原始数据说明
- 数据文件:
矿物数据.xls(Excel格式) - 特征列:多个数值型特征(具体列名略)
- 标签列:
矿物类型,取值为 A、B、C、D、E - 存在问题:
- 存在异常类别
E(需删除) - 存在大量缺失值(需填充)
- 类别分布可能不平衡(需过采样)
- 数据类型可能不一致(需统一)
- 存在异常类别
二、数据预处理
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.preprocessing import LabelEncoder
from imblearn.over_sampling import SMOTE
import warnings
warnings.filterwarnings('ignore')
2.1 读取数据
# 读取Excel文件
data = pd.read_excel("矿物数据.xls")
print(f"原始数据形状: {data.shape}")
print(f"数据预览:\n{data.head()}")
print(f"列名: {data.columns.tolist()}")
2.2 删除异常类别
原始数据中存在类别 E,属于异常数据或噪声数据,直接删除:
# 删除类别为E的行
data = data[data['矿物类型'] != 'E']
print(f"删除异常类别后数据形状: {data.shape}")
print(f"类别分布:\n{data['矿物类型'].value_counts()}")
2.3 检查缺失情况
# 统计缺失值
null_num = data.isnull()
null_total = null_num.sum()
null_percent = (null_total / len(data)) * 100
print(f"原始数据集总缺失单元格数:{null_total.sum()}")
print("\n各列缺失情况:")
for col in data.columns:
if null_total[col] > 0:
print(f"{col}: {null_total[col]}个缺失 ({null_percent[col]:.2f}%)")
2.3 特征与标签分离
# 剔除序号列(无意义)和标签列
X_whole = data.drop(['矿物类型', '序号'], axis=1)
y_whole = data['矿物类型']
print(f"特征矩阵形状: {X_whole.shape}")
print(f"标签向量形状: {y_whole.shape}")
2.4 标签编码
将文字标签 A/B/C/D 转换为数值 0/1/2/3,便于模型处理:
# 方法1:使用字典映射
label_dict = {"A": 0, "B": 1, "C": 2, "D": 3}
encoded_labels = [label_dict[label] for label in y_whole]
y_whole = pd.Series(encoded_labels, name='矿物类型')
# 方法2:使用LabelEncoder(更通用)
# from sklearn.preprocessing import LabelEncoder
# le = LabelEncoder()
# y_whole = pd.Series(le.fit_transform(y_whole), name='矿物类型')
print(f"编码后标签分布:\n{y_whole.value_counts()}")
2.5 数据类型统一
确保所有特征列为数值类型,非数值内容转为 NaN:
# 统一转换为数值类型
for column_name in X_whole.columns:
X_whole[column_name] = pd.to_numeric(X_whole[column_name], errors='coerce')
2.6 划分训练集与测试集
# 划分数据集(先划分再填充,避免数据泄露)
x_train, x_test, y_train, y_test = train_test_split(
X_whole, y_whole, test_size=0.2, random_state=42, stratify=y_whole
)
print(f"训练集形状: {x_train.shape}")
print(f"测试集形状: {x_test.shape}")
print(f"训练集标签分布:\n{y_train.value_counts()}")
print(f"测试集标签分布:\n{y_test.value_counts()}")
三、缺失值填充(核心环节)
3.1 填充策略说明
本项目实现了三种填充策略,并以中位数填充作为最终方案:
| 填充方法 | 训练集策略 | 测试集策略 | 优点 | 缺点 |
|---|---|---|---|---|
| 均值填充 | 按类别分组,用组内均值填充 | 用训练集同类均值填充 | 简单快速 | 对异常值敏感 |
| 众数填充 | 按类别分组,用组内众数填充 | 用训练集同类众数填充 | 适合分类特征 | 可能不适用于连续特征 |
| 中位数填充 | 按类别分组,用组内中位数填充 | 用训练集同类中位数填充 | 对异常值稳健 | 可能忽略数据分布 |
3.2 中位数填充代码实现
训练集填充函数(分组中位数):
def median_method(data):
"""按列计算中位数并填充缺失值"""
fill_values = data.apply(lambda x: x.median() if not x.isna().all() else None)
return data.fillna(fill_values)
def median_train_fill(train_data, train_label):
"""训练集分组中位数填充"""
# 合并特征和标签
data = pd.concat([train_data, train_label], axis=1)
# 按类别分组
A = data[data['矿物类型'] == 0]
B = data[data['矿物类型'] == 1]
C = data[data['矿物类型'] == 2]
D = data[data['矿物类型'] == 3]
# 分别填充
A = median_method(A)
B = median_method(B)
C = median_method(C)
D = median_method(D)
# 合并结果
df_filled = pd.concat([A, B, C, D]).reset_index(drop=True)
# 分离特征和标签
return df_filled.drop('矿物类型', axis=1), df_filled['矿物类型']
测试集填充函数(使用训练集统计量填充):
def median_test_fill(train_data, test_data, train_label, test_label):
"""测试集使用训练集统计量填充"""
# 合并训练集特征和标签
train_combined = pd.concat([train_data, train_label], axis=1)
# 计算每个类别的中位数
median_by_class = {}
for class_val in [0, 1, 2, 3]:
class_data = train_combined[train_combined['矿物类型'] == class_val]
median_by_class[class_val] = class_data.median()
# 合并测试集特征和标签
test_combined = pd.concat([test_data, test_label], axis=1)
test_filled = test_combined.copy()
# 按类别填充测试集
for class_val in [0, 1, 2, 3]:
class_mask = test_filled['矿物类型'] == class_val
if class_mask.any():
test_filled.loc[class_mask] = test_filled.loc[class_mask].fillna(median_by_class[class_val])
# 分离特征和标签
return test_filled.drop('矿物类型', axis=1), test_filled['矿物类型']
3.3 执行填充
# 训练集填充
x_train_fill, y_train_fill = median_train_fill(x_train, y_train)
# 测试集填充
x_test_fill, y_test_fill = median_test_fill(x_train, x_test, y_train, y_test)
print(f"填充后训练集缺失值: {x_train_fill.isnull().sum().sum()}")
print(f"填充后测试集缺失值: {x_test_fill.isnull().sum().sum()}")
四、数据标准化(Z-Score)
标准化使各特征处于同一量纲,避免大数值特征主导模型训练:
# 创建标准化器
scaler = StandardScaler()
# 训练集:fit + transform
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, columns=x_train_fill.columns)
x_test_scaled = pd.DataFrame(x_test_scaled, columns=x_test_fill.columns)
print("标准化后训练集统计:")
print(f"均值: {x_train_scaled.mean().values[:5]}...")
print(f"标准差: {x_train_scaled.std().values[:5]}...")
五、处理类别不平衡(SMOTE过采样)
使用 SMOTE(合成少数类过采样技术)对训练集进行平衡处理:
# 检查过采样前的类别分布
print("过采样前训练集类别分布:")
print(y_train_fill.value_counts())
# 应用SMOTE
smote = SMOTE(random_state=42, k_neighbors=5)
x_train_bal, y_train_bal = smote.fit_resample(x_train_scaled, y_train_fill)
print("\n过采样后训练集类别分布:")
print(y_train_bal.value_counts())
print(f"过采样后训练集形状: {x_train_bal.shape}")
# ⚠️ 重要:只对训练集进行过采样,测试集保持原样
print(f"测试集保持原样,形状: {x_test_scaled.shape}")
六、导出最终数据集
将处理后的数据保存为 Excel 文件,便于后续建模使用:
# 保存特征列名
feature_cols = x_train_fill.columns.tolist()
# 训练集
df_train = pd.DataFrame(x_train_bal, columns=feature_cols)
df_train["矿物类型"] = y_train_bal.reset_index(drop=True)
# 测试集
df_test = pd.DataFrame(x_test_scaled, columns=feature_cols)
df_test["矿物类型"] = y_test_fill.reset_index(drop=True)
# 保存到Excel
df_train.to_excel("中位数填充训练集.xlsx", index=False)
df_test.to_excel("中位数填充测试集.xlsx", index=False)
print("数据集已保存:")
print(f"- 训练集: 中位数填充训练集.xlsx ({df_train.shape})")
print(f"- 测试集: 中位数填充测试集.xlsx ({df_test.shape})")
七、fill_data.py
fill_data.py 模块包含的函数:
| 函数名 | 功能 | 适用场景 |
|---|---|---|
cca_train_fill / cca_test_fill | 删除含缺失值的行(完整案例分析) | 缺失值很少时 |
mean_train_fill / mean_test_fill | 分组均值填充 | 数据分布近似正态 |
mode_train_fill / mode_test_fill | 分组众数填充 | 分类特征或离散值 |
median_train_fill / median_test_fill | 分组中位数填充(最终方案) | 存在异常值或偏态分布 |
八、总结与思考
本项目的关键知识点
- 数据清洗:删除异常类别、处理非数值数据
- 缺失值处理:分组填充优于全局填充,因为不同类别的特征分布可能不同
- 数据泄露防范:
- 测试集使用训练集的统计量(均值/中位数/众数)进行填充
- 标准化时 fit 训练集,transform 测试集
- SMOTE 只应用于训练集
- 类别不平衡:SMOTE 能有效提升少数类的识别率
更多推荐

所有评论(0)