136 lines
5.1 KiB
Python
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}")
|