import streamlit as st import pandas as pd import numpy as np st.set_page_config(page_title="SPK Pemilihan Cabang untuk Lokasi Coffeeshop", layout="wide") st.title("Sistem Pendukung Keputusan Pemilihan Cabang untuk Lokasi Coffeeshop") st.markdown("Metode: AHP, TOPSIS, dan Profile Matching") # --- Step 1: Pilih Metode --- st.sidebar.header("⚙ Pengaturan") method = st.selectbox("Pilih Metode", ["None","TOPSIS", "Profile Matching", "AHP"], key="selected_method") criteria_default = "Building Area, Road Access, Distance, Rental Price" criteria_input = st.sidebar.text_area("Masukkan Kriteria (pisahkan dengan koma)", criteria_default) criteria = [c.strip() for c in criteria_input.split(",") if c.strip()] alternatives_default = "Location 1, Location 2, Location 3, Location 4" alternatives_input = st.sidebar.text_area("Masukkan Alternatif (pisahkan dengan koma)", alternatives_default) alternatives = [a.strip() for a in alternatives_input.split(",") if a.strip()] if not criteria or not alternatives: st.warning("Masukkan minimal satu kriteria dan alternatif.") st.stop() # --------------------------- if method == "AHP": st.subheader("Perbandingan Berpasangan Antar Kriteria (AHP)") pairwise_criteria_data = np.ones((len(criteria), len(criteria))) for i in range(len(criteria)): for j in range(i + 1, len(criteria)): pairwise_criteria_data[i, j] = 1.0 pairwise_criteria_df = pd.DataFrame(pairwise_criteria_data, index=criteria, columns=criteria) edited_pairwise_criteria_matrix = st.data_editor( pairwise_criteria_df, use_container_width=True, key="pairwise_comparison_criteria_matrix", hide_index=False, ) # Reciprocity for criteria matrix final_pairwise_criteria_matrix = edited_pairwise_criteria_matrix.copy() for i in range(len(criteria)): for j in range(i + 1, len(criteria)): val = final_pairwise_criteria_matrix.iloc[i, j] if val == 0: val = 1e-9 final_pairwise_criteria_matrix.iloc[j, i] = 1 / val final_pairwise_criteria_matrix.iloc[i, i] = 1.0 # Hitung bobot kriteria AHP weights_ahp_criteria = pd.Series(dtype='float64') if not final_pairwise_criteria_matrix.apply(pd.to_numeric, errors='coerce').isnull().values.any(): try: numeric_criteria_matrix = final_pairwise_criteria_matrix.apply(pd.to_numeric) norm_matrix = numeric_criteria_matrix / numeric_criteria_matrix.sum() weights_ahp_criteria = norm_matrix.mean(axis=1) weights_ahp_criteria /= weights_ahp_criteria.sum() st.write("Bobot Kriteria dari AHP:") st.dataframe(weights_ahp_criteria.round(3).to_frame(name="Bobot AHP")) RI = {1: 0.00, 2: 0.00, 3: 0.58, 4: 0.90, 5: 1.12, 6: 1.24, 7: 1.32, 8: 1.41, 9: 1.45, 10: 1.49} n_criteria = len(criteria) # Weighted Sum Vector (WSV) wsv = numeric_criteria_matrix.dot(weights_ahp_criteria) # λ_max manual: rata-rata dari WSV[i] / W[i] lambda_max_criteria = (wsv / weights_ahp_criteria).mean() # CI manual CI_criteria = (lambda_max_criteria - n_criteria) / (n_criteria - 1) if n_criteria > 1 else 0 CR_criteria = np.nan if n_criteria in RI and RI[n_criteria] > 0: CR_criteria = CI_criteria / RI[n_criteria] st.subheader("Analisis Konsistensi Kriteria AHP") st.write(f"$\\lambda_{{max}} = {lambda_max_criteria:.10f}$") st.write(f"CI = {CI_criteria:.10f}") if not np.isnan(CR_criteria): st.write(f"CR = {CR_criteria:.10f}") if CR_criteria < 0.1: st.success("Konsistensi Rasio Kriteria AHP *BAIK* (< 0.1)") else: st.error("Konsistensi Rasio Kriteria AHP *BURUK* (>= 0.1). Harap sesuaikan perbandingan kriteria.") else: st.info("CR kriteria tidak dapat dihitung (untuk 1 kriteria atau RI tidak tersedia).") except Exception as e: st.error(f"Terjadi kesalahan saat menghitung bobot kriteria AHP: {e}") st.warning("Pastikan semua nilai pada matriks perbandingan kriteria adalah numerik.") else: st.warning("Matriks perbandingan kriteria belum lengkap atau tidak valid.") # --- Perbandingan berpasangan antar alternatif untuk setiap kriteria --- st.subheader("🔗 Perbandingan Berpasangan Antar Alternatif untuk Setiap Kriteria (AHP)") alternative_pairwise_matrices = {} alternative_weights_per_criterion = {} for criterion in criteria: st.markdown(f"### Kriteria: *{criterion}*") pairwise_alt_data = np.ones((len(alternatives), len(alternatives))) for i in range(len(alternatives)): for j in range(i + 1, len(alternatives)): pairwise_alt_data[i, j] = 1.0 pairwise_alt_df = pd.DataFrame(pairwise_alt_data, index=alternatives, columns=alternatives) st.markdown(f"Isi perbandingan berpasangan alternatif untuk *{criterion}* (misal: 3 berarti alternatif baris 3x lebih baik dari alternatif kolom terhadap {criterion}).") st.markdown("Nilai akan otomatis disesuaikan secara resiprokal saat perhitungan.") edited_pairwise_alt_matrix = st.data_editor( pairwise_alt_df, use_container_width=True, key=f"pairwise_comparison_alternatives_matrix_{criterion}", hide_index=False, ) final_pairwise_alt_matrix = edited_pairwise_alt_matrix.copy() for i in range(len(alternatives)): for j in range(i + 1, len(alternatives)): val = final_pairwise_alt_matrix.iloc[i, j] if val == 0: val = 1e-9 final_pairwise_alt_matrix.iloc[j, i] = 1 / val final_pairwise_alt_matrix.iloc[i, i] = 1.0 alternative_pairwise_matrices[criterion] = final_pairwise_alt_matrix # Hitung bobot alternatif untuk criterion ini try: numeric_alt_matrix = final_pairwise_alt_matrix.apply(pd.to_numeric) norm_matrix = numeric_alt_matrix / numeric_alt_matrix.sum() weights_alternatives = norm_matrix.mean(axis=1) weights_alternatives /= weights_alternatives.sum() alternative_weights_per_criterion[criterion] = weights_alternatives st.write(f"Bobot alternatif untuk kriteria *{criterion}*:") st.dataframe(weights_alternatives.round(3).to_frame(name=f"Bobot alternatif ({criterion})")) except Exception as e: st.error(f"Error menghitung bobot alternatif pada kriteria {criterion}: {e}") elif method == "None": st.subheader("Pilih Metode terlebih dahulu") elif method =="TOPSIS": # TOPSIS st.subheader("Input Nilai Alternatif terhadap Kriteria") empty_data = pd.DataFrame(np.nan, index=alternatives, columns=criteria) df = st.data_editor(empty_data, use_container_width=True, key="input_matrix") if df.isnull().values.any(): st.warning("⚠ Harap lengkapi semua nilai pada tabel sebelum menjalankan perhitungan untuk TOPSIS/Profile Matching.") criteria_default_type= { "Building Area" : "benefit", "Road Access": "benefit", "Distance" : "benefit", "Rental Price" : "cost" } st.subheader("Bobot Kriteria (Manual)") weight_dict = {} cols = st.columns(len(criteria)) for i, c in enumerate(criteria): with cols[i]: weight_dict[c] = st.number_input(f"Bobot untuk '{c}' dengan rentang 1-5", min_value=0.0, max_value=10.0, value=0.0, step=0.1, key=f"weight_{c}") types = np.array([1 if criteria_default_type.get(c,"benefit") == "benefit" else 0 for c in criteria]) weights = np.array([weight_dict[c] for c in criteria]) weights /= weights.sum() st.write("Bobot Kriteria (Ternormalisasi):") # Gabungkan ke dalam DataFrame summary_df = pd.DataFrame({ "Kriteria": criteria, "Bobot (Ternormalisasi)": weights, "Tipe ": types }) # Tampilkan st.write("Tabel Kriteria, Bobot, dan Tipe:") st.dataframe(summary_df) #st.dataframe(pd.Series(weights, index=criteria, name="Bobot", )) else: #Profile Matching st.subheader("Input Nilai Alternatif terhadap Kriteria") empty_data = pd.DataFrame(np.nan, index=alternatives, columns=criteria) df = st.data_editor(empty_data, use_container_width=True, key="input_matrix") if df.isnull().values.any(): st.warning("Harap lengkapi semua nilai pada tabel sebelum menjalankan perhitungan untuk TOPSIS/Profile Matching.") st.subheader("Ideal Profile (untuk Profile Matching)") ideal_profile_dict = {} cols = st.columns(len(criteria)) for i, c in enumerate(criteria): with cols[i]: ideal_profile_dict[c] = st.number_input(f"Ideal '{c}' (1-5)", min_value=1, max_value=5, value=5, key=f"ideal_{c}") ideal_profile = pd.Series(ideal_profile_dict, index=criteria) st.write("Profil Ideal:") st.dataframe(ideal_profile.to_frame(name="Nilai Ideal")) st.subheader("⚙ Pengaturan Faktor (untuk Profile Matching)") core_factors_options = criteria default_core_factors = [criteria[-1]] if criteria else [] selected_core_factors = st.multiselect( "Pilih Kriteria Core Factor (Faktor Inti)", options=core_factors_options, default=default_core_factors, help="Kriteria yang dianggap paling penting. Sisanya akan menjadi Secondary Factor.", key="pm_core_factors" ) if not selected_core_factors: st.warning("Setidaknya satu Core Factor harus dipilih untuk Profile Matching.") st.stop() cf_weight = st.number_input(f"Bobot Core Factor", min_value=0.0, max_value=1.0, value=0.6, step=0.05) sf_weight = 1 - cf_weight # Tombol jalankan perhitungan run_calc = st.button("Jalankan Perhitungan") if run_calc: if method == "AHP": if len(alternative_weights_per_criterion) != len(criteria): st.error("Perbandingan alternatif belum lengkap untuk semua kriteria. Lengkapi terlebih dahulu.") elif weights_ahp_criteria.empty: st.error("Bobot kriteria AHP belum terhitung dengan benar.") else: # Hitung skor akhir AHP per alternatif = sum bobot kriteria * bobot alternatif pada kriteria final_scores_ahp = pd.Series(0.0, index=alternatives) for c in criteria: w_crit = weights_ahp_criteria.get(c, 0) w_alts = alternative_weights_per_criterion.get(c, pd.Series(0, index=alternatives)) final_scores_ahp += w_crit * w_alts st.subheader("Hasil Perhitungan AHP") st.write(final_scores_ahp.sort_values(ascending=False).to_frame("Skor Akhir")) st.write("*Alternatif terbaik berdasarkan AHP:*") st.success(final_scores_ahp.idxmax()) elif method == "TOPSIS": if df.isnull().values.any(): st.error("Isi semua nilai alternatif terlebih dahulu.") else: data = df.values.astype(float) w = weights # Normalisasi norm_data = data / np.sqrt((data ** 2).sum(axis=0)) # Bobot weighted_data = norm_data * w # Tentukan ideal positif dan negatif (max/min) # ideal_pos = np.max(weighted_data, axis=0) # ideal_neg = np.min(weighted_data, axis=0) ideal_pos = np.where(types == 1, weighted_data.max(), weighted_data.min()) ideal_neg = np.where(types == 1, weighted_data.min(), weighted_data.max()) # Jarak ke ideal positif dan negatif dist_pos = np.sqrt(((weighted_data - ideal_pos) ** 2).sum(axis=1)) dist_neg = np.sqrt(((weighted_data - ideal_neg) ** 2).sum(axis=1)) # Skor preferensi scores_topsis = dist_neg / (dist_pos + dist_neg) topsis_result = pd.Series(scores_topsis, index=alternatives).sort_values(ascending=False) st.subheader("Hasil Perhitungan TOPSIS") st.write(topsis_result.to_frame("Skor")) st.write("*Alternatif terbaik berdasarkan TOPSIS:*") st.success(topsis_result.idxmax()) elif method == "Profile Matching": if df.isnull().values.any(): st.error("Isi semua nilai alternatif terlebih dahulu.") else: def scale_row(row): scaled = {} # Luas Bangunan (semakin besar semakin baik) if row["Building Area"] >= 90: scaled["Building Area"] = 5 elif row["Building Area"] >= 89: scaled["Building Area"] = 4 elif row["Building Area"] >= 70: scaled["Building Area"] = 3 elif row["Building Area"] >= 60: scaled["Building Area"] = 2 else: scaled["Building Area"] = 1 # Akses Jalan (semakin besar semakin baik) if row["Road Access"] > 1500: scaled["Road Access"] = 1 elif row["Road Access"] > 1000: scaled["Road Access"] = 2 elif row["Road Access"] > 800: scaled["Road Access"] = 3 elif row["Road Access"] > 500: scaled["Road Access"] = 4 else: scaled["Road Access"] = 5 # Jarak ke Pusat Keramaian (semakin kecil semakin baik) if row["Distance"] >= 1500: scaled["Distance"] = 1 elif row["Distance"] >= 1000: scaled["Distance"] = 2 elif row["Distance"] >= 800: scaled["Distance"] = 3 elif row["Distance"] >= 500: scaled["Distance"] = 4 else: scaled["Distance"] = 5 # Harga Sewa (semakin kecil semakin baik) if row["Rental Price"] >= 90: scaled["Rental Price"] = 1 elif row["Rental Price"] >= 70: scaled["Rental Price"] = 2 elif row["Rental Price"] >= 60: scaled["Rental Price"] = 3 elif row["Rental Price"] >= 50: scaled["Rental Price"] = 4 else: scaled["Rental Price"] = 5 return pd.Series(scaled) df_scaled = df.apply(scale_row, axis=1) st.write("📏 Data Skala 1–5", df_scaled) # Get core factor indices cf_indices = [criteria.index(c) for c in selected_core_factors] # Run profile matching with the improved function gap_weights = { 0: 5, 1: 4.5, -1: 4.5, 2: 4, -2: 4, 3: 3.5, -3: 3.5, 4: 3, -4: 3, 5: 2.5, -5: 2.5 } df_gap = df_scaled - ideal_profile df_wgap = df_gap.applymap(lambda x: gap_weights.get(int(x), 0)) cf_cols = df_scaled.columns[cf_indices] sf_cols = df_scaled.columns.drop(cf_cols) ncf = df_wgap[cf_cols].mean(axis=1) nsf = df_wgap[sf_cols].mean(axis=1) if len(sf_cols) > 0 else 0 final_scores_pm = cf_weight * ncf + sf_weight * nsf st.subheader("📈 Hasil Perhitungan Profile Matching") st.write(final_scores_pm.sort_values(ascending=False).to_frame("Skor")) st.write("*Alternatif terbaik berdasarkan Profile Matching:*") st.success(final_scores_pm.idxmax())