☰
古代玻璃成分分析与风化效应:数学建模实战全流程拆解
2026/10/14 17:51:30 网站建设 项目流程

简介:2022全国大学生数学建模C题完整方案资料,面向准备数模竞赛的学生及数据分析爱好者。内容围绕古代玻璃文物的成分分析与鉴别,给出从数据预处理、探索性分析到建模求解的完整思路,重点涉及主成分分析、因子分析、敏感性分析及灰色关联度等方法,并在文档中附有代码实现,方便读者复现结果并迁移到同类问题上。压缩包内为1个PDF文档,大小3.55MB,详细记录了题目重述、数据探索结论、模型建立过程与关键代码片段。目前已有927人学习下载,对想系统梳理C题解题流程、掌握风化系数定义与亚类划分技巧的同学尤其有参考价值。资源难度适中,适合具有基础Python或MATLAB使用经验的数模参赛者。

1. 数学建模的实战价值:古代玻璃成分分析里的风化效应解谜

数学建模圈子里流传着一句话:C题通常不是最难啃的,但绝对是最容易让人在细节上翻车的。放在“古代玻璃制品的成分分析与鉴别”这道题上,这句话体现得格外明显——表面看只是一堆化学成分百分比,真正做起来才发现,标签有矛盾、风化效应把成分比例搅得面目全非、不同类型的风化方向甚至完全相反。这篇笔记把拆这道题的完整流程梳理清楚:数据预处理、风化系数定义、主成分降维、因子分析预测、区间分类、亚类划分、敏感性分析,再到灰色关联度,以及每一步里我踩过的坑。适合准备数学建模竞赛的参赛者,也适合想用真实数据练手数据分析或特征工程的学习者。

2. 数据预处理与探索:从表单关联到风化系数的三个反直觉结论

2.1 表单1与表单2的关联:从样品编号到四列标签

原题的数据分两个表单:表单1记录文物采样点的属性信息,包括玻璃类型、纹饰、颜色和是否风化;表单2是每个采样点各种化学成分的含量,以百分比形式给出。数据清洗的第一步,就是按采样点编号把两份表单关联起来,同时在主表里补充“玻璃类型”“是否风化”“颜色”“纹饰”四列。

这里我一般用pandas直接做left join,注意两个表单的编号列类型要一致,否则合并后标签全成了NaN。实际操作时还有个细节:表单2的B列到O列正好是14个成分列,我习惯先算一列成分合计,后面筛异常样本时有大用——如果一个样本的合计严重偏离100%,大概率是录入错误或数据单位问题。

import pandas as pd df2 = pd.read_excel("表单2.xlsx") # 成分数据 df1 = pd.read_excel("表单1.xlsx") # 属性信息 # B列到O列共14个成分,先算合计列,后期筛样时用 comp_cols = df2.columns[1:15] df2["成分合计"] = df2[comp_cols].sum(axis=1) # 按采样点编号关联 df = df2.merge(df1, on="采样点编号", how="left") # 组合出建模用的宽表 df_model = df[["玻璃类型", "是否风化", "颜色", "纹饰"] + list(comp_cols) + ["成分合计"]] df_model.to_excel("合并后数据.xlsx", index=False)

这段代码里有三个容易翻车的地方。第一,comp_cols的切片依赖Excel列顺序,B列到O列正好14个成分列,如果原表后面还有别的统计列,索引就会错位。第二,合并前一定要检查左右两边的“采样点编号”是否都是字符串格式,Excel里经常出现数值与文本混存的情况,dtype不一致时merge会因为匹配不上而静默生成NaN,等后面跑模型才发现行数不对。第三,成分合计列不是必须的,但在筛选异常样本时很有用——风化样本的成分总和可能偏离100%,但偏离太多就要回头核对原始记录。

2.2 风化系数:把风化程度变成可计算的指标

完成合并之后,接下来要处理风化效应。参考文献对两种玻璃的风化规律有定性描述:铅钡玻璃风化后SiO2含量显著下降,PbO和BaO显著上升;高钾玻璃则相反,风化后SiO2含量显著上升,K2O、CaO、MgO、Al2O3等成分含量下降。基于这个规律,可以给两类玻璃分别定义风化系数:

  • 铅钡玻璃:风化系数 = (PbO + BaO) / SiO2
  • 高钾玻璃:风化系数 = SiO2 / (K2O + CaO + MgO + Al2O3 + Fe2O3 + CuO)
