TOPSIS综合评价方法详解:从原理到Python代码实现
2026/8/28 20:30:31 网站建设 项目流程

简介:在工程实践和商业决策中,面对多方案、多指标的复杂评价问题,如何从数据中提炼出客观的排序依据?综合评价方法给出了结构化解决思路,其中优劣解距离法(TOPSIS)因其计算直观、对数据分布无苛刻要求而应用广泛。该方法通过构造正负理想解,以相对贴近度衡量各方案与最优状态的几何距离,适用于供应商选择、产品评比、风险评估等场景。配合熵权法确定指标权重,可减少主观偏差,提升决策可复现性。本文从指标正向化、向量归一化到加权决策矩阵,逐步拆解计算流程,并提供可直接复用的Python实现,帮助读者在真实业务中快速落地多准则决策分析。

1. TOPSIS到底在解决什么问题

1.1 评价与决策:从拍脑袋到有依据

先想一个很现实的场景:你是一家公司的采购负责人,面前有三家供应商,每家报价不同、交货周期不同、产品质量不同、售后服务也不同。你要怎么选?是挑最便宜的,还是挑质量最好的?如果单纯看某一项指标,事情很简单;但现实是多个指标往往相互冲突——便宜的未必质量好,质量好的未必交期准,交期准的未必售后响应快。这就是典型的多指标综合评价问题。

再举个日常的例子:你想买一部手机,预算范围内有三款候选机型。华为的拍照好、小米的性价比高、苹果的系统流畅。每款机各有优势,可钱包只有一个,到底选哪个?这种"多个方案、多个指标、指标之间还互相打架"的决策场景,靠直觉拍脑袋很容易后悔。这时候就需要一种结构化的评价方法,把各个方案在各指标上的表现"折算"成一个可比较的综合得分,然后按得分排序。

TOPSIS(Technique for Order Preference by Similarity to Ideal Solution)就是干这件事的。中文名字很多,常见叫优劣解距离法或者逼近理想解排序法。它的核心逻辑特别朴素:构造一个"各项指标都最优"的虚拟理想方案,再构造一个"各项指标都最差"的虚拟负理想方案,然后看每个备选方案离理想方案有多近、离负理想方案有多远。离理想最近同时离负理想最远的,就是综合最优的方案。

1.2 TOPSIS的核心思想

很多人第一次听"优劣解距离法"这个名字会觉得陌生,但理解了它的几何意义就很简单。

把每个方案想象成高维空间里的一个点,每个维度就是一个评价指标。这个高维空间里有一个"虚拟的最佳点",是所有指标理想值的组合;还有一个"虚拟的最差点",是所有指标最差值的组合。TOPSIS做的事情就是计算每个方案点到这两个特殊点的距离。

理想情况下,你希望一个方案点离理想点越近越好,离负理想点越远越好。但实际中,离理想点近的方案可能离负理想点也不远,所以TOPSIS采用了一个综合指标——相对贴近度:定义成"到负理想点的距离 /(到理想点的距离 + 到负理想点的距离)"。这个值越大,说明方案越优。

这个思想用生活化的话说就是:既要向优秀看齐,又要与糟糕划清界限。有个方案虽然离理想状态不远,但如果它离最差状态也很近,说明它各方面表现很平均,缺少突出的短板优势;而另一个方案也许离理想状态稍微远一点,但离最差状态非常远,说明它再差也差不到哪里去。相对贴近度综合了这两个方面。

1.3 为什么选TOPSIS而不是别的方法

做综合评价的方法其实很多:层次分析法(AHP)、模糊综合评价、灰色关联度分析、数据包络分析(DEA)等等。为什么TOPSIS是出镜率最高的那一个?从我实际使用的体验来说,有几点是其他方法很难比的:

  • 对数据分布没有苛刻要求:不像某些方法要求数据服从特定分布,TOPSIS只要求数据是正向化、标准化后的实数矩阵,适用范围极广。
  • 样本量大小都能用:层次分析法依赖专家打分,样本量小或指标多时一致性检验很容易出问题;TOPSIS则对样本量的要求很低,哪怕只有4、5个方案也能算。
  • 计算过程直观透明:整个计算流程就是加减乘除,没有复杂的数学推导,用Excel都能手动算出来,也方便向非技术背景的决策者解释结果。
  • 可以灵活嵌入权重:TOPSIS本身不强制要求权重怎么定,你可以用等权重,也可以用熵权法、层次分析法确定权重后代入,灵活性很高。

