文章目录
- 一、项目背景
- 二、环境准备与依赖
- 三、数据集说明
- 四、整体流程
- 五、数据预处理主脚本详解
- 六、缺失值填充函数库 — 六种方法详解
-
- 6.1 删除空行填充(Complete Case Analysis, CCA)
- 6.2 平均值 / 中位数填充
- 6.3 众数填充
- 6.4 线性回归填充 & 随机森林填充(模型预测填充)
- 七、训练与评估主脚本详解
-
- 7.1 六个分类模型
- 7.2 评估指标提取
- 八、总结
一、项目背景
在实际的机器学习项目中,数据缺失(Missing Value) 是最常见也最头疼的问题之一。原始数据里往往存在大量空值,如果直接丢给模型,要么报错、要么导致训练效果极差。
本项目以一份矿物数据为例,完整演示了一条经典的分类建模流水线:
整个项目由三个核心脚本构成,各司其职:
| 数据预处理主脚本 | 数据预处理主函数:读数据、编码、标准化、划分、填充、过采样、保存 |
| 填充函数库 | 填充函数库:实现 6 种缺失值填充方法(含训练集 / 测试集两套函数) |
| 训练与评估主脚本 | 训练主函数:对 6 种填充结果分别训练 6 个模型并输出评估指标 |
二、环境准备与依赖
pip install pandas numpy scikit-learn imbalanced-learn xgboost matplotlib openpyxl xlrd
| pandas | 数据读取与处理 |
| numpy | 数值计算(被 sklearn 依赖) |
| scikit-learn | 数据划分、标准化、模型训练、评估 |
| imbalanced-learn | SMOTE 过采样(处理类别不平衡) |
| xgboost | XGBoost 分类模型 |
| matplotlib | 可视化(本项目仅注释代码中用到) |
| xlrd | 读取旧版 .xls 格式 Excel |
| openpyxl | 保存 .xlsx 文件 |
注意:.xls 是 Excel 97-2003 旧格式,pandas.read_excel 读取它需要 xlrd(1.x/2.x 均可,新版 xlrd 只支持 xls 不支持 xlsx,因此保存用 openpyxl 引擎)。
三、数据集说明
数据集为 矿物数据.xls,共 1043 行 × 15 列(含表头),其中 14 个特征、1 个标签列。
| 序号 | 样本编号(非特征,建模时剔除) |
| 氯、钠、镁、硫、钙、钾、碳、溴、锶、pH、硼、氟、硒 | 13 个矿物指标特征 |
| 矿物类型 | 标签列,取值 A/B/C/D/E |

