# ====================================================================== # --- 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." }