SPKC / src /streamlit_app.py
mayy26's picture
Update src/streamlit_app.py
8736f96 verified
Raw
History Blame Contribute Delete
37.5 kB
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)