tututz commited on
Commit
f4be2a1
·
verified ·
1 Parent(s): 86fde72

update main.py , tambahan endpoint recommend

Browse files
Files changed (1) hide show
  1. main.py +237 -61
main.py CHANGED
@@ -1,23 +1,30 @@
1
- # main.py
 
 
2
 
3
  import os
 
 
4
  import pandas as pd
5
  import skops.io as sio
6
- from fastapi import FastAPI
7
  from pydantic import BaseModel
 
8
 
9
- # ==============================
10
- # 1. Inisialisasi aplikasi FastAPI
11
- # ==============================
12
  app = FastAPI(
13
- title="API Prediksi Risiko Akademik Mahasiswa",
14
- description="API untuk memprediksi risiko akademik mahasiswa menggunakan model Machine Learning.",
15
- version="1.0.0"
16
  )
17
 
18
- # ==============================
19
- # 2. Struktur data input (Pydantic)
20
- # ==============================
 
 
21
  class StudentFeatures(BaseModel):
22
  IPK_Terakhir: float
23
  IPS_Terakhir: float
@@ -30,34 +37,33 @@ class StudentFeatures(BaseModel):
30
  Tren_IPS_Slope: float
31
  Perubahan_Kinerja_Terakhir: float
32
  IPK_Ternormalisasi_SKS: float
33
- Profil_Tren: str # Fitur kategorikal sebelum di-encode
34
-
35
- # ==============================
36
- # 3. Muat model (kompatibel dengan skops >= 0.10)
37
- # ==============================
38
- MODEL_PATH = os.path.join(os.path.dirname(__file__), "model_risiko_akademik.skops")
39
-
40
- try:
41
- trusted_types = [
42
- "numpy.ndarray",
43
- "numpy.core.multiarray.scalar",
44
- "sklearn.tree._classes.DecisionTreeClassifier",
45
- "_codecs.encode",
46
- "joblib.numpy_pickle.NumpyArrayWrapper",
47
- "numpy.core.multiarray._reconstruct",
48
- "numpy.dtype",
49
- "sklearn.tree._tree.Tree"
50
- ]
51
-
52
- # Muat model dengan daftar tipe yang sudah didefinisikan secara eksplisit
53
- model = sio.load(MODEL_PATH, trusted=trusted_types)
54
 
55
- except Exception as e:
56
- raise RuntimeError(f"Gagal memuat model dari {MODEL_PATH}: {e}")
 
57
 
