Gcompro / recommendation_builder.py
tututz's picture
Update recommendation_builder.py
b17f54f verified
# ======================================================================
# --- recommendation_builder.py (LOGIKA DIPERBAIKI) ---
# ======================================================================
import random
from typing import List, Dict, Any
# --- SENTIMENT CONFIG ---
SENTIMENT_MAP = {
"IPS_Terakhir": {"rendah": -1, "tinggi": 1},
"IPK_Terakhir": {"rendah": -1, "tinggi": 1},
"Jumlah_MK_Gagal": {"rendah": 1, "tinggi": -1},
"Total_SKS": {"rendah": -1, "tinggi": 1},
"Tren_IPS_Slope": {"rendah": -1, "tinggi": 1},
"Rentang_IPS": {"rendah": 1, "tinggi": -1},
"Total_SKS_Gagal": {"rendah": 1, "tinggi": -1},
}
# --- KLAUSA TEKS (FALLBACK) ---
# Ini digunakan jika tidak ada 'Sanity Check' khusus
CLAUSES_FALLBACK = {
"IPS_Terakhir": {
"rendah": "capaian IPS semester terakhir yang berada di bawah standar",
"tinggi": "capaian IPS semester terakhir yang cukup baik", # Dibuat netral
},
"IPK_Terakhir": {
"rendah": "nilai IPK kumulatif yang masih relatif rendah",
"tinggi": "rekam IPK kumulatif yang solid",
},
"Jumlah_MK_Gagal": {
"rendah": "rekam jejak mata kuliah yang cukup baik",
"tinggi": "adanya beban mata kuliah gagal",
},
"Total_SKS_Gagal": {
"rendah": "minimnya SKS yang terbuang",
"tinggi": "besarnya jumlah SKS yang harus diulang",
},
"Tren_IPS_Slope": {
"rendah": "tren performa yang menurun belakangan ini",
"tinggi": "tren peningkatan nilai yang konsisten",
},
"Total_SKS": {
"rendah": "jumlah SKS yang masih di bawah target progres studi",
"tinggi": "kemajuan pengambilan SKS yang sejalan dengan rencana studi",
},
"Rentang_IPS": {
"rendah": "konsistensi performa yang stabil",
"tinggi": "fluktuasi performa yang tidak stabil"
}
}
# --- KATA SAMBUNG ---
CONNECTORS = {
"same_bad": [". Masalah ini diperberat dengan ", ". Selain itu, terdeteksi juga ", ". Sayangnya, hal ini diikuti oleh "],
"same_good": [". Hal ini didukung pula oleh ", ". Ditambah lagi dengan ", ". Serta adanya "],
"contrast_bad_to_good": [". Namun kabar baiknya, ", ". Walaupun begitu, Anda memiliki ", ". Untungnya, hal ini diimbangi oleh "],
"contrast_good_to_bad": [". Namun sayangnya, ", ". Meskipun demikian, perlu diwaspadai adanya ", ". Akan tetapi, sistem mencatat "],
}
# --- TEMPLATE STATIS (FORMATTING DIHAPUS) ---
REKOMENDASI_BANK = {
"Resiko Tinggi": [
(
"🚨 Tindakan Mendesak Diperlukan. Berdasarkan analisis sistem, Anda berada di kategori Resiko Tinggi. "
"Segera lakukan evaluasi mendalam terhadap kebiasaan belajar, disiplin waktu, dan strategi akademik Anda. "
"Prioritaskan perbaikan pada mata kuliah dengan nilai rendah, manfaatkan bimbingan dosen, "
"dan pertimbangkan untuk mengurangi beban SKS sementara agar fokus pada peningkatan performa inti."
),
(
"⚠️ Perhatian Serius. Performa akademik Anda menunjukkan tanda risiko tinggi. "
"Usahakan untuk memperbaiki IPK dan IPS dengan memperkuat dasar konsep, "
"bergabung dalam kelompok belajar, serta mencari mentor akademik. "
"Manajemen waktu dan pola belajar teratur akan sangat membantu dalam mengembalikan performa Anda."
)
],
"Resiko Sedang": [
(
"⚠️ Waspada & Antisipasi. Anda berada di zona Resiko Sedang. "
"Hal ini menandakan performa Anda masih fluktuatif. "
"Pertahankan aspek yang sudah baik, namun segera identifikasi area yang masih lemah. "
"Disarankan untuk membuat jadwal belajar lebih terstruktur dan melakukan evaluasi kecil tiap minggu."
),
(
"💡 Perlu Peningkatan. Kinerja akademik Anda stabil namun belum optimal. "
"Fokuslah pada konsistensi nilai dan hindari penurunan mendadak di semester berikutnya. "
"Coba tingkatkan interaksi dengan dosen dan teman sekelas untuk memperkuat pemahaman materi."
)
],
"Resiko Rendah": [
(
"✅ Pertahankan Momentum. Anda berada di kategori Resiko Rendah. "
"Performa Anda sudah cukup baik dan konsisten. "
"Teruskan pola belajar yang efektif, namun jangan lengah terhadap materi yang sulit. "
"Pertimbangkan untuk mengambil tantangan baru seperti proyek penelitian atau lomba akademik."
),
(
"📈 Progres Positif. Anda menunjukkan performa yang solid. "
"Gunakan kesempatan ini untuk memperkuat area yang masih lemah dan menjaga keseimbangan antara studi dan istirahat. "
"Tetap evaluasi hasil belajar secara berkala untuk memastikan kestabilan performa."
)
],
"Aman": [
(
"🌟 Luar Biasa! Anda berada di kategori Aman. "
"Kinerja akademik Anda konsisten dan menunjukkan kedewasaan belajar yang tinggi. "
"Pertahankan strategi belajar yang sudah terbukti efektif, "
"dan jangan ragu berbagi pengalaman dengan rekan yang membutuhkan bantuan."
),
(
"🏆 Prestasi Stabil. Sistem mendeteksi profil akademik Anda sangat kuat. "
"Anda dapat mulai mengeksplorasi kegiatan tambahan seperti magang, penelitian, atau lomba akademik "
"untuk memperluas wawasan dan pengalaman profesional."
)
],
"default": (
"🔍 Evaluasi Umum. Hasil prediksi Anda menunjukkan area yang perlu diperhatikan. "
"Tetap jaga semangat belajar dan lakukan refleksi berkala terhadap hasil akademik Anda."
)
}
# --- [FUNGSI BARU] LOGIC SANITY CHECK ---
def _get_dynamic_clause(feature: str, condition: str, value: float) -> str:
"""
Logika Cerdas: Menyesuaikan kata sifat berdasarkan NILAI ASLI,
bukan hanya label 'tinggi/rendah' dari decision tree.
"""
# --- LOGIKA OVERRIDE (SANITY CHECK) ---
# 1. IPS Terakhir (Skala 0-4)
if feature == "IPS_Terakhir":
if condition == "tinggi": # DT bilang "tinggi"
if value < 2.5:
return "capaian IPS semester terakhir yang sedikit membaik namun masih rawan"
elif value < 3.0:
# Ini akan menangkap 2.62 dan 2.92
return "capaian IPS semester terakhir yang cukup aman"
else:
return "capaian IPS semester terakhir yang sangat memuaskan"
else: # DT bilang "rendah"
return "capaian IPS semester terakhir yang cukup rendah"
# 2. IPK Terakhir (Skala 0-4)
elif feature == "IPK_Terakhir":
if condition == "tinggi": # DT bilang "tinggi"
if value < 2.5:
# Ini akan menangkap 2.15
return "IPK kumulatif yang baru saja lolos ambang batas kritis"
elif value < 3.0:
# Ini akan menangkap 2.78
return "IPK kumulatif yang tergolong cukup baik"
else:
return "IPK kumulatif yang sangat solid"
else: # DT bilang "rendah"
if value < 2.0:
return "IPK kumulatif yang berada di zona bahaya"
else:
return "IPK kumulatif yang masih relatif rendah"
# 3. MK Gagal
elif feature == "Jumlah_MK_Gagal":
if condition == "rendah":
if value == 0:
return "rekam jejak mata kuliah yang bersih tanpa kegagalan"
else:
return f"jumlah mata kuliah gagal yang masih dalam batas wajar ({int(value)} MK)"
else: # DT bilang "tinggi"
if value < 3:
return f"adanya {int(value)} mata kuliah gagal yang perlu segera diulang"
else:
# Ini akan menangkap 4 Gagal
return f"adanya beban {int(value)} mata kuliah gagal yang menumpuk"
# 4. Tren
elif feature == "Tren_IPS_Slope":
if condition == "tinggi":
if value < 0.1: # Jika naiknya sedikit (kasus 0.066)
return "tren performa yang cukup membaik"
else:
return "tren peningkatan nilai yang konsisten"
else:
return "tren performa yang menurun belakangan ini"
# Jika tidak ada aturan khusus, gunakan Fallback
fallback = CLAUSES_FALLBACK.get(feature, {}).get(condition)
if fallback:
return fallback
# Jika tidak ada fallback, return string kosong
return ""
def generate_recommendation_paragraph(prediction_val: str, structured_rules: List[Dict[str, Any]]) -> str:
"""
Menghasilkan rekomendasi personal: Template Statis + Jahitan Dinamis
"""
# 1️⃣ Pilih template dasar (statis) dari REKOMENDASI_BANK
base_templates = REKOMENDASI_BANK.get(prediction_val)
if not base_templates:
base_templates = [REKOMENDASI_BANK["default"]]
if isinstance(base_templates, list):
base_text = random.choice(base_templates)
else:
base_text = base_templates # Handle 'default' yang bukan list
# 2️⃣ Buat bagian dinamis (berdasarkan fitur dominan)
active_clauses = []
features_seen = set()
for rule in reversed(structured_rules):
feature = rule["feature"]
if feature in features_seen: continue
features_seen.add(feature)
condition = rule["condition"]
value = rule["value"] # Ambil nilai asli
# [DIUBAH] Panggil helper baru, bukan CLAUSES.get()
text = _get_dynamic_clause(feature, condition, value)
sentiment = SENTIMENT_MAP.get(feature, {}).get(condition, 0)
if text: # Hanya tambahkan jika text tidak kosong
active_clauses.append({"text": text, "sentiment": sentiment})
if not active_clauses:
# Jika tidak ada fitur penting, kembalikan template statis saja
return base_text
# 3️⃣ Stitching: gabungkan klausa
dynamic_part = " Dalam penjelasan yang lebih spesifik, profil Anda dipengaruhi oleh "
current = active_clauses[0]
dynamic_part += current["text"]
last_sentiment = current["sentiment"]
for i in range(1, len(active_clauses)):
item = active_clauses[i]
current_sentiment = item["sentiment"]
if last_sentiment == -1 and current_sentiment == -1:
connector = random.choice(CONNECTORS["same_bad"])
elif last_sentiment == 1 and current_sentiment == 1:
connector = random.choice(CONNECTORS["same_good"])
elif last_sentiment == -1 and current_sentiment == 1:
connector = random.choice(CONNECTORS["contrast_bad_to_good"])
elif last_sentiment == 1 and current_sentiment == -1:
connector = random.choice(CONNECTORS["contrast_good_to_bad"])
else:
connector = ". Selanjutnya, perhatikan juga "
dynamic_part += connector + item["text"]
last_sentiment = current_sentiment
dynamic_part += "."
# 4️⃣ Gabungkan template + hasil stitching
# Format: [Paragraf Statis] + [Paragraf Dinamis]
# \n\n digunakan untuk membuat paragraf baru
final_text = f"{base_text}{dynamic_part}"
return final_text