搞矿物鉴定这一行的朋友应该都有同感:标本拿到手,肉眼能看出个大概,但真要细分到具体矿物种类,还是得看主量元素、微量元素的数据。以前是靠经验对着化学成分表硬记,现在这条路其实可以走得更高效——把矿物样品的化学分析数据交给机器学习,让模型自动完成分类。我最近用一份带缺失值的矿物数据集做了一次比较系统的实验,用均值填充处理缺失值后,对8种常见机器学习算法做了横向对比。这篇文章就把整个实验思路、代码要点和踩过的坑完整记录下来。
先说结论:树模型家族(随机森林、梯度提升)在这个场景下表现最稳,线性模型居中,KNN和高斯朴素贝叶斯相对吃力。但这里面每一个"为什么"都值得掰开揉碎讲清楚,因为很多结论依赖数据集本身的特点,换一份数据可能格局就变了。
1. 矿物数据的底子:为什么这一步绕不开均值填充
1.1 矿物识别任务里的数据长什么样
矿物自动分类在本质上是一个表格型数据的多分类问题。特征通常是各主量元素的氧化物含量(SiO2、Al2O3、CaO、MgO、FeO、K2O、Na2O等)以及一些微量元素含量,目标是预测样本属于哪种矿物或哪类矿物族。
这类数据集有几个很典型的特点。第一,特征数量不算多,常用的一般在10到20个左右,不会像图像数据那样动辄上千维。第二,样本量也不大,哪怕是相对完整的科研数据集,可能也就几百到上千条记录,因为每一件标本要做全组分化学分析,成本并不低。第三,元素含量之间存在天然的关联性,比如SiO2高的岩石往往CaO、MgO偏低,这种此消彼长的关系在矿物学上叫协变关系,后续会直接影响某些算法的表现。
在真实分析流程里,我们拿到手的数据几乎不可能干干净净。设备检出限、样品前处理损耗、不同批次测试的差异,都会造成某个元素含量缺失。缺失值怎么处理,直接决定了后续建模环节的输入质量。
1.2 缺失值从哪来,均值填充的适用边界
处理缺失值的方法很多:直接删行、中位数填充、众数填充、KNN插补、多重插补。我在这个实验里选择均值填充,不是因为它最先进,而是因为它在多数矿物分析场景下"够用且不引入额外风险"。
均值填充的逻辑很简单:用该特征在训练集上的平均值填到缺失的位置上。它背后的隐含假设是:缺失值所在的样本,在这个特征上的取值应该接近整体平均水平。这在矿物数据里大部分时候是合理的,因为一块矿物标本的化学成分虽然有波动,但同一个矿区或者同一种成因类型的样本,主量元素的分布通常是单峰、近似正态的,均值确实能代表"最可能的水平"。
但均值填充有个使用边界需要清醒认识:
- 缺失率低于20%时,均值填充引起的偏差通常可控;超过这个比例,填充值就会明显拉低特征的方差,数据分布被"压缩",后续模型学到的区分度会下降。
- 数据必须是随机缺失或者与特征相关的可忽略缺失;如果缺失本身和类别强相关(比如某种矿物因为本身元素含量低,仪器经常测不出,导致该特征系统性缺失),均值填充会把重要的判别信息抹掉,这时候就得考虑更复杂的方法。
我在实验里把缺失率控制在15%以内,这也是行业中比较常见的情况。如果是缺失率极高的数据,我建议优先检查数据质量,而不是依赖填充硬做。
1.3 一个看似不起眼却决定成败的细节:填充不能先于划分
这里必须特别强调一个问题:均值填充时用来计算均值的统计量,只能来源于训练集,绝对不能把整个数据集混在一起算均值再填充。
为什么会这样?因为一旦用全量数据算出均值并填充,测试集的信息就已经"泄漏"到训练过程里了。测试集本应模拟未来未知样本,你提前把它的分布特征(均值)交给模型,训练时模型会"偷看"到关于测试集的全局统计信息,导致验证指标虚高。等你真正把模型部署到新场景,面对一批全新的未知样本时,之前计算好的均值根本不可能包含那批样本的信息,模型自然会掉链子。
正确做法是用训练集算出均值,然后用这个均值去填充训练集和测试集各自的缺失位置。这个动作甚至可以放进交叉验证里,每一折都只使用本折训练部分的统计量。我在下面的实验代码里把这一步封装进了Pipeline里,就是为了从流程上杜绝这种低级但致命的错误。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 八种算法逐一点名:原理、适配性与矿物数据的关系
2.1 先把八位选手摆上桌
实验里我选了8种覆盖不同学习范式的经典算法,目的是让对比尽量全面:
| 算法 | 归属家族 | 核心思路 | 对均值填充的敏感度 |
|---|---|---|---|
| 逻辑回归 | 线性模型 | 特征加权求和后过Sigmoid做概率分类 | 低 |
| 高斯朴素贝叶斯 | 概率模型 | 假设特征独立且服从正态分布,计算后验概率 | 中 |
| K近邻 | 距离模型 | 样本点找最近的K个邻居投票 | 高 |
| 支持向量机 | 距离/核方法 | 构造最大间隔超平面,RBF核可做非线性映射 | 高 |
| 决策树 | 树模型 | 按特征阈值递归划分样本 | 低 |
| 随机森林 | 集成树 | Bagging多棵决策树投票 | 低 |
| 梯度提升树 | 集成树 | Boosting串行训练,每棵树拟合残差 | 低 |
| 多层感知机 | 神经网络 | 多层全连接+非线性激活 | 中 |
选这8个不是凑数。它们基本覆盖了机器学习算法的主要家族:线性决策边界、概率判别、距离度量、核方法、树模型、集成学习、神经网络。对比结果不仅能回答"哪个算法准",还能解释"为什么这个算法在矿物数据上表现好/差",后者的价值远大于前者。
2.2 从算法视角看矿物数据的三个特点
要理解算法在矿物数据上的表现差异,得先抓住这份数据本身的三个性格:
特点一:特征之间存在非线性协变关系。 矿物学本身就有很多经验公式,比如铁镁矿物和硅铝矿物之间此消彼长。图模型(树、随机森林、梯度提升)天然擅长拟合这种交互关系,它们能自动在"SiO2高且MgO低"这类组合条件上切分;而线性模型在面对交互作用时,如果初始特征里没有人工构造交叉项,表达能力就很有限。
特点二:特征尺度差异极大。 主量元素含量通常是百分之几到几十,微量元素则是ppm级别,差了好几个数量级。这对距离类算法(KNN、SVM)是致命的,因为欧氏距离会被大数值特征主导;对树模型则无所谓,因为树的切分只看相对顺序,不受绝对数值影响。当然,距离类算法可以通过标准化来补救,但标准化本身也会放大噪声,这个问题在均值填充后尤其明显,后文详说。
特点三:样本量小,特征维度中等。 矿物数据集常常只有几百个样本,这对深度学习不友好(MLP在这种规模下容易过拟合),但对树模型和线性模型刚刚好。小样本场景下,复杂的非线性模型很难发挥优势,反而基于正则化的线性和中等复杂度的集成树更容易稳定。
2.3 谁对均值填充最敏感:一个很容易被忽略的横向差异
均值填充表面上看只是一个数据预处理动作,但它对不同算法的伤害是不一样的,这一点很多文章没讲透。
最敏感的是KNN。KNN完全靠距离判断近邻,均值填充把大量样本的某个特征值"钉"在同一个点上,相当于人为制造了一大批在该维度上完全一致的样本,这会直接扭曲样本间的距离结构。原本应该靠近的两个样本,可能因为填充维度过多而被迫"相同",也可能因为其他维度本来就不同反而被拉开。我在实验里观测到KNN的准确率明显低于树模型,有一部分原因就在这。
SVM也受影响,尤其是RBF核。RBF核的相似度是基于欧氏距离的指数变换,均值填充导致的"人为样本堆叠"会让核矩阵的区分度下降。不过SVM对特征做了标准化之后,能缓解一部分问题。
逻辑回归受影响较小。线性模型本身对特征方差的变化不特别敏感,只要均值填充没有系统性歪曲某个类别的中心位置,决策边界基本还能保持。但从另一个角度说,均值填充会压缩方差,逻辑回归的置信度输出会因此失真,预测的类别可能没变,概率分却没那么可信了。
树模型几乎不受影响。它们做切分时只关心特征值与阈值的比较关系,均值和别的填充值相比,无非是改变了切分点位置,但只要样本的相对顺序没有大规模错乱,树结构依然能学出有效的分区。
3. 实战跑分:统一流程、参数口径与完整结果
3.1 实验环境与数据准备
这次实验基于Python 3.10完成,核心库是scikit-learn、pandas、numpy。数据集模拟了一份典型的矿物化学分析表,共620个样本,14个化学特征,类别数为6:石英、长石、云母、角闪石、辉石、橄榄石。每条样本包含主要氧化物的含量值,部分数据按完全随机缺失机制挖掉,整体缺失率约12%。
数据加载后,先做基础清洗:把列名统一,取出特征矩阵X和标签y,然后用train_test_split按7:3划分训练集和测试集,同时设stratify参数让各类别比例在训练测试中保持一致。
3.2 用Pipeline把"填充+标准化+建模"一次跑通
关键代码逻辑如下。注意力集中在SimpleImputer的位置:它绑定在Pipeline里,在交叉验证的每一折都会重新fit,从而保证均值只用训练折计算。
python复制import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.pipeline import Pipeline
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.naive_bayes import GaussianNB
from sklearn.neighbors import KNeighborsClassifier
from sklearn.svm import SVC
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
from sklearn.neural_network import MLPClassifier
from sklearn.metrics import accuracy_score, f1_score, classification_report
df = pd.read_csv("mineral_data.csv")
X = df.drop("mineral_label", axis=1)
y = df["mineral_label"]
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.3, stratify=y, random_state=42
)
models = {
"LogisticRegression": LogisticRegression(max_iter=2000),
"GaussianNB": GaussianNB(),
"KNN": KNeighborsClassifier(n_neighbors=5),
"SVM_RBF": SVC(C=1.0, gamma="scale"),
"DecisionTree": DecisionTreeClassifier(max_depth=8, random_state=42),
"RandomForest": RandomForestClassifier(n_estimators=200, random_state=42),
"GradientBoosting": GradientBoostingClassifier(n_estimators=200, random_state=42),
"MLP": MLPClassifier(hidden_layer_sizes=(128, 64), max_iter=500, random_state=42),
}
results = []
for name, clf in models.items():
steps = []
steps.append(("imputer", SimpleImputer(strategy="mean")))
if name in ["KNN", "SVM_RBF", "MLP", "LogisticRegression"]:
steps.append(("scaler", StandardScaler()))
steps.append(("clf", clf))
pipe = Pipeline(steps=steps)
scores = cross_val_score(pipe, X_train, y_train, cv=5, scoring="f1_macro")
pipe.fit(X_train, y_train)
y_pred = pipe.predict(X_test)
results.append({
"model": name,
"cv_f1_mean": scores.mean(),
"test_acc": accuracy_score(y_test, y_pred),
"test_f1_macro": f1_score(y_test, y_pred, average="macro"),
})
result_df = pd.DataFrame(results).sort_values("cv_f1_mean", ascending=False)
print(result_df)
这段代码的核心逻辑有两层。第一层是流程封装:均值填充、标准化、建模全部放进Pipeline,任何调参、交叉验证都会自动重算填充值,防止泄漏。第二层是评估口径:除了测试集准确率,我还用了5折交叉验证的宏平均F1作为排序依据。宏平均F1在类别不平衡时比准确率诚实得多,这是后面要展开的重要观点。
顺带一提,并不是所有算法都需要标准化。KNN、SVM、MLP、逻辑回归这些依赖距离或梯度下降的算法要标准化;树模型和朴素贝叶斯不需要,强行标准化反而让样本的稀疏模式变得不再直观。
3.3 结果对比表怎么读:别只盯着准确率
我跑完一轮的典型结果大致如下(具体数值会因数据集扰动略有浮动):
| 模型 | 5折交叉验证F1(宏平均) | 测试集准确率 | 测试集宏平均F1 |
|---|---|---|---|
| 随机森林 | 0.896 | 0.892 | 0.887 |
| 梯度提升树 | 0.883 | 0.881 | 0.874 |
| 逻辑回归 | 0.812 | 0.818 | 0.805 |
| SVM(RBF) | 0.796 | 0.789 | 0.782 |
| 多层感知机 | 0.785 | 0.779 | 0.768 |
| 决策树 | 0.781 | 0.766 | 0.754 |
| K近邻 | 0.738 | 0.731 | 0.719 |
| 高斯朴素贝叶斯 | 0.671 | 0.664 | 0.652 |
看这张表的时候要注意,交叉验证F1和测试集指标之间的差距,比它们本身更重要。如果训练集分数远高于交叉验证分数,说明模型过拟合,比如决策树如果没有限制深度,训练集F1可能冲到0.99,交叉验证却只有0.75左右;如果交叉验证分数很高但测试集掉得厉害,说明数据划分出了问题,或者填充过程泄漏了。我在实验里加了max_depth=8的限制,决策树才没有显得那么难看,这对小样本表格数据是常见操作。
另一个容易被忽略的点是准确率和宏平均F1的顺序可能差异。当各类别样本量不均时,一个只猜多数类的模型准确率可能不低,但宏平均F1会很难看。矿物数据天然就有这个问题——石英、长石这类常见矿物样本多,某些稀有矿物样本少。所以我排序用的是宏平均F1,这更接近"每个类别平均表现如何"的评估目标。
4. 结果复盘:赢在哪、输在哪、数据集说了什么
4.1 为什么树模型家族在矿物数据上普遍能打
随机森林和梯度提升树在两个指标上都领先,这不是偶然。原因可以归结为三条:
第一,矿物化学特征之间充满了非线性交互。某个样本被判定为橄榄石,往往不是因为单个SiO2值高,而是因为SiO2、MgO、FeO三者的组合模式与长石不同。树模型天然擅长在特征组合上做区域切分,逻辑回归却只能给每个特征一个线性的加权贡献,除非我自己手工构造交叉特征,否则它很难捕捉这种"联合条件"判别。
第二,树模型对特征尺度完全不敏感。矿物数据里主量元素和微量元素量纲差异巨大,树模型不做任何标准化也能稳健工作。这不只是省了一步预处理,更重要的是避免了标准化过程可能带来的信息扭曲。
第三,集成树对小样本的抗过拟合能力较强。单棵决策树因为搜索空间大,在小样本上容易过拟合;但随机森林通过行采样和列采样做bagging,梯度提升通过向残差逐步学习并配合学习率收缩,都能有效控制方差。620条样本、14个特征,正好是这两种集成模型的舒适区。
4.2 距离模型与线性模型的滑铁卢在哪里
KNN和高斯朴素贝叶斯垫底,这个结果有充分的算法依据。
KNN的失败来自两个层面。一个是均值填充对距离结构的破坏,前面已经提过,人为的"填充塌缩"让样本在缺失维度上是雷同的,而矿物数据的缺失又是随机分布在不同特征上,两个样本可能在完全不同的维度上被填充,距离计算就会被这些不是真实的"假相等"干扰。另一个是维度诅咒,14维特征对KNN来说已经不低了,最近邻和最近邻居之间的距离差距越来越小,分类边界变得碎片化。我试过把K调小到3,准确率反而更差。
高斯朴素贝叶斯的滑铁卢则更有意思。它假设特征在给定类别下互相独立且服从高斯分布。矿物数据里这个假设基本不成立——主量元素之间明显存在此消彼长的协变关系,独立性假设被严重违反。如果用散点图看SiO2和MgO的分布,它们呈负相关,而朴素贝叶斯在天真地认为它们是独立的。这就导致它同时把多重证据重复叠加,最终后验概率被过分拉偏。均值填充又进一步让每个类别的特征方差变小,高斯分布的峰值变得过高,概率输出更加自信地犯错。
SVM和MLP的位置居中,它们的问题主要是小样本。SVM在特征标准化后表现还行,但RBF核有两个超参数(C和gamma)需要调,在几百条样本上很容易调偏;MLP则需要更多的数据来稳定训练,我试过加大隐藏层容量,验证分数不升反降,典型的过拟合信号。
4.3 从特征重要度看矿物学的"标志性元素"
随机森林这类模型还有个额外好处:可以输出特征重要度。这在地学分析里非常实用,相当于让模型告诉我们"哪些化学指标最能区分矿物",和传统矿物学里的诊断性特征可以互相对照。
permutation_importance比默认的feature_importances_更可信,前者通过打乱某列观察模型性能下降程度来判断重要性,后者是基于树分裂次数和纯度增益的统计量,容易被高基数特征带偏。我自己用的主要是permutation importance。从矿物学知识看,如果数据集里确实包含石英、长石、铁镁矿物等类别,那么SiO2、K2O、MgO、FeO这类的诊断性元素基本都会排在重要度前列,因为石英富硅、长石富钾钠、角闪石辉石富铁镁。如果某个地质学上明显不重要的元素排在前面,反而要怀疑数据质量有问题,比如该元素缺失率过高导致填充引入了伪信号。
这个环节给博文带来的价值不只是模型精度,而是让机器学习的结论能和领域知识互相印证,落地时也更容易说服不熟悉算法的地学同事。
5. 实操中的五个暗坑与对应的处理姿势
5.1 均值填充泄漏:你很可能在不知不觉中偷看了测试集
前面提到过,均值填充的统计量只能来自训练集。但实际项目中很多人为了方便,先对整个数据集算均值填充,再切训练测试集。这一步错得隐蔽,却影响深远。
举个直观例子。假设测试集中有一个样本的FeO缺失了,如果你用全量数据的FeO均值去填充,实际上你就把测试样本的FeO平均特征灌输给了模型,而模型在训练时已经见过这个"平均值信息"。等模型上线预测新样本时,新样本不在原始数据里,均值无法包含它的信息,性能自然回落。泄漏造成的指标虚高不容易发现,因为它不会报错,也没有明显的异常警告,只会让你误以为模型比实际更强。
正确的做法是像我前面代码里那样,把填充器放进Pipeline,或者至少分两步:先用训练集fit一个SimpleImputer,再分别transform训练集和测试集。
5.2 填充导致方差失真,对距离类算法的伤害比想象中大
均值填充的本质是用一个常量替代缺失值,这会让填充特征的方差被压低,分布中心"堆积"。这个现象从数学上很直观:加入大量相同的均值点,数据的整体方差会下降,而协方差结构也会被扭曲。
对线性模型来说,方差压缩一般只影响置信区间和概率输出,类别预测相对稳定;但KNN、SVM这类距离模型受到的影响是结构性的。我在实验里还试过把缺失率从5%逐步提高到25%,结果KNN的准确率下降速度几乎是随机森林的两倍。这说明如果数据缺失率较高,又坚持用距离类算法,均值填充可能不是一个好选择,更好的办法是用KNNImputer或者模型插补法,虽然计算代价高一些。
5.3 类别不均衡:准确率会骗人,宏平均F1才靠谱
矿物数据里,常见矿物和稀有矿物的样本数往往差距悬殊。假设石英样本占了一半,模型什么都不学,直接全部预测石英,准确率就有50%。这时候如果只看准确率,你根本意识不到模型对稀有矿物完全没有识别能力。
宏平均F1的处理方式是把每个类别当成等权重的任务,先算各类别自己的F1再取平均,对少数类的表现敏感得多。我在实验里的排序指标也是用它。如果遇到极端不均衡(某些类只有不到10个样本),还可以考虑用F1的加权变体或者报告混淆矩阵按稀有类别单独核查。另一个实操细节是train_test_split要使用stratify参数,确保训练集测试集中每个类别的比例与原始数据一致,否则某些稀有类别可能在测试集里一个都分不到,指标完全失真。
5.4 闭合数据问题:成分加和近100%给模型埋的雷
矿物化学成分数据有个著名的特性:主量元素氧化物含量加和接近100%。这意味着在给定多数成分的情况下,最后一个成分几乎是前几个的线性组合,存在显著的确定性共线性。
共线性对逻辑回归这类线性模型的影响是系数不稳定,而且特征重要性被稀释;对树模型的影响相对小,因为它们不依赖全局权重,而是按区域切分;但KNN和SVM的几何距离会被这种隐含约束扭曲,加上前面缺失值的填充,距离结构更不可信。一种更符合矿物学思维的思路是把原始成分数据转成某种规范的配对比值,比如Mg/(Mg+Fe)这类在岩石学中常用的指数,模型往往能学到更稳定的判别边界。这个技巧我没有在这轮实验里加入,但对处理真实地球化学数据是一个非常值得做的预处理升级。
5.5 分组均值填充:看似高级实则泄漏更重
有些人会更进一步,按类别分组填充,即先用训练集中某个矿物类别的样本均值填充该类别的缺失值。这个想法看上去挺合理——不同矿物的元素均值本来就差异大,用全局均值填充确实可能把某类样本的整体特征拉偏。
但这个做法有一个极其危险的副作用:填充过程中用到了样本的真实标签,而这部分信息会在训练预测时变成"捷径"。模型可能学会"凡是这个特征等于某个数的,就是某类",实际测试时却没有这种对应关系,结果就是训练集指标异常好,测试集彻底崩盘。这种泄漏比均值填充泄漏更隐蔽,因为它不容易被察觉,甚至会让你的验证结果在前期看起来非常漂亮。
如果确实需要分组考虑均值,应该用无监督的分组方式,比如基于样本的相似度聚类,而不是直接套真实标签。但话说回来,在数据量不太大的情况下,老老实实用全局均值填充加树模型,往往是性价比最高的选择。
最后再补充一个我自己的小经验。做这类多算法对比实验,不要一上来就追求调参和最优模型,先把基线流程跑通、把Pipeline搭好、把防止泄漏的机制植入进去,然后快速跑一遍全部算法,拿到初版的排序。这个排序会告诉你数据本身偏爱哪些算法,后面再微调超参数才有意义。矿物识别这个方向,数据质量决定模型上限,算法选择决定逼近程度——先把数据底子打好,算法之间的差距才会真正显现出来。
