final-project-2 / prediction.py
fernandobriann's picture
Update prediction.py
aeeb928 verified
Raw
History Blame Contribute Delete
10 kB
# prediction.py
import pandas as pd
import numpy as np
import pickle
import json
from sklearn.preprocessing import MinMaxScaler
from sklearn.metrics.pairwise import cosine_similarity
from scipy.spatial.distance import cdist
# =========================================================
# LOAD FILES
# =========================================================
# Memuat semua file yang dibutuhkan:
# - kmeans_model.pkl : model clustering yang sudah dilatih
# - app_metadata.json : metadata seperti nama fitur, label cluster, dan range harga
# - df_clean.csv : dataset smartphone yang sudah dibersihkan
# - feat_df_clean.csv : dataset fitur yang sudah dinormalisasi untuk similarity
with open("kmeans_model.pkl", "rb") as f:
kmeans = pickle.load(f)
with open("app_metadata.json") as f:
meta = json.load(f)
df = pd.read_csv("df_clean.csv", index_col=0)
feat_df = pd.read_csv("feat_df_clean.csv", index_col=0)
FEATURE_COLS = meta["FEATURE_COLS"]
CLUSTER_LABELS = {
int(k): v
for k, v in meta["CLUSTER_LABELS"].items()
}
price_min = meta["price_min"]
price_max = meta["price_max"]
# =========================================================
# PRIORITY WEIGHTS
# =========================================================
# Setiap priority memiliki bobot yang berbeda untuk setiap fitur.
# Bobot ini menentukan fitur mana yang paling penting saat menghitung
# kecocokan antara preferensi user dengan smartphone.
#
# Contoh: priority "gaming" memberikan bobot tertinggi ke cpu_tier_norm
# dan ram_norm karena performa CPU dan RAM paling penting untuk gaming.
PRIORITY_WEIGHTS = {
'gaming': {
'cpu_tier_norm': 0.35, # raw processing power is #1 for gamers
'ram_norm': 0.30, # games need memory for loading assets
'battery_norm': 0.25, # games are battery-hungry
'price_to_perf': 0.25, # gaming phones can be pricey; value matters
'camera_norm': 0.05, # camera is secondary for gamers
'battery_efficiency': 0.05,
},
'camera': {
'camera_norm': 0.55, # dominant: highest MP sensor is the main ask
'cpu_tier_norm': 0.15, # camera processing pipeline needs a decent chip
'ram_norm': 0.10,
'battery_norm': 0.10,
'battery_efficiency': 0.10,
'price_to_perf': 0.10,
},
'budget': {
'battery_norm': 0.15,
'battery_efficiency': 0.25,
'price_to_perf': 0.40,
'ram_norm': 0.10,
'cpu_tier_norm': 0.05,
'camera_norm': 0.05,
},
'balanced': {
'cpu_tier_norm': 0.20,
'ram_norm': 0.20,
'price_to_perf': 0.20,
'battery_norm': 0.15,
'camera_norm': 0.20,
'battery_efficiency': 0.10,
},
}
# Mapping priority ke nama segmen cluster tertentu
# Gaming dan camera langsung diarahkan ke cluster yang relevan
PRIORITY_TO_SEGMENT = {
'gaming': 'Gaming / Performance',
'camera': 'Camera-focused',
'budget': 'Budget',
'flagship': 'Flagship',
}
# =========================================================
# PRICE SCALER
# =========================================================
# MinMaxScaler digunakan untuk mengubah nilai harga (USD) ke skala 0-1
# agar bisa dibandingkan dengan fitur-fitur lain yang sudah dinormalisasi.
# Scaler di-fit dengan nilai min dan max harga dari dataset.
price_scaler = MinMaxScaler()
price_scaler.fit([
[price_min],
[price_max]
])
# =========================================================
# BUILD USER VECTOR
# =========================================================
# Fungsi ini membuat vektor preferensi user berdasarkan:
# - budget_usd : anggaran user dalam USD
# - priority : fokus utama user (gaming/camera/battery/balanced)
#
# Vektor ini nantinya akan dibandingkan dengan vektor fitur setiap
# smartphone menggunakan cosine similarity.
def build_user_vector(
budget_usd: float,
priority: str
) -> np.ndarray:
# Ambil bobot sesuai priority user
weights = PRIORITY_WEIGHTS.get(
priority,
PRIORITY_WEIGHTS['balanced']
)
max_w = max(weights.values())
# Normalisasi budget user ke skala 0-1
budget_pos = float(
np.clip(
price_scaler.transform([[budget_usd]])[0][0],
0.0,
1.0
)
)
vec = np.zeros(len(FEATURE_COLS))
for i, feat in enumerate(FEATURE_COLS):
if feat == 'price_norm':
# Posisi harga user dalam skala dataset
vec[i] = budget_pos
elif feat == 'price_to_perf':
# User dengan budget rendah cenderung lebih memperhatikan
# nilai per harga, sehingga nilainya dibalik (1 - budget_pos)
w = weights.get(feat, 0.0) / max_w
vec[i] = w * (1.0 - budget_pos)
else:
# Fitur lain dinormalisasi berdasarkan bobotnya
w = weights.get(feat, 0.0)
vec[i] = (w / max_w) if w > 0 else 0.5
return vec
# =========================================================
# RECOMMEND PHONES
# =========================================================
# Fungsi utama rekomendasi dengan tambahan filter spesifikasi.
#
# Parameter:
# - budget_usd : anggaran maksimum user (USD)
# - priority : fokus utama (gaming/camera/battery/balanced)
# - top_n : jumlah rekomendasi yang ditampilkan
# - min_ram : minimal RAM yang diinginkan (GB), default 0 = tidak difilter
# - min_camera_mp : minimal kamera utama (MP), default 0 = tidak difilter
# - min_battery : minimal kapasitas baterai (mAh), default 0 = tidak difilter
#
# Alur kerja:
# 1. Bangun vektor preferensi user
# 2. Tentukan cluster terbaik berdasarkan priority
# 3. Filter smartphone berdasarkan budget + spesifikasi minimum
# 4. Hitung cosine similarity antara vektor user dan vektor setiap smartphone
# 5. Hitung proximity (seberapa dekat harga smartphone dengan budget user)
# 6. Gabungkan similarity dan proximity menjadi Match Score
# 7. Kembalikan top-N smartphone dengan Match Score tertinggi
def recommend_phones(
budget_usd: float,
priority: str,
top_n: int = 5,
min_ram: int = 0,
min_camera_mp: int = 0,
min_battery: int = 0,
min_year: int = 0,
):
# -----------------------------------------
# STEP 1: Bangun vektor preferensi user
# -----------------------------------------
user_vec = build_user_vector(budget_usd, priority).reshape(1, -1)
# -----------------------------------------
# STEP 2: Tentukan cluster terbaik
# -----------------------------------------
reverse_lbl = {
label: cid
for cid, label in CLUSTER_LABELS.items()
}
if priority in PRIORITY_TO_SEGMENT:
# Untuk gaming dan camera, langsung arahkan ke cluster yang sesuai
best_cluster = reverse_lbl.get(
PRIORITY_TO_SEGMENT[priority],
int(np.argmin(cdist(user_vec, kmeans.cluster_centers_)[0]))
)
else:
# Untuk balanced dan battery, cari cluster terdekat dari vektor user
best_cluster = int(
np.argmin(
cdist(user_vec, kmeans.cluster_centers_, metric='euclidean')[0]
)
)
seg_name = CLUSTER_LABELS[best_cluster]
# -----------------------------------------
# STEP 3: Filter berdasarkan budget + spesifikasi minimum
# -----------------------------------------
in_cluster = df['cluster_kmeans'] == best_cluster
in_budget = df['price'] <= budget_usd
# Filter spesifikasi minimum dari user
in_ram = df['ram'] >= min_ram if min_ram > 0 else True
in_camera = df['main_camera_mp'] >= min_camera_mp if min_camera_mp > 0 else True
in_battery = df['battery_capacity'] >= min_battery if min_battery > 0 else True
in_year = df['launched year'] >= min_year if min_year > 0 else True
pool = df[in_cluster & in_budget & in_ram & in_camera & in_battery & in_year].copy()
# Jika tidak ada smartphone yang memenuhi semua kriteria dalam cluster,
# perluas pencarian ke semua cluster (tetapi tetap filter spesifikasi)
if pool.empty:
pool = df[in_budget & in_ram & in_camera & in_battery & in_year].copy()
seg_name += " (all clusters)"
# Jika masih kosong setelah filter spesifikasi, kembalikan pesan kosong
if pool.empty:
return pd.DataFrame(), seg_name + " — no phones match your filters"
# -----------------------------------------
# STEP 4 & 5: Hitung cosine similarity + proximity harga
# -----------------------------------------
ALPHA = 0.4 # Bobot proximity harga terhadap similarity (40% proximity, 60% similarity)
pool_X = feat_df.loc[pool.index, FEATURE_COLS].values
# Cosine similarity: mengukur seberapa "searah" vektor user dengan vektor smartphone
cos_sims = cosine_similarity(user_vec, pool_X)[0]
# Proximity: mengukur seberapa dekat harga smartphone dengan budget user
# Semakin dekat harganya ke budget, semakin tinggi nilai proximity-nya
proximity = np.clip(
1.0 - np.abs(budget_usd - pool['price'].values) / (budget_usd + 1e-6),
0, 1
)
# -----------------------------------------
# STEP 6: Gabungkan menjadi Match Score
# -----------------------------------------
pool['Match Score (%)'] = (
cos_sims * (ALPHA * proximity + (1.0 - ALPHA)) * 100
).round(1)
# -----------------------------------------
# STEP 7: Ambil top-N hasil terbaik
# -----------------------------------------
result_cols = [
'brand', 'model', 'price',
'ram', 'battery_capacity', 'main_camera_mp',
'Match Score (%)'
]
result_cols = [c for c in result_cols if c in pool.columns]
result = (
pool
.nlargest(top_n, 'Match Score (%)')[result_cols]
.reset_index(drop=True)
)
result.index += 1
return result, seg_name