Gcompro / explanation_builder.py
tututz's picture
Upload 9 files
410b443 verified
# ======================================================================
# --- 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."
}