每年全国大学生数学建模竞赛结束后,都会有大量同学下载优秀论文来学习。拿到手之后兴奋地打开,结果发现摘要高大上、模型结构复杂、图表精致得像出版级,但真想自己动手把这篇论文“复现”一遍,却经常卡在第一步:数据不知道从哪里来,代码不知道从哪行开始写,图表也不知道怎么才能画得一样好。
最近我在系统复现 2025 年国赛 B 题优秀论文时,把整个过程整理成了一条比较清晰的路径:从读题、拆解论文,到数据重构、模型重建、代码实现,再到图表复现和结果对比。这篇文章就把这套流程完整分享出来,覆盖概念、环境、代码示例、常见坑点和最佳实践,打算备战 2026 数模国赛、或者想系统提升数学建模代码能力的同学,都可以直接照着做。
1. 背景:为什么值得复现一篇国赛B题优秀论文
1.1 复现优秀论文到底在复现什么
很多同学误以为“复现优秀论文”就是把别人代码跑一遍,或者把论文里的公式抄一遍。实际上,数学建模竞赛的优秀论文往往不公开源代码,连原始数据有时候也不完整,所以复现的真正含义是:根据论文描述的建模思路、图表结果和算法流程,重新实现一套可运行的解决方案。
一篇优秀论文的核心资产不是最后那张奖状,而是它解决问题的思路。比如它如何定义问题、如何做数据预处理、为什么选择某个模型、约束条件是怎么构造的、灵敏度分析做了哪些参数扫描。这些内容,只有在“重新实现一遍”的过程中才能真正理解。
1.2 复现对备赛有哪些实际帮助
- 提升代码能力:论文里的模型要落到 Pandas、NumPy、Scipy、Pulp 这些工具上,代码能力会快速进步。
- 形成自己的建模模板:复现两三篇优秀论文之后,你会积累一套通用流程,后续比赛可以直接复用。
- 训练图表和写作规范:优秀论文的三线表、坐标轴标注、结果对比图,值得一比一模仿。
- 培养Debug能力:复现过程中会踩到大量数据、代码、求解器相关的坑,这些经验在真正比赛时非常宝贵。
1.3 竞赛B题的特点
从历年国赛题目来看,B题通常属于“实际问题驱动型”,常见于交通、物流、能源、制造等领域。它通常包含两个核心环节:一是基于历史数据的分析与预测,二是基于预测结果的优化决策。2025 年国赛 B 题优秀论文也延续了这一框架,先构造预测模型,再建立优化模型求解方案。因此本文复现流程会围绕“预测 + 优化”这个组合展开。
2. 环境准备:复现必备工具链
2.1 系统与版本说明
本文示例使用 Python 环境,操作系统可以是 Windows、macOS 或 Linux。需要注意,数学建模类代码对版本并不敏感,建议使用 Python 3.9 及以上版本。具体库的版本不必刻意追求最新,使用当前最新稳定版即可。
推荐环境组合:
- Python 3.9+
- NumPy:数值计算
- Pandas:数据处理
- Scikit-learn:机器学习建模
- Matplotlib:数据可视化
- Pulp:线性规划与整数规划建模
- Statsmodels:时间序列分析,可选
- Openpyxl:Excel 文件读写,处理导出数据时用
2.2 安装命令
在命令行中执行:
pip install numpy pandas scikit-learn matplotlib statsmodels pulp openpyxl如果安装速度慢,可以使用国内镜像:
pip install numpy pandas scikit-learn matplotlib statsmodels pulp openpyxl -i https://pypi.tuna.tsinghua.edu.cn/simple2.3 项目目录结构
建议复现时按如下结构组织项目,这样代码更清晰,也方便后续替换数据和模型:
2025_guosai_B_reproduction/ ├── data/ │ ├── raw/ # 原始数据 │ └── processed/ # 预处理后的数据 ├── src/ │ ├── data_preprocess.py # 数据预处理模块 │ ├── model_predict.py # 预测模型模块 │ ├── model_optimize.py # 优化模型模块 │ ├── visualize.py # 可视化模块 │ └── main.py # 主流程 ├── output/ │ ├── figures/ # 输出图片 │ └── tables/ # 输出表格 └── requirements.txt在实际复现中,这个目录结构可以按需调整,但建议保持“数据、代码、输出”分离,避免所有文件堆在一起。
3. 复现一篇优秀论文的五个关键步骤
3.1 第一步:读题与拆解论文结构
拿到优秀论文后,不要直接看代码,先做两件事:
第一,回到题目本身,用自己的话复述问题。国赛 B 题通常有多个小问,每一问要解决什么、输入是什么、输出是什么,必须整理成表格。
第二,拆解论文的章节结构。优秀论文一般包含:
- 问题背景与重述
- 问题分析
- 模型假设与符号说明
- 数据预处理与特征工程
- 模型建立
- 模型求解与结果分析
- 灵敏度分析
- 模型评价与改进
在拆解过程中,建议使用荧光笔标注每一问对应的模型、输入特征、输出指标和关键约束。这些信息是后续复现的“施工图”。
3.2 第二步:数据重构与预处理
优秀论文不公开数据是常态,怎么办?通常有三个途径:
- 题目附件:国赛题目一般会提供附件,这是最基础的数据来源。
- 论文图表反推:论文中的表格和坐标图往往包含数值信息,可以读取图中坐标轴范围、曲线趋势、表格数值,反推数据量级。
- 公开数据补充:如果题目背景是交通、气象、物流等方向,可以寻找公开数据集进行替代。
拿到数据之后,按照标准流程处理:
import pandas as pd import numpy as np def load_raw_data(file_path): """读取原始数据""" df = pd.read_csv(file_path, encoding='utf-8-sig') print(f"数据维度: {df.shape}") return df def clean_data(df): """基础清洗:去重、排序、缺失值处理、异常值处理""" df = df.drop_duplicates() if 'date' in df.columns: df['date'] = pd.to_datetime(df['date']) df = df.sort_values('date') df = df.set_index('date') numeric_cols = df.select_dtypes(include=[np.number]).columns # 缺失值:数值列使用前向填充与线性插值组合 df[numeric_cols] = df[numeric_cols].fillna(method='ffill').interpolate() # 异常值:采用 3σ 原则 for col in numeric_cols: mean = df[col].mean() std = df[col].std() lower = mean - 3 * std upper = mean + 3 * std df[col] = df[col].clip(lower, upper) return df这里用到了两个常见的处理策略:
- 缺失值:时间序列数据优先使用前向填充,再对剩余空值做线性插值,保留时间趋势。
- 异常值:直接用 3σ 原则裁剪,简单稳定,适合竞赛场景。
3.3 第三步:模型重建
B 题优秀论文的核心往往是一个预测模型加一个优化模型。复现时建议先跑通简单模型,再逐步逼近论文中的复杂模型。
预测模型常见选择:
- 线性回归:可解释性强,适合作为 baseline。
- 随机森林 / XGBoost:适合表格数据,特征工程灵活。
- ARIMA / Prophet:适合有明显时间趋势的数据。
优化模型常见选择:
- 线性规划:目标函数和约束条件为线性。
- 整数规划 / 混合整数规划:包含 0-1 变量或整数变量。
- 多目标优化:将多个目标加权转化为单目标,或使用 Pareto 方法。
复现时要重点搞清楚:优化变量是什么、目标函数是什么、约束条件有哪些。这些信息通常可以从论文的模型假设和符号说明里找到。
3.4 第四步:代码实现与结果对比
实现流程建议按模块拆分,不要把所有代码写在一个文件里。每完成一个模块,就运行一次,确认输出正确。最后把预测结果、优化结果与论文中的表格、图进行对比。
对比时可以关注:
- 数值是否在同一量级。
- 曲线趋势是否一致。
- 优化结果是否满足论文中的约束条件。
- 灵敏度分析的方向和幅度是否接近。
注意,由于数据和代码实现细节不同,复现结果不可能与论文完全一致。复现的目标是“流程一致、逻辑一致、趋势一致”,而不是追求数值完全相同。
3.5 第五步:图表与论文输出
优秀论文的图表风格非常规范,复现时也应该做到:
- 使用三线表。
- 坐标轴有明确名称和单位。
- 图例清晰,字体大小合适。
- 保存图片时使用 300dpi。
后面实战部分会给出具体绘图代码。
4. 完整实战案例:预测 + 优化组合流程
下面用一个简化示例演示完整复现流程。需要注意,这里的数据是模拟生成的,目的是展示流程,不代表真实国赛题。你可以把数据替换成自己的数据,保留这套流程骨架。
4.1 生成模拟数据
import numpy as np import pandas as pd np.random.seed(42) date_rng = pd.date_range(start='2024-01-01', end='2024-12-31', freq='D') n = len(date_rng) trend = np.linspace(20, 50, n) season = 10 * np.sin(np.linspace(0, 2 * np.pi, n)) noise = np.random.normal(0, 2, n) demand = trend + season + noise df = pd.DataFrame({'date': date_rng, 'demand': demand}) df.to_csv('data/raw/demand.csv', index=False, encoding='utf-8-sig')这段代码生成了一年的日度需求数据,包含趋势项、季节项和噪声项。实际比赛中,这个文件应该是题目附件中的真实历史数据。
4.2 数据预处理模块
文件路径:src/data_preprocess.py
import pandas as pd import numpy as np def load_raw_data(file_path): df = pd.read_csv(file_path, encoding='utf-8-sig') return df def clean_data(df): df = df.drop_duplicates() df['date'] = pd.to_datetime(df['date']) df = df.sort_values('date') df = df.set_index('date') df['demand'] = df['demand'].ffill().interpolate() mean = df['demand'].mean() std = df['demand'].std() df['demand'] = df['demand'].clip(mean - 3 * std, mean + 3 * std) return df这个模块强调“职责单一”:读取和清洗分开,后面替换真实数据时不需要改动其他文件。
4.3 预测模型模块
文件路径:src/model_predict.py
预测部分使用随机森林,配合滞后特征和滚动统计特征,适合没有太多时间序列经验的同学。
import pandas as pd from sklearn.ensemble import RandomForestRegressor from sklearn.model_selection import train_test_split from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score def create_features(df, target_col='demand'): data = df.copy() data['month'] = data.index.month data['dayofweek'] = data.index.dayofweek data['lag_1'] = data[target_col].shift(1) data['lag_7'] = data[target_col].shift(7) data['rolling_mean_3'] = data[target_col].rolling(3).mean() data['rolling_mean_7'] = data[target_col].rolling(7).mean() return data.dropna() def train_predict_model(df, target_col='demand', test_size=0.2): data = create_features(df, target_col) feature_cols = ['month', 'dayofweek', 'lag_1', 'lag_7', 'rolling_mean_3', 'rolling_mean_7'] X = data[feature_cols] y = data[target_col] split_idx = int(len(data) * (1 - test_size)) X_train, X_test = X.iloc[:split_idx], X.iloc[split_idx:] y_train, y_test = y.iloc[:split_idx], y.iloc[split_idx:] model = RandomForestRegressor(n_estimators=200, random_state=42) model.fit(X_train, y_train) y_pred = model.predict(X_test) print("MAE:", round(mean_absolute_error(y_test, y_pred), 4)) print("RMSE:", round(mean_squared_error(y_test, y_pred, squared=False), 4)) print("R2:", round(r2_score(y_test, y_pred), 4)) return model, y_test, pd.Series(y_pred, index=y_test.index)这里有一个关键细节:train_test_split没有使用随机切分,而是按时间顺序切分。因为时间序列数据一旦随机打乱,就会造成“用未来预测过去”的数据泄漏,导致评估结果虚高。这一点在复现论文时尤其要注意,优秀论文都会强调时间顺序划分。
4.4 优化模型模块
文件路径:src/model_optimize.py
预测得到未来需求后,需要决策如何安排生产方案或调度方案。这里用整数规划演示一个简单的“需求分配 + 方案启动”问题。
import pulp def optimize_plan(demand_list): # 可选方案的固定成本、单位成本和容量 fixed_cost = [100, 150, 120] unit_cost = [8, 5, 9] capacity = [50, 80, 60] num_plan = len(fixed_cost) num_period = len(demand_list) model = pulp.LpProblem("Demand_Allocation", pulp.LpMinimize) # x[i][t] 表示第 i 个方案在第 t 个时段提供的需求量 x = pulp.LpVariable.dicts("x", (range(num_plan), range(num_period)), lowBound=0, cat=pulp.LpInteger) # y[i] 表示第 i 个方案是否启动 y = pulp.LpVariable.dicts("y", range(num_plan), cat=pulp.LpBinary) # 目标函数:单位成本 + 固定启动成本 model += pulp.lpSum(x[i][t] * unit_cost[i] for i in range(num_plan) for t in range(num_period)) + \ pulp.lpSum(y[i] * fixed_cost[i] for i in range(num_plan)) # 约束1:每个时段的需求必须被满足 for t in range(num_period): model += pulp.lpSum(x[i][t] for i in range(num_plan)) >= demand_list[t] # 约束2:未启动的方案不能分配任务,且分配量不能超过容量 for i in range(num_plan): for t in range(num_period): model += x[i][t] <= capacity[i] * y[i] # 求解 model.solve() if pulp.LpStatus[model.status] == 'Optimal': print("最优目标值:", pulp.value(model.objective)) plan_result = [] for t in range(num_period): row = [int(x[i][t].varValue) for i in range(num_plan)] plan_result.append(row) return plan_result else: print("求解失败,状态:", pulp.LpStatus[model.status]) return None这段代码体现了两类经典约束:
- 需求满足约束:所有方案提供的数量之和,必须大于等于预测需求量。
- 容量与启动逻辑约束:未启动的方案不能分配任务,已分配的任务不能超过该方案最大容量。
cat=pulp.LpBinary定义了 0-1 决策变量,用来表达“方案是否启动”这个逻辑。这种建模思路在国赛 B 题优化类问题中出现频率很高。
4.5 可视化模块
文件路径:src/visualize.py
Matplotlib 默认不支持中文,在复现论文图表时必须显式设置字体。
import matplotlib.pyplot as plt plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei', 'PingFang SC'] plt.rcParams['axes.unicode_minus'] = False def plot_forecast(y_test, y_pred, save_path=None): plt.figure(figsize=(10, 5)) plt.plot(y_test.index, y_test.values, label="真实值", linewidth=2) plt.plot(y_pred.index, y_pred.values, label="预测值", linestyle="--", linewidth=2) plt.title("需求预测结果对比") plt.xlabel("日期") plt.ylabel("需求量") plt.legend() plt.grid(alpha=0.3) if save_path: plt.savefig(save_path, dpi=300, bbox_inches='tight') plt.show() def plot_allocation(plan_result, demand_list, save_path=None): plan_result = np.array(plan_result) num_period = plan_result.shape[1] plt.figure(figsize=(10, 5)) bottom = np.zeros(num_period) colors = ['#4C72B0', '#DD8452', '#55A868'] labels = ['方案1', '方案2', '方案3'] for i in range(plan_result.shape[0]): plt.bar(range(num_period), plan_result[i], bottom=bottom, label=labels[i], color=colors[i], alpha=0.8) bottom += plan_result[i] plt.plot(range(num_period), demand_list, 'k--', label="预测需求", linewidth=2) plt.title("多时段需求分配方案") plt.xlabel("时段") plt.ylabel("分配量") plt.legend() if save_path: plt.savefig(save_path, dpi=300, bbox_inches='tight') plt.show()柱状堆叠图可以直观展示每个时段各方案承担了多少需求,黑色虚线表示预测需求,便于检查总供给曲线是否完全覆盖需求线。
4.6 主流程串联
文件路径:src/main.py
import sys sys.path.append('../') from src.data_preprocess import load_raw_data, clean_data from src.model_predict import train_predict_model from src.model_optimize import optimize_plan from src.visualize import plot_forecast, plot_allocation def main(): # 1. 数据加载与清洗 df = load_raw_data('data/raw/demand.csv') df = clean_data(df) # 2. 预测未来需求 model, y_test, y_pred = train_predict_model(df) # 3. 预测结果可视化 plot_forecast(y_test, y_pred, save_path='output/figures/forecast.png') # 4. 将预测结果作为优化模型的输入 demand_list = y_pred.round().astype(int).values plan_result = optimize_plan(demand_list) if plan_result: # 5. 优化结果可视化 plot_allocation(plan_result, demand_list, save_path='output/figures/allocation.png') print("复现流程运行完成") if __name__ == '__main__': main()运行主流程:
cd 2025_guosai_B_reproduction python src/main.py预期输出类似:
数据维度: (366, 2) MAE: 1.2034 RMSE: 1.5011 R2: 0.9821 最优目标值: 17350.0 复现流程运行完成实际数值会因为随机种子、数据分布、模型参数不同而变化,这里只展示输出格式。
5. 复现中的常见问题与排查思路
复现过程中,最消耗时间的往往不是算法本身,而是环境、数据和求解器带来的各种问题。下面整理了一份高频问题清单。
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
ModuleNotFoundError: No module named 'pulp' | 未安装 Pulp 库 | 执行 pip install pulp,或写入 requirements.txt 统一安装 |
| 图片中文显示为方框 | 系统缺少中文字体或 Matplotlib 未配置 | 设置 plt.rcParams['font.sans-serif'],并使用系统已安装的字体 |
| 时间序列预测 R² 很低 | 数据泄漏 / 特征工程不足 / 数据量太少 | 按时间顺序切分数据,增加滞后特征与滚动窗口特征 |
| 线性规划求解结果为 Infeasible | 约束条件之间矛盾,或变量范围设置过小 | 逐个检查约束,先去掉 0-1 启动约束测试可行性 |
| 数据中出现 NaN,建模报错 | 缺失值未处理,或滞后特征导致首行无值 | 使用 ffill + interpolate,并在建模前 dropna |
| 预测结果出现负值 | 模型没有对输出做下限约束 | 使用 np.clip(0, None) 对预测结果截断,或选择适合非负数据的模型 |
| 多次运行结果不一致 | 模型存在随机性 | 设置 random_state=42,并固定全局随机种子 |
针对最后一个问题,可以在主流程开头加上全局随机种子:
import numpy as np import random random.seed(42) np.random.seed(42)如果使用了 Scikit-learn 模型,还需要在模型初始化时设置random_state,代码中的随机森林已经这样做了。
6. 建模方法与工程实践建议
6.1 先做 Baseline,再优化模型
复现优秀论文时,不要一开始就追求和论文一模一样的复杂模型。先用线性回归或随机森林跑出一个 Baseline,确认数据、评估指标、可视化流程都正确,再逐步增加复杂度。这样做的好处是:如果后面模型出问题,可以快速判断是数据问题还是模型问题。
6.2 固定随机种子,保证结果可复现
竞赛论文和代码都强调可复现性。在项目入口处固定随机种子,不仅方便自己调试,也能让论文中的结果经得起验证。
import numpy as np import random random.seed(42) np.random.seed(42)如果使用 PyTorch 等深度学习框架,还需要额外设置torch.manual_seed。
6.3 模块化开发,避免“一个文件跑到底”
优秀论文复现通常涉及多个模型、多次实验。建议按模块拆分,每个模块只做一件事:
- 数据处理模块只负责数据读取、清洗、特征工程。
- 模型训练模块只负责训练和评估。
- 可视化模块只负责绘图和保存。
这样后期替换数据、调整参数、对比实验结果时,改动范围可控。
6.4 重视模型评估与灵敏度分析
许多同学复现论文时只关注“模型能不能跑出结果”,忽略了评估和灵敏度分析。实际上,国赛论文中灵敏度分析是评委重点看的内容之一。
通用的做法是:对模型中的关键参数进行扫描,观察目标函数或输出指标的变化趋势。例如:
def sensitivity_analysis(base_demand, step=0.05, times=5): results = [] for i in range(-times, times + 1): scale = 1 + i * step demand_adjusted = base_demand * scale plan = optimize_plan(demand_adjusted.round().astype(int)) results.append((round(scale, 2), plan)) return results如果调整参数 5% 时结果波动剧烈,说明模型稳定性不足;如果波动较小,说明模型具有一定的鲁棒性。这个分析结果可以直接用于论文中的“灵敏度分析”章节。
6.5 关于数据安全与代码规范
在竞赛准备中,使用的数据通常来自题目附件或公开数据集。复现优秀论文时,也要注意学术诚信:
- 不直接抄袭论文中的大段文字和公式,要理解后用自己的语言表达。
- 不非法获取其他参赛队伍的代码和数据。
- 引用公开数据时,说明数据来源。
- 涉及真实业务数据时,遵循最小权限原则,不传播敏感数据。
7. 总结与下一步学习路线
本文围绕 2025 年国赛 B 题优秀论文的复现,梳理了一套完整的技术路径:从读题拆解、数据处理,到预测模型、优化模型,再到可视化和常见排错。文中给出的代码虽然使用了模拟数据,但流程和真实复现完全一致。你可以把数据预处理、预测、优化、绘图四个模块直接迁移到自己的项目中使用。
下一步,建议按以下顺序继续深入:
先找一篇近两年的国赛 B 题优秀论文,按照本文的五步流程复现一遍;然后替换成题目附带的真实数据,调整特征工程和模型参数,让结果尽可能接近论文;再尝试改进论文中的模型,看能否在评价指标上超越它;最后把复现过程中积累的代码、图表模板和方法论整理成自己的建模工具箱。
复现优秀论文的价值,不只是在比赛前“临时抱佛脚”,而是真正把别人的建模思想转化为自己的工程能力。如果你在复现过程中遇到数据、代码或求解器相关的问题,欢迎收藏这篇文章,按章节索引排查。祝你备赛顺利。