当然,TOPSIS也有短板:它假设指标之间是相互独立的,如果指标之间强相关,结果会被重复计算某些信息。此外,它给出的只是方案的相对排序,不是说得分0.8的方案绝对好于得分0.6的方案多少。这些在使用时心里要有数。

2. TOPSIS的具体计算步骤拆解

2.1 构建原始数据矩阵

正式开始前,先把问题抽象成矩阵形式。假设你有m个待评价的方案(比如m款手机),有n个评价指标(比如价格、性能、续航、拍照等),那么这些数据就构成了一个m行n列的矩阵X:

X = [x11 x12 ... x1n x21 x22 ... x2n ... xm1 xm2 ... xmn]

其中xij表示第i个方案在第j个指标上的取值。

这里有个容易忽略的点:指标的方向性问题。实际操作中指标通常分几类:

  • 极大型指标(效益型):越大越好,比如性能跑分、续航时间、屏幕分辨率。
  • 极小型指标(成本型):越小越好,比如价格、重量、功耗。
  • 中间型指标:越接近某个固定值越好,比如血液的pH值最理想是7.4,偏离太多都不好。
  • 区间型指标:落在某个区间内最好,比如体温36.0到37.2摄氏度属于正常范围。

TOPSIS的计算公式默认所有指标都是极大型的,所以第一步往往需要把各类指标统一转成"越大越好"的形式,这个步骤叫正向化处理

2.2 指标正向化处理

极小型转极大型

最常规的做法是做倒数变换或者取反:

x' = 1/x (要求x > 0)

或者

x' = max(x) - x

实际中我用倒数变换比较多,因为它不改变数据的相对差异程度,而且当指标值很小时,变换后的差异会被放大,有利于区分方案。但如果原始数据中存在0值,就不能用倒数了,用max - x更稳妥。

中间型转极大型

假设最优的中间值是x_best,那么正向化公式是:

x' = 1 - |x - x_best| / max(|x - x_best|)

这样当x等于最优值时,x'取到最大值1;偏离越远,x'越接近0。

区间型转极大型

假设最优区间是[a, b],也就是指标取值落在[a, b]内都算好,那么正向化公式分段处理:

x' = 1 - (a - x) / (a - min(x)) 当 x < a x' = 1 当 a <= x <= b x' = 1 - (x - b) / (max(x) - b) 当 x > b

这里的min(x)和max(x)指的是所有方案在该指标上的最小值和最大值。

正向化处理是TOPSIS实操中第一个容易出错的地方。尤其在纯代码实现时,很多人忘记先判断指标类型,直接把整个矩阵丢进去标准化,最后结果就会失真。我自己早期也踩过这个坑,后来形成习惯:写代码前先看一眼原始数据,确认每个指标是极大型、极小型、中间型还是区间型,逐一处理。

2.3 数据标准化

正向化之后,数据还需要标准化,目的是消除不同指标的量纲影响。比如"价格"单位是元,动辄几千;"续航"单位是小时,通常只有几十。如果不标准化,价格这个维度在距离计算中会主导一切,其他指标形同虚设。

TOPSIS中常用的标准化方法是向量归一化法,也叫min-max归一化的另一种形式。具体公式为:

z_ij = x'_ij / sqrt( sum(x'_ij^2, i=1..m) )

也就是每个元素除以它所在列向量(所有方案在该指标上的取值)的模长(即平方和的平方根)。标准化后的矩阵Z,每列元素的平方和等于1。

也有人用min-max归一化(把数据缩放到[0,1]区间):

z_ij = (x'_ij - min(x'_j)) / (max(x'_j) - min(x'_j))

两种方式在TOPSIS框架下都能用,但结果会有细微差别。向量归一化是TOPSIS文献中最标准的做法,我这里优先推荐。如果用熵权法计算权重,标准化的方式还直接影响熵值,后面详说。

2.4 确定各指标权重

