farwew commited on
Commit
c06f559
Β·
verified Β·
1 Parent(s): 4d8963e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +130 -48
app.py CHANGED
@@ -31,30 +31,64 @@ def _ensure_columns(df: pd.DataFrame):
31
  if missing:
32
  raise ValueError(f"CSV missing required columns: {missing}")
33
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  # Loader CSV dengan cache
35
  @lru_cache(maxsize=1)
36
  def load_data_cached() -> pd.DataFrame:
37
  if not os.path.exists(CSV_PATH):
38
  raise FileNotFoundError(f"CSV not found at: {CSV_PATH}")
39
 
 
40
  df = pd.read_csv(CSV_PATH)
41
- df.columns = [c.strip() for c in df.columns] # Hilangkan spasi
 
42
 
43
  _ensure_columns(df)
44
 
 
45
  df["sks"] = pd.to_numeric(df["sks"], errors="coerce").fillna(0).astype(int)
46
  df["id_smt"] = pd.to_numeric(df["id_smt"], errors="coerce").fillna(0).astype(int)
47
 
48
- df["nilai_huruf"] = df["nilai_huruf"].astype(str).strip()
 
 
49
  df["nilai_numerik"] = df["nilai_huruf"].map(MAP_NILAI)
50
 
51
- # Jika huruf invalid β†’ fallback ke nilai_akhir 0–100
52
  def fallback_numeric(row):
 
53
  if pd.notna(row["nilai_numerik"]):
54
  return row["nilai_numerik"]
55
-
56
  try:
57
- v = float(row["nilai_akhir"])
58
  if v >= 86: return 4.0
59
  if v >= 76: return 3.5
60
  if v >= 66: return 3.0
@@ -62,10 +96,11 @@ def load_data_cached() -> pd.DataFrame:
62
  if v >= 56: return 2.0
63
  if v >= 41: return 1.0
64
  return 0.0
65
- except:
66
  return 0.0
67
 
68
  df["nilai_numerik"] = df.apply(fallback_numeric, axis=1)
 
69
  return df
70
 
71
  # Pakai nilai terakhir per (mhs, matkul)
@@ -77,14 +112,13 @@ def get_final_records(df: pd.DataFrame) -> pd.DataFrame:
77
  # ENDPOINT 1 β€” TOTAL MAHASISWA
78
  # ----------------------------------------------------
79
  @app.get("/jumlah_mahasiswa")
80
- def jumlah_mahasiswa(reload: bool = False):
81
  if reload:
82
  load_data_cached.cache_clear()
83
-
84
  try:
85
  df = load_data_cached()
86
  except Exception as e:
87
- raise HTTPException(500, str(e))
88
 
89
  total = int(df["kode_mhs"].nunique())
90
  return {"total_mahasiswa": total}
@@ -93,57 +127,49 @@ def jumlah_mahasiswa(reload: bool = False):
93
  # ENDPOINT 2 β€” JUMLAH PER ANGKATAN
94
  # ----------------------------------------------------
95
  @app.get("/jumlah_per_angkatan")
96
- def jumlah_per_angkatan(reload: bool = False):
97
  if reload:
98
  load_data_cached.cache_clear()
99
-
100
  try:
101
  df = load_data_cached()
102
  except Exception as e:
103
- raise HTTPException(500, str(e))
104
 
105
  per_ang = df.groupby("Tahun angkatan")["kode_mhs"].nunique().sort_index().to_dict()
106
  per_ang = {str(k): int(v) for k, v in per_ang.items()}
107
-
108
  return {"mahasiswa_per_angkatan": per_ang}
109
 
110
  # ----------------------------------------------------
111
  # ENDPOINT 3 β€” MAHASISWA ELIGIBLE TA
112
  # ----------------------------------------------------
113
  @app.get("/eligible_ta")
114
- def eligible_ta(reload: bool = False, min_sks: int = 110):
 
115
  if reload:
116
  load_data_cached.cache_clear()
117
-
118
  try:
119
  df = load_data_cached()
120
  except Exception as e:
121
- raise HTTPException(500, str(e))
122
 
123
  final = get_final_records(df)
