
简介这是一款基于 JavaScript 实现的层次分析法AHP可视化工具面向需要借助结构化方法辅助个人或团队决策的学习者与开发者可用于多准则方案比选、权重计算等场景。压缩包共 14 个文件约 476KB以 5 个 js 脚本承载核心计算与模块逻辑配合 2 个 json 配置、2 个 css 样式及 1 个 html 入口页面另附 license、notice、md 说明等文档整体轻量、结构清晰便于本地直接运行与二次开发。目前已有 814 人学习下载。工具将 AHP 的判断矩阵与权重计算过程可视化呈现读者可借此理解层次分析法的建模思路与计算流程源码模块划分明确适合作为前端练手项目或决策类应用的参考实现。需注意当前版本尚无数据持久化能力页面回退可能导致输入丢失作者也欢迎提交拉请求共同完善。1. 层次分析法工具为什么一个“打分工具”值得用 JavaScript 重写很多人第一次接触层次分析法AHP是在 Excel 里手敲判断矩阵然后对着特征向量发呆。表面上看AHP 就是个“打分加权”的活儿似乎用不着专门写个工具。但真正做过一次完整 AHP 的人都知道麻烦不在打分而在一致性检验——你填的矩阵很可能通不过 CR 0.1得回头改改完又可能牵动别的元素来回折腾。ahp-tool 这个 JavaScript 工具解决的正是这个循环把判断矩阵的构造、特征向量求解、一致性比率计算和敏感性回看串成一条可复现的流程。它值得搞懂因为 AHP 的“玄学感”几乎全部来自矩阵特征值求解和一致性修正这两步而这两步恰恰是可以用代码讲清楚的。2. 判断矩阵与特征向量AHP 的数学骨架到底在算什么AHP 的核心不是“打分”而是把人对两两比较的主观判断压缩成一个可计算的权重向量。这个过程依赖一个关键假设如果决策者的判断完全一致那么判断矩阵 A 应该满足 a_ij w_i / w_j此时 A 是一个秩为 1 的正互反矩阵最大特征值恰好等于 n对应的特征向量就是权重。现实中的判断不可能完全一致所以最大特征值 λ_max 会略大于 n偏离程度就成了衡量“判断有多不一致”的标尺。2.1 正互反矩阵的结构约束一个 n 阶 AHP 判断矩阵只有 n(n-1)/2 个自由元素因为对角线恒为 1且 a_ji 1/a_ij。这个约束不是数学装饰它直接决定了工具的数据结构该怎么设计。常见的实现思路是用一个上三角数组存储读取时按索引映射回二维坐标写入时同时维护对称位置。这样做的好处是杜绝了“只改了一半”的脏数据——如果让用户直接编辑二维表很容易出现 a_ij 改了、a_ji 没跟着变的情况后续求特征向量时结果就会莫名其妙地偏。// 上三角压缩存储values 长度 n*(n-1)/2 function buildMatrix(n, values) { const A Array.from({ length: n }, () Array(n).fill(1)); let k 0; for (let i 0; i n; i) { for (let j i 1; j n; j) { const v values[k]; A[i][j] v; // 上三角存原始判断 A[j][i] 1 / v; // 下三角自动取倒数保证正互反 } } return A; }这段代码的关键在第 8 行下三角永远由上三角推导而不是独立存储。参数 n 是准则或方案的数量values 是用户逐对比较后得到的 n(n-1)/2 个标度值通常取 1 到 9 及其倒数。这样构造出来的矩阵天然满足正互反性后续求特征值时才不会因为数据不自洽而翻车。2.2 特征向量的数值求解幂法为什么够用理论上权重向量是 A 的主特征向量可以用解析法求特征多项式但 n 一旦超过 4手算基本不现实。工程上普遍用幂法迭代任取一个正初始向量反复用 A 去乘并归一化收敛后得到的就是主特征向量。幂法能 work 的前提是矩阵存在唯一的主特征值而正互反矩阵恰好满足 Perron-Frobenius 条件主特征值是正实数且严格占优所以迭代一定收敛。function powerIteration(A, maxIter 100, tol 1e-9) { const n A.length; let w Array(n).fill(1 / n); // 均匀初始权重 let lambda 0; for (let iter 0; iter maxIter; iter) { const next A.map(row row.reduce((s, a, j) s a * w[j], 0) // A * w ); const sum next.reduce((s, x) s x, 0); const newW next.map(x x / sum); // 归一化 lambda sum; // 归一化因子即 λ_max 的估计 const diff newW.reduce((s, x, i) s Math.abs(x - w[i]), 0); w newW; if (diff tol) break; // 收敛判据 } return { weights: w, lambdaMax: lambda }; }这里有个容易被忽略的细节归一化因子 sum 在收敛时正好趋近 λ_max所以不需要额外再算一次特征值。maxIter 设 100 是保守值实际 n 在 10 以内时通常 20 次以内就收敛tol 用 1e-9 是权重精度和浮点误差之间的折中。如果迭代不收敛八成是矩阵里混进了 0 或负数幂法对非正矩阵没有保证。2.3 一致性比率 CR把“感觉不对”量化成数字算出 λ_max 之后一致性指标 CI (λ_max - n) / (n - 1)再除以随机一致性指标 RI得到 CR CI / RI。CR 0.1 是通行阈值。RI 是一张按 n 查表的经验值n1 到 9 分别对应 0、0、0.58、0.90、1.12、1.24、1.32、1.41、1.45。这张表不是推导出来的而是大量随机正互反矩阵的 CI 平均值所以工具里必须内置这张表不能现场算。n123456789RI000.580.901.121.241.321.411.45n1 和 n2 时 CR 恒为 0因为二阶正互反矩阵天然完全一致这一点在工具里要特殊处理否则会出现除零。很多手写脚本在这里翻车就是因为没判断 n 3 的情况。3. 一致性修正与敏感性工具真正省时间的地方算出 CR 超标只是开始真正耗人的是“改哪个元素”。ahp-tool 这类工具的价值在于把修正过程从盲试变成有方向的调整。常见做法是定位对一致性破坏最大的元素对提示用户重新审视那一对比较而不是让人从头再填一遍。3.1 定位不一致的来源偏差矩阵一个实用的思路是构造偏差矩阵 D其中 d_ij a_ij * w_j / w_i。如果判断完全一致D 的所有元素都等于 1偏离 1 越远说明这一对比较与整体权重越矛盾。工具可以按 |d_ij - 1| 排序把最矛盾的几对高亮出来。function inconsistencyMap(A, weights) { const n A.length; const issues []; for (let i 0; i n; i) { for (let j i 1; j n; j) { const expected weights[i] / weights[j]; // 一致时应有的比值 const deviation Math.abs(A[i][j] / expected - 1); issues.push({ i, j, deviation }); } } return issues.sort((a, b) b.deviation - a.deviation); }deviation 越大说明用户填的 a_ij 和最终权重推出来的比值差距越大。参数 weights 来自上一节的幂法结果。这个函数不直接改矩阵而是给出“最值得复核的比较对”把修正变成有靶子的动作。我一般会建议用户只调 deviation 排名前三的对因为一致性是全局性质局部大改往往比全面微调更快收敛。3.2 敏感性分析权重稳不稳换个标度会不会翻盘AHP 的另一个痛点是标度主观性。同一组比较有人用 3 表示“稍重要”有人用 4最终权重排序可能变。敏感性分析就是回答“排序对扰动有多敏感”。简单做法是对每个 a_ij 施加 ±10% 的扰动重算权重观察排序是否翻转。工具里可以只对排名相邻的方案做这个检查计算量可控。function sensitivityCheck(A, baseWeights, delta 0.1) { const n A.length; const flips []; for (let i 0; i n; i) { for (let j i 1; j n; j) { const B A.map(r r.slice()); B[i][j] * (1 delta); B[j][i] 1 / B[i][j]; // 保持正互反 const { weights } powerIteration(B); // 检查原本 i 优于 j 的关系是否反转 if ((baseWeights[i] - baseWeights[j]) * (weights[i] - weights[j]) 0) { flips.push({ pair: [i, j], delta }); } } } return flips; }这段代码对每一对元素做单点扰动delta 默认 10%。flips 为空说明排序稳健如果某个方案对频繁出现在 flips 里就说明它的排名是“踩线”的汇报时应当注明。注意每次扰动后都要重新归一化下三角否则矩阵不再是正互反幂法结果没有意义。3.3 群决策聚合多个判断矩阵怎么合并实际决策往往不止一个人填矩阵。聚合方式有两种先聚合矩阵再求权重AIJ或先各自求权重再加权平均AIP。AIJ 用几何平均更合理因为判断值是比率标度算术平均会破坏倒数关系。function geometricAggregate(matrices) { const n matrices[0].length; const G Array.from({ length: n }, () Array(n).fill(1)); for (let i 0; i n; i) { for (let j 0; j n; j) { let prod 1; for (const M of matrices) prod * M[i][j]; G[i][j] Math.pow(prod, 1 / matrices.length); // 几何平均 } } return G; }几何平均保证聚合后的矩阵仍是正互反的G[j][i] 自然等于 1/G[i][j]。如果改用算术平均这个性质就丢了后续一致性检验会失真。参数 matrices 是各决策者的判断矩阵数组要求维度一致。4. 避坑与排查AHP 工具里最容易翻车的几处4.1 CR 算出来是 NaN现象一致性比率显示 NaN权重全是 0 或无穷大。原因通常是矩阵里出现了 0 或负数幂法归一化时除零。解决在 buildMatrix 入口校验每个标度值必须大于 0且落在 1/9 到 9 之间越界直接拒绝而不是静默修正。4.2 二阶矩阵报除零现象n2 时 CR 计算抛异常。原因CI 公式分母 n-1 为 1 没问题但 RI 表中 n2 对应 0CR CI/0。解决n 3 时直接返回 CR0跳过除法并在界面上说明二阶无需一致性检验。4.3 改了上三角下三角没同步现象用户手动编辑后a_ij 和 a_ji 不再互为倒数权重结果诡异。原因数据结构允许独立写入下三角。解决所有写入走统一入口下三角永远由 1/a_ij 推导禁止直接赋值。这是血泪经验早期版本就栽在这。4.4 幂法不收敛但没报错现象迭代跑满 maxIter权重看起来“差不多”但 CR 明显异常。原因收敛判据太松或矩阵接近不可约边界。解决迭代结束后检查 diff 是否真的小于 tol不满足就抛出警告提示用户检查矩阵是否存在极端值如 1/9 和 9 混用过多。4.5 群决策聚合后一致性反而变差现象每个人单独 CR 都合格几何平均后 CR 超标。原因不同决策者的判断方向冲突聚合放大了矛盾。解决聚合前先看各人权重向量的排序是否一致排序分歧大时不应强行聚合而应回到讨论环节。工具可以输出各人排序对比表作为提示。5. 设计取舍为什么用 JavaScript 做 AHP以及边界在哪把 AHP 做成 JavaScript 工具最直接的动机是“在浏览器里跑完整个流程”。判断矩阵的规模通常很小n 很少超过 10幂法迭代的计算量对现代 JS 引擎来说微不足道所以性能不是瓶颈交互和可复现才是。用 JS 意味着矩阵构造、实时 CR 反馈、敏感性扫描都能在客户端完成不需要把决策数据传到服务端这对涉及内部评审的场景是个实际优势。但取舍也很明显。JS 的浮点数精度在 n 较大、标度极端时会让幂法收敛变慢甚至出现权重微小负值理论上不可能数值误差导致。常见的处理思路是在归一化后做一次截断把小于 1e-12 的分量置零再重新归一化而不是放任负值进入后续计算。另一个边界是 RI 表只覆盖到 n9超过 9 个元素时 AHP 本身的一致性判断就不可靠了工具应当明确拒绝而不是硬算。验证一个 AHP 实现是否可信我习惯用两个固定用例完全一致的矩阵如所有权重按 2:1:1 构造应当得到 CR≈0 且权重精确随机矩阵应当得到 CR 明显大于 0.1。这两个用例跑通基本能排除大部分实现错误。从那以后我每次接入新的 AHP 库都强制先跑这两组对照再谈业务逻辑。希望帮到你。本文还有配套的精品资源点击获取