def weathering_coef(row, glass_type): if glass_type == "铅钡玻璃": return (row["PbO"] + row["BaO"]) / row["SiO2"] # 高钾玻璃:风化后SiO2占比显著提升,其余助熔成分下降 flux = row["K2O"] + row["CaO"] + row["MgO"] + row["Al2O3"] + row["Fe2O3"] + row["CuO"] return row["SiO2"] / flux

两个公式的设计逻辑是对称的:铅钡玻璃风化方向是SiO2流失、PbO和BaO富集,所以系数越大代表风化越严重;高钾玻璃风化方向是SiO2相对富集,所以系数同样越大风化越严重。这样两类玻璃的风化程度可以用统一的方向来比较,只是数值范围和物理含义各自独立。

把风化系数算完并按“类型+纹饰”分组排序后,会发现非常明显的断裂带:铅钡玻璃A纹样本风化系数从0.879跳到1.3,铅钡玻璃C纹在0.8和1.2之间跳跃;高钾玻璃在7.23和12.41之间存在明显间隔。这些断裂带就是风化与未风化的天然判别阈值,铅钡玻璃取1.0,高钾玻璃取10.0,而不是凭经验拍一个0.5或0.8。阈值从数据里“长”出来,这是整套模型能被评委认可的第一个关键点。

2.3 数据探索发现的三个反直觉结论

把表单1的定性属性量化后做相关分析和卡方检验,能发现几个对后续建模影响很深的规律。

第一,纹饰B这个类别非常特殊:纹饰B的样品全部是高钾玻璃,全部风化,颜色全部是蓝绿色。这说明纹饰B对应的年代或埋藏环境风化效应极强,连相对耐风化的高钾玻璃都无一幸免。第二,同等条件下铅钡玻璃比高钾玻璃更容易风化。纹饰A中铅钡玻璃风化与未风化比例为11:5,纹饰C中为17:73,而高钾玻璃几乎全部未风化——这个结论直接决定了问题一必须分类型讨论。第三,相关性分析显示纹饰与颜色、类型的相关性显著,但“表面风化”与纹饰、颜色的p值分别高达0.504和0.393,只在10%水平上与类型相关。换句话说,风化状态和纹饰、颜色没有统计上的强关联,唯一能拿来做问题一建模的定性变量就是玻璃类型。

这三个结论集中到一起,指向一个核心判断:后续所有建模都要按玻璃类型拆开做,不能把铅钡和高钾混在一起,否则风化方向互相抵消,模型根本看不出规律。这个结论看起来简单,但很多参赛队在第2章就翻车了——他们拿着全量数据做PCA、做聚类,结果主成分被两类玻璃完全不同的成分体系带偏,后面的结论全都对不上号。

3. 主成分分析与因子分析:用PCA降维与转换矩阵反推未风化成分

3.1 为什么先做主成分分析而不是直接做回归

问题一的要求有两层:一是研究风化与类型、纹饰、颜色的关系,二是根据风化点检测数据预测风化前的成分含量。第一层通过卡方检验就可以回答,第二层才是真正的建模难点。

预测未风化成分时,最直接的想法是拿“已风化成分”做自变量、用文献或经验公式去还原,但化学成分之间存在严重的多重共线性,而且风化过程是多个成分同步变化,直接回归很容易过拟合。常见做法是先做PCA降维,保留累计贡献率足够高的主成分,再在主成分空间里做因子分析。

from sklearn.preprocessing import StandardScaler from sklearn.decomposition import PCA scaler = StandardScaler() X_scaled = scaler.fit_transform(X_comp) pca = PCA(n_components=0.97) X_pca = pca.fit_transform(X_scaled) print("主成分个数:", X_pca.shape[1]) print("累计贡献率:", pca.explained_variance_ratio_.cumsum())

参数n_components=0.97表示自动选择能让累计方差贡献率达到97%的主成分数量,不同类别算出来的k会不同。之所以取97%而不是85%,是因为预测任务对成分还原精度要求更高,85%可能在后续转换矩阵求逆时丢失过多信息。以铅钡玻璃C纹为例,取8个主成分能保留97%以上的成分构成信息,这就是一个可以直接写进论文的参数。

3.2 因子分析:把风化过程建模成线性组合

因子分析的建模思路是这样的:假设玻璃当前观测到的成分特征X,是未风化原始成分F经过持续风化演进得到的线性组合,即X = μ + A·F + ε。其中μ是期望向量,A是因子负载矩阵,F是公共因子向量(也就是我们要预测的未风化成分),ε是随机误差。

