mayy26 commited on
Commit
e95d3a0
ยท
verified ยท
1 Parent(s): a8decc0

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +402 -38
src/streamlit_app.py CHANGED
@@ -1,40 +1,404 @@
1
- import altair as alt
2
- import numpy as np
3
- import pandas as pd
4
  import streamlit as st
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
- """
7
- # Welcome to Streamlit!
8
-
9
- Edit `/streamlit_app.py` to customize this app to your heart's desire :heart:.
10
- If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
11
- forums](https://discuss.streamlit.io).
12
-
13
- In the meantime, below is an example of what you can do with just a few lines of code:
14
- """
15
-
16
- num_points = st.slider("Number of points in spiral", 1, 10000, 1100)
17
- num_turns = st.slider("Number of turns in spiral", 1, 300, 31)
18
-
19
- indices = np.linspace(0, 1, num_points)
20
- theta = 2 * np.pi * num_turns * indices
21
- radius = indices
22
-
23
- x = radius * np.cos(theta)
24
- y = radius * np.sin(theta)
25
-
26
- df = pd.DataFrame({
27
- "x": x,
28
- "y": y,
29
- "idx": indices,
30
- "rand": np.random.randn(num_points),
31
- })
32
-
33
- st.altair_chart(alt.Chart(df, height=700, width=700)
34
- .mark_point(filled=True)
35
- .encode(
36
- x=alt.X("x", axis=None),
37
- y=alt.Y("y", axis=None),
38
- color=alt.Color("idx", legend=None, scale=alt.Scale()),
39
- size=alt.Size("rand", legend=None, scale=alt.Scale(range=[1, 150])),
40
- ))
 
 
 
 
1
  import streamlit as st