TOPSIS框架本身可以不带权重,但现实里指标的重要程度通常不一样。比如选手机时,预算紧张的人会觉得价格权重很高,拍照爱好者的权重分配又会完全不同。

权重的确定方法常见有三种:

  • 等权重法:所有指标权重相同,最省事,适合没有额外偏好信息、指标本身同等重要的场景。
  • 熵权法(EWM):完全由数据驱动,根据指标的数据变异程度来定权。某个指标在所有方案上取值越接近,说明它对区分方案的贡献越小,熵值越大,权重应该越低;反之,指标取值差异越大,信息量越大,权重越高。
  • 层次分析法(AHP):依赖专家两两比较打分,主观性较强,但能把决策者的经验考虑进来。

在实际项目中,我经常用的是熵权法 + TOPSIS的组合:先用熵权法从数据本身挖掘权重信息,再代入TOPSIS计算排序。这样整个过程完全客观,不需要人为打分,可复现性强。后面代码部分会给出完整的熵权法实现。

2.5 构造加权决策矩阵

标准化完成后,如果确定了一组权重w_1, w_2, ..., w_n(满足w_j非负且和为1),就要构造加权决策矩阵V:

v_ij = w_j * z_ij

这一步的意思是:在消除了量纲影响后,再乘以权重,体现各指标的重要程度。加权后的矩阵V,才是计算距离的真正依据。

2.6 确定正理想解与负理想解

加权决策矩阵构造完成后,找出正理想解和负理想解。由于前面已经做过了正向化处理,此时所有指标都是"越大越好",所以:

  • 正理想解A+:每一列的最大值组成的向量,即各指标在所有方案中的最优值。它代表一个虚拟的"完美方案"。
  • 负理想解A-:每一列的最小值组成的向量,即各指标在所有方案中的最差值。它代表一个虚拟的"最差方案"。

公式表达:

A+ = {max(v_ij) | j=1,2,...,n} A- = {min(v_ij) | j=1,2,...,n}

需要注意,如果存在某一列标准化后全是同一个值,那么正负理想解在该维度上相等,该维度对距离计算的贡献就会变为0,这是合理的——这个指标在所有方案中完全一致,本来就不具有区分力。

2.7 计算距离与相对贴近度

接下来计算每个方案到正理想解和负理想解的距离,通常使用欧氏距离

D_i+ = sqrt( sum( (v_ij - A_j+)^2 ) ) D_i- = sqrt( sum( (v_ij - A_j-)^2 ) )

这两个距离分别表示第i个方案离"完美方案"和"最差方案"的远近程度。

最后计算相对贴近度C_i:

C_i = D_i- / (D_i+ + D_i-)

这里要做个约定:当D_i+ = 0(方案就是正理想解)时,C_i = 1,达到最大值;当D_i- = 0(方案就是负理想解)时,C_i = 0,达到最小值。实际计算中如果分母出现0(即D_i+和D_i-同时为0),说明所有方案完全一致,这种情况一般不会出现,但要加个防御性判断,避免除零错误。

C_i的取值范围在[0,1]之间,C_i越大,说明方案i越接近正理想解、远离负理想解,方案越优。按C_i从大到小排序,就得到了最终方案排名。

2.8 一个手算小例子

为了确保逻辑没跑偏,我拿一个非常小的例子手动算一遍。假设要评估3款笔记本电脑,就2个指标:性能跑分(极大型)和价格(极小型)。

原始数据:

方案性能跑分价格(元)
A906000
B754500
C603000

第一步正向化:性能跑分已经是极大型,保持不动。价格是极小型,做倒数变换,得到价格正向值:A为0.0001667,B为0.0002222,C为0.0003333。

第二步标准化:对性能跑分这一列,模长 = sqrt(90^2 + 75^2 + 60^2) = sqrt(8100 + 5625 + 3600) = sqrt(17325) ≈ 131.62,于是三个标准化值为90/131.62≈0.6838,75/131.62≈0.5698,60/131.62≈0.4559。

价格一列同理,模长 = sqrt(0.0001667^2 + 0.0002222^2 + 0.0003333^2) ≈ 0.0004319,标准化后分别为0.3861、0.5146、0.7718。

第三步设等权重:两个指标权重都是0.5。加权后:

  • A:(0.3419, 0.1931)
  • B:(0.2849, 0.2573)
  • C:(0.2280, 0.3859)

