简介:这是基于MATLAB实现的加权TOPSIS(优劣距离法)代码包,面向需要处理多指标决策问题的科研人员与学生,解决各评价指标重要程度不同时的方案排序难题。代码包共八个文件,包含五个MATLAB源文件、两个Excel数据表和一个MAT数据文件,整体体积很小,压缩包仅约二十KB。五个源文件分别对应数据正向化、权重处理、理想解与反理想解计算、欧氏距离求解、相对贴近度排序等关键环节,两个Excel数据表提供可直接运行的河流水质示例数据,一个MAT数据文件用于存放对应数据。目前已有一千九百四十六人学习。通过这份代码,读者可以掌握加权TOPSIS的完整实现流程,理解如何结合层次分析法或熵权法确定权重,并利用归一化和距离度量完成多目标评价,便于迁移到项目选择、系统评估等实际场景。
1. 加权TOPSIS是什么:加了权重的优劣距离法,和直接打分排序有什么本质区别
大多数决策场景里,我们面对的是一张多指标评价表:方案A的价格低但性能差,方案B性能好但交付周期长,直接按总分排序总觉得哪里不对。TOPSIS优劣距离法解决的就是这个问题——它不直接算总分,而是先虚构一个“正理想解”和一个“负理想解”,再度量每个方案离这两者的相对距离。真正让这套方法从论文走进业务的是“加入了权重”这四个字。权重从哪来、怎么进距离公式、正负理想解怎么随之变化,才是落地时最容易翻车的地方。本文给出一套可复现的加权TOPSIS代码,讲清楚每一步的数学前提和参数设置,适合做方案选型、供应商评价、多指标排名这类工作的工程师直接抄作业。
2. 指标正向化和熵权法:权重和距离的数学前提,这步错了后面全白算
2.1 指标方向不统一是头号坑:极大型、极小型、中间型必须“调成同向”
TOPSIS 默认每个指标都是越大越好,也就是极大型。但真实业务里价格、故障率、响应时间都是越小越好,pH 值、转速这类指标则是越接近某个值越好。这三种方向混在同一个矩阵里直接算欧氏距离,结果会和常识完全相反——价格高的方案反而因为“数值大”被当成优秀方案,排名直接反着走。
统一方向的操作叫正向化,常见做法有三种:极小型指标用倒数变换 x' = 1/(x + ε),ε 取 1e-6 到 1e-8 防止除零;中间型指标用偏离度变换 x' = 1 - |x - best| / M,其中 best 是有业务依据的最佳取值,M 是 |x - best| 的最大值;区间型指标则看是否落在业务认可的区间内,落在区间取 1,否则按离区间边界的相对距离衰减。
这里要特别提醒:取倒数比取负数更稳。取负 x' = -x 虽然把排序方向调过来了,但会破坏原指标内部的间距几何形态,距离计算会失真。而平移后的倒数保留了“差距越小越敏感”的特性,和 TOPSIS 的欧氏距离天然匹配。正向化在加权 TOPSIS 里必须放在熵权法之前、归一化之前,顺序反了后面一切都是无效计算。
2.2 熵权法自动算权重:离散程度越大,信息量越大,权重越高
权重怎么来,是“加入了权重”这件事的核心问题。主观打分容易引起争议,AHP 又需要额外的专家判断矩阵。我一般先用熵权法做自动权重,它不需要任何先验知识,只看数据本身:某个指标在所有方案之间的差异越大,它对排序的区分能力就越强,权重应该越高;如果所有方案在某个指标上几乎一样,这个指标就没有提供区分信息,权重趋近于 0。
熵权法的计算步骤是固定的:先把正向化后的数据做 min-max 归一化到 [0,1],得到矩阵 R;再算每个方案在第 j 个指标上的比重 p_ij = r_ij / Σ r_ij;然后算信息熵 e_j = -1 / ln(n) × Σ p_ij × ln(p_ij),n 是方案数量;最后算差异系数 d_j = 1 - e_j,权重 w_j = d_j / Σ d_j。
举个例子就有体感了:假设有三个方案,在“价格”指标上的归一化值是 0.1、0.5、0.9,在“售后网点数”上的值是 0.48、0.50、0.52。前者离散程度大,p_ij 分布不均,熵值小,差异系数大,权重高;后者几乎没差异,熵值接近 1,差异系数接近 0,权重也接近 0。这个结果其实是合理的——售后网点数大家差不多,就不该在排名里起决定作用。
纯熵权法有个缺点:它只看数据离散度,不看业务重要性。比如“报价”这个指标如果离散度特别大,熵权法会把权重抬得很高,可能压过业务上更重要的“交付能力”。我在项目里常用的不是纯熵权,而是组合权重:W = α × W_主观 + β × W_熵权,α 和 β 按业务信任度取 0.4 和 0.6。这样既保留了数据本身的区分度,又不会让权重完全脱离业务经验。
2.3 两种归一化的分工:向量归一化喂TOPSIS,min-max归一化喂熵权法
归一化是 TOPSIS 代码里最容易混淆的一步。很多人把 min-max 归一化后的数据直接拿去算欧氏距离,觉得反正都压到 0 到 1 了,量纲统一了。这是错的。min-max 会改变指标内部的绝对间距,数据被整体平移压缩之后,欧氏距离的几何意义就变了,加权之后结果更不可控。
TOPSIS 标准做法是向量归一化:x_ij / sqrt(Σ x_ij²)。它把每个指标向量缩放到单位长度,保留指标内部的比例关系,和权重矩阵逐列相乘时不会破坏距离的几何性质。熵权法则相反,它需要的是“占比”意义上的归一化,min-max 后的数据算 p_ij 才有概率分布的含义。z-score 虽然均值归零,但会出现负值,负值没法算比重,所以也不能喂给熵权法。
分工总结一下:正向化后的数据分成两路,一路做 min-max 归一化喂给熵权法算权重,另一路做向量归一化后等权重算完之后逐列相乘,再做正负理想解距离计算。这两条路径不要交叉使用。
3. 加权TOPSIS的Python实现:从熵权法算权重到输出排名的一套完整代码
3.1 完整代码:正向化、熵权法、加权、正负理想解、贴近度一条龙
下面这段代码是我在多个项目里反复用过的版本。输入是一个 DataFrame,每行一个方案,每列一个指标;输出是每个方案的正理想解距离、负理想解距离、综合得分和排名。权重默认用熵权法自动算,也可以手动传入。
import numpy as np import pandas as pd # ---------- 1. 熵权法计算权重 ---------- def entropy_weight(data, epsilon=1e-12): """ 输入 data: 已做 min-max 归一化的矩阵, 形状 (n_samples, n_features) 返回 weights: 每个指标的熵权权重, 和为 1 """ # 防止除零: 列求和加极小值 P = data / (data.sum(axis=0) + epsilon) # 信息熵: 加 epsilon 防止 log(0) e_j = -1 / np.log(data.shape[0]) * np.sum(P * np.log(P + epsilon), axis=0) # 差异系数越大, 权重越大 d_j = 1 - e_j weights = d_j / d_j.sum() return weights # ---------- 2. 指标正向化 ---------- def positive_transform(df, types, mids=None): """ types: 每个指标一个方向, 'max' 极大型, 'min' 极小型, 'mid' 中间型 mids: 中间型指标的最佳取值列表, 与 types 中的 'mid' 一一对应 """ X = df.copy().astype(float) for i, t in enumerate(types): if t == 'max': continue elif t == 'min': # 极小型 -> 平移倒数正向化, eps 防止分母为 0 X.iloc[:, i] = 1 / (X.iloc[:, i] + 1e-8) elif t == 'mid': best = mids[i] # 偏离度越大, 正向化后越接近 0 M = np.max(np.abs(X.iloc[:, i] - best)) X.iloc[:, i] = 1 - np.abs(X.iloc[:, i] - best) / (M + 1e-8) return X # ---------- 3. 向量归一化 ---------- def vector_normalize(X): """把每列向量缩放到单位长度, 保留指标内部分布比例""" norms = np.sqrt(np.sum(X ** 2, axis=0)) + 1e-8 return X / norms, norms # ---------- 4. 加权 TOPSIS 主流程 ---------- def weighted_topsis(df, types, weights=None, mids=None): """ df: 原始指标表, 每行一个方案 types: 每个指标的方向类型 weights: 手动权重数组, 传 None 时自动用熵权法 mids: 中间型指标的最佳取值 返回: (结果表, 权重数组) """ X_pos = positive_transform(df, types, mids) # 第一路: min-max 归一化后喂给熵权法算权重 if weights is None: X_mm = (X_pos - X_pos.min(axis=0)) / (X_pos.max(axis=0) - X_pos.min(axis=0) + 1e-8) weights = entropy_weight(X_mm) # 第二路: 向量归一化后乘权重 X_norm, _ = vector_normalize(X_pos) # 权重归一化, 防止手动传的权重和不为 1 weights = np.array(weights, dtype=float) weights = weights / weights.sum() # 加权: 逐列相乘 X_weighted = X_norm * weights # 正负理想解: 加权后每列的最大值 / 最小值 ideal_best = X_weighted.max(axis=0) ideal_worst = X_weighted.min(axis=0) # 欧氏距离 dist_best = np.sqrt(((X_weighted - ideal_best) ** 2).sum(axis=1)) dist_worst = np.sqrt(((X_weighted - ideal_worst) ** 2).sum(axis=1)) # 贴近度: 越接近 1 越优 closeness = dist_worst / (dist_best + dist_worst) result = df.copy() result['正理想解距离'] = dist_best result['负理想解距离'] = dist_worst result['综合得分'] = closeness result['排名'] = result['综合得分'].rank(ascending=False, method='min') return result, weights # ---------- 5. 调用示例 ---------- if __name__ == "__main__": data = pd.DataFrame({ '价格': [120, 86, 112, 100], '性能': [8.2, 7.5, 9.0, 8.6], '响应时间':[35, 42, 30, 38], '售后': [0.9, 0.7, 0.8, 0.85] }) # 价格 / 响应时间是极小型, 性能和售后是极大型 types = ['min', 'max', 'min', 'max'] result, w = weighted_topsis(data, types) print("自动权重:", w) print(result)这份代码直接复制就能跑。结果表里综合得分就是贴近度,取值在 0 到 1 之间,越大代表离正理想解越近、离负理想解越远。排名用 method='min' 处理并列情况,出现同分时名次相同。
3.2 逐块拆解:每个函数参数怎么设,改什么会影响排名
熵权法的 epsilon 参数。它同时做了两件事:防止列求和为 0 时除零,以及防止 P 为 0 时 log(0) 报错。epsilon 设得过大,比如 1e-4,会明显压低低值区域的信息量,导致熵值偏大、权重失真;设 1e-12 或者直接用 1e-10 比较稳。如果数据本身很大,比如销售额到百万级,建议先做 min-max 归一化再进熵权法,这样 epsilon 的量级才匹配。
正向化的 types 和 mids。types 列表必须和 DataFrame 列顺序一一对应,漏一个错一个,排名会整体偏移。中间型指标必须给 mids,不能用默认值;我见过有人把中间型指标的 best 写成该列均值,这是常见的错误——均值不等于业务意义上的最佳值。
向量归一化的返回值。函数返回了 norms 但主流程里用下划线接收后丢弃了。如果你需要查看每个指标向量的原始长度,可以打印 norms 检查量级差异;如果某个 norms 特别小,说明该列数值整体很小,但权重可能很大,这个组合要留意。
贴近度和距离的关系。综合得分 = 负理想解距离 / (正理想解距离 + 负理想解距离)。这个比值天然落在 0 到 1 区间,只有权重归一化时不等于 1,比值会偏离这个区间。所以我在主流程里强制做了一次 weights = weights / weights.sum(),手动传权重也给你归一化好,避免越界。
3.3 不想要自动权重时:如何传一份专家权重并保持代码逻辑不变
熵权法只适合没有先验知识的情况。实际业务里,有时候客户明确说“价格权重必须 0.4,别的你们自己定”,或者团队已经用 AHP 算好了一组权重。这时候不用改函数,直接把 weights 参数传进去就行:
# AHP或者专家打分得到的权重, 顺序和 data 列顺序一致 expert_weights = [0.4, 0.3, 0.2, 0.1] result, w = weighted_topsis(data, types, weights=expert_weights)传入后主流程会自动归一化,所以你传 [0.4, 0.3, 0.2, 0.1] 和传 [4, 3, 2, 1] 结果是一样的。我建议还是传归一化后的值,方便检查权重比例是否符合业务预期。如果你想把熵权法和主观权重做组合,在调用前合并就行:
entropy_w, _ = weighted_topsis(data, types) # 先拿自动权重 alpha = 0.6 # 熵权占比 combined_w = alpha * entropy_w + (1 - alpha) * np.array(expert_weights) result, _ = weighted_topsis(data, types, weights=combined_w)这个组合权重的方式比纯熵权法稳健得多。纯熵权容易把离散度大的指标权重抬得太高,纯主观权重又容易陷入“谁嗓门大谁有理”的争议。用组合权重两边的质疑都能挡回去。
4. 加权TOPSIS避坑:5个让排名翻车的常见问题
做过多轮加权 TOPSIS 之后,我发现大多数翻车场景都集中在数据预处理和权重处理上,模型本身反而很少出问题。下面这五条是我真实踩过的坑,每条按现象、原因、解决来写,可以当成一个自查清单。
4.1 极小型指标没正向化,排名直接跟常识反着走
现象。价格越低越好,但跑出来的排名里,价格最高的方案排第一。检查原始数据,发现代码里 types 全写成了 'max'。
原因。TOPSIS 的“正理想解”取的是每列最大值。如果极小型指标不经过正向化,最大的价格值会被当成理想值,越贵的方案距离正理想解越近,排名自然和常识相反。
解决。types 里对价格这类指标写 'min',正向化函数会自动做倒数变换。检查方法也简单:把结果表按排名排序,肉眼扫一遍每个指标的方向是否符合业务直觉。这一步花不了 10 秒,能挡住 80% 的无效分析。
4.2 熵权法输入没做归一化,权重被量纲大的列绑架
现象。自动权重算出来,某列权重 0.9,其他几列加起来才 0.1。看了下数据,“价格”是百位数,“性能”是个位数。
原因。直接把原始数据喂给了 entropy_weight 函数。熵权法里的 p_ij 是列内比重,但 e_j 的计算会受数值绝对大小的影响,量纲大的列熵值偏低、权重虚高。这不是熵权法本身的问题,是输入数据不符合它的使用前提。
解决。记住一个顺序:正向化 → min-max 归一化 → 熵权法。加权 TOPSIS 的代码里权重计算分支已经做了 min-max,所以只要别绕过主流程单独调用 entropy_weight 就不会踩这个坑。如果自己单独调熵权法函数,务必先手动归一化。
4.3 常量列不处理:min-max 分母为 0,熵权法权重失真
现象。某列所有方案数值相同,比如售后期统一 12 个月。跑代码时 min-max 归一化那一步出现 NaN,或者熵权法给这一列算出一个非零权重。
原因。max - min = 0,分母保护加的是 1e-8,算出来这一列全是 0。熵权法再算时,p_ij = 0,log(0) 被 epsilon 兜住,但熵值趋近于 1,差异系数趋近于 0,权重按理该是 0;如果出现非零权重,说明 epsilon 设得太大,污染了计算。
解决。在建 DataFrame 之前做一次列方差检查,方差为 0 的列直接删除,或单独注明“指标无区分度,不参与本轮评估”。这样做还有一个好处:和业务方沟通时能说明白为什么某个指标没进模型,而不是留给对方猜。
4.4 权重数组没归一化,贴近度失去跨方案可比性
现象。手动传了权重 [0.5, 0.5, 0.5, 0.5],权重总和 2.0。结果综合得分普遍高于 0.8,所有方案挤在一起,排名虽然能出来,但无法分档。
原因。权重总和影响加权矩阵的尺度。正理想解和负理想解跟着一起缩放,理论上比值不变,但数值上的分布跨度会被压缩,直观上“所有方案都很优秀”,失去区分度。贴近度应该是一个概率意义上的相对指标,权重和偏离 1 太远,这个指标就不好解释了。
解决。主流程里已经做了 weights / weights.sum() 强制归一化,所以手动传 [0.5, 0.5, 0.5, 0.5] 也会被修正为 [0.25, 0.25, 0.25, 0.25]。提醒一点:修正后的结果和业务方拍板的权重比例是一致的,但需要主动说明“可解释的权重”和“参与计算的权重”之间的归一化关系,避免被当成黑匣子。
4.5 中间型指标的 best 值定错,正向化反而把排序搞乱
现象。某指标用 'mid' 类型处理,但排名结果和业务直觉明显冲突。查了代码,发现 mids 里填的是该列平均值。
原因。中间型指标的最佳值必须来自业务定义,不是统计值。比如设备工作温度,最佳值是厂家标称的 25 度,不是实测均值 32 度。用均值当 best,等于把“越接近大多数方案越好”当成了目标,正向化后所有方案都往中间挤,区分度被系统性抹平。
解决。定 mids 之前先画一列直方图,和业务方确认最佳取值范围。如果拿不准,宁可改成区间型处理:落在业务认可的区间内都取 1,区间外按距离衰减。这比定一个虚的 best 值稳健得多。我在实际项目中,凡是能拿到厂家标称值或行业标准的,一律用区间型。
5. 验证结果:权重敏感性分析和排名分档,比看单次排序更可靠
一次排名只能说明“在这个权重下谁最好”,但权重本身就带有不确定性。熵权法的权重来自样本数据,换一批方案权重可能变;专家权重来自人为判断,存在系统性偏差。所以我在每次报告里都会加一层权重敏感性分析,看看排名对权重的扰动有多敏感。
# 权重敏感性分析: 给权重加 ±10% 随机扰动, 观察排名波动 from collections import defaultdict def sensitivity_analysis(df, types, base_weights, n_trials=200, mids=None): rank_records = defaultdict(list) for _ in range(n_trials): # 在 0.9~1.1 之间随机扰动每个权重 noise = np.random.uniform(0.9, 1.1, size=len(base_weights)) w_trial = base_weights * noise w_trial = w_trial / w_trial.sum() # 扰动后重新归一化 result_trial, _ = weighted_topsis(df, types, weights=w_trial, mids=mids) for idx in result_trial.index: rank_records[idx].append(result_trial.loc[idx, '排名']) return {k: (np.mean(v), np.std(v)) for k, v in rank_records.items()} # 使用示例 sens = sensitivity_analysis(data, types, w) for name, (mean_rank, std_rank) in sens.items(): print(f"{name}: 平均排名 {mean_rank:.2f}, 标准差 {std_rank:.2f}")标准差大说明这个方案的排名对权重敏感,业务决策时要特别谨慎。比如平均排名 1.2、标准差 1.1 的方案A,和平均排名 2.0、标准差 0.2 的方案B,A 虽然均值排第一,但在某些合理权重组合下会掉到第二。这种场景我更倾向于选 B,理由是稳定性优先。
贴近度也建议做分档而不是直接咬死名次。我一般用 0.7 以上算第一梯队,0.5 到 0.7 算第二梯队,0.5 以下不建议采用。分档的好处是避免 0.63 和 0.64 之间的微弱差异被过度解读——在权重扰动下,这两个分数随时可能互换,但你不会因为 0.01 的差距把一个 0.63 的方案直接淘汰。
说个我自己的教训:之前做供应商评估,熵权法把“报价”权重拉到很高,结果前三名贴近度全部挤在 0.6 到 0.7 之间,排名天天变,会议开了三轮都没结论。后来加了敏感性分析才发现,有两家供应商的排名在权重轻微扰动下就会互换,根本原因是它们在各指标上互有胜负,综合实力非常接近。最后改成“两家都进入候选,进入二轮谈判”的处理方式,问题才解决。从那以后我就养成了一个习惯:单次排名只是起点,权重扰动下的稳定性才是决策依据。希望帮到你。
本文还有配套的精品资源,点击获取