在代码实现中,负载矩阵A可以由PCA的特征值和特征向量近似得到。A的每一列等于对应特征值的平方根乘以特征向量。得到A之后,对方程做逆变换就能把公共因子F表示为观测变量X的线性组合,这正是预测未风化成分的关键。

import numpy as np # pca.components_ 对应特征向量,explained_variance_ 对应特征值 loadings = pca.components_.T * np.sqrt(pca.explained_variance_) # F = pinv(A) @ (X - mu),用伪逆而不是直接求逆 F_hat = np.linalg.pinv(loadings) @ (X_pca.T)

这里有一个很容易卡住的地方:A不一定是方阵,成分数通常大于主成分数,直接调np.linalg.inv会报矩阵不可逆的错误。换成np.linalg.pinv求Moore-Penrose伪逆,就可以在行列不一致的情况下得到最小二乘解。这一步对初学者来说是最容易翻车的,代码排错往往就卡在这一行。我当时第一次跑也在这里报错,后来把inv改成pinv才通过。

3.3 预测结果如何验证:与化学规律对表

算出F_hat之后,把它投影回原始成分空间,得到的就是每个风化样本的未风化成分预测值。以铅钡玻璃C纹为例,严重风化点原数据中SiO2只有个位数,PbO接近50%,而预测值里SiO2回到51-67%区间,PbO回落到20%上下。这与参考文献中“铅钡玻璃风化方向是SiO2减少、PbO和BaO富集”的结论完全吻合,也说明因子分析模型本质上学到的就是风化过程的逆映射。

验证这一步建议写成自动化对比:对每个样本输出“原值 vs 预测值”的对照表,再按成分画几个散点图。如果发现某个成分的预测值出现负值或远超100%的不合理数值,优先检查标准化过程是否用了全样本的均值方差,而不是当前类别的——类内标准化和全局标准化对因子分析结果的影响比想象中大得多。

4. 区间分类与亚类划分:主成分筛选、最短距离法与3%扰动测试

4.1 四类样品的主成分分析与区间构造

问题二要在“高钾玻璃”“铅钡玻璃”两个大类内部找出更细的分类规律,并按类别做亚类划分。数据可以天然分成四组:铅钡未风化、铅钡风化、高钾未风化、高钾风化。对每一组单独做PCA,选出贡献率最高的成分作为关键分类特征。

各组筛选结果如下:

  • 铅钡未风化:SiO2、Na2O、K2O、CaO、MgO、Al2O3,累计贡献率88.89%
  • 高钾未风化:SiO2、Na2O、K2O、CaO、MgO,累计贡献率88.89%
  • 铅钡风化:SiO2、Na2O、K2O、CaO、MgO,累计贡献率87.36%
  • 高钾风化:SiO2、Na2O、K2O,累计贡献率95.41%

对筛选出的每个成分,在去掉样本最大值和最小值之后取剩余数据的最大值和最小值构成区间,这就是大类的分类判据。以铅钡未风化类型为例,SiO2区间为[50.61, 69.71],Na2O区间为[0.8, 5.74],K2O区间为[0.15, 0.32],CaO区间为[0.38, 2.82],MgO区间为[0.71, 1.49],Al2O3区间为[1.44, 13.65]。未知样本带入后,数值落在哪个类别区间,或距离哪个类别区间最近,就归属哪一类。

def build_interval(s): s = s.dropna().sort_values().values trimmed = s[1:-1] # 去掉max和min return (trimmed.min(), trimmed.max())

去掉最大值和最小值的目的,是防止个别异常采样点把区间撑得太宽。如果不剔除,任何风化程度极端的样本都会让整个类别的区间失去区分度。区间构造的代码逻辑很简单,真正的细节在于:哪些成分进区间、哪些不进,完全由PCA的贡献率决定,而不是把14个成分全部塞进去。进区间的成分越多,看似分类越细,实际上噪声也越多,未知样本误判的概率反而更高。

4.2 最短距离法做亚类划分

在四类样品中,观察各成分的数据分布,发现SiO2的类内差异最大,因此选SiO2作为亚类划分的依据。亚类划分采用最短距离法,即单连接层次聚类:先计算样品两两距离,每个样品自成一类,每次找到距离最近的两类合并,直到只剩目标类别数。

