Gabungkan aplikasi RPS & Portofolio OBE (dashboard jadi bagian aplikasi)
Dashboard pemetaan statis kini dilayani aplikasi di /dashboard (app/static/dashboard.html). Fitur lengkap Tahap 1-4 + dasbor monitoring, menu global, filter semester, input nilai langsung, penggabungan nilai team teaching, tabel bisa urut. Dockerfile berubah dari nginx statis ke uvicorn/FastAPI: healthcheck, proxy-headers, volume /data (SQLite). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+226
@@ -0,0 +1,226 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Mesin pengukuran ketercapaian CPL/CPMK (inti OBE).
|
||||
|
||||
Kebijakan (RANCANGAN.md §7, disepakati 13 Jul 2026):
|
||||
- Nilai dicatat per mahasiswa per Sub-CPMK (mengikuti format contoh portofolio);
|
||||
mode 'agregat' (rata-rata kelas per Sub-CPMK) diperbolehkan tapi ditandai.
|
||||
- Roll-up dalam MK : Sub-CPMK → CPMK → CPL, rata-rata sederhana (bagi rata).
|
||||
- Roll-up prodi : CPMK prodi = rata-rata lintas MK pengukurnya;
|
||||
CPL = rata-rata CPMK pendukungnya. Opsi tertimbang SKS
|
||||
tersedia via pengaturan 'metode_antar_mk' = 'sks'
|
||||
(butuh data SKS; tanpa data SKS jatuh ke bagi rata).
|
||||
- % kelulusan = persentase mahasiswa dengan skor ≥ ambang (hanya mode
|
||||
per-mahasiswa; mode agregat tidak menghasilkan % kelulusan).
|
||||
"""
|
||||
import statistics
|
||||
|
||||
from . import db
|
||||
|
||||
|
||||
def _rata(xs):
|
||||
xs = [x for x in xs if x is not None]
|
||||
return sum(xs) / len(xs) if xs else None
|
||||
|
||||
|
||||
def parse_skala(teks: str) -> list[dict]:
|
||||
"""'A+|91|100|4.00' per baris → [{'huruf','lo','hi','angka'}] terurut turun."""
|
||||
out = []
|
||||
for line in (teks or "").splitlines():
|
||||
bag = [b.strip() for b in line.split("|")]
|
||||
if len(bag) >= 3:
|
||||
try:
|
||||
out.append({
|
||||
"huruf": bag[0], "lo": float(bag[1]), "hi": float(bag[2]),
|
||||
"angka": float(bag[3]) if len(bag) > 3 else None,
|
||||
})
|
||||
except ValueError:
|
||||
continue
|
||||
return sorted(out, key=lambda s: -s["lo"])
|
||||
|
||||
|
||||
def huruf_mutu(skor: float, skala: list[dict]) -> str:
|
||||
for s in skala:
|
||||
if s["lo"] <= skor <= s["hi"]:
|
||||
return s["huruf"]
|
||||
return "?"
|
||||
|
||||
|
||||
def skor_sub(nilai_data: dict, ambang: float) -> dict:
|
||||
"""Skor per Sub-CPMK dari data nilai tersimpan.
|
||||
|
||||
nilai_data = {"mode": "per_mahasiswa"|"agregat",
|
||||
"mahasiswa": [{"npm","nama","skor":{sub: nilai}}] |
|
||||
"agregat": {sub: rata}}
|
||||
→ {sub: {"rata": x, "n": jumlah_mhs, "lulus_pct": y|None}}
|
||||
"""
|
||||
out = {}
|
||||
if nilai_data.get("mode") == "per_mahasiswa":
|
||||
per_sub: dict[str, list[float]] = {}
|
||||
for m in nilai_data.get("mahasiswa", []):
|
||||
for sub, v in (m.get("skor") or {}).items():
|
||||
if v is not None:
|
||||
per_sub.setdefault(sub, []).append(float(v))
|
||||
for sub, xs in per_sub.items():
|
||||
out[sub] = {
|
||||
"rata": _rata(xs),
|
||||
"n": len(xs),
|
||||
"lulus_pct": 100.0 * sum(1 for x in xs if x >= ambang) / len(xs),
|
||||
}
|
||||
else:
|
||||
for sub, v in (nilai_data.get("agregat") or {}).items():
|
||||
if v in (None, ""):
|
||||
continue
|
||||
out[sub] = {"rata": float(v), "n": None, "lulus_pct": None}
|
||||
return out
|
||||
|
||||
|
||||
def bobot_sub_dari_rps(rps_data: dict | None) -> dict[str, float]:
|
||||
"""Bobot (%) tiap Sub-CPMK = jumlah porsi bagi-rata dari komponen RPS (kisi-kisi)."""
|
||||
bobot: dict[str, float] = {}
|
||||
if not rps_data:
|
||||
return bobot
|
||||
for k in rps_data.get("komponen", []):
|
||||
subs = k.get("subs", [])
|
||||
try:
|
||||
b = float(k.get("bobot") or 0)
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
for s in subs:
|
||||
bobot[s] = bobot.get(s, 0.0) + b / len(subs)
|
||||
return bobot
|
||||
|
||||
|
||||
def nilai_akhir_mahasiswa(nilai_data: dict, bobot_sub: dict[str, float]) -> list[dict]:
|
||||
"""Nilai akhir tertimbang bobot Sub-CPMK per mahasiswa (mode per_mahasiswa)."""
|
||||
out = []
|
||||
for m in nilai_data.get("mahasiswa", []):
|
||||
skor = m.get("skor") or {}
|
||||
pasangan = [(bobot_sub.get(s), v) for s, v in skor.items() if v is not None]
|
||||
berbobot = [(b, v) for b, v in pasangan if b]
|
||||
if berbobot:
|
||||
total_b = sum(b for b, _ in berbobot)
|
||||
akhir = sum(b * v for b, v in berbobot) / total_b
|
||||
else: # RPS belum mengisi bobot → rata-rata sederhana
|
||||
akhir = _rata([v for _, v in pasangan])
|
||||
if akhir is not None:
|
||||
out.append({"npm": m.get("npm", ""), "nama": m.get("nama", ""),
|
||||
"akhir": akhir})
|
||||
return out
|
||||
|
||||
|
||||
def statistik_akhir(akhir: list[dict], skala: list[dict]) -> dict | None:
|
||||
xs = [a["akhir"] for a in akhir]
|
||||
if not xs:
|
||||
return None
|
||||
frek = {s["huruf"]: 0 for s in skala}
|
||||
for x in xs:
|
||||
h = huruf_mutu(x, skala)
|
||||
frek[h] = frek.get(h, 0) + 1
|
||||
return {
|
||||
"n": len(xs),
|
||||
"mean": statistics.mean(xs),
|
||||
"median": statistics.median(xs),
|
||||
"stdev": statistics.stdev(xs) if len(xs) > 1 else 0.0,
|
||||
"min": min(xs),
|
||||
"max": max(xs),
|
||||
"range": max(xs) - min(xs),
|
||||
"frekuensi": frek,
|
||||
}
|
||||
|
||||
|
||||
def rollup_mk(peta: dict, nilai_data: dict, ambang: float) -> dict:
|
||||
"""Ketercapaian satu MK: per Sub-CPMK → CPMK → CPL (bagi rata)."""
|
||||
sub = skor_sub(nilai_data, ambang)
|
||||
|
||||
cpmk: dict[str, dict] = {}
|
||||
for c in peta["cpmks"]:
|
||||
skor = _rata([sub[s["kode"]]["rata"] for s in c["subs"] if s["kode"] in sub])
|
||||
n_ternilai = sum(1 for s in c["subs"] if s["kode"] in sub)
|
||||
cpmk[c["kode"]] = {"skor": skor, "cpl": c["cpl"],
|
||||
"n_sub": len(c["subs"]), "n_ternilai": n_ternilai}
|
||||
|
||||
cpl: dict[str, dict] = {}
|
||||
for cp in peta["cpls"]:
|
||||
skor = _rata([v["skor"] for v in cpmk.values()
|
||||
if cp["kode"] in v["cpl"] and v["skor"] is not None])
|
||||
cpl[cp["kode"]] = {"skor": skor}
|
||||
|
||||
return {"sub": sub, "cpmk": cpmk, "cpl": cpl,
|
||||
"mode": nilai_data.get("mode", "agregat")}
|
||||
|
||||
|
||||
def rollup_prodi(tahun: str | None = None) -> dict:
|
||||
"""Ketercapaian tingkat prodi + kelengkapan bukti per CPL/CPMK."""
|
||||
tahun = tahun or db.tahun_aktif()
|
||||
pengaturan = db.ambil_pengaturan()
|
||||
ambang = float(pengaturan["ambang"])
|
||||
pakai_sks = pengaturan.get("metode_antar_mk") == "sks"
|
||||
sks = db.sks_mk() if pakai_sks else {}
|
||||
k = db.kurikulum()
|
||||
semua = db.semua_nilai(tahun)
|
||||
|
||||
def rata_antar_mk(entri):
|
||||
"""Skor CPMK lintas MK: bagi rata, atau tertimbang SKS bila diatur."""
|
||||
if not entri:
|
||||
return None
|
||||
if pakai_sks and sks:
|
||||
bobot = [(float(sks.get(mk, {}).get("sks") or 0), s) for mk, s, _ in entri]
|
||||
total = sum(b for b, _ in bobot if b)
|
||||
if total:
|
||||
return sum(b * s for b, s in bobot if b) / total
|
||||
return _rata([s for _, s, _ in entri])
|
||||
|
||||
# skor CPMK per MK
|
||||
per_cpmk: dict[str, list[tuple[str, float, str]]] = {} # key cpmk → [(mk, skor, mode)]
|
||||
for mk_kode, nilai_data in semua.items():
|
||||
peta = db.peta_mk(mk_kode)
|
||||
if not peta:
|
||||
continue
|
||||
hasil = rollup_mk(peta, nilai_data, ambang)
|
||||
for kode_cpmk, v in hasil["cpmk"].items():
|
||||
if v["skor"] is not None:
|
||||
per_cpmk.setdefault(kode_cpmk, []).append(
|
||||
(mk_kode, v["skor"], hasil["mode"])
|
||||
)
|
||||
|
||||
# CPMK prodi = bagi rata lintas MK pengukur; kelengkapan = pengukur/pemikul
|
||||
cpmk_prodi: dict[str, dict] = {}
|
||||
for ckey, info in k["cpmk"].items():
|
||||
kode = info.get("kode", ckey)
|
||||
pemikul = info.get("mk", [])
|
||||
entri = per_cpmk.get(kode, [])
|
||||
cpmk_prodi[kode] = {
|
||||
"kode": kode,
|
||||
"cpl": info.get("cpl", []),
|
||||
"skor": rata_antar_mk(entri),
|
||||
"n_pemikul": len(pemikul),
|
||||
"n_setor": len(entri),
|
||||
"ada_agregat": any(m == "agregat" for _, _, m in entri),
|
||||
"mk_terlemah": min(entri, key=lambda e: e[1])[:2] if entri else None,
|
||||
}
|
||||
|
||||
# CPL = bagi rata CPMK pendukung
|
||||
cpl_prodi: dict[str, dict] = {}
|
||||
for kode_cpl, info in k["cpl"].items():
|
||||
pendukung = [c for c in cpmk_prodi.values() if kode_cpl in c["cpl"]]
|
||||
ternilai = [c for c in pendukung if c["skor"] is not None]
|
||||
skor = _rata([c["skor"] for c in ternilai])
|
||||
terlemah = min(ternilai, key=lambda c: c["skor"]) if ternilai else None
|
||||
cpl_prodi[kode_cpl] = {
|
||||
"kode": kode_cpl,
|
||||
"desk": info["desk"],
|
||||
"skor": skor,
|
||||
"tercapai": (skor is not None and skor >= ambang),
|
||||
"n_cpmk": len(pendukung),
|
||||
"n_cpmk_ternilai": len(ternilai),
|
||||
"n_mk_pemikul": sum(c["n_pemikul"] for c in pendukung),
|
||||
"n_mk_setor": sum(c["n_setor"] for c in pendukung),
|
||||
"ada_agregat": any(c["ada_agregat"] for c in ternilai),
|
||||
"cpmk_terlemah": terlemah["kode"] if terlemah else None,
|
||||
"mk_terlemah": (terlemah["mk_terlemah"] if terlemah else None),
|
||||
}
|
||||
|
||||
return {"cpl": cpl_prodi, "cpmk": cpmk_prodi, "ambang": ambang,
|
||||
"n_mk_setor": len(semua), "tahun": tahun,
|
||||
"metode": "sks" if (pakai_sks and sks) else "rata"}
|
||||
Reference in New Issue
Block a user