简介:这份资源是2025年五一数学建模竞赛B题「矿山监测数据的高效处理与建模优化」的完整参赛作品,包含完整论文与配套代码,适合具备一定数学建模基础、关注矿山监测数据处理的科研人员与工程师参考学习。作品综合运用BP神经网络、主成分分析、卡尔曼滤波与贝叶斯优化等方法,围绕数据拟合、降维压缩、时序去噪、超参数调优及高维数据重构五个问题给出完整建模方案,各问拟合优度均达0.97以上,压缩比达167:1。资源包共1个PDF文件,大小约3.23MB,内容涵盖问题重述、模型假设、求解过程、误差分析与检验方法,结构完整。目前已有306人学习下载。读者可从中获取完整赛题求解思路、模型构建细节与误差分析框架,理解PCA降维、卡尔曼滤波去噪、贝叶斯优化调参及早停机制的具体实现,并借鉴其模型检验与复杂度分析写法,为自身建模与论文撰写提供参考。
1. 矿山数据处理赛题:从三张表到一条可复现的建模链路
2025年五一赛B题矿山数据处理问题,核心不是“算出一个数”,而是把矿山生产里散落的多源表格,整理成能支撑决策的完整证据链。很多队伍拿到题第一反应是套预测模型,结果卡在数据对不上:产量表按日、设备表按班次、品位表按采样点,三张表的时间粒度和空间粒度都不一致。这道题真正考的是数据治理能力——你能不能把矿山数据从“看起来能用”变成“建模敢用”。适合谁?适合参加过数学建模竞赛、做过数据清洗、想拿完整论文和代码复现一套矿山数据处理流程的人。下面按我实际做这类题的顺序,把选型、实现、参数和坑一次讲透。
2. 矿山多源数据怎么对齐:时间粒度与空间粒度的双重校准
矿山数据处理最耗时的不是建模,是对齐。B题通常会给三类表:生产日报、设备运行记录、品位采样记录。生产日报按“日”汇总,设备记录按“班次”打点,品位采样按“爆堆/台阶/钻孔”记录。直接merge会丢大量行,因为粒度不匹配。我一般先把所有表统一到“日+作业面”这个最小公共粒度,再做后续特征。
2.1 时间粒度统一:把班次聚合到日
设备表里一个班次可能跨零点,比如夜班从20:00到次日8:00。如果按自然日切,会把一个班次劈成两半。常见做法是给每个班次定义一个“生产日”,以班次开始时间所在日期为准。下面这段pandas代码做三件事:解析时间、标记生产日、按日+设备聚合。
import pandas as pd # 设备记录:equip_df 含 equip_id, start_time, end_time, run_hours, shift equip_df['start_time'] = pd.to_datetime(equip_df['start_time']) equip_df['end_time'] = pd.to_datetime(equip_df['end_time']) # 生产日 = 班次开始时间所在日期,避免跨零点被劈开 equip_df['prod_date'] = equip_df['start_time'].dt.normalize() # 按生产日+设备聚合运行时长和班次数 daily_equip = equip_df.groupby(['prod_date', 'equip_id']).agg( run_hours_sum=('run_hours', 'sum'), shift_count=('shift', 'count'), first_start=('start_time', 'min'), last_end=('end_time', 'max') ).reset_index() # 校验:单日单设备运行时长不应超过24小时 over = daily_equip[daily_equip['run_hours_sum'] > 24] print(f"异常记录数:{len(over)}")逻辑说明:normalize()把时间戳归到当天零点,作为生产日主键。聚合时用sum而不是mean,因为后续要算设备利用率,需要总运行时长。参数上,run_hours字段如果原始表没有,需要用end_time - start_time换算成小时,注意跨零点时直接相减会得到负值,要先判断再补24小时。校验那一步不能省,超过24小时的记录要么是跨班次重复计数,要么是时间字段有脏数据,必须回查。
2.2 空间粒度统一:作业面编码映射
品位采样表里的位置可能是“南帮+1240台阶+3号钻孔”,生产日报里写的是“南帮1240”。字符串不统一,直接join匹配不上。我一般建一张映射表,把采样点编码到作业面编码。做法是提取“区域+台阶”作为主键,钻孔号作为次级维度。
import re def extract_bench(location_str): # 从“南帮+1240台阶+3号钻孔”提取区域和台阶 pattern = r'([\u4e00-\u9fa5]+)[+\-]?(\d{3,4})' match = re.search(pattern, location_str) if match: return match.group(1), int(match.group(2)) return None, None sample_df[['area', 'bench']] = sample_df['location'].apply( lambda x: pd.Series(extract_bench(x)) ) sample_df['face_code'] = sample_df['area'] + '_' + sample_df['bench'].astype(str) # 生产日报同样处理 prod_df[['area', 'bench']] = prod_df['face'].apply( lambda x: pd.Series(extract_bench(x)) ) prod_df['face_code'] = prod_df['area'] + '_' + prod_df['bench'].astype(str)逻辑说明:正则里[\u4e00-\u9fa5]+匹配中文区域名,\d{3,4}匹配三位或四位台阶标高。参数上,台阶标高一般是1240、1180这种四位数,也可能是三位数,所以用{3,4}。如果区域名里带“号”字,比如“1号采区”,正则会把它切进去,需要额外处理。映射完先做一次merge的indicator检查,看有多少行匹配不上,匹配率低于90%就要回头改正则。
2.3 对齐后的完整性校验
对齐完不要急着建模,先跑三张校验:时间连续性、空间覆盖率、数值合理性。时间连续性看每个作业面是否每天都有记录,缺的日子要标记;空间覆盖率看采样点是否覆盖了所有生产作业面;数值合理性看品位是否在0到100之间、产量是否为负。这三步能挡掉大部分后续翻车。
| 校验项 | 检查方法 | 异常处理 |
|---|---|---|
| 时间连续性 | 按face_code+prod_date做完整日期序列,看缺失 | 缺失日标记为NaN,建模时用前向填充或剔除 |
| 空间覆盖率 | 采样face_code集合与生产face_code集合求差 | 未覆盖作业面单独列出,论文里说明局限 |
| 数值合理性 | 品位0-100,产量>=0,运行时长0-24 | 越界值回查原始记录,确认是录入错误还是单位问题 |
3. 从清洗到特征:矿山数据建模前的特征工程怎么做
对齐之后,原始表变成了一张以“生产日+作业面”为主键的宽表。但直接拿品位、产量、运行时长去回归,效果通常一般,因为矿山数据有强自相关和滞后效应。这一章讲怎么构造有物理意义的特征,而不是盲目多项式展开。
3.1 滞后特征与滑动窗口
矿山生产里,今天的品位往往受前两天爆破和配矿影响。我一般构造1到3天的滞后品位、3天滑动平均产量、7天累计运行时长。注意滑动窗口要用shift避免数据泄漏——用当天及之前的数据算特征,预测当天或之后的目标。
# 宽表按face_code分组,按prod_date排序 df = df.sort_values(['face_code', 'prod_date']) # 滞后品位:前1、2、3天 for lag in [1, 2, 3]: df[f'grade_lag{lag}'] = df.groupby('face_code')['grade'].shift(lag) # 3天滑动平均产量:用当天及之前两天,避免泄漏 df['prod_ma3'] = df.groupby('face_code')['production'].transform( lambda x: x.rolling(window=3, min_periods=1).mean() ) # 7天累计运行时长 df['run_cum7'] = df.groupby('face_code')['run_hours_sum'].transform( lambda x: x.rolling(window=7, min_periods=1).sum() )逻辑说明:shift(lag)把前lag天的值挪到当前行,构造滞后特征。rolling的min_periods=1保证序列开头不会全变NaN。参数上,滞后阶数选1到3是因为矿山配矿周期通常2到3天,再长相关性就弱了。滑动窗口选3和7,对应周内生产节奏。如果数据量少,窗口再缩小,否则特征大量缺失。
3.2 类别特征编码与作业面嵌入
作业面编码是类别特征,直接one-hot维度太高。我一般用目标编码或频率编码。目标编码要注意用K折交叉,否则会泄漏目标均值。下面用5折目标编码示例。
from sklearn.model_selection import KFold def target_encode(df, col, target, n_splits=5): df[f'{col}_te'] = 0.0 kf = KFold(n_splits=n_splits, shuffle=True, random_state=42) for train_idx, val_idx in kf.split(df): # 用训练折的均值编码验证折,避免泄漏 means = df.iloc[train_idx].groupby(col)[target].mean() df.loc[df.index[val_idx], f'{col}_te'] = ( df.iloc[val_idx][col].map(means) ) # 全局均值填充未匹配 df[f'{col}_te'] = df[f'{col}_te'].fillna(df[target].mean()) return df df = target_encode(df, 'face_code', 'grade')逻辑说明:K折目标编码把数据分成5份,每次用4份算均值去编码剩下1份,这样验证折的编码不包含自身目标值。参数上,n_splits一般5,数据量小用3。random_state固定保证可复现。注意目标编码只适合树模型,线性模型下容易过拟合,线性模型建议用频率编码或嵌入层。
3.3 特征筛选:用相关性+重要性双过滤
特征不是越多越好。我一般先算Pearson相关,剔除相关性高于0.95的冗余特征,再用随机森林看重要性,保留前15个。这样论文里特征表也好看,模型也稳。
import numpy as np from sklearn.ensemble import RandomForestRegressor # 相关性过滤 corr_matrix = df[numeric_cols].corr().abs() upper = corr_matrix.where(np.triu(np.ones(corr_matrix.shape), k=1).astype(bool)) to_drop = [col for col in upper.columns if any(upper[col] > 0.95)] df = df.drop(columns=to_drop) # 随机森林重要性 rf = RandomForestRegressor(n_estimators=200, random_state=42) rf.fit(df[feature_cols], df['grade']) importance = pd.Series(rf.feature_importances_, index=feature_cols) top_features = importance.nlargest(15).index.tolist()逻辑说明:np.triu取上三角,避免重复计算对称相关性。阈值0.95是经验值,矿山数据里产量和运行时长相关性经常到0.9以上,但不到0.95,所以能保留。随机森林n_estimators=200足够稳定,再多收益小。重要性前15是平衡模型复杂度和可解释性,论文里可以画重要性条形图。
4. 模型选型与调参:为什么矿山数据更适合树模型加残差修正
矿山数据有非线性、缺失值、异常值,树模型天然抗这些。但纯树模型在品位预测上容易欠拟合趋势,我一般用LightGBM做主干,再用线性回归修正残差。这一章讲选型理由和具体参数。
4.1 为什么选LightGBM而不是LSTM
LSTM需要序列完整、样本量大,矿山数据按作业面分组后,每个作业面可能只有几十到几百天,LSTM容易过拟合。LightGBM对缺失值不敏感,训练快,特征重要性可解释。如果题目要求时序预测,可以用LightGBM加滞后特征替代LSTM,效果通常不差。常见做法是先用LightGBM跑基线,再考虑要不要上时序模型。
4.2 关键参数与调参顺序
LightGBM参数多,但矿山数据量下,我一般只调五个:num_leaves、learning_rate、feature_fraction、bagging_fraction、min_data_in_leaf。调参顺序:先定num_leaves和min_data_in_leaf控制复杂度,再降learning_rate加n_estimators,最后调采样比例。
import lightgbm as lgb from sklearn.model_selection import TimeSeriesSplit params = { 'objective': 'regression', 'metric': 'mae', 'num_leaves': 31, # 控制树复杂度,数据量小别超63 'learning_rate': 0.05, # 先0.1粗调,再0.05精调 'feature_fraction': 0.8, # 每棵树用80%特征,防过拟合 'bagging_fraction': 0.8, # 每轮用80%样本 'bagging_freq': 5, 'min_data_in_leaf': 20, # 叶子最小样本,矿山数据噪声大,别低于10 'verbose': -1, 'random_state': 42 } # 时序交叉验证,避免随机切分泄漏 tscv = TimeSeriesSplit(n_splits=5) for train_idx, val_idx in tscv.split(X): dtrain = lgb.Dataset(X.iloc[train_idx], y.iloc[train_idx]) dval = lgb.Dataset(X.iloc[val_idx], y.iloc[val_idx]) model = lgb.train(params, dtrain, valid_sets=[dval], num_boost_round=1000, callbacks=[lgb.early_stopping(50)])逻辑说明:num_leaves=31是数据量几千行时的安全值,超过63容易过拟合。min_data_in_leaf=20防止叶子节点样本太少学噪声。early_stopping(50)表示验证集50轮不提升就停,避免无效迭代。时序交叉验证用TimeSeriesSplit,不能随机切,否则滞后特征会泄漏未来信息。
4.3 残差修正:线性模型补趋势
LightGBM对趋势外推弱,如果品位有长期下降趋势,树模型会在训练集边界外预测平缓。我一般用线性回归拟合残差,再把两部分相加。做法是先用LightGBM预测,算残差,再用线性回归对残差建模,最后叠加。
from sklearn.linear_model import LinearRegression # 训练集残差 train_pred = model.predict(X_train) residual = y_train - train_pred # 用时间索引和滞后特征拟合残差 lr = LinearRegression() lr.fit(X_train[['day_index', 'grade_lag1']], residual) # 最终预测 = LightGBM + 线性残差修正 final_pred = model.predict(X_test) + lr.predict(X_test[['day_index', 'grade_lag1']])逻辑说明:day_index是时间序号,捕捉趋势。grade_lag1捕捉残差自相关。线性回归系数可解释,论文里能写“趋势项系数为负,说明品位随开采深度下降”。注意残差修正只在验证集上有效再上测试集,否则可能引入额外方差。
5. 避坑与排查:矿山数据处理里最容易翻车的五个点
这一章按“现象→原因→解决”写,都是我在做这类题时真实踩过的坑。
5.1 现象:merge后行数暴涨
原因:生产日报和采样表在“作业面+日期”上不唯一,一个作业面一天多条采样记录,直接merge产生笛卡尔积。解决:merge前先按主键聚合,采样表按face_code+prod_date取均值或加权均值,生产表去重。聚合后再merge,行数应该等于生产表行数。
5.2 现象:模型验证集MAE远低于测试集
原因:特征里用了未来信息,比如滑动窗口没shift,或者目标编码用了全量数据。解决:所有时序特征必须用shift或rolling且只包含当前及过去。目标编码必须K折。检查方法:把验证集按时间切,看特征在验证集时间点是否可获取。
5.3 现象:品位预测值超出0-100
原因:回归模型无界,树模型在边界外会外推。解决:训练前对目标做logit变换或截断,预测后再逆变换。或者用分位数回归,输出区间。简单做法是预测后clip到0-100,但论文里要说明这是后处理。
5.4 现象:LightGBM训练很快但早停轮数很少
原因:min_data_in_leaf太小或num_leaves太大,模型几轮就过拟合。解决:调大min_data_in_leaf到20-50,调小num_leaves到15-31,降低learning_rate到0.03再试。看验证集曲线,如果训练损失降但验证损失升,就是过拟合。
5.5 现象:论文里特征重要性全是滞后特征
原因:滞后特征和目标相关性太强,掩盖了其他特征。解决:分两组建模,一组只用当天特征,一组加滞后特征,对比效果。论文里可以写“滞后品位贡献最大,但设备运行时长对残差有修正作用”。不要只放一张重要性图就完事。
6. 完整论文与代码的复现技巧:从结果到可交付
最后一章讲怎么把上面流程整理成可复现的论文和代码。核心是三个习惯:固定随机种子、保存中间结果、写清参数来源。
6.1 固定随机种子与结果可复现
所有涉及随机的步骤——K折、LightGBM、随机森林——都要设random_state。我一般建一个SEED=42常量,全局引用。数据切分用时间切,不用随机切。这样别人跑你的代码,结果能对上。
SEED = 42 import numpy as np np.random.seed(SEED) import random random.seed(SEED)6.2 中间结果落盘与版本管理
清洗后的宽表、特征表、模型预测结果都存成CSV或parquet,文件名带日期和参数。比如features_ma3_lag3_20250501.csv。这样调参时不用重跑清洗,也方便论文里回溯哪个参数对应哪个结果。
| 文件 | 内容 | 用途 |
|---|---|---|
| raw_merged.csv | 三表对齐后宽表 | 检查对齐质量 |
| features_v1.csv | 滞后+滑动窗口特征 | 建模输入 |
| pred_lgb_v1.csv | LightGBM预测结果 | 论文图表 |
| pred_final.csv | 残差修正后结果 | 最终提交 |
6.3 论文里怎么描述参数来源
不要写“经过多次实验,参数设为31”。要写“num_leaves在[15,31,63]中网格搜索,31在验证集MAE最低;min_data_in_leaf从10到50步长10,20最优”。这样审阅者能复现你的调参过程。表格里放参数、搜索范围、最优值、验证指标四列。
6.4 一个具体技巧:用残差图判断模型是否欠拟合
训练完画残差对预测值的散点图。如果残差呈漏斗形,说明异方差,考虑对目标取对数。如果残差有U形趋势,说明模型欠拟合非线性,加特征或换模型。如果残差围绕0随机分布,说明模型基本可用。这个图比MAE更能说明问题,论文里放一张,审阅者会觉得你懂模型诊断。
我自己的习惯是:每次跑完模型先看残差图,再看特征重要性,最后才看MAE。因为MAE低不代表模型对,残差图能暴露很多玄学问题。希望帮到你。
本文还有配套的精品资源,点击获取