Spaces:
Sleeping
Sleeping
| 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.zeros((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.zeros((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=0, max_value=5, value=0, 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.0, 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: | |
| # Scale input values to 1-5 range first | |
| 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 | |
| df_scaled = df.applymap(scale_to_five) | |
| 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()) | |
| # data = df.astype(float) | |
| # ideal_profile = pd.Series(ideal_profile_dict) | |
| # core_factors = selected_core_factors | |
| # secondary_factors = [c for c in criteria if c not in core_factors] | |
| # # Hitung gap untuk tiap alternatif dan kriteria | |
| # gaps = data.subtract(ideal_profile, axis=1) | |
| # # Ubah gap menjadi nilai bobot sesuai dengan skala gap (bisa disesuaikan) | |
| # def gap_weight(g): | |
| # if g == 0: | |
| # return 5 | |
| # elif g == -1: | |
| # return 4.5 | |
| # elif g == 1: | |
| # return 4 | |
| # elif g == -2: | |
| # return 3.5 | |
| # elif g == 2: | |
| # return 3 | |
| # elif g == -3: | |
| # return 2.5 | |
| # elif g == 3: | |
| # return 2 | |
| # elif g == -4: | |
| # return 1.5 | |
| # elif g == 4: | |
| # return 1 | |
| # else: | |
| # return 1 | |
| # weight_gaps = gaps.applymap(gap_weight) | |
| # # Hitung core factor dan secondary factor scores per alternatif | |
| # cf_scores = weight_gaps[core_factors].mean(axis=1) | |
| # sf_scores = weight_gaps[secondary_factors].mean(axis=1) if secondary_factors else 0 | |
| # final_scores_pm = cf_weight * cf_scores + sf_weight * sf_scores | |
| # 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()) |