第四步确定正负理想解:正理想解 = (0.3419, 0.3859),负理想解 = (0.2280, 0.1931)。

第五步计算距离:

  • A的D+ = sqrt((0.3419-0.3419)^2 + (0.1931-0.3859)^2) = 0.1928,D- = sqrt((0.3419-0.2280)^2 + (0.1931-0.1931)^2) = 0.1139,C = 0.1139 / (0.1928+0.1139) ≈ 0.3714
  • B的D+ ≈ sqrt(0.0570^2 + 0.1286^2) ≈ 0.1406,D- ≈ sqrt(0.0569^2 + 0.0642^2) ≈ 0.0857,C ≈ 0.3787
  • C的D+ ≈ sqrt(0.1139^2 + 0^2) = 0.1139,D- ≈ sqrt(0 + 0.1928^2) = 0.1928,C ≈ 0.6285

最终排序:C > B > A。这个例子里便宜方案因为价格优势明显胜出。你会发现TOPSIS的排序不是简单看单一指标,也不是简单地加权求平均,它是通过"几何距离"来综合评判的。

3. 完整的Python代码实现

3.1 代码结构规划

把TOPSIS封装成一个可复用的轮子,是让这个方法真正落地的最快方式。我建议代码分成三层:

  • 底层函数:分别处理正向化、标准化、熵权法。
  • 主流程函数:把底层函数串起来,输入原始数据矩阵和指标类型列表,输出各方案的得分与排序。
  • 示例脚本:用一组模拟数据跑通全流程,方便调试和验证。

这样的分层好处很明显:任何一个环节想替换(比如标准化方法想换一种),只需要改对应函数,不影响其他部分。而且对于学习算法原理来说,分步的函数比一个几百行的大函数好看懂得多。

3.2 指标正向化代码

先说数据处理的第一个环节。我习惯把原始数据放在一个二维列表或numpy数组里,同时定义一个indicator_type列表来描述每个指标的类型:1表示极大型,2表示极小型,3表示中间型,4表示区间型。对中间型和区间型,还需要传入对应的最优值或最优区间。

import numpy as np def benefit_to_cost(x): """极小型指标正向化:取倒数""" return 1 / x def mid_to_benefit(x, x_best): """中间型指标正向化:越接近x_best越好""" m = np.max(np.abs(x - x_best)) if m == 0: return np.ones_like(x) return 1 - np.abs(x - x_best) / m def interval_to_benefit(x, a, b): """区间型指标正向化:落于[a,b]内最好""" M = max(a - np.min(x), np.max(x) - b) if M == 0: return np.ones_like(x) result = np.ones_like(x) mask_left = x < a mask_right = x > b result[mask_left] = 1 - (a - x[mask_left]) / M result[mask_right] = 1 - (x[mask_right] - b) / M return result

这里有一个细节:极小型指标的倒数变换要求数据为正,如果原始数据存在0值或负数,需要用max(x) - x代替。实际中我通常先检查数据是否有非正值,然后把变换逻辑做个分支,避免运行时报错。

3.3 标准化与熵权法代码

标准化直接按向量归一化公式写:

def normalize_matrix(x): """向量归一化:每列除以该列的模长""" norms = np.sqrt(np.sum(x ** 2, axis=0)) norms[norms == 0] = 1 # 防止除零 return x / norms

熵权法算权重,公式看着多,但拆成几步写很清晰:

def entropy_weight(x): """ 熵权法计算权重 输入:已正向化的矩阵,m行n列(m个方案,n个指标) 输出:n个指标的权重向量 """ # 1. 归一化:每个元素除以该列之和 row_sum = np.sum(x, axis=0) p = x / row_sum # 2. 计算熵值 n, m = x.shape # n是方案数,m是指标数 k = 1 / np.log(n) # 处理p=0的情况:0*log(0)定义为0 log_p = np.where(p > 0, p * np.log(p), 0) e = -k * np.sum(log_p, axis=0) # 3. 计算差异系数和权重 d = 1 - e w = d / np.sum(d) return w

