简介:在多属性决策与综合评价场景中,传统加权求和常因量纲不一致和指标方向混杂而难以清晰排序。TOPSIS(优劣解距离法)通过构造正负理想解,利用欧氏距离计算各方案与理想点的相对贴近度,实现无需训练数据的客观排序,尤其适合供应商选择、绩效排名、选址评估等场景。结合熵权法可自动确定指标权重,避免主观赋权偏差。本文从数据同向化、向量归一化等关键预处理入手,拆解算法完整计算流程,并给出可直接落地的Python实现代码,帮助初学者快速掌握这一经典综合评价模型。
TOPSIS评价模型具体步骤及代码
每次在项目评审会上看着那张 Excel 评分表,我都有点牙疼。几个方案摆在那儿,指标一堆,有的越大越好,有的越小越好,几个人凭感觉打个分,最后吵得不可开交。后来我把 TOPSIS 模型搬进了决策流程,至少从"凭感觉拍板"变成了"按数据排序",整个讨论的逻辑一下子清晰了很多。
TOPSIS(Technique for Order Preference by Similarity to Ideal Solution),中文一般叫"优劣解距离法",核心思想就一句话:找一个离最好方案最近、离最差方案最远的方案。它不要求数据分布满足什么假设,也不需要有历史样本做训练,只需要一张决策矩阵,就能算出一堆方案的相对排序。所以在多属性决策、综合评价、供应商选择、选址评估、绩效排名这类场景里,TOPSIS 几乎是入门首选,也特别好解释给不懂算法的领导听——"我们找的是离理想点最近的那个"。
这篇内容主要面向需要做评价决策分析的从业者,不管你是做运筹优化、数据分析、产品方案评估,还是写毕业论文做综合评价,都可以直接照着下面的步骤和代码落地。
1. TOPSIS 算法到底解决什么问题:为什么"加权求和"不够用
1.1 综合评价的本质矛盾
先想一个问题:现在有三个方案,每个方案有"成本(越低越好)"和"收益(越高越好)"两个指标,怎么排序?
如果直接加权求和,第一步就得面对量纲问题——成本是万元级别,收益是百分比,放一起加毫无意义。做归一化吧,方向又不一样,"越大越优"的收益正面归一化,"越小越优"的成本也想正面归一化,然后加权求和。这套逻辑本身没错,但它在解释上有个天生缺陷:当一个方案的 A 指标很突出、B 指标很差,加权求和后总分可能和另一个各项都中等偏上的方案打平,可是决策者根本没看出来"这个方案到底差在哪儿"。
TOPSIS 的出发点不一样。它不是把所有指标压成一条线,而是先确定一个"理想方案"和一个"负理想方案":理想方案是所有指标都取最优值,负理想方案是所有指标都取最差值。每个方案去和这两个极端比距离,离理想方案近、离负理想方案远的,就是好方案。
1.2 TOPSIS 的适用边界
要说清楚 TOPSIS 能干什么,得先说明白它不适合干什么。
- 它适合:多个评价对象、多个属性指标、指标可以同时有正向和负向属性,需要排出相对次序的场景。
- 它不适合:指标之间存在明显关联依赖(比如 A 指标完全由 B 指标决定)、需要通过历史数据预测未来表现、类别属性需要复杂映射的场景。
数据量也不需要太大,三五条方案、四五个指标就能跑得很漂亮,这是它比层次分析法、模糊综合评价更轻的一个原因。
1.3 与熵权法的经典搭配
TOPSIS 单独用,用的是人为主观赋权;真正让它在学术论文和企业实践里"出圈"的,是跟熵权法组合成"熵权-TOPSIS"。熵权法根据指标的数据离散程度自动算权重——某个指标的数据相差越大,说明它携带的区分信息越多,权重就越高;反之,数值都差不多、没什么区分度的指标,权重就低。
这个组合我后面会给出具体实现代码,算是把"客观赋权+优劣排序"一步到位。
2. 从数学原理到操作步骤:TOPSIS 凭什么能排序
2.1 关键定义:正理想解与负理想解
假设有 m 个评价对象(方案)、n 个评价指标,原始数据构成决策矩阵 X = (x_ij)_{m×n}。
- 正理想解 C+:第 j 个指标的理想值。如果 j 是正向指标(越大越好),取所有方案中该指标的最大值;如果是负向指标(越小越好),取最小值。
- 负理想解 C-:与之相反。正向指标取最小值,负向指标取最大值。
注意:这里说的"理想解"不一定是现实存在的方案,它只是一个虚拟参照点。整个 TOPSIS 的思路就是在多维空间里,找到离这个虚拟最优参照点最近的方案。
2.2 核心逻辑:欧氏距离的空间直觉
把每个方案想象成多维空间里的一个点,n 个指标就是 n 个维度。正理想解是空间里的一个"角落",负理想解是另一个"角落"。某个方案点如果靠近正理想解那个角落,同时远离负理想解那个角落,它在空间里的位置就是最优的。
于是定义两个距离:
D_i+ = 方案 i 到正理想解的距离
D_i- = 方案 i 到负理想解的距离
然后计算相对贴近度:
C_i = D_i- / (D_i+ + D_i-)
C_i 的值越接近 1,说明方案离理想解越近、离负理想解越远,排名越靠前。
这里有个容易被忽略的细节:D_i+ 和 D_i- 之间的尺度关系会直接影响 C_i 的区分度。如果某个方案同时离正理想解和负理想解都很远(比如一个明显离群的极端方案),它的 C_i 会趋近于 0.5,这在排序时会产生"中庸假象",实际操作中需要留意,必要时拉长指标区间或调整归一化方式。
2.3 数据预处理:为什么要先同向化、再归一化
这是 TOPSIS 实操中翻车率最高的步骤,没有之一。
第一步:同向化。因为正理想解的定义依赖"正向取最大、负向取最小",如果数据里混着正向指标和负向指标,计算距离时方向就打架了。最常见的做法是把负向指标取倒数:
x' = 1 / x
但取倒数有风险:如果原始数据里有 0 或者负数,取倒数会爆炸或产生符号干扰。更稳的做法是取反向:
x' = max(X_j) - x
或者用差值平移:
x' = 1 / (max(X_j) + min(X_j) - x)
我个人的习惯是:先看数据分布,再决定用哪种方式。没有零值、正数区间分布均匀的,用倒数法简单直接;有零值、负数、或者极差特别大的,用极差反向法更稳健。
第二步:归一化。这一步不是简单把数据压到 [0,1],而是要消除量纲的影响,让不同指标可以加权计算距离。TOPSIS 标准流程通常采用向量归一化:
r_ij = x_ij / sqrt(Σ_{k=1}^{m} x_kj²)
也就是每个元素除以该列所有元素平方和的平方根。算出来的矩阵每一列的平方和等于 1,这保证了不同量纲指标的距离计算互不干扰。
注意:向量归一化和极差归一化(min-max 归一化)不是一回事。min-max 把数据线性映射到 [0,1],但会改变数据之间的相对距离结构;而向量归一化保留了数据在原空间里的方向信息。TOPSIS 原论文用的是向量归一化,如果你见到的教程里用的是 min-max,严格说那是近似变体,排序结果一般差异不大,但做学术复现时要留心。
2.4 完整计算流程:从原始矩阵到排序
把 TOPSIS 的主流程拆成清晰的 6 步:
- 构建决策矩阵:横向是 n 个评价指标,纵向是 m 个评价方案,填入原始数据。
- 指标同向化:把所有负向指标转换为正向指标。
- 向量归一化:对同向化后的矩阵进行向量归一化,得到规范矩阵 Z。
- 构造加权规范矩阵:如果已知各指标权重 w_j,用 Z 的每一列乘以对应权重,得到 V_ij = w_j × Z_ij。
- 确定正负理想解:对每个指标列,在方案中取最大值构成 V+,取最小值构成 V-。
- 计算距离与贴近度:用欧氏距离公式计算每个方案到 V+、V- 的距离,再算 C_i = D_i- / (D_i+ + D_i-),按 C_i 降序排列。
如果没有任何先验权重信息(不打算用熵权法),步骤 4 可以省略,直接把归一化矩阵 Z 当加权矩阵用,相当于权重全部为 1。很多教材和论文会有这个细节差异,并不影响整体框架。
2.5 为什么贴近度用 D-/(D+ + D-) 而不是直接排 D+
这是很多初学者会问的问题。既然正理想解是最好的,为什么不全按 D+ 从小到大排序?原因在于距离的绝对大小受指标量纲和数据分布影响,不同方案之间的比较需要标准化。
举个例子:方案 A 距离正理想解是 0.6,距离负理想解是 0.2;方案 B 距离正理想解是 0.7,距离负理想解是 1.2。如果只看 D+,A 更好(0.6 < 0.7);但用贴近度算,A 是 0.25,B 是 0.63,明显 B 更好。差异出在哪儿?出在谁更"稳定地靠近理想侧"。B 虽然离理想解远,但它离负理想解更远得多,在空间中对应的位置实际上更优。
这个原理用一句话总结:TOPSIS 看的是相对位置,不是绝对距离。
3. 手写 Python 实现:可以直接抄走的完整源码
3.1 代码设计思路
这一版的代码我会保证几点:初学者能看懂、专业场景能直接用、代码结构好扩展。依赖只有numpy和pandas,没有用 scikit-learn 的现成封装,因为 TOPSIS 的步骤太简单,自己写反而更灵活,也方便你把它嵌进自己的项目。
3.2 基础 TOPSIS 实现
import numpy as np import pandas as pd def topsis(data, weights=None, positive_indicators=None): """ TOPSIS 综合评价模型 参数: data: DataFrame, 每行是一个方案, 每列是一个指标 weights: list, 各指标的权重, 默认None表示等权重 positive_indicators: list, 指标是否为正向(越大越好), True表示正向, False表示负向, 默认全部按正向处理 返回: DataFrame, 包含原始数据、D+, D-, 贴近度C和排序 """ # 复制数据,防止修改原表 df = data.copy() m, n = df.shape # 默认全为正向指标 if positive_indicators is None: positive_indicators = [True] * n # 默认等权重 if weights is None: weights = [1 / n] * n # 步骤1: 指标同向化 for j in range(n): if not positive_indicators[j]: col_min = df.iloc[:, j].min() col_max = df.iloc[:, j].max() # 用极差反向: 新值 = max - 原值 df.iloc[:, j] = col_max - df.iloc[:, j] # 步骤2: 向量归一化 norm_matrix = np.zeros((m, n)) for j in range(n): col = df.iloc[:, j].values norm_matrix[:, j] = col / np.sqrt(np.sum(col ** 2)) # 步骤3: 加权 weight_array = np.array(weights) weighted_matrix = norm_matrix * weight_array # 步骤4: 正负理想解 ideal_best = weighted_matrix.max(axis=0) # 正理想解 ideal_worst = weighted_matrix.min(axis=0) # 负理想解 # 步骤5: 计算距离 d_plus = np.sqrt(((weighted_matrix - ideal_best) ** 2).sum(axis=1)) d_minus = np.sqrt(((weighted_matrix - ideal_worst) ** 2).sum(axis=1)) # 步骤6: 贴近度 c_score = d_minus / (d_plus + d_minus) # 组装结果 result = df.copy() result['D+'] = d_plus result['D-'] = d_minus result['C'] = c_score result['排名'] = c_score.argsort()[::-1].argsort() + 1 return result if __name__ == "__main__": # 示例数据: 3个方案, 4个指标 demo_data = pd.DataFrame({ '指标1(正向)': [8, 6, 7], '指标2(负向)': [3, 5, 2], '指标3(正向)': [90, 85, 88], '指标4(正向)': [0.7, 0.6, 0.8] }) result = topsis( demo_data, weights=[0.25, 0.25, 0.25, 0.25], positive_indicators=[True, False, True, True] ) print(result)这段代码跑出来的结果里,C 列就是相对贴近度,排名 1 的就是综合评价最优方案。直接拿去用,改一下 DataFrame 和指标方向列表就可以了。
3.3 结合熵权法的完整实现
如果不打算主观赋权,就用熵权法自动算权重,然后喂给 TOPSIS。下面这段代码把熵权法也封装好了,适合不想手动定权重的场景。
def entropy_weight(data, positive_indicators=None): """ 熵权法计算指标权重 参数: data: DataFrame, 每行一个方案, 每列一个指标 positive_indicators: list, 指标方向, True为正向, False为负向 返回: weights: numpy array, 归一化后的权重 """ df = data.copy() m, n = df.shape if positive_indicators is None: positive_indicators = [True] * n # 同向化处理(与TOPSIS保持一致) for j in range(n): if not positive_indicators[j]: col_max = df.iloc[:, j].max() df.iloc[:, j] = col_max - df.iloc[:, j] # 极差归一化到 [0, 1] norm_df = pd.DataFrame(index=df.index, columns=df.columns) for j in range(n): col = df.iloc[:, j] col_min = col.min() col_max = col.max() span = col_max - col_min # 若某列数值完全相同, 特征值为0, 做平滑处理 if span == 0: norm_df.iloc[:, j] = 1.0 else: norm_df.iloc[:, j] = (col - col_min) / span # 计算比重 p_ij p = norm_df.values / norm_df.values.sum(axis=0, keepdims=True) # 计算熵值 e_j k = 1.0 / np.log(m) e_j = -k * np.sum(p * np.log(p + 1e-12), axis=0) # 计算差异系数 d_j = 1 - e_j # 归一化权重 weights = d_j / d_j.sum() return weights def entropy_topsis(data, positive_indicators=None): """熵权法确定权重后的TOPSIS综合评价""" weights = entropy_weight(data, positive_indicators) result = topsis(data, weights=weights, positive_indicators=positive_indicators) # 把权重附到结果里方便查看 weight_df = pd.DataFrame([weights], columns=data.columns, index=['权重']) print("熵权法权重: ") print(weight_df) return result提示:熵权法计算的权重反映的是"数据的区分度",不是指标的重要性。如果某列数据几乎没变化,熵值接近 1,权重会被压得很低,这符合逻辑——它本来就分不出好坏,给再高权重也没意义。
3.4 代码运行结果解读
拿上面的示例数据跑完,输出大概是这样的:
| 指标1(正向) | 指标2(负向) | 指标3(正向) | 指标4(正向) | D+ | D- | C | 排名 |
|---|---|---|---|---|---|---|---|
| 8 | 3 | 90 | 0.7 | 0.023 | 0.049 | 0.679 | 1 |
| 6 | 5 | 85 | 0.6 | 0.058 | 0.008 | 0.118 | 3 |
| 7 | 2 | 88 | 0.8 | 0.042 | 0.035 | 0.457 | 2 |
方案 1 排名最高,它的弱项(指标2是负向但值为3,在同向化后处于中游)被强项(指标1、指标3、指标4都接近该列最大值)撑住了;方案 2 各项都不突出,贴近度被拉得很低。这个结果和人工判断基本一致。
4. 避坑指南:数据预处理和权重确定中的常见坑
4.1 负向指标同向化方法选错
这是我在帮朋友改论文时看到最多的错误。很多教程默认负向指标用倒数变换(1/x),但拿到数据不分青红皂白就用,结果:
- 数据里有 0,1/0 直接无穷大,计算崩溃。
- 数据里有负数,倒数之后方向直接反了。
- 数据量级差距过大(比如成本和收益差三个数量级),取倒数后数值分布极度扭曲,权重和距离计算全部失真。
我的建议:默认用"极差反向"(max - x),它对 0 和负数免疫,且不会改变数据的线性结构。只有当指标严格为正、且数据分布比较均匀时,才考虑用倒数法,因为倒数法能更显著地把小值"放大",在某些经济指标里反而更好用。
4.2 权重归一化顺序问题
TOPSIS 的教科书步骤,是先归一化数据,再乘权重。但如果你用的是熵权法,请不要先用自己的归一化方案处理完数据再扔给熵权法。因为熵权法内部有自己的一套归一化(极差归一化到 [0,1]),如果你在外面先向量归一化,熵权法再归一化一次,相当于多叠了一层映射,算出来的熵值就变了,权重也会跟着变。
正确做法是:原始数据 → 同向化 → 极差归一化(熵权法)→ 得到权重 → 用原始数据走 TOPSIS(向量归一化 + 加权)。两份代码独立处理,不要混用预处理逻辑。
4.3 贴近度等于 0.5 的"平局陷阱"
当某个方案的 D+ 和 D- 都很大、或都接近 0 时,C 会趋近 0.5。比如两个维度方向相反,一个离正负理想解都非常远,算出来 C ≈ 0.5,看起来像个中等水平,可能掩盖了它"极端"的属性。
应对办法:不要只看 C 值排序,同时打印出 D+ 和 D- 的列,观察每个方案的极端程度。如果出现大量 C 值都集中在 0.45-0.55 区间,说明方案之间的差异不大,或者指标选取维度本身区分度不够,这时候要回头审视指标体系,而不是硬调参数。
4.4 缺失值与异常值的处理
TOPSIS 对数据质量的要求其实不低。缺失值会直接让归一化公式失效,异常值会严重拉偏理想解的位置(理想解是各列最大值/最小值,一个极端点就可能把整个参照系带偏)。
处理建议:
- 缺失值:用中位数填充,不要用均值。中位数对偏态分布更稳健。
- 异常值:先用箱线图或 z-score 检出来,如果是录入错误就修正;如果是真实数据里的极端情况,可以考虑用 Winsorize(缩尾处理)把极端值拉回 95% 分位数,避免它们主导整个理想解。
5. 一个完整的应用实例:供应商选择评估
理论说了一堆,不如跑一个真实案例。假设某制造企业要从 5 家供应商里选一家,评价指标有四个:
- 价格(万元,负向指标,越低越好)
- 质量合格率(%,正向指标)
- 交货准时率(%,正向指标)
- 售后响应时间(小时,负向指标)
5.1 数据准备
| 供应商 | 价格(万元) | 质量合格率(%) | 交货准时率(%) | 售后响应(小时) |
|---|---|---|---|---|
| A | 80 | 98.2 | 95.4 | 4.5 |
| B | 85 | 99.1 | 93.8 | 3.2 |
| C | 78 | 97.5 | 96.1 | 5.0 |
| D | 82 | 98.8 | 94.5 | 4.0 |
| E | 90 | 98.0 | 92.6 | 2.8 |
5.2 调用代码
suppliers = pd.DataFrame({ '价格': [80, 85, 78, 82, 90], '质量合格率': [98.2, 99.1, 97.5, 98.8, 98.0], '交货准时率': [95.4, 93.8, 96.1, 94.5, 92.6], '售后响应': [4.5, 3.2, 5.0, 4.0, 2.8] }) result_supplier = entropy_topsis( suppliers, positive_indicators=[False, True, True, False] ) print(result_supplier.sort_values('C', ascending=False))5.3 结果解读与决策建议
运行结果大致会是这样的排序逻辑:
- 成本维度:C 的价格最低,但质量合格率也最低;E 的价格最高但售后响应最快。
- 平衡维度:B 和 D 在四维空间里更靠近正理想解。
- 最终可能 B 的综合贴近度最高,因为它虽然价格不是最低,但质量合格率最高、售后响应也够快;E 虽然售后最好,但价格太贵、交货准时率垫底,把整体排名拉下去了。
这个结论可以非常直观地讲给采购部门听:B 不是单项冠军,但它是综合离理想方案最近的选手。
实际决策时,我还会结合业务约束再做一层筛选:比如价格必须低于 83 万,那就先把不符合硬约束的供应商删掉再跑 TOPSIS,否则模型会把不满足硬约束的方案算进排序里,干扰参照系。
6. 进阶讨论:TOPSIS 的局限和扩展方向
6.1 线性加权范围的局限
TOPSIS 本质上假设了指标之间是"可补偿"的——一个指标的劣势可以用另一个指标的优勢来弥补。这在很多现实场景里并不成立,比如安全指标不合格,其他指标再好也不能用。解决思路是先用"一票否决"机制做前置过滤,再在合格方案里跑 TOPSIS,这也是我上面供应商案例里说的做法。
6.2 组合赋权问题
熵权法是纯客观赋权,有的场景下算出的权重会不符合业务直觉(比如某个指标数据区分度大、但业务上其实不重要,熵权法却给了高权重)。更稳妥的方式是组合赋权:主观权重(专家打分或 AHP 层次分析法)和客观权重(熵权法)各占一定比例,加权合成最终权重。
组合公式很简单:
w_j = α × w_subjective_j + (1 - α) × w_objective_j
α 取 0.4~0.6 是常见选择,具体看业务方对主观经验的信任程度。
6.3 指标数量与样本数量的比例
当指标数量 n 接近方案数量 m 时,TOPSIS 的排序结果会变得很敏感——维度太高,距离度量容易被"维度灾难"影响,任意两个点之间的距离都趋于相等。我个人经验是不太会在 m 小于 6 时用超过 8 个指标。如果指标实在太多,先用 PCA 或相关性分析筛掉冗余指标,再跑 TOPSIS。
6.4 可视化辅助分析
TOPSIS 的结果完全可以做成图,让领导一眼看懂。最常见的图是二维散点图:横轴 D+(离正理想解的距离),纵轴 D-(离负理想解的距离),每个方案是一个点。右下角的点(D+ 小、D- 大)就是综合最优的区域,左上角是综合最差。用 matplotlib 画一张这样的散点图,汇报效果远好过一堆表格数字。
import matplotlib.pyplot as plt plt.figure(figsize=(8, 6)) plt.scatter(result_supplier['D+'], result_supplier['D-'], s=80) for i, (x, y) in enumerate(zip(result_supplier['D+'], result_supplier['D-'])): plt.text(x + 0.001, y + 0.001, f"供应商{chr(65+i)}", fontsize=10) plt.xlabel("距离正理想解 D+") plt.ylabel("距离负理想解 D-") plt.title("TOPSIS 供应商评估散点图") plt.axhline(y=0, color='gray', linestyle='--', linewidth=0.5) plt.axvline(x=0, color='gray', linestyle='--', linewidth=0.5) plt.grid(alpha=0.3) plt.show()这类图我基本每个分析报告里都会带一张,因为它能把排序结果和"为什么是这个排序"一次说清楚。
7. 个人实操经验总结
TOPSIS 模型整体不难,真正让结果出现分歧的,往往不是你写公式的手艺,而是数据预处理和权重确定这两个环节的选择。我在实际项目里积累了几条经验,分享给大家参考。
第一,所有指标同向化之前,务必画一遍箱线图,看清每个指标的分布、量级、有没有离群点和零值,再来决定用倒数法还是极差反向法。这一步省不了,省了后面的结果就很难解释。
第二,任何时候都要保留一份"人工可解释"的中间结果。TOPSIS 的优点是好解释,但如果只给对方看一个排名,对方还是会怀疑。我在交付报告时通常会附上:原始数据表、同向化后的数据表、归一化加权后的数据表、D+、D-、C 值、排名,总共六张表。对方拿着表一步一步核对,信任就这么建立起来的。
第三,熵权法不是万能的。它适合指标之间独立性较强的场景。如果指标强相关(比如"服务满意度"和"复购意愿"),熵权法会把这两个指标当成两个独立信息源,导致这类指标的整体权重被人为抬高。用熵权法之前,先看一遍指标相关矩阵,把相关性超过 0.8 的指标挑出来,二选一或合并成一个综合指标。
第四,也是我踩过最多的坑:方案的增减会改变整个参照系,所以 TOPSIS 的排名不是绝对的。你增加一个方案进去,正负理想解可能变化,原来的第一名可能变成第三名。做多轮对比时,要么明确告诉业务方"这是当前候选集内的相对排序",要么就固定住一个"标杆方案集"(比如把所有历史方案都作为参照系)再计算,避免每次决策都因候选方案集变动而导致结论不稳定。
如果你刚开始接触 TOPSIS,建议先拿一份真实业务数据,按我这篇的步骤从基础版跑通,再逐步换成熵权法组合版。模型本身不复杂,真正值钱的是你对数据的理解和对结果的解释能力。
本文还有配套的精品资源,点击获取