简介:本资源面向多属性决策与评价算法学习者,提供TOPSIS评价模型的完整MATLAB实现与步骤讲解,适合运筹学、数据分析及数学建模方向的学生与研究人员参考。压缩包共7个文件,约20KB,包含5个m脚本、1个mat数据文件和1份docx说明文档,脚本分别承担主流程、指标正向化与标准化等任务,数据文件可直接加载运行,文档则梳理模型原理与操作要点。已有374人学习下载,说明该模型在课程作业与竞赛场景中具有稳定需求。读者可借助源码掌握数据标准化、理想解与反理想解构建、欧氏距离计算、相对贴近度排序等关键环节,并理解正向、负向及区间型指标的转换处理方式,从而将TOPSIS方法迁移到供应商选择、方案评估等实际决策问题中,配合探索性数据分析进一步提升评价结论的科学性。
1. 从一份 TOPSIS 源码说起:评价模型到底在算什么
手上拿到一份名为「算法源码-评价与决策:TOPSIS评价模型具体步骤及代码」的压缩包时,多数人的第一反应是打开看代码,然后被里面一堆归一化、加权、正负理想解的公式绕晕。其实 TOPSIS(逼近理想解排序法)的核心思想非常朴素:把每个评价对象想象成空间里的一个点,先找出一个「全能冠军」点(正理想解,所有指标都取最优)和一个「全面垫底」点(负理想解,所有指标都取最差),然后算每个对象离这两个点各有多远,离冠军越近、离垫底越远的对象排名越靠前。这套逻辑在供应商选择、员工绩效、城市宜居度、方案选型里被反复使用,原因就一个——它不依赖专家打分的主观权重,只要指标数据确定,排序结果就能复现。热搜里常出现的「topsis法」「topsis综合评价法」说的都是它。这份源码包的价值不在于代码多长,而在于它把「指标正向化 → 归一化 → 加权 → 找理想解 → 算距离 → 排序」这条链路完整落成了可运行脚本。适合谁?适合手头有一张「对象×指标」的 Excel 表、需要给出一个能写进报告、能被人追问细节的排序结论的人。下面我按自己复现这类源码的习惯,把每一步拆开讲清楚,包括参数怎么设、哪里容易翻车。
2. TOPSIS 的数学骨架与源码目录拆解
2.1 六个步骤的数学表达与选型理由
TOPSIS 的完整流程可以写成六步,每一步都有明确的数学动作,理解这些动作才能看懂源码里为什么那样写。
第一步,构造决策矩阵。假设有 m 个评价对象、n 个指标,原始数据构成矩阵 X,其中 x_ij 表示第 i 个对象在第 j 个指标上的取值。
第二步,指标正向化。指标分四类:极大型(越大越好)、极小型(越小越好)、中间型(越接近某个值越好)、区间型(落在某个区间最好)。TOPSIS 要求所有指标方向一致,所以极小型、中间型、区间型都要转成极大型。极小型转换最常见的是 max - x,中间型用 1 - |x - best| / max|x - best|,区间型按区间上下界分段处理。
第三步,归一化。把不同量纲的指标压到同一尺度,最常用的是向量归一化:z_ij = x_ij / sqrt(sum(x_ij^2))。也有用 min-max 归一化的,但向量归一化保留了指标间的比例关系,在 TOPSIS 里更主流。
第四步,加权。把归一化矩阵的每一列乘以该指标的权重 w_j,得到加权决策矩阵。权重可以来自熵权法、层次分析法或直接指定。
第五步,确定正理想解 Z+ 和负理想解 Z-。Z+ 的每个分量是该指标在所有对象中的最大值,Z- 是最小值。
第六步,计算距离并排序。用欧氏距离算每个对象到 Z+ 和 Z- 的距离 D_i+ 和 D_i-,然后算相对贴近度 C_i = D_i- / (D_i+ + D_i-)。C_i 越大越优。
为什么选 TOPSIS 而不是灰色关联或模糊综合评价?因为 TOPSIS 对指标数量不敏感,不需要构造判断矩阵,结果可解释性强——你能明确告诉别人「这个方案离最优解差在哪几个指标上」。源码包通常就是把这六步拆成函数,主脚本负责读数据、调函数、输出排序。
2.2 源码包常见文件结构与数据格式约定
一份典型的 TOPSIS 源码包,目录结构大致如下(不同作者命名会有差异,但功能模块大同小异):
topsis_project/ ├── data/ │ └── input.xlsx # 原始决策矩阵,第一列对象名,后续列指标值 ├── src/ │ ├── normalize.py # 正向化 + 归一化 │ ├── weight.py # 权重计算(熵权法或手动) │ ├── topsis_core.py # 理想解、距离、贴近度 │ └── main.py # 主流程入口 ├── config.yaml # 指标方向、权重方法等参数 └── output/ └── result.csv # 排序结果数据格式约定很关键:input.xlsx 第一列是对象名称(字符串),第二列开始是指标值(数值),第一行是指标名。config.yaml 里通常要声明每个指标的类型(max/min/center/interval)和权重方法。如果源码包里没有 config 而是把参数硬编码在 main.py 里,复现时第一件事就是把它们抽出来,否则换个数据集就要改代码。
提示:拿到源码先看 README 或 main.py 顶部的注释,确认指标方向的定义顺序是否和你的数据列顺序一致,这是最常见的错位来源。
2.3 用 Python 复现核心计算的最小脚本
下面这段代码把 TOPSIS 六步压缩成一个可运行脚本,依赖 numpy 和 pandas,适合在本地快速验证源码包的计算逻辑是否和你理解的一致。
import numpy as np import pandas as pd # 读取数据:第一列为对象名,其余为指标值 df = pd.read_excel("data/input.xlsx", index_col=0) X = df.values.astype(float) m, n = X.shape # 指标方向:1 表示极大型,-1 表示极小型 directions = np.array([1, 1, -1, 1]) # 按实际列顺序修改 # 正向化:极小型取 max - x X_pos = X.copy() for j in range(n): if directions[j] == -1: X_pos[:, j] = X[:, j].max() - X[:, j] # 向量归一化 norm = np.sqrt((X_pos ** 2).sum(axis=0)) Z = X_pos / norm # 权重:这里先用等权,实际可替换为熵权法结果 w = np.ones(n) / n Z_weighted = Z * w # 正负理想解 Z_plus = Z_weighted.max(axis=0) Z_minus = Z_weighted.min(axis=0) # 欧氏距离 D_plus = np.sqrt(((Z_weighted - Z_plus) ** 2).sum(axis=1)) D_minus = np.sqrt(((Z_weighted - Z_minus) ** 2).sum(axis=1)) # 相对贴近度 C = D_minus / (D_plus + D_minus) df["贴近度"] = C df["排名"] = df["贴近度"].rank(ascending=False).astype(int) print(df.sort_values("排名"))逻辑说明:directions数组控制每一列的正向化方式,必须和数据列顺序严格对应;norm是按列求 L2 范数,对应向量归一化;w是权重向量,等权只是占位,真实场景要用熵权法或业务指定权重替换;Z_plus和Z_minus是按列取最大最小,对应正负理想解;最后C越大排名越靠前。参数说明:如果指标里有中间型或区间型,需要单独写转换函数替换X_pos的对应列;权重如果来自熵权法,把w换成计算出的权重数组即可,注意权重之和为 1。
3. 权重怎么定:熵权法接入与参数调试
3.1 熵权法的计算逻辑与代码接入点
等权在演示里能用,但真实评价场景里指标重要性不同,权重直接决定排序结果。熵权法是最常用的客观赋权方法,逻辑是:某个指标下各对象取值差异越大,该指标携带的信息越多,权重越高。计算步骤是:先对正向化后的矩阵做比重变换 p_ij = x_ij / sum(x_ij),再算熵值 e_j = -k * sum(p_ij * ln(p_ij)),其中 k = 1 / ln(m),然后算差异系数 d_j = 1 - e_j,最后归一化得到权重 w_j = d_j / sum(d_j)。
把熵权法接入上面的脚本,只需要在归一化之后、加权之前插入一段:
# 熵权法计算权重 p = X_pos / X_pos.sum(axis=0) # 比重矩阵 p = np.clip(p, 1e-12, None) # 避免 log(0) k = 1.0 / np.log(m) e = -k * (p * np.log(p)).sum(axis=0) # 熵值 d = 1 - e # 差异系数 w = d / d.sum() # 权重 print("熵权法权重:", w)逻辑说明:np.clip是防止某个对象在某个指标上取值为 0 导致 log 报错,这是熵权法最常见的翻车点;k的分母是 ln(m),m 是对象数量,不是指标数量,写错会导致熵值不在 [0,1] 区间。参数说明:如果某个指标所有对象取值完全相同,差异系数为 0,该指标权重为 0,这是合理的,但要在报告里说明该指标无区分度。
3.2 权重敏感性测试:排序结果稳不稳
权重定完不能直接用,要做敏感性测试。做法是:把某个指标的权重上下浮动 10%~20%,重新归一化其他权重,看排序结果是否发生大幅变化。如果第一名和第三名在权重微调后就互换,说明这几个对象综合水平接近,结论要谨慎表述。
一个简单的敏感性测试脚本:
base_w = w.copy() for j in range(n): for delta in [-0.2, -0.1, 0.1, 0.2]: w_test = base_w.copy() w_test[j] = max(0.01, w_test[j] * (1 + delta)) w_test = w_test / w_test.sum() # 用 w_test 重跑 TOPSIS 核心计算,记录排名 # 此处省略重复计算代码,实际使用时封装成函数 print(f"指标{j}权重变化{delta:+.0%}后权重分布: {np.round(w_test, 3)}")逻辑说明:这段代码只演示权重扰动方式,实际要封装一个run_topsis(X_pos, w)函数,每次扰动后调用并记录排名。参数说明:扰动幅度建议从 ±10% 开始,如果排序稳定再放大到 ±20%;如果 ±10% 就翻车,说明数据本身区分度不够,需要考虑增加指标或换评价方法。
注意:熵权法完全依赖数据分布,如果某个指标是「越大越好」但所有对象取值都很接近,熵权法会给它极低权重,而业务上这个指标可能很关键。这时候要么改用组合赋权,要么在报告里说明客观权重的局限性。
3.3 权重结果写入配置与复现一致性
源码包如果支持从 config.yaml 读权重,复现时要把计算出的权重写回去,而不是每次跑都重新算。因为熵权法依赖输入数据,一旦数据更新,权重会变,排序也会变。工程上的做法是:把权重计算和 TOPSIS 排序分成两个脚本,权重脚本输出 weights.csv,排序脚本读 weights.csv。这样数据更新时先跑权重脚本,人工确认权重合理后再跑排序脚本,避免「数据一换、结论全变」的玄学现象。
4. 避坑与排查:TOPSIS 源码复现中最容易翻车的五个点
4.1 指标方向配错导致排序完全反转
现象:跑完脚本发现排名和业务直觉完全相反,明明各项数据都好的对象排到了后面。原因:directions数组和实际数据列顺序不一致,或者把极小型指标当成了极大型。解决:打印正向化前后的矩阵对比,逐个指标检查。极小型指标正向化后,原来最小的值应该变成最大值。如果发现某列正向化后最大值出现在原来最小值的位置,说明方向配对了;反之就是配错。
4.2 归一化时除零或量纲残留
现象:脚本报 RuntimeWarning: invalid value encountered in divide,或者排序结果里某个对象贴近度为 nan。原因:某个指标所有对象取值相同,向量归一化时分母为 0;或者数据里混入了空值、文本。解决:归一化前先做X = np.nan_to_num(X)和X_pos = np.where(norm == 0, 1e-12, X_pos / norm),同时检查 Excel 里是否有合并单元格或空行被读成了 NaN。
4.3 正负理想解取错轴向
现象:贴近度全部集中在 0.5 附近,排序区分度极低。原因:Z_plus = Z_weighted.max(axis=0)里的 axis 写成了 1,变成按行取最大,正理想解变成了每个对象的自身最大值,距离计算完全失效。解决:确认 axis=0 是按列操作,正理想解是每个指标列的最大值组成的一行向量,负理想解是最小值组成的一行向量。打印Z_plus和Z_minus的形状,应该是 (n,),不是 (m,)。
4.4 权重之和不等于 1 导致距离失真
现象:加权后矩阵数值异常大或异常小,贴近度超出 [0,1]。原因:手动指定权重时没有归一化,或者熵权法计算时d.sum()为 0(所有指标差异系数都为 0 的极端情况)。解决:加权前强制w = w / w.sum(),并加断言assert abs(w.sum() - 1) < 1e-6。如果 d.sum() 为 0,说明所有指标在所有对象上取值完全相同,这份数据不适合做 TOPSIS,需要重新选指标。
4.5 对象名称在排序后丢失对应关系
现象:输出结果只有排名和贴近度,不知道哪个分数对应哪个对象。原因:df.values丢掉了索引,排序后没有把对象名带回来。解决:始终用 pandas 的 DataFrame 操作,保留 index,最后df.sort_values("排名")输出时对象名自然跟着走。如果源码包里用的是纯 numpy 数组,复现时第一件事就是改成 DataFrame 或额外维护一个 names 列表。
5. 进阶技巧:把 TOPSIS 封装成可复用评价管道
5.1 用类封装六步流程,支持多数据集切换
把前面散落的代码封装成一个TOPSISEvaluator类,好处是换数据集时只改配置,不改逻辑。下面是一个精简实现:
class TOPSISEvaluator: def __init__(self, directions, weight_method="entropy"): self.directions = np.array(directions) self.weight_method = weight_method def fit(self, X): self.X_pos = self._positiveize(X) self.Z = self._normalize(self.X_pos) self.w = self._calc_weight() self.Z_w = self.Z * self.w self.Z_plus = self.Z_w.max(axis=0) self.Z_minus = self.Z_w.min(axis=0) return self def _positiveize(self, X): X_pos = X.copy().astype(float) for j, d in enumerate(self.directions): if d == -1: X_pos[:, j] = X[:, j].max() - X[:, j] return X_pos def _normalize(self, X): norm = np.sqrt((X ** 2).sum(axis=0)) norm = np.where(norm == 0, 1e-12, norm) return X / norm def _calc_weight(self): if self.weight_method == "entropy": p = self.X_pos / self.X_pos.sum(axis=0) p = np.clip(p, 1e-12, None) k = 1.0 / np.log(self.X_pos.shape[0]) e = -k * (p * np.log(p)).sum(axis=0) d = 1 - e return d / d.sum() return np.ones(self.X_pos.shape[1]) / self.X_pos.shape[1] def score(self): D_plus = np.sqrt(((self.Z_w - self.Z_plus) ** 2).sum(axis=1)) D_minus = np.sqrt(((self.Z_w - self.Z_minus) ** 2).sum(axis=1)) return D_minus / (D_plus + D_minus)逻辑说明:fit方法串联六步,score返回贴近度数组。_calc_weight里weight_method支持切换熵权法和等权,后续可以扩展成读外部权重文件。参数说明:directions长度必须等于指标列数;X传入前要确保是纯数值矩阵,对象名在外面用 DataFrame 维护。
5.2 结果验证:用极端数据做冒烟测试
封装完不能直接上真实数据,先用构造的极端数据验证逻辑。构造三个对象、两个指标:对象 A 两个指标都是最大值,对象 B 都是最小值,对象 C 居中。正确的 TOPSIS 结果应该是 A 贴近度接近 1,B 接近 0,C 在中间。如果跑出来不是这个顺序,说明正向化、归一化或理想解某一步写错了。这个冒烟测试花五分钟,能省掉后面几小时的排查。
X_test = np.array([[10, 10], [1, 1], [5, 5]], dtype=float) evaluator = TOPSISEvaluator(directions=[1, 1], weight_method="equal").fit(X_test) print(evaluator.score()) # 预期输出接近 [1.0, 0.0, 0.5]5.3 输出报告时把中间结果一起带上
最后一步,别只输出排名。把正向化矩阵、归一化矩阵、权重、正负理想解、距离、贴近度全部写进一个 Excel 的多个 sheet。这样别人质疑排序时,你能直接翻到对应 sheet 指出「这个对象在第三个指标上离正理想解差了 0.3,所以总分被拉下来了」。这也是 TOPSIS 相比神经网络类评价模型的最大优势——每一步都能摊开给人看。我自己做项目时养成的习惯是:任何评价模型,只要中间结果不能逐层导出,就不敢把结论写进正式报告。TOPSIS 恰好满足这个要求,所以它虽然老,但在需要「讲清楚为什么」的场景里一直没被淘汰。希望帮到你。
本文还有配套的精品资源,点击获取