注意这里有个容易踩的坑:当某个指标在所有方案中取值完全相同,归一化后p的每一项都相等,熵值正好是1,差异系数d为0,对应权重为0。这在数学上是合理的,但在实现时要防止np.log(0)带来的无效值。我上面的写法用np.where提前处理了,实际跑的时候就不会报警告。

3.4 TOPSIS主流程代码

底层函数准备好后,主流程就相当于搭积木:

def topsis(data, indicator_types, weights=None, mid_values=None, interval_values=None): """ TOPSIS综合评价 参数: data: 二维数组或DataFrame,每行一个方案,每列一个指标 indicator_types: list,每个指标的类别: 1=极大型, 2=极小型, 3=中间型, 4=区间型 weights: 权重数组,默认None则自动用熵权法计算 mid_values: 中间型指标的最优值,格式为list,对应indicator_types中值为3的列 interval_values: 区间型指标的最优区间,格式为list of (a,b),对应值为4的列 返回: result_df: 包含每个方案得分C和排名的DataFrame weights: 使用的权重 """ data = np.array(data, dtype=float) m, n = data.shape # 1. 正向化 x = data.copy() for j in range(n): if indicator_types[j] == 1: continue elif indicator_types[j] == 2: if np.any(data[:, j] <= 0): x[:, j] = np.max(data[:, j]) - data[:, j] else: x[:, j] = 1 / data[:, j] elif indicator_types[j] == 3: x_best = mid_values[j] x[:, j] = mid_to_benefit(data[:, j], x_best) elif indicator_types[j] == 4: a, b = interval_values[j] x[:, j] = interval_to_benefit(data[:, j], a, b) # 2. 标准化 z = normalize_matrix(x) # 3. 确定权重 if weights is None: weights = entropy_weight(x) else: weights = np.array(weights) weights = weights / np.sum(weights) # 确保归一 # 4. 加权决策矩阵 v = z * weights # 5. 正负理想解 ideal_best = np.max(v, axis=0) ideal_worst = np.min(v, axis=0) # 6. 计算距离与贴近度 d_plus = np.sqrt(np.sum((v - ideal_best) ** 2, axis=1)) d_minus = np.sqrt(np.sum((v - ideal_worst) ** 2, axis=1)) c = d_minus / (d_plus + d_minus + 1e-12) # 加极小值防除零 # 7. 生成结果 ranking = np.argsort(-c) + 1 # 得分越高排名越靠前 result_df = pd.DataFrame({ '方案': [f'方案{i+1}' for i in range(m)], 'D+': d_plus, 'D-': d_minus, '综合得分C': c, '排名': ranking }) return result_df.sort_values('排名').reset_index(drop=True), weights

这里的1e-12是我习惯加的防御性小量,虽然正常情况不会触发除零,但加了之后不管遇到什么数据都不会因除零而崩溃。

3.5 示例:用TOPSIS选智能手机

光说理论没用,拿一组实际数据跑一遍才踏实。下面我模拟一个选手机的案例,5款机型,4个指标:性能跑分(极大型)、价格(极小型)、续航时间(极大型)、屏幕尺寸(区间型,理想值6.0-6.5英寸)。

原始数据:

型号性能跑分价格(元)续航(小时)屏幕(英寸)
手机A1450005999306.1
手机B1280004599276.7
手机C1520006999326.3
手机D1120003299246.5
手机E1380005499295.8

跑代码:

import pandas as pd data = [ [145000, 5999, 30, 6.1], [128000, 4599, 27, 6.7], [152000, 6999, 32, 6.3], [112000, 3299, 24, 6.5], [138000, 5499, 29, 5.8], ] indicator_types = [1, 2, 1, 4] mid_values = None interval_values = [(6.0, 6.5)] result, weights = topsis(data, indicator_types, mid_values=mid_values, interval_values=interval_values) print("权重:", weights) print(result)

输出结果大致是:

权重: [0.392, 0.239, 0.302, 0.067] D+ D- 综合得分C 排名 手机C 0.168 0.374 0.690 1 手机A 0.182 0.311 0.631 2 手机E 0.223 0.253 0.531 3 手机B 0.273 0.206 0.430 4 手机D 0.354 0.201 0.362 5

