Spaces:
Sleeping
Sleeping
| 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) |