124
  sks_per_mhs = final.groupby("kode_mhs")["sks"].sum()
125
-
126
  eligible = sks_per_mhs[sks_per_mhs > min_sks]
127
 
128
- data = [
129
- {"kode_mhs": m, "total_sks": int(sks_per_mhs[m])}
130
- for m in eligible.index
131
- ]
132
-
133
  return {"jumlah_eligible": len(data), "daftar": data}
134
 
135
  # ----------------------------------------------------
136
  # ENDPOINT 4 β€” IPK RATA-RATA
137
  # ----------------------------------------------------
138
  @app.get("/ipk_rata_rata")
139
- def ipk_rata_rata(reload: bool = False):
140
  if reload:
141
  load_data_cached.cache_clear()
142
-
143
  try:
144
  df = load_data_cached()
145
  except Exception as e:
146
- raise HTTPException(500, str(e))
147
 
148
  final = get_final_records(df)
149
  final["total_bobot"] = final["sks"] * final["nilai_numerik"]
@@ -153,34 +179,29 @@ def ipk_rata_rata(reload: bool = False):
153
  total_sks=("sks", "sum")
154
  ).reset_index()
155
 
156
- grp["ips"] = grp.apply(
157
- lambda r: (r["total_bobot"] / r["total_sks"]) if r["total_sks"] > 0 else 0.0,
158
- axis=1
159
- )
160
-
161
  ipk_series = grp.groupby("kode_mhs")["ips"].mean()
162
- mean_ipk = float(round(ipk_series.mean(), 3))
163
 
 
 
 
 
164
  q = ipk_series.quantile([0.25, 0.5, 0.75]).to_dict()
165
  q = {str(k): float(v) for k, v in q.items()}
166
 
167
- return {
168
- "ipk_rata_rata": mean_ipk,
169
- "ipk_quartiles": q
170
- }
171
 
172
  # ----------------------------------------------------
173
  # ENDPOINT 5 β€” DASHBOARD SUMMARY (SEMUA RINGKASAN)
174
  # ----------------------------------------------------
175
  @app.get("/dashboard_summary")
176
- def dashboard_summary(reload: bool = False):
177
  if reload:
178
  load_data_cached.cache_clear()
179
-
180
  try:
181
  df = load_data_cached()
182
  except Exception as e:
183
- raise HTTPException(500, str(e))
184
 
185
  final = get_final_records(df)
186
  total_mhs = int(df["kode_mhs"].nunique())
@@ -190,11 +211,7 @@ def dashboard_summary(reload: bool = False):
190
 
191
  sks_per_mhs = final.groupby("kode_mhs")["sks"].sum()
192
  eligible = sks_per_mhs[sks_per_mhs > 110]
193
-
194
- eligible_list = [
195
- {"kode_mhs": m, "total_sks": int(sks_per_mhs[m])}
196
- for m in eligible.index
197
- ]
198
 
199
  # Hitung IPK
200
  final["total_bobot"] = final["sks"] * final["nilai_numerik"]
@@ -202,10 +219,9 @@ def dashboard_summary(reload: bool = False):
202
  total_bobot=("total_bobot", "sum"),
203
  total_sks=("sks", "sum")
204
  ).reset_index()
205
-
206
- grp["ips"] = grp.apply(lambda r: r["total_bobot"] / r["total_sks"] if r["total_sks"] > 0 else 0, axis=1)
207
  ipk_series = grp.groupby("kode_mhs")["ips"].mean()
208
- mean_ipk = float(round(ipk_series.mean(), 3))
209
 
210
  return {
211
  "total_mahasiswa": total_mhs,
@@ -220,7 +236,73 @@ def dashboard_summary(reload: bool = False):
220
  }
221
 
222
  # ----------------------------------------------------
223
- # ENDPOINT 6 β€” RELOAD CSV
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
224
  # ----------------------------------------------------
225
  @app.post("/reload_data")
226
  def reload_data():
@@ -228,5 +310,5 @@ def reload_data():
228
  try:
229
  _ = load_data_cached()
230
  except Exception as e:
231
- raise HTTPException(500, f"Reload failed: {e}")
232
  return {"status": "reloaded"}
 