2
+ import pandas as pd
3
+ import numpy as np
4
+
5
+ st.set_page_config(page_title="SPK Pemilihan Cabang untuk Lokasi Coffeeshop", layout="wide")
6
+
7
+ st.title("๐Ÿ“Š Sistem Pendukung Keputusan Pemilihan Cabang untuk Lokasi Coffeeshop")
8
+ st.markdown("Metode: AHP, TOPSIS, dan Profile Matching")
9
+
10
+ # --- Step 1: Pilih Metode ---
11
+ st.sidebar.header("โš™ Pengaturan")
12
+ method = st.selectbox("๐Ÿง  Pilih Metode", ["None","TOPSIS", "Profile Matching", "AHP"], key="selected_method")
13
+
14
+ criteria_default = "Building Area, Road Access, Distance, Rental Price"
15
+ criteria_input = st.sidebar.text_area("Masukkan Kriteria (pisahkan dengan koma)", criteria_default)
16
+ criteria = [c.strip() for c in criteria_input.split(",") if c.strip()]
17
+
18
+ alternatives_default = "Location 1, Location 2, Location 3, Location 4"
19
+ alternatives_input = st.sidebar.text_area("Masukkan Alternatif (pisahkan dengan koma)", alternatives_default)
20
+ alternatives = [a.strip() for a in alternatives_input.split(",") if a.strip()]
21
+
22
+ if not criteria or not alternatives:
23
+ st.warning("Masukkan minimal satu kriteria dan alternatif.")
24
+ st.stop()
25
+
26
+ # ---------------------------
27
+ if method == "AHP":
28
+ st.subheader("๐Ÿ”— Perbandingan Berpasangan Antar Kriteria (AHP)")
29
+
30
+ pairwise_criteria_data = np.ones((len(criteria), len(criteria)))
31
+ for i in range(len(criteria)):
32
+ for j in range(i + 1, len(criteria)):
33
+ pairwise_criteria_data[i, j] = 1.0
34
+
35
+ pairwise_criteria_df = pd.DataFrame(pairwise_criteria_data, index=criteria, columns=criteria)
36
+
37
+ edited_pairwise_criteria_matrix = st.data_editor(
38
+ pairwise_criteria_df,
39
+ use_container_width=True,
40
+ key="pairwise_comparison_criteria_matrix",
41
+ hide_index=False,
42
+ )
43
+
44
+ # Reciprocity for criteria matrix
45
+ final_pairwise_criteria_matrix = edited_pairwise_criteria_matrix.copy()
46
+ for i in range(len(criteria)):
47
+ for j in range(i + 1, len(criteria)):
48
+ val = final_pairwise_criteria_matrix.iloc[i, j]
49
+ if val == 0:
50
+ val = 1e-9
51
+ final_pairwise_criteria_matrix.iloc[j, i] = 1 / val
52
+ final_pairwise_criteria_matrix.iloc[i, i] = 1.0
53
+
54
+ # Hitung bobot kriteria AHP
55
+ weights_ahp_criteria = pd.Series(dtype='float64')
56
+ if not final_pairwise_criteria_matrix.apply(pd.to_numeric, errors='coerce').isnull().values.any():
57
+ try:
58
+ numeric_criteria_matrix = final_pairwise_criteria_matrix.apply(pd.to_numeric)
59
+ norm_matrix = numeric_criteria_matrix / numeric_criteria_matrix.sum()
60
+ weights_ahp_criteria = norm_matrix.mean(axis=1)
61
+ weights_ahp_criteria /= weights_ahp_criteria.sum()
62
+
63
+ st.write("๐ŸŽฏ Bobot Kriteria dari AHP:")
64
+ st.dataframe(weights_ahp_criteria.round(3).to_frame(name="Bobot AHP"))
65
+
66
+ 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}
67
+ n_criteria = len(criteria)
68
+ # Weighted Sum Vector (WSV)
69
+ wsv = numeric_criteria_matrix.dot(weights_ahp_criteria)
70
+ # ฮป_max manual: rata-rata dari WSV[i] / W[i]
71
+ lambda_max_criteria = (wsv / weights_ahp_criteria).mean()
72
+ # CI manual
73
+ CI_criteria = (lambda_max_criteria - n_criteria) / (n_criteria - 1) if n_criteria > 1 else 0
74
+
75
+ CR_criteria = np.nan
76
+ if n_criteria in RI and RI[n_criteria] > 0:
77
+ CR_criteria = CI_criteria / RI[n_criteria]
78
+
79
+ st.subheader("โœ… Analisis Konsistensi Kriteria AHP")
80
+ st.write(f"$\\lambda_{{max}} = {lambda_max_criteria:.10f}$")
81
+ st.write(f"CI = {CI_criteria:.10f}")
82
+ if not np.isnan(CR_criteria):
83
+ st.write(f"CR = {CR_criteria:.10f}")
84
+ if CR_criteria < 0.1:
85
+ st.success("Konsistensi Rasio Kriteria AHP *BAIK* (< 0.1)")
86
+ else:
87
+ st.error("Konsistensi Rasio Kriteria AHP *BURUK* (>= 0.1). Harap sesuaikan perbandingan kriteria.")
88
+ else:
89
+ st.info("CR kriteria tidak dapat dihitung (untuk 1 kriteria atau RI tidak tersedia).")
90
+
91
+ except Exception as e:
92
+ st.error(f"Terjadi kesalahan saat menghitung bobot kriteria AHP: {e}")
93
+ st.warning("Pastikan semua nilai pada matriks perbandingan kriteria adalah numerik.")
94
+ else:
95
+ st.warning("Matriks perbandingan kriteria belum lengkap atau tidak valid.")
96
+
97
+ # --- Perbandingan berpasangan antar alternatif untuk setiap kriteria ---
98
+ st.subheader("๐Ÿ”— Perbandingan Berpasangan Antar Alternatif untuk Setiap Kriteria (AHP)")
99
+
100
+ alternative_pairwise_matrices = {}
101
+ alternative_weights_per_criterion = {}
102
+
103
+ for criterion in criteria:
104
+ st.markdown(f"### Kriteria: *{criterion}*")
105
+
106
+ pairwise_alt_data = np.ones((len(alternatives), len(alternatives)))
107
+ for i in range(len(alternatives)):
108
+ for j in range(i + 1, len(alternatives)):
109
+ pairwise_alt_data[i, j] = 1.0
110
+
111
+ pairwise_alt_df = pd.DataFrame(pairwise_alt_data, index=alternatives, columns=alternatives)
112
+
113
+ st.markdown(f"Isi perbandingan berpasangan alternatif untuk *{criterion}* (misal: 3 berarti alternatif baris 3x lebih baik dari alternatif kolom terhadap {criterion}).")
114
+ st.markdown("Nilai akan otomatis disesuaikan secara resiprokal saat perhitungan.")
115
+
116
+ edited_pairwise_alt_matrix = st.data_editor(
117
+ pairwise_alt_df,
118
+ use_container_width=True,
119
+ key=f"pairwise_comparison_alternatives_matrix_{criterion}",
120
+ hide_index=False,
121
+ )
122
+
123
+ final_pairwise_alt_matrix = edited_pairwise_alt_matrix.copy()
124
+ for i in range(len(alternatives)):
125
+ for j in range(i + 1, len(alternatives)):
126
+ val = final_pairwise_alt_matrix.iloc[i, j]
127
+ if val == 0:
128
+ val = 1e-9
129
+ final_pairwise_alt_matrix.iloc[j, i] = 1 / val
130
+ final_pairwise_alt_matrix.iloc[i, i] = 1.0
131
+
132
+ alternative_pairwise_matrices[criterion] = final_pairwise_alt_matrix
133
+
134
+ # Hitung bobot alternatif untuk criterion ini
135
+ try:
136
+ numeric_alt_matrix = final_pairwise_alt_matrix.apply(pd.to_numeric)
137
+ norm_matrix = numeric_alt_matrix / numeric_alt_matrix.sum()
138
+ weights_alternatives = norm_matrix.mean(axis=1)
139
+ weights_alternatives /= weights_alternatives.sum()
140
+ alternative_weights_per_criterion[criterion] = weights_alternatives
141
+ st.write(f"Bobot alternatif untuk kriteria *{criterion}*:")
142
+ st.dataframe(weights_alternatives.round(3).to_frame(name=f"Bobot alternatif ({criterion})"))
143
+ except Exception as e:
144
+ st.error(f"Error menghitung bobot alternatif pada kriteria {criterion}: {e}")
145
+
146
+ elif method == "None":
147
+ st.subheader("Pilih Metode terlebih dahulu")
148
+ elif method =="TOPSIS":
149
+ # TOPSIS
150
+ st.subheader("๐Ÿ“‹ Input Nilai Alternatif terhadap Kriteria")
151
+ empty_data = pd.DataFrame(np.nan, index=alternatives, columns=criteria)
152
+ df = st.data_editor(empty_data, use_container_width=True, key="input_matrix")
153
+ if df.isnull().values.any():
154
+ st.warning("โš  Harap lengkapi semua nilai pada tabel sebelum menjalankan perhitungan untuk TOPSIS/Profile Matching.")
155
+
156
+ criteria_default_type= {
157
+ "Building Area" : "benefit",
158
+ "Road Access": "benefit",
159
+ "Distance" : "benefit",
160
+ "Rental Price" : "cost"
161
+ }
162
+
163
+ st.subheader("โš– Bobot Kriteria (Manual)")
164
+ weight_dict = {}
165
+ cols = st.columns(len(criteria))
166
+ for i, c in enumerate(criteria):
167
+ with cols[i]:
168
+ 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}")
169
+
170
+ types = np.array([1 if criteria_default_type.get(c,"benefit") == "benefit" else 0 for c in criteria])
171
+
172
+
173
+ weights = np.array([weight_dict[c] for c in criteria])
174
+ weights /= weights.sum()
175
+ st.write("๐ŸŽฏ Bobot Kriteria (Ternormalisasi):")
176
+ # Gabungkan ke dalam DataFrame
177
+ summary_df = pd.DataFrame({
178
+ "Kriteria": criteria,
179
+ "Bobot (Ternormalisasi)": weights,
180
+ "Tipe ": types
181
+ })
182
+
183
+ # Tampilkan
184
+ st.write("๐Ÿ“‹ Tabel Kriteria, Bobot, dan Tipe:")
185
+ st.dataframe(summary_df)
186
+ #st.dataframe(pd.Series(weights, index=criteria, name="Bobot", ))
187
+
188
+
189
+ else:
190
+ #Profile Matching
191
+ st.subheader("๐Ÿ“‹ Input Nilai Alternatif terhadap Kriteria")
192
+ empty_data = pd.DataFrame(np.nan, index=alternatives, columns=criteria)
193
+ df = st.data_editor(empty_data, use_container_width=True, key="input_matrix")
194
+ if df.isnull().values.any():
195
+ st.warning("โš  Harap lengkapi semua nilai pada tabel sebelum menjalankan perhitungan untuk TOPSIS/Profile Matching.")
196
+
197
+ # st.subheader("โš– Bobot Kriteria (Manual)")
198
+ # weight_dict = {}
199
+ # cols = st.columns(len(criteria))
200
+ # for i, c in enumerate(criteria):
201
+ # with cols[i]:
202
+ # weight_dict[c] = st.number_input(f"Bobot untuk '{c}'", min_value=0.0, max_value=10.0, value=0.0, step=0.1, key=f"weight_{c}")
203
+
204
+ # weights = np.array([weight_dict[c] for c in criteria])
205
+ # weights /= weights.sum()
206
+ # st.write("๐ŸŽฏ Bobot Kriteria (Ternormalisasi):")
207
+ # st.dataframe(pd.Series(weights, index=criteria, name="Bobot"))
208
+
209
+ #if method == "Profile Matching":
210
+ st.subheader("๐ŸŽฏ Ideal Profile (untuk Profile Matching)")
211
+ ideal_profile_dict = {}
212
+ cols = st.columns(len(criteria))
213
+ for i, c in enumerate(criteria):
214
+ with cols[i]:
215
+ ideal_profile_dict[c] = st.number_input(f"Ideal '{c}' (1-5)", min_value=0, max_value=5, value=0, key=f"ideal_{c}")
216
+ ideal_profile = pd.Series(ideal_profile_dict, index=criteria)
217
+ st.write("๐Ÿ’ก Profil Ideal:")
218
+ st.dataframe(ideal_profile.to_frame(name="Nilai Ideal"))
219
+
220
+ st.subheader("โš™ Pengaturan Faktor (untuk Profile Matching)")
221
+ core_factors_options = criteria
222
+ default_core_factors = [criteria[-1]] if criteria else []
223
+
224
+ selected_core_factors = st.multiselect(
225
+ "Pilih Kriteria Core Factor (Faktor Inti)",
226
+ options=core_factors_options,
227
+ default=default_core_factors,
228
+ help="Kriteria yang dianggap paling penting. Sisanya akan menjadi Secondary Factor.",
229
+ key="pm_core_factors"
230
+ )
231
+
232
+ if not selected_core_factors:
233
+ st.warning("Setidaknya satu Core Factor harus dipilih untuk Profile Matching.")
234
+ st.stop()
235
+
236
+ cf_weight = st.number_input(f"Bobot Core Factor", min_value=0.0, max_value=1.0, value=0.0, step=0.05)
237
+ sf_weight = 1 - cf_weight
238
+
239
+ # Tombol jalankan perhitungan
240
+ run_calc = st.button("โ–ถ Jalankan Perhitungan")
241
+
242
+ if run_calc:
243
+ if method == "AHP":
244
+ if len(alternative_weights_per_criterion) != len(criteria):
245
+ st.error("Perbandingan alternatif belum lengkap untuk semua kriteria. Lengkapi terlebih dahulu.")
246
+ elif weights_ahp_criteria.empty:
247
+ st.error("Bobot kriteria AHP belum terhitung dengan benar.")
248
+ else:
249
+ # Hitung skor akhir AHP per alternatif = sum bobot kriteria * bobot alternatif pada kriteria
250
+ final_scores_ahp = pd.Series(0.0, index=alternatives)
251
+ for c in criteria:
252
+ w_crit = weights_ahp_criteria.get(c, 0)
253
+ w_alts = alternative_weights_per_criterion.get(c, pd.Series(0, index=alternatives))
254
+ final_scores_ahp += w_crit * w_alts
255
+
256
+ st.subheader("๐Ÿ“ˆ Hasil Perhitungan AHP")
257
+ st.write(final_scores_ahp.sort_values(ascending=False).to_frame("Skor Akhir"))
258
+ st.write("*Alternatif terbaik berdasarkan AHP:*")
259
+ st.success(final_scores_ahp.idxmax())
260
+
261
+ elif method == "TOPSIS":
262
+ if df.isnull().values.any():
263
+ st.error("Isi semua nilai alternatif terlebih dahulu.")
264
+ else:
265
+ data = df.values.astype(float)
266
+ w = weights
267
+
268
+ # Normalisasi
269
+ norm_data = data / np.sqrt((data ** 2).sum(axis=0))
270
+
271
+ # Bobot
272
+ weighted_data = norm_data * w
273
+
274
+ # Tentukan ideal positif dan negatif (max/min)
275
+ # ideal_pos = np.max(weighted_data, axis=0)
276
+ # ideal_neg = np.min(weighted_data, axis=0)
277
+ ideal_pos = np.where(types == 1, weighted_data.max(), weighted_data.min())
278
+ ideal_neg = np.where(types == 1, weighted_data.min(), weighted_data.max())
279
+
280
+ # Jarak ke ideal positif dan negatif
281
+ dist_pos = np.sqrt(((weighted_data - ideal_pos) ** 2).sum(axis=1))
282
+ dist_neg = np.sqrt(((weighted_data - ideal_neg) ** 2).sum(axis=1))
283
+
284
+ # Skor preferensi
285
+ scores_topsis = dist_neg / (dist_pos + dist_neg)
286
+
287
+ topsis_result = pd.Series(scores_topsis, index=alternatives).sort_values(ascending=False)
288
+
289
+ st.subheader("๐Ÿ“ˆ Hasil Perhitungan TOPSIS")
290
+ st.write(topsis_result.to_frame("Skor"))
291
+ st.write("*Alternatif terbaik berdasarkan TOPSIS:*")
292
+ st.success(topsis_result.idxmax())
293
+
294
+ elif method == "Profile Matching":
295
+ if df.isnull().values.any():
296
+ st.error("Isi semua nilai alternatif terlebih dahulu.")
297
+ else:
298
+ # 1. Scaling nilai ke skala 1โ€“5
299
+ def scale_to_five(val):
300
+ if val >= 90: return 5
301
+ elif val >= 80: return 4
302
+ elif val >= 70: return 3
303
+ elif val >= 60: return 2
304
+ elif val >= 50: return 1
305
+ else: return 1 # default minimum
306
+
307
+ df_scaled = df.astype(float).applymap(scale_to_five)
308
+
309
+ # 2. Ambil profil ideal yang dimasukkan user (sudah dalam skala 1โ€“5)
310
+ ideal_profile = pd.Series(ideal_profile_dict)
311
+
312
+ # 3. Hitung GAP
313
+ gaps = df_scaled.subtract(ideal_profile, axis=1)
314
+
315
+ # 4. Konversi GAP ke bobot nilai berdasarkan tabel bobot GAP
316
+ def gap_weight(gap):
317
+ gap_table = {
318
+ 0: 5,
319
+ -1: 4.5, 1: 4.5,
320
+ -2: 4, 2: 4,
321
+ -3: 3.5, 3: 3.5,
322
+ -4: 3, 4: 3,
323
+ -5: 2.5, 5: 2.5,
324
+ }
325
+ return gap_table.get(int(gap), 2) # jika di luar -5 hingga 5, berikan nilai default 2
326
+
327
+ weight_gaps = gaps.applymap(gap_weight)
328
+
329
+ # 5. Hitung nilai rata-rata core factor dan secondary factor
330
+ core_factors = selected_core_factors
331
+ secondary_factors = [c for c in criteria if c not in core_factors]
332
+
333
+ cf_scores = weight_gaps[core_factors].mean(axis=1)
334
+ sf_scores = weight_gaps[secondary_factors].mean(axis=1) if secondary_factors else 0
335
+
336
+ # 6. Hitung skor total berdasarkan bobot CF dan SF
337
+ final_scores_pm = cf_weight * cf_scores + sf_weight * sf_scores
338
+
339
+ # 7. Tampilkan hasil
340
+ st.subheader("๐Ÿ“ˆ Hasil Perhitungan Profile Matching")
341
+ st.write("๐Ÿ“ Skala 1โ€“5:")
342
+ st.dataframe(df_scaled)
343
+ st.write("๐Ÿงฎ GAP Weight:")
344
+ st.dataframe(weight_gaps)
345
+ st.write("๐Ÿ“Š Skor Akhir:")
346
+ st.dataframe(final_scores_pm.sort_values(ascending=False).to_frame("Skor"))
347
+ st.write("*Alternatif terbaik berdasarkan Profile Matching:*")
348
+ st.success(final_scores_pm.idxmax())
349
+
350
+ elif method == "Profile Matching":
351
+ if df.isnull().values.any():
352
+ st.error("Isi semua nilai alternatif terlebih dahulu.")
353
+ else:
354
+ # 1. Scaling nilai ke skala 1โ€“5
355
+ def scale_to_five(val):
356
+ if val >= 90: return 5
357
+ elif val >= 80: return 4
358
+ elif val >= 70: return 3
359
+ elif val >= 60: return 2
360
+ elif val >= 50: return 1
361
+ else: return 1 # default minimum
362
+
363
+ df_scaled = df.astype(float).applymap(scale_to_five)
364
+
365
+ # 2. Ambil profil ideal yang dimasukkan user (sudah dalam skala 1โ€“5)
366
+ ideal_profile = pd.Series(ideal_profile_dict)
367
+
368
+ # 3. Hitung GAP
369
+ gaps = df_scaled.subtract(ideal_profile, axis=1)
370
+
371
+ # 4. Konversi GAP ke bobot nilai berdasarkan tabel bobot GAP
372
+ def gap_weight(gap):
373
+ gap_table = {
374
+ 0: 5,
375
+ -1: 4.5, 1: 4.5,
376
+ -2: 4, 2: 4,
377
+ -3: 3.5, 3: 3.5,
378
+ -4: 3, 4: 3,
379
+ -5: 2.5, 5: 2.5,
380
+ }
381
+ return gap_table.get(int(gap), 2) # jika di luar -5 hingga 5, berikan nilai default 2
382
+
383
+ weight_gaps = gaps.applymap(gap_weight)
384
+
385
+ # 5. Hitung nilai rata-rata core factor dan secondary factor
386
+ core_factors = selected_core_factors
387
+ secondary_factors = [c for c in criteria if c not in core_factors]
388
 
389
+ cf_scores = weight_gaps[core_factors].mean(axis=1)
390
+ sf_scores = weight_gaps[secondary_factors].mean(axis=1) if secondary_factors else 0
391
+
392
+ # 6. Hitung skor total berdasarkan bobot CF dan SF
393
+ final_scores_pm = cf_weight * cf_scores + sf_weight * sf_scores
394
+
395
+ # 7. Tampilkan hasil
396
+ st.subheader("๐Ÿ“ˆ Hasil Perhitungan Profile Matching")
397
+ st.write("๐Ÿ“ Skala 1โ€“5:")
398
+ st.dataframe(df_scaled)
399
+ st.write("๐Ÿงฎ GAP Weight:")
400
+ st.dataframe(weight_gaps)
401
+ st.write("๐Ÿ“Š Skor Akhir:")
402
+ st.dataframe(final_scores_pm.sort_values(ascending=False).to_frame("Skor"))
403
+ st.write("*Alternatif terbaik berdasarkan Profile Matching:*")
404
+ st.success(final_scores_pm.idxmax())