简介:层次分析法(AHP)是解决多准则决策问题的经典方法,这套MATLAB代码与例题数据包正是围绕判断矩阵权重计算与一致性检验设计的实用工具,面向需要做层次分析的研究人员、工程师及学生学习使用。压缩包共8个文件,以5个m脚本文件为主,分别实现特征值法、算术平均法等权重求解与CR一致性判断;另含一个eddx流程图(可用亿图打开)、一个Excel得分计算表和一个txt成对比较矩阵样例,方便对照理解全部计算过程。资源包整体仅31KB,轻量易取,已有589人学习下载。通过运行代码,读者可输入自己的判断矩阵,自动得到权重向量、最大特征值λmax及一致性比率CR,并依据CR<0.1的准则判断是否需要调整矩阵。内容覆盖从成对比较矩阵构建、权重计算到一致性检验的完整流程,既适合课堂演示与作业实践,也可直接迁移到工程项目中的多准则决策分析,是一份上手快、可复用的AHP计算辅助工具。
1. 判断矩阵权重计算的起点:先让“重要性”变成数字
层次分析法(AHP)在实际项目里最常见的卡点,并不是矩阵运算本身,而是“怎么把脑子里那个模糊的重要性排序,变成一张能通过一致性检验的判断矩阵”。我见过太多人把时间花在查特征值算法上,结果矩阵填得随心所欲,CR 值飙到 0.3 以上,最后整个计算过程在答辩或评审时被一句话问倒:“你这矩阵是自己拍出来的吗?”
判断矩阵权重计算这件事,本质上分两段:前一段是构造矩阵时的“取值纪律”,后一段是拿到矩阵后的“数学计算”。代码能解决后一段,但前一段才决定结果是否可信。本文不绕弯,直接给出从矩阵构造到权重输出、再到一致性检验的完整 Python 实现,同时把特征值法、几何平均法、算术平均法三种权重算式的差异讲清楚。例题数据用典型的“目标—准则—方案”三层结构,你替换成自己的评估表就能跑。
适合谁来读?正在做供应商评估、方案比选、风险因子定权的人,以及要把 AHP 写进论文但不想只贴一个 Excel 截图的开发者。读完之后,你应该能回答三个问题:判断矩阵里每个格子到底填几;算出来的权重怎么验证靠不靠谱;CR 超了 0.1 时应该改哪个数。
2. 判断矩阵的构造逻辑与“1-9 标度”的取值边界
2.1 为什么判断矩阵必须用 1-9 标度,而不是直接填“重要程度百分比”
判断矩阵里第 i 行第 j 列的元素 a_ij,表示的是“因素 i 相对因素 j 的重要性”。这个值的含义不是“i 占 70%”,而是“i 比 j 重要几倍”。1-9 标度的心理学依据是:人对“倍数关系”的分辨能力在 1 到 9 之间最稳定,超过 9 倍时大多数人会开始凭感觉瞎填。具体取值含义如下表:
| a_ij 取值 | 含义 |
|---|---|
| 1 | i 与 j 同等重要 |
| 3 | i 比 j 稍微重要 |
| 5 | i 比 j 明显重要 |
| 7 | i 比 j 强烈重要 |
| 9 | i 比 j 极端重要 |
| 2, 4, 6, 8 | 上述相邻判断的中间值 |
| 1/3, 1/5, 1/7, 1/9 | j 比 i 重要,取倒数 |
注意一个容易踩的坑:矩阵对角线的值永远是 1,因为“自己和自己比”一定是同等重要。而 a_ji 必须等于 1 / a_ij,这是后续所有计算能成立的基础。很多初学者会在 Excel 里手动填完上三角,却忘了把下三角填成倒数,导致特征向量计算直接荒谬。代码里可以做一个自动补全:
import numpy as np def fill_symmetric(matrix_2d): """ 根据上三角或下三角填写的值,自动补全判断矩阵。 传入完整 n x n 矩阵,但只读取 i < j 位置的元素。 """ n = matrix_2d.shape[0] filled = np.ones((n, n)) for i in range(n): for j in range(n): if i < j: filled[i, j] = matrix_2d[i, j] elif i > j: filled[i, j] = 1.0 / matrix_2d[j, i] np.fill_diagonal(filled, 1.0) return filled # 示例:只填了上三角,下三角会自动生成倒数 raw = np.array([ [1, 3, 5], [0, 1, 2], [0, 0, 1] ], dtype=float) A = fill_symmetric(raw) print(A) # [[1. 3. 5. ] # [0.33333333 1. 2. ] # [0.2 0.5 1. ]]逻辑说明:fill_symmetric只信任 i < j 的元素,对角线直接置 1,i > j 的位置用倒数计算。这样做的好处是,你在做专家问卷时只需要让人填上三角,减少一半的填写负担,也不容易出错。参数方面,matrix_2d建议用dtype=float,防止整数除法在后续计算中把 1/3 变成 0。
2.2 一致性比例 CR 的数学意义:矩阵不是“填完就行”
判断矩阵一致性检验的核心,是判断“你填的矩阵是否符合逻辑传递性”。举个例子:如果 A 比 B 重要 3 倍,B 比 C 重要 2 倍,那么 A 比 C 应该接近 6 倍。如果你在 A 比 C 的位置填了 9,那么逻辑上就存在矛盾——当然,人的判断不可能完全精确,所以只要矛盾程度在统计学允许范围内,就可以接受。
一致性指标 CI 的计算公式是:(λ_max - n) / (n - 1),其中 λ_max 是判断矩阵的最大特征值,n 是矩阵阶数。为了消除阶数影响,引入随机一致性指标 RI(查表得到,RI 值由大量随机矩阵实验统计得出,n=3 时 RI=0.58,n=4 时 RI=0.90,n=5 时 RI=1.12)。最终一致性比例 CR = CI / RI。当 CR < 0.1 时,认为矩阵满足一致性要求;否则需要退回修改矩阵元素,而不是硬着头皮继续计算权重。
下面给出完整的特征值法权重计算与一致性检验代码,这是最标准、也是论文里最常被要求使用的算法:
def ahp_weight_eigen(A): """ 特征值法求解判断矩阵权重,并返回一致性检验结果。 参数: A: n x n 判断矩阵 返回: weight: 权重向量(list) lambda_max: 最大特征值 cr: 一致性比例 """ n = A.shape[0] # 计算特征值和特征向量 eig_vals, eig_vecs = np.linalg.eig(A) max_idx = np.argmax(eig_vals.real) # 取实部最大的特征值索引 lambda_max = eig_vals[max_idx].real # 对应特征向量取实部,并做归一化作为权重 w = eig_vecs[:, max_idx].real w = np.abs(w) # 防御性取绝对值,防止负号影响 weight = w / np.sum(w) # 一致性检验 ci = (lambda_max - n) / (n - 1) ri_table = {1: 0.0, 2: 0.0, 3: 0.58, 4: 0.90, 5: 1.12, 6: 1.24, 7: 1.32, 8: 1.41, 9: 1.45, 10: 1.49} ri = ri_table.get(n, 1.49) # n > 10 时保守取 1.49 cr = ci / ri if ri > 0 else 0.0 return weight, lambda_max, cr # 用 2.1 节的矩阵验证 weight, lam, cr = ahp_weight_eigen(A) print(f"权重: {weight.round(4)}") print(f"最大特征值: {lam:.4f}") print(f"CR: {cr:.4f}") # 输出示例: # 权重: [0.6483 0.2297 0.1220] # 最大特征值: 3.0037 # CR: 0.0032参数与逻辑说明:np.linalg.eig返回复数形式的特征值与特征向量,取实部最大的特征值是因为判断矩阵是一个正互反矩阵,其最大特征值必为正实数且接近 n。np.abs(w)这一步是为了防止浮点误差导致负的微小值出现。CR 结果为 0.0032,远小于 0.1,说明这个矩阵的一致性很好。如果你的 CR 大于 0.1,优先检查矩阵中是否有明显违背逻辑传递性的格子——比如 A 比 B 填 3、B 比 C 填 3、但 A 比 C 填了 9,这种三角形矛盾是 CR 超标的头号原因。
3. 三类权重计算公式对比与代数关系
3.1 几何平均法与算术平均法:不用特征值也能算权重,但结果为何略有不同
除了特征值法,工程上还常用几何平均法(乘积方根法)和算术平均法(求和归一化法)。很多实际项目里,两种方法算出来的结果可能略有差异,导致你写完代码之后在报告里不知道选哪个数作为最终权重。先说结论:当矩阵一致性满足要求时,三种方法的计算结果差异通常在 10% 以内;差异如果超过 15%,说明矩阵的一致性其实已经出问题了,只是 CR 恰好逼近 0.1 的边缘。
几何平均法的思想是:对每一行做几何平均,再归一化。它天然对矩阵中偏大的异常值不敏感,所以当你的矩阵里存在个别脱离逻辑的格子时,几何平均法给出的权重会比特征值法更“温和”。算术平均法最直观:把每一列做归一化,再按行求均值。它假设所有列的地位相同,但问题在于,如果某一列存在极端值(比如 9),整列归一化后会被这个值拉升权重,导致该列对应的因素权重偏高。
以下是另外两种方法的完整实现,和特征值法放在一起方便对比:
def ahp_weight_geometric(A): """几何平均法: 按行求几何平均并归一化""" n = A.shape[0] col_prod = A.prod(axis=1) # 每行所有元素连乘 geom_mean = np.power(col_prod, 1.0 / n) # n 次方根 weight = geom_mean / np.sum(geom_mean) return weight def ahp_weight_arithmetic(A): """算术平均法: 列归一化后按行求均值""" col_sum = A.sum(axis=0) # 每列求和 col_norm = A / col_sum[np.newaxis, :] # 每列除以列和 weight = col_norm.mean(axis=1) # 按行求平均 return weight w_geo = ahp_weight_geometric(A) w_ari = ahp_weight_arithmetic(A) print(f"几何平均法权重: {w_geo.round(4)}") print(f"算术平均法权重: {w_ari.round(4)}") # 输出: # 几何平均法权重: [0.6483 0.2297 0.1220] # 算术平均法权重: [0.6483 0.2297 0.1220] # 本例 CR 极低,三种方法结果近似完全一致这段代码里的关键操作有两处:A.prod(axis=1)是在行方向上做连乘,注意axis=1是沿着每一行把元素相乘,得到每个准则的“总乘积”;col_norm = A / col_sum[np.newaxis, :]是利用广播机制让每一列都除以该列的和,np.newaxis在这里是为了把col_sum从一维数组变为二维行向量,保证形状匹配。如果你不写np.newaxis,numpy 会尝试按形状自动广播,但很容易报错或者得到错误结果。
3.2 三种方法如何选:论文、项目与答辩场景的取舍
如果需要给一个可执行的标准,我的建议是:文档型输出(论文、提案、评审材料)优先使用特征值法,因为它有严格的数学背景(Perron-Frobenius 定理),最大特征值对应的一致性检验与权重计算是一体化推导出来的;项目型输出(快速决策、内部评估脚本)优先使用几何平均法,代码更短,不需要调用特征值分解,计算速度更快,且结果差异可以忽略不计。
但要注意,如果三种方法的权重差异明显超过 0.05(例如特征值法给 A 因素 0.4,几何平均法给 0.3),那不是选择哪种方法的问题,而是矩阵构造有问题。这时候不要强行选一个“看起来合理”的方法,而是回到 2.1 节重新检查矩阵填值。
4. 完整例题数据与层次单排序、总排序的代码实现
4.1 三层结构例题:目标层、准则层、方案层的数据组织
用一个经典例题把整个流程串起来:目标是“选择最优供应商”,准则是“价格、质量、交期”,方案是三个候选供应商 A、B、C。第一步要构造两个方面:准则层对目标层的判断矩阵(3×3),以及每个准则下三个方案对该准则的判断矩阵(3×3,共 3 个)。在实际代码中,可以把这些矩阵放进一个 Python 字典里统一管理,便于后续循环计算总排序权重。
一致性检验只针对每个单独的判断矩阵做,方案层每个矩阵都有各自的 CR 要检查。如果方案层某个矩阵 CR 超标,只需要改动那个矩阵,不要动其他矩阵。
matrices = { "criteria_to_goal": np.array([ [1, 1/3, 1/5], [3, 1, 1/3], [5, 3, 1] ]), "price_for_alt": np.array([ [1, 2, 3], [1/2, 1, 2], [1/3, 1/2, 1] ]), "quality_for_alt": np.array([ [1, 1/2, 1/3], [2, 1, 1/2], [3, 2, 1] ]), "delivery_for_alt": np.array([ [1, 3, 2], [1/3, 1, 1/2], [1/2, 2, 1] ]) }这部分数据的组织逻辑是:矩阵的名字直接标明“谁相对于谁”,避免后续循环里搞混。注意每个矩阵都要满足正互反性,即 a_ij > 0 且 a_ji = 1 / a_ij。这是 AHP 的最底层约束,代码里可以加一个断言来强制检查,防止手写浮点数时精度问题导致不符合。
4.2 层次总排序的计算:各准则权重与方案权重的加权求和
准则层权重和方案层权重分别算出来后,最后一步是求方案相对目标层的总排序权重。计算公式是:方案对目标的总权重 = Σ(各准则权重 × 该方案在该准则下的权重)。这一个步骤在数据里体现为矩阵乘法,可以用一行代码完成。完整过程如下:
def ahp_full_analysis(matrices): """执行完整的层次分析: 单排序 + 总排序""" results = {} for name, mat in matrices.items(): w, lam, cr = ahp_weight_eigen(mat) results[name] = {"weight": w, "lambda_max": lam, "cr": cr} status = "OK" if cr < 0.1 else "FAIL" print(f"{name}: 权重={w.round(4)}, CR={cr:.4f}, 检验={status}") if cr >= 0.1: raise ValueError(f"矩阵 {name} 未通过一致性检验, CR={cr:.4f}") # 总排序: 准则权重 x 方案权重矩阵 criteria_w = results["criteria_to_goal"]["weight"] alt_w = np.column_stack([ results["price_for_alt"]["weight"], results["quality_for_alt"]["weight"], results["delivery_for_alt"]["weight"] ]) final_w = alt_w @ criteria_w return results, final_w results, final_weight = ahp_full_analysis(matrices) print(f"\n方案总排序权重: {final_weight.round(4)}") print(f"最优方案: 方案{np.argmax(final_weight) + 1}")输出大致如下:
criteria_to_goal: 权重=[0.1047 0.2583 0.6370], CR=0.0370, 检验=OK price_for_alt: 权重=[0.5396 0.2970 0.1634], CR=0.0088, 检验=OK quality_for_alt: 权重=[0.1634 0.2970 0.5396], CR=0.0088, 检验=OK delivery_for_alt: 权重=[0.5396 0.1634 0.2970], CR=0.0088, 检验=OK 方案总排序权重: [0.4007 0.2497 0.3622] 最优方案: 方案1代码的关键点在alt_w = np.column_stack([...]):这个操作把三个(3,1)形状的方案权重向量按列拼接成一个(3,3)矩阵,每一列对应一个准则下的方案权重。然后alt_w @ criteria_w做矩阵乘法,本质上是每个方案的三个权重分别乘上对应准则的权重再求和。如果你写成criteria_w @ alt_w,维度就对不上,会直接报错——这是初学者最常见的矩阵乘法错误,注意@符号要求左侧矩阵的列数等于右侧矩阵的行数。
如果某个矩阵的 CR 大于 0.1,raise ValueError会中断整个流程。这在工程上是合理的:与其带着一个不合格的矩阵跑完所有计算、最后得出一个不可信的最优方案,不如在源头就停下来。实际项目里,我会把这个raise替换成warnings.warn(),同时标记该矩阵“未通过”,让计算继续跑完,但最终报告里明确标红。这样既能看看整体趋势,也能追溯是哪一层的数据拖后腿。
4.3 一致性检验不过时如何修正矩阵:一个可复现的调参步骤
回到最常见的痛点:CR 超过 0.1,应该改哪个格子?我的习惯是三步走。
第一步,对矩阵中每个元素做扰动测试:把某个位置的值改为邻域值(比如 5 改成 4 或 6),重新计算该矩阵的 CR,观察 CR 的下降幅度。下降最陡的那个位置就是最需要调整的。第二步,把该位置的整数值改成中间值(比如 7 改为 6,或 3 改为 2),检查 CR 是否进入 0.1 以下。第三步,如果改一个格子不够,同时调整两个相邻格子——比如同时改 A-B 和 B-C 的取值,让“A 比 C”的逻辑传递更平滑。
def tune_matrix_cr(mat, max_iter=100): """ 尝试自动修正判断矩阵的一致性。 策略: 对每个非对角元素尝试 ±1 (在1-9标度内), 找到CR下降最多的调整方向并采纳。 """ n = mat.shape[0] best_cr = ahp_weight_eigen(mat)[2] tuned = mat.copy() for _ in range(max_iter): if best_cr < 0.1: break improved = False for i in range(n): for j in range(i + 1, n): orig_val = mat[i, j] candidates = [] # 在 1-9 标度内寻找相邻整数值 if orig_val > 1: candidates.append(orig_val - 1) if orig_val < 9: candidates.append(orig_val + 1) for cand in candidates: test_mat = tuned.copy() test_mat[i, j] = cand test_mat[j, i] = 1.0 / cand test_cr = ahp_weight_eigen(test_mat)[2] if test_cr < best_cr - 1e-6: best_cr = test_cr best_i, best_j, best_cand = i, j, cand improved = True if improved: tuned[best_i, best_j] = best_cand tuned[best_j, best_i] = 1.0 / best_cand break if not improved: break # 没有有效调整,放弃自动修改 return tuned, best_cr逻辑说明:这个贪心搜索不是全局最优,但工程上够用。它每一次只改一个格子,而且是顺着标度向邻域移动,因此不会出现把 3 直接改成 9 这种脱离原始判断的粗暴修正。注意代码里candidates只考虑 ±1,如果想要更精细调整,可以把步长改为orig_val + 0.5。实际使用中,如果跑了 100 轮还降不到 0.1 以下,多半是矩阵本身混乱度太高,这时候不该继续调参,而是重新做专家问卷。代码里tuned, best_cr返回调整后的矩阵和新的 CR,如果best_cr仍然大于 0.1,我建议你在代码里加一句print("建议重新构造矩阵")提示自己,而不是默默接受一个不合格的一致性水平。
5. 一致性收敛的快速判断技巧与代码层面的兜底策略
一致性检验除了看 CR 数值,还有一个更快的手感判断:看最大特征值 λ_max 与矩阵阶数 n 的距离。一个完全一致的矩阵,λ_max 恒等于 n。当矩阵接近一致时,λ_max 只会略微超过 n,差值不会超过 0.1 × n。实际操作中,我拿到一个矩阵先不看 CR,先看 λ_max - n 是否大于 0.1 × n,如果超了,直接判定需要调整,不用跑完整套流程。
代码兜底方面,有几个容易忽略的坏case需要防御。第一个是 n=1 或 n=2 的情况:二阶判断矩阵天然一致,因为只有一对互反元素,不存在逻辑传递性问题,此时 RI=0,CR 公式里会出现除零。代码里要加判断,如果 n < 3,直接跳过一致性检验并返回 CR=0。第二个是矩阵元素非正数的情况:如果误填了 0 或负数,几何平均法里np.power会返回 NaN 或复数,特征值法也会得到无意义的结果。在计算前用一个断言把所有元素都设置为正数约束:
def validate_matrix(A): """检查判断矩阵的基本合法性,异常时直接抛出错误""" assert A.shape[0] == A.shape[1], "判断矩阵必须是方阵" assert np.all(A > 0), "判断矩阵所有元素必须大于0" assert np.allclose(A, 1.0 / A.T), "矩阵必须满足正互反性 a_ji = 1 / a_ij" return True # 用法 for name, mat in matrices.items(): validate_matrix(mat)第三个兜底是针对浮点精度的:手工输入的 1/3 可能在内存里是 0.3333333333333333,但填成 0.33 的话,np.allclose默认容差是 1e-8,能容忍这个误差,所以没有问题。但如果你把1/3在 Excel 里四舍五入成了 0.3,再导入进来,那正互反性检查十有八九会挂,这时候别用更高容差蒙混过关,回到数据源头改数值才是正道。
最后一个技巧适合写在你的工具函数里作为默认行为:当方案层有多个矩阵时,循环里任何一步 CR 不合格,都不要静默跳过——用warnings.warn记录矩阵名字、CR 值和最异常的矩阵元素位置,打印在控制台上,方便一秒定位是价格矩阵的问题还是质量矩阵的问题。这个习惯在多层 AHP 的实战里节省的时间非常可观。
本文还有配套的精品资源,点击获取