跳转至

D1 · AHP 层次分析法 + 熵权法

今天的目标:不是"看懂"这两个方法,而是能独立构造一个判断矩阵/数据表,手推出权重,用代码验证,并写出一段经得起追问的论文文字。

1. AHP 到底在解决什么问题

当你面对多个评价指标(比如"成本""时间""舒适度"),但它们量纲完全不同、没法直接相加时,AHP 让你通过两两比较的方式,把"我觉得成本比舒适度重要得多"这种主观判断,转换成可以直接乘进模型的数字权重。它的价值不在数学多复杂,而在于把说不清楚的"重要性"量化成了可追溯的过程——这也是为什么评委爱看:你不是拍脑袋给的权重,是有一套流程支撑的。

2. 五步流程

  1. 建层次结构:目标层 → 准则层(指标)→ 方案层。指标不要超过 7±2 个,太多两两比较会自相矛盾。
  2. 构造判断矩阵:对每一对指标,按 Saaty 1-9 标度打分(下表),aji = 1/aij
  3. 计算权重:三种常见算法(选一种即可,特征值法最标准):
    • 算术平均法:每列归一化后按行取平均 —— 手算最快,写论文可以说"简化处理"
    • 几何平均法:每行元素连乘后开 n 次方再归一化 —— 对不一致矩阵更稳健
    • 特征值法:取矩阵最大特征值对应的特征向量并归一化 —— 教材标准做法,代码里默认用这个
  4. 一致性检验:CI = (λmax − n)/(n − 1),CR = CI / RI,CR < 0.1 才算通过。
  5. 不通过怎么办:不是重新瞎猜,是回头检查哪一对比较和其他比较逻辑矛盾(比如你说 A 比 B 重要,B 比 C 重要,却说 C 比 A 重要),调整后重新算。
标度 含义
1 两者同等重要
3 前者比后者稍微重要
5 前者比后者明显重要
7 前者比后者强烈重要
9 前者比后者极端重要
2,4,6,8 相邻判断的中间值

关键概念

RI(随机一致性指标)是查表值,只和指标个数 n 有关,不用自己算:n=3 时 RI=0.58,n=4 时 0.90,n=5 时 1.12(完整表在代码里)。CR 本质是在问:"如果我是瞎填的,一致性会有多差?你实际填的比瞎填好多少?"

3. 例题精算(手推,不跳步)

场景:从成本、时间、舒适度三个指标比较三种出行方式的重要性,判断矩阵如下:

例子

        成本    时间    舒适度
成本     1      3       5
时间    1/3     1       3
舒适度  1/5    1/3      1
  1. 列求和:成本列 1+1/3+1/5 = 1.5333;时间列 3+1+1/3 = 4.3333;舒适度列 5+3+1 = 9
  2. 每列归一化后按行平均,得权重:成本 w₁=0.633,时间 w₂=0.261,舒适度 w₃=0.106(三者相加=1)
  3. 用权重反乘回矩阵估算 λmax:算出三行的 (Aw)ᵢ/wᵢ 分别约为 3.071 / 3.033 / 3.012,取平均 λmax ≈ 3.039
  4. CI = (3.039 − 3) / (3 − 1) = 0.0194
  5. CR = CI / RI = 0.0194 / 0.58 = 0.033 —— 小于 0.1,通过一致性检验,权重可用

易错

很多人算完权重就直接用了,压根没做一致性检验——CR 没过关意味着你打的分自相矛盾,权重是不可信的,评委一眼能看出你根本没做这一步。

4. 代码逐行拆解

ahp_weight.py

import numpy as np

def ahp_weight(A):
    n = A.shape[0]
    eigvals, eigvecs = np.linalg.eig(A)      # 求全部特征值/特征向量
    idx = np.argmax(eigvals.real)            # 取最大特征值的位置
    w = eigvecs[:, idx].real                 # 对应的特征向量就是权重(未归一)
    w = w / w.sum()                          # 归一化,使权重和为1
    lam_max = eigvals[idx].real
    CI = (lam_max - n) / (n - 1)
    RI = {1:0,2:0,3:0.58,4:0.9,5:1.12,6:1.24,7:1.32,8:1.41,9:1.45}[n]
    CR = CI / RI if RI else 0
    return w, CR

