数学建模竞赛必备:七大模型算法源码库实战解析
简介:这是一份面向Python数学建模学习者的算法源码合集,覆盖线性规划、排队论、微分方程、时间序列、支持向量机、预测方法和层次分析法等常见模型。压缩包共32个文件,以12个.py源码脚本为主体,辅以7个txt数据/说明文档、2个ipynb交互式笔记、2个md章节文档以及xlsx、pdf等材料,整体仅97KB。目前已有1734人学习下载,适合本科建模竞赛备赛、研究生科研入门及工程数据分析场景。资源按章节组织,每类模型均有可直接运行的Python脚本,并配套数据文件与结果文档,例如乳腺癌诊断预测、人口模型、GM11灰色预测等,有助于快速复现实验、理解算法原理,并迁移到自己的建模任务中。目录中还包含算法模型可调用封装与流程图,便于二次开发和系统学习,是快速上手与进阶实战的实用参考。 数学建模这事,说难确实难,说不难也不难。我带过的队伍里,最典型的翻车现场不是算法不会写,而是赛题读完后根本不知道该用哪个模型。三个人对着A题发愣两小时,最后抄了个神经网络硬套,评委一眼就能看出你是在"为用而用"。这份源码库覆盖的七个模块——线性规划、排队论、微分方程、时间序列、支持向量机、预测方法、层次分析法,恰好就是近几年国赛、研赛、美赛里出场频率最高的那批模型。把这套东西吃透,你至少能解决70%的建模题"该上什么模型"的问题。
1. 这套源码库解决的核心痛点:不是算法难,是"配错题"
1.1 数学建模竞赛里最常见的"算法错配"现场
先说说我见过最多的错误操作。一个是拿到优化题就写遗传算法,哪怕变量就两三个、约束全是线性的,用scipy的linprog十几行就能解完的事,非要去搞种群迭代。另一个是拿到分类题就上深度学习,样本量才两百条,特征维度倒是高得吓人,最后过拟合到训练集准确率99%、测试集一塌糊涂。还有更离谱的,排队论、微分方程这类自带明确"物理意义"的模型,很多队伍压根不知道怎么跟具体赛题挂钩,看到一个"服务台""等待时间"的描述也反应不过来该上排队论。
这就是为什么我把这套源码按"模型类型"而不是"难度等级"来组织。你拿到赛题后,先判断这是优化问题、评价问题、预测问题还是动态演化问题,然后直接到对应模块里找可改造的代码基底。源码给你的不是"标准答案",而是一个能跑的起点。
1.2 源码库整体结构与快速检索思路
整套源码的核心文件组织大致是这样:
- 线性规划模块:基于scipy.optimize.linprog与pulp封装,含标准型和混合整数型两类示例。
- 层次分析法:含判断矩阵输入、一致性检验、权重向量计算与组合权重汇总。
- 预测与时间序列:含移动平均、指数平滑、ARIMA参数搜索与残差检验。
- 微分方程建模:含odeint求解器封装,附传染病SIR模型与种群Logistic增长示例。
- 排队论:含M/M/1与M/M/c排队系统的稳态指标计算。
- 支持向量机:基于sklearn.svm,含特征缩放、网格搜索调参与分类可视化。
使用时的检索逻辑很简单:题目里有"约束""最大化/最小化""资源分配"就直奔线性规划;有"权重""专家打分""多方案比选"就去看层次分析法;有"随时间变化""增长速率""扩散趋势"就往微分方程那边靠;有"到达""等待""服务窗口"这类词就锁定排队论。后面我会把每个模块的核心代码逻辑和适用边界都过一遍。
2. 线性规划与层次分析法:最容易被低估的两个基础模块
2.1 线性规划源码:不只是会调linprog
线性规划在国赛里几乎每年都有影子。2023年C题、2022年B题,本质都是带约束的资源配置或路径优化问题。很多新手觉得线性规划"太简单",不屑于用,但实际上它的可解释性极强,评阅时最容易获得认可。
我在源码里封装了一段可复用的求解结构,核心逻辑是这样的:
from scipy.optimize import linprog
import numpy as np
def solve_lp(c, A_ub, b_ub, A_eq=None, b_eq=None, bounds=None):
# scipy的linprog默认求最小值,并且不等式统一为<=
# 如果你的目标是最大化,记得把c取负
res = linprog(c, A_ub=A_ub, b_ub=b_ub, A_eq=A_eq, b_eq=b_eq,
bounds=bounds, method='highs')
return res
这里有两个坑。第一个是 scipy 的 linprog 只做最小化,想做最大化就得把目标函数系数取负,得到结果后把目标值也取负回来。第二个是 bounds 参数的设置,不传的话默认变量非负,但有些题目里变量允许为负(比如"允许空载""允许库存不足"),这个默认值就会直接带偏结果。
更进阶一点,如果题目要求变量必须是整数,就得切换方案。 scipy 的 linprog 不支持整数规划,我把pulp的写法也放进去了:
import pulp
def solve_ilp(c, A, b, var_types='Integer'):
prob = pulp.LpProblem("ILP", pulp.LpMinimize)
n = len(c)
x = [pulp.LpVariable(f'x{i}', lowBound=0, cat=var_types) for i in range(n)]
prob += pulp.lpSum(c[i] * x[i] for i in range(n))
for row, rhs in zip(A, b):
prob += pulp.lpSum(row[i] * x[i] for i in range(n)) <= rhs
status = prob.solve()
return [pulp.value(x[i]) for i in range(n)]
2.2 层次分析法源码:判断矩阵一致性检验不能只走形式
层次分析法看起来是"填表算权重",但评阅时最容易被抓问题的点是在一致性检验上。我见过太多队伍直接拿别人论文里的判断矩阵来用,CR值验算出来大于0.1还硬着头皮往下走。
源码里我写了一个完整的AHP处理流程:
import numpy as np
def ahp_weights(matrix):
n = matrix.shape[0]
eigvals, eigvecs = np.linalg.eig(matrix)
max_eig = np.max(eigvals.real)
eigvec = eigvecs[:, np.argmax(eigvals.real)].real
weights = eigvec / eigvec.sum()
# 一致性检验
ci = (max_eig - n) / (n - 1)
ri = {1:0, 2:0, 3:0.58, 4:0.9, 5:1.12, 6:1.24, 7:1.32, 8:1.41, 9:1.45}
cr = ci / ri[n]
return weights, cr
实践经验是:当n=3或4时,判断矩阵填写要格外小心,因为RI查表值很小,CR很容易超标。我通常建议队伍先用"1-9标度法"把两两比较矩阵填出来,然后立刻在代码里跑一次CR。如果超了,不要怕改数据——直接改判断矩阵里的某几个比值,重新算权重,直到CR降下来,然后把这个"修正后"的矩阵写进论文里,说明调整过程和理由。
另外,层次分析法经常被误用成"万能评价法"。实际上它更适合指标层级清晰、数据不全、主要靠专家经验打分的场景。如果题目已经给出大量客观数据,应该优先考虑熵权法或TOPSIS,AHP更适合和它们组合使用,作为主观权重的来源。
3. 预测方法与时间序列:处理"按时间演进的数据"的完整套路
3.1 移动平均、指数平滑与ARIMA的选用逻辑
预测类题目在建模竞赛里出现频率极高,但很多队伍有一个通病:不管什么数据上来就ARIMA,甚至连数据是不是平稳的都没看。正确的思路应该是按数据特征分层选择预测方法。
如果你的数据有明显的趋势和季节性,短期预测用Holt-Winters指数平滑往往比ARIMA还好用,因为它参数少、稳定,不容易报错。源码里我把三种常用方法封在了一起:
from statsmodels.tsa.holtwinters import ExponentialSmoothing
from statsmodels.tsa.arima.model import ARIMA
def forecast_by_smooth(data, periods=10, trend='add', seasonal='add', seasonal_periods=12):
model = ExponentialSmoothing(data, trend=trend, seasonal=seasonal,
seasonal_periods=seasonal_periods)
fitted = model.fit()
return fitted.forecast(periods)
指数平滑的关键参数就三个:trend、seasonal、seasonal_periods。有趋势没周期就把seasonal设为None,有周期就要明确seasonal_periods是多少(月度数据是12,季度数据是4)。这个参数每届比赛都会有人填错。
3.2 时间序列建模中最容易忽略的平稳性检验
ARIMA的使用门槛不是拟合,而是前置检验。直接用原始数据拟合ARIMA,大概率得到的模型残差是自相关的,预测区间会失真。
源码中预测模块的第一步不是建模,而是ADF检验:
from statsmodels.tsa.stattools import adfuller
def check_stationary(series):
result = adfuller(series)
if result[1] < 0.05:
return True # 平稳,不需要差分
else:
return False # 非平稳,需要差分
def make_stationary(series, max_diff=2):
diff = 0
s = series.copy()
while not check_stationary(s) and diff < max_diff:
s = s.diff().dropna()
diff += 1
return s, diff
拿到一份时间序列数据,我的固定处理流程是:先画时序图看趋势和周期,再做ADF检验,若非平稳则差分,差分后重新检验,然后用AIC/BIC去搜索ARIMA(p,d,q)参数。这四步走完,模型的可靠性就比"直接填个(1,1,1)"高出一大截。
4. 微分方程建模:把"变化率"变成可计算的结果
4.1 从传染病模型到种群增长:什么题该用微分方程
微分方程模型在国赛里属于"用对了很出彩、用错了很尴尬"的类型。它的适用范围非常明确:题目里出现了"随时间变化的速率""扩散范围""增长趋势""感染人数变化"这类描述,并且你能写出一个合理的"变化率=输入-输出"关系式时,才值得用微分方程。
最常见的两个基础模型是人口/资源增长的Logistic模型和传染病SIR模型。传染病模型在2020年之后的赛题里几乎成了标配。源码里我给了一份完整的SIR求解示例:
from scipy.integrate import odeint
import numpy as np
def sir_model(y, t, beta, gamma):
S, I, R = y
dS = -beta * S * I
dI = beta * S * I - gamma * I
dR = gamma * I
return [dS, dI, dR]
def solve_sir(S0, I0, R0, beta, gamma, t_end=50, steps=500):
t = np.linspace(0, t_end, steps)
y0 = [S0, I0, R0]
result = odeint(sir_model, y0, t, args=(beta, gamma))
return t, result
4.2 odeint求解与参数估计的实操细节
用 odeint 时有几个细节容易出问题。一是 t 的离散点数量要足够,不然曲线不光滑;二是 sir_model 的返回值必须是一个列表,顺序要和 y 对应;三是最关键的—— beta 和 gamma 不是拍脑袋定的,需要拿真实数据去反推。
参数估计的常见做法是最小二乘拟合。把求解出的感染人数曲线和题目给出的实际感染人数做差,用 scipy.optimize.curve_fit 或 least_squares 把 beta 和 gamma 逼近到一个合理值。这套代码的核心思想不算复杂,但真正在比赛时能写出来的队伍不多。多数队伍写得出来SIR,却做不了参数拟合,导致整篇论文的模型看起来"悬在空中",没有和题目数据真正挂钩。这一块是拉开差距的关键。
微分方程模型还有一个加分技巧:做完参数拟合后,做敏感性分析。稍微改一下 beta 和 gamma 的值,看感染峰值怎么变化,然后在论文里讨论"如果采取隔离措施,beta下降20%,峰值会推迟多久"这类场景。这部分一旦写出来,评阅老师的直观感受就会从"他会套模型"变成"他真的理解了这个模型"。
5. 排队论模型:看似冷门,其实年年有题等着你
5.1 M/M/1与M/M/c的核心指标计算
排队论在本科阶段的数学建模课程里经常被一笔带过,但在实际竞赛里,凡是出现"窗口""工位""收费站""接诊台"这种服务场景,排队论就是最贴切的分析工具。
排队论的几个核心指标是:队长、排队长、等待时间、逗留时间、服务台利用率。以最基础的M/M/1模型为例,假设顾客到达率是λ,服务率是μ,要保证系统稳定必须满足λ<μ。核心公式是:
- 系统利用率ρ=λ/μ
- 队长Ls=ρ/(1-ρ)
- 平均等待时间Wq=λ/(μ(μ-λ))
源码里我直接实现了M/M/1和M/M/c两组公式:
def mm1_metrics(arrival_rate, service_rate):
rho = arrival_rate / service_rate
if rho >= 1:
return None # 系统不稳定
L = rho / (1 - rho)
Lq = rho**2 / (1 - rho)
W = 1 / (service_rate - arrival_rate)
Wq = arrival_rate / (service_rate * (service_rate - arrival_rate))
return {'rho': rho, 'L': L, 'Lq': Lq, 'W': W, 'Wq': Wq}
5.2 排队论源码的核心构成:多服务台与成本优化
单服务台排队论只是入门,真正在竞赛里出现更多是M/M/c,也就是多服务台并联。这种情况下的公式就复杂多了,涉及Erlang C公式,手算极其痛苦。源码里用循环累加实现了Erlang C的概率计算,然后在此基础上做"服务台数量优化"——即找到一个最优的c值,使得总成本(服务台运营成本+顾客等待成本)最小。
这个优化思路相当实用。比如某题目要决定仓库开几个出货口,你就需要估算每个口每小时运营成本,以及每个顾客等待一分钟的损失成本。然后对c从1到10遍历,看总成本曲线的最低点落在哪,取那个c作为最优解。这个"排队论+成本优化"的组合套路,写进论文里既严谨又有应用味。
6. 支持向量机:数学建模里的"分类天花板"
6.1 什么时候用SVM,什么时候应该换随机森林
支持向量机在数学建模中一直很受评委欢迎,原因是它的数学原理清晰(间隔最大化、核函数映射),论文里好写推导过程。但SVM不是什么分类问题都好用。根据我这些年的使用经验,以下几个场景优先选SVM:
- 特征维度中等(几十到几百维),样本量不大(几百到几千条),SVM泛化能力好。
- 类别线性可分或近似线性可分,线性核就够用,训练速度快。
- 需要高可解释性,你能画出决策边界和支撑向量来辅助论文配图。
反过来,如果你的样本量很大(几万条以上),或者特征非常稀疏(比如文本TF-IDF向量),SVM的训练速度和调参难度就会上来,这时候换随机森林或XGBoost更合适。另外,SVM天然是个二分类器,做多分类要靠"一对一"或"一对多"策略,sklearn底层默认使用一对一,这个细节在论文里写清楚会显得很内行。
6.2 sklearn中SVM的关键参数与调参心得
源码里的SVM模块用到了完整的"标准化+网格搜索+交叉验证"流程:
from sklearn.svm import SVC
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import GridSearchCV, train_test_split
from sklearn.pipeline import Pipeline
def svm_train(X, y, kernel='rbf'):
pipe = Pipeline([
('scaler', StandardScaler()),
('svm', SVC(kernel=kernel, probability=True))
])
params = {'svm__C': [0.1, 1, 10, 100],
'svm__gamma': [0.001, 0.01, 0.1, 1]}
grid = GridSearchCV(pipe, params, cv=5, scoring='f1_macro')
grid.fit(X, y)
return grid.best_estimator_, grid.best_params_
这里有几个细节值得强调:
- 特征缩放是SVM的生死线。SVM是基于距离的模型,如果不做标准化,量纲大的特征直接主导决策边界。Pipeline里先放Scaler再放SVM是标准做法。
- 参数搜索范围不要拍脑袋。C是误分类惩罚,越大越容易过拟合;gamma(仅对RBF核)控制单个样本的影响半径,越大越容易过拟合。网格搜索时最好用对数间隔取值,像上面这样0.1到100按10倍步长,隔一个量级取一个值。
- 多分类的SVM在sklearn里调参没区别,但训练时会额外生成多个二分类子模型,论文里解释算法原理时需要注意这一点。
SVM的可视化也是论文加分项。二维特征可以用 plot_decision_regions 画分类边界,三维特征可以做3D散点图加投影,这部分代码我也放进了源码库,比赛时直接改特征列就行。
7. 多算法组合实战:一份答卷如何把多个模型串起来
7.1 一个综合评价题的标准解法链路
把源码库里的算法混在一起用,才是数学建模竞赛里真正的"正确姿势"。单独跑一个模型很难覆盖一道完整赛题的所有小问,通常需要"数据预处理→模型A选特征→模型B做核心计算→模型C做验证"这样的链路。
以我之前带过的一个物流站点选址题目为例,完整链路是这样的:
第一问是区域需求分析。先用层次分析法构建"需求潜力"评价体系,算出一级指标权重;然后用熵权法对客观数据再算一组权重,最后把AHP的主观权重和熵权法的客观权重做加权组合,得到综合权重,排序选出需求热点区域。这段主要用AHP模块。
第二问是配送时间预测。给出历史订单数据,要求预测未来高峰期订单量。这里先用时间序列模块做ADF检验和平稳化,再用ARIMA搜索最优参数,预测曲线画出来后,和真实的吞吐量做对比验证。如果你觉得序列有周期性,可以切到Holt-Winters跑一版,两版结果对比着写。
第三问是调度优化。在已知各区域需求的前提下,要设计配送路线使总成本最低。这就回到了线性规划模块,把约束条件列成矩阵,用linprog或整数规划求解。如果规模太大,就先用聚类把配送点分群,再对每一群内部做线性规划。
最后,如果想加一个"方法论亮点",在需求分类部分插入支持向量机,用历史数据训练一个分类器预测某区域属于"高需求""中需求"还是"低需求",再把SVM分类结果和AHP排序结果做一致性对比。整篇论文从评价、预测、优化到机器学习都有了,模型的丰富度和衔接性都会上一个台阶。
7.2 代码组织与论文写作如何互相配合
多算法组合对代码组织提出了更高要求,不然后面写论文时根本找不到对应结果。我的个人习惯是每个模块单独一个脚本,入口文件用函数封装,跑完自动把结果存成CSV或JSON,论文里的表格直接引用这些输出数据。不要在一个notebook里从头写到尾,比赛到第三天你会发现找某个中间结果会占用大量时间。
论文方面,每个模型必须回答三个问题:为什么选这个模型、输入是什么、输出怎么解释。像线性规划的单纯形法、SVM的间隔最大化这类理论推导,在论文中写两三行公式点明即可,重点应该放在赛题数据和模型之间的映射关系——约束条件怎么来的、核函数为什么选RBF、层次分析法的一致性比率为什么达标。这些内容才是评阅老师关注的核心。
8. 竞赛实战中的源码使用心得与常见翻车点
8.1 用"最小可行代码"跑通再优化,别一上来追求完美
我的经验是:第一天下午必须把每个小问的"最小可行模型"跑出结果。哪怕粗糙一点,先拿到一个数值,后面再逐步加细节。最怕的是三个人讨论了两天"要不要用神经网络""要不要加约束",到第三天晚上还在调参,最后提交前只能硬凑一版结果进论文。
源码库给你打底就是为了避免这个局面。线性规划和层次分析法这种基础模块,拿到题目当天就该直接套用跑通。
8.2 数据可视化不是装饰,是评阅老师快速理解你模型的最佳入口
同一道题,两篇论文模型完全一样,但一篇附带清晰的决策边界图、预测曲线对比图、排队系统利用率热力图,另一篇全是文字描述,分数差距会很大。源码库里每个模块我都配了对应的可视化函数:SVM画决策边界,预测模块画历史与预测对照曲线,排队论画服务台数量与总成本曲线,层次分析法画权重条形图。
一个小技巧:论文里的图不要只放模型输出,要"图上加标注"。比如预测曲线图上用阴影标注95%置信区间,SVM图上把支持向量画成空心圆,排队论成本曲线上把最优解点标红。这些细节会让图的信息量瞬间上一个档次。
8.3 源码不是终点,落地前务必做的三件事
拿到这套源码,建议做三件小事再上赛场。第一,换一套自己的数据测试每个模块能否跑通——注意不是换列名,而是换数据量和数据范围,确认边界情况不会报错。第二,把每个模块的输入输出格式背下来,这样比赛时改数据不用翻代码。第三,挑两道以往的真题,完整地跑一遍"评价+预测+优化"的串联流程,计时练习,培养节奏感。
实际带队过程中我注意到一个很有意思的现象:很多队伍比赛前一个算法一个算法地看,觉得每个都看懂了,但真到比赛时一组合就乱套。原因就是没做组合演练。三个模块单独跑都通,拼在一起后数据格式对不上、单位不一致、函数接口不匹配,这种问题要在赛前暴露出来,而不是赛场上解决。
我个人用过这套流程后最大的体会是:数学建模竞赛比的不是谁会的高级算法多,而是谁能在有限时间内用合适的模型解决题目里的实际问题。把基础模型的原理吃透、边界摸清、代码跑熟,比囤积一堆花哨但用不上的算法库有效得多。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐
所有评论(0)