用Python手把手教你实现TOPSIS算法:从数据预处理到结果排序的完整流程
当我们需要在多个候选方案中做出选择时,TOPSIS(Technique for Order Preference by Similarity to Ideal Solution)算法提供了一种科学而直观的决策方法。想象一下,你正在评估几家供应商,或者需要为团队选择最佳的项目方案,每个选项都有多个维度的评价指标——这正是TOPSIS大显身手的场景。
与那些停留在理论层面的教程不同,本文将带你用Python从零开始实现完整的TOPSIS流程。我们会使用Pandas处理真实业务数据,用Numpy进行高效向量化计算,最终封装成可复用的Python函数。特别地,我会分享在实际项目中遇到的坑——比如如何处理缺失值、为什么某些归一化方法会导致结果失真,以及如何用向量化运算提升10倍性能。
1. 理解TOPSIS:从业务场景到数学原理
TOPSIS的核心思想非常直观:找出距离"理想解"最近且距离"负理想解"最远的方案。这里的"理想解"指的是每个指标都达到最佳值的虚拟方案,而"负理想解"则是每个指标都是最差值的虚拟方案。
典型应用场景包括:
- 供应商评估(价格、交货期、质量评分)
- 投资项目选择(收益率、风险等级、流动性)
- 人才选拔(技能评分、工作经验、面试表现)
让我们用一个具体的例子来说明。假设我们需要评估5个软件开发团队,考虑三个指标:
- 代码质量评分(越高越好)
- 平均交付时间(越短越好)
- 每行代码成本(越低越好)
import pandas as pd teams = pd.DataFrame({ '团队': ['A', 'B', 'C', 'D', 'E'], '代码质量': [85, 70, 90, 60, 80], '交付时间': [10, 15, 8, 20, 12], '代码成本': [0.5, 0.3, 0.6, 0.2, 0.4] })2. 数据预处理:标准化与权重分配
数据预处理是TOPSIS中最容易出错的环节。不同的指标往往有不同的量纲(单位)和变化范围,直接计算会导致量纲大的指标主导结果。我们需要先将所有指标规范化到统一尺度。
2.1 数据标准化方法对比
| 方法 | 公式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|---|
| 向量归一化 | $b_{ij} = a_{ij}/\sqrt{\sum a_{ij}^2}$ | 大多数TOPSIS应用 | 保持相对关系 | 结果不在[0,1]区间 |
| 线性比例变换 | $b_{ij} = a_{ij}/\max(a_j)$ | 效益型指标 | 直观易理解 | 对异常值敏感 |
| 极差变换 | $(a_{ij}-\min(a_j))/(\max(a_j)-\min(a_j))$ | 需要严格[0,1]范围 | 结果标准化 | 丢失原始比例关系 |
def normalize(df, criteria_types): """ 向量归一化标准化 :param df: 原始数据DataFrame :param criteria_types: 字典,指定每个指标是效益型(1)还是成本型(-1) :return: 标准化后的DataFrame """ normalized = df.copy() for col in criteria_types: if criteria_types[col] == 1: # 效益型 normalized[col] = df[col] / np.sqrt((df[col]**2).sum()) else: # 成本型 normalized[col] = df[col] / np.sqrt((df[col]**2).sum()) normalized[col] = 1 - normalized[col] # 成本型取反 return normalized2.2 权重分配的艺术
权重反映了各指标的重要性差异。确定权重的方法包括:
- 主观赋权法:专家打分、AHP层次分析法
- 客观赋权法:熵权法、CRITIC法
- 组合赋权:主客观结合
提示:权重分配是TOPSIS中最具主观性的环节,建议通过敏感性分析验证权重变化的鲁棒性。
3. 核心计算:寻找理想解与距离度量
标准化和加权后,我们进入TOPSIS的核心计算阶段。这一部分将展示如何用Numpy高效实现。
3.1 确定正负理想解
def get_ideal_solutions(normalized_df, criteria_types): """ 获取正理想解和负理想解 :param normalized_df: 标准化后的DataFrame :param criteria_types: 指标类型字典 :return: 正理想解, 负理想解 """ positive_ideal = [] negative_ideal = [] for col in criteria_types: if criteria_types[col] == 1: # 效益型 positive_ideal.append(normalized_df[col].max()) negative_ideal.append(normalized_df[col].min()) else: # 成本型 positive_ideal.append(normalized_df[col].min()) negative_ideal.append(normalized_df[col].max()) return np.array(positive_ideal), np.array(negative_ideal)3.2 欧氏距离计算优化
传统实现可能使用循环计算每个方案到理想解的距离,但在Python中我们可以利用Numpy的广播机制实现向量化运算,性能提升显著:
def calculate_distances(normalized_df, positive_ideal, negative_ideal): """ 向量化计算距离 :param normalized_df: 标准化后的DataFrame :param positive_ideal: 正理想解 :param negative_ideal: 负理想解 :return: 到正理想解的距离, 到负理想解的距离 """ values = normalized_df.drop(columns=['团队']).values pos_dist = np.sqrt(((values - positive_ideal)**2).sum(axis=1)) neg_dist = np.sqrt(((values - negative_ideal)**2).sum(axis=1)) return pos_dist, neg_dist4. 结果分析与可视化
计算出各方案到理想解的距离后,我们可以计算相对贴近度并排序:
def calculate_ranking(pos_dist, neg_dist): """ 计算相对贴近度并排序 :param pos_dist: 到正理想解的距离数组 :param neg_dist: 到负理想解的距离数组 :return: 排序后的索引(从优到劣) """ closeness = neg_dist / (pos_dist + neg_dist) return np.argsort(-closeness) # 降序排列结果可视化建议:
- 雷达图展示各方案指标对比
- 二维散点图(正理想距离 vs 负理想距离)
- 排序柱状图
import matplotlib.pyplot as plt def plot_results(df, closeness, ranking): plt.figure(figsize=(10, 5)) plt.barh(df['团队'][ranking], closeness[ranking], color='skyblue') plt.xlabel('相对贴近度') plt.title('TOPSIS评估结果排序') plt.gca().invert_yaxis() # 最佳方案显示在顶部 plt.show()5. 工程实践:封装完整TOPSIS类
将上述步骤封装成可复用的Python类,方便在不同项目中调用:
class TOPSIS: def __init__(self, data, criteria_types, weights=None): """ 初始化TOPSIS评估器 :param data: 包含方案名称和指标值的DataFrame :param criteria_types: 字典,指定每个指标是效益型(1)还是成本型(-1) :param weights: 各指标权重,默认为等权重 """ self.original_data = data self.criteria_types = criteria_types self.weights = weights if weights else np.ones(len(criteria_types))/len(criteria_types) self.normalized_data = None self.positive_ideal = None self.negative_ideal = None self.pos_distances = None self.neg_distances = None self.closeness = None self.ranking = None def normalize(self): """数据标准化""" self.normalized_data = self.original_data.copy() for i, col in enumerate(self.criteria_types): norm = np.sqrt((self.original_data[col]**2).sum()) if self.criteria_types[col] == 1: # 效益型 self.normalized_data[col] = self.original_data[col] / norm else: # 成本型 self.normalized_data[col] = (1 - self.original_data[col]/norm) def calculate_ideal_solutions(self): """计算理想解""" values = self.normalized_data.drop(columns=['团队']).values self.positive_ideal = [] self.negative_ideal = [] for i, col in enumerate(self.criteria_types): if self.criteria_types[col] == 1: # 效益型 self.positive_ideal.append(values[:, i].max()) self.negative_ideal.append(values[:, i].min()) else: # 成本型 self.positive_ideal.append(values[:, i].min()) self.negative_ideal.append(values[:, i].max()) self.positive_ideal = np.array(self.positive_ideal) * self.weights self.negative_ideal = np.array(self.negative_ideal) * self.weights def evaluate(self): """执行完整评估流程""" self.normalize() self.calculate_ideal_solutions() # 加权标准化矩阵 weighted_matrix = self.normalized_data.drop(columns=['团队']).values * self.weights # 计算距离 self.pos_distances = np.sqrt(((weighted_matrix - self.positive_ideal)**2).sum(axis=1)) self.neg_distances = np.sqrt(((weighted_matrix - self.negative_ideal)**2).sum(axis=1)) # 计算相对贴近度 self.closeness = self.neg_distances / (self.pos_distances + self.neg_distances) self.ranking = np.argsort(-self.closeness) return { 'closeness': self.closeness, 'ranking': self.ranking, 'normalized_data': self.normalized_data }6. 实战案例:供应商选择系统
让我们用一个完整的案例演示如何使用封装的TOPSIS类解决实际问题。假设我们需要从6家供应商中选择最佳合作伙伴,评估指标包括:
- 产品单价(成本型)
- 交货准时率(效益型)
- 质量合格率(效益型)
- 售后服务评分(效益型)
# 准备数据 suppliers = pd.DataFrame({ '供应商': ['S1', 'S2', 'S3', 'S4', 'S5', 'S6'], '单价': [85, 70, 90, 60, 80, 75], '准时率': [0.95, 0.85, 0.92, 0.88, 0.90, 0.82], '合格率': [0.98, 0.97, 0.99, 0.96, 0.95, 0.94], '服务评分': [4.2, 3.8, 4.5, 3.9, 4.1, 3.7] }) # 定义指标类型和权重 criteria = { '单价': -1, # 成本型 '准时率': 1, '合格率': 1, '服务评分': 1 } weights = [0.3, 0.25, 0.25, 0.2] # 总和不一定要等于1,会自动归一化 # 执行TOPSIS评估 topsis = TOPSIS(suppliers, criteria, weights) results = topsis.evaluate() # 输出结果 result_df = suppliers.copy() result_df['相对贴近度'] = results['closeness'] result_df['排名'] = results['ranking'] + 1 # 转为1-based print(result_df.sort_values('排名'))常见问题解决方案:
缺失值处理:
- 删除包含缺失值的方案(样本量足够时)
- 用列均值或中位数填充
- 使用KNN等算法预测缺失值
异常值处理:
- Winsorize处理(缩尾)
- 用IQR方法识别并处理异常值
- 考虑使用更鲁棒的距离度量(如曼哈顿距离)
敏感性分析:
- 对权重进行±10%的扰动,观察排名变化
- 使用蒙特卡洛模拟评估不同权重组合下的结果稳定性
注意:当两个方案的相对贴近度非常接近时(如差值<0.01),在实际应用中可视为同等优秀,不必强行区分。