File size: 12,881 Bytes
410b443
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5735bd9
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
# ======================================================================
# --- 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."
        }