ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

加权TOPSIS与熵权法详解:指标正向化到Python实现

加权TOPSIS与熵权法详解:指标正向化到Python实现 简介这是基于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 两种归一化的分工向量归一化喂TOPSISmin-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, epsilon1e-12): 输入 data: 已做 min-max 归一化的矩阵, 形状 (n_samples, n_features) 返回 weights: 每个指标的熵权权重, 和为 1 # 防止除零: 列求和加极小值 P data / (data.sum(axis0) epsilon) # 信息熵: 加 epsilon 防止 log(0) e_j -1 / np.log(data.shape[0]) * np.sum(P * np.log(P epsilon), axis0) # 差异系数越大, 权重越大 d_j 1 - e_j weights d_j / d_j.sum() return weights # ---------- 2. 指标正向化 ---------- def positive_transform(df, types, midsNone): 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, axis0)) 1e-8 return X / norms, norms # ---------- 4. 加权 TOPSIS 主流程 ---------- def weighted_topsis(df, types, weightsNone, midsNone): 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(axis0)) / (X_pos.max(axis0) - X_pos.min(axis0) 1e-8) weights entropy_weight(X_mm) # 第二路: 向量归一化后乘权重 X_norm, _ vector_normalize(X_pos) # 权重归一化, 防止手动传的权重和不为 1 weights np.array(weights, dtypefloat) weights weights / weights.sum() # 加权: 逐列相乘 X_weighted X_norm * weights # 正负理想解: 加权后每列的最大值 / 最小值 ideal_best X_weighted.max(axis0) ideal_worst X_weighted.min(axis0) # 欧氏距离 dist_best np.sqrt(((X_weighted - ideal_best) ** 2).sum(axis1)) dist_worst np.sqrt(((X_weighted - ideal_worst) ** 2).sum(axis1)) # 贴近度: 越接近 1 越优 closeness dist_worst / (dist_best dist_worst) result df.copy() result[正理想解距离] dist_best result[负理想解距离] dist_worst result[综合得分] closeness result[排名] result[综合得分].rank(ascendingFalse, methodmin) 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 之间越大代表离正理想解越近、离负理想解越远。排名用 methodmin 处理并列情况出现同分时名次相同。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, weightsexpert_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, weightscombined_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 0log(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_trials200, midsNone): rank_records defaultdict(list) for _ in range(n_trials): # 在 0.9~1.1 之间随机扰动每个权重 noise np.random.uniform(0.9, 1.1, sizelen(base_weights)) w_trial base_weights * noise w_trial w_trial / w_trial.sum() # 扰动后重新归一化 result_trial, _ weighted_topsis(df, types, weightsw_trial, midsmids) 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 的方案BA 虽然均值排第一但在某些合理权重组合下会掉到第二。这种场景我更倾向于选 B理由是稳定性优先。贴近度也建议做分档而不是直接咬死名次。我一般用 0.7 以上算第一梯队0.5 到 0.7 算第二梯队0.5 以下不建议采用。分档的好处是避免 0.63 和 0.64 之间的微弱差异被过度解读——在权重扰动下这两个分数随时可能互换但你不会因为 0.01 的差距把一个 0.63 的方案直接淘汰。说个我自己的教训之前做供应商评估熵权法把“报价”权重拉到很高结果前三名贴近度全部挤在 0.6 到 0.7 之间排名天天变会议开了三轮都没结论。后来加了敏感性分析才发现有两家供应商的排名在权重轻微扰动下就会互换根本原因是它们在各指标上互有胜负综合实力非常接近。最后改成“两家都进入候选进入二轮谈判”的处理方式问题才解决。从那以后我就养成了一个习惯单次排名只是起点权重扰动下的稳定性才是决策依据。希望帮到你。本文还有配套的精品资源点击获取
返回列表