Spaces:
Sleeping
Sleeping
File size: 12,881 Bytes
410b443 5735bd9 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 | # ======================================================================
# --- explanation_builder.py (LOGIKA DIPERBAIKI & DIBERSIHKAN) ---
# ======================================================================
import random
from typing import List, Dict, Any
# Impor fungsi paragraf dari file terpisah
from .recommendation_builder import generate_recommendation_paragraph
# ======================================================================
# 1. KONFIGURASI NAMA FITUR (MAPPING)
# ======================================================================
# Dictionary ini mengubah nama variabel kode (raw) menjadi teks yang enak dibaca user.
FEATURE_LABEL_MAP = {
# Fitur Utama
"IPK_Terakhir": "IPK Semester Terakhir",
"IPS_Terakhir": "IPS Semester Terakhir",
"Total_SKS": "Total SKS Diambil",
"IPS_Tertinggi": "Capaian IPS Tertinggi",
"IPS_Terendah": "Capaian IPS Terendah",
"Rentang_IPS": "Stabilitas Nilai (Rentang IPS)",
"Jumlah_MK_Gagal": "Jumlah Mata Kuliah Gagal",
"Total_SKS_Gagal": "Total SKS Gagal/Hangus",
# Fitur Analitik/Tren
"Tren_IPS_Slope": "Tren Perubahan Nilai",
"Perubahan_Kinerja_Terakhir": "Perubahan Kinerja Terakhir",
"IPK_Ternormalisasi_SKS": "Rasio Efisiensi IPK per SKS",
# Fitur OHE (One Hot Encoding)
"Tren_Menaik": "Pola Tren Menaik",
"Tren_Menurun": "Pola Tren Menurun",
"Tren_Stabil": "Pola Tren Stabil"
}
# ======================================================================
# 2. BANK TEMPLATE (Faktor / Poin-Poin)
# ======================================================================
EXPLANATION_TEMPLATES = {
"pembuka": {
"Resiko Tinggi": [
"β οΈ Perhatian Serius Diperlukan: Sistem mendeteksi indikator risiko tinggi pada profil akademik Anda. Berikut adalah faktor krusial yang memicunya:",
"π¨ Peringatan Dini: Berdasarkan pola data historis, performa Anda saat ini berada dalam zona 'Resiko Tinggi'.",
],
"Resiko Sedang": [
"β οΈ Waspada: Profil Anda menunjukkan tanda-tanda 'Resiko Sedang'. Belum kritis, namun perlu perbaikan segera.",
"π‘ Perlu Evaluasi: Sistem mendeteksi adanya ketidakstabilan yang memicu status 'Resiko Sedang'.",
],
"Resiko Rendah": [
"β
Cukup Aman: Profil akademik Anda tergolong 'Resiko Rendah', namun tetap ada beberapa catatan kecil:",
"π Progres Baik: Secara umum performa Anda stabil di zona aman. Sistem menyoroti beberapa hal minor:",
],
"Aman": [
"π Sangat Baik: Selamat! Rekam jejak akademik Anda sangat solid sehingga dikategorikan 'Aman'.",
"π Performa Unggul: Sistem tidak mendeteksi masalah berarti. Prediksi 'Aman' didukung pondasi yang kuat.",
],
"default": "π Berikut adalah faktor-faktor analisis sistem untuk kategori '{prediction_val}':"
},
"fitur": {
# --- (Template ini akan dipilih oleh logic Sanity Check di bawah) ---
"IPS_Terakhir": {
"rendah_parah": "π Penurunan Drastis: IPS semester terakhir ({value:.2f}) sangat rendah.",
"rendah": "β οΈ Penurunan: IPS semester terakhir ({value:.2f}) berada di bawah ambang batas ideal.",
"cukup": "β
Cukup: IPS semester terakhir ({value:.2f}) tercatat di atas ambang batas kritis model.",
"baik": "π Baik: Capaian IPS semester terakhir ({value:.2f}) memberikan kontribusi positif.",
"tinggi": "π Sangat Solid: IPS semester terakhir Anda ({value:.2f}) sangat baik."
},
"IPK_Terakhir": {
"rendah_parah": "π Zona Bahaya: IPK kumulatif ({value:.2f}) berada di bawah 2.00.",
"rendah": "ποΈ Pondasi Rapuh: IPK kumulatif ({value:.2f}) terdeteksi di zona yang memerlukan perbaikan.",
"cukup": "π‘οΈ Cukup Aman: IPK kumulatif ({value:.2f}) telah lolos ambang batas kritis model.",
"baik": "ποΈ Pondasi Kokoh: IPK kumulatif ({value:.2f}) Anda tergolong baik.",
"tinggi": "π Prestasi: IPK kumulatif ({value:.2f}) Anda sangat solid."
},
"Jumlah_MK_Gagal": {
"rendah": "β¨ Rekam Jejak Bersih: Anda memiliki sedikit/tanpa mata kuliah gagal (Total: {value}).",
"tinggi_sedikit": "π Beban Mengulang: Terdapat {value} mata kuliah gagal yang perlu diwaspadai.",
"tinggi_banyak": "π¨ Beban Berat: Terdapat {value} mata kuliah gagal. Tumpukan beban ini meningkatkan risiko."
},
"Tren_IPS_Slope": {
"rendah": "π Tren Menurun: Grafik performa Anda cenderung melandai/turun.",
"tinggi_sedikit": "π Tren Membaik: Grafik nilai Anda menunjukkan sedikit tren kenaikan (slope positif).",
"tinggi_kuat": "π Tren Menanjak: Grafik nilai Anda menunjukkan tren kenaikan yang kuat."
},
"Rentang_IPS": {
"rendah": "βοΈ Performa Stabil: Fluktuasi nilai Anda kecil ({value:.2f}), menunjukkan konsistensi.",
"tinggi": "π’ Nilai Fluktuatif: Terdeteksi rentang nilai yang lebar ({value:.2f}) (tidak stabil)."
},
"Total_SKS_Gagal": {
"rendah": "β
Minim SKS Hangus: Total SKS dari mata kuliah gagal sangat minim ({value}).",
"tinggi": "β οΈ SKS Terbuang: Total SKS gagal ({value}) cukup besar dan membebani rasio kelulusan."
},
"Total_SKS": {
"rendah": "β³ Progres Lambat: Total SKS ({value}) masih tertinggal dari target.",
"tinggi": "π On-Track: Tabungan SKS Anda ({value}) sudah cukup banyak."
},
# --- Fitur OHE ---
"Tren_Menaik": { "ya": "π Grafik Positif: Pola data dikategorikan sebagai tren 'Menaik'." },
"Tren_Menurun": { "ya": "π Peringatan Penurunan: Pola data dikategorikan sebagai tren 'Menurun'." },
"Tren_Stabil": { "ya": "β‘οΈ Stagnan/Stabil: Pola nilai Anda cenderung datar (Stabil)." },
# --- Fallback (Generik) ---
"default": {
"rendah": "πΉ Nilai {feature_name} tercatat {value:.2f}, di bawah acuan ({threshold:.2f}).",
"tinggi": "πΈ Nilai {feature_name} tercatat {value:.2f}, di atas acuan ({threshold:.2f})."
}
}
}
# ======================================================================
# 3. FUNGSI LOGIC PEMILIH TEKS
# ======================================================================
def _get_explanation_text(rule: Dict[str, Any]) -> str:
"""Memilih template FAKTOR yang paling sesuai berdasarkan NILAI ASLI."""
raw_feature = rule["feature"] # Contoh: "Perubahan_Kinerja_Terakhir"
condition = rule["condition"]
value = rule["value"]
threshold = rule["threshold"]
# [PERBAIKAN UTAMA] Translasi Nama Fitur
# Jika nama ada di map, gunakan. Jika tidak, hilangkan underscore manual.
readable_feature_name = FEATURE_LABEL_MAP.get(raw_feature, raw_feature.replace("_", " "))
templates = EXPLANATION_TEMPLATES["fitur"]
# 1. Fitur OHE
if raw_feature in ["Tren_Menaik", "Tren_Menurun", "Tren_Stabil"]:
key = "ya" if condition == "tinggi" else "tidak"
# Kita hanya definisikan 'ya', jadi jika 'tidak' akan di-skip (return None)
return templates.get(raw_feature, {}).get(key)
# 2. Fitur Numerik (Sanity Check)
chosen_template_key = ""
if raw_feature == "IPS_Terakhir":
if condition == "tinggi":
if value < 2.75: chosen_template_key = "cukup" # Untuk 2.62
elif value < 3.25: chosen_template_key = "baik" # Untuk 2.92
else: chosen_template_key = "tinggi"
else:
if value < 2.0: chosen_template_key = "rendah_parah"
else: chosen_template_key = "rendah"
elif raw_feature == "IPK_Terakhir":
if condition == "tinggi":
if value < 2.75: chosen_template_key = "cukup" # Untuk 2.15
elif value < 3.25: chosen_template_key = "baik" # Untuk 2.78
else: chosen_template_key = "tinggi"
else:
if value < 2.0: chosen_template_key = "rendah_parah"
else: chosen_template_key = "rendah"
elif raw_feature == "Jumlah_MK_Gagal":
if condition == "rendah": # (value 0)
chosen_template_key = "rendah"
else: # tinggi
if value <= 3: chosen_template_key = "tinggi_sedikit"
else: chosen_template_key = "tinggi_banyak" # Untuk 4
elif raw_feature == "Tren_IPS_Slope":
if condition == "tinggi":
if value < 0.1: chosen_template_key = "tinggi_sedikit" # Untuk 0.066
else: chosen_template_key = "tinggi_kuat" # Untuk 0.125
else:
chosen_template_key = "rendah"
# 3. Fallback Logic (Jika Sanity Check tidak menemukan key)
if not chosen_template_key:
if raw_feature in templates:
# Jika fitur punya template khusus di dictionary 'fitur'
chosen_template_key = condition
else:
# Jika fitur benar-benar baru/tidak ada di dictionary, pakai DEFAULT.
# Di sini kita gunakan 'readable_feature_name' agar output bersih.
return templates["default"][condition].format(
feature_name=readable_feature_name,
value=value,
threshold=threshold
)
# 4. Ambil template berdasarkan key yang sudah dipilih
template_str = templates.get(raw_feature, {}).get(chosen_template_key)
# 5. Handle jika key (misal 'cukup') ada logic-nya, tapi string templatenya belum dibuat
if not template_str:
# Fallback ke default 'tinggi'/'rendah' milik fitur tersebut
template_str = templates.get(raw_feature, {}).get(condition)
if not template_str:
# Jika 'rendah' pun tidak ada, kembali ke DEFAULT global
return templates["default"][condition].format(
feature_name=readable_feature_name,
value=value,
threshold=threshold
)
# Format string (jika template adalah list, pilih acak)
if isinstance(template_str, list):
template_str = random.choice(template_str)
# Pastikan string tidak None sebelum di-format
if not template_str:
return None
# [PERBAIKAN] Inject readable_feature_name ke dalam format
return template_str.format(
feature_name=readable_feature_name,
value=value,
threshold=threshold
)
# ======================================================================
# 4. FUNGSI BUILDER UTAMA (Facade)
# ======================================================================
def build_full_response(structured_rules: List[Dict[str, Any]], prediction_val: str) -> Dict[str, Any]:
"""
Merakit respons lengkap: Poin Faktor + Paragraf Rekomendasi
"""
try:
# --- BAGIAN 1: BUAT POIN FAKTOR ---
opening_templates = EXPLANATION_TEMPLATES["pembuka"].get(prediction_val)
if not opening_templates:
# Fallback jika key prediksi (misal 'Resiko Sedang') tidak ada
default_template = EXPLANATION_TEMPLATES["pembuka"].get("default", "Analisis Faktor:")
opening_line = default_template.format(prediction_val=prediction_val)
elif isinstance(opening_templates, list):
opening_line = random.choice(opening_templates)
else:
opening_line = opening_templates
factors_list = []
features_explained = set()
for rule in reversed(structured_rules):
feature = rule["feature"]
if feature in features_explained: continue
features_explained.add(feature)
# Panggil fungsi yang sudah diperbaiki
chosen_template = _get_explanation_text(rule)
if chosen_template: # Hanya tambahkan jika string tidak None/Kosong
factors_list.append(chosen_template)
# --- BAGIAN 2: BUAT PARAGRAF REKOMENDASI ---
recommendation_text = generate_recommendation_paragraph(prediction_val, structured_rules)
# --- BAGIAN 3: GABUNGKAN ---
return {
"opening_line": opening_line,
"factors": factors_list,
"recommendation": recommendation_text
}
except Exception as e:
return {
"opening_line": f"β οΈ Maaf, terjadi kesalahan saat menyusun penjelasan: {str(e)}",
"factors": [],
"recommendation": "Gagal memuat rekomendasi personal."
} |