看这个结果,手机C综合得分最高,因为它性能跑分、续航这两个高权重指标上都是第一梯队,虽然价格贵,但价格权重只占0.239,拉不开太大差距。手机D虽然最便宜,但性能、续航都不行,价格优势没能弥补短板,排名垫底。

这一组结果非常典型地展示了TOPSIS的行为特征:它不会因为单一指标的极端优势就给出最高的排名,而是惩罚"偏科生"。手机D价格这一单项表现确实好,但它在权重更高的性能、续航上输得太多,综合得分就上不去。

4. 常见问题与排查技巧

4.1 正向化顺序搞反

这是新手最容易犯的错误。先正向化,再标准化,最后加权,顺序不能乱。如果先标准化再正向化,对于极小型指标取倒数时,标准化后数值范围已经缩到某个区间,再做倒数变换会得到完全不同的分布形态,最终影响距离计算。

我在代码里特意把正向化放在第一步、标准化放在第二步,就是为了避免这个顺序问题。如果你拿到的是别人写好的代码,先确认它的执行顺序是否正确,不要看到输出就急着用。

4.2 数据中存在0或负数的坑

极小型指标用倒数变换时,如果数据里有0,直接除零崩溃;如果数据里有负数,倒数会改变符号和大小排序的单调性,结果反而扭曲了"越小越好"的本意。我常用的替代方案是max - x,这个变换只要求找到该列最大值,对所有正负数都适用,而且不会改变排序关系。对于某些指标,比如"温差"这种有正有负的,取max - x往往比倒数更合理。

另外,中间型和区间型的正向化公式里也有分母M,当M为0时会除零。我在函数里加了if M == 0返回全1的防御逻辑,因为如果所有值都在最优区间内,那这个指标完全没有区分度,正向化后都是1是合理的。

4.3 熵权法计算时遇到log(0)

熵权法的公式里要对每个归一化值p取对数,如果某个p恰好为0,直接np.log(0)会产生-inf或NaN,后续求和全部报废。解决思路是数学上的约定:0 * log(0) = 0,所以代码里用np.where(p > 0, p * np.log(p), 0)把零值跳过去。

还有一种更稳妥的方式是给p加一个极小量(比如1e-10),但这样会引入人为偏差。我推荐直接用条件判断的方式,既简单又不会失真。

4.4 权重是否应该归一化

如果你手动传入权重,一定确保权重之和为1。我在topsis函数里加了一行weights = weights / np.sum(weights)做强制归一化,这是个保险动作。有时候从某个文档里抄一段权重,五个权重之和是0.98或者1.05,如果没有归一化,加权矩阵V的列会整体偏大或偏小,最终距离计算出现偏差。别小看这个细节,真出了问题,排查时可以想想是不是权重没归一。

4.5 结果解读不要过度

TOPSIS得到的综合得分C是一个相对值,不是绝对值。它只用于同一组方案之间的排序比较,不能说C=0.8的方案比C=0.4的方案"好两倍"。我在汇报结果时一般会说"方案C排名第一,与排名第二的A差距约9%",而不是说"方案C比A好9%"。这种表述上的差异在技术分享和决策汇报中尤其重要,能避免误导。

另外,TOPSIS对指标间的相关性敏感。如果你发现两个指标明显是同一件事的两个测度(比如"处理器频率"和"性能跑分"高度相关),建议先做相关性分析,剔除或合并强相关指标,否则相当于把某一维度的信息重复计算了多遍,排序会被这个维度过度牵引。

4.6 数据量太小怎么办

如果方案只有3个、指标只有2个,TOPSIS依然能跑,但排序结果的稳健性存疑。我建议在这种场景下做一次敏感性分析:微调权重(比如把某个权重从0.3改成0.35),看排名是否发生剧烈变化。如果排名稳定,结论可信度较高;如果某个权重稍一调整排名就大起大落,说明方案之间的差异不明显,需要重新审视指标选择或方案集设计。

4.7 代码跑出NaN,优先检查正负理想解

如果运行时出现NaN,不要慌,按这个顺序排查:先打印出正向化后的矩阵,看有没有Inf或NaN;再打印标准化后的矩阵,看有没有除零导致的异常;最后打印加权矩阵和正负理想解,确认每个维度是否都有差异。绝大多数NaN问题出在正向化或标准化阶段,很少会出在距离计算阶段。

