简介:数学建模算法与应用源码包以Python实现,面向数学建模竞赛参与者、数据分析学习者及科研人员,覆盖线性规划、排队论模型、微分方程建模、时间序列模型、支持向量机、预测方法和层次分析法等常用模型。压缩包共三十二个文件,以十二个Python脚本为主体,另有七个文本数据文件、四个XML配置、两个Markdown说明、两个Jupyter交互式笔记及Excel结果、PDF图形等,整包约97KB,轻量便于快速部署。已有1740人学习下载,参考价值较为突出。内容包含乳腺癌诊断预测、单服务台排队模型、人口微分方程、移动平均时间序列等实战案例,配套数据文件与交互式笔记可让读者直接运行、调整参数,快速复现典型建模场景。目录按章节模块清晰组织,并附有模型原理说明、算法扩展和可视化图形,适合从算法理解到Python实现的系统学习。
1. 为什么这套 Python 数学建模算法与应用源码值得你跑一遍
数学建模比赛进行到最后一晚,你手里同时压着七套算法要跑通:线性规划安排生产计划,排队论计算窗口服务效率,微分方程模拟传播过程,时间序列预测下季度的销量,支持向量机给样本分类,层次分析法给方案打分排序。那时候最崩溃的不是算法推导没思路,而是项目里的代码写成了黑匣子——函数紧耦合,数据格式硬编码,换一批数据就得从 main 函数开始改。这套 Python 数学建模算法与应用源码,就是为了解决这个尴尬整理的:七个核心模块全部以赛题场景为入口组织,每个目录有独立入口脚本和说明,拿到手可以直接改参数重跑。适合备战建模竞赛的学生、准备用 Python 替换 MATLAB 做实验的老手,以及不想在 SciPy 文档和调试器之间反复横跳的开发者。
2. 源码结构拆解:七个算法模块加一个工具底座
拿到一份源码压缩包,我一般先不看 readme,而是把整棵目录树扫一遍。目录布局直接决定后续排查问题的路径:按算法散着放还是按赛题聚合,改起来的体感完全不同。这份源码属于后者——每个顶层目录都能直接对应一类常见赛题,而不是一个抽象算法名。
2.1 目录先扫一遍:按赛题粒度组织的模块布局
源码采用了“问题场景优先、算法实现次之”的组织方式,顶层是数字前缀的七个算法目录,每个目录内有 readme.md 说明对应题型和入口脚本,然后是按解法拆分的若干 .py 文件。整体结构如下:
math_modeling_algorithms/ ├── 01_linear_programming/ # 线性规划、整数规划、运输问题 │ ├── lp_production_plan.py # 生产计划优化:连续变量求解 │ ├── ip_knapsack.py # 0-1 背包:整数规划求解 │ ├── transport_problem.py # 运输问题:产销平衡与最小费用 │ └── readme.md ├── 02_queueing_theory/ # 排队论模型 │ ├── mm1_mms_metrics.py # M/M/c 指标计算 │ ├── queue_simulation.py # 离散事件仿真验证 │ └── readme.md ├── 03_differential_equations/ # 微分方程建模 │ ├── logistic_growth.py # 种群增长逻辑斯蒂模型 │ ├── predator_prey.py # 捕食者-被捕食者方程组 │ └── sir_epidemic.py # SIR 传播模型 ├── 04_time_series/ # 时间序列模型 │ ├── stationarity_check.py # ADF 平稳性检验 │ ├── arima_fit.py # ARIMA 拟合与预测 │ └── forecast_evaluate.py # 回测与误差评估 ├── 05_svm/ # 支持向量机 │ ├── svm_classifier.py # 分类:RBF 核默认配置 │ ├── svm_regressor.py # 回归:SVR 实现 │ └── grid_search_cv.py # 参数网格搜索与交叉验证 ├── 06_prediction_methods/ # 预测方法:回归 / 插值 / 灰色预测 │ ├── linear_regression.py # 多元线性回归 │ ├── interpolation_predict.py # 插值预测 │ └── grey_model.py # 灰色 GM(1,1) ├── 07_ahp/ # 层次分析法 │ ├── ahp_calculate.py # 权重计算(几何平均法) │ ├── consistency_check.py # 一致性校验:CI 与 CR │ └── ahp_visualization.py # 权重可视化 ├── utils/ │ ├── data_loader.py # 统一数据读入:csv / excel / json │ ├── standardize.py # z-score 与 min-max 标准化 │ └── result_export.py # 结果写出为 CSV / Markdown └── requirements.txt这棵树传达了两个信息:第一,每个模块都是可直接运行的,进入算法目录后按 readme 描述跑入口脚本,就能看到该算法在赛题数据上的完整输出,不需要在整个项目里猜函数定义位置;第二,01 到 07 的编号顺序暗含了备赛节奏——01 到 03 偏运筹与连续模型,04 到 06 偏统计与机器学习,07 是评价决策类模型,基本覆盖了建模竞赛里八成以上的题型。
2.2 公共工具层:统一数据读入、标准化与结果导出
我建议你从 utils 开始读,再去看算法文件。几乎所有算法模块的入口都调用 utils.data_loader 读数据,这个公共层的价值在于把“读数据”这件破事收敛到一处,避免每个脚本各自写一份参数不统一的文件解析。
# utils/data_loader.py import pandas as pd def load_data(path, index_col=None, sheet_name=0): """统一数据入口,自动识别 csv / excel / json 格式""" if path.endswith(".csv"): df = pd.read_csv(path, index_col=index_col) elif path.endswith((".xlsx", ".xls")): df = pd.read_excel(path, index_col=index_col, sheet_name=sheet_name) elif path.endswith(".json"): df = pd.read_json(path) else: raise ValueError(f"未知数据格式: {path}") return df这段代码的逻辑很直接:按后缀路由到 pandas 的读取函数,index_col 控制哪一列作为行索引,sheet_name 指定 Excel 工作表,调用方只需传数据路径并约定好索引列即可。这样设计还有一个好处——当赛题数据从 Excel 换成 CSV 时,算法模块代码一行不用改。
utils/standardize.py 提供 z-score 和 min-max 两种标准化算子,SVM 和回归模块都会用到。utils/result_export.py 则负责把计算结果统一写入 result/ 目录下的 CSV 或 Markdown 表格。这点在写建模论文时是实打实的后悔药:所有中间结果都沉淀为文件,论文里的数据表直接引用,不用再担心某个关键数值被终端日志刷掉。
2.3 依赖环境:requirements 版本锚定是先决条件
源码附带一份 requirements.txt,关键库的最低版本都锁住了。我复现别人项目时有个习惯:一定先建全新的虚拟环境再装依赖,绝不用全局环境的旧版本硬跑。原因很实际——scikit-learn 从 1.0 到 1.2 的部分参数默认值不一致,statsmodels 的 ARIMA 接口在 0.12 到 0.14 之间改动更大,不锁定版本,结果差异大到会让你误以为代码有 bug。
python -m venv .venv source .venv/bin/activate # Windows 用 .venv\Scripts\activate pip install -r requirements.txt依赖装完后按 01 到 07 的顺序跑入口脚本,前面模块依赖问题更少,能先把基础环境验证掉,再逐步试后面更复杂的模型。这一步走完,你才算真正拿到了一份可以自由改动的建模工具箱。
3. 传统建模模块拆解:线性规划、排队论和微分方程的工程实现
这一章对应源码里 01 到 03 三个目录,它们属于建模竞赛中出现频率最高的部分。这里不讲算法公式推导,只看代码怎么落地、参数怎么调、结果怎么解读。
3.1 线性规划:从 linprog 连续求解到整数规划的解法切换
线性规划模块的典型场景是生产计划优化。假设某装配线生产两种产品,每件 A 耗 2 工时和 1 单位原料,每件 B 耗 1 工时和 1 单位原料,总工时 10、总原料 8、B 的产能上限 7,要求最大化利润 4A + 3B。源码里的写法如下:
# 01_linear_programming/lp_production_plan.py import numpy as np from scipy.optimize import linprog # scipy 的 linprog 默认做最小化,最大化利润要取负 c = np.array([-4.0, -3.0]) # 约束条件写成 A_ub @ x <= b_ub 的形式 A_ub = np.array([ [2.0, 1.0], # 工时约束:2A + B <= 10 [1.0, 1.0], # 原料约束:A + B <= 8 [0.0, 1.0] # 产能约束:B <= 7 ]) b_ub = np.array([10.0, 8.0, 7.0]) result = linprog(c, A_ub=A_ub, b_ub=b_ub, bounds=[(0, None), (0, None)], method="highs") if result.success: a, b = result.x print(f"最优产量: A={a:.2f}, B={b:.2f}") print(f"最大利润: {-result.fun:.2f}") else: print("求解失败:", result.message)参数说明:c 是目标函数系数向量,因为 linprog 只做最小化,所以利润目标要取负号;A_ub 和 b_ub 定义不等式约束,默认方向是小于等于;bounds 设置每个变量的取值范围,这里两个变量都非负;method 选 "highs" 是调 HiGHS 求解器,相比老版本默认的单纯形法在处理大规模约束时稳定得多。
连续变量模型跑通后,你会发现竞赛题经常需要整数解,比如设备台数不能是 2.7 台。此时 linprog 就无能为力了,源码里用 PuLP 来解决:
# 01_linear_programming/ip_knapsack.py import pulp prob = pulp.LpProblem("knapsack", pulp.LpMaximize) # 三个 0-1 决策变量:选或不选 x1 = pulp.LpVariable("item_A", cat="Binary") x2 = pulp.LpVariable("item_B", cat="Binary") x3 = pulp.LpVariable("item_C", cat="Binary") # 目标:最大化总价值 15 + 10 + 8 prob += 15*x1 + 10*x2 + 8*x3 # 容量约束:5*x1 + 3*x2 + 2*x3 <= 6 prob += 5*x1 + 3*x2 + 2*x3 <= 6 prob.solve() print([(v.name, v.varValue) for v in prob.variables()])这里的关键是 cat="Binary" 把变量声明为 0-1 整数,PuLP 会调用内置的 CBC 求解器做分支定界。连续和整数模型的取舍在竞赛里是个高频考点:如果题目允许小数解,用 linprog 秒出;一旦涉及“多少个”“几台”这种离散量,必须切到整数规划。源码在同一目录里放两种解法,方便你直接对比同一组数据下的连续最优值和整数最优值差距。
3.2 排队论模型:指标公式计算与离散事件仿真双轨验证
排队论的代码分两层:第一层直接用 Erlang-C 公式算 M/M/c 排队系统的稳态指标,第二层用离散事件仿真验证理论值。为什么要两层?因为竞赛题经常出现非标准假设,比如到达不是泊松过程、服务时间不是指数分布,公式失效时仿真还能兜底。
# 02_queueing_theory/mm1_mms_metrics.py import math def mmc_metrics(arrival_rate, service_rate, servers=1): """M/M/c 排队系统关键指标公式计算""" rho = arrival_rate / (servers * service_rate) if rho >= 1: raise ValueError(f"服务强度 rho={rho:.2f} >= 1,系统会无限排队") # 计算系统空闲概率 p0,这是 Erlang-C 的前置步骤 sum_part = sum((servers * rho)**k / math.factorial(k) for k in range(servers)) last_part = (servers * rho)**servers / (math.factorial(servers) * (1 - rho)) p0 = 1 / (sum_part + last_part) # 顾客需要等待的概率和平均排队队长 p_wait = last_part * p0 lq = p_wait * rho / (1 - rho) wq = lq / arrival_rate # 平均等待时间 w = wq + 1 / service_rate # 平均逗留时间 = 等待 + 服务 ls = arrival_rate * w # 平均队长 return { "rho": rho, "p0": p0, "p_wait": p_wait, "ls": ls, "lq": lq, "ws": w, "wq": wq }arrival_rate 是单位时间平均到达顾客数,service_rate 是单个服务台单位时间平均服务人数,servers 是服务台数量。rho 必须小于 1,否则队列理论不存在稳态,这一点我在赛题里踩过:数据给出来时没检查强度,后面算出的全是负数指标。公式算完,再用仿真去核对:
# 02_queueing_theory/queue_simulation.py import random def simulate_mmc(arrival_rate, service_rate, servers=2, sim_time=10000): """离散事件仿真 M/M/c,返回平均等待时间与最大排队长度""" server_free_times = [0.0] * servers clock = 0.0 total_wait = 0.0 total_customers = 0 max_queue = 0 while clock < sim_time: # 到达间隔服从指数分布 clock += random.expovariate(arrival_rate) service_time = random.expovariate(service_rate) # 找最早空闲的服务台 earliest = min(server_free_times) idx = server_free_times.index(earliest) if earliest <= clock: # 顾客到达时服务台空闲,直接服务 server_free_times[idx] = clock + service_time else: # 所有服务台都忙,进入排队 total_wait += earliest - clock max_queue += 1 server_free_times[idx] = earliest + service_time total_customers += 1 return { "sim_avg_wait": total_wait / total_customers, "sim_max_queue": max_queue }仿真函数的入参和公式版本一致,sim_time 是仿真总时长。它通过不断生成到达事件和服务时间,模拟服务台的状态流转,最后统计平均等待时间。我的习惯是先用公式快速算一遍标准解,再改参数做仿真对比;如果两组结果偏差超过 5%,就检查是不是随机种子没固定或者热身期没排除。固定随机种子可以让仿真结果可复现,这在比赛论文中尤其重要。
3.3 微分方程建模:odeint 数值积分与多方程系统写法
微分方程模块的核心是 scipy.integrate.odeint,它是把连续问题离散化的通用入口。以种群增长逻辑斯蒂模型为例:
# 03_differential_equations/logistic_growth.py import numpy as np from scipy.integrate import odeint def logistic(pop, t, r, K): """逻辑斯蒂增长模型: dP/dt = r * P * (1 - P/K)""" return r * pop * (1 - pop / K) p0 = 10.0 # 初始种群数量 t = np.linspace(0, 100, 500) # 模拟 100 个时间单位 r = 0.3 # 内禀增长率 K = 500.0 # 环境容纳量 sol = odeint(logistic, p0, t, args=(r, K))odeint 的签名是 odeint(函数, 初值, 时间轴, args=参数元组),要求函数第一个参数是状态变量,第二个是时间,然后返回 dP/dt。注意这里微分方程函数必须像 logistic 一样写成“输入当前状态、返回变化率”的形式,这是所有 scipy 积分器的共同约定。我在讲这部分时经常强调:不要试图在函数里直接打印或计步,积分器内部会多次调用这个函数,你只需要保证它纯粹。
同样的模式可以扩展到多方程系统,比如捕食者-被捕食者模型:
def predator_prey(state, t, alpha, beta, delta, gamma): prey, predator = state dprey_dt = alpha * prey - beta * prey * predator dpredator_dt = delta * prey * predator - gamma * predator return [dprey_dt, dpredator_dt] state0 = [50.0, 10.0] t = np.linspace(0, 50, 500) sol = odeint(predator_prey, state0, t, args=(0.5, 0.02, 0.01, 0.3))这里的状态参数变成一个长度为 2 的列表,返回值也是两个导数的列表,对应关系和单个方程完全一致。SIR 模型在源码里也是这个套路,只是状态变量换成 S、I、R 三个。处理多方程时参数特别容易传错顺序,我的习惯是先固定写一行注释标注每个参数的含义,再检查返回值长度是否和状态长度一致。
4. 避坑手记:跑这套源码最翻车的五个实践问题
从源码跑通到真正用在自己赛题上,中间隔着无数细节。这一章记录我实际调试中遇到过的问题,每条都是“现象 → 原因 → 解决”的完整链路,希望能帮你省掉排查的几个晚上。
4.1 linprog 报 Phase 1 simplex failed
现象:约束看起来完全合理,但 linprog 返回的 message 里写着 "Phase 1 simplex failed",result.success 为 False。
原因:绝大多数是可行域为空,即约束条件互相矛盾。比如你前一个约束写了 x + y <= 8,后一个又写成 x + y >= 10,组合后没有任何点能同时满足。另一个常见来源是 bounds 和 A_ub 冲突,例如 bounds 里写了 x <= 5,A_ub 却又有 2x <= 6。
解决:把每个约束单独注释,逐个放开跑一次,先定位到哪个约束导致无解。然后检查目标函数符号:linprog 只做最小化,如果你按最大化习惯写了系数,需要全部取负。最后把所有约束全部删除、只留 bounds,确认能解出平凡解,再逐条加回去。
4.2 odeint 输出满屏 nan,系统直接崩溃
现象:微分方程求解结果全是 nan,或者报 "REAL BIGNUM",程序当场崩掉。
原因:通常是参数尺度失衡。比如种群模型的初值 p0 给到 100000,但 K 只有 500,增长率 r 又很大,微分方程右侧瞬间产生超大数值,积分器步长控制不住。另一个更隐蔽的原因是刚性系统——方程里同时存在变化极快和极慢的分量,默认的 lsoda 算法没能自适应切换。
解决:先把初值调整到和参数同一量级,比如种群模型里 p0 取 K 的十分之一;然后加密时间轴 t 的网格,np.linspace 的步长不要超过系统特征时间的一半。如果还是炸,改用 scipy.integrate.solve_ivp 并指定 method="Radau",对刚性系统更稳。最后再检查微分方程函数里的系数有没有写反,比如正反馈项写成了负反馈。
4.3 SVM 归一化后精度判若云泥——数据泄漏的锅
现象:同一份数据,不归一化直接跑 SVM,交叉验证准确率只有 0.6,归一化后突然跳到 0.95。
原因:SVM 的 RBF 核在计算样本间距离时,数值范围大的特征完全主导了核函数输出,小量纲特征的信息被吞掉。所以归一化在这里不是可选项,而是必选项。
解决:用 sklearn 的 Pipeline 把 StandardScaler 和 SVC 串起来,保证归一化始终包裹在交叉验证的每个 fold 内部。这里有个常见误用:先对整个数据集台 StandardScaler 的 fit_transform,再做 train_test_split,这会把测试集的均值方差泄漏进训练过程,导致交叉验证结果虚假偏高。正确的做法是先切分,再用训练集 fit、对测试集只做 transform。
4.4 ARIMA 差分阶数选错,预测退化成一条直线
现象:时间序列取了二阶差分 d=2 后,ARIMA 拟合得很好,AIC 也很低,但预测结果几乎是一条水平直线,置信区间大得没意义。
原因:过度差分抹掉了序列里的确定性趋势。原始序列如果本身只是均值漂移,一阶差分就能平稳;做二阶差分等于把趋势信息全差分掉了,模型只剩随机噪声的截距,预测自然是一条水平线。
解决:不要靠猜,先运行源码里的 stationarity_check.py,用 ADF 检验算 p 值。p 值大于 0.05 就差分一次再检验,直到平稳为止,这样可以确定最小的 d。选 p 和 q 时结合 ACF/PACF 图和 AIC 一起看,并检查残差的 Ljung-Box 检验 p 值是否大于 0.05——只要残差还有自相关结构,说明模型没把信息提取干净。
4.5 AHP 一致性比例 CR 怎么调都超 0.1
现象:手工构造的两两比较矩阵,一致性比例 CR 算出来 0.3 甚至更高,反复微调数值都降不到 0.1 以下。
原因:判断矩阵人工打分时,多个比较对之间存在传递性矛盾。比如你写了 A 比 B 重要 3 倍、B 比 C 重要 2 倍,但 A 比 C 只写了 4 倍,理论上应该是 6 倍,这就破坏了矩阵的一致性。
解决:先用 ahp_calculate.py 输出每个比较对的偏差,找到偏差最大的那个元素,把整行整列重新审视一遍。如果阶数不超过 5,几何平均法算出的权重通常比特征值法更稳定。另一个实用技巧是调循环:把最大偏差元素往“理论值”方向收敛,重新计算 CR,一般两三轮就能压到 0.1 以下。如果实在降不下来,回去重新设计层级结构,把比较维度拆得更细。
5. 机器学习与预测模块实战:SVM、时间序列与层次分析法
04 到 07 目录是统计和评价类模型的集中区。这三个部分在竞赛里通常是“最后一公里”,用来对前面模型的结果做分类、预测或排序。它们的共同点是代码短,但参数选择和验证方法决定了最终分数。
5.1 支持向量机:Pipeline 装载数据标准化和网格搜索
SVM 分类模块的默认实现用 make_pipeline 把标准化和分类器打包,避免了每次手动 fit 标准化器再传给模型的繁琐。RBF 核会把样本映射到高维特征空间,几何上看是找一个能最大化间隔的超平面,这正是 SVM 处理线性不可分数据的核心优势。
# 05_svm/svm_classifier.py from sklearn.svm import SVC from sklearn.preprocessing import StandardScaler from sklearn.pipeline import make_pipeline from sklearn.model_selection import GridSearchCV # 标准化 + RBF 核 SVM,开启概率输出 pipe = make_pipeline( StandardScaler(), SVC(C=1.0, kernel="rbf", gamma="scale", probability=True) ) # 参数网格搜索:C 控制误分类惩罚,gamma 控制 RBF 的径向范围 param_grid = { "svc__C": [0.1, 1, 10], "svc__gamma": ["scale", 0.01, 0.1] } grid = GridSearchCV(pipe, param_grid, cv=5, scoring="f1") grid.fit(X_train, y_train) print(f"最佳参数: {grid.best_params_}") print(f"最佳交叉验证分数: {grid.best_score_:.4f}")C 越大对误分类的容忍越小,容易过拟合;gamma 越大影响半径越小,决策边界越曲折。网格搜索的关键是参数名前要加 "svc__" 前缀,因为 pipeline 里第一层是 StandardScaler,第二层才是 SVC,sklearn 用双下划线分隔 estimator 名称和参数名。交叉验证设为 5 折,评分指标选 f1 而不是 accuracy,主要因为建模赛题里样本经常类别不平衡,accuracy 被多数类主导时没有参考价值。
5.2 时间序列模型:平稳性检验、ARIMA 拟合与回测验证
时间序列模块的标准流程是:先做 ADF 平稳性检验,不平稳就差分,再拟合 ARIMA,最后做回测。源码把这几步拆成了独立文件,方便你替换中间任何一步。
# 04_time_series/arima_fit.py from statsmodels.tsa.arima.model import ARIMA from statsmodels.tsa.stattools import adfuller def check_stationarity(series, alpha=0.05): """返回是否平稳以及 p 值""" p_value = adfuller(series)[1] return p_value < alpha, p_value def fit_arima(train, order=(1, 1, 1)): """train: pd.Series, order: (p, d, q)""" model = ARIMA(train, order=order) result = model.fit() return result # 使用流程 # ok, p = check_stationarity(sales) # if not ok: # sales_diff = sales.diff().dropna() # result = fit_arima(sales_diff, order=(2, 0, 2)) # forecast = result.forecast(steps=5)ARIMA 的 order 中 d 是差分阶数,p 是自回归阶数,q 是移动平均阶数。statsmodels 的新版接口已经用 ARIMA(train, order=...) 替代了旧的 ARIMA(train, order=...).fit() 的写法,如果你网上搜到老教程,注意接口差异。回测部分用 rolling window 思想:把序列最后 N 个点当作测试集,模型只在之前的数据上拟合,然后逐期向后滚动预测。
def backtest_arima(series, order, horizon=5): """拿最后 horizon 个点做测试,前面的做训练""" split = len(series) - horizon train, test = series[:split], series[split:] model = ARIMA(train, order=order).fit() pred = model.forecast(steps=horizon) return pred, test回测保证了训练和测试在时间上不重叠,这一点比随机交叉验证更适合时间序列——时间序列随机打乱会引入未来数据,让评估结果虚高。我一般在回测里同时输出 MAE 和 RMSE,如果 MAE 明显小于 RMSE,说明有少量时间点预测偏差特别大,需要回头查是不是有异常点干扰了差分。
5.3 层次分析法:判断矩阵到权重输出的完整链路
AHP 模块的作用是把定性比较转化为定量权重,源码里的实现同时计算权重和一致性指标。这里用几何平均法而不是特征值法,因为手工矩阵经常不完美一致,几何平均法对偏差的容忍度更高,结果也更容易解释。
# 07_ahp/ahp_calculate.py import numpy as np # 随机一致性指标 RI,按矩阵阶数查表 RI_TABLE = {1: 0, 2: 0, 3: 0.58, 4: 0.90, 5: 1.12, 6: 1.24, 7: 1.32, 8: 1.41, 9: 1.45, 10: 1.49} def ahp_solve(matrix): """matrix: n x n 两两比较矩阵,返回权重、最大特征值、一致性比例 CR""" n = len(matrix) # 几何平均法求权重向量 row_prod = matrix.prod(axis=1) weights = row_prod ** (1 / n) weights = weights / weights.sum() # 用权重反推最大特征值 aw = matrix @ weights lambda_max = (aw / weights).mean() # 一致性指标 CI 与一致性比例 CR ci = (lambda_max - n) / (n - 1) cr = ci / RI_TABLE.get(n, 1.49) return weights, lambda_max, cr # 示例:3 个方案在性价比、可靠性、易用性上的两两比较 judgement = np.array([ [1, 3, 5], [1/3, 1, 2], [1/5, 1/2, 1] ]) w, lam, cr = ahp_solve(judgement) print(f"权重: {w}") print(f"CR 一致性比例: {cr:.3f}")流程说明:先对判断矩阵的每一行求乘积,开 n 次方,再归一化得到权重;然后用权重反算最大特征值,进而得到 CI,最后除以 RI 得到 CR。RI 是平均随机一致性指标,查表得到,n 越大临界值也越大。CR 小于 0.1 表示矩阵一致性可接受,否则需要回到上一章避坑部分的修正流程。
| 矩阵阶数 n | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
|---|---|---|---|---|---|---|---|---|---|---|
| RI 值 | 0 | 0 | 0.58 | 0.90 | 1.12 | 1.24 | 1.32 | 1.41 | 1.45 | 1.49 |
这个权重结果可以直接用于竞赛论文的方案排序或综合评分,输出格式也已经兼容 utils/result_export.py,一个语句就能导出为 Markdown 表格。
6. 二次开发技巧:把这套源码变成你自己的建模工具箱
源码跑通只是起点,真正有价值的部分是把它改造成自己的工具箱。这一章给三个我实践下来最有效的改造方向。
6.1 用配置文件驱动参数,摆脱硬编码
原版代码的参数写在脚本里,换成新数据就要打开文件改数字。我建议把模型参数抽到 YAML 或 JSON 配置文件里,让数据路径、超参数、开关项都集中管理。
# config/svm_config.yaml svm: C: 2.0 kernel: rbf gamma: scale cv_folds: 5 data: path: data/credit_score.csv target: is_default然后在算法脚本里用 PyYAML 读入参数,再把配置对象传给建模函数。这样比赛换赛题时,只需要新增一个配置文件,不用碰任何算法代码。对整个项目维护来说是质的提升。
6.2 新增一个算法模块的最少步骤
如果你想往这套源码里塞新的算法,比如随机森林,按现有组织方式的惯例来做:
创建08_random_forest/目录,放入入口脚本rf_classifier.py,保证脚本接受数据路径和参数字典,输出结果并调用 utils/result_export.py 落盘;在目录里补一份 readme.md,说明适用题型和使用方法;在 requirements.txt 中补充新增依赖;最后跑通一遍并把输出记录为基线。这四个步骤做完,新模块和旧模块在结构上完全对齐,后续接任何赛题都不需要特殊对待。
6.3 凡是修改必先跑基线
我后来养成了一个习惯:每次拿到新的建模源码,第一件事不是看算法,而是先把整套跑一遍,记录每个模块的标准输出。后续改任何一行代码,都拿新输出和基线对比——如果结果变了但没达到预期,立刻能定位是当前这次改动引入的;如果结果没变,说明改动没影响现有逻辑。这套源码给了我从零搭建一个自己的建模工具的起点,也让我在时间紧迫的赛题里少走很多弯路。希望帮到你。
本文还有配套的精品资源,点击获取