0. Motivation
就是覺得有趣,在想可不可以用到午餐的推薦上面?
TOPSIS(Technique for Order Preference by Similarity to Ideal Solution)是一種多準則決策分析方法,適用於需要在多個評估指標下對多個方案進行排序和選擇的情境。這邊主要簡述使用情境,之後紀錄計算的流程步驟,最後使用 Python 給出一個範例。
1. 使用情境
1-1. 適用情境
多目標決策問題: 當需要在多個評估指標下對多個方案進行綜合評估時,TOPSIS方法能有效地將各指標進行標準化處理,計算各方案與理想解的距離,從而進行排序。
例子: 在供應商選擇中,企業需要考慮成本、品質、交貨時間等多個因素。TOPSIS方法可以綜合這些指標,評估各供應商的表現,幫助企業選擇最合適的供應商。指標數據已知且可靠: 當各評估指標的數據已知且具有可靠性時,TOPSIS方法可以充分利用這些原始數據的信息,精確地反映各方案之間的差距。
例子: 在產品品質評估中,若各產品的性能指標(如耐用性、功耗、效率等)數據已知且可靠,TOPSIS方法可以根據這些數據對產品進行排序,協助決策者選擇最佳產品。
1-2. 不適用情境
指標數據缺乏或不可靠: 當評估指標的數據缺乏或不可靠時,TOPSIS方法可能無法提供準確的評估結果。
例子: 在新產品開發初期,許多性能指標的數據可能尚未確定或不完整,此時使用TOPSIS方法可能導致評估結果偏差。指標之間存在高度相關性: 當評估指標之間存在高度相關性或多重共線性時,TOPSIS方法可能會受到影響,因為它假設各指標是相互獨立的。
例子: 在評估城市發展水平時,若同時使用人均收入和GDP作為指標,這兩者之間可能存在高度相關性,使用TOPSIS方法可能會放大某一方面的影響,導致評估結果失真。決策者無法確定指標權重: TOPSIS方法需要為各評估指標分配權重,若決策者無法確定各指標的重要性,可能影響評估結果的準確性。
例子: 在公共政策評估中,若涉及經濟、環境、社會等多個方面,且決策者無法確定各方面的相對重要性,使用TOPSIS方法可能難以得出公正的結論。
2. TOPSIS 計算步驟
Step 1. 建立一個決策矩陣 $A$
並且 $1\leq i\leq n, 1\leq j\leq m$。
說明 :
- $n$ 個評估的對象
- $m$ 個評估的指標
- 每一行(column)為一個指標,每一列(row)為一個評估對象的數據
Step 2. 無因次化(Nondimensionalization)、標準化數據
消除指標間不同因次的影響,將指標數據標準化 :
Step 3. 計算加權標準化決策矩陣
標準化後的數據乘以對應的權重 $w_{ij}$ :
其中 :
- $w_j$ 是指標 $j$ 的權重,這邊不細講權重的計算,下面的範例將使用熵權法(Entropy Weight method)。
並且我們因此得到加權標準化決策矩陣 :
Step 4. 確定理想解(PIS)與反理想解(NIS)
理想解(Positive Ideal Solution) : 取加權標準化決策矩陣中每個指標的最大值
反理想解(Negative Ideal Solution) : 取加權標準化決策矩陣中每個指標的最小值
Step 5. 計算與PIS與NIS的歐氏距離
其中 :
- $d_i^+$ 為樣本 $i$ 到 PIS 的距離
- $d_i^-$ 為樣本 $i$ 到 NIS 的距離
Step 6. 計算相對相似度
計算每個評估對象相對於理想解的相似度:
其中 :
- $f_{i}$ 為對象樣本 $i$ 的相似度,數值範圍為 $[0,1]$。
- 當 $f_i$ 越接近 1 ,表示評估的對象越好;當 $f_i$ 越接近 0,表示評估的對象越差。
3. Python Example
這邊使用的範例是《數學建模:算法與編程實現》的河流水質評價的範例 :
import numpy as np
# 設定全域浮點數輸出精度為小數點後四位
np.set_printoptions(precision=4)
def read_csv_with_numpy(file_path, delimiter=',', skip_header=0, dtype=float, encoding='utf-8'):
"""
使用 numpy 讀取 CSV 檔案
參數:
file_path : 檔案路徑
delimiter : 分隔符號 (預設為 ',')
skip_header: 跳過的標題列數 (預設為 0)
dtype : 資料型別 (預設為 float)
encoding : 檔案編碼 (預設為 'utf-8')
返回:
讀取到的 numpy 陣列,若失敗則返回 None
"""
try:
with open(file_path, 'r', encoding=encoding) as file:
data = np.genfromtxt(file, delimiter=delimiter, skip_header=skip_header, dtype=dtype)
return data
except UnicodeDecodeError as e:
print(f"讀取檔案時發生編碼錯誤:{e}")
return None
except Exception as e:
print(f"讀取檔案時發生錯誤:{e}")
return None
def transform_indicator(data, indicator_type, target=None, interval=None):
"""
根據指標類型對數據進行轉換。
參數:
data : 待轉換數據 (numpy 陣列)
indicator_type : 指標類型,可選 'positive' (正向)、'negative' (負向)、'center' (居中型)、'interval' (區間型)
target : 居中型指標的目標值 (必須提供)
interval : 區間型指標的上下限 tuple,例如 (lower_bound, upper_bound)
返回:
轉換後的數據 (numpy 陣列)
"""
if indicator_type == 'positive':
# 正向指標:數值越大越好,無需轉換
return data
elif indicator_type == 'negative':
# 負向指標:數值越小越好,轉換為正向指標 (極小極大化轉換)
max_val = np.max(data)
return max_val - data
elif indicator_type == 'center':
# 居中型指標:數值越接近目標值越好
if target is None:
raise ValueError("居中型指標需要提供目標值 target")
M_Value = np.max(np.abs(target - data))
return 1 - (np.abs(target - data)) / M_Value
elif indicator_type == 'interval':
# 區間型指標:數值在指定區間內最好,超出區間者給予懲罰
if interval is None or len(interval) != 2:
raise ValueError("區間型指標需要提供上下限值的 tuple,例如 (lower_bound, upper_bound)")
lower_bound, upper_bound = interval
dt_tmp = np.ones_like(data, dtype=float)
M_low_value = lower_bound - np.min(data)
M_up_value = np.max(data) - upper_bound
M_Value = np.max([M_low_value, M_up_value])
dt_tmp[data < lower_bound] = 1 - (lower_bound - data[data < lower_bound]) / M_Value
dt_tmp[data > upper_bound] = 1 - (data[data > upper_bound] - upper_bound) / M_Value
return dt_tmp
else:
raise ValueError("未知的指標類型:{}".format(indicator_type))
def entropy_weight_method(data):
"""
使用熵權法計算每個指標的權重。
參數:
data: numpy.ndarray,形狀為 (n, m),其中 n 為評估對象數量,m 為評估指標數量。
返回:
weights: numpy.ndarray,各指標的權重 (形狀: (m,))
"""
# 定義縮放區間 [0.002, 0.996]
lower_bound = 0.002
upper_bound = 0.996
# 第一步:對每個欄位進行最小-最大正規化
min_vals = np.min(data, axis=0)
max_vals = np.max(data, axis=0)
range_vals = max_vals - min_vals
# 避免除以零,若範圍為 0 則設為 1
range_vals[range_vals == 0] = 1
# 將數據正規化到 [0, 1]
normalized_data = (data - min_vals) / range_vals
# 再縮放到 [0.002, 0.996]
scaled_data = normalized_data * (upper_bound - lower_bound) + lower_bound
n, m = scaled_data.shape
# 計算各指標下各樣本的比重 pij
p = scaled_data / np.sum(scaled_data, axis=0)
epsilon = 1e-10 # 為避免 log(0) 的錯誤
# 計算熵值
e = -np.sum(p * np.log(p + epsilon), axis=0) / np.log(n)
# 計算熵冗餘度
d = 1 - e
# 計算權重,並標準化使得權重和為 1
weights = d / np.sum(d)
return weights
def topsis_analysis(data, weights):
"""
使用 TOPSIS 方法進行多準則決策分析。
參數:
data : numpy.ndarray,處理後的決策矩陣 (形狀: (n, m))
weights: numpy.ndarray,各指標的權重 (形狀: (m,))
返回:
relative_closeness: numpy.ndarray,每個方案的 TOPSIS 得分 (已重新縮放至 [0, 100])
"""
# 第一步:標準化決策矩陣 (向量規範化)
norm_data = data / np.sqrt(np.sum(data ** 2, axis=0))
# 第二步:計算加權標準化矩陣
weighted_data = norm_data * weights
# 第三步:確定正理想解與負理想解
ideal_best = np.max(weighted_data, axis=0) # 正理想解:各指標最大值
ideal_worst = np.min(weighted_data, axis=0) # 負理想解:各指標最小值
# 第四步:計算各方案與正/負理想解的歐幾里得距離
dist_to_best = np.sqrt(np.sum((weighted_data - ideal_best) ** 2, axis=1))
dist_to_worst = np.sqrt(np.sum((weighted_data - ideal_worst) ** 2, axis=1))
# 第五步:計算相對貼近度得分
relative_closeness = dist_to_worst / (dist_to_best + dist_to_worst)
# 將得分重新縮放到 [0, 100]
min_score = np.min(relative_closeness)
max_score = np.max(relative_closeness)
if max_score - min_score != 0:
relative_closeness = (relative_closeness - min_score) / (max_score - min_score) * 100
else:
relative_closeness = np.full_like(relative_closeness, 0)
return relative_closeness
# 主程式執行區
if __name__ == '__main__':
# 設定資料檔案路徑
file_path = 'river_data.csv'
# 讀取 CSV 檔案 (跳過標題列)
data_array = read_csv_with_numpy(file_path, skip_header=1)
if data_array is not None:
# 提取各欄位資料
river_id = data_array[:, 0].astype(int) # 第一欄:河流編號 (轉換為整數)
oxygen_content = data_array[:, 1] # 第二欄:含氧量
ph_value = data_array[:, 2] # 第三欄: pH 值
bacteria_count = data_array[:, 3] # 第四欄:細菌總數 (個數/ml)
nutrients = data_array[:, 4] # 第五欄:植物性營養物量 (ppm)
# 輸出原始資料
print("河流編號:", river_id)
print("含氧量:", oxygen_content)
print("pH 值:", ph_value)
print("細菌總數(個數/ml):", bacteria_count)
print("植物性營養物量(ppm):", nutrients)
# 依據各指標的特性進行數據預處理
oxygen_content_dt = transform_indicator(oxygen_content, indicator_type='positive')
ph_value_dt = transform_indicator(ph_value, indicator_type='center', target=7)
bacteria_count_dt = transform_indicator(bacteria_count, indicator_type='negative')
nutrients_dt = transform_indicator(nutrients, indicator_type='interval', interval=(10, 20))
# 合併處理後的各指標數據,並四捨五入至小數點後五位
processed_data = np.round(np.array([oxygen_content_dt, ph_value_dt, bacteria_count_dt, nutrients_dt]).T, 5)
print("\n處理後的決策矩陣:")
print(processed_data)
# 利用熵權法計算各指標權重
weights = entropy_weight_method(processed_data)
print("\n各指標的權重:")
print(weights)
# 進行 TOPSIS 分析,計算每個方案的相對貼近度得分 (TOPSIS 得分已縮放至 [0, 100])
scores = topsis_analysis(processed_data, weights)
print("\n各方案的 TOPSIS 得分 (已縮放到 [0, 100]):")
for i, score in enumerate(scores):
print(f"河流編號: {river_id[i]}, TOPSIS 得分: {score:.2f}")
資料 (river_data.csv
):
河流 | 含氧量(ppm) | PH值 | 細菌總數(個/ml) | 植物性營養物量(ppm) |
---|---|---|---|---|
1 | 4.69 | 6.59 | 51 | 11.94 |
2 | 2.03 | 7.86 | 19 | 6.46 |
3 | 9.11 | 6.31 | 46 | 8.91 |
4 | 8.61 | 7.05 | 46 | 26.43 |
5 | 7.13 | 6.50 | 50 | 23.57 |
6 | 2.39 | 6.77 | 38 | 24.62 |
7 | 7.69 | 6.79 | 38 | 6.01 |
8 | 9.30 | 6.81 | 27 | 31.57 |
9 | 5.45 | 7.62 | 5 | 18.46 |
10 | 6.19 | 7.27 | 17 | 7.51 |
11 | 7.93 | 7.53 | 9 | 6.52 |
12 | 4.40 | 7.28 | 17 | 25.30 |
13 | 7.46 | 8.24 | 23 | 14.42 |
14 | 2.01 | 5.55 | 47 | 26.31 |
15 | 2.04 | 6.40 | 23 | 17.91 |
16 | 7.73 | 6.14 | 52 | 15.72 |
17 | 6.35 | 7.58 | 25 | 29.46 |
18 | 8.29 | 8.41 | 39 | 12.02 |
19 | 3.54 | 7.27 | 54 | 3.16 |
20 | 7.44 | 6.26 | 8 | 28.41 |
結果 :