简介:这份资源是2025年深圳杯数学建模竞赛D题的完整分析论文,面向参赛选手、数学建模学习者及需要赛题复盘的研究者,帮助系统理解D题从问题重述到模型求解的全过程。压缩包内共1个PDF文件,约2.71MB,内容涵盖摘要、问题重述、问题分析、模型假设、符号定义,以及问题一至问题三的模型建立与求解,涉及特征工程设计、分类模型结构与数学表达、模型性能对比、贡献者兼容性筛选、混合比例优化估计和组合匹配评分函数等关键模块。论文按步骤展开,配有可运行代码与相关数据,便于读者对照复现建模流程、理解评估指标定义与重要特征分析。目前已有330人学习,适合希望掌握竞赛论文写作框架、借鉴特征工程与分类建模思路的读者参考,也可作为数学建模实践中的案例材料。
1. 深圳杯D题到底在考什么:从一份“完整分析论文”拆出可复现的建模链路
深圳杯数学建模竞赛的D题,历年都是那种“看起来像数据分析、做起来像信号处理、最后发现是机器学习调参”的复合题型。2025年这道题也不例外。很多队伍拿到题目第一反应是套一个回归模型交差,结果在评审环节被一句“数据预处理依据是什么”问穿。真正拉开差距的,不是模型多花哨,而是你有没有把小波阈值去噪、GBDT、STR趋势分解这条链路讲清楚,并且每一步都能跑出可复现的结果。这篇笔记不讲空话,我按自己带队的实际流程,把一份“完整分析论文”该有的模型、代码、数据组织方式拆开讲。适合正在准备数学建模竞赛、或者想用机器学习方法处理时序信号的从业者。读完你能拿到一条从原始数据到论文图表的完整路径,而不是一堆散落的算法名词。
2. 数据预处理:小波阈值去噪为什么是D题的第一道分水岭
2.1 小波阈值去噪的选型理由与参数含义
D题的数据通常带有明显的测量噪声,尤其是传感器采集的时序信号。直接拿原始数据喂给GBDT,模型会把噪声当成特征学进去,导致验证集表现虚高、测试集崩盘。常见做法是先做小波阈值去噪,把高频噪声压掉,保留趋势和突变点。
小波去噪的核心参数只有三个:小波基函数、分解层数、阈值规则。小波基决定波形匹配程度,D题这类信号我一般选db4或sym8,因为它们在工程信号里对突变点的定位比较稳。分解层数不是越多越好,层数过高会把有效信号也当成噪声滤掉,通常取3到5层,具体看采样率和信号长度。阈值规则常用rigrsure或sqtwolog,前者更保守,后者去噪更狠。
下面是我在D题数据上实际用的一段Python代码,依赖PyWavelets和numpy:
import numpy as np import pywt def wavelet_denoise(signal, wavelet='db4', level=4, threshold_mode='soft'): """ signal: 一维原始信号 wavelet: 小波基,D题常用db4/sym8 level: 分解层数,建议3-5 threshold_mode: soft或hard,软阈值更平滑 """ # 小波分解 coeffs = pywt.wavedec(signal, wavelet, level=level) # 用第一层细节系数估计噪声标准差 sigma = np.median(np.abs(coeffs[-1])) / 0.6745 # 通用阈值 uthresh = sigma * np.sqrt(2 * np.log(len(signal))) # 对细节系数做阈值处理,近似系数保留 denoised_coeffs = [coeffs[0]] for c in coeffs[1:]: if threshold_mode == 'soft': c_denoised = pywt.threshold(c, uthresh, mode='soft') else: c_denoised = pywt.threshold(c, uthresh, mode='hard') denoised_coeffs.append(c_denoised) # 重构信号 return pywt.waverec(denoised_coeffs, wavelet)[:len(signal)]这段代码的逻辑是:先分解,再用最细层的细节系数估计噪声水平,因为那一层几乎全是噪声。sigma的估计用了中位数绝对偏差,比直接算标准差更抗异常值。阈值uthresh是通用阈值公式,对高斯噪声有理论支撑。软阈值把小于阈值的系数压缩到零,大于阈值的做收缩,重构出来的信号更平滑,适合后续做趋势分析。
参数怎么调:如果去噪后信号毛刺还很多,把level加一层;如果趋势被削平了,换hard阈值或者降低level。我一般会画出去噪前后的对比图,肉眼确认突变点没被抹掉再往下走。
2.2 去噪效果验证与常见误用
去噪做完不能直接信,得验证。我常用的指标是信噪比改善量和均方根误差,但这两个指标需要干净信号做参考,实际竞赛里没有。退而求其次,看去噪前后信号的一阶差分方差,噪声被压掉后这个值会明显下降,但下降太多说明过度去噪。
常见误用有三个:一是对所有通道用同一组参数,不同传感器采样率不同,分解层数必须分开定;二是把去噪后的数据直接当标签,去噪只处理特征,标签该是什么还是什么;三是忽略边界效应,小波重构在信号两端会有畸变,我一般会裁掉前2^level个点再参与建模。
提示:去噪不是必须的。如果D题数据本身信噪比很高,强行去噪反而引入伪影。先画原始信号频谱,确认高频段有明显噪声再动手。
3. 特征工程与STR分解:把时序信号变成GBDT能吃的表格
3.1 STR趋势分解的操作步骤
STR是Seasonal-Trend decomposition using Regression的缩写,本质是把时序拆成趋势项、季节项和残差项。D题里很多指标有周期性,比如按天采样的数据存在日周期,直接做回归会把周期当成趋势,系数解释不通。STR分解后,趋势项拿去做回归,季节项单独建模,残差项用来判断模型有没有漏掉信息。
Python里没有现成的STR库,我一般用statsmodels的STL做近似,或者自己写一个基于回归的分解。下面是一个简化版STR实现,用傅里叶项拟合季节:
import numpy as np import pandas as pd from sklearn.linear_model import LinearRegression def str_decompose(series, period=24, n_fourier=3): """ series: 一维时序,长度>=2*period period: 周期长度,日数据取24,周数据取7 n_fourier: 傅里叶项对数,控制季节项灵活度 """ t = np.arange(len(series)) # 构造傅里叶特征 X = np.column_stack([ np.sin(2 * np.pi * k * t / period) for k in range(1, n_fourier+1) ] + [ np.cos(2 * np.pi * k * t / period) for k in range(1, n_fourier+1) ]) # 加入线性趋势项 X = np.column_stack([t, X]) model = LinearRegression().fit(X, series) trend = model.predict(X)[:, 0] * 0 + model.coef_[0] * t + model.intercept_ seasonal = model.predict(X) - trend residual = series - trend - seasonal return trend, seasonal, residual逻辑说明:X里第一列是线性趋势,后面是傅里叶正余弦项。LinearRegression同时拟合趋势和季节,trend单独取线性部分,seasonal是拟合值减去趋势,residual是原始值减去前两者。n_fourier越大季节项越灵活,但容易过拟合,D题数据我一般取2到4。
参数怎么定:先画自相关图,看周期峰值出现在哪个滞后,那个滞后就是period。如果自相关图没有明显峰值,说明数据没有强周期性,可以跳过STR分解,直接做特征工程。
3.2 从分解结果构造GBDT特征
GBDT不能直接吃时序,必须转成表格。我一般构造这几类特征:
| 特征类型 | 具体构造 | 说明 |
|---|---|---|
| 趋势特征 | 趋势项当前值、一阶差分 | 反映长期走向 |
| 季节特征 | 季节项当前值、周期内位置 | 反映周期波动 |
| 残差特征 | 残差滚动均值、滚动标准差 | 反映局部异常 |
| 滞后特征 | 原始值滞后1/2/3期 | 给树模型提供时序记忆 |
| 统计特征 | 滚动窗口均值、最大最小值 | 窗口大小取周期长度 |
构造完特征后,用pandas拼成一张宽表,每一行是一个时间点,每一列是一个特征,最后一列是标签。标签通常是下一期的值或者下一期的变化量,看题目问什么。
注意:滞后特征和滚动特征在训练集和测试集之间不能有信息泄露。滚动窗口只能用当前时刻及之前的数据,我一般用
shift(1)把窗口整体后移一位再算。
4. GBDT建模与调参:D题里树模型到底该怎么用
4.1 GBDT在D题中的定位与选型对比
GBDT是梯度提升决策树的缩写,D题里它主要用来做回归预测和特征重要性分析。相比随机森林,GBDT对残差的迭代拟合让它在小样本上更容易拿到低偏差;相比神经网络,它不需要大量数据就能稳定收敛,而且特征重要性可以直接输出,方便写论文时解释“哪些因素影响最大”。
我常用的实现是LightGBM,因为它在几千条数据上训练速度比XGBoost快一个量级,而且原生支持缺失值。如果竞赛环境不允许装额外库,sklearn的GradientBoostingRegressor也能用,只是调参空间小一些。
选型建议:数据量小于5000条、特征数小于50,用LightGBM默认参数就能跑出不错的结果;如果特征里有大量类别变量,优先用LightGBM的categorical_feature,不要自己独热编码,树模型对类别编码的处理更高效。
4.2 可运行的训练脚本与必调参数
下面是我在D题数据上用的训练脚本,包含训练集测试集划分、早停和特征重要性输出:
import lightgbm as lgb import pandas as pd from sklearn.model_selection import TimeSeriesSplit from sklearn.metrics import mean_squared_error # df是构造好的特征宽表,最后一列是标签 X = df.drop(columns=['target']) y = df['target'] # 时序数据必须用TimeSeriesSplit,不能随机划分 tscv = TimeSeriesSplit(n_splits=5) for train_idx, val_idx in tscv.split(X): X_train, X_val = X.iloc[train_idx], X.iloc[val_idx] y_train, y_val = y.iloc[train_idx], y.iloc[val_idx] train_data = lgb.Dataset(X_train, label=y_train) val_data = lgb.Dataset(X_val, label=y_val, reference=train_data) params = { 'objective': 'regression', 'metric': 'rmse', 'boosting_type': 'gbdt', 'num_leaves': 31, # 控制树复杂度,D题数据小,31够用 'learning_rate': 0.05, # 学习率,0.05比默认0.1更稳 'feature_fraction': 0.8, # 每棵树随机选80%特征,防过拟合 'bagging_fraction': 0.8, # 每轮随机选80%样本 'bagging_freq': 5, 'min_data_in_leaf': 20, # 叶子最小样本数,小数据要调大 'verbose': -1 } model = lgb.train( params, train_data, num_boost_round=1000, valid_sets=[val_data], callbacks=[lgb.early_stopping(50), lgb.log_evaluation(100)] ) pred = model.predict(X_val, num_iteration=model.best_iteration) rmse = mean_squared_error(y_val, pred, squared=False) print(f'Fold RMSE: {rmse:.4f}') # 输出特征重要性 importance = pd.DataFrame({ 'feature': X.columns, 'importance': model.feature_importance(importance_type='gain') }).sort_values('importance', ascending=False) print(importance.head(10))逻辑说明:TimeSeriesSplit保证训练集永远在验证集之前,避免时序泄露。early_stopping(50)表示验证集50轮不提升就停,防止过拟合。num_leaves和min_data_in_leaf是控制模型复杂度的关键,D题数据量通常不大,num_leaves超过63就容易过拟合。feature_fraction和bagging_fraction是随机采样,增加模型多样性。
参数怎么调:先固定learning_rate=0.05,用early_stopping确定num_boost_round;然后调num_leaves,从31开始,每次翻倍看验证集RMSE;最后调min_data_in_leaf,如果训练集RMSE远低于验证集,把这个值调大。feature_importance用gain而不是split,因为gain反映特征对损失的实际贡献。
提示:如果验证集RMSE波动很大,检查特征里有没有未来信息。最常见的泄露是滚动窗口没有
shift,或者用了全局均值做填充。
5. 避坑与排查:D题建模过程中最容易翻车的五个地方
5.1 去噪过度导致趋势被削平
现象:去噪后信号变得过于平滑,原始数据里的突变点消失,GBDT在验证集上RMSE反而升高。原因:分解层数过高或者阈值规则选得太激进,把有效信号的高频成分也滤掉了。解决:把level降一层,或者把阈值规则从sqtwolog换成rigrsure。画出去噪前后的一阶差分对比图,如果差分方差下降超过80%,基本就是过度去噪。
5.2 STR分解的周期选错
现象:季节项看起来像随机噪声,残差项还有明显周期。原因:period参数设错了,比如日数据设成了7,或者数据本身没有周期性却强行分解。解决:先画自相关图,找第一个显著峰值对应的滞后。如果自相关图衰减很快没有峰值,直接跳过STR,用原始值做滞后特征。
5.3 GBDT训练集和验证集划分方式错误
现象:交叉验证RMSE很低,但测试集RMSE高出一大截。原因:用了train_test_split随机划分,时序数据被打乱,未来信息泄露到训练集。解决:必须用TimeSeriesSplit,而且滚动特征的计算要用shift确保只用到历史数据。如果题目要求预测未来多期,验证集要按时间顺序切最后一段。
5.4 特征重要性解读错误
现象:论文里写“特征A最重要”,但去掉特征A后模型表现没变化。原因:feature_importance默认用split次数,分裂次数多不代表贡献大。另外相关特征会互相稀释重要性。解决:用importance_type='gain',并且做一次特征剔除实验,每次去掉重要性最低的特征,看验证集RMSE变化。如果去掉后RMSE不变,说明这个特征确实没用。
5.5 忽略数据泄漏的隐蔽形式
现象:模型在训练集上表现完美,验证集一塌糊涂,但检查代码没发现明显错误。原因:用了全局统计量做填充,比如用整个数据集的均值填充缺失值,验证集的分布信息泄露到了训练集。解决:所有填充、标准化、编码操作只能在训练集上拟合,然后应用到验证集。我一般把预处理封装成Pipeline,用fit_transform和transform分开。
6. 论文图表复现:把模型结果变成评审能看懂的图
6.1 三张必画的图与绘制脚本
D题论文里,评审最先看的是图,不是公式。我一般画三张图:去噪前后对比图、特征重要性排序图、预测值与真实值对比图。这三张图能直接说明数据预处理有效、模型选对了特征、预测精度可接受。
去噪对比图用matplotlib:
import matplotlib.pyplot as plt fig, axes = plt.subplots(2, 1, figsize=(12, 6)) axes[0].plot(raw_signal, color='gray', alpha=0.7, label='Raw') axes[0].set_title('Raw Signal') axes[1].plot(denoised_signal, color='steelblue', label='Denoised') axes[1].set_title('Denoised Signal (db4, level=4, soft)') for ax in axes: ax.legend() ax.grid(alpha=0.3) plt.tight_layout() plt.savefig('denoise_compare.png', dpi=300)特征重要性图用LightGBM的输出直接画横向条形图,按gain排序取前15个特征。预测对比图把验证集的真实值和预测值画在同一张图上,再在下面画残差图。残差图如果呈现随机分布,说明模型没有系统性偏差;如果残差有趋势,说明趋势项没提取干净。
6.2 论文里怎么描述参数和结果
论文里不要只写“用了GBDT”,要写清楚参数取值和选择依据。比如:“学习率设为0.05,因为0.1时验证集RMSE波动超过15%;叶子数设为31,因为增加到63后验证集RMSE上升,出现明显过拟合。”这种描述评审一看就知道你真的调过参,不是抄的模板。
结果部分要给出具体数值,不要只写“效果较好”。比如:“去噪后信噪比改善量为6.2dB,GBDT在五折时序交叉验证上的RMSE为0.034,特征重要性前三位分别是滞后1期值、趋势项当前值、滚动标准差。”数值精确到小数点后三位就够了,太多反而显得刻意。
注意:所有图表必须在论文正文里被引用和解释,不能只贴图不说话。每张图下面写两到三句分析,说明这张图证明了什么。
6.3 我踩过的一个坑:图表配色和字体
血泪经验:论文图表用默认配色,打印出来灰度模式下根本分不清哪条线是哪条。我后来固定用seaborn的colorblind调色板,并且把线型也区分开,实线、虚线、点划线各一种。字体统一用SimHei或Arial,字号不小于10,否则评审放大看很费劲。这些细节不影响模型分数,但影响评审的第一印象。
最后一句话,我带队做建模这些年最大的习惯是:每跑完一个模型,先把中间结果存成CSV,再画图。因为论文写到一半发现某个参数要改,重新跑一遍模型只要几分钟,但重新画一遍图可能要半小时。希望帮到你。
本文还有配套的精品资源,点击获取