Spaces:
Sleeping
Sleeping
| import streamlit as st | |
| import pandas as pd | |
| import numpy as np | |
| st.set_page_config(page_title="SPK Lokasi Kedai Kopi", layout="wide") | |
| st.title("π Sistem Pendukung Keputusan Lokasi Kedai Kopi") | |
| st.markdown("Metode: **AHP**, **TOPSIS**, dan **Profile Matching**") | |
| # --- Step 1: User Inputs for Criteria and Alternatives --- | |
| st.sidebar.header("βοΈ Pengaturan") | |
| criteria = st.sidebar.text_area("Masukkan Kriteria (pisahkan dengan koma)", "Building Area, Road Access, Distance, Rental Price") | |
| criteria = [c.strip() for c in criteria.split(",") if c.strip()] | |
| alternatives = st.sidebar.text_area("Masukkan Alternatif (pisahkan dengan koma)", "Location 1, Location 2, Location 3, Location 4") | |
| alternatives = [a.strip() for a in alternatives.split(",") if a.strip()] | |
| if not criteria or not alternatives: | |
| st.warning("Masukkan minimal satu kriteria dan alternatif.") | |
| st.stop() | |
| # --- Step 2: Input Matrix (User-Editable) --- | |
| 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") | |
| # Validasi isian | |
| if df.isnull().values.any(): | |
| st.warning("β οΈ Harap lengkapi semua nilai pada tabel sebelum menjalankan perhitungan.") | |
| st.stop() | |
| if method == "AHP": | |
| st.subheader("π Perbandingan Berpasangan Antar Kriteria (AHP)") | |
| pairwise_matrix = pd.DataFrame(np.ones((len(criteria), len(criteria))), index=criteria, columns=criteria) | |
| for i in range(len(criteria)): | |
| for j in range(i + 1, len(criteria)): | |
| val = st.number_input( | |
| f"Seberapa penting '{criteria[i]}' dibandingkan '{criteria[j]}'?", | |
| min_value=1/9.0, max_value=9.0, value=1.0, step=0.1, | |
| key=f"{criteria[i]}_{criteria[j]}" | |
| ) | |
| pairwise_matrix.iloc[i, j] = val | |
| pairwise_matrix.iloc[j, i] = 1 / val | |
| st.write("π Matriks Perbandingan Kriteria:") | |
| st.dataframe(pairwise_matrix) | |
| norm_matrix = pairwise_matrix / pairwise_matrix.sum() | |
| weights = norm_matrix.mean(axis=1).values | |
| weights /= weights.sum() | |
| st.write("π― Bobot Kriteria dari AHP:") | |
| st.dataframe(pd.Series(weights, index=criteria, name="Bobot")) | |
| else: | |
| # --- Step 3 (non-AHP): Manual Bobot Kriteria --- | |
| st.subheader("βοΈ Bobot Kriteria") | |
| 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}'", min_value=1.0, max_value=10.0, value=5.0, step=0.1) | |
| weights = np.array([weight_dict[c] for c in criteria]) | |
| weights /= weights.sum() # normalize | |
| # --- Step 4: Ideal Profile --- | |
| 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) | |
| ideal_profile = pd.Series(ideal_profile_dict) | |
| # --- Step 5: Metode --- | |
| st.subheader("π§ Pilih Metode") | |
| method = st.selectbox("Metode yang ingin dijalankan", ["AHP", "TOPSIS", "Profile Matching"]) | |
| # --- UTILITIES --- | |
| def scale_to_five(val): | |
| if val >= 90: return 5 | |
| elif val >= 80: return 4 | |
| elif val >= 70: return 3 | |
| elif val >= 60: return 2 | |
| elif val >= 50: return 1 | |
| else: return 1 | |
| # --- ALGORITHMS --- | |
| def run_ahp(df, weights): | |
| normalized = df / df.sum() | |
| weighted = normalized * weights | |
| return weighted.sum(axis=1) | |
| def run_topsis(df, weights, types): | |
| norm_df = df / np.sqrt((df**2).sum()) | |
| weighted = norm_df * weights | |
| ideal_pos = np.where(types == 1, weighted.max(), weighted.min()) | |
| ideal_neg = np.where(types == 1, weighted.min(), weighted.max()) | |
| d_pos = np.sqrt(((weighted - ideal_pos)**2).sum(axis=1)) | |
| d_neg = np.sqrt(((weighted - ideal_neg)**2).sum(axis=1)) | |
| return d_neg / (d_pos + d_neg) | |
| def run_profile_matching(df_scaled, ideal, cf_weight, sf_weight, cf_indices): | |
| 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 | |
| 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) | |
| return cf_weight * ncf + sf_weight * nsf | |
| # --- Step 6: Run --- | |
| if st.button("π Jalankan Perhitungan"): | |
| df_raw = df.copy() | |
| st.write("π Data Masukan", df_raw) | |
| if method == "AHP": | |
| st.subheader("π Hasil AHP") | |
| result = run_ahp(df_raw, weights) | |
| st.dataframe(result.rename("Skor").sort_values(ascending=False)) | |
| elif method == "TOPSIS": | |
| st.subheader("π Hasil TOPSIS") | |
| types = np.array([1 if "price" not in c.lower() else -1 for c in criteria]) | |
| result = run_topsis(df_raw, weights, types) | |
| st.dataframe(result.rename("Skor").sort_values(ascending=False)) | |
| elif method == "Profile Matching": | |
| st.subheader("π Hasil Profile Matching") | |
| df_scaled = df_raw.applymap(scale_to_five) | |
| st.write("π Data Skala 1β5", df_scaled) | |
| core_factors = [criteria[-1]] # default: last one is core (e.g., Rental Price) | |
| cf_indices = [criteria.index(c) for c in core_factors] | |
| result = run_profile_matching(df_scaled, ideal_profile, cf_weight=0.6, sf_weight=0.4, cf_indices=cf_indices) | |
| st.dataframe(result.rename("Skor").sort_values(ascending=False)) | |