注意np.linalg.eig 返回的可能是复数(虚部通常是 0),所以要 .real 取实部;eigvecs[:, idx] 里正负号可能反过来,如果权重全是负数,整体取相反数即可。

5. 熵权法:让数据自己说话

AHP 靠人的主观判断;熵权法反过来,看一个指标在各方案之间差异有多大:差异越大,说明这个指标越能区分方案优劣,权重就该越高;如果所有方案在某个指标上几乎一样,这个指标就是"废话",权重趋近于 0。

易错

所有指标要先正向化(成本型指标,比如"价格",值越小越好,要先转换成"越大越好":常用 max−x 或取倒数),再归一化,才能算熵值——漏了这一步,算出来的权重会是反的。

6. 例题精算:为什么"没有差异的指标权重为0"

3 个方案 A/B/C,2 个已经正向化的指标:

例子

        指标1(有差异)   指标2(无差异)
方案A       4              7
方案B       6              7
方案C       8              7
  1. 归一化(每列除以列和):指标1列和=18 → P=(0.222, 0.333, 0.444);指标2列和=21 → P=(0.333, 0.333, 0.333)
  2. 常数 k = 1/ln(3) = 0.9102
  3. 指标1 熵值 e₁ = −k·Σ(P·lnP) = 0.9657;指标2 熵值 e₂ = 1.0000(完全无差异时熵恒为1,这是理论最大值)
  4. 差异系数 d = 1 − e:d₁ = 0.0343,d₂ = 0
  5. 归一化得权重:w₁ = 1.000,w₂ = 0.000

关键概念

这个例子是刻意设计的极端案例:指标2 在三个方案里完全相同,携带的信息量为零,熵权法直接把它的权重判为 0——这就是"熵"在直觉上的意思:越"意外"(越有差异)的信息,权重越高。

7. 代码逐行拆解

entropy_weight.py

import numpy as np

def entropy_weight(X):
    # X: n个方案 x m个指标,必须已经正向化 + 归一化到同一量纲
    X = X + 1e-12                     # 防止 log(0)
    P = X / X.sum(axis=0)             # 每列(每个指标)内部占比
    k = 1 / np.log(len(X))            # len(X)=方案数 n
    e = -k * (P * np.log(P)).sum(axis=0)   # 每个指标的熵值
    d = 1 - e                         # 差异系数:熵越低,差异系数越高
    return d / d.sum()                # 归一化为权重

+1e-12 是工程技巧:如果某方案在某指标上取值为 0,log(0) 会报错/得到 -inf,加一个极小值规避,不影响结果精度。

8. 进阶:组合赋权

实战中很少单用一种赋权法——AHP 有主观性,熵权法完全依赖数据、可能和常识相悖(比如碰巧本次样本里一个重要指标恰好差异很小)。常见做法是组合权重

例子

w_combined = α · w_AHP + (1-α) · w_entropy

α 取 0.5 最简单(写论文可以说"等权重组合,兼顾主客观信息");更严谨的做法是用博弱组合赋权(求一组线性组合系数使组合权重与两组权重的离差平方和最小),属于加分项,时间够再做。

9. 写作表达

论文里"确定权重"这段的标准结构:为什么需要权重 → 用什么方法 → 关键结果(带数字)→ 结果为何合理。用今天例题的真实数字填一遍:

论文里可以这么写

由于成本、时间、舒适度三项指标难以直接量化比较,本文采用层次分析法确定各指标权重。通过构造两两比较判断矩阵,并计算一致性比率 CR = 0.033 < 0.1,通过一致性检验,得到权重向量为 (0.633, 0.261, 0.106),其中成本权重最高,符合出行决策中成本因素通常被优先考量的实际情况。

把方括号内容换成你自己任务的真实数字和场景描述,其余句式基本可以照搬。

10. 今日任务清单

  • [ ]

任务1:跑通 AHP

用本文的判断矩阵跑 ahp_weight,核对你的 CR 是否也在 0.03 附近(允许小数点后差异,特征值法和算术平均法结果会略有不同)

  • [ ]

任务2:自己编一组熵权法数据

4 个方案 × 3 个指标,其中至少 1 个是成本型(记得先正向化),跑 entropy_weight,检查哪个指标权重最高,想一想是否符合你对"差异大小"的直觉

  • [ ]

任务3:写一段权重确定的论文文字

套用第9节模板,用任务2的真实数字重写一遍

自查