from sklearn.cluster import AgglomerativeClustering X_sio2 = df_sub["SiO2"].values.reshape(-1, 1) model = AgglomerativeClustering(n_clusters=2, linkage="single") labels = model.fit_predict(X_sio2) p1 = X_sio2[labels == 0].mean() p2 = X_sio2[labels == 1].mean() split = (p1 + p2) / 2 print(f"聚点1: {p1:.2f}, 聚点2: {p2:.2f}, 分界点: {split:.2f}")

linkage="single"对应最短距离法,类间距离取两簇最邻近样本的距离,n_clusters=2表示把每个类别内部的样品分成两个亚类。聚类完成后,取两个聚点的平均值作为分界点,将SiO2划分为两段区间。四类的聚点与分界点整理成参数表如下:

类别聚点1聚点2分界点亚类1区间亚类2区间
铅钡未风化52.4964.8258.70[50.61, 58.70)[58.70, 69.71]
高钾未风化62.7771.2267.00[59.81, 67.00)[67.00, 76.68]
铅钡风化15.6333.9624.80[4.61, 24.80)[24.80, 37.36]
高钾风化92.5794.6693.62[92.35, 93.62)[93.62, 95.02]

每个亚类画饼状图检查分布比例,只要两亚类的样本量没有出现极端失衡,就认为亚类划分合理。注意这里的分界点不是聚类中心,而是两个聚点的均值,这样区间两侧的样本量会更均衡,后续分类时也不容易因为边界偏移而产生系统性误判。

4.3 3%随机扰动下的敏感性分析

参赛论文里的敏感性分析通常是这样写的:“给数据添加3%的随机扰动,再分类,结果基本不变。”但从复现的角度看,这个说法不够严谨——随机扰动只跑一次,结果完全取决于随机种子,不具备统计意义。

rng = np.random.default_rng(42) noise = 1 + rng.uniform(-0.03, 0.03, size=X_sio2.shape) X_noisy = X_sio2 * noise # 重新按原分界点划分亚类 new_labels = (X_noisy >= split).astype(int) change_rate = (new_labels != labels).mean() print(f"变化比例: {change_rate:.1%}")

我在复现时会把seed 42跑一次作为展示结果,然后额外用不同的随机种子批量跑100次,统计变化比例的均值和最大值。结果是:问题二的亚类划分在3%扰动下有三类的变化比例超过20%,说明亚类边界对噪声相当敏感;而问题三的大类鉴别在同样扰动下分类结果完全不变,说明大类区间的边界足够宽、足够稳健。这个对比放在论文里,比单一句“敏感性较强/不强”更有说服力。

5. 避坑与常见问题排查:玻璃成分分析里的五处翻车现场

做这道题最容易出问题的地方不在模型本身,而在数据处理和参数设定这些“看不到的角落”。以下五条都是我在复现过程中真实踩过的坑,按现象、原因、解决的顺序写清楚,希望能帮你少走弯路。

5.1 风化系数阈值拍脑袋定,分类结果全乱

现象:拿到数据后直接按0.8、1.0这种直觉值设阈值,结果风化与未风化的分类跟采样点标注对不上,很多明显为未风化的样本被判成风化。

原因:不同玻璃类型的风化方向相反,铅钡和高钾的风化系数定义本身就不同,绝对值没有可比性,更不能共用一个阈值。铅钡玻璃的风化系数正常波动范围在0.2到1.3之间,高钾玻璃却在1.6到31.8之间,两者差了一个数量级。

解决:先按类型和纹饰分组,把风化系数从小到大排序,找到数据分布的突变位置再定阈值。铅钡玻璃A纹在0.879与1.3之间突变,高钾玻璃在7.23与12.41之间断裂,阈值分别取1.0和10.0。我在复现时把这个逻辑写成了函数,输入类别和纹饰自动返回建议阈值,避免每次手工翻表格。

5.2 采样点标签与风化系数判断冲突

现象:样本20、48、30部位1这三个样品的标签与风化系数计算出的风化状态不一致。

原因:采样点标注是人工现场判断,存在主观性,而风化系数是连续计算值,更能客观反映成分变化程度。现场标注靠的是肉眼观察玻璃表面状态,而风化系数靠的是成分比例的反演,两者在临界样本上必然会出现分歧。

解决:以风化系数为准手工调整这三个样本的标签。但要记住,调整标签后所有统计分析都要重跑一遍,卡方检验、相关性、PCA的结果都可能发生变化。我在第一次调整后忘记重跑卡方检验,结果后面的相关性结论还停留在旧标签上,差点写进论文里。

5.3 PCA之前忘了标准化

