MIF_E31231430/topsis_recalculator.py

136 lines
5.1 KiB
Python

import math
import numpy as np
# Let's list the raw camera data
# Raw specifications: Price, ISO, AF Point, Sensor (ordinal)
raw_data = [
# [Nama, Price, ISO, AF, Sensor]
["Canon M10", 80000, 25600, 49, "APS-C"],
["Sony A5000", 90000, 25600, 25, "APS-C"],
["Canon M3", 100000, 25600, 49, "APS-C"],
["Sony A6000", 110000, 25600, 179, "APS-C"],
["Canon M50", 130000, 51200, 143, "APS-C"],
["Sony A6300", 155000, 51200, 425, "APS-C"],
["Fujifilm XA3", 80000, 25600, 77, "APS-C"],
["Sony A6400", 185000, 51200, 425, "APS-C"],
["Fujifilm XA5", 90000, 25600, 77, "APS-C"],
["Nikon J5", 75000, 12800, 171, "1-inch"],
["Fujifilm XT20", 145000, 51200, 325, "APS-C"],
["Canon 500D", 55000, 12800, 9, "APS-C"],
["Canon 60D", 100000, 12800, 9, "APS-C"],
["Canon 1100D", 55000, 12800, 9, "APS-C"],
["Canon 80D", 140000, 25600, 45, "APS-C"],
["Canon 600D", 80000, 12800, 9, "APS-C"],
["Canon 6D", 165000, 102400, 61, "Full Frame"],
["Canon 550D", 75000, 12800, 9, "APS-C"],
["Canon 5D Mark III", 215000, 102400, 61, "Full Frame"]
]
price_min = 55000
price_max = 215000
iso_max = 102400
af_max = 425
print("--- SCALE 0 to 1 (x1) ---")
print("| No | Nama Kamera | Merek | Price Score | ISO Score | AF Score | Sensor Score |")
print("|---|---|---|---|---|---|---|")
processed_data = []
sensor_mapping = {
"Full Frame": 1.0,
"APS-C": 0.7,
"1-inch": 0.5
}
for idx, cam in enumerate(raw_data):
name = cam[0]
brand = name.split()[0]
price = cam[1]
iso = cam[2]
af = cam[3]
sensor = cam[4]
price_score = ((price_max - price) / (price_max - price_min)) * 1.0
iso_score = (iso / iso_max) * 1.0
af_score = (af / af_max) * 1.0
sensor_score = sensor_mapping[sensor]
# Let's round to 4 decimals for exact tracking, and 2 decimals for display
# Check what rounding was used.
# In table 4.9: Canon M10: 8.44 (rounded from 8.4375), Sony A6400: 1.88 (rounded from 1.875)
# AF score: Canon M10 49/425 * 10 = 1.1529... -> 1.15
# So it is rounded to 2 decimal places.
ps_rnd = round(price_score, 2)
is_rnd = round(iso_score, 2)
af_rnd = round(af_score, 2)
ss_rnd = round(sensor_score, 2)
print(f"| {idx+1} | {name} | {brand} | {ps_rnd:.2f} | {is_rnd:.2f} | {af_rnd:.2f} | {ss_rnd:.2f} |")
processed_data.append([name, ps_rnd, is_rnd, af_rnd, ss_rnd])
# Let's compute TOPSIS for Outdoor - Day (Siang) using the rounded table values
# Bobot = [0.30, 0.15, 0.25, 0.30]
# Jenis = [cost (since PriceScore is benefit, wait! Is PriceScore cost or benefit in TOPSIS?)
# In RekomendasiController, PriceScore is defined as cost: $jenis = ['cost', 'benefit', 'benefit', 'benefit'];
# BUT wait, PriceScore already has (PriceMax - Price) / (PriceMax - PriceMin), so higher is cheaper (better).
# If PriceScore is marked as 'cost' in $jenis, then Step 4 (Ideal Pos/Neg) does:
# for cost kriteria: idealPos is MIN(col), idealNeg is MAX(col).
# So idealPos will select the minimum of PriceScore (which corresponds to highest price, e.g. 0.00), and idealNeg will select maximum of PriceScore (which corresponds to lowest price, e.g. 1.00).
# Let's verify if that matches the PDF!
# In PDF:
# Solusi Ideal Positif A+ = [0.000000, 0.079823, 0.140025, 0.094444]
# Here, the first element of A+ is indeed 0.000000 (which is the minimum of weighted normal for PriceScore).
# And the first element of A- is indeed 0.098277 (which is the maximum of weighted normal for PriceScore, corresponding to Canon 500D/1100D with PriceScore 10.00).
# Yes! The code in RekomendasiController marks it as 'cost', which means it takes min for idealPos and max for idealNeg.
# Let's perform this calculation.
scores = np.array([[row[1], row[2], row[3], row[4]] for row in processed_data])
m, n = scores.shape
# Euclidean Divider
dividers = []
for j in range(n):
dividers.append(math.sqrt(sum(scores[i][j]**2 for i in range(m))))
print("\n--- Dividers ---")
for j, div in enumerate(dividers):
print(f"C_{j+1} Divider: {div:.4f}")
# Normalization
r = np.zeros((m, n))
for i in range(m):
for j in range(n):
r[i][j] = scores[i][j] / dividers[j]
# Weighted Normalized Matrix
bobot = [0.30, 0.15, 0.25, 0.30]
y = np.zeros((m, n))
for i in range(m):
for j in range(n):
y[i][j] = r[i][j] * bobot[j]
# Ideal solutions
# C1 is cost, C2-C4 is benefit
ideal_pos = [min(y[:, 0]), max(y[:, 1]), max(y[:, 2]), max(y[:, 3])]
ideal_neg = [max(y[:, 0]), min(y[:, 1]), min(y[:, 2]), min(y[:, 3])]
print("\nA+:", [f"{val:.6f}" for val in ideal_pos])
print("A-:", [f"{val:.6f}" for val in ideal_neg])
# Distances and Preferences
results = []
for i in range(m):
dp = math.sqrt(sum((y[i][j] - ideal_pos[j])**2 for j in range(n)))
dm = math.sqrt(sum((y[i][j] - ideal_neg[j])**2 for j in range(n)))
v = dm / (dp + dm) if (dp + dm) > 0 else 0.0
results.append((processed_data[i][0], scores[i], dp, dm, v))
# Sort by preference
results.sort(key=lambda x: x[4], reverse=True)
print("\n--- Ranking for Outdoor - Day ---")
for rank, res in enumerate(results):
name, original_scores, dp, dm, v = res
print(f"Rank {rank+1}: {name} | Scores: {original_scores} | D+: {dp:.4f} | D-: {dm:.4f} | V: {v:.6f}")