58
- # ==============================
59
- # 4. Daftar fitur yang diharapkan model
60
- # ==============================
61
  MODEL_FEATURES = [
62
  'IPK_Terakhir', 'IPS_Terakhir', 'Total_SKS', 'IPS_Tertinggi',
63
  'IPS_Terendah', 'Rentang_IPS', 'Jumlah_MK_Gagal', 'Total_SKS_Gagal',
@@ -65,41 +71,211 @@ MODEL_FEATURES = [
65
  'IPK_Ternormalisasi_SKS', 'Tren_Menaik', 'Tren_Menurun', 'Tren_Stabil'
66
  ]
67
 
68
- # ==============================
69
- # 5. Endpoint root
70
- # ==============================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
  @app.get("/")
72
  def read_root():
73
  return {
74
- "message": "API untuk Prediksi Risiko Akademik Mahasiswa",
75
- "status": "ready"
 
76
  }
77
 
78
- # ==============================
79
- # 6. Endpoint untuk prediksi
80
- # ==============================
81
  @app.post("/predict/")
82
  def predict_risk(student_data: StudentFeatures):
83
- # Konversi input ke DataFrame
 
 
84
  data = student_data.dict()
85
  input_df = pd.DataFrame([data])
86
-
87
- # One-hot encoding kolom kategorikal
88
  input_encoded = pd.get_dummies(input_df, columns=['Profil_Tren'], prefix='Tren')
89
-
90
- # Pastikan semua kolom model tersedia
91
  input_encoded = input_encoded.reindex(columns=MODEL_FEATURES, fill_value=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
92
 
93
- # Prediksi
94
- prediction = model.predict(input_encoded)
95
- prediction_proba = model.predict_proba(input_encoded)
 
 
96
 
97
- # Ambil hasil probabilitas per kelas
98
- classes = model.classes_
99
- probabilities = dict(zip(classes, prediction_proba[0]))
 
 
 
 
 
 
 
 
 
100
 
101
- # Return hasil dalam format JSON
102
- return {
103
- "prediction": prediction[0],
104
- "probabilities": probabilities
105
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ======================================================================
2
+ # --- main.py (GABUNGAN DUA API) ---
3
+ # ======================================================================
4
 
5
  import os
6
+ import json
7
+ import networkx as nx
8
  import pandas as pd
9
  import skops.io as sio
10
+ from fastapi import FastAPI, HTTPException
11
  from pydantic import BaseModel
12
+ from typing import List, Dict, Any
13
 
14
+ # ======================================================================
15
+ # 1. Inisialisasi Aplikasi FastAPI
16
+ # ======================================================================
17
  app = FastAPI(
18
+ title="API Layanan Akademik Mahasiswa",
19
+ description="Menggabungkan API Prediksi Risiko Akademik dan Rekomendasi Mata Kuliah.",
20
+ version="1.1.0"
21
  )
22
 
23
+ # ======================================================================
24
+ # 2. Struktur Data Input/Output (Pydantic)
25
+ # ======================================================================
26
+
27
+ # --- Model untuk API Prediksi Risiko (App 1) ---
28
  class StudentFeatures(BaseModel):
29
  IPK_Terakhir: float
30
  IPS_Terakhir: float
 
37
  Tren_IPS_Slope: float
38
  Perubahan_Kinerja_Terakhir: float
39
  IPK_Ternormalisasi_SKS: float
40
+ Profil_Tren: str
41
+
42
+ # --- Model untuk API Rekomendasi MK (App 2) ---
43
+ class RecommendationRequest(BaseModel):
44
+ current_semester: int
45
+ courses_passed: List[str]
46
+
47
+ class PrerequisiteInfo(BaseModel):
48
+ code: str
49
+ name: str
50
+
51
+ class CourseRecommendation(BaseModel):
52
+ rank: int
53
+ code: str
54
+ name: str
55
+ sks: int
56
+ semester_plan: int
57
+ reason: str
58
+ priority_score: float
59
+ prerequisites: List[PrerequisiteInfo]
 
60
 
61
+ # ======================================================================
62
+ # 3. Variabel Global & Pemuatan Model/Data
63
+ # ======================================================================
64
 
65
+ # --- Variabel Global untuk API Prediksi Risiko (App 1) ---
66
+ ml_model = None
 
67
  MODEL_FEATURES = [
68
  'IPK_Terakhir', 'IPS_Terakhir', 'Total_SKS', 'IPS_Tertinggi',
69
  'IPS_Terendah', 'Rentang_IPS', 'Jumlah_MK_Gagal', 'Total_SKS_Gagal',
 
71
  'IPK_Ternormalisasi_SKS', 'Tren_Menaik', 'Tren_Menurun', 'Tren_Stabil'
72
  ]
73
 
74
+ # --- Variabel Global untuk API Rekomendasi MK (App 2) ---
75
+ G = nx.DiGraph()
76
+ course_details_map = {}
77
+ prereq_map = {}
78
+ coreq_map = {}
79
+ out_degree_map = {}
80
+
81
+ # --- Fungsi Pemuatan (dipanggil saat startup) ---
82
+
83
+ def load_ml_model():
84
+ """Memuat model ML dari file .skops"""
85
+ global ml_model
86
+ MODEL_PATH = os.path.join(os.path.dirname(__file__), "model_risiko_akademik.skops")
87
+ print(f"Mencoba memuat model ML dari: {MODEL_PATH}")
88
+ try:
89
+ trusted_types = [
90
+ "numpy.ndarray", "numpy.core.multiarray.scalar",
91
+ "sklearn.tree._classes.DecisionTreeClassifier", "_codecs.encode",
92
+ "joblib.numpy_pickle.NumpyArrayWrapper", "numpy.core.multiarray._reconstruct",
93
+ "numpy.dtype", "sklearn.tree._tree.Tree"
94
+ ]
95
+ ml_model = sio.load(MODEL_PATH, trusted=trusted_types)
96
+ print("Model ML berhasil dimuat.")
97
+ except Exception as e:
98
+ print(f"ERROR: Gagal memuat model ML dari {MODEL_PATH}: {e}")
99
+ # Di produksi, Anda mungkin ingin ini menghentikan server
100
+ # raise RuntimeError(f"Gagal memuat model dari {MODEL_PATH}: {e}")
101
+
102
+ def load_graph_data():
103
+ """Memuat dan memproses data graf kurikulum dari JSON"""
104
+ global G, course_details_map, prereq_map, coreq_map, out_degree_map
105
+ JSON_PATH = "OK_matkul_graph.json"
106
+ print(f"Mencoba memuat data graf dari: {JSON_PATH}")
107
+ try:
108
+ with open(JSON_PATH, "r") as f:
109
+ data = json.load(f)
110
+
111
+ for node in data["nodes"]:
112
+ course_details_map[node["code"]] = node
113
+ G.add_node(node["code"])
114
+
115
+ for edge in data["edges"]:
116
+ if edge["type"] == "prereq":
117
+ G.add_edge(edge["from"], edge["to"])
118
+ if edge["to"] not in prereq_map:
119
+ prereq_map[edge["to"]] = []
120
+ prereq_map[edge["to"]].append(edge["from"])
121
+ elif edge["type"] == "coreq":
122
+ if edge["to"] not in coreq_map:
123
+ coreq_map[edge["to"]] = []
124
+ coreq_map[edge["to"]].append(edge["from"])
125
+
126
+ for node_code in G.nodes():
127
+ out_degree_map[node_code] = G.out_degree(node_code)
128
+
129
+ print(f"Data graf berhasil dimuat. Prereqs: {len(prereq_map)}, Coreqs: {len(coreq_map)}")
130
+ except FileNotFoundError:
131
+ print(f"ERROR: {JSON_PATH} tidak ditemukan!")
132
+ except Exception as e:
133
+ print(f"Error saat memuat graf: {e}")
134
+
135
+ # --- Startup Event: Muat semua data saat server menyala ---
136
+ @app.on_event("startup")
137
+ def on_startup():
138
+ load_ml_model()
139
+ load_graph_data()
140
+
141
+ # ======================================================================
142
+ # 4. Helper Function (Untuk API Rekomendasi)
143
+ # ======================================================================
144
+
145
+ def get_recommendations_logic(current_semester: int, courses_passed_list: List[str]) -> List[Dict[str, Any]]:
146
+ """Inti dari logika rekomendasi, dengan validasi prereq dan coreq."""
147
+ passed_set = set(courses_passed_list)
148
+ all_courses_set = set(course_details_map.keys())
149
+ not_passed_courses = all_courses_set - passed_set
150
+
151
+ prereq_valid_candidates = []
152
+
153
+ # Tahap 1: Cek Prasyarat (Prereq)
154
+ for course_code in not_passed_courses:
155
+ prereqs = prereq_map.get(course_code, [])
156
+ if all(p_code in passed_set for p_code in prereqs):
157
+ details = course_details_map.get(course_code)
158
+ if not details: continue # Lewati jika MK tidak ada di map (data anomali)
159
+
160
+ out_degree = out_degree_map.get(course_code, 0)
161
+ semester = details.get("semester_plan", 1)
162
+ priority_score = (out_degree / semester) if semester > 0 else 0
163
+
164
+ candidate_data = details.copy()
165
+ candidate_data["priority_score"] = priority_score
166
+ prereq_valid_candidates.append(candidate_data)
167
+
168
+ # Tahap 2: Cek Ko-requisite (Coreq)
169
+ prereq_valid_codes = {c['code'] for c in prereq_valid_candidates}
170
+ final_valid_candidates = []
171
+
172
+ for candidate in prereq_valid_candidates:
173
+ course_code = candidate['code']
174
+ coreqs = coreq_map.get(course_code, [])
175
+
176
+ if not coreqs:
177
+ final_valid_candidates.append(candidate)
178
+ continue
179
+
180
+ is_coreq_met = True
181
+ for coreq_code in coreqs:
182
+ if (coreq_code not in passed_set) and (coreq_code not in prereq_valid_codes):
183
+ is_coreq_met = False
184
+ break
185
+
186
+ if is_coreq_met:
187
+ final_valid_candidates.append(candidate)
188
+
189
+ # Tahap 3: Urutkan berdasarkan prioritas
190
+ catch_up_courses = [c for c in final_valid_candidates if c["semester_plan"] < current_semester]
191
+ current_semester_courses = [c for c in final_valid_candidates if c["semester_plan"] == current_semester]
192
+ future_courses = [c for c in final_valid_candidates if c["semester_plan"] > current_semester]
193
+
194
+ sorted_catch_up = sorted(catch_up_courses, key=lambda x: (x["semester_plan"], -x["priority_score"]))
195
+ sorted_current = sorted(current_semester_courses, key=lambda x: -x["priority_score"], reverse=True)
196
+ sorted_future = sorted(future_courses, key=lambda x: (x["semester_plan"], -x["priority_score"]))
197
+
198
+ final_ranked_list = sorted_catch_up + sorted_current + sorted_future
199
+
200
+ return final_ranked_list
201
+
202
+ # ======================================================================
203
+ # 5. Endpoints API
204
+ # ======================================================================
205
+
206
+ # --- Endpoint dari App 1 (Prediksi) ---
207
  @app.get("/")
208
  def read_root():
209
  return {
210
+ "message": "Selamat Datang di API Layanan Akademik Mahasiswa",
211
+ "status": "ready",
212
+ "endpoints": ["/predict/", "/recommend/"]
213
  }
214
 
 
 
 
215
  @app.post("/predict/")
216
  def predict_risk(student_data: StudentFeatures):
217
+ if ml_model is None:
218
+ raise HTTPException(status_code=503, detail="Model ML belum siap. Silakan coba lagi nanti.")
219
+
220
  data = student_data.dict()
221
  input_df = pd.DataFrame([data])
 
 
222
  input_encoded = pd.get_dummies(input_df, columns=['Profil_Tren'], prefix='Tren')
 
 
223
  input_encoded = input_encoded.reindex(columns=MODEL_FEATURES, fill_value=False)
224
+
225
+ try:
226
+ prediction = ml_model.predict(input_encoded)
227
+ prediction_proba = ml_model.predict_proba(input_encoded)
228
+ classes = ml_model.classes_
229
+ probabilities = dict(zip(classes, prediction_proba[0]))
230
+
231
+ return {
232
+ "prediction": prediction[0],
233
+ "probabilities": probabilities
234
+ }
235
+ except Exception as e:
236
+ raise HTTPException(status_code=500, detail=f"Terjadi kesalahan saat prediksi: {e}")
237
 
238
+ # --- Endpoint dari App 2 (Rekomendasi) ---
239
+ @app.post("/recommend/", response_model=List[CourseRecommendation])
240
+ async def recommend_courses(request: RecommendationRequest):
241
+ if not course_details_map:
242
+ raise HTTPException(status_code=503, detail="Data kurikulum belum siap. Silakan coba lagi nanti.")
243
 
244
+ ranked_candidates = get_recommendations_logic(request.current_semester, request.courses_passed)
245
+ top_3_candidates = ranked_candidates[:3]
246
+
247
+ response_output = []
248
+ for i, course in enumerate(top_3_candidates):
249
+ rank = i + 1
250
+
251
+ reason = "Rekomendasi semester ini"
252
+ if course["semester_plan"] < request.current_semester:
253
+ reason = f"Mata kuliah tertinggal (Smt {course['semester_plan']})"
254
+ elif course["semester_plan"] > request.current_semester:
255
+ reason = f"Akselerasi (Smt {course['semester_plan']})"
256
 
257
+ prereq_codes = prereq_map.get(course["code"], [])
258
+ prereq_details_list = []
259
+ for p_code in prereq_codes:
260
+ if p_code in course_details_map:
261
+ prereq_details_list.append(
262
+ PrerequisiteInfo(
263
+ code=p_code,
264
+ name=course_details_map[p_code]["name"]
265
+ )
266
+ )
267
+
268
+ response_output.append(
269
+ CourseRecommendation(
270
+ rank=rank,
271
+ code=course["code"],
272
+ name=course["name"],
273
+ sks=course["sks"],
274
+ semester_plan=course["semester_plan"],
275
+ reason=reason,
276
+ priority_score=course["priority_score"],
277
+ prerequisites=prereq_details_list
278
+ )
279
+ )
280
+
281
+ return response_output