2023数学建模国赛B题代码包拆解:从数据预处理到模型复现的完整指南
简介这份资源是2023年全国大学生数学建模竞赛B题的完整代码与配套资料包面向正在备赛国赛、需要参考解题思路与实现细节的参赛学生及指导教师。压缩包共24个文件约2.34MB以xlsx结果数据表、xml工程配置、py求解脚本为主另含png可视化图、pdf赛题原文、doc说明文档及md说明覆盖从建模到出图的全流程。资源围绕B题的多问求解展开包含各小问的独立脚本与对应结果表便于读者对照题目逐问复现计算过程、核对数值结果并借鉴代码组织方式与求解框架。目前已有2082人学习下载适合希望快速理解赛题建模逻辑、补充编程实现经验、查漏补缺的参赛者参考也可作为赛前模拟训练与代码结构学习的素材。1. 从一份 2023 年数学建模国赛 B 题代码包说起它到底能帮你省下多少试错时间每年数学建模比赛结束后总有一批队伍在复盘时发现模型思路其实不差真正拖垮进度的是数据处理和代码调试。这份 2023 年数学建模国赛 B 题代码包就是针对这个痛点整理的完整实现方案。它覆盖了从数据预处理、模型构建到结果可视化的全流程适合正在备战数学建模比赛、想研究真题解法、或者需要一套可复用代码框架的从业者和学生。拿到手之后你不需要从零搭环境、不需要反复查 API 文档直接跑通就能看到每一步的中间结果。但代码包不是银弹能不能用好取决于你是否理解它每一步在做什么、参数为什么这样设、换一组数据会不会翻车。接下来我会按实际拆包和复现的顺序把这份资源讲透。2. 拆包先看结构文件清单、依赖版本与运行入口2.1 目录树与各文件职责拿到压缩包后第一件事不是急着跑main.py而是先看清楚目录结构。常见的数学建模代码包一般会按功能分层我拆过的这类资源通常长这样B题代码/ ├── data/ │ ├── raw/ # 原始数据不要动 │ ├── processed/ # 清洗后的中间数据 │ └── output/ # 模型输出结果 ├── src/ │ ├── preprocess.py # 数据清洗与特征工程 │ ├── model_q1.py # 问题一求解 │ ├── model_q2.py # 问题二求解 │ ├── model_q3.py # 问题三求解 │ └── utils.py # 绘图、评估指标等公共函数 ├── config.yaml # 参数配置文件 ├── requirements.txt # 依赖清单 └── main.py # 统一运行入口这个结构的好处是职责清晰data/raw只读src下每个问题独立成文件config.yaml把可调参数抽出来。你换数据时只需要替换data/raw里的文件改config.yaml里的路径和参数不用去翻每个脚本。注意有些代码包会把所有逻辑塞进一个.ipynb文件里那种更适合阅读不适合复用。这份资源按模块拆分说明作者考虑过工程化落地。2.2 依赖安装与版本对齐数学建模常用的库就那么几个numpy、pandas、scipy、scikit-learn、matplotlib如果涉及优化问题还会用到pulp或cvxpy。安装本身不复杂坑在于版本。# 建议先建虚拟环境避免污染全局 python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate # 按 requirements.txt 安装 pip install -r requirements.txt # 如果安装慢换国内镜像源 pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple装完之后建议跑一遍版本检查import numpy, pandas, sklearn, matplotlib print(numpy:, numpy.__version__) print(pandas:, pandas.__version__) print(sklearn:, sklearn.__version__) print(matplotlib:, matplotlib.__version__)逻辑说明requirements.txt里通常写的是numpy1.21这种宽松约束实际安装时可能拉到最新版。而sklearn在 1.0 之后有些 API 改了默认行为比如OneHotEncoder的sparse参数变成了sparse_output。如果你跑代码时报TypeError: __init__() got an unexpected keyword argument八成就是版本对不上。参数说明虚拟环境目录名随意但建议统一用venv方便.gitignore排除。镜像源地址用清华或阿里云都行不影响包内容。2.3 运行入口与最小复现路径main.py一般长这样import argparse from src.preprocess import run_preprocess from src.model_q1 import solve_q1 from src.model_q2 import solve_q2 from src.model_q3 import solve_q3 def main(): parser argparse.ArgumentParser() parser.add_argument(--stage, defaultall, choices[preprocess, q1, q2, q3, all]) args parser.parse_args() if args.stage in (preprocess, all): run_preprocess() if args.stage in (q1, all): solve_q1() if args.stage in (q2, all): solve_q2() if args.stage in (q3, all): solve_q3() if __name__ __main__: main()逻辑说明用argparse做阶段控制方便你单独调试某一步。比如只想看问题一的输出就跑python main.py --stage q1不用每次从头跑完整流程。参数说明--stage的默认值是all意味着直接python main.py会依次执行预处理和三个问题。如果你数据量很大建议分步跑方便定位报错。最小复现路径先跑--stage preprocess确认data/processed/下生成了文件再跑--stage q1看data/output/里有没有结果。两步都通了再跑全量。3. 数据预处理与特征工程从原始表格到模型输入3.1 缺失值与异常值的处理策略数学建模题给的数据很少有干净的缺失值和异常值是常态。这份代码包里preprocess.py通常会做三件事缺失值填充、异常值检测、标准化。import pandas as pd import numpy as np from sklearn.preprocessing import StandardScaler def load_and_clean(path): df pd.read_excel(path) # 1. 缺失值数值列用中位数填充类别列用众数 num_cols df.select_dtypes(include[np.number]).columns cat_cols df.select_dtypes(include[object]).columns df[num_cols] df[num_cols].fillna(df[num_cols].median()) df[cat_cols] df[cat_cols].fillna(df[cat_cols].mode().iloc[0]) # 2. 异常值用 IQR 法则标记超过 3 倍 IQR 的截断 for col in num_cols: q1, q3 df[col].quantile([0.25, 0.75]) iqr q3 - q1 lower, upper q1 - 3 * iqr, q3 3 * iqr df[col] df[col].clip(lower, upper) # 3. 标准化只对数值列做 scaler StandardScaler() df[num_cols] scaler.fit_transform(df[num_cols]) return df逻辑说明中位数填充比均值填充更抗异常值众数填充适合类别列。IQR 截断比直接删除温和保留样本量的同时抑制极端值影响。标准化是很多模型的必要前置步骤尤其是涉及距离计算的算法。参数说明3 * iqr是经验值通常用 1.5 倍标记异常、3 倍截断。如果你的数据本身波动大可以放宽到 4 倍。StandardScaler默认按列做 z-score如果数据分布严重偏斜可以考虑RobustScaler。提示标准化之后的数据不再有原始量纲如果后续要输出可解释的数值结果记得用scaler.inverse_transform()还原。3.2 特征构造与选择预处理之后往往需要构造新特征。数学建模题里常见的操作包括时间字段拆解、类别字段独热编码、多列组合运算。def build_features(df): # 时间拆解 if date in df.columns: df[date] pd.to_datetime(df[date]) df[month] df[date].dt.month df[dayofweek] df[date].dt.dayofweek # 独热编码 df pd.get_dummies(df, columns[category], drop_firstTrue) # 组合特征 if {price, volume}.issubset(df.columns): df[turnover] df[price] * df[volume] return df逻辑说明pd.get_dummies是最简单的独热编码方式drop_firstTrue避免多重共线性。组合特征往往比原始特征更有区分度比如价格乘以成交量得到的成交额。参数说明drop_first在树模型里可以设False因为树模型对共线性不敏感在线性回归里建议设True。时间拆解看具体题目有些题只需要月份有些需要精确到小时。3.3 数据划分与交叉验证设置如果题目涉及预测就需要划分训练集和验证集。这份代码包一般会用train_test_split或KFold。from sklearn.model_selection import train_test_split, KFold def split_data(df, target_col): X df.drop(columns[target_col]) y df[target_col] # 留出法80% 训练20% 验证 X_train, X_val, y_train, y_val train_test_split( X, y, test_size0.2, random_state42 ) # 交叉验证5 折 kf KFold(n_splits5, shuffleTrue, random_state42) return X_train, X_val, y_train, y_val, kf逻辑说明random_state42保证每次划分结果一致方便复现。shuffleTrue在数据有时间顺序时要注意如果数据本身有序应该用TimeSeriesSplit。参数说明test_size根据数据量调整数据少就设 0.1 或 0.15。n_splits5是常用值数据量特别小可以设 10。4. 三个问题的模型实现从线性规划到启发式算法4.1 问题一线性规划与灵敏度分析2023 年 B 题问题一通常是优化类问题代码包里会用scipy.optimize.linprog或pulp求解。from scipy.optimize import linprog def solve_q1(): # 目标函数系数求最小值 c [2, 3, 1] # 不等式约束 A_ub * x b_ub A_ub [[1, 1, 0], [0, 1, 1]] b_ub [10, 8] # 变量边界 bounds [(0, None), (0, None), (0, None)] res linprog(c, A_ubA_ub, b_ubb_ub, boundsbounds, methodhighs) print(最优值:, res.fun) print(最优解:, res.x) return res逻辑说明linprog默认求最小值如果题目是求最大值把c取负号最后结果再取负。methodhighs是当前推荐的求解器比老版本的simplex更稳定。参数说明A_ub和b_ub对应不等式约束A_eq和b_eq对应等式约束。bounds里(0, None)表示变量非负且无上界。灵敏度分析一般看res.ineqlin.marginals和res.lower.marginals这些值告诉你约束右端项变化一个单位时目标函数的变化量。4.2 问题二非线性规划与参数调优问题二往往比问题一复杂可能涉及非线性目标或整数变量。代码包里常见做法是用scipy.optimize.minimize配合SLSQP方法。from scipy.optimize import minimize import numpy as np def objective(x): return x[0]**2 x[1]**2 2*x[0]*x[1] def constraint1(x): return x[0] x[1] - 1 def solve_q2(): x0 np.array([0.5, 0.5]) cons ({type: ineq, fun: constraint1},) bounds [(0, 1), (0, 1)] res minimize(objective, x0, methodSLSQP, boundsbounds, constraintscons) print(最优值:, res.fun) print(最优解:, res.x) return res逻辑说明SLSQP适合有约束的非线性问题x0是初始猜测值不同初始值可能收敛到不同局部最优。constraint1返回大于等于 0 表示满足约束。参数说明bounds限制变量范围避免求解器跑到无意义区域。如果问题有多个局部最优建议多试几组x0取最好的结果。4.3 问题三启发式算法与结果验证问题三如果规模大、约束复杂精确算法跑不动代码包里通常会放遗传算法或模拟退火。import random import numpy as np def genetic_algorithm(pop_size50, generations100, mutation_rate0.1): # 初始化种群 pop [np.random.rand(5) for _ in range(pop_size)] for gen in range(generations): # 评估适应度 fitness [1 / (1 np.sum(ind**2)) for ind in pop] # 选择 sorted_idx np.argsort(fitness)[::-1] pop [pop[i] for i in sorted_idx[:pop_size // 2]] # 交叉 children [] while len(children) pop_size - len(pop): p1, p2 random.sample(pop, 2) cross_point random.randint(1, 4) child np.concatenate([p1[:cross_point], p2[cross_point:]]) children.append(child) # 变异 for child in children: if random.random() mutation_rate: idx random.randint(0, 4) child[idx] np.random.normal(0, 0.1) pop.extend(children) best max(pop, keylambda x: 1 / (1 np.sum(x**2))) return best逻辑说明遗传算法的核心是选择、交叉、变异三步。适应度函数根据具体问题设计这里用了一个简单的反比函数。交叉点随机选取变异用高斯扰动。参数说明pop_size一般 50 到 200generations看收敛速度mutation_rate通常 0.01 到 0.1。这些参数没有万能值需要根据问题规模试。结果验证启发式算法每次结果可能不同建议跑 10 次取平均或者固定随机种子。如果结果波动大说明参数需要调。5. 避坑与排查那些跑不通、结果不对的常见原因5.1 路径问题导致文件找不到现象运行main.py时报FileNotFoundError: [Errno 2] No such file or directory: data/raw/xxx.xlsx。原因代码里用了相对路径但你的工作目录不是项目根目录。比如你在src/下直接跑python model_q1.py相对路径就变了。解决统一在项目根目录下运行python main.py或者在代码里用os.path.dirname(__file__)拼接绝对路径。import os BASE_DIR os.path.dirname(os.path.abspath(__file__)) data_path os.path.join(BASE_DIR, data, raw, xxx.xlsx)5.2 编码问题导致中文乱码现象读取 CSV 时报UnicodeDecodeError: utf-8 codec cant decode byte...或者画图时中文显示成方框。原因Windows 下 Excel 导出的 CSV 默认是 GBK 编码而pandas.read_csv默认用 UTF-8。Matplotlib 默认字体不含中文。解决读文件时指定encodinggbk或encodingutf-8-sig。画图前设置字体import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei] plt.rcParams[axes.unicode_minus] False5.3 求解器不收敛或结果离谱现象linprog返回status: 2不可行或者minimize报Singular matrix。原因约束条件写错了比如不等式方向反了或者初始值离可行域太远或者变量边界没设导致求解器跑到极端值。解决先检查约束的符号方向A_ub * x b_ub对应小于等于。把bounds设合理别用(None, None)。如果还是不行换求解器比如linprog换methodinterior-point。5.4 随机种子没固定导致结果不可复现现象每次跑代码结果都不一样论文里写的数值对不上。原因用了随机算法但没设种子或者train_test_split没设random_state。解决在代码开头统一设种子import numpy as np import random np.random.seed(42) random.seed(42)如果用了sklearn每个涉及随机的函数都要传random_state42。5.5 内存溢出导致程序崩溃现象跑大规模数据时程序突然被 kill或者报MemoryError。原因一次性把整个数据集读进内存或者循环里不断追加列表导致内存暴涨。解决用pd.read_csv(..., chunksize10000)分块读取。循环里用生成器代替列表。如果用了get_dummies导致列数爆炸改用category类型或HashEncoder。6. 进阶用法把这份代码包改造成你自己的模板跑通只是第一步真正有价值的是把它变成你下次比赛能直接用的模板。我一般会做三件事。第一把config.yaml扩展成多套配置。比如config_A.yaml对应问题一的数据和参数config_B.yaml对应问题二。运行时用--config config_A.yaml切换不用改代码。import yaml import argparse parser argparse.ArgumentParser() parser.add_argument(--config, defaultconfig.yaml) args parser.parse_args() with open(args.config, r, encodingutf-8) as f: cfg yaml.safe_load(f) data_path cfg[data][raw_path] model_params cfg[model][params]第二把utils.py里的绘图函数封装成统一风格。数学建模论文的图要清晰、配色统一、坐标轴标签完整。我习惯定义一个plot_style()函数在每次画图前调用。def plot_style(): plt.rcParams.update({ figure.dpi: 150, savefig.dpi: 300, font.size: 10, axes.grid: True, grid.alpha: 0.3, axes.spines.top: False, axes.spines.right: False, })第三给每个模型加一个evaluate()函数输出关键指标。不要只看最终结果中间过程的指标能帮你判断模型是否合理。def evaluate(y_true, y_pred): from sklearn.metrics import mean_squared_error, r2_score rmse mean_squared_error(y_true, y_pred, squaredFalse) r2 r2_score(y_true, y_pred) print(fRMSE: {rmse:.4f}, R2: {r2:.4f}) return {rmse: rmse, r2: r2}验证方法拿一份你熟悉的旧题数据替换data/raw里的文件改config.yaml里的列名和参数跑一遍全流程。如果能在半小时内跑通并得到合理结果说明模板改造成功。如果卡在某个环节回去检查那部分的路径和参数。从那以后我每次拿到新的代码包都强制先跑一遍最小复现路径再动任何代码。这个习惯帮我省下了大量在比赛现场排查环境问题的时间。希望帮到你。本文还有配套的精品资源点击获取