前言
在矿物分类任务中,原始数据往往存在缺失值、异常字符等问题。本文记录一次完整的数据预处理流程,重点讲解缺失值填充的两种基础方法:完整行删除(CCA)和均值填充(按类别)。后续文章会继续介绍中位数、众数填充以及更高级的模型填充方法(线性回归、随机森林)。
代码使用 Python 的 pandas 和 scikit-learn,保持原样,仅最后保存 CSV 的文件名可自行调整。
一、数据准备与基础处理
1.先导入必要的库
python
import pandas as pd
import matplotlib.pyplot as plt
import fill_data
2.读取数据并做简单清洗
python
data = pd.read_excel("矿物数据.xls")
data = data[data['矿物类型'] != 'E'] # 删除仅1条的类别E
3.检查缺失值
data.isnull() 返回一张布尔表格:
- 缺失值位置 →
True - 有数据位置 →
False
sum()默认按列求和 ,True=1,False=0
python
#统计每列空缺值数量
null_num = data.isnull()
null_total = null_num.sum()
4.提取特征和标签,并将标签编码为数字
python
X_whole = data.drop('矿物类型', axis=1).drop('序号', axis=1)
y_whole = data.矿物类型
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='矿物类型')
5.将特征列中的非数值内容(如 `|`、空格、字符串数字)转为数值,无法转换的置为 NaN
pd.to_numeric()函数将参数中的数据转换为数值类型。如果转换失败,它会引发一个异常,
设置errors="coerce",会将无法转换的值设置为NaN
python
for column_name in X_whole.columns:
X_whole[column_name] = pd.to_numeric(X_whole[column_name], errors='coerce')
6.标准化(Z-score)使特征均值为0、方差为1
Z标准化处理后为umpy数据,这里再转换成pondas数据
python
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.划分训练集和测试集(70%训练,30%测试)
python
from sklearn.model_selection import train_test_split
x_train, x_test, y_train, y_test = train_test_split(X_whole, y_whole, test_size=0.3, random_state=10)
二、缺失值填充的思路
数据中存在 NaN 时,直接送入模型会报错或导致结果偏差。填充缺失值的目标是:用合理的估计值代替缺失位置,尽量保留数据信息,同时避免引入过大噪声。
常见的填充策略有:
-
删除法:直接删除含有缺失值的行或列(如 CCA)。
-
统计量填充:用均值、中位数、众数等代替。
-
模型预测填充:用其他特征建立模型预测缺失值(如回归、随机森林)。
对于分类问题,若不同类别的数据分布差异较大,按类别分别计算统计量往往比全局统计量更合理。例如,矿物类型 A 的某特征均值可能远高于类型 B,若用全局均值填充所有缺失值,会模糊类别间的差异。因此,本文实现的均值填充是按矿物类型分组进行的。
三、完整行分析(CCA)
思路
CCA 是最简单的处理方式:只保留没有任何缺失值的样本。这样做的好处是无需估计,完全保留原始信息;缺点是当缺失比例较高时,会丢弃大量数据,可能导致模型训练不充分。
代码实现
fill_data模块中定义了训练集和测试集的 CCA 填充函数:
python
def cca_train_fill(train_data, train_label):
'''CCA(Complete Case Analysis)只考虑包含完整数据的行'''
data = pd.concat([train_data, train_label], axis=1)
#用于重囂索引的。当你对数据进行工排序、筛选或其他操作后案引可能会变得不连续或混乱。
data = data.reset_index(drop=True)
#用于删除(或过滤掉)包含缺失值(NaN)的行或列。pandas里面有大量和数据清洗相关的函数
df_filled = data.dropna()
return df_filled.drop('矿物类型', axis=1), df_filled.矿物类型
def cca_test_fill(train_data, train_label, test_data, test_label):
'''CCA测试集填充:直接删除含缺失值的行(与训练集无关)'''
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.矿物类型
注意:训练集和测试集的删除是独立的,即分别删除各自含缺失值的行。这样做可能导致训练集和测试集样本数不同,但各自内部都是完整数据,可以直接用于模型。
适用场景
-
缺失值很少(例如 <5%),删除后不影响样本量。
-
数据量本身很大,即使删除一部分也足够训练。
四、均值填充(按类别)
思路
对于每个特征,计算同一矿物类型下该特征的非缺失值的平均值,然后用该平均值填充该类别样本的缺失位置。这样既利用了类别信息,又避免了全局均值可能带来的偏差。
例如:对于矿物类型 A,若特征"硬度"有缺失,则用所有 A 类样本"硬度"的平均值填充;类型 B 的缺失则用 B 类样本的平均值填充,以此类推。
代码实现
首先定义一个辅助函数 'mean_method',对单个 DataFrame 用每列均值填充:
python
def mean_method(train_data):
'''数据集中的空值使用每列的均值替代'''
fill_values = train_data.mean()
return train_data.fillna(fill_values)
然后分别处理训练集和测试集。训练集:按类别拆分,每个子集独立填充,再合并:
python
def mean_train_fill(train_data, train_label):
data = pd.concat([train_data, train_label], axis=1)
data = data.reset_index(drop=True)
A = data[data['矿物类型'] == 0]
B = data[data['矿物类型'] == 1]
C = data[data['矿物类型'] == 2]
D = data[data['矿物类型'] == 3]
A = mean_method(A)
B = mean_method(B)
C = mean_method(C)
D = mean_method(D)
df_filled = pd.concat([A, B, C, D])
df_filled = df_filled.reset_index(drop=True)
return df_filled.drop('矿物类型', axis=1), df_filled.矿物类型
测试集:为了避免数据泄露,必须使用训练集计算出的均值来填充测试集,而不是用测试集自身的均值。因此'mean_test_fill'函数的设计如下:
python
def mean_test_method(train_data, test_data):
'''用训练集的均值填充测试集'''
fill_values = train_data.mean()
return test_data.fillna(fill_values)
def mean_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)
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 = mean_test_method(A_train, A_test)
B = mean_test_method(B_train, B_test)
C = mean_test_method(C_train, C_test)
D = mean_test_method(D_train, D_test)
df_filled = pd.concat([A, B, C, D])
df_filled = df_filled.reset_index(drop=True)
return df_filled.drop('矿物类型', axis=1), df_filled.矿物类型
优点与局限
优点:
-
简单快速,容易实现。
-
保留了样本数量,不会丢失数据。
-
按类别填充更符合数据分布。
局限:
-
均值对异常值敏感,若某类别中存在极端值,均值可能偏离真实中心。
-
忽略了特征之间的相关性,填充值可能不够精确。
五、中位数、众数填充
中位数填充与均值填充思路完全一致,只需将 'mean()' 替换为 'median()';众数填充则使用 'mode()'并取第一个众数值。
python
def mode_test_method(train_data, test_data):
'''数据集中的空值使用每列的众数替代'''
fill_values =train_data.apply(lambda x: x.mode().iloc[0] if len(x.mode()) > 0 else None)
return test_data.fillna(fill_values) # 使用众数填充缺失值
六、线性回归填充
思路
线性回归假设特征与目标之间存在线性关系。在填充某个特征时,我们把该特征作为目标变量 y,其他已处理过的完整特征作为输入 X。用非缺失行训练模型,然后预测缺失行的目标值。由于我们按缺失数量从小到大依次处理,所以当处理到某个特征时,前面处理过的特征都已经填充完整,可以作为输入特征使用,这就是迭代填充的核心。
训练集代码
python
def lr_train_fill(train_data, train_label):
# 合并特征和标签
train_data_all = pd.concat([train_data, train_label], axis=1)
train_data_all = train_data_all.reset_index(drop=True)
train_data_X = train_data_all.drop('矿物类型', axis=1)
# 统计每个特征的缺失值数量,并排序
null_num = train_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: # 如果该特征有缺失,才进行填充
# 用 filling_feature 中除当前特征外的所有特征作为输入
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]
# 训练线性回归模型
regr = LinearRegression()
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.矿物类型
关键点:
-
filling_feature列表会依次加入所有特征名。当处理第一个有缺失的特征时,列表中已经包含了所有缺失数量比它少(包括无缺失)的特征,这些特征都是完整的,可以作为输入。处理第二个有缺失的特征时,第一个缺失特征已经被填充,也变成了完整特征,所以也能作为输入。这样实现了迭代填充。 -
输入特征中不包含当前待填充的特征,避免自我预测。
-
训练集和测试集的划分基于当前特征是否缺失,而不是随机划分。
测试集代码
python
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 = train_data_X[filling_feature].drop(i, axis=1)
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.矿物类型
注意 :这里训练模型时使用的
X_train来自已经填充好的训练集,而不是测试集本身,保证了填充的客观性。
七、随机森林填充
思路
随机森林是一种集成学习算法,由多棵决策树组成,能够捕捉特征间的非线性关系,对异常值和噪声更稳健。其填充思路与线性回归完全一致:同样采用迭代填充,按缺失数量从小到大依次处理每个特征,用其他完整特征训练随机森林回归模型,预测当前特征的缺失值。区别仅在于回归器的选择------用 RandomForestRegressor 替代 LinearRegression。
python
def rf_train_fill(train_data, train_label):
# 与 lr_train_fill 几乎相同,仅模型不同
...
regr = RandomForestRegressor(n_estimators=100, random_state=42)
regr.fit(X_train, y_train)
y_pred = regr.predict(X_test)
...
测试集填充同样如此。这里不再重复贴出全部代码,但可以在实际项目中参考上面的线性回归版本,将 LinearRegression() 替换为 RandomForestRegressor(n_estimators=100, random_state=42) 即可。
八、调用函数
python
#1.完整行数据
cca_x_train,cca_y_train=fill_data.cca_train_fill(x_train,y_train)
cca_x_test,cca_y_test=fill_data.cca_test_fill(x_train,y_train,x_test,y_test)
注意:后面的改一下即可,但是最后一个随机森林不行,他要借助训练集填充过的数据集进行测试集的训练
python
#6.随机森林
rf_x_train,rf_y_train=fill_data.rf_train_fill(x_train,y_train)
rf_x_test,rf_y_test=fill_data.rf_test_fill(rf_x_train,rf_y_train,x_test,y_test)
九、保存csv文件
均值填充后的数据保存以下代码展示了均值填充完成后,将训练集和测试集合并并导出为 CSV 文件
python
# 训练集:特征 + 标签横向合并
df_train = pd.concat([cca_x_train, cca_y_train], axis=1)
# 测试集:特征 + 标签横向合并
df_test = pd.concat([cca_x_test, cca_y_test], axis=1)
# 保存csv,utf-8-sig防止Excel中文乱码,index=False不输出行号
df_train.to_csv("data/训练数据集_均值填充.csv", index=False, encoding="utf-8-sig")
df_test.to_csv("data/测试数据集_均值填充.csv", index=False, encoding="utf-8-sig")
文件名可以根据需要修改,例如 "data/train_rf_filled.csv"。