自查:这4个问题答不上来,别进 D2

  • CR 是用来检验什么的?如果 CR = 0.15 你会怎么处理?
  • 熵权法为什么要求指标先"正向化"?不做会怎样?
  • 如果某个指标在所有方案里数值都相同,熵权法算出的权重会是多少?为什么?
  • AHP 和熵权法分别适合什么场景?能不能各举一个国赛题目里可能出现的例子?
参考答案

参考答案

  1. CR 检验判断矩阵的一致性,即你的两两打分之间有没有逻辑矛盾(说 A>B、B>C,却又说 C>A)。CR = 0.15 > 0.1 不通过,需要修正矩阵。 定位问题元素的方法:完全一致时 aij = wi/wj,因此计算 a_ij · w_j / w_i − 1,绝对值最大的那个元素就是矛盾来源,把它往 wi/wj 拉近后重算。注意同步修改倒数位置 aji = 1/aij,否则矩阵不再是互反矩阵,后续计算全部失效。

  2. 正向化服务于综合评价的打分逻辑,不是为了修正权重方向。 熵权法量的是离散程度:一个成本型指标只要方案间差异大,无论正不正向化,算出的权重都高——所以指标权重的排序并不会翻。真正翻掉的是方案排序,因为加权求和默认"每列数值越大越好"。 用一组数据看后果(价格成本型,评分效益型):

            价格(万元)   评分
    方案A      10        8
    方案B      50        7
    方案C      90        6
    

    指标权重几乎不受影响:不正向化 (0.973, 0.027),正向化后 (0.985, 0.015),价格始终占主导。但综合得分完全不同:

    方案 不正向化 正向化后
    A(价10 / 评8) 0.075 0.663 ①
    B(价50 / 评7) 0.333 0.333 ②
    C(价90 / 评6) 0.591 ① 0.004 ③

    不正向化时最贵、评分最低的 C 排第一,因为 90 万的价格被当成了优点。 补充:正向化确实会让权重数值变化(0.973→0.985),因为 max−x 改变了该列的占比结构 P,但这是副作用而非目的。

  3. 权重为 0。该指标各方案取值相同 → 归一化后 P 每项都是 1/n → 熵值 ej 达到理论最大值 1 → 差异系数 dj = 1 − ej = 0 → 权重为 0。含义是:这个指标对区分方案毫无贡献,信息量为零。

  4. AHP 熵权法
    前提 不需要数据,只要有领域判断 必须有多方案实测数据
    国赛例子 "评价一项政策的社会效益",指标是安全性/公平性/可持续性——根本没有客观数据,只能靠专家两两比较 "给 30 个城市的宜居程度排名",已给出 GDP/绿化率/房价等实测表格,用数据定权更有说服力
    失效模式 指标超过 7~9 个时人的判断必然自相矛盾,CR 难过关 样本依赖:公认重要的指标若碰巧在本次样本里差异很小,权重被压到接近 0,结论违背常识

    熵权法的失效模式无法靠它自己修复,必须引入主观信息——这就是组合赋权存在的真正理由。论文里写 w = α·w_AHP + (1−α)·w_entropy 时,理由应写成"避免客观赋权受样本分布影响导致关键指标失权",而不是"兼顾主客观"这种套话。

11. 把正向化写进函数签名

正向化是最容易漏的一步,别靠记性。把它做成熵权法函数的必填参数,漏写直接报错而不是静默出错:

entropy_weight_safe.py

import numpy as np

def positivize(X, kinds):
    """kinds: 每列一个, 'max'=效益型(越大越好), 'min'=成本型(越小越好)"""
    X = np.asarray(X, dtype=float).copy()
    for j, kind in enumerate(kinds):
        if kind == 'min':
            X[:, j] = X[:, j].max() - X[:, j]
    return X

def entropy_weight(X, kinds):
    X = positivize(X, kinds)        # 从签名上就不可能"忘记"
    X = X + 1e-12
    P = X / X.sum(axis=0)
    k = 1 / np.log(len(X))
    e = -k * (P * np.log(P)).sum(axis=0)
    d = 1 - e
    return d / d.sum()

# w = entropy_weight(data, kinds=['min', 'max', 'max'])

设计意图:把易错步骤变成函数签名的必填参数,比在注释里写"记得正向化"可靠得多。后面每个模板都会沿用这个思路。