现象:第一主成分几乎完全由SiO2主导,其他成分的贡献率全部被压缩到个位数,后续因子分析的转换矩阵也失真。

原因:SiO2的含量在50到90之间,而PbO、SrO这些成分含量往往不到5,量纲差异让方差计算被高含量成分完全支配。不标准化就直接跑PCA,等于默认了“含量越高的成分越重要”,但成分含量的绝对值与分类价值并没有直接关系。

解决:PCA之前必须用StandardScaler把每个特征标准化为均值0、方差1,做完后再看explained_variance_ratio_,各主成分的贡献率分布才会合理。这个坑在各类数据竞赛里出现频率极高,凡是涉及PCA的地方都建议先把标准化写进代码模板。

5.4 敏感性分析只做一次随机扰动

现象:论文里写“3%扰动后分类结果不变”,但复现时换个随机种子就出现了不同的分类结果。

原因:单次随机扰动只是抽取一个随机场景,不能代表模型的真实稳定性。特别是样本量不大的时候,一个随机种子的结果波动很大,跑一次就说“稳健”属于典型的幸存者偏差。

解决:至少跑100次不同随机种子的扰动,统计分类结果变化的均值和极值。我一般保留seed 42跑一次的确定性结果做展示,再用批量随机种子验证,两者结论一致才写入最终报告。问题二的亚类划分,单次跑可能只看到某两类变化超过20%,但多跑几次会发现四类里第三类也经常被扰动影响,结论更全面。

5.5 灰色关联度直接拿原始成分计算

现象:关联度排序结果中所有成分数值都很接近,排序没有区分度,无法体现类别间的差异性。

原因:各化学成分原始含量跨度太大,没有做无量纲化处理,导致关联系数失效。灰色关联度的核心是比较序列形态的相似性,如果不消除量纲影响,SiO2这种高含量成分天然会主导计算,低含量成分的关联系数全被压缩在很小的范围内。

解决:按灰色关联度的标准流程,先对每个序列做均值化处理(每列除以该列均值),再计算两级差与两级最大差,分辨系数取0.5,最后取平均得到关联度。我在代码里把均值化写成了独立函数,每次调用前先检查输入数据是否已经做过无量纲化,避免这个低级错误反复出现。

6. 灰色关联度分析:四类玻璃的成分关联排序与差异性解读

6.1 灰色关联度的计算流程与代码

灰色关联度适合小样本、数据不完全确定的系统做关联分析。问题四中,每组样品数量不多,化学成分之间又存在非线性关系,正好是灰色关联度的适用场景。计算方法分四步:确定参考数列与比较数列、均值化处理、计算关联系数、平均得到关联度并排序。

def grey_relational_degree(ref, comp, rho=0.5): # 均值化处理,消除量纲影响 ref = ref / ref.mean() comp = comp / comp.mean() diff = np.abs(ref - comp) delta_min = diff.min() delta_max = diff.max() # 关联系数公式,rho为分辨系数 coef = (delta_min + rho * delta_max) / (diff + rho * delta_max) return coef.mean()

rho参数代表分辨系数,取值在0到1之间,通常取0.5,数值越小分辨力越强。代码里先均值化,再算每个时刻的比较序列与参考序列的差序列,然后按标准公式计算关联系数,最后取平均得到整体关联度。所有成分的关联度排序后,就是后续差异性分析的输入。

6.2 四类玻璃的关联度排序与差异性解读

复现后得到的排序结果与赛题文档一致:铅钡玻璃为SiO2 > CaO > K2O > Al2O3 > MgO > Na2O,高钾玻璃为SiO2 > Na2O > K2O > CaO > MgO。共同点是SiO2在四类玻璃中都排名第一,对应它作为玻璃网络骨架的核心地位。差异在于铅钡玻璃中CaO排名靠前,高钾玻璃中Na2O和K2O排名靠前,这与两类玻璃的助熔剂体系直接相关:铅钡玻璃使用铅矿石助燃,钙的参与度更高;高钾玻璃使用草木灰助燃,钾钠含量更高,排序结果能和化学原理对上,整个模型的可信度就上来了。

从验证习惯来说,关联度算完我会强制做一次“与化学原理交叉检查”,一旦出现明显矛盾(比如SiO2落到倒数),优先回头检查均值化和分辨系数是否设置正确。从那以后我每次做成分数据都强制走一遍“标准化—PCA—区间分类—扰动验证—灰色关联度”的完整流程,尤其是扰动验证,多跑几个随机种子再下结论。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询