31
  if missing:
32
  raise ValueError(f"CSV missing required columns: {missing}")
33
 
34
+ # Helper untuk normalisasi nilai_huruf sel yang mungkin berisi list/Series/None
35
+ def _to_simple_string(val):
36
+ try:
37
+ # pandas NA
38
+ if pd.isna(val):
39
+ return ""
40
+ except Exception:
41
+ # if val is unhashable, continue
42
+ pass
43
+
44
+ # If the cell contains a pandas Series or ndarray-like
45
+ if isinstance(val, pd.Series):
46
+ # take first non-null element if possible
47
+ non_null = val.dropna()
48
+ if not non_null.empty:
49
+ return str(non_null.iloc[0])
50
+ if not val.empty:
51
+ return str(val.iloc[0])
52
+ return ""
53
+ if isinstance(val, (list, tuple)):
54
+ if len(val) == 0:
55
+ return ""
56
+ return str(val[0])
57
+ # dict or other object: convert to string
58
+ try:
59
+ return str(val)
60
+ except Exception:
61
+ return ""
62
+
63
  # Loader CSV dengan cache
64
  @lru_cache(maxsize=1)
65
  def load_data_cached() -> pd.DataFrame:
66
  if not os.path.exists(CSV_PATH):
67
  raise FileNotFoundError(f"CSV not found at: {CSV_PATH}")
68
 
69
+ # Read CSV (let pandas infer types). If encoding issues happen, set encoding='utf-8'
70
  df = pd.read_csv(CSV_PATH)
71
+ # Normalize column names: strip whitespace
72
+ df.columns = [c.strip() for c in df.columns]
73
 
74
  _ensure_columns(df)
75
 
76
+ # Normalize numeric columns
77
  df["sks"] = pd.to_numeric(df["sks"], errors="coerce").fillna(0).astype(int)
78
  df["id_smt"] = pd.to_numeric(df["id_smt"], errors="coerce").fillna(0).astype(int)
79
 
80
+ # SAFELY normalize nilai_huruf (handle lists, Series, NaN, ints, etc.)
81
+ df["nilai_huruf"] = df["nilai_huruf"].apply(_to_simple_string).astype(str).str.strip()
82
+ # Map A, AB, B, ... -> numeric
83
  df["nilai_numerik"] = df["nilai_huruf"].map(MAP_NILAI)
84
 
85
+ # Jika huruf invalid β†’ fallback ke nilai_akhir (0–100 -> skala 0-4)
86
  def fallback_numeric(row):
87
+ # jika sudah mapped, kembalikan
88
  if pd.notna(row["nilai_numerik"]):
89
  return row["nilai_numerik"]
 
90
  try:
91
+ v = float(row.get("nilai_akhir", 0))
92
  if v >= 86: return 4.0
93
  if v >= 76: return 3.5
94
  if v >= 66: return 3.0
 
96
  if v >= 56: return 2.0
97
  if v >= 41: return 1.0
98
  return 0.0
99
+ except Exception:
100
  return 0.0
101
 
102
  df["nilai_numerik"] = df.apply(fallback_numeric, axis=1)
103
+
104
  return df
105
 
106
  # Pakai nilai terakhir per (mhs, matkul)
 
112
  # ENDPOINT 1 β€” TOTAL MAHASISWA
113
  # ----------------------------------------------------
114
  @app.get("/jumlah_mahasiswa")
115
+ def jumlah_mahasiswa(reload: bool = Query(False, description="reload CSV cache")):
116
  if reload:
117
  load_data_cached.cache_clear()
 
118
  try:
119
  df = load_data_cached()
120
  except Exception as e:
121
+ raise HTTPException(status_code=500, detail=str(e))
122
 
123
  total = int(df["kode_mhs"].nunique())
124
  return {"total_mahasiswa": total}
 
127
  # ENDPOINT 2 β€” JUMLAH PER ANGKATAN
128
  # ----------------------------------------------------
129
  @app.get("/jumlah_per_angkatan")
130
+ def jumlah_per_angkatan(reload: bool = Query(False, description="reload CSV cache")):
131
  if reload:
132
  load_data_cached.cache_clear()
 
