多目标决策实战:用TOPSIS与Python从决策矩阵到排序分析

多目标决策实战:用TOPSIS与Python从决策矩阵到排序分析 简介这份《多目标决策讲义》PPT共1个pptx文件容量约689KB适合决策科学、管理科学、运筹学及相关专业学生与研究者快速建立多目标决策知识框架。讲义从传统单目标决策的局限出发系统梳理了多维效用合并、AHP层次分析、DEA数据包络分析、多目标规划等主流评价方法并结合医疗机构科室评价、就业机会选择等实例演示指标归一化、权重分配与雷达图比较的思路。内容还重点介绍了H.A.西蒙“管理人”理论对“最优解”转向“满意解”的影响借助身高体重选人、玉米田找玉米等通俗例子阐释劣解、非劣解与有效解等核心概念进而说明多目标决策为何能提供一组方案供决策者灵活选择。文件虽仅1个pptx但知识点密度较高已有66人学习可作为课堂讲授、自学入门或考前系统复习的提纲式参考资料。1. 多目标决策讲义从一个排期难题说起感觉上云成本要涨又急着上线新功能研发想要更多人老板只批两个名额。这类问题的共同点是任何一个单目标都说得通放在一起怎么都不满意。多目标决策MCDM就是处理「多个指标互相打架」的决策方法它把方案、指标、权重组织成一张可计算的矩阵再输出可解释的排序结果。这份讲义把完整的决策流程做成可复现的 Python 代码并最终落到一份「多目标决策讲义.pptx」文档里中间的计算表、排序和敏感性分析都能直接作为幻灯片页交付。适合要拍板技术选型、需求优先级排序、资源池评估的工程师和架构师也适合要向评审讲清楚「为什么选它」的场合。2. 多目标决策的起点决策矩阵、帕累托最优与归一化2.1 一份「方案 × 指标」的决策矩阵应该怎么建多目标决策的第一步不是选算法而是把问题写成结构化数据。常见做法是行放备选方案列放评价指标值就是方案在原单位下的真实数值。以数据库选型为例候选方案是 MySQL 8.0、PostgreSQL 15、TiDB 6.x评价指标取部署成本万元、P99 延迟毫秒、运维负担人月/年和是否支持在线扩缩容。其中部署成本、P99 延迟和运维负担都是「越小越好」的成本型指标在线扩缩容是「越大越好」的效益型指标类型不同后面对待方式也不同。用 pandas 建矩阵的代码是这样import pandas as pd data { 方案: [MySQL 8.0, PostgreSQL 15, TiDB 6.x], 部署成本(万元): [8, 10, 25], P99延迟(ms): [120, 95, 80], 运维负担(人月/年): [12, 15, 18], 在线扩缩容: [0, 0, 1], } dm pd.DataFrame(data) print(dm)这段代码里「方案」列是主键不参与后续数值计算其余四列是决策矩阵的数值区用dm.iloc[:, 1:]切片拿到。建矩阵时要把指标口径写死并同步到讲义页脚比如延迟统一写 P99 而不是平均延迟成本统一写首次采购加三年运维否则不同人填的数据无法比较。指标数量建议控制在 5 到 9 个超过这个范围后续权重解释和敏感性分析都会变得很难收敛。2.2 帕累托最优先删掉绝对劣势方案建好矩阵后先做一次帕累托筛选手工淘汰。它的定义是方案 A 在所有指标上都不差于方案 B且至少在一个指标上严格优于 B就称 A 支配 B。被支配的 B 没有任何理由保留因为不管决策者怎么偏好都不会选一个处处更差的方案。上面的例子里PostgreSQL 15 与 MySQL 8.0 相比延迟更好但成本更高两者互不支配TiDB 延迟最好、成本最高也不支配别人。把被支配行删掉候选集通常会缩小到十几个以内。下面这个函数可以判断两个方案之间的支配关系def dominates(row_a, row_b, cost_idx): 判断 row_a 是否支配 row_b。 cost_idx 传入成本型指标的下标集合其余按效益型处理。 not_worse [ a b if idx in cost_idx else a b for idx, (a, b) in enumerate(zip(row_a, row_b)) ] strict [ a b if idx in cost_idx else a b for idx, (a, b) in enumerate(zip(row_a, row_b)) ] return all(not_worse) and any(strict) # numeric 来自 dm.iloc[:, 1:]0/1/2 分别是三列成本型指标 numeric dm.iloc[:, 1:] for i in range(len(numeric)): for j in range(len(numeric)): if i ! j and dominates(numeric.iloc[i], numeric.iloc[j], {0, 1, 2}): print(f方案 {i 1} 支配方案 {j 1})调用时把cost_idx传成本型指标的下标集合返回True说明第一个参数对应的方案支配第二个。实战中建议把「互不支配的候选数量」作为讲义第一页的数据点它能直观说明为什么不能只靠拍脑袋。需要注意的是帕累托筛选只能缩小范围不能给出最终排序因为剩下的方案往往是「一个指标好、另一个指标差」这就是多目标决策必须引入权重的原因。2.3 为什么必须做归一化量纲与方向未归一化的数据直接加权会把决策结果交给量纲最大的那一列。比如部署成本单位是万元数值量级是 10 左右P99 延迟单位毫秒量级是 100两者直接相加成本几乎不起作用。归一化把每列映射到同一尺度同时把成本型指标转向为「越大越优」。三种常用方法适合的场景不同min-max 适合指标有明确上下界且分布均匀的场景Z-score 对离群点更稳健但会丢失上下界语义向量归一化是 TOPSIS 的标准做法按列求模后没有固定上下界。方法公式效益型适用场景min-max(x - min) / (max - min)指标有明确边界、分布均匀Z-score(x - mean) / std存在离群点保留分布形状向量归一化x / sqrt(Σ x_j^2)TOPSIS 标准步骤不设固定边界上面这个表在市场化的团队汇报里经常直接贴进讲义推荐的做法是把「选了哪种归一化」和理由写在表下方的一行注释里。下面是 min-max 归一化并反转成本型方向的代码def minmax_norm(df, cost_columns): arr df.to_numpy(dtypefloat) for col in range(arr.shape[1]): min_v, max_v arr[:, col].min(), arr[:, col].max() if max_v min_v: print(f警告: 第 {col} 列零方差归一化跳过) continue arr[:, col] (arr[:, col] - min_v) / (max_v - min_v) if col in cost_columns: arr[:, col] 1 - arr[:, col] return arr函数的参数cost_columns是一个集合例如{0, 1, 2}表示前三列是成本型指标归一化后做1 - x反转让成本最低的方案得到最高分。零方差列必须单独提示否则后续加权计算会把该列当成全 0实际是信息量为 0 的指标应当从矩阵中删除。归一化只改变量纲不改变每个方案在同一指标上的相对大小也不改变帕累托支配关系所以它和 2.2 的筛选互不干扰。2.4 权重从哪来主观赋值与熵权法权重是多目标决策里争议最大、也最容易被质疑的参数。业务上常见做法是让负责人先拍一版再用熵权法做交叉验证。熵权法的逻辑是某个指标在各方案间差异越大说明它承载的区分信息越多权重就该越高。实现很短直接复用归一化后的矩阵def entropy_weight(norm_matrix): # norm_matrix: 已经是同一尺度的决策矩阵行是方案列是指标 p norm_matrix / (norm_matrix.sum(axis0) 1e-12) n norm_matrix.shape[0] k 1.0 / np.log(n 1e-12) e -k * (p * np.log(p 1e-12)).sum(axis0) d 1 - e return d / d.sum()添加1e-12是为了防止某列出现 0 值导致log(0)报错。返回的权重数组自动满足和为 1。现实中熵权法算出的权重经常与业务直觉冲突比如「在线扩缩容」这种布尔指标方差大、熵小权重会被抬得很高这时要以业务语义为准做人工修正并把「人工修正了哪个指标、理由是什么」写进讲义。没有绝对正确的权重只有经过校验的假设熵权法提供的是与经验对照的基准而不是最终答案。权重确定之后才进入第 3 章的两类排序算法。3. 加权和与 TOPSIS两套可落地的多目标决策排序实现3.1 加权和法最直观的线性聚合加权和法WSM是所有方法里最好向非技术同事解释的每个方案的总分等于各指标归一化得分乘权重的累加。它隐含一个假设——指标之间完全可替代即一个指标的失分可以由另一个指标的得分补平。对大多数粗排序场景这个假设可以接受。实现只有一行核心运算def wsm(r_matrix, weights): # r_matrix: 归一化后的矩阵形状 (方案数, 指标数) # weights: 与指标列逐位对齐的权重列表建议和为 1 return (r_matrix * np.array(weights)).sum(axis1) weights [0.4, 0.3, 0.2, 0.1] # 成本权重最高扩缩容最低 norm_data minmax_norm(numeric, {0, 1, 2}) # 直接复用 2.3 的函数 scores wsm(norm_data, weights) print(pd.Series(scores, indexdm[方案]).sort_values(ascendingFalse))r_matrix * np.array(weights)用的是 numpy 广播每一列与对应权重相乘行方向聚合出总分。排序时ascendingFalse表示分数高者优先。要提醒的是权重列表的长度必须和矩阵列数相等否则广播会直接报错如果归一化时跳过过零方差列这里记得从权重里同步删掉对应位置。加权和法输出的分数是线性可加的适合在讲义里用一张条形图展示但它的短板也很明显某个方案只要总分高就能排第一哪怕某个关键指标差到不可接受所以引入了 3.2 的距离思想。3.2 TOPSIS用距离找离理想解最近的方案TOPSIS 的思路是构造一个假想的最优方案正理想解和最劣方案负理想解然后看每个真实方案离谁更近。正理想解在效益型指标上取当前所有方案的最大值在成本型指标上取最小值负理想解正好相反。每个方案的相对接近度 到负理想解的距离 / (到正理想解的距离 到负理想解的距离)取值范围 0 到 1越大越优。与加权和法相比TOPSIS 保留了「和理想方案还有多大差距」这一直觉解释成本并不高。如果继续沿用 2.3 的 min-max 结果也可以得到可用的排序但基线建议换成向量归一化r_ij x_ij / sqrt(Σ_k x_kj^2)代码里直接对列做arr / np.linalg.norm(arr, axis0)即可。下面是完整实现def topsis(norm_matrix, weights, cost_columns): # norm_matrix 使用向量归一化weights 与列对齐 r norm_matrix * np.array(weights) best, worst [], [] for col in range(r.shape[1]): if col in cost_columns: best.append(r[:, col].min()) worst.append(r[:, col].max()) else: best.append(r[:, col].max()) worst.append(r[:, col].min()) s_plus np.sqrt(((r - np.array(best)) ** 2).sum(axis1)) s_minus np.sqrt(((r - np.array(worst)) ** 2).sum(axis1)) c_i s_minus / (s_plus s_minus 1e-12) return c_i # 调用示例cost_columns 继续使用 {0, 1, 2} c_i topsis(vector_norm_data, [0.4, 0.3, 0.2, 0.1], {0, 1, 2}) print(pd.Series(c_i, indexdm[方案]).sort_values(ascendingFalse))best和worst按列同位置扫描成本型列取min作为理想值意味着成本越低越好s_plus和s_minus都是欧氏距离1e-12防止两个方案重合时除零。如果输出发现所有 C_i 都集中在 0.7 以上说明方案区分度不大这时要回头检查归一化方法和指标选择而不是急着下结论。TOPSIS 的加权顺序是先乘权重再算距离这会让零方差列在距离计算里自动被忽略行为比 WSM 更平滑。3.3 权重与指标的四个常见坑跑通两套代码后真正的问题往往在参数配置上。整理一个表方便直接对照对比项加权和法 WSMTOPSIS输出语义线性总分对理想解的相对接近度指标互补性完全可替代距离度量弱化替代效应归一化偏好min-max / Z-score向量归一化典型误用权重没归一化成本型列忘了反转第一个坑是权重没做归一化就直接加权。权重和为 1 的目的是让总分落在与归一化得分同一量级便于解释不归一化倒不报错但排序可能被权重绝对大小带偏。第二个坑是成本型指标漏反转。TOPSIS 的cost_columns传错集合理想解方向就会反排名完全翻转建议在代码里加一行assert把成本列的取值范围打在日志里。第三个坑是熵权法与主观权重的冲突没有解释讲义里要让读者看到权重表的版本号和修订原因。第四个坑是零方差列它既不能归一化也不能参与权重计算应该在预处理阶段直接删除。我一般会把成本和效益列写成一张配置表而不是散落在代码里这样同一份矩阵换一批方案时只需改配置不用动算法函数。配置表通常长这样第一列是指标名第二列是类型成本或效益第三列是权重来源经验或熵权法它最终会被渲染进讲义的附录页。4. 把多目标决策结果做进讲义.pptxpython-pptx 模板化输出4.1 生成讲义的最小流程从空演示文稿到第一页前面章节算出的矩阵、权重和排名最终要变成能直接投屏的「多目标决策讲义.pptx」。手工复制粘贴效率低且容易错更可控的做法是用 python-pptx 按模板批量生成。先装依赖并把画布调整为宽屏 16:9pip install python-pptxfrom pptx import Presentation from pptx.util import Pt, Inches prs Presentation() prs.slide_width, prs.slide_height Inches(13.333), Inches(7.5) slide prs.slides.add_slide(prs.slide_layouts[6]) # 6 是空白版式 box slide.shapes.add_textbox(Inches(0.6), Inches(0.4), Inches(12), Inches(1.2)) tf box.text_frame tf.text 多目标决策方案综合排序 tf.paragraphs[0].font.size Pt(32) tf.paragraphs[0].font.bold True prs.save(多目标决策讲义.pptx)add_textbox的四个参数依次是左侧、顶部、宽度、高度单位英寸tf.text是第一段的文本段落样式对象在paragraphs[0]上修改。保存前的所有页面都在prs.slides里累积后续追加页面不会覆盖前面内容。这个最小流程适合当作生成脚本的骨架页面内容再逐步替换成真实数据。4.2 把决策矩阵和排名结果渲染成表格页决策矩阵是讲义里最有说服力的原始证据。把 2.1 的 DataFrame 和 3.2 的 TOPSIS 排序合并成一张带排名列的表格页。python-pptx 原生支持表格操作路径是add_table返回GraphicFrame再通过.table访问单元格def add_result_table(slide, result_df, left, top, width, height): rows, cols result_df.shape[0] 1, result_df.shape[1] 1 frame slide.shapes.add_table(rows, cols, left, top, width, height) table frame.table headers [排名] list(result_df.columns) for j, h in enumerate(headers): table.cell(0, j).text str(h) for i, (_, row) in enumerate(result_df.iterrows(), start1): table.cell(i, 0).text str(i) for j, value in enumerate(row, start1): table.cell(i, j).text f{value:.2f} if isinstance(value, float) else str(value) return tableresult_df应该是在进函数之前按排名排好序的 DataFrameiterrows逐行写入第一列「排名」是本函数强制插入的所以headers和循环里的j都要偏移一位。写入数值时统一保留两位小数避免原始数据里 15 和 15.0 混排。表格的列宽和行高可以在table.columns[0].width或table.rows[0].height上单独调整模板里通常直接把整体宽度做到 12 英寸让内容自适应。实际交付时这一页通常放两样东西上方是完整决策矩阵下方是归一化后的得分条形图。可以用 matplotlib 跑一个fig.savefig(score.png)再通过slide.shapes.add_picture(score.png, Inches(7), Inches(3), widthInches(5))插入到表格右侧。add_picture只接受文件路径或二进制流所以图表生成和 PPT 生成建议分成两个脚本或者按顺序在同一个脚本里执行。4.3 模板化排版的参数与批量生成技巧页面一多手工排版就会失控。给讲义定三条硬规则一页只讲一个主题每页左上角固定标题位每页底部固定页脚标注「权重版本 v2.1」和页码。python-pptx 里这三条都可以用一个函数落到每张新页上def new_slide(prs, title_text, footer_text): slide prs.slides.add_slide(prs.slide_layouts[6]) box slide.shapes.add_textbox(Inches(0.6), Inches(0.4), Inches(12), Inches(1.0)) box.text_frame.text title_text box.text_frame.paragraphs[0].font.size Pt(28) box.text_frame.paragraphs[0].font.bold True footer slide.shapes.add_textbox( Inches(0.6), Inches(7.0), Inches(12), Inches(0.4) ) footer.text_frame.text footer_text footer.text_frame.paragraphs[0].font.size Pt(10) return slide模板参数可以直接用表格固化方便不同项目间复制元素参数建议值标题字号font.sizePt(28)正文/表格字号font.sizePt(16) 到 Pt(18)页脚字号font.sizePt(10)表格整体宽度width12 英寸标题字号 28 磅、页脚 10 磅、正文 16 到 18 磅是投影时比较稳妥的参数。批量生成时按页面内容组织第一页放背景与决策问题第二页放决策矩阵和归一化说明第三页放加权与 TOPSIS 的排序对比第四页放权重敏感性结论。每一页都用new_slide统一创建后续改权重只需重跑脚本不会出现「改了数据忘了改 PPT」的问题。提示用 python-pptx 生成的表格默认样式是蓝色主题投放在深色投影仪上对比度偏低。提前在table.columns[j].cell.fill里设置浅色填充或者把表格背景统一改成白色能显著提升可读性。5. 一致性验证与敏感性分析让多目标决策讲义经得起追问如果权重来自 AHP 两两比较讲义里必须放一张一致性检验结果页否则评审很可能会质疑这个权重是拍脑袋拍出来的。一致性比例 CR 的计算需要判断矩阵的特征值和随机一致性指标 RI下面这个实现可以直接嵌入脚本def consistency_ratio(matrix): # matrix: AHP 两两比较判断矩阵方阵且对角线为 1 eigvals np.linalg.eigvals(matrix).real lamda_max np.max(eigvals) n matrix.shape[0] ci (lamda_max - n) / (n - 1) ri {3: 0.58, 4: 0.90, 5: 1.12, 6: 1.24, 7: 1.32}.get(n, 1.0) return ci / riCR 小于 0.1 说明判断矩阵的一致性可接受超过 0.1 就要回 AHP 的成对比较表里找哪一对矛盾最大。常见的矛盾源是「成本比延迟重要、延迟比扩缩容重要、但成本直逼扩缩容」这类循环关系。处理 CR 超限时不要直接把判断矩阵改成符合一致性的形状那属于事后凑数。实际操作是先定位最大特征值对应的成对比较项找出矛盾来源和业务方重新确认优先级再重算。这一过程要留痕因为「第一次一致性和第二次差在哪」本身就是评审关心的点。敏感性分析是这套方法里回报最高的一步把每个权重在 80% 到 120% 之间随机抖动重新归一化并跑 TOPSIS统计第一名变化的频率。下面这段代码跑 2000 次模拟输出的是「第一名对权重波动的敏感程度」base np.array([0.4, 0.3, 0.2, 0.1]) n_sim 2000 np.random.seed(42) flip 0 for _ in range(n_sim): w np.random.uniform(0.8, 1.2, size4) * base w w / w.sum() c topsis(vector_norm_data, w, {0, 1, 2}) if np.argsort(-c)[0] ! np.argsort(-base_result)[0]: flip 1 print(f第一名变化概率: {flip / n_sim:.2%})如果第一名变化概率超过 30%说明结论高度依赖主观权重讲义里应明确写「当前排序对权重敏感建议补充熵权法结果对比」如果低于 10%可以放心把排序作为推荐结论。np.random.seed(42)固定随机种子保证在任何机器上重跑结果一致这是讲稿类文档必须守住的可复现性要求。把敏感性结论做成一个小表格放进讲义第四页例如「权重抖动 ±20%前两名互换概率 12%」比任何文字说明都更有说服力。再补充一个可以放进讲义尾页的验证技巧把 WSM 和 TOPSIS 两种算法的排序结果并列列出标记一致与不一致的方案。如果一个方案在两种算法下都排名靠前它就是稳健选项如果某两个方案在两种方法下名次互换说明它们综合实力接近讲义里应把它们写成「并列候选按团队风格二选一」。用同一份归一化矩阵跑两套排序成本几乎为零但能让讲义经得起「换种方法是不是就翻盘」的追问。本文还有配套的精品资源点击获取