| 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") |
|
|
| |
| 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, |
| ) |
|
|
| |
| 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 |
|
|
| |
| 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) |
| |
| wsv = numeric_criteria_matrix.dot(weights_ahp_criteria) |
| |
| lambda_max_criteria = (wsv / weights_ahp_criteria).mean() |
| |
| 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.") |
|
|
| |
| 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 |
|
|
| |
| 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": |
| |
| 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):") |
| |
| summary_df = pd.DataFrame({ |
| "Kriteria": criteria, |
| "Bobot (Ternormalisasi)": weights, |
| "Tipe ": types |
| }) |
| |
| |
| st.write("Tabel Kriteria, Bobot, dan Tipe:") |
| st.dataframe(summary_df) |
| |
|
|
| |
| else: |
| |
| 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 |
|
|
| |
| 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: |
| |
| 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 |
|
|
| |
| norm_data = data / np.sqrt((data ** 2).sum(axis=0)) |
|
|
| |
| weighted_data = norm_data * w |
|
|
| |
| |
| |
| ideal_pos = np.where(types == 1, weighted_data.max(), weighted_data.min()) |
| ideal_neg = np.where(types == 1, weighted_data.min(), weighted_data.max()) |
|
|
| |
| dist_pos = np.sqrt(((weighted_data - ideal_pos) ** 2).sum(axis=1)) |
| dist_neg = np.sqrt(((weighted_data - ideal_neg) ** 2).sum(axis=1)) |
|
|
| |
| 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 = {} |
| |
| |
| 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 |
|
|
| |
| 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 |
|
|
| |
| 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 |
|
|
| |
| 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) |
| |
| |
| cf_indices = [criteria.index(c) for c in selected_core_factors] |
| |
| |
| 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()) |