219232de74
以讲义内容为骨架迁移到标准目录格式: - officefile/ 主内容(12章 + 附录 + CC4SI补充) - dofile/ 代码示例(11个Python脚本) - data/ 图片资源 - output/ 生成输出(忽略) - Archive/ 归档旧目录(忽略) - .claude/skills/ 保留markdown-to-docx工具链 - .pandoc/ 保留CSL和本地化配置 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1121 lines
35 KiB
Python
1121 lines
35 KiB
Python
"""
|
|
多准则决策示例 (Multi-Criteria Decision Making Example)
|
|
======================================================
|
|
|
|
本示例展示空间智能系统中的多准则决策方法。
|
|
MCDA/MCDM 用于处理多个冲突准则下的决策问题。
|
|
|
|
核心概念:
|
|
1. 准则体系 - 构建评价准则层次结构
|
|
2. 权重确定 - AHP、熵权法等
|
|
3. 决策矩阵 - 标准化与规范化
|
|
4. 综合评价 - WSM、WPM、TOPSIS等
|
|
5. 灵敏度分析 - 权重变化对结果的影响
|
|
|
|
应用场景:
|
|
- 选址决策
|
|
- 项目评估
|
|
- 资源配置
|
|
- 风险评估
|
|
|
|
作者: CC4SI 项目组
|
|
"""
|
|
|
|
import math
|
|
import json
|
|
from typing import List, Dict, Tuple, Optional, Any, Callable
|
|
from dataclasses import dataclass, field
|
|
from enum import Enum
|
|
import random
|
|
|
|
|
|
# ============================================================================
|
|
# 准则类型与方向
|
|
# ============================================================================
|
|
|
|
class CriterionType(Enum):
|
|
"""准则类型"""
|
|
BENEFIT = "benefit" # 效益型 (越大越好)
|
|
COST = "cost" # 成本型 (越小越好)
|
|
NON_MONOTONIC = "non_monotonic" # 非单调 (有最优值)
|
|
|
|
|
|
@dataclass
|
|
class Criterion:
|
|
"""
|
|
决策准则
|
|
|
|
定义评价的维度。
|
|
"""
|
|
name: str
|
|
criterion_type: CriterionType
|
|
weight: float = 1.0
|
|
scale: Tuple[float, float] = (0, 1) # 取值范围
|
|
optimal_value: Optional[float] = None # 最优值 (用于非单调型)
|
|
|
|
def __repr__(self) -> str:
|
|
return f"Criterion({self.name}, {self.criterion_type.value}, w={self.weight:.2f})"
|
|
|
|
|
|
# ============================================================================
|
|
# 决策方案
|
|
# ============================================================================
|
|
|
|
@dataclass
|
|
class Alternative:
|
|
"""
|
|
决策方案
|
|
|
|
表示一个待评估的备选方案。
|
|
"""
|
|
id: str
|
|
name: str
|
|
values: Dict[str, float] # 准则名称到值的映射
|
|
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
|
|
def get_value(self, criterion_name: str) -> Optional[float]:
|
|
"""获取准则值"""
|
|
return self.values.get(criterion_name)
|
|
|
|
def set_value(self, criterion_name: str, value: float) -> None:
|
|
"""设置准则值"""
|
|
self.values[criterion_name] = value
|
|
|
|
def __repr__(self) -> str:
|
|
return f"Alternative({self.name}, values={len(self.values)})"
|
|
|
|
|
|
# ============================================================================
|
|
# 标准化方法
|
|
# ============================================================================
|
|
|
|
class NormalizationMethod(Enum):
|
|
"""标准化方法"""
|
|
MIN_MAX = "min_max" # Min-Max标准化
|
|
VECTOR = "vector" # 向量标准化
|
|
Z_SCORE = "z_score" # Z-Score标准化
|
|
SUM = "sum" # 总和标准化
|
|
|
|
|
|
class Normalizer:
|
|
"""数据标准化器"""
|
|
|
|
@staticmethod
|
|
def min_max(values: List[float],
|
|
target_range: Tuple[float, float] = (0, 1),
|
|
criterion_type: CriterionType = CriterionType.BENEFIT) -> List[float]:
|
|
"""
|
|
Min-Max标准化
|
|
|
|
Args:
|
|
values: 原始值列表
|
|
target_range: 目标范围
|
|
criterion_type: 准则类型
|
|
|
|
Returns:
|
|
标准化后的值列表
|
|
"""
|
|
min_val = min(values)
|
|
max_val = max(values)
|
|
|
|
if max_val == min_val:
|
|
return [target_range[0] for _ in values]
|
|
|
|
t_min, t_max = target_range
|
|
result = []
|
|
|
|
for v in values:
|
|
if criterion_type == CriterionType.BENEFIT:
|
|
# 效益型: 越大越好
|
|
normalized = (v - min_val) / (max_val - min_val)
|
|
else: # COST
|
|
# 成本型: 越小越好
|
|
normalized = (max_val - v) / (max_val - min_val)
|
|
|
|
result.append(t_min + normalized * (t_max - t_min))
|
|
|
|
return result
|
|
|
|
@staticmethod
|
|
def vector(values: List[float],
|
|
criterion_type: CriterionType = CriterionType.BENEFIT) -> List[float]:
|
|
"""
|
|
向量标准化
|
|
|
|
Args:
|
|
values: 原始值列表
|
|
criterion_type: 准则类型
|
|
|
|
Returns:
|
|
标准化后的值列表
|
|
"""
|
|
sum_squares = sum(v * v for v in values)
|
|
if sum_squares == 0:
|
|
return [0.0 for _ in values]
|
|
|
|
norm = math.sqrt(sum_squares)
|
|
result = [v / norm for v in values]
|
|
|
|
if criterion_type == CriterionType.COST:
|
|
# 成本型: 取倒数
|
|
result = [1.0 / (v + 1e-10) if v > 0 else 1.0 for v in result]
|
|
# 重新归一化
|
|
total = sum(result)
|
|
result = [v / total for v in result]
|
|
|
|
return result
|
|
|
|
@staticmethod
|
|
def z_score(values: List[float],
|
|
criterion_type: CriterionType = CriterionType.BENEFIT) -> List[float]:
|
|
"""
|
|
Z-Score标准化
|
|
|
|
Args:
|
|
values: 原始值列表
|
|
criterion_type: 准则类型
|
|
|
|
Returns:
|
|
标准化后的值列表
|
|
"""
|
|
import statistics
|
|
|
|
if len(values) < 2:
|
|
return [0.0 for _ in values]
|
|
|
|
mean = statistics.mean(values)
|
|
stdev = statistics.stdev(values)
|
|
|
|
if stdev == 0:
|
|
return [0.0 for _ in values]
|
|
|
|
result = [(v - mean) / stdev for v in values]
|
|
|
|
# 转换到正值范围
|
|
min_result = min(result)
|
|
if min_result < 0:
|
|
result = [v - min_result for v in result]
|
|
|
|
# 归一化到0-1
|
|
max_result = max(result)
|
|
if max_result > 0:
|
|
result = [v / max_result for v in result]
|
|
|
|
if criterion_type == CriterionType.COST:
|
|
result = [1.0 - v for v in result]
|
|
|
|
return result
|
|
|
|
|
|
# ============================================================================
|
|
# AHP层次分析法
|
|
# ============================================================================
|
|
|
|
class AHP:
|
|
"""
|
|
层次分析法 (Analytic Hierarchy Process)
|
|
|
|
用于确定准则权重的方法。
|
|
"""
|
|
|
|
# Saaty标度
|
|
SAATY_SCALE = {
|
|
1: "同等重要",
|
|
2: "稍微重要",
|
|
3: "明显重要",
|
|
4: "非常重要",
|
|
5: "极端重要"
|
|
}
|
|
|
|
def __init__(self, criteria: List[str]):
|
|
"""
|
|
初始化AHP
|
|
|
|
Args:
|
|
criteria: 准则名称列表
|
|
"""
|
|
self.criteria = criteria
|
|
self.n = len(criteria)
|
|
self.comparison_matrix: List[List[float]] = []
|
|
|
|
def build_comparison_matrix(self, comparisons: Dict[Tuple[str, str], float]) -> None:
|
|
"""
|
|
构建比较矩阵
|
|
|
|
Args:
|
|
comparisons: 准则对比较值字典
|
|
((criterion_i, criterion_j), value)
|
|
value > 1 表示 i 比 j 重要
|
|
value < 1 表示 j 比 i 重要
|
|
"""
|
|
# 初始化单位矩阵
|
|
self.comparison_matrix = [[1.0 for _ in range(self.n)] for _ in range(self.n)]
|
|
|
|
# 填充比较矩阵
|
|
for (c1, c2), value in comparisons.items():
|
|
if c1 in self.criteria and c2 in self.criteria:
|
|
i = self.criteria.index(c1)
|
|
j = self.criteria.index(c2)
|
|
self.comparison_matrix[i][j] = value
|
|
self.comparison_matrix[j][i] = 1.0 / value
|
|
|
|
def calculate_weights(self) -> Tuple[List[float], float, float]:
|
|
"""
|
|
计算权重
|
|
|
|
Returns:
|
|
(权重列表, 一致性比率, 最大特征值)
|
|
"""
|
|
if not self.comparison_matrix:
|
|
return [1.0 / self.n] * self.n, 0.0, self.n
|
|
|
|
# 特征向量法 (幂法)
|
|
weights = self._eigenvector_method()
|
|
lambda_max = self._calculate_lambda_max(weights)
|
|
ci = (lambda_max - self.n) / (self.n - 1) if self.n > 1 else 0
|
|
ri = self._random_consistency_index(self.n)
|
|
cr = ci / ri if ri > 0 else 0
|
|
|
|
return weights, cr, lambda_max
|
|
|
|
def _eigenvector_method(self, max_iterations: int = 100,
|
|
tolerance: float = 1e-6) -> List[float]:
|
|
"""使用幂法计算特征向量"""
|
|
# 初始化权重向量
|
|
weights = [1.0 / self.n] * self.n
|
|
|
|
for _ in range(max_iterations):
|
|
# 矩阵向量乘法
|
|
new_weights = []
|
|
for i in range(self.n):
|
|
new_weights.append(
|
|
sum(self.comparison_matrix[i][j] * weights[j] for j in range(self.n))
|
|
)
|
|
|
|
# 归一化
|
|
total = sum(new_weights)
|
|
new_weights = [w / total for w in new_weights]
|
|
|
|
# 检查收敛
|
|
if max(abs(new_weights[i] - weights[i]) for i in range(self.n)) < tolerance:
|
|
break
|
|
|
|
weights = new_weights
|
|
|
|
return weights
|
|
|
|
def _calculate_lambda_max(self, weights: List[float]) -> float:
|
|
"""计算最大特征值"""
|
|
lambda_sum = 0.0
|
|
for i in range(self.n):
|
|
weighted_sum = sum(self.comparison_matrix[i][j] * weights[j] for j in range(self.n))
|
|
lambda_sum += weighted_sum / weights[i] if weights[i] > 0 else 0
|
|
|
|
return lambda_sum / self.n
|
|
|
|
def _random_consistency_index(self, n: int) -> float:
|
|
"""随机一致性指标RI"""
|
|
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
|
|
}
|
|
return ri_table.get(n, 1.49)
|
|
|
|
|
|
# ============================================================================
|
|
# 熵权法
|
|
# ============================================================================
|
|
|
|
class EntropyWeightMethod:
|
|
"""
|
|
熵权法
|
|
|
|
基于数据离散度的客观权重确定方法。
|
|
"""
|
|
|
|
@staticmethod
|
|
def calculate_weights(decision_matrix: List[List[float]],
|
|
criterion_types: List[CriterionType]) -> List[float]:
|
|
"""
|
|
计算熵权
|
|
|
|
Args:
|
|
decision_matrix: 决策矩阵 (方案 x 准则)
|
|
criterion_types: 各准则的类型
|
|
|
|
Returns:
|
|
权重列表
|
|
"""
|
|
n_alternatives = len(decision_matrix)
|
|
n_criteria = len(decision_matrix[0]) if decision_matrix else 0
|
|
|
|
if n_alternatives == 0 or n_criteria == 0:
|
|
return []
|
|
|
|
# 标准化
|
|
normalized_matrix = []
|
|
for j in range(n_criteria):
|
|
column = [decision_matrix[i][j] for i in range(n_alternatives)]
|
|
normalized = EntropyWeightMethod._normalize_column(
|
|
column, criterion_types[j]
|
|
)
|
|
normalized_matrix.append(normalized)
|
|
|
|
# 计算熵值
|
|
entropy_values = []
|
|
for j in range(n_criteria):
|
|
column = normalized_matrix[j]
|
|
# 转换为概率
|
|
total = sum(column)
|
|
if total == 0:
|
|
entropy_values.append(0)
|
|
continue
|
|
|
|
probabilities = [v / total for v in column]
|
|
# 计算熵
|
|
entropy = 0.0
|
|
k = 1 / math.log(n_alternatives) if n_alternatives > 1 else 0
|
|
for p in probabilities:
|
|
if p > 0:
|
|
entropy -= k * p * math.log(p)
|
|
|
|
entropy_values.append(entropy)
|
|
|
|
# 计算权重
|
|
diversity = [1 - e for e in entropy_values]
|
|
total_diversity = sum(diversity)
|
|
|
|
if total_diversity == 0:
|
|
return [1.0 / n_criteria] * n_criteria
|
|
|
|
weights = [d / total_diversity for d in diversity]
|
|
return weights
|
|
|
|
@staticmethod
|
|
def _normalize_column(column: List[float],
|
|
criterion_type: CriterionType) -> List[float]:
|
|
"""标准化列"""
|
|
min_val = min(column)
|
|
max_val = max(column)
|
|
|
|
if max_val == min_val:
|
|
return [1.0 for _ in column]
|
|
|
|
result = []
|
|
for v in column:
|
|
if criterion_type == CriterionType.BENEFIT:
|
|
normalized = (v - min_val) / (max_val - min_val)
|
|
else: # COST
|
|
normalized = (max_val - v) / (max_val - min_val)
|
|
result.append(normalized)
|
|
|
|
return result
|
|
|
|
|
|
# ============================================================================
|
|
# MCDA方法实现
|
|
# ============================================================================
|
|
|
|
class WeightedSumModel:
|
|
"""
|
|
加权求和模型 (WSM)
|
|
|
|
最简单的多准则决策方法。
|
|
"""
|
|
|
|
def __init__(self, criteria: List[Criterion]):
|
|
self.criteria = criteria
|
|
self.criterion_map = {c.name: c for c in criteria}
|
|
|
|
def evaluate(self, alternatives: List[Alternative]) -> List[Tuple[Alternative, float]]:
|
|
"""
|
|
评估方案
|
|
|
|
Args:
|
|
alternatives: 备选方案列表
|
|
|
|
Returns:
|
|
(方案, 得分) 列表,按得分降序排列
|
|
"""
|
|
results = []
|
|
|
|
for alt in alternatives:
|
|
score = 0.0
|
|
valid = True
|
|
|
|
for criterion in self.criteria:
|
|
value = alt.get_value(criterion.name)
|
|
if value is None:
|
|
valid = False
|
|
break
|
|
|
|
# 标准化
|
|
normalized = self._normalize_value(value, criterion)
|
|
score += criterion.weight * normalized
|
|
|
|
if valid:
|
|
results.append((alt, score))
|
|
|
|
results.sort(key=lambda x: x[1], reverse=True)
|
|
return results
|
|
|
|
def _normalize_value(self, value: float, criterion: Criterion) -> float:
|
|
"""标准化单个值"""
|
|
min_val, max_val = criterion.scale
|
|
|
|
if criterion.criterion_type == CriterionType.BENEFIT:
|
|
if max_val == min_val:
|
|
return 0.5
|
|
return (value - min_val) / (max_val - min_val)
|
|
else: # COST
|
|
if max_val == min_val:
|
|
return 0.5
|
|
return (max_val - value) / (max_val - min_val)
|
|
|
|
|
|
class TOPSIS:
|
|
"""
|
|
TOPSIS (逼近理想解排序法)
|
|
|
|
考虑方案与理想解的距离。
|
|
"""
|
|
|
|
def __init__(self, criteria: List[Criterion]):
|
|
self.criteria = criteria
|
|
self.criterion_map = {c.name: c for c in criteria}
|
|
|
|
def evaluate(self, alternatives: List[Alternative]) -> List[Tuple[Alternative, float]]:
|
|
"""
|
|
评估方案
|
|
|
|
Args:
|
|
alternatives: 备选方案列表
|
|
|
|
Returns:
|
|
(方案, 相对贴近度) 列表,按贴近度降序排列
|
|
"""
|
|
# 构建决策矩阵
|
|
matrix, criterion_names = self._build_matrix(alternatives)
|
|
|
|
if not matrix or not criterion_names:
|
|
return []
|
|
|
|
n_alternatives = len(matrix)
|
|
n_criteria = len(matrix[0])
|
|
|
|
# 向量标准化
|
|
normalized_matrix = self._normalize_matrix(matrix)
|
|
|
|
# 构建加权标准化矩阵
|
|
weights = [self.criterion_map[c].weight for c in criterion_names]
|
|
weighted_matrix = [
|
|
[normalized_matrix[i][j] * weights[j] for j in range(n_criteria)]
|
|
for i in range(n_alternatives)
|
|
]
|
|
|
|
# 确定理想解和负理想解
|
|
ideal_positive, ideal_negative = self._determine_ideals(
|
|
weighted_matrix, criterion_names
|
|
)
|
|
|
|
# 计算距离
|
|
distances_positive = self._calculate_distances(weighted_matrix, ideal_positive)
|
|
distances_negative = self._calculate_distances(weighted_matrix, ideal_negative)
|
|
|
|
# 计算相对贴近度
|
|
results = []
|
|
for i, alt in enumerate(alternatives):
|
|
d_pos = distances_positive[i]
|
|
d_neg = distances_negative[i]
|
|
|
|
if d_pos + d_neg == 0:
|
|
closeness = 0
|
|
else:
|
|
closeness = d_neg / (d_pos + d_neg)
|
|
|
|
results.append((alt, closeness))
|
|
|
|
results.sort(key=lambda x: x[1], reverse=True)
|
|
return results
|
|
|
|
def _build_matrix(self, alternatives: List[Alternative]) -> Tuple[List[List[float]], List[str]]:
|
|
"""构建决策矩阵"""
|
|
if not self.criteria:
|
|
return [], []
|
|
|
|
criterion_names = [c.name for c in self.criteria]
|
|
matrix = []
|
|
|
|
for alt in alternatives:
|
|
row = []
|
|
valid = True
|
|
for name in criterion_names:
|
|
value = alt.get_value(name)
|
|
if value is None:
|
|
valid = False
|
|
break
|
|
row.append(value)
|
|
|
|
if valid:
|
|
matrix.append(row)
|
|
|
|
return matrix, criterion_names
|
|
|
|
def _normalize_matrix(self, matrix: List[List[float]]) -> List[List[float]]:
|
|
"""向量标准化"""
|
|
if not matrix:
|
|
return []
|
|
|
|
n_criteria = len(matrix[0])
|
|
result = []
|
|
|
|
for j in range(n_criteria):
|
|
column = [matrix[i][j] for i in range(len(matrix))]
|
|
sum_squares = sum(v * v for v in column)
|
|
norm = math.sqrt(sum_squares) if sum_squares > 0 else 1
|
|
|
|
for i in range(len(matrix)):
|
|
if j == 0:
|
|
result.append([])
|
|
result[i].append(matrix[i][j] / norm)
|
|
|
|
return result
|
|
|
|
def _determine_ideals(self, matrix: List[List[float]],
|
|
criterion_names: List[str]) -> Tuple[List[float], List[float]]:
|
|
"""确定理想解和负理想解"""
|
|
n_criteria = len(matrix[0])
|
|
ideal_positive = []
|
|
ideal_negative = []
|
|
|
|
for j in range(n_criteria):
|
|
column = [matrix[i][j] for i in range(len(matrix))]
|
|
criterion = self.criterion_map[criterion_names[j]]
|
|
|
|
if criterion.criterion_type == CriterionType.BENEFIT:
|
|
ideal_positive.append(max(column))
|
|
ideal_negative.append(min(column))
|
|
else: # COST
|
|
ideal_positive.append(min(column))
|
|
ideal_negative.append(max(column))
|
|
|
|
return ideal_positive, ideal_negative
|
|
|
|
def _calculate_distances(self, matrix: List[List[float]],
|
|
ideal: List[float]) -> List[float]:
|
|
"""计算到理想解的距离"""
|
|
distances = []
|
|
|
|
for i in range(len(matrix)):
|
|
dist = math.sqrt(
|
|
sum((matrix[i][j] - ideal[j]) ** 2 for j in range(len(ideal)))
|
|
)
|
|
distances.append(dist)
|
|
|
|
return distances
|
|
|
|
|
|
class VIKOR:
|
|
"""
|
|
VIKOR (VIseKriterijumska Optimizacija I Kompromisno Resenje)
|
|
|
|
折衷排序方法,适用于准则间存在冲突的情况。
|
|
"""
|
|
|
|
def __init__(self, criteria: List[Criterion], v: float = 0.5):
|
|
"""
|
|
初始化VIKOR
|
|
|
|
Args:
|
|
criteria: 准则列表
|
|
v: 决策机制系数 (0-1)
|
|
v > 0.5: 按群体效益最大化
|
|
v < 0.5: 按个别遗憾最小化
|
|
v = 0.5: 折衷解
|
|
"""
|
|
self.criteria = criteria
|
|
self.criterion_map = {c.name: c for c in criteria}
|
|
self.v = v
|
|
|
|
def evaluate(self, alternatives: List[Alternative]) -> List[Tuple[Alternative, float]]:
|
|
"""评估方案"""
|
|
# 构建决策矩阵
|
|
matrix, criterion_names = self._build_matrix(alternatives)
|
|
|
|
if not matrix or not criterion_names:
|
|
return []
|
|
|
|
n_alternatives = len(matrix)
|
|
n_criteria = len(matrix[0])
|
|
|
|
# 标准化
|
|
normalized_matrix = self._normalize_matrix(matrix, criterion_names)
|
|
|
|
# 确定最优最劣值
|
|
f_best, f_worst = self._determine_best_worst(normalized_matrix, criterion_names)
|
|
|
|
# 计算S和R
|
|
weights = [self.criterion_map[c].weight for c in criterion_names]
|
|
|
|
S_values = [] # 群体效益
|
|
R_values = [] # 个别遗憾
|
|
|
|
for i in range(n_alternatives):
|
|
S = 0.0
|
|
R = 0.0
|
|
|
|
for j in range(n_criteria):
|
|
weight = weights[j]
|
|
value = normalized_matrix[i][j]
|
|
|
|
# 距离最优值的归一化距离
|
|
if f_best[j] == f_worst[j]:
|
|
distance = 0
|
|
else:
|
|
distance = (f_best[j] - value) / (f_best[j] - f_worst[j])
|
|
|
|
S += weight * distance
|
|
R = max(R, weight * distance)
|
|
|
|
S_values.append(S)
|
|
R_values.append(R)
|
|
|
|
# 计算Q值
|
|
S_min, S_max = min(S_values), max(S_values)
|
|
R_min, R_max = min(R_values), max(R_values)
|
|
|
|
Q_values = []
|
|
for i in range(n_alternatives):
|
|
if S_max == S_min:
|
|
s_term = 0
|
|
else:
|
|
s_term = (S_values[i] - S_min) / (S_max - S_min)
|
|
|
|
if R_max == R_min:
|
|
r_term = 0
|
|
else:
|
|
r_term = (R_values[i] - R_min) / (R_max - R_min)
|
|
|
|
Q = self.v * s_term + (1 - self.v) * r_term
|
|
Q_values.append(Q)
|
|
|
|
# 返回按Q值排序的结果
|
|
results = [(alternatives[i], Q_values[i]) for i in range(n_alternatives)]
|
|
results.sort(key=lambda x: x[1])
|
|
return results
|
|
|
|
def _build_matrix(self, alternatives: List[Alternative]) -> Tuple[List[List[float]], List[str]]:
|
|
"""构建决策矩阵"""
|
|
criterion_names = [c.name for c in self.criteria]
|
|
matrix = []
|
|
|
|
for alt in alternatives:
|
|
row = [alt.get_value(name) for name in criterion_names]
|
|
if None not in row:
|
|
matrix.append(row)
|
|
|
|
return matrix, criterion_names
|
|
|
|
def _normalize_matrix(self, matrix: List[List[float]],
|
|
criterion_names: List[str]) -> List[List[float]]:
|
|
"""标准化决策矩阵"""
|
|
result = []
|
|
n_criteria = len(matrix[0])
|
|
|
|
for j in range(n_criteria):
|
|
column = [matrix[i][j] for i in range(len(matrix))]
|
|
criterion = self.criterion_map[criterion_names[j]]
|
|
|
|
min_val = min(column)
|
|
max_val = max(column)
|
|
|
|
for i in range(len(matrix)):
|
|
if j == 0:
|
|
result.append([])
|
|
|
|
if max_val == min_val:
|
|
result[i].append(1.0)
|
|
elif criterion.criterion_type == CriterionType.BENEFIT:
|
|
result[i].append((matrix[i][j] - min_val) / (max_val - min_val))
|
|
else:
|
|
result[i].append((max_val - matrix[i][j]) / (max_val - min_val))
|
|
|
|
return result
|
|
|
|
def _determine_best_worst(self, matrix: List[List[float]],
|
|
criterion_names: List[str]) -> Tuple[List[float], List[float]]:
|
|
"""确定最优值和最劣值"""
|
|
n_criteria = len(matrix[0])
|
|
f_best = []
|
|
f_worst = []
|
|
|
|
for j in range(n_criteria):
|
|
column = [matrix[i][j] for i in range(len(matrix))]
|
|
f_best.append(max(column))
|
|
f_worst.append(min(column))
|
|
|
|
return f_best, f_worst
|
|
|
|
|
|
# ============================================================================
|
|
# 灵敏度分析
|
|
# ============================================================================
|
|
|
|
class SensitivityAnalyzer:
|
|
"""
|
|
灵敏度分析器
|
|
|
|
分析权重变化对决策结果的影响。
|
|
"""
|
|
|
|
@staticmethod
|
|
def weight_sensitivity(alternatives: List[Alternative],
|
|
criteria: List[Criterion],
|
|
method: str = "TOPSIS",
|
|
perturbation: float = 0.1) -> Dict[str, Any]:
|
|
"""
|
|
权重灵敏度分析
|
|
|
|
Args:
|
|
alternatives: 备选方案
|
|
criteria: 准则列表
|
|
method: 评价方法
|
|
perturbation: 扰动幅度
|
|
|
|
Returns:
|
|
灵敏度分析结果
|
|
"""
|
|
# 原始权重
|
|
original_weights = [c.weight for c in criteria]
|
|
n_criteria = len(criteria)
|
|
|
|
# 原始排名
|
|
if method == "TOPSIS":
|
|
evaluator = TOPSIS(criteria)
|
|
else:
|
|
evaluator = WeightedSumModel(criteria)
|
|
|
|
original_results = evaluator.evaluate(alternatives)
|
|
original_ranking = [alt.id for alt, _ in original_results]
|
|
|
|
# 分析每个准则的权重变化
|
|
sensitivity_data = {}
|
|
|
|
for i, criterion in enumerate(criteria):
|
|
# 增加权重
|
|
weights_plus = original_weights.copy()
|
|
weights_plus[i] += perturbation
|
|
# 归一化
|
|
total = sum(weights_plus)
|
|
weights_plus = [w / total for w in weights_plus]
|
|
|
|
# 减少权重
|
|
weights_minus = original_weights.copy()
|
|
weights_minus[i] = max(0, weights_minus[i] - perturbation)
|
|
total = sum(weights_minus)
|
|
weights_minus = [w / total for w in weights_minus]
|
|
|
|
# 评估
|
|
criteria_plus = [CriteriaWrapper(c, w) for c, w in zip(criteria, weights_plus)]
|
|
criteria_minus = [CriteriaWrapper(c, w) for c, w in zip(criteria, weights_minus)]
|
|
|
|
if method == "TOPSIS":
|
|
evaluator_plus = TOPSIS(criteria_plus)
|
|
evaluator_minus = TOPSIS(criteria_minus)
|
|
else:
|
|
evaluator_plus = WeightedSumModel(criteria_plus)
|
|
evaluator_minus = WeightedSumModel(criteria_minus)
|
|
|
|
results_plus = evaluator_plus.evaluate(alternatives)
|
|
results_minus = evaluator_minus.evaluate(alternatives)
|
|
|
|
ranking_plus = [alt.id for alt, _ in results_plus]
|
|
ranking_minus = [alt.id for alt, _ in results_minus]
|
|
|
|
# 计算排名变化
|
|
rank_changes_plus = sum(
|
|
1 for a, b in zip(original_ranking, ranking_plus) if a != b
|
|
)
|
|
rank_changes_minus = sum(
|
|
1 for a, b in zip(original_ranking, ranking_minus) if a != b
|
|
)
|
|
|
|
sensitivity_data[criterion.name] = {
|
|
"weight_change": perturbation,
|
|
"rank_changes_increase": rank_changes_plus,
|
|
"rank_changes_decrease": rank_changes_minus,
|
|
"sensitive": rank_changes_plus > 0 or rank_changes_minus > 0
|
|
}
|
|
|
|
return {
|
|
"original_ranking": original_ranking,
|
|
"sensitivity_data": sensitivity_data
|
|
}
|
|
|
|
|
|
class CriteriaWrapper:
|
|
"""准则包装器,用于临时修改权重"""
|
|
def __init__(self, original: Criterion, weight: float):
|
|
self.name = original.name
|
|
self.criterion_type = original.criterion_type
|
|
self.weight = weight
|
|
self.scale = original.scale
|
|
self.optimal_value = original.optimal_value
|
|
|
|
|
|
# ============================================================================
|
|
# 主程序
|
|
# ============================================================================
|
|
|
|
def main():
|
|
"""主程序 - 演示多准则决策的使用"""
|
|
|
|
print("="*70)
|
|
print("多准则决策示例演示")
|
|
print("="*70)
|
|
|
|
# ========================================================================
|
|
# 1. 定义问题
|
|
# ========================================================================
|
|
print("\n[部分 1] 商场选址决策问题")
|
|
print("-" * 50)
|
|
|
|
# 定义准则
|
|
criteria = [
|
|
Criterion("人流量", CriterionType.BENEFIT, scale=(1000, 50000)),
|
|
Criterion("租金成本", CriterionType.COST, scale=(50, 200)),
|
|
Criterion("交通便利", CriterionType.BENEFIT, scale=(1, 10)),
|
|
Criterion("竞争强度", CriterionType.COST, scale=(0, 10)),
|
|
Criterion("发展潜力", CriterionType.BENEFIT, scale=(1, 10))
|
|
]
|
|
|
|
print("\n评价准则:")
|
|
for i, c in enumerate(criteria, 1):
|
|
type_cn = "效益型" if c.criterion_type == CriterionType.BENEFIT else "成本型"
|
|
print(f" {i}. {c.name:8s} ({type_cn}): {c.scale}")
|
|
|
|
# 定义备选方案
|
|
alternatives = [
|
|
Alternative("A1", "西湖商圈", {
|
|
"人流量": 45000,
|
|
"租金成本": 180,
|
|
"交通便利": 9,
|
|
"竞争强度": 8,
|
|
"发展潜力": 6
|
|
}),
|
|
Alternative("A2", "滨江新城", {
|
|
"人流量": 25000,
|
|
"租金成本": 120,
|
|
"交通便利": 7,
|
|
"竞争强度": 4,
|
|
"发展潜力": 9
|
|
}),
|
|
Alternative("A3", "萧山城区", {
|
|
"人流量": 18000,
|
|
"租金成本": 80,
|
|
"交通便利": 5,
|
|
"竞争强度": 3,
|
|
"发展潜力": 7
|
|
}),
|
|
Alternative("A4", "城西商圈", {
|
|
"人流量": 32000,
|
|
"租金成本": 150,
|
|
"交通便利": 8,
|
|
"竞争强度": 6,
|
|
"发展潜力": 8
|
|
}),
|
|
Alternative("A5", "下沙副中心", {
|
|
"人流量": 28000,
|
|
"租金成本": 100,
|
|
"交通便利": 6,
|
|
"竞争强度": 5,
|
|
"发展潜力": 7
|
|
})
|
|
]
|
|
|
|
print("\n备选方案:")
|
|
for alt in alternatives:
|
|
print(f" {alt.id}: {alt.name}")
|
|
for c in criteria:
|
|
print(f" {c.name}: {alt.get_value(c.name)}")
|
|
|
|
# ========================================================================
|
|
# 2. AHP确定权重
|
|
# ========================================================================
|
|
print("\n\n[部分 2] AHP层次分析法确定权重")
|
|
print("-" * 50)
|
|
|
|
ahp = AHP([c.name for c in criteria])
|
|
|
|
# 构建比较矩阵 (专家判断)
|
|
comparisons = {
|
|
("人流量", "租金成本"): 2, # 人流量稍微比租金重要
|
|
("人流量", "交通便利"): 3, # 人流量明显比交通重要
|
|
("人流量", "竞争强度"): 4, # 人流量比竞争重要
|
|
("人流量", "发展潜力"): 2, # 人流量稍微比发展潜力重要
|
|
("租金成本", "交通便利"): 2, # 租金稍微比交通重要
|
|
("租金成本", "竞争强度"): 2, # 租金稍微比竞争重要
|
|
("租金成本", "发展潜力"): 3, # 租金明显比发展潜力重要
|
|
("交通便利", "竞争强度"): 2, # 交通稍微比竞争重要
|
|
("交通便利", "发展潜力"): 2, # 交通稍微比发展潜力重要
|
|
("竞争强度", "发展潜力"): 2, # 竞争稍微比发展潜力重要
|
|
}
|
|
|
|
ahp.build_comparison_matrix(comparisons)
|
|
weights, cr, lambda_max = ahp.calculate_weights()
|
|
|
|
print(f"\nAHP权重计算结果:")
|
|
print(f" 最大特征值: {lambda_max:.4f}")
|
|
print(f" 一致性比率 CR: {cr:.4f}", end="")
|
|
if cr < 0.1:
|
|
print(" (通过一致性检验)")
|
|
else:
|
|
print(" (未通过一致性检验)")
|
|
|
|
print(f"\n准则权重:")
|
|
for i, (name, weight) in enumerate(zip([c.name for c in criteria], weights)):
|
|
criteria[i].weight = weight
|
|
print(f" {name:8s}: {weight:.4f}")
|
|
|
|
# ========================================================================
|
|
# 3. 熵权法确定权重
|
|
# ========================================================================
|
|
print("\n\n[部分 3] 熵权法确定客观权重")
|
|
print("-" * 50)
|
|
|
|
# 构建决策矩阵
|
|
decision_matrix = [
|
|
[alt.get_value(c.name) for c in criteria]
|
|
for alt in alternatives
|
|
]
|
|
|
|
entropy_weights = EntropyWeightMethod.calculate_weights(
|
|
decision_matrix,
|
|
[c.criterion_type for c in criteria]
|
|
)
|
|
|
|
print(f"\n熵权法计算结果:")
|
|
for name, weight in zip([c.name for c in criteria], entropy_weights):
|
|
print(f" {name:8s}: {weight:.4f}")
|
|
|
|
# 组合权重 (AHP 0.6 + 熵权 0.4)
|
|
print(f"\n组合权重 (AHP 60% + 熵权 40%):")
|
|
for i, c in enumerate(criteria):
|
|
combined_weight = 0.6 * c.weight + 0.4 * entropy_weights[i]
|
|
c.weight = combined_weight
|
|
print(f" {c.name:8s}: {combined_weight:.4f}")
|
|
|
|
# ========================================================================
|
|
# 4. TOPSIS评价
|
|
# ========================================================================
|
|
print("\n\n[部分 4] TOPSIS评价结果")
|
|
print("-" * 50)
|
|
|
|
topsis = TOPSIS(criteria)
|
|
topsis_results = topsis.evaluate(alternatives)
|
|
|
|
print(f"\nTOPSIS排名:")
|
|
print(f"{'排名':<6} {'方案ID':<8} {'名称':<12} {'贴近度':<10}")
|
|
print("-" * 40)
|
|
for i, (alt, score) in enumerate(topsis_results, 1):
|
|
print(f"{i:<6} {alt.id:<8} {alt.name:<12} {score:<10.4f}")
|
|
|
|
# ========================================================================
|
|
# 5. WSM评价
|
|
# ========================================================================
|
|
print("\n\n[部分 5] 加权求和模型(WSM)评价结果")
|
|
print("-" * 50)
|
|
|
|
wsm = WeightedSumModel(criteria)
|
|
wsm_results = wsm.evaluate(alternatives)
|
|
|
|
print(f"\nWSM排名:")
|
|
print(f"{'排名':<6} {'方案ID':<8} {'名称':<12} {'得分':<10}")
|
|
print("-" * 40)
|
|
for i, (alt, score) in enumerate(wsm_results, 1):
|
|
print(f"{i:<6} {alt.id:<8} {alt.name:<12} {score:<10.4f}")
|
|
|
|
# ========================================================================
|
|
# 6. VIKOR评价
|
|
# ========================================================================
|
|
print("\n\n[部分 6] VIKOR评价结果")
|
|
print("-" * 50)
|
|
|
|
vikor = VIKOR(criteria, v=0.5)
|
|
vikor_results = vikor.evaluate(alternatives)
|
|
|
|
print(f"\nVIKOR排名 (Q值越小越好):")
|
|
print(f"{'排名':<6} {'方案ID':<8} {'名称':<12} {'Q值':<10}")
|
|
print("-" * 40)
|
|
for i, (alt, score) in enumerate(vikor_results, 1):
|
|
print(f"{i:<6} {alt.id:<8} {alt.name:<12} {score:<10.4f}")
|
|
|
|
# ========================================================================
|
|
# 7. 方法比较
|
|
# ========================================================================
|
|
print("\n\n[部分 7] 不同方法排名比较")
|
|
print("-" * 50)
|
|
|
|
print(f"\n{'方案':<12} {'TOPSIS':<8} {'WSM':<8} {'VIKOR':<8}")
|
|
print("-" * 40)
|
|
|
|
for alt in alternatives:
|
|
topsis_rank = next(i for i, (a, _) in enumerate(topsis_results, 1) if a.id == alt.id)
|
|
wsm_rank = next(i for i, (a, _) in enumerate(wsm_results, 1) if a.id == alt.id)
|
|
vikor_rank = next(i for i, (a, _) in enumerate(vikor_results, 1) if a.id == alt.id)
|
|
|
|
print(f"{alt.name:<12} {topsis_rank:<8} {wsm_rank:<8} {vikor_rank:<8}")
|
|
|
|
# ========================================================================
|
|
# 8. 灵敏度分析
|
|
# ========================================================================
|
|
print("\n\n[部分 8] 权重灵敏度分析")
|
|
print("-" * 50)
|
|
|
|
sensitivity = SensitivityAnalyzer.weight_sensitivity(
|
|
alternatives, criteria, method="TOPSIS", perturbation=0.2
|
|
)
|
|
|
|
print(f"\n权重变化 ±20% 对排名的影响:")
|
|
print(f"{'准则':<10} {'排名变化':<12} {'敏感':<6}")
|
|
print("-" * 30)
|
|
|
|
for name, data in sensitivity["sensitivity_data"].items():
|
|
max_changes = max(data["rank_changes_increase"], data["rank_changes_decrease"])
|
|
sensitive = "是" if data["sensitive"] else "否"
|
|
print(f"{name:<10} {max_changes:<12} {sensitive:<6}")
|
|
|
|
# ========================================================================
|
|
# 9. 决策建议
|
|
# ========================================================================
|
|
print("\n\n[部分 9] 决策建议")
|
|
print("-" * 50)
|
|
|
|
best_topsis = topsis_results[0][0]
|
|
best_wsm = wsm_results[0][0]
|
|
best_vikor = vikor_results[0][0]
|
|
|
|
print(f"\n各方法推荐的最佳方案:")
|
|
print(f" TOPSIS: {best_topsis.name}")
|
|
print(f" WSM: {best_wsm.name}")
|
|
print(f" VIKOR: {best_vikor.name}")
|
|
|
|
# 综合推荐
|
|
vote_counts = {}
|
|
for alt in [best_topsis, best_wsm, best_vikor]:
|
|
vote_counts[alt.id] = vote_counts.get(alt.id, 0) + 1
|
|
|
|
recommended = max(vote_counts.items(), key=lambda x: x[1])[0]
|
|
recommended_alt = next(alt for alt in alternatives if alt.id == recommended)
|
|
|
|
print(f"\n综合推荐: {recommended_alt.name}")
|
|
print(f" 理由: 该方案在多种评价方法中表现最佳")
|
|
|
|
print("\n" + "="*70)
|
|
print("演示完成!")
|
|
print("="*70)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|