import streamlit as st import pandas as pd import numpy as np import altair as alt from sklearn.preprocessing import StandardScaler 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") # --------------------------- if method == "AHP": # --- Setup Kriteria (TANPA COST/BENEFIT) --- st.sidebar.subheader("📝 Manajemen Kriteria") # Default kriteria (struktur disederhanakan) default_criteria = [ {"name": "Building Area"}, {"name": "Road Access"}, {"name": "Distance"}, {"name": "Rental Price"} ] # Initialize session state untuk kriteria if 'criteria_list' not in st.session_state: st.session_state.criteria_list = default_criteria.copy() # Initialize pairwise comparison matrix in session state if 'pairwise_matrix' not in st.session_state: st.session_state.pairwise_matrix = {} # Fungsi untuk menambah kriteria (disesuaikan) def add_criterion(): # Hanya mengambil nama kriteria new_criterion = { "name": st.session_state.new_criterion_name } # Logika sisanya tetap sama if new_criterion["name"] and new_criterion["name"] not in [c["name"] for c in st.session_state.criteria_list]: st.session_state.criteria_list.append(new_criterion) st.session_state.new_criterion_name = "" st.session_state.pairwise_matrix = {} # Fungsi untuk menghapus kriteria (tidak ada perubahan) def remove_criterion(idx): if len(st.session_state.criteria_list) > 1: st.session_state.criteria_list.pop(idx) st.session_state.pairwise_matrix = {} # Form untuk menambah kriteria baru (input tipe dihapus) with st.sidebar.expander("➕ Tambah Kriteria Baru"): st.text_input("Nama Kriteria", key="new_criterion_name", placeholder="Masukkan nama kriteria") # Tombol selectbox untuk tipe kriteria DIHAPUS st.button("Tambah Kriteria", on_click=add_criterion) # Tampilkan daftar kriteria yang ada (tampilan disederhanakan) st.sidebar.subheader("📋 Daftar Kriteria") for i, criterion in enumerate(st.session_state.criteria_list): # Menggunakan layout 2 kolom, sama seperti alternatif col1, col2 = st.sidebar.columns([4, 1]) with col1: # Menghapus format tebal dan miring agar konsisten st.write(criterion['name']) with col2: # Kolom untuk tipe kriteria DIHAPUS if st.button("🗑", key=f"remove_{i}", help="Hapus kriteria"): remove_criterion(i) st.rerun() # Extract nama kriteria (variabel criteria_types DIHAPUS) criteria = [c["name"] for c in st.session_state.criteria_list] # criteria_types = {c["name"]: c["type"] for c in st.session_state.criteria_list} # <-- BARIS INI DIHAPUS # --- Setup Alternatif (tidak ada perubahan) --- st.sidebar.subheader("🏢 Manajemen Alternatif") default_alternatives = [ {"name": "Location 1"}, {"name": "Location 2"}, {"name": "Location 3"}, {"name": "Location 4"} ] if 'alternatives_list' not in st.session_state: st.session_state.alternatives_list = default_alternatives.copy() def add_alternative(): new_alternative = {"name": st.session_state.new_alternative_name} if new_alternative["name"] and new_alternative["name"] not in [a["name"] for a in st.session_state.alternatives_list]: st.session_state.alternatives_list.append(new_alternative) st.session_state.new_alternative_name = "" def remove_alternative(idx): if len(st.session_state.alternatives_list) > 1: st.session_state.alternatives_list.pop(idx) with st.sidebar.expander("➕ Tambah Alternatif Baru"): st.text_input("Nama Alternatif", key="new_alternative_name", placeholder="Masukkan nama alternatif") st.button("Tambah Alternatif", on_click=add_alternative) st.sidebar.subheader("📋 Daftar Alternatif") for i, alternative in enumerate(st.session_state.alternatives_list): col1, col2 = st.sidebar.columns([4, 1]) with col1: st.write(alternative['name']) with col2: if st.button("🗑", key=f"remove_alt_{i}", help="Hapus alternatif"): remove_alternative(i) st.rerun() alternatives = [a["name"] for a in st.session_state.alternatives_list] if not criteria or not alternatives: st.warning("Masukkan minimal satu kriteria dan alternatif.") st.stop() st.subheader("🔗 Perbandingan Berpasangan Antar Kriteria (AHP)") # Create improved pairwise comparison matrix st.markdown(""" Petunjuk Pengisian: - Nilai 1 = Sama penting - Nilai 3 = Sedikit lebih penting - Nilai 5 = Lebih penting - Nilai 7 = Sangat lebih penting - Nilai 9 = Mutlak lebih penting - Nilai 2,4,6,8 = Nilai tengah - Dapat menggunakan desimal (contoh: 1.5, 2.5, dll) """) # Create pairwise comparison table n_criteria = len(criteria) # Initialize matrix if not exists matrix_key = f"criteria_matrix_{len(criteria)}" if matrix_key not in st.session_state.pairwise_matrix: st.session_state.pairwise_matrix[matrix_key] = np.ones((n_criteria, n_criteria)) # Create the comparison matrix display st.markdown("### Matriks Perbandingan Berpasangan") # Create input fields for upper triangular matrix comparison_matrix = st.session_state.pairwise_matrix[matrix_key].copy() # First, collect all upper triangular inputs upper_triangular_inputs = {} # Create table for displaying the matrix for i in range(n_criteria): cols = st.columns(n_criteria + 1) # Row header with cols[0]: st.write(f"{criteria[i]}") for j in range(n_criteria): with cols[j + 1]: if i == j: # Diagonal elements are always 1 st.write("1.0") elif i < j: # Upper triangular - allow number input key = f"comparison_{i}_{j}" comparison_value = st.number_input( f"{criteria[i]} vs {criteria[j]}", min_value=0.1, max_value=9.0, value=float(comparison_matrix[i, j]), step=0.1, key=key, label_visibility="collapsed" ) upper_triangular_inputs[(i, j)] = comparison_value else: # Lower triangular - show reciprocal # Get the corresponding upper triangular value upper_value = upper_triangular_inputs.get((j, i), comparison_matrix[j, i]) reciprocal_value = 1.0 / upper_value if upper_value != 0 else 1.0 st.write(f"{reciprocal_value:.3f}") # Update the matrix with all values for (i, j), value in upper_triangular_inputs.items(): comparison_matrix[i, j] = value comparison_matrix[j, i] = 1.0 / value # Update session state st.session_state.pairwise_matrix[matrix_key] = comparison_matrix # Display the complete matrix st.markdown("### Matriks Lengkap") matrix_df = pd.DataFrame(comparison_matrix, index=criteria, columns=criteria) st.dataframe(matrix_df.round(3), use_container_width=True) # Calculate AHP weights try: # Calculate normalized matrix column_sums = comparison_matrix.sum(axis=0) normalized_matrix = comparison_matrix / column_sums # Calculate criteria weights weights_ahp_criteria = normalized_matrix.mean(axis=1) weights_ahp_criteria = pd.Series(weights_ahp_criteria, index=criteria) st.write("🎯 Bobot Kriteria dari AHP:") weights_df = weights_ahp_criteria.round(3).to_frame(name="Bobot AHP") st.dataframe(weights_df) # Consistency check 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 = len(criteria) # Calculate λ_max weighted_sum = comparison_matrix.dot(weights_ahp_criteria) lambda_max = (weighted_sum / weights_ahp_criteria).mean() # Calculate CI and CR CI = (lambda_max - n) / (n - 1) if n > 1 else 0 CR = CI / RI.get(n, 1) if n in RI and RI[n] > 0 else 0 st.subheader("✅ Analisis Konsistensi") col1, col2, col3 = st.columns(3) with col1: st.metric("λ max", f"{lambda_max:.3f}") with col2: st.metric("CI", f"{CI:.3f}") with col3: st.metric("CR", f"{CR:.3f}") if CR < 0.1: st.success("✅ Konsistensi Rasio BAIK (CR < 0.1)") else: st.error("❌ Konsistensi Rasio BURUK (CR ≥ 0.1). Harap sesuaikan perbandingan.") except Exception as e: st.error(f"Error dalam perhitungan AHP: {e}") # Alternative pairwise comparisons for each criterion st.subheader("🔗 Perbandingan Berpasangan Antar Alternatif") alternative_weights_per_criterion = {} for criterion in criteria: with st.expander(f"Perbandingan untuk Kriteria: {criterion}"): st.markdown(f"Bandingkan alternatif berdasarkan {criterion}") n_alternatives = len(alternatives) alt_matrix_key = f"alt_matrix_{criterion}_{n_alternatives}" # Initialize alternative matrix if alt_matrix_key not in st.session_state.pairwise_matrix: st.session_state.pairwise_matrix[alt_matrix_key] = np.ones((n_alternatives, n_alternatives)) alt_comparison_matrix = st.session_state.pairwise_matrix[alt_matrix_key].copy() # Create comparison inputs alt_upper_triangular_inputs = {} for i in range(n_alternatives): cols = st.columns(n_alternatives + 1) with cols[0]: st.write(f"{alternatives[i]}") for j in range(n_alternatives): with cols[j + 1]: if i == j: st.write("1.0") elif i < j: key = f"alt_comparison_{criterion}{i}{j}" comparison_value = st.number_input( f"{alternatives[i]} vs {alternatives[j]}", min_value=0.1, max_value=9.0, value=float(alt_comparison_matrix[i, j]), step=0.1, key=key, label_visibility="collapsed" ) alt_upper_triangular_inputs[(i, j)] = comparison_value else: # Lower triangular - show reciprocal upper_value = alt_upper_triangular_inputs.get((j, i), alt_comparison_matrix[j, i]) reciprocal_value = 1.0 / upper_value if upper_value != 0 else 1.0 st.write(f"{reciprocal_value:.3f}") # Update the alternative matrix with all values for (i, j), value in alt_upper_triangular_inputs.items(): alt_comparison_matrix[i, j] = value alt_comparison_matrix[j, i] = 1.0 / value # Update session state st.session_state.pairwise_matrix[alt_matrix_key] = alt_comparison_matrix # Calculate alternative weights for this criterion try: alt_column_sums = alt_comparison_matrix.sum(axis=0) alt_normalized_matrix = alt_comparison_matrix / alt_column_sums alt_weights = alt_normalized_matrix.mean(axis=1) alt_weights_series = pd.Series(alt_weights, index=alternatives) alternative_weights_per_criterion[criterion] = alt_weights_series st.write(f"Bobot alternatif untuk {criterion}:") st.dataframe(alt_weights_series.round(3).to_frame(name="Bobot")) except Exception as e: st.error(f"Error menghitung bobot alternatif untuk {criterion}: {e}") elif method == "None": st.subheader("Pilih Metode terlebih dahulu") elif method == "TOPSIS": # --- Setup Kriteria dengan Tipe --- st.sidebar.subheader("📝 Manajemen Kriteria") # Default kriteria default_criteria = [ {"name": "Building Area (m2)", "type": "benefit"}, {"name": "Road Access (m)", "type": "benefit"}, {"name": "Distance (m)", "type": "benefit"}, {"name": "Rental Price (jt Rp)", "type": "cost"} ] # Initialize session state untuk kriteria if 'criteria_list' not in st.session_state: st.session_state.criteria_list = default_criteria.copy() # Initialize pairwise comparison matrix in session state if 'pairwise_matrix' not in st.session_state: st.session_state.pairwise_matrix = {} # Fungsi untuk menambah kriteria def add_criterion(): new_criterion = { "name": st.session_state.new_criterion_name, "type": st.session_state.new_criterion_type } if new_criterion["name"] and new_criterion["name"] not in [c["name"] for c in st.session_state.criteria_list]: st.session_state.criteria_list.append(new_criterion) st.session_state.new_criterion_name = "" # Reset pairwise matrix when criteria change st.session_state.pairwise_matrix = {} # Fungsi untuk menghapus kriteria def remove_criterion(idx): if len(st.session_state.criteria_list) > 1: # Minimal 1 kriteria st.session_state.criteria_list.pop(idx) # Reset pairwise matrix when criteria change st.session_state.pairwise_matrix = {} # Form untuk menambah kriteria baru with st.sidebar.expander("➕ Tambah Kriteria Baru"): st.text_input("Nama Kriteria", key="new_criterion_name", placeholder="Masukkan nama kriteria") st.selectbox("Tipe Kriteria", ["benefit", "cost"], key="new_criterion_type", help="Benefit = semakin tinggi semakin baik, Cost = semakin rendah semakin baik") st.button("Tambah Kriteria", on_click=add_criterion) # Tampilkan daftar kriteria yang ada st.sidebar.subheader("📋 Daftar Kriteria") for i, criterion in enumerate(st.session_state.criteria_list): col1, col2, col3 = st.sidebar.columns([3, 2, 1]) with col1: st.write(f"{criterion['name']}") with col2: badge_color = "🟢" if criterion['type'] == 'benefit' else "🔴" st.write(f"{badge_color} {criterion['type']}") with col3: if st.button("🗑", key=f"remove_{i}", help="Hapus kriteria"): remove_criterion(i) st.rerun() # Extract nama kriteria dan tipe criteria = [c["name"] for c in st.session_state.criteria_list] criteria_types = {c["name"]: c["type"] for c in st.session_state.criteria_list} # --- Setup Alternatif --- st.sidebar.subheader("🏢 Manajemen Alternatif") # Default alternatif default_alternatives = [ {"name": "Location 1"}, {"name": "Location 2"}, {"name": "Location 3"}, {"name": "Location 4"} ] # Initialize session state untuk alternatif if 'alternatives_list' not in st.session_state: st.session_state.alternatives_list = default_alternatives.copy() # Fungsi untuk menambah alternatif def add_alternative(): new_alternative = {"name": st.session_state.new_alternative_name} if new_alternative["name"] and new_alternative["name"] not in [a["name"] for a in st.session_state.alternatives_list]: st.session_state.alternatives_list.append(new_alternative) st.session_state.new_alternative_name = "" # Fungsi untuk menghapus alternatif def remove_alternative(idx): if len(st.session_state.alternatives_list) > 1: # Minimal 1 alternatif st.session_state.alternatives_list.pop(idx) # Form untuk menambah alternatif baru with st.sidebar.expander("➕ Tambah Alternatif Baru"): st.text_input("Nama Alternatif", key="new_alternative_name", placeholder="Masukkan nama alternatif") st.button("Tambah Alternatif", on_click=add_alternative) # Tampilkan daftar alternatif yang ada st.sidebar.subheader("📋 Daftar Alternatif") for i, alternative in enumerate(st.session_state.alternatives_list): col1, col2 = st.sidebar.columns([4, 1]) with col1: st.write(f"{alternative['name']}") with col2: if st.button("🗑", key=f"remove_alt_{i}", help="Hapus alternatif"): remove_alternative(i) st.rerun() # Extract nama alternatif alternatives = [a["name"] for a in st.session_state.alternatives_list] if not criteria or not alternatives: st.warning("Masukkan minimal satu kriteria dan alternatif.") st.stop() # Tampilkan ringkasan kriteria st.subheader("📊 Ringkasan Kriteria") criteria_df = pd.DataFrame(st.session_state.criteria_list) criteria_df.index = criteria_df.index + 1 st.dataframe(criteria_df, use_container_width=True) # TOPSIS implementation 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.") # Add Standard Scaler option st.subheader("⚙️ Pengaturan Normalisasi") use_standard_scaler = st.checkbox("Gunakan Standard Scaler", value=False, help="Standard Scaler akan menormalisasi data dengan mean=0 dan std=1") 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}' (1-10)", min_value=0.0, max_value=10.0, value=1.0, step=0.1, key=f"weight_{c}") types = np.array([1 if criteria_types[c] == "benefit" else 0 for c in criteria]) weights = np.array([weight_dict[c] for c in criteria]) weights /= weights.sum() st.write("🎯 Ringkasan Kriteria dan Bobot:") summary_df = pd.DataFrame({ "Kriteria": criteria, "Tipe": [criteria_types[c] for c in criteria], "Bobot (Ternormalisasi)": weights.round(3) }) st.dataframe(summary_df, use_container_width=True) else: # Profile Matching implementation # --- Setup Kriteria dengan Tipe --- st.sidebar.subheader("📝 Manajemen Kriteria") # Default kriteria default_criteria = [ {"name": "Building Area (m2)", "type": "benefit"}, {"name": "Road Access (m)", "type": "benefit"}, {"name": "Distance (m)", "type": "benefit"}, {"name": "Rental Price (jt Rp)", "type": "cost"} ] # Initialize session state untuk kriteria if 'criteria_list' not in st.session_state: st.session_state.criteria_list = default_criteria.copy() # Initialize pairwise comparison matrix in session state if 'pairwise_matrix' not in st.session_state: st.session_state.pairwise_matrix = {} # Fungsi untuk menambah kriteria def add_criterion(): new_criterion = { "name": st.session_state.new_criterion_name, "type": st.session_state.new_criterion_type } if new_criterion["name"] and new_criterion["name"] not in [c["name"] for c in st.session_state.criteria_list]: st.session_state.criteria_list.append(new_criterion) st.session_state.new_criterion_name = "" # Reset pairwise matrix when criteria change st.session_state.pairwise_matrix = {} # Fungsi untuk menghapus kriteria def remove_criterion(idx): if len(st.session_state.criteria_list) > 1: # Minimal 1 kriteria st.session_state.criteria_list.pop(idx) # Reset pairwise matrix when criteria change st.session_state.pairwise_matrix = {} # Form untuk menambah kriteria baru with st.sidebar.expander("➕ Tambah Kriteria Baru"): st.text_input("Nama Kriteria", key="new_criterion_name", placeholder="Masukkan nama kriteria") st.selectbox("Tipe Kriteria", ["benefit", "cost"], key="new_criterion_type", help="Benefit = semakin tinggi semakin baik, Cost = semakin rendah semakin baik") st.button("Tambah Kriteria", on_click=add_criterion) # Tampilkan daftar kriteria yang ada st.sidebar.subheader("📋 Daftar Kriteria") for i, criterion in enumerate(st.session_state.criteria_list): col1, col2, col3 = st.sidebar.columns([3, 2, 1]) with col1: st.write(f"{criterion['name']}") with col2: badge_color = "🟢" if criterion['type'] == 'benefit' else "🔴" st.write(f"{badge_color} {criterion['type']}") with col3: if st.button("🗑", key=f"remove_{i}", help="Hapus kriteria"): remove_criterion(i) st.rerun() # Extract nama kriteria dan tipe criteria = [c["name"] for c in st.session_state.criteria_list] criteria_types = {c["name"]: c["type"] for c in st.session_state.criteria_list} # --- Setup Alternatif --- st.sidebar.subheader("🏢 Manajemen Alternatif") # Default alternatif default_alternatives = [ {"name": "Location 1"}, {"name": "Location 2"}, {"name": "Location 3"}, {"name": "Location 4"} ] # Initialize session state untuk alternatif if 'alternatives_list' not in st.session_state: st.session_state.alternatives_list = default_alternatives.copy() # Fungsi untuk menambah alternatif def add_alternative(): new_alternative = {"name": st.session_state.new_alternative_name} if new_alternative["name"] and new_alternative["name"] not in [a["name"] for a in st.session_state.alternatives_list]: st.session_state.alternatives_list.append(new_alternative) st.session_state.new_alternative_name = "" # Fungsi untuk menghapus alternatif def remove_alternative(idx): if len(st.session_state.alternatives_list) > 1: # Minimal 1 alternatif st.session_state.alternatives_list.pop(idx) # Form untuk menambah alternatif baru with st.sidebar.expander("➕ Tambah Alternatif Baru"): st.text_input("Nama Alternatif", key="new_alternative_name", placeholder="Masukkan nama alternatif") st.button("Tambah Alternatif", on_click=add_alternative) # Tampilkan daftar alternatif yang ada st.sidebar.subheader("📋 Daftar Alternatif") for i, alternative in enumerate(st.session_state.alternatives_list): col1, col2 = st.sidebar.columns([4, 1]) with col1: st.write(f"{alternative['name']}") with col2: if st.button("🗑", key=f"remove_alt_{i}", help="Hapus alternatif"): remove_alternative(i) st.rerun() # Extract nama alternatif alternatives = [a["name"] for a in st.session_state.alternatives_list] if not criteria or not alternatives: st.warning("Masukkan minimal satu kriteria dan alternatif.") st.stop() # Tampilkan ringkasan kriteria st.subheader("📊 Ringkasan Kriteria") criteria_df = pd.DataFrame(st.session_state.criteria_list) criteria_df.index = criteria_df.index + 1 st.dataframe(criteria_df, use_container_width=True) 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.") # Add Standard Scaler option for Profile Matching st.subheader("⚙️ Pengaturan Normalisasi") use_standard_scaler_pm = st.checkbox("Gunakan Standard Scaler", value=False, help="Standard Scaler akan menormalisasi data dengan mean=0 dan std=1") 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}' ", min_value=1, max_value=2000, value=3, key=f"ideal_{c}") ideal_profile = pd.Series(ideal_profile_dict, index=criteria) st.write("💡 Profil Ideal:") ideal_df = pd.DataFrame({ "Kriteria": criteria, "Tipe": [criteria_types[c] for c in criteria], "Nilai Ideal": [ideal_profile_dict[c] for c in criteria] }) st.dataframe(ideal_df, use_container_width=True) 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 (0-1)", min_value=0.0, max_value=1.0, value=0.6, step=0.05) sf_weight = 1 - cf_weight st.write(f"📊 Core Factor Weight: {cf_weight:.2f}") st.write(f"📊 Secondary Factor Weight: {sf_weight:.2f}") # 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' not in locals(): st.error("Bobot kriteria AHP belum terhitung dengan benar.") else: # Calculate final AHP scores 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") result_df = final_scores_ahp.sort_values(ascending=False).to_frame("Skor Akhir") result_df['Ranking'] = range(1, len(result_df) + 1) st.dataframe(result_df, use_container_width=True) # --- PERBAIKAN GRAFIK AHP --- st.subheader("🏆 Hasil AHP - Ranking Alternatif") # Ubah data Series ke DataFrame untuk Altair chart_data = final_scores_ahp.sort_values(ascending=False).reset_index() chart_data.columns = ['Alternatif', 'Skor'] # Buat grafik dengan Altair chart = alt.Chart(chart_data).mark_bar().encode( x=alt.X('Alternatif:N', sort=None, axis=alt.Axis(labelAngle=0), title="Alternatif"), y=alt.Y('Skor:Q', title="Skor Akhir"), tooltip=['Alternatif', alt.Tooltip('Skor', format='.3f')] ).properties( title='Ranking Alternatif Berdasarkan Skor AHP' ) # Tampilkan grafik dengan st.altair_chart st.altair_chart(chart, use_container_width=True) # --- AKHIR PERBAIKAN --- st.write("🏆 Alternatif terbaik berdasarkan AHP:") st.success(f"{final_scores_ahp.idxmax()} dengan skor {final_scores_ahp.max():.3f}") elif method == "TOPSIS": if df.isnull().values.any(): st.error("Isi semua nilai alternatif terlebih dahulu.") else: data = df.values.astype(float) w = weights # Apply Standard Scaler if selected if use_standard_scaler: scaler = StandardScaler() scaled_data = scaler.fit_transform(data) st.write("📊 Data setelah Standard Scaling:") st.dataframe(pd.DataFrame(scaled_data, index=alternatives, columns=criteria), use_container_width=True) data = scaled_data # Normalisasi norm_data = data / np.sqrt((data ** 2).sum(axis=0)) weighted_data = norm_data * w # Tentukan ideal positif dan negatif berdasarkan tipe kriteria ideal_pos = np.where(types == 1, weighted_data.max(axis=0), weighted_data.min(axis=0)) ideal_neg = np.where(types == 1, weighted_data.min(axis=0), weighted_data.max(axis=0)) # 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") result_df = topsis_result.to_frame("Skor") result_df['Ranking'] = range(1, len(result_df) + 1) st.dataframe(result_df, use_container_width=True) # --- PERBAIKAN GRAFIK TOPSIS --- st.subheader("🏆 Hasil TOPSIS - Ranking Alternatif") # Ubah data Series ke DataFrame untuk Altair chart_data = topsis_result.sort_values(ascending=False).reset_index() chart_data.columns = ['Alternatif', 'Skor'] # Buat grafik dengan Altair chart = alt.Chart(chart_data).mark_bar().encode( x=alt.X('Alternatif:N', sort=None, axis=alt.Axis(labelAngle=0), title="Alternatif"), y=alt.Y('Skor:Q', title="Skor Preferensi"), tooltip=['Alternatif', alt.Tooltip('Skor', format='.3f')] ).properties( title='Ranking Alternatif Berdasarkan Skor TOPSIS' ) # Tampilkan grafik dengan st.altair_chart st.altair_chart(chart, use_container_width=True) # --- AKHIR PERBAIKAN --- st.write("🏆 Alternatif terbaik berdasarkan TOPSIS:") st.success(f"{topsis_result.idxmax()} dengan skor {topsis_result.max():.3f}") elif method == "Profile Matching": if df.isnull().values.any(): st.error("Isi semua nilai alternatif terlebih dahulu.") else: # Pastikan data dalam format yang benar df_working = df.copy() if use_standard_scaler_pm: scaler = StandardScaler() scaled_data = scaler.fit_transform(df.values.astype(float)) df_scaled = pd.DataFrame(scaled_data, index=alternatives, columns=criteria) # Scale to 1-5 range after standard scaling min_val = df_scaled.min().min() max_val = df_scaled.max().max() df_scaled = df_scaled.apply(lambda x: 1 + 4 * (x - min_val) / (max_val - min_val)).round(0).astype(int) # Scale ideal profile juga ideal_values = ideal_profile.values.reshape(1,-1) scaled_ideal = scaler.transform(ideal_values) scaled_ideal_df = pd.DataFrame(scaled_ideal, columns=criteria) # Convert scaled ideal to 1-5 range scaled_ideal_profile = scaled_ideal_df.apply(lambda x: 1 + 4 * (x - min_val) / (max_val - min_val)).round(0).astype(int) ideal_profile_working = pd.Series(scaled_ideal_profile.iloc[0], index=criteria) st.write("📊 Data setelah Standard Scaling dan konversi ke skala 1-5:") st.dataframe(df_scaled, use_container_width=True) df_working = df_scaled else: # Jika tidak menggunakan standard scaler, pastikan data dalam skala 1-5 df_working = df.copy() ideal_profile_working = ideal_profile.copy() st.write("📏 Data yang digunakan untuk Profile Matching:") st.dataframe(df_working, use_container_width=True) st.write("🎯 Ideal Profile yang digunakan:") st.write(ideal_profile_working) # Gap Analysis: Nilai Alternatif - Nilai Ideal df_gap = df_working.subtract(ideal_profile_working, axis=1) st.write("📊 Gap Analysis (Nilai Alternatif - Nilai Ideal):") st.dataframe(df_gap, use_container_width=True) # Mapping gap ke bobot gap_weights = { 0: 5, # Perfect match 1: 4.5, -1: 4.5, # Gap ±1 2: 4, -2: 4, # Gap ±2 3: 3.5, -3: 3.5, # Gap ±3 4: 3, -4: 3, # Gap ±4 5: 2.5, -5: 2.5 # Gap ±5 } # Konversi gap ke weighted gap df_wgap = df_gap.copy() for col in df_gap.columns: df_wgap[col] = df_gap[col].map(lambda x: gap_weights.get(int(x), 1)) # Default 1 jika gap > 5 st.write("⚖ Weighted Gap (Bobot berdasarkan Gap):") st.dataframe(df_wgap, use_container_width=True) # Pisahkan Core Factor dan Secondary Factor cf_cols = [c for c in criteria if c in selected_core_factors] sf_cols = [c for c in criteria if c not in selected_core_factors] st.write(f"🔴 Core Factors: {cf_cols}") st.write(f"🔵 Secondary Factors: {sf_cols}") # Hitung NCF (Nilai Core Factor) - rata-rata weighted gap untuk core factors if len(cf_cols) > 0: ncf = df_wgap[cf_cols].mean(axis=1) else: ncf = pd.Series(0, index=df_working.index) # Hitung NSF (Nilai Secondary Factor) - rata-rata weighted gap untuk secondary factors if len(sf_cols) > 0: nsf = df_wgap[sf_cols].mean(axis=1) else: nsf = pd.Series(0, index=df_working.index) st.write("📈 NCF (Core Factor Score):") st.write(ncf.round(3)) st.write("📈 NSF (Secondary Factor Score):") st.write(nsf.round(3)) # Hitung skor akhir final_scores_pm = cf_weight * ncf + sf_weight * nsf st.subheader("📈 Hasil Perhitungan Profile Matching") result_df = final_scores_pm.sort_values(ascending=False).to_frame("Skor") result_df['Ranking'] = range(1, len(result_df) + 1) st.dataframe(result_df, use_container_width=True) # Grafik hasil st.subheader("🏆 Hasil Profile Matching - Ranking Alternatif") chart_data = final_scores_pm.sort_values(ascending=False).reset_index() chart_data.columns = ['Alternatif', 'Skor'] chart = alt.Chart(chart_data).mark_bar().encode( x=alt.X('Alternatif:N', sort=None, axis=alt.Axis(labelAngle=0), title="Alternatif"), y=alt.Y('Skor:Q', title="Skor Akhir"), tooltip=['Alternatif', alt.Tooltip('Skor', format='.3f')] ).properties( title='Ranking Alternatif Berdasarkan Skor Profile Matching' ) st.altair_chart(chart, use_container_width=True) st.write("🏆 Alternatif terbaik berdasarkan Profile Matching:") st.success(f"{final_scores_pm.idxmax()} dengan skor {final_scores_pm.max():.3f}") # Tampilkan breakdown st.subheader("📊 Breakdown Perhitungan") breakdown_df = pd.DataFrame({ 'NCF (Core Factor)': ncf.round(3), 'NSF (Secondary Factor)': nsf.round(3), 'Skor Akhir': final_scores_pm.round(3) }) st.dataframe(breakdown_df, use_container_width=True)