# ====================================================================== # --- 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