farwew commited on
Commit
4d8963e
·
verified ·
1 Parent(s): 9c19644

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +106 -92
app.py CHANGED
@@ -6,9 +6,10 @@ from typing import Dict, Any
6
 
7
  app = FastAPI(title="Dashboard Akademik API")
8
 
9
- # Path ke CSV: bisa override lewat env var CSV_PATH
10
  CSV_PATH = os.getenv("CSV_PATH", "generated_dummy_data.csv")
11
 
 
12
  MAP_NILAI = {
13
  "A": 4.0,
14
  "AB": 3.5,
@@ -19,6 +20,7 @@ MAP_NILAI = {
19
  "E": 0.0
20
  }
21
 
 
22
  def _ensure_columns(df: pd.DataFrame):
23
  required = {
24
  "kode_mhs", "nama_prodi", "id_smt", "kode_mk", "nama_mk",
@@ -29,190 +31,202 @@ def _ensure_columns(df: pd.DataFrame):
29
  if missing:
30
  raise ValueError(f"CSV missing required columns: {missing}")
31
 
 
32
  @lru_cache(maxsize=1)
33
  def load_data_cached() -> pd.DataFrame:
34
- """
35
- Load CSV into DataFrame and cache it for reuse.
36
- Jika ingin reload, panggil /reload_data endpoint (opsional).
37
- """
38
  if not os.path.exists(CSV_PATH):
39
- raise FileNotFoundError(f"CSV not found at path: {CSV_PATH}")
 
40
  df = pd.read_csv(CSV_PATH)
41
- # Normalisasi kolom whitespace
42
- df.columns = [c.strip() for c in df.columns]
43
  _ensure_columns(df)
44
- # Pastikan tipe
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
- # map nilai huruf -> numerik, jika kosong set NaN -> 0.0
48
- df["nilai_huruf"] = df["nilai_huruf"].astype(str).str.strip()
49
  df["nilai_numerik"] = df["nilai_huruf"].map(MAP_NILAI)
50
- # bila nilai_huruf tidak ada / invalid -> gunakan nilai_akhir jika ada (0-100 scale)
 
51
  def fallback_numeric(row):
52
  if pd.notna(row["nilai_numerik"]):
53
  return row["nilai_numerik"]
 
54
  try:
55
- v = float(row.get("nilai_akhir", None))
56
- # convert angka 0-100 ke skala 0-4 berdasarkan mapping ranges
57
- if v >= 86:
58
- return 4.0
59
- if v >= 76:
60
- return 3.5
61
- if v >= 66:
62
- return 3.0
63
- if v >= 61:
64
- return 2.5
65
- if v >= 56:
66
- return 2.0
67
- if v >= 41:
68
- return 1.0
69
  return 0.0
70
- except Exception:
71
  return 0.0
 
72
  df["nilai_numerik"] = df.apply(fallback_numeric, axis=1)
73
  return df
74
 
 
75
  def get_final_records(df: pd.DataFrame) -> pd.DataFrame:
76
- """
77
- Ambil record terakhir tiap (kode_mhs, kode_mk) berdasarkan id_smt.
78
- Ini memenuhi aturan: nilai terakhir berlaku, SKS dihitung sekali.
79
- """
80
- # sort by id_smt lalu ambil tail(1) per group
81
  df_sorted = df.sort_values(["kode_mhs", "kode_mk", "id_smt"])
82
- final = df_sorted.groupby(["kode_mhs", "kode_mk"], as_index=False).last()
83
- return final
84
 
 
 
 
85
  @app.get("/jumlah_mahasiswa")
86
- def jumlah_mahasiswa(reload: bool = Query(False, description="set true to reload CSV from disk")):
87
- """
88
- Mengembalikan total mahasiswa unik (kode_mhs).
89
- Tambah ?reload=true untuk load ulang CSV.
90
- """
91
  if reload:
92
  load_data_cached.cache_clear()
 
93
  try:
94
  df = load_data_cached()
95
  except Exception as e:
96
- raise HTTPException(status_code=500, detail=str(e))
 
97
  total = int(df["kode_mhs"].nunique())
98
  return {"total_mahasiswa": total}
99
 
 
 
 
100
  @app.get("/jumlah_per_angkatan")
101
- def jumlah_per_angkatan(reload: bool = Query(False, description="set true to reload CSV from disk")):
102
  if reload:
103
  load_data_cached.cache_clear()
 
104
  try:
105
  df = load_data_cached()
106
  except Exception as e:
107
- raise HTTPException(status_code=500, detail=str(e))
 
108
  per_ang = df.groupby("Tahun angkatan")["kode_mhs"].nunique().sort_index().to_dict()
109
- # convert keys to str (JSON-friendly)
110
  per_ang = {str(k): int(v) for k, v in per_ang.items()}
 
111
  return {"mahasiswa_per_angkatan": per_ang}
112
 
 
 
 
113
  @app.get("/eligible_ta")
114
- def eligible_ta(reload: bool = Query(False, description="set true to reload CSV from disk"),
115
- min_sks: int = Query(110, description="threshold SKS untuk eligible (default:110)")):
116
- """
117
- Mengembalikan daftar mahasiswa yang total SKS (menggunakan nilai terakhir tiap matkul) > min_sks.
118
- """
119
  if reload:
120
  load_data_cached.cache_clear()
 
121
  try:
122
  df = load_data_cached()
123
  except Exception as e:
124
- raise HTTPException(status_code=500, detail=str(e))
 
125
  final = get_final_records(df)
126
  sks_per_mhs = final.groupby("kode_mhs")["sks"].sum()
127
- eligible_series = sks_per_mhs[sks_per_mhs > min_sks]
128
- eligible_list = eligible_series.sort_values(ascending=False).index.to_list()
129
- # optional: juga sertakan total SKS per mahasiswa pada hasil
130
- eligible_info = [{"kode_mhs": m, "total_sks": int(sks_per_mhs.loc[m])} for m in eligible_list]
131
- return {"jumlah_eligible": len(eligible_list), "daftar": eligible_info}
132
 
 
 
 
 
 
 
 
 
 
 
 
 
133
  @app.get("/ipk_rata_rata")
134
- def ipk_rata_rata(reload: bool = Query(False, description="set true to reload CSV from disk")):
135
- """
136
- Menghitung IPK rata-rata seluruh mahasiswa:
137
- - IPS per semester = (Σ sks * nilai_numerik) / Σ sks (menggunakan nilai terakhir utk matkul yang diulang)
138
- - IPK mahasiswa = rata-rata IPS mahasiswa
139
- - IPK rata-rata = rata-rata IPK semua mahasiswa
140
- """
141
  if reload:
142
  load_data_cached.cache_clear()
 
143
  try:
144
  df = load_data_cached()
145
  except Exception as e:
146
- raise HTTPException(status_code=500, detail=str(e))
 
147
  final = get_final_records(df)
148
- # bobot per record
149
  final["total_bobot"] = final["sks"] * final["nilai_numerik"]
150
- # IPS per mahasiswa per semester
151
  grp = final.groupby(["kode_mhs", "id_smt"]).agg(
152
- total_bobot=pd.NamedAgg(column="total_bobot", aggfunc="sum"),
153
- total_sks=pd.NamedAgg(column="sks", aggfunc="sum")
154
  ).reset_index()
155
- # Hindari pembagian 0
156
- grp["ips"] = grp.apply(lambda r: (r["total_bobot"] / r["total_sks"]) if r["total_sks"] > 0 else 0.0, axis=1)
157
- # IPK = rata-rata ips tiap mahasiswa
 
 
 
158
  ipk_series = grp.groupby("kode_mhs")["ips"].mean()
159
- mean_ipk = float(round(ipk_series.mean(), 3)) if not ipk_series.empty else 0.0
160
- # optional: distribusi ipk (quartiles)
161
- q = ipk_series.quantile([0.25, 0.5, 0.75]).to_dict() if not ipk_series.empty else {}
162
  q = {str(k): float(v) for k, v in q.items()}
163
- return {"ipk_rata_rata": mean_ipk, "ipk_quartiles": q}
164
 
 
 
 
 
 
 
 
 
165
  @app.get("/dashboard_summary")
166
- def dashboard_summary(reload: bool = Query(False, description="set true to reload CSV from disk")):
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
  total_mhs = int(df["kode_mhs"].nunique())
176
- per_ang = df.groupby("Tahun angkatan")["kode_mhs"].nunique().sort_index().to_dict()
 
177
  per_ang = {str(k): int(v) for k, v in per_ang.items()}
178
 
179
  sks_per_mhs = final.groupby("kode_mhs")["sks"].sum()
180
- eligible_series = sks_per_mhs[sks_per_mhs > 110]
181
- eligible_list = [{"kode_mhs": m, "total_sks": int(sks_per_mhs.loc[m])} for m in eligible_series.sort_values(ascending=False).index]
 
 
 
 
182
 
183
- # IPK
184
  final["total_bobot"] = final["sks"] * final["nilai_numerik"]
185
  grp = final.groupby(["kode_mhs", "id_smt"]).agg(
186
- total_bobot=pd.NamedAgg(column="total_bobot", aggfunc="sum"),
187
- total_sks=pd.NamedAgg(column="sks", aggfunc="sum")
188
  ).reset_index()
189
- grp["ips"] = grp.apply(lambda r: (r["total_bobot"] / r["total_sks"]) if r["total_sks"] > 0 else 0.0, axis=1)
 
190
  ipk_series = grp.groupby("kode_mhs")["ips"].mean()
191
- mean_ipk = float(round(ipk_series.mean(), 3)) if not ipk_series.empty else 0.0
192
 
193
- summary: Dict[str, Any] = {
194
  "total_mahasiswa": total_mhs,
195
  "mahasiswa_per_angkatan": per_ang,
196
  "eligible_ta": {
197
- "jumlah": int(eligible_series.shape[0]),
198
  "daftar": eligible_list
199
  },
200
  "ipk": {
201
  "rata_rata_ipk": mean_ipk
202
  }
203
  }
204
- return summary
205
 
 
 
 
206
  @app.post("/reload_data")
207
  def reload_data():
208
- """
209
- Endpoint untuk menghapus cache dan reload CSV.
210
- (tidak perlu query param, panggil endpoint ini setelah CSV diganti)
211
- """
212
  load_data_cached.cache_clear()
213
  try:
214
- # force load to check file validity
215
  _ = load_data_cached()
216
  except Exception as e:
217
- raise HTTPException(status_code=500, detail=f"Reload failed: {e}")
218
  return {"status": "reloaded"}
 
6
 
7
  app = FastAPI(title="Dashboard Akademik API")
8
 
9
+ # Lokasi CSV dapat dioverride di HuggingFace: Settings Variables CSV_PATH
10
  CSV_PATH = os.getenv("CSV_PATH", "generated_dummy_data.csv")
11
 
12
+ # Konversi nilai huruf ke skala 4.0
13
  MAP_NILAI = {
14
  "A": 4.0,
15
  "AB": 3.5,
 
20
  "E": 0.0
21
  }
22
 
23
+ # Pastikan CSV memiliki kolom wajib
24
  def _ensure_columns(df: pd.DataFrame):
25
  required = {
26
  "kode_mhs", "nama_prodi", "id_smt", "kode_mk", "nama_mk",
 
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
61
+ if v >= 61: return 2.5
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)
72
  def get_final_records(df: pd.DataFrame) -> pd.DataFrame:
 
 
 
 
 
73
  df_sorted = df.sort_values(["kode_mhs", "kode_mk", "id_smt"])
74
+ return df_sorted.groupby(["kode_mhs", "kode_mk"], as_index=False).last()
 
75
 
76
+ # ----------------------------------------------------
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}
91
 
92
+ # ----------------------------------------------------
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"]
150
+
151
  grp = final.groupby(["kode_mhs", "id_smt"]).agg(
152
+ total_bobot=("total_bobot", "sum"),
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())
187
+
188
+ per_ang = df.groupby("Tahun angkatan")["kode_mhs"].nunique().to_dict()
189
  per_ang = {str(k): int(v) for k, v in per_ang.items()}
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"]
201
  grp = final.groupby(["kode_mhs", "id_smt"]).agg(
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,
212
  "mahasiswa_per_angkatan": per_ang,
213
  "eligible_ta": {
214
+ "jumlah": len(eligible_list),
215
  "daftar": eligible_list
216
  },
217
  "ipk": {
218
  "rata_rata_ipk": mean_ipk
219
  }
220
  }
 
221
 
222
+ # ----------------------------------------------------
223
+ # ENDPOINT 6 — RELOAD CSV
224
+ # ----------------------------------------------------
225
  @app.post("/reload_data")
226
  def reload_data():
 
 
 
 
227
  load_data_cached.cache_clear()
228
  try:
 
229
  _ = load_data_cached()
230
  except Exception as e:
231
+ raise HTTPException(500, f"Reload failed: {e}")
232
  return {"status": "reloaded"}