TOPSIS综合评价法实战:熵权法、AHP赋权与贴近度排名代码包
简介面向多准则决策初学者、课程实验与数据分析实践者这份 Topsis 综合评价法配套材料围绕方案排序与优选问题提供可直接参考的评价基础数据与算法实现适合用于软件或插件选型、项目评估等场景的入门练习与复现。压缩包共 2 个文件约 10KB包含 1 个 xlsx 评价数据表和 1 个 m 脚本表格可用于承载指标矩阵与原始评分脚本则对应 MATLAB 环境下的标准化、权重处理、最优最劣向量构建、距离计算与综合排序流程。已有 643 人学习说明该主题在课程作业和实际决策分析中具有一定关注度。读者可借助银行数据示例理解评价矩阵的组织方式并对照脚本梳理从数据预处理、指标权重设定到接近度计算和结果解读的完整链路为后续替换指标、调整权重或扩展模糊 Topsis 等方法提供可操作的基础模板。1. 十几个方案、八个指标谁排第一TOPSIS 综合评价法代码包解决的就是这件事某实验室要给一批候选方案排个名指标里有成本越低越好、含水率越接近某个中间值越好、覆盖率越高越好量纲从元到百分比混在一起。直接加权求和数值大的指标会把结果整体带偏用 Excel 一列一列排序又拼不出综合名次。TOPSIS 综合评价法的做法是先把指标方向统一、再消掉量纲然后算每个方案离理想解和最差解各有多远用一个 0 到 1 的贴近度把顺序定下来。这份代码包给的不是公式推导而是一套改完数据就能出结果的脚本和配套数据表正向化、标准化、赋权、贴近度四段各自独立成函数指标是正向还是负向写在表里不硬编码进代码。适合两类人一类是要快速跑出排名、不想手推矩阵的另一类是想把这套评价逻辑塞进自己的评分软件里的。2. 项目结构与数据约定一张 Excel 元信息表决定整条流水线这个包的设计思路是「数据驱动」算法代码一行都不用改换指标、换方向、换参数全部在 Excel 里完成。理解这一点后面所有章节都会顺很多。2.1 目录结构与文件职责拿到包先看目录各目录的职责划分得比较清楚原始数据、算法、输出三者互不污染topsis-eval/ ├── data/ │ ├── raw_indicators.xlsx # 原始指标表第 1 列方案名第 2 列起是各指标数值 │ └── indicator_meta.xlsx # 指标元信息方向类型、参数、权重来源 ├── matlab/ │ ├── main_topsis.m # 主入口按顺序串起四步 │ ├── forward.m # 正向化把负向/中间/区间型指标统一成越大越好 │ ├── normalize.m # 标准化向量归一化消量纲 │ ├── entropy_weight.m # 熵权法赋权客观 │ ├── ahp_weight.m # 层次分析法赋权主观带一致性检验 │ └── closeness.m # 正负理想解 贴近度打分 排名 ├── python/ │ ├── topsis.py # 与 MATLAB 等价的实现方便接后端 │ └── sensitivity.py # 权重扰动灵敏度分析 └── output/ ├── score_rank.xlsx # 每个方案的贴近度与排名 └── weight_report.xlsx # 每个指标的权重明细data/是唯一需要你手动维护的目录。matlab/和python/是两套等价实现前者适合做快速验证和出图后者适合嵌进服务里跑批量任务。output/每次执行都会被覆盖所以不要把人工整理的中间结果放进去我一般会在里面再建一个archive/子目录按日期存历史结果。这种分层的好处是排查问题时能快速缩小范围结果不对先看output/weight_report.xlsx里权重是否合理权重合理再往前查标准化一层层往回退比在一个几百行的大脚本里打断点舒服得多。提示目录名和文件名在 MATLAB 里路径区分大小写的情况很常见从 Windows 拷到 Linux 上跑之前先用find . -name *.m检查一遍引用路径。2.2 数据表字段设计与指标方向约定整个项目的规则都集中在indicator_meta.xlsx这张小表里它决定了每个指标该怎么处理。表结构大致长这样指标名方向类型参数权重来源单位成本min-熵权含水率midbest8熵权响应时长rangea2, b5熵权覆盖率max-AHP方向类型只有四个取值对应四种处理方式max表示极大型指标越大越好原样保留min表示极小型指标越小越好用max - x翻转mid表示中间型指标越接近某个最优值越好需要给best参数range表示区间型指标落在[a, b]区间内最好越远越差。raw_indicators.xlsx的约定更简单第一列是方案名参加排名的对象后面每一列是一个指标列名必须和indicator_meta.xlsx里的指标名严格一致。列顺序不重要因为代码是按列名读取的。这个「按名匹配」的细节挺关键我见过不少人调换了 Excel 里的列顺序后直接跑结果全乱套——按列名匹配就不会有这个问题。function meta load_meta(metaFile) % 读取指标元信息表转成与数据列一一对应的结构体数组 T readtable(metaFile, VariableNamingRule, preserve); meta struct(name, {}, type, {}, param, {}); for i 1:height(T) meta(i).name string(T.(指标名)(i)); meta(i).type string(T.(方向类型)(i)); % max / min / mid / range meta(i).param T.(参数)(i); % 区间型写 2,5中间型写 8 end end这段代码负责把 Excel 里的人类可读配置翻译成代码可用的结构体。VariableNamingRule设成preserve是为了让中文列名不被 MATLAB 改成Var1、Var2这类占位名这个选项在 R2019b 之后才支持老版本得用readcell或者手动指定列名。param字段的解析放在forward.m里做因为只有正向化才需要用到它mid类型拆出bestrange类型拆出a和b。把解析和使用放在一起比在读取阶段就拆开要清晰——读取阶段只管搬运处理阶段才关心语义。2.3 技术选型MATLAB 主脚本 Python 对照实现为什么同时给两套因为这两个使用场景差别很大。MATLAB 的优势是矩阵运算写得像数学公式Z ./ sqrt(sum(Z.^2, 1))一行就是向量归一化跟纸上推导完全对应调试的时候看着舒服做课程作业或者方案预研效率很高。缺点是部署麻烦别人机器上没有 MATLAB 就只能干瞪眼。Python 版本就是为了解决部署问题numpypandas的组合把同样的逻辑重写一遍打包成命令行工具或者 Flask 接口都行。我在实际项目里一般用 MATLAB 版做算法确认用 Python 版做线上服务两边结果对不上就以 MATLAB 版为准去查 Python 的浮点误差和缺失值处理差异。至于权重方法包给了熵权法和 AHP 两条路。熵权法是客观赋权完全由数据本身的信息量决定某个指标在各方案间差异越大权重越高AHP 是主观赋权靠判断矩阵表达专家偏好但必须做一致性检验。实际使用中常见做法是主客观组合赋权比如乘性合成或者线性加权这个在最后一章会给出改造思路。3. 核心算法拆解正向化、标准化、赋权、贴近度四段链路四步的顺序不能调换每一步的输入都是上一步输出的规整矩阵。下面按执行顺序逐个拆。3.1 指标正向化极小型、中间型、区间型三类的转换公式正向化的目标只有一个——让所有指标都变成「越大越好」。这样后面的理想解就是每列最大值、负理想解就是每列最小值逻辑统一不用再对不同类型的指标写不同的分支判断。function X forward(X, meta) % X: n×m 原始指标矩阵n 个方案m 个指标 % meta: 指标元信息与 X 的列一一对应 for j 1:size(X, 2) col X(:, j); switch meta(j).type case max continue % 极大型原样保留 case min X(:, j) max(col) - col; % 极小型翻转min 值变成最大 case mid best meta(j).param; % 最优值 d abs(col - best); X(:, j) 1 - d / max(d); % 中间型离 best 越远值越小 case range p str2double(split(meta(j).param, ,)); a p(1); b p(2); M max([a - min(col), max(col) - b]); % 最远偏离距离 out max(max(a - col, 0), max(col - b, 0)); X(:, j) 1 - out / M; % 区间型区间内得 1 end end endmin类型用max(col) - col而不是1 ./ col是因为后者在两个方案成本值都很小时会把差距放得极大容易让一个指标影响整个排名前者是线性翻转保留原有比例关系稳健得多。mid和range两种类型在真实数据里经常被忽略。比如「pH 值」这种指标太酸太碱都不好用极大型处理会得出「越碱越好」的荒谬结论再比如「库存周转天数」,合理区间是 30 到 45 天低于 30 说明备货不足高于 45 说明压货用区间型才是对的。M的计算是max([a - min(col), max(col) - b])取的是当前列数据到区间两端的最大偏离距离用它做分母是为了把结果压到[0, 1]。如果某个方案恰好落在区间内out是 0结果是 1偏离最远的那个方案结果是 0。注意正向化之后所有列都变成非负值了这是有意为之。下一步熵权法要算P .* log(P)出现负数或者零会让对数无定义先正向化就顺手把这个问题规避了。3.2 标准化矩阵消去量纲的向量归一化正向化只统一了「方向」没统一「尺度」。单位成本可能是几千覆盖率是 0.8 左右直接算距离的话成本这一项会压倒其他所有指标。标准化就是为了抹平这种数量级差异。function Z normalize(X) % 向量归一化每列除以该列的 2-范数 norm2 sqrt(sum(X .^ 2, 1)); % 1×m每列的欧氏长度 norm2(norm2 eps) 1; % 兜底整列为 0 时不做缩放避免除零 Z X ./ norm2; % 隐式扩展逐列相除 end这里用的是向量归一化也叫平方和归一化公式是z_ij x_ij / sqrt(sum_i x_ij^2)。它的特点是变换后每列平方和为 1各方案之间仍然保留原始的比例关系。另一种常见做法是极差标准化(x - min) / (max - min)把每列压到[0,1]但极差法对异常值特别敏感——一个离群点就能把其余所有方案挤到 0.1 以下的窄带里排名区分度会变得很差。norm2(norm2 eps) 1这一行是保命的。某个指标在所有方案上取值完全一样比如所有方案都是同一家供应商采购价相同这一列的范数就是 0不处理的话整列变NaN后面所有矩阵运算全废而且 MATLAB 不会报错只会静默地把结果变成NaN。这种错误排查起来非常费劲属于典型的「沉默失败」。Z X ./ norm2用的是 MATLAB R2016b 之后的隐式扩展特性n×m除以1×m会自动按行广播。老版本需要写成X ./ repmat(norm2, size(X,1), 1)。3.3 赋权熵权法与 AHP 两种权重入口权重决定了哪个指标说话算数。熵权法的逻辑是「数据自己说话」某个指标在各方案之间差异越大说明它提供的信息量越多权重就越高如果所有方案在某指标上完全一样这个指标对排序毫无贡献权重为 0。function w entropy_weight(Z) % Z: 标准化后的非负矩阵 [n, m] size(Z); colSum sum(Z, 1); colSum(colSum eps) eps; % 整列为 0 时兜底 P Z ./ colSum; % 第 j 列第 i 个方案的比重 P(P eps) eps; % 0·log0 按极限取 0这里给极小值兜底 e -sum(P .* log(P), 1) / log(n); % 每个指标的信息熵取值 [0,1] d 1 - e; % 差异系数熵越小差异越大 w d / sum(d); % 归一化成和为 1 的权重向量 end信息熵e越接近 1说明这个指标的分布越均匀、信息量越少差异系数d 1 - e就越小。log(n)做分母是为了归一化保证e落在[0,1]区间内。P(P eps) eps这一行处理的是比重为 0 的情况。数学上0 * log(0) 0但数值计算里log(0)是-Inf0 * -Inf是NaN。用eps兜底相当于给一个极小值对最终权重的影响在小数点后十几位可以忽略但能避免整个结果被污染。AHP 那条路走的是另一套逻辑靠判断矩阵表达主观偏好function [w, CR] ahp_weight(A) % A: n×n 正互反判断矩阵A(i,j) 表示指标 i 相对指标 j 的重要程度 n size(A, 1); [V, D] eig(A); [~, idx] max(real(diag(D))); % 最大特征值对应的特征向量 w abs(V(:, idx)); w w / sum(w); % 归一化即为权重 lambda real(diag(D)); lambda max(lambda); CI (lambda - n) / (n - 1); % 一致性指标 RI [0 0 0.58 0.90 1.12 1.24 1.32 1.41 1.45 1.49]; CR CI / RI(n); % 一致性比率要求 0.1 endRI是随机一致性指标跟矩阵阶数相关表中只列到 10 阶超过 10 阶的指标数量说明你的评价体系该分层了。CR 0.1才算通过一致性检验否则说明判断矩阵自相矛盾——比如你觉得 A 比 B 重要、B 比 C 重要却又填了 C 比 A 重要这时候就得回去改矩阵。两套权重可以单独用也可以组合。常见做法是w (w_entropy .* w_ahp) ./ sum(w_entropy .* w_ahp)主客观各占一半话语权。3.4 正负理想解与贴近度打分前三步都在做准备这一步才是真正出结果的地方。思路很直观构造一个「理想方案」每个指标都取所有方案里的最优值和一个「最差方案」每个指标都取最差值然后看每个实际方案离理想方案近、离最差方案远最后用一个比值表达这种相对位置。function [score, rankVec] closeness(Z, w) % Z: 标准化矩阵w: 权重列向量 Zplus max(Z, [], 1); % 正理想解每列最大值 Zminus min(Z, [], 1); % 负理想解每列最小值 Dplus sqrt(sum(((Z - Zplus ) .^ 2) .* w, 2)); % 到正理想解的加权距离 Dminus sqrt(sum(((Z - Zminus) .^ 2) .* w, 2)); % 到负理想解的加权距离 score Dminus ./ (Dplus Dminus); % 贴近度越接近 1 越优 [~, order] sort(score, descend); rankVec zeros(size(score)); rankVec(order) 1:numel(score); % 反查每个方案的名次 endmax(Z, [], 1)的第二个参数[]表示沿第一个非单一维度也就是行方向求最大值得到1×m的理想解向量。.* w把权重乘进平方差里这里w是列向量转置成行向量后才能和n×m矩阵按列广播。score Dminus ./ (Dplus Dminus)这个公式叫相对贴近度取值在[0,1]。如果某个方案正好等于理想解Dplus 0得分就是 1如果正好等于最差解Dminus 0得分是 0。用比值而不是直接用Dplus好处是消掉了不同数据集之间距离量级不一致的问题不同批次的评分能横向比较。最后两行把排序结果反查成每个方案的名次向量。sort返回的order是「第几名是原数组里的第几个元素」直接拿去做rankVec(order) 1:n就能得到「第 i 个方案排第几」。4. 运行起来从环境准备到 score_rank.xlsx 保存到文件算法看着都懂但真正卡住人的往往是环境、路径和文件格式。这一章把完整流程串一遍。4.1 环境与依赖清单MATLAB 路线要求 R2019b 及以上主要是因为readtable的VariableNamingRule参数和隐式扩展特性。如果没有对应版本把readtable换成readcell再手动转换把X ./ norm2换成X ./ repmat(norm2, size(X,1), 1)就能跑。Python 路线的依赖只有三个写进requirements.txtpip install numpy pandas openpyxlnumpy负责矩阵运算pandas负责读写 Excelopenpyxl是pandas读写.xlsx的底层引擎不装会报Missing optional dependency openpyxl。如果数据表是.xls老格式还得装xlrd不过现在基本见不到这种文件了。Python 版本对版本号的要求不严numpy 1.20就够了主要用到的是广播和np.maximum。4.2 完整执行流程与命令MATLAB 版的主脚本把四步串起来中间不做任何交互方便直接挂到批处理里%% main_topsis.m —— 一键跑通整条链路 clear; clc; addpath(genpath(fullfile(pwd, matlab))); % 把子目录都加进搜索路径 raw readtable(data/raw_indicators.xlsx, VariableNamingRule, preserve); names string(raw{:, 1}); % 第 1 列是方案名 X table2array(raw(:, 2:end)); % 第 2 列起是指标数值 meta load_meta(data/indicator_meta.xlsx); X forward(X, meta); % 1) 正向化全部变成越大越好 Z normalize(X); % 2) 标准化消掉量纲 w entropy_weight(Z); % 3) 熵权法赋权 [score, rankVec] closeness(Z, w); % 4) 贴近度与排名 result table(names, score, rankVec, ... VariableNames, {方案, 贴近度, 排名}); result sortrows(result, 排名); writetable(result, output/score_rank.xlsx); weightTbl table(meta_names, w, VariableNames, {指标, 权重}); writetable(weightTbl, output/weight_report.xlsx);readtable配合VariableNamingRule, preserve读进来之后第一列是方案名文本table2array只对数值列有效所以先切出raw(:, 2:end)再转矩阵。如果直接把整张表转数组文本列会被转成NaN这是新手最常遇到的问题之一。命令行直接跑# 无界面执行适合放到定时任务或 CI 里 matlab -batch cd(/path/to/topsis-eval); main_topsis # Python 版输出到同样格式的 Excel python python/topsis.py --data data/raw_indicators.xlsx \ --meta data/indicator_meta.xlsx \ --out output/score_rank.xlsx-batch参数是 R2019a 引入的跑完自动退出不需要在脚本里写exit。老版本得用-nodisplay -nosplash -r ...再加exit写法上麻烦一点。Python 版的核心四步和 MATLAB 一一对应函数名都一样方便两边对照着看# python/topsis.py 核心片段 X forward(X, kinds, params) # 正向化 Z X / np.linalg.norm(X, axis0) # 标准化按列求 2-范数 w entropy_weight(Z) # 熵权 score, rank closeness(Z, w) # 贴近度与排名np.linalg.norm(X, axis0)沿行方向求范数得到长度m的向量再用广播逐列相除跟 MATLAB 的./ norm2是同一件事。注意axis0这个参数很容易写错成axis1写错了得到的是每行的范数除下去维度对不上会直接报错反倒不会静默出错算是比较好排查的。4.3 结果文件解读得分、排名与权重表跑通之后output/下会多出两个文件。score_rank.xlsx有三列示例数据跑出来大概是这样方案贴近度排名方案 C0.74211方案 A0.61052方案 E0.55833方案 B0.43174方案 D0.28045贴近度的绝对值本身不需要过度解读它的意义在于相对位置0.74 和 0.61 之间的差距说明 C 比 A 明显更接近理想解但如果两个方案分别是 0.6105 和 0.6102那基本可以认为它们并列硬要分个高下没有意义。我在给别人看结果时一般会在表里加一列「与上一名差值」方便判断排名是否稳定。weight_report.xlsx记录每个指标最终拿到的权重。这张表值得单独看一眼如果某个指标权重超过 0.5说明它在数据上差异极大几乎单枪匹马决定了排名这时候要考虑是不是数据本身有问题比如混入了异常值或者这个指标该不该拆成两个。验证结果是否可信有个土办法把权重表里权重最高的指标单独拿出来排序看和最终排名是否高度相似。如果完全一致说明其他指标基本没起作用权重分配可能失衡如果差得离谱倒可能是正常的因为综合评价本来就是要综合多个维度。5. 避坑与排查得分全是 1、排名反了、权重挤在一列是怎么回事这些问题我在不同的数据集上都遇到过按「现象—原因—解决」记下来遇到时对照着查能省不少时间。现象一所有方案的贴近度都接近 1或者全是 0.5 上下排名看不出区分度。原因通常是标准化之前就有某一列存在极端离群值。向量归一化虽然比极差法稳健但如果一个方案在某个指标上是其他方案的几十倍这一列的范数会被它主导其余方案归一化后全部挤在 0 附近权重和距离计算就失去区分能力了。解决办法是先在原始数据层面做异常值检查用boxplot或者简单的 3σ 规则把离群点找出来确认是录入错误就修正确实是真实极值就考虑对指标取对数log(x 1)再做后续处理。取对数会把数量级压缩对右偏分布特别有效。现象二排名结果和常识完全相反成本最低的方案反而排最后。九成是指标方向标错了。indicator_meta.xlsx里那一列写成了max实际应该是min。这种错误特别隐蔽因为代码不报错、结果看起来也很「正常」只是逻辑反了。排查方法是打开raw_indicators.xlsx找那个最该排第一的方案看它在成本这一列上是不是最小值再去看权重表里这个指标的权重顺着往回查forward.m里switch走的是哪个分支。我一般会在forward.m里加一行调试输出把每个指标的处理类型打出来跑一次就能确认。现象三熵权法算出来的权重是NaN或者某一列权重直接是 0。前者的典型原因是某列在正向化后全部变成 0。比如一个极小型指标所有方案取值恰好相同max(col) - col全是 0sum为 0P Z ./ colSum就变成了0/0。后者的原因则是某列数据在各方案间完全一致信息熵为 1差异系数为 0权重自然为 0。这两种情况本质上都是「这个指标没有提供任何区分信息」。处理方式有两种一是把这类指标从评价体系里剔除因为它对排序确实没贡献二是如果这个指标在业务上必须保留比如合规性指标所有方案都达标那就改用 AHP 给它一个人为设定的权重不要指望熵权法。现象四MATLAB 读 Excel 时报错或者读进来数值全变成NaN。readtable会自动推断列类型。如果某一列里混了一个文本比如成本列里有人填了「暂无」整列会被判定为cell或stringtable2array一转就全是NaN。日期格式的列也有类似问题Excel 里的日期存的是序列号读进来可能是datetime类型。解决方式是先用summary(raw)看一眼每列的类型把文本列单独处理。对于「暂无」这种缺失值要么回原始表补全要么在代码里做插补用列均值或中位数填充不要让它悄悄变成NaN流到后面去。现象五往数据表里加了一个新方案之前所有方案的名次全变了。这个不是 bug是 TOPSIS 的固有特性。标准化和理想解都依赖全体样本加入新样本后每列的范数变了、max和min也可能变了所有人的分数都会跟着调整。所以用这个模型做的排名只在同一批数据的语境下有效跨批次比较没有意义。如果业务上确实需要跨批次可比得改成用固定的基准样本做标准化参数比如用历史全量数据算出每列的范数和理想解固化下来当配置新方案只用这组固定参数做变换。代价是基准样本一旦确定就不能随便改否则历史结果会失去可比性。6. 二次开发把评价模型接进自己的评分软件这个包最容易改造的地方就是它的「数据驱动」结构。想加指标在两张 Excel 里各加一行一列就行代码一行不动。想换权重方法把entropy_weight(Z)换成ahp_weight(A)或者写一个新的组合权重函数只要返回1×m的归一化向量后面的closeness完全不用改。这种「函数边界即扩展点」的设计是它能被塞进各种评分软件的前提。真正要接进业务系统还有两件事值得做。第一件是把指标配置从 Excel 迁移到数据库或 JSON因为线上环境读写 Excel 既慢又容易出并发问题。我一般会定义一个指标对象把name、type、param、weight_source四个字段序列化读取时反序列化成同样的结构体数组这样forward.m和entropy_weight.m的入参签名完全不用变属于无痛替换。第二件是加灵敏度分析判断排名稳不稳。做法很简单对权重向量做小幅度扰动比如每个权重乘以1 ± 5%后重新归一化跑 N 次常用 100 到 1000 次统计每个方案进入前三的频率。频率高的说明排名稳健频率低的说明它和相邻方案差距太小结论要谨慎。# python/sensitivity.py 核心逻辑 def weight_perturb(Z, base_w, n_iter500, sigma0.05, seed42): rng np.random.default_rng(seed) top3 np.zeros(Z.shape[0]) for _ in range(n_iter): w base_w * rng.normal(1.0, sigma, sizebase_w.shape) w np.clip(w, 0, None) w w / w.sum() # 扰动后重新归一化 score, _ closeness(Z, w) top3[np.argsort(-score)[:3]] 1 # 记录进入前三的次数 return top3 / n_iter # 转成频率rng.normal(1.0, sigma, size...)生成以 1 为中心、标准差sigma的正态扰动乘到原权重上就实现了「每个权重上下浮动 5%」。np.clip(w, 0, None)是防止扰动把权重变成负数负数权重在 TOPSIS 里没有物理意义会让距离计算出错。最后除以n_iter转成频率直接就能看出哪个方案的排名是「稳的」、哪个是「玄学」。seed42固定随机种子保证同一批数据每次跑出来的结果完全一致便于复现和对比。这个细节在给别人交付分析报告时很重要——别人拿到你的代码重跑一遍结果必须一模一样否则说服力会打折扣。还有一个我踩过的坑把closeness函数直接复制到业务代码里用忘了带权重归一化。熵权法算出来的权重天然和为 1但组合赋权之后如果不做归一化权重和可能是 2 或者 0.5距离会被整体放大或缩小虽然排名不变但贴近度的绝对值就没法跨批次比较了。从那以后我每次新增权重函数都强制在返回前加一行w w / sum(w)并写个断言校验图个心安。希望这些改造思路能帮到你把一套只跑一次的脚本变成能反复用的评分组件。本文还有配套的精品资源点击获取