类别分布(原始):
| 样本数 | 544 | 367 | 84 | 47 | 1 |
可以看到:
- 类别分布极不均衡,D 类仅 47 条,E 类只有 1 条;
- 因此预处理脚本首先剔除 E 类(样本太少没有建模价值),最终用 A/B/C/D 四类共 1042 条数据建模。
缺失值情况(剔除 E 后):
| 硫 | 886 | 溴 | 0 |
| 锶 | 748 | 氯 | 0 |
| 氟 | 150 | 钠 | 0 |
| 钙 | 137 | 硼 | 0 |
| 硒 | 131 | 序号 | 0 |
| 钾 | 65 | 矿物类型 | 0 |
| pH | 21 | ||
| 碳 | 2 | ||
| 镁 | 1 |
样本量大幅减少,其中「硫」「锶」两列缺失率超过 70%,是填充的重灾区,也是「删除空行」策略失效的根本原因。
四、整体流程
读取 Excel(剔除E类)
↓
标签编码 A/B/C/D → 0/1/2/3
↓
特征强制转数值(非法值→NaN)
↓
Z-score 标准化
↓
划分训练集 / 测试集(7:3)
↓
6 种填充方法(train / test 分别处理)
↓
SMOTE 过采样(仅训练集)
↓
保存填充后的训练 / 测试数据集
↓
6 个分类模型 × 6 种填充 → 评估对比
五、数据预处理主脚本详解
该脚本是整条流水线的「指挥中心」。核心逻辑如下:
import os
import pandas as pd
import fill_data
# 1. 读取 excel 数据,剔除类别 E
data = pd.read_excel('矿物数据.xls')
data = data[data['矿物类型'] != 'E']
# 2. 统计各列缺失值(用于观察数据质量)
null_total = data.isnull().sum()
# 3. 划分特征与标签
X_whole = data.drop('矿物类型', axis=1).drop('序号', axis=1)
y_whole = data['矿物类型']
# 4. 标签编码 A/B/C/D → 0/1/2/3
label_dict = {'A': 0, 'B': 1, 'C': 2, 'D': 3}
y_whole = pd.Series([label_dict[label] for label in y_whole], name='矿物类型')
# 5. 强制转数值,非法内容置 NaN(统一数据格式,方便后续填充)
for column_name in X_whole.columns:
X_whole[column_name] = pd.to_numeric(X_whole[column_name], errors='coerce')
# 6. Z-score 标准化
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. 划分训练集 / 测试集
from sklearn.model_selection import train_test_split
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=1000)
# 8. 选择一种填充方法(下面以随机森林填充为例)
x_train_fill, y_train_fill = fill_data.rf_train_fill(x_train_w, y_train_w)
x_test_fill, y_test_fill = fill_data.rf_test_fill(x_train_fill, y_train_fill, x_test_w, y_test_w)
# 9. SMOTE 过采样(缓解类别不平衡,仅针对训练集)
from imblearn.over_sampling import SMOTE
smote = SMOTE(k_neighbors=2, random_state=1000)
os_x_train, os_y_train = smote.fit_resample(x_train_fill, y_train_fill)
# 10. 保存训练 / 测试数据集(标签列放第一列)
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)
os.makedirs('.//temp_data', exist_ok=True) # 先创建目录再保存
data_train.to_excel(r'.//temp_data//6_训练数据集_随机森林填充.xlsx', index=False)
data_test.to_excel(r'.//temp_data//6_测试数据集_随机森林填充.xlsx', index=False)
项目里其它 5 种填充方法只需把第 8 步换成 fill_data.cca/mean/median/mode/lr_train_fill 等函数,并把保存的文件名改为 1~5 对应序号即可(代码中已经用注释保留)。
六、缺失值填充函数库 — 六种方法详解
填充函数库提供了每个方法的 _train_fill(训练集) 与 _test_fill(测试集) 两个函数
6.1 删除空行填充(Complete Case Analysis, CCA)
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.矿物类型
- 原理:直接把含缺失值的整行删除,只保留完整样本。
- 优点:简单粗暴、不引入人为估计的偏差。
- 致命缺点:本数据「硫」列缺失 886 行,删除后训练集 729 → 64 行、测试集 313 → 28 行,丢失了约 90% 的数据,且测试集中类别 2 直接消失。此时训练出的模型基本没有参考价值,也不满足「类别齐全」的对比前提。
6.2 平均值 / 中位数填充
def mean_method(data):
fill_values = data.mean() # 求每列均值
return data.fillna(fill_values) # 用均值填充缺失
def median_method(data):
fill_values = data.median() # 求每列中位数
return data.fillna(fill_values)
- 训练集填充时按类别分组,用「同类别样本」的均值/中位数填充——因为不同类别矿物的指标分布差异大,按类填充更合理:
def mean_train_fill(train_data, train_label):
data = pd.concat([train_data, train_label], axis=1).reset_index(drop=True)
A = mean_method(data[data['矿物类型'] == 0]) # 仅用 0 类样本的均值填 0 类缺失
B = mean_method(data[data['矿物类型'] == 1])
C = mean_method(data[data['矿物类型'] == 2])
D = mean_method(data[data['矿物类型'] == 3])
df_filled = pd.concat([A, B, C, D]).reset_index(drop=True)
return df_filled.drop('矿物类型', axis=1), df_filled.矿物类型
- 测试集填充防数据泄露:测试集的填充值必须来自训练集,绝不能用测试集自身统计量,否则相当于把「答案」泄露给模型:
def mean_test_method(train_data, test_data):
fill_values = train_data.mean() # 用训练集均值
return test_data.fillna(fill_values) # 填充测试集
- 均值 vs 中位数:均值对异常值敏感,数据存在离群点时中位数更稳健。
6.3 众数填充
def mode_method(data):
# 对每列求众数(出现次数最多的值),取第一个;空列返回 None
fill_values = data.apply(lambda x: x.mode().iloc[0] if len(x.mode()) > 0 else None)
return data.fillna(fill_values)
- 原理:用该列出现频率最高的值填充。
- 适用场景:适合分类/离散型特征;对连续型特征意义不大。
- 小瑕疵:x.mode() 被重复计算两次,且 a = data.mode() 是冗余的无效变量(见第八节优化)。
6.4 线性回归填充 & 随机森林填充(模型预测填充)
这是两种基于回归模型的高级填充法,思想一致:
把含缺失的特征当作「目标变量」,用其它无缺失的特征训练回归模型,预测缺失位置的值。
def rf_train_fill(train_data, train_label):
train_data_all = pd.concat([train_data, train_label], axis=1).reset_index(drop=True)
train_data_X = train_data_all.drop('矿物类型', axis=1)
null_num_sorted = train_data_X.isnull().sum().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_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] # 缺失行 → 待预测
rf = RandomForestRegressor(n_estimators=100, random_state=42)
rf.fit(X_train, y_train)
y_pred = rf.predict(X_test)
train_data_X.loc[row_numbers_mg_null, i] = y_pred # 回填预测值
print(f'完成训练集中的“{i}”列数据的填充')
return train_data_X, train_data_all.矿物类型
算法要点:
线性回归 vs 随机森林:前者假设特征间是线性关系、快但欠拟合复杂关系;后者用集成树、能捕捉非线性、精度更高但更慢。所以线性回归填充用 LinearRegression,随机森林填充用 RandomForestRegressor。
测试集函数:lr_test_fill / rf_test_fill 的思路是——用「已填充好的训练集特征」训练模型,去预测「测试集缺失列」,保证测试集不参与模型拟合(无数据泄露)。
七、训练与评估主脚本详解
该脚本对 6 种填充结果逐一进行同样的建模评估。核心结构如下:
import pandas as pd
from sklearn import metrics
# 以平均值填充为例:读取填充后的数据
train_data = pd.read_excel(r'.//temp_data//2_训练数据集_平均值填充.xlsx')
train_data_x = train_data.iloc[:, 1:] # 第 2 列起为特征
train_data_y = train_data.iloc[:, 0] # 第 1 列为标签
test_data = pd.read_excel(r'.//temp_data//2_测试数据集_平均值填充.xlsx')
test_data_x = test_data.iloc[:, 1:]
test_data_y = test_data.iloc[:, 0]
7.1 六个分类模型
| 逻辑回归 LogisticRegression | C=0.001, max_iter=100, penalty=None, solver='lbfgs' |
| 随机森林 RandomForestClassifier | n_estimators=50, max_depth=20, bootstrap=False, max_features='log2' |
| SVM SVC | kernel='poly', C=1, degree=4, gamma=1 |
| AdaBoost AdaBoostClassifier | 基学习器为 DecisionTreeClassifier(max_depth=2),n_estimators=200 |
| 高斯朴素贝叶斯 GaussianNB | 无参数,直接用 |
| XGBoost XGBClassifier | learning_rate=0.05, n_estimators=200, objective='multi:softmax' |
以逻辑回归为例:
from sklearn.linear_model import LogisticRegression
lr = LogisticRegression(C=0.001, max_iter=100, penalty=None, solver='lbfgs')
lr.fit(train_data_x, train_data_y)
train_predicted = lr.predict(train_data_x)
print('平均值填充_LR的train:\\n', metrics.classification_report(train_data_y, train_predicted, digits=6))
test_predicted = lr.predict(test_data_x)
print('平均值填充_LR的test:\\n', metrics.classification_report(test_data_y, test_predicted, digits=6))
7.2 评估指标提取
原代码通过 classification_report 输出字符串的固定下标取值:
# 生成分类报告字符串,保留6位小数
a = metrics.classification_report(test_data_y, test_predicted, digits=6)
# 按空白字符分割为列表
b = a.split()
# 提取类别0召回率
LR_result['recall_0'] = float(b[6])
# 提取类别1召回率
LR_result['recall_1'] = float(b[11])
# 提取类别2召回率
LR_result['recall_2'] = float(b[16])
# 提取类别3召回率
LR_result['recall_3'] = float(b[21])
# 提取整体准确率
LR_result['acc'] = float(b[25])
对分类报告 split()后 b[6]/b[11]/b[16]/b[21]/b[25] 恰好对应 4 类召回率与总体准确率。
八、总结
通过这个项目,可以掌握以下知识点:
- 删除空行:简单但数据损失大;
- 均值/中位数:适合数值型,中位数抗异常值;
- 众数:适合离散型;
- 线性回归/随机森林:用模型预测填充,精度更高但更复杂;
本项目完整完成了矿物数据清洗、缺失值填充、数据平衡、模型训练与指标评估全流程实验。实验发现,高缺失特征会导致直接删值策略彻底失效,按类别统计填充、模型预测填充等精细化处理方式更适配本数据集。