133
  try:
134
  df = load_data_cached()
135
  except Exception as e:
136
+ raise HTTPException(status_code=500, detail=str(e))
137
 
138
  per_ang = df.groupby("Tahun angkatan")["kode_mhs"].nunique().sort_index().to_dict()
139
  per_ang = {str(k): int(v) for k, v in per_ang.items()}
 
140
  return {"mahasiswa_per_angkatan": per_ang}
141
 
142
  # ----------------------------------------------------
143
  # ENDPOINT 3 β€” MAHASISWA ELIGIBLE TA
144
  # ----------------------------------------------------
145
  @app.get("/eligible_ta")
146
+ def eligible_ta(reload: bool = Query(False, description="reload CSV cache"),
147
+ min_sks: int = Query(110, description="threshold SKS untuk eligible (default:110)")):
148
  if reload:
149
  load_data_cached.cache_clear()
 
150
  try:
151
  df = load_data_cached()
152
  except Exception as e:
153
+ raise HTTPException(status_code=500, detail=str(e))
154
 
155
  final = get_final_records(df)
156
  sks_per_mhs = final.groupby("kode_mhs")["sks"].sum()
 
157
  eligible = sks_per_mhs[sks_per_mhs > min_sks]
158
 
159
+ data = [{"kode_mhs": m, "total_sks": int(sks_per_mhs[m])} for m in eligible.index]
 
 
 
 
160
  return {"jumlah_eligible": len(data), "daftar": data}
161
 
162
  # ----------------------------------------------------
163
  # ENDPOINT 4 β€” IPK RATA-RATA
164
  # ----------------------------------------------------
165
  @app.get("/ipk_rata_rata")
166
+ def ipk_rata_rata(reload: bool = Query(False, description="reload CSV cache")):
167
  if reload:
168
  load_data_cached.cache_clear()
 
169
  try:
170
  df = load_data_cached()
171
  except Exception as e:
172
+ raise HTTPException(status_code=500, detail=str(e))
173
 
174
  final = get_final_records(df)
175
  final["total_bobot"] = final["sks"] * final["nilai_numerik"]
 
179
  total_sks=("sks", "sum")
180
  ).reset_index()
181
 
182
+ grp["ips"] = grp.apply(lambda r: (r["total_bobot"] / r["total_sks"]) if r["total_sks"] > 0 else 0.0, axis=1)
 
 
 
 
183
  ipk_series = grp.groupby("kode_mhs")["ips"].mean()
 
184
 
185
+ if ipk_series.empty:
186
+ return {"ipk_rata_rata": 0.0, "ipk_quartiles": {}}
187
+
188
+ mean_ipk = float(round(ipk_series.mean(), 3))
189
  q = ipk_series.quantile([0.25, 0.5, 0.75]).to_dict()
190
  q = {str(k): float(v) for k, v in q.items()}
191
 
192
+ return {"ipk_rata_rata": mean_ipk, "ipk_quartiles": q}
 
 
 
193
 
194
  # ----------------------------------------------------
195
  # ENDPOINT 5 β€” DASHBOARD SUMMARY (SEMUA RINGKASAN)
196
  # ----------------------------------------------------
197
  @app.get("/dashboard_summary")
198
+ def dashboard_summary(reload: bool = Query(False, description="reload CSV cache")):
199
  if reload:
200
  load_data_cached.cache_clear()
 
201
  try:
202
  df = load_data_cached()
203
  except Exception as e:
204
+ raise HTTPException(status_code=500, detail=str(e))
205
 
206
  final = get_final_records(df)
207
  total_mhs = int(df["kode_mhs"].nunique())
 
211
 
212
  sks_per_mhs = final.groupby("kode_mhs")["sks"].sum()
213
  eligible = sks_per_mhs[sks_per_mhs > 110]
214
+ eligible_list = [{"kode_mhs": m, "total_sks": int(sks_per_mhs[m])} for m in eligible.index]
 
 
 
 
215
 
216
  # Hitung IPK
217
  final["total_bobot"] = final["sks"] * final["nilai_numerik"]
 
219
  total_bobot=("total_bobot", "sum"),