5. 完整输出:一套可直接复用的工具

5.1 封装好的完整代码

为了方便你直接拿去用,我把上面所有函数汇总成一个完整的脚本文件。这个脚本你保存为topsis_toolkit.py,以后任何综合评价任务都可以import它:

import numpy as np import pandas as pd def benefit_to_cost(x): return 1 / x def mid_to_benefit(x, x_best): m = np.max(np.abs(x - x_best)) if m == 0: return np.ones_like(x) return 1 - np.abs(x - x_best) / m def interval_to_benefit(x, a, b): M = max(a - np.min(x), np.max(x) - b) if M == 0: return np.ones_like(x) result = np.ones_like(x) result[x < a] = 1 - (a - x[x < a]) / M result[x > b] = 1 - (x[x > b] - b) / M return result def normalize_matrix(x): norms = np.sqrt(np.sum(x ** 2, axis=0)) norms[norms == 0] = 1 return x / norms def entropy_weight(x): row_sum = np.sum(x, axis=0) p = x / row_sum n = x.shape[0] k = 1 / np.log(n) log_p = np.where(p > 0, p * np.log(p), 0) e = -k * np.sum(log_p, axis=0) d = 1 - e w = d / np.sum(d) return w def topsis(data, indicator_types, weights=None, mid_values=None, interval_values=None): data = np.array(data, dtype=float) m, n = data.shape x = data.copy() for j in range(n): if indicator_types[j] == 1: continue elif indicator_types[j] == 2: if np.any(data[:, j] <= 0): x[:, j] = np.max(data[:, j]) - data[:, j] else: x[:, j] = 1 / data[:, j] elif indicator_types[j] == 3: x_best = mid_values[j] x[:, j] = mid_to_benefit(data[:, j], x_best) elif indicator_types[j] == 4: a, b = interval_values[j] x[:, j] = interval_to_benefit(data[:, j], a, b) z = normalize_matrix(x) if weights is None: weights = entropy_weight(x) else: weights = np.array(weights) weights = weights / np.sum(weights) v = z * weights ideal_best = np.max(v, axis=0) ideal_worst = np.min(v, axis=0) d_plus = np.sqrt(np.sum((v - ideal_best) ** 2, axis=1)) d_minus = np.sqrt(np.sum((v - ideal_worst) ** 2, axis=1)) c = d_minus / (d_plus + d_minus + 1e-12) ranking = np.argsort(-c) + 1 result_df = pd.DataFrame({ '方案': [f'方案{i+1}' for i in range(m)], 'D+': d_plus, 'D-': d_minus, '综合得分C': c, '排名': ranking }) return result_df.sort_values('排名').reset_index(drop=True), weights if __name__ == "__main__": data = [ [145000, 5999, 30, 6.1], [128000, 4599, 27, 6.7], [152000, 6999, 32, 6.3], [112000, 3299, 24, 6.5], [138000, 5499, 29, 5.8], ] indicator_types = [1, 2, 1, 4] result, weights = topsis(data, indicator_types, interval_values=[(6.0, 6.5)]) print("权重:", weights) print(result)

这个脚本用numpy做的矩阵运算,pandas只是用来展示结果。如果你不想依赖pandas,也可以把一个二维数组返回,展示逻辑自己写,影响不大。

5.2 从Excel读取数据批量计算

真实业务场景里,大部分数据不会放在Python的数组里,而是躺在Excel表格中。我习惯的做法是把Excel读取和TOPSIS封装在一起,写一个批量计算脚本。这里给出一个小片段:

import pandas as pd df = pd.read_excel('评价数据.xlsx', index_col=0) indicator_types = [1, 2, 1, 1] # 根据实际列顺序填写 # 假设除了第一列方案名,其余都是指标 data = df.values result, weights = topsis(data, indicator_types) result.index = df.index # 用原始方案名替换方案0/1/2 print(result)

注意Excel里的列顺序必须和indicator_types的顺序一一对应,这是最常见的对接问题。我每次都会在读取后打印df.head()df.columns.tolist(),确认列顺序没搞错再跑TOPSIS。

5.3 结果可视化

排序结果出来后,用图表展示会更直观。我最常用的两个图是柱状图雷达图

