SPKC2 / src /streamlit_app.py
mayy26's picture
Update src/streamlit_app.py
2206cf6 verified
Raw
History Blame Contribute Delete
15.9 kB
import streamlit as st
import pandas as pd
import numpy as np
st.set_page_config(page_title="SPK Pemilihan Cabang untuk Lokasi Coffeeshop", layout="wide")
st.title("Sistem Pendukung Keputusan Pemilihan Cabang untuk Lokasi Coffeeshop")
st.markdown("Metode: AHP, TOPSIS, dan Profile Matching")
# --- Step 1: Pilih Metode ---
st.sidebar.header("βš™ Pengaturan")
method = st.selectbox("Pilih Metode", ["None","TOPSIS", "Profile Matching", "AHP"], key="selected_method")
criteria_default = "Building Area, Road Access, Distance, Rental Price"
criteria_input = st.sidebar.text_area("Masukkan Kriteria (pisahkan dengan koma)", criteria_default)
criteria = [c.strip() for c in criteria_input.split(",") if c.strip()]
alternatives_default = "Location 1, Location 2, Location 3, Location 4"
alternatives_input = st.sidebar.text_area("Masukkan Alternatif (pisahkan dengan koma)", alternatives_default)
alternatives = [a.strip() for a in alternatives_input.split(",") if a.strip()]
if not criteria or not alternatives:
st.warning("Masukkan minimal satu kriteria dan alternatif.")
st.stop()
# ---------------------------
if method == "AHP":
st.subheader("Perbandingan Berpasangan Antar Kriteria (AHP)")
pairwise_criteria_data = np.ones((len(criteria), len(criteria)))
for i in range(len(criteria)):
for j in range(i + 1, len(criteria)):
pairwise_criteria_data[i, j] = 1.0
pairwise_criteria_df = pd.DataFrame(pairwise_criteria_data, index=criteria, columns=criteria)
edited_pairwise_criteria_matrix = st.data_editor(
pairwise_criteria_df,
use_container_width=True,
key="pairwise_comparison_criteria_matrix",
hide_index=False,
)
# Reciprocity for criteria matrix
final_pairwise_criteria_matrix = edited_pairwise_criteria_matrix.copy()
for i in range(len(criteria)):
for j in range(i + 1, len(criteria)):
val = final_pairwise_criteria_matrix.iloc[i, j]
if val == 0:
val = 1e-9
final_pairwise_criteria_matrix.iloc[j, i] = 1 / val
final_pairwise_criteria_matrix.iloc[i, i] = 1.0
# Hitung bobot kriteria AHP
weights_ahp_criteria = pd.Series(dtype='float64')
if not final_pairwise_criteria_matrix.apply(pd.to_numeric, errors='coerce').isnull().values.any():
try:
numeric_criteria_matrix = final_pairwise_criteria_matrix.apply(pd.to_numeric)
norm_matrix = numeric_criteria_matrix / numeric_criteria_matrix.sum()
weights_ahp_criteria = norm_matrix.mean(axis=1)
weights_ahp_criteria /= weights_ahp_criteria.sum()
st.write("Bobot Kriteria dari AHP:")
st.dataframe(weights_ahp_criteria.round(3).to_frame(name="Bobot AHP"))
RI = {1: 0.00, 2: 0.00, 3: 0.58, 4: 0.90, 5: 1.12, 6: 1.24, 7: 1.32, 8: 1.41, 9: 1.45, 10: 1.49}
n_criteria = len(criteria)
# Weighted Sum Vector (WSV)
wsv = numeric_criteria_matrix.dot(weights_ahp_criteria)
# Ξ»_max manual: rata-rata dari WSV[i] / W[i]
lambda_max_criteria = (wsv / weights_ahp_criteria).mean()
# CI manual
CI_criteria = (lambda_max_criteria - n_criteria) / (n_criteria - 1) if n_criteria > 1 else 0
CR_criteria = np.nan
if n_criteria in RI and RI[n_criteria] > 0:
CR_criteria = CI_criteria / RI[n_criteria]
st.subheader("Analisis Konsistensi Kriteria AHP")
st.write(f"$\\lambda_{{max}} = {lambda_max_criteria:.10f}$")
st.write(f"CI = {CI_criteria:.10f}")
if not np.isnan(CR_criteria):
st.write(f"CR = {CR_criteria:.10f}")
if CR_criteria < 0.1:
st.success("Konsistensi Rasio Kriteria AHP *BAIK* (< 0.1)")
else:
st.error("Konsistensi Rasio Kriteria AHP *BURUK* (>= 0.1). Harap sesuaikan perbandingan kriteria.")
else:
st.info("CR kriteria tidak dapat dihitung (untuk 1 kriteria atau RI tidak tersedia).")
except Exception as e:
st.error(f"Terjadi kesalahan saat menghitung bobot kriteria AHP: {e}")
st.warning("Pastikan semua nilai pada matriks perbandingan kriteria adalah numerik.")
else:
st.warning("Matriks perbandingan kriteria belum lengkap atau tidak valid.")
# --- Perbandingan berpasangan antar alternatif untuk setiap kriteria ---
st.subheader("πŸ”— Perbandingan Berpasangan Antar Alternatif untuk Setiap Kriteria (AHP)")
alternative_pairwise_matrices = {}
alternative_weights_per_criterion = {}
for criterion in criteria:
st.markdown(f"### Kriteria: *{criterion}*")
pairwise_alt_data = np.ones((len(alternatives), len(alternatives)))
for i in range(len(alternatives)):
for j in range(i + 1, len(alternatives)):
pairwise_alt_data[i, j] = 1.0
pairwise_alt_df = pd.DataFrame(pairwise_alt_data, index=alternatives, columns=alternatives)
st.markdown(f"Isi perbandingan berpasangan alternatif untuk *{criterion}* (misal: 3 berarti alternatif baris 3x lebih baik dari alternatif kolom terhadap {criterion}).")
st.markdown("Nilai akan otomatis disesuaikan secara resiprokal saat perhitungan.")
edited_pairwise_alt_matrix = st.data_editor(
pairwise_alt_df,
use_container_width=True,
key=f"pairwise_comparison_alternatives_matrix_{criterion}",
hide_index=False,
)
final_pairwise_alt_matrix = edited_pairwise_alt_matrix.copy()
for i in range(len(alternatives)):
for j in range(i + 1, len(alternatives)):
val = final_pairwise_alt_matrix.iloc[i, j]
if val == 0:
val = 1e-9
final_pairwise_alt_matrix.iloc[j, i] = 1 / val
final_pairwise_alt_matrix.iloc[i, i] = 1.0
alternative_pairwise_matrices[criterion] = final_pairwise_alt_matrix
# Hitung bobot alternatif untuk criterion ini
try:
numeric_alt_matrix = final_pairwise_alt_matrix.apply(pd.to_numeric)
norm_matrix = numeric_alt_matrix / numeric_alt_matrix.sum()
weights_alternatives = norm_matrix.mean(axis=1)
weights_alternatives /= weights_alternatives.sum()
alternative_weights_per_criterion[criterion] = weights_alternatives
st.write(f"Bobot alternatif untuk kriteria *{criterion}*:")
st.dataframe(weights_alternatives.round(3).to_frame(name=f"Bobot alternatif ({criterion})"))
except Exception as e:
st.error(f"Error menghitung bobot alternatif pada kriteria {criterion}: {e}")
elif method == "None":
st.subheader("Pilih Metode terlebih dahulu")
elif method =="TOPSIS":
# TOPSIS
st.subheader("Input Nilai Alternatif terhadap Kriteria")
empty_data = pd.DataFrame(np.nan, index=alternatives, columns=criteria)
df = st.data_editor(empty_data, use_container_width=True, key="input_matrix")
if df.isnull().values.any():
st.warning("⚠ Harap lengkapi semua nilai pada tabel sebelum menjalankan perhitungan untuk TOPSIS/Profile Matching.")
criteria_default_type= {
"Building Area" : "benefit",
"Road Access": "benefit",
"Distance" : "benefit",
"Rental Price" : "cost"
}
st.subheader("Bobot Kriteria (Manual)")
weight_dict = {}
cols = st.columns(len(criteria))
for i, c in enumerate(criteria):
with cols[i]:
weight_dict[c] = st.number_input(f"Bobot untuk '{c}' dengan rentang 1-5", min_value=0.0, max_value=10.0, value=0.0, step=0.1, key=f"weight_{c}")
types = np.array([1 if criteria_default_type.get(c,"benefit") == "benefit" else 0 for c in criteria])
weights = np.array([weight_dict[c] for c in criteria])
weights /= weights.sum()
st.write("Bobot Kriteria (Ternormalisasi):")
# Gabungkan ke dalam DataFrame
summary_df = pd.DataFrame({
"Kriteria": criteria,
"Bobot (Ternormalisasi)": weights,
"Tipe ": types
})
# Tampilkan
st.write("Tabel Kriteria, Bobot, dan Tipe:")
st.dataframe(summary_df)
#st.dataframe(pd.Series(weights, index=criteria, name="Bobot", ))
else:
#Profile Matching
st.subheader("Input Nilai Alternatif terhadap Kriteria")
empty_data = pd.DataFrame(np.nan, index=alternatives, columns=criteria)
df = st.data_editor(empty_data, use_container_width=True, key="input_matrix")
if df.isnull().values.any():
st.warning("Harap lengkapi semua nilai pada tabel sebelum menjalankan perhitungan untuk TOPSIS/Profile Matching.")
st.subheader("Ideal Profile (untuk Profile Matching)")
ideal_profile_dict = {}
cols = st.columns(len(criteria))
for i, c in enumerate(criteria):
with cols[i]:
ideal_profile_dict[c] = st.number_input(f"Ideal '{c}' (1-5)", min_value=1, max_value=5, value=5, key=f"ideal_{c}")
ideal_profile = pd.Series(ideal_profile_dict, index=criteria)
st.write("Profil Ideal:")
st.dataframe(ideal_profile.to_frame(name="Nilai Ideal"))
st.subheader("βš™ Pengaturan Faktor (untuk Profile Matching)")
core_factors_options = criteria
default_core_factors = [criteria[-1]] if criteria else []
selected_core_factors = st.multiselect(
"Pilih Kriteria Core Factor (Faktor Inti)",
options=core_factors_options,
default=default_core_factors,
help="Kriteria yang dianggap paling penting. Sisanya akan menjadi Secondary Factor.",
key="pm_core_factors"
)
if not selected_core_factors:
st.warning("Setidaknya satu Core Factor harus dipilih untuk Profile Matching.")
st.stop()
cf_weight = st.number_input(f"Bobot Core Factor", min_value=0.0, max_value=1.0, value=0.6, step=0.05)
sf_weight = 1 - cf_weight
# Tombol jalankan perhitungan
run_calc = st.button("Jalankan Perhitungan")
if run_calc:
if method == "AHP":
if len(alternative_weights_per_criterion) != len(criteria):
st.error("Perbandingan alternatif belum lengkap untuk semua kriteria. Lengkapi terlebih dahulu.")
elif weights_ahp_criteria.empty:
st.error("Bobot kriteria AHP belum terhitung dengan benar.")
else:
# Hitung skor akhir AHP per alternatif = sum bobot kriteria * bobot alternatif pada kriteria
final_scores_ahp = pd.Series(0.0, index=alternatives)
for c in criteria:
w_crit = weights_ahp_criteria.get(c, 0)
w_alts = alternative_weights_per_criterion.get(c, pd.Series(0, index=alternatives))
final_scores_ahp += w_crit * w_alts
st.subheader("Hasil Perhitungan AHP")
st.write(final_scores_ahp.sort_values(ascending=False).to_frame("Skor Akhir"))
st.write("*Alternatif terbaik berdasarkan AHP:*")
st.success(final_scores_ahp.idxmax())
elif method == "TOPSIS":
if df.isnull().values.any():
st.error("Isi semua nilai alternatif terlebih dahulu.")
else:
data = df.values.astype(float)
w = weights
# Normalisasi
norm_data = data / np.sqrt((data ** 2).sum(axis=0))
# Bobot
weighted_data = norm_data * w
# Tentukan ideal positif dan negatif (max/min)
# ideal_pos = np.max(weighted_data, axis=0)
# ideal_neg = np.min(weighted_data, axis=0)
ideal_pos = np.where(types == 1, weighted_data.max(), weighted_data.min())
ideal_neg = np.where(types == 1, weighted_data.min(), weighted_data.max())
# Jarak ke ideal positif dan negatif
dist_pos = np.sqrt(((weighted_data - ideal_pos) ** 2).sum(axis=1))
dist_neg = np.sqrt(((weighted_data - ideal_neg) ** 2).sum(axis=1))
# Skor preferensi
scores_topsis = dist_neg / (dist_pos + dist_neg)
topsis_result = pd.Series(scores_topsis, index=alternatives).sort_values(ascending=False)
st.subheader("Hasil Perhitungan TOPSIS")
st.write(topsis_result.to_frame("Skor"))
st.write("*Alternatif terbaik berdasarkan TOPSIS:*")
st.success(topsis_result.idxmax())
elif method == "Profile Matching":
if df.isnull().values.any():
st.error("Isi semua nilai alternatif terlebih dahulu.")
else:
def scale_row(row):
scaled = {}
# Luas Bangunan (semakin besar semakin baik)
if row["Building Area"] >= 90:
scaled["Building Area"] = 5
elif row["Building Area"] >= 89:
scaled["Building Area"] = 4
elif row["Building Area"] >= 70:
scaled["Building Area"] = 3
elif row["Building Area"] >= 60:
scaled["Building Area"] = 2
else:
scaled["Building Area"] = 1
# Akses Jalan (semakin besar semakin baik)
if row["Road Access"] > 1500:
scaled["Road Access"] = 1
elif row["Road Access"] > 1000:
scaled["Road Access"] = 2
elif row["Road Access"] > 800:
scaled["Road Access"] = 3
elif row["Road Access"] > 500:
scaled["Road Access"] = 4
else:
scaled["Road Access"] = 5
# Jarak ke Pusat Keramaian (semakin kecil semakin baik)
if row["Distance"] >= 1500:
scaled["Distance"] = 1
elif row["Distance"] >= 1000:
scaled["Distance"] = 2
elif row["Distance"] >= 800:
scaled["Distance"] = 3
elif row["Distance"] >= 500:
scaled["Distance"] = 4
else:
scaled["Distance"] = 5
# Harga Sewa (semakin kecil semakin baik)
if row["Rental Price"] >= 90:
scaled["Rental Price"] = 1
elif row["Rental Price"] >= 70:
scaled["Rental Price"] = 2
elif row["Rental Price"] >= 60:
scaled["Rental Price"] = 3
elif row["Rental Price"] >= 50:
scaled["Rental Price"] = 4
else:
scaled["Rental Price"] = 5
return pd.Series(scaled)
df_scaled = df.apply(scale_row, axis=1)
st.write("πŸ“ Data Skala 1–5", df_scaled)
# Get core factor indices
cf_indices = [criteria.index(c) for c in selected_core_factors]
# Run profile matching with the improved function
gap_weights = {
0: 5, 1: 4.5, -1: 4.5, 2: 4, -2: 4,
3: 3.5, -3: 3.5, 4: 3, -4: 3, 5: 2.5, -5: 2.5
}
df_gap = df_scaled - ideal_profile
df_wgap = df_gap.applymap(lambda x: gap_weights.get(int(x), 0))
cf_cols = df_scaled.columns[cf_indices]
sf_cols = df_scaled.columns.drop(cf_cols)
ncf = df_wgap[cf_cols].mean(axis=1)
nsf = df_wgap[sf_cols].mean(axis=1) if len(sf_cols) > 0 else 0
final_scores_pm = cf_weight * ncf + sf_weight * nsf
st.subheader("πŸ“ˆ Hasil Perhitungan Profile Matching")
st.write(final_scores_pm.sort_values(ascending=False).to_frame("Skor"))
st.write("*Alternatif terbaik berdasarkan Profile Matching:*")
st.success(final_scores_pm.idxmax())