220
  total_sks=("sks", "sum")
221
  ).reset_index()
222
+ grp["ips"] = grp.apply(lambda r: (r["total_bobot"] / r["total_sks"]) if r["total_sks"] > 0 else 0.0, axis=1)
 
223
  ipk_series = grp.groupby("kode_mhs")["ips"].mean()
224
+ mean_ipk = float(round(ipk_series.mean(), 3)) if not ipk_series.empty else 0.0
225
 
226
  return {
227
  "total_mahasiswa": total_mhs,
 
236
  }
237
 
238
  # ----------------------------------------------------
239
+ # ADDITIONAL ENDPOINT β€” RATA-RATA SKS
240
+ # ----------------------------------------------------
241
+ @app.get("/rata_sks")
242
+ def rata_sks(reload: bool = Query(False, description="reload CSV cache")):
243
+ if reload:
244
+ load_data_cached.cache_clear()
245
+ try:
246
+ df = load_data_cached()
247
+ except Exception as e:
248
+ raise HTTPException(status_code=500, detail=str(e))
249
+
250
+ final = get_final_records(df)
251
+ sks_per_mhs = final.groupby("kode_mhs")["sks"].sum()
252
+ if sks_per_mhs.empty:
253
+ return {"rata_rata_sks": 0.0}
254
+ rata2 = float(round(sks_per_mhs.mean(), 2))
255
+ return {"rata_rata_sks": rata2}
256
+
257
+ # ----------------------------------------------------
258
+ # ADDITIONAL ENDPOINT β€” IPS TREND (per angkatan per semester)
259
+ # ----------------------------------------------------
260
+ @app.get("/ips_trend")
261
+ def ips_trend(reload: bool = Query(False, description="reload CSV cache")):
262
+ if reload:
263
+ load_data_cached.cache_clear()
264
+ try:
265
+ df = load_data_cached()
266
+ except Exception as e:
267
+ raise HTTPException(status_code=500, detail=str(e))
268
+
269
+ final = get_final_records(df)
270
+ final["total_bobot"] = final["sks"] * final["nilai_numerik"]
271
+
272
+ grp = final.groupby(["kode_mhs", "id_smt", "Tahun angkatan"]).agg(
273
+ total_bobot=("total_bobot", "sum"),
274
+ total_sks=("sks", "sum")
275
+ ).reset_index()
276
+
277
+ grp["ips"] = grp.apply(lambda r: (r["total_bobot"] / r["total_sks"]) if r["total_sks"] > 0 else 0.0, axis=1)
278
+
279
+ result = grp.groupby(["Tahun angkatan", "id_smt"])["ips"].mean().round(3).reset_index()
280
+
281
+ output = {}
282
+ for _, row in result.iterrows():
283
+ angkatan = str(int(row["Tahun angkatan"]))
284
+ semester = str(int(row["id_smt"]))
285
+ output.setdefault(angkatan, {})[semester] = float(row["ips"])
286
+ return output
287
+
288
+ # ----------------------------------------------------
289
+ # ADDITIONAL ENDPOINT β€” POPULASI (jumlah mahasiswa per angkatan)
290
+ # ----------------------------------------------------
291
+ @app.get("/populasi")
292
+ def populasi(reload: bool = Query(False, description="reload CSV cache")):
293
+ if reload:
294
+ load_data_cached.cache_clear()
295
+ try:
296
+ df = load_data_cached()
297
+ except Exception as e:
298
+ raise HTTPException(status_code=500, detail=str(e))
299
+
300
+ per_ang = df.groupby("Tahun angkatan")["kode_mhs"].nunique().sort_index().to_dict()
301
+ per_ang = {str(k): int(v) for k, v in per_ang.items()}
302
+ return {"populasi": per_ang}
303
+
304
+ # ----------------------------------------------------
305
+ # ENDPOINT β€” RELOAD CSV
306
  # ----------------------------------------------------
307
  @app.post("/reload_data")
308
  def reload_data():
 
310
  try:
311
  _ = load_data_cached()
312
  except Exception as e:
313
+ raise HTTPException(status_code=500, detail=f"Reload failed: {e}")
314
  return {"status": "reloaded"}