资讯动态

优秀论文复现:熵权TOPSIS与优化预测Python流水线

发布时间:2026/10/27 11:42:01 来源:尧图企业网站定制
简介这份PDF为第八届MathorCup数学建模竞赛C题优秀论文聚焦陆基导弹打击航母的轨道建模与算法设计适合备赛学生、数模爱好者及需要参考军事运筹案例的读者研读。全文围绕静止目标与移动目标分别建立分段静态轨道模型和引入三维坐标的动态轨道模型并借助MATLAB绘轨、Excel生成散点、SPSS绘制三维图最后讨论风速温度等系统误差、随机误差及指令延迟下的命中率计算。压缩包仅含1个PDF文件约511KB篇幅紧凑却保留完整摘要、问题分析、模型假设、公式推导与结果展示便于按目录快速定位。已有107人浏览学习可作为竞赛论文写作、模型构建和软件实现思路的对照范本。1. 数学建模竞赛优秀论文的价值在于可复现的建模骨架而不是结论拿到一份编号 C3289 的数学建模竞赛优秀论文多数人的第一反应是翻到最后一页找结论、把公式抄进自己的模板。恰好相反真正有价值的是它如何把一个模糊的现实问题拆解成可计算的结构哪些指标被留下、哪些假设被砍掉、模型为什么这样拼、结果靠什么检验。竞赛论文的结论往往只对那一道题有效可这套拆解流程能迁移到下一个题目、下一份业务需求上。这篇笔记要做的就是把 C3289 这类论文的骨架逐层剥出来再用 Python 重建一条能跑通、能改参数、能复现表格里每一个数字的流水线。适合正在备赛、需要在几天内交出一份前后自洽方案的同学也适合想把业务问题量化落地的工程同学。2. 拆开 C3289 的论文骨架从摘要往下倒推建模路线图优秀论文和普通论文的第一个分水岭在目录之前就已经出现。普通论文是「先写背景再堆模型最后凑结论」优秀论文是「先用一句话锁定问题类型再用一张表锁定输入输出最后才动笔写模型」。C3289 这类被选出来的稿子几乎都能在摘要里找到一条完整的因果链因为要解决什么所以选了哪些指标因为指标有这个性质所以用了这个模型因为求解出了这个结果所以给出这个建议。复现的第一步不是抄代码是把这条链还原成一张自己能照着做的施工图。2.1 摘要里的四句话目标、指标、模型、结论把摘要按句号切开逐句标出动词你会发现有价值的句子只有四类动词刻画、建立、求解、检验。「刻画」后面跟的是指标体系「建立」后面跟的是模型结构「求解」后面跟的是算法和工具「检验」后面跟的是灵敏度或误差分析。凡是找不到这四类动词的句子基本都是背景铺垫复现时可以整句跳过。我一般会做一张这样的解构表左边抄原句右边写「我要准备什么」摘要句子类型典型动词对应的复现动作需要准备的输入问题刻画选取、构建、刻画列出指标清单和方向原始数据表、指标单位模型建立建立、引入、改进写出目标函数与约束符号表、假设条件模型求解采用、求解、迭代选定库与算法参数求解器、初值、终止条件结果检验检验、对比、扰动复跑一组对照实验对照组方案、扰动幅度这张表填完一篇二十页的论文就被压缩成了四行。剩下十几页的公式推导作用只有一个证明这四行是自洽的其中任何一行对不上说明你还没读懂它。判断有没有读懂有个很土但有效的办法把解构表盖住只看原始数据你能不能自己说出「我要算出一个什么形状的结果」。说不出来说明摘要里那句「建立」你只是认字没认出结构。2.2 把假设与符号表翻译成数据字典论文里的「模型假设」那一段很多人当成凑字数跳过。实际上它是一份隐式的数据契约。假设写「不考虑运输损耗」意思就是你的数据表里不能出现损耗列假设写「各指标相互独立」意思就是你后面不能拿相关系数去挑指标否则和假设自相矛盾。符号表则决定了每个变量的类型和量纲这一层不落地代码写到一半一定会出问题。我的做法是先写一份 schema把符号表直接翻译成程序能校验的结构读数据时顺手把单位、缺失、异常一起卡住import pandas as pd # 符号表 - 数据字典列名、类型、单位、是否参与建模 SCHEMA { obj_id: (str, None, False), # 对象编号不参与计算 cost: (float, 万元, True), # 符号表里的 c1极小型 output: (float, 吨, True), # 符号表里的 c2极大型 coverage: (float, %, True), # 符号表里的 c3极大型 risk: (float, 分, True), # 符号表里的 c4极小型 } def load_and_check(path, schema): df pd.read_csv(path) report [] for col, (dtype, unit, used) in schema.items(): if col not in df.columns: report.append(f[缺失列] {col}) continue if dtype float: df[col] pd.to_numeric(df[col], errorscoerce) na int(df[col].isna().sum()) if na: report.append(f[空值] {col}: {na} 行) report.append(f[OK] {col} 单位{unit} 参与建模{used} f范围[{df[col].min()}, {df[col].max()}]) return df, report df, report load_and_check(raw.csv, SCHEMA) print(\n.join(report))这段代码的价值不在读文件在于把「假设」变成了可执行的检查。errorscoerce会把「12吨」「约30」这类脏值变成 NaN从而暴露出来而不是在后面求熵值时悄悄变成字符串拼接。unit字段看着多余但它是量纲统一的唯一凭证一旦有人在cost列里混进「元」你从 min/max 的量级差就能一眼看出来。最后那个used标记是为了让「假设里排除掉的字段」在代码里也有名字防止后面有人顺手把它加进指标矩阵。跑完这一步你应该能回答三个问题指标一共几个、每个的方向是什么、有没有缺失或超量纲。回答不了就先别往下走后面所有的排序结果都会建立在一个不确定的输入上。2.3 题型与模型对照表三步锁定技术路线数学建模题看起来五花八门落到可执行的层面其实就五类。C3289 这类优秀论文的第二个共同点是模型选得「刚好够用」——不上来就搬神经网络也不为了显得朴素而硬用小学算术。选型的底层逻辑是模型复杂度必须和数据的支撑能力匹配样本只有十几个对象你还上深度学习评委一眼就能看出这是包装。题目特征读题时抓这几句题型首选模型备选与适用边界「给出若干对象的多项指标要求排序/分级/优选」评价类熵权法 TOPSISAHP指标少且需专家判断、灰色关联样本极少、模糊综合评价指标含定性描述「在资源/预算/产能限制下使某目标最大或最小」优化类线性/整数规划遗传算法、粒子群目标非凸或含离散逻辑、动态规划多阶段决策「根据历年数据预测未来若干期」预测类灰色 GM(1,1)小样本ARIMA平稳序列、指数平滑趋势平稳、回归有明确驱动因素「给出节点与连接关系求路径/流量/覆盖」图论类最短路、最小生成树、最大流旅行商问题需启发式「机理清晰变化率与当前量相关」微分方程类常微分方程组 数值解差分方程离散时间三步锁定法第一步读题找上面第一列的关键句第二步数样本量样本小于 15 且没有分布假设时优先灰色和 TOPSIS 这类不依赖大样本的方法第三步看有没有硬约束只要有「不超过」「至少」「必须满足」就是优化类别硬套评价模型。选型还有一个容易被忽略的判据备选模型之间必须能互相校验。评价类用熵权算权重就该用等权或 AHP 再算一遍做对比预测类用 GM(1,1)就该留出最后两期做回测。单一模型跑出来的数字没有说服力两个模型结论一致才是证据。这一点在后面几章会反复用到。3. 评价类主模型的复现熵权法与 TOPSIS 的完整代码链评价类是这类论文里出现频率最高的一类也是复现时最容易「看起来对、一排序就错」的一类。整条链只有三步把不同方向的指标掰到同一个方向、用信息熵算出各指标的权重、用正负理想解算出每个对象的贴近度。三步都不难难在边界条件——标准化后出现 0、指标方向写反、正负理想解取错轴任何一个都会让最终排名整体平移一到两位而这一到两位恰好是论文里最关键的结论。这一章按「正向化 → 赋权 → 排序 → 校验」的顺序给完整实现每一段都能单独跑也能拼成一条流水线。3.1 指标正向化与标准化三个最容易写错的边界正向化的本质是把「越小越好」「越靠近某个值越好」「落在某个区间最好」统一变成「越大越好」。数学上很直白工程上有三个坑极小型指标用max - x还是1/x中间型指标的「中点」怎么定标准化之后出现 0 会不会让后面的对数炸掉。import numpy as np def normalize(X, kindsNone): X: (n_objects, n_indicators) 原始指标矩阵行是对象列是指标 kinds: 每个指标的类型 pos 极大型越大越好 neg 极小型越小越好 (mid, a, b) 中间型最佳落在 [a, b] 的中点附近 (range, a, b) 区间型落在 [a, b] 内即为最优 返回: 正向化并归一化后的矩阵 Z取值压缩在 [0.001, 0.999] X np.asarray(X, dtypefloat) n, m X.shape kinds kinds or [pos] * m Z np.zeros_like(X) for j in range(m): col X[:, j] kind kinds[j] if kind pos: Z[:, j] col elif kind neg: Z[:, j] col.max() - col elif isinstance(kind, tuple) and kind[0] mid: a, b kind[1], kind[2] mid (a b) / 2.0 M max(abs(col.max() - mid), abs(col.min() - mid)) Z[:, j] 1.0 - np.abs(col - mid) / M elif isinstance(kind, tuple) and kind[0] range: a, b kind[1], kind[2] M max(a - col.min(), col.max() - b) Z[:, j] np.where(col a, 1.0 - (a - col) / M, np.where(col b, 1.0 - (col - b) / M, 1.0)) # min-max 归一化并平移到 [0.001, 0.999] span Z.max(axis0) - Z.min(axis0) span[span 0] 1.0 # 整列相同的指标避免除零 Z (Z - Z.min(axis0)) / span * 0.998 0.001 return Zkinds用字符串和元组混合表达是为了让「指标方向」这件事在代码里显式存在而不是散落在注释里。(mid, a, b)里的M是这一列到中点的最大偏离距离除以它之后最靠近中点的对象得 1最远的得 0量纲自动消掉。区间型用np.where做三段判断落在区间内直接给满分这个处理比「换算成到区间端点的距离」更符合业务直觉。末尾那行* 0.998 0.001是整套代码里最关键的一行。归一化后每列必然出现至少一个 0就是最小值而熵权法要算log(P)P里一旦有 0 就是-inf最终整个权重向量变成 NaN。平移之后最小值是 0.001对数有定义代价是指标区分度被压缩千分之一不影响排序。span 0的保护同样必要如果某个指标在所有对象上取值完全一样它在评价里就不携带任何信息强行归一化会得到一列 NaN 并污染所有结果。提示kinds的顺序必须和原始数据列顺序严格一致。我在第一次复现时就因为把「成本」和「产出」的顺序写反得到了一份排名完全倒置的结果还以为是模型有问题。3.2 熵权求权重从信息熵到权重的完整实现熵权法的逻辑只有一句话某个指标在对象之间分布得越分散它携带的区分信息越多权重就该越大。数学上先算每个指标的信息熵e越分散越接近 1越均匀越接近 0再用1 - e得到差异系数d最后把d归一化就是权重。def entropy_weight(Z): Z: 正向化 归一化后的指标矩阵要求所有元素 0 返回: (w 权重向量, e 信息熵, d 差异系数) n, m Z.shape P Z / Z.sum(axis0, keepdimsTrue) # 每个指标下各对象的比重 e -np.sum(P * np.log(P), axis0) / np.log(n) # 信息熵取值范围 [0, 1] d 1.0 - e # 差异系数 w d / d.sum() # 归一化权重 return w, e, d def topsis(Z, w): 加权 TOPSIS返回贴近度 C越大越优、到正理想解距离、到负理想解距离 V Z * w # 加权规范化矩阵 v_pos V.max(axis0) # 正理想解每列最大 v_neg V.min(axis0) # 负理想解每列最小 d_pos np.sqrt(((V - v_pos) ** 2).sum(axis1)) d_neg np.sqrt(((V - v_neg) ** 2).sum(axis1)) C d_neg / (d_pos d_neg 1e-12) # 加极小量防止除零 return C, d_pos, d_negP Z / Z.sum(axis0)是按列归一化axis0这个参数写错成axis1是最常见的翻车点因为按行归一化在语法上完全合法只是含意变成了「每个对象内部各指标的占比」算出来的熵和权重毫无意义。口诀指标在列上所以永远axis0。np.log(P)除以np.log(n)是在做归一化让e落在[0, 1]区间。如果忘了除熵值会随对象数量 n 变化同一份数据换个规模权重就变了论文里就没法写「权重由数据客观确定」。topsis里 1e-12是防御性写法。理论上d_pos d_neg不会为 0除非所有对象在所有指标上完全相同但真实数据里真出现过某一列全等导致d_pos d_neg 0的情况加了极小量至少能返回 0.5 而不是 NaN方便你回头定位问题。3.3 TOPSIS 贴近度与排序校验跑出贴近度只是拿到了一列数论文里真正的结论是排名。这时候必须做两件事一是换一种权重方案重算看排名是否稳定二是换一种排序方法重算看方法本身是否影响结论。两个都稳才敢把排名写进结论。import pandas as pd from scipy.stats import spearmanr Z normalize(X_raw, kinds[neg, pos, pos, neg]) m Z.shape[1] w_entropy, e, d entropy_weight(Z) # 方案一熵权 w_equal np.ones(m) / m # 方案二等权 C_entropy, _, _ topsis(Z, w_entropy) C_equal, _, _ topsis(Z, w_equal) score_sum (Z * w_entropy).sum(axis1) # 方案三加权和法 print(熵权:, np.round(w_entropy, 4)) print(信息熵:, np.round(e, 4)) # 排名一致性两种方法排出来的顺序有多大差别 rho1, _ spearmanr(C_entropy, C_equal) rho2, _ spearmanr(C_entropy, score_sum) print(f熵权TOPSIS vs 等权TOPSIS 秩相关: {rho1:.4f}) print(f熵权TOPSIS vs 加权和法 秩相关: {rho2:.4f}) rank pd.Series(C_entropy).rank(ascendingFalse).astype(int)spearmanr算的是秩相关系数两端 1.0 表示两种方案给出的排名完全一致0.9 以上说明头部稳定、尾部可能换位。经验上秩相关低于 0.85 就应该警觉不是模型不稳而是权重差异太大这时候更该回到 3.1 检查指标方向有没有写错而不是硬解释为什么两个方案结论不同。pd.Series(...).rank(ascendingFalse)返回的是「名次」不是得分。这一步有个细节当两个对象贴近度完全相等时rank默认给它们相同名次的平均值比如并列第 3 会得到 3.5写进论文表格时要说明这一点否则读者会以为你算错了。到这里评价类的链条就完整了normalize出矩阵、entropy_weight出权重、topsis出贴近度、spearmanr出可信度。四个函数的输入输出都是数组或 DataFrame没有任何隐藏状态随便挑一段单独跑都能复现。4. 优化与预测模块把论文里的「求解」写成能跑的脚本论文正文里最常见的两种「求解」段落一种是在约束下求极值一种是根据历史推未来。这两种在纸面上都能写成漂亮的公式落到代码里却各自有一套坑。优化类的坑在约束的方向和变量的界预测类的坑在样本量和模型适用区间。这一章给出的写法全部围绕一个目标一次运行直接产出论文表格里的每一个数字。4.1 线性与整数规划scipy 与 pulp 两种写法对照小规模线性规划用scipy.optimize.linprog就够了不用装额外依赖一旦出现「只能取整数」「0-1 变量」「分段条件」这类逻辑约束就该换pulp因为手写整数约束进 linprog 的矩阵形式极易出错而且调试成本远高于多装一个库。import numpy as np from scipy.optimize import linprog # linprog 统一求最小值最大化问题把目标系数取负 c np.array([-3.0, -5.0]) # 目标max 3x1 5x2 A_ub np.array([[1.0, 2.0], [3.0, 1.0]]) # 约束左侧系数 b_ub np.array([8.0, 12.0]) # 约束右侧上限 bounds [(0, None), (0, None)] # 每个变量的取值范围(下界, 上界) res linprog(c, A_ubA_ub, b_ubb_ub, boundsbounds, methodhighs) print(状态码:, res.status, 最优值:, -res.fun, 解:, np.round(res.x, 4))A_ub和b_ub必须成对且行序对应A_ub x b_ub是它的固定语义。如果原题是「不小于」不能靠改b_ub的符号解决必须整条不等式乘 -1 再填这是最容易写错的一处。methodhighs是新版本 scipy 的默认求解器速度和对偶信息的完整度都比老方法好res.status的取值要记住0 表示成功2 表示不可行约束互相矛盾3 表示无界变量没设上界又不收敛。bounds是最容易漏的参数。默认情况下 scipy 会认为变量非负如果你的决策变量在业务上允许取负值比如差值型变量不显式写(None, None)就会得到一个「看起来有解、其实被截断」的结果。同一道题的 pulp 写法读起来更接近数学表达import pulp prob pulp.LpProblem(resource_allocation, pulp.LpMaximize) x1 pulp.LpVariable(x1, lowBound0, catpulp.LpInteger) # 整数变量 x2 pulp.LpVariable(x2, lowBound0, catpulp.LpContinuous) # 连续变量 prob 3 * x1 5 * x2 # 目标函数直接写表达式 prob x1 2 * x2 8 # 约束一 prob 3 * x1 x2 12 # 约束二 prob.solve(pulp.PULP_CBC_CMD(msgFalse)) print(pulp.LpStatus[prob.status], x1.value(), x2.value(), pulp.value(prob.objective))cat是变量类别的开关LpInteger表示整数、LpBinary表示 0-1不写就是连续变量。msgFalse关掉求解器日志批量跑参数扫描时非常必要否则控制台会被刷屏。pulp.LpStatus[prob.status]直接给出 Optimal / Infeasible 这样的可读字符串比记状态码省事。两种写法的选择判据约束全是线性、变量全连续、规模在几百以内用 linprog一旦有整数、0-1、或者约束数量随数据变化需要动态拼接用 pulp。后者可以写成循环往里加约束前者需要先构造矩阵。4.2 时间序列预测灰色 GM(1,1) 与 ARIMA 的选型判据小样本预测是竞赛论文的常客GM(1,1) 几乎是标配。它的核心是把原始序列做一次累加把随机波动弱化成单调趋势再用一个一阶微分方程去拟合这个累加序列。样本只有 6 到 10 个点时它的表现往往比 ARIMA 稳因为 ARIMA 的参数估计在这个样本量下几乎没有统计意义。import numpy as np def gm11(x0, predict_n3): x0: 原始非负序列建议长度 6 predict_n: 向后预测的期数 返回: (拟合预测序列, 发展系数 a, 灰作用量 b) x0 np.asarray(x0, dtypefloat) x1 np.cumsum(x0) # 一次累加生成 z1 0.5 * (x1[1:] x1[:-1]) # 紧邻均值背景值 B np.column_stack([-z1, np.ones_like(z1)]) Y x0[1:].reshape(-1, 1) a, b np.linalg.lstsq(B, Y, rcondNone)[0].ravel() # 最小二乘估计参数 k np.arange(len(x0) predict_n) x1_hat (x0[0] - b / a) * np.exp(-a * k) b / a # 累加序列的预测值 x0_hat np.diff(x1_hat, prepend0.0) # 累减还原 return x0_hat, a, b # 精度检验 def gm11_check(x0, x0_hat): resid np.asarray(x0) - x0_hat[:len(x0)] S1 np.std(x0, ddof1) S2 np.std(resid, ddof1) C S2 / S1 # 后验差比 P np.mean(np.abs(resid - resid.mean()) 0.6745 * S1) # 小误差概率 return C, Pnp.cumsum和np.diff是一对互逆操作最后要prepend0才能让diff后的长度和原序列对齐这个小细节漏了会导致所有拟合值整体错位一期。np.linalg.lstsq比手写正规方程稳rcondNone让它用默认截断阈值处理接近奇异的矩阵。参数a是发展系数它的绝对值直接决定模型能不能用。这是灰色预测最常见被忽略的适用边界|a| 区间适用性处理建议 0.3适合中长期预测可直接外推 3 到 5 期0.3 ~ 0.5适合短期预测外推不超过 2 期0.5 ~ 0.8短期慎用只做拟合展示不做外推 0.8不宜使用改用其他模型或缩短序列精度等级看两个指标后验差比C 0.35为好、 0.5为合格小误差概率P 0.95为好、 0.8为合格。两个都不达标说明这组数据本身不适合灰色模型硬写进论文会被追问。样本量足够一般 30 期以上且序列平稳时才轮到 ARIMA。选型顺序是先做 ADF 单位根检验定差分阶数d再在p、q的小范围网格里按 AIC 选阶最后对残差做白噪声检验。from statsmodels.tsa.arima.model import ARIMA from statsmodels.tsa.stattools import adfuller from statsmodels.stats.diagnostic import acorr_ljungbox def pick_arima(series, p_max3, q_max3, d1): best None for p in range(p_max 1): for q in range(q_max 1): try: model ARIMA(series, order(p, d, q)).fit() except Exception: continue # 不收敛的组合直接跳过 if best is None or model.aic best[0]: best (model.aic, p, q, model) return best p_value adfuller(series)[1] # 0.05 认为序列平稳 aic, p, q, fitted pick_arima(series, d0 if p_value 0.05 else 1) lb acorr_ljungbox(fitted.resid, lags[6], return_dfTrue)order(p, d, q)里的d必须由 ADF 结果决定不能凭感觉设。acorr_ljungbox的 p 值大于 0.05 才说明残差里没有剩余信息模型把能提取的都提取了p 值很小意味着还有结构没被抓住这时候加阶数比换模型更有效。4.3 把结果回填论文表格一次运行产出全部数字复现到这一步最耗时的往往不是建模是把散落在各处的中间结果整理成论文里的那张表。手工抄数字不仅慢还会在改参数后忘记同步。我的做法是在脚本最后留一段导出逻辑把所有权重方案、所有排序方法的组合一次跑完直接落成 CSV。import pandas as pd rows [] for w_name, w in [(等权, w_equal), (熵权, w_entropy)]: for s_name, score in [(TOPSIS, topsis(Z, w)[0]), (加权和, (Z * w).sum(axis1))]: rank pd.Series(score).rank(ascendingFalse).astype(int) for i in range(len(score)): rows.append({ 权重方案: w_name, 排序方法: s_name, 对象: fS{i 1}, 得分: round(float(score[i]), 4), 排名: int(rank.iloc[i]), }) df_out pd.DataFrame(rows) df_out.to_csv(result_table.csv, indexFalse, encodingutf-8-sig) print(df_out.pivot_table(index对象, columns[权重方案, 排序方法], values排名))encodingutf-8-sig这个参数不是洁癖。默认的 utf-8 编码在部分表格软件里打开会让中文列名变成乱码加了 BOM 头就正常了这是踩过坑之后形成的习惯。pivot_table把四种组合的排名并排放一眼就能看出哪个对象的排名对方案敏感——那些在不同方案下排名跳动超过两位的对象就是论文里做灵敏度分析时最该拿出来讨论的例子。5. 复现优秀论文时的排查清单五个高频翻车点复现别人的论文最难受的不是不会写是写完了数字对不上。下面五条是我在不同题目上反复遇到的按「现象、原因、解决」记下来遇到问题先照着对一遍。一、熵权算出来的权重有 NaN或者某列权重为 0现象w里出现nan或者某一个指标权重精确等于 0排序结果对方案二完全不敏感。原因标准化后某列最小值是 0P里有 0log(0)得到-inf-inf * 0在 numpy 里是nan。另一种情况是某个指标在所有对象上取值相同信息熵等于 1差异系数为 0。解决在归一化末尾加* 0.998 0.001把值域压离 0对整列相同的指标直接剔除并单独说明——它对排序没有任何贡献留着只会污染权重。二、优化模型返回不可行但论文里明明有解现象linprog的status等于 2或者 pulp 返回Infeasible但你照着论文的约束抄了一遍。原因三种可能。一是把「≥」直接填进了A_ublinprog只认「≤」方向反了必然无解二是漏设bounds变量在业务上应取负值却被默认截成非负三是约束里的常数项忘了移到右边。解决把每条约束单独打印出来代入一个手工构造的可行解验证一遍看是否满足。这一步花两分钟能省掉半小时的瞎猜。三、排名和论文差一到两位头部对象还能对上现象排序整体接近但中间几个对象互换位置作者在论文里恰好对这个顺序做了重点分析。原因标准化方法不同。min-max、向量归一化、z-score 三种做法在 TOPSIS 里得到的顺序并不完全等价尤其是当某个指标的分布有长尾时。另一种原因是正向化时极小型指标用了1/x而不是max - x1/x会放大接近 0 的值的差异。解决先用论文自己描述的方法复现一遍确认一致后再换成你偏好的方法并在论文里注明「采用 X 标准化与 Y 方法的秩相关系数为 Z」。方法差异本身就一段可以写的讨论。四、灰色模型拟合得漂亮外推两期就发散现象拟合值和原始数据几乎重合向后预测却突然翻倍或者变成负数。原因发展系数|a|超过 0.5模型把短期趋势当成了指数增长或者原始序列本身有波动但长度不足 6累加后信息不够。解决先算|a|超过 0.5 就只报拟合不报外推序列太短就补数据或者换指数平滑。预测值出现负数更是明确信号——这类模型对非负序列才成立。五、改了一个参数整张表都要重抄现象调整权重下限后论文表格里的 20 个数字得手工改 20 次改到第三次就开始抄错。原因中间结果靠肉眼传递没有单一的产出出口。解决从 4.3 那段导出逻辑开始把所有要进论文的数字都从同一个 CSV 里取正文表格用脚本生成的版本。这一条严格说不算 bug但它是复现效率的分水岭。6. 灵敏度分析与收口让结论站得住的最后一步模型跑通、表格填满很多人到这里就收笔了。但评审看一篇论文的最后一眼往往落在「你的结论有多脆弱」上。灵敏度分析做的就是这件事把关键参数动一动看结论还在不在。评价类动权重优化类动约束右端项预测类动样本窗口。我一般固定做两种扰动权重在 ±10% 和 ±20% 两个幅度内随机抽样 200 次约束右端项按 ±5%、±10% 缩放然后只统计一个指标——头部对象的排名保持不变的比例。import numpy as np, pandas as pd rng np.random.default_rng(42) # 固定种子保证结果可复现 base_rank pd.Series(topsis(Z, w_entropy)[0]).rank(ascendingFalse) records [] for amp in (0.10, 0.20): stable 0 for _ in range(200): w_pert w_entropy * (1 rng.uniform(-amp, amp, sizelen(w_entropy))) w_pert w_pert / w_pert.sum() # 扰动后重新归一化保持权重和为 1 rank pd.Series(topsis(Z, w_pert)[0]).rank(ascendingFalse) stable int(rank.iloc[:3].equals(base_rank.iloc[:3])) records.append({扰动幅度: f±{int(amp*100)}%, 前三名保持不变的比例: round(stable / 200, 3)}) print(pd.DataFrame(records))default_rng(42)固定随机种子这一步不能省否则你写「扰动 200 次后 93% 稳定」别人重跑得到 91%这段结论就废了。w_pert / w_pert.sum()是扰动后必须做的重归一化漏掉的话权重和不再等于 1贴近度的量级会整体漂移稳定性统计失去意义。结果怎么读我给一个自己的经验表扰动幅度前三名稳定比例结论写法±10% 0.95结论稳健可直接作为建议依据±10%0.80 ~ 0.95头部稳健中部需说明敏感性±20% 0.70结论依赖参数设定应改为分情景讨论写作上有个很实用的收口方式一张热力图加一句话。热力图横轴是扰动幅度、纵轴是对象编号、颜色是排名变化次数一句话写「在 ±10% 的权重扰动下前两名保持不变的比例为 96%第三名与第四名出现互换说明头部结论不依赖具体权重设定」。这比写三段「本模型具有良好的鲁棒性」有用得多。最后说一句自己的教训。我最早复现这类论文时总想着把模型做得更复杂一点觉得简单模型拿不出手结果花了三天调一个带约束的非线性规划最后发现原题数据量只有十二组评委一句「样本量是否支撑该模型的参数估计」就能问住。后来我改了习惯先用最简单的方法把整条链跑通把表格填满再考虑要不要升级模型——绝大多数情况下把假设写清楚、把检验做扎实、把灵敏度补上比换一个更炫的算法得分高得多。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