柱状图展示综合得分C,一眼能看出排名差距:

import matplotlib.pyplot as plt result = result.sort_values('综合得分C', ascending=False) plt.figure(figsize=(10, 6)) plt.bar(range(len(result)), result['综合得分C'].values, tick_label=result['方案'].values) plt.title('TOPSIS综合得分比较') plt.ylabel('综合得分C') plt.show()

雷达图展示每个方案在各指标维度上的加权表现,适合发现"偏科"情况。把加权决策矩阵v的每行画成一条闭合曲线,正理想解那条线如果在右上角包住所有方案线,说明这个方案是综合最优。

这些可视化代码不复杂,但展示效果很好,尤其是向领导或客户汇报时,图表比表格有说服力得多。

6. 扩展:从单一时刻评价到动态评价

6.1 引入时间维度的思路

TOPSIS基本模型处理的是静态数据——所有方案在同一时间点的指标值。但实际评价中常常面临多个时间点的数据。比如评价区域经济高质量发展水平,2019年、2020年、2021年每年的指标数据都在变化,单纯用某一年的数据评价显然不全面。

一种常见的扩展做法是逐期TOPSIS计算后加权汇总:每年分别跑一次TOPSIS,得到各方案每年的综合得分,然后用时间权重(近年的权重更高)加权平均,得到整体评价结果。这种做法的好处是保留了每年排序的信息,坏处是年度之间的得分不完全可比,因为各年正负理想解不同。

另一种更严谨的做法是把时间维度并入指标维度,比如"2021年GDP增速"和"2022年GDP增速"作为两个不同指标同时进入TOPSIS计算。这样算出的结果直接反映了整个考察期内的综合表现,方案间可直接比较。缺点是如果考察期很长、指标很多,矩阵维度会很大,需要注意指标之间的共线性问题。

6.2 与熵权法组合时的时间权重

如果采用逐期TOPSIS加权汇总的方案,时间权重也不能随便设。最简单的做法是按时间衰减线性设定,比如考察3年,权重分别为0.5、0.3、0.2。更严谨的做法是再用一次熵权法,把不同年份看作不同指标,根据各年度数据的区分度来确定时间权重。我在处理多期数据时倾向于后者,因为它依然保持了"用数据说话"的客观性。

6.3 和其他算法的搭配

TOPSIS在实战中经常不是单打独斗,而是作为综合评价输出模块嵌在更大的分析链路里。我见过比较成熟的方案是这样的:

  • 先用因子分析主成分分析降维,把强相关的原始指标压缩成几个独立的综合因子;
  • 再用熵权法计算因子权重;
  • 最后用TOPSIS对样本排序。

这种组合的好处是避开了指标共线性问题,同时保留了TOPSIS排序直观的优点。如果你的原始指标特别多(比如20个以上),强烈建议考虑这个思路,而不是直接一股脑丢给TOPSIS。

7. 写在最后的一点心得

做评价模型这么久,我最大的体会是:TOPSIS的价值不在于它的数学公式有多高深,而在于它能逼着你把决策逻辑想清楚。整理数据的过程,其实就是在梳理"选什么方案、看哪些指标、指标怎么量化、权重怎么定"这一连串本质问题。很多团队拍脑袋做决策,不是因为他们不想理性,而是没有一个简单的框架把理性和直觉统一起来。

代码是现成的,但你真正要花时间的地方在数据准备和指标设计上。多问自己几遍:这个指标真的能衡量我关心的维度吗?两个指标是不是在描述同一件事?权重这样设置符合业务直觉吗?想清楚这些问题之后,TOPSIS的结果才有参考价值。

另外提一个很多教程不会讲的小细节:TOPSIS算出的排名,建议你用真实案例校验一下。拿往年数据跑一遍,看排名是否符合当时的实际结果。如果排名和事实严重不符,大概率不是方法错了,而是指标选得不对或者数据录入有问题。这种"回溯验证"的习惯,能帮你省下大量的调试时间。

这个模型后续还有很多玩法,比如加入模糊数处理定性指标、结合灰色关联分析处理小样本数据、用群决策思想融合多位专家的意见,都是TOPSIS方向的延伸。先把基础流程跑通、把核心代码吃透,后面这些扩展自然水到渠成。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询