前言

在机器学习项目中,数据预处理往往占据整个工作流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分组中位数填充(最终方案)存在异常值或偏态分布

八、总结与思考

本项目的关键知识点

  1. 数据清洗:删除异常类别、处理非数值数据
  2. 缺失值处理:分组填充优于全局填充,因为不同类别的特征分布可能不同
  3. 数据泄露防范:
    • 测试集使用训练集的统计量(均值/中位数/众数)进行填充
    • 标准化时 fit 训练集,transform 测试集
    • SMOTE 只应用于训练集
  4. 类别不平衡:SMOTE 能有效提升少数类的识别率

更多推荐