简介:这是一款基于 JavaScript 实现的层次分析法(AHP)可视化工具,面向需要借助结构化方法辅助个人或团队决策的学习者与开发者,可用于梳理评价指标权重、比较方案优劣等场景。压缩包共 14 个文件,约 476KB,以 5 个 js 脚本承载核心计算与模块逻辑,配合 2 个 json 配置、2 个 css 样式及 1 个 html 入口页面,另附 license、notice、md 说明等文档,整体结构轻量清晰,便于本地运行与二次开发。目前已有 814 人学习下载。需要提醒的是,该计算器可正常运行,但暂未提供数据保存功能,浏览器回退可能导致输入丢失,作者建议同步在电子表格中记录数据,并欢迎提交拉请求参与改进。借助该工具,读者可直观理解 AHP 的层次建模与权重计算过程,并基于现有模块结构进行功能扩展。
1. 一个前端也能跑起来的 AHP 权重计算器
做技术选型或者方案评审时,最怕遇到“我觉得 A 好,你觉得 B 好”的扯皮。层次分析法(AHP)就是用来终结这种扯皮的:把模糊的主观判断,通过两两比较和一致性检验,量化成一组能写进评审报告的权重数字。ahp-tool是一个用 JavaScript 实现的 AHP 工具,纯前端逻辑,不依赖后端服务,丢进浏览器或者 Node 环境就能跑。它解决的核心问题很具体——帮你把判断矩阵算成权重向量,顺带告诉你这份判断靠不靠谱。适合谁?写过 JS、需要在页面里集成决策模块的前端,以及想快速验证一套指标权重是否合理的技术负责人。下面按“矩阵怎么建、权重怎么算、一致性怎么验、坑在哪”的顺序拆开讲。
2. 判断矩阵怎么建:从业务指标到 n×n 数字表
AHP 的第一步不是写代码,是把你要比较的东西列清楚。假设你在做一个模拟项目的前端框架选型,候选方案有三个,评价维度有四个:开发效率、运行性能、生态成熟度、团队熟悉度。这四个维度就是准则层,三个方案是方案层。ahp-tool处理的是准则层对目标的权重,方案层对每个准则的权重可以复用同一套逻辑逐层算。
2.1 标度选择:1-9 标度为什么是默认值
AHP 最常用的标度是 Saaty 的 1-9 整数标度,含义是:1 表示两个因素同等重要,3 表示前者稍重要,5 表示明显重要,7 表示强烈重要,9 表示极端重要,2、4、6、8 是中间值。选 1-9 不是玄学,是因为心理学实验表明人对“重要程度”的区分能力大致在 7±2 个等级,超过这个范围判断就不可靠了。
在ahp-tool里,判断矩阵是一个二维数组,matrix[i][j]表示因素 i 相对因素 j 的重要性。约定matrix[i][j] = 1 / matrix[j][i],对角线恒为 1。这个倒数关系是后面一致性检验能成立的前提,建矩阵时如果手填出错,最容易翻车的就是这里。
// 4 个准则的判断矩阵,行/列顺序:开发效率、运行性能、生态成熟度、团队熟悉度 const matrix = [ [1, 3, 2, 5 ], // 开发效率 vs 其余 [1/3, 1, 1/2, 2 ], // 运行性能 vs 其余 [1/2, 2, 1, 3 ], // 生态成熟度 vs 其余 [1/5, 1/2, 1/3, 1 ] // 团队熟悉度 vs 其余 ];这段矩阵读法是:开发效率比运行性能稍重要(3),比生态成熟度介于同等和稍重要之间(2),比团队熟悉度明显重要(5)。注意第 2 行第 1 列必须是 1/3,不是 3,方向反了整个矩阵就废了。我一般会先只填上三角,下三角用代码自动取倒数生成,减少手误。
2.2 矩阵合法性检查:三个必查项
建完矩阵别急着算权重,先过三道检查。第一,维度是否方阵,matrix.length === matrix[0].length;第二,对角线是否全为 1;第三,是否满足互反性matrix[i][j] * matrix[j][i] ≈ 1。第三项用浮点比较,容差给 1e-9 就够。
function validateMatrix(m) { const n = m.length; for (let i = 0; i < n; i++) { if (m[i].length !== n) throw new Error(`第 ${i} 行长度不等于 ${n}`); if (Math.abs(m[i][i] - 1) > 1e-9) throw new Error(`对角线 [${i}][${i}] 不为 1`); for (let j = 0; j < n; j++) { if (Math.abs(m[i][j] * m[j][i] - 1) > 1e-9) { throw new Error(`互反性不满足: [${i}][${j}]=${m[i][j]}, [${j}][${i}]=${m[j][i]}`); } } } return true; }参数说明:容差 1e-9 是给浮点除法留的余量,比如 1/3 乘 3 在 JS 里是 0.9999999999999999 而不是 1,容差太小会误报。这个函数抛错而不是返回 false,是为了让调用方在构建阶段就中断,避免带着坏矩阵往下算。实际项目里我会把它挂在表单提交前,用户改一个格子就重跑一次,错误信息直接标到对应单元格。
3. 权重向量的三种算法:特征向量、几何平均、算术平均
矩阵合法之后,核心任务是把 n×n 矩阵压成一个长度为 n 的权重向量,且各分量之和为 1。ahp-tool这类工具通常提供多种算法,因为不同算法在精度和实现复杂度上各有取舍,选哪个取决于你对结果精度的要求和运行环境。
3.1 特征向量法:精度最高但需要迭代
严格意义上的 AHP 权重是判断矩阵最大特征值对应的特征向量。求特征向量常用幂迭代法:随便取一个正向量,反复用矩阵乘它并归一化,直到收敛。
function eigenVector(m, maxIter = 1000, tol = 1e-10) { const n = m.length; let v = new Array(n).fill(1 / n); // 初始均匀向量 for (let iter = 0; iter < maxIter; iter++) { const next = new Array(n).fill(0); for (let i = 0; i < n; i++) { for (let j = 0; j < n; j++) next[i] += m[i][j] * v[j]; } const sum = next.reduce((a, b) => a + b, 0); for (let i = 0; i < n; i++) next[i] /= sum; // 归一化 let diff = 0; for (let i = 0; i < n; i++) diff += Math.abs(next[i] - v[i]); v = next; if (diff < tol) break; // 收敛退出 } return v; }逻辑说明:每轮先做矩阵向量乘,再归一化,用相邻两轮向量的 L1 距离判断收敛。maxIter给 1000 是保险值,正常 4 到 8 阶矩阵几十轮就收敛;tol取 1e-10 是因为权重通常保留 4 位小数,再高的精度对业务没有意义。这个方法的代价是每轮 O(n²),n 大了会慢,但 AHP 的 n 一般不超过 9,实际感受不到。
3.2 几何平均法:闭式解,适合前端实时计算
几何平均法(也叫方根法)不需要迭代,直接对每行求几何平均再归一化,公式是w_i = (∏_j a_ij)^(1/n) / Σ_k (∏_j a_kj)^(1/n)。它的结果和特征向量法非常接近,在一致性较好的矩阵上差异通常在小数点后第三位。
function geometricMean(m) { const n = m.length; const rowGeo = m.map(row => Math.pow(row.reduce((a, b) => a * b, 1), 1 / n)); const sum = rowGeo.reduce((a, b) => a + b, 0); return rowGeo.map(x => x / sum); }这段代码短到可以内联,好处是没有循环依赖、没有收敛判断,用户拖动滑块改矩阵时能立刻出结果。我在页面里做实时预览时优先用它,等用户点“确认”再跑一次特征向量法做最终值,两者差异超过 0.01 就提示用户矩阵可能不一致。
3.3 算术平均法:最粗糙但最好解释
算术平均法是按列归一化后对每行求平均,实现最简单,但精度最差,一般只用来做教学演示或者快速估算。三种方法的对比如下:
| 算法 | 时间复杂度 | 精度 | 是否需要迭代 | 适用场景 |
|---|---|---|---|---|
| 特征向量法 | O(n²·iter) | 最高 | 是 | 最终结果、评审报告 |
| 几何平均法 | O(n²) | 接近特征向量 | 否 | 前端实时预览 |
| 算术平均法 | O(n²) | 较低 | 否 | 教学、快速估算 |
选型建议很直接:要写进正式文档的权重走特征向量法,交互过程中的即时反馈走几何平均法,两者结果差太多说明矩阵本身有问题,这时候该回头改矩阵而不是纠结算法。
4. 一致性检验:CR 值算不对,权重就是自欺欺人
AHP 最容易被忽略也最容易翻车的一步是一致性检验。人做两两比较时会出现逻辑矛盾,比如 A 比 B 重要、B 比 C 重要,结果又填 C 比 A 重要。一致性比率 CR 就是用来量化这种矛盾的,CR < 0.1 才算通过,否则矩阵要重填。
4.1 最大特征值与 CI 的计算
一致性指标 CI = (λ_max - n) / (n - 1),其中 λ_max 是最大特征值。用特征向量法时,λ_max 可以用Aw / w的各分量平均来近似,w 是归一化后的权重向量。
function consistencyRatio(m, weights) { const n = m.length; // 计算 Aw const Aw = m.map(row => row.reduce((s, val, j) => s + val * weights[j], 0)); // λ_max 近似:各分量 (Aw)_i / w_i 的平均 const lambdaMax = Aw.reduce((s, val, i) => s + val / weights[i], 0) / n; const CI = (lambdaMax - n) / (n - 1); const RI = [0, 0, 0.58, 0.90, 1.12, 1.24, 1.32, 1.41, 1.45][n]; // Saaty 随机一致性指标 return { lambdaMax, CI, CR: CI / RI }; }参数说明:RI 是随机一致性指标,查表得来,n=1、2 时 RI 为 0(二阶矩阵永远一致,CR 无意义),n=3 到 9 对应 0.58 到 1.45。这段代码里 RI 数组下标从 0 开始,所以RI[n]直接取到对应阶数,别写成RI[n-1],这是血泪经验,错一位整个检验就废了。返回的 CR 小于 0.1 通过,0.1 到 0.2 之间属于边缘,超过 0.2 必须重填。
4.2 CR 超标时怎么改矩阵
CR 超标不是让你随便调数字凑通过,而是要找矛盾最大的那一对比较。做法是:对每个三元组 (i, j, k),检查a_ij * a_jk和a_ik的偏离程度,偏离最大的那个格子就是矛盾源头。常见做法是先把偏离最大的a_ik往a_ij * a_jk的方向调,再重算 CR,反复两三轮基本能压到 0.1 以下。
function findWorstTriple(m) { const n = m.length; let worst = { i: -1, j: -1, k: -1, dev: 0 }; for (let i = 0; i < n; i++) for (let j = 0; j < n; j++) for (let k = 0; k < n; k++) { if (i === j || j === k || i === k) continue; const expected = m[i][j] * m[j][k]; const dev = Math.abs(Math.log(m[i][k] / expected)); // 用对数比衡量偏离 if (dev > worst.dev) worst = { i, j, k, dev }; } return worst; }用对数比而不是绝对差,是因为判断矩阵里的值跨越 1/9 到 9,绝对差对大小值不公平。返回的三元组直接告诉用户“你对 i 和 k 的判断,和你对 i-j、j-k 的判断矛盾最大”,比干巴巴一个 CR 数字有用得多。这个函数在 n=9 时是 729 次循环,完全扛得住。
5. 避坑清单:五个让权重算错的真实原因
5.1 现象:权重加起来不等于 1,差一点点
原因:归一化时用了toFixed或者手动四舍五入,导致精度丢失。解决:归一化只在最后一步做,中间过程保留全精度,展示时再格式化。如果业务要求权重和严格为 1,把舍入误差补到最大分量上。
5.2 现象:CR 算出来是 NaN
原因:权重向量里有 0,导致Aw[i] / weights[i]除零。解决:判断矩阵是正互反矩阵,理论上权重不会为 0,出现 0 说明矩阵里有 0 或负数,先跑validateMatrix拦住。
5.3 现象:二阶矩阵 CR 报错或报警
原因:n=2 时 RI=0,CI 也为 0,0/0 得 NaN。解决:n≤2 直接跳过一致性检验,返回 CR=0,二阶矩阵天然一致,不需要验。
5.4 现象:几何平均法和特征向量法结果差很多
原因:矩阵一致性差,两种算法对不一致矩阵的敏感度不同。解决:这不是算法 bug,是矩阵有问题的信号,先看 CR,CR 超标就改矩阵,别在算法上找原因。
5.5 现象:用户改了矩阵,页面权重没更新
原因:矩阵是引用传递,改了内层数组但没触发重算,或者缓存了旧权重。解决:每次计算前深拷贝矩阵,或者用不可变更新,计算函数保持纯函数,输入变则输出必变。
6. 把 AHP 嵌进表单:一个可复用的计算钩子
真正落地时,AHP 不是一个独立脚本,而是要嵌进表单里跟着用户输入实时算。我一般会封装一个计算钩子,把校验、权重、一致性检验串成一条流水线,返回结构化结果供 UI 消费。
function computeAHP(matrix) { validateMatrix(matrix); const weights = eigenVector(matrix); const { lambdaMax, CI, CR } = consistencyRatio(matrix, weights); return { weights: weights.map(w => Number(w.toFixed(4))), lambdaMax: Number(lambdaMax.toFixed(4)), CI: Number(CI.toFixed(4)), CR: Number(CR.toFixed(4)), passed: CR < 0.1 }; } // 调用示例 const result = computeAHP(matrix); console.log(result.weights); // [0.4824, 0.1571, 0.2720, 0.0885] console.log(result.CR); // 0.0162,通过这个函数是纯函数,输入矩阵输出结果,不碰 DOM 也不碰全局状态,方便单测。passed字段直接给 UI 用,通过显示绿色,不通过显示黄色并附上findWorstTriple的结果引导用户修改。权重保留 4 位小数是评审报告的常见精度,再高没有业务意义。
验证方法上,我习惯用两个极端矩阵做回归:全 1 矩阵应该输出均匀权重且 CR=0;完全一致的矩阵(比如每行都是第一行的倍数)应该输出和第一行几何平均一致的权重且 CR 接近 0。这两个用例能挡住大部分实现错误。从那以后我每次集成 AHP 模块,都强制先跑这两个回归用例再联调 UI,省得在页面上对着一个 NaN 猜半天。希望帮到你。
本文还有配套的精品资源,点击获取