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