farwew commited on
Commit
5f0dd74
·
verified ·
1 Parent(s): fac09be

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +218 -0
app.py ADDED
@@ -0,0 +1,218 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, HTTPException, Query
2
+ from functools import lru_cache
3
+ import pandas as pd
4
+ import os
5
+ 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,
15
+ "B": 3.0,
16
+ "BC": 2.5,
17
+ "C": 2.0,
18
+ "D": 1.0,
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",
25
+ "RMK", "sks", "nilai_akhir", "nilai_huruf", "Tahun angkatan",
26
+ "Semester_sekarang", "Deskripsi Matkul"
27
+ }
28
+ missing = required - set(df.columns)
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"}