Spaces:
Sleeping
Sleeping
Update main.py
Browse files
main.py
CHANGED
|
@@ -21,7 +21,7 @@ from .explanation_builder import build_explanation_from_rules
|
|
| 21 |
app = FastAPI(
|
| 22 |
title="GCOMPRO",
|
| 23 |
description="API Prediksi Risiko Akademik dan Rekomendasi Mata Kuliah.",
|
| 24 |
-
version="1.
|
| 25 |
)
|
| 26 |
|
| 27 |
# ======================================================================
|
|
@@ -60,6 +60,7 @@ class PredictionResponse(BaseModel):
|
|
| 60 |
class RecommendationRequest(BaseModel):
|
| 61 |
current_semester: int
|
| 62 |
courses_passed: List[str]
|
|
|
|
| 63 |
|
| 64 |
class PrerequisiteInfo(BaseModel):
|
| 65 |
code: str
|
|
@@ -80,6 +81,38 @@ class CourseRecommendation(BaseModel):
|
|
| 80 |
# 3. Variabel Global & Pemuatan Model/Data
|
| 81 |
# ======================================================================
|
| 82 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
# --- Variabel Global untuk API Prediksi Risiko (App 1) ---
|
| 84 |
ml_model = None
|
| 85 |
MODEL_FEATURES = [
|
|
@@ -159,36 +192,88 @@ def on_startup():
|
|
| 159 |
# 4. Helper Function (Untuk API Rekomendasi)
|
| 160 |
# ======================================================================
|
| 161 |
|
| 162 |
-
def get_recommendations_logic(current_semester: int, courses_passed_list: List[str]) -> List[Dict[str, Any]]:
|
| 163 |
-
"""
|
|
|
|
|
|
|
| 164 |
passed_set = set(courses_passed_list)
|
| 165 |
all_courses_set = set(course_details_map.keys())
|
| 166 |
not_passed_courses = all_courses_set - passed_set
|
| 167 |
|
| 168 |
-
|
|
|
|
| 169 |
|
| 170 |
for course_code in not_passed_courses:
|
| 171 |
prereqs = prereq_map.get(course_code, [])
|
| 172 |
if all(p_code in passed_set for p_code in prereqs):
|
| 173 |
details = course_details_map.get(course_code)
|
| 174 |
if not details: continue
|
|
|
|
| 175 |
out_degree = out_degree_map.get(course_code, 0)
|
| 176 |
semester = details.get("semester_plan", 1)
|
|
|
|
|
|
|
| 177 |
priority_score = (out_degree / semester) if semester > 0 else 0
|
| 178 |
|
| 179 |
candidate_data = details.copy()
|
| 180 |
candidate_data["priority_score"] = priority_score
|
| 181 |
-
|
|
|
|
|
|
|
| 182 |
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 192 |
|
| 193 |
return final_ranked_list
|
| 194 |
|
|
@@ -221,7 +306,7 @@ def predict_risk(student_data: StudentFeatures):
|
|
| 221 |
classes = ml_model.classes_
|
| 222 |
probabilities = dict(zip(classes, prediction_proba[0]))
|
| 223 |
|
| 224 |
-
explanation_obj = {}
|
| 225 |
|
| 226 |
if hasattr(ml_model, 'tree_'):
|
| 227 |
try:
|
|
@@ -248,7 +333,6 @@ def predict_risk(student_data: StudentFeatures):
|
|
| 248 |
"value": sample_value
|
| 249 |
})
|
| 250 |
|
| 251 |
-
# Panggil builder, yang sekarang mengembalikan DICT
|
| 252 |
explanation_obj = build_explanation_from_rules(structured_rules, prediction_val)
|
| 253 |
|
| 254 |
except Exception as e:
|
|
@@ -262,7 +346,6 @@ def predict_risk(student_data: StudentFeatures):
|
|
| 262 |
"factors": []
|
| 263 |
}
|
| 264 |
|
| 265 |
-
# Kembalikan objek Pydantic yang sudah divalidasi
|
| 266 |
return PredictionResponse(
|
| 267 |
prediction=prediction_val,
|
| 268 |
probabilities=probabilities,
|
|
@@ -278,7 +361,12 @@ async def recommend_courses(request: RecommendationRequest):
|
|
| 278 |
if not course_details_map:
|
| 279 |
raise HTTPException(status_code=503, detail="Data kurikulum belum siap. Silakan coba lagi nanti.")
|
| 280 |
|
| 281 |
-
ranked_candidates = get_recommendations_logic(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 282 |
top_3_candidates = ranked_candidates[:3]
|
| 283 |
|
| 284 |
response_output = []
|
|
@@ -288,13 +376,19 @@ async def recommend_courses(request: RecommendationRequest):
|
|
| 288 |
is_tertinggal_status = False
|
| 289 |
reason = "Rekomendasi semester ini"
|
| 290 |
|
| 291 |
-
if course
|
|
|
|
|
|
|
|
|
|
| 292 |
reason = f"Mata kuliah tertinggal (Semester {course['semester_plan']})"
|
| 293 |
is_tertinggal_status = True
|
| 294 |
elif course["semester_plan"] > request.current_semester:
|
| 295 |
reason = f"Akselerasi (Semester {course['semester_plan']})"
|
| 296 |
|
| 297 |
prereq_codes = prereq_map.get(course["code"], [])
|
|
|
|
|
|
|
|
|
|
| 298 |
prereq_details_list = []
|
| 299 |
for p_code in prereq_codes:
|
| 300 |
if p_code in course_details_map:
|
|
@@ -319,5 +413,4 @@ async def recommend_courses(request: RecommendationRequest):
|
|
| 319 |
)
|
| 320 |
)
|
| 321 |
|
| 322 |
-
return response_output
|
| 323 |
-
|
|
|
|
| 21 |
app = FastAPI(
|
| 22 |
title="GCOMPRO",
|
| 23 |
description="API Prediksi Risiko Akademik dan Rekomendasi Mata Kuliah.",
|
| 24 |
+
version="1.6.0"
|
| 25 |
)
|
| 26 |
|
| 27 |
# ======================================================================
|
|
|
|
| 60 |
class RecommendationRequest(BaseModel):
|
| 61 |
current_semester: int
|
| 62 |
courses_passed: List[str]
|
| 63 |
+
mk_pilihan_failed: List[str] = [] # <--- Berisi kode spesifik (misal: "AAK4ABB3")
|
| 64 |
|
| 65 |
class PrerequisiteInfo(BaseModel):
|
| 66 |
code: str
|
|
|
|
| 81 |
# 3. Variabel Global & Pemuatan Model/Data
|
| 82 |
# ======================================================================
|
| 83 |
|
| 84 |
+
# <--- [UPDATE] DATABASE HARDCODE MATA KULIAH PILIHAN
|
| 85 |
+
# Ini digunakan untuk lookup nama resmi saat user mengirim kode MK Pilihan yang gagal
|
| 86 |
+
ELECTIVE_COURSES_DB = {
|
| 87 |
+
"AAK4ABB3": {"name": "New Generation Network", "sks": 3},
|
| 88 |
+
"AAK4BBB3": {"name": "Software Defined Network", "sks": 3},
|
| 89 |
+
"AAK4CBB3": {"name": "Rekayasa Jaringan", "sks": 3},
|
| 90 |
+
"AAK4DBB3": {"name": "Aplikasi Cyber Security", "sks": 3},
|
| 91 |
+
"AAK4EBB3": {"name": "Manajemen Telekomunikasi dan Transformasi Digital", "sks": 3},
|
| 92 |
+
"AAK4FBB3": {"name": "Adaptive Network", "sks": 3},
|
| 93 |
+
"AAK4GBB3": {"name": "Cloud Computing", "sks": 3},
|
| 94 |
+
"AAK4HBB3": {"name": "Koding dan Kompresi", "sks": 3},
|
| 95 |
+
"AAK4IBB3": {"name": "Steganografi dan Watermarking", "sks": 3},
|
| 96 |
+
"AAK4JBB3": {"name": "Mobile Application", "sks": 3},
|
| 97 |
+
"AAK4KBB3": {"name": "Speech Signal Processing", "sks": 3},
|
| 98 |
+
"AAK4LBB3": {"name": "Komunikasi Akses Wireless", "sks": 3},
|
| 99 |
+
"AAK4MBB3": {"name": "Wireless Optical Communication", "sks": 3},
|
| 100 |
+
"AAK4NBB3": {"name": "Broadband Optical Network", "sks": 3},
|
| 101 |
+
"AAK4OBB3": {"name": "Sistem Komunikasi Satelit", "sks": 3},
|
| 102 |
+
"AAK4PBB3": {"name": "Rekayasa Radio", "sks": 3},
|
| 103 |
+
"AAK4QBB3": {"name": "Radar, Navigasi dan Remote Sensing", "sks": 3},
|
| 104 |
+
"AAK4RBB3": {"name": "5G and Beyond", "sks": 3},
|
| 105 |
+
"AAK4SBB3": {"name": "Software Defined Radio", "sks": 3},
|
| 106 |
+
"AAK4TBB3": {"name": "Robotic Process Automation", "sks": 3},
|
| 107 |
+
"AAK4UBB3": {"name": "Rekayasa Frekuensi Radio dalam Komunikasi Selular", "sks": 3},
|
| 108 |
+
"AAK4VBB3": {"name": "Teknologi Radio Access Network (RAN)", "sks": 3},
|
| 109 |
+
"AAK4WBB3": {"name": "Internet of Things: Protokol, Platform, dan AI", "sks": 3},
|
| 110 |
+
"AAK4XBB3": {"name": "Jaringan Core Telekomunikasi", "sks": 3},
|
| 111 |
+
"AAK4YBB3": {"name": "Ethical Hacking", "sks": 3},
|
| 112 |
+
"AAK4ZBB3": {"name": "Keamanan Komunikasi Data", "sks": 3},
|
| 113 |
+
"AAK47BB3": {"name": "Rekayasa Penyiaran Digital", "sks": 3}
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
# --- Variabel Global untuk API Prediksi Risiko (App 1) ---
|
| 117 |
ml_model = None
|
| 118 |
MODEL_FEATURES = [
|
|
|
|
| 192 |
# 4. Helper Function (Untuk API Rekomendasi)
|
| 193 |
# ======================================================================
|
| 194 |
|
| 195 |
+
def get_recommendations_logic(current_semester: int, courses_passed_list: List[str], mk_pilihan_failed_list: List[str]) -> List[Dict[str, Any]]:
|
| 196 |
+
"""
|
| 197 |
+
Logika rekomendasi dengan injeksi data MK Pilihan dari Database Hardcode.
|
| 198 |
+
"""
|
| 199 |
passed_set = set(courses_passed_list)
|
| 200 |
all_courses_set = set(course_details_map.keys())
|
| 201 |
not_passed_courses = all_courses_set - passed_set
|
| 202 |
|
| 203 |
+
# Tahap 1: Kumpulkan semua kandidat valid (Regular & Slot Pilihan)
|
| 204 |
+
raw_candidates = []
|
| 205 |
|
| 206 |
for course_code in not_passed_courses:
|
| 207 |
prereqs = prereq_map.get(course_code, [])
|
| 208 |
if all(p_code in passed_set for p_code in prereqs):
|
| 209 |
details = course_details_map.get(course_code)
|
| 210 |
if not details: continue
|
| 211 |
+
|
| 212 |
out_degree = out_degree_map.get(course_code, 0)
|
| 213 |
semester = details.get("semester_plan", 1)
|
| 214 |
+
|
| 215 |
+
# Hitung Base Score
|
| 216 |
priority_score = (out_degree / semester) if semester > 0 else 0
|
| 217 |
|
| 218 |
candidate_data = details.copy()
|
| 219 |
candidate_data["priority_score"] = priority_score
|
| 220 |
+
candidate_data["is_retake_elective"] = False
|
| 221 |
+
|
| 222 |
+
raw_candidates.append(candidate_data)
|
| 223 |
|
| 224 |
+
# Tahap 2: Pisahkan Slot MK Pilihan vs MK Wajib
|
| 225 |
+
elective_slots = []
|
| 226 |
+
regular_candidates = []
|
| 227 |
+
|
| 228 |
+
for cand in raw_candidates:
|
| 229 |
+
# Deteksi slot template berdasarkan prefix kode
|
| 230 |
+
if cand["code"].startswith("MK_PILIHAN"):
|
| 231 |
+
elective_slots.append(cand)
|
| 232 |
+
else:
|
| 233 |
+
regular_candidates.append(cand)
|
| 234 |
+
|
| 235 |
+
# Urutkan slot pilihan agar mengisi dari semester terkecil (misal sem 7 dulu)
|
| 236 |
+
elective_slots.sort(key=lambda x: x["semester_plan"])
|
| 237 |
+
|
| 238 |
+
# <--- [UPDATE] Tahap 3: Suntikkan MK Pilihan Gagal + Lookup Database
|
| 239 |
+
processed_electives = []
|
| 240 |
+
failed_idx = 0
|
| 241 |
+
|
| 242 |
+
# Prioritaskan mengisi slot dengan MK yang gagal
|
| 243 |
+
while failed_idx < len(mk_pilihan_failed_list) and len(elective_slots) > 0:
|
| 244 |
+
slot = elective_slots.pop(0) # Ambil slot template (misal MK_PILIHAN1)
|
| 245 |
+
failed_code = mk_pilihan_failed_list[failed_idx]
|
| 246 |
+
|
| 247 |
+
# <--- [UPDATE] Lookup detail mata kuliah dari ELECTIVE_COURSES_DB
|
| 248 |
+
if failed_code in ELECTIVE_COURSES_DB:
|
| 249 |
+
real_name = ELECTIVE_COURSES_DB[failed_code]["name"]
|
| 250 |
+
real_sks = ELECTIVE_COURSES_DB[failed_code]["sks"]
|
| 251 |
+
else:
|
| 252 |
+
# Fallback jika kode tidak ada di DB (safety net)
|
| 253 |
+
real_name = "Mata Kuliah Pilihan (Unknown)"
|
| 254 |
+
real_sks = 3
|
| 255 |
+
|
| 256 |
+
# Modifikasi slot menjadi MK spesifik dengan data asli
|
| 257 |
+
slot["code"] = failed_code
|
| 258 |
+
slot["name"] = f"{real_name} (Mengulang)" # Ubah nama jadi nama asli
|
| 259 |
+
slot["sks"] = real_sks
|
| 260 |
+
slot["priority_score"] += 1.0 # Boost score
|
| 261 |
+
slot["is_retake_elective"] = True
|
| 262 |
+
|
| 263 |
+
processed_electives.append(slot)
|
| 264 |
+
failed_idx += 1
|
| 265 |
+
|
| 266 |
+
# Masukkan sisa slot pilihan (yang tidak di-override) kembali ke list
|
| 267 |
+
processed_electives.extend(elective_slots)
|
| 268 |
+
|
| 269 |
+
# Gabungkan kembali semua kandidat
|
| 270 |
+
final_pool = regular_candidates + processed_electives
|
| 271 |
+
|
| 272 |
+
# Tahap 4: Sorting Akhir (Score tinggi -> Semester kecil)
|
| 273 |
+
final_ranked_list = sorted(
|
| 274 |
+
final_pool,
|
| 275 |
+
key=lambda x: (-x["priority_score"], x["semester_plan"])
|
| 276 |
+
)
|
| 277 |
|
| 278 |
return final_ranked_list
|
| 279 |
|
|
|
|
| 306 |
classes = ml_model.classes_
|
| 307 |
probabilities = dict(zip(classes, prediction_proba[0]))
|
| 308 |
|
| 309 |
+
explanation_obj = {}
|
| 310 |
|
| 311 |
if hasattr(ml_model, 'tree_'):
|
| 312 |
try:
|
|
|
|
| 333 |
"value": sample_value
|
| 334 |
})
|
| 335 |
|
|
|
|
| 336 |
explanation_obj = build_explanation_from_rules(structured_rules, prediction_val)
|
| 337 |
|
| 338 |
except Exception as e:
|
|
|
|
| 346 |
"factors": []
|
| 347 |
}
|
| 348 |
|
|
|
|
| 349 |
return PredictionResponse(
|
| 350 |
prediction=prediction_val,
|
| 351 |
probabilities=probabilities,
|
|
|
|
| 361 |
if not course_details_map:
|
| 362 |
raise HTTPException(status_code=503, detail="Data kurikulum belum siap. Silakan coba lagi nanti.")
|
| 363 |
|
| 364 |
+
ranked_candidates = get_recommendations_logic(
|
| 365 |
+
request.current_semester,
|
| 366 |
+
request.courses_passed,
|
| 367 |
+
request.mk_pilihan_failed
|
| 368 |
+
)
|
| 369 |
+
|
| 370 |
top_3_candidates = ranked_candidates[:3]
|
| 371 |
|
| 372 |
response_output = []
|
|
|
|
| 376 |
is_tertinggal_status = False
|
| 377 |
reason = "Rekomendasi semester ini"
|
| 378 |
|
| 379 |
+
if course.get("is_retake_elective"):
|
| 380 |
+
reason = "Wajib Mengulang (MK Pilihan Gagal)"
|
| 381 |
+
is_tertinggal_status = True
|
| 382 |
+
elif course["semester_plan"] < request.current_semester:
|
| 383 |
reason = f"Mata kuliah tertinggal (Semester {course['semester_plan']})"
|
| 384 |
is_tertinggal_status = True
|
| 385 |
elif course["semester_plan"] > request.current_semester:
|
| 386 |
reason = f"Akselerasi (Semester {course['semester_plan']})"
|
| 387 |
|
| 388 |
prereq_codes = prereq_map.get(course["code"], [])
|
| 389 |
+
# Jika kode MK Pilihan diganti (misal AAK4ABB3), prereq_map.get("AAK4ABB3") akan None/Empty
|
| 390 |
+
# Ini benar karena MK Pilihan umumnya tidak punya prereq di graf ini (hanya placeholder)
|
| 391 |
+
|
| 392 |
prereq_details_list = []
|
| 393 |
for p_code in prereq_codes:
|
| 394 |
if p_code in course_details_map:
|
|
|
|
| 413 |
)
|
| 414 |
)
|
| 415 |
|
| 416 |
+
return response_output
|
|
|