Tambah Analisis Proses Asesmen, Kelompok Capaian, riwayat nilai terstruktur, dan perbaikan bug CPL

- Portofolio: section "Analisis Proses Asesmen" (dropdown tertutup default) —
  distribusi huruf mutu, kelompok capaian (mean ± SD), Sub-CPMK terlemah/terkuat,
  kesenjangan rencana vs realisasi, mahasiswa berisiko, tren lintas tahun ajaran
- Portofolio: Nomor Dokumen & Tanggal Terbit terisi otomatis; placeholder contoh
  di Realisasi Pembelajaran dan Kendala & Tindak Lanjut; tombol "Isi Otomatis"
  merangkai draf Simpulan dari analisis + realisasi + kendala
- Nilai: tabel Kelompok Capaian (Atas/Tengah/Bawah, shading warna) dipindah ke
  dropdown "Detail Tambahan" bersama Skala Poin/Rincian Bobot/Hasil Ketercapaian;
  tabel utama tak lagi ada scroll vertikal internal
- Nilai: riwayat pengisian dicatat terstruktur (waktu, dosen, aksi, komponen),
  tanpa batas 8 entri, dengan ringkasan per komponen + dropdown Log Lengkap —
  mendukung akuntabilitas team teaching
- Fix bug data: seed kurikulum menyimpan 'cpl' CPMK sebagai string tunggal,
  bukan list, sehingga pecah per huruf saat di-join() di tabel Hasil
  Ketercapaian/Rekap (mis. "CPL-03" -> "C, P, L, -, 0, 3"); dinormalisasi di
  db.py (runtime) dan tools/extract_seed.py (agar re-seed berikutnya benar)
- Hover tooltip deskripsi lengkap di kode Sub-CPMK/CPMK/CPL (halaman Nilai
  dan Rekap CPL/CPMK)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Power BI Dev
2026-08-08 00:09:57 +07:00
parent af369883f3
commit 5d7ab16733
8 changed files with 491 additions and 37 deletions
+140
View File
@@ -205,6 +205,146 @@ def rollup_mk(peta: dict, nilai_data: dict, ambang: float) -> dict:
"mode": nilai_data.get("mode", "agregat")}
def sub_terurut(peta: dict, rollup_sub: dict) -> list[dict]:
"""Sub-CPMK ternilai, terurut naik dari yang terlemah — bahan analisis
proses asesmen di Portofolio (mana yang paling perlu perhatian)."""
out = []
for s in peta["subs"]:
info = rollup_sub.get(s["kode"])
if info and info["rata"] is not None:
out.append({"kode": s["kode"], "uraian": s["uraian"],
"rata": info["rata"], "lulus_pct": info["lulus_pct"]})
return sorted(out, key=lambda s: s["rata"])
def kesenjangan_rencana_realisasi(bobot_sub: dict[str, float],
rollup_sub: dict) -> list[str]:
"""Sub-CPMK yang diberi bobot penilaian di RPS tapi belum ada nilai
masuk sama sekali — kesenjangan antara rencana dan realisasi asesmen."""
return sorted(
s for s in bobot_sub
if bobot_sub[s] and (s not in rollup_sub or rollup_sub[s]["rata"] is None)
)
def mahasiswa_berisiko(nilai_data: dict, akhir: list[dict], ambang: float,
min_sub_dibawah: int = 2) -> list[dict]:
"""Mahasiswa dengan >= min_sub_dibawah Sub-CPMK di bawah ambang, atau
nilai akhir tertimbangnya sendiri di bawah ambang (early-warning list)."""
if nilai_data.get("mode") != "per_mahasiswa":
return []
akhir_by_nim = {a["nim"]: a["akhir"] for a in akhir}
out = []
for m in nilai_data.get("mahasiswa", []):
skor = {s: v for s, v in (m.get("skor") or {}).items() if v is not None}
if not skor:
continue
n_dibawah = sum(1 for v in skor.values() if v < ambang)
nim = m.get("nim", "")
nilai_akhir = akhir_by_nim.get(nim)
if n_dibawah >= min_sub_dibawah or (nilai_akhir is not None and nilai_akhir < ambang):
out.append({"nim": nim, "nama": m.get("nama", ""),
"n_dibawah": n_dibawah, "total_sub": len(skor),
"akhir": nilai_akhir})
return sorted(out, key=lambda m: (-m["n_dibawah"], m["akhir"] if m["akhir"] is not None else 999))
def tren_mk(kode: str, daftar_tahun: list[str], ambang: float) -> list[dict]:
"""Rata-rata nilai akhir & % kelulusan MK ini per tahun ajaran yang
berdata — untuk melihat apakah capaian membaik atau tidak antar tahun."""
out = []
for t in daftar_tahun:
nilai_t = db.ambil_dok("nilai", kode, t)
if not nilai_t:
continue
nilai_data_t = nilai_t["data"]
rps_t = db.ambil_rps(kode, t)
bobot_t = bobot_sub_dari_rps(rps_t["data"] if rps_t else None)
if nilai_data_t.get("mode") == "per_mahasiswa":
akhir_t = nilai_akhir_mahasiswa(nilai_data_t, bobot_t)
if akhir_t:
xs = [a["akhir"] for a in akhir_t]
out.append({"tahun": t, "mean": _rata(xs),
"lulus_pct": 100.0 * sum(1 for x in xs if x >= ambang) / len(xs),
"n": len(xs)})
continue
sub_t = skor_sub(nilai_data_t, ambang)
nilai_ada = [v["rata"] for v in sub_t.values() if v["rata"] is not None]
if nilai_ada:
out.append({"tahun": t, "mean": _rata(nilai_ada), "lulus_pct": None, "n": None})
return out
def kelompok_capaian(akhir: list[dict], statistik: dict | None) -> dict | None:
"""Kelompokkan mahasiswa jadi Atas/Tengah/Bawah berdasarkan mean ± 1 SD
(acuan norma, lazim dipakai dalam evaluasi pendidikan) — pelengkap
'mahasiswa berisiko' yang acuannya ambang kelulusan tetap, bukan sebaran
kelas ini."""
if not statistik or not akhir:
return None
batas_bawah = statistik["mean"] - statistik["stdev"]
batas_atas = statistik["mean"] + statistik["stdev"]
atas, tengah, bawah = [], [], []
for a in akhir:
if a["akhir"] > batas_atas:
atas.append(a)
elif a["akhir"] < batas_bawah:
bawah.append(a)
else:
tengah.append(a)
return {
"batas_bawah": batas_bawah, "batas_atas": batas_atas,
"atas": sorted(atas, key=lambda a: -a["akhir"]),
"tengah": sorted(tengah, key=lambda a: -a["akhir"]),
"bawah": sorted(bawah, key=lambda a: -a["akhir"]),
}
def saran_simpulan(akhir: list[dict], statistik: dict | None, sub_urut: list[dict],
kesenjangan: list[str], berisiko: list[dict],
tren: list[dict], ambang: float) -> str:
"""Rangkai draf narasi Simpulan Hasil Evaluasi dari hasil Analisis Proses
Asesmen — dipakai sebagai placeholder awal, dosen tetap yang menulis versi
finalnya (termasuk simpulan ketercapaian CPL/CPMK dan tindak lanjut)."""
bagian = []
if statistik:
lulus_pct = 100.0 * sum(1 for a in akhir if a["akhir"] >= ambang) / len(akhir)
bagian.append(
f"Rata-rata nilai akhir {statistik['mean']:.1f} (median {statistik['median']:.1f}) "
f"dari {statistik['n']} mahasiswa; {lulus_pct:.0f}% mencapai ambang {ambang:.0f}."
)
if sub_urut:
terlemah, terkuat = sub_urut[0], sub_urut[-1]
if terlemah["kode"] != terkuat["kode"]:
bagian.append(
f"Capaian terendah pada {terlemah['kode']} (skor {terlemah['rata']:.1f}), "
f"tertinggi pada {terkuat['kode']} (skor {terkuat['rata']:.1f})."
)
else:
bagian.append(f"Satu-satunya Sub-CPMK ternilai: {terlemah['kode']} (skor {terlemah['rata']:.1f}).")
if kesenjangan:
bagian.append(
f"{len(kesenjangan)} Sub-CPMK direncanakan di RPS namun belum ternilai: "
f"{', '.join(kesenjangan)}."
)
if berisiko:
bagian.append(f"{len(berisiko)} mahasiswa teridentifikasi berisiko (nilai rendah di beberapa Sub-CPMK).")
if len(tren) > 1:
sekarang, sebelum = tren[-1], tren[-2]
if sekarang["mean"] is not None and sebelum["mean"] is not None:
selisih = sekarang["mean"] - sebelum["mean"]
arah = "naik" if selisih > 0 else ("turun" if selisih < 0 else "stabil")
bagian.append(
f"Dibanding {sebelum['tahun']} ({sebelum['mean']:.1f}), rata-rata {arah} "
f"{abs(selisih):.1f} poin di {sekarang['tahun']}."
)
bagian.append(
"Lengkapi dengan simpulan ketercapaian CPL/CPMK terkait dan rencana tindak lanjut "
"untuk siklus berikutnya."
)
return " ".join(bagian)
def rollup_prodi(tahun: str | None = None) -> dict:
"""Ketercapaian tingkat prodi + kelengkapan bukti per CPL/CPMK."""
tahun = tahun or db.tahun_aktif()