实战:从判断矩阵到一致性检验的Python实现)
大家有没有遇到过这种场景部门评审会上方案A的支持者说它成本低方案B的支持者说它交付快方案C的支持者说它综合实力强各说各话谁也说服不了谁最后只能靠领导拍脑袋或者大家投票投出一个谁都不服的结果。我前几年做供应商选型项目时就陷进过这种僵局。后来把一个老办法重新捡起来——AHP也就是层次分析法。它不复杂核心思想就一句话把决策目标拆成若干层然后让人对不同指标、不同方案两两比较打分最后用矩阵计算把主观判断变成一组可比较的权重和得分。这篇文章我就把AHP的完整逻辑、数学细节、Python实现以及我在实际项目里踩过的坑一次性讲清楚适合正在做多准则决策、评价模型或者课程项目设计的同学参考。1. AHP到底解决什么问题一次供应商选型引发的思考1.1 为什么多准则决策这么容易吵起来先说一个反直觉的点多准则决策之所以难不是因为信息少而是因为信息维度不统一。成本可以用人民币量化交期可以用天数量化质量合格率是百分比服务响应快慢又完全是主观感受。这些指标放在一起连谁比谁好都很难定义更别提好多少了。AHP的思路是把复杂的综合判断拆成一层一层的两两比较。你不需要直接回答供应商A综合得分比B高多少你只需要回答一个更简单的问题在质量这个指标上A比B明显好但是不到绝对优势那给5分合适还是4分合适每个判断都简单但把它们组装起来通过矩阵运算就能得到一套自洽的权重和排序结果。我之所以重新用AHP是因为它有三个别的工具替代不了的优势结果可解释。每一步比较、每个权重都能追溯评审会上能拿得出来讲道理。不依赖样本量。没有历史数据也能做专家经验本身就是输入。有内在的一致性检验。打分互相矛盾的时候算法会告诉你这里有问题这一点很多拍脑袋决策根本做不到。1.2 一个可以完整跑通的小案例三家供应商选型为了让后面的原理和代码不悬空我先把这次项目里用到的案例固定下来。假设要在这三家供应商里选一家长期合作伙伴供应商A质量过硬价格偏高交期中等供应商B价格最低但质量有波动服务响应快供应商C综合表现均衡技术和交付都稳评价指标体系拆成两层目标层是选择最佳供应商准则层有五个指标分别是质量、价格、交期、服务、技术能力。这个结构就是AHP里的层次结构下面所有计算都会围绕它展开。我在实际项目里做的是双层的完整流程先对五个准则做两两比较算出准则的权重再分别针对每个准则对三家供应商做两两比较算出每个准则下三家供应商的得分最后加权求和得到总得分排序。这套流程既适合单层决策也适合直接扩展到多层。下面每一节都会对应到这个案例的某一个环节。2. 从判断矩阵到权重AHP的核心计算逻辑2.1 1-9标度与成对比较矩阵的构造规则AHP的交易成本在于两两比较它的计分规则是经典的1-9标度表。拿质量指标比较供应商A和供应商B时如果我认为A比B明显好就填5如果只是稍微好一点填3如果两者一样好填1。反过来如果B比A差就填对应的倒数比如1/5。具体标度含义如下标度含义1两个因素同等重要3前者比后者稍微重要5前者比后者明显重要7前者比后者强烈重要9前者比后者极端重要2、4、6、8上述相邻判断的中间值倒数后一个因素相对前一个因素的重要程度把这组比较结果填进矩阵就得到判断矩阵A。它的每一行代表以行指标为基准行指标相对于各列指标的重要程度。主对角线一定是1因为自己和自己在同一个标准下当然同等重要。同时有 a_ij 1 / a_ji这就是互反性。给我留下印象最深的是很多新手第一次构造矩阵会把重要程度和偏好方向搞反填了个3进去等发现矩阵不对称又不知道怎么改。构造完成的5阶判断矩阵比如准则层可能是这样A [ [1, 3, 5, 4, 2], [1/3, 1, 2, 3, 1/2], [1/5, 1/2, 1, 2, 1/3], [1/4, 1/3, 1/2, 1, 1/5], [1/2, 2, 3, 5, 1] ]这组数看起来随意但矩阵里隐含了一定的一致性要求。比如质量比价格重要3倍价格比交期重要2倍那质量相对交期应该是6倍左右才合理填5或者7都算可接受填9就说明前后判断打架了。这正是下一节一致性检验要做的事。2.2 三种权重计算方法特征向量、几何平均、算数平均拿到判断矩阵后要从中提取每个指标的权重。理论上有三种主流算法都能算出权重向量只是数学思想不同特征向量法。矩阵A的最大特征值 λmax 对应的特征向量经过归一化后就是权重向量。这是层次分析法原始定义里的计算方法数学上最正统。当矩阵完全一致时λmax 恰好等于矩阵阶数n对应的特征向量精确反映相对重要性。几何平均法也叫根法。对每一行做几何平均也就是把该行所有元素连乘后再开n次方得到一个临时值再把所有行的临时值归一化成权重w_i (∏ a_ij)^(1/n) / Σ_k (∏ a_kj)^(1/n)算数平均法。先把每一列归一化让每列之和等于1再对每一行求平均w_i (1/n) Σ_j ( a_ij / Σ_k a_kj )需要说明的是三种方法在矩阵一致性良好时算出的结果非常接近差异通常在小数点后第二位。但如果矩阵一致性很差三种结果会明显发散这时候发散程度本身就是判断不可靠的一种信号。2.3 底层原理为什么几何平均法在工程实践里最稳理论上推荐特征向量法因为从特征分解的角度看它和最大特征值、一致性指标天然绑定一套计算流程通吃。但我在实际写代码和做项目时更常用几何平均法原因有三个。第一几何平均法对互反性的利用方式更接近工程直觉。特征向量法的结果受最大特征值主导如果矩阵一致性不佳个别夸大或缩小的判断会对权重产生不成比例的影响几何平均法则对每一行做连乘开方天然对异常值做了平滑。第二几何平均法和最小二乘意义下的对数回归有内在联系在鲁棒性上优于算数平均法而且计算量极小不涉及特征分解对高维矩阵也不会出现复数特征值这种让人头疼的问题。第三就算用了几何平均法算权重一致性检验用的最大特征值依然可以单独用数值方法求出来两条线互不干扰。所以我的推荐组合是权重用几何平均法一致性检验用特征值法各取所长。如果矩阵一致性完美三种方法本质上殊途同归。真正出现显著差异时你反倒应该回头检查判断矩阵本身而不是纠结算法。3. 一致性检验CR的计算、查表与矩阵修正3.1 CI、RI、CR到底在算什么一致性检验是整个AHP里最容易被跳过、却最值得认真对待的环节。它的核心思想是如果判断矩阵内部逻辑自洽那么最大特征值 λmax 应该刚好等于矩阵阶数n。一旦 λmax 大于n就说明判断之间存在矛盾。矛盾程度用一致性指标CI来度量CI ( λmax - n ) / ( n - 1 )但CI的绝对值受矩阵阶数影响n越大随机产生的矩阵天然的一致性就越差。所以还要引入随机一致性指标RI用CR CI / RI 做归一化。RI的数值是固定的查表值n12345678910RI000.520.891.121.261.361.411.461.49判断标准是CR 0.10 时认为矩阵的一致性可接受CR ≥ 0.10 时需要修正判断矩阵。需要注意n1和n2的矩阵天然完全一致因为只有一两个比较关系根本不可能出现循环矛盾计算CI无意义直接跳过检验即可。顺带说明一个很多人误解的点一致性检验不是学术表演。我遇到过判断矩阵CR高达0.35的情况仔细一查是评审专家对价格和交期的关系前后打了矛盾的分。如果不做检验这个矛盾会直接污染权重导致最终排序失真。而在评审会上CR值反而是最有说服力的工具它能告诉所有人打分是有质量控制的。3.2 矩阵不满足一致性时应该如何修正修正矩阵最忌讳的做法是直接把矛盾项改成看起来合理的数字。正确流程是先定位最矛盾的判断再小步调整反复检验。具体操作可以这样来做。计算 λmax 对应的特征向量也就是权重向量 w然后逐项比较矩阵原始值与理论值。理论值就是如果矩阵完全一致第i行第j列应该等于权重比 w_i / w_j。找出原始值偏离理论值最大的一项把它向理论值方向调整一到两个标度比如原来是5理论值接近3就先改成3或4重新计算CR。我通常一次只改一个值因为同时改多项会让矩阵完全失去解释性你无法向评审专家说清楚改了哪几个数字、为什么改。定位矛盾项的代码逻辑其实不复杂后面第4节会一起给出。这里先记住修正原则小步、单点、可解释。我在项目里还留了一手把每一轮修正前后的矩阵和CR值都记录在案评审会上直接展示第一次打分CR0.18其中质量与价格的比较偏差最大调整为4之后CR0.07这个解释路径非常容易被接受。4. Python实现一个完整的AHP计算模块4.1 模块结构设计校验、权重、一致性、得分一体化我建议不要在网上随便拉一个一行版AHP代码就用那种精简实现通常缺了互反性校验和异常处理拿到真实数据时很容易算出个负权重或者复特征值。一个能用于实际项目的AHP模块至少要包含四层能力输入校验、权重计算、一致性检验、方案综合得分。输入校验负责确认矩阵元素全部为正、互反、对角为1权重计算用几何平均法和特征值法双实现一致性检验输出λmax、CI、RI、CR和矛盾定位综合得分负责把准则权重和各方案得分矩阵合并成总排序。4.2 核心代码权重计算与一致性检验下面是我在项目里实际使用的模块精简版只依赖numpy连pandas都不是必需的import numpy as np class AHP: 层次分析法计算模块 def __init__(self, matrix): self.matrix np.array(matrix, dtypefloat) self.n self.matrix.shape[0] self._validate() def _validate(self): if self.matrix.ndim ! 2 or self.matrix.shape[0] ! self.matrix.shape[1]: raise ValueError(判断矩阵必须是方阵) if (self.matrix 0).any(): raise ValueError(判断矩阵所有元素必须为正) if not np.allclose(self.matrix, 1.0 / self.matrix.T, rtol1e-6): raise ValueError(判断矩阵不满足互反性) if not np.allclose(np.diag(self.matrix), 1.0): raise ValueError(判断矩阵对角线必须为1) def weight_by_geometric_mean(self): 几何平均法求权重 row_geom self.matrix.prod(axis1) ** (1.0 / self.n) return row_geom / row_geom.sum() def weight_by_eigen(self): 特征向量法求权重 eig_vals, eig_vecs np.linalg.eig(self.matrix) max_idx np.argmax(eig_vals.real) w np.abs(eig_vecs[:, max_idx].real) return w / w.sum(), eig_vals[max_idx].real def consistency_check(self): 返回 lambda_max, CI, RI, CR RI {1: 0.0, 2: 0.0, 3: 0.52, 4: 0.89, 5: 1.12, 6: 1.26, 7: 1.36, 8: 1.41, 9: 1.46, 10: 1.49} if self.n 2: return self.n, 0.0, 0.0, 0.0 _, lambda_max self.weight_by_eigen() ci (lambda_max - self.n) / (self.n - 1) ri RI[self.n] cr ci / ri if ri 0 else 0.0 return lambda_max, ci, ri, cr def locate_bad_judgment(self): 定位偏差最大的一项 w self.weight_by_geometric_mean() theory np.outer(w, 1.0 / w) deviation np.abs(self.matrix - theory) np.fill_diagonal(deviation, 0) idx np.unravel_index(np.argmax(deviation), deviation.shape) return idx, theory[idx], self.matrix[idx]这段代码要重点说几个设计细节。_validate方法里的互反性校验用了np.allclose而不是严格相等因为打分结果可能是1/3这种除不尽的浮点数直接判相等一定会误报。weight_by_eigen里取特征向量时做了np.abs和.real处理因为数值计算中复特征向量的虚部通常只是浮点误差不能直接当错误处理。consistency_check里把n小于等于2的情况直接返回0避免除以0。有一个让我印象深刻的坑判断矩阵的某一项被打成0.2时互反项应该是5但人工录入容易写成5.0的反倒数0.333。这会导致整个矩阵虽然数值上能算但CR值高得离谱。套用这个模块的校验后会第一时间被拦下输入质量提升非常明显。4.3 完整案例跑通从判断矩阵到方案排序继续用供应商选型的例子。先定义准则层判断矩阵然后计算权重和一致性criteria_matrix [ [1, 3, 5, 4, 2], [1/3, 1, 2, 3, 1/2], [1/5, 1/2, 1, 2, 1/3], [1/4, 1/3, 1/2, 1, 1/5], [1/2, 2, 3, 5, 1] ] ahp AHP(criteria_matrix) w_criteria ahp.weight_by_geometric_mean() lambda_max, ci, ri, cr ahp.consistency_check() print(准则权重, np.round(w_criteria, 4)) print(fλmax{lambda_max:.4f}, CI{ci:.4f}, CR{cr:.4f})我用的这个矩阵算出来的CR大约是0.02左右远小于0.1一致性良好。接下来对每个准则构造方案层判断矩阵。以质量准则为例三家供应商A、B、C的成对判断可能是quality_matrix [ [1, 5, 3], [1/5, 1, 1/2], [1/3, 2, 1] ] price_matrix [ [1, 1/3, 1/2], [3, 1, 2], [2, 1/2, 1] ] delivery_matrix [ [1, 2, 1/2], [1/2, 1, 1/3], [2, 3, 1] ] service_matrix [ [1, 1/2, 2], [2, 1, 3], [1/2, 1/3, 1] ] tech_matrix [ [1, 3, 2], [1/3, 1, 1/2], [1/2, 2, 1] ]对每个矩阵分别算权重和CR然后把各准则下得到的方案得分横向拼接成得分矩阵再与准则权重做矩阵乘法all_matrices [quality_matrix, price_matrix, delivery_matrix, service_matrix, tech_matrix] score_matrix [] for m in all_matrices: a AHP(m) score_matrix.append(a.weight_by_geometric_mean()) _, _, _, cr a.consistency_check() if cr 0.1: print(警告某准则下的方案判断矩阵CR过高) score_matrix np.array(score_matrix) # 形状 (5, 3) final_score w_criteria score_matrix print(三家供应商最终得分, np.round(final_score, 4))我这里的模拟数据显示供应商A在最终加权后略领先但和C非常接近差距在1个百分点以内。这种时候就暴露了另一个现实问题AHP给出的排序结果未必是所有参与者喜闻乐见的但它至少把分歧从我觉得拉到了数据上差多少。差距很小的情况下我会自动触发敏感性分析也就是逐个改变准则权重观察排序是否翻转这比直接下结论稳妥得多。4.4 边界条件与异常输入处理真实项目里除了常规计算边界条件往往才是写代码时最容易翻车的地方。我总结了几类常见异常以及模块中对应的处理策略。判断矩阵阶数为1或2时一致性检验直接跳过否则RI0会导致除零错误。矩阵阶数超过10时标准RI查表值已经不够用了更关键的是专家几乎不可能对10阶以上的矩阵给出稳定判断。我建议在模块里对n10的情况直接抛出警告或者引导用户对准则进行分组。特征值计算返回复数时只有虚部远小于实部才能安全取实部。如果虚部占比异常说明矩阵数值稳定性有问题应该回到原始打分数据检查。缺失值处理的思路要特别说一句标准AHP不允许判断矩阵存在空缺。如果专家真的没法比较某两项那种不知道填多少的分值应该明确标记而不是填0或者填1因为0会直接触发非正校验填1则会给用户两者同等重要的误导。我在项目里遇到这种情况通常回到评审现场让相关专家小组补一轮讨论而不是在代码层面硬凑。5. 实战里的坑专家打分、多层级与主客观结合5.1 打分环节最常见的三类失误代码写对了不代表项目就成功了因为AHP的输入是人打出来的分。我见过最多的三类失误如下。第一类是反向指标没统一方向。价格、交期这类指标是越低越好质量、服务是越高越好。如果比较矩阵里混用方向计算出的方案得分就会失真。我的做法是进入打分环节之前先把所有指标统一成越高越好的表达比如价格转化为性价比交期转化为准时交付能力评审专家就不会被带偏。第二类是打分区间的人为收缩。很多专家打分偏好集中在1到3之间很少用7和9。这会导致权重区分度不足排序结果整体向均等靠拢。我在实际项目里的处理方式是打分前展示完整的标度对照表并提醒5分是什么含义、9分是什么含义让分值有锚点。第三类是被多方博弈绑架。当多个部门参与评审时质量优先和成本优先的两派会在打分表里刻意放大自己关心指标的权重。AHP管不了政治博弈但能暴露博弈。我一般会让每位专家独立填表、互不见面然后把多份矩阵的平均矩阵作为正式输入同时在汇总后对每位专家的CR做公示这个流程下来刻意放大权重的现象会收敛很多。5.2 多层级AHP局部权重怎么合并成全局权重单层AHP的规则是准则层权重直接乘方案得分矩阵。到了多层结构比如质量下面又分合格率返修率认证体系三个子准则就需要先把子准则的局部权重算出来再和它父准则的权重做乘法得到全局权重。举一个我项目里的简化例子。准则层里质量的权重是0.42子准则层里合格率相对质量的局部权重是0.6返修率是0.4那么合格率在全模型中的全局权重就是0.42 × 0.6 0.252。最终方案得分等于所有最底层准则的全局权重和方案在这些底层准则上的得分的加权和。做多层结构时容易踩的坑是只检验每一层的矩阵一致性却忘了跨层的一致性也要统一判断。比如质量在准则层明显高于价格但到了子准则层价格下属的子准则数量比质量多很多那么方案的最终排序可能被数量优势拉偏。这不是算法错误而是模型设计问题。我的建议是子准则尽量均衡分配每个父准则下子准则数量不要相差过大。5.3 AHP与熵权法组合让主观判断与客观数据互相校正最后聊一个我在实际项目里用下来效果很好的扩展把AHP的主观权重和熵权法的客观权重做组合。AHP的优点是从专家经验出发缺点是完全受主观判断影响如果评审团队对业务理解本身有偏差权重就会系统性失真。熵权法正好相反它完全从实际的指标数据出发根据数据的离散程度确定权重数据波动大的指标权重更高但它完全不理解业务含义。组合公式很直接w_组合 α × w_AHP (1 - α) × w_熵权α的取值通常在0.4到0.6之间需要结合业务场景确定或者做多档α的敏感性分析观察最终排序是否稳定。我记得在某个设备选型项目里纯AHP给出的权重把性能放在第一但熵权法因为样本里性能指标几乎没有波动给出的性能权重极低。两种方法差异大到排序直接变脸。后来采用等权组合取α0.5得到的排序既照顾了专家判断又没有被主观偏好完全带偏评审各方对结果的意见比之前统一多了。还要提醒一句熵权法要求数据矩阵标准化而且遇到负向指标要先做取倒数或取反处理否则方向一错整个客观权重就反了。另外样本量太少时熵权法极不稳定这时宁可减少α的权重也不能让数据噪声主导模型。我后来把AHP、一致性检验报告、敏感性分析这三个模块做成了一套流水线每次评审会直接导出一份带中间矩阵、CR值和排序结果的报告。做这行的朋友如果有类似的多准则决策需求我建议也从这套最小闭环开始先把判断矩阵和一致性检验跑通再逐层加多层结构、组合权重这些进阶功能。那套代码我至今还在复用只是每到一个新项目就换一套指标体系和矩阵数据性价比非常高。