tututz commited on
Commit
53bf810
·
verified ·
1 Parent(s): 5735bd9

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +114 -21
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.5.0"
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
- """Inti dari logika rekomendasi, HANYA dengan validasi prereq."""
 
 
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
- prereq_valid_candidates = []
 
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
- prereq_valid_candidates.append(candidate_data)
 
 
182
 
183
- catch_up_courses = [c for c in prereq_valid_candidates if c["semester_plan"] < current_semester]
184
- current_semester_courses = [c for c in prereq_valid_candidates if c["semester_plan"] == current_semester]
185
- future_courses = [c for c in prereq_valid_candidates if c["semester_plan"] > current_semester]
186
-
187
- sorted_catch_up = sorted(catch_up_courses, key=lambda x: (x["semester_plan"], -x["priority_score"]))
188
- sorted_current = sorted(current_semester_courses, key=lambda x: -x["priority_score"])
189
- sorted_future = sorted(future_courses, key=lambda x: (x["semester_plan"], -x["priority_score"]))
190
-
191
- final_ranked_list = sorted_catch_up + sorted_current + sorted_future
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 = {} # Ganti nama dari explanation_str
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(request.current_semester, request.courses_passed)
 
 
 
 
 
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["semester_plan"] < request